Repository navigation
Golden-prompt utility: iterate on agent prompts as data (closes #2118) - #2121
Conversation
Tune agent prompts by editing a TOML file and re-running — no recompile, no NodeSpace plumbing in the path. The prompt text becomes the artifact under iteration; the assembly line is then engineered to reproduce it. This generalizes the methodology golden_scenario6_handauthored.rs and golden_scenario6_sequence.rs proved but kept bespoke: one scenario, hand-rolled engine loading, assertions in Rust, prompts as string literals. Every tweak needed a recompile, which is why it never became the general tuning loop. Separating two questions is the point. "Does this exact prompt text get the right tool call?" is answerable in seconds here. "Does the real pipeline assemble that prompt?" is a separate, deterministic, zero-model-call question. Conflating them is what made every prior checkpoint cost a daemon rebuild, a DB purge, and minutes per iteration. Fidelity: the runner goes through LlamaChatInferenceEngine -> ChatEngine, so it gets the model's own chat template, llama.cpp's tool-call parsing, and production's repair_tool_call_arguments applied at the same parse boundary agent_loop.rs applies it. Tuning against a hand-rolled call or llama-server HTTP would tune against a fiction — ADR-038's findings were gathered that way and ADR-046 recorded that they did not transfer. - src/golden/case.rs — format + expectation evaluation, model-free and unit-tested in the default run. Expectation kinds: tool (with recursive subset argument matching), sequence (with min_properties_on, so a case can require the write actually carried properties), clarify (a first-class pass, per ADR-038's clarification contract), text, observe. - src/golden/runner.rs — execution. Multi-turn cases chain a turn's REAL output into the next turn's history as a well-formed assistant-plus-tool- result exchange, closing the loop the sequence golden left as a TODO. chain = false isolates a turn so one file can hold independent arms of one experiment. - src/bin/golden_runner.rs — a bin, so it is outside cargo test entirely and cannot be pulled into the default run (it loads a ~5GB GGUF). Reps are mandatory, defaulting to 3: single runs are not decision-grade. "No tool call parsed" and "wrong tool called" are distinct outcomes with distinct tally lines — they have opposite fixes. Five case files committed as worked reference instances, all verified 3/3 against the locked native model, reproducing the ported goldens' recorded results. Two findings the ported probes were built to answer now have clean answers recorded in their notes: the resolve_query history defect is size/shape rather than "any history at all", and narrowing further, STYLE rather than size — a terse arm at the failing arm's token count resolves, while the narrative arm at that count refuses. Carried-forward history should therefore be compacted into terse facts, not shorter narrative summaries. The two Rust golden test files are deleted, not left alongside: they are fully expressible as cases now, and their doc-comment references elsewhere are repointed at the case files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pragmatic Code Review — PR #2121Review Type: Initial Review (0 prior reviews, 0 prior comments) SummaryThis is a strong, well-motivated change and a clear net improvement. It converts a bespoke, recompile-per-edit tuning ritual into a data-driven instrument, and it does so while increasing fidelity to production rather than trading it away. The module and case-file documentation is genuinely excellent: it records not just what the code does but which findings it produced and why particular design choices are load-bearing. The expectation-evaluation layer is model-free and covered by 40 unit tests that run in the default I verified independently: Fidelity claim: verified. Worth stating plainly: the deleted tests were less faithful than what replaces them. Their Two things are worth resolving, neither in the runner code itself. First, TOML multi-line string handling has injected whitespace into ~30 of the strings the model actually sees, and the drift is unevenly distributed across the arms of the one case that treats token count as a controlled variable. Second, the "fully expressible as cases now" claim holds for Requirements Check
12 met, 3 met-with-a-defect. No criteria entirely missing. Code Review Findings🟡 Important1. TOML multi-line strings inject whitespace into ~30 of the prompts the model actually sees Indented The Rust literal it was ported from ( So the ported case sends a leading newline, two leading spaces, a doubled internal space, and a trailing Why this is Important rather than Nit — and it is sharper than a general tidiness complaint. The It also breaks the port's premise: the files claim to reproduce the deleted tests' results, and reproduction requires the same input. And the next person editing a case will not know which whitespace is intentional. The engineering principle is correctness of the measuring device before correctness of the measurement: an instrument that silently perturbs its own input cannot distinguish a prompt effect from an artifact of its own encoding. Two ways to fix, either acceptable:
Whichever route, please add a unit test asserting a representative indented case file parses to the exact expected string. Without a test this regresses the moment someone adds a sixth case. 2. Four of The commit message and the case-file notes present the sequence golden as "fully expressible as cases now". For
I want to be careful here, because most of this is defensible and the notes argue the case well: replacing hand-authored stand-ins with real chained output is the stated point of the feature, and the notes explain persuasively why the isolated variants should not survive (they each passed while silently disagreeing on the schema id). I agree with deleting the isolated turn-2/turn-3 probes. Two things still need attention:
The daemon test still passes — it builds its own The principle in both halves is the same: a deletion is safe when the value is genuinely relocated, and the claim of relocation should be checkable. Here it is asserted in three places and is only partly true. 🟢 Suggestions3. //! `tests/golden_scenario6_handauthored.rs` and
//! `tests/golden_scenario6_sequence.rs`: assemble a sequence of literal prompt
The prose reads as a live pointer ("The methodology this generalizes was proved by …"), not as history, so a reader will go looking for files that are gone. 4. The documented invocation names a case file that does not exist The committed file is 5. Both use 6. The literal is field-for-field identical to 7. A vacuous assertion is expressible, though not currently written
I checked the neighbouring cases for genuine vacuity and found none: 8.
Verified clean
RecommendationCOMMENT — approve-in-spirit, with findings 1 and 2 worth resolving before merge. The architecture, the fidelity to production, the separation of the model-free evaluation layer from the model-loading runner, and the documentation quality are all clearly above bar. This is a decisive net improvement to how prompt work gets done here, and the runner code itself I have no substantive objection to — findings 3–8 are minor and several are optional. On the central question the issue raises, the fidelity claim holds up under line-by-line checking, and the replacement is measurably more faithful to production than what it deletes. The two I would not wave through are both about the corpus and its claims rather than the machinery:
Neither is a design objection. Both are the artifact not yet matching what it says about itself, which for this particular tool is the thing most worth getting right. 🤖 Generated with Claude Code |
malibio
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review: COMMENT (posted as a comment — GitHub blocks self-approval). Full findings in the review comment above.
The issue was rescoped after this branch started: a small utility that reads a
case file, calls the model through NodeSpace's real path, and prints what came
back. The human reads the output and judges it. Not a harness.
Removed from the previous commit:
- The whole expectation/assertion engine — Expectation kinds, evaluate(),
subset_mismatch, is_subsequence, properties_shortfall, min_properties_on
- Outcome/CaseRun/RepRun, pass/fail, N/N tallies, failure-kind breakdown
- Directory batch execution and multi-case invocation
- The chain flag and cuts_the_chain — both existed only to keep independent
ASSERTION arms from contaminating each other; with no assertions, no arms
- 27 unit tests covering the above
Nothing automated consumes expectations here. Holding production to a frozen
golden is the snapshot gate's job and end-to-end scoring is the matrix's, so a
third scoring system duplicated both — and would have been new surface for the
failure this project has already paid for, a harness bug reported as a model
failure.
Restored: golden_scenario6_handauthored.rs and golden_scenario6_sequence.rs
stay in tests/ as the validated Rust-side reference that proves the runner is
faithful to production templating and parsing. The daemon and
live_update_node_noop_gate.rs edits are reverted — they were doc-comment
repoints forced by deleting those files, and are unnecessary now.
Kept, and load-bearing:
- LlamaChatInferenceEngine -> ChatEngine, so apply_chat_template (PEG_GEMMA4),
chat/parser.rs, and repair_tool_call_arguments are production's own
- TOML whitespace normalization. Rust's "...\ strips the next line's indent;
TOML's """ does not, so without it every prompt reaches the model with a
leading newline, a per-line indent, and doubled spaces at continuations. One
ported block measured 35 chars longer than the literal it reproduces. For a
utility whose premise is "the file is exactly what the model sees", that is
the one bug that poisons every result. 4 tests pin it.
- An inert `expect` field: free-text prose, printed beside the result, never
compared. A tuned case handed downstream has to answer "what was this tuned
to produce?" from the file itself. Free-text on purpose — the moment it has
structure something will assert on it.
Two committed cases, both verified 3/3 against the locked native model:
- indirect-reference-resolves.toml — the worked reference instance. Calls
resolve_query rather than asking for a node id. Notes record that node_type
is NOT stable across sessions ("equipment" vs "Product"), since nothing in
the case tells the model what types exist; the tool name is the signal.
- scenario6-full-sequence.toml — the multi-turn example, chaining each turn's
real output forward. Byte-identical across reps, with turn 2 picking up turn
1's actual schema name rather than a guess.
1,182 lines total across 4 files (~420 logic, ~250 tests, ~230 docs), down
from 2,215. 17 unit tests in the default run. test:all exit 0 at baseline;
clippy clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rescoped, and review findings addressedTwo things happened since the review: the issue was rescoped to a one-time utility, and the review's findings 1 and 2 both turned out to matter. Commit 99c0550 handles both. Rescope — what was cut
The snapshot gate holds production to a frozen golden and the matrix does end-to-end scoring, so a third scoring system duplicated both — and would have been new surface for the failure mode this project has already paid for. 2,215 → 1,182 lines. Diff vs main is now +1,450 / −0, additive only, entirely within Review findings🟡 1 — TOML whitespace. Confirmed and fixed. You were right, and the uneven distribution was the sharp part. Measured after the fix, all three arms match their Rust originals exactly:
All 83 model-facing strings across the corpus now parse free of leading/trailing newlines and doubled spaces. Writing those tests caught a second bug in my own fix: the space-collapsing pass was eating the relative indentation the dedent had just preserved. Collapsing now applies only within a line, so paragraph breaks and numbered lists survive — which matters, since narrative shape is sometimes the variable under test. 🟡 2 — Dropped tests. Resolved by reverting the deletion. Rather than porting the missing probes, 🟢 3, 4 — fixed (stale On
|
Pragmatic Code Review — PR #2121 (Re-Review)Review Type: Re-Review SummaryThe rescope is a genuine improvement and it was executed honestly. 2,215 → 1,182 lines, the assertion engine and scoring are gone, the diff is now additive-only, and — the part that matters most — the simplification did not quietly take correctness with it. I re-verified the fidelity claim on the rewritten The two 🟡 findings from the prior review are both properly resolved, one of them by a better route than the one suggested. Finding 2 in particular: restoring One real defect remains, and it is in exactly the code the rescope introduced and the prior review asked for: Everything else below is small. Against the "keep it simple" constraint I looked specifically for remaining fat and found less than the 1,182-line count suggests: ~420 lines of logic, ~250 of tests, ~230 of doc comments, and the doc comments are carrying real findings rather than restating the code. I am not going to argue for cuts I cannot justify — see the note at the end on the one place I would have, and why I withdrew it. Verification I ran independently: Previous Review: 8 recommendations
7 addressed or genuinely moot, 1 addressed with a residual defect, 0 lost, 1 newly introduced (🟡 1, which is a defect in the fix for prior finding 1). I checked that 5, 7 and 8 are moot rather than silently retained: Verification of resolution 2 specifically, since it was the structurally largest change:
Requirements Check — revised acceptance criteria onlyThe original criteria list is superseded; scoring against the 7-item list in the scope correction.
7 of 7 met. No criterion missing, none partially met. On criterion 7, I checked the port rather than taking the notes' word for it, since the restored Rust file makes it directly diffable. Comparing
So the whitespace fix worked: the case file now reproduces the Rust prompt byte for byte, which is the property prior finding 1 said it had lost. The three-arm size-vs-style case that made that finding sharp is no longer in the corpus at all (it lived in the deleted files), so that specific risk is retired rather than merely mitigated. Findings🟡 Important1. let dedented = if line.len() >= indent {
&line[indent..]
} else {
line
};
I probed this against an extracted copy of the function. It survives everything the case files actually contain — em-dashes mid-line, the literal It breaks when a line's leading whitespace is multibyte whitespace. Reproduced three ways: leading-position, trailing-position, and — the most plausible of the three — a blank line made of two ideographic spaces between two conventionally-indented lines ( Severity is 🟡 rather than 🔴 because nothing committed triggers it and the blast radius is one CLI invocation. But the principle is robustness at the trust boundary, and this function sits at the only trust boundary the utility has: it is the code that decides what the model sees, taking arbitrary hand-pasted text as input. A panic there produces a stack trace with no case name and no line number, and the author's next move is to suspect their prompt edit. That is the failure mode the rescope was explicitly meant to stop creating. The fix does not need new machinery — it needs the char-oriented API instead of the byte-oriented one: let dedented = line
.char_indices()
.nth(indent_chars)
.map_or("", |(byte_idx, _)| &line[byte_idx..]);with Please add the panic case as a unit test. The existing four normalizer tests are good and cover the cases the prior review asked for — leading-newline strip, common-indent over non-blank lines only, interior-vs-leading collapsing, idempotence — but all four use ASCII whitespace, so none of them would catch this. One test with a U+3000 in it closes it permanently. 🟢 Suggestions2. Unconditional chaining is right for this tool, but a multi-arm case file will now silently mislead Removing the
The footgun is narrower than "chaining is wrong". Because every turn now unconditionally inherits every prior turn, a case file with independent arms — several turns probing different phrasings of the same question, which is a natural thing to reach for and is exactly what the deleted multi-arm files did — will feed arm 1's exchange into arm 2's history. The reader sees three results, and would reasonably read them as three independent measurements when they are a chain. Given the whole point is to distinguish a prompt effect from an artifact, that is the wrong default to leave undocumented. I am not suggesting bringing the flag back — that is the machinery the rescope correctly deleted, and one case file per arm is the simpler answer. What I would do is spend one sentence on it in 3.
Flagging it only so the constraint is on the record rather than discovered by someone whose case behaves oddly: the count is right (one result per call, ids pair correctly), only the content is shared. A sentence on the field's doc comment would settle it. I would not change the data structure — an indexed form would be worse to author for the case it fixes, and YAGNI applies until a case actually needs it. 4.
The behavior is fine and arguably desirable (a 5.
Both committed files are LF-only and tab-free (verified: zero Verified clean
On the line countThe brief asked whether there are justified cuts, given 1,182 lines against a ~300 target. My honest answer is that I found one candidate and withdrew it. The candidate was the ~230 lines of doc comments. But they are not restating the code — they record why the fidelity line sits at the template (ADR-038 gathered findings via The one place I would push back if it grows: RecommendationCOMMENT — approve-in-spirit. One 🟡 worth fixing before merge; the rest are optional. The rescope did what it said. The assertion engine, scoring, batch execution, and chain flag are genuinely gone rather than renamed, the diff is additive-only, the restored Rust tests resolve the prior review's finding 2 more cleanly than the porting route I suggested, and the fidelity claim — which is the entire justification for this being a Rust bin — holds up under line-by-line re-checking of the rewritten The one thing I would fix first is 🟡 1. Not because it breaks anything today — it does not, and both case files run clean — but because a panic in the normalizer is the specific failure this rescope was meant to stop manufacturing: a harness bug that presents to the author as a problem with their prompt. It is a byte-index-versus-char-index choice, a few lines, and one U+3000 in a unit test. Findings 3, 4 and 5 are doc-comment accuracy and a one-character robustness fix that can ride along with it. Finding 2 is one sentence documenting a semantic that is correct but not inferable. None of these are design objections. The shape of the thing is right, and it is simpler than what it replaced. 🤖 Generated with Claude Code |
malibio
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review (re-review): COMMENT (posted as a comment — GitHub blocks self-approval). Full findings in the review comment above.
Found by re-review. `char::is_whitespace` accepts U+00A0, U+2003, and U+3000,
so an indent counted in BYTES and then sliced lands mid-character:
input: "\n\u{3000}alpha\n beta\n"
panic: byte index 2 is not a char boundary; it is inside '\u{3000}'
Unreachable from either committed case file, but the carrier is mundane and
invisible: paste prompt text out of a chat window, get a stray NBSP or
ideographic space, and the likeliest form is a "blank" line of them between
normally-indented lines — filtered from the indent computation, still sliced
by the per-line map.
That matters more than its reachability suggests. This is the one function
that rewrites what the model sees, and its panic names no case file, so an
author's next move would be to suspect their prompt edit. Manufacturing that
particular confusion is what the rescope existed to stop.
Indentation is now counted and skipped in CHARACTERS, which removes the panic
class rather than special-casing the three code points that trigger it. Also
strips a leading CRLF, not just LF.
Verified the rewrite is output-identical on both committed cases: the system
prompt is still the same 211 characters as its restored Rust original, em-dash
intact.
Four new tests: multibyte leading whitespace (U+3000/U+00A0/U+2003, including
the reachable blank-line form), CRLF and tabs and degenerate input, and
multibyte content mid-line.
Also from the re-review, doc accuracy only, no behavior change:
- `turns` now states that turns always chain, so independent probes belong in
separate files
- `tool_results` now states that keying by tool name shares content across
repeated calls to the same tool
- `normalize()` now describes its real scope, which includes `expect`
20 unit tests. test:all exit 0 at baseline; quality:fix and clippy clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Re-review findings addressed — d399ba9🟡 1 — The finding was right and my earlier reasoning was wrong: I'd assumed leading whitespace is always ASCII, but Indentation is now counted and skipped in characters, which removes the panic class rather than special-casing the three code points that reach it. Also strips a leading CRLF (finding 5), not just LF. Verified output-identical on both committed cases after the rewrite — the system prompt is still the same 211 characters as its restored Rust original, em-dash intact. Four new tests: multibyte leading whitespace including the reachable blank-line-of-ideographic-spaces form; CRLF, tabs, and degenerate input ( 🟢 2, 3, 4 — doc accuracy, no behavior change. 🟢 5 — folded into the 🟡 1 fix. 20 unit tests. On the reviewer's line-count noteAgreed, and worth recording: the ~230 doc-comment lines carry why — ADR-038/046 on where the fidelity line sits, the 6/7/7 matrix variance behind Scope note for whoever picks this upThis PR delivers the utility and two ported case files. The prompts in them are reproductions of the existing Rust goldens, run 3/3 against the real model to confirm the port is faithful — they are not newly tuned prompts. No edit-rerun-compare loop has been run. Authoring tuned prompts is downstream work. One genuine negative result did fall out of running it, and it is cheaply reproducible now: in the sequence case, turn 3 resolves and then stops — it never continues to 🤖 Generated with Claude Code |
Running /address-review properly (it was skipped last round — findings were applied by hand, so the triage and re-review decision never happened) turned up an overclaim in the previous commit. d399ba9's message said stripping a leading CRLF was a robustness fix. It is not: `.lines()` already strips a trailing `\r` from every line, so the opening `strip_prefix` only ever removes the blank first line, and both branch orderings are behaviorally identical. Verified by running the two side by side over CRLF-at-head, mid, and tail inputs — SAME on all four. The reviewer's suggested ordering is still the better-reading one and is now used, but the comment says what it actually does instead of implying a bug that was not there. Also demonstrated, rather than asserted, that the two normalizer regression tests fail when their fix is removed: - multibyte: reverting to byte-slicing gives "byte index 1 is not a char boundary; it is inside '\u{3000}'" — the test bites. - CRLF: reverting the prefix order does NOT fail the test, because there is no bug to catch. The test was rewritten to assert the guarantee that actually holds (no `\r` survives, via `.lines()`) and to name where it comes from, instead of implying it guards the prefix order. That second one is the point of checking: a regression test that passes with the fix removed is worse than no test, because it reads as coverage. 21 unit tests. test:all exit 0 at baseline; quality:fix and clippy clean. Both case files still parse byte-identical — system prompt 211 chars, em-dash intact. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
| # | Sev | Finding | Disposition |
|---|---|---|---|
| 1 | 🟡 | normalize_case_text panics on multibyte leading whitespace |
Addressed (d399ba9), and now proven — see below |
| 2 | 🟢 | Unconditional chaining misleads a multi-arm case file | Addressed — doc sentence on turns |
| 3 | 🟢 | tool_results keyed by tool name collapses repeated calls |
Addressed — doc sentence |
| 4 | 🟢 | expect normalized though the model never sees it |
Addressed — normalize() doc now states its real scope |
| 5 | 🟢 | strip_prefix('\n') misses a CRLF leading blank line |
Addressed, and my earlier claim about it corrected |
Addressed 5, skipped 0.
The correction (2f15f53)
d399ba9's message presented finding 5 as a robustness fix. It isn't. .lines() already strips a trailing \r from every line, so the opening strip_prefix only ever removes the blank first line — and both branch orderings are behaviorally identical. Ran the two side by side over CRLF at head, middle, and tail: SAME on all four inputs.
The reviewer's ordering is still the better-reading one and is now used, but the comment describes what it does rather than implying a bug that was never there.
Proving the regression tests bite
Both normalizer tests were checked by removing their fix and confirming failure:
- multibyte — reverting to byte-slicing produces
byte index 1 is not a char boundary; it is inside '\u{3000}'. The test bites. - CRLF — reverting the prefix order produces no failure, because there is no bug to catch. So that test was rewritten to assert the guarantee that actually holds (no
\rsurvives, via.lines()) and to name where it comes from, rather than implying it guards the prefix ordering.
A regression test that still passes with its fix removed is worse than no test: it reads as coverage. Worth checking rather than assuming — I'd have shipped the false version otherwise.
State
21 unit tests. test:all exit 0 at baseline (4704 frontend / 24 browser / 46 Rust suites), quality:fix and clippy clean. Both case files parse byte-identical — system prompt 211 chars, em-dash intact.
Re-Review Decision
Decision: NO RE-REVIEW NEEDED
Rationale: The one 🟡 was already reviewed as a fix in d399ba9 and is unchanged in substance; this round only strengthened its evidence. 2f15f539 contains no behavior change at all — the two strip_prefix orderings are demonstrably equivalent — plus one rewritten test assertion and comment accuracy. Findings 2–4 are doc-only. Nothing new was introduced, no logic moved, and the full gate is green at baseline.
The PR is ready for merge, pending your decision. Not merging — that's your call.
🤖 Generated with Claude Code
A small utility that reads a case file, calls the model through NodeSpace's real inference path, and prints what came back. The human reads the output and judges it.
The case files are the deliverable — a set of prompt strings that reliably get the right tool call, which the assembly pipeline is then engineered to reproduce. The bin is scaffolding for producing them.
What's here
src/golden/case.rssrc/golden/runner.rssrc/bin/golden_runner.rssrc/golden/mod.rsgoldens/*.toml~420 lines of logic, ~250 of tests, ~230 of doc comments.
Fidelity — the load-bearing constraint
Goes through
LlamaChatInferenceEngine→nlp-engine::ChatEngine, soapply_chat_template(PEG_GEMMA4),chat/parser.rs, andrepair_tool_call_argumentsare production's own, the last applied at the same parse boundaryagent_loop.rs:1370applies it.Out of the path: daemon, DB, gRPC,
LocalAgentLoop,PromptAssembler, routing.This is why it's a Rust bin and not curl against
llama-server: ADR-038's findings were gathered that way and ADR-046 recorded they did not transfer through NodeSpace's inference path.No assertions, by design
No expectation kinds, no scoring, no pass/fail. Nothing automated consumes expectations here — the snapshot gate holds production to a frozen golden, and the matrix does end-to-end scoring. A third scoring system would duplicate both, and would be new surface for the failure mode this project has already paid for: a harness bug reported as a model failure.
expectsurvives as an inert free-text field, printed beside the result and never compared, so a tuned case can answer "what was this tuned to produce?" from the file itself. Free-text on purpose — the moment it has structure, something will assert on it.One real bug worth calling out
TOML's
"""does not strip indentation the way Rust's"…\does. Without normalization every prompt reaches the model with a leading newline, a per-line indent, and doubled spaces at each continuation. One ported history block measured 35 characters longer than the Rust literal it reproduces.For a utility whose entire premise is the file is exactly what the model sees, that's the one defect that silently poisons every result it produces.
normalize_case_textfixes it; 4 tests pin it, including that relative indentation and paragraph breaks survive (narrative shape is sometimes the variable under test).Verification
Both cases run 3/3 against the locked native model:
Byte-identical across all reps. Turn 2 picks up turn 1's actual schema name — the isolated Rust turns guessed
equipmentandequipment_item, neither of which turn 1 emitted.Recorded honestly in the case notes:
node_typeis not stable across sessions (equipmentone session,Productanother). Nothing in that case tells the model what types exist, so it invents one. The tool name is the signal.Also observed and left for its own issue: turn 3 resolves and then stops rather than continuing to
update_node— the single-turn-pull boundary.bun run test:all— exit 0, 4704 frontend + 24 browser + 46 Rust suites, matches baselinebun run quality:fix— clean, no collateral reformattingcargo clippy -p nodespace-agent --all-targets— no warningsNot touched
golden_scenario6_handauthored.rsandgolden_scenario6_sequence.rsstay intests/as the validated Rust-side reference proving the runner is faithful to production templating and parsing. Earlier commits deleted them and repointed citations in the daemon; both reverted.To capture the post-template string for the snapshot gate,
NODESPACE_PROMPT_DUMPworks unchanged when running this bin — it hooks the chokepoint innlp-engine'schat/mod.rsand needs nothing from here. Noted in the bin's docs.🤖 Generated with Claude Code