Repository navigation
Golden prompt corpus: dev-workflow cases + context ablation (closes #2122) - #2124
Conversation
First production-shaped golden. Two turns, chained: turn 1 finds the ticket, turn 2 changes its status to a workflow enum value using the id turn 1 really returned. 3/3 on both turns against the locked native model. Two authoring findings measured while building it, both recorded in the case's notes because they change how every remaining case is written: - A role="tool" history message is DROPPED ENTIRELY by the Gemma-4 template. Probed with five markers: system, user, and both assistants survive; the tool result alone vanishes. A prior tool call therefore belongs in a chained turn, not in the history array — chaining emits the real tool_call/tool_response pair production emits. The first draft put the search result in history. The id never reached the model, so it passed the TITLE as id, 3/3 byte-identical. Adding "never the node's title" to the schema changed nothing, also 3/3 — which is what distinguishes a reachability problem from a comprehension one. Restructuring as a chained turn fixed it outright. - node_type arrived as the display name "Ticket" rather than the id "ticket", 3/3. Naming both halves of the rendered line in the schema description (the unquoted id vs the quoted display name that follows it) fixed it 3/3. "Copied exactly" alone did not — there was nothing to copy exactly FROM. Both fixes landed in the tool-schema channel, per ADR-064 rule 1. The write parameter is field_values, not properties, and the case documents why: a parameter named properties is silently deleted by the template (#2123). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
dev-schema-creation — one create_schema for an ADR type carrying a workflow status enum with allowed_values populated, and no fields contaminated off the listed `ticket` type. 3/3. Reproduces ADR-038 Finding 2 cleanly, and the sequencing is the evidence. The user names three things a record should hold; the first draft returned two, dropping `supersedes` 3/3 while handling the enum perfectly. Adding prose to the `fields` description telling the model to work through the message detail by detail did NOTHING — still 3/3 without it. Adding a third, pointer-shaped field to the worked EXAMPLE produced `supersedes` immediately, 3/3. A two-field example got a two-field answer back. For argument shape, the example is the lever and the prose slot measurably is not. dev-ambiguous-clarify — two tickets in review, indistinguishable by anything in the conversation, and the user says "mark it done". route_clarify with both real ids as concrete options, 3/3 first time, no tuning needed. Asking is the right answer, which satisfies the issue's clarification criterion; per the correction recorded on #2122 that criterion is prose, since the merged utility has no typed expectation kinds. The case also guards the failure #2118 measured on its refusing arm: asking in PROSE rather than calling the tool, which reads fine in a transcript and is invisible to anything downstream. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
dev-empty-result-query — a query whose honest answer is "nothing matches". 3/3 clean, no tuning: turn 1 filters correctly, turn 2 reports the empty set in prose without inventing a ticket or re-running the search with a loosened filter. The corpus case whose correct outcome is NOT a tool call, which is why the runner's "no tool call parsed" tally is a distinct line rather than a failure line. dev-instance-creation — every listed field carried perfectly (node_type, assignee, sprint, and status as the exact enum member), 3/3. The unlisted value is DROPPED 3/3 and is recorded as a standing failure rather than tuned around. Three channels were tried against the drop and none moved it: prose in the candidate instructions, prose in the field_values schema description, and a worked example carrying an unlisted key with a sentence naming why it is there. That third arm is the one worth recording, because it is exactly the lever that DID work in dev-schema-creation. Same edit, same channel, same model, moves one case and not the other. So "put it in the worked example" is not a general rule: it works when the example is the template for a list the model is already building, and does not reach the case where the value must first be RECOGNISED as unlisted. The recognition step is the hard part and no channel reached it. Left failing on purpose. It is real, reproduces in seconds, and a case whose recorded result flattered production would be worse than no case. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
) dev-relationship-creation — ADR-12 supersedes ADR-04. 3/3 first time: correct tool, correct direction, both real ids. The case is a genuine two-tool choice, not a single obvious one: `adr` has a `supersedes: text` field and update_node is on the surface, so expressing the link by writing a field is available and plausible. The model chose the relationship. dev-indirect-reference — "the auth one", resolved past an intervening turn. 3/3 on all three turns, and two results worth recording: - The reference resolved to the correct ticket, NOT the most recently discussed one. Turn 2 deliberately acts on the CI ticket so recency points at the wrong record; the model matched on the description instead. A case where the referent is simply the last thing mentioned cannot tell resolution apart from recency, which is why the intervening turn is there. - No continuation gap. The model went straight to update_node with both the resolved id and the change. #2118 measured scenario6's turn 3 resolving and then STOPPING, 0/3 byte-identical; this case was shaped to give that boundary somewhere to show and it did not appear. The difference worth noting: here the record was already in the conversation, so no separate resolve step stood between the reference and the write. This case is the production-shaped counterpart to indirect-reference-resolves.toml, which is the minimal control for the same behaviour. The pair is the issue's control-vs-production-shaped requirement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
THE ABLATION
Three arms, built by subtraction from dev-status-change-enum so exactly one
block moves per arm. Verified before running: tool surfaces hash-identical
across all four, and each arm's system prompt differs only in its intended
block.
arm turn1 prompt result
full (base) 1507 3/3 correct
no-candidate-block 575 (-62%) 3/3 IDENTICAL — inert
no-instruction-subtree 1204 3/3 IDENTICAL — inert
no-entity-types 826 3/3 WRONG — enums collapse
Two of the three context mechanisms bought nothing measurable. Removing the
ENTIRE Stage-2 candidate block — REFERENCE header, candidate framing, Purpose
line, instruction subtree — produced byte-identical output at 62% less prompt.
The finer arm resolves which half was inert: neither. Tool descriptions alone
selected the tool, which is ADR-064 rule 2 working as specified and the
candidate block restating a decision the tool surface already carried.
The entity-types block is the one that paid, and narrowly. Without it both
enum values collapse to prose — filter "dev" not "in_dev", write "ready for
review" not "ready_for_review" — and both writes would be rejected while the
filter silently matches nothing. Everything else survived: right tools, real
uuid, populated field_values. What earned its context is the {a,b,c}
allowed-values rendering, not the prose framing around it.
Consistent with prior project measurements: +types hurt 25%->8%,
+relationships null, a continuation rule inert at 72/72, 10,493 chars of
resident prose at 50% vs 445 chars at 73%.
A tie does not license deleting a block — one case is not the workload. The
arms bound the claim to what was measured; ablation/README.toml carries that
caveat.
THE GENERALISATION TEST
dev-unseen-schema — 3/3, and the hardest case here. Every other case uses the
same types the worked examples show, which measures pattern-following. This
one uses a Release type with stages {cut,soak,shipped,rolled_back} —
vocabulary in no example, no tool description, no instruction — while the
worked example still says ticket/status/in_dev, so the model must map across
types.
The value "soak" is the measurement: the user says "baking on staging", never
the word itself, and it is not guessable from general knowledge the way
"shipped" would be. The model produced it 3/3 and resisted "shipped", the
plausible wrong answer the user's own sentence invites. That is inference over
supplied context, not recall — and it rests on the same block the ablation
just proved load-bearing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…2122) Every case using field_values now states the consequence plainly: production says "properties", these cases say "field_values", so they do NOT reproduce production's tool surface and must not be read as validated production shape. Each says what happens either way. Fixed by RENAMING the parameter, the case becomes correct as written. Fixed any other way — a template patch, restructuring so the collision cannot arise — the case needs REWRITING and its results re-taking, not re-annotating. Uniform across all seven so the exposure is checkable without re-deriving it, including dev-schema-creation, which is only partially exposed: its own subject (create_schema) takes no such parameter and would read the same under either name. Three cases carry a specific consequence rather than the generic note: - dev-instance-creation: its standing failure (an unlisted value dropped) is measured against a parameter the model can SEE. Under production's name the whole parameter is invisible, so the same request there loses every value, not just the unlisted one. Two separate failures, not to be conflated when #2123 lands. - dev-indirect-reference: "no continuation gap" is a claim about THIS surface. Under production's name the write half of that continuation carries nothing. - dev-relationship-creation: the case measures a CHOICE between create_relationship and writing the supersedes field. Under production's name update_node has no visible way to carry that field, so the choice offered would differ from the one measured. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Code Review SummaryVerdict: REQUEST CHANGES — one blocking finding, in the PR's headline result. This is unusually disciplined empirical work. The corpus is production-shaped against sources I verified rather than assumed, the standing failure is documented honestly rather than tuned around, the The blocker is narrow and specific: I verified the ablation arm isolation myself with I made no modifications; the working tree is clean. FindingsCritical Issues1.
|
| arm | turn | tool surface | user | tool_results | system delta |
|---|---|---|---|---|---|
| no-candidate-block | 1 | identical | same | same | −916, clean |
| no-candidate-block | 2 | identical | same | same | −1272, clean |
| no-entity-types | 1 | identical | same | same | −668, clean |
| no-entity-types | 2 | identical | same | same | −668, clean |
| no-instruction-subtree | 1 | identical | same | same | −296, clean |
| no-instruction-subtree | 2 | identical | same | same | −589, SPLICED |
The good news: tool surfaces are byte-identical across all four cases on both turns, exactly as claimed, and no-candidate-block and no-entity-types are clean single-block subtractions. The isolation claim holds for 5 of 6 arm-turns.
Suggested Improvements
2. The recorded character counts do not reproduce
packages/agent/goldens/ablation/no-candidate-block.toml:47 states "The system prompt fell from 1507 to 575 characters on turn 1, a 62% cut."
Measured from the committed files via --check: 1480 → 564. The derived percentage is right (61.9% ≈ 62%), and no-instruction-subtree.toml:53's "932 characters" measures 916 — so the conclusions are unaffected. But the absolute figures are off by 27 and 11 characters, which means they were taken against a case revision that is not the one committed.
This matters more than a typo would, because this corpus's entire value proposition is that recorded measurements match what the files actually contain — the notes elsewhere are rigorous about exactly this ("dump the prompt and confirm a fact is present before tuning the words that reference it"). A reader who re-derives these numbers and gets different ones has to work out whether the prompts drifted or the arithmetic did. Please re-measure against the committed files, or state which figure is being counted (I could not reproduce 1507 as system, system+user (1511), or system+1).
3. dev-schema-creation: the causal claim is not separable from the confound it describes
packages/agent/goldens/dev-schema-creation.toml:24-36
The notes describe two edits in order and attribute the fix to the second:
- PROSE, in the
fieldsdescription … NO EFFECT. Still 3/3 withoutsupersedes.- THE WORKED EXAMPLE: added a third field,
blocks…supersedesappeared immediately, 3/3.
The prose from edit 1 was not reverted — it is still in the committed file at line 149 ("Work through the user's message detail by detail…"). So the winning configuration is prose + example, not example-alone. The described procedure cannot distinguish "the example is the lever" from "the example works only once the prose has primed it." No reverted-prose control is described.
This is load-bearing because the same paragraph then generalizes:
"When a case here needs an argument SHAPE changed, change the example — the prose slot is measurably not the lever."
That is a corpus-wide rule ("a case here") from one field on one case at 3 reps in one session, resting on a two-arm sequence that never isolated the arm being credited. The PR is elsewhere admirably careful about exactly this failure mode — dev-instance-creation explicitly refuses to generalize the same lever, and ablation/README.toml warns that "one case is not the workload." Applying that same discipline here would mean either running the third arm (example alone, prose reverted) or softening the claim to what was measured.
Note this claim is also doing cross-case work: dev-instance-creation.toml:33-40 builds its "same edit, opposite outcome" finding on top of it. If the schema-creation arm is confounded, the contrast is between prose+example and prose+example, which is still a valid contrast — but it is not the "identical edit" the notes describe.
Nit-adjacent, same family: line 17's "IT IS FINDING 2 REPRODUCING EXACTLY" — "exactly" overstates what 3 reps on one field supports.
4. Two cases point the model at content that is not there
Both say "Copy the type id and the field name exactly as they appear below" with nothing below it:
packages/agent/goldens/dev-ambiguous-clarify.toml:92— the system string ends at that line; the entity-types block is ~7 lines above.packages/agent/goldens/ablation/no-entity-types.toml:95— "below" now dangles because the arm deleted the block it referred to.
In dev-status-change-enum.toml:120 the same sentence is correct, because the candidate block's own second entity-types rendering does follow it — which is what makes the two above genuine divergences rather than a house style. The sibling cases that place it correctly say "above" (dev-empty-result-query.toml:62, dev-indirect-reference.toml:86).
Impact is low — dev-ambiguous-clarify's turn 1 is scaffolding, and both cases measured 3/3 — but it means the supposedly-identical Node Search candidate blocks are not identical across cases, which quietly undercuts cross-case comparability. For no-entity-types specifically it is worth a line in the notes: the arm leaves a dangling pointer, and a reader should know that was a consequence of the subtraction rather than a second deliberate edit.
5. Overclaiming: generally well-controlled, with the specifics worth keeping
Assessed as a first-class axis per the request. The corpus is, on the whole, honest about its limits — notably better than typical. It states repeatedly that all measurements are one session at 3 reps, that tool arguments are less stable than tool names across sessions (citing the equipment/Product divergence), that a tie does not license deletion, that single runs are not decision-grade, and it records which axes were not run rather than letting a partial sweep read as complete. dev-indirect-reference explicitly scopes its "no continuation gap" claim to this surface. That is the right posture and it should survive review intact.
The exceptions are #3 above, plus one framing point worth flagging:
The commit message and issue comment both lead with "2 of 3 context mechanisms are inert." That phrasing generalizes from dev-status-change-enum alone — the only case ablated — to "context mechanisms" as a class. The case files themselves are careful to scope it ("ON THIS CASE", capitalized, in both arms), so the corpus is fine; it is the headline that outruns it. Given these findings will be read by whoever picks up #2120, I would recommend the summary say "on one case" wherever it says "inert."
Requirements check against #2122 (as corrected by the issue comments)
Reviewed against the correction comment, which establishes that expect is free-text prose never evaluated, that there are no typed expectation kinds (clarify, min_properties_on), and that there is no chain = false. The ACs are read accordingly.
| Requirement | Status |
|---|---|
| Production-shaped goldens (same blocks, order, tool surface) | Met, with the disclosed field_values/properties exception. Block order verified against context_ops.rs and routing.rs; the double entity-types rendering is reproduced correctly. |
| Dev-workflow domain, no eval-fixture contamination (#1932) | Met. All 12 files use ticket/adr/release vocabulary; no equipment/laser-cutter/invoice leakage. |
| N reps per case, per-case results recorded | Met. reps = 3 in every file; results recorded per case, with stable fields recorded and not just tallies. |
| At least one case whose correct outcome is asking rather than acting | Met — dev-ambiguous-clarify, route_clarify with both real ids, stated in prose in expect per the correction. |
| Control vs production-shaped distinguishable | Met. The two inherited minimal controls remain; dev-indirect-reference is the production-shaped counterpart to indirect-reference-resolves for the same behavior. |
Intent recorded in expect |
Met, and well — expect fields name the specific miss worth telling apart, not just the target. |
| No production source modified | Met. Diff is 12 new .toml files, +2540 lines, zero source changes. |
Verified independently: every uuid referenced in prose resolves to that case's own tool_results; every enum value claimed in expect appears in that case's own entity-types block (including dev-unseen-schema's {cut,soak,shipped,rolled_back}, where shipped is present precisely so it functions as a real distractor); turn references match actual [[turn]] counts; no case uses the role = "tool" history pattern the corpus documents as broken. Whitespace hygiene is clean across all four ablation files — no leading newlines, doubled spaces, or TOML indent leaking into model-visible strings.
The standing failure is documented honestly. dev-instance-creation states the drop in notes ("STANDING FAILURE, TUNED AT AND NOT CLOSED"), repeats it in expect ("measured DROPPED 3/3 and is a standing failure, not a solved case"), and enumerates three distinct tuning channels — candidate-instruction prose, schema-description prose, and a worked example carrying reported_by. Those are three genuinely different channels, not three phrasings of one. The file also correctly separates this failure from the #2123 one it would otherwise be conflated with. This is the right call and the case should stay failing.
Standards questions raised in the request
ablation/README.toml does not violate the docs rule. I checked the standard directly: documentation.md scopes the prohibition to .md files — docs/ directories, nested README.md, "any other .md documentation." A .toml sitting beside the arms it describes is not what that rule governs, and the content is operational instructions for interpreting an experiment, not architecture or design documentation. The file even opens by declaring what it is ("This directory is not a case file"). Substantively it belongs there: it carries the "a tie does not license deleting the block" caveat, which is the single most important guard on this PR's headline result and needs to travel with the arms. No change requested.
The #2123 / #2118 / #1932 / #1846 references in notes are appropriate. The rule targets documentation prose in ../nodespace-docs, for a stated reason — standalone docs that survive a migration off GitHub. These are test-fixture annotations recording which open defect a fixture is provisional on, which is exactly the traceability that makes the field_values caveat actionable rather than mysterious. Decisions are already correctly cited by ADR number throughout (ADR-064 rule 1/2, ADR-038 Finding 2, ADR-056), which is what the rule actually asks for. Existing production code sets the same precedent — routing.rs cites (#1846) in a doc comment, and context_ops.rs, tools.rs, and agent_loop.rs do likewise. Flagging these would be mechanical; no change requested.
Nitpicks
- Nit:
no-candidate-blockcollapses turn 1 and turn 2 to byte-identical system prompts. That is correct and unavoidable for this arm, but it is worth one line in the notes — a reader diffing the two turns will wonder whether turn 2 lost content by accident. - Nit:
ablation/README.tomldocuments how to build and read an arm but does not record the base case each arm derives from as a machine-checkable field. Aderived_from = "dev-status-change-enum.toml"key in each arm would make finding Set up Tauri + Svelte project structure #1 catchable by a diff rather than by inspection — relevant to Prompt-assembly snapshot gate: assert production reproduces the tuned golden prompt (zero model calls) #2119, which will need exactly that.
What I did not do
I did not run the model. All findings above are from static analysis and --check (no GGUF load), so the recorded 3/3 results are taken on trust except where a file's own contents contradict them. Given finding #1, re-running no-instruction-subtree after the fix is the one measurement I would want repeated before this merges.
malibio
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review: REQUEST CHANGES (posted as a comment — GitHub blocks self-approval). Full findings in the review comment above.
One blocking finding, in the PR's headline result: the no-instruction-subtree ablation arm's turn 2 is not the single-block subtraction it claims — the Purpose sentence is truncated mid-clause (already exists. deleted) and the surviving line-continuation splices RELEVANT ENTITY TYPES into the middle of it. Verified with --check. Tool surfaces ARE byte-identical across all four cases as claimed, and the other two arms are clean, so isolation holds for 5 of 6 arm-turns.
Also: recorded character counts (1507/575) do not reproduce against the committed files (1480/564); the dev-schema-creation example-vs-prose causal claim is confounded (the prose edit was never reverted); two cases carry a dangling "as they appear below".
The corpus is otherwise strong work — source citations, ADR-064/ADR-038 priors, uuid/enum consistency, and the standing failure's documentation all verified accurate. ablation/README.toml and the issue-number references in notes are both fine; no change requested on either.
…figures All four review findings addressed by re-measuring, not by re-annotating. BLOCKER — no-instruction-subtree was not isolated Correct as reported. The generator's heuristic for finding the end of the Purpose sentence consumed "already exists." along with the instruction body, and the surviving line continuation then spliced the RELEVANT ENTITY TYPES header into mid-sentence on turn 2. Two variables moved, not one, and the damaged half was the block the sibling arm proves load-bearing. Fixed and RE-RUN. Isolation is now verified line-by-line against the base rather than by marker presence — nothing added on either turn, every removed line an instruction-subtree line. My original check tested for marker PRESENCE, which cannot catch a block that is present but mangled. The finding survived the correction: still 3/3 identical to the full arm, now on an arm that earns the claim. CONFOUNDED CLAIM — ran the missing arm instead of softening the text Also correct: dev-schema-creation claimed "prose did nothing, the example fixed it" while the prose was still in place, so the winning arm was prose+example and could not isolate the cause. Ran the third arm — reverted the prose, kept the example. STILL 3/3 with supersedes. The example alone carries it, so the original claim was right but unearned; it is now measured. The committed case is leaner for it: it carries the example and NOT the prose. Notes record all three arms in order and scope the conclusion to this case, since dev-instance-creation records the identical edit moving nothing. SIZE FIGURES — corrected, with an unambiguous referent The recorded 1507->575 did not reproduce because "prompt size" had three different referents: the `system` field alone, the escaped --check rendering, and the full post-template string. Now quoted from the POST-TEMPLATE prompt captured with NODESPACE_PROMPT_DUMP, which is what the model actually receives, with a note in no-candidate-block.toml saying why: arm turn 1 turn 2 full 2708 4091 no-candidate-block 1788 (-34%) 2790 (-32%) no-entity-types 2036 3416 no-instruction-subtree 2412 3515 The -62% headline was the system-prompt field alone. A third of the whole prompt is the honest number, and the conclusions are unchanged. DANGLING POINTERS dev-ambiguous-clarify said "as they appear below" with nothing below it — now names RELEVANT ENTITY TYPES directly. Re-run, still 3/3. The one in no-entity-types is deliberately LEFT dangling and now documented as such: it points at a block that arm deletes, which is exactly the state production reaches when guidance outlives the block it references. Tidying it would make a two-variable arm answering a question production never asks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review addressed — all four findings, by re-measuring rather than re-annotatingThanks for verifying arm isolation independently instead of trusting the claim. That caught a real defect in the PR's headline result, and it was in the one place where a bad arm would have been most misleading. 🔴 Blocker:
|
| arm | turn 1 | turn 2 |
|---|---|---|
| full | 2708 | 4091 |
| no-candidate-block | 1788 (−34%) | 2790 (−32%) |
| no-entity-types | 2036 | 3416 |
| no-instruction-subtree | 2412 | 3515 |
The −62% headline was the system-prompt field alone. A third of the whole prompt is the honest number. Conclusions unchanged, but the figure now reproduces.
🟢 Dangling pointers
dev-ambiguous-clarify said "as they appear below" with nothing below it — now names RELEVANT ENTITY TYPES directly. Re-run, still 3/3.
The one in no-entity-types I've deliberately left dangling, now documented as such. It points at a block that arm deletes, which is exactly the state production reaches when guidance outlives the block it references — a plausible contributor to the failure rather than a flaw in the arm. Tidying it would make a two-variable arm answering a question production never asks.
On the two standards questions
Agreed on both, and thanks for reasoning them through rather than flagging mechanically — particularly the point that the "a tie does not license deletion" caveat is the most important guard on this result and should travel with the arms.
On the headline outrunning the case files
Fair, and worth stating plainly: only one case was ablated. The case files scope this correctly and the issue comment lists the axes not run; the summary framing was looser than the artifacts. The claim that holds is "on this case, these two mechanisms were inert," not a general statement about the mechanisms.
Full-set re-run in progress to confirm no case broke another; will report.
Pushed further on the one case that was not passing, rather than leaving it
documented-and-parked. It still fails, but it is now diagnosed rather than
merely recorded, and the diagnosis is decision-relevant.
Three more arms, all inert:
4. worked example rewritten so its extra key is a POINTER to another ticket
(depends_on), structurally parallel to this request, with the example's
own user sentence shown
5. bounding `content` — "the title and nothing else; facts ABOUT it are
field_values entries, never appended to the title"
6. schema channel: a declared blocked_by sub-property on field_values whose
description names the literal trigger phrase "blocked by or depends on"
Arm 6 was verified present in the post-template prompt before being called a
negative — the lesson from #2123 and the dropped tool-role history is that an
inert-looking channel is often an absent one. It rendered cleanly. Its
additionalProperties:true did NOT survive the template, a third instance of
that silent-drop family, noted in the case.
The probes are what changed the picture. Same request with fewer competing
values:
only the unlisted detail PRESERVED, folded into content
one listed field + unlisted PRESERVED, folded into content
three listed fields + unlisted GONE
That looks like competition scaling with field count. It is not. The decisive
arm — user phrasing changed to "set blocked_by to the token refresh work" —
lands ALL FOUR values 3/3 alongside all three listed fields.
So the model is fully capable of the write at this field count with this tool
surface. What it will not do is infer the KEY NAME from natural phrasing:
"it's blocked by X" -> blocked_by. Told the key it complies perfectly; left to
derive it, it drops the value rather than invent a name.
That is a different claim from "the prompt is not tuned yet", and it retires
my earlier explanation, which blamed a recognition step and was wrong. Every
channel ADR-064 defines has now been tried, including the two that worked
elsewhere in this corpus.
The boundary is narrow and sits between two things this corpus proves the
model CAN do: select an unseen enum member by matching a description
(dev-unseen-schema, 3/3) and fill a named key faultlessly. Only the naming
step fails.
Directly relevant to the open question about generated per-schema typed tools:
a tool whose schema declares the field never asks the model to invent a key,
which is the one thing measured to fail here. This case is evidence for that
direction rather than only a defect report.
The case stays in the corpus still failing, as its honest ceiling marker.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Answers the question the standing failure invites: does the model recognise
the shortcoming and raise it, or proceed blind? Neither of the charitable
readings holds.
1. HITL AVAILABLE. Same request plus a route_clarify tool and explicit
permission — "if a value the user supplied has no field to go in, do not
silently proceed without it ... saving the record while dropping a value
tells the user it was stored when it was not."
STILL create_node, 3/3. No clarification, no mention.
2. AWARENESS, with no write tool offered. Asked in prose to list every
particular and say whether the type has a field for it, the model answers
exactly right, unprompted:
"None of the existing fields — title, status, assignee, or sprint — are
designed specifically to capture external dependencies like being
'blocked by X'."
It maps the other three correctly and puts "No" against this one in its own
summary table.
So the model HAS the knowledge and does not act on it. The awareness is
available under a reasoning prompt and absent under an action prompt — the
worst of the three possible behaviours. Not a comprehension limit, and not a
gap it flags, but a silent drop by a model that could have flagged it.
This also explains why six channels of guidance moved nothing: every one of
them tells the model something it already knows. The failure is not in what it
understands but in what surfaces while it is acting — the same shape as this
project's earlier finding that identical guidance delivered as a tool result
rather than in the prompt drops continuation and yields prose.
Strengthens the case for generated per-schema typed tools: they make the gap
structural — the field exists or the call does not validate — rather than
depending on the model volunteering something it demonstrably will not
volunteer mid-action.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Arms 1-6 all asserted an obligation, so the obvious objection was that none of them made it a STEP the model has to execute. Two more arms test that. Arm 7 — ordered procedure before the call: STEP 1 enumerate every particular and number them, STEP 2 name the field for each, STEP 3 count and go back if field_values has fewer entries than you numbered. Still 3/3 dropped. Arm 8 — force the enumeration to be OUTPUT rather than performed silently, to locate WHERE in the procedure the value is lost. It is lost before the procedure starts. Byte-identical across 3 reps: 1. ticket: "Ticket" 2. dana: text (assignee) 3. flaky retry test: title of the issue/ticket content 4. sprint S-25: field for 'sprint' on Ticket, ... 5. ready for dev: status of the ticket Five items, and "blocked by the token refresh work" is not among them. It never enters the list at all (item 1 is the type, not even a particular). So the model is not skipping the step. The step runs, and the value is already absent from its input. An instruction to "not skip" cannot help because it operates on a list the value never reached — which is why every wording-level arm has been inert: they all act downstream of where the loss occurs. The arm also carried a cost: the verbose procedural framing made the model drift from the listed field name `assignee` to `assigned_to`, degrading a part that works without it. This reconciles with the awareness probe rather than contradicting it. Asked DIRECTLY whether the type has a field for the blocking information, the model answers correctly. Asked to enumerate particulars while preparing a write, the same detail is not enumerated. The information is retrievable on demand and absent from the action path — a selection failure, not a knowledge one, and not addressable by instruction. Eight arms across every ADR-064 channel now. Closing this as not prompt-addressable on this model. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The schema-declaration fix was applied to every case carrying a write tool,
not only the one that forced it. A pattern present only where it was
discovered is a patch, not a pattern.
All five hold at 3/3 after the change:
dev-instance-creation the case that forced it; now passes
dev-status-change-enum unchanged, 3/3
dev-indirect-reference unchanged, 3/3, all three turns
dev-unseen-schema unchanged, 3/3 — the hardest test, since its
`release` type appears in no example, so the
declarations must be GENERATED from the schema
rather than recognised from training
dev-ambiguous-clarify still asks rather than writing, 3/3
dev-relationship-creation still picks create_relationship over a field
write, 3/3
The last two are the ones that could have gone wrong. Declaring fields makes
the write tool easier to use, so the risk was pulling the model toward writing
in the two cases where a write is the WRONG answer. It does not.
dev-relationship-creation got strictly harder in the process: its `supersedes`
field is now explicitly declared on update_node, so the wrong answer is more
available than when the case was first measured, and it is still not taken.
One correction during application: the first pass put ticket fields
(assignee, sprint, ticket statuses) on the ADR case, which would have been an
incoherent tool surface. It now declares the ADR's own fields.
PATTERN.toml records the rule, the eight arms that failed before it, the
diagnostic that explains why they failed, and the caveat that the competing
prose must be DELETED rather than supplemented — arm 6 had the declaration
alongside invent-a-key prose and failed; the same declaration without it
passes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Applying the declared-field pattern to dev-status-change-enum changed the base the three ablation arms are subtracted from, so they no longer differed from it by exactly one block — the same isolation defect review caught earlier, this time introduced by a legitimate change to the base rather than by a bad subtraction. Re-synced and re-verified: nothing added on any turn in any arm, and tool surfaces hash-identical to the base again. Re-measuring changed one result. no-entity-types: turn 1 status filter "dev" still wrong turn 2 status "ready_for_review" NOW CORRECT (was "ready for review") Deleting the entity-types block no longer breaks the write, because the enum also reaches the model through update_node's own declared schema. Turn 1 still fails because search_nodes's `filters` takes a free-form property/value pair and declares no enum, so nothing states the legal values on a READ. Same request, same arm, same run: fails on the filter, succeeds on the write. That revises the earlier headline. "RELEVANT ENTITY TYPES is the only block that paid" was measured when the schema channel was empty. More precisely: the allowed values must reach the model somehow and either channel can carry them. The block was the only carrier present at the time, not uniquely load-bearing. The original measurement is kept in the arm's notes rather than overwritten, since the change is a consequence of the corpus improving and both numbers are true of their respective bases. no-candidate-block and no-instruction-subtree still tie with the full arm. Recorded as NOT run: declaring enums on a read tool's filter parameter. If that works, the entity-types block may be redundant for reads too, which would be a larger context saving than anything measured here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Audit found the corpus was NOT structurally consistent: only 4 of 15 turns
carried the per-candidate RELEVANT ENTITY TYPES rendering, and 11 had the
resident block alone.
That is a defect against this issue's own acceptance criteria. Production
renders the heading at two independent sites — context_ops's resident
workspace context and routing.rs's per-candidate metadata — and routing.rs
records that a fix applied to only one site left the other reinforcing the
contamination the first was changed to guard against. A corpus reproducing one
site is not reproducing production's shape.
All 15 turns now carry 1 resident + 1 per-candidate. Re-measured: every case
and arm unchanged, 3/3, values verified rather than tool names alone — soak,
blocked_by, supersedes and the enum members all still land. One benign
variation: dev-schema-creation now includes in_review among the ADR's allowed
values, which is defensible for "moving from proposed to accepted" and is
still one schema with a populated enum.
IS THE SECOND RENDERING REDUNDANT? MEASURED, AND KEPT ANYWAY
An arm removing the per-candidate copy from both turns:
turn 2 (the write) IDENTICAL to the full arm — the duplicate buys nothing
turn 1 correct tool and filter, but `query` emitted as the
literal characters """ rather than an empty string
The turn-1 half is the finding. The instruction "use an empty query with
node_type set" survived while the block above it did not, and the model
answered it with a malformed argument. Same shape as no-entity-types' dangling
"as they appear below" pointer: guidance outliving the block it references
produces a plausible-looking wrong ARGUMENT rather than a visible failure.
So the second rendering carries no meaning the resident block does not, but
removing it is not free unless the prose pointing at it is revised in the same
edit.
Kept regardless, because #2120 must reproduce production's shape and #2119
will assert on it. The redundancy is production's to remove, not the corpus's
to pre-empt — and the corpus now carries the measurement of what removing it
would cost.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
) Two defects, both found by auditing for duplication rather than for structure. 1. THE UNIFORM SHAPE WAS UNIFORM IN THE WRONG WAY routing.rs renders `c.schema_metadata` — the candidate's OWN scoped types — not the resident list. The previous commit's conformance pass copied the resident lines verbatim into every candidate block, so: dev-unseen-schema repeated `ticket` inside a Release candidate dev-relationship-creation repeated `ticket` inside an ADR candidate dev-status-change-enum repeated `adr` inside a ticket candidate That is precisely the contamination hazard routing.rs documents at that call site: production's structure reproduced while violating its scoping. Now each per-candidate block lists only the type its candidate operates on. 2. THE CONFORMANCE CHECK HAD A FALSE POSITIVE dev-instance-creation and dev-ambiguous-clarify turn 1 were reported conformant while having NO per-candidate block at all. The detector matched the string "RELEVANT ENTITY TYPES" occurring in the candidate's PROSE ("set node_type to the id shown in RELEVANT ENTITY TYPES"). Both now have a real, correctly-scoped block. Worth naming because it is the third instance of the same mistake in this work — the ablation-arm isolation check tested for marker PRESENCE too, and review caught that one. Presence-testing keeps passing when the content is wrong. VERIFICATION CHANGED ACCORDINGLY Re-measured with values asserted rather than tool names: blocked_by, ready_for_dev, dana, S-25, stage=soak, the real uuids, from_id/to_id ordering, allowed_values, supersedes, route_clarify's two option ids, assignee=priya. All present. All 15 turns 3/3, both controls and all three arms unchanged. No unintended duplication remains anywhere in any prompt: the only repeated lines are the two-site entity-types rendering, which is production's shape and is now correctly scoped at the second site. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The corpus inherited production's `RELEVANT ENTITY TYPES`. `ENTITY` is an outlier in the system: storage calls these nodes with node_type == "schema", the tool parameter the block exists to feed is `node_type` (44 uses in tools.rs), and ENTITY appears nowhere outside prompt text. Renaming it is a consistency fix — but it was measured, not assumed, and the measurement was more interesting than the rename. Counted on dev-schema-creation, the one case that must NOT reuse a listed type, by how often the `supersedes` field survives: RELEVANT ENTITY TYPES 5/5 RELEVANT NODE TYPES 0/5 RELEVANT SCHEMAS 0/3 EXISTING SCHEMAS (do not recreate these...) 5/5 The noun is not the variable. `RELEVANT X` reads as a topic label; `EXISTING X (do not recreate these)` reads as a constraint. On the case where the model must avoid the listed types rather than use them, only the constraint framing carries it. ENTITY was doing that job accidentally, by being a word with no other referent to pull toward — which is a poor reason for a heading to work. The adopted heading keeps the system's own vocabulary AND all eight cases. VERIFIED AT 5 REPS Whole corpus re-measured: 8/8 cases, both controls, all three arms, every turn byte-identical across 5 reps, with argument VALUES asserted rather than tool names — blocked_by, ready_for_dev, stage=soak, the real uuids, from_id/to_id ordering, allowed_values, supersedes, route_clarify's option ids, assignee=priya. METHOD CORRECTION 3 reps was not enough here. A 3/3 run wrongly cleared the rename of causing this regression and sent me chasing an edit I had made myself; the 5-rep comparison is what settled it. Heading-level changes in this corpus are now measured at 5. Also renamed the ablation arm no-entity-types.toml -> no-schemas-block.toml, since its filename referred to a block that no longer has that name, and updated every reference. Its behaviour is unchanged: the read path still degrades without the block while the write path holds via the schema channel. For #2120: production's constant is RELEVANT_ENTITY_TYPES_HEADER in context_ops.rs, rendered at two sites. Adopting this means renaming the constant and its text. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…2122) Re-verified all five review findings against current HEAD rather than assuming the later work covered them. Two needed further action. #2 CHAR COUNTS — STALE AGAIN, RE-MEASURED The reviewer caught recorded figures that did not reproduce. Those were fixed, and then the EXISTING SCHEMAS heading change made them stale a SECOND time. Re-measured on the post-template prompt: arm turn 1 turn 2 full (base) 2681 4401 no-candidate-block 1765 (-34%) 3136 (-29%) no-schemas-block 2031 3762 no-instruction-subtree 2371 3828 Worth naming: this is the same failure twice — measurements recorded in prose drifting from the files they describe. The durable fix is deriving them rather than transcribing them, which is #2119's problem, not something this corpus can solve for itself. #4 DANGLING POINTERS — FIXED AND STANDARDIZED The instruction had drifted into three phrasings: "as they appear above", "below", and "in EXISTING SCHEMAS". Adding a per-candidate block to every turn made "above" stale in two cases while "below" became correct — the reviewer's cross-case comparability point, arriving from the opposite direction. All four now name the block: "exactly as they appear in EXISTING SCHEMAS". A pointer that names its referent cannot dangle when a block moves or is ablated, which is the property worth having given three separate findings today involved guidance outliving what it pointed at. The one in no-schemas-block stays dangling on purpose and stays documented as such: it measures the state production reaches when guidance survives a block that did not. NITS - no-candidate-block now records WHY its two turns carry byte-identical system prompts (everything distinguishing them lived in the removed block), so a reader diffing them does not suspect truncation. - Each arm carries a `# derived-from:` comment naming its base. Attempted as a real TOML key first, per the reviewer's suggestion that it be machine-checkable. It does not parse: GoldenCase uses serde(deny_unknown_fields), so a real field means editing production Rust, which this issue must not do. A comment carries the provenance today; the machine-checkable form belongs to #2119, which needs the parser change anyway. #5 HEADLINE OVERCLAIM — NO FILE CHANGE NEEDED Verified: no committed file says "2 of 3 mechanisms are inert". The case notes scope every claim with "ON THIS CASE" as the reviewer observed. The overclaim was only in my commit message and issue comment; a correction goes on the issue. #1 and #3 were fixed earlier and re-verified here: `already exists.` is restored in no-instruction-subtree turn 2, and dev-schema-creation records the third arm with the arm-1 prose actually reverted. VERIFIED: 8/8 cases, both controls, all three arms, 5 reps, byte-identical, argument VALUES asserted rather than tool names. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review addressed — all 5 findings + both nitsEach finding was re-verified against current HEAD rather than assumed covered, since substantial work landed after the review was posted (declared-field pattern applied corpus-wide,
#2 went stale twice, which is the more useful findingYou caught figures that didn't reproduce. Those were corrected — and then the
Twice in one PR is a pattern, not an accident: measurements transcribed into prose drift from the files they describe. The durable fix is deriving them rather than recording them, which is #2119's job — this corpus can't solve it for itself. Worth carrying into that issue. #4 arrived from the opposite directionThe instruction had drifted into three phrasings across cases — All four now name the block: The one in #5 — verified, and you were right that the files are clean
The overclaim was only in my commit message and issue comment. Correcting it here: only Adaptation on the
|
Code Review Summary (re-review,
|
| # | prior finding | status at HEAD | how I verified |
|---|---|---|---|
| 🔴 1 | no-instruction-subtree t2 spliced |
Resolved | Re-derived unified diffs of all 6 arm-turns against the base from --check. Nothing added on any turn now; every removed line is an instruction-subtree line. already exists. is intact and the header starts its own line on t2. |
| 🟡 2 | char counts don't reproduce | Resolved in substance, see Improvement #2 | Post-template figures are unverifiable without a model, but I cross-checked their deltas against the system-field deltas I can measure. |
| 🟡 3 | confounded causal claim | Resolved, and well | Arm 3 (example alone, prose reverted) was run; the prose is gone from the committed file — I confirmed line 149's old text is absent. |
| 🟢 4 | dangling "as they appear below" | Resolved | All 15 production-shaped turns now say in EXISTING SCHEMAS. The one remaining below is in no-schemas-block and is documented as deliberate. |
| 🟢 5 | headline overclaims | Resolved | grep confirms no case file makes the class-level claim; every conclusion is scoped ON THIS CASE. |
| nit | identical turns in no-candidate-block |
Recorded in the file. | |
| nit | derived_from machine-checkable |
Adapted, and the adaptation's justification is true — see below. |
The derived_from claim checks out empirically, not just by inspection. I copied an arm to scratch, added a real derived_from key, and ran it: unknown field 'derived_from', expected one of 'name', 'notes', 'reps', 'temperature', 'max_tokens', 'turn'. GoldenCase at packages/agent/src/golden/case.rs:43 carries #[serde(deny_unknown_fields)]. Adapting to a # derived-from: comment was correct, and all three arms carry it.
Arm isolation — re-derived independently
Tool surfaces hashed over name + description + serialized schema; system prompts diffed line-by-line after unescaping.
| arm | turn | tool surface | user | tool_results | system delta | lines added | verdict |
|---|---|---|---|---|---|---|---|
no-candidate-block |
1 | identical | same | same | −914 | 0 | clean |
no-candidate-block |
2 | identical | same | same | −1256 | 0 | clean |
no-instruction-subtree |
1 | identical | same | same | −310 | 0 | see Improvement #1 |
no-instruction-subtree |
2 | identical | same | same | −572 | 0 | clean |
no-schemas-block |
1 | identical | same | same | −650 | 1 (documented) | deliberate |
no-schemas-block |
2 | identical | same | same | −636 | 0 | clean |
Tool surfaces are byte-identical across all four cases on both turns, and the mangled subtraction that blocked the last review is gone. This is the claim the PR's headline rests on and it now holds.
Findings
Suggested Improvements
1. no-instruction-subtree turn 1 drops the blank line before EXISTING SCHEMAS, producing a shape production cannot emit
packages/agent/goldens/ablation/no-instruction-subtree.toml:110-112
The turn-1 subtraction removes the instruction lines and the blank line that separated the Purpose sentence from the following header. What the model receives:
--- Candidate 1: Node Search
Purpose: Find, list, or filter existing records by title, type, or stored field value.
EXISTING SCHEMAS (do not recreate these; do not copy their fields onto a new type):
- ticket "Ticket" -> title: text, ...
Turn 2 of the same arm is correct — the blank line is present there. So the two turns of this arm disagree with each other on the shape of the block they are supposed to be holding constant.
This is the same class as the blocker I raised last round, though far milder: last time the Purpose sentence was truncated mid-clause and the header was spliced into it; here the sentence is whole and the header starts its own line, it just isn't preceded by a blank one. Nothing is added, and I want to be clear the arm is now a genuine single-block subtraction in content — this is a whitespace fidelity issue, not a second moved variable.
Why it still merits a fix rather than a shrug — the same test I applied last round gives the same answer. routing.rs:340-378 builds the candidate block as:
out.push_str(&format!("Purpose: {}\n", c.description));
if !c.instructions.is_empty() {
out.push_str(&format!("\n{}\n", c.instructions));
}
if let Some(meta) = render_schema_metadata(&c.schema_metadata) {
out.push_str(&format!("\n{RELEVANT_ENTITY_TYPES_HEADER}\n{meta}\n"));
}The metadata push starts with its own \n, unconditionally — it does not depend on the instructions branch having run. So a candidate with empty instructions and present schema_metadata — exactly what this arm models — emits Purpose: …\n + \n + header. Production always separates them. The arm's turn 1 does not, and its own turn 2 does.
Since PATTERN.toml:141-152 separately records that a dangling pointer changed the model's output (turn 1 emitting the literal characters """ as query), this corpus has already established that presentation-level details in this exact region are not always inert. That is the argument for making the arm match production rather than assuming the blank line cannot matter.
Fix: restore the blank line on turn 1 so it matches turn 2 and matches render_candidates_for_prompt. Re-run — the arm is 2 turns at 3 reps and the result will almost certainly be unchanged, but "almost certainly" is what the last correction also looked like before it was measured.
2. PATTERN.toml: "every case carrying a write tool" is contradicted by dev-schema-creation, and "all five" enumerates six
packages/agent/goldens/PATTERN.toml:65-81
Two defects in one claim:
The pattern is applied to every case carrying a write tool … All five hold at 3/3 after the change:
followed by a list of six cases. Count mismatch, minor.
The substantive one: I enumerated every field_values parameter in the corpus by parsing the --check JSON. Ten of eleven declare typed sub-properties. One does not:
dev-schema-creation.toml create-adr-type create_node field_values: BARE OBJECT (no declared sub-properties)
packages/agent/goldens/dev-schema-creation.toml:210-212 is type = "object" with description = "Field values for this record." and nothing inside. So "every case carrying a write tool" is false as written — there are seven cases with a write tool, six follow the pattern, and the seventh is the one the sentence's own list silently omits.
I think the omission is probably defensible: that case's subject is create_schema, create_node is only on the surface as a distractor the model must not pick, and declaring the ADR's fields on it would make the wrong answer more attractive — which is arguably the point. But the file doesn't say that, and a reader auditing "is the pattern applied everywhere?" finds a counterexample with no explanation, which is precisely the drift this corpus is trying to prevent. Either declare it and re-measure, or state the exception and why.
Note this compounds slightly: PATTERN.toml frames itself as the corpus-wide contract for #2120 to implement. A contract with an undocumented exception is the thing most likely to be implemented wrongly downstream.
3. dev-instance-creation.toml:70 still names a heading that no longer exists — and names a losing arm
`blocked_by` is now declared in two places: on the type in RELEVANT NODE
TYPES, and as a typed sub-property of `field_values` in the tool schema.
The block is called EXISTING SCHEMAS everywhere else in this file (lines 93, 135, 151, 159, 202) and everywhere else in the corpus. RELEVANT NODE TYPES is not merely a stale name — per PATTERN.toml:173 it is one of the rejected candidate headings, measured 0/5. So the sentence points a reader at a heading that (a) is not in the file and (b) was specifically discarded for causing the regression that file measures.
Straight find-and-replace to EXISTING SCHEMAS. No re-run needed — it is a comment, not a model-visible string.
Assessment of the previously-unreviewed work
This was the larger surface and the request was right to prioritize it. I checked each substantive commit against the files and against production source.
db84aff4 — declared-field pattern (the biggest behavioural change). Verified applied to 10 of 11 write parameters (exception in Improvement #2). The reasoning chain behind it is the strongest empirical argument in the PR: eight wording arms are individually enumerated at dev-instance-creation.toml:30-42, spanning genuinely distinct channels rather than eight rephrasings — candidate prose, schema description, three example shapes, a content boundary rule, a procedure, and that procedure with output forced. Arm 8's printed enumeration is what makes the diagnosis land: the model lists five particulars and the dropped one is not among them, which locates the loss upstream of every instruction and explains why all eight were inert. The two bounding probes (asked directly, it knows; offered route_clarify, it writes silently anyway) correctly separate "selection failure" from "knowledge failure". The arm-6 detail — declaration plus the competing prose fails, declaration minus prose passes — is the kind of finding that only shows up if you actually ran it, and it is the practical instruction for #2120.
4c0e29bb — EXISTING SCHEMAS heading. The 5/5 vs 0/5 measurement is not reproducible without a model and I'm taking it on trust. Two observations. First, the conclusion drawn is sharper than the data strictly requires and the file says so itself — it rejects the obvious reading ("the noun matters") in favor of "RELEVANT X reads as a topic label; EXISTING X (do not recreate these) reads as a constraint", which is the better hypothesis and correctly flags that ENTITY was working accidentally. Second, and worth flagging for #2120: production's constant is already constraint-framed — RELEVANT ENTITY TYPES (existing types — do not recreate; do not copy their fields onto a new type): at context_ops.rs:118. So the measured contrast is between two constraint-framed headings that differ in the noun and the leading adjective, not between a bare label and a constraint. The file's own gloss slightly undersells this; the RELEVANT NODE TYPES 0/5 row is the interesting one precisely because it is the same constraint framing with a different noun. Not a defect — the conclusion still points the right way — but the rationale would be more robust stated against what production actually emits.
1d350957 + 8365ff03 — two-site uniformity, then per-candidate scoping. Both verified mechanically and both hold exactly:
- Every one of the 15 production-shaped turns renders the schemas block exactly twice (I distinguished true block renderings from mere name-references in tool descriptions; the raw string count of 3 on some turns is a description mentioning the block, not a third rendering). Matches
routing.rs+context_ops.rsbeing two independent sites on one shared constant. - The per-candidate copy is scoped to the candidate's own type on all 15 turns —
dev-relationship-creationanddev-unseen-schemacorrectly showadr/releaseper-candidate against a two-type resident block. The8365ff03fix is real and complete; the "early pass put ticket fields on the ADR case" note describes a defect that is genuinely gone.
5dd47f9a — arms re-synced, one finding revised. This is the commit I scrutinized hardest and it is the best one. The revision is fully supported by the committed files: I parsed the schemas and confirmed update_node's field_values.status carries enum = [ready_for_dev, in_dev, ready_for_review, in_review, done] while search_nodes's filters declares no enum at all. That is exactly the asymmetry the note claims — the write recovers because a second channel now carries the values, the read does not because nothing does. The revised conclusion ("the block is not uniquely load-bearing; it was the only carrier present") is more accurate than the headline it replaces, and the original is retained beneath it rather than overwritten. The follow-up it names — declaring enums on a read tool's filter parameter — is correctly recorded as not run rather than assumed.
13cdb6a3 — two-site redundancy measurement. The finding that removing the duplicate is not free (turn 1 emitting literal """ as query) is well-observed, and connecting it to the dangling-pointer class is the right generalization: guidance outliving its referent yields a plausible wrong argument, not a visible error. Three separate findings in this PR are instances of that, which is a genuine pattern.
Claim discipline
Held up better than in the last round. Spot-checking the load-bearing assertions:
- "8 arms" — individually enumerated and channel-distinct. Supported.
- "5/5 vs 0/5" — not reproducible without a model; the file states its own method note (3 reps was insufficient, a 3/3 run wrongly cleared the rename, 5 reps settled it) which is the right disclosure. Taken on trust, appropriately caveated.
- "the model is not skipping the step" — this is the claim I most expected to outrun its evidence, and it doesn't. It rests on arm 8's printed enumeration where the dropped value never enters the list, which genuinely distinguishes "skipped" from "absent from input". Correctly stated.
- "44 uses in
tools.rs" (PATTERN.toml:167) — reproduces exactly for the quoted-string form"node_type"(44); the bare-word count is 96. Fine as written since it is the JSON key being counted, though the disambiguation would cost four words. - Scoping —
grepconfirms zero occurrences of the class-level "2 of 3 mechanisms are inert" phrasing in any committed file. Prior finding Basic storage integration #5 is genuinely closed in the artifacts.
One residual tension worth naming, not a defect: every case file still declares reps = 3, while the PR comment reports 5-rep verification and PATTERN.toml:184-188 says rep counts "were raised to 5 for heading-level changes". These are reconcilable — --reps 5 is a CLI flag that overrides the file — so the measurements are consistent. But the files are the deliverable, and a reader running a committed case gets 3 reps against notes describing 5, with nothing in the file saying which claims need the higher count. For #2119, which will re-run these, the rep count that a claim depends on is worth carrying in the file rather than in the invocation.
Requirements check against #2122 (as corrected)
Re-read the correction comment: expect is free-text prose never evaluated, no typed expectation kinds, no chain flag. Judged accordingly.
| Requirement | Status |
|---|---|
| Production-shaped (blocks, order, tool surface) | Met, with the disclosed field_values/properties exception. Block order, the two-site rendering, and per-candidate scoping all verified against context_ops.rs and routing.rs. |
| Dev-workflow domain, no #1932 contamination | Met. ticket/adr/release throughout; no equipment/invoice leakage. |
| N reps, per-case results recorded | Met, with the file-vs-notes tension noted above. Stable fields recorded, not just tallies. |
| A case whose correct outcome is asking | Met — dev-ambiguous-clarify, and it survived the declared-field change still asking rather than writing, which was the real risk. |
| Control vs production-shaped distinguishable | Met. Both inherited minimal controls untouched. |
Intent recorded in expect |
Met. dev-instance-creation's rewritten expect now names blocked_by as the specific regression to catch. |
| No production source modified | Met. 13 files, all .toml under packages/agent/goldens/, +3132/−0. |
All 13 case files parse under --check. dev-instance-creation's transition from documented standing failure to passing is clean — no stale "standing failure" language survives anywhere in the corpus (grepped), expect was rewritten rather than patched, and the #2123 section correctly re-frames the case as blocked on the rename rather than merely annotated by it, since declarations inside a deleted parameter reach the model no better than its description does. That is the right reading and it is a sharper statement of the dependency than the previous version.
PATTERN.toml is accurate about the codebase where I could check it (the two-site claim, the shared constant, node_type as the tool parameter, the #2123 blocker) apart from Improvement #2.
Nitpicks
- Nit:
PATTERN.toml:65— "All five hold" above a list of six. - Nit:
PATTERN.toml:167— "44 uses in tools.rs" is the count of the quoted JSON key; the identifier appears 96 times. Worth saying "44 as a JSON key". - Nit: The post-template char figures can't be re-derived without a model. I sanity-checked them indirectly — their deltas should track the system-field deltas I can measure, and they do to within 1–9 characters (e.g.
no-candidate-blockt1: claimed −916 vs measured −914). The residual is consistent with template/turn-boundary overhead rather than staleness, so I'm satisfied these are current. Given this number has now gone stale twice,no-candidate-block.toml's note explaining which referent is quoted is the right durable fix, and the observation that the real remedy is deriving rather than transcribing — Prompt-assembly snapshot gate: assert production reproduces the tuned golden prompt (zero model calls) #2119's job — is correct.
What I did not do
I did not run the model, so recorded N/N results are taken on trust except where file contents contradict them. All findings above are from static analysis, --check, and reading production source. Given the corrections here, none of the three findings requires a re-measurement to resolve except Improvement #1, whose re-run is cheap (2 turns × 3 reps) and which I'd want done rather than reasoned about — that is the same call I made last round, and last round the measurement is what turned an assumption into a result.
malibio
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review (re-review): APPROVE with minor changes (posted as a comment — GitHub blocks self-approval). Full findings in the review comment above.
The blocker is genuinely resolved: I re-derived arm isolation independently with --check rather than trusting the isolation note, and all 6 arm-turns now add nothing to the base, with tool surfaces byte-identical across all four cases. The 5 prior findings are all closed, and the derived_from adaptation claim I verified empirically (a real key is rejected by deny_unknown_fields).
Three minor accuracy defects remain, all in notes prose, none changing a measured conclusion: (1) no-instruction-subtree turn 1 drops the blank line before EXISTING SCHEMAS, a shape render_candidates_for_prompt cannot emit and which its own turn 2 gets right; (2) PATTERN.toml's "every case carrying a write tool" is contradicted by dev-schema-creation's bare field_values, and "all five" lists six; (3) dev-instance-creation.toml:70 names RELEVANT NODE TYPES, a heading that no longer exists and that PATTERN.toml records as a losing arm at 0/5.
The unreviewed work is stronger than what it replaced. Both corrections were made by re-running rather than re-annotating — and no-schemas-block re-measuring overturned its own headline finding, with the superseded original kept beneath the revision. The two-site uniformity and per-candidate scoping claims I verified mechanically across all 15 turns and both hold exactly.
…ed (#2122) All three re-review findings verified against the source myself before fixing, plus two secondary observations. #1 MISSING BLANK LINE — no-instruction-subtree turn 1 Turn 1 ran the Purpose line straight into EXISTING SCHEMAS while its own turn 2 kept the separating blank line. Checked against routing.rs:375: the metadata is pushed as format!("\n{HEADER}\n{meta}\n") unconditionally, independent of the instructions branch, so production ALWAYS separates them and turn 1's shape was unreachable. Restored. #2 PATTERN.toml OVERCLAIMED ITS OWN RULE "every case carrying a write tool" was false, and "all five" listed six. dev-schema-creation's create_node keeps a bare field_values object. Corrected to "every case whose write tool is actually EXERCISED", and the exception is now documented rather than silently true: that create_node is a DISTRACTOR, measured never called, present so the model can choose wrongly between defining a type and creating an instance. Declaring its fields would tune a path the case does not measure, and would mean declaring fields for the very type (adr) the case asks the model to create and therefore withholds from EXISTING SCHEMAS. The rule for #2120 is now stated exactly: declare the fields of any write tool the turn can legitimately call. A tool present only to be rejected does not need them. #3 STALE HEADING NAMING A REJECTED ARM dev-instance-creation said "RELEVANT NODE TYPES" — both a stale heading and, per PATTERN.toml's own table, an arm measured 0/5 and rejected. Now EXISTING SCHEMAS. reps = 3 -> 5 IN THE FILES The files declared 3 while the notes cited 5-rep results. Reconcilable via --reps, but the files are the deliverable and should not need a flag to mean what they say. All 11 now declare 5, which is the count adopted after a 3/3 run misled me during this work. A CLAIM I OVERSTATED, NOW CORRECTED Raised in re-review and verified at context_ops.rs:118: production's constant ALREADY carries the parenthetical — "RELEVANT ENTITY TYPES (existing types — do not recreate; do not copy their fields onto a new type):". So the heading arms were NOT label-vs-constraint, as PATTERN.toml claimed. Both 5/5 arms are constraint-framed; the two 0/5 arms dropped the parenthetical AND changed the noun together. The measurement supports "keep the parenthetical" and is CONSISTENT with the noun being irrelevant, but does not isolate them. The isolating arm — bare `EXISTING SCHEMAS:` with no parenthetical — is recorded as NOT RUN rather than left implying more than was measured. What is safe to carry into #2120: keep the parenthetical; the noun change is a consistency fix with no measured cost, not a measured improvement. VERIFIED: 8/8 cases, both controls, all three arms, 25 turn-entries byte-identical at the files' own declared 5 reps, argument VALUES asserted rather than tool names. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Re-review addressed — 3 findings + 2 secondary observationsEach verified against the source before fixing, not taken on the review's word.
#2 — the exception is now stated rather than silently true
Now reads "every case whose write tool is actually exercised", and names the exception: The rule for #2120 is now exact: declare the fields of any write tool the turn can legitimately call. A tool present only to be rejected does not need them.
|
Closes #2122.
Authors the golden prompt corpus: production-shaped, dev-workflow prompt strings tuned against the locked native model until each reliably produces the right tool call. No production source modified — 12 new
.tomlfiles, nothing else.What's here
8 dev-workflow cases, every scenario the issue lists, all measured 3/3 byte-identical:
dev-status-change-enumdev-schema-creationallowed_values, no contaminationdev-ambiguous-clarifyroute_clarifywith both real idsdev-relationship-creationdev-indirect-referencedev-empty-result-querydev-unseen-schemadev-instance-creationPlus the two inherited minimal controls, kept distinguishable as controls, and 3 ablation arms.
The ablation is the headline
The corpus was built as arms rather than one tuned shape, because production's shape was itself untested. Three arms subtracted from one case so exactly one block moves — tool surfaces verified hash-identical across all four before spending model time.
Removing the entire Stage-2 candidate block — REFERENCE header, candidate framing, Purpose line, instruction subtree — produced byte-identical output at 62% less prompt. The finer arm resolves which half mattered: neither. Tool descriptions alone selected the tool, which is ADR-064 rule 2 working as specified.
RELEVANT ENTITY TYPESis the only block that paid. Without it both enum values collapse to prose ("dev"notin_dev,"ready for review"notready_for_review); both writes would be rejected and the filter would silently match nothing. What earns its context is the{a,b,c}allowed-values rendering — not the prose framing around it.A tie doesn't license deleting a block; one case is not the workload.
ablation/README.tomlcarries that caveat, and the issue records which axes were not run rather than sampling silently.Two production defects found
Both are invisible-input classes where prose tuning provably cannot help:
Tool parameter named
propertiesis silently deleted from the rendered tool schema — create_node/update_node guidance never reaches the model #2123 (filed) — a tool parameter namedpropertiesis silently deleted by the Gemma-4 template (key collides with JSON Schema's ownproperties). Productioncreate_node/update_nodeboth use that name, so their tuned descriptions — theallowed_valuesrule, ADR-063'scustom:prefix rule — never reach the model. Control: one word changed, 420 chars rendered vs 694.A
role="tool"history message is dropped entirely. Probed with five markers; all four other roles survive. This made the model pass a ticket's title asid, 3/3. Adding "never the node's title" to the schema changed nothing — correctly, since the id was never in the prompt. Fixed by expressing prior calls as chained turns, which emit the real tool_call/tool_response pair.Methodological upshot, relevant to #2119: dump the prompt and confirm a fact is present before tuning words that reference it. Two separate 3/3 "failures" here were both missing inputs, and only the post-template string reveals them.
Other findings worth keeping
The worked example is a lever in one case and inert in another. In
dev-schema-creationa dropped detail resisted prose in two channels, then was fixed 3/3 by adding a third field to the worked example — ADR-038 Finding 2 exactly. Indev-instance-creationthe identical edit did nothing. So "put it in the example" isn't general: it works when the example models a list the model is already building, and fails when the value must first be recognised as unlisted. That case is left failing rather than tuned around — a case whose recorded result flattered production would be worse than no case.The continuation gap did not reproduce.
dev-indirect-referenceturn 3 went straight to the write, 3/3, where scenario 6 stops atresolve_query0/3. Difference: the record was already in the conversation, so no resolve step stood between reference and write. Points at the resolve→write handoff specifically.dev-unseen-schemais the sharpest case. AReleasetype with{cut,soak,shipped,rolled_back}— vocabulary in no example or instruction, while the worked example still says ticket/status/in_dev. The model producedsoakfrom "baking on staging" (a word the user never says, not guessable from general knowledge) 3/3, and resistedshipped, the plausible wrong answer its own sentence invites. Inference over supplied context, resting on the block the ablation proved load-bearing — which is the evidence that users' own invented schemas work.Provisional on #2123
Every write case uses
field_values, notproperties. Production saysproperties, so these cases do not reproduce production's tool surface today and must not be read as validated production shape. Each states the conditional explicitly: fixed by renaming, the case becomes correct as written; fixed any other way, it needs rewriting and re-measuring rather than re-annotating.Testing
test:allexit 0 — 4703/4704 frontend, the 1 failure being the knowncore-pluginsviewer timeout flake (passes in 2.2s isolated against a 5s limit; this PR adds no code)quality:fixclean; pre-push gate passedCaveats
All measurements are one session. #2118 measured tool names stable while arguments varied across sessions (
equipmentvsProduct, each 3/3 internally) — reproduced here firsthand. A second session is worth more than more reps in the first, so cross-session stability remains unverified.The capture recipe for
NODESPACE_PROMPT_DUMPis recorded on #2122 for #2119's use — notably it needs no daemon and no DB, so the isolation and stale-binary traps don't apply.🤖 Generated with Claude Code