kenaz 0.3.5 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/core/.claude/commands/Ansuz.md +1 -0
  2. package/core/.collaboration/core/reconcile.js +193 -0
  3. package/core/.collaboration/core/task-2404-reconcile.test.js +135 -0
  4. package/core/.collaboration/core/task-api.js +27 -1
  5. package/core/.formation/native/claude/README.md +1 -1
  6. package/core/.formation/role-map.yaml +3 -3
  7. package/core/.formation/spec/task_operations.yaml +3 -3
  8. package/core/.formation/tools/__tests__/subagent-bifrost-guard.test.js +4 -4
  9. package/core/.formation/tools/dispatch.js +5 -5
  10. package/core/.formation/tools/prompt.js +4 -0
  11. package/core/.formation/unit/do.yaml +2 -4
  12. package/core/.rule/general/agent-workflow.yaml +4 -4
  13. package/core/.rule/general/file-size-threshold.yaml +16 -4
  14. package/core/.rule/general/rust-verify-target.yaml +0 -1
  15. package/core/.rule/general/system-invariants.yaml +8 -25
  16. package/core/.specialist/Ansuz/SYSTEM_DOC.md +17 -42
  17. package/core/.specialist/Ansuz/capability.yaml +7 -14
  18. package/core/.specialist/Ansuz/concepts/formation_quick_ref.yaml +4 -11
  19. package/core/.specialist/Ansuz/elixir.yaml +3 -4
  20. package/core/.specialist/Ansuz/identity_contract.yaml +1 -1
  21. package/core/.specialist/Ansuz/knowledge.yaml +7 -10
  22. package/core/.specialist/Ansuz/rule.yaml +2 -2
  23. package/core/.specialist/Ansuz/tools/verify-and-land.js +22 -0
  24. package/core/.specialist/Ansuz/tools/verify-and-land.test.js +59 -5
  25. package/core/.specialist/Ansuz/workflow.yaml +7 -11
  26. package/core/.specialist/Forseti/core.yaml +1 -1
  27. package/core/.specialist/Heimdall/SKILL.md +1 -1
  28. package/core/.specialist/Heimdall/core.yaml +2 -2
  29. package/core/.specialist/Huginn/rule.yaml +1 -1
  30. package/core/.specialist/tools/README.md +2 -2
  31. package/core/.specialist/tools/ansuz-boot-fast.js +20 -0
  32. package/core/.system/collaboration/README.md +1 -1
  33. package/core/.system/formation/LEGACY_SKILLS.md +2 -0
  34. package/core/.system/formation/POST_COMPLETE.md +2 -0
  35. package/core/.system/formation/README.md +2 -0
  36. package/core/KENAZ_CORE_VERSION +1 -1
  37. package/core/dev/scripts/check-file-size.js +44 -17
  38. package/core/k-cli/README.md +1 -94
  39. package/core/k-cli/test/TEST_SPEC.md +5 -22
  40. package/core/k-cli/test/run.sh +1 -67
  41. package/core/plugins/kenaz/commands/Ansuz.md +1 -0
  42. package/package.json +6 -6
  43. package/core/.formation/legion/forge.yaml +0 -299
  44. package/core/.formation/legion/hive.yaml +0 -238
  45. package/core/.formation/squad/blitz.yaml +0 -192
  46. package/core/.formation/squad/strategy-tribunal.yaml +0 -354
  47. package/core/.formation/squad/strategy.yaml +0 -349
  48. package/core/.formation/squad/sweep.yaml +0 -208
@@ -90,19 +90,6 @@ invariants:
90
90
  # Parallel execution caps, locks, isolation boundaries
91
91
  # ============================================================================
92
92
 
93
- - name: max_parallel_dispatches
94
- category: concurrency
95
- formula: "concurrent_worktree_dispatches ≤ MAX_PARALLEL"
96
- bound: "MAX_PARALLEL = 5"
97
- enforcement: |
98
- Semaphore in formations.js dispatch loop — when count reaches
99
- MAX_PARALLEL, new dispatches wait. Logs "Max parallel dispatches
100
- reached (N/5), waiting...".
101
- violation_detection: |
102
- Count live worktree dispatches (.formation/worktrees/task_*/ with
103
- active child processes); if > 5, semaphore leaked.
104
- source: ".formation/tools/formations.js#L773-L810 (MAX_PARALLEL semaphore)"
105
-
106
93
  - name: worktree_isolation_boundary
107
94
  category: concurrency
108
95
  formula: |
@@ -111,17 +98,13 @@ invariants:
111
98
  repo paths)
112
99
  bound: "one worktree per active dispatch; no cross-worktree writes"
113
100
  enforcement: |
114
- runner.js spawn-time prompt injection — spawnClaude() and spawnBifrost()
115
- prepend "[WORKTREE] All file operations MUST use paths under:
116
- <worktreeDir>" (the CONCRETE path) to the final prompt whenever
117
- worktreeDir is set. Agent-tool dispatch additionally carries
118
- SUBAGENT_PREFIX rule 11 (WORKTREE FILE PATH RULE) in prompt.js.
119
- (TASK_1819: the former do.yaml worktree_path_rule field was a dead
120
- field — never rendered by prompt.js — and has been removed.)
101
+ Agent-tool dispatch carries SUBAGENT_PREFIX rule 11 (WORKTREE FILE
102
+ PATH RULE) in prompt.js. (The former runner.js spawn-time "[WORKTREE]"
103
+ injection was removed with the headless path, TASK_2401.)
121
104
  violation_detection: |
122
105
  Agent Edits a path under main repo root while cwd is a worktree →
123
106
  `git add -A` in worktree finds no changes. See FAIL-013.
124
- source: ".formation/tools/runner.js#L958,L1069 ([WORKTREE] prompt injection) + .formation/tools/prompt.js SUBAGENT_PREFIX rule 11"
107
+ source: ".formation/tools/prompt.js SUBAGENT_PREFIX rule 11"
125
108
  failure_witness: ".failure/agent-execution/FAIL-013-search-cwd-drift-and-case-mismatch.yaml"
126
109
 
127
110
  - name: per_project_dispatch_lock
@@ -129,13 +112,13 @@ invariants:
129
112
  formula: "per-project dispatch-lock serializes non-worktree dispatches"
130
113
  bound: "1 in-flight serial dispatch per project at a time"
131
114
  enforcement: |
132
- Dispatch-lock fallback in formations.js — when worktree parallel
133
- slot unavailable or task unsuitable for worktree, fall back to
134
- per-project serial lock.
115
+ Dispatch-lock fallback in dispatch.js (tryAcquireDispatchLock) — when a
116
+ worktree is unavailable or the task is unsuitable for one, fall back
117
+ to the per-project serial lock.
135
118
  violation_detection: |
136
119
  Two concurrent non-worktree dispatches writing the same project
137
120
  state (e.g., task-api moves). Lock contention visible in logs.
138
- source: ".formation/tools/formations.js#L773 (parallel semaphore + serial dispatch-lock fallback)"
121
+ source: ".formation/tools/dispatch.js (tryAcquireDispatchLock) + .formation/tools/utils.js (dispatchLockPath)"
139
122
 
140
123
  # ============================================================================
141
124
  # CATEGORY: cognitive
@@ -36,7 +36,7 @@
36
36
  身份: "Ansuz = 產品全生命週期引擎(PM + Lead 合一)"
37
37
  行為規則:
38
38
  ❌ 不 invoke 其他 Skill — 不切換身份
39
- ✅ 需要執行時 → Formation 系統 dispatch(Task tool + headless.js)
39
+ ✅ 需要執行時 → Agent tool subagent dispatch(dispatch.js --dispatch-mode=subagent)
40
40
  ✅ 本體負責:需求理解、任務設計、陣型判斷、調度、追蹤、回報
41
41
  ❌ 不親自寫程式碼 — 只做決策和調度
42
42
  ```
@@ -44,7 +44,7 @@
44
44
  **工作模式**:
45
45
  - **Ansuz = 指揮官 + 調度者**,不是執行者
46
46
  - 所有實作交給 Formation 系統的 agent(developer, backend, designer...)
47
- - 通過 `.formation/tools/headless.js` 啟動獨立 Claude CLI 進程執行任務
47
+ - 通過 `.formation/tools/dispatch.js` 組裝 prompt,再由 Agent tool subagent 執行任務
48
48
 
49
49
  ### 2. 事件驅動生命週期(7 Phases)
50
50
 
@@ -146,7 +146,7 @@ fi
146
146
 
147
147
  # 子專案範例
148
148
  node {CORE}/.collaboration/core/task-api.js --project-path {PROJECT_PATH} list
149
- node {CORE}/.formation/tools/headless.js --project-path {PROJECT_PATH} --skill=do --task=TASK_XXX
149
+ node {CORE}/.formation/tools/dispatch.js --project-path {PROJECT_PATH} --skill=do --task=TASK_XXX --dispatch-mode=subagent
150
150
  node {CORE}/.formation/tools/post-complete.js --task=TASK_XXX --project-path={PROJECT_PATH}
151
151
  ```
152
152
 
@@ -154,48 +154,26 @@ node {CORE}/.formation/tools/post-complete.js --task=TASK_XXX --project-path={PR
154
154
 
155
155
  ### 2. Dispatch 執行機制
156
156
 
157
- **工具**: `.formation/tools/headless.js`
158
- **方法**: Bash tool → `node headless.js` → 啟動獨立 Claude CLI 進程
157
+ **工具**: `.formation/tools/dispatch.js`
158
+ **方法**: Bash tool → `node dispatch.js --dispatch-mode=subagent` → 取回 `promptText` + `agentParams` → Agent tool 開 subagent
159
+ (headless CLI 進程、squad/legion 陣型已退役;只剩 unit/solo skill。)
159
160
 
160
161
  **CLI 命令格式**:
161
162
 
162
163
  ```bash
163
164
  # Unit 陣型
164
- node .formation/tools/headless.js --skill=do --task=TASK_XXX
165
+ node .formation/tools/dispatch.js --skill=do --task=TASK_XXX --dispatch-mode=subagent
165
166
 
166
167
  # Solo 陣型
167
- node .formation/tools/headless.js --formation=solo --skill=strategy --task=TASK_XXX
168
-
169
- # Squad 陣型
170
- node .formation/tools/headless.js --formation=squad --skill=blitz --task=TASK_XXX
171
-
172
- # Legion 陣型
173
- node .formation/tools/headless.js --formation=legion --skill=forge --task=TASK_XXX
174
- ```
175
-
176
- **並行模式**:
177
-
178
- ```javascript
179
- // 單任務(單一 Bash)
180
- Bash(run_in_background: true): node headless.js --task=TASK_148
181
-
182
- // 並行多任務(多個 Bash 同時啟動)
183
- Bash(run_in_background: true): node headless.js --task=TASK_148
184
- Bash(run_in_background: true): node headless.js --task=TASK_149
185
- Bash(run_in_background: true): node headless.js --task=TASK_150
186
-
187
- // 有依賴(順序執行)
188
- Bash: node headless.js --task=TASK_147
189
- // 等待完成後
190
- Bash: node headless.js --task=TASK_148
168
+ node .formation/tools/dispatch.js --formation=solo --skill=strategy --task=TASK_XXX --dispatch-mode=subagent
191
169
  ```
192
170
 
193
171
  **關鍵規則**:
194
172
  - ✅ 所有調度必須經過 `.collaboration` 任務系統
195
- - ✅ 流程:create → claim → dispatch(headless.js) → complete
196
- - ✅ 用 Bash tool 呼叫 headless.js,`run_in_background=true` 實現並行
173
+ - ✅ 流程:create → dispatch.js(subagent) → Agent tool → complete
174
+ - ✅ 有依賴 → 前一個完成再 dispatch 下一個;同一 project 序列化
197
175
  - ❌ 不跳過任務系統直接 dispatch
198
- - ❌ 不用 Task tool 開 subagent 繞過 Formation(Formation 有 prompt 組裝、log、principles 注入)
176
+ - ❌ 不手寫 subagent prompt — 用 dispatch.js 組裝(prompt 組裝、principles 注入)
199
177
 
200
178
  ### 3. Scope 安全保證
201
179
 
@@ -254,7 +232,7 @@ rigor: "medium" # low | medium | high
254
232
  dependencies: ["TASK_147"] # 真依賴(B 的輸入 = A 的輸出)
255
233
  scope_paths: # 修改的檔案/目錄(用於衝突檢測)
256
234
  - "CLAUDE.md"
257
- - ".formation/tools/runner.js"
235
+ - ".formation/tools/dispatch.js"
258
236
  requirements:
259
237
  - "新增 Lead Agent 開發流程章節"
260
238
  - "引用 .rule/pipeline/task-execution.yaml"
@@ -402,9 +380,8 @@ Step 2: 展示 Banner
402
380
  └── Ansuz.md # 斜線指令觸發點
403
381
 
404
382
  .formation/tools/
405
- ├── headless.js # Formation 調度核心
383
+ ├── dispatch.js # Formation 調度核心(subagent)
406
384
  ├── prompt.js # Prompt 組裝 + principles 注入
407
- ├── runner.js # 執行引擎
408
385
  ├── post-complete.js # 任務完成後自動化
409
386
  └── logger.js # 日誌系統
410
387
 
@@ -439,11 +416,9 @@ Phase 3: 建立任務
439
416
  - node task-api.js create ... (x3 任務)
440
417
 
441
418
  Phase 4: 陣型判斷 + Dispatch
442
- - 判斷:3 個獨立任務 → squad/blitz
419
+ - 判斷:3 個任務 → 各自 unit/do,序列化 dispatch
443
420
  - Dispatch:
444
- Bash(bg): node headless.js --formation=squad --skill=blitz --task=TASK_151
445
- Bash(bg): node headless.js --formation=squad --skill=blitz --task=TASK_152
446
- Bash(bg): node headless.js --formation=squad --skill=blitz --task=TASK_153
421
+ node dispatch.js --skill=do --task=TASK_151 --dispatch-mode=subagent(完成後再下一個)
447
422
 
448
423
  Phase 5: 結果處理
449
424
  - 驗證:檔案變更 + Git commit + 驗收標準
@@ -531,7 +506,7 @@ Phase 7: 回報
531
506
  ```yaml
532
507
  無衝突(可並行):
533
508
  TASK_A: scope_paths: ["CLAUDE.md"]
534
- TASK_B: scope_paths: [".formation/tools/runner.js"]
509
+ TASK_B: scope_paths: [".formation/tools/dispatch.js"]
535
510
 
536
511
  有衝突(必須順序執行):
537
512
  TASK_A: scope_paths: [".rule/pipeline/"]
@@ -601,7 +576,7 @@ Phase 7: 回報
601
576
  | 能力定義 | `.specialist/Ansuz/capability.yaml` | 需求分析、任務設計、陣型判斷等能力 |
602
577
  | 執行模式 | `.specialist/Ansuz/workflow.yaml` | 事件驅動 7 phases 工作流程 |
603
578
  | 共享詞彙 | `.specialist/Ansuz/knowledge.yaml` | Formation、任務系統、Agent 類型 |
604
- | Formation 系統 | `.formation/tools/` | headless.js, prompt.js, runner.js... |
579
+ | Formation 系統 | `.formation/tools/` | dispatch.js, prompt.js, post-complete.js... |
605
580
  | 任務系統 | `.collaboration/core/task-api.js` | 任務 API |
606
581
  | CLAUDE.md | `CLAUDE.md` | 專案級指令與工作流程 |
607
582
 
@@ -91,24 +91,17 @@ capabilities:
91
91
  # ─── 調度執行 ───
92
92
  # §dispatch
93
93
  dispatch:
94
- description: "使用 Formation 系統調度 agent 執行任務"
95
- tool: ".formation/tools/headless.js"
96
- method: "Bash tool → node headless.js → 啟動獨立 claude CLI 進程"
94
+ description: "使用 Formation prompt 組裝 + Agent tool subagent 執行任務(headless CLI 進程已退役)"
95
+ tool: ".formation/tools/dispatch.js"
96
+ method: "Bash tool → node dispatch.js --dispatch-mode=subagent → 取回 assembled prompt → Agent tool 開 subagent"
97
97
  rules:
98
98
  - "✅ 所有調度必須經過 .collaboration 任務系統"
99
- - "✅ create → claim → dispatch(headless.js) → complete"
100
- - "✅ 用 Bash tool 呼叫 headless.js,run_in_background=true 實現並行"
101
- - "⚠️ 同一 project 的 dispatch 必須序列化 — 一個 task 完成後再 dispatch 下一個"
102
- - "⚠️ 原因:多個 agent 共用同一 git working directory,並行會 git 衝突 + agent stall"
103
- - "⚠️ 有依賴鏈時:按依賴順序逐一 dispatch,等 TaskOutput/poll 確認前一個完成"
99
+ - "✅ create → dispatch.js(subagent) → Agent tool → complete"
100
+ - "⚠️ 同一 project 的 dispatch 必須序列化 — 多個 agent 共用同一 git working directory,並行會衝突"
101
+ - "⚠️ 有依賴鏈時:按依賴順序逐一 dispatch,確認前一個完成"
104
102
  - "❌ 不跳過任務系統直接 dispatch"
105
- - "❌ 不用 Task tool 開 subagent 繞過 Formation — Formation 有 prompt 組裝、log、principles 注入"
106
- - "❌ 禁止同時 dispatch 多個 task 到同一 project(即使它們沒有依賴關係)"
107
103
  commands:
108
- unit: "node .formation/tools/headless.js --skill=do --task=TASK_XXX"
109
- solo: "node .formation/tools/headless.js --formation=solo --skill=strategy --task=TASK_XXX"
110
- squad: "node .formation/tools/headless.js --formation=squad --skill=blitz --task=TASK_XXX"
111
- legion: "node .formation/tools/headless.js --formation=legion --skill=forge --task=TASK_XXX"
104
+ unit: "node .formation/tools/dispatch.js --skill=do --task=TASK_XXX --dispatch-mode=subagent"
112
105
 
113
106
  # ─── 結果追蹤 ───
114
107
  # §result_tracking
@@ -9,20 +9,13 @@ size_kb: 0.98
9
9
  evolution_status: reference
10
10
  ---
11
11
  formation_quick_ref:
12
- principle: "Lightest viable formation: unit > solo > squad > legion"
12
+ principle: "Only unit/solo skills are dispatchable (squad/legion retired); dispatch = Agent-tool subagent"
13
13
  unit:
14
14
  when: "Clear goal, single scope, direct execution"
15
- command: "node .formation/tools/headless.js --skill=do --task=TASK_XXX"
15
+ command: "node .formation/tools/dispatch.js --skill=do --task=TASK_XXX --dispatch-mode=subagent"
16
16
  solo:
17
17
  when: "Multi-perspective analysis needed"
18
- command: "node .formation/tools/headless.js --formation=solo --skill=strategy --task=TASK_XXX"
19
- squad:
20
- when: "Independent parallel subtasks possible"
21
- command: "node .formation/tools/headless.js --formation=squad --skill=blitz --task=TASK_XXX"
22
- legion:
23
- when: "Sequential dependencies or real-time coordination required"
24
- command: "node .formation/tools/headless.js --formation=legion --skill=forge --task=TASK_XXX"
18
+ command: "node .formation/tools/dispatch.js --formation=solo --skill=strategy --task=TASK_XXX --dispatch-mode=subagent"
25
19
  dispatch_rules:
26
- - "✅ Always background=true for parallel dispatch (Bash run_in_background=true)"
27
20
  - "⚠️ Same project: serialize dispatches — wait for each to complete (git conflicts)"
28
- - "✅ create → claim → dispatch(headless.js) → complete lifecycle"
21
+ - "✅ create → dispatch.js(subagent) → Agent tool → complete lifecycle"
@@ -54,13 +54,12 @@ mandates:
54
54
  - "Project pinning: when boot banner shows a `[pin:...]` line or session was started with --project-path, EVERY MCP call to synapse_*/hippocampus_*/cortex_* that accepts a projectPath param MUST pass projectPath=<pin>; every specialist tool CLI call (task-api, phase-gate, dispatch) MUST pass --project-path <pin>. Missing projectPath leaks memory into the wrong project graph."
55
55
  - "Verification policy (PARTIAL/UNVERIFIED/FIX-keeps-original/verified_uncommitted vs committed_unmerged vs merged): .specialist/Ansuz/VERIFY.md, loaded on demand at Phase 9."
56
56
  - "Landing mechanics for a verified card (branch check -> Rust/node suites -> optional negative control -> merge -> task-api complete -> mark-landed -> optional backend-restart wait): run .specialist/Ansuz/tools/verify-and-land.js instead of doing the same 10-20 tool calls by hand; it only executes, judgement (what to verify, scope) stays with you."
57
- - "WISHING WELL (every incarnation, TASK_1997/TASK_2040): file a wish with kenaz_wish_create — BEFORE working around the problem — whenever the Kenaz app itself blocks or degrades the work: a Kenaz tool/route fails or returns wrong data, a capability you needed is missing, a UX confusion cost you a turn, or a task ends failed/blocked because of the app. wish_type=bug|feature|question, one-line repro + what you expected, task_id when there is one. SCOPE (required on every wish): scope 'kenaz' — the Kenaz app/tools/dispatch itself blocked or degraded the work; scope 'project' — an idea or problem specific to the project being worked on (still sent; the mother may adopt it globally when it benefits Kenaz overall). FEATURE PROPOSALS: when the work reveals a capability Kenaz itself lacks that would help beyond this one task (not only blockers), you may file kenaz_wish_create with wish_type=feature, scope=kenaz. Content, in this order, within ~3000 chars: need (what you were trying to do), why current Kenaz falls short (evidence), proposed shape, who else benefits. Propose only - do not build Kenaz-core changes from a branch instance; the mother decides whether and how. MOTHER-SIDE TRIAGE: on the mother machine, field-report cards tagged needs-triage (blockers, project-scope wishes, and feature proposals alike) are judged at boot — adopt (rewrite as a global card stating the Kenaz-wide benefit, cancel the original with a pointer) or decline (cancel with a one-line reason); a feature proposal may be reshaped rather than built as proposed; never implement a project-specific ask verbatim in core. EXAMPLES — blocker: scope=kenaz, wish_type=bug, 'kenaz_dispatch timed out on a cross-project worktree with no partial log, TASK_2011'. feature proposal: scope=kenaz, wish_type=feature, 'need: retry one failed sub-task without rerunning the whole formation; current Kenaz only retries the full formation; shape: kenaz_dispatch --retry-task <id>; benefits every multi-task formation'. project-specific: scope=project, wish_type=feature, 'this project needs a CSV importer for onboarding — project-specific, flagging in case it generalizes'. At session close, anything unfinished BECAUSE of the app also becomes a wish. This is the channel that feeds the mother from every branch (TASK_1994); a session that hit an app problem and filed no wish has skipped a mandate, not saved time. Boot banner shows the open-wish count (and needs-triage count when >0) so you can see the channel is live."
57
+ - "WISHING WELL (every incarnation, TASK_1997/TASK_2040): file a wish with kenaz_wish_create — BEFORE working around the problem — whenever the Kenaz app itself blocks or degrades the work: a Kenaz tool/route fails or returns wrong data, a capability you needed is missing, a UX confusion cost you a turn, or a task ends failed/blocked because of the app. wish_type=bug|feature|question, one-line repro + what you expected, task_id when there is one. SCOPE (required on every wish): scope 'kenaz' — the Kenaz app/tools/dispatch itself blocked or degraded the work; scope 'project' — an idea or problem specific to the project being worked on (still sent; the mother may adopt it globally when it benefits Kenaz overall). FEATURE PROPOSALS: when the work reveals a capability Kenaz itself lacks that would help beyond this one task (not only blockers), you may file kenaz_wish_create with wish_type=feature, scope=kenaz. Content, in this order, within ~3000 chars: need (what you were trying to do), why current Kenaz falls short (evidence), proposed shape, who else benefits. Propose only - do not build Kenaz-core changes from a branch instance; the mother decides whether and how. MOTHER-SIDE TRIAGE: on the mother machine, field-report cards tagged needs-triage (blockers, project-scope wishes, and feature proposals alike) are judged at boot — adopt (rewrite as a global card stating the Kenaz-wide benefit, cancel the original with a pointer) or decline (cancel with a one-line reason); a feature proposal may be reshaped rather than built as proposed; never implement a project-specific ask verbatim in core. EXAMPLES — blocker: scope=kenaz, wish_type=bug, 'dispatch.js timed out on a cross-project worktree with no partial log, TASK_2011'. feature proposal: scope=kenaz, wish_type=feature, 'need: retry one failed sub-task without rerunning the whole formation; current Kenaz only retries the full formation; shape: dispatch.js --retry-task <id>; benefits every multi-task formation'. project-specific: scope=project, wish_type=feature, 'this project needs a CSV importer for onboarding — project-specific, flagging in case it generalizes'. At session close, anything unfinished BECAUSE of the app also becomes a wish. This is the channel that feeds the mother from every branch (TASK_1994); a session that hit an app problem and filed no wish has skipped a mandate, not saved time. Boot banner shows the open-wish count (and needs-triage count when >0) so you can see the channel is live."
58
58
  dispatch_routing:
59
59
  paths:
60
60
  subagent: "Agent tool within interactive session — stays on subscription billing, results return directly, no log polling. Use: short/medium tasks (rigor:low/medium, same-project or trivial scope)."
61
- formation: "kenaz_dispatch / claude -p — independent process, own log, subscription billing (the June 15 credit-pool split was rescinded, user 2026-10-02). Use: long tasks (rigor:high, multi-hour), cross-project (different cwd needed), or when no interactive session."
62
- how: "Run `node .formation/tools/dispatch.js --skill=do --task=TASK_XXX --dispatch-mode=subagent` (include the project pin when present). Read the assembled prompt from top-level promptText and pass it as the Agent tool prompt alongside agentParams. Top-level isolation is orchestration metadata, NOT an Agent tool argument: cross-project worktreePath specifies the target cwd; serialLock specifies required serialization and release. agentParams.isolation is present only for the native same-project worktree route. agentParams.model is ALREADY translated to the Agent tool enum (sonnet|opus|haiku|fable, TASK_1815) — pass it verbatim to a compatible Agent tool, do NOT re-translate or improvise. LIMIT: the Agent tool expresses family names ONLY, so dashboard version/speed picks (claude-opus-5[1m] vs claude-opus-4-8[1m], [1m] modifiers) all degrade to the family name; the payload's modelTranslation {requested, agentToolModel, granularityLoss} shows exactly what was lost — if the exact version matters, use the formation path (kenaz_dispatch → headless.js --dispatch-model accepts full model ids). Unmappable dispatchModel values make dispatch.js exit 1 loudly; never substitute a model silently. For formation path, use kenaz_dispatch MCP as before."
63
- mandate: "ALWAYS evaluate both paths before dispatching. Check .kenaz/dashboard-models.json dispatchMode for the user override (set from the Dashboard Model Selection panel > Dispatch Routing, persisted by dashboard_settings.rs since TASK_1814): 'formation' or 'subagent' = hard override, route ALL dispatches through that path; 'auto' = no forced route, apply the heuristic (subagent when within interactive session unless task requires cross-project cwd or is rigor:high, else formation); key missing = dispatch.js falls back to 'formation' (TASK_1813 contract) — treat missing the same as no override and apply the heuristic at the Ansuz level."
61
+ how: "Run `node .formation/tools/dispatch.js --skill=do --task=TASK_XXX --dispatch-mode=subagent` (include the project pin when present). Read the assembled prompt from top-level promptText and pass it as the Agent tool prompt alongside agentParams. Top-level isolation is orchestration metadata, NOT an Agent tool argument: cross-project worktreePath specifies the target cwd; serialLock specifies required serialization and release. agentParams.isolation is present only for the native same-project worktree route. agentParams.model is ALREADY translated to the Agent tool enum (sonnet|opus|haiku|fable, TASK_1815) — pass it verbatim to a compatible Agent tool, do NOT re-translate or improvise. LIMIT: the Agent tool expresses family names ONLY, so dashboard version/speed picks (claude-opus-5[1m] vs claude-opus-4-8[1m], [1m] modifiers) all degrade to the family name; the payload's modelTranslation {requested, agentToolModel, granularityLoss} shows exactly what was lost. Unmappable dispatchModel values make dispatch.js exit 1 loudly; never substitute a model silently."
62
+ mandate: "Subagent is the only dispatch path (Formation headless/kenaz_dispatch retired, TASK_2359/2401/2402). dispatchMode in .kenaz/dashboard-models.json no longer selects a route."
64
63
  exo_cortex:
65
64
  opening_move: synapse_recall + kenaz_lsp_callers
66
65
  anti_pattern: "Bash grep/rg/find for source archaeology — prefer Grep/Glob (and kenaz_lsp_symbols for symbols); the hook nudge is downstream of you knowing the tool exists"
@@ -339,7 +339,7 @@ cross_model:
339
339
  - "ME-001 governance (immutable rules cannot be deleted) must hold"
340
340
  - "AskUserQuestion tool must be available and used for all user-response questions"
341
341
  - "Phase sequence 0-11 must be respected; phase-gate.js must be callable"
342
- - "Formation dispatch must work (headless.js must be callable)"
342
+ - "Subagent dispatch must work (.formation/tools/dispatch.js --dispatch-mode=subagent must be callable)"
343
343
  - "Task-api.js must be callable for task lifecycle management"
344
344
 
345
345
  acceptable_substrate_drift:
@@ -34,9 +34,8 @@ formation:
34
34
  - "Squad ≠ 開會。Squad = 多個獨立 subagent 並行,零溝通"
35
35
  - "Legion ≠ 大號 Squad。Legion = 持久化團隊,成員間即時通訊"
36
36
  key_files:
37
- headless: ".formation/tools/headless.js"
37
+ dispatch: ".formation/tools/dispatch.js"
38
38
  prompt: ".formation/tools/prompt.js"
39
- runner: ".formation/tools/runner.js"
40
39
  logs: ".formation/logs/{formation}/"
41
40
  post_complete: ".formation/tools/post-complete.js"
42
41
  research_notes:
@@ -49,12 +48,10 @@ formation:
49
48
  - "Better mapping: represent agent-role reliability and coordination cost as factorised hidden states, then evaluate a whole formation against task context."
50
49
  - "Keep a collective dispatch objective separate from per-agent or per-symbol EFE because ensemble-level EFE is not guaranteed to be aggregate-optimal."
51
50
  dispatch_mechanism:
52
- how: "Bash tool → node headless.js → runner.js → spawn claude CLI 獨立進程"
53
- why: "headless.js 統一處理 prompt 組裝(prompt.js)、principles 注入、log 管理"
54
- anti_pattern: "❌ 禁止用 Task tool 直接開 subagent — 繞過 Formation 會失去 prompt 組裝和 log"
55
- parallel: "多個 Bash(run_in_background=true) 同時啟動 → 真正的進程級並行"
56
- max_parallel: 5 # 同時最多 5 個 headless 進程,超過排隊等空位
57
- monitoring: "讀 .formation/logs/{formation}/ 下的 log 確認結果"
51
+ how: "Bash tool → node dispatch.js --dispatch-mode=subagent → Agent tool subagent"
52
+ why: "dispatch.js 統一處理 prompt 組裝(prompt.js)、principles 注入"
53
+ anti_pattern: "❌ 不手寫 subagent prompt — 用 dispatch.js 組裝"
54
+ monitoring: "Agent tool 結果直接返回"
58
55
 
59
56
  # ═══════════════════════════════════════
60
57
  # 任務系統
@@ -89,10 +86,10 @@ task_system:
89
86
  project_yaml: ".kenaz/project.yaml"
90
87
  rule: |
91
88
  子專案的任務存在 .kenaz/tasks/,不是 .collaboration/tasks/。
92
- 所有 task-api.js、headless.js、post-complete.js 呼叫都必須加 --project-path。
89
+ 所有 task-api.js、dispatch.js、post-complete.js 呼叫都必須加 --project-path。
93
90
  cli_pattern:
94
91
  task_api: "node {CORE}/.collaboration/core/task-api.js --project-path {PROJECT_PATH} <command>"
95
- headless: "node {CORE}/.formation/tools/headless.js --project-path {PROJECT_PATH} --skill=do --task=TASK_XXX"
92
+ dispatch: "node {CORE}/.formation/tools/dispatch.js --project-path {PROJECT_PATH} --skill=do --task=TASK_XXX --dispatch-mode=subagent"
96
93
  post_complete: "node {CORE}/.formation/tools/post-complete.js --task=TASK_XXX --project-path={PROJECT_PATH}"
97
94
 
98
95
  # ═══════════════════════════════════════
@@ -35,7 +35,7 @@ allow:
35
35
  - "讀取專案內所有原始碼"
36
36
  - "讀寫 specialist/Ansuz/ 下所有檔案"
37
37
  - "使用 .collaboration/core/task-api.js 管理任務"
38
- - "使用 .formation/tools/headless.js 調度執行"
38
+ - "使用 .formation/tools/dispatch.js --dispatch-mode=subagent 調度執行"
39
39
  - "使用 .formation/tools/post-complete.js 完成後自動化"
40
40
  - "執行 git 操作(建立分支、commit、merge 到用戶分支)"
41
41
 
@@ -51,7 +51,7 @@ identity:
51
51
  symbol: "ᚨ — 神諭符文:理解意圖,傳遞給執行層"
52
52
  rules:
53
53
  - "❌ 不 invoke 其他 Skill — 不切換身份"
54
- - "✅ 需要執行時 → 用 Formation 系統 dispatch(Task tool + headless.js)"
54
+ - "✅ 需要執行時 → 用 Agent tool subagent dispatch(dispatch.js --dispatch-mode=subagent)"
55
55
  - "✅ Ansuz 本體負責:需求理解、任務設計、陣型判斷、調度、追蹤、回報"
56
56
  - "❌ 不親自寫程式碼 — 只做決策和調度"
57
57
  - "✅ Task-First — 預設先建立任務卡再 dispatch,除非是極簡單的單檔修改(< 20 行)"
@@ -147,6 +147,8 @@ function buildPlan(config) {
147
147
  plan.push({ name: 'negative-control', kind: 'neg' });
148
148
  }
149
149
  if (!config.noMerge) {
150
+ // TASK_2400: ARCH-001 file-size gate, cheap and deterministic, so before the heavy checks.
151
+ plan.push({ name: 'file-size', kind: 'file-size' });
150
152
  // TASK_2176: the authoritative, serialized heavyweight check runs right
151
153
  // before merge, on the main tree (config.repo) -- gates the merge that
152
154
  // follows it, same as the worktree-side negative control gates it above.
@@ -759,6 +761,24 @@ function coreToolPath(config, rel) {
759
761
  return path.join(__dirname, '..', '..', '..', rel);
760
762
  }
761
763
 
764
+ /**
765
+ * TASK_2400: ARCH-001 gate. Runs the core's check-file-size.js on branch vs merge-base
766
+ * (--repo = the project, so a user project uses its own .rule yaml or the built-in
767
+ * defaults). BLOCK -> step fails, merge never runs; WARN lines go into the step summary.
768
+ */
769
+ function stepFileSize(config, deps, ctx) {
770
+ const script = coreToolPath(config, path.join('dev', 'scripts', 'check-file-size.js'));
771
+ if (!fs.existsSync(script)) return { ok: true, summary: 'check-file-size.js not found in core, skipped', log: '' };
772
+ const base = git(deps, config.repo, ['merge-base', 'HEAD', ctx.branch]);
773
+ const res = deps.spawnSync('node', [script, '--base', base, '--head', ctx.branch, '--repo', config.repo], { encoding: 'utf8' });
774
+ const output = (res.stdout || '') + (res.stderr || '');
775
+ const found = output.split('\n').filter((l) => /ARCH-001 (BLOCK|WARN)/.test(l)).map((l) => l.trim());
776
+ if (res.status === 0) {
777
+ return { ok: true, summary: found.length ? `${found.length} warning(s): ${found.join(' | ')}`.slice(0, 600) : 'within thresholds', log: output };
778
+ }
779
+ return { ok: false, summary: `ARCH-001 blocked (exit ${res.status}): ${found.join(' | ') || output.trim()}`.slice(0, 800), log: output };
780
+ }
781
+
762
782
  function writeLandingInfo(config, deps, ctx) {
763
783
  if (!ctx || !ctx.postMergeHead) return; // no merge landed (--no-merge etc.) -- nothing to record
764
784
  const shortSha = ctx.postMergeHead.slice(0, 8);
@@ -845,6 +865,7 @@ async function executeStep(stepDef, config, deps, ctx) {
845
865
  case 'rust': return stepRust(config, deps, ctx, stepDef.filter);
846
866
  case 'node': return stepNode(config, deps, ctx, stepDef.file);
847
867
  case 'neg': return stepNegativeControl(config, deps, ctx);
868
+ case 'file-size': return stepFileSize(config, deps, ctx);
848
869
  case 'verify-offline': return stepVerifyOffline(config, deps);
849
870
  case 'merge': return stepMerge(config, deps, ctx);
850
871
  case 'task-complete': return stepTaskComplete(config, deps, ctx);
@@ -1000,6 +1021,7 @@ module.exports = {
1000
1021
  latestStartedAtAfter,
1001
1022
  resolveRepoFromWorktree,
1002
1023
  coreToolPath,
1024
+ stepFileSize,
1003
1025
  assertRepoOwnsWorktree,
1004
1026
  resolveRepo,
1005
1027
  UsageError,
@@ -33,6 +33,7 @@ const {
33
33
  DEFAULT_RUST_MAX_S,
34
34
  resolveVerifyOfflineCmd,
35
35
  stepVerifyOffline,
36
+ stepFileSize,
36
37
  } = require('./verify-and-land.js');
37
38
 
38
39
  // Same shared list verify-and-land.js reads (TASK_2069) — tests assert
@@ -273,6 +274,7 @@ test('buildPlan: fixed step order for a full config (default warm route — no c
273
274
  'rust:foo',
274
275
  'node:t.js',
275
276
  'negative-control',
277
+ 'file-size',
276
278
  'merge',
277
279
  'task-complete',
278
280
  'mark-landed',
@@ -362,6 +364,8 @@ test('success path: runs steps in order (rust via the warm rust-verify-worktree.
362
364
  return ok('test result: ok. 5 passed; 0 failed\n');
363
365
  }
364
366
  if (cmd === 'node' && args[0] === '--test') return ok('ℹ pass 2\nℹ fail 0\n');
367
+ if (cmd === 'git' && args[2] === 'merge-base' && args[3] === 'HEAD') return ok('basesha\n');
368
+ if (cmd === 'node' && args[0].includes('check-file-size.js')) return ok('');
365
369
  if (cmd === 'git' && args[2] === 'merge' && args.includes('--no-ff')) {
366
370
  mergeCount++;
367
371
  return ok('Merge made by the recursive strategy.\n');
@@ -386,7 +390,7 @@ test('success path: runs steps in order (rust via the warm rust-verify-worktree.
386
390
  // copies its own resources into the fixed verify worktree.
387
391
  assert.deepEqual(
388
392
  result.steps.map(s => s.name),
389
- ['preflight', 'rust:foo', 'node:test/a.test.js', 'merge', 'task-complete', 'mark-landed']
393
+ ['preflight', 'rust:foo', 'node:test/a.test.js', 'file-size', 'merge', 'task-complete', 'mark-landed']
390
394
  );
391
395
  assert.ok(result.steps.every(s => s.ok === true));
392
396
  assert.equal(mergeCount, 1, 'merge must run exactly once');
@@ -416,6 +420,8 @@ test('TASK_2142 R2: task-complete bakes landed commit sha + verification summary
416
420
  if (cmd === 'git' && args[2] === 'status') return ok('');
417
421
  if (cmd === 'git' && args[2] === 'rev-list') return ok('1\n');
418
422
  if (cmd === 'node' && args[0].includes('rust-verify-worktree.js') && args[1] === 'run') return ok('test result: ok. 3 passed; 0 failed\n');
423
+ if (cmd === 'git' && args[2] === 'merge-base' && args[3] === 'HEAD') return ok('basesha\n');
424
+ if (cmd === 'node' && args[0].includes('check-file-size.js')) return ok('');
419
425
  if (cmd === 'git' && args[2] === 'merge' && args.includes('--no-ff')) return ok('Merge made by the recursive strategy.\n');
420
426
  if (cmd === 'git' && args[2] === 'rev-parse' && args[3] === 'HEAD') {
421
427
  headCount++;
@@ -772,7 +778,7 @@ test('dry-run: resolves/validates --repo via read-only git (same guard as a real
772
778
  assert.equal(result.repo, config.repo, 'dry-run output includes the resolved repo path');
773
779
  assert.deepEqual(
774
780
  result.steps.map(s => s.name),
775
- ['preflight', 'rust:foo', 'node:t.js', 'negative-control', 'merge', 'task-complete', 'mark-landed', 'wait-backend', 'live']
781
+ ['preflight', 'rust:foo', 'node:t.js', 'negative-control', 'file-size', 'merge', 'task-complete', 'mark-landed', 'wait-backend', 'live']
776
782
  );
777
783
  assert.ok(result.steps.every(s => s.planned === true));
778
784
  });
@@ -923,6 +929,8 @@ test('merge verification: "Already up to date" (unchanged HEAD) fails the merge
923
929
  if (cmd === 'git' && args[2] === 'status') return ok('');
924
930
  if (cmd === 'git' && args[2] === 'rev-list') return ok('1\n');
925
931
  if (cmd === 'node' && args[0].includes('rust-verify-worktree.js') && args[1] === 'run') return ok('test result: ok. 1 passed; 0 failed\n');
932
+ if (cmd === 'git' && args[2] === 'merge-base' && args[3] === 'HEAD') return ok('basesha\n');
933
+ if (cmd === 'node' && args[0].includes('check-file-size.js')) return ok('');
926
934
  if (cmd === 'git' && args[2] === 'merge' && args.includes('--no-ff')) return ok('Already up to date.\n');
927
935
  // HEAD is the SAME before and after — this is the exact bug that shipped.
928
936
  if (cmd === 'git' && args[2] === 'rev-parse' && args[3] === 'HEAD') return ok('deadbeef\n');
@@ -934,7 +942,7 @@ test('merge verification: "Already up to date" (unchanged HEAD) fails the merge
934
942
  const result = await runVerifyAndLand(config, deps);
935
943
  assert.equal(result.ok, false);
936
944
  assert.equal(result.failed_step, 'merge');
937
- assert.deepEqual(result.steps.map(s => s.name), ['preflight', 'rust:foo', 'merge']);
945
+ assert.deepEqual(result.steps.map(s => s.name), ['preflight', 'rust:foo', 'file-size', 'merge']);
938
946
  assert.match(result.steps[result.steps.length - 1].summary, /unchanged|did not land/);
939
947
  assert.equal(taskCompleteCalled, false, 'task-api complete must never run after a merge that did not actually land');
940
948
  });
@@ -958,7 +966,7 @@ test('resolveVerifyOfflineCmd: project.yaml without the key -> null', () => {
958
966
 
959
967
  test('buildPlan: verify-offline step is included only when verifyOfflineCmd is set, placed right before merge', () => {
960
968
  const withCmd = buildPlan(baseConfig({ verifyOfflineCmd: 'node fake-unity.js' }));
961
- assert.deepEqual(withCmd.map(s => s.name), ['preflight', 'verify-offline', 'merge', 'task-complete', 'mark-landed']);
969
+ assert.deepEqual(withCmd.map(s => s.name), ['preflight', 'file-size', 'verify-offline', 'merge', 'task-complete', 'mark-landed']);
962
970
  const withoutCmd = buildPlan(baseConfig({ verifyOfflineCmd: null }));
963
971
  assert.ok(!withoutCmd.some(s => s.kind === 'verify-offline'), 'no key in project.yaml -> no step, unaffected');
964
972
  });
@@ -1028,6 +1036,8 @@ test('runVerifyAndLand: verify_offline failure blocks landing (full pipeline, re
1028
1036
  if (cmd === 'git' && args[2] === 'status') return ok('');
1029
1037
  if (cmd === 'git' && args[2] === 'rev-list') return ok('1\n');
1030
1038
  if (cmd === 'node fake-unity.js --break') return bad(1, '', 'compile error'); // shell:true -- cmd is the whole command string, args is []
1039
+ if (cmd === 'git' && args[2] === 'merge-base' && args[3] === 'HEAD') return ok('basesha\n');
1040
+ if (cmd === 'node' && args[0].includes('check-file-size.js')) return ok('');
1031
1041
  if (cmd === 'git' && args[2] === 'merge') { mergeCalled = true; return ok('merged\n'); }
1032
1042
  throw new Error('unexpected spawnSync in verify_offline-blocks-landing test: ' + JSON.stringify({ cmd, args }));
1033
1043
  },
@@ -1035,7 +1045,7 @@ test('runVerifyAndLand: verify_offline failure blocks landing (full pipeline, re
1035
1045
  const result = await runVerifyAndLand(config, deps);
1036
1046
  assert.equal(result.ok, false);
1037
1047
  assert.equal(result.failed_step, 'verify-offline');
1038
- assert.deepEqual(result.steps.map(s => s.name), ['preflight', 'verify-offline']);
1048
+ assert.deepEqual(result.steps.map(s => s.name), ['preflight', 'file-size', 'verify-offline']);
1039
1049
  assert.equal(mergeCalled, false, 'verify_offline failure must block the merge that follows it');
1040
1050
  });
1041
1051
 
@@ -1097,3 +1107,47 @@ test('coreToolPath: a sub-project repo (only .kenaz/) resolves core tools via pr
1097
1107
  fs.rmSync(tmp, { recursive: true, force: true });
1098
1108
  }
1099
1109
  });
1110
+
1111
+ // ── TASK_2400: ARCH-001 file-size gate (real temp repo, real check-file-size.js) ──
1112
+
1113
+ function makeSizeFixture(extraFiles) {
1114
+ const fsr = require('node:fs');
1115
+ const os = require('node:os');
1116
+ const repo = fsr.mkdtempSync(path.join(os.tmpdir(), 'size-gate-'));
1117
+ const run = (...a) => { const r = realSpawnSync('git', ['-C', repo, ...a], { encoding: 'utf8' }); assert.equal(r.status, 0, r.stderr); return r.stdout; };
1118
+ const body = n => Array.from({ length: n }, (_, i) => `const v${i} = ${i};`).join('\n') + '\n';
1119
+ run('init', '-q', '-b', 'main');
1120
+ run('config', 'user.email', 't@t'); run('config', 'user.name', 't');
1121
+ fsr.writeFileSync(path.join(repo, 'big.js'), body(2100)); // over the 2000 hard limit: grandfathered debt
1122
+ run('add', '-A'); run('commit', '-q', '-m', 'base');
1123
+ run('checkout', '-q', '-b', 'task');
1124
+ fsr.writeFileSync(path.join(repo, 'big.js'), body(2200)); // +100 lines
1125
+ for (const [name, n] of Object.entries(extraFiles || {})) fsr.writeFileSync(path.join(repo, name), body(n));
1126
+ run('add', '-A'); run('commit', '-q', '-m', 'grow');
1127
+ run('checkout', '-q', 'main');
1128
+ return { repo, cleanup: () => fsr.rmSync(repo, { recursive: true, force: true }) };
1129
+ }
1130
+
1131
+ test('stepFileSize: growing an over-limit file without an extraction is refused (BLOCK)', () => {
1132
+ const fx = makeSizeFixture();
1133
+ try {
1134
+ const res = stepFileSize({ repo: fx.repo }, { spawnSync: realSpawnSync }, { branch: 'task' });
1135
+ assert.equal(res.ok, false);
1136
+ assert.match(res.summary, /ARCH-001 blocked/);
1137
+ assert.match(res.summary, /big\.js/);
1138
+ } finally { fx.cleanup(); }
1139
+ });
1140
+
1141
+ test('stepFileSize: same growth paired with a new extracted module passes', () => {
1142
+ const fx = makeSizeFixture({ 'extracted.js': 60 });
1143
+ try {
1144
+ const res = stepFileSize({ repo: fx.repo }, { spawnSync: realSpawnSync }, { branch: 'task' });
1145
+ assert.equal(res.ok, true, res.summary);
1146
+ } finally { fx.cleanup(); }
1147
+ });
1148
+
1149
+ test('buildPlan: file-size runs before merge, and not at all with --no-merge', () => {
1150
+ const names = buildPlan(baseConfig({})).map(s => s.name);
1151
+ assert.deepEqual(names, ['preflight', 'file-size', 'merge', 'task-complete', 'mark-landed']);
1152
+ assert.ok(!buildPlan(baseConfig({ noMerge: true })).some(s => s.name === 'file-size'));
1153
+ });
@@ -94,7 +94,7 @@ phase_0_initialization:
94
94
  1. 檢查當前工作目錄(cwd)是否有 .kenaz/project.yaml
95
95
  2. 如果有 → 子專案模式:設定 PROJECT_PATH = cwd, CORE = project.yaml 的 core_path
96
96
  3. 如果沒有 → Core 模式:PROJECT_PATH 為空,不需要 --project-path
97
- 4. 後續所有 task-api.js / headless.js / post-complete.js 呼叫都根據此結果決定是否加 --project-path
97
+ 4. 後續所有 task-api.js / dispatch.js / post-complete.js 呼叫都根據此結果決定是否加 --project-path
98
98
  check: |
99
99
  # 子專案偵測(在 cwd 執行)
100
100
  if [ -f .kenaz/project.yaml ]; then
@@ -753,19 +753,15 @@ phase_8_dispatch:
753
753
 
754
754
  # ─── Dispatch 執行 ───
755
755
  execution:
756
- tool: ".formation/tools/headless.js"
757
- method: "Bash tool → node headless.js → 啟動獨立 claude CLI 進程"
758
- max_parallel: 5
756
+ tool: ".formation/tools/dispatch.js"
757
+ method: "Bash tool → node dispatch.js --dispatch-mode=subagent → 取回 promptText + agentParams → Agent tool 開 subagent"
759
758
  patterns:
760
- single: "一個任務 → Bash(run_in_background=true): node headless.js --task=TASK_XXX [--project-path ...]"
761
- parallel: "多個無依賴 → 多個 Bash(background) 同時啟動(上限 5,超過排隊)"
759
+ single: "一個任務 → node dispatch.js --task=TASK_XXX --dispatch-mode=subagent [--project-path ...] → Agent tool"
762
760
  sequential: "有依賴 → 前一個完成再 dispatch 下一個"
763
761
  cli:
764
- unit: "node {CORE}/.formation/tools/headless.js --skill=do --task=TASK_XXX [--project-path {PROJECT_PATH}]"
765
- solo: "node {CORE}/.formation/tools/headless.js --formation=solo --skill=strategy --task=TASK_XXX [--project-path {PROJECT_PATH}]"
766
- squad: "node {CORE}/.formation/tools/headless.js --formation=squad --skill=blitz --task=TASK_XXX [--project-path {PROJECT_PATH}]"
767
- legion: "node {CORE}/.formation/tools/headless.js --formation=legion --skill=forge --task=TASK_XXX [--project-path {PROJECT_PATH}]"
768
- anti_pattern: "❌ 禁止用 Task tool 直接開 subagent — 繞過 Formation 會失去 prompt 組裝、principles 注入和 log"
762
+ unit: "node {CORE}/.formation/tools/dispatch.js --skill=do --task=TASK_XXX --dispatch-mode=subagent [--project-path {PROJECT_PATH}]"
763
+ solo: "node {CORE}/.formation/tools/dispatch.js --formation=solo --skill=strategy --task=TASK_XXX --dispatch-mode=subagent [--project-path {PROJECT_PATH}]"
764
+ anti_pattern: "❌ 不手寫 subagent prompt — 用 dispatch.js 組裝(prompt 組裝、principles 注入)"
769
765
 
770
766
  # ─── Scope 安全 ───
771
767
  scope:
@@ -162,7 +162,7 @@ degradation:
162
162
  # ═══════════════════════════════════════
163
163
  invocation:
164
164
  via_formation:
165
- command: "node .formation/tools/headless.js --skill=do --task={FORSETI_TASK_ID} --project-path={path}"
165
+ command: "node .formation/tools/dispatch.js --skill=do --task={FORSETI_TASK_ID} --project-path={path} --dispatch-mode=subagent"
166
166
  context_required:
167
167
  - "task_id: 被驗證的 Dev Task ID(必填)"
168
168
  - "project_path: /absolute/path/to/project(必填)"
@@ -49,7 +49,7 @@ post_complete_verdict: RECORDED | delta: {N.NN|n/a} | reason: {text}
49
49
  | 項目 | 說明 |
50
50
  |------|------|
51
51
  | 路徑 | `.kenaz/health_snapshots/{taskId}_before.json` / `_after.json` |
52
- | 寫入者 | `headless.js`(dispatch 時)或首次運行時由 post-complete.js 寫入 |
52
+ | 寫入者 | 首次運行時由 post-complete.js 寫入 |
53
53
  | 格式 | `{ "score": 82, "grade": "B", "timestamp": "..." }` |
54
54
  | 無快照時 | 保存當前分數為基線 |
55
55