create-pmos 0.38.3 → 0.38.5

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": "create-pmos",
3
- "version": "0.38.3",
3
+ "version": "0.38.5",
4
4
  "description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@58c817d
1
+ kernel-source: pmos@f5daae8
2
2
  assembled: 2026-10-02
3
3
  mode: vendor
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Eval calibration log"
4
4
  description: "Append-only record of every PM override of an evaluator verdict."
5
- generated: {by: process:create-pmos, at: "2026-10-02T06:19:09Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-02T23:30:04Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Health budgets"
4
4
  description: "Closed registry of health-budget anchors (D59), edited only by the PM."
5
- generated: {by: process:create-pmos, at: "2026-10-02T06:19:09Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-02T23:30:04Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -0,0 +1,226 @@
1
+ 'use strict';
2
+ /*
3
+ * Lane definitions — the one place a workflow is defined (D77).
4
+ *
5
+ * A lane is a kind of work. This file says, for each lane: what it is for, what goes in, which
6
+ * rungs the work passes, what comes out, and when it is done. The workflow engine, the lane
7
+ * picker, the lane write and the workspace detail pane all read THIS file. Change a lane here and
8
+ * every one of them changes; there is no second list to keep in step.
9
+ *
10
+ * Two kinds of field:
11
+ * · prose (label, when, input, output, done) — shown to the PM, never read by the engine's rules;
12
+ * · rules (track, rungs, labels) — read by the engine, so a change here changes the ladder.
13
+ *
14
+ * The engine runs under stock Node with no dependencies, and the kit vendors it by walking its
15
+ * relative requires, so this is a plain CommonJS module rather than a JSON file.
16
+ *
17
+ * `rungs` lists the pm-artifacts rungs a lane passes through. A rung left out reads `n/a` with the
18
+ * lane named as the reason, so a lane that skips a rung says so instead of showing it pending.
19
+ * The code profile's ladder is fixed by D56/D58/D60; its definitions here are descriptive.
20
+ */
21
+
22
+ const PM_ALL_RUNGS = ['brief', 'draft', 'review', 'approved', 'handoff'];
23
+ const PM_NO_HANDOFF = ['brief', 'draft', 'review', 'approved'];
24
+
25
+ const PM_ARTIFACTS = {
26
+ 'new-feature': {
27
+ label: 'New feature',
28
+ group: 'work',
29
+ when: 'A capability that does not exist yet.',
30
+ input: 'The problem, who has it, and the evidence that it is real.',
31
+ output: 'A product requirements document (PRD).',
32
+ done: 'The PRD exists, a reviewer has judged it, the PM has accepted it, and it is handed to the team or tracker.',
33
+ track: 'governed', rungs: PM_ALL_RUNGS, brief_label: 'problem brief', draft_label: 'PRD',
34
+ },
35
+ improvement: {
36
+ label: 'Feature improvement',
37
+ group: 'work',
38
+ when: 'A change to a feature that already exists.',
39
+ input: 'The feature as it is now, what is wrong or missing, and the measure the change should move.',
40
+ output: 'A change specification: what changes, what stays the same, and how to tell it worked.',
41
+ done: 'The change specification exists, a reviewer has judged it, the PM has accepted it, and it is handed off.',
42
+ track: 'governed', rungs: PM_ALL_RUNGS, brief_label: 'change brief', draft_label: 'change specification',
43
+ },
44
+ 'bug-fix': {
45
+ label: 'Bug fix',
46
+ group: 'work',
47
+ when: 'Behaviour that is wrong against what was promised.',
48
+ input: 'A bug report: steps to reproduce, expected result, actual result, severity.',
49
+ output: 'A fix specification: the cause, the behaviour after the fix, and the check that proves it.',
50
+ done: 'The fix specification exists, a reviewer has judged it, the PM has accepted it, and it is handed off.',
51
+ track: 'light', rungs: PM_ALL_RUNGS, brief_label: 'bug report', draft_label: 'fix specification',
52
+ },
53
+ discovery: {
54
+ label: 'New product discovery',
55
+ group: 'work',
56
+ when: 'A question that must be answered before anyone commits to build.',
57
+ input: 'The question, what is unknown, and the evidence that would make this worth picking up or dropping.',
58
+ output: 'Findings and a recommendation: go, drop, or learn more.',
59
+ done: 'The findings exist, a reviewer has judged them, the PM has accepted them, and the recommendation is handed off.',
60
+ track: 'governed', rungs: PM_ALL_RUNGS, brief_label: 'question brief', draft_label: 'findings',
61
+ },
62
+ chore: {
63
+ label: 'Chore',
64
+ group: 'work',
65
+ when: 'Upkeep that needs no requirements document.',
66
+ input: 'One paragraph that states the intent.',
67
+ output: 'The change itself, with the paragraph in the record.',
68
+ done: 'The change is made and the PM has accepted it. Nothing is handed off.',
69
+ track: 'light', rungs: PM_NO_HANDOFF, brief_label: 'intent', draft_label: 'change',
70
+ },
71
+ prd: {
72
+ label: 'PRD (document)',
73
+ group: 'document',
74
+ when: 'A standalone requirements document that is not a new feature, an improvement or a bug fix.',
75
+ input: 'The brief: the question, the reader, the decision it informs.',
76
+ output: 'A product requirements document.',
77
+ done: 'The PRD exists, a reviewer has judged it, the PM has accepted it, and it is handed off.',
78
+ track: 'governed', rungs: PM_ALL_RUNGS, brief_label: 'brief', draft_label: 'PRD',
79
+ },
80
+ strategy: {
81
+ label: 'Strategy (document)',
82
+ group: 'document',
83
+ when: 'A statement of direction: where to play, how to win, what not to do.',
84
+ input: 'The brief: the question, the reader, the decision it informs.',
85
+ output: 'A strategy document.',
86
+ done: 'The document exists, a reviewer has judged it, the PM has accepted it, and it is handed off.',
87
+ track: 'governed', rungs: PM_ALL_RUNGS, brief_label: 'brief', draft_label: 'strategy document',
88
+ },
89
+ 'project-plan': {
90
+ label: 'Project plan (document)',
91
+ group: 'document',
92
+ when: 'Who does what, by when, in what order.',
93
+ input: 'The scope, the people and the dates that are fixed.',
94
+ output: 'A project plan.',
95
+ done: 'The plan exists, a reviewer has judged it, the PM has accepted it, and it is handed off.',
96
+ track: 'governed', rungs: PM_ALL_RUNGS, brief_label: 'brief', draft_label: 'project plan',
97
+ },
98
+ 'decision-record': {
99
+ label: 'Decision record (document)',
100
+ group: 'document',
101
+ when: 'A decision that was made and must not be re-argued.',
102
+ input: 'The options, the choice, and the reason.',
103
+ output: 'A decision record.',
104
+ done: 'The record exists, a reviewer has judged it, and the PM has accepted it. Nothing is handed off.',
105
+ track: 'light', rungs: PM_NO_HANDOFF, brief_label: 'brief', draft_label: 'decision record',
106
+ },
107
+ report: {
108
+ label: 'Report (document)',
109
+ group: 'document',
110
+ when: 'An account of what happened or where things stand.',
111
+ input: 'The period or subject, and the reader.',
112
+ output: 'A report.',
113
+ done: 'The report exists, a reviewer has judged it, and the PM has accepted it. Nothing is handed off.',
114
+ track: 'light', rungs: PM_NO_HANDOFF, brief_label: 'brief', draft_label: 'report',
115
+ },
116
+ };
117
+
118
+ /*
119
+ * The code profile's lanes. They share ONE ladder; a lane changes only the artifact the spec rung
120
+ * requires (`requires`), whether a rubric is needed, the prototype rule, and the build-path rule. Those
121
+ * rules are fixed by D56/D58/D60 and live in workflow-state.js; what is here is what the PM reads.
122
+ */
123
+ const PRODUCT_CODE = {
124
+ feature: {
125
+ label: 'Feature',
126
+ group: 'work',
127
+ when: 'New or changed behaviour in the product.',
128
+ input: 'A product requirements document and an evaluation rubric, written before the build.',
129
+ requires: 'A PRD and a rubric, both written before the build.',
130
+ output: 'A pull request that satisfies the rubric.',
131
+ done: 'The build passes the rubric, an explainer is written, and the PM has accepted it.',
132
+ },
133
+ discovery: {
134
+ label: 'Discovery',
135
+ group: 'work',
136
+ when: 'A question to answer before building.',
137
+ input: 'A spike brief: the question and the evidence that would end the spike.',
138
+ requires: 'A spike brief.',
139
+ output: 'Findings that answer the question.',
140
+ done: 'The findings answer the brief and the PM has accepted them.',
141
+ },
142
+ chore: {
143
+ label: 'Chore',
144
+ group: 'work',
145
+ when: 'Upkeep that needs no requirements document.',
146
+ input: 'One paragraph that states the intent.',
147
+ requires: 'One paragraph of intent, written in the record\'s status.',
148
+ output: 'The change.',
149
+ done: 'The change is made and the PM has accepted it.',
150
+ },
151
+ incident: {
152
+ label: 'Incident',
153
+ group: 'work',
154
+ when: 'Something broke and was fixed first.',
155
+ input: 'A record written after the fix: what broke, the cause, the repair.',
156
+ requires: 'An incident record.',
157
+ output: 'The fix and its post-hoc record.',
158
+ done: 'The record states the cause and the repair, and the PM has accepted it.',
159
+ },
160
+ content: {
161
+ label: 'Content',
162
+ group: 'work',
163
+ when: 'Writing or media that ships with the product.',
164
+ input: 'A content brief. A rubric is recommended.',
165
+ requires: 'A content brief. A rubric is optional.',
166
+ output: 'The content.',
167
+ done: 'The content meets the brief and the PM has accepted it.',
168
+ },
169
+ prototype: {
170
+ label: 'Prototype',
171
+ group: 'work',
172
+ when: 'An idea the PM cannot judge from a description.',
173
+ input: 'A quick brief.',
174
+ requires: 'A quick prototype brief.',
175
+ output: '2–3 specimens the PM can look at. A prototype is never scored.',
176
+ done: 'The PM has looked at the specimens and chosen what to do with them.',
177
+ },
178
+ };
179
+
180
+ /*
181
+ * What decides each rung. This is the answer to "how does PMOS know this step is done" — one
182
+ * sentence per rung, shown beside the evidence in the detail pane. It restates the engine's test;
183
+ * the engine remains the authority, and the suite checks that every rung the engine emits has a
184
+ * sentence here.
185
+ */
186
+ const RUNG_RULES = {
187
+ 'pm-artifacts': {
188
+ route: 'The lane named in the record is one this repository offers.',
189
+ anchor: 'parent_kr names a KR, a declared health budget, or a domain goal that resolves.',
190
+ brief: 'The file named in brief_path (or prd_path) exists.',
191
+ draft: 'The file named in doc_path exists. On the governed track it did not first appear before the brief.',
192
+ review: 'evals.jsonl holds a verdict for this initiative, or the record names a reviewer in reviewed_by.',
193
+ approved: 'acceptances.jsonl holds an accepted line for this initiative. The PM records it; it is never automatic.',
194
+ handoff: 'handed_off in the record names where the work went.',
195
+ },
196
+ 'product-code': {
197
+ route: 'The lane is one this repository offers, and its specimen and surface questions are answered.',
198
+ anchor: 'parent_kr names a KR, a declared health budget, or a domain goal that resolves.',
199
+ prototype: 'A prototype brief first appeared before the PRD, where the specimen test calls for one.',
200
+ understand: 'An explainer for the existing surface first appeared before the PRD.',
201
+ spec: 'The PRD and rubric named in the record exist and first appeared at or before the first build commit.',
202
+ priors: 'The record cites a prior run or states that none applies.',
203
+ contract: 'A run in agent-runs.jsonl holds done criteria and a test method with PM_approved set.',
204
+ build: 'Commits attributed to this initiative change a build path.',
205
+ quality: 'CI decides on the pull request. The engine never claims a test result it did not run.',
206
+ review: 'evals.jsonl holds a Review-Gate verdict for this initiative, or a skip.',
207
+ explain: 'An explainer exists for the run.',
208
+ accept: 'acceptances.jsonl holds an accepted line for this initiative. The PM records it.',
209
+ outcome: 'outcome_checks holds a validated, refuted or inconclusive disposition with evidence.',
210
+ },
211
+ };
212
+
213
+ function defsFor(profile) {
214
+ return profile === 'pm-artifacts' ? PM_ARTIFACTS : PRODUCT_CODE;
215
+ }
216
+ function laneNames(profile) {
217
+ return Object.keys(defsFor(profile));
218
+ }
219
+ function laneDef(profile, lane) {
220
+ return defsFor(profile)[lane] || null;
221
+ }
222
+ function rungRule(profile, key) {
223
+ return (RUNG_RULES[profile] || {})[key] || null;
224
+ }
225
+
226
+ module.exports = { PM_ARTIFACTS, PRODUCT_CODE, RUNG_RULES, defsFor, laneNames, laneDef, rungRule };
@@ -532,9 +532,9 @@ function surfaceRoute(v) {
532
532
  // A repository declares `pm-artifacts` (a PM's documents, strategy and specifications — no code) or
533
533
  // `product-code` (PMOS-self and code repositories) in a one-line `.pmos-profile` file at its root.
534
534
  // No file, or anything else in it, is `product-code`: every existing repository behaves as before.
535
- const PM_LANES = ['strategy', 'prd', 'project-plan', 'discovery', 'decision-record', 'report', 'chore'];
536
- const PM_LIGHT_BY_DEFAULT = ['decision-record', 'report', 'chore'];
537
- const PM_NO_HANDOFF = ['decision-record', 'report', 'chore'];
535
+ // D77 — the lane lists, tracks and rung sets are DATA in scripts/lane-defs.js. This file reads them;
536
+ // it holds no lane list of its own, so editing a definition changes the ladder with no edit here.
537
+ const laneDefs = require('./lane-defs');
538
538
  function profileOf(root) {
539
539
  try {
540
540
  const line = fs.readFileSync(path.join(root, '.pmos-profile'), 'utf8')
@@ -544,9 +544,10 @@ function profileOf(root) {
544
544
  }
545
545
 
546
546
  /** The lanes a repository's initiatives may declare — what the lane picker offers and the lane write accepts. */
547
+ function laneNames(profile) { return laneDefs.laneNames(profile === 'pm-artifacts' ? 'pm-artifacts' : 'product-code'); }
547
548
  function lanesFor(root) {
548
549
  const profile = profileOf(path.resolve(root || process.cwd()));
549
- return { profile, lanes: profile === 'pm-artifacts' ? PM_LANES.slice() : ['feature', 'discovery', 'chore', 'incident', 'content', 'prototype'] };
550
+ return { profile, lanes: laneNames(profile), defs: laneDefs.defsFor(profile) };
550
551
  }
551
552
 
552
553
  // ── Step model ─────────────────────────────────────────────────────────────────────────────────
@@ -561,6 +562,60 @@ function step(key, label, status, evidence, next, opts) {
561
562
  };
562
563
  }
563
564
 
565
+ /*
566
+ * D77 — the basis of every step: the rule that decides it, and what it read.
567
+ *
568
+ * `rule` is one sentence from lane-defs.js (the engine's test, restated for a PM). `file` is a
569
+ * repository path the step read, and is set ONLY when that file exists — a path the engine did not
570
+ * find is never offered as a basis. `source` says what kind of thing decided the step: a `file`, a
571
+ * `ledger` (a .jsonl row), `git` (commit history) or `none`. `first_seen` is the date the file first
572
+ * appeared, read from the range's own index where it holds one; it is null rather than guessed.
573
+ */
574
+ function attachBasis(steps, c) {
575
+ const { profile, root, L, fm, recRel, range, hasEvals, hasAcceptance } = c;
576
+ const rel = (abs) => path.relative(root, abs);
577
+ const runs = rel(path.join(L.runsDir, L.runsFile));
578
+ const evalsLedger = rel(path.join(L.runsDir, 'evals.jsonl'));
579
+ const acceptLedger = rel(path.join(L.runsDir, 'acceptances.jsonl'));
580
+ const outcomeLedger = rel(path.join(L.runsDir, 'outcome_checks.jsonl'));
581
+ const exists = (p) => !!p && fs.existsSync(path.join(root, p));
582
+ const inEvidence = (st) => {
583
+ const m = String(st.evidence || '').match(/[\w./-]+\.(?:md|jsonl)\b/);
584
+ return m && exists(m[0]) ? m[0] : null;
585
+ };
586
+ const prd = String(fm.prd_path || '').trim();
587
+ const pick = {
588
+ 'pm-artifacts': {
589
+ route: [recRel], anchor: [recRel],
590
+ brief: [String(fm.brief_path || fm.prd_path || '').trim()], draft: [String(fm.doc_path || '').trim()],
591
+ review: [hasEvals ? evalsLedger : recRel], approved: [acceptLedger], handoff: [recRel],
592
+ },
593
+ 'product-code': {
594
+ route: [recRel], anchor: [recRel], prototype: [prd], understand: [String(fm.explainer_path || '').trim()],
595
+ spec: [prd, fm.lane === 'chore' ? recRel : null], priors: [runs], contract: [runs], build: [], quality: [],
596
+ review: [hasEvals ? evalsLedger : null], explain: [], accept: [hasAcceptance ? acceptLedger : null],
597
+ outcome: [outcomeLedger],
598
+ },
599
+ }[profile] || {};
600
+ for (const st of steps) {
601
+ let file = null;
602
+ for (const cand of (pick[st.key] || [])) if (exists(cand)) { file = cand; break; }
603
+ if (!file && st.status === 'done') file = inEvidence(st);
604
+ if (st.status === 'na') file = null;
605
+ let source = 'none';
606
+ if (file) source = file.endsWith('.jsonl') ? 'ledger' : 'file';
607
+ else if (st.key === 'build' && st.status === 'done') source = 'git';
608
+ let firstSeen = null;
609
+ if (file && source === 'file' && range && range.ok && range.dates) {
610
+ const idx = firstAppearance(root, range, file);
611
+ const t = typeof idx === 'number' && idx >= 0 ? range.dates[idx] : null;
612
+ if (t) firstSeen = new Date(t * 1000).toISOString().slice(0, 10);
613
+ }
614
+ st.basis = { rule: laneDefs.rungRule(profile, st.key), file, source, first_seen: firstSeen };
615
+ }
616
+ return steps;
617
+ }
618
+
564
619
  // D74 — `generated: {by: x, at: "2026-10-02T01:00:00Z"}` on one line (the only form PMOS writes). Returns the `at`, or ''.
565
620
  function generatedAt(v) {
566
621
  const m = /\bat:\s*["']?([^"',}\s]+)/.exec(String(v || ''));
@@ -578,8 +633,8 @@ function computeWorkflowDerived(id, opts) {
578
633
  const profile = profileOf(root);
579
634
  const pmProfile = profile === 'pm-artifacts';
580
635
  const lane = (fm.lane || (pmProfile ? 'prd' : 'feature')).trim() || (pmProfile ? 'prd' : 'feature');
581
- const knownLane = pmProfile ? PM_LANES.includes(lane)
582
- : ['feature', 'discovery', 'chore', 'incident', 'content', 'prototype'].includes(lane);
636
+ const knownLane = pmProfile ? laneNames('pm-artifacts').includes(lane)
637
+ : laneNames('product-code').includes(lane);
583
638
  // unknown lane = the profile's strictest (feature in product-code, a governed prd in pm-artifacts)
584
639
  const effLane = knownLane ? lane : (pmProfile ? 'prd' : 'feature');
585
640
  const range = (opts && opts.range) || commitRange(root);
@@ -1163,6 +1218,8 @@ function computeWorkflowDerived(id, opts) {
1163
1218
  }
1164
1219
  }
1165
1220
 
1221
+ attachBasis(steps, { profile: 'product-code', root, L, fm, recRel, range, hasEvals: evals.length > 0, hasAcceptance: accepted.length > 0 });
1222
+
1166
1223
  // ── stage, derived from what the steps found ─────────────────────────────────────────────────
1167
1224
  const by = Object.fromEntries(steps.map((s) => [s.key, s]));
1168
1225
  let stage = 'intake';
@@ -1208,51 +1265,60 @@ function computeWorkflowDerived(id, opts) {
1208
1265
  function pmLadder({ id, fm, lane, effLane, knownLane, legacy, steps, root, L, recRel, range, inChange, lastActivity }) {
1209
1266
  const declared = String(fm.track || '').trim().toLowerCase();
1210
1267
  const track = declared === 'light' || declared === 'governed' ? declared
1211
- : (PM_LIGHT_BY_DEFAULT.includes(effLane) ? 'light' : 'governed');
1268
+ : (laneDefs.laneDef('pm-artifacts', effLane) || laneDefs.laneDef('pm-artifacts', 'prd')).track;
1212
1269
  const enforce = track === 'governed' && !legacy;
1213
1270
  const exists = (p) => !!p && fs.existsSync(path.join(root, p));
1271
+ // D77 — the lane's own definition decides which rungs it passes and what it calls them.
1272
+ const def = laneDefs.laneDef('pm-artifacts', effLane) || laneDefs.laneDef('pm-artifacts', 'prd');
1273
+ const flows = (k) => def.rungs.includes(k);
1274
+ const skipped = (key, label) => step(key, label, 'na', `${effLane} lane — ${label} is not part of this flow`);
1214
1275
 
1215
1276
  // route — the lane is a pm-artifacts lane
1216
1277
  steps[0] = knownLane
1217
1278
  ? step('route', 'route', 'done', `${lane} lane · ${track} track (pm-artifacts profile, D68)`)
1218
1279
  : step('route', 'route', 'done', `unknown lane '${lane}' — treated as a governed prd, the profile's strictest (D68)`,
1219
- `set lane: to one of ${PM_LANES.join(', ')}`);
1280
+ `set lane: to one of ${laneNames('pm-artifacts').join(', ')}`);
1220
1281
 
1221
1282
  const briefPath = String(fm.brief_path || fm.prd_path || '').trim();
1222
1283
  const docPath = String(fm.doc_path || '').trim();
1223
1284
  const briefIdx = briefPath ? firstAppearance(root, range, briefPath) : null;
1224
1285
  const docIdx = docPath ? firstAppearance(root, range, docPath) : null;
1225
1286
 
1226
- if (exists(briefPath)) steps.push(step('brief', 'brief', 'done', `${briefPath} present`));
1227
- else steps.push(step('brief', 'brief', enforce ? 'blocked' : 'pending',
1228
- briefPath ? `brief_path '${briefPath}' does not exist` : 'no brief yet',
1229
- `write a short brief (the question, the reader, the decision it informs) and set brief_path in ${recRel}`,
1287
+ if (!flows('brief')) steps.push(skipped('brief', def.brief_label));
1288
+ else if (exists(briefPath)) steps.push(step('brief', def.brief_label, 'done', `${briefPath} present`));
1289
+ else steps.push(step('brief', def.brief_label, enforce ? 'blocked' : 'pending',
1290
+ briefPath ? `brief_path '${briefPath}' does not exist` : `no ${def.brief_label} yet`,
1291
+ `write the ${def.brief_label} (${def.input}) and set brief_path in ${recRel}`,
1230
1292
  { blocking: enforce, rule: 'P1' }));
1231
1293
 
1232
- if (exists(docPath)) {
1294
+ if (!flows('draft')) steps.push(skipped('draft', def.draft_label));
1295
+ else if (exists(docPath)) {
1233
1296
  const inOrder = briefIdx == null || docIdx == null ? null : docIdx >= briefIdx;
1234
1297
  if (enforce && inOrder === false) {
1235
- steps.push(step('draft', 'draft', 'blocked', `${docPath} appeared before its brief ${briefPath}`,
1298
+ steps.push(step('draft', def.draft_label, 'blocked', `${docPath} appeared before its brief ${briefPath}`,
1236
1299
  'a governed document is briefed before it is drafted — the brief must land first', { blocking: true, rule: 'P2' }));
1237
- } else steps.push(step('draft', 'draft', 'done', `${docPath} present`));
1238
- } else steps.push(step('draft', 'draft', enforce && docPath ? 'blocked' : 'pending',
1239
- docPath ? `doc_path '${docPath}' does not exist` : 'no draft yet',
1240
- `write the document and set doc_path in ${recRel}`, { blocking: enforce && !!docPath, rule: 'P2' }));
1300
+ } else steps.push(step('draft', def.draft_label, 'done', `${docPath} present`));
1301
+ } else steps.push(step('draft', def.draft_label, enforce && docPath ? 'blocked' : 'pending',
1302
+ docPath ? `doc_path '${docPath}' does not exist` : `no ${def.draft_label} yet`,
1303
+ `write the ${def.draft_label} (${def.output}) and set doc_path in ${recRel}`, { blocking: enforce && !!docPath, rule: 'P2' }));
1241
1304
 
1242
1305
  const evals = cachedJsonl(path.join(L.runsDir, 'evals.jsonl')).filter((e) => e && e.initiative_id === id);
1243
1306
  const reviewer = String(fm.reviewed_by || '').trim();
1244
- if (evals.length) steps.push(step('review', 'review', 'done', `${evals[evals.length - 1].verdict || 'verdict'} recorded in evals.jsonl`));
1307
+ if (!flows('review')) steps.push(skipped('review', 'review'));
1308
+ else if (evals.length) steps.push(step('review', 'review', 'done', `${evals[evals.length - 1].verdict || 'verdict'} recorded in evals.jsonl`));
1245
1309
  else if (reviewer) steps.push(step('review', 'review', 'done', `reviewed by ${reviewer}`));
1246
1310
  else steps.push(step('review', 'review', 'pending', 'not reviewed yet',
1247
1311
  'name the reviewer in reviewed_by: (a person, an agent, or a rubric verdict in evals.jsonl)'));
1248
1312
 
1249
1313
  const accepted = cachedJsonl(path.join(L.runsDir, 'acceptances.jsonl'))
1250
1314
  .filter((a) => a && a.initiative_id === id && a.accepted === true);
1251
- steps.push(accepted.length
1252
- ? step('approved', 'approved', 'done', `accepted by the PM${accepted[accepted.length - 1].accepted_at ? ' on ' + String(accepted[accepted.length - 1].accepted_at).slice(0, 10) : ''}`)
1315
+ const acceptedOn = accepted.length ? (accepted[accepted.length - 1].accepted_at || accepted[accepted.length - 1].at) : null;
1316
+ steps.push(!flows('approved') ? skipped('approved', 'approved')
1317
+ : accepted.length
1318
+ ? step('approved', 'approved', 'done', `accepted by the PM${acceptedOn ? ' on ' + String(acceptedOn).slice(0, 10) : ''}`)
1253
1319
  : step('approved', 'approved', 'pending', 'awaiting the PM\'s acceptance (D12)', 'the PM accepts the document — never automatic'));
1254
1320
 
1255
- if (PM_NO_HANDOFF.includes(effLane)) steps.push(step('handoff', 'handed off', 'na', `${effLane} — nothing to hand off`));
1321
+ if (!flows('handoff')) steps.push(step('handoff', 'handed off', 'na', `${effLane} — nothing to hand off`));
1256
1322
  else {
1257
1323
  const to = String(fm.handed_off || '').trim();
1258
1324
  steps.push(to ? step('handoff', 'handed off', 'done', `handed off — ${to}`)
@@ -1260,6 +1326,7 @@ function pmLadder({ id, fm, lane, effLane, knownLane, legacy, steps, root, L, re
1260
1326
  'record where it went in handed_off: (a tracker link, the team, or "accepted as the deliverable")'));
1261
1327
  }
1262
1328
 
1329
+ attachBasis(steps, { profile: 'pm-artifacts', root, L, fm, recRel, range, hasEvals: evals.length > 0, hasAcceptance: accepted.length > 0 });
1263
1330
  const by = Object.fromEntries(steps.map((x) => [x.key, x]));
1264
1331
  let stage = 'intake';
1265
1332
  if (by.brief.status === 'done') stage = 'brief';
@@ -1425,7 +1492,7 @@ function listInitiatives(root) {
1425
1492
  // ── CLI ────────────────────────────────────────────────────────────────────────────────────────
1426
1493
  function main(argv) {
1427
1494
  const args = argv.slice(2);
1428
- let root = process.cwd(); let json = false; let gate = false; let all = false;
1495
+ let root = process.cwd(); let json = false; let gate = false; let all = false; let lanes = false;
1429
1496
  const rest = [];
1430
1497
  for (let i = 0; i < args.length; i++) {
1431
1498
  const a = args[i];
@@ -1433,6 +1500,7 @@ function main(argv) {
1433
1500
  if (a === '--json') { json = true; continue; }
1434
1501
  if (a === '--gate') { gate = true; continue; }
1435
1502
  if (a === '--all') { all = true; continue; }
1503
+ if (a === '--lanes') { lanes = true; continue; }
1436
1504
  if (a === '--open') { continue; } // the board is in-flight-only by default now
1437
1505
  if (a === '--help' || a === '-h') {
1438
1506
  process.stdout.write([
@@ -1441,12 +1509,28 @@ function main(argv) {
1441
1509
  ' <id> the ladder for one initiative: where you are, what is next, what blocks',
1442
1510
  ' --gate <id> CI mode — exit 1 on a blocking order violation, with the next action',
1443
1511
  ' --all the board without the 12-row cap',
1512
+ ' --lanes every lane this repository offers: when to use it, input, output, done',
1444
1513
  ' --json machine-readable (what an agent reads at session start)', '',
1445
1514
  ].join('\n') + '\n');
1446
1515
  return 0;
1447
1516
  }
1448
1517
  rest.push(a);
1449
1518
  }
1519
+ if (lanes) {
1520
+ const info = lanesFor(root);
1521
+ if (json) { process.stdout.write(JSON.stringify(info, null, 2) + '\n'); return 0; }
1522
+ const out = ['', ` ${info.profile} — ${info.lanes.length} lanes`, ''];
1523
+ for (const name of info.lanes) {
1524
+ const d = info.defs[name];
1525
+ out.push(` ${c(1, name)}${d.label ? ` — ${d.label}` : ''}${d.track ? c(90, ` [${d.track} track]`) : ''}`);
1526
+ out.push(` when ${d.when}`);
1527
+ out.push(` input ${d.requires || d.input}`);
1528
+ out.push(` output ${d.output}`);
1529
+ out.push(` done ${d.done}`, '');
1530
+ }
1531
+ process.stdout.write(out.join('\n') + '\n');
1532
+ return 0;
1533
+ }
1450
1534
  const self = path.relative(root, __filename) || 'scripts/workflow-state.js';
1451
1535
  const id = rest[0];
1452
1536
 
@@ -1505,7 +1589,7 @@ module.exports = {
1505
1589
  // under BOTH ranges, and `in this change` is false under the trunk one.
1506
1590
  historyRange,
1507
1591
  ENFORCED_FROM,
1508
- lanesFor,
1592
+ lanesFor, laneNames,
1509
1593
  };
1510
1594
 
1511
1595
  /*