refacil-sdd-ai 5.2.2 → 5.3.0

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 +209 -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 +46 -2
  8. package/agents/tester.md +45 -8
  9. package/agents/validator.md +67 -13
  10. package/bin/cli.js +428 -83
  11. package/bin/postinstall.js +20 -0
  12. package/lib/bus/broker.js +121 -3
  13. package/lib/bus/spawn.js +189 -121
  14. package/lib/check-review.js +102 -0
  15. package/lib/codegraph-telemetry.js +135 -0
  16. package/lib/codegraph.js +273 -0
  17. package/lib/commands/autopilot.js +120 -0
  18. package/lib/commands/bus.js +29 -36
  19. package/lib/commands/compact.js +185 -46
  20. package/lib/commands/read-spec.js +352 -0
  21. package/lib/commands/sdd.js +429 -44
  22. package/lib/compact-guidance.js +122 -77
  23. package/lib/config.js +136 -0
  24. package/lib/global-paths.js +56 -20
  25. package/lib/hooks.js +32 -4
  26. package/lib/ide-detection.js +1 -1
  27. package/lib/ignore-files.js +5 -1
  28. package/lib/installer.js +202 -19
  29. package/lib/kapso.js +241 -0
  30. package/lib/methodology-migration-pending.js +13 -0
  31. package/lib/open-browser.js +32 -0
  32. package/lib/opencode-migrate.js +148 -0
  33. package/lib/opencode-plugin/index.js +84 -104
  34. package/lib/opencode-plugin/rules.js +236 -0
  35. package/lib/project-root.js +154 -0
  36. package/lib/repo-ide-sync.js +5 -0
  37. package/lib/spec-reader/lang.js +72 -0
  38. package/lib/spec-reader/md-parser.js +299 -0
  39. package/lib/spec-reader/session.js +139 -0
  40. package/lib/spec-reader/ui/app.js +685 -0
  41. package/lib/spec-reader/ui/index.html +59 -0
  42. package/lib/spec-reader/ui/mixed-lang.js +200 -0
  43. package/lib/spec-reader/ui/model-cache.js +117 -0
  44. package/lib/spec-reader/ui/style.css +294 -0
  45. package/lib/spec-reader/ui/supertonic-helper.js +565 -0
  46. package/lib/spec-sync.js +258 -0
  47. package/lib/test-scope.js +713 -0
  48. package/lib/testing-policy-sync.js +14 -2
  49. package/package.json +6 -3
  50. package/skills/apply/SKILL.md +39 -64
  51. package/skills/archive/SKILL.md +74 -48
  52. package/skills/ask/SKILL.md +43 -8
  53. package/skills/autopilot/SKILL.md +476 -0
  54. package/skills/bug/SKILL.md +52 -53
  55. package/skills/explore/SKILL.md +48 -1
  56. package/skills/guide/SKILL.md +31 -13
  57. package/skills/inbox/SKILL.md +9 -0
  58. package/skills/join/SKILL.md +1 -1
  59. package/skills/prereqs/BUS-CROSS-REPO.md +33 -16
  60. package/skills/prereqs/METHODOLOGY-CONTRACT.md +96 -17
  61. package/skills/prereqs/SKILL.md +1 -1
  62. package/skills/propose/SKILL.md +74 -19
  63. package/skills/read-spec/SKILL.md +76 -0
  64. package/skills/reply/SKILL.md +42 -9
  65. package/skills/review/SKILL.md +63 -25
  66. package/skills/review/checklist.md +2 -2
  67. package/skills/say/SKILL.md +40 -4
  68. package/skills/setup/SKILL.md +59 -5
  69. package/skills/setup/troubleshooting.md +11 -3
  70. package/skills/stats/SKILL.md +157 -0
  71. package/skills/test/SKILL.md +35 -10
  72. package/skills/up-code/SKILL.md +20 -13
  73. package/skills/update/SKILL.md +32 -1
  74. package/skills/verify/SKILL.md +78 -41
  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,40 @@ 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);
508
+
509
+ // Self-healing: if selected-ides.json exists but global skills/hooks are missing, restore them
510
+ // Covers the case where the Claude Code desktop app overwrites settings.json and wipes SDD hooks,
511
+ // or where skills were deleted by any means while the package is still installed.
512
+ try {
513
+ const home = os.homedir();
514
+ const sddSelectedIDEs = readSelectedIDEs(home);
515
+ if (sddSelectedIDEs && sddSelectedIDEs.length > 0) {
516
+ const dirMap = {
517
+ '.claude': globalClaudeDir(home),
518
+ '.cursor': globalCursorDir(home),
519
+ '.opencode': globalOpenCodeDir(home),
520
+ '.codex': globalCodexDir(home),
521
+ };
522
+ const missingSkills = sddSelectedIDEs.some(
523
+ (ide) => dirMap[ide] && !fs.existsSync(path.join(dirMap[ide], 'skills')),
524
+ );
525
+ if (missingSkills) {
526
+ installSkills(packageRoot, home, sddSelectedIDEs);
527
+ installAgents(packageRoot, home, sddSelectedIDEs);
528
+ writeGlobalVersion(getPackageVersion(packageRoot));
529
+ process.stdout.write('[refacil-sdd-ai] Self-healed: global skills and agents restored.\n');
530
+ }
531
+ // Always verify hooks are present for all selected IDEs — idempotent, only writes if missing
532
+ for (const ide of ['.claude', '.cursor', '.opencode', '.codex']) {
533
+ if (sddSelectedIDEs.includes(ide)) {
534
+ installHooks(ide, home, root);
535
+ }
536
+ }
537
+ }
538
+ } catch (_) {
539
+ // Tolerant — self-healing must never break session startup
540
+ }
384
541
 
385
542
  // Step 1: update the global package if a newer version is available on npm
386
543
  try {
@@ -406,15 +563,15 @@ function checkUpdate() {
406
563
  }
407
564
 
408
565
  // Step 2: sync repo skills with the (now updated) package
409
- const syncResult = syncRepoSkillsIfStale(localVersion);
566
+ const syncResult = syncRepoSkillsIfStale(localVersion, root);
410
567
  if (syncResult && !syncResult.failed) {
411
568
  const fromLabel = syncResult.from ? `v${syncResult.from}` : 'unknown version';
412
569
  console.log(
413
570
  `[refacil-sdd-ai] Repo skills synced (${fromLabel} -> v${syncResult.to}). ` +
414
- 'Restart the Claude Code or Cursor session to pick up the changes.',
571
+ 'Restart your IDE session to pick up the changes.',
415
572
  );
416
- if (methodologyMigrationPending(projectRoot).pending) {
417
- writePendingUpdateFlag(projectRoot, syncResult.from, syncResult.to);
573
+ if (methodologyMigrationPending(root).pending) {
574
+ writePendingUpdateFlag(root, syncResult.from, syncResult.to);
418
575
  }
419
576
  } else if (syncResult && syncResult.failed) {
420
577
  console.log(
@@ -422,6 +579,40 @@ function checkUpdate() {
422
579
  'but automatic sync failed. Run manually: refacil-sdd-ai update',
423
580
  );
424
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
+ }
425
616
  }
426
617
 
427
618
  // --- Check review (PreToolUse hook) ---
@@ -436,38 +627,8 @@ function checkReview() {
436
627
  }
437
628
 
438
629
  const command = (input.tool_input && input.tool_input.command) || '';
439
- if (!command.match(/git\s+push/)) return;
440
-
441
- const sddChangesDir = path.join(projectRoot, 'refacil-sdd', 'changes');
442
- const legacyChangesDir = path.join(projectRoot, 'openspec', 'changes');
443
- const changesDir = fs.existsSync(sddChangesDir) ? sddChangesDir : legacyChangesDir;
444
- if (!fs.existsSync(changesDir)) return;
445
-
446
- const entries = fs.readdirSync(changesDir, { withFileTypes: true });
447
- const activeChanges = entries.filter(
448
- (e) => e.isDirectory() && e.name !== 'archive',
449
- );
450
-
451
- if (activeChanges.length === 0) return;
452
-
453
- const missing = activeChanges.filter(
454
- (e) => !fs.existsSync(path.join(changesDir, e.name, '.review-passed')),
455
- );
456
-
457
- if (missing.length > 0) {
458
- const names = missing.map((e) => e.name).join(', ');
459
- const reason =
460
- missing.length === 1
461
- ? `[refacil-sdd-ai] Review pending for: ${names}. ` +
462
- 'Stop the push and run /refacil:review on that change before pushing code. ' +
463
- 'If the review passes, retry the git push. ' +
464
- 'If the review requires corrections, report the findings to the user and DO NOT retry the push.'
465
- : `[refacil-sdd-ai] Multiple changes without approved review: ${names}. ` +
466
- 'Stop the push and ask the user to explicitly select which change they want to push. ' +
467
- 'Then run /refacil:review <change-name> for that specific change and retry the push. ' +
468
- 'Do not run automatic review without explicit selection when there is more than one pending change.';
469
- console.log(JSON.stringify({ decision: 'block', reason }));
470
- }
630
+ const block = evaluateGitPushReview(command, resolveWorkspaceRoot({ hookInput: input, skipStdin: true }));
631
+ if (block) console.log(JSON.stringify(block));
471
632
  }
472
633
 
473
634
  // --- Branch config prompt (used by init) ---
@@ -624,6 +785,9 @@ async function init() {
624
785
  // Prompt for global branch configuration (skipped with --yes/--defaults or non-TTY)
625
786
  await promptBranchConfig();
626
787
 
788
+ // Prompt for CodeGraph integration mode (skipped with --yes/--defaults or non-TTY)
789
+ await promptCodegraphMode(os.homedir());
790
+
627
791
  if (selectedIDEs.length === 0) {
628
792
  console.log('\n No IDEs selected. Nothing installed.\n');
629
793
  console.log(' Re-run with: refacil-sdd-ai init --all to install for all IDEs');
@@ -862,6 +1026,14 @@ function update() {
862
1026
  console.error(` Warning: session repo sync: ${err.message}`);
863
1027
  }
864
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
+
865
1037
  console.log('\n RESTART your IDE session to apply the changes.\n');
866
1038
  }
867
1039
 
@@ -933,8 +1105,10 @@ function clean() {
933
1105
 
934
1106
  const homeDir = os.homedir();
935
1107
 
936
- const selectedIDEs = readSelectedIDEs() || ['claude', 'cursor', 'opencode'];
937
- 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);
938
1112
  if (globalCount > 0) {
939
1113
  console.log(` ${globalCount} global skills removed from IDE user directories`);
940
1114
  }
@@ -944,33 +1118,42 @@ function clean() {
944
1118
  console.log(` ${count} project-level skills removed from .claude/skills/ and .cursor/skills/`);
945
1119
  }
946
1120
 
947
- if (uninstallHooks('.claude', homeDir)) {
948
- console.log(' SDD-AI hooks removed from ~/.claude/settings.json');
949
- } else {
950
- 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
+ }
951
1127
  }
952
- if (uninstallHooks('.cursor', homeDir)) {
953
- 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
+ }
954
1132
  }
955
1133
 
956
- // Always attempt to uninstall the global OpenCode plugin
957
- try {
958
- if (uninstallOpenCodePlugin(homeDir)) {
959
- 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}`);
960
1144
  }
961
- } catch (err) {
962
- console.error(` Warning: could not remove OpenCode plugin: ${err.message}`);
963
1145
  }
964
1146
 
965
- // Remove Codex global artifacts (skills + agents) and hooks
966
- try {
967
- removeCodexArtifacts(homeDir);
968
- console.log(' Codex skills and agents removed from ~/.codex/');
969
- } catch (err) {
970
- console.error(` Warning: could not remove Codex artifacts: ${err.message}`);
971
- }
972
- if (uninstallHooks('.codex', homeDir)) {
973
- 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
+ }
974
1157
  }
975
1158
 
976
1159
  // Clean project-level OpenCode artifacts if .opencode/ directory is present
@@ -1032,10 +1215,12 @@ function help() {
1032
1215
  check-review Verify that review has been completed (used by PreToolUse hook)
1033
1216
  compact-bash Rewrite bare Bash commands to reduce tokens (used by PreToolUse hook)
1034
1217
  compact Subcommands for the compact-bash hook:
1035
- 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)
1036
1220
  compact disable - Temporarily disable rewrite
1037
1221
  compact enable - Re-enable rewrite
1038
- compact clear-log - Clear the history log
1222
+ compact clear-log - Clear compact.log
1223
+ compact codegraph-clear-log - Clear codegraph.log
1039
1224
  bus Subcommands for the inter-agent chat room (refacil-bus):
1040
1225
  bus start - Start the local broker (auto-spawn detached)
1041
1226
  bus stop - Stop the broker
@@ -1043,15 +1228,17 @@ function help() {
1043
1228
  bus serve - (internal) Run the broker in foreground
1044
1229
  bus join --room <room> [--session <s>] [--intro "..."]
1045
1230
  bus leave [--session <s>]
1046
- bus say --text "..." [--session <s>]
1047
- bus ask --to <name|all|*|everyone> --text "..." [--wait N] [--session <s>]
1048
- 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>]
1049
1234
  bus history [--n N] [--session <s>]
1050
1235
  bus inbox [--session <s>]
1051
1236
  bus rooms
1052
1237
  bus watch <session> [--room <room>] (live panel, no tokens)
1053
1238
  bus attend [--timeout N] (listen for directed questions)
1054
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]
1055
1242
  sdd Subcommands for managing SDD-AI artifacts in refacil-sdd/:
1056
1243
  sdd new-change <name> Create a change with proposal/design/tasks/specs scaffold
1057
1244
  sdd archive <name> Archive a change to refacil-sdd/changes/archive/
@@ -1066,6 +1253,14 @@ function help() {
1066
1253
  [--base-branch <branch>] Base branch for new changes
1067
1254
  [--protected-branches <csv>] Protected branches (comma-separated)
1068
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])
1069
1264
  clean Remove SDD-AI hooks from global IDE config dirs and skills from global dirs
1070
1265
  help Show this help
1071
1266
 
@@ -1093,7 +1288,8 @@ function help() {
1093
1288
 
1094
1289
  // --- Main ---
1095
1290
 
1096
- const command = process.argv[2] || 'help';
1291
+ const args = process.argv.slice(2);
1292
+ const command = args[0] || 'help';
1097
1293
 
1098
1294
  if (command === '--version' || command === '-v') {
1099
1295
  console.log(getPackageVersion(packageRoot));
@@ -1129,14 +1325,163 @@ switch (command) {
1129
1325
  compactBash.run();
1130
1326
  break;
1131
1327
  case 'compact':
1132
- handleCompact(process.argv[3]);
1328
+ handleCompact(process.argv[3], process.argv.slice(4));
1133
1329
  break;
1134
1330
  case 'bus':
1135
1331
  handleBus(process.argv[3], process.argv.slice(4), packageRoot);
1136
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;
1137
1339
  case 'sdd':
1138
1340
  handleSdd(process.argv[3], process.argv.slice(4), projectRoot);
1139
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;
1140
1485
  case 'clean':
1141
1486
  clean();
1142
1487
  break;