@hanzlaa/rcode 4.1.1 → 4.3.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 (105) hide show
  1. package/AGENTS.md +1 -1
  2. package/CONTRIBUTING.md +3 -0
  3. package/README.md +3 -0
  4. package/cli/agent.js +3 -1
  5. package/cli/index.js +29 -0
  6. package/cli/install.js +233 -15
  7. package/cli/lib/config.cjs +4 -2
  8. package/cli/lib/fsutil.cjs +13 -2
  9. package/cli/lib/homedir.cjs +21 -0
  10. package/cli/lib/schemas.cjs +6 -1
  11. package/cli/nuke.js +13 -8
  12. package/cli/postinstall.js +14 -4
  13. package/cli/rcode-slash-router.cjs +118 -0
  14. package/cli/uninstall.js +59 -1
  15. package/cli/update.js +10 -5
  16. package/cli/workflow.js +3 -1
  17. package/dist/rcode.js +241 -227
  18. package/package.json +1 -1
  19. package/rcode/bin/rcode-tools.cjs +15 -6
  20. package/rcode/commands/scaffold-project.md +2 -2
  21. package/rcode/skills/actions/2-plan/rcode-create-epics-and-stories/steps/step-04-final-validation.md +1 -1
  22. package/rcode/skills/actions/2-plan/rcode-create-milestone/steps/README.md +2 -2
  23. package/rcode/skills/actions/2-plan/rcode-create-milestone/steps/step-09-state-sync.md +1 -1
  24. package/rcode/skills/actions/4-implementation/rcode-code-review/steps/step-02-review.md +1 -1
  25. package/rcode/skills/actions/4-implementation/rcode-git-flow/SKILL.md +1 -1
  26. package/rcode/skills/actions/4-implementation/rcode-scaffold-project/SKILL.md +39 -12
  27. package/rcode/skills/actions/4-implementation/rcode-scaffold-project/steps/step-01-target.md +18 -3
  28. package/rcode/skills/actions/4-implementation/rcode-scaffold-project/steps/step-02-safety.md +27 -3
  29. package/rcode/skills/actions/4-implementation/rcode-scaffold-project/steps/step-03-brownfield.md +57 -0
  30. package/rcode/skills/actions/4-implementation/rcode-scaffold-project/steps/step-03-clone.md +4 -1
  31. package/rcode/skills/actions/4-implementation/rcode-scaffold-project/steps/step-04-post-setup.md +15 -1
  32. package/rcode/skills/actions/4-implementation/rcode-trim/SKILL.md +1 -1
  33. package/rcode/workflows/audit-milestone.md +1 -1
  34. package/rcode/workflows/discuss-phase.md +1 -1
  35. package/rcode/workflows/execute-milestone.md +1 -1
  36. package/rcode/workflows/execute-regression-gates.md +3 -0
  37. package/rcode/workflows/execute-sprint.md +27 -1
  38. package/rcode/workflows/execute-waves.md +6 -0
  39. package/rcode/workflows/execute.md +13 -3
  40. package/rcode/workflows/new-milestone.md +2 -2
  41. package/rcode/workflows/new-project.md +4 -0
  42. package/rcode/workflows/plan-research-validation.md +1 -1
  43. package/rcode/workflows/plan-spawn-planner.md +2 -2
  44. package/rcode/workflows/plan.md +34 -15
  45. package/rcode/workflows/review.md +2 -0
  46. package/rcode/workflows/scaffold-project.md +5 -1
  47. package/rcode/workflows/session-report.md +1 -1
  48. package/rcode/workflows/ship.md +39 -0
  49. package/rcode/workflows/sprint-planning.md +27 -0
  50. package/rcode/workflows/status.md +3 -3
  51. package/server/dashboard.js +26 -7
  52. package/server/lib/api.js +62 -4
  53. package/server/lib/html/client/agents-data.js +22 -18
  54. package/server/lib/html/client/app.js +3 -0
  55. package/server/lib/html/client/components/AgentCard.js +127 -0
  56. package/server/lib/html/client/components/App.js +104 -39
  57. package/server/lib/html/client/components/CommandPalette.js +133 -0
  58. package/server/lib/html/client/components/FileReader.js +116 -0
  59. package/server/lib/html/client/components/FilterChips.js +94 -0
  60. package/server/lib/html/client/components/NotifyCenter.js +117 -0
  61. package/server/lib/html/client/components/OrchPanel.js +80 -52
  62. package/server/lib/html/client/components/PhaseGraph.js +300 -0
  63. package/server/lib/html/client/components/RejectDialog.js +78 -0
  64. package/server/lib/html/client/components/RunnerPicker.js +190 -0
  65. package/server/lib/html/client/components/Sidebar.js +106 -61
  66. package/server/lib/html/client/components/StatusSummaryBar.js +76 -0
  67. package/server/lib/html/client/components/TaskPipeline.js +83 -0
  68. package/server/lib/html/client/components/Topbar.js +86 -39
  69. package/server/lib/html/client/components/dashboard/Blockers.js +57 -0
  70. package/server/lib/html/client/components/dashboard/CompletedTasks.js +47 -0
  71. package/server/lib/html/client/components/dashboard/CurrentPhase.js +107 -0
  72. package/server/lib/html/client/components/dashboard/InProgress.js +72 -0
  73. package/server/lib/html/client/components/dashboard/ProgressDonut.js +101 -0
  74. package/server/lib/html/client/components/dashboard/ProgressTimeline.js +101 -0
  75. package/server/lib/html/client/components/dashboard/ProjectHealth.js +80 -0
  76. package/server/lib/html/client/components/dashboard/RecentDecisions.js +57 -0
  77. package/server/lib/html/client/components/dashboard/Timeline.js +143 -0
  78. package/server/lib/html/client/components/shared.js +47 -11
  79. package/server/lib/html/client/filter-state.js +72 -0
  80. package/server/lib/html/client/icons-client.js +7 -0
  81. package/server/lib/html/client/notify.js +75 -0
  82. package/server/lib/html/client/orchestrator.js +168 -41
  83. package/server/lib/html/client/preact.js +13 -8
  84. package/server/lib/html/client/store.js +70 -6
  85. package/server/lib/html/client/util.js +78 -0
  86. package/server/lib/html/client/vendor/htm.js +1 -0
  87. package/server/lib/html/client/vendor/preact-hooks.js +2 -0
  88. package/server/lib/html/client/vendor/preact.js +2 -0
  89. package/server/lib/html/client/views/AgentsView.js +144 -51
  90. package/server/lib/html/client/views/FilesView.js +20 -103
  91. package/server/lib/html/client/views/KanbanView.js +40 -21
  92. package/server/lib/html/client/views/MemoryView.js +26 -9
  93. package/server/lib/html/client/views/MilestonesView.js +4 -4
  94. package/server/lib/html/client/views/OrchestrationView.js +154 -19
  95. package/server/lib/html/client/views/OverviewView.js +47 -239
  96. package/server/lib/html/client/views/PhasesView.js +50 -6
  97. package/server/lib/html/client/views/RoadmapView.js +6 -3
  98. package/server/lib/html/client/views/SprintsView.js +50 -6
  99. package/server/lib/html/client/views/TasksView.js +4 -3
  100. package/server/lib/html/client.js +21 -4
  101. package/server/lib/html/css.js +2761 -8
  102. package/server/lib/html/icons.js +7 -0
  103. package/server/lib/html/shell.js +10 -3
  104. package/server/lib/scanner.js +376 -39
  105. package/server/orchestrator.js +329 -5
package/cli/nuke.js CHANGED
@@ -20,9 +20,12 @@
20
20
  'use strict';
21
21
 
22
22
  const fs = require('fs');
23
- const os = require('os');
24
23
  const path = require('path');
25
24
  const { spawnSync } = require('child_process');
25
+ // HOME-aware home resolution (#889) — os.homedir() ignores a stubbed HOME
26
+ // on Windows, so tests pointing HOME at a temp dir still scanned the real
27
+ // profile dir there (and tripped over real ~/.rcode state).
28
+ const { homedir } = require('./lib/homedir.cjs');
26
29
 
27
30
  function exists(p) {
28
31
  try { fs.accessSync(p); return true; } catch { return false; }
@@ -37,7 +40,7 @@ function readDirSafe(p) {
37
40
  * Returns a list of { manager, dir } — dir may not exist.
38
41
  */
39
42
  function getGlobalNodeModulesDirs() {
40
- const home = os.homedir();
43
+ const home = homedir();
41
44
  const candidates = [];
42
45
 
43
46
  // npm — npm root -g resolves to the active node version's lib/node_modules.
@@ -106,7 +109,7 @@ function findRcodePackages(globalNodeModules) {
106
109
  * Resolve global bin directories where rcode/rcode/rcode may live.
107
110
  */
108
111
  function getGlobalBinDirs() {
109
- const home = os.homedir();
112
+ const home = homedir();
110
113
  const dirs = new Set();
111
114
 
112
115
  // npm prefix bin
@@ -214,7 +217,7 @@ function findClaudeArtifacts(claudeDir) {
214
217
  }
215
218
 
216
219
  function buildPlan({ includePlanning }) {
217
- const home = os.homedir();
220
+ const home = homedir();
218
221
  const cwd = process.cwd();
219
222
  const plan = {
220
223
  packages: [],
@@ -244,13 +247,15 @@ function buildPlan({ includePlanning }) {
244
247
  plan.globalClaude = findClaudeArtifacts(path.join(home, '.claude'));
245
248
 
246
249
  // Global state (~/.rcode/)
250
+ // #889: was `= globalRcode` (undefined) — a ReferenceError that only fired
251
+ // when ~/.rcode existed, crashing every dry-run on machines with global state.
247
252
  const globalrcode = path.join(home, '.rcode');
248
- if (exists(globalrcode)) plan.globalrcode = globalRcode;
253
+ if (exists(globalrcode)) plan.globalrcode = globalrcode;
249
254
 
250
255
  // Project-level (CWD only — never recurse, user may have many projects)
251
256
  plan.projectClaude = findClaudeArtifacts(path.join(cwd, '.claude'));
252
257
  const projectrcode = path.join(cwd, '.rcode');
253
- if (exists(projectrcode) && cwd !== home) plan.projectrcode = projectRcode;
258
+ if (exists(projectrcode) && cwd !== home) plan.projectrcode = projectrcode;
254
259
 
255
260
  if (includePlanning) {
256
261
  const projectPlanning = path.join(cwd, '.planning');
@@ -351,7 +356,7 @@ function executePlan(plan) {
351
356
  if (rmrf(a.path)) { console.log(` ✓ removed ${a.path}`); removed++; }
352
357
  }
353
358
  if (plan.globalrcode && rmrf(plan.globalrcode)) {
354
- console.log(` ✓ removed ${plan.globalRcode}`); removed++;
359
+ console.log(` ✓ removed ${plan.globalrcode}`); removed++;
355
360
  }
356
361
 
357
362
  // Claude artifacts (project)
@@ -359,7 +364,7 @@ function executePlan(plan) {
359
364
  if (rmrf(a.path)) { console.log(` ✓ removed ${a.path}`); removed++; }
360
365
  }
361
366
  if (plan.projectrcode && rmrf(plan.projectrcode)) {
362
- console.log(` ✓ removed ${plan.projectRcode}`); removed++;
367
+ console.log(` ✓ removed ${plan.projectrcode}`); removed++;
363
368
  }
364
369
  if (plan.projectPlanning && rmrf(plan.projectPlanning)) {
365
370
  console.log(` ✓ removed ${plan.projectPlanning}`); removed++;
@@ -13,6 +13,17 @@
13
13
  const os = require('os');
14
14
  const path = require('path');
15
15
 
16
+ /**
17
+ * Path containment check that survives Windows: path.relative normalizes
18
+ * separators (/ vs \) and compares drive letters case-insensitively, which
19
+ * a raw startsWith prefix compare does not.
20
+ */
21
+ function isPathInside(child, parent) {
22
+ const rel = path.relative(parent, child);
23
+ if (rel === '') return true;
24
+ return rel !== '..' && !rel.startsWith(`..${path.sep}`) && !path.isAbsolute(rel);
25
+ }
26
+
16
27
  /**
17
28
  * Decide whether the current postinstall invocation represents a GLOBAL
18
29
  * `npm install -g @hanzlaa/rcode` (true) or a transitive devDep install
@@ -28,17 +39,16 @@ const path = require('path');
28
39
  function isGlobalInstall(env, dirname, cwd) {
29
40
  try {
30
41
  if (env.npm_config_global === 'true') return true;
31
- if (env.PNPM_HOME && dirname.startsWith(env.PNPM_HOME)) return true;
42
+ if (env.PNPM_HOME && isPathInside(dirname, env.PNPM_HOME)) return true;
32
43
  const globalPatterns = [
33
- /\/node_modules\/@hanzlaa\/rcode/,
44
+ /[/\\]node_modules[/\\]@hanzlaa[/\\]rcode/,
34
45
  /[/\\]lib[/\\]node_modules[/\\]/,
35
46
  /\.nvm[/\\]versions[/\\]/,
36
47
  /\.pnpm[/\\]/,
37
48
  /\.yarn[/\\]global/,
38
49
  ];
39
50
  if (globalPatterns.some((re) => re.test(dirname))) return true;
40
- const localNodeModules = path.join(cwd, 'node_modules');
41
- if (!dirname.startsWith(localNodeModules)) return true;
51
+ if (!isPathInside(dirname, path.join(cwd, 'node_modules'))) return true;
42
52
  return false;
43
53
  } catch {
44
54
  return false;
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // rcode slash-command hook router.
5
+ //
6
+ // WHY this exists: Codex CLI and Antigravity CLI do NOT surface file-based
7
+ // `/slash` commands the way Claude Code / Grok do (verified live). They DO,
8
+ // however, support a prompt-submit hook (`UserPromptSubmit` / `UserPrompt`)
9
+ // that can inject extra context into the model's turn. This router is wired
10
+ // into that hook by the installer. When the user types `/rcode-<name> [args]`,
11
+ // the router loads the matching command body and injects it as additional
12
+ // context so the model executes that command — the closest thing to a native
13
+ // slash command those CLIs allow.
14
+ //
15
+ // Dependency-free (Node stdlib only) so it can run from a stable home dir
16
+ // (~/.rcode/bin/) without an install step. NEVER throws to the host CLI: any
17
+ // error exits 0 with no output so a malfunctioning router can never break or
18
+ // swallow the user's real prompt.
19
+
20
+ const fs = require('fs');
21
+ const os = require('os');
22
+ const path = require('path');
23
+
24
+ // Command bodies are copied here by the installer (installSlashRouterCommands).
25
+ // A fixed home-dir location means the hook can always read them regardless of
26
+ // the user's current working directory.
27
+ // HOME wins over os.homedir() (#889): os.homedir() ignores HOME on Windows
28
+ // (it reads USERPROFILE), so HOME-redirected runs (tests, git-bash) would read
29
+ // the wrong profile dir. Inlined — this script is copied standalone to
30
+ // ~/.rcode/bin/ and must stay dependency-free (no ./lib requires).
31
+ const COMMANDS_DIR = path.join(process.env.HOME || os.homedir(), '.rcode', 'slash-commands');
32
+
33
+ // Matches `/rcode-<name>` at the very start, optional whitespace, then the
34
+ // rest of the line(s) as arguments. `\b` ends the command name so trailing
35
+ // punctuation/args don't leak into <name>.
36
+ const SLASH_RE = /^\/rcode-([a-z0-9-]+)\b[ \t]*([\s\S]*)$/;
37
+
38
+ function readStdin() {
39
+ try {
40
+ return fs.readFileSync(0, 'utf8');
41
+ } catch {
42
+ return '';
43
+ }
44
+ }
45
+
46
+ // Strip a leading YAML frontmatter block (`---\n...\n---`). The frontmatter is
47
+ // CLI-tooling metadata (name/description/allowed-tools) that only confuses the
48
+ // model — we want the executable command body injected, not its header.
49
+ // \r?\n because Windows checkouts may deliver CRLF command bodies (#889).
50
+ function stripFrontmatter(text) {
51
+ return text.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '');
52
+ }
53
+
54
+ function emit(hookEventName, additionalContext) {
55
+ const payload = {
56
+ hookSpecificOutput: {
57
+ hookEventName: hookEventName || 'UserPromptSubmit',
58
+ additionalContext,
59
+ },
60
+ };
61
+ process.stdout.write(JSON.stringify(payload));
62
+ }
63
+
64
+ function main() {
65
+ const raw = readStdin();
66
+ if (!raw.trim()) return;
67
+
68
+ let data;
69
+ try {
70
+ data = JSON.parse(raw);
71
+ } catch {
72
+ return; // not JSON we understand → pass-through (no output)
73
+ }
74
+
75
+ // Field names vary across CLIs; accept the common spellings.
76
+ const prompt =
77
+ data.prompt ??
78
+ data.user_prompt ??
79
+ data.userPrompt ??
80
+ data.message ??
81
+ data.input ??
82
+ '';
83
+ const hookEventName = data.hook_event_name || data.hookEventName || 'UserPromptSubmit';
84
+
85
+ if (typeof prompt !== 'string') return;
86
+
87
+ const match = prompt.replace(/^\s+/, '').match(SLASH_RE);
88
+ if (!match) return; // not an rcode command → pass-through (no output)
89
+
90
+ const name = match[1];
91
+ const args = (match[2] || '').trim();
92
+
93
+ const cmdFile = path.join(COMMANDS_DIR, `${name}.md`);
94
+ if (!fs.existsSync(cmdFile)) {
95
+ // Unknown command: inject a short note rather than silently doing nothing,
96
+ // so the user learns the command name didn't resolve.
97
+ emit(
98
+ hookEventName,
99
+ `Unknown rcode command: /rcode-${name}. No matching command body was found in ${COMMANDS_DIR}.`,
100
+ );
101
+ return;
102
+ }
103
+
104
+ let body = stripFrontmatter(fs.readFileSync(cmdFile, 'utf8')).trim();
105
+ if (args) {
106
+ // Surface user-supplied args the way the command bodies expect ($ARGUMENTS).
107
+ body += `\n\nArguments: ${args}`;
108
+ }
109
+
110
+ emit(hookEventName, body);
111
+ }
112
+
113
+ try {
114
+ main();
115
+ } catch {
116
+ // Never break the host CLI's prompt — fail open, silently.
117
+ }
118
+ process.exit(0);
package/cli/uninstall.js CHANGED
@@ -25,6 +25,7 @@
25
25
  */
26
26
 
27
27
  const fs = require('fs');
28
+ const os = require('os');
28
29
  const path = require('path');
29
30
  const { spawnSync } = require('child_process');
30
31
  const { askConfirm, PromptAbortError } = require('./lib/prompts.cjs');
@@ -128,6 +129,35 @@ function cleanRcodePreCommitHook(cwd) {
128
129
  } catch { return 'skipped'; }
129
130
  }
130
131
 
132
+ /**
133
+ * Remove the rcode slash-router hook entry from a CLI hooks JSON file
134
+ * (codex: ~/.codex/hooks.json UserPromptSubmit, antigravity:
135
+ * ~/.gemini/antigravity/settings.json UserPrompt) while preserving every
136
+ * other entry (herdr's, the user's). Matches the router by command substring.
137
+ * Idempotent + guarded: missing/unparseable files are no-ops.
138
+ * Returns 'removed' | 'unchanged' | 'skipped'.
139
+ */
140
+ function removeSlashRouterHook(jsonPath, eventKey) {
141
+ if (!fs.existsSync(jsonPath)) return 'skipped';
142
+ let root;
143
+ try { root = JSON.parse(fs.readFileSync(jsonPath, 'utf8')); } catch { return 'skipped'; }
144
+ if (!root || typeof root !== 'object' || !root.hooks || !Array.isArray(root.hooks[eventKey])) {
145
+ return 'unchanged';
146
+ }
147
+ const before = root.hooks[eventKey].length;
148
+ root.hooks[eventKey] = root.hooks[eventKey].filter(group =>
149
+ !(Array.isArray(group?.hooks) &&
150
+ group.hooks.some(h => typeof h?.command === 'string' && h.command.includes('rcode-slash-router.cjs'))),
151
+ );
152
+ if (root.hooks[eventKey].length === before) return 'unchanged';
153
+ // Drop the event key entirely if it is now empty, to leave a tidy file.
154
+ if (root.hooks[eventKey].length === 0) delete root.hooks[eventKey];
155
+ try {
156
+ writeFileAtomic(jsonPath, JSON.stringify(root, null, 2) + '\n');
157
+ return 'removed';
158
+ } catch { return 'skipped'; }
159
+ }
160
+
131
161
  /**
132
162
  * Walk a directory and remove all files/subdirs whose name matches a predicate.
133
163
  * Returns the number of entries removed. Always skips local overrides (#382).
@@ -433,7 +463,11 @@ function planToPathList(plan, cwd, options = {}) {
433
463
  * still proceed since the user already confirmed the destructive action.
434
464
  */
435
465
  function createBackup(cwd, plan, options = {}) {
436
- const paths = planToPathList(plan, cwd, { purge: options.purge === true });
466
+ // Tar entry names are always '/'-separated; on Windows planToPathList
467
+ // produces '\'-joined paths that bsdtar may store verbatim. Normalize so
468
+ // the archive (and any `tar -tzf` consumer) sees portable paths.
469
+ const paths = planToPathList(plan, cwd, { purge: options.purge === true })
470
+ .map((p) => p.split(path.sep).join('/'));
437
471
  if (paths.length === 0) {
438
472
  return { ok: false, warning: 'nothing to back up' };
439
473
  }
@@ -746,6 +780,29 @@ async function runUninstall(args) {
746
780
  if (n > 0) console.log(` ✓ removed ${n} Antigravity agents`);
747
781
  }
748
782
 
783
+ // Slash-router hooks (home-dir, CLI-native). Installed only via `--global`
784
+ // for codex/antigravity; remove the rcode UserPromptSubmit/UserPrompt entry
785
+ // from each CLI's hooks JSON, leaving herdr + other hooks intact.
786
+ if (editors.includes('codex')) {
787
+ const r = removeSlashRouterHook(path.join(os.homedir(), '.codex', 'hooks.json'), 'UserPromptSubmit');
788
+ if (r === 'removed') console.log(` ✓ removed rcode slash-router hook from ~/.codex/hooks.json`);
789
+ }
790
+ if (editors.includes('antigravity')) {
791
+ const r = removeSlashRouterHook(path.join(os.homedir(), '.gemini', 'antigravity', 'settings.json'), 'UserPrompt');
792
+ if (r === 'removed') console.log(` ✓ removed rcode slash-router hook from ~/.gemini/antigravity/settings.json`);
793
+ }
794
+ // The router script + command-body copies are shared by both CLIs; remove
795
+ // them once if either is in scope.
796
+ if (editors.includes('codex') || editors.includes('antigravity')) {
797
+ const home = os.homedir();
798
+ for (const dir of [path.join(home, '.rcode', 'slash-commands'), path.join(home, '.rcode', 'bin')]) {
799
+ if (fs.existsSync(dir)) {
800
+ const rr = safeRmSync(dir, home);
801
+ if (rr.ok) console.log(` ✓ removed ${dir}`);
802
+ }
803
+ }
804
+ }
805
+
749
806
  // #706 — gemini removal (.gemini/rcode/{agents,commands})
750
807
  if (editors.includes('gemini')) {
751
808
  let n = 0;
@@ -925,6 +982,7 @@ module.exports.isLocalOverride = isLocalOverride;
925
982
  module.exports.planToPathList = planToPathList;
926
983
  module.exports.discoverKnownActionSkills = discoverKnownActionSkills;
927
984
  module.exports.stripRcodeGitignoreBlock = stripRcodeGitignoreBlock;
985
+ module.exports.removeSlashRouterHook = removeSlashRouterHook;
928
986
 
929
987
  // Direct invocation — allow `node cli/uninstall.js [flags]` to run end-to-end.
930
988
  // When called via cli/index.js, module.exports is invoked directly.
package/cli/update.js CHANGED
@@ -34,6 +34,8 @@ const { spawnSync } = require('child_process');
34
34
  const clack = require('@clack/prompts');
35
35
  const { PromptAbortError } = require('./lib/prompts.cjs');
36
36
  const { writeFileAtomic } = require('./lib/fsutil.cjs');
37
+ // HOME-aware home resolution (#889): os.homedir() ignores HOME on Windows.
38
+ const { homedir } = require('./lib/homedir.cjs');
37
39
  const { verifyInstall, formatReport } = require('./lib/manifest.cjs');
38
40
  const install = require('./install');
39
41
 
@@ -46,7 +48,9 @@ const install = require('./install');
46
48
  function readConfigYaml(configPath) {
47
49
  const text = fs.readFileSync(configPath, 'utf8');
48
50
  const obj = {};
49
- for (const raw of text.split('\n')) {
51
+ // CRLF tolerance (#889): split on \r?\n — a stray \r otherwise defeats the
52
+ // `#.*$` comment strip ($ won't cross the \r) and leaks comments into values.
53
+ for (const raw of text.split(/\r?\n/)) {
50
54
  const line = raw.replace(/#.*$/, '').trimEnd();
51
55
  if (!line) continue;
52
56
  if (line.startsWith(' ')) continue; // ignore nested keys (rare; preserved on disk via raw text path)
@@ -68,14 +72,16 @@ function readConfigYaml(configPath) {
68
72
  * If the key doesn't exist, append it.
69
73
  */
70
74
  function setYamlKey(rawText, key, value) {
71
- const re = new RegExp(`^${key}:\\s*.*$`, 'm');
75
+ // CRLF tolerance (#889): [^\S\n]/[^\r\n] keep the match on one line even
76
+ // when the file uses \r\n (plain \s would walk across the line break).
77
+ const re = new RegExp(`^${key}:[^\\S\\n]*[^\\r\\n]*$`, 'm');
72
78
  const replacement = typeof value === 'string'
73
79
  ? `${key}: "${value.replace(/"/g, '\\"')}"`
74
80
  : `${key}: ${value}`;
75
81
  if (re.test(rawText)) {
76
82
  return rawText.replace(re, replacement);
77
83
  }
78
- return rawText.replace(/\n*$/, '') + `\n${replacement}\n`;
84
+ return rawText.replace(/(?:\r?\n)*$/, '') + `\n${replacement}\n`;
79
85
  }
80
86
 
81
87
  function parseArgs(args) {
@@ -98,8 +104,7 @@ function detectInstalledEditors(cwd) {
98
104
  // .rcode/config.yaml as the canonical signal — if config exists, the
99
105
  // project ran rcode install at least once for claude. The presence of
100
106
  // any commands/agents/skills then becomes secondary evidence.
101
- const os = require('os');
102
- const homeSkills = path.join(os.homedir(), '.claude/skills');
107
+ const homeSkills = path.join(homedir(), '.claude/skills');
103
108
  const projectClaude = (
104
109
  (fs.existsSync(path.join(cwd, '.claude/skills')) &&
105
110
  fs.readdirSync(path.join(cwd, '.claude/skills')).some(n => n.startsWith('rcode-'))) ||
package/cli/workflow.js CHANGED
@@ -73,9 +73,11 @@ function listWorkflows(workflowsDir) {
73
73
  }
74
74
 
75
75
  function showWorkflow(workflowsDir, name) {
76
+ // Normalise: strip rcode- prefix so both `plan` and `rcode-plan` resolve (#883)
77
+ const bare = name.startsWith('rcode-') ? name.slice('rcode-'.length) : name;
76
78
  const candidates = [
79
+ path.join(workflowsDir, `${bare}.md`),
77
80
  path.join(workflowsDir, `${name}.md`),
78
- path.join(workflowsDir, `rcode-${name}.md`),
79
81
  ];
80
82
 
81
83
  for (const p of candidates) {