@hanzlaa/rcode 4.7.3 → 4.9.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 (95) hide show
  1. package/AGENTS.md +1 -1
  2. package/CLAUDE.md +1 -1
  3. package/CONTRIBUTING.md +3 -0
  4. package/README.md +7 -5
  5. package/cli/github-sync.js +17 -201
  6. package/cli/install.js +16 -13
  7. package/cli/lib/config.cjs +1 -1
  8. package/cli/lib/github-sync-discover.cjs +304 -0
  9. package/cli/lib/namespace-migrate.cjs +16 -1
  10. package/cli/uninstall.js +7 -2
  11. package/dist/rcode.js +197 -197
  12. package/package.json +1 -1
  13. package/rcode/agents/rcode-code-reviewer.md +1 -1
  14. package/rcode/agents/rcode-docs-auditor.md +1 -1
  15. package/rcode/agents/rcode-edge-case-hunter.md +1 -1
  16. package/rcode/agents/rcode-haitham.md +1 -1
  17. package/rcode/agents/rcode-hanzla.md +1 -1
  18. package/rcode/agents/rcode-hussain-pm.md +1 -1
  19. package/rcode/agents/rcode-nyquist-auditor.md +1 -1
  20. package/rcode/agents/rcode-omar.md +1 -1
  21. package/rcode/agents/rcode-remediation-planner.md +1 -1
  22. package/rcode/agents/rcode-security-adversary.md +1 -1
  23. package/rcode/agents/rcode-security-auditor.md +1 -1
  24. package/rcode/agents/rcode-sprint-checker.md +1 -1
  25. package/rcode/agents/rcode-verifier.md +1 -1
  26. package/rcode/agents/rcode-waleed.md +1 -1
  27. package/rcode/agents/rcode-yousef.md +1 -1
  28. package/rcode/agents/rules/planner/task-templates.md +24 -22
  29. package/rcode/agents/rules/verifier/verification-report.md +2 -2
  30. package/rcode/bin/lib/roadmap.cjs +80 -4
  31. package/rcode/bin/rcode-tools.cjs +124 -1
  32. package/rcode/brain/best-practices/state-sync-rule.md +1 -1
  33. package/rcode/references/council-protocol.md +9 -4
  34. package/rcode/references/execute-auto-copy-learnings.md +15 -0
  35. package/rcode/references/execute-close-parent-artifacts.md +49 -0
  36. package/rcode/references/execute-interactive-mode.md +44 -0
  37. package/rcode/references/execute-notify-webhooks.md +22 -0
  38. package/rcode/references/execution-protocol.md +11 -11
  39. package/rcode/references/git-preflight.md +7 -4
  40. package/rcode/references/output-format.md +5 -5
  41. package/rcode/references/plan-gaps-mode.md +89 -0
  42. package/rcode/references/plan-thinking-partner.md +20 -0
  43. package/rcode/references/plan-windows-troubleshooting.md +25 -0
  44. package/rcode/references/planner-playbook.md +23 -29
  45. package/rcode/skills/_shared/state-sync-rule.md +1 -1
  46. package/rcode/skills/actions/1-analysis/rcode-document-project/SKILL.md +2 -0
  47. package/rcode/skills/actions/1-analysis/rcode-prfaq/SKILL.md +2 -0
  48. package/rcode/skills/actions/2-plan/rcode-create-epics-and-stories/SKILL.md +11 -0
  49. package/rcode/skills/actions/2-plan/rcode-create-prd/SKILL.md +2 -0
  50. package/rcode/skills/actions/2-plan/rcode-create-story/SKILL.md +11 -0
  51. package/rcode/skills/actions/2-plan/rcode-edit-prd/SKILL.md +2 -0
  52. package/rcode/skills/actions/2-plan/rcode-validate-prd/SKILL.md +2 -0
  53. package/rcode/skills/actions/3-solutioning/rcode-check-implementation-readiness/SKILL.md +2 -0
  54. package/rcode/skills/actions/3-solutioning/rcode-create-architecture/SKILL.md +2 -0
  55. package/rcode/skills/actions/4-implementation/rcode-checkpoint-preview/SKILL.md +2 -0
  56. package/rcode/skills/actions/4-implementation/rcode-code-review/SKILL.md +2 -0
  57. package/rcode/skills/actions/4-implementation/rcode-correct-course/SKILL.md +2 -0
  58. package/rcode/skills/actions/4-implementation/rcode-debug/SKILL.md +1 -1
  59. package/rcode/skills/actions/4-implementation/rcode-dev-story/SKILL.md +2 -0
  60. package/rcode/skills/actions/4-implementation/rcode-retrospective/SKILL.md +2 -0
  61. package/rcode/skills/actions/4-implementation/rcode-scaffold-project/SKILL.md +2 -0
  62. package/rcode/skills/actions/4-implementation/rcode-sprint-planning/SKILL.md +3 -2
  63. package/rcode/skills/actions/4-implementation/rcode-sprint-status/SKILL.md +2 -0
  64. package/rcode/skills/agents/haitham-frontend/SKILL.md +6 -0
  65. package/rcode/skills/agents/hanzla-engineer/SKILL.md +6 -0
  66. package/rcode/skills/agents/hussain-pm/SKILL.md +1 -1
  67. package/rcode/skills/agents/hussain-sm/SKILL.md +3 -3
  68. package/rcode/skills/agents/majlis-council/references.md +17 -7
  69. package/rcode/skills/agents/raees-orchestrator/SKILL.md +13 -11
  70. package/rcode/skills/agents/yousef-backend/SKILL.md +6 -0
  71. package/rcode/templates/github/epic-template.md +1 -1
  72. package/rcode/templates/github/feature-template.md +1 -1
  73. package/rcode/templates/github/task-template.md +1 -1
  74. package/rcode/templates/sprint.md +14 -3
  75. package/rcode/templates/verification-report.md +7 -4
  76. package/rcode/workflows/add-phase.md +33 -14
  77. package/rcode/workflows/code-review-fix.md +5 -5
  78. package/rcode/workflows/code-review.md +1 -1
  79. package/rcode/workflows/council.md +1 -0
  80. package/rcode/workflows/create-epics-and-stories.md +12 -6
  81. package/rcode/workflows/dev-story.md +12 -14
  82. package/rcode/workflows/do.md +12 -9
  83. package/rcode/workflows/execute-sprint.md +7 -4
  84. package/rcode/workflows/execute-waves.md +38 -2
  85. package/rcode/workflows/execute.md +42 -138
  86. package/rcode/workflows/init.md +10 -2
  87. package/rcode/workflows/lens-audit.md +7 -7
  88. package/rcode/workflows/plan-research-validation.md +10 -5
  89. package/rcode/workflows/plan-spawn-planner.md +9 -14
  90. package/rcode/workflows/plan.md +27 -138
  91. package/rcode/workflows/quick.md +1 -1
  92. package/rcode/workflows/scaffold-skill.md +19 -1
  93. package/rcode/workflows/secure-phase.md +7 -1
  94. package/rcode/workflows/sprint-planning.md +14 -37
  95. package/rcode/workflows/validate-phase.md +7 -1
@@ -0,0 +1,304 @@
1
+ /**
2
+ * cli/lib/github-sync-discover.cjs — phase/epic/story discovery for github-sync
3
+ *
4
+ * Owns discovery for BOTH artifact tracks this project produces:
5
+ * - sprint-track: .planning/phases/{slug}/{phase}-{plan}-SPRINT.md
6
+ * (`<task id="" title="">` XML blocks, current planner output)
7
+ * - epic-track: .planning/epics/EPIC-{NN}.md + .planning/epics/stories/{N}.{M}.md
8
+ * (PRD → epics → stories chain, phase-agnostic per docs/adr/0001)
9
+ *
10
+ * Required by both cli/github-sync.js (the CLI entry point) and
11
+ * test/github-sync.test.cjs (so discovery logic is exercised directly instead
12
+ * of being duplicated inline in tests).
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ /**
19
+ * Parse YAML-ish frontmatter from the top of a markdown file.
20
+ * Used to extract explicit epic/sprint linking from story files.
21
+ * Returns an object of key/value strings, or {} if no frontmatter block.
22
+ */
23
+ function extractFrontmatter(content) {
24
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
25
+ if (!match) return {};
26
+ const fm = {};
27
+ for (const line of match[1].split(/\r?\n/)) {
28
+ const m = line.match(/^([\w_-]+)\s*:\s*(.*)$/);
29
+ if (m) fm[m[1].trim()] = m[2].trim().replace(/^["']|["']$/g, '');
30
+ }
31
+ return fm;
32
+ }
33
+
34
+ function extractTitle(markdown) {
35
+ const match = markdown.match(/^#\s+(.+)$/m);
36
+ return match ? match[1].trim() : null;
37
+ }
38
+
39
+ /**
40
+ * Parse `<task id="" title="">` blocks out of a SPRINT.md's text, mirroring
41
+ * server/lib/scanner.js's buildPhaseTree parser (the proven-working reference
42
+ * implementation for the current planner output format). Falls back to the
43
+ * legacy `### Story|Task N — title` heading format when no `<task>` blocks
44
+ * are present (pre-<task> SPRINT.md era).
45
+ *
46
+ * Each returned item becomes a story-equivalent GitHub issue — the
47
+ * sprint-track has no separate epic level; sprint grouping is surfaced via
48
+ * the `sprintId` field in the issue body's "Sprint:" line.
49
+ */
50
+ function parseSprintTasks(text, sprintId, file, sourcePathPrefix) {
51
+ const tasks = [];
52
+ const taskRe = /<task\b([^>]*)>([\s\S]*?)<\/task>/g;
53
+ let tm;
54
+ let n = 0;
55
+ while ((tm = taskRe.exec(text))) {
56
+ n += 1;
57
+ const idM = tm[1].match(/id="([^"]+)"/);
58
+ const titleAttrM = tm[1].match(/title="([^"]*)"/);
59
+ const titleTagM = tm[2].match(/<title>([\s\S]*?)<\/title>/);
60
+ tasks.push({
61
+ id: idM ? idM[1] : `${sprintId}-task-${n}`,
62
+ file,
63
+ content: tm[0],
64
+ title: (titleAttrM && titleAttrM[1].trim()) || (titleTagM && titleTagM[1].trim()) || `Task ${n}`,
65
+ parentEpic: null,
66
+ sprintId,
67
+ frontmatter: {},
68
+ sourcePath: `${sourcePathPrefix}/${file}`,
69
+ });
70
+ }
71
+
72
+ // Fallback for pre-<task> SPRINT.md format: "### Story N — title" / "### Task X — title".
73
+ if (tasks.length === 0) {
74
+ const headRe = /^#{2,4}\s+(?:Story|Task)\s+([^\s—–-]+)\s*[—–-]\s*(.+?)\s*$/gm;
75
+ let hm;
76
+ while ((hm = headRe.exec(text))) {
77
+ tasks.push({
78
+ id: hm[1].trim(),
79
+ file,
80
+ content: hm[0],
81
+ title: hm[2].trim(),
82
+ parentEpic: null,
83
+ sprintId,
84
+ frontmatter: {},
85
+ sourcePath: `${sourcePathPrefix}/${file}`,
86
+ });
87
+ }
88
+ }
89
+
90
+ return tasks;
91
+ }
92
+
93
+ /**
94
+ * Discover sprint-track phases from .planning/phases/*-SPRINT.md files.
95
+ * Returns every discovered phase directory, even ones with zero SPRINT.md
96
+ * files (parity with the dead legacy directory-tree discovery behavior).
97
+ */
98
+ function discoverSprintTrackPhases(cwd) {
99
+ const phasesDir = path.join(cwd, '.planning', 'phases');
100
+ if (!fs.existsSync(phasesDir)) return [];
101
+
102
+ const phases = [];
103
+ for (const entry of fs.readdirSync(phasesDir, { withFileTypes: true })) {
104
+ if (!entry.isDirectory()) continue;
105
+
106
+ const numericIdM = entry.name.match(/^(\d+)-/);
107
+ const numericId = numericIdM ? numericIdM[1] : null;
108
+ const phaseDir = path.join(phasesDir, entry.name);
109
+ const sourcePathPrefix = `.planning/phases/${entry.name}`;
110
+
111
+ const sprintFiles = fs
112
+ .readdirSync(phaseDir)
113
+ .filter((f) => /-SPRINT\.md$/i.test(f))
114
+ .sort((a, b) => {
115
+ const na = parseInt((a.match(/-(\d+)-SPRINT\.md$/i) || [])[1] || '0', 10);
116
+ const nb = parseInt((b.match(/-(\d+)-SPRINT\.md$/i) || [])[1] || '0', 10);
117
+ return na - nb;
118
+ });
119
+
120
+ const stories = [];
121
+ for (const file of sprintFiles) {
122
+ const text = fs.readFileSync(path.join(phaseDir, file), 'utf8');
123
+ const fileM = file.match(/^(\d+)-(\d+)-SPRINT\.md$/i);
124
+ let sprintId;
125
+ if (fileM) {
126
+ sprintId = `${parseInt(fileM[1], 10)}.${parseInt(fileM[2], 10)}`;
127
+ } else {
128
+ const fm = extractFrontmatter(text);
129
+ sprintId = fm.sprint || file.replace(/-SPRINT\.md$/i, '');
130
+ }
131
+ stories.push(...parseSprintTasks(text, sprintId, file, sourcePathPrefix));
132
+ }
133
+
134
+ phases.push({
135
+ id: entry.name,
136
+ numericId,
137
+ brief: null,
138
+ sprints: null,
139
+ sprintMap: {},
140
+ stories,
141
+ epics: [],
142
+ noMilestone: false,
143
+ });
144
+ }
145
+
146
+ return phases;
147
+ }
148
+
149
+ /**
150
+ * Discover the epic-track from .planning/epics/EPIC-{NN}.md +
151
+ * .planning/epics/stories/{N}.{M}.md, per the layout in
152
+ * rcode/workflows/create-epics-and-stories.md. Story files use bold
153
+ * Markdown fields (`**Epic:** EPIC-{N} — {title}`), NOT YAML frontmatter —
154
+ * extractFrontmatter correctly returns {} for these and is not used here.
155
+ *
156
+ * Epic file names are zero-padded (EPIC-01.md) but a story's `**Epic:**`
157
+ * field is unpadded (EPIC-1) — match by NUMERIC value so padding
158
+ * differences never break the parent link.
159
+ *
160
+ * Returns null when .planning/epics/ doesn't exist, or when it exists but
161
+ * has no epics and no stories (nothing to sync — avoid a spurious empty
162
+ * synthetic phase).
163
+ */
164
+ function discoverEpicTrackPhase(cwd) {
165
+ const epicsDir = path.join(cwd, '.planning', 'epics');
166
+ if (!fs.existsSync(epicsDir)) return null;
167
+
168
+ const epics = [];
169
+ const epicFiles = fs.readdirSync(epicsDir).filter((f) => /^EPIC-\d+\.md$/i.test(f));
170
+ for (const file of epicFiles) {
171
+ const content = fs.readFileSync(path.join(epicsDir, file), 'utf8');
172
+ const numM = file.match(/(\d+)/);
173
+ const epicNumber = numM ? parseInt(numM[1], 10) : null;
174
+ const id = file.replace(/\.md$/i, '');
175
+ epics.push({
176
+ id,
177
+ file,
178
+ epicNumber,
179
+ content,
180
+ title: extractTitle(content) || id,
181
+ frontmatter: {},
182
+ sourcePath: `.planning/epics/${file}`,
183
+ });
184
+ }
185
+
186
+ const stories = [];
187
+ const storiesDir = path.join(epicsDir, 'stories');
188
+ if (fs.existsSync(storiesDir)) {
189
+ const storyFiles = fs.readdirSync(storiesDir).filter((f) => /^[\d.]+\.md$/.test(f));
190
+ for (const file of storyFiles) {
191
+ const content = fs.readFileSync(path.join(storiesDir, file), 'utf8');
192
+ const id = file.replace(/\.md$/i, '');
193
+ const epicRefM = content.match(/\*\*Epic:\*\*\s*EPIC-(\d+)/i);
194
+ let parentEpic = null;
195
+ if (epicRefM) {
196
+ const refNum = parseInt(epicRefM[1], 10);
197
+ const matchedEpic = epics.find((e) => e.epicNumber === refNum);
198
+ if (matchedEpic) parentEpic = matchedEpic.id;
199
+ }
200
+ stories.push({
201
+ id,
202
+ file,
203
+ content,
204
+ title: extractTitle(content) || id,
205
+ parentEpic,
206
+ sprintId: null,
207
+ frontmatter: {},
208
+ sourcePath: `.planning/epics/stories/${file}`,
209
+ });
210
+ }
211
+ }
212
+
213
+ if (epics.length === 0 && stories.length === 0) return null;
214
+
215
+ return {
216
+ id: 'epics',
217
+ numericId: null,
218
+ brief: null,
219
+ sprints: null,
220
+ sprintMap: {},
221
+ stories,
222
+ epics,
223
+ // Signals this synthetic phase has no numbered-phase milestone to
224
+ // attach to on GitHub — epic-track is a deliberately phase-agnostic
225
+ // PRD→epics→stories chain per docs/adr/0001, distinct from the numbered
226
+ // .planning/phases/ milestones.
227
+ noMilestone: true,
228
+ };
229
+ }
230
+
231
+ /**
232
+ * Combine sprint-track and epic-track discovery into one phases array.
233
+ */
234
+ function discoverPhases(cwd) {
235
+ const phases = discoverSprintTrackPhases(cwd);
236
+ const epicPhase = discoverEpicTrackPhase(cwd);
237
+ if (epicPhase) phases.push(epicPhase);
238
+ return phases;
239
+ }
240
+
241
+ /**
242
+ * Apply granular --sprint/--epic/--story filters to the discovered phases.
243
+ * Mutates a shallow copy — original discovery result is not touched.
244
+ * Returns a new phases array where only the requested items remain.
245
+ */
246
+ function applyGranularFilters(phases, opts) {
247
+ // No filters → return as-is
248
+ if (!opts.sprint && !opts.epic && !opts.story) return phases;
249
+
250
+ const filtered = phases.map((p) => ({
251
+ ...p,
252
+ epics: [...p.epics],
253
+ stories: [...p.stories],
254
+ }));
255
+
256
+ if (opts.sprint) {
257
+ // Normalize both dash and dot sprint-id forms so --sprint=44-1 and
258
+ // --sprint=44.1 both match a stored sprintId of "44.1".
259
+ const sprintNorm = opts.sprint.replace(/-/g, '.');
260
+ for (const p of filtered) {
261
+ // Keep only stories whose sprintId matches
262
+ p.stories = p.stories.filter((s) => s.sprintId === opts.sprint || s.sprintId === sprintNorm);
263
+ // Keep only epics that have at least one remaining story or whose id
264
+ // the user might also be interested in (conservative: keep all epics
265
+ // in this phase so child stories have a visible parent reference)
266
+ // Actually: if filtering by sprint, user wants the sprint's work —
267
+ // stories only. Drop epics unless they're referenced by a remaining
268
+ // story.
269
+ const liveEpicIds = new Set(p.stories.map((s) => s.parentEpic).filter(Boolean));
270
+ p.epics = p.epics.filter((e) => liveEpicIds.has(e.id));
271
+ }
272
+ }
273
+
274
+ if (opts.epic) {
275
+ for (const p of filtered) {
276
+ p.epics = p.epics.filter((e) => e.id === opts.epic);
277
+ // Keep stories whose parentEpic matches
278
+ p.stories = p.stories.filter((s) => s.parentEpic === opts.epic);
279
+ }
280
+ }
281
+
282
+ if (opts.story) {
283
+ for (const p of filtered) {
284
+ p.stories = p.stories.filter((s) => s.id === opts.story);
285
+ // Keep any epic a surviving story points at (so link target exists)
286
+ const parents = new Set(p.stories.map((s) => s.parentEpic).filter(Boolean));
287
+ p.epics = p.epics.filter((e) => parents.has(e.id));
288
+ }
289
+ }
290
+
291
+ // Drop phases that ended up completely empty after filtering
292
+ return filtered.filter(
293
+ (p) => p.epics.length > 0 || p.stories.length > 0,
294
+ );
295
+ }
296
+
297
+ module.exports = {
298
+ extractFrontmatter,
299
+ extractTitle,
300
+ discoverSprintTrackPhases,
301
+ discoverEpicTrackPhase,
302
+ discoverPhases,
303
+ applyGranularFilters,
304
+ };
@@ -62,7 +62,22 @@ function findLegacyRihalArtifacts(claudeDir) {
62
62
  }
63
63
  }
64
64
 
65
- return { skills, commands };
65
+ const agents = [];
66
+ const agentsDir = path.join(claudeDir, 'agents');
67
+ for (const entry of listDirSafe(agentsDir)) {
68
+ if (!entry.isFile() || !entry.name.startsWith('rihal-') || !entry.name.endsWith('.md')) continue;
69
+ const twinName = 'rcode-' + entry.name.slice('rihal-'.length);
70
+ if (fs.existsSync(path.join(agentsDir, twinName))) {
71
+ agents.push({
72
+ name: entry.name,
73
+ twin: twinName,
74
+ srcPath: path.join(agentsDir, entry.name),
75
+ kind: 'agent',
76
+ });
77
+ }
78
+ }
79
+
80
+ return { skills, commands, agents };
66
81
  }
67
82
 
68
83
  /**
package/cli/uninstall.js CHANGED
@@ -261,9 +261,14 @@ function buildPlan(cwd, editors) {
261
261
  // v2 installs agents to .claude/agents/rcode-*.md — scan for them
262
262
  const agentsDir = path.join(cwd, '.claude/agents');
263
263
  if (fs.existsSync(agentsDir)) {
264
- plan.claude.agents = fs
265
- .readdirSync(agentsDir)
264
+ const agentFiles = fs.readdirSync(agentsDir);
265
+ plan.claude.agents = agentFiles
266
266
  .filter((name) => name.startsWith('rcode-') && name.endsWith('.md'));
267
+ // Stale pre-rebrand rihal-*.md twins never get removed otherwise (#991)
268
+ plan.claude.agents = [
269
+ ...plan.claude.agents,
270
+ ...agentFiles.filter((name) => name.startsWith('rihal-') && name.endsWith('.md')),
271
+ ];
267
272
  }
268
273
  // Installer copies rcode/agents/rules/ tree → .claude/agents/rules/ (#876)
269
274
  if (fs.existsSync(path.join(cwd, '.claude/agents/rules'))) {