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
@@ -211,7 +211,7 @@ workflow:
211
211
  - "✅ Always write post_complete_verdict: RECORDED to the task YAML when a post-task score exists"
212
212
  pre_task_snapshot:
213
213
  description: "Health baseline captured before Formation dispatch"
214
- written_by: "headless.js at task dispatch time (or post-complete.js as fallback)"
214
+ written_by: "post-complete.js (first run when no baseline exists)"
215
215
  path: ".kenaz/health_snapshots/{task_id}_before.json"
216
216
  schema: "{ score: number, grade: string, timestamp: ISO-8601 }"
217
217
 
@@ -233,7 +233,7 @@ degradation:
233
233
  # ═══════════════════════════════════════
234
234
  invocation:
235
235
  via_formation:
236
- command: "node .formation/tools/headless.js --skill=do --task=HEIMDALL_SCAN --project-path={path}"
236
+ command: "node .formation/tools/dispatch.js --skill=do --task=HEIMDALL_SCAN --project-path={path} --dispatch-mode=subagent"
237
237
  context_required:
238
238
  - "trigger: attention | commit | scheduled | manual"
239
239
  - "project_path: /absolute/path/to/project"
@@ -50,7 +50,7 @@ deny:
50
50
  - "寫程式碼或修改 src/ 下任何檔案"
51
51
  - "修改其他 specialist 的檔案"
52
52
  - "建立 .collaboration/tasks/(那是 Ansuz 的事)"
53
- - "使用 .formation/tools/headless.js 調度任何任務"
53
+ - "使用 .formation/tools/dispatch.js --dispatch-mode=subagent 調度任何任務"
54
54
  - "呼叫 hippocampus_episode_start/append/close"
55
55
  - "修改 .rule/ 下任何規則(安全邊界 — Huginn 不能改自己的規則)"
56
56
  - "建立 proposal(Prefrontal 提案系統已移除 — 只寫 huginn_advisory.jsonl)"
@@ -19,7 +19,7 @@
19
19
 
20
20
  | 模式 | 說明 | 用途 |
21
21
  |------|------|------|
22
- | **assembled**(預設) | 將所有 YAML 內容內嵌到 prompt | headless.js、API、Dashboard |
22
+ | **assembled**(預設) | 將所有 YAML 內容內嵌到 prompt | dispatch.js、API、Dashboard |
23
23
  | **reference** | 回傳檔案讀取指令(同 slash command) | Claude Code 內互動 |
24
24
 
25
25
  ### CLI 用法
@@ -79,7 +79,7 @@ const active = session.getActiveSessions();
79
79
 
80
80
  ### 整合範例
81
81
 
82
- **Formation headless.js**:
82
+ **Formation dispatch.js**:
83
83
  ```js
84
84
  const { buildPrompt } = require('../.specialist/tools/session');
85
85
  const prompt = buildPrompt('Ansuz', { task: taskData, mode: 'assembled' });
@@ -1022,6 +1022,8 @@ async function main() {
1022
1022
  const wishesLine = formatWishesLine(await fetchOpenWishes(mcpBase, WISH_TIMEOUT_MS, IS_PINNED ? BASE : null));
1023
1023
  if (wishesLine) lines.push(wishesLine);
1024
1024
  for (const l of openCardLines()) lines.push(l);
1025
+ const landedLine = landedActiveCardsLine();
1026
+ if (landedLine) lines.push(landedLine);
1025
1027
  const peerLines = await fetchPendingPeerLines(mcpBase, WISH_TIMEOUT_MS, IS_PINNED ? BASE : process.env.CLAUDE_PROJECT_DIR || process.cwd());
1026
1028
  for (const l of peerLines) lines.push(l);
1027
1029
  lines.push('(boot 完成:依 Open cards 提議程;小修正直接做,中型工作開卡派工)');
@@ -1039,6 +1041,24 @@ if (require.main === module) {
1039
1041
  });
1040
1042
  }
1041
1043
 
1044
+ // TASK_2404: active cards whose task branch is already an ancestor of origin/main (local remote-tracking
1045
+ // refs only -- no fetch at boot). Silent when none or when anything fails.
1046
+ function landedActiveCardsLine() {
1047
+ try {
1048
+ const dir = path.join(resolveTasksDir(), 'active');
1049
+ const pick = (txt, key) => {
1050
+ const m = txt.match(new RegExp('^' + key + ':[ \\t]*(.*)$', 'm'));
1051
+ return m ? m[1].trim().replace(/^['"]|['"]$/g, '') : '';
1052
+ };
1053
+ const cards = fs.readdirSync(dir).filter((f) => /\.ya?ml$/.test(f)).map((f) => {
1054
+ const txt = fs.readFileSync(path.join(dir, f), 'utf8');
1055
+ return { id: pick(txt, 'id'), branch: pick(txt, 'branch') || null, keep_open: pick(txt, 'keep_open') === 'true' };
1056
+ }).filter((c) => c.id);
1057
+ const n = require(path.join(__dirname, '..', '..', '.collaboration', 'core', 'reconcile.js')).countLanded(BASE, cards, {});
1058
+ return n > 0 ? '[reconcile] ' + n + ' active cards already landed (task-api reconcile)' : null;
1059
+ } catch { return null; }
1060
+ }
1061
+
1042
1062
  module.exports = {
1043
1063
  isOutsideAnyProject,
1044
1064
  harnessSessionIdFromEnv,
@@ -156,7 +156,7 @@ node .collaboration/core/task-api.js move TASK_194 blocked --force
156
156
  `project_name`);需要更精確的來源(例如具體的 in-app agent 名稱)可用 `--origin <name>` 覆寫。
157
157
 
158
158
  **不是 auto-pipeline**:`createTask()` 一律寫入 `backlog/`(`pending` 狀態),走正常 triage 流程 ——
159
- `submit` 完全不呼叫 dispatch/headless.js,不會自動執行任務。來源內容(title/description 等)視為未信任資料,
159
+ `submit` 完全不呼叫 dispatch.js,不會自動執行任務。來源內容(title/description 等)視為未信任資料,
160
160
  與 field report 相同處理方式(寫入前不會被當作指令執行)。
161
161
 
162
162
  **為什麼不是 HTTP server**:呼叫方本身就是一個能 spawn `node`/`k-cli` 的本地 process;開一個 HTTP server
@@ -1,5 +1,7 @@
1
1
  # Formation Legacy Skills - 全角色定義
2
2
 
3
+ > **[RETIRED 2026-10, TASK_2401/2402/2403]** 本文描述已退役的 headless / legacy-scheduler Formation,僅留作歷史參考。現行路徑:`node .formation/tools/dispatch.js --skill=do --task=TASK_XXX --dispatch-mode=subagent`(見 `.formation/README.md`)。
4
+
3
5
  > **專案位置**: `.formation/legacy/`
4
6
  > **系統類型**: Formation Legacy 陣型專用 Skills
5
7
  > **適用場景**: Legacy Scheduler 自動調度、Formation Lead 任務分派
@@ -1,5 +1,7 @@
1
1
  # Formation Post-Complete - 任務完成後自動化
2
2
 
3
+ > **[RETIRED CALLERS 2026-10, TASK_2401/2402/2403]** 文中的 legacy-scheduler.js / headless.js 呼叫端已移除,僅留作歷史參考;post-complete.js 本身仍存在。現行派工:`node .formation/tools/dispatch.js --skill=do --task=TASK_XXX --dispatch-mode=subagent`。
4
+
3
5
  > **專案位置**: `.formation/tools/post-complete.js`
4
6
  > **系統類型**: Formation Lead 專用工具
5
7
  > **適用場景**: 任務完成後的自動 Archive + Git 分支管理
@@ -1,5 +1,7 @@
1
1
  # Formation System - 多陣型調度系統
2
2
 
3
+ > **[RETIRED 2026-10, TASK_2401/2402/2403]** 本文描述已退役的 headless / legacy-scheduler Formation,僅留作歷史參考。現行路徑:`node .formation/tools/dispatch.js --skill=do --task=TASK_XXX --dispatch-mode=subagent`(見 `.formation/README.md`)。
4
+
3
5
  > **專案位置**: `.formation/`
4
6
  > **系統類型**: Multi-Agent 協作陣型系統
5
7
  > **適用場景**: 根據任務規模動態選擇最佳 Agent 部署策略
@@ -1 +1 @@
1
- 0.3.5
1
+ 0.3.7
@@ -4,7 +4,9 @@
4
4
  * ARCH-001 deterministic delta-aware file-size gate.
5
5
  *
6
6
  * Single source of truth for thresholds/scope: .rule/general/file-size-threshold.yaml
7
- * (this script never hardcodes 2000/4000 or the include/exclude globs).
7
+ * (project copy first, then core's; DEFAULT_CONFIG below when neither exists, e.g. a user
8
+ * project). Thresholds 2026-10-10: warn 1000 / hard 2000; existing over-limit files are
9
+ * grandfathered (only growth without a paired extraction blocks).
8
10
  *
9
11
  * Delta-aware + grandfather semantics:
10
12
  * - BLOCK: a change newly pushes a file past hard_limit, OR grows a file
@@ -30,7 +32,6 @@ const fs = require('fs');
30
32
  const path = require('path');
31
33
  const { spawnSync } = require('child_process');
32
34
  const yaml = (() => { try { return require('js-yaml'); } catch { return require('../../.vendor/js-yaml.cjs'); } })();
33
- const micromatch = require('micromatch');
34
35
 
35
36
  const ROOT = path.resolve(__dirname, '..', '..');
36
37
  const RULE_PATH = path.join(ROOT, '.rule', 'general', 'file-size-threshold.yaml');
@@ -40,17 +41,28 @@ const RULE_PATH = path.join(ROOT, '.rule', 'general', 'file-size-threshold.yaml'
40
41
  // define this separately for warn vs hard_limit, so both cases share it.
41
42
  const GROWTH_THRESHOLD_LOC = 50;
42
43
 
43
- function loadConfig() {
44
- let raw;
45
- try {
46
- raw = fs.readFileSync(RULE_PATH, 'utf8');
47
- } catch (e) {
48
- throw new Error(`ARCH-001: cannot read rule source ${RULE_PATH}: ${e.message}`);
49
- }
50
- const doc = yaml.load(raw);
44
+ // Built-in defaults, used when neither the project nor the core has the rule yaml
45
+ // (a user project installed from npm). Keep in sync with the yaml.
46
+ const DEFAULT_CONFIG = {
47
+ warn: 1000,
48
+ hardLimit: 2000,
49
+ include: ['**/*.rs', '**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.py', '**/*.go'],
50
+ exclude: [
51
+ '**/vendor/**', '**/target/**', '**/node_modules/**', '**/dist/**', '**/.next/**',
52
+ '**/*.test.*', '**/*.spec.*', '**/*_test.*', '**/tests/**', '**/__tests__/**',
53
+ '**/*.d.ts', '**/*.generated.*', '**/generated/**', '**/*.min.js',
54
+ ],
55
+ };
56
+
57
+ /** Rule yaml: the project's own first, then the core's; none found -> DEFAULT_CONFIG. */
58
+ function loadConfig(repoPath) {
59
+ const candidates = [path.join(repoPath || process.cwd(), '.rule', 'general', 'file-size-threshold.yaml'), RULE_PATH];
60
+ const rulePath = candidates.find((p) => fs.existsSync(p));
61
+ if (!rulePath) return DEFAULT_CONFIG;
62
+ const doc = yaml.load(fs.readFileSync(rulePath, 'utf8'));
51
63
  const rule = (doc && doc.rules || []).find((r) => r.id === 'ARCH-001');
52
64
  if (!rule || !rule.thresholds || !rule.scope) {
53
- throw new Error(`ARCH-001: malformed rule source ${RULE_PATH}`);
65
+ throw new Error(`ARCH-001: malformed rule source ${rulePath}`);
54
66
  }
55
67
  return {
56
68
  warn: rule.thresholds.warn,
@@ -60,11 +72,26 @@ function loadConfig() {
60
72
  };
61
73
  }
62
74
 
75
+ // The npm core ships without node_modules, so the scope globs (`**/`, `*`, `?`) are matched here.
76
+ function globToRegExp(glob) {
77
+ let re = '';
78
+ for (let i = 0; i < glob.length; i++) {
79
+ const c = glob[i];
80
+ if (c === '*' && glob[i + 1] === '*') {
81
+ i++;
82
+ if (glob[i + 1] === '/') { i++; re += '(?:.*/)?'; } else re += '.*';
83
+ } else if (c === '*') re += '[^/]*';
84
+ else if (c === '?') re += '[^/]';
85
+ else re += c.replace(/[.+^${}()|[\]\\]/g, '\\$&');
86
+ }
87
+ return new RegExp('^' + re + '$');
88
+ }
89
+
90
+ const matchesAny = (p, globs) => globs.some((g) => globToRegExp(g).test(p));
91
+
63
92
  function isInScope(relPath, config) {
64
93
  const posixPath = relPath.split(path.sep).join('/');
65
- if (!micromatch.isMatch(posixPath, config.include, { dot: true })) return false;
66
- if (micromatch.isMatch(posixPath, config.exclude, { dot: true })) return false;
67
- return true;
94
+ return matchesAny(posixPath, config.include) && !matchesAny(posixPath, config.exclude);
68
95
  }
69
96
 
70
97
  function countLines(text) {
@@ -127,7 +154,7 @@ function absoluteStatus(lines, config) {
127
154
 
128
155
  /** --staged: compare staged (index) content against HEAD. */
129
156
  function runStaged(config, repoPath) {
130
- const cwd = repoPath || ROOT;
157
+ const cwd = repoPath || process.cwd();
131
158
  // -c core.quotePath=false: avoid octal-escaped/double-quoted non-ASCII paths
132
159
  // in porcelain output, which would break the plain string parsing below.
133
160
  const nameStatus = git(['-c', 'core.quotePath=false', 'diff', '--cached', '--name-status'], cwd);
@@ -217,7 +244,7 @@ function main() {
217
244
  const args = parseArgs(process.argv.slice(2));
218
245
  let config;
219
246
  try {
220
- config = loadConfig();
247
+ config = loadConfig(args.repo);
221
248
  } catch (e) {
222
249
  console.error(e.message);
223
250
  process.exit(2);
@@ -230,7 +257,7 @@ function main() {
230
257
  } else if (args.file) {
231
258
  results = runFile(args.file, config);
232
259
  } else if (args.base && args.head) {
233
- results = runRefDiff(args.base, args.head, args.repo || ROOT, config);
260
+ results = runRefDiff(args.base, args.head, args.repo || process.cwd(), config);
234
261
  } else {
235
262
  console.error('Usage: check-file-size.js --staged | --file <path> | --base <ref> --head <ref> [--repo <path>]');
236
263
  process.exit(2);
@@ -33,7 +33,7 @@ cat k-cli.md
33
33
  ## 核心價值
34
34
 
35
35
  - **零依賴**:不需要 Dashboard 運行,直接呼叫底層工具
36
- - **spawnClaude 架構**:寫入操作透過 `spawnClaude` 直接啟動 Claude CLI,無需 headless.js 中間層
36
+ - **spawnClaude 架構**:寫入操作透過 `spawnClaude` 直接啟動 Claude CLI
37
37
  - **dry-run 支援**:所有寫入命令支援 `--dry-run`,驗證但不執行
38
38
  - **可腳本化**:所有輸出支援 `--json` 格式,方便 pipe 和自動化
39
39
  - **明確偵測**:不猜測、不 fallback、不向上找目錄,偵測失敗就是失敗
@@ -50,8 +50,6 @@ cat k-cli.md
50
50
  | 讀取 | specialist list, task list, task get, project status... | 直接呼叫 |
51
51
  | 寫入(任務) | task create | → spawnClaude + Ansuz(分析需求 → Amelia 審查 role → 建立) |
52
52
  | 刪除 | task delete | → 直接 rm 檔案 |
53
- | 調度 | dispatch run TASK_XXX | → spawnClaude + Ansuz(讀 role → 判斷陣型 → headless.js) |
54
- | 調度 | dispatch run --auto | → spawnClaude + Ansuz(掃描 backlog → 逐個 dispatch) |
55
53
  | 執行 | specialist run | → spawnClaude + 指定 Specialist |
56
54
 
57
55
  ## 命令一覽
@@ -149,65 +147,13 @@ k-cli task create "描述"
149
147
 
150
148
  | 命令 | 說明 | 執行方式 |
151
149
  |------|------|---------|
152
- | `k-cli dispatch run <taskId>` | Dispatch 指定任務 | → spawnClaude + Ansuz + headless.js |
153
- | `k-cli dispatch run --auto` | 自動掃描 backlog 並 dispatch | → spawnClaude + Ansuz + headless.js |
154
150
  | `k-cli dispatch status <taskId>` | 查看 dispatch 狀態 | 直接讀取 |
155
151
 
156
152
  ```bash
157
- # 指定任務 dispatch(Ansuz 讀 role → 判斷陣型 → headless.js 執行)
158
- k-cli dispatch run TASK_340
159
- k-cli dispatch run TASK_340 --dry-run
160
-
161
- # 自動掃描(Ansuz 掃 backlog → 按優先級逐個 dispatch)
162
- k-cli dispatch run --auto
163
- k-cli dispatch run --auto --project-path /path/to/project
164
-
165
153
  # 查看狀態(自動搜尋 .kenaz/logs/ 或 .formation/logs/)
166
154
  k-cli dispatch status TASK_340 --tail 50
167
155
  ```
168
156
 
169
- #### dispatch run 流程
170
-
171
- ```
172
- k-cli dispatch run TASK_XXX
173
- │
174
- └→ spawnClaude(Ansuz)
175
- ├─ Step 1: task-api get → 讀取任務 role
176
- ├─ Step 2: role → --skill, 判斷陣型(unit > solo > squad > legion)
177
- └─ Step 3: node headless.js --skill={role} --task=TASK_XXX
178
- │
179
- headless.js 自動處理:
180
- ├─ auto-claim(backlog → active)
181
- ├─ 依賴預檢
182
- ├─ spawnClaude(agent) 執行任務
183
- ├─ ensureTaskComplete
184
- └─ postComplete(archive + git branch + merge)
185
- ```
186
-
187
- #### Formation Role → Skill 對應
188
-
189
- | role | --skill | 說明 |
190
- |------|---------|------|
191
- | frontend | frontend | React/TypeScript UI、client-side |
192
- | backend | backend | API、DB、server-side |
193
- | designer | designer | 設計規範產出 |
194
- | qa | qa | 測試、Bug 驗證 |
195
- | sre | sre | 基礎設施、部署 |
196
- | developer | developer | 通才兜底 |
197
-
198
- Skill 定義檔:`.formation/legacy/{role}.yaml`
199
-
200
- #### 陣型判斷
201
-
202
- 原則:能 unit 不 solo,能 solo 不 squad,能 squad 不 legion。
203
-
204
- | 陣型 | 條件 | headless.js 參數 |
205
- |------|------|-----------------|
206
- | unit(預設) | 目標明確、單一 scope | `--skill={role} --task=TASK_XXX` |
207
- | solo | 需多角度分析 | `--formation=solo --skill=strategy --task=TASK_XXX` |
208
- | squad | 多個獨立子任務可並行 | `--formation=squad --skill=blitz --task=TASK_XXX` |
209
- | legion | 有順序依賴或即時協調 | `--formation=legion --skill=forge --task=TASK_XXX` |
210
-
211
157
  ### Project 命令
212
158
 
213
159
  | 命令 | 說明 | 執行方式 |
@@ -274,7 +220,6 @@ k-cli project install k-cli --dry-run
274
220
  | `--dry-run` | 驗證但不執行(適用所有寫入 / 調度命令) |
275
221
  | `--json` | 輸出 JSON 格式 |
276
222
  | `--project-path <path>` | 指定子專案路徑(使用 `.kenaz/project.yaml` 中的 `project_path` 值) |
277
- | `--auto` | 自動掃描模式(僅 `dispatch run`) |
278
223
  | `--force` | 跳過品質檢查(僅 `task create`) |
279
224
  | `--no-git` | 初始化時跳過 Git(僅 `project initialize`) |
280
225
  | `--core-path <path>` | 覆蓋自動偵測的 core 路徑(僅 `project initialize`) |
@@ -306,7 +251,6 @@ k-cli/
306
251
  │ │ ├── update.ts # task update(直接 task-api.js)
307
252
  │ │ └── delete.ts # task delete(直接 task-api.js)
308
253
  │ ├── dispatch/ # Formation 調度功能
309
- │ │ ├── run.ts # dispatch run(spawnClaude + Ansuz + headless.js)
310
254
  │ │ └── status.ts # dispatch status(直接讀 log)
311
255
  │ └── project/ # 專案管理功能
312
256
  │ ├── status.ts # project status(含 project.yaml 詳情)
@@ -333,10 +277,6 @@ k-cli
333
277
  │ ├─ task/create → Ansuz 分析需求
334
278
  │ │ → Agent tool subagent(Amelia 審查 + role)
335
279
  │ │ → task-api.js create(最終建立)
336
- │ ├─ dispatch/run → Ansuz 讀 role + 判斷陣型
337
- │ │ TASK_XXX → headless.js --skill={role} --task=TASK_XXX
338
- │ └─ dispatch/run → Ansuz 掃 backlog + 逐個 dispatch
339
- │ --auto → headless.js × N
340
280
  │
341
281
  └─ spawnClaude(Specialist 直接執行)
342
282
  └─ specialist/run → session.js(組裝 prompt)+ spawnClaude
@@ -354,38 +294,6 @@ spawnClaude(opts)
354
294
  └─ spawn(claude, args) → stdin pipe prompt, stream-json 輸出
355
295
  ```
356
296
 
357
- ### Git 流程(自動觸發鏈)
358
-
359
- ```
360
- k-cli dispatch run TASK_XXX
361
- └→ Ansuz → headless.js
362
- ├─ auto-claim(backlog → active)
363
- ├─ 依賴預檢
364
- ├─ 保存當前分支 → .formation/.dispatch-branch
365
- ├─ spawnClaude(agent) 執行任務
366
- └─ agent exit 0 → 自動觸發:
367
- ├─ ensureTaskComplete → task-api complete
368
- └─ postComplete()
369
- ├─ archive(done/ 超過 10 個 → archived/)
370
- └─ gitBranchAndMerge()
371
- ├─ stash 未提交變更
372
- ├─ checkout -b task/task_xxx
373
- ├─ unstash + git add + commit
374
- ├─ checkout {userBranch}
375
- ├─ merge task/task_xxx --no-ff
376
- └─ 提示: merge to main when ready
377
- ```
378
-
379
- | 階段 | 誰處理 | 自動/手動 |
380
- |------|--------|----------|
381
- | claim | headless.js | 自動 |
382
- | agent 執行 | headless.js → spawnClaude | 自動 |
383
- | task complete | headless.js ensureTaskComplete() | 自動 |
384
- | archive | post-complete.js | 自動 |
385
- | task branch + commit | post-complete.js gitBranchAndMerge() | 自動 |
386
- | merge 到 user branch | post-complete.js | 自動 |
387
- | merge 到 main | **用戶** | **手動** |
388
-
389
297
  ## Dry-Run 行為
390
298
 
391
299
  `--dry-run` 適用於所有有副作用的命令:
@@ -393,7 +301,6 @@ k-cli dispatch run TASK_XXX
393
301
  | 命令 | dry-run 行為 |
394
302
  |------|-------------|
395
303
  | `specialist run` | 組裝 prompt → 顯示 agent + prompt 長度 → **不 spawn Claude** |
396
- | `dispatch run` | 組裝 Ansuz prompt → 顯示模式 + 資訊 → **不 spawn Ansuz** |
397
304
  | `task create` | 組裝 Ansuz prompt → 顯示將建立的任務資訊 → **不啟動 Ansuz** |
398
305
  | `task update` | 組裝 task-api args → 顯示將更新的內容 → **不呼叫 task-api** |
399
306
  | `task delete` | 組裝 task-api args → 顯示將刪除的任務 → **不呼叫 task-api** |
@@ -14,7 +14,7 @@ npm run build && npm test
14
14
 
15
15
  ## 前置條件
16
16
 
17
- - Claude CLI 可用(`task create` 和 `dispatch run` 會 spawnClaude)
17
+ - Claude CLI 可用(`task create` 會 spawnClaude)
18
18
  - `npm run build` 必須先執行
19
19
 
20
20
  ## 測試環境
@@ -64,20 +64,8 @@ npm run build && npm test
64
64
  - `k-cli task get TASK_A --project-path test/sandbox/`
65
65
  - 驗證:輸出含 TASK_A 詳細資訊
66
66
 
67
- ### Test 6: dispatch run(指定任務)
68
- - 使用 Test 5 的 TASK_A
69
- - `k-cli dispatch run TASK_A --project-path test/sandbox/`
70
- - 驗證:Ansuz 讀 role → 判斷陣型 → headless.js 執行,輸出含 TASK_A
71
-
72
- ### Test 7: dispatch status(等待任務完成)
73
- - `k-cli dispatch status TASK_A --project-path test/sandbox/`
74
- - 輪詢直到任務完成(或 timeout)
75
- - 驗證:輸出含 TASK_A 相關資訊,最終狀態為 done
76
-
77
- ### Test 8: dispatch run --auto(自動掃描)
78
- - `k-cli dispatch run --auto --project-path test/sandbox/`
79
- - 驗證:自動掃描 backlog,dispatch TASK_B 和 TASK_C(或剩餘待處理任務)
80
- - 驗證:輸出含掃描結果
67
+ ### Test 6-8: 已移除
68
+ - `dispatch run` 已刪除(Formation headless 調度已退役),`dispatch status` 失去可驗證的前置動作,一併移出測試
81
69
 
82
70
  ### Test 9: task delete → verify
83
71
  - 使用 Test 5 的 TASK_A
@@ -99,11 +87,9 @@ npm run build && npm test
99
87
  - 驗證:輸出含 `dry-run`,不呼叫 spawnClaude
100
88
  3. `k-cli task delete TASK_B --project-path test/sandbox/ --dry-run`
101
89
  - 驗證:輸出含 `dry-run`,任務仍存在
102
- 4. `k-cli dispatch run TASK_B --project-path test/sandbox/ --dry-run`
103
- - 驗證:輸出含 `dry-run`,不呼叫 spawnClaude
104
- 5. `k-cli specialist run --agent Amelia 測試 --project-path test/sandbox/ --dry-run`
90
+ 4. `k-cli specialist run --agent Amelia 測試 --project-path test/sandbox/ --dry-run`
105
91
  - 驗證:輸出含 `dry-run`,不呼叫 spawnClaude
106
- 6. `k-cli project install k-cli --project-path test/sandbox/ --dry-run`
92
+ 5. `k-cli project install k-cli --project-path test/sandbox/ --dry-run`
107
93
  - 驗證:輸出含 `dry-run`
108
94
 
109
95
  ## 結果格式
@@ -116,9 +102,6 @@ k-cli Test Suite
116
102
  [ 3/11] project status ✅ PASS
117
103
  [ 4/11] specialist list ✅ PASS
118
104
  [ 5/11] task create × 3 ✅ PASS
119
- [ 6/11] dispatch run ✅ PASS
120
- [ 7/11] dispatch status ✅ PASS
121
- [ 8/11] dispatch run --auto ✅ PASS
122
105
  [ 9/11] task delete ✅ PASS
123
106
  [10/11] specialist run Amelia ✅ PASS
124
107
  [11/11] --dry-run ✅ PASS
@@ -280,62 +280,6 @@ test_5_task_create() {
280
280
  return 0
281
281
  }
282
282
 
283
- # =========================================
284
- # Test 6: dispatch run (specific task)
285
- # =========================================
286
- test_6_dispatch_run() {
287
- local task_id
288
- task_id=$(cat "$SANDBOX/.test_task_a" 2>/dev/null) || { echo " No TASK_A from test 5"; return 1; }
289
- log " Dispatching: $task_id"
290
-
291
- run_cmd $KCLI dispatch run "$task_id" --project-path "$SANDBOX"
292
- local output="$LOG_CMD_OUTPUT"
293
- assert_contains "dispatch run" "$output" "$task_id" || return 1
294
- return 0
295
- }
296
-
297
- # =========================================
298
- # Test 7: dispatch status (wait for done)
299
- # =========================================
300
- test_7_dispatch_status() {
301
- local task_id
302
- task_id=$(cat "$SANDBOX/.test_task_a" 2>/dev/null) || { echo " No TASK_A from test 5"; return 1; }
303
-
304
- # Poll up to 10 times (10 sec intervals)
305
- local i output
306
- for i in $(seq 1 10); do
307
- log " --- Poll $i/10 ---"
308
- run_cmd $KCLI dispatch status "$task_id" --project-path "$SANDBOX"
309
- output="$LOG_CMD_OUTPUT"
310
- if echo "$output" | grep -qi "done\|complete\|finished"; then
311
- log " ✓ Task completed"
312
- return 0
313
- fi
314
- sleep 10
315
- done
316
-
317
- # After polling, still check if task is in done dir
318
- if [ -d "$SANDBOX/.kenaz/tasks/done/$task_id" ]; then
319
- log " ✓ Task found in done dir"
320
- return 0
321
- fi
322
-
323
- # Accept any output containing the task ID as partial pass
324
- assert_contains "dispatch status" "$output" "$task_id" || return 1
325
- return 0
326
- }
327
-
328
- # =========================================
329
- # Test 8: dispatch run --auto
330
- # =========================================
331
- test_8_dispatch_auto() {
332
- run_cmd $KCLI dispatch run --auto --project-path "$SANDBOX"
333
- local output="$LOG_CMD_OUTPUT"
334
- # Should scan backlog and attempt to dispatch remaining tasks
335
- assert_contains "dispatch auto" "$output" "backlog\|scan\|dispatch\|TASK_\|no.*task\|0 " || return 1
336
- return 0
337
- }
338
-
339
283
  # =========================================
340
284
  # Test 9: task delete -> verify
341
285
  # =========================================
@@ -412,19 +356,12 @@ test_11_dry_run() {
412
356
  output="$LOG_CMD_OUTPUT"
413
357
  assert_contains "delete dry-run" "$output" "dry-run\|dry.run\|dryRun" || return 1
414
358
 
415
- # 4. dispatch run --dry-run
416
- log " --- dry-run: dispatch run ---"
417
- run_cmd $KCLI dispatch run "$tb" --project-path "$SANDBOX" --dry-run
418
- output="$LOG_CMD_OUTPUT"
419
- assert_contains "dispatch dry-run" "$output" "dry-run\|dry.run\|dryRun" || return 1
420
-
421
- # 5. specialist run --dry-run
422
359
  log " --- dry-run: specialist run ---"
423
360
  run_cmd $KCLI specialist run --agent Amelia "test" --project-path "$SANDBOX" --dry-run
424
361
  output="$LOG_CMD_OUTPUT"
425
362
  assert_contains "specialist dry-run" "$output" "dry-run\|dry.run\|dryRun" || return 1
426
363
 
427
- # 6. project install --dry-run
364
+ # 4. project install --dry-run
428
365
  log " --- dry-run: project install ---"
429
366
  run_cmd $KCLI project install k-cli --project-path "$SANDBOX" --dry-run
430
367
  output="$LOG_CMD_OUTPUT"
@@ -441,9 +378,6 @@ run_test 2 "ping" test_2_ping
441
378
  run_test 3 "project status" test_3_project_status
442
379
  run_test 4 "specialist list" test_4_specialist_list
443
380
  run_test 5 "task create x3" test_5_task_create
444
- run_test 6 "dispatch run" test_6_dispatch_run
445
- run_test 7 "dispatch status" test_7_dispatch_status
446
- run_test 8 "dispatch run --auto" test_8_dispatch_auto
447
381
  run_test 9 "task delete" test_9_task_delete
448
382
  run_test 10 "specialist run Amelia" test_10_specialist_run
449
383
  run_test 11 "--dry-run" test_11_dry_run
@@ -23,6 +23,7 @@ ARGUMENTS 含 `[專案: X]` 或 `--project-path X` → PROJECT_PATH = X,下方
23
23
  - 有**對照組**:證明測試在舊程式上會失敗,或修改前後結果不同。
24
24
  - subagent 回報「未驗證」的項目,由你補驗或明講仍未驗證。
25
25
  - 合併:`git merge` 後跑相關測試(或 `node "${CLAUDE_PLUGIN_ROOT}/hooks/core-resolve.js" .specialist/Ansuz/tools/verify-and-land.js`),用這個專案自己的測試指令。
26
+ - 檔案長度:verify-and-land 合併前會跑 `check-file-size`(警告 1000 / 上限 2000 行,既有超長檔只擋「未配套拆分的增長」);被擋就按職責拆成 module / component / hook。
26
27
  - 只在 Kenaz 核心 repo 本身:Rust 測試走 `npm run test:rust` / `dev/scripts/rust-verify-worktree.js`。
27
28
 
28
29
  ## 回報
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kenaz",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "description": "Install Kenaz (plugin, hooks, kenaz-server) for Claude Code and Codex: npx kenaz install",
5
5
  "bin": {
6
6
  "kenaz": "bin/kenaz.js"
@@ -20,11 +20,11 @@
20
20
  "test": "node --test lib/cli.test.js"
21
21
  },
22
22
  "optionalDependencies": {
23
- "@kenazai/server-darwin-arm64": "0.3.5",
24
- "@kenazai/server-darwin-x64": "0.3.5",
25
- "@kenazai/server-linux-arm64": "0.3.5",
26
- "@kenazai/server-linux-x64": "0.3.5",
27
- "@kenazai/server-win32-x64": "0.3.5"
23
+ "@kenazai/server-darwin-arm64": "0.3.7",
24
+ "@kenazai/server-darwin-x64": "0.3.7",
25
+ "@kenazai/server-linux-arm64": "0.3.7",
26
+ "@kenazai/server-linux-x64": "0.3.7",
27
+ "@kenazai/server-win32-x64": "0.3.7"
28
28
  },
29
29
  "license": "UNLICENSED",
30
30
  "repository": {