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 +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/scripts/lane-defs.js +226 -0
- package/payload/scripts/workflow-state.js +108 -24
- package/payload/scripts/workflow-sweep.js +155 -3
- package/payload/state/discoveries.md +1 -1
- package/workspace/lib/audit.js +54 -0
- package/workspace/lib/engine/lane-defs.js +226 -0
- package/workspace/lib/engine/workflow-state.js +108 -24
- package/workspace/lib/engine/workflow-sweep.js +934 -0
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +2 -2
- package/workspace/public/pm/data.js +87 -24
- package/workspace/public/pm/panel-detail.js +12 -2
- package/workspace/public/pm/panel-repos.js +54 -0
- package/workspace/public/pm/tab-task.js +4 -1
- package/workspace/server.js +48 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.38.
|
|
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": {
|
package/payload/KIT-VERSION
CHANGED
|
@@ -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-
|
|
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-
|
|
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
|
-
|
|
536
|
-
|
|
537
|
-
const
|
|
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
|
|
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 ?
|
|
582
|
-
:
|
|
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
|
-
: (
|
|
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 ${
|
|
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 (
|
|
1227
|
-
else steps.push(step('brief',
|
|
1228
|
-
|
|
1229
|
-
|
|
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 (
|
|
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',
|
|
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',
|
|
1238
|
-
} else steps.push(step('draft',
|
|
1239
|
-
docPath ? `doc_path '${docPath}' does not exist` :
|
|
1240
|
-
`write the
|
|
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 (
|
|
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
|
-
|
|
1252
|
-
|
|
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 (
|
|
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
|
/*
|