kenaz 0.3.5 → 0.3.6

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 (44) hide show
  1. package/core/.claude/commands/Ansuz.md +1 -0
  2. package/core/.formation/native/claude/README.md +1 -1
  3. package/core/.formation/role-map.yaml +3 -3
  4. package/core/.formation/spec/task_operations.yaml +3 -3
  5. package/core/.formation/tools/__tests__/subagent-bifrost-guard.test.js +4 -4
  6. package/core/.formation/tools/dispatch.js +5 -5
  7. package/core/.formation/tools/prompt.js +4 -0
  8. package/core/.formation/unit/do.yaml +2 -4
  9. package/core/.rule/general/agent-workflow.yaml +4 -4
  10. package/core/.rule/general/file-size-threshold.yaml +16 -4
  11. package/core/.rule/general/rust-verify-target.yaml +0 -1
  12. package/core/.rule/general/system-invariants.yaml +8 -25
  13. package/core/.specialist/Ansuz/SYSTEM_DOC.md +17 -42
  14. package/core/.specialist/Ansuz/capability.yaml +7 -14
  15. package/core/.specialist/Ansuz/concepts/formation_quick_ref.yaml +4 -11
  16. package/core/.specialist/Ansuz/elixir.yaml +3 -4
  17. package/core/.specialist/Ansuz/identity_contract.yaml +1 -1
  18. package/core/.specialist/Ansuz/knowledge.yaml +7 -10
  19. package/core/.specialist/Ansuz/rule.yaml +2 -2
  20. package/core/.specialist/Ansuz/tools/verify-and-land.js +22 -0
  21. package/core/.specialist/Ansuz/tools/verify-and-land.test.js +59 -5
  22. package/core/.specialist/Ansuz/workflow.yaml +7 -11
  23. package/core/.specialist/Forseti/core.yaml +1 -1
  24. package/core/.specialist/Heimdall/SKILL.md +1 -1
  25. package/core/.specialist/Heimdall/core.yaml +2 -2
  26. package/core/.specialist/Huginn/rule.yaml +1 -1
  27. package/core/.specialist/tools/README.md +2 -2
  28. package/core/.system/collaboration/README.md +1 -1
  29. package/core/.system/formation/LEGACY_SKILLS.md +2 -0
  30. package/core/.system/formation/POST_COMPLETE.md +2 -0
  31. package/core/.system/formation/README.md +2 -0
  32. package/core/KENAZ_CORE_VERSION +1 -1
  33. package/core/dev/scripts/check-file-size.js +44 -17
  34. package/core/k-cli/README.md +1 -94
  35. package/core/k-cli/test/TEST_SPEC.md +5 -22
  36. package/core/k-cli/test/run.sh +1 -67
  37. package/core/plugins/kenaz/commands/Ansuz.md +1 -0
  38. package/package.json +6 -6
  39. package/core/.formation/legion/forge.yaml +0 -299
  40. package/core/.formation/legion/hive.yaml +0 -238
  41. package/core/.formation/squad/blitz.yaml +0 -192
  42. package/core/.formation/squad/strategy-tribunal.yaml +0 -354
  43. package/core/.formation/squad/strategy.yaml +0 -349
  44. package/core/.formation/squad/sweep.yaml +0 -208
@@ -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
 
@@ -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' });
@@ -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.6
@@ -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** |