From e9c4484d244b0f70a46edd820318ea3750146c34 Mon Sep 17 00:00:00 2001
From: thisisjun786 <259586770+thisisjun786@users.noreply.github.com>
Date: Wed, 16 Sep 2026 02:27:53 +0900
Subject: [PATCH 1/4] [agent] docs(plan): consolidate architect and selective
execution policies
---
.../260916_selective_execution/000_plan.md | 80 +++++++++++++++++++
1 file changed, 80 insertions(+)
create mode 100644 devlog/_plan/260916_selective_execution/000_plan.md
diff --git a/devlog/_plan/260916_selective_execution/000_plan.md b/devlog/_plan/260916_selective_execution/000_plan.md
new file mode 100644
index 00000000..c0fcc025
--- /dev/null
+++ b/devlog/_plan/260916_selective_execution/000_plan.md
@@ -0,0 +1,80 @@
+# Architect consultation and selective execution
+
+The owning implementation task should be able to develop directly. Executor-first
+wording in PR #177 made this an exception even when another coordination layer
+already delegated the issue to an independent task. Consolidate #177 and #179,
+retain required formal-P architect consultation and independent review, and use
+executors selectively for independent parallel work or bounded routine work.
+
+## Scope and execution
+
+Class: C3 policy/hint integration; previously reviewed ciphertext and recovery
+implementations are carried unchanged. Compact plan, architect consultation,
+independent audit, main implementation, checks and delivery; no new runtime gate.
+Loop archetype: satisfy-spec. Trigger: explicit request to consolidate both PRs,
+withdraw executor-first policy, and organize related issues. Goal: one replacement
+PR plus matching local source and installed payload. Non-goals: upstream merge,
+release, model-setting changes, or automatic patch reapplication.
+Verifier: existing phase hook/CLI tests observe injected instructions and actual
+phase state; recovery/ciphertext regressions protect the retained behavior; build,
+full root suite, gate and inventory check integration. Baseline phase tests at
+19afc764: 185 pass, 0 fail. Policy semantics require independent source review.
+Stop: replacement PR published and checked, predecessor PRs closed with links,
+issue #178 updated, local/installed state verified and patch backup preserved.
+Memory artifact: this unit; raw receipts remain outside Git. Expected outcomes:
+verified delivery, or a concrete unresolved blocker. Escalation: scope beyond the
+authorized repositories/install paths; no arbitrary time or token budget is set.
+
+## Decisions and file map
+
+Main implements all edits; architect and reviewer are read-only. The user explicitly
+selected direct implementation with optional executors, so this task needs no
+executor. Original branch histories and local edits remain preserved.
+
+- D1 accepted: `skills/dev/SKILL.md` Implementation delegation defaults to direct
+ work by the owning task's main, including an independently delegated task.
+ Executor use is optional for disjoint parallel or bounded routine work. No
+ justification is required for direct work. Keep headings/links, scoped packets,
+ diff verification, and dispatch safety. State the child-task rule generically;
+ do not couple CXC to Linear or a model vendor.
+- D1 accepted: `skills/pabcd/references/plan-output.md` records executor assignments
+ only when selected; main owns other in-scope work without a special exception.
+ B follows assigned scopes and new handoffs remain plan amendments. Update the B
+ sentence in `skills/pabcd/SKILL.md` consistently. Architect sections stay intact.
+- D2 accepted: `components/pabcd-state/src/hook.ts` and `orchestrate-cli.ts` P/B
+ hints say main implements by default and executor work is optional. Preserve
+ formal-P architect sequence, A recheck, user limits, and phase state behavior.
+ Build matching `dist/` files. No change to dispatch/permission algorithms.
+- D3 accepted with correction: update existing `test/hook.test.ts` and
+ `test/orchestrate-cli.test.ts` assertions on actual injected output. Preserve
+ all transition, independent-review and architect checks. Assertions do not
+ increase the test inventory; regenerate badges only from the measured total.
+ Synchronize `structure/20_pabcd_dispatch_doctrine.md` and `structure/INDEX.md`.
+- D4 accepted: carry the union at 19afc764 relative to upstream 03541398, excluding
+ old executor-first devlog additions. Preserve ciphertext guards, tests and
+ documentation; preserve `task_failed`, waiting and reconciliation contracts.
+ Add missing architect/optional-executor/ciphertext CHANGELOG entries and a short
+ public-guide explanation. Original author thisisjun786 retains credit in the
+ replacement PR. Old PRs and backup patches retain historical evidence.
+
+## Acceptance and delivery
+
+1. Direct single-task/independent-child implementation needs no executor or excuse.
+2. Optional parallel work has disjoint write scope; routine work has a clear check.
+3. Formal P requires architect proposal, main plan, same-architect reflection, then
+ independent A; C0/C1 and explicit user limits retain their existing precedence.
+4. Choosing optional dispatch never bypasses a live child's reconciliation or a
+ terminal stop; `main-direct` still governs managed reclaim.
+5. All carried runtime files match reviewed 19afc764 bytes. New P/B instructions are
+ exercised through existing hook and real CLI paths; no new automatic spawn.
+6. Publish one ordinary PR to dev. Only then link/close #177 and #179; organize
+ #178 with the replacement and current acceptance criteria, leave it open pending
+ upstream merge. Preserve remote/local predecessor branches.
+7. Apply the policy delta to local dev and the installed plugin with original-file
+ backups, drift checks, regenerated dist and actual installed CLI/hook checks.
+
+## Consultation
+
+Architect proposal D1-D4 received and dispositions recorded above. The same
+architect reviews this concrete file before implementation; independent audit
+follows. Proposal/response handles remain in local execution evidence.
From 0c52fe9b9927d95e5cae27429467ee38bd015310 Mon Sep 17 00:00:00 2001
From: thisisjun786 <259586770+thisisjun786@users.noreply.github.com>
Date: Wed, 16 Sep 2026 02:39:14 +0900
Subject: [PATCH 2/4] [agent] feat(delegation): retain architect review and
make executors optional
---
CHANGELOG.md | 31 +++
README.ko.md | 2 +-
README.md | 2 +-
README.zh.md | 2 +-
.../260916_selective_execution/000_plan.md | 6 +-
.../src/content/docs/guides/subagents.md | 43 +++--
.../components/pabcd-state/dist/hook.js | 4 +
.../pabcd-state/dist/orchestrate-cli.js | 20 +-
.../components/pabcd-state/src/hook.ts | 4 +
.../pabcd-state/src/orchestrate-cli.ts | 20 +-
.../components/pabcd-state/test/hook.test.ts | 53 ++++++
.../pabcd-state/test/orchestrate-cli.test.ts | 90 +++++++++
.../dist/fallback-dispatch-cli.js | 2 +-
.../subagent-config/dist/fallback-dispatch.js | 45 ++++-
.../subagent-config/dist/spawn-attach-hook.js | 68 +++++--
.../src/fallback-dispatch-cli.ts | 2 +-
.../subagent-config/src/fallback-dispatch.ts | 45 ++++-
.../subagent-config/src/spawn-attach-hook.ts | 68 +++++--
.../test/fallback-dispatch-cli.test.ts | 32 +++-
.../test/fallback-dispatch.test.ts | 176 +++++++++++++++++-
.../test/spawn-attach-hook.test.ts | 154 ++++++++++++++-
plugins/codexclaw/skills/dev/SKILL.md | 22 +++
.../skills/loop/references/waiting.md | 69 ++++++-
plugins/codexclaw/skills/pabcd/SKILL.md | 4 +-
.../skills/pabcd/references/delegation.md | 59 ++++--
.../skills/pabcd/references/phase-plan.md | 13 +-
.../skills/pabcd/references/plan-output.md | 37 +++-
plugins/codexclaw/skills/search/SKILL.md | 12 +-
plugins/codexclaw/test/hook-e2e.test.mjs | 26 ++-
structure/10_subagent_skill_routing.md | 16 +-
structure/20_pabcd_dispatch_doctrine.md | 42 +++--
structure/INDEX.md | 5 +-
32 files changed, 1059 insertions(+), 115 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 96dee162..c6bf242e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,37 @@ All notable changes to codexclaw are documented here. The format follows
## [Unreleased]
+### Changed
+
+- Formal P now requires an architect proposal, a main-owned executable plan and
+ reflection by the same architect before independent audit. C0/C1 fast paths and
+ explicit user limits keep their existing precedence; this adds guidance, not a
+ runtime gate.
+- The owning task's main agent implements directly by default, including inside
+ independently delegated tasks. Executors are optional for independent parallel
+ or bounded routine work. P records selected assignments; P/B hook and CLI hints
+ follow the same policy. Managed reclaim still requires `main-direct`.
+
+### Fixed
+
+- Native V2 spawn hooks preserve canonical Fernet-shaped task messages without
+ appending plaintext skill affordances or leaf guards. Structural validation
+ rejects malformed lookalikes; omission notices identify the guards that could
+ not be attached. Ciphertext recognition does not authenticate the message.
+
+- 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. 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
### Fixed
diff --git a/README.ko.md b/README.ko.md
index 35736a36..c86353c2 100644
--- a/README.ko.md
+++ b/README.ko.md
@@ -13,7 +13,7 @@
-
+
diff --git a/README.md b/README.md
index 2dcee674..7b670e9c 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@
-
+
diff --git a/README.zh.md b/README.zh.md
index f0a07a0b..0d455ff6 100644
--- a/README.zh.md
+++ b/README.zh.md
@@ -13,7 +13,7 @@
-
+
diff --git a/devlog/_plan/260916_selective_execution/000_plan.md b/devlog/_plan/260916_selective_execution/000_plan.md
index c0fcc025..3af44594 100644
--- a/devlog/_plan/260916_selective_execution/000_plan.md
+++ b/devlog/_plan/260916_selective_execution/000_plan.md
@@ -76,5 +76,7 @@ executor. Original branch histories and local edits remain preserved.
## Consultation
Architect proposal D1-D4 received and dispositions recorded above. The same
-architect reviews this concrete file before implementation; independent audit
-follows. Proposal/response handles remain in local execution evidence.
+architect returned ALIGNED against plan e9c4484d. Independent audit returned PASS
+with no blockers before implementation, retaining the public-guide clarification
+that managed reclaim requires `main-direct`. Proposal, reflection and audit
+receipts remain in private local execution evidence.
diff --git a/docs-site/src/content/docs/guides/subagents.md b/docs-site/src/content/docs/guides/subagents.md
index 482f1c2f..7bbdffc4 100644
--- a/docs-site/src/content/docs/guides/subagents.md
+++ b/docs-site/src/content/docs/guides/subagents.md
@@ -15,6 +15,15 @@ Four roles cover the common subagent workflow:
- **executor** — focused implementation.
- **architect** — read-only design proposals and checks of main-owned executable plans.
+The owning task's main agent implements directly by default, including when it
+receives an issue from another coordinator. Executors are optional for independent
+parallel work or bounded routine tasks with clear write scopes and checks.
+
+Formal P keeps the required design sequence: architect proposal, main's executable
+plan, then reflection by the same architect before independent review. The existing
+C0/C1 fast path and explicit user limits still apply. See the
+[workflow guide](/codexclaw/guides/pabcd/) for phase requirements.
+
## Native architect setup
Architect uses `agent_type: "architect"`, its own model/effort/prompt settings and
@@ -56,14 +65,15 @@ surfaces, but it does not choose skills. Dispatchers explicitly name each requir
skill with preferred `[$cxc-](skill://)` links or the plugin-native
`$codexclaw:cxc-` fallback. When the spawn message is plaintext, the hook normalizes
known broken/bare mentions and inlines recognized SKILL.md bodies on V2-shaped spawns.
-Native ChatGPT-backend V2 gives the hook ciphertext, so both operations are no-ops there;
-when no body can be inlined, it appends a plaintext `[CXC-SKILL-AFFORDANCE]` block telling
-the child to self-load any `$cxc-` / `$codexclaw:cxc-` mention from
-`//SKILL.md`; fork inheritance remains a secondary channel. Its other
-reliable native V2 channels are the leaf guard and omitted configured
-`model`/`reasoning_effort` injection for non-full-history spawns. It never invents role
-baselines or inferred surface skills. Role config, resolver, and spawn-wrapper are all
-shipped (L9).
+Native ChatGPT-backend V2 can give the hook ciphertext. Recognized canonical
+Fernet-shaped messages are preserved byte-for-byte: no skill text, affordance or
+leaf guard is appended to them. The hook reports the omitted prompt guards;
+structural checks identify an opaque frame but do not authenticate or decrypt it.
+For plaintext messages where no skill body can be inlined, the hook appends a
+`[CXC-SKILL-AFFORDANCE]` telling the child to self-load named skills. Configured
+omitted `model`/`reasoning_effort` injection remains available for non-full-history
+spawns. It never invents role baselines or inferred surface skills. Role config,
+resolver, and spawn-wrapper are all shipped (L9).
## First fallback
@@ -82,9 +92,11 @@ cxc subagents set executor --fallback-model cursor/grok-4.6 --fallback-effort hi
cxc subagents set executor --clear-fallback
```
-The same flags apply to explorer and reviewer. When both attempts fail, the main
-agent takes over remaining work. An independent review requirement remains
-outstanding; main-agent work does not satisfy it.
+The same flags apply to explorer, reviewer and architect. Only a returned
+`main-direct` action permits main to reclaim remaining work from a managed dispatch.
+A `ready` action requires a fresh claim; `stop` or `reconcile` permits neither a
+replacement nor direct takeover. Independent review remains outstanding when
+required; main-agent work does not satisfy it.
### Execution and evidence
@@ -106,6 +118,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/pabcd-state/dist/hook.js b/plugins/codexclaw/components/pabcd-state/dist/hook.js
index 3b1a36bd..d091c3bd 100644
--- a/plugins/codexclaw/components/pabcd-state/dist/hook.js
+++ b/plugins/codexclaw/components/pabcd-state/dist/hook.js
@@ -303,6 +303,8 @@ const PHASE_DIRECTIVES = {
"[codexclaw: PLAN]",
"Apply this pointer and its owners within exact user limits and permissions. No-delegation means no dispatch.",
"Load $codexclaw:cxc-pabcd for P and C2+ plan-output; $codexclaw:cxc-dev selects class and relevant surfaces. No implementation yet.",
+ "Formal P, including C2 compact and plan-only P: obtain the configured read-only architect proposal BEFORE the executable plan, send that concrete plan to the SAME architect for reflection BEFORE A, and record the consultation per $codexclaw:cxc-pabcd phase-plan/plan-output. The C0/C1 fast path needs none.",
+ "Implementation ownership: main implements by default; record any optional executor assignment (independent parallel or bounded routine work) with its write scope and check ($codexclaw:cxc-dev Implementation delegation).",
"Plan-only ends with the plan. Forbidden checks: NOT RUN; naming an artifact grants no write permission.",
].join("\n"),
A: [
@@ -310,11 +312,13 @@ const PHASE_DIRECTIVES = {
"Apply this pointer and its owners within exact user limits and permissions. No-delegation means no dispatch.",
"Load $codexclaw:cxc-dev-code-reviewer for review and $codexclaw:cxc-dev for relevant surfaces; authorized PABCD A uses $codexclaw:cxc-pabcd's audit owner. Do not build yet.",
"Authorized dispatch follows the owner's named-skill, same-reviewer and verdict contracts; main synthesizes. Report unmet independent review; inline review is not its proof. Do not bypass gates.",
+ "An amendment changing a module-responsibility, data-structure, interface or execution-flow decision needs reflection from the SAME architect before A completes ($codexclaw:cxc-pabcd phase-audit); text/test clarification alone does not. The reviewer stays independent.",
].join("\n"),
B: [
"[codexclaw: BUILD]",
"Apply this pointer and its owners within exact user limits and permissions. No-delegation means no dispatch.",
"Use $codexclaw:cxc-dev for class/surfaces; authorized PABCD B uses $codexclaw:cxc-pabcd. Implement only authorized scope.",
+ "Follow the plan's recorded implementation ownership: main implements other in-scope work directly; a new executor assignment needs a P amendment; verify a returned diff rather than the report.",
"Forbidden checks: NOT RUN; no invented proof.",
].join("\n"),
C: [
diff --git a/plugins/codexclaw/components/pabcd-state/dist/orchestrate-cli.js b/plugins/codexclaw/components/pabcd-state/dist/orchestrate-cli.js
index 28e28528..3522190a 100644
--- a/plugins/codexclaw/components/pabcd-state/dist/orchestrate-cli.js
+++ b/plugins/codexclaw/components/pabcd-state/dist/orchestrate-cli.js
@@ -453,6 +453,22 @@ function hasPabcdCloseRow(
);
}
+/**
+ * 260914: the phase directive only reaches a turn through UserPromptSubmit, so a
+ * same-turn CLI progression (P -> ... -> B inside one turn) never sees it. Echo a
+ * one-line pointer on the two edges whose owner decision is about to be made or
+ * executed. Advice only — it changes no gate.
+ */
+const OWNERSHIP_HINT = {
+ P: "formal P: architect proposal -> main executable plan -> same-architect reflection before A (cxc-pabcd phase-plan); implementation ownership: main implements by default; record optional executor assignments for independent parallel or bounded routine work with scope and check (cxc-dev Implementation delegation)",
+ B: "implementation ownership: follow recorded executor assignments; main implements other in-scope work directly; a new executor assignment needs a P amendment",
+};
+
+function withOwnershipHint(phase , output ) {
+ const hint = OWNERSHIP_HINT[phase];
+ return hint ? `${output} [${hint}]` : output;
+}
+
export function runOrchestrateCli(args , commitHooks = {}, nativeEnv = {}) {
if ("help" in args) return { code: 0, output: renderOrchestrateHelp() };
@@ -653,7 +669,7 @@ export function runOrchestrateCli(args
scanEvidence: { scanRounds: state.interview?.scanRounds ?? 0, highContradictionCount: gate.highContradictionCount },
...(args.attest?.did ? { evidence: args.attest.did } : {}),
});
- return { code: 0, output: `orchestrate P: I → P (agent override, session ${sessionId})` };
+ return { code: 0, output: withOwnershipHint("P", `orchestrate P: I → P (agent override, session ${sessionId})`) };
} else {
// Not ready and no override: advise-block with gate warnings.
return {
@@ -1118,7 +1134,7 @@ export function runOrchestrateCli(args
reason: "cli",
...(args.attest?.did ? { evidence: args.attest.did } : {}),
});
- return { code: 0, output: `orchestrate ${args.verb}: current=${state.phase} -> ${result.state.phase} (${state.phase} → ${result.state.phase}, session ${sessionId})` };
+ return { code: 0, output: withOwnershipHint(result.state.phase, `orchestrate ${args.verb}: current=${state.phase} -> ${result.state.phase} (${state.phase} → ${result.state.phase}, session ${sessionId})`) };
}
/**
* #48: candidate trees to check for the SAME session id. Deliberately shallow —
diff --git a/plugins/codexclaw/components/pabcd-state/src/hook.ts b/plugins/codexclaw/components/pabcd-state/src/hook.ts
index 153d5d98..b214df9b 100644
--- a/plugins/codexclaw/components/pabcd-state/src/hook.ts
+++ b/plugins/codexclaw/components/pabcd-state/src/hook.ts
@@ -303,6 +303,8 @@ const PHASE_DIRECTIVES: Partial> = {
"[codexclaw: PLAN]",
"Apply this pointer and its owners within exact user limits and permissions. No-delegation means no dispatch.",
"Load $codexclaw:cxc-pabcd for P and C2+ plan-output; $codexclaw:cxc-dev selects class and relevant surfaces. No implementation yet.",
+ "Formal P, including C2 compact and plan-only P: obtain the configured read-only architect proposal BEFORE the executable plan, send that concrete plan to the SAME architect for reflection BEFORE A, and record the consultation per $codexclaw:cxc-pabcd phase-plan/plan-output. The C0/C1 fast path needs none.",
+ "Implementation ownership: main implements by default; record any optional executor assignment (independent parallel or bounded routine work) with its write scope and check ($codexclaw:cxc-dev Implementation delegation).",
"Plan-only ends with the plan. Forbidden checks: NOT RUN; naming an artifact grants no write permission.",
].join("\n"),
A: [
@@ -310,11 +312,13 @@ const PHASE_DIRECTIVES: Partial> = {
"Apply this pointer and its owners within exact user limits and permissions. No-delegation means no dispatch.",
"Load $codexclaw:cxc-dev-code-reviewer for review and $codexclaw:cxc-dev for relevant surfaces; authorized PABCD A uses $codexclaw:cxc-pabcd's audit owner. Do not build yet.",
"Authorized dispatch follows the owner's named-skill, same-reviewer and verdict contracts; main synthesizes. Report unmet independent review; inline review is not its proof. Do not bypass gates.",
+ "An amendment changing a module-responsibility, data-structure, interface or execution-flow decision needs reflection from the SAME architect before A completes ($codexclaw:cxc-pabcd phase-audit); text/test clarification alone does not. The reviewer stays independent.",
].join("\n"),
B: [
"[codexclaw: BUILD]",
"Apply this pointer and its owners within exact user limits and permissions. No-delegation means no dispatch.",
"Use $codexclaw:cxc-dev for class/surfaces; authorized PABCD B uses $codexclaw:cxc-pabcd. Implement only authorized scope.",
+ "Follow the plan's recorded implementation ownership: main implements other in-scope work directly; a new executor assignment needs a P amendment; verify a returned diff rather than the report.",
"Forbidden checks: NOT RUN; no invented proof.",
].join("\n"),
C: [
diff --git a/plugins/codexclaw/components/pabcd-state/src/orchestrate-cli.ts b/plugins/codexclaw/components/pabcd-state/src/orchestrate-cli.ts
index 43800ea3..75feeaaa 100644
--- a/plugins/codexclaw/components/pabcd-state/src/orchestrate-cli.ts
+++ b/plugins/codexclaw/components/pabcd-state/src/orchestrate-cli.ts
@@ -453,6 +453,22 @@ function hasPabcdCloseRow(
);
}
+/**
+ * 260914: the phase directive only reaches a turn through UserPromptSubmit, so a
+ * same-turn CLI progression (P -> ... -> B inside one turn) never sees it. Echo a
+ * one-line pointer on the two edges whose owner decision is about to be made or
+ * executed. Advice only — it changes no gate.
+ */
+const OWNERSHIP_HINT: Partial> = {
+ P: "formal P: architect proposal -> main executable plan -> same-architect reflection before A (cxc-pabcd phase-plan); implementation ownership: main implements by default; record optional executor assignments for independent parallel or bounded routine work with scope and check (cxc-dev Implementation delegation)",
+ B: "implementation ownership: follow recorded executor assignments; main implements other in-scope work directly; a new executor assignment needs a P amendment",
+};
+
+function withOwnershipHint(phase: Phase, output: string): string {
+ const hint = OWNERSHIP_HINT[phase];
+ return hint ? `${output} [${hint}]` : output;
+}
+
export function runOrchestrateCli(args: OrchestrateCliArgs | OrchestrateCliHelpArgs, commitHooks: OrchestrateCommitHooks = {}, nativeEnv: NodeJS.ProcessEnv = {}): CliResult {
if ("help" in args) return { code: 0, output: renderOrchestrateHelp() };
@@ -653,7 +669,7 @@ export function runOrchestrateCli(args: OrchestrateCliArgs | OrchestrateCliHelpA
scanEvidence: { scanRounds: state.interview?.scanRounds ?? 0, highContradictionCount: gate.highContradictionCount },
...(args.attest?.did ? { evidence: args.attest.did } : {}),
});
- return { code: 0, output: `orchestrate P: I → P (agent override, session ${sessionId})` };
+ return { code: 0, output: withOwnershipHint("P", `orchestrate P: I → P (agent override, session ${sessionId})`) };
} else {
// Not ready and no override: advise-block with gate warnings.
return {
@@ -1118,7 +1134,7 @@ export function runOrchestrateCli(args: OrchestrateCliArgs | OrchestrateCliHelpA
reason: "cli",
...(args.attest?.did ? { evidence: args.attest.did } : {}),
});
- return { code: 0, output: `orchestrate ${args.verb}: current=${state.phase} -> ${result.state.phase} (${state.phase} → ${result.state.phase}, session ${sessionId})` };
+ return { code: 0, output: withOwnershipHint(result.state.phase, `orchestrate ${args.verb}: current=${state.phase} -> ${result.state.phase} (${state.phase} → ${result.state.phase}, session ${sessionId})`) };
}
/**
* #48: candidate trees to check for the SAME session id. Deliberately shallow —
diff --git a/plugins/codexclaw/components/pabcd-state/test/hook.test.ts b/plugins/codexclaw/components/pabcd-state/test/hook.test.ts
index 211d7451..5895c23a 100644
--- a/plugins/codexclaw/components/pabcd-state/test/hook.test.ts
+++ b/plugins/codexclaw/components/pabcd-state/test/hook.test.ts
@@ -116,11 +116,64 @@ test("wp3: phase pointers retain owners and active work-phase boundaries", () =>
assert.match(phaseDirective("P"), /No implementation yet/);
assert.match(phaseDirective("A"), /cxc-dev-code-reviewer/);
assert.match(phaseDirective("C"), /C-RENDER-GROUNDING-01/);
+ assert.match(phaseDirective("P"), /Implementation ownership: main implements by default/);
+ assert.match(phaseDirective("B"), /recorded implementation ownership/);
+ assert.match(phaseDirective("B"), /main implements other in-scope work directly/);
+ assert.doesNotMatch(phaseDirective("P"), /defaults to the configured executor|main needs a stated reason/);
const bound = phaseDirective("B", { activeWorkPhase: { id: "wp3", title: "minimal hooks" } });
assert.match(bound, /ACTIVE WORK-PHASE: wp3 — minimal hooks/);
assert.match(bound, /other work-phases are OUT OF SCOPE until D closes/);
});
+test("260914: P and A directives carry the architect consultation contract", () => {
+ const p = phaseDirective("P");
+ // Formal-P sequence: read-only architect proposal before the executable plan,
+ // the concrete plan back to the SAME architect for reflection before A, and a
+ // recorded consultation — inside the existing user-limit and implementation wording.
+ assert.match(p, /read-only architect proposal BEFORE the executable plan/);
+ assert.match(p, /SAME architect for reflection BEFORE A/);
+ assert.match(p, /phase-plan\/plan-output/);
+ assert.match(p, /C0\/C1 fast path needs none/);
+ assert.match(p, /No-delegation means no dispatch/);
+ assert.match(p, /Implementation ownership: main implements by default/);
+
+ const a = phaseDirective("A");
+ // A's reminder is the amendment recheck, distinct from P's initial sequence:
+ // only design-decision changes go back to the same architect.
+ assert.match(a, /module-responsibility, data-structure, interface or execution-flow/);
+ assert.match(a, /SAME architect before A completes/);
+ assert.match(a, /text\/test clarification alone does not/);
+ assert.match(a, /reviewer stays independent/i);
+ assert.doesNotMatch(a, /architect proposal BEFORE/i);
+
+ // No other phase carries an initial-consultation hint; B keeps implementation ownership only.
+ for (const phase of ["B", "C", "D"] as const) {
+ assert.doesNotMatch(phaseDirective(phase), /architect/i, `${phase} directive`);
+ }
+ assert.doesNotMatch(interviewDirective(), /architect/i);
+});
+
+test("260914: hook P output carries the architect sequence; A output carries the amendment recheck", () => {
+ const cwd = freshCwd();
+ try {
+ const pOut = handleUserPromptSubmit(ups("orchestrate P", cwd, "arch-seq", "t1"));
+ assert.notEqual(pOut, "");
+ const pCtx = JSON.parse(pOut.trimEnd()).hookSpecificOutput.additionalContext;
+ assert.match(pCtx, /read-only architect proposal BEFORE the executable plan/);
+ assert.match(pCtx, /SAME architect for reflection BEFORE A/);
+ assert.match(pCtx, /Implementation ownership: main implements by default/);
+
+ const aOut = handleUserPromptSubmit(ups("orchestrate a", cwd, "arch-seq", "t2"));
+ assert.notEqual(aOut, "");
+ const aCtx = JSON.parse(aOut.trimEnd()).hookSpecificOutput.additionalContext;
+ assert.match(aCtx, /SAME architect before A completes/);
+ assert.match(aCtx, /text\/test clarification alone does not/);
+ assert.doesNotMatch(aCtx, /architect proposal BEFORE/i);
+ } finally {
+ rmSync(cwd, { recursive: true, force: true });
+ }
+});
+
const WP3_ORIGINAL_C2_PROMPT = "README 계약에 맞게 기존 내부 메모 생성/목록 기능을 완성해줘. 네트워크 서버나 공개 API는 아니고 src/route.mjs와 src/service.mjs의 기존 빈 구현을 채우는 작업이야. src/store.mjs와 test/notes.test.mjs는 수정하지 마. 기존 번호 문서에 결과를 기록하고 node --test test/notes.test.mjs로 실제 검증해줘. 새 의존성/추상화/파일, goal/FSM 변경, 커밋, 서브에이전트 파견은 하지 마.";
test("wp3: original Korean C2 still reaches scoped CHECK without entering C", () => {
diff --git a/plugins/codexclaw/components/pabcd-state/test/orchestrate-cli.test.ts b/plugins/codexclaw/components/pabcd-state/test/orchestrate-cli.test.ts
index c3364881..8a294a06 100644
--- a/plugins/codexclaw/components/pabcd-state/test/orchestrate-cli.test.ts
+++ b/plugins/codexclaw/components/pabcd-state/test/orchestrate-cli.test.ts
@@ -2683,3 +2683,93 @@ test("wp7 preservation: CLI D-close keeps dependsOn and outcome", () => {
assert.equal(saved.workPhases[1].status, "in_progress");
assert.deepEqual(taskFields(saved), expectedTaskFields);
});
+
+test("260914: P and B entry echo the implementation-ownership pointer; other verbs do not", () => {
+ const cwd = freshCwd(); // not a repo: captureSourceIdentity is "unavailable", so B>C is not delta-gated
+ try {
+ const id = "wp3-hint";
+ seedSession(cwd, id, "IDLE");
+
+ // IDLE -> P carries the pointer.
+ const toP = runOrchestrateCli({ verb: "P", attest: null, session: id, cwd, json: false });
+ assert.equal(toP.code, 0, toP.output);
+ assert.equal(readState(cwd, id).phase, "P");
+ assert.match(toP.output, /implementation ownership/);
+ assert.match(toP.output, /architect proposal -> main executable plan -> same-architect reflection before A/);
+ assert.match(toP.output, /main implements by default/);
+ assert.match(toP.output, /optional executor assignments/);
+ assert.doesNotMatch(toP.output, /record an owner per planned change/);
+
+ // status is read-only and must not echo the pointer while parked at P.
+ const statusAtP = runOrchestrateCli({ verb: "status", attest: null, session: id, cwd, json: false });
+ assert.equal(statusAtP.code, 0, statusAtP.output);
+ assert.equal(readState(cwd, id).phase, "P");
+ assert.doesNotMatch(statusAtP.output, /implementation ownership/);
+ assert.doesNotMatch(statusAtP.output, /architect/i);
+
+ // P -> A does not carry the pointer.
+ const planUnit = seedPlanUnit(cwd);
+ const toA = runOrchestrateCli({ verb: "A", attest: { from: "P", to: "A", did: "audited", planUnit }, session: id, cwd, json: false });
+ assert.equal(toA.code, 0, toA.output);
+ assert.equal(readState(cwd, id).phase, "A");
+ assert.doesNotMatch(toA.output, /implementation ownership/);
+ assert.doesNotMatch(toA.output, /architect/i);
+
+ // A -> B carries the pointer.
+ const toB = runOrchestrateCli({
+ verb: "B",
+ attest: { from: "A", to: "B", did: "audit folded back", auditOutput: "reviewer: GO; refs verified", auditVerdict: "pass" },
+ session: id, cwd, json: false,
+ });
+ assert.equal(toB.code, 0, toB.output);
+ assert.equal(readState(cwd, id).phase, "B");
+ assert.match(toB.output, /implementation ownership/);
+ assert.match(toB.output, /main implements other in-scope work directly/);
+ assert.match(toB.output, /new executor assignment needs a P amendment/);
+ assert.doesNotMatch(toB.output, /architect/i);
+
+ // status at B stays clean too.
+ const statusAtB = runOrchestrateCli({ verb: "status", attest: null, session: id, cwd, json: false });
+ assert.equal(statusAtB.code, 0, statusAtB.output);
+ assert.equal(readState(cwd, id).phase, "B");
+ assert.doesNotMatch(statusAtB.output, /implementation ownership/);
+ assert.doesNotMatch(statusAtB.output, /architect/i);
+
+ // B -> C does not carry the pointer.
+ const toC = runOrchestrateCli({ verb: "C", attest: { from: "B", to: "C", did: "implemented the slice" }, session: id, cwd, json: false });
+ assert.equal(toC.code, 0, toC.output);
+ assert.equal(readState(cwd, id).phase, "C");
+ assert.doesNotMatch(toC.output, /implementation ownership/);
+ assert.doesNotMatch(toC.output, /architect/i);
+
+ // C -> D closes to IDLE and does not carry the pointer.
+ const toD = runOrchestrateCli({ verb: "D", attest: { from: "C", to: "D", did: "checks passed", checkOutput: "tests 1 pass 1", exitCode: 0 }, session: id, cwd, json: false });
+ assert.equal(toD.code, 0, toD.output);
+ assert.equal(readState(cwd, id).phase, "IDLE");
+ assert.doesNotMatch(toD.output, /implementation ownership/);
+ assert.doesNotMatch(toD.output, /architect/i);
+ } finally { rmSync(cwd, { recursive: true, force: true }); }
+});
+
+test("260914: I->P agent override echoes the implementation-ownership pointer", () => {
+ const cwd = freshCwd();
+ try {
+ // Same unready-interview fixture as the override tests above (line ~615).
+ seedSession(cwd, "s1", "I");
+ const r = runOrchestrateCli({
+ verb: "P",
+ attest: { from: "I", to: "P", did: "interview done", override: true },
+ session: "s1",
+ cwd,
+ json: false,
+ });
+ assert.equal(r.code, 0, r.output);
+ assert.equal(readState(cwd, "s1").phase, "P");
+ assert.match(r.output, /agent override/);
+ assert.match(r.output, /implementation ownership/);
+ assert.match(r.output, /architect proposal -> main executable plan -> same-architect reflection before A/);
+ assert.match(r.output, /main implements by default/);
+ assert.match(r.output, /optional executor assignments/);
+ assert.doesNotMatch(r.output, /record an owner per planned change/);
+ } finally { rmSync(cwd, { recursive: true, force: true }); }
+});
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/dist/spawn-attach-hook.js b/plugins/codexclaw/components/subagent-config/dist/spawn-attach-hook.js
index 5c661cff..0b9daaaa 100644
--- a/plugins/codexclaw/components/subagent-config/dist/spawn-attach-hook.js
+++ b/plugins/codexclaw/components/subagent-config/dist/spawn-attach-hook.js
@@ -23,12 +23,12 @@
* recognized cxc mention's SKILL.md body to the message. Atomic overflow rule:
* if the normalized message plus ALL candidate bodies would exceed
* MAX_NORMALIZE_LENGTH, no bodies are appended (never truncated/partial).
- * ENCRYPTION LIMIT (live-proven 260710, devlog 260710_v1_v2_parity/080): on
- * native ChatGPT-backend V2 sessions the hook receives `message` as backend
- * ciphertext, so normalization/inlining are silent no-ops there — the
- * hook-borne channels that survive encryption are the plaintext-prepended
- * leaf guard and the plaintext model/reasoning_effort fields. Inlining works
- * on plaintext surfaces (v1, non-encrypted provider/proxy paths).
+ * ENCRYPTION LIMIT: native V2 messages can be backend ciphertext. Preserve
+ * those bytes: plaintext guards, skill bodies or prompt overrides in that
+ * encrypted slot make the backend reject the child's task. Metadata-based
+ * recursion denial and separate model/effort fields still apply. Message
+ * augmentation is available only on plaintext surfaces; disclose the gap
+ * to the caller rather than claiming those instructions reached the child.
*
* SAFETY: `updatedInput` is a FULL REPLACEMENT of tool_input (registry.rs:122),
* honored only on permissionDecision "allow" (output_parser.rs:162). We echo the
@@ -534,6 +534,41 @@ export function isV2SpawnInput(toolInput ) {
return "task_name" in toolInput || "fork_turns" in toolInput;
}
+/**
+ * Structural recognition of a native Fernet task envelope. The wire form is
+ * base64url(version || timestamp || IV || ciphertext || HMAC) = 57 + 16n bytes
+ * with n >= 1, version 0x80. This is a SHAPE check, never authentication: the
+ * hook holds no key and cannot verify the HMAC, so it accepts both the
+ * canonical padded and the wholly unpadded base64url encodings of a well-formed
+ * frame. A strict decode/re-encode comparison rejects bad alphabet, embedded
+ * whitespace, impossible encoded lengths, partial or excess padding, and
+ * nonzero unused pad bits — every rejected input is ordinary plaintext and
+ * keeps its guards. No `gAAAA` prefix, timestamp, or MAC requirement: prefix
+ * resemblance alone must never strip plaintext attachment.
+ */
+function isFernetTokenShape(token ) {
+ const firstPad = token.indexOf("=");
+ const core = firstPad === -1 ? token : token.slice(0, firstPad);
+ // Padding is legal only as a trailing run.
+ if (firstPad !== -1 && !/^=+$/.test(token.slice(firstPad))) return false;
+ const rem = core.length % 4;
+ if (firstPad === -1) {
+ // Entirely unpadded: a %4==1 core is an impossible base64 length.
+ if (rem === 1) return false;
+ } else {
+ // Canonical padding only: exactly the count that rounds the core to a
+ // 4-char block. rem 0 or 1 can never take padding.
+ if (rem < 2 || token.length - core.length !== 4 - rem) return false;
+ }
+ if (!/^[A-Za-z0-9_-]+$/.test(core)) return false;
+ const decoded = Buffer.from(core, "base64url");
+ // Re-encode catches nonzero unused pad bits and any lenient-decode drift.
+ if (decoded.toString("base64url") !== core) return false;
+ // version(1) + timestamp(8) + IV(16) + ciphertext(16n, n>=1) + HMAC(32).
+ if (decoded.length < 73 || (decoded.length - 57) % 16 !== 0) return false;
+ return decoded[0] === 0x80;
+}
+
/**
* Hook-facing spawn tool names across surfaces: plain/V1 canonicalizes to
* `spawn_agent`; native V2 rides the `collaboration` namespace and reaches hooks
@@ -848,6 +883,7 @@ export function runSpawnAttachHook(raw ) {
// Keep the native one-of shape. Attachment-only requests still need routing.
const message = validItems ? outgoing : toolInput.message;
if (typeof message !== "string" || (!validItems && message.trim().length === 0)) return "";
+ const encryptedV2Message = v2Spawn && isFernetTokenShape(message);
const cwd = typeof obj.cwd === "string" && obj.cwd.length > 0 ? obj.cwd : process.cwd();
const dispatchScan = validItems
? textItems.map(item => scanInlineSkillBlocks(item.text).scanSource).join("\n\n")
@@ -913,11 +949,9 @@ export function runSpawnAttachHook(raw ) {
// (WP2 live bug: doc-quoted markers poisoned raw includes()).
const markerScanSource = scanInlineSkillBlocks(inlinedMessage).scanSource;
- // WP2 cr3 — V2 affordance: when inlining attached nothing (encrypted native
- // path, or no plaintext mentions), append the plaintext self-load instruction
- // so the child can resolve mentions itself. Marker-deduped; size-guarded;
- // never on v1 (upstream parses mentions there). Zero-mention plaintext V2
- // also gets it — deliberate small overhead (090_plan).
+ // V2 plaintext without inlined bodies gets a self-load instruction.
+ // Marker-deduped and size-guarded; the ciphertext boundary below discards
+ // all generated text for encrypted messages and discloses that omission.
let affordanceMessage = inlinedMessage;
if (
v2Spawn &&
@@ -1010,8 +1044,11 @@ export function runSpawnAttachHook(raw ) {
}
}
}
- const promptChanged = injectedPrompt !== null;
+ const promptChanged = !encryptedV2Message && injectedPrompt !== null;
if (trustPrefix) evidenceExemptMessage = `${trustPrefix}${evidenceExemptMessage}`;
+ // The native backend treats this whole value as ciphertext. Keep D1 and
+ // routing above, but never put our plaintext inside its encrypted slot.
+ if (encryptedV2Message) evidenceExemptMessage = message;
const updatedItems = mappedItems ? [...mappedItems] : null;
if (updatedItems) {
if (firstText < 0) updatedItems.unshift({ type: "text", text: evidenceExemptMessage });
@@ -1042,7 +1079,10 @@ export function runSpawnAttachHook(raw ) {
const fallbackNotice = !managed && readConfig(cwd).roles[role].fallback
? `[codexclaw] This direct spawn is not managed by first-fallback tracking. For subsequent tasks: ${DISPATCH_GUIDANCE}` : null;
- if (!managed && !fallbackNotice && !messageChanged && injectedModel === null && injectedEffort === null) return "";
+ const additionalContext = [fallbackNotice, encryptedV2Message
+ ? `[codexclaw] Native V2 task ciphertext was preserved. Hook-added skill text, scope instructions and prompt overrides were not attached; native recursion checks and separate routing fields still apply.${resolution.trustWarning ? ` ${resolution.trustWarning}` : ""}`
+ : null].filter(Boolean).join("\n");
+ if (!managed && !additionalContext && !messageChanged && injectedModel === null && injectedEffort === null) return "";
// Full replacement preserves whichever native input form the caller chose.
const updatedInput = updatedItems
@@ -1064,7 +1104,7 @@ export function runSpawnAttachHook(raw ) {
hookEventName: "PreToolUse",
permissionDecision: "allow",
updatedInput,
- ...(fallbackNotice ? { additionalContext: fallbackNotice } : {}),
+ ...(additionalContext ? { additionalContext } : {}),
},
})}\n`;
} catch {
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/src/spawn-attach-hook.ts b/plugins/codexclaw/components/subagent-config/src/spawn-attach-hook.ts
index 1f1d734a..23980a3e 100644
--- a/plugins/codexclaw/components/subagent-config/src/spawn-attach-hook.ts
+++ b/plugins/codexclaw/components/subagent-config/src/spawn-attach-hook.ts
@@ -23,12 +23,12 @@
* recognized cxc mention's SKILL.md body to the message. Atomic overflow rule:
* if the normalized message plus ALL candidate bodies would exceed
* MAX_NORMALIZE_LENGTH, no bodies are appended (never truncated/partial).
- * ENCRYPTION LIMIT (live-proven 260710, devlog 260710_v1_v2_parity/080): on
- * native ChatGPT-backend V2 sessions the hook receives `message` as backend
- * ciphertext, so normalization/inlining are silent no-ops there — the
- * hook-borne channels that survive encryption are the plaintext-prepended
- * leaf guard and the plaintext model/reasoning_effort fields. Inlining works
- * on plaintext surfaces (v1, non-encrypted provider/proxy paths).
+ * ENCRYPTION LIMIT: native V2 messages can be backend ciphertext. Preserve
+ * those bytes: plaintext guards, skill bodies or prompt overrides in that
+ * encrypted slot make the backend reject the child's task. Metadata-based
+ * recursion denial and separate model/effort fields still apply. Message
+ * augmentation is available only on plaintext surfaces; disclose the gap
+ * to the caller rather than claiming those instructions reached the child.
*
* SAFETY: `updatedInput` is a FULL REPLACEMENT of tool_input (registry.rs:122),
* honored only on permissionDecision "allow" (output_parser.rs:162). We echo the
@@ -534,6 +534,41 @@ export function isV2SpawnInput(toolInput: Record): boolean {
return "task_name" in toolInput || "fork_turns" in toolInput;
}
+/**
+ * Structural recognition of a native Fernet task envelope. The wire form is
+ * base64url(version || timestamp || IV || ciphertext || HMAC) = 57 + 16n bytes
+ * with n >= 1, version 0x80. This is a SHAPE check, never authentication: the
+ * hook holds no key and cannot verify the HMAC, so it accepts both the
+ * canonical padded and the wholly unpadded base64url encodings of a well-formed
+ * frame. A strict decode/re-encode comparison rejects bad alphabet, embedded
+ * whitespace, impossible encoded lengths, partial or excess padding, and
+ * nonzero unused pad bits — every rejected input is ordinary plaintext and
+ * keeps its guards. No `gAAAA` prefix, timestamp, or MAC requirement: prefix
+ * resemblance alone must never strip plaintext attachment.
+ */
+function isFernetTokenShape(token: string): boolean {
+ const firstPad = token.indexOf("=");
+ const core = firstPad === -1 ? token : token.slice(0, firstPad);
+ // Padding is legal only as a trailing run.
+ if (firstPad !== -1 && !/^=+$/.test(token.slice(firstPad))) return false;
+ const rem = core.length % 4;
+ if (firstPad === -1) {
+ // Entirely unpadded: a %4==1 core is an impossible base64 length.
+ if (rem === 1) return false;
+ } else {
+ // Canonical padding only: exactly the count that rounds the core to a
+ // 4-char block. rem 0 or 1 can never take padding.
+ if (rem < 2 || token.length - core.length !== 4 - rem) return false;
+ }
+ if (!/^[A-Za-z0-9_-]+$/.test(core)) return false;
+ const decoded = Buffer.from(core, "base64url");
+ // Re-encode catches nonzero unused pad bits and any lenient-decode drift.
+ if (decoded.toString("base64url") !== core) return false;
+ // version(1) + timestamp(8) + IV(16) + ciphertext(16n, n>=1) + HMAC(32).
+ if (decoded.length < 73 || (decoded.length - 57) % 16 !== 0) return false;
+ return decoded[0] === 0x80;
+}
+
/**
* Hook-facing spawn tool names across surfaces: plain/V1 canonicalizes to
* `spawn_agent`; native V2 rides the `collaboration` namespace and reaches hooks
@@ -848,6 +883,7 @@ export function runSpawnAttachHook(raw: string): string {
// Keep the native one-of shape. Attachment-only requests still need routing.
const message = validItems ? outgoing : toolInput.message;
if (typeof message !== "string" || (!validItems && message.trim().length === 0)) return "";
+ const encryptedV2Message = v2Spawn && isFernetTokenShape(message);
const cwd = typeof obj.cwd === "string" && obj.cwd.length > 0 ? obj.cwd : process.cwd();
const dispatchScan = validItems
? textItems.map(item => scanInlineSkillBlocks(item.text).scanSource).join("\n\n")
@@ -913,11 +949,9 @@ export function runSpawnAttachHook(raw: string): string {
// (WP2 live bug: doc-quoted markers poisoned raw includes()).
const markerScanSource = scanInlineSkillBlocks(inlinedMessage).scanSource;
- // WP2 cr3 — V2 affordance: when inlining attached nothing (encrypted native
- // path, or no plaintext mentions), append the plaintext self-load instruction
- // so the child can resolve mentions itself. Marker-deduped; size-guarded;
- // never on v1 (upstream parses mentions there). Zero-mention plaintext V2
- // also gets it — deliberate small overhead (090_plan).
+ // V2 plaintext without inlined bodies gets a self-load instruction.
+ // Marker-deduped and size-guarded; the ciphertext boundary below discards
+ // all generated text for encrypted messages and discloses that omission.
let affordanceMessage = inlinedMessage;
if (
v2Spawn &&
@@ -1010,8 +1044,11 @@ export function runSpawnAttachHook(raw: string): string {
}
}
}
- const promptChanged = injectedPrompt !== null;
+ const promptChanged = !encryptedV2Message && injectedPrompt !== null;
if (trustPrefix) evidenceExemptMessage = `${trustPrefix}${evidenceExemptMessage}`;
+ // The native backend treats this whole value as ciphertext. Keep D1 and
+ // routing above, but never put our plaintext inside its encrypted slot.
+ if (encryptedV2Message) evidenceExemptMessage = message;
const updatedItems = mappedItems ? [...mappedItems] : null;
if (updatedItems) {
if (firstText < 0) updatedItems.unshift({ type: "text", text: evidenceExemptMessage });
@@ -1042,7 +1079,10 @@ export function runSpawnAttachHook(raw: string): string {
const fallbackNotice = !managed && readConfig(cwd).roles[role].fallback
? `[codexclaw] This direct spawn is not managed by first-fallback tracking. For subsequent tasks: ${DISPATCH_GUIDANCE}` : null;
- if (!managed && !fallbackNotice && !messageChanged && injectedModel === null && injectedEffort === null) return "";
+ const additionalContext = [fallbackNotice, encryptedV2Message
+ ? `[codexclaw] Native V2 task ciphertext was preserved. Hook-added skill text, scope instructions and prompt overrides were not attached; native recursion checks and separate routing fields still apply.${resolution.trustWarning ? ` ${resolution.trustWarning}` : ""}`
+ : null].filter(Boolean).join("\n");
+ if (!managed && !additionalContext && !messageChanged && injectedModel === null && injectedEffort === null) return "";
// Full replacement preserves whichever native input form the caller chose.
const updatedInput: Record = updatedItems
@@ -1064,7 +1104,7 @@ export function runSpawnAttachHook(raw: string): string {
hookEventName: "PreToolUse",
permissionDecision: "allow",
updatedInput,
- ...(fallbackNotice ? { additionalContext: fallbackNotice } : {}),
+ ...(additionalContext ? { additionalContext } : {}),
},
})}\n`;
} catch {
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/components/subagent-config/test/spawn-attach-hook.test.ts b/plugins/codexclaw/components/subagent-config/test/spawn-attach-hook.test.ts
index 93e5668d..0affce69 100644
--- a/plugins/codexclaw/components/subagent-config/test/spawn-attach-hook.test.ts
+++ b/plugins/codexclaw/components/subagent-config/test/spawn-attach-hook.test.ts
@@ -971,7 +971,10 @@ test("same-intent v1/v2 spawns produce surface-appropriate effective payloads",
runSpawnAttachHook(spawnPayloadAt(cwd, { message: intent, agent_type: "explorer" })),
);
const v2 = updatedInputOf(
- runSpawnAttachHook(spawnPayloadAt(cwd, { task_name: "map_fe", fork_turns: "none", message: intent })),
+ // Explicit agent_type: the normalized mention link carries the checkout path,
+ // and a "delegation-review"-style path would otherwise hit legacy review-keyword
+ // inference. v1 already pins explorer; keep the surfaces comparable.
+ runSpawnAttachHook(spawnPayloadAt(cwd, { task_name: "map_fe", fork_turns: "none", message: intent, agent_type: "explorer" })),
);
// Same model + effort routing on both surfaces.
assert.equal(v1.model, v2.model);
@@ -995,19 +998,161 @@ test("v2 affordance: appended only when inlining attached nothing", () => {
);
assert.ok((inlined.message as string).includes(`${INLINE_SKILL_OPEN}dev">`));
assert.ok(!(inlined.message as string).includes(affordanceOpening));
- // No mentions (ciphertext-like opaque text) -> affordance appended after the task text.
+ // Plaintext without mentions still gets the self-load affordance.
const opaque = updatedInputOf(
- runSpawnAttachHook(spawnPayload({ task_name: "t", fork_turns: "none", message: "gAAAAABopaquetoken" })),
+ runSpawnAttachHook(spawnPayload({ task_name: "t", fork_turns: "none", message: "Inspect the catalog module." })),
);
assert.ok((opaque.message as string).includes(SKILL_AFFORDANCE_MARKER));
assert.ok((opaque.message as string).startsWith(`${LEAF_GUARD_BLOCK}\n\n`), "guard stays first");
assert.ok(
- (opaque.message as string).indexOf("gAAAAABopaquetoken") <
+ (opaque.message as string).indexOf("Inspect the catalog module.") <
(opaque.message as string).indexOf(SKILL_AFFORDANCE_MARKER),
"affordance rides after the task text",
);
});
+// Public Fernet generate.json "hello" vector: a real envelope wire form, no key.
+const FERNET_VECTOR = "gAAAAAAdwJ6wAAECAwQFBgcICQoLDA0ODy021cpGVWKZ_eEwCGM4BLLF_5CV9dOPmrhuVUPgJobwOz7JcbmrR64jVmpU4IwqDA==";
+
+// Synthetic Fernet-shaped frame: version(1) || timestamp(8) || IV(16) ||
+// ciphertext(ciphertextBytes) || HMAC(32). Structural fixture only — the bytes
+// are not authenticated and carry no key material.
+function fernetFrame(options: { version?: number; timestamp?: Uint8Array; ciphertextBytes?: number } = {}) {
+ return Buffer.concat([
+ Buffer.from([options.version ?? 0x80]),
+ options.timestamp ?? Buffer.alloc(8, 0),
+ Buffer.alloc(16, 0x11),
+ Buffer.alloc(options.ciphertextBytes ?? 16, 0x22),
+ Buffer.alloc(32, 0x33),
+ ]);
+}
+
+// Canonical padded or wholly unpadded base64url wire form of a frame.
+function fernetToken(frame: Uint8Array, padded = true): string {
+ const unpadded = Buffer.from(frame).toString("base64url");
+ return padded ? `${unpadded}${"=".repeat((4 - (unpadded.length % 4)) % 4)}` : unpadded;
+}
+
+const CIPHERTEXT_NOTICE = /ciphertext was preserved/;
+
+test("native V2 ciphertext survives routing and prompt overrides byte-for-byte", () => {
+ const message = FERNET_VECTOR;
+ const cwd = workspaceWithConfig({
+ architect: { mode: "model", model: "architect-fixture", effort: "high", promptOverride: "Architect-only instructions" },
+ });
+ for (const tool_name of ["spawn_agent", "collaborationspawn_agent"]) {
+ const input = { task_name: "design", agent_type: "architect", fork_turns: "none", message };
+ const payload = { ...JSON.parse(spawnPayloadAt(cwd, input)), tool_name };
+ const output = runSpawnAttachHook(JSON.stringify(payload));
+ const updated = updatedInputOf(output);
+ assert.deepEqual(updated, { ...input, model: "architect-fixture", reasoning_effort: "high" });
+ assert.match(JSON.parse(output).hookSpecificOutput.additionalContext, /prompt overrides were not attached/);
+ }
+});
+
+test("native V2 ciphertext preserves explicit settings and full-history fork restrictions", () => {
+ const message = FERNET_VECTOR;
+ const cwd = workspaceWithConfig({
+ architect: { mode: "model", model: "configured-fixture", effort: "high", promptOverride: "Architect-only instructions" },
+ });
+ for (const fields of [
+ { fork_turns: "none", model: "caller-fixture", reasoning_effort: "low" },
+ { fork_turns: "all" },
+ ]) {
+ const input = { task_name: "design", agent_type: "architect", message, ...fields };
+ const out = runSpawnAttachHook(spawnPayloadAt(cwd, input));
+ assert.deepEqual(out ? updatedInputOf(out) : input, input);
+ }
+});
+
+test("native V2 ciphertext cannot bypass the existing recursion denial", () => {
+ const message = FERNET_VECTOR;
+ const result = JSON.parse(runSpawnAttachHook(subagentSpawnPayload({ task_name: "nested", message })));
+ assert.equal(result.hookSpecificOutput.permissionDecision, "deny");
+ assert.equal(result.hookSpecificOutput.updatedInput, undefined);
+});
+
+test("valid Fernet frames stay byte-identical across padding forms and block counts", () => {
+ const variants: [string, string][] = [
+ ["reference vector (canonical padded)", FERNET_VECTOR],
+ ["reference vector (unpadded)", FERNET_VECTOR.slice(0, -2)],
+ ];
+ // n=3 frames fill a whole base64 group, so their padded and unpadded wire
+ // forms coincide; both entries still exercise the accepted shape.
+ for (const blocks of [1, 2, 3]) {
+ const frame = fernetFrame({ ciphertextBytes: 16 * blocks });
+ variants.push([`n=${blocks} padded`, fernetToken(frame)], [`n=${blocks} unpadded`, fernetToken(frame, false)]);
+ }
+ // A timestamp whose bytes do not encode the old gAAAA prefix must still be
+ // recognized: the classifier checks structure, not the prefix.
+ const noPrefix = fernetToken(fernetFrame({ timestamp: Buffer.alloc(8, 0xff) }));
+ assert.ok(!noPrefix.startsWith("gAAAA"), "fixture must actually lack the legacy prefix");
+ variants.push(["non-gAAAA timestamp", noPrefix]);
+ for (const [label, message] of variants) {
+ const out = runSpawnAttachHook(spawnPayload({ task_name: "t", fork_turns: "none", message }));
+ const parsed = JSON.parse(out);
+ assert.equal(parsed.hookSpecificOutput.permissionDecision, "allow", label);
+ assert.equal(parsed.hookSpecificOutput.updatedInput.message, message, `${label}: byte-identical`);
+ assert.match(parsed.hookSpecificOutput.additionalContext ?? "", CIPHERTEXT_NOTICE, label);
+ }
+});
+
+test("malformed V2 ciphertext lookalikes keep the guard, affordance and configured prompt", () => {
+ const cwd = workspaceWithConfig({
+ architect: { mode: "model", model: "architect-fixture", effort: "high", promptOverride: "Architect-only instructions" },
+ });
+ const core = FERNET_VECTOR.slice(0, -2);
+ const malformed: [string, string][] = [
+ ["short gAAAA prefix", "gAAAAx"],
+ ["old e2e fixture", "gAAAAABopaque-payload"],
+ ["impossible base64 length", "gAAAA"],
+ ["old invalid fixture", `gAAAAAB${"aB9_".repeat(30)}==`],
+ ["wrong version byte", fernetToken(fernetFrame({ version: 0x81 }))],
+ ["truncated frame", FERNET_VECTOR.slice(0, 80)],
+ ["empty ciphertext", fernetToken(fernetFrame({ ciphertextBytes: 0 }))],
+ ["non-16-byte-block ciphertext", fernetToken(fernetFrame({ ciphertextBytes: 24 }))],
+ ["embedded whitespace", `${core.slice(0, 40)} ${core.slice(40)}==`],
+ ["standard-alphabet characters", FERNET_VECTOR.replaceAll("_", "/")],
+ ["partial padding", `${core}=`],
+ ["excess padding", `${FERNET_VECTOR}=`],
+ ["trailing newline", `${FERNET_VECTOR}\n`],
+ ["padding with carriage return", `${core}=\r`],
+ ["mid-string padding", `${core.slice(0, 20)}=${core.slice(20)}==`],
+ ["nonzero unused pad bits", `${core.slice(0, -1)}B==`],
+ ];
+ for (const [label, message] of malformed) {
+ const out = runSpawnAttachHook(spawnPayloadAt(cwd, { task_name: "t", fork_turns: "none", agent_type: "architect", message }));
+ const parsed = JSON.parse(out);
+ assert.equal(parsed.hookSpecificOutput.permissionDecision, "allow", label);
+ const ui = parsed.hookSpecificOutput.updatedInput;
+ assert.ok((ui.message as string).startsWith(`${LEAF_GUARD_BLOCK}\n\n`), `${label}: guard attached`);
+ assert.ok((ui.message as string).includes("Architect-only instructions"), `${label}: prompt override attached`);
+ assert.ok((ui.message as string).includes(SKILL_AFFORDANCE_MARKER), `${label}: plaintext affordance attached`);
+ // Plaintext attachment already trims trailing whitespace; only ciphertext
+ // has the byte-for-byte preservation contract.
+ assert.ok((ui.message as string).includes(message.trimEnd()), `${label}: task text preserved`);
+ assert.doesNotMatch(parsed.hookSpecificOutput.additionalContext ?? "", CIPHERTEXT_NOTICE, label);
+ }
+});
+
+test("v1 items carrying a Fernet-shaped token stay on the normal attachment path", () => {
+ const out = runSpawnAttachHook(spawnPayload({
+ agent_type: "explorer",
+ items: [
+ { type: "text", text: FERNET_VECTOR },
+ { type: "attachment", ref: "fixture-1" },
+ ],
+ }));
+ const parsed = JSON.parse(out);
+ const ui = parsed.hookSpecificOutput.updatedInput;
+ assert.ok(Array.isArray(ui.items));
+ const [first, second] = ui.items as Record[];
+ assert.ok((first.text as string).startsWith(`${V1_SCOPE_BLOCK}\n\n`));
+ assert.ok((first.text as string).includes(FERNET_VECTOR));
+ assert.deepEqual(second, { type: "attachment", ref: "fixture-1" });
+ assert.doesNotMatch(parsed.hookSpecificOutput.additionalContext ?? "", CIPHERTEXT_NOTICE);
+});
+
test("v1 spawns never get the affordance (upstream parses mentions there)", () => {
const ui = updatedInputOf(runSpawnAttachHook(spawnPayload({ message: "no mentions here", agent_type: "explorer" })));
assert.ok(!(ui.message as string).includes(SKILL_AFFORDANCE_MARKER));
@@ -1174,4 +1319,3 @@ test("explicit executor and reviewer roles take precedence over message keywords
assert.equal(inferRole("executor", "review the implementation"), "executor");
assert.equal(inferRole("reviewer", "inspect correctness"), "reviewer");
});
-
diff --git a/plugins/codexclaw/skills/dev/SKILL.md b/plugins/codexclaw/skills/dev/SKILL.md
index ba8b7341..b0cd831f 100644
--- a/plugins/codexclaw/skills/dev/SKILL.md
+++ b/plugins/codexclaw/skills/dev/SKILL.md
@@ -203,6 +203,28 @@ Confirm actual model routing from runtime evidence when reporting identity or co
When comparing cost, use main plus child served-model input/cache/output prices and
applicable tiers; token totals alone cannot compare differently priced models.
+### Implementation delegation
+
+The main agent of the owning implementation task implements directly by default.
+This also applies when another coordinator has assigned work to that independent
+task: it does not need to delegate again. Direct implementation needs no special
+justification. Main owns the plan, design decisions, integration and verification.
+
+Use the configured `executor` when delegation helps: independent parallel work
+with disjoint write scopes, or bounded routine work with a clear contract and
+check. Sequential or tightly coupled work can stay with main. Delegate one
+verifiable bundle rather than one packet per file.
+
+When an executor is selected, P records its scope and check; B follows that
+assignment. Main implements other in-scope work directly. A new executor
+assignment mid-B requires a P amendment (`../pabcd/references/plan-output.md`).
+Verify returned implementation against the VCS diff, never the report alone (§3).
+Model, effort, fallback and receipts stay with the configured dispatch path.
+Choosing optional delegation does not authorize reclaiming live work: stop and
+reconciliation requirements still apply, and managed reclaim requires the
+`main-direct` result. No-delegation limits, host restrictions and
+DISPATCH-ECONOMY-01's specifiability / verifiability / judgment axes take precedence.
+
### Capability Routing Hub
**Independent peers:** keep work local and use selective read-only evidence when
diff --git a/plugins/codexclaw/skills/loop/references/waiting.md b/plugins/codexclaw/skills/loop/references/waiting.md
index 534827cb..3afbfe3d 100644
--- a/plugins/codexclaw/skills/loop/references/waiting.md
+++ b/plugins/codexclaw/skills/loop/references/waiting.md
@@ -28,8 +28,67 @@ 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, 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
+ 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.
+
+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/SKILL.md b/plugins/codexclaw/skills/pabcd/SKILL.md
index 49675ddb..6f630bb1 100644
--- a/plugins/codexclaw/skills/pabcd/SKILL.md
+++ b/plugins/codexclaw/skills/pabcd/SKILL.md
@@ -69,7 +69,7 @@ link is a conditional routing edge, not a command to preload the entire graph.
| Phase / trigger | Mandatory owner before work |
|---|---|
| I | cxc-interview; no active host goal |
-| P, including plan-only | [Plan phase](references/phase-plan.md); C2+ plans also read [Plan output](references/plan-output.md) |
+| P, including plan-only | [Plan phase](references/phase-plan.md): architect proposal → main executable plan → same-architect reflection; C2+ plans also read [Plan output](references/plan-output.md) for consultation evidence and any executor assignments |
| A, if authorized | [Audit phase](references/phase-audit.md) |
| C | [Check phase](references/phase-check.md) |
| P/A specifying render or conditional-path verification | [Check phase](references/phase-check.md), to define reachable activation and observable evidence |
@@ -80,7 +80,7 @@ or justified near-pass exits. C requires fresh relevant proof and SoT sync;
passing unrelated checks is not evidence. Explicit execution restrictions are not
overridden by a reference asking to run a verifier or dispatch a reviewer.
-3. **B — Build**: Implement the audited plan in small atomic commits (DEV-GIT-COMMIT-01). Verify as you go. Stay inside the plan's scope boundary; surface deviations instead of silently expanding scope. Never push to a remote without explicit user approval (DEV-GIT-PUSH-01, ESCALATE). When P declared a stack, follow `DEV-STACK-02` in `cxc-dev` `references/stacked-prs.md`.
+3. **B — Build**: Implement the audited plan in small atomic commits (DEV-GIT-COMMIT-01). Verify as you go. Stay inside the plan's scope boundary; surface deviations instead of silently expanding scope. Never push to a remote without explicit user approval (DEV-GIT-PUSH-01, ESCALATE). When P declared a stack, follow `DEV-STACK-02` in `cxc-dev` `references/stacked-prs.md`. Follow the plan's recorded executor assignments and verify returned diffs (`cxc-dev` Implementation delegation). Main implements other in-scope work directly; a new executor assignment needs a P amendment.
5. **D — Done**: Summarize what was checked with evidence, update STATUS/devlog, commit (local only — pushing remains gated by DEV-GIT-PUSH-01), and confirm no pending work remains for this work-phase before returning to idle. The D summary is written for a reader who was not in the loop — conclusion, what changed, evidence pointers — per [Reader documents](../dev/references/reader-documents.md) READER-DOC-02/04. For loop/multi-pass work, **LOOP-PESSIMIST-01 (DEFAULT)** also records what did not improve, which hypothesis died, and what evidence would show the current direction is wrong; D -> IDLE -> P is a context/bias-flush boundary, so the next cycle resumes from disk artifacts rather than transcript momentum.
## Work-Phase Loop (multi-pass tasks)
diff --git a/plugins/codexclaw/skills/pabcd/references/delegation.md b/plugins/codexclaw/skills/pabcd/references/delegation.md
index 2d4e76cc..44d68959 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 })` |
@@ -239,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)
@@ -269,20 +283,41 @@ 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;
- 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.
+ 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/plugins/codexclaw/skills/pabcd/references/phase-plan.md b/plugins/codexclaw/skills/pabcd/references/phase-plan.md
index 28120fea..e84fcc2f 100644
--- a/plugins/codexclaw/skills/pabcd/references/phase-plan.md
+++ b/plugins/codexclaw/skills/pabcd/references/phase-plan.md
@@ -2,9 +2,12 @@
## Architect consultation for formal P
-Every formal P plan follows this sequence, including plan-only work that enters P.
-The dev-owned C0/C1 fast path remains unchanged. Explicit user limits (such as
-no delegation) take precedence; record any resulting consultation gap honestly.
+Every formal P plan follows this sequence, including C2 compact plans and plan-only
+P work; it is not limited to large or full plans. Applicability follows the planning
+work, not whether the agent happened to write the required plan header. Explanation
+or read-only assessment alone does not enter P. The dev-owned C0/C1 fast path needs
+neither consultation nor a consultation record. Explicit user limits (such as no
+delegation) take precedence; record any resulting consultation gap honestly.
1. Main gathers requirements and source evidence, identifies scope and existing owners.
2. Dispatch a read-only `architect` with `dev` and `dev-architecture` attached.
@@ -17,7 +20,9 @@ no delegation) take precedence; record any resulting consultation gap honestly.
4. Send that concrete plan to the SAME architect for a reflection check. Require
`ALIGNED` or `MISALIGNED`, a decision-ID-to-plan mapping and exact gaps. Resolve
material gaps with recorded main dispositions before independent A audit. A missing
- proposal or reflection check is not completed consultation.
+ proposal or reflection check is not completed consultation. Keep the actual
+ handle, proposal, main dispositions and reflection tied to the submitted plan
+ revision in the [Plan output consultation record](plan-output.md#architect-consultation).
5. Dispatch the independent A reviewer. Architect reflection never replaces A.
Use the [delegation owner](delegation.md#architect-context-and-routing) for supported
diff --git a/plugins/codexclaw/skills/pabcd/references/plan-output.md b/plugins/codexclaw/skills/pabcd/references/plan-output.md
index d856c0ab..115b48d4 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.
@@ -26,6 +26,41 @@ exit code or treat the plan as implementation proof. Likewise, naming a memory
artifact or escalation path does not authorize a file write, host goal, dispatch,
phase transition or external action.
+## Architect consultation
+
+Formal P follows the [Plan phase owner](phase-plan.md#architect-consultation-for-formal-p),
+including C2 compact and plan-only P plans. Keep a compact record in the existing
+plan, using summaries and evidence references rather than copying conversation
+transcripts:
+
+- The actual returned architect handle and proposal reference, with its design
+ decision IDs.
+- Main's acceptance, rejection or amendment of each decision, with a short reason.
+- The concrete executable-plan path/revision sent to that same architect.
+- The reflection response reference, `ALIGNED` or `MISALIGNED`, and any remaining
+ gaps with main's dispositions. Resolve material gaps before independent A audit.
+
+An explicit user limit such as no delegation is a disclosed consultation gap.
+A failed call or unavailable required role remains unmet consultation; recording
+the reason does not make it complete. Follow the existing
+[routing and failure owner](delegation.md#architect-context-and-routing).
+C0/C1 fast-path work requires neither consultation nor this record. Main owns
+the executable plan and final decisions; architect reflection is not A review.
+
+## Implementation ownership
+
+Main implements the plan's in-scope work directly by default, without an exception
+statement. When selecting an executor for independent parallel or bounded routine
+work, record its write scope, contract and check in the file change map. Parallel
+write scopes must be disjoint. B follows recorded executor assignments; main owns
+other in-scope work. A new executor assignment mid-B remains a P amendment (see
+Escalation condition above). Discovery and review keep their own owners.
+
+```text
+feature implementation and integration — main
+independent fixture update — optional executor: named files, expected data, fixture check
+```
+
## Reader summary
A C2+ unit's `000_plan.md` opens with a reader-facing summary per
diff --git a/plugins/codexclaw/skills/search/SKILL.md b/plugins/codexclaw/skills/search/SKILL.md
index b400133e..0489c71b 100644
--- a/plugins/codexclaw/skills/search/SKILL.md
+++ b/plugins/codexclaw/skills/search/SKILL.md
@@ -145,11 +145,13 @@ The shared payload form is a **link-form mention in the spawn message**. On V1 t
child's first turn parses the mention and injects the full SKILL.md body. When a
V2-shaped spawn message reaches the codexclaw hook as plaintext (non-encrypted
provider/proxy paths), the hook recognizes the same mention and inlines the full body.
-Native ChatGPT-backend V2 sends the hook ciphertext, so mention normalization and body
-inlining are no-ops there; when no body can be inlined, a plaintext
-`[CXC-SKILL-AFFORDANCE]` block tells the child to self-load any `$cxc-` /
-`$codexclaw:cxc-` mention from `//SKILL.md`; fork inheritance
-remains a secondary channel. If the path is not link-safe, use the plugin-native
+Plaintext V2 without an inlined body receives a `[CXC-SKILL-AFFORDANCE]` block
+asking the child to self-load mentions from `//SKILL.md`.
+Native ChatGPT-backend V2 sends ciphertext with a Fernet envelope. The hook
+preserves structurally recognized ciphertext byte-for-byte and tells the caller
+that hook-added skill text, scope instructions and prompt overrides were omitted;
+it cannot attach the plaintext affordance to encrypted task bytes. Native recursion
+checks and separate model/effort routing still apply. If the path is not link-safe, use the plugin-native
`$codexclaw:cxc-search` fallback instead:
```text
diff --git a/plugins/codexclaw/test/hook-e2e.test.mjs b/plugins/codexclaw/test/hook-e2e.test.mjs
index 9cb8b11e..ac4ac328 100644
--- a/plugins/codexclaw/test/hook-e2e.test.mjs
+++ b/plugins/codexclaw/test/hook-e2e.test.mjs
@@ -802,9 +802,13 @@ test("260710: spawn hook e2e - native collaboration name drives the V2 path", ()
}
});
-// WP2 cr3: an opaque (ciphertext-like) V2 message that inlines nothing gains the
-// plaintext self-load affordance block, after the task text, under the guard.
-test("260710 WP2: spawn hook e2e - opaque V2 message gains the skill affordance", () => {
+// The shipped entrypoint preserves native ciphertext and augments plaintext.
+for (const [name, message, encrypted] of [
+ // Public Fernet generate.json vector; no live task or key material.
+ ["ciphertext stays byte-identical", "gAAAAAAdwJ6wAAECAwQFBgcICQoLDA0ODy021cpGVWKZ_eEwCGM4BLLF_5CV9dOPmrhuVUPgJobwOz7JcbmrR64jVmpU4IwqDA==", true],
+ ["plaintext gains the skill affordance", "Inspect the catalog module.", false],
+ ["short ciphertext lookalike gains the skill affordance", "gAAAAx", false],
+]) test(`spawn hook e2e - V2 ${name}`, () => {
const { hookEvent, distAbs } = readHookCommand("./hooks/pre-tool-use-attaching-skills.json");
const ep = snapshotEntrypoint(distAbs);
assert.ok(ep, "subagent-config dist entrypoint must settle");
@@ -813,13 +817,21 @@ test("260710 WP2: spawn hook e2e - opaque V2 message gains the skill affordance"
const res = runHook(ep, hookEvent, {
hook_event_name: "PreToolUse", session_id: "s1", cwd,
tool_name: "collaborationspawn_agent",
- tool_input: { task_name: "t", fork_turns: "none", message: "gAAAAABopaque-payload" },
+ tool_input: { task_name: "t", fork_turns: "none", message },
}, { CXC_SKILLS_DIR: join(pluginRoot, "skills") });
assert.equal(res.status, 0, res.stderr);
- const ui = JSON.parse(res.stdout).hookSpecificOutput.updatedInput;
+ const output = JSON.parse(res.stdout).hookSpecificOutput;
+ const ui = output.updatedInput;
+ if (encrypted) {
+ assert.equal(ui.message, message);
+ assert.equal(ui.task_name, "t");
+ assert.equal(ui.fork_turns, "none");
+ assert.match(output.additionalContext, /prompt overrides were not attached/);
+ return;
+ }
assert.ok(ui.message.startsWith("[CXC-LEAF-GUARD]"));
assert.match(ui.message, /\[CXC-SKILL-AFFORDANCE\]/);
- assert.ok(ui.message.indexOf("gAAAAABopaque-payload") < ui.message.indexOf("[CXC-SKILL-AFFORDANCE]"));
+ assert.ok(ui.message.indexOf(message) < ui.message.indexOf("[CXC-SKILL-AFFORDANCE]"));
assert.match(ui.message, /skills\/\/SKILL\.md/);
} finally {
rmSync(cwd, { recursive: true, force: true });
@@ -884,7 +896,7 @@ test("260713: spawn hook e2e - snapshot override composes mention repair with th
const v2Guard = runHook(ep, hookEvent, {
hook_event_name: "PreToolUse", session_id: "s1", cwd: configuredCwd,
tool_name: "spawn_agent",
- tool_input: { task_name: "child_task", fork_turns: "none", message: "$cxc-dev map the codebase" },
+ tool_input: { task_name: "child_task", agent_type: "explorer", fork_turns: "none", message: "$cxc-dev map the codebase" },
}, skillsEnv);
assert.equal(v2Guard.status, 0, v2Guard.stderr);
const v2Ui = JSON.parse(v2Guard.stdout).hookSpecificOutput.updatedInput;
diff --git a/structure/10_subagent_skill_routing.md b/structure/10_subagent_skill_routing.md
index ed1bff17..071ac933 100644
--- a/structure/10_subagent_skill_routing.md
+++ b/structure/10_subagent_skill_routing.md
@@ -6,7 +6,7 @@ aliases: [L14 Design, subagent skill routing, cxc skill attachment]
# L14 — Subagent Skill Routing + Loop/Goal Handoff (Design SOT)
-Status: DESIGN + SHIPPED — **spawn surface status 260710: V1 is codexclaw's default; the model catalog pins sol/terra to V2 and luna to V1, while `features.multi_agent_v2` selects V2 only for fallback models. The surface pins on the first turn.** V2 requires task_name+message and rejects `items`; production builders emit `fork_turns:"none"` plus resolvable message mentions. Historical shape below documents the v1-era evolution. (E5 dispatch builder shipped in L15; lazygap_impl 020 added `INTENT_ROLE`/`routeDispatch` and the E3 spawn PreToolUse hook. When the spawn message is plaintext, the hook normalizes recognized mentions and inlines full SKILL.md bodies on V2-shaped spawns. Native ChatGPT-backend V2 presents ciphertext, so the hook appends a plaintext `[CXC-SKILL-AFFORDANCE]` self-load instruction when it cannot inline a body; the leaf guard and model+effort injection remain reliable. It never invents role baselines or missing surfaces. The old `CODEXCLAW_SPAWN_ATTACH=v1` opt-in is gone; V1 `items` remains a manual, strongest channel.) · 2026-07-10
+Status: DESIGN + SHIPPED — **spawn surface status 260710: V1 is codexclaw's default; the model catalog pins sol/terra to V2 and luna to V1, while `features.multi_agent_v2` selects V2 only for fallback models. The surface pins on the first turn.** V2 requires task_name+message and rejects `items`; production builders emit `fork_turns:"none"` plus resolvable message mentions. Historical shape below documents the v1-era evolution. (E5 dispatch builder shipped in L15; lazygap_impl 020 added `INTENT_ROLE`/`routeDispatch` and the E3 spawn PreToolUse hook. When the spawn message is plaintext, the hook normalizes recognized mentions and inlines full SKILL.md bodies on V2-shaped spawns. Native ChatGPT-backend V2 presents ciphertext; the hook checks the Fernet envelope structure and preserves recognized ciphertext byte-for-byte, disclosing omitted hook text to the caller. Only plaintext V2 receives the leaf guard and self-load affordance; metadata-based recursion denial and separate model+effort routing remain active. It never invents role baselines or missing surfaces. The old `CODEXCLAW_SPAWN_ATTACH=v1` opt-in is gone; V1 `items` remains a manual, strongest channel.) · 2026-07-10
> This is the design source of truth for the L14 hardening track. The defect
> diagnosis with file:line evidence lives in
@@ -97,12 +97,14 @@ are therefore the production shared channel, but their delivery differs: V1 turn
spawn message into `UserInput::Text` and parses link/plugin mentions natively; V2 sends
`InterAgentCommunication`, which upstream excludes from skill collection. The codexclaw
spawn hook compensates only when the V2 message reaches it as plaintext by inlining full
-SKILL.md bodies for recognized cxc mentions. Native ChatGPT-backend V2 presents encrypted
-ciphertext, so normalization and inlining are safe no-ops there. When no body can be
-inlined, the hook appends a plaintext `[CXC-SKILL-AFFORDANCE]` block telling the child to
-self-load any `$cxc-` / `$codexclaw:cxc-` mention by reading
-`//SKILL.md`; fork inheritance remains a secondary channel. The native
-V2 hook also reliably prepends the leaf guard and injects configured model/effort fields.
+SKILL.md bodies for recognized cxc mentions. Plaintext V2 without an inlined body
+receives a `[CXC-SKILL-AFFORDANCE]` block asking the child to read
+`//SKILL.md`. Native ChatGPT-backend V2 presents ciphertext;
+the hook recognizes canonical base64url Fernet envelope structure, preserves those
+bytes, and discloses that hook-added skill text, scope instructions and prompt
+overrides were omitted. This is structural recognition, not authentication.
+Malformed prefix-like plaintext still receives normal attachment. Metadata-based
+recursion denial and separate configured model/effort routing remain active.
### L15.2 follow-up (SHIPPED as WP2, E3 — mention normalization)
When `message` is plaintext, the spawn PreToolUse hook scans it for known cxc mentions,
diff --git a/structure/20_pabcd_dispatch_doctrine.md b/structure/20_pabcd_dispatch_doctrine.md
index ef76fcbb..0c72ca4b 100644
--- a/structure/20_pabcd_dispatch_doctrine.md
+++ b/structure/20_pabcd_dispatch_doctrine.md
@@ -139,9 +139,13 @@ codexclaw translation:
lane first — it is almost always a read-only packet on a `worker`.
- **Architect consultation in formal P.** Main evidence -> architect proposal -> main
executable plan -> same architect reflection -> independent A reviewer. Main retains
- every final decision. Recheck only named module/data/interface/flow decision changes;
+ every final decision. Formal P includes C2 compact and plan-only P plans; C0/C1
+ fast-path work needs neither consultation nor a consultation record. Plan output
+ records the actual handle, proposal, main dispositions and reflection against the
+ submitted plan revision. P hook/CLI hints expose the sequence before planning.
+ Recheck only named module/data/interface/flow decision changes;
reuse context within one plan and start fresh for a new plan. Existing installed
- owners: `pabcd/references/phase-plan.md`, `phase-audit.md` and `delegation.md`.
+ owners: `pabcd/references/phase-plan.md`, `plan-output.md`, `phase-audit.md` and `delegation.md`.
This is E7 guidance, not a new phase or runtime consultation gate. Missing consultation
is not complete; architect reflection never substitutes for independent A review.
- **Audit (A) is never skipped.** Before B, the main session must dispatch an independent
@@ -179,17 +183,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.
@@ -199,8 +212,11 @@ codexclaw translation:
`devlog/_plan/260711_dispatch_economy_docs_site/005_research_claim_ledger.md`).
For authorized source/log investigation, [dev's Discovery delegation](../plugins/codexclaw/skills/dev/SKILL.md#discovery-delegation)
owns the early ownership decision, concrete local exceptions and reconsideration
- after scope growth or truncation. Parallelism alone does not reduce returned context;
- this guidance adds no runtime enforcement or mandatory spawn count.
+ after scope growth or truncation. For implementation, [dev's Implementation delegation](../plugins/codexclaw/skills/dev/SKILL.md#implementation-delegation)
+ owns the decision: main implements directly by default, with optional executors
+ for independent parallel or bounded routine work. P records selected executor
+ assignments and B follows them. Parallelism alone does not reduce
+ returned context; this guidance adds no runtime enforcement or mandatory spawn count.
Four clauses:
- *Three-axis delegability test.* Decide what to delegate by
**specifiability** (the TASK packet can carry the full spec, including its
@@ -267,11 +283,15 @@ channel (`buildSpawnItems`/`SpawnPayload.items`). Prefer `[$cxc-](skill://
plugin-native `$codexclaw:cxc-` when a link is unsafe. The WP2 E3
spawn PreToolUse hook normalizes known broken/bare cxc mentions and inlines recognized
skill bodies on V2-shaped spawns only when `message` reaches it as plaintext. Native
-ChatGPT-backend V2 presents ciphertext, so both operations are no-ops there. When no body
-can be inlined, the hook appends a plaintext `[CXC-SKILL-AFFORDANCE]` block telling the
+ChatGPT-backend V2 can present ciphertext. The hook preserves that message byte-for-byte:
+putting plaintext inside the encrypted slot makes the backend reject the child task.
+It reports omitted hook-added instructions to the caller; this is not skill delivery.
+Metadata-based recursion denial and separate model/effort routing still apply.
+On plaintext V2, when no body can be inlined, the hook appends a
+`[CXC-SKILL-AFFORDANCE]` block telling the
child to self-load any `$cxc-` / `$codexclaw:cxc-` mention from
`//SKILL.md`; fork inheritance remains a secondary channel. The native
-V2 hook also carries the leaf guard and configured model/effort injection; it does not add
+V2 plaintext hook also carries the leaf guard; it does not add
role baselines or infer surface skills.
Dispatchers remain responsible for naming every required skill (DISPATCH-TASK-01).
diff --git a/structure/INDEX.md b/structure/INDEX.md
index 0b175db8..2b2e6e9f 100644
--- a/structure/INDEX.md
+++ b/structure/INDEX.md
@@ -199,7 +199,7 @@ The manifest wires 22 hook JSON files; `plugin.json` `hooks` and `hooks/*.json`
| `PreToolUse` `^request_user_input$` | `hooks/pre-tool-use-guarding-interview-in-goal.json` | same pabcd-state CLI | denies user-input/interview tool use while native goal mode is active or unreadable |
| `PostToolUse` `^request_user_input$` | `hooks/post-tool-use-capturing-interview-answers.json` | same pabcd-state CLI | captures interview question/answer events to the ledger; in an interactive I-phase also reinjects the Mind-rescan directive as `additionalContext` (L18); never blocks |
| `SubagentStop` `^worker$` | `hooks/subagent-stop-verifying-evidence.json` | same pabcd-state CLI | verifies worker evidence expectations on subagent stop |
-| `PreToolUse` spawn/collaboration variants | `hooks/pre-tool-use-attaching-skills.json` | `node "${PLUGIN_ROOT}/components/subagent-config/dist/spawn-attach-hook.js" hook pre-tool-use` | normalizes cxc mentions and inlines recognized skill bodies on plaintext V2-shaped spawns; when no body can be inlined (including encrypted native V2), appends a plaintext `[CXC-SKILL-AFFORDANCE]` child self-load instruction; also applies model+effort routing plus leaf guards on both surfaces and never adds missing skills |
+| `PreToolUse` spawn/collaboration variants | `hooks/pre-tool-use-attaching-skills.json` | `node "${PLUGIN_ROOT}/components/subagent-config/dist/spawn-attach-hook.js" hook pre-tool-use` | normalizes mentions, inlines recognized skills and adds scope instructions on plaintext; plaintext V2 without inlined bodies gets a self-load affordance. Native V2 ciphertext stays byte-identical and omitted hook text is disclosed to the caller. Metadata-based recursion denial and separate model+effort routing remain active |
| `PostCompact` | `hooks/post-compact-resetting-reinject-cursor.json` | `node "${PLUGIN_ROOT}/components/pabcd-state/dist/cli.js" hook post-compact` | resets reinjection cursor/stage context after compaction |
| `PreToolUse` `^(apply_patch|Write|Edit)$` | `hooks/pre-tool-use-linting-apply-patch.json` | `node "${PLUGIN_ROOT}/components/pabcd-state/dist/cli.js" hook pre-tool-use-edit` | combined edit path: comment lint (deny-capable) then IDLE-edit arming advisory |
| `PostToolUse` `^(view_image|browser:control-in-app-browser|chrome:control-chrome|computer-use:computer-use|apply_patch)$` | `hooks/post-tool-use-tracking-render-observations.json` | `node "${PLUGIN_ROOT}/components/pabcd-state/dist/cli.js" hook post-tool-use-render-observation` | tracks render/visual observation events for QA evidence |
@@ -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` |
@@ -323,7 +324,7 @@ Subagent role TOMLs live under `plugins/codexclaw/agents/`: `explorer`, `reviewe
| `executor` | `worker` | scoped yes | bounded implementation inside an assigned write scope |
| `architect` | `architect` | no | design proposals and executable-plan reflection; main owns decisions |
-The subagent config component selects per-role model, effort and prompt overrides; default mode inherits the main Codex model. Architect attaches the existing `dev` and `dev-architecture` skills. Formal P and changed-decision rechecks follow `skills/pabcd/references/phase-plan.md`, `phase-audit.md` and `delegation.md`; these are agent-followed instructions, not new runtime enforcement.
+The subagent config component selects per-role model, effort and prompt overrides; default mode inherits the main Codex model. Architect attaches the existing `dev` and `dev-architecture` skills. Formal P (including compact C2 and plan-only P plans) and changed-decision rechecks follow `skills/pabcd/references/phase-plan.md`, `phase-audit.md` and `delegation.md`; `plan-output.md` records the actual consultation and same-architect reflection against the executable-plan revision. P hook/CLI hints expose that sequence. Implementation ownership (main implements directly by default; optional executor assignments for independent parallel or bounded routine work are recorded in P and followed in B) follows `skills/dev/SKILL.md` Implementation delegation and `skills/pabcd/references/plan-output.md`; these are agent-followed instructions, not new runtime enforcement.
---
From df4802cdce9aafec33e20fc9dbc5dbadceb251bf Mon Sep 17 00:00:00 2001
From: thisisjun786 <259586770+thisisjun786@users.noreply.github.com>
Date: Wed, 16 Sep 2026 02:41:22 +0900
Subject: [PATCH 3/4] [agent] docs(devlog): record selective execution
verification
---
.../260916_selective_execution/000_plan.md | 0
.../010_implementation.md | 54 +++++++++++++++++++
2 files changed, 54 insertions(+)
rename devlog/{_plan => _fin}/260916_selective_execution/000_plan.md (100%)
create mode 100644 devlog/_fin/260916_selective_execution/010_implementation.md
diff --git a/devlog/_plan/260916_selective_execution/000_plan.md b/devlog/_fin/260916_selective_execution/000_plan.md
similarity index 100%
rename from devlog/_plan/260916_selective_execution/000_plan.md
rename to devlog/_fin/260916_selective_execution/000_plan.md
diff --git a/devlog/_fin/260916_selective_execution/010_implementation.md b/devlog/_fin/260916_selective_execution/010_implementation.md
new file mode 100644
index 00000000..4fcac453
--- /dev/null
+++ b/devlog/_fin/260916_selective_execution/010_implementation.md
@@ -0,0 +1,54 @@
+# Selective execution implementation
+
+The owning implementation task now works directly by default. Optional executor
+assignments have a scope and check, while formal-P architect consultation and
+independent review retain their existing requirements. Choosing direct work does
+not bypass recovery for a previously dispatched child.
+
+## Changes and provenance
+
+The consolidated implementation carries the reviewed union of #177 and #179
+(19afc764, upstream base 03541398). Relative to that union, 13 files change:
+implementation policy, P/B hook and CLI hints, their output assertions, the public
+subagent guide, changelog and structure mirrors. The complete subagent-config
+component and the waiting, delegation and formal-P consultation references remain
+byte-identical. This withdraws executor-first wording without dropping ciphertext
+preservation or reconciled task failure recovery. No dependency or runtime gate
+was added.
+
+Credits: thisisjun786, original contributions in #177 and #179.
+
+## Verification
+
+- Architect proposal and same-architect reflection: ALIGNED against plan e9c4484d.
+ Separate plan audit: PASS. Fresh implementation review: PASS, no blockers.
+- Phase hook/CLI suite: before hint changes, 185 tests with five expected failures
+ for the old ownership instructions; after changes, 185 pass and zero fail.
+ Existing phase-state and architect checks remain.
+- `npm run build`: exit 0, 181 compiled files.
+- `TMPDIR= CODEXCLAW_SKIP_REPOMAP_SMOKE=1 npm test`:
+ exit 0, 3177 total, 3103 pass, 74 conditional skips, zero failures.
+- `npm run gate` and
+ `node plugins/codexclaw/scripts/inventory.mjs --check --tests 3177`: pass.
+- `git diff --check`: pass.
+- Compiled CLI/hook QA: five real invocations on source and five on the updated
+ installation verify P/B output and CLI phase state using isolated fixtures.
+ The first QA fixture tried the illegal IDLE-to-B transition and was corrected
+ to start at A; the rejection was expected product behavior.
+- Installed recovery QA: 35 real CLI invocations pass, including stopped-child
+ recovery, live/unknown-state reconciliation, input limits and stop precedence.
+- Local source receives the 13-file delta; nine changed installed files are
+ replaced after drift checks. Backups and source/install reapplication patches
+ are retained privately. Installed patch reverse/reapply matches original/final
+ bytes. These checks spawn no provider or live worker.
+
+Raw logs, review receipts and installation manifests are retained in the private
+consolidation backup dated 2026-09-16. Agent-followed policy still needs semantic
+review; automated checks do not prove a model will always follow instructions.
+
+## Delivery
+
+Publish one ordinary PR targeting dev, superseding #177 and #179. Leave #178 open
+until upstream integration and link its current criteria to the replacement.
+Hosted CI and remote status are checked separately from the local evidence above.
+No upstream merge or release is part of this unit.
From 7c328045d1c95fb6c27f31fa1bf5d0b7cbdf5cdd Mon Sep 17 00:00:00 2001
From: thisisjun786 <259586770+thisisjun786@users.noreply.github.com>
Date: Wed, 16 Sep 2026 02:54:00 +0900
Subject: [PATCH 4/4] [agent] fix(delegation): preserve existing CXC
implementation policy
---
CHANGELOG.md | 4 -
.../260916_selective_execution/000_plan.md | 132 +++++++-----------
.../010_implementation.md | 76 +++++-----
.../src/content/docs/guides/subagents.md | 4 -
.../components/pabcd-state/dist/hook.js | 2 -
.../pabcd-state/dist/orchestrate-cli.js | 22 ++-
.../components/pabcd-state/src/hook.ts | 2 -
.../pabcd-state/src/orchestrate-cli.ts | 22 ++-
.../components/pabcd-state/test/hook.test.ts | 13 +-
.../pabcd-state/test/orchestrate-cli.test.ts | 20 +--
plugins/codexclaw/skills/dev/SKILL.md | 22 ---
plugins/codexclaw/skills/pabcd/SKILL.md | 4 +-
.../skills/pabcd/references/plan-output.md | 14 --
structure/20_pabcd_dispatch_doctrine.md | 7 +-
structure/INDEX.md | 2 +-
15 files changed, 118 insertions(+), 228 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c6bf242e..e8e9e0f3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,10 +12,6 @@ All notable changes to codexclaw are documented here. The format follows
reflection by the same architect before independent audit. C0/C1 fast paths and
explicit user limits keep their existing precedence; this adds guidance, not a
runtime gate.
-- The owning task's main agent implements directly by default, including inside
- independently delegated tasks. Executors are optional for independent parallel
- or bounded routine work. P records selected assignments; P/B hook and CLI hints
- follow the same policy. Managed reclaim still requires `main-direct`.
### Fixed
diff --git a/devlog/_fin/260916_selective_execution/000_plan.md b/devlog/_fin/260916_selective_execution/000_plan.md
index 3af44594..aab377e8 100644
--- a/devlog/_fin/260916_selective_execution/000_plan.md
+++ b/devlog/_fin/260916_selective_execution/000_plan.md
@@ -1,82 +1,50 @@
-# Architect consultation and selective execution
-
-The owning implementation task should be able to develop directly. Executor-first
-wording in PR #177 made this an exception even when another coordination layer
-already delegated the issue to an independent task. Consolidate #177 and #179,
-retain required formal-P architect consultation and independent review, and use
-executors selectively for independent parallel work or bounded routine work.
-
-## Scope and execution
-
-Class: C3 policy/hint integration; previously reviewed ciphertext and recovery
-implementations are carried unchanged. Compact plan, architect consultation,
-independent audit, main implementation, checks and delivery; no new runtime gate.
-Loop archetype: satisfy-spec. Trigger: explicit request to consolidate both PRs,
-withdraw executor-first policy, and organize related issues. Goal: one replacement
-PR plus matching local source and installed payload. Non-goals: upstream merge,
-release, model-setting changes, or automatic patch reapplication.
-Verifier: existing phase hook/CLI tests observe injected instructions and actual
-phase state; recovery/ciphertext regressions protect the retained behavior; build,
-full root suite, gate and inventory check integration. Baseline phase tests at
-19afc764: 185 pass, 0 fail. Policy semantics require independent source review.
-Stop: replacement PR published and checked, predecessor PRs closed with links,
-issue #178 updated, local/installed state verified and patch backup preserved.
-Memory artifact: this unit; raw receipts remain outside Git. Expected outcomes:
-verified delivery, or a concrete unresolved blocker. Escalation: scope beyond the
-authorized repositories/install paths; no arbitrary time or token budget is set.
-
-## Decisions and file map
-
-Main implements all edits; architect and reviewer are read-only. The user explicitly
-selected direct implementation with optional executors, so this task needs no
-executor. Original branch histories and local edits remain preserved.
-
-- D1 accepted: `skills/dev/SKILL.md` Implementation delegation defaults to direct
- work by the owning task's main, including an independently delegated task.
- Executor use is optional for disjoint parallel or bounded routine work. No
- justification is required for direct work. Keep headings/links, scoped packets,
- diff verification, and dispatch safety. State the child-task rule generically;
- do not couple CXC to Linear or a model vendor.
-- D1 accepted: `skills/pabcd/references/plan-output.md` records executor assignments
- only when selected; main owns other in-scope work without a special exception.
- B follows assigned scopes and new handoffs remain plan amendments. Update the B
- sentence in `skills/pabcd/SKILL.md` consistently. Architect sections stay intact.
-- D2 accepted: `components/pabcd-state/src/hook.ts` and `orchestrate-cli.ts` P/B
- hints say main implements by default and executor work is optional. Preserve
- formal-P architect sequence, A recheck, user limits, and phase state behavior.
- Build matching `dist/` files. No change to dispatch/permission algorithms.
-- D3 accepted with correction: update existing `test/hook.test.ts` and
- `test/orchestrate-cli.test.ts` assertions on actual injected output. Preserve
- all transition, independent-review and architect checks. Assertions do not
- increase the test inventory; regenerate badges only from the measured total.
- Synchronize `structure/20_pabcd_dispatch_doctrine.md` and `structure/INDEX.md`.
-- D4 accepted: carry the union at 19afc764 relative to upstream 03541398, excluding
- old executor-first devlog additions. Preserve ciphertext guards, tests and
- documentation; preserve `task_failed`, waiting and reconciliation contracts.
- Add missing architect/optional-executor/ciphertext CHANGELOG entries and a short
- public-guide explanation. Original author thisisjun786 retains credit in the
- replacement PR. Old PRs and backup patches retain historical evidence.
-
-## Acceptance and delivery
-
-1. Direct single-task/independent-child implementation needs no executor or excuse.
-2. Optional parallel work has disjoint write scope; routine work has a clear check.
-3. Formal P requires architect proposal, main plan, same-architect reflection, then
- independent A; C0/C1 and explicit user limits retain their existing precedence.
-4. Choosing optional dispatch never bypasses a live child's reconciliation or a
- terminal stop; `main-direct` still governs managed reclaim.
-5. All carried runtime files match reviewed 19afc764 bytes. New P/B instructions are
- exercised through existing hook and real CLI paths; no new automatic spawn.
-6. Publish one ordinary PR to dev. Only then link/close #177 and #179; organize
- #178 with the replacement and current acceptance criteria, leave it open pending
- upstream merge. Preserve remote/local predecessor branches.
-7. Apply the policy delta to local dev and the installed plugin with original-file
- backups, drift checks, regenerated dist and actual installed CLI/hook checks.
-
-## Consultation
-
-Architect proposal D1-D4 received and dispositions recorded above. The same
-architect returned ALIGNED against plan e9c4484d. Independent audit returned PASS
-with no blockers before implementation, retaining the public-guide clarification
-that managed reclaim requires `main-direct`. Proposal, reflection and audit
-receipts remain in private local execution evidence.
+# Architect consultation with existing delegation policy
+
+Formal-P architect consultation needs a concrete proposal, a main-owned executable
+plan and reflection by the same architect before independent review. Consolidate
+the architect and recovery fixes from #177 and #179 while preserving CXC's
+existing delegation-selection policy. Neither executor-first nor main-direct
+implementation becomes a new default.
+
+## Scope
+
+Class: C3 compact policy/hint integration. No new delegation-selection rule,
+runtime gate, dependency or model setting. Preserve reviewed ciphertext handling
+and task failure recovery. Main applies the bounded correction; independent
+review checks the changed instructions and emitted output.
+
+- Restore `plugins/codexclaw/skills/dev/SKILL.md` exactly to upstream 03541398.
+- Remove added implementation-ownership sections from P plan output, B guidance
+ and structure mirrors. Existing DISPATCH-ECONOMY-01 remains the selection owner.
+- Keep formal-P consultation records and P/A hook reminders. CLI P entry repeats
+ only the architect sequence; B returns to its original instructions/output.
+- Keep ciphertext preservation from #177 and retirement/task-failure recovery
+ from #179. Their existing managed-dispatch bounds and stop precedence remain.
+- Update hook/CLI assertions to cover architect output, unchanged phase state and
+ absence of the added implementation-default instructions.
+- Align PR/issue descriptions, local source, installed files and reapply patches.
+
+## Acceptance
+
+1. No new executor-first or main-direct default remains in active policy.
+2. The canonical dev skill equals upstream bytes; existing delegation criteria
+ and build-phase instructions remain unchanged.
+3. Formal P requires architect proposal, main plan and same-architect reflection
+ before independent audit, subject to existing fast paths and explicit limits.
+4. #179's evidence-based retirement and reconciled task-failure recovery remain.
+5. Source/dist, affected tests, build, full suite, gate/inventory, independent
+ review and installed CLI/hook checks agree.
+6. One ordinary PR targets dev. #177 and #179 are superseded without deleting their
+ branches; #178 stays open until upstream integration.
+7. Local and installed corrections have drift checks, backups and verified patches.
+ No upstream merge, release or model-setting change is authorized here.
+
+## Review context
+
+The initial architect proposal and reflection established the retained
+consultation/transport/recovery boundaries. A later clarification withdraws the
+added implementation-default policy and restores the upstream selection rules.
+This correction adds no new architecture decision; it removes the unsupported
+policy extension. The earlier reviews do not certify the corrected diff: a fresh
+independent review and checks cover delivery. Private receipts retain both
+revisions without presenting the withdrawn plan as current behavior.
diff --git a/devlog/_fin/260916_selective_execution/010_implementation.md b/devlog/_fin/260916_selective_execution/010_implementation.md
index 4fcac453..cb5ada6f 100644
--- a/devlog/_fin/260916_selective_execution/010_implementation.md
+++ b/devlog/_fin/260916_selective_execution/010_implementation.md
@@ -1,54 +1,50 @@
-# Selective execution implementation
+# Architect and recovery consolidation
-The owning implementation task now works directly by default. Optional executor
-assignments have a scope and check, while formal-P architect consultation and
-independent review retain their existing requirements. Choosing direct work does
-not bypass recovery for a previously dispatched child.
+The final change strengthens formal-P architect consultation while keeping CXC's
+existing delegation-selection rules. It preserves the transport and recovery
+fixes from #177 and #179 without introducing an executor-first or main-direct
+implementation default.
## Changes and provenance
-The consolidated implementation carries the reviewed union of #177 and #179
-(19afc764, upstream base 03541398). Relative to that union, 13 files change:
-implementation policy, P/B hook and CLI hints, their output assertions, the public
-subagent guide, changelog and structure mirrors. The complete subagent-config
-component and the waiting, delegation and formal-P consultation references remain
-byte-identical. This withdraws executor-first wording without dropping ciphertext
-preservation or reconciled task failure recovery. No dependency or runtime gate
-was added.
-
Credits: thisisjun786, original contributions in #177 and #179.
+The canonical dev skill is restored byte-for-byte to upstream 03541398. Added
+implementation ownership sections and P/B ownership hints are removed. Formal P
+retains proposal, main plan and same-architect reflection before independent audit;
+the CLI repeats that sequence only at P entry. Existing fast paths, explicit
+limits, DISPATCH-ECONOMY-01 and B guidance keep their original roles.
+
+The complete subagent-config component and the waiting/delegation recovery
+references retain the reviewed combined implementation. Recovery requires the
+registered stopped child and reconciliation; cancellation and permission failures
+still stop. Ciphertext shape recognition preserves opaque native messages and
+reports omitted prompt instructions without claiming authentication.
+
## Verification
-- Architect proposal and same-architect reflection: ALIGNED against plan e9c4484d.
- Separate plan audit: PASS. Fresh implementation review: PASS, no blockers.
-- Phase hook/CLI suite: before hint changes, 185 tests with five expected failures
- for the old ownership instructions; after changes, 185 pass and zero fail.
- Existing phase-state and architect checks remain.
+Fresh checks on the corrected revision:
+
+- Independent review: PASS, no blockers. Baseline comparisons confirm the original
+ dev skill, B guidance and DISPATCH-ECONOMY-01; recovery/ciphertext files are unchanged.
+- Phase hook/CLI suite: 185 pass, zero failures.
- `npm run build`: exit 0, 181 compiled files.
- `TMPDIR= CODEXCLAW_SKIP_REPOMAP_SMOKE=1 npm test`:
exit 0, 3177 total, 3103 pass, 74 conditional skips, zero failures.
-- `npm run gate` and
- `node plugins/codexclaw/scripts/inventory.mjs --check --tests 3177`: pass.
-- `git diff --check`: pass.
-- Compiled CLI/hook QA: five real invocations on source and five on the updated
- installation verify P/B output and CLI phase state using isolated fixtures.
- The first QA fixture tried the illegal IDLE-to-B transition and was corrected
- to start at A; the rejection was expected product behavior.
-- Installed recovery QA: 35 real CLI invocations pass, including stopped-child
- recovery, live/unknown-state reconciliation, input limits and stop precedence.
-- Local source receives the 13-file delta; nine changed installed files are
- replaced after drift checks. Backups and source/install reapplication patches
- are retained privately. Installed patch reverse/reapply matches original/final
- bytes. These checks spawn no provider or live worker.
-
-Raw logs, review receipts and installation manifests are retained in the private
-consolidation backup dated 2026-09-16. Agent-followed policy still needs semantic
-review; automated checks do not prove a model will always follow instructions.
+- `npm run gate`, `inventory.mjs --check --tests 3177` and `git diff --check`: pass.
+- Compiled CLI/hook QA: five real invocations each on source and installation
+ check architect output at P and unchanged B instructions/phase behavior.
+- Installed recovery QA: 35 CLI invocations pass, including stopped-child
+ recovery, live/unknown reconciliation, evidence limits and stop precedence.
+- Nine changed installed files pass pre-write drift checks. The installed dev
+ skill matches upstream bytes. Patch reverse/reapply matches before/after copies.
+
+These checks use isolated fixtures and spawn no live provider or worker.
+Agent-followed policy remains a semantic-review obligation. Raw logs, review
+receipts and patch proofs remain in the private correction backup.
## Delivery
-Publish one ordinary PR targeting dev, superseding #177 and #179. Leave #178 open
-until upstream integration and link its current criteria to the replacement.
-Hosted CI and remote status are checked separately from the local evidence above.
-No upstream merge or release is part of this unit.
+#180 supersedes closed #177 and #179. Original branches are preserved. #178 tracks
+the retained recovery work and stays open until upstream integration. Source and
+installation updates are separate from upstream merge or release.
diff --git a/docs-site/src/content/docs/guides/subagents.md b/docs-site/src/content/docs/guides/subagents.md
index 7bbdffc4..7cfe92ce 100644
--- a/docs-site/src/content/docs/guides/subagents.md
+++ b/docs-site/src/content/docs/guides/subagents.md
@@ -15,10 +15,6 @@ Four roles cover the common subagent workflow:
- **executor** — focused implementation.
- **architect** — read-only design proposals and checks of main-owned executable plans.
-The owning task's main agent implements directly by default, including when it
-receives an issue from another coordinator. Executors are optional for independent
-parallel work or bounded routine tasks with clear write scopes and checks.
-
Formal P keeps the required design sequence: architect proposal, main's executable
plan, then reflection by the same architect before independent review. The existing
C0/C1 fast path and explicit user limits still apply. See the
diff --git a/plugins/codexclaw/components/pabcd-state/dist/hook.js b/plugins/codexclaw/components/pabcd-state/dist/hook.js
index d091c3bd..2f863ff1 100644
--- a/plugins/codexclaw/components/pabcd-state/dist/hook.js
+++ b/plugins/codexclaw/components/pabcd-state/dist/hook.js
@@ -304,7 +304,6 @@ const PHASE_DIRECTIVES = {
"Apply this pointer and its owners within exact user limits and permissions. No-delegation means no dispatch.",
"Load $codexclaw:cxc-pabcd for P and C2+ plan-output; $codexclaw:cxc-dev selects class and relevant surfaces. No implementation yet.",
"Formal P, including C2 compact and plan-only P: obtain the configured read-only architect proposal BEFORE the executable plan, send that concrete plan to the SAME architect for reflection BEFORE A, and record the consultation per $codexclaw:cxc-pabcd phase-plan/plan-output. The C0/C1 fast path needs none.",
- "Implementation ownership: main implements by default; record any optional executor assignment (independent parallel or bounded routine work) with its write scope and check ($codexclaw:cxc-dev Implementation delegation).",
"Plan-only ends with the plan. Forbidden checks: NOT RUN; naming an artifact grants no write permission.",
].join("\n"),
A: [
@@ -318,7 +317,6 @@ const PHASE_DIRECTIVES = {
"[codexclaw: BUILD]",
"Apply this pointer and its owners within exact user limits and permissions. No-delegation means no dispatch.",
"Use $codexclaw:cxc-dev for class/surfaces; authorized PABCD B uses $codexclaw:cxc-pabcd. Implement only authorized scope.",
- "Follow the plan's recorded implementation ownership: main implements other in-scope work directly; a new executor assignment needs a P amendment; verify a returned diff rather than the report.",
"Forbidden checks: NOT RUN; no invented proof.",
].join("\n"),
C: [
diff --git a/plugins/codexclaw/components/pabcd-state/dist/orchestrate-cli.js b/plugins/codexclaw/components/pabcd-state/dist/orchestrate-cli.js
index 3522190a..74ae0c7f 100644
--- a/plugins/codexclaw/components/pabcd-state/dist/orchestrate-cli.js
+++ b/plugins/codexclaw/components/pabcd-state/dist/orchestrate-cli.js
@@ -454,19 +454,13 @@ function hasPabcdCloseRow(
}
/**
- * 260914: the phase directive only reaches a turn through UserPromptSubmit, so a
- * same-turn CLI progression (P -> ... -> B inside one turn) never sees it. Echo a
- * one-line pointer on the two edges whose owner decision is about to be made or
- * executed. Advice only — it changes no gate.
+ * CLI entry can reach P without a UserPromptSubmit turn. Repeat the formal-P
+ * architect consultation pointer there. Advice only; no new phase gate.
*/
-const OWNERSHIP_HINT = {
- P: "formal P: architect proposal -> main executable plan -> same-architect reflection before A (cxc-pabcd phase-plan); implementation ownership: main implements by default; record optional executor assignments for independent parallel or bounded routine work with scope and check (cxc-dev Implementation delegation)",
- B: "implementation ownership: follow recorded executor assignments; main implements other in-scope work directly; a new executor assignment needs a P amendment",
-};
-
-function withOwnershipHint(phase , output ) {
- const hint = OWNERSHIP_HINT[phase];
- return hint ? `${output} [${hint}]` : output;
+function withArchitectHint(phase , output ) {
+ return phase === "P"
+ ? `${output} [formal P: architect proposal -> main executable plan -> same-architect reflection before A (cxc-pabcd phase-plan)]`
+ : output;
}
export function runOrchestrateCli(args , commitHooks = {}, nativeEnv = {}) {
@@ -669,7 +663,7 @@ export function runOrchestrateCli(args
scanEvidence: { scanRounds: state.interview?.scanRounds ?? 0, highContradictionCount: gate.highContradictionCount },
...(args.attest?.did ? { evidence: args.attest.did } : {}),
});
- return { code: 0, output: withOwnershipHint("P", `orchestrate P: I → P (agent override, session ${sessionId})`) };
+ return { code: 0, output: withArchitectHint("P", `orchestrate P: I → P (agent override, session ${sessionId})`) };
} else {
// Not ready and no override: advise-block with gate warnings.
return {
@@ -1134,7 +1128,7 @@ export function runOrchestrateCli(args
reason: "cli",
...(args.attest?.did ? { evidence: args.attest.did } : {}),
});
- return { code: 0, output: withOwnershipHint(result.state.phase, `orchestrate ${args.verb}: current=${state.phase} -> ${result.state.phase} (${state.phase} → ${result.state.phase}, session ${sessionId})`) };
+ return { code: 0, output: withArchitectHint(result.state.phase, `orchestrate ${args.verb}: current=${state.phase} -> ${result.state.phase} (${state.phase} → ${result.state.phase}, session ${sessionId})`) };
}
/**
* #48: candidate trees to check for the SAME session id. Deliberately shallow —
diff --git a/plugins/codexclaw/components/pabcd-state/src/hook.ts b/plugins/codexclaw/components/pabcd-state/src/hook.ts
index b214df9b..a0a6d3e5 100644
--- a/plugins/codexclaw/components/pabcd-state/src/hook.ts
+++ b/plugins/codexclaw/components/pabcd-state/src/hook.ts
@@ -304,7 +304,6 @@ const PHASE_DIRECTIVES: Partial> = {
"Apply this pointer and its owners within exact user limits and permissions. No-delegation means no dispatch.",
"Load $codexclaw:cxc-pabcd for P and C2+ plan-output; $codexclaw:cxc-dev selects class and relevant surfaces. No implementation yet.",
"Formal P, including C2 compact and plan-only P: obtain the configured read-only architect proposal BEFORE the executable plan, send that concrete plan to the SAME architect for reflection BEFORE A, and record the consultation per $codexclaw:cxc-pabcd phase-plan/plan-output. The C0/C1 fast path needs none.",
- "Implementation ownership: main implements by default; record any optional executor assignment (independent parallel or bounded routine work) with its write scope and check ($codexclaw:cxc-dev Implementation delegation).",
"Plan-only ends with the plan. Forbidden checks: NOT RUN; naming an artifact grants no write permission.",
].join("\n"),
A: [
@@ -318,7 +317,6 @@ const PHASE_DIRECTIVES: Partial> = {
"[codexclaw: BUILD]",
"Apply this pointer and its owners within exact user limits and permissions. No-delegation means no dispatch.",
"Use $codexclaw:cxc-dev for class/surfaces; authorized PABCD B uses $codexclaw:cxc-pabcd. Implement only authorized scope.",
- "Follow the plan's recorded implementation ownership: main implements other in-scope work directly; a new executor assignment needs a P amendment; verify a returned diff rather than the report.",
"Forbidden checks: NOT RUN; no invented proof.",
].join("\n"),
C: [
diff --git a/plugins/codexclaw/components/pabcd-state/src/orchestrate-cli.ts b/plugins/codexclaw/components/pabcd-state/src/orchestrate-cli.ts
index 75feeaaa..0eea24af 100644
--- a/plugins/codexclaw/components/pabcd-state/src/orchestrate-cli.ts
+++ b/plugins/codexclaw/components/pabcd-state/src/orchestrate-cli.ts
@@ -454,19 +454,13 @@ function hasPabcdCloseRow(
}
/**
- * 260914: the phase directive only reaches a turn through UserPromptSubmit, so a
- * same-turn CLI progression (P -> ... -> B inside one turn) never sees it. Echo a
- * one-line pointer on the two edges whose owner decision is about to be made or
- * executed. Advice only — it changes no gate.
+ * CLI entry can reach P without a UserPromptSubmit turn. Repeat the formal-P
+ * architect consultation pointer there. Advice only; no new phase gate.
*/
-const OWNERSHIP_HINT: Partial> = {
- P: "formal P: architect proposal -> main executable plan -> same-architect reflection before A (cxc-pabcd phase-plan); implementation ownership: main implements by default; record optional executor assignments for independent parallel or bounded routine work with scope and check (cxc-dev Implementation delegation)",
- B: "implementation ownership: follow recorded executor assignments; main implements other in-scope work directly; a new executor assignment needs a P amendment",
-};
-
-function withOwnershipHint(phase: Phase, output: string): string {
- const hint = OWNERSHIP_HINT[phase];
- return hint ? `${output} [${hint}]` : output;
+function withArchitectHint(phase: Phase, output: string): string {
+ return phase === "P"
+ ? `${output} [formal P: architect proposal -> main executable plan -> same-architect reflection before A (cxc-pabcd phase-plan)]`
+ : output;
}
export function runOrchestrateCli(args: OrchestrateCliArgs | OrchestrateCliHelpArgs, commitHooks: OrchestrateCommitHooks = {}, nativeEnv: NodeJS.ProcessEnv = {}): CliResult {
@@ -669,7 +663,7 @@ export function runOrchestrateCli(args: OrchestrateCliArgs | OrchestrateCliHelpA
scanEvidence: { scanRounds: state.interview?.scanRounds ?? 0, highContradictionCount: gate.highContradictionCount },
...(args.attest?.did ? { evidence: args.attest.did } : {}),
});
- return { code: 0, output: withOwnershipHint("P", `orchestrate P: I → P (agent override, session ${sessionId})`) };
+ return { code: 0, output: withArchitectHint("P", `orchestrate P: I → P (agent override, session ${sessionId})`) };
} else {
// Not ready and no override: advise-block with gate warnings.
return {
@@ -1134,7 +1128,7 @@ export function runOrchestrateCli(args: OrchestrateCliArgs | OrchestrateCliHelpA
reason: "cli",
...(args.attest?.did ? { evidence: args.attest.did } : {}),
});
- return { code: 0, output: withOwnershipHint(result.state.phase, `orchestrate ${args.verb}: current=${state.phase} -> ${result.state.phase} (${state.phase} → ${result.state.phase}, session ${sessionId})`) };
+ return { code: 0, output: withArchitectHint(result.state.phase, `orchestrate ${args.verb}: current=${state.phase} -> ${result.state.phase} (${state.phase} → ${result.state.phase}, session ${sessionId})`) };
}
/**
* #48: candidate trees to check for the SAME session id. Deliberately shallow —
diff --git a/plugins/codexclaw/components/pabcd-state/test/hook.test.ts b/plugins/codexclaw/components/pabcd-state/test/hook.test.ts
index 5895c23a..dd977045 100644
--- a/plugins/codexclaw/components/pabcd-state/test/hook.test.ts
+++ b/plugins/codexclaw/components/pabcd-state/test/hook.test.ts
@@ -114,12 +114,11 @@ test("wp3: phase pointers retain owners and active work-phase boundaries", () =>
assert.match(interviewDirective(), /\$codexclaw:cxc-interview/);
assert.match(interviewDirective(), /Mind dispatch/i);
assert.match(phaseDirective("P"), /No implementation yet/);
+ for (const phase of ["P", "B"] as const) {
+ assert.doesNotMatch(phaseDirective(phase), /implementation ownership|main implements by default|defaults to the configured executor/i);
+ }
assert.match(phaseDirective("A"), /cxc-dev-code-reviewer/);
assert.match(phaseDirective("C"), /C-RENDER-GROUNDING-01/);
- assert.match(phaseDirective("P"), /Implementation ownership: main implements by default/);
- assert.match(phaseDirective("B"), /recorded implementation ownership/);
- assert.match(phaseDirective("B"), /main implements other in-scope work directly/);
- assert.doesNotMatch(phaseDirective("P"), /defaults to the configured executor|main needs a stated reason/);
const bound = phaseDirective("B", { activeWorkPhase: { id: "wp3", title: "minimal hooks" } });
assert.match(bound, /ACTIVE WORK-PHASE: wp3 — minimal hooks/);
assert.match(bound, /other work-phases are OUT OF SCOPE until D closes/);
@@ -129,13 +128,12 @@ test("260914: P and A directives carry the architect consultation contract", ()
const p = phaseDirective("P");
// Formal-P sequence: read-only architect proposal before the executable plan,
// the concrete plan back to the SAME architect for reflection before A, and a
- // recorded consultation — inside the existing user-limit and implementation wording.
+ // recorded consultation — inside the existing user-limit wording.
assert.match(p, /read-only architect proposal BEFORE the executable plan/);
assert.match(p, /SAME architect for reflection BEFORE A/);
assert.match(p, /phase-plan\/plan-output/);
assert.match(p, /C0\/C1 fast path needs none/);
assert.match(p, /No-delegation means no dispatch/);
- assert.match(p, /Implementation ownership: main implements by default/);
const a = phaseDirective("A");
// A's reminder is the amendment recheck, distinct from P's initial sequence:
@@ -146,7 +144,7 @@ test("260914: P and A directives carry the architect consultation contract", ()
assert.match(a, /reviewer stays independent/i);
assert.doesNotMatch(a, /architect proposal BEFORE/i);
- // No other phase carries an initial-consultation hint; B keeps implementation ownership only.
+ // No other phase carries an initial-consultation hint.
for (const phase of ["B", "C", "D"] as const) {
assert.doesNotMatch(phaseDirective(phase), /architect/i, `${phase} directive`);
}
@@ -161,7 +159,6 @@ test("260914: hook P output carries the architect sequence; A output carries the
const pCtx = JSON.parse(pOut.trimEnd()).hookSpecificOutput.additionalContext;
assert.match(pCtx, /read-only architect proposal BEFORE the executable plan/);
assert.match(pCtx, /SAME architect for reflection BEFORE A/);
- assert.match(pCtx, /Implementation ownership: main implements by default/);
const aOut = handleUserPromptSubmit(ups("orchestrate a", cwd, "arch-seq", "t2"));
assert.notEqual(aOut, "");
diff --git a/plugins/codexclaw/components/pabcd-state/test/orchestrate-cli.test.ts b/plugins/codexclaw/components/pabcd-state/test/orchestrate-cli.test.ts
index 8a294a06..adcb0a74 100644
--- a/plugins/codexclaw/components/pabcd-state/test/orchestrate-cli.test.ts
+++ b/plugins/codexclaw/components/pabcd-state/test/orchestrate-cli.test.ts
@@ -2684,7 +2684,7 @@ test("wp7 preservation: CLI D-close keeps dependsOn and outcome", () => {
assert.deepEqual(taskFields(saved), expectedTaskFields);
});
-test("260914: P and B entry echo the implementation-ownership pointer; other verbs do not", () => {
+test("260914: P entry echoes architect consultation; other verbs do not", () => {
const cwd = freshCwd(); // not a repo: captureSourceIdentity is "unavailable", so B>C is not delta-gated
try {
const id = "wp3-hint";
@@ -2694,11 +2694,8 @@ test("260914: P and B entry echo the implementation-ownership pointer; other ver
const toP = runOrchestrateCli({ verb: "P", attest: null, session: id, cwd, json: false });
assert.equal(toP.code, 0, toP.output);
assert.equal(readState(cwd, id).phase, "P");
- assert.match(toP.output, /implementation ownership/);
+ assert.doesNotMatch(toP.output, /implementation ownership|main implements by default/);
assert.match(toP.output, /architect proposal -> main executable plan -> same-architect reflection before A/);
- assert.match(toP.output, /main implements by default/);
- assert.match(toP.output, /optional executor assignments/);
- assert.doesNotMatch(toP.output, /record an owner per planned change/);
// status is read-only and must not echo the pointer while parked at P.
const statusAtP = runOrchestrateCli({ verb: "status", attest: null, session: id, cwd, json: false });
@@ -2715,7 +2712,7 @@ test("260914: P and B entry echo the implementation-ownership pointer; other ver
assert.doesNotMatch(toA.output, /implementation ownership/);
assert.doesNotMatch(toA.output, /architect/i);
- // A -> B carries the pointer.
+ // A -> B keeps the original output without an ownership or architect hint.
const toB = runOrchestrateCli({
verb: "B",
attest: { from: "A", to: "B", did: "audit folded back", auditOutput: "reviewer: GO; refs verified", auditVerdict: "pass" },
@@ -2723,9 +2720,7 @@ test("260914: P and B entry echo the implementation-ownership pointer; other ver
});
assert.equal(toB.code, 0, toB.output);
assert.equal(readState(cwd, id).phase, "B");
- assert.match(toB.output, /implementation ownership/);
- assert.match(toB.output, /main implements other in-scope work directly/);
- assert.match(toB.output, /new executor assignment needs a P amendment/);
+ assert.doesNotMatch(toB.output, /implementation ownership|main implements by default/);
assert.doesNotMatch(toB.output, /architect/i);
// status at B stays clean too.
@@ -2751,7 +2746,7 @@ test("260914: P and B entry echo the implementation-ownership pointer; other ver
} finally { rmSync(cwd, { recursive: true, force: true }); }
});
-test("260914: I->P agent override echoes the implementation-ownership pointer", () => {
+test("260914: I->P agent override echoes architect consultation", () => {
const cwd = freshCwd();
try {
// Same unready-interview fixture as the override tests above (line ~615).
@@ -2766,10 +2761,7 @@ test("260914: I->P agent override echoes the implementation-ownership pointer",
assert.equal(r.code, 0, r.output);
assert.equal(readState(cwd, "s1").phase, "P");
assert.match(r.output, /agent override/);
- assert.match(r.output, /implementation ownership/);
+ assert.doesNotMatch(r.output, /implementation ownership|main implements by default/);
assert.match(r.output, /architect proposal -> main executable plan -> same-architect reflection before A/);
- assert.match(r.output, /main implements by default/);
- assert.match(r.output, /optional executor assignments/);
- assert.doesNotMatch(r.output, /record an owner per planned change/);
} finally { rmSync(cwd, { recursive: true, force: true }); }
});
diff --git a/plugins/codexclaw/skills/dev/SKILL.md b/plugins/codexclaw/skills/dev/SKILL.md
index b0cd831f..ba8b7341 100644
--- a/plugins/codexclaw/skills/dev/SKILL.md
+++ b/plugins/codexclaw/skills/dev/SKILL.md
@@ -203,28 +203,6 @@ Confirm actual model routing from runtime evidence when reporting identity or co
When comparing cost, use main plus child served-model input/cache/output prices and
applicable tiers; token totals alone cannot compare differently priced models.
-### Implementation delegation
-
-The main agent of the owning implementation task implements directly by default.
-This also applies when another coordinator has assigned work to that independent
-task: it does not need to delegate again. Direct implementation needs no special
-justification. Main owns the plan, design decisions, integration and verification.
-
-Use the configured `executor` when delegation helps: independent parallel work
-with disjoint write scopes, or bounded routine work with a clear contract and
-check. Sequential or tightly coupled work can stay with main. Delegate one
-verifiable bundle rather than one packet per file.
-
-When an executor is selected, P records its scope and check; B follows that
-assignment. Main implements other in-scope work directly. A new executor
-assignment mid-B requires a P amendment (`../pabcd/references/plan-output.md`).
-Verify returned implementation against the VCS diff, never the report alone (§3).
-Model, effort, fallback and receipts stay with the configured dispatch path.
-Choosing optional delegation does not authorize reclaiming live work: stop and
-reconciliation requirements still apply, and managed reclaim requires the
-`main-direct` result. No-delegation limits, host restrictions and
-DISPATCH-ECONOMY-01's specifiability / verifiability / judgment axes take precedence.
-
### Capability Routing Hub
**Independent peers:** keep work local and use selective read-only evidence when
diff --git a/plugins/codexclaw/skills/pabcd/SKILL.md b/plugins/codexclaw/skills/pabcd/SKILL.md
index 6f630bb1..34e94918 100644
--- a/plugins/codexclaw/skills/pabcd/SKILL.md
+++ b/plugins/codexclaw/skills/pabcd/SKILL.md
@@ -69,7 +69,7 @@ link is a conditional routing edge, not a command to preload the entire graph.
| Phase / trigger | Mandatory owner before work |
|---|---|
| I | cxc-interview; no active host goal |
-| P, including plan-only | [Plan phase](references/phase-plan.md): architect proposal → main executable plan → same-architect reflection; C2+ plans also read [Plan output](references/plan-output.md) for consultation evidence and any executor assignments |
+| P, including plan-only | [Plan phase](references/phase-plan.md): architect proposal → main executable plan → same-architect reflection; C2+ plans also read [Plan output](references/plan-output.md) for consultation evidence |
| A, if authorized | [Audit phase](references/phase-audit.md) |
| C | [Check phase](references/phase-check.md) |
| P/A specifying render or conditional-path verification | [Check phase](references/phase-check.md), to define reachable activation and observable evidence |
@@ -80,7 +80,7 @@ or justified near-pass exits. C requires fresh relevant proof and SoT sync;
passing unrelated checks is not evidence. Explicit execution restrictions are not
overridden by a reference asking to run a verifier or dispatch a reviewer.
-3. **B — Build**: Implement the audited plan in small atomic commits (DEV-GIT-COMMIT-01). Verify as you go. Stay inside the plan's scope boundary; surface deviations instead of silently expanding scope. Never push to a remote without explicit user approval (DEV-GIT-PUSH-01, ESCALATE). When P declared a stack, follow `DEV-STACK-02` in `cxc-dev` `references/stacked-prs.md`. Follow the plan's recorded executor assignments and verify returned diffs (`cxc-dev` Implementation delegation). Main implements other in-scope work directly; a new executor assignment needs a P amendment.
+3. **B — Build**: Implement the audited plan in small atomic commits (DEV-GIT-COMMIT-01). Verify as you go. Stay inside the plan's scope boundary; surface deviations instead of silently expanding scope. Never push to a remote without explicit user approval (DEV-GIT-PUSH-01, ESCALATE). When P declared a stack, follow `DEV-STACK-02` in `cxc-dev` `references/stacked-prs.md`.
5. **D — Done**: Summarize what was checked with evidence, update STATUS/devlog, commit (local only — pushing remains gated by DEV-GIT-PUSH-01), and confirm no pending work remains for this work-phase before returning to idle. The D summary is written for a reader who was not in the loop — conclusion, what changed, evidence pointers — per [Reader documents](../dev/references/reader-documents.md) READER-DOC-02/04. For loop/multi-pass work, **LOOP-PESSIMIST-01 (DEFAULT)** also records what did not improve, which hypothesis died, and what evidence would show the current direction is wrong; D -> IDLE -> P is a context/bias-flush boundary, so the next cycle resumes from disk artifacts rather than transcript momentum.
## Work-Phase Loop (multi-pass tasks)
diff --git a/plugins/codexclaw/skills/pabcd/references/plan-output.md b/plugins/codexclaw/skills/pabcd/references/plan-output.md
index 115b48d4..a0e5fe79 100644
--- a/plugins/codexclaw/skills/pabcd/references/plan-output.md
+++ b/plugins/codexclaw/skills/pabcd/references/plan-output.md
@@ -47,20 +47,6 @@ the reason does not make it complete. Follow the existing
C0/C1 fast-path work requires neither consultation nor this record. Main owns
the executable plan and final decisions; architect reflection is not A review.
-## Implementation ownership
-
-Main implements the plan's in-scope work directly by default, without an exception
-statement. When selecting an executor for independent parallel or bounded routine
-work, record its write scope, contract and check in the file change map. Parallel
-write scopes must be disjoint. B follows recorded executor assignments; main owns
-other in-scope work. A new executor assignment mid-B remains a P amendment (see
-Escalation condition above). Discovery and review keep their own owners.
-
-```text
-feature implementation and integration — main
-independent fixture update — optional executor: named files, expected data, fixture check
-```
-
## Reader summary
A C2+ unit's `000_plan.md` opens with a reader-facing summary per
diff --git a/structure/20_pabcd_dispatch_doctrine.md b/structure/20_pabcd_dispatch_doctrine.md
index 0c72ca4b..fa707bb4 100644
--- a/structure/20_pabcd_dispatch_doctrine.md
+++ b/structure/20_pabcd_dispatch_doctrine.md
@@ -212,11 +212,8 @@ codexclaw translation:
`devlog/_plan/260711_dispatch_economy_docs_site/005_research_claim_ledger.md`).
For authorized source/log investigation, [dev's Discovery delegation](../plugins/codexclaw/skills/dev/SKILL.md#discovery-delegation)
owns the early ownership decision, concrete local exceptions and reconsideration
- after scope growth or truncation. For implementation, [dev's Implementation delegation](../plugins/codexclaw/skills/dev/SKILL.md#implementation-delegation)
- owns the decision: main implements directly by default, with optional executors
- for independent parallel or bounded routine work. P records selected executor
- assignments and B follows them. Parallelism alone does not reduce
- returned context; this guidance adds no runtime enforcement or mandatory spawn count.
+ after scope growth or truncation. Parallelism alone does not reduce returned context;
+ this guidance adds no runtime enforcement or mandatory spawn count.
Four clauses:
- *Three-axis delegability test.* Decide what to delegate by
**specifiability** (the TASK packet can carry the full spec, including its
diff --git a/structure/INDEX.md b/structure/INDEX.md
index 2b2e6e9f..49beaa98 100644
--- a/structure/INDEX.md
+++ b/structure/INDEX.md
@@ -324,7 +324,7 @@ Subagent role TOMLs live under `plugins/codexclaw/agents/`: `explorer`, `reviewe
| `executor` | `worker` | scoped yes | bounded implementation inside an assigned write scope |
| `architect` | `architect` | no | design proposals and executable-plan reflection; main owns decisions |
-The subagent config component selects per-role model, effort and prompt overrides; default mode inherits the main Codex model. Architect attaches the existing `dev` and `dev-architecture` skills. Formal P (including compact C2 and plan-only P plans) and changed-decision rechecks follow `skills/pabcd/references/phase-plan.md`, `phase-audit.md` and `delegation.md`; `plan-output.md` records the actual consultation and same-architect reflection against the executable-plan revision. P hook/CLI hints expose that sequence. Implementation ownership (main implements directly by default; optional executor assignments for independent parallel or bounded routine work are recorded in P and followed in B) follows `skills/dev/SKILL.md` Implementation delegation and `skills/pabcd/references/plan-output.md`; these are agent-followed instructions, not new runtime enforcement.
+The subagent config component selects per-role model, effort and prompt overrides; default mode inherits the main Codex model. Architect attaches the existing `dev` and `dev-architecture` skills. Formal P (including compact C2 and plan-only P plans) and changed-decision rechecks follow `skills/pabcd/references/phase-plan.md`, `phase-audit.md` and `delegation.md`; `plan-output.md` records the actual consultation and same-architect reflection against the executable-plan revision. P hook/CLI hints expose that sequence. These are agent-followed instructions, not new runtime enforcement.
---