@hanzlaa/rcode 2.2.0 → 2.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/CLAUDE.md +15 -0
  2. package/CONTRIBUTING.md +138 -0
  3. package/README.md +74 -15
  4. package/cli/install.js +312 -80
  5. package/cli/postinstall.js +4 -4
  6. package/cli/uninstall.js +8 -0
  7. package/dist/rcode.js +19777 -0
  8. package/package.json +18 -5
  9. package/rihal/DOCS-AUDIT.md +14 -0
  10. package/rihal/agents/rihal-code-reviewer.md +1 -1
  11. package/rihal/agents/rihal-codebase-mapper.md +1 -1
  12. package/rihal/agents/rihal-docs-auditor.md +1 -1
  13. package/rihal/agents/rihal-edge-case-hunter.md +1 -1
  14. package/rihal/agents/rihal-executor.md +2 -1
  15. package/rihal/agents/rihal-hussain-pm.md +1 -0
  16. package/rihal/agents/rihal-nyquist-auditor.md +1 -1
  17. package/rihal/agents/rihal-phase-researcher.md +2 -2
  18. package/rihal/agents/rihal-planner.md +3 -2
  19. package/rihal/agents/rihal-roadmapper.md +1 -0
  20. package/rihal/agents/rihal-security-adversary.md +1 -1
  21. package/rihal/agents/rihal-security-auditor.md +1 -1
  22. package/rihal/agents/rihal-sprint-checker.md +1 -1
  23. package/rihal/agents/rihal-verifier.md +1 -1
  24. package/rihal/bin/lib/roadmap.cjs +2 -3
  25. package/rihal/bin/rihal-tools.cjs +11 -31
  26. package/rihal/brain/best-practices/no-theoretical-suggestions.md +56 -0
  27. package/rihal/commands/add-phase.md +2 -2
  28. package/rihal/commands/audit.md +8 -0
  29. package/rihal/commands/checkpoint-preview.md +13 -0
  30. package/rihal/commands/cleanup.md +2 -2
  31. package/rihal/commands/config.md +4 -4
  32. package/rihal/commands/pr-branch.md +2 -2
  33. package/rihal/commands/prfaq.md +15 -0
  34. package/rihal/commands/remove-phase.md +2 -2
  35. package/rihal/commands/research-phase.md +2 -2
  36. package/rihal/commands/settings.md +2 -2
  37. package/rihal/commands/ship.md +15 -3
  38. package/rihal/commands/validate-phase.md +1 -1
  39. package/rihal/commands/verify-phase.md +2 -2
  40. package/rihal/references/agent-contracts.md +12 -0
  41. package/rihal/references/karpathy-guidelines-full.md +79 -0
  42. package/rihal/references/karpathy-guidelines.md +8 -76
  43. package/rihal/references/model-profile-resolution.md +8 -0
  44. package/rihal/references/phase-argument-parsing.md +11 -0
  45. package/rihal/references/revision-loop.md +11 -0
  46. package/rihal/references/universal-anti-patterns.md +15 -0
  47. package/rihal/skills/actions/1-analysis/research/rihal-domain-research/SKILL.md +11 -0
  48. package/rihal/skills/actions/1-analysis/research/rihal-market-research/SKILL.md +11 -0
  49. package/rihal/skills/actions/1-analysis/research/rihal-technical-research/SKILL.md +13 -0
  50. package/rihal/skills/actions/1-analysis/rihal-document-project/SKILL.md +11 -0
  51. package/rihal/skills/actions/1-analysis/rihal-prfaq/SKILL.md +12 -0
  52. package/rihal/skills/actions/1-analysis/rihal-product-brief/SKILL.md +7 -0
  53. package/rihal/skills/actions/2-plan/rihal-create-epics-and-stories/SKILL.md +15 -1
  54. package/rihal/skills/actions/2-plan/rihal-create-milestone/SKILL.md +21 -1
  55. package/rihal/skills/actions/2-plan/rihal-create-milestone/steps/step-10-complete.md +1 -1
  56. package/rihal/skills/actions/2-plan/rihal-create-prd/SKILL.md +26 -0
  57. package/rihal/skills/actions/2-plan/rihal-create-story/SKILL.md +16 -2
  58. package/rihal/skills/actions/2-plan/rihal-create-ux-design/SKILL.md +12 -0
  59. package/rihal/skills/actions/2-plan/rihal-edit-prd/SKILL.md +11 -0
  60. package/rihal/skills/actions/2-plan/rihal-frontend-design/SKILL.md +13 -0
  61. package/rihal/skills/actions/2-plan/rihal-validate-prd/SKILL.md +12 -0
  62. package/rihal/skills/actions/3-solutioning/rihal-check-implementation-readiness/SKILL.md +12 -0
  63. package/rihal/skills/actions/3-solutioning/rihal-create-architecture/SKILL.md +14 -0
  64. package/rihal/skills/actions/3-solutioning/rihal-generate-project-context/SKILL.md +12 -0
  65. package/rihal/skills/actions/4-implementation/rihal-checkpoint-preview/SKILL.md +16 -0
  66. package/rihal/skills/actions/4-implementation/rihal-code-review/SKILL.md +12 -0
  67. package/rihal/skills/actions/4-implementation/rihal-correct-course/SKILL.md +13 -0
  68. package/rihal/skills/actions/4-implementation/rihal-dev-story/SKILL.md +12 -0
  69. package/rihal/skills/actions/4-implementation/rihal-qa-generate-e2e-tests/SKILL.md +12 -0
  70. package/rihal/skills/actions/4-implementation/rihal-retrospective/SKILL.md +11 -0
  71. package/rihal/skills/actions/4-implementation/rihal-scaffold-project/SKILL.md +10 -0
  72. package/rihal/skills/actions/4-implementation/rihal-sprint-planning/SKILL.md +14 -1
  73. package/rihal/skills/actions/4-implementation/rihal-sprint-status/SKILL.md +10 -0
  74. package/rihal/skills/agents/ahmed-hassani-director/SKILL.md +13 -1
  75. package/rihal/skills/agents/fatima-qa/SKILL.md +14 -1
  76. package/rihal/skills/agents/haitham-frontend/SKILL.md +15 -1
  77. package/rihal/skills/agents/hanzla-engineer/SKILL.md +14 -1
  78. package/rihal/skills/agents/hussain-pm/SKILL.md +22 -1
  79. package/rihal/skills/agents/hussain-sm/SKILL.md +22 -1
  80. package/rihal/skills/agents/layla-designer/SKILL.md +15 -1
  81. package/rihal/skills/agents/majlis-council/SKILL.md +14 -1
  82. package/rihal/skills/agents/mariam-marketing/SKILL.md +15 -1
  83. package/rihal/skills/agents/nasser-eng-manager/SKILL.md +14 -1
  84. package/rihal/skills/agents/noor-writer/SKILL.md +14 -1
  85. package/rihal/skills/agents/raees-orchestrator/SKILL.md +13 -1
  86. package/rihal/skills/agents/sadiq-analyst/SKILL.md +15 -1
  87. package/rihal/skills/agents/waleed-architect/SKILL.md +16 -1
  88. package/rihal/skills/agents/yousef-backend/SKILL.md +16 -1
  89. package/rihal/skills/agents/zahra-branding/SKILL.md +15 -1
  90. package/rihal/skills/agents/zayd-ml/SKILL.md +17 -1
  91. package/rihal/skills/core/rihal-advanced-elicitation/SKILL.md +12 -0
  92. package/rihal/skills/core/rihal-brainstorming/SKILL.md +16 -0
  93. package/rihal/skills/core/rihal-clone-website/SKILL.md +21 -0
  94. package/rihal/skills/core/rihal-distillator/SKILL.md +8 -0
  95. package/rihal/skills/core/rihal-editorial-review-prose/SKILL.md +12 -0
  96. package/rihal/skills/core/rihal-editorial-review-structure/SKILL.md +18 -0
  97. package/rihal/skills/core/rihal-help/SKILL.md +12 -0
  98. package/rihal/skills/core/rihal-index-docs/SKILL.md +18 -0
  99. package/rihal/skills/core/rihal-init/SKILL.md +8 -0
  100. package/rihal/skills/core/rihal-party-mode/SKILL.md +14 -0
  101. package/rihal/skills/core/rihal-review-adversarial-general/SKILL.md +12 -0
  102. package/rihal/skills/core/rihal-review-edge-case-hunter/SKILL.md +18 -0
  103. package/rihal/skills/core/rihal-shard-doc/SKILL.md +18 -0
  104. package/rihal/team.yaml +205 -0
  105. package/rihal/templates/UAT.md +29 -0
  106. package/rihal/templates/milestone.md +2 -0
  107. package/rihal/templates/sprint.md +11 -28
  108. package/rihal/templates/summary.md +30 -0
  109. package/rihal/templates/verification-report.md +28 -0
  110. package/rihal/workflows/audit-milestone.md +34 -2
  111. package/rihal/workflows/audit.md +172 -0
  112. package/rihal/workflows/autonomous.md +67 -0
  113. package/rihal/workflows/checkpoint-preview.md +7 -0
  114. package/rihal/workflows/council.md +3 -1
  115. package/rihal/workflows/debug.md +8 -1
  116. package/rihal/workflows/diagnose-issues.md +34 -0
  117. package/rihal/workflows/do.md +47 -3
  118. package/rihal/workflows/document-project.md +1 -1
  119. package/rihal/workflows/execute-sprint.md +11 -4
  120. package/rihal/workflows/execute.md +9 -3
  121. package/rihal/workflows/help.md +1 -1
  122. package/rihal/workflows/karpathy-audit.md +7 -14
  123. package/rihal/workflows/pause-work.md +7 -1
  124. package/rihal/workflows/prfaq.md +7 -0
  125. package/rihal/workflows/profile-user.md +2 -2
  126. package/rihal/workflows/settings.md +116 -117
  127. package/rihal/workflows/ship.md +31 -1
  128. package/rihal/workflows/sprint-planning.md +39 -8
  129. package/rihal/workflows/status.md +5 -0
  130. package/rihal/workflows/ui-phase.md +3 -3
  131. package/rihal/workflows/update.md +80 -22
  132. package/rihal/workflows/validate-phase.md +7 -1
  133. package/server/dashboard.js +34 -575
  134. package/server/lib/api.js +123 -0
  135. package/server/lib/html/client.js +969 -0
  136. package/server/lib/html/css.js +416 -0
  137. package/server/lib/html/shell.js +230 -0
  138. package/server/lib/scanner.js +142 -0
  139. package/rihal/agents/rihal-ui-designer.md +0 -6
  140. package/rihal/skills/core/rihal-advanced-elicitation/rihal-advanced-elicitation/SKILL.md +0 -148
  141. package/rihal/skills/core/rihal-advanced-elicitation/rihal-advanced-elicitation/methods.csv +0 -51
  142. package/rihal/skills/core/rihal-shard-doc/rihal-shard-doc/SKILL.md +0 -122
  143. package/rihal/workflows/config.md +0 -105
package/cli/install.js CHANGED
@@ -32,18 +32,22 @@
32
32
  * .planning/
33
33
  * council-sessions/ (empty dir, populated on first council run)
34
34
  *
35
- * Zero external dependencies. Pure Node stdlib.
35
+ * Bundled packages (devDeps, inlined by esbuild in dist/rcode.js):
36
+ * picocolors, nanospinner, fast-glob, zod, semver, diff
36
37
  *
37
38
  * Usage:
38
39
  * node cli/install.js [target-project-dir]
39
40
  * node cli/install.js --help
40
41
  *
41
42
  * Flags:
42
- * --force overwrite existing files without prompting
43
- * --yes non-interactive, accept defaults
44
- * --user <name> set user_name in config.yaml (default: $USER)
45
- * --project <name> set project_name in config.yaml (default: basename of target)
46
- * --language <lang> set communication_language (default: English)
43
+ * --force overwrite existing files without prompting
44
+ * --yes non-interactive, accept defaults
45
+ * --user <name> set user_name in config.yaml (default: $USER)
46
+ * --project <name> set project_name in config.yaml (default: basename of target)
47
+ * --language <lang> set communication_language (default: English)
48
+ * --show-diff print full unified diff for preserved files during update
49
+ * --diff-stat print +N -N summary for preserved files (default on update)
50
+ * --accept-all overwrite all user-modified files with source version
47
51
  */
48
52
 
49
53
  const fs = require('fs');
@@ -51,9 +55,51 @@ const path = require('path');
51
55
  const crypto = require('crypto');
52
56
  const os = require('os');
53
57
 
58
+ // Bundled packages — devDeps inlined by esbuild, loaded from node_modules in dev.
59
+ const pc = require('picocolors');
60
+ const { createSpinner } = require('nanospinner');
61
+ const fg = require('fast-glob');
62
+ const { z } = require('zod');
63
+ const semver = require('semver');
64
+ const { createTwoFilesPatch } = require('diff');
65
+
66
+ // Output helpers: always respect NO_COLOR / non-TTY (picocolors handles this).
67
+ const ok = (s) => pc.green('✓') + ' ' + s;
68
+ const fail = (s) => pc.red('✗') + ' ' + s;
69
+ const warn = (s) => pc.yellow('⚠') + ' ' + s;
70
+ const info = (s) => pc.cyan('→') + ' ' + s;
71
+ const dim = (s) => pc.dim(s);
72
+ const bold = (s) => pc.bold(s);
73
+
54
74
  const PACKAGE_ROOT = path.resolve(__dirname, '..');
55
75
  const SOURCE_ROOT = path.join(PACKAGE_ROOT, 'rihal');
56
76
 
77
+ // Zod schema for .rihal/config.yaml validation (#250).
78
+ const ConfigSchema = z.object({
79
+ user_name: z.string().min(1),
80
+ project_name: z.string().min(1),
81
+ communication_language: z.string().default('English'),
82
+ mode: z.enum(['guided', 'yolo'], {
83
+ errorMap: () => ({ message: 'expected "guided" or "yolo"' }),
84
+ }).default('guided'),
85
+ model_profile: z.string().optional(),
86
+ commit_planning: z.boolean().optional(),
87
+ rihal_source_path: z.string().optional(),
88
+ workflow: z.object({
89
+ research_by_default: z.boolean().optional(),
90
+ plan_checker: z.boolean().optional(),
91
+ post_execute_gates: z.boolean().optional(),
92
+ ui_safety_gate: z.boolean().optional(),
93
+ nyquist_validation: z.boolean().optional(),
94
+ }).optional(),
95
+ output: z.object({
96
+ verbose: z.boolean().optional(),
97
+ }).optional(),
98
+ git: z.object({
99
+ branching_strategy: z.string().optional(),
100
+ }).optional(),
101
+ }).passthrough();
102
+
57
103
  /**
58
104
  * Parse command-line args into a normalized options object.
59
105
  */
@@ -74,6 +120,16 @@ function parseArgs(argv) {
74
120
  // #189 — planning commit policy. null = ask interactively (or default true under --yes).
75
121
  // Set true by --commit-planning, false by --no-commit-planning or --ignore-planning.
76
122
  commitPlanning: null,
123
+ // #232 — non-destructive update. Preserves files the user modified after install.
124
+ nonDestructive: false,
125
+ // #232 — force-overwrite always wins.
126
+ forceOverwrite: false,
127
+ // #251 — diff display flags
128
+ showDiff: false,
129
+ diffStat: false,
130
+ acceptAll: false,
131
+ // #252 — skip update-notifier check
132
+ noUpdateCheck: false,
77
133
  };
78
134
  const positional = [];
79
135
  for (let i = 0; i < argv.length; i++) {
@@ -90,6 +146,12 @@ function parseArgs(argv) {
90
146
  else if (arg === '--module') opts.modules.push(argv[++i]);
91
147
  else if (arg === '--commit-planning') opts.commitPlanning = true;
92
148
  else if (arg === '--no-commit-planning' || arg === '--ignore-planning') opts.commitPlanning = false;
149
+ else if (arg === '--non-destructive') opts.nonDestructive = true;
150
+ else if (arg === '--force-overwrite') opts.forceOverwrite = true;
151
+ else if (arg === '--show-diff') opts.showDiff = true; // #251 full unified diff
152
+ else if (arg === '--diff-stat') opts.diffStat = true; // #251 +N -N summary (default)
153
+ else if (arg === '--accept-all') opts.acceptAll = true; // #251 overwrite all preserved
154
+ else if (arg === '--no-update-check') opts.noUpdateCheck = true; // #252
93
155
  else if (!arg.startsWith('--')) positional.push(arg);
94
156
  }
95
157
  if (positional[0]) {
@@ -188,17 +250,32 @@ function getPathsForIde(ide, target) {
188
250
  }
189
251
 
190
252
  /**
191
- * Recursively walk a directory and return absolute file paths.
253
+ * Walk a directory and return absolute file paths. Uses fast-glob so
254
+ * symlink cycles are never followed and patterns can be excluded via
255
+ * .rihalignore files (#249).
192
256
  */
193
- function walkFiles(dir) {
194
- const out = [];
195
- if (!fs.existsSync(dir)) return out;
196
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
197
- const full = path.join(dir, entry.name);
198
- if (entry.isDirectory()) out.push(...walkFiles(full));
199
- else if (entry.isFile()) out.push(full);
200
- }
201
- return out;
257
+ function walkFiles(dir, extraIgnore = []) {
258
+ if (!fs.existsSync(dir)) return [];
259
+ return fg.sync('**/*', {
260
+ cwd: dir,
261
+ dot: true,
262
+ onlyFiles: true,
263
+ followSymbolicLinks: false,
264
+ ignore: extraIgnore,
265
+ }).map((rel) => path.join(dir, rel));
266
+ }
267
+
268
+ /**
269
+ * Read .rihalignore patterns from a given root directory.
270
+ * Returns an array of glob-style ignore patterns (same syntax as .gitignore).
271
+ */
272
+ function readRihalIgnore(root) {
273
+ const ignoreFile = path.join(root, '.rihalignore');
274
+ if (!fs.existsSync(ignoreFile)) return [];
275
+ return fs.readFileSync(ignoreFile, 'utf8')
276
+ .split('\n')
277
+ .map((l) => l.trim())
278
+ .filter((l) => l && !l.startsWith('#'));
202
279
  }
203
280
 
204
281
  function sha256(buffer) {
@@ -817,13 +894,18 @@ function readPackageVersion() {
817
894
  function generateInstallManifest(opts) {
818
895
  const version = readPackageVersion();
819
896
  const newModules = opts.modules.length > 0 ? opts.modules : listAvailableModules();
820
- // Merge with existing manifest if present
897
+ // Merge with existing manifest if present; capture previous_version for rollback (#253).
821
898
  let existingModules = [];
899
+ let previousVersion = null;
822
900
  const existingPath = path.join(opts.target, '.rihal', '_config', 'manifest.yaml');
823
901
  if (fs.existsSync(existingPath)) {
824
902
  const text = fs.readFileSync(existingPath, 'utf8');
825
903
  let inModules = false;
826
904
  for (const line of text.split('\n')) {
905
+ if (line.startsWith('version:')) {
906
+ const v = line.replace('version:', '').trim();
907
+ if (semver.valid(v) && v !== version) previousVersion = v;
908
+ }
827
909
  if (line.startsWith('modules:')) { inModules = true; continue; }
828
910
  if (inModules && line.trim().startsWith('-')) { existingModules.push(line.trim().slice(1).trim()); }
829
911
  else if (inModules && !line.startsWith(' ')) { inModules = false; }
@@ -831,16 +913,14 @@ function generateInstallManifest(opts) {
831
913
  }
832
914
  const allModules = [...new Set([...existingModules, ...newModules])];
833
915
  const moduleLines = allModules.map((m) => ` - ${m}`).join('\n');
834
- return [
916
+ const lines = [
835
917
  '# Rihal v2 install manifest',
836
918
  `version: ${version}`,
837
919
  `installDate: ${new Date().toISOString()}`,
838
- 'modules:',
839
- moduleLines,
840
- 'ides:',
841
- ' - claude-code',
842
- '',
843
- ].join('\n');
920
+ ];
921
+ if (previousVersion) lines.push(`previous_version: ${previousVersion}`);
922
+ lines.push('modules:', moduleLines, 'ides:', ' - claude-code', '');
923
+ return lines.join('\n');
844
924
  }
845
925
 
846
926
  function sanitizeYamlValue(val) {
@@ -869,6 +949,57 @@ function generateConfigYaml(opts) {
869
949
  ].join('\n');
870
950
  }
871
951
 
952
+ /**
953
+ * Validate a parsed config.yaml object against ConfigSchema (#250).
954
+ * Returns { valid: true } or { valid: false, errors: string[] }.
955
+ */
956
+ function validateConfig(data) {
957
+ const result = ConfigSchema.safeParse(data);
958
+ if (result.success) return { valid: true };
959
+ const errors = result.error.issues.map((issue) => {
960
+ const field = issue.path.join('.');
961
+ return ` ${field || '(root)'}: ${issue.message}`;
962
+ });
963
+ return { valid: false, errors };
964
+ }
965
+
966
+ /**
967
+ * Parse a minimal YAML key:value file into a plain object.
968
+ * Only handles scalar values — sufficient for config.yaml.
969
+ */
970
+ function parseSimpleYaml(text) {
971
+ const obj = {};
972
+ let currentParent = null;
973
+ for (const raw of text.split('\n')) {
974
+ const line = raw.replace(/#.*$/, '');
975
+ if (!line.trim()) continue;
976
+ const indent = line.match(/^(\s*)/)[1].length;
977
+ if (indent === 0) {
978
+ const colonAt = line.indexOf(':');
979
+ if (colonAt === -1) continue;
980
+ const key = line.slice(0, colonAt).trim();
981
+ let val = line.slice(colonAt + 1).trim();
982
+ if (val === '') { currentParent = key; obj[key] = {}; continue; }
983
+ currentParent = null;
984
+ if (val.startsWith('"') && val.endsWith('"')) val = val.slice(1, -1);
985
+ if (val.startsWith("'") && val.endsWith("'")) val = val.slice(1, -1);
986
+ if (val === 'true') val = true;
987
+ else if (val === 'false') val = false;
988
+ obj[key] = val;
989
+ } else if (currentParent && indent > 0) {
990
+ const colonAt = line.indexOf(':');
991
+ if (colonAt === -1) continue;
992
+ const key = line.slice(0, colonAt).trim();
993
+ let val = line.slice(colonAt + 1).trim();
994
+ if (val.startsWith('"') && val.endsWith('"')) val = val.slice(1, -1);
995
+ if (val === 'true') val = true;
996
+ else if (val === 'false') val = false;
997
+ obj[currentParent][key] = val;
998
+ }
999
+ }
1000
+ return obj;
1001
+ }
1002
+
872
1003
  /**
873
1004
  * Convert a markdown command/agent file to Cursor's .mdc format.
874
1005
  * Wraps the file with Cursor-specific rules frontmatter.
@@ -889,21 +1020,24 @@ async function install(opts) {
889
1020
  // Resolve commit-planning preference (interactive prompt or flag) — #189.
890
1021
  opts.commitPlanning = await resolveCommitPlanning(opts);
891
1022
 
892
- console.log(`\n🕌 Rihal Code v${readPackageVersion()} installer → ${opts.target}`);
1023
+ const pkgVersion = readPackageVersion();
1024
+ console.log(`\n🕌 ${bold('Rihal Code')} ${pc.cyan('v' + pkgVersion)} ${dim('→')} ${opts.target}`);
893
1025
 
894
1026
  // Detect an existing install and surface it (#195).
895
1027
  const existingManifestPath = path.join(opts.target, '.rihal', '_config', 'manifest.yaml');
896
1028
  if (fs.existsSync(existingManifestPath)) {
897
1029
  const m = fs.readFileSync(existingManifestPath, 'utf8').match(/^version:\s*(.+)$/m);
898
1030
  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).`);
1031
+ const isUpgrade = semver.valid(existingVersion) && semver.valid(pkgVersion)
1032
+ ? semver.lt(existingVersion, pkgVersion)
1033
+ : existingVersion !== pkgVersion;
1034
+ if (isUpgrade) {
1035
+ console.log(' ' + info(`Upgrading ${pc.dim('v' + existingVersion)} → ${pc.green('v' + pkgVersion)} (config + state + .planning preserved)`));
902
1036
  } else {
903
- console.log(` Existing install at v${existingVersion} — upgrading to v${newVersion} (config + state + .planning preserved).`);
1037
+ console.log(' ' + info(`Refreshing v${existingVersion} (config + state + .planning preserved)`));
904
1038
  }
905
1039
  if (!opts.force) {
906
- console.log(' Pass --force to also sweep orphaned files from the previous version.');
1040
+ console.log(dim(' Pass --force to also sweep orphaned files from the previous version.'));
907
1041
  }
908
1042
  }
909
1043
  if (!fs.existsSync(SOURCE_ROOT)) {
@@ -971,46 +1105,96 @@ async function install(opts) {
971
1105
  sweptOrphans = sweepStaleInstalledFiles(opts.target, plan);
972
1106
  }
973
1107
 
974
- // Copy files
1108
+ // Load previous manifest for non-destructive mode (#232).
1109
+ // Map<rel, expectedHashFromPriorInstall> — if a file's current hash matches
1110
+ // its expected-from-prior-install hash, the user hasn't touched it → safe
1111
+ // to overwrite. If hashes differ, user customized it → preserve.
1112
+ const priorManifest = new Map();
1113
+ if (opts.nonDestructive) {
1114
+ const manifestPath = path.join(opts.target, '.rihal', '_config', 'files-manifest.csv');
1115
+ if (fs.existsSync(manifestPath)) {
1116
+ try {
1117
+ const lines = fs.readFileSync(manifestPath, 'utf8').split('\n').slice(1).filter(Boolean);
1118
+ for (const line of lines) {
1119
+ const [rel, hash] = line.split(',');
1120
+ if (rel && hash) priorManifest.set(rel, hash);
1121
+ }
1122
+ } catch {
1123
+ // best-effort — if manifest is malformed, fall back to behaving like fresh install
1124
+ }
1125
+ }
1126
+ }
1127
+
1128
+ // Copy files — spinner gives feedback on long installs (#248).
975
1129
  let copied = 0;
976
1130
  let skipped = 0;
1131
+ let preserved = 0;
1132
+ const preservedFiles = [];
1133
+ const preservedDiffs = []; // { rel, insertions, deletions, patch } for #251
1134
+ const spinner = createSpinner(dim(`Installing ${plan.length} files…`), { color: 'cyan' }).start();
1135
+
977
1136
  for (const entry of plan) {
978
1137
  const destPath = path.join(opts.target, entry.rel);
1138
+ const relForward = entry.rel.split(path.sep).join('/');
979
1139
  ensureDir(path.dirname(destPath));
980
- if (fs.existsSync(destPath) && !opts.force) {
1140
+
1141
+ // Non-destructive guard (#232): preserve user-modified files.
1142
+ // --accept-all (#251) overrides: treat all files as pristine.
1143
+ if (opts.nonDestructive && !opts.forceOverwrite && !opts.acceptAll && fs.existsSync(destPath)) {
1144
+ const priorHash = priorManifest.get(relForward);
1145
+ if (priorHash) {
1146
+ const installedContent = fs.readFileSync(destPath, 'utf8');
1147
+ const currentHash = sha256(Buffer.from(installedContent));
1148
+ if (currentHash !== priorHash) {
1149
+ // Compute diff stat for display (#251)
1150
+ const srcContent = fs.readFileSync(entry.src, 'utf8');
1151
+ const patch = createTwoFilesPatch(relForward, relForward, installedContent, srcContent, 'installed', 'source');
1152
+ let ins = 0, del = 0;
1153
+ for (const line of patch.split('\n')) {
1154
+ if (line.startsWith('+') && !line.startsWith('+++')) ins++;
1155
+ if (line.startsWith('-') && !line.startsWith('---')) del++;
1156
+ }
1157
+ preserved += 1;
1158
+ preservedFiles.push(relForward);
1159
+ preservedDiffs.push({ rel: relForward, insertions: ins, deletions: del, patch });
1160
+ skipped += 1;
1161
+ continue;
1162
+ }
1163
+ // Hash matches prior install → pristine → safe to overwrite
1164
+ }
1165
+ // No prior hash → new file in this plan → install normally
1166
+ }
1167
+
1168
+ if (fs.existsSync(destPath) && !opts.force && !opts.forceOverwrite) {
981
1169
  const existingHash = sha256(fs.readFileSync(destPath));
982
1170
  const sourceHash = sha256(fs.readFileSync(entry.src));
983
1171
  if (existingHash === sourceHash) { skipped++; continue; }
984
- if (!opts.yes) {
985
- console.warn(` ⚠ ${entry.rel} differs from package version — use --force to overwrite`);
1172
+ if (!opts.yes && !opts.nonDestructive) {
1173
+ spinner.stop();
1174
+ console.warn(' ' + warn(`${entry.rel} differs from package version — use --force-overwrite to overwrite`));
1175
+ spinner.start();
986
1176
  skipped++;
987
1177
  continue;
988
1178
  }
989
1179
  }
990
1180
 
991
- // Warn if overwriting modified file
992
- if (fs.existsSync(destPath) && opts.force) {
1181
+ if (fs.existsSync(destPath) && opts.forceOverwrite) {
993
1182
  const existing = fs.readFileSync(destPath);
994
1183
  const incoming = fs.readFileSync(entry.src);
995
1184
  if (!existing.equals(incoming)) {
996
- console.log(` ⚠ Overwriting modified file: ${destPath}`);
1185
+ spinner.update({ text: dim(`overwriting ${entry.rel}`) });
997
1186
  }
998
1187
  }
999
1188
 
1000
- // Read source file
1001
1189
  let content = fs.readFileSync(entry.src, 'utf8');
1002
-
1003
- // Convert to Cursor .mdc format if needed
1004
- if (entry.cursor) {
1005
- content = convertToCursorMdc(content);
1006
- }
1007
-
1008
- // Write to destination
1190
+ if (entry.cursor) content = convertToCursorMdc(content);
1009
1191
  fs.writeFileSync(destPath, content, 'utf8');
1010
1192
  if (entry.executable) fs.chmodSync(destPath, 0o755);
1011
1193
  copied++;
1012
1194
  }
1013
1195
 
1196
+ spinner.success({ text: ok(`${copied} files installed`) });
1197
+
1014
1198
  // Write .rihal/_config/manifest.yaml + agent-manifest.csv + files-manifest.csv
1015
1199
  const configDir = path.join(opts.target, '.rihal', '_config');
1016
1200
  ensureDir(configDir);
@@ -1038,6 +1222,18 @@ async function install(opts) {
1038
1222
  if (!fs.existsSync(configPath)) {
1039
1223
  fs.writeFileSync(configPath, generateConfigYaml(opts));
1040
1224
  }
1225
+ // Validate config.yaml with zod schema (#250) — warn but never block install.
1226
+ try {
1227
+ const configText = fs.readFileSync(configPath, 'utf8');
1228
+ const configData = parseSimpleYaml(configText);
1229
+ const validation = validateConfig(configData);
1230
+ if (!validation.valid) {
1231
+ console.log('');
1232
+ console.log(' ' + warn('config.yaml has validation errors:'));
1233
+ for (const e of validation.errors) console.log(pc.yellow(e));
1234
+ console.log(dim(' → Edit .rihal/config.yaml to fix, then run /rihal:status'));
1235
+ }
1236
+ } catch { /* best-effort */ }
1041
1237
 
1042
1238
  // Seed .rihal/state.json (skip if already exists — don't overwrite on re-install unless --reset)
1043
1239
  if (!fs.existsSync(stateDest)) {
@@ -1104,14 +1300,17 @@ async function install(opts) {
1104
1300
 
1105
1301
  // Summary
1106
1302
  console.log('');
1107
- console.log(` Files: ${copied} installed` + (opts.force && sweptOrphans > 0 ? `, ${sweptOrphans} stale swept` : ''));
1303
+ if (opts.force && sweptOrphans > 0) console.log(' ' + info(`${sweptOrphans} stale files swept`));
1304
+ if (opts.force && existedBefore) {
1305
+ console.log(' ' + warn('config.yaml and state.json preserved (pass --reset to wipe)'));
1306
+ }
1108
1307
  if (brainReport && brainReport.ok) {
1109
1308
  const pulledCount = (brainReport.pulled || []).length;
1110
1309
  const skippedCount = (brainReport.skipped || []).length;
1111
- console.log(` Brain: ${pulledCount} source${pulledCount === 1 ? '' : 's'} pulled` +
1112
- (skippedCount ? `, ${skippedCount} skipped (placeholder URLs — see issue #162)` : ''));
1310
+ console.log(' ' + ok(`Brain: ${pulledCount} source${pulledCount === 1 ? '' : 's'} pulled` +
1311
+ (skippedCount ? `, ${skippedCount} skipped (placeholder URLs)` : '')));
1113
1312
  } else if (brainReport && brainReport.error) {
1114
- console.log(` Brain: skipped (${brainReport.error})`);
1313
+ console.log(' ' + dim(`Brain: skipped (${brainReport.error})`));
1115
1314
  }
1116
1315
  if (gitignoreReport) {
1117
1316
  const gitMsg = {
@@ -1121,12 +1320,29 @@ async function install(opts) {
1121
1320
  'updated': '.gitignore rcode block refreshed',
1122
1321
  'skipped-error': `.gitignore skipped (${gitignoreReport.error})`,
1123
1322
  }[gitignoreReport.action] || '.gitignore unchanged';
1124
- console.log(` Gitignore: ${gitMsg}`);
1323
+ console.log(' ' + dim(gitMsg));
1125
1324
  }
1126
- if (skipped > 0) console.log(` Skipped: ${skipped} (already present, unchanged)`);
1127
- if (opts.force && existedBefore) {
1128
- console.log(' Preserved: .rihal/config.yaml and .rihal/state.json');
1129
- console.log(' Pass --reset to wipe and re-init those too.');
1325
+ if (skipped > 0) console.log(' ' + dim(`${skipped} files skipped (unchanged)`));
1326
+
1327
+ // Diff display for preserved files (#251)
1328
+ if (preserved > 0 && opts.nonDestructive) {
1329
+ console.log('');
1330
+ console.log(' ' + warn(`${preserved} file${preserved === 1 ? '' : 's'} preserved (modified since install):`));
1331
+ for (const d of preservedDiffs.slice(0, 10)) {
1332
+ const stat = pc.green(`+${d.insertions}`) + ' ' + pc.red(`-${d.deletions}`);
1333
+ console.log(` ${dim(d.rel)} ${stat}`);
1334
+ if (opts.showDiff && d.patch) {
1335
+ for (const line of d.patch.split('\n').slice(4)) { // skip file headers
1336
+ if (line.startsWith('+')) process.stdout.write(pc.green(line) + '\n');
1337
+ else if (line.startsWith('-')) process.stdout.write(pc.red(line) + '\n');
1338
+ else if (line.startsWith('@')) process.stdout.write(pc.cyan(line) + '\n');
1339
+ else process.stdout.write(dim(line) + '\n');
1340
+ }
1341
+ }
1342
+ }
1343
+ if (preservedDiffs.length > 10) console.log(dim(` … and ${preservedDiffs.length - 10} more`));
1344
+ console.log(dim(' To overwrite: re-run with --force-overwrite | To see full diffs: --show-diff'));
1345
+ console.log('');
1130
1346
  }
1131
1347
 
1132
1348
  // Count installed agents + commands dynamically (#190).
@@ -1142,41 +1358,57 @@ async function install(opts) {
1142
1358
  }
1143
1359
  } catch {}
1144
1360
 
1361
+ const version = readPackageVersion();
1145
1362
  console.log('');
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)`);
1363
+ console.log(` ${bold('Version:')} ${pc.cyan('@hanzlaa/rcode@' + version)}`);
1364
+ console.log(` ${bold('IDE:')} ${opts.ide}`);
1365
+ console.log(` ${bold('Language:')} ${opts.language} ${dim('(change in .rihal/config.yaml)')}`);
1366
+ console.log(` ${bold('Mode:')} ${opts.mode} ${dim('(guided=confirm at gates, yolo=autonomous)')}`);
1367
+ console.log(` ${bold('Planning:')} ${opts.commitPlanning !== false ? 'committed' : 'gitignored'} ${dim('(flip: rihal-tools gitignore refresh)')}`);
1152
1368
  console.log('');
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
- }
1369
+ console.log(` ${bold('Agents:')} ${pc.green(String(agentCount))} in .claude/agents/`);
1370
+ console.log(` ${bold('Commands:')} ${pc.green(String(commandCount))} slash commands in .claude/commands/rihal/`);
1371
+ if (skillsInstalled > 0) console.log(` ${bold('Skills:')} ${pc.green(String(skillsInstalled))} phrase-activated in .claude/skills/`);
1160
1372
  console.log('');
1161
1373
  if (starterSeeded) {
1162
- console.log(' Starter planning scaffolded in .planning/ (ROADMAP, STATE, PROJECT)');
1374
+ console.log(' ' + ok('Starter planning scaffolded in .planning/ (ROADMAP, STATE, PROJECT)'));
1163
1375
  console.log('');
1164
1376
  }
1165
- console.log(' Next:');
1377
+ console.log(` ${bold('Next:')}`);
1166
1378
  console.log(` cd ${opts.target}`);
1167
1379
  console.log(' claude # start Claude Code (reload window if already open)');
1168
1380
  console.log(' /rihal:progress # where you are, what\'s next');
1169
1381
  console.log(' /rihal:do # interactive command picker');
1170
1382
  console.log(' /rihal:council <q> # multi-agent strategic answer');
1171
1383
  console.log('');
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');
1384
+ console.log(dim(' Refresh anytime:'));
1385
+ console.log(dim(' npx @hanzlaa/rcode@latest install # pull the latest rcode + brain'));
1386
+ console.log(dim(` /rihal:update v${version} # pin rcode to a specific version`));
1175
1387
  console.log('');
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');
1388
+ console.log(' ' + warn('If your IDE is already open, reload the window to refresh skills/commands.'));
1389
+ console.log(dim(' Claude Code / VS Code / Cursor: Cmd+Shift+P → Reload Window'));
1178
1390
  console.log('');
1179
1391
 
1392
+ // Lightweight update check (#252) — async background, never blocks install.
1393
+ // Suppressed in non-TTY / CI or when --no-update-check is passed.
1394
+ if (!opts.noUpdateCheck && process.stdout.isTTY && !process.env.CI && !process.env.RIHAL_NO_UPDATE_NOTIFIER) {
1395
+ const { execFile } = require('child_process');
1396
+ execFile('npm', ['view', '@hanzlaa/rcode', 'version', '--json'], { timeout: 4000 }, (err, stdout) => {
1397
+ if (err) return;
1398
+ try {
1399
+ const latest = JSON.parse(stdout.trim());
1400
+ if (semver.valid(latest) && semver.gt(latest, version)) {
1401
+ console.log('');
1402
+ console.log(' ╭──────────────────────────────────────────────────────╮');
1403
+ console.log(` │ ${pc.yellow('Update available:')} ${pc.dim(version)} → ${pc.green(latest)}${' '.repeat(Math.max(0, 20 - version.length - latest.length))} │`);
1404
+ console.log(' │ Run: npx @hanzlaa/rcode@latest install . │');
1405
+ console.log(' ╰──────────────────────────────────────────────────────╯');
1406
+ console.log('');
1407
+ }
1408
+ } catch { /* ignore parse errors */ }
1409
+ });
1410
+ }
1411
+
1180
1412
  // Health check — smoke test that the install actually works (#193).
1181
1413
  const healthPass = runInstallHealthCheck(opts.target, { agentCount, commandCount, skillsInstalled });
1182
1414
  return healthPass ? 0 : 1;
@@ -1188,17 +1420,17 @@ async function install(opts) {
1188
1420
  * Prints a clean ✓/✖ line per check.
1189
1421
  */
1190
1422
  function runInstallHealthCheck(target, counts) {
1191
- console.log(' Health check:');
1423
+ console.log(` ${bold('Health check:')}`);
1192
1424
  const { execFileSync } = require('child_process');
1193
1425
  let fails = 0;
1194
1426
 
1195
1427
  function check(label, fn) {
1196
1428
  try {
1197
1429
  const out = fn();
1198
- console.log(` ${label}${out ? ' — ' + out : ''}`);
1430
+ console.log(` ${ok(label)}${out ? dim(' — ' + out) : ''}`);
1199
1431
  } catch (err) {
1200
1432
  fails += 1;
1201
- console.log(` ${label} — ${String(err.message || err).slice(0, 120)}`);
1433
+ console.log(` ${fail(label)} ${pc.red('')} ${String(err.message || err).slice(0, 120)}`);
1202
1434
  }
1203
1435
  }
1204
1436
 
@@ -1239,9 +1471,9 @@ function runInstallHealthCheck(target, counts) {
1239
1471
 
1240
1472
  if (fails > 0) {
1241
1473
  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');
1474
+ console.log(' ' + fail(`${fails} health check${fails === 1 ? '' : 's'} failed — install may be broken.`));
1475
+ console.log(dim(' Debug: node .rihal/bin/rihal-tools.cjs state read && ls -la .rihal/'));
1476
+ console.log(dim(' Reinstall: npx @hanzlaa/rcode install . --force'));
1245
1477
  console.log('');
1246
1478
  return false;
1247
1479
  }
@@ -11,8 +11,8 @@ console.log(`
11
11
  🕌 Rihal Code installed.
12
12
 
13
13
  First-time setup:
14
- npx @hanzlahabib/rihal-code install # set up agents + slash commands
15
- npx @hanzlahabib/rihal-code tiers # see the tier map
14
+ npx @hanzlaa/rcode install # set up agents + slash commands
15
+ npx @hanzlaa/rcode tiers # see the tier map
16
16
 
17
17
  🌱 The Golden Path (say these phrases in your AI IDE):
18
18
  1. "scaffold a new project" → rihal-scaffold-project
@@ -24,8 +24,8 @@ First-time setup:
24
24
  7. "sprint status" → rihal-sprint-status
25
25
 
26
26
  More:
27
- npx @hanzlahabib/rihal-code help # all commands (grouped)
28
- npx @hanzlahabib/rihal-code dashboard # view-only Diwan on :7717
27
+ npx @hanzlaa/rcode help # all commands (grouped)
28
+ npx @hanzlaa/rcode dashboard # view-only Diwan on :7717
29
29
 
30
30
  Docs: https://github.com/hanzlahabib/rihal-code
31
31
  Tiers: docs/TIERS.md · Standards: docs/STANDARDS.md
package/cli/uninstall.js CHANGED
@@ -549,6 +549,14 @@ async function runUninstall(args) {
549
549
  '.antigravity',
550
550
  ]);
551
551
 
552
+ // Always remove .rihal/brain/ — it's pulled rcode content (issue #202),
553
+ // not user data. Refreshed by `brain pull` on next install.
554
+ const brainDir = path.join(cwd, '.rihal', 'brain');
555
+ if (fs.existsSync(brainDir)) {
556
+ fs.rmSync(brainDir, { recursive: true, force: true });
557
+ console.log(` ✓ removed .rihal/brain/ (pulled content, will refresh on reinstall)`);
558
+ }
559
+
552
560
  // Handle .rihal/ state directory
553
561
  if (plan.stateDir) {
554
562
  const rihalDir = path.join(cwd, '.rihal');