kenaz 0.3.17 → 0.3.18
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/hooks/synapse-first-investigation.js +15 -0
- package/core/.formation/bifrost/exec.js +29 -4
- package/core/.formation/bifrost/provider-launch.js +18 -11
- package/core/.formation/bifrost/provider-launch.test.js +26 -2
- package/core/.formation/tools/__tests__/dispatch-cwd-project.test.js +42 -0
- package/core/.formation/tools/__tests__/subagent-executor.test.js +3 -2
- package/core/.formation/tools/dispatch.js +14 -1
- package/core/KENAZ_CORE_VERSION +1 -1
- package/core/dev/scripts/run-rust-tests.js +21 -1
- package/core/dev/scripts/run-rust-tests.test.js +17 -0
- package/core/plugins/kenaz/scripts/ygg.js +6 -0
- package/package.json +6 -6
|
@@ -201,9 +201,24 @@ function isDeduped(normalized) {
|
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
// Every Bash call appends a line; uncapped it reached 11.7 MB (2026-10-12). Past MAX_LOG_BYTES keep the newest
|
|
205
|
+
// KEEP_LOG_BYTES (cut at a line boundary), so the readers still see recent history.
|
|
206
|
+
const MAX_LOG_BYTES = 2 * 1024 * 1024;
|
|
207
|
+
const KEEP_LOG_BYTES = 1024 * 1024;
|
|
208
|
+
function capLog(file) {
|
|
209
|
+
let size = 0;
|
|
210
|
+
try { size = fs.statSync(file).size; } catch { return; }
|
|
211
|
+
if (size <= MAX_LOG_BYTES) return;
|
|
212
|
+
const buf = fs.readFileSync(file);
|
|
213
|
+
const tail = buf.subarray(buf.length - KEEP_LOG_BYTES);
|
|
214
|
+
const nl = tail.indexOf(10);
|
|
215
|
+
fs.writeFileSync(file, nl >= 0 ? tail.subarray(nl + 1) : tail);
|
|
216
|
+
}
|
|
217
|
+
|
|
204
218
|
function appendTelemetry(record) {
|
|
205
219
|
try {
|
|
206
220
|
fs.mkdirSync(path.dirname(NUDGES_PATH), { recursive: true });
|
|
221
|
+
capLog(NUDGES_PATH);
|
|
207
222
|
fs.appendFileSync(NUDGES_PATH, JSON.stringify(record) + '\n', 'utf8');
|
|
208
223
|
} catch {
|
|
209
224
|
// best-effort
|
|
@@ -19,7 +19,7 @@ const { spawn, spawnSync } = require('child_process');
|
|
|
19
19
|
|
|
20
20
|
// First = default. From GET https://api.deepseek.com/models on 2026-10-11 (V4 ids, 1M context); the V3 ids still
|
|
21
21
|
// answer but are no longer listed. `kenaz launch` re-reads the context window from that endpoint at start.
|
|
22
|
-
const DEEPSEEK_MODELS = ['deepseek-
|
|
22
|
+
const DEEPSEEK_MODELS = ['deepseek-flash', 'deepseek-v4-pro', 'deepseek-chat', 'deepseek-reasoner']; // flash = default (user decision 2026-10-12)
|
|
23
23
|
const DEEPSEEK_BASE_URL = 'https://api.deepseek.com/anthropic';
|
|
24
24
|
const DEEPSEEK_CONTEXT_TOKENS = { 'deepseek-v4-pro': 1048576, 'deepseek-flash': 1048576, 'deepseek-chat': 128000, 'deepseek-reasoner': 128000 };
|
|
25
25
|
const DEEPSEEK_FAST = 'deepseek-flash'; // Claude Code's small/fast slot (titles, summaries): the cheap model
|
|
@@ -38,12 +38,37 @@ function parseExecutorModel(raw) {
|
|
|
38
38
|
return { adapter, id };
|
|
39
39
|
}
|
|
40
40
|
if (adapter === 'deepseek') {
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// Any id the vendor may add later is accepted (no release needed); `deepseek:` alone = the user's saved default.
|
|
42
|
+
const model = id || providerDefault('deepseek') || DEEPSEEK_MODELS[0];
|
|
43
|
+
if (!MODEL_ID.test(model)) throw new Error(`invalid deepseek model id '${id}' (letters, digits, . _ - only); known: ${DEEPSEEK_MODELS.map((i) => 'deepseek:' + i).join(', ')}`);
|
|
44
|
+
return { adapter, id: model };
|
|
43
45
|
}
|
|
44
46
|
throw new Error(`unknown executor prefix '${m[1]}:'; valid prefixes: codex:<id>, deepseek:<model>`);
|
|
45
47
|
}
|
|
46
48
|
|
|
49
|
+
const MODEL_ID = /^[A-Za-z0-9][\w.-]{0,63}$/;
|
|
50
|
+
const configFile = (home) => path.join(home, '.kenaz', 'config.json');
|
|
51
|
+
|
|
52
|
+
/** The user's own default model for a provider: KENAZ_<PROVIDER>_MODEL, else ~/.kenaz/config.json
|
|
53
|
+
* providers.<provider>.model (written by `kenaz launch --provider p --model m --save-default`); null when unset. */
|
|
54
|
+
function providerDefault(provider, env = process.env, home = os.homedir()) {
|
|
55
|
+
const e = env['KENAZ_' + provider.toUpperCase() + '_MODEL'];
|
|
56
|
+
if (e && e.trim()) return e.trim();
|
|
57
|
+
try { const m = JSON.parse(fs.readFileSync(configFile(home), 'utf8')).providers[provider].model; return typeof m === 'string' && m.trim() ? m.trim() : null; } catch { return null; }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Save the default (other keys in config.json are kept; atomic write). */
|
|
61
|
+
function saveProviderDefault(provider, model, home = os.homedir()) {
|
|
62
|
+
let cfg = {};
|
|
63
|
+
try { cfg = JSON.parse(fs.readFileSync(configFile(home), 'utf8')) || {}; } catch { /* new file */ }
|
|
64
|
+
cfg.providers = { ...(cfg.providers || {}), [provider]: { ...((cfg.providers || {})[provider] || {}), model } };
|
|
65
|
+
fs.mkdirSync(path.dirname(configFile(home)), { recursive: true });
|
|
66
|
+
const tmp = configFile(home) + '.tmp';
|
|
67
|
+
fs.writeFileSync(tmp, JSON.stringify(cfg, null, 2) + '\n');
|
|
68
|
+
fs.renameSync(tmp, configFile(home));
|
|
69
|
+
return configFile(home);
|
|
70
|
+
}
|
|
71
|
+
|
|
47
72
|
/** DEEPSEEK_API_KEY, else deepseek_api_key in ~/.kenaz/credentials.yaml; null when neither. */
|
|
48
73
|
function deepseekKey(env = process.env, home = os.homedir()) {
|
|
49
74
|
if (env.DEEPSEEK_API_KEY && env.DEEPSEEK_API_KEY.trim()) return env.DEEPSEEK_API_KEY.trim();
|
|
@@ -175,4 +200,4 @@ async function main(argv) {
|
|
|
175
200
|
}
|
|
176
201
|
|
|
177
202
|
if (require.main === module) main(process.argv.slice(2)).then((c) => { process.exitCode = c; });
|
|
178
|
-
module.exports = { otherVendorSession, parseExecutorModel, deepseekKey, deepseekEnv, deepseekModels, DEEPSEEK_UNSET, winQuote, buildChild, run, main, DEEPSEEK_MODELS };
|
|
203
|
+
module.exports = { otherVendorSession, providerDefault, saveProviderDefault, MODEL_ID, parseExecutorModel, deepseekKey, deepseekEnv, deepseekModels, DEEPSEEK_UNSET, winQuote, buildChild, run, main, DEEPSEEK_MODELS };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
/**
|
|
4
|
-
* `kenaz launch --provider deepseek [--model deepseek-v4-pro
|
|
4
|
+
* `kenaz launch --provider deepseek [--model deepseek-flash (default)|deepseek-v4-pro|...] [claude args...]`
|
|
5
5
|
* Starts Claude Code with every model request sent to another vendor's Anthropic-compatible endpoint, so
|
|
6
6
|
* Ansuz (/kenaz:Ansuz), its subagents and background requests run on that vendor and use no Claude quota.
|
|
7
7
|
* The key is read from the env or ~/.kenaz/credentials.yaml and set only in the claude child's env: never in
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
const os = require('os');
|
|
11
11
|
const { spawn } = require('child_process');
|
|
12
|
-
const { deepseekKey, deepseekEnv, deepseekModels, DEEPSEEK_UNSET, DEEPSEEK_MODELS, winQuote } = require('./exec');
|
|
12
|
+
const { deepseekKey, deepseekEnv, deepseekModels, DEEPSEEK_UNSET, DEEPSEEK_MODELS, winQuote, providerDefault, saveProviderDefault, MODEL_ID } = require('./exec');
|
|
13
13
|
|
|
14
14
|
const PROVIDERS = {
|
|
15
15
|
deepseek: {
|
|
@@ -17,29 +17,31 @@ const PROVIDERS = {
|
|
|
17
17
|
keyHelp: 'set DEEPSEEK_API_KEY, or add `deepseek_api_key: <key>` to ~/.kenaz/credentials.yaml',
|
|
18
18
|
},
|
|
19
19
|
};
|
|
20
|
-
const USAGE = 'usage: kenaz launch --provider <' + Object.keys(PROVIDERS).join('|') + '> [--model <id>] [claude args...]';
|
|
20
|
+
const USAGE = 'usage: kenaz launch --provider <' + Object.keys(PROVIDERS).join('|') + '> [--model <id> [--save-default]] [claude args...]';
|
|
21
21
|
|
|
22
22
|
function parse(argv) {
|
|
23
|
-
const rest = []; let provider = null; let model = null;
|
|
23
|
+
const rest = []; let provider = null; let model = null; let save = false;
|
|
24
24
|
for (let i = 0; i < argv.length; i++) {
|
|
25
|
+
if (argv[i] === '--save-default') { save = true; continue; }
|
|
25
26
|
const m = /^--(provider|model)(?:=(.*))?$/.exec(argv[i]);
|
|
26
27
|
if (!m) { rest.push(argv[i]); continue; }
|
|
27
28
|
const v = m[2] !== undefined ? m[2] : argv[++i];
|
|
28
29
|
if (m[1] === 'provider') provider = v; else model = v;
|
|
29
30
|
}
|
|
30
|
-
return { rest, provider, model };
|
|
31
|
+
return { rest, provider, model, save };
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
/** Child process plan: { bin, args, env }.
|
|
34
|
-
*
|
|
35
|
-
*
|
|
34
|
+
/** Child process plan: { bin, args, env }. Model = --model, else the user's own default (KENAZ_<P>_MODEL or
|
|
35
|
+
* ~/.kenaz/config.json, see providerDefault), else the built-in default. live = {id: context_window} from the
|
|
36
|
+
* vendor's model list (or null): when known it is the only validation (new vendor models work without a Kenaz
|
|
37
|
+
* release); when unreachable any well-formed id is accepted. Throws with a user-facing message. */
|
|
36
38
|
function plan(argv, env = process.env, home = os.homedir(), live = null) {
|
|
37
39
|
const { rest, provider, model } = parse(argv);
|
|
38
40
|
const p = PROVIDERS[provider];
|
|
39
41
|
if (!p) throw new Error((provider ? `unknown provider '${provider}'. ` : '') + USAGE);
|
|
40
|
-
const id = model || p.models[0];
|
|
41
|
-
|
|
42
|
-
if (!
|
|
42
|
+
const id = model || providerDefault(provider, env, home) || p.models[0];
|
|
43
|
+
if (!MODEL_ID.test(id)) throw new Error(`invalid ${provider} model id '${id}'`);
|
|
44
|
+
if (live && !(id in live)) throw new Error(`unknown ${provider} model '${id}'; the vendor currently lists: ${Object.keys(live).join(', ')}`);
|
|
43
45
|
const key = p.key(env, home);
|
|
44
46
|
if (!key) throw new Error(`${provider}: no API key (${p.keyHelp})`);
|
|
45
47
|
const childEnv = { ...env, ...p.env(id, key, live && live[id]), CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
|
|
@@ -56,6 +58,11 @@ async function main(argv, out = console.log, deps = {}) {
|
|
|
56
58
|
const live = key ? await (deps.live || p.live)(key) : null;
|
|
57
59
|
let pl;
|
|
58
60
|
try { pl = plan(argv, env, home, live); } catch (e) { out(e.message); return 2; }
|
|
61
|
+
const { model: asked, save } = parse(argv);
|
|
62
|
+
if (save) {
|
|
63
|
+
if (!asked) { out('--save-default needs --model <id>'); return 2; }
|
|
64
|
+
out(`kenaz launch: ${pl.provider} default model saved: ${pl.model} (${saveProviderDefault(pl.provider, pl.model, home)})`);
|
|
65
|
+
}
|
|
59
66
|
out(`kenaz launch: Claude Code on ${pl.provider} (${pl.model}, context ${pl.context} tokens); no Claude quota is used in this session`);
|
|
60
67
|
const win = (deps.platform || process.platform) === 'win32'; // claude is a .cmd shim on npm installs
|
|
61
68
|
const child = (deps.spawn || spawn)(win ? [pl.bin, ...pl.args].map(winQuote).join(' ') : pl.bin, win ? [] : pl.args,
|
|
@@ -25,7 +25,7 @@ test('deepseek: every model slot points at DeepSeek, key only in the child env,
|
|
|
25
25
|
assert.strictEqual(e.ANTHROPIC_BASE_URL, 'https://api.deepseek.com/anthropic');
|
|
26
26
|
assert.strictEqual(e.ANTHROPIC_AUTH_TOKEN, KEY);
|
|
27
27
|
for (const k of ['ANTHROPIC_MODEL', 'ANTHROPIC_DEFAULT_SONNET_MODEL', 'ANTHROPIC_DEFAULT_OPUS_MODEL', 'CLAUDE_CODE_SUBAGENT_MODEL']) {
|
|
28
|
-
assert.strictEqual(e[k], 'deepseek-
|
|
28
|
+
assert.strictEqual(e[k], 'deepseek-flash', k);
|
|
29
29
|
}
|
|
30
30
|
assert.strictEqual(e.ANTHROPIC_SMALL_FAST_MODEL, 'deepseek-flash'); assert.strictEqual(e.ANTHROPIC_DEFAULT_HAIKU_MODEL, 'deepseek-flash');
|
|
31
31
|
assert.strictEqual(e.CLAUDE_CODE_MAX_CONTEXT_TOKENS, '1048576', 'V4 window from the built-in table when the live list is unavailable');
|
|
@@ -54,7 +54,7 @@ test('control: no key, unknown provider or model -> exit 2 and claude is never s
|
|
|
54
54
|
[['--provider', 'deepseek'], {}, /no API key/],
|
|
55
55
|
[['--provider', 'nope'], { DEEPSEEK_API_KEY: KEY }, /unknown provider 'nope'/],
|
|
56
56
|
[[], { DEEPSEEK_API_KEY: KEY }, /usage: kenaz launch --provider <deepseek>/],
|
|
57
|
-
[['--provider', 'deepseek', '--model', '
|
|
57
|
+
[['--provider', 'deepseek', '--model', 'bad id'], { DEEPSEEK_API_KEY: KEY }, /invalid deepseek model id/],
|
|
58
58
|
]) {
|
|
59
59
|
const calls = [], lines = [];
|
|
60
60
|
const rc = await main(argv, (l) => lines.push(l), { spawn: fakeSpawn(calls), live: async () => null, env, home: home(), platform: 'linux' });
|
|
@@ -74,3 +74,27 @@ test('live model list: a newer listed id is accepted and its context window driv
|
|
|
74
74
|
assert.strictEqual(calls[0].opts.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS, '2097152');
|
|
75
75
|
assert.match(lines[0], /context 2097152 tokens/);
|
|
76
76
|
});
|
|
77
|
+
|
|
78
|
+
test('model not in the vendor list is refused only when the list is known; unknown list = accepted (no release needed)', async () => {
|
|
79
|
+
const calls = [], lines = [];
|
|
80
|
+
const rc = await main(['--provider', 'deepseek', '--model', 'gpt-x'], (l) => lines.push(l), { spawn: fakeSpawn(calls), live: async () => ({ 'deepseek-flash': 1048576 }), env: { DEEPSEEK_API_KEY: KEY }, home: home(), platform: 'linux' });
|
|
81
|
+
assert.strictEqual(rc, 2); assert.strictEqual(calls.length, 0); assert.match(lines.join(String.fromCharCode(10)), /the vendor currently lists: deepseek-flash/);
|
|
82
|
+
const calls2 = [];
|
|
83
|
+
assert.strictEqual(await main(['--provider', 'deepseek', '--model', 'deepseek-v9'], () => {}, { spawn: fakeSpawn(calls2), live: async () => null, env: { DEEPSEEK_API_KEY: KEY }, home: home(), platform: 'linux' }), 0);
|
|
84
|
+
assert.strictEqual(calls2[0].opts.env.ANTHROPIC_MODEL, 'deepseek-v9');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('the user picks the default: --save-default writes ~/.kenaz/config.json (other keys kept), env overrides it, --model overrides both', async () => {
|
|
88
|
+
const h = home(); fs.mkdirSync(path.join(h, '.kenaz'));
|
|
89
|
+
fs.writeFileSync(path.join(h, '.kenaz', 'config.json'), JSON.stringify({ setupCompleted: true }));
|
|
90
|
+
const run = async (argv, env = {}) => { const calls = [], lines = []; const rc = await main(argv, (l) => lines.push(l), { spawn: fakeSpawn(calls), live: async () => null, env: { DEEPSEEK_API_KEY: KEY, ...env }, home: h, platform: 'linux' }); return { rc, model: calls[0] && calls[0].opts.env.ANTHROPIC_MODEL, lines }; };
|
|
91
|
+
assert.strictEqual((await run(['--provider', 'deepseek'])).model, 'deepseek-flash', 'built-in default');
|
|
92
|
+
const saved = await run(['--provider', 'deepseek', '--model', 'deepseek-v4-pro', '--save-default']);
|
|
93
|
+
assert.strictEqual(saved.model, 'deepseek-v4-pro'); assert.match(saved.lines.join(String.fromCharCode(10)), /default model saved: deepseek-v4-pro/);
|
|
94
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(h, '.kenaz', 'config.json'), 'utf8'));
|
|
95
|
+
assert.strictEqual(cfg.setupCompleted, true); assert.strictEqual(cfg.providers.deepseek.model, 'deepseek-v4-pro');
|
|
96
|
+
assert.strictEqual((await run(['--provider', 'deepseek'])).model, 'deepseek-v4-pro', 'saved default used');
|
|
97
|
+
assert.strictEqual((await run(['--provider', 'deepseek'], { KENAZ_DEEPSEEK_MODEL: 'deepseek-chat' })).model, 'deepseek-chat', 'env overrides saved');
|
|
98
|
+
assert.strictEqual((await run(['--provider', 'deepseek', '--model', 'deepseek-flash'])).model, 'deepseek-flash', '--model overrides');
|
|
99
|
+
assert.strictEqual((await run(['--provider', 'deepseek', '--save-default'])).rc, 2, '--save-default without --model');
|
|
100
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// dispatch.js without --project-path, run from a user's project through the plugin (cwd = that project, ROOT = core):
|
|
2
|
+
// the card and the worktree belong to the cwd project, not the core (repro 2026-10-12: '任務不存在' and a worktree
|
|
3
|
+
// created inside the core repo, leaving Ansuz waiting on a subagent that had no card).
|
|
4
|
+
const test = require('node:test');
|
|
5
|
+
const assert = require('node:assert');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { spawnSync } = require('child_process');
|
|
10
|
+
|
|
11
|
+
const ROOT = path.resolve(__dirname, '..', '..', '..');
|
|
12
|
+
const DISPATCH = path.join(ROOT, '.formation', 'tools', 'dispatch.js');
|
|
13
|
+
const git = (cwd, args) => spawnSync('git', ['-c', 'user.name=t', '-c', 'user.email=t@t', ...args], { cwd, encoding: 'utf8' });
|
|
14
|
+
|
|
15
|
+
function project() {
|
|
16
|
+
const p = fs.mkdtempSync(path.join(os.tmpdir(), 'dcp-'));
|
|
17
|
+
git(p, ['init', '-q']); fs.writeFileSync(path.join(p, 'README.md'), 'x'); git(p, ['add', '-A']); git(p, ['commit', '-q', '-m', 'init']);
|
|
18
|
+
for (const d of ['backlog', 'active', 'done', 'blocked', 'cancel']) fs.mkdirSync(path.join(p, '.kenaz', 'tasks', d), { recursive: true });
|
|
19
|
+
fs.writeFileSync(path.join(p, '.kenaz', 'project.yaml'), 'name: dcp\n');
|
|
20
|
+
fs.writeFileSync(path.join(p, '.kenaz', 'tasks', 'backlog', 'TASK_7201_x.yaml'),
|
|
21
|
+
'id: TASK_7201\ntitle: "Unique card title 7201"\nstatus: backlog\npriority: low\nrigor: low\nrequirements: ["r"]\nacceptance_criteria: ["a"]\nscope_paths: [x.txt]\n');
|
|
22
|
+
return p;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const env = () => { const e = { ...process.env }; for (const k of ['CLAUDE_PROJECT_DIR', 'KENAZ_PROVIDER', 'ANTHROPIC_BASE_URL', 'CODEX_SESSION_ID', 'CODEX_THREAD_ID']) delete e[k]; return e; };
|
|
26
|
+
|
|
27
|
+
test('from a user project without --project-path: the card is found and isolation targets that project', () => {
|
|
28
|
+
const p = project();
|
|
29
|
+
const r = spawnSync(process.execPath, [DISPATCH, '--skill=do', '--task=TASK_7201', '--dispatch-mode=subagent', '--dry-run'], { cwd: p, env: env(), encoding: 'utf8' });
|
|
30
|
+
assert.strictEqual(r.status, 0, r.stderr);
|
|
31
|
+
assert.doesNotMatch(r.stderr, /任務不存在/);
|
|
32
|
+
const j = JSON.parse(r.stdout);
|
|
33
|
+
assert.match(j.promptText, /Unique card title 7201/);
|
|
34
|
+
assert.match(r.stderr, new RegExp('cross-project detected .*' + path.basename(p)));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('control: in the core repo itself nothing changes (same-project Agent worktree)', () => {
|
|
38
|
+
const r = spawnSync(process.execPath, [DISPATCH, '--skill=do', '--task=TASK_2376', '--dispatch-mode=subagent', '--dry-run'], { cwd: ROOT, env: env(), encoding: 'utf8' });
|
|
39
|
+
assert.strictEqual(r.status, 0, r.stderr);
|
|
40
|
+
const j = JSON.parse(r.stdout);
|
|
41
|
+
assert.strictEqual(j.isolation.mode, 'agent_tool_worktree');
|
|
42
|
+
});
|
|
@@ -139,8 +139,9 @@ test('subagent set: unknown codex id and unknown prefix are refused with the val
|
|
|
139
139
|
const p = tmp(t, 'dm-');
|
|
140
140
|
const a = dmRun(['set', 'codex:nope'], p);
|
|
141
141
|
assert.equal(a.code, 1); assert.match(a.out, /unknown codex model 'nope'.*codex:gpt-5\.6-sol/);
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
// any well-formed deepseek id is accepted (a new vendor model needs no Kenaz release); a malformed one is refused
|
|
143
|
+
const b = dmRun(['set', 'deepseek:bad id'], p);
|
|
144
|
+
assert.equal(b.code, 1); assert.match(b.out, /invalid deepseek model id/);
|
|
144
145
|
const c = dmRun(['set', 'gemini:pro'], p);
|
|
145
146
|
assert.equal(c.code, 1); assert.match(c.out, /unknown executor prefix 'gemini:'.*codex:<id>, deepseek:<model>/);
|
|
146
147
|
assert.equal(fs.existsSync(path.join(p, '.kenaz', 'dashboard-models.json')), false);
|
|
@@ -487,6 +487,15 @@ function samePath(a, b) {
|
|
|
487
487
|
* @param {boolean|null} [opts.useWorktreeOverride] - dispatch-time toggle (CLI --use-worktree)
|
|
488
488
|
* @returns {{ isCrossProject: boolean, mode: string, useWorktree: boolean, source: string, verifyOffline: string|null }}
|
|
489
489
|
*/
|
|
490
|
+
/** The Kenaz project around the cwd (CLAUDE_PROJECT_DIR first), or null when it is the core itself / none. */
|
|
491
|
+
function cwdProject(start = process.env.CLAUDE_PROJECT_DIR || process.cwd()) {
|
|
492
|
+
let root = null;
|
|
493
|
+
try { root = require('../../.claude/hooks/_project-guard').findProjectRoot(start); } catch { return null; }
|
|
494
|
+
if (!root) return null;
|
|
495
|
+
const norm = (p) => path.resolve(p).replace(/\\/g, '/').toLowerCase();
|
|
496
|
+
return norm(root) === norm(ROOT) ? null : root;
|
|
497
|
+
}
|
|
498
|
+
|
|
490
499
|
function resolveDispatchIsolation({ projectPath = null, useWorktreeOverride = null } = {}) {
|
|
491
500
|
// TASK_1710 semantics: samePath, not raw string compare (separator/case).
|
|
492
501
|
const isCrossProject = !!(projectPath && !samePath(projectPath, ROOT));
|
|
@@ -829,7 +838,11 @@ Sub-agent dispatch (Ansuz billing-aware path):
|
|
|
829
838
|
const formation = getArg('--formation') || 'unit';
|
|
830
839
|
const skill = getArg('--skill');
|
|
831
840
|
const task = getArg('--task');
|
|
832
|
-
|
|
841
|
+
// No --project-path: the Kenaz project around the cwd, when that is not the core itself. Plugin users run
|
|
842
|
+
// dispatch.js from their own project through core-resolve, so ROOT is the core: before this, the card was looked
|
|
843
|
+
// up in the core ("任務不存在") and the worktree was created in the core repo, leaving Ansuz waiting on a
|
|
844
|
+
// subagent with no card in the wrong repo (repro 2026-10-12).
|
|
845
|
+
const projectPath = getArg('--project-path') || cwdProject();
|
|
833
846
|
const dryRun = args.includes('--dry-run');
|
|
834
847
|
const dispatchModeCli = getArg('--dispatch-mode');
|
|
835
848
|
// TASK_1998 R3: subagent-path model override — wins over dashboard
|
package/core/KENAZ_CORE_VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.3.
|
|
1
|
+
0.3.18
|
|
@@ -115,6 +115,24 @@ function renderCompilerErrors(stdoutText, maxErrors = 20) {
|
|
|
115
115
|
return out;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
// The shared verify cache only grows (incremental builds are never pruned): it reached 551 GB by 2026-10-12.
|
|
119
|
+
// Every KENAZ_VERIFY_CLEAN_DAYS (default 7) days run `cargo clean` on it first. A stamp file, not a size walk:
|
|
120
|
+
// walking hundreds of GB takes minutes. cargo clean takes cargo's own lock, so a running build is waited for.
|
|
121
|
+
const CLEAN_STAMP = '.kenaz-last-clean';
|
|
122
|
+
function periodicClean(targetDir, env, { now = Date.now(), run = spawnSync, log = (m) => process.stderr.write(m) } = {}) {
|
|
123
|
+
const days = Number(env.KENAZ_VERIFY_CLEAN_DAYS || 7);
|
|
124
|
+
const stamp = path.join(targetDir, CLEAN_STAMP);
|
|
125
|
+
let last = 0;
|
|
126
|
+
try { last = fs.statSync(stamp).mtimeMs; } catch (_) { /* never stamped */ }
|
|
127
|
+
const touch = () => { try { fs.mkdirSync(targetDir, { recursive: true }); fs.writeFileSync(stamp, new Date(now).toISOString() + '\n'); } catch (_) { /* best effort */ } };
|
|
128
|
+
if (!last) { touch(); return 'stamped'; } // first run after this change: start the clock, do not wipe
|
|
129
|
+
if (!(days > 0) || now - last < days * 86400000) return 'fresh';
|
|
130
|
+
log(`[run-rust-tests] verify cache last cleaned ${Math.round((now - last) / 86400000)} days ago: cargo clean (one full rebuild follows)\n`);
|
|
131
|
+
const r = run('cargo', ['clean'], { cwd: crateDir, env, encoding: 'utf8', shell: false });
|
|
132
|
+
touch();
|
|
133
|
+
return r && r.status === 0 ? 'cleaned' : 'clean-failed';
|
|
134
|
+
}
|
|
135
|
+
|
|
118
136
|
function main() {
|
|
119
137
|
const exeArgs = buildExeArgs(process.argv.slice(2));
|
|
120
138
|
|
|
@@ -131,6 +149,8 @@ function main() {
|
|
|
131
149
|
);
|
|
132
150
|
}
|
|
133
151
|
|
|
152
|
+
if (env.CARGO_TARGET_DIR === verifyTargetDir()) periodicClean(env.CARGO_TARGET_DIR, env);
|
|
153
|
+
|
|
134
154
|
// 1. Build the lib test binary without running it.
|
|
135
155
|
const build = spawnSync(
|
|
136
156
|
'cargo',
|
|
@@ -204,4 +224,4 @@ if (require.main === module) {
|
|
|
204
224
|
main();
|
|
205
225
|
}
|
|
206
226
|
|
|
207
|
-
module.exports = { parseArgs, buildExeArgs, renderCompilerErrors };
|
|
227
|
+
module.exports = { parseArgs, buildExeArgs, renderCompilerErrors, periodicClean, CLEAN_STAMP };
|
|
@@ -152,3 +152,20 @@ test('renderCompilerErrors: cap applied -> first N kept + omitted-count note', (
|
|
|
152
152
|
'error[E0001]: synthetic error 1\n... 2 more errors omitted\n'
|
|
153
153
|
);
|
|
154
154
|
});
|
|
155
|
+
|
|
156
|
+
test('periodicClean: first run only stamps; fresh = nothing; older than N days = cargo clean then restamp', () => {
|
|
157
|
+
const fs = require('node:fs'), os = require('node:os'), path = require('node:path');
|
|
158
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rrt-clean-'));
|
|
159
|
+
const calls = [];
|
|
160
|
+
const run = (cmd, args) => { calls.push([cmd, ...args].join(' ')); return { status: 0 }; };
|
|
161
|
+
const t0 = Date.parse('2026-10-01T00:00:00Z');
|
|
162
|
+
assert.equal(mod.periodicClean(dir, {}, { now: t0, run, log: () => {} }), 'stamped');
|
|
163
|
+
assert.deepEqual(calls, [], 'never wipes on the first run');
|
|
164
|
+
const stamp = path.join(dir, mod.CLEAN_STAMP);
|
|
165
|
+
fs.utimesSync(stamp, t0 / 1000, t0 / 1000);
|
|
166
|
+
assert.equal(mod.periodicClean(dir, {}, { now: t0 + 6 * 86400000, run, log: () => {} }), 'fresh');
|
|
167
|
+
assert.deepEqual(calls, []);
|
|
168
|
+
assert.equal(mod.periodicClean(dir, {}, { now: t0 + 8 * 86400000, run, log: () => {} }), 'cleaned');
|
|
169
|
+
assert.deepEqual(calls, ['cargo clean']);
|
|
170
|
+
assert.equal(mod.periodicClean(dir, { KENAZ_VERIFY_CLEAN_DAYS: '30' }, { now: t0 + 10 * 86400000, run, log: () => {} }), 'fresh', 'restamped, and the window is configurable');
|
|
171
|
+
});
|
|
@@ -76,6 +76,10 @@ function buildStatus(cfg, relay, peer) {
|
|
|
76
76
|
waiting_mail: peer && typeof peer.waiting_count === 'number' ? peer.waiting_count : null,
|
|
77
77
|
undeliverable_mail: peer && typeof peer.undeliverable_count === 'number' ? peer.undeliverable_count : null,
|
|
78
78
|
// TASK_2380: real delivery mode per live session (push | pull-on-prompt | none), never a bare 'enabled'.
|
|
79
|
+
// TASK_2436: when the poller last reached the relay, and the relay's same-name warning.
|
|
80
|
+
last_poll_at: peer && peer.last_poll_at ? peer.last_poll_at : null,
|
|
81
|
+
last_poll_ok: peer && typeof peer.last_poll_ok === 'boolean' ? peer.last_poll_ok : null,
|
|
82
|
+
same_name_warning: peer && peer.same_name_warning ? peer.same_name_warning : '',
|
|
79
83
|
delivery_mode: peer && peer.delivery_mode ? peer.delivery_mode : 'unknown',
|
|
80
84
|
delivery_note: peer && peer.delivery_note ? peer.delivery_note : '',
|
|
81
85
|
sessions: peer && Array.isArray(peer.sessions) ? peer.sessions : [],
|
|
@@ -95,6 +99,7 @@ function renderStatus(s) {
|
|
|
95
99
|
'Pending mail: ' + (s.pending_mail == null ? 'unknown' : s.pending_mail),
|
|
96
100
|
'Waiting for a session: ' + (s.waiting_mail == null ? 'unknown' : s.waiting_mail) + ' (stored on this node, readable)',
|
|
97
101
|
'Undeliverable: ' + (s.undeliverable_mail == null ? 'unknown' : s.undeliverable_mail),
|
|
102
|
+
'Last poll: ' + (s.last_poll_at ? s.last_poll_at + (s.last_poll_ok === false ? ' (FAILED)' : ' (ok)') : 'none yet'),
|
|
98
103
|
'Delivery: ' + s.delivery_mode + (s.sessions.length ? '' : ' (no live sessions)'),
|
|
99
104
|
];
|
|
100
105
|
for (const x of s.sessions) L.push(' - ' + x.agent + ' ' + String(x.session_id).slice(0, 8) + ': ' + x.delivery_mode);
|
|
@@ -102,6 +107,7 @@ function renderStatus(s) {
|
|
|
102
107
|
L.push('', 'New team key (shown once, save it): ' + s.created.key + (s.created.team_name ? ' [' + s.created.team_name + ']' : ''));
|
|
103
108
|
L.push('share with teammates: kenaz ygg join ' + s.created.key);
|
|
104
109
|
}
|
|
110
|
+
if (s.same_name_warning) L.push('WARNING: ' + s.same_name_warning);
|
|
105
111
|
if (s.room_warning) L.push('WARNING: ' + s.room_warning);
|
|
106
112
|
if (s.delivery_note) L.push('Note: ' + s.delivery_note);
|
|
107
113
|
return L.join('\n');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kenaz",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.18",
|
|
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.18",
|
|
24
|
+
"@kenazai/server-darwin-x64": "0.3.18",
|
|
25
|
+
"@kenazai/server-linux-arm64": "0.3.18",
|
|
26
|
+
"@kenazai/server-linux-x64": "0.3.18",
|
|
27
|
+
"@kenazai/server-win32-x64": "0.3.18"
|
|
28
28
|
},
|
|
29
29
|
"license": "UNLICENSED",
|
|
30
30
|
"repository": {
|