Commit Graph
100 Commits
Author SHA1 Message Date
Dark-Alex-17 0defc61bec test(TASK-035): run deep compress tests on a big-stack thread
The .compress-session success test polls through the whole summarization
call chain; those nested poll frames overflow the default test-thread
stack on Windows debug builds even with the outer future boxed. Drive
all four deep-compress tests through the same big-stack run_async
harness the macro tests already use.
2026-09-16 13:44:35 -06:00
Dark-Alex-17 7155b1b4fb docs(TASK-040): add hooks block to config.example.yaml 2026-09-16 13:44:35 -06:00
Dark-Alex-17 42380d4b82 feat(TASK-039): wire hooks into bundle installs and reconcile builtin hook dirs
Bundles: RemoteLayout.hooks + scan/is_empty/plan routing into hooks_dir,
a functional --filter hooks arm replacing the inert interim arm,
TopCategory::Hooks provenance rows, and per-file 'bundle defines hook
script' summary lines (visibility only, no approval prompt).

Reconciliation: new builtin_manifest module — a hidden per-directory
.builtin-manifest records the files the builtin installer actually owns
(written this run, previously owned, or pending failed deletions);
deletion is strictly manifest-minus-embed so user-created files in the
same directory are never touched. Wired into install_builtin_agents for
agents/<name>/hooks/ and a greenfield minimal Role::install_builtin_role_hooks
for roles/hooks/.

Hardening: bundles are barred from shipping any path component that
aliases .builtin-manifest (Unicode case fold + trailing dot/space,
directories included); manifest entries reuse the bundle store's
is_safe_component rules; malformed manifests fail safe toward keeping
files; role-side reconcile failures degrade to a warning like the
agent side.
2026-09-16 13:44:35 -06:00
Dark-Alex-17 e62502c862 test(TASK-038): adopt hook usage-probe regression script
Black-box scenarios against a built binary with dry_run configs: hook
cadence + env scoping, runtime inertness of failing hooks, --info firing
zero agent.* events, --install-builtins hooks install/force/cold-start
semantics, and end-to-end firing of the installed log-events.sh via the
documented relative-path wiring.
2026-09-16 13:44:35 -06:00
Dark-Alex-17 fc3bb259cb feat(TASK-038): add hooks builtin-asset category with example scripts and roles mixin scanning
- AssetCategory::Hooks + InstallFilter::Hooks (NAMES/parse/clap), reserving
  the 'hooks' bundle name via the existing AssetCategory::parse check
- HookAssets embed + install_builtin_hooks(force): installs the bundled
  example scripts 0755 into paths::hooks_dir() via write_file_atomic, wired
  into install_builtins() and install_assets(); warns once when
  COYOTE_HOOKS_DIR diverges from the config-dir default
- assets/hooks/notify.sh: notify-send -> osascript -> terminal fallback,
  with AppleScript-literal sanitization of model-influenced values
- assets/hooks/log-events.sh: appends a COYOTE_* env snapshot (secrets
  excluded, 0600 log, symlink-refusing) to ${COYOTE_HOOK_LOG:-/tmp/coyote-hooks.log}
- sbx-mixin discovery now scans roles/*.sbx-mixin.yaml and
  roles/hooks/*.sbx-mixin.yaml (mirroring the role-hook cwd)
- tests: install+0755+force, install_assets(Hooks) end-to-end, discover()
  roles scanning, reserved bundle name, no-ARGC-BUILD embed content, script
  content order + real execution (secret exclusion, tty-less fallback),
  CLI parser round-trips; hooks_dir env test made #[serial]
2026-09-16 13:44:35 -06:00
Dark-Alex-17 0c6b7c8594 fix(TASK-035): close agent-bracket leaks and prove compression/role-hook seams
- Open the top-level agent bracket only at committed dispatch sites so
  inspection-flag early returns (--list-sessions, --info, --macro,
  --rebuild-rag, --init-*, --build-tools) never leak agent.started
- Close the agent bracket ahead of process::exit in shell_execute's
  execute branch, where the dispatch wrapper's close is unreachable
- Extract the ask-loop auto-compression seam and cover the
  session.compressed success path at both call sites with sink tests
- Prove Session.role_hooks never serializes and that injected
  role_hooks YAML is ignored on deserialization
2026-09-16 13:44:35 -06:00
Dark-Alex-17 8710737964 feat(TASK-035): wire turn/session/compression seams + top-level agent bracket
Turn bracket (repl::TurnBracket): turn.started on entry and exactly one of
turn.{completed,interrupted,failed} on exit, with the interrupted-vs-failed
discrimination (abort_signal.aborted_ctrlc()) in one place. Wired at the
three outer boundaries: ask(), start_directive, and shell_execute — never at
the per-LLM-round before/after_chat_completion seams.

Session seams: session.started fires for newly created sessions only
(use_session's new_from_ctx paths; resume fires nothing), session.ended in
exit_session while the session is still attached, session.compressed on the
success path of both compress_session call sites (auto-compression and the
.compress command).

Session-held role hooks: Session snapshots the role's HooksMap on set_role,
clears it on clear_role/sync_agent, and re-reads it from the role file on
load; resolved_hooks gains a session leg so role hooks keep firing when the
role lives in the session (fresh set_role and resumed sessions alike).

Top-level agent bracket: agent.started/completed/failed once per agent RUN —
headless --agent dispatch and the interactive agent-context lifetime (enter
via .agent or startup; exit via switch-away or REPL teardown), never per
message. COYOTE_AGENT_ID is synthesized as top-<agent-name>-<nonce>.

Exit-path seams call hooks::drain_pending with a 2s bound so exit hooks
finish spawning before process teardown; interactive mid-session events
never drain.
2026-09-16 13:44:35 -06:00
Dark-Alex-17 363104fe77 fix(TASK-036): reuse hooks::base_envs_parts in job terminal hook firing 2026-09-16 13:44:35 -06:00
Dark-Alex-17 8f775dc2d9 fix(TASK-036): pin exact user__-prefixed COYOTE_TOOL_NAME on tool hook events 2026-09-16 13:44:35 -06:00
Dark-Alex-17 2731e98ed1 feat(TASK-036): fire tool, LLM, and job hook events at their seams
Wire the hook engine into the three runtime seams:

- tool.*: ToolCall::eval fires tool.started once the name and arguments
  are resolved, then exactly one of tool.completed/tool.failed,
  discriminated on both the Err path and tool_call_error-shaped outputs.
  The arguments JSON is handed to the engine as the payload; eval itself
  performs no payload-file I/O.
- llm.request.*: the three transport free functions
  (call_chat_completions, call_chat_completions_streaming, and the quiet
  graph-node variant) fire started/completed/failed for every provider
  and caller, classifying failures as aborted, api_error (with
  COYOTE_LLM_STATUS), or other.
- job.*: handle_job_tool fires job.started before either spawn and the
  terminal event inside the detached closures via a pre-resolved
  fire_resolved snapshot, so job__start's return is never delayed.

The test sink now captures the payload and cwd of each dispatch
(sink-only change), and seam tests pin event cadence, env contracts, and
byte-identical tool results and errors.
2026-09-16 13:44:35 -06:00
Dark-Alex-17 0c2a68ecc4 fix(TASK-037): cover genuine node failure payload and malformed graph.yaml hook degradation
Two test gaps: no test fired a genuine graph.node.failed and inspected
its payload (the only node-failure test asserted the hook's absence
because a fallback absorbed the error), and no test drove a malformed
hook definition through the new graph seams. The first test fails an
agent node with no fallback and asserts the captured envs carry the
node id, node type, a numeric duration, and a non-empty error while
the failure still propagates out of the executor. The second routes a
blank-command hook from graph.yaml's top level through the executor
seam and proves it degrades safely: the node completes, the malformed
hook is skipped with a debug log, and a well-formed sibling on the
same event still fires.
2026-09-16 13:44:35 -06:00
Dark-Alex-17 bd7cad21e2 feat(TASK-037): wire supervisor-agent and graph hook seams
Fire agent.started before the supervisor spawn and agent.completed/failed
from the spawn task's result arms, resolved against the child context so
global hooks stay gated by the child's own whitelist. Graph AgentNode
failures fire agent.failed at the point of failure, before a declared
fallback converts them into a successful route. The graph executor fires
graph.node.started before each branch spawn and graph.node.completed/failed
beside the narrate calls, with per-branch pre-resolved snapshots and
metadata so parallel super-step branches never cross-contaminate. Base
envs for detached dispatches are rebuilt at fire time via a shared
hook_base_envs helper.
2026-09-16 13:44:34 -06:00
Dark-Alex-17 0c4d0d777c feat(TASK-034): hook dispatch engine in src/hooks.rs
- HookEvent (22 dotted variants) + owned ResolvedHook snapshots
- RequestContext::resolved_hooks: global (gated by agent global_hooks)
  -> role (additive) -> agent precedence; per-scope cwds; unknown
  whitelist entries inert + debug-logged with agent name
- fire/fire_resolved: fire-and-forget detached dispatch via platform
  shell (sh -c / cmd /C raw_arg), null stdio, fresh unix process group,
  reap-only await, no-runtime guard (skip + debug, never panic)
- tool.* payload files: crypto-random nonce, create_new + 0600, verbatim
  JSON, best-effort delete after reap, hook still runs on write failure
- base env (COYOTE_EVENT/HOOK_NAME/CONFIG_DIR/EVENT_TIMESTAMP RFC3339
  UTC, SESSION_ID/AGENT_NAME when present) + per-event extras; every
  value truncated engine-side to 2048 bytes (UTF-8-safe; payload path
  documented exempt)
- drain_pending(timeout): opt-in exit-path seam with spawn-ack
  semantics (Drop-guard counter; never waits for child completion)
- #[cfg(test)] synchronous capture sink (records instead of spawning)
  + hooks-scoped debug capture in src/testing.rs (poison-proof,
  boundary-aware target prefix)
- Agent::hooks()/global_hooks() accessors; Role::hooks() dead_code
  marker removed

Engine compiles unused by design; call sites land in TASK-035/036/037.
2026-09-16 13:44:34 -06:00
Dark-Alex-17 b020e1b75e fix(TASK-033): cover hooks env overrides with tests and debug-log malformed hook config 2026-09-16 13:44:34 -06:00
Dark-Alex-17 9693cdd4ac feat(TASK-033): add hook config schema across all scopes
Introduce src/hooks.rs as a types-only module (HookDef, HooksMap) and
plumb hooks:/global_hooks: config fields through every scope:

- Config + AppConfig hooks field, both Default impls, the manual
  from_config copy, and a COYOTE_HOOKS env override
- Role frontmatter: hand-parsed hooks arm, private field, getter, and
  JSON-inline re-serialization in Role::export (malformed maps degrade
  to None)
- AgentConfig + Graph: hooks + global_hooks on both structs, copied in
  AgentConfig::from_graph, with a per-agent global_hooks env override
- paths::hooks_dir() = <config_dir>/hooks with COYOTE_HOOKS_DIR override
- Commented hooks: block in the global config template (global scope
  only; no global_hooks key)

No engine or dispatch logic yet; existing configs parse unchanged.
2026-09-16 13:44:34 -06:00
Dark-Alex-17 e4dfbe906c feat: updated sisyphus and architect agents so that when reviewing a PR, they add review round summaries as new comments rather than updating the PR description 2026-09-16 11:36:34 -06:00
Dark-Alex-17 fa7f96e9e8 Merge remote-tracking branch 'refs/remotes/origin/main' 2026-09-16 10:18:21 -06:00
Dark-Alex-17 5046aebca3 feat: support config-driven OAuth endpoints on the openai and gemini clients 2026-09-16 10:17:05 -06:00
Dark-Alex-17 9fb90be851 fix: added additional constraints around parallelization bugs in verification tasks 2026-09-16 00:36:33 -06:00
Dark-Alex-17 4cc5563ba8 feat: support config-driven OAuth endpoints on the claude client
- ClaudeConfig gains an oauth: block (custom endpoints, e.g. a private
  gateway); without it, auth: oauth keeps the stock Claude Pro/Max provider
- Claude-Code system-prefix spoof + anthropic-beta header are only sent on
  the stock provider path
- OAuth tokens are stamped with an issuer fingerprint (token_url|client_id)
  so switching endpoints never replays a token against the wrong issuer
- All auth flows refresh the in-memory access-token cache on save, so
  .authenticate mid-session takes effect immediately
- A custom oauth: block now requires api_base: refusing to silently fall
  back to api.anthropic.com prevents sending a gateway token to the wrong
  host
2026-09-15 23:51:51 -06:00
Dark-Alex-17 fcc0049ab2 feat: further increased budgets for adversary and code reviewer agents 2026-09-15 23:09:59 -06:00
Dark-Alex-17 dca9b1553e test: bumped the adversary's holistic max iterations from 20 to 30 in the tests 2026-09-15 21:41:13 -06:00
Dark-Alex-17 ea31236d29 Merge branch 'main' of github.com:Dark-Alex-17/coyote 2026-09-15 20:06:16 -06:00
Dark-Alex-17 d16db663df feat: increased resource budgets for review suite 2026-09-15 20:06:07 -06:00
Dark-Alex-17 6c50c60c22 feat: improve the code review suite to support larger PRs 2026-09-15 15:14:25 -06:00
Dark-Alex-17 f31885ba07 test: fixed tests for the new finding-verifier agent changes 2026-09-15 15:04:48 -06:00
Dark-Alex-17 fd26f43b84 feat: Improved the code-reviewer suite so it doesn't need to loop as frequently for failed reviews based on the finding-verifier (which may need many round trips to verify large PRs) 2026-09-15 14:50:39 -06:00
Dark-Alex-17 e240be8bda feat: improved the adversary agent to look for facts and not criteria. 2026-09-15 12:02:16 -06:00
Dark-Alex-17 9e2f3c0f12 feat: auto continuation is unavailable in llm nodes; continuation driven loops should be in agent nodes only
CI / All (ubuntu-latest) (push) Failing after 28s
CI / All (macos-latest) (push) Canceled after 0s
CI / All (windows-latest) (push) Canceled after 0s
2026-09-15 11:42:39 -06:00
Dark-Alex-17 8746e3e29a Merge branch 'main' of github.com:Dark-Alex-17/coyote 2026-09-15 11:14:24 -06:00
Dark-Alex-17 5b9852d80e fix: Require injection of user interaction functions for LLM nodes to be explicit 2026-09-15 11:13:12 -06:00
Dark-Alex-17 cfb83a629e feat: create a config alias for default_reasoning_level (to match some wording and make the config flexible)
CI / All (ubuntu-latest) (push) Failing after 30s
CI / All (macos-latest) (push) Canceled after 0s
CI / All (windows-latest) (push) Canceled after 0s
2026-09-14 17:47:01 -06:00
Dark-Alex-17 8377f8287b feat: support custom resource definitions for openai-compatible Oauth customization 2026-09-14 17:46:44 -06:00
Dark-Alex-17 ffb75cb168 feat: Support specifying the compression model to use when compressing a session for greater cost controls 2026-09-14 15:49:44 -06:00
Dark-Alex-17 58b0dbd812 feat: Added full cost tracking to Coyote and added it to the default right prompt 2026-09-14 15:24:48 -06:00
Dark-Alex-17 bd5c0a7cdd chore: updated the models.yaml to incorporate cached read/write prices pulled from models.dev and openrouter 2026-09-14 14:47:22 -06:00
Dark-Alex-17 d84dea21b6 feat: added support for input/output token cache cost tracking 2026-09-14 14:47:01 -06:00
Dark-Alex-17 755b25f9b2 fix: empty tool results can't cause errors in bedrock, claude, openai, or vertexai clients anymore 2026-09-14 14:46:46 -06:00
Dark-Alex-17 23f941b09f fix: proper support for reasoning/thinking in bedrock models 2026-09-14 14:46:15 -06:00
Dark-Alex-17 77da48a40f feat: Support persisting paused todo lists to sessions so users can exit and resume paused todo sessions; Also fixed a bug so todo list pausing is only supported when being used in a session since it typically requires multi-turn communication 2026-09-14 13:57:08 -06:00
Dark-Alex-17 5d59adadbc feat: Support token caching to reduce LLM costs in Anthropic models (not typically handled automatically by APIs) 2026-09-14 13:56:13 -06:00
Dark-Alex-17 beca133082 style: removed unneccessary path prefixes and organized imports 2026-09-14 11:13:34 -06:00
Dark-Alex-17 c7ec4ed124 docs: restored formatting to agent example config file 2026-09-14 11:13:34 -06:00
Dark-Alex-17 cee5cf0f36 test(rag): make TempDir names unique across parallel tests
SystemTime nanos alone collide on coarse clocks, so concurrent tests shared
a scratch dir and read each other's generated sbx mixins (flaky
generated_sbx_mixin_* failures in CI).
2026-09-14 11:13:34 -06:00
Dark-Alex-17 85cb7b0fbf test: normalize CRLF in the gauntlet README contract pin (Windows CI) 2026-09-14 11:13:34 -06:00
Dark-Alex-17 d89592fee6 test: normalize CRLF in the gauntlet script/README source pins (Windows CI) 2026-09-14 11:13:34 -06:00
Dark-Alex-17 342f39f30e ci: run pull_request checks for PRs based on any branch
'*' does not match '/' in GitHub branch filters, so a PR whose base is a
feature branch (e.g. a stacked PR) never triggered CI.
2026-09-14 11:13:34 -06:00
Dark-Alex-17 4f0c7e4431 test(TASK-030): pin fallback-note placement; detect concrete task ids (with illustrative-example allowlist) instead of run-local literals 2026-09-14 11:13:34 -06:00
Dark-Alex-17 fcf67b07a7 test(TASK-030): pin placement, ladder clause, findings-first rule, README sync; reconcile gauntlet README caller guidance 2026-09-14 11:13:34 -06:00
Dark-Alex-17 78e326039d feat(TASK-030): callers escalate on GAUNTLET_REVIEW_INCOMPLETE instead of findings-routing 2026-09-14 11:13:34 -06:00
Dark-Alex-17 f18c436e2c fix(TASK-032): enforce incomplete⇒BLOCKED structurally; anchor adversary degraded detection 2026-09-14 11:13:34 -06:00
Dark-Alex-17 786e5542d4 feat(TASK-032): review-gauntlet incomplete-review machine line + ×N attempt details
verdict_gate.py now names every incomplete review on a machine line the
done template renders immediately after the verdict:
`GAUNTLET_REVIEW_INCOMPLETE: <name>[, <name>]` (sorted). Names are the
lanes retry_gate gave up on (`review_incomplete`) plus the `pipeline`
pseudo-lane whenever `signals_error` carries a `PIPELINE-FAULT:` —
parse death, builder / retry-gate / default-lanes crash guards — and the
gate's own crash guard emits the `pipeline` line too. The report gains a
`## Review incomplete` section (after the signals_error blockquote,
immediately before `## Full lane reports`) and the three PIPELINE-FAULT
lane rows carry ` ×N` when the lane was re-run (N = 1 texts unchanged).

retry_gate.py stands in a synthetic `PIPELINE-FAULT: <lane> lane produced
no report` marker for a lane that ran but collected nothing, so it is
stashed, classified FAULTED and never renders SKIPPED downstream.
MAX_RETRY_ELAPSED_SECS trimmed 45600 → 45000 for margin slack.

graph.yaml: `gauntlet_incomplete_line` seeded in initial_state and
spliced into the done output. README + mermaid document retry_gate, the
dotted _next back-edge, the 3-attempt / wall-clock budget, that retry
never narrows selection, the crash-guard append semantics, and the
machine-line contract (present whenever any lane or the pipeline was
incomplete; it can accompany real findings).

Tests: ×N on all three branches (N>1, N=1 byte-identical, no leak onto
non-fault rows), exact incomplete line / section placement, pipeline
pseudo-lane alone and alongside real lanes (via default_lanes.py's real
crash guard), mixed finding+fault, crash-guard chaining from
build_items / retry_gate / parse_fault / verdict_gate, synthetic
no-report marker, done-template wiring, README pins (positive + stale
negative).
2026-09-14 11:13:34 -06:00
Dark-Alex-17 18f5ada585 feat(TASK-029): review-gauntlet fault-only lane re-run loop
Give the review gauntlet a deterministic retry loop for lanes that FAULT
(PIPELINE-FAULT marker, missing verdict sentinel, or a nested graph's own
degraded-run wording) without ever re-running a lane that reached a real
verdict.

- graph.yaml: the four lane maps route to a new `retry_gate` script node
  (static `next: verdict_gate`; emits `_next: build_items` to retry).
  initial_state gains lane_attempts / kept_results / retry_lanes /
  review_incomplete / gauntlet_started_at. settings.timeout 24000 -> 69600
  (3 passes x 2 attempts x 11400 + 1200 margin).
- scripts/retry_gate.py (new): stashes every lane report that ran
  (completed and faulted alike) and restores reports a re-run pass
  overwrote with []; classifies with verdict_gate.py's sentinel regexes
  verbatim; counts attempts; retries while attempts < 3 and inside a
  45600 s wall-clock budget; otherwise records the lane in
  review_incomplete and falls through. Crash guard appends to
  signals_error and never retries.
- scripts/signals.py: stamps gauntlet_started_at before the git work so a
  failed diff still starts the clock.
- scripts/build_items.py: retry pass emits items only for retry_lanes
  (with attempt numbers), adds a `retried:` note to lanes_summary, resets
  retry_lanes; falsy-guarded stamp fallback; crash guard appends to
  signals_error and preserves lanes_summary.
- Tests: hermetic python3-guarded script tests for retry_gate /
  build_items / signals, graph wiring + initial_state pins, sentinel and
  LANE_KEYS cross-pins, retry-budget envelope vs settings.timeout, a
  first-pass golden payload, and an engine-level executor test proving a
  script `_next` back-edge re-fires a static fan-out and an empty map
  re-run clears its collect_into.

Plan: PLAN-review-retry section A items 1-3 and 5. Verdict/report surface
(section A items 4 and 6) is TASK-032.
2026-09-14 11:13:34 -06:00
Dark-Alex-17 b90e04f227 fix(TASK-028): pin example-doc wording; make not-found escalation hint match queue semantics 2026-09-14 11:13:34 -06:00
Dark-Alex-17 3339920377 fix(TASK-028): pin global-template exclusion, tighten timeout boundary test, reword stale hint 2026-09-14 11:13:34 -06:00
Dark-Alex-17 9a505f951a feat(TASK-028): escalation_timeout 0 waits indefinitely and is the default
Child escalations (coder plan approvals, deviation gates, user__* calls
from subagents) previously expired after the escalating agent's
escalation_timeout, defaulting to 300 s, and 0 timed out instantly. In
autonomous runs the orchestrator often could not reply within that
window, so approvals silently fell through to the best-judgment
fallback.

- Extract await_escalation_reply as the wait seam: 0 awaits the oneshot
  with no deadline; nonzero keeps tokio::time::timeout with the same
  timeout message. A dropped sender still resolves to the existing
  cancelled reply either way.
- DEFAULT_ESCALATION_TIMEOUT_SECS and default_escalation_timeout() -> 0,
  so graph agents (which inherit the default via from_graph) are covered.
- Document the new semantics in config.agent.example.yaml and drop the
  5-minute-timeout claim from the default spawn instructions.
- Tests: paused-clock unlimited wait (3600 s), nonzero expiry message,
  dropped-sender completion, default pin, and a grep pin that the stale
  timeout wording is gone from src/, assets/, and the example config.

Plan: PLAN-review-retry §C (D3).
2026-09-14 11:13:34 -06:00
Dark-Alex-17 b87367bb1b style: Refactored some imports and LLM hints 2026-09-14 10:50:34 -06:00
Dark-Alex-17 ad3a5c6d49 test: normalize CRLF in the run_checks docstring pin (Windows CI) 2026-09-14 10:50:34 -06:00
Dark-Alex-17 68e893d684 fix(adversary): red verification runs block CONFORMS; re-derive timeout envelope; robust runner
verdict.py: a recorded verification run with exit≠0 (including the -1
TIMEOUT sentinel) or a skipped record now forces DIVERGES with a numbered
complaint naming the command — previously a [FAIL] line sat in the report
appendix while the sentinel read CONFORMS, and callers route on the
sentinel alone. Fail-closed by the same rule: an ENVIRONMENT runner-error
marker when commands WERE declared is one red complaint (the declared
commands never ran, so the run is unproven); the "none declared" marker
and an empty declaration stay non-red. Headers name the red-run count.

run_checks.py: decode command output with encoding=utf-8/errors=replace
(a single non-UTF-8 byte used to raise inside communicate() and collapse
the whole evidence record to an ENVIRONMENT marker); bound the post-kill
pipe drain by DRAIN_TIMEOUT_SECS so a session-escaped grandchild holding
the pipe cannot turn a TIMEOUT record into a node kill.

graph.yaml: settings.timeout 5400 → 9000 = 3600 (run_checks node timeout)
+ 5400 (the previous whole-graph budget, kept for every other stage); the
old value predated run_checks and a graph-level timeout kills from
outside, losing the DIVERGES sentinel. Descriptions and README doctrine
updated (CONFORMS = all MET + zero holistic complaints + zero pipeline
faults + zero red runs).

Tests: envelope pin (settings >= run_checks + 5400), gauntlet lane
timeouts > child graph timeouts and gauntlet >= max_attempts × lane +
margin over every agent lane, non-UTF-8 output, bounded drain (unix),
red/skipped/timeout/none-declared/ENVIRONMENT×declaration matrix,
numbering continuity, header punctuation, wording pins on graph.yaml,
run_checks.py and both READMEs. The deadline test's hung command is now
python3-based instead of GNU sleep (cmd.exe on the Windows CI leg).
2026-09-14 10:50:34 -06:00
Dark-Alex-17 156f3193a9 fix(review-suite): detect nested finding-verifier faults; harden render/fault markers
code-reviewer verdict.py: finding-verifier now completes "successfully"
with fault markers INSIDE its FINDING_VERIFIER_RESULTS payload
(parse_fault's `pipeline-fault` sentinel id, verify_fault / verdict_gate
crash-guard notes prefixed PIPELINE-FAULT:), which the old
"Agent node failed:" prefix check never saw — a dead parse degraded
silently to "unverified" with no fault recorded. Detect them, anchored on
the sentinel id / note prefix only (never a substring over the report),
counted over the raw entry list so duplicate crash-guard ids are not
collapsed. Faults lead the attention list uncapped; the display cap of 5
applies to non-fault entries; the "always-human trigger(s) fired" reason
fires only when non-fault triggers exist.

code-reviewer render.py: CONTRACT comments at the verdict and summary
lines review-gauntlet's gate parses; every LLM-influenced value rendered
after the summary line (dropped_titles, rigor, bar_provenance,
resolved_surfaces, reason) is newline-sanitised so none can forge a
second summary line.

finding-verifier verdict_gate.py: the outer crash-guard note carries the
PIPELINE-FAULT: prefix (the after-retry UNVERIFIABLE path stays
unprefixed — model misbehaviour, not infrastructure); README names all
three emitters.

deep-research: vet_fault's UNVETTED note tells the critic not to request
revision solely because the assessment is unavailable (it matched
critique's PREPRINT/UNVERIFIED REVISE trigger verbatim, burning the
revision budget on an infra fault); vet_fault and critique_fault dedupe
repeated faults and preserve accumulated faults on crash via
safe_faults(); question_fault keeps its plan-pinned prefix and appends the
lane's question identity so distinct dead lanes no longer collapse in
combine_findings' dedupe.
2026-09-14 10:50:34 -06:00
Dark-Alex-17 4555af8299 fix(review-gauntlet): summary-line critical count, nested-fault hook, fail-closed lane fallback
verdict_gate.py: the code-review critical count is read from render.py's
deterministic summary line (full-shape regex, LAST match wins — a 🔴
quoted inside a finding body or Changes-table row is not a finding); the
raw 🔴 count remains the fail-closed fallback when the line is absent. A
code-review lane whose own rendered verdict line records a pipeline
fault (degraded nested run) now BLOCKS instead of passing as GREEN
(attention), and fault + reds append both blockers. A non-string lane
result item is BLOCKED as malformed, never routed to GREEN.

default_lanes.py: the crash handler now emits the WIDEST safe selection
(code-review ∪ adversary ∪ security) and records a PIPELINE-FAULT in
signals_error so the gate blocks — previously it repaired malformed state
into a NARROWER passable selection with no fault. Probe is added only
when consumer_surface AND a probe_context (local-run recipe) are present,
mirroring build_items' guard, so degraded selection cannot spawn an
INCONCLUSIVE-by-construction probe.

graph.yaml: re-derive the timeout envelope. Agent-node retries each get a
fresh per-attempt budget (agent.rs retry_transient/bounded_attempt) and
the child's "Graph … timed out" matches the transient anchor, so lanes
exceed their child graph's settings.timeout (code-review 11400 > 10800,
adversary 9600 > 9000) and the gauntlet's own timeout covers
max_attempts × lane timeout + margin (2 × 11400 + 1200 = 24000). The
child graphs' internal retry envelopes are deliberately not derived here
(tracked as a follow-up). README/descriptions updated and pinned.

Tests: summary-count/quoted-🔴/fallback/last-match/nested-fault/both-
blockers/malformed-lane, crash-widens-and-records-fault, probe guard,
ALIASES parity with build_items, and e2e render.py → verdict_gate runs.
2026-09-14 10:50:34 -06:00
Dark-Alex-17 ea76331c49 fix(graph): typed ApiStatusError transient classification; warn on bare output_schema
is_transient_error: a typed crate::client::ApiStatusError anywhere in the
chain is now authoritative (positive and negative) — catch_error renders
429s as "… (type: rate_limit_error)" / "… (status: 429)", which none of
the old string anchors matched, so provider rate limits were never
retried, while a 400 whose body mentioned "rate limit" was. Transient
statuses: 429, 500, 502, 503, 504, 529; message text only classifies
untyped errors, and those anchors are now case-insensitive.

validator: warn for an llm/agent node whose output_schema lacks a
non-empty object `properties` map (auto-merge writes nothing since
402d25a) unless the node lifts keys via a non-empty state_updates; the
same absent-or-empty predicate now also gates validate_fallback_capture.
Bundled and graph.example.yaml warning baselines are unchanged.

Tests build the 429 fixture through real catch_error output and a
.context() chain, cover the status table, typed-overrides-text, and all
validator branches.
2026-09-14 10:50:34 -06:00
Dark-Alex-17 84b5a72eac fix(review-suite): mark example verification commands as placeholders; pin variable description and prompt note
Adversary re-review follow-ups to 665fee9/402d25a:

- architect/sisyphus spawn templates: the pinned cargo commands are only an
  EXAMPLE in a language-agnostic orchestrator — a literal follower in a
  non-Rust repo would record 'cargo: command not found' and yield a false
  DIVERGES. Each --variables line now carries a substitution cue (pinned).
- run_checks.py: a non-list prior pipeline_faults value is no longer iterated
  character by character (test case added).
- Pins: both graphs' verification_commands description wording; the
  run_adversary prompt states where the commands come from; the replacement
  sentence sits on one physical line.
2026-09-14 10:50:34 -06:00
Dark-Alex-17 27101909e0 fix(graph): auto-merge only schema-declared keys from llm output
state_updates::apply merged EVERY top-level key of an llm/agent node's JSON
output into graph state whenever the node had an output_schema, and
structured::extract never validated the output against that schema. Any
prompt text able to steer the model — including plan/diff text pasted from a
repo under review — could therefore plant an undeclared key (e.g.
verification_commands) into state and overwrite a caller-declared value
before a downstream script consumed it.

- apply() now takes the node's output_schema and merges only keys present in
  schema.properties; undeclared keys are dropped. No schema, or a schema
  without an object properties map, merges nothing.
- Nodes without output_schema are unchanged (None == the old false path);
  explicit state_updates are unchanged and still see the full output via
  {{output.<key>}}, which remains the author-controlled way to lift a key.
- Callers: llm.rs, agent.rs (x2), rag.rs (None).
- Sweep test bundled_graph_output_schemas_declare_every_key_relied_on: for
  every bundled graph, every consumed state key has a declared writer
  (initial_state, variable, state_updates target, map key, script-emitted key,
  or an output_schema property), and no schema node's field list names a key
  its schema omits — so this restriction cannot silently break a shipped
  graph. It passes with no graph.yaml changes.
- graph.example.yaml documents the declared-keys-only merge; the residual
  disclosed in 665fee9's README/docstring notes is now recorded as closed.
2026-09-14 10:50:34 -06:00
Dark-Alex-17 300c7aeff0 fix(review-suite): pass verification_commands as a declared variable, never LLM-extracted from prose
verification_commands — the shell commands the adversary's run_checks stage
executes — were extracted by the LLM parse node from {{initial_prompt}}, a
flat prompt that also carries untrusted plan/diff text pasted from the repo
under review. A planted '## VERIFICATION' block was indistinguishable from the
caller's declaration: prompt injection -> arbitrary command execution.

- adversary + review-gauntlet graphs declare verification_commands as a graph
  variable (default '[]'); the key leaves initial_state (or_insert seeding
  would otherwise shadow it) and leaves both parse nodes' instructions,
  output_schema.properties and required.
- review-gauntlet forwards it to run_adversary via a structured inputs:
  passthrough (lone {{verification_commands}} template), not lane-prompt prose.
- run_checks.py accepts a JSON list or a JSON-encoded string of one (variables
  arrive as strings); '[]'/[]/missing = none declared; any other shape fails
  closed with a PIPELINE-FAULT (nothing executed) that verdict.py turns into
  DIVERGES complaint #1.
- architect/sisyphus spawn templates and both READMEs document the
  --variables / --agent-variable form and the trust boundary. Known residual
  (llm-node auto-merge of undeclared output keys) is disclosed in the docs.
- Tests: list/JSON-string acceptance, fail-closed no-execution proof,
  prompt-prose injection proof, verdict wiring, asset pins for both graphs and
  orchestrator configs, dispatch-inputs > variable-override > default
  precedence.
2026-09-14 10:50:34 -06:00
Dark-Alex-17 00c3b60c2f feat(graph): stream llm-node completions so node timeout governs long generations
Graph llm nodes (and the structured-output extractor they share with agent
nodes) now obtain the model reply through a quiet streaming transport,
`call_chat_completions_streaming_quiet`, instead of the non-streaming call.
Because chunks arrive as they are generated, reqwest's client-level
`read_timeout` (default 300s) bounds only a genuine stall between chunks and
the node's own `timeout` becomes the effective bound on total generation.
Previously zero bytes arrived until the whole reply finished, so a 300s
read_timeout silently capped every llm node regardless of its `timeout`
(seen live: code-reviewer's `synthesize` node dying on a 61-file review).

The quiet transport renders nothing (silent SseHandler, no spinner), keeps
tool-call handling identical to the non-streaming path (call-loop detection
off), falls back to the non-streaming request for catalog `no_stream` models
(raced against the abort), and never returns partial output as success.

Supporting changes:
- `sse_stream`/`json_stream` keep the typed `reqwest::Error` in the anyhow
  chain so `is_transient_error` still classifies read_timeout stalls as
  transient and `max_attempts` retries them (previously stringified).
- `structured::extract` takes the graph/session AbortSignal and refuses to
  issue an extraction request for an already-aborted graph.
- `run_chat_loop` gains an injectable CompletionRunner seam (owning client
  construction + the before-hook) so the timeout/retry/abort behaviour is
  unit-testable over a paused clock; a fake `impl Client` drives the real
  trait-default streaming for 600s of virtual time; real-socket tests pin
  that read_timeout fires on a stall but not on a slow continuous stream.
2026-09-14 10:50:34 -06:00
Dark-Alex-17 22421a4e74 test(adversary): pin run_checks process-group kill and deadline-vs-timeout ordering
run_checks.py SIGKILLs the timed-out command's whole process group so a
backgrounded grandchild cannot outlive the record; nothing proved it.
Add a unix-only test that backgrounds `sleep 30`, records its pid, and
probes `kill -0` after the 1s deadline fires.

TOTAL_DEADLINE_SECS (run_checks.py) and the run_checks node's `timeout:`
(graph.yaml) live in different files and could drift until the node
executor kills the script before its in-script degradation runs. Parse
the script's default and assert it stays strictly below the node
timeout, without hardcoding either value.
2026-09-14 10:50:34 -06:00
Dark-Alex-17 fa5ee5f4fc test(graphs): lock retry counts, fallback wiring, and check_criterion's read-only tools
Sweep every agent node in the six bundled graphs and assert max_attempts
is exactly 2 (step-runner/implement stays 1) against a pinned node list,
so a dropped `max_attempts:` line cannot silently revert a lane to
one-shot (the serde default is 1). Sweep every llm/agent node for a
declared, existing fallback and pin the specific fault routes in
review-gauntlet, code-reviewer, and adversary.

Pin check_criterion's tool list to fs_read/fs_cat/fs_grep/ast_grep and
its 'judged ONLY against the recorded verification runs' instruction,
so the criterion branch can never grow an execution tool unnoticed.
2026-09-14 10:50:34 -06:00
Dark-Alex-17 ba3a904baf test(graph): prove per-attempt timeout budget and cover extraction-fault text
agent.rs: the retry-after-timeout test ran its second attempt with no
timeout bound, so it passed whether or not the retry owned a fresh
budget. Attempt 2 now runs under the same 1s bounded_attempt and sleeps
900ms under the paused clock; it only succeeds if the deadline is
per-attempt rather than shared with the attempt that already used it.

llm.rs: the `LLM node structured-extraction failed: {e:#}` branch had no
test because `run` cannot succeed without a model. Split the post-run
half of execute into `finish(node, state, ctx, result)` (byte-identical
body, mirrors agent.rs's outcome_from seam) and add tests feeding
Ok("not json") through it, with and without a fallback, asserting the
captured text prefix and the extractor's full context chain.
2026-09-14 10:50:34 -06:00
Dark-Alex-17 8e42a59b98 docs(adversary): fix mermaid node label and criterion_fault join edge
Node B is the graph's `facts` node (script diff_facts.py), not a node
named diff_facts. criterion_fault has no `next`; it reaches holistic
through the map join, so draw it dashed like the other branch-terminal
edges instead of as a solid successor edge.
2026-09-14 10:50:34 -06:00
Dark-Alex-17 081fa2c9d4 test(suite): replace plan-label references with self-contained wording
Rust test comments/assert messages and deep-research script docstrings
cited numbered requirement labels (R3/R5) and task labels (TASK-016,
TASK-021) that are defined only in an external plan document. Reword
each to say what it means on its own: fail-safe crash guards, the
validator's fallback-capture warning, task-queue anchors.
2026-09-14 10:50:34 -06:00
Dark-Alex-17 48753eaa46 fix(agents): scrub GRAPH_STATE* from adversary verification commands; raise review-suite max_agent_depth
FIX A: run_checks.py passed no env= to Popen, so verification commands
inherited the live GRAPH_STATE/GRAPH_STATE_FILE. Every suite script's
load_state() prefers GRAPH_STATE_FILE, so when the adversary reviews a repo
whose own tests exec these scripts (this repo's R5 tests) the leaked file
silently overrode the test state -> false failures. Popen now gets an env
with all GRAPH_STATE* keys removed. The five sibling script-test helpers
(review-gauntlet, code-reviewer, finding-verifier, step-runner,
deep-research) gain .env_remove("GRAPH_STATE_FILE") for hermeticity. New
R5 test loads state via GRAPH_STATE_FILE and asserts a shell-neutral
python3 -c probe sees both vars UNSET.

FIX B: the depth check compares the child's absolute depth against the
SPAWNER's own max_agent_depth (root = 0), so under
architect(0)->sisyphus(1)->review-gauntlet(2)->code-reviewer(3) the
code-review lane faulted "Max agent depth exceeded (4/3)". Limits are now
child depth + 1 headroom along the documented chains, with the chain in a
comment next to each key: code-reviewer 3->5, domain-reviewer 2->6,
architecture-reviewer 2->4, step-runner default->4 (explicit),
review-gauntlet stays 4. domain-reviewer's file-reviewer hop is documented
but inert (can_spawn_agents unset) - left unwired by decision; noted in
config and pinned via a DOCUMENTED_BUT_UNWIRED exemption in the new
bundled-config test, which pins every value and asserts per-hop headroom +
reachability along four chains.
2026-09-14 10:50:34 -06:00
Dark-Alex-17 62ecb5b415 test(adversary): pin exact died-evidence prefix on the criterion_fault crash path
The crash-path assertion checked only "PIPELINE-FAULT:"; verdict.py's died() keys on "PIPELINE-FAULT: criterion ", so a regression there would have passed the test yet stopped the crashed marker from rendering as DIED.
2026-09-14 10:50:34 -06:00
Dark-Alex-17 7ada3fb47c fix(adversary): narrow died-marker predicate, degraded banner for died criteria, hermetic script tests 2026-09-14 10:50:34 -06:00
Dark-Alex-17 1cf42fffa2 fix(adversary): fail closed on PIPELINE-FAULT evidence regardless of status; pin bail-chain and validator shape 2026-09-14 10:50:34 -06:00
Dark-Alex-17 66b30bb06a test(code-reviewer): pin synthesize max_attempts and fail-closed wiring
A live code-reviewer run degraded through synthesize's fallback; the
graph already retries once (max_attempts: 2) and falls back to verdict
with synth_failure captured, but nothing pinned that wiring. Add the
wiring test so a future edit cannot silently drop the retry or route.
2026-09-14 10:50:34 -06:00
Dark-Alex-17 3f49db86b2 fix(adversary): fail closed per criterion when check_criterion dies
A live run died at map_criteria: one heavy criterion exhausted
max_iterations (10) with no fallback and sank the whole 17-criterion
review. check_criterion now has a 30-iteration budget, a tool-budget
instruction, and falls back to criterion_fault, which records a
schema-shaped UNMET verdict with a PIPELINE-FAULT evidence marker for
that one criterion; verdict.py renders it as DIED rather than judged.
2026-09-14 10:50:34 -06:00
Dark-Alex-17 7dac4620a8 fix(engine): carry full error chain in llm node fault text
A live code-reviewer run reported "LLM node failed: Failed to call
chat-completions api" with the root cause invisible, because the
state_updates fault text used `{e}` (top context only). Format with
`{e:#}` so fault scripts and warn! logs see the whole anyhow chain.
2026-09-14 10:50:34 -06:00
Dark-Alex-17 5b4462135f style: cargo fmt after doc-comment cleanup 2026-09-14 10:50:34 -06:00
Dark-Alex-17 fdae98adff docs: sync agent READMEs + diagrams with resilience hardening 2026-09-14 10:50:34 -06:00
Dark-Alex-17 f230c28bf4 docs: removed references to individual task files in the code documentation 2026-09-14 10:50:33 -06:00
Dark-Alex-17 13b8eb87a9 fix(TASK-020): skip POSIX bash script tests on Windows 2026-09-14 10:50:33 -06:00
Dark-Alex-17 0ce2654173 feat(TASK-023): deep-research hardening — fail-safe scripts, fault fallbacks, pipeline notes
R3 guards: parse_request (crash → ask_topic), bootstrap_research,
combine_findings (crash → fault-text findings; also lifts branch-local
PIPELINE-FAULT findings into pipeline_faults, deduped), reflexion_gate
(crash fails forward to synthesize), incorporate_feedback (crash still
loops with a generic feedback note), verify_sources (top-level guard +
pipeline_notes fold).

Fallbacks per the plan's naming pins: plan → plan_fault → end_fault
(fail-closed; gains state_updates plan_failure per the TASK-016 rule);
research_one_question → branch-local question_fault (map collects the
fault finding); vet_sources/critique → degrade-visibly markers that
continue to the stage each node's own next pointed to; synthesize →
synth_fault → end_fault. end_fault renders
'DEEP_RESEARCH FAILED — PIPELINE-FAULT: <fault text>'.

Accepted-path visibility: verify_sources folds pipeline_faults into
pipeline_notes ('' when fault-free), appended to end_accepted's output
and the approval question — happy-path output stays byte-identical.
initial_state seeds questions: [] so a dying plan inside the static
fan-out leaves the sibling knowledge_lookup lane mapping over zero
items while end_fault renders.

R5: 19 python3-guarded regression tests + GraphParser wiring test;
bundled warning baseline unchanged.
2026-09-14 10:50:33 -06:00
Dark-Alex-17 c98b6bc9cd test(TASK-021): pin task-queue guidance text in shipped artifacts 2026-09-14 10:50:33 -06:00
Dark-Alex-17 f2d90ed7a6 docs(TASK-021): fix task-queue example IDs + document dispatch/collect/failure semantics
- prompts.rs DEFAULT_SPAWN_INSTRUCTIONS: example IDs task_1/task_2 -> "1"/"2"
  (real numeric-string IDs; old format silently produced dep_error + unblocked
  task), plus task-queue rules: root tasks with agent are never auto-dispatched,
  task_complete is what dispatches dependents, auto-dispatched agents must be
  collected, failed tasks permanently block dependents.
- architect config: optional task-queue mirroring guidance for parallel mode
  (disk task files stay the durable store; queue is in-memory).
- sisyphus config: short note on encoding dependency-shaped bursts as
  agent__task_create chains; todos remain the tracking source of truth.
2026-09-14 10:50:33 -06:00
Dark-Alex-17 22976a79f9 fix(TASK-020): surface step-runner LLM-fault captures where the fallback lands
The three fallback-capture keys (orient_failure, sweep_failure,
handoff_failure) were write-only dead state: captured on LLM-node failure
but interpolated nowhere, so end_failure rendered an empty STEP_FAILED
shell and write_handoff carried no trace a dead sweep.

- new crash-proof script node note_llm_fault distills orient/handoff
  'LLM node...' failure text into fault_note (prefix-anchored; empty when
  neither key holds a fault); orient and write_handoff fallbacks now route
  through it before end_failure
- end_failure renders 'Pipeline fault (if any): {{fault_note}}' (sentinel
  and all existing fields preserved; fault_note defaulted in initial_state)
- write_handoff prompt interpolates {{sweep_failure}} with a
  PIPELINE-FAULT VISIBILITY instruction mirroring the review_report
  treatment (fault -> Deviations entry + step_summary; success/empty
  ignored)
- tests: note_llm_fault fault/clean/crash/truncation paths via the
  GRAPH_STATE harness; wiring test pins the retargeted fallbacks,
  note_llm_fault routing, {{fault_note}} in end_failure, and the
  sweep-fault prompt/instruction anchors; warning baseline unchanged
2026-09-14 10:50:33 -06:00
Dark-Alex-17 4263d684e9 feat(TASK-020): finding-verifier + step-runner fail-closed hardening
finding-verifier: parse gains max_attempts 2 + parse_failure capture +
fallback to a new parse_fault marker that rewrites verdicts to a single
UNVERIFIABLE PIPELINE-FAULT entry and continues to done (sentinel
preserved — the caller's verify lane degrades instead of dying);
verify_one gains a branch-local verify_fault marker normalizing the
engine failure text into a schema-conformant UNVERIFIABLE verdict.

step-runner: implement gains fallback end_failure (no retry — reruns
against a mutated tree are unsafe; STEP_FAILED renders the failure via
coder_result); independent_review gains fallback write_handoff with the
handoff instructions and route_review treating an "Agent node failed:"
review_report as review-did-not-run (fail-visible, guard ahead of the
🔴 grep); orient/edge_case_sweep/write_handoff capture their failure
text via state_updates, shrinking the bundled fallback-capture warning
baseline back to zero.

R5 tests: fault-marker normalization + crash paths (raw non-JSON
GRAPH_STATE), route_review.sh fault/critical/clean routing via bash+jq,
and structural wiring tests pinning fallbacks, sentinels, and
end_failure's initial_state-backed template keys.
2026-09-14 10:50:33 -06:00
Dark-Alex-17 a8496aa50e fix(TASK-019): carry PIPELINE-FAULT prefix through verdict.py's crash guard
render.py's empty-diff stub bypass is anchored on the 'PIPELINE-FAULT:'
prefix in reason/attention, but verdict.py's crash guard emitted
'verdict computation error: …' / 'verdict script error: …' without it —
on an empty diff a crashed verdict gate collapsed into the
'No changes to review.' stub and the fail-closed NEEDS-HUMAN verdict
never reached the final report.

- prefix the crash-guard reason and attention entry with PIPELINE-FAULT:
- strengthen the existing crash test to assert the prefix on both fields
- add R5 chained test: empty diff + crash-inducing state piped
  verdict.py -> render.py must render the fault, never the stub
- add crash-guard symmetry tests for parse_fault.py / aux_fault.py
  (raw non-JSON GRAPH_STATE keeps the fault marker) and a test that a
  fault survives the attention[:5] cap alongside 6 user flags
2026-09-14 10:50:33 -06:00
Dark-Alex-17 4e72c6f5e6 feat(TASK-019): code-reviewer fail-closed review pipeline
Every llm/agent stage that can die persistently now falls back to a
machine-readable PIPELINE-FAULT sentinel instead of killing the graph:

- review_domain -> branch-local domain_fault marker (banner-prefixed
  domain_report the map collects; completeness_gate precedent)
- verify -> verdict (verifier fault renders findings unverified + a
  PIPELINE-FAULT attention line)
- parse -> parse_fault -> render (complete NEEDS-HUMAN verdict_out;
  render's no-changes stub is bypassed for fault verdicts)
- refine_groups -> cover_gate (deterministic-proposal fallback absorbs
  the failure; degradation noted in groups_note)
- aux_lanes -> aux_fault -> synthesize (aux_note fault marker; visible,
  never blocking - aux is enrichment)
- synthesize -> verdict (synth_failure marker; keyed off the failure
  prefix + non-empty diff, never findings==[], so clean zero-finding
  reviews stay MERGE-READY)

verdict.py gains unified fault accounting (R2): any PIPELINE-FAULT in
domain_reports / verifier_output / state fault keys forces NEEDS-HUMAN,
prefix-anchored so a report merely quoting the marker mid-text never
trips it; old 'PIPELINE FAULT' spelling migrated. Faults are prepended
to attention so the [:5] cap can never hide one. Dedicated *_failure
state keys capture engine failure text on every fallback+output_schema
node, keeping the bundled warning baseline at zero.

15 python3-guarded R5 tests: fault paths, quoted-marker precedence,
happy-path byte-compat regression, marker normalization, crash-path
fail-closed (verdict/cover_gate/domain_fault), verifier-fault-on-empty-
diff render, and the no-changes stub regression.
2026-09-14 10:50:33 -06:00
Dark-Alex-17 ebb9e9af62 fix(TASK-018): harden verdict_gate fault routing and widen default_lanes fallback
Adversary finding 1: verdict_gate treated any lane report CONTAINING
'PIPELINE-FAULT:' as a lane fault, so a real review quoting the marker
silently bypassed red-finding counting and sentinel parsing. Lane-fault
detection is now precise — only a report that IS the lane_fault.py
marker (starts with it) routes to the fault rule. Adds the missing
regression tests: reds block despite MERGE-READY, NEEDS-HUMAN without
reds passes with attention, probe INCONCLUSIVE blocks with the
environment note, a gate crash fails closed, and a quoting report is
routed to the red rule, not the fault rule.

Adversary finding 2: default_lanes overwrote caller-forced lanes and
dropped the security lane the deterministic signals guaranteed. The
fallback now unions its defaults with caller-forced lanes (canonicalized
via the same aliases build_items uses) and forces security on
auth/deps/exec signals or hardened posture — selection degrades wider,
never narrower, and stays deterministic via a fixed lane order. Tests
cover forced-lane survival, alias canonicalization, unknown-name
tolerance, each security signal, and determinism.
2026-09-14 10:50:33 -06:00
Dark-Alex-17 80c20afcdd test(TASK-018): cover parse_fault.py fault normalization 2026-09-14 10:50:33 -06:00
Dark-Alex-17 8407729d5a feat(TASK-018): review-gauntlet lane degradation + verification passthrough
- lane_fault.py + fallback on all four run_* lane nodes: an agent-node
  failure is normalized into a PIPELINE-FAULT lane result instead of
  killing the graph
- verdict_gate.py: PIPELINE-FAULT lane results block distinctly from
  SKIPPED and missing-sentinel; a PIPELINE-FAULT in signals_error blocks
  at the gauntlet level
- build_items.py: restructured into main() + fail-closed guard (crash
  emits empty lanes + PIPELINE-FAULT) and folds lanes_degraded into
  lanes_summary
- parse: state_updates/fallback -> parse_fault.py, which records the
  fault in signals_error and jumps to the verdict gate
- select_lanes: state_updates/fallback -> default_lanes.py, a
  deterministic wider-never-narrower lane fallback
- verification_commands threaded through parse into run_adversary's
  prompt (declared-only, never invented)
- run_probe prompt gains the aborted-attempt reconcile line
- 8 gauntlet_* R5 tests in src/config/request_context.rs (script harness
  + graph interpolation)
2026-09-14 10:50:33 -06:00
Dark-Alex-17 1e46743420 fix(TASK-017): bound run_checks runtime and fail-close its node-level death
Adversary conformance-review fixes for the TASK-017 exec-evidence stage:

- run_checks.py: total-runtime deadline (3300s = node timeout minus margin);
  each command gets min(900s, remaining budget), commands past the deadline
  are recorded as {cmd, skipped: deadline} entries so in-script handling
  stays authoritative instead of the node timeout killing the script from
  outside. POSIX hardening: per-command process group (start_new_session)
  + killpg on timeout so hung grandchildren cannot linger; non-POSIX
  limitation documented in the docstring.
- graph.yaml: run_checks gains fallback: pipeline_fault; check_criterion
  treats SKIPPED records as at-best PARTIAL (unproven, never failed).
- pipeline_fault.py: run_checks-stage attribution grounded in real engine
  behavior (script-node fallback writes nothing into state, executor.rs
  script arm) — nonempty diff_text proves facts completed, so the fault is
  the verification runner; emits 'PIPELINE-FAULT: verification runner
  killed'. Empty diff_text still attributes to diff resolution.
- verdict.py: renders skipped records as [SKIPPED] instead of a bogus
  [FAIL] exit-? line; non-degraded verdict semantics untouched.
- tests: outer-except ENVIRONMENT degradation, deadline timeout+skip (env
  seam ADVERSARY_RUN_CHECKS_DEADLINE_SECS), pipeline_fault stage
  attribution (parse / facts / run_checks), check_criterion prompt cites
  {{exec_results}}, verdict SKIPPED rendering; run_adversary_script_env
  helper for env-carrying invocations.
- cargo fmt violations in 3d7dea9 test code fixed (fmt --check exit 0).
2026-09-14 10:50:33 -06:00
Dark-Alex-17 d50059a56f feat(TASK-017): adversary execution evidence + fail-closed routing
- parse extracts caller-declared verification_commands (schema + initial_state);
  never invents commands. parse gains fallback + state_updates capturing the
  failure string (TASK-016 fallback-capture warning stays silent).
- new run_checks script node (facts -> run_checks -> map_criteria, timeout
  3600): runs each declared command in the project dir (900s per-command
  timeout), records exec_results {cmd, exit, duration_s, tail~50}; 'none
  declared' / ENVIRONMENT markers instead of guessing or failing the node.
- check_criterion judges execution criteria against {{exec_results}} only:
  recorded green run = MET citing it, recorded failure = UNMET, markers =
  PARTIAL with an ENVIRONMENT note.
- fail-closed routing (R1/R2): parse/facts fallback -> pipeline_fault marker
  script -> verdict; holistic fallback -> verdict with holistic_failure
  captured. verdict.py: any PIPELINE-FAULT forces DIVERGES with the fault as
  complaint #1, per-criterion results still reported; report gains a
  'Verification runs:' section. Non-degraded semantics unchanged.
- R5 rust tests (python3-guarded, local cmd_available): verdict fault path,
  holistic-failure fault, CONFORMS regression, run_checks markers + recorded
  runs, crit_gate contract regression.
2026-09-14 10:50:33 -06:00
Dark-Alex-17 3dcb9541b1 test(TASK-016): assert fallback warn carries the error root cause 2026-09-14 10:50:33 -06:00
Dark-Alex-17 7dc950a092 test(TASK-016): assert the fallback warn names the node id and fallback target
The adversary conformance review flagged that the fallback warn! at
outcome_from (src/graph/agent.rs:71) was never asserted — only the retry
warn had captured-text coverage. Adds one test installing the shared warn
collector, filtered by the unique node id 'fallback_warn_capture' for
parallel safety, asserting the warn names both the node and the fallback
target and carries the error chain.
2026-09-14 10:50:33 -06:00
Dark-Alex-17 80ace77e44 feat(TASK-016): engine + test fixes from the Phase-1 re-review
- warn-per-retry test via shared test-only WarnCollector (src/testing.rs)
- extraction-placement test: extraction failures are never retried
- is_transient_error: typed reqwest/Elapsed pre-check + anchored 429 matchers
- outcome_from docstring corrected; validator warning for
  fallback+output_schema without state_updates (llm + agent)
- validator error for max_attempts == 0 (llm + agent); runtime backstop kept
- nits: generalized branch-escape error text, Continue payload comment,
  BFS test assignments assert, node_id threaded into AgentNodeExecutor warns
- bundled-graph warning baseline records 3 true-positive step-runner nodes
  (orient, edge_case_sweep, write_handoff) pending state_updates there
2026-09-14 10:50:33 -06:00
Dark-Alex-17 18875a4cc9 feat(TASK-011): traverse agent fallback edges in map peer-discovery BFS 2026-09-14 10:50:33 -06:00
Dark-Alex-17 75cc2a4b9c feat(TASK-010): add max_attempts to read-only agent lanes in bundled graphs 2026-09-14 10:50:33 -06:00
Dark-Alex-17 be0c5462ed feat(TASK-009): add fallback routing to agent nodes
Agent nodes gain an optional fallback target with llm-node parity: on any
failure (retries exhausted, hard error, or structured-extraction failure)
the failure chain is written into state as 'Agent node failed: <chain>'
and the chain routes to the fallback node. Without a fallback the error
propagates unchanged. Validator collects agent fallback edges for
missing-target checks, reachability, and map-branch locality.
2026-09-14 10:50:33 -06:00