From a85e0c8be0bdc205f38116aa07efaf07b861be36 Mon Sep 17 00:00:00 2001 From: thisisjun786 <259586770+thisisjun786@users.noreply.github.com> Date: Tue, 15 Sep 2026 04:53:45 +0900 Subject: [PATCH 1/7] [agent] docs(plan): define evidence-based subagent retirement --- .../260915_subagent_retirement/000_plan.md | 84 +++++++++++++++++++ .../260915_subagent_retirement/010_policy.md | 63 ++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 devlog/_plan/260915_subagent_retirement/000_plan.md create mode 100644 devlog/_plan/260915_subagent_retirement/010_policy.md diff --git a/devlog/_plan/260915_subagent_retirement/000_plan.md b/devlog/_plan/260915_subagent_retirement/000_plan.md new file mode 100644 index 00000000..c976b888 --- /dev/null +++ b/devlog/_plan/260915_subagent_retirement/000_plan.md @@ -0,0 +1,84 @@ +# Require evidence before retiring a subagent + +An executor can still be investigating while its working tree is clean. Issue +[#178](https://github.com/lidge-jun/codexclaw/issues/178) records a worker stopped +seconds after a successful read. This unit replaces the wait-count heuristic with +an activity-based decision procedure in the existing waiting reference. It also +keeps repeated busywork, explicit limits and safe handoff in view. + +## Scope and completion + +- Class C2; one satisfy-spec PABCD work-phase, `wp1`. +- Trigger: implement #178 and open a linked pull request. +- Goal: a coordinator can distinguish slow progress, suspected stagnation, + confirmed failure and unavailable observations before deciding to retire. +- Out: runtime watchdogs, provider retries, new configuration/schema fields, + installed-payload changes, merge, release and deployment. +- Tools: local Git/source reads, native V1 subagents, existing checks and GitHub + branch/PR publication. Writes stay in this task's linked worktree and native + session evidence. No user token/cost/wall-clock budget was specified; preserve + explicit limits when present and do not invent a universal timeout. +- Verifier: the baseline commands below plus independent semantic review of + [the scenario matrix](010_policy.md#acceptance-scenarios). +- Stop: close the cycle with verified text and publish an ordinary PR to `dev`. + DONE requires a linked PR and accurate check results; unmet evidence stays + incomplete. Host limits or cancellation are reported as such, never success. +- Memory artifact: this unit, then its `_fin/` archive; raw consultation/dispatch + handles and receipts remain in untracked native session evidence. +- Escalation: main resolves scope/judgment gaps. A new worker handoff requires a + plan amendment. Retirement and replacement follow managed dispatch results; + `reconcile`/`stop` never authorize direct work or another spawn. + +## Existing owners and baseline + +`plugins/codexclaw/skills/loop/references/waiting.md:31-35` retires after about +three waits. `structure/20_pabcd_dispatch_doctrine.md:181-195` includes bare +timeout among failure reasons. The V1 table in +`plugins/codexclaw/skills/pabcd/references/delegation.md:133` calls a wait timeout +a normal outcome. The waiting reference already owns the shared decision rule; +the other files should point to it rather than copy a second classifier. + +Verified on upstream `dev` at `03541398` before edits: + +| Command | Result | What it observes | +| --- | --- | --- | +| `npm run gate` | exit 0 | `gate.mjs` scans skill references and `structure/*.md` for claim hygiene and checks inventory; not retirement semantics | +| `node plugins/codexclaw/scripts/test.mjs plugins/codexclaw/test/manifest-policy.test.mjs` | 7 pass, 0 fail | existing owner-route resolution includes `waiting.md`; not model behavior | + +No phrase-presence test or unused decision helper will be added. They would test +the wording or a second implementation that the coordinator never executes +(`dev-testing` TEST-PROMPT-SEAM-01). The source is agent-followed E7 guidance; +execution surface: main's judgment; bypass: ignore/misread it; residual: model +variance and missing observations; final enforcement layer: none. + +## Architect consultation and main decisions + +The native V1 architect proposed D1-D5 from the owner files. Its initial proposal +included phrase tests; its revised proposal withdrew them after the existing +testing rule was supplied. Actual handle, proposal and reflection are retained +with this session's dispatch evidence, outside published project documents. + +| Decision | Main disposition | +| --- | --- | +| D1 trigger taxonomy | Accept with amendment: a confirmed stop/cancel is not automatically a provider failure. Separate wait timeout, terminal error and evidenced stagnation. | +| D2 activity evidence | Accept with amendment: new reads or command events are candidate evidence only when they advance the packet. Repeated reads/messages prompt investigation, not an automatic new count threshold. | +| D3 checkpoint | Accept: use supported non-interrupting delivery, explain queued-input limits, request findings/remaining work/next artifact. Reviewers report next review result, not a forced first edit. | +| D4 retirement evidence | Amend: record decision before stop; verify terminal state and owned processes after stop. A returned *previous* `running` status does not prove termination. Preserve managed fallback gates. | +| D5 observation and proof | Accept revised semantic review. No assumed access to child rollouts. Do not link installed skills to repository-only devlogs. | + +No-code alternatives: doing nothing retains the contradiction; merely lengthening +three waits still mistakes elapsed observations for failure; a runtime helper has +no existing consumer and expands this change. Reuse the current protocol owners. + +Reflection of this concrete plan: ALIGNED on D1-D5, with the named main +amendments accepted. Independent A audit: PASS, no blockers. S3 now explicitly +permits retirement only after stagnation is evidenced; the canonical recovery +rule must also govern shorthand references such as `plan-output.md:18`. + +## Change ownership + +The executor owns the four bounded source-document edits listed in +[010_policy.md](010_policy.md). Main owns this plan, semantic acceptance decisions, +consultation records, final review/integration, checks and publication. One commit +may carry the coherent document fix; follow repository `[agent] fix(...)` style. +No branch stacking is needed. Preserve unrelated local integration work. diff --git a/devlog/_plan/260915_subagent_retirement/010_policy.md b/devlog/_plan/260915_subagent_retirement/010_policy.md new file mode 100644 index 00000000..7b796ffd --- /dev/null +++ b/devlog/_plan/260915_subagent_retirement/010_policy.md @@ -0,0 +1,63 @@ +# wp1: Change the retirement decision at its existing owners + +Depends on `000_plan.md` D1-D5. One C2 work-phase; no production code or new types. + +## Exact edit map + +| File | Before | After | Owner | +| --- | --- | --- | --- | +| `plugins/codexclaw/skills/loop/references/waiting.md` | final bullet retires after about three waits | Replace that bullet with compact progress/checkpoint/retirement sections. Keep existing peer boundary and V1/V2 wait visibility. | executor | +| `structure/20_pabcd_dispatch_doctrine.md` | DISPATCH-RETIRE-01 treats bare timeout as failure and implies unconditional replacement/reclaim | Refer to waiting's evidence rule; qualify timeout as actual terminal error only; keep bounded recovery but defer managed replacement/reclaim to protocol result. | executor | +| `plugins/codexclaw/skills/pabcd/references/delegation.md` | wait route and normal-timeout table; generic failure recovery | Clarify non-interrupting checkpoint delivery and timeout observation. Preserve transport schemas and existing first-fallback gates. | executor | +| `CHANGELOG.md` | empty Unreleased | One concise Fixed entry for #178, explicitly agent-followed guidance. | executor | + +The waiting rule must distinguish substantive progress, suspected stagnation, +confirmed failure and unavailable observation. Refresh task-scoped observations +just before retiring. Liveness alone and file timestamps alone do not prove +semantic progress; no edits alone do not prove a stall. A healthy long command +may have sparse output and must be inspected before cancellation. + +For suspected stagnation, request one non-interrupting checkpoint where supported, +then compare new evidence with the prior observation at a stated, task-appropriate +next review point. Repeated/no-op work must not reset that review point forever. +That review point is not a new cancellation budget. An unread queued message and +silence with missing visibility do not confirm a stall. If an opaque task cannot +be assessed within authorized limits, report the observation gap and seek direction +without claiming failure or silently replacing it. + +Explicit cancellation, actual terminal failures and stated user/host resource +limits take precedence over progress. Keep cancellations/exhausted limits separate +from provider errors. Record pre-stop reason and last meaningful activity, then +post-stop terminal state, owned processes/partial edits, retained results and +remaining work. If termination is unknown, no overlapping writer may be started. +Managed `ready` requires a new claim; `main-direct` permits reclaim; +`reconcile`/`stop` permit neither. Never manufacture an OCX error code for a stall. + +## Acceptance scenarios + +These are semantic review cases, not executable runtime tests. A fresh C reviewer +must derive actions from the final policy and name ambiguities before acceptance. + +| ID | Activation | Expected observable decision | +| --- | --- | --- | +| S1 | Four waits, no edits, nine meaningful reads; last read seconds ago | keep active; no retirement from polls/clean Git | +| S2 | A long build is running with sparse output and no expired limit | inspect command state; sparse output alone cannot retire | +| S3 | Same unchanged reads/status repeatedly; delivered checkpoint and later comparable observations show no advancement | eligible to retire once stagnation is evidenced at the stated review point; repeats alone only prompt investigation | +| S4 | No child log access; clean Git; checkpoint may still be queued | observation unavailable; no invented failure or unanswered-message deadline | +| S5 | Native V2 wait reports updates without final text | obtain the separately delivered answer before judging | +| S6 | Actual terminal error | preserve original error and use managed failure/reconciliation path | +| S7 | User cancels or an explicit resource bound expires despite progress | stop within authority; report cancellation/bound, not provider failure | +| S8 | Stop call returns previous status `running` | verify current terminal state and owned jobs before handoff | +| S9 | Failure report returns `reconcile` or `stop` | neither direct implementation nor replacement spawn | +| S10 | New irrelevant reads or identical heartbeats keep arriving | liveness only; do not postpone reassessment forever | +| S11 | Reviewer produces no files while making new findings | treat review artifacts as progress; never require an edit | +| S12 | Finished child still has a queued checkpoint response/turn | reconcile actual current status; a checkpoint request is not permission to duplicate work | + +## Verification and delivery + +Run `npm run gate`, the existing manifest-policy test command in `000_plan.md`, +and `git diff --check`. Review links and all active retirement-rule references. +The first two checks already ran successfully at baseline; whitespace check is +mechanical only. No typecheck/build is claimed for prose. Record per-scenario +review and limitations in `011_verification.md`, archive this unit at completion, +and publish an ordinary fork PR to upstream `dev` with `Closes #178`. From 3767ae8826866ad1186639763b25de8d03bf06fc Mon Sep 17 00:00:00 2001 From: thisisjun786 <259586770+thisisjun786@users.noreply.github.com> Date: Tue, 15 Sep 2026 05:13:44 +0900 Subject: [PATCH 2/7] [agent] fix(delegation): require evidence before retiring subagents --- CHANGELOG.md | 10 ++++ .../260915_subagent_retirement/000_plan.md | 17 ++++++ .../260915_subagent_retirement/010_policy.md | 0 .../011_verification.md | 59 +++++++++++++++++++ .../skills/loop/references/waiting.md | 58 ++++++++++++++++-- .../skills/pabcd/references/delegation.md | 12 +++- structure/20_pabcd_dispatch_doctrine.md | 17 ++++-- 7 files changed, 162 insertions(+), 11 deletions(-) rename devlog/{_plan => _fin}/260915_subagent_retirement/000_plan.md (86%) rename devlog/{_plan => _fin}/260915_subagent_retirement/010_policy.md (100%) create mode 100644 devlog/_fin/260915_subagent_retirement/011_verification.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 96dee162..12b653bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ All notable changes to codexclaw are documented here. The format follows ## [Unreleased] +### Fixed + +- Subagent waiting guidance instructed coordinators to retire workers after + about three empty waits, even when analysis could still be progressing. + `waiting.md` now requires evidence of progress or stagnation, preserves + unavailable observations, and uses non-interrupting checkpoints. Delegation + and DISPATCH-RETIRE-01 clarify actual shutdown checks and managed recovery; + cancellation or exhausted limits grant no retry or replacement. This changes + agent-followed guidance, not runtime enforcement (#178). + ## [0.2.28] - 2026-09-14 ### Fixed diff --git a/devlog/_plan/260915_subagent_retirement/000_plan.md b/devlog/_fin/260915_subagent_retirement/000_plan.md similarity index 86% rename from devlog/_plan/260915_subagent_retirement/000_plan.md rename to devlog/_fin/260915_subagent_retirement/000_plan.md index c976b888..5fa48a15 100644 --- a/devlog/_plan/260915_subagent_retirement/000_plan.md +++ b/devlog/_fin/260915_subagent_retirement/000_plan.md @@ -82,3 +82,20 @@ The executor owns the four bounded source-document edits listed in consultation records, final review/integration, checks and publication. One commit may carry the coherent document fix; follow repository `[agent] fix(...)` style. No branch stacking is needed. Preserve unrelated local integration work. + +## Delivery conclusion + +The four document edits are implemented. The final independent reviewer derived +all twelve intended actions from the policy without seeing expected answers; +its initial and focused follow-up verdicts were PASS. The existing gate, seven +owner-route tests and whitespace check passed. See +[011_verification.md](011_verification.md) for scope and limits. + +The chosen hypothesis was that clearer evidence requirements can prevent the +observed premature retirement. This unit does not measure model compliance; +there is no basis for claiming faster execution or a runtime guarantee. A +watchdog and phrase-presence tests were rejected because neither would verify +the coordinator behavior without a real runtime consumer. + +Delivery stops at a reviewed contribution to upstream `dev`, linked to #178. +No merge, release, deployment or installed-payload update is part of this unit. diff --git a/devlog/_plan/260915_subagent_retirement/010_policy.md b/devlog/_fin/260915_subagent_retirement/010_policy.md similarity index 100% rename from devlog/_plan/260915_subagent_retirement/010_policy.md rename to devlog/_fin/260915_subagent_retirement/010_policy.md diff --git a/devlog/_fin/260915_subagent_retirement/011_verification.md b/devlog/_fin/260915_subagent_retirement/011_verification.md new file mode 100644 index 00000000..619baf36 --- /dev/null +++ b/devlog/_fin/260915_subagent_retirement/011_verification.md @@ -0,0 +1,59 @@ +# Verification of the retirement guidance + +The changed artifact is coordinator guidance. Structural checks and semantic +review are separate evidence; neither is a guarantee of future model compliance. + +## Independent review + +A fresh C reviewer received the four changed source documents and twelve +observation-only scenarios, without the plan or its expected-answer matrix. +It independently derived the following actions and returned PASS, no blockers. +Main compared the results with the pre-written acceptance rows. + +| Scenario | Derived action | Result | +| --- | --- | --- | +| S1 meaningful reads, no edits | continue bounded waiting | matches | +| S2 healthy long command | inspect state; sparse output alone is not failure | matches | +| S3 evidenced stagnation | retire only after evidence, then verify shutdown and permitted recovery | matches | +| S4 unavailable observations | report the gap; queued input/timeouts do not prove failure | matches | +| S5 V2 mailbox | obtain separately delivered answer | matches | +| S6 terminal error | preserve error and follow managed reconciliation | matches | +| S7 cancellation/explicit limit | stop; do not automatically continue | matches | +| S8 previous status running | withhold overlapping work until current state and owned jobs are checked | matches | +| S9 reconcile/stop result | no replacement or direct implementation | matches | +| S10 irrelevant activity | keep reassessment point; noise is not progress | matches | +| S11 reviewer findings | continue without requiring edits | matches | +| S12 queued checkpoint after completion | reconcile current state before handoff | matches | + +The reviewer briefly stumbled over the Unobservable bullet's comma list and +the changelog's verb "retired". Main clarified insufficient observations as +the condition and changed the historical mechanism to "instructed coordinators +to retire". These are readability fixes, not new runtime behavior. The same +reviewer checked those two edits and returned PASS with no remaining notes; +all twelve independently derived actions were unchanged. + +## Checks observed by the independent reviewer + +- `npm run gate`: exit 0; no status/claim/count/inventory drift. +- `node plugins/codexclaw/scripts/test.mjs plugins/codexclaw/test/manifest-policy.test.mjs`: + exit 0, 7 pass, 0 fail. +- `git diff --check`: exit 0. +- Source hashes were stable during the initial four-file review. + +The existing route test reads the waiting reference's routing path. The gate +walks skill references and structure documents for claim hygiene. These checks +do not execute a model's retirement decisions. The scenario matrix is a semantic +review, not a deterministic unit test or an instrumented lifecycle replay. + +## Limits and retained evidence + +No runtime code, model routing, installed plugin bytes, configuration or tests +were changed. No phrase-presence test or unused helper was introduced. Source +tests cannot establish cross-model compliance or delivery timing on every host. +A future coordinator ignoring substantive progress despite reading this policy +would falsify the assumption that clearer guidance alone is sufficient. + +Raw native dispatch handles, reviewer responses and check receipts remain in +untracked session evidence. Public evidence is summarized here; no private +session transcript or host-specific path is published. The contribution is +tracked by [issue #178](https://github.com/lidge-jun/codexclaw/issues/178). diff --git a/plugins/codexclaw/skills/loop/references/waiting.md b/plugins/codexclaw/skills/loop/references/waiting.md index 534827cb..8aedaf53 100644 --- a/plugins/codexclaw/skills/loop/references/waiting.md +++ b/plugins/codexclaw/skills/loop/references/waiting.md @@ -28,8 +28,56 @@ external processes inside a loop: See `cxc-pabcd` `references/delegation.md`. - Never end the turn just because a wait timed out — re-wait or poll, and keep the user informed each cycle. -- If a reviewer/worker has produced nothing after ~3 wait cycles, treat it as - a failed dispatch (DISPATCH-RETIRE-01) rather than waiting silently forever. - That retirement CONSUMES the DISPATCH-RETIRE-01 same-agent retry: go straight - to a fresh spawn with the failure folded into the new packet — the silent - agent does not get a second retry. + +## Progress, stagnation, failure, unobservable (LOOP-WAIT-EVIDENCE-01, DEFAULT) + +Wait count and elapsed time are not the retirement signal; evidence is. Before +retiring a dispatched agent, refresh task-scoped observations (VCS diff, owned +processes, recent output) and classify what you actually see: + +- **Progress** — new evidence advancing the packet: edits, findings, reads, + command events, delivered artifacts. A read-only reviewer produces findings, + not edits; never require a file change from one. Liveness alone — identical + heartbeats, repeated no-op reads or messages — is not semantic progress and + does not postpone reassessment forever. On V2, a wait reporting updates is + not the answer: obtain the separately delivered final message first. +- **Suspected stagnation** — comparable observations show no advancement. + Where supported, send one non-interrupting checkpoint asking for findings, + remaining work and the next artifact; a queued-but-unread checkpoint is not + proof of a stall. Compare new evidence with the prior observation at one + stated, task-appropriate next review point. That point fixes when you look + again; it is not a new cancellation budget, and repeated no-op activity does + not reset it. +- **Confirmed failure** — an actual terminal error, or stagnation evidenced at + the stated review point. A wait timeout alone is a normal outcome, and a + healthy long command may emit sparse output — inspect command state before + treating silence as failure. Missing edits alone do not prove a stall. +- **Unobservable** — available observations cannot establish progress or + failure; for example, child state is inaccessible and the only signals are + a clean tree and a checkpoint that may still be queued. Report the observation + gap and seek direction within authorized limits; never manufacture a failure + or an OCX error code for a stall. + +Explicit cancellation, actual terminal failures and stated user/host resource +limits outrank progress evidence; report cancellations and exhausted bounds as +what they are, separate from provider errors, and preserve the original error. + +## Retirement and handoff + +Retire on confirmed failure or an explicit cancellation/bound, not on a wait +count. Record the pre-stop reason and last meaningful activity; after the stop +call, verify the actual terminal state, owned processes and partial edits — a +returned *previous* status of `running` is not proof of termination. If +termination is unknown, start no overlapping writer. A finished child may still +hold a queued checkpoint response; reconcile its real status, and never treat a +checkpoint request as permission to duplicate its work. + +Recovery from confirmed failure follows the bounded lifecycle +(DISPATCH-RETIRE-01): at most one retry on the same handle, then a fresh spawn +with the failure folded into the new packet. When the configured +first-fallback protocol manages the dispatch its result owns the next step +instead — `ready` means claim the next attempt, `main-direct` means main +reclaims the work, and `reconcile`/`stop` authorize neither a replacement +spawn nor direct execution. Cancellation or an exhausted bound grants no +continuation: stop within authority and report the cancellation or bound, +never as a provider failure. diff --git a/plugins/codexclaw/skills/pabcd/references/delegation.md b/plugins/codexclaw/skills/pabcd/references/delegation.md index 2d4e76cc..aa2878ce 100644 --- a/plugins/codexclaw/skills/pabcd/references/delegation.md +++ b/plugins/codexclaw/skills/pabcd/references/delegation.md @@ -97,6 +97,12 @@ reuse the same reviewer throughout the A loop. Before waiting on dispatched work, read the mode-neutral [Waiting on work](../../loop/references/waiting.md) rules in either HITL or HOTL. This route does not authorize an otherwise forbidden dispatch, wait, or mode transition. +A wait timeout is an observation outcome, not a verdict: classify progress, +suspected stagnation, confirmed failure and unavailable observation per that +reference before any retirement. A suspected-stall checkpoint uses +non-interrupting delivery where the family supports it — V1 `send_input` +without `interrupt`, V2 `send_message` — and a queued message is context the +child may not have read yet, never proof of a stall. ### Detect the family first (DISPATCH-SCHEMA-DETECT-01, STRICT) @@ -130,7 +136,7 @@ thread surface: a separate Codex task is not a bigger subagent. See |---|---| | spawn | `spawn_agent({ message \| items, model?, reasoning_effort?, fork_context? })` | | handle | returns `{ agent_id, nickname }`; address by `agent_id` | -| wait | `wait_agent({ targets[], timeout_ms })` returns final status that **may carry the final message**; a timeout is a normal outcome | +| wait | `wait_agent({ targets[], timeout_ms })` returns final status that **may carry the final message**; a timeout is a normal outcome, not failure evidence | | follow-up | `send_input({ target, message \| items, interrupt? })` | | stop | `close_agent({ target })`, returning the previous status | | restore | `resume_agent({ id })` | @@ -275,7 +281,9 @@ protocol. A PreToolUse reminder after a direct call cannot retroactively manage `not_created`, `stopped`, `unknown`, or `running`. Known no-child failures need concrete `reconciliation` evidence. A stopped child requires its recorded `agentId` and evidence that work/processes stopped and changes were inspected; - pass only remaining work to the replacement. Unknown outcomes never authorize + a stop call returning previous status `running` is not that evidence — verify + the current terminal state and owned processes first. Pass only remaining work + to the replacement. Unknown outcomes never authorize another child. If native spawn is absent, report `outcome:unavailable` with confirmed `not_created` and capability evidence, never a policy denial. 5. `ready` means claim the next attempt. `main-direct` means main reclaims the diff --git a/structure/20_pabcd_dispatch_doctrine.md b/structure/20_pabcd_dispatch_doctrine.md index ef76fcbb..efbfe7f0 100644 --- a/structure/20_pabcd_dispatch_doctrine.md +++ b/structure/20_pabcd_dispatch_doctrine.md @@ -179,17 +179,26 @@ codexclaw translation: `agents.max_threads` (default 6) and V2 `max_concurrent_threads_per_session` (default 4, including the root). - **DISPATCH-RETIRE-01 (fresh-spawn fallback).** This is the exception to the reuse - default above: an agent id that failed (error, timeout, unresponsive, nonsense - output) is retired, not nursed. At most ONE retry against the same task_name; then + default above: an agent id that failed is retired, not nursed — where failure + means an actual terminal error, nonsense output, or stagnation evidenced per + the waiting reference's evidence rule + (`plugins/codexclaw/skills/loop/references/waiting.md`). A bare wait timeout is + a normal outcome, not a failure. Explicit cancellation or an exhausted + user/host bound stops within authority and is reported as such; it is not a + failure and grants no retry, replacement, or reclaim. At most ONE retry against the + same task_name; then abandon it (V2 has only `interrupt_agent`; V1 has `close_agent` and `resume_agent`) and fresh-spawn with the failure summary folded into the new TASK packet. Repeated `followup_task`/`send_message` against a broken agent is a broken-resume loop — the dispatch analogue of LOOP-REPAIR-01's doom loop. When the fresh spawn — a SECOND distinct agent — also fails the SAME task packet, stop blaming agents: two independent failures on one packet are evidence the packet - itself failed the DISPATCH-ECONOMY-01 specifiability bar. The main session + itself failed the DISPATCH-ECONOMY-01 specifiability bar. Absent a managed + dispatch, the main session reclaims that slice and does the work directly instead of dispatching a third - copy (packet-failure reclaim, 260711 fork-debate verdict #4). Lineage: + copy (packet-failure reclaim, 260711 fork-debate verdict #4); under configured + first fallback the protocol result owns that decision — `main-direct` permits + reclaim, `reconcile`/`stop` permit neither reclaim nor a replacement spawn. Lineage: `../jawcode/devlog/_plan/260616_actor_fresh_fallback/_fin/00_moc.md` (implementation-verified). Both rules are E7 doctrine (agent-followed); no hook observes agent lifecycles. From 4a829dfe9567254119b1c756370e5388ca1129c3 Mon Sep 17 00:00:00 2001 From: thisisjun786 <259586770+thisisjun786@users.noreply.github.com> Date: Tue, 15 Sep 2026 16:05:40 +0900 Subject: [PATCH 3/7] [agent] fix(pabcd): preserve managed recovery in plan template --- .../011_verification.md | 18 ++++++++++++++++++ .../skills/pabcd/references/plan-output.md | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/devlog/_fin/260915_subagent_retirement/011_verification.md b/devlog/_fin/260915_subagent_retirement/011_verification.md index 619baf36..e2bd2a59 100644 --- a/devlog/_fin/260915_subagent_retirement/011_verification.md +++ b/devlog/_fin/260915_subagent_retirement/011_verification.md @@ -57,3 +57,21 @@ Raw native dispatch handles, reviewer responses and check receipts remain in untracked session evidence. Public evidence is summarized here; no private session transcript or host-specific path is published. The contribution is tracked by [issue #178](https://github.com/lidge-jun/codexclaw/issues/178). + +## PR review follow-up: plan template recovery + +[Review comment](https://github.com/lidge-jun/codexclaw/pull/179#discussion_r4009201474) +identified an unconditional two-failure reclaim instruction still present in +`pabcd/references/plan-output.md`. The earlier review treated it as shorthand; +that was insufficient because coordinators use the template to write plans. + +The escalation row now distinguishes unmanaged recovery from configured first +fallback and links to the existing recovery owner. Main reclaims only on +`main-direct` under managed dispatch; `reconcile` and `stop` authorize neither +reclaim nor replacement, even after two failures. Unmanaged reclaim requires +prior work to have stopped. The P-phase amendment requirement is retained. + +Manual comparison with `delegation.md` and DISPATCH-RETIRE-01 confirmed the +`main-direct`, `reconcile`, `stop`, and unmanaged-recovery cases. Fresh checks: +`npm run gate` passed; manifest-policy tests passed 7/7; `git diff --check` +was clean. These remain document checks and semantic review, not runtime proof. diff --git a/plugins/codexclaw/skills/pabcd/references/plan-output.md b/plugins/codexclaw/skills/pabcd/references/plan-output.md index d856c0ab..8fa25f4b 100644 --- a/plugins/codexclaw/skills/pabcd/references/plan-output.md +++ b/plugins/codexclaw/skills/pabcd/references/plan-output.md @@ -15,7 +15,7 @@ not silently remove a field. | Stop condition | When the authorized work ends; plan-only stops after returning the requested plan. | | Memory artifact | Where the plan/evidence is recorded. For a no-file request, name this returned plan rather than creating a file. Authorized persistent execution still follows the implementation-unit record rules. | | Expected terminal outcomes | What success, unresolved requirements or blocked execution would mean for this scope. Report outcomes are not new FSM phases or host goal statuses. | -| Escalation condition | What requires main/user direction. If delegation is planned, state both directions: main reclaims a slice after two distinct agents fail its packet (DISPATCH-RETIRE-01); pushing a slice to a worker requires a P-phase amendment, never a mid-B improvisation. | +| Escalation condition | What requires main/user direction. Delegation plans follow DISPATCH-RETIRE-01: without managed dispatch, main reclaims after two distinct agents fail the same packet, once prior work is stopped. With [configured first fallback](delegation.md#configured-first-fallback), the returned action governs recovery: only `main-direct` permits reclaim; `reconcile`/`stop` permit neither reclaim nor replacement. Pushing a slice to a worker requires a P-phase amendment, never a mid-B improvisation. | HOTL goal plans also state the cxc-loop resource bounds. Follow the live host goal-tool contract; do not invent a token or time budget that the user did not set. From 3c0bcc6f71eeef41b4c6f0e31b3c965e3077bbad Mon Sep 17 00:00:00 2001 From: thisisjun786 <259586770+thisisjun786@users.noreply.github.com> Date: Tue, 15 Sep 2026 16:10:18 +0900 Subject: [PATCH 4/7] [agent] fix(delegation): defer architect recovery to managed dispatch --- .../011_verification.md | 16 ++++++++++++++ .../skills/pabcd/references/delegation.md | 22 +++++++++++++------ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/devlog/_fin/260915_subagent_retirement/011_verification.md b/devlog/_fin/260915_subagent_retirement/011_verification.md index e2bd2a59..7f83ee0d 100644 --- a/devlog/_fin/260915_subagent_retirement/011_verification.md +++ b/devlog/_fin/260915_subagent_retirement/011_verification.md @@ -75,3 +75,19 @@ Manual comparison with `delegation.md` and DISPATCH-RETIRE-01 confirmed the `main-direct`, `reconcile`, `stop`, and unmanaged-recovery cases. Fresh checks: `npm run gate` passed; manifest-policy tests passed 7/7; `git diff --check` was clean. These remain document checks and semantic review, not runtime proof. + +## PR review follow-up: architect recovery + +[Review comment](https://github.com/lidge-jun/codexclaw/pull/179#discussion_r4012925635) +found the same incomplete propagation in the architect-specific recovery clause. +Accepted: a role-specific instruction must not bypass the common recovery owner. +The clause now defers managed retries and reclaim to the returned action, and +limits the same-handle/two-context rule to unmanaged dispatch. Prior work must +be stopped and inspected before unmanaged recovery. Missing architect consultation +still blocks dependent completion, including when main reclaims planning. + +Reviewed recovery references in the active skills and structure doctrine. Manual +comparison covered managed `ready`, `main-direct`, `reconcile`, `stop`, unmanaged +recovery, and the unmet-consultation constraint. Fresh `npm run gate`, all 7 +manifest-policy tests, and `git diff --check` passed. These are document checks +and main's semantic review; the initial independent scenario review is unchanged. diff --git a/plugins/codexclaw/skills/pabcd/references/delegation.md b/plugins/codexclaw/skills/pabcd/references/delegation.md index aa2878ce..fff7deb4 100644 --- a/plugins/codexclaw/skills/pabcd/references/delegation.md +++ b/plugins/codexclaw/skills/pabcd/references/delegation.md @@ -245,13 +245,21 @@ named decision revisions within ONE plan; a separate new plan starts a fresh con Do not promise cost savings from reuse. Use the host's supported follow-up and wait operations; an empty timed wait alone is not evidence of a failed call. -On an actual failed call, preserve the failure evidence and apply the existing -retirement rule: at most one retry on the same handle, then a fresh context carrying -the failure and plan. If a second distinct context also fails, main reclaims the -planning work under the existing lifecycle rule, but the missing architect consultation -remains unmet. Report the gap and stop dependent completion; main self-check does not -replace it. Do not silently switch models, register roles, or bypass host restrictions. -Explicit user limits still govern dispatch and completion scope. +On an actual failed call, preserve the failure evidence. With +[configured first fallback](#configured-first-fallback), the returned action governs +recovery: `ready` requires a new claim, only `main-direct` permits reclaim, and +`reconcile`/`stop` permit neither reclaim nor replacement. The unmanaged retry rule +below does not authorize extra calls on this path. + +Without managed dispatch, apply the existing retirement rule: at most one retry +on the same handle, then a fresh context carrying the failure and plan. If a second +distinct context also fails, main reclaims the planning work. Confirm prior work +has stopped and inspect partial results before retry, replacement or reclaim. + +In either path, a missing architect consultation remains unmet. Report the gap +and stop dependent completion; main self-check does not replace it. Do not silently +switch models, register roles, or bypass host restrictions. Explicit user limits +still govern dispatch and completion scope. ## Speculative dispatch (DISPATCH-SPECULATE-01, HEURISTIC) From 20e50485f994bed509f44e071e7119b536b1d520 Mon Sep 17 00:00:00 2001 From: thisisjun786 <259586770+thisisjun786@users.noreply.github.com> Date: Tue, 15 Sep 2026 16:15:52 +0900 Subject: [PATCH 5/7] [agent] fix(loop): classify unusable final output as failure --- .../260915_subagent_retirement/011_verification.md | 13 +++++++++++++ plugins/codexclaw/skills/loop/references/waiting.md | 11 +++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/devlog/_fin/260915_subagent_retirement/011_verification.md b/devlog/_fin/260915_subagent_retirement/011_verification.md index 7f83ee0d..a1e485b7 100644 --- a/devlog/_fin/260915_subagent_retirement/011_verification.md +++ b/devlog/_fin/260915_subagent_retirement/011_verification.md @@ -91,3 +91,16 @@ comparison covered managed `ready`, `main-direct`, `reconcile`, `stop`, unmanage recovery, and the unmet-consultation constraint. Fresh `npm run gate`, all 7 manifest-policy tests, and `git diff --check` passed. These are document checks and main's semantic review; the initial independent scenario review is unchanged. + +## PR review follow-up: unusable final output + +[Review comment](https://github.com/lidge-jun/codexclaw/pull/179#discussion_r4012960563) +identified another owner mismatch: the doctrine includes nonsense output as a +failure, but the waiting classification only named terminal errors and stagnation. +Accepted: a transport-successful final response can still fail the task packet. +The waiting reference now includes demonstrably nonsensical or unusable final +output and requires concrete evidence. Interim updates and supported disagreement +alone do not count as failure. Existing shutdown and managed recovery rules apply. + +Fresh `npm run gate`, all 7 manifest-policy tests, and `git diff --check` passed. +These checks cover document hygiene and routes, not the output-quality judgment. diff --git a/plugins/codexclaw/skills/loop/references/waiting.md b/plugins/codexclaw/skills/loop/references/waiting.md index 8aedaf53..ba4fdc45 100644 --- a/plugins/codexclaw/skills/loop/references/waiting.md +++ b/plugins/codexclaw/skills/loop/references/waiting.md @@ -48,10 +48,13 @@ processes, recent output) and classify what you actually see: stated, task-appropriate next review point. That point fixes when you look again; it is not a new cancellation budget, and repeated no-op activity does not reset it. -- **Confirmed failure** — an actual terminal error, or stagnation evidenced at - the stated review point. A wait timeout alone is a normal outcome, and a - healthy long command may emit sparse output — inspect command state before - treating silence as failure. Missing edits alone do not prove a stall. +- **Confirmed failure** — an actual terminal error, final output demonstrably + nonsensical or unusable for the task packet, or stagnation evidenced at the + stated review point. Record concrete output evidence for an output-failure + judgment; interim updates and supported disagreement alone are not failures. + A wait timeout alone is a normal outcome, and a healthy long command may emit + sparse output — inspect command state before treating silence as failure. + Missing edits alone do not prove a stall. - **Unobservable** — available observations cannot establish progress or failure; for example, child state is inaccessible and the only signals are a clean tree and a checkpoint that may still be queued. Report the observation From 997cdf7ab203f650999eef1b110d147f622a97da Mon Sep 17 00:00:00 2001 From: thisisjun786 <259586770+thisisjun786@users.noreply.github.com> Date: Tue, 15 Sep 2026 16:33:58 +0900 Subject: [PATCH 6/7] [agent] docs(plan): specify reconciled task failure recovery --- .../260915_task_failure_recovery/000_plan.md | 48 ++++++++ .../010_recovery.md | 105 ++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 devlog/_plan/260915_task_failure_recovery/000_plan.md create mode 100644 devlog/_plan/260915_task_failure_recovery/010_recovery.md diff --git a/devlog/_plan/260915_task_failure_recovery/000_plan.md b/devlog/_plan/260915_task_failure_recovery/000_plan.md new file mode 100644 index 00000000..ef3ced7a --- /dev/null +++ b/devlog/_plan/260915_task_failure_recovery/000_plan.md @@ -0,0 +1,48 @@ +# Managed task failure recovery + +PR #179 follow-up to [review](https://github.com/lidge-jun/codexclaw/pull/179#discussion_r4013017929). +The prior documentation cycle preserved managed recovery authority. A later review +showed that confirmed task failures have no executable recovery path through it. +This cycle changes that conclusion with direct runtime evidence, not another prose exception. + +## Requirements and scope + +- Represent confirmed stagnation and unusable final output separately from provider errors. +- Require a recorded child, confirmed termination, partial-work inspection and concrete + task-failure evidence before handing off remaining work. +- Preserve provider decoding, cancellation/policy stops, identity, claim/spawn deduplication, + the two-candidate bound and independent-review obligations. +- Retain old dispatch-state compatibility and consistent installed caller guidance. +- Finish with relevant runtime/CLI regression evidence, current CI and no medium-or-higher + unresolved source or GitHub review findings. + +One bounded PABCD cycle follows this specification. The affected report contract receives +C4 review and negative-case coverage. Main owns decisions, integration, records and PR; +the configured executor will own the audited implementation bundle. The configured +architect proposes the contract and reflects on the executable plan before independent audit. + +Use the existing subagent-config module, tests, waiting/delegation owners and structure +index. No new dependency, retry framework, watchdog, provider call or installed configuration. +No merge, release or deployment. No user token, cost or wall-clock budget was specified; +the work remains bounded by this PR and its review findings. Preserve other checkouts. + +## Reproduction and trust boundary + +At `20e50485`, an isolated real `runDispatch` fixture creates and claims an executor, +records its child, then reports unusable final output with `executionState: stopped`, +the recorded identity and reconciliation evidence. First and repeated reports both +return `reconcile`, retain one attempt and request structured OCX evidence that does not +exist for this failure. Existing dispatch/CLI tests pass 23/23; they omit this recovery case. + +The assets are exclusive work ownership and bounded model invocation. The boundary is +main-supplied CLI JSON into persisted dispatch state. Malformed or conflicting reports, +quoted child output and stale identities must not create recovery permission. Main can +misreport observations already; these are caller assertions, not authenticated native +receipts. Tests must prove validation and state transitions without claiming to observe +real child termination or judge model output automatically. + +The executable file map, decision IDs, acceptance cases and consultation record belong +in `010_recovery.md`. Existing code-only checks and the normal build are available: +dispatch/CLI tests exit 0 (23 tests), `npm run build` exits 0 (181 files), and the gate +exits 0 on the baseline. Tests invoke the actual state machine and separate CLI processes; +the build compiles component source; the gate covers document hygiene only. diff --git a/devlog/_plan/260915_task_failure_recovery/010_recovery.md b/devlog/_plan/260915_task_failure_recovery/010_recovery.md new file mode 100644 index 00000000..59d556a6 --- /dev/null +++ b/devlog/_plan/260915_task_failure_recovery/010_recovery.md @@ -0,0 +1,105 @@ +# Task failure report: executable plan + +| Field | Contract | +|---|---| +| Goal | Safely recover confirmed task failures and close PR #179's substantive reviews. | +| Mode / class | Scoped HOTL; report contract receives C4 care. One recovery cycle. | +| Scope / resources | This checkout and PR only; no explicit token or time budget; no installation, live provider probe, merge or release. | +| Completion | Runtime/CLI positive and negative cases, affected suites/build, independent review and current CI pass; no medium-or-higher findings remain. | +| Escalation | Managed results govern all child recovery. `reconcile`/`stop` grant no reclaim or replacement; broader unrelated changes require a new decision. | + +## Decisions and consultation + +Architect proposal accepted as D1-D8 below. Main reproduced the stranded state and +accepts rotation to the existing next candidate, not a new same-model retry mode. +Generated repository payload is in scope; rebuilding it does not install the plugin. +Use `task_failed` consistently. Stagnation's review-point evidence stays in bounded +evidence text; no additional scheduler or observation database is needed. + +- D1: Add `outcome: task_failed`; keep `failed` provider decoding unchanged. +- D2: Require `taskFailure: {kind: stagnation|unusable_output, evidence: string}`. + Reject unknown keys/kinds, empty evidence, non-string values and text over 2000 characters. +- D3: Require the recorded child identity, `executionState: stopped`, and existing + bounded reconciliation evidence. Running/unknown state returns `reconcile`; no-child + task failure is rejected. Check state before any candidate or direct-work grant. +- D4: If `error` is supplied with `task_failed`, provider restrictions win: decoded + stop records the code and stops; unknown returns `reconcile`; next-eligible errors + are rejected as mixed reports and must use `outcome: failed`. No error means task path. +- D5: Reuse the existing failed-attempt tail: next configured candidate is `ready`, + then requires claim; after the last candidate return `main-direct`. No third attempt. +- D6: Persist nullable `Attempt.taskFailure`; preserve `code` for provider codes. + Old version-1 records missing the field normalize to null. Invalid stored values + fail closed. New records initialize null. Expose metadata through existing `attempts`. +- D7: Keep `DispatchResult` actions, role constraints, locks and native issuance unchanged. + Optional reason text may explain task recovery; it is not the authority signal. +- D8: Document the same payload in SessionStart guidance, delegation, waiting and + the public subagents guide. Update CHANGELOG and the structure index owner pointer. + +Alternative rejected: adding a fake OCX code or treating arbitrary `failed.error` +prose as next-eligible. That weakens the provider boundary and confuses observation +with provider error. A new `task_failed` report is an explicit main judgment with +separate evidence. It is not inferred from model output or a wait count. + +Architect proposal and reflection completed: ALIGNED for D1-D8, no material gaps. +Main accepts both implementation clarifications: decoded stop precedes the child-state +gate; ordinary `failed` reports retain their existing behavior even with stray task metadata. +Actual handles and raw consultation outputs stay in local task evidence. + +## File and field chain + +Paths below are repository-relative. The configured executor owns this implementation +bundle after A; main owns planning records, generated build output, measured README +test badges, verification records, source review and PR publication. + +| File | Change | +|---|---| +| `plugins/codexclaw/components/subagent-config/src/fallback-dispatch.ts` | MODIFY input outcome validation, task payload parser, attempt metadata factory/read validation, report transition; share bounded handoff tail. | +| `plugins/codexclaw/components/subagent-config/src/fallback-dispatch-cli.ts` | MODIFY `DISPATCH_GUIDANCE` with concrete task report. CLI already passes parsed JSON to `runDispatch`; no new command or flags. | +| `plugins/codexclaw/components/subagent-config/test/fallback-dispatch.test.ts` | MODIFY existing real fixture tests for state transitions and negatives; no deleted assertions. | +| `plugins/codexclaw/components/subagent-config/test/fallback-dispatch-cli.test.ts` | MODIFY real separate-process round trip covering task failure and persisted metadata. | +| `plugins/codexclaw/skills/pabcd/references/delegation.md` | MODIFY report instructions and evidence/limit precedence. | +| `plugins/codexclaw/skills/loop/references/waiting.md` | MODIFY recovery route for confirmed task failures; keep wait classification evidence-based. | +| `docs-site/src/content/docs/guides/subagents.md` | MODIFY public protocol example and limits. | +| `CHANGELOG.md`, `structure/INDEX.md` | MODIFY change record and owner description to include runtime recovery. | +| `plugins/codexclaw/components/subagent-config/dist/fallback-dispatch{,-cli}.js` | REGENERATE from source with the standard build; never hand-edit. | +| `README.md`, `README.ko.md` | MODIFY test count only if new tests change measured inventory; use normal inventory tool. | + +Creation is explicit CLI JSON → `runDispatch`/`report`; validation is the task parser +and stopped-child gate; serialization is `saveState`; deserialization is `readState`; +consumption is the existing `result().attempts` plus main guidance. `managedSpawn` and +`issueManagedSpawn` consume unchanged candidate/claim/state fields. The decoder remains +provider-only. No SDK, GUI input form or alternative report parser owns this field. + +## Acceptance and verification + +| Reachable trigger | Required observation | +|---|---| +| Created, stopped child; each task kind with valid evidence | First failure → ready; claim selects configured fallback; metadata round-trips with null provider code. | +| Same valid failure on last candidate, all roles | main-direct, exactly two attempts; reviewer still requires independent review. | +| State persisted without the new key | status works and later task failure recovers; malformed new metadata is rejected. | +| Running/unknown child state | reconcile, unchanged candidate count, no spawn/direct permission. | +| not_created, missing/wrong child, missing reconciliation | Rejected; no recovery grant or hidden state advancement. | +| Invalid kind/shape/unknown key, blank/oversized evidence | Rejected; existing state remains readable and unchanged. | +| Permission/policy/cancellation error plus task label | stop wins and persists; a later label cannot reopen it. | +| Unknown error plus task label | reconcile; no extra attempt. | +| Next-eligible provider error plus task label | Rejected as mixed; ordinary failed path retains old behavior. | +| Plain failed report with task metadata and no provider error | Existing unknown-error reconcile behavior, never implicit task recovery. | +| Before claim, stale ID, duplicate claim/issuance | Existing rejection/reconciliation and one-spawn invariant remain. | +| Already complete/stopped dispatch | No reopening by task report. | +| Separate CLI processes, valid task failures through both candidates | ready → claim → main-direct, metadata survives status/restart. | + +Record a failing task-recovery assertion before source changes, then the same test +passing. Focused baseline command (exit 0, 23 tests) directly loads the two target suites: +`node plugins/codexclaw/scripts/test.mjs plugins/codexclaw/components/subagent-config/test/fallback-dispatch.test.ts plugins/codexclaw/components/subagent-config/test/fallback-dispatch-cli.test.ts`. +After implementation run the complete subagent-config suite, `npm run build`, +`npm run gate`, full root tests for the measured badge count, inventory check and +`git diff --check`. Normal hosted CI covers OS and packed-payload compatibility. +Semantic review checks the caller docs against the live report contract. A fresh C +reviewer re-derives recovery reachability; merely preserving unknown-error rejection +does not prove task recovery works, which was the earlier review's blind spot. + +Enforcement scope: CLI validation and persisted transitions execute in code for managed +callers; direct native calls bypass this protocol. Evidence truth and actual child +termination remain main observations. No universal lifecycle-enforcement claim is made. +Keep this repair in PR #179 because its new failure classification otherwise strands +managed callers; the larger aggregate diff includes the original planning/evidence records. From cb25e3b11904b486c38990603374a80bdda2b19b Mon Sep 17 00:00:00 2001 From: thisisjun786 <259586770+thisisjun786@users.noreply.github.com> Date: Tue, 15 Sep 2026 17:42:45 +0900 Subject: [PATCH 7/7] [agent] fix(dispatch): recover confirmed task failures after reconciliation --- CHANGELOG.md | 9 +- README.ko.md | 2 +- README.md | 2 +- README.zh.md | 2 +- .../011_verification.md | 19 +- .../260915_task_failure_recovery/000_plan.md | 0 .../010_recovery.md | 11 +- .../011_verification.md | 61 ++++++ .../src/content/docs/guides/subagents.md | 9 + .../dist/fallback-dispatch-cli.js | 2 +- .../subagent-config/dist/fallback-dispatch.js | 45 ++++- .../src/fallback-dispatch-cli.ts | 2 +- .../subagent-config/src/fallback-dispatch.ts | 45 ++++- .../test/fallback-dispatch-cli.test.ts | 32 +++- .../test/fallback-dispatch.test.ts | 176 +++++++++++++++++- .../skills/loop/references/waiting.md | 8 + .../skills/pabcd/references/delegation.md | 25 ++- structure/INDEX.md | 1 + 18 files changed, 430 insertions(+), 21 deletions(-) rename devlog/{_plan => _fin}/260915_task_failure_recovery/000_plan.md (100%) rename devlog/{_plan => _fin}/260915_task_failure_recovery/010_recovery.md (93%) create mode 100644 devlog/_fin/260915_task_failure_recovery/011_verification.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 12b653bf..1f81accb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,8 +13,13 @@ All notable changes to codexclaw are documented here. The format follows `waiting.md` now requires evidence of progress or stagnation, preserves unavailable observations, and uses non-interrupting checkpoints. Delegation and DISPATCH-RETIRE-01 clarify actual shutdown checks and managed recovery; - cancellation or exhausted limits grant no retry or replacement. This changes - agent-followed guidance, not runtime enforcement (#178). + cancellation or exhausted limits grant no retry or replacement. Progress + assessment remains agent-followed (#178). +- Managed dispatch now accepts explicit `task_failed` reports for confirmed + stagnation or unusable final output. A recorded stopped child, task evidence + and reconciliation are required before the existing bounded handoff can run. + Provider errors keep their existing classification; cancellation and permission + denials cannot be overridden by a task-failure label (#178). ## [0.2.28] - 2026-09-14 diff --git a/README.ko.md b/README.ko.md index 35736a36..e6b0c524 100644 --- a/README.ko.md +++ b/README.ko.md @@ -13,7 +13,7 @@

CI - 3,150 tests passing + 3,165 tests passing 29 skills 28 hooks Documentation diff --git a/README.md b/README.md index 2dcee674..e3fd4f04 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@

CI - 3,150 tests passing + 3,165 tests passing 29 skills 28 hooks Documentation diff --git a/README.zh.md b/README.zh.md index f0a07a0b..0bab5bb6 100644 --- a/README.zh.md +++ b/README.zh.md @@ -13,7 +13,7 @@

CI - 3,150 tests passing + 3,165 tests passing 29 skills 28 hooks Documentation diff --git a/devlog/_fin/260915_subagent_retirement/011_verification.md b/devlog/_fin/260915_subagent_retirement/011_verification.md index a1e485b7..a6c9a6e3 100644 --- a/devlog/_fin/260915_subagent_retirement/011_verification.md +++ b/devlog/_fin/260915_subagent_retirement/011_verification.md @@ -45,10 +45,10 @@ walks skill references and structure documents for claim hygiene. These checks do not execute a model's retirement decisions. The scenario matrix is a semantic review, not a deterministic unit test or an instrumented lifecycle replay. -## Limits and retained evidence +## Initial policy revision: limits and retained evidence -No runtime code, model routing, installed plugin bytes, configuration or tests -were changed. No phrase-presence test or unused helper was introduced. Source +At the initial policy revision, no runtime code, model routing, installed plugin +bytes, configuration or tests were changed. No phrase-presence test or unused helper was introduced. Source tests cannot establish cross-model compliance or delivery timing on every host. A future coordinator ignoring substantive progress despite reading this policy would falsify the assumption that clearer guidance alone is sufficient. @@ -104,3 +104,16 @@ alone do not count as failure. Existing shutdown and managed recovery rules appl Fresh `npm run gate`, all 7 manifest-policy tests, and `git diff --check` passed. These checks cover document hygiene and routes, not the output-quality judgment. + +## Later review: managed recovery reachability + +[Review comment](https://github.com/lidge-jun/codexclaw/pull/179#discussion_r4013017929) +found that recognizing task failures did not make managed recovery executable: +descriptive stagnation or unusable-output reports remained `reconcile` even after +termination was confirmed. A source review at `20e50485` correctly verified that +unknown error prose cannot rotate providers, but missed the need for a separate +task-failure path. Main reproduced that gap with the real state machine. + +The follow-up cycle is recorded in `devlog/_fin/260915_task_failure_recovery/`. +Its runtime/CLI evidence supersedes the initial policy-only scope above; it must +not be inferred from the earlier semantic scenario review. diff --git a/devlog/_plan/260915_task_failure_recovery/000_plan.md b/devlog/_fin/260915_task_failure_recovery/000_plan.md similarity index 100% rename from devlog/_plan/260915_task_failure_recovery/000_plan.md rename to devlog/_fin/260915_task_failure_recovery/000_plan.md diff --git a/devlog/_plan/260915_task_failure_recovery/010_recovery.md b/devlog/_fin/260915_task_failure_recovery/010_recovery.md similarity index 93% rename from devlog/_plan/260915_task_failure_recovery/010_recovery.md rename to devlog/_fin/260915_task_failure_recovery/010_recovery.md index 59d556a6..fe72e7d6 100644 --- a/devlog/_plan/260915_task_failure_recovery/010_recovery.md +++ b/devlog/_fin/260915_task_failure_recovery/010_recovery.md @@ -47,9 +47,12 @@ Actual handles and raw consultation outputs stay in local task evidence. ## File and field chain -Paths below are repository-relative. The configured executor owns this implementation -bundle after A; main owns planning records, generated build output, measured README -test badges, verification records, source review and PR publication. +Paths below are repository-relative. After A, the executor owns the state-machine +implementation and two test files. During B it explicitly released the untouched +guidance/documentation files to main; the sequential handoff was delaying integration. +Main owns that wording, planning records, generated build output, measured README +test badges, verification records, source review and PR publication. D1-D8 and the +acceptance criteria are unchanged; this is a successful scope split, not failure recovery. | File | Change | |---|---| @@ -62,7 +65,7 @@ test badges, verification records, source review and PR publication. | `docs-site/src/content/docs/guides/subagents.md` | MODIFY public protocol example and limits. | | `CHANGELOG.md`, `structure/INDEX.md` | MODIFY change record and owner description to include runtime recovery. | | `plugins/codexclaw/components/subagent-config/dist/fallback-dispatch{,-cli}.js` | REGENERATE from source with the standard build; never hand-edit. | -| `README.md`, `README.ko.md` | MODIFY test count only if new tests change measured inventory; use normal inventory tool. | +| `README.md`, `README.ko.md`, `README.zh.md` | MODIFY test count only if new tests change measured inventory; use normal inventory tool. | Creation is explicit CLI JSON → `runDispatch`/`report`; validation is the task parser and stopped-child gate; serialization is `saveState`; deserialization is `readState`; diff --git a/devlog/_fin/260915_task_failure_recovery/011_verification.md b/devlog/_fin/260915_task_failure_recovery/011_verification.md new file mode 100644 index 00000000..fe6d2eae --- /dev/null +++ b/devlog/_fin/260915_task_failure_recovery/011_verification.md @@ -0,0 +1,61 @@ +# Task failure recovery verification + +The implemented `task_failed` path closes the runtime gap reported in +[PR review](https://github.com/lidge-jun/codexclaw/pull/179#discussion_r4013017929). +Both confirmed task kinds now reach the existing bounded handoff after a recorded +child is stopped and evidence is supplied. Unknown provider prose still reconciles; +no provider error code was invented or added to the decoder. + +## Observed checks + +- RED: `confirmed task failure recovers through the next candidate, then main-direct` + failed with `invalid report outcome` against the prior source. +- GREEN: the same test passed after implementation; the initial focused run passed + 24 tests. Later coverage added malformed metadata, legacy records, identities, + provider precedence, terminal guards and separate CLI persistence. +- Main reproduced and corrected stale provider-code metadata after a prior + reconciled report; accepted task failures now clear that code. +- Standard build passed: 181 source files compiled, manifest layout validated. +- Compiled root CLI QA passed 35 separate invocations with captured stdin, + stdout, stderr and exit codes. Cases covered failure recovery through both + candidates, status/resume, duplicate claim, live/unknown child, malformed input, + 2000/2001-character bounds and terminal cancellation/permission precedence. + No native agents or providers were invoked by the fixtures; temporary QA + state was removed and its absence confirmed. +- Full root suite: 3165 total, 3091 pass, 74 skip, 0 fail. The source diff hash + was unchanged throughout the run. Inventory badges were then regenerated from + that measured total; inventory check, document gate and diff check passed. + +The initial full run failed five unrelated environment checks: this checkout had +no installed React dependency, and an existing `/tmp/.git` made four temporary-root +fixtures resolve their ancestor. Installing the lockfile's existing dependencies +and setting an isolated temporary directory fixed the environment. The ten tests +in those two files passed, then the whole suite passed. No product code or test +assertion was changed to suppress these failures; package manifests and lockfile +are unchanged. + +## Independent review + +The architect proposed D1-D8 and reflected ALIGNED on the concrete plan. Independent +A audit found no blockers and reran the 23-test baseline. A fresh C reviewer reviewed +all changed source, tests, generated bytes, caller guidance and records; it independently +derived positive recovery and negative cases before returning PASS with no substantive +findings. Source-to-dist compilation matched, 19 reviewed file hashes stayed stable, +and a later pass accounted for the three generated README badge updates. + +Reviewer VM probes covered eight scenario groups. Its first documentation-example +probe selected the wrong example and failed; correcting that extractor proved the +actual task-report example reaches `ready`. Removing the new task branch in memory +restored `invalid report outcome`, confirming the new recovery test is not vacuous. + +## Evidence limits + +Runtime tests and CLI fixtures prove report validation and state transitions. They +do not authenticate main's observations, measure live model compliance or establish +that a real child/process stopped. The output-quality judgment remains main-owned. +No plugin was installed, and no merge or release occurred. The fixtures made no +provider calls. +The earlier policy-only review missed recovery reachability; broader runtime evidence +was necessary to close this issue. Raw receipts and native handles remain local. + +Hosted CI and GitHub re-review are verified on the published commit separately. diff --git a/docs-site/src/content/docs/guides/subagents.md b/docs-site/src/content/docs/guides/subagents.md index 482f1c2f..8bde1a62 100644 --- a/docs-site/src/content/docs/guides/subagents.md +++ b/docs-site/src/content/docs/guides/subagents.md @@ -106,6 +106,15 @@ authentication and cancellation failures stop. Ambiguous creation or ongoing wor must be reconciled before replacement; stopped executors require a change review and cleanup evidence. Unknown error prose does not trigger blind rotation. +Confirmed stagnation or unusable final output uses a separate `outcome:task_failed` +report with `taskFailure: {kind: "stagnation" | "unusable_output", evidence: "..."}`. +Include the recorded child ID, `executionState:stopped`, and `reconciliation` +describing termination and partial-work inspection. Both evidence strings must be +non-empty and at most 2000 characters. This report has no provider `error`; it uses +the same bounded handoff and returned actions as provider recovery. Cancellation, +exhausted limits and a wait timeout alone are not task failures. Validate final +work before reporting `outcome:complete`, which cannot be reopened for recovery. + OCX retains its own retries and global/per-model fallback. The two-attempt limit applies to CXC-issued native attempts, not every downstream provider request. Requested and observed models are recorded separately; an unobserved actual model diff --git a/plugins/codexclaw/components/subagent-config/dist/fallback-dispatch-cli.js b/plugins/codexclaw/components/subagent-config/dist/fallback-dispatch-cli.js index 76d2fbda..4a224ce2 100644 --- a/plugins/codexclaw/components/subagent-config/dist/fallback-dispatch-cli.js +++ b/plugins/codexclaw/components/subagent-config/dist/fallback-dispatch-cli.js @@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url"; import { runDispatch } from "./fallback-dispatch.js"; import { readConfig, ROLES } from "./store.js"; -export const DISPATCH_GUIDANCE = `Roles with a first fallback use the main-owned dispatch protocol before native spawn. Run cxc subagents dispatch with one JSON object on stdin: {action:"start",sessionId:,dispatchId:,role:}. Then claim with {action:"claim",sessionId,dispatchId,attemptId}. Only action=spawn authorizes one native call; prepend its marker followed by a newline to the original task/skills, pass candidate model and effort when non-null, and use a fresh context. Report creation with {action:"report",outcome:"created",sessionId,dispatchId,attemptId,agentId:}, then use native wait. Report success with {action:"report",outcome:"complete",sessionId,dispatchId,attemptId,agentId}; creation is not completion. Report failure with action:"report",outcome:"failed", the same IDs, the original error and executionState (not_created/stopped/unknown/running). Failed handoff requires concrete reconciliation evidence and the recorded agentId for a stopped child. Inspect changes and stop all prior work before retrying. A ready result requires a new claim. Status never authorizes a second spawn. main-direct returns remaining work to the main agent; independent review still requires independent evidence. stop/reconcile never authorizes another model or direct execution. OCX owns provider retries; CXC selects at most two native attempts. Do not invent error codes from arbitrary prose; preserve structured errors or canonical transport error text. Explicit caller model overrides and full-history forks are outside this managed path. Never use a dispatch marker to bypass native permissions.`; +export const DISPATCH_GUIDANCE = `Roles with a first fallback use the main-owned dispatch protocol before native spawn. Run cxc subagents dispatch with one JSON object on stdin: {action:"start",sessionId:,dispatchId:,role:}. Then claim with {action:"claim",sessionId,dispatchId,attemptId}. Only action=spawn authorizes one native call; prepend its marker followed by a newline to the original task/skills, pass candidate model and effort when non-null, and use a fresh context. Report creation with {action:"report",outcome:"created",sessionId,dispatchId,attemptId,agentId:}, then use native wait. Report success with {action:"report",outcome:"complete",sessionId,dispatchId,attemptId,agentId} only after validating final work; native completion alone is not task success and terminal reports cannot reopen. Report provider failure with action:"report",outcome:"failed", the same IDs, the original error and executionState (not_created/stopped/unknown/running). For confirmed stagnation or unusable final output, report outcome:"task_failed" with taskFailure:{kind:"stagnation"|"unusable_output",evidence:}, the same IDs, executionState:"stopped", and reconciliation:. Task evidence and reconciliation must each be non-empty text of at most 2000 characters; taskFailure permits only kind and evidence. Omit error on task reports; supplied stop errors still stop, unknown errors reconcile, and next-eligible provider errors must use outcome:"failed". Never relabel cancellation, exhausted bounds, a bare wait timeout or supported disagreement as task failure. Failed handoff requires concrete reconciliation evidence and the recorded agentId for a stopped child. Inspect changes and stop all prior work before retrying. A ready result requires a new claim. Status never authorizes a second spawn. main-direct returns remaining work to the main agent; independent review still requires independent evidence. stop/reconcile never authorizes another model or direct execution. OCX owns provider retries; CXC selects at most two native attempts. Do not invent error codes from arbitrary prose; preserve structured errors or canonical transport error text. Explicit caller model overrides and full-history forks are outside this managed path. Never use a dispatch marker to bypass native permissions.`; export function sessionFallbackNotice(cwd ) { const roles = readConfig(cwd).roles; diff --git a/plugins/codexclaw/components/subagent-config/dist/fallback-dispatch.js b/plugins/codexclaw/components/subagent-config/dist/fallback-dispatch.js index 3a26c444..8659668d 100644 --- a/plugins/codexclaw/components/subagent-config/dist/fallback-dispatch.js +++ b/plugins/codexclaw/components/subagent-config/dist/fallback-dispatch.js @@ -8,6 +8,9 @@ import { renameWithRetry } from "./atomic-write.js"; import { decodeDispatchFailure } from "./fallback-errors.js"; +/** Confirmed task failure is main's judgment with bounded observational evidence, never a provider code. */ + + @@ -57,6 +60,13 @@ function smallText(value , field ) { if (typeof value !== "string" || !value.trim() || value.length > 2000) throw new Error(`invalid ${field}`); return value.trim(); } +const TASK_FAILURE_KINDS = new Set(["stagnation", "unusable_output"]); +function taskFailure(raw ) { + const t = record(raw); + for (const key of Object.keys(t)) if (key !== "kind" && key !== "evidence") throw new Error("invalid taskFailure key"); + if (!TASK_FAILURE_KINDS.has(t.kind )) throw new Error("invalid taskFailure kind"); + return { kind: t.kind , evidence: smallText(t.evidence, "taskFailure evidence") }; +} function directory(cwd , sessionId ) { let dir = cwd; for (const part of [".codexclaw", "dispatches", sessionId]) { @@ -89,6 +99,8 @@ function readState(path , sessionId , dispatchId ) for (const field of ["agentId", "observedModel", "code", "reconciliation", "toolUseId"]) { if (a[field] !== null && typeof a[field] !== "string") throw new Error(`invalid attempt ${field}`); } + // Version-1 records predate taskFailure; absent normalizes to null, malformed fails closed. + a.taskFailure = a.taskFailure === undefined || a.taskFailure === null ? null : taskFailure(a.taskFailure); } return d ; } @@ -100,7 +112,7 @@ function saveState(path , state ) { } finally { rmSync(temp, { force: true }); } } function attempt(c ) { - return { id: randomUUID(), candidate: c, claimed: false, agentId: null, observedModel: null, code: null, status: "ready", reconciliation: null, spawnIssued: false, toolUseId: null }; + return { id: randomUUID(), candidate: c, claimed: false, agentId: null, observedModel: null, code: null, taskFailure: null, status: "ready", reconciliation: null, spawnIssued: false, toolUseId: null }; } function result(d , action , reason ) { const a = d.attempts.at(-1) ; @@ -165,6 +177,7 @@ function report(d , b ) { if (!a.agentId || b.agentId !== a.agentId) throw new Error("complete requires the recorded agentId"); a.status = "complete"; d.status = "complete"; return result(d); } + if (b.outcome === "task_failed") return taskFailed(d, b); if (b.outcome !== "failed" && b.outcome !== "unavailable") throw new Error("invalid report outcome"); const failure = b.outcome === "unavailable" ? null : decodeDispatchFailure(b.error); a.code = failure?.code ?? null; @@ -182,12 +195,42 @@ function report(d , b ) { a.status = "failed"; d.status = "main-direct"; return result(d); } if (b.executionState === "stopped" && !a.agentId) throw new Error("record created agent before stopped handoff"); + return handoff(d); +} + +/** Bounded rotation shared by provider and task failures: next candidate, else main reclaims. */ +function handoff(d ) { + const a = d.attempts.at(-1) ; a.status = "failed"; if (d.attempts.length === d.candidates.length) { d.status = "main-direct"; return result(d); } d.attempts.push(attempt(d.candidates[d.attempts.length])); return result(d); } +/** A supplied provider error keeps its own decoding; without one the task-failure path runs. */ +function taskFailed(d , b ) { + const a = d.attempts.at(-1) ; + if (b.error !== undefined) { + const failure = decodeDispatchFailure(b.error); + a.code = failure.code; + if (failure.action === "stop") { d.status = "stopped"; return result(d, "stop", "failure does not permit model fallback"); } + if (failure.action === "unknown") { a.status = "reconcile"; return result(d, "reconcile", "error is unclassified; obtain structured OCX evidence, do not guess a code"); } + throw new Error("fallback-eligible provider error must report outcome failed, not task_failed"); + } + const failure = taskFailure(b.taskFailure); + if (b.executionState === "not_created") throw new Error("task failure requires a recorded stopped child"); + if (b.executionState !== "stopped") { + a.status = "reconcile"; return result(d, "reconcile", "confirm the child is stopped before task handoff"); + } + if (!a.agentId) throw new Error("record created agent before stopped handoff"); + if (b.agentId !== a.agentId) throw new Error("recorded child must be stopped and identified"); + a.reconciliation = smallText(b.reconciliation, "reconciliation evidence"); + // An accepted task failure supersedes any provider code parked by an earlier reconciled report. + a.code = null; + a.taskFailure = failure; + return handoff(d); +} + /** Marker resolution never creates a dispatch. The hook uses it to avoid primary effort reinjection. */ export function managedSpawn(cwd , sessionId , message ) { cwd = dispatchRoot(cwd); diff --git a/plugins/codexclaw/components/subagent-config/src/fallback-dispatch-cli.ts b/plugins/codexclaw/components/subagent-config/src/fallback-dispatch-cli.ts index 7b3d691e..6073da82 100644 --- a/plugins/codexclaw/components/subagent-config/src/fallback-dispatch-cli.ts +++ b/plugins/codexclaw/components/subagent-config/src/fallback-dispatch-cli.ts @@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url"; import { runDispatch } from "./fallback-dispatch.ts"; import { readConfig, ROLES } from "./store.ts"; -export const DISPATCH_GUIDANCE = `Roles with a first fallback use the main-owned dispatch protocol before native spawn. Run cxc subagents dispatch with one JSON object on stdin: {action:"start",sessionId:,dispatchId:,role:}. Then claim with {action:"claim",sessionId,dispatchId,attemptId}. Only action=spawn authorizes one native call; prepend its marker followed by a newline to the original task/skills, pass candidate model and effort when non-null, and use a fresh context. Report creation with {action:"report",outcome:"created",sessionId,dispatchId,attemptId,agentId:}, then use native wait. Report success with {action:"report",outcome:"complete",sessionId,dispatchId,attemptId,agentId}; creation is not completion. Report failure with action:"report",outcome:"failed", the same IDs, the original error and executionState (not_created/stopped/unknown/running). Failed handoff requires concrete reconciliation evidence and the recorded agentId for a stopped child. Inspect changes and stop all prior work before retrying. A ready result requires a new claim. Status never authorizes a second spawn. main-direct returns remaining work to the main agent; independent review still requires independent evidence. stop/reconcile never authorizes another model or direct execution. OCX owns provider retries; CXC selects at most two native attempts. Do not invent error codes from arbitrary prose; preserve structured errors or canonical transport error text. Explicit caller model overrides and full-history forks are outside this managed path. Never use a dispatch marker to bypass native permissions.`; +export const DISPATCH_GUIDANCE = `Roles with a first fallback use the main-owned dispatch protocol before native spawn. Run cxc subagents dispatch with one JSON object on stdin: {action:"start",sessionId:,dispatchId:,role:}. Then claim with {action:"claim",sessionId,dispatchId,attemptId}. Only action=spawn authorizes one native call; prepend its marker followed by a newline to the original task/skills, pass candidate model and effort when non-null, and use a fresh context. Report creation with {action:"report",outcome:"created",sessionId,dispatchId,attemptId,agentId:}, then use native wait. Report success with {action:"report",outcome:"complete",sessionId,dispatchId,attemptId,agentId} only after validating final work; native completion alone is not task success and terminal reports cannot reopen. Report provider failure with action:"report",outcome:"failed", the same IDs, the original error and executionState (not_created/stopped/unknown/running). For confirmed stagnation or unusable final output, report outcome:"task_failed" with taskFailure:{kind:"stagnation"|"unusable_output",evidence:}, the same IDs, executionState:"stopped", and reconciliation:. Task evidence and reconciliation must each be non-empty text of at most 2000 characters; taskFailure permits only kind and evidence. Omit error on task reports; supplied stop errors still stop, unknown errors reconcile, and next-eligible provider errors must use outcome:"failed". Never relabel cancellation, exhausted bounds, a bare wait timeout or supported disagreement as task failure. Failed handoff requires concrete reconciliation evidence and the recorded agentId for a stopped child. Inspect changes and stop all prior work before retrying. A ready result requires a new claim. Status never authorizes a second spawn. main-direct returns remaining work to the main agent; independent review still requires independent evidence. stop/reconcile never authorizes another model or direct execution. OCX owns provider retries; CXC selects at most two native attempts. Do not invent error codes from arbitrary prose; preserve structured errors or canonical transport error text. Explicit caller model overrides and full-history forks are outside this managed path. Never use a dispatch marker to bypass native permissions.`; export function sessionFallbackNotice(cwd: string): string { const roles = readConfig(cwd).roles; diff --git a/plugins/codexclaw/components/subagent-config/src/fallback-dispatch.ts b/plugins/codexclaw/components/subagent-config/src/fallback-dispatch.ts index aa8f5dd1..8f03f3e4 100644 --- a/plugins/codexclaw/components/subagent-config/src/fallback-dispatch.ts +++ b/plugins/codexclaw/components/subagent-config/src/fallback-dispatch.ts @@ -8,6 +8,8 @@ import { renameWithRetry } from "./atomic-write.ts"; import { decodeDispatchFailure } from "./fallback-errors.ts"; export interface Candidate { model: string | null; effort: EffortName | null; } +/** Confirmed task failure is main's judgment with bounded observational evidence, never a provider code. */ +interface TaskFailure { kind: "stagnation" | "unusable_output"; evidence: string; } interface Attempt { id: string; candidate: Candidate; @@ -15,6 +17,7 @@ interface Attempt { agentId: string | null; observedModel: string | null; code: string | null; + taskFailure: TaskFailure | null; status: "ready" | "claimed" | "running" | "reconcile" | "failed" | "complete"; reconciliation: string | null; spawnIssued: boolean; @@ -57,6 +60,13 @@ function smallText(value: unknown, field: string): string { if (typeof value !== "string" || !value.trim() || value.length > 2000) throw new Error(`invalid ${field}`); return value.trim(); } +const TASK_FAILURE_KINDS = new Set(["stagnation", "unusable_output"]); +function taskFailure(raw: unknown): TaskFailure { + const t = record(raw); + for (const key of Object.keys(t)) if (key !== "kind" && key !== "evidence") throw new Error("invalid taskFailure key"); + if (!TASK_FAILURE_KINDS.has(t.kind as string)) throw new Error("invalid taskFailure kind"); + return { kind: t.kind as TaskFailure["kind"], evidence: smallText(t.evidence, "taskFailure evidence") }; +} function directory(cwd: string, sessionId: string): string { let dir = cwd; for (const part of [".codexclaw", "dispatches", sessionId]) { @@ -89,6 +99,8 @@ function readState(path: string, sessionId: string, dispatchId: string): Dispatc for (const field of ["agentId", "observedModel", "code", "reconciliation", "toolUseId"]) { if (a[field] !== null && typeof a[field] !== "string") throw new Error(`invalid attempt ${field}`); } + // Version-1 records predate taskFailure; absent normalizes to null, malformed fails closed. + a.taskFailure = a.taskFailure === undefined || a.taskFailure === null ? null : taskFailure(a.taskFailure); } return d as unknown as Dispatch; } @@ -100,7 +112,7 @@ function saveState(path: string, state: Dispatch): void { } finally { rmSync(temp, { force: true }); } } function attempt(c: Candidate): Attempt { - return { id: randomUUID(), candidate: c, claimed: false, agentId: null, observedModel: null, code: null, status: "ready", reconciliation: null, spawnIssued: false, toolUseId: null }; + return { id: randomUUID(), candidate: c, claimed: false, agentId: null, observedModel: null, code: null, taskFailure: null, status: "ready", reconciliation: null, spawnIssued: false, toolUseId: null }; } function result(d: Dispatch, action?: DispatchResult["action"], reason?: string): DispatchResult { const a = d.attempts.at(-1)!; @@ -165,6 +177,7 @@ function report(d: Dispatch, b: Record): DispatchResult { if (!a.agentId || b.agentId !== a.agentId) throw new Error("complete requires the recorded agentId"); a.status = "complete"; d.status = "complete"; return result(d); } + if (b.outcome === "task_failed") return taskFailed(d, b); if (b.outcome !== "failed" && b.outcome !== "unavailable") throw new Error("invalid report outcome"); const failure = b.outcome === "unavailable" ? null : decodeDispatchFailure(b.error); a.code = failure?.code ?? null; @@ -182,12 +195,42 @@ function report(d: Dispatch, b: Record): DispatchResult { a.status = "failed"; d.status = "main-direct"; return result(d); } if (b.executionState === "stopped" && !a.agentId) throw new Error("record created agent before stopped handoff"); + return handoff(d); +} + +/** Bounded rotation shared by provider and task failures: next candidate, else main reclaims. */ +function handoff(d: Dispatch): DispatchResult { + const a = d.attempts.at(-1)!; a.status = "failed"; if (d.attempts.length === d.candidates.length) { d.status = "main-direct"; return result(d); } d.attempts.push(attempt(d.candidates[d.attempts.length])); return result(d); } +/** A supplied provider error keeps its own decoding; without one the task-failure path runs. */ +function taskFailed(d: Dispatch, b: Record): DispatchResult { + const a = d.attempts.at(-1)!; + if (b.error !== undefined) { + const failure = decodeDispatchFailure(b.error); + a.code = failure.code; + if (failure.action === "stop") { d.status = "stopped"; return result(d, "stop", "failure does not permit model fallback"); } + if (failure.action === "unknown") { a.status = "reconcile"; return result(d, "reconcile", "error is unclassified; obtain structured OCX evidence, do not guess a code"); } + throw new Error("fallback-eligible provider error must report outcome failed, not task_failed"); + } + const failure = taskFailure(b.taskFailure); + if (b.executionState === "not_created") throw new Error("task failure requires a recorded stopped child"); + if (b.executionState !== "stopped") { + a.status = "reconcile"; return result(d, "reconcile", "confirm the child is stopped before task handoff"); + } + if (!a.agentId) throw new Error("record created agent before stopped handoff"); + if (b.agentId !== a.agentId) throw new Error("recorded child must be stopped and identified"); + a.reconciliation = smallText(b.reconciliation, "reconciliation evidence"); + // An accepted task failure supersedes any provider code parked by an earlier reconciled report. + a.code = null; + a.taskFailure = failure; + return handoff(d); +} + /** Marker resolution never creates a dispatch. The hook uses it to avoid primary effort reinjection. */ export function managedSpawn(cwd: string, sessionId: string, message: string): { candidate: Candidate; role: RoleName } | null { cwd = dispatchRoot(cwd); diff --git a/plugins/codexclaw/components/subagent-config/test/fallback-dispatch-cli.test.ts b/plugins/codexclaw/components/subagent-config/test/fallback-dispatch-cli.test.ts index 498dc502..d2d8ae80 100644 --- a/plugins/codexclaw/components/subagent-config/test/fallback-dispatch-cli.test.ts +++ b/plugins/codexclaw/components/subagent-config/test/fallback-dispatch-cli.test.ts @@ -1,6 +1,6 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { mkdtempSync, writeFileSync } from "node:fs"; +import { mkdtempSync, readFileSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; @@ -43,6 +43,36 @@ test("real CLI refuses corrupt state and invalid JSON rather than resetting it", const out = run(JSON.stringify({ ...base, action: "status" })); assert.equal(out.status, 1); assert.match(out.stdout, /invalid dispatch identity/); }); +test("real CLI recovers confirmed task failures across separate processes", () => { + const cwd = mkdtempSync(join(tmpdir(), "cxc-dispatch-cli-task-")); + const { CODEX_THREAD_ID: _nativeSession, ...inherited } = process.env; + const env = { ...inherited, CODEXCLAW_HOME: join(cwd, "global") }; + setRole(cwd, "executor", { mode: "model", model: "xai/grok-4.6", fallback: { model: "cursor/grok-4.6", effort: "low" } }, "project", env); + const call = (input: unknown) => { + const child = spawnSync(process.execPath, [cli], { cwd, env, input: JSON.stringify(input), encoding: "utf8" }); + assert.equal(child.status, 0, child.stdout + child.stderr); + return child.stdout ? JSON.parse(child.stdout) : null; + }; + const base = { sessionId: "fixture", dispatchId: "task" }; + const first = call({ ...base, action: "start", role: "executor" }); + call({ ...base, action: "claim", attemptId: first.attemptId }); + call({ ...base, action: "report", attemptId: first.attemptId, outcome: "created", agentId: "child-a" }); + const second = call({ ...base, action: "report", attemptId: first.attemptId, outcome: "task_failed", agentId: "child-a", executionState: "stopped", reconciliation: "child stopped; diff inspected", taskFailure: { kind: "unusable_output", evidence: "final message unrelated to the packet" } }); + assert.equal(second.action, "ready"); + assert.equal(second.attempts[0].taskFailure?.kind, "unusable_output"); + assert.equal(second.attempts[0].code, null); + const claim = call({ ...base, action: "claim", attemptId: second.attemptId }); + assert.equal(claim.candidate.model, "cursor/grok-4.6"); + call({ ...base, action: "report", attemptId: second.attemptId, outcome: "created", agentId: "child-b" }); + const end = call({ ...base, action: "report", attemptId: second.attemptId, outcome: "task_failed", agentId: "child-b", executionState: "stopped", reconciliation: "second child stopped; partial work preserved", taskFailure: { kind: "stagnation", evidence: "no advancement at the stated review point" } }); + assert.equal(end.action, "main-direct"); + const status = call({ ...base, action: "status" }); + assert.equal(status.action, "main-direct"); + assert.equal(status.attempts[1].taskFailure?.kind, "stagnation"); + const raw = JSON.parse(readFileSync(join(cwd, ".codexclaw", "dispatches", "fixture", "task.json"), "utf8")); + assert.equal(raw.attempts[0].taskFailure.kind, "unusable_output"); +}); + test("malformed startup payload is silent, malformed dispatch input is visible", () => { const hook = spawnSync(process.execPath, [cli, "hook", "session-start"], { input: "", encoding: "utf8" }); assert.equal(hook.status, 0); assert.equal(hook.stdout, ""); diff --git a/plugins/codexclaw/components/subagent-config/test/fallback-dispatch.test.ts b/plugins/codexclaw/components/subagent-config/test/fallback-dispatch.test.ts index e41bc7ad..ee202170 100644 --- a/plugins/codexclaw/components/subagent-config/test/fallback-dispatch.test.ts +++ b/plugins/codexclaw/components/subagent-config/test/fallback-dispatch.test.ts @@ -1,6 +1,6 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { mkdtempSync, readFileSync, mkdirSync } from "node:fs"; +import { mkdtempSync, readFileSync, writeFileSync, mkdirSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { runDispatch, managedSpawn } from "../src/fallback-dispatch.ts"; @@ -170,3 +170,177 @@ test("candidate snapshot remains stable if role settings change after start", () const next = call({ action: "report", attemptId: start.attemptId, outcome: "failed", error: "rate_limit_exceeded", executionState: "not_created", reconciliation: "native creation returned no child" }); assert.equal(call({ action: "claim", attemptId: next.attemptId }).candidate?.model, "cursor/grok-4.6"); }); + +test("confirmed task failure recovers through the next candidate, then main-direct", () => { + const { call, start } = fixture(); + call({ action: "claim", attemptId: start.attemptId }); + call({ action: "report", attemptId: start.attemptId, outcome: "created", agentId: "child-a" }); + const next = call({ + action: "report", attemptId: start.attemptId, outcome: "task_failed", + agentId: "child-a", executionState: "stopped", + reconciliation: "child stopped; inspected diff and preserved edits", + taskFailure: { kind: "unusable_output", evidence: "final message contained no findings or diff for the packet" }, + }); + assert.equal(next.action, "ready"); + assert.equal(next.attempts[0].taskFailure?.kind, "unusable_output"); + assert.equal(next.attempts[0].code, null); + const claim = call({ action: "claim", attemptId: next.attemptId }); + assert.equal(claim.candidate?.model, "cursor/grok-4.6"); + call({ action: "report", attemptId: next.attemptId, outcome: "created", agentId: "child-b" }); + const end = call({ + action: "report", attemptId: next.attemptId, outcome: "task_failed", + agentId: "child-b", executionState: "stopped", + reconciliation: "second child stopped; partial work inspected", + taskFailure: { kind: "stagnation", evidence: "no new edits or output at the stated review point" }, + }); + assert.equal(end.action, "main-direct"); + assert.equal(end.attempts.length, 2); + assert.equal(end.attempts[1].taskFailure?.kind, "stagnation"); +}); + +for (const role of ROLES) test(`${role}: task failure on the last candidate returns main-direct`, () => { + const { call, start } = fixture(role); + call({ action: "claim", attemptId: start.attemptId }); + call({ action: "report", attemptId: start.attemptId, outcome: "created", agentId: "child-a" }); + const next = call({ action: "report", attemptId: start.attemptId, outcome: "task_failed", agentId: "child-a", executionState: "stopped", reconciliation: "child stopped; partial edits inspected", taskFailure: { kind: "stagnation", evidence: "no advancement at the stated review point" } }); + const second = call({ action: "claim", attemptId: next.attemptId }); + call({ action: "report", attemptId: second.attemptId, outcome: "created", agentId: "child-b" }); + const end = call({ action: "report", attemptId: second.attemptId, outcome: "task_failed", agentId: "child-b", executionState: "stopped", reconciliation: "second child stopped; output unusable", taskFailure: { kind: "unusable_output", evidence: "final message unrelated to the packet" } }); + assert.equal(end.action, "main-direct"); + assert.equal(end.attempts.length, 2); + assert.equal(end.independentReviewRequired, role === "reviewer"); +}); + +test("dispatch state without taskFailure stays readable and recovers; malformed metadata fails closed", () => { + const { call, start, cwd, base } = fixture(); + call({ action: "claim", attemptId: start.attemptId }); + call({ action: "report", attemptId: start.attemptId, outcome: "created", agentId: "child-a" }); + const path = join(cwd, ".codexclaw", "dispatches", base.sessionId, base.dispatchId + ".json"); + const legacy = JSON.parse(readFileSync(path, "utf8")); + delete legacy.attempts[0].taskFailure; + writeFileSync(path, JSON.stringify(legacy)); + assert.equal(call({ action: "status" }).attempts[0].taskFailure, null); + const next = call({ action: "report", attemptId: start.attemptId, outcome: "task_failed", agentId: "child-a", executionState: "stopped", reconciliation: "child stopped; inspected", taskFailure: { kind: "stagnation", evidence: "no advancement at the stated review point" } }); + assert.equal(next.action, "ready"); + const corrupt = JSON.parse(readFileSync(path, "utf8")); + corrupt.attempts[0].taskFailure = { kind: "timeout", evidence: "x" }; + writeFileSync(path, JSON.stringify(corrupt)); + assert.throws(() => call({ action: "status" }), /taskFailure kind/); +}); + +test("task failure needs a recorded stopped child before any handoff", () => { + const { call, start } = fixture(); + call({ action: "claim", attemptId: start.attemptId }); + call({ action: "report", attemptId: start.attemptId, outcome: "created", agentId: "child-a" }); + const taskFailure = { kind: "unusable_output", evidence: "final message unusable" }; + for (const executionState of ["running", "unknown"]) { + const out = call({ action: "report", attemptId: start.attemptId, outcome: "task_failed", agentId: "child-a", executionState, reconciliation: "still checking", taskFailure }); + assert.equal(out.action, "reconcile"); + assert.equal(out.attempts.length, 1); + } + assert.throws(() => call({ action: "report", attemptId: start.attemptId, outcome: "task_failed", executionState: "not_created", reconciliation: "x", taskFailure }), /recorded stopped child/); + assert.throws(() => call({ action: "report", attemptId: start.attemptId, outcome: "task_failed", agentId: "child-b", executionState: "stopped", reconciliation: "x", taskFailure }), /stopped and identified/); + assert.throws(() => call({ action: "report", attemptId: start.attemptId, outcome: "task_failed", agentId: "child-a", executionState: "stopped", taskFailure }), /reconciliation/); + const next = call({ action: "report", attemptId: start.attemptId, outcome: "task_failed", agentId: "child-a", executionState: "stopped", reconciliation: "child stopped; inspected", taskFailure }); + assert.equal(next.action, "ready"); +}); + +test("task failure without a recorded child is rejected", () => { + const { call, start } = fixture(); + call({ action: "claim", attemptId: start.attemptId }); + assert.throws(() => call({ action: "report", attemptId: start.attemptId, outcome: "task_failed", agentId: "ghost", executionState: "stopped", reconciliation: "claimed child stopped", taskFailure: { kind: "stagnation", evidence: "no output" } }), /record created agent/); +}); + +test("invalid taskFailure payloads are rejected without state advancement", () => { + const { call, start } = fixture(); + call({ action: "claim", attemptId: start.attemptId }); + call({ action: "report", attemptId: start.attemptId, outcome: "created", agentId: "child-a" }); + const report = { action: "report", attemptId: start.attemptId, outcome: "task_failed", agentId: "child-a", executionState: "stopped", reconciliation: "child stopped; inspected" }; + assert.throws(() => call({ ...report }), /JSON object/); + assert.throws(() => call({ ...report, taskFailure: "stagnation" }), /JSON object/); + assert.throws(() => call({ ...report, taskFailure: { kind: "stagnation", evidence: "x", extra: 1 } }), /taskFailure key/); + assert.throws(() => call({ ...report, taskFailure: { kind: "timeout", evidence: "x" } }), /taskFailure kind/); + assert.throws(() => call({ ...report, taskFailure: { kind: "stagnation", evidence: " " } }), /evidence/); + assert.throws(() => call({ ...report, taskFailure: { kind: "stagnation", evidence: 42 } }), /evidence/); + assert.throws(() => call({ ...report, taskFailure: { kind: "stagnation", evidence: "x".repeat(2001) } }), /evidence/); + const status = call({ action: "status" }); + assert.equal(status.attempts.length, 1); + assert.equal(status.attempts[0].status, "running"); + assert.equal(status.attempts[0].taskFailure, null); +}); + +test("provider error decoding wins over the task_failed label", () => { + // A decoded stop wins before the child-state gate, even while the child is live. + for (const [error, code, executionState] of [ + [{ code: "permission_denied" }, "permission_denied", "running"], + ["client_cancelled", "client_cancelled", "unknown"], + [{ code: "cyber_policy" }, "cyber_policy", "running"], + ] as const) { + const { call, start } = fixture(); + call({ action: "claim", attemptId: start.attemptId }); + call({ action: "report", attemptId: start.attemptId, outcome: "created", agentId: "child-a" }); + const stop = call({ action: "report", attemptId: start.attemptId, outcome: "task_failed", error, agentId: "child-a", executionState, reconciliation: "still live", taskFailure: { kind: "stagnation", evidence: "x" } }); + assert.equal(stop.action, "stop"); + assert.equal(stop.attempts[0].code, code); + const again = call({ action: "report", attemptId: start.attemptId, outcome: "task_failed", agentId: "child-a", executionState: "stopped", reconciliation: "x", taskFailure: { kind: "stagnation", evidence: "x" } }); + assert.equal(again.action, "stop"); + assert.equal(again.attempts.length, 1); + } + + const unknown = fixture(); + unknown.call({ action: "claim", attemptId: unknown.start.attemptId }); + unknown.call({ action: "report", attemptId: unknown.start.attemptId, outcome: "created", agentId: "child-a" }); + const rec = unknown.call({ action: "report", attemptId: unknown.start.attemptId, outcome: "task_failed", error: "vague prose", agentId: "child-a", executionState: "stopped", reconciliation: "x", taskFailure: { kind: "stagnation", evidence: "x" } }); + assert.equal(rec.action, "reconcile"); + assert.equal(rec.attempts.length, 1); + + const mixed = fixture(); + mixed.call({ action: "claim", attemptId: mixed.start.attemptId }); + mixed.call({ action: "report", attemptId: mixed.start.attemptId, outcome: "created", agentId: "child-a" }); + assert.throws(() => mixed.call({ action: "report", attemptId: mixed.start.attemptId, outcome: "task_failed", error: "insufficient_quota", agentId: "child-a", executionState: "stopped", reconciliation: "x", taskFailure: { kind: "stagnation", evidence: "x" } }), /outcome failed/); + const next = mixed.call({ action: "report", attemptId: mixed.start.attemptId, outcome: "failed", error: "insufficient_quota", agentId: "child-a", executionState: "stopped", reconciliation: "child stopped; inspected" }); + assert.equal(next.action, "ready"); + assert.equal(next.attempts[0].code, "insufficient_quota"); +}); + +test("a plain failed report with task metadata never implies task recovery", () => { + const { call, start } = fixture(); + call({ action: "claim", attemptId: start.attemptId }); + call({ action: "report", attemptId: start.attemptId, outcome: "created", agentId: "child-a" }); + const out = call({ action: "report", attemptId: start.attemptId, outcome: "failed", agentId: "child-a", executionState: "stopped", reconciliation: "child stopped; inspected", taskFailure: { kind: "unusable_output", evidence: "no usable output" } }); + assert.equal(out.action, "reconcile"); + assert.equal(out.attempts.length, 1); + assert.equal(out.attempts[0].taskFailure, null); +}); + +test("terminal dispatch states never reopen for a task report", () => { + const { call, start } = fixture(); + call({ action: "claim", attemptId: start.attemptId }); + call({ action: "report", attemptId: start.attemptId, outcome: "created", agentId: "child-a" }); + call({ action: "report", attemptId: start.attemptId, outcome: "complete", agentId: "child-a" }); + const out = call({ action: "report", attemptId: start.attemptId, outcome: "task_failed", agentId: "child-a", executionState: "stopped", reconciliation: "x", taskFailure: { kind: "stagnation", evidence: "x" } }); + assert.equal(out.action, "complete"); + assert.equal(out.attempts.length, 1); +}); + +test("task failure before claim or on a stale attempt is rejected", () => { + const { call, start } = fixture(); + assert.throws(() => call({ action: "report", attemptId: start.attemptId, outcome: "task_failed", executionState: "stopped", reconciliation: "x", taskFailure: { kind: "stagnation", evidence: "x" } }), /claim the attempt/); + call({ action: "claim", attemptId: start.attemptId }); + call({ action: "report", attemptId: start.attemptId, outcome: "created", agentId: "child-a" }); + const next = call({ action: "report", attemptId: start.attemptId, outcome: "task_failed", agentId: "child-a", executionState: "stopped", reconciliation: "child stopped; inspected", taskFailure: { kind: "stagnation", evidence: "no advancement at the stated review point" } }); + assert.equal(next.action, "ready"); + assert.throws(() => call({ action: "report", attemptId: start.attemptId, outcome: "task_failed", agentId: "child-a", executionState: "stopped", reconciliation: "x", taskFailure: { kind: "stagnation", evidence: "x" } }), /stale/); +}); +test("a reconciled provider report followed by task failure clears the stale code", () => { + const { call, start } = fixture(); + call({ action: "claim", attemptId: start.attemptId }); + call({ action: "report", attemptId: start.attemptId, outcome: "created", agentId: "child-a" }); + const rec = call({ action: "report", attemptId: start.attemptId, outcome: "failed", error: "insufficient_quota", agentId: "child-a", executionState: "running" }); + assert.equal(rec.action, "reconcile"); + assert.equal(rec.attempts[0].code, "insufficient_quota"); + const next = call({ action: "report", attemptId: start.attemptId, outcome: "task_failed", agentId: "child-a", executionState: "stopped", reconciliation: "child stopped; inspected", taskFailure: { kind: "unusable_output", evidence: "final message unusable" } }); + assert.equal(next.action, "ready"); + assert.equal(next.attempts[0].code, null); + assert.equal(next.attempts[0].taskFailure?.kind, "unusable_output"); +}); diff --git a/plugins/codexclaw/skills/loop/references/waiting.md b/plugins/codexclaw/skills/loop/references/waiting.md index ba4fdc45..3afbfe3d 100644 --- a/plugins/codexclaw/skills/loop/references/waiting.md +++ b/plugins/codexclaw/skills/loop/references/waiting.md @@ -84,3 +84,11 @@ reclaims the work, and `reconcile`/`stop` authorize neither a replacement spawn nor direct execution. Cancellation or an exhausted bound grants no continuation: stop within authority and report the cancellation or bound, never as a provider failure. + +For managed stagnation or unusable final output, report `outcome:task_failed` +with the matching `taskFailure.kind`, concrete `taskFailure.evidence`, the +recorded child ID, `executionState:stopped` and termination/partial-work +`reconciliation`. Follow the [report contract](../../pabcd/references/delegation.md#configured-first-fallback). +Provider errors use `outcome:failed`; do not invent a provider code for a task +failure or label cancellation or exhausted bounds as stagnation. Validate the +final work before reporting `outcome:complete`, which closes the dispatch. diff --git a/plugins/codexclaw/skills/pabcd/references/delegation.md b/plugins/codexclaw/skills/pabcd/references/delegation.md index fff7deb4..44d68959 100644 --- a/plugins/codexclaw/skills/pabcd/references/delegation.md +++ b/plugins/codexclaw/skills/pabcd/references/delegation.md @@ -283,9 +283,9 @@ protocol. A PreToolUse reminder after a direct call cannot retroactively manage candidate's model/effort (null inherits the original session). Preserve the role. 3. Every report includes `sessionId`, `dispatchId`, and the current `attemptId`. Report `outcome:created` and the actual `agentId`, then use native wait. Report - `outcome:complete` with that ID on successful completion. Do not confuse a - successful spawn with successful work. -4. On failure report `outcome:failed`, the original `error`, and `executionState`: + `outcome:complete` with that ID only after validating the final work. A native + completed status does not prove the task succeeded; terminal reports cannot be reopened. +4. On provider failure report `outcome:failed`, the original `error`, and `executionState`: `not_created`, `stopped`, `unknown`, or `running`. Known no-child failures need concrete `reconciliation` evidence. A stopped child requires its recorded `agentId` and evidence that work/processes stopped and changes were inspected; @@ -294,11 +294,30 @@ protocol. A PreToolUse reminder after a direct call cannot retroactively manage to the replacement. Unknown outcomes never authorize another child. If native spawn is absent, report `outcome:unavailable` with confirmed `not_created` and capability evidence, never a policy denial. + For confirmed stagnation or unusable final output, use `outcome:task_failed` + with `taskFailure: {kind: "stagnation" | "unusable_output", evidence: "..."}`. + This requires a recorded child, `executionState:stopped`, matching `agentId` + and `reconciliation`; running or unknown work must be reconciled first. + Task evidence explains the failure; reconciliation explains termination and + partial-work inspection. Both are non-empty text of at most 2000 characters. + No other task kinds or taskFailure keys are accepted. Never label cancellation, + exhausted bounds, a wait timeout alone or a supported disagreement as task failure. 5. `ready` means claim the next attempt. `main-direct` means main reclaims the remaining work; `independentReviewRequired` stays true for reviewer tasks. Main implementation is never independent review. `stop` or `reconcile` means no model switch or direct-execution permission. Inspect the reason and state. +A task-failure report has no provider `error`; for example: + +```json +{"action":"report","outcome":"task_failed","sessionId":"","dispatchId":"","attemptId":"","agentId":"","executionState":"stopped","taskFailure":{"kind":"unusable_output","evidence":"Final answer addresses a different task; the required result is absent."},"reconciliation":"Verified terminal child, no owned processes, and inspected partial edits."} +``` + +A supplied provider error retains precedence: stop errors stop and unknown errors +reconcile; next-eligible provider errors must use `outcome:failed` instead of a mixed +report. Accepted task failures record `taskFailure` and clear the attempt's provider +`code`. These observations are main's assertions, not authenticated native receipts. + Use `action:status` to recover after interruption. It never reissues an executable spawn. A claimed attempt with a lost response must be reconciled, not claimed again. Do not remove locks to make a retry work. If a lock survives a crashed diff --git a/structure/INDEX.md b/structure/INDEX.md index 0b175db8..76e11a7d 100644 --- a/structure/INDEX.md +++ b/structure/INDEX.md @@ -265,6 +265,7 @@ though they have no package-local `test` script. This asymmetry is intentional, | `cxc loop` | `components/pabcd-state/dist/cli.js loop` | initializes, shows, or validates the project-local goalplan substrate | | `cxc goalplan` | `components/pabcd-state/dist/cli.js goalplan` | deprecated alias for `cxc loop` | | `cxc subagents` | `components/subagent-config/dist/cli.js` (list/get/set) | reads/writes the per-role `.codexclaw/subagents.json` model+effort+prompt config | +| `cxc subagents dispatch` | `components/subagent-config/dist/fallback-dispatch-cli.js` | records main-owned claims and outcomes; provider failures and explicit `task_failed` evidence use bounded handoff after child reconciliation; never invokes a model itself | | `cxc provider` | `components/provider-bridge/dist/cli.js` (detect) | read-only ocx provider detect/status; never mutates provider state | | `cxc serve` | `components/messenger-bridge/dist/cli.js serve` | runs the loopback bridge server for the GUI, JSON API, and messenger adapters | | `cxc service` | `components/messenger-bridge/dist/cli.js service` | installs, uninstalls, or reports the macOS launchd daemon for `cxc serve` |