@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.
@@ -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
- const re = /\bREQ-[A-Z0-9][A-Z0-9-]*\b/g;
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: slugMatch ? slugMatch[1] : '',
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