@hanzlaa/rcode 2.1.0 → 2.2.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.
package/README.md CHANGED
@@ -54,22 +54,27 @@ It's not a chatbot. It's a methodology.
54
54
 
55
55
  ## Install — one command
56
56
 
57
- In any project directory:
57
+ In any project directory (existing codebase OR empty folder):
58
58
 
59
59
  ```bash
60
60
  npx @hanzlaa/rcode install
61
61
  ```
62
62
 
63
- That's it. One unified installer. Pure file shipping, no runtime dependencies. Installs into:
63
+ [Live on npm](https://www.npmjs.com/package/@hanzlaa/rcode) as `@hanzlaa/rcode` · current version `v2.1.0`. See [`docs/install.md`](docs/install.md) for flavors (module subsets, IDE options, version pinning, yolo mode).
64
+
65
+ One unified installer. Pure file shipping, no runtime dependencies. Installs into:
64
66
 
65
67
  - `.rihal/` — config, workflows, references, bin (Rihal infrastructure)
66
68
  - `.claude/agents/` — 44 first-class subagents
67
69
  - `.claude/commands/rihal/` — 93 slash commands
68
70
  - `.claude/skills/` — 58 phrase-activated skills (scaffold-project, create-prd, retrospective, etc.)
71
+ - `rihal/brain/` — Rihal standards pulled from upstream (PR / commit / architecture docs)
69
72
  - `.planning/` — where your artifacts land (council sessions, plans, chains, summaries)
70
73
 
71
74
  Restart Claude Code (or your IDE), type `/`, and every `rihal:*` command appears.
72
75
 
76
+ Update anytime with `npx @hanzlaa/rcode update` (or `/rihal:update` inside a Claude session).
77
+
73
78
  ### Then begin the rihla
74
79
 
75
80
  ```
package/cli/install.js CHANGED
@@ -71,6 +71,9 @@ function parseArgs(argv) {
71
71
  ide: 'claude', // claude, cursor, gemini (copilot = TODO)
72
72
  help: false,
73
73
  modules: [], // --module core --module execution or empty = all
74
+ // #189 — planning commit policy. null = ask interactively (or default true under --yes).
75
+ // Set true by --commit-planning, false by --no-commit-planning or --ignore-planning.
76
+ commitPlanning: null,
74
77
  };
75
78
  const positional = [];
76
79
  for (let i = 0; i < argv.length; i++) {
@@ -85,6 +88,8 @@ function parseArgs(argv) {
85
88
  else if (arg === '--mode') opts.mode = argv[++i];
86
89
  else if (arg === '--ide') opts.ide = argv[++i];
87
90
  else if (arg === '--module') opts.modules.push(argv[++i]);
91
+ else if (arg === '--commit-planning') opts.commitPlanning = true;
92
+ else if (arg === '--no-commit-planning' || arg === '--ignore-planning') opts.commitPlanning = false;
88
93
  else if (!arg.startsWith('--')) positional.push(arg);
89
94
  }
90
95
  if (positional[0]) {
@@ -95,6 +100,30 @@ function parseArgs(argv) {
95
100
  return opts;
96
101
  }
97
102
 
103
+ /**
104
+ * Resolve commit-planning preference — CLI flag wins, then interactive
105
+ * prompt (when TTY + not --yes), else GSD-style default: true.
106
+ * #189.
107
+ */
108
+ async function resolveCommitPlanning(opts) {
109
+ if (opts.commitPlanning !== null) return opts.commitPlanning;
110
+ if (opts.yes || !process.stdin.isTTY) return true; // non-interactive default
111
+
112
+ const readline = require('readline');
113
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
114
+ const prompt = (q) => new Promise(r => rl.question(q, a => r(a)));
115
+ console.log('');
116
+ console.log('📋 .planning/ holds PRDs, roadmaps, sprints, SUMMARY files.');
117
+ console.log(' Commit them to git, or keep them local?');
118
+ console.log('');
119
+ console.log(' [Y] Commit — collaborators see the same plans (default, recommended)');
120
+ console.log(' [n] Gitignore — planning stays local (good for sensitive PRDs)');
121
+ console.log('');
122
+ const answer = (await prompt(' Commit planning artifacts? [Y/n]: ')).trim().toLowerCase();
123
+ rl.close();
124
+ return !(answer === 'n' || answer === 'no');
125
+ }
126
+
98
127
  function printHelp() {
99
128
  console.log(`
100
129
  Rihal Code installer
@@ -284,6 +313,146 @@ function seedStarterPlanning(target, projectName) {
284
313
  return true;
285
314
  }
286
315
 
316
+ /**
317
+ * Ensure the target project's .gitignore has the rcode-managed block.
318
+ *
319
+ * Idempotent via a sentinel comment line. On first install, appends a block
320
+ * that separates:
321
+ * - installed methodology files (ignored; re-install to refresh)
322
+ * - user's project config, state, and planning artifacts (committable)
323
+ *
324
+ * If the user already has a block (marker present) we leave their customizations
325
+ * alone. This function is best-effort — never throws. A missing .gitignore
326
+ * is created. A read/write error is logged and install continues.
327
+ *
328
+ * Returns: { action: 'created' | 'appended' | 'already-present' | 'skipped-error' }
329
+ */
330
+ function ensureRcodeGitignore(target, options = {}) {
331
+ const commitPlanning = options.commitPlanning !== false; // default true
332
+ const BEGIN = '# ===== rcode-managed gitignore block (npx @hanzlaa/rcode install) =====';
333
+ const END = '# ===== end rcode-managed gitignore block =====';
334
+
335
+ const lines = [
336
+ '',
337
+ BEGIN,
338
+ '# Added automatically on first rcode install. Idempotent — safe to re-run.',
339
+ '# Edit `commit_planning` in .rihal/config.yaml to flip planning-artifact tracking.',
340
+ '',
341
+ '# Installed methodology files (regenerate with: npx @hanzlaa/rcode install)',
342
+ '.claude/',
343
+ '.rihal/bin/',
344
+ '.rihal/workflows/',
345
+ '.rihal/references/',
346
+ '.rihal/commands/',
347
+ '.rihal/skills/',
348
+ '',
349
+ '# Pulled Rihal brain content (refresh with: rcode brain pull)',
350
+ '.rihal/brain/rihal-github/',
351
+ '.rihal/brain/rihal-docs/',
352
+ '.rihal/brain/best-practices/',
353
+ '',
354
+ '# Runtime noise',
355
+ '.rihal/state.json.lock',
356
+ '.planning/debug/',
357
+ '.planning/_backup/',
358
+ ];
359
+
360
+ if (!commitPlanning) {
361
+ lines.push(
362
+ '',
363
+ '# Planning artifacts — kept local (commit_planning: false)',
364
+ '.planning/'
365
+ );
366
+ }
367
+
368
+ lines.push(
369
+ '',
370
+ '# What you DO commit:',
371
+ '# .rihal/config.yaml - project mode/language/profile/commit_planning',
372
+ '# .rihal/state.json - decisions, roadmap pointer, blockers',
373
+ '# .rihal/brain/sources.yaml - brain source manifest',
374
+ commitPlanning
375
+ ? '# .planning/ - PRD, roadmap, sprints, SUMMARY.md files'
376
+ : '# (planning artifacts are NOT committed — see commit_planning in config)',
377
+ END,
378
+ ''
379
+ );
380
+ const BLOCK = lines.join('\n');
381
+
382
+ const gitignorePath = path.join(target, '.gitignore');
383
+ try {
384
+ if (!fs.existsSync(gitignorePath)) {
385
+ fs.writeFileSync(gitignorePath, BLOCK);
386
+ return { action: 'created' };
387
+ }
388
+ const existing = fs.readFileSync(gitignorePath, 'utf8');
389
+ // Replace existing rcode block using indexOf (regex escaping on the
390
+ // sentinel is fiddly — indexOf is deterministic and easier to audit).
391
+ function spliceBlock(text, newBlock) {
392
+ const start = text.indexOf(BEGIN);
393
+ if (start < 0) return null;
394
+ const endIdx = text.indexOf(END, start);
395
+ if (endIdx < 0) return null;
396
+ let sliceStart = start;
397
+ if (sliceStart > 0 && text[sliceStart - 1] === '\n') sliceStart -= 1;
398
+ let sliceEnd = endIdx + END.length;
399
+ if (text[sliceEnd] === '\n') sliceEnd += 1;
400
+ return text.slice(0, sliceStart) + newBlock + text.slice(sliceEnd);
401
+ }
402
+ if (existing.includes(BEGIN)) {
403
+ const rewritten = spliceBlock(existing, BLOCK);
404
+ if (rewritten !== null && rewritten !== existing) {
405
+ fs.writeFileSync(gitignorePath, rewritten);
406
+ return { action: 'updated' };
407
+ }
408
+ return { action: 'already-present' };
409
+ }
410
+ fs.writeFileSync(gitignorePath, existing + BLOCK);
411
+ return { action: 'appended' };
412
+ } catch (err) {
413
+ return { action: 'skipped-error', error: err.message };
414
+ }
415
+ }
416
+
417
+ /**
418
+ * Install brain scaffold (sources.yaml + README.md) into .rihal/brain/ on target.
419
+ * Actual brain content lands after `brain pull` runs.
420
+ * Closes #188 — previously the package's rihal/brain/sources.yaml was never
421
+ * copied to the target at all, leaving brain pull permanently broken.
422
+ */
423
+ function installBrainScaffold(packageRoot, target) {
424
+ const srcDir = path.join(packageRoot, 'rihal', 'brain');
425
+ const destDir = path.join(target, '.rihal', 'brain');
426
+ fs.mkdirSync(destDir, { recursive: true });
427
+ let copied = 0;
428
+ for (const name of ['sources.yaml', 'README.md']) {
429
+ const src = path.join(srcDir, name);
430
+ const dest = path.join(destDir, name);
431
+ if (fs.existsSync(src) && !fs.existsSync(dest)) {
432
+ fs.copyFileSync(src, dest);
433
+ copied++;
434
+ }
435
+ }
436
+ // Also pre-seed the best-practices subfolder from the package's
437
+ // rihal/skills/_shared/ so a fresh install has working brain content
438
+ // immediately, even before brain pull runs against real upstream URLs.
439
+ const sharedSrc = path.join(packageRoot, 'rihal', 'skills', '_shared');
440
+ if (fs.existsSync(sharedSrc)) {
441
+ const bpDest = path.join(destDir, 'best-practices');
442
+ fs.mkdirSync(bpDest, { recursive: true });
443
+ for (const entry of fs.readdirSync(sharedSrc, { withFileTypes: true })) {
444
+ if (entry.isFile() && entry.name.endsWith('.md')) {
445
+ const dest = path.join(bpDest, entry.name);
446
+ if (!fs.existsSync(dest)) {
447
+ fs.copyFileSync(path.join(sharedSrc, entry.name), dest);
448
+ copied++;
449
+ }
450
+ }
451
+ }
452
+ }
453
+ return copied;
454
+ }
455
+
287
456
  /**
288
457
  * Install v1-style skills into the target project.
289
458
  *
@@ -575,6 +744,67 @@ function generateFilesManifest(plan, target) {
575
744
  return rows.map((r) => r.join(',')).join('\n') + '\n';
576
745
  }
577
746
 
747
+ /**
748
+ * Orphan sweep — remove files that were part of a previous install but aren't
749
+ * in the current plan. Reads `.rihal/_config/files-manifest.csv` from the
750
+ * previous install and computes the diff against the new plan.
751
+ *
752
+ * Closes #196 — without this, upgrading rcode leaves stale skill/command
753
+ * files around that show up as ghost slash commands in the IDE.
754
+ *
755
+ * Deliberately conservative:
756
+ * - Only removes files that appeared in the PREVIOUS manifest.
757
+ * - Never removes files the user created themselves.
758
+ * - Never touches .rihal/config.yaml, .rihal/state.json, or .planning/.
759
+ *
760
+ * Returns the number of orphan files removed.
761
+ */
762
+ function sweepStaleInstalledFiles(target, newPlan) {
763
+ const manifestPath = path.join(target, '.rihal', '_config', 'files-manifest.csv');
764
+ if (!fs.existsSync(manifestPath)) return 0;
765
+
766
+ let oldRels;
767
+ try {
768
+ const rows = fs.readFileSync(manifestPath, 'utf8').split('\n').slice(1).filter(Boolean);
769
+ oldRels = rows.map(r => r.split(',')[0]).filter(Boolean);
770
+ } catch {
771
+ return 0;
772
+ }
773
+
774
+ const newRelsSet = new Set(newPlan.map(e => e.rel.split(path.sep).join('/')));
775
+ // Safety — never sweep these, even if they somehow landed in the manifest.
776
+ const neverSweep = /^(\.rihal\/config\.yaml|\.rihal\/state\.json|\.rihal\/state\.json\.lock|\.planning\/|\.rihal\/brain\/sources\.yaml)/;
777
+
778
+ let removed = 0;
779
+ const emptyCandidateDirs = new Set();
780
+ for (const rel of oldRels) {
781
+ if (newRelsSet.has(rel)) continue;
782
+ if (neverSweep.test(rel)) continue;
783
+ const full = path.join(target, rel);
784
+ try {
785
+ if (fs.existsSync(full)) {
786
+ fs.rmSync(full, { force: true });
787
+ emptyCandidateDirs.add(path.dirname(full));
788
+ removed += 1;
789
+ }
790
+ } catch {
791
+ // ignore individual failures — sweep is best-effort
792
+ }
793
+ }
794
+
795
+ // Remove any now-empty parent dirs (bottom-up, so nested emptiness cascades).
796
+ const dirsSortedDeep = Array.from(emptyCandidateDirs).sort((a, b) => b.length - a.length);
797
+ for (const dir of dirsSortedDeep) {
798
+ try {
799
+ if (fs.existsSync(dir) && fs.readdirSync(dir).length === 0) {
800
+ fs.rmdirSync(dir);
801
+ }
802
+ } catch {}
803
+ }
804
+
805
+ return removed;
806
+ }
807
+
578
808
  function readPackageVersion() {
579
809
  try {
580
810
  const pkg = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8'));
@@ -626,6 +856,7 @@ function generateConfigYaml(opts) {
626
856
  `communication_language: "${sanitizeYamlValue(opts.language)}"`,
627
857
  `mode: "${sanitizeYamlValue(opts.mode)}"`,
628
858
  `model_profile: "balanced"`,
859
+ `commit_planning: ${opts.commitPlanning !== false}`,
629
860
  `rihal_source_path: "${sanitizeYamlValue(path.dirname(path.dirname(process.argv[1])))}/"`,
630
861
  'workflow:',
631
862
  ' research_by_default: false',
@@ -652,18 +883,53 @@ function convertToCursorMdc(sourceText) {
652
883
  /**
653
884
  * Main install routine. Copies files, generates manifests, writes config.
654
885
  */
655
- function install(opts) {
886
+ async function install(opts) {
656
887
  if (opts.help) { printHelp(); return 0; }
657
888
 
658
- console.log(`\n🕌 Rihal Code installer ${opts.target}`);
889
+ // Resolve commit-planning preference (interactive prompt or flag) #189.
890
+ opts.commitPlanning = await resolveCommitPlanning(opts);
891
+
892
+ console.log(`\n🕌 Rihal Code v${readPackageVersion()} installer → ${opts.target}`);
893
+
894
+ // Detect an existing install and surface it (#195).
895
+ const existingManifestPath = path.join(opts.target, '.rihal', '_config', 'manifest.yaml');
896
+ if (fs.existsSync(existingManifestPath)) {
897
+ const m = fs.readFileSync(existingManifestPath, 'utf8').match(/^version:\s*(.+)$/m);
898
+ const existingVersion = m ? m[1].trim() : 'unknown';
899
+ const newVersion = readPackageVersion();
900
+ if (existingVersion === newVersion) {
901
+ console.log(` ↻ Existing install at v${existingVersion} — refreshing (config + state + .planning preserved).`);
902
+ } else {
903
+ console.log(` ⬆ Existing install at v${existingVersion} — upgrading to v${newVersion} (config + state + .planning preserved).`);
904
+ }
905
+ if (!opts.force) {
906
+ console.log(' Pass --force to also sweep orphaned files from the previous version.');
907
+ }
908
+ }
659
909
  if (!fs.existsSync(SOURCE_ROOT)) {
660
910
  console.error(`✖ Source tree not found at ${SOURCE_ROOT}. Running from wrong dir?`);
661
911
  return 1;
662
912
  }
663
913
 
664
- // Validate IDE
914
+ // Validate IDE — structured error for unsupported editors (#197).
665
915
  if (!['claude', 'cursor', 'gemini'].includes(opts.ide)) {
666
- console.error(`✖ Unknown IDE: ${opts.ide}. Supported: claude, cursor, gemini`);
916
+ console.error(`✖ --ide ${opts.ide} is not supported in v${readPackageVersion()}.`);
917
+ console.error('');
918
+ console.error(' Currently supported:');
919
+ console.error(' claude — Claude Code native (recommended)');
920
+ console.error(' cursor — Cursor IDE');
921
+ console.error(' gemini — Gemini CLI');
922
+ console.error('');
923
+ console.error(' Tracked for v3.0 (see issue #182):');
924
+ console.error(' vscode — VS Code native extension');
925
+ console.error(' jetbrains — IntelliJ / PyCharm');
926
+ console.error(' zed — Zed editor');
927
+ console.error('');
928
+ if (/^(vscode|vs-code|code)$/i.test(opts.ide)) {
929
+ console.error(' Workaround: if you use VS Code WITH the Claude Code extension,');
930
+ console.error(' run `--ide claude` — the extension reads from .claude/ too.');
931
+ console.error('');
932
+ }
667
933
  return 1;
668
934
  }
669
935
 
@@ -698,6 +964,13 @@ function install(opts) {
698
964
  console.log(` Modules: ${opts.modules.join(', ')}`);
699
965
  }
700
966
 
967
+ // Orphan sweep — remove files from previous install not in the new plan (#196).
968
+ // Runs on --force only, to preserve user-edited or hand-dropped files on regular installs.
969
+ let sweptOrphans = 0;
970
+ if (opts.force) {
971
+ sweptOrphans = sweepStaleInstalledFiles(opts.target, plan);
972
+ }
973
+
701
974
  // Copy files
702
975
  let copied = 0;
703
976
  let skipped = 0;
@@ -801,6 +1074,14 @@ function install(opts) {
801
1074
  // Seed .planning/ with starter ROADMAP + STATE so workflows work immediately
802
1075
  const starterSeeded = seedStarterPlanning(opts.target, opts.projectName);
803
1076
 
1077
+ // Install brain scaffolding at .rihal/brain/ (sources.yaml + README).
1078
+ // Actual brain content lands after first brain pull runs.
1079
+ installBrainScaffold(PACKAGE_ROOT, opts.target);
1080
+
1081
+ // Ensure .gitignore separates installed methodology from committable artifacts.
1082
+ // Reads opts.commitPlanning to decide whether .planning/ is in the ignore block.
1083
+ const gitignoreReport = ensureRcodeGitignore(opts.target, { commitPlanning: opts.commitPlanning });
1084
+
804
1085
  // Pull Rihal brain content (v2.0 — issue #158).
805
1086
  // Runs rihal-tools brain pull as a child process. Placeholder URLs
806
1087
  // are skipped gracefully so this does not fail a fresh install.
@@ -823,10 +1104,7 @@ function install(opts) {
823
1104
 
824
1105
  // Summary
825
1106
  console.log('');
826
- console.log(` Installed: ${copied} file${copied === 1 ? '' : 's'}`);
827
- if (skillsInstalled > 0) {
828
- console.log(` Skills: ${skillsInstalled} phrase-activated (in .claude/skills/)`);
829
- }
1107
+ console.log(` Files: ${copied} installed` + (opts.force && sweptOrphans > 0 ? `, ${sweptOrphans} stale swept` : ''));
830
1108
  if (brainReport && brainReport.ok) {
831
1109
  const pulledCount = (brainReport.pulled || []).length;
832
1110
  const skippedCount = (brainReport.skipped || []).length;
@@ -835,26 +1113,50 @@ function install(opts) {
835
1113
  } else if (brainReport && brainReport.error) {
836
1114
  console.log(` Brain: skipped (${brainReport.error})`);
837
1115
  }
1116
+ if (gitignoreReport) {
1117
+ const gitMsg = {
1118
+ 'created': '.gitignore created with rcode block',
1119
+ 'appended': '.gitignore updated — rcode block appended',
1120
+ 'already-present': '.gitignore rcode block already present',
1121
+ 'updated': '.gitignore rcode block refreshed',
1122
+ 'skipped-error': `.gitignore skipped (${gitignoreReport.error})`,
1123
+ }[gitignoreReport.action] || '.gitignore unchanged';
1124
+ console.log(` Gitignore: ${gitMsg}`);
1125
+ }
838
1126
  if (skipped > 0) console.log(` Skipped: ${skipped} (already present, unchanged)`);
839
1127
  if (opts.force && existedBefore) {
840
1128
  console.log(' ⚠ Preserved: .rihal/config.yaml and .rihal/state.json');
841
1129
  console.log(' Pass --reset to wipe and re-init those too.');
842
1130
  }
1131
+
1132
+ // Count installed agents + commands dynamically (#190).
1133
+ const agentsDir = path.join(opts.target, '.claude', 'agents');
1134
+ const commandsDir = path.join(opts.target, '.claude', 'commands', 'rihal');
1135
+ let agentCount = 0, commandCount = 0;
1136
+ try {
1137
+ if (fs.existsSync(agentsDir)) {
1138
+ agentCount = fs.readdirSync(agentsDir).filter(f => f.startsWith('rihal-') && f.endsWith('.md')).length;
1139
+ }
1140
+ if (fs.existsSync(commandsDir)) {
1141
+ commandCount = fs.readdirSync(commandsDir).filter(f => f.endsWith('.md')).length;
1142
+ }
1143
+ } catch {}
1144
+
843
1145
  console.log('');
844
- console.log(` Installed for IDE: ${opts.ide}`);
845
- console.log(` Language: ${opts.language} (change in .rihal/config.yaml → communication_language)`);
846
- console.log(` Mode: ${opts.mode} (guided=confirm at gates, yolo=autonomous)`);
847
- console.log(` Model profile: balanced`);
848
- console.log('');
849
- console.log(' Agents installed (first-class subagents):');
850
- console.log(' 🧭 rihal-sadiq — Director of Strategy');
851
- console.log(' 🏗️ rihal-waleed — CTO');
852
- console.log(' 🛡️ rihal-fatima — QA Lead');
1146
+ console.log(` Version: @hanzlaa/rcode@${readPackageVersion()}`);
1147
+ console.log(` IDE: ${opts.ide}`);
1148
+ console.log(` Language: ${opts.language} (change in .rihal/config.yaml)`);
1149
+ console.log(` Mode: ${opts.mode} (guided=confirm at gates, yolo=autonomous)`);
1150
+ console.log(` Profile: balanced`);
1151
+ console.log(` Planning: ${opts.commitPlanning !== false ? 'committed' : 'gitignored'} (flip: rihal-tools gitignore refresh)`);
853
1152
  console.log('');
854
- console.log(' Slash commands installed:');
855
- console.log(' /rihal:council — parallel multi-agent council');
856
- console.log(' /rihal:status — project state dashboard');
857
- console.log(' /rihal:insert-phase — insert decimal phase for urgent work');
1153
+ console.log(` Agents: ${agentCount} installed in .claude/agents/ (e.g. rihal-sadiq, rihal-waleed, rihal-fatima)`);
1154
+ console.log(` Full roster: node .rihal/bin/rihal-tools.cjs list-agents`);
1155
+ console.log(` Commands: ${commandCount} slash commands in .claude/commands/rihal/ (e.g. /rihal:council, /rihal:create-prd, /rihal:progress)`);
1156
+ console.log(` Full list: ls .claude/commands/rihal/`);
1157
+ if (skillsInstalled > 0) {
1158
+ console.log(` Skills: ${skillsInstalled} phrase-activated in .claude/skills/`);
1159
+ }
858
1160
  console.log('');
859
1161
  if (starterSeeded) {
860
1162
  console.log(' ✓ Starter planning scaffolded in .planning/ (ROADMAP, STATE, PROJECT)');
@@ -862,14 +1164,89 @@ function install(opts) {
862
1164
  }
863
1165
  console.log(' Next:');
864
1166
  console.log(` cd ${opts.target}`);
865
- console.log(' claude # start Claude Code (or restart if already open)');
866
- console.log(' /rihal:sprint-planning # plan your first sprint');
867
- console.log(' /rihal:do # interactive command picker');
868
- console.log(' /rihal:council <question> # multi-agent strategic answer');
1167
+ console.log(' claude # start Claude Code (reload window if already open)');
1168
+ console.log(' /rihal:progress # where you are, what\'s next');
1169
+ console.log(' /rihal:do # interactive command picker');
1170
+ console.log(' /rihal:council <q> # multi-agent strategic answer');
869
1171
  console.log('');
870
- console.log(' If Claude Code is already running, start a new session to load commands.');
1172
+ console.log(' Refresh anytime:');
1173
+ console.log(' npx @hanzlaa/rcode@latest install # pull the latest rcode + brain');
1174
+ console.log(' /rihal:update v2.2.0 # pin rcode to a specific version');
871
1175
  console.log('');
872
- return 0;
1176
+ console.log(' ⚠ If your IDE is already open, reload the window to refresh skills/commands.');
1177
+ console.log(' Claude Code / VS Code / Cursor: Cmd+Shift+P → Reload Window');
1178
+ console.log('');
1179
+
1180
+ // Health check — smoke test that the install actually works (#193).
1181
+ const healthPass = runInstallHealthCheck(opts.target, { agentCount, commandCount, skillsInstalled });
1182
+ return healthPass ? 0 : 1;
1183
+ }
1184
+
1185
+ /**
1186
+ * Run a 5-point smoke test against the fresh install. Closes #193.
1187
+ * Returns true if all pass, false if any critical check failed.
1188
+ * Prints a clean ✓/✖ line per check.
1189
+ */
1190
+ function runInstallHealthCheck(target, counts) {
1191
+ console.log(' Health check:');
1192
+ const { execFileSync } = require('child_process');
1193
+ let fails = 0;
1194
+
1195
+ function check(label, fn) {
1196
+ try {
1197
+ const out = fn();
1198
+ console.log(` ✓ ${label}${out ? ' — ' + out : ''}`);
1199
+ } catch (err) {
1200
+ fails += 1;
1201
+ console.log(` ✖ ${label} — ${String(err.message || err).slice(0, 120)}`);
1202
+ }
1203
+ }
1204
+
1205
+ check('rihal-tools.cjs runs', () => {
1206
+ const toolsPath = path.join(target, '.rihal', 'bin', 'rihal-tools.cjs');
1207
+ if (!fs.existsSync(toolsPath)) throw new Error('bin/rihal-tools.cjs not installed');
1208
+ execFileSync('node', ['-c', toolsPath], { stdio: 'pipe' });
1209
+ return 'syntax ok';
1210
+ });
1211
+
1212
+ check('.rihal/config.yaml present', () => {
1213
+ const p = path.join(target, '.rihal', 'config.yaml');
1214
+ if (!fs.existsSync(p)) throw new Error('missing');
1215
+ const text = fs.readFileSync(p, 'utf8');
1216
+ if (!/user_name:|project_name:/.test(text)) throw new Error('config.yaml incomplete');
1217
+ return `${fs.statSync(p).size} bytes`;
1218
+ });
1219
+
1220
+ check('.rihal/state.json parses', () => {
1221
+ const p = path.join(target, '.rihal', 'state.json');
1222
+ if (!fs.existsSync(p)) throw new Error('missing');
1223
+ JSON.parse(fs.readFileSync(p, 'utf8'));
1224
+ return 'valid JSON';
1225
+ });
1226
+
1227
+ check('agents installed', () => {
1228
+ if ((counts.agentCount || 0) < 20) throw new Error(`only ${counts.agentCount} agents (expected ≥ 20)`);
1229
+ return `${counts.agentCount}`;
1230
+ });
1231
+
1232
+ check('skills + commands installed', () => {
1233
+ const issues = [];
1234
+ if ((counts.skillsInstalled || 0) < 20) issues.push(`${counts.skillsInstalled} skills`);
1235
+ if ((counts.commandCount || 0) < 20) issues.push(`${counts.commandCount} commands`);
1236
+ if (issues.length) throw new Error(`low count: ${issues.join(', ')}`);
1237
+ return `${counts.skillsInstalled} skills + ${counts.commandCount} commands`;
1238
+ });
1239
+
1240
+ if (fails > 0) {
1241
+ console.log('');
1242
+ console.log(` ✖ ${fails} health check${fails === 1 ? '' : 's'} failed — install may be broken.`);
1243
+ console.log(' Debug: node .rihal/bin/rihal-tools.cjs state read && ls -la .rihal/');
1244
+ console.log(' Reinstall: npx @hanzlaa/rcode install . --force');
1245
+ console.log('');
1246
+ return false;
1247
+ }
1248
+ console.log('');
1249
+ return true;
873
1250
  }
874
1251
 
875
1252
  async function main() {
@@ -909,9 +1286,7 @@ async function main() {
909
1286
  }
910
1287
  }
911
1288
 
912
- try {
913
- process.exit(install(opts));
914
- } catch (err) {
1289
+ install(opts).then(code => process.exit(code)).catch(err => {
915
1290
  if (err.code === 'EACCES' || err.code === 'EPERM') {
916
1291
  console.error(`✖ Permission denied: ${err.path || err.message}`);
917
1292
  process.exit(1);
@@ -923,7 +1298,7 @@ async function main() {
923
1298
  console.error(`✖ Install failed: ${err.message}`);
924
1299
  if (process.env.DEBUG) console.error(err.stack);
925
1300
  process.exit(1);
926
- }
1301
+ });
927
1302
  }
928
1303
 
929
1304
  if (require.main === module) main();
@@ -933,10 +1308,10 @@ if (require.main === module) main();
933
1308
  * Converts the index.js-style (args, ctx) signature into a cli/install.js
934
1309
  * parseArgs-compatible argv and runs install().
935
1310
  */
936
- function runFromCli(args /* , ctx */) {
1311
+ async function runFromCli(args /* , ctx */) {
937
1312
  const argv = Array.isArray(args) ? args : [];
938
1313
  const opts = parseArgs(argv);
939
- const code = install(opts);
1314
+ const code = await install(opts);
940
1315
  if (code !== 0) process.exit(code);
941
1316
  }
942
1317
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzlaa/rcode",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Rihal Code (rcode) — installable context-brain for Rihalians. 44 agents, 93 slash commands, 58 skills, pullable Rihal standards. Unified install for Claude Code, Cursor, and Gemini.",
5
5
  "main": "cli/index.js",
6
6
  "bin": {
@@ -2805,8 +2805,26 @@ function cmdNotesCount() {
2805
2805
  */
2806
2806
  function cmdBrain(args) {
2807
2807
  const sub = args[0] || 'help';
2808
- const sourcesPath = path.join(PROJECT_ROOT, 'rihal', 'brain', 'sources.yaml');
2809
- const brainDir = path.join(PROJECT_ROOT, 'rihal', 'brain');
2808
+ // sources.yaml lives under .rihal/brain/ in user installs (v2.2+).
2809
+ // Older installs may have it at rihal/brain/ (pre-v2.2) — fall back for compat.
2810
+ let sourcesPath = path.join(RIHAL_DIR, 'brain', 'sources.yaml');
2811
+ let brainDir = path.join(RIHAL_DIR, 'brain');
2812
+ if (!fs.existsSync(sourcesPath)) {
2813
+ const legacyPath = path.join(PROJECT_ROOT, 'rihal', 'brain', 'sources.yaml');
2814
+ if (fs.existsSync(legacyPath)) {
2815
+ sourcesPath = legacyPath;
2816
+ brainDir = path.join(PROJECT_ROOT, 'rihal', 'brain');
2817
+ }
2818
+ }
2819
+
2820
+ // Resolve a source's dest directory relative to brainDir.
2821
+ // Accepts legacy absolute-looking values ("rihal/brain/rihal-github/") by
2822
+ // stripping any leading "rihal/brain/" so the resolved path sits inside the
2823
+ // chosen brainDir. New sources.yaml should use bare names ("rihal-github/").
2824
+ function resolveDest(dest) {
2825
+ const trimmed = String(dest || '').replace(/^rihal\/brain\//, '').replace(/^\/+/, '');
2826
+ return path.join(brainDir, trimmed);
2827
+ }
2810
2828
 
2811
2829
  if (!fs.existsSync(sourcesPath)) {
2812
2830
  return {
@@ -2913,7 +2931,7 @@ function cmdBrain(args) {
2913
2931
  if (sub === 'status') {
2914
2932
  const report = { ok: true, sources: [] };
2915
2933
  for (const s of sources) {
2916
- const destPath = path.join(PROJECT_ROOT, s.dest || '');
2934
+ const destPath = resolveDest(s.dest);
2917
2935
  const exists = fs.existsSync(destPath);
2918
2936
  report.sources.push({
2919
2937
  name: s.name,
@@ -2947,7 +2965,7 @@ function cmdBrain(args) {
2947
2965
 
2948
2966
  if (repo === 'self') {
2949
2967
  // In-repo copy — use rsync-ish node copy from paths under project root.
2950
- const destPath = path.join(PROJECT_ROOT, s.dest);
2968
+ const destPath = resolveDest(s.dest);
2951
2969
  fs.mkdirSync(destPath, { recursive: true });
2952
2970
  const paths = Array.isArray(s.paths) ? s.paths : [];
2953
2971
  let copied = 0;
@@ -2989,7 +3007,7 @@ function cmdBrain(args) {
2989
3007
  const paths = Array.isArray(s.paths) ? s.paths : [];
2990
3008
  execSync(`git -C "${tmp}" sparse-checkout set ${paths.map(p => `"${p}"`).join(' ')}`, { stdio: 'pipe' });
2991
3009
 
2992
- const destPath = path.join(PROJECT_ROOT, s.dest);
3010
+ const destPath = resolveDest(s.dest);
2993
3011
  fs.mkdirSync(destPath, { recursive: true });
2994
3012
  // Copy everything the sparse checkout materialized.
2995
3013
  function copyTree(src, dst) {
@@ -3301,6 +3319,121 @@ function cmdStateSnapshot() {
3301
3319
  };
3302
3320
  }
3303
3321
 
3322
+ /**
3323
+ * cmdGitignore — re-render the rcode-managed block in .gitignore based on
3324
+ * current config (specifically commit_planning from .rihal/config.yaml).
3325
+ *
3326
+ * Subcommands:
3327
+ * gitignore refresh rewrite the rcode block in-place
3328
+ * gitignore status report current commit_planning + block presence
3329
+ *
3330
+ * Mirrors the logic in cli/install.js ensureRcodeGitignore — kept in sync
3331
+ * by convention. Any change to the block format should update both.
3332
+ * Closes #189 — runtime toggle for commit_planning.
3333
+ */
3334
+ function cmdGitignore(args) {
3335
+ const sub = args[0] || 'refresh';
3336
+ const gitignorePath = path.join(PROJECT_ROOT, '.gitignore');
3337
+ const configPath = path.join(RIHAL_DIR, 'config.yaml');
3338
+
3339
+ // Read commit_planning from config; default true if missing.
3340
+ let commitPlanning = true;
3341
+ if (fs.existsSync(configPath)) {
3342
+ const cfg = fs.readFileSync(configPath, 'utf8');
3343
+ const m = cfg.match(/^\s*commit_planning:\s*(true|false)\s*$/m);
3344
+ if (m) commitPlanning = (m[1] === 'true');
3345
+ }
3346
+
3347
+ const BEGIN = '# ===== rcode-managed gitignore block (npx @hanzlaa/rcode install) =====';
3348
+ const END = '# ===== end rcode-managed gitignore block =====';
3349
+
3350
+ if (sub === 'status') {
3351
+ const exists = fs.existsSync(gitignorePath);
3352
+ const hasBlock = exists && fs.readFileSync(gitignorePath, 'utf8').includes(BEGIN);
3353
+ return {
3354
+ ok: true,
3355
+ gitignore_exists: exists,
3356
+ block_present: hasBlock,
3357
+ commit_planning: commitPlanning,
3358
+ };
3359
+ }
3360
+
3361
+ if (sub !== 'refresh') {
3362
+ return { ok: false, error: `Unknown gitignore subcommand: ${sub}. Try: refresh | status` };
3363
+ }
3364
+
3365
+ const lines = [
3366
+ '',
3367
+ BEGIN,
3368
+ '# Added automatically on rcode install. Idempotent — safe to re-run.',
3369
+ '# Edit `commit_planning` in .rihal/config.yaml, then: rihal-tools gitignore refresh',
3370
+ '',
3371
+ '# Installed methodology files (regenerate with: npx @hanzlaa/rcode install)',
3372
+ '.claude/',
3373
+ '.rihal/bin/',
3374
+ '.rihal/workflows/',
3375
+ '.rihal/references/',
3376
+ '.rihal/commands/',
3377
+ '.rihal/skills/',
3378
+ '',
3379
+ '# Pulled Rihal brain content (refresh with: rcode brain pull)',
3380
+ '.rihal/brain/rihal-github/',
3381
+ '.rihal/brain/rihal-docs/',
3382
+ '.rihal/brain/best-practices/',
3383
+ '',
3384
+ '# Runtime noise',
3385
+ '.rihal/state.json.lock',
3386
+ '.planning/debug/',
3387
+ '.planning/_backup/',
3388
+ ];
3389
+ if (!commitPlanning) {
3390
+ lines.push('', '# Planning artifacts — kept local (commit_planning: false)', '.planning/');
3391
+ }
3392
+ lines.push(
3393
+ '',
3394
+ '# What you DO commit:',
3395
+ '# .rihal/config.yaml - project mode/language/profile/commit_planning',
3396
+ '# .rihal/state.json - decisions, roadmap pointer, blockers',
3397
+ '# .rihal/brain/sources.yaml - brain source manifest',
3398
+ commitPlanning
3399
+ ? '# .planning/ - PRD, roadmap, sprints, SUMMARY.md files'
3400
+ : '# (planning artifacts are NOT committed — see commit_planning in config)',
3401
+ END,
3402
+ ''
3403
+ );
3404
+ const BLOCK = lines.join('\n');
3405
+
3406
+ /** Replace the rcode block in text using indexOf — safer than regex. */
3407
+ function spliceBlock(existing, newBlock) {
3408
+ const start = existing.indexOf(BEGIN);
3409
+ if (start < 0) return null;
3410
+ const endIdx = existing.indexOf(END, start);
3411
+ if (endIdx < 0) return null;
3412
+ // Include trailing newline after END if present, and leading newline before BEGIN.
3413
+ let sliceStart = start;
3414
+ if (sliceStart > 0 && existing[sliceStart - 1] === '\n') sliceStart -= 1;
3415
+ let sliceEnd = endIdx + END.length;
3416
+ if (existing[sliceEnd] === '\n') sliceEnd += 1;
3417
+ return existing.slice(0, sliceStart) + newBlock + existing.slice(sliceEnd);
3418
+ }
3419
+
3420
+ if (!fs.existsSync(gitignorePath)) {
3421
+ fs.writeFileSync(gitignorePath, BLOCK);
3422
+ return { ok: true, action: 'created', commit_planning: commitPlanning };
3423
+ }
3424
+ const existing = fs.readFileSync(gitignorePath, 'utf8');
3425
+ if (existing.includes(BEGIN)) {
3426
+ const rewritten = spliceBlock(existing, BLOCK);
3427
+ if (rewritten !== null && rewritten !== existing) {
3428
+ fs.writeFileSync(gitignorePath, rewritten);
3429
+ return { ok: true, action: 'updated', commit_planning: commitPlanning };
3430
+ }
3431
+ return { ok: true, action: 'no-change', commit_planning: commitPlanning };
3432
+ }
3433
+ fs.writeFileSync(gitignorePath, existing + BLOCK);
3434
+ return { ok: true, action: 'appended', commit_planning: commitPlanning };
3435
+ }
3436
+
3304
3437
  function cmdFindFiles(rawArgs) {
3305
3438
  const flags = {};
3306
3439
  const parts = rawArgs.split(/\s+/).filter(p => p);
@@ -3455,6 +3588,10 @@ async function main() {
3455
3588
  result = cmdStateSnapshot();
3456
3589
  break;
3457
3590
  }
3591
+ case 'gitignore': {
3592
+ result = cmdGitignore(args);
3593
+ break;
3594
+ }
3458
3595
  case 'agent-skills':
3459
3596
  result = cmdAgentInfo(args[0]);
3460
3597
  break;
@@ -34,7 +34,7 @@ sources:
34
34
  - docs/issue-standards.md
35
35
  - docs/commit-standards.md
36
36
  - docs/review-checklist.md
37
- dest: rihal/brain/rihal-github/
37
+ dest: rihal-github/
38
38
 
39
39
  - name: rihal-docs
40
40
  description: >
@@ -47,13 +47,16 @@ sources:
47
47
  - "guides/**/*.md"
48
48
  - "standards/**/*.md"
49
49
  - "playbooks/**/*.md"
50
- dest: rihal/brain/rihal-docs/
50
+ dest: rihal-docs/
51
51
 
52
52
  - name: rihal-best-practices
53
53
  description: >
54
54
  Hanzla's one-year-of-usage best practices — in-repo, version-
55
- controlled alongside the rest of Rihal Code.
55
+ controlled alongside the rest of Rihal Code. Resolved from the
56
+ installed .rihal/skills/_shared/ dir (or the package's own
57
+ rihal/skills/_shared/ when running inside the rihal-code repo).
56
58
  repo: self
57
59
  paths:
60
+ - .rihal/skills/_shared/**/*.md
58
61
  - rihal/skills/_shared/**/*.md
59
- dest: rihal/brain/best-practices/
62
+ dest: best-practices/
@@ -27,14 +27,14 @@ Check for the dashboard server in priority order:
27
27
 
28
28
  1. `./server/dashboard.js` (when inside the rihal-code source repo)
29
29
  2. `./.rihal/lib/server/dashboard.js` (installed package copy)
30
- 3. `$(npm root -g)/@hanzlahabib/rihal-code/server/dashboard.js` (global install)
30
+ 3. `$(npm root -g)/@hanzlaa/rcode/server/dashboard.js` (global install)
31
31
 
32
32
  Store the resolved path as `$DASHBOARD`.
33
33
 
34
34
  If none found, print:
35
35
  ```
36
36
  ❌ Dashboard script not found.
37
- Run `npx rihal-code install` to install the package, or check you're inside a project with .rihal/.
37
+ Run `npx @hanzlaa/rcode install` to install the package, or check you're inside a project with .rihal/.
38
38
  ```
39
39
  Exit.
40
40
 
@@ -50,8 +50,8 @@ The installer needs to be called with the `--module` flag. Detect the rihal-code
50
50
  # Try local dev first, then global
51
51
  if [ -f ./cli/install-v2.js ]; then
52
52
  node ./cli/install-v2.js . --module {name} --force --yes
53
- elif [ -f "$(npm root -g)/@hanzlahabib/rihal-code/cli/install-v2.js" ]; then
54
- node "$(npm root -g)/@hanzlahabib/rihal-code/cli/install-v2.js" . --module {name} --force --yes
53
+ elif [ -f "$(npm root -g)/@hanzlaa/rcode/cli/install-v2.js" ]; then
54
+ node "$(npm root -g)/@hanzlaa/rcode/cli/install-v2.js" . --module {name} --force --yes
55
55
  else
56
56
  echo "Cannot find rihal-code package. Install it globally or run from the repo."
57
57
  exit 1
@@ -179,6 +179,6 @@ If `SNAPSHOT.routes[]` is empty or only has fallback entries, print:
179
179
 
180
180
  ## On Error
181
181
 
182
- - **CLI missing:** "Rihal Code install missing or stale. Run: npx @hanzlahabib/rihal-code install"
182
+ - **CLI missing:** "Rihal Code install missing or stale. Run: npx @hanzlaa/rcode install"
183
183
  - **CLI returns `ok: false`:** surface the CLI's error verbatim. Do not attempt to compensate — the CLI's failures are the source of truth on what's wrong.
184
184
  - **Network-dependent insights:** there should be none. Insights are computed from local state + disk only.
@@ -111,6 +111,6 @@ Group routes by letter. If multiple routes share a letter, list them indented. I
111
111
 
112
112
  ## On Error
113
113
 
114
- - **CLI not found:** "Rihal Code install missing. Run: npx @hanzlahabib/rihal-code install"
114
+ - **CLI not found:** "Rihal Code install missing. Run: npx @hanzlaa/rcode install"
115
115
  - **state.json invalid JSON:** report the CLI's exact error string — the CLI already has a clean error shape.
116
116
  - **Unexpected shape:** fall back to the banner + "State present but unreadable. Try: node .rihal/bin/rihal-tools.cjs state read"