kenaz 0.3.2 → 0.3.5

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 (71) hide show
  1. package/core/.claude/commands/Ansuz.md +36 -35
  2. package/core/.claude/hooks/__tests__/task-2395-project-registration.test.js +84 -0
  3. package/core/.claude/hooks/_ansuz-conversation.js +1 -0
  4. package/core/.claude/hooks/_ansuz-conversation.test.js +6 -0
  5. package/core/.claude/hooks/_run-user-prompt.js +0 -4
  6. package/core/.claude/hooks/_run-user-prompt.test.js +2 -3
  7. package/core/.claude/hooks/handoff-staleness.js +2 -2
  8. package/core/.claude/hooks/turn-context.js +50 -2
  9. package/core/.claude/settings.json +0 -37
  10. package/core/.collaboration/core/task-api.js +4 -12
  11. package/core/.formation/README.md +21 -146
  12. package/core/.formation/bifrost/codex-doctor.js +37 -2
  13. package/core/.formation/bifrost/codex-doctor.test.js +18 -0
  14. package/core/.formation/bifrost/launch.js +1 -1
  15. package/core/.formation/bifrost/openai/codex-hook-shim.js +6 -0
  16. package/core/.formation/bifrost/openai/codex-hook-shim.test.js +11 -1
  17. package/core/.formation/bifrost/setup-codex.js +3 -0
  18. package/core/.formation/spec/task_operations.parity.test.js +0 -1
  19. package/core/.formation/tools/dashboard-models-chain.test.js +0 -6
  20. package/core/.formation/tools/dispatch-detail/cross-project-worktree.md +1 -9
  21. package/core/.formation/tools/dispatch-detail/worktree-subagent-mode.md +2 -2
  22. package/core/.formation/tools/dispatch.js +28 -549
  23. package/core/.formation/tools/mcp-allowlist.test.js +0 -1
  24. package/core/.formation/tools/post-complete/task2345.test.js +0 -5
  25. package/core/.formation/tools/pre-dispatch-snapshot.test.js +0 -100
  26. package/core/.formation/tools/prompt.js +66 -1607
  27. package/core/.formation/tools/prompt.test.js +43 -582
  28. package/core/.kenaz/claude-hooks-manifest.json +16 -16
  29. package/core/.specialist/Ansuz/RULES.md +6 -10
  30. package/core/.specialist/tools/__tests__/freya-cli-contract.test.js +0 -1
  31. package/core/.specialist/tools/__tests__/task-2218-boot-session-id.test.js +17 -1
  32. package/core/.specialist/tools/__tests__/task-2222-session-identity-interleave.test.js +17 -1
  33. package/core/.specialist/tools/__tests__/task-2254-handoff-archive-contract.test.js +6 -5
  34. package/core/.specialist/tools/ansuz-boot-fast.js +44 -100
  35. package/core/.specialist/tools/ansuz-boot-fast.test.js +45 -1
  36. package/core/KENAZ_CORE_VERSION +1 -1
  37. package/core/dev/plans/ANSUZ_SLIM_PLAN.md +68 -0
  38. package/core/plugins/kenaz/commands/Amelia.md +1 -1
  39. package/core/plugins/kenaz/commands/Ansuz.md +36 -35
  40. package/core/plugins/kenaz/hooks/hooks.json +1 -5
  41. package/core/plugins/kenaz/hooks/kenaz-hook.js +0 -4
  42. package/core/plugins/kenaz/scripts/kenaz-mode.js +11 -1
  43. package/core/plugins/kenaz/scripts/kenaz.test.js +1 -1
  44. package/core/plugins/kenaz/scripts/sync.js +6 -2
  45. package/core/plugins/kenaz/scripts/ygg.js +13 -1
  46. package/core/plugins/kenaz/skills/product_manager/SKILL.md +12 -12
  47. package/lib/cli.js +47 -6
  48. package/lib/cli.test.js +24 -0
  49. package/lib/dashboard.js +10 -0
  50. package/package.json +11 -7
  51. package/vendor/install-kenaz-cli.js +18 -7
  52. package/core/.formation/tools/broadcast.js +0 -103
  53. package/core/.formation/tools/dispatch-dryrun.test.js +0 -54
  54. package/core/.formation/tools/dispatch-notifier.js +0 -84
  55. package/core/.formation/tools/formations.js +0 -1203
  56. package/core/.formation/tools/headless-branch-recovery.test.js +0 -64
  57. package/core/.formation/tools/headless-dryrun.test.js +0 -79
  58. package/core/.formation/tools/headless-v2.js +0 -659
  59. package/core/.formation/tools/headless.js +0 -1071
  60. package/core/.formation/tools/legacy-scheduler.js +0 -1994
  61. package/core/.formation/tools/logger.js +0 -273
  62. package/core/.formation/tools/memory-loader.js +0 -422
  63. package/core/.formation/tools/process-state.js +0 -33
  64. package/core/.formation/tools/remote-conflict.js +0 -98
  65. package/core/.formation/tools/runner.js +0 -1332
  66. package/core/.formation/tools/task-utils.js +0 -344
  67. package/core/.formation/tools/task2028-headless-worktree.test.js +0 -112
  68. package/core/.formation/tools/tests/headless-mcp-args-plumbing.test.js +0 -123
  69. package/core/.formation/tools/worktree-leak.test.js +0 -162
  70. package/core/.specialist/tools/__tests__/task-1863-boot-lease-reuse.test.js +0 -99
  71. package/core/.specialist/tools/__tests__/task-1977-banner-cap.test.js +0 -129
@@ -2,14 +2,12 @@
2
2
  /**
3
3
  * Formation Dispatch — CLI 入口
4
4
  *
5
- * 根據陣型調度 subagent 執行任務。
6
- * 實際邏輯分散在各模組:loader, prompt, runner, logger, utils。
5
+ * Sub-agent dispatch: prints the JSON payload (promptText + agentParams) the
6
+ * Agent tool (or Codex spawn_agent) consumes. The only live dispatch path.
7
7
  *
8
8
  * Usage:
9
- * node .formation/tools/dispatch.js --skill=do --task=TASK_172
10
- * node .formation/tools/dispatch.js --formation=solo --skill=strategy --task=TASK_172
11
- * node .formation/tools/dispatch.js --formation=squad --skill=blitz --task=TASK_173
12
- * node .formation/tools/dispatch.js --formation=legion --skill=forge --task=TASK_174 --dry-run
9
+ * node .formation/tools/dispatch.js --skill=do --task=TASK_172 --dispatch-mode=subagent
10
+ * node .formation/tools/dispatch.js --skill=do --task=TASK_172 --dispatch-mode=subagent --harness=codex
13
11
  */
14
12
 
15
13
  const path = require('path');
@@ -21,14 +19,12 @@ const {
21
19
  promptProfile, extractRigor, extractComplexity, splitPromptSections,
22
20
  applyRustCheckToPrefix,
23
21
  } = require('./prompt');
24
- const { runSingle, runParallel } = require('./runner');
25
22
  const {
26
- generateSessionId, ensureGitReady, ROOT, loadDashboardModels,
23
+ ROOT, loadDashboardModels,
27
24
  resolveWorktreeDecision, dispatchLockPath, tryAcquireDispatchLock, dispatchLockStaleness,
28
25
  createWorktree, ensureWorktreeDirIgnored, COLLABORATION_API,
29
26
  getVerifyOfflineCommand,
30
27
  } = require('./utils');
31
- const { refreshCache, hasRemoteConflict, hasRemoteFileConflict, isCacheStale } = require('./remote-conflict');
32
28
 
33
29
  // TASK_1815: the Agent tool's `model` parameter accepts ONLY these Claude
34
30
  // family names — no versions, no [1m] speed modifiers, no full model ids.
@@ -448,66 +444,9 @@ function samePath(a, b) {
448
444
  }
449
445
 
450
446
  // ─────────────────────────────────────────
451
- // Yggdrasil 廣播(lazy load, fire-and-forget)
447
+ // Isolation routing
452
448
  // ─────────────────────────────────────────
453
449
 
454
- let _supabaseClient = null;
455
- let _taskSync = null;
456
- let _commitSync = null;
457
- function getSupabaseClient(projectPath) {
458
- if (_supabaseClient === undefined) return null;
459
- if (!_supabaseClient) try { _supabaseClient = require('./supabase-client'); } catch { _supabaseClient = undefined; return null; }
460
- return _supabaseClient.getClient(projectPath);
461
- }
462
- function getTaskSync() {
463
- if (_taskSync === undefined) return null;
464
- if (!_taskSync) try { _taskSync = require('./task-sync'); } catch { _taskSync = undefined; return null; }
465
- return _taskSync;
466
- }
467
- function getCommitSync() {
468
- if (_commitSync === undefined) return null;
469
- if (!_commitSync) try { _commitSync = require('./commit-sync'); } catch { _commitSync = undefined; return null; }
470
- return _commitSync;
471
- }
472
-
473
- /**
474
- * 同步 commit 到 kenaz_commits。
475
- * 回傳 Promise(await 時確保完成,或 fire-and-forget)。
476
- */
477
- function syncCommit(taskId, projectPath) {
478
- try {
479
- const cs = getCommitSync();
480
- if (!cs) return Promise.resolve();
481
- return cs.syncLatestCommit({ taskId, projectPath }).catch(() => {});
482
- } catch { return Promise.resolve(); }
483
- }
484
-
485
- /**
486
- * 廣播 kenaz_status。
487
- * 回傳 Promise — caller 可 await 確保完成(cleanup 場景),
488
- * 或 fire-and-forget(上線廣播場景)。
489
- */
490
- function broadcastStatus(projectPath, updates) {
491
- try {
492
- const sb = getSupabaseClient(projectPath);
493
- if (!sb) return Promise.resolve();
494
- const { client, config } = sb;
495
- const ts = getTaskSync();
496
- const project = ts?.resolveProjectName?.(projectPath || ROOT) || path.basename(ROOT);
497
- return client.from('kenaz_status').upsert({
498
- node_name: config.node_name,
499
- project,
500
- ...updates,
501
- }, { onConflict: 'node_name,project' }).then(() => {}).catch(() => {});
502
- } catch { return Promise.resolve(); }
503
- }
504
-
505
- // ─────────────────────────────────────────
506
- // Active dispatch registry(Dashboard Agent Monitor 用)
507
- // ─────────────────────────────────────────
508
-
509
- const ACTIVE_DISPATCH_FILE = path.join(ROOT, '.formation', '.active-dispatches.json');
510
-
511
450
  /**
512
451
  * TASK_1833: THE single isolation-routing decision for a dispatch. Consumed by
513
452
  * the real sub-agent path — do not copy the check:
@@ -566,59 +505,6 @@ function resolveDispatchIsolation({ projectPath = null, useWorktreeOverride = nu
566
505
  };
567
506
  }
568
507
 
569
- function readActiveDispatches() {
570
- try {
571
- if (fs.existsSync(ACTIVE_DISPATCH_FILE)) {
572
- return JSON.parse(fs.readFileSync(ACTIVE_DISPATCH_FILE, 'utf-8'));
573
- }
574
- } catch { /* corrupted file */ }
575
- return [];
576
- }
577
-
578
- function writeActiveDispatches(list) {
579
- try {
580
- fs.writeFileSync(ACTIVE_DISPATCH_FILE, JSON.stringify(list, null, 2));
581
- } catch { /* ignore */ }
582
- }
583
-
584
- function registerDispatch({ sessionId, logPath, taskId, formation, projectPath: pp }) {
585
- const list = readActiveDispatches();
586
- const nextEntry = {
587
- sessionId,
588
- logPath,
589
- taskId,
590
- formation,
591
- startedAt: new Date().toISOString(),
592
- projectPath: pp || null,
593
- };
594
- const existingIndex = list.findIndex(d => d.sessionId === sessionId);
595
- if (existingIndex >= 0) {
596
- const mergedEntry = {
597
- ...list[existingIndex],
598
- startedAt: list[existingIndex].startedAt || nextEntry.startedAt,
599
- };
600
- for (const [key, value] of Object.entries(nextEntry)) {
601
- if (key === 'startedAt') continue;
602
- if (value !== null && value !== undefined) {
603
- mergedEntry[key] = value;
604
- }
605
- }
606
- list[existingIndex] = mergedEntry;
607
- } else {
608
- list.push(nextEntry);
609
- }
610
- writeActiveDispatches(list);
611
- }
612
-
613
- function unregisterDispatch(sessionId) {
614
- const list = readActiveDispatches().filter(d => d.sessionId !== sessionId);
615
- if (list.length === 0) {
616
- try { fs.unlinkSync(ACTIVE_DISPATCH_FILE); } catch { /* ignore */ }
617
- } else {
618
- writeActiveDispatches(list);
619
- }
620
- }
621
-
622
508
  // ─────────────────────────────────────────
623
509
  // Cortex Context 注入(dispatch 前自動注入相關檔案摘要)
624
510
  // ─────────────────────────────────────────
@@ -837,385 +723,6 @@ function checkDependencies(taskData, projectPath) {
837
723
  const unmet = deps.filter(dep => !completedIds.has(dep));
838
724
  return { blocked: unmet.length > 0, unmet, blockedDeps };
839
725
  }
840
-
841
- // ─────────────────────────────────────────
842
- // 調度主函數
843
- // ─────────────────────────────────────────
844
-
845
- async function dispatch(options) {
846
- const {
847
- formation = 'unit',
848
- skill = 'do',
849
- task,
850
- output = null,
851
- dryRun = false,
852
- projectPath = null
853
- } = options;
854
-
855
- const sessionId = generateSessionId();
856
-
857
- // 1. 載入技能定義
858
- const skillDef = loadSkill(formation, skill);
859
-
860
- // 2. 載入任務(如果是 TASK_ID,支援 projectPath)
861
- const loadOpts = projectPath ? { projectPath } : undefined;
862
- const taskData = task && task.startsWith('TASK_') ? loadTask(task, loadOpts) : null;
863
- const taskInput = task || '';
864
-
865
- // 2.5 依賴預檢:確認所有依賴都已完成
866
- if (taskData) {
867
- const depCheck = checkDependencies(taskData, projectPath);
868
- if (depCheck.blocked) {
869
- console.error(`❌ Dispatch blocked: ${taskInput} has unmet dependencies`);
870
- for (const dep of depCheck.unmet) {
871
- if (depCheck.blockedDeps && depCheck.blockedDeps.includes(dep)) {
872
- console.error(` ??${dep} ??blocked`);
873
- continue;
874
- }
875
- console.error(` ⛔ ${dep} — not in done/completed`);
876
- }
877
- console.error(` → Complete the dependencies first, or use --force to override`);
878
- if (!options.force) {
879
- process.exitCode = 1;
880
- return null;
881
- }
882
- console.warn(` ⚠️ --force: dispatching despite unmet dependencies`);
883
- }
884
- }
885
-
886
- // 3. 組裝 prompt(注入 projectPath context + Yggdrasil network context)
887
- const promptOptions = {};
888
- if (projectPath) {
889
- promptOptions.projectPath = projectPath;
890
- }
891
- if (taskInput && taskInput.startsWith('TASK_')) {
892
- promptOptions.taskId = taskInput;
893
- }
894
-
895
- // Yggdrasil: 查詢遠端節點狀態,注入 prompt 讓 Agent 知道其他人在做什麼
896
- const taskScopePaths = (() => {
897
- try {
898
- if (!taskData) return [];
899
- const parsed = JSON.parse(taskData);
900
- return parsed.scope_paths || [];
901
- } catch { return []; }
902
- })();
903
- try {
904
- const ts = getTaskSync();
905
- if (ts) {
906
- const [onlineNodes, remoteTasks] = await Promise.all([
907
- ts.getOnlineNodes(projectPath).catch(() => []),
908
- ts.getRemoteActiveTasks(projectPath).catch(() => []),
909
- ]);
910
- if (onlineNodes.length > 0 || remoteTasks.length > 0) {
911
- const sb = getSupabaseClient(projectPath);
912
- promptOptions.yggdrasilContext = {
913
- onlineNodes,
914
- remoteTasks,
915
- myNodeName: sb?.config?.node_name || 'unknown',
916
- myScopePaths: taskScopePaths,
917
- };
918
- }
919
- }
920
- } catch { /* Yggdrasil context is best-effort */ }
921
-
922
- let prompt = buildPrompt(skillDef, taskInput, taskData, promptOptions);
923
-
924
- // 3.5 Cortex Context 注入:將 scope_paths 相關檔案摘要注入 prompt
925
- // TASK_1998: "Related Files Preview" is CONTEXT volume, driven by
926
- // complexity — simple/medium drop it entirely (it was 3.9 KB of mostly
927
- // low-relevance file-head previews on the measured TASK_1992 prompt),
928
- // complex keeps it.
929
- const _dietProfile = promptProfile({ rigor: extractRigor(taskData), complexity: extractComplexity(taskData) });
930
- if (typeof prompt === 'string' && taskScopePaths.length > 0 && !_dietProfile.skipRelatedFilesPreview) {
931
- const cortexCtx = buildCortexContext(taskScopePaths, projectPath);
932
- if (cortexCtx) {
933
- prompt = prompt + '\n' + cortexCtx;
934
- }
935
- }
936
-
937
- console.log(`⚔️ Formation Dispatch`);
938
- console.log(` Formation: ${formation}`);
939
- console.log(` Skill: ${skill}`);
940
- console.log(` Task: ${taskInput.slice(0, 60)}${taskInput.length > 60 ? '...' : ''}`);
941
- console.log(` Session: ${sessionId}`);
942
- if (projectPath) console.log(` Project: ${projectPath}`);
943
-
944
- // 4. 確保子專案 git 已就緒(TASK_2274: dry-run 不得 git init — 只回報)
945
- if (dryRun) {
946
- if (projectPath && !fs.existsSync(path.join(projectPath, '.git'))) console.log(' [dry-run] would git init: ' + projectPath);
947
- } else {
948
- ensureGitReady(projectPath);
949
- }
950
-
951
- // 4.5 遠端衝突預檢(警告但不阻擋)
952
- const scopePaths = taskData?.scope_paths || [];
953
- if (scopePaths.length > 0) {
954
- try {
955
- if (isCacheStale()) {
956
- await refreshCache(projectPath);
957
- }
958
- const taskConflict = hasRemoteConflict(scopePaths);
959
- if (taskConflict.conflict) {
960
- console.log(`\n ⚠️ Remote conflict: ${taskConflict.conflictNode} is working on ${taskConflict.conflictTask}`);
961
- }
962
- const fileConflict = hasRemoteFileConflict(scopePaths);
963
- if (fileConflict.conflict) {
964
- console.log(` ⚠️ Remote file conflict: ${fileConflict.conflictNode} is touching ${fileConflict.conflictFile}`);
965
- }
966
- } catch {
967
- // 衝突檢查失敗不阻擋 dispatch
968
- }
969
- }
970
-
971
-
972
- // M1 (TASK_1758): Dispatch-time scope gate + resource lease
973
- {
974
- const _sgRoot = projectPath || ROOT;
975
- const _sgSession = _getProvenSessionId(_sgRoot); // TASK_2265: proven identity only
976
- const _sgPaths = (() => { try { return taskData ? JSON.parse(taskData).scope_paths || [] : []; } catch { return []; } })();
977
- const _sgResult = checkDispatchScopeGate(_sgPaths, _sgRoot, _sgSession, !!options.force, taskInput);
978
- if (_sgResult.collisions.length > 0) {
979
- for (const col of _sgResult.collisions) {
980
- const pl = col.paths.slice(0,3).join(', ') + (col.paths.length > 3 ? ' +' + (col.paths.length-3) + ' more' : '');
981
- if (_sgResult.blocked) {
982
- console.error(' Scope conflict with stream ' + col.stream_label + ' (task ' + col.task_id + '): ' + pl);
983
- } else {
984
- console.warn(' WARNING scope conflict with stream ' + col.stream_label + ' (task ' + col.task_id + '): ' + pl);
985
- }
986
- }
987
- if (_sgResult.blocked) {
988
- console.error('Dispatch blocked: scope overlap with live stream(s). Use --force to override.');
989
- process.exitCode = 1;
990
- return null;
991
- }
992
- }
993
- // TASK_1863: adopt our own claim marker (written by the chain's pre-claim /
994
- // move→active) so a post-gate rejection below releases it — TASK_1833
995
- // late-failure semantics extended from resource leases to the claim marker.
996
- const _sgResources = (() => { try { return taskData ? JSON.parse(taskData).resources || [] : []; } catch { return []; } })();
997
- // TASK_2274: dry-run takes no claim adoption / resource locks (mirrors the subagent path)
998
- const _sgClaim = dryRun ? null : adoptSelfClaimMarker(taskInput, _sgRoot, _sgSession);
999
- if (_sgResources.length > 0 && dryRun) {
1000
- console.log(' [dry-run] resource locks NOT acquired: ' + _sgResources.join(', '));
1001
- } else if (_sgResources.length > 0) {
1002
- const _sgLock = acquireResourceLocks(_sgResources, _sgRoot, _sgSession, taskInput);
1003
- if (_sgLock.failed.length > 0) {
1004
- for (const fail of _sgLock.failed) {
1005
- const hi = fail.holder ? ' (session=' + fail.holder.owner_session + ', task=' + fail.holder.task_id + ')' : '';
1006
- console.error((fail.orphan ? 'WARNING' : 'ERROR') + ': resource ' + fail.name + (fail.orphan ? ' ORPHAN lock' : ' busy') + hi);
1007
- }
1008
- releaseResourceLocks(_sgLock.acquired, _sgRoot);
1009
- if (!options.force) { releaseAdoptedClaimMarker(_sgClaim); console.error('Dispatch blocked: resource lock conflict. Use --force to override.'); process.exitCode = 1; return null; }
1010
- console.warn('WARNING: proceeding despite resource lock conflicts (--force).');
1011
- } else if (_sgLock.acquired.length > 0) {
1012
- console.log(' Resource locks acquired: ' + _sgLock.acquired.join(', '));
1013
- console.log(' (Release after merge: node .specialist/tools/stream-lease.js resource-release --name=<resource>)');
1014
- }
1015
- }
1016
- }
1017
-
1018
- // 5. Dry run
1019
- if (dryRun) {
1020
- return handleDryRun(formation, prompt);
1021
- }
1022
-
1023
- // 5.5 Yggdrasil: 廣播上線狀態
1024
- // Note: cleanup 由 child.on('close') 處理(await 確保完成)
1025
- // process.on('exit') 不支援 async,不適合做 Supabase cleanup
1026
- if (formation !== 'legion') {
1027
- broadcastStatus(projectPath, {
1028
- is_online: true,
1029
- current_task: taskInput || null,
1030
- touching_files: scopePaths || [],
1031
- });
1032
-
1033
- // Yggdrasil: 同步任務狀態為 active
1034
- if (taskInput.startsWith('TASK_')) {
1035
- const ts = getTaskSync();
1036
- if (ts) {
1037
- const sb = getSupabaseClient(projectPath);
1038
- const myNode = sb?.config?.node_name || null;
1039
- ts.syncTaskStatus({
1040
- taskId: taskInput,
1041
- status: 'active',
1042
- claimedBy: myNode,
1043
- scopePaths: taskScopePaths,
1044
- projectPath,
1045
- }).catch(() => {});
1046
- }
1047
- }
1048
-
1049
- // Signal handlers: 盡力 cleanup(best-effort,process 會等 promise)
1050
- const handleSignal = (code) => {
1051
- broadcastStatus(projectPath, { current_task: null, touching_files: [] })
1052
- .finally(() => process.exit(code));
1053
- };
1054
- process.on('SIGINT', () => handleSignal(130));
1055
- process.on('SIGTERM', () => handleSignal(143));
1056
- }
1057
-
1058
- // 6. 根據陣型執行(傳入 projectPath + sessionId for registry)
1059
- return execute(formation, { prompt, skill, sessionId, output, projectPath, taskId: taskInput });
1060
- }
1061
-
1062
- // ─────────────────────────────────────────
1063
- // Dry Run
1064
- // ─────────────────────────────────────────
1065
-
1066
- function handleDryRun(formation, prompt) {
1067
- if (formation === 'squad' && Array.isArray(prompt)) {
1068
- console.log(`\n--- SQUAD PROMPTS (${prompt.length} members) ---\n`);
1069
- for (const m of prompt) {
1070
- console.log(`=== Member: ${m.memberId} (skill: ${m.skill}) ===`);
1071
- console.log(m.prompt);
1072
- console.log('');
1073
- }
1074
- console.log(`--- END ---`);
1075
- } else if (formation === 'legion' && prompt.members) {
1076
- console.log(`\n--- LEGION CONFIG ---`);
1077
- console.log(`Team: ${prompt.teamName}`);
1078
- console.log(`Description: ${prompt.teamDesc}`);
1079
- console.log(`Members: ${prompt.members.length}`);
1080
- console.log(`\n--- LEAD PROMPT ---\n`);
1081
- console.log(prompt.lead.prompt);
1082
- console.log(`\n--- MEMBER PROMPTS ---\n`);
1083
- for (const m of prompt.members) {
1084
- console.log(`=== ${m.name} (${m.agentType}) ===`);
1085
- console.log(m.prompt);
1086
- console.log('');
1087
- }
1088
- console.log(`--- END ---`);
1089
- } else {
1090
- console.log(`\n--- PROMPT ---\n`);
1091
- console.log(prompt);
1092
- console.log(`\n--- END ---`);
1093
- }
1094
- }
1095
-
1096
- // ─────────────────────────────────────────
1097
- // 執行分發
1098
- // ─────────────────────────────────────────
1099
-
1100
- function execute(formation, opts) {
1101
- const { prompt, skill, sessionId, output, projectPath, taskId } = opts;
1102
-
1103
- switch (formation) {
1104
- case 'unit':
1105
- case 'solo': {
1106
- const child = runSingle({
1107
- prompt,
1108
- formation,
1109
- skill,
1110
- sessionId,
1111
- outputPath: output,
1112
- projectPath
1113
- });
1114
-
1115
- // 註冊 dispatch(Dashboard Agent Monitor 用)
1116
- registerDispatch({ sessionId, logPath: child._formation?.logPath, taskId, formation, projectPath });
1117
-
1118
- // Cleanup guard: 確保只執行一次,防止 close + formation:done 重複觸發
1119
- let cleanedUp = false;
1120
- const cleanup = (code) => {
1121
- if (cleanedUp) return;
1122
- cleanedUp = true;
1123
- unregisterDispatch(sessionId);
1124
- // Keep event loop alive with a timer so Supabase HTTP requests can complete
1125
- // before Node.js exits (child close leaves no other handles active)
1126
- const keepAlive = setTimeout(() => {}, 10000);
1127
-
1128
- // Yggdrasil: 同步任務最終狀態到 kenaz_tasks
1129
- const syncFinalStatus = (() => {
1130
- if (!taskId || !taskId.startsWith('TASK_')) return Promise.resolve();
1131
- try {
1132
- const ts = getTaskSync();
1133
- if (!ts) return Promise.resolve();
1134
- return ts.syncTaskStatus({
1135
- taskId,
1136
- status: code === 0 ? 'done' : 'failed',
1137
- projectPath,
1138
- }).catch(() => {});
1139
- } catch { return Promise.resolve(); }
1140
- })();
1141
-
1142
- Promise.allSettled([
1143
- broadcastStatus(projectPath, { current_task: null, touching_files: [] }),
1144
- code === 0 ? syncCommit(taskId, projectPath) : Promise.resolve(),
1145
- syncFinalStatus,
1146
- ]).then(() => {
1147
- clearTimeout(keepAlive);
1148
- });
1149
- };
1150
-
1151
- child.on('close', cleanup);
1152
- // Fallback: formation:done fires when result is received, even if process lingers
1153
- child.on('formation:done', cleanup);
1154
-
1155
- return child;
1156
- }
1157
-
1158
- case 'squad': {
1159
- if (!Array.isArray(prompt)) {
1160
- throw new Error('Squad prompt builder should return an array of member prompts');
1161
- }
1162
-
1163
- // Squad: 註冊整個 session
1164
- registerDispatch({ sessionId, logPath: null, taskId, formation: 'squad', projectPath });
1165
-
1166
- const result = runParallel({
1167
- members: prompt,
1168
- formation,
1169
- sessionId,
1170
- projectPath
1171
- });
1172
-
1173
- // runParallel returns a promise; await cleanup 確保完成
1174
- if (result && typeof result.then === 'function') {
1175
- const cleanup = (results) => {
1176
- unregisterDispatch(sessionId);
1177
-
1178
- // Yggdrasil: 同步任務最終狀態(squad 以全員成功為準)
1179
- const syncFinalStatus = (() => {
1180
- if (!taskId || !taskId.startsWith('TASK_')) return Promise.resolve();
1181
- try {
1182
- const ts = getTaskSync();
1183
- if (!ts) return Promise.resolve();
1184
- const allSuccess = Array.isArray(results) && results.every(r => r.exitCode === 0);
1185
- return ts.syncTaskStatus({
1186
- taskId,
1187
- status: allSuccess ? 'done' : 'failed',
1188
- projectPath,
1189
- }).catch(() => {});
1190
- } catch { return Promise.resolve(); }
1191
- })();
1192
-
1193
- return Promise.allSettled([
1194
- broadcastStatus(projectPath, { current_task: null, touching_files: [] }),
1195
- syncFinalStatus,
1196
- ]);
1197
- };
1198
- result.then(cleanup, () => cleanup(null));
1199
- }
1200
-
1201
- return result;
1202
- }
1203
-
1204
- case 'legion':
1205
- // Legion 不 spawn 進程,輸出 TeamCreate 配置供 Formation Lead 使用
1206
- console.log(`\n⚔️ Legion: TeamCreate configuration generated`);
1207
- console.log(` Team: ${prompt.teamName}`);
1208
- console.log(` Members: ${prompt.members.length}`);
1209
- console.log(`\n Legion 不直接啟動進程。`);
1210
- console.log(` 請使用 TeamCreate API 搭配以上配置啟動團隊。`);
1211
- console.log(` 或使用 --dry-run 查看完整 prompt。`);
1212
- return prompt;
1213
-
1214
- default:
1215
- throw new Error(`Unknown formation: ${formation}`);
1216
- }
1217
- }
1218
-
1219
726
  // ─────────────────────────────────────────
1220
727
  // CLI
1221
728
  // ─────────────────────────────────────────
@@ -1228,22 +735,21 @@ if (require.main === module) {
1228
735
  ⚔️ Formation Dispatch
1229
736
 
1230
737
  Usage:
1231
- node .formation/tools/dispatch.js --skill=<name> --task=<TASK_ID|description>
738
+ node .formation/tools/dispatch.js --skill=<name> --task=<TASK_ID|description> --dispatch-mode=subagent
1232
739
 
1233
740
  Options:
1234
- --formation <name> Formation type: unit (default), solo, squad, legion
741
+ --formation <name> Skill folder: unit (default) or solo
1235
742
  --skill <name> Skill to use (YAML filename without extension)
1236
743
  --task <value> Task ID (TASK_XXX) or free-text description
1237
- --concurrency <n> Parallel execution slots (default 3, max 5, legacy only)
1238
- --output <path> Custom log output path
1239
- --dry-run Only print the prompt, don't launch
1240
- --prompt-only Print raw prompt text to stdout (machine-readable, no headers)
1241
- --dispatch-mode <mode> Output mode: formation (default), subagent (JSON params for Agent tool)
1242
- --dispatch-model <id> TASK_1998 R3: subagent-path model override (wins over
744
+ --dry-run Build and print the payload with no side effects
745
+ --dispatch-mode <mode> Required: subagent (JSON params for the Agent tool)
746
+ --harness <name> claude (default) | codex (Codex spawn_agent payload)
747
+ --project-path <path> Target sub-project (omit for the core repo)
748
+ --dispatch-model <id> TASK_1998 R3: model override (wins over
1243
749
  dashboard-models.json dispatchModelByRigor and the
1244
750
  default — the dashboard dispatchModel, used for
1245
751
  every rigor unless dispatchModelByRigor sets that
1246
- rigor's entry). Only used with --dispatch-mode=subagent.
752
+ rigor's entry).
1247
753
  --use-worktree <bool> TASK_1833: dispatch-time worktree toggle (true|false).
1248
754
  Overrides env FORMATION_USE_WORKTREE and the target
1249
755
  project's .kenaz/project.yaml use_worktree. For
@@ -1262,25 +768,12 @@ Options:
1262
768
  (with --release-lock: release a lock this session
1263
769
  cannot prove it owns)
1264
770
 
1265
- Formation-Skill Quick Reference:
1266
- unit: do, think, plan
1267
- solo: strategy, diagnosis
1268
- squad: blitz, sweep
1269
- legion: forge, hive
771
+ Skills: unit: do, think, plan; solo: strategy, diagnosis.
1270
772
 
1271
773
  Sub-agent dispatch (Ansuz billing-aware path):
1272
774
  node .formation/tools/dispatch.js --skill=do --task=TASK_172 --dispatch-mode=subagent
1273
775
  → prints JSON: { promptText, taskId, agentParams: { model, isolation, description } }
1274
776
  Ansuz passes promptText to Agent tool with agentParams for worktree-isolated execution.
1275
-
1276
- Headless Mode (recommended for long tasks):
1277
- node .formation/tools/headless.js --formation=legion --skill=forge --task=TASK_174
1278
-
1279
- Examples:
1280
- node .formation/tools/dispatch.js --skill=do --task=TASK_172
1281
- node .formation/tools/dispatch.js --formation=solo --skill=strategy --task="評估新功能"
1282
- node .formation/tools/dispatch.js --formation=squad --skill=blitz --task=TASK_173
1283
- node .formation/tools/dispatch.js --formation=legion --skill=forge --task=TASK_174 --dry-run
1284
777
  `);
1285
778
  process.exit(0);
1286
779
  }
@@ -1300,10 +793,8 @@ Examples:
1300
793
  const formation = getArg('--formation') || 'unit';
1301
794
  const skill = getArg('--skill');
1302
795
  const task = getArg('--task');
1303
- const output = getArg('--output');
1304
796
  const projectPath = getArg('--project-path');
1305
797
  const dryRun = args.includes('--dry-run');
1306
- const promptOnly = args.includes('--prompt-only');
1307
798
  const dispatchModeCli = getArg('--dispatch-mode');
1308
799
  // TASK_1998 R3: subagent-path model override — wins over dashboard
1309
800
  // dispatchModelByRigor and the dashboard dispatchModel default. Accepts
@@ -1370,30 +861,18 @@ Examples:
1370
861
  process.exit(1);
1371
862
  }
1372
863
 
1373
- // Sub-agent mode: output JSON params for Agent tool consumption
1374
- if (dispatchModeCli === 'subagent') {
1375
- getSubagentDispatchParams({ formation, skill, task, projectPath, force, useWorktree: useWorktreeCli, dispatchModelOverride: dispatchModelCli, dryRun, harness: harnessCli }).then(result => {
1376
- if (!result) { process.exitCode = 1; return; }
1377
- process.stdout.write(JSON.stringify(result, null, 2) + '\n');
1378
- });
1379
- return;
1380
- }
1381
-
1382
- // Prompt-only mode: output raw prompt to stdout (machine-readable, no decorations)
1383
- if (promptOnly) {
1384
- const skillDef = loadSkill(formation, skill);
1385
- const loadOpts = projectPath ? { projectPath } : undefined;
1386
- const taskData = task && task.startsWith('TASK_') ? loadTask(task, loadOpts) : null;
1387
- const promptOptions = {};
1388
- if (projectPath) promptOptions.projectPath = projectPath;
1389
- if (task && task.startsWith('TASK_')) promptOptions.taskId = task;
1390
- const rawPrompt = buildPrompt(skillDef, task, taskData, promptOptions);
1391
- const text = typeof rawPrompt === 'string' ? rawPrompt : JSON.stringify(rawPrompt);
1392
- process.stdout.write(text + '\n');
1393
- return;
864
+ // Sub-agent mode (the only dispatch path): output JSON params for Agent tool consumption
865
+ if (dispatchModeCli !== 'subagent') {
866
+ console.error('❌ --dispatch-mode=subagent is required (Formation headless/squad/legion dispatch is retired)');
867
+ process.exit(1);
1394
868
  }
1395
-
1396
- dispatch({ formation, skill, task, output, dryRun, projectPath, force });
869
+ getSubagentDispatchParams({ formation, skill, task, projectPath, force, useWorktree: useWorktreeCli, dispatchModelOverride: dispatchModelCli, dryRun, harness: harnessCli }).then(result => {
870
+ if (!result) { process.exitCode = 1; return; }
871
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
872
+ }).catch(e => {
873
+ console.error('❌ ' + (e && e.message ? e.message : e));
874
+ process.exitCode = 1;
875
+ });
1397
876
  }
1398
877
 
1399
878
  // ─────────────────────────────────────────
@@ -1705,7 +1184,7 @@ async function getSubagentDispatchParams(options) {
1705
1184
  // (same two-axis gate as the Formation-dispatch path above). `rigor` and
1706
1185
  // `complexity` were both extracted in step 1 (rigor needed earlier, for
1707
1186
  // model resolution).
1708
- const dietProfile = promptProfile({ rigor, complexity });
1187
+ const dietProfile = promptProfile({ complexity });
1709
1188
  if (taskScopePaths.length > 0 && !dietProfile.skipRelatedFilesPreview) {
1710
1189
  const cortexCtx = buildCortexContext(taskScopePaths, projectPath);
1711
1190
  if (cortexCtx) promptText = promptText + '\n' + cortexCtx;
@@ -1928,4 +1407,4 @@ function setupSubagentCrossProjectWorktree(projectPath, taskInput) {
1928
1407
  };
1929
1408
  }
1930
1409
 
1931
- module.exports = { dispatch, getSubagentDispatchParams, checkDispatchScopeGate, releaseDispatchLock, resolveDispatchIsolation, _getProvenSessionId, _getOwnConversationId, _isOwnClaimMarker, _buildCortexContext: buildCortexContext };
1410
+ module.exports = { getSubagentDispatchParams, checkDispatchScopeGate, releaseDispatchLock, resolveDispatchIsolation, _getProvenSessionId, _getOwnConversationId, _isOwnClaimMarker, _buildCortexContext: buildCortexContext };
@@ -9,7 +9,6 @@ const path = require('path');
9
9
  const ROOT = path.resolve(__dirname, '..', '..');
10
10
  const MCP_RS = path.join(ROOT, 'dashboard', 'src-tauri', 'src', 'infrastructure', 'http_server', 'routes', 'mcp.rs');
11
11
  const ALLOWLIST_FILES = [
12
- '.formation/tools/runner.js',
13
12
  '.specialist/Ansuz/tools/ansuz-sdk-runner.js',
14
13
  ];
15
14
  const REMOVED = ['synapse_associate', 'synapse_audit', 'synapse_apply_usage_boosts', 'synapse_pulse', 'synapse_thought_push', 'synapse_verify', 'cortex_search'];