@hanzlaa/rcode 3.4.8 → 3.4.10

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/doctor.js CHANGED
@@ -54,7 +54,9 @@ function isWritable(dir) {
54
54
  }
55
55
 
56
56
  function commandAvailable(cmd) {
57
- const result = spawnSync('which', [cmd], { stdio: 'ignore' });
57
+ // 'which' is POSIX-only; 'where' is Windows. Try both so this works cross-platform.
58
+ const checker = process.platform === 'win32' ? 'where' : 'which';
59
+ const result = spawnSync(checker, [cmd], { stdio: 'ignore' });
58
60
  return result.status === 0;
59
61
  }
60
62
 
@@ -649,6 +649,7 @@ async function main(args) {
649
649
  results.errors.push(`label ${label.name}: ${result.error}`);
650
650
  } else if (!result.dryRun) {
651
651
  if (!syncMap.labels.includes(label.name)) syncMap.labels.push(label.name);
652
+ if (opts.execute) saveSyncMap(cwd, syncMap);
652
653
  console.log(` ✓ ${result.existed ? 'exists' : 'created'}: ${label.name}`);
653
654
  }
654
655
  }
@@ -670,6 +671,7 @@ async function main(args) {
670
671
  url: result.url,
671
672
  synced_at: new Date().toISOString(),
672
673
  };
674
+ if (opts.execute) saveSyncMap(cwd, syncMap);
673
675
  console.log(` ✓ created: ${phase.id} → milestone #${result.number}`);
674
676
  }
675
677
  }
@@ -686,7 +688,7 @@ async function main(args) {
686
688
  ``,
687
689
  `## 📋 Source Content`,
688
690
  ``,
689
- epic.content.slice(0, 3000),
691
+ epic.content.slice(0, 60000),
690
692
  ``,
691
693
  `---`,
692
694
  ``,
@@ -727,6 +729,7 @@ async function main(args) {
727
729
  content_hash: contentHash(epic.content),
728
730
  child_story_issues: [],
729
731
  };
732
+ if (opts.execute) saveSyncMap(cwd, syncMap);
730
733
  console.log(` ✓ created: ${epic.id} → #${result.number}`);
731
734
  }
732
735
  }
@@ -766,7 +769,7 @@ async function main(args) {
766
769
  ``,
767
770
  `## 📋 Source Content`,
768
771
  ``,
769
- story.content.slice(0, 3000),
772
+ story.content.slice(0, 60000),
770
773
  ``,
771
774
  `---`,
772
775
  ``,
@@ -807,6 +810,7 @@ async function main(args) {
807
810
  synced_at: new Date().toISOString(),
808
811
  content_hash: contentHash(story.content),
809
812
  };
813
+ if (opts.execute) saveSyncMap(cwd, syncMap);
810
814
  // Remember this child on the parent epic so we can update the
811
815
  // epic body with a task list after all stories have been created.
812
816
  if (parentEpicEntry) {
@@ -885,7 +889,7 @@ async function main(args) {
885
889
  ``,
886
890
  `## 📋 Source Content`,
887
891
  ``,
888
- epic.content.slice(0, 3000),
892
+ epic.content.slice(0, 60000),
889
893
  ``,
890
894
  `---`,
891
895
  ``,
@@ -930,7 +934,7 @@ async function main(args) {
930
934
  ``,
931
935
  `## 📋 Source Content`,
932
936
  ``,
933
- story.content.slice(0, 3000),
937
+ story.content.slice(0, 60000),
934
938
  ``,
935
939
  `---`,
936
940
  ``,
package/cli/install.js CHANGED
@@ -690,7 +690,13 @@ function ensureRcodeGitignore(target, options = {}) {
690
690
  const start = text.indexOf(BEGIN);
691
691
  if (start < 0) return null;
692
692
  const endIdx = text.indexOf(END, start);
693
- if (endIdx < 0) return null;
693
+ // If BEGIN exists but END is missing (manual edit removed it), strip
694
+ // everything from BEGIN to EOF and rewrite — avoids duplicate blocks.
695
+ if (endIdx < 0) {
696
+ let sliceStart = start;
697
+ if (sliceStart > 0 && text[sliceStart - 1] === '\n') sliceStart -= 1;
698
+ return text.slice(0, sliceStart) + newBlock;
699
+ }
694
700
  let sliceStart = start;
695
701
  if (sliceStart > 0 && text[sliceStart - 1] === '\n') sliceStart -= 1;
696
702
  let sliceEnd = endIdx + END.length;
@@ -765,7 +771,12 @@ function ensureRcodePreCommitHook(target, options = {}) {
765
771
  const start = text.indexOf(BEGIN);
766
772
  if (start < 0) return null;
767
773
  const endIdx = text.indexOf(END, start);
768
- if (endIdx < 0) return null;
774
+ // If BEGIN exists but END is missing, strip from BEGIN to EOF and rewrite.
775
+ if (endIdx < 0) {
776
+ let sliceStart = start;
777
+ if (sliceStart > 0 && text[sliceStart - 1] === '\n') sliceStart -= 1;
778
+ return text.slice(0, sliceStart) + newBlock;
779
+ }
769
780
  let sliceStart = start;
770
781
  if (sliceStart > 0 && text[sliceStart - 1] === '\n') sliceStart -= 1;
771
782
  let sliceEnd = endIdx + END.length;
@@ -1131,14 +1142,41 @@ function generateAgentManifest(plan, target) {
1131
1142
  * Generate files-manifest.csv with SHA256 per installed file. Used by
1132
1143
  * update/doctor to detect drift. Columns: rel, sha256, size.
1133
1144
  */
1134
- function generateFilesManifest(plan, target) {
1145
+ function generateFilesManifest(plan, target, { mergeExistingManifest = false } = {}) {
1135
1146
  const rows = [['rel', 'sha256', 'size']];
1147
+ const newRels = new Set();
1148
+
1136
1149
  for (const entry of plan) {
1137
1150
  const filePath = path.join(target, entry.rel);
1138
1151
  if (!fs.existsSync(filePath)) continue;
1139
1152
  const buf = fs.readFileSync(filePath);
1140
- rows.push([entry.rel.split(path.sep).join('/'), sha256(buf), String(buf.length)]);
1153
+ const rel = entry.rel.split(path.sep).join('/');
1154
+ rows.push([rel, sha256(buf), String(buf.length)]);
1155
+ newRels.add(rel);
1156
+ }
1157
+
1158
+ // Merge old manifest entries that are still on disk but not in the current
1159
+ // plan — this keeps orphaned files traceable by doctor/uninstall even when
1160
+ // --force sweep was not run. Without this, a re-install without --force
1161
+ // silently drops stale files from the manifest, making them invisible.
1162
+ if (mergeExistingManifest) {
1163
+ const manifestPath = path.join(target, '.rihal', '_config', 'files-manifest.csv');
1164
+ if (fs.existsSync(manifestPath)) {
1165
+ try {
1166
+ const oldRows = fs.readFileSync(manifestPath, 'utf8').split('\n').slice(1).filter(Boolean);
1167
+ for (const row of oldRows) {
1168
+ const [rel] = row.split(',');
1169
+ if (!rel || newRels.has(rel)) continue;
1170
+ const full = path.join(target, rel);
1171
+ if (!fs.existsSync(full)) continue; // already gone — don't re-add
1172
+ const buf = fs.readFileSync(full);
1173
+ rows.push([rel, sha256(buf), String(buf.length)]);
1174
+ newRels.add(rel);
1175
+ }
1176
+ } catch { /* best-effort */ }
1177
+ }
1141
1178
  }
1179
+
1142
1180
  return rows.map((r) => r.join(',')).join('\n') + '\n';
1143
1181
  }
1144
1182
 
@@ -1764,7 +1802,7 @@ async function install(opts) {
1764
1802
  // self-referential nonsense).
1765
1803
  fs.writeFileSync(
1766
1804
  path.join(configDir, 'files-manifest.csv'),
1767
- generateFilesManifest(plan, opts.target),
1805
+ generateFilesManifest(plan, opts.target, { mergeExistingManifest: !opts.force }),
1768
1806
  );
1769
1807
 
1770
1808
  // Install v1-style phrase-activated skills (scaffold-project, create-prd,
@@ -109,6 +109,10 @@ function projectLevelPath(cwd) {
109
109
  return path.join(cwd, '.rihal', 'config.json');
110
110
  }
111
111
 
112
+ function projectYamlPath(cwd) {
113
+ return path.join(cwd, '.rihal', 'config.yaml');
114
+ }
115
+
112
116
  // ---------- Loaders ----------
113
117
 
114
118
  function readJsonSafe(filePath) {
@@ -126,8 +130,33 @@ function loadUserDefaults() {
126
130
  return readJsonSafe(userLevelPath()) || {};
127
131
  }
128
132
 
133
+ // YAML key → JSON key remapping (install.js writes YAML with some different names)
134
+ const YAML_KEY_MAP = {
135
+ mode: 'communication_mode',
136
+ };
137
+
138
+ function readYamlFlat(filePath) {
139
+ if (!fs.existsSync(filePath)) return null;
140
+ try {
141
+ const result = {};
142
+ for (const raw of fs.readFileSync(filePath, 'utf8').split('\n')) {
143
+ const m = raw.match(/^([a-zA-Z_]+):\s*["']?([^"'\n#]+?)["']?\s*(?:#.*)?$/);
144
+ if (!m) continue;
145
+ const key = YAML_KEY_MAP[m[1].trim()] || m[1].trim();
146
+ const val = m[2].trim();
147
+ result[key] = val === 'true' ? true : val === 'false' ? false : val;
148
+ }
149
+ return Object.keys(result).length ? result : null;
150
+ } catch {
151
+ return null;
152
+ }
153
+ }
154
+
129
155
  function loadProjectConfig(cwd) {
130
- return readJsonSafe(projectLevelPath(cwd)) || {};
156
+ const json = readJsonSafe(projectLevelPath(cwd));
157
+ if (json) return json;
158
+ // Fall back to config.yaml written by installer — migrate keys on the fly
159
+ return readYamlFlat(projectYamlPath(cwd)) || {};
131
160
  }
132
161
 
133
162
  /**
@@ -330,5 +359,6 @@ module.exports = {
330
359
  loadProjectConfig,
331
360
  userLevelPath,
332
361
  projectLevelPath,
362
+ projectYamlPath,
333
363
  suggestClosest,
334
364
  };
@@ -9,6 +9,7 @@
9
9
  * a partial truncation.
10
10
  */
11
11
 
12
+ const crypto = require('crypto');
12
13
  const fs = require('fs');
13
14
  const path = require('path');
14
15
 
@@ -34,10 +35,10 @@ function writeFileAtomic(filePath, content, opts = {}) {
34
35
  const dir = path.dirname(filePath);
35
36
  fs.mkdirSync(dir, { recursive: true });
36
37
 
37
- // Include pid + rand so concurrent processes don't stomp each other's tmp.
38
+ // Include pid + crypto random so concurrent processes don't collide on tmp.
38
39
  const tmpPath = path.join(
39
40
  dir,
40
- `.${path.basename(filePath)}.tmp-${process.pid}-${Math.random().toString(36).slice(2, 8)}`
41
+ `.${path.basename(filePath)}.tmp-${process.pid}-${crypto.randomBytes(8).toString('hex')}`
41
42
  );
42
43
 
43
44
  let fd;
@@ -108,24 +108,22 @@ function verifyClaudeInstall(cwd, packageRoot) {
108
108
  const pkg = readPackageManifest(packageRoot);
109
109
  const skillsDir = path.join(cwd, '.claude/skills');
110
110
 
111
- // Agents are installed as rihal-{name}
111
+ // Agents are installed as rihal-{name} — strip prefix to match pkg.agents keys
112
112
  const installedAgents = readInstalledDirs(skillsDir, 'rihal-');
113
- // Agents come back with the 'rihal-' stripped; filter out action skills that
114
- // happen to also start with 'rihal-' (they end up in the agents set too).
115
- // We resolve this by intersecting: an entry is an agent only if the package
116
- // manifest has it as an agent.
117
- const agentsInstalled = new Set(
118
- [...installedAgents].filter((n) => pkg.agents.has(n))
119
- );
113
+ // Do NOT pre-filter against pkg.agents: we want stale entries (installed but
114
+ // not in current package) to appear in the `extra` list of diffSet so that
115
+ // `rcode doctor` can flag them as stale and `rcode uninstall` can remove them.
116
+ // The old intersection filter was hiding orphaned agent dirs after version bumps.
120
117
 
121
- // Action skills: installed with their bare name (no rihal- prefix)
118
+ // Action skills: installed with their bare name (no rihal- prefix).
119
+ // Exclude known agent dirs (rihal-prefixed) so actions and agents don't bleed.
122
120
  const allInstalled = readInstalledDirs(skillsDir);
123
121
  const actionsInstalled = new Set(
124
- [...allInstalled].filter((n) => pkg.actions.has(n))
122
+ [...allInstalled].filter((n) => !n.startsWith('rihal-'))
125
123
  );
126
124
 
127
125
  return [
128
- diffSet('claude', 'agents', pkg.agents, agentsInstalled),
126
+ diffSet('claude', 'agents', pkg.agents, installedAgents),
129
127
  diffSet('claude', 'actions', pkg.actions, actionsInstalled),
130
128
  ];
131
129
  }
@@ -24,10 +24,22 @@ const isGlobalInstall = (() => {
24
24
  try {
25
25
  // npm sets npm_config_global=true for global installs
26
26
  if (process.env.npm_config_global === 'true') return true;
27
- // Fallback: check if the install prefix is a global npm prefix
28
- const prefix = process.env.npm_config_prefix || '';
29
- const home = os.homedir();
30
- if (prefix && !prefix.startsWith(home) && !prefix.includes('node_modules')) return true;
27
+ // pnpm sets npm_config_global too, but check PNPM_HOME as a fallback
28
+ if (process.env.PNPM_HOME && __dirname.startsWith(process.env.PNPM_HOME)) return true;
29
+ // Check if __dirname is inside a known global node_modules path.
30
+ // Covers: /usr/local/lib, /usr/lib, ~/.nvm/.../lib, ~/.pnpm/..., ~/.yarn/...
31
+ const globalPatterns = [
32
+ /\/node_modules\/@hanzlaa\/rcode/, // any global node_modules
33
+ /[/\\]lib[/\\]node_modules[/\\]/, // /usr/local/lib/node_modules
34
+ /\.nvm[/\\]versions[/\\]/, // nvm
35
+ /\.pnpm[/\\]/, // pnpm global store
36
+ /\.yarn[/\\]global/, // yarn global
37
+ ];
38
+ if (globalPatterns.some((re) => re.test(__dirname))) return true;
39
+ // Last resort: package is NOT inside a project's local node_modules
40
+ // (local installs have .../project/node_modules/@hanzlaa/rcode/cli)
41
+ const localNodeModules = path.join(process.cwd(), 'node_modules');
42
+ if (!__dirname.startsWith(localNodeModules)) return true;
31
43
  return false;
32
44
  } catch {
33
45
  return false;
package/dist/rcode.js CHANGED
@@ -15456,7 +15456,11 @@ Say "plan a sprint" or run \`/rihal-sprint-planning\` to break Phase 01 into sto
15456
15456
  const start = text.indexOf(BEGIN);
15457
15457
  if (start < 0) return null;
15458
15458
  const endIdx = text.indexOf(END, start);
15459
- if (endIdx < 0) return null;
15459
+ if (endIdx < 0) {
15460
+ let sliceStart2 = start;
15461
+ if (sliceStart2 > 0 && text[sliceStart2 - 1] === "\n") sliceStart2 -= 1;
15462
+ return text.slice(0, sliceStart2) + newBlock;
15463
+ }
15460
15464
  let sliceStart = start;
15461
15465
  if (sliceStart > 0 && text[sliceStart - 1] === "\n") sliceStart -= 1;
15462
15466
  let sliceEnd = endIdx + END.length;
@@ -15512,7 +15516,11 @@ Say "plan a sprint" or run \`/rihal-sprint-planning\` to break Phase 01 into sto
15512
15516
  const start = text.indexOf(BEGIN);
15513
15517
  if (start < 0) return null;
15514
15518
  const endIdx = text.indexOf(END, start);
15515
- if (endIdx < 0) return null;
15519
+ if (endIdx < 0) {
15520
+ let sliceStart2 = start;
15521
+ if (sliceStart2 > 0 && text[sliceStart2 - 1] === "\n") sliceStart2 -= 1;
15522
+ return text.slice(0, sliceStart2) + newBlock;
15523
+ }
15516
15524
  let sliceStart = start;
15517
15525
  if (sliceStart > 0 && text[sliceStart - 1] === "\n") sliceStart -= 1;
15518
15526
  let sliceEnd = endIdx + END.length;
@@ -15785,13 +15793,34 @@ ${BLOCK}`);
15785
15793
  }
15786
15794
  return rows.map((r) => r.join(",")).join("\n") + "\n";
15787
15795
  }
15788
- function generateFilesManifest(plan, target) {
15796
+ function generateFilesManifest(plan, target, { mergeExistingManifest = false } = {}) {
15789
15797
  const rows = [["rel", "sha256", "size"]];
15798
+ const newRels = /* @__PURE__ */ new Set();
15790
15799
  for (const entry of plan) {
15791
15800
  const filePath = path2.join(target, entry.rel);
15792
15801
  if (!fs2.existsSync(filePath)) continue;
15793
15802
  const buf = fs2.readFileSync(filePath);
15794
- rows.push([entry.rel.split(path2.sep).join("/"), sha256(buf), String(buf.length)]);
15803
+ const rel = entry.rel.split(path2.sep).join("/");
15804
+ rows.push([rel, sha256(buf), String(buf.length)]);
15805
+ newRels.add(rel);
15806
+ }
15807
+ if (mergeExistingManifest) {
15808
+ const manifestPath = path2.join(target, ".rihal", "_config", "files-manifest.csv");
15809
+ if (fs2.existsSync(manifestPath)) {
15810
+ try {
15811
+ const oldRows = fs2.readFileSync(manifestPath, "utf8").split("\n").slice(1).filter(Boolean);
15812
+ for (const row of oldRows) {
15813
+ const [rel] = row.split(",");
15814
+ if (!rel || newRels.has(rel)) continue;
15815
+ const full = path2.join(target, rel);
15816
+ if (!fs2.existsSync(full)) continue;
15817
+ const buf = fs2.readFileSync(full);
15818
+ rows.push([rel, sha256(buf), String(buf.length)]);
15819
+ newRels.add(rel);
15820
+ }
15821
+ } catch {
15822
+ }
15823
+ }
15795
15824
  }
15796
15825
  return rows.map((r) => r.join(",")).join("\n") + "\n";
15797
15826
  }
@@ -16296,7 +16325,7 @@ ${BLOCK}`);
16296
16325
  ensureDir(globalAgentsDir);
16297
16326
  fs2.writeFileSync(
16298
16327
  path2.join(configDir, "files-manifest.csv"),
16299
- generateFilesManifest(plan, opts.target)
16328
+ generateFilesManifest(plan, opts.target, { mergeExistingManifest: !opts.force })
16300
16329
  );
16301
16330
  let skillsInstalled = installSkills(PACKAGE_ROOT2, opts.target);
16302
16331
  try {
@@ -16867,6 +16896,7 @@ var require_prompts = __commonJS({
16867
16896
  // cli/lib/fsutil.cjs
16868
16897
  var require_fsutil = __commonJS({
16869
16898
  "cli/lib/fsutil.cjs"(exports2, module2) {
16899
+ var crypto = require("crypto");
16870
16900
  var fs2 = require("fs");
16871
16901
  var path2 = require("path");
16872
16902
  function writeFileAtomic(filePath, content, opts = {}) {
@@ -16875,7 +16905,7 @@ var require_fsutil = __commonJS({
16875
16905
  fs2.mkdirSync(dir, { recursive: true });
16876
16906
  const tmpPath = path2.join(
16877
16907
  dir,
16878
- `.${path2.basename(filePath)}.tmp-${process.pid}-${Math.random().toString(36).slice(2, 8)}`
16908
+ `.${path2.basename(filePath)}.tmp-${process.pid}-${crypto.randomBytes(8).toString("hex")}`
16879
16909
  );
16880
16910
  let fd;
16881
16911
  try {
@@ -16966,15 +16996,12 @@ var require_manifest = __commonJS({
16966
16996
  const pkg = readPackageManifest(packageRoot);
16967
16997
  const skillsDir = path2.join(cwd, ".claude/skills");
16968
16998
  const installedAgents = readInstalledDirs(skillsDir, "rihal-");
16969
- const agentsInstalled = new Set(
16970
- [...installedAgents].filter((n) => pkg.agents.has(n))
16971
- );
16972
16999
  const allInstalled = readInstalledDirs(skillsDir);
16973
17000
  const actionsInstalled = new Set(
16974
- [...allInstalled].filter((n) => pkg.actions.has(n))
17001
+ [...allInstalled].filter((n) => !n.startsWith("rihal-"))
16975
17002
  );
16976
17003
  return [
16977
- diffSet("claude", "agents", pkg.agents, agentsInstalled),
17004
+ diffSet("claude", "agents", pkg.agents, installedAgents),
16978
17005
  diffSet("claude", "actions", pkg.actions, actionsInstalled)
16979
17006
  ];
16980
17007
  }
@@ -18242,7 +18269,8 @@ var require_doctor = __commonJS({
18242
18269
  }
18243
18270
  }
18244
18271
  function commandAvailable(cmd) {
18245
- const result = spawnSync("which", [cmd], { stdio: "ignore" });
18272
+ const checker = process.platform === "win32" ? "where" : "which";
18273
+ const result = spawnSync(checker, [cmd], { stdio: "ignore" });
18246
18274
  return result.status === 0;
18247
18275
  }
18248
18276
  function runPreflight(cwd, packageRoot) {
@@ -18606,6 +18634,9 @@ var require_config = __commonJS({
18606
18634
  function projectLevelPath(cwd) {
18607
18635
  return path2.join(cwd, ".rihal", "config.json");
18608
18636
  }
18637
+ function projectYamlPath(cwd) {
18638
+ return path2.join(cwd, ".rihal", "config.yaml");
18639
+ }
18609
18640
  function readJsonSafe(filePath) {
18610
18641
  if (!fs2.existsSync(filePath)) return null;
18611
18642
  try {
@@ -18617,8 +18648,29 @@ var require_config = __commonJS({
18617
18648
  function loadUserDefaults() {
18618
18649
  return readJsonSafe(userLevelPath()) || {};
18619
18650
  }
18651
+ var YAML_KEY_MAP = {
18652
+ mode: "communication_mode"
18653
+ };
18654
+ function readYamlFlat(filePath) {
18655
+ if (!fs2.existsSync(filePath)) return null;
18656
+ try {
18657
+ const result = {};
18658
+ for (const raw of fs2.readFileSync(filePath, "utf8").split("\n")) {
18659
+ const m = raw.match(/^([a-zA-Z_]+):\s*["']?([^"'\n#]+?)["']?\s*(?:#.*)?$/);
18660
+ if (!m) continue;
18661
+ const key = YAML_KEY_MAP[m[1].trim()] || m[1].trim();
18662
+ const val = m[2].trim();
18663
+ result[key] = val === "true" ? true : val === "false" ? false : val;
18664
+ }
18665
+ return Object.keys(result).length ? result : null;
18666
+ } catch {
18667
+ return null;
18668
+ }
18669
+ }
18620
18670
  function loadProjectConfig(cwd) {
18621
- return readJsonSafe(projectLevelPath(cwd)) || {};
18671
+ const json = readJsonSafe(projectLevelPath(cwd));
18672
+ if (json) return json;
18673
+ return readYamlFlat(projectYamlPath(cwd)) || {};
18622
18674
  }
18623
18675
  function loadConfig(cwd) {
18624
18676
  const defaults = { ...HARDCODED_DEFAULTS };
@@ -18749,6 +18801,7 @@ var require_config = __commonJS({
18749
18801
  loadProjectConfig,
18750
18802
  userLevelPath,
18751
18803
  projectLevelPath,
18804
+ projectYamlPath,
18752
18805
  suggestClosest
18753
18806
  };
18754
18807
  }
@@ -19936,6 +19989,7 @@ var require_github_sync = __commonJS({
19936
19989
  results.errors.push(`label ${label.name}: ${result.error}`);
19937
19990
  } else if (!result.dryRun) {
19938
19991
  if (!syncMap.labels.includes(label.name)) syncMap.labels.push(label.name);
19992
+ if (opts.execute) saveSyncMap(cwd, syncMap);
19939
19993
  console.log(` \u2713 ${result.existed ? "exists" : "created"}: ${label.name}`);
19940
19994
  }
19941
19995
  }
@@ -19956,6 +20010,7 @@ var require_github_sync = __commonJS({
19956
20010
  url: result.url,
19957
20011
  synced_at: (/* @__PURE__ */ new Date()).toISOString()
19958
20012
  };
20013
+ if (opts.execute) saveSyncMap(cwd, syncMap);
19959
20014
  console.log(` \u2713 created: ${phase.id} \u2192 milestone #${result.number}`);
19960
20015
  }
19961
20016
  }
@@ -19971,7 +20026,7 @@ var require_github_sync = __commonJS({
19971
20026
  ``,
19972
20027
  `## \u{1F4CB} Source Content`,
19973
20028
  ``,
19974
- epic.content.slice(0, 3e3),
20029
+ epic.content.slice(0, 6e4),
19975
20030
  ``,
19976
20031
  `---`,
19977
20032
  ``,
@@ -20009,6 +20064,7 @@ var require_github_sync = __commonJS({
20009
20064
  content_hash: contentHash(epic.content),
20010
20065
  child_story_issues: []
20011
20066
  };
20067
+ if (opts.execute) saveSyncMap(cwd, syncMap);
20012
20068
  console.log(` \u2713 created: ${epic.id} \u2192 #${result.number}`);
20013
20069
  }
20014
20070
  }
@@ -20033,7 +20089,7 @@ var require_github_sync = __commonJS({
20033
20089
  ``,
20034
20090
  `## \u{1F4CB} Source Content`,
20035
20091
  ``,
20036
- story.content.slice(0, 3e3),
20092
+ story.content.slice(0, 6e4),
20037
20093
  ``,
20038
20094
  `---`,
20039
20095
  ``,
@@ -20071,6 +20127,7 @@ var require_github_sync = __commonJS({
20071
20127
  synced_at: (/* @__PURE__ */ new Date()).toISOString(),
20072
20128
  content_hash: contentHash(story.content)
20073
20129
  };
20130
+ if (opts.execute) saveSyncMap(cwd, syncMap);
20074
20131
  if (parentEpicEntry) {
20075
20132
  parentEpicEntry.child_story_issues = parentEpicEntry.child_story_issues || [];
20076
20133
  if (!parentEpicEntry.child_story_issues.includes(result.number)) {
@@ -20137,7 +20194,7 @@ ${taskList}
20137
20194
  ``,
20138
20195
  `## \u{1F4CB} Source Content`,
20139
20196
  ``,
20140
- epic.content.slice(0, 3e3),
20197
+ epic.content.slice(0, 6e4),
20141
20198
  ``,
20142
20199
  `---`,
20143
20200
  ``,
@@ -20179,7 +20236,7 @@ ${taskList}
20179
20236
  ``,
20180
20237
  `## \u{1F4CB} Source Content`,
20181
20238
  ``,
20182
- story.content.slice(0, 3e3),
20239
+ story.content.slice(0, 6e4),
20183
20240
  ``,
20184
20241
  `---`,
20185
20242
  ``,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzlaa/rcode",
3
- "version": "3.4.8",
3
+ "version": "3.4.10",
4
4
  "description": "rcode — the memory bank for AI-driven SaaS teams. Persistent project context, distinctive engineering personas, and phase-based workflows. Built by Rihal. Works in Claude Code, Cursor, Gemini, VS Code, and Antigravity.",
5
5
  "main": "cli/index.js",
6
6
  "bin": {
@@ -10,13 +10,15 @@ After completing a task's implementation, follow this protocol to atomically com
10
10
 
11
11
  Before writing any commit messages, read the project's commit standard from source — do NOT invent one or ask the user unless nothing is found.
12
12
 
13
- **Check in this order (stop at first hit):**
14
-
15
- 1. `.github/COMMIT_CONVENTION.md` — explicit commit format doc
16
- 2. `CONTRIBUTING.md`look for a "Commit" or "Git" section
17
- 3. `.commitlintrc`, `.commitlintrc.json`, `.commitlintrc.yaml`, `commitlint.config.js`, `commitlint.config.cjs` — commitlint config
18
- 4. `package.json` → `"commitlint"` key
19
- 5. `.github/pull_request_template.md` — PR template reveals expected format (e.g., ticket prefix, changelog format)
13
+ **Check in this order — enforcement configs first, docs last (stop at first hit):**
14
+
15
+ 1. `.git/hooks/commit-msg` — the active enforcement hook (if exists and non-empty, read it to detect the validator)
16
+ 2. `.husky/commit-msg` — husky-managed commit hook
17
+ 3. `.commitlintrc`, `.commitlintrc.json`, `.commitlintrc.yaml`, `.commitlintrc.js`, `commitlint.config.js`, `commitlint.config.cjs` — commitlint config
18
+ 4. `package.json` → `"commitlint"` or `"config.commitizen"` key — commitizen config
19
+ 5. `.czrc` — commitizen standalone config
20
+ 6. `.github/COMMIT_CONVENTION.md` — explicit doc (lower priority than machine configs)
21
+ 7. `CONTRIBUTING.md` — look for a "Commit" or "Git" section (lowest priority)
20
22
 
21
23
  **If a standard is found:** Use it silently for all commits in this sprint. No need to confirm with user.
22
24
 
@@ -22,17 +22,30 @@ Tell the user:
22
22
  Rihal isn't configured for this project yet. Let me set it up — takes 30 seconds.
23
23
  ```
24
24
 
25
- **1. Bootstrap local tooling** — copy bin and workflows from the global install:
25
+ **1. Bootstrap local tooling** — copy bin from the global install:
26
26
 
27
27
  ```bash
28
28
  GLOBAL_RIHAL="$HOME/.rihal"
29
- if [ -d "$GLOBAL_RIHAL/bin" ]; then
30
- mkdir -p .rihal/bin .rihal/workflows .rihal/references
31
- cp "$GLOBAL_RIHAL/bin/rihal-tools.cjs" .rihal/bin/ 2>/dev/null || true
32
- # workflows and references are read from ~/.rihal at runtime via @.rihal/ resolution
29
+ TOOLS_SRC="$GLOBAL_RIHAL/bin/rihal-tools.cjs"
30
+
31
+ if [ ! -f "$TOOLS_SRC" ]; then
32
+ echo "ERROR: Global rihal tools not found at $TOOLS_SRC"
33
+ echo "Run: npm install -g @hanzlaa/rcode"
34
+ echo "Then retry this command."
35
+ # STOP — do not continue without tools; writing config.yaml alone is not enough
36
+ exit 1
37
+ fi
38
+
39
+ mkdir -p .rihal/bin
40
+ cp "$TOOLS_SRC" .rihal/bin/rihal-tools.cjs
41
+ if [ $? -ne 0 ]; then
42
+ echo "ERROR: Could not copy rihal-tools.cjs to .rihal/bin/ (permission denied?)"
43
+ exit 1
33
44
  fi
34
45
  ```
35
46
 
47
+ Note: workflows and references are resolved from `~/.rihal/` at runtime — only the bin needs to be local.
48
+
36
49
  **2. Ask the 5 config questions** using AskUserQuestion:
37
50
 
38
51
  | # | Question | Options | Default |
@@ -99,6 +112,6 @@ Continuing with your original request...
99
112
  ## Notes
100
113
 
101
114
  - This guard is **non-blocking** — it asks questions but does not stop the session; once config is written it resumes the original request automatically.
102
- - If `AskUserQuestion` is unavailable (non-interactive mode), use all defaults and skip the questions.
115
+ - If `AskUserQuestion` is unavailable (non-interactive mode), use all defaults and skip the questions. Always derive `project_name` from `basename $(pwd)` — never leave it as a placeholder.
103
116
  - If the bootstrap `cp` fails (global rihal not found), print a warning and attempt to continue — some workflows work without local rihal-tools if they only need config.
104
117
  - On **subsequent runs**, the guard exits immediately (config exists) with zero overhead.
@@ -296,6 +296,7 @@ Evaluate `$QUESTION` against these routing rules. Apply the **first matching** r
296
296
  | Implement a story, "work on story", "dev story", "build story" | `/rihal-dev-story` | Story-level implementation |
297
297
  | Find gaps in milestone plans, "gaps in plans", "missing plan", "unplanned phases" | `/rihal-plan-milestone-gaps` | Identify and fill planning gaps |
298
298
  | Executing a phase, "build phase N", "run phase N", "implement phase" | `/rihal-execute` | Direct phase execution request |
299
+ | `/rihal-phase <number>` where number matches an existing phase dir | `/rihal-execute <N>` | User mistyped phase instead of execute — detect bare integer + existing dir, route to execute |
299
300
  | Running all remaining phases automatically | `/rihal-autonomous` | Full autonomous execution |
300
301
  | A review or quality concern about existing work | `/rihal-verify-work` | Needs verification |
301
302
  | "Council", "discuss strategy", "should we" | `/rihal-council` | Multi-agent strategic discussion |
@@ -14,7 +14,42 @@ If `$ARGUMENTS` is empty AND no flag is set:
14
14
 
15
15
  STOP — do not proceed.
16
16
 
17
- ## Step 1 — Parse mode flags
17
+ ## Step 1 — Disambiguate numeric arguments BEFORE routing
18
+
19
+ If `$ARGUMENTS` is a bare integer (e.g. `116`, `20`, `7`) with no other words or flags:
20
+
21
+ ```bash
22
+ # Check if a phase directory matching this number already exists
23
+ PHASE_NUM="$ARGUMENTS"
24
+ EXISTING=$(find .planning/phases -maxdepth 1 -type d -name "${PHASE_NUM}-*" 2>/dev/null | head -1)
25
+ ```
26
+
27
+ If `$EXISTING` is non-empty, the user typed a phase number that already exists — they almost certainly meant to operate on it, not create a new one. Stop and ask:
28
+
29
+ ```
30
+ Phase {N} already exists: {directory name}
31
+
32
+ What did you mean to do?
33
+
34
+ /rihal-execute {N} — execute the sprint plan for this phase
35
+ /rihal-plan {N} — re-plan or view the plan for this phase
36
+ /rihal-status — see overall project status
37
+
38
+ /rihal-phase "{description}" — add a NEW phase (put the description in quotes)
39
+ ```
40
+
41
+ Do NOT proceed to add/insert/remove. Wait for the user to clarify.
42
+
43
+ If `$ARGUMENTS` is a bare integer and `$EXISTING` is empty, it's an ambiguous but plausible new-phase name. Proceed to Step 2 but warn:
44
+
45
+ ```
46
+ Note: "{N}" looks like a number. If you meant to execute/plan phase {N}, use /rihal-execute {N} or /rihal-plan {N}.
47
+ Adding a new phase named "{N}" — press Ctrl+C to cancel, or continue.
48
+ ```
49
+
50
+ Then proceed to Step 2.
51
+
52
+ ## Step 1b — Parse mode flags
18
53
 
19
54
  Inspect `$ARGUMENTS`:
20
55
 
@@ -24,7 +59,7 @@ Inspect `$ARGUMENTS`:
24
59
 
25
60
  Strip the mode flag and pass remaining args to the underlying workflow.
26
61
 
27
- ## Step 2 — Dispatch to underlying workflow
62
+ ## Step 2 — Dispatch to underlying workflow (after disambiguation)
28
63
 
29
64
  Each mode is implemented by an existing workflow:
30
65