kenaz 0.3.20 → 0.3.22
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 +3 -0
- package/core/.claude/commands/architect.md +3 -0
- package/core/.claude/commands/incident.md +3 -0
- package/core/.claude/commands/review.md +3 -0
- package/core/.claude/commands/security.md +3 -0
- package/core/.claude/commands/spec.md +3 -0
- package/core/.claude/hooks/__tests__/peer-session-fill.test.js +37 -0
- package/core/.claude/hooks/_ansuz-conversation.js +32 -1
- package/core/.claude/hooks/peer-optin.test.js +74 -0
- package/core/.claude/hooks/peer-session-fill.js +38 -0
- package/core/.claude/hooks/turn-context.js +5 -4
- package/core/.claude/settings.json +9 -0
- package/core/.claude/skills/architect_agent/SKILL.md +1 -1
- package/core/.claude/skills/assessment_agent/SKILL.md +1 -1
- package/core/.claude/skills/code_reviewer/SKILL.md +1 -1
- package/core/.claude/skills/incident_response/SKILL.md +1 -1
- package/core/.claude/skills/security_agent/SKILL.md +1 -1
- package/core/.claude/skills/spec_optimizer/SKILL.md +1 -1
- package/core/.formation/bifrost/codex-hooks-repair.js +115 -0
- package/core/.formation/bifrost/codex-hooks-repair.test.js +94 -0
- package/core/.formation/bifrost/openai/codex-hook-shim.js +26 -7
- package/core/.formation/bifrost/openai/codex-hook-shim.test.js +2 -0
- package/core/.formation/bifrost/openai/codex-peer-pull.test.js +42 -0
- package/core/.formation/bifrost/provider-launch.js +2 -1
- package/core/.formation/bifrost/setup-codex.js +22 -12
- package/core/.specialist/Ansuz/RULES.md +7 -0
- package/core/KENAZ_CORE_VERSION +1 -1
- package/core/dashboard/scripts/install-kenaz-cli.js +456 -0
- package/core/dev/scripts/relay-admin-ui.js +154 -0
- package/core/dev/scripts/relay-admin-ui.test.js +112 -0
- package/core/dev/scripts/relay-admin.js +19 -10
- package/core/dev/scripts/skill-routing-replay.js +80 -0
- package/core/dev/scripts/skill-routing-replay.test.js +24 -0
- package/core/dev/specs/peer/codex-hook-shim-peer.test.js +17 -5
- package/core/plugins/kenaz/INVENTORY.md +1 -1
- package/core/plugins/kenaz/commands/Ansuz.md +3 -0
- package/core/plugins/kenaz/commands/architect.md +3 -0
- package/core/plugins/kenaz/commands/incident.md +3 -0
- package/core/plugins/kenaz/commands/review.md +3 -0
- package/core/plugins/kenaz/commands/security.md +3 -0
- package/core/plugins/kenaz/commands/spec.md +3 -0
- package/core/plugins/kenaz/guidance.md +1 -0
- package/core/plugins/kenaz/hooks/hooks.json +9 -0
- package/core/plugins/kenaz/hooks/kenaz-hook.js +1 -0
- package/core/plugins/kenaz/scripts/kenaz-mode.js +5 -0
- package/core/plugins/kenaz/scripts/sync.js +1 -1
- package/core/plugins/kenaz/scripts/ygg.js +40 -4
- package/core/plugins/kenaz/scripts/ygg.test.js +63 -3
- package/core/plugins/kenaz/skills/architect_agent/SKILL.md +1 -1
- package/core/plugins/kenaz/skills/assessment_agent/SKILL.md +1 -1
- package/core/plugins/kenaz/skills/code_reviewer/SKILL.md +1 -1
- package/core/plugins/kenaz/skills/incident_response/SKILL.md +1 -1
- package/core/plugins/kenaz/skills/security_agent/SKILL.md +1 -1
- package/core/plugins/kenaz/skills/spec_optimizer/SKILL.md +1 -1
- package/core/plugins/kenaz-peer/hooks/register.test.ts +59 -2
- package/core/plugins/kenaz-peer/hooks/register.ts +24 -1
- package/lib/cli.js +4 -0
- package/package.json +6 -6
- package/core/plugins/kenaz/skills/feature_agent/SKILL.md +0 -289
- /package/core/{.claude → dev/archive}/skills/feature_agent/SKILL.md +0 -0
|
@@ -5,6 +5,8 @@
|
|
|
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.
|
|
7
7
|
const path = require('path');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
8
10
|
|
|
9
11
|
const BASE = () => 'http://127.0.0.1:' + (process.env.KENAZ_HTTP_PORT || process.env.KENAZ_BACKEND_PORT || '18080');
|
|
10
12
|
|
|
@@ -86,6 +88,33 @@ function buildStatus(cfg, relay, peer) {
|
|
|
86
88
|
};
|
|
87
89
|
}
|
|
88
90
|
|
|
91
|
+
// TASK_2461: push needs the kenaz-peer plugin loaded by Claude Code (only in Ansuz sessions). join/create
|
|
92
|
+
// leave the node ready: register kenaz-peer in ~/.claude/settings.json when missing (same writer as the
|
|
93
|
+
// installer). Returns {state: 'ok'|'added'|'missing'|'skipped'|'error', note}. Never throws.
|
|
94
|
+
function loadInstaller() {
|
|
95
|
+
for (const p of ['../../../dashboard/scripts/install-kenaz-cli.js', '../../../packages/kenaz/vendor/install-kenaz-cli.js']) {
|
|
96
|
+
try { return require(path.join(__dirname, p)); } catch (e) { if (e.code !== 'MODULE_NOT_FOUND') throw e; }
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
function peerPlugin(home, register, version) {
|
|
101
|
+
try {
|
|
102
|
+
const inst = loadInstaller();
|
|
103
|
+
if (!inst) return { state: 'skipped', note: 'installer not found' };
|
|
104
|
+
const settings = path.join(home, '.claude', 'settings.json');
|
|
105
|
+
if (!fs.existsSync(path.join(home, '.claude'))) return { state: 'skipped', note: 'claude is not installed' };
|
|
106
|
+
const ver = version === undefined ? inst.claudeVersion() : version;
|
|
107
|
+
if (ver && inst.cmpVersion(ver, inst.MIN_CLI) < 0) return { state: 'skipped', note: 'claude ' + ver.join('.') + ' < ' + inst.MIN_CLI.join('.') + ' (run `claude update`)' };
|
|
108
|
+
const core = path.resolve(process.env.KENAZ_CORE_DIR || path.join(__dirname, '..', '..', '..'));
|
|
109
|
+
const dir = path.join(core, 'plugins', 'kenaz-peer');
|
|
110
|
+
const list = inst.entriesOf(inst.loadSettings(settings));
|
|
111
|
+
if (list.some((e) => inst.norm(e) === inst.norm(dir))) return { state: 'ok', note: 'kenaz-peer plugin is registered' };
|
|
112
|
+
if (!register || !fs.existsSync(dir)) return { state: 'missing', note: 'kenaz-peer plugin is not registered in ' + settings + (fs.existsSync(dir) ? ' (run `kenaz ygg join <key>` or `kenaz install`)' : ' (run `npx kenaz@latest install`)') };
|
|
113
|
+
inst.registerPlugin(settings, dir, false);
|
|
114
|
+
return { state: 'added', note: 'registered the kenaz-peer plugin in ' + settings + '; restart Claude Code (new session) for it to load, then run /Ansuz for idle push' };
|
|
115
|
+
} catch (e) { return { state: 'error', note: 'could not check the kenaz-peer plugin: ' + e.message }; }
|
|
116
|
+
}
|
|
117
|
+
|
|
89
118
|
function renderStatus(s) {
|
|
90
119
|
const L = [
|
|
91
120
|
'Yggdrasil room: ' + s.room,
|
|
@@ -102,14 +131,19 @@ function renderStatus(s) {
|
|
|
102
131
|
'Last poll: ' + (s.last_poll_at ? s.last_poll_at + (s.last_poll_ok === false ? ' (FAILED)' : ' (ok)') : 'none yet'),
|
|
103
132
|
'Delivery: ' + s.delivery_mode + (s.sessions.length ? '' : ' (no live sessions)'),
|
|
104
133
|
];
|
|
105
|
-
for (const x of s.sessions) L.push('
|
|
134
|
+
for (const x of s.sessions) L.push(' ' + (x.name || x.agent) + ' (' + String(x.session_id).slice(0, 8) + ') - ' + x.delivery_mode);
|
|
106
135
|
if (s.created) {
|
|
107
136
|
L.push('', 'New team key (shown once, save it): ' + s.created.key + (s.created.team_name ? ' [' + s.created.team_name + ']' : ''));
|
|
108
137
|
L.push('share with teammates: kenaz ygg join ' + s.created.key);
|
|
109
138
|
}
|
|
110
139
|
if (s.same_name_warning) L.push('WARNING: ' + s.same_name_warning);
|
|
111
140
|
if (s.room_warning) L.push('WARNING: ' + s.room_warning);
|
|
112
|
-
|
|
141
|
+
const pl = s.plugin;
|
|
142
|
+
if (pl && ['added', 'missing', 'error'].includes(pl.state)) L.push('Push plugin: ' + pl.note);
|
|
143
|
+
// The server cannot know whether the plugin is registered; when this machine knows it is, do not repeat "register it".
|
|
144
|
+
const stale = pl && (pl.state === 'ok' || pl.state === 'added') && /register the kenaz-peer plugin|may not be registered/.test(s.delivery_note || '');
|
|
145
|
+
if (stale) L.push('Note: No push channel yet: the kenaz-peer plugin is registered but only serves Ansuz sessions. Run /Ansuz or /kenaz:Ansuz in this session' + (pl.state === 'added' ? ' after restarting Claude Code' : '') + ' for idle push; until then messages arrive on your next prompt.');
|
|
146
|
+
else if (s.delivery_note) L.push('Note: ' + s.delivery_note);
|
|
113
147
|
return L.join('\n');
|
|
114
148
|
}
|
|
115
149
|
|
|
@@ -200,7 +234,7 @@ async function leave(dir) {
|
|
|
200
234
|
}
|
|
201
235
|
|
|
202
236
|
async function main(argv) {
|
|
203
|
-
const rest = []; let dir = process.env.CLAUDE_PROJECT_DIR || process.cwd(); let scope = 'project'; let json = false; let name = '';
|
|
237
|
+
const rest = []; let dir = process.env.CLAUDE_PROJECT_DIR || process.cwd(); let scope = 'project'; let json = false; let name = ''; let home = os.homedir();
|
|
204
238
|
for (let i = 0; i < argv.length; i++) {
|
|
205
239
|
const a = argv[i];
|
|
206
240
|
if (a === '--project-path' && argv[i + 1]) dir = argv[++i];
|
|
@@ -208,6 +242,7 @@ async function main(argv) {
|
|
|
208
242
|
else if (a === '--global') scope = 'global';
|
|
209
243
|
else if (a === '--project') scope = 'project';
|
|
210
244
|
else if (a === '--json') json = true;
|
|
245
|
+
else if (a === '--home' && argv[i + 1]) home = argv[++i];
|
|
211
246
|
else if (a === '--name' && argv[i + 1]) name = argv[++i];
|
|
212
247
|
else if (a.startsWith('--name=')) name = a.slice(7);
|
|
213
248
|
else rest.push(a);
|
|
@@ -221,6 +256,7 @@ async function main(argv) {
|
|
|
221
256
|
else if (sub === 'leave') s = await leave(dir);
|
|
222
257
|
else if (sub === 'status') s = await status(dir);
|
|
223
258
|
else throw new Error('unknown ygg subcommand: ' + sub + ' (join <key> | create [--name N] | status | leave)');
|
|
259
|
+
s.plugin = peerPlugin(path.resolve(home), sub === 'join' || sub === 'create');
|
|
224
260
|
} catch (e) {
|
|
225
261
|
console.error('ygg: ' + e.message + (/fetch failed|timeout|abort/i.test(e.message) ? ' (is the Kenaz backend running on ' + BASE() + '?)' : ''));
|
|
226
262
|
process.exit(1);
|
|
@@ -228,5 +264,5 @@ async function main(argv) {
|
|
|
228
264
|
console.log(json ? JSON.stringify(s, null, 2) : renderStatus(s));
|
|
229
265
|
}
|
|
230
266
|
|
|
231
|
-
module.exports = { nodeIsOnline, maskKey, buildStatus, renderStatus, join, createTeam };
|
|
267
|
+
module.exports = { peerPlugin, nodeIsOnline, maskKey, buildStatus, renderStatus, join, createTeam };
|
|
232
268
|
if (require.main === module) main(process.argv.slice(2));
|
|
@@ -29,10 +29,10 @@ async function withBackend(persist, fn) {
|
|
|
29
29
|
try { return await fn(srv.address().port); } finally { srv.close(); }
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
function cli(port, dir) {
|
|
32
|
+
function cli(port, dir, home = fs.mkdtempSync(path.join(os.tmpdir(), 'ygg-home-'))) { // never the real ~/.claude
|
|
33
33
|
return new Promise((resolve) => {
|
|
34
|
-
const c = spawn(process.execPath, [YGG, 'join', 'ygk_abc', '--project-path', dir],
|
|
35
|
-
{ env: Object.assign({}, process.env, { KENAZ_HTTP_PORT: String(port) }) });
|
|
34
|
+
const c = spawn(process.execPath, [YGG, 'join', 'ygk_abc', '--project-path', dir].concat(['--home', home]),
|
|
35
|
+
{ env: Object.assign({}, process.env, { KENAZ_HTTP_PORT: String(port), KENAZ_CORE_DIR: CORE }) });
|
|
36
36
|
let out = ''; let err = '';
|
|
37
37
|
c.stdout.on('data', (d) => { out += d; }); c.stderr.on('data', (d) => { err += d; });
|
|
38
38
|
c.on('close', (code) => resolve({ code, out, err }));
|
|
@@ -70,3 +70,63 @@ test('join refuses when the backend reports no effective_project_key (nothing to
|
|
|
70
70
|
assert.match(r.err, /cannot work out this project's room/);
|
|
71
71
|
} finally { srv.close(); }
|
|
72
72
|
});
|
|
73
|
+
|
|
74
|
+
// TASK_2461: join leaves the node push-ready (kenaz-peer registered), touching only the --home it is given.
|
|
75
|
+
// a temp core: the checkout may itself be a .kenaz-worktrees path, which the installer refuses to register
|
|
76
|
+
const CORE = fs.mkdtempSync(path.join(os.tmpdir(), 'ygg2461-core-'));
|
|
77
|
+
const CORE_PEER = path.join(CORE, 'plugins', 'kenaz-peer');
|
|
78
|
+
fs.mkdirSync(CORE_PEER, { recursive: true });
|
|
79
|
+
function tempHome(settings) {
|
|
80
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'ygg2461-'));
|
|
81
|
+
fs.mkdirSync(path.join(home, '.claude'));
|
|
82
|
+
const f = path.join(home, '.claude', 'settings.json');
|
|
83
|
+
fs.writeFileSync(f, settings);
|
|
84
|
+
return { home, f };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
test('join registers a missing kenaz-peer in the temp settings.json, names the restart, and is idempotent', async () => {
|
|
88
|
+
const { home, f } = tempHome(JSON.stringify({ env: { CLAUDE_CODE_PLUGIN_DIRS: '/other/plugin' } }));
|
|
89
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ygg2461-'));
|
|
90
|
+
await withBackend(true, async (port) => {
|
|
91
|
+
const r = await cli(port, dir, home);
|
|
92
|
+
assert.strictEqual(r.code, 0, r.err);
|
|
93
|
+
assert.ok(JSON.parse(fs.readFileSync(f, 'utf8')).env.CLAUDE_CODE_PLUGIN_DIRS.split(path.delimiter).includes(CORE_PEER));
|
|
94
|
+
assert.match(r.out, /restart Claude Code/);
|
|
95
|
+
const after = fs.readFileSync(f, 'utf8');
|
|
96
|
+
const r2 = await cli(port, dir, home);
|
|
97
|
+
assert.strictEqual(r2.code, 0, r2.err);
|
|
98
|
+
assert.strictEqual(fs.readFileSync(f, 'utf8'), after);
|
|
99
|
+
assert.doesNotMatch(r2.out, /restart Claude Code/);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('join leaves settings.json byte-identical when kenaz-peer is already registered', async () => {
|
|
104
|
+
const txt = JSON.stringify({ env: { CLAUDE_CODE_PLUGIN_DIRS: CORE_PEER } });
|
|
105
|
+
const { home, f } = tempHome(txt);
|
|
106
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ygg2461-'));
|
|
107
|
+
await withBackend(true, async (port) => {
|
|
108
|
+
const r = await cli(port, dir, home);
|
|
109
|
+
assert.strictEqual(r.code, 0, r.err);
|
|
110
|
+
assert.strictEqual(fs.readFileSync(f, 'utf8'), txt);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('rendered status: registered but not Ansuz never says to register the plugin; unregistered says what to do', () => {
|
|
115
|
+
const { renderStatus, buildStatus } = require('./ygg.js');
|
|
116
|
+
const base = buildStatus({ effective_project_key: 'r', node_name: 'n', enabled: true, collaboration_projects: ['r'] }, { connected: true, nodes: [] },
|
|
117
|
+
{ delivery_mode: 'pull-on-prompt', delivery_note: 'No push channel. No kenaz-peer plugin heartbeat seen: register the kenaz-peer plugin, then run /Ansuz' });
|
|
118
|
+
const ok = renderStatus(Object.assign({}, base, { plugin: { state: 'ok', note: 'x' } }));
|
|
119
|
+
assert.doesNotMatch(ok, /register the kenaz-peer plugin/);
|
|
120
|
+
assert.match(ok, /only serves Ansuz sessions/);
|
|
121
|
+
const miss = renderStatus(Object.assign({}, base, { plugin: { state: 'missing', note: 'kenaz-peer plugin is not registered in S (run `kenaz ygg join <key>` or `kenaz install`)' } }));
|
|
122
|
+
assert.match(miss, /Push plugin:.*not registered/);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('TASK_2463: rendered status shows each session as name (short id) - mode', () => {
|
|
126
|
+
const { renderStatus, buildStatus } = require('./ygg.js');
|
|
127
|
+
const st = buildStatus({ effective_project_key: 'r', node_name: 'n', enabled: true, collaboration_projects: ['r'] }, { connected: true, nodes: [] },
|
|
128
|
+
{ delivery_mode: 'push', sessions: [{ session_id: 'abcdef123456', agent: 'claude', name: 'my-work', delivery_mode: 'push' }, { session_id: 'zzzzzzzz9999', agent: 'codex', delivery_mode: 'pull-on-prompt' }] });
|
|
129
|
+
const out = renderStatus(st);
|
|
130
|
+
assert.match(out, /^ {2}my-work \(abcdef12\) - push$/m);
|
|
131
|
+
assert.match(out, /^ {2}codex \(zzzzzzzz\) - pull-on-prompt$/m);
|
|
132
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: Architect Agent
|
|
3
|
-
description:
|
|
3
|
+
description: Architecture design and requirement breakdown into parallel tasks with interfaces and dependencies. Use before carding multi-module work, when planning a migration (for example replacing a subsystem), or when designing a new protocol. 架構設計、需求拆解、遷移規劃。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Architect Agent (系統架構師)
|
|
@@ -5,7 +5,7 @@ aliases:
|
|
|
5
5
|
- assessment
|
|
6
6
|
- evaluate
|
|
7
7
|
- review
|
|
8
|
-
description:
|
|
8
|
+
description: Deep evaluation of a feature, API, repo, paper or product with improvement options and priorities. Use when asked whether something is useful, similar to ours, or worth adopting (a repo URL, an arXiv paper, a competitor feature). 評估、可行性、值不值得採用、優先級。
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# Assessment Agent(評估 Agent)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: Code Reviewer
|
|
3
|
-
description:
|
|
3
|
+
description: Review a code diff for bugs and quality with Blocker/Suggestion/Nit grading. Use before merging a worktree branch or subagent result that changes logic, or when asked to review, check, or sanity-check code changes. 程式碼審查、合併前審 diff、三級分類。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Code Reviewer (程式碼審查 Agent)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: Incident Response
|
|
3
|
-
description:
|
|
3
|
+
description: Incident handling with SEV1-4 triage, runbook and post-mortem. Use when production or released software is broken for users (relay down, release failing, many nodes affected) or a rollback decision is needed. 事件應變、嚴重性分級、復盤。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Incident Response (事件應變 Agent)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: Security Agent
|
|
3
|
-
description:
|
|
3
|
+
description: Security review and STRIDE threat modeling. Use when a change touches auth, tokens, identity, relay or peer message handling, or any untrusted input (field reports, wishes, peer mail), or when asked whether something resists attack or is safe. 安全審查、威脅建模、漏洞評估、惡意輸入。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Security Agent (安全工程 Agent)
|
|
@@ -4,7 +4,7 @@ aliases:
|
|
|
4
4
|
- 規格優化
|
|
5
5
|
- spec
|
|
6
6
|
- optimizer
|
|
7
|
-
description:
|
|
7
|
+
description: Consolidate specs and docs, remove dead or duplicated rules, and turn .failure/.rule/task-card findings into improvements. Use when asked what is unused or unnecessary, or to tidy docs and specs. 規格優化、文件整理、清理無用系統。
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Spec Optimizer Agent
|
|
@@ -2,14 +2,17 @@ import { test, expect, mock } from 'claude-code/testing'
|
|
|
2
2
|
|
|
3
3
|
const msg = { id: 'm1', framed: '[Peer message] hello' }
|
|
4
4
|
|
|
5
|
-
function setup(on: any, rows: () => unknown[]) {
|
|
5
|
+
function setup(on: any, rows: () => unknown[], marker?: () => string | null) {
|
|
6
6
|
const calls: string[] = []
|
|
7
7
|
let delivered = false
|
|
8
8
|
on('session.start', async (_$: any, e: any) => ({ cwd: e.cwd }))
|
|
9
9
|
on('session.id', async () => ({ value: 'sess-1' }))
|
|
10
10
|
on('session.cwd', async () => ({ value: '/tmp/proj' }))
|
|
11
11
|
on('session.messages', async () => ({ value: rows() }))
|
|
12
|
-
on('fs.read', async (
|
|
12
|
+
on('fs.read', async (_$: any, e: any) => {
|
|
13
|
+
const m = marker?.()
|
|
14
|
+
return String(e.path ?? e.file ?? '').endsWith('/sess-1.json') && m ? { value: m } : { deny: 'no pin' }
|
|
15
|
+
})
|
|
13
16
|
on('http.fetch', async (_$: any, e: any) => {
|
|
14
17
|
const url = String(e.url)
|
|
15
18
|
calls.push(`${e.init?.method ?? 'GET'} ${url}`)
|
|
@@ -144,3 +147,57 @@ test('plugin-namespaced /kenaz:Ansuz also activates delivery', async ($, on) =>
|
|
|
144
147
|
expect(calls[0]).toContain('POST http://127.0.0.1:18080/api/peer/mod-heartbeat')
|
|
145
148
|
expect(submits.length).toBe(1)
|
|
146
149
|
})
|
|
150
|
+
|
|
151
|
+
// TASK_2465: collaboration opt-in (marker written by peer-session-fill.js, or KENAZ_PEER_OPTIN=1 from kenaz launch).
|
|
152
|
+
const PLAIN = [{ role: 'user', content: 'hello there' }]
|
|
153
|
+
const MARKER_ENV = { CLAUDE_CODE_ENTRYPOINT: 'cli', KENAZ_PEER_OPTIN_DIR: '/m' }
|
|
154
|
+
|
|
155
|
+
test('TASK_2465 privacy: plain session without opt-in never heartbeats, peeks or submits', async ($, on) => {
|
|
156
|
+
const clock = mock.clock(on)
|
|
157
|
+
mock.env(on, MARKER_ENV)
|
|
158
|
+
const { calls, submits } = setup(on, () => PLAIN, () => null)
|
|
159
|
+
await $.session.start(START)
|
|
160
|
+
await clock.advance(15_000)
|
|
161
|
+
expect(calls.length).toBe(0)
|
|
162
|
+
expect(submits.length).toBe(0)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('TASK_2465: opt-in marker makes a plain session heartbeat and receive push', async ($, on) => {
|
|
166
|
+
const clock = mock.clock(on)
|
|
167
|
+
mock.env(on, MARKER_ENV)
|
|
168
|
+
const { calls, submits } = setup(on, () => PLAIN, () => JSON.stringify({ ts: Date.now() }))
|
|
169
|
+
await $.session.start(START)
|
|
170
|
+
await clock.advance(5_000)
|
|
171
|
+
await clock.advance(5_000)
|
|
172
|
+
expect(calls[0]).toContain('POST http://127.0.0.1:18080/api/peer/mod-heartbeat')
|
|
173
|
+
expect(submits.length).toBe(1)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
test('TASK_2465: an expired marker is not an opt-in', async ($, on) => {
|
|
177
|
+
const clock = mock.clock(on)
|
|
178
|
+
mock.env(on, MARKER_ENV)
|
|
179
|
+
const { calls, submits } = setup(on, () => PLAIN, () => JSON.stringify({ ts: Date.now() - 25 * 3600 * 1000 }))
|
|
180
|
+
await $.session.start(START)
|
|
181
|
+
await clock.advance(15_000)
|
|
182
|
+
expect(calls.length).toBe(0)
|
|
183
|
+
expect(submits.length).toBe(0)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
test('TASK_2465: KENAZ_PEER_OPTIN=1 does not opt in a claude -p worker (sdk-cli)', async ($, on) => {
|
|
187
|
+
const clock = mock.clock(on)
|
|
188
|
+
mock.env(on, { CLAUDE_CODE_ENTRYPOINT: 'sdk-cli', KENAZ_PEER_OPTIN: '1' })
|
|
189
|
+
const { calls } = setup(on, () => PLAIN)
|
|
190
|
+
await $.session.start(START)
|
|
191
|
+
await clock.advance(15_000)
|
|
192
|
+
expect(calls.length).toBe(0)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
test('TASK_2465: KENAZ_PEER_OPTIN=1 (kenaz launch) opts a plain session in', async ($, on) => {
|
|
196
|
+
const clock = mock.clock(on)
|
|
197
|
+
mock.env(on, { CLAUDE_CODE_ENTRYPOINT: 'cli', KENAZ_PEER_OPTIN: '1' })
|
|
198
|
+
const { calls } = setup(on, () => PLAIN)
|
|
199
|
+
await $.session.start(START)
|
|
200
|
+
await clock.advance(5_000)
|
|
201
|
+
await clock.advance(5_000)
|
|
202
|
+
expect(calls[0]).toContain('POST http://127.0.0.1:18080/api/peer/mod-heartbeat')
|
|
203
|
+
})
|
|
@@ -20,6 +20,8 @@ let cwd = ''
|
|
|
20
20
|
let beaten = false
|
|
21
21
|
let busy = false
|
|
22
22
|
let ansuz = false // sticky: once a session is Ansuz it never flips back
|
|
23
|
+
let entrypoint = '' // CLAUDE_CODE_ENTRYPOINT; the env opt-in counts only for the interactive 'cli'
|
|
24
|
+
let optinLive = false // TASK_2465: opt-in marker seen on the last check (expires, so not sticky)
|
|
23
25
|
// Same markers as .claude/hooks/_ansuz-conversation.js (turn-context's gate).
|
|
24
26
|
const MARKERS = [
|
|
25
27
|
'<command-name>/Ansuz</command-name>',
|
|
@@ -65,8 +67,28 @@ async function project($: any): Promise<string> {
|
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
// Only USER-role text counts (strings or text blocks); tool results do not.
|
|
70
|
+
// TASK_2465: opt-in marker written by .claude/hooks/peer-session-fill.js (kenaz_peer_send / ygg join|create) under
|
|
71
|
+
// <KENAZ_PEER_OPTIN_DIR | ~/.kenaz/peer-optin>/<session id>.json, or KENAZ_PEER_OPTIN=1 (kenaz launch). 24 h expiry.
|
|
72
|
+
const OPTIN_TTL_MS = 24 * 3600 * 1000
|
|
73
|
+
async function optedIn($: any): Promise<boolean> {
|
|
74
|
+
try {
|
|
75
|
+
if (entrypoint === 'cli' && (await $.env.get('KENAZ_PEER_OPTIN')) === '1') return true
|
|
76
|
+
if (!/^[A-Za-z0-9_-]{1,128}$/.test(sessionId)) return false
|
|
77
|
+
let dir = (await $.env.get('KENAZ_PEER_OPTIN_DIR')) ?? ''
|
|
78
|
+
if (!dir) {
|
|
79
|
+
const home = (await $.env.get('USERPROFILE')) ?? (await $.env.get('HOME')) ?? ''
|
|
80
|
+
if (!home) return false
|
|
81
|
+
dir = `${home}/.kenaz/peer-optin`
|
|
82
|
+
}
|
|
83
|
+
const f = await $.fs.read(`${dir}/${sessionId}.json`)
|
|
84
|
+
const age = Date.now() - Number(JSON.parse(typeof f === 'string' ? f : f.text).ts)
|
|
85
|
+
return age >= 0 && age < OPTIN_TTL_MS
|
|
86
|
+
} catch { return false }
|
|
87
|
+
}
|
|
88
|
+
|
|
68
89
|
async function isAnsuz($: any): Promise<boolean> {
|
|
69
90
|
if (ansuz) return true
|
|
91
|
+
if ((optinLive = await optedIn($))) return true
|
|
70
92
|
try {
|
|
71
93
|
const rows = await $.session.messages({ as: 'api' })
|
|
72
94
|
for (const m of rows) {
|
|
@@ -163,10 +185,11 @@ export const register: Register = on => {
|
|
|
163
185
|
ep = (await $.env.get('CLAUDE_CODE_ENTRYPOINT')) ?? ''
|
|
164
186
|
optIn = (await $.env.get('KENAZ_IN_SDK')) ?? ''
|
|
165
187
|
} catch { /* env unreadable: behave as an interactive session */ }
|
|
188
|
+
entrypoint = ep
|
|
166
189
|
if (ep.startsWith('sdk-') && ep !== 'sdk-cli' && optIn !== '1') return next(e)
|
|
167
190
|
sessionId = String(await $.session.id())
|
|
168
191
|
cwd = String(await $.session.cwd())
|
|
169
|
-
$.clock.every(HEARTBEAT_MS, () => { if (ansuz) void beat($) })
|
|
192
|
+
$.clock.every(HEARTBEAT_MS, () => { if (ansuz || optinLive) void beat($) })
|
|
170
193
|
$.clock.every(POLL_MS, () => { void cycle($) })
|
|
171
194
|
return next(e)
|
|
172
195
|
})
|
package/lib/cli.js
CHANGED
|
@@ -283,6 +283,10 @@ async function doctor(p, flags, out, deps = {}) {
|
|
|
283
283
|
const cx = codexModule(p), url = cx.getRegisteredMcpUrl(), miss = cx.missingHookEvents();
|
|
284
284
|
add('codex mcp', url ? 'ok' : 'FAIL', url || 'kenaz-dashboard entry missing in config.toml');
|
|
285
285
|
add('codex hooks', miss.length ? 'FAIL' : 'ok', miss.length ? 'missing: ' + miss.join(', ') : 'registered (trust once via /hooks in codex)');
|
|
286
|
+
// TASK_2460: entries in hooks.json / config.toml that run a script which no longer exists fail on every prompt
|
|
287
|
+
let stale = null;
|
|
288
|
+
try { const rep = require(path.join(p.core, '.formation', 'bifrost', 'codex-hooks-repair.js')); stale = rep.describe(rep.scan(cx.getCodexHome())); } catch (_) { /* older core without the module */ }
|
|
289
|
+
add('codex hook files', stale ? 'FAIL' : 'ok', stale || 'every Kenaz hook entry points at an existing file');
|
|
286
290
|
}
|
|
287
291
|
const dm = dispatchModel(p);
|
|
288
292
|
if (dm) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kenaz",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.22",
|
|
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.22",
|
|
24
|
+
"@kenazai/server-darwin-x64": "0.3.22",
|
|
25
|
+
"@kenazai/server-linux-arm64": "0.3.22",
|
|
26
|
+
"@kenazai/server-linux-x64": "0.3.22",
|
|
27
|
+
"@kenazai/server-win32-x64": "0.3.22"
|
|
28
28
|
},
|
|
29
29
|
"license": "UNLICENSED",
|
|
30
30
|
"repository": {
|