@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzlaa/rcode",
3
- "version": "4.14.0",
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*
@@ -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