create-pmos 0.38.4 → 0.38.6

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.
Files changed (35) hide show
  1. package/package.json +1 -1
  2. package/payload/AGENTS.md +1 -1
  3. package/payload/KIT-VERSION +2 -2
  4. package/payload/okf/core/templates/requirements-brief-template.md +61 -0
  5. package/payload/planning/evals/eval-calibration.md +1 -1
  6. package/payload/planning/okrs/health-budgets.md +1 -1
  7. package/payload/scripts/lane-defs.js +226 -0
  8. package/payload/scripts/workflow-state.js +108 -24
  9. package/payload/scripts/workflow-sweep.js +155 -3
  10. package/payload/skills/design-brief.skill +5 -6
  11. package/payload/skills/discovery.skill +1 -2
  12. package/payload/skills/eval-rubric.skill +2 -5
  13. package/payload/skills/pmos-router.skill +3 -2
  14. package/payload/skills/prd.skill +7 -13
  15. package/payload/skills/requirements-elicitation.skill +231 -0
  16. package/payload/skills-native/design-brief/SKILL.md +5 -6
  17. package/payload/skills-native/discovery/SKILL.md +1 -2
  18. package/payload/skills-native/eval-rubric/SKILL.md +2 -5
  19. package/payload/skills-native/pmos-router/SKILL.md +3 -2
  20. package/payload/skills-native/prd/SKILL.md +7 -13
  21. package/payload/skills-native/requirements-elicitation/SKILL.md +233 -0
  22. package/payload/state/discoveries.md +1 -1
  23. package/payload/state/log.md +1 -1
  24. package/workspace/lib/audit.js +54 -0
  25. package/workspace/lib/engine/lane-defs.js +226 -0
  26. package/workspace/lib/engine/workflow-state.js +108 -24
  27. package/workspace/lib/engine/workflow-sweep.js +934 -0
  28. package/workspace/package-lock.json +1 -1
  29. package/workspace/package.json +2 -2
  30. package/workspace/public/pm/data.js +67 -24
  31. package/workspace/public/pm/panel-detail.js +12 -2
  32. package/workspace/public/pm/panel-repos.js +36 -5
  33. package/workspace/public/pm/tab-task.js +4 -1
  34. package/workspace/server.js +22 -1
  35. package/workspace/skills/gather-requirements.skill +13 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.38.4",
3
+ "version": "0.38.6",
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": {
package/payload/AGENTS.md CHANGED
@@ -81,7 +81,7 @@ Every initiative carries a `type` — the lane picks what the gate requires **be
81
81
  - **Choose the model for the work (D75).** Before you delegate a sub-task or start a long routine one, run `node pmos/scripts/pmos-model.js recommend "<the work>"`: it returns a tier (`fast` for routine, `balanced`, `deep` where a mistake is costly to find later), the model and effort it maps to in the PM's policy, and why. Use it; do not pick the strongest model for everything. Review and evaluation work is pinned and is never downgraded.
82
82
  - **Documents you write here follow OKF v0.2 (D73).** `type`, `title`, `description`, then `generated: {by: claude-code/<skill>, at: "<ISO time>"}`, `sources: [{resource: "<link or id>", title: "…"}]` and `status: draft` — **one key per line, nothing indented** (the readers stop at the first indented line), no `timestamp`. Only the PM sets `status: stable` or adds `verified`. This does not apply to initiative records, tickets or ledgers. `node pmos/scripts/okf-upgrade.js .` reports what in this repository is not yet conformant.
83
83
  - **Existing surface → the PM needs the understanding too.** That retrieval warms *you*. When the work touches something that already exists — the test is *would specifying this honestly require reading code that already exists?* — run `skills/explain-surface.skill` on that region **before** the brief/PRD is authored, in a fresh context and **not** by the agent that will write it. Standard for existing surfaces, skipped for greenfield. Without it, the agent acquires the understanding and the **PM approves a spec built on facts they never saw**.
84
- 4. Anchor: the initiative file must exist at stage `intake`/`spec` with its **lane's** spec/eval artifacts authored **before** build (eval-first, D13, generalized per lane — see the lane table above; `feature` = PRD + rubric; the incident lane alone records post-hoc). Contract per lane: `feature` = the rubric's sprint-contract section with the PM's explicit `PM_approved: true`; `discovery` = the PM-approved spike brief (question + timebox); `chore` = the stated intent; `content` = the PM-approved content brief; `prototype` = the PM-approved quick brief (the judgement exercised + the specimen format) — and **no rubric**, which is the decision, not an omission. No work before the lane's approval. Feature-lane `test_method`s SHOULD be executable checks written before build (D58) — wire `templates/product-ci-starter.yml` so tier 1 is never silently empty.
84
+ 4. Anchor: the initiative file must exist at stage `intake`/`spec` with its **lane's** spec/eval artifacts authored **before** build (eval-first, D13, generalized per lane — see the lane table above; `feature` = PRD + rubric; the incident lane alone records post-hoc). Contract per lane: `feature` = the rubric's sprint-contract section with the PM's explicit `PM_approved: true`; `discovery` = the PM-approved spike brief (question + timebox); `chore` = the stated intent; `content` = the PM-approved content brief; `prototype` = the PM-approved quick brief (the judgement exercised + the specimen format) — and **no rubric**, which is the decision, not an omission. No work before the lane's approval. When you question the PM to reach any of these, follow `skills/requirements-elicitation.skill`. Feature-lane `test_method`s SHOULD be executable checks written before build (D58) — wire `templates/product-ci-starter.yml` so tier 1 is never silently empty.
85
85
  5. Branch `initiative/<id>`; execute at control altitude; one commit per artifact.
86
86
  6. Self-evaluate against the contract — self-eval is unreliable; it is an input to the gates, never a verdict.
87
87
  7. Close the gates: **Quality** = this repo's CI plus the kit gate (`pmos/scripts/gate-initiative.sh`
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@fb3d4bc
2
- assembled: 2026-10-02
1
+ kernel-source: pmos@cb4d5ff
2
+ assembled: 2026-10-03
3
3
  mode: vendor
@@ -0,0 +1,61 @@
1
+ ---
2
+ type: Template
3
+ title: Requirements Brief Template
4
+ description: The scaffold for the brief that the requirements-elicitation skill writes — problem, evidence, constraints, stakeholders, dependencies, success measure, out of scope, open questions, gathered notes used and decisions. Copy into /planning/briefs/ as <id>-requirements.md and set it as the initiative's brief_path.
5
+ tags: [template, requirements, elicitation, lanes]
6
+ timestamp: 2026-10-03
7
+ ---
8
+
9
+ # Requirements Brief Template
10
+
11
+ **Situating context:** Authored with the `requirements-skills-overhaul` initiative. Each lane used to start from its own artifact with no shared record of what the PM said. The [requirements-elicitation skill](/skills/requirements-elicitation.skill) fills this brief from a dialogue with the PM. A PRD, a spike brief or a fix specification starts from it. Copy the block below into `/planning/briefs/<id>-requirements.md`. The copy is an operational instance, not OKF knowledge.
12
+
13
+ > Usage: replace every `<…>` placeholder. Write each answer in the PM's words where the PM gave words. Leave no section empty: write `none` or move the item to *Open questions*. Never write an unconfirmed item as a fact.
14
+
15
+ ---
16
+
17
+ ```markdown
18
+ # Requirements Brief: <initiative title>
19
+
20
+ ## Identity
21
+ - initiative_id: <id>
22
+ - lane: <lane name>
23
+ - parent_KR: <objective or KR reference, or health:<id>>
24
+ - date: <YYYY-MM-DD>
25
+
26
+ ## Problem and user
27
+ <!-- Who has the problem, and what do they do today instead? -->
28
+
29
+ ## Evidence
30
+ <!-- What shows that the problem is real? Give a source for each item. -->
31
+
32
+ ## Constraints
33
+ <!-- What is fixed: dates, budget, technology, rules, promises already made. -->
34
+
35
+ ## Stakeholders and decision owner
36
+ <!-- Who is affected, who must agree, and who decides if they disagree. -->
37
+
38
+ ## Dependencies
39
+ <!-- What must exist, or be done by someone else, first. -->
40
+
41
+ ## Success measure
42
+ <!-- The measure, its start value, its target and the date. -->
43
+
44
+ ## Out of scope
45
+ <!-- What this will not do, even though someone might expect it to. -->
46
+
47
+ ## Lane answers
48
+ <!-- The answers to the lane's own questions: bug report fields for a bug-fix,
49
+ the question and drop criteria for a discovery, and so on. -->
50
+
51
+ ## Open questions
52
+ <!-- Each question the PM left open, and each deferred note item. Do not answer them here. -->
53
+
54
+ ## Gathered notes used
55
+ <!-- For each note item shown to the PM: the pointer, its confidence, and the PM's answer
56
+ (confirmed, rejected or deferred). Write `none` if no notes existed. -->
57
+
58
+ ## Decisions
59
+ <!-- The PM's confirmed decisions, in the PM's words. A confirmed note item goes here with
60
+ its pointer. -->
61
+ ```
@@ -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-02T08:57:21Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-03T00:48:37Z"}
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-02T08:57:21Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-03T00:48:37Z"}
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
  /*