@hanzlaa/rcode 4.13.0 → 4.14.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/package.json +1 -1
- package/rcode/agents/rcode-project-researcher.md +19 -1
- package/rcode/bin/lib/roadmap.cjs +62 -22
- package/rcode/bin/rcode-tools.cjs +184 -3
- package/rcode/references/agent-shared-rules.md +36 -0
- package/rcode/references/planner-playbook.md +11 -0
- package/rcode/references/questioning.md +100 -2
- package/rcode/references/roadmapper-playbook.md +14 -0
- package/rcode/references/verifier-playbook.md +12 -0
- package/rcode/workflows/execute.md +6 -0
- package/rcode/workflows/new-project-define-requirements.md +36 -0
- package/rcode/workflows/new-project-research-decision.md +61 -1
- package/rcode/workflows/new-project.md +95 -6
- package/rcode/workflows/plan.md +70 -8
- package/rcode/workflows/resume-work.md +18 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzlaa/rcode",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.14.0",
|
|
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": {
|
|
@@ -21,7 +21,7 @@ Your files feed the roadmap:
|
|
|
21
21
|
| File | How Roadmap Uses It |
|
|
22
22
|
|------|---------------------|
|
|
23
23
|
| `SUMMARY.md` | Phase structure recommendations, ordering rationale |
|
|
24
|
-
| `STACK.md` | Technology
|
|
24
|
+
| `STACK.md` | Technology **suggestions** for the project, with the premise behind each |
|
|
25
25
|
| `FEATURES.md` | What to build in each phase |
|
|
26
26
|
| `ARCHITECTURE.md` | System structure, component boundaries |
|
|
27
27
|
| `PITFALLS.md` | What phases need deeper research flags |
|
|
@@ -125,3 +125,21 @@ Log each pass as:
|
|
|
125
125
|
## Examples
|
|
126
126
|
|
|
127
127
|
See `.rcode/agents-rules/project-researcher/detailed-guide.md` for full worked examples (happy path, edge case, negative).
|
|
128
|
+
|
|
129
|
+
## STACK.md is a suggestion, never a decision
|
|
130
|
+
|
|
131
|
+
You do not choose the stack. You surface options and the trade-off, and the user
|
|
132
|
+
decides at the stack gate (`new-project-research-decision.md` step 6b).
|
|
133
|
+
|
|
134
|
+
Write STACK.md accordingly:
|
|
135
|
+
|
|
136
|
+
- **Every recommendation carries its premise** — the one condition that makes it
|
|
137
|
+
right. Not "WordPress is popular for content sites" but "WordPress IF a
|
|
138
|
+
non-technical person will update content with no dev retainer."
|
|
139
|
+
- **Name the closest alternative and when it wins.** A recommendation with no
|
|
140
|
+
alternative is a decision wearing a recommendation's clothes.
|
|
141
|
+
- **Never write "Locked", "Final", or "Decided"** in STACK.md. You have no
|
|
142
|
+
authority to lock anything; only a user answer at the gate does that.
|
|
143
|
+
|
|
144
|
+
A premise you cannot state is a recommendation you have not justified. Say you
|
|
145
|
+
don't have enough to recommend one rather than picking the familiar option.
|
|
@@ -127,28 +127,69 @@ function extractPhases(content) {
|
|
|
127
127
|
return phases;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
// Requirement IDs come in two shapes and BOTH are real:
|
|
131
|
+
// REQ-AUTH, REQ-FOO-BAR — the documented convention
|
|
132
|
+
// FOUND-01, RENT-04, OBJ-06, AUTHZ-04 — what projects actually write
|
|
133
|
+
// This must stay in step with extractReqIds() in rcode-tools.cjs. Two copies of
|
|
134
|
+
// this pattern already drifted once: one was widened for domain prefixes and
|
|
135
|
+
// this one was not, so `roadmap get-phase` kept returning requirements: [] on
|
|
136
|
+
// every domain-prefixed project.
|
|
137
|
+
const REQ_ID_RE = /\bREQ-[A-Z0-9][A-Z0-9-]*\b|\b[A-Z][A-Z0-9]{1,15}-\d+[a-z]?\b/g;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Build a matcher for a labelled block, tolerant of how the label is actually
|
|
141
|
+
* written. Roadmapper emits `**Success criteria:**` (colon inside the bold,
|
|
142
|
+
* lowercase c) while the old parser demanded `**Success Criteria**:` (colon
|
|
143
|
+
* outside). Two rcode components disagreeing about rcode's own format is what
|
|
144
|
+
* made get-phase unable to read its own roadmapper's output.
|
|
145
|
+
*
|
|
146
|
+
* Accepts: **Label:** | **Label**: | ## Label | Label:
|
|
147
|
+
* Returns { inline, list } — inline is same-line content, list is the block
|
|
148
|
+
* of bullet/numbered lines that follows. Callers use whichever is present.
|
|
149
|
+
*/
|
|
150
|
+
function matchLabelledBlock(section, label) {
|
|
151
|
+
const l = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
152
|
+
// [ \t]* everywhere the match must NOT cross a line. Using \s* here let the
|
|
153
|
+
// label matcher swallow the newline, so the first bullet of the following
|
|
154
|
+
// list was captured as "inline" and its description text was thrown away —
|
|
155
|
+
// `- CITY-02 per-city page exists` came back as just `CITY-02`.
|
|
156
|
+
const re = new RegExp(
|
|
157
|
+
'(?:\\*\\*[ \\t]*' + l + '[ \\t]*:?[ \\t]*\\*\\*[ \\t]*:?|#{1,4}[ \\t]*' + l + '[ \\t]*:?|^[ \\t]*' + l + '[ \\t]*:)' +
|
|
158
|
+
'([^\\n]*)\\n((?:[ \\t]*(?:\\d+\\.|[-*])[ \\t]+[^\\n]+\\n?)*)',
|
|
159
|
+
'im'
|
|
160
|
+
);
|
|
161
|
+
const m = section.match(re);
|
|
162
|
+
if (!m) return null;
|
|
163
|
+
return { inline: (m[1] || '').trim(), list: m[2] || '' };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function splitListBlock(block) {
|
|
167
|
+
return String(block).split('\n')
|
|
168
|
+
.map((l) => l.replace(/^[ \t]*(?:\d+\.|[-*])[ \t]+/, '').trim())
|
|
169
|
+
.filter(Boolean);
|
|
170
|
+
}
|
|
171
|
+
|
|
130
172
|
function parseRequirements(section) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
173
|
+
const block = matchLabelledBlock(section, 'Requirements');
|
|
174
|
+
if (block) {
|
|
175
|
+
// A following list block wins; otherwise take the same-line value, which is
|
|
176
|
+
// how roadmapper writes it: `**Requirements:** FOUND-01, FOUND-02, RENT-04`.
|
|
177
|
+
const fromList = splitListBlock(block.list);
|
|
178
|
+
if (fromList.length > 0) return fromList;
|
|
179
|
+
if (block.inline) {
|
|
180
|
+
const ids = block.inline.match(REQ_ID_RE);
|
|
181
|
+
if (ids && ids.length > 0) return [...new Set(ids)];
|
|
182
|
+
return block.inline.split(/\s*,\s*/).map(x => x.trim()).filter(Boolean);
|
|
183
|
+
}
|
|
138
184
|
}
|
|
139
185
|
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
// Requirements: REQ-001, REQ-002
|
|
143
|
-
// **Covers:** REQ-001, REQ-003
|
|
144
|
-
// Collect every line in the section that contains REQ-\d+ patterns.
|
|
186
|
+
// Last resort: sweep the whole section for requirement IDs on any line
|
|
187
|
+
// (covers `**REQs:**`, `**Covers:**`, and prose mentions).
|
|
145
188
|
const seen = new Set();
|
|
146
189
|
const out = [];
|
|
147
|
-
const reqIdRe = /\bREQ-[A-Z0-9][A-Z0-9-]*\b/g;
|
|
148
190
|
for (const line of section.split('\n')) {
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
for (const id of ids) {
|
|
191
|
+
const matches = line.match(REQ_ID_RE) || [];
|
|
192
|
+
for (const id of matches) {
|
|
152
193
|
if (!seen.has(id)) { seen.add(id); out.push(id); }
|
|
153
194
|
}
|
|
154
195
|
}
|
|
@@ -156,12 +197,11 @@ function parseRequirements(section) {
|
|
|
156
197
|
}
|
|
157
198
|
|
|
158
199
|
function parseSuccessCriteria(section) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
.filter(Boolean);
|
|
200
|
+
const block = matchLabelledBlock(section, 'Success criteria');
|
|
201
|
+
if (!block) return [];
|
|
202
|
+
const fromList = splitListBlock(block.list);
|
|
203
|
+
if (fromList.length > 0) return fromList;
|
|
204
|
+
return block.inline ? [block.inline] : [];
|
|
165
205
|
}
|
|
166
206
|
|
|
167
207
|
function parsePlans(section) {
|
|
@@ -193,7 +193,13 @@ function extractReqIds(requirements) {
|
|
|
193
193
|
if (!Array.isArray(requirements) || requirements.length === 0) return [];
|
|
194
194
|
const seen = new Set();
|
|
195
195
|
const out = [];
|
|
196
|
-
|
|
196
|
+
// Two shapes, because real projects rarely use the REQ- prefix:
|
|
197
|
+
// REQ-AUTH, REQ-FOO-BAR — the documented convention
|
|
198
|
+
// FOUND-01, RENT-04, AUTHZ-04, OBJ-06, CITY-02 — what projects actually write
|
|
199
|
+
// Matching only the first shape returned an empty phase_req_ids on every
|
|
200
|
+
// domain-prefixed project, and plan.md's Requirements Coverage Gate skips
|
|
201
|
+
// itself when that array is empty. The gate was silently off, not passing.
|
|
202
|
+
const re = /\bREQ-[A-Z0-9][A-Z0-9-]*\b|\b[A-Z][A-Z0-9]{1,15}-\d+[a-z]?\b/g;
|
|
197
203
|
for (const line of requirements) {
|
|
198
204
|
const matches = String(line).match(re) || [];
|
|
199
205
|
for (const m of matches) {
|
|
@@ -1414,6 +1420,26 @@ function cmdState(subArgs) {
|
|
|
1414
1420
|
// --- set-phase ---
|
|
1415
1421
|
if (sub === 'set-phase') {
|
|
1416
1422
|
const name = subArgs[1];
|
|
1423
|
+
// A flag-looking argument is never a phase name. Without this,
|
|
1424
|
+
// `state set-phase --phase 99 --status complete` created a phase literally
|
|
1425
|
+
// NAMED "--phase", set current_phase to "--phase", and returned ok:true.
|
|
1426
|
+
// Silent state corruption reported from a live project.
|
|
1427
|
+
if (typeof name === 'string' && name.startsWith('--')) {
|
|
1428
|
+
throw new Error(
|
|
1429
|
+
`set-phase takes a phase NAME as a positional argument, not flags. ` +
|
|
1430
|
+
`Got "${name}". Did you mean:\n` +
|
|
1431
|
+
` state set-phase "Phase name" (set the current phase pointer)\n` +
|
|
1432
|
+
` phase complete <N> (mark a phase complete)\n` +
|
|
1433
|
+
` state planned-phase --phase <N> (record a phase as planned)`
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1436
|
+
const strayFlags = subArgs.slice(2).filter(a => typeof a === 'string' && a.startsWith('--'));
|
|
1437
|
+
if (strayFlags.length > 0) {
|
|
1438
|
+
throw new Error(
|
|
1439
|
+
`set-phase does not accept flags (${strayFlags.join(', ')}). It sets the ` +
|
|
1440
|
+
`current-phase pointer only. Use 'phase complete <N>' to change a phase's status.`
|
|
1441
|
+
);
|
|
1442
|
+
}
|
|
1417
1443
|
if (!name) throw new Error('set-phase requires a phase name argument');
|
|
1418
1444
|
const state = readState() || defaultState();
|
|
1419
1445
|
// Fix #854 — mark the previously active phase as completed before switching.
|
|
@@ -3256,6 +3282,29 @@ function cmdState(subArgs) {
|
|
|
3256
3282
|
// instead. Its stale-executing-phase hygiene warning was ported there.
|
|
3257
3283
|
// Kept only for backward compatibility with anyone scripting against it
|
|
3258
3284
|
// directly; do not wire new callers to this — use `phase complete`.
|
|
3285
|
+
// Records what the user actually authorized this session — 'plan', 'build',
|
|
3286
|
+
// 'research', 'audit'. `resume-work` reads it so "resume" restores POSITION
|
|
3287
|
+
// AND SCOPE, not position alone. Without it, a resume after a planning
|
|
3288
|
+
// session reads as "keep going" and starts building work nobody asked for.
|
|
3289
|
+
if (sub === 'set-intent') {
|
|
3290
|
+
const flags = parseFlags(1);
|
|
3291
|
+
const intent = flags.intent || subArgs[1];
|
|
3292
|
+
const ALLOWED = ['plan', 'build', 'research', 'audit', 'review'];
|
|
3293
|
+
if (!intent) throw new Error(`set-intent requires an intent (${ALLOWED.join('|')})`);
|
|
3294
|
+
if (!ALLOWED.includes(intent)) {
|
|
3295
|
+
throw new Error(`unknown intent "${intent}" — expected one of: ${ALLOWED.join(', ')}`);
|
|
3296
|
+
}
|
|
3297
|
+
const state = readState() || defaultState();
|
|
3298
|
+
const previous = state.last_intent ? state.last_intent.intent : null;
|
|
3299
|
+
state.last_intent = {
|
|
3300
|
+
intent,
|
|
3301
|
+
recorded_at: new Date().toISOString(),
|
|
3302
|
+
source: flags.source || 'workflow',
|
|
3303
|
+
};
|
|
3304
|
+
writeState(state);
|
|
3305
|
+
return { ok: true, intent, previous };
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3259
3308
|
if (sub === 'complete-phase') {
|
|
3260
3309
|
const flags = parseFlags(1);
|
|
3261
3310
|
if (!flags.phase) throw new Error('complete-phase requires --phase <N>');
|
|
@@ -3582,6 +3631,32 @@ function cmdState(subArgs) {
|
|
|
3582
3631
|
}
|
|
3583
3632
|
|
|
3584
3633
|
if (existingIdx >= 0) {
|
|
3634
|
+
// Identity check BEFORE anything is carried over. Sync matched this
|
|
3635
|
+
// entry by NUMBER, but a number is a slot, not an identity. When a
|
|
3636
|
+
// roadmap is replaced, slot 3 can go from "Location Template" to
|
|
3637
|
+
// "Competitor Gap Analysis" — two unrelated pieces of work. Carrying
|
|
3638
|
+
// the old status across told a live project that competitor analysis
|
|
3639
|
+
// was "complete" when it had never been started, and only a manual
|
|
3640
|
+
// disk audit caught it.
|
|
3641
|
+
const priorName = String(state.phases[existingIdx].name || '').trim();
|
|
3642
|
+
const incomingName = String(phaseName || '').trim();
|
|
3643
|
+
const normName = (n) => n.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
|
|
3644
|
+
const identityChanged = priorName && incomingName
|
|
3645
|
+
&& normName(priorName) !== normName(incomingName);
|
|
3646
|
+
if (identityChanged) {
|
|
3647
|
+
// Different work in the same slot. Drop the inherited status and
|
|
3648
|
+
// completion, and let the disk-derived pass below decide afresh.
|
|
3649
|
+
state.phases[existingIdx].status = 'planned';
|
|
3650
|
+
delete state.phases[existingIdx].completed;
|
|
3651
|
+
delete state.phases[existingIdx].started;
|
|
3652
|
+
parsed.identity_changed = parsed.identity_changed || [];
|
|
3653
|
+
parsed.identity_changed.push({
|
|
3654
|
+
phase: phaseNum,
|
|
3655
|
+
was: priorName,
|
|
3656
|
+
now: incomingName,
|
|
3657
|
+
carried_status_dropped: true,
|
|
3658
|
+
});
|
|
3659
|
+
}
|
|
3585
3660
|
// Backfill both id and number so future readers using either schema find it.
|
|
3586
3661
|
state.phases[existingIdx].number = state.phases[existingIdx].number || phaseNum;
|
|
3587
3662
|
state.phases[existingIdx].id = state.phases[existingIdx].id || phaseNum;
|
|
@@ -4468,6 +4543,70 @@ function cmdPhase(subArgs) {
|
|
|
4468
4543
|
// Closes #731. No --names arg required — reads the ROADMAP table directly.
|
|
4469
4544
|
// Only creates directories; does NOT create .md files inside them.
|
|
4470
4545
|
// =====================================================================
|
|
4546
|
+
// phase rename-dir <N> — align a phase directory's slug with its ROADMAP name.
|
|
4547
|
+
// Dry-run by default: renaming a directory moves artifacts and, without git mv,
|
|
4548
|
+
// detaches their history. There was no mechanism for this at all, so a roadmap
|
|
4549
|
+
// rewrite left every directory carrying the name of whatever it used to be.
|
|
4550
|
+
if (sub === 'rename-dir') {
|
|
4551
|
+
// cmdPhase has no shared flag parser (parseFlags is local to cmdState), so
|
|
4552
|
+
// read the two flags this needs directly.
|
|
4553
|
+
const argvIdx = subArgs.findIndex((a, i) => i > 0 && !String(a).startsWith('--'));
|
|
4554
|
+
const phaseFlagIdx = subArgs.indexOf('--phase');
|
|
4555
|
+
const target = argvIdx > 0 ? subArgs[argvIdx]
|
|
4556
|
+
: (phaseFlagIdx !== -1 ? subArgs[phaseFlagIdx + 1] : null);
|
|
4557
|
+
if (!target) throw new Error('phase rename-dir requires a phase number');
|
|
4558
|
+
const apply = subArgs.includes('--apply');
|
|
4559
|
+
|
|
4560
|
+
const found = cmdFindPhase([String(target)]);
|
|
4561
|
+
if (!found.exists) throw new Error(`No phase directory on disk for phase ${target}`);
|
|
4562
|
+
|
|
4563
|
+
const roadmapLib = require(path.join(__dirname, 'lib', 'roadmap.cjs'));
|
|
4564
|
+
const rp = roadmapLib.dispatch(PROJECT_ROOT, ['get-phase', String(target)]);
|
|
4565
|
+
if (!rp || !rp.found || !rp.name) {
|
|
4566
|
+
throw new Error(`Phase ${target} not found in ROADMAP.md — nothing to rename toward`);
|
|
4567
|
+
}
|
|
4568
|
+
|
|
4569
|
+
const slugify = (t) => String(t).toLowerCase()
|
|
4570
|
+
.replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').replace(/-+/g, '-');
|
|
4571
|
+
const newSlug = slugify(rp.name);
|
|
4572
|
+
const phasesDir = path.join(PLANNING_DIR, 'phases');
|
|
4573
|
+
const oldDirName = path.basename(found.dir);
|
|
4574
|
+
const newDirName = `${target}-${newSlug}`;
|
|
4575
|
+
|
|
4576
|
+
if (oldDirName === newDirName) {
|
|
4577
|
+
return { ok: true, renamed: false, reason: 'directory already matches the roadmap name', dir: found.dir };
|
|
4578
|
+
}
|
|
4579
|
+
const newPath = path.join(phasesDir, newDirName);
|
|
4580
|
+
if (fs.existsSync(newPath)) {
|
|
4581
|
+
throw new Error(`Target directory already exists: ${newDirName}. Resolve by hand — two phase dirs for one number is worse than a stale name.`);
|
|
4582
|
+
}
|
|
4583
|
+
|
|
4584
|
+
if (!apply) {
|
|
4585
|
+
return {
|
|
4586
|
+
ok: true,
|
|
4587
|
+
renamed: false,
|
|
4588
|
+
dry_run: true,
|
|
4589
|
+
from: oldDirName,
|
|
4590
|
+
to: newDirName,
|
|
4591
|
+
note: 'Dry run. Re-run with --apply to rename. Check first that the artifacts in this directory belong to the phase the roadmap now describes — if the phase was REPLACED rather than renamed, renaming hides that instead of fixing it.',
|
|
4592
|
+
};
|
|
4593
|
+
}
|
|
4594
|
+
|
|
4595
|
+
// Prefer `git mv` so the artifacts keep their history.
|
|
4596
|
+
const oldPath = path.join(phasesDir, oldDirName);
|
|
4597
|
+
let method = 'fs';
|
|
4598
|
+
const { spawnSync } = require('child_process');
|
|
4599
|
+
const gitMv = spawnSync('git', ['mv', oldPath, newPath], { cwd: PROJECT_ROOT, encoding: 'utf8' });
|
|
4600
|
+
if (gitMv.status === 0) { method = 'git mv'; }
|
|
4601
|
+
else { fs.renameSync(oldPath, newPath); }
|
|
4602
|
+
|
|
4603
|
+
return {
|
|
4604
|
+
ok: true, renamed: true, method,
|
|
4605
|
+
from: oldDirName, to: newDirName,
|
|
4606
|
+
warning: 'Any file referencing the old path (SPRINT frontmatter, SUMMARY links, notes) still points at it. Grep for the old slug.',
|
|
4607
|
+
};
|
|
4608
|
+
}
|
|
4609
|
+
|
|
4471
4610
|
if (sub === 'scaffold-all') {
|
|
4472
4611
|
const roadmapPath = path.join(PLANNING_DIR, 'ROADMAP.md');
|
|
4473
4612
|
const phasesDir = path.join(PLANNING_DIR, 'phases');
|
|
@@ -5801,11 +5940,52 @@ function cmdFindPhase(args) {
|
|
|
5801
5940
|
.map((d) => path.relative(PROJECT_ROOT, path.join(phasesDir, d)));
|
|
5802
5941
|
if (!exact) return { number: target, exists: false, dir: null, slug: null, decimal_children };
|
|
5803
5942
|
const slugMatch = exact.match(/^\d+(?:\.\d+)?[-](.+)$/);
|
|
5943
|
+
const slug = slugMatch ? slugMatch[1] : '';
|
|
5944
|
+
|
|
5945
|
+
// Name drift: the directory keeps the slug it was created with, but ROADMAP.md
|
|
5946
|
+
// can be rewritten under it. Resolving to the existing directory is correct —
|
|
5947
|
+
// that is where the artifacts and the git history live, and auto-renaming would
|
|
5948
|
+
// orphan both. Staying SILENT about the divergence is not: an agent reading
|
|
5949
|
+
// `slug: foundation-contact-loop` while the roadmap says "Rentable Contact
|
|
5950
|
+
// Layer" has no way to tell whether it is looking at the same work.
|
|
5951
|
+
// Same class as the phase-identity drift in `state sync --from-disk`.
|
|
5952
|
+
let name_drift = null;
|
|
5953
|
+
try {
|
|
5954
|
+
const roadmapPath = path.join(PLANNING_DIR, 'ROADMAP.md');
|
|
5955
|
+
if (slug && fs.existsSync(roadmapPath)) {
|
|
5956
|
+
const roadmapLib = require(path.join(__dirname, 'lib', 'roadmap.cjs'));
|
|
5957
|
+
const rp = roadmapLib.dispatch(PROJECT_ROOT, ['get-phase', String(target)]);
|
|
5958
|
+
const roadmapName = rp && rp.found ? String(rp.name || '') : '';
|
|
5959
|
+
if (roadmapName) {
|
|
5960
|
+
const slugify = (t) => String(t).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
5961
|
+
const fromRoadmap = slugify(roadmapName);
|
|
5962
|
+
// Compare on word sets, not exact slugs — a truncated or reordered slug
|
|
5963
|
+
// is normal, a different subject is the signal.
|
|
5964
|
+
const words = (t) => new Set(String(t).split('-').filter(w => w.length > 3));
|
|
5965
|
+
const dirWords = words(slug);
|
|
5966
|
+
const roadWords = words(fromRoadmap);
|
|
5967
|
+
const shared = [...dirWords].filter(w => roadWords.has(w)).length;
|
|
5968
|
+
if (dirWords.size > 0 && roadWords.size > 0 && shared === 0) {
|
|
5969
|
+
name_drift = {
|
|
5970
|
+
dir_slug: slug,
|
|
5971
|
+
roadmap_name: roadmapName,
|
|
5972
|
+
note: 'Directory name and ROADMAP name share no significant words. '
|
|
5973
|
+
+ 'The phase may have been replaced under the same number. '
|
|
5974
|
+
+ 'The directory is NOT auto-renamed: its artifacts and git history '
|
|
5975
|
+
+ 'belong to whatever was built there. Confirm they are the same work '
|
|
5976
|
+
+ 'before planning or executing against it.',
|
|
5977
|
+
};
|
|
5978
|
+
}
|
|
5979
|
+
}
|
|
5980
|
+
}
|
|
5981
|
+
} catch { /* drift detection is advisory — never fail a lookup over it */ }
|
|
5982
|
+
|
|
5804
5983
|
return {
|
|
5805
5984
|
number: target,
|
|
5806
5985
|
exists: true,
|
|
5807
5986
|
dir: path.relative(PROJECT_ROOT, path.join(phasesDir, exact)),
|
|
5808
|
-
slug
|
|
5987
|
+
slug,
|
|
5988
|
+
name_drift,
|
|
5809
5989
|
decimal_children,
|
|
5810
5990
|
};
|
|
5811
5991
|
}
|
|
@@ -7434,6 +7614,7 @@ async function main() {
|
|
|
7434
7614
|
console.log(' phase next-range [count] → return next N contiguous free phase numbers (#730)');
|
|
7435
7615
|
console.log(' phase scaffold-milestone --names "n1|n2|..." → bulk-create phase folders for a milestone (#731)');
|
|
7436
7616
|
console.log(' phase scaffold-all → create missing phase folders for all phases in ROADMAP.md (#731)');
|
|
7617
|
+
console.log(' phase rename-dir <N> [--apply] → align a phase dir slug with its ROADMAP name (dry-run by default)');
|
|
7437
7618
|
console.log(' workflow-config-audit → find workflows still referencing .planning/config.json (#733)');
|
|
7438
7619
|
console.log(' commit "<msg>" [--files p1 p2 ...] → atomic git commit with conventional-commits validation (no AI attribution, no --no-verify, no auto-push)');
|
|
7439
7620
|
console.log(' commit-to-subrepo --subrepo <p> "<msg>" → atomic commit inside a git subrepo (same validation as commit)');
|
|
@@ -7524,7 +7705,7 @@ async function main() {
|
|
|
7524
7705
|
console.log(' state story list [--sprint <NN.S>] [--status <status>]');
|
|
7525
7706
|
return;
|
|
7526
7707
|
default: {
|
|
7527
|
-
const stateSubs = ['read','get','init','set-phase','advance-plan','snapshot','update-progress','record-execution','record-council','record-chain','add-decision','decisions-global','add-blocker','resolve-blocker','record-session','set-ids-in-state','migrate-ids','migrate-schema','next-phase-id','next-plan-id','next-task-id','resolve-id','workstream-create','workstream-switch','workstream-list','workstream-status','workstream-complete','workstream-validate','insert-phase','planned-phase','begin-phase','complete-phase','reset'];
|
|
7708
|
+
const stateSubs = ['read','get','init','set-phase','advance-plan','snapshot','update-progress','record-execution','record-council','record-chain','add-decision','decisions-global','add-blocker','resolve-blocker','record-session','set-ids-in-state','migrate-ids','migrate-schema','next-phase-id','next-plan-id','next-task-id','resolve-id','workstream-create','workstream-switch','workstream-list','workstream-status','workstream-complete','workstream-validate','insert-phase','planned-phase','begin-phase','complete-phase','set-intent','reset'];
|
|
7528
7709
|
// Issue #656 — top-level aliases for intuitive guesses.
|
|
7529
7710
|
const intuitionAliases = {
|
|
7530
7711
|
blocker: 'state resolve-blocker',
|
|
@@ -41,6 +41,42 @@ on what it touches, so a high score is corroboration and a zero score is no
|
|
|
41
41
|
information. When your reading disagrees with the score, your reading wins, and
|
|
42
42
|
you name the file or decision that made you override.
|
|
43
43
|
|
|
44
|
+
**Elicitation is not authoring — hand the pen back.** When gathering what the user
|
|
45
|
+
wants, the moment you catch yourself naming the stack, picking the MVP cut, or
|
|
46
|
+
proposing the phase breakdown, stop and hand it back to them. Infer-and-confirm
|
|
47
|
+
("I'm assuming the maintainer is you, not a client — right?") is fine; presenting
|
|
48
|
+
your conclusion as a finding is not. Every inferred value that reaches an artifact
|
|
49
|
+
carries an inline `[ASSUMPTION]` tag, and every tag gets walked with the user
|
|
50
|
+
before that artifact is final. An untriaged assumption in a finished document is a
|
|
51
|
+
decision nobody made.
|
|
52
|
+
|
|
53
|
+
**Never decide the stack for the user.** Technology choice — language, framework,
|
|
54
|
+
CMS, database, hosting model — is the user's call, always. Research produces a
|
|
55
|
+
suggestion; only the user turns it into a decision. Present it with the ONE reason
|
|
56
|
+
that actually drove it, in their terms, and offer three ways out: confirm it, name
|
|
57
|
+
their own, or ask for more comparison. Then stop. No default, no auto-selection,
|
|
58
|
+
no "the obvious choice for this domain" — a wrong stack is the single most
|
|
59
|
+
expensive thing in a project to reverse.
|
|
60
|
+
|
|
61
|
+
**Write the premise into the decision, and re-open it when the premise dies.** "X
|
|
62
|
+
because a non-technical client updates content themselves" stays true only while
|
|
63
|
+
there is a non-technical client. When the project pivots, every decision whose
|
|
64
|
+
stated reason the pivot invalidated goes back to the user. A decision whose reason
|
|
65
|
+
has expired is not locked, it is stale — and treating it as locked is how a
|
|
66
|
+
codebase gets built twice.
|
|
67
|
+
|
|
68
|
+
**Planning never authorizes building.** If the user asked you to plan, design,
|
|
69
|
+
research, or audit, the deliverable is the plan, the design, the findings — not
|
|
70
|
+
the implementation. Finishing the plan and continuing into code is not
|
|
71
|
+
thoroughness, it is doing work nobody approved, and it costs more to unwind than
|
|
72
|
+
it saved. The same applies to an ambiguous continuation like "resume", "carry
|
|
73
|
+
on", or "next": it restores POSITION, never SCOPE. When the standing instruction
|
|
74
|
+
was to plan, a resume continues planning.
|
|
75
|
+
|
|
76
|
+
If you believe the next step is obvious and valuable, say so in one line and stop.
|
|
77
|
+
The user typing the next command takes two seconds; undoing an unrequested build
|
|
78
|
+
took a whole session.
|
|
79
|
+
|
|
44
80
|
---
|
|
45
81
|
|
|
46
82
|
## Redirect protocol
|
|
@@ -154,6 +154,17 @@ autonomous: true|false
|
|
|
154
154
|
files_modified: [...]
|
|
155
155
|
requirements: [...]
|
|
156
156
|
must_haves: {truths, artifacts, key_links}
|
|
157
|
+
|
|
158
|
+
**`truths` are copied from the requirement's Consequences, not invented.** For
|
|
159
|
+
each requirement this plan claims, read its `**Consequences (testable):**` list in
|
|
160
|
+
REQUIREMENTS.md and carry those lines into `must_haves.truths` verbatim. You are
|
|
161
|
+
transcribing a decision someone already made, not making a new one.
|
|
162
|
+
|
|
163
|
+
Invent a truth only when the requirement has no consequences recorded — and when
|
|
164
|
+
you do, say so in the plan (`[DERIVED]` prefix on that truth) so the verifier
|
|
165
|
+
knows it is checking your reconstruction rather than the requirement's own
|
|
166
|
+
criteria. A phase full of `[DERIVED]` truths is a signal the requirements were
|
|
167
|
+
never finished, not a signal to proceed quietly.
|
|
157
168
|
---
|
|
158
169
|
|
|
159
170
|
## Sprint {phase}.{plan}: {one-line sprint goal, plain English, no jargon}
|
|
@@ -105,10 +105,108 @@ A well-paced Socratic conversation follows a natural arc:
|
|
|
105
105
|
↓ Proceed to planning or revisit if gaps remain
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
-
Each phase should feel **natural, conversational**, not like a checklist. If the user volunteers information, use it; don't force a predetermined sequence.
|
|
108
|
+
Each phase should feel **natural, conversational**, not like a checklist. If the user volunteers information, use it; don't force a predetermined sequence. **This is a rule about TONE, not about coverage** — the decisions in the Mandatory decision set below still all get resolved, in whatever order the conversation makes natural.
|
|
109
109
|
|
|
110
110
|
---
|
|
111
111
|
|
|
112
|
+
## Working mode — offer it, don't read it from config
|
|
113
|
+
|
|
114
|
+
**Before any planning questions, ask how the user wants to work.** This is a
|
|
115
|
+
per-run choice presented to them, never a config flag read silently:
|
|
116
|
+
|
|
117
|
+
- **Fast path** — batch the remaining gaps into one or two consolidated
|
|
118
|
+
questions, then draft the full artifact, marking every inferred value with an
|
|
119
|
+
`[ASSUMPTION]` tag inline. The user reviews and iterates. Initial quality
|
|
120
|
+
depends on how much they gave upfront.
|
|
121
|
+
- **Coaching path** — walk the decisions together, section by section.
|
|
122
|
+
|
|
123
|
+
Why it must be asked: a user who never enabled autonomous mode should never be
|
|
124
|
+
*treated* as if they had. Confirmed live — a user asked for a project to be
|
|
125
|
+
planned, was never offered this choice, got no defined questions, and received a
|
|
126
|
+
plan built on assumptions they never saw. "You didn't turn on yolo" is not a
|
|
127
|
+
defence when nothing ever asked.
|
|
128
|
+
|
|
129
|
+
Auto/yolo mode picks Fast path automatically. Everything else asks.
|
|
130
|
+
|
|
131
|
+
## Stakes calibration — one probe, before anything else
|
|
132
|
+
|
|
133
|
+
Ask once, early: **is this a hobby/solo thing, an internal tool, or a launch?**
|
|
134
|
+
Then scale rigor to the answer. rcode's pipeline is built for the launch case and
|
|
135
|
+
applying it whole to a weekend project is its own kind of failure — the user
|
|
136
|
+
abandons the process rather than the project.
|
|
137
|
+
|
|
138
|
+
| Stakes | Depth |
|
|
139
|
+
|---|---|
|
|
140
|
+
| Hobby / solo | Minimal artifacts. Reviewer gates run quietly or not at all |
|
|
141
|
+
| Internal tool | Normal pipeline, lighter review |
|
|
142
|
+
| Launch / production | Full pipeline, all gates, nothing skipped |
|
|
143
|
+
|
|
144
|
+
## Elicitation, not direction — the hand-back rule
|
|
145
|
+
|
|
146
|
+
Discovery pulls the user's vision out. It does not insert yours.
|
|
147
|
+
|
|
148
|
+
**When you catch yourself naming the stack, picking the MVP cut, or proposing the
|
|
149
|
+
phase breakdown — stop. You have crossed from asking into authoring. Hand the pen
|
|
150
|
+
back.**
|
|
151
|
+
|
|
152
|
+
Infer-and-confirm is fine: *"I'm assuming the maintainer is you, not a client —
|
|
153
|
+
right?"* Quizzing the user through a tree of your own options is not, and neither
|
|
154
|
+
is presenting your conclusion as the finding.
|
|
155
|
+
|
|
156
|
+
This is the rule that would have prevented the most expensive failure in rcode's
|
|
157
|
+
own history: a session picked a stack, phased a roadmap around it, and built on
|
|
158
|
+
it, having never handed the pen back once.
|
|
159
|
+
|
|
160
|
+
## Mandatory decision set — tone is conversational, coverage is not
|
|
161
|
+
|
|
162
|
+
The "don't feel like a checklist" rule above governs **tone**. It does not govern
|
|
163
|
+
**coverage**. There is a set of decisions that shape everything downstream, and
|
|
164
|
+
each one must be either answered by the user or recorded as an assumption with
|
|
165
|
+
its reason. Silently deciding one on the user's behalf is not conversational
|
|
166
|
+
skill, it is skipping the question.
|
|
167
|
+
|
|
168
|
+
Confirmed live: a user asked for a project to be planned, was never asked a
|
|
169
|
+
single defined question, and got a stack, a roadmap, and an implementation built
|
|
170
|
+
on a premise they had never confirmed. When the premise turned out to be wrong
|
|
171
|
+
the whole build was thrown away. Nothing in this file forced the question,
|
|
172
|
+
because this file told the asker to avoid predetermined sequences.
|
|
173
|
+
|
|
174
|
+
Every one of these must be resolved before PROJECT.md is written:
|
|
175
|
+
|
|
176
|
+
| Decision | Why it cannot be assumed |
|
|
177
|
+
|---|---|
|
|
178
|
+
| **Who maintains this after launch** | Drives the stack more than any technical factor. "Non-technical client" and "you, the technical owner" give opposite answers |
|
|
179
|
+
| **Stack** | Most expensive thing in the project to reverse. Never decided for the user — see the stack gate |
|
|
180
|
+
| **Who the users are, and whether there are roles** | Auth, permissions, and data model all hang off it |
|
|
181
|
+
| **What is explicitly OUT of scope for v1** | An unstated exclusion reappears later as a gap |
|
|
182
|
+
| **What already exists** | Greenfield vs brownfield changes every phase |
|
|
183
|
+
| **What "done" means for the first milestone** | Without it there is no way to verify anything |
|
|
184
|
+
| **Any hard constraint** — budget, deadline, hosting, compliance, locale | These invalidate otherwise-correct plans |
|
|
185
|
+
|
|
186
|
+
**How to run it without sounding like a form:** weave them into the conversation
|
|
187
|
+
in whatever order the user's own answers suggest — that part stays conversational.
|
|
188
|
+
But **track them, and before you write PROJECT.md, state which ones the user
|
|
189
|
+
actually answered and which you are assuming, with the assumption spelled out.**
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
Before I write this up — you answered: maintainer (you), scope (city pages only),
|
|
193
|
+
users (visitors, no login).
|
|
194
|
+
I'm assuming: no deadline, hosting undecided, English only.
|
|
195
|
+
Correct any of those, or say go.
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Tag assumptions in the artifact itself, not just in chat.** Every inferred value
|
|
199
|
+
written into PROJECT.md, REQUIREMENTS.md, or ROADMAP.md carries an inline
|
|
200
|
+
`[ASSUMPTION]` marker. A summary the user scrolled past is not consent; a tag in
|
|
201
|
+
the document survives the conversation and can be triaged later.
|
|
202
|
+
|
|
203
|
+
Before any artifact is marked final, **walk every `[ASSUMPTION]` tag with the
|
|
204
|
+
user**: confirm it, correct it, or defer it with an owner. An untriaged assumption
|
|
205
|
+
in a finalised document is a decision nobody made.
|
|
206
|
+
|
|
207
|
+
That block is not optional and auto mode does not remove it. An assumption the
|
|
208
|
+
user never saw is indistinguishable from a decision you made for them.
|
|
209
|
+
|
|
112
210
|
## Context Checklist
|
|
113
211
|
|
|
114
212
|
After Socratic questioning, verify these dimensions were covered:
|
|
@@ -122,7 +220,7 @@ After Socratic questioning, verify these dimensions were covered:
|
|
|
122
220
|
- [ ] **Auth/identity** — SSO, local accounts, guest access, or a specific IdP?
|
|
123
221
|
- [ ] **Locale/i18n** — Which languages/regions must be supported, RTL needed?
|
|
124
222
|
|
|
125
|
-
If gaps remain after natural conversation, weave questions naturally. Don't suddenly shift to checklist mode.
|
|
223
|
+
If gaps remain after natural conversation, weave questions naturally. Don't suddenly shift to checklist mode — but do NOT let "not a checklist" become "never asked". Anything from the Mandatory decision set still unresolved gets asked outright before you move on, plainly, rather than silently assumed.
|
|
126
224
|
|
|
127
225
|
---
|
|
128
226
|
|
|
@@ -145,6 +145,20 @@ Read only when the current task needs the detail. Don't preemptively load.
|
|
|
145
145
|
## Workflow
|
|
146
146
|
|
|
147
147
|
1. **Read context** — REQUIREMENTS.md, FEATURES.md, ARCHITECTURE.md, STACK.md, RESEARCH.md (per `<files_to_read>`).
|
|
148
|
+
**STACK.md is a suggestion until a `state add-decision` entry shows the user
|
|
149
|
+
confirmed it.** If no such entry exists, do not build the roadmap around that
|
|
150
|
+
stack — say the stack is unconfirmed and route back to the stack gate. A
|
|
151
|
+
roadmap phased around an unconfirmed stack is what makes the wrong choice
|
|
152
|
+
expensive: by the time anyone questions it, every phase depends on it.
|
|
153
|
+
If the project has pivoted since the stack was chosen, check whether the
|
|
154
|
+
premise recorded with that decision still holds. If it does not, the decision
|
|
155
|
+
is stale, not locked.
|
|
156
|
+
**Read PROJECT.md's Glossary and use its terms verbatim** in phase names and
|
|
157
|
+
goals. A roadmap that renames the domain's nouns forces every downstream
|
|
158
|
+
agent to guess which concept a phase is about.
|
|
159
|
+
**Read PROJECT.md's Out of Scope (Non-Goals) before phasing.** A phase whose
|
|
160
|
+
goal reaches into a declared non-goal is scope creep with a plan attached —
|
|
161
|
+
flag it rather than quietly phasing it.
|
|
148
162
|
2. **Cluster requirements** — group related requirements into natural delivery units.
|
|
149
163
|
3. **Derive phases** — name each phase by what the user can DO after it, not what was built.
|
|
150
164
|
3b. **Declare the Information Architecture** (UI projects only) — before phases are finalized, explicitly decide the app's eventual final-state IA, not per-phase: enumerate the top-level sections (e.g. Dashboard / Operations / Reports / Admin), pick sidebar vs topbar vs tabs, state max nesting depth (e.g. 2 levels: section > subsection), and group every planned phase's screens under one of those sections. Persist this as an `IA.md` (or a "## Information Architecture" section in ROADMAP.md). A flat list of nav links that grows by one item per phase is not an IA decision — it's the failure mode this step exists to prevent. Later phases must slot new routes under an existing top-level section or explicitly propose adding one, never silently append a new sidebar item.
|
|
@@ -41,6 +41,18 @@ Before verifying, discover project context:
|
|
|
41
41
|
4. **Verify observable truths** — for each truth, status ✓ VERIFIED / ✗ FAILED / ? UNCERTAIN.
|
|
42
42
|
5. **Verify artifacts (4 levels)** — exists, substantive, wired, data-flows. Use `rcode-tools.cjs verify artifacts`.
|
|
43
43
|
6. **Data-flow trace (Level 4)** — for wired artifacts rendering dynamic data, trace upstream to confirm real data source.
|
|
44
|
+
6e. **Check the requirement's own consequences, not your reconstruction of them.**
|
|
45
|
+
For every requirement this phase claims, read its `**Consequences (testable):**`
|
|
46
|
+
list in REQUIREMENTS.md and verify those. Where a plan's truth carries a
|
|
47
|
+
`[DERIVED]` prefix, the requirement had none recorded and you are checking an
|
|
48
|
+
invented criterion — say so in VERIFICATION.md. A phase that passes only against
|
|
49
|
+
derived criteria has not been verified against what anyone actually asked for.
|
|
50
|
+
|
|
51
|
+
6d. **Unconfirmed assumptions are verification gaps.** Read PROJECT.md's
|
|
52
|
+
Assumptions Index. Any row still `unconfirmed` that this phase's must-haves
|
|
53
|
+
depend on is a gap, not a formality — the phase was built on something nobody
|
|
54
|
+
agreed to. Name it in VERIFICATION.md rather than passing over it.
|
|
55
|
+
|
|
44
56
|
6c. **Production reachability (Level 5b) — EVERY phase, including backend-only.**
|
|
45
57
|
For each non-UI module this phase delivered, list its importers and classify them
|
|
46
58
|
production vs test. If every importer is a test file, the phase shipped dead code
|
|
@@ -37,6 +37,12 @@ Bypassing it produces a built project with no execution trace, no SUMMARY.md, an
|
|
|
37
37
|
a dashboard frozen at `planned`. See issue #915.
|
|
38
38
|
|
|
39
39
|
<pre_flight>
|
|
40
|
+
0a. **Record the authorized scope** — the user ran an execute command, so building
|
|
41
|
+
is authorized from here:
|
|
42
|
+
```bash
|
|
43
|
+
node ".rcode/bin/rcode-tools.cjs" state set-intent build --source execute.md
|
|
44
|
+
```
|
|
45
|
+
|
|
40
46
|
**Mandatory before execution begins.** Run these checks first and surface
|
|
41
47
|
findings BEFORE any subagents are spawned. If any check fails, stop and
|
|
42
48
|
route back to the user.
|
|
@@ -111,6 +111,41 @@ Create `.planning/REQUIREMENTS.md` with:
|
|
|
111
111
|
|
|
112
112
|
**REQ-ID format:** `[CATEGORY]-[NUMBER]` (AUTH-01, CONTENT-02)
|
|
113
113
|
|
|
114
|
+
**Every requirement carries its testable consequences.** This is the shape:
|
|
115
|
+
|
|
116
|
+
```markdown
|
|
117
|
+
- [ ] **AUTH-01**: User can log in with email and password and stay logged in
|
|
118
|
+
across sessions.
|
|
119
|
+
- **Consequences (testable):**
|
|
120
|
+
- A valid credential pair returns a session cookie with a 30-day expiry
|
|
121
|
+
- An invalid password returns 401 and does not reveal whether the email exists
|
|
122
|
+
- A logged-in user reloading the page stays logged in
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Why the consequences live here and not in the plan.** rcode's verifier derives
|
|
126
|
+
`must_haves` at verification time, long after the requirement was written — so it
|
|
127
|
+
is guessing at what "done" meant for a requirement someone else authored. That
|
|
128
|
+
guess is where verification quietly goes wrong: a phase passes because the
|
|
129
|
+
verifier's invented criterion was met, not the one the requirement intended.
|
|
130
|
+
|
|
131
|
+
Writing the consequences with the requirement moves that decision to the moment
|
|
132
|
+
the person actually knows the answer. The planner then copies them into
|
|
133
|
+
`must_haves.truths` instead of inventing them, and the verifier checks the
|
|
134
|
+
requirement's own criteria rather than its own reconstruction.
|
|
135
|
+
|
|
136
|
+
A requirement whose consequences you cannot state is a requirement you have not
|
|
137
|
+
finished writing. "Handle authentication properly" has no consequences because it
|
|
138
|
+
has no meaning — that is the signal to push for specificity, not to move on.
|
|
139
|
+
|
|
140
|
+
**Scope dial:** hobby/solo — one consequence per requirement is usually enough,
|
|
141
|
+
and it can be a sentence. Internal tool — the happy path plus the one failure
|
|
142
|
+
mode that matters. Launch — every condition a reviewer would ask about, including
|
|
143
|
+
the negative cases.
|
|
144
|
+
|
|
145
|
+
**Do not change the traceability table's shape.** `requirements mark-complete`
|
|
146
|
+
rewrites the status cell of a `| ID | ... | status |` row; consequences are nested
|
|
147
|
+
under the requirement in the list above, not added as table columns.
|
|
148
|
+
|
|
114
149
|
**Requirement quality criteria:**
|
|
115
150
|
|
|
116
151
|
Good requirements are:
|
|
@@ -119,6 +154,7 @@ Good requirements are:
|
|
|
119
154
|
- **User-centric:** "User can X"
|
|
120
155
|
- **Atomic:** One capability per requirement
|
|
121
156
|
- **Independent:** Minimal dependencies on other requirements
|
|
157
|
+
- **Consequential:** you can name what must be true for it to be done
|
|
122
158
|
|
|
123
159
|
Reject vague requirements. Push for specificity:
|
|
124
160
|
|
|
@@ -243,7 +243,67 @@ Display research complete banner and key findings:
|
|
|
243
243
|
Files: `.planning/research/`
|
|
244
244
|
```
|
|
245
245
|
|
|
246
|
-
|
|
246
|
+
## 6b. Stack confirmation gate — HARD STOP
|
|
247
|
+
|
|
248
|
+
**The stack is never decided for the user. Not by research, not by a
|
|
249
|
+
recommendation, not by "the obvious choice for this domain."** Research produces
|
|
250
|
+
a *suggestion*; only the user turns it into a decision.
|
|
251
|
+
|
|
252
|
+
This gate runs whether research ran or was skipped. If research was skipped, ask
|
|
253
|
+
with no recommendation attached — you have no grounds for one.
|
|
254
|
+
|
|
255
|
+
```
|
|
256
|
+
AskUserQuestion:
|
|
257
|
+
header: "Stack"
|
|
258
|
+
question: "Research suggests {STACK} because {the one reason that actually drove it}. Confirm?"
|
|
259
|
+
options:
|
|
260
|
+
- label: "Confirm {STACK}"
|
|
261
|
+
description: "{the trade-off the user is accepting, stated plainly}"
|
|
262
|
+
- label: "I'll choose the stack"
|
|
263
|
+
description: "Tell me what to build on and I'll record that instead."
|
|
264
|
+
- label: "Research more first"
|
|
265
|
+
description: "Compare against {the closest alternative} before deciding."
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**Nothing proceeds until the user answers.** Not requirements, not the roadmap,
|
|
269
|
+
not a single file. An unanswered question is not a confirmation, and neither is
|
|
270
|
+
silence, `--auto`, or `auto_advance`. **Auto mode does NOT bypass this gate** —
|
|
271
|
+
every other question in this workflow has an auto default; this one does not,
|
|
272
|
+
because a wrong stack is the most expensive thing in the project to reverse.
|
|
273
|
+
|
|
274
|
+
State the reason the suggestion exists, in one sentence, in the user's terms.
|
|
275
|
+
"Research recommends WordPress" is not a reason. "WordPress because a
|
|
276
|
+
non-technical client will update content themselves, with no dev retainer" is —
|
|
277
|
+
and stated that way the user can see immediately whether the premise is true.
|
|
278
|
+
|
|
279
|
+
Record the answer with `state add-decision`, including the premise it rests on:
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
node ".rcode/bin/rcode-tools.cjs" state add-decision \
|
|
283
|
+
"Stack: {chosen}. Premise: {the one reason}. Confirmed by user {date}."
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### The premise is part of the decision
|
|
287
|
+
|
|
288
|
+
**A stack decision is only valid while its premise holds.** Write the premise
|
|
289
|
+
into the decision, then re-open the decision the moment the premise changes.
|
|
290
|
+
|
|
291
|
+
Confirmed live: a site was scoped for a non-technical client, so research picked
|
|
292
|
+
WordPress and the roadmap locked it. The project later pivoted to a model with no
|
|
293
|
+
client at all — the maintainer was the technical owner. Every planning doc was
|
|
294
|
+
rewritten for the pivot and the stack stayed "Locked", because nothing in the
|
|
295
|
+
loop treats a locked decision as re-openable. A PHP theme got built and then
|
|
296
|
+
migrated wholesale to a static generator to undo it.
|
|
297
|
+
|
|
298
|
+
**On any pivot, re-run this gate** for every decision whose stated premise the
|
|
299
|
+
pivot invalidated. A decision whose reason has expired is not locked, it is
|
|
300
|
+
stale.
|
|
301
|
+
|
|
302
|
+
**If "Research more first":** run the comparison against the named alternative,
|
|
303
|
+
then return to this gate. Do not proceed past it.
|
|
304
|
+
|
|
305
|
+
**If "Skip research":** Continue to Step 6b — you still need the stack gate,
|
|
306
|
+
you just have no suggestion to offer.
|
|
247
307
|
|
|
248
308
|
|
|
249
309
|
## Next Up
|
|
@@ -475,7 +475,12 @@ Proceed to Step 4 (skip Steps 3 and 5).
|
|
|
475
475
|
|
|
476
476
|
## 3. Deep Questioning
|
|
477
477
|
|
|
478
|
-
**If auto mode:**
|
|
478
|
+
**If auto mode:** extract project context from the provided document instead of
|
|
479
|
+
asking. **You still owe the user the Mandatory decision set** — resolve each item
|
|
480
|
+
from the document where it answers one, and where it does not, list what you are
|
|
481
|
+
assuming before Step 4 writes PROJECT.md. Auto mode removes the conversation, not
|
|
482
|
+
the accountability. If the document leaves the maintainer or the stack unanswered,
|
|
483
|
+
stop and ask those two regardless of mode.
|
|
479
484
|
|
|
480
485
|
**Display stage banner:**
|
|
481
486
|
|
|
@@ -491,6 +496,20 @@ Ask inline (freeform, NOT AskUserQuestion):
|
|
|
491
496
|
|
|
492
497
|
"What do you want to build?"
|
|
493
498
|
|
|
499
|
+
**Then, before any deeper questioning, run two short probes from
|
|
500
|
+
`@.rcode/references/questioning.md` — in this order:**
|
|
501
|
+
|
|
502
|
+
1. **Stakes calibration** — hobby/solo, internal tool, or launch? Scale every
|
|
503
|
+
artifact and gate below to the answer. Do not run the launch-grade pipeline on
|
|
504
|
+
a weekend project.
|
|
505
|
+
2. **Working mode** — Fast path (batched questions, draft with `[ASSUMPTION]`
|
|
506
|
+
tags) or Coaching path (walk the decisions together)? **Ask it. Never infer it
|
|
507
|
+
from `auto_advance`, from how detailed their opening message was, or from your
|
|
508
|
+
own read of their hurry.** Only `--auto`/yolo picks Fast path without asking.
|
|
509
|
+
|
|
510
|
+
These two answers govern the rest of this workflow. Record them with
|
|
511
|
+
`state add-decision` so a later resume does not re-guess them.
|
|
512
|
+
|
|
494
513
|
Wait for their response. This gives you the context needed to ask intelligent follow-up questions.
|
|
495
514
|
|
|
496
515
|
**Research-before-questions mode:** Check if `workflow.research_before_questions` is enabled in `.rcode/config.yaml` (via `node .rcode/bin/rcode-tools.cjs config-get workflow.research_before_questions`). When enabled, before asking follow-up questions about a topic:
|
|
@@ -527,6 +546,11 @@ Mentally check the context checklist. If gaps remain, weave questions naturally.
|
|
|
527
546
|
|
|
528
547
|
**Decision gate:**
|
|
529
548
|
|
|
549
|
+
**Before the decision gate, show your coverage.** State plainly which items of the
|
|
550
|
+
Mandatory decision set (`@.rcode/references/questioning.md`) the user actually
|
|
551
|
+
answered and which you are assuming, with each assumption spelled out in one line.
|
|
552
|
+
An assumption the user never saw is a decision you made for them.
|
|
553
|
+
|
|
530
554
|
When you could write a clear PROJECT.md, use AskUserQuestion:
|
|
531
555
|
|
|
532
556
|
- header: "Ready?"
|
|
@@ -601,16 +625,61 @@ Synthesize all context into `.planning/PROJECT.md`. If `.rcode/templates/project
|
|
|
601
625
|
- [ ] {Requirement 2}
|
|
602
626
|
- [ ] {Requirement 3}
|
|
603
627
|
|
|
604
|
-
### Out of Scope
|
|
628
|
+
### Out of Scope (Non-Goals)
|
|
629
|
+
|
|
630
|
+
*What this project is NOT and will NOT do. This does outsized work downstream —
|
|
631
|
+
it is what prevents the "let me also add this nearby thing" failure at every
|
|
632
|
+
level: phase, sprint, task, and code. An unstated exclusion reappears later as a
|
|
633
|
+
gap; a stated one ends the argument before it starts.*
|
|
605
634
|
|
|
606
635
|
- {Exclusion 1} — {why}
|
|
607
636
|
- {Exclusion 2} — {why}
|
|
608
637
|
|
|
638
|
+
**Scope dial:** hobby/solo — the two or three things you keep being tempted by.
|
|
639
|
+
Internal tool — plus anything a stakeholder has already asked for and been told
|
|
640
|
+
no. Launch — plus the "we are not becoming X" statements about the product's
|
|
641
|
+
identity.
|
|
642
|
+
|
|
643
|
+
## Glossary
|
|
644
|
+
|
|
645
|
+
*Every domain noun this project uses, defined once. Downstream agents and
|
|
646
|
+
documents use these terms verbatim — introducing a synonym anywhere is a
|
|
647
|
+
discipline violation, because two names for one thing is how a codebase ends up
|
|
648
|
+
with two implementations of it.*
|
|
649
|
+
|
|
650
|
+
- **{Term}** — {definition}. {relationship to other terms, cardinality if it matters}
|
|
651
|
+
|
|
652
|
+
**Scope dial:** hobby/solo — only terms that are genuinely ambiguous, often 2-3.
|
|
653
|
+
Internal tool — every domain noun. Launch — every domain noun plus the ones the
|
|
654
|
+
team argues about.
|
|
655
|
+
|
|
609
656
|
## Key Decisions
|
|
610
657
|
|
|
611
|
-
| Decision | Rationale | Outcome |
|
|
612
|
-
|
|
613
|
-
| {Choice} | {Why} | — Pending |
|
|
658
|
+
| Decision | Premise (what makes it right) | Rationale | Outcome |
|
|
659
|
+
|----------|-------------------------------|-----------|---------|
|
|
660
|
+
| {Choice} | {the condition this rests on} | {Why} | — Pending |
|
|
661
|
+
|
|
662
|
+
*The **Premise** column is load-bearing. A decision is valid only while its
|
|
663
|
+
premise holds — when the project pivots, every decision whose premise the pivot
|
|
664
|
+
invalidated goes back to the user. A decision whose reason has expired is not
|
|
665
|
+
locked, it is stale.*
|
|
666
|
+
|
|
667
|
+
## Assumptions Index
|
|
668
|
+
|
|
669
|
+
*Every `[ASSUMPTION]` tag in this document and in REQUIREMENTS.md, gathered here
|
|
670
|
+
for explicit confirmation. An assumption the user never saw is a decision nobody
|
|
671
|
+
made.*
|
|
672
|
+
|
|
673
|
+
| # | Assumption | Where | Status |
|
|
674
|
+
|---|-----------|-------|--------|
|
|
675
|
+
| A-1 | {what was inferred} | §{section} | unconfirmed |
|
|
676
|
+
|
|
677
|
+
**This table is walked with the user before the document is treated as settled.**
|
|
678
|
+
Each row ends as confirmed, corrected, or deferred with an owner. Auto mode does
|
|
679
|
+
not skip the walk; it defers it to the first interactive turn.
|
|
680
|
+
|
|
681
|
+
**Scope dial:** the table exists at every stakes level. Hobby/solo may resolve it
|
|
682
|
+
in one exchange; launch resolves it row by row.
|
|
614
683
|
|
|
615
684
|
## Constraints
|
|
616
685
|
|
|
@@ -628,19 +697,39 @@ This document evolves at phase transitions and milestone boundaries.
|
|
|
628
697
|
1. Requirements invalidated? → Move to Out of Scope with reason
|
|
629
698
|
2. Requirements validated? → Move to Validated with phase reference
|
|
630
699
|
3. New requirements emerged? → Add to Active
|
|
631
|
-
4. Decisions to log? → Add to Key Decisions
|
|
700
|
+
4. Decisions to log? → Add to Key Decisions, WITH its premise
|
|
632
701
|
5. "What This Is" still accurate? → Update if drifted
|
|
702
|
+
6. New domain nouns introduced? → Add to Glossary in the same pass
|
|
703
|
+
7. Any `[ASSUMPTION]` resolved or added? → Update the Assumptions Index
|
|
704
|
+
8. Did anything invalidate a recorded premise? → That decision reopens
|
|
633
705
|
|
|
634
706
|
**After each milestone** (via `/rcode-complete-milestone`):
|
|
635
707
|
1. Full review of all sections
|
|
636
708
|
2. Core Value check — still the right priority?
|
|
637
709
|
3. Audit Out of Scope — reasons still valid?
|
|
638
710
|
4. Update Context with current state
|
|
711
|
+
5. Assumptions Index — any row still `unconfirmed` after a whole milestone is a
|
|
712
|
+
finding, not a formality. Resolve or escalate it
|
|
713
|
+
6. Key Decisions — check every premise still holds
|
|
639
714
|
|
|
640
715
|
---
|
|
641
716
|
*Last updated: {date} after initialization*
|
|
642
717
|
```
|
|
643
718
|
|
|
719
|
+
**Scale every section to the stakes answer from Step 3.** The template is one
|
|
720
|
+
document that serves a weekend project and a launch; the scope dials on each
|
|
721
|
+
section say how. Running the launch-grade depth on a hobby project is its own
|
|
722
|
+
failure — the user abandons the process, not the project.
|
|
723
|
+
|
|
724
|
+
| Stakes | PROJECT.md target |
|
|
725
|
+
|---|---|
|
|
726
|
+
| Hobby / solo | About a page. Glossary only where terms are ambiguous |
|
|
727
|
+
| Internal tool | Two to four pages. Every section present, lightly filled |
|
|
728
|
+
| Launch | As long as the requirements and concerns need |
|
|
729
|
+
|
|
730
|
+
Never pad a section to look thorough, and never drop one silently — if a section
|
|
731
|
+
genuinely does not apply, say so in one line where it would have been.
|
|
732
|
+
|
|
644
733
|
**For greenfield projects:** Initialize requirements as hypotheses (all Active).
|
|
645
734
|
|
|
646
735
|
**For brownfield projects (codebase map exists):** Read `.planning/codebase/ARCHITECTURE.md` and `STACK.md`. Identify what the codebase already does — these become the initial Validated set.
|
package/rcode/workflows/plan.md
CHANGED
|
@@ -100,6 +100,15 @@ drafting tasks inline, the run has lost its orchestrator — spawn the planner
|
|
|
100
100
|
instead. A SPRINT.md with no planner `Task()` behind it is the failure this rule
|
|
101
101
|
exists to prevent (see step 8).
|
|
102
102
|
|
|
103
|
+
## 0.4. Record the authorized scope
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
node ".rcode/bin/rcode-tools.cjs" state set-intent plan --source plan.md
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
This is what the user asked for on THIS invocation, and it is what `resume-work`
|
|
110
|
+
will restore later. Planning does not authorize building — see step 15.
|
|
111
|
+
|
|
103
112
|
## 0.5. Project-Status Preflight
|
|
104
113
|
|
|
105
114
|
```bash
|
|
@@ -306,11 +315,11 @@ If `TEXT_MODE` is true, present as a plain-text numbered list:
|
|
|
306
315
|
```
|
|
307
316
|
No CONTEXT.md found for Phase {X}. Plans will use research and requirements only — your design preferences won't be included.
|
|
308
317
|
|
|
309
|
-
1. Continue without context — Plan using research + requirements only
|
|
310
318
|
[If DISCUSS_MODE is "assumptions":]
|
|
311
|
-
|
|
319
|
+
1. Gather context (assumptions mode) [recommended] — Analyze codebase and surface assumptions before planning
|
|
312
320
|
[If DISCUSS_MODE is "discuss" or unset:]
|
|
313
|
-
|
|
321
|
+
1. Run discuss-phase first [recommended] — Capture design decisions before planning
|
|
322
|
+
2. Continue without context — Plan using research + requirements only; your design preferences will not be in the plan
|
|
314
323
|
|
|
315
324
|
Enter number:
|
|
316
325
|
```
|
|
@@ -319,11 +328,13 @@ Otherwise use AskUserQuestion:
|
|
|
319
328
|
- header: "No context"
|
|
320
329
|
- question: "No CONTEXT.md found for Phase {X}. Plans will use research and requirements only — your design preferences won't be included. Continue or capture context first?"
|
|
321
330
|
- options:
|
|
322
|
-
|
|
331
|
+
(Recommended option FIRST — rcode was recommending the skip, which is how phases
|
|
332
|
+
got planned with the user's design decisions never captured.)
|
|
323
333
|
If `DISCUSS_MODE` is `"assumptions"`:
|
|
324
|
-
- "Gather context (assumptions mode)" — Analyze codebase and surface assumptions before planning
|
|
334
|
+
- "Gather context (assumptions mode) (Recommended)" — Analyze codebase and surface assumptions before planning
|
|
325
335
|
If `DISCUSS_MODE` is `"discuss"` (or unset):
|
|
326
|
-
- "Run discuss-phase first" — Capture design decisions before planning
|
|
336
|
+
- "Run discuss-phase first (Recommended)" — Capture design decisions before planning
|
|
337
|
+
- "Continue without context" — Plan using research + requirements only; your design preferences will not be in the plan
|
|
327
338
|
|
|
328
339
|
If "Continue without context": Proceed to step 5.
|
|
329
340
|
If "Run discuss-phase first":
|
|
@@ -926,7 +937,30 @@ Returns (else branch only):
|
|
|
926
937
|
|
|
927
938
|
After plans pass the checker (or checker is skipped), verify that all phase requirements are covered by at least one plan.
|
|
928
939
|
|
|
929
|
-
**
|
|
940
|
+
**If `phase_req_ids` is empty, the gate does NOT silently skip — it reports why.**
|
|
941
|
+
An empty array has two very different causes and they must not look the same:
|
|
942
|
+
|
|
943
|
+
1. This phase genuinely maps to no requirements. Fine, say so and continue.
|
|
944
|
+
2. REQUIREMENTS.md HAS a traceability table and nothing parsed out of it. That is
|
|
945
|
+
a broken gate reporting as a passing one.
|
|
946
|
+
|
|
947
|
+
Distinguish them before proceeding:
|
|
948
|
+
|
|
949
|
+
```bash
|
|
950
|
+
if [ -f .planning/REQUIREMENTS.md ] && grep -qE '\b[A-Z][A-Z0-9]{1,15}-[0-9]+\b' .planning/REQUIREMENTS.md; then
|
|
951
|
+
echo "⚠ Requirements coverage gate SKIPPED but REQUIREMENTS.md contains requirement IDs."
|
|
952
|
+
echo " phase_req_ids came back empty — the phase→requirement mapping in ROADMAP.md"
|
|
953
|
+
echo " is missing or unparseable, so nothing is verifying coverage for this phase."
|
|
954
|
+
echo " Fix the phase's **Requirements:** line in ROADMAP.md, then re-run."
|
|
955
|
+
fi
|
|
956
|
+
```
|
|
957
|
+
|
|
958
|
+
Surface that warning to the user; do not bury it. Confirmed live: a project's
|
|
959
|
+
requirement IDs were all domain-prefixed (`FOUND-01`, `RENT-04`), the extractor
|
|
960
|
+
only matched `REQ-*`, and this gate skipped itself on every phase while
|
|
961
|
+
appearing to pass.
|
|
962
|
+
|
|
963
|
+
Then proceed to step 14 when the array really is empty.
|
|
930
964
|
|
|
931
965
|
**Step 1: Extract requirement IDs claimed by plans**
|
|
932
966
|
```bash
|
|
@@ -1028,7 +1062,34 @@ if ([[ "$ARGUMENTS" =~ --auto ]] || [[ "$ARGUMENTS" =~ --chain ]]) && [[ "$AUTO_
|
|
|
1028
1062
|
fi
|
|
1029
1063
|
```
|
|
1030
1064
|
|
|
1031
|
-
|
|
1065
|
+
**`AUTO_CFG` alone is NOT sufficient.** A persistent `workflow.auto_advance: true`
|
|
1066
|
+
in settings must never silently turn "plan this" into "plan and build this". The
|
|
1067
|
+
user's invocation is their declared scope: they typed a planning command, so
|
|
1068
|
+
planning is what was authorized. A config flag set weeks ago is not consent for
|
|
1069
|
+
this build.
|
|
1070
|
+
|
|
1071
|
+
Confirmed live: a user asked for a project to be planned, `auto_advance` was on,
|
|
1072
|
+
and the session planned and then built a WordPress theme, then migrated the whole
|
|
1073
|
+
thing to Astro to undo its own stack choice. The user's words were "plan karo".
|
|
1074
|
+
Nothing in the loop stopped at the boundary they actually drew.
|
|
1075
|
+
|
|
1076
|
+
**If `AUTO_CFG` is true but neither `--auto`/`--chain` nor `AUTO_CHAIN` is set:**
|
|
1077
|
+
ask before advancing, and default to stopping:
|
|
1078
|
+
|
|
1079
|
+
```
|
|
1080
|
+
AskUserQuestion:
|
|
1081
|
+
question: "Plans are ready. auto_advance is on in your config — execute phase {N} now?"
|
|
1082
|
+
options:
|
|
1083
|
+
- label: "Stop here (Recommended)"
|
|
1084
|
+
description: "Plans written and verified. Review them, then run /rcode-execute {N} when ready."
|
|
1085
|
+
- label: "Execute now"
|
|
1086
|
+
description: "Chain straight into execution, as auto_advance requests."
|
|
1087
|
+
```
|
|
1088
|
+
|
|
1089
|
+
In `--text` mode present this as a numbered list. If the user does not answer,
|
|
1090
|
+
STOP — an unanswered question is not approval.
|
|
1091
|
+
|
|
1092
|
+
**If `--auto` or `--chain` flag present OR `AUTO_CHAIN` is true:**
|
|
1032
1093
|
|
|
1033
1094
|
Display banner:
|
|
1034
1095
|
```
|
|
@@ -1156,6 +1217,7 @@ ${WINDOWS === 'true' ? '@.rcode/references/plan-windows-troubleshooting.md' : ''
|
|
|
1156
1217
|
- [ ] Phase directory created if needed
|
|
1157
1218
|
- [ ] CONTEXT.md loaded early (step 4) and passed to ALL agents
|
|
1158
1219
|
- [ ] Research completed (unless --skip-research or --gaps or exists)
|
|
1220
|
+
- [ ] Auto-advance fired only on an explicit `--auto`/`--chain` or an answered confirmation, never on `auto_advance` config alone
|
|
1159
1221
|
- [ ] Specialist review panel spawned (Waleed + Fatima + domain agents) and its blocking issues fed into the revision loop, or `workflow.specialist_review: false` recorded
|
|
1160
1222
|
- [ ] rcode-phase-researcher spawned with CONTEXT.md
|
|
1161
1223
|
- [ ] Existing plans checked
|
|
@@ -197,6 +197,24 @@ Based on project state, determine the most logical next action:
|
|
|
197
197
|
</step>
|
|
198
198
|
|
|
199
199
|
<step name="offer_options">
|
|
200
|
+
**"Resume" restores position, never scope.** It tells you WHERE the work stopped;
|
|
201
|
+
it does not tell you what the user authorized. Present options and wait — do not
|
|
202
|
+
pick one and start.
|
|
203
|
+
|
|
204
|
+
The failure this prevents: a user asks for a project to be planned, the session
|
|
205
|
+
plans it, the user later types "resume", and the session reads that as "keep
|
|
206
|
+
going" and starts building. The standing instruction was still "plan". Confirmed
|
|
207
|
+
live, and the cleanup cost more than the work.
|
|
208
|
+
|
|
209
|
+
Two rules:
|
|
210
|
+
|
|
211
|
+
- **Never begin implementation from a resume.** If the state says the phase is
|
|
212
|
+
ready to execute, that is an option to OFFER, not an action to take. Execution
|
|
213
|
+
starts when the user runs `/rcode-execute`, not when a menu suggests it.
|
|
214
|
+
- **Say what the last authorized scope was**, if the state records one (see
|
|
215
|
+
`state read` → `last_intent`, written by plan.md and execute.md). If it does not, say that plainly rather than
|
|
216
|
+
inferring one: `Last recorded scope: unknown — tell me plan or build.`
|
|
217
|
+
|
|
200
218
|
Present contextual options based on project state:
|
|
201
219
|
|
|
202
220
|
```
|