@hanzlaa/rcode 4.8.0 → 4.9.1
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/CONTRIBUTING.md +3 -0
- package/README.md +7 -5
- package/cli/doctor.js +4 -2
- package/cli/github-sync.js +26 -17
- package/cli/install.js +59 -34
- package/cli/lib/namespace-migrate.cjs +62 -4
- package/cli/migrate-namespace.js +4 -0
- package/dist/rcode.js +207 -207
- 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-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/bin/lib/brain.cjs +16 -1
- package/rcode/bin/lib/gitignore.cjs +3 -5
- package/rcode/bin/rcode-hooks.cjs +22 -5
- package/rcode/bin/rcode-tools.cjs +166 -14
- package/rcode/data/intent-table.json +1 -1
- package/rcode/references/git-preflight.md +5 -2
- package/rcode/references/output-format.md +5 -5
- package/rcode/workflows/add-phase.md +33 -14
- package/rcode/workflows/do.md +33 -1
- package/rcode/workflows/execute-sprint.md +3 -4
- package/rcode/workflows/execute-waves.md +25 -32
- package/rcode/workflows/execute.md +80 -21
- package/rcode/workflows/init.md +28 -6
- package/rcode/workflows/plan-research-validation.md +10 -5
- package/rcode/workflows/plan-spawn-planner.md +9 -13
- package/rcode/workflows/plan.md +2 -2
- package/rcode/workflows/scaffold-skill.md +19 -1
- package/rcode/workflows/scan.md +22 -1
- package/rcode/workflows/secure-phase.md +7 -1
- package/rcode/workflows/validate-phase.md +7 -1
- package/server/lib/html/client/components/Sidebar.js +8 -5
- package/server/lib/html/client/views/PhasesView.js +1 -1
- package/server/lib/html/css.js +4 -0
- package/server/lib/scanner.js +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzlaa/rcode",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.9.1",
|
|
4
4
|
"description": "rcode — the AI team that never forgets. Persistent memory, specialist agents, and slash commands for AI IDEs. Works in Claude Code, Cursor, Gemini, VS Code, and Antigravity.",
|
|
5
5
|
"main": "cli/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -6,7 +6,7 @@ color: yellow
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
@.rcode/references/response-style.md
|
|
9
|
-
@.rcode/references/karpathy-guidelines
|
|
9
|
+
@.rcode/references/karpathy-guidelines.md
|
|
10
10
|
@.rcode/references/no-unauthorized-git-ops.md
|
|
11
11
|
@.rcode/references/auditor-shared-checklists.md
|
|
12
12
|
@.rcode/references/docs-auditor-playbook.md
|
package/rcode/bin/lib/brain.cjs
CHANGED
|
@@ -243,7 +243,22 @@ function cmdBrain(args, { PROJECT_ROOT, RCODE_DIR }) {
|
|
|
243
243
|
const os = require('os');
|
|
244
244
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'rcode-brain-'));
|
|
245
245
|
const branch = s.branch || cfg.defaults?.branch || 'main';
|
|
246
|
-
|
|
246
|
+
// #1029 — `sparse-checkout set --no-cone` treats each path as a
|
|
247
|
+
// .gitignore-style pattern, not a literal path pin. A bare filename
|
|
248
|
+
// (no '/', no wildcard) matches that filename at any depth in the repo
|
|
249
|
+
// tree, over-fetching every same-named file repo-wide. Anchor bare
|
|
250
|
+
// filenames to the repo root with a leading '/' so they pin the
|
|
251
|
+
// root-level file only. Patterns that already start with '/', contain a
|
|
252
|
+
// '/', or use wildcards (already scoped or intentionally recursive) are
|
|
253
|
+
// left untouched.
|
|
254
|
+
function anchorBareFilename(p) {
|
|
255
|
+
const str = String(p || '');
|
|
256
|
+
if (str.startsWith('/')) return str;
|
|
257
|
+
if (/[*?[]/.test(str)) return str;
|
|
258
|
+
if (str.includes('/')) return str;
|
|
259
|
+
return `/${str}`;
|
|
260
|
+
}
|
|
261
|
+
const sparsePaths = (Array.isArray(s.paths) ? s.paths : []).map(anchorBareFilename);
|
|
247
262
|
|
|
248
263
|
// Cache key = sha1(repo + branch + sparsePaths joined). Changing any of
|
|
249
264
|
// those gets a fresh cache slot. Different projects pulling the same
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Gitignore — extracted from rcode-tools.cjs (issue #204).
|
|
3
|
-
* move, no behavior change (including the pre-existing `slice_end` typo in
|
|
4
|
-
* spliceBlock, left untouched — out of scope for this extraction).
|
|
2
|
+
* Gitignore — extracted from rcode-tools.cjs (issue #204).
|
|
5
3
|
*/
|
|
6
4
|
|
|
7
5
|
const fs = require('fs');
|
|
@@ -102,8 +100,8 @@ function cmdGitignore(args, { PROJECT_ROOT, RCODE_DIR }) {
|
|
|
102
100
|
let sliceStart = start;
|
|
103
101
|
if (sliceStart > 0 && existing[sliceStart - 1] === '\n') sliceStart -= 1;
|
|
104
102
|
let sliceEnd = endIdx + END.length;
|
|
105
|
-
if (existing[
|
|
106
|
-
return existing.slice(0, sliceStart) + newBlock + existing.slice(
|
|
103
|
+
if (existing[sliceEnd] === '\n') sliceEnd += 1;
|
|
104
|
+
return existing.slice(0, sliceStart) + newBlock + existing.slice(sliceEnd);
|
|
107
105
|
}
|
|
108
106
|
|
|
109
107
|
if (!fs.existsSync(gitignorePath)) {
|
|
@@ -743,7 +743,10 @@ function parseSimpleYamlInline(text) {
|
|
|
743
743
|
/**
|
|
744
744
|
* Read prompt_nudge from .rcode/config.yaml.
|
|
745
745
|
* Returns 'every' | 'once-per-intent' | 'when-stale' | 'off'.
|
|
746
|
-
* Defaults to '
|
|
746
|
+
* Defaults to 'once-per-intent' when key is absent, file is missing, or value
|
|
747
|
+
* is unknown — #953: 'every' re-nudged on every matching prompt during an
|
|
748
|
+
* active session, which read as noise once the keyword matcher's false
|
|
749
|
+
* positives compounded it. 'every' is still available as an opt-in.
|
|
747
750
|
*/
|
|
748
751
|
function readPromptNudgeToggle(cwd) {
|
|
749
752
|
const VALID = new Set(['every', 'once-per-intent', 'when-stale', 'off']);
|
|
@@ -752,9 +755,9 @@ function readPromptNudgeToggle(cwd) {
|
|
|
752
755
|
const text = fs.readFileSync(cfgPath, 'utf8');
|
|
753
756
|
const parsed = parseSimpleYamlInline(text);
|
|
754
757
|
const val = (parsed.prompt_nudge || '').trim().toLowerCase();
|
|
755
|
-
return VALID.has(val) ? val : '
|
|
758
|
+
return VALID.has(val) ? val : 'once-per-intent';
|
|
756
759
|
} catch {
|
|
757
|
-
return '
|
|
760
|
+
return 'once-per-intent';
|
|
758
761
|
}
|
|
759
762
|
}
|
|
760
763
|
|
|
@@ -791,6 +794,20 @@ function isStateStaleFallbackTrue(cwd) {
|
|
|
791
794
|
}
|
|
792
795
|
}
|
|
793
796
|
|
|
797
|
+
/**
|
|
798
|
+
* Word-boundary keyword match — #953: plain `lower.includes(kw)` matched
|
|
799
|
+
* generic keywords like "bug" and "crash" inside unrelated words ("debugger",
|
|
800
|
+
* "crashing"), over-firing the debug nudge. Boundaries are checked against
|
|
801
|
+
* Unicode letters/numbers (not just ASCII \w) so Arabic/Urdu keyword phrases
|
|
802
|
+
* still match correctly.
|
|
803
|
+
*/
|
|
804
|
+
function keywordMatches(lower, kw) {
|
|
805
|
+
const kwLower = kw.toLowerCase();
|
|
806
|
+
const escaped = kwLower.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
807
|
+
const re = new RegExp(`(?<![\\p{L}\\p{N}])${escaped}(?![\\p{L}\\p{N}])`, 'u');
|
|
808
|
+
return re.test(lower);
|
|
809
|
+
}
|
|
810
|
+
|
|
794
811
|
/**
|
|
795
812
|
* prompt-router: Nudge toward rcode commands for memory consistency (#892).
|
|
796
813
|
* Reads stdin synchronously (NOT async — rejects bad JSON). Keyword-matches INTENT_TABLE,
|
|
@@ -868,12 +885,12 @@ function promptRouter() {
|
|
|
868
885
|
process.exit(0);
|
|
869
886
|
}
|
|
870
887
|
|
|
871
|
-
// ── Keyword match (first-match-wins, case-insensitive)
|
|
888
|
+
// ── Keyword match (first-match-wins, case-insensitive, word-boundary) ─
|
|
872
889
|
const lower = prompt.toLowerCase();
|
|
873
890
|
let matched = null;
|
|
874
891
|
for (const entry of INTENT_TABLE) {
|
|
875
892
|
for (const kw of entry.keywords) {
|
|
876
|
-
if (lower
|
|
893
|
+
if (keywordMatches(lower, kw)) {
|
|
877
894
|
matched = entry;
|
|
878
895
|
break;
|
|
879
896
|
}
|
|
@@ -4547,9 +4547,11 @@ function cmdGenerateClaudeMd(rawArgs) {
|
|
|
4547
4547
|
const force = args.includes('--force');
|
|
4548
4548
|
const claudeMdPath = path.join(PROJECT_ROOT, 'CLAUDE.md');
|
|
4549
4549
|
const agentsMdPath = path.join(PROJECT_ROOT, 'AGENTS.md');
|
|
4550
|
+
const claudeExisted = fs.existsSync(claudeMdPath);
|
|
4551
|
+
const agentsExisted = fs.existsSync(agentsMdPath);
|
|
4550
4552
|
|
|
4551
|
-
if (
|
|
4552
|
-
throw new Error(`CLAUDE.md already
|
|
4553
|
+
if (claudeExisted && agentsExisted && !force) {
|
|
4554
|
+
throw new Error(`CLAUDE.md and AGENTS.md already exist at ${PROJECT_ROOT}. Use --force to overwrite.`);
|
|
4553
4555
|
}
|
|
4554
4556
|
|
|
4555
4557
|
// Resolve project name from package.json or directory.
|
|
@@ -4647,26 +4649,33 @@ Before handling planning, exploration, auditing, refactoring, or multi-step buil
|
|
|
4647
4649
|
**This file is part of the project. Treat it as load-bearing.**
|
|
4648
4650
|
`;
|
|
4649
4651
|
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
+
// Each file's own existence gates only its own write — a project with an
|
|
4653
|
+
// existing CLAUDE.md must still get a missing AGENTS.md backfilled, and
|
|
4654
|
+
// vice versa (#1025).
|
|
4655
|
+
const wroteClaude = !claudeExisted || force;
|
|
4656
|
+
if (wroteClaude) {
|
|
4657
|
+
fs.writeFileSync(claudeMdPath, content);
|
|
4658
|
+
}
|
|
4652
4659
|
|
|
4653
4660
|
// Mirror the same rules to AGENTS.md (the cross-tool standard Codex, Cursor,
|
|
4654
4661
|
// Windsurf, Antigravity, and Gemini read). Skip when it already exists without
|
|
4655
4662
|
// --force so an install-appended "## rcode Agents (installed)" roster survives.
|
|
4656
|
-
const agentsExisted = fs.existsSync(agentsMdPath);
|
|
4657
4663
|
const wroteAgents = !agentsExisted || force;
|
|
4658
4664
|
if (wroteAgents) {
|
|
4659
4665
|
fs.writeFileSync(agentsMdPath, content);
|
|
4660
4666
|
}
|
|
4661
4667
|
|
|
4668
|
+
const writtenPaths = [];
|
|
4669
|
+
if (wroteClaude) writtenPaths.push(path.relative(PROJECT_ROOT, claudeMdPath));
|
|
4670
|
+
if (wroteAgents) writtenPaths.push(path.relative(PROJECT_ROOT, agentsMdPath));
|
|
4671
|
+
|
|
4662
4672
|
return {
|
|
4663
4673
|
ok: true,
|
|
4664
4674
|
path: path.relative(PROJECT_ROOT, claudeMdPath),
|
|
4665
|
-
paths:
|
|
4666
|
-
? [path.relative(PROJECT_ROOT, claudeMdPath), path.relative(PROJECT_ROOT, agentsMdPath)]
|
|
4667
|
-
: [path.relative(PROJECT_ROOT, claudeMdPath)],
|
|
4675
|
+
paths: writtenPaths,
|
|
4668
4676
|
project_name: projectName,
|
|
4669
|
-
overwritten: force && claudeExisted,
|
|
4677
|
+
overwritten: force && (claudeExisted || agentsExisted),
|
|
4678
|
+
claude_md_skipped: !wroteClaude,
|
|
4670
4679
|
agents_md_skipped: !wroteAgents,
|
|
4671
4680
|
};
|
|
4672
4681
|
}
|
|
@@ -5283,18 +5292,55 @@ function cmdPhasePlanIndex(rawArgs) {
|
|
|
5283
5292
|
const stem = file.replace(/-SPRINT\.md$/i, '');
|
|
5284
5293
|
const text = fs.readFileSync(path.join(phaseDir, file), 'utf8');
|
|
5285
5294
|
const { frontmatter, body } = parseFrontmatter(text);
|
|
5295
|
+
let block = '';
|
|
5296
|
+
if (text.startsWith('---\n')) {
|
|
5297
|
+
const end = text.indexOf('\n---\n', 4);
|
|
5298
|
+
if (end !== -1) block = text.slice(4, end);
|
|
5299
|
+
}
|
|
5286
5300
|
const id = frontmatter.sprint || frontmatter.plan || stem;
|
|
5287
|
-
|
|
5288
|
-
|
|
5301
|
+
// `wave:` may be an explicit scalar key, or absent — in which case it's
|
|
5302
|
+
// derived from `depends_on` below (block-list or inline form, issue #951).
|
|
5303
|
+
const hasExplicitWave = /^wave\s*:\s*\d+/m.test(block);
|
|
5304
|
+
const wave = hasExplicitWave ? (parseInt(frontmatter.wave, 10) || 1) : null;
|
|
5305
|
+
const dependsOn = fmListField(block, 'depends_on');
|
|
5306
|
+
const hasAutonomousKey = /^autonomous\s*:/m.test(block);
|
|
5307
|
+
const autonomous = hasAutonomousKey
|
|
5308
|
+
? String(frontmatter.autonomous || '').toLowerCase() === 'true'
|
|
5309
|
+
: /<automated>/i.test(body);
|
|
5289
5310
|
const gapClosure = String(frontmatter.gap_closure || frontmatter.type || '').toLowerCase() === 'gap_closure';
|
|
5290
5311
|
const objMatch = body.match(/^##\s+(?:Objective|Goal)\s*\n+([^\n]+)/mi);
|
|
5291
5312
|
const objective = objMatch ? objMatch[1].trim() : (frontmatter.goal || '').replace(/^["']|["']$/g, '');
|
|
5292
|
-
const
|
|
5293
|
-
const
|
|
5313
|
+
const checkboxCount = (body.match(/^[-*]\s+\[[ xX]\]/gm) || []).length;
|
|
5314
|
+
const storyHeaderCount = (body.match(/^###\s+Story\s+\S+/gm) || []).length;
|
|
5315
|
+
const taskCount = checkboxCount > 0 ? checkboxCount : storyHeaderCount;
|
|
5316
|
+
const filesModifiedList = fmListField(block, 'files_modified');
|
|
5317
|
+
const filesModified = filesModifiedList.length > 0
|
|
5318
|
+
? filesModifiedList.length
|
|
5319
|
+
: (body.match(/^\s*-\s*path:\s*["']?([^"'\n]+)/gm) || []).length;
|
|
5294
5320
|
const hasSummary = summarySet.has(stem);
|
|
5295
5321
|
if (/checkpoint/i.test(body)) hasCheckpoints = true;
|
|
5296
|
-
return { id, wave, autonomous, gap_closure: gapClosure, objective, task_count: taskCount, files_modified: filesModified, has_summary: hasSummary, file: path.relative(PROJECT_ROOT, path.join(phaseDir, file)) };
|
|
5322
|
+
return { id, wave, dependsOn, autonomous, gap_closure: gapClosure, objective, task_count: taskCount, files_modified: filesModified, has_summary: hasSummary, file: path.relative(PROJECT_ROOT, path.join(phaseDir, file)) };
|
|
5297
5323
|
});
|
|
5324
|
+
|
|
5325
|
+
// Resolve waves left undetermined (no explicit `wave:` key) from depends_on:
|
|
5326
|
+
// wave(p) = 1 + max(wave of each same-phase dependency), or 1 if none.
|
|
5327
|
+
const idToPlan = new Map(plans.map((p) => [p.id, p]));
|
|
5328
|
+
function resolveWave(p, seen) {
|
|
5329
|
+
if (p.wave !== null) return p.wave;
|
|
5330
|
+
if (seen.has(p.id)) { p.wave = 1; return 1; }
|
|
5331
|
+
seen.add(p.id);
|
|
5332
|
+
let maxDepWave = 0;
|
|
5333
|
+
for (const depId of p.dependsOn) {
|
|
5334
|
+
const dep = idToPlan.get(depId);
|
|
5335
|
+
if (!dep) continue;
|
|
5336
|
+
maxDepWave = Math.max(maxDepWave, resolveWave(dep, seen));
|
|
5337
|
+
}
|
|
5338
|
+
p.wave = maxDepWave > 0 ? maxDepWave + 1 : 1;
|
|
5339
|
+
return p.wave;
|
|
5340
|
+
}
|
|
5341
|
+
for (const p of plans) resolveWave(p, new Set());
|
|
5342
|
+
for (const p of plans) delete p.dependsOn;
|
|
5343
|
+
|
|
5298
5344
|
const waves = {};
|
|
5299
5345
|
for (const p of plans) {
|
|
5300
5346
|
const k = String(p.wave);
|
|
@@ -5402,6 +5448,109 @@ function cmdPlanCheckWaveOverlaps(rawArgs) {
|
|
|
5402
5448
|
return { phase: phaseArg, phase_dir: path.relative(PROJECT_ROOT, phaseDir), plans_checked: plans.length, conflicts };
|
|
5403
5449
|
}
|
|
5404
5450
|
|
|
5451
|
+
/**
|
|
5452
|
+
* Deterministic frontend/backend glob check for classify-plan (issue #1021).
|
|
5453
|
+
* Mirrors the FRONTEND_GLOBS / BACKEND_GLOBS rules formerly hand-applied by
|
|
5454
|
+
* the orchestrating LLM in execute-waves.md — kept here as the single source
|
|
5455
|
+
* of truth so both the CLI and the workflow doc describe the same behavior.
|
|
5456
|
+
*/
|
|
5457
|
+
function matchesFrontendGlob(file) {
|
|
5458
|
+
const f = String(file || '').toLowerCase();
|
|
5459
|
+
if (/\.(tsx|jsx|css)$/.test(f)) return true;
|
|
5460
|
+
return f.includes('client') || f.includes('ui');
|
|
5461
|
+
}
|
|
5462
|
+
function matchesBackendGlob(file) {
|
|
5463
|
+
const f = String(file || '').toLowerCase();
|
|
5464
|
+
return f.includes('api') || f.includes('server') || f.includes('db') || f.includes('service');
|
|
5465
|
+
}
|
|
5466
|
+
|
|
5467
|
+
const CLASSIFY_PLAN_ROUTE = { frontend: 'rcode-haitham', backend: 'rcode-yousef', 'full-stack': 'rcode-hanzla', other: 'rcode-executor' };
|
|
5468
|
+
|
|
5469
|
+
function classifyPlanFiles(files, objective) {
|
|
5470
|
+
const touchesFrontend = files.some(matchesFrontendGlob);
|
|
5471
|
+
const touchesBackend = files.some(matchesBackendGlob);
|
|
5472
|
+
let classification;
|
|
5473
|
+
if (touchesFrontend && touchesBackend) classification = 'full-stack';
|
|
5474
|
+
else if (touchesFrontend) classification = 'frontend';
|
|
5475
|
+
else if (touchesBackend) classification = 'backend';
|
|
5476
|
+
else classification = 'other';
|
|
5477
|
+
|
|
5478
|
+
if (classification === 'other') {
|
|
5479
|
+
const obj = String(objective || '').toLowerCase();
|
|
5480
|
+
const frontendKeywords = ['react', 'component', 'ui', 'css', 'tailwind', 'frontend', 'client-side', 'accessibility', 'a11y'];
|
|
5481
|
+
const backendKeywords = ['api', 'endpoint', 'database', 'schema', 'service', 'queue', 'backend', 'server-side'];
|
|
5482
|
+
if (frontendKeywords.some((k) => obj.includes(k))) classification = 'frontend';
|
|
5483
|
+
else if (backendKeywords.some((k) => obj.includes(k))) classification = 'backend';
|
|
5484
|
+
}
|
|
5485
|
+
return classification;
|
|
5486
|
+
}
|
|
5487
|
+
|
|
5488
|
+
/**
|
|
5489
|
+
* classify-plan — deterministic replacement for execute-waves.md's
|
|
5490
|
+
* hand-computed FRONTEND_GLOBS/BACKEND_GLOBS classification (issue #1021).
|
|
5491
|
+
* A live execution run showed the orchestrating LLM never actually carried
|
|
5492
|
+
* out the prose pseudocode, so a plan with a "db"-containing path still fell
|
|
5493
|
+
* back to rcode-executor instead of rcode-yousef.
|
|
5494
|
+
*
|
|
5495
|
+
* Two call shapes:
|
|
5496
|
+
* classify-plan <phase> <plan-id> — reads files_modified/objective from the plan's SPRINT.md
|
|
5497
|
+
* classify-plan --files=a,b,c --objective="..." — classify an already-parsed list directly
|
|
5498
|
+
*/
|
|
5499
|
+
function cmdClassifyPlan(args) {
|
|
5500
|
+
const flags = {};
|
|
5501
|
+
const positional = [];
|
|
5502
|
+
for (const t of args) {
|
|
5503
|
+
if (t.startsWith('--files=')) flags.files = t.slice('--files='.length);
|
|
5504
|
+
else if (t.startsWith('--objective=')) flags.objective = t.slice('--objective='.length);
|
|
5505
|
+
else positional.push(t);
|
|
5506
|
+
}
|
|
5507
|
+
|
|
5508
|
+
let files = [];
|
|
5509
|
+
let objective = '';
|
|
5510
|
+
|
|
5511
|
+
if (flags.files !== undefined || flags.objective !== undefined) {
|
|
5512
|
+
files = flags.files ? flags.files.split(',').map((s) => s.trim()).filter(Boolean) : [];
|
|
5513
|
+
objective = flags.objective || '';
|
|
5514
|
+
} else {
|
|
5515
|
+
const [phaseArg, planArg] = positional;
|
|
5516
|
+
if (!phaseArg || !planArg) {
|
|
5517
|
+
throw new Error('Usage: classify-plan <phase> <plan-id> OR classify-plan --files=a,b,c --objective="text"');
|
|
5518
|
+
}
|
|
5519
|
+
const phasesDir = path.join(PLANNING_DIR, 'phases');
|
|
5520
|
+
const norm = phaseArg.replace(/^0+/, '') || '0';
|
|
5521
|
+
let phaseDir = null;
|
|
5522
|
+
if (fs.existsSync(phasesDir)) {
|
|
5523
|
+
for (const d of fs.readdirSync(phasesDir)) {
|
|
5524
|
+
const m = d.match(/^(\d+)(?:[-.])/);
|
|
5525
|
+
if (m && (m[1].replace(/^0+/, '') || '0') === norm) { phaseDir = path.join(phasesDir, d); break; }
|
|
5526
|
+
}
|
|
5527
|
+
}
|
|
5528
|
+
if (!phaseDir) throw new Error(`Phase not found: ${phaseArg}`);
|
|
5529
|
+
let planFile = null;
|
|
5530
|
+
for (const file of fs.readdirSync(phaseDir).filter((f) => /-SPRINT\.md$/i.test(f)).sort()) {
|
|
5531
|
+
const stem = file.replace(/-SPRINT\.md$/i, '');
|
|
5532
|
+
if (stem === planArg || stem.endsWith(`-${planArg}`)) { planFile = file; break; }
|
|
5533
|
+
const text = fs.readFileSync(path.join(phaseDir, file), 'utf8');
|
|
5534
|
+
const { frontmatter } = parseFrontmatter(text);
|
|
5535
|
+
if ((frontmatter.sprint || frontmatter.plan) === planArg) { planFile = file; break; }
|
|
5536
|
+
}
|
|
5537
|
+
if (!planFile) throw new Error(`Plan not found: ${planArg} in phase ${phaseArg}`);
|
|
5538
|
+
const text = fs.readFileSync(path.join(phaseDir, planFile), 'utf8');
|
|
5539
|
+
const { frontmatter, body } = parseFrontmatter(text);
|
|
5540
|
+
let block = '';
|
|
5541
|
+
if (text.startsWith('---\n')) {
|
|
5542
|
+
const end = text.indexOf('\n---\n', 4);
|
|
5543
|
+
if (end !== -1) block = text.slice(4, end);
|
|
5544
|
+
}
|
|
5545
|
+
files = fmListField(block, 'files_modified');
|
|
5546
|
+
const objMatch = body.match(/^##\s+(?:Objective|Goal)\s*\n+([^\n]+)/mi);
|
|
5547
|
+
objective = objMatch ? objMatch[1].trim() : (frontmatter.goal || '').replace(/^["']|["']$/g, '');
|
|
5548
|
+
}
|
|
5549
|
+
|
|
5550
|
+
const classification = classifyPlanFiles(files, objective);
|
|
5551
|
+
return { classification, subagent_type: CLASSIFY_PLAN_ROUTE[classification], files_checked: files.length };
|
|
5552
|
+
}
|
|
5553
|
+
|
|
5405
5554
|
/** phases list — directory inventory under .planning/phases with optional --type filter and --pick path. */
|
|
5406
5555
|
function cmdPhasesList(args) {
|
|
5407
5556
|
const argv = Array.isArray(args) ? args : String(args || '').trim().split(/\s+/).filter(Boolean);
|
|
@@ -6658,6 +6807,9 @@ async function main() {
|
|
|
6658
6807
|
case 'phase-plan-index':
|
|
6659
6808
|
result = cmdPhasePlanIndex(args.join(' '));
|
|
6660
6809
|
break;
|
|
6810
|
+
case 'classify-plan':
|
|
6811
|
+
result = cmdClassifyPlan(args);
|
|
6812
|
+
break;
|
|
6661
6813
|
case 'phases':
|
|
6662
6814
|
if (args[0] === 'list') { result = cmdPhasesList(args.slice(1)); if (result === undefined) return; }
|
|
6663
6815
|
else { console.error('Unknown phases subcommand. Valid: list'); process.exit(1); }
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
{
|
|
13
13
|
"intent": "debug",
|
|
14
|
-
"keywords": ["
|
|
14
|
+
"keywords": ["getting an error", "throwing an error", "error in the", "fix the error", "debug this", "crash", "not working", "exception", "traceback", "kharab", "masla", "ye kaam nahi kar raha", "yeh kaam nahi kar raha", "error a raha hai", "theek karo", "khud se crash", "خطأ", "مشكلة", "لا يعمل", "أصلح الخطأ", "تعطل البرنامج"],
|
|
15
15
|
"command": "/rcode-debug"
|
|
16
16
|
},
|
|
17
17
|
{
|
|
@@ -14,8 +14,11 @@ Run these read-only commands in order. Any failure halts the workflow with the f
|
|
|
14
14
|
# Check 1: working tree clean
|
|
15
15
|
DIRTY=$(git status --porcelain 2>/dev/null)
|
|
16
16
|
|
|
17
|
-
# Check 2: not on a protected branch
|
|
17
|
+
# Check 2: not on a protected branch (skipped entirely when `git.branching_strategy`
|
|
18
|
+
# config is `none` — committing directly to main/master is the deliberately configured
|
|
19
|
+
# workflow in that case)
|
|
18
20
|
BRANCH=$(git branch --show-current 2>/dev/null)
|
|
21
|
+
BRANCHING_STRATEGY=$(node .rcode/bin/rcode-tools.cjs config-get git.branching_strategy 2>/dev/null)
|
|
19
22
|
PROTECTED="main master develop v2-prototype"
|
|
20
23
|
|
|
21
24
|
# Check 3: branch follows naming convention
|
|
@@ -35,7 +38,7 @@ fi
|
|
|
35
38
|
The workflow MUST stop and print the banner below if ANY of:
|
|
36
39
|
|
|
37
40
|
- `DIRTY` is non-empty AND user did not pass `--allow-dirty`
|
|
38
|
-
- `BRANCH` is in `$PROTECTED` AND user did not pass `--on-main`
|
|
41
|
+
- `BRANCH` is in `$PROTECTED` AND `BRANCHING_STRATEGY` is not `none` AND user did not pass `--on-main`
|
|
39
42
|
- `BRANCH_OK` is `no` AND user did not pass `--allow-dirty` (branch-name lint is advisory if working tree is dirty AND user accepted the dirty override)
|
|
40
43
|
- `OUT_OF_SCOPE` is non-empty AND user did not pass `--allow-scope-drift`
|
|
41
44
|
|
|
@@ -42,7 +42,7 @@ Use for major workflow transitions.
|
|
|
42
42
|
|
|
43
43
|
```
|
|
44
44
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
45
|
-
|
|
45
|
+
rcode ► {STAGE NAME}
|
|
46
46
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
47
47
|
```
|
|
48
48
|
|
|
@@ -69,7 +69,7 @@ Use this when a router command dispatches to another command:
|
|
|
69
69
|
|
|
70
70
|
```
|
|
71
71
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
72
|
-
|
|
72
|
+
rcode ► ROUTING
|
|
73
73
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
74
74
|
|
|
75
75
|
Input: {user's question or intent}
|
|
@@ -328,7 +328,7 @@ Use standard markdown pipe tables with status symbols:
|
|
|
328
328
|
**Majlis banner** (multi-agent council):
|
|
329
329
|
```
|
|
330
330
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
331
|
-
|
|
331
|
+
rcode ► MAJLIS CONVENING
|
|
332
332
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
333
333
|
```
|
|
334
334
|
|
|
@@ -354,7 +354,7 @@ the banner, not inside it.
|
|
|
354
354
|
|
|
355
355
|
```
|
|
356
356
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
357
|
-
|
|
357
|
+
rcode ► PLANNING SPRINT 01.1
|
|
358
358
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
359
359
|
التخطيط للسباق 01.1 — يرجى الانتظار
|
|
360
360
|
```
|
|
@@ -389,7 +389,7 @@ translated prose goes outside the art, on its own line(s).
|
|
|
389
389
|
|
|
390
390
|
- Varying box/banner widths within same output
|
|
391
391
|
- Mixing banner styles (`===`, `---`, `***`)
|
|
392
|
-
- Skipping `
|
|
392
|
+
- Skipping `rcode ►` prefix in stage banners
|
|
393
393
|
- Random emoji (`🚀`, `✨`, `💫`) outside the approved set
|
|
394
394
|
- Missing Next Up block after workflow completions
|
|
395
395
|
- Hardcoding references to other methodologies in rcode's UX
|
|
@@ -29,11 +29,21 @@ Exit.
|
|
|
29
29
|
Load phase operation context:
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
|
-
INIT=$(node ".rcode/bin/rcode-tools.cjs" init phase-op "
|
|
32
|
+
INIT=$(node ".rcode/bin/rcode-tools.cjs" init phase-op "1" 2>/dev/null)
|
|
33
33
|
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
`init phase-op` only populates the phase-aware fields (`roadmap_exists`,
|
|
37
|
+
`planning_exists`, `phase_found`, ...) when its `question` argument's first
|
|
38
|
+
token parses as an integer `> 0` (rcode-tools.cjs's `phase-op` handler gates
|
|
39
|
+
on `phaseNum > 0`). add-phase has no target phase to pass — it's creating a
|
|
40
|
+
new one — so a placeholder is required. `"0"` fails that guard, silently
|
|
41
|
+
dropping `roadmap_exists` from every response (#1017); `"1"` satisfies it.
|
|
42
|
+
The dummy value only affects unused fields like `phase_found`/`phase_name`
|
|
43
|
+
(harmless here) — `roadmap_exists` itself is purely a file-existence check
|
|
44
|
+
and doesn't depend on phase 1 actually existing.
|
|
45
|
+
|
|
46
|
+
If `INIT` is empty, print error and exit:
|
|
37
47
|
```
|
|
38
48
|
Error: rcode-tools init failed. Verify .rcode/ is installed and state.json is valid.
|
|
39
49
|
```
|
|
@@ -92,7 +102,13 @@ The CLI handles:
|
|
|
92
102
|
- Creating the phase directory (`.planning/phases/{NN}-{slug}/`)
|
|
93
103
|
- Inserting the phase entry into ROADMAP.md with Goal, Depends on, and Plans sections
|
|
94
104
|
|
|
95
|
-
Extract from result: `phase_number`, `padded`, `name`, `slug`, `directory
|
|
105
|
+
Extract from result: `phase_number`, `padded`, `name`, `slug`, `directory`,
|
|
106
|
+
`milestone_health` (object: `open_phases`, `recommendation`, `threshold_should`,
|
|
107
|
+
`threshold_consider`), `nudge` (present only when `recommendation` isn't
|
|
108
|
+
`healthy` — a ready-to-print one-liner naming the milestone). `phase add`
|
|
109
|
+
computes these via `milestoneCloseNudge()` (issue #942) as part of the same
|
|
110
|
+
call, so `milestone_health_check` below reads them straight off `$RESULT`
|
|
111
|
+
instead of re-deriving them.
|
|
96
112
|
|
|
97
113
|
**If `BULK_MODE=true`:** after the CLI returns, write the bulk body to `${directory}/TASKS.md` per the structure defined in `detect_task_list`. This step is non-destructive — it only ADDs a TASKS.md file inside the new phase directory.
|
|
98
114
|
</step>
|
|
@@ -110,26 +126,30 @@ If "Roadmap Evolution" section doesn't exist, create it.
|
|
|
110
126
|
</step>
|
|
111
127
|
|
|
112
128
|
<step name="milestone_health_check">
|
|
113
|
-
After the phase is added,
|
|
129
|
+
After the phase is added, read the milestone-health gauge (issue #718)
|
|
130
|
+
straight off the `phase add` result captured in `add_phase` — no extra
|
|
131
|
+
subprocess calls. Previously this step spawned a separate `milestone-health`
|
|
132
|
+
call plus 3 `node -e` JSON field extractions to re-derive data that `phase
|
|
133
|
+
add` already returns inline via `milestoneCloseNudge()` (#942); that was
|
|
134
|
+
4 wasted calls per phase-add for data already sitting in `$RESULT` (#1018).
|
|
114
135
|
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
MILESTONE_NAME=$(echo "$HEALTH" | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{try{console.log(JSON.parse(s).milestone||'')}catch{console.log('')}})")
|
|
136
|
+
```
|
|
137
|
+
RECOMMENDATION="$RESULT.milestone_health.recommendation" # e.g. healthy | consider-closing | should-close
|
|
138
|
+
OPEN_COUNT="$RESULT.milestone_health.open_phases"
|
|
139
|
+
NUDGE="$RESULT.nudge" # ready-to-print, names the milestone; absent when healthy
|
|
120
140
|
```
|
|
121
141
|
|
|
122
142
|
If `RECOMMENDATION` is `should-close` (≥12 open phases), surface a hard nudge:
|
|
123
143
|
|
|
124
144
|
```
|
|
125
|
-
⚠
|
|
145
|
+
⚠ {NUDGE}
|
|
126
146
|
|
|
127
147
|
Phase {N} is now in this milestone, but the milestone is well past the
|
|
128
148
|
12-phase threshold for considering closure. Phases are accumulating without
|
|
129
149
|
a milestone boundary — historically this is where roadmaps lose structure.
|
|
130
150
|
|
|
131
151
|
Recommended next step:
|
|
132
|
-
/rcode-complete-milestone close
|
|
152
|
+
/rcode-complete-milestone close the milestone cleanly + archive done phases
|
|
133
153
|
/rcode-new-milestone start a fresh milestone for ongoing work
|
|
134
154
|
|
|
135
155
|
If you genuinely want a giant single-milestone roadmap, ignore this and
|
|
@@ -139,11 +159,10 @@ continue. The threshold is conservative on purpose.
|
|
|
139
159
|
If `RECOMMENDATION` is `consider-closing` (8-11 open phases), softer nudge:
|
|
140
160
|
|
|
141
161
|
```
|
|
142
|
-
ℹ
|
|
143
|
-
Consider /rcode-complete-milestone before adding more.
|
|
162
|
+
ℹ {NUDGE}
|
|
144
163
|
```
|
|
145
164
|
|
|
146
|
-
If `RECOMMENDATION` is `healthy
|
|
165
|
+
If `RECOMMENDATION` is `healthy` or `milestone_health` is absent (no state.json / no milestone), say nothing.
|
|
147
166
|
</step>
|
|
148
167
|
|
|
149
168
|
<step name="completion">
|
package/rcode/workflows/do.md
CHANGED
|
@@ -387,6 +387,38 @@ Scope: {one-line scope summary}
|
|
|
387
387
|
Routing to: {chosen command}
|
|
388
388
|
Reason: {one-line why}
|
|
389
389
|
```
|
|
390
|
+
|
|
391
|
+
**herdr hint (cached read only — no live check from here).** When the chosen command is
|
|
392
|
+
`/rcode-execute` or `/rcode-add-phase` (routes that fan out substantial multi-file
|
|
393
|
+
execution work), read the cached availability flag left by `/rcode-execute`'s own
|
|
394
|
+
availability check:
|
|
395
|
+
|
|
396
|
+
```bash
|
|
397
|
+
HERDR_AVAILABLE=$(node .rcode/bin/rcode-tools.cjs config-get workflow._herdr_available 2>/dev/null || echo "false")
|
|
398
|
+
HERDR_AVAILABLE=${HERDR_AVAILABLE:-false}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
This is a plain `config-get` read — the same sanctioned category as `config-get mode`
|
|
402
|
+
used elsewhere in this workflow (see `<guardrails>`). It reuses the exact cache
|
|
403
|
+
`/rcode-execute` writes to (`workflow._herdr_checked` / `workflow._herdr_available`) —
|
|
404
|
+
do NOT duplicate the `command -v herdr` probe or call `config-set` here. `/rcode-do`
|
|
405
|
+
has no sanctioned way to run that shell probe or persist a value itself, so if the
|
|
406
|
+
cache hasn't been populated yet (`_herdr_checked` still false), say nothing — the
|
|
407
|
+
first `/rcode-execute` run downstream will perform and cache the check itself.
|
|
408
|
+
|
|
409
|
+
If `HERDR_AVAILABLE == "true"`, append one extra line to the banner above:
|
|
410
|
+
|
|
411
|
+
```
|
|
412
|
+
Note: herdr is available on this machine — {chosen command} will offer a
|
|
413
|
+
multi-agent orchestration option if the work fans out into independent
|
|
414
|
+
plans; you'll be asked to confirm before anything runs via herdr.
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Do NOT turn this into an `AskUserQuestion` prompt and do NOT ask the user to
|
|
418
|
+
decide here — `/rcode-do` only routes, it never presents execution-mode choices.
|
|
419
|
+
The actual offer (with the full tradeoff explanation and required confirmation)
|
|
420
|
+
is `/rcode-execute`'s `three_options` step, which fires after dispatch regardless
|
|
421
|
+
of whether this hint line was shown.
|
|
390
422
|
</step>
|
|
391
423
|
|
|
392
424
|
<step name="dispatch">
|
|
@@ -426,7 +458,7 @@ If the chosen command expects a phase number and one wasn't provided in the text
|
|
|
426
458
|
<guardrails>
|
|
427
459
|
**Hard prohibitions during /rcode-do execution (issue #458, refined by #1007):**
|
|
428
460
|
|
|
429
|
-
The steps above (`parse_args`, `check_project`, `auto_init_check`, `greenfield_guard`, `explicit_intent_check`, `persona_shortcut`) legitimately call Bash for structured state/config lookups (`rcode-tools.cjs state load`, `progress init`, `config-get mode`, `classify-question`, milestone/PRD/epic detection via `ls`/`grep`) and Read for the specific persona/capability-table lookup in `persona_shortcut` step 2. That is routing plumbing, not investigation, and is allowed. What's prohibited is using those same tools to figure out the route by inspecting application code, or to do the routed work itself:
|
|
461
|
+
The steps above (`parse_args`, `check_project`, `auto_init_check`, `greenfield_guard`, `explicit_intent_check`, `persona_shortcut`, `display`) legitimately call Bash for structured state/config lookups (`rcode-tools.cjs state load`, `progress init`, `config-get mode`, `config-get workflow._herdr_available`, `classify-question`, milestone/PRD/epic detection via `ls`/`grep`) and Read for the specific persona/capability-table lookup in `persona_shortcut` step 2. That is routing plumbing, not investigation, and is allowed. `config-get workflow._herdr_available` is a read of a cache another workflow (`/rcode-execute`) already populated — `/rcode-do` MUST NOT run `command -v herdr` itself or call `config-set` to populate that cache; it only reads whatever is already there. What's prohibited is using those same tools to figure out the route by inspecting application code, or to do the routed work itself:
|
|
430
462
|
|
|
431
463
|
- MUST NOT use Bash/Read/Grep/Glob to explore or read application source code to guess what a vague request means. The state/config lookups named above are the only sanctioned uses — anything beyond them (grepping `src/`, reading a feature file to understand behavior, etc.) means the dispatcher contract has failed — STOP and use the no-route exit instead.
|
|
432
464
|
- MUST NOT call Write or Edit. The dispatcher never modifies files.
|