@hanzlaa/rcode 4.14.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/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/rcode-tools.cjs +13 -0
- package/rcode/references/agent-shared-rules.md +32 -0
- package/rcode/references/roadmapper-playbook.md +8 -0
- package/rcode/references/universal-anti-patterns.md +26 -0
- package/rcode/workflows/execute.md +5 -0
- package/rcode/workflows/new-project-research-decision.md +2 -0
- package/rcode/workflows/new-project.md +5 -0
- package/rcode/workflows/plan.md +9 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzlaa/rcode",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.15.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": {
|
|
@@ -194,6 +194,31 @@ issue:
|
|
|
194
194
|
- Single task with 10+ files
|
|
195
195
|
- Complex work (auth, payments) crammed into one plan
|
|
196
196
|
|
|
197
|
+
### Check 5b — Vertical slice, not a layer
|
|
198
|
+
|
|
199
|
+
Scope Sanity above measures plan SIZE. This measures plan SHAPE, which size
|
|
200
|
+
cannot see: a perfectly sized plan that builds one horizontal layer is still
|
|
201
|
+
wrong.
|
|
202
|
+
|
|
203
|
+
For the phase as a whole, answer: **what can someone do after this phase that
|
|
204
|
+
they could not do before?** If the honest answer is "nothing yet, later phases
|
|
205
|
+
need it" → **WARNING**, and name it as layer-first phasing.
|
|
206
|
+
|
|
207
|
+
Specific red flags:
|
|
208
|
+
|
|
209
|
+
- Schema or migration work that creates tables no task in this phase reads or
|
|
210
|
+
writes. Tables belong to the first phase that uses them, not to a phase whose
|
|
211
|
+
purpose is tables.
|
|
212
|
+
- A plan that builds a service, repository, or endpoint with no caller inside
|
|
213
|
+
the same phase and no task wiring it to one.
|
|
214
|
+
- A phase goal naming a layer or artifact type ("the API", "the data model")
|
|
215
|
+
rather than a capability.
|
|
216
|
+
|
|
217
|
+
Warning, not blocker: a genuine foundation phase (auth, migration framework) is
|
|
218
|
+
sometimes correct. But it must name what it unblocks, and be no bigger than that
|
|
219
|
+
— an unbounded foundation is the failure this check exists for. See
|
|
220
|
+
`universal-anti-patterns.md` → Layer-first phasing.
|
|
221
|
+
|
|
197
222
|
**Example issue:**
|
|
198
223
|
```yaml
|
|
199
224
|
issue:
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Customize — per-workflow and per-skill overrides that survive an update.
|
|
3
|
+
*
|
|
4
|
+
* The problem: customising rcode meant editing files under `.rcode/workflows/`,
|
|
5
|
+
* `.rcode/references/`, or `~/.claude/skills/rcode-*`. Every one of those is
|
|
6
|
+
* regenerated by the installer, so the edit worked until the next
|
|
7
|
+
* `rcode install` and then silently vanished. Users learned not to customise,
|
|
8
|
+
* or learned not to update.
|
|
9
|
+
*
|
|
10
|
+
* The fix: overrides live in `.rcode/custom/`, which the installer never writes,
|
|
11
|
+
* and are composed onto the shipped file at read time.
|
|
12
|
+
*
|
|
13
|
+
* .rcode/custom/<name>.md — team override, committed
|
|
14
|
+
* .rcode/custom/<name>.user.md — personal override, gitignored
|
|
15
|
+
*
|
|
16
|
+
* Both are APPENDED after the shipped content, in that order. Append rather than
|
|
17
|
+
* replace is deliberate: a replacing override silently drops whatever the next
|
|
18
|
+
* rcode version adds to that workflow, which is the same trap as editing the
|
|
19
|
+
* installed file, just slower to notice. If a shipped rule genuinely has to go,
|
|
20
|
+
* an override that says "ignore the X rule above, here is why" is auditable in a
|
|
21
|
+
* way a deletion never is.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
|
|
27
|
+
function customDir(rcodeDir) {
|
|
28
|
+
return path.join(rcodeDir, 'custom');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Layers for one name, base first. Missing files are simply absent. */
|
|
32
|
+
function layers(rcodeDir, name) {
|
|
33
|
+
const dir = customDir(rcodeDir);
|
|
34
|
+
const out = [];
|
|
35
|
+
for (const [kind, file] of [['team', `${name}.md`], ['user', `${name}.user.md`]]) {
|
|
36
|
+
const p = path.join(dir, file);
|
|
37
|
+
if (fs.existsSync(p)) {
|
|
38
|
+
const body = fs.readFileSync(p, 'utf8').trim();
|
|
39
|
+
if (body) out.push({ kind, path: path.join('.rcode', 'custom', file), body });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function resolve(rcodeDir, name) {
|
|
46
|
+
if (!name) throw new Error('customize resolve requires a name (workflow or skill id)');
|
|
47
|
+
const found = layers(rcodeDir, name);
|
|
48
|
+
return {
|
|
49
|
+
ok: true,
|
|
50
|
+
name,
|
|
51
|
+
has_overrides: found.length > 0,
|
|
52
|
+
layers: found.map((l) => ({ kind: l.kind, path: l.path })),
|
|
53
|
+
// Rendered block a workflow can paste after its own content. Empty string
|
|
54
|
+
// when nothing is overridden, so callers never special-case.
|
|
55
|
+
block: found.length === 0 ? '' : [
|
|
56
|
+
'',
|
|
57
|
+
`<!-- rcode custom overrides for "${name}" — appended, not replacing -->`,
|
|
58
|
+
...found.map((l) => `\n### Override (${l.kind}) — ${l.path}\n\n${l.body}`),
|
|
59
|
+
].join('\n'),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function list(rcodeDir) {
|
|
64
|
+
const dir = customDir(rcodeDir);
|
|
65
|
+
if (!fs.existsSync(dir)) return { ok: true, exists: false, overrides: [] };
|
|
66
|
+
const names = new Map();
|
|
67
|
+
for (const f of fs.readdirSync(dir)) {
|
|
68
|
+
if (!f.endsWith('.md')) continue;
|
|
69
|
+
const isUser = f.endsWith('.user.md');
|
|
70
|
+
const name = isUser ? f.slice(0, -'.user.md'.length) : f.slice(0, -'.md'.length);
|
|
71
|
+
if (!names.has(name)) names.set(name, []);
|
|
72
|
+
names.get(name).push(isUser ? 'user' : 'team');
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
ok: true,
|
|
76
|
+
exists: true,
|
|
77
|
+
overrides: [...names.entries()].map(([name, kinds]) => ({ name, kinds: kinds.sort() })),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function init(rcodeDir, name) {
|
|
82
|
+
const dir = customDir(rcodeDir);
|
|
83
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
84
|
+
const p = path.join(dir, `${name}.md`);
|
|
85
|
+
if (fs.existsSync(p)) return { ok: true, created: false, path: p };
|
|
86
|
+
fs.writeFileSync(p, `# Override: ${name}
|
|
87
|
+
|
|
88
|
+
Appended after rcode's shipped content for \`${name}\`, every run.
|
|
89
|
+
The installer never touches this file, so it survives \`rcode install\`.
|
|
90
|
+
|
|
91
|
+
Write additional rules, project conventions, or explicit carve-outs here. To
|
|
92
|
+
neutralise a shipped rule, say so and say why — that is auditable in a way
|
|
93
|
+
deleting it from the installed file is not:
|
|
94
|
+
|
|
95
|
+
> Ignore the "no files over 1000 lines" rule for \`src/generated/\` — those are
|
|
96
|
+
> machine-generated and never hand-edited.
|
|
97
|
+
`, 'utf8');
|
|
98
|
+
return { ok: true, created: true, path: p };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function dispatch(rcodeDir, args) {
|
|
102
|
+
const sub = args[0];
|
|
103
|
+
switch (sub) {
|
|
104
|
+
case 'resolve': return resolve(rcodeDir, args[1]);
|
|
105
|
+
case 'list': return list(rcodeDir);
|
|
106
|
+
case 'init': {
|
|
107
|
+
if (!args[1]) throw new Error('customize init requires a name');
|
|
108
|
+
return init(rcodeDir, args[1]);
|
|
109
|
+
}
|
|
110
|
+
default:
|
|
111
|
+
throw new Error(`Unknown customize subcommand: ${sub}. Use: resolve <name> | list | init <name>`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = { dispatch, resolve, list, init };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memlog — the run's append-only memory.
|
|
3
|
+
*
|
|
4
|
+
* The problem it solves: rcode recorded decisions with `state add-decision`,
|
|
5
|
+
* manually, usually at the end of a session when someone remembered. Everything
|
|
6
|
+
* decided in between — an override, an assumption, a change of direction —
|
|
7
|
+
* existed only in the conversation, and was gone on the next `/clear` or resume.
|
|
8
|
+
* That is how a project ends up with artifacts nobody can explain and a state
|
|
9
|
+
* file that disagrees with what actually happened.
|
|
10
|
+
*
|
|
11
|
+
* The contract: one line per event, appended AS THE WORK HAPPENS, never
|
|
12
|
+
* reconstructed afterwards. Whatever is not logged is lost on resume.
|
|
13
|
+
*
|
|
14
|
+
* Deliberately append-only and never rewritten. A log you can edit is a log you
|
|
15
|
+
* can quietly correct, which defeats the point — a wrong entry is followed by a
|
|
16
|
+
* correcting entry, so the disagreement itself stays visible.
|
|
17
|
+
*
|
|
18
|
+
* This does NOT replace state.decisions[] / ~/.rcode/decisions.jsonl. Those are
|
|
19
|
+
* the curated, queryable record of decisions that stuck. The memlog is the raw
|
|
20
|
+
* trail, including the ones that were reversed.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
|
|
26
|
+
const TYPES = Object.freeze(['decision', 'change', 'override', 'assumption', 'event', 'blocker']);
|
|
27
|
+
const HEADER = `# Memlog
|
|
28
|
+
|
|
29
|
+
Append-only. One line per event, written as the work happens.
|
|
30
|
+
Never edit or delete a line — append a correcting entry instead, so the
|
|
31
|
+
disagreement stays visible.
|
|
32
|
+
|
|
33
|
+
| When | Type | Entry |
|
|
34
|
+
|------|------|-------|
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
function memlogPath(planningDir) {
|
|
38
|
+
return path.join(planningDir, 'MEMLOG.md');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function ensureMemlog(planningDir) {
|
|
42
|
+
const p = memlogPath(planningDir);
|
|
43
|
+
if (!fs.existsSync(p)) {
|
|
44
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
45
|
+
fs.writeFileSync(p, HEADER, 'utf8');
|
|
46
|
+
return { created: true, path: p };
|
|
47
|
+
}
|
|
48
|
+
return { created: false, path: p };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Escape pipes so a free-text entry cannot break the markdown table. */
|
|
52
|
+
function cell(text) {
|
|
53
|
+
return String(text).replace(/\|/g, '\\|').replace(/\r?\n/g, ' ').trim();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function append(planningDir, { type, text, phase }) {
|
|
57
|
+
if (!type) throw new Error(`memlog append requires --type (${TYPES.join('|')})`);
|
|
58
|
+
if (!TYPES.includes(type)) {
|
|
59
|
+
throw new Error(`unknown memlog type "${type}" — expected one of: ${TYPES.join(', ')}`);
|
|
60
|
+
}
|
|
61
|
+
if (!text || !String(text).trim()) throw new Error('memlog append requires --text');
|
|
62
|
+
|
|
63
|
+
ensureMemlog(planningDir);
|
|
64
|
+
const p = memlogPath(planningDir);
|
|
65
|
+
const when = new Date().toISOString().replace('T', ' ').slice(0, 16);
|
|
66
|
+
const label = phase ? `${type} · phase ${phase}` : type;
|
|
67
|
+
const line = `| ${when} | ${cell(label)} | ${cell(text)} |\n`;
|
|
68
|
+
|
|
69
|
+
// appendFileSync is atomic enough for single-line appends on every platform
|
|
70
|
+
// rcode targets, and an append cannot corrupt earlier lines the way a
|
|
71
|
+
// read-modify-write can when two agents log at once.
|
|
72
|
+
fs.appendFileSync(p, line, 'utf8');
|
|
73
|
+
return { ok: true, appended: true, type, path: path.relative(path.dirname(planningDir), p) };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function read(planningDir, { type, limit } = {}) {
|
|
77
|
+
const p = memlogPath(planningDir);
|
|
78
|
+
if (!fs.existsSync(p)) return { ok: true, exists: false, entries: [] };
|
|
79
|
+
const rows = fs.readFileSync(p, 'utf8').split('\n')
|
|
80
|
+
.filter((l) => l.startsWith('| 2') || /^\| \d{4}-/.test(l))
|
|
81
|
+
.map((l) => {
|
|
82
|
+
// Split on unescaped pipes only. cell() writes `\|` for a literal pipe in
|
|
83
|
+
// the text; a naive split('|') tore one entry into three columns and lost
|
|
84
|
+
// the tail. Caught by a test, not by reading the code.
|
|
85
|
+
const parts = l.split(/(?<!\\)\|/).map((c) => c.trim().replace(/\\\|/g, '|'));
|
|
86
|
+
return { when: parts[1] || '', type: parts[2] || '', text: parts[3] || '' };
|
|
87
|
+
});
|
|
88
|
+
let entries = type ? rows.filter((r) => r.type.split(' ')[0] === type) : rows;
|
|
89
|
+
if (limit) entries = entries.slice(-Number(limit));
|
|
90
|
+
return { ok: true, exists: true, total: rows.length, entries };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Unresolved assumptions and overrides — the entries that must not be forgotten
|
|
95
|
+
* at a milestone boundary. An assumption still sitting here after a whole
|
|
96
|
+
* milestone is a finding, not a formality.
|
|
97
|
+
*/
|
|
98
|
+
function open(planningDir) {
|
|
99
|
+
const all = read(planningDir);
|
|
100
|
+
if (!all.exists) return { ok: true, exists: false, open: [] };
|
|
101
|
+
const flagged = all.entries.filter((e) => {
|
|
102
|
+
const t = e.type.split(' ')[0];
|
|
103
|
+
return t === 'assumption' || t === 'override' || t === 'blocker';
|
|
104
|
+
});
|
|
105
|
+
return { ok: true, exists: true, open: flagged, count: flagged.length };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function dispatch(planningDir, args) {
|
|
109
|
+
const sub = args[0];
|
|
110
|
+
const flag = (name) => {
|
|
111
|
+
const i = args.indexOf(`--${name}`);
|
|
112
|
+
return i !== -1 ? args[i + 1] : undefined;
|
|
113
|
+
};
|
|
114
|
+
switch (sub) {
|
|
115
|
+
case 'init': return { ok: true, ...ensureMemlog(planningDir) };
|
|
116
|
+
case 'append': return append(planningDir, { type: flag('type'), text: flag('text'), phase: flag('phase') });
|
|
117
|
+
case 'read': return read(planningDir, { type: flag('type'), limit: flag('limit') });
|
|
118
|
+
case 'open': return open(planningDir);
|
|
119
|
+
default:
|
|
120
|
+
throw new Error(`Unknown memlog subcommand: ${sub}. Use: init | append | read | open`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = { dispatch, append, read, open, ensureMemlog, TYPES };
|
|
@@ -7167,6 +7167,16 @@ async function main() {
|
|
|
7167
7167
|
if (args[0] === 'list') { result = cmdPhasesList(args.slice(1)); if (result === undefined) return; }
|
|
7168
7168
|
else { console.error('Unknown phases subcommand. Valid: list'); process.exit(1); }
|
|
7169
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
|
+
}
|
|
7170
7180
|
case 'find-phase':
|
|
7171
7181
|
result = cmdFindPhase(args);
|
|
7172
7182
|
break;
|
|
@@ -7615,6 +7625,9 @@ async function main() {
|
|
|
7615
7625
|
console.log(' phase scaffold-milestone --names "n1|n2|..." → bulk-create phase folders for a milestone (#731)');
|
|
7616
7626
|
console.log(' phase scaffold-all → create missing phase folders for all phases in ROADMAP.md (#731)');
|
|
7617
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]');
|
|
7618
7631
|
console.log(' workflow-config-audit → find workflows still referencing .planning/config.json (#733)');
|
|
7619
7632
|
console.log(' commit "<msg>" [--files p1 p2 ...] → atomic git commit with conventional-commits validation (no AI attribution, no --no-verify, no auto-push)');
|
|
7620
7633
|
console.log(' commit-to-subrepo --subrepo <p> "<msg>" → atomic commit inside a git subrepo (same validation as commit)');
|
|
@@ -41,6 +41,38 @@ 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
|
+
|
|
44
76
|
**Elicitation is not authoring — hand the pen back.** When gathering what the user
|
|
45
77
|
wants, the moment you catch yourself naming the stack, picking the MVP cut, or
|
|
46
78
|
proposing the phase breakdown, stop and hand it back to them. Infer-and-confirm
|
|
@@ -156,6 +156,14 @@ Read only when the current task needs the detail. Don't preemptively load.
|
|
|
156
156
|
**Read PROJECT.md's Glossary and use its terms verbatim** in phase names and
|
|
157
157
|
goals. A roadmap that renames the domain's nouns forces every downstream
|
|
158
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.
|
|
159
167
|
**Read PROJECT.md's Out of Scope (Non-Goals) before phasing.** A phase whose
|
|
160
168
|
goal reaches into a declared non-goal is scope creep with a plan attached —
|
|
161
169
|
flag it rather than quietly phasing it.
|
|
@@ -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,8 +41,13 @@ a dashboard frozen at `planned`. See issue #915.
|
|
|
41
41
|
is authorized from here:
|
|
42
42
|
```bash
|
|
43
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}"
|
|
44
45
|
```
|
|
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
|
+
|
|
46
51
|
**Mandatory before execution begins.** Run these checks first and surface
|
|
47
52
|
findings BEFORE any subagents are spawned. If any check fails, stop and
|
|
48
53
|
route back to the user.
|
|
@@ -281,6 +281,8 @@ Record the answer with `state add-decision`, including the premise it rests on:
|
|
|
281
281
|
```bash
|
|
282
282
|
node ".rcode/bin/rcode-tools.cjs" state add-decision \
|
|
283
283
|
"Stack: {chosen}. Premise: {the one reason}. Confirmed by user {date}."
|
|
284
|
+
node ".rcode/bin/rcode-tools.cjs" memlog append --type decision \
|
|
285
|
+
--text "Stack: {chosen}. Premise: {the one reason}. User-confirmed at the stack gate."
|
|
284
286
|
```
|
|
285
287
|
|
|
286
288
|
### The premise is part of the decision
|
|
@@ -711,6 +711,11 @@ This document evolves at phase transitions and milestone boundaries.
|
|
|
711
711
|
5. Assumptions Index — any row still `unconfirmed` after a whole milestone is a
|
|
712
712
|
finding, not a formality. Resolve or escalate it
|
|
713
713
|
6. Key Decisions — check every premise still holds
|
|
714
|
+
7. **Memlog audit** — `rcode-tools.cjs memlog open` lists every assumption,
|
|
715
|
+
override, and blocker logged during the milestone. Walk it: each one is
|
|
716
|
+
captured in PROJECT.md, resolved, or explicitly deferred with an owner.
|
|
717
|
+
Entries that survive a whole milestone unexamined are the ones that turn into
|
|
718
|
+
"nobody knows why this is like this"
|
|
714
719
|
|
|
715
720
|
---
|
|
716
721
|
*Last updated: {date} after initialization*
|
package/rcode/workflows/plan.md
CHANGED
|
@@ -103,9 +103,18 @@ exists to prevent (see step 8).
|
|
|
103
103
|
## 0.4. Record the authorized scope
|
|
104
104
|
|
|
105
105
|
```bash
|
|
106
|
+
# Project overrides for this workflow — appended after everything below, and
|
|
107
|
+
# they win on conflict. The installer never writes .rcode/custom/, so these
|
|
108
|
+
# survive `rcode install`.
|
|
109
|
+
node ".rcode/bin/rcode-tools.cjs" customize resolve plan
|
|
106
110
|
node ".rcode/bin/rcode-tools.cjs" state set-intent plan --source plan.md
|
|
111
|
+
node ".rcode/bin/rcode-tools.cjs" memlog append --type event --text "Planning started for phase ${PHASE}" --phase "${PHASE}"
|
|
107
112
|
```
|
|
108
113
|
|
|
114
|
+
**Log as you go from here.** Every decision the panel forces, every checker issue
|
|
115
|
+
you accept or reject, every assumption the planner had to make — one
|
|
116
|
+
`memlog append` line each, at the moment it happens.
|
|
117
|
+
|
|
109
118
|
This is what the user asked for on THIS invocation, and it is what `resume-work`
|
|
110
119
|
will restore later. Planning does not authorize building — see step 15.
|
|
111
120
|
|