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.
@@ -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
  /*