@hanzlaa/rcode 4.0.0 → 4.1.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 (29) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +2 -2
  3. package/cli/doctor.js +17 -0
  4. package/cli/github-sync.js +3 -2
  5. package/cli/lib/manifest.cjs +13 -0
  6. package/cli/set-mode.js +10 -0
  7. package/cli/set-profile.js +10 -0
  8. package/cli/uninstall.js +100 -39
  9. package/dist/rcode.js +215 -213
  10. package/package.json +1 -1
  11. package/rcode/skills/SKILLS_INDEX.md +4 -3
  12. package/rcode/skills/actions/1-analysis/rcode-document-project/SKILL.md +6 -0
  13. package/rcode/skills/actions/3-solutioning/rcode-check-implementation-readiness/SKILL.md +6 -0
  14. package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/SKILL.md +162 -0
  15. package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/references.md +136 -0
  16. package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/rules/backlog-building.md +113 -0
  17. package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/rules/composition-with-herdr.md +85 -0
  18. package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/rules/integration-branch.md +191 -0
  19. package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/rules/merge-strategy.md +113 -0
  20. package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/rules/orchestrator-rhythm.md +119 -0
  21. package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/rules/wave-design.md +100 -0
  22. package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/templates/BACKLOG-template.md +34 -0
  23. package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/templates/STATE-template.md +40 -0
  24. package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/templates/heartbeat.sh +29 -0
  25. package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/templates/wave-prompt.md +69 -0
  26. package/rcode/templates/sprint.md +16 -0
  27. package/rcode/workflows/plan-spawn-planner.md +96 -0
  28. package/rcode/workflows/plan.md +67 -0
  29. package/server/dashboard.js +2 -2
package/AGENTS.md CHANGED
@@ -48,7 +48,7 @@ If a user says "just keep going" or "don't stop until done", that authorization
48
48
 
49
49
  ## Naming & Branding (per `BRAND.md`)
50
50
 
51
- - **Skill names** in frontmatter: `rcode-<verb>-<noun>` for legacy skills; new branded skills use `rcode-<verb>-<noun>` ONLY in slash command surface (`/rcode:<name>`); folder names stay `rcode-*` because `cli/install.js` hardcodes that prefix.
51
+ - **Skill names** in frontmatter: `rcode-<verb>-<noun>` for legacy skills; new branded skills use `rcode-<verb>-<noun>` ONLY in slash command surface (`/rcode-<name>`); folder names stay `rcode-*` because `cli/install.js` hardcodes that prefix.
52
52
  - **Persona IDs** in `team.yaml` stay `rcode-<name>` (dashboard scanner reads them by id; renaming breaks rendering).
53
53
  - **Persona display names** keep Arabic alongside Latin: `Sadiq (صادق)`, `Dalil (دليل)`, etc.
54
54
  - **Concept primitives** (Memory Bank, Distillate, Majlis, Diwan) are named tooling — capitalised, used consistently in user-facing copy.
package/README.md CHANGED
@@ -13,7 +13,7 @@ pnpm dlx @hanzlaa/rcode install
13
13
  [![CI](https://github.com/hanzlahabib/rihal-code/actions/workflows/test.yml/badge.svg)](https://github.com/hanzlahabib/rihal-code/actions/workflows/test.yml)
14
14
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
15
15
 
16
- Status: `@hanzlaa/rcode` v4.0.0 on npm. 339 automated tests across 58 files, 45 agents, 116 commands, 86 skills. Actively dogfooded on real projects every week.
16
+ Status: `@hanzlaa/rcode` v4.0.0 on npm. 457 automated tests across 63 files, 45 agents, 116 commands, 87 skills. Actively dogfooded on real projects every week.
17
17
 
18
18
  ---
19
19
 
@@ -42,7 +42,7 @@ Three layers, specialised for software delivery:
42
42
  | Layer | What lives here | Example |
43
43
  |-------|-----------------|---------|
44
44
  | **Memory** | `.rcode/memory/` — git-tracked markdown, lossless distillates | "We chose Postgres over Mongo because of JSON-B + RLS — see ADR-007" |
45
- | **Skills** | `rcode/skills/` — 85 phrase-activated playbooks | `rcode-sprint-checker` validates file/symbol refs before execute |
45
+ | **Skills** | `rcode/skills/` — 87 phrase-activated playbooks | `rcode-sprint-checker` validates file/symbol refs before execute |
46
46
  | **Workflows** | `rcode/workflows/` — orchestrated multi-step paths | `/rcode-plan` runs research → planner → checker → confirm |
47
47
 
48
48
  Single agent navigates the structure. No LangChain, no AutoGen, no orchestrator process. Just folders the model can read.
package/cli/doctor.js CHANGED
@@ -52,13 +52,30 @@ function findAgentFiles(dir) {
52
52
  .map((e) => path.join(dir, e.name));
53
53
  }
54
54
 
55
+ // Negative-boundary signal — an explicit statement of what a skill does NOT do.
56
+ // Mirrors the same constant in cli/lib/schemas.cjs so both checks stay in sync.
57
+ const NEGATIVE_BOUNDARY_RE = /not for|do not|does not|don't|never\b|audit-only|negative/i;
58
+
55
59
  function checkCompliance(filePath) {
56
60
  const content = fs.readFileSync(filePath, 'utf8');
61
+ const { frontmatter, body } = parseFrontmatter(content);
57
62
  const missing = [];
58
63
  if (!/^name:/m.test(content)) missing.push('name');
59
64
  if (!/^description:/m.test(content)) missing.push('description');
65
+ if (!/^## Overview/m.test(content)) missing.push('Overview section');
60
66
  if (!/^## Output Format/m.test(content)) missing.push('Output Format');
61
67
  if (!/^## Examples/m.test(content)) missing.push('Examples');
68
+
69
+ // Negative-boundary clause (component 1 of the 5-component standard).
70
+ // Parse the frontmatter description so folded-block YAML is normalized
71
+ // before the regex runs — "Do\n NOT" becomes "Do NOT" after normalization.
72
+ const desc = typeof frontmatter.description === 'string' ? frontmatter.description : '';
73
+ const hasBoundary =
74
+ NEGATIVE_BOUNDARY_RE.test(desc) ||
75
+ /##[^\n]*\bnot\b/i.test(body) ||
76
+ /\bdo not (use|include)\b/i.test(body);
77
+ if (!hasBoundary) missing.push('negative-boundary clause');
78
+
62
79
  return missing;
63
80
  }
64
81
 
@@ -481,8 +481,9 @@ async function main(args) {
481
481
  console.error(` Check the filter value or run without filters to see available ids.`);
482
482
  process.exit(1);
483
483
  }
484
- console.error(`❌ No phases found in .rcode/phases/.`);
485
- process.exit(1);
484
+ console.log(`ℹ No phases found in .rcode/phases/ — nothing to sync.`);
485
+ console.log(` Run 'rcode init' or create a phase to get started.`);
486
+ process.exit(0);
486
487
  }
487
488
 
488
489
  console.log(` ✓ Phases found: ${phases.length}`);
@@ -47,12 +47,25 @@ function readPackageManifest(packageRoot) {
47
47
  // Mirror installSkills() walkForSkills: recurse into action bucket dirs
48
48
  // (1-analysis, 2-plan, etc.) until a dir with SKILL.md is found, then add
49
49
  // the dir name as installed. Bucket dirs themselves are never installed.
50
+ // Issue #873: skills with `internal: true` in frontmatter are installed to
51
+ // .rcode/skills/ (not .claude/skills/), so omit them from manifest.actions
52
+ // to avoid false drift reports.
53
+ function isInternalSkill(skillDir) {
54
+ try {
55
+ const text = fs.readFileSync(path.join(skillDir, 'SKILL.md'), 'utf8');
56
+ return /^internal:\s*true\s*$/m.test(text);
57
+ } catch {
58
+ return false;
59
+ }
60
+ }
61
+
50
62
  function walkActions(dir) {
51
63
  if (!fs.existsSync(dir)) return;
52
64
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
53
65
  if (!entry.isDirectory()) continue;
54
66
  const full = path.join(dir, entry.name);
55
67
  if (fs.existsSync(path.join(full, 'SKILL.md'))) {
68
+ if (isInternalSkill(full)) continue; // internal → .rcode/skills/, not .claude/skills/
56
69
  // Use the name as it lands in .claude/skills/ (installSkills prefixes
57
70
  // non-rcode- dirs with 'rcode-', but all current skills already have it)
58
71
  const installedName = entry.name.startsWith('rcode-')
package/cli/set-mode.js CHANGED
@@ -51,6 +51,16 @@ module.exports = function setMode(args) {
51
51
 
52
52
  const requested = args[0];
53
53
 
54
+ if (requested === '--help' || requested === '-h') {
55
+ console.log(`Usage: rcode set-mode [<mode>]`);
56
+ console.log();
57
+ console.log(` rcode set-mode show current mode + explanation`);
58
+ console.log(` rcode set-mode <mode> switch to mode <mode>`);
59
+ console.log();
60
+ console.log(`Available modes: ${[...VALID_COMMUNICATION_MODES].join(', ')}`);
61
+ process.exit(0);
62
+ }
63
+
54
64
  if (!requested) {
55
65
  // Show current + available modes
56
66
  const config = loadConfig(cwd);
@@ -37,6 +37,16 @@ module.exports = function setProfile(args) {
37
37
  const requested = args[0];
38
38
  const available = listProfiles();
39
39
 
40
+ if (requested === '--help' || requested === '-h') {
41
+ console.log(`Usage: rcode set-profile [<name>]`);
42
+ console.log();
43
+ console.log(` rcode set-profile show current profile + available options`);
44
+ console.log(` rcode set-profile <name> switch to profile <name>`);
45
+ console.log();
46
+ console.log(`Available profiles: ${available.join(', ')}`);
47
+ process.exit(0);
48
+ }
49
+
40
50
  if (!requested) {
41
51
  // Show current profile + available options
42
52
  const current = getProjectProfile(cwd);
package/cli/uninstall.js CHANGED
@@ -87,6 +87,47 @@ function stripRcodeGitignoreBlock(text) {
87
87
  .replace(/\n{3,}/g, '\n\n');
88
88
  }
89
89
 
90
+ /**
91
+ * Strip the rcode-managed block from .git/hooks/pre-commit.
92
+ * Removes the file entirely when only the shebang + rcode block remain.
93
+ * Returns 'removed' | 'stripped' | 'unchanged' | 'skipped'.
94
+ */
95
+ function cleanRcodePreCommitHook(cwd) {
96
+ const hookPath = path.join(cwd, '.git', 'hooks', 'pre-commit');
97
+ if (!fs.existsSync(hookPath)) return 'skipped';
98
+
99
+ const BEGIN = '# ===== rcode-managed pre-commit block =====';
100
+ const END = '# ===== end rcode pre-commit block =====';
101
+
102
+ let content;
103
+ try { content = fs.readFileSync(hookPath, 'utf8'); } catch { return 'skipped'; }
104
+
105
+ if (!content.includes(BEGIN)) return 'unchanged';
106
+
107
+ const startIdx = content.indexOf(BEGIN);
108
+ const endIdx = content.indexOf(END, startIdx);
109
+ if (endIdx < 0) return 'unchanged'; // malformed — leave it
110
+
111
+ // Trim the newline that precedes BEGIN and the newline that follows END
112
+ let lo = startIdx;
113
+ if (lo > 0 && content[lo - 1] === '\n') lo--;
114
+ let hi = endIdx + END.length;
115
+ if (hi < content.length && content[hi] === '\n') hi++;
116
+
117
+ const stripped = content.slice(0, lo) + content.slice(hi);
118
+
119
+ // If only a shebang (or blank) remains, remove the whole file
120
+ const remnant = stripped.trim();
121
+ if (remnant === '' || remnant === '#!/bin/sh' || remnant === '#!/bin/bash') {
122
+ try { fs.unlinkSync(hookPath); return 'removed'; } catch { return 'skipped'; }
123
+ }
124
+
125
+ try {
126
+ writeFileAtomic(hookPath, stripped, { mode: 0o755 });
127
+ return 'stripped';
128
+ } catch { return 'skipped'; }
129
+ }
130
+
90
131
  /**
91
132
  * Walk a directory and remove all files/subdirs whose name matches a predicate.
92
133
  * Returns the number of entries removed. Always skips local overrides (#382).
@@ -148,7 +189,7 @@ function cleanupEmptyDirs(cwd, relPaths) {
148
189
  */
149
190
  function buildPlan(cwd, editors) {
150
191
  const plan = {
151
- claude: { skills: [], commands: [], agents: [] },
192
+ claude: { skills: [], commands: [], agents: [], agentsRulesDir: false },
152
193
  cursor: [],
153
194
  windsurf: [],
154
195
  antigravity: [],
@@ -194,6 +235,10 @@ function buildPlan(cwd, editors) {
194
235
  .readdirSync(agentsDir)
195
236
  .filter((name) => name.startsWith('rcode-') && name.endsWith('.md'));
196
237
  }
238
+ // Installer copies rcode/agents/rules/ tree → .claude/agents/rules/ (#876)
239
+ if (fs.existsSync(path.join(cwd, '.claude/agents/rules'))) {
240
+ plan.claude.agentsRulesDir = true;
241
+ }
197
242
  }
198
243
 
199
244
  if (editors.includes('cursor')) {
@@ -201,7 +246,8 @@ function buildPlan(cwd, editors) {
201
246
  if (fs.existsSync(cursorDir)) {
202
247
  plan.cursor = fs
203
248
  .readdirSync(cursorDir)
204
- .filter((name) => name.startsWith('rcode-') || name === 'rcode.mdc' || name === 'rcode-method.mdc');
249
+ // 'rcode' matches the .cursor/rules/rcode/ subdir installed by the cursor IDE path (#876)
250
+ .filter((name) => name.startsWith('rcode-') || name === 'rcode.mdc' || name === 'rcode-method.mdc' || name === 'rcode');
205
251
  }
206
252
  }
207
253
 
@@ -331,6 +377,9 @@ function planToPathList(plan, cwd, options = {}) {
331
377
  for (const name of plan.claude.agents) {
332
378
  paths.push(path.join('.claude/agents', name));
333
379
  }
380
+ if (plan.claude.agentsRulesDir) {
381
+ paths.push('.claude/agents/rules');
382
+ }
334
383
  for (const name of plan.cursor) {
335
384
  paths.push(path.join('.cursor/rules', name));
336
385
  }
@@ -650,6 +699,17 @@ async function runUninstall(args) {
650
699
  removed += nAgents;
651
700
  if (nAgents > 0) console.log(` ✓ removed ${nAgents} Claude agents`);
652
701
 
702
+ // .claude/agents/rules/ — installed by the agent-rules sub-tree (#876)
703
+ if (plan.claude.agentsRulesDir) {
704
+ const rulesDir = path.join(cwd, '.claude/agents/rules');
705
+ const r = safeRmSync(rulesDir, path.resolve(cwd));
706
+ if (r.ok && r.reason !== 'missing') {
707
+ console.log(` ✓ removed .claude/agents/rules/ (agent reference rules)`);
708
+ } else if (r.reason === 'outside-root') {
709
+ console.log(` ⚠ refused to remove .claude/agents/rules/ — symlink resolves outside project root`);
710
+ }
711
+ }
712
+
653
713
  // Clean up now-empty .claude/commands and .claude/agents dirs
654
714
  try {
655
715
  if (fs.existsSync(path.join(cwd, '.claude/commands')) && fs.readdirSync(path.join(cwd, '.claude/commands')).length === 0) {
@@ -664,7 +724,7 @@ async function runUninstall(args) {
664
724
  if (editors.includes('cursor')) {
665
725
  const cursorDir = path.join(cwd, '.cursor/rules');
666
726
  const n = removeMatching(cursorDir, (name) =>
667
- name.startsWith('rcode-') || name === 'rcode.mdc' || name === 'rcode-method.mdc',
727
+ name.startsWith('rcode-') || name === 'rcode.mdc' || name === 'rcode-method.mdc' || name === 'rcode',
668
728
  );
669
729
  removed += n;
670
730
  if (n > 0) console.log(` ✓ removed ${n} Cursor rules`);
@@ -719,6 +779,29 @@ async function runUninstall(args) {
719
779
  }
720
780
  }
721
781
 
782
+ // Strip the rcode block from .gitignore — always, not just on --purge (#876)
783
+ const gitignorePath = path.join(cwd, '.gitignore');
784
+ if (fs.existsSync(gitignorePath)) {
785
+ try {
786
+ const before = fs.readFileSync(gitignorePath, 'utf8');
787
+ const after = stripRcodeGitignoreBlock(before);
788
+ if (after !== before) {
789
+ fs.writeFileSync(gitignorePath, after);
790
+ console.log(` ✓ stripped rcode block from .gitignore`);
791
+ }
792
+ } catch (err) {
793
+ console.log(` ⚠ could not strip .gitignore block: ${err.message}`);
794
+ }
795
+ }
796
+
797
+ // Remove .git/hooks/pre-commit rcode block (or the whole file if rcode-only) (#876)
798
+ const hookResult = cleanRcodePreCommitHook(cwd);
799
+ if (hookResult === 'removed') {
800
+ console.log(` ✓ removed .git/hooks/pre-commit (was rcode-only)`);
801
+ } else if (hookResult === 'stripped') {
802
+ console.log(` ✓ stripped rcode block from .git/hooks/pre-commit`);
803
+ }
804
+
722
805
  // Cleanup empty editor directories left behind after removing rcode-*
723
806
  // entries. Only removes dirs that are COMPLETELY empty — never touches
724
807
  // user content. Order matters: innermost first so each parent gets a
@@ -784,7 +867,7 @@ async function runUninstall(args) {
784
867
  }
785
868
  }
786
869
 
787
- // --purge: also wipe .planning/ artifacts and the rcode .gitignore block.
870
+ // --purge: also wipe .planning/ artifacts (user project data beyond .rcode/).
788
871
  // Without this, "uninstall + reinstall" carries forward stale phases /
789
872
  // sprints / SUMMARY files even after .rcode/ is gone.
790
873
  if (opts.purge) {
@@ -799,36 +882,6 @@ async function runUninstall(args) {
799
882
  console.log(` ⚠ could not remove .planning/: ${r.reason}`);
800
883
  }
801
884
  }
802
-
803
- // Strip the rcode-managed block from .gitignore. The installer writes
804
- // a fenced block; we remove it cleanly without touching user lines.
805
- //
806
- // Issue #684: previous regex `/\n?# rcode[\s\S]*?(?=\n\n|\n$|$)/g` was a
807
- // footgun — it matched ANY user line starting with "# rcode" (e.g.
808
- // "# rcode notes", "# rcode is great") and greedily consumed everything
809
- // up to the next blank line, silently nuking user content.
810
- //
811
- // Three shapes have ever shipped:
812
- // 1. Current (install.js:653-654): "# ===== rcode-managed gitignore block ... =====" ... "# ===== end rcode-managed gitignore block ====="
813
- // 2. Old fenced markers: "# >>> rcode >>>" ... "# <<< rcode <<<"
814
- // 3. Hypothetical legacy single-line "# rcode" — never actually
815
- // committed by any installer version we can find. Removed.
816
- //
817
- // Both kept patterns require BOTH sentinel markers to be present —
818
- // user content with "# rcode" prefix is now safe.
819
- const gitignorePath = path.join(cwd, '.gitignore');
820
- if (fs.existsSync(gitignorePath)) {
821
- try {
822
- const before = fs.readFileSync(gitignorePath, 'utf8');
823
- const stripped = stripRcodeGitignoreBlock(before);
824
- if (stripped !== before) {
825
- fs.writeFileSync(gitignorePath, stripped);
826
- console.log(` ✓ stripped rcode block from .gitignore (--purge)`);
827
- }
828
- } catch (err) {
829
- console.log(` ⚠ could not strip .gitignore block: ${err.message}`);
830
- }
831
- }
832
885
  }
833
886
 
834
887
  console.log(`\n✅ Uninstall complete. Removed ${removed} files.`);
@@ -836,12 +889,20 @@ async function runUninstall(args) {
836
889
  console.log(` Backup: ${backup.path} (restore with: tar -xzf ${backup.path})`);
837
890
  }
838
891
 
839
- // Hint about the purge flag if the user kept state closes the user's
840
- // most common confusion: "I uninstalled but /rcode-init still says configured."
841
- if (plan.stateDir && fs.existsSync(path.join(cwd, '.rcode'))) {
892
+ // Notice about what was intentionally preserved (#876 never delete user data silently)
893
+ const rcodeStillExists = plan.stateDir && fs.existsSync(path.join(cwd, '.rcode'));
894
+ const planningStillExists = !opts.purge && fs.existsSync(path.join(cwd, '.planning'));
895
+ if (rcodeStillExists || planningStillExists) {
842
896
  console.log();
843
- console.log(`ℹ .rcode/ state was preserved. /rcode-init will detect this on reinstall.`);
844
- console.log(` For a fully clean slate next time, use: rcode uninstall --purge`);
897
+ console.log(`ℹ Preserved (your project data not removed by default):`);
898
+ if (rcodeStillExists) {
899
+ console.log(` .rcode/ phases, decisions, progress, config`);
900
+ console.log(` /rcode-init will detect this on reinstall`);
901
+ }
902
+ if (planningStillExists) {
903
+ console.log(` .planning/ planning scaffolds (ROADMAP, STATE, PROJECT)`);
904
+ }
905
+ console.log(` To remove these on next uninstall: rcode uninstall --purge`);
845
906
  }
846
907
 
847
908
  // IDE cache reload hint — Claude Code caches the slash-command list in memory.