kenaz 0.3.12 → 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.
Files changed (34) hide show
  1. package/core/.claude/commands/Ansuz.md +11 -1
  2. package/core/.claude/hooks/__tests__/correction-detect.test.js +74 -0
  3. package/core/.claude/hooks/_run-user-prompt.js +2 -0
  4. package/core/.claude/hooks/_run-user-prompt.test.js +2 -2
  5. package/core/.claude/hooks/correction-detect.js +99 -0
  6. package/core/.formation/bifrost/exec.js +147 -0
  7. package/core/.formation/bifrost/provider-launch.js +57 -0
  8. package/core/.formation/bifrost/provider-launch.test.js +62 -0
  9. package/core/.formation/tools/__tests__/dispatch-model.test.js +1 -1
  10. package/core/.formation/tools/__tests__/subagent-executor.test.js +198 -0
  11. package/core/.formation/tools/dispatch-model.js +15 -10
  12. package/core/.formation/tools/dispatch.js +46 -10
  13. package/core/.formation/tools/kenaz-stats.js +57 -16
  14. package/core/.formation/tools/kenaz-stats.test.js +2 -2
  15. package/core/.specialist/Ansuz/RULES.md +7 -2
  16. package/core/.specialist/tools/__tests__/handoff-doc.test.js +46 -0
  17. package/core/.specialist/tools/__tests__/task-2425-lessons-lifecycle.test.js +142 -0
  18. package/core/.specialist/tools/__tests__/task-2427-harvest.test.js +150 -0
  19. package/core/.specialist/tools/ansuz-boot-fast.js +37 -1
  20. package/core/.specialist/tools/handoff-doc.js +95 -0
  21. package/core/.specialist/tools/lessons-harvest.js +243 -0
  22. package/core/.specialist/tools/lessons-lifecycle.js +103 -0
  23. package/core/.specialist/tools/lessons.js +149 -24
  24. package/core/KENAZ_CORE_VERSION +1 -1
  25. package/core/dev/plans/CLI_FIRST_INSTALL_FLOW.md +1 -1
  26. package/core/dev/scripts/correction-detect-eval.js +70 -0
  27. package/core/plugins/kenaz/commands/Ansuz.md +11 -1
  28. package/core/plugins/kenaz/commands/kenaz.md +5 -3
  29. package/core/plugins/kenaz/scripts/kenaz-mode.js +6 -0
  30. package/core/plugins/kenaz/scripts/statusline.js +60 -0
  31. package/core/plugins/kenaz/scripts/statusline.test.js +41 -0
  32. package/lib/cli.js +11 -4
  33. package/package.json +6 -6
  34. package/vendor/install-kenaz-cli.js +27 -1
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * correction-detect-eval.js (TASK_2426) — replays real user prompts from Claude Code
4
+ * transcripts through the correction detector. Read-only.
5
+ * node dev/scripts/correction-detect-eval.js [--transcripts <dir>] [--show 20]
6
+ * Default dir: ~/.claude/projects/<current project, path separators/colons -> "-">.
7
+ * Prints prompts scanned, fire rate (target <= 10%) and the first N fired prompts (80 chars).
8
+ */
9
+ 'use strict';
10
+ const fs = require('fs');
11
+ const os = require('os');
12
+ const path = require('path');
13
+ const { isCorrection } = require('../../.claude/hooks/correction-detect');
14
+
15
+ function arg(name, dflt) {
16
+ const i = process.argv.indexOf('--' + name);
17
+ return i > 0 && process.argv[i + 1] ? process.argv[i + 1] : dflt;
18
+ }
19
+
20
+ function defaultDir() {
21
+ const slug = path.resolve(process.cwd()).replace(/[^A-Za-z0-9]/g, '-');
22
+ return path.join(os.homedir(), '.claude', 'projects', slug);
23
+ }
24
+
25
+ function listJsonl(dir) {
26
+ const out = [];
27
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
28
+ const p = path.join(dir, e.name);
29
+ if (e.isDirectory()) out.push(...listJsonl(p));
30
+ else if (e.name.endsWith('.jsonl')) out.push(p);
31
+ }
32
+ return out;
33
+ }
34
+
35
+ function userText(rec) {
36
+ if (!rec || rec.type !== 'user' || rec.isMeta || rec.isSidechain) return '';
37
+ const c = rec.message && rec.message.content;
38
+ if (typeof c === 'string') return c;
39
+ if (Array.isArray(c) && !c.some((b) => b && b.type === 'tool_result')) {
40
+ return c.filter((b) => b && b.type === 'text').map((b) => b.text).join('\n');
41
+ }
42
+ return '';
43
+ }
44
+
45
+ function main() {
46
+ const dir = arg('transcripts', defaultDir());
47
+ const show = Number(arg('show', 20));
48
+ let scanned = 0;
49
+ const fired = [];
50
+ for (const file of listJsonl(dir)) {
51
+ let text;
52
+ try { text = fs.readFileSync(file, 'utf8'); } catch (_) { continue; }
53
+ for (const line of text.split('\n')) {
54
+ if (!line) continue;
55
+ let rec;
56
+ try { rec = JSON.parse(line); } catch (_) { continue; }
57
+ const t = userText(rec).trim();
58
+ if (!t) continue;
59
+ scanned++;
60
+ if (isCorrection(t)) fired.push(t);
61
+ }
62
+ }
63
+ const rate = scanned ? (100 * fired.length / scanned) : 0;
64
+ console.log('dir: ' + dir);
65
+ console.log('prompts scanned: ' + scanned);
66
+ console.log('fired: ' + fired.length + ' (' + rate.toFixed(1) + '%, target <= 10%)');
67
+ fired.slice(0, show).forEach((p, i) => console.log((i + 1) + '. ' + p.replace(/\s+/g, ' ').slice(0, 80)));
68
+ }
69
+
70
+ main();
@@ -8,7 +8,8 @@ ARGUMENTS 含 `[專案: X]` 或 `--project-path X` → PROJECT_PATH = X,下方
8
8
  ## Boot(一次 Bash,無其他儀式)
9
9
  1. Turn Context 若有 `[session] session_id=<id>`,記住 `<id>`。
10
10
  2. 執行 `node "${CLAUDE_PLUGIN_ROOT}/hooks/core-resolve.js" .specialist/tools/ansuz-boot-fast.js`(有 `<id>` 加 `--conversation-id="<id>"`;有 PROJECT_PATH 加 `--project-path`)。
11
- 3. 把 banner 轉述給用戶(Bash 輸出在 UI 會摺疊),並依 `📋 Open cards` 直接提出議程。
11
+ 3. banner 有 `📎 Project handoff` 時,**先用 Read 讀 → 標記的那份全文**(不用問用戶),以它為準接續上一個 session。
12
+ 4. 把 banner 轉述給用戶(Bash 輸出在 UI 會摺疊),依 handoff 的 R 段與 `📋 Open cards` 直接提出議程。
12
13
 
13
14
  ## 每件工作的做法
14
15
  - **自己做**:小而明確的修正(約 30 行內、單一檔案或少數檔案)→ 直接改,跑最近的測試,提交。
@@ -16,6 +17,7 @@ ARGUMENTS 含 `[專案: X]` 或 `--project-path X` → PROJECT_PATH = X,下方
16
17
  1. 用 `kenaz_task_create` 開卡(`projectPath` = 目前專案;寫清楚 requirements、acceptance criteria、scope paths)。查卡、改狀態用 `kenaz_task_list` / `kenaz_task_get` / `kenaz_task_update`。
17
18
  2. 產生派工內容:`node "${CLAUDE_PLUGIN_ROOT}/hooks/core-resolve.js" .formation/tools/dispatch.js --skill=do --task=TASK_XXX --dispatch-mode=subagent`(有 PROJECT_PATH 加 `--project-path`)。把輸出 JSON 的 `promptText` 交給 subagent,`agentParams` 照用(模型不要自己換)。
18
19
  3. subagent 在 worktree 裡改完並 commit;你驗證後合併。
20
+ 若輸出是 `executor` 區塊(模型為 `codex:<id>` / `deepseek:<model>`,無 agentParams):用 Bash run_in_background 執行 `executor.command`,等完成通知,讀 `executor.resultFile`(ok、commits;`no commits` = 失敗),再照常驗證並合併 `executor.branchName`。
19
21
  - 開卡前先確認 scope 與驗收條件對準真正的問題,不要照著錯的假設派工。
20
22
 
21
23
  ## 驗證(合併前一定做,強度跟風險相稱)
@@ -24,6 +26,12 @@ ARGUMENTS 含 `[專案: X]` 或 `--project-path X` → PROJECT_PATH = X,下方
24
26
  - subagent 回報「未驗證」的項目,由你補驗或明講仍未驗證。
25
27
  - 合併:`git merge` 後跑相關測試(或 `node "${CLAUDE_PLUGIN_ROOT}/hooks/core-resolve.js" .specialist/Ansuz/tools/verify-and-land.js`),用這個專案自己的測試指令。
26
28
 
29
+ ## 交接(handoff,ISBAR)
30
+ 用戶說交接/暫停/換 session、context 快滿、或結束時仍有未完成的工作 → 寫交接:
31
+ 1. `node "${CLAUDE_PLUGIN_ROOT}/hooks/core-resolve.js" .specialist/tools/handoff-doc.js new --topic "<主題>" --session <id>`(有 PROJECT_PATH 加 `--project-path`),建立 `<專案>/.kenaz/handoffs/<日期時間>-<主題>.md`,I 段已自動填好。
32
+ 2. 用 Edit 填 S(現況)、B(背景:用戶決定原話、相關卡與 commit)、A(已驗證/未驗證/風險)、R(下一個 session 第一步、待用戶決定的事)。寫給「沒看過這段對話的人」,不寫空泛句。
33
+ 3. 一份交接一個檔,不覆寫舊的;下一個 session 的 boot 會自動列出並讀最新那份。
34
+
27
35
  ## 回報與提問
28
36
  - 一律用用戶的語言(中文訊息 → 中文回覆,含提問選項)。
29
37
  - 需要用戶決定時用 `AskUserQuestion`(2-4 個選項、建議的放第一個),不要在文字裡列選項等回覆;有證據能自己判斷的事不問。
@@ -38,5 +46,7 @@ Kenaz 的工具、hook、安裝、peer、派工擋住或拖慢工作時,**繞
38
46
  ## 參考產品(REA)與學習教訓
39
47
  - banner `[rea]` 行:用戶提到參考產品時,有 REA 就提議分析,沒有就提示一次 `npx rea-agents setup`(範圍見 RULES.md 的 REA)。
40
48
  - 用戶糾正/重複指示、推翻決定、或驗證抓到失敗時,自動寫教訓並在回報點名一行:`node "${CLAUDE_PLUGIN_ROOT}/hooks/core-resolve.js" .specialist/tools/lessons.js add --scope user|project --type pref|fact|pitfall|check --text "..."`(規則見 RULES.md 的 Lessons)。
49
+ - banner 出現 `[lessons] N to graduate`:跑 `kenaz learn review`,提議把排第一的教訓改成代碼(測試、hook 檢查或專案 CLAUDE.md/AGENTS.md 規則,這是首選結果);糾正若與既有教訓相符屬於重複(recurrence),不是新教訓(`add` 會自動累加)。
50
+ - banner 出現 `[learn] N history items to review`:跑 `kenaz learn review`,每個歷史項目(返工、executor 失敗、blocked 卡、handoff 風險行)有現成的 `kenaz learn add ... --source harvest:<id>`,值得記的補上 why 後執行,其餘忽略;指標建議(各 rigor 一次過率、返工最多的檔案)只建議給用戶,不自行套用。詳見 RULES.md。
41
51
 
42
52
  需要更多規則時(授權自主、派工前、合併前、參考產品、寫教訓)讀 `${CLAUDE_PLUGIN_ROOT}/../../.specialist/Ansuz/RULES.md`。
@@ -1,12 +1,14 @@
1
1
  ---
2
- description: Switch or show the Kenaz mode (off | assist | full | status | init), or set up Yggdrasil (ygg join|status|leave)
3
- argument-hint: off|assist|full|status|init|ygg
2
+ description: Switch or show the Kenaz mode (off | assist | full | status | init), set up Yggdrasil (ygg join|status|leave), or show/set the subagent model (subagent [set <id>|unset] [--rigor r])
3
+ argument-hint: off|assist|full|status|init|ygg|subagent
4
4
  allowed-tools: Bash(node:*)
5
5
  ---
6
- Run exactly this and report its output (one line for modes; the full block for `ygg`), nothing else:
6
+ Run exactly this and report its output (one line for modes; the full block for `ygg` and `subagent`), nothing else:
7
7
 
8
8
  `node "${CLAUDE_PLUGIN_ROOT}/scripts/kenaz-mode.js" $ARGUMENTS`
9
9
 
10
10
  Modes: full = guidance + all hooks; assist = silent telemetry/bookkeeping hooks only (no context injection); off = every Kenaz hook exits immediately. init creates .kenaz/project.yaml so hooks act in a fresh project.
11
11
 
12
12
  `ygg join <key> [--project|--global]` stores the Yggdrasil connection key (default: this project's own slot) and activates collaboration here; `ygg status` shows room, node, relay, peers and mail (key masked); `ygg leave` removes this project's key. Needs kenaz-server running (installed by `npx kenaz install`).
13
+
14
+ `subagent` shows the subagent (dispatch) model per rigor for this project; `subagent set <id> [--rigor low|medium|high]` changes it, `subagent unset [--rigor r]` reverts (writes .kenaz/dashboard-models.json). Ansuz itself runs on this session's model (/model).
@@ -10,6 +10,12 @@ if (arg === 'ygg') {
10
10
  const r = require('child_process').spawnSync(process.execPath, [path.join(__dirname, 'ygg.js'), ...process.argv.slice(3)], { stdio: 'inherit' });
11
11
  process.exit(r.status === null ? 1 : r.status);
12
12
  }
13
+ if (arg === 'subagent' || arg === 'model') {
14
+ // Subagent (dispatch) model: subagent [set <id>|unset] [--rigor r]; same as `kenaz subagent`, for this project
15
+ const tool = path.join(__dirname, '..', '..', '..', '.formation', 'tools', 'dispatch-model.js');
16
+ const r = require('child_process').spawnSync(process.execPath, [tool, ...process.argv.slice(3), '--project-path', projectDir()], { stdio: 'inherit' });
17
+ process.exit(r.status === null ? 1 : r.status);
18
+ }
13
19
  const dir = projectDir();
14
20
  if (MODES.includes(arg)) {
15
21
  setMode(arg, dir);
@@ -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
+ });
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 create [--name N]|ygg status|ygg leave|alaya status|alaya register|model [set <id>|unset] [--rigor r]|stats [--days N] [--json]|learn <add|list|rm>> [--dry-run] [--home <dir>] [--no-service] [--no-claude] [--no-codex] [--keep-dashboard] [--server-bin <path>]';
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) {
@@ -284,7 +285,7 @@ async function doctor(p, flags, out, deps = {}) {
284
285
  if (dm) {
285
286
  const d = dm.describe(process.cwd());
286
287
  const uniq = [...new Set(d.rows.map((r) => r.model))];
287
- 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 model set <id>`');
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>`');
288
289
  }
289
290
  // Optional and user-installed: never a failure.
290
291
  const rea = reaDetect(p);
@@ -340,18 +341,24 @@ async function alaya(argv, out = console.log) {
340
341
 
341
342
  function run(argv, out = console.log, deps = {}) {
342
343
  if (argv[0] === 'alaya') return alaya(argv, out);
343
- if (argv[0] === 'model') {
344
+ if (argv[0] === 'subagent' || argv[0] === 'model') { // model: pre-0.3.13 name
344
345
  const dm = dispatchModel(paths(os.homedir()));
345
346
  if (!dm) { out('kenaz-core not installed: run `kenaz install`'); return 1; }
346
347
  return dm.main(argv.slice(1), out);
347
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
+ }
348
354
  if (argv[0] === 'stats') {
349
355
  const ks = kenazStats(paths(os.homedir()));
350
356
  if (!ks) { out('kenaz-core not installed: run `kenaz install`'); return 1; }
351
357
  return ks.main(argv.slice(1), out);
352
358
  }
353
359
  if (argv[0] === 'learn') {
354
- const lm = lessons(paths(os.homedir()));
360
+ const hi = argv.indexOf('--home');
361
+ const lm = lessons(paths(hi >= 0 && argv[hi + 1] ? path.resolve(argv[hi + 1]) : os.homedir()));
355
362
  if (!lm) { out('kenaz-core not installed: run `kenaz install`'); return 1; }
356
363
  return lm.main(argv.slice(1), out);
357
364
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kenaz",
3
- "version": "0.3.12",
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.12",
24
- "@kenazai/server-darwin-x64": "0.3.12",
25
- "@kenazai/server-linux-arm64": "0.3.12",
26
- "@kenazai/server-linux-x64": "0.3.12",
27
- "@kenazai/server-win32-x64": "0.3.12"
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": {
@@ -86,6 +86,29 @@ function unregisterPlugin(file, dir, dry) {
86
86
  return 'removed';
87
87
  }
88
88
 
89
+ // Status line with the project's task counts (plugins/kenaz/scripts/statusline.js). Set only when the user has no
90
+ // statusLine; a user's own is never replaced. Ours is recognised by the script path, so a moved core is updated.
91
+ const STATUSLINE_TAIL = 'plugins/kenaz/scripts/statusline.js';
92
+ const isOurStatusline = (s) => !!(s && typeof s.command === 'string' && norm(s.command).includes(STATUSLINE_TAIL));
93
+ /** @returns 'added'|'already'|'replaced'|'kept-user' */
94
+ function registerStatusline(file, core, dry) {
95
+ const v = loadSettings(file);
96
+ const command = 'node "' + path.join(core, ...STATUSLINE_TAIL.split('/')) + '"';
97
+ if (v.statusLine && !isOurStatusline(v.statusLine)) return 'kept-user';
98
+ if (v.statusLine && norm(v.statusLine.command) === norm(command)) return 'already';
99
+ const r = v.statusLine ? 'replaced' : 'added';
100
+ if (!dry) { v.statusLine = { type: 'command', command }; saveSettings(file, v); }
101
+ return r;
102
+ }
103
+ /** @returns 'removed'|'absent' */
104
+ function unregisterStatusline(file, dry) {
105
+ if (!fs.existsSync(file)) return 'absent';
106
+ const v = loadSettings(file);
107
+ if (!isOurStatusline(v.statusLine)) return 'absent';
108
+ if (!dry) { delete v.statusLine; saveSettings(file, v); }
109
+ return 'removed';
110
+ }
111
+
89
112
  // TASK_2384: Dashboard-era user-level hooks (wrapped by .claude/hooks/_user-scope.js, direct .claude/hooks/*.js, or the
90
113
  // agent-flow curl/node posts) duplicate what plugins/kenaz/hooks/hooks.json now runs. Single owner = the plugin, so
91
114
  // once it is registered these are removed (first write backs up the pristine file to settings.json.bak-kenaz-peer).
@@ -389,6 +412,7 @@ function main(argv, out = console.log, getVersion = claudeVersion, deps = {}) {
389
412
  out(name + ': ' + (list.some((e) => norm(e) === norm(pluginDir)) ? 'registered' : 'not registered') + ' (' + pluginDir + ')');
390
413
  } else if (cmd === 'uninstall') {
391
414
  log(name + ': ' + unregisterPlugin(settings, pluginDir, dry) + ' (' + settings + ')');
415
+ if (name === 'kenaz') log('statusline: ' + unregisterStatusline(settings, dry));
392
416
  } else if (process.env.KENAZ_SKIP_CLAUDE_PROVISION === '1') {
393
417
  log(name + ': skipped (KENAZ_SKIP_CLAUDE_PROVISION=1)');
394
418
  } else if (!fs.existsSync(path.join(pluginDir, 'hooks', 'hooks.json'))) {
@@ -400,6 +424,8 @@ function main(argv, out = console.log, getVersion = claudeVersion, deps = {}) {
400
424
  else {
401
425
  log(name + ': ' + registerPlugin(settings, pluginDir, dry) + ' (' + settings + ')');
402
426
  if (name === 'kenaz') {
427
+ const sl = registerStatusline(settings, core, dry);
428
+ log('statusline: ' + (sl === 'kept-user' ? 'kept your own statusLine; to show Kenaz task counts, append the output of: node "' + path.join(core, ...STATUSLINE_TAIL.split('/')) + '"' : sl));
403
429
  const n = retireDashboardHooks(settings, dry);
404
430
  if (n) log('kenaz: retired ' + n + ' Dashboard-era user-level hook(s), now owned by the plugin (backup: ' + path.join(path.dirname(settings), BACKUP) + ')');
405
431
  }
@@ -427,4 +453,4 @@ function main(argv, out = console.log, getVersion = claudeVersion, deps = {}) {
427
453
  }
428
454
 
429
455
  if (require.main === module) process.exit(main(process.argv.slice(2)));
430
- module.exports = { main, verifyRuntime, healthOnce, serviceInstall, serviceRestart, restartHint, claudeVersion, loadSettings, entriesOf, norm, MIN_CLI, registerPlugin, unregisterPlugin, retireDashboardHooks, servicePlan, detectServiceMode, probeLinux, cronLine, CRON_MARK, winVbs, launchdPlist, systemdUnit, parseVersion, cmpVersion };
456
+ module.exports = { main, verifyRuntime, healthOnce, serviceInstall, serviceRestart, restartHint, claudeVersion, loadSettings, entriesOf, norm, MIN_CLI, registerPlugin, unregisterPlugin, registerStatusline, unregisterStatusline, retireDashboardHooks, servicePlan, detectServiceMode, probeLinux, cronLine, CRON_MARK, winVbs, launchdPlist, systemdUnit, parseVersion, cmpVersion };