@hanzlaa/rcode 4.13.0 → 4.15.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/cli/install.js +61 -5
- package/cli/uninstall.js +13 -0
- package/dist/rcode.js +162 -162
- package/package.json +1 -1
- package/rcode/agents/rcode-project-researcher.md +19 -1
- package/rcode/agents/rules/sprint-checker/dimensions.md +25 -0
- package/rcode/bin/lib/customize.cjs +115 -0
- package/rcode/bin/lib/memlog.cjs +124 -0
- package/rcode/bin/lib/roadmap.cjs +62 -22
- package/rcode/bin/rcode-tools.cjs +197 -3
- package/rcode/references/agent-shared-rules.md +68 -0
- package/rcode/references/planner-playbook.md +11 -0
- package/rcode/references/questioning.md +100 -2
- package/rcode/references/roadmapper-playbook.md +22 -0
- package/rcode/references/universal-anti-patterns.md +26 -0
- package/rcode/references/verifier-playbook.md +12 -0
- package/rcode/workflows/execute.md +11 -0
- package/rcode/workflows/new-project-define-requirements.md +36 -0
- package/rcode/workflows/new-project-research-decision.md +63 -1
- package/rcode/workflows/new-project.md +100 -6
- package/rcode/workflows/plan.md +79 -8
- package/rcode/workflows/resume-work.md +18 -0
|
@@ -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
|
}
|
|
@@ -6987,6 +7167,16 @@ async function main() {
|
|
|
6987
7167
|
if (args[0] === 'list') { result = cmdPhasesList(args.slice(1)); if (result === undefined) return; }
|
|
6988
7168
|
else { console.error('Unknown phases subcommand. Valid: list'); process.exit(1); }
|
|
6989
7169
|
break;
|
|
7170
|
+
case 'customize': {
|
|
7171
|
+
const customize = require(path.join(__dirname, 'lib', 'customize.cjs'));
|
|
7172
|
+
result = customize.dispatch(RCODE_DIR, args);
|
|
7173
|
+
break;
|
|
7174
|
+
}
|
|
7175
|
+
case 'memlog': {
|
|
7176
|
+
const memlog = require(path.join(__dirname, 'lib', 'memlog.cjs'));
|
|
7177
|
+
result = memlog.dispatch(PLANNING_DIR, args);
|
|
7178
|
+
break;
|
|
7179
|
+
}
|
|
6990
7180
|
case 'find-phase':
|
|
6991
7181
|
result = cmdFindPhase(args);
|
|
6992
7182
|
break;
|
|
@@ -7434,6 +7624,10 @@ async function main() {
|
|
|
7434
7624
|
console.log(' phase next-range [count] → return next N contiguous free phase numbers (#730)');
|
|
7435
7625
|
console.log(' phase scaffold-milestone --names "n1|n2|..." → bulk-create phase folders for a milestone (#731)');
|
|
7436
7626
|
console.log(' phase scaffold-all → create missing phase folders for all phases in ROADMAP.md (#731)');
|
|
7627
|
+
console.log(' phase rename-dir <N> [--apply] → align a phase dir slug with its ROADMAP name (dry-run by default)');
|
|
7628
|
+
console.log(' customize <resolve <name>|list|init <name>> → per-workflow overrides in .rcode/custom/ that survive an update');
|
|
7629
|
+
console.log(' memlog <init|append|read|open> → append-only run memory (.planning/MEMLOG.md)');
|
|
7630
|
+
console.log(' memlog append --type <decision|change|override|assumption|event|blocker> --text "..." [--phase N]');
|
|
7437
7631
|
console.log(' workflow-config-audit → find workflows still referencing .planning/config.json (#733)');
|
|
7438
7632
|
console.log(' commit "<msg>" [--files p1 p2 ...] → atomic git commit with conventional-commits validation (no AI attribution, no --no-verify, no auto-push)');
|
|
7439
7633
|
console.log(' commit-to-subrepo --subrepo <p> "<msg>" → atomic commit inside a git subrepo (same validation as commit)');
|
|
@@ -7524,7 +7718,7 @@ async function main() {
|
|
|
7524
7718
|
console.log(' state story list [--sprint <NN.S>] [--status <status>]');
|
|
7525
7719
|
return;
|
|
7526
7720
|
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'];
|
|
7721
|
+
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
7722
|
// Issue #656 — top-level aliases for intuitive guesses.
|
|
7529
7723
|
const intuitionAliases = {
|
|
7530
7724
|
blocker: 'state resolve-blocker',
|
|
@@ -41,6 +41,74 @@ 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
|
+
**Apply the project's overrides before you follow a shipped rule.** rcode's
|
|
45
|
+
workflows and references are regenerated by the installer, so anything a project
|
|
46
|
+
edited into them is lost on the next update. Overrides live in `.rcode/custom/`,
|
|
47
|
+
which the installer never writes:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
node ".rcode/bin/rcode-tools.cjs" customize resolve <workflow-or-reference-name>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Non-empty `block` is appended AFTER the shipped content and wins where they
|
|
54
|
+
conflict. Overrides append rather than replace on purpose: a replacing override
|
|
55
|
+
silently drops whatever the next rcode version adds, which is the same trap as
|
|
56
|
+
editing the installed file, just slower to notice.
|
|
57
|
+
|
|
58
|
+
**Log it when it happens, not when you remember.** Every decision, change of
|
|
59
|
+
direction, override, and assumption goes into the memlog at the moment it occurs:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
node ".rcode/bin/rcode-tools.cjs" memlog append \
|
|
63
|
+
--type <decision|change|override|assumption|event|blocker> \
|
|
64
|
+
--text "<one line, with the reason>" [--phase N]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Not at the end of the session, not "when there's a natural pause" — those are the
|
|
68
|
+
entries that never get written. **Whatever is not logged is lost on the next
|
|
69
|
+
`/clear` or resume**, and a project whose artifacts nobody can explain is the
|
|
70
|
+
result. The memlog is append-only: a wrong entry is followed by a correcting
|
|
71
|
+
entry, never edited away, so the disagreement stays visible.
|
|
72
|
+
|
|
73
|
+
This does not replace `state add-decision` — that is the curated record of
|
|
74
|
+
decisions that stuck. The memlog is the raw trail, including the reversals.
|
|
75
|
+
|
|
76
|
+
**Elicitation is not authoring — hand the pen back.** When gathering what the user
|
|
77
|
+
wants, the moment you catch yourself naming the stack, picking the MVP cut, or
|
|
78
|
+
proposing the phase breakdown, stop and hand it back to them. Infer-and-confirm
|
|
79
|
+
("I'm assuming the maintainer is you, not a client — right?") is fine; presenting
|
|
80
|
+
your conclusion as a finding is not. Every inferred value that reaches an artifact
|
|
81
|
+
carries an inline `[ASSUMPTION]` tag, and every tag gets walked with the user
|
|
82
|
+
before that artifact is final. An untriaged assumption in a finished document is a
|
|
83
|
+
decision nobody made.
|
|
84
|
+
|
|
85
|
+
**Never decide the stack for the user.** Technology choice — language, framework,
|
|
86
|
+
CMS, database, hosting model — is the user's call, always. Research produces a
|
|
87
|
+
suggestion; only the user turns it into a decision. Present it with the ONE reason
|
|
88
|
+
that actually drove it, in their terms, and offer three ways out: confirm it, name
|
|
89
|
+
their own, or ask for more comparison. Then stop. No default, no auto-selection,
|
|
90
|
+
no "the obvious choice for this domain" — a wrong stack is the single most
|
|
91
|
+
expensive thing in a project to reverse.
|
|
92
|
+
|
|
93
|
+
**Write the premise into the decision, and re-open it when the premise dies.** "X
|
|
94
|
+
because a non-technical client updates content themselves" stays true only while
|
|
95
|
+
there is a non-technical client. When the project pivots, every decision whose
|
|
96
|
+
stated reason the pivot invalidated goes back to the user. A decision whose reason
|
|
97
|
+
has expired is not locked, it is stale — and treating it as locked is how a
|
|
98
|
+
codebase gets built twice.
|
|
99
|
+
|
|
100
|
+
**Planning never authorizes building.** If the user asked you to plan, design,
|
|
101
|
+
research, or audit, the deliverable is the plan, the design, the findings — not
|
|
102
|
+
the implementation. Finishing the plan and continuing into code is not
|
|
103
|
+
thoroughness, it is doing work nobody approved, and it costs more to unwind than
|
|
104
|
+
it saved. The same applies to an ambiguous continuation like "resume", "carry
|
|
105
|
+
on", or "next": it restores POSITION, never SCOPE. When the standing instruction
|
|
106
|
+
was to plan, a resume continues planning.
|
|
107
|
+
|
|
108
|
+
If you believe the next step is obvious and valuable, say so in one line and stop.
|
|
109
|
+
The user typing the next command takes two seconds; undoing an unrequested build
|
|
110
|
+
took a whole session.
|
|
111
|
+
|
|
44
112
|
---
|
|
45
113
|
|
|
46
114
|
## 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,28 @@ 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
|
+
**Cut phases vertically.** Every phase must answer: what can someone do after
|
|
160
|
+
this that they could not do before? A phase whose goal names a layer ("the
|
|
161
|
+
data model", "the API", "all the repositories") rather than a capability is
|
|
162
|
+
horizontal, and everything it builds goes unexercised until some later phase
|
|
163
|
+
reaches for it — which is how a service ships with no caller. Create the
|
|
164
|
+
schema, services, and endpoints a phase's own capability needs, and no more.
|
|
165
|
+
A genuine foundation phase is allowed, but it names what it unblocks in the
|
|
166
|
+
same sentence and is no bigger than that.
|
|
167
|
+
**Read PROJECT.md's Out of Scope (Non-Goals) before phasing.** A phase whose
|
|
168
|
+
goal reaches into a declared non-goal is scope creep with a plan attached —
|
|
169
|
+
flag it rather than quietly phasing it.
|
|
148
170
|
2. **Cluster requirements** — group related requirements into natural delivery units.
|
|
149
171
|
3. **Derive phases** — name each phase by what the user can DO after it, not what was built.
|
|
150
172
|
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.
|
|
@@ -10,6 +10,32 @@ Phase plan picks up scope adjacent to the actual goal. Symptom: phase descriptio
|
|
|
10
10
|
### Implicit prerequisites
|
|
11
11
|
Phase assumes another phase has shipped without declaring the dependency. Symptom: plan refers to a file or table that doesn't exist yet. Fix: surface the dependency in the phase's `Depends on` line in ROADMAP.md.
|
|
12
12
|
|
|
13
|
+
### Layer-first phasing (horizontal slices)
|
|
14
|
+
|
|
15
|
+
A phase whose whole job is one technical layer: "Phase 1 — create all the
|
|
16
|
+
database tables", "Phase 2 — build every repository", "Phase 3 — the API".
|
|
17
|
+
Symptom: the phase's goal names a layer or an artifact type rather than
|
|
18
|
+
something a user can do afterwards, and no phase before the last one produces
|
|
19
|
+
anything anybody can use.
|
|
20
|
+
|
|
21
|
+
Why it costs more than it looks: nothing in a layer-first phase is exercised
|
|
22
|
+
until a much later phase reaches for it, so a table, a service, or an endpoint
|
|
23
|
+
can be built wrong — or built and never wired to anything — and pass every gate
|
|
24
|
+
in between. Confirmed live: a project shipped a cycle-closing service with
|
|
25
|
+
exactly one importer in the whole repo, its own test, because the phase that
|
|
26
|
+
built it was never obliged to connect it to anything a user touches.
|
|
27
|
+
|
|
28
|
+
Fix: **cut phases vertically.** Each phase delivers one thing end to end, and
|
|
29
|
+
creates only the schema, services, and endpoints that thing needs. Tables get
|
|
30
|
+
created by the first phase that reads or writes them, not by a phase whose
|
|
31
|
+
purpose is tables. If a foundation genuinely must come first (auth, a
|
|
32
|
+
migration framework), name what it unblocks in the same sentence and keep it as
|
|
33
|
+
small as that.
|
|
34
|
+
|
|
35
|
+
The test: read a phase goal and ask *what can someone do after this that they
|
|
36
|
+
could not do before?* If the honest answer is "nothing yet, but later phases
|
|
37
|
+
need it", the phase is horizontal.
|
|
38
|
+
|
|
13
39
|
### Vague acceptance
|
|
14
40
|
Acceptance criterion is "users can do X" with no measurable threshold. Fix: make every acceptance criterion observable from outside the system — a CLI command, an API response, a log line, a UI assertion.
|
|
15
41
|
|
|
@@ -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,17 @@ 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
|
+
node ".rcode/bin/rcode-tools.cjs" memlog append --type event --text "Execution started for phase ${PHASE_NUMBER}" --phase "${PHASE_NUMBER}"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Log every deviation, checkpoint decision, and override with
|
|
48
|
+
`memlog append` as it happens — a deviation nobody recorded is
|
|
49
|
+
indistinguishable from a plan that was followed.
|
|
50
|
+
|
|
40
51
|
**Mandatory before execution begins.** Run these checks first and surface
|
|
41
52
|
findings BEFORE any subagents are spawned. If any check fails, stop and
|
|
42
53
|
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
|
|