DEV Community

Sam Yang
Sam Yang

Posted on

The Inferred Stack Is a Hypothesis: A Myth-Busting FAQ

The opening is a reconstructed incident pattern, not a claim about one company outage.

On a quiet lab bench, a coding agent received a one-line task: add a health endpoint and keep the existing tests green. The repository was a small Go service, except a leftover package.json from an abandoned frontend spike still sat in the root. Before any human reviewed a diff, the agent inferred Node, ran an install, and rewrote the README around Express. Nothing in the prompt had named JavaScript; the files had simply outvoted the Go module in the model's prior.

Teams keep describing that moment as a model failure, which parks the defect in the wrong layer of the stack. The model completed a plausible story from noisy evidence, then the tool loop treated that story as a signed specification. Cheap generation makes the wrong story cheap to continue, so a package manager can land files before review even opens. The useful question is not whether agents understand repositories in some conversational sense. The useful question is which inferred facts were allowed to become commands.

This FAQ collects five claims that regularly show up in agent logs, standups, and review threads. Each claim sounds reasonable after a green demo, especially when the model explains itself fluently. Each claim collapses once teams record assumptions as structured data instead of leftover conversational vibes. The corrected mental model is simple enough to test: observations are not pins, and pins are not optional before mutation.

Myth 1: it scanned the repo, so it knows the stack

Scanning is not inventory, and inventory is not a decision about the runtime you actually ship. A recursive listing returns paths; it does not return a conflict-aware verdict about compilers, lockfiles, or the service entrypoint. Mixed trees are ordinary in long-lived services during 2026, with generated clients, vendored examples, abandoned lockfiles, and docs that still advertise the last framework. An agent that concatenates paths into a prompt will overweight famous filenames, because those tokens are cheap evidence rather than ranked proof.

The corrected model is narrower and slightly less flattering to the demo. Detected files are observations, similar to footprints around a building rather than a signed floor plan. The stack is a hypothesis that must be pinned before the first mutating tool call. If go.mod and package.json both exist, the session is in conflict, not in Node and not in Go, until a policy file says otherwise.

The lab snippet below is unlabeled production magic; it is a deterministic inventory you can run before any agent is allowed to touch a shell.

# detect_manifests.py — lab example, unexecuted until you run it on a copy of a repo
from __future__ import annotations

import json
from pathlib import Path

MARKERS = {
    "go.mod": "go",
    "package.json": "node",
    "pnpm-lock.yaml": "node",
    "pyproject.toml": "python",
    "Cargo.toml": "rust",
    "Gemfile": "ruby",
}

def detect(root: Path) -> dict:
    hits = []
    for path in root.rglob("*"):
        if path.name in MARKERS and ".git" not in path.parts:
            hits.append({"path": str(path.relative_to(root)), "stack": MARKERS[path.name]})
    stacks = sorted({item["stack"] for item in hits})
    return {
        "observations": hits,
        "candidate_stacks": stacks,
        "conflict": len(stacks) > 1,
        "status": "unpinned",
    }

if __name__ == "__main__":
    payload = detect(Path(".").resolve())
    print(json.dumps(payload, indent=2))
Enter fullscreen mode Exit fullscreen mode
python3 detect_manifests.py > observations.json
Enter fullscreen mode Exit fullscreen mode

If that JSON lists more than one candidate stack, the session does not have a stack yet. It has a rumor with file paths attached, which is a different kind of object.

Myth 2: a longer prompt will cancel a bad prior

Adding README excerpts, a directory tree, and a paragraph of "do not use npm" feels like control, and it often changes nothing material. Priors in tool-using agents are not only linguistic; they are procedural once a shell is in the loop. Package managers, test runners, and formatters become working memory because they return tokens the model can continue from. A prohibition in the system prompt is a suggestion with polite punctuation. A blocked binary is a constraint that fails closed.

Session traces are boring in a useful way when you stop reading the model's self-report as evidence. The same agent that promised to edit only Go files will still run npm test if that binary is on the path and a leftover snippet mentioned Jest. Prompt volume is not a lock, even when the prose is confident and the token count looks expensive. An allowlist of interpreters, package managers, and write roots is a lock, and unlike a paragraph of instructions it can be unit tested.

Think of the prompt as a hallway sign and the allowlist as a locked door. Signs help people who already intended to walk the right way. Doors help everyone else, including a model that is locally certain about the wrong building.

Myth 3: a free-tier model is too weak to damage a real repo

Capacity and permission are different axes, and teams still collapse them into one reassuring feeling. A smaller model can misread a manifest and still invoke a very real package manager on a very real working tree. The damage function is the tool adapter, the working directory, and the missing budget on mutating calls. Weaker reasoning increases the rate of wrong inferences about language and layout. It does not reduce the blast radius of rm, chmod, a lockfile rewrite, or a second service scaffolded beside the one you asked to patch.

Treat free-tier access as a reason to tighten the harness, not as a reason to skip the harness after a cheap demo. If the model is more error-prone, the ledger and the allowlist do more work per turn, not less. That is closer to how you would treat an intern with production credentials than how marketing copy talks about "lightweight" assistants.

Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode is an open-source coding-agent project with operator-supplied free model access and a free server option, including the ten-million-token free-tier allotment discussed on this account, which is enough room to rehearse an assumption ledger against real tool traces. If you need a throwaway server to run the inventory and interceptor below without buying a GPU hour, that free server option is one honest place to try the workflow.

Myth 4: if the tests passed, the change stayed in scope

Agents optimize for the visible green signal because that is usually the only success predicate the prompt bothered to celebrate. Tests that already existed may not cover an Express server the agent added beside a Go binary, and they will not complain about a README that now describes the wrong runtime. Coverage of the original contract can remain green while the tree contains a second lockfile, a second toolchain, and a commit message that sounds like the ticket. Passing tests are evidence about assertions you already wrote. They are not evidence about assertions the agent declined to write.

The corrected model records a scope diff the way a change-control board records materials, not vibes: paths touched, commands run, and assumptions consumed. If the health-endpoint task rewrites package-lock.json in a Go service, the session failed the contract even if go test ./... is green. Green is a local instrument reading. Scope is a comparison against the pinned hypothesis.

A tiny check that belongs in the same lab folder looks like this.

# test_scope_diff.py — lab example
from pathlib import Path

ALLOWED_PREFIXES = ("cmd/", "internal/", "go.mod", "go.sum")

def test_no_foreign_lockfiles(tmp_path_factory=None):
    touched = Path("touched_paths.txt").read_text(encoding="utf-8").splitlines()
    foreign = [p for p in touched if not p.startswith(ALLOWED_PREFIXES) and p.strip()]
    assert foreign == [], f"out-of-scope paths: {foreign}"
Enter fullscreen mode Exit fullscreen mode
git diff --name-only HEAD > touched_paths.txt
python3 -m pytest test_scope_diff.py -q
Enter fullscreen mode Exit fullscreen mode

The test is deliberately unintelligent. Intelligence is what created the extra lockfile. The test only asks whether the file tree still matches the pin.

Myth 5: open-source agents ask before they write, because the code is inspectable

Inspectability of an orchestrator is not a runtime policy, any more than a published engine schematic is a speed limit. Default tool loops in many coding agents treat file writes as ordinary steps, the way compilers treat object files: expected, frequent, and unremarkable. Reading the source of an agent framework tells you that you could insert a confirmation gate before apply_patch. It does not tell you that the gate is enabled in the profile you actually launched at 17:40 on a Friday.

Operators still ship with auto-apply enabled because demos look faster that way, and free-tier sessions inherit that default unless someone changes it on purpose. The corrected model is an explicit state machine with four named states: propose, record, allow, apply. Anything else is improvisation with a git checkout -- . story attached, which is not a control system. Open source makes the state machine forkable. It does not make the default conservative.

Artifact: pin the rumor before the shell

The original artifact for this FAQ is an assumption ledger plus a pre-tool interceptor. The ledger is a JSON document that starts as observations and becomes a pin only after a human, or a checked-in policy, fills the required fields. The interceptor refuses mutating prefixes until status equals pinned and the command matches the pinned prefixes. This is a lab harness, not a sandbox proof against hostile inputs.

{
  "status": "pinned",
  "stack": "go",
  "evidence": ["go.mod", "cmd/server/main.go"],
  "rejected_observations": ["package.json"],
  "write_roots": ["cmd/", "internal/"],
  "allowed_command_prefixes": ["go test", "go build", "gofmt"],
  "task": "add a health endpoint without introducing a second runtime"
}
Enter fullscreen mode Exit fullscreen mode
# intercept.py — lab example: fail closed on unpinned or out-of-stack commands
from __future__ import annotations

import json
import sys
from pathlib import Path

MUTATING_PREFIXES = (
    "npm ", "npx ", "pnpm ", "yarn ", "pip ", "poetry ",
    "go get ", "cargo ", "rm ", "mv ", "chmod ",
    "git commit", "git push",
)

def load_ledger(path: Path) -> dict:
    data = json.loads(path.read_text(encoding="utf-8"))
    if data.get("status") != "pinned":
        raise SystemExit("assumption ledger is not pinned; refusing tools")
    return data

def main() -> None:
    ledger = load_ledger(Path("assumption_ledger.json"))
    command = sys.argv[1] if len(sys.argv) > 1 else ""
    if not command:
        raise SystemExit("usage: intercept.py '<command>'")
    stripped = command.strip()
    mutating = any(stripped.startswith(prefix) for prefix in MUTATING_PREFIXES)
    allowed = any(
        stripped.startswith(prefix)
        for prefix in ledger.get("allowed_command_prefixes", [])
    )
    if mutating and not allowed:
        print(json.dumps({"ok": False, "reason": "outside pinned stack", "command": command}))
        raise SystemExit(2)
    print(json.dumps({"ok": True, "stack": ledger.get("stack"), "command": command}))

if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode
# test_assumption_ledger.py — lab example
import json
import subprocess
import sys
from pathlib import Path

def test_unpinned_ledger_refuses(tmp_path):
    ledger = tmp_path / "assumption_ledger.json"
    ledger.write_text(json.dumps({"status": "unpinned"}), encoding="utf-8")
    intercept = Path("intercept.py").resolve()
    result = subprocess.run(
        [sys.executable, str(intercept), "npm install"],
        cwd=tmp_path,
        capture_output=True,
    )
    assert result.returncode != 0

def test_pinned_go_stack_blocks_npm(tmp_path):
    ledger = {
        "status": "pinned",
        "stack": "go",
        "allowed_command_prefixes": ["go test", "go build"],
    }
    (tmp_path / "assumption_ledger.json").write_text(json.dumps(ledger), encoding="utf-8")
    intercept = Path("intercept.py").resolve()
    blocked = subprocess.run(
        [sys.executable, str(intercept), "npm install"],
        cwd=tmp_path,
        capture_output=True,
    )
    allowed = subprocess.run(
        [sys.executable, str(intercept), "go test ./..."],
        cwd=tmp_path,
        capture_output=True,
    )
    assert blocked.returncode == 2
    assert allowed.returncode == 0
Enter fullscreen mode Exit fullscreen mode
cp observations.json assumption_ledger.json
# pin stack, write_roots, and allowed_command_prefixes by hand before tools run
python3 intercept.py "npm install"; echo "exit $?"
python3 intercept.py "go test ./..."; echo "exit $?"
python3 -m pytest test_assumption_ledger.py -q
Enter fullscreen mode Exit fullscreen mode

A compact decision table belongs next to the ledger, because people still try to negotiate with filenames after the interceptor has already answered.

Observation Illegal inference Required pin Mutating tools
go.mod plus leftover package.json "this is a Node service" stack=go, reject Node markers go test, go build only
Only pyproject.toml at repo root "any Python tool is in scope" write roots limited to the app package pytest on those roots only
Docs mention Kubernetes "apply manifests now" deploy out of scope for the ticket no kubectl apply
Tests are green after extra lockfile "the task is done" scope diff must match write roots refuse commit until foreign paths drop

Limitations, and who should not use this

The harness does not replace sandboxing, mandatory code review, or a real identity boundary on the server that runs the agent. Prefix matching is crude on purpose so the test stays readable; it will miss obfuscated shells, make targets that wrap npm, and editors that write files without a command line. It also does not prove that the pinned stack is the correct product stack, only that the session stopped improvising after someone accepted a hypothesis.

Do not use this approach as the only control if you handle regulated data, unattended production deploys, or repositories where a bad write is not recoverable from version control. Do not use it to argue that a free-tier model is "safe" because the interceptor returned exit code two in a lab folder. The method is for developers who already expect to review diffs and who want the review to start from pinned facts instead of a fluent story. If your workflow requires the agent to discover a polyglot monorepo without a human pin, this FAQ is arguing against that workflow, not offering a clever prompt that makes it true.

The rumor still arrives first. The only durable change is refusing to let the rumor hold the shell.

Top comments (0)