kenaz 0.3.11 → 0.3.13
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 +11 -1
- package/core/.claude/hooks/__tests__/correction-detect.test.js +74 -0
- package/core/.claude/hooks/_run-user-prompt.js +2 -0
- package/core/.claude/hooks/_run-user-prompt.test.js +2 -2
- package/core/.claude/hooks/correction-detect.js +99 -0
- package/core/.collaboration/core/task-2288-followups.test.js +2 -37
- package/core/.collaboration/core/task-2421-replica.test.js +229 -0
- package/core/.collaboration/core/task-api.js +221 -12
- package/core/.collaboration/core/task-replica.js +189 -0
- package/core/.formation/.merge-lock +1 -0
- package/core/.formation/bifrost/codex-doctor.js +12 -2
- package/core/.formation/bifrost/codex-doctor.test.js +15 -0
- package/core/.formation/bifrost/exec.js +147 -0
- package/core/.formation/bifrost/provider-launch.js +57 -0
- package/core/.formation/bifrost/provider-launch.test.js +62 -0
- package/core/.formation/bifrost/setup-codex.js +84 -16
- package/core/.formation/tools/__tests__/dispatch-model.test.js +1 -1
- package/core/.formation/tools/__tests__/subagent-executor.test.js +198 -0
- package/core/.formation/tools/dispatch-model.js +15 -10
- package/core/.formation/tools/dispatch.js +46 -10
- package/core/.formation/tools/kenaz-stats.js +57 -16
- package/core/.formation/tools/kenaz-stats.test.js +2 -2
- package/core/.formation/tools/post-complete/task2420.test.js +129 -0
- package/core/.formation/tools/post-complete.js +129 -8
- package/core/.kenaz/specialist/memory_guard_fired.json +1 -1
- package/core/.kenaz/specialist/memory_guard_trigger_cache.json +1 -1
- package/core/.specialist/Ansuz/RULES.md +7 -2
- package/core/.specialist/tools/__tests__/handoff-doc.test.js +46 -0
- package/core/.specialist/tools/__tests__/rea-detect.test.js +10 -0
- package/core/.specialist/tools/__tests__/task-2425-lessons-lifecycle.test.js +142 -0
- package/core/.specialist/tools/__tests__/task-2427-harvest.test.js +150 -0
- package/core/.specialist/tools/ansuz-boot-fast.js +37 -1
- package/core/.specialist/tools/handoff-doc.js +95 -0
- package/core/.specialist/tools/lessons-harvest.js +243 -0
- package/core/.specialist/tools/lessons-lifecycle.js +103 -0
- package/core/.specialist/tools/lessons.js +149 -24
- package/core/.specialist/tools/rea-detect.js +6 -2
- package/core/.synapse/memory-shared.json.sync +1 -0
- package/core/KENAZ_CORE_VERSION +1 -1
- package/core/dev/plans/CLI_FIRST_INSTALL_FLOW.md +75 -109
- package/core/dev/scripts/correction-detect-eval.js +70 -0
- package/core/dev/specs/peer/CODEX_MANAGED_DAEMON.md +54 -0
- package/core/dev/specs/peer/setup-codex-hooks.test.js +38 -0
- package/core/dev/spikes/task2145-evidence-20261004/backend-log-excerpt.txt +56 -0
- package/core/dev/spikes/task2145-evidence-20261004/graph.shadow +0 -0
- package/core/dev/spikes/task2145-evidence-20261004/graph.wal +0 -0
- package/core/dev/spikes/task2145-evidence-20261004/graph_corrupt_suspect.marker +1 -0
- package/core/plugins/kenaz/commands/Ansuz.md +11 -1
- package/core/plugins/kenaz/commands/kenaz.md +5 -3
- package/core/plugins/kenaz/scripts/kenaz-mode.js +6 -0
- package/core/plugins/kenaz/scripts/kenaz.test.js +68 -0
- package/core/plugins/kenaz/scripts/statusline.js +60 -0
- package/core/plugins/kenaz/scripts/statusline.test.js +41 -0
- package/core/plugins/kenaz/scripts/ygg.js +43 -4
- package/lib/cli.js +27 -10
- package/lib/cli.test.js +10 -0
- package/lib/dashboard.test.js +3 -3
- package/package.json +6 -6
- package/vendor/install-kenaz-cli.js +193 -16
- package/core/.collaboration/core/task-2264-single-after-capture.test.js +0 -61
|
@@ -122,3 +122,71 @@ test('ygg join activates the room in collaboration_projects; status explains how
|
|
|
122
122
|
const { buildStatus, renderStatus } = require('./ygg.js');
|
|
123
123
|
assert.match(renderStatus(buildStatus(cfg, { connected: false, nodes: [] }, null)), /not active in this project - run "ygg join <key>"/);
|
|
124
124
|
});
|
|
125
|
+
|
|
126
|
+
// TASK_2416: `ygg create` posts to the relay (config relay_url), then joins with the returned key.
|
|
127
|
+
// One local stub plays both the Kenaz backend and the relay; nothing touches ~/.kenaz or ~/.claude.
|
|
128
|
+
async function withStub(relayReply, fn) {
|
|
129
|
+
const http = require('http');
|
|
130
|
+
const seen = { relay: [], config: [] };
|
|
131
|
+
const cfg = { enabled: false, effective_project_key: 'tmp-room', node_name: 'n', publishable_key: '', collaboration_projects: [], relay_url: '' };
|
|
132
|
+
const srv = http.createServer((req, res) => {
|
|
133
|
+
let b = ''; req.on('data', (d) => { b += d; });
|
|
134
|
+
req.on('end', () => {
|
|
135
|
+
res.setHeader('content-type', 'application/json');
|
|
136
|
+
if (req.url === '/api/v1/teams') {
|
|
137
|
+
seen.relay.push({ method: req.method, body: b });
|
|
138
|
+
res.statusCode = relayReply.status; return res.end(JSON.stringify(relayReply.body));
|
|
139
|
+
}
|
|
140
|
+
if (req.url.startsWith('/api/nodes')) return res.end('[]');
|
|
141
|
+
if (req.url.startsWith('/api/yggdrasil/config')) {
|
|
142
|
+
if (req.method === 'POST') { seen.config.push(JSON.parse(b)); return res.end(JSON.stringify({ ok: true })); }
|
|
143
|
+
return res.end(JSON.stringify(cfg));
|
|
144
|
+
}
|
|
145
|
+
res.end(JSON.stringify({ success: true, created: false }));
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
await new Promise((r) => srv.listen(0, '127.0.0.1', r));
|
|
149
|
+
cfg.relay_url = 'ws://127.0.0.1:' + srv.address().port;
|
|
150
|
+
const prev = process.env.KENAZ_HTTP_PORT;
|
|
151
|
+
process.env.KENAZ_HTTP_PORT = String(srv.address().port);
|
|
152
|
+
try { return await fn(seen, cfg); } finally {
|
|
153
|
+
if (prev === undefined) delete process.env.KENAZ_HTTP_PORT; else process.env.KENAZ_HTTP_PORT = prev;
|
|
154
|
+
srv.close();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
test('ygg create posts the name to the relay, joins with the new key, prints it once (TASK_2416)', async () => {
|
|
159
|
+
const KEY = 'ygk_' + '2'.repeat(32);
|
|
160
|
+
await withStub({ status: 201, body: { ok: true, team_id: 't1', team_name: 'Acme', key: KEY, max_nodes: 10 } }, async (seen) => {
|
|
161
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ygg-create-'));
|
|
162
|
+
const { createTeam, renderStatus } = require('./ygg.js');
|
|
163
|
+
const s = await createTeam(dir, 'Acme', 'project');
|
|
164
|
+
assert.deepStrictEqual(JSON.parse(seen.relay[0].body), { name: 'Acme' });
|
|
165
|
+
assert.strictEqual(seen.config[0].publishable_key, KEY);
|
|
166
|
+
assert.strictEqual(seen.config[0].enabled, true);
|
|
167
|
+
assert.deepStrictEqual(seen.config[0].collaboration_projects, ['tmp-room']);
|
|
168
|
+
const out = renderStatus(s);
|
|
169
|
+
assert.match(out, new RegExp('share with teammates: kenaz ygg join ' + KEY));
|
|
170
|
+
assert.ok(!out.includes('Key: ' + KEY), 'status line keeps the key masked');
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('ygg create explains 429, 503, missing relay_url and unreachable relay (TASK_2416)', async () => {
|
|
175
|
+
const { createTeam } = require('./ygg.js');
|
|
176
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ygg-create-err-'));
|
|
177
|
+
await withStub({ status: 429, body: { ok: false, error: 'Too many team creations (limit 3 per hour)' } }, async (seen) => {
|
|
178
|
+
await assert.rejects(createTeam(dir, '', 'project'), /rate limited by the relay: Too many team creations/);
|
|
179
|
+
assert.strictEqual(seen.config.length, 0, 'no join after a failed create');
|
|
180
|
+
});
|
|
181
|
+
await withStub({ status: 503, body: { ok: false, error: 'REGISTRY_SERVICE_KEY not configured' } }, async () => {
|
|
182
|
+
await assert.rejects(createTeam(dir, '', 'project'), /does not offer self-serve keys: REGISTRY_SERVICE_KEY/);
|
|
183
|
+
});
|
|
184
|
+
await withStub({ status: 201, body: {} }, async (seen, cfg) => {
|
|
185
|
+
cfg.relay_url = '';
|
|
186
|
+
await assert.rejects(createTeam(dir, '', 'project'), /no relay_url configured/);
|
|
187
|
+
});
|
|
188
|
+
await withStub({ status: 201, body: {} }, async (seen, cfg) => {
|
|
189
|
+
cfg.relay_url = 'ws://127.0.0.1:1'; // nothing listens
|
|
190
|
+
await assert.rejects(createTeam(dir, '', 'project'), /cannot reach the relay at http:\/\/127\.0\.0\.1:1/);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// Claude Code status line: task counts of the Kenaz project around the session's cwd.
|
|
4
|
+
// ᚨ KenazAI · ▶ 1 進行中 · ☐ 12 待辦 · ⛔ 2 卡住 · ◎ 3 池 (zero counts left out)
|
|
5
|
+
// ᚨ KenazAI · 無待辦任務
|
|
6
|
+
// Prints nothing outside a Kenaz project. Reads files only (no HTTP): Claude Code runs it on every refresh.
|
|
7
|
+
// Registered in ~/.claude/settings.json `statusLine` by the installer only when the user has none.
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
const STATES = [['active', '▶', '進行中'], ['backlog', '☐', '待辦'], ['blocked', '⛔', '卡住'], ['pool', '◎', '池']];
|
|
12
|
+
|
|
13
|
+
function projectRoot(start) {
|
|
14
|
+
let d = path.resolve(start);
|
|
15
|
+
for (;;) {
|
|
16
|
+
if (fs.existsSync(path.join(d, '.kenaz', 'project.yaml'))) return d;
|
|
17
|
+
const up = path.dirname(d);
|
|
18
|
+
if (up === d) return null;
|
|
19
|
+
d = up;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function tasksDir(root) {
|
|
24
|
+
const k = path.join(root, '.kenaz', 'tasks');
|
|
25
|
+
return fs.existsSync(k) ? k : path.join(root, '.collaboration', 'tasks');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function count(dir) {
|
|
29
|
+
try { return fs.readdirSync(dir).filter((f) => /\.ya?ml$/.test(f)).length; } catch { return 0; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function projectName(root) {
|
|
33
|
+
try {
|
|
34
|
+
const m = fs.readFileSync(path.join(root, '.kenaz', 'project.yaml'), 'utf8').match(/^name:\s*(.+)$/m);
|
|
35
|
+
if (m) { const n = m[1].trim().replace(/^(['"])(.*)\1$/, '$2'); if (n) return n; }
|
|
36
|
+
} catch { /* fall back to the folder name */ }
|
|
37
|
+
return path.basename(root);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function render(cwd) {
|
|
41
|
+
const root = projectRoot(cwd);
|
|
42
|
+
if (!root) return '';
|
|
43
|
+
const dir = tasksDir(root);
|
|
44
|
+
const parts = STATES.map(([s, icon, label]) => [count(path.join(dir, s)), icon, label]).filter(([n]) => n > 0)
|
|
45
|
+
.map(([n, icon, label]) => icon + ' ' + n + ' ' + label);
|
|
46
|
+
return 'ᚨ ' + projectName(root) + ' · ' + (parts.length ? parts.join(' · ') : '無待辦任務');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function cwdFromInput(raw) {
|
|
50
|
+
try { const j = JSON.parse(raw); return (j.workspace && j.workspace.current_dir) || j.cwd || null; } catch { return null; }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (require.main === module) {
|
|
54
|
+
let raw = '';
|
|
55
|
+
const done = () => { try { process.stdout.write(render(cwdFromInput(raw) || process.cwd())); } catch { /* never break the status bar */ } };
|
|
56
|
+
if (process.stdin.isTTY) done();
|
|
57
|
+
else { process.stdin.setEncoding('utf8'); process.stdin.on('data', (c) => { raw += c; }); process.stdin.on('end', done); }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { render, projectRoot, cwdFromInput };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Status line task counts (statusline.js). Temp project dirs only.
|
|
2
|
+
const test = require('node:test');
|
|
3
|
+
const assert = require('node:assert');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { execFileSync } = require('child_process');
|
|
8
|
+
const { render, cwdFromInput } = require('./statusline');
|
|
9
|
+
|
|
10
|
+
function project(counts, name) {
|
|
11
|
+
const p = fs.mkdtempSync(path.join(os.tmpdir(), 'kenaz-sl-'));
|
|
12
|
+
fs.mkdirSync(path.join(p, '.kenaz'), { recursive: true });
|
|
13
|
+
fs.writeFileSync(path.join(p, '.kenaz', 'project.yaml'), name ? 'name: "' + name + '"\n' : 'id: x\n');
|
|
14
|
+
for (const [s, n] of Object.entries(counts)) {
|
|
15
|
+
fs.mkdirSync(path.join(p, '.kenaz', 'tasks', s), { recursive: true });
|
|
16
|
+
for (let i = 0; i < n; i++) fs.writeFileSync(path.join(p, '.kenaz', 'tasks', s, 'TASK_' + i + '.yaml'), 'id: TASK_' + i + '\n');
|
|
17
|
+
}
|
|
18
|
+
return p;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test('counts per state, zero states left out, from a subfolder of the project', () => {
|
|
22
|
+
const p = project({ active: 1, backlog: 12, blocked: 0, pool: 3, done: 40 }, 'Demo');
|
|
23
|
+
fs.mkdirSync(path.join(p, 'src', 'deep'), { recursive: true });
|
|
24
|
+
assert.strictEqual(render(path.join(p, 'src', 'deep')), 'ᚨ Demo · ▶ 1 進行中 · ☐ 12 待辦 · ◎ 3 池');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('no open tasks, folder name when project.yaml has no name', () => {
|
|
28
|
+
const p = project({ done: 5 });
|
|
29
|
+
assert.strictEqual(render(p), 'ᚨ ' + path.basename(p) + ' · 無待辦任務');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('control: outside a Kenaz project prints nothing', () => {
|
|
33
|
+
assert.strictEqual(render(fs.mkdtempSync(path.join(os.tmpdir(), 'kenaz-sl-none-'))), '');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('reads the cwd from the Claude Code stdin JSON (real process)', () => {
|
|
37
|
+
const p = project({ blocked: 2 }, 'Piped');
|
|
38
|
+
assert.strictEqual(cwdFromInput(JSON.stringify({ workspace: { current_dir: p } })), p);
|
|
39
|
+
const out = execFileSync(process.execPath, [path.join(__dirname, 'statusline.js')], { input: JSON.stringify({ workspace: { current_dir: p }, cwd: os.tmpdir() }), encoding: 'utf8' });
|
|
40
|
+
assert.strictEqual(out, 'ᚨ Piped · ⛔ 2 卡住');
|
|
41
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
|
-
// node ygg.js <join <key> [--project|--global] | status | leave> [--project-path <dir>] [--json]
|
|
3
|
+
// node ygg.js <join <key> [--project|--global] | create [--name N] [--project|--global] | status | leave> [--project-path <dir>] [--json]
|
|
4
4
|
// Yggdrasil setup without the Dashboard (TASK_2366). Thin client over the Rust backend's
|
|
5
5
|
// /api/yggdrasil/config + /api/peer/status (same per-project/global key rules as the Dashboard,
|
|
6
6
|
// TASK_2321/2349). k-cli `ygg` shells out to this file - the one implementation.
|
|
@@ -98,6 +98,10 @@ function renderStatus(s) {
|
|
|
98
98
|
'Delivery: ' + s.delivery_mode + (s.sessions.length ? '' : ' (no live sessions)'),
|
|
99
99
|
];
|
|
100
100
|
for (const x of s.sessions) L.push(' - ' + x.agent + ' ' + String(x.session_id).slice(0, 8) + ': ' + x.delivery_mode);
|
|
101
|
+
if (s.created) {
|
|
102
|
+
L.push('', 'New team key (shown once, save it): ' + s.created.key + (s.created.team_name ? ' [' + s.created.team_name + ']' : ''));
|
|
103
|
+
L.push('share with teammates: kenaz ygg join ' + s.created.key);
|
|
104
|
+
}
|
|
101
105
|
if (s.room_warning) L.push('WARNING: ' + s.room_warning);
|
|
102
106
|
if (s.delivery_note) L.push('Note: ' + s.delivery_note);
|
|
103
107
|
return L.join('\n');
|
|
@@ -143,6 +147,38 @@ async function join(dir, key, scope) {
|
|
|
143
147
|
return status(dir);
|
|
144
148
|
}
|
|
145
149
|
|
|
150
|
+
// TASK_2416: self-serve room key. POST <relay>/api/v1/teams (anonymous, per-IP limited), then join
|
|
151
|
+
// with the new key so this node is in the room immediately. The full key is returned once.
|
|
152
|
+
async function createTeam(dir, name, scope) {
|
|
153
|
+
const cfg = await getConfig(dir);
|
|
154
|
+
if (cfg.error) throw new Error(cfg.error);
|
|
155
|
+
const base = relayHttpBase(cfg.relay_url);
|
|
156
|
+
if (!base) throw new Error('no relay_url configured - cannot reach the relay to create a key (run "npx kenaz@latest install" or set relay_url in .kenaz/yggdrasil.yaml)');
|
|
157
|
+
let res;
|
|
158
|
+
try {
|
|
159
|
+
res = await fetch(base + '/api/v1/teams', {
|
|
160
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
161
|
+
body: JSON.stringify(name ? { name } : {}), signal: AbortSignal.timeout(15000),
|
|
162
|
+
});
|
|
163
|
+
} catch (e) {
|
|
164
|
+
throw new Error('cannot reach the relay at ' + base + ' (' + (e.cause && e.cause.code || e.message) + '); check your network and try again');
|
|
165
|
+
}
|
|
166
|
+
let data = null;
|
|
167
|
+
try { data = await res.json(); } catch (e) { /* non-JSON body */ }
|
|
168
|
+
if (!res.ok || !data || !data.key) {
|
|
169
|
+
const why = data && data.error ? data.error : 'HTTP ' + res.status;
|
|
170
|
+
if (res.status === 429) throw new Error('rate limited by the relay: ' + why);
|
|
171
|
+
if (res.status === 503) throw new Error('this relay does not offer self-serve keys: ' + why);
|
|
172
|
+
throw new Error('relay refused to create a key (HTTP ' + res.status + '): ' + why);
|
|
173
|
+
}
|
|
174
|
+
let s;
|
|
175
|
+
try { s = await join(dir, data.key, scope); } catch (e) {
|
|
176
|
+
// The key exists on the relay now; never lose it because the local join failed.
|
|
177
|
+
throw new Error('key created (' + data.key + ') but joining failed: ' + e.message + ' - retry with: kenaz ygg join ' + data.key);
|
|
178
|
+
}
|
|
179
|
+
return Object.assign(s, { created: { key: data.key, team_name: data.team_name || name || '' } });
|
|
180
|
+
}
|
|
181
|
+
|
|
146
182
|
async function leave(dir) {
|
|
147
183
|
const cfg = await getConfig(dir);
|
|
148
184
|
await postConfig(dir, postBody(cfg, { key_scope: 'clear_project', collaboration_projects: withCollab(cfg, false) }));
|
|
@@ -150,7 +186,7 @@ async function leave(dir) {
|
|
|
150
186
|
}
|
|
151
187
|
|
|
152
188
|
async function main(argv) {
|
|
153
|
-
const rest = []; let dir = process.env.CLAUDE_PROJECT_DIR || process.cwd(); let scope = 'project'; let json = false;
|
|
189
|
+
const rest = []; let dir = process.env.CLAUDE_PROJECT_DIR || process.cwd(); let scope = 'project'; let json = false; let name = '';
|
|
154
190
|
for (let i = 0; i < argv.length; i++) {
|
|
155
191
|
const a = argv[i];
|
|
156
192
|
if (a === '--project-path' && argv[i + 1]) dir = argv[++i];
|
|
@@ -158,6 +194,8 @@ async function main(argv) {
|
|
|
158
194
|
else if (a === '--global') scope = 'global';
|
|
159
195
|
else if (a === '--project') scope = 'project';
|
|
160
196
|
else if (a === '--json') json = true;
|
|
197
|
+
else if (a === '--name' && argv[i + 1]) name = argv[++i];
|
|
198
|
+
else if (a.startsWith('--name=')) name = a.slice(7);
|
|
161
199
|
else rest.push(a);
|
|
162
200
|
}
|
|
163
201
|
dir = path.resolve(dir);
|
|
@@ -165,9 +203,10 @@ async function main(argv) {
|
|
|
165
203
|
let s;
|
|
166
204
|
try {
|
|
167
205
|
if (sub === 'join') s = await join(dir, rest[1], scope);
|
|
206
|
+
else if (sub === 'create') s = await createTeam(dir, name, scope);
|
|
168
207
|
else if (sub === 'leave') s = await leave(dir);
|
|
169
208
|
else if (sub === 'status') s = await status(dir);
|
|
170
|
-
else throw new Error('unknown ygg subcommand: ' + sub + ' (join <key> | status | leave)');
|
|
209
|
+
else throw new Error('unknown ygg subcommand: ' + sub + ' (join <key> | create [--name N] | status | leave)');
|
|
171
210
|
} catch (e) {
|
|
172
211
|
console.error('ygg: ' + e.message + (/fetch failed|timeout|abort/i.test(e.message) ? ' (is the Kenaz backend running on ' + BASE() + '?)' : ''));
|
|
173
212
|
process.exit(1);
|
|
@@ -175,5 +214,5 @@ async function main(argv) {
|
|
|
175
214
|
console.log(json ? JSON.stringify(s, null, 2) : renderStatus(s));
|
|
176
215
|
}
|
|
177
216
|
|
|
178
|
-
module.exports = { nodeIsOnline, maskKey, buildStatus, renderStatus, join };
|
|
217
|
+
module.exports = { nodeIsOnline, maskKey, buildStatus, renderStatus, join, createTeam };
|
|
179
218
|
if (require.main === module) main(process.argv.slice(2));
|
package/lib/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ const dash = require('./dashboard');
|
|
|
14
14
|
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
15
15
|
const PKG = require('../package.json');
|
|
16
16
|
const MIN_NODE = 18;
|
|
17
|
-
const USAGE = 'usage: kenaz <install|uninstall|status|doctor|init|ygg join <key>|ygg status|ygg leave|alaya status|alaya register|
|
|
17
|
+
const USAGE = 'usage: kenaz <install|uninstall|status|doctor|init|ygg join <key>|ygg create [--name N]|ygg status|ygg leave|alaya status|alaya register|subagent [set <id>|unset] [--rigor r]|launch --provider deepseek [--model id]|stats [--days N] [--json]|learn <add|list|rm|review|merge|graduate>> [--dry-run] [--home <dir>] [--no-service] [--no-claude] [--no-codex] [--keep-dashboard] [--server-bin <path>]';
|
|
18
18
|
|
|
19
19
|
function parseArgs(argv) {
|
|
20
20
|
const o = { cmd: argv[0], flags: {} };
|
|
@@ -43,6 +43,7 @@ function coreModule(p, ...rel) {
|
|
|
43
43
|
const reaDetect = (p) => coreModule(p, '.specialist', 'tools', 'rea-detect.js');
|
|
44
44
|
const lessons = (p) => coreModule(p, '.specialist', 'tools', 'lessons.js');
|
|
45
45
|
const dispatchModel = (p) => coreModule(p, '.formation', 'tools', 'dispatch-model.js');
|
|
46
|
+
const providerLaunch = (p) => coreModule(p, '.formation', 'bifrost', 'provider-launch.js');
|
|
46
47
|
const kenazStats = (p) => coreModule(p, '.formation', 'tools', 'kenaz-stats.js');
|
|
47
48
|
|
|
48
49
|
function paths(home) {
|
|
@@ -114,7 +115,7 @@ function retryBusy(fn, tries = 10) {
|
|
|
114
115
|
}
|
|
115
116
|
}
|
|
116
117
|
|
|
117
|
-
function install(p, flags, out, deps = {}) {
|
|
118
|
+
async function install(p, flags, out, deps = {}) {
|
|
118
119
|
const dry = !!flags['dry-run'];
|
|
119
120
|
const tag = dry ? '[dry-run] ' : '';
|
|
120
121
|
const log = (m) => out(tag + m);
|
|
@@ -167,7 +168,10 @@ function install(p, flags, out, deps = {}) {
|
|
|
167
168
|
if (flags['no-service']) a.push('--no-service');
|
|
168
169
|
if (dry) a.push('--dry-run');
|
|
169
170
|
const ver = hasClaude ? inst.claudeVersion() : null;
|
|
170
|
-
|
|
171
|
+
let plan = null;
|
|
172
|
+
if (inst.main(a, out, () => ver, { sh: deps.sh, platform: deps.platform, uid: deps.uid, onPlan: (x) => { plan = x; } }) !== 0) rc = 1;
|
|
173
|
+
// TASK_2418: the service was (re)started above; confirm the running server is the one just installed.
|
|
174
|
+
if (!dry && plan && srv && !flags['no-service']) await inst.verifyRuntime(plan, srv.version, out, deps.verify);
|
|
171
175
|
}
|
|
172
176
|
if (dry && hasClaude) out(`[dry-run] note: plugin paths above are the packaged core; after install they live under ${path.join(p.core, 'plugins')}`);
|
|
173
177
|
|
|
@@ -235,6 +239,10 @@ async function doctor(p, flags, out, deps = {}) {
|
|
|
235
239
|
let ver = ''; try { ver = JSON.parse(r.body).version; } catch (_) { /* health body is optional */ }
|
|
236
240
|
add('server reachable', r.ok ? 'ok' : 'FAIL', r.detail + (ver ? `, version ${ver}` : ''));
|
|
237
241
|
|
|
242
|
+
try { // TASK_2413: which mechanism restarts the server after a reboot (read-only; `deps.sh` / `deps.platform` for tests)
|
|
243
|
+
const sm = installer().detectServiceMode(p.home, p.core, deps.sh, deps.platform);
|
|
244
|
+
add('service mode', sm.mode === 'manual' ? 'skip' : 'ok', sm.mode + ' (' + sm.detail + ')');
|
|
245
|
+
} catch (e) { add('service mode', 'skip', 'unknown: ' + e.message); }
|
|
238
246
|
const d = dash.detect(p.home, deps.dashboard);
|
|
239
247
|
if (!d.installed) add('dashboard', 'skip', 'no Kenaz Dashboard on this machine');
|
|
240
248
|
else {
|
|
@@ -277,20 +285,20 @@ async function doctor(p, flags, out, deps = {}) {
|
|
|
277
285
|
if (dm) {
|
|
278
286
|
const d = dm.describe(process.cwd());
|
|
279
287
|
const uniq = [...new Set(d.rows.map((r) => r.model))];
|
|
280
|
-
add('dispatch model', 'ok', (uniq.length === 1 ? uniq[0] : d.rows.map((r) => `${r.rigor}=${r.model}`).join(' ')) + (d.file ? '' : ' (built-in default)') + '; change with `kenaz
|
|
288
|
+
add('dispatch model', 'ok', (uniq.length === 1 ? uniq[0] : d.rows.map((r) => `${r.rigor}=${r.model}`).join(' ')) + (d.file ? '' : ' (built-in default)') + '; change with `kenaz subagent set <id>`');
|
|
281
289
|
}
|
|
282
290
|
// Optional and user-installed: never a failure.
|
|
283
291
|
const rea = reaDetect(p);
|
|
284
292
|
if (rea) {
|
|
285
293
|
const r = rea.detectRea({ home: p.home, projectPath: process.cwd() });
|
|
286
294
|
add('rea (optional)', r.installed ? 'ok' : 'skip', r.installed ? 'MCP registered in ' + [r.claude && 'claude', r.codex && 'codex'].filter(Boolean).join(', ')
|
|
287
|
-
: `not installed; \`${rea.SETUP_CMD}\` lets Ansuz analyse reference products` + (r.nodeOk ? '' : ` (needs Node >=22.19, found ${process.versions.node})`));
|
|
295
|
+
: `not installed; \`${r.setupCmd || rea.SETUP_CMD}\` lets Ansuz analyse reference products` + (r.nodeOk ? '' : ` (needs Node >=22.19, found ${process.versions.node})`));
|
|
288
296
|
}
|
|
289
297
|
for (const [l, s, d] of rows) out(`${s.padEnd(4)} ${l.padEnd(20)} ${d}`);
|
|
290
298
|
return rows.some((r) => r[1] === 'FAIL') ? 1 : 0;
|
|
291
299
|
}
|
|
292
300
|
|
|
293
|
-
// `kenaz ygg join|status|leave ...` and `kenaz init`: short forms of the same core scripts the
|
|
301
|
+
// `kenaz ygg join|create|status|leave ...` and `kenaz init`: short forms of the same core scripts the
|
|
294
302
|
// /kenaz (Claude) and $kenaz (Codex) commands run, from a normal terminal in the project folder.
|
|
295
303
|
function passthrough(argv, deps) {
|
|
296
304
|
const p = paths(os.homedir());
|
|
@@ -333,18 +341,24 @@ async function alaya(argv, out = console.log) {
|
|
|
333
341
|
|
|
334
342
|
function run(argv, out = console.log, deps = {}) {
|
|
335
343
|
if (argv[0] === 'alaya') return alaya(argv, out);
|
|
336
|
-
if (argv[0] === 'model') {
|
|
344
|
+
if (argv[0] === 'subagent' || argv[0] === 'model') { // model: pre-0.3.13 name
|
|
337
345
|
const dm = dispatchModel(paths(os.homedir()));
|
|
338
346
|
if (!dm) { out('kenaz-core not installed: run `kenaz install`'); return 1; }
|
|
339
347
|
return dm.main(argv.slice(1), out);
|
|
340
348
|
}
|
|
349
|
+
if (argv[0] === 'launch') {
|
|
350
|
+
const pl = providerLaunch(paths(os.homedir()));
|
|
351
|
+
if (!pl) { out('kenaz-core not installed: run `kenaz install`'); return 1; }
|
|
352
|
+
return pl.main(argv.slice(1), out, deps);
|
|
353
|
+
}
|
|
341
354
|
if (argv[0] === 'stats') {
|
|
342
355
|
const ks = kenazStats(paths(os.homedir()));
|
|
343
356
|
if (!ks) { out('kenaz-core not installed: run `kenaz install`'); return 1; }
|
|
344
357
|
return ks.main(argv.slice(1), out);
|
|
345
358
|
}
|
|
346
359
|
if (argv[0] === 'learn') {
|
|
347
|
-
const
|
|
360
|
+
const hi = argv.indexOf('--home');
|
|
361
|
+
const lm = lessons(paths(hi >= 0 && argv[hi + 1] ? path.resolve(argv[hi + 1]) : os.homedir()));
|
|
348
362
|
if (!lm) { out('kenaz-core not installed: run `kenaz install`'); return 1; }
|
|
349
363
|
return lm.main(argv.slice(1), out);
|
|
350
364
|
}
|
|
@@ -353,8 +367,11 @@ function run(argv, out = console.log, deps = {}) {
|
|
|
353
367
|
const fn = { install, uninstall, status, doctor }[cmd];
|
|
354
368
|
if (!fn) { out(USAGE); return 2; }
|
|
355
369
|
if (!nodeOk()) { out(`Node.js >= ${MIN_NODE} is required (found ${process.versions.node}). Install it from https://nodejs.org and rerun.`); return 1; }
|
|
356
|
-
|
|
357
|
-
|
|
370
|
+
const fail = (e) => { out('kenaz ' + cmd + ': ERROR ' + e.message); return 1; };
|
|
371
|
+
try {
|
|
372
|
+
const r = fn(paths(path.resolve(flags.home || os.homedir())), flags, out, deps);
|
|
373
|
+
return r && typeof r.then === 'function' ? r.catch(fail) : r;
|
|
374
|
+
} catch (e) { return fail(e); }
|
|
358
375
|
}
|
|
359
376
|
|
|
360
377
|
module.exports = { run, parseArgs, platformServer, paths, stopOwnServer, retryBusy };
|
package/lib/cli.test.js
CHANGED
|
@@ -107,3 +107,13 @@ test('retryBusy retries a locked copy and then succeeds; other errors are thrown
|
|
|
107
107
|
assert.strictEqual(n, 3);
|
|
108
108
|
assert.throws(() => retryBusy(() => { const e = new Error('nope'); e.code = 'ENOENT'; throw e; }, 5), /nope/);
|
|
109
109
|
});
|
|
110
|
+
|
|
111
|
+
// TASK_2413: doctor names the active service mode. Stubbed `sh` + forced platform: no real system command runs.
|
|
112
|
+
test('doctor reports the service mode (cron on a Linux box without systemd, manual when nothing is installed)', async () => {
|
|
113
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'kenaz-doc-'));
|
|
114
|
+
const dashboard = { candidateDirs: () => [], procs: () => [], portPids: () => [] };
|
|
115
|
+
const go = async (sh) => { const l = []; await run(['doctor', '--home', home, '--no-claude', '--no-codex'], (x) => l.push(x), { dashboard, sh, platform: 'linux' }); return l.find((x) => /service mode/.test(x)); };
|
|
116
|
+
const cron = await go((c) => (c === 'crontab' ? { status: 0, stdout: '@reboot x # kenaz-server (managed by kenaz install)\n' } : { status: 1, stdout: '' }));
|
|
117
|
+
assert.match(cron, /^ok\s+service mode\s+cron/);
|
|
118
|
+
assert.match(await go(() => ({ status: 1, stdout: '' })), /^skip\s+service mode\s+manual/);
|
|
119
|
+
});
|
package/lib/dashboard.test.js
CHANGED
|
@@ -12,9 +12,9 @@ const tmpHome = () => fs.mkdtempSync(path.join(os.tmpdir(), 'kenaz-dash-'));
|
|
|
12
12
|
const fake = (o = {}) => ({ candidateDirs: () => o.dirs || [], procs: () => o.procs || [], portPids: () => o.owners || [] });
|
|
13
13
|
const DASH_PROC = { pid: 4242, name: 'kenaz-dashboard.exe', exe: 'C:/Users/u/AppData/Local/BLOCK ACTION/kenaz-dashboard.exe' };
|
|
14
14
|
|
|
15
|
-
test('install with a running Dashboard owning the port: Chinese steps, rc 1, nothing written', () => {
|
|
15
|
+
test('install with a running Dashboard owning the port: Chinese steps, rc 1, nothing written', async () => {
|
|
16
16
|
const home = tmpHome(), lines = [];
|
|
17
|
-
const rc = run(['install', '--home', home, '--no-claude', '--no-codex'], (m) => lines.push(m), { dashboard: fake({ procs: [DASH_PROC], owners: [4242] }) });
|
|
17
|
+
const rc = await run(['install', '--home', home, '--no-claude', '--no-codex'], (m) => lines.push(m), { dashboard: fake({ procs: [DASH_PROC], owners: [4242] }) });
|
|
18
18
|
assert.strictEqual(rc, 1);
|
|
19
19
|
assert.ok(lines.join('\n').includes('npx kenaz install') && lines.join('\n').includes('開機時啟動'));
|
|
20
20
|
assert.ok(!fs.existsSync(path.join(home, '.kenaz')), 'no files, no service');
|
|
@@ -43,7 +43,7 @@ test('a foreign process on the port is not treated as the Dashboard', () => {
|
|
|
43
43
|
|
|
44
44
|
test('doctor flags a running Dashboard, its port and Dashboard plugin dirs', async () => {
|
|
45
45
|
const home = tmpHome(), lines = [];
|
|
46
|
-
const rc = await run(['doctor', '--home', home, '--no-claude', '--no-codex'], (m) => lines.push(m), { dashboard: fake({ dirs: [os.tmpdir()], procs: [DASH_PROC], owners: [4242] }) });
|
|
46
|
+
const rc = await run(['doctor', '--home', home, '--no-claude', '--no-codex'], (m) => lines.push(m), { dashboard: fake({ dirs: [os.tmpdir()], procs: [DASH_PROC], owners: [4242] }), sh: () => ({ status: 1, stdout: '' }) });
|
|
47
47
|
assert.strictEqual(rc, 1);
|
|
48
48
|
assert.ok(lines.some((l) => /^FAIL\s+dashboard running/.test(l)) && lines.some((l) => /^FAIL\s+port \d+ owner/.test(l)));
|
|
49
49
|
const d = dash.detect(home, fake({ dirs: [os.tmpdir()] }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kenaz",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.13",
|
|
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,11 @@
|
|
|
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.13",
|
|
24
|
+
"@kenazai/server-darwin-x64": "0.3.13",
|
|
25
|
+
"@kenazai/server-linux-arm64": "0.3.13",
|
|
26
|
+
"@kenazai/server-linux-x64": "0.3.13",
|
|
27
|
+
"@kenazai/server-win32-x64": "0.3.13"
|
|
28
28
|
},
|
|
29
29
|
"license": "UNLICENSED",
|
|
30
30
|
"repository": {
|