refacil-sdd-ai 5.2.3 → 5.3.1

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 (76) hide show
  1. package/NOTICE.md +46 -0
  2. package/README.md +210 -42
  3. package/agents/auditor.md +46 -0
  4. package/agents/debugger.md +41 -1
  5. package/agents/implementer.md +76 -10
  6. package/agents/investigator.md +36 -0
  7. package/agents/proposer.md +56 -2
  8. package/agents/tester.md +45 -8
  9. package/agents/validator.md +67 -13
  10. package/bin/cli.js +396 -84
  11. package/lib/bus/broker.js +121 -3
  12. package/lib/bus/spawn.js +189 -121
  13. package/lib/check-review.js +102 -0
  14. package/lib/codegraph-telemetry.js +135 -0
  15. package/lib/codegraph.js +273 -0
  16. package/lib/commands/autopilot.js +120 -0
  17. package/lib/commands/bus.js +29 -36
  18. package/lib/commands/compact.js +185 -46
  19. package/lib/commands/read-spec.js +352 -0
  20. package/lib/commands/sdd.js +600 -47
  21. package/lib/compact-guidance.js +122 -77
  22. package/lib/config.js +136 -0
  23. package/lib/global-paths.js +56 -20
  24. package/lib/hooks.js +26 -4
  25. package/lib/ide-detection.js +1 -1
  26. package/lib/ignore-files.js +5 -1
  27. package/lib/installer.js +196 -19
  28. package/lib/kapso.js +308 -0
  29. package/lib/methodology-migration-pending.js +13 -0
  30. package/lib/open-browser.js +32 -0
  31. package/lib/opencode-migrate.js +148 -0
  32. package/lib/opencode-plugin/index.js +84 -104
  33. package/lib/opencode-plugin/rules.js +236 -0
  34. package/lib/project-root.js +154 -0
  35. package/lib/repo-ide-sync.js +5 -0
  36. package/lib/spec-reader/lang.js +72 -0
  37. package/lib/spec-reader/md-parser.js +299 -0
  38. package/lib/spec-reader/session.js +139 -0
  39. package/lib/spec-reader/ui/app.js +685 -0
  40. package/lib/spec-reader/ui/index.html +59 -0
  41. package/lib/spec-reader/ui/mixed-lang.js +200 -0
  42. package/lib/spec-reader/ui/model-cache.js +117 -0
  43. package/lib/spec-reader/ui/style.css +294 -0
  44. package/lib/spec-reader/ui/supertonic-helper.js +565 -0
  45. package/lib/spec-sync.js +258 -0
  46. package/lib/test-scope.js +713 -0
  47. package/lib/testing-policy-sync.js +14 -2
  48. package/package.json +5 -3
  49. package/skills/apply/SKILL.md +50 -65
  50. package/skills/archive/SKILL.md +84 -50
  51. package/skills/ask/SKILL.md +43 -8
  52. package/skills/autopilot/SKILL.md +505 -0
  53. package/skills/bug/SKILL.md +52 -53
  54. package/skills/explore/SKILL.md +48 -1
  55. package/skills/guide/SKILL.md +35 -13
  56. package/skills/inbox/SKILL.md +9 -0
  57. package/skills/join/SKILL.md +1 -1
  58. package/skills/prereqs/BUS-CROSS-REPO.md +33 -16
  59. package/skills/prereqs/METHODOLOGY-CONTRACT.md +96 -17
  60. package/skills/prereqs/SKILL.md +1 -1
  61. package/skills/propose/SKILL.md +82 -19
  62. package/skills/read-spec/SKILL.md +76 -0
  63. package/skills/reply/SKILL.md +42 -9
  64. package/skills/review/SKILL.md +71 -25
  65. package/skills/review/checklist.md +2 -2
  66. package/skills/say/SKILL.md +40 -4
  67. package/skills/setup/SKILL.md +59 -5
  68. package/skills/setup/troubleshooting.md +11 -3
  69. package/skills/stats/SKILL.md +160 -0
  70. package/skills/status/SKILL.md +116 -0
  71. package/skills/test/SKILL.md +38 -11
  72. package/skills/up-code/SKILL.md +20 -13
  73. package/skills/update/SKILL.md +32 -1
  74. package/skills/verify/SKILL.md +85 -40
  75. package/templates/compact-guidance.md +10 -0
  76. package/templates/methodology-guide.md +5 -0
package/bin/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  'use strict';
4
4
 
@@ -16,6 +16,7 @@ const {
16
16
  removeSkills,
17
17
  removeGlobalSkills,
18
18
  removeOpenCodeArtifacts,
19
+ removeOpenCodeGlobalArtifacts,
19
20
  removeCodexArtifacts,
20
21
  removeOpenspecLegacyAssets,
21
22
  removeProjectLevelArtifacts,
@@ -28,16 +29,22 @@ const {
28
29
  getPackageVersion,
29
30
  checkNodeVersion,
30
31
  checkClaudeCodeVersion,
32
+ promptCodegraphMode,
31
33
  } = require('../lib/installer');
32
34
  const { installHooks, uninstallHooks, cleanLegacySettingsHooks, installOpenCodePlugin, uninstallOpenCodePlugin, removeProjectLevelHooks } = require('../lib/hooks');
33
35
  const { globalClaudeDir, globalCursorDir, globalOpenCodeDir, globalCodexDir, readSelectedIDEs, writeSelectedIDEs } = require('../lib/global-paths');
34
36
  const { detectInstalledIDEs } = require('../lib/ide-detection');
35
37
  const { handleCompact } = require('../lib/commands/compact');
36
38
  const { handleBus } = require('../lib/commands/bus');
39
+ const { handleReadSpec } = require('../lib/commands/read-spec');
37
40
  const { handleSdd, autoMigrateOpenspec, findProjectRoot, cmdWriteConfig } = require('../lib/commands/sdd');
41
+ const { handleAutopilot } = require('../lib/commands/autopilot');
42
+ const { resolveWorkspaceRoot } = require('../lib/project-root');
43
+ const { evaluateGitPushReview } = require('../lib/check-review');
38
44
  const { syncIgnoreFiles } = require('../lib/ignore-files');
39
45
  const { methodologyMigrationPending } = require('../lib/methodology-migration-pending');
40
46
  const { resolveSelectedIDEsForRepo, syncRepoIdeFiles } = require('../lib/repo-ide-sync');
47
+ const codegraph = require('../lib/codegraph');
41
48
 
42
49
  const packageRoot = path.resolve(__dirname, '..');
43
50
  const projectRoot = findProjectRoot();
@@ -102,8 +109,15 @@ function notifyUpdate() {
102
109
  if (!info.shown) {
103
110
  // First time: block so the user sees it clearly
104
111
  try { fs.writeFileSync(flagPath, JSON.stringify({ ...info, shown: true })); } catch (_) {}
112
+ const isSameVersion = info.from && info.to && info.from === info.to;
113
+ const header = isSameVersion
114
+ ? '[refacil-sdd-ai] Pending SDD-AI configuration detected.'
115
+ : `[refacil-sdd-ai] Methodology update detected (${fromLabel} → v${info.to}).`;
116
+ const reasonLines = mig.reasons.length
117
+ ? '\nPending:\n' + mig.reasons.map((r) => ` • ${r}`).join('\n')
118
+ : '';
105
119
  const userMsg =
106
- `[refacil-sdd-ai] Methodology update detected (${fromLabel} → v${info.to}).\n` +
120
+ `${header}${reasonLines}\n` +
107
121
  `Your message was paused. Do you want to apply the pending migrations before continuing?\n` +
108
122
  ` • Yes: run /refacil:update\n` +
109
123
  ` • No: resend your previous message to continue without updating.`;
@@ -118,15 +132,16 @@ function notifyUpdate() {
118
132
  // Second time: let through and notify the LLM (only Claude Code supports context injection)
119
133
  try { fs.unlinkSync(flagPath); } catch (_) {}
120
134
  if (!isCursor) {
135
+ const reasonSummary = mig.reasons.length ? ` Pending: ${mig.reasons.join('; ')}` : '';
121
136
  console.log(
122
- `[refacil-sdd-ai] IMPORTANT: there is a pending SDD-AI methodology update (${fromLabel} → v${info.to}). ` +
137
+ `[refacil-sdd-ai] IMPORTANT: there is a pending SDD-AI methodology update (${fromLabel} → v${info.to}).${reasonSummary} ` +
123
138
  `Before responding to the user, ask them if they want to run /refacil:update now.`,
124
139
  );
125
140
  }
126
141
  }
127
142
  }
128
143
 
129
- function repoIsInitialized() {
144
+ function repoIsInitialized(repoRoot = projectRoot) {
130
145
  const home = os.homedir();
131
146
  // Check global dirs first (new installation model)
132
147
  if (
@@ -139,9 +154,9 @@ function repoIsInitialized() {
139
154
  }
140
155
  // Fallback: check legacy project-level dirs for backward compat
141
156
  return (
142
- fs.existsSync(path.join(projectRoot, '.claude', 'skills')) ||
143
- fs.existsSync(path.join(projectRoot, '.cursor', 'skills')) ||
144
- fs.existsSync(path.join(projectRoot, '.opencode', 'skills'))
157
+ fs.existsSync(path.join(repoRoot, '.claude', 'skills')) ||
158
+ fs.existsSync(path.join(repoRoot, '.cursor', 'skills')) ||
159
+ fs.existsSync(path.join(repoRoot, '.opencode', 'skills'))
145
160
  );
146
161
  }
147
162
 
@@ -274,10 +289,10 @@ function semverGt(a, b) {
274
289
  return false;
275
290
  }
276
291
 
277
- function syncRepoSkillsIfStale(globalVersion) {
278
- if (!repoIsInitialized()) return null;
292
+ function syncRepoSkillsIfStale(globalVersion, repoRoot = projectRoot) {
293
+ if (!repoIsInitialized(repoRoot)) return null;
279
294
  // Repo-level version takes priority; fall back to global store
280
- const repoVersion = readRepoVersion(projectRoot) || readGlobalVersion(os.homedir(), projectRoot);
295
+ const repoVersion = readRepoVersion(repoRoot) || readGlobalVersion(os.homedir(), repoRoot);
281
296
  if (repoVersion === globalVersion) return null;
282
297
 
283
298
  // Repo has newer skills than the installed package — do not downgrade
@@ -319,17 +334,126 @@ function migrationPendingCmd() {
319
334
  process.exit(pending ? 1 : 0);
320
335
  }
321
336
 
337
+ /**
338
+ * Detect whether any active refacil flow is in progress (any .review-passed, proposal.md, etc.).
339
+ * Returns true if the user is mid-flow so we don't interrupt with the CodeGraph suggestion.
340
+ */
341
+ function isActiveRefacilFlow() {
342
+ try {
343
+ const changesDir = path.join(projectRoot, 'refacil-sdd', 'changes');
344
+ if (!fs.existsSync(changesDir)) return false;
345
+ const entries = fs.readdirSync(changesDir, { withFileTypes: true });
346
+ return entries.some(
347
+ (e) => e.isDirectory() && e.name !== 'archive' &&
348
+ fs.existsSync(path.join(changesDir, e.name, 'proposal.md')),
349
+ );
350
+ } catch (_) {
351
+ return false;
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Inject a CodeGraph suggestion block if conditions are met:
357
+ * 1. codegraphMode is null/undefined (no preference set yet)
358
+ * 2. No active refacil flow detected
359
+ * 3. Suggestion has not been permanently suppressed (codegraph-suggest-shown: true)
360
+ * 4. Snooze period has not expired
361
+ */
362
+ function maybeInjectCodegraphSuggestion() {
363
+ try {
364
+ const { readConfigFile, extractCodegraphMode, extractCodegraphSuggestState, writeConfigValue } = require('../lib/config');
365
+ const globalConfigPath = path.join(os.homedir(), '.refacil-sdd-ai', 'config.yaml');
366
+ const globalCfg = readConfigFile(globalConfigPath) || {};
367
+
368
+ const codegraphMode = extractCodegraphMode(globalCfg, 'global');
369
+ if (codegraphMode !== null) return; // Already answered — do not suggest again
370
+
371
+ const { shown, snoozeUntil } = extractCodegraphSuggestState(globalCfg, 'global');
372
+ if (shown === true) return; // Permanently suppressed
373
+
374
+ if (snoozeUntil) {
375
+ try {
376
+ const snoozeDate = new Date(snoozeUntil);
377
+ if (!isNaN(snoozeDate.getTime()) && new Date() < snoozeDate) return; // Still snoozed
378
+ } catch (_) {}
379
+ }
380
+
381
+ if (isActiveRefacilFlow()) return; // Mid-flow — do not interrupt
382
+
383
+ const msg =
384
+ '[refacil-sdd-ai] CodeGraph is available — an optional tool that reduces token usage ~71% ' +
385
+ 'in exploratory sub-agents (investigate, propose, debug) by querying the call graph instead of reading files.\n' +
386
+ 'https://github.com/colbymchenry/codegraph (MIT, Colby McHenry)\n\n' +
387
+ 'Would you like to enable CodeGraph integration?\n' +
388
+ ' 1) yes-now — enable and index this repo immediately\n' +
389
+ ' 2) yes-later — enable globally (index repos on /refacil:setup)\n' +
390
+ ' 3) no-7d — remind me in 7 days\n' +
391
+ ' 4) never — disable permanently\n\n' +
392
+ 'Type a number (1-4) and press Enter, or press Enter to skip: ';
393
+
394
+ process.stdout.write(msg);
395
+
396
+ // Only read stdin when running in a real TTY — in hook context stdin is a pipe
397
+ // and fs.readFileSync(0) would block indefinitely waiting for data or EOF.
398
+ if (!process.stdin.isTTY) return;
399
+
400
+ let response = '';
401
+ try {
402
+ const raw = fs.readFileSync(0, { encoding: 'utf8', flag: 'r' });
403
+ response = raw.trim().toLowerCase();
404
+ } catch (_) {
405
+ return; // stdin not readable in this context — skip
406
+ }
407
+
408
+ if (response === '1' || response === 'yes-now') {
409
+ writeConfigValue('codegraphMode', 'enabled', os.homedir());
410
+ writeConfigValue('codegraph-suggest-shown', 'true', os.homedir());
411
+ const selectedIDEs = readSelectedIDEs() || ['.claude', '.cursor', '.opencode', '.codex'];
412
+ codegraph.registerMcp(selectedIDEs);
413
+ if (codegraph.isInstalled()) {
414
+ codegraph.init(projectRoot);
415
+ process.stdout.write('[refacil-sdd-ai] CodeGraph indexing started in background.\n');
416
+ } else {
417
+ process.stdout.write(
418
+ '[refacil-sdd-ai] CodeGraph mode set to enabled. ' +
419
+ 'Install @colbymchenry/codegraph to activate: npm install -g @colbymchenry/codegraph\n',
420
+ );
421
+ }
422
+ } else if (response === '2' || response === 'yes-later') {
423
+ writeConfigValue('codegraphMode', 'enabled', os.homedir());
424
+ writeConfigValue('codegraph-suggest-shown', 'true', os.homedir());
425
+ process.stdout.write('[refacil-sdd-ai] CodeGraph mode set to enabled. Repos will be indexed on /refacil:setup.\n');
426
+ } else if (response === '3' || response === 'no-7d') {
427
+ const snoozeDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString();
428
+ writeConfigValue('codegraph-suggest-snooze', snoozeDate, os.homedir());
429
+ process.stdout.write('[refacil-sdd-ai] CodeGraph suggestion snoozed for 7 days.\n');
430
+ } else if (response === '4' || response === 'never') {
431
+ writeConfigValue('codegraphMode', 'disabled', os.homedir());
432
+ writeConfigValue('codegraph-suggest-shown', 'true', os.homedir());
433
+ process.stdout.write(
434
+ '[refacil-sdd-ai] CodeGraph disabled. Re-enable with: ' +
435
+ 'refacil-sdd-ai sdd config --set codegraph enabled\n',
436
+ );
437
+ }
438
+ // Empty/unrecognised response — skip silently
439
+ } catch (_) {
440
+ // Suggestion injection must never break session startup
441
+ }
442
+ }
443
+
322
444
  function checkUpdate() {
323
- clearStalePendingUpdateFlag(projectRoot);
445
+ const root = resolveWorkspaceRoot();
446
+
447
+ clearStalePendingUpdateFlag(root);
324
448
 
325
449
  try {
326
- autoMigrateOpenspec(projectRoot);
450
+ autoMigrateOpenspec(root);
327
451
  } catch (err) {
328
452
  process.stderr.write(`[refacil-sdd-ai] Could not migrate openspec/ to refacil-sdd/: ${err.message}\n`);
329
453
  }
330
454
 
331
455
  try {
332
- const legacyRemoved = removeOpenspecLegacyAssets(projectRoot);
456
+ const legacyRemoved = removeOpenspecLegacyAssets(root);
333
457
  if (legacyRemoved > 0) {
334
458
  process.stderr.write(`[refacil-sdd-ai] Removed ${legacyRemoved} legacy OpenSpec assets (openspec-* skills, opsx-* commands)\n`);
335
459
  }
@@ -347,8 +471,8 @@ function checkUpdate() {
347
471
  fs.existsSync(path.join(globalCodexDir(home), 'skills'));
348
472
 
349
473
  if (globalActive) {
350
- const cleaned = removeProjectLevelArtifacts(projectRoot);
351
- removeProjectLevelHooks(projectRoot);
474
+ const cleaned = removeProjectLevelArtifacts(root);
475
+ removeProjectLevelHooks(root);
352
476
  if (cleaned > 0) {
353
477
  process.stdout.write(`[refacil-sdd-ai] Cleaned up ${cleaned} project-level artifact(s) — global installation is active.\n`);
354
478
  }
@@ -361,7 +485,7 @@ function checkUpdate() {
361
485
  let localVersion = getPackageVersion(packageRoot);
362
486
 
363
487
  try {
364
- const syncOut = syncRepoSessionMarkers(projectRoot, packageRoot);
488
+ const syncOut = syncRepoSessionMarkers(root, packageRoot);
365
489
  if (!syncOut.ok) {
366
490
  process.stderr.write(`[refacil-sdd-ai] session repo sync: ${syncOut.reason}\n`);
367
491
  } else {
@@ -380,7 +504,7 @@ function checkUpdate() {
380
504
  process.stderr.write(`[refacil-sdd-ai] session repo sync: ${err.message}\n`);
381
505
  }
382
506
 
383
- cleanLegacySettingsHooks(projectRoot);
507
+ cleanLegacySettingsHooks(root);
384
508
 
385
509
  // Self-healing: if selected-ides.json exists but global skills/hooks are missing, restore them
386
510
  // Covers the case where the Claude Code desktop app overwrites settings.json and wipes SDD hooks,
@@ -407,7 +531,7 @@ function checkUpdate() {
407
531
  // Always verify hooks are present for all selected IDEs — idempotent, only writes if missing
408
532
  for (const ide of ['.claude', '.cursor', '.opencode', '.codex']) {
409
533
  if (sddSelectedIDEs.includes(ide)) {
410
- installHooks(ide, home, projectRoot);
534
+ installHooks(ide, home, root);
411
535
  }
412
536
  }
413
537
  }
@@ -439,15 +563,15 @@ function checkUpdate() {
439
563
  }
440
564
 
441
565
  // Step 2: sync repo skills with the (now updated) package
442
- const syncResult = syncRepoSkillsIfStale(localVersion);
566
+ const syncResult = syncRepoSkillsIfStale(localVersion, root);
443
567
  if (syncResult && !syncResult.failed) {
444
568
  const fromLabel = syncResult.from ? `v${syncResult.from}` : 'unknown version';
445
569
  console.log(
446
570
  `[refacil-sdd-ai] Repo skills synced (${fromLabel} -> v${syncResult.to}). ` +
447
- 'Restart the Claude Code or Cursor session to pick up the changes.',
571
+ 'Restart your IDE session to pick up the changes.',
448
572
  );
449
- if (methodologyMigrationPending(projectRoot).pending) {
450
- writePendingUpdateFlag(projectRoot, syncResult.from, syncResult.to);
573
+ if (methodologyMigrationPending(root).pending) {
574
+ writePendingUpdateFlag(root, syncResult.from, syncResult.to);
451
575
  }
452
576
  } else if (syncResult && syncResult.failed) {
453
577
  console.log(
@@ -455,6 +579,40 @@ function checkUpdate() {
455
579
  'but automatic sync failed. Run manually: refacil-sdd-ai update',
456
580
  );
457
581
  }
582
+
583
+ // Inject CodeGraph suggestion if the user has not set a preference yet
584
+ maybeInjectCodegraphSuggestion();
585
+
586
+ // CodeGraph auto-refresh — silent background operation, no user interaction
587
+ try {
588
+ const { loadBranchConfigWithSources } = require('../lib/config');
589
+ const cfgInfo = loadBranchConfigWithSources(root);
590
+ const cgMode = cfgInfo.codegraphMode;
591
+ if (cgMode === 'enabled' || cgMode === 'per-repo') {
592
+ if (!codegraph.isInstalled()) {
593
+ // CLI missing: surface via notify-update banner (blocking once)
594
+ writePendingUpdateFlag(root, localVersion, localVersion);
595
+ process.stdout.write(
596
+ '[refacil-sdd-ai] CodeGraph is enabled but the CLI is not installed. ' +
597
+ 'Run /refacil:update to install and configure it.\n',
598
+ );
599
+ } else if (!codegraph.isInitialized(root)) {
600
+ // Not indexed: start automatically in the background, no question needed
601
+ codegraph.init(root);
602
+ process.stdout.write('[refacil-sdd-ai] CodeGraph: building index in background (~30s).\n');
603
+ } else if (codegraph.isStale(root)) {
604
+ // Index exists but new commits since last init: refresh silently
605
+ codegraph.init(root);
606
+ process.stdout.write('[refacil-sdd-ai] CodeGraph: refreshing index (new commits detected).\n');
607
+ } else {
608
+ // Initialized and up to date. If the timestamp file is absent (index was built
609
+ // externally, not through our tools), write it now so isStale() has a reference.
610
+ codegraph.touchTimestamp(root);
611
+ }
612
+ }
613
+ } catch (_) {
614
+ // Tolerant — CodeGraph state check must never break session startup
615
+ }
458
616
  }
459
617
 
460
618
  // --- Check review (PreToolUse hook) ---
@@ -469,38 +627,8 @@ function checkReview() {
469
627
  }
470
628
 
471
629
  const command = (input.tool_input && input.tool_input.command) || '';
472
- if (!command.match(/git\s+push/)) return;
473
-
474
- const sddChangesDir = path.join(projectRoot, 'refacil-sdd', 'changes');
475
- const legacyChangesDir = path.join(projectRoot, 'openspec', 'changes');
476
- const changesDir = fs.existsSync(sddChangesDir) ? sddChangesDir : legacyChangesDir;
477
- if (!fs.existsSync(changesDir)) return;
478
-
479
- const entries = fs.readdirSync(changesDir, { withFileTypes: true });
480
- const activeChanges = entries.filter(
481
- (e) => e.isDirectory() && e.name !== 'archive',
482
- );
483
-
484
- if (activeChanges.length === 0) return;
485
-
486
- const missing = activeChanges.filter(
487
- (e) => !fs.existsSync(path.join(changesDir, e.name, '.review-passed')),
488
- );
489
-
490
- if (missing.length > 0) {
491
- const names = missing.map((e) => e.name).join(', ');
492
- const reason =
493
- missing.length === 1
494
- ? `[refacil-sdd-ai] Review pending for: ${names}. ` +
495
- 'Stop the push and run /refacil:review on that change before pushing code. ' +
496
- 'If the review passes, retry the git push. ' +
497
- 'If the review requires corrections, report the findings to the user and DO NOT retry the push.'
498
- : `[refacil-sdd-ai] Multiple changes without approved review: ${names}. ` +
499
- 'Stop the push and ask the user to explicitly select which change they want to push. ' +
500
- 'Then run /refacil:review <change-name> for that specific change and retry the push. ' +
501
- 'Do not run automatic review without explicit selection when there is more than one pending change.';
502
- console.log(JSON.stringify({ decision: 'block', reason }));
503
- }
630
+ const block = evaluateGitPushReview(command, resolveWorkspaceRoot({ hookInput: input, skipStdin: true }));
631
+ if (block) console.log(JSON.stringify(block));
504
632
  }
505
633
 
506
634
  // --- Branch config prompt (used by init) ---
@@ -657,6 +785,9 @@ async function init() {
657
785
  // Prompt for global branch configuration (skipped with --yes/--defaults or non-TTY)
658
786
  await promptBranchConfig();
659
787
 
788
+ // Prompt for CodeGraph integration mode (skipped with --yes/--defaults or non-TTY)
789
+ await promptCodegraphMode(os.homedir());
790
+
660
791
  if (selectedIDEs.length === 0) {
661
792
  console.log('\n No IDEs selected. Nothing installed.\n');
662
793
  console.log(' Re-run with: refacil-sdd-ai init --all to install for all IDEs');
@@ -895,6 +1026,14 @@ function update() {
895
1026
  console.error(` Warning: session repo sync: ${err.message}`);
896
1027
  }
897
1028
 
1029
+ try {
1030
+ const { loadBranchConfigWithSources } = require('../lib/config');
1031
+ const cfgInfo = loadBranchConfigWithSources(projectRoot);
1032
+ if (cfgInfo.codegraphMode && cfgInfo.codegraphMode !== 'disabled') {
1033
+ codegraph.registerMcp(selectedIDEs, homeDir);
1034
+ }
1035
+ } catch (_) {}
1036
+
898
1037
  console.log('\n RESTART your IDE session to apply the changes.\n');
899
1038
  }
900
1039
 
@@ -966,8 +1105,10 @@ function clean() {
966
1105
 
967
1106
  const homeDir = os.homedir();
968
1107
 
969
- const selectedIDEs = readSelectedIDEs() || ['claude', 'cursor', 'opencode'];
970
- const globalCount = removeGlobalSkills(homeDir, selectedIDEs);
1108
+ const selectedIDEs = resolveSelectedIDEsForRepo(projectRoot, homeDir);
1109
+ const fallbackIDEs = ['.claude', '.cursor', '.opencode', '.codex'];
1110
+ const ideDirsForClean = selectedIDEs.length > 0 ? selectedIDEs : fallbackIDEs;
1111
+ const globalCount = removeGlobalSkills(homeDir, ideDirsForClean);
971
1112
  if (globalCount > 0) {
972
1113
  console.log(` ${globalCount} global skills removed from IDE user directories`);
973
1114
  }
@@ -977,33 +1118,42 @@ function clean() {
977
1118
  console.log(` ${count} project-level skills removed from .claude/skills/ and .cursor/skills/`);
978
1119
  }
979
1120
 
980
- if (uninstallHooks('.claude', homeDir)) {
981
- console.log(' SDD-AI hooks removed from ~/.claude/settings.json');
982
- } else {
983
- console.log(' No SDD-AI hooks found to remove in ~/.claude/settings.json.');
1121
+ if (ideDirsForClean.includes('.claude') || ideDirsForClean.includes('claude')) {
1122
+ if (uninstallHooks('.claude', homeDir)) {
1123
+ console.log(' SDD-AI hooks removed from ~/.claude/settings.json');
1124
+ } else {
1125
+ console.log(' No SDD-AI hooks found to remove in ~/.claude/settings.json.');
1126
+ }
984
1127
  }
985
- if (uninstallHooks('.cursor', homeDir)) {
986
- console.log(' SDD-AI hooks removed from ~/.cursor/hooks.json');
1128
+ if (ideDirsForClean.includes('.cursor') || ideDirsForClean.includes('cursor')) {
1129
+ if (uninstallHooks('.cursor', homeDir)) {
1130
+ console.log(' SDD-AI hooks removed from ~/.cursor/hooks.json');
1131
+ }
987
1132
  }
988
1133
 
989
- // Always attempt to uninstall the global OpenCode plugin
990
- try {
991
- if (uninstallOpenCodePlugin(homeDir)) {
992
- console.log(' OpenCode plugin removed from global plugins directory');
1134
+ if (ideDirsForClean.includes('.opencode') || ideDirsForClean.includes('opencode')) {
1135
+ try {
1136
+ removeOpenCodeGlobalArtifacts(homeDir);
1137
+ if (uninstallOpenCodePlugin(homeDir)) {
1138
+ console.log(' OpenCode global skills, agents, and plugin removed');
1139
+ } else {
1140
+ console.log(' OpenCode global artifacts removed from config directory');
1141
+ }
1142
+ } catch (err) {
1143
+ console.error(` Warning: could not remove OpenCode global artifacts: ${err.message}`);
993
1144
  }
994
- } catch (err) {
995
- console.error(` Warning: could not remove OpenCode plugin: ${err.message}`);
996
1145
  }
997
1146
 
998
- // Remove Codex global artifacts (skills + agents) and hooks
999
- try {
1000
- removeCodexArtifacts(homeDir);
1001
- console.log(' Codex skills and agents removed from ~/.codex/');
1002
- } catch (err) {
1003
- console.error(` Warning: could not remove Codex artifacts: ${err.message}`);
1004
- }
1005
- if (uninstallHooks('.codex', homeDir)) {
1006
- console.log(' SDD-AI hooks removed from ~/.codex/config.toml');
1147
+ if (ideDirsForClean.includes('.codex') || ideDirsForClean.includes('codex')) {
1148
+ try {
1149
+ removeCodexArtifacts(homeDir);
1150
+ console.log(' Codex skills and agents removed from ~/.codex/');
1151
+ } catch (err) {
1152
+ console.error(` Warning: could not remove Codex artifacts: ${err.message}`);
1153
+ }
1154
+ if (uninstallHooks('.codex', homeDir)) {
1155
+ console.log(' SDD-AI hooks removed from ~/.codex/config.toml');
1156
+ }
1007
1157
  }
1008
1158
 
1009
1159
  // Clean project-level OpenCode artifacts if .opencode/ directory is present
@@ -1065,10 +1215,12 @@ function help() {
1065
1215
  check-review Verify that review has been completed (used by PreToolUse hook)
1066
1216
  compact-bash Rewrite bare Bash commands to reduce tokens (used by PreToolUse hook)
1067
1217
  compact Subcommands for the compact-bash hook:
1068
- compact stats - Full stats (hook + already-compact) and estimated savings
1218
+ compact stats - Stats (hook + CodeGraph) and estimated savings
1219
+ compact log-codegraph-event - Log CodeGraph session (--skill --has-graph --tool-calls --tokens)
1069
1220
  compact disable - Temporarily disable rewrite
1070
1221
  compact enable - Re-enable rewrite
1071
- compact clear-log - Clear the history log
1222
+ compact clear-log - Clear compact.log
1223
+ compact codegraph-clear-log - Clear codegraph.log
1072
1224
  bus Subcommands for the inter-agent chat room (refacil-bus):
1073
1225
  bus start - Start the local broker (auto-spawn detached)
1074
1226
  bus stop - Stop the broker
@@ -1076,15 +1228,17 @@ function help() {
1076
1228
  bus serve - (internal) Run the broker in foreground
1077
1229
  bus join --room <room> [--session <s>] [--intro "..."]
1078
1230
  bus leave [--session <s>]
1079
- bus say --text "..." [--session <s>]
1080
- bus ask --to <name|all|*|everyone> --text "..." [--wait N] [--session <s>]
1081
- bus reply --text "..." [--correlation <id>] [--to <name>]
1231
+ bus say --text "..." [--from-env VAR] [--session <s>]
1232
+ bus ask --to <name|all|*|everyone> --text "..." [--from-env VAR] [--wait N] [--session <s>]
1233
+ bus reply --text "..." [--from-env VAR] [--correlation <id>] [--to <name>]
1082
1234
  bus history [--n N] [--session <s>]
1083
1235
  bus inbox [--session <s>]
1084
1236
  bus rooms
1085
1237
  bus watch <session> [--room <room>] (live panel, no tokens)
1086
1238
  bus attend [--timeout N] (listen for directed questions)
1087
1239
  bus view (open the web UI in the browser)
1240
+ read-spec Read a Markdown spec aloud in the browser (on-device TTS):
1241
+ read-spec --file <path> [--lang es] [--voice M3] [--speed 1]
1088
1242
  sdd Subcommands for managing SDD-AI artifacts in refacil-sdd/:
1089
1243
  sdd new-change <name> Create a change with proposal/design/tasks/specs scaffold
1090
1244
  sdd archive <name> Archive a change to refacil-sdd/changes/archive/
@@ -1099,6 +1253,14 @@ function help() {
1099
1253
  [--base-branch <branch>] Base branch for new changes
1100
1254
  [--protected-branches <csv>] Protected branches (comma-separated)
1101
1255
  [--artifact-language <lang>] Artifact language: english (default) or spanish
1256
+ kapso Subcommands for Kapso WhatsApp notification setup:
1257
+ kapso setup Interactive setup of ~/.refacil-sdd-ai/kapso.env
1258
+ (KAPSO_API_KEY, KAPSO_PHONE_NUMBER_ID, NOTIFY_PHONE)
1259
+ kapso preflight Check credentials and print 24h window notice (--json)
1260
+ kapso notify Send a WhatsApp notification (success|failure) with flags
1261
+ autopilot Subcommands for the autopilot pipeline:
1262
+ autopilot ready-message Generate the pre-flight ready message
1263
+ (--change <name> [--ide <ide>] [--lang es|en])
1102
1264
  clean Remove SDD-AI hooks from global IDE config dirs and skills from global dirs
1103
1265
  help Show this help
1104
1266
 
@@ -1126,7 +1288,8 @@ function help() {
1126
1288
 
1127
1289
  // --- Main ---
1128
1290
 
1129
- const command = process.argv[2] || 'help';
1291
+ const args = process.argv.slice(2);
1292
+ const command = args[0] || 'help';
1130
1293
 
1131
1294
  if (command === '--version' || command === '-v') {
1132
1295
  console.log(getPackageVersion(packageRoot));
@@ -1162,14 +1325,163 @@ switch (command) {
1162
1325
  compactBash.run();
1163
1326
  break;
1164
1327
  case 'compact':
1165
- handleCompact(process.argv[3]);
1328
+ handleCompact(process.argv[3], process.argv.slice(4));
1166
1329
  break;
1167
1330
  case 'bus':
1168
1331
  handleBus(process.argv[3], process.argv.slice(4), packageRoot);
1169
1332
  break;
1333
+ case 'read-spec':
1334
+ handleReadSpec(process.argv.slice(3), packageRoot, projectRoot).catch((err) => {
1335
+ console.error(` Error: ${err.message}`);
1336
+ process.exit(1);
1337
+ });
1338
+ break;
1170
1339
  case 'sdd':
1171
1340
  handleSdd(process.argv[3], process.argv.slice(4), projectRoot);
1172
1341
  break;
1342
+ case 'codegraph': {
1343
+ const cgSub = process.argv[3];
1344
+ if (cgSub === 'init') {
1345
+ codegraph.init(projectRoot);
1346
+ codegraph.registerMcp(readSelectedIDEs() || ['.claude', '.cursor', '.opencode', '.codex']);
1347
+ console.log('[refacil-sdd-ai] CodeGraph indexing started in background. Run your next /refacil:explore when it finishes.');
1348
+ } else if (cgSub === 'status') {
1349
+ const { loadBranchConfigWithSources } = require('../lib/config');
1350
+ const cfgInfo = loadBranchConfigWithSources(projectRoot);
1351
+ const installed = codegraph.isInstalled();
1352
+ const initialized = codegraph.isInitialized(projectRoot);
1353
+ if (process.argv.includes('--json')) {
1354
+ console.log(JSON.stringify({
1355
+ installed,
1356
+ initialized,
1357
+ mode: cfgInfo.codegraphMode,
1358
+ modeSource: cfgInfo.sources.codegraphMode,
1359
+ }));
1360
+ } else {
1361
+ console.log(' CodeGraph status:');
1362
+ console.log(` installed: ${installed}`);
1363
+ console.log(` initialized: ${initialized} (${projectRoot})`);
1364
+ console.log(` mode: ${cfgInfo.codegraphMode || '(not set)'} [source: ${cfgInfo.sources.codegraphMode}]`);
1365
+ }
1366
+ } else if (cgSub === 'setup') {
1367
+ // Install CLI if missing, register MCP, then index synchronously so the caller
1368
+ // (skill / agent) does not continue until .codegraph/ is fully built.
1369
+ const { execSync } = require('child_process');
1370
+ const selectedIDEs = readSelectedIDEs() || ['.claude', '.cursor', '.opencode', '.codex'];
1371
+ if (!codegraph.isInstalled()) {
1372
+ process.stdout.write('[refacil-sdd-ai] Installing @colbymchenry/codegraph globally...\n');
1373
+ try {
1374
+ execSync('npm install -g @colbymchenry/codegraph', { stdio: 'inherit', timeout: 120000 });
1375
+ process.stdout.write('[refacil-sdd-ai] @colbymchenry/codegraph installed.\n');
1376
+ } catch (err) {
1377
+ process.stderr.write(`[refacil-sdd-ai] Failed to install codegraph: ${err.message}\n`);
1378
+ process.exit(1);
1379
+ }
1380
+ }
1381
+ codegraph.registerMcp(selectedIDEs);
1382
+ if (!codegraph.isInitialized(projectRoot)) {
1383
+ process.stdout.write(`[refacil-sdd-ai] Initializing CodeGraph at ${projectRoot} ...\n`);
1384
+ try {
1385
+ // Step 1: init creates .codegraph/ structure (fast, ~1s).
1386
+ execSync('codegraph init', {
1387
+ stdio: 'inherit',
1388
+ cwd: projectRoot,
1389
+ timeout: 30000,
1390
+ shell: true,
1391
+ });
1392
+ process.stdout.write('[refacil-sdd-ai] Building index (this may take ~30s) ...\n');
1393
+ // Step 2: index builds the actual symbol graph.
1394
+ // Run synchronously with inherited stdio so the agent sees all output and
1395
+ // waits for the graph to be fully built before continuing.
1396
+ execSync('codegraph index', {
1397
+ stdio: 'inherit',
1398
+ cwd: projectRoot,
1399
+ timeout: 300000, // 5 min ceiling for large repos
1400
+ shell: true,
1401
+ });
1402
+ // Record timestamp so isStale() can detect subsequent commits
1403
+ try {
1404
+ const lastInitPath = path.join(projectRoot, '.codegraph', '.refacil-last-init');
1405
+ fs.mkdirSync(path.dirname(lastInitPath), { recursive: true });
1406
+ fs.writeFileSync(lastInitPath, new Date().toISOString());
1407
+ } catch (_) {}
1408
+ process.stdout.write('[refacil-sdd-ai] CodeGraph index complete. .codegraph/ is ready.\n');
1409
+ } catch (err) {
1410
+ process.stderr.write(`[refacil-sdd-ai] codegraph index failed: ${err.message}\n`);
1411
+ process.exit(1);
1412
+ }
1413
+ } else {
1414
+ console.log('[refacil-sdd-ai] CodeGraph already initialized for this repo. MCP registration refreshed.');
1415
+ }
1416
+ } else {
1417
+ console.error(` Unknown codegraph subcommand: ${cgSub || '(none)'}. Usage: refacil-sdd-ai codegraph <init|setup|status>`);
1418
+ process.exit(1);
1419
+ }
1420
+ break;
1421
+ }
1422
+ case 'kapso': {
1423
+ const kapsoSub = args[1];
1424
+ const kapso = require('../lib/kapso');
1425
+ if (kapsoSub === 'setup') {
1426
+ kapso.setup().catch((err) => {
1427
+ console.error(` Error during kapso setup: ${err.message}`);
1428
+ process.exit(1);
1429
+ });
1430
+ } else if (kapsoSub === 'preflight') {
1431
+ const result = kapso.preflight();
1432
+ process.stdout.write(JSON.stringify(result) + '\n');
1433
+ process.exit(0);
1434
+ } else if (kapsoSub === 'notify') {
1435
+ const notifyType = args[2]; // "success" or "failure"
1436
+ if (!notifyType) {
1437
+ console.error(' Usage: refacil-sdd-ai kapso notify <success|failure> [flags]');
1438
+ process.exit(1);
1439
+ }
1440
+ // Parse flags from process.argv
1441
+ const getFlag = (flag, fallback = '') => {
1442
+ const idx = process.argv.indexOf(flag);
1443
+ return idx !== -1 && process.argv[idx + 1] ? process.argv[idx + 1] : fallback;
1444
+ };
1445
+ // Compute duration from marker startedAt (authoritative) or fall back to --duration flag.
1446
+ let computedDuration = getFlag('--duration', '0');
1447
+ try {
1448
+ const markerPath = path.join(findProjectRoot(), 'refacil-sdd', '.autopilot-active');
1449
+ if (fs.existsSync(markerPath)) {
1450
+ const marker = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
1451
+ if (marker.startedAt) {
1452
+ const elapsed = Math.round((Date.now() - new Date(marker.startedAt).getTime()) / 60000);
1453
+ computedDuration = String(elapsed);
1454
+ }
1455
+ }
1456
+ } catch (_) { /* keep fallback */ }
1457
+ const opts = {
1458
+ repo: getFlag('--repo'),
1459
+ change: getFlag('--change'),
1460
+ branch: getFlag('--branch'),
1461
+ tasks: getFlag('--tasks', '0/0'),
1462
+ duration: computedDuration,
1463
+ pr: getFlag('--pr') || null,
1464
+ apply: getFlag('--apply', '0'),
1465
+ test: getFlag('--test', '0'),
1466
+ review: getFlag('--review', '0'),
1467
+ warnings: getFlag('--warnings', ''),
1468
+ phase: getFlag('--phase'),
1469
+ lastCommit: getFlag('--last-commit'),
1470
+ error: getFlag('--error'),
1471
+ };
1472
+ kapso.notify(notifyType, opts).catch((err) => {
1473
+ console.error(` Error: ${err.message}`);
1474
+ process.exit(1);
1475
+ });
1476
+ } else {
1477
+ console.error(` Unknown kapso subcommand: ${kapsoSub || '(none)'}. Usage: kapso <setup|preflight|notify>`);
1478
+ process.exit(1);
1479
+ }
1480
+ break;
1481
+ }
1482
+ case 'autopilot':
1483
+ handleAutopilot(process.argv[3], process.argv.slice(4));
1484
+ break;
1173
1485
  case 'clean':
1174
1486
  clean();
1175
1487
  break;