create-pmos 0.38.4 → 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 +67 -24
- package/workspace/public/pm/panel-detail.js +12 -2
- package/workspace/public/pm/panel-repos.js +36 -5
- package/workspace/public/pm/tab-task.js +4 -1
- package/workspace/server.js +22 -1
|
@@ -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
|
/*
|