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
|
@@ -12,10 +12,7 @@
|
|
|
12
12
|
],
|
|
13
13
|
"PostToolUse": [
|
|
14
14
|
{ "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/kenaz-hook.js\" flow-post" }] },
|
|
15
|
-
{ "matcher": "TaskCreate|TaskUpdate", "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/kenaz-hook.js\" task-bridge" }] }
|
|
16
|
-
{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/kenaz-hook.js\" snapshot-commit" }] },
|
|
17
|
-
{ "matcher": "AskUserQuestion", "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/kenaz-hook.js\" snapshot-question" }] },
|
|
18
|
-
{ "matcher": "mcp__kenaz-dashboard__synapse_inscribe", "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/kenaz-hook.js\" snapshot-decision" }] }
|
|
15
|
+
{ "matcher": "TaskCreate|TaskUpdate", "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/kenaz-hook.js\" task-bridge" }] }
|
|
19
16
|
],
|
|
20
17
|
"PostToolUseFailure": [{ "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/kenaz-hook.js\" flow-post" }] }],
|
|
21
18
|
"Notification": [{ "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/kenaz-hook.js\" flow-post" }] }],
|
|
@@ -24,7 +21,6 @@
|
|
|
24
21
|
"SessionEnd": [{ "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/kenaz-hook.js\" flow-post" }] }],
|
|
25
22
|
"Stop": [{ "hooks": [
|
|
26
23
|
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/kenaz-hook.js\" flow-post" },
|
|
27
|
-
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/kenaz-hook.js\" lease-release" },
|
|
28
24
|
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/kenaz-hook.js\" session-stop" }
|
|
29
25
|
] }]
|
|
30
26
|
}
|
|
@@ -27,14 +27,10 @@ const HOOKS = {
|
|
|
27
27
|
'session-start': { kind: 'session-start', minMode: 'assist', backend: true },
|
|
28
28
|
'guidance': { kind: 'guidance', minMode: 'full' },
|
|
29
29
|
'prompt-runner': { kind: 'script', file: '_run-user-prompt.js', minMode: 'full' },
|
|
30
|
-
'lease-release': { kind: 'script', file: 'lease-release.js', minMode: 'full' },
|
|
31
30
|
'session-stop': { kind: 'core-script', rel: '.specialist/tools/session.js', args: ['stop'], minMode: 'full' },
|
|
32
31
|
'synapse-first': { kind: 'script', file: 'synapse-first-investigation.js', minMode: 'full' },
|
|
33
32
|
'memory-guard': { kind: 'script', file: 'memory-guard.js', call: 'main', minMode: 'full' },
|
|
34
33
|
'task-bridge': { kind: 'script', file: 'task-bridge.js', minMode: 'assist' },
|
|
35
|
-
'snapshot-commit': { kind: 'script', file: 'handoff-snapshot-on-commit.js', minMode: 'assist' },
|
|
36
|
-
'snapshot-question': { kind: 'script', file: 'handoff-snapshot-on-question.js', minMode: 'assist' },
|
|
37
|
-
'snapshot-decision': { kind: 'script', file: 'handoff-snapshot-on-decision.js', minMode: 'assist' },
|
|
38
34
|
};
|
|
39
35
|
|
|
40
36
|
function readStdin() {
|
|
@@ -26,7 +26,17 @@ if (MODES.includes(arg)) {
|
|
|
26
26
|
// TASK_2367: first-run guide. Optional steps are offered, never forced; the user (or the
|
|
27
27
|
// assistant relaying this output) runs them. Backend probe tells whether they can work now.
|
|
28
28
|
const port = process.env.KENAZ_HTTP_PORT || process.env.KENAZ_BACKEND_PORT || '18080';
|
|
29
|
-
|
|
29
|
+
// TASK_2395: register the project in ~/.kenaz/projects.json (idempotent, written by the server's
|
|
30
|
+
// single registry writer) so peer routing by to_project finds it. Server down = skipped; the
|
|
31
|
+
// first turn of a session retries.
|
|
32
|
+
const registered = fetch('http://127.0.0.1:' + port + '/api/projects/register', {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: { 'Content-Type': 'application/json' },
|
|
35
|
+
body: JSON.stringify({ projectPath: dir }),
|
|
36
|
+
signal: AbortSignal.timeout(2000),
|
|
37
|
+
}).then((r) => r.json(), () => null).then((j) => !!(j && j.success), () => false);
|
|
38
|
+
registered
|
|
39
|
+
.then(() => fetch('http://127.0.0.1:' + port + '/api/health', { signal: AbortSignal.timeout(2000) }))
|
|
30
40
|
.then((r) => r.ok, () => false)
|
|
31
41
|
.then((up) => {
|
|
32
42
|
// Codex runs shell commands in a sandbox without network (it sets CODEX_SANDBOX_NETWORK_DISABLED=1),
|
|
@@ -27,7 +27,7 @@ test('full prints guidance (control for off)', () => {
|
|
|
27
27
|
|
|
28
28
|
test('off: zero output from every hook, exit 0', () => {
|
|
29
29
|
const dir = proj();
|
|
30
|
-
for (const n of ['guidance', 'prompt-runner', 'memory-guard', 'synapse-first', 'flow-post', 'session-start'
|
|
30
|
+
for (const n of ['guidance', 'prompt-runner', 'memory-guard', 'synapse-first', 'flow-post', 'session-start']) {
|
|
31
31
|
const r = hook(n, dir, { KENAZ_MODE: 'off' });
|
|
32
32
|
assert.strictEqual(r.status, 0, n);
|
|
33
33
|
assert.strictEqual(r.stdout + r.stderr, '', n);
|
|
@@ -19,11 +19,15 @@ const SKILLS = [
|
|
|
19
19
|
const COMMANDS = ['Amelia', 'Ansuz', 'architect', 'incident', 'review', 'security', 'spec', 'strategy', '產品', '設計', '評估'];
|
|
20
20
|
|
|
21
21
|
// Core-relative references become plugin-relative / core-resolved.
|
|
22
|
+
// In a user's project (npm install) the core lives in ~/.kenaz/core, not the project folder, so a
|
|
23
|
+
// command's `node .specialist/...` / `.formation/...` / `.collaboration/...` runs through core-resolve,
|
|
24
|
+
// and a core doc it tells the model to read gets a path under the plugin's core (plugin root/../..).
|
|
22
25
|
function rewrite(text) {
|
|
23
26
|
return text
|
|
24
27
|
.replace(/\.claude\/skills\//g, '${CLAUDE_PLUGIN_ROOT}/skills/')
|
|
25
|
-
.replace(/node \.specialist\/
|
|
26
|
-
'node "${CLAUDE_PLUGIN_ROOT}/hooks/core-resolve.js"
|
|
28
|
+
.replace(/node \.(specialist|formation|collaboration)\/([\w./-]+\.js)/g,
|
|
29
|
+
'node "${CLAUDE_PLUGIN_ROOT}/hooks/core-resolve.js" .$1/$2')
|
|
30
|
+
.replace(/`\.specialist\/([\w./-]+\.(?:md|yaml))`/g, '`${CLAUDE_PLUGIN_ROOT}/../../.specialist/$1`');
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
function copyDir(src, dst) {
|
|
@@ -77,6 +77,7 @@ function renderStatus(s) {
|
|
|
77
77
|
const L = [
|
|
78
78
|
'Yggdrasil room: ' + s.room,
|
|
79
79
|
'Node name: ' + s.node_name,
|
|
80
|
+
...(s.registered ? ['Registered: ' + s.registered] : []),
|
|
80
81
|
'Key: ' + s.key + ' (source: ' + s.key_source + ')',
|
|
81
82
|
'Collaboration: ' + (s.enabled && s.active_in_project ? 'active in this project'
|
|
82
83
|
: 'not active in this project - run "ygg join <key>" (adds room "' + s.room + '" to collaboration_projects; presence is only sent for activated rooms)'),
|
|
@@ -92,13 +93,24 @@ function renderStatus(s) {
|
|
|
92
93
|
return L.join('\n');
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
// The poller and presence only know projects in ~/.kenaz/projects.json. A server-only machine has
|
|
97
|
+
// no Dashboard to register them, so before 0.3.4 a joined project never showed up in its room, with
|
|
98
|
+
// no error anywhere (field report). Registering is idempotent; status says when it had to.
|
|
99
|
+
async function ensureRegistered(dir) {
|
|
100
|
+
try {
|
|
101
|
+
const r = await call('POST', '/api/projects/register', { projectPath: dir });
|
|
102
|
+
return r && r.success ? (r.created ? 'registered now (it was missing: this machine could not be seen in the room or get mail for it)' : 'yes') : 'NO: ' + ((r && r.error) || 'unknown');
|
|
103
|
+
} catch (e) { return 'NO: ' + e.message + ' (kenaz-server too old? run npx kenaz@latest install)'; }
|
|
104
|
+
}
|
|
105
|
+
|
|
95
106
|
async function status(dir) {
|
|
107
|
+
const registered = await ensureRegistered(dir);
|
|
96
108
|
const cfg = await getConfig(dir);
|
|
97
109
|
if (cfg.error) throw new Error(cfg.error);
|
|
98
110
|
const relay = await relayNodes(cfg);
|
|
99
111
|
let peer = null;
|
|
100
112
|
try { peer = await call('GET', '/api/peer/status?projectPath=' + encodeURIComponent(dir)); } catch (e) { /* unknown */ }
|
|
101
|
-
return buildStatus(cfg, relay, peer);
|
|
113
|
+
return Object.assign(buildStatus(cfg, relay, peer), { registered });
|
|
102
114
|
}
|
|
103
115
|
|
|
104
116
|
function withCollab(cfg, on) {
|
|
@@ -381,7 +381,7 @@ Scope 路徑: ✓ 合理
|
|
|
381
381
|
|
|
382
382
|
```bash
|
|
383
383
|
# 使用 Task API 建立任務
|
|
384
|
-
node .collaboration/core/task-api.js create --title "任務名稱" --priority P1
|
|
384
|
+
node "${CLAUDE_PLUGIN_ROOT}/hooks/core-resolve.js" .collaboration/core/task-api.js create --title "任務名稱" --priority P1
|
|
385
385
|
|
|
386
386
|
# 或直接建立 YAML 文件到 .collaboration/tasks/backlog/
|
|
387
387
|
```
|
|
@@ -390,7 +390,7 @@ node .collaboration/core/task-api.js create --title "任務名稱" --priority P1
|
|
|
390
390
|
|
|
391
391
|
```bash
|
|
392
392
|
# 驗證任務文件格式
|
|
393
|
-
node .collaboration/core/task-api.js list
|
|
393
|
+
node "${CLAUDE_PLUGIN_ROOT}/hooks/core-resolve.js" .collaboration/core/task-api.js list
|
|
394
394
|
|
|
395
395
|
# 檢查必填欄位
|
|
396
396
|
# id, title, description, priority, status, role, tags, source, acceptance_criteria
|
|
@@ -415,7 +415,7 @@ node .collaboration/core/task-api.js list
|
|
|
415
415
|
ls .collaboration/tasks/backlog/TASK_113_*.yaml
|
|
416
416
|
|
|
417
417
|
# 使用 Task API 驗證
|
|
418
|
-
node .collaboration/core/task-api.js list
|
|
418
|
+
node "${CLAUDE_PLUGIN_ROOT}/hooks/core-resolve.js" .collaboration/core/task-api.js list
|
|
419
419
|
|
|
420
420
|
# 檢查必填欄位
|
|
421
421
|
# id, title, description, priority, status, role, tags, source, acceptance_criteria
|
|
@@ -457,7 +457,7 @@ node .collaboration/core/task-api.js list
|
|
|
457
457
|
**驗證任務**(必須執行):
|
|
458
458
|
```bash
|
|
459
459
|
# 使用 Task API 驗證(新系統)
|
|
460
|
-
node .collaboration/core/task-api.js list
|
|
460
|
+
node "${CLAUDE_PLUGIN_ROOT}/hooks/core-resolve.js" .collaboration/core/task-api.js list
|
|
461
461
|
```
|
|
462
462
|
|
|
463
463
|
#### 8.4 批量更新 PROGRESS.md
|
|
@@ -688,7 +688,7 @@ metadata:
|
|
|
688
688
|
|
|
689
689
|
```bash
|
|
690
690
|
# 使用 Task API 建立任務(新系統)
|
|
691
|
-
node .collaboration/core/task-api.js create --title "任務名稱" --priority P1
|
|
691
|
+
node "${CLAUDE_PLUGIN_ROOT}/hooks/core-resolve.js" .collaboration/core/task-api.js create --title "任務名稱" --priority P1
|
|
692
692
|
|
|
693
693
|
# 或直接建立 YAML 文件到 .collaboration/tasks/backlog/
|
|
694
694
|
```
|
|
@@ -766,9 +766,9 @@ node .collaboration/core/task-api.js create --title "任務名稱" --priority P1
|
|
|
766
766
|
|
|
767
767
|
```bash
|
|
768
768
|
# 使用 Task API 建立任務
|
|
769
|
-
node .collaboration/core/task-api.js create --title "實作撤銷機制" --priority P1
|
|
770
|
-
node .collaboration/core/task-api.js create --title "新增 Undo/Redo UI" --priority P2
|
|
771
|
-
node .collaboration/core/task-api.js create --title "整合快捷鍵" --priority P2
|
|
769
|
+
node "${CLAUDE_PLUGIN_ROOT}/hooks/core-resolve.js" .collaboration/core/task-api.js create --title "實作撤銷機制" --priority P1
|
|
770
|
+
node "${CLAUDE_PLUGIN_ROOT}/hooks/core-resolve.js" .collaboration/core/task-api.js create --title "新增 Undo/Redo UI" --priority P2
|
|
771
|
+
node "${CLAUDE_PLUGIN_ROOT}/hooks/core-resolve.js" .collaboration/core/task-api.js create --title "整合快捷鍵" --priority P2
|
|
772
772
|
```
|
|
773
773
|
|
|
774
774
|
**Stage 8: Pipeline Integration**
|
|
@@ -1055,16 +1055,16 @@ Dev Agent 會按照優先級執行這些任務。
|
|
|
1055
1055
|
|
|
1056
1056
|
```bash
|
|
1057
1057
|
# 列出所有任務
|
|
1058
|
-
node .collaboration/core/task-api.js list
|
|
1058
|
+
node "${CLAUDE_PLUGIN_ROOT}/hooks/core-resolve.js" .collaboration/core/task-api.js list
|
|
1059
1059
|
|
|
1060
1060
|
# 建立新任務
|
|
1061
|
-
node .collaboration/core/task-api.js create --title "新功能" --priority P1
|
|
1061
|
+
node "${CLAUDE_PLUGIN_ROOT}/hooks/core-resolve.js" .collaboration/core/task-api.js create --title "新功能" --priority P1
|
|
1062
1062
|
|
|
1063
1063
|
# 認領任務
|
|
1064
|
-
node .collaboration/core/task-api.js claim TASK_XXX
|
|
1064
|
+
node "${CLAUDE_PLUGIN_ROOT}/hooks/core-resolve.js" .collaboration/core/task-api.js claim TASK_XXX
|
|
1065
1065
|
|
|
1066
1066
|
# 完成任務
|
|
1067
|
-
node .collaboration/core/task-api.js complete TASK_XXX
|
|
1067
|
+
node "${CLAUDE_PLUGIN_ROOT}/hooks/core-resolve.js" .collaboration/core/task-api.js complete TASK_XXX
|
|
1068
1068
|
```
|
|
1069
1069
|
|
|
1070
1070
|
**詳細文檔**:`.collaboration/README.md`
|
package/lib/cli.js
CHANGED
|
@@ -55,7 +55,10 @@ function platformServer() {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
function onPath(cmd) {
|
|
58
|
-
|
|
58
|
+
const r = process.platform === 'win32' // one string with shell (.cmd shims); args + shell:true is deprecated (DEP0190)
|
|
59
|
+
? spawnSync(cmd + ' --version', { encoding: 'utf8', shell: true, timeout: 15000 })
|
|
60
|
+
: spawnSync(cmd, ['--version'], { encoding: 'utf8', timeout: 15000 });
|
|
61
|
+
return r.status === 0;
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
/** setup-codex.js from the installed core (dry-run and doctor-before-install: from the package). */
|
|
@@ -76,6 +79,29 @@ function prepareCodexHome(p, flags) {
|
|
|
76
79
|
|
|
77
80
|
function nodeOk() { return Number(process.versions.node.split('.')[0]) >= MIN_NODE; }
|
|
78
81
|
|
|
82
|
+
const samePath = (a, b) => {
|
|
83
|
+
const n = (x) => path.resolve(String(x || '')).replace(/\\/g, '/');
|
|
84
|
+
return process.platform === 'win32' ? n(a).toLowerCase() === n(b).toLowerCase() : n(a) === n(b);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** Stop the kenaz-server running from exactly binPath (never by name). Returns the PIDs stopped. */
|
|
88
|
+
function stopOwnServer(binPath, adapter) {
|
|
89
|
+
if (!binPath || !adapter || typeof adapter.procs !== 'function') return [];
|
|
90
|
+
const pids = adapter.procs().filter((pr) => pr.exe && samePath(pr.exe, binPath)).map((pr) => pr.pid);
|
|
91
|
+
for (const pid of pids) { try { (adapter.kill || process.kill)(pid); } catch (_) { /* already gone */ } }
|
|
92
|
+
return pids;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Windows keeps an exe locked for a moment after its process exits: retry the copy briefly. */
|
|
96
|
+
function retryBusy(fn, tries = 10) {
|
|
97
|
+
for (let i = 1; ; i++) {
|
|
98
|
+
try { return fn(); } catch (e) {
|
|
99
|
+
if (i >= tries || !['EBUSY', 'EPERM', 'EPIPE', 'EACCES', 'ETXTBSY'].includes(e.code)) throw e;
|
|
100
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 500);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
79
105
|
function install(p, flags, out, deps = {}) {
|
|
80
106
|
const dry = !!flags['dry-run'];
|
|
81
107
|
const tag = dry ? '[dry-run] ' : '';
|
|
@@ -96,11 +122,22 @@ function install(p, flags, out, deps = {}) {
|
|
|
96
122
|
else { out(`server: no @kenazai/server-${process.platform}-${process.arch} package installed (npm install it, or pass --server-bin)`); rc = 1; }
|
|
97
123
|
}
|
|
98
124
|
if (!dry) {
|
|
125
|
+
if (srv) {
|
|
126
|
+
// Reinstall/upgrade: the kenaz-server this install manages may be running from p.bin. Windows
|
|
127
|
+
// locks a running exe (field report: "EPIPE ... being used by another process" on
|
|
128
|
+
// `npx kenaz@latest install`) and Linux refuses to overwrite one (ETXTBSY). Stop exactly that
|
|
129
|
+
// binary (exe path match, by PID); the service step below starts the new one.
|
|
130
|
+
const stopped = stopOwnServer(binPath, deps.dashboard || dash.realAdapter(p.home));
|
|
131
|
+
if (stopped.length) log(`server: stopped the running kenaz-server (PID ${stopped.join(', ')}) to replace it`);
|
|
132
|
+
}
|
|
99
133
|
fs.rmSync(p.core, { recursive: true, force: true });
|
|
100
134
|
fs.cpSync(srcCore, p.core, { recursive: true });
|
|
101
135
|
if (srv) {
|
|
102
136
|
fs.mkdirSync(p.bin, { recursive: true });
|
|
103
|
-
|
|
137
|
+
// Unix: unlink first. A running binary can be unlinked (its process keeps the old inode) but not
|
|
138
|
+
// overwritten (ETXTBSY), so this works even when the running server was not found above.
|
|
139
|
+
if (process.platform !== 'win32') fs.rmSync(binPath, { force: true });
|
|
140
|
+
retryBusy(() => fs.cpSync(srv.dir, p.bin, { recursive: true }));
|
|
104
141
|
fs.chmodSync(binPath, 0o755);
|
|
105
142
|
}
|
|
106
143
|
fs.writeFileSync(p.meta, JSON.stringify({ version: PKG.version, server: srv ? srv.version : null, serverBin: binPath, installedAt: new Date().toISOString() }, null, 2));
|
|
@@ -163,9 +200,9 @@ function status(p, flags, out) {
|
|
|
163
200
|
return 0;
|
|
164
201
|
}
|
|
165
202
|
|
|
166
|
-
function probe(port) {
|
|
203
|
+
function probe(port, urlPath = '/api/health') {
|
|
167
204
|
return new Promise((resolve) => {
|
|
168
|
-
const req = http.get({ host: '127.0.0.1', port, path:
|
|
205
|
+
const req = http.get({ host: '127.0.0.1', port, path: urlPath, timeout: 2500 }, (res) => {
|
|
169
206
|
let body = ''; res.on('data', (d) => { body += d; }); res.on('end', () => resolve({ ok: res.statusCode < 500, detail: `HTTP ${res.statusCode} on :${port}`, body }));
|
|
170
207
|
});
|
|
171
208
|
req.on('timeout', () => { req.destroy(); resolve({ ok: false, detail: `no answer on 127.0.0.1:${port}` }); });
|
|
@@ -195,7 +232,11 @@ async function doctor(p, flags, out, deps = {}) {
|
|
|
195
232
|
const o = d.portOwner;
|
|
196
233
|
add(`port ${d.port} owner`, o && d.ownerIsDashboard ? 'FAIL' : 'ok', o ? `PID ${o.pid} ${o.exe || o.name}${d.ownerIsDashboard ? ' (Dashboard)' : ''}` : 'nobody listening');
|
|
197
234
|
}
|
|
198
|
-
|
|
235
|
+
// Ask the running server first: its answer carries its own version. The status file can still
|
|
236
|
+
// describe the previous binary right after an upgrade (field report: "[running 0.3.1]" on 0.3.3).
|
|
237
|
+
let upd = null;
|
|
238
|
+
if (r.ok) { const u = await probe(process.env.KENAZ_HTTP_PORT || '18080', '/api/update/status'); try { upd = JSON.parse(u.body); } catch (_) { /* older server */ } }
|
|
239
|
+
if (!upd || !upd.state) upd = readJson(path.join(p.dir, 'update', 'status.json')); // TASK_2378, written by kenaz-server
|
|
199
240
|
if (!upd) add('self-update', 'skip', 'no status yet (kenaz-server has not run the updater)');
|
|
200
241
|
else {
|
|
201
242
|
const d = { disabled: `off: ${upd.reason}`, error: `last check failed: ${upd.last_error}`, staged: `${upd.staged_version} staged, applies when idle`,
|
|
@@ -248,4 +289,4 @@ function run(argv, out = console.log, deps = {}) {
|
|
|
248
289
|
catch (e) { out('kenaz ' + cmd + ': ERROR ' + e.message); return 1; }
|
|
249
290
|
}
|
|
250
291
|
|
|
251
|
-
module.exports = { run, parseArgs, platformServer, paths };
|
|
292
|
+
module.exports = { run, parseArgs, platformServer, paths, stopOwnServer, retryBusy };
|
package/lib/cli.test.js
CHANGED
|
@@ -57,3 +57,27 @@ test('kenaz ygg / init pass through to the core scripts with the remaining args'
|
|
|
57
57
|
assert.match(calls[1][0].split(path.sep).join('/'), /plugins\/kenaz\/scripts\/kenaz-mode\.js$/);
|
|
58
58
|
assert.deepStrictEqual(calls[1].slice(1), ['init']);
|
|
59
59
|
});
|
|
60
|
+
|
|
61
|
+
test('reinstall stops only the kenaz-server running from the managed path (exact exe match, by PID)', () => {
|
|
62
|
+
const { stopOwnServer } = require('./cli');
|
|
63
|
+
const killed = [];
|
|
64
|
+
const bin = path.join(os.tmpdir(), 'kh', '.kenaz', 'bin', process.platform === 'win32' ? 'kenaz-server.exe' : 'kenaz-server');
|
|
65
|
+
const adapter = {
|
|
66
|
+
procs: () => [
|
|
67
|
+
{ pid: 11, exe: bin },
|
|
68
|
+
{ pid: 12, exe: path.join(os.tmpdir(), 'other', 'kenaz-server.exe') }, // same name, different path: untouched
|
|
69
|
+
{ pid: 13, exe: '' },
|
|
70
|
+
],
|
|
71
|
+
kill: (pid) => killed.push(pid),
|
|
72
|
+
};
|
|
73
|
+
assert.deepStrictEqual(stopOwnServer(bin, adapter), [11]);
|
|
74
|
+
assert.deepStrictEqual(killed, [11]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('retryBusy retries a locked copy and then succeeds; other errors are thrown at once', () => {
|
|
78
|
+
const { retryBusy } = require('./cli');
|
|
79
|
+
let n = 0;
|
|
80
|
+
assert.strictEqual(retryBusy(() => { if (++n < 3) { const e = new Error('busy'); e.code = 'EBUSY'; throw e; } return 'ok'; }, 5), 'ok');
|
|
81
|
+
assert.strictEqual(n, 3);
|
|
82
|
+
assert.throws(() => retryBusy(() => { const e = new Error('nope'); e.code = 'ENOENT'; throw e; }, 5), /nope/);
|
|
83
|
+
});
|
package/lib/dashboard.js
CHANGED
|
@@ -29,6 +29,16 @@ function realAdapter(home) {
|
|
|
29
29
|
if (r.status !== 0) return [];
|
|
30
30
|
try { const v = JSON.parse(r.stdout); return (Array.isArray(v) ? v : [v]).map((x) => ({ pid: x.ProcessId, name: x.Name || '', exe: x.ExecutablePath || '' })); } catch (_) { return []; }
|
|
31
31
|
}
|
|
32
|
+
if (process.platform === 'linux') {
|
|
33
|
+
// /proc is always there; `ps` is not (node:22-slim has no procps, so a reinstall could not
|
|
34
|
+
// find the running kenaz-server and failed with ETXTBSY).
|
|
35
|
+
const out = [];
|
|
36
|
+
for (const d of fs.readdirSync('/proc')) {
|
|
37
|
+
if (!/^\d+$/.test(d)) continue;
|
|
38
|
+
try { const exe = fs.readlinkSync('/proc/' + d + '/exe'); out.push({ pid: Number(d), name: path.basename(exe), exe }); } catch (_) { /* gone or not ours */ }
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
32
42
|
const r = sh('ps', ['-eo', 'pid=,args=']);
|
|
33
43
|
return String(r.stdout || '').split('\n').map((l) => /^\s*(\d+)\s+(.+)$/.exec(l)).filter(Boolean)
|
|
34
44
|
.map((m) => ({ pid: Number(m[1]), name: path.basename(m[2].split(' ')[0]), exe: m[2].split(' ')[0] }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kenaz",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "Install Kenaz (plugin, hooks, kenaz-server) for Claude Code and Codex: npx kenaz install",
|
|
5
5
|
"bin": {
|
|
6
6
|
"kenaz": "bin/kenaz.js"
|
|
@@ -20,11 +20,15 @@
|
|
|
20
20
|
"test": "node --test lib/cli.test.js"
|
|
21
21
|
},
|
|
22
22
|
"optionalDependencies": {
|
|
23
|
-
"@kenazai/server-darwin-arm64": "0.3.
|
|
24
|
-
"@kenazai/server-darwin-x64": "0.3.
|
|
25
|
-
"@kenazai/server-linux-arm64": "0.3.
|
|
26
|
-
"@kenazai/server-linux-x64": "0.3.
|
|
27
|
-
"@kenazai/server-win32-x64": "0.3.
|
|
23
|
+
"@kenazai/server-darwin-arm64": "0.3.5",
|
|
24
|
+
"@kenazai/server-darwin-x64": "0.3.5",
|
|
25
|
+
"@kenazai/server-linux-arm64": "0.3.5",
|
|
26
|
+
"@kenazai/server-linux-x64": "0.3.5",
|
|
27
|
+
"@kenazai/server-win32-x64": "0.3.5"
|
|
28
28
|
},
|
|
29
|
-
"license": "UNLICENSED"
|
|
29
|
+
"license": "UNLICENSED",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/xyz9516/KenazAI.git"
|
|
33
|
+
}
|
|
30
34
|
}
|
|
@@ -111,7 +111,9 @@ function retireDashboardHooks(file, dry) {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
function claudeVersion() {
|
|
114
|
-
const r =
|
|
114
|
+
const r = process.platform === 'win32' // one string with shell (.cmd shim): args + shell:true is deprecated (DEP0190)
|
|
115
|
+
? spawnSync('claude --version', { encoding: 'utf8', shell: true, timeout: 15000 })
|
|
116
|
+
: spawnSync('claude', ['--version'], { encoding: 'utf8', timeout: 15000 });
|
|
115
117
|
return r.status === 0 ? parseVersion(r.stdout) : null;
|
|
116
118
|
}
|
|
117
119
|
|
|
@@ -125,14 +127,21 @@ function winVbs(bin, core) { // hidden window, env for task tools; server binds
|
|
|
125
127
|
return 'Set sh = WScript.CreateObject("WScript.Shell")\r\nsh.Environment("Process")("KENAZ_SOURCE_DIR") = "' + core + '"\r\n' +
|
|
126
128
|
'sh.Run """' + bin + '""", 0, False\r\n';
|
|
127
129
|
}
|
|
128
|
-
|
|
130
|
+
// launchd and systemd start the server with a bare PATH (/usr/bin:/bin...), so `node` from Homebrew,
|
|
131
|
+
// nvm or /usr/local was not found and every MCP tool that runs a node script failed with
|
|
132
|
+
// "No such file or directory" (field report, macOS 0.3.1). Put the node running this install first.
|
|
133
|
+
function servicePath(nodeExe = process.execPath) {
|
|
134
|
+
return [path.dirname(nodeExe), '/opt/homebrew/bin', '/usr/local/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin']
|
|
135
|
+
.filter((d, i, a) => a.indexOf(d) === i).join(':');
|
|
136
|
+
}
|
|
137
|
+
function launchdPlist(bin, core, log, nodeExe) {
|
|
129
138
|
return '<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict>\n' +
|
|
130
139
|
' <key>Label</key><string>' + LABEL + '</string>\n <key>ProgramArguments</key><array><string>' + bin + '</string></array>\n' +
|
|
131
|
-
' <key>EnvironmentVariables</key><dict><key>KENAZ_SOURCE_DIR</key><string>' + core + '</string></dict>\n' +
|
|
140
|
+
' <key>EnvironmentVariables</key><dict><key>KENAZ_SOURCE_DIR</key><string>' + core + '</string><key>PATH</key><string>' + servicePath(nodeExe) + '</string></dict>\n' +
|
|
132
141
|
' <key>RunAtLoad</key><true/>\n <key>KeepAlive</key><true/>\n <key>StandardOutPath</key><string>' + log + '</string>\n <key>StandardErrorPath</key><string>' + log + '</string>\n</dict></plist>\n';
|
|
133
142
|
}
|
|
134
|
-
function systemdUnit(bin, core) {
|
|
135
|
-
return '[Unit]\nDescription=Kenaz server (headless backend)\nAfter=network.target\n\n[Service]\nExecStart=' + bin + '\nEnvironment=KENAZ_SOURCE_DIR=' + core + '\nRestart=on-failure\n\n[Install]\nWantedBy=default.target\n';
|
|
143
|
+
function systemdUnit(bin, core, nodeExe) {
|
|
144
|
+
return '[Unit]\nDescription=Kenaz server (headless backend)\nAfter=network.target\n\n[Service]\nExecStart=' + bin + '\nEnvironment=KENAZ_SOURCE_DIR=' + core + '\nEnvironment=PATH=' + servicePath(nodeExe) + '\nRestart=on-failure\n\n[Install]\nWantedBy=default.target\n';
|
|
136
145
|
}
|
|
137
146
|
|
|
138
147
|
function servicePlan(home, bin, core, platform = process.platform) {
|
|
@@ -163,7 +172,9 @@ function serviceInstall(plan, dry, log) {
|
|
|
163
172
|
fs.mkdirSync(path.dirname(plan.fallbackFile), { recursive: true });
|
|
164
173
|
fs.copyFileSync(plan.files[0][0], plan.fallbackFile);
|
|
165
174
|
sh('wscript.exe', [plan.files[0][0]]);
|
|
166
|
-
|
|
175
|
+
// schtasks' stderr is in the console's OEM code page (garbled when echoed); the usual cause is
|
|
176
|
+
// missing admin rights for /SC ONLOGON. The Startup-folder launcher starts at logon the same way.
|
|
177
|
+
log('schtasks needs administrator rights here (exit ' + r.status + '); installed the Startup-folder launcher instead (also starts at logon)');
|
|
167
178
|
}
|
|
168
179
|
} else if (plan.kind === 'launchd') {
|
|
169
180
|
log('launchctl load -w ' + plan.unit); if (!dry) sh('launchctl', ['load', '-w', plan.unit]);
|
|
@@ -259,7 +270,7 @@ function main(argv, out = console.log, getVersion = claudeVersion) { // getVersi
|
|
|
259
270
|
else if (!bin) { out('service: skipped, kenaz-server binary not found (pass --server-bin)'); rc = 1; }
|
|
260
271
|
else serviceInstall(plan, dry, log);
|
|
261
272
|
}
|
|
262
|
-
if (cmd === 'install' && rc === 0) out('Next: cd <your project>; claude; /kenaz init; /Ansuz');
|
|
273
|
+
if (cmd === 'install' && rc === 0) out('Next: cd <your project>; claude; /kenaz:kenaz init; /kenaz:Ansuz (Codex: run /hooks once to trust the kenaz hooks, then $kenaz status)');
|
|
263
274
|
if (cmd === 'uninstall') out('Project .kenaz/ data was left untouched.');
|
|
264
275
|
return rc;
|
|
265
276
|
}
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
/**
|
|
3
|
-
* Broadcast Layer
|
|
4
|
-
*
|
|
5
|
-
* Supabase/Yggdrasil broadcast helpers extracted from headless.js.
|
|
6
|
-
* Lazy-loaded singletons — no hard dependency on supabase-client at require time.
|
|
7
|
-
*
|
|
8
|
-
* Exports: getSupabaseClient, getTaskSync, getCommitSync, syncCommit,
|
|
9
|
-
* broadcastStatus, broadcastStatusSync
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
const path = require('path');
|
|
13
|
-
const { ROOT } = require('./utils');
|
|
14
|
-
|
|
15
|
-
// ─────────────────────────────────────────
|
|
16
|
-
// Yggdrasil: kenaz_status 廣播 + commit-sync(lazy loaded, graceful degradation)
|
|
17
|
-
// ─────────────────────────────────────────
|
|
18
|
-
let _supabaseClient = null;
|
|
19
|
-
let _taskSync = null;
|
|
20
|
-
let _commitSync = null;
|
|
21
|
-
|
|
22
|
-
function getSupabaseClient(projectPath) {
|
|
23
|
-
if (_supabaseClient === undefined) return null;
|
|
24
|
-
if (!_supabaseClient) try { _supabaseClient = require('./supabase-client'); } catch { _supabaseClient = undefined; return null; }
|
|
25
|
-
return _supabaseClient.getClient(projectPath);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function getTaskSync() {
|
|
29
|
-
if (_taskSync === undefined) return null;
|
|
30
|
-
if (!_taskSync) try { _taskSync = require('./task-sync'); } catch { _taskSync = undefined; return null; }
|
|
31
|
-
return _taskSync;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function getCommitSync() {
|
|
35
|
-
if (_commitSync === undefined) return null;
|
|
36
|
-
if (!_commitSync) try { _commitSync = require('./commit-sync'); } catch { _commitSync = undefined; return null; }
|
|
37
|
-
return _commitSync;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* 同步 commit 到 kenaz_commits。
|
|
42
|
-
* 回傳 Promise(await 時確保完成,或 fire-and-forget)。
|
|
43
|
-
*/
|
|
44
|
-
function syncCommit(taskId, projectPath) {
|
|
45
|
-
try {
|
|
46
|
-
const cs = getCommitSync();
|
|
47
|
-
if (!cs) return Promise.resolve();
|
|
48
|
-
return cs.syncLatestCommit({ taskId, projectPath }).catch(() => {});
|
|
49
|
-
} catch { return Promise.resolve(); }
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* 廣播 kenaz_status — 讓 Agent Monitor 和其他機器看到 dispatch agent。
|
|
54
|
-
* 回傳 Promise — caller 可 await 確保完成。
|
|
55
|
-
*/
|
|
56
|
-
function broadcastStatus(projectPath, updates) {
|
|
57
|
-
try {
|
|
58
|
-
const sb = getSupabaseClient(projectPath);
|
|
59
|
-
if (!sb) return Promise.resolve();
|
|
60
|
-
const { client, config } = sb;
|
|
61
|
-
const ts = getTaskSync();
|
|
62
|
-
const project = ts?.resolveProjectName?.(projectPath || ROOT) || path.basename(ROOT);
|
|
63
|
-
return client.from('kenaz_status').upsert({
|
|
64
|
-
node_name: config.node_name,
|
|
65
|
-
project,
|
|
66
|
-
...updates,
|
|
67
|
-
}, { onConflict: 'node_name,project' }).then(() => {}).catch(() => {});
|
|
68
|
-
} catch { return Promise.resolve(); }
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* 同步版 broadcastStatus — 用於 process exit 前確保送達。
|
|
73
|
-
* 使用 synchronous HTTP(spawnSync + curl)避免 async 在退出時丟失。
|
|
74
|
-
*/
|
|
75
|
-
function broadcastStatusSync(projectPath, updates) {
|
|
76
|
-
try {
|
|
77
|
-
const sb = getSupabaseClient(projectPath);
|
|
78
|
-
if (!sb) return;
|
|
79
|
-
const { config } = sb;
|
|
80
|
-
const ts = getTaskSync();
|
|
81
|
-
const project = ts?.resolveProjectName?.(projectPath || ROOT) || path.basename(ROOT);
|
|
82
|
-
const payload = { node_name: config.node_name, project, ...updates };
|
|
83
|
-
const url = `${config.supabase_url}/rest/v1/kenaz_status`;
|
|
84
|
-
const { spawnSync } = require('child_process');
|
|
85
|
-
spawnSync('curl', [
|
|
86
|
-
'-s', '-X', 'POST', url,
|
|
87
|
-
'-H', `apikey: ${config.supabase_anon_key}`,
|
|
88
|
-
'-H', `Authorization: Bearer ${config.supabase_anon_key}`,
|
|
89
|
-
'-H', 'Content-Type: application/json',
|
|
90
|
-
'-H', 'Prefer: resolution=merge-duplicates',
|
|
91
|
-
'-d', JSON.stringify(payload),
|
|
92
|
-
], { timeout: 5000, stdio: 'ignore', windowsHide: true });
|
|
93
|
-
} catch { /* best-effort */ }
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
module.exports = {
|
|
97
|
-
getSupabaseClient,
|
|
98
|
-
getTaskSync,
|
|
99
|
-
getCommitSync,
|
|
100
|
-
syncCommit,
|
|
101
|
-
broadcastStatus,
|
|
102
|
-
broadcastStatusSync,
|
|
103
|
-
};
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
// TASK_2274: formation-mode `--dry-run` must have NO side effects: no git init
|
|
2
|
-
// of the target, no resource lock files, no claim adoption. Drives the real CLI
|
|
3
|
-
// entry (dispatch.js -> dispatch()) against a temp project root.
|
|
4
|
-
const test = require('node:test');
|
|
5
|
-
const assert = require('node:assert/strict');
|
|
6
|
-
const fs = require('fs');
|
|
7
|
-
const os = require('os');
|
|
8
|
-
const path = require('path');
|
|
9
|
-
const { spawnSync } = require('child_process');
|
|
10
|
-
|
|
11
|
-
const DISPATCH = path.join(__dirname, 'dispatch.js');
|
|
12
|
-
|
|
13
|
-
function mkProject() {
|
|
14
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 't2274_'));
|
|
15
|
-
fs.mkdirSync(path.join(root, '.kenaz', 'tasks', 'backlog'), { recursive: true });
|
|
16
|
-
fs.mkdirSync(path.join(root, '.kenaz', 'tasks', 'active'), { recursive: true });
|
|
17
|
-
fs.writeFileSync(path.join(root, '.kenaz', 'tasks', 'backlog', 'TASK_9001_dryrun.yaml'), [
|
|
18
|
-
'id: TASK_9001', 'title: dry run probe', 'description: probe', 'status: backlog',
|
|
19
|
-
'priority: P2', 'scope_paths:', ' - src/x.js', 'resources:', ' - dryrun_res', ''].join('\n'));
|
|
20
|
-
return root;
|
|
21
|
-
}
|
|
22
|
-
function run(root, extra, envOver) {
|
|
23
|
-
const env = { ...process.env, ...(envOver || {}) }; delete env.CLAUDE_PROJECT_DIR;
|
|
24
|
-
return spawnSync(process.execPath, [DISPATCH, '--formation=unit', '--skill=do', '--task=TASK_9001',
|
|
25
|
-
'--project-path', root, ...extra], { encoding: 'utf8', env, windowsHide: true, timeout: 90000 });
|
|
26
|
-
}
|
|
27
|
-
const lockFile = (root) => path.join(root, '.kenaz', 'locks', 'resource_dryrun_res.lock');
|
|
28
|
-
|
|
29
|
-
test('formation-mode --dry-run: no git init, no resource lock, reports intent', () => {
|
|
30
|
-
const root = mkProject();
|
|
31
|
-
try {
|
|
32
|
-
const r = run(root, ['--dry-run']);
|
|
33
|
-
const out = (r.stdout || '') + (r.stderr || '');
|
|
34
|
-
assert.equal(r.status, 0, out.slice(-500));
|
|
35
|
-
assert.ok(!fs.existsSync(path.join(root, '.git')), 'dry-run must not git init');
|
|
36
|
-
assert.ok(!fs.existsSync(lockFile(root)), 'dry-run must not acquire resource lock');
|
|
37
|
-
assert.match(out, /\[dry-run\] resource locks NOT acquired: dryrun_res/);
|
|
38
|
-
assert.match(out, /\[dry-run\] would git init/);
|
|
39
|
-
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test('control: non-dry-run formation dispatch DOES git init and acquire the lock', () => {
|
|
43
|
-
const root = mkProject();
|
|
44
|
-
try {
|
|
45
|
-
// PATH = node + git dirs only and a fake HOME, so the real agent launch cannot
|
|
46
|
-
// find a claude binary (it fails after the lock/git steps we are asserting).
|
|
47
|
-
const gitDirs = spawnSync(process.platform === 'win32' ? 'where' : 'which', ['git'], { encoding: 'utf8' })
|
|
48
|
-
.stdout.split(/\r?\n/).filter(Boolean).map((p) => path.dirname(p));
|
|
49
|
-
const PATH = [path.dirname(process.execPath), ...gitDirs].join(path.delimiter);
|
|
50
|
-
run(root, [], { PATH, Path: PATH, USERPROFILE: root, HOME: root });
|
|
51
|
-
assert.ok(fs.existsSync(path.join(root, '.git')), 'control: git init expected');
|
|
52
|
-
assert.ok(fs.existsSync(lockFile(root)), 'control: lock expected');
|
|
53
|
-
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
54
|
-
});
|