@hanzlaa/rcode 2.7.0 → 2.7.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.
package/cli/install.js CHANGED
@@ -1025,12 +1025,18 @@ function sweepStaleInstalledFiles(target, newPlan) {
1025
1025
  const newRelsSet = new Set(newPlan.map(e => e.rel.split(path.sep).join('/')));
1026
1026
  // Safety — never sweep these, even if they somehow landed in the manifest.
1027
1027
  const neverSweep = /^(\.rihal\/config\.yaml|\.rihal\/state\.json|\.rihal\/state\.json\.lock|\.planning\/|\.rihal\/brain\/sources\.yaml)/;
1028
+ // #382 — local overrides: files matching <name>.local.md are user-managed.
1029
+ // The installer never touches them: not in copy, not in sweep, not even on
1030
+ // --force-overwrite. This gives users a stable path to customize agent
1031
+ // voice / examples / project-specific rules without losing them on update.
1032
+ const isLocalOverride = (rel) => /\.local\.(md|mdc|json|yaml|yml|toml|js|ts)$/.test(rel);
1028
1033
 
1029
1034
  let removed = 0;
1030
1035
  const emptyCandidateDirs = new Set();
1031
1036
  for (const rel of oldRels) {
1032
1037
  if (newRelsSet.has(rel)) continue;
1033
1038
  if (neverSweep.test(rel)) continue;
1039
+ if (isLocalOverride(rel)) continue; // #382 — never sweep user-owned overrides
1034
1040
  const full = path.join(target, rel);
1035
1041
  try {
1036
1042
  if (fs.existsSync(full)) {
@@ -1603,6 +1609,10 @@ async function install(opts) {
1603
1609
  console.log(dim(' npx @hanzlaa/rcode@latest install # pull the latest rcode + brain'));
1604
1610
  console.log(dim(` /rihal:update v${version} # pin rcode to a specific version`));
1605
1611
  console.log('');
1612
+ console.log(dim(' Customize without losing changes on update:'));
1613
+ console.log(dim(' Create <name>.local.md siblings (e.g. .claude/agents/rihal-waleed.local.md)'));
1614
+ console.log(dim(' *.local.md files are NEVER touched by install / --force-overwrite / uninstall.'));
1615
+ console.log('');
1606
1616
  console.log(' ' + warn('If your IDE is already open, reload the window to refresh skills/commands.'));
1607
1617
  console.log(dim(' Claude Code / VS Code / Cursor: Cmd+Shift+P → Reload Window'));
1608
1618
  console.log('');
package/cli/uninstall.js CHANGED
@@ -57,14 +57,26 @@ function parseArgs(args) {
57
57
  return opts;
58
58
  }
59
59
 
60
+ /**
61
+ * #382 — Local overrides: files matching <name>.local.md (or .local.mdc /
62
+ * .local.json / etc.) are user-managed. The uninstaller never removes them
63
+ * — they survive both regular uninstall AND --purge. Users can customize
64
+ * an agent voice / skill / command by creating a .local.md sibling, knowing
65
+ * it'll persist across updates and uninstalls.
66
+ */
67
+ function isLocalOverride(name) {
68
+ return /\.local\.(md|mdc|json|yaml|yml|toml|js|ts)$/.test(name);
69
+ }
70
+
60
71
  /**
61
72
  * Walk a directory and remove all files/subdirs whose name matches a predicate.
62
- * Returns the number of entries removed.
73
+ * Returns the number of entries removed. Always skips local overrides (#382).
63
74
  */
64
75
  function removeMatching(dir, predicate) {
65
76
  if (!fs.existsSync(dir)) return 0;
66
77
  let count = 0;
67
78
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
79
+ if (isLocalOverride(entry.name)) continue; // #382 — never remove user overrides
68
80
  if (!predicate(entry.name)) continue;
69
81
  const full = path.join(dir, entry.name);
70
82
  fs.rmSync(full, { recursive: true, force: true });
package/dist/rcode.js CHANGED
@@ -16530,11 +16530,13 @@ Say "plan a sprint" or run \`/rihal:sprint-planning\` to break Phase 01 into sto
16530
16530
  }
16531
16531
  const newRelsSet = new Set(newPlan.map((e) => e.rel.split(path2.sep).join("/")));
16532
16532
  const neverSweep = /^(\.rihal\/config\.yaml|\.rihal\/state\.json|\.rihal\/state\.json\.lock|\.planning\/|\.rihal\/brain\/sources\.yaml)/;
16533
+ const isLocalOverride = (rel) => /\.local\.(md|mdc|json|yaml|yml|toml|js|ts)$/.test(rel);
16533
16534
  let removed = 0;
16534
16535
  const emptyCandidateDirs = /* @__PURE__ */ new Set();
16535
16536
  for (const rel of oldRels) {
16536
16537
  if (newRelsSet.has(rel)) continue;
16537
16538
  if (neverSweep.test(rel)) continue;
16539
+ if (isLocalOverride(rel)) continue;
16538
16540
  const full = path2.join(target, rel);
16539
16541
  try {
16540
16542
  if (fs2.existsSync(full)) {
@@ -17005,6 +17007,10 @@ Say "plan a sprint" or run \`/rihal:sprint-planning\` to break Phase 01 into sto
17005
17007
  console.log(dim(" npx @hanzlaa/rcode@latest install # pull the latest rcode + brain"));
17006
17008
  console.log(dim(` /rihal:update v${version} # pin rcode to a specific version`));
17007
17009
  console.log("");
17010
+ console.log(dim(" Customize without losing changes on update:"));
17011
+ console.log(dim(" Create <name>.local.md siblings (e.g. .claude/agents/rihal-waleed.local.md)"));
17012
+ console.log(dim(" *.local.md files are NEVER touched by install / --force-overwrite / uninstall."));
17013
+ console.log("");
17008
17014
  console.log(" " + warn("If your IDE is already open, reload the window to refresh skills/commands."));
17009
17015
  console.log(dim(" Claude Code / VS Code / Cursor: Cmd+Shift+P \u2192 Reload Window"));
17010
17016
  console.log("");
@@ -17953,10 +17959,14 @@ var require_uninstall = __commonJS({
17953
17959
  }
17954
17960
  return opts;
17955
17961
  }
17962
+ function isLocalOverride(name) {
17963
+ return /\.local\.(md|mdc|json|yaml|yml|toml|js|ts)$/.test(name);
17964
+ }
17956
17965
  function removeMatching(dir, predicate) {
17957
17966
  if (!fs2.existsSync(dir)) return 0;
17958
17967
  let count = 0;
17959
17968
  for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
17969
+ if (isLocalOverride(entry.name)) continue;
17960
17970
  if (!predicate(entry.name)) continue;
17961
17971
  const full = path2.join(dir, entry.name);
17962
17972
  fs2.rmSync(full, { recursive: true, force: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzlaa/rcode",
3
- "version": "2.7.0",
3
+ "version": "2.7.2",
4
4
  "description": "Rihal Code (rcode) — installable context-brain for Rihalians. 43 agents, 99 slash commands, 56 skills, pullable Rihal standards. Unified install for Claude Code, Cursor, and Gemini.",
5
5
  "main": "cli/index.js",
6
6
  "bin": {
@@ -28,6 +28,56 @@ fi
28
28
  ```
29
29
  </step>
30
30
 
31
+ <step name="persona_shortcut" priority="first-match">
32
+ **Recognize `@persona CODE` shortcuts as the deterministic API surface.**
33
+
34
+ Every Rihal persona file has a Capabilities table listing 2-3-letter codes (Waleed: ADR/RV/TS/FZ/KS · Hussain-PM: CP/VP/EP/CE/CS/IR/CC · Mariam: MR/ICP/GTM/POS/LP · Fatima: TS/RG/EC/RR/RP/FT · Hanzla: DS/IS/BF/RF/KA/CR · Sadiq: KC/OC/PT/MT/KS · Dalil: SC/MC/RF/TS · Khattat / Munaffidh / Bahith / Muhaqqiq similarly).
35
+
36
+ Match if `$QUESTION` starts with `@<persona> <CODE>` or `@<persona>:<CODE>` — case-insensitive on persona, codes uppercase. Examples:
37
+
38
+ - `@hussain CP` → dispatch to Hussain-PM with capability `CP` (Create PRD via interview)
39
+ - `@waleed ADR` → dispatch to Waleed with capability `ADR` (write an ADR)
40
+ - `@fatima RG` → dispatch to Fatima with capability `RG` (release-gate review)
41
+ - `@dalil SC --topic "Sentry"` → dispatch to Dalil with capability `SC` (lightweight scan, topic phrase passed)
42
+
43
+ **Persona-name aliases** (lowercased, common nicknames):
44
+ | Alias | Resolves to | Agent file |
45
+ |---|---|---|
46
+ | `sadiq`, `strategy`, `director` | Sadiq | rihal-sadiq |
47
+ | `waleed`, `cto`, `architect` | Waleed | rihal-waleed |
48
+ | `hussain`, `hussain-pm`, `pm` | Hussain | rihal-hussain-pm |
49
+ | `mariam`, `marketing` | Mariam | rihal-mariam |
50
+ | `fatima`, `qa` | Fatima | rihal-fatima |
51
+ | `hanzla`, `dev`, `engineer` | Hanzla | rihal-hanzla |
52
+ | `dalil`, `scout`, `mapper` | Dalil | rihal-codebase-mapper |
53
+ | `khattat`, `planner` | Khattat | rihal-planner |
54
+ | `munaffidh`, `executor` | Munaffidh | rihal-executor |
55
+
56
+ **Behavior:**
57
+
58
+ 1. Parse the persona alias and CODE.
59
+ 2. Read the persona's agent file at `.claude/agents/{agent-id}.md` (or `.claude/agents/{agent-id}.local.md` if it exists — local overrides take precedence).
60
+ 3. Look up the CODE in the Capabilities table. If found, the table row's "Skill / workflow" column tells you which sub-command to invoke; pass the rest of `$QUESTION` (after the shortcut) as arguments.
61
+ 4. If the CODE is not in that persona's Capabilities table, print:
62
+ ```
63
+ Persona '{persona}' has no capability '{CODE}'. Available codes:
64
+ {list from the persona's Capabilities table}
65
+ ```
66
+ And stop. Do not fall back to fuzzy intent matching — the user used the deterministic API, honour it.
67
+ 5. Dispatch directly via the routing banner. Skip greenfield_guard / external_data_guard / explicit_intent_check / route — the user already chose the persona AND the action. The persona itself can still refuse internally if its preconditions aren't met.
68
+
69
+ **This is the deterministic API surface.** Power users (and other agents in council follow-ups) can invoke specific capabilities without re-reading triggers or risking fuzzy match. It's the cheapest way to get repeatable behaviour out of the persona system.
70
+
71
+ **Edge cases:**
72
+
73
+ - `@waleed` (no code) → dispatch to Waleed with no capability hint; persona uses its default workflow.
74
+ - `@nobody CP` (unknown persona) → fail loud: list known personas, exit.
75
+ - `CP @hussain` (code first) → reorder; do not match. The `@persona CODE` order is canonical.
76
+ - `@hussain CP fix the auth bug` → dispatch with `CP` and pass `fix the auth bug` as argument context.
77
+
78
+ If this step does NOT fire (no `@` prefix), continue to validate.
79
+ </step>
80
+
31
81
  <step name="validate">
32
82
  **Check for input.**
33
83