Summary
- What broke:
maintainer-standup-persist.ts fails to extract the state JSON when the synthesize node emits more than one ARCHON_STATE_JSON_BEGIN block (e.g. a truncated/aborted block followed by a complete one). The script uses indexOf(BEGIN) and indexOf(END), which pairs BEGIN[0] with END[0] and slices across the embedded second BEGIN marker — yielding invalid JSON. The bash node exits 1 and the workflow is reported as failed, even though a usable brief and state were produced.
- When it started (if known): pre-existing — surfaces whenever the synthesize node (Claude Sonnet, for
maintainer-standup) restarts its output mid-emission. Observed today (2026-05-14) during a normal standup run after the synth node ran ~20 minutes.
- Severity:
major
Steps to Reproduce
- Run
bun run cli workflow run maintainer-standup. Wait for the synthesize node to finish.
- If Claude emits the state block twice (e.g. partial + restart), the persist node fails with
Bash node 'persist' failed [exit 1].
- The brief and state files are NOT written. The workflow exits non-zero.
Today's reproducer: synth output had two ARCHON_STATE_JSON_BEGIN lines and one ARCHON_STATE_JSON_END. The slice between BEGIN[0] and END[0] contained [truncated state JSON]\n\nARCHON_STATE_JSON_BEGIN\n[complete state JSON], which JSON.parse can't accept. Tier-2 (JSON-wrapper fallback) also fails because it tries to parse starting at the first {, which is inside the truncated copy.
Expected vs Actual
- Expected: When the synthesize output contains the markers at least once correctly, persist should extract the last complete
BEGIN..END block (treat earlier emissions as preamble) and write the brief and state.
- Actual: persist exits 1, no files are written, and the user must hand-extract the brief and state from the run log.
User Flow
User Archon synthesize (Claude) persist (bash)
──── ────────────────────────── ───────────────
runs maintainer-standup ─▶ emits partial state ──┐
then restarts: │ ──▶ reads stdin
emits complete state │ indexOf(BEGIN) → first (partial)
│ indexOf(END) → only one
▼ slice spans both blocks
JSON.parse fails
[X] tier-2 also fails on truncated leading {
exits 1
sees "Workflow failed" ◀── persist failed; no brief, no state.json written
Environment
- Platform: CLI (also reproducible from any caller — the bug is in the persist script)
- Database: SQLite
- Running in worktree? No (
worktree.enabled: false)
- OS: macOS / Linux
Logs
{"level":40,"module":"workflow.dag-executor","exitCode":1,"killed":false,
"stderrTail":"...truncated state JSON...ARCHON_STATE_JSON_END\\n--- END raw output ---",
"nodeId":"persist","nodeType":"bash","isTimeout":false,"msg":"dag_node_failed"}
[persist] Failed: Bash node 'persist' failed [exit 1]: ...
Raw synth output (markers only, line numbers from one observed run):
59: # Maintainer Standup — 2026-05-14
241: ARCHON_STATE_JSON_BEGIN ← partial / aborted state JSON below
244: ARCHON_STATE_JSON_BEGIN ← restart; complete state JSON below
246: ARCHON_STATE_JSON_END
Impact
- Affected workflows/commands:
maintainer-standup, maintainer-standup-minimax (same persist script). Any future workflow that uses delimiter-based JSON extraction with indexOf is vulnerable to the same pattern.
- Reproduction rate: Intermittent — depends on whether the synth model restarts mid-emission. Observed once today. Cost is high when it does happen (~20-minute synth call is wasted because persist throws away the output).
- Workaround: Hand-extract brief markdown and state JSON from the run log and write them manually to
.archon/maintainer-standup/. Tedious but possible because the data IS in the log.
- Data loss risk: No (raw run output is preserved on disk).
Scope
- Package(s) likely involved: workflow-script (project-level,
.archon/scripts/maintainer-standup-persist.ts)
- Not in
@archon/workflows itself — fix is in the project script, not the engine.
Proposed fix
Switch the tier-1 extractor from indexOf to a paired-marker scan that returns the last valid BEGIN..END block. Pseudocode:
// Find the last END, then the last BEGIN before it.
const endIdx = raw.lastIndexOf(END);
if (endIdx !== -1) {
const beginIdx = raw.lastIndexOf(BEGIN, endIdx);
if (beginIdx !== -1 && beginIdx < endIdx) {
const stateText = raw.slice(beginIdx + BEGIN.length, endIdx).trim();
state = JSON.parse(stateText);
brief = raw.slice(0, beginIdx).trim();
source = 'delimiter';
}
}
This makes the script robust to:
- Multiple
BEGIN emissions before the END (today's failure mode — model restarted).
- A trailing
BEGIN after END that the model forgot to close (the last-END-then-look-back rule still picks the correct pair).
The tier-2 JSON-wrapper fallback can stay as-is, but should probably scan for the last { followed by valid JSON, not the first, for the same reason.
Optional follow-up: surface a warning when more than one BEGIN was seen so we know when the model is misbehaving, even though we recovered.
Related
The synthesize-node behavior of duplicating output mid-emission is its own concern, but fixing the parser is the right first move — the persist script's contract is "tolerate model output that contains the markers somewhere," and indexOf violates that contract whenever there's more than one BEGIN.
Summary
maintainer-standup-persist.tsfails to extract the state JSON when the synthesize node emits more than oneARCHON_STATE_JSON_BEGINblock (e.g. a truncated/aborted block followed by a complete one). The script usesindexOf(BEGIN)andindexOf(END), which pairsBEGIN[0]withEND[0]and slices across the embedded secondBEGINmarker — yielding invalid JSON. The bash node exits 1 and the workflow is reported as failed, even though a usable brief and state were produced.maintainer-standup) restarts its output mid-emission. Observed today (2026-05-14) during a normal standup run after the synth node ran ~20 minutes.majorSteps to Reproduce
bun run cli workflow run maintainer-standup. Wait for the synthesize node to finish.Bash node 'persist' failed [exit 1].Today's reproducer: synth output had two
ARCHON_STATE_JSON_BEGINlines and oneARCHON_STATE_JSON_END. The slice betweenBEGIN[0]andEND[0]contained[truncated state JSON]\n\nARCHON_STATE_JSON_BEGIN\n[complete state JSON], whichJSON.parsecan't accept. Tier-2 (JSON-wrapper fallback) also fails because it tries to parse starting at the first{, which is inside the truncated copy.Expected vs Actual
BEGIN..ENDblock (treat earlier emissions as preamble) and write the brief and state.User Flow
Environment
worktree.enabled: false)Logs
Raw synth output (markers only, line numbers from one observed run):
Impact
maintainer-standup,maintainer-standup-minimax(same persist script). Any future workflow that uses delimiter-based JSON extraction withindexOfis vulnerable to the same pattern..archon/maintainer-standup/. Tedious but possible because the data IS in the log.Scope
.archon/scripts/maintainer-standup-persist.ts)@archon/workflowsitself — fix is in the project script, not the engine.Proposed fix
Switch the tier-1 extractor from
indexOfto a paired-marker scan that returns the last validBEGIN..ENDblock. Pseudocode:This makes the script robust to:
BEGINemissions before theEND(today's failure mode — model restarted).BEGINafterENDthat the model forgot to close (the last-END-then-look-back rule still picks the correct pair).The tier-2 JSON-wrapper fallback can stay as-is, but should probably scan for the last
{followed by valid JSON, not the first, for the same reason.Optional follow-up: surface a warning when more than one
BEGINwas seen so we know when the model is misbehaving, even though we recovered.Related
The synthesize-node behavior of duplicating output mid-emission is its own concern, but fixing the parser is the right first move — the persist script's contract is "tolerate model output that contains the markers somewhere," and
indexOfviolates that contract whenever there's more than one BEGIN.