fabro/run.json
Fabro 69abcb72b5 checkpoint
⚒️ Generated with [Fabro](https://fabro.sh)
2026-07-23 20:33:25 +00:00

2944 lines
No EOL
392 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"title": "Per-Branch Fidelity for Parallel Branches — Implementation Plan",
"spec": {
"run_id": "01KY7Y01REECZ24XXTMBZ3PPV9",
"settings": {
"project": {
"name": null,
"description": null,
"metadata": {}
},
"workflow": {
"name": null,
"description": null,
"graph": "workflow.fabro",
"metadata": {}
},
"run": {
"goal": {
"type": "inline",
"value": "# Per-Branch Fidelity for Parallel Branches — Implementation Plan\n\n## Context\n\nParallel branch nodes are dispatched via `dispatch_handler`, bypassing `FidelityLifecycle::before_node` (`lib/crates/fabro-workflow/src/lifecycle/fidelity.rs:76-180`) — the only place fidelity is resolved and preambles are built. Every branch therefore inherits the stale `current.preamble` copied at `context.fork()` (`handler/parallel.rs:226`), and `fidelity`/`thread_id` on branch nodes or `fork -> branch` edges are silently ignored. Confirmed live on the testing server (probes `01KY7KRA7E…`/`01KY7KRAAW…`, 2026-07-23): a `truncate` branch received the identical compact preamble as its default sibling; fork-level fidelity works and is the current workaround. Design reviewed via the Quarry doc \"Fix: Per-Branch Fidelity for Parallel Branches\".\n\nWhy not run the lifecycle per branch: it is a single-token state machine (one-slot `incoming_edge_data` baton, singleton context keys written to shared run state); concurrent invocation would corrupt run state. And `build_preamble` (`handler/llm/preamble.rs:24`, public and pure) needs `state.completed_nodes`/`state.node_outcomes`, which only the lifecycle sees. So: **pre-render per-branch preambles in the lifecycle, hand off to the handler via one context key.**\n\n## Semantics (final, after design pressure-test)\n\n- **Explicit-only resolution.** A branch's fidelity comes from the `fork -> branch` edge attr, else the branch node attr, else **no entry** — the branch inherits the fork's preamble via `fork()` exactly as today. The fork's own resolved fidelity is never re-applied per branch; this keeps the default path byte-identical even when the fork resolves `Full` (where re-derivation would have wrongly degraded every branch).\n- **`full` degrades to `summary:high`** (`Fidelity::degraded()`, `fabro-graphviz/src/fidelity.rs:35-40`) — applied only to *explicitly set* branch fidelity, with a log line (per `docs/internal/logging-strategy.md` — read before writing it).\n- **Equality skip**: if the branch's post-degradation fidelity equals the fork's post-degradation fidelity, store no entry (avoid redundant renders).\n- **`thread_id` stays inert in branches** (concurrent branches must never share an LLM session).\n- **`CURRENT_NODE` in branch contexts stays inherited (fork id).** The pressure-test showed changing it would re-attribute every branch-internal event's stage scope (`context.rs:185-190` → `StageScope::for_handler` used by all handlers) with a visit mismatch against `for_parallel_branch` scoping. The Quarry doc's \"bookkeeping keys describe the branch\" line is consciously deferred to a separate change with proper visit accounting.\n- **`simulate()` untouched.** No simulated handler reads preambles; partial mirroring would risk nested-parallel stash misreads. All-or-nothing → nothing.\n- **Stash shape**: `Value::Array`, length = branch count, `Null` = inherit, else `{\"fidelity\": \"...\", \"preamble\": \"...\"}`. Array length ≠ edge count → treat as absent (legacy). Keyed by edge index; `graph.outgoing_edges` is an ordered Vec filter (`fabro-types/src/graph.rs:393-395`) and lifecycle + handler share the same `Arc<GvGraph>`, so indices align deterministically (including two edges to the same target).\n\n## Implementation steps (ordered; tree compiles at each step)\n\n1. **`lib/crates/fabro-workflow/src/context.rs`** — add `pub const INTERNAL_PARALLEL_BRANCH_PREAMBLES: &str = \"internal.parallel_branch_preambles\";` to `keys`. The `internal.` prefix already excludes it from preamble rendering (`preamble.rs:99-109`) and child→parent propagation (`context.rs:80-85`).\n\n2. **`lib/crates/fabro-workflow/src/artifact.rs`** — strip the new key in `durable_context_snapshot` (`:81`) and `normalize_checkpoint_for_resume` (`:101`), beside `CURRENT_PREAMBLE`. Without this, every post-parallel checkpoint and `CheckpointCompleted` event payload carries the full per-branch preamble map (a `summary:high` preamble embeds up to 50 lines of every command output — multiplied per branch).\n\n3. **`lib/crates/fabro-workflow/src/lifecycle/fidelity.rs`** — in `before_node`:\n - Set the stash key to `Null` on `state.context` **first**, before the two fallible `resolve_*` calls, so the always-overwritten invariant holds on every early-return path.\n - After the existing preamble build, when `gv_node.handler_type() == Some(\"parallel\")`: iterate `self.graph.outgoing_edges(node.id())` in order; per edge resolve explicit fidelity (edge attr → target-node attr → none); apply `degraded()` to explicit values (log when it was `full`); push `Null` for inherit/equal-to-fork, else render `build_preamble(final_fidelity, …)` reusing the already-resolved snapshot (blobs resolved once at `:113-128`) and push the entry. Set the array on the stash key.\n - Extract the per-branch resolution as a pure helper beside `resolve_fidelity` (`:206`, same module — no visibility change) for unit testing.\n\n4. **`lib/crates/fabro-workflow/src/handler/parallel.rs`** — in `execute()`'s branch-setup loop (insert after `:238`, where `INTERNAL_PARALLEL_BRANCH_ID` is set):\n - Read the stash from the parent context once before the loop; `None`, `Some(Null)`, or length-mismatch all mean strict legacy behavior (note: `Context::get` returns `Some(Null)` for a Nulled key — both must be treated as absent).\n - Per branch with an entry: `branch_context.set(CURRENT_PREAMBLE, preamble)` and `branch_context.set(INTERNAL_FIDELITY, fidelity)`. Downstream needs nothing: `agent.rs:244`/`prompt.rs:63` read `context.preamble()`.\n - In **every** branch fork, set the stash key to `Null` — load-bearing, not hygiene: a nested parallel branch target reads its fork's stash, and without the Null it would misinterpret the outer node's array as its own.\n - After the loop, set the stash key to `Null` on the handler's own context — the write-back diff (`node_handler.rs:99-105`) clears `state.context` so the post-parallel checkpoint carries Null even before the artifact strip.\n\n5. **`lib/crates/fabro-validate/src/rules/parallel_branch_inert_attribute.rs`** — drop `\"fidelity\"` from `BRANCH_IGNORED_ATTRS` and its `fix_message` arm; add a narrow diagnostic in its place: `fidelity=\"full\"` on a fork→branch edge or branch-only node warns \"parallel branches run at most at summary:high; full is degraded at runtime because branches cannot share a session\". Other fidelity values now lint clean. Update doc comment (the snapshot rationale now applies to `thread_id` only) and tests.\n\n6. **`lib/crates/fabro-validate/src/rules/thread_id_requires_fidelity_full.rs`** — skip fork→branch edges and branch-only nodes (factor the branch-only detection from rule 5 into a shared helper). Today it tells branch nodes with `thread_id` to *add* `fidelity=\"full\"` — advice that, post-change, would actively alter runtime behavior while the other rule says \"remove thread_id\". Defer to the inert-attribute rule's guidance on branches.\n\n7. **Docs** — `docs/public/execution/context.mdx` (fidelity precedence: branch edge → branch node → inherit fork; per-branch preamble rendering; `thread_id` inert in branches; `full` degradation), `docs/public/workflows/stages-and-nodes.mdx` (parallel fan-out section + fidelity attribute notes), `docs/public/reference/dot-language.mdx` (edge/node attr rows). Optional changelog entry via the changelog conventions.\n\n## Tests\n\nPer `docs/internal/testing-strategy.md`, preamble content is implementation-facing → `fabro-workflow`, not CLI layers.\n\n- **Pure unit tests** (`lifecycle/fidelity.rs` tests, beside `resolve_fidelity`'s at `:271-321`): explicit edge > node precedence; no-attr → inherit (no entry); explicit `full` → `summary:high` entry; branch fidelity equal to fork's (post-degradation) → no entry; fork resolved `Full` + no branch attrs → no entries at all.\n- **Lifecycle-level**: two consecutive `before_node` calls on the same parallel node rebuild (not merge) the stash; non-parallel node overwrites stash to Null; resume-degrade flag interaction (fork degrades, fallback branches still get no entry).\n- **`artifact.rs` tests** (`:519+` pattern): both snapshot functions strip the stash key.\n- **Parallel handler unit tests** (`handler/parallel.rs` tests module, `EngineServices::test_default()` + recording handler mirroring `PreambleEchoHandler`, `manager_loop.rs:973-1043`): entry applies `CURRENT_PREAMBLE`/`INTERNAL_FIDELITY` to the right branch by index; stash Null in every branch fork; `Some(Null)`/absent/length-mismatch → legacy; duplicate-target edges get distinct entries at indices 0/1 (no-git test — a pre-existing worktree-name collision exists for that topology, don't let it pollute the assertion); existing tests stay unmodified as the legacy guard.\n- **Engine-level regression** in `lib/crates/fabro-workflow/tests/it/integration.rs` beside the `fidelity_prompt_*` tests (`:9245-9479`), reusing `FidelityCapturingHandler` (`:4917-4974`) and the `end_to_end_parallel_fan_out_fan_in` scaffold (`:2441-2480`) via `WorkflowRunner::run_with_state`:\n - **Probe A analog** (`parallel_branches_get_per_branch_preambles_by_fidelity`): seed sets a context marker → fork → `branch_a` (`fidelity=\"truncate\"`) + `branch_b` (default) → fan-in. Assert branch_a's preamble is goal-only (no marker) while branch_b's contains the marker.\n - **Probe B analog**: `fidelity=\"truncate\"` on the fork only → both branches goal-only (compat guarantee, unchanged behavior).\n - Edge-attr-beats-node-attr variant.\n- **Lint tests**: no warning for non-full branch fidelity; warning for branch `fidelity=\"full\"`; `thread_id_requires_fidelity_full` silent on branch-only nodes, still firing elsewhere.\n\n## Verification\n\n- `cargo nextest run -p fabro-workflow -p fabro-validate`, then `ulimit -n 4096 && cargo nextest run --workspace` (do not export `FORCE_COLOR`).\n- `cargo +nightly-2026-04-14 fmt --check --all`; `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`.\n- Live confirmation on the testing server: re-run the two probe workflows (session scratchpad `probes/isolation-a`, `probes/isolation-b`) against a locally built binary — probe A's `stage.prompt` events must now show differentiated branch preambles; probe B byte-identical to before.\n\n## Compatibility\n\n| Situation | Impact |\n|---|---|\n| No fidelity attrs near the parallel node | None — byte-identical (inherit path, no re-render) |\n| Fidelity on the fork node / its incoming edge | None — fork snapshot semantics unchanged |\n| Previously-dead attrs on branch nodes / fork→branch edges | Start working (the fix) |\n| `full` on a branch | Degrades to `summary:high` + log + lint warning |\n| `thread_id` on a branch | Still inert; lint still warns; the contradictory companion lint goes quiet on branches |\n\n## Decisions (user-confirmed 2026-07-23)\n\n1. **`CURRENT_NODE` in branch contexts stays inherited** — the branch-scoped bookkeeping change is deferred to a dedicated event-attribution change.\n2. **The narrow `fidelity=\"full\"` branch lint is in scope** (step 5 stands as written).\n3. **No changelog entry in this PR** — changelog handled in the usual batch.\n"
},
"working_dir": null,
"metadata": {},
"inputs": {},
"model": {
"provider": "openrouter",
"name": "anthropic/claude-sonnet-4-6",
"fallbacks": [],
"controls": {
"reasoning_effort": null,
"speed": null
}
},
"git": {
"author": null
},
"prepare": {
"steps": [],
"timeout_ms": 300000
},
"execution": {
"mode": "normal",
"approval": "prompt"
},
"checkpoint": {
"exclude_globs": [],
"skip_git_hooks": false,
"commit_timeout_ms": 30000
},
"clone": {
"enabled": true
},
"run_branch": {
"enabled": true,
"push": true
},
"meta_branch": {
"enabled": true,
"push": true
},
"environment": {
"id": "fabro-dev",
"provider": "daytona",
"image": {
"docker": null,
"dockerfile": {
"type": "inline",
"value": "FROM ubuntu:24.04\n\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n curl git ripgrep ca-certificates build-essential pkg-config libssl-dev unzip python3 \\\n xvfb xfce4 xfce4-terminal x11vnc novnc dbus-x11 \\\n libx11-6 libxrandr2 libxext6 libxrender1 libxfixes3 libxss1 libxtst6 libxi6 \\\n && rm -rf /var/lib/apt/lists/*\n\n# Install real Chromium (not the snap stub) via xtradeb PPA\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n software-properties-common curl gnupg \\\n && add-apt-repository -y ppa:xtradeb/apps \\\n && apt-get update \\\n && apt-get install -y --no-install-recommends chromium \\\n && rm -rf /var/lib/apt/lists/*\n\n# Wrapper: Chromium needs --no-sandbox when running as root in a container,\n# and --disable-dev-shm-usage avoids crashes from small /dev/shm\nRUN printf '#!/bin/bash\\nexec /usr/bin/chromium --no-sandbox --disable-dev-shm-usage \"$@\"\\n' \\\n > /usr/local/bin/chromium-wrapper \\\n && chmod +x /usr/local/bin/chromium-wrapper\n\n# Make the wrapper the default in the system .desktop file and via alternatives\nRUN sed -i 's|^Exec=.*|Exec=/usr/local/bin/chromium-wrapper %U|' \\\n /usr/share/applications/chromium.desktop \\\n && update-alternatives --install /usr/bin/x-www-browser x-www-browser \\\n /usr/local/bin/chromium-wrapper 100\n\n# Tell XFCE's exo-open that Chromium is the WebBrowser helper (system-wide)\nRUN mkdir -p /etc/xdg/xfce4 /usr/share/xfce4/helpers \\\n && printf 'WebBrowser=custom-WebBrowser\\n' > /etc/xdg/xfce4/helpers.rc \\\n && printf '[Desktop Entry]\\n\\\nVersion=1.0\\n\\\nType=X-XFCE-Helper\\n\\\nName=Chromium\\n\\\nIcon=chromium\\n\\\nX-XFCE-Category=WebBrowser\\n\\\nX-XFCE-CommandsWithParameter=/usr/local/bin/chromium-wrapper \"%%s\"\\n\\\nX-XFCE-Commands=/usr/local/bin/chromium-wrapper\\n' \\\n > /usr/share/xfce4/helpers/custom-WebBrowser.desktop\n\n# GitHub CLI\nRUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \\\n | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \\\n && echo \"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main\" \\\n | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \\\n && apt-get update && apt-get install -y --no-install-recommends gh \\\n && rm -rf /var/lib/apt/lists/*\n\n# Rust\nRUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y\nENV PATH=\"/root/.cargo/bin:${PATH}\"\nRUN rustup toolchain install nightly-2026-04-14 --profile minimal --component clippy,rustfmt\nRUN cargo install cargo-nextest --locked\nENV CARGO_INCREMENTAL=0\n\n# Bun\nRUN curl -fsSL https://bun.sh/install | bash\nENV PATH=\"/root/.bun/bin:${PATH}\"\n\nWORKDIR /root\n"
}
},
"resources": {
"cpu": 8,
"memory": "16GB",
"disk": "20GB"
},
"network": {
"mode": "allow_all",
"allow": []
},
"lifecycle": {
"preserve": false,
"stop_on_terminal": true,
"auto_stop": "30m"
},
"labels": {
"repo": "fabro-sh/fabro"
},
"env": {}
},
"notifications": {
"feed": {
"enabled": true,
"provider": "slack",
"events": [
"run.started",
"run.completed",
"run.failed"
],
"slack": {
"channel": "#feed-fabro"
}
}
},
"interviews": {
"provider": null,
"slack": null
},
"agent": {
"fabro_tools": false,
"permissions": null,
"mcps": {}
},
"hooks": [],
"scm": {
"provider": null,
"owner": null,
"repository": null,
"github": null
},
"pull_request": {
"enabled": true,
"draft": false,
"auto_merge": false,
"merge_strategy": "squash"
},
"artifacts": {
"include": []
},
"integrations": {
"github": {
"permissions": {}
}
}
}
},
"graph": {
"name": "ImplementPlan",
"nodes": {
"exit": {
"id": "exit",
"attrs": {
"shape": {
"String": "Msquare"
},
"label": {
"String": "Exit"
}
}
},
"fix_lints": {
"id": "fix_lints",
"attrs": {
"provider": {
"String": "openrouter"
},
"max_visits": {
"Integer": 3
},
"prompt": {
"String": "The preflight lint step failed. Read the build output from context and fix all clippy lint warnings."
},
"reasoning_effort": {
"String": "xhigh"
},
"label": {
"String": "Fix Lints"
},
"model": {
"String": "anthropic/claude-fable-5"
}
}
},
"fixup": {
"id": "fixup",
"attrs": {
"reasoning_effort": {
"String": "xhigh"
},
"provider": {
"String": "openrouter"
},
"max_visits": {
"Integer": 3
},
"label": {
"String": "Fixup"
},
"model": {
"String": "anthropic/claude-fable-5"
},
"prompt": {
"String": "The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures."
}
}
},
"start": {
"id": "start",
"attrs": {
"shape": {
"String": "Mdiamond"
},
"label": {
"String": "Start"
}
}
},
"implement": {
"id": "implement",
"attrs": {
"model": {
"String": "openai/gpt-5.6-sol"
},
"label": {
"String": "Implement"
},
"prompt": {
"String": "Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."
},
"reasoning_effort": {
"String": "xhigh"
},
"provider": {
"String": "openrouter"
}
}
},
"simplify_fable": {
"id": "simplify_fable",
"attrs": {
"reasoning_effort": {
"String": "xhigh"
},
"provider": {
"String": "openrouter"
},
"model": {
"String": "anthropic/claude-fable-5"
},
"label": {
"String": "Simplify (Claude Fable 5)"
},
"prompt": {
"String": "# Simplify: Code Review and Cleanup\n\nReview all changed files for reuse, quality, and efficiency. Fix any issues found.\n\n## Phase 1: Identify Changes\n\nRun \\`git diff\\` (or \\`git diff HEAD\\` if there are staged changes) to see what changed. If there are no git changes, review the most recently modified files that the user mentioned or that you edited earlier in this conversation.\n\n## Phase 2: Launch Three Review Agents in Parallel\n\nUse the ${AGENT_TOOL_NAME} tool to launch all three agents concurrently in a single message. Pass each agent the full diff so it has the complete context.\n\n### Agent 1: Code Reuse Review\n\nFor each change:\n\n1. **Search for existing utilities and helpers** that could replace newly written code. Look for similar patterns elsewhere in the codebase — common locations are utility directories, shared modules, and files adjacent to the changed ones.\n2. **Flag any new function that duplicates existing functionality.** Suggest the existing function to use instead.\n3. **Flag any inline logic that could use an existing utility** — hand-rolled string manipulation, manual path handling, custom environment checks, ad-hoc type guards, and similar patterns are common candidates.\n\n### Agent 2: Code Quality Review\n\nReview the same changes for hacky patterns:\n\n1. **Redundant state**: state that duplicates existing state, cached values that could be derived, observers/effects that could be direct calls\n2. **Parameter sprawl**: adding new parameters to a function instead of generalizing or restructuring existing ones\n3. **Copy-paste with slight variation**: near-duplicate code blocks that should be unified with a shared abstraction\n4. **Leaky abstractions**: exposing internal details that should be encapsulated, or breaking existing abstraction boundaries\n5. **Stringly-typed code**: using raw strings where constants, enums (string unions), or branded types already exist in the codebase\n6. **Unnecessary JSX nesting**: wrapper Boxes/elements that add no layout value — check if inner component props (flexShrink, alignItems, etc.) already provide the needed behavior\n7. **Unnecessary comments**: comments explaining WHAT the code does (well-named identifiers already do that), narrating the change, or referencing the task/caller — delete; keep only non-obvious WHY (hidden constraints, subtle invariants, workarounds)\n\n### Agent 3: Efficiency Review\n\nReview the same changes for efficiency:\n\n1. **Unnecessary work**: redundant computations, repeated file reads, duplicate network/API calls, N+1 patterns\n2. **Missed concurrency**: independent operations run sequentially when they could run in parallel\n3. **Hot-path bloat**: new blocking work added to startup or per-request/per-render hot paths\n4. **Recurring no-op updates**: state/store updates inside polling loops, intervals, or event handlers that fire unconditionally — add a change-detection guard so downstream consumers aren't notified when nothing changed. Also: if a wrapper function takes an updater/reducer callback, verify it honors same-reference returns (or whatever the \"no change\" signal is) — otherwise callers' early-return no-ops are silently defeated\n5. **Unnecessary existence checks**: pre-checking file/resource existence before operating (TOCTOU anti-pattern) — operate directly and handle the error\n6. **Memory**: unbounded data structures, missing cleanup, event listener leaks\n7. **Overly broad operations**: reading entire files when only a portion is needed, loading all items when filtering for one\n\n## Phase 3: Fix Issues\n\nWait for all three agents to complete. Aggregate their findings and fix each issue directly. If a finding is a false positive or not worth addressing, note it and move on — do not argue with the finding, just skip it.\n\nWhen done, briefly summarize what was fixed (or confirm the code was already clean).\n"
}
}
},
"simplify_sol": {
"id": "simplify_sol",
"attrs": {
"prompt": {
"String": "# Simplify: Code Review and Cleanup\n\nReview all changed files for reuse, quality, and efficiency. Fix any issues found.\n\n## Phase 1: Identify Changes\n\nRun \\`git diff\\` (or \\`git diff HEAD\\` if there are staged changes) to see what changed. If there are no git changes, review the most recently modified files that the user mentioned or that you edited earlier in this conversation.\n\n## Phase 2: Launch Three Review Agents in Parallel\n\nUse the ${AGENT_TOOL_NAME} tool to launch all three agents concurrently in a single message. Pass each agent the full diff so it has the complete context.\n\n### Agent 1: Code Reuse Review\n\nFor each change:\n\n1. **Search for existing utilities and helpers** that could replace newly written code. Look for similar patterns elsewhere in the codebase — common locations are utility directories, shared modules, and files adjacent to the changed ones.\n2. **Flag any new function that duplicates existing functionality.** Suggest the existing function to use instead.\n3. **Flag any inline logic that could use an existing utility** — hand-rolled string manipulation, manual path handling, custom environment checks, ad-hoc type guards, and similar patterns are common candidates.\n\n### Agent 2: Code Quality Review\n\nReview the same changes for hacky patterns:\n\n1. **Redundant state**: state that duplicates existing state, cached values that could be derived, observers/effects that could be direct calls\n2. **Parameter sprawl**: adding new parameters to a function instead of generalizing or restructuring existing ones\n3. **Copy-paste with slight variation**: near-duplicate code blocks that should be unified with a shared abstraction\n4. **Leaky abstractions**: exposing internal details that should be encapsulated, or breaking existing abstraction boundaries\n5. **Stringly-typed code**: using raw strings where constants, enums (string unions), or branded types already exist in the codebase\n6. **Unnecessary JSX nesting**: wrapper Boxes/elements that add no layout value — check if inner component props (flexShrink, alignItems, etc.) already provide the needed behavior\n7. **Unnecessary comments**: comments explaining WHAT the code does (well-named identifiers already do that), narrating the change, or referencing the task/caller — delete; keep only non-obvious WHY (hidden constraints, subtle invariants, workarounds)\n\n### Agent 3: Efficiency Review\n\nReview the same changes for efficiency:\n\n1. **Unnecessary work**: redundant computations, repeated file reads, duplicate network/API calls, N+1 patterns\n2. **Missed concurrency**: independent operations run sequentially when they could run in parallel\n3. **Hot-path bloat**: new blocking work added to startup or per-request/per-render hot paths\n4. **Recurring no-op updates**: state/store updates inside polling loops, intervals, or event handlers that fire unconditionally — add a change-detection guard so downstream consumers aren't notified when nothing changed. Also: if a wrapper function takes an updater/reducer callback, verify it honors same-reference returns (or whatever the \"no change\" signal is) — otherwise callers' early-return no-ops are silently defeated\n5. **Unnecessary existence checks**: pre-checking file/resource existence before operating (TOCTOU anti-pattern) — operate directly and handle the error\n6. **Memory**: unbounded data structures, missing cleanup, event listener leaks\n7. **Overly broad operations**: reading entire files when only a portion is needed, loading all items when filtering for one\n\n## Phase 3: Fix Issues\n\nWait for all three agents to complete. Aggregate their findings and fix each issue directly. If a finding is a false positive or not worth addressing, note it and move on — do not argue with the finding, just skip it.\n\nWhen done, briefly summarize what was fixed (or confirm the code was already clean).\n"
},
"label": {
"String": "Simplify (GPT-5.6 Sol)"
},
"reasoning_effort": {
"String": "max"
},
"provider": {
"String": "openrouter"
},
"model": {
"String": "openai/gpt-5.6-sol"
}
}
},
"verify": {
"id": "verify",
"attrs": {
"shape": {
"String": "parallelogram"
},
"goal_gate": {
"Boolean": true
},
"retry_target": {
"String": "fixup"
},
"label": {
"String": "Verify"
},
"script": {
"String": "git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\\bActorRef\\b|\\bActorKind\\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\\s*==\\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1"
}
}
},
"preflight_compile": {
"id": "preflight_compile",
"attrs": {
"shape": {
"String": "parallelogram"
},
"label": {
"String": "Preflight Compile"
},
"max_retries": {
"Integer": 0
},
"script": {
"String": "cargo check -q --workspace 2>&1"
}
}
},
"toolchain": {
"id": "toolchain",
"attrs": {
"shape": {
"String": "parallelogram"
},
"max_retries": {
"Integer": 0
},
"label": {
"String": "Toolchain"
},
"script": {
"String": "command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1"
}
}
},
"preflight_lint": {
"id": "preflight_lint",
"attrs": {
"label": {
"String": "Preflight Lint"
},
"max_retries": {
"Integer": 0
},
"script": {
"String": "cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1"
},
"shape": {
"String": "parallelogram"
}
}
}
},
"edges": [
{
"from": "start",
"to": "toolchain",
"attrs": {}
},
{
"from": "toolchain",
"to": "preflight_compile",
"attrs": {
"condition": {
"String": "outcome=succeeded"
}
}
},
{
"from": "toolchain",
"to": "exit",
"attrs": {}
},
{
"from": "preflight_compile",
"to": "preflight_lint",
"attrs": {
"condition": {
"String": "outcome=succeeded"
}
}
},
{
"from": "preflight_compile",
"to": "exit",
"attrs": {}
},
{
"from": "preflight_lint",
"to": "implement",
"attrs": {
"condition": {
"String": "outcome=succeeded"
}
}
},
{
"from": "preflight_lint",
"to": "fix_lints",
"attrs": {}
},
{
"from": "fix_lints",
"to": "preflight_lint",
"attrs": {}
},
{
"from": "implement",
"to": "simplify_fable",
"attrs": {}
},
{
"from": "simplify_fable",
"to": "simplify_sol",
"attrs": {}
},
{
"from": "simplify_sol",
"to": "verify",
"attrs": {}
},
{
"from": "verify",
"to": "exit",
"attrs": {
"condition": {
"String": "outcome=succeeded"
}
}
},
{
"from": "verify",
"to": "fixup",
"attrs": {}
},
{
"from": "fixup",
"to": "verify",
"attrs": {}
}
],
"attrs": {
"rankdir": {
"String": "LR"
},
"goal": {
"String": "# Per-Branch Fidelity for Parallel Branches — Implementation Plan\n\n## Context\n\nParallel branch nodes are dispatched via `dispatch_handler`, bypassing `FidelityLifecycle::before_node` (`lib/crates/fabro-workflow/src/lifecycle/fidelity.rs:76-180`) — the only place fidelity is resolved and preambles are built. Every branch therefore inherits the stale `current.preamble` copied at `context.fork()` (`handler/parallel.rs:226`), and `fidelity`/`thread_id` on branch nodes or `fork -> branch` edges are silently ignored. Confirmed live on the testing server (probes `01KY7KRA7E…`/`01KY7KRAAW…`, 2026-07-23): a `truncate` branch received the identical compact preamble as its default sibling; fork-level fidelity works and is the current workaround. Design reviewed via the Quarry doc \"Fix: Per-Branch Fidelity for Parallel Branches\".\n\nWhy not run the lifecycle per branch: it is a single-token state machine (one-slot `incoming_edge_data` baton, singleton context keys written to shared run state); concurrent invocation would corrupt run state. And `build_preamble` (`handler/llm/preamble.rs:24`, public and pure) needs `state.completed_nodes`/`state.node_outcomes`, which only the lifecycle sees. So: **pre-render per-branch preambles in the lifecycle, hand off to the handler via one context key.**\n\n## Semantics (final, after design pressure-test)\n\n- **Explicit-only resolution.** A branch's fidelity comes from the `fork -> branch` edge attr, else the branch node attr, else **no entry** — the branch inherits the fork's preamble via `fork()` exactly as today. The fork's own resolved fidelity is never re-applied per branch; this keeps the default path byte-identical even when the fork resolves `Full` (where re-derivation would have wrongly degraded every branch).\n- **`full` degrades to `summary:high`** (`Fidelity::degraded()`, `fabro-graphviz/src/fidelity.rs:35-40`) — applied only to *explicitly set* branch fidelity, with a log line (per `docs/internal/logging-strategy.md` — read before writing it).\n- **Equality skip**: if the branch's post-degradation fidelity equals the fork's post-degradation fidelity, store no entry (avoid redundant renders).\n- **`thread_id` stays inert in branches** (concurrent branches must never share an LLM session).\n- **`CURRENT_NODE` in branch contexts stays inherited (fork id).** The pressure-test showed changing it would re-attribute every branch-internal event's stage scope (`context.rs:185-190` → `StageScope::for_handler` used by all handlers) with a visit mismatch against `for_parallel_branch` scoping. The Quarry doc's \"bookkeeping keys describe the branch\" line is consciously deferred to a separate change with proper visit accounting.\n- **`simulate()` untouched.** No simulated handler reads preambles; partial mirroring would risk nested-parallel stash misreads. All-or-nothing → nothing.\n- **Stash shape**: `Value::Array`, length = branch count, `Null` = inherit, else `{\"fidelity\": \"...\", \"preamble\": \"...\"}`. Array length ≠ edge count → treat as absent (legacy). Keyed by edge index; `graph.outgoing_edges` is an ordered Vec filter (`fabro-types/src/graph.rs:393-395`) and lifecycle + handler share the same `Arc<GvGraph>`, so indices align deterministically (including two edges to the same target).\n\n## Implementation steps (ordered; tree compiles at each step)\n\n1. **`lib/crates/fabro-workflow/src/context.rs`** — add `pub const INTERNAL_PARALLEL_BRANCH_PREAMBLES: &str = \"internal.parallel_branch_preambles\";` to `keys`. The `internal.` prefix already excludes it from preamble rendering (`preamble.rs:99-109`) and child→parent propagation (`context.rs:80-85`).\n\n2. **`lib/crates/fabro-workflow/src/artifact.rs`** — strip the new key in `durable_context_snapshot` (`:81`) and `normalize_checkpoint_for_resume` (`:101`), beside `CURRENT_PREAMBLE`. Without this, every post-parallel checkpoint and `CheckpointCompleted` event payload carries the full per-branch preamble map (a `summary:high` preamble embeds up to 50 lines of every command output — multiplied per branch).\n\n3. **`lib/crates/fabro-workflow/src/lifecycle/fidelity.rs`** — in `before_node`:\n - Set the stash key to `Null` on `state.context` **first**, before the two fallible `resolve_*` calls, so the always-overwritten invariant holds on every early-return path.\n - After the existing preamble build, when `gv_node.handler_type() == Some(\"parallel\")`: iterate `self.graph.outgoing_edges(node.id())` in order; per edge resolve explicit fidelity (edge attr → target-node attr → none); apply `degraded()` to explicit values (log when it was `full`); push `Null` for inherit/equal-to-fork, else render `build_preamble(final_fidelity, …)` reusing the already-resolved snapshot (blobs resolved once at `:113-128`) and push the entry. Set the array on the stash key.\n - Extract the per-branch resolution as a pure helper beside `resolve_fidelity` (`:206`, same module — no visibility change) for unit testing.\n\n4. **`lib/crates/fabro-workflow/src/handler/parallel.rs`** — in `execute()`'s branch-setup loop (insert after `:238`, where `INTERNAL_PARALLEL_BRANCH_ID` is set):\n - Read the stash from the parent context once before the loop; `None`, `Some(Null)`, or length-mismatch all mean strict legacy behavior (note: `Context::get` returns `Some(Null)` for a Nulled key — both must be treated as absent).\n - Per branch with an entry: `branch_context.set(CURRENT_PREAMBLE, preamble)` and `branch_context.set(INTERNAL_FIDELITY, fidelity)`. Downstream needs nothing: `agent.rs:244`/`prompt.rs:63` read `context.preamble()`.\n - In **every** branch fork, set the stash key to `Null` — load-bearing, not hygiene: a nested parallel branch target reads its fork's stash, and without the Null it would misinterpret the outer node's array as its own.\n - After the loop, set the stash key to `Null` on the handler's own context — the write-back diff (`node_handler.rs:99-105`) clears `state.context` so the post-parallel checkpoint carries Null even before the artifact strip.\n\n5. **`lib/crates/fabro-validate/src/rules/parallel_branch_inert_attribute.rs`** — drop `\"fidelity\"` from `BRANCH_IGNORED_ATTRS` and its `fix_message` arm; add a narrow diagnostic in its place: `fidelity=\"full\"` on a fork→branch edge or branch-only node warns \"parallel branches run at most at summary:high; full is degraded at runtime because branches cannot share a session\". Other fidelity values now lint clean. Update doc comment (the snapshot rationale now applies to `thread_id` only) and tests.\n\n6. **`lib/crates/fabro-validate/src/rules/thread_id_requires_fidelity_full.rs`** — skip fork→branch edges and branch-only nodes (factor the branch-only detection from rule 5 into a shared helper). Today it tells branch nodes with `thread_id` to *add* `fidelity=\"full\"` — advice that, post-change, would actively alter runtime behavior while the other rule says \"remove thread_id\". Defer to the inert-attribute rule's guidance on branches.\n\n7. **Docs** — `docs/public/execution/context.mdx` (fidelity precedence: branch edge → branch node → inherit fork; per-branch preamble rendering; `thread_id` inert in branches; `full` degradation), `docs/public/workflows/stages-and-nodes.mdx` (parallel fan-out section + fidelity attribute notes), `docs/public/reference/dot-language.mdx` (edge/node attr rows). Optional changelog entry via the changelog conventions.\n\n## Tests\n\nPer `docs/internal/testing-strategy.md`, preamble content is implementation-facing → `fabro-workflow`, not CLI layers.\n\n- **Pure unit tests** (`lifecycle/fidelity.rs` tests, beside `resolve_fidelity`'s at `:271-321`): explicit edge > node precedence; no-attr → inherit (no entry); explicit `full` → `summary:high` entry; branch fidelity equal to fork's (post-degradation) → no entry; fork resolved `Full` + no branch attrs → no entries at all.\n- **Lifecycle-level**: two consecutive `before_node` calls on the same parallel node rebuild (not merge) the stash; non-parallel node overwrites stash to Null; resume-degrade flag interaction (fork degrades, fallback branches still get no entry).\n- **`artifact.rs` tests** (`:519+` pattern): both snapshot functions strip the stash key.\n- **Parallel handler unit tests** (`handler/parallel.rs` tests module, `EngineServices::test_default()` + recording handler mirroring `PreambleEchoHandler`, `manager_loop.rs:973-1043`): entry applies `CURRENT_PREAMBLE`/`INTERNAL_FIDELITY` to the right branch by index; stash Null in every branch fork; `Some(Null)`/absent/length-mismatch → legacy; duplicate-target edges get distinct entries at indices 0/1 (no-git test — a pre-existing worktree-name collision exists for that topology, don't let it pollute the assertion); existing tests stay unmodified as the legacy guard.\n- **Engine-level regression** in `lib/crates/fabro-workflow/tests/it/integration.rs` beside the `fidelity_prompt_*` tests (`:9245-9479`), reusing `FidelityCapturingHandler` (`:4917-4974`) and the `end_to_end_parallel_fan_out_fan_in` scaffold (`:2441-2480`) via `WorkflowRunner::run_with_state`:\n - **Probe A analog** (`parallel_branches_get_per_branch_preambles_by_fidelity`): seed sets a context marker → fork → `branch_a` (`fidelity=\"truncate\"`) + `branch_b` (default) → fan-in. Assert branch_a's preamble is goal-only (no marker) while branch_b's contains the marker.\n - **Probe B analog**: `fidelity=\"truncate\"` on the fork only → both branches goal-only (compat guarantee, unchanged behavior).\n - Edge-attr-beats-node-attr variant.\n- **Lint tests**: no warning for non-full branch fidelity; warning for branch `fidelity=\"full\"`; `thread_id_requires_fidelity_full` silent on branch-only nodes, still firing elsewhere.\n\n## Verification\n\n- `cargo nextest run -p fabro-workflow -p fabro-validate`, then `ulimit -n 4096 && cargo nextest run --workspace` (do not export `FORCE_COLOR`).\n- `cargo +nightly-2026-04-14 fmt --check --all`; `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`.\n- Live confirmation on the testing server: re-run the two probe workflows (session scratchpad `probes/isolation-a`, `probes/isolation-b`) against a locally built binary — probe A's `stage.prompt` events must now show differentiated branch preambles; probe B byte-identical to before.\n\n## Compatibility\n\n| Situation | Impact |\n|---|---|\n| No fidelity attrs near the parallel node | None — byte-identical (inherit path, no re-render) |\n| Fidelity on the fork node / its incoming edge | None — fork snapshot semantics unchanged |\n| Previously-dead attrs on branch nodes / fork→branch edges | Start working (the fix) |\n| `full` on a branch | Degrades to `summary:high` + log + lint warning |\n| `thread_id` on a branch | Still inert; lint still warns; the contradictory companion lint goes quiet on branches |\n\n## Decisions (user-confirmed 2026-07-23)\n\n1. **`CURRENT_NODE` in branch contexts stays inherited** — the branch-scoped bookkeeping change is deferred to a dedicated event-attribution change.\n2. **The narrow `fidelity=\"full\"` branch lint is in scope** (step 5 stands as written).\n3. **No changelog entry in this PR** — changelog handled in the usual batch.\n"
}
}
},
"graph_source": "digraph ImplementPlan {\n graph [goal=\"Implement and simplify\"]\n rankdir=LR\n\n start [shape=Mdiamond, label=\"Start\"]\n exit [shape=Msquare, label=\"Exit\"]\n\n toolchain [label=\"Toolchain\", shape=parallelogram, script=\"command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1\", max_retries=0]\n preflight_compile [label=\"Preflight Compile\", shape=parallelogram, script=\"cargo check -q --workspace 2>&1\", max_retries=0]\n preflight_lint [label=\"Preflight Lint\", shape=parallelogram, script=\"cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1\", max_retries=0]\n fix_lints [label=\"Fix Lints\", prompt=\"The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.\", model=\"anthropic/claude-fable-5\", provider=\"openrouter\", reasoning_effort=\"xhigh\", max_visits=3]\n implement [label=\"Implement\", prompt=\"Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.\", model=\"openai/gpt-5.6-sol\", provider=\"openrouter\", reasoning_effort=\"xhigh\"]\n simplify_fable [label=\"Simplify (Claude Fable 5)\", prompt=\"@prompts/simplify.md\", model=\"anthropic/claude-fable-5\", provider=\"openrouter\", reasoning_effort=\"xhigh\"]\n simplify_sol [label=\"Simplify (GPT-5.6 Sol)\", prompt=\"@prompts/simplify.md\", model=\"openai/gpt-5.6-sol\", provider=\"openrouter\", reasoning_effort=\"max\"]\n verify [label=\"Verify\", shape=parallelogram, script=\"git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\\bActorRef\\b|\\bActorKind\\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\\s*==\\s*\\\"disabled\\\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1\", goal_gate=true, retry_target=\"fixup\"]\n fixup [label=\"Fixup\", prompt=\"The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.\", model=\"anthropic/claude-fable-5\", provider=\"openrouter\", reasoning_effort=\"xhigh\", max_visits=3]\n\n start -> toolchain\n toolchain -> preflight_compile [condition=\"outcome=succeeded\"]\n toolchain -> exit\n preflight_compile -> preflight_lint [condition=\"outcome=succeeded\"]\n preflight_compile -> exit\n preflight_lint -> implement [condition=\"outcome=succeeded\"]\n preflight_lint -> fix_lints\n fix_lints -> preflight_lint\n implement -> simplify_fable -> simplify_sol -> verify\n verify -> exit [condition=\"outcome=succeeded\"]\n verify -> fixup\n fixup -> verify\n}\n",
"workflow_slug": "implement-plan",
"source_directory": "/Users/bhelmkamp/p/fabro-sh/fabro",
"provenance": {
"server": {
"version": "0.303.0-nightly.1"
},
"client": {
"user_agent": "fabro-cli/0.302.0-nightly.0",
"name": "fabro-cli",
"version": "0.302.0-nightly.0"
},
"subject": {
"kind": "user",
"identity": {
"issuer": "https://github.com",
"subject": "19"
},
"login": "brynary",
"auth_method": "github",
"avatar_url": "https://avatars.githubusercontent.com/u/19?v=4"
}
},
"manifest_blob": "6adb2a32e7caffbc89104fd8e5b6e36310392b85a8a4de96928aeedb41d053cf",
"definition_blob": "6d1638305831db24ef3d301b91b00bec5f882e430c81d264eccb4f1ac074cf83",
"git": {
"origin_url": "https://github.com/fabro-sh/fabro",
"branch": "main",
"sha": "30d770046a1883bb1e0935ce16ef0f1c5cf4b740",
"dirty": "clean",
"push_outcome": {
"type": "succeeded",
"remote": "origin",
"branch": "main"
}
}
},
"web_url": "https://fabro-testing.walleye-rainbow.ts.net/runs/01KY7Y01REECZ24XXTMBZ3PPV9",
"start": {
"start_time": "2026-07-23T16:47:45.194372139Z",
"run_branch": "fabro/run/01KY7Y01REECZ24XXTMBZ3PPV9",
"base_sha": "30d770046a1883bb1e0935ce16ef0f1c5cf4b740"
},
"status": {
"kind": "running"
},
"status_updated_at": "2026-07-23T20:08:04.324130402Z",
"last_event_at": "2026-07-23T20:29:21.169648609Z",
"pending_control": null,
"checkpoints": [
{
"seq": 20,
"checkpoint": {
"timestamp": "2026-07-23T16:47:47.855104017Z",
"current_node": "start",
"completed_nodes": [
"start"
],
"node_retries": {},
"context_values": {
"internal.work_dir": "/home/daytona/workspace/fabro",
"outcome": "succeeded",
"internal.fidelity": "compact",
"current_node": "start",
"graph.goal": "# Per-Branch Fidelity for Parallel Branches — Implementation Plan\n\n## Context\n\nParallel branch nodes are dispatched via `dispatch_handler`, bypassing `FidelityLifecycle::before_node` (`lib/crates/fabro-workflow/src/lifecycle/fidelity.rs:76-180`) — the only place fidelity is resolved and preambles are built. Every branch therefore inherits the stale `current.preamble` copied at `context.fork()` (`handler/parallel.rs:226`), and `fidelity`/`thread_id` on branch nodes or `fork -> branch` edges are silently ignored. Confirmed live on the testing server (probes `01KY7KRA7E…`/`01KY7KRAAW…`, 2026-07-23): a `truncate` branch received the identical compact preamble as its default sibling; fork-level fidelity works and is the current workaround. Design reviewed via the Quarry doc \"Fix: Per-Branch Fidelity for Parallel Branches\".\n\nWhy not run the lifecycle per branch: it is a single-token state machine (one-slot `incoming_edge_data` baton, singleton context keys written to shared run state); concurrent invocation would corrupt run state. And `build_preamble` (`handler/llm/preamble.rs:24`, public and pure) needs `state.completed_nodes`/`state.node_outcomes`, which only the lifecycle sees. So: **pre-render per-branch preambles in the lifecycle, hand off to the handler via one context key.**\n\n## Semantics (final, after design pressure-test)\n\n- **Explicit-only resolution.** A branch's fidelity comes from the `fork -> branch` edge attr, else the branch node attr, else **no entry** — the branch inherits the fork's preamble via `fork()` exactly as today. The fork's own resolved fidelity is never re-applied per branch; this keeps the default path byte-identical even when the fork resolves `Full` (where re-derivation would have wrongly degraded every branch).\n- **`full` degrades to `summary:high`** (`Fidelity::degraded()`, `fabro-graphviz/src/fidelity.rs:35-40`) — applied only to *explicitly set* branch fidelity, with a log line (per `docs/internal/logging-strategy.md` — read before writing it).\n- **Equality skip**: if the branch's post-degradation fidelity equals the fork's post-degradation fidelity, store no entry (avoid redundant renders).\n- **`thread_id` stays inert in branches** (concurrent branches must never share an LLM session).\n- **`CURRENT_NODE` in branch contexts stays inherited (fork id).** The pressure-test showed changing it would re-attribute every branch-internal event's stage scope (`context.rs:185-190` → `StageScope::for_handler` used by all handlers) with a visit mismatch against `for_parallel_branch` scoping. The Quarry doc's \"bookkeeping keys describe the branch\" line is consciously deferred to a separate change with proper visit accounting.\n- **`simulate()` untouched.** No simulated handler reads preambles; partial mirroring would risk nested-parallel stash misreads. All-or-nothing → nothing.\n- **Stash shape**: `Value::Array`, length = branch count, `Null` = inherit, else `{\"fidelity\": \"...\", \"preamble\": \"...\"}`. Array length ≠ edge count → treat as absent (legacy). Keyed by edge index; `graph.outgoing_edges` is an ordered Vec filter (`fabro-types/src/graph.rs:393-395`) and lifecycle + handler share the same `Arc<GvGraph>`, so indices align deterministically (including two edges to the same target).\n\n## Implementation steps (ordered; tree compiles at each step)\n\n1. **`lib/crates/fabro-workflow/src/context.rs`** — add `pub const INTERNAL_PARALLEL_BRANCH_PREAMBLES: &str = \"internal.parallel_branch_preambles\";` to `keys`. The `internal.` prefix already excludes it from preamble rendering (`preamble.rs:99-109`) and child→parent propagation (`context.rs:80-85`).\n\n2. **`lib/crates/fabro-workflow/src/artifact.rs`** — strip the new key in `durable_context_snapshot` (`:81`) and `normalize_checkpoint_for_resume` (`:101`), beside `CURRENT_PREAMBLE`. Without this, every post-parallel checkpoint and `CheckpointCompleted` event payload carries the full per-branch preamble map (a `summary:high` preamble embeds up to 50 lines of every command output — multiplied per branch).\n\n3. **`lib/crates/fabro-workflow/src/lifecycle/fidelity.rs`** — in `before_node`:\n - Set the stash key to `Null` on `state.context` **first**, before the two fallible `resolve_*` calls, so the always-overwritten invariant holds on every early-return path.\n - After the existing preamble build, when `gv_node.handler_type() == Some(\"parallel\")`: iterate `self.graph.outgoing_edges(node.id())` in order; per edge resolve explicit fidelity (edge attr → target-node attr → none); apply `degraded()` to explicit values (log when it was `full`); push `Null` for inherit/equal-to-fork, else render `build_preamble(final_fidelity, …)` reusing the already-resolved snapshot (blobs resolved once at `:113-128`) and push the entry. Set the array on the stash key.\n - Extract the per-branch resolution as a pure helper beside `resolve_fidelity` (`:206`, same module — no visibility change) for unit testing.\n\n4. **`lib/crates/fabro-workflow/src/handler/parallel.rs`** — in `execute()`'s branch-setup loop (insert after `:238`, where `INTERNAL_PARALLEL_BRANCH_ID` is set):\n - Read the stash from the parent context once before the loop; `None`, `Some(Null)`, or length-mismatch all mean strict legacy behavior (note: `Context::get` returns `Some(Null)` for a Nulled key — both must be treated as absent).\n - Per branch with an entry: `branch_context.set(CURRENT_PREAMBLE, preamble)` and `branch_context.set(INTERNAL_FIDELITY, fidelity)`. Downstream needs nothing: `agent.rs:244`/`prompt.rs:63` read `context.preamble()`.\n - In **every** branch fork, set the stash key to `Null` — load-bearing, not hygiene: a nested parallel branch target reads its fork's stash, and without the Null it would misinterpret the outer node's array as its own.\n - After the loop, set the stash key to `Null` on the handler's own context — the write-back diff (`node_handler.rs:99-105`) clears `state.context` so the post-parallel checkpoint carries Null even before the artifact strip.\n\n5. **`lib/crates/fabro-validate/src/rules/parallel_branch_inert_attribute.rs`** — drop `\"fidelity\"` from `BRANCH_IGNORED_ATTRS` and its `fix_message` arm; add a narrow diagnostic in its place: `fidelity=\"full\"` on a fork→branch edge or branch-only node warns \"parallel branches run at most at summary:high; full is degraded at runtime because branches cannot share a session\". Other fidelity values now lint clean. Update doc comment (the snapshot rationale now applies to `thread_id` only) and tests.\n\n6. **`lib/crates/fabro-validate/src/rules/thread_id_requires_fidelity_full.rs`** — skip fork→branch edges and branch-only nodes (factor the branch-only detection from rule 5 into a shared helper). Today it tells branch nodes with `thread_id` to *add* `fidelity=\"full\"` — advice that, post-change, would actively alter runtime behavior while the other rule says \"remove thread_id\". Defer to the inert-attribute rule's guidance on branches.\n\n7. **Docs** — `docs/public/execution/context.mdx` (fidelity precedence: branch edge → branch node → inherit fork; per-branch preamble rendering; `thread_id` inert in branches; `full` degradation), `docs/public/workflows/stages-and-nodes.mdx` (parallel fan-out section + fidelity attribute notes), `docs/public/reference/dot-language.mdx` (edge/node attr rows). Optional changelog entry via the changelog conventions.\n\n## Tests\n\nPer `docs/internal/testing-strategy.md`, preamble content is implementation-facing → `fabro-workflow`, not CLI layers.\n\n- **Pure unit tests** (`lifecycle/fidelity.rs` tests, beside `resolve_fidelity`'s at `:271-321`): explicit edge > node precedence; no-attr → inherit (no entry); explicit `full` → `summary:high` entry; branch fidelity equal to fork's (post-degradation) → no entry; fork resolved `Full` + no branch attrs → no entries at all.\n- **Lifecycle-level**: two consecutive `before_node` calls on the same parallel node rebuild (not merge) the stash; non-parallel node overwrites stash to Null; resume-degrade flag interaction (fork degrades, fallback branches still get no entry).\n- **`artifact.rs` tests** (`:519+` pattern): both snapshot functions strip the stash key.\n- **Parallel handler unit tests** (`handler/parallel.rs` tests module, `EngineServices::test_default()` + recording handler mirroring `PreambleEchoHandler`, `manager_loop.rs:973-1043`): entry applies `CURRENT_PREAMBLE`/`INTERNAL_FIDELITY` to the right branch by index; stash Null in every branch fork; `Some(Null)`/absent/length-mismatch → legacy; duplicate-target edges get distinct entries at indices 0/1 (no-git test — a pre-existing worktree-name collision exists for that topology, don't let it pollute the assertion); existing tests stay unmodified as the legacy guard.\n- **Engine-level regression** in `lib/crates/fabro-workflow/tests/it/integration.rs` beside the `fidelity_prompt_*` tests (`:9245-9479`), reusing `FidelityCapturingHandler` (`:4917-4974`) and the `end_to_end_parallel_fan_out_fan_in` scaffold (`:2441-2480`) via `WorkflowRunner::run_with_state`:\n - **Probe A analog** (`parallel_branches_get_per_branch_preambles_by_fidelity`): seed sets a context marker → fork → `branch_a` (`fidelity=\"truncate\"`) + `branch_b` (default) → fan-in. Assert branch_a's preamble is goal-only (no marker) while branch_b's contains the marker.\n - **Probe B analog**: `fidelity=\"truncate\"` on the fork only → both branches goal-only (compat guarantee, unchanged behavior).\n - Edge-attr-beats-node-attr variant.\n- **Lint tests**: no warning for non-full branch fidelity; warning for branch `fidelity=\"full\"`; `thread_id_requires_fidelity_full` silent on branch-only nodes, still firing elsewhere.\n\n## Verification\n\n- `cargo nextest run -p fabro-workflow -p fabro-validate`, then `ulimit -n 4096 && cargo nextest run --workspace` (do not export `FORCE_COLOR`).\n- `cargo +nightly-2026-04-14 fmt --check --all`; `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`.\n- Live confirmation on the testing server: re-run the two probe workflows (session scratchpad `probes/isolation-a`, `probes/isolation-b`) against a locally built binary — probe A's `stage.prompt` events must now show differentiated branch preambles; probe B byte-identical to before.\n\n## Compatibility\n\n| Situation | Impact |\n|---|---|\n| No fidelity attrs near the parallel node | None — byte-identical (inherit path, no re-render) |\n| Fidelity on the fork node / its incoming edge | None — fork snapshot semantics unchanged |\n| Previously-dead attrs on branch nodes / fork→branch edges | Start working (the fix) |\n| `full` on a branch | Degrades to `summary:high` + log + lint warning |\n| `thread_id` on a branch | Still inert; lint still warns; the contradictory companion lint goes quiet on branches |\n\n## Decisions (user-confirmed 2026-07-23)\n\n1. **`CURRENT_NODE` in branch contexts stays inherited** — the branch-scoped bookkeeping change is deferred to a dedicated event-attribution change.\n2. **The narrow `fidelity=\"full\"` branch lint is in scope** (step 5 stands as written).\n3. **No changelog entry in this PR** — changelog handled in the usual batch.\n",
"internal.run_id": "01KY7Y01REECZ24XXTMBZ3PPV9",
"failure_class": "",
"failure_signature": "",
"graph.rankdir": "LR",
"internal.node_visit_count": 1,
"internal.retry_count.start": 0,
"internal.thread_id": null
},
"node_outcomes": {
"start": {
"status": "succeeded",
"usage": null
}
},
"next_node_id": "toolchain",
"node_visits": {
"start": 1
}
},
"diff": {}
},
{
"seq": 28,
"checkpoint": {
"timestamp": "2026-07-23T16:47:52.746349014Z",
"current_node": "toolchain",
"completed_nodes": [
"start",
"toolchain"
],
"node_retries": {},
"context_values": {
"graph.goal": "# Per-Branch Fidelity for Parallel Branches — Implementation Plan\n\n## Context\n\nParallel branch nodes are dispatched via `dispatch_handler`, bypassing `FidelityLifecycle::before_node` (`lib/crates/fabro-workflow/src/lifecycle/fidelity.rs:76-180`) — the only place fidelity is resolved and preambles are built. Every branch therefore inherits the stale `current.preamble` copied at `context.fork()` (`handler/parallel.rs:226`), and `fidelity`/`thread_id` on branch nodes or `fork -> branch` edges are silently ignored. Confirmed live on the testing server (probes `01KY7KRA7E…`/`01KY7KRAAW…`, 2026-07-23): a `truncate` branch received the identical compact preamble as its default sibling; fork-level fidelity works and is the current workaround. Design reviewed via the Quarry doc \"Fix: Per-Branch Fidelity for Parallel Branches\".\n\nWhy not run the lifecycle per branch: it is a single-token state machine (one-slot `incoming_edge_data` baton, singleton context keys written to shared run state); concurrent invocation would corrupt run state. And `build_preamble` (`handler/llm/preamble.rs:24`, public and pure) needs `state.completed_nodes`/`state.node_outcomes`, which only the lifecycle sees. So: **pre-render per-branch preambles in the lifecycle, hand off to the handler via one context key.**\n\n## Semantics (final, after design pressure-test)\n\n- **Explicit-only resolution.** A branch's fidelity comes from the `fork -> branch` edge attr, else the branch node attr, else **no entry** — the branch inherits the fork's preamble via `fork()` exactly as today. The fork's own resolved fidelity is never re-applied per branch; this keeps the default path byte-identical even when the fork resolves `Full` (where re-derivation would have wrongly degraded every branch).\n- **`full` degrades to `summary:high`** (`Fidelity::degraded()`, `fabro-graphviz/src/fidelity.rs:35-40`) — applied only to *explicitly set* branch fidelity, with a log line (per `docs/internal/logging-strategy.md` — read before writing it).\n- **Equality skip**: if the branch's post-degradation fidelity equals the fork's post-degradation fidelity, store no entry (avoid redundant renders).\n- **`thread_id` stays inert in branches** (concurrent branches must never share an LLM session).\n- **`CURRENT_NODE` in branch contexts stays inherited (fork id).** The pressure-test showed changing it would re-attribute every branch-internal event's stage scope (`context.rs:185-190` → `StageScope::for_handler` used by all handlers) with a visit mismatch against `for_parallel_branch` scoping. The Quarry doc's \"bookkeeping keys describe the branch\" line is consciously deferred to a separate change with proper visit accounting.\n- **`simulate()` untouched.** No simulated handler reads preambles; partial mirroring would risk nested-parallel stash misreads. All-or-nothing → nothing.\n- **Stash shape**: `Value::Array`, length = branch count, `Null` = inherit, else `{\"fidelity\": \"...\", \"preamble\": \"...\"}`. Array length ≠ edge count → treat as absent (legacy). Keyed by edge index; `graph.outgoing_edges` is an ordered Vec filter (`fabro-types/src/graph.rs:393-395`) and lifecycle + handler share the same `Arc<GvGraph>`, so indices align deterministically (including two edges to the same target).\n\n## Implementation steps (ordered; tree compiles at each step)\n\n1. **`lib/crates/fabro-workflow/src/context.rs`** — add `pub const INTERNAL_PARALLEL_BRANCH_PREAMBLES: &str = \"internal.parallel_branch_preambles\";` to `keys`. The `internal.` prefix already excludes it from preamble rendering (`preamble.rs:99-109`) and child→parent propagation (`context.rs:80-85`).\n\n2. **`lib/crates/fabro-workflow/src/artifact.rs`** — strip the new key in `durable_context_snapshot` (`:81`) and `normalize_checkpoint_for_resume` (`:101`), beside `CURRENT_PREAMBLE`. Without this, every post-parallel checkpoint and `CheckpointCompleted` event payload carries the full per-branch preamble map (a `summary:high` preamble embeds up to 50 lines of every command output — multiplied per branch).\n\n3. **`lib/crates/fabro-workflow/src/lifecycle/fidelity.rs`** — in `before_node`:\n - Set the stash key to `Null` on `state.context` **first**, before the two fallible `resolve_*` calls, so the always-overwritten invariant holds on every early-return path.\n - After the existing preamble build, when `gv_node.handler_type() == Some(\"parallel\")`: iterate `self.graph.outgoing_edges(node.id())` in order; per edge resolve explicit fidelity (edge attr → target-node attr → none); apply `degraded()` to explicit values (log when it was `full`); push `Null` for inherit/equal-to-fork, else render `build_preamble(final_fidelity, …)` reusing the already-resolved snapshot (blobs resolved once at `:113-128`) and push the entry. Set the array on the stash key.\n - Extract the per-branch resolution as a pure helper beside `resolve_fidelity` (`:206`, same module — no visibility change) for unit testing.\n\n4. **`lib/crates/fabro-workflow/src/handler/parallel.rs`** — in `execute()`'s branch-setup loop (insert after `:238`, where `INTERNAL_PARALLEL_BRANCH_ID` is set):\n - Read the stash from the parent context once before the loop; `None`, `Some(Null)`, or length-mismatch all mean strict legacy behavior (note: `Context::get` returns `Some(Null)` for a Nulled key — both must be treated as absent).\n - Per branch with an entry: `branch_context.set(CURRENT_PREAMBLE, preamble)` and `branch_context.set(INTERNAL_FIDELITY, fidelity)`. Downstream needs nothing: `agent.rs:244`/`prompt.rs:63` read `context.preamble()`.\n - In **every** branch fork, set the stash key to `Null` — load-bearing, not hygiene: a nested parallel branch target reads its fork's stash, and without the Null it would misinterpret the outer node's array as its own.\n - After the loop, set the stash key to `Null` on the handler's own context — the write-back diff (`node_handler.rs:99-105`) clears `state.context` so the post-parallel checkpoint carries Null even before the artifact strip.\n\n5. **`lib/crates/fabro-validate/src/rules/parallel_branch_inert_attribute.rs`** — drop `\"fidelity\"` from `BRANCH_IGNORED_ATTRS` and its `fix_message` arm; add a narrow diagnostic in its place: `fidelity=\"full\"` on a fork→branch edge or branch-only node warns \"parallel branches run at most at summary:high; full is degraded at runtime because branches cannot share a session\". Other fidelity values now lint clean. Update doc comment (the snapshot rationale now applies to `thread_id` only) and tests.\n\n6. **`lib/crates/fabro-validate/src/rules/thread_id_requires_fidelity_full.rs`** — skip fork→branch edges and branch-only nodes (factor the branch-only detection from rule 5 into a shared helper). Today it tells branch nodes with `thread_id` to *add* `fidelity=\"full\"` — advice that, post-change, would actively alter runtime behavior while the other rule says \"remove thread_id\". Defer to the inert-attribute rule's guidance on branches.\n\n7. **Docs** — `docs/public/execution/context.mdx` (fidelity precedence: branch edge → branch node → inherit fork; per-branch preamble rendering; `thread_id` inert in branches; `full` degradation), `docs/public/workflows/stages-and-nodes.mdx` (parallel fan-out section + fidelity attribute notes), `docs/public/reference/dot-language.mdx` (edge/node attr rows). Optional changelog entry via the changelog conventions.\n\n## Tests\n\nPer `docs/internal/testing-strategy.md`, preamble content is implementation-facing → `fabro-workflow`, not CLI layers.\n\n- **Pure unit tests** (`lifecycle/fidelity.rs` tests, beside `resolve_fidelity`'s at `:271-321`): explicit edge > node precedence; no-attr → inherit (no entry); explicit `full` → `summary:high` entry; branch fidelity equal to fork's (post-degradation) → no entry; fork resolved `Full` + no branch attrs → no entries at all.\n- **Lifecycle-level**: two consecutive `before_node` calls on the same parallel node rebuild (not merge) the stash; non-parallel node overwrites stash to Null; resume-degrade flag interaction (fork degrades, fallback branches still get no entry).\n- **`artifact.rs` tests** (`:519+` pattern): both snapshot functions strip the stash key.\n- **Parallel handler unit tests** (`handler/parallel.rs` tests module, `EngineServices::test_default()` + recording handler mirroring `PreambleEchoHandler`, `manager_loop.rs:973-1043`): entry applies `CURRENT_PREAMBLE`/`INTERNAL_FIDELITY` to the right branch by index; stash Null in every branch fork; `Some(Null)`/absent/length-mismatch → legacy; duplicate-target edges get distinct entries at indices 0/1 (no-git test — a pre-existing worktree-name collision exists for that topology, don't let it pollute the assertion); existing tests stay unmodified as the legacy guard.\n- **Engine-level regression** in `lib/crates/fabro-workflow/tests/it/integration.rs` beside the `fidelity_prompt_*` tests (`:9245-9479`), reusing `FidelityCapturingHandler` (`:4917-4974`) and the `end_to_end_parallel_fan_out_fan_in` scaffold (`:2441-2480`) via `WorkflowRunner::run_with_state`:\n - **Probe A analog** (`parallel_branches_get_per_branch_preambles_by_fidelity`): seed sets a context marker → fork → `branch_a` (`fidelity=\"truncate\"`) + `branch_b` (default) → fan-in. Assert branch_a's preamble is goal-only (no marker) while branch_b's contains the marker.\n - **Probe B analog**: `fidelity=\"truncate\"` on the fork only → both branches goal-only (compat guarantee, unchanged behavior).\n - Edge-attr-beats-node-attr variant.\n- **Lint tests**: no warning for non-full branch fidelity; warning for branch `fidelity=\"full\"`; `thread_id_requires_fidelity_full` silent on branch-only nodes, still firing elsewhere.\n\n## Verification\n\n- `cargo nextest run -p fabro-workflow -p fabro-validate`, then `ulimit -n 4096 && cargo nextest run --workspace` (do not export `FORCE_COLOR`).\n- `cargo +nightly-2026-04-14 fmt --check --all`; `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`.\n- Live confirmation on the testing server: re-run the two probe workflows (session scratchpad `probes/isolation-a`, `probes/isolation-b`) against a locally built binary — probe A's `stage.prompt` events must now show differentiated branch preambles; probe B byte-identical to before.\n\n## Compatibility\n\n| Situation | Impact |\n|---|---|\n| No fidelity attrs near the parallel node | None — byte-identical (inherit path, no re-render) |\n| Fidelity on the fork node / its incoming edge | None — fork snapshot semantics unchanged |\n| Previously-dead attrs on branch nodes / fork→branch edges | Start working (the fix) |\n| `full` on a branch | Degrades to `summary:high` + log + lint warning |\n| `thread_id` on a branch | Still inert; lint still warns; the contradictory companion lint goes quiet on branches |\n\n## Decisions (user-confirmed 2026-07-23)\n\n1. **`CURRENT_NODE` in branch contexts stays inherited** — the branch-scoped bookkeeping change is deferred to a dedicated event-attribution change.\n2. **The narrow `fidelity=\"full\"` branch lint is in scope** (step 5 stands as written).\n3. **No changelog entry in this PR** — changelog handled in the usual batch.\n",
"internal.retry_count.toolchain": 0,
"outcome": "succeeded",
"internal.fidelity": "compact",
"internal.run_id": "01KY7Y01REECZ24XXTMBZ3PPV9",
"current_node": "toolchain",
"internal.retry_count.start": 0,
"internal.thread_id": "start",
"failure_class": "",
"thread.start.current_node": "toolchain",
"failure_signature": "",
"command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c",
"internal.node_visit_count": 1,
"internal.work_dir": "/home/daytona/workspace/fabro",
"graph.rankdir": "LR"
},
"node_outcomes": {
"toolchain": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c"
},
"notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
"usage": null,
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 0,
"tool_time_ms": 1360,
"active_time_ms": 1360
}
},
"start": {
"status": "succeeded",
"usage": null
}
},
"next_node_id": "preflight_compile",
"git_commit_sha": "a2650503e8517d0bd41c3fd6d09e79d41e709514",
"node_visits": {
"toolchain": 1,
"start": 1
}
},
"diff": {
"summary": {
"files_changed": 0,
"additions": 0,
"deletions": 0
}
}
},
{
"seq": 38,
"checkpoint": {
"timestamp": "2026-07-23T16:50:16.540593968Z",
"current_node": "preflight_compile",
"completed_nodes": [
"start",
"toolchain",
"preflight_compile"
],
"node_retries": {},
"context_values": {
"thread.start.current_node": "toolchain",
"internal.run_id": "01KY7Y01REECZ24XXTMBZ3PPV9",
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
"internal.fidelity": "compact",
"failure_class": "",
"internal.thread_id": "toolchain",
"internal.work_dir": "/home/daytona/workspace/fabro",
"current_node": "preflight_compile",
"failure_signature": "",
"graph.goal": "# Per-Branch Fidelity for Parallel Branches — Implementation Plan\n\n## Context\n\nParallel branch nodes are dispatched via `dispatch_handler`, bypassing `FidelityLifecycle::before_node` (`lib/crates/fabro-workflow/src/lifecycle/fidelity.rs:76-180`) — the only place fidelity is resolved and preambles are built. Every branch therefore inherits the stale `current.preamble` copied at `context.fork()` (`handler/parallel.rs:226`), and `fidelity`/`thread_id` on branch nodes or `fork -> branch` edges are silently ignored. Confirmed live on the testing server (probes `01KY7KRA7E…`/`01KY7KRAAW…`, 2026-07-23): a `truncate` branch received the identical compact preamble as its default sibling; fork-level fidelity works and is the current workaround. Design reviewed via the Quarry doc \"Fix: Per-Branch Fidelity for Parallel Branches\".\n\nWhy not run the lifecycle per branch: it is a single-token state machine (one-slot `incoming_edge_data` baton, singleton context keys written to shared run state); concurrent invocation would corrupt run state. And `build_preamble` (`handler/llm/preamble.rs:24`, public and pure) needs `state.completed_nodes`/`state.node_outcomes`, which only the lifecycle sees. So: **pre-render per-branch preambles in the lifecycle, hand off to the handler via one context key.**\n\n## Semantics (final, after design pressure-test)\n\n- **Explicit-only resolution.** A branch's fidelity comes from the `fork -> branch` edge attr, else the branch node attr, else **no entry** — the branch inherits the fork's preamble via `fork()` exactly as today. The fork's own resolved fidelity is never re-applied per branch; this keeps the default path byte-identical even when the fork resolves `Full` (where re-derivation would have wrongly degraded every branch).\n- **`full` degrades to `summary:high`** (`Fidelity::degraded()`, `fabro-graphviz/src/fidelity.rs:35-40`) — applied only to *explicitly set* branch fidelity, with a log line (per `docs/internal/logging-strategy.md` — read before writing it).\n- **Equality skip**: if the branch's post-degradation fidelity equals the fork's post-degradation fidelity, store no entry (avoid redundant renders).\n- **`thread_id` stays inert in branches** (concurrent branches must never share an LLM session).\n- **`CURRENT_NODE` in branch contexts stays inherited (fork id).** The pressure-test showed changing it would re-attribute every branch-internal event's stage scope (`context.rs:185-190` → `StageScope::for_handler` used by all handlers) with a visit mismatch against `for_parallel_branch` scoping. The Quarry doc's \"bookkeeping keys describe the branch\" line is consciously deferred to a separate change with proper visit accounting.\n- **`simulate()` untouched.** No simulated handler reads preambles; partial mirroring would risk nested-parallel stash misreads. All-or-nothing → nothing.\n- **Stash shape**: `Value::Array`, length = branch count, `Null` = inherit, else `{\"fidelity\": \"...\", \"preamble\": \"...\"}`. Array length ≠ edge count → treat as absent (legacy). Keyed by edge index; `graph.outgoing_edges` is an ordered Vec filter (`fabro-types/src/graph.rs:393-395`) and lifecycle + handler share the same `Arc<GvGraph>`, so indices align deterministically (including two edges to the same target).\n\n## Implementation steps (ordered; tree compiles at each step)\n\n1. **`lib/crates/fabro-workflow/src/context.rs`** — add `pub const INTERNAL_PARALLEL_BRANCH_PREAMBLES: &str = \"internal.parallel_branch_preambles\";` to `keys`. The `internal.` prefix already excludes it from preamble rendering (`preamble.rs:99-109`) and child→parent propagation (`context.rs:80-85`).\n\n2. **`lib/crates/fabro-workflow/src/artifact.rs`** — strip the new key in `durable_context_snapshot` (`:81`) and `normalize_checkpoint_for_resume` (`:101`), beside `CURRENT_PREAMBLE`. Without this, every post-parallel checkpoint and `CheckpointCompleted` event payload carries the full per-branch preamble map (a `summary:high` preamble embeds up to 50 lines of every command output — multiplied per branch).\n\n3. **`lib/crates/fabro-workflow/src/lifecycle/fidelity.rs`** — in `before_node`:\n - Set the stash key to `Null` on `state.context` **first**, before the two fallible `resolve_*` calls, so the always-overwritten invariant holds on every early-return path.\n - After the existing preamble build, when `gv_node.handler_type() == Some(\"parallel\")`: iterate `self.graph.outgoing_edges(node.id())` in order; per edge resolve explicit fidelity (edge attr → target-node attr → none); apply `degraded()` to explicit values (log when it was `full`); push `Null` for inherit/equal-to-fork, else render `build_preamble(final_fidelity, …)` reusing the already-resolved snapshot (blobs resolved once at `:113-128`) and push the entry. Set the array on the stash key.\n - Extract the per-branch resolution as a pure helper beside `resolve_fidelity` (`:206`, same module — no visibility change) for unit testing.\n\n4. **`lib/crates/fabro-workflow/src/handler/parallel.rs`** — in `execute()`'s branch-setup loop (insert after `:238`, where `INTERNAL_PARALLEL_BRANCH_ID` is set):\n - Read the stash from the parent context once before the loop; `None`, `Some(Null)`, or length-mismatch all mean strict legacy behavior (note: `Context::get` returns `Some(Null)` for a Nulled key — both must be treated as absent).\n - Per branch with an entry: `branch_context.set(CURRENT_PREAMBLE, preamble)` and `branch_context.set(INTERNAL_FIDELITY, fidelity)`. Downstream needs nothing: `agent.rs:244`/`prompt.rs:63` read `context.preamble()`.\n - In **every** branch fork, set the stash key to `Null` — load-bearing, not hygiene: a nested parallel branch target reads its fork's stash, and without the Null it would misinterpret the outer node's array as its own.\n - After the loop, set the stash key to `Null` on the handler's own context — the write-back diff (`node_handler.rs:99-105`) clears `state.context` so the post-parallel checkpoint carries Null even before the artifact strip.\n\n5. **`lib/crates/fabro-validate/src/rules/parallel_branch_inert_attribute.rs`** — drop `\"fidelity\"` from `BRANCH_IGNORED_ATTRS` and its `fix_message` arm; add a narrow diagnostic in its place: `fidelity=\"full\"` on a fork→branch edge or branch-only node warns \"parallel branches run at most at summary:high; full is degraded at runtime because branches cannot share a session\". Other fidelity values now lint clean. Update doc comment (the snapshot rationale now applies to `thread_id` only) and tests.\n\n6. **`lib/crates/fabro-validate/src/rules/thread_id_requires_fidelity_full.rs`** — skip fork→branch edges and branch-only nodes (factor the branch-only detection from rule 5 into a shared helper). Today it tells branch nodes with `thread_id` to *add* `fidelity=\"full\"` — advice that, post-change, would actively alter runtime behavior while the other rule says \"remove thread_id\". Defer to the inert-attribute rule's guidance on branches.\n\n7. **Docs** — `docs/public/execution/context.mdx` (fidelity precedence: branch edge → branch node → inherit fork; per-branch preamble rendering; `thread_id` inert in branches; `full` degradation), `docs/public/workflows/stages-and-nodes.mdx` (parallel fan-out section + fidelity attribute notes), `docs/public/reference/dot-language.mdx` (edge/node attr rows). Optional changelog entry via the changelog conventions.\n\n## Tests\n\nPer `docs/internal/testing-strategy.md`, preamble content is implementation-facing → `fabro-workflow`, not CLI layers.\n\n- **Pure unit tests** (`lifecycle/fidelity.rs` tests, beside `resolve_fidelity`'s at `:271-321`): explicit edge > node precedence; no-attr → inherit (no entry); explicit `full` → `summary:high` entry; branch fidelity equal to fork's (post-degradation) → no entry; fork resolved `Full` + no branch attrs → no entries at all.\n- **Lifecycle-level**: two consecutive `before_node` calls on the same parallel node rebuild (not merge) the stash; non-parallel node overwrites stash to Null; resume-degrade flag interaction (fork degrades, fallback branches still get no entry).\n- **`artifact.rs` tests** (`:519+` pattern): both snapshot functions strip the stash key.\n- **Parallel handler unit tests** (`handler/parallel.rs` tests module, `EngineServices::test_default()` + recording handler mirroring `PreambleEchoHandler`, `manager_loop.rs:973-1043`): entry applies `CURRENT_PREAMBLE`/`INTERNAL_FIDELITY` to the right branch by index; stash Null in every branch fork; `Some(Null)`/absent/length-mismatch → legacy; duplicate-target edges get distinct entries at indices 0/1 (no-git test — a pre-existing worktree-name collision exists for that topology, don't let it pollute the assertion); existing tests stay unmodified as the legacy guard.\n- **Engine-level regression** in `lib/crates/fabro-workflow/tests/it/integration.rs` beside the `fidelity_prompt_*` tests (`:9245-9479`), reusing `FidelityCapturingHandler` (`:4917-4974`) and the `end_to_end_parallel_fan_out_fan_in` scaffold (`:2441-2480`) via `WorkflowRunner::run_with_state`:\n - **Probe A analog** (`parallel_branches_get_per_branch_preambles_by_fidelity`): seed sets a context marker → fork → `branch_a` (`fidelity=\"truncate\"`) + `branch_b` (default) → fan-in. Assert branch_a's preamble is goal-only (no marker) while branch_b's contains the marker.\n - **Probe B analog**: `fidelity=\"truncate\"` on the fork only → both branches goal-only (compat guarantee, unchanged behavior).\n - Edge-attr-beats-node-attr variant.\n- **Lint tests**: no warning for non-full branch fidelity; warning for branch `fidelity=\"full\"`; `thread_id_requires_fidelity_full` silent on branch-only nodes, still firing elsewhere.\n\n## Verification\n\n- `cargo nextest run -p fabro-workflow -p fabro-validate`, then `ulimit -n 4096 && cargo nextest run --workspace` (do not export `FORCE_COLOR`).\n- `cargo +nightly-2026-04-14 fmt --check --all`; `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`.\n- Live confirmation on the testing server: re-run the two probe workflows (session scratchpad `probes/isolation-a`, `probes/isolation-b`) against a locally built binary — probe A's `stage.prompt` events must now show differentiated branch preambles; probe B byte-identical to before.\n\n## Compatibility\n\n| Situation | Impact |\n|---|---|\n| No fidelity attrs near the parallel node | None — byte-identical (inherit path, no re-render) |\n| Fidelity on the fork node / its incoming edge | None — fork snapshot semantics unchanged |\n| Previously-dead attrs on branch nodes / fork→branch edges | Start working (the fix) |\n| `full` on a branch | Degrades to `summary:high` + log + lint warning |\n| `thread_id` on a branch | Still inert; lint still warns; the contradictory companion lint goes quiet on branches |\n\n## Decisions (user-confirmed 2026-07-23)\n\n1. **`CURRENT_NODE` in branch contexts stays inherited** — the branch-scoped bookkeeping change is deferred to a dedicated event-attribution change.\n2. **The narrow `fidelity=\"full\"` branch lint is in scope** (step 5 stands as written).\n3. **No changelog entry in this PR** — changelog handled in the usual batch.\n",
"internal.retry_count.preflight_compile": 0,
"outcome": "succeeded",
"internal.node_visit_count": 1,
"thread.toolchain.current_node": "preflight_compile",
"graph.rankdir": "LR",
"internal.retry_count.start": 0,
"internal.retry_count.toolchain": 0
},
"node_outcomes": {
"preflight_compile": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
},
"notes": "Script completed: cargo check -q --workspace 2>&1",
"usage": null,
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 0,
"tool_time_ms": 140243,
"active_time_ms": 140243
}
},
"toolchain": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c"
},
"notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
"usage": null,
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 0,
"tool_time_ms": 1360,
"active_time_ms": 1360
}
},
"start": {
"status": "succeeded",
"usage": null
}
},
"next_node_id": "preflight_lint",
"git_commit_sha": "dfddfb747b5f438c1ad16bcfaae09b7d7d01f018",
"node_visits": {
"toolchain": 1,
"preflight_compile": 1,
"start": 1
}
},
"diff": {
"summary": {
"files_changed": 0,
"additions": 0,
"deletions": 0
}
}
},
{
"seq": 48,
"checkpoint": {
"timestamp": "2026-07-23T16:52:54.790679232Z",
"current_node": "preflight_lint",
"completed_nodes": [
"start",
"toolchain",
"preflight_compile",
"preflight_lint"
],
"node_retries": {},
"context_values": {
"internal.retry_count.preflight_compile": 0,
"internal.thread_id": "preflight_compile",
"thread.toolchain.current_node": "preflight_compile",
"internal.retry_count.toolchain": 0,
"failure_signature": "",
"internal.node_visit_count": 1,
"internal.run_id": "01KY7Y01REECZ24XXTMBZ3PPV9",
"current_node": "preflight_lint",
"internal.fidelity": "compact",
"internal.work_dir": "/home/daytona/workspace/fabro",
"thread.start.current_node": "toolchain",
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
"internal.retry_count.preflight_lint": 0,
"thread.preflight_compile.current_node": "preflight_lint",
"internal.retry_count.start": 0,
"graph.goal": "# Per-Branch Fidelity for Parallel Branches — Implementation Plan\n\n## Context\n\nParallel branch nodes are dispatched via `dispatch_handler`, bypassing `FidelityLifecycle::before_node` (`lib/crates/fabro-workflow/src/lifecycle/fidelity.rs:76-180`) — the only place fidelity is resolved and preambles are built. Every branch therefore inherits the stale `current.preamble` copied at `context.fork()` (`handler/parallel.rs:226`), and `fidelity`/`thread_id` on branch nodes or `fork -> branch` edges are silently ignored. Confirmed live on the testing server (probes `01KY7KRA7E…`/`01KY7KRAAW…`, 2026-07-23): a `truncate` branch received the identical compact preamble as its default sibling; fork-level fidelity works and is the current workaround. Design reviewed via the Quarry doc \"Fix: Per-Branch Fidelity for Parallel Branches\".\n\nWhy not run the lifecycle per branch: it is a single-token state machine (one-slot `incoming_edge_data` baton, singleton context keys written to shared run state); concurrent invocation would corrupt run state. And `build_preamble` (`handler/llm/preamble.rs:24`, public and pure) needs `state.completed_nodes`/`state.node_outcomes`, which only the lifecycle sees. So: **pre-render per-branch preambles in the lifecycle, hand off to the handler via one context key.**\n\n## Semantics (final, after design pressure-test)\n\n- **Explicit-only resolution.** A branch's fidelity comes from the `fork -> branch` edge attr, else the branch node attr, else **no entry** — the branch inherits the fork's preamble via `fork()` exactly as today. The fork's own resolved fidelity is never re-applied per branch; this keeps the default path byte-identical even when the fork resolves `Full` (where re-derivation would have wrongly degraded every branch).\n- **`full` degrades to `summary:high`** (`Fidelity::degraded()`, `fabro-graphviz/src/fidelity.rs:35-40`) — applied only to *explicitly set* branch fidelity, with a log line (per `docs/internal/logging-strategy.md` — read before writing it).\n- **Equality skip**: if the branch's post-degradation fidelity equals the fork's post-degradation fidelity, store no entry (avoid redundant renders).\n- **`thread_id` stays inert in branches** (concurrent branches must never share an LLM session).\n- **`CURRENT_NODE` in branch contexts stays inherited (fork id).** The pressure-test showed changing it would re-attribute every branch-internal event's stage scope (`context.rs:185-190` → `StageScope::for_handler` used by all handlers) with a visit mismatch against `for_parallel_branch` scoping. The Quarry doc's \"bookkeeping keys describe the branch\" line is consciously deferred to a separate change with proper visit accounting.\n- **`simulate()` untouched.** No simulated handler reads preambles; partial mirroring would risk nested-parallel stash misreads. All-or-nothing → nothing.\n- **Stash shape**: `Value::Array`, length = branch count, `Null` = inherit, else `{\"fidelity\": \"...\", \"preamble\": \"...\"}`. Array length ≠ edge count → treat as absent (legacy). Keyed by edge index; `graph.outgoing_edges` is an ordered Vec filter (`fabro-types/src/graph.rs:393-395`) and lifecycle + handler share the same `Arc<GvGraph>`, so indices align deterministically (including two edges to the same target).\n\n## Implementation steps (ordered; tree compiles at each step)\n\n1. **`lib/crates/fabro-workflow/src/context.rs`** — add `pub const INTERNAL_PARALLEL_BRANCH_PREAMBLES: &str = \"internal.parallel_branch_preambles\";` to `keys`. The `internal.` prefix already excludes it from preamble rendering (`preamble.rs:99-109`) and child→parent propagation (`context.rs:80-85`).\n\n2. **`lib/crates/fabro-workflow/src/artifact.rs`** — strip the new key in `durable_context_snapshot` (`:81`) and `normalize_checkpoint_for_resume` (`:101`), beside `CURRENT_PREAMBLE`. Without this, every post-parallel checkpoint and `CheckpointCompleted` event payload carries the full per-branch preamble map (a `summary:high` preamble embeds up to 50 lines of every command output — multiplied per branch).\n\n3. **`lib/crates/fabro-workflow/src/lifecycle/fidelity.rs`** — in `before_node`:\n - Set the stash key to `Null` on `state.context` **first**, before the two fallible `resolve_*` calls, so the always-overwritten invariant holds on every early-return path.\n - After the existing preamble build, when `gv_node.handler_type() == Some(\"parallel\")`: iterate `self.graph.outgoing_edges(node.id())` in order; per edge resolve explicit fidelity (edge attr → target-node attr → none); apply `degraded()` to explicit values (log when it was `full`); push `Null` for inherit/equal-to-fork, else render `build_preamble(final_fidelity, …)` reusing the already-resolved snapshot (blobs resolved once at `:113-128`) and push the entry. Set the array on the stash key.\n - Extract the per-branch resolution as a pure helper beside `resolve_fidelity` (`:206`, same module — no visibility change) for unit testing.\n\n4. **`lib/crates/fabro-workflow/src/handler/parallel.rs`** — in `execute()`'s branch-setup loop (insert after `:238`, where `INTERNAL_PARALLEL_BRANCH_ID` is set):\n - Read the stash from the parent context once before the loop; `None`, `Some(Null)`, or length-mismatch all mean strict legacy behavior (note: `Context::get` returns `Some(Null)` for a Nulled key — both must be treated as absent).\n - Per branch with an entry: `branch_context.set(CURRENT_PREAMBLE, preamble)` and `branch_context.set(INTERNAL_FIDELITY, fidelity)`. Downstream needs nothing: `agent.rs:244`/`prompt.rs:63` read `context.preamble()`.\n - In **every** branch fork, set the stash key to `Null` — load-bearing, not hygiene: a nested parallel branch target reads its fork's stash, and without the Null it would misinterpret the outer node's array as its own.\n - After the loop, set the stash key to `Null` on the handler's own context — the write-back diff (`node_handler.rs:99-105`) clears `state.context` so the post-parallel checkpoint carries Null even before the artifact strip.\n\n5. **`lib/crates/fabro-validate/src/rules/parallel_branch_inert_attribute.rs`** — drop `\"fidelity\"` from `BRANCH_IGNORED_ATTRS` and its `fix_message` arm; add a narrow diagnostic in its place: `fidelity=\"full\"` on a fork→branch edge or branch-only node warns \"parallel branches run at most at summary:high; full is degraded at runtime because branches cannot share a session\". Other fidelity values now lint clean. Update doc comment (the snapshot rationale now applies to `thread_id` only) and tests.\n\n6. **`lib/crates/fabro-validate/src/rules/thread_id_requires_fidelity_full.rs`** — skip fork→branch edges and branch-only nodes (factor the branch-only detection from rule 5 into a shared helper). Today it tells branch nodes with `thread_id` to *add* `fidelity=\"full\"` — advice that, post-change, would actively alter runtime behavior while the other rule says \"remove thread_id\". Defer to the inert-attribute rule's guidance on branches.\n\n7. **Docs** — `docs/public/execution/context.mdx` (fidelity precedence: branch edge → branch node → inherit fork; per-branch preamble rendering; `thread_id` inert in branches; `full` degradation), `docs/public/workflows/stages-and-nodes.mdx` (parallel fan-out section + fidelity attribute notes), `docs/public/reference/dot-language.mdx` (edge/node attr rows). Optional changelog entry via the changelog conventions.\n\n## Tests\n\nPer `docs/internal/testing-strategy.md`, preamble content is implementation-facing → `fabro-workflow`, not CLI layers.\n\n- **Pure unit tests** (`lifecycle/fidelity.rs` tests, beside `resolve_fidelity`'s at `:271-321`): explicit edge > node precedence; no-attr → inherit (no entry); explicit `full` → `summary:high` entry; branch fidelity equal to fork's (post-degradation) → no entry; fork resolved `Full` + no branch attrs → no entries at all.\n- **Lifecycle-level**: two consecutive `before_node` calls on the same parallel node rebuild (not merge) the stash; non-parallel node overwrites stash to Null; resume-degrade flag interaction (fork degrades, fallback branches still get no entry).\n- **`artifact.rs` tests** (`:519+` pattern): both snapshot functions strip the stash key.\n- **Parallel handler unit tests** (`handler/parallel.rs` tests module, `EngineServices::test_default()` + recording handler mirroring `PreambleEchoHandler`, `manager_loop.rs:973-1043`): entry applies `CURRENT_PREAMBLE`/`INTERNAL_FIDELITY` to the right branch by index; stash Null in every branch fork; `Some(Null)`/absent/length-mismatch → legacy; duplicate-target edges get distinct entries at indices 0/1 (no-git test — a pre-existing worktree-name collision exists for that topology, don't let it pollute the assertion); existing tests stay unmodified as the legacy guard.\n- **Engine-level regression** in `lib/crates/fabro-workflow/tests/it/integration.rs` beside the `fidelity_prompt_*` tests (`:9245-9479`), reusing `FidelityCapturingHandler` (`:4917-4974`) and the `end_to_end_parallel_fan_out_fan_in` scaffold (`:2441-2480`) via `WorkflowRunner::run_with_state`:\n - **Probe A analog** (`parallel_branches_get_per_branch_preambles_by_fidelity`): seed sets a context marker → fork → `branch_a` (`fidelity=\"truncate\"`) + `branch_b` (default) → fan-in. Assert branch_a's preamble is goal-only (no marker) while branch_b's contains the marker.\n - **Probe B analog**: `fidelity=\"truncate\"` on the fork only → both branches goal-only (compat guarantee, unchanged behavior).\n - Edge-attr-beats-node-attr variant.\n- **Lint tests**: no warning for non-full branch fidelity; warning for branch `fidelity=\"full\"`; `thread_id_requires_fidelity_full` silent on branch-only nodes, still firing elsewhere.\n\n## Verification\n\n- `cargo nextest run -p fabro-workflow -p fabro-validate`, then `ulimit -n 4096 && cargo nextest run --workspace` (do not export `FORCE_COLOR`).\n- `cargo +nightly-2026-04-14 fmt --check --all`; `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`.\n- Live confirmation on the testing server: re-run the two probe workflows (session scratchpad `probes/isolation-a`, `probes/isolation-b`) against a locally built binary — probe A's `stage.prompt` events must now show differentiated branch preambles; probe B byte-identical to before.\n\n## Compatibility\n\n| Situation | Impact |\n|---|---|\n| No fidelity attrs near the parallel node | None — byte-identical (inherit path, no re-render) |\n| Fidelity on the fork node / its incoming edge | None — fork snapshot semantics unchanged |\n| Previously-dead attrs on branch nodes / fork→branch edges | Start working (the fix) |\n| `full` on a branch | Degrades to `summary:high` + log + lint warning |\n| `thread_id` on a branch | Still inert; lint still warns; the contradictory companion lint goes quiet on branches |\n\n## Decisions (user-confirmed 2026-07-23)\n\n1. **`CURRENT_NODE` in branch contexts stays inherited** — the branch-scoped bookkeeping change is deferred to a dedicated event-attribution change.\n2. **The narrow `fidelity=\"full\"` branch lint is in scope** (step 5 stands as written).\n3. **No changelog entry in this PR** — changelog handled in the usual batch.\n",
"failure_class": "",
"graph.rankdir": "LR",
"outcome": "succeeded"
},
"node_outcomes": {
"preflight_lint": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
},
"notes": "Script completed: cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1",
"usage": null,
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 0,
"tool_time_ms": 154518,
"active_time_ms": 154518
}
},
"toolchain": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c"
},
"notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
"usage": null,
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 0,
"tool_time_ms": 1360,
"active_time_ms": 1360
}
},
"start": {
"status": "succeeded",
"usage": null
},
"preflight_compile": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
},
"notes": "Script completed: cargo check -q --workspace 2>&1",
"usage": null,
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 0,
"tool_time_ms": 140243,
"active_time_ms": 140243
}
}
},
"next_node_id": "implement",
"git_commit_sha": "1879fe0906049f3bd4d113f5de2736c7f3ff5af7",
"node_visits": {
"preflight_lint": 1,
"preflight_compile": 1,
"start": 1,
"toolchain": 1
}
},
"diff": {
"summary": {
"files_changed": 0,
"additions": 0,
"deletions": 0
}
}
},
{
"seq": 1348,
"checkpoint": {
"timestamp": "2026-07-23T17:41:31.844575764Z",
"current_node": "implement",
"completed_nodes": [
"start",
"toolchain",
"preflight_compile",
"preflight_lint",
"implement"
],
"node_retries": {},
"context_values": {
"internal.retry_count.toolchain": 0,
"internal.retry_count.preflight_compile": 0,
"last_stage": "implement",
"failure_class": "",
"outcome": "succeeded",
"last_response": "Implemented the complete per-branch fidelity plan.\n\n### Highlights\n- Added ordered per-branch preamble stash generation and consumption.\n- Implemented edge → branch node → fork inheritance precede",
"internal.work_dir": "/home/daytona/workspace/fabro",
"internal.retry_count.preflight_lint": 0,
"graph.goal": "# Per-Branch Fidelity for Parallel Branches — Implementation Plan\n\n## Context\n\nParallel branch nodes are dispatched via `dispatch_handler`, bypassing `FidelityLifecycle::before_node` (`lib/crates/fabro-workflow/src/lifecycle/fidelity.rs:76-180`) — the only place fidelity is resolved and preambles are built. Every branch therefore inherits the stale `current.preamble` copied at `context.fork()` (`handler/parallel.rs:226`), and `fidelity`/`thread_id` on branch nodes or `fork -> branch` edges are silently ignored. Confirmed live on the testing server (probes `01KY7KRA7E…`/`01KY7KRAAW…`, 2026-07-23): a `truncate` branch received the identical compact preamble as its default sibling; fork-level fidelity works and is the current workaround. Design reviewed via the Quarry doc \"Fix: Per-Branch Fidelity for Parallel Branches\".\n\nWhy not run the lifecycle per branch: it is a single-token state machine (one-slot `incoming_edge_data` baton, singleton context keys written to shared run state); concurrent invocation would corrupt run state. And `build_preamble` (`handler/llm/preamble.rs:24`, public and pure) needs `state.completed_nodes`/`state.node_outcomes`, which only the lifecycle sees. So: **pre-render per-branch preambles in the lifecycle, hand off to the handler via one context key.**\n\n## Semantics (final, after design pressure-test)\n\n- **Explicit-only resolution.** A branch's fidelity comes from the `fork -> branch` edge attr, else the branch node attr, else **no entry** — the branch inherits the fork's preamble via `fork()` exactly as today. The fork's own resolved fidelity is never re-applied per branch; this keeps the default path byte-identical even when the fork resolves `Full` (where re-derivation would have wrongly degraded every branch).\n- **`full` degrades to `summary:high`** (`Fidelity::degraded()`, `fabro-graphviz/src/fidelity.rs:35-40`) — applied only to *explicitly set* branch fidelity, with a log line (per `docs/internal/logging-strategy.md` — read before writing it).\n- **Equality skip**: if the branch's post-degradation fidelity equals the fork's post-degradation fidelity, store no entry (avoid redundant renders).\n- **`thread_id` stays inert in branches** (concurrent branches must never share an LLM session).\n- **`CURRENT_NODE` in branch contexts stays inherited (fork id).** The pressure-test showed changing it would re-attribute every branch-internal event's stage scope (`context.rs:185-190` → `StageScope::for_handler` used by all handlers) with a visit mismatch against `for_parallel_branch` scoping. The Quarry doc's \"bookkeeping keys describe the branch\" line is consciously deferred to a separate change with proper visit accounting.\n- **`simulate()` untouched.** No simulated handler reads preambles; partial mirroring would risk nested-parallel stash misreads. All-or-nothing → nothing.\n- **Stash shape**: `Value::Array`, length = branch count, `Null` = inherit, else `{\"fidelity\": \"...\", \"preamble\": \"...\"}`. Array length ≠ edge count → treat as absent (legacy). Keyed by edge index; `graph.outgoing_edges` is an ordered Vec filter (`fabro-types/src/graph.rs:393-395`) and lifecycle + handler share the same `Arc<GvGraph>`, so indices align deterministically (including two edges to the same target).\n\n## Implementation steps (ordered; tree compiles at each step)\n\n1. **`lib/crates/fabro-workflow/src/context.rs`** — add `pub const INTERNAL_PARALLEL_BRANCH_PREAMBLES: &str = \"internal.parallel_branch_preambles\";` to `keys`. The `internal.` prefix already excludes it from preamble rendering (`preamble.rs:99-109`) and child→parent propagation (`context.rs:80-85`).\n\n2. **`lib/crates/fabro-workflow/src/artifact.rs`** — strip the new key in `durable_context_snapshot` (`:81`) and `normalize_checkpoint_for_resume` (`:101`), beside `CURRENT_PREAMBLE`. Without this, every post-parallel checkpoint and `CheckpointCompleted` event payload carries the full per-branch preamble map (a `summary:high` preamble embeds up to 50 lines of every command output — multiplied per branch).\n\n3. **`lib/crates/fabro-workflow/src/lifecycle/fidelity.rs`** — in `before_node`:\n - Set the stash key to `Null` on `state.context` **first**, before the two fallible `resolve_*` calls, so the always-overwritten invariant holds on every early-return path.\n - After the existing preamble build, when `gv_node.handler_type() == Some(\"parallel\")`: iterate `self.graph.outgoing_edges(node.id())` in order; per edge resolve explicit fidelity (edge attr → target-node attr → none); apply `degraded()` to explicit values (log when it was `full`); push `Null` for inherit/equal-to-fork, else render `build_preamble(final_fidelity, …)` reusing the already-resolved snapshot (blobs resolved once at `:113-128`) and push the entry. Set the array on the stash key.\n - Extract the per-branch resolution as a pure helper beside `resolve_fidelity` (`:206`, same module — no visibility change) for unit testing.\n\n4. **`lib/crates/fabro-workflow/src/handler/parallel.rs`** — in `execute()`'s branch-setup loop (insert after `:238`, where `INTERNAL_PARALLEL_BRANCH_ID` is set):\n - Read the stash from the parent context once before the loop; `None`, `Some(Null)`, or length-mismatch all mean strict legacy behavior (note: `Context::get` returns `Some(Null)` for a Nulled key — both must be treated as absent).\n - Per branch with an entry: `branch_context.set(CURRENT_PREAMBLE, preamble)` and `branch_context.set(INTERNAL_FIDELITY, fidelity)`. Downstream needs nothing: `agent.rs:244`/`prompt.rs:63` read `context.preamble()`.\n - In **every** branch fork, set the stash key to `Null` — load-bearing, not hygiene: a nested parallel branch target reads its fork's stash, and without the Null it would misinterpret the outer node's array as its own.\n - After the loop, set the stash key to `Null` on the handler's own context — the write-back diff (`node_handler.rs:99-105`) clears `state.context` so the post-parallel checkpoint carries Null even before the artifact strip.\n\n5. **`lib/crates/fabro-validate/src/rules/parallel_branch_inert_attribute.rs`** — drop `\"fidelity\"` from `BRANCH_IGNORED_ATTRS` and its `fix_message` arm; add a narrow diagnostic in its place: `fidelity=\"full\"` on a fork→branch edge or branch-only node warns \"parallel branches run at most at summary:high; full is degraded at runtime because branches cannot share a session\". Other fidelity values now lint clean. Update doc comment (the snapshot rationale now applies to `thread_id` only) and tests.\n\n6. **`lib/crates/fabro-validate/src/rules/thread_id_requires_fidelity_full.rs`** — skip fork→branch edges and branch-only nodes (factor the branch-only detection from rule 5 into a shared helper). Today it tells branch nodes with `thread_id` to *add* `fidelity=\"full\"` — advice that, post-change, would actively alter runtime behavior while the other rule says \"remove thread_id\". Defer to the inert-attribute rule's guidance on branches.\n\n7. **Docs** — `docs/public/execution/context.mdx` (fidelity precedence: branch edge → branch node → inherit fork; per-branch preamble rendering; `thread_id` inert in branches; `full` degradation), `docs/public/workflows/stages-and-nodes.mdx` (parallel fan-out section + fidelity attribute notes), `docs/public/reference/dot-language.mdx` (edge/node attr rows). Optional changelog entry via the changelog conventions.\n\n## Tests\n\nPer `docs/internal/testing-strategy.md`, preamble content is implementation-facing → `fabro-workflow`, not CLI layers.\n\n- **Pure unit tests** (`lifecycle/fidelity.rs` tests, beside `resolve_fidelity`'s at `:271-321`): explicit edge > node precedence; no-attr → inherit (no entry); explicit `full` → `summary:high` entry; branch fidelity equal to fork's (post-degradation) → no entry; fork resolved `Full` + no branch attrs → no entries at all.\n- **Lifecycle-level**: two consecutive `before_node` calls on the same parallel node rebuild (not merge) the stash; non-parallel node overwrites stash to Null; resume-degrade flag interaction (fork degrades, fallback branches still get no entry).\n- **`artifact.rs` tests** (`:519+` pattern): both snapshot functions strip the stash key.\n- **Parallel handler unit tests** (`handler/parallel.rs` tests module, `EngineServices::test_default()` + recording handler mirroring `PreambleEchoHandler`, `manager_loop.rs:973-1043`): entry applies `CURRENT_PREAMBLE`/`INTERNAL_FIDELITY` to the right branch by index; stash Null in every branch fork; `Some(Null)`/absent/length-mismatch → legacy; duplicate-target edges get distinct entries at indices 0/1 (no-git test — a pre-existing worktree-name collision exists for that topology, don't let it pollute the assertion); existing tests stay unmodified as the legacy guard.\n- **Engine-level regression** in `lib/crates/fabro-workflow/tests/it/integration.rs` beside the `fidelity_prompt_*` tests (`:9245-9479`), reusing `FidelityCapturingHandler` (`:4917-4974`) and the `end_to_end_parallel_fan_out_fan_in` scaffold (`:2441-2480`) via `WorkflowRunner::run_with_state`:\n - **Probe A analog** (`parallel_branches_get_per_branch_preambles_by_fidelity`): seed sets a context marker → fork → `branch_a` (`fidelity=\"truncate\"`) + `branch_b` (default) → fan-in. Assert branch_a's preamble is goal-only (no marker) while branch_b's contains the marker.\n - **Probe B analog**: `fidelity=\"truncate\"` on the fork only → both branches goal-only (compat guarantee, unchanged behavior).\n - Edge-attr-beats-node-attr variant.\n- **Lint tests**: no warning for non-full branch fidelity; warning for branch `fidelity=\"full\"`; `thread_id_requires_fidelity_full` silent on branch-only nodes, still firing elsewhere.\n\n## Verification\n\n- `cargo nextest run -p fabro-workflow -p fabro-validate`, then `ulimit -n 4096 && cargo nextest run --workspace` (do not export `FORCE_COLOR`).\n- `cargo +nightly-2026-04-14 fmt --check --all`; `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`.\n- Live confirmation on the testing server: re-run the two probe workflows (session scratchpad `probes/isolation-a`, `probes/isolation-b`) against a locally built binary — probe A's `stage.prompt` events must now show differentiated branch preambles; probe B byte-identical to before.\n\n## Compatibility\n\n| Situation | Impact |\n|---|---|\n| No fidelity attrs near the parallel node | None — byte-identical (inherit path, no re-render) |\n| Fidelity on the fork node / its incoming edge | None — fork snapshot semantics unchanged |\n| Previously-dead attrs on branch nodes / fork→branch edges | Start working (the fix) |\n| `full` on a branch | Degrades to `summary:high` + log + lint warning |\n| `thread_id` on a branch | Still inert; lint still warns; the contradictory companion lint goes quiet on branches |\n\n## Decisions (user-confirmed 2026-07-23)\n\n1. **`CURRENT_NODE` in branch contexts stays inherited** — the branch-scoped bookkeeping change is deferred to a dedicated event-attribution change.\n2. **The narrow `fidelity=\"full\"` branch lint is in scope** (step 5 stands as written).\n3. **No changelog entry in this PR** — changelog handled in the usual batch.\n",
"internal.retry_count.implement": 0,
"internal.run_id": "01KY7Y01REECZ24XXTMBZ3PPV9",
"thread.preflight_compile.current_node": "preflight_lint",
"thread.toolchain.current_node": "preflight_compile",
"response.implement": "Implemented the complete per-branch fidelity plan.\n\n### Highlights\n- Added ordered per-branch preamble stash generation and consumption.\n- Implemented edge → branch node → fork inheritance precedence.\n- Explicit branch `full` degrades to `summary:high` with structured logging.\n- Preserved byte-identical fork inheritance for unspecified/equal fidelities.\n- Added strict legacy fallback for missing, null, malformed, non-array, and mismatched stashes.\n- Cleared branch stashes for nested-parallel safety.\n- Removed stash data from checkpoints, durable snapshots, and persisted event contexts.\n- Kept branch `thread_id` inert, `CURRENT_NODE` inherited, and `simulate()` unchanged.\n- Updated validation rules and all three requested documentation pages.\n- Added unit, lifecycle, handler, integration, persistence, and lint regression coverage.\n- No changelog added.\n\n### Verification\n- `cargo nextest run --workspace`: **7,063 passed, 193 skipped**\n- `cargo +nightly-2026-04-14 fmt --check --all`: passed\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`: passed\n- `git diff --check`: passed\n\nLive testing-server probes were not run because the probe definitions and testing-server access were not available in this checkout/session.",
"internal.retry_count.start": 0,
"graph.rankdir": "LR",
"internal.fidelity": "compact",
"current_node": "implement",
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
"internal.node_visit_count": 1,
"thread.preflight_lint.current_node": "implement",
"thread.start.current_node": "toolchain",
"internal.thread_id": "preflight_lint",
"failure_signature": ""
},
"node_outcomes": {
"implement": {
"status": "succeeded",
"context_updates": {
"last_stage": "implement",
"last_response": "Implemented the complete per-branch fidelity plan.\n\n### Highlights\n- Added ordered per-branch preamble stash generation and consumption.\n- Implemented edge → branch node → fork inheritance precede",
"response.implement": "Implemented the complete per-branch fidelity plan.\n\n### Highlights\n- Added ordered per-branch preamble stash generation and consumption.\n- Implemented edge → branch node → fork inheritance precedence.\n- Explicit branch `full` degrades to `summary:high` with structured logging.\n- Preserved byte-identical fork inheritance for unspecified/equal fidelities.\n- Added strict legacy fallback for missing, null, malformed, non-array, and mismatched stashes.\n- Cleared branch stashes for nested-parallel safety.\n- Removed stash data from checkpoints, durable snapshots, and persisted event contexts.\n- Kept branch `thread_id` inert, `CURRENT_NODE` inherited, and `simulate()` unchanged.\n- Updated validation rules and all three requested documentation pages.\n- Added unit, lifecycle, handler, integration, persistence, and lint regression coverage.\n- No changelog added.\n\n### Verification\n- `cargo nextest run --workspace`: **7,063 passed, 193 skipped**\n- `cargo +nightly-2026-04-14 fmt --check --all`: passed\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`: passed\n- `git diff --check`: passed\n\nLive testing-server probes were not run because the probe definitions and testing-server access were not available in this checkout/session."
},
"notes": "Stage completed: implement",
"usage": {
"input": {
"usage": {
"model": {
"provider": "openrouter",
"model_id": "openai/gpt-5.6-sol"
},
"tokens": {
"input_tokens": 394190,
"output_tokens": 32119,
"reasoning_tokens": 32341,
"cache_read_tokens": 13269488,
"cache_write_tokens": 886818
}
},
"facts": {
"algorithm": "openai"
}
}
},
"files_touched": [
"/home/daytona/workspace/fabro/docs/public/execution/context.mdx",
"/home/daytona/workspace/fabro/docs/public/reference/dot-language.mdx",
"/home/daytona/workspace/fabro/docs/public/workflows/stages-and-nodes.mdx",
"/home/daytona/workspace/fabro/lib/crates/fabro-validate/src/rules/mod.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-validate/src/rules/parallel_branch.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-validate/src/rules/parallel_branch_inert_attribute.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-validate/src/rules/thread_id_requires_fidelity_full.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/artifact.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/context.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/handler/parallel.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/lifecycle/event.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/lifecycle/fidelity.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/tests/it/integration.rs"
],
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 1625877,
"tool_time_ms": 1190483,
"active_time_ms": 2816360
}
},
"start": {
"status": "succeeded",
"usage": null
},
"preflight_lint": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
},
"notes": "Script completed: cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1",
"usage": null,
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 0,
"tool_time_ms": 154518,
"active_time_ms": 154518
}
},
"toolchain": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c"
},
"notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
"usage": null,
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 0,
"tool_time_ms": 1360,
"active_time_ms": 1360
}
},
"preflight_compile": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
},
"notes": "Script completed: cargo check -q --workspace 2>&1",
"usage": null,
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 0,
"tool_time_ms": 140243,
"active_time_ms": 140243
}
}
},
"next_node_id": "simplify_fable",
"git_commit_sha": "1a2bd7966dc15ac5228546964879e1b4b577ad85",
"node_visits": {
"implement": 1,
"preflight_lint": 1,
"start": 1,
"toolchain": 1,
"preflight_compile": 1
}
},
"diff": {
"patch": "diff --git a/docs/public/execution/context.mdx b/docs/public/execution/context.mdx\nindex c67012b3c..a08b2d960 100644\n--- a/docs/public/execution/context.mdx\n+++ b/docs/public/execution/context.mdx\n@@ -139,6 +139,16 @@ Fidelity can be set at three levels. The first match wins:\n \n If none of these are set, fidelity defaults to `compact`.\n \n+### Parallel branch fidelity\n+\n+The first node in each parallel branch uses this precedence:\n+\n+1. `fidelity` on the fork-to-branch edge\n+2. `fidelity` on the branch node\n+3. Otherwise, inherit the fork's preamble unchanged\n+\n+Fabro renders any branch-specific preambles before fan-out from the fork's context snapshot, then places them into the isolated branch contexts. An explicit branch-level `full` degrades to `summary:high` because concurrent branches cannot share conversation sessions. `thread_id` on a branch node or fork-to-branch edge is inert.\n+\n ### Full fidelity and threads\n \n `full` fidelity is typically used with `thread_id` to create a shared conversation across multiple nodes. Nodes with the same `thread_id` share a single LLM session, preserving full context continuity:\ndiff --git a/docs/public/reference/dot-language.mdx b/docs/public/reference/dot-language.mdx\nindex f01fbf2ed..3e407242b 100644\n--- a/docs/public/reference/dot-language.mdx\n+++ b/docs/public/reference/dot-language.mdx\n@@ -201,8 +201,8 @@ Start nodes can also be identified by ID (`start` or `Start`). Exit nodes can be\n | `prompt` | String | Task instructions for the LLM. Supports file references with `@path/to/file.md` |\n | `reasoning_effort` | String | `low`, `medium`, or `high` (default: `high`) |\n | `max_tokens` | Integer | Maximum output tokens |\n-| `fidelity` | String | How much prior context is passed: `compact`, `full`, `summary:high`, `summary:medium`, `summary:low`, `truncate` |\n-| `thread_id` | String | Groups nodes into a shared conversation thread |\n+| `fidelity` | String | How much prior context is passed: `compact`, `full`, `summary:high`, `summary:medium`, `summary:low`, `truncate`. On a node entered directly from a parallel fork, this is overridden by the fork-to-branch edge; explicit `full` degrades to `summary:high`. |\n+| `thread_id` | String | Groups nodes into a shared conversation thread. Inert when the node is entered directly from a parallel fork. |\n | `model` | String | Explicit model ID (overrides stylesheet) |\n | `provider` | String | Explicit provider name (overrides stylesheet). Auto-inferred from the model catalog when omitted. |\n | `project_memory` | Boolean | When `true` (default), prompt nodes discover and include project docs (`AGENTS.md`, `CLAUDE.md`, etc.) as a system prompt. Set to `false` to disable. |\n@@ -252,6 +252,8 @@ audit [\n | `join_policy` | String | When the merge can proceed: `wait_all` (default), `first_success` |\n | `max_parallel` | Integer | Maximum concurrent branches (default: 4) |\n \n+For the first node in each branch, `fidelity` resolves from the fork-to-branch edge, then the branch node; without either, the fork preamble is inherited unchanged. Branch-specific preambles are rendered before fan-out from the fork's context snapshot. Concurrent branches cannot share sessions, so explicit branch `full` becomes `summary:high`, and branch-level `thread_id` is inert.\n+\n ### Wait nodes\n \n | Attribute | Type | Description |\n@@ -282,8 +284,8 @@ audit [\n | `label` | String | Display text; also used for human gate option matching |\n | `condition` | String | Boolean expression for conditional routing (see below) |\n | `weight` | Integer | Priority for tiebreaking (higher wins, default: 0) |\n-| `fidelity` | String | Override fidelity level for this transition |\n-| `thread_id` | String | Override thread ID for this transition |\n+| `fidelity` | String | Override fidelity level for this transition. On a fork-to-branch edge, takes precedence over the branch node; explicit `full` degrades to `summary:high`. |\n+| `thread_id` | String | Override thread ID for this transition. Inert on fork-to-branch edges. |\n | `loop_restart` | Boolean | Restart the workflow from this edge's target when taken: stage history and retry counts clear and the context resets to empty (visit counts are kept). Failed outcomes may only take it for `transient_infra` failures — see [Failures](/execution/failures#loop-restart-edges) |\n | `freeform` | Boolean | When `true` on a human-gate edge, accept free-text input instead of fixed choices |\n \ndiff --git a/docs/public/workflows/stages-and-nodes.mdx b/docs/public/workflows/stages-and-nodes.mdx\nindex 1dd0f71ac..1b8c252c2 100644\n--- a/docs/public/workflows/stages-and-nodes.mdx\n+++ b/docs/public/workflows/stages-and-nodes.mdx\n@@ -170,6 +170,8 @@ fork -> quality\n | `join_policy` | When the merge can proceed (see table below) |\n | `max_parallel` | Maximum concurrent branches (default: 4) |\n \n+For each branch's first node, fidelity resolves from the fork-to-branch edge, then the branch node; otherwise it inherits the fork preamble unchanged. Fabro renders branch-specific preambles before fan-out from the fork snapshot. Branch-level `full` degrades to `summary:high` because concurrent branches cannot share sessions, and `thread_id` on a branch node or fork-to-branch edge is inert.\n+\n **Join policies:**\n \n | Policy | Behavior |\ndiff --git a/lib/crates/fabro-validate/src/rules/mod.rs b/lib/crates/fabro-validate/src/rules/mod.rs\nindex 428004240..87075bc17 100644\n--- a/lib/crates/fabro-validate/src/rules/mod.rs\n+++ b/lib/crates/fabro-validate/src/rules/mod.rs\n@@ -12,6 +12,7 @@ mod inert_attribute;\n mod model_support;\n mod node_model_known;\n mod orphan_custom_outcome;\n+mod parallel_branch;\n mod parallel_branch_inert_attribute;\n mod prompt_on_llm_nodes;\n mod random_selection_no_conditions;\ndiff --git a/lib/crates/fabro-validate/src/rules/parallel_branch.rs b/lib/crates/fabro-validate/src/rules/parallel_branch.rs\nnew file mode 100644\nindex 000000000..2f1cba2e7\n--- /dev/null\n+++ b/lib/crates/fabro-validate/src/rules/parallel_branch.rs\n@@ -0,0 +1,56 @@\n+use std::collections::BTreeSet;\n+\n+use fabro_graphviz::graph::{Edge, Graph};\n+\n+pub(super) struct ParallelBranches<'a> {\n+ graph: &'a Graph,\n+ fork_ids: BTreeSet<&'a str>,\n+}\n+\n+impl<'a> ParallelBranches<'a> {\n+ pub(super) fn new(graph: &'a Graph) -> Self {\n+ let fork_ids = graph\n+ .nodes\n+ .values()\n+ .filter(|node| node.handler_type() == Some(\"parallel\"))\n+ .map(|node| node.id.as_str())\n+ .collect();\n+ Self { graph, fork_ids }\n+ }\n+\n+ pub(super) fn is_empty(&self) -> bool {\n+ self.fork_ids.is_empty()\n+ }\n+\n+ pub(super) fn is_fork_edge(&self, edge: &Edge) -> bool {\n+ self.fork_ids.contains(edge.from.as_str())\n+ }\n+\n+ pub(super) fn branch_targets(&self) -> BTreeSet<&str> {\n+ self.graph\n+ .edges\n+ .iter()\n+ .filter(|edge| self.is_fork_edge(edge))\n+ .map(|edge| edge.to.as_str())\n+ .collect()\n+ }\n+\n+ pub(super) fn is_branch_only_node(&self, node_id: &str) -> bool {\n+ self.branch_only_parents(node_id).is_some()\n+ }\n+\n+ pub(super) fn branch_only_parents(&self, node_id: &str) -> Option<Vec<String>> {\n+ let mut incoming = self\n+ .graph\n+ .edges\n+ .iter()\n+ .filter(|edge| edge.to == node_id)\n+ .peekable();\n+ incoming.peek()?;\n+\n+ incoming\n+ .map(|edge| self.is_fork_edge(edge).then(|| edge.from.clone()))\n+ .collect::<Option<BTreeSet<_>>>()\n+ .map(|parents| parents.into_iter().collect())\n+ }\n+}\ndiff --git a/lib/crates/fabro-validate/src/rules/parallel_branch_inert_attribute.rs b/lib/crates/fabro-validate/src/rules/parallel_branch_inert_attribute.rs\nindex eb051281e..da36dbbea 100644\n--- a/lib/crates/fabro-validate/src/rules/parallel_branch_inert_attribute.rs\n+++ b/lib/crates/fabro-validate/src/rules/parallel_branch_inert_attribute.rs\n@@ -1,18 +1,16 @@\n-use std::collections::BTreeSet;\n-\n use fabro_graphviz::graph::Graph;\n \n+use super::parallel_branch::ParallelBranches;\n use crate::{Diagnostic, LintRule, Severity};\n \n pub(super) fn rule() -> Box<dyn LintRule> {\n Box::new(Rule)\n }\n \n-/// Attributes that parallel branch execution does not resolve. Branch nodes\n-/// are dispatched with a snapshot of the context taken when the parallel node\n-/// started, so per-branch `fidelity` never changes what a branch sees, and\n-/// per-branch `thread_id` never replaces the thread inherited in that snapshot.\n-const BRANCH_IGNORED_ATTRS: &[&str] = &[\"fidelity\", \"thread_id\"];\n+/// Attributes that parallel branch execution does not resolve. Per-branch\n+/// preambles now honor fidelity, while `thread_id` remains inert because\n+/// concurrent branches cannot share an LLM session.\n+const BRANCH_IGNORED_ATTRS: &[&str] = &[\"thread_id\"];\n \n struct Rule;\n \n@@ -24,21 +22,8 @@ fn quoted_list(ids: &[String]) -> String {\n .join(\", \")\n }\n \n-fn fix_message(attr: &str, parallel_ids: &[String]) -> String {\n+fn fix_message(attr: &str) -> String {\n match attr {\n- \"fidelity\" => {\n- if parallel_ids.len() == 1 {\n- format!(\n- \"Set fidelity on the parallel node {} (or its incoming edge) to control what every branch sees\",\n- quoted_list(parallel_ids),\n- )\n- } else {\n- format!(\n- \"Set fidelity on the parallel nodes {} (or their incoming edges) to control what every branch sees\",\n- quoted_list(parallel_ids),\n- )\n- }\n- }\n \"thread_id\" => format!(\n \"Remove '{attr}': parallel branches inherit the thread resolved when the parallel node started\"\n ),\n@@ -46,30 +31,49 @@ fn fix_message(attr: &str, parallel_ids: &[String]) -> String {\n }\n }\n \n+fn full_fidelity_message() -> String {\n+ \"parallel branches run at most at summary:high; full is degraded at runtime because branches cannot share a session\".to_string()\n+}\n+\n+fn full_fidelity_fix(parallel_ids: &[String]) -> String {\n+ let parent = if parallel_ids.len() == 1 {\n+ format!(\"parallel node {}\", quoted_list(parallel_ids))\n+ } else {\n+ format!(\"parallel nodes {}\", quoted_list(parallel_ids))\n+ };\n+ format!(\n+ \"Use fidelity=\\\"summary:high\\\" or another lower mode on this branch; to reuse a full session before fan-out, set fidelity=\\\"full\\\" on {parent} or its incoming edge\"\n+ )\n+}\n+\n impl LintRule for Rule {\n fn name(&self) -> &'static str {\n \"parallel_branch_inert_attribute\"\n }\n \n fn apply(&self, graph: &Graph) -> Vec<Diagnostic> {\n- let parallel_ids: BTreeSet<&str> = graph\n- .nodes\n- .values()\n- .filter(|n| n.handler_type() == Some(\"parallel\"))\n- .map(|n| n.id.as_str())\n- .collect();\n- if parallel_ids.is_empty() {\n+ let branches = ParallelBranches::new(graph);\n+ if branches.is_empty() {\n return Vec::new();\n }\n \n let mut diagnostics = Vec::new();\n \n- // Branch edges (parallel node -> branch target) carrying an attribute\n- // that branch dispatch never reads.\n for edge in &graph.edges {\n- if !parallel_ids.contains(edge.from.as_str()) {\n+ if !branches.is_fork_edge(edge) {\n continue;\n }\n+ if edge.fidelity() == Some(\"full\") {\n+ diagnostics.push(Diagnostic {\n+ rule: self.name().to_string(),\n+ severity: Severity::Warning,\n+ message: full_fidelity_message(),\n+ node_id: None,\n+ edge: Some((edge.from.clone(), edge.to.clone())),\n+ fix: Some(full_fidelity_fix(std::slice::from_ref(&edge.from))),\n+ ..Diagnostic::default()\n+ });\n+ }\n for attr in BRANCH_IGNORED_ATTRS {\n if !edge.attrs.contains_key(*attr) {\n continue;\n@@ -83,42 +87,32 @@ impl LintRule for Rule {\n ),\n node_id: None,\n edge: Some((edge.from.clone(), edge.to.clone())),\n- fix: Some(fix_message(attr, std::slice::from_ref(&edge.from))),\n+ fix: Some(fix_message(attr)),\n ..Diagnostic::default()\n });\n }\n }\n \n- // Branch target nodes carrying such an attribute — but only when every\n- // incoming edge comes from a parallel node. A node that is also\n- // reachable through a normal edge resolves the attribute on that path,\n- // so it is not inert there.\n- let branch_targets: BTreeSet<&str> = graph\n- .edges\n- .iter()\n- .filter(|e| parallel_ids.contains(e.from.as_str()))\n- .map(|e| e.to.as_str())\n- .collect();\n- for target in branch_targets {\n- let only_branch_entries = graph\n- .edges\n- .iter()\n- .filter(|e| e.to == target)\n- .all(|e| parallel_ids.contains(e.from.as_str()));\n- if !only_branch_entries {\n+ // A node with any normal incoming path still resolves its attributes on\n+ // that path, so branch-only diagnostics do not apply to it.\n+ for target in branches.branch_targets() {\n+ let Some(parents) = branches.branch_only_parents(target) else {\n continue;\n- }\n+ };\n let Some(node) = graph.nodes.get(target) else {\n continue;\n };\n- let parents: Vec<String> = graph\n- .edges\n- .iter()\n- .filter(|e| e.to == target && parallel_ids.contains(e.from.as_str()))\n- .map(|e| e.from.clone())\n- .collect::<BTreeSet<_>>()\n- .into_iter()\n- .collect();\n+ if node.fidelity() == Some(\"full\") {\n+ diagnostics.push(Diagnostic {\n+ rule: self.name().to_string(),\n+ severity: Severity::Warning,\n+ message: full_fidelity_message(),\n+ node_id: Some(node.id.clone()),\n+ edge: None,\n+ fix: Some(full_fidelity_fix(&parents)),\n+ ..Diagnostic::default()\n+ });\n+ }\n for attr in BRANCH_IGNORED_ATTRS {\n if !node.attrs.contains_key(*attr) {\n continue;\n@@ -133,7 +127,7 @@ impl LintRule for Rule {\n ),\n node_id: Some(node.id.clone()),\n edge: None,\n- fix: Some(fix_message(attr, &parents)),\n+ fix: Some(fix_message(attr)),\n ..Diagnostic::default()\n });\n }\n@@ -181,7 +175,7 @@ mod tests {\n }\n \n #[test]\n- fn warns_on_fidelity_on_branch_node() {\n+ fn accepts_non_full_fidelity_on_branch_node() {\n let mut g = parallel_graph();\n g.nodes\n .get_mut(\"branch_a\")\n@@ -191,14 +185,73 @@ mod tests {\n \"fidelity\".to_string(),\n AttrValue::String(\"truncate\".to_string()),\n );\n+\n+ assert!(Rule.apply(&g).is_empty());\n+ }\n+\n+ #[test]\n+ fn warns_when_full_fidelity_on_branch_node_degrades() {\n+ let mut g = parallel_graph();\n+ g.nodes\n+ .get_mut(\"branch_a\")\n+ .expect(\"graph has branch_a\")\n+ .attrs\n+ .insert(\n+ \"fidelity\".to_string(),\n+ AttrValue::String(\"full\".to_string()),\n+ );\n+\n let d = Rule.apply(&g);\n+\n assert_eq!(d.len(), 1);\n assert_eq!(d[0].severity, Severity::Warning);\n assert_eq!(d[0].node_id.as_deref(), Some(\"branch_a\"));\n- assert!(d[0].message.contains(\"'fidelity'\"));\n+ assert!(d[0].message.contains(\"full\"));\n+ assert!(d[0].message.contains(\"summary:high\"));\n assert!(d[0].fix.as_deref().is_some_and(|f| f.contains(\"'fork'\")));\n }\n \n+ #[test]\n+ fn accepts_every_non_full_fidelity_on_branch_edges() {\n+ for fidelity in [\n+ \"truncate\",\n+ \"compact\",\n+ \"summary:low\",\n+ \"summary:medium\",\n+ \"summary:high\",\n+ ] {\n+ let mut g = parallel_graph();\n+ g.edges[1].attrs.insert(\n+ \"fidelity\".to_string(),\n+ AttrValue::String(fidelity.to_string()),\n+ );\n+\n+ assert!(\n+ Rule.apply(&g).is_empty(),\n+ \"{fidelity} should be accepted on a branch edge\"\n+ );\n+ }\n+ }\n+\n+ #[test]\n+ fn warns_when_full_fidelity_on_branch_edge_degrades() {\n+ let mut g = parallel_graph();\n+ g.edges[1].attrs.insert(\n+ \"fidelity\".to_string(),\n+ AttrValue::String(\"full\".to_string()),\n+ );\n+\n+ let d = Rule.apply(&g);\n+\n+ assert_eq!(d.len(), 1);\n+ assert_eq!(\n+ d[0].edge,\n+ Some((\"fork\".to_string(), \"branch_a\".to_string()))\n+ );\n+ assert!(d[0].message.contains(\"full\"));\n+ assert!(d[0].message.contains(\"summary:high\"));\n+ }\n+\n #[test]\n fn warns_on_thread_id_on_branch_edge() {\n let mut g = parallel_graph();\n@@ -221,6 +274,31 @@ mod tests {\n );\n }\n \n+ #[test]\n+ fn warns_on_thread_id_on_branch_only_node() {\n+ let mut g = parallel_graph();\n+ g.nodes\n+ .get_mut(\"branch_a\")\n+ .expect(\"graph has branch_a\")\n+ .attrs\n+ .insert(\n+ \"thread_id\".to_string(),\n+ AttrValue::String(\"impl\".to_string()),\n+ );\n+\n+ let d = Rule.apply(&g);\n+\n+ assert_eq!(d.len(), 1);\n+ assert_eq!(d[0].node_id.as_deref(), Some(\"branch_a\"));\n+ assert!(d[0].message.contains(\"'thread_id'\"));\n+ assert_eq!(\n+ d[0].fix.as_deref(),\n+ Some(\n+ \"Remove 'thread_id': parallel branches inherit the thread resolved when the parallel node started\"\n+ )\n+ );\n+ }\n+\n #[test]\n fn accepts_fidelity_on_the_parallel_node_itself() {\n let mut g = parallel_graph();\n@@ -265,11 +343,10 @@ mod tests {\n .attrs\n .insert(\n \"fidelity\".to_string(),\n- AttrValue::String(\"truncate\".to_string()),\n+ AttrValue::String(\"full\".to_string()),\n );\n let d = Rule.apply(&g);\n assert_eq!(d.len(), 1);\n- assert!(d[0].message.contains(\"'fork', 'fork2'\"));\n let fix = d[0].fix.as_deref().expect(\"diagnostic has a fix\");\n assert!(fix.contains(\"'fork', 'fork2'\"));\n assert!(fix.contains(\"parallel nodes\"));\ndiff --git a/lib/crates/fabro-validate/src/rules/thread_id_requires_fidelity_full.rs b/lib/crates/fabro-validate/src/rules/thread_id_requires_fidelity_full.rs\nindex 457553788..a73335da5 100644\n--- a/lib/crates/fabro-validate/src/rules/thread_id_requires_fidelity_full.rs\n+++ b/lib/crates/fabro-validate/src/rules/thread_id_requires_fidelity_full.rs\n@@ -1,5 +1,6 @@\n use fabro_graphviz::graph::Graph;\n \n+use super::parallel_branch::ParallelBranches;\n use crate::{Diagnostic, LintRule, Severity};\n \n pub(super) fn rule() -> Box<dyn LintRule> {\n@@ -20,9 +21,13 @@ impl LintRule for Rule {\n fn apply(&self, graph: &Graph) -> Vec<Diagnostic> {\n let mut diagnostics = Vec::new();\n let graph_default_full = graph.default_fidelity() == Some(\"full\");\n+ let branches = ParallelBranches::new(graph);\n \n for node in graph.nodes.values() {\n- if node.thread_id().is_some() && node.fidelity() != Some(\"full\") && !graph_default_full\n+ if node.thread_id().is_some()\n+ && !branches.is_branch_only_node(&node.id)\n+ && node.fidelity() != Some(\"full\")\n+ && !graph_default_full\n {\n diagnostics.push(Diagnostic {\n rule: self.name().to_string(),\n@@ -41,7 +46,7 @@ impl LintRule for Rule {\n }\n \n for edge in &graph.edges {\n- if edge.thread_id().is_some() {\n+ if edge.thread_id().is_some() && !branches.is_fork_edge(edge) {\n let edge_full = edge.fidelity() == Some(\"full\");\n let target_full =\n graph.nodes.get(&edge.to).and_then(|n| n.fidelity()) == Some(\"full\");\n@@ -82,12 +87,29 @@ impl LintRule for Rule {\n \n #[cfg(test)]\n mod tests {\n- use fabro_graphviz::graph::{AttrValue, Edge, Node};\n+ use fabro_graphviz::graph::{AttrValue, Edge, Graph, Node};\n \n use super::Rule;\n use crate::rules::test_support::minimal_graph;\n use crate::{LintRule, Severity};\n \n+ fn parallel_graph() -> Graph {\n+ let mut g = minimal_graph();\n+ let mut fork = Node::new(\"fork\");\n+ fork.attrs.insert(\n+ \"shape\".to_string(),\n+ AttrValue::String(\"component\".to_string()),\n+ );\n+ g.nodes.insert(\"fork\".to_string(), fork);\n+ g.nodes.insert(\"branch\".to_string(), Node::new(\"branch\"));\n+ g.edges = vec![\n+ Edge::new(\"start\", \"fork\"),\n+ Edge::new(\"fork\", \"branch\"),\n+ Edge::new(\"branch\", \"exit\"),\n+ ];\n+ g\n+ }\n+\n #[test]\n fn thread_id_requires_fidelity_full_node_warns() {\n let mut g = minimal_graph();\n@@ -206,6 +228,51 @@ mod tests {\n assert!(d.is_empty());\n }\n \n+ #[test]\n+ fn skips_thread_id_on_parallel_branch_edge() {\n+ let mut g = parallel_graph();\n+ g.edges[1].attrs.insert(\n+ \"thread_id\".to_string(),\n+ AttrValue::String(\"branch-thread\".to_string()),\n+ );\n+\n+ assert!(Rule.apply(&g).is_empty());\n+ }\n+\n+ #[test]\n+ fn skips_thread_id_on_branch_only_node() {\n+ let mut g = parallel_graph();\n+ g.nodes\n+ .get_mut(\"branch\")\n+ .expect(\"graph has branch\")\n+ .attrs\n+ .insert(\n+ \"thread_id\".to_string(),\n+ AttrValue::String(\"branch-thread\".to_string()),\n+ );\n+\n+ assert!(Rule.apply(&g).is_empty());\n+ }\n+\n+ #[test]\n+ fn checks_thread_id_on_branch_node_with_normal_entry() {\n+ let mut g = parallel_graph();\n+ g.edges.push(Edge::new(\"start\", \"branch\"));\n+ g.nodes\n+ .get_mut(\"branch\")\n+ .expect(\"graph has branch\")\n+ .attrs\n+ .insert(\n+ \"thread_id\".to_string(),\n+ AttrValue::String(\"shared-thread\".to_string()),\n+ );\n+\n+ let d = Rule.apply(&g);\n+\n+ assert_eq!(d.len(), 1);\n+ assert_eq!(d[0].node_id.as_deref(), Some(\"branch\"));\n+ }\n+\n #[test]\n fn thread_id_requires_fidelity_full_graph_warns() {\n let mut g = minimal_graph();\ndiff --git a/lib/crates/fabro-workflow/src/artifact.rs b/lib/crates/fabro-workflow/src/artifact.rs\nindex 3f9f9eb7d..975fee26d 100644\n--- a/lib/crates/fabro-workflow/src/artifact.rs\n+++ b/lib/crates/fabro-workflow/src/artifact.rs\n@@ -79,6 +79,7 @@ pub fn format_artifact_reference(path: &str) -> String {\n pub fn durable_context_snapshot(context: &Context) -> HashMap<String, Value> {\n let mut snapshot = context.snapshot();\n snapshot.remove(context::keys::CURRENT_PREAMBLE);\n+ snapshot.remove(context::keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES);\n normalize_durable_updates(&mut snapshot);\n snapshot\n }\n@@ -99,6 +100,9 @@ pub fn normalize_checkpoint_for_resume(checkpoint: &mut Checkpoint) {\n checkpoint\n .context_values\n .remove(context::keys::CURRENT_PREAMBLE);\n+ checkpoint\n+ .context_values\n+ .remove(context::keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES);\n normalize_durable_updates(&mut checkpoint.context_values);\n normalize_durable_outcomes(&mut checkpoint.node_outcomes);\n }\n@@ -515,6 +519,59 @@ mod tests {\n );\n }\n \n+ #[test]\n+ fn durable_context_snapshot_drops_parallel_branch_preambles() {\n+ let context = Context::new();\n+ context.set(\n+ context::keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES,\n+ serde_json::json!({\"branch-a\": \"runtime only\"}),\n+ );\n+ context.set(\"response.work\", serde_json::json!(\"durable\"));\n+\n+ let snapshot = durable_context_snapshot(&context);\n+\n+ assert!(!snapshot.contains_key(context::keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES));\n+ assert_eq!(\n+ snapshot.get(\"response.work\"),\n+ Some(&serde_json::json!(\"durable\"))\n+ );\n+ }\n+\n+ #[test]\n+ fn normalize_checkpoint_for_resume_drops_parallel_branch_preambles() {\n+ let mut checkpoint = crate::records::Checkpoint {\n+ timestamp: chrono::Utc::now(),\n+ current_node: \"work\".to_string(),\n+ completed_nodes: vec![\"work\".to_string()],\n+ node_retries: HashMap::new(),\n+ context_values: HashMap::from([\n+ (\n+ context::keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES.to_string(),\n+ serde_json::json!({\"branch-a\": \"runtime only\"}),\n+ ),\n+ (\"response.work\".to_string(), serde_json::json!(\"durable\")),\n+ ]),\n+ node_outcomes: HashMap::new(),\n+ next_node_id: Some(\"exit\".to_string()),\n+ git_commit_sha: None,\n+ loop_failure_signatures: HashMap::new(),\n+ restart_failure_signatures: HashMap::new(),\n+ node_visits: HashMap::new(),\n+ };\n+\n+ normalize_checkpoint_for_resume(&mut checkpoint);\n+\n+ assert!(\n+ !checkpoint\n+ .context_values\n+ .contains_key(context::keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES)\n+ );\n+ assert_eq!(\n+ checkpoint.context_values.get(\"response.work\"),\n+ Some(&serde_json::json!(\"durable\"))\n+ );\n+ }\n+\n #[test]\n fn normalize_checkpoint_for_resume_converts_managed_blob_file_refs_and_drops_preamble() {\n let blob_id = fabro_types::RunBlobId::new(b\"managed\");\ndiff --git a/lib/crates/fabro-workflow/src/context.rs b/lib/crates/fabro-workflow/src/context.rs\nindex af1233586..b911e2210 100644\n--- a/lib/crates/fabro-workflow/src/context.rs\n+++ b/lib/crates/fabro-workflow/src/context.rs\n@@ -25,6 +25,7 @@ pub mod keys {\n pub const INTERNAL_PARENT_PREAMBLE: &str = \"internal.parent_preamble\";\n pub const INTERNAL_PARALLEL_GROUP_ID: &str = \"internal.parallel_group_id\";\n pub const INTERNAL_PARALLEL_BRANCH_ID: &str = \"internal.parallel_branch_id\";\n+ pub const INTERNAL_PARALLEL_BRANCH_PREAMBLES: &str = \"internal.parallel_branch_preambles\";\n \n // --- current.* keys ---\n pub const CURRENT_PREAMBLE: &str = \"current.preamble\";\ndiff --git a/lib/crates/fabro-workflow/src/handler/parallel.rs b/lib/crates/fabro-workflow/src/handler/parallel.rs\nindex 22aa2aa9c..ac4e4f3e1 100644\n--- a/lib/crates/fabro-workflow/src/handler/parallel.rs\n+++ b/lib/crates/fabro-workflow/src/handler/parallel.rs\n@@ -4,6 +4,7 @@ use std::time::Instant;\n \n use async_trait::async_trait;\n use fabro_agent::{Sandbox, WorktreeOptions, WorktreeSandbox};\n+use fabro_graphviz::Fidelity;\n use fabro_graphviz::graph::{AttrValue, Graph, Node};\n use fabro_hooks::{HookContext, HookEvent};\n use fabro_types::{ParallelBranchId, RunId, StageId};\n@@ -56,6 +57,39 @@ struct BranchResult {\n worktree_path: Option<PathBuf>,\n }\n \n+struct BranchPreamble {\n+ fidelity: Fidelity,\n+ preamble: String,\n+}\n+\n+fn parse_branch_preambles(\n+ value: Option<serde_json::Value>,\n+ branch_count: usize,\n+) -> Option<Vec<Option<BranchPreamble>>> {\n+ let serde_json::Value::Array(entries) = value? else {\n+ return None;\n+ };\n+ if entries.len() != branch_count {\n+ return None;\n+ }\n+\n+ entries\n+ .into_iter()\n+ .map(|entry| match entry {\n+ serde_json::Value::Null => Some(None),\n+ serde_json::Value::Object(entry) if entry.len() == 2 => {\n+ let fidelity = entry.get(\"fidelity\")?.as_str()?.parse().ok()?;\n+ let preamble = entry.get(\"preamble\")?.as_str()?;\n+ Some(Some(BranchPreamble {\n+ fidelity,\n+ preamble: preamble.to_string(),\n+ }))\n+ }\n+ _ => None,\n+ })\n+ .collect()\n+}\n+\n #[async_trait]\n impl Handler for ParallelHandler {\n async fn simulate(\n@@ -220,6 +254,10 @@ impl Handler for ParallelHandler {\n None\n };\n \n+ let branch_preambles = parse_branch_preambles(\n+ context.get(keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES),\n+ branches.len(),\n+ );\n let mut branch_setups: Vec<BranchSetup> = Vec::new();\n for (branch_index, edge) in branches.iter().enumerate() {\n let target_id = edge.to.clone();\n@@ -236,6 +274,21 @@ impl Handler for ParallelHandler {\n keys::INTERNAL_PARALLEL_BRANCH_ID,\n serde_json::Value::String(parallel_branch_id.to_string()),\n );\n+ if let Some(entry) = branch_preambles\n+ .as_ref()\n+ .and_then(|entries| entries.get(branch_index))\n+ .and_then(Option::as_ref)\n+ {\n+ branch_context.set(keys::CURRENT_PREAMBLE, serde_json::json!(&entry.preamble));\n+ branch_context.set(\n+ keys::INTERNAL_FIDELITY,\n+ serde_json::json!(entry.fidelity.to_string()),\n+ );\n+ }\n+ branch_context.set(\n+ keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES,\n+ serde_json::Value::Null,\n+ );\n \n let (branch_sandbox, worktree_path): (Arc<dyn Sandbox>, Option<PathBuf>) = if let (\n Some(ref gs),\n@@ -297,6 +350,10 @@ impl Handler for ParallelHandler {\n worktree_path,\n });\n }\n+ context.set(\n+ keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES,\n+ serde_json::Value::Null,\n+ );\n \n // --- Fan out: concurrent execution ---\n let mut handles = Vec::new();\n@@ -693,7 +750,7 @@ fn parallel_branch_commit_cmd(\n \n #[cfg(test)]\n mod tests {\n- use std::sync::Arc;\n+ use std::sync::{Arc, Mutex};\n use std::time::Duration;\n \n use fabro_graphviz::graph::{AttrValue, Edge};\n@@ -756,6 +813,185 @@ mod tests {\n context\n }\n \n+ #[derive(Clone, Debug, PartialEq)]\n+ struct BranchContextCapture {\n+ node_id: String,\n+ preamble: String,\n+ fidelity: String,\n+ stash: Option<serde_json::Value>,\n+ }\n+\n+ struct BranchContextRecordingHandler {\n+ captures: Arc<Mutex<Vec<BranchContextCapture>>>,\n+ }\n+\n+ #[async_trait]\n+ impl Handler for BranchContextRecordingHandler {\n+ async fn execute(\n+ &self,\n+ node: &Node,\n+ context: &Context,\n+ _graph: &Graph,\n+ _run_dir: &Path,\n+ _services: &EngineServices,\n+ ) -> Result<Outcome, Error> {\n+ self.captures.lock().unwrap().push(BranchContextCapture {\n+ node_id: node.id.clone(),\n+ preamble: context.preamble(),\n+ fidelity: context.fidelity().to_string(),\n+ stash: context.get(keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES),\n+ });\n+ Ok(Outcome::success())\n+ }\n+ }\n+\n+ async fn execute_with_branch_stash(\n+ stash: Option<serde_json::Value>,\n+ duplicate_target: bool,\n+ ) -> (Context, Vec<BranchContextCapture>) {\n+ let captures = Arc::new(Mutex::new(Vec::new()));\n+ let recorder = BranchContextRecordingHandler {\n+ captures: Arc::clone(&captures),\n+ };\n+ let mut registry = super::super::HandlerRegistry::new(Box::new(recorder));\n+ registry.register(\n+ \"record\",\n+ Box::new(BranchContextRecordingHandler {\n+ captures: Arc::clone(&captures),\n+ }),\n+ );\n+ let mut services = EngineServices::test_default();\n+ services.registry = Arc::new(registry);\n+\n+ let mut node = Node::new(\"par\");\n+ node.attrs.insert(\n+ \"shape\".to_string(),\n+ AttrValue::String(\"component\".to_string()),\n+ );\n+ let mut branch_a = Node::new(\"branch_a\");\n+ branch_a\n+ .attrs\n+ .insert(\"type\".to_string(), AttrValue::String(\"record\".to_string()));\n+ let mut branch_b = Node::new(\"branch_b\");\n+ branch_b\n+ .attrs\n+ .insert(\"type\".to_string(), AttrValue::String(\"record\".to_string()));\n+\n+ let mut graph = Graph::new(\"test\");\n+ graph.nodes.insert(node.id.clone(), node.clone());\n+ graph.nodes.insert(branch_a.id.clone(), branch_a);\n+ graph.nodes.insert(branch_b.id.clone(), branch_b);\n+ graph.edges.push(Edge::new(\"par\", \"branch_a\"));\n+ graph.edges.push(Edge::new(\n+ \"par\",\n+ if duplicate_target {\n+ \"branch_a\"\n+ } else {\n+ \"branch_b\"\n+ },\n+ ));\n+\n+ let context = test_context();\n+ context.set(keys::CURRENT_PREAMBLE, serde_json::json!(\"fork preamble\"));\n+ context.set(keys::INTERNAL_FIDELITY, serde_json::json!(\"compact\"));\n+ if let Some(stash) = stash {\n+ context.set(keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES, stash);\n+ }\n+\n+ let run_dir = tempfile::tempdir().unwrap();\n+ ParallelHandler\n+ .execute(&node, &context, &graph, run_dir.path(), &services)\n+ .await\n+ .unwrap();\n+\n+ let captures = captures.lock().unwrap().clone();\n+ (context, captures)\n+ }\n+\n+ #[tokio::test]\n+ async fn parallel_handler_applies_indexed_branch_preambles_and_clears_stash() {\n+ let stash = serde_json::json!([\n+ {\"fidelity\": \"truncate\", \"preamble\": \"branch zero\"},\n+ {\"fidelity\": \"summary:high\", \"preamble\": \"branch one\"}\n+ ]);\n+\n+ let (context, mut captures) = execute_with_branch_stash(Some(stash), false).await;\n+ captures.sort_by(|left, right| left.node_id.cmp(&right.node_id));\n+\n+ assert_eq!(captures.len(), 2);\n+ assert_eq!(captures[0].node_id, \"branch_a\");\n+ assert_eq!(captures[0].preamble, \"branch zero\");\n+ assert_eq!(captures[0].fidelity, \"truncate\");\n+ assert_eq!(captures[0].stash, Some(serde_json::Value::Null));\n+ assert_eq!(captures[1].node_id, \"branch_b\");\n+ assert_eq!(captures[1].preamble, \"branch one\");\n+ assert_eq!(captures[1].fidelity, \"summary:high\");\n+ assert_eq!(captures[1].stash, Some(serde_json::Value::Null));\n+ assert_eq!(\n+ context.get(keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES),\n+ Some(serde_json::Value::Null)\n+ );\n+ }\n+\n+ #[tokio::test]\n+ async fn parallel_handler_uses_edge_index_for_duplicate_targets() {\n+ let stash = serde_json::json!([\n+ {\"fidelity\": \"truncate\", \"preamble\": \"first edge\"},\n+ {\"fidelity\": \"summary:low\", \"preamble\": \"second edge\"}\n+ ]);\n+\n+ let (_context, captures) = execute_with_branch_stash(Some(stash), true).await;\n+ let observed = captures\n+ .iter()\n+ .map(|capture| (capture.preamble.as_str(), capture.fidelity.as_str()))\n+ .collect::<std::collections::HashSet<_>>();\n+\n+ assert_eq!(observed.len(), 2);\n+ assert!(observed.contains(&(\"first edge\", \"truncate\")));\n+ assert!(observed.contains(&(\"second edge\", \"summary:low\")));\n+ assert!(\n+ captures\n+ .iter()\n+ .all(|capture| capture.stash == Some(serde_json::Value::Null))\n+ );\n+ }\n+\n+ #[tokio::test]\n+ async fn parallel_handler_legacy_stashes_inherit_fork_context() {\n+ for stash in [\n+ None,\n+ Some(serde_json::Value::Null),\n+ Some(serde_json::json!({\n+ \"fidelity\": \"truncate\",\n+ \"preamble\": \"not an array\"\n+ })),\n+ Some(serde_json::json!([\n+ {\"fidelity\": \"truncate\", \"preamble\": \"wrong length\"}\n+ ])),\n+ Some(serde_json::json!([\n+ {\"fidelity\": \"truncate\"},\n+ null\n+ ])),\n+ Some(serde_json::json!([\n+ {\"fidelity\": \"not-a-fidelity\", \"preamble\": \"malformed fidelity\"},\n+ null\n+ ])),\n+ ] {\n+ let (context, captures) = execute_with_branch_stash(stash, false).await;\n+\n+ assert_eq!(captures.len(), 2);\n+ assert!(captures.iter().all(|capture| {\n+ capture.preamble == \"fork preamble\"\n+ && capture.fidelity == \"compact\"\n+ && capture.stash == Some(serde_json::Value::Null)\n+ }));\n+ assert_eq!(\n+ context.get(keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES),\n+ Some(serde_json::Value::Null)\n+ );\n+ }\n+ }\n+\n #[tokio::test]\n async fn parallel_handler_no_branches() {\n let services = make_services();\ndiff --git a/lib/crates/fabro-workflow/src/lifecycle/event.rs b/lib/crates/fabro-workflow/src/lifecycle/event.rs\nindex 5828dc86b..3965d7261 100644\n--- a/lib/crates/fabro-workflow/src/lifecycle/event.rs\n+++ b/lib/crates/fabro-workflow/src/lifecycle/event.rs\n@@ -14,7 +14,7 @@ use fabro_types::{Principal, RunId, StageTiming};\n \n use super::circuit_breaker::CircuitBreakerLifecycle;\n use super::git::GitCheckpointResult;\n-use crate::context::WorkflowContext;\n+use crate::context::{Context, WorkflowContext};\n use crate::event::{Emitter, Event, StageScope};\n use crate::graph::{WorkflowGraph, WorkflowNode};\n use crate::outcome::{BilledModelUsage, FailureCategory, FailureDetail, Outcome, StageOutcome};\n@@ -92,6 +92,12 @@ fn response_from_outcome(node_id: &str, outcome: &Outcome) -> Option<String> {\n .and_then(|value| value.as_str().map(ToOwned::to_owned))\n }\n \n+fn stage_context_values(workflow_context: &Context) -> Option<BTreeMap<String, serde_json::Value>> {\n+ let mut snapshot = workflow_context.snapshot();\n+ snapshot.remove(context::keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES);\n+ (!snapshot.is_empty()).then(|| snapshot.into_iter().collect())\n+}\n+\n pub(super) fn stage_visit(state: &WfRunState, node_id: &str) -> u32 {\n let visits = state.node_visits.get(node_id).copied().unwrap_or(1);\n u32::try_from(visits).unwrap_or(u32::MAX)\n@@ -318,11 +324,7 @@ impl RunLifecycle<WorkflowGraph> for EventLifecycle {\n .collect::<BTreeMap<_, _>>()\n }),\n jump_to_node: outcome.jump_to_node.clone(),\n- context_values: {\n- let snapshot = state.context.snapshot();\n- (!snapshot.is_empty())\n- .then(|| snapshot.into_iter().collect::<BTreeMap<_, _>>())\n- },\n+ context_values: stage_context_values(&state.context),\n node_visits: (!state.node_visits.is_empty()).then(|| {\n state\n .node_visits\n@@ -446,3 +448,26 @@ impl RunLifecycle<WorkflowGraph> for EventLifecycle {\n Ok(())\n }\n }\n+\n+#[cfg(test)]\n+mod tests {\n+ use super::*;\n+\n+ #[test]\n+ fn stage_context_values_drops_parallel_branch_preambles() {\n+ let workflow_context = Context::new();\n+ workflow_context.set(\n+ context::keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES,\n+ serde_json::json!([{\"fidelity\": \"summary:high\", \"preamble\": \"runtime only\"}]),\n+ );\n+ workflow_context.set(\"response.work\", serde_json::json!(\"durable\"));\n+\n+ let values = stage_context_values(&workflow_context).expect(\"snapshot should not be empty\");\n+\n+ assert!(!values.contains_key(context::keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES));\n+ assert_eq!(\n+ values.get(\"response.work\"),\n+ Some(&serde_json::json!(\"durable\"))\n+ );\n+ }\n+}\ndiff --git a/lib/crates/fabro-workflow/src/lifecycle/fidelity.rs b/lib/crates/fabro-workflow/src/lifecycle/fidelity.rs\nindex 3f45c8142..3f0b24228 100644\n--- a/lib/crates/fabro-workflow/src/lifecycle/fidelity.rs\n+++ b/lib/crates/fabro-workflow/src/lifecycle/fidelity.rs\n@@ -78,6 +78,11 @@ impl RunLifecycle<WorkflowGraph> for FidelityLifecycle {\n node: &WorkflowNode,\n state: &WfRunState,\n ) -> CoreResult<WfNodeDecision> {\n+ state.context.set(\n+ keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES,\n+ serde_json::Value::Null,\n+ );\n+\n let incoming = self\n .incoming_edge_data\n .lock()\n@@ -138,6 +143,46 @@ impl RunLifecycle<WorkflowGraph> for FidelityLifecycle {\n .context\n .set(keys::CURRENT_PREAMBLE, serde_json::json!(preamble));\n \n+ if gv_node.handler_type() == Some(\"parallel\") {\n+ let mut branch_preambles = Vec::new();\n+ for (branch_index, edge) in self.graph.outgoing_edges(node.id()).iter().enumerate() {\n+ let Some(target_node) = self.graph.nodes.get(&edge.to) else {\n+ branch_preambles.push(serde_json::Value::Null);\n+ continue;\n+ };\n+ let resolution = resolve_parallel_branch_fidelity(edge, target_node, fidelity);\n+ if resolution.requested() == Some(keys::Fidelity::Full) {\n+ tracing::warn!(\n+ parallel_node = %node.id(),\n+ branch = %edge.to,\n+ branch_index,\n+ fidelity = %keys::Fidelity::Full,\n+ effective_fidelity = %keys::Fidelity::SummaryHigh,\n+ \"Parallel branch fidelity degraded\"\n+ );\n+ }\n+ let Some(branch_fidelity) = resolution.entry_fidelity() else {\n+ branch_preambles.push(serde_json::Value::Null);\n+ continue;\n+ };\n+ let branch_preamble = build_preamble(\n+ branch_fidelity,\n+ &resolved_context,\n+ &self.graph,\n+ &state.completed_nodes,\n+ &resolved_outcomes,\n+ );\n+ branch_preambles.push(serde_json::json!({\n+ \"fidelity\": branch_fidelity.to_string(),\n+ \"preamble\": branch_preamble,\n+ }));\n+ }\n+ state.context.set(\n+ keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES,\n+ serde_json::Value::Array(branch_preambles),\n+ );\n+ }\n+\n // 5. Thread ID resolution via resolve_thread_id: edge → node → graph default →\n // class → previous\n let thread_id = resolve_thread_id(\n@@ -198,6 +243,47 @@ impl RunLifecycle<WorkflowGraph> for FidelityLifecycle {\n }\n }\n \n+#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n+struct ParallelBranchFidelityResolution {\n+ requested: Option<keys::Fidelity>,\n+ effective: Option<keys::Fidelity>,\n+}\n+\n+impl ParallelBranchFidelityResolution {\n+ fn requested(self) -> Option<keys::Fidelity> {\n+ self.requested\n+ }\n+\n+ fn entry_fidelity(self) -> Option<keys::Fidelity> {\n+ self.effective\n+ }\n+}\n+\n+/// Resolve explicit branch fidelity with edge-over-node precedence.\n+///\n+/// Branches with no explicit fidelity inherit the parallel node's preamble.\n+/// Explicit full fidelity is degraded because concurrent branches cannot share\n+/// an LLM session. An effective fidelity equal to the parallel node also\n+/// inherits, avoiding a redundant preamble render.\n+fn resolve_parallel_branch_fidelity(\n+ edge: &GvEdge,\n+ target_node: &GvNode,\n+ parallel_fidelity: keys::Fidelity,\n+) -> ParallelBranchFidelityResolution {\n+ let requested = edge\n+ .fidelity()\n+ .and_then(|value| value.parse().ok())\n+ .or_else(|| target_node.fidelity().and_then(|value| value.parse().ok()));\n+ let effective = requested\n+ .map(keys::Fidelity::degraded)\n+ .filter(|fidelity| *fidelity != parallel_fidelity);\n+\n+ ParallelBranchFidelityResolution {\n+ requested,\n+ effective,\n+ }\n+}\n+\n /// Resolve the context fidelity for a node, following the precedence:\n /// 1. Incoming edge `fidelity` attribute\n /// 2. Target node `fidelity` attribute\n@@ -263,11 +349,216 @@ fn resolve_thread_id(\n \n #[cfg(test)]\n mod tests {\n+ use std::path::Path;\n+ use std::time::Duration;\n+\n+ use fabro_core::graph::Graph as CoreGraph;\n use fabro_graphviz::graph::{AttrValue, Edge, Graph, Node};\n+ use fabro_store::Database;\n+ use fabro_types::fixtures;\n+ use object_store::memory::InMemory;\n \n use super::*;\n+ use crate::context::WorkflowContext;\n use crate::context::keys::Fidelity;\n \n+ fn fidelity_attr(value: &str) -> AttrValue {\n+ AttrValue::String(value.to_string())\n+ }\n+\n+ fn parallel_workflow_graph(\n+ fork_fidelity: Option<&str>,\n+ branch_a_fidelity: Option<&str>,\n+ ) -> WorkflowGraph {\n+ let mut graph = Graph::new(\"parallel-fidelity\");\n+ let mut start = Node::new(\"start\");\n+ start\n+ .attrs\n+ .insert(\"shape\".to_string(), fidelity_attr(\"Mdiamond\"));\n+ let mut fork = Node::new(\"fork\");\n+ fork.attrs\n+ .insert(\"shape\".to_string(), fidelity_attr(\"component\"));\n+ if let Some(fidelity) = fork_fidelity {\n+ fork.attrs\n+ .insert(\"fidelity\".to_string(), fidelity_attr(fidelity));\n+ }\n+ let mut branch_a = Node::new(\"branch_a\");\n+ if let Some(fidelity) = branch_a_fidelity {\n+ branch_a\n+ .attrs\n+ .insert(\"fidelity\".to_string(), fidelity_attr(fidelity));\n+ }\n+ let branch_b = Node::new(\"branch_b\");\n+ let mut work = Node::new(\"work\");\n+ work.attrs.insert(\"shape\".to_string(), fidelity_attr(\"box\"));\n+\n+ graph.nodes.insert(start.id.clone(), start);\n+ graph.nodes.insert(fork.id.clone(), fork);\n+ graph.nodes.insert(branch_a.id.clone(), branch_a);\n+ graph.nodes.insert(branch_b.id.clone(), branch_b);\n+ graph.nodes.insert(work.id.clone(), work);\n+ graph.edges.push(Edge::new(\"start\", \"fork\"));\n+ graph.edges.push(Edge::new(\"fork\", \"branch_a\"));\n+ graph.edges.push(Edge::new(\"fork\", \"branch_b\"));\n+\n+ WorkflowGraph(Arc::new(graph))\n+ }\n+\n+ async fn test_lifecycle(graph: &WorkflowGraph, run_dir: &Path) -> FidelityLifecycle {\n+ let store = Arc::new(Database::new(\n+ Arc::new(InMemory::new()),\n+ \"\",\n+ Duration::from_millis(1),\n+ None,\n+ ));\n+ let run_store = store.create_run(&fixtures::RUN_1).await.unwrap();\n+ let sandbox: Arc<dyn Sandbox> =\n+ Arc::new(fabro_agent::LocalSandbox::new(run_dir.to_path_buf()));\n+ FidelityLifecycle::new(\n+ graph.0.clone(),\n+ sandbox,\n+ RunStoreHandle::local(run_store),\n+ run_dir.to_path_buf(),\n+ )\n+ }\n+\n+ #[test]\n+ fn parallel_branch_fidelity_edge_overrides_node() {\n+ let mut node = Node::new(\"branch\");\n+ node.attrs\n+ .insert(\"fidelity\".to_string(), fidelity_attr(\"compact\"));\n+ let mut edge = Edge::new(\"fork\", \"branch\");\n+ edge.attrs\n+ .insert(\"fidelity\".to_string(), fidelity_attr(\"truncate\"));\n+\n+ let resolved = resolve_parallel_branch_fidelity(&edge, &node, Fidelity::SummaryHigh);\n+\n+ assert_eq!(resolved.requested(), Some(Fidelity::Truncate));\n+ assert_eq!(resolved.entry_fidelity(), Some(Fidelity::Truncate));\n+ }\n+\n+ #[test]\n+ fn parallel_branch_fidelity_without_attribute_inherits() {\n+ let node = Node::new(\"branch\");\n+ let edge = Edge::new(\"fork\", \"branch\");\n+\n+ let resolution = resolve_parallel_branch_fidelity(&edge, &node, Fidelity::Compact);\n+\n+ assert_eq!(resolution.requested(), None);\n+ assert_eq!(resolution.entry_fidelity(), None);\n+ }\n+\n+ #[test]\n+ fn parallel_branch_full_fidelity_degrades_to_summary_high() {\n+ let mut node = Node::new(\"branch\");\n+ node.attrs\n+ .insert(\"fidelity\".to_string(), fidelity_attr(\"full\"));\n+ let edge = Edge::new(\"fork\", \"branch\");\n+\n+ let resolved = resolve_parallel_branch_fidelity(&edge, &node, Fidelity::Compact);\n+\n+ assert_eq!(resolved.requested(), Some(Fidelity::Full));\n+ assert_eq!(resolved.entry_fidelity(), Some(Fidelity::SummaryHigh));\n+ }\n+\n+ #[test]\n+ fn parallel_branch_fidelity_equal_to_fork_inherits() {\n+ let mut node = Node::new(\"branch\");\n+ node.attrs\n+ .insert(\"fidelity\".to_string(), fidelity_attr(\"summary:high\"));\n+ let edge = Edge::new(\"fork\", \"branch\");\n+\n+ let resolution = resolve_parallel_branch_fidelity(&edge, &node, Fidelity::SummaryHigh);\n+\n+ assert_eq!(resolution.requested(), Some(Fidelity::SummaryHigh));\n+ assert_eq!(resolution.entry_fidelity(), None);\n+ }\n+\n+ #[test]\n+ fn explicit_full_branch_equal_to_degraded_fork_inherits() {\n+ let mut node = Node::new(\"branch\");\n+ node.attrs\n+ .insert(\"fidelity\".to_string(), fidelity_attr(\"full\"));\n+ let edge = Edge::new(\"fork\", \"branch\");\n+\n+ let resolution = resolve_parallel_branch_fidelity(&edge, &node, Fidelity::SummaryHigh);\n+\n+ assert_eq!(resolution.requested(), Some(Fidelity::Full));\n+ assert_eq!(resolution.entry_fidelity(), None);\n+ }\n+\n+ #[test]\n+ fn full_fork_without_branch_fidelity_does_not_create_entry() {\n+ let node = Node::new(\"branch\");\n+ let edge = Edge::new(\"fork\", \"branch\");\n+\n+ let resolution = resolve_parallel_branch_fidelity(&edge, &node, Fidelity::Full);\n+\n+ assert_eq!(resolution.requested(), None);\n+ assert_eq!(resolution.entry_fidelity(), None);\n+ }\n+\n+ #[tokio::test]\n+ async fn parallel_before_node_rebuilds_branch_preamble_stash() {\n+ let graph = parallel_workflow_graph(None, Some(\"truncate\"));\n+ let run_dir = tempfile::tempdir().unwrap();\n+ let lifecycle = test_lifecycle(&graph, run_dir.path()).await;\n+ let state: WfRunState = ExecutionState::new(&graph).unwrap();\n+ let fork = graph.get_node(\"fork\").unwrap();\n+\n+ lifecycle.before_node(&fork, &state).await.unwrap();\n+ state.context.set(\n+ keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES,\n+ serde_json::json!([\"stale\", \"entries\", \"must disappear\"]),\n+ );\n+ lifecycle.before_node(&fork, &state).await.unwrap();\n+\n+ let stash = state\n+ .context\n+ .get(keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES)\n+ .expect(\"parallel stash should be set\");\n+ let entries = stash.as_array().expect(\"parallel stash should be an array\");\n+ assert_eq!(entries.len(), 2);\n+ assert!(entries[0].is_object());\n+ assert!(entries[1].is_null());\n+ }\n+\n+ #[tokio::test]\n+ async fn non_parallel_before_node_overwrites_branch_preamble_stash_with_null() {\n+ let graph = parallel_workflow_graph(None, Some(\"truncate\"));\n+ let run_dir = tempfile::tempdir().unwrap();\n+ let lifecycle = test_lifecycle(&graph, run_dir.path()).await;\n+ let state: WfRunState = ExecutionState::new(&graph).unwrap();\n+ let fork = graph.get_node(\"fork\").unwrap();\n+ let work = graph.get_node(\"work\").unwrap();\n+\n+ lifecycle.before_node(&fork, &state).await.unwrap();\n+ lifecycle.before_node(&work, &state).await.unwrap();\n+\n+ assert_eq!(\n+ state.context.get(keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES),\n+ Some(serde_json::Value::Null)\n+ );\n+ }\n+\n+ #[tokio::test]\n+ async fn resumed_full_fork_degrades_without_rendering_fallback_branches() {\n+ let graph = parallel_workflow_graph(Some(\"full\"), None);\n+ let run_dir = tempfile::tempdir().unwrap();\n+ let lifecycle = test_lifecycle(&graph, run_dir.path()).await;\n+ lifecycle.set_degrade_fidelity_on_resume(true);\n+ let state: WfRunState = ExecutionState::new(&graph).unwrap();\n+ let fork = graph.get_node(\"fork\").unwrap();\n+\n+ lifecycle.before_node(&fork, &state).await.unwrap();\n+\n+ assert_eq!(state.context.fidelity(), Fidelity::SummaryHigh);\n+ assert_eq!(\n+ state.context.get(keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES),\n+ Some(serde_json::json!([null, null]))\n+ );\n+ }\n+\n #[test]\n fn fidelity_defaults_to_compact() {\n let node = Node::new(\"work\");\ndiff --git a/lib/crates/fabro-workflow/tests/it/integration.rs b/lib/crates/fabro-workflow/tests/it/integration.rs\nindex 87ebeee4d..88e1f1aa9 100644\n--- a/lib/crates/fabro-workflow/tests/it/integration.rs\n+++ b/lib/crates/fabro-workflow/tests/it/integration.rs\n@@ -4936,6 +4936,27 @@ struct FidelityCapturingHandler {\n captures: FidelityCaptures,\n }\n \n+struct ParallelFidelitySeedHandler;\n+\n+#[async_trait::async_trait]\n+impl Handler for ParallelFidelitySeedHandler {\n+ async fn execute(\n+ &self,\n+ _node: &Node,\n+ _context: &Context,\n+ _graph: &Graph,\n+ _run_dir: &Path,\n+ _services: &fabro_workflow::handler::EngineServices,\n+ ) -> Result<Outcome, Error> {\n+ let mut outcome = Outcome::success();\n+ outcome.context_updates.insert(\n+ \"parallel_fidelity_marker\".to_string(),\n+ serde_json::json!(\"marker visible to inherited preambles\"),\n+ );\n+ Ok(outcome)\n+ }\n+}\n+\n #[async_trait::async_trait]\n impl Handler for FidelityCapturingHandler {\n async fn execute(\n@@ -9333,6 +9354,176 @@ async fn run_fidelity_prompt_pipeline(fidelity: &str) -> String {\n .expect(\"report prompt should exist\")\n }\n \n+async fn run_parallel_fidelity_capture(\n+ fork_fidelity: Option<&str>,\n+ branch_node_fidelity: Option<&str>,\n+ branch_edge_fidelity: Option<&str>,\n+) -> FidelityCaptures {\n+ use fabro_workflow::handler::fan_in::FanInHandler;\n+ use fabro_workflow::handler::parallel::ParallelHandler;\n+\n+ let mut graph = make_graph_with_start_exit(\"ParallelFidelityTest\");\n+ graph.attrs.insert(\n+ \"goal\".to_string(),\n+ AttrValue::String(\"Verify parallel branch context\".to_string()),\n+ );\n+\n+ let mut seed = Node::new(\"seed\");\n+ seed.attrs.insert(\n+ \"type\".to_string(),\n+ AttrValue::String(\"parallel_fidelity_seed\".to_string()),\n+ );\n+ let mut fork = Node::new(\"fork\");\n+ fork.attrs.insert(\n+ \"shape\".to_string(),\n+ AttrValue::String(\"component\".to_string()),\n+ );\n+ if let Some(fidelity) = fork_fidelity {\n+ fork.attrs.insert(\n+ \"fidelity\".to_string(),\n+ AttrValue::String(fidelity.to_string()),\n+ );\n+ }\n+ let mut branch_a = Node::new(\"branch_a\");\n+ branch_a.attrs.insert(\n+ \"type\".to_string(),\n+ AttrValue::String(\"fidelity_capture\".to_string()),\n+ );\n+ if let Some(fidelity) = branch_node_fidelity {\n+ branch_a.attrs.insert(\n+ \"fidelity\".to_string(),\n+ AttrValue::String(fidelity.to_string()),\n+ );\n+ }\n+ let mut branch_b = Node::new(\"branch_b\");\n+ branch_b.attrs.insert(\n+ \"type\".to_string(),\n+ AttrValue::String(\"fidelity_capture\".to_string()),\n+ );\n+ let mut fan_in = Node::new(\"fan_in\");\n+ fan_in.attrs.insert(\n+ \"shape\".to_string(),\n+ AttrValue::String(\"tripleoctagon\".to_string()),\n+ );\n+\n+ graph.nodes.insert(seed.id.clone(), seed);\n+ graph.nodes.insert(fork.id.clone(), fork);\n+ graph.nodes.insert(branch_a.id.clone(), branch_a);\n+ graph.nodes.insert(branch_b.id.clone(), branch_b);\n+ graph.nodes.insert(fan_in.id.clone(), fan_in);\n+ graph.edges.push(Edge::new(\"start\", \"seed\"));\n+ graph.edges.push(Edge::new(\"seed\", \"fork\"));\n+ let mut branch_a_edge = Edge::new(\"fork\", \"branch_a\");\n+ if let Some(fidelity) = branch_edge_fidelity {\n+ branch_a_edge.attrs.insert(\n+ \"fidelity\".to_string(),\n+ AttrValue::String(fidelity.to_string()),\n+ );\n+ }\n+ graph.edges.push(branch_a_edge);\n+ graph.edges.push(Edge::new(\"fork\", \"branch_b\"));\n+ graph.edges.push(Edge::new(\"branch_a\", \"fan_in\"));\n+ graph.edges.push(Edge::new(\"branch_b\", \"fan_in\"));\n+ graph.edges.push(Edge::new(\"fan_in\", \"exit\"));\n+\n+ let captures = FidelityCaptures::new();\n+ let mut registry = HandlerRegistry::new(Box::new(StartHandler));\n+ registry.register(\"start\", Box::new(StartHandler));\n+ registry.register(\"exit\", Box::new(ExitHandler));\n+ registry.register(\"parallel\", Box::new(ParallelHandler));\n+ registry.register(\n+ \"parallel.fan_in\",\n+ Box::new(FanInHandler::new(Some(Box::new(MockCodergenBackend)))),\n+ );\n+ registry.register(\n+ \"parallel_fidelity_seed\",\n+ Box::new(ParallelFidelitySeedHandler),\n+ );\n+ registry.register(\n+ \"fidelity_capture\",\n+ Box::new(FidelityCapturingHandler {\n+ captures: captures.clone(),\n+ }),\n+ );\n+\n+ let dir = tempfile::tempdir().expect(\"parallel fidelity run directory should be created\");\n+ let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env());\n+ let run_options = RunOptions {\n+ settings: WorkflowSettings::default(),\n+ run_dir: dir.path().to_path_buf(),\n+ cancel_token: CancellationToken::new(),\n+ run_id: test_run_id(\"parallel-fidelity\"),\n+ labels: std::collections::HashMap::new(),\n+ workflow_slug: None,\n+ github_app: None,\n+ base_branch: None,\n+ display_base_sha: None,\n+ pre_run_git: None,\n+ fork_source_ref: None,\n+ git: None,\n+ };\n+ let (outcome, _state) = engine\n+ .run_with_state(&graph, &run_options)\n+ .await\n+ .expect(\"parallel fidelity workflow should succeed\");\n+ assert_eq!(outcome.status, StageOutcome::Succeeded);\n+ captures\n+}\n+\n+fn captured_fidelity_preamble(captures: &FidelityCaptures, node_id: &str) -> (String, String) {\n+ let fidelity = captures\n+ .fidelities\n+ .lock()\n+ .unwrap()\n+ .iter()\n+ .find(|(captured_node_id, _)| captured_node_id == node_id)\n+ .map(|(_, fidelity)| fidelity.clone())\n+ .expect(\"branch fidelity should be captured\");\n+ let preamble = captures\n+ .preambles\n+ .lock()\n+ .unwrap()\n+ .iter()\n+ .find(|(captured_node_id, _)| captured_node_id == node_id)\n+ .map(|(_, preamble)| preamble.clone())\n+ .expect(\"branch preamble should be captured\");\n+ (fidelity, preamble)\n+}\n+\n+#[tokio::test]\n+async fn parallel_branches_get_per_branch_preambles_by_fidelity() {\n+ let captures = run_parallel_fidelity_capture(None, Some(\"truncate\"), None).await;\n+\n+ let (branch_a_fidelity, branch_a_preamble) = captured_fidelity_preamble(&captures, \"branch_a\");\n+ let (branch_b_fidelity, branch_b_preamble) = captured_fidelity_preamble(&captures, \"branch_b\");\n+\n+ assert_eq!(branch_a_fidelity, \"truncate\");\n+ assert!(!branch_a_preamble.contains(\"parallel_fidelity_marker\"));\n+ assert_eq!(branch_b_fidelity, \"compact\");\n+ assert!(branch_b_preamble.contains(\"parallel_fidelity_marker\"));\n+}\n+\n+#[tokio::test]\n+async fn parallel_fork_fidelity_still_applies_to_all_branches() {\n+ let captures = run_parallel_fidelity_capture(Some(\"truncate\"), None, None).await;\n+\n+ for branch_id in [\"branch_a\", \"branch_b\"] {\n+ let (fidelity, preamble) = captured_fidelity_preamble(&captures, branch_id);\n+ assert_eq!(fidelity, \"truncate\");\n+ assert!(!preamble.contains(\"parallel_fidelity_marker\"));\n+ }\n+}\n+\n+#[tokio::test]\n+async fn parallel_branch_edge_fidelity_overrides_node_fidelity() {\n+ let captures =\n+ run_parallel_fidelity_capture(None, Some(\"summary:high\"), Some(\"truncate\")).await;\n+\n+ let (fidelity, preamble) = captured_fidelity_preamble(&captures, \"branch_a\");\n+ assert_eq!(fidelity, \"truncate\");\n+ assert!(!preamble.contains(\"parallel_fidelity_marker\"));\n+}\n+\n #[tokio::test]\n async fn fidelity_prompt_compact() {\n let prompt = run_fidelity_prompt_pipeline(\"compact\").await;\n",
"summary": {
"files_changed": 13,
"additions": 1093,
"deletions": 77
}
}
},
{
"seq": 2391,
"checkpoint": {
"timestamp": "2026-07-23T19:10:41.901185547Z",
"current_node": "simplify_fable",
"completed_nodes": [
"start",
"toolchain",
"preflight_compile",
"preflight_lint",
"implement",
"simplify_fable"
],
"node_retries": {},
"context_values": {
"internal.retry_count.toolchain": 0,
"last_stage": "simplify_fable",
"failure_class": "",
"internal.retry_count.preflight_compile": 0,
"outcome": "succeeded",
"internal.node_visit_count": 1,
"thread.preflight_lint.current_node": "implement",
"thread.toolchain.current_node": "preflight_compile",
"internal.work_dir": "/home/daytona/workspace/fabro",
"internal.retry_count.start": 0,
"internal.retry_count.simplify_fable": 0,
"response.implement": "Implemented the complete per-branch fidelity plan.\n\n### Highlights\n- Added ordered per-branch preamble stash generation and consumption.\n- Implemented edge → branch node → fork inheritance precedence.\n- Explicit branch `full` degrades to `summary:high` with structured logging.\n- Preserved byte-identical fork inheritance for unspecified/equal fidelities.\n- Added strict legacy fallback for missing, null, malformed, non-array, and mismatched stashes.\n- Cleared branch stashes for nested-parallel safety.\n- Removed stash data from checkpoints, durable snapshots, and persisted event contexts.\n- Kept branch `thread_id` inert, `CURRENT_NODE` inherited, and `simulate()` unchanged.\n- Updated validation rules and all three requested documentation pages.\n- Added unit, lifecycle, handler, integration, persistence, and lint regression coverage.\n- No changelog added.\n\n### Verification\n- `cargo nextest run --workspace`: **7,063 passed, 193 skipped**\n- `cargo +nightly-2026-04-14 fmt --check --all`: passed\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`: passed\n- `git diff --check`: passed\n\nLive testing-server probes were not run because the probe definitions and testing-server access were not available in this checkout/session.",
"response.simplify_fable": "Everything is green. Summary of the simplify stage:\n\n## Review findings applied\n\n**Wire-format root cause (reuse #1, quality #1/#2, efficiency #2/#3):**\n- Added `Serialize`/`Deserialize` to `fabro_graphviz::Fidelity` (serde renames aligned with strum per CLAUDE.md, plus `Hash` and a serde↔strum parity test; `serde_json` added as dev-dependency).\n- Replaced the hand-rolled JSON producer/parser pair with one shared `ParallelBranchPreamble` struct in `context.rs` (`deny_unknown_fields` replaces the brittle `entry.len() == 2` check). Producer uses `serde_json::to_value`, consumer uses `from_value` — a field rename can no longer silently split the contract. Documented the index-correlation invariant and the `Option<Option<…>>` semantics (quality #5/#8).\n\n**Duplication (reuse #2/#3, quality #6/#7):**\n- Extracted `explicit_fidelity()` — the edge→node precedence core is now shared between `resolve_fidelity` and `resolve_parallel_branch_fidelity`.\n- Added `keys::TRANSIENT_CONTEXT_KEYS` + `artifact::strip_transient_keys()`; both artifact strip sites use it. `stage_context_values` keeps its intentionally different behavior (retains `CURRENT_PREAMBLE`) with a WHY comment.\n- Unified the two copy-pasted full-fidelity diagnostic blocks into `full_fidelity_diagnostic()`; `full_fidelity_message()` → `const`; dead `fix_message` match → `const THREAD_ID_FIX`; capitalized the message for consistency; reworded the \"now honors\" changelog-style doc comment.\n\n**Structure & style (quality #3/#4/#9/#10/#14):**\n- Extracted the 38-line parallel stash block into `build_parallel_branch_preambles()`, restoring `before_node`'s numbered-step narrative (steps renumbered 5–9).\n- `ParallelBranchFidelityResolution`: dropped accessor ceremony and unused derives; plain documented fields.\n- Fixed the warn! to log `effective_fidelity` via `degraded()` instead of dressed-up constants; removed the redundant `fidelity` field.\n- `parallel.rs` uses `keys::`-scoped types; test helper `fidelity_attr` → `str_attr`.\n\n**Efficiency (#1, #4):**\n- Moved the parent stash clear to *before* the fork loop — branch forks no longer copy the full preamble array B times just to null it, and the per-branch null-set became unnecessary (forks inherit Null; nested-parallel safety preserved, verified by the existing `stash == Null` test assertions).\n- Memoized per-fidelity preamble renders — duplicate branch fidelities render once.\n\n**Validate helper (reuse #4, quality #13, efficiency #6):**\n- `ParallelBranches` now uses `Graph::incoming_edges`; `is_branch_only_node` is allocation-free; `branch_only_parents` replaced the dense peekable/`then`/`collect::<Option<BTreeSet>>` chain with straightforward code. Added the cross-rule suppression WHY comment in `thread_id_requires_fidelity_full`.\n\n**Skipped (noted, not addressed):** test-fixture consolidation into `rules/test_support` and a shared in-memory-store test helper (pre-existing pattern across ~15 sites, out of scope); `\"parallel\"` string literal (codebase-wide convention); `make_run_options` parameterization (follows existing file precedent).\n\n## Verification\n- `cargo nextest run -p fabro-graphviz -p fabro-validate -p fabro-workflow` — all pass (302 + 1218)\n- `cargo nextest run --workspace` — **7064 passed, 0 failed**\n- `cargo +nightly-2026-04-14 fmt --check --all` — clean\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` — clean\n\nNo blockers.",
"internal.retry_count.implement": 0,
"current_node": "simplify_fable",
"failure_signature": "",
"thread.start.current_node": "toolchain",
"thread.preflight_compile.current_node": "preflight_lint",
"internal.run_id": "01KY7Y01REECZ24XXTMBZ3PPV9",
"internal.fidelity": "compact",
"internal.thread_id": "implement",
"graph.goal": "# Per-Branch Fidelity for Parallel Branches — Implementation Plan\n\n## Context\n\nParallel branch nodes are dispatched via `dispatch_handler`, bypassing `FidelityLifecycle::before_node` (`lib/crates/fabro-workflow/src/lifecycle/fidelity.rs:76-180`) — the only place fidelity is resolved and preambles are built. Every branch therefore inherits the stale `current.preamble` copied at `context.fork()` (`handler/parallel.rs:226`), and `fidelity`/`thread_id` on branch nodes or `fork -> branch` edges are silently ignored. Confirmed live on the testing server (probes `01KY7KRA7E…`/`01KY7KRAAW…`, 2026-07-23): a `truncate` branch received the identical compact preamble as its default sibling; fork-level fidelity works and is the current workaround. Design reviewed via the Quarry doc \"Fix: Per-Branch Fidelity for Parallel Branches\".\n\nWhy not run the lifecycle per branch: it is a single-token state machine (one-slot `incoming_edge_data` baton, singleton context keys written to shared run state); concurrent invocation would corrupt run state. And `build_preamble` (`handler/llm/preamble.rs:24`, public and pure) needs `state.completed_nodes`/`state.node_outcomes`, which only the lifecycle sees. So: **pre-render per-branch preambles in the lifecycle, hand off to the handler via one context key.**\n\n## Semantics (final, after design pressure-test)\n\n- **Explicit-only resolution.** A branch's fidelity comes from the `fork -> branch` edge attr, else the branch node attr, else **no entry** — the branch inherits the fork's preamble via `fork()` exactly as today. The fork's own resolved fidelity is never re-applied per branch; this keeps the default path byte-identical even when the fork resolves `Full` (where re-derivation would have wrongly degraded every branch).\n- **`full` degrades to `summary:high`** (`Fidelity::degraded()`, `fabro-graphviz/src/fidelity.rs:35-40`) — applied only to *explicitly set* branch fidelity, with a log line (per `docs/internal/logging-strategy.md` — read before writing it).\n- **Equality skip**: if the branch's post-degradation fidelity equals the fork's post-degradation fidelity, store no entry (avoid redundant renders).\n- **`thread_id` stays inert in branches** (concurrent branches must never share an LLM session).\n- **`CURRENT_NODE` in branch contexts stays inherited (fork id).** The pressure-test showed changing it would re-attribute every branch-internal event's stage scope (`context.rs:185-190` → `StageScope::for_handler` used by all handlers) with a visit mismatch against `for_parallel_branch` scoping. The Quarry doc's \"bookkeeping keys describe the branch\" line is consciously deferred to a separate change with proper visit accounting.\n- **`simulate()` untouched.** No simulated handler reads preambles; partial mirroring would risk nested-parallel stash misreads. All-or-nothing → nothing.\n- **Stash shape**: `Value::Array`, length = branch count, `Null` = inherit, else `{\"fidelity\": \"...\", \"preamble\": \"...\"}`. Array length ≠ edge count → treat as absent (legacy). Keyed by edge index; `graph.outgoing_edges` is an ordered Vec filter (`fabro-types/src/graph.rs:393-395`) and lifecycle + handler share the same `Arc<GvGraph>`, so indices align deterministically (including two edges to the same target).\n\n## Implementation steps (ordered; tree compiles at each step)\n\n1. **`lib/crates/fabro-workflow/src/context.rs`** — add `pub const INTERNAL_PARALLEL_BRANCH_PREAMBLES: &str = \"internal.parallel_branch_preambles\";` to `keys`. The `internal.` prefix already excludes it from preamble rendering (`preamble.rs:99-109`) and child→parent propagation (`context.rs:80-85`).\n\n2. **`lib/crates/fabro-workflow/src/artifact.rs`** — strip the new key in `durable_context_snapshot` (`:81`) and `normalize_checkpoint_for_resume` (`:101`), beside `CURRENT_PREAMBLE`. Without this, every post-parallel checkpoint and `CheckpointCompleted` event payload carries the full per-branch preamble map (a `summary:high` preamble embeds up to 50 lines of every command output — multiplied per branch).\n\n3. **`lib/crates/fabro-workflow/src/lifecycle/fidelity.rs`** — in `before_node`:\n - Set the stash key to `Null` on `state.context` **first**, before the two fallible `resolve_*` calls, so the always-overwritten invariant holds on every early-return path.\n - After the existing preamble build, when `gv_node.handler_type() == Some(\"parallel\")`: iterate `self.graph.outgoing_edges(node.id())` in order; per edge resolve explicit fidelity (edge attr → target-node attr → none); apply `degraded()` to explicit values (log when it was `full`); push `Null` for inherit/equal-to-fork, else render `build_preamble(final_fidelity, …)` reusing the already-resolved snapshot (blobs resolved once at `:113-128`) and push the entry. Set the array on the stash key.\n - Extract the per-branch resolution as a pure helper beside `resolve_fidelity` (`:206`, same module — no visibility change) for unit testing.\n\n4. **`lib/crates/fabro-workflow/src/handler/parallel.rs`** — in `execute()`'s branch-setup loop (insert after `:238`, where `INTERNAL_PARALLEL_BRANCH_ID` is set):\n - Read the stash from the parent context once before the loop; `None`, `Some(Null)`, or length-mismatch all mean strict legacy behavior (note: `Context::get` returns `Some(Null)` for a Nulled key — both must be treated as absent).\n - Per branch with an entry: `branch_context.set(CURRENT_PREAMBLE, preamble)` and `branch_context.set(INTERNAL_FIDELITY, fidelity)`. Downstream needs nothing: `agent.rs:244`/`prompt.rs:63` read `context.preamble()`.\n - In **every** branch fork, set the stash key to `Null` — load-bearing, not hygiene: a nested parallel branch target reads its fork's stash, and without the Null it would misinterpret the outer node's array as its own.\n - After the loop, set the stash key to `Null` on the handler's own context — the write-back diff (`node_handler.rs:99-105`) clears `state.context` so the post-parallel checkpoint carries Null even before the artifact strip.\n\n5. **`lib/crates/fabro-validate/src/rules/parallel_branch_inert_attribute.rs`** — drop `\"fidelity\"` from `BRANCH_IGNORED_ATTRS` and its `fix_message` arm; add a narrow diagnostic in its place: `fidelity=\"full\"` on a fork→branch edge or branch-only node warns \"parallel branches run at most at summary:high; full is degraded at runtime because branches cannot share a session\". Other fidelity values now lint clean. Update doc comment (the snapshot rationale now applies to `thread_id` only) and tests.\n\n6. **`lib/crates/fabro-validate/src/rules/thread_id_requires_fidelity_full.rs`** — skip fork→branch edges and branch-only nodes (factor the branch-only detection from rule 5 into a shared helper). Today it tells branch nodes with `thread_id` to *add* `fidelity=\"full\"` — advice that, post-change, would actively alter runtime behavior while the other rule says \"remove thread_id\". Defer to the inert-attribute rule's guidance on branches.\n\n7. **Docs** — `docs/public/execution/context.mdx` (fidelity precedence: branch edge → branch node → inherit fork; per-branch preamble rendering; `thread_id` inert in branches; `full` degradation), `docs/public/workflows/stages-and-nodes.mdx` (parallel fan-out section + fidelity attribute notes), `docs/public/reference/dot-language.mdx` (edge/node attr rows). Optional changelog entry via the changelog conventions.\n\n## Tests\n\nPer `docs/internal/testing-strategy.md`, preamble content is implementation-facing → `fabro-workflow`, not CLI layers.\n\n- **Pure unit tests** (`lifecycle/fidelity.rs` tests, beside `resolve_fidelity`'s at `:271-321`): explicit edge > node precedence; no-attr → inherit (no entry); explicit `full` → `summary:high` entry; branch fidelity equal to fork's (post-degradation) → no entry; fork resolved `Full` + no branch attrs → no entries at all.\n- **Lifecycle-level**: two consecutive `before_node` calls on the same parallel node rebuild (not merge) the stash; non-parallel node overwrites stash to Null; resume-degrade flag interaction (fork degrades, fallback branches still get no entry).\n- **`artifact.rs` tests** (`:519+` pattern): both snapshot functions strip the stash key.\n- **Parallel handler unit tests** (`handler/parallel.rs` tests module, `EngineServices::test_default()` + recording handler mirroring `PreambleEchoHandler`, `manager_loop.rs:973-1043`): entry applies `CURRENT_PREAMBLE`/`INTERNAL_FIDELITY` to the right branch by index; stash Null in every branch fork; `Some(Null)`/absent/length-mismatch → legacy; duplicate-target edges get distinct entries at indices 0/1 (no-git test — a pre-existing worktree-name collision exists for that topology, don't let it pollute the assertion); existing tests stay unmodified as the legacy guard.\n- **Engine-level regression** in `lib/crates/fabro-workflow/tests/it/integration.rs` beside the `fidelity_prompt_*` tests (`:9245-9479`), reusing `FidelityCapturingHandler` (`:4917-4974`) and the `end_to_end_parallel_fan_out_fan_in` scaffold (`:2441-2480`) via `WorkflowRunner::run_with_state`:\n - **Probe A analog** (`parallel_branches_get_per_branch_preambles_by_fidelity`): seed sets a context marker → fork → `branch_a` (`fidelity=\"truncate\"`) + `branch_b` (default) → fan-in. Assert branch_a's preamble is goal-only (no marker) while branch_b's contains the marker.\n - **Probe B analog**: `fidelity=\"truncate\"` on the fork only → both branches goal-only (compat guarantee, unchanged behavior).\n - Edge-attr-beats-node-attr variant.\n- **Lint tests**: no warning for non-full branch fidelity; warning for branch `fidelity=\"full\"`; `thread_id_requires_fidelity_full` silent on branch-only nodes, still firing elsewhere.\n\n## Verification\n\n- `cargo nextest run -p fabro-workflow -p fabro-validate`, then `ulimit -n 4096 && cargo nextest run --workspace` (do not export `FORCE_COLOR`).\n- `cargo +nightly-2026-04-14 fmt --check --all`; `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`.\n- Live confirmation on the testing server: re-run the two probe workflows (session scratchpad `probes/isolation-a`, `probes/isolation-b`) against a locally built binary — probe A's `stage.prompt` events must now show differentiated branch preambles; probe B byte-identical to before.\n\n## Compatibility\n\n| Situation | Impact |\n|---|---|\n| No fidelity attrs near the parallel node | None — byte-identical (inherit path, no re-render) |\n| Fidelity on the fork node / its incoming edge | None — fork snapshot semantics unchanged |\n| Previously-dead attrs on branch nodes / fork→branch edges | Start working (the fix) |\n| `full` on a branch | Degrades to `summary:high` + log + lint warning |\n| `thread_id` on a branch | Still inert; lint still warns; the contradictory companion lint goes quiet on branches |\n\n## Decisions (user-confirmed 2026-07-23)\n\n1. **`CURRENT_NODE` in branch contexts stays inherited** — the branch-scoped bookkeeping change is deferred to a dedicated event-attribution change.\n2. **The narrow `fidelity=\"full\"` branch lint is in scope** (step 5 stands as written).\n3. **No changelog entry in this PR** — changelog handled in the usual batch.\n",
"internal.retry_count.preflight_lint": 0,
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
"thread.implement.current_node": "simplify_fable",
"graph.rankdir": "LR",
"last_response": "Everything is green. Summary of the simplify stage:\n\n## Review findings applied\n\n**Wire-format root cause (reuse #1, quality #1/#2, efficiency #2/#3):**\n- Added `Serialize`/`Deserialize` to `fabro_gra"
},
"node_outcomes": {
"preflight_lint": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
},
"notes": "Script completed: cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1",
"usage": null,
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 0,
"tool_time_ms": 154518,
"active_time_ms": 154518
}
},
"toolchain": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c"
},
"notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
"usage": null,
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 0,
"tool_time_ms": 1360,
"active_time_ms": 1360
}
},
"implement": {
"status": "succeeded",
"context_updates": {
"last_stage": "implement",
"last_response": "Implemented the complete per-branch fidelity plan.\n\n### Highlights\n- Added ordered per-branch preamble stash generation and consumption.\n- Implemented edge → branch node → fork inheritance precede",
"response.implement": "Implemented the complete per-branch fidelity plan.\n\n### Highlights\n- Added ordered per-branch preamble stash generation and consumption.\n- Implemented edge → branch node → fork inheritance precedence.\n- Explicit branch `full` degrades to `summary:high` with structured logging.\n- Preserved byte-identical fork inheritance for unspecified/equal fidelities.\n- Added strict legacy fallback for missing, null, malformed, non-array, and mismatched stashes.\n- Cleared branch stashes for nested-parallel safety.\n- Removed stash data from checkpoints, durable snapshots, and persisted event contexts.\n- Kept branch `thread_id` inert, `CURRENT_NODE` inherited, and `simulate()` unchanged.\n- Updated validation rules and all three requested documentation pages.\n- Added unit, lifecycle, handler, integration, persistence, and lint regression coverage.\n- No changelog added.\n\n### Verification\n- `cargo nextest run --workspace`: **7,063 passed, 193 skipped**\n- `cargo +nightly-2026-04-14 fmt --check --all`: passed\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`: passed\n- `git diff --check`: passed\n\nLive testing-server probes were not run because the probe definitions and testing-server access were not available in this checkout/session."
},
"notes": "Stage completed: implement",
"usage": {
"input": {
"usage": {
"model": {
"provider": "openrouter",
"model_id": "openai/gpt-5.6-sol"
},
"tokens": {
"input_tokens": 394190,
"output_tokens": 32119,
"reasoning_tokens": 32341,
"cache_read_tokens": 13269488,
"cache_write_tokens": 886818
}
},
"facts": {
"algorithm": "openai"
}
}
},
"files_touched": [
"/home/daytona/workspace/fabro/docs/public/execution/context.mdx",
"/home/daytona/workspace/fabro/docs/public/reference/dot-language.mdx",
"/home/daytona/workspace/fabro/docs/public/workflows/stages-and-nodes.mdx",
"/home/daytona/workspace/fabro/lib/crates/fabro-validate/src/rules/mod.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-validate/src/rules/parallel_branch.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-validate/src/rules/parallel_branch_inert_attribute.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-validate/src/rules/thread_id_requires_fidelity_full.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/artifact.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/context.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/handler/parallel.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/lifecycle/event.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/lifecycle/fidelity.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/tests/it/integration.rs"
],
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 1625877,
"tool_time_ms": 1190483,
"active_time_ms": 2816360
}
},
"preflight_compile": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
},
"notes": "Script completed: cargo check -q --workspace 2>&1",
"usage": null,
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 0,
"tool_time_ms": 140243,
"active_time_ms": 140243
}
},
"simplify_fable": {
"status": "succeeded",
"context_updates": {
"last_response": "Everything is green. Summary of the simplify stage:\n\n## Review findings applied\n\n**Wire-format root cause (reuse #1, quality #1/#2, efficiency #2/#3):**\n- Added `Serialize`/`Deserialize` to `fabro_gra",
"response.simplify_fable": "Everything is green. Summary of the simplify stage:\n\n## Review findings applied\n\n**Wire-format root cause (reuse #1, quality #1/#2, efficiency #2/#3):**\n- Added `Serialize`/`Deserialize` to `fabro_graphviz::Fidelity` (serde renames aligned with strum per CLAUDE.md, plus `Hash` and a serde↔strum parity test; `serde_json` added as dev-dependency).\n- Replaced the hand-rolled JSON producer/parser pair with one shared `ParallelBranchPreamble` struct in `context.rs` (`deny_unknown_fields` replaces the brittle `entry.len() == 2` check). Producer uses `serde_json::to_value`, consumer uses `from_value` — a field rename can no longer silently split the contract. Documented the index-correlation invariant and the `Option<Option<…>>` semantics (quality #5/#8).\n\n**Duplication (reuse #2/#3, quality #6/#7):**\n- Extracted `explicit_fidelity()` — the edge→node precedence core is now shared between `resolve_fidelity` and `resolve_parallel_branch_fidelity`.\n- Added `keys::TRANSIENT_CONTEXT_KEYS` + `artifact::strip_transient_keys()`; both artifact strip sites use it. `stage_context_values` keeps its intentionally different behavior (retains `CURRENT_PREAMBLE`) with a WHY comment.\n- Unified the two copy-pasted full-fidelity diagnostic blocks into `full_fidelity_diagnostic()`; `full_fidelity_message()` → `const`; dead `fix_message` match → `const THREAD_ID_FIX`; capitalized the message for consistency; reworded the \"now honors\" changelog-style doc comment.\n\n**Structure & style (quality #3/#4/#9/#10/#14):**\n- Extracted the 38-line parallel stash block into `build_parallel_branch_preambles()`, restoring `before_node`'s numbered-step narrative (steps renumbered 5–9).\n- `ParallelBranchFidelityResolution`: dropped accessor ceremony and unused derives; plain documented fields.\n- Fixed the warn! to log `effective_fidelity` via `degraded()` instead of dressed-up constants; removed the redundant `fidelity` field.\n- `parallel.rs` uses `keys::`-scoped types; test helper `fidelity_attr` → `str_attr`.\n\n**Efficiency (#1, #4):**\n- Moved the parent stash clear to *before* the fork loop — branch forks no longer copy the full preamble array B times just to null it, and the per-branch null-set became unnecessary (forks inherit Null; nested-parallel safety preserved, verified by the existing `stash == Null` test assertions).\n- Memoized per-fidelity preamble renders — duplicate branch fidelities render once.\n\n**Validate helper (reuse #4, quality #13, efficiency #6):**\n- `ParallelBranches` now uses `Graph::incoming_edges`; `is_branch_only_node` is allocation-free; `branch_only_parents` replaced the dense peekable/`then`/`collect::<Option<BTreeSet>>` chain with straightforward code. Added the cross-rule suppression WHY comment in `thread_id_requires_fidelity_full`.\n\n**Skipped (noted, not addressed):** test-fixture consolidation into `rules/test_support` and a shared in-memory-store test helper (pre-existing pattern across ~15 sites, out of scope); `\"parallel\"` string literal (codebase-wide convention); `make_run_options` parameterization (follows existing file precedent).\n\n## Verification\n- `cargo nextest run -p fabro-graphviz -p fabro-validate -p fabro-workflow` — all pass (302 + 1218)\n- `cargo nextest run --workspace` — **7064 passed, 0 failed**\n- `cargo +nightly-2026-04-14 fmt --check --all` — clean\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` — clean\n\nNo blockers.",
"last_stage": "simplify_fable"
},
"notes": "Stage completed: simplify_fable",
"usage": {
"input": {
"usage": {
"model": {
"provider": "openrouter",
"model_id": "anthropic/claude-fable-5"
},
"tokens": {
"input_tokens": 8653765,
"output_tokens": 127619,
"reasoning_tokens": 33321,
"cache_read_tokens": 0,
"cache_write_tokens": 0
}
},
"facts": {
"algorithm": "openai"
}
}
},
"files_touched": [
"/home/daytona/workspace/fabro/lib/crates/fabro-graphviz/Cargo.toml",
"/home/daytona/workspace/fabro/lib/crates/fabro-graphviz/src/fidelity.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-validate/src/rules/parallel_branch.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-validate/src/rules/parallel_branch_inert_attribute.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-validate/src/rules/thread_id_requires_fidelity_full.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/artifact.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/context.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/handler/parallel.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/lifecycle/event.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/lifecycle/fidelity.rs"
],
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 2153413,
"tool_time_ms": 3192086,
"active_time_ms": 5345499
}
},
"start": {
"status": "succeeded",
"usage": null
}
},
"next_node_id": "simplify_sol",
"git_commit_sha": "56119990beb0cfbcbec078e331389ec8becc4e6c",
"node_visits": {
"simplify_fable": 1,
"preflight_lint": 1,
"implement": 1,
"start": 1,
"toolchain": 1,
"preflight_compile": 1
}
},
"diff": {
"patch": "diff --git a/Cargo.lock b/Cargo.lock\nindex 098d71cbf..6a4a8069d 100644\n--- a/Cargo.lock\n+++ b/Cargo.lock\n@@ -2659,6 +2659,7 @@ dependencies = [\n \"nom\",\n \"regex\",\n \"serde\",\n+ \"serde_json\",\n \"strum 0.28.0\",\n \"thiserror 2.0.18\",\n ]\ndiff --git a/lib/crates/fabro-graphviz/Cargo.toml b/lib/crates/fabro-graphviz/Cargo.toml\nindex 98e017097..3b33151d8 100644\n--- a/lib/crates/fabro-graphviz/Cargo.toml\n+++ b/lib/crates/fabro-graphviz/Cargo.toml\n@@ -21,3 +21,6 @@ regex = { workspace = true }\n serde = { workspace = true }\n strum.workspace = true\n thiserror = { workspace = true }\n+\n+[dev-dependencies]\n+serde_json = { workspace = true }\ndiff --git a/lib/crates/fabro-graphviz/src/fidelity.rs b/lib/crates/fabro-graphviz/src/fidelity.rs\nindex 8b33c98e8..98605c135 100644\n--- a/lib/crates/fabro-graphviz/src/fidelity.rs\n+++ b/lib/crates/fabro-graphviz/src/fidelity.rs\n@@ -1,9 +1,24 @@\n+use serde::{Deserialize, Serialize};\n use strum::{Display, EnumString, VariantArray};\n \n /// Fidelity mode controlling how much prior context is provided to LLM\n /// sessions.\n-#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Display, EnumString, VariantArray)]\n+#[derive(\n+ Debug,\n+ Clone,\n+ Copy,\n+ Default,\n+ PartialEq,\n+ Eq,\n+ Hash,\n+ Display,\n+ EnumString,\n+ VariantArray,\n+ Serialize,\n+ Deserialize,\n+)]\n #[strum(serialize_all = \"lowercase\")]\n+#[serde(rename_all = \"lowercase\")]\n pub enum Fidelity {\n /// Complete context, no summarization — sessions share a thread.\n Full,\n@@ -14,12 +29,15 @@ pub enum Fidelity {\n Compact,\n /// Brief textual summary (~600 token target).\n #[strum(serialize = \"summary:low\")]\n+ #[serde(rename = \"summary:low\")]\n SummaryLow,\n /// Moderate textual summary (~1500 token target).\n #[strum(serialize = \"summary:medium\")]\n+ #[serde(rename = \"summary:medium\")]\n SummaryMedium,\n /// Detailed per-stage Markdown report.\n #[strum(serialize = \"summary:high\")]\n+ #[serde(rename = \"summary:high\")]\n SummaryHigh,\n }\n \n@@ -73,4 +91,14 @@ mod tests {\n fn fidelity_unknown_mode_errors() {\n assert!(\"bogus\".parse::<Fidelity>().is_err());\n }\n+\n+ #[test]\n+ fn fidelity_serde_matches_strum_display() {\n+ for mode in Fidelity::variants() {\n+ let json = serde_json::to_value(mode).unwrap();\n+ assert_eq!(json, serde_json::Value::String(mode.to_string()));\n+ let parsed: Fidelity = serde_json::from_value(json).unwrap();\n+ assert_eq!(parsed, *mode);\n+ }\n+ }\n }\ndiff --git a/lib/crates/fabro-validate/src/rules/parallel_branch.rs b/lib/crates/fabro-validate/src/rules/parallel_branch.rs\nindex 2f1cba2e7..d8a7d2fbd 100644\n--- a/lib/crates/fabro-validate/src/rules/parallel_branch.rs\n+++ b/lib/crates/fabro-validate/src/rules/parallel_branch.rs\n@@ -35,22 +35,25 @@ impl<'a> ParallelBranches<'a> {\n .collect()\n }\n \n+ /// True when every incoming edge of `node_id` comes from a parallel fork\n+ /// (and there is at least one). Such a node only ever runs as a branch.\n pub(super) fn is_branch_only_node(&self, node_id: &str) -> bool {\n- self.branch_only_parents(node_id).is_some()\n+ let incoming = self.graph.incoming_edges(node_id);\n+ !incoming.is_empty() && incoming.iter().all(|edge| self.is_fork_edge(edge))\n }\n \n+ /// The sorted, deduplicated fork parents of a branch-only node, or `None`\n+ /// when the node has a non-fork entry path (or no entry at all).\n pub(super) fn branch_only_parents(&self, node_id: &str) -> Option<Vec<String>> {\n- let mut incoming = self\n+ if !self.is_branch_only_node(node_id) {\n+ return None;\n+ }\n+ let parents: BTreeSet<&str> = self\n .graph\n- .edges\n- .iter()\n- .filter(|edge| edge.to == node_id)\n- .peekable();\n- incoming.peek()?;\n-\n- incoming\n- .map(|edge| self.is_fork_edge(edge).then(|| edge.from.clone()))\n- .collect::<Option<BTreeSet<_>>>()\n- .map(|parents| parents.into_iter().collect())\n+ .incoming_edges(node_id)\n+ .into_iter()\n+ .map(|edge| edge.from.as_str())\n+ .collect();\n+ Some(parents.into_iter().map(String::from).collect())\n }\n }\ndiff --git a/lib/crates/fabro-validate/src/rules/parallel_branch_inert_attribute.rs b/lib/crates/fabro-validate/src/rules/parallel_branch_inert_attribute.rs\nindex da36dbbea..bac461103 100644\n--- a/lib/crates/fabro-validate/src/rules/parallel_branch_inert_attribute.rs\n+++ b/lib/crates/fabro-validate/src/rules/parallel_branch_inert_attribute.rs\n@@ -7,11 +7,15 @@ pub(super) fn rule() -> Box<dyn LintRule> {\n Box::new(Rule)\n }\n \n-/// Attributes that parallel branch execution does not resolve. Per-branch\n-/// preambles now honor fidelity, while `thread_id` remains inert because\n-/// concurrent branches cannot share an LLM session.\n+/// Attributes that parallel branch execution does not resolve. Only\n+/// `thread_id` is inert on branches (concurrent branches cannot share an LLM\n+/// session); per-branch `fidelity` is honored via pre-rendered preambles.\n const BRANCH_IGNORED_ATTRS: &[&str] = &[\"thread_id\"];\n \n+const FULL_FIDELITY_MESSAGE: &str = \"Parallel branches run at most at summary:high; full is degraded at runtime because branches cannot share a session\";\n+\n+const THREAD_ID_FIX: &str = \"Remove 'thread_id': parallel branches inherit the thread resolved when the parallel node started\";\n+\n struct Rule;\n \n /// Renders one or more parallel-node ids as `'a'` or `'a', 'b'`.\n@@ -22,19 +26,6 @@ fn quoted_list(ids: &[String]) -> String {\n .join(\", \")\n }\n \n-fn fix_message(attr: &str) -> String {\n- match attr {\n- \"thread_id\" => format!(\n- \"Remove '{attr}': parallel branches inherit the thread resolved when the parallel node started\"\n- ),\n- _ => format!(\"Remove '{attr}'\"),\n- }\n-}\n-\n-fn full_fidelity_message() -> String {\n- \"parallel branches run at most at summary:high; full is degraded at runtime because branches cannot share a session\".to_string()\n-}\n-\n fn full_fidelity_fix(parallel_ids: &[String]) -> String {\n let parent = if parallel_ids.len() == 1 {\n format!(\"parallel node {}\", quoted_list(parallel_ids))\n@@ -46,6 +37,23 @@ fn full_fidelity_fix(parallel_ids: &[String]) -> String {\n )\n }\n \n+fn full_fidelity_diagnostic(\n+ rule_name: &str,\n+ node_id: Option<String>,\n+ edge: Option<(String, String)>,\n+ parallel_ids: &[String],\n+) -> Diagnostic {\n+ Diagnostic {\n+ rule: rule_name.to_string(),\n+ severity: Severity::Warning,\n+ message: FULL_FIDELITY_MESSAGE.to_string(),\n+ node_id,\n+ edge,\n+ fix: Some(full_fidelity_fix(parallel_ids)),\n+ ..Diagnostic::default()\n+ }\n+}\n+\n impl LintRule for Rule {\n fn name(&self) -> &'static str {\n \"parallel_branch_inert_attribute\"\n@@ -64,15 +72,12 @@ impl LintRule for Rule {\n continue;\n }\n if edge.fidelity() == Some(\"full\") {\n- diagnostics.push(Diagnostic {\n- rule: self.name().to_string(),\n- severity: Severity::Warning,\n- message: full_fidelity_message(),\n- node_id: None,\n- edge: Some((edge.from.clone(), edge.to.clone())),\n- fix: Some(full_fidelity_fix(std::slice::from_ref(&edge.from))),\n- ..Diagnostic::default()\n- });\n+ diagnostics.push(full_fidelity_diagnostic(\n+ self.name(),\n+ None,\n+ Some((edge.from.clone(), edge.to.clone())),\n+ std::slice::from_ref(&edge.from),\n+ ));\n }\n for attr in BRANCH_IGNORED_ATTRS {\n if !edge.attrs.contains_key(*attr) {\n@@ -87,7 +92,7 @@ impl LintRule for Rule {\n ),\n node_id: None,\n edge: Some((edge.from.clone(), edge.to.clone())),\n- fix: Some(fix_message(attr)),\n+ fix: Some(THREAD_ID_FIX.to_string()),\n ..Diagnostic::default()\n });\n }\n@@ -103,15 +108,12 @@ impl LintRule for Rule {\n continue;\n };\n if node.fidelity() == Some(\"full\") {\n- diagnostics.push(Diagnostic {\n- rule: self.name().to_string(),\n- severity: Severity::Warning,\n- message: full_fidelity_message(),\n- node_id: Some(node.id.clone()),\n- edge: None,\n- fix: Some(full_fidelity_fix(&parents)),\n- ..Diagnostic::default()\n- });\n+ diagnostics.push(full_fidelity_diagnostic(\n+ self.name(),\n+ Some(node.id.clone()),\n+ None,\n+ &parents,\n+ ));\n }\n for attr in BRANCH_IGNORED_ATTRS {\n if !node.attrs.contains_key(*attr) {\n@@ -127,7 +129,7 @@ impl LintRule for Rule {\n ),\n node_id: Some(node.id.clone()),\n edge: None,\n- fix: Some(fix_message(attr)),\n+ fix: Some(THREAD_ID_FIX.to_string()),\n ..Diagnostic::default()\n });\n }\ndiff --git a/lib/crates/fabro-validate/src/rules/thread_id_requires_fidelity_full.rs b/lib/crates/fabro-validate/src/rules/thread_id_requires_fidelity_full.rs\nindex a73335da5..157b2a032 100644\n--- a/lib/crates/fabro-validate/src/rules/thread_id_requires_fidelity_full.rs\n+++ b/lib/crates/fabro-validate/src/rules/thread_id_requires_fidelity_full.rs\n@@ -23,6 +23,9 @@ impl LintRule for Rule {\n let graph_default_full = graph.default_fidelity() == Some(\"full\");\n let branches = ParallelBranches::new(graph);\n \n+ // thread_id is inert on parallel branches, where\n+ // parallel_branch_inert_attribute already says \"remove thread_id\" —\n+ // advising fidelity=\"full\" there would contradict it.\n for node in graph.nodes.values() {\n if node.thread_id().is_some()\n && !branches.is_branch_only_node(&node.id)\ndiff --git a/lib/crates/fabro-workflow/src/artifact.rs b/lib/crates/fabro-workflow/src/artifact.rs\nindex 975fee26d..bbf7f7ed8 100644\n--- a/lib/crates/fabro-workflow/src/artifact.rs\n+++ b/lib/crates/fabro-workflow/src/artifact.rs\n@@ -78,12 +78,18 @@ pub fn format_artifact_reference(path: &str) -> String {\n \n pub fn durable_context_snapshot(context: &Context) -> HashMap<String, Value> {\n let mut snapshot = context.snapshot();\n- snapshot.remove(context::keys::CURRENT_PREAMBLE);\n- snapshot.remove(context::keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES);\n+ strip_transient_keys(&mut snapshot);\n normalize_durable_updates(&mut snapshot);\n snapshot\n }\n \n+/// Remove runtime-only keys that must never reach durable storage or events.\n+pub fn strip_transient_keys(values: &mut HashMap<String, Value>) {\n+ for key in context::keys::TRANSIENT_CONTEXT_KEYS {\n+ values.remove(*key);\n+ }\n+}\n+\n pub fn normalize_durable_updates(updates: &mut HashMap<String, Value>) {\n for value in updates.values_mut() {\n normalize_durable_value(value);\n@@ -97,12 +103,7 @@ pub fn normalize_durable_outcomes(node_outcomes: &mut HashMap<String, Outcome>)\n }\n \n pub fn normalize_checkpoint_for_resume(checkpoint: &mut Checkpoint) {\n- checkpoint\n- .context_values\n- .remove(context::keys::CURRENT_PREAMBLE);\n- checkpoint\n- .context_values\n- .remove(context::keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES);\n+ strip_transient_keys(&mut checkpoint.context_values);\n normalize_durable_updates(&mut checkpoint.context_values);\n normalize_durable_outcomes(&mut checkpoint.node_outcomes);\n }\ndiff --git a/lib/crates/fabro-workflow/src/context.rs b/lib/crates/fabro-workflow/src/context.rs\nindex b911e2210..a890c0e8f 100644\n--- a/lib/crates/fabro-workflow/src/context.rs\n+++ b/lib/crates/fabro-workflow/src/context.rs\n@@ -25,6 +25,9 @@ pub mod keys {\n pub const INTERNAL_PARENT_PREAMBLE: &str = \"internal.parent_preamble\";\n pub const INTERNAL_PARALLEL_GROUP_ID: &str = \"internal.parallel_group_id\";\n pub const INTERNAL_PARALLEL_BRANCH_ID: &str = \"internal.parallel_branch_id\";\n+ /// Stash of pre-rendered per-branch preambles for a parallel node; see\n+ /// [`super::ParallelBranchPreamble`] for the entry shape and the\n+ /// producer/consumer contract.\n pub const INTERNAL_PARALLEL_BRANCH_PREAMBLES: &str = \"internal.parallel_branch_preambles\";\n \n // --- current.* keys ---\n@@ -45,6 +48,12 @@ pub mod keys {\n pub const PARALLEL_FAN_IN_BEST_OUTCOME: &str = \"parallel.fan_in.best_outcome\";\n pub const PARALLEL_FAN_IN_BEST_HEAD_SHA: &str = \"parallel.fan_in.best_head_sha\";\n \n+ /// Runtime-only keys stripped from durable context projections\n+ /// (checkpoint snapshots and resume normalization). Add new transient\n+ /// keys here so both strip sites stay in sync.\n+ pub const TRANSIENT_CONTEXT_KEYS: &[&str] =\n+ &[CURRENT_PREAMBLE, INTERNAL_PARALLEL_BRANCH_PREAMBLES];\n+\n // --- Prefix constants (for filtering and dynamic keys) ---\n pub const GRAPH_PREFIX: &str = \"graph.\";\n pub const INTERNAL_PREFIX: &str = \"internal.\";\n@@ -136,9 +145,24 @@ pub mod keys {\n pub use fabro_core::Context;\n use fabro_graphviz::Fidelity;\n use fabro_types::{ParallelBranchId, StageId};\n+use serde::{Deserialize, Serialize};\n \n use crate::event::StageScope;\n \n+/// One entry of the [`keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES`] stash.\n+///\n+/// The stash is a JSON array indexed by the parallel node's outgoing-edge\n+/// order (`Graph::outgoing_edges` preserves declaration order, so producer and\n+/// consumer align even with duplicate targets). `null` entries mean the branch\n+/// inherits the fork's preamble. `FidelityLifecycle::before_node` produces the\n+/// stash; `ParallelHandler::execute` consumes and clears it.\n+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n+#[serde(deny_unknown_fields)]\n+pub struct ParallelBranchPreamble {\n+ pub fidelity: Fidelity,\n+ pub preamble: String,\n+}\n+\n /// Domain-specific typed accessors for workflow context values.\n pub trait WorkflowContext {\n fn fidelity(&self) -> Fidelity;\ndiff --git a/lib/crates/fabro-workflow/src/handler/parallel.rs b/lib/crates/fabro-workflow/src/handler/parallel.rs\nindex ac4e4f3e1..32e4a8b10 100644\n--- a/lib/crates/fabro-workflow/src/handler/parallel.rs\n+++ b/lib/crates/fabro-workflow/src/handler/parallel.rs\n@@ -4,14 +4,13 @@ use std::time::Instant;\n \n use async_trait::async_trait;\n use fabro_agent::{Sandbox, WorktreeOptions, WorktreeSandbox};\n-use fabro_graphviz::Fidelity;\n use fabro_graphviz::graph::{AttrValue, Graph, Node};\n use fabro_hooks::{HookContext, HookEvent};\n use fabro_types::{ParallelBranchId, RunId, StageId};\n use tokio::sync::Semaphore;\n \n use super::{EngineServices, Handler};\n-use crate::context::{Context, WorkflowContext, keys};\n+use crate::context::{Context, ParallelBranchPreamble, WorkflowContext, keys};\n use crate::error::Error;\n use crate::event::{Event, RunNoticeCode, RunNoticeLevel, StageScope};\n use crate::git::sanitize_ref_component;\n@@ -57,15 +56,15 @@ struct BranchResult {\n worktree_path: Option<PathBuf>,\n }\n \n-struct BranchPreamble {\n- fidelity: Fidelity,\n- preamble: String,\n-}\n-\n+/// Parse the per-branch preamble stash produced by `FidelityLifecycle`.\n+///\n+/// Outer `None` means the stash is absent, malformed, or has the wrong branch\n+/// count — every branch then inherits the fork context (legacy behavior).\n+/// Inner `None` means that single branch inherits.\n fn parse_branch_preambles(\n value: Option<serde_json::Value>,\n branch_count: usize,\n-) -> Option<Vec<Option<BranchPreamble>>> {\n+) -> Option<Vec<Option<ParallelBranchPreamble>>> {\n let serde_json::Value::Array(entries) = value? else {\n return None;\n };\n@@ -77,15 +76,7 @@ fn parse_branch_preambles(\n .into_iter()\n .map(|entry| match entry {\n serde_json::Value::Null => Some(None),\n- serde_json::Value::Object(entry) if entry.len() == 2 => {\n- let fidelity = entry.get(\"fidelity\")?.as_str()?.parse().ok()?;\n- let preamble = entry.get(\"preamble\")?.as_str()?;\n- Some(Some(BranchPreamble {\n- fidelity,\n- preamble: preamble.to_string(),\n- }))\n- }\n- _ => None,\n+ entry => serde_json::from_value(entry).ok().map(Some),\n })\n .collect()\n }\n@@ -258,6 +249,13 @@ impl Handler for ParallelHandler {\n context.get(keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES),\n branches.len(),\n );\n+ // Clear the stash before forking so branch contexts never carry the\n+ // outer array — a nested parallel branch target must not misread it as\n+ // its own. The write-back diff also clears it on the run state.\n+ context.set(\n+ keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES,\n+ serde_json::Value::Null,\n+ );\n let mut branch_setups: Vec<BranchSetup> = Vec::new();\n for (branch_index, edge) in branches.iter().enumerate() {\n let target_id = edge.to.clone();\n@@ -279,16 +277,15 @@ impl Handler for ParallelHandler {\n .and_then(|entries| entries.get(branch_index))\n .and_then(Option::as_ref)\n {\n- branch_context.set(keys::CURRENT_PREAMBLE, serde_json::json!(&entry.preamble));\n+ branch_context.set(\n+ keys::CURRENT_PREAMBLE,\n+ serde_json::Value::String(entry.preamble.clone()),\n+ );\n branch_context.set(\n keys::INTERNAL_FIDELITY,\n- serde_json::json!(entry.fidelity.to_string()),\n+ serde_json::Value::String(entry.fidelity.to_string()),\n );\n }\n- branch_context.set(\n- keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES,\n- serde_json::Value::Null,\n- );\n \n let (branch_sandbox, worktree_path): (Arc<dyn Sandbox>, Option<PathBuf>) = if let (\n Some(ref gs),\n@@ -350,10 +347,6 @@ impl Handler for ParallelHandler {\n worktree_path,\n });\n }\n- context.set(\n- keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES,\n- serde_json::Value::Null,\n- );\n \n // --- Fan out: concurrent execution ---\n let mut handles = Vec::new();\ndiff --git a/lib/crates/fabro-workflow/src/lifecycle/event.rs b/lib/crates/fabro-workflow/src/lifecycle/event.rs\nindex 3965d7261..f547f5fe5 100644\n--- a/lib/crates/fabro-workflow/src/lifecycle/event.rs\n+++ b/lib/crates/fabro-workflow/src/lifecycle/event.rs\n@@ -92,6 +92,10 @@ fn response_from_outcome(node_id: &str, outcome: &Outcome) -> Option<String> {\n .and_then(|value| value.as_str().map(ToOwned::to_owned))\n }\n \n+/// Context values for `StageCompleted` events. Unlike\n+/// `artifact::strip_transient_keys`, this keeps `CURRENT_PREAMBLE` — stage\n+/// events have always included the active preamble — and drops only the\n+/// parallel stash, which can embed every branch's rendered preamble.\n fn stage_context_values(workflow_context: &Context) -> Option<BTreeMap<String, serde_json::Value>> {\n let mut snapshot = workflow_context.snapshot();\n snapshot.remove(context::keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES);\ndiff --git a/lib/crates/fabro-workflow/src/lifecycle/fidelity.rs b/lib/crates/fabro-workflow/src/lifecycle/fidelity.rs\nindex 3f0b24228..437550cc1 100644\n--- a/lib/crates/fabro-workflow/src/lifecycle/fidelity.rs\n+++ b/lib/crates/fabro-workflow/src/lifecycle/fidelity.rs\n@@ -1,3 +1,4 @@\n+use std::collections::HashMap;\n use std::path::PathBuf;\n use std::sync::{Arc, Mutex};\n \n@@ -10,10 +11,10 @@ use fabro_core::state::ExecutionState;\n use fabro_graphviz::graph::types::{Edge as GvEdge, Graph as GvGraph, Node as GvNode};\n \n use crate::artifact;\n-use crate::context::keys;\n+use crate::context::{Context, ParallelBranchPreamble, keys};\n use crate::graph::{WorkflowGraph, WorkflowNode};\n use crate::handler::llm::preamble::build_preamble;\n-use crate::outcome::BilledModelUsage;\n+use crate::outcome::{BilledModelUsage, Outcome};\n use crate::runtime_store::RunStoreHandle;\n \n type WfRunState = ExecutionState<Option<BilledModelUsage>>;\n@@ -61,6 +62,60 @@ impl FidelityLifecycle {\n \"fidelity mutex should not be poisoned: no code panics while holding this lock\",\n ) = flag;\n }\n+\n+ /// Render the per-branch preamble stash for a parallel node, indexed by\n+ /// outgoing-edge order (the same order `ParallelHandler` fans out in).\n+ /// `Null` entries inherit the fork's preamble.\n+ fn build_parallel_branch_preambles(\n+ &self,\n+ node_id: &str,\n+ fork_fidelity: keys::Fidelity,\n+ resolved_context: &Context,\n+ resolved_outcomes: &HashMap<String, Outcome>,\n+ completed_nodes: &[String],\n+ ) -> Vec<serde_json::Value> {\n+ let mut rendered: HashMap<keys::Fidelity, serde_json::Value> = HashMap::new();\n+ self.graph\n+ .outgoing_edges(node_id)\n+ .into_iter()\n+ .enumerate()\n+ .map(|(branch_index, edge)| {\n+ let Some(target_node) = self.graph.nodes.get(&edge.to) else {\n+ return serde_json::Value::Null;\n+ };\n+ let resolution = resolve_parallel_branch_fidelity(edge, target_node, fork_fidelity);\n+ if resolution.requested == Some(keys::Fidelity::Full) {\n+ tracing::warn!(\n+ parallel_node = %node_id,\n+ branch = %edge.to,\n+ branch_index,\n+ effective_fidelity = %keys::Fidelity::Full.degraded(),\n+ \"Parallel branch fidelity degraded from full\"\n+ );\n+ }\n+ let Some(branch_fidelity) = resolution.effective else {\n+ return serde_json::Value::Null;\n+ };\n+ rendered\n+ .entry(branch_fidelity)\n+ .or_insert_with(|| {\n+ let entry = ParallelBranchPreamble {\n+ fidelity: branch_fidelity,\n+ preamble: build_preamble(\n+ branch_fidelity,\n+ resolved_context,\n+ &self.graph,\n+ completed_nodes,\n+ resolved_outcomes,\n+ ),\n+ };\n+ serde_json::to_value(entry)\n+ .expect(\"ParallelBranchPreamble serialization cannot fail\")\n+ })\n+ .clone()\n+ })\n+ .collect()\n+ }\n }\n \n #[async_trait]\n@@ -143,47 +198,23 @@ impl RunLifecycle<WorkflowGraph> for FidelityLifecycle {\n .context\n .set(keys::CURRENT_PREAMBLE, serde_json::json!(preamble));\n \n+ // 5. Parallel nodes: pre-render per-branch preambles into the stash that\n+ // ParallelHandler consumes at fan-out.\n if gv_node.handler_type() == Some(\"parallel\") {\n- let mut branch_preambles = Vec::new();\n- for (branch_index, edge) in self.graph.outgoing_edges(node.id()).iter().enumerate() {\n- let Some(target_node) = self.graph.nodes.get(&edge.to) else {\n- branch_preambles.push(serde_json::Value::Null);\n- continue;\n- };\n- let resolution = resolve_parallel_branch_fidelity(edge, target_node, fidelity);\n- if resolution.requested() == Some(keys::Fidelity::Full) {\n- tracing::warn!(\n- parallel_node = %node.id(),\n- branch = %edge.to,\n- branch_index,\n- fidelity = %keys::Fidelity::Full,\n- effective_fidelity = %keys::Fidelity::SummaryHigh,\n- \"Parallel branch fidelity degraded\"\n- );\n- }\n- let Some(branch_fidelity) = resolution.entry_fidelity() else {\n- branch_preambles.push(serde_json::Value::Null);\n- continue;\n- };\n- let branch_preamble = build_preamble(\n- branch_fidelity,\n- &resolved_context,\n- &self.graph,\n- &state.completed_nodes,\n- &resolved_outcomes,\n- );\n- branch_preambles.push(serde_json::json!({\n- \"fidelity\": branch_fidelity.to_string(),\n- \"preamble\": branch_preamble,\n- }));\n- }\n+ let branch_preambles = self.build_parallel_branch_preambles(\n+ node.id(),\n+ fidelity,\n+ &resolved_context,\n+ &resolved_outcomes,\n+ &state.completed_nodes,\n+ );\n state.context.set(\n keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES,\n serde_json::Value::Array(branch_preambles),\n );\n }\n \n- // 5. Thread ID resolution via resolve_thread_id: edge → node → graph default →\n+ // 6. Thread ID resolution via resolve_thread_id: edge → node → graph default →\n // class → previous\n let thread_id = resolve_thread_id(\n incoming_edge_ref,\n@@ -192,13 +223,13 @@ impl RunLifecycle<WorkflowGraph> for FidelityLifecycle {\n state.previous_node_id.as_deref(),\n );\n \n- // 6. Set thread.{tid}.current_node\n+ // 7. Set thread.{tid}.current_node\n if let Some(ref tid) = thread_id {\n let key = keys::thread_current_node_key(tid);\n state.context.set(key, serde_json::json!(node.id()));\n }\n \n- // 7. Set INTERNAL_THREAD_ID (or null)\n+ // 8. Set INTERNAL_THREAD_ID (or null)\n match thread_id {\n Some(tid) => {\n state\n@@ -212,7 +243,7 @@ impl RunLifecycle<WorkflowGraph> for FidelityLifecycle {\n }\n }\n \n- // 8. Set INTERNAL_NODE_VISIT_COUNT and CURRENT_NODE\n+ // 9. Set INTERNAL_NODE_VISIT_COUNT and CURRENT_NODE\n let visits = state.node_visits.get(node.id()).copied().unwrap_or(1);\n state\n .context\n@@ -243,22 +274,14 @@ impl RunLifecycle<WorkflowGraph> for FidelityLifecycle {\n }\n }\n \n-#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n+#[derive(Debug, Clone, Copy)]\n struct ParallelBranchFidelityResolution {\n+ /// The explicit fidelity requested on the edge or node, pre-degradation.\n requested: Option<keys::Fidelity>,\n+ /// The fidelity to render an entry for; `None` inherits the fork preamble.\n effective: Option<keys::Fidelity>,\n }\n \n-impl ParallelBranchFidelityResolution {\n- fn requested(self) -> Option<keys::Fidelity> {\n- self.requested\n- }\n-\n- fn entry_fidelity(self) -> Option<keys::Fidelity> {\n- self.effective\n- }\n-}\n-\n /// Resolve explicit branch fidelity with edge-over-node precedence.\n ///\n /// Branches with no explicit fidelity inherit the parallel node's preamble.\n@@ -270,10 +293,7 @@ fn resolve_parallel_branch_fidelity(\n target_node: &GvNode,\n parallel_fidelity: keys::Fidelity,\n ) -> ParallelBranchFidelityResolution {\n- let requested = edge\n- .fidelity()\n- .and_then(|value| value.parse().ok())\n- .or_else(|| target_node.fidelity().and_then(|value| value.parse().ok()));\n+ let requested = explicit_fidelity(Some(edge), target_node).map(|(fidelity, _)| fidelity);\n let effective = requested\n .map(keys::Fidelity::degraded)\n .filter(|fidelity| *fidelity != parallel_fidelity);\n@@ -284,6 +304,23 @@ fn resolve_parallel_branch_fidelity(\n }\n }\n \n+/// Explicit fidelity from the incoming edge attribute, else the node\n+/// attribute, with the winning source labeled for logging.\n+fn explicit_fidelity(\n+ incoming_edge: Option<&GvEdge>,\n+ node: &GvNode,\n+) -> Option<(keys::Fidelity, &'static str)> {\n+ incoming_edge\n+ .and_then(|e| e.fidelity())\n+ .and_then(|s| s.parse().ok())\n+ .map(|f| (f, \"edge\"))\n+ .or_else(|| {\n+ node.fidelity()\n+ .and_then(|s| s.parse().ok())\n+ .map(|f| (f, \"node\"))\n+ })\n+}\n+\n /// Resolve the context fidelity for a node, following the precedence:\n /// 1. Incoming edge `fidelity` attribute\n /// 2. Target node `fidelity` attribute\n@@ -294,13 +331,8 @@ fn resolve_fidelity(\n node: &GvNode,\n graph: &GvGraph,\n ) -> keys::Fidelity {\n- let (resolved, source) = if let Some(f) = incoming_edge\n- .and_then(|e| e.fidelity())\n- .and_then(|s| s.parse().ok())\n- {\n- (f, \"edge\")\n- } else if let Some(f) = node.fidelity().and_then(|s| s.parse().ok()) {\n- (f, \"node\")\n+ let (resolved, source) = if let Some((f, source)) = explicit_fidelity(incoming_edge, node) {\n+ (f, source)\n } else if let Some(f) = graph.default_fidelity().and_then(|s| s.parse().ok()) {\n (f, \"graph\")\n } else {\n@@ -362,7 +394,7 @@ mod tests {\n use crate::context::WorkflowContext;\n use crate::context::keys::Fidelity;\n \n- fn fidelity_attr(value: &str) -> AttrValue {\n+ fn str_attr(value: &str) -> AttrValue {\n AttrValue::String(value.to_string())\n }\n \n@@ -374,23 +406,23 @@ mod tests {\n let mut start = Node::new(\"start\");\n start\n .attrs\n- .insert(\"shape\".to_string(), fidelity_attr(\"Mdiamond\"));\n+ .insert(\"shape\".to_string(), str_attr(\"Mdiamond\"));\n let mut fork = Node::new(\"fork\");\n fork.attrs\n- .insert(\"shape\".to_string(), fidelity_attr(\"component\"));\n+ .insert(\"shape\".to_string(), str_attr(\"component\"));\n if let Some(fidelity) = fork_fidelity {\n fork.attrs\n- .insert(\"fidelity\".to_string(), fidelity_attr(fidelity));\n+ .insert(\"fidelity\".to_string(), str_attr(fidelity));\n }\n let mut branch_a = Node::new(\"branch_a\");\n if let Some(fidelity) = branch_a_fidelity {\n branch_a\n .attrs\n- .insert(\"fidelity\".to_string(), fidelity_attr(fidelity));\n+ .insert(\"fidelity\".to_string(), str_attr(fidelity));\n }\n let branch_b = Node::new(\"branch_b\");\n let mut work = Node::new(\"work\");\n- work.attrs.insert(\"shape\".to_string(), fidelity_attr(\"box\"));\n+ work.attrs.insert(\"shape\".to_string(), str_attr(\"box\"));\n \n graph.nodes.insert(start.id.clone(), start);\n graph.nodes.insert(fork.id.clone(), fork);\n@@ -426,15 +458,15 @@ mod tests {\n fn parallel_branch_fidelity_edge_overrides_node() {\n let mut node = Node::new(\"branch\");\n node.attrs\n- .insert(\"fidelity\".to_string(), fidelity_attr(\"compact\"));\n+ .insert(\"fidelity\".to_string(), str_attr(\"compact\"));\n let mut edge = Edge::new(\"fork\", \"branch\");\n edge.attrs\n- .insert(\"fidelity\".to_string(), fidelity_attr(\"truncate\"));\n+ .insert(\"fidelity\".to_string(), str_attr(\"truncate\"));\n \n let resolved = resolve_parallel_branch_fidelity(&edge, &node, Fidelity::SummaryHigh);\n \n- assert_eq!(resolved.requested(), Some(Fidelity::Truncate));\n- assert_eq!(resolved.entry_fidelity(), Some(Fidelity::Truncate));\n+ assert_eq!(resolved.requested, Some(Fidelity::Truncate));\n+ assert_eq!(resolved.effective, Some(Fidelity::Truncate));\n }\n \n #[test]\n@@ -444,47 +476,45 @@ mod tests {\n \n let resolution = resolve_parallel_branch_fidelity(&edge, &node, Fidelity::Compact);\n \n- assert_eq!(resolution.requested(), None);\n- assert_eq!(resolution.entry_fidelity(), None);\n+ assert_eq!(resolution.requested, None);\n+ assert_eq!(resolution.effective, None);\n }\n \n #[test]\n fn parallel_branch_full_fidelity_degrades_to_summary_high() {\n let mut node = Node::new(\"branch\");\n- node.attrs\n- .insert(\"fidelity\".to_string(), fidelity_attr(\"full\"));\n+ node.attrs.insert(\"fidelity\".to_string(), str_attr(\"full\"));\n let edge = Edge::new(\"fork\", \"branch\");\n \n let resolved = resolve_parallel_branch_fidelity(&edge, &node, Fidelity::Compact);\n \n- assert_eq!(resolved.requested(), Some(Fidelity::Full));\n- assert_eq!(resolved.entry_fidelity(), Some(Fidelity::SummaryHigh));\n+ assert_eq!(resolved.requested, Some(Fidelity::Full));\n+ assert_eq!(resolved.effective, Some(Fidelity::SummaryHigh));\n }\n \n #[test]\n fn parallel_branch_fidelity_equal_to_fork_inherits() {\n let mut node = Node::new(\"branch\");\n node.attrs\n- .insert(\"fidelity\".to_string(), fidelity_attr(\"summary:high\"));\n+ .insert(\"fidelity\".to_string(), str_attr(\"summary:high\"));\n let edge = Edge::new(\"fork\", \"branch\");\n \n let resolution = resolve_parallel_branch_fidelity(&edge, &node, Fidelity::SummaryHigh);\n \n- assert_eq!(resolution.requested(), Some(Fidelity::SummaryHigh));\n- assert_eq!(resolution.entry_fidelity(), None);\n+ assert_eq!(resolution.requested, Some(Fidelity::SummaryHigh));\n+ assert_eq!(resolution.effective, None);\n }\n \n #[test]\n fn explicit_full_branch_equal_to_degraded_fork_inherits() {\n let mut node = Node::new(\"branch\");\n- node.attrs\n- .insert(\"fidelity\".to_string(), fidelity_attr(\"full\"));\n+ node.attrs.insert(\"fidelity\".to_string(), str_attr(\"full\"));\n let edge = Edge::new(\"fork\", \"branch\");\n \n let resolution = resolve_parallel_branch_fidelity(&edge, &node, Fidelity::SummaryHigh);\n \n- assert_eq!(resolution.requested(), Some(Fidelity::Full));\n- assert_eq!(resolution.entry_fidelity(), None);\n+ assert_eq!(resolution.requested, Some(Fidelity::Full));\n+ assert_eq!(resolution.effective, None);\n }\n \n #[test]\n@@ -494,8 +524,8 @@ mod tests {\n \n let resolution = resolve_parallel_branch_fidelity(&edge, &node, Fidelity::Full);\n \n- assert_eq!(resolution.requested(), None);\n- assert_eq!(resolution.entry_fidelity(), None);\n+ assert_eq!(resolution.requested, None);\n+ assert_eq!(resolution.effective, None);\n }\n \n #[tokio::test]\n",
"summary": {
"files_changed": 16,
"additions": 1209,
"deletions": 101
}
}
},
{
"seq": 3842,
"checkpoint": {
"timestamp": "2026-07-23T20:29:21.166129354Z",
"current_node": "simplify_sol",
"completed_nodes": [
"start",
"toolchain",
"preflight_compile",
"preflight_lint",
"implement",
"simplify_fable",
"simplify_sol"
],
"node_retries": {},
"context_values": {
"internal.retry_count.start": 0,
"internal.run_id": "01KY7Y01REECZ24XXTMBZ3PPV9",
"internal.retry_count.preflight_lint": 0,
"graph.rankdir": "LR",
"internal.retry_count.simplify_fable": 0,
"current_node": "simplify_sol",
"response.implement": "Implemented the complete per-branch fidelity plan.\n\n### Highlights\n- Added ordered per-branch preamble stash generation and consumption.\n- Implemented edge → branch node → fork inheritance precedence.\n- Explicit branch `full` degrades to `summary:high` with structured logging.\n- Preserved byte-identical fork inheritance for unspecified/equal fidelities.\n- Added strict legacy fallback for missing, null, malformed, non-array, and mismatched stashes.\n- Cleared branch stashes for nested-parallel safety.\n- Removed stash data from checkpoints, durable snapshots, and persisted event contexts.\n- Kept branch `thread_id` inert, `CURRENT_NODE` inherited, and `simulate()` unchanged.\n- Updated validation rules and all three requested documentation pages.\n- Added unit, lifecycle, handler, integration, persistence, and lint regression coverage.\n- No changelog added.\n\n### Verification\n- `cargo nextest run --workspace`: **7,063 passed, 193 skipped**\n- `cargo +nightly-2026-04-14 fmt --check --all`: passed\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`: passed\n- `git diff --check`: passed\n\nLive testing-server probes were not run because the probe definitions and testing-server access were not available in this checkout/session.",
"response.simplify_fable": "Everything is green. Summary of the simplify stage:\n\n## Review findings applied\n\n**Wire-format root cause (reuse #1, quality #1/#2, efficiency #2/#3):**\n- Added `Serialize`/`Deserialize` to `fabro_graphviz::Fidelity` (serde renames aligned with strum per CLAUDE.md, plus `Hash` and a serde↔strum parity test; `serde_json` added as dev-dependency).\n- Replaced the hand-rolled JSON producer/parser pair with one shared `ParallelBranchPreamble` struct in `context.rs` (`deny_unknown_fields` replaces the brittle `entry.len() == 2` check). Producer uses `serde_json::to_value`, consumer uses `from_value` — a field rename can no longer silently split the contract. Documented the index-correlation invariant and the `Option<Option<…>>` semantics (quality #5/#8).\n\n**Duplication (reuse #2/#3, quality #6/#7):**\n- Extracted `explicit_fidelity()` — the edge→node precedence core is now shared between `resolve_fidelity` and `resolve_parallel_branch_fidelity`.\n- Added `keys::TRANSIENT_CONTEXT_KEYS` + `artifact::strip_transient_keys()`; both artifact strip sites use it. `stage_context_values` keeps its intentionally different behavior (retains `CURRENT_PREAMBLE`) with a WHY comment.\n- Unified the two copy-pasted full-fidelity diagnostic blocks into `full_fidelity_diagnostic()`; `full_fidelity_message()` → `const`; dead `fix_message` match → `const THREAD_ID_FIX`; capitalized the message for consistency; reworded the \"now honors\" changelog-style doc comment.\n\n**Structure & style (quality #3/#4/#9/#10/#14):**\n- Extracted the 38-line parallel stash block into `build_parallel_branch_preambles()`, restoring `before_node`'s numbered-step narrative (steps renumbered 5–9).\n- `ParallelBranchFidelityResolution`: dropped accessor ceremony and unused derives; plain documented fields.\n- Fixed the warn! to log `effective_fidelity` via `degraded()` instead of dressed-up constants; removed the redundant `fidelity` field.\n- `parallel.rs` uses `keys::`-scoped types; test helper `fidelity_attr` → `str_attr`.\n\n**Efficiency (#1, #4):**\n- Moved the parent stash clear to *before* the fork loop — branch forks no longer copy the full preamble array B times just to null it, and the per-branch null-set became unnecessary (forks inherit Null; nested-parallel safety preserved, verified by the existing `stash == Null` test assertions).\n- Memoized per-fidelity preamble renders — duplicate branch fidelities render once.\n\n**Validate helper (reuse #4, quality #13, efficiency #6):**\n- `ParallelBranches` now uses `Graph::incoming_edges`; `is_branch_only_node` is allocation-free; `branch_only_parents` replaced the dense peekable/`then`/`collect::<Option<BTreeSet>>` chain with straightforward code. Added the cross-rule suppression WHY comment in `thread_id_requires_fidelity_full`.\n\n**Skipped (noted, not addressed):** test-fixture consolidation into `rules/test_support` and a shared in-memory-store test helper (pre-existing pattern across ~15 sites, out of scope); `\"parallel\"` string literal (codebase-wide convention); `make_run_options` parameterization (follows existing file precedent).\n\n## Verification\n- `cargo nextest run -p fabro-graphviz -p fabro-validate -p fabro-workflow` — all pass (302 + 1218)\n- `cargo nextest run --workspace` — **7064 passed, 0 failed**\n- `cargo +nightly-2026-04-14 fmt --check --all` — clean\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` — clean\n\nNo blockers.",
"thread.simplify_fable.current_node": "simplify_sol",
"thread.preflight_lint.current_node": "implement",
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
"internal.fidelity": "compact",
"graph.goal": "# Per-Branch Fidelity for Parallel Branches — Implementation Plan\n\n## Context\n\nParallel branch nodes are dispatched via `dispatch_handler`, bypassing `FidelityLifecycle::before_node` (`lib/crates/fabro-workflow/src/lifecycle/fidelity.rs:76-180`) — the only place fidelity is resolved and preambles are built. Every branch therefore inherits the stale `current.preamble` copied at `context.fork()` (`handler/parallel.rs:226`), and `fidelity`/`thread_id` on branch nodes or `fork -> branch` edges are silently ignored. Confirmed live on the testing server (probes `01KY7KRA7E…`/`01KY7KRAAW…`, 2026-07-23): a `truncate` branch received the identical compact preamble as its default sibling; fork-level fidelity works and is the current workaround. Design reviewed via the Quarry doc \"Fix: Per-Branch Fidelity for Parallel Branches\".\n\nWhy not run the lifecycle per branch: it is a single-token state machine (one-slot `incoming_edge_data` baton, singleton context keys written to shared run state); concurrent invocation would corrupt run state. And `build_preamble` (`handler/llm/preamble.rs:24`, public and pure) needs `state.completed_nodes`/`state.node_outcomes`, which only the lifecycle sees. So: **pre-render per-branch preambles in the lifecycle, hand off to the handler via one context key.**\n\n## Semantics (final, after design pressure-test)\n\n- **Explicit-only resolution.** A branch's fidelity comes from the `fork -> branch` edge attr, else the branch node attr, else **no entry** — the branch inherits the fork's preamble via `fork()` exactly as today. The fork's own resolved fidelity is never re-applied per branch; this keeps the default path byte-identical even when the fork resolves `Full` (where re-derivation would have wrongly degraded every branch).\n- **`full` degrades to `summary:high`** (`Fidelity::degraded()`, `fabro-graphviz/src/fidelity.rs:35-40`) — applied only to *explicitly set* branch fidelity, with a log line (per `docs/internal/logging-strategy.md` — read before writing it).\n- **Equality skip**: if the branch's post-degradation fidelity equals the fork's post-degradation fidelity, store no entry (avoid redundant renders).\n- **`thread_id` stays inert in branches** (concurrent branches must never share an LLM session).\n- **`CURRENT_NODE` in branch contexts stays inherited (fork id).** The pressure-test showed changing it would re-attribute every branch-internal event's stage scope (`context.rs:185-190` → `StageScope::for_handler` used by all handlers) with a visit mismatch against `for_parallel_branch` scoping. The Quarry doc's \"bookkeeping keys describe the branch\" line is consciously deferred to a separate change with proper visit accounting.\n- **`simulate()` untouched.** No simulated handler reads preambles; partial mirroring would risk nested-parallel stash misreads. All-or-nothing → nothing.\n- **Stash shape**: `Value::Array`, length = branch count, `Null` = inherit, else `{\"fidelity\": \"...\", \"preamble\": \"...\"}`. Array length ≠ edge count → treat as absent (legacy). Keyed by edge index; `graph.outgoing_edges` is an ordered Vec filter (`fabro-types/src/graph.rs:393-395`) and lifecycle + handler share the same `Arc<GvGraph>`, so indices align deterministically (including two edges to the same target).\n\n## Implementation steps (ordered; tree compiles at each step)\n\n1. **`lib/crates/fabro-workflow/src/context.rs`** — add `pub const INTERNAL_PARALLEL_BRANCH_PREAMBLES: &str = \"internal.parallel_branch_preambles\";` to `keys`. The `internal.` prefix already excludes it from preamble rendering (`preamble.rs:99-109`) and child→parent propagation (`context.rs:80-85`).\n\n2. **`lib/crates/fabro-workflow/src/artifact.rs`** — strip the new key in `durable_context_snapshot` (`:81`) and `normalize_checkpoint_for_resume` (`:101`), beside `CURRENT_PREAMBLE`. Without this, every post-parallel checkpoint and `CheckpointCompleted` event payload carries the full per-branch preamble map (a `summary:high` preamble embeds up to 50 lines of every command output — multiplied per branch).\n\n3. **`lib/crates/fabro-workflow/src/lifecycle/fidelity.rs`** — in `before_node`:\n - Set the stash key to `Null` on `state.context` **first**, before the two fallible `resolve_*` calls, so the always-overwritten invariant holds on every early-return path.\n - After the existing preamble build, when `gv_node.handler_type() == Some(\"parallel\")`: iterate `self.graph.outgoing_edges(node.id())` in order; per edge resolve explicit fidelity (edge attr → target-node attr → none); apply `degraded()` to explicit values (log when it was `full`); push `Null` for inherit/equal-to-fork, else render `build_preamble(final_fidelity, …)` reusing the already-resolved snapshot (blobs resolved once at `:113-128`) and push the entry. Set the array on the stash key.\n - Extract the per-branch resolution as a pure helper beside `resolve_fidelity` (`:206`, same module — no visibility change) for unit testing.\n\n4. **`lib/crates/fabro-workflow/src/handler/parallel.rs`** — in `execute()`'s branch-setup loop (insert after `:238`, where `INTERNAL_PARALLEL_BRANCH_ID` is set):\n - Read the stash from the parent context once before the loop; `None`, `Some(Null)`, or length-mismatch all mean strict legacy behavior (note: `Context::get` returns `Some(Null)` for a Nulled key — both must be treated as absent).\n - Per branch with an entry: `branch_context.set(CURRENT_PREAMBLE, preamble)` and `branch_context.set(INTERNAL_FIDELITY, fidelity)`. Downstream needs nothing: `agent.rs:244`/`prompt.rs:63` read `context.preamble()`.\n - In **every** branch fork, set the stash key to `Null` — load-bearing, not hygiene: a nested parallel branch target reads its fork's stash, and without the Null it would misinterpret the outer node's array as its own.\n - After the loop, set the stash key to `Null` on the handler's own context — the write-back diff (`node_handler.rs:99-105`) clears `state.context` so the post-parallel checkpoint carries Null even before the artifact strip.\n\n5. **`lib/crates/fabro-validate/src/rules/parallel_branch_inert_attribute.rs`** — drop `\"fidelity\"` from `BRANCH_IGNORED_ATTRS` and its `fix_message` arm; add a narrow diagnostic in its place: `fidelity=\"full\"` on a fork→branch edge or branch-only node warns \"parallel branches run at most at summary:high; full is degraded at runtime because branches cannot share a session\". Other fidelity values now lint clean. Update doc comment (the snapshot rationale now applies to `thread_id` only) and tests.\n\n6. **`lib/crates/fabro-validate/src/rules/thread_id_requires_fidelity_full.rs`** — skip fork→branch edges and branch-only nodes (factor the branch-only detection from rule 5 into a shared helper). Today it tells branch nodes with `thread_id` to *add* `fidelity=\"full\"` — advice that, post-change, would actively alter runtime behavior while the other rule says \"remove thread_id\". Defer to the inert-attribute rule's guidance on branches.\n\n7. **Docs** — `docs/public/execution/context.mdx` (fidelity precedence: branch edge → branch node → inherit fork; per-branch preamble rendering; `thread_id` inert in branches; `full` degradation), `docs/public/workflows/stages-and-nodes.mdx` (parallel fan-out section + fidelity attribute notes), `docs/public/reference/dot-language.mdx` (edge/node attr rows). Optional changelog entry via the changelog conventions.\n\n## Tests\n\nPer `docs/internal/testing-strategy.md`, preamble content is implementation-facing → `fabro-workflow`, not CLI layers.\n\n- **Pure unit tests** (`lifecycle/fidelity.rs` tests, beside `resolve_fidelity`'s at `:271-321`): explicit edge > node precedence; no-attr → inherit (no entry); explicit `full` → `summary:high` entry; branch fidelity equal to fork's (post-degradation) → no entry; fork resolved `Full` + no branch attrs → no entries at all.\n- **Lifecycle-level**: two consecutive `before_node` calls on the same parallel node rebuild (not merge) the stash; non-parallel node overwrites stash to Null; resume-degrade flag interaction (fork degrades, fallback branches still get no entry).\n- **`artifact.rs` tests** (`:519+` pattern): both snapshot functions strip the stash key.\n- **Parallel handler unit tests** (`handler/parallel.rs` tests module, `EngineServices::test_default()` + recording handler mirroring `PreambleEchoHandler`, `manager_loop.rs:973-1043`): entry applies `CURRENT_PREAMBLE`/`INTERNAL_FIDELITY` to the right branch by index; stash Null in every branch fork; `Some(Null)`/absent/length-mismatch → legacy; duplicate-target edges get distinct entries at indices 0/1 (no-git test — a pre-existing worktree-name collision exists for that topology, don't let it pollute the assertion); existing tests stay unmodified as the legacy guard.\n- **Engine-level regression** in `lib/crates/fabro-workflow/tests/it/integration.rs` beside the `fidelity_prompt_*` tests (`:9245-9479`), reusing `FidelityCapturingHandler` (`:4917-4974`) and the `end_to_end_parallel_fan_out_fan_in` scaffold (`:2441-2480`) via `WorkflowRunner::run_with_state`:\n - **Probe A analog** (`parallel_branches_get_per_branch_preambles_by_fidelity`): seed sets a context marker → fork → `branch_a` (`fidelity=\"truncate\"`) + `branch_b` (default) → fan-in. Assert branch_a's preamble is goal-only (no marker) while branch_b's contains the marker.\n - **Probe B analog**: `fidelity=\"truncate\"` on the fork only → both branches goal-only (compat guarantee, unchanged behavior).\n - Edge-attr-beats-node-attr variant.\n- **Lint tests**: no warning for non-full branch fidelity; warning for branch `fidelity=\"full\"`; `thread_id_requires_fidelity_full` silent on branch-only nodes, still firing elsewhere.\n\n## Verification\n\n- `cargo nextest run -p fabro-workflow -p fabro-validate`, then `ulimit -n 4096 && cargo nextest run --workspace` (do not export `FORCE_COLOR`).\n- `cargo +nightly-2026-04-14 fmt --check --all`; `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`.\n- Live confirmation on the testing server: re-run the two probe workflows (session scratchpad `probes/isolation-a`, `probes/isolation-b`) against a locally built binary — probe A's `stage.prompt` events must now show differentiated branch preambles; probe B byte-identical to before.\n\n## Compatibility\n\n| Situation | Impact |\n|---|---|\n| No fidelity attrs near the parallel node | None — byte-identical (inherit path, no re-render) |\n| Fidelity on the fork node / its incoming edge | None — fork snapshot semantics unchanged |\n| Previously-dead attrs on branch nodes / fork→branch edges | Start working (the fix) |\n| `full` on a branch | Degrades to `summary:high` + log + lint warning |\n| `thread_id` on a branch | Still inert; lint still warns; the contradictory companion lint goes quiet on branches |\n\n## Decisions (user-confirmed 2026-07-23)\n\n1. **`CURRENT_NODE` in branch contexts stays inherited** — the branch-scoped bookkeeping change is deferred to a dedicated event-attribution change.\n2. **The narrow `fidelity=\"full\"` branch lint is in scope** (step 5 stands as written).\n3. **No changelog entry in this PR** — changelog handled in the usual batch.\n",
"internal.thread_id": "simplify_fable",
"last_response": "## Review complete\n\nThe three review agents completed, but their detailed reports were lost by the environment. As approved, I completed the reuse, quality, and efficiency review independently.\n\n### C",
"internal.retry_count.preflight_compile": 0,
"outcome": "succeeded",
"thread.preflight_compile.current_node": "preflight_lint",
"internal.retry_count.implement": 0,
"failure_signature": "",
"last_stage": "simplify_sol",
"thread.implement.current_node": "simplify_fable",
"internal.node_visit_count": 1,
"internal.retry_count.simplify_sol": 0,
"response.simplify_sol": "## Review complete\n\nThe three review agents completed, but their detailed reports were lost by the environment. As approved, I completed the reuse, quality, and efficiency review independently.\n\n### Cleanup applied\n\n- Narrowed implementation-only APIs to `pub(crate)`:\n - `ParallelBranchPreamble` and its fields\n - `TRANSIENT_CONTEXT_KEYS`\n - `artifact::strip_transient_keys`\n- Simplified comments and corrected the transient-key helper documentation.\n- Improved branch-preamble caching:\n - Preallocates the output array.\n - Renders each distinct fidelity only once.\n - Reuses an existing output entry rather than retaining a second potentially large JSON copy.\n- Preserved all required branch-fidelity, stash, nesting, `CURRENT_NODE`, `thread_id`, and simulation semantics.\n- No changelog entry added.\n\n### Verification\n\n- Focused fidelity tests: **23 passed**\n- Focused parallel-handler tests: **12 passed**\n- Targeted workflow/validation suite: **1,405 passed**\n- Workspace suite: **7,064 passed, 193 skipped**\n- Workspace Clippy with `-D warnings`: **passed**\n- `cargo fmt --check --all`: **passed**\n- `git diff --check`: **passed**\n\nThe cleanup remains uncommitted in three `fabro-workflow` files.",
"thread.start.current_node": "toolchain",
"thread.toolchain.current_node": "preflight_compile",
"internal.retry_count.toolchain": 0,
"internal.work_dir": "/home/daytona/workspace/fabro",
"failure_class": ""
},
"node_outcomes": {
"implement": {
"status": "succeeded",
"context_updates": {
"last_stage": "implement",
"last_response": "Implemented the complete per-branch fidelity plan.\n\n### Highlights\n- Added ordered per-branch preamble stash generation and consumption.\n- Implemented edge → branch node → fork inheritance precede",
"response.implement": "Implemented the complete per-branch fidelity plan.\n\n### Highlights\n- Added ordered per-branch preamble stash generation and consumption.\n- Implemented edge → branch node → fork inheritance precedence.\n- Explicit branch `full` degrades to `summary:high` with structured logging.\n- Preserved byte-identical fork inheritance for unspecified/equal fidelities.\n- Added strict legacy fallback for missing, null, malformed, non-array, and mismatched stashes.\n- Cleared branch stashes for nested-parallel safety.\n- Removed stash data from checkpoints, durable snapshots, and persisted event contexts.\n- Kept branch `thread_id` inert, `CURRENT_NODE` inherited, and `simulate()` unchanged.\n- Updated validation rules and all three requested documentation pages.\n- Added unit, lifecycle, handler, integration, persistence, and lint regression coverage.\n- No changelog added.\n\n### Verification\n- `cargo nextest run --workspace`: **7,063 passed, 193 skipped**\n- `cargo +nightly-2026-04-14 fmt --check --all`: passed\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`: passed\n- `git diff --check`: passed\n\nLive testing-server probes were not run because the probe definitions and testing-server access were not available in this checkout/session."
},
"notes": "Stage completed: implement",
"usage": {
"input": {
"usage": {
"model": {
"provider": "openrouter",
"model_id": "openai/gpt-5.6-sol"
},
"tokens": {
"input_tokens": 394190,
"output_tokens": 32119,
"reasoning_tokens": 32341,
"cache_read_tokens": 13269488,
"cache_write_tokens": 886818
}
},
"facts": {
"algorithm": "openai"
}
}
},
"files_touched": [
"/home/daytona/workspace/fabro/docs/public/execution/context.mdx",
"/home/daytona/workspace/fabro/docs/public/reference/dot-language.mdx",
"/home/daytona/workspace/fabro/docs/public/workflows/stages-and-nodes.mdx",
"/home/daytona/workspace/fabro/lib/crates/fabro-validate/src/rules/mod.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-validate/src/rules/parallel_branch.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-validate/src/rules/parallel_branch_inert_attribute.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-validate/src/rules/thread_id_requires_fidelity_full.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/artifact.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/context.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/handler/parallel.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/lifecycle/event.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/lifecycle/fidelity.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/tests/it/integration.rs"
],
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 1625877,
"tool_time_ms": 1190483,
"active_time_ms": 2816360
}
},
"preflight_compile": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
},
"notes": "Script completed: cargo check -q --workspace 2>&1",
"usage": null,
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 0,
"tool_time_ms": 140243,
"active_time_ms": 140243
}
},
"simplify_fable": {
"status": "succeeded",
"context_updates": {
"last_response": "Everything is green. Summary of the simplify stage:\n\n## Review findings applied\n\n**Wire-format root cause (reuse #1, quality #1/#2, efficiency #2/#3):**\n- Added `Serialize`/`Deserialize` to `fabro_gra",
"response.simplify_fable": "Everything is green. Summary of the simplify stage:\n\n## Review findings applied\n\n**Wire-format root cause (reuse #1, quality #1/#2, efficiency #2/#3):**\n- Added `Serialize`/`Deserialize` to `fabro_graphviz::Fidelity` (serde renames aligned with strum per CLAUDE.md, plus `Hash` and a serde↔strum parity test; `serde_json` added as dev-dependency).\n- Replaced the hand-rolled JSON producer/parser pair with one shared `ParallelBranchPreamble` struct in `context.rs` (`deny_unknown_fields` replaces the brittle `entry.len() == 2` check). Producer uses `serde_json::to_value`, consumer uses `from_value` — a field rename can no longer silently split the contract. Documented the index-correlation invariant and the `Option<Option<…>>` semantics (quality #5/#8).\n\n**Duplication (reuse #2/#3, quality #6/#7):**\n- Extracted `explicit_fidelity()` — the edge→node precedence core is now shared between `resolve_fidelity` and `resolve_parallel_branch_fidelity`.\n- Added `keys::TRANSIENT_CONTEXT_KEYS` + `artifact::strip_transient_keys()`; both artifact strip sites use it. `stage_context_values` keeps its intentionally different behavior (retains `CURRENT_PREAMBLE`) with a WHY comment.\n- Unified the two copy-pasted full-fidelity diagnostic blocks into `full_fidelity_diagnostic()`; `full_fidelity_message()` → `const`; dead `fix_message` match → `const THREAD_ID_FIX`; capitalized the message for consistency; reworded the \"now honors\" changelog-style doc comment.\n\n**Structure & style (quality #3/#4/#9/#10/#14):**\n- Extracted the 38-line parallel stash block into `build_parallel_branch_preambles()`, restoring `before_node`'s numbered-step narrative (steps renumbered 5–9).\n- `ParallelBranchFidelityResolution`: dropped accessor ceremony and unused derives; plain documented fields.\n- Fixed the warn! to log `effective_fidelity` via `degraded()` instead of dressed-up constants; removed the redundant `fidelity` field.\n- `parallel.rs` uses `keys::`-scoped types; test helper `fidelity_attr` → `str_attr`.\n\n**Efficiency (#1, #4):**\n- Moved the parent stash clear to *before* the fork loop — branch forks no longer copy the full preamble array B times just to null it, and the per-branch null-set became unnecessary (forks inherit Null; nested-parallel safety preserved, verified by the existing `stash == Null` test assertions).\n- Memoized per-fidelity preamble renders — duplicate branch fidelities render once.\n\n**Validate helper (reuse #4, quality #13, efficiency #6):**\n- `ParallelBranches` now uses `Graph::incoming_edges`; `is_branch_only_node` is allocation-free; `branch_only_parents` replaced the dense peekable/`then`/`collect::<Option<BTreeSet>>` chain with straightforward code. Added the cross-rule suppression WHY comment in `thread_id_requires_fidelity_full`.\n\n**Skipped (noted, not addressed):** test-fixture consolidation into `rules/test_support` and a shared in-memory-store test helper (pre-existing pattern across ~15 sites, out of scope); `\"parallel\"` string literal (codebase-wide convention); `make_run_options` parameterization (follows existing file precedent).\n\n## Verification\n- `cargo nextest run -p fabro-graphviz -p fabro-validate -p fabro-workflow` — all pass (302 + 1218)\n- `cargo nextest run --workspace` — **7064 passed, 0 failed**\n- `cargo +nightly-2026-04-14 fmt --check --all` — clean\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` — clean\n\nNo blockers.",
"last_stage": "simplify_fable"
},
"notes": "Stage completed: simplify_fable",
"usage": {
"input": {
"usage": {
"model": {
"provider": "openrouter",
"model_id": "anthropic/claude-fable-5"
},
"tokens": {
"input_tokens": 8653765,
"output_tokens": 127619,
"reasoning_tokens": 33321,
"cache_read_tokens": 0,
"cache_write_tokens": 0
}
},
"facts": {
"algorithm": "openai"
}
}
},
"files_touched": [
"/home/daytona/workspace/fabro/lib/crates/fabro-graphviz/Cargo.toml",
"/home/daytona/workspace/fabro/lib/crates/fabro-graphviz/src/fidelity.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-validate/src/rules/parallel_branch.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-validate/src/rules/parallel_branch_inert_attribute.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-validate/src/rules/thread_id_requires_fidelity_full.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/artifact.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/context.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/handler/parallel.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/lifecycle/event.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/lifecycle/fidelity.rs"
],
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 2153413,
"tool_time_ms": 3192086,
"active_time_ms": 5345499
}
},
"preflight_lint": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
},
"notes": "Script completed: cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1",
"usage": null,
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 0,
"tool_time_ms": 154518,
"active_time_ms": 154518
}
},
"start": {
"status": "succeeded",
"usage": null
},
"simplify_sol": {
"status": "succeeded",
"context_updates": {
"last_stage": "simplify_sol",
"response.simplify_sol": "## Review complete\n\nThe three review agents completed, but their detailed reports were lost by the environment. As approved, I completed the reuse, quality, and efficiency review independently.\n\n### Cleanup applied\n\n- Narrowed implementation-only APIs to `pub(crate)`:\n - `ParallelBranchPreamble` and its fields\n - `TRANSIENT_CONTEXT_KEYS`\n - `artifact::strip_transient_keys`\n- Simplified comments and corrected the transient-key helper documentation.\n- Improved branch-preamble caching:\n - Preallocates the output array.\n - Renders each distinct fidelity only once.\n - Reuses an existing output entry rather than retaining a second potentially large JSON copy.\n- Preserved all required branch-fidelity, stash, nesting, `CURRENT_NODE`, `thread_id`, and simulation semantics.\n- No changelog entry added.\n\n### Verification\n\n- Focused fidelity tests: **23 passed**\n- Focused parallel-handler tests: **12 passed**\n- Targeted workflow/validation suite: **1,405 passed**\n- Workspace suite: **7,064 passed, 193 skipped**\n- Workspace Clippy with `-D warnings`: **passed**\n- `cargo fmt --check --all`: **passed**\n- `git diff --check`: **passed**\n\nThe cleanup remains uncommitted in three `fabro-workflow` files.",
"last_response": "## Review complete\n\nThe three review agents completed, but their detailed reports were lost by the environment. As approved, I completed the reuse, quality, and efficiency review independently.\n\n### C"
},
"notes": "Stage completed: simplify_sol",
"usage": {
"input": {
"usage": {
"model": {
"provider": "openrouter",
"model_id": "openai/gpt-5.6-sol"
},
"tokens": {
"input_tokens": 7067,
"output_tokens": 22374,
"reasoning_tokens": 115580,
"cache_read_tokens": 17705408,
"cache_write_tokens": 400919
}
},
"facts": {
"algorithm": "openai"
}
}
},
"files_touched": [
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/artifact.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/context.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/lifecycle/fidelity.rs"
],
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 3189823,
"tool_time_ms": 1428855,
"active_time_ms": 4618678
}
},
"toolchain": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c"
},
"notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
"usage": null,
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 0,
"tool_time_ms": 1360,
"active_time_ms": 1360
}
}
},
"next_node_id": "verify",
"git_commit_sha": "9b85653a2dc62007a5ec2ce8cc8b8f1f4c817ef0",
"node_visits": {
"start": 1,
"preflight_lint": 1,
"toolchain": 1,
"simplify_sol": 1,
"implement": 1,
"simplify_fable": 1,
"preflight_compile": 1
}
},
"diff": {
"patch": "diff --git a/lib/crates/fabro-workflow/src/artifact.rs b/lib/crates/fabro-workflow/src/artifact.rs\nindex bbf7f7ed8..8ea8e38f0 100644\n--- a/lib/crates/fabro-workflow/src/artifact.rs\n+++ b/lib/crates/fabro-workflow/src/artifact.rs\n@@ -83,8 +83,8 @@ pub fn durable_context_snapshot(context: &Context) -> HashMap<String, Value> {\n snapshot\n }\n \n-/// Remove runtime-only keys that must never reach durable storage or events.\n-pub fn strip_transient_keys(values: &mut HashMap<String, Value>) {\n+/// Remove runtime-only keys that must never reach durable storage.\n+pub(crate) fn strip_transient_keys(values: &mut HashMap<String, Value>) {\n for key in context::keys::TRANSIENT_CONTEXT_KEYS {\n values.remove(*key);\n }\ndiff --git a/lib/crates/fabro-workflow/src/context.rs b/lib/crates/fabro-workflow/src/context.rs\nindex a890c0e8f..b02a8dc51 100644\n--- a/lib/crates/fabro-workflow/src/context.rs\n+++ b/lib/crates/fabro-workflow/src/context.rs\n@@ -48,10 +48,8 @@ pub mod keys {\n pub const PARALLEL_FAN_IN_BEST_OUTCOME: &str = \"parallel.fan_in.best_outcome\";\n pub const PARALLEL_FAN_IN_BEST_HEAD_SHA: &str = \"parallel.fan_in.best_head_sha\";\n \n- /// Runtime-only keys stripped from durable context projections\n- /// (checkpoint snapshots and resume normalization). Add new transient\n- /// keys here so both strip sites stay in sync.\n- pub const TRANSIENT_CONTEXT_KEYS: &[&str] =\n+ /// Runtime-only keys stripped from durable context projections.\n+ pub(crate) const TRANSIENT_CONTEXT_KEYS: &[&str] =\n &[CURRENT_PREAMBLE, INTERNAL_PARALLEL_BRANCH_PREAMBLES];\n \n // --- Prefix constants (for filtering and dynamic keys) ---\n@@ -152,15 +150,12 @@ use crate::event::StageScope;\n /// One entry of the [`keys::INTERNAL_PARALLEL_BRANCH_PREAMBLES`] stash.\n ///\n /// The stash is a JSON array indexed by the parallel node's outgoing-edge\n-/// order (`Graph::outgoing_edges` preserves declaration order, so producer and\n-/// consumer align even with duplicate targets). `null` entries mean the branch\n-/// inherits the fork's preamble. `FidelityLifecycle::before_node` produces the\n-/// stash; `ParallelHandler::execute` consumes and clears it.\n+/// order. `null` entries mean the branch inherits the fork's preamble.\n #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n #[serde(deny_unknown_fields)]\n-pub struct ParallelBranchPreamble {\n- pub fidelity: Fidelity,\n- pub preamble: String,\n+pub(crate) struct ParallelBranchPreamble {\n+ pub(crate) fidelity: Fidelity,\n+ pub(crate) preamble: String,\n }\n \n /// Domain-specific typed accessors for workflow context values.\ndiff --git a/lib/crates/fabro-workflow/src/lifecycle/fidelity.rs b/lib/crates/fabro-workflow/src/lifecycle/fidelity.rs\nindex 437550cc1..37d2c7e33 100644\n--- a/lib/crates/fabro-workflow/src/lifecycle/fidelity.rs\n+++ b/lib/crates/fabro-workflow/src/lifecycle/fidelity.rs\n@@ -74,47 +74,52 @@ impl FidelityLifecycle {\n resolved_outcomes: &HashMap<String, Outcome>,\n completed_nodes: &[String],\n ) -> Vec<serde_json::Value> {\n- let mut rendered: HashMap<keys::Fidelity, serde_json::Value> = HashMap::new();\n- self.graph\n- .outgoing_edges(node_id)\n- .into_iter()\n- .enumerate()\n- .map(|(branch_index, edge)| {\n- let Some(target_node) = self.graph.nodes.get(&edge.to) else {\n- return serde_json::Value::Null;\n- };\n- let resolution = resolve_parallel_branch_fidelity(edge, target_node, fork_fidelity);\n- if resolution.requested == Some(keys::Fidelity::Full) {\n- tracing::warn!(\n- parallel_node = %node_id,\n- branch = %edge.to,\n- branch_index,\n- effective_fidelity = %keys::Fidelity::Full.degraded(),\n- \"Parallel branch fidelity degraded from full\"\n- );\n- }\n- let Some(branch_fidelity) = resolution.effective else {\n- return serde_json::Value::Null;\n- };\n- rendered\n- .entry(branch_fidelity)\n- .or_insert_with(|| {\n- let entry = ParallelBranchPreamble {\n- fidelity: branch_fidelity,\n- preamble: build_preamble(\n- branch_fidelity,\n- resolved_context,\n- &self.graph,\n- completed_nodes,\n- resolved_outcomes,\n- ),\n- };\n- serde_json::to_value(entry)\n- .expect(\"ParallelBranchPreamble serialization cannot fail\")\n- })\n- .clone()\n- })\n- .collect()\n+ let edges = self.graph.outgoing_edges(node_id);\n+ let mut preambles: Vec<serde_json::Value> = Vec::with_capacity(edges.len());\n+ let mut rendered: HashMap<keys::Fidelity, usize> = HashMap::new();\n+\n+ for (branch_index, edge) in edges.into_iter().enumerate() {\n+ let Some(target_node) = self.graph.nodes.get(&edge.to) else {\n+ preambles.push(serde_json::Value::Null);\n+ continue;\n+ };\n+ let resolution = resolve_parallel_branch_fidelity(edge, target_node, fork_fidelity);\n+ if resolution.requested == Some(keys::Fidelity::Full) {\n+ tracing::warn!(\n+ parallel_node = %node_id,\n+ branch = %edge.to,\n+ branch_index,\n+ effective_fidelity = %keys::Fidelity::Full.degraded(),\n+ \"Parallel branch fidelity degraded from full\"\n+ );\n+ }\n+ let Some(branch_fidelity) = resolution.effective else {\n+ preambles.push(serde_json::Value::Null);\n+ continue;\n+ };\n+ if let Some(&rendered_index) = rendered.get(&branch_fidelity) {\n+ preambles.push(preambles[rendered_index].clone());\n+ continue;\n+ }\n+\n+ let entry = ParallelBranchPreamble {\n+ fidelity: branch_fidelity,\n+ preamble: build_preamble(\n+ branch_fidelity,\n+ resolved_context,\n+ &self.graph,\n+ completed_nodes,\n+ resolved_outcomes,\n+ ),\n+ };\n+ rendered.insert(branch_fidelity, preambles.len());\n+ preambles.push(\n+ serde_json::to_value(entry)\n+ .expect(\"ParallelBranchPreamble serialization cannot fail\"),\n+ );\n+ }\n+\n+ preambles\n }\n }\n \n",
"summary": {
"files_changed": 16,
"additions": 1209,
"deletions": 101
}
}
},
{
"seq": 0,
"checkpoint": {
"timestamp": "2026-07-23T20:33:25.184885675Z",
"current_node": "verify",
"completed_nodes": [
"start",
"toolchain",
"preflight_compile",
"preflight_lint",
"implement",
"simplify_fable",
"simplify_sol",
"verify"
],
"node_retries": {},
"context_values": {
"internal.fidelity": "compact",
"internal.thread_id": "simplify_sol",
"last_stage": "simplify_sol",
"graph.goal": "# Per-Branch Fidelity for Parallel Branches — Implementation Plan\n\n## Context\n\nParallel branch nodes are dispatched via `dispatch_handler`, bypassing `FidelityLifecycle::before_node` (`lib/crates/fabro-workflow/src/lifecycle/fidelity.rs:76-180`) — the only place fidelity is resolved and preambles are built. Every branch therefore inherits the stale `current.preamble` copied at `context.fork()` (`handler/parallel.rs:226`), and `fidelity`/`thread_id` on branch nodes or `fork -> branch` edges are silently ignored. Confirmed live on the testing server (probes `01KY7KRA7E…`/`01KY7KRAAW…`, 2026-07-23): a `truncate` branch received the identical compact preamble as its default sibling; fork-level fidelity works and is the current workaround. Design reviewed via the Quarry doc \"Fix: Per-Branch Fidelity for Parallel Branches\".\n\nWhy not run the lifecycle per branch: it is a single-token state machine (one-slot `incoming_edge_data` baton, singleton context keys written to shared run state); concurrent invocation would corrupt run state. And `build_preamble` (`handler/llm/preamble.rs:24`, public and pure) needs `state.completed_nodes`/`state.node_outcomes`, which only the lifecycle sees. So: **pre-render per-branch preambles in the lifecycle, hand off to the handler via one context key.**\n\n## Semantics (final, after design pressure-test)\n\n- **Explicit-only resolution.** A branch's fidelity comes from the `fork -> branch` edge attr, else the branch node attr, else **no entry** — the branch inherits the fork's preamble via `fork()` exactly as today. The fork's own resolved fidelity is never re-applied per branch; this keeps the default path byte-identical even when the fork resolves `Full` (where re-derivation would have wrongly degraded every branch).\n- **`full` degrades to `summary:high`** (`Fidelity::degraded()`, `fabro-graphviz/src/fidelity.rs:35-40`) — applied only to *explicitly set* branch fidelity, with a log line (per `docs/internal/logging-strategy.md` — read before writing it).\n- **Equality skip**: if the branch's post-degradation fidelity equals the fork's post-degradation fidelity, store no entry (avoid redundant renders).\n- **`thread_id` stays inert in branches** (concurrent branches must never share an LLM session).\n- **`CURRENT_NODE` in branch contexts stays inherited (fork id).** The pressure-test showed changing it would re-attribute every branch-internal event's stage scope (`context.rs:185-190` → `StageScope::for_handler` used by all handlers) with a visit mismatch against `for_parallel_branch` scoping. The Quarry doc's \"bookkeeping keys describe the branch\" line is consciously deferred to a separate change with proper visit accounting.\n- **`simulate()` untouched.** No simulated handler reads preambles; partial mirroring would risk nested-parallel stash misreads. All-or-nothing → nothing.\n- **Stash shape**: `Value::Array`, length = branch count, `Null` = inherit, else `{\"fidelity\": \"...\", \"preamble\": \"...\"}`. Array length ≠ edge count → treat as absent (legacy). Keyed by edge index; `graph.outgoing_edges` is an ordered Vec filter (`fabro-types/src/graph.rs:393-395`) and lifecycle + handler share the same `Arc<GvGraph>`, so indices align deterministically (including two edges to the same target).\n\n## Implementation steps (ordered; tree compiles at each step)\n\n1. **`lib/crates/fabro-workflow/src/context.rs`** — add `pub const INTERNAL_PARALLEL_BRANCH_PREAMBLES: &str = \"internal.parallel_branch_preambles\";` to `keys`. The `internal.` prefix already excludes it from preamble rendering (`preamble.rs:99-109`) and child→parent propagation (`context.rs:80-85`).\n\n2. **`lib/crates/fabro-workflow/src/artifact.rs`** — strip the new key in `durable_context_snapshot` (`:81`) and `normalize_checkpoint_for_resume` (`:101`), beside `CURRENT_PREAMBLE`. Without this, every post-parallel checkpoint and `CheckpointCompleted` event payload carries the full per-branch preamble map (a `summary:high` preamble embeds up to 50 lines of every command output — multiplied per branch).\n\n3. **`lib/crates/fabro-workflow/src/lifecycle/fidelity.rs`** — in `before_node`:\n - Set the stash key to `Null` on `state.context` **first**, before the two fallible `resolve_*` calls, so the always-overwritten invariant holds on every early-return path.\n - After the existing preamble build, when `gv_node.handler_type() == Some(\"parallel\")`: iterate `self.graph.outgoing_edges(node.id())` in order; per edge resolve explicit fidelity (edge attr → target-node attr → none); apply `degraded()` to explicit values (log when it was `full`); push `Null` for inherit/equal-to-fork, else render `build_preamble(final_fidelity, …)` reusing the already-resolved snapshot (blobs resolved once at `:113-128`) and push the entry. Set the array on the stash key.\n - Extract the per-branch resolution as a pure helper beside `resolve_fidelity` (`:206`, same module — no visibility change) for unit testing.\n\n4. **`lib/crates/fabro-workflow/src/handler/parallel.rs`** — in `execute()`'s branch-setup loop (insert after `:238`, where `INTERNAL_PARALLEL_BRANCH_ID` is set):\n - Read the stash from the parent context once before the loop; `None`, `Some(Null)`, or length-mismatch all mean strict legacy behavior (note: `Context::get` returns `Some(Null)` for a Nulled key — both must be treated as absent).\n - Per branch with an entry: `branch_context.set(CURRENT_PREAMBLE, preamble)` and `branch_context.set(INTERNAL_FIDELITY, fidelity)`. Downstream needs nothing: `agent.rs:244`/`prompt.rs:63` read `context.preamble()`.\n - In **every** branch fork, set the stash key to `Null` — load-bearing, not hygiene: a nested parallel branch target reads its fork's stash, and without the Null it would misinterpret the outer node's array as its own.\n - After the loop, set the stash key to `Null` on the handler's own context — the write-back diff (`node_handler.rs:99-105`) clears `state.context` so the post-parallel checkpoint carries Null even before the artifact strip.\n\n5. **`lib/crates/fabro-validate/src/rules/parallel_branch_inert_attribute.rs`** — drop `\"fidelity\"` from `BRANCH_IGNORED_ATTRS` and its `fix_message` arm; add a narrow diagnostic in its place: `fidelity=\"full\"` on a fork→branch edge or branch-only node warns \"parallel branches run at most at summary:high; full is degraded at runtime because branches cannot share a session\". Other fidelity values now lint clean. Update doc comment (the snapshot rationale now applies to `thread_id` only) and tests.\n\n6. **`lib/crates/fabro-validate/src/rules/thread_id_requires_fidelity_full.rs`** — skip fork→branch edges and branch-only nodes (factor the branch-only detection from rule 5 into a shared helper). Today it tells branch nodes with `thread_id` to *add* `fidelity=\"full\"` — advice that, post-change, would actively alter runtime behavior while the other rule says \"remove thread_id\". Defer to the inert-attribute rule's guidance on branches.\n\n7. **Docs** — `docs/public/execution/context.mdx` (fidelity precedence: branch edge → branch node → inherit fork; per-branch preamble rendering; `thread_id` inert in branches; `full` degradation), `docs/public/workflows/stages-and-nodes.mdx` (parallel fan-out section + fidelity attribute notes), `docs/public/reference/dot-language.mdx` (edge/node attr rows). Optional changelog entry via the changelog conventions.\n\n## Tests\n\nPer `docs/internal/testing-strategy.md`, preamble content is implementation-facing → `fabro-workflow`, not CLI layers.\n\n- **Pure unit tests** (`lifecycle/fidelity.rs` tests, beside `resolve_fidelity`'s at `:271-321`): explicit edge > node precedence; no-attr → inherit (no entry); explicit `full` → `summary:high` entry; branch fidelity equal to fork's (post-degradation) → no entry; fork resolved `Full` + no branch attrs → no entries at all.\n- **Lifecycle-level**: two consecutive `before_node` calls on the same parallel node rebuild (not merge) the stash; non-parallel node overwrites stash to Null; resume-degrade flag interaction (fork degrades, fallback branches still get no entry).\n- **`artifact.rs` tests** (`:519+` pattern): both snapshot functions strip the stash key.\n- **Parallel handler unit tests** (`handler/parallel.rs` tests module, `EngineServices::test_default()` + recording handler mirroring `PreambleEchoHandler`, `manager_loop.rs:973-1043`): entry applies `CURRENT_PREAMBLE`/`INTERNAL_FIDELITY` to the right branch by index; stash Null in every branch fork; `Some(Null)`/absent/length-mismatch → legacy; duplicate-target edges get distinct entries at indices 0/1 (no-git test — a pre-existing worktree-name collision exists for that topology, don't let it pollute the assertion); existing tests stay unmodified as the legacy guard.\n- **Engine-level regression** in `lib/crates/fabro-workflow/tests/it/integration.rs` beside the `fidelity_prompt_*` tests (`:9245-9479`), reusing `FidelityCapturingHandler` (`:4917-4974`) and the `end_to_end_parallel_fan_out_fan_in` scaffold (`:2441-2480`) via `WorkflowRunner::run_with_state`:\n - **Probe A analog** (`parallel_branches_get_per_branch_preambles_by_fidelity`): seed sets a context marker → fork → `branch_a` (`fidelity=\"truncate\"`) + `branch_b` (default) → fan-in. Assert branch_a's preamble is goal-only (no marker) while branch_b's contains the marker.\n - **Probe B analog**: `fidelity=\"truncate\"` on the fork only → both branches goal-only (compat guarantee, unchanged behavior).\n - Edge-attr-beats-node-attr variant.\n- **Lint tests**: no warning for non-full branch fidelity; warning for branch `fidelity=\"full\"`; `thread_id_requires_fidelity_full` silent on branch-only nodes, still firing elsewhere.\n\n## Verification\n\n- `cargo nextest run -p fabro-workflow -p fabro-validate`, then `ulimit -n 4096 && cargo nextest run --workspace` (do not export `FORCE_COLOR`).\n- `cargo +nightly-2026-04-14 fmt --check --all`; `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`.\n- Live confirmation on the testing server: re-run the two probe workflows (session scratchpad `probes/isolation-a`, `probes/isolation-b`) against a locally built binary — probe A's `stage.prompt` events must now show differentiated branch preambles; probe B byte-identical to before.\n\n## Compatibility\n\n| Situation | Impact |\n|---|---|\n| No fidelity attrs near the parallel node | None — byte-identical (inherit path, no re-render) |\n| Fidelity on the fork node / its incoming edge | None — fork snapshot semantics unchanged |\n| Previously-dead attrs on branch nodes / fork→branch edges | Start working (the fix) |\n| `full` on a branch | Degrades to `summary:high` + log + lint warning |\n| `thread_id` on a branch | Still inert; lint still warns; the contradictory companion lint goes quiet on branches |\n\n## Decisions (user-confirmed 2026-07-23)\n\n1. **`CURRENT_NODE` in branch contexts stays inherited** — the branch-scoped bookkeeping change is deferred to a dedicated event-attribution change.\n2. **The narrow `fidelity=\"full\"` branch lint is in scope** (step 5 stands as written).\n3. **No changelog entry in this PR** — changelog handled in the usual batch.\n",
"failure_class": "transient_infra",
"outcome": "failed",
"thread.start.current_node": "toolchain",
"thread.implement.current_node": "simplify_fable",
"graph.rankdir": "LR",
"internal.node_visit_count": 1,
"internal.work_dir": "/home/daytona/workspace/fabro",
"internal.retry_count.start": 0,
"internal.retry_count.preflight_compile": 0,
"response.implement": "Implemented the complete per-branch fidelity plan.\n\n### Highlights\n- Added ordered per-branch preamble stash generation and consumption.\n- Implemented edge → branch node → fork inheritance precedence.\n- Explicit branch `full` degrades to `summary:high` with structured logging.\n- Preserved byte-identical fork inheritance for unspecified/equal fidelities.\n- Added strict legacy fallback for missing, null, malformed, non-array, and mismatched stashes.\n- Cleared branch stashes for nested-parallel safety.\n- Removed stash data from checkpoints, durable snapshots, and persisted event contexts.\n- Kept branch `thread_id` inert, `CURRENT_NODE` inherited, and `simulate()` unchanged.\n- Updated validation rules and all three requested documentation pages.\n- Added unit, lifecycle, handler, integration, persistence, and lint regression coverage.\n- No changelog added.\n\n### Verification\n- `cargo nextest run --workspace`: **7,063 passed, 193 skipped**\n- `cargo +nightly-2026-04-14 fmt --check --all`: passed\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`: passed\n- `git diff --check`: passed\n\nLive testing-server probes were not run because the probe definitions and testing-server access were not available in this checkout/session.",
"internal.retry_count.implement": 0,
"last_response": "## Review complete\n\nThe three review agents completed, but their detailed reports were lost by the environment. As approved, I completed the reuse, quality, and efficiency review independently.\n\n### C",
"internal.retry_count.simplify_sol": 0,
"thread.toolchain.current_node": "preflight_compile",
"internal.retry_count.preflight_lint": 0,
"internal.retry_count.verify": 0,
"internal.retry_count.toolchain": 0,
"internal.run_id": "01KY7Y01REECZ24XXTMBZ3PPV9",
"thread.preflight_compile.current_node": "preflight_lint",
"response.simplify_fable": "Everything is green. Summary of the simplify stage:\n\n## Review findings applied\n\n**Wire-format root cause (reuse #1, quality #1/#2, efficiency #2/#3):**\n- Added `Serialize`/`Deserialize` to `fabro_graphviz::Fidelity` (serde renames aligned with strum per CLAUDE.md, plus `Hash` and a serde↔strum parity test; `serde_json` added as dev-dependency).\n- Replaced the hand-rolled JSON producer/parser pair with one shared `ParallelBranchPreamble` struct in `context.rs` (`deny_unknown_fields` replaces the brittle `entry.len() == 2` check). Producer uses `serde_json::to_value`, consumer uses `from_value` — a field rename can no longer silently split the contract. Documented the index-correlation invariant and the `Option<Option<…>>` semantics (quality #5/#8).\n\n**Duplication (reuse #2/#3, quality #6/#7):**\n- Extracted `explicit_fidelity()` — the edge→node precedence core is now shared between `resolve_fidelity` and `resolve_parallel_branch_fidelity`.\n- Added `keys::TRANSIENT_CONTEXT_KEYS` + `artifact::strip_transient_keys()`; both artifact strip sites use it. `stage_context_values` keeps its intentionally different behavior (retains `CURRENT_PREAMBLE`) with a WHY comment.\n- Unified the two copy-pasted full-fidelity diagnostic blocks into `full_fidelity_diagnostic()`; `full_fidelity_message()` → `const`; dead `fix_message` match → `const THREAD_ID_FIX`; capitalized the message for consistency; reworded the \"now honors\" changelog-style doc comment.\n\n**Structure & style (quality #3/#4/#9/#10/#14):**\n- Extracted the 38-line parallel stash block into `build_parallel_branch_preambles()`, restoring `before_node`'s numbered-step narrative (steps renumbered 5–9).\n- `ParallelBranchFidelityResolution`: dropped accessor ceremony and unused derives; plain documented fields.\n- Fixed the warn! to log `effective_fidelity` via `degraded()` instead of dressed-up constants; removed the redundant `fidelity` field.\n- `parallel.rs` uses `keys::`-scoped types; test helper `fidelity_attr` → `str_attr`.\n\n**Efficiency (#1, #4):**\n- Moved the parent stash clear to *before* the fork loop — branch forks no longer copy the full preamble array B times just to null it, and the per-branch null-set became unnecessary (forks inherit Null; nested-parallel safety preserved, verified by the existing `stash == Null` test assertions).\n- Memoized per-fidelity preamble renders — duplicate branch fidelities render once.\n\n**Validate helper (reuse #4, quality #13, efficiency #6):**\n- `ParallelBranches` now uses `Graph::incoming_edges`; `is_branch_only_node` is allocation-free; `branch_only_parents` replaced the dense peekable/`then`/`collect::<Option<BTreeSet>>` chain with straightforward code. Added the cross-rule suppression WHY comment in `thread_id_requires_fidelity_full`.\n\n**Skipped (noted, not addressed):** test-fixture consolidation into `rules/test_support` and a shared in-memory-store test helper (pre-existing pattern across ~15 sites, out of scope); `\"parallel\"` string literal (codebase-wide convention); `make_run_options` parameterization (follows existing file precedent).\n\n## Verification\n- `cargo nextest run -p fabro-graphviz -p fabro-validate -p fabro-workflow` — all pass (302 + 1218)\n- `cargo nextest run --workspace` — **7064 passed, 0 failed**\n- `cargo +nightly-2026-04-14 fmt --check --all` — clean\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` — clean\n\nNo blockers.",
"internal.retry_count.simplify_fable": 0,
"thread.preflight_lint.current_node": "implement",
"thread.simplify_sol.current_node": "verify",
"command.output": "blob://sha256/20aad968504680e75d4178af570cdb1342400f3c5ce35c85432b519d463ebd87",
"failure_signature": "verify|transient_infra|script failed with exit code: <n> ## output epos/fabro-sh/fabro/target/debug/deps/libtokio_stream-<hex>.rlib --extern tokio_tungstenite=/home/daytona/repos/fabro-sh/fabro/target/debug/deps/libtokio_tungstenite-<hex>.rlib --extern tokio_util",
"thread.simplify_fable.current_node": "simplify_sol",
"response.simplify_sol": "## Review complete\n\nThe three review agents completed, but their detailed reports were lost by the environment. As approved, I completed the reuse, quality, and efficiency review independently.\n\n### Cleanup applied\n\n- Narrowed implementation-only APIs to `pub(crate)`:\n - `ParallelBranchPreamble` and its fields\n - `TRANSIENT_CONTEXT_KEYS`\n - `artifact::strip_transient_keys`\n- Simplified comments and corrected the transient-key helper documentation.\n- Improved branch-preamble caching:\n - Preallocates the output array.\n - Renders each distinct fidelity only once.\n - Reuses an existing output entry rather than retaining a second potentially large JSON copy.\n- Preserved all required branch-fidelity, stash, nesting, `CURRENT_NODE`, `thread_id`, and simulation semantics.\n- No changelog entry added.\n\n### Verification\n\n- Focused fidelity tests: **23 passed**\n- Focused parallel-handler tests: **12 passed**\n- Targeted workflow/validation suite: **1,405 passed**\n- Workspace suite: **7,064 passed, 193 skipped**\n- Workspace Clippy with `-D warnings`: **passed**\n- `cargo fmt --check --all`: **passed**\n- `git diff --check`: **passed**\n\nThe cleanup remains uncommitted in three `fabro-workflow` files.",
"current_node": "verify"
},
"node_outcomes": {
"toolchain": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c"
},
"notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
"usage": null,
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 0,
"tool_time_ms": 1360,
"active_time_ms": 1360
}
},
"preflight_compile": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
},
"notes": "Script completed: cargo check -q --workspace 2>&1",
"usage": null,
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 0,
"tool_time_ms": 140243,
"active_time_ms": 140243
}
},
"verify": {
"status": "failed",
"context_updates": {
"command.output": "blob://sha256/20aad968504680e75d4178af570cdb1342400f3c5ce35c85432b519d463ebd87"
},
"failure": {
"message": "Script failed with exit code: 101\n\n## output\nepos/fabro-sh/fabro/target/debug/deps/libtokio_stream-591d8361188ee34e.rlib --extern tokio_tungstenite=/home/daytona/repos/fabro-sh/fabro/target/debug/deps/libtokio_tungstenite-f1516df7c1a94213.rlib --extern tokio_util=/home/daytona/repos/fabro-sh/fabro/target/debug/deps/libtokio_util-6348da4834ef424c.rlib --extern toml=/home/daytona/repos/fabro-sh/fabro/target/debug/deps/libtoml-bd4a9c4a711943a2.rlib --extern toml_edit=/home/daytona/repos/fabro-sh/fabro/target/debug/deps/libtoml_edit-596d73760f61acc1.rlib --extern tower=/home/daytona/repos/fabro-sh/fabro/target/debug/deps/libtower-10bfb87f98b92ff9.rlib --extern tower_http=/home/daytona/repos/fabro-sh/fabro/target/debug/deps/libtower_http-7236314f780bc699.rlib --extern tracing=/home/daytona/repos/fabro-sh/fabro/target/debug/deps/libtracing-6a027b00b1962923.rlib --extern tracing_subscriber=/home/daytona/repos/fabro-sh/fabro/target/debug/deps/libtracing_subscriber-fdb31653e6f70184.rlib --extern ulid=/home/daytona/repos/fabro-sh/fabro/target/debug/deps/libulid-415d39fc809ee67d.rlib --extern url=/home/daytona/repos/fabro-sh/fabro/target/debug/deps/liburl-884f84cc053543dd.rlib --extern uuid=/home/daytona/repos/fabro-sh/fabro/target/debug/deps/libuuid-0c3f38d800d5485b.rlib --extern walkdir=/home/daytona/repos/fabro-sh/fabro/target/debug/deps/libwalkdir-2974096791bcfa71.rlib --extern zeroize=/home/daytona/repos/fabro-sh/fabro/target/debug/deps/libzeroize-31c33fae13119713.rlib -L native=/home/daytona/repos/fabro-sh/fabro/target/debug/build/aws-lc-sys-7d7d45fe7882ea72/out -L native=/home/daytona/repos/fabro-sh/fabro/target/debug/build/ring-c0e8b6197f946676/out -L native=/home/daytona/repos/fabro-sh/fabro/target/debug/build/zstd-sys-1d5acc4c863d67ed/out -L native=/home/daytona/repos/fabro-sh/fabro/target/debug/build/libsqlite3-sys-47935f06e0c0a7ac/out -L native=/home/daytona/repos/fabro-sh/fabro/target/debug/build/fabro-proc-e8a41fbb5d44224d/out -L native=/home/daytona/repos/fabro-sh/fabro/target/debug/build/libgit2-sys-d50fa761712f9305/out/build -L native=/home/daytona/repos/fabro-sh/fabro/target/debug/build/libz-sys-867407a1ddfbbfa7/out/lib -L native=/home/daytona/repos/fabro-sh/fabro/target/debug/build/libz-sys-867407a1ddfbbfa7/out/lib -L native=/home/daytona/repos/fabro-sh/fabro/target/debug/build/openssl-sys-642e2caf82d0615b/out/openssl-build/install/lib -L native=/home/daytona/repos/fabro-sh/fabro/target/debug/build/graphviz-sys-e250031db13fd10b/out -L native=/home/daytona/repos/fabro-sh/fabro/target/debug/build/graphviz-sys-e250031db13fd10b/out -L native=/home/daytona/repos/fabro-sh/fabro/target/debug/build/graphviz-sys-e250031db13fd10b/out -L native=/home/daytona/repos/fabro-sh/fabro/target/debug/build/graphviz-sys-e250031db13fd10b/out -L native=/home/daytona/repos/fabro-sh/fabro/target/debug/build/graphviz-sys-e250031db13fd10b/out -L native=/home/daytona/repos/fabro-sh/fabro/target/debug/build/graphviz-sys-e250031db13fd10b/out -L native=/home/daytona/repos/fabro-sh/fabro/target/debug/build/graphviz-sys-e250031db13fd10b/out -L native=/home/daytona/repos/fabro-sh/fabro/target/debug/build/graphviz-sys-e250031db13fd10b/out -L native=/home/daytona/repos/fabro-sh/fabro/target/debug/build/graphviz-sys-e250031db13fd10b/out -L native=/home/daytona/repos/fabro-sh/fabro/target/debug/build/graphviz-sys-e250031db13fd10b/out -L native=/home/daytona/repos/fabro-sh/fabro/target/debug/build/graphviz-sys-e250031db13fd10b/out -L native=/home/daytona/repos/fabro-sh/fabro/target/debug/build/graphviz-sys-e250031db13fd10b/out -L native=/home/daytona/repos/fabro-sh/fabro/target/debug/build/graphviz-sys-e250031db13fd10b/out -L native=/home/daytona/repos/fabro-sh/fabro/target/debug/build/graphviz-sys-e250031db13fd10b/out -L native=/home/daytona/repos/fabro-sh/fabro/target/debug/build/graphviz-sys-e250031db13fd10b/out -L native=/home/daytona/repos/fabro-sh/fabro/target/debug/build/lz4-sys-d3db501f590f79c2/out` (exit status: 101)\nerror: command `/root/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin/cargo test --no-run --message-format json-render-diagnostics --workspace` exited with code 101\n",
"category": "transient_infra"
},
"usage": null,
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 0,
"tool_time_ms": 243888,
"active_time_ms": 243888
}
},
"simplify_fable": {
"status": "succeeded",
"context_updates": {
"last_response": "Everything is green. Summary of the simplify stage:\n\n## Review findings applied\n\n**Wire-format root cause (reuse #1, quality #1/#2, efficiency #2/#3):**\n- Added `Serialize`/`Deserialize` to `fabro_gra",
"response.simplify_fable": "Everything is green. Summary of the simplify stage:\n\n## Review findings applied\n\n**Wire-format root cause (reuse #1, quality #1/#2, efficiency #2/#3):**\n- Added `Serialize`/`Deserialize` to `fabro_graphviz::Fidelity` (serde renames aligned with strum per CLAUDE.md, plus `Hash` and a serde↔strum parity test; `serde_json` added as dev-dependency).\n- Replaced the hand-rolled JSON producer/parser pair with one shared `ParallelBranchPreamble` struct in `context.rs` (`deny_unknown_fields` replaces the brittle `entry.len() == 2` check). Producer uses `serde_json::to_value`, consumer uses `from_value` — a field rename can no longer silently split the contract. Documented the index-correlation invariant and the `Option<Option<…>>` semantics (quality #5/#8).\n\n**Duplication (reuse #2/#3, quality #6/#7):**\n- Extracted `explicit_fidelity()` — the edge→node precedence core is now shared between `resolve_fidelity` and `resolve_parallel_branch_fidelity`.\n- Added `keys::TRANSIENT_CONTEXT_KEYS` + `artifact::strip_transient_keys()`; both artifact strip sites use it. `stage_context_values` keeps its intentionally different behavior (retains `CURRENT_PREAMBLE`) with a WHY comment.\n- Unified the two copy-pasted full-fidelity diagnostic blocks into `full_fidelity_diagnostic()`; `full_fidelity_message()` → `const`; dead `fix_message` match → `const THREAD_ID_FIX`; capitalized the message for consistency; reworded the \"now honors\" changelog-style doc comment.\n\n**Structure & style (quality #3/#4/#9/#10/#14):**\n- Extracted the 38-line parallel stash block into `build_parallel_branch_preambles()`, restoring `before_node`'s numbered-step narrative (steps renumbered 5–9).\n- `ParallelBranchFidelityResolution`: dropped accessor ceremony and unused derives; plain documented fields.\n- Fixed the warn! to log `effective_fidelity` via `degraded()` instead of dressed-up constants; removed the redundant `fidelity` field.\n- `parallel.rs` uses `keys::`-scoped types; test helper `fidelity_attr` → `str_attr`.\n\n**Efficiency (#1, #4):**\n- Moved the parent stash clear to *before* the fork loop — branch forks no longer copy the full preamble array B times just to null it, and the per-branch null-set became unnecessary (forks inherit Null; nested-parallel safety preserved, verified by the existing `stash == Null` test assertions).\n- Memoized per-fidelity preamble renders — duplicate branch fidelities render once.\n\n**Validate helper (reuse #4, quality #13, efficiency #6):**\n- `ParallelBranches` now uses `Graph::incoming_edges`; `is_branch_only_node` is allocation-free; `branch_only_parents` replaced the dense peekable/`then`/`collect::<Option<BTreeSet>>` chain with straightforward code. Added the cross-rule suppression WHY comment in `thread_id_requires_fidelity_full`.\n\n**Skipped (noted, not addressed):** test-fixture consolidation into `rules/test_support` and a shared in-memory-store test helper (pre-existing pattern across ~15 sites, out of scope); `\"parallel\"` string literal (codebase-wide convention); `make_run_options` parameterization (follows existing file precedent).\n\n## Verification\n- `cargo nextest run -p fabro-graphviz -p fabro-validate -p fabro-workflow` — all pass (302 + 1218)\n- `cargo nextest run --workspace` — **7064 passed, 0 failed**\n- `cargo +nightly-2026-04-14 fmt --check --all` — clean\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` — clean\n\nNo blockers.",
"last_stage": "simplify_fable"
},
"notes": "Stage completed: simplify_fable",
"usage": {
"input": {
"usage": {
"model": {
"provider": "openrouter",
"model_id": "anthropic/claude-fable-5"
},
"tokens": {
"input_tokens": 8653765,
"output_tokens": 127619,
"reasoning_tokens": 33321,
"cache_read_tokens": 0,
"cache_write_tokens": 0
}
},
"facts": {
"algorithm": "openai"
}
}
},
"files_touched": [
"/home/daytona/workspace/fabro/lib/crates/fabro-graphviz/Cargo.toml",
"/home/daytona/workspace/fabro/lib/crates/fabro-graphviz/src/fidelity.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-validate/src/rules/parallel_branch.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-validate/src/rules/parallel_branch_inert_attribute.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-validate/src/rules/thread_id_requires_fidelity_full.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/artifact.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/context.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/handler/parallel.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/lifecycle/event.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/lifecycle/fidelity.rs"
],
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 2153413,
"tool_time_ms": 3192086,
"active_time_ms": 5345499
}
},
"simplify_sol": {
"status": "succeeded",
"context_updates": {
"last_stage": "simplify_sol",
"response.simplify_sol": "## Review complete\n\nThe three review agents completed, but their detailed reports were lost by the environment. As approved, I completed the reuse, quality, and efficiency review independently.\n\n### Cleanup applied\n\n- Narrowed implementation-only APIs to `pub(crate)`:\n - `ParallelBranchPreamble` and its fields\n - `TRANSIENT_CONTEXT_KEYS`\n - `artifact::strip_transient_keys`\n- Simplified comments and corrected the transient-key helper documentation.\n- Improved branch-preamble caching:\n - Preallocates the output array.\n - Renders each distinct fidelity only once.\n - Reuses an existing output entry rather than retaining a second potentially large JSON copy.\n- Preserved all required branch-fidelity, stash, nesting, `CURRENT_NODE`, `thread_id`, and simulation semantics.\n- No changelog entry added.\n\n### Verification\n\n- Focused fidelity tests: **23 passed**\n- Focused parallel-handler tests: **12 passed**\n- Targeted workflow/validation suite: **1,405 passed**\n- Workspace suite: **7,064 passed, 193 skipped**\n- Workspace Clippy with `-D warnings`: **passed**\n- `cargo fmt --check --all`: **passed**\n- `git diff --check`: **passed**\n\nThe cleanup remains uncommitted in three `fabro-workflow` files.",
"last_response": "## Review complete\n\nThe three review agents completed, but their detailed reports were lost by the environment. As approved, I completed the reuse, quality, and efficiency review independently.\n\n### C"
},
"notes": "Stage completed: simplify_sol",
"usage": {
"input": {
"usage": {
"model": {
"provider": "openrouter",
"model_id": "openai/gpt-5.6-sol"
},
"tokens": {
"input_tokens": 7067,
"output_tokens": 22374,
"reasoning_tokens": 115580,
"cache_read_tokens": 17705408,
"cache_write_tokens": 400919
}
},
"facts": {
"algorithm": "openai"
}
}
},
"files_touched": [
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/artifact.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/context.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/lifecycle/fidelity.rs"
],
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 3189823,
"tool_time_ms": 1428855,
"active_time_ms": 4618678
}
},
"implement": {
"status": "succeeded",
"context_updates": {
"last_stage": "implement",
"last_response": "Implemented the complete per-branch fidelity plan.\n\n### Highlights\n- Added ordered per-branch preamble stash generation and consumption.\n- Implemented edge → branch node → fork inheritance precede",
"response.implement": "Implemented the complete per-branch fidelity plan.\n\n### Highlights\n- Added ordered per-branch preamble stash generation and consumption.\n- Implemented edge → branch node → fork inheritance precedence.\n- Explicit branch `full` degrades to `summary:high` with structured logging.\n- Preserved byte-identical fork inheritance for unspecified/equal fidelities.\n- Added strict legacy fallback for missing, null, malformed, non-array, and mismatched stashes.\n- Cleared branch stashes for nested-parallel safety.\n- Removed stash data from checkpoints, durable snapshots, and persisted event contexts.\n- Kept branch `thread_id` inert, `CURRENT_NODE` inherited, and `simulate()` unchanged.\n- Updated validation rules and all three requested documentation pages.\n- Added unit, lifecycle, handler, integration, persistence, and lint regression coverage.\n- No changelog added.\n\n### Verification\n- `cargo nextest run --workspace`: **7,063 passed, 193 skipped**\n- `cargo +nightly-2026-04-14 fmt --check --all`: passed\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`: passed\n- `git diff --check`: passed\n\nLive testing-server probes were not run because the probe definitions and testing-server access were not available in this checkout/session."
},
"notes": "Stage completed: implement",
"usage": {
"input": {
"usage": {
"model": {
"provider": "openrouter",
"model_id": "openai/gpt-5.6-sol"
},
"tokens": {
"input_tokens": 394190,
"output_tokens": 32119,
"reasoning_tokens": 32341,
"cache_read_tokens": 13269488,
"cache_write_tokens": 886818
}
},
"facts": {
"algorithm": "openai"
}
}
},
"files_touched": [
"/home/daytona/workspace/fabro/docs/public/execution/context.mdx",
"/home/daytona/workspace/fabro/docs/public/reference/dot-language.mdx",
"/home/daytona/workspace/fabro/docs/public/workflows/stages-and-nodes.mdx",
"/home/daytona/workspace/fabro/lib/crates/fabro-validate/src/rules/mod.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-validate/src/rules/parallel_branch.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-validate/src/rules/parallel_branch_inert_attribute.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-validate/src/rules/thread_id_requires_fidelity_full.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/artifact.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/context.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/handler/parallel.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/lifecycle/event.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/lifecycle/fidelity.rs",
"/home/daytona/workspace/fabro/lib/crates/fabro-workflow/tests/it/integration.rs"
],
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 1625877,
"tool_time_ms": 1190483,
"active_time_ms": 2816360
}
},
"start": {
"status": "succeeded",
"usage": null
},
"preflight_lint": {
"status": "succeeded",
"context_updates": {
"command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"
},
"notes": "Script completed: cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1",
"usage": null,
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 0,
"tool_time_ms": 154518,
"active_time_ms": 154518
}
}
},
"next_node_id": "fixup",
"node_visits": {
"preflight_lint": 1,
"verify": 1,
"simplify_fable": 1,
"preflight_compile": 1,
"implement": 1,
"toolchain": 1,
"simplify_sol": 1,
"start": 1
}
},
"diff": {}
}
],
"conclusion": null,
"sandbox": {
"kind": "ready",
"plan": {
"provider": "daytona"
},
"instance": {
"provider": "daytona",
"snapshot": "fabro-fdb28dec-1233-892c-b9d7-9f88f8353e7a",
"runtime": {
"id": "fabro-01KY7Y01REECZ24XXTMBZ3PPV9",
"working_directory": "/home/daytona/workspace/fabro",
"repo_cloned": true,
"clone_origin_url": "https://github.com/fabro-sh/fabro",
"clone_branch": "main",
"workspace_root": "/home/daytona/workspace",
"repos_root": "/home/daytona/repos",
"primary_repo_path": "/home/daytona/repos/fabro-sh/fabro",
"primary_repo_link": "/home/daytona/workspace/fabro"
}
}
},
"pull_request": null,
"superseded_by": null,
"pending_interviews": {},
"stages": {
"preflight_lint@1": {
"first_event_seq": 41,
"prompt": null,
"response": null,
"completion": {
"outcome": "succeeded",
"notes": "Script completed: cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1",
"failure_reason": null,
"timestamp": "2026-07-23T16:52:51.063693166Z"
},
"provider_used": null,
"diff": null,
"script_invocation": {
"script": "cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1",
"command": "exec 2>&1\ncargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1",
"language": "shell"
},
"script_timing": {
"output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
"exit_code": 0,
"duration_ms": 154518,
"termination": "exited",
"output_bytes": 0,
"live_streaming": false
},
"parallel_results": null,
"output": null,
"output_bytes": 0,
"live_streaming": false,
"termination": "exited",
"started_at": "2026-07-23T16:50:16.542294456Z",
"handler": "command",
"timing": {
"wall_time_ms": 154521,
"inference_time_ms": 0,
"tool_time_ms": 154518,
"active_time_ms": 154518
},
"usage": {
"input_tokens": 0,
"output_tokens": 0,
"total_tokens": 0,
"reasoning_tokens": 0,
"cache_read_tokens": 0,
"cache_write_tokens": 0
},
"state": "succeeded"
},
"toolchain@1": {
"first_event_seq": 21,
"prompt": null,
"response": null,
"completion": {
"outcome": "succeeded",
"notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
"failure_reason": null,
"timestamp": "2026-07-23T16:47:49.218153106Z"
},
"provider_used": null,
"diff": null,
"script_invocation": {
"script": "command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
"command": "exec 2>&1\ncommand -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
"language": "shell"
},
"script_timing": {
"output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c",
"exit_code": 0,
"duration_ms": 1360,
"termination": "exited",
"output_bytes": 36,
"live_streaming": true
},
"parallel_results": null,
"output": null,
"output_bytes": 36,
"live_streaming": true,
"termination": "exited",
"started_at": "2026-07-23T16:47:47.855252051Z",
"handler": "command",
"timing": {
"wall_time_ms": 1362,
"inference_time_ms": 0,
"tool_time_ms": 1360,
"active_time_ms": 1360
},
"usage": {
"input_tokens": 0,
"output_tokens": 0,
"total_tokens": 0,
"reasoning_tokens": 0,
"cache_read_tokens": 0,
"cache_write_tokens": 0
},
"state": "succeeded"
},
"implement@1": {
"first_event_seq": 51,
"prompt": null,
"response": null,
"completion": {
"outcome": "succeeded",
"notes": "Stage completed: implement",
"failure_reason": null,
"timestamp": "2026-07-23T17:41:28.062493494Z"
},
"provider_used": {
"mode": "agent",
"provider": "openrouter",
"model": "openai/gpt-5.6-sol",
"reasoning_effort": "xhigh"
},
"diff": null,
"script_invocation": null,
"script_timing": null,
"parallel_results": null,
"output": null,
"started_at": "2026-07-23T16:52:54.792975774Z",
"handler": "agent",
"timing": {
"wall_time_ms": 2913269,
"inference_time_ms": 1625877,
"tool_time_ms": 1190483,
"active_time_ms": 2816360
},
"usage": {
"input_tokens": 394190,
"output_tokens": 32119,
"total_tokens": 14614956,
"reasoning_tokens": 32341,
"cache_read_tokens": 13269488,
"cache_write_tokens": 886818
},
"model": {
"provider": "openrouter",
"model_id": "openai/gpt-5.6-sol"
},
"todos": {
"kind": "openai_plan",
"list_id": "openai_plan:8977dddf-4a2c-43ee-8cf4-1857febfd156",
"items": [
{
"id": "a6a09ff21cdffb6e",
"status": "in_progress",
"order": 0,
"subject": "Locate and read the referenced plan, repository guidance, and relevant implementation/test code"
},
{
"id": "797802baf1b164c7",
"status": "pending",
"order": 1,
"subject": "Add failing workflow tests for lifecycle branch fidelity resolution and stash invariants"
},
{
"id": "48d1a604c387101a",
"status": "pending",
"order": 2,
"subject": "Implement lifecycle pre-rendering and make focused lifecycle tests pass"
},
{
"id": "a6b29d5815498873",
"status": "pending",
"order": 3,
"subject": "Add failing parallel handler tests for indexed stash consumption and legacy fallbacks"
},
{
"id": "fa516f5193fecc32",
"status": "pending",
"order": 4,
"subject": "Implement parallel handler stash handoff/reset and make focused tests pass"
},
{
"id": "a61df157709241e6",
"status": "pending",
"order": 5,
"subject": "Add engine-level parallel fidelity regressions and make them pass"
},
{
"id": "4326ed9eecb8fb59",
"status": "pending",
"order": 6,
"subject": "Implement context/artifact stash durability exclusions with focused TDD"
},
{
"id": "092d676c8ec0fbb6",
"status": "pending",
"order": 7,
"subject": "Implement validation rule changes and public documentation with focused TDD"
},
{
"id": "2619fc3f8186f0d9",
"status": "pending",
"order": 8,
"subject": "Run targeted crate tests, workspace tests, formatting, and Clippy; review diff"
}
]
},
"subagents": [
{
"agent_id": "30098430",
"depth": 1,
"task": "Implement goal steps 1-2 only with red/green TDD: add INTERNAL_PARALLEL_BRANCH_PREAMBLES key in lib/crates/fabro-workflow/src/context.rs; strip it in both artifact snapshot/normalization functions; add focused artifact tests per existing patterns. Read all relevant files/instructions first. Demonstrate red by adding tests and running focused test(s) before production changes, then green. Do not touch lifecycle/fidelity.rs, handler/parallel.rs, validation, docs, or integration.rs. Do not commit. Report changed files and exact test commands/results.",
"status": {
"kind": "completed",
"success": true,
"turns_used": 21
}
},
{
"agent_id": "42b3ed1a",
"depth": 1,
"task": "Implement goal steps 5-7 only with red/green TDD: update fabro-validate parallel branch inert fidelity behavior and full degradation warning; factor shared branch-only/fork-edge detection helper and make thread_id_requires_fidelity_full skip those cases; update narrow lint tests; update docs/public/execution/context.mdx, docs/public/workflows/stages-and-nodes.mdx, docs/public/reference/dot-language.mdx. No changelog. Read repository instructions and relevant files first. Demonstrate red with focused tests before production changes, then green. Do not touch fabro-workflow files. Do not commit. Report changed files and exact commands/results.",
"status": {
"kind": "completed",
"success": true,
"turns_used": 35
}
},
{
"agent_id": "e1360f54",
"depth": 1,
"task": "Update exactly these public docs for the implemented per-branch parallel fidelity semantics: docs/public/execution/context.mdx, docs/public/workflows/stages-and-nodes.mdx, docs/public/reference/dot-language.mdx. Cover branch fidelity precedence fork->branch edge then branch node then inherit fork preamble unchanged; per-branch preambles are rendered before fan-out from the fork snapshot; explicit branch full degrades to summary:high because concurrent branches cannot share sessions; thread_id on branch node or fork->branch edge is inert; update relevant node/edge attribute table notes. Do not add a changelog. Keep edits concise and consistent. Do not edit code or other files. Report files changed and rationale.",
"status": {
"kind": "completed",
"success": true,
"turns_used": 21
}
},
{
"agent_id": "1b10b11b",
"depth": 1,
"task": "Independently audit the current uncommitted implementation for the per-branch parallel fidelity plan. Focus on lib/crates/fabro-workflow src context/artifact/lifecycle/fidelity/handler/parallel and integration tests. Identify semantic bugs, missing required tests, race/nested-parallel issues, CURRENT_NODE or simulate changes, stash serialization mismatch, fidelity/logging issues. Do not edit files. Return actionable findings with file/line references, highest severity first.",
"status": {
"kind": "completed",
"success": true,
"turns_used": 21
}
},
{
"agent_id": "82ea593a",
"depth": 1,
"task": "Independently review the current uncommitted diff against the user's Per-Branch Fidelity plan. Do not edit files. Look for correctness gaps, edge cases, missing tests/docs, accidental simulate/CURRENT_NODE changes, persistence leaks, malformed stash behavior, validation topology issues, or style concerns. Report concrete findings with file/line references; say no findings if none. Note that full workspace nextest, pinned fmt, and pinned clippy are currently green.",
"status": {
"kind": "completed",
"success": true,
"turns_used": 21
}
}
],
"skills": {
"available": [
{
"name": "rust-style-guide",
"description": "Apply this Rust style guide when writing, reviewing, refactoring, or configuring Rust code for this project. Covers Rust 2024/MSRV, library vs application conventions, public API design, errors, panics, ownership and cloning, async/Tokio/concurrency, tracing, rustfmt/Clippy, testing with nextest, and unsafe/macro policy. Also use when setting up new Rust projects, investigating Rust performance, verifying library releases, or reviewing Rust code changes."
}
],
"activated": [
{
"name": "rust-style-guide",
"source": "tool"
},
{
"name": "rust-style-guide",
"source": "tool"
},
{
"name": "rust-style-guide",
"source": "tool"
},
{
"name": "rust-style-guide",
"source": "tool"
},
{
"name": "rust-style-guide",
"source": "tool"
},
{
"name": "rust-style-guide",
"source": "tool"
}
]
},
"permission_level": "full",
"agent_tools": [
{
"name": "close_agent",
"description": "Close a running subagent that is no longer needed.",
"source": {
"kind": "native"
},
"category": "subagent",
"invoked": false
},
{
"name": "edit_file",
"description": "Edit a file by replacing an exact string. The old_string must be an exact match and unique unless replace_all is true; include surrounding context when needed. Read the file first and preserve existing indentation.",
"source": {
"kind": "native"
},
"category": "write",
"invoked": true
},
{
"name": "glob",
"description": "Find files by file names using a glob pattern. Use path to choose the search root. Prefer this over shell find or ls when locating repository files.",
"source": {
"kind": "native"
},
"category": "read",
"invoked": true
},
{
"name": "grep",
"description": "Search file contents with a regex pattern. Use path to choose the search root, glob_filter to limit matching files, case_insensitive for case folding, and max_results to cap output.",
"source": {
"kind": "native"
},
"category": "read",
"invoked": true
},
{
"name": "read_file",
"description": "Read files before editing them. Returns line-numbered text and supports offset/limit for large files. Use this instead of shell cat, head, tail, or sed when inspecting repository files.",
"source": {
"kind": "native"
},
"category": "read",
"invoked": true
},
{
"name": "request_user_input",
"description": "Ask the human one or more questions and wait for their answers before continuing this stage.",
"source": {
"kind": "native"
},
"category": "other",
"invoked": false
},
{
"name": "send_input",
"description": "Send a follow-up message to a running subagent when new information or corrected instructions are needed.",
"source": {
"kind": "native"
},
"category": "subagent",
"invoked": true
},
{
"name": "shell",
"description": "Execute shell commands for terminal operations, package managers, tests and builds. Use dedicated tools for file reads, file edits, filename searches, and content searches. Provide timeout_ms for long-running commands.",
"source": {
"kind": "native"
},
"category": "shell",
"invoked": true
},
{
"name": "spawn_agent",
"description": "Spawn a subagent for independent work or context isolation. Use it for tasks that can proceed separately, and avoid duplicating the same work in the parent session.",
"source": {
"kind": "native"
},
"category": "subagent",
"invoked": true
},
{
"name": "update_plan",
"description": "Update the multi-step plan for the current task. Submit the entire plan; existing steps are reconciled by exact step text.",
"source": {
"kind": "native"
},
"category": "other",
"invoked": true
},
{
"name": "use_skill",
"description": "Load a skill's instructions by name. Call this when the user's request matches an available skill.",
"source": {
"kind": "skill"
},
"category": "other",
"invoked": true
},
{
"name": "wait",
"description": "Wait for a subagent to complete, then use the result to synthesize the outcome for the user.",
"source": {
"kind": "native"
},
"category": "subagent",
"invoked": true
},
{
"name": "web_fetch",
"description": "Fetch content from a URL that starts with http:// or https://. Pass a prompt to extract specific information or summarize the page; omit prompt to return the page content.",
"source": {
"kind": "native"
},
"category": "other",
"invoked": false
},
{
"name": "web_search",
"description": "Search the web using Brave Search when current external information is needed. Returns result titles, URLs, and descriptions; use web_fetch for a specific URL.",
"source": {
"kind": "native"
},
"category": "other",
"invoked": false
},
{
"name": "write_file",
"description": "Create new files, or overwrite an existing file only when replacement is explicitly intended. Prefer edit_file for targeted changes to existing files because write_file overwrites the full file content.",
"source": {
"kind": "native"
},
"category": "write",
"invoked": true
}
],
"context_window": {
"provider": "openrouter",
"model": "openai/gpt-5.6-sol",
"context_window_tokens": 200000,
"input_tokens": 68679,
"usage_percent": 34.3395,
"count_method": "response_usage_scaled_breakdown",
"staleness": "live",
"generated_at": "2026-07-23T17:41:28.002129821Z",
"event_seq": 1341,
"breakdown": [
{
"category": "system_prompt",
"tokens": 1077,
"usage_percent": 0.5385
},
{
"category": "tools",
"tokens": 1593,
"usage_percent": 0.7965
},
{
"category": "skills",
"tokens": 213,
"usage_percent": 0.1065
},
{
"category": "memory",
"tokens": 3915,
"usage_percent": 1.9575
},
{
"category": "conversation",
"tokens": 61873,
"usage_percent": 30.9365
},
{
"category": "other",
"tokens": 8,
"usage_percent": 0.004
}
],
"warnings": [
{
"code": "activated_skill_context_counted_as_conversation",
"message": "Activated skill instructions are counted as conversation in this version."
}
]
},
"state": "succeeded"
},
"simplify_fable@1": {
"first_event_seq": 1351,
"prompt": null,
"response": null,
"completion": {
"outcome": "succeeded",
"notes": "Stage completed: simplify_fable",
"failure_reason": null,
"timestamp": "2026-07-23T19:10:38.040880678Z"
},
"provider_used": {
"mode": "agent",
"provider": "openrouter",
"model": "anthropic/claude-fable-5",
"reasoning_effort": "xhigh"
},
"diff": null,
"script_invocation": null,
"script_timing": null,
"parallel_results": null,
"output": null,
"started_at": "2026-07-23T17:41:31.847060646Z",
"handler": "agent",
"timing": {
"wall_time_ms": 5346193,
"inference_time_ms": 2153413,
"tool_time_ms": 3192086,
"active_time_ms": 5345499
},
"usage": {
"input_tokens": 8653765,
"output_tokens": 127619,
"total_tokens": 8814705,
"reasoning_tokens": 33321,
"cache_read_tokens": 0,
"cache_write_tokens": 0
},
"model": {
"provider": "openrouter",
"model_id": "anthropic/claude-fable-5"
},
"subagents": [
{
"agent_id": "d24c4e8d",
"depth": 1,
"task": "You are a CODE REUSE reviewer for the Rust repo at /home/daytona/workspace/fabro. A feature \"Per-Branch Fidelity for Parallel Branches\" was just implemented. The full diff is saved at /tmp/implement.diff (also viewable via `git diff 1879fe090..1a2bd7966`). Read the diff first.\n\nChanged files:\n- lib/crates/fabro-workflow/src/lifecycle/fidelity.rs (per-branch preamble stash rendering in before_node, resolve_parallel_branch_fidelity helper, tests)\n- lib/crates/fabro-workflow/src/handler/parallel.rs (parse_branch_preambles, branch context injection, tests)\n- lib/crates/fabro-workflow/src/lifecycle/event.rs (stage_context_values helper)\n- lib/crates/fabro-workflow/src/artifact.rs (stripping new context key)\n- lib/crates/fabro-workflow/src/context.rs (new key constant)\n- lib/crates/fabro-validate/src/rules/parallel_branch.rs (NEW shared helper ParallelBranches)\n- lib/crates/fabro-validate/src/rules/parallel_branch_inert_attribute.rs\n- lib/crates/fabro-validate/src/rules/thread_id_requires_fidelity_full.rs\n- lib/crates/fabro-workflow/tests/it/integration.rs\n- docs (mdx files)\n\nYour job — for each change:\n1. Search the codebase for existing utilities/helpers that could replace newly written code. Look in fabro-workflow, fabro-graphviz, fabro-types, fabro-validate, fabro-util. E.g.: is there an existing \"is parallel node\" / handler_type helper, an existing fidelity parse helper, an existing serde struct that could replace the hand-rolled JSON parsing in parse_branch_preambles, existing test fixtures/helpers (e.g. test graph builders, test lifecycles, test stores) that the new tests duplicate?\n2. Flag any new function duplicating existing functionality — name the existing function to use instead, with file:line.\n3. Flag inline logic that could use an existing utility (hand-rolled string manipulation, JSON handling, graph traversal like outgoing_edges, ad-hoc filters).\n\nPay special attention to:\n- resolve_parallel_branch_fidelity vs the existing resolve_fidelity in the same file\n- parse_branch_preambles hand-rolled JSON vs serde derive patterns used elsewhere in the crate\n- ParallelBranches in fabro-validate vs any existing graph query helpers\n- test fixtures in the new tests vs existing test_support / fixture helpers (e.g. `EngineServices::test_default`, existing lifecycle test scaffolding, integration.rs helpers)\n- stage_context_values in event.rs vs durable_context_snapshot in artifact.rs — is there duplication of \"strip internal keys from snapshot\" logic?\n\nDo NOT make edits. Output a numbered list of findings, each with: file:line, what the issue is, what existing code should be used instead, and severity (high/medium/low). If no issues, say so. Be precise; verify claims by reading actual code.",
"status": {
"kind": "completed",
"success": true,
"turns_used": 105
}
},
{
"agent_id": "6bcd05e4",
"depth": 1,
"task": "You are a CODE QUALITY reviewer for the Rust repo at /home/daytona/workspace/fabro. A feature \"Per-Branch Fidelity for Parallel Branches\" was just implemented. The full diff is saved at /tmp/implement.diff (also viewable via `git diff 1879fe090..1a2bd7966`). Read the diff first, then read the surrounding code in changed files for context.\n\nChanged files:\n- lib/crates/fabro-workflow/src/lifecycle/fidelity.rs\n- lib/crates/fabro-workflow/src/handler/parallel.rs\n- lib/crates/fabro-workflow/src/lifecycle/event.rs\n- lib/crates/fabro-workflow/src/artifact.rs\n- lib/crates/fabro-workflow/src/context.rs\n- lib/crates/fabro-validate/src/rules/parallel_branch.rs (new)\n- lib/crates/fabro-validate/src/rules/parallel_branch_inert_attribute.rs\n- lib/crates/fabro-validate/src/rules/thread_id_requires_fidelity_full.rs\n- lib/crates/fabro-workflow/tests/it/integration.rs\n- docs mdx files\n\nReview for hacky patterns:\n1. Redundant state: state duplicating existing state, cached values derivable elsewhere\n2. Parameter sprawl: new params where restructuring would be better\n3. Copy-paste with slight variation: near-duplicate blocks needing shared abstraction (e.g. the two diagnostic-push blocks in parallel_branch_inert_attribute.rs for edges vs nodes; the full-fidelity warn blocks; repeated attr-insert boilerplate in tests)\n4. Leaky abstractions: exposing internals that should be encapsulated. Look hard at the ParallelBranchFidelityResolution struct in fidelity.rs — it has `requested()` and `entry_fidelity()` accessor methods on a private struct with public-looking API ceremony; is this over-engineered for a module-private helper? Could it be a simpler return type?\n5. Stringly-typed code: raw strings where constants/enums exist. Check \"parallel\" handler_type literal, \"fidelity\"/\"thread_id\" attr strings, the {\"fidelity\",\"preamble\"} JSON object shape in parallel.rs vs a serde struct, `keys::Fidelity` usage.\n6. Unnecessary comments: comments explaining WHAT (delete) vs non-obvious WHY (keep)\n7. Naming/consistency with the codebase style (check CLAUDE.md conventions: strum for enum conversions, import style, no glob imports in prod code)\n\nAlso check:\n- In fidelity.rs before_node, the parallel stash block: is the structure clear, does the resolution struct pull its weight, is the tracing::warn! per docs/internal/logging-strategy.md conventions (read that doc)?\n- In parallel.rs parse_branch_preambles: the `entry.len() == 2` check — brittle? The Option<Option<BranchPreamble>> return — clear enough?\n- Test quality: do new tests follow existing patterns in the files they were added to?\n\nDo NOT make edits. Output a numbered list of findings with file:line, issue, suggested fix, and severity (high/medium/low). If clean, say so. Verify claims against actual code.",
"status": {
"kind": "completed",
"success": true,
"turns_used": 59
}
},
{
"agent_id": "46c8d151",
"depth": 1,
"task": "You are an EFFICIENCY reviewer for the Rust repo at /home/daytona/workspace/fabro. A feature \"Per-Branch Fidelity for Parallel Branches\" was just implemented. The full diff is saved at /tmp/implement.diff (also viewable via `git diff 1879fe090..1a2bd7966`). Read the diff first, then read the surrounding code in the changed files.\n\nChanged files:\n- lib/crates/fabro-workflow/src/lifecycle/fidelity.rs (before_node now sets a Null context key on EVERY node visit, and for parallel nodes renders per-branch preambles)\n- lib/crates/fabro-workflow/src/handler/parallel.rs (parse_branch_preambles + branch context injection)\n- lib/crates/fabro-workflow/src/lifecycle/event.rs (stage_context_values takes a full snapshot per stage-completion event)\n- lib/crates/fabro-workflow/src/artifact.rs\n- lib/crates/fabro-workflow/src/context.rs\n- lib/crates/fabro-validate/src/rules/parallel_branch.rs (new ParallelBranches helper — check its per-call complexity: branch_only_parents scans all edges per node; is_branch_only_node called per node in thread_id_requires_fidelity_full → O(nodes × edges)?)\n- lib/crates/fabro-validate/src/rules/parallel_branch_inert_attribute.rs\n- lib/crates/fabro-validate/src/rules/thread_id_requires_fidelity_full.rs\n- lib/crates/fabro-workflow/tests/it/integration.rs\n\nReview for:\n1. Unnecessary work: redundant computations, repeated snapshots/clones, rebuilding data per iteration. Check: does before_node's unconditional `state.context.set(..., Null)` cause churn (e.g. context write-back diffs, event emission, checkpoint dirty-marking) on every node even in workflows with no parallel nodes? Read context.rs set() and node_handler.rs write-back logic to verify whether setting Null over an already-Null/absent key is a no-op or triggers downstream work.\n2. Missed concurrency: none expected, but check.\n3. Hot-path bloat: stage_context_values clones the full context snapshot per event — was that already the case before? (compare to previous code in the diff; only flag regressions).\n4. Recurring no-op updates: the Null-set in every branch fork and after the loop in parallel.rs — necessary per design (load-bearing), but check whether context.set of Null on a key that's absent creates spurious diff entries in the parent write-back (node_handler.rs:99-105) and whether that pollutes checkpoints/events with \"internal.parallel_branch_preambles\": null entries. Read artifact.rs strip logic and event.rs stage_context_values to see if Null entries leak anywhere (e.g. progress.jsonl, stage completed events, checkpoints).\n5. Memory: preamble strings duplicated across stash + branch contexts — any unnecessary clones that could be moves? Check parse_branch_preambles (entry.get + to_string vs remove/take), and the json! macro usages cloning strings.\n6. Overly broad operations: snapshot() of whole context when only one key needed, scanning all graph edges repeatedly.\n7. In fidelity.rs: build_preamble called per branch — fine — but check resolved_context / resolved_outcomes are NOT re-resolved per branch (blobs resolved once). Verify by reading the code.\n\nDo NOT make edits. Output a numbered list of findings with file:line, issue, why it matters, suggested fix, severity (high/medium/low). Distinguish real regressions from pre-existing behavior. If clean, say so.",
"status": {
"kind": "completed",
"success": true,
"turns_used": 46
}
}
],
"skills": {
"available": [
{
"name": "rust-style-guide",
"description": "Apply this Rust style guide when writing, reviewing, refactoring, or configuring Rust code for this project. Covers Rust 2024/MSRV, library vs application conventions, public API design, errors, panics, ownership and cloning, async/Tokio/concurrency, tracing, rustfmt/Clippy, testing with nextest, and unsafe/macro policy. Also use when setting up new Rust projects, investigating Rust performance, verifying library releases, or reviewing Rust code changes."
}
],
"activated": [
{
"name": "rust-style-guide",
"source": "tool"
}
]
},
"permission_level": "full",
"agent_tools": [
{
"name": "close_agent",
"description": "Close a running subagent that is no longer needed.",
"source": {
"kind": "native"
},
"category": "subagent",
"invoked": false
},
{
"name": "edit_file",
"description": "Edit a file by replacing an exact string. The old_string must be an exact match and unique unless replace_all is true; include surrounding context when needed. Read the file first and preserve existing indentation.",
"source": {
"kind": "native"
},
"category": "write",
"invoked": true
},
{
"name": "glob",
"description": "Find files by file names using a glob pattern. Use path to choose the search root. Prefer this over shell find or ls when locating repository files.",
"source": {
"kind": "native"
},
"category": "read",
"invoked": true
},
{
"name": "grep",
"description": "Search file contents with a regex pattern. Use path to choose the search root, glob_filter to limit matching files, case_insensitive for case folding, and max_results to cap output.",
"source": {
"kind": "native"
},
"category": "read",
"invoked": true
},
{
"name": "read_file",
"description": "Read files before editing them. Returns line-numbered text and supports offset/limit for large files. Use this instead of shell cat, head, tail, or sed when inspecting repository files.",
"source": {
"kind": "native"
},
"category": "read",
"invoked": true
},
{
"name": "request_user_input",
"description": "Ask the human one or more questions and wait for their answers before continuing this stage.",
"source": {
"kind": "native"
},
"category": "other",
"invoked": false
},
{
"name": "send_input",
"description": "Send a follow-up message to a running subagent when new information or corrected instructions are needed.",
"source": {
"kind": "native"
},
"category": "subagent",
"invoked": false
},
{
"name": "shell",
"description": "Execute shell commands for terminal operations, package managers, tests and builds. Use dedicated tools for file reads, file edits, filename searches, and content searches. Provide timeout_ms for long-running commands.",
"source": {
"kind": "native"
},
"category": "shell",
"invoked": true
},
{
"name": "spawn_agent",
"description": "Spawn a subagent for independent work or context isolation. Use it for tasks that can proceed separately, and avoid duplicating the same work in the parent session.",
"source": {
"kind": "native"
},
"category": "subagent",
"invoked": true
},
{
"name": "update_plan",
"description": "Update the multi-step plan for the current task. Submit the entire plan; existing steps are reconciled by exact step text.",
"source": {
"kind": "native"
},
"category": "other",
"invoked": false
},
{
"name": "use_skill",
"description": "Load a skill's instructions by name. Call this when the user's request matches an available skill.",
"source": {
"kind": "skill"
},
"category": "other",
"invoked": true
},
{
"name": "wait",
"description": "Wait for a subagent to complete, then use the result to synthesize the outcome for the user.",
"source": {
"kind": "native"
},
"category": "subagent",
"invoked": true
},
{
"name": "web_fetch",
"description": "Fetch content from a URL that starts with http:// or https://. Pass a prompt to extract specific information or summarize the page; omit prompt to return the page content.",
"source": {
"kind": "native"
},
"category": "other",
"invoked": false
},
{
"name": "web_search",
"description": "Search the web using Brave Search when current external information is needed. Returns result titles, URLs, and descriptions; use web_fetch for a specific URL.",
"source": {
"kind": "native"
},
"category": "other",
"invoked": false
},
{
"name": "write_file",
"description": "Create new files, or overwrite an existing file only when replacement is explicitly intended. Prefer edit_file for targeted changes to existing files because write_file overwrites the full file content.",
"source": {
"kind": "native"
},
"category": "write",
"invoked": true
}
],
"context_window": {
"provider": "openrouter",
"model": "anthropic/claude-fable-5",
"context_window_tokens": 200000,
"input_tokens": 148278,
"usage_percent": 74.139,
"count_method": "response_usage_scaled_breakdown",
"staleness": "live",
"generated_at": "2026-07-23T19:10:37.948719737Z",
"event_seq": 2384,
"breakdown": [
{
"category": "system_prompt",
"tokens": 1238,
"usage_percent": 0.619
},
{
"category": "tools",
"tokens": 1827,
"usage_percent": 0.9135
},
{
"category": "skills",
"tokens": 245,
"usage_percent": 0.1225
},
{
"category": "memory",
"tokens": 4490,
"usage_percent": 2.245
},
{
"category": "conversation",
"tokens": 140470,
"usage_percent": 70.235
},
{
"category": "other",
"tokens": 8,
"usage_percent": 0.004
}
],
"warnings": [
{
"code": "activated_skill_context_counted_as_conversation",
"message": "Activated skill instructions are counted as conversation in this version."
}
]
},
"state": "succeeded"
},
"simplify_sol@1": {
"first_event_seq": 2394,
"prompt": null,
"response": null,
"completion": {
"outcome": "succeeded",
"notes": "Stage completed: simplify_sol",
"failure_reason": null,
"timestamp": "2026-07-23T20:29:17.175621286Z"
},
"provider_used": {
"mode": "agent",
"provider": "openrouter",
"model": "openai/gpt-5.6-sol",
"reasoning_effort": "max"
},
"diff": null,
"script_invocation": null,
"script_timing": null,
"parallel_results": null,
"output": null,
"started_at": "2026-07-23T19:10:41.904013633Z",
"handler": "agent",
"timing": {
"wall_time_ms": 4715271,
"inference_time_ms": 3189823,
"tool_time_ms": 1428855,
"active_time_ms": 4618678
},
"usage": {
"input_tokens": 7067,
"output_tokens": 22374,
"total_tokens": 18251348,
"reasoning_tokens": 115580,
"cache_read_tokens": 17705408,
"cache_write_tokens": 400919
},
"model": {
"provider": "openrouter",
"model_id": "openai/gpt-5.6-sol"
},
"todos": {
"kind": "openai_plan",
"list_id": "openai_plan:ca7c0d77-437e-4954-9e45-eb4e813be0af",
"items": [
{
"id": "1937f943602a0454",
"status": "completed",
"order": 0,
"subject": "Inspect the complete implementation diff"
},
{
"id": "275edfd0e6a8ce69",
"status": "completed",
"order": 1,
"subject": "Launch code reuse, code quality, and efficiency reviews concurrently"
},
{
"id": "2f48a9a76d58f2cb",
"status": "completed",
"order": 2,
"subject": "Aggregate review findings and identify worthwhile fixes"
},
{
"id": "379dbfda729b8d1f",
"status": "completed",
"order": 3,
"subject": "Apply focused cleanup fixes while preserving branch-fidelity semantics"
},
{
"id": "321a2ee1817b5ea0",
"status": "completed",
"order": 4,
"subject": "Run formatting, linting, targeted tests, and workspace tests"
},
{
"id": "763f5a8f0e0dad20",
"status": "completed",
"order": 5,
"subject": "Inspect final diff and report results"
}
]
},
"subagents": [
{
"agent_id": "c2b3c022",
"depth": 1,
"task": "Perform the CODE REUSE review for the per-branch fidelity implementation. Do not edit files. Before reviewing, read the ENTIRE exact full diff at `/tmp/fabro-per-branch-fidelity-review.diff` (1,801 lines; generated by `git diff --binary 1879fe090..HEAD`) so you have complete context, then inspect/search the repository wherever needed. Review every changed file for: existing utilities/helpers that could replace new code, duplicate new functions, inline hand-rolled logic that should use an existing utility, and abstractions already available nearby or in shared modules. Pay special attention to the Rust project conventions in AGENTS.md and relevant adjacent code. Return only actionable findings, each with severity, file/line, explanation, existing helper/location to reuse, and a concrete suggested fix. If no worthwhile issue exists, explicitly say the changes are clean for reuse. Do not flag intentional behavior from the goal merely because it is specialized.",
"status": {
"kind": "completed",
"success": true,
"turns_used": 15
}
},
{
"agent_id": "fe19a5c8",
"depth": 1,
"task": "Perform the CODE QUALITY review for the per-branch fidelity implementation. Do not edit files. Before reviewing, read the ENTIRE exact full diff at `/tmp/fabro-per-branch-fidelity-review.diff` (1,801 lines; generated by `git diff --binary 1879fe090..HEAD`) so you have complete context, then inspect adjacent/current repository code wherever needed. Review every changed file for redundant state, parameter sprawl, copy-paste variants, leaky abstractions, stringly typed code where types/constants exist, unnecessary comments that narrate WHAT/change history, brittle tests, and any correctness or maintainability issue exposed by the cleanup. Respect the user-confirmed semantics (explicit-only branch fidelity; full degrades; equality skip; thread_id inert; CURRENT_NODE inherited; simulate untouched; indexed array stash). Return only actionable findings with severity, file/line, explanation, and concrete fix. If no worthwhile issue exists, explicitly say the changes are clean. Do not edit or merely restate intended design.",
"status": {
"kind": "completed",
"success": true,
"turns_used": 15
}
},
{
"agent_id": "d0d7ef25",
"depth": 1,
"task": "Perform the EFFICIENCY review for the per-branch fidelity implementation. Do not edit files. Before reviewing, read the ENTIRE exact full diff at `/tmp/fabro-per-branch-fidelity-review.diff` (1,801 lines; generated by `git diff --binary 1879fe090..HEAD`) so you have complete context, then inspect/search current and adjacent code as needed. Review every changed file for redundant computation or reads, missed safe concurrency, hot-path bloat, recurring no-op state updates, unnecessary prechecks, memory growth/cleanup, overly broad operations, avoidable preamble/blob rendering, and inefficient test setup. Account for the required concurrency safety and explicit-only semantics; do not suggest concurrently invoking the lifecycle. Return only actionable findings with severity, file/line, evidence, and concrete fix. If no worthwhile issue exists, explicitly say the changes are clean for efficiency. Do not edit files.",
"status": {
"kind": "completed",
"success": true,
"turns_used": 15
}
},
{
"agent_id": "05fc028b",
"depth": 1,
"task": "CODE REUSE REVIEW (read-only). Review the complete implementation diff in /tmp/fabro-per-branch-fidelity-review.diff, which is the full `git diff 1879fe090..HEAD` for the Per-Branch Fidelity implementation. You MUST read the entire diff before concluding, then inspect current surrounding source and search the repository for existing helpers/utilities that could replace new code. Flag duplicated functions, ad-hoc logic that should reuse an existing abstraction, unnecessary new dependencies/types, or missed consolidation. Preserve these fixed semantics: explicit branch fidelity only (edge then node then inherit fork preamble); explicit full degrades to summary:high; skip override if post-degradation equal to fork post-degradation; thread_id inert; CURRENT_NODE remains fork; simulate unchanged; index-aligned JSON stash with Null inherit; malformed/missing/null/length mismatch legacy; null stash in every branch fork; no changelog. Do not edit files. Return concise, prioritized actionable findings with exact file/line references and suggested reuse. Explicitly say if no actionable findings.",
"status": {
"kind": "completed",
"success": true,
"turns_used": 15
}
},
{
"agent_id": "a62887da",
"depth": 1,
"task": "CODE QUALITY REVIEW (read-only). Review the complete implementation diff in /tmp/fabro-per-branch-fidelity-review.diff, which is the full `git diff 1879fe090..HEAD` for the Per-Branch Fidelity implementation. You MUST read the entire diff before concluding, then inspect current surrounding source. Look for redundant state, parameter sprawl, near-copy/paste, leaky abstractions/public API exposure, stringly typed code, unnecessary comments, weak error handling, incorrect edge cases, and tests/docs that fail to encode the contract. Preserve these fixed semantics: explicit branch fidelity only (edge then node then inherit fork preamble); explicit full degrades to summary:high; skip override if post-degradation equal to fork post-degradation; thread_id inert; CURRENT_NODE remains fork; simulate unchanged; index-aligned JSON stash with Null inherit; malformed/missing/null/length mismatch legacy; null stash in every branch fork; no changelog. Follow AGENTS/CLAUDE and relevant strategy docs. Do not edit files. Return concise, prioritized actionable findings with exact file/line references and proposed fixes. Explicitly say if no actionable findings.",
"status": {
"kind": "completed",
"success": true,
"turns_used": 15
}
},
{
"agent_id": "d37b2c88",
"depth": 1,
"task": "EFFICIENCY REVIEW (read-only). Review the complete implementation diff in /tmp/fabro-per-branch-fidelity-review.diff, which is the full `git diff 1879fe090..HEAD` for the Per-Branch Fidelity implementation. You MUST read the entire diff before concluding, then inspect current surrounding source. Find unnecessary/repeated computation, avoidable clones/serialization/deserialization/allocation, hot-path bloat, recurring no-op updates, missed safe concurrency, TOCTOU checks, unbounded memory, and overly broad operations. Distinguish meaningful efficiency issues from tiny non-actionable micro-optimizations. Preserve these fixed semantics: explicit branch fidelity only (edge then node then inherit fork preamble); explicit full degrades to summary:high; skip override if post-degradation equal to fork post-degradation; thread_id inert; CURRENT_NODE remains fork; simulate unchanged; index-aligned JSON stash with Null inherit; malformed/missing/null/length mismatch legacy; null stash in every branch fork; no changelog. Do not edit files. Return concise, prioritized actionable findings with exact file/line references and proposed fixes. Explicitly say if no actionable findings.",
"status": {
"kind": "completed",
"success": true,
"turns_used": 15
}
},
{
"agent_id": "d4e2a1bc",
"depth": 1,
"task": "FINAL CODE-REUSE REVIEW, READ-ONLY. Read ALL of /tmp/fabro-per-branch-fidelity-review.diff first (it is the complete 1879fe090..HEAD diff), obey repository instructions, then inspect only enough surrounding code/search results to substantiate findings. This is a final report task: reserve your last turn for a concise final answer and do not end on a progress update. Focus on existing utilities/helpers, duplicated abstractions, unnecessary new public types/dependencies, and consolidation opportunities. Fixed semantics are exactly those in the user plan; do not suggest changing them. No edits. Report only actionable issues with severity, exact current file:line, evidence, and concrete fix. If none, say no actionable reuse findings. You have up to 24 turns but MUST deliver the final report no later than turn 22.",
"status": {
"kind": "completed",
"success": true,
"turns_used": 25
}
},
{
"agent_id": "0f16d862",
"depth": 1,
"task": "FINAL CODE-QUALITY REVIEW, READ-ONLY. Read ALL of /tmp/fabro-per-branch-fidelity-review.diff first (it is the complete 1879fe090..HEAD diff), obey repository instructions, then inspect only enough surrounding code to substantiate findings. This is a final report task: reserve your last turn for a concise final answer and do not end on a progress update. Focus on correctness edge cases, redundant/leaky/stringly state, public API exposure, copy-paste, comments, diagnostics, tests, and docs. Fixed semantics are exactly those in the user plan; do not suggest changing them. No edits. Report only actionable issues with severity, exact current file:line, evidence, and concrete fix. If none, say no actionable quality findings. You have up to 24 turns but MUST deliver the final report no later than turn 22.",
"status": {
"kind": "completed",
"success": true,
"turns_used": 25
}
},
{
"agent_id": "31a207d5",
"depth": 1,
"task": "FINAL EFFICIENCY REVIEW, READ-ONLY. Read ALL of /tmp/fabro-per-branch-fidelity-review.diff first (it is the complete 1879fe090..HEAD diff), obey repository instructions, then inspect only enough surrounding code to substantiate findings. This is a final report task: reserve your last turn for a concise final answer and do not end on a progress update. Focus on meaningful avoidable cloning/serialization/allocation/repeated graph scans, hot-path work, no-op updates, memory, and overly broad operations; skip tiny micro-optimizations. Fixed semantics are exactly those in the user plan; do not suggest changing them. No edits. Report only actionable issues with severity, exact current file:line, evidence, and concrete fix. If none, say no actionable efficiency findings. You have up to 24 turns but MUST deliver the final report no later than turn 22.",
"status": {
"kind": "completed",
"success": true,
"turns_used": 25
}
}
],
"skills": {
"available": [
{
"name": "rust-style-guide",
"description": "Apply this Rust style guide when writing, reviewing, refactoring, or configuring Rust code for this project. Covers Rust 2024/MSRV, library vs application conventions, public API design, errors, panics, ownership and cloning, async/Tokio/concurrency, tracing, rustfmt/Clippy, testing with nextest, and unsafe/macro policy. Also use when setting up new Rust projects, investigating Rust performance, verifying library releases, or reviewing Rust code changes."
}
],
"activated": [
{
"name": "rust-style-guide",
"source": "tool"
},
{
"name": "rust-style-guide",
"source": "tool"
},
{
"name": "rust-style-guide",
"source": "tool"
},
{
"name": "rust-style-guide",
"source": "tool"
},
{
"name": "rust-style-guide",
"source": "tool"
},
{
"name": "rust-style-guide",
"source": "tool"
},
{
"name": "rust-style-guide",
"source": "tool"
},
{
"name": "rust-style-guide",
"source": "tool"
},
{
"name": "rust-style-guide",
"source": "tool"
},
{
"name": "rust-style-guide",
"source": "tool"
}
]
},
"permission_level": "full",
"agent_tools": [
{
"name": "close_agent",
"description": "Close a running subagent that is no longer needed.",
"source": {
"kind": "native"
},
"category": "subagent",
"invoked": false
},
{
"name": "edit_file",
"description": "Edit a file by replacing an exact string. The old_string must be an exact match and unique unless replace_all is true; include surrounding context when needed. Read the file first and preserve existing indentation.",
"source": {
"kind": "native"
},
"category": "write",
"invoked": true
},
{
"name": "glob",
"description": "Find files by file names using a glob pattern. Use path to choose the search root. Prefer this over shell find or ls when locating repository files.",
"source": {
"kind": "native"
},
"category": "read",
"invoked": true
},
{
"name": "grep",
"description": "Search file contents with a regex pattern. Use path to choose the search root, glob_filter to limit matching files, case_insensitive for case folding, and max_results to cap output.",
"source": {
"kind": "native"
},
"category": "read",
"invoked": true
},
{
"name": "read_file",
"description": "Read files before editing them. Returns line-numbered text and supports offset/limit for large files. Use this instead of shell cat, head, tail, or sed when inspecting repository files.",
"source": {
"kind": "native"
},
"category": "read",
"invoked": true
},
{
"name": "request_user_input",
"description": "Ask the human one or more questions and wait for their answers before continuing this stage.",
"source": {
"kind": "native"
},
"category": "other",
"invoked": true
},
{
"name": "send_input",
"description": "Send a follow-up message to a running subagent when new information or corrected instructions are needed.",
"source": {
"kind": "native"
},
"category": "subagent",
"invoked": true
},
{
"name": "shell",
"description": "Execute shell commands for terminal operations, package managers, tests and builds. Use dedicated tools for file reads, file edits, filename searches, and content searches. Provide timeout_ms for long-running commands.",
"source": {
"kind": "native"
},
"category": "shell",
"invoked": true
},
{
"name": "spawn_agent",
"description": "Spawn a subagent for independent work or context isolation. Use it for tasks that can proceed separately, and avoid duplicating the same work in the parent session.",
"source": {
"kind": "native"
},
"category": "subagent",
"invoked": true
},
{
"name": "update_plan",
"description": "Update the multi-step plan for the current task. Submit the entire plan; existing steps are reconciled by exact step text.",
"source": {
"kind": "native"
},
"category": "other",
"invoked": true
},
{
"name": "use_skill",
"description": "Load a skill's instructions by name. Call this when the user's request matches an available skill.",
"source": {
"kind": "skill"
},
"category": "other",
"invoked": true
},
{
"name": "wait",
"description": "Wait for a subagent to complete, then use the result to synthesize the outcome for the user.",
"source": {
"kind": "native"
},
"category": "subagent",
"invoked": true
},
{
"name": "web_fetch",
"description": "Fetch content from a URL that starts with http:// or https://. Pass a prompt to extract specific information or summarize the page; omit prompt to return the page content.",
"source": {
"kind": "native"
},
"category": "other",
"invoked": false
},
{
"name": "web_search",
"description": "Search the web using Brave Search when current external information is needed. Returns result titles, URLs, and descriptions; use web_fetch for a specific URL.",
"source": {
"kind": "native"
},
"category": "other",
"invoked": false
},
{
"name": "write_file",
"description": "Create new files, or overwrite an existing file only when replacement is explicitly intended. Prefer edit_file for targeted changes to existing files because write_file overwrites the full file content.",
"source": {
"kind": "native"
},
"category": "write",
"invoked": false
}
],
"context_window": {
"provider": "openrouter",
"model": "openai/gpt-5.6-sol",
"context_window_tokens": 200000,
"input_tokens": 88650,
"usage_percent": 44.325,
"count_method": "response_usage_scaled_breakdown",
"staleness": "live",
"generated_at": "2026-07-23T20:29:17.100781894Z",
"event_seq": 3835,
"breakdown": [
{
"category": "system_prompt",
"tokens": 1096,
"usage_percent": 0.548
},
{
"category": "tools",
"tokens": 1621,
"usage_percent": 0.8105
},
{
"category": "skills",
"tokens": 217,
"usage_percent": 0.1085
},
{
"category": "memory",
"tokens": 3985,
"usage_percent": 1.9925
},
{
"category": "conversation",
"tokens": 81723,
"usage_percent": 40.8615
},
{
"category": "other",
"tokens": 8,
"usage_percent": 0.004
}
],
"warnings": [
{
"code": "activated_skill_context_counted_as_conversation",
"message": "Activated skill instructions are counted as conversation in this version."
}
]
},
"state": "succeeded"
},
"verify@1": {
"first_event_seq": 3845,
"prompt": null,
"response": null,
"completion": null,
"provider_used": null,
"diff": null,
"script_invocation": {
"script": "git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\\bActorRef\\b|\\bActorKind\\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\\s*==\\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1",
"command": "exec 2>&1\ngit fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\\bActorRef\\b|\\bActorKind\\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\\s*==\\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1",
"language": "shell"
},
"script_timing": null,
"parallel_results": null,
"output": null,
"started_at": "2026-07-23T20:29:21.169084156Z",
"handler": "command",
"usage": {
"input_tokens": 0,
"output_tokens": 0,
"total_tokens": 0,
"reasoning_tokens": 0,
"cache_read_tokens": 0,
"cache_write_tokens": 0
},
"state": "running"
},
"preflight_compile@1": {
"first_event_seq": 31,
"prompt": null,
"response": null,
"completion": {
"outcome": "succeeded",
"notes": "Script completed: cargo check -q --workspace 2>&1",
"failure_reason": null,
"timestamp": "2026-07-23T16:50:12.994571961Z"
},
"provider_used": null,
"diff": null,
"script_invocation": {
"script": "cargo check -q --workspace 2>&1",
"command": "exec 2>&1\ncargo check -q --workspace 2>&1",
"language": "shell"
},
"script_timing": {
"output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
"exit_code": 0,
"duration_ms": 140243,
"termination": "exited",
"output_bytes": 0,
"live_streaming": false
},
"parallel_results": null,
"output": null,
"output_bytes": 0,
"live_streaming": false,
"termination": "exited",
"started_at": "2026-07-23T16:47:52.748027473Z",
"handler": "command",
"timing": {
"wall_time_ms": 140246,
"inference_time_ms": 0,
"tool_time_ms": 140243,
"active_time_ms": 140243
},
"usage": {
"input_tokens": 0,
"output_tokens": 0,
"total_tokens": 0,
"reasoning_tokens": 0,
"cache_read_tokens": 0,
"cache_write_tokens": 0
},
"state": "succeeded"
},
"start@1": {
"first_event_seq": 17,
"prompt": null,
"response": null,
"completion": {
"outcome": "succeeded",
"notes": null,
"failure_reason": null,
"timestamp": "2026-07-23T16:47:47.854929172Z"
},
"provider_used": null,
"diff": null,
"script_invocation": null,
"script_timing": null,
"parallel_results": null,
"output": null,
"started_at": "2026-07-23T16:47:47.854782618Z",
"handler": "start",
"timing": {
"wall_time_ms": 0,
"inference_time_ms": 0,
"tool_time_ms": 0,
"active_time_ms": 0
},
"usage": {
"input_tokens": 0,
"output_tokens": 0,
"total_tokens": 0,
"reasoning_tokens": 0,
"cache_read_tokens": 0,
"cache_write_tokens": 0
},
"state": "succeeded"
}
}
}