create-pmos 0.36.0 → 0.38.0

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 (71) hide show
  1. package/bin/create-pmos.js +22 -3
  2. package/package.json +1 -1
  3. package/payload/AGENTS.md +6 -0
  4. package/payload/KIT-VERSION +2 -2
  5. package/payload/okf/core/concepts/frontmatter-format.md +27 -61
  6. package/payload/okf/core/templates/decision-record-template.md +34 -0
  7. package/payload/okf/core/templates/index.md +7 -2
  8. package/payload/okf/core/templates/pm-brief-template.md +34 -0
  9. package/payload/okf/core/templates/project-plan-template.md +43 -0
  10. package/payload/okf/core/templates/report-weekly-template.md +35 -0
  11. package/payload/okf/core/templates/strategy-long-range-template.md +37 -0
  12. package/payload/okf/core/templates/strategy-one-pager-template.md +34 -0
  13. package/payload/planning/evals/eval-calibration.md +8 -0
  14. package/payload/planning/okrs/health-budgets.md +8 -0
  15. package/payload/scripts/anchor-resolve.js +56 -6
  16. package/payload/scripts/gate-initiative.sh +32 -0
  17. package/payload/scripts/okf-upgrade.js +232 -0
  18. package/payload/scripts/pmos-memory.js +375 -0
  19. package/payload/scripts/pmos-model.js +239 -0
  20. package/payload/scripts/pmos-skills.js +274 -0
  21. package/payload/scripts/workflow-state.js +178 -10
  22. package/payload/scripts/workflow-sweep.js +2 -1
  23. package/payload/state/discoveries.md +8 -0
  24. package/payload/state/index.md +7 -0
  25. package/payload/state/log.md +7 -0
  26. package/workspace/README.md +61 -204
  27. package/workspace/lib/board-cache.js +18 -8
  28. package/workspace/lib/console-state.js +2 -0
  29. package/workspace/lib/engine/anchor-resolve.js +56 -6
  30. package/workspace/lib/engine/pmos-memory.js +375 -0
  31. package/workspace/lib/engine/pmos-model.js +239 -0
  32. package/workspace/lib/engine/pmos-skills.js +274 -0
  33. package/workspace/lib/engine/workflow-state.js +178 -10
  34. package/workspace/lib/focus.js +5 -6
  35. package/workspace/lib/memory.js +54 -0
  36. package/workspace/lib/models.js +50 -0
  37. package/workspace/lib/preview-kind.js +12 -1
  38. package/workspace/lib/schedule-logic.js +31 -2
  39. package/workspace/lib/skill-store.js +5 -1
  40. package/workspace/lib/skills-registry.js +74 -0
  41. package/workspace/lib/sync.js +132 -0
  42. package/workspace/lib/tree-share.js +2 -0
  43. package/workspace/package-lock.json +2 -2
  44. package/workspace/package.json +3 -3
  45. package/workspace/public/pm/app.js +32 -2
  46. package/workspace/public/pm/data.js +160 -10
  47. package/workspace/public/pm/lifecycle.js +105 -0
  48. package/workspace/public/pm/markdown.js +10 -1
  49. package/workspace/public/pm/modes-live.js +371 -0
  50. package/workspace/public/pm/modes.js +7 -4
  51. package/workspace/public/pm/panel-detail.js +22 -2
  52. package/workspace/public/pm/panel-initiatives.js +57 -12
  53. package/workspace/public/pm/panel-newsess.js +25 -0
  54. package/workspace/public/pm/panel-repos.js +130 -4
  55. package/workspace/public/pm/panel-tabs.js +49 -2
  56. package/workspace/public/pm/state.js +11 -1
  57. package/workspace/public/pm/tab-doc.js +160 -26
  58. package/workspace/public/pm/tab-term.js +2 -0
  59. package/workspace/public/pm/tab-web.js +89 -0
  60. package/workspace/public/pm/tokens.css +4 -0
  61. package/workspace/public/pm/xlsx-read.js +117 -0
  62. package/workspace/server.js +690 -40
  63. package/workspace/skills/gather-requirements.skill +53 -0
  64. package/workspace/skills/memory-groom.skill +46 -0
  65. package/workspace/skills/report-biweekly.skill +53 -0
  66. package/workspace/skills/report-daily.skill +53 -0
  67. package/workspace/skills/report-monthly.skill +53 -0
  68. package/workspace/skills/report-special.skill +49 -0
  69. package/workspace/skills/report-weekly.skill +53 -0
  70. package/workspace/skills/skill-discover.skill +49 -0
  71. package/workspace/skills/skill-generate.skill +33 -0
@@ -94,7 +94,8 @@ function budgetsFor(root, L) {
94
94
  catch { value = { ok: false, text: '', path: path.relative(root, L.budgets) }; }
95
95
  _budCache.set(root, { stamp, value });
96
96
  }
97
- return _budCache.get(root).value;
97
+ // D67: the domain registry rides on the budgets object, read fresh (a small, PM-edited file)
98
+ return anchors.withDomains(_budCache.get(root).value, path.dirname(L.budgets), root);
98
99
  }
99
100
 
100
101
  /*
@@ -527,6 +528,21 @@ function surfaceRoute(v) {
527
528
  return 'malformed';
528
529
  }
529
530
 
531
+ // ── Repository profile (D68) ──────────────────────────────────────────────────────────────────
532
+ // A repository declares `pm-artifacts` (a PM's documents, strategy and specifications — no code) or
533
+ // `product-code` (PMOS-self and code repositories) in a one-line `.pmos-profile` file at its root.
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'];
538
+ function profileOf(root) {
539
+ try {
540
+ const line = fs.readFileSync(path.join(root, '.pmos-profile'), 'utf8')
541
+ .split('\n').map((l) => l.replace(/#.*$/, '').trim()).find(Boolean);
542
+ return line === 'pm-artifacts' ? 'pm-artifacts' : 'product-code';
543
+ } catch { return 'product-code'; }
544
+ }
545
+
530
546
  // ── Step model ─────────────────────────────────────────────────────────────────────────────────
531
547
  // status ∈ done | current | blocked | pending | na | unknown
532
548
  // `blocking` marks a step whose failure fails CI. O5/O6/O7 are deliberately NOT blocking: none can
@@ -539,7 +555,12 @@ function step(key, label, status, evidence, next, opts) {
539
555
  };
540
556
  }
541
557
 
542
- function computeWorkflow(id, opts) {
558
+ // D74 — `generated: {by: x, at: "2026-10-02T01:00:00Z"}` on one line (the only form PMOS writes). Returns the `at`, or ''.
559
+ function generatedAt(v) {
560
+ const m = /\bat:\s*["']?([^"',}\s]+)/.exec(String(v || ''));
561
+ return m ? m[1] : '';
562
+ }
563
+ function computeWorkflowDerived(id, opts) {
543
564
  const root = path.resolve((opts && opts.root) || process.cwd());
544
565
  const L = layout(root);
545
566
  const recRel = path.relative(root, path.join(L.initiatives, `${id}.md`));
@@ -548,9 +569,13 @@ function computeWorkflow(id, opts) {
548
569
  return { ok: false, id, error: `no initiative record at ${recRel} — an initiative must exist before its work` };
549
570
  }
550
571
  const fm = readFrontmatter(recAbs) || {};
551
- const lane = (fm.lane || 'feature').trim() || 'feature';
552
- const knownLane = ['feature', 'discovery', 'chore', 'incident', 'content', 'prototype'].includes(lane);
553
- const effLane = knownLane ? lane : 'feature'; // unknown lane = strictest, as the bash gate does
572
+ const profile = profileOf(root);
573
+ const pmProfile = profile === 'pm-artifacts';
574
+ const lane = (fm.lane || (pmProfile ? 'prd' : 'feature')).trim() || (pmProfile ? 'prd' : 'feature');
575
+ const knownLane = pmProfile ? PM_LANES.includes(lane)
576
+ : ['feature', 'discovery', 'chore', 'incident', 'content', 'prototype'].includes(lane);
577
+ // unknown lane = the profile's strictest (feature in product-code, a governed prd in pm-artifacts)
578
+ const effLane = knownLane ? lane : (pmProfile ? 'prd' : 'feature');
554
579
  const range = (opts && opts.range) || commitRange(root);
555
580
  // See ENFORCED_FROM: git decides, the frontmatter only breaks a tie git cannot see.
556
581
  //
@@ -575,7 +600,7 @@ function computeWorkflow(id, opts) {
575
600
  const legacy = recIdx === -1 ? true
576
601
  : /^\d{4}-\d{2}-\d{2}$/.test(recon) ? recon < ENFORCED_FROM
577
602
  : recAt ? new Date(recAt * 1000).toISOString().slice(0, 10) < ENFORCED_FROM
578
- : String(fm.timestamp || '').slice(0, 10) < ENFORCED_FROM; // fallback — see ENFORCED_FROM
603
+ : String(fm.timestamp || generatedAt(fm.generated) || '').slice(0, 10) < ENFORCED_FROM; // fallback — see ENFORCED_FROM; D74: OKF v0.2 `generated.at` stands in for `timestamp`
579
604
  /*
580
605
  * `legacy` answers the ENFORCEMENT question — should this PR fail? It is NOT the visibility
581
606
  * question. Filtering the board by it was a real bug: on `main`, every record is legacy, so the
@@ -673,6 +698,18 @@ function computeWorkflow(id, opts) {
673
698
  `only the PM adds a budget — use an existing id from ${budRel} or anchor to a KR`,
674
699
  { blocking: !legacy, rule: 'R1' }));
675
700
  }
701
+ } else if (r.kind === 'domain') {
702
+ // D67 — the domain's standing goal: resolved like a health budget, rejected like one when the
703
+ // id is not in the PM-edited registry.
704
+ const domRel = (budgetsFor(root, L).domains || {}).path || 'planning/okrs/domains.md';
705
+ if (r.status === 'resolved') {
706
+ steps.push(step('anchor', 'anchor', 'done', `domain:${r.domain} — ${r.domain_goal || 'standing goal not written'} (no OKR, D67)`));
707
+ } else {
708
+ steps.push(step('anchor', 'anchor', legacy ? 'unknown' : 'blocked',
709
+ `domain:${r.domain} is not a declared domain`,
710
+ `only the PM adds a domain — use an id from ${domRel}, or anchor to a KR or a health budget`,
711
+ { blocking: !legacy, rule: 'R1' }));
712
+ }
676
713
  } else if (r.status === 'resolved') {
677
714
  steps.push(step('anchor', 'anchor', 'done',
678
715
  `${anchors.canonical(r, (okrIndexFor(root) || {}).period) || anchor} — ${r.reason}`));
@@ -691,6 +728,11 @@ function computeWorkflow(id, opts) {
691
728
  }
692
729
  }
693
730
 
731
+ // ── D68: the pm-artifacts ladder replaces everything after the anchor ─────────────────────────
732
+ if (pmProfile) {
733
+ return pmLadder({ id, fm, lane, effLane, knownLane, legacy, steps, root, L, recRel, range, inChange, lastActivity });
734
+ }
735
+
694
736
  const prd = String(fm.prd_path || '').trim();
695
737
  const rubric = String(fm.rubric_path || '').trim();
696
738
  const prdIdx = prd ? firstAppearance(root, range, prd) : null;
@@ -1133,7 +1175,7 @@ function computeWorkflow(id, opts) {
1133
1175
  return {
1134
1176
  ok: true, id, lane, laneKnown: knownLane, legacy, in_change: inChange,
1135
1177
  last_activity: lastActivity, accepted: accepted.length > 0, stage, record: recRel,
1136
- title: fm.title || id, anchor: fm.parent_kr || null,
1178
+ title: fm.title || id, anchor: fm.parent_kr || null, profile: 'product-code',
1137
1179
  steps, blockers: blockers.map((s) => s.key), gate_failures: gateFailures.map((s) => s.rule || s.key),
1138
1180
  next: next ? { step: next.key, why: next.evidence, action: next.next || next.evidence } : null,
1139
1181
  range: range.ok ? { base: range.base, commits: range.shas.length, whole_history: !!range.wholeHistory } : { error: range.reason },
@@ -1141,6 +1183,122 @@ function computeWorkflow(id, opts) {
1141
1183
  };
1142
1184
  }
1143
1185
 
1186
+ /**
1187
+ * D68 — the pm-artifacts ladder: route → anchor → brief → draft → review → approved → handed off.
1188
+ *
1189
+ * Route and anchor are computed by the shared code above and keep their rules (anchoring holds in
1190
+ * every profile — D59/D67); route is re-stated here because its product-code form asks the
1191
+ * specimen/surface questions, which a PM document does not need. What follows is evidence-derived
1192
+ * like every other rung — nothing is declared done:
1193
+ * brief — the brief file (`brief_path`, or `prd_path`) exists
1194
+ * draft — the deliverable (`doc_path`) exists, and — governed only — did not appear before the brief
1195
+ * review — an evals.jsonl verdict for this id, or a named `reviewed_by:` (a rubric, an agent, a human)
1196
+ * approved — the PM's acceptance in acceptances.jsonl (D12, never automatic)
1197
+ * handoff — `handed_off:` names where it went; n/a for decision records, reports and chores
1198
+ * Track: `light` (guidance — only route and anchor can fail CI) or `governed` (brief and draft
1199
+ * existence and their order fail CI for a non-legacy record). Review, approval and handoff are never
1200
+ * CI-blocking in either track: none can be true before the PR exists.
1201
+ */
1202
+ function pmLadder({ id, fm, lane, effLane, knownLane, legacy, steps, root, L, recRel, range, inChange, lastActivity }) {
1203
+ const declared = String(fm.track || '').trim().toLowerCase();
1204
+ const track = declared === 'light' || declared === 'governed' ? declared
1205
+ : (PM_LIGHT_BY_DEFAULT.includes(effLane) ? 'light' : 'governed');
1206
+ const enforce = track === 'governed' && !legacy;
1207
+ const exists = (p) => !!p && fs.existsSync(path.join(root, p));
1208
+
1209
+ // route — the lane is a pm-artifacts lane
1210
+ steps[0] = knownLane
1211
+ ? step('route', 'route', 'done', `${lane} lane · ${track} track (pm-artifacts profile, D68)`)
1212
+ : step('route', 'route', 'done', `unknown lane '${lane}' — treated as a governed prd, the profile's strictest (D68)`,
1213
+ `set lane: to one of ${PM_LANES.join(', ')}`);
1214
+
1215
+ const briefPath = String(fm.brief_path || fm.prd_path || '').trim();
1216
+ const docPath = String(fm.doc_path || '').trim();
1217
+ const briefIdx = briefPath ? firstAppearance(root, range, briefPath) : null;
1218
+ const docIdx = docPath ? firstAppearance(root, range, docPath) : null;
1219
+
1220
+ if (exists(briefPath)) steps.push(step('brief', 'brief', 'done', `${briefPath} present`));
1221
+ else steps.push(step('brief', 'brief', enforce ? 'blocked' : 'pending',
1222
+ briefPath ? `brief_path '${briefPath}' does not exist` : 'no brief yet',
1223
+ `write a short brief (the question, the reader, the decision it informs) and set brief_path in ${recRel}`,
1224
+ { blocking: enforce, rule: 'P1' }));
1225
+
1226
+ if (exists(docPath)) {
1227
+ const inOrder = briefIdx == null || docIdx == null ? null : docIdx >= briefIdx;
1228
+ if (enforce && inOrder === false) {
1229
+ steps.push(step('draft', 'draft', 'blocked', `${docPath} appeared before its brief ${briefPath}`,
1230
+ 'a governed document is briefed before it is drafted — the brief must land first', { blocking: true, rule: 'P2' }));
1231
+ } else steps.push(step('draft', 'draft', 'done', `${docPath} present`));
1232
+ } else steps.push(step('draft', 'draft', enforce && docPath ? 'blocked' : 'pending',
1233
+ docPath ? `doc_path '${docPath}' does not exist` : 'no draft yet',
1234
+ `write the document and set doc_path in ${recRel}`, { blocking: enforce && !!docPath, rule: 'P2' }));
1235
+
1236
+ const evals = cachedJsonl(path.join(L.runsDir, 'evals.jsonl')).filter((e) => e && e.initiative_id === id);
1237
+ const reviewer = String(fm.reviewed_by || '').trim();
1238
+ if (evals.length) steps.push(step('review', 'review', 'done', `${evals[evals.length - 1].verdict || 'verdict'} recorded in evals.jsonl`));
1239
+ else if (reviewer) steps.push(step('review', 'review', 'done', `reviewed by ${reviewer}`));
1240
+ else steps.push(step('review', 'review', 'pending', 'not reviewed yet',
1241
+ 'name the reviewer in reviewed_by: (a person, an agent, or a rubric verdict in evals.jsonl)'));
1242
+
1243
+ const accepted = cachedJsonl(path.join(L.runsDir, 'acceptances.jsonl'))
1244
+ .filter((a) => a && a.initiative_id === id && a.accepted === true);
1245
+ steps.push(accepted.length
1246
+ ? 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) : ''}`)
1247
+ : step('approved', 'approved', 'pending', 'awaiting the PM\'s acceptance (D12)', 'the PM accepts the document — never automatic'));
1248
+
1249
+ if (PM_NO_HANDOFF.includes(effLane)) steps.push(step('handoff', 'handed off', 'na', `${effLane} — nothing to hand off`));
1250
+ else {
1251
+ const to = String(fm.handed_off || '').trim();
1252
+ steps.push(to ? step('handoff', 'handed off', 'done', `handed off — ${to}`)
1253
+ : step('handoff', 'handed off', 'pending', 'not handed off yet',
1254
+ 'record where it went in handed_off: (a tracker link, the team, or "accepted as the deliverable")'));
1255
+ }
1256
+
1257
+ const by = Object.fromEntries(steps.map((x) => [x.key, x]));
1258
+ let stage = 'intake';
1259
+ if (by.brief.status === 'done') stage = 'brief';
1260
+ if (by.draft.status === 'done') stage = 'draft';
1261
+ if (by.review.status === 'done') stage = 'review';
1262
+ if (accepted.length) stage = 'accepted';
1263
+ const blockers = steps.filter((x) => x.status === 'blocked');
1264
+ const gateFailures = blockers.filter((x) => x.blocking);
1265
+ const next = steps.find((x) => x.status === 'blocked') || steps.find((x) => x.status === 'pending' && x.next) || null;
1266
+ return {
1267
+ ok: true, id, lane, laneKnown: knownLane, legacy, in_change: inChange,
1268
+ last_activity: lastActivity, accepted: accepted.length > 0, stage, record: recRel,
1269
+ title: fm.title || id, anchor: fm.parent_kr || null, profile: 'pm-artifacts', track,
1270
+ steps, blockers: blockers.map((x) => x.key), gate_failures: gateFailures.map((x) => x.rule || x.key),
1271
+ next: next ? { step: next.key, why: next.evidence, action: next.next || next.evidence } : null,
1272
+ range: range.ok ? { base: range.base, commits: range.shas.length, whole_history: !!range.wholeHistory } : { error: range.reason },
1273
+ enforced_from: ENFORCED_FROM,
1274
+ };
1275
+ }
1276
+
1277
+ // ── Lifecycle (D70) — a stopped initiative is a ledger line, never a record field ───────────────
1278
+ // `lifecycle.jsonl` beside the other ledgers: {initiative_id, state, reason, pm, at}. The latest line
1279
+ // for an id wins. `active` reopens. The same reader exists in workspace/lib/lifecycle.js; a parity
1280
+ // fixture keeps the two in step. A lifecycle line is never an acceptance and never writes one.
1281
+ const STOPPED_STATES = ['closed', 'on-hold', 'cancelled'];
1282
+ function readLifecycle(root) {
1283
+ const L = layout(root);
1284
+ const latest = new Map();
1285
+ for (const r of cachedJsonl(path.join(L.runsDir, 'lifecycle.jsonl'))) {
1286
+ if (!r || typeof r.initiative_id !== 'string') continue;
1287
+ if (r.state !== 'active' && !STOPPED_STATES.includes(r.state)) continue;
1288
+ latest.set(r.initiative_id, { state: r.state, reason: r.reason || '', pm: r.pm || '', at: r.at || '' });
1289
+ }
1290
+ return latest;
1291
+ }
1292
+ function computeWorkflow(id, opts) {
1293
+ const w = computeWorkflowDerived(id, opts);
1294
+ if (!w.ok) return w;
1295
+ const lc = readLifecycle(path.resolve((opts && opts.root) || process.cwd())).get(id);
1296
+ if (!lc || lc.state === 'active') return w;
1297
+ // Stopped: leaves the board and stops gating. The ladder is kept so a reopen shows where it was.
1298
+ return { ...w, stopped: lc, blockers: [], gate_failures: [], next: null,
1299
+ steps: w.steps.map((x) => (x.status === 'blocked' ? { ...x, status: 'pending', blocking: false } : x)) };
1300
+ }
1301
+
1144
1302
  // ── Rendering ──────────────────────────────────────────────────────────────────────────────────
1145
1303
  const COLOR = process.stdout.isTTY && !process.env.NO_COLOR;
1146
1304
  const c = (code, s) => (COLOR ? `\x1b[${code}m${s}\x1b[0m` : s);
@@ -1192,7 +1350,15 @@ function renderBoard(rows, opts) {
1192
1350
  out.push(` ${c(1, 'PMOS workflow board')} · ${total} without a recorded acceptance · ${blocked ? c(31, `${blocked} blocked`) : c(32, 'none blocked')}${acceptedN ? c(90, ` · ${acceptedN} accepted`) : ''}`);
1193
1351
  out.push(` ${c(90, 'most recently worked on first · ✗ blocked · ● in this change')}`);
1194
1352
  out.push(` ${c(90, '─'.repeat(78))}`);
1195
- if (!shown.length) { out.push(` ${c(32, 'every initiative here has a recorded acceptance.')}`); out.push(''); return out.join('\n'); }
1353
+ const stoppedList = () => {
1354
+ const stopped = rows.filter((r) => r.ok && r.stopped);
1355
+ if (stopped.length) {
1356
+ out.push(` ${c(90, `stopped (${stopped.length}) — off the board, not gating:`)}`);
1357
+ for (const r of stopped) out.push(` ${c(90, `─ ${r.id} ${r.stopped.state}${r.stopped.at ? ' ' + String(r.stopped.at).slice(0, 10) : ''}${r.stopped.reason ? ' — ' + brief(r.stopped.reason, 60) : ''}`)}`);
1358
+ out.push('');
1359
+ }
1360
+ };
1361
+ if (!shown.length) { out.push(` ${c(32, 'every initiative here has a recorded acceptance.')}`); out.push(''); stoppedList(); return out.join('\n'); }
1196
1362
  const w1 = Math.min(34, Math.max(...shown.map((r) => r.id.length), 4));
1197
1363
  for (const r of shown) {
1198
1364
  if (!r.ok) { out.push(` ${c(31, '✗')} ${r.id.padEnd(w1)} ${c(31, r.error)}`); continue; }
@@ -1210,6 +1376,7 @@ function renderBoard(rows, opts) {
1210
1376
  // The two ranges, said out loud. The board reads the whole trunk so a delivered initiative does
1211
1377
  // not render as unstarted; the gate reads the PR so "spec before build" stays decidable. A rule
1212
1378
  // can therefore read done here and still be evaluated on the PR — intended, and not a discrepancy.
1379
+ stoppedList();
1213
1380
  out.push(` ${c(90, 'derived from the whole trunk history; the CI gate evaluates order within the PR only')}`);
1214
1381
  out.push(` ${c(90, `one initiative in detail: node ${opts.self} <id>`)}`);
1215
1382
  out.push('');
@@ -1232,7 +1399,7 @@ function renderBoard(rows, opts) {
1232
1399
  const BOARD_CAP = 12;
1233
1400
  function inFlight(rows, opts) {
1234
1401
  const cap = (opts && opts.cap) || BOARD_CAP;
1235
- const live = rows.filter((r) => !r.ok || !r.accepted);
1402
+ const live = rows.filter((r) => !r.ok || (!r.accepted && !r.stopped));
1236
1403
  const rank = (r) => (!r.ok ? 0 : r.blockers.length ? 1 : r.in_change ? 2 : 3);
1237
1404
  const sorted = [...live].sort((a, b) =>
1238
1405
  rank(a) - rank(b)
@@ -1285,6 +1452,7 @@ function main(argv) {
1285
1452
 
1286
1453
  // ── gate mode ──────────────────────────────────────────────────────────────────────────────
1287
1454
  if (!w.ok) { process.stdout.write(`::error::workflow-gate: ${w.error}\n`); return 1; }
1455
+ if (w.stopped) { process.stdout.write(`ℹ️ '${w.id}' is ${w.stopped.state} — stopped initiatives do not gate (D70).\n✅ workflow gate PASSED — '${w.id}' is ${w.stopped.state}.\n`); return 0; }
1288
1456
  const fails = w.steps.filter((s) => s.status === 'blocked' && s.blocking);
1289
1457
  if (fails.length) {
1290
1458
  for (const f of fails) {
@@ -1325,7 +1493,7 @@ function main(argv) {
1325
1493
  }
1326
1494
 
1327
1495
  module.exports = {
1328
- computeWorkflow, renderLadder, renderBoard, listInitiatives, layout, inFlight, BOARD_CAP,
1496
+ computeWorkflow, readLifecycle, STOPPED_STATES, renderLadder, renderBoard, listInitiatives, layout, inFlight, BOARD_CAP,
1329
1497
  parseLeadingFrontmatter, specimenRoute, surfaceRoute, priorsCitation, isBuildPath, commitRange,
1330
1498
  // the board's trunk range, exported so the suite can assert the split: the same record is legacy
1331
1499
  // under BOTH ranges, and `in this change` is false under the trunk one.
@@ -467,7 +467,8 @@ function budgetsFor(root, L) {
467
467
  catch { value = { ok: false, text: '', path: path.relative(root, L.budgets) }; }
468
468
  _bud.set(root, { stamp, value });
469
469
  }
470
- return _bud.get(root).value;
470
+ // D67: the domain registry rides on the budgets object, read fresh (a small, PM-edited file)
471
+ return anchors.withDomains(_bud.get(root).value, path.dirname(L.budgets), root);
471
472
  }
472
473
 
473
474
  /*
@@ -1,2 +1,10 @@
1
+ ---
2
+ type: Reference
3
+ title: "Discoveries and friction log"
4
+ description: "Append-only record of what runs discovered and where the process rubbed."
5
+ generated: {by: process:create-pmos, at: "2026-10-02T01:05:10Z"}
6
+ status: stable
7
+ ---
8
+
1
9
  # Discoveries & friction log (append-only; ISO dates; tag friction with #friction)
2
10
 
@@ -0,0 +1,7 @@
1
+ ---
2
+ okf_version: "0.2"
3
+ ---
4
+
5
+ # Knowledge bundle
6
+
7
+ Documents PMOS and its agents write in this repository (OKF v0.2, D73).
@@ -0,0 +1,7 @@
1
+ # Log
2
+
3
+ Newest first, ISO dates.
4
+
5
+ ## 2026-10-02
6
+
7
+ - Adopted OKF v0.2 (D73).