kushi-agents 5.9.0 → 5.9.2

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/bin/cli.mjs CHANGED
@@ -1,639 +1,640 @@
1
- #!/usr/bin/env node
2
-
3
- import { main } from '../src/main.mjs';
4
- import { runMultiHost } from '../src/multi-host.mjs';
5
-
6
- const args = process.argv.slice(2);
7
-
8
- // ── bare invocation (v5.4.0+) ────────────────────────────────────────────────
9
- // v5.4.1: on an interactive TTY, auto-launch the setup wizard (matches the
10
- // ergonomics of `npx create-*`). Non-TTY (CI, scripts, piped stdin) and the
11
- // explicit KUSHI_SKIP_WELCOME=1 still print the welcome card and exit 0, so
12
- // nothing is installed by side-effect.
13
- if (args.length === 0) {
14
- const forceWelcome = process.env.KUSHI_SKIP_WELCOME === '1';
15
- const forceWizard = process.env.KUSHI_FORCE_WIZARD === '1';
16
- const interactive = forceWizard || (process.stdin.isTTY && !forceWelcome);
17
- if (interactive) {
18
- const { runSetupWizard } = await import('../src/setup-wizard.mjs');
19
- await runSetupWizard({ args: [] });
20
- process.exit(0);
21
- }
22
- await printWelcome();
23
- process.exit(0);
24
- }
25
-
26
- // ── doctor verb (v5.4.0+) ───────────────────────────────────────────────────
27
- if (args.length > 0 && args[0] === 'doctor') {
28
- const { spawnSync } = await import('node:child_process');
29
- const pathMod = await import('node:path');
30
- const urlMod = await import('node:url');
31
- const here = pathMod.dirname(urlMod.fileURLToPath(import.meta.url));
32
- const script = pathMod.resolve(here, '..', 'plugin', 'skills', 'doctor', 'doctor.ps1');
33
- const psArgs = ['-NoProfile', '-File', script];
34
- if (args.includes('--json')) psArgs.push('-Json');
35
- if (args.includes('--strict')) psArgs.push('-Strict');
36
- const r = spawnSync('pwsh', psArgs, { stdio: 'inherit' });
37
- process.exit(r.status ?? 1);
38
- }
39
-
40
- // ── setup-wizard flag (v5.4.0+) ─────────────────────────────────────────────
41
- if (args.includes('--setup-wizard')) {
42
- const { runSetupWizard } = await import('../src/setup-wizard.mjs');
43
- await runSetupWizard({ args });
44
- process.exit(0);
45
- }
46
-
47
- async function printWelcome() {
48
- const pathMod = await import('node:path');
49
- const urlMod = await import('node:url');
50
- const fsMod = await import('node:fs');
51
- const here = pathMod.dirname(urlMod.fileURLToPath(import.meta.url));
52
- const repoRoot = pathMod.resolve(here, '..');
53
- let version = 'unknown';
54
- try { version = JSON.parse(fsMod.readFileSync(pathMod.join(repoRoot, 'package.json'), 'utf-8')).version; } catch {}
55
- let skillCount = 0;
56
- try {
57
- const skillsDir = pathMod.join(repoRoot, 'plugin', 'skills');
58
- skillCount = fsMod.readdirSync(skillsDir, { withFileTypes: true })
59
- .filter((d) => d.isDirectory() && !d.name.startsWith('_'))
60
- .length;
61
- } catch {}
62
- console.log(`
63
- kushi v${version} — multi-source M365 project evidence agent
64
-
65
- (non-interactive shell — nothing was installed)
66
-
67
- First time? kushi doctor
68
- Bootstrap a project: kushi setup <project>
69
- Ask a question: kushi ask <project> "..."
70
- Wizard install: npx kushi-agents --setup-wizard
71
- Host install: npx kushi-agents --clawpilot | --vscode | --all-hosts
72
-
73
- Docs: https://gim-home.github.io/kushi/
74
- Skills: ${skillCount} installed in plugin/skills/
75
-
76
- Run kushi --help for the full verb list.
77
- `);
78
- }
79
-
80
- // ── skill-authoring verbs (v5.0.4+) ─────────────────────────────────────────
81
- // Dispatch directly to the skill-creator / skill-checker pwsh scripts.
82
- const SKILL_VERBS = new Set(['create-skill', 'check-skill', 'optimize-description', 'review-evals']);
83
- if (args.length > 0 && SKILL_VERBS.has(args[0])) {
84
- const verb = args[0];
85
- const rest = args.slice(1);
86
- await dispatchSkillVerb(verb, rest);
87
- process.exit(0);
88
- }
89
-
90
- // ── lint verb (v5.1.0+) ──────────────────────────────────────────────────────
91
- if (args.length > 0 && args[0] === 'lint') {
92
- if (args.includes('--global')) {
93
- const { runGlobalLint } = await import('../src/global-wiki-cli.mjs');
94
- await runGlobalLint();
95
- process.exit(0);
96
- }
97
- const project = args[1] || '';
98
- if (!project) {
99
- console.error('\n Usage: kushi lint <project>\n kushi lint --global\n');
100
- process.exit(1);
101
- }
102
- await dispatchLint(project);
103
- process.exit(0);
104
- }
105
-
106
- // ── global verb (v5.3.0+) ────────────────────────────────────────────────────
107
- if (args.length > 0 && args[0] === 'global') {
108
- const sub = args[1] || '';
109
- const validSubs = ['init', 'status', 'ask', 'lint'];
110
- if (!validSubs.includes(sub)) {
111
- console.error('\n Usage: kushi global init Scaffold ~/.kushi-global/State/');
112
- console.error(' kushi global status Show counts + freshness');
113
- console.error(' kushi global ask <question> Ask the global wiki');
114
- console.error(' kushi global lint Lint the global wiki\n');
115
- process.exit(1);
116
- }
117
- const { runGlobalInit, runGlobalStatus, runGlobalAsk, runGlobalLint } = await import('../src/global-wiki-cli.mjs');
118
- if (sub === 'init') await runGlobalInit();
119
- else if (sub === 'status') await runGlobalStatus();
120
- else if (sub === 'ask') await runGlobalAsk(args.slice(2).join(' '));
121
- else if (sub === 'lint') await runGlobalLint();
122
- process.exit(0);
123
- }
124
-
125
- // ── promote verb (v5.3.0+) ───────────────────────────────────────────────────
126
- if (args.length > 0 && args[0] === 'promote') {
127
- const project = args[1] || '';
128
- const page = args[2] || '';
129
- if (!project || !page) {
130
- console.error('\n Usage: kushi promote <project> <page-path>\n');
131
- console.error(' Copies a project State page into the global wiki with provenance metadata.');
132
- console.error(' Refuses by default if customer identifiers are detected; pass --force after review.\n');
133
- process.exit(1);
134
- }
135
- const force = args.includes('--force');
136
- const { runPromote } = await import('../src/global-wiki-cli.mjs');
137
- await runPromote(project, page, { force });
138
- process.exit(0);
139
- }
140
-
141
- // ── hooks verb (v5.2.0+) ─────────────────────────────────────────────────────
142
- if (args.length > 0 && args[0] === 'hooks') {
143
- const sub = args[1] || '';
144
- const project = args[2] || '';
145
- if (!sub || !project || !['list', 'test'].includes(sub)) {
146
- console.error('\n Usage: kushi hooks list <project>\n kushi hooks test <project> <event>\n');
147
- process.exit(1);
148
- }
149
- await dispatchHooks(sub, project, args.slice(3));
150
- process.exit(0);
151
- }
152
-
153
- // ── explain verb (v5.2.0+) ───────────────────────────────────────────────────
154
- if (args.length > 0 && args[0] === 'explain') {
155
- const topic = args.slice(1).join(' ');
156
- if (!topic) {
157
- console.error('\n Usage: kushi explain <topic>\n\n Available topics: contradictions, refresh, state, hooks, parallel, otel, csc, graph, workiq, schema, install, evals\n');
158
- process.exit(1);
159
- }
160
- await dispatchExplain(topic);
161
- process.exit(0);
162
- }
163
-
164
- // ── remember verb (v5.2.0+) ──────────────────────────────────────────────────
165
- if (args.length > 0 && args[0] === 'remember') {
166
- const rule = args.slice(1).join(' ');
167
- if (!rule) {
168
- console.error('\n Usage: kushi remember <rule>\n\n Example: kushi remember "always use Northwind not Healthcare Accelerator"\n');
169
- process.exit(1);
170
- }
171
- await dispatchRemember(rule);
172
- process.exit(0);
173
- }
174
-
175
- if (args.includes('--help') || args.includes('-h')) {
176
- console.log(`
177
- Usage: npx kushi-agents [options]
178
-
179
- Installs the Kushi multi-source project-evidence + Q&A agent.
180
-
181
- Host installs (v5.0.2+ — install into a host's user-global skill folder):
182
- --clawpilot Install to ~/.copilot/m-skills/kushi/
183
- --vscode Install to ~/.vscode/chat/skills/kushi/ (a.k.a. GitHub Copilot Chat)
184
- --all-hosts Install to BOTH hosts
185
- --no-workspace Skip the auto workspace install (host install only)
186
- --uninstall [--clawpilot|--vscode|--all]
187
- Cleanly remove the kushi install + skills-metadata.json entry
188
- from the chosen host(s). Default = all detected hosts.
189
-
190
- Note: when run from inside a project directory (any of: package.json, .git,
191
- .kushi/, Evidence/, etc.), host installs ALSO refresh the workspace
192
- .kushi/ install in cwd. One command covers both. Pass --no-workspace to
193
- suppress, or run from a non-project directory.
194
-
195
- Workspace install (legacy / default when no host flag is given):
196
- --target vscode Install to <cwd>/.kushi/ + update .vscode/settings.json [default]
197
- --target clawpilot Alias for --clawpilot (kept for back-compat)
198
-
199
- Profile (controls what gets installed):
200
- --profile core Aggregator only (pull + consolidate + ask).
201
- --profile standard Core + State/ rollup. [DEFAULT]
202
- --profile full Standard + report packs.
203
-
204
- Options:
205
- --force Overwrite existing destination without asking
206
- --yes, -y Skip the project-root check
207
- --no-settings Skip .vscode/settings.json update (vscode workspace target only)
208
- --no-instructions Skip .github/copilot-instructions.md merge (vscode workspace target only)
209
- --no-prompt Skip the post-install m365-auth quickstart (3 fields that
210
- make 'kushi discover' fast). Also disabled by KUSHI_NO_PROMPT=1.
211
-
212
- WorkIQ (REQUIRED Kushi cannot pull evidence without it):
213
- --with-workiq Auto-install WorkIQ via winget (Windows) / brew (macOS)
214
- --workiq-path <abs> Use this explicit path to the workiq binary
215
- --skip-workiq-check Bypass the WorkIQ pre-flight check
216
-
217
- --help, -h Show this help
218
-
219
- Skill authoring (v5.0.4+):
220
- create-skill <name> --type <pull|writer|orchestrator|other> --description "<d>"
221
- Scaffold a new plugin/skills/<name>/ tree.
222
- check-skill <name> Lint a skill against the agentskills.io blueprint.
223
- check-skill --all [--retrofit [--apply]]
224
- Audit (or retrofit) every skill in plugin/skills/.
225
- optimize-description <skill>
226
- Rewrite a skill's description per the optimizer rules.
227
- review-evals <skill> Render an HTML side-by-side eval-review viewer.
228
-
229
- Wiki maintenance (v5.1.0+):
230
- lint <project> Run wiki-lint checks on State/ (contradictions, stale claims, orphans).
231
- lint --global Lint the global wiki at ~/.kushi-global/State/.
232
-
233
- Hooks & observability (v5.2.0+):
234
- hooks list <project> List configured hooks for a project.
235
- hooks test <project> <event> Fire a synthetic hook event for testing.
236
- explain <topic> Explain a kushi concept (pedagogical, read-only).
237
- remember <rule> Persist a project convention to CLAUDE.md.
238
-
239
- Global wiki (v5.3.0+):
240
- global init Scaffold ~/.kushi-global/State/ (env: KUSHI_GLOBAL_ROOT)
241
- global status Show page counts + freshness for the global wiki.
242
- global ask <question> Search the global wiki specifically.
243
- global lint Lint the global wiki (alias for 'lint --global').
244
- promote <project> <page> Move a project State page into global with redaction + back-link.
245
- Refuses if customer identifiers are detected; --force after review.
246
-
247
- Stabilization (v5.4.0+):
248
- doctor Aggregated health check (env, self-check, evals, skill-checker, drift, global).
249
- --json for CI; --strict to fail on yellow.
250
- --setup-wizard Interactive first-run flow (engagement root, hosts, global wiki).
251
-
252
- After install, talk to Kushi:
253
- bootstrap <project> First-time setup
254
- refresh <project> Incremental refresh + rebuild State/
255
- state <project> Re-render State/ from existing Evidence (deterministic
256
- inventory; LLM build-state skill does narrative synthesis)
257
- references <project> Scan Evidence for URLs and refresh the shared
258
- references pool (Evidence/_shared/references/)
259
- consolidate <project> Merge per-user evidence
260
- status <project> Show run-log
261
- ask <project> <q> Cited Q&A over Evidence/ (auto-routes, --file-back to save)
262
- lint <project> Run wiki-lint checks on State/
263
-
264
- Workspace lifecycle (v5.9.0+):
265
- uninstall [--keep-config] Remove <cwd>/.kushi/ (preserves Evidence/, State/).
266
- --keep-config preserves config/user/ identity files.
267
- upgrade npm i -g kushi-agents@latest then re-seed assets
268
- in cwd (config preserved).
269
-
270
- In VS Code Chat the prefix is "@Kushi". In Clawpilot just say "kushi <verb>".
271
- `);
272
- process.exit(0);
273
- }
274
-
275
- // ── state / refresh / bootstrap verbs (v5.9.0+) ─────────────────────────────
276
- // Thin shells that exec the deterministic runners. Keeps `kushi state HCA` etc.
277
- // runnable from the global bin without users having to know the runner paths.
278
- if (args.length > 0 && ['state', 'refresh-runner', 'bootstrap-runner', 'discover', 'references'].includes(args[0])) {
279
- const verb = args[0];
280
- const project = args[1];
281
- if (!project) {
282
- console.error(`\n Usage: kushi ${verb} <project> [options]\n`);
283
- process.exit(1);
284
- }
285
- const { spawnSync } = await import('node:child_process');
286
- const pathMod = await import('node:path');
287
- const urlMod = await import('node:url');
288
- const here = pathMod.dirname(urlMod.fileURLToPath(import.meta.url));
289
- const runnerMap = {
290
- state: 'pull-state.mjs',
291
- references: 'pull-references.mjs',
292
- discover: 'discover.mjs',
293
- 'refresh-runner': 'refresh.mjs',
294
- 'bootstrap-runner': 'bootstrap.mjs',
295
- };
296
- const runner = pathMod.resolve(here, '..', 'plugin', 'runners', runnerMap[verb]);
297
- const passthrough = args.slice(2);
298
- const r = spawnSync(process.execPath, [runner, '--project', project, ...passthrough], { stdio: 'inherit' });
299
- process.exit(r.status ?? 1);
300
- }
301
-
302
- // ── workspace uninstall / upgrade verbs (v5.9.0+) ───────────────────────────
303
- if (args.length > 0 && args[0] === 'uninstall' && !args.includes('--clawpilot') && !args.includes('--vscode') && !args.includes('--all-hosts')) {
304
- // Workspace uninstall: remove .kushi/ from cwd (preserves Evidence/, State/).
305
- const fsMod = await import('node:fs');
306
- const pathMod = await import('node:path');
307
- const dest = pathMod.resolve(process.cwd(), '.kushi');
308
- const keepConfig = args.includes('--keep-config');
309
- if (!fsMod.existsSync(dest)) {
310
- console.error(`\n No .kushi/ directory found at ${dest}\n`);
311
- process.exit(1);
312
- }
313
- if (keepConfig) {
314
- const assetDirs = ['agents', 'instructions', 'prompts', 'skills', 'templates', 'reference-packs', 'lib', 'runners'];
315
- let removed = 0;
316
- for (const d of assetDirs) {
317
- const p = pathMod.join(dest, d);
318
- if (fsMod.existsSync(p)) { fsMod.rmSync(p, { recursive: true, force: true }); removed++; }
319
- }
320
- console.log(`\n Removed ${removed} asset dir(s) from ${dest} (config/user/ preserved).\n`);
321
- } else {
322
- fsMod.rmSync(dest, { recursive: true, force: true });
323
- console.log(`\n Removed ${dest}\n Evidence/ and State/ left untouched.\n`);
324
- }
325
- process.exit(0);
326
- }
327
-
328
- if (args.length > 0 && args[0] === 'upgrade') {
329
- // Upgrade: npm i -g @latest, then re-seed assets in cwd preserving config.
330
- const { spawnSync } = await import('node:child_process');
331
- console.log('\n Upgrading kushi-agents globally via npm...\n');
332
- const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
333
- const r1 = spawnSync(npm, ['install', '-g', 'kushi-agents@latest'], { stdio: 'inherit' });
334
- if (r1.status !== 0) {
335
- console.error('\n npm install failed.\n');
336
- process.exit(r1.status ?? 1);
337
- }
338
- console.log('\n Refreshing assets in cwd (config preserved)...\n');
339
- const fsMod = await import('node:fs');
340
- if (fsMod.existsSync('.kushi')) {
341
- const r2 = spawnSync(npm, ['exec', '--', 'kushi-agents', '--no-prompt', '--force'], { stdio: 'inherit' });
342
- process.exit(r2.status ?? 0);
343
- } else {
344
- console.log('\n No .kushi/ in cwd — global upgrade complete; cd into a project and run `kushi` to install.\n');
345
- process.exit(0);
346
- }
347
- }
348
-
349
- // ── multi-host mode (v5.0.2+) ───────────────────────────────────────────────
350
- // Trigger when the user passes any of: --vscode, --all-hosts, --uninstall.
351
- // --clawpilot ALONE continues to route through the legacy main.mjs path so
352
- // the existing target=clawpilot flow stays byte-identical.
353
- const wantsVscode = args.includes('--vscode');
354
- const wantsAllHosts = args.includes('--all-hosts');
355
- const wantsUninstall = args.includes('--uninstall');
356
-
357
- if (wantsVscode || wantsAllHosts || wantsUninstall) {
358
- const hosts = [];
359
- if (args.includes('--clawpilot')) hosts.push('clawpilot');
360
- if (wantsVscode) hosts.push('vscode');
361
- const all = wantsAllHosts || args.includes('--all');
362
-
363
- runMultiHost({
364
- hosts,
365
- all,
366
- uninstall: wantsUninstall,
367
- profile: getFlag('--profile'),
368
- includeWorkspace: !args.includes('--no-workspace'),
369
- noPrompt: args.includes('--no-prompt'),
370
- }).catch((err) => {
371
- console.error(`\n ${err.message}\n`);
372
- process.exit(1);
373
- });
374
- } else {
375
- let target = getFlag('--target');
376
- if (args.includes('--clawpilot')) {
377
- if (target && target !== 'clawpilot') {
378
- console.error(`\n Conflicting flags: --target ${target} and --clawpilot.\n`);
379
- process.exit(1);
380
- }
381
- target = 'clawpilot';
382
- }
383
-
384
- const options = {
385
- force: args.includes('--force'),
386
- yes: args.includes('--yes') || args.includes('-y'),
387
- noSettings: args.includes('--no-settings'),
388
- noInstructions: args.includes('--no-instructions'),
389
- noPrompt: args.includes('--no-prompt'),
390
- target,
391
- profile: getFlag('--profile'),
392
- withWorkiq: args.includes('--with-workiq'),
393
- workiqPath: getFlag('--workiq-path'),
394
- skipWorkiqCheck: args.includes('--skip-workiq-check'),
395
- };
396
-
397
- main(options).catch((err) => {
398
- console.error(`\n ${err.message}\n`);
399
- process.exit(1);
400
- });
401
- }
402
-
403
- function getFlag(flag) {
404
- const idx = args.indexOf(flag);
405
- if (idx !== -1 && idx + 1 < args.length) {
406
- return args[idx + 1];
407
- }
408
- const prefix = flag + '=';
409
- const match = args.find((a) => a.startsWith(prefix));
410
- return match ? match.slice(prefix.length) : undefined;
411
- }
412
-
413
- // ── skill-authoring verb dispatch (v5.0.4+) ─────────────────────────────────
414
- async function dispatchSkillVerb(verb, rest) {
415
- const { spawnSync } = await import('node:child_process');
416
- const path = await import('node:path');
417
- const url = await import('node:url');
418
- const here = path.dirname(url.fileURLToPath(import.meta.url));
419
- const repoRoot = path.resolve(here, '..');
420
- const creatorDir = path.join(repoRoot, 'plugin', 'skills', 'skill-creator');
421
- const checkerDir = path.join(repoRoot, 'plugin', 'skills', 'skill-checker');
422
-
423
- let script, scriptArgs = [];
424
- switch (verb) {
425
- case 'create-skill': {
426
- // Usage: kushi create-skill <name> [--type <t>] [--description "<d>"] [--force]
427
- const name = rest.find((a) => !a.startsWith('-'));
428
- if (!name) {
429
- console.error('Usage: kushi-agents create-skill <name> --type <pull|writer|orchestrator|other> --description "USE WHEN ... DO NOT USE FOR ..."');
430
- process.exit(1);
431
- }
432
- const type = pickFlag(rest, '--type') || 'other';
433
- const desc = pickFlag(rest, '--description') || `USE WHEN ${name} is invoked. DO NOT USE FOR unrelated tasks.`;
434
- script = path.join(creatorDir, 'scaffold.ps1');
435
- scriptArgs = ['-Name', name, '-Type', type, '-Description', desc];
436
- if (rest.includes('--force')) scriptArgs.push('-Force');
437
- if (rest.includes('--dry-run')) scriptArgs.push('-DryRun');
438
- break;
439
- }
440
- case 'check-skill': {
441
- // Usage: kushi check-skill <name> | --all [--retrofit] [--apply]
442
- script = path.join(checkerDir, 'check-skill.ps1');
443
- const allFlag = rest.includes('--all') || rest.includes('-All');
444
- const name = rest.find((a) => !a.startsWith('-'));
445
- if (allFlag) scriptArgs.push('-All');
446
- else if (name) scriptArgs.push('-Skill', name);
447
- else {
448
- console.error('Usage: kushi-agents check-skill <name> | --all [--retrofit] [--apply] [--dry-run]');
449
- process.exit(1);
450
- }
451
- if (rest.includes('--retrofit')) scriptArgs.push('-Retrofit');
452
- if (rest.includes('--apply')) scriptArgs.push('-Apply');
453
- if (rest.includes('--dry-run')) scriptArgs.push('-DryRun');
454
- if (rest.includes('--json')) scriptArgs.push('-Json');
455
- break;
456
- }
457
- case 'optimize-description': {
458
- // Usage: kushi optimize-description <skill>
459
- const name = rest.find((a) => !a.startsWith('-'));
460
- if (!name) {
461
- console.error('Usage: kushi-agents optimize-description <skill>');
462
- process.exit(1);
463
- }
464
- script = path.join(checkerDir, 'check-skill.ps1');
465
- scriptArgs = ['-Skill', name, '-OptimizeDescription'];
466
- break;
467
- }
468
- case 'review-evals': {
469
- // Usage: kushi review-evals <skill>
470
- const name = rest.find((a) => !a.startsWith('-'));
471
- if (!name) {
472
- console.error('Usage: kushi-agents review-evals <skill>');
473
- process.exit(1);
474
- }
475
- script = path.join(checkerDir, 'check-skill.ps1');
476
- scriptArgs = ['-Skill', name, '-Review'];
477
- break;
478
- }
479
- default:
480
- console.error(`Unknown skill verb: ${verb}`);
481
- process.exit(1);
482
- }
483
-
484
- const result = spawnSync('pwsh', ['-NoProfile', '-File', script, ...scriptArgs], { stdio: 'inherit' });
485
- process.exit(result.status ?? 1);
486
- }
487
-
488
- async function dispatchLint(project) {
489
- const { spawn } = await import('node:child_process');
490
- const { resolve } = await import('node:path');
491
- const { fileURLToPath } = await import('node:url');
492
- const __dirname = fileURLToPath(new URL('.', import.meta.url));
493
- const scriptPath = resolve(__dirname, '..', 'plugin', 'skills', 'lint-state', 'lint.ps1');
494
-
495
- const cwd = process.cwd();
496
- const { readdirSync, existsSync } = await import('node:fs');
497
- let stateDir = '';
498
-
499
- const evidenceDir = resolve(cwd, project, 'Evidence');
500
- if (existsSync(evidenceDir)) {
501
- const aliases = readdirSync(evidenceDir, { withFileTypes: true })
502
- .filter(d => d.isDirectory() && !d.name.startsWith('_'));
503
- for (const alias of aliases) {
504
- const candidate = resolve(evidenceDir, alias.name, 'State');
505
- if (existsSync(candidate)) { stateDir = candidate; break; }
506
- }
507
- }
508
-
509
- if (!stateDir) {
510
- const direct = resolve(cwd, project, 'State');
511
- if (existsSync(direct)) { stateDir = direct; }
512
- }
513
-
514
- if (!stateDir) {
515
- console.error(`\n Could not find State/ directory for project '${project}'.`);
516
- console.error(` Looked in: ${evidenceDir}/*/State/ and ${resolve(cwd, project, 'State')}/`);
517
- console.error(` Run 'kushi state ${project}' first to build State/.\n`);
518
- process.exit(1);
519
- }
520
-
521
- const child = spawn('pwsh', ['-NoProfile', '-File', scriptPath, '-StateDir', stateDir], {
522
- stdio: 'inherit',
523
- cwd: resolve(__dirname, '..'),
524
- });
525
-
526
- return new Promise((res, rej) => {
527
- child.on('close', (code) => {
528
- if (code !== 0) rej(new Error(`lint-state exited with code ${code}`));
529
- else res();
530
- });
531
- child.on('error', rej);
532
- });
533
- }
534
-
535
- function pickFlag(args, flag) {
536
- const idx = args.indexOf(flag);
537
- if (idx !== -1 && idx + 1 < args.length) return args[idx + 1];
538
- const prefix = flag + '=';
539
- const m = args.find((a) => a.startsWith(prefix));
540
- return m ? m.slice(prefix.length) : undefined;
541
- }
542
-
543
- // ── v5.2.0 dispatch helpers ──────────────────────────────────────────────────
544
-
545
- async function dispatchHooks(sub, project, extra) {
546
- const { spawn } = await import('node:child_process');
547
- const { resolve } = await import('node:path');
548
- const { fileURLToPath } = await import('node:url');
549
- const __dirname = fileURLToPath(new URL('.', import.meta.url));
550
- const invokeHooks = resolve(__dirname, '..', 'plugin', 'skills', '_shared', 'Invoke-Hooks.ps1');
551
-
552
- if (sub === 'list') {
553
- // List hooks from .kushi/hooks.yml
554
- const { existsSync, readFileSync } = await import('node:fs');
555
- const hooksYml = resolve(process.cwd(), project, '.kushi', 'hooks.yml');
556
- if (!existsSync(hooksYml)) {
557
- console.log(`\n No hooks configured for '${project}'. Create ${hooksYml} to add hooks.\n`);
558
- return;
559
- }
560
- console.log(`\n Hooks for '${project}' (${hooksYml}):\n`);
561
- console.log(readFileSync(hooksYml, 'utf8'));
562
- } else if (sub === 'test') {
563
- const event = extra[0] || 'post-pull';
564
- const script = `
565
- $payload = @{ project = '${project}'; source = 'test'; success = $true; duration_ms = 0; event = '${event}' }
566
- & '${invokeHooks.replace(/\\/g, '\\\\')}' -ProjectRoot '${resolve(process.cwd(), project).replace(/\\/g, '\\\\')}' -Event '${event}' -Payload $payload
567
- `;
568
- const child = spawn('pwsh', ['-NoProfile', '-Command', script], { stdio: 'inherit' });
569
- return new Promise((res, rej) => {
570
- child.on('close', (code) => { if (code !== 0) rej(new Error(`hooks test exited ${code}`)); else res(); });
571
- child.on('error', rej);
572
- });
573
- }
574
- }
575
-
576
- async function dispatchExplain(topic) {
577
- const { resolve } = await import('node:path');
578
- const { existsSync, readFileSync } = await import('node:fs');
579
- const { fileURLToPath } = await import('node:url');
580
- const __dirname = fileURLToPath(new URL('.', import.meta.url));
581
- const repoRoot = resolve(__dirname, '..');
582
- const instructionsDir = resolve(repoRoot, 'plugin', 'instructions');
583
-
584
- const topicMap = {
585
- contradictions: 'living-wiki.instructions.md',
586
- conflicts: 'living-wiki.instructions.md',
587
- refresh: 'parallel-execution.instructions.md',
588
- pull: 'parallel-execution.instructions.md',
589
- state: 'living-wiki.instructions.md',
590
- 'build-state': 'living-wiki.instructions.md',
591
- wiki: 'living-wiki.instructions.md',
592
- hooks: 'hooks.instructions.md',
593
- events: 'hooks.instructions.md',
594
- webhooks: 'hooks.instructions.md',
595
- parallel: 'parallel-execution.instructions.md',
596
- workers: 'parallel-execution.instructions.md',
597
- otel: 'otel.instructions.md',
598
- telemetry: 'otel.instructions.md',
599
- tracing: 'otel.instructions.md',
600
- csc: 'comprehensive-structured-capture.instructions.md',
601
- capture: 'comprehensive-structured-capture.instructions.md',
602
- graph: 'entity-graph.instructions.md',
603
- entities: 'entity-graph.instructions.md',
604
- workiq: 'workiq-only.instructions.md',
605
- schema: 'schema-evolve.instructions.md',
606
- conventions: 'schema-evolve.instructions.md',
607
- remember: 'schema-evolve.instructions.md',
608
- install: 'multi-host-install.instructions.md',
609
- setup: 'multi-host-install.instructions.md',
610
- evals: 'skill-evals.instructions.md',
611
- };
612
-
613
- const key = Object.keys(topicMap).find(k => topic.toLowerCase().includes(k));
614
- if (!key) {
615
- console.log(`\n Topic not found: "${topic}"\n`);
616
- console.log(' Available topics:', Object.keys(topicMap).filter((v, i, a) => a.indexOf(v) === i).join(', '));
617
- console.log('');
618
- return;
619
- }
620
-
621
- const docFile = resolve(instructionsDir, topicMap[key]);
622
- if (!existsSync(docFile)) {
623
- console.error(` Doctrine file missing: ${topicMap[key]}`);
624
- process.exit(1);
625
- }
626
-
627
- const content = readFileSync(docFile, 'utf8');
628
- const lines = content.split('\n').slice(0, 40);
629
- console.log(`\n 📖 Topic: ${key} → ${topicMap[key]}\n`);
630
- console.log(lines.join('\n'));
631
- console.log(`\n ... (full doctrine at: plugin/instructions/${topicMap[key]})\n`);
632
- }
633
-
634
- async function dispatchRemember(rule) {
635
- console.log(`\n ✅ Rule noted: "${rule}"`);
636
- console.log(' To persist this rule, run schema-evolve from within a project context:');
637
- console.log(' @Kushi remember ' + rule);
638
- console.log(' This will write to Evidence/<alias>/State/CLAUDE.md\n');
639
- }
1
+ #!/usr/bin/env node
2
+
3
+ import { main } from '../src/main.mjs';
4
+ import { runMultiHost } from '../src/multi-host.mjs';
5
+
6
+ const args = process.argv.slice(2);
7
+
8
+ // ── bare invocation (v5.4.0+) ────────────────────────────────────────────────
9
+ // v5.4.1: on an interactive TTY, auto-launch the setup wizard (matches the
10
+ // ergonomics of `npx create-*`). Non-TTY (CI, scripts, piped stdin) and the
11
+ // explicit KUSHI_SKIP_WELCOME=1 still print the welcome card and exit 0, so
12
+ // nothing is installed by side-effect.
13
+ if (args.length === 0) {
14
+ const forceWelcome = process.env.KUSHI_SKIP_WELCOME === '1';
15
+ const forceWizard = process.env.KUSHI_FORCE_WIZARD === '1';
16
+ const interactive = forceWizard || (process.stdin.isTTY && !forceWelcome);
17
+ if (interactive) {
18
+ const { runSetupWizard } = await import('../src/setup-wizard.mjs');
19
+ await runSetupWizard({ args: [] });
20
+ process.exit(0);
21
+ }
22
+ await printWelcome();
23
+ process.exit(0);
24
+ }
25
+
26
+ // ── doctor verb (v5.4.0+) ───────────────────────────────────────────────────
27
+ if (args.length > 0 && args[0] === 'doctor') {
28
+ const { spawnSync } = await import('node:child_process');
29
+ const pathMod = await import('node:path');
30
+ const urlMod = await import('node:url');
31
+ const here = pathMod.dirname(urlMod.fileURLToPath(import.meta.url));
32
+ const script = pathMod.resolve(here, '..', 'plugin', 'skills', 'doctor', 'doctor.ps1');
33
+ const psArgs = ['-NoProfile', '-File', script];
34
+ if (args.includes('--json')) psArgs.push('-Json');
35
+ if (args.includes('--strict')) psArgs.push('-Strict');
36
+ const r = spawnSync('pwsh', psArgs, { stdio: 'inherit' });
37
+ process.exit(r.status ?? 1);
38
+ }
39
+
40
+ // ── setup-wizard flag (v5.4.0+) ─────────────────────────────────────────────
41
+ if (args.includes('--setup-wizard')) {
42
+ const { runSetupWizard } = await import('../src/setup-wizard.mjs');
43
+ await runSetupWizard({ args });
44
+ process.exit(0);
45
+ }
46
+
47
+ async function printWelcome() {
48
+ const pathMod = await import('node:path');
49
+ const urlMod = await import('node:url');
50
+ const fsMod = await import('node:fs');
51
+ const here = pathMod.dirname(urlMod.fileURLToPath(import.meta.url));
52
+ const repoRoot = pathMod.resolve(here, '..');
53
+ let version = 'unknown';
54
+ try { version = JSON.parse(fsMod.readFileSync(pathMod.join(repoRoot, 'package.json'), 'utf-8')).version; } catch {}
55
+ let skillCount = 0;
56
+ try {
57
+ const skillsDir = pathMod.join(repoRoot, 'plugin', 'skills');
58
+ skillCount = fsMod.readdirSync(skillsDir, { withFileTypes: true })
59
+ .filter((d) => d.isDirectory() && !d.name.startsWith('_'))
60
+ .length;
61
+ } catch {}
62
+ console.log(`
63
+ kushi v${version} — multi-source M365 project evidence agent
64
+
65
+ (non-interactive shell — nothing was installed)
66
+
67
+ First time? kushi doctor
68
+ Bootstrap a project: kushi setup <project>
69
+ Ask a question: kushi ask <project> "..."
70
+ Wizard install: npx kushi-agents --setup-wizard
71
+ Host install: npx kushi-agents --clawpilot | --vscode | --all-hosts
72
+
73
+ Docs: https://gim-home.github.io/kushi/
74
+ Skills: ${skillCount} installed in plugin/skills/
75
+
76
+ Run kushi help for the full command list.
77
+ `);
78
+ }
79
+
80
+ // ── skill-authoring verbs (v5.0.4+) ─────────────────────────────────────────
81
+ // Dispatch directly to the skill-creator / skill-checker pwsh scripts.
82
+ const SKILL_VERBS = new Set(['create-skill', 'check-skill', 'optimize-description', 'review-evals']);
83
+ if (args.length > 0 && SKILL_VERBS.has(args[0])) {
84
+ const verb = args[0];
85
+ const rest = args.slice(1);
86
+ await dispatchSkillVerb(verb, rest);
87
+ process.exit(0);
88
+ }
89
+
90
+ // ── lint verb (v5.1.0+) ──────────────────────────────────────────────────────
91
+ if (args.length > 0 && args[0] === 'lint') {
92
+ if (args.includes('--global')) {
93
+ const { runGlobalLint } = await import('../src/global-wiki-cli.mjs');
94
+ await runGlobalLint();
95
+ process.exit(0);
96
+ }
97
+ const project = args[1] || '';
98
+ if (!project) {
99
+ console.error('\n Usage: kushi lint <project>\n kushi lint --global\n');
100
+ process.exit(1);
101
+ }
102
+ await dispatchLint(project);
103
+ process.exit(0);
104
+ }
105
+
106
+ // ── one-shot wiki verb (v5.9.1+) ─────────────────────────────────────────────
107
+ // `kushi wiki` deterministic do-the-wiki: resolve root, init if missing,
108
+ // print path + status + clickable file:// URL. Idempotent across
109
+ // "create wiki" / "do wiki" / "refresh wiki" / "update wiki".
110
+ if (args.length > 0 && args[0] === 'wiki') {
111
+ const cliMod = await import('../src/global-wiki-cli.mjs');
112
+ await cliMod.runWiki();
113
+ process.exit(0);
114
+ }
115
+
116
+ // ── global verb (v5.3.0+) ────────────────────────────────────────────────────
117
+ if (args.length > 0 && args[0] === 'global') {
118
+ const sub = args[1] || '';
119
+ const validSubs = ['init', 'status', 'ask', 'lint', 'migrate', 'set-root', 'show-root'];
120
+ if (!validSubs.includes(sub)) {
121
+ console.error('\n Usage: kushi global init Scaffold State/ at the resolved root');
122
+ console.error(' kushi global status Show counts + freshness');
123
+ console.error(' kushi global ask <question> Ask the global wiki');
124
+ console.error(' kushi global lint Lint the global wiki');
125
+ console.error(' kushi global show-root Show how the root path is resolved');
126
+ console.error(' kushi global set-root <path> [--scope workspace|home]');
127
+ console.error(' Persist root (workspace shared by default,');
128
+ console.error(' falls back to ~/.kushi/config.json)');
129
+ console.error(' kushi global migrate <new-path> Copy wiki to a new root + persist setting\n');
130
+ console.error(' Tip: Set the root to a OneDrive-synced SharePoint folder to share');
131
+ console.error(' the wiki across a team. See docs/how-to/wiki-on-sharepoint.md.\n');
132
+ process.exit(1);
133
+ }
134
+ const cliMod = await import('../src/global-wiki-cli.mjs');
135
+ if (sub === 'init') await cliMod.runGlobalInit();
136
+ else if (sub === 'status') await cliMod.runGlobalStatus();
137
+ else if (sub === 'ask') await cliMod.runGlobalAsk(args.slice(2).join(' '));
138
+ else if (sub === 'lint') await cliMod.runGlobalLint();
139
+ else if (sub === 'migrate') await cliMod.runGlobalMigrate(args[2]);
140
+ else if (sub === 'set-root') {
141
+ const scopeIdx = args.indexOf('--scope');
142
+ const scope = scopeIdx > 0 && args[scopeIdx + 1] ? args[scopeIdx + 1] : null;
143
+ const target = args.find((a, i) => i >= 2 && !a.startsWith('--') && args[i - 1] !== '--scope');
144
+ await cliMod.runGlobalSetRoot(target, { scope });
145
+ }
146
+ else if (sub === 'show-root') await cliMod.runGlobalShowRoot();
147
+ process.exit(0);
148
+ }
149
+
150
+ // ── promote verb (v5.3.0+) ───────────────────────────────────────────────────
151
+ if (args.length > 0 && args[0] === 'promote') {
152
+ const project = args[1] || '';
153
+ const page = args[2] || '';
154
+ if (!project || !page) {
155
+ console.error('\n Usage: kushi promote <project> <page-path>\n');
156
+ console.error(' Copies a project State page into the global wiki with provenance metadata.');
157
+ console.error(' Refuses by default if customer identifiers are detected; pass --force after review.\n');
158
+ process.exit(1);
159
+ }
160
+ const force = args.includes('--force');
161
+ const { runPromote } = await import('../src/global-wiki-cli.mjs');
162
+ await runPromote(project, page, { force });
163
+ process.exit(0);
164
+ }
165
+
166
+ // ── hooks verb (v5.2.0+) ─────────────────────────────────────────────────────
167
+ if (args.length > 0 && args[0] === 'hooks') {
168
+ const sub = args[1] || '';
169
+ const project = args[2] || '';
170
+ if (!sub || !project || !['list', 'test'].includes(sub)) {
171
+ console.error('\n Usage: kushi hooks list <project>\n kushi hooks test <project> <event>\n');
172
+ process.exit(1);
173
+ }
174
+ await dispatchHooks(sub, project, args.slice(3));
175
+ process.exit(0);
176
+ }
177
+
178
+ // ── explain verb (v5.2.0+) ───────────────────────────────────────────────────
179
+ if (args.length > 0 && args[0] === 'explain') {
180
+ const topic = args.slice(1).join(' ');
181
+ if (!topic) {
182
+ console.error('\n Usage: kushi explain <topic>\n\n Available topics: contradictions, refresh, state, hooks, parallel, otel, csc, graph, workiq, schema, install, evals\n');
183
+ process.exit(1);
184
+ }
185
+ await dispatchExplain(topic);
186
+ process.exit(0);
187
+ }
188
+
189
+ // ── remember verb (v5.2.0+) ──────────────────────────────────────────────────
190
+ if (args.length > 0 && args[0] === 'remember') {
191
+ const rule = args.slice(1).join(' ');
192
+ if (!rule) {
193
+ console.error('\n Usage: kushi remember <rule>\n\n Example: kushi remember "always use Northwind not Healthcare Accelerator"\n');
194
+ process.exit(1);
195
+ }
196
+ await dispatchRemember(rule);
197
+ process.exit(0);
198
+ }
199
+
200
+ if (args.includes('--help') || args.includes('-h') || args.includes('-help') || args[0] === 'help') {
201
+ await printHelp();
202
+ process.exit(0);
203
+ }
204
+
205
+ async function printHelp() {
206
+ const pathMod = await import('node:path');
207
+ const urlMod = await import('node:url');
208
+ const fsMod = await import('node:fs');
209
+ const here = pathMod.dirname(urlMod.fileURLToPath(import.meta.url));
210
+ const repoRoot = pathMod.resolve(here, '..');
211
+ let version = 'unknown';
212
+ try { version = JSON.parse(fsMod.readFileSync(pathMod.join(repoRoot, 'package.json'), 'utf-8')).version; } catch {}
213
+ console.log(`
214
+ Usage: kushi [COMMAND] [ARGS...]
215
+ npx kushi-agents [INSTALL OPTIONS]
216
+
217
+ Kushi v${version} — multi-source M365 project evidence + Q&A agent.
218
+
219
+ Common Commands:
220
+ bootstrap <project> First-time setup for a project (full pull)
221
+ refresh <project> Incremental refresh + rebuild State/
222
+ ask <project> <question> Cited Q&A over Evidence/ (auto-routes; --file-back to save)
223
+ status <project> Show run-log for a project
224
+ wiki Resolve + scaffold + open the global wiki (one-shot)
225
+ doctor Aggregated health check (env, drift, evals, etc.)
226
+
227
+ Project Commands:
228
+ state <project> Re-render State/ from existing Evidence
229
+ references <project> Refresh shared references pool from Evidence URLs
230
+ consolidate <project> Merge per-user evidence into _Consolidated
231
+ lint <project> Run wiki-lint checks on State/
232
+ promote <project> <page> Move a project page into the global wiki (with redaction)
233
+ setup [<project>] Run the onboarding wizard / fill missing config
234
+
235
+ Global Wiki Commands:
236
+ global init Scaffold ~/.kushi-global/State/ (or KUSHI_GLOBAL_ROOT)
237
+ global status Page counts + freshness
238
+ global ask <question> Search the global wiki specifically
239
+ global lint Privacy + freshness scan
240
+ global show-root Show the 4-tier resolution chain
241
+ global set-root <path> Persist globalRoot (--scope workspace|home)
242
+ global migrate <new-path> Copy State/ to a new root + re-persist
243
+
244
+ Lifecycle Commands:
245
+ upgrade npm i -g kushi-agents@latest then re-seed assets in cwd
246
+ uninstall [--keep-config] Remove <cwd>/.kushi/ (preserves Evidence/, State/)
247
+
248
+ Authoring Commands:
249
+ create-skill <name> Scaffold a new plugin/skills/<name>/ tree
250
+ check-skill <name|--all> Lint a skill (or all skills) against the blueprint
251
+ optimize-description <s> Rewrite a skill's description per optimizer rules
252
+ review-evals <skill> Render an HTML eval-review viewer
253
+ explain <topic> Explain a kushi concept (read-only)
254
+ remember <rule> Persist a project convention to CLAUDE.md
255
+ hooks list|test <project> Inspect or fire hook events
256
+
257
+ Install (run via npx first-time / host install):
258
+ npx kushi-agents --clawpilot Install to ~/.copilot/m-skills/kushi/
259
+ npx kushi-agents --vscode Install to ~/.vscode/chat/skills/kushi/
260
+ npx kushi-agents --all-hosts Install to BOTH hosts
261
+ npx kushi-agents --setup-wizard Interactive first-run flow
262
+ npx kushi-agents --uninstall Cleanly remove host install + skills-metadata entry
263
+
264
+ Profile: --profile core | standard (default) | full
265
+ Other: --force, --yes, --no-workspace, --no-settings, --no-instructions, --no-prompt
266
+ WorkIQ: --with-workiq | --workiq-path <abs> | --skip-workiq-check
267
+
268
+ Run 'kushi <command> --help' for more information on a command (where supported).
269
+ Docs: https://gim-home.github.io/kushi/
270
+
271
+ In VS Code Chat the prefix is "@Kushi". In Clawpilot just say "kushi <verb>".
272
+ `);
273
+ }
274
+
275
+
276
+ // ── state / refresh / bootstrap verbs (v5.9.0+) ─────────────────────────────
277
+ // Thin shells that exec the deterministic runners. Keeps `kushi state HCA` etc.
278
+ // runnable from the global bin without users having to know the runner paths.
279
+ if (args.length > 0 && ['state', 'refresh-runner', 'bootstrap-runner', 'discover', 'references'].includes(args[0])) {
280
+ const verb = args[0];
281
+ const project = args[1];
282
+ if (!project) {
283
+ console.error(`\n Usage: kushi ${verb} <project> [options]\n`);
284
+ process.exit(1);
285
+ }
286
+ const { spawnSync } = await import('node:child_process');
287
+ const pathMod = await import('node:path');
288
+ const urlMod = await import('node:url');
289
+ const here = pathMod.dirname(urlMod.fileURLToPath(import.meta.url));
290
+ const runnerMap = {
291
+ state: 'pull-state.mjs',
292
+ references: 'pull-references.mjs',
293
+ discover: 'discover.mjs',
294
+ 'refresh-runner': 'refresh.mjs',
295
+ 'bootstrap-runner': 'bootstrap.mjs',
296
+ };
297
+ const runner = pathMod.resolve(here, '..', 'plugin', 'runners', runnerMap[verb]);
298
+ const passthrough = args.slice(2);
299
+ const r = spawnSync(process.execPath, [runner, '--project', project, ...passthrough], { stdio: 'inherit' });
300
+ process.exit(r.status ?? 1);
301
+ }
302
+
303
+ // ── workspace uninstall / upgrade verbs (v5.9.0+) ───────────────────────────
304
+ if (args.length > 0 && args[0] === 'uninstall' && !args.includes('--clawpilot') && !args.includes('--vscode') && !args.includes('--all-hosts')) {
305
+ // Workspace uninstall: remove .kushi/ from cwd (preserves Evidence/, State/).
306
+ const fsMod = await import('node:fs');
307
+ const pathMod = await import('node:path');
308
+ const dest = pathMod.resolve(process.cwd(), '.kushi');
309
+ const keepConfig = args.includes('--keep-config');
310
+ if (!fsMod.existsSync(dest)) {
311
+ console.error(`\n No .kushi/ directory found at ${dest}\n`);
312
+ process.exit(1);
313
+ }
314
+ if (keepConfig) {
315
+ const assetDirs = ['agents', 'instructions', 'prompts', 'skills', 'templates', 'reference-packs', 'lib', 'runners'];
316
+ let removed = 0;
317
+ for (const d of assetDirs) {
318
+ const p = pathMod.join(dest, d);
319
+ if (fsMod.existsSync(p)) { fsMod.rmSync(p, { recursive: true, force: true }); removed++; }
320
+ }
321
+ console.log(`\n Removed ${removed} asset dir(s) from ${dest} (config/user/ preserved).\n`);
322
+ } else {
323
+ fsMod.rmSync(dest, { recursive: true, force: true });
324
+ console.log(`\n Removed ${dest}\n Evidence/ and State/ left untouched.\n`);
325
+ }
326
+ process.exit(0);
327
+ }
328
+
329
+ if (args.length > 0 && args[0] === 'upgrade') {
330
+ // Upgrade: npm i -g @latest, then re-seed assets in cwd preserving config.
331
+ const { spawnSync } = await import('node:child_process');
332
+ console.log('\n Upgrading kushi-agents globally via npm...\n');
333
+ const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
334
+ const r1 = spawnSync(npm, ['install', '-g', 'kushi-agents@latest'], { stdio: 'inherit' });
335
+ if (r1.status !== 0) {
336
+ console.error('\n npm install failed.\n');
337
+ process.exit(r1.status ?? 1);
338
+ }
339
+ console.log('\n Refreshing assets in cwd (config preserved)...\n');
340
+ const fsMod = await import('node:fs');
341
+ if (fsMod.existsSync('.kushi')) {
342
+ const r2 = spawnSync(npm, ['exec', '--', 'kushi-agents', '--no-prompt', '--force'], { stdio: 'inherit' });
343
+ process.exit(r2.status ?? 0);
344
+ } else {
345
+ console.log('\n No .kushi/ in cwd — global upgrade complete; cd into a project and run `kushi` to install.\n');
346
+ process.exit(0);
347
+ }
348
+ }
349
+
350
+ // ── multi-host mode (v5.0.2+) ───────────────────────────────────────────────
351
+ // Trigger when the user passes any of: --vscode, --all-hosts, --uninstall.
352
+ // --clawpilot ALONE continues to route through the legacy main.mjs path so
353
+ // the existing target=clawpilot flow stays byte-identical.
354
+ const wantsVscode = args.includes('--vscode');
355
+ const wantsAllHosts = args.includes('--all-hosts');
356
+ const wantsUninstall = args.includes('--uninstall');
357
+
358
+ if (wantsVscode || wantsAllHosts || wantsUninstall) {
359
+ const hosts = [];
360
+ if (args.includes('--clawpilot')) hosts.push('clawpilot');
361
+ if (wantsVscode) hosts.push('vscode');
362
+ const all = wantsAllHosts || args.includes('--all');
363
+
364
+ runMultiHost({
365
+ hosts,
366
+ all,
367
+ uninstall: wantsUninstall,
368
+ profile: getFlag('--profile'),
369
+ includeWorkspace: !args.includes('--no-workspace'),
370
+ noPrompt: args.includes('--no-prompt'),
371
+ }).catch((err) => {
372
+ console.error(`\n ${err.message}\n`);
373
+ process.exit(1);
374
+ });
375
+ } else {
376
+ let target = getFlag('--target');
377
+ if (args.includes('--clawpilot')) {
378
+ if (target && target !== 'clawpilot') {
379
+ console.error(`\n Conflicting flags: --target ${target} and --clawpilot.\n`);
380
+ process.exit(1);
381
+ }
382
+ target = 'clawpilot';
383
+ }
384
+
385
+ const options = {
386
+ force: args.includes('--force'),
387
+ yes: args.includes('--yes') || args.includes('-y'),
388
+ noSettings: args.includes('--no-settings'),
389
+ noInstructions: args.includes('--no-instructions'),
390
+ noPrompt: args.includes('--no-prompt'),
391
+ target,
392
+ profile: getFlag('--profile'),
393
+ withWorkiq: args.includes('--with-workiq'),
394
+ workiqPath: getFlag('--workiq-path'),
395
+ skipWorkiqCheck: args.includes('--skip-workiq-check'),
396
+ };
397
+
398
+ main(options).catch((err) => {
399
+ console.error(`\n ${err.message}\n`);
400
+ process.exit(1);
401
+ });
402
+ }
403
+
404
+ function getFlag(flag) {
405
+ const idx = args.indexOf(flag);
406
+ if (idx !== -1 && idx + 1 < args.length) {
407
+ return args[idx + 1];
408
+ }
409
+ const prefix = flag + '=';
410
+ const match = args.find((a) => a.startsWith(prefix));
411
+ return match ? match.slice(prefix.length) : undefined;
412
+ }
413
+
414
+ // ── skill-authoring verb dispatch (v5.0.4+) ─────────────────────────────────
415
+ async function dispatchSkillVerb(verb, rest) {
416
+ const { spawnSync } = await import('node:child_process');
417
+ const path = await import('node:path');
418
+ const url = await import('node:url');
419
+ const here = path.dirname(url.fileURLToPath(import.meta.url));
420
+ const repoRoot = path.resolve(here, '..');
421
+ const creatorDir = path.join(repoRoot, 'plugin', 'skills', 'skill-creator');
422
+ const checkerDir = path.join(repoRoot, 'plugin', 'skills', 'skill-checker');
423
+
424
+ let script, scriptArgs = [];
425
+ switch (verb) {
426
+ case 'create-skill': {
427
+ // Usage: kushi create-skill <name> [--type <t>] [--description "<d>"] [--force]
428
+ const name = rest.find((a) => !a.startsWith('-'));
429
+ if (!name) {
430
+ console.error('Usage: kushi-agents create-skill <name> --type <pull|writer|orchestrator|other> --description "USE WHEN ... DO NOT USE FOR ..."');
431
+ process.exit(1);
432
+ }
433
+ const type = pickFlag(rest, '--type') || 'other';
434
+ const desc = pickFlag(rest, '--description') || `USE WHEN ${name} is invoked. DO NOT USE FOR unrelated tasks.`;
435
+ script = path.join(creatorDir, 'scaffold.ps1');
436
+ scriptArgs = ['-Name', name, '-Type', type, '-Description', desc];
437
+ if (rest.includes('--force')) scriptArgs.push('-Force');
438
+ if (rest.includes('--dry-run')) scriptArgs.push('-DryRun');
439
+ break;
440
+ }
441
+ case 'check-skill': {
442
+ // Usage: kushi check-skill <name> | --all [--retrofit] [--apply]
443
+ script = path.join(checkerDir, 'check-skill.ps1');
444
+ const allFlag = rest.includes('--all') || rest.includes('-All');
445
+ const name = rest.find((a) => !a.startsWith('-'));
446
+ if (allFlag) scriptArgs.push('-All');
447
+ else if (name) scriptArgs.push('-Skill', name);
448
+ else {
449
+ console.error('Usage: kushi-agents check-skill <name> | --all [--retrofit] [--apply] [--dry-run]');
450
+ process.exit(1);
451
+ }
452
+ if (rest.includes('--retrofit')) scriptArgs.push('-Retrofit');
453
+ if (rest.includes('--apply')) scriptArgs.push('-Apply');
454
+ if (rest.includes('--dry-run')) scriptArgs.push('-DryRun');
455
+ if (rest.includes('--json')) scriptArgs.push('-Json');
456
+ break;
457
+ }
458
+ case 'optimize-description': {
459
+ // Usage: kushi optimize-description <skill>
460
+ const name = rest.find((a) => !a.startsWith('-'));
461
+ if (!name) {
462
+ console.error('Usage: kushi-agents optimize-description <skill>');
463
+ process.exit(1);
464
+ }
465
+ script = path.join(checkerDir, 'check-skill.ps1');
466
+ scriptArgs = ['-Skill', name, '-OptimizeDescription'];
467
+ break;
468
+ }
469
+ case 'review-evals': {
470
+ // Usage: kushi review-evals <skill>
471
+ const name = rest.find((a) => !a.startsWith('-'));
472
+ if (!name) {
473
+ console.error('Usage: kushi-agents review-evals <skill>');
474
+ process.exit(1);
475
+ }
476
+ script = path.join(checkerDir, 'check-skill.ps1');
477
+ scriptArgs = ['-Skill', name, '-Review'];
478
+ break;
479
+ }
480
+ default:
481
+ console.error(`Unknown skill verb: ${verb}`);
482
+ process.exit(1);
483
+ }
484
+
485
+ const result = spawnSync('pwsh', ['-NoProfile', '-File', script, ...scriptArgs], { stdio: 'inherit' });
486
+ process.exit(result.status ?? 1);
487
+ }
488
+
489
+ async function dispatchLint(project) {
490
+ const { spawn } = await import('node:child_process');
491
+ const { resolve } = await import('node:path');
492
+ const { fileURLToPath } = await import('node:url');
493
+ const __dirname = fileURLToPath(new URL('.', import.meta.url));
494
+ const scriptPath = resolve(__dirname, '..', 'plugin', 'skills', 'lint-state', 'lint.ps1');
495
+
496
+ const cwd = process.cwd();
497
+ const { readdirSync, existsSync } = await import('node:fs');
498
+ let stateDir = '';
499
+
500
+ const evidenceDir = resolve(cwd, project, 'Evidence');
501
+ if (existsSync(evidenceDir)) {
502
+ const aliases = readdirSync(evidenceDir, { withFileTypes: true })
503
+ .filter(d => d.isDirectory() && !d.name.startsWith('_'));
504
+ for (const alias of aliases) {
505
+ const candidate = resolve(evidenceDir, alias.name, 'State');
506
+ if (existsSync(candidate)) { stateDir = candidate; break; }
507
+ }
508
+ }
509
+
510
+ if (!stateDir) {
511
+ const direct = resolve(cwd, project, 'State');
512
+ if (existsSync(direct)) { stateDir = direct; }
513
+ }
514
+
515
+ if (!stateDir) {
516
+ console.error(`\n Could not find State/ directory for project '${project}'.`);
517
+ console.error(` Looked in: ${evidenceDir}/*/State/ and ${resolve(cwd, project, 'State')}/`);
518
+ console.error(` Run 'kushi state ${project}' first to build State/.\n`);
519
+ process.exit(1);
520
+ }
521
+
522
+ const child = spawn('pwsh', ['-NoProfile', '-File', scriptPath, '-StateDir', stateDir], {
523
+ stdio: 'inherit',
524
+ cwd: resolve(__dirname, '..'),
525
+ });
526
+
527
+ return new Promise((res, rej) => {
528
+ child.on('close', (code) => {
529
+ if (code !== 0) rej(new Error(`lint-state exited with code ${code}`));
530
+ else res();
531
+ });
532
+ child.on('error', rej);
533
+ });
534
+ }
535
+
536
+ function pickFlag(args, flag) {
537
+ const idx = args.indexOf(flag);
538
+ if (idx !== -1 && idx + 1 < args.length) return args[idx + 1];
539
+ const prefix = flag + '=';
540
+ const m = args.find((a) => a.startsWith(prefix));
541
+ return m ? m.slice(prefix.length) : undefined;
542
+ }
543
+
544
+ // ── v5.2.0 dispatch helpers ──────────────────────────────────────────────────
545
+
546
+ async function dispatchHooks(sub, project, extra) {
547
+ const { spawn } = await import('node:child_process');
548
+ const { resolve } = await import('node:path');
549
+ const { fileURLToPath } = await import('node:url');
550
+ const __dirname = fileURLToPath(new URL('.', import.meta.url));
551
+ const invokeHooks = resolve(__dirname, '..', 'plugin', 'skills', '_shared', 'Invoke-Hooks.ps1');
552
+
553
+ if (sub === 'list') {
554
+ // List hooks from .kushi/hooks.yml
555
+ const { existsSync, readFileSync } = await import('node:fs');
556
+ const hooksYml = resolve(process.cwd(), project, '.kushi', 'hooks.yml');
557
+ if (!existsSync(hooksYml)) {
558
+ console.log(`\n No hooks configured for '${project}'. Create ${hooksYml} to add hooks.\n`);
559
+ return;
560
+ }
561
+ console.log(`\n Hooks for '${project}' (${hooksYml}):\n`);
562
+ console.log(readFileSync(hooksYml, 'utf8'));
563
+ } else if (sub === 'test') {
564
+ const event = extra[0] || 'post-pull';
565
+ const script = `
566
+ $payload = @{ project = '${project}'; source = 'test'; success = $true; duration_ms = 0; event = '${event}' }
567
+ & '${invokeHooks.replace(/\\/g, '\\\\')}' -ProjectRoot '${resolve(process.cwd(), project).replace(/\\/g, '\\\\')}' -Event '${event}' -Payload $payload
568
+ `;
569
+ const child = spawn('pwsh', ['-NoProfile', '-Command', script], { stdio: 'inherit' });
570
+ return new Promise((res, rej) => {
571
+ child.on('close', (code) => { if (code !== 0) rej(new Error(`hooks test exited ${code}`)); else res(); });
572
+ child.on('error', rej);
573
+ });
574
+ }
575
+ }
576
+
577
+ async function dispatchExplain(topic) {
578
+ const { resolve } = await import('node:path');
579
+ const { existsSync, readFileSync } = await import('node:fs');
580
+ const { fileURLToPath } = await import('node:url');
581
+ const __dirname = fileURLToPath(new URL('.', import.meta.url));
582
+ const repoRoot = resolve(__dirname, '..');
583
+ const instructionsDir = resolve(repoRoot, 'plugin', 'instructions');
584
+
585
+ const topicMap = {
586
+ contradictions: 'living-wiki.instructions.md',
587
+ conflicts: 'living-wiki.instructions.md',
588
+ refresh: 'parallel-execution.instructions.md',
589
+ pull: 'parallel-execution.instructions.md',
590
+ state: 'living-wiki.instructions.md',
591
+ 'build-state': 'living-wiki.instructions.md',
592
+ wiki: 'living-wiki.instructions.md',
593
+ hooks: 'hooks.instructions.md',
594
+ events: 'hooks.instructions.md',
595
+ webhooks: 'hooks.instructions.md',
596
+ parallel: 'parallel-execution.instructions.md',
597
+ workers: 'parallel-execution.instructions.md',
598
+ otel: 'otel.instructions.md',
599
+ telemetry: 'otel.instructions.md',
600
+ tracing: 'otel.instructions.md',
601
+ csc: 'comprehensive-structured-capture.instructions.md',
602
+ capture: 'comprehensive-structured-capture.instructions.md',
603
+ graph: 'entity-graph.instructions.md',
604
+ entities: 'entity-graph.instructions.md',
605
+ workiq: 'workiq-only.instructions.md',
606
+ schema: 'schema-evolve.instructions.md',
607
+ conventions: 'schema-evolve.instructions.md',
608
+ remember: 'schema-evolve.instructions.md',
609
+ install: 'multi-host-install.instructions.md',
610
+ setup: 'multi-host-install.instructions.md',
611
+ evals: 'skill-evals.instructions.md',
612
+ };
613
+
614
+ const key = Object.keys(topicMap).find(k => topic.toLowerCase().includes(k));
615
+ if (!key) {
616
+ console.log(`\n Topic not found: "${topic}"\n`);
617
+ console.log(' Available topics:', Object.keys(topicMap).filter((v, i, a) => a.indexOf(v) === i).join(', '));
618
+ console.log('');
619
+ return;
620
+ }
621
+
622
+ const docFile = resolve(instructionsDir, topicMap[key]);
623
+ if (!existsSync(docFile)) {
624
+ console.error(` Doctrine file missing: ${topicMap[key]}`);
625
+ process.exit(1);
626
+ }
627
+
628
+ const content = readFileSync(docFile, 'utf8');
629
+ const lines = content.split('\n').slice(0, 40);
630
+ console.log(`\n 📖 Topic: ${key} → ${topicMap[key]}\n`);
631
+ console.log(lines.join('\n'));
632
+ console.log(`\n ... (full doctrine at: plugin/instructions/${topicMap[key]})\n`);
633
+ }
634
+
635
+ async function dispatchRemember(rule) {
636
+ console.log(`\n Rule noted: "${rule}"`);
637
+ console.log(' To persist this rule, run schema-evolve from within a project context:');
638
+ console.log(' @Kushi remember ' + rule);
639
+ console.log(' This will write to Evidence/<alias>/State/CLAUDE.md\n');
640
+ }