From 9b752f49ef255e29115e7b346ab9be3aca671119 Mon Sep 17 00:00:00 2001 From: thisisjun786 <259586770+thisisjun786@users.noreply.github.com> Date: Mon, 14 Sep 2026 19:58:34 +0900 Subject: [PATCH 1/7] [agent] docs(devlog): executor-first implementation delegation roadmap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P에서 구현 담당을 기록하고 B가 그 담당을 실행하도록 dev/pabcd 지침과 P/B 단계 안내를 연결하는 diff 수준 로드맵. wp2 문구, wp3 hook/CLI 배선, wp4 격리 CODEX_HOME 검증 절차를 각각 decade 문서로 확정했다. 독립 리뷰어 3라운드 감사 통과(blocker 5건 fold, 반박 0건). --- .../000_plan.md | 124 +++++++++++++ .../001_source_evidence.md | 36 ++++ .../010_wp2_policy_text.md | 135 ++++++++++++++ .../020_wp3_directive_wiring.md | 171 ++++++++++++++++++ .../030_wp4_isolated_verification.md | 143 +++++++++++++++ 5 files changed, 609 insertions(+) create mode 100644 devlog/_plan/260914_executor_first_delegation/000_plan.md create mode 100644 devlog/_plan/260914_executor_first_delegation/001_source_evidence.md create mode 100644 devlog/_plan/260914_executor_first_delegation/010_wp2_policy_text.md create mode 100644 devlog/_plan/260914_executor_first_delegation/020_wp3_directive_wiring.md create mode 100644 devlog/_plan/260914_executor_first_delegation/030_wp4_isolated_verification.md diff --git a/devlog/_plan/260914_executor_first_delegation/000_plan.md b/devlog/_plan/260914_executor_first_delegation/000_plan.md new file mode 100644 index 00000000..3b14a086 --- /dev/null +++ b/devlog/_plan/260914_executor_first_delegation/000_plan.md @@ -0,0 +1,124 @@ +# 260914 executor-first implementation delegation + +## Reader summary + +CXC는 지금 "무엇을 탐색에 맡길지"는 안내하지만 "구현을 누가 맡을지"는 안내하지 않는다. +그래서 메인 모델이 명세가 이미 확정된 구현까지 직접 처리하기 쉽고, 비싼 메인 모델이 +싼 executor가 할 수 있는 작업을 대신하게 된다. 이 유닛은 "명세·수정 범위·검증 방법이 +확정된 구현은 설정된 executor에 우선 배정한다"는 기본값을 CXC 지침에 추가하고, +P 계획이 담당을 기록하고 B가 그 기록을 실행하도록 연결한다. 바뀌는 대상은 CXC를 +쓰는 모든 세션의 메인 에이전트이며, 런타임 하드 게이트는 새로 만들지 않는다. + +근거와 소스 앵커는 [001_source_evidence.md](001_source_evidence.md)에 있다. + +## Loop spec + +| Field | Content | +| --- | --- | +| Loop archetype | satisfy-spec. 정해진 문구/배선을 넣고 실제 주입 출력으로 확인한다. | +| Trigger | 사용자 요청: 구현 위임을 기본값으로 만들어 메인 모델 비용을 줄이고, 로컬 구현 후 격리 환경에서 검증한 뒤 CXC에 PR. | +| Goal | P에서 구현 담당이 기록되고 B에서 그 담당이 실행되도록, dev/pabcd 지침과 P/B 단계 안내가 연결된다. | +| Non-goals | 전역 AGENTS.md 수정, 모델별 강제 규칙, 최소 호출 수 강제, 새 런타임 하드 게이트, 설치본 덮어쓰기, push/PR/merge/release. | +| Verifier | `node plugins/codexclaw/scripts/build.mjs`, `npm test`, 격리 CODEX_HOME 설치본에서의 실제 directive 출력. 아래 PLAN-VERIFIER-REAL-01 참고. | +| Stop condition | wp4의 격리 주입 증거와 로컬 커밋까지. push/PR은 별도 승인 단계. | +| Memory artifact | 이 유닛(`devlog/_plan/260914_executor_first_delegation/`)과 goalplan `cxc-executor-p-b-home-jun-code-worktrees-codexcl`. | +| Expected terminal outcomes | DONE = 문구·배선·테스트·격리 주입 증거·로컬 커밋 완료. NOOP = 동등 지침이 이미 존재. BLOCKED = 격리 설치가 호스트 제약으로 불가. NEEDS_HUMAN = 정책 문구 방향 결정 필요. | +| Escalation condition | 같은 packet을 서로 다른 에이전트가 두 번 실패하면 메인이 회수(DISPATCH-RETIRE-01). 계획에 없는 슬라이스를 B 중간에 위임하려면 P 수정이 먼저. | + +## HOTL resource bounds + +- Write scope: `/home/jun/code-worktrees/codexclaw/executor-first-delegation` 워크트리와 + 네이티브 cwd `/home/jun/code/codexclaw`의 `.codexclaw/` 상태·증거, 그리고 wp4의 임시 CODEX_HOME. +- Tool/credential scope: 로컬 셸, git(로컬 커밋까지), 서브에이전트 dispatch. 네트워크·유료 API·계정 변경 없음. +- Token/cost budget: 호스트 토큰 예산은 설정하지 않음. 모델 비용은 메인 + executor 합산으로만 논의하고, 측정 없이 절감액을 주장하지 않는다. +- Wall-clock bound: 이 세션 내. 자원 한도 초과는 DONE이 아니라 BUDGET_EXHAUSTED. + +## Work-phase map (dependency order) + +| Phase | Outcome | Depends on | Doc | +| --- | --- | --- | --- | +| wp1 | 이 로드맵(문서만, 구현 없음) | — | 이 문서 | +| wp2 | 지침 문구: 판단 기준의 단일 소유자와 계획 규약 | wp1 | [010](010_wp2_policy_text.md) | +| wp3 | 단계 안내 배선: hook/CLI가 그 소유자를 가리킴 | wp2 | [020](020_wp3_directive_wiring.md) | +| wp4 | 격리 검증과 로컬 커밋 | wp3 | [030](030_wp4_isolated_verification.md) | + +순서 근거(PHASE-SPLIT-01): 판단 기준이 먼저 존재해야 단계 안내가 그것을 가리킬 수 있고, +배선이 끝나야 격리 환경에서 실제 주입 문자열을 확인할 수 있다. 노력 크기로 자른 분할이 아니다. + +## File change map + +| Path | Action | Phase | +| --- | --- | --- | +| `plugins/codexclaw/skills/dev/SKILL.md` | MODIFY: `### Implementation delegation` 신설 | wp2 | +| `plugins/codexclaw/skills/pabcd/references/plan-output.md` | MODIFY: `## Implementation ownership` 신설 | wp2 | +| `plugins/codexclaw/skills/pabcd/SKILL.md` | MODIFY: B 항목에 담당 실행 문장 | wp2 | +| `structure/20_pabcd_dispatch_doctrine.md` | MODIFY: DISPATCH-ECONOMY-01에 구현 담당 소유자 포인터 | wp2 | +| `structure/INDEX.md` | CONDITIONAL: 규칙/소유자 목록이 실제로 있을 때만 동기화. 없으면 `N/A + 이유` 기록 | wp2 | +| `plugins/codexclaw/components/pabcd-state/src/hook.ts` | MODIFY: P/B directive에 포인터 한 줄 | wp3 | +| `plugins/codexclaw/components/pabcd-state/src/orchestrate-cli.ts` | MODIFY: P/B 진입 성공 출력에 포인터 | wp3 | +| `plugins/codexclaw/components/pabcd-state/dist/hook.js`, `dist/orchestrate-cli.js` | REBUILD: 설치 payload가 dist를 쓰므로 빌드 산출물도 커밋 | wp3 | +| `plugins/codexclaw/components/pabcd-state/test/hook.test.ts` | MODIFY: P/B 포인터 단언 | wp3 | +| `plugins/codexclaw/components/pabcd-state/test/orchestrate-cli.test.ts` | MODIFY: P/B 출력 포인터 단언 + 다른 verb 미포함 | wp3 | +| `plugins/codexclaw/inventory.json` | 변경 없음 예상: `build.mjs`는 inventory를 재생성하지 않는다. 실제로 변하면 그때 함께 커밋 | wp3 | + +Scope boundary (OUT): `/home/jun/.codex/AGENTS.md`, 설치본 `/home/jun/.codex/plugins/cache/**`, +`agents/*.toml` 역할 정의, fallback/모델 선택 코드, 새 CLI 서브커맨드, 전역 하드 게이트. + +## Accept criteria + +1. `dev/SKILL.md`에 구현 위임 기본값과 직접 구현 예외 3종이 있고, 예외가 아닌 사유 + ("순차 작업이라서", "이미 이해해서")를 명시적으로 배제한다. +2. `plan-output.md`가 파일 변경마다 담당과 직접 구현 사유를 요구한다. +3. `pabcd/SKILL.md` B가 계획된 담당을 실행하도록 dev 소유자를 가리킨다. +4. `phaseDirective("P")`와 `phaseDirective("B")` 출력에 포인터가 포함된다. +5. `orchestrate P` / `orchestrate B` 성공 출력에 같은 포인터가 포함되고, `status`와 + `A`/`C`/`D` 출력에는 포함되지 않는다. +6. `npm test` 0 failures, 빌드 exit 0. +7. 격리된 임시 CODEX_HOME의 설치본에서 P/B 진입 시 포인터가 실제 출력에 나타난다. +8. 변경이 `codex/executor-first-delegation`에 로컬 커밋되고 push는 없다. + +### Conditional-path activation (C-ACTIVATION-GROUNDING-01) + +이 유닛이 추가하는 유일한 조건부 경로는 "P/B일 때만 포인터를 붙인다"이다. +활성화 시나리오: wp3 테스트가 `P`와 `B`에 대해 포인터 포함을, `A`/`C`/`D`와 +`status`에 대해 미포함을 각각 단언한다. 관측 효과: 두 경우의 실제 문자열 차이. +wp4는 같은 분기를 설치본에서 한 번 더 실행해 관측한다. + +### PLAN-VERIFIER-REAL-01 + +| Verifier | 이 유닛의 변경을 실제로 읽는가 | 확인 방법 | +| --- | --- | --- | +| `npm test` | 예 | `package.json` test 글롭에 `components/pabcd-state/test/*.test.ts`가 포함됨 | +| `node plugins/codexclaw/scripts/build.mjs` | 예 | `src/*.ts` → `dist/*.js` 변환 대상에 hook/orchestrate-cli 포함 | +| 격리 CODEX_HOME 실행 | 예 | 설치된 payload의 `dist`를 직접 실행해 문자열 관측 | + +`npm test` 실행 전 wp3에서 RED 확인(새 단언이 기존 코드에서 실패)을 먼저 남긴다. + +### PLAN-BYPASS-NAMED-01 + +| Field | Value | +| --- | --- | +| Tier | E7 (agent-followed doctrine). 문구는 훅이 강제하지 않는다. | +| Executing surface | `UserPromptSubmit` 훅의 phase directive와 `cxc orchestrate` CLI 출력 | +| Known bypass | 모델이 주입된 문장을 읽고도 직접 구현할 수 있다. 훅 출력은 조언이며 차단이 아니다. | +| Residual risk | 실제 위임 비율은 보장되지 않는다. 효과는 관측으로만 판정한다. | +| Downgraded wording | 그렇다. "enforcement"가 아니라 "early warning/pointer"로 쓴다. | + +### SoT sync target (SOT-SYNC-01) + +`structure/20_pabcd_dispatch_doctrine.md`가 이 유닛의 SoT다. wp2에서 함께 패치한다. +`structure/INDEX.md`는 실제로 규칙/소유자 목록을 들고 있을 때만 동기화하고, +아니면 010 문서에 `N/A + 이유`로 남긴다. + +## Implementation ownership (이 유닛 자체의 분담) + +이 유닛은 제안하는 규칙을 스스로 적용한다. + +```text +wp1 로드맵과 정책 문구 결정 — main: 판단 자체가 산출물이라 위임 불가 +wp2 지침 문구 반영 — main: 문장 자체가 판단물이며 슬라이스가 작음 +wp3 hook/CLI 배선과 테스트 — executor: 수정 파일과 검증 방법이 확정됨 +wp4 격리 설치 검증 — main: 호스트 환경 조작과 증거 판정이 필요 +``` + +wp3 위임 결과는 VCS diff로 검증한다(보고서만으로는 완료로 보지 않는다). diff --git a/devlog/_plan/260914_executor_first_delegation/001_source_evidence.md b/devlog/_plan/260914_executor_first_delegation/001_source_evidence.md new file mode 100644 index 00000000..313670d9 --- /dev/null +++ b/devlog/_plan/260914_executor_first_delegation/001_source_evidence.md @@ -0,0 +1,36 @@ +# 001 source evidence + +읽은 지점과 사실만 기록한다. 판단은 000/decade 문서가 소유한다. + +## 지금 존재하는 것 + +- `plugins/codexclaw/skills/dev/SKILL.md:167` `### Discovery delegation` — 탐색 위임 기준의 소유자. + 같은 절 `:201`은 "Discovery does not replace implementation delegation or independent review."라고 + 말하지만, 그 implementation delegation 기준을 정의하는 절은 이 파일에 없다. +- `structure/20_pabcd_dispatch_doctrine.md:196` DISPATCH-ECONOMY-01 — 3축 위임 가능성 판정, + `:227` *Model routing* — 정형화된 구현 슬라이스는 기본적으로 싸고 빠른 모델 계열로 보낸다. + `:200`은 탐색 소유권을 dev의 Discovery delegation에 위임한다. 구현 담당에 대한 같은 포인터는 없다. +- `plugins/codexclaw/skills/pabcd/references/delegation.md:73` — "For implementation dispatch, + prefer `executor` when exposed by the live schema." 전송 방법은 정의돼 있다. +- `plugins/codexclaw/skills/pabcd/references/plan-output.md:8` 9필드 loop-spec 표 — 담당 필드 없음. + `:18` Escalation condition — "pushing a slice to a worker requires a P-phase amendment, + never a mid-B improvisation." 즉 B 중간 위임은 이미 금지돼 있는데, P에서 담당을 정하라는 요구가 없다. +- `plugins/codexclaw/skills/pabcd/SKILL.md:83` B 항목 — 구현·검증·범위 이탈만 말하고 담당은 말하지 않는다. +- `plugins/codexclaw/components/pabcd-state/src/hook.ts:292` `PHASE_DIRECTIVES`, + `:302` P, `:314` B, `:360` `phaseDirective()` — 주입 문자열의 단일 소유 지점. +- `plugins/codexclaw/components/pabcd-state/src/orchestrate-cli.ts:1121` — 전이 성공 출력은 + `orchestrate : current=X -> Y (...)` 한 줄이며 단계 안내를 담지 않는다. + `:656`은 I→P 오버라이드 경로의 별도 성공 출력이다. + +## 현재 설정 (확인 시점 2026-09-14) + +`readSettings()` 해석 결과 `/home/jun/code/codexclaw`, `/home/jun/code/opencodex`, +`/home/jun/code/quota-monitor` 모두 executor = `devin/swe-2` / `high`, +fallback = `combo/grok-4.6` / `xhigh`, source=global. `$CODEX_HOME/agents/executor.toml` 존재. +이는 설정 해석 결과이며, 실제 서비스 모델이나 비용 절감의 증거가 아니다. + +## 관측된 행동 (원인 아님) + +연결된 작업 `01a09ed3-c8ea-77c1-9e29-bbda8fc4d299`에서 생성된 서브에이전트는 리뷰어 8개, +탐색·구현 0개였다. 같은 세션이 dev/SKILL.md의 Discovery delegation 본문을 실제로 읽었으므로 +"지침이 전달되지 않았다"는 설명은 성립하지 않는다. 단일 사례이므로 모델 일반 성향의 증거로 쓰지 않는다. diff --git a/devlog/_plan/260914_executor_first_delegation/010_wp2_policy_text.md b/devlog/_plan/260914_executor_first_delegation/010_wp2_policy_text.md new file mode 100644 index 00000000..3f965db2 --- /dev/null +++ b/devlog/_plan/260914_executor_first_delegation/010_wp2_policy_text.md @@ -0,0 +1,135 @@ +# 010 wp2 — 지침 문구 (policy text) + +구현 담당 판단의 소유자를 한 곳에 만들고, 계획 규약과 B 단계가 그것을 가리키게 한다. +런타임 강제는 추가하지 않는다(E7). + +## Owner + +main. 문장 자체가 판단물이고 슬라이스가 작다. + +## Change 1 — `plugins/codexclaw/skills/dev/SKILL.md` + +Anchor: `### Discovery delegation` 절 마지막 줄(현재 204행, +"applicable tiers; token totals alone cannot compare differently priced models.")과 +`### Capability Routing Hub` 사이에 새 절을 삽입한다. + +AFTER (삽입할 전체 텍스트): + +```markdown +### Implementation delegation + +Once a slice is specified — goal, editable files, the contract it must preserve +and the check that proves it — assign it to the configured `executor` by +default. Sequential work qualifies: a dependency order decides the order of the +packets, not who types the diff. Main keeps the specification, the load-bearing +judgment, the review of the returned diff and the integration. + +Implementing a specified slice locally needs a stated reason, and only three +hold by default: the edit is small enough that packaging it costs more than +making it (§0.1), a load-bearing judgment inside the slice is still open so the +packet cannot state its decision boundary, or handoff plus re-work measurably +exceeds the delegated work. "It is sequential", "I already understand it" and +"typing it is faster" are not reasons; neither is read-only-style parallelism, +which is a separate question from who implements. + +Delegate one verifiable bundle — the slice a single check can prove — rather +than one packet per file. P records the owner of each planned change and B +executes that assignment (`../pabcd/references/plan-output.md`); handing a new +slice to a subagent mid-B still requires a P amendment. Model, effort, fallback +and receipts stay with the configured dispatch path. Verify a returned +implementation against the VCS diff, never the report alone (§3). No-delegation +limits, host restrictions and DISPATCH-ECONOMY-01's specifiability / +verifiability / judgment axes take precedence. +``` + +검증: `rg -n 'Implementation delegation' plugins/codexclaw/skills/dev/SKILL.md`가 +새 절 헤더를 반환하고, 기존 `:201` "Discovery does not replace implementation +delegation" 문장이 그대로 남아 있을 것. + +## Change 2 — `plugins/codexclaw/skills/pabcd/references/plan-output.md` + +Anchor: 9필드 표 아래 본문 마지막 문단(현재 23-27행, "Scope restrictions remain +authoritative. ...")과 `## Reader summary` 사이에 새 절을 삽입한다. +9필드 표와 "nine concepts" 문구는 건드리지 않는다(개수 불일치 방지). + +AFTER (삽입할 전체 텍스트): + +```markdown +## Implementation ownership + +The file change map names an owner for each planned change: the configured +`executor` for slices whose scope and check are settled, or main with a +one-line reason from `cxc-dev`'s Implementation delegation exceptions. A plan +that assigns nothing has not decided; it has defaulted to main silently. B +executes the recorded assignment, and a new handoff mid-B remains a P amendment +(see Escalation condition above). Discovery and review keep their own owners. + +```text +transform helper + its unit test — executor: scope and check are settled +shared policy wording and integration — main: the judgment is the deliverable +one-line typo in a file already open — main: packaging costs more than the edit +``` +``` + +검증: `rg -n 'Implementation ownership' plugins/codexclaw/skills/pabcd/references/plan-output.md`. +중첩 코드펜스이므로 삽입 후 파일을 실제로 열어 펜스가 깨지지 않았는지 확인한다. + +## Change 3 — `plugins/codexclaw/skills/pabcd/SKILL.md` + +Anchor: 83행 B 항목의 마지막 문장 +("When P declared a stack, follow `DEV-STACK-02` in `cxc-dev` `references/stacked-prs.md`.") +뒤에 한 문장을 같은 줄에 이어 붙인다. + +AFTER (해당 줄의 꼬리): + +```text +... When P declared a stack, follow `DEV-STACK-02` in `cxc-dev` `references/stacked-prs.md`. Execute the plan's recorded implementation ownership — specified slices go to the configured executor and main verifies the returned diff (`cxc-dev` Implementation delegation); a slice the plan did not assign needs a P amendment, not a mid-B improvisation. +``` + +## Change 4 — `structure/20_pabcd_dispatch_doctrine.md` + +Anchor: DISPATCH-ECONOMY-01 본문에서 탐색 소유권을 dev로 넘기는 문장(현재 200-203행) +바로 뒤에 구현 담당 소유자 포인터를 추가한다. + +BEFORE: + +```text + 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: + +```text + 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 same decision: a specified slice defaults to the configured executor, and + the P plan records the owner that B then executes. Parallelism alone does not + reduce returned context; this guidance adds no runtime enforcement or mandatory spawn count. +``` + +줄바꿈 주의: `rg`는 줄 단위로 매치하므로 "Implementation delegation"을 줄 경계로 쪼개면 +아래 accept criteria가 빈 결과(exit 1)를 낸다. 링크 라벨을 한 줄에 유지한다. + + +## Change 5 — `structure/INDEX.md` (conditional) + +INDEX가 규칙 ID나 스킬 절 목록을 실제로 들고 있으면 같은 변경을 반영한다. +들고 있지 않으면(현재 확인된 범위에서는 파일 단위 맵만 존재) 이 문서에 +`N/A + 이유`로 기록하고 파일은 수정하지 않는다. 추측으로 항목을 만들지 않는다. + +## Accept criteria (wp2) + +- `rg -n 'Implementation delegation' plugins/codexclaw/skills/dev/SKILL.md` 히트. +- `rg -n 'Implementation ownership' plugins/codexclaw/skills/pabcd/references/plan-output.md` 히트. +- `rg -n "recorded implementation ownership" plugins/codexclaw/skills/pabcd/SKILL.md` 히트. +- `rg -n 'Implementation delegation' structure/20_pabcd_dispatch_doctrine.md` 히트 + (고정 문자열, 한 줄 안에 있어야 함). +- `npm test`가 이 단계에서도 통과(문서 변경이 manifest/skill 테스트를 깨지 않음). + +## Out of scope (wp2) + +전역 AGENTS.md, `agents/*.toml`, delegation.md의 전송 규약, 코드 변경. diff --git a/devlog/_plan/260914_executor_first_delegation/020_wp3_directive_wiring.md b/devlog/_plan/260914_executor_first_delegation/020_wp3_directive_wiring.md new file mode 100644 index 00000000..29cc256b --- /dev/null +++ b/devlog/_plan/260914_executor_first_delegation/020_wp3_directive_wiring.md @@ -0,0 +1,171 @@ +# 020 wp3 — 단계 안내 배선 (directive wiring) + +P/B 진입 시 구현 담당 소유자를 실제로 가리키게 한다. 훅 경로와 CLI 경로 둘 다 고친다. +같은 턴 안에서 CLI로 단계를 올리면 `UserPromptSubmit` 훅이 다시 돌지 않으므로, +훅만 고치면 그 경로에는 안내가 전달되지 않는다. + +## Owner + +executor. 수정 파일, 문자열, 검증 방법이 모두 확정돼 있다. main은 반환된 diff를 검토·통합한다. + +## Change 1 — `components/pabcd-state/src/hook.ts` P directive + +Anchor: `PHASE_DIRECTIVES`(292행)의 `P` 배열(302-307행). + +BEFORE: + +```ts + P: [ + "[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.", + "Plan-only ends with the plan. Forbidden checks: NOT RUN; naming an artifact grants no write permission.", + ].join("\n"), +``` + +AFTER: + +```ts + P: [ + "[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.", + "Record implementation ownership per planned change: a specified slice defaults to the configured executor, main needs a stated reason ($codexclaw:cxc-dev Implementation delegation).", + "Plan-only ends with the plan. Forbidden checks: NOT RUN; naming an artifact grants no write permission.", + ].join("\n"), +``` + +## Change 2 — `components/pabcd-state/src/hook.ts` B directive + +Anchor: 같은 객체의 `B` 배열(314-319행). + +BEFORE: + +```ts + 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.", + "Forbidden checks: NOT RUN; no invented proof.", + ].join("\n"), +``` + +AFTER: + +```ts + 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.", + "Execute the plan's recorded implementation ownership; an unassigned slice needs a P amendment, and main verifies a returned diff rather than the report.", + "Forbidden checks: NOT RUN; no invented proof.", + ].join("\n"), +``` + +주의: `phaseDirective("B", opts)`는 base 뒤에 ACTIVE WORK-PHASE 블록을 붙인다(360-371행). +새 줄은 base 안에 들어가므로 bound/unbound 양쪽에서 모두 나타난다. + +## Change 3 — `components/pabcd-state/src/orchestrate-cli.ts` 공통 힌트 + +파일 상단 import 아래, `runOrchestrateCli`(456행) 앞에 상수와 헬퍼를 추가한다. + +```ts +/** + * 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: "implementation ownership: record an owner per planned change (cxc-dev Implementation delegation)", + B: "implementation ownership: execute the plan's recorded owners; a new handoff needs a P amendment", +}; + +function withOwnershipHint(phase: Phase, output: string): string { + const hint = OWNERSHIP_HINT[phase]; + return hint ? `${output} [${hint}]` : output; +} +``` + +`Phase` 타입이 아직 import 되지 않았다면 `./state.ts`(또는 현재 `Phase`를 export 하는 +모듈)에서 타입 import를 추가한다. 새 의존성은 넣지 않는다. + +## Change 4 — 전이 성공 출력 두 곳 + +Anchor A: 1121행 일반 전이 성공 반환. + +BEFORE: + +```ts + return { code: 0, output: `orchestrate ${args.verb}: current=${state.phase} -> ${result.state.phase} (${state.phase} → ${result.state.phase}, session ${sessionId})` }; +``` + +AFTER: + +```ts + return { code: 0, output: withOwnershipHint(result.state.phase, `orchestrate ${args.verb}: current=${state.phase} -> ${result.state.phase} (${state.phase} → ${result.state.phase}, session ${sessionId})`) }; +``` + +Anchor B: 656행 I→P 오버라이드 경로. + +BEFORE: + +```ts + return { code: 0, output: `orchestrate P: I → P (agent override, session ${sessionId})` }; +``` + +AFTER: + +```ts + return { code: 0, output: withOwnershipHint("P", `orchestrate P: I → P (agent override, session ${sessionId})`) }; +``` + +D 종료 출력(739행)과 `status`/`reset` 출력은 건드리지 않는다. + +## Change 5 — 테스트 + +1. `components/pabcd-state/test/hook.test.ts`의 "wp3: phase pointers retain owners and + active work-phase boundaries" 테스트(110-122행)에 두 줄을 추가한다. + +```ts + assert.match(phaseDirective("P"), /Record implementation ownership/); + assert.match(phaseDirective("B"), /recorded implementation ownership/); +``` + +2. `components/pabcd-state/test/orchestrate-cli.test.ts`에 새 테스트를 추가한다. + 기존 헬퍼(임시 cwd 생성, 세션 준비)를 재사용하고 새 헬퍼를 만들지 않는다. + +```ts +test("260914: P and B entry echo the implementation-ownership pointer; other verbs do not", () => { + // IDLE -> P 출력에 포인터가 있고, A 전이 출력에는 없다. + // 실제 러너 형태는 이 파일의 기존 테스트를 그대로 따른다. +}); +``` + + 최소 단언: `orchestrate P` 출력이 `/implementation ownership/`에 match, + `orchestrate A` 출력은 doesNotMatch, `orchestrate B` 출력은 match, + `orchestrate D`(C→IDLE 종료) 출력도 doesNotMatch. + I→P 오버라이드 경로(656행 반환)도 별도로 match를 단언한다. 기존 테스트의 + `readyInterview()`, `seedPlanUnit()`, `freshCwd()` 헬퍼를 재사용한다. + +3. 기존 테스트 중 전이 출력 문자열을 `assert.equal`로 고정한 곳이 있으면 함께 갱신한다. + 먼저 `rg -n "orchestrate P: current=|orchestrate B: current=|assert.equal\(res.output"`로 찾는다. + +RED/GREEN: 새 단언을 먼저 추가해 실패(RED)를 확인한 뒤 소스를 고쳐 통과(GREEN)시킨다. + +## Change 6 — 빌드 산출물 + +`node plugins/codexclaw/scripts/build.mjs`를 돌려 `dist/hook.js`와 +`dist/orchestrate-cli.js`를 갱신하고 함께 커밋한다. 설치 payload는 `src`가 아니라 +`dist`를 쓰므로 이걸 빼면 격리 검증이 옛 문자열을 본다. +`inventory.json`은 이 빌드가 재생성하지 않는다. 실제로 변하면 그 변경도 함께 커밋한다. + +## Accept criteria (wp3) + +- `npm test` 0 failures(새 단언 포함), RED 확인 기록. +- `node plugins/codexclaw/scripts/build.mjs` exit 0. +- `dist/hook.js`와 `dist/orchestrate-cli.js`에 새 문자열이 존재(`rg`로 확인). + +## Out of scope (wp3) + +새 CLI 서브커맨드, 새 훅 이벤트, gate/transition 로직 변경, D/status 출력 변경. diff --git a/devlog/_plan/260914_executor_first_delegation/030_wp4_isolated_verification.md b/devlog/_plan/260914_executor_first_delegation/030_wp4_isolated_verification.md new file mode 100644 index 00000000..2904b106 --- /dev/null +++ b/devlog/_plan/260914_executor_first_delegation/030_wp4_isolated_verification.md @@ -0,0 +1,143 @@ +# 030 wp4 — 격리 검증과 로컬 커밋 + +설치된 payload에서 새 문자열이 실제로 전달되는지 확인한다. 실제 설치본 +`/home/jun/.codex/plugins/cache/codexclaw`는 건드리지 않는다. + +## Owner + +main. 호스트 환경 조작과 증거 판정이 필요하다. + +## 경로 변수 + +```bash +CXC_WT=/home/jun/code-worktrees/codexclaw/executor-first-delegation +CXC_TEST_HOME=/home/jun/tmp/cxc-exec-first-home-01a09f4f +CXC_WS=/home/jun/tmp/cxc-exec-first-ws-01a09f4f +CXC_REAL=/home/jun/.codex/plugins/cache/codexclaw +``` + +모든 명령은 `git -C "$CXC_WT"` 또는 `cd "$CXC_WT"` 형태로 대상을 명시한다. +임시 작업 디렉터리로 이동한 상태에서 커밋하지 않는다. + +## Step 1 — 빌드와 전체 테스트 (워크트리) + +```bash +cd "$CXC_WT" && node plugins/codexclaw/scripts/build.mjs && npm test +``` + +tail 출력과 exit code를 증거로 남기고 실패 수를 직접 읽는다. +`rg -n 'implementation ownership' plugins/codexclaw/components/pabcd-state/dist/*.js`로 +빌드 산출물에 새 문자열이 들어갔는지 확인한다(설치본은 `src`가 아니라 `dist`를 쓴다). + +## Step 2 — 실제 설치본 지문 + 격리 설치 + +설치 전후로 재귀 내용 지문을 뜬다. 최상위 `ls`는 버전 디렉터리 안의 덮어쓰기를 못 잡는다. + +```bash +find "$CXC_REAL" -type f -exec sha256sum {} + | sort | sha256sum # BEFORE +mkdir -p "$CXC_TEST_HOME" +cd "$CXC_WT" && CODEX_HOME="$CXC_TEST_HOME" scripts/dev-install.sh +CODEX_HOME="$CXC_TEST_HOME" scripts/dev-install.sh --status +find "$CXC_REAL" -type f -exec sha256sum {} + | sort | sha256sum # AFTER, BEFORE와 동일해야 함 +``` + +두 지문이 다르면 즉시 중단하고 원인을 기록한다(실제 설치본 변경은 범위 밖). +설치본 루트를 확정한다. + +```bash +INSTALLED_ROOT="$(ls -d "$CXC_TEST_HOME"/plugins/cache/codexclaw/codexclaw/*/ | tail -1)" +rg -n 'implementation ownership' "${INSTALLED_ROOT}components/pabcd-state/dist/"*.js | head +``` + +이후 모든 실행은 `CODEX_HOME="$CXC_TEST_HOME"`를 붙이고 `$INSTALLED_ROOT`의 파일을 +절대경로로 직접 호출한다. PATH의 `cxc`나 전역 설치본을 부르지 않는다. + +## Step 3 — 픽스처 준비 + +```bash +mkdir -p "$CXC_WS" && cd "$CXC_WS" +git init -q && git commit -q --allow-empty -m "fixture" +mkdir -p devlog/_plan/000000_isolated-check +printf '# 000 isolated check\n' > devlog/_plan/000000_isolated-check/000_plan.md +``` + +B 진입은 소스 정체성을 스냅샷하므로 픽스처는 커밋이 있는 git 저장소여야 한다. +세션 키는 예약된 터미널 키 `cli`를 쓴다. 이 세션의 실제 id를 쓰지 않는다. + +## Step 4 — 훅 주입 경로 관측 (UserPromptSubmit) + +`cxc hook` 서브커맨드는 존재하지 않는다. 훅 매니페스트가 실제로 부르는 진입점은 +`components/pabcd-state/dist/cli.js hook user-prompt-submit`이고, 페이로드는 stdin JSON이다. +채팅 표면은 사람 free-pass이므로 인접 단계 이동에 attest가 필요 없다. + +페이로드 형태는 `{hook_event_name, session_id, cwd, prompt, transcript_path, turn_id}`다. +`turn_id`는 호출마다 달라야 한다. 훅은 이미 주입한 turn을 `injectedTurns`로 기억하고 +같은 값이 다시 오면 명령 파싱 전에 빈 문자열을 반환한다(hook.ts:651). 같은 `t1`을 세 번 쓰면 +A와 B는 아예 실행되지 않고, 빈 A 출력이 음성 검증을 거짓 통과시킨다. + +```bash +hookrun() { # $1 = prompt, $2 = turn id (호출마다 달라야 함) + printf '%s' "{\"hook_event_name\":\"UserPromptSubmit\",\"session_id\":\"cli\",\"cwd\":\"$CXC_WS\",\"prompt\":\"$1\",\"transcript_path\":null,\"turn_id\":\"$2\"}" \ + | CODEX_HOME="$CXC_TEST_HOME" node "${INSTALLED_ROOT}components/pabcd-state/dist/cli.js" hook user-prompt-submit +} +hookrun "orchestrate P" t1 # 기대: [codexclaw: PLAN] + Record implementation ownership +hookrun "orchestrate A" t2 # 기대: [codexclaw: AUDIT] + 해당 문장 없음 +hookrun "orchestrate B" t3 # 기대: [codexclaw: BUILD] + recorded implementation ownership +``` + +세 출력을 모두 직접 읽고 증거에 붙인다. 생성만 하고 읽지 않은 출력은 관측이 아니다. +각 출력마다 단계 헤더(`[codexclaw: PLAN]` / `AUDIT` / `BUILD`)가 실제로 있는지 먼저 확인한다. +헤더가 없거나 출력이 비어 있으면 그 단계는 실행되지 않은 것이므로, 포인터 미포함을 +음성 증거로 쓸 수 없다. 기존 테스트도 같은 흐름에 `t1/t2/t3`을 쓴다(hook.test.ts:727-731). + +## Step 5 — CLI 경로 관측 (같은 턴 진행) + +훅과 별개 경로다. Step 4가 픽스처 상태를 이미 움직였으므로 새 픽스처에서 시작한다. + +```bash +CXC_WS2="${CXC_WS}-cli" +mkdir -p "$CXC_WS2" && cd "$CXC_WS2" && git init -q && git commit -q --allow-empty -m fixture +mkdir -p devlog/_plan/000000_isolated-check +printf '# 000 isolated check\n' > devlog/_plan/000000_isolated-check/000_plan.md +CXCRUN() { CODEX_HOME="$CXC_TEST_HOME" node "${INSTALLED_ROOT}bin/cxc.mjs" "$@"; } +CXCRUN orchestrate P --session cli --cwd "$CXC_WS2" +printf '%s' '{"from":"P","to":"A","did":"isolated fixture plan","planUnit":"devlog/_plan/000000_isolated-check"}' > "$CXC_WS2/pa.json" +CXCRUN orchestrate A --session cli --cwd "$CXC_WS2" --attest-file "$CXC_WS2/pa.json" +printf '%s' '{"from":"A","to":"B","did":"isolated fixture audit","auditOutput":"VERDICT: PASS","auditVerdict":"pass"}' > "$CXC_WS2/ab.json" +CXCRUN orchestrate B --session cli --cwd "$CXC_WS2" --attest-file "$CXC_WS2/ab.json" +``` + +기대: P와 B 출력에 `[implementation ownership: ...]`가 있고 A 출력에는 없다. 모두 exit 0. +attest가 거부되면 거부 사유를 그대로 증거에 남기고, 통과했다고 쓰지 않는다. + +## Step 6 — 커밋 (워크트리) + +```bash +git -C "$CXC_WT" status --short --branch +git -C "$CXC_WT" add -A +git -C "$CXC_WT" commit -m "<범위에 맞는 메시지>" +git -C "$CXC_WT" log --oneline -3 +``` + +임시 픽스처는 워크트리 밖(`/home/jun/tmp`)에 있으므로 커밋 대상이 아니다. +`git -C`로 대상을 고정해 임시 저장소에 커밋되는 사고를 막는다. +push 하지 않는다(DEV-GIT-PUSH-01). PR은 사용자 승인 후 별도 단계. + +## Step 7 — 정리 + +증거를 남긴 뒤 `$CXC_TEST_HOME`과 `$CXC_WS`, `$CXC_WS2`만 정리한다. 셋 다 이 작업이 만든 것이다. +워크트리와 브랜치는 다음 PR 단계의 입력이므로 남긴다. + +## Accept criteria (wp4) + +- build exit 0, `npm test` 0 failures의 실제 tail 출력. +- 실제 설치본 재귀 지문이 설치 전후 동일. +- 훅 경로에서 P/B 포함, A 미포함이 같은 증거에 기록되고, 세 출력 모두 자기 단계 헤더를 갖는다 + (빈 출력이나 헤더 없는 출력은 음성 증거로 인정하지 않는다). +- CLI 경로에서 P/B 포함, A 미포함이 exit 0과 함께 기록됨. +- `git -C "$CXC_WT" log`에 커밋 존재, 원격 push 없음. + +## 이 유닛이 증명하지 않는 것 + +실제 비용 절감. Astra/Opus 동일 조건 비교와 메인+executor 합산 비용 측정이 필요하며 +이 유닛의 범위 밖이다. 여기서 증명하는 것은 지침과 안내가 실제로 전달된다는 사실까지다. From 24f8d11227f37edfe13474dc5cf71b92638e7f79 Mon Sep 17 00:00:00 2001 From: thisisjun786 <259586770+thisisjun786@users.noreply.github.com> Date: Mon, 14 Sep 2026 20:03:42 +0900 Subject: [PATCH 2/7] [agent] docs(skills): make executor the default owner of specified implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CXC는 탐색 위임 기준만 갖고 있어서, 명세가 이미 확정된 구현도 메인이 직접 처리하기 쉬웠다. dev/SKILL.md에 Implementation delegation 절을 신설해 확정된 슬라이스는 설정된 executor에 우선 배정하고, 직접 구현은 이유를 밝히도록 했다. plan-output.md는 파일 변경마다 담당을 기록하게 하고, pabcd B는 그 기록을 실행하도록 연결했다. dispatch doctrine과 INDEX를 같은 소유자로 동기화했다. E7 지침이며 런타임 게이트는 추가하지 않는다. gate.mjs exit 0. --- .../010_wp2_policy_text.md | 11 ++++++++ plugins/codexclaw/skills/dev/SKILL.md | 25 +++++++++++++++++++ plugins/codexclaw/skills/pabcd/SKILL.md | 2 +- .../skills/pabcd/references/plan-output.md | 15 +++++++++++ structure/20_pabcd_dispatch_doctrine.md | 6 +++-- structure/INDEX.md | 2 +- 6 files changed, 57 insertions(+), 4 deletions(-) diff --git a/devlog/_plan/260914_executor_first_delegation/010_wp2_policy_text.md b/devlog/_plan/260914_executor_first_delegation/010_wp2_policy_text.md index 3f965db2..bd9e6d6d 100644 --- a/devlog/_plan/260914_executor_first_delegation/010_wp2_policy_text.md +++ b/devlog/_plan/260914_executor_first_delegation/010_wp2_policy_text.md @@ -133,3 +133,14 @@ INDEX가 규칙 ID나 스킬 절 목록을 실제로 들고 있으면 같은 변 ## Out of scope (wp2) 전역 AGENTS.md, `agents/*.toml`, delegation.md의 전송 규약, 코드 변경. + +## P 재검증 (wp2 사이클, 2026-09-14) + +이전 D 결론: wp1은 로드맵을 문서로 확정했고 코드는 건드리지 않았다. 방향 변경 없음. +이 문서의 BEFORE 앵커 4곳은 커밋 e3b22b0b 시점에 `bash /home/jun/tmp/cxc-wp1-doccheck-01a09f4f.sh` +(exit 0)로 현재 소스에 실재함을 확인했다. 재작성 없이 그대로 실행한다. + +Change 5(INDEX.md) 판정: INDEX.md는 파일·컴포넌트·훅·CLI 맵과 역할 표를 들고 있고, +326행 부근이 "Formal P and changed-decision rechecks follow skills/pabcd/references/..."로 +에이전트 준수 지침의 소재를 요약한다. 구현 담당 기본값도 같은 성격이므로 그 문장에 +한 구절만 덧붙인다. 규칙 ID 목록은 INDEX에 없으므로 새 표를 만들지 않는다. diff --git a/plugins/codexclaw/skills/dev/SKILL.md b/plugins/codexclaw/skills/dev/SKILL.md index ba8b7341..dfbbac6c 100644 --- a/plugins/codexclaw/skills/dev/SKILL.md +++ b/plugins/codexclaw/skills/dev/SKILL.md @@ -203,6 +203,31 @@ 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 + +Once a slice is specified — goal, editable files, the contract it must preserve +and the check that proves it — assign it to the configured `executor` by +default. Sequential work qualifies: a dependency order decides the order of the +packets, not who types the diff. Main keeps the specification, the load-bearing +judgment, the review of the returned diff and the integration. + +Implementing a specified slice locally needs a stated reason, and only three +hold by default: the edit is small enough that packaging it costs more than +making it (§0.1), a load-bearing judgment inside the slice is still open so the +packet cannot state its decision boundary, or handoff plus re-work measurably +exceeds the delegated work. "It is sequential", "I already understand it" and +"typing it is faster" are not reasons; neither is read-only-style parallelism, +which is a separate question from who implements. + +Delegate one verifiable bundle — the slice a single check can prove — rather +than one packet per file. P records the owner of each planned change and B +executes that assignment (`../pabcd/references/plan-output.md`); handing a new +slice to a subagent mid-B still requires a P amendment. Model, effort, fallback +and receipts stay with the configured dispatch path. Verify a returned +implementation against the VCS diff, never the report alone (§3). 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 49675ddb..f85cd39d 100644 --- a/plugins/codexclaw/skills/pabcd/SKILL.md +++ b/plugins/codexclaw/skills/pabcd/SKILL.md @@ -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`. Execute the plan's recorded implementation ownership — a specified slice goes to the configured executor and main verifies the returned diff (`cxc-dev` Implementation delegation); a slice the plan did not assign needs a P amendment, not a mid-B improvisation. 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 d856c0ab..c0ecc268 100644 --- a/plugins/codexclaw/skills/pabcd/references/plan-output.md +++ b/plugins/codexclaw/skills/pabcd/references/plan-output.md @@ -26,6 +26,21 @@ 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. +## Implementation ownership + +The file change map names an owner for each planned change: the configured +`executor` for slices whose scope and check are settled, or main with a +one-line reason from `cxc-dev`'s Implementation delegation exceptions. A plan +that assigns nothing has not decided; it has defaulted to main silently. B +executes the recorded assignment, and a new handoff mid-B remains a P amendment +(see Escalation condition above). Discovery and review keep their own owners. + +```text +transform helper + its unit test — executor: scope and check are settled +shared policy wording and integration — main: the judgment is the deliverable +one-line typo in a file already open — main: packaging costs more than the edit +``` + ## 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 ef76fcbb..5d483de9 100644 --- a/structure/20_pabcd_dispatch_doctrine.md +++ b/structure/20_pabcd_dispatch_doctrine.md @@ -199,8 +199,10 @@ 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 same decision: a specified slice defaults to the configured executor, and + the P plan records the owner that B then executes. 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 0b175db8..53f9ceb1 100644 --- a/structure/INDEX.md +++ b/structure/INDEX.md @@ -323,7 +323,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 and changed-decision rechecks follow `skills/pabcd/references/phase-plan.md`, `phase-audit.md` and `delegation.md`; implementation ownership (a specified slice defaults to the configured executor, recorded in P and executed 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 ef325fab463889080fd9fc5b9388f4dbc0660f25 Mon Sep 17 00:00:00 2001 From: thisisjun786 <259586770+thisisjun786@users.noreply.github.com> Date: Mon, 14 Sep 2026 21:02:24 +0900 Subject: [PATCH 3/7] [agent] feat(pabcd): point P and B at the implementation-ownership owner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 단계 안내가 wp2에서 만든 소유자를 실제로 가리키게 한다. hook.ts의 P/B directive에 한 줄씩 추가하고, orchestrate CLI의 P/B 진입 성공 출력에도 같은 포인터를 붙였다. 훅은 UserPromptSubmit에서만 도착하므로, 한 턴 안에서 CLI로 단계를 올리는 경로에는 안내가 전달되지 않았다. 포인터는 전이 출력에만 붙는다. A/C/D 종료와 status에는 붙지 않으며, 테스트가 exit 0과 실제 phase를 먼저 확인한 뒤 그 차이를 단언한다. dist 재빌드 포함. --- .../020_wp3_directive_wiring.md | 36 +++++++-- .../components/pabcd-state/dist/hook.js | 2 + .../pabcd-state/dist/orchestrate-cli.js | 20 ++++- .../components/pabcd-state/src/hook.ts | 2 + .../pabcd-state/src/orchestrate-cli.ts | 20 ++++- .../components/pabcd-state/test/hook.test.ts | 2 + .../pabcd-state/test/orchestrate-cli.test.ts | 74 +++++++++++++++++++ 7 files changed, 144 insertions(+), 12 deletions(-) diff --git a/devlog/_plan/260914_executor_first_delegation/020_wp3_directive_wiring.md b/devlog/_plan/260914_executor_first_delegation/020_wp3_directive_wiring.md index 29cc256b..77a33eb2 100644 --- a/devlog/_plan/260914_executor_first_delegation/020_wp3_directive_wiring.md +++ b/devlog/_plan/260914_executor_first_delegation/020_wp3_directive_wiring.md @@ -142,14 +142,19 @@ test("260914: P and B entry echo the implementation-ownership pointer; other ver }); ``` - 최소 단언: `orchestrate P` 출력이 `/implementation ownership/`에 match, - `orchestrate A` 출력은 doesNotMatch, `orchestrate B` 출력은 match, - `orchestrate D`(C→IDLE 종료) 출력도 doesNotMatch. - I→P 오버라이드 경로(656행 반환)도 별도로 match를 단언한다. 기존 테스트의 - `readyInterview()`, `seedPlanUnit()`, `freshCwd()` 헬퍼를 재사용한다. - -3. 기존 테스트 중 전이 출력 문자열을 `assert.equal`로 고정한 곳이 있으면 함께 갱신한다. - 먼저 `rg -n "orchestrate P: current=|orchestrate B: current=|assert.equal\(res.output"`로 찾는다. + 각 단언 전에 먼저 `assert.equal(r.code, 0)`와 전이 후 실제 phase를 확인한다. + 그러지 않으면 거부 메시지가 음성 단언을 거짓 통과시킨다. + 양성: `orchestrate P`와 `orchestrate B` 출력이 `/implementation ownership/`에 match. + 음성: `orchestrate A`, `orchestrate C`, `orchestrate D`(C→IDLE 종료) 출력은 doesNotMatch. + `orchestrate status`도 저장된 phase가 P일 때와 B일 때 각각 doesNotMatch를 단언한다 + (안내는 전이 출력에만 붙고 상태 조회에는 붙지 않는다). + I→P 오버라이드 경로(656행 반환)도 별도로 match를 단언하되, 새로 만들지 말고 + orchestrate-cli.test.ts:615의 기존 unready 픽스처를 재사용한다. + 그 밖에는 `seedPlanUnit()`, `freshCwd()` 등 기존 헬퍼를 재사용한다. + +3. 전이 출력 문자열을 `assert.equal`로 고정한 기존 테스트는 두 곳뿐이고 둘 다 D 종료 + 출력이다(orchestrate-cli.test.ts:982, :2401). D에는 포인터를 붙이지 않으므로 그대로 둔다. + platform-smoke(거부/상태)와 cli-usage(도움말)도 영향 없음. 새 스냅샷 소비자는 없다. RED/GREEN: 새 단언을 먼저 추가해 실패(RED)를 확인한 뒤 소스를 고쳐 통과(GREEN)시킨다. @@ -169,3 +174,18 @@ RED/GREEN: 새 단언을 먼저 추가해 실패(RED)를 확인한 뒤 소스를 ## Out of scope (wp3) 새 CLI 서브커맨드, 새 훅 이벤트, gate/transition 로직 변경, D/status 출력 변경. + +## P 재검증 (wp3 사이클, 2026-09-14) + +이전 D 결론: wp2가 dev/SKILL.md:206, plan-output.md:29, pabcd/SKILL.md:83, doctrine:202, +INDEX.md:326에 구현 담당 소유자를 만들었다(커밋 1a063c76). 이제 단계 안내가 그 소유자를 +가리키게 한다. 방향 변경 없음. + +이 문서의 BEFORE 앵커(hook.ts P/B 배열, orchestrate-cli.ts 1121/656 반환)는 wp2가 건드리지 +않은 파일이며 `bash /home/jun/tmp/cxc-wp1-doccheck-01a09f4f.sh` exit 0으로 다시 확인했다. + +담당: executor. 수정 파일과 문자열과 검증 방법이 확정돼 있어 위임 조건을 충족한다. +main은 패킷 작성, 반환 diff 검토, 통합을 맡는다(dev/SKILL.md Implementation delegation). +`Phase` 타입은 orchestrate-cli.ts:139에 이미 import 돼 있다. 새 import를 추가하지 말고 +그것을 재사용한다(순환 import 없음). dist 재빌드는 전체 GREEN 실행 전에 끝낸다 — +dist 신선도 검사가 소스 변경을 관측하기 때문이다. diff --git a/plugins/codexclaw/components/pabcd-state/dist/hook.js b/plugins/codexclaw/components/pabcd-state/dist/hook.js index 3b1a36bd..2a883c8b 100644 --- a/plugins/codexclaw/components/pabcd-state/dist/hook.js +++ b/plugins/codexclaw/components/pabcd-state/dist/hook.js @@ -303,6 +303,7 @@ 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.", + "Record implementation ownership per planned change: a specified slice defaults to the configured executor, main needs a stated reason ($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: [ @@ -315,6 +316,7 @@ 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.", + "Execute the plan's recorded implementation ownership; an unassigned slice needs a P amendment, and main verifies 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..2d8658b8 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: "implementation ownership: record an owner per planned change (cxc-dev Implementation delegation)", + B: "implementation ownership: execute the plan's recorded owners; a new handoff 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..f96f740f 100644 --- a/plugins/codexclaw/components/pabcd-state/src/hook.ts +++ b/plugins/codexclaw/components/pabcd-state/src/hook.ts @@ -303,6 +303,7 @@ 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.", + "Record implementation ownership per planned change: a specified slice defaults to the configured executor, main needs a stated reason ($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: [ @@ -315,6 +316,7 @@ 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.", + "Execute the plan's recorded implementation ownership; an unassigned slice needs a P amendment, and main verifies 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..3e5316de 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: "implementation ownership: record an owner per planned change (cxc-dev Implementation delegation)", + B: "implementation ownership: execute the plan's recorded owners; a new handoff 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..f36c5503 100644 --- a/plugins/codexclaw/components/pabcd-state/test/hook.test.ts +++ b/plugins/codexclaw/components/pabcd-state/test/hook.test.ts @@ -116,6 +116,8 @@ 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"), /Record implementation ownership/); + assert.match(phaseDirective("B"), /recorded implementation ownership/); 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/); 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..e1d01be5 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,77 @@ 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/); + + // 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/); + + // 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/); + + // 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/); + + // 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/); + + // 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/); + + // 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/); + } 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/); + } finally { rmSync(cwd, { recursive: true, force: true }); } +}); From c1b184688a5306340029ceada97d0c112b450923 Mon Sep 17 00:00:00 2001 From: thisisjun786 <259586770+thisisjun786@users.noreply.github.com> Date: Mon, 14 Sep 2026 21:10:06 +0900 Subject: [PATCH 4/7] [agent] docs(devlog): isolated-install evidence for the ownership pointer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 격리된 CODEX_HOME에 이 브랜치를 설치해 훅 경로와 CLI 경로 양쪽에서 실제 주입 문자열을 관측했다. P와 B에는 포인터가 붙고 A와 status에는 붙지 않으며, 네 호출 모두 exit 0이라 거부 메시지가 음성 결과를 만든 경우가 아니다. 실제 설치본은 재귀 지문(3601개 파일) 기준 무변경. --- .../030_wp4_isolated_verification.md | 33 ++++++++++-- .../031_wp4_receipt.md | 54 +++++++++++++++++++ 2 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 devlog/_plan/260914_executor_first_delegation/031_wp4_receipt.md diff --git a/devlog/_plan/260914_executor_first_delegation/030_wp4_isolated_verification.md b/devlog/_plan/260914_executor_first_delegation/030_wp4_isolated_verification.md index 2904b106..74921de6 100644 --- a/devlog/_plan/260914_executor_first_delegation/030_wp4_isolated_verification.md +++ b/devlog/_plan/260914_executor_first_delegation/030_wp4_isolated_verification.md @@ -33,15 +33,30 @@ tail 출력과 exit code를 증거로 남기고 실패 수를 직접 읽는다. 설치 전후로 재귀 내용 지문을 뜬다. 최상위 `ls`는 버전 디렉터리 안의 덮어쓰기를 못 잡는다. +지문 함수는 실패를 전파해야 한다. 파이프 끝의 `sha256sum`은 `find`가 실패해도 0을 +반환하므로, 실패한 스캔 두 번이 "빈 입력 해시"로 똑같이 나와 무변경으로 보일 수 있다. + ```bash -find "$CXC_REAL" -type f -exec sha256sum {} + | sort | sha256sum # BEFORE +fingerprint() { # 실패 전파 + 비어 있지 않은 목록 요구 + ( + set -o pipefail + n=$(find "$CXC_REAL" -type f | wc -l) || exit 1 + [ "$n" -gt 0 ] || { echo "FAIL: empty inventory"; exit 1; } + d=$(find "$CXC_REAL" -type f -exec sha256sum {} + | sort | sha256sum) || exit 1 + printf 'files=%s digest=%s\n' "$n" "$d" + ) +} +fingerprint > /var/tmp/cxc-real-before-01a09f4f.txt || { echo "BEFORE scan failed"; exit 1; } mkdir -p "$CXC_TEST_HOME" cd "$CXC_WT" && CODEX_HOME="$CXC_TEST_HOME" scripts/dev-install.sh CODEX_HOME="$CXC_TEST_HOME" scripts/dev-install.sh --status -find "$CXC_REAL" -type f -exec sha256sum {} + | sort | sha256sum # AFTER, BEFORE와 동일해야 함 +fingerprint > /var/tmp/cxc-real-after-01a09f4f.txt || { echo "AFTER scan failed"; exit 1; } +diff /var/tmp/cxc-real-before-01a09f4f.txt /var/tmp/cxc-real-after-01a09f4f.txt && echo "REAL INSTALL UNCHANGED" ``` -두 지문이 다르면 즉시 중단하고 원인을 기록한다(실제 설치본 변경은 범위 밖). +두 스캔이 모두 성공하고 파일 수가 0이 아니며 서로 같아야 한다. 하나라도 실패하면 비교 자체가 +무효다. 이 지문은 정규 파일 내용의 동일성만 증명하며 권한·타임스탬프나 ~/.codex의 다른 경로는 +다루지 않는다. 두 지문이 다르면 즉시 중단하고 원인을 기록한다(실제 설치본 변경은 범위 밖). 설치본 루트를 확정한다. ```bash @@ -131,12 +146,22 @@ push 하지 않는다(DEV-GIT-PUSH-01). PR은 사용자 승인 후 별도 단계 ## Accept criteria (wp4) - build exit 0, `npm test` 0 failures의 실제 tail 출력. -- 실제 설치본 재귀 지문이 설치 전후 동일. +- 실제 설치본 재귀 지문이 설치 전후 동일하고, 두 스캔 모두 성공했으며 파일 수가 0이 아니다. - 훅 경로에서 P/B 포함, A 미포함이 같은 증거에 기록되고, 세 출력 모두 자기 단계 헤더를 갖는다 (빈 출력이나 헤더 없는 출력은 음성 증거로 인정하지 않는다). - CLI 경로에서 P/B 포함, A 미포함이 exit 0과 함께 기록됨. - `git -C "$CXC_WT" log`에 커밋 존재, 원격 push 없음. +## P 재검증 (wp4 사이클, 2026-09-14) + +이전 D 결론: wp3이 hook.ts P/B directive와 orchestrate CLI 두 성공 반환에 포인터를 배선했고 +dist까지 재빌드했다(커밋 a99d3b86). RED 3건 재현 후 GREEN 183/183, 전체 3079 pass / 0 fail, +gate.mjs exit 0. 이제 설치된 payload에서 같은 문자열이 실제로 전달되는지 본다. 방향 변경 없음. + +Step 1의 `npm test`는 wp3 C에서 이미 실행했고 리시트로 남았다. wp4에서는 재실행 대신 +`TMPDIR=/var/tmp/cxc-t-01a09f4f`를 계속 쓰고(이 호스트의 `/tmp/.git` 때문에 GUI +project-root 테스트가 깨진다), 격리 설치 직전에 빌드 산출물이 최신인지만 확인한다. + ## 이 유닛이 증명하지 않는 것 실제 비용 절감. Astra/Opus 동일 조건 비교와 메인+executor 합산 비용 측정이 필요하며 diff --git a/devlog/_plan/260914_executor_first_delegation/031_wp4_receipt.md b/devlog/_plan/260914_executor_first_delegation/031_wp4_receipt.md new file mode 100644 index 00000000..dd4028ba --- /dev/null +++ b/devlog/_plan/260914_executor_first_delegation/031_wp4_receipt.md @@ -0,0 +1,54 @@ +# 031 wp4 receipt — 격리 환경 관측 증거 + +실행일 2026-09-14. 워크트리 /home/jun/code-worktrees/codexclaw/executor-first-delegation, 브랜치 codex/executor-first-delegation. +격리 홈 `/home/jun/tmp/cxc-exec-first-home-01a09f4f`, 설치본 +`0.2.28+codex.20260914090142`. + +## 실제 설치본 무변경 + +```text +files=3601 digest=ffd090c279d72db000f008e42210e740b183d9ebc568a6aaae2e68f714e6c4a8 - (BEFORE) +files=3601 digest=ffd090c279d72db000f008e42210e740b183d9ebc568a6aaae2e68f714e6c4a8 - (AFTER) +REAL INSTALL UNCHANGED +``` + +두 스캔 모두 성공했고 파일 수가 0이 아니며 서로 같다. + +## 훅 경로 (UserPromptSubmit, 설치본 dist/cli.js) + +세 호출 모두 자기 단계 헤더를 갖고 실행됐다(turn_id t1/t2/t3). + +| 프롬프트 | 헤더 | 구현 담당 포인터 | +|---|---|---| +| orchestrate P | `[codexclaw: PLAN]` | 있음 — "Record implementation ownership per planned change: a specified slice defaults to the configured executor, main needs a stated reason ($codexclaw:cxc-dev Implementation delegation)." | +| orchestrate A | `[codexclaw: AUDIT]` | 없음 | +| orchestrate B | `[codexclaw: BUILD]` | 있음 — "Execute the plan's recorded implementation ownership; an unassigned slice needs a P amendment, and main verifies a returned diff rather than the report." | + +## CLI 경로 (설치본 bin/cxc.mjs, 별도 픽스처) + +```text +orchestrate P: current=IDLE -> P (IDLE → P, session cli) [implementation ownership: record an owner per planned change (cxc-dev Implementation delegation)] exit=0 +orchestrate A: current=P -> A (P → A, session cli) exit=0 +orchestrate B: current=A -> B (A → B, session cli) [implementation ownership: execute the plan's recorded owners; a new handoff needs a P amendment] exit=0 +session=cli phase=B interview=false auditPassed=true checkPassed=false exit=0 +``` + +P와 B에만 붙고 A와 status에는 붙지 않는다. 네 호출 모두 exit 0이므로 +거부 메시지가 음성 결과를 만든 경우가 아니다. + +## 워크트리 쪽 증거 (wp3에서 확보) + +- RED: 기준 커밋 1a063c76 임시 워크트리에 새 테스트만 얹어 3건 실패 재현. +- GREEN: 현재 HEAD에서 183/183, 전체 `npm test` 3152개 중 3079 pass / 0 fail / 73 skipped. +- `node plugins/codexclaw/scripts/gate.mjs` exit 0. + +## 재현 스크립트 + +`/var/tmp/cxc-wp4-verify-01a09f4f.sh` — 격리 설치본에 대해 위 관측을 다시 실행하고 +단언한다. 픽스처는 매 실행마다 새로 만든다. + +## 증명하지 않은 것 + +실제 비용 절감. 그것은 동일 조건에서 메인+executor 합산 비용을 측정해야 하며 +이 유닛의 범위 밖이다. 호스트 제약 하나를 기록해 둔다: 이 머신에는 `/tmp/.git`이 있어 +TMPDIR을 분리하지 않으면 무관한 GUI project-root 테스트가 실패한다. From 1ed174b153a948f5f41585f8fb5b0287221869c1 Mon Sep 17 00:00:00 2001 From: thisisjun786 <259586770+thisisjun786@users.noreply.github.com> Date: Tue, 15 Sep 2026 00:16:21 +0900 Subject: [PATCH 5/7] [agent] feat(pabcd): record and remind architect consultation --- .../000_plan.md | 15 +-- .../040_architect_consultation.md | 96 +++++++++++++++++++ .../041_architect_receipt.md | 93 ++++++++++++++++++ .../components/pabcd-state/dist/hook.js | 2 + .../pabcd-state/dist/orchestrate-cli.js | 2 +- .../components/pabcd-state/src/hook.ts | 2 + .../pabcd-state/src/orchestrate-cli.ts | 2 +- .../components/pabcd-state/test/hook.test.ts | 49 ++++++++++ .../pabcd-state/test/orchestrate-cli.test.ts | 8 ++ plugins/codexclaw/skills/pabcd/SKILL.md | 2 +- .../skills/pabcd/references/phase-plan.md | 13 ++- .../skills/pabcd/references/plan-output.md | 21 ++++ structure/20_pabcd_dispatch_doctrine.md | 8 +- structure/INDEX.md | 2 +- 14 files changed, 298 insertions(+), 17 deletions(-) create mode 100644 devlog/_plan/260914_executor_first_delegation/040_architect_consultation.md create mode 100644 devlog/_plan/260914_executor_first_delegation/041_architect_receipt.md diff --git a/devlog/_plan/260914_executor_first_delegation/000_plan.md b/devlog/_plan/260914_executor_first_delegation/000_plan.md index 3b14a086..2166e18c 100644 --- a/devlog/_plan/260914_executor_first_delegation/000_plan.md +++ b/devlog/_plan/260914_executor_first_delegation/000_plan.md @@ -1,15 +1,16 @@ -# 260914 executor-first implementation delegation +# 260914 planning consultation and implementation delegation ## Reader summary -CXC는 지금 "무엇을 탐색에 맡길지"는 안내하지만 "구현을 누가 맡을지"는 안내하지 않는다. -그래서 메인 모델이 명세가 이미 확정된 구현까지 직접 처리하기 쉽고, 비싼 메인 모델이 -싼 executor가 할 수 있는 작업을 대신하게 된다. 이 유닛은 "명세·수정 범위·검증 방법이 -확정된 구현은 설정된 executor에 우선 배정한다"는 기본값을 CXC 지침에 추가하고, -P 계획이 담당을 기록하고 B가 그 기록을 실행하도록 연결한다. 바뀌는 대상은 CXC를 -쓰는 모든 세션의 메인 에이전트이며, 런타임 하드 게이트는 새로 만들지 않는다. +이 변경은 명세·수정 범위·검증 방법이 확정된 구현을 executor에 우선 배정하고, +P 계획에 담당을 기록해 B에서 그 분담을 실행하도록 안내한다. 후속 변경은 기존 +architect 절차도 P 진입 시 알린다. Architect가 설계를 제안하고, 메인이 실행 계획을 +쓴 뒤, 같은 architect가 반영 상태를 확인하는 순서와 실제 결과를 계획에 남긴다. +메인이 최종 판단과 통합을 맡으며, 두 규칙 모두 모델이 따르는 지침이다. 근거와 소스 앵커는 [001_source_evidence.md](001_source_evidence.md)에 있다. +기존 executor 작업의 wp1–wp4와 검증 기록은 아래에 보존한다. 승인된 architect +후속 변경의 범위·분담·검증은 [040](040_architect_consultation.md)에 기록한다. ## Loop spec diff --git a/devlog/_plan/260914_executor_first_delegation/040_architect_consultation.md b/devlog/_plan/260914_executor_first_delegation/040_architect_consultation.md new file mode 100644 index 00000000..2399bd5d --- /dev/null +++ b/devlog/_plan/260914_executor_first_delegation/040_architect_consultation.md @@ -0,0 +1,96 @@ +# 040 — Architect consultation follow-up + +Depends on: 14c20791 (executor ownership policy and P/B delivery). + +## Outcome and scope + +Expose the existing architect proposal → main executable plan → same-architect +reflection sequence at P entry, and record its actual result beside implementation +ownership. This is one C2 follow-up to the completed executor change. Main owns +decisions and integration; the architect remains read-only and the A reviewer +remains independent. No model routing, role registration, gate, attestation schema, +global installation, publication or minimum-spawn-count change is included. + +## Consultation and decisions + +The design proposal and same-handle reflection were received from architect +`01a0a052-2b6a-77b3-a5ef-f5d15ee9617b` in task +`01a0a049-f583-7633-981a-efbd0d680b96`. The reviewed executable-plan revision is +the five-part main packet below, based on `14c20791`; this file records that packet's +scope and decisions rather than claiming the architect reviewed this later file. + +| Evidence | Retrievable reference | +|---|---| +| Design proposal | Architect message `msg_685bf51a98b54deca1f4d162572be52e`, 2026-09-14 14:31:03 UTC, rollout line 76 | +| Submitted executable-plan revision | Main message `msg_01a0a054-e122-7a82-adcf-133dbe530993`, 14:31:52 UTC, rollout line 82; SHA-256 `ce3a1198b3fc7daf2bc6df772cd981b7dd796e8154df9eaae8baae344f7972ca` of its UTF-8 text | +| Same-handle reflection | Architect message `msg_1dd8ebb104004267b7d8b77f6d26401e`, 14:32:16 UTC, rollout line 87; ALIGNED with ARCH-P-01 through ARCH-P-04 mapped to main items 1–4 and verification to item 5 | + +The source is the local architect rollout +`rollout-2026-09-14T23-28-54-01a0a052-2b6a-77b3-a5ef-f5d15ee9617b.jsonl` +under the native Codex home's `sessions/2026/09/14/`. Raw conversation evidence +stays outside this repository. The dispositions below and file map implement the +submitted packet; the independent reviewer checks their correspondence to the diff. + +- ARCH-P-01: accept with amendment. Formal P includes C2 compact and plan-only P + plans; applicability is not conditional on whether a header was written. + C0/C1 fast-path work needs neither consultation nor an omission record. +- ARCH-P-02: accept with amendment. Record the actual handle, proposal reference, + decision dispositions, executable-plan revision submitted, reflection reference + and unresolved gaps. User-forbidden delegation and failed consultation are + distinct; a failed call is not a successful exemption. +- ARCH-P-03: accept. Keep phase-plan as the policy owner, add a short P skill/hook + pointer, and remind A only about changed design decisions. Main still decides. +- ARCH-P-04: accept. Extend the existing CLI P hint; retain executor P/B hints and + keep status and other CLI phases free of the new consultation hint. + +## File change map and owners + +| File | Change | Owner | +|---|---|---| +| `skills/pabcd/references/phase-plan.md` | Clarify formal-P scope and link consultation record | main: policy decision | +| `skills/pabcd/references/plan-output.md` | Add compact consultation evidence record before implementation ownership | main: policy decision | +| `skills/pabcd/SKILL.md` | Name proposal and reflection in the P owner pointer | main: policy decision | +| `structure/20_pabcd_dispatch_doctrine.md`, `structure/INDEX.md` | Synchronize owner pointers and evidence boundary | main: integration | +| `components/pabcd-state/src/hook.ts` | Add proposal/reflection at P and design-change recheck at A | executor | +| `components/pabcd-state/src/orchestrate-cli.ts` | Extend existing P hint, preserving B and status behavior | executor | +| `components/pabcd-state/test/{hook,orchestrate-cli}.test.ts` | Exercise actual hook/CLI outputs and negative paths | executor | +| `components/pabcd-state/dist/{hook,orchestrate-cli}.js` | Rebuild shipped payload | main: integration | + +Skill and component paths above are relative to `plugins/codexclaw/`. + +## Before / after contracts + +- P skill/hook: general plan-owner pointer → explicit architect proposal before + main's executable plan, then same-architect reflection before A, within user limits. +- Plan output: no consultation record → references and dispositions tied to the + concrete plan, with honest gaps; no transcript copies or new machine schema. +- A hook: reviewer guidance → same guidance plus recheck only when module + responsibility, data structure, interface or execution flow decisions change. +- CLI P: implementation owner hint → architect sequence plus that existing hint. + CLI B and all transition gates retain their existing behavior. + +## Verification + +Run the existing hook/CLI tests before editing; extend runtime-output assertions +and establish a failing-before/passing-after regression. Run the affected suites, +`node plugins/codexclaw/scripts/build.mjs` and `node plugins/codexclaw/scripts/gate.mjs`. +Use the repository's full suite once on the combined change if required by its gate. +Use a task-owned TMPDIR outside `/tmp` (this host has an unrelated `/tmp/.git`). + +Verify the installed payload in an isolated home: P carries both policies; A hook +rechecks only design changes; B retains implementation ownership; CLI status and +other phases do not acquire the consultation hint. Compare the real plugin cache +before and after to detect accidental writes. + +Behavioral evidence is separate: an actual planning run must obtain a proposal +before finalizing its plan, send that plan to the same architect, and preserve the +independent reviewer. Include C0/C1 and no-delegation negative scenarios. A string +test or this task's earlier manually dispatched consultation is not proof of the +new policy's autonomous adoption. Report any unobserved scenario honestly. + +## Review and completion + +An independent reviewer checks the combined diff, existing executor behavior, +scope exemptions, evidence semantics and meaningful test coverage. Record fixes +and final verification in `041_architect_receipt.md`; leave the previous receipt +as historical evidence. Local commits follow the repository's `[agent]` convention. diff --git a/devlog/_plan/260914_executor_first_delegation/041_architect_receipt.md b/devlog/_plan/260914_executor_first_delegation/041_architect_receipt.md new file mode 100644 index 00000000..f6d386a6 --- /dev/null +++ b/devlog/_plan/260914_executor_first_delegation/041_architect_receipt.md @@ -0,0 +1,93 @@ +# 041 — Combined delegation verification + +Verified on 2026-09-15 KST. Worktree: `codex/executor-first-delegation`; +combined base `03541398`, previous executor-only head `14c20791`. + +The combined policy/runtime implementation passed independent review. P now +delivers architect consultation and executor ownership together; A reminds only +about changed design decisions; B retains executor ownership. Consultation records +identify the actual proposal, submitted plan revision, main dispositions and +same-architect reflection. No consultation gate or routing change was introduced. + +## Build and regression evidence + +| Check | Result | +|---|---| +| Existing hook/CLI baseline | 183 passed, 0 failed | +| New regression assertions on a temporary `14c20791` archive | 4 failed as expected; test process exit 1 | +| Current hook/CLI suites | 185 passed, 0 failed | +| `node plugins/codexclaw/scripts/build.mjs` | Exit 0; 181 generated files; only the two expected dist files differ | +| `npm test` | Exit 0; 3,154 total, 3,081 passed, 0 failed, 73 skipped | +| `node plugins/codexclaw/scripts/gate.mjs` | Exit 0 | +| Changed documentation links | 39 local file links resolve; reviewer also checked new fragments | +| `git diff --check` | Exit 0 | + +The executor returned its implementation and RED/GREEN report, but its native +lifecycle remained running. Main closed it after delivery, inspected the four-file +diff and independently ran build/full tests/gate. Completion rests on those files +and checks. The initial worker RED wrapper filtered stdout and returned pipeline +exit 0; the separate archive replay captured the actual test process exit 1. + +## Isolated installed payload + +Installed a real copy with `scripts/dev-install.sh --no-build` into a task-owned +Codex home. Final installation doctor: PASS. The installed hook/CLI module probe +passed 13 observations: P/A/B/C/D hook outputs, CLI P/A/B/C/D, status at P and B, +and I→P override. Initial fixture construction omitted the event name and armed +state and correctly failed on empty output; the fixture was fixed, not production. + +The independent reviewer recomputed source→dist output and compared it with both +generated files and the isolated installed bytes. All matched. The real plugin +cache's 1,042 file paths and SHA-256 values stayed unchanged through the final +behavioral trials. + +## Actual model behavior: bounded observations + +The requests asked for ordinary CXC planning or a one-word correction; they did +not instruct the model to call an architect by name. Native rollouts, not prose +claims or injected tool documentation, were used to identify calls. + +| Scenario | Observation | Limit | +|---|---|---| +| One-word typo | Exit 0; model selected C0, changed only that word, no subagent calls | Run after policy updates and before final runtime wiring; the C0 path was unchanged | +| Formal P plan | Model entered P and spawned architect before its final executable plan; one same-context retry and a fresh architect context followed | Both contexts failed; no design proposal or reflection was received; trial ended at its 240-second bound | +| Same formal plan after matching the app's V2-disable setting | Again attempted architect consultation | CLI still exposed the task-name/fork-turns surface; the same error recurred and the owned CLI process group was stopped | +| Formal P with explicit no delegation | Exit 0; no subagent calls, plan files written, consultation gap recorded, source unchanged, no A entry | One observed case, not a general compliance-rate measurement | + +The positive trials returned this native error: + +```text +stream disconnected before completion: Encrypted function output content could not be decrypted or decoded. +``` + +This change does not establish the cause of that transport failure. Full autonomous +proposal→plan→same-architect-reflection completion remains **unverified**. So do +live design-amendment rechecks and any improvement in invocation rate, cost or +quality. Main's earlier successful app-native architect consultation is design +evidence in 040, not proof that the patched policy is autonomously followed. + +## Independent review + +Reviewer `01a0a060-fcb3-75f3-9ec6-7e82f7e4efe2` reviewed the combined base-to-working +diff. Its one preliminary finding was missing exact consultation references in +040. Main added proposal/submitted-plan/reflection message IDs, timestamps, +rollout lines and the submitted packet hash; the reviewer independently reproduced +the hash and closed the finding. Final policy/runtime verdict: PASS, no blockers. +The reviewer covered all 19 pre-receipt changed files, including generated output +and historical evidence; it did not claim complete autonomous adoption. + +## Local evidence and reproduction + +Task evidence root: `/var/tmp/cxc-architect-01a0a049-aah8dgrt/`. + +- `run-checks.py`, `build.log`, `full-tests.log`, `gate.log`. +- `independent-red.log`: four failing regression tests on the old source archive. +- `install-final.log`, `verify-installed.mjs`, `installed-observations.json`. +- `source-manifest.json`: hashes of the 11 changed product/structure files. +- `real-cache-before.json`, `real-cache-after.json`: installation fingerprints. +- `run-behavior.py`, `behavior-summary.json`, and case directories: bounded CLI + trials and local rollout references. Raw transcripts remain outside Git. + +Temporary authentication copies were removed after each trial. The owned CLI +processes were reaped. The evidence directory is retained for inspection. +No changes were installed into the real Codex home and nothing was pushed. diff --git a/plugins/codexclaw/components/pabcd-state/dist/hook.js b/plugins/codexclaw/components/pabcd-state/dist/hook.js index 2a883c8b..8b793da9 100644 --- a/plugins/codexclaw/components/pabcd-state/dist/hook.js +++ b/plugins/codexclaw/components/pabcd-state/dist/hook.js @@ -303,6 +303,7 @@ 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.", "Record implementation ownership per planned change: a specified slice defaults to the configured executor, main needs a stated reason ($codexclaw:cxc-dev Implementation delegation).", "Plan-only ends with the plan. Forbidden checks: NOT RUN; naming an artifact grants no write permission.", ].join("\n"), @@ -311,6 +312,7 @@ 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]", diff --git a/plugins/codexclaw/components/pabcd-state/dist/orchestrate-cli.js b/plugins/codexclaw/components/pabcd-state/dist/orchestrate-cli.js index 2d8658b8..4e5ee603 100644 --- a/plugins/codexclaw/components/pabcd-state/dist/orchestrate-cli.js +++ b/plugins/codexclaw/components/pabcd-state/dist/orchestrate-cli.js @@ -460,7 +460,7 @@ function hasPabcdCloseRow( * executed. Advice only — it changes no gate. */ const OWNERSHIP_HINT = { - P: "implementation ownership: record an owner per planned change (cxc-dev Implementation delegation)", + P: "formal P: architect proposal -> main executable plan -> same-architect reflection before A (cxc-pabcd phase-plan); implementation ownership: record an owner per planned change (cxc-dev Implementation delegation)", B: "implementation ownership: execute the plan's recorded owners; a new handoff needs a P amendment", }; diff --git a/plugins/codexclaw/components/pabcd-state/src/hook.ts b/plugins/codexclaw/components/pabcd-state/src/hook.ts index f96f740f..be5449ef 100644 --- a/plugins/codexclaw/components/pabcd-state/src/hook.ts +++ b/plugins/codexclaw/components/pabcd-state/src/hook.ts @@ -303,6 +303,7 @@ 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.", "Record implementation ownership per planned change: a specified slice defaults to the configured executor, main needs a stated reason ($codexclaw:cxc-dev Implementation delegation).", "Plan-only ends with the plan. Forbidden checks: NOT RUN; naming an artifact grants no write permission.", ].join("\n"), @@ -311,6 +312,7 @@ 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]", diff --git a/plugins/codexclaw/components/pabcd-state/src/orchestrate-cli.ts b/plugins/codexclaw/components/pabcd-state/src/orchestrate-cli.ts index 3e5316de..4fd81d69 100644 --- a/plugins/codexclaw/components/pabcd-state/src/orchestrate-cli.ts +++ b/plugins/codexclaw/components/pabcd-state/src/orchestrate-cli.ts @@ -460,7 +460,7 @@ function hasPabcdCloseRow( * executed. Advice only — it changes no gate. */ const OWNERSHIP_HINT: Partial> = { - P: "implementation ownership: record an owner per planned change (cxc-dev Implementation delegation)", + P: "formal P: architect proposal -> main executable plan -> same-architect reflection before A (cxc-pabcd phase-plan); implementation ownership: record an owner per planned change (cxc-dev Implementation delegation)", B: "implementation ownership: execute the plan's recorded owners; a new handoff needs a P amendment", }; diff --git a/plugins/codexclaw/components/pabcd-state/test/hook.test.ts b/plugins/codexclaw/components/pabcd-state/test/hook.test.ts index f36c5503..bd3bd8df 100644 --- a/plugins/codexclaw/components/pabcd-state/test/hook.test.ts +++ b/plugins/codexclaw/components/pabcd-state/test/hook.test.ts @@ -123,6 +123,55 @@ test("wp3: phase pointers retain owners and active work-phase boundaries", () => 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 executor 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, /Record implementation ownership/); + + 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 executor 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, /Record implementation ownership/); + + 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 e1d01be5..9e44a5d0 100644 --- a/plugins/codexclaw/components/pabcd-state/test/orchestrate-cli.test.ts +++ b/plugins/codexclaw/components/pabcd-state/test/orchestrate-cli.test.ts @@ -2695,12 +2695,14 @@ test("260914: P and B entry echo the implementation-ownership pointer; other ver 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/); // 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); @@ -2708,6 +2710,7 @@ test("260914: P and B entry echo the implementation-ownership pointer; other ver 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({ @@ -2718,24 +2721,28 @@ 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.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 }); } }); @@ -2755,5 +2762,6 @@ test("260914: I->P agent override echoes the implementation-ownership pointer", 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/); } finally { rmSync(cwd, { recursive: true, force: true }); } }); diff --git a/plugins/codexclaw/skills/pabcd/SKILL.md b/plugins/codexclaw/skills/pabcd/SKILL.md index f85cd39d..499fc1d4 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 implementation owners | | 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 | 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 c0ecc268..8bc0ab23 100644 --- a/plugins/codexclaw/skills/pabcd/references/plan-output.md +++ b/plugins/codexclaw/skills/pabcd/references/plan-output.md @@ -26,6 +26,27 @@ 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 The file change map names an owner for each planned change: the configured diff --git a/structure/20_pabcd_dispatch_doctrine.md b/structure/20_pabcd_dispatch_doctrine.md index 5d483de9..32e5daec 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 diff --git a/structure/INDEX.md b/structure/INDEX.md index 53f9ceb1..0c5b98d0 100644 --- a/structure/INDEX.md +++ b/structure/INDEX.md @@ -323,7 +323,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`; implementation ownership (a specified slice defaults to the configured executor, recorded in P and executed 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. Implementation ownership (a specified slice defaults to the configured executor, recorded in P and executed 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 a0163227e75ab06861c07721e8382aa5924f26a6 Mon Sep 17 00:00:00 2001 From: thisisjun786 <259586770+thisisjun786@users.noreply.github.com> Date: Tue, 15 Sep 2026 00:52:46 +0900 Subject: [PATCH 6/7] [agent] fix(subagents): preserve encrypted V2 tasks and verify delegation --- README.ko.md | 2 +- README.md | 2 +- README.zh.md | 2 +- .../000_plan.md | 2 + .../041_architect_receipt.md | 4 + .../042_runtime_completion.md | 148 ++++++++++++++++++ .../subagent-config/dist/spawn-attach-hook.js | 33 ++-- .../subagent-config/src/spawn-attach-hook.ts | 33 ++-- .../test/spawn-attach-hook.test.ts | 45 +++++- plugins/codexclaw/test/hook-e2e.test.mjs | 22 ++- structure/20_pabcd_dispatch_doctrine.md | 10 +- structure/INDEX.md | 2 +- 12 files changed, 260 insertions(+), 45 deletions(-) create mode 100644 devlog/_plan/260914_executor_first_delegation/042_runtime_completion.md diff --git a/README.ko.md b/README.ko.md index 35736a36..24a61f33 100644 --- a/README.ko.md +++ b/README.ko.md @@ -13,7 +13,7 @@

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

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

CI - 3,150 tests passing + 3,158 tests passing 29 skills 28 hooks Documentation diff --git a/devlog/_plan/260914_executor_first_delegation/000_plan.md b/devlog/_plan/260914_executor_first_delegation/000_plan.md index 2166e18c..ccd151ab 100644 --- a/devlog/_plan/260914_executor_first_delegation/000_plan.md +++ b/devlog/_plan/260914_executor_first_delegation/000_plan.md @@ -11,6 +11,8 @@ architect 절차도 P 진입 시 알린다. Architect가 설계를 제안하고, 근거와 소스 앵커는 [001_source_evidence.md](001_source_evidence.md)에 있다. 기존 executor 작업의 wp1–wp4와 검증 기록은 아래에 보존한다. 승인된 architect 후속 변경의 범위·분담·검증은 [040](040_architect_consultation.md)에 기록한다. +실제 설계·재확인 흐름의 완주 검증과 호출 훅의 암호문 보존 수정은 +[042](042_runtime_completion.md)에 기록한다. ## Loop spec diff --git a/devlog/_plan/260914_executor_first_delegation/041_architect_receipt.md b/devlog/_plan/260914_executor_first_delegation/041_architect_receipt.md index f6d386a6..c23b9044 100644 --- a/devlog/_plan/260914_executor_first_delegation/041_architect_receipt.md +++ b/devlog/_plan/260914_executor_first_delegation/041_architect_receipt.md @@ -3,6 +3,10 @@ Verified on 2026-09-15 KST. Worktree: `codex/executor-first-delegation`; combined base `03541398`, previous executor-only head `14c20791`. +Follow-up: [042](042_runtime_completion.md) identifies and corrects the pre-existing +encrypted-message corruption, and completes the previously missing formal P and +design-amendment runtime observations. This receipt preserves the earlier results. + The combined policy/runtime implementation passed independent review. P now delivers architect consultation and executor ownership together; A reminds only about changed design decisions; B retains executor ownership. Consultation records diff --git a/devlog/_plan/260914_executor_first_delegation/042_runtime_completion.md b/devlog/_plan/260914_executor_first_delegation/042_runtime_completion.md new file mode 100644 index 00000000..16610e10 --- /dev/null +++ b/devlog/_plan/260914_executor_first_delegation/042_runtime_completion.md @@ -0,0 +1,148 @@ +# 042 — Complete the remaining architect runtime verification + +Follow-up to 041, 2026-09-15 KST, based on `8f4b6bf9` in the same worktree. +The user requested completion of the remaining live verification. The formal P +trial now completes proposal → main executable plan → same-architect reflection. +It exposed a pre-existing spawn-hook defect that had blocked every earlier +positive trial. Design-amendment and text-only observations also completed below. +These are bounded observations, not a measured invocation-rate or quality gain. + +## Failure, causal test and correction + +Native V2 supplied the task as a Fernet-shaped encrypted `message`. The existing +spawn hook put a plaintext leaf guard, skill affordance and optional prompt override +around it. The child received the mixture in an `encrypted_content` slot and the +backend rejected it with: + +```text +stream disconnected before completion: Encrypted function output content could not be decrypted or decoded. +``` + +Three hypotheses were tested: CXC message mutation, a general V2/backend failure, +and mismatched test configuration/fork arguments. All controls used the same local +CLI 0.154.0 and account-backed inherited model. The actual exposed surface was V2 +despite the disabled feature flag; the schema, not that flag, identifies the trial. + +| Control | Actual child result | +| --- | --- | +| `no-plugin-control` | CHILD_OK, then FOLLOWUP_OK | +| `no-plugin-fresh-explicit` | Same success with a fresh context | +| `plugin-control` | Original decryption error | +| `plugin-minimal-fields` | Same error with optional spawn fields omitted | +| `plugin-preserve-ciphertext` | Same installed plugin, only the message-preservation expression changed: both replies succeeded | +| `plugin-restored` | Original installed hook restored in a fresh fixture: same error returned | + +The last two trials establish the causal off/on check. A general V2 failure and +fork-argument explanation do not account for these observations. Parent CLI exit +0 in a control means it reported its result; child success/failure above comes +from native replies and error events, not from that exit code. + +The local correction is in `components/subagent-config/src/spawn-attach-hook.ts` +under `plugins/codexclaw/`, with rebuilt dist and runtime regressions. It preserves +recognized native V2 ciphertext byte-for-byte and discloses that hook-added text +was not attached. Metadata-based recursion denial, explicit settings, full-history +fork restrictions, separate routing fields and V1 item attachments retain their +existing behavior. Main made this small local boundary correction; independent +source investigation and review ran separately. No role, model, permission or +consultation gate was added. Existing doctrine claims about encrypted delivery +were corrected in `structure/20_pabcd_dispatch_doctrine.md` and `structure/INDEX.md`. + +The token detector is a format heuristic, not decryption or authentication. +Encrypted managed markers and encrypted cross-provider delivery remain outside +this fix. This trial uses inherited routing and does not certify those paths. + +## Regression and installed evidence + +- Old source with new byte-preservation assertions: two tests failed. The unchanged + recursion-denial case passed. See `cipher-red.log`. +- Subagent-config suite after the correction: 328 passed, 0 failed. +- The first full run found one old E2E assertion that required corrupting ciphertext. + It was replaced with separate ciphertext-preservation and plaintext-affordance + cases through the shipped entrypoint. Both capabilities remain tested; no skip + or threshold was added. +- Final focused source/entrypoint suites: 120 passed, 0 failed. +- Final full suite: 3,158 tests, 3,085 passed, 0 failed, 73 existing skips. +- Build, repository gate and `git diff --check`: exit 0. +- Real plugin copy installed only into the task-owned `followup-home`; doctor PASS. + The final reinstall includes the comment correction made after initial P startup; + that correction does not change executable behavior. + +## Actual formal P + +The natural request asked for a plan to add title search and pagination to a tiny +existing catalog module. It did not name architect or prescribe its calls. It +forbade implementation, goal creation and A entry. + +- Parent: `01a0a08c-adda-7f23-aaca-b4658d7490be`. +- Architect: `01a0a08d-80b2-7310-99cf-c4e48c74aa14`, returned V2 handle + `/root/catalog_architect`. +- Parent rollout: architect spawn at line 38; concrete two-file plan write at + line 85; same-handle `followup_task` at line 90. +- Architect rollout: CAT-01–CAT-05 proposal at line 32; revision-1 reflection at + line 48, ALIGNED, mapping every decision to plan paths and acceptance criteria. +- Parent then recorded the reflection and stopped in P, exit 0. Source and README + stayed unchanged. The plan records executor ownership for the later implementation. +- The separate cold-read helper is document readability evidence, not A review. + +The plan is in `behavior-positive-fixed/devlog/_plan/260915_catalog_query/`. +`positive-fixed-r1/` preserves its completed revision before subsequent amendments. +Raw native transcripts remain outside Git under the evidence root. + +## Design amendment and text-only amendment + +The same parent resumed with a natural requirement change: replace page/pageSize +with offset/limit and complete A review, without implementation or B entry. + +- Main revised CAT-02, CAT-03 and CAT-05, preserving CAT-01 and CAT-04, and sent + revision 2 to `/root/catalog_architect` at parent rollout line 160. +- The same architect returned ALIGNED at its rollout line 64, with changed and + preserved decisions mapped to the revised contract and acceptance criteria. +- Parent then spawned independent `/root/catalog_audit` at line 205, using the + supported read-only explorer plus logical reviewer packet. Native reviewer was + absent from that trial schema; no architect-role substitution occurred. +- Reviewer `01a0a094-1295-7b51-92a6-e5a3d1c22dda` returned VERDICT: PASS and no + blocking issues at rollout line 75. The parent's completed turn is line 281. +- The amended executable specification's SHA-256 is + `fecec8d67011628435b55dd97e25d4c2bc20441fc15a4b84aa10a04f28d07efa`. + `002_audit.md` records the main disposition, architect reflection and independent + review separately. `positive-fixed-r2/` preserves that completed revision. +- The final wording-only request changed just the first heading's “페이지 조회” + to “구간 조회”. Parent rollout lines 282–302 contain one shell/tool composition + and no spawn or architect follow-up. Independent byte comparison found exactly + that replacement, with every other plan file, catalog source and README unchanged. + See `text-amendment-proof.json`. + +Both resumed CLI runs exited 0. The trial stayed at A; no implementation, B entry +or goal was created. The real parent task remained IDLE throughout. Earlier C0 +and explicit-no-delegation observations in 041 remain separate evidence for those +unchanged paths. Actual model routing across providers was not measured. + +## Independent review and evidence location + +Reviewer `01a0a08c-afe5-7660-8c9c-103296617b54` initially returned PASS with no blockers +and independently ran 52 assertions on source/dist parity, recursion, routing, +ciphertext variants, disclosure, plaintext and items. Its two minor observations +were addressed: the stale inline comment and missing persistent disclosure check. +The same reviewer subsequently returned PASS for the corrected E2E case and +completed P evidence, reproduced source→plan→same-architect ordering, checked +the final suite logs and independently compared all 14 changed plugin files with +installed bytes. Its verdict is distinct from the fixture's independent A review. +The final receipt-only review also returned PASS, confirming revision-2 reflection +before independent A, the preserved revision hash, the title-only diff and forward +links. Cleanup and the real-cache comparison are explicitly main-verified evidence. + +Evidence root: `/var/tmp/cxc-architect-01a0a049-aah8dgrt/`. Relevant additions: +`control-probe.py`, the six control directories, `remaining-validation-hypotheses.md`, +`cipher-{red,green,focused-final}.log`, `followup-{build,install,gate}-final.log`, +`followup-full-tests-final.log`, `run-behavior-fixed.py`, `run-amendment.py`, +`followup-home/sessions/`, `behavior-positive-fixed/`, and `positive-fixed-r1/`. +An initial resume command rejected unsupported `--color` before starting a turn; +the runner removed that argument. This was a harness correction, not a model retry. + +`transport-controls-summary.json` records the original encrypted slots and child +outcomes without copying ciphertext into Git. `followup-source-installed-manifest.json` +records changed tracked-file hashes. Main compared all 1,042 real plugin-cache +paths and hashes against the original baseline: unchanged. All temporary auth +copies were removed, and owned CLI processes were reaped. The evidence directory +is retained. This follow-up remains a local working-tree change; no commit, push, +merge or real Codex-home installation was performed in this verification follow-up. 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..445063d1 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 @@ -848,6 +848,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 && /^gAAAA[A-Za-z0-9_-]+={0,2}$/.test(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 +914,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 +1009,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 +1044,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 +1069,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/spawn-attach-hook.ts b/plugins/codexclaw/components/subagent-config/src/spawn-attach-hook.ts index 1f1d734a..042983d0 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 @@ -848,6 +848,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 && /^gAAAA[A-Za-z0-9_-]+={0,2}$/.test(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 +914,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 +1009,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 +1044,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 +1069,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/spawn-attach-hook.test.ts b/plugins/codexclaw/components/subagent-config/test/spawn-attach-hook.test.ts index 93e5668d..9580e075 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 @@ -995,19 +995,57 @@ 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", ); }); +test("native V2 ciphertext survives routing and prompt overrides byte-for-byte", () => { + // Synthetic Fernet-shaped input, not a credential or a decryptable task. + const message = `gAAAAAB${"aB9_".repeat(30)}==`; + 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 = `gAAAAAB${"aB9_".repeat(30)}==`; + 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 = `gAAAAAB${"aB9_".repeat(30)}==`; + const result = JSON.parse(runSpawnAttachHook(subagentSpawnPayload({ task_name: "nested", message }))); + assert.equal(result.hookSpecificOutput.permissionDecision, "deny"); + assert.equal(result.hookSpecificOutput.updatedInput, undefined); +}); + 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 +1212,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/test/hook-e2e.test.mjs b/plugins/codexclaw/test/hook-e2e.test.mjs index 9cb8b11e..1c39a228 100644 --- a/plugins/codexclaw/test/hook-e2e.test.mjs +++ b/plugins/codexclaw/test/hook-e2e.test.mjs @@ -802,9 +802,11 @@ 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 [ + ["ciphertext stays byte-identical", "gAAAAABopaque-payload", true], + ["plaintext gains the skill affordance", "Inspect the catalog module.", 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 +815,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 }); diff --git a/structure/20_pabcd_dispatch_doctrine.md b/structure/20_pabcd_dispatch_doctrine.md index 32e5daec..4d1e53b6 100644 --- a/structure/20_pabcd_dispatch_doctrine.md +++ b/structure/20_pabcd_dispatch_doctrine.md @@ -273,11 +273,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 0c5b98d0..b90b35fb 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 | From 56355b73e1343822f2c32e4aee906bbe7bfe8993 Mon Sep 17 00:00:00 2001 From: thisisjun786 <259586770+thisisjun786@users.noreply.github.com> Date: Tue, 15 Sep 2026 02:04:44 +0900 Subject: [PATCH 7/7] [agent] fix(subagents): validate native V2 ciphertext envelopes --- README.ko.md | 2 +- README.md | 2 +- README.zh.md | 2 +- .../050_codex_review.md | 131 ++++++++++++++++++ .../051_codex_review_receipt.md | 73 ++++++++++ .../subagent-config/dist/spawn-attach-hook.js | 37 ++++- .../subagent-config/src/spawn-attach-hook.ts | 37 ++++- .../test/spawn-attach-hook.test.ts | 117 +++++++++++++++- plugins/codexclaw/skills/search/SKILL.md | 12 +- plugins/codexclaw/test/hook-e2e.test.mjs | 6 +- structure/10_subagent_skill_routing.md | 16 ++- 11 files changed, 411 insertions(+), 24 deletions(-) create mode 100644 devlog/_plan/260914_executor_first_delegation/050_codex_review.md create mode 100644 devlog/_plan/260914_executor_first_delegation/051_codex_review_receipt.md diff --git a/README.ko.md b/README.ko.md index 24a61f33..e7de4b25 100644 --- a/README.ko.md +++ b/README.ko.md @@ -13,7 +13,7 @@

CI - 3,158 tests passing + 3,162 tests passing 29 skills 28 hooks Documentation diff --git a/README.md b/README.md index 2e2b81cf..25a36f4f 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@

CI - 3,158 tests passing + 3,162 tests passing 29 skills 28 hooks Documentation diff --git a/README.zh.md b/README.zh.md index a5b2e441..4cd794b1 100644 --- a/README.zh.md +++ b/README.zh.md @@ -13,7 +13,7 @@

CI - 3,158 tests passing + 3,162 tests passing 29 skills 28 hooks Documentation diff --git a/devlog/_plan/260914_executor_first_delegation/050_codex_review.md b/devlog/_plan/260914_executor_first_delegation/050_codex_review.md new file mode 100644 index 00000000..1057df53 --- /dev/null +++ b/devlog/_plan/260914_executor_first_delegation/050_codex_review.md @@ -0,0 +1,131 @@ +# 050 — PR 177 ciphertext classification review + +Loop: satisfy-spec, triggered by the user's request to fix consequential PR Codex +feedback. Memory artifact: this unit and its verification receipt. Success is the +verified outcome below; an unavailable review/runtime is reported as unresolved, +never a pass. Main reclaims implementation only after the configured two-attempt +dispatch path fails with reconciled evidence. A new worker handoff needs a P +amendment. Ask the user only for a scope change or an irreversible action beyond the +already authorized PR/fork/local patch; no upstream merge is authorized. + +## Objective and class + +Close the consequential Codex finding on PR 177, keep the executor/architect +policy intact, obtain a completed review on the corrected head, and retain that +validated patch in the local source, fork and installed plugin. C4 care applies +because the predicate controls whether child task guards are attached. + +## Repository evidence and failure + +Baseline: public head `a0163227e75ab06861c07721e8382aa5924f26a6`, identical tree to +the retained private local head `2a1e3b7e0680157296354d871bd7ce71fb508221`. +Codex comment `4007056665` at `spawn-attach-hook.ts:851` correctly identifies +that `gAAAAx` bypasses task guard and prompt-override attachment. A direct hook +probe reproduced unchanged message and a false ciphertext-preservation notice. +The previous unit/e2e ciphertext fixtures are not valid envelopes. + +## Threat model and constraints + +Asset: child task instructions and native encrypted-task integrity. Entrypoint: +PreToolUse V2 spawn message. The caller can provide arbitrary plaintext; the hook +cannot decrypt or authenticate backend ciphertext. Prefix resemblance alone +must not remove plaintext guards. Conversely, rewriting real ciphertext breaks +native spawn decryption. Recognize envelope structure only; never claim HMAC +authentication. D1 recursion denial, explicit routing and fork restrictions keep +their existing ordering. No provider, dependency, credential or gate changes. + +## Architect consultation and main decisions + +Architect `01a0a0b7-cc7b-7be1-88b2-2aece0d3171c`, managed dispatch +`pr177-envelope-design`, supplied ENV-01 through ENV-07. Main dispositions: + +- ENV-01: amend: colocate a private pure predicate beside the V2 shape helper; + test observable hook behavior rather than exporting solely for tests. +- ENV-02: amend: accept canonical padded and wholly unpadded base64url. Reject + bad alphabet, partial/excess padding, impossible encoded lengths and nonzero + unused pad bits with a decode/re-encode check. This is structural recognition, + not authentication; HMAC cannot distinguish encodings of identical bytes. +- ENV-03: accept: decoded version 0x80, minimum 73 bytes and + `(length - 57) % 16 === 0`. Drop the timestamp-dependent `gAAAA` prefix test. + No timestamp/TTL or MAC validation. +- ENV-04: accept: malformed input follows existing plaintext attachment and + emits no ciphertext notice. V1 stays on its existing attachment path. +- ENV-05/07: amend: real public Fernet vector plus synthetic block/padding + variants and malformed inputs, exercised through the public hook. Preserve + explicit settings, full-history restrictions and recursion denial assertions. +- ENV-06: accept: rebuild and commit the distributed hook. + +Primary sources: [Fernet specification](https://github.com/fernet/spec/blob/master/Spec.md) +and [public generation vector](https://raw.githubusercontent.com/fernet/spec/master/generate.json). +No live task token or key belongs in committed fixtures. + +## Scope and ownership + +Executor: `components/subagent-config/src/spawn-attach-hook.ts`, its existing unit +test, and `test/hook-e2e.test.mjs` under `plugins/codexclaw/`. Main: consultation, +independent review, this record, matching search-skill wording, generated dist, +test inventory badges, integration, publication and installation evidence. Matching +stale ciphertext/affordance paragraphs in `structure/10_subagent_skill_routing.md` +are synchronized with the existing INDEX behavior description; no routing change. + +Two existing V2 transport-parity fixtures omit `agent_type`; the new checkout's +`delegation-review` path enters the skill text and triggers legacy keyword-based +role inference. Baseline focused run was 118/120 passing. Make those fixtures +explicitly explorer like their V1 counterparts; retain all routing assertions. +Changing production inference is outside this repair and remains a known quirk. + +## Interfaces and behavior + +Before: any prefix-shaped `gAAAA...` token bypasses plaintext attachment. +After: only canonically encoded Fernet-shaped frames use byte preservation; +short/malformed text receives the ordinary guard, affordance and configured +prompt override. Existing behavior for actual ciphertext remains byte-identical. +Search skill wording must describe the plaintext affordance only on plaintext +V2, and explicitly state encrypted-message attachment is unavailable. + +## Executable steps and budget + +1. Obtain same-architect reflection on this concrete plan and independent A audit. +2. Delegate bounded hook/test changes; main corrects the matching search paragraph. +3. Inspect diff, rebuild shipped JavaScript, run negative/positive regression and + full tests, update measured inventory and run repository gate. +4. Push correction to the existing PR branch, reply to the finding, request a + fresh Codex review, and inspect latest-head findings and CI. Repeat only for + consequential remaining defects, updating this unit when design changes. +5. Apply reviewed delta atop preserved local history, fast-forward fork dev, + install from local source, compare bytes and preserved configuration, and + close work/goal only with the evidence captured. + +No user token/time ceiling was set. Commands and probes use bounded background +handles. Waiting for a running review is not a failed attempt. + +## Verification and acceptance + +Malformed cases: short prefix, old invalid fixtures, wrong version, undersized +frame, empty/non-block ciphertext, invalid alphabet/whitespace/padding/pad bits. +Each V2 malformed case must retain guard and override and lack omission notice. +Valid cases: real reference token, padded/unpadded forms, several ciphertext block +counts, timestamp bytes that do not encode the old prefix, exact settings/fork +behavior, recursion denial, and V1 nonclassification. E2e must use rebuilt dist. +Repeat a bounded actual native encrypted V2 spawn against isolated updated plugin +bytes; require actual child/follow-up results and the hook's ciphertext-preservation +notice. If a diagnostic exposes envelope metadata, retain only lengths/padding, +never raw live tokens. The canonical padding cases have independent unit evidence; +do not infer the backend's padding variant from child success. Prior runtime proof +is historical and does not alone validate the stricter classifier. + +Done requires: regression and repository gate pass, completed latest-head Codex +review with no unaddressed consequential finding, passing hosted CI, and local +source/fork/installed equality with role settings preserved. No upstream merge. + +## Review and remaining uncertainty + +Same-handle architect reflection was ALIGNED on plan SHA-256 +`0d0e68a3d0ea33911f6dcf937787971eecd298655b250835da979bb76e35f9bc`. +Main resolves its G1 evidence gap above: actual hook notice plus child results are +required; emitter padding is not claimed without a separate shape observation. +Independent A reviewer `01a0a0c1-efce-7c13-a6bb-d5424701cac4` returned PASS without +findings on plan SHA-256 `a1da4dcda5fca75c5f7a6a0047eb9d8565b917e7b0d4c59a09a0c08218d1ce33`. +Backend format support +is limited to the observed Fernet envelope; recognition cannot prove authenticity. +The main agent owns all final decisions and integration. diff --git a/devlog/_plan/260914_executor_first_delegation/051_codex_review_receipt.md b/devlog/_plan/260914_executor_first_delegation/051_codex_review_receipt.md new file mode 100644 index 00000000..d4a3e166 --- /dev/null +++ b/devlog/_plan/260914_executor_first_delegation/051_codex_review_receipt.md @@ -0,0 +1,73 @@ +# 051 — PR 177 review correction evidence + +Codex finding [4007056665](https://github.com/lidge-jun/codexclaw/pull/177#discussion_r4007056665) +was accepted. The previous regex treated `gAAAAx` as ciphertext, suppressing task +guards and the configured prompt override. The correction recognizes canonical +base64url Fernet frame structure. It does not authenticate or decrypt a token. +Existing executor/architect ownership and routing policy remain unchanged. + +## Regression and native execution + +Evidence root: task-owned `cxc-architect-01a0a049-aah8dgrt` under `/var/tmp/`. +Raw runtime data and operator settings remain outside the repository. + +| Check | Observed result | +|---|---| +| `pr177-boundary-probe.mjs` against previous installed dist | Exit 1: guard false, override false, false ciphertext notice true | +| Same probe against rebuilt corrected dist | Exit 0: guard true, override true, false ciphertext notice false | +| `npm run build` | Exit 0; 181 generated component files validated | +| `npm audit --json` | Exit 0; 0 reported vulnerabilities; dependency files unchanged | +| Isolated native V2 spawn and same-child follow-up | Exit 0; actual child replies `CHILD_OK` and `FOLLOWUP_OK`, with the actual hook preservation notice | + +Native evidence: `pr177-native-envelope/{artifact,observations,exit}.json` in that +external root. Parent task `01a0a0d1-a298-7693-88d5-6fd0ac135742` records the hook's +developer notice at rollout line 17. Child +`01a0a0d1-c194-7db2-936a-734121d0ad90` records the two actual replies at lines 12 +and 21. The probe's distributed hook bytes match the reviewed source build. +Its CLI process exited and was reaped; temporary authentication was removed. +Native V2 exposed interrupt rather than close; interrupt returned the child's +completed follow-up status. No probe process was left running. + +The probe confirms the recognized native transport path. It does not identify the +emitter's padding variant or prove cross-provider transport compatibility. + +## Independent review + +Reviewer `01a0a0c1-efce-7c13-a6bb-d5424701cac4` approved the executable plan, then +returned a separate code-only PASS without findings on the source, generated dist +and matching search/routing documentation. Its read-only Node probes passed +20,878 assertions covering lengths 0–512, all version bytes at valid lengths, +padding variants, malformed encodings and old invalid fixtures. Source and dist +were both exercised; `git diff --check` passed. Final committed test coverage and +remote latest-head review are separate checks, recorded at completion. + +Two pre-existing transport-parity tests failed in the review-named checkout +because their V2 fixtures omitted an explicit role. The repair makes them explorer +like their paired V1 inputs, retaining the routing assertions. The production +keyword inference quirk remains outside this repair. + +## Implementation integration + +Executor `01a0a0c3-aa3f-7070-ae55-530abed68915` authored the predicate and unit +regression patch. Repeated tool-call quoting errors prevented it from applying +the latter. Main stopped the worker, confirmed shutdown, preserved its source +changes and recovered the authored patch from its failed call. Main integrated +that patch, completed the planned e2e fixture alignment and added the missing +whitespace/lookalike cases during review. No model fallback or successful worker +completion is claimed; the unclassified dispatch failure remains recorded. +The first integrated focused run passed 123 tests. The final suite includes the +additional e2e short-lookalike case and three further negative table rows. + +The final independent review returned PASS with no findings on test coverage and +raw native evidence. The first full run measured 3,162 tests and found one +overstrict test assertion: the plaintext path already trims trailing whitespace, +so its trailing-carriage-return case cannot demand ciphertext-style byte identity. +Main changed that assertion to preserve task text while retaining every guard, +affordance, prompt-override and omission-notice check. The corrected focused run +passed all 124 tests. Generated inventory uses the measured total of 3,162. + +Final full-suite, published-head CI/review and local-install results are recorded +outside the repository against the fixed commit: the session's producer receipt, +PR conversation and the protected `cxc-pr177-review-20260914T163533Z` backup. +Those later checks must pass before task completion; this record does not claim +that a queued or historical remote review passed on a newer head. 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 445063d1..0b9daaaa 100644 --- a/plugins/codexclaw/components/subagent-config/dist/spawn-attach-hook.js +++ b/plugins/codexclaw/components/subagent-config/dist/spawn-attach-hook.js @@ -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,7 +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 && /^gAAAA[A-Za-z0-9_-]+={0,2}$/.test(message); + 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") 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 042983d0..23980a3e 100644 --- a/plugins/codexclaw/components/subagent-config/src/spawn-attach-hook.ts +++ b/plugins/codexclaw/components/subagent-config/src/spawn-attach-hook.ts @@ -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,7 +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 && /^gAAAA[A-Za-z0-9_-]+={0,2}$/.test(message); + 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") 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 9580e075..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); @@ -1008,9 +1011,32 @@ test("v2 affordance: appended only when inlining attached nothing", () => { ); }); +// 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", () => { - // Synthetic Fernet-shaped input, not a credential or a decryptable task. - const message = `gAAAAAB${"aB9_".repeat(30)}==`; + const message = FERNET_VECTOR; const cwd = workspaceWithConfig({ architect: { mode: "model", model: "architect-fixture", effort: "high", promptOverride: "Architect-only instructions" }, }); @@ -1025,7 +1051,7 @@ test("native V2 ciphertext survives routing and prompt overrides byte-for-byte", }); test("native V2 ciphertext preserves explicit settings and full-history fork restrictions", () => { - const message = `gAAAAAB${"aB9_".repeat(30)}==`; + const message = FERNET_VECTOR; const cwd = workspaceWithConfig({ architect: { mode: "model", model: "configured-fixture", effort: "high", promptOverride: "Architect-only instructions" }, }); @@ -1040,12 +1066,93 @@ test("native V2 ciphertext preserves explicit settings and full-history fork res }); test("native V2 ciphertext cannot bypass the existing recursion denial", () => { - const message = `gAAAAAB${"aB9_".repeat(30)}==`; + 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)); 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 1c39a228..ac4ac328 100644 --- a/plugins/codexclaw/test/hook-e2e.test.mjs +++ b/plugins/codexclaw/test/hook-e2e.test.mjs @@ -804,8 +804,10 @@ test("260710: spawn hook e2e - native collaboration name drives the V2 path", () // The shipped entrypoint preserves native ciphertext and augments plaintext. for (const [name, message, encrypted] of [ - ["ciphertext stays byte-identical", "gAAAAABopaque-payload", true], + // 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); @@ -894,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,