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
|
@@ -1,38 +1,39 @@
|
|
|
1
|
-
##
|
|
2
|
-
|
|
3
|
-
在執行任何步驟前,先從 `/Ansuz` 的 ARGUMENTS 解析 PROJECT_PATH:
|
|
4
|
-
- 格式 `[專案: X]` → PROJECT_PATH = X
|
|
5
|
-
- 格式 `--project-path X` → PROJECT_PATH = X
|
|
6
|
-
- 無此格式(cwd = core,一般情況)→ PROJECT_PATH 為空,全程不附加任何 flag,行為完全不變
|
|
7
|
-
|
|
8
|
-
PROJECT_PATH 有值時,必須下傳至:下方快速 Boot 的 ansuz-boot-fast.js 呼叫(附加 `--project-path "<PROJECT_PATH>"`)。
|
|
9
|
-
|
|
10
|
-
---
|
|
1
|
+
## 專案路徑
|
|
2
|
+
ARGUMENTS 含 `[專案: X]` 或 `--project-path X` → PROJECT_PATH = X,下方 boot 腳本與 task-api / dispatch 呼叫都附加 `--project-path "<PROJECT_PATH>"`。沒有就留空,什麼都不加。
|
|
11
3
|
|
|
12
4
|
# Ansuz boot
|
|
13
5
|
|
|
14
|
-
你是 **Ansuz**,Kenaz 的
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
6
|
+
你是 **Ansuz**,Kenaz 的 orchestrator:理解需求,依情境決定自己動手或拆任務派給 subagent,合併前在真實路徑上驗證,然後回報。先查現況、確認 scope,再動手。
|
|
7
|
+
|
|
8
|
+
## Boot(一次 Bash,無其他儀式)
|
|
9
|
+
1. Turn Context 若有 `[session] session_id=<id>`,記住 `<id>`。
|
|
10
|
+
2. 執行 `node .specialist/tools/ansuz-boot-fast.js`(有 `<id>` 加 `--conversation-id="<id>"`;有 PROJECT_PATH 加 `--project-path`)。
|
|
11
|
+
3. 把 banner 轉述給用戶(Bash 輸出在 UI 會摺疊),並依 `📋 Open cards` 直接提出議程。
|
|
12
|
+
|
|
13
|
+
## 每件工作的做法
|
|
14
|
+
- **自己做**:小而明確的修正(約 30 行內、單一檔案或少數檔案)→ 直接改,跑最近的測試,提交。
|
|
15
|
+
- **開卡派工**:中型、可平行、或高風險路徑的工作:
|
|
16
|
+
1. 用 `kenaz_task_create` 開卡(`projectPath` = 目前專案;寫清楚 requirements、acceptance criteria、scope paths)。查卡、改狀態用 `kenaz_task_list` / `kenaz_task_get` / `kenaz_task_update`。
|
|
17
|
+
2. 產生派工內容:`node .formation/tools/dispatch.js --skill=do --task=TASK_XXX --dispatch-mode=subagent`(加 `--project-path`;Codex 加 `--harness=codex`)。把輸出 JSON 的 `promptText` 交給 subagent,`agentParams` 照用(模型不要自己換)。
|
|
18
|
+
3. subagent 在 worktree 裡改完並 commit;你驗證後合併。
|
|
19
|
+
- 開卡前先確認 scope 與驗收條件對準真正的問題,不要照著錯的假設派工。
|
|
20
|
+
|
|
21
|
+
## 驗證(合併前一定做,強度跟風險相稱)
|
|
22
|
+
- 走**真實路徑**:實際執行會被使用者用到的那條路(真的安裝、真的啟動、真的收發訊息),不只看單元測試。
|
|
23
|
+
- 有**對照組**:證明測試在舊程式上會失敗,或修改前後結果不同。
|
|
24
|
+
- subagent 回報「未驗證」的項目,由你補驗或明講仍未驗證。
|
|
25
|
+
- 合併:`git merge` 後跑相關測試(或 `node .specialist/Ansuz/tools/verify-and-land.js`),用這個專案自己的測試指令。
|
|
26
|
+
- 只在 Kenaz 核心 repo 本身:Rust 測試走 `npm run test:rust` / `dev/scripts/rust-verify-worktree.js`。
|
|
27
|
+
|
|
28
|
+
## 回報
|
|
29
|
+
用用戶的語言。說清楚:做了什麼、怎麼驗證的、還有什麼沒驗證或需要用戶決定。不說「應該可以」。
|
|
30
|
+
|
|
31
|
+
## Kenaz 本身出問題時:先回報再繞過(wish / field report)
|
|
32
|
+
Kenaz 的工具、hook、安裝、peer 訊息、派工擋住或拖慢工作時(指令失敗、回傳錯的資料、少了能力、行為不如預期),**繞過之前**先呼叫 `kenaz_wish_create`。scope=kenaz 的 wish 會自動送到 Kenaz 團隊成為 field report。
|
|
33
|
+
- `projectPath`:目前專案路徑;`scope`:`kenaz`(Kenaz 本身的問題)或 `project`(只跟這個專案有關)
|
|
34
|
+
- `wish_type`:`bug`(壞了)/`feature`(缺能力,內容寫:需求、現在哪裡不夠、建議做法、還有誰受益)/`question`
|
|
35
|
+
- `content`:一行重現步驟 + 預期 vs 實際 + 版本(`npx kenaz doctor` 那幾行);附上 session id。不要貼金鑰或 token。
|
|
36
|
+
- 一個問題只報一次;報完照常用替代做法把工作完成,並在回報裡告訴用戶已送出 wish。
|
|
37
|
+
- 用戶直接說「回報這個問題」時也用同一個工具(`source`=`user`)。
|
|
38
|
+
|
|
39
|
+
需要更多規則時(用戶授權自主、派工前、合併前)讀 `.specialist/Ansuz/RULES.md`。
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// TASK_2395: kenaz init and the first turn-context call per session ask the server to register the
|
|
3
|
+
// project (POST /api/projects/register). The stub server stands in for the Rust route: it writes a
|
|
4
|
+
// temp-HOME projects.json idempotently, so the real ~/.kenaz is never touched.
|
|
5
|
+
const test = require('node:test');
|
|
6
|
+
const assert = require('node:assert/strict');
|
|
7
|
+
const fs = require('node:fs');
|
|
8
|
+
const os = require('node:os');
|
|
9
|
+
const path = require('node:path');
|
|
10
|
+
const http = require('node:http');
|
|
11
|
+
const { spawn } = require('node:child_process');
|
|
12
|
+
|
|
13
|
+
const REPO = path.resolve(__dirname, '..', '..', '..');
|
|
14
|
+
const KENAZ_MODE = path.join(REPO, 'plugins', 'kenaz', 'scripts', 'kenaz-mode.js');
|
|
15
|
+
const TURN_CONTEXT = path.join(REPO, '.claude', 'hooks', 'turn-context.js');
|
|
16
|
+
|
|
17
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'k2395-'));
|
|
18
|
+
const home = path.join(tmp, 'home');
|
|
19
|
+
const proj = path.join(tmp, 'Ashfall-game');
|
|
20
|
+
fs.mkdirSync(path.join(home, '.kenaz'), { recursive: true });
|
|
21
|
+
fs.mkdirSync(proj, { recursive: true });
|
|
22
|
+
const registryFile = path.join(home, '.kenaz', 'projects.json');
|
|
23
|
+
let posts = 0;
|
|
24
|
+
let server;
|
|
25
|
+
let port;
|
|
26
|
+
|
|
27
|
+
test.before(async () => {
|
|
28
|
+
server = http.createServer((req, res) => {
|
|
29
|
+
let body = '';
|
|
30
|
+
req.on('data', (c) => { body += c; });
|
|
31
|
+
req.on('end', () => {
|
|
32
|
+
if (req.method === 'POST' && req.url === '/api/projects/register') {
|
|
33
|
+
posts += 1;
|
|
34
|
+
const p = JSON.parse(body).projectPath;
|
|
35
|
+
let doc = { projects: [] };
|
|
36
|
+
try { doc = JSON.parse(fs.readFileSync(registryFile, 'utf8')); } catch { /* new */ }
|
|
37
|
+
const hit = doc.projects.find((x) => x.path === p);
|
|
38
|
+
if (!hit) doc.projects.push({ id: 'proj_' + doc.projects.length, name: path.basename(p), path: p });
|
|
39
|
+
fs.writeFileSync(registryFile, JSON.stringify(doc));
|
|
40
|
+
res.setHeader('Content-Type', 'application/json');
|
|
41
|
+
return res.end(JSON.stringify({ success: true, created: !hit }));
|
|
42
|
+
}
|
|
43
|
+
res.statusCode = 404;
|
|
44
|
+
res.end('{}');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
|
48
|
+
port = server.address().port;
|
|
49
|
+
});
|
|
50
|
+
test.after(() => { server.close(); fs.rmSync(tmp, { recursive: true, force: true }); });
|
|
51
|
+
|
|
52
|
+
function run(script, args, input) {
|
|
53
|
+
return new Promise((resolve) => {
|
|
54
|
+
const env = { ...process.env, HOME: home, USERPROFILE: home, CLAUDE_PROJECT_DIR: proj, KENAZ_HTTP_PORT: String(port) };
|
|
55
|
+
const child = spawn(process.execPath, [script, ...args], { cwd: proj, env });
|
|
56
|
+
let out = '';
|
|
57
|
+
child.stdout.on('data', (c) => { out += c; });
|
|
58
|
+
child.stderr.on('data', (c) => { out += c; });
|
|
59
|
+
child.on('close', () => resolve(out));
|
|
60
|
+
child.stdin.end(input || '');
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
const registered = () => JSON.parse(fs.readFileSync(registryFile, 'utf8')).projects.filter((p) => p.path === proj);
|
|
64
|
+
|
|
65
|
+
test('kenaz init registers the project, and a second init does not duplicate it', async () => {
|
|
66
|
+
await run(KENAZ_MODE, ['init']);
|
|
67
|
+
assert.equal(registered().length, 1);
|
|
68
|
+
assert.ok(fs.existsSync(path.join(proj, '.kenaz', 'project.yaml')));
|
|
69
|
+
await run(KENAZ_MODE, ['init']);
|
|
70
|
+
assert.equal(registered().length, 1);
|
|
71
|
+
assert.ok(posts >= 2, 'init asks the server every time (the server is the idempotent part)');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('turn-context registers once per session, again for a new session', async () => {
|
|
75
|
+
const before = posts;
|
|
76
|
+
const payload = (sid) => JSON.stringify({ session_id: sid });
|
|
77
|
+
await run(TURN_CONTEXT, [], payload('sess-1'));
|
|
78
|
+
assert.equal(posts, before + 1);
|
|
79
|
+
await run(TURN_CONTEXT, [], payload('sess-1'));
|
|
80
|
+
assert.equal(posts, before + 1, 'same session: no second call');
|
|
81
|
+
await run(TURN_CONTEXT, [], payload('sess-2'));
|
|
82
|
+
assert.equal(posts, before + 2, 'new session asks again');
|
|
83
|
+
assert.equal(registered().length, 1);
|
|
84
|
+
});
|
|
@@ -22,6 +22,7 @@ const path = require('path');
|
|
|
22
22
|
|
|
23
23
|
const MARKERS = [
|
|
24
24
|
'<command-name>/Ansuz</command-name>', // user typed /Ansuz
|
|
25
|
+
'<command-name>/kenaz:Ansuz</command-name>', // typed through the kenaz plugin (npm install)
|
|
25
26
|
'# Ansuz boot', // .claude/commands/Ansuz.md body since the elixir boot prompt was retired (2026-10-07)
|
|
26
27
|
'session.js prompt Ansuz --mode=assembled', // older Ansuz.md body (transcripts before 2026-10-07)
|
|
27
28
|
'# Ansuz -- Specialist Prompt', // assembled prompt handed over as a user message (headless)
|
|
@@ -88,3 +88,9 @@ test('the current Ansuz.md body (no boot prompt, 2026-10-07) is still recognised
|
|
|
88
88
|
const mention = JSON.stringify({ type: 'user', message: { role: 'user', content: 'what is in .specialist/Ansuz/RULES.md?' } });
|
|
89
89
|
assert.equal(lineIsAnsuzEntry(mention), false);
|
|
90
90
|
});
|
|
91
|
+
|
|
92
|
+
test('plugin-namespaced /kenaz:Ansuz is recognised as an Ansuz session', () => {
|
|
93
|
+
const { lineIsAnsuzEntry } = require('./_ansuz-conversation');
|
|
94
|
+
const line = JSON.stringify({ type: 'user', message: { role: 'user', content: '<command-name>/kenaz:Ansuz</command-name>' } });
|
|
95
|
+
assert.strictEqual(lineIsAnsuzEntry(line), true);
|
|
96
|
+
});
|
|
@@ -33,14 +33,10 @@ const path = require('path');
|
|
|
33
33
|
const { Readable } = require('stream');
|
|
34
34
|
|
|
35
35
|
const DEFAULT_HOOKS = [
|
|
36
|
-
'handoff-staleness.js',
|
|
37
|
-
'fatigue-check.js',
|
|
38
36
|
'daemon-outbox-hook.js',
|
|
39
37
|
// TASK_2218: ansuz-session-pin.js is called from turn-context.js (every install layout runs that hook).
|
|
40
38
|
'turn-context.js',
|
|
41
39
|
'task-bridge.js',
|
|
42
|
-
'lease-touch.js',
|
|
43
|
-
'stream-delta.js',
|
|
44
40
|
];
|
|
45
41
|
|
|
46
42
|
const HARD_CAP_MS = 10000;
|
|
@@ -69,12 +69,11 @@ test('runner replays stdin, traps exits, concatenates stdout, exits 0', () => {
|
|
|
69
69
|
assert.ok(elapsed < 5000, 'runner drained promptly, took ' + elapsed + ' ms');
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
-
test('default list is the
|
|
72
|
+
test('default list is the three UserPromptSubmit hooks in settings order', () => {
|
|
73
73
|
// TASK_2218: the pin writer is invoked from turn-context.js, not listed here.
|
|
74
74
|
const { DEFAULT_HOOKS } = require(RUNNER);
|
|
75
75
|
assert.deepStrictEqual(DEFAULT_HOOKS, [
|
|
76
|
-
'
|
|
77
|
-
'task-bridge.js', 'lease-touch.js', 'stream-delta.js',
|
|
76
|
+
'daemon-outbox-hook.js', 'turn-context.js', 'task-bridge.js',
|
|
78
77
|
]);
|
|
79
78
|
for (const f of DEFAULT_HOOKS) assert.ok(fs.existsSync(path.join(__dirname, f)), f + ' exists');
|
|
80
79
|
});
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* (HANDOFF_STALE_AFTER_MS) — see TASK_1837/TASK_1838.
|
|
21
21
|
*
|
|
22
22
|
* Dispatch detection (TASK_1830 measurement — two routes, two trace families):
|
|
23
|
-
* 1.
|
|
23
|
+
* 1. Formation dispatch route → .formation/.active-dispatches.json
|
|
24
24
|
* (written by Rust, kenaz.rs). Kept for that route.
|
|
25
25
|
* 2. sub-agent route (Agent tool — the DEFAULT since 2026-06-09) never touches
|
|
26
26
|
* that file. Its in-project trace is the dispatch brief
|
|
@@ -104,7 +104,7 @@ try {
|
|
|
104
104
|
const { toolUseMentionsAny, taskIdNeedles } = require('./_ansuz-conversation');
|
|
105
105
|
const candidateNeedles = [];
|
|
106
106
|
try {
|
|
107
|
-
// Route 1:
|
|
107
|
+
// Route 1: Formation registry (entries carry taskId).
|
|
108
108
|
if (fs.existsSync(dispatchFile)) {
|
|
109
109
|
const dispatches = JSON.parse(fs.readFileSync(dispatchFile, 'utf8'));
|
|
110
110
|
if (Array.isArray(dispatches)) {
|
|
@@ -93,15 +93,61 @@ try {
|
|
|
93
93
|
state = { last_ts: 0, last_event_count: 0 };
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
// ── TASK_2395: register this project in ~/.kenaz/projects.json (first call per session) ──
|
|
97
|
+
// Peer routing by to_project only knows registered projects; under kenaz-server nothing else
|
|
98
|
+
// registers them. The registry write itself is the server's (POST /api/projects/register, one
|
|
99
|
+
// Rust writer, idempotent); this only asks, once per session, and remembers success.
|
|
100
|
+
// Per machine, not per project: a file under <project>/.kenaz would show up in repos that do not
|
|
101
|
+
// ignore .kenaz/specialist (e.g. a fresh `kenaz init` project).
|
|
102
|
+
const REGISTERED_PATH = path.join(require('os').homedir(), '.kenaz', 'registered_sessions.json');
|
|
103
|
+
const REGISTERED_KEY = (sid) => projectDir.split(path.sep).join('/').toLowerCase() + '|' + sid;
|
|
104
|
+
function registerProjectOnce() {
|
|
105
|
+
return new Promise((resolve) => {
|
|
106
|
+
try {
|
|
107
|
+
const sid = peerHookPayload.session_id ? String(peerHookPayload.session_id) : '';
|
|
108
|
+
if (!sid) return resolve();
|
|
109
|
+
let seen = [];
|
|
110
|
+
try { seen = JSON.parse(fs.readFileSync(REGISTERED_PATH, 'utf8')).sessions || []; } catch { /* first run */ }
|
|
111
|
+
if (seen.includes(REGISTERED_KEY(sid))) return resolve();
|
|
112
|
+
const body = JSON.stringify({ projectPath: projectDir });
|
|
113
|
+
const req = http.request({
|
|
114
|
+
host: '127.0.0.1',
|
|
115
|
+
port: Number(process.env.KENAZ_HTTP_PORT || process.env.KENAZ_BACKEND_PORT || 18080),
|
|
116
|
+
path: '/api/projects/register',
|
|
117
|
+
method: 'POST',
|
|
118
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
|
|
119
|
+
timeout: 800,
|
|
120
|
+
}, (res) => {
|
|
121
|
+
let raw = '';
|
|
122
|
+
res.on('data', (c) => { raw += c; });
|
|
123
|
+
res.on('end', () => {
|
|
124
|
+
try {
|
|
125
|
+
if (JSON.parse(raw).success === true) {
|
|
126
|
+
fs.mkdirSync(path.dirname(REGISTERED_PATH), { recursive: true });
|
|
127
|
+
fs.writeFileSync(REGISTERED_PATH, JSON.stringify({ sessions: seen.concat(REGISTERED_KEY(sid)).slice(-200) }));
|
|
128
|
+
}
|
|
129
|
+
} catch { /* retried next turn */ }
|
|
130
|
+
resolve();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
req.on('timeout', () => req.destroy());
|
|
134
|
+
req.on('error', () => resolve());
|
|
135
|
+
req.end(body);
|
|
136
|
+
} catch { resolve(); }
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
const registeredP = registerProjectOnce();
|
|
140
|
+
|
|
96
141
|
// ── Cooldown check ───────────────────────────────────────────────────────────
|
|
97
142
|
const now = Date.now();
|
|
98
|
-
|
|
143
|
+
const inCooldown = now - state.last_ts < TURN_CONTEXT_COOLDOWN_MS;
|
|
144
|
+
if (inCooldown) {
|
|
99
145
|
// The cooldown throttles the peer pull only; the session line must still reach
|
|
100
146
|
// the model on every turn (a second session in the same cwd lands here).
|
|
101
147
|
if (peerHookPayload.session_id) {
|
|
102
148
|
process.stdout.write('\n' + fenceSignal('context', '[Turn Context]\n[session] session_id=' + String(peerHookPayload.session_id)) + '\n');
|
|
103
149
|
}
|
|
104
|
-
process.exit(0);
|
|
150
|
+
registeredP.then(() => process.exit(0));
|
|
105
151
|
}
|
|
106
152
|
|
|
107
153
|
// ── Helper: HTTP GET returning a promise ─────────────────────────────────────
|
|
@@ -142,6 +188,8 @@ const sessionLine = peerHookPayload.session_id
|
|
|
142
188
|
|
|
143
189
|
// ── Main async flow ──────────────────────────────────────────────────────────
|
|
144
190
|
(async () => {
|
|
191
|
+
if (inCooldown) return; // the cooldown branch above exits after the registration settles
|
|
192
|
+
await registeredP;
|
|
145
193
|
// 1. Synapse turn-context: retired with Kuzu (0.2.266) -- the endpoint only returns
|
|
146
194
|
// empty text. Not fetched any more: a slow or unreachable backend used to exit
|
|
147
195
|
// here and skip the peer pull below.
|
|
@@ -33,16 +33,6 @@
|
|
|
33
33
|
]
|
|
34
34
|
}
|
|
35
35
|
],
|
|
36
|
-
"Stop": [
|
|
37
|
-
{
|
|
38
|
-
"hooks": [
|
|
39
|
-
{
|
|
40
|
-
"type": "command",
|
|
41
|
-
"command": "node -e \"const d=process.env.CLAUDE_PROJECT_DIR||process.cwd();process.chdir(d);require(d+'/.claude/hooks/lease-release.js')\""
|
|
42
|
-
}
|
|
43
|
-
]
|
|
44
|
-
}
|
|
45
|
-
],
|
|
46
36
|
"UserPromptSubmit": [
|
|
47
37
|
{
|
|
48
38
|
"hooks": [
|
|
@@ -82,33 +72,6 @@
|
|
|
82
72
|
}
|
|
83
73
|
],
|
|
84
74
|
"matcher": "TaskCreate|TaskUpdate"
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
"hooks": [
|
|
88
|
-
{
|
|
89
|
-
"type": "command",
|
|
90
|
-
"command": "node -e \"const d=process.env.CLAUDE_PROJECT_DIR;process.chdir(d);require(d+'/.claude/hooks/handoff-snapshot-on-commit.js')\""
|
|
91
|
-
}
|
|
92
|
-
],
|
|
93
|
-
"matcher": "Bash"
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
"hooks": [
|
|
97
|
-
{
|
|
98
|
-
"type": "command",
|
|
99
|
-
"command": "node -e \"const d=process.env.CLAUDE_PROJECT_DIR;process.chdir(d);require(d+'/.claude/hooks/handoff-snapshot-on-question.js')\""
|
|
100
|
-
}
|
|
101
|
-
],
|
|
102
|
-
"matcher": "AskUserQuestion"
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
"hooks": [
|
|
106
|
-
{
|
|
107
|
-
"type": "command",
|
|
108
|
-
"command": "node -e \"const d=process.env.CLAUDE_PROJECT_DIR;process.chdir(d);require(d+'/.claude/hooks/handoff-snapshot-on-decision.js')\""
|
|
109
|
-
}
|
|
110
|
-
],
|
|
111
|
-
"matcher": "mcp__kenaz-dashboard__synapse_inscribe"
|
|
112
75
|
}
|
|
113
76
|
]
|
|
114
77
|
},
|
|
@@ -1589,17 +1589,12 @@ class TaskAPI {
|
|
|
1589
1589
|
console.warn('WARNING: overriding acceptance_criteria required for code-touching task via --force');
|
|
1590
1590
|
}
|
|
1591
1591
|
|
|
1592
|
-
//
|
|
1592
|
+
// 品質提示(2026-10-10 起只警告、不阻擋;ANSUZ_SLIM_PLAN):數量門檻擋卡的成本高於價值。
|
|
1593
|
+
// 仍強制的只有上方「改到程式碼的卡至少一條 acceptance_criteria」。
|
|
1593
1594
|
if (!options.force) {
|
|
1594
1595
|
const quality = this.validateTaskQuality(taskData);
|
|
1595
1596
|
if (!quality.valid) {
|
|
1596
|
-
|
|
1597
|
-
'❌ 任務品質不足,無法建立:',
|
|
1598
|
-
...quality.warnings.map(w => ` - ${w}`),
|
|
1599
|
-
'',
|
|
1600
|
-
'提示:補齊欄位後重試,或使用 --force 強制建立'
|
|
1601
|
-
].join('\n');
|
|
1602
|
-
throw new Error(msg);
|
|
1597
|
+
console.warn(['⚠️ 任務內容偏少(仍會建立):', ...quality.warnings.map(w => ` - ${w}`)].join('\n'));
|
|
1603
1598
|
}
|
|
1604
1599
|
}
|
|
1605
1600
|
|
|
@@ -2116,8 +2111,7 @@ class TaskAPI {
|
|
|
2116
2111
|
// TASK_2264: decide up front whether post-complete (below) will measure 'after' in-process.
|
|
2117
2112
|
// TASK_2288 single-winner rule: whenever a post-complete run is expected for this completion,
|
|
2118
2113
|
// post-complete's own measurement is the ONE 'after' capture (it runs after merge and feeds the
|
|
2119
|
-
// verdict), so moveTask must not also spawn one. Expected = in-process (below)
|
|
2120
|
-
// (headless.js / scheduler, signalled by FORMATION_HEADLESS / FORMATION_SCHEDULER) runs it itself.
|
|
2114
|
+
// verdict), so moveTask must not also spawn one. Expected = in-process (below).
|
|
2121
2115
|
// moveTask's detached capture stays the sole one only when nobody measures: --skip-post-complete.
|
|
2122
2116
|
const postCompleteRuns = !options.skipPostComplete;
|
|
2123
2117
|
const result = await this.moveTask(taskId, 'done', updatedTask, { skipHealthAfter: postCompleteRuns });
|
|
@@ -2140,8 +2134,6 @@ class TaskAPI {
|
|
|
2140
2134
|
}
|
|
2141
2135
|
|
|
2142
2136
|
// 自動執行 post-complete(除非明確跳過)
|
|
2143
|
-
// - FORMATION_SCHEDULER: 由 scheduler 統一管理,避免重複執行
|
|
2144
|
-
// - FORMATION_HEADLESS: 由 headless.js onDone 統一執行,避免並行 agent git 衝突
|
|
2145
2137
|
if (postCompleteRuns) {
|
|
2146
2138
|
try {
|
|
2147
2139
|
const { postComplete } = require('../../.formation/tools/post-complete.js');
|
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
# Formation System
|
|
2
2
|
|
|
3
|
-
Agent
|
|
3
|
+
Subagent 調度。唯一的 live 路徑:`dispatch.js --dispatch-mode=subagent` 印出 JSON(`promptText` + `agentParams`),由 Ansuz 交給 Agent tool(或 `--harness=codex` 的 Codex `spawn_agent`)執行。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Headless(`claude -p`、`kenaz_dispatch`)、Squad、Legion、Legacy scheduler 已退役;`squad/`、`legion/` 的 YAML 只是歷史定義,`dispatch.js` 遇到會直接報錯結束。
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
Unit → Solo → Squad → Legion
|
|
9
|
-
1×1 1×N N×1 Native Team
|
|
10
|
-
最輕量 最靈活 真並行 最重火力
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
---
|
|
7
|
+
## 陣型
|
|
14
8
|
|
|
15
9
|
### Unit
|
|
16
10
|
|
|
@@ -18,138 +12,37 @@ Unit → Solo → Squad → Legion
|
|
|
18
12
|
|
|
19
13
|
派出一個 subagent 執行單一任務,完成後回報結果。不切換角色、不協作,純粹的「去做這件事」。
|
|
20
14
|
|
|
21
|
-
- 機制:`Task` 工具,單次調用
|
|
22
|
-
- 成本:最低
|
|
23
15
|
- 適用:明確的單一任務、Bug 修復、檔案修改、腳本執行
|
|
24
|
-
|
|
25
|
-
---
|
|
16
|
+
- 可用 skill:`do`、`think`、`plan`
|
|
26
17
|
|
|
27
18
|
### Solo
|
|
28
19
|
|
|
29
20
|
**1 agent, N perspectives** — 單人多視角自主切換。
|
|
30
21
|
|
|
31
|
-
|
|
22
|
+
一個 subagent,同時裝備多個專業視角,由 agent 自主決定何時切換。
|
|
32
23
|
|
|
33
|
-
- 機制:`dispatch.js --formation=solo
|
|
34
|
-
- 成本:低(單次 subagent 調用)
|
|
24
|
+
- 機制:`dispatch.js --formation=solo`
|
|
35
25
|
- 適用:策略分析、程式碼審查、方案評估、設計決策
|
|
36
26
|
- 可用 skill:`strategy`(策略分析)、`diagnosis`(問題診斷)
|
|
37
27
|
|
|
38
|
-
```
|
|
39
|
-
範例:Solo + Strategy
|
|
40
|
-
→ [技術專家] 分析架構影響
|
|
41
|
-
→ 發現成本問題 → [產品策略師] 評估 ROI
|
|
42
|
-
→ 發現安全疑慮 → [風險分析師] 識別風險
|
|
43
|
-
→ 綜合判斷,產出結論
|
|
44
|
-
(視角切換由 agent 自主決定,非固定順序)
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
---
|
|
48
|
-
|
|
49
|
-
### Squad
|
|
50
|
-
|
|
51
|
-
**N agents, 1 task** — 多 agent 並行協作。
|
|
52
|
-
|
|
53
|
-
同時派出多個 subagent,各自負責任務的不同部分,並行執行後整合成果。成員之間不能互相通訊(那是 Legion 的事),各自獨立完成後由 lead 整合。
|
|
54
|
-
|
|
55
|
-
- 機制:`dispatch.js --formation=squad`,並行啟動多個 Claude 進程
|
|
56
|
-
- 成本:中等(隨 agent 數量線性增長)
|
|
57
|
-
- 適用:並行子任務開發、跨模組批量修改、多區域同步掃蕩
|
|
58
|
-
- 可用 skill:`blitz`(通用並行拆分)、`sweep`(同規則多範圍掃蕩)
|
|
59
|
-
|
|
60
|
-
**核心約束**:
|
|
61
|
-
- 成員之間 **不通訊**,只靠 `shared_context`(啟動前定義的共享約定)協調
|
|
62
|
-
- 每個成員的 `scope_paths` 不得重疊(避免檔案衝突)
|
|
63
|
-
- 如果子任務之間有依賴關係,不適合用 Squad
|
|
64
|
-
|
|
65
|
-
```
|
|
66
|
-
範例 1:Squad + blitz(並行拆分)
|
|
67
|
-
├── [part_1] → 實作模組 A 的功能
|
|
68
|
-
├── [part_2] → 實作模組 B 的功能
|
|
69
|
-
└── [part_3] → 實作模組 C 的功能
|
|
70
|
-
→ lead 整合驗證,統一提交
|
|
71
|
-
|
|
72
|
-
範例 2:Squad + sweep(同規則掃蕩)
|
|
73
|
-
├── [zone_1] → 重構 cmd_modules/ 下的模組
|
|
74
|
-
├── [zone_2] → 重構 components/ 下的元件
|
|
75
|
-
└── [zone_3] → 重構 lib/ 下的工具函數
|
|
76
|
-
→ lead 驗證一致性,統一提交
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
---
|
|
80
|
-
|
|
81
|
-
### Legion
|
|
82
|
-
|
|
83
|
-
**Native Team** — 即時溝通的完整團隊。
|
|
84
|
-
|
|
85
|
-
使用 Claude Code 原生的 TeamCreate 機制,建立持久化團隊。成員之間可互相通訊、即時協調。Legion 的核心價值不是「更多人」,而是「即時溝通」— 只在成員的工作互相影響、需要即時協調時使用。
|
|
86
|
-
|
|
87
|
-
- 機制:`TeamCreate` + `SendMessage`
|
|
88
|
-
- 成本:最高(持久化 context + 通訊開銷)
|
|
89
|
-
- 適用:有順序依賴、回饋循環、或高度耦合的複雜任務
|
|
90
|
-
- 可用 skill:`forge`(順序流水線)、`hive`(網狀即時協調)
|
|
91
|
-
|
|
92
|
-
**核心價值**(Squad 做不到的事):
|
|
93
|
-
- 即時通知「我改了 X,你要更新」
|
|
94
|
-
- 回饋循環(review → fix → re-review)
|
|
95
|
-
- 順序依賴(A 做完 B 才開始)
|
|
96
|
-
- 動態調整(做到一半改方向)
|
|
97
|
-
|
|
98
|
-
```
|
|
99
|
-
範例 1:Legion + forge(鍛造)
|
|
100
|
-
Designer ──→ Implementer(s) ──→ Reviewer
|
|
101
|
-
↑ 回饋修正 ←──────────────────↓
|
|
102
|
-
→ 設計 → 打造 → 淬煉,有序且有品質把關
|
|
103
|
-
|
|
104
|
-
範例 2:Legion + hive(蜂巢)
|
|
105
|
-
Worker A ←→ Worker B
|
|
106
|
-
↕ ↕
|
|
107
|
-
Worker C ←→ Worker D
|
|
108
|
-
→ 所有人即時互聯,任何變更即時同步
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
---
|
|
112
|
-
|
|
113
|
-
## 選擇指南
|
|
114
|
-
|
|
115
|
-
| 問自己 | 答案 | 陣型 |
|
|
116
|
-
|--------|------|------|
|
|
117
|
-
| 任務簡單、目標明確? | Yes | **Unit** |
|
|
118
|
-
| 需要多角度分析? | Yes | **Solo** |
|
|
119
|
-
| 需要同時做多件事? | Yes | **Squad** |
|
|
120
|
-
| 有順序依賴或回饋循環? | Yes | **Legion** (forge) |
|
|
121
|
-
| 工作高度耦合、需即時協調? | Yes | **Legion** (hive) |
|
|
122
|
-
|
|
123
28
|
---
|
|
124
29
|
|
|
125
30
|
## 啟動方式
|
|
126
31
|
|
|
127
|
-
### Headless Mode(推薦)
|
|
128
|
-
|
|
129
|
-
由 Formation Lead 配好任務後直接呼叫,subagent 不問問題:
|
|
130
|
-
|
|
131
32
|
```bash
|
|
132
|
-
# Unit
|
|
133
|
-
node .formation/tools/
|
|
33
|
+
# Unit
|
|
34
|
+
node .formation/tools/dispatch.js --skill=do --task=TASK_172 --dispatch-mode=subagent
|
|
134
35
|
|
|
135
|
-
# Solo
|
|
136
|
-
node .formation/tools/
|
|
36
|
+
# Solo
|
|
37
|
+
node .formation/tools/dispatch.js --formation=solo --skill=strategy --task=TASK_172 --dispatch-mode=subagent
|
|
137
38
|
|
|
138
|
-
#
|
|
139
|
-
node .formation/tools/
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
node .formation/tools/headless.js --formation=legion --skill=forge --task=TASK_202
|
|
143
|
-
|
|
144
|
-
# Dry run(只看 prompt,不啟動)
|
|
145
|
-
node .formation/tools/headless.js --formation=legion --skill=forge --task=TASK_202 --dry-run
|
|
39
|
+
# Codex harness / 子專案 / 只看 payload 不產生副作用
|
|
40
|
+
node .formation/tools/dispatch.js --skill=do --task=TASK_172 --dispatch-mode=subagent --harness=codex
|
|
41
|
+
node .formation/tools/dispatch.js --skill=do --task=TASK_172 --dispatch-mode=subagent --project-path <path>
|
|
42
|
+
node .formation/tools/dispatch.js --skill=do --task=TASK_172 --dispatch-mode=subagent --dry-run
|
|
146
43
|
```
|
|
147
44
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
```bash
|
|
151
|
-
node .formation/tools/dispatch.js --skill=do --task=TASK_172
|
|
152
|
-
```
|
|
45
|
+
缺 `--dispatch-mode=subagent`、或指定 squad/legion skill,都會以非零 exit code 結束並印出原因。
|
|
153
46
|
|
|
154
47
|
---
|
|
155
48
|
|
|
@@ -158,31 +51,13 @@ node .formation/tools/dispatch.js --skill=do --task=TASK_172
|
|
|
158
51
|
```
|
|
159
52
|
.formation/
|
|
160
53
|
├── README.md # 本文件
|
|
161
|
-
├── lead.yaml # Formation Lead
|
|
54
|
+
├── lead.yaml # Formation Lead
|
|
162
55
|
├── tools/
|
|
163
|
-
│ ├──
|
|
164
|
-
│ ├── dispatch.js # CLI 入口(互動式)
|
|
56
|
+
│ ├── dispatch.js # CLI 入口(--dispatch-mode=subagent)
|
|
165
57
|
│ ├── loader.js # YAML/Task 載入
|
|
166
|
-
│ ├── prompt.js #
|
|
167
|
-
│ ├──
|
|
168
|
-
│ ├── logger.js # Log 格式化
|
|
58
|
+
│ ├── prompt.js # Subagent prompt 組裝 + 傳輸前綴
|
|
59
|
+
│ ├── post-complete.js # 完成後 merge / 驗證
|
|
169
60
|
│ └── utils.js # 工具函數
|
|
170
|
-
├──
|
|
171
|
-
|
|
172
|
-
│ ├── solo/
|
|
173
|
-
│ ├── squad/
|
|
174
|
-
│ └── legion/
|
|
175
|
-
├── unit/ # Unit 陣型定義
|
|
176
|
-
│ ├── do.yaml # 執行者
|
|
177
|
-
│ ├── think.yaml # 分析者
|
|
178
|
-
│ └── plan.yaml # 規劃者
|
|
179
|
-
├── solo/ # Solo 陣型定義(含視角集)
|
|
180
|
-
│ ├── strategy.yaml # 策略分析(6 視角)
|
|
181
|
-
│ └── diagnosis.yaml # 問題診斷(4 視角)
|
|
182
|
-
├── squad/ # Squad 陣型定義(含協作模板)
|
|
183
|
-
│ ├── blitz.yaml # 通用並行拆分
|
|
184
|
-
│ └── sweep.yaml # 同規則多範圍掃蕩
|
|
185
|
-
└── legion/ # Legion 陣型定義(含溝通模式)
|
|
186
|
-
├── forge.yaml # 順序流水線(設計→打造→淬煉)
|
|
187
|
-
└── hive.yaml # 網狀即時協調(全連接)
|
|
61
|
+
├── unit/ # do / think / plan
|
|
62
|
+
└── solo/ # strategy / diagnosis
|
|
188
63
|
```
|