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.
- package/core/.claude/commands/Ansuz.md +36 -35
- package/core/.claude/hooks/__tests__/task-2395-project-registration.test.js +84 -0
- package/core/.claude/hooks/_ansuz-conversation.js +1 -0
- package/core/.claude/hooks/_ansuz-conversation.test.js +6 -0
- package/core/.claude/hooks/_run-user-prompt.js +0 -4
- package/core/.claude/hooks/_run-user-prompt.test.js +2 -3
- package/core/.claude/hooks/handoff-staleness.js +2 -2
- package/core/.claude/hooks/turn-context.js +50 -2
- package/core/.claude/settings.json +0 -37
- package/core/.collaboration/core/task-api.js +4 -12
- package/core/.formation/README.md +21 -146
- package/core/.formation/bifrost/codex-doctor.js +37 -2
- package/core/.formation/bifrost/codex-doctor.test.js +18 -0
- package/core/.formation/bifrost/launch.js +1 -1
- package/core/.formation/bifrost/openai/codex-hook-shim.js +6 -0
- package/core/.formation/bifrost/openai/codex-hook-shim.test.js +11 -1
- package/core/.formation/bifrost/setup-codex.js +3 -0
- package/core/.formation/spec/task_operations.parity.test.js +0 -1
- package/core/.formation/tools/dashboard-models-chain.test.js +0 -6
- package/core/.formation/tools/dispatch-detail/cross-project-worktree.md +1 -9
- package/core/.formation/tools/dispatch-detail/worktree-subagent-mode.md +2 -2
- package/core/.formation/tools/dispatch.js +28 -549
- package/core/.formation/tools/mcp-allowlist.test.js +0 -1
- package/core/.formation/tools/post-complete/task2345.test.js +0 -5
- package/core/.formation/tools/pre-dispatch-snapshot.test.js +0 -100
- package/core/.formation/tools/prompt.js +66 -1607
- package/core/.formation/tools/prompt.test.js +43 -582
- package/core/.kenaz/claude-hooks-manifest.json +16 -16
- package/core/.specialist/Ansuz/RULES.md +6 -10
- package/core/.specialist/tools/__tests__/freya-cli-contract.test.js +0 -1
- package/core/.specialist/tools/__tests__/task-2218-boot-session-id.test.js +17 -1
- package/core/.specialist/tools/__tests__/task-2222-session-identity-interleave.test.js +17 -1
- package/core/.specialist/tools/__tests__/task-2254-handoff-archive-contract.test.js +6 -5
- package/core/.specialist/tools/ansuz-boot-fast.js +44 -100
- package/core/.specialist/tools/ansuz-boot-fast.test.js +45 -1
- package/core/KENAZ_CORE_VERSION +1 -1
- package/core/dev/plans/ANSUZ_SLIM_PLAN.md +68 -0
- package/core/plugins/kenaz/commands/Amelia.md +1 -1
- package/core/plugins/kenaz/commands/Ansuz.md +36 -35
- package/core/plugins/kenaz/hooks/hooks.json +1 -5
- package/core/plugins/kenaz/hooks/kenaz-hook.js +0 -4
- package/core/plugins/kenaz/scripts/kenaz-mode.js +11 -1
- package/core/plugins/kenaz/scripts/kenaz.test.js +1 -1
- package/core/plugins/kenaz/scripts/sync.js +6 -2
- package/core/plugins/kenaz/scripts/ygg.js +13 -1
- package/core/plugins/kenaz/skills/product_manager/SKILL.md +12 -12
- package/lib/cli.js +47 -6
- package/lib/cli.test.js +24 -0
- package/lib/dashboard.js +10 -0
- package/package.json +11 -7
- package/vendor/install-kenaz-cli.js +18 -7
- package/core/.formation/tools/broadcast.js +0 -103
- package/core/.formation/tools/dispatch-dryrun.test.js +0 -54
- package/core/.formation/tools/dispatch-notifier.js +0 -84
- package/core/.formation/tools/formations.js +0 -1203
- package/core/.formation/tools/headless-branch-recovery.test.js +0 -64
- package/core/.formation/tools/headless-dryrun.test.js +0 -79
- package/core/.formation/tools/headless-v2.js +0 -659
- package/core/.formation/tools/headless.js +0 -1071
- package/core/.formation/tools/legacy-scheduler.js +0 -1994
- package/core/.formation/tools/logger.js +0 -273
- package/core/.formation/tools/memory-loader.js +0 -422
- package/core/.formation/tools/process-state.js +0 -33
- package/core/.formation/tools/remote-conflict.js +0 -98
- package/core/.formation/tools/runner.js +0 -1332
- package/core/.formation/tools/task-utils.js +0 -344
- package/core/.formation/tools/task2028-headless-worktree.test.js +0 -112
- package/core/.formation/tools/tests/headless-mcp-args-plumbing.test.js +0 -123
- package/core/.formation/tools/worktree-leak.test.js +0 -162
- package/core/.specialist/tools/__tests__/task-1863-boot-lease-reuse.test.js +0 -99
- 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
|
-
*
|
|
6
|
-
*
|
|
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 --
|
|
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
|
-
|
|
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
|
-
//
|
|
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>
|
|
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
|
-
--
|
|
1238
|
-
--
|
|
1239
|
-
--
|
|
1240
|
-
--
|
|
1241
|
-
--dispatch-
|
|
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).
|
|
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
|
-
|
|
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
|
|
1375
|
-
|
|
1376
|
-
|
|
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
|
-
|
|
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({
|
|
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 = {
|
|
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'];
|