DEV Community

Quinn Sun
Quinn Sun

Posted on

The Question Ledger Stopped the Agent Before Round Four

The report looked one day short, and the sidebar already had a patch. A coding agent wanted a remote runtime, a date library, and one more tool round. The senior did not open the diff. They opened a blank question-ledger.json and wrote the questions the pair would keep, in order, before any model got a shell.

The junior wanted the missing day restored before standup. The senior wanted a written record of what the pair had already decided not to try.

The pairing room, before the runtime

The bug was narrow. A usage report accepted from and to as calendar dates, converted both to UTC midnight, and treated the end as inclusive in the UI copy. The SQL used a half-open interval. Tuesday's dashboard dropped Monday when a viewer in America/Los_Angeles picked a Monday-to-Monday range.

The agent summarized the fix as "normalize dates." That phrase is how a one-line bound error becomes a new helper package. The senior stopped the prompt and asked the junior to type, not talk.

They were not scoring whether models write better code than most developers. They were deciding which questions had to be answered in a file before a free model or a free server was allowed to draft anything.

The ledger, not the vibe

The senior kept three questions. Each row needed an evidence command, an allowed answer shape, and a hard stop if the evidence was missing.

  1. What instant is the exclusive end bound? Evidence: a failing unit test that prints both instants.
  2. What may leave the laptop? Evidence: a path list with secrets excluded.
  3. When does the agent loop die? Evidence: a round cap and a human apply step.

Spoken answers do not survive the next tool call. Typed rows do. The pair treated the ledger as the session, and the model as a guest inside it.

Question 1, with a command attached

They did not start with a vendor. They started with a test that failed on the laptop.

// example: tests/report-range-bounds.test.mjs
// Proposed pairing artifact. Run locally; do not treat as a measured benchmark.
import test from "node:test";
import assert from "node:assert/strict";
import { exclusiveEndUtc } from "../src/report-range.js";

test("Monday-to-Monday in Los Angeles keeps Monday in range", () => {
  const bound = exclusiveEndUtc({
    from: "2026-09-07",
    to: "2026-09-07",
    timeZone: "America/Los_Angeles",
  });

  // Exclusive end must be the first instant after 2026-09-07 in that zone.
  assert.equal(bound.toISOString(), "2026-09-08T07:00:00.000Z");
});
Enter fullscreen mode Exit fullscreen mode

The test failed because the implementation appended T00:00:00.000Z to the to date and called that inclusive. The UI promised a calendar day. The query promised an instant. That mismatch became ledger row one. Until the command failed for that reason, the agent was not allowed to discuss libraries.

Dead end one: upload the app so the model can "see timezones"

The junior suggested a free remote coding server so the agent could boot the analytics app and click the date picker. The senior asked which files would have to travel for that demo to work.

The tree had .env.local, a snapshots folder with customer-shaped rows, and a staging DSN in a compose file. That was the end of the upload plan. They wrote it down as a dead end, not as a preference.

  • Tried: send the working tree to a remote runtime so the model could reproduce the picker.
  • Failed because: secrets and customer-shaped snapshots would leave the laptop for a timezone bug that already had a unit test.
  • Kept instead: question 1 stays local; a remote draft may see only the two files named in the ledger.

The pair still needed a cheap place to draft a patch during a one-hour block without standing up extra hardware. MonkeyCode's free model access and free server option were on the table as a draft surface after the ledger existed, not as a dump of the app.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

The vendor did not change the rule. If a path was not listed as evidence for question 2, it did not go to any remote runtime.

Dead end two: let the loop pick a date library

The agent, given "fix the missing day," started a loop. Round one added new Date(to + "T23:59:59Z"). Round two imported a date package. Round three invented ReportCalendar and a config flag the test never named.

That is an agent loop without an exit condition. Each round was locally reasonable. Together they replaced a bound function with a policy layer. The senior halted before round four and stamped the second dead end.

  • Tried: one prompt, default tools, no round cap.
  • Failed because: the loop answered a question nobody wrote ("which library should we adopt?").
  • Kept instead: the only question still live was the exclusive end instant, and the only allowed edit was src/report-range.js.

The junior wanted to keep the library because the model had already written the import. The senior pointed at the ledger. Unasked work is not a gift. It is an unreviewed decision.

The decision the pair kept

The kept decision was not "ban agents" and not "trust the free server." The kept decision was: the next round starts only when the previous question has evidence, and round four does not exist.

{
  "pairing_id": "report-range-end-bound-2026-09-13",
  "questions": [
    {
      "id": "q1-exclusive-end",
      "ask": "What instant is the exclusive end bound?",
      "evidence_cmd": "node --test tests/report-range-bounds.test.mjs",
      "status": "open"
    },
    {
      "id": "q2-what-leaves",
      "ask": "What paths may a draft runtime read?",
      "allowed_paths": ["src/report-range.js", "tests/report-range-bounds.test.mjs"],
      "forbidden_paths": [".env", ".env.local", "snapshots/", "docker-compose.yml"],
      "status": "answered"
    },
    {
      "id": "q3-loop-death",
      "ask": "When does the agent loop die?",
      "max_rounds": 3,
      "human_applies_diff": true,
      "status": "answered"
    }
  ],
  "dead_ends": [
    "do not upload the working tree to reproduce the picker",
    "do not add a date library or ReportCalendar"
  ],
  "remote_runtime": "draft-only"
}
Enter fullscreen mode Exit fullscreen mode

The file is dull on purpose. A pairing session that cannot fill it is not ready to rent a runtime, including a free one.

A gate that refuses round four

Talk fades when the agent offers a second patch. The pair added a small checker that exits non-zero if the ledger is incomplete, if secret-shaped paths are allowlisted, or if a round counter tries to pass three. Proposed example, not a production sandbox:

// example: scripts/assert-question-ledger.mjs
import { readFileSync, existsSync } from "node:fs";
import { resolve } from "node:path";

const ledger = JSON.parse(readFileSync("question-ledger.json", "utf8"));
const round = Number(process.argv[2] || "1");

if (!ledger.pairing_id) {
  console.error("pairing_id missing");
  process.exit(1);
}

const q1 = ledger.questions?.find((q) => q.id === "q1-exclusive-end");
const q2 = ledger.questions?.find((q) => q.id === "q2-what-leaves");
const q3 = ledger.questions?.find((q) => q.id === "q3-loop-death");

if (!q1?.evidence_cmd || !q2 || !q3) {
  console.error("ledger is missing required questions");
  process.exit(1);
}

if (q3.max_rounds !== 3 || q3.human_applies_diff !== true) {
  console.error("loop death must stay: 3 rounds, human applies the diff");
  process.exit(1);
}

if (round > q3.max_rounds) {
  console.error(`round ${round} is past the pairing cap; stop and review dead_ends`);
  process.exit(1);
}

const allowed = q2.allowed_paths || [];
const forbidden = q2.forbidden_paths || [];

if (allowed.length === 0) {
  console.error("q2 needs a non-empty allowed_paths list");
  process.exit(1);
}

for (const p of [...allowed, ...forbidden]) {
  if (p.includes("..")) {
    console.error(`refusing path traversal: ${p}`);
    process.exit(1);
  }
}

const secretHits = allowed.filter((p) =>
  /(^|\/)\.env(\.|$)|credentials|id_rsa|kubeconfig/i.test(p)
);
if (secretHits.length) {
  console.error(`allowed_paths includes secret-shaped files: ${secretHits.join(", ")}`);
  process.exit(1);
}

if (existsSync(resolve(".env.local")) && ledger.remote_runtime !== "draft-only") {
  console.error("local secrets exist; remote_runtime must stay draft-only");
  process.exit(1);
}

if (round > 1 && q1.status !== "answered") {
  console.error("round 2+ blocked until q1 has evidence");
  process.exit(1);
}

console.log(`ledger ok: ${ledger.pairing_id} round ${round}/${q3.max_rounds}`);
console.log(`run first: ${q1.evidence_cmd}`);
Enter fullscreen mode Exit fullscreen mode

The pairing order stayed mechanical:

node scripts/assert-question-ledger.mjs 1
node --test tests/report-range-bounds.test.mjs
# draft a patch against allowed_paths only, then:
node scripts/assert-question-ledger.mjs 2
Enter fullscreen mode Exit fullscreen mode

If the second command is reached before q1 is marked answered, the checker dies. That is the whole control. The agent does not get to vote on whether the question was "mostly" handled.

Decision table left on the board

Pairing situation Draft on a free model / free server Do not start a remote draft
Question 1 has a local failing command Yes, after the ledger file exists If the bug is only visible on production rows
Question 2 names two to four files Yes If the model needs the monorepo to "get context"
Secrets stay on the laptop Yes, draft-only If the runtime needs env to reproduce the bug
Question 3 caps the loop at three rounds Yes If nobody is reading the tool trace
Human applies the diff Yes If the plan is to auto-merge the agent branch

The table is not a vendor ranking. A free server is useful when the draft is cheap and the questions are written. It is the wrong tool when the pair cannot name the exclusive end bound without uploading snapshots.

What the agent was allowed to answer

After round one of the checker passed, the junior pasted a prompt that quoted the ledger instead of restating the bug as a slogan.

Answer q1-exclusive-end only.
Make tests/report-range-bounds.test.mjs pass.
Edit only src/report-range.js.
Do not add files or date libraries.
Do not read .env, snapshots/, or docker-compose.yml.
Stop after the test command passes or after 3 rounds.
Do not apply the diff. Print the patch.
Enter fullscreen mode Exit fullscreen mode

The useful patch was a small exclusive-end function. It converted the calendar to date in America/Los_Angeles, then advanced one local day, then emitted UTC. The T23:59:59Z attempt died in dead ends. ReportCalendar never entered the tree because it never entered the ledger.

That was the pairing result worth keeping: three questions, two stamped dead ends, and a loop that was not allowed to invent round four.

Limitations

A ledger does not make a model careful. It makes the pair slow enough to notice when the model is answering a different question.

  • The checker is a seatbelt, not isolation. A model granted broad shell tools can read files the JSON never named.
  • A free remote server, including a draft-only option, still receives whatever the pair uploads. Rows in forbidden_paths only help if humans obey them.
  • The three-round cap is a pairing choice for a short block. It is not a quality score and not a claim about any model.
  • The date test above is a teaching example. It does not prove a named production system, and it does not report latency or accuracy numbers.

Teams under regulated data rules, incident response, or secret rotation should not ship traces to a remote runtime to save a pairing hour. People who want an unsupervised coding agent should not use this approach. The senior stays in the loop, or the ledger is theater.

Who this is for

It fits pairs who already distrust sidebar patches and want a file they can paste into the pull request. It also fits people trying a free model or a free server for the draft step without pretending that "free" means "safe to receive the repo."

The ledger is the artifact. The vendor is interchangeable. If a session cannot fill question-ledger.json, the pair is not ready for anyone else's runtime.

Readers who need a draft surface that does not require extra hardware for a short pairing block can run the same ledger against MonkeyCode's free model access and free server option, then keep the human apply step.

Top comments (0)