@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.
- package/AGENTS.md +1 -1
- package/CLAUDE.md +1 -1
- package/CONTRIBUTING.md +3 -0
- package/README.md +7 -5
- package/cli/github-sync.js +17 -201
- package/cli/install.js +16 -13
- package/cli/lib/config.cjs +1 -1
- package/cli/lib/github-sync-discover.cjs +304 -0
- package/cli/lib/namespace-migrate.cjs +16 -1
- package/cli/uninstall.js +7 -2
- package/dist/rcode.js +197 -197
- package/package.json +1 -1
- package/rcode/agents/rcode-code-reviewer.md +1 -1
- package/rcode/agents/rcode-docs-auditor.md +1 -1
- package/rcode/agents/rcode-edge-case-hunter.md +1 -1
- package/rcode/agents/rcode-haitham.md +1 -1
- package/rcode/agents/rcode-hanzla.md +1 -1
- package/rcode/agents/rcode-hussain-pm.md +1 -1
- package/rcode/agents/rcode-nyquist-auditor.md +1 -1
- package/rcode/agents/rcode-omar.md +1 -1
- package/rcode/agents/rcode-remediation-planner.md +1 -1
- package/rcode/agents/rcode-security-adversary.md +1 -1
- package/rcode/agents/rcode-security-auditor.md +1 -1
- package/rcode/agents/rcode-sprint-checker.md +1 -1
- package/rcode/agents/rcode-verifier.md +1 -1
- package/rcode/agents/rcode-waleed.md +1 -1
- package/rcode/agents/rcode-yousef.md +1 -1
- package/rcode/agents/rules/planner/task-templates.md +24 -22
- package/rcode/agents/rules/verifier/verification-report.md +2 -2
- package/rcode/bin/lib/roadmap.cjs +80 -4
- package/rcode/bin/rcode-tools.cjs +124 -1
- package/rcode/brain/best-practices/state-sync-rule.md +1 -1
- package/rcode/references/council-protocol.md +9 -4
- package/rcode/references/execute-auto-copy-learnings.md +15 -0
- package/rcode/references/execute-close-parent-artifacts.md +49 -0
- package/rcode/references/execute-interactive-mode.md +44 -0
- package/rcode/references/execute-notify-webhooks.md +22 -0
- package/rcode/references/execution-protocol.md +11 -11
- package/rcode/references/git-preflight.md +7 -4
- package/rcode/references/output-format.md +5 -5
- package/rcode/references/plan-gaps-mode.md +89 -0
- package/rcode/references/plan-thinking-partner.md +20 -0
- package/rcode/references/plan-windows-troubleshooting.md +25 -0
- package/rcode/references/planner-playbook.md +23 -29
- package/rcode/skills/_shared/state-sync-rule.md +1 -1
- package/rcode/skills/actions/1-analysis/rcode-document-project/SKILL.md +2 -0
- package/rcode/skills/actions/1-analysis/rcode-prfaq/SKILL.md +2 -0
- package/rcode/skills/actions/2-plan/rcode-create-epics-and-stories/SKILL.md +11 -0
- package/rcode/skills/actions/2-plan/rcode-create-prd/SKILL.md +2 -0
- package/rcode/skills/actions/2-plan/rcode-create-story/SKILL.md +11 -0
- package/rcode/skills/actions/2-plan/rcode-edit-prd/SKILL.md +2 -0
- package/rcode/skills/actions/2-plan/rcode-validate-prd/SKILL.md +2 -0
- package/rcode/skills/actions/3-solutioning/rcode-check-implementation-readiness/SKILL.md +2 -0
- package/rcode/skills/actions/3-solutioning/rcode-create-architecture/SKILL.md +2 -0
- package/rcode/skills/actions/4-implementation/rcode-checkpoint-preview/SKILL.md +2 -0
- package/rcode/skills/actions/4-implementation/rcode-code-review/SKILL.md +2 -0
- package/rcode/skills/actions/4-implementation/rcode-correct-course/SKILL.md +2 -0
- package/rcode/skills/actions/4-implementation/rcode-debug/SKILL.md +1 -1
- package/rcode/skills/actions/4-implementation/rcode-dev-story/SKILL.md +2 -0
- package/rcode/skills/actions/4-implementation/rcode-retrospective/SKILL.md +2 -0
- package/rcode/skills/actions/4-implementation/rcode-scaffold-project/SKILL.md +2 -0
- package/rcode/skills/actions/4-implementation/rcode-sprint-planning/SKILL.md +3 -2
- package/rcode/skills/actions/4-implementation/rcode-sprint-status/SKILL.md +2 -0
- package/rcode/skills/agents/haitham-frontend/SKILL.md +6 -0
- package/rcode/skills/agents/hanzla-engineer/SKILL.md +6 -0
- package/rcode/skills/agents/hussain-pm/SKILL.md +1 -1
- package/rcode/skills/agents/hussain-sm/SKILL.md +3 -3
- package/rcode/skills/agents/majlis-council/references.md +17 -7
- package/rcode/skills/agents/raees-orchestrator/SKILL.md +13 -11
- package/rcode/skills/agents/yousef-backend/SKILL.md +6 -0
- package/rcode/templates/github/epic-template.md +1 -1
- package/rcode/templates/github/feature-template.md +1 -1
- package/rcode/templates/github/task-template.md +1 -1
- package/rcode/templates/sprint.md +14 -3
- package/rcode/templates/verification-report.md +7 -4
- package/rcode/workflows/add-phase.md +33 -14
- package/rcode/workflows/code-review-fix.md +5 -5
- package/rcode/workflows/code-review.md +1 -1
- package/rcode/workflows/council.md +1 -0
- package/rcode/workflows/create-epics-and-stories.md +12 -6
- package/rcode/workflows/dev-story.md +12 -14
- package/rcode/workflows/do.md +12 -9
- package/rcode/workflows/execute-sprint.md +7 -4
- package/rcode/workflows/execute-waves.md +38 -2
- package/rcode/workflows/execute.md +42 -138
- package/rcode/workflows/init.md +10 -2
- package/rcode/workflows/lens-audit.md +7 -7
- package/rcode/workflows/plan-research-validation.md +10 -5
- package/rcode/workflows/plan-spawn-planner.md +9 -14
- package/rcode/workflows/plan.md +27 -138
- package/rcode/workflows/quick.md +1 -1
- package/rcode/workflows/scaffold-skill.md +19 -1
- package/rcode/workflows/secure-phase.md +7 -1
- package/rcode/workflows/sprint-planning.md +14 -37
- 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
|
-
|
|
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
|
-
|
|
265
|
-
|
|
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'))) {
|