create-pmos 0.34.0 → 0.36.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 (82) hide show
  1. package/bin/create-pmos.js +15 -3
  2. package/package.json +1 -1
  3. package/payload/KIT-VERSION +2 -2
  4. package/payload/okf/core/concepts/agent-harness.md +2 -2
  5. package/payload/okf/core/concepts/design-loop.md +5 -5
  6. package/payload/okf/core/concepts/eval-driven-pm.md +1 -1
  7. package/payload/okf/core/concepts/okr-anti-optimism.md +1 -1
  8. package/payload/okf/core/concepts/okr-tree.md +2 -2
  9. package/payload/okf/core/concepts/output-eval.md +2 -2
  10. package/payload/okf/core/concepts/sdlc-loop.md +1 -1
  11. package/payload/okf/core/concepts/self-refining-harness.md +3 -3
  12. package/payload/okf/core/templates/content-brief-template.md +1 -1
  13. package/payload/okf/core/templates/design-brief-template.md +1 -1
  14. package/payload/okf/core/templates/eval-rubric-template.md +1 -1
  15. package/payload/okf/core/templates/log.md +1 -1
  16. package/payload/okf/core/templates/prd-template.md +1 -1
  17. package/payload/okf/core/templates/spike-brief-template.md +1 -1
  18. package/payload/scripts/frontier.sh +6 -2
  19. package/payload/scripts/gate-initiative.sh +24 -3
  20. package/payload/scripts/metrics.py +20 -4
  21. package/payload/scripts/read-jsonl.js +53 -0
  22. package/payload/scripts/workflow-state.js +144 -35
  23. package/payload/scripts/workflow-sweep.js +39 -22
  24. package/payload/skills/SKILL-FORMAT.md +1 -1
  25. package/payload/skills/copywriting.skill +1 -1
  26. package/payload/skills/correction-signal-derive.skill +1 -1
  27. package/payload/skills/design-brief.skill +4 -4
  28. package/payload/skills/design-reconcile.skill +1 -1
  29. package/payload/skills/discovery.skill +3 -3
  30. package/payload/skills/eval-rubric.skill +4 -4
  31. package/payload/skills/evaluator.skill +1 -1
  32. package/payload/skills/explain-run.skill +1 -1
  33. package/payload/skills/explain-surface.skill +6 -6
  34. package/payload/skills/harness-refine.skill +1 -1
  35. package/payload/skills/ingest-repo.skill +6 -6
  36. package/payload/skills/okr.skill +2 -2
  37. package/payload/skills/pmos-router.skill +8 -8
  38. package/payload/skills/prd.skill +5 -5
  39. package/payload/skills/prototype-grill.skill +8 -8
  40. package/payload/skills/prototype.skill +7 -7
  41. package/payload/skills/skill-creator.skill +1 -1
  42. package/payload/skills/source-to-concept.skill +1 -1
  43. package/payload/skills/ste-term-map.md +69 -0
  44. package/payload/skills/strategic-brief.skill +4 -4
  45. package/payload/skills/technical-writing-ste.skill +176 -0
  46. package/payload/skills-native/copywriting/SKILL.md +1 -1
  47. package/payload/skills-native/correction-signal-derive/SKILL.md +1 -1
  48. package/payload/skills-native/design-brief/SKILL.md +4 -4
  49. package/payload/skills-native/design-reconcile/SKILL.md +1 -1
  50. package/payload/skills-native/discovery/SKILL.md +3 -3
  51. package/payload/skills-native/eval-rubric/SKILL.md +4 -4
  52. package/payload/skills-native/evaluator/SKILL.md +1 -1
  53. package/payload/skills-native/explain-run/SKILL.md +1 -1
  54. package/payload/skills-native/explain-surface/SKILL.md +6 -6
  55. package/payload/skills-native/harness-refine/SKILL.md +1 -1
  56. package/payload/skills-native/ingest-repo/SKILL.md +6 -6
  57. package/payload/skills-native/okr/SKILL.md +2 -2
  58. package/payload/skills-native/pmos-router/SKILL.md +8 -8
  59. package/payload/skills-native/prd/SKILL.md +5 -5
  60. package/payload/skills-native/prototype/SKILL.md +7 -7
  61. package/payload/skills-native/prototype-grill/SKILL.md +8 -8
  62. package/payload/skills-native/skill-creator/SKILL.md +1 -1
  63. package/payload/skills-native/source-to-concept/SKILL.md +1 -1
  64. package/payload/skills-native/strategic-brief/SKILL.md +4 -4
  65. package/payload/skills-native/technical-writing-ste/SKILL.md +178 -0
  66. package/workspace/bin/pmos.js +9 -3
  67. package/workspace/lib/engine/read-jsonl.js +53 -0
  68. package/workspace/lib/engine/workflow-state.js +144 -35
  69. package/workspace/lib/file-mode-parse.js +19 -4
  70. package/workspace/lib/tree-share.js +69 -1
  71. package/workspace/package-lock.json +2 -2
  72. package/workspace/package.json +1 -1
  73. package/workspace/public/pm/board-read.js +38 -0
  74. package/workspace/public/pm/data.js +102 -1
  75. package/workspace/public/pm/modes.js +50 -28
  76. package/workspace/public/pm/panel-detail.js +2 -17
  77. package/workspace/public/pm/panel-initiatives.js +3 -20
  78. package/workspace/public/pm/panel-newsess.js +14 -1
  79. package/workspace/public/pm/panel-tabs.js +53 -14
  80. package/workspace/public/pm/tab-term.js +30 -2
  81. package/workspace/server.js +95 -70
  82. package/workspace/test-run.js +51 -9
@@ -58,18 +58,43 @@ const delivery = require('./delivery-index');
58
58
  // The OKR documents and the budget registry are read once per root, not once per initiative: the
59
59
  // board derives ~100 ladders in a single pass and re-reading the same three files each time is the
60
60
  // same waste the shared commit-range exists to avoid.
61
- const _okrCache = new Map();
62
- const _budCache = new Map();
61
+ //
62
+ // KEYED BY WHAT THE FILES ARE, never by where they live alone. The workspace server requires this
63
+ // module in-process and holds it for days, so a root-only key kept serving the ladder yesterday's
64
+ // anchor index after the PM fixed the OKR document — the OKR panel (which reads fresh) and the
65
+ // anchor rung disagreed about one field until a restart, the exact two-readers defect
66
+ // anchor-resolve was extracted to end. The stamp is mtime+size over the documents and their
67
+ // directories (the directory catches a file added or removed); the stats cost microseconds against
68
+ // the parse they guard. delivery-index keys its cache on the trunk head sha for the same reason.
69
+ // One stated limit: an `okrs/` directory born at a location no prior read has seen is picked up on
70
+ // the next change to a watched path or the next process, not instantly.
71
+ const _okrCache = new Map(); // root -> { stamp, files, index }
72
+ const _budCache = new Map(); // root -> { stamp, value }
73
+ function statStamp(p) {
74
+ try { const s = fs.statSync(p); return `${s.mtimeMs}:${s.size}`; } catch { return 'absent'; }
75
+ }
76
+ function okrStamp(files) {
77
+ const dirs = [...new Set(files.map((f) => path.dirname(f)))];
78
+ return files.concat(dirs).map((p) => `${p}=${statStamp(p)}`).join('|');
79
+ }
63
80
  function okrIndexFor(root) {
64
- if (!_okrCache.has(root)) _okrCache.set(root, anchors.parseOkrIndex(root));
65
- return _okrCache.get(root);
81
+ const hit = _okrCache.get(root);
82
+ if (hit && hit.files.length && okrStamp(hit.files) === hit.stamp) return hit.index;
83
+ const files = anchors.listOkrFiles(root);
84
+ const entry = { stamp: okrStamp(files), files, index: anchors.parseOkrIndex(root) };
85
+ _okrCache.set(root, entry);
86
+ return entry.index;
66
87
  }
67
88
  function budgetsFor(root, L) {
68
- if (!_budCache.has(root)) {
69
- try { _budCache.set(root, { ok: true, text: fs.readFileSync(L.budgets, 'utf8'), path: path.relative(root, L.budgets) }); }
70
- catch { _budCache.set(root, { ok: false, text: '', path: path.relative(root, L.budgets) }); }
89
+ const stamp = statStamp(L.budgets);
90
+ const hit = _budCache.get(root);
91
+ if (!hit || hit.stamp !== stamp) {
92
+ let value;
93
+ try { value = { ok: true, text: fs.readFileSync(L.budgets, 'utf8'), path: path.relative(root, L.budgets) }; }
94
+ catch { value = { ok: false, text: '', path: path.relative(root, L.budgets) }; }
95
+ _budCache.set(root, { stamp, value });
71
96
  }
72
- return _budCache.get(root);
97
+ return _budCache.get(root).value;
73
98
  }
74
99
 
75
100
  /*
@@ -105,18 +130,31 @@ function isBuildPath(p, lane) {
105
130
  }
106
131
 
107
132
  // ── Frontmatter ────────────────────────────────────────────────────────────────────────────────
108
- // The LEADING block only — body text must never be able to fake a declaration. Mirrors
109
- // workspace/lib/file-mode-parse.js and kit/scripts/metrics.py, plus the surrounding-quote strip the
110
- // bash gate does, so all four readers agree on what a record declared.
133
+ // The LEADING block only — body text must never be able to fake a declaration. The closing ---
134
+ // fence is TERMINAL, never skipped: the fence used to be stepped over, so a key-shaped FIRST body
135
+ // line was absorbed into the block and could fake a declaration at the fence boundary (the one
136
+ // shape the gamma prose-then-keys fixture never exercised). Fenceless files keep parsing from line
137
+ // 1 and stop at the first non-key line — the kit's smoke fixtures carry no fences and must keep
138
+ // parsing. Mirrors workspace/lib/file-mode-parse.js, kit/scripts/metrics.py and the two bash gates'
139
+ // readers — first value wins, one pair of surrounding double quotes stripped — so all the readers
140
+ // agree on what a record declared (kit/fixtures/test_parsers.py + workspace/fixture-parity.test.mjs
141
+ // hold the agreement).
111
142
  function parseLeadingFrontmatter(text) {
112
143
  const fm = {};
144
+ let fenceOpen = false;
113
145
  for (const line of String(text == null ? '' : text).split('\n')) {
146
+ if (line.startsWith('---')) {
147
+ // Only the FIRST ---, before any key, opens the block; every later --- ends it.
148
+ if (fenceOpen || Object.keys(fm).length) break;
149
+ fenceOpen = true;
150
+ continue;
151
+ }
114
152
  const m = line.match(/^([a-z_]+):\s*(.*?)\s*$/);
115
153
  if (m) {
116
154
  if (!(m[1] in fm)) fm[m[1]] = m[2].replace(/^"(.*)"$/s, '$1');
117
155
  continue;
118
156
  }
119
- if (line.trim() && !line.startsWith('---')) break;
157
+ if (line.trim()) break;
120
158
  }
121
159
  return fm;
122
160
  }
@@ -136,23 +174,32 @@ function readFrontmatter(file) {
136
174
  * changed return type, because every existing call site wants the rows and only the sweep wants the
137
175
  * damage. Changing the shape would have made ~six callers handle a case none of them can act on.
138
176
  */
177
+ // The implementation lives in scripts/read-jsonl.js now — this contract became the shared one after
178
+ // six hand-rolled readers each re-decided the torn-line question (and two got it wrong). The thin
179
+ // wrapper keeps this file's call sites and the out-of-band defects channel unchanged.
180
+ const jsonl = require('./read-jsonl');
139
181
  function readJsonl(file, defects) {
140
- let text;
141
- try { text = fs.readFileSync(file, 'utf8'); } catch { return []; }
142
- const out = [];
143
- const lines = text.split('\n');
144
- for (let i = 0; i < lines.length; i++) {
145
- const t = lines[i].trim();
146
- if (!t) continue;
147
- try { out.push(JSON.parse(t)); }
148
- catch (e) {
149
- // Dropped, never guessed — and now said out loud to anyone who asked to hear it.
150
- if (Array.isArray(defects)) {
151
- defects.push({ file, line: i + 1, reason: String((e && e.message) || 'not valid JSON').slice(0, 120), text: t.slice(0, 120) });
152
- }
153
- }
154
- }
155
- return out;
182
+ const r = jsonl.readJsonl(file);
183
+ // Dropped, never guessed — and said out loud to anyone who asked to hear it.
184
+ if (Array.isArray(defects)) defects.push(...r.defects);
185
+ return r.rows;
186
+ }
187
+
188
+ // The ledgers are read once per VERSION OF THE FILE, not once per initiative — the same per-root
189
+ // rule the OKR cache above states, applied to the other four per-root inputs it was skipping. The
190
+ // board filters ~665 KB of parsed rows once per ladder; re-reading and re-parsing the files each
191
+ // time was ~145 MB of work per board. The stamp is mtime+size for the same reason as the OKR
192
+ // cache's: the workspace server holds this module in-process, and a run or acceptance appended
193
+ // between requests must be visible on the next derivation, not after a restart. Callers filter the
194
+ // shared rows and never mutate them.
195
+ const _ledgerCache = new Map(); // absolute path -> { stamp, rows }
196
+ function cachedJsonl(file) {
197
+ const stamp = statStamp(file);
198
+ const hit = _ledgerCache.get(file);
199
+ if (hit && hit.stamp === stamp) return hit.rows;
200
+ const rows = readJsonl(file);
201
+ _ledgerCache.set(file, { stamp, rows });
202
+ return rows;
156
203
  }
157
204
 
158
205
  // ── Repo layout — pmos-self and the kit disagree on where state lives, so detect rather than assume
@@ -308,6 +355,39 @@ function firstAppearance(root, range, rel) {
308
355
  return range.index.has(sha) ? range.index.get(sha) : -1;
309
356
  }
310
357
 
358
+ /*
359
+ * When was each path last touched, answered from the range's OWN single pass. A `git log -1
360
+ * --format=%ct -- <paths>` per initiative was one subprocess per board row — ~85% of the board's
361
+ * runtime spent re-reading history buildRange had already captured (see the single-pass note
362
+ * above). Built lazily, memoised on the range object, so every ladder on the board shares one map.
363
+ */
364
+ function touchIndex(range) {
365
+ if (!range._touch) {
366
+ const m = new Map();
367
+ for (let i = 0; i < range.shas.length; i++) {
368
+ const t = range.dates[i];
369
+ if (!Number.isFinite(t)) continue;
370
+ for (const f of (range.changed[i] || [])) {
371
+ const prev = m.get(f);
372
+ if (prev === undefined || t > prev) m.set(f, t);
373
+ }
374
+ }
375
+ range._touch = m;
376
+ }
377
+ return range._touch;
378
+ }
379
+
380
+ // The set of paths git tracks, one `ls-files` shared across the board via the range object. Used
381
+ // only where a range is EMPTY — see the note at the chgIdx computation. `null` = git could not
382
+ // answer, which callers must treat as "fall back", never as "nothing is tracked".
383
+ function trackedIn(root, range) {
384
+ if (range._tracked === undefined) {
385
+ const out = git(root, ['ls-files', '-z']);
386
+ range._tracked = out == null ? null : new Set(out.split('\0').filter(Boolean));
387
+ }
388
+ return range._tracked;
389
+ }
390
+
311
391
  /*
312
392
  * The earliest position in the range at which this initiative's implementation was touched, plus a
313
393
  * count. A docs-only PR has no build commits, which makes every "before the build" rule vacuous —
@@ -510,10 +590,38 @@ function computeWorkflow(id, opts) {
510
590
  // that is true of everything, which makes the marker noise — so the board passes the GATE's range
511
591
  // separately and the marker is answered from that, or dropped when there is no such range.
512
592
  const changeRange = (opts && opts.changeRange) || (range.wholeHistory ? null : range);
513
- const chgIdx = changeRange ? firstAppearance(root, changeRange, recRel) : null;
593
+ // A ZERO-COMMIT range contains nothing, so firstAppearance's per-path fallback — a full-history
594
+ // `git log --diff-filter=A`, once per initiative on any full clone whose base..HEAD is empty
595
+ // (HEAD level with main) — can only conclude what one shared `ls-files` already says: a tracked
596
+ // record predates the range (-1), an on-disk-untracked one is uncommitted (Infinity). The gate's
597
+ // range — non-empty — keeps the fallback, which is what tells "added before the range" from
598
+ // "never committed" for O1/O2.
599
+ const emptyChange = !!changeRange && changeRange.ok && changeRange.shas.length === 0;
600
+ const tracked = emptyChange ? trackedIn(root, changeRange) : null;
601
+ const chgIdx = !changeRange ? null
602
+ : tracked ? (tracked.has(recRel) ? -1 : fs.existsSync(recAbs) ? Infinity : null)
603
+ : firstAppearance(root, changeRange, recRel);
514
604
  const inChange = !!changeRange && chgIdx !== -1 && chgIdx !== null && chgIdx !== undefined;
515
605
  const lastActivity = (() => {
516
606
  const paths = [recRel, fm.prd_path, fm.rubric_path].filter(Boolean);
607
+ // The board supplies the trunk range, whose single pass already holds every commit's paths and
608
+ // dates — so the answer is a map lookup, not a subprocess. The trunk alone would demote
609
+ // branch-fresh work (`git log -1` reads from HEAD, which sees branch-local commits the trunk
610
+ // range excludes), so the gate's base..HEAD range is overlaid, max date per path. The
611
+ // subprocess survives only where no whole-history range was supplied — the single-ladder call.
612
+ // One deliberate difference from the per-path subprocess: a true merge lists no files in the
613
+ // single pass, so a merge that merely reconciles histories no longer counts as activity on the
614
+ // paths it carried — the date it stamped was the sync's, not the work's.
615
+ if (opts && opts.range && range.ok && range.wholeHistory) {
616
+ const trunk = touchIndex(range);
617
+ const branch = changeRange && changeRange.ok ? touchIndex(changeRange) : null;
618
+ let best = null;
619
+ for (const p of paths) {
620
+ const t = Math.max(trunk.get(p) || 0, (branch && branch.get(p)) || 0);
621
+ if (t > 0 && (best === null || t > best)) best = t;
622
+ }
623
+ return best;
624
+ }
517
625
  const out = git(root, ['log', '-1', '--format=%ct', '--', ...paths]);
518
626
  const t = out && parseInt(out.trim(), 10);
519
627
  return Number.isFinite(t) ? t : null;
@@ -589,8 +697,9 @@ function computeWorkflow(id, opts) {
589
697
  const build = buildCommits(root, range, effLane, {
590
698
  id,
591
699
  // Where this initiative's own record first appears in the range — the earliest commit that could
592
- // possibly be its build. See the note in buildCommits' attribution loop.
593
- recordIdx: firstAppearance(root, range, recRel),
700
+ // possibly be its build. Already computed above as `recIdx`, from identical arguments.
701
+ // See the note in buildCommits' attribution loop.
702
+ recordIdx: recIdx,
594
703
  deliveryIndex: opts && opts.deliveryIndex,
595
704
  declaredDelivery: fm.delivered_by,
596
705
  });
@@ -748,7 +857,7 @@ function computeWorkflow(id, opts) {
748
857
  }
749
858
  }
750
859
 
751
- const runs = readJsonl(path.join(L.runsDir, L.runsFile)).filter((r) => r && r.initiative_id === id);
860
+ const runs = cachedJsonl(path.join(L.runsDir, L.runsFile)).filter((r) => r && r.initiative_id === id);
752
861
  const runsRel = path.relative(root, path.join(L.runsDir, L.runsFile));
753
862
 
754
863
  // ── priors ─ O7: the episodic read is CITED, and the citation is checked ─────────────────────
@@ -874,7 +983,7 @@ function computeWorkflow(id, opts) {
874
983
  steps.push(step('quality', 'quality', 'unknown', 'CI decides on the PR — this engine never claims a test result it did not run'));
875
984
 
876
985
  // ── review ─ the Review Gate's verdict, where the lane has a rubric ──────────────────────────
877
- const evals = readJsonl(path.join(L.runsDir, 'evals.jsonl')).filter((e) => e && e.initiative_id === id);
986
+ const evals = cachedJsonl(path.join(L.runsDir, 'evals.jsonl')).filter((e) => e && e.initiative_id === id);
878
987
  {
879
988
  /* The rung reads the RUBRIC, not the lane.
880
989
  *
@@ -946,7 +1055,7 @@ function computeWorkflow(id, opts) {
946
1055
  }
947
1056
 
948
1057
  // ── accept ─ the human PM, final and non-delegable (D12) ─────────────────────────────────────
949
- const accepted = readJsonl(path.join(L.runsDir, 'acceptances.jsonl'))
1058
+ const accepted = cachedJsonl(path.join(L.runsDir, 'acceptances.jsonl'))
950
1059
  .filter((a) => a && a.initiative_id === id && a.accepted === true);
951
1060
  {
952
1061
  const blockers = steps.filter((s) => s.status === 'blocked').map((s) => s.key);
@@ -974,7 +1083,7 @@ function computeWorkflow(id, opts) {
974
1083
  {
975
1084
  const metric = String(fm.outcome_metric || '').trim();
976
1085
  const horizon = String(fm.outcome_horizon || '').trim();
977
- const checks = readJsonl(path.join(L.runsDir, 'outcome_checks.jsonl'))
1086
+ const checks = cachedJsonl(path.join(L.runsDir, 'outcome_checks.jsonl'))
978
1087
  .filter((c) => c && c.initiative_id === id && ['validated', 'refuted', 'inconclusive'].includes(c.disposition));
979
1088
  const latest = checks.length ? checks[checks.length - 1] : null;
980
1089
  const today = new Date().toISOString().slice(0, 10);
@@ -364,16 +364,16 @@ function sweep(opts) {
364
364
  }
365
365
  }
366
366
 
367
- // a declared delivery that does not check out
368
- {
369
- const dd = delivery.declaredDelivery(root, fm.delivered_by);
370
- if (dd.declared && !dd.ok && !dd.unknown) {
371
- findings.push(finding('unverifiable-delivery', {
372
- id, file: recRel,
373
- what: dd.reason,
374
- evidence: `delivered_by: ${String(fm.delivered_by).slice(0, 100)}`,
375
- }));
376
- }
367
+ // a declared delivery that does not check out — read ONCE, the owed gate below reuses it: a
368
+ // second declaredDelivery call on the same inputs paid its git subprocesses twice per record
369
+ // for the same answer.
370
+ const dd = delivery.declaredDelivery(root, fm.delivered_by);
371
+ if (dd.declared && !dd.ok && !dd.unknown) {
372
+ findings.push(finding('unverifiable-delivery', {
373
+ id, file: recRel,
374
+ what: dd.reason,
375
+ evidence: `delivered_by: ${String(fm.delivered_by).slice(0, 100)}`,
376
+ }));
377
377
  }
378
378
 
379
379
  // ── owed ────────────────────────────────────────────────────────────────────────────────────
@@ -381,7 +381,7 @@ function sweep(opts) {
381
381
  // the reason is written on each finding rather than only in this file's header — the person
382
382
  // reading the output is the one who needs to know why the tool stopped.
383
383
  const signal = del.ok ? delivery.attribution(del, id) : 'unknown';
384
- const declaredOk = delivery.declaredDelivery(root, fm.delivered_by).ok === true;
384
+ const declaredOk = dd.ok === true;
385
385
  const delivered = del.ok && (!!signal || declaredOk);
386
386
  const runs = runsBy.get(id) || [];
387
387
 
@@ -436,19 +436,38 @@ function sweep(opts) {
436
436
  };
437
437
  }
438
438
 
439
- // Cached the same way the engine caches them — the sweep derives over every record in one pass.
440
- const _okr = new Map();
439
+ // Cached the same way the engine caches them — the sweep derives over every record in one pass —
440
+ // and keyed the same way too: mtime+size over the documents (plus their directories, so a file
441
+ // added or removed re-lists), never the root alone. The sweep is CLI-today, but a copy of the
442
+ // engine's cache with a weaker key is exactly the two-places-one-rule drift this file exists to
443
+ // sweep for.
444
+ const _okr = new Map(); // root -> { stamp, files, index }
445
+ const _bud = new Map(); // root -> { stamp, value }
446
+ function statStamp(p) {
447
+ try { const s = fs.statSync(p); return `${s.mtimeMs}:${s.size}`; } catch { return 'absent'; }
448
+ }
449
+ function okrStamp(files) {
450
+ const dirs = [...new Set(files.map((f) => path.dirname(f)))];
451
+ return files.concat(dirs).map((p) => `${p}=${statStamp(p)}`).join('|');
452
+ }
441
453
  function engineOkrIndex(root) {
442
- if (!_okr.has(root)) _okr.set(root, anchors.parseOkrIndex(root));
443
- return _okr.get(root);
454
+ const hit = _okr.get(root);
455
+ if (hit && hit.files.length && okrStamp(hit.files) === hit.stamp) return hit.index;
456
+ const files = anchors.listOkrFiles(root);
457
+ const entry = { stamp: okrStamp(files), files, index: anchors.parseOkrIndex(root) };
458
+ _okr.set(root, entry);
459
+ return entry.index;
444
460
  }
445
- const _bud = new Map();
446
461
  function budgetsFor(root, L) {
447
- if (!_bud.has(root)) {
448
- try { _bud.set(root, { ok: true, text: fs.readFileSync(L.budgets, 'utf8'), path: path.relative(root, L.budgets) }); }
449
- catch { _bud.set(root, { ok: false, text: '', path: path.relative(root, L.budgets) }); }
462
+ const stamp = statStamp(L.budgets);
463
+ const hit = _bud.get(root);
464
+ if (!hit || hit.stamp !== stamp) {
465
+ let value;
466
+ try { value = { ok: true, text: fs.readFileSync(L.budgets, 'utf8'), path: path.relative(root, L.budgets) }; }
467
+ catch { value = { ok: false, text: '', path: path.relative(root, L.budgets) }; }
468
+ _bud.set(root, { stamp, value });
450
469
  }
451
- return _bud.get(root);
470
+ return _bud.get(root).value;
452
471
  }
453
472
 
454
473
  /*
@@ -740,8 +759,6 @@ function main(argv) {
740
759
 
741
760
  if (json) {
742
761
  process.stdout.write(JSON.stringify({ ...res, repairs }, null, 2) + '\n');
743
- } else if (!fix || owedOnly || defectsOnly) {
744
- process.stdout.write(render(res, { owedOnly, defectsOnly, all }));
745
762
  } else {
746
763
  process.stdout.write(render(res, { owedOnly, defectsOnly, all }));
747
764
  }
@@ -64,7 +64,7 @@ A skill exists to **wring determinism out of a stochastic system**: it makes the
64
64
 
65
65
  ### Per-step completion criteria
66
66
 
67
- Every procedural step ends on a **checkable done-condition** — the agent must be able to tell done from not-done. Where it matters, make the criterion **exhaustive**, not vague: *"every living mirror of the rule accounted for"*, not *"produce a change list"*. A fuzzy criterion invites **premature completion** — the agent stops when it feels done, not when it is. The [semantic-pass rule](/build-skills/evaluator.skill) ("enumerate the live-doc set, then read each for meaning — a grep is evidence, never done") is the exemplar this generalizes: it exists because a vague "sweep it" criterion let real stale mirrors survive five "clean" passes.
67
+ Every procedural step ends on a **checkable done-condition** — the agent must be able to tell done from not-done. Where it matters, make the criterion **exhaustive**, not vague: *"every living mirror of the rule accounted for"*, not *"produce a change list"*. A fuzzy criterion invites **premature completion** — the agent stops when it feels done, not when it is. The [semantic-pass rule](/skills/evaluator.skill) ("enumerate the live-doc set, then read each for meaning — a grep is evidence, never done") is the exemplar this generalizes: it exists because a vague "sweep it" criterion let real stale mirrors survive five "clean" passes.
68
68
 
69
69
  ### Pruning — hunt these named failure modes
70
70
 
@@ -16,7 +16,7 @@ The first `/skills/domains/` skill: how copy work runs through PMOS's **content
16
16
  — never the `no-initiative` hatch — but it iterates fast, so the lane contracts on a **brief**,
17
17
  and a rubric is *recommended, not required* (a required one recreates fake-rubric pressure).
18
18
 
19
- > **Format:** three-level progressive disclosure ([SKILL-FORMAT](/skills/core/SKILL-FORMAT.md)).
19
+ > **Format:** three-level progressive disclosure ([SKILL-FORMAT](/skills/SKILL-FORMAT.md)).
20
20
 
21
21
  ## When to use vs. not
22
22
 
@@ -13,7 +13,7 @@ self-report (`agent_runs.human_corrections`) — pre-commit conversational steer
13
13
  git-invisible and stays the self-report's lane; the **delta** between the two is the point
14
14
  (KR-B consumes it).
15
15
 
16
- > **Format:** three-level progressive disclosure ([SKILL-FORMAT](/skills/core/SKILL-FORMAT.md)).
16
+ > **Format:** three-level progressive disclosure ([SKILL-FORMAT](/skills/SKILL-FORMAT.md)).
17
17
  > Level 1 frontmatter above is the trigger; this Level 2 body is the full procedure.
18
18
 
19
19
  ## Non-negotiable stance
@@ -12,10 +12,10 @@ scope: read:okf, write:planning/design
12
12
 
13
13
  This skill produces a design brief instance for one initiative. It is **leg 1** of the
14
14
  [design loop](/okf/core/concepts/design-loop.md): the context handoff from Claude Code to
15
- claude.ai/design. The brief is to design what the [PRD](/skills/core/prd.skill) is to build — the shared
15
+ claude.ai/design. The brief is to design what the [PRD](/skills/prd.skill) is to build — the shared
16
16
  definition of *what to design* and the hard constraints, agreed before any design or build starts.
17
17
 
18
- > **Format note:** three-level progressive disclosure ([SKILL-FORMAT](/skills/core/SKILL-FORMAT.md)).
18
+ > **Format note:** three-level progressive disclosure ([SKILL-FORMAT](/skills/SKILL-FORMAT.md)).
19
19
  > Level 1 above is the trigger; this body is Level 2; rare variants go to Level 3 sub-files by name.
20
20
 
21
21
  ## When to use vs. not
@@ -34,11 +34,11 @@ definition of *what to design* and the hard constraints, agreed before any desig
34
34
  [design-reference grilling rubric](/planning/evals/design-reference-eval-rubric.md). Anything beyond
35
35
  what the PRD decided is still flagged back.
36
36
  - **Do not use** for the reconciliation gate — classifying a returned design against real capability is the
37
- [design-reconcile skill](/skills/core/design-reconcile.skill) (leg 3).
37
+ [design-reconcile skill](/skills/design-reconcile.skill) (leg 3).
38
38
 
39
39
  ## Procedure
40
40
 
41
- 0. **Grill before you brief** (the [prd skill](/skills/core/prd.skill)'s step-0 discipline, applied
41
+ 0. **Grill before you brief** (the [prd skill](/skills/prd.skill)'s step-0 discipline, applied
42
42
  to design intent): where the PM's aesthetic or structural intent is ambiguous, ask **one question
43
43
  at a time with a recommended answer**, stop at unambiguous — no cap; a batch questionnaire is not
44
44
  compliant. Look up what the environment answers (the capability ledger, the design system, the
@@ -17,7 +17,7 @@ realizes the reconciliation rule captured in the product's capability ledger (PM
17
17
  [watermelon-flag](/okf/core/concepts/watermelon-flag.md) defense applied to UI — it catches a design that
18
18
  *looks* functional but has no backend, before it becomes hollow, green-on-the-surface code.
19
19
 
20
- > **Format note:** three-level progressive disclosure ([SKILL-FORMAT](/skills/core/SKILL-FORMAT.md)).
20
+ > **Format note:** three-level progressive disclosure ([SKILL-FORMAT](/skills/SKILL-FORMAT.md)).
21
21
  > Level 1 above is the trigger; this body is Level 2.
22
22
 
23
23
  ## Non-negotiable stance
@@ -17,7 +17,7 @@ and a timebox**, not requirements, and closes with a mandatory **disposition** i
17
17
  verdict. It resolves the stress-test finding that pre-certainty work either got blocked, got a
18
18
  fake rubric, or escaped the record entirely.
19
19
 
20
- > **Format:** three-level progressive disclosure ([SKILL-FORMAT](/skills/core/SKILL-FORMAT.md)).
20
+ > **Format:** three-level progressive disclosure ([SKILL-FORMAT](/skills/SKILL-FORMAT.md)).
21
21
  > Level 1 above is the trigger; this body is Level 2.
22
22
 
23
23
  ## When to use vs. not
@@ -25,7 +25,7 @@ fake rubric, or escaped the record entirely.
25
25
  - **Use** when the deliverable is an answer: "is X feasible?", "which approach?", "what should the
26
26
  PRD even say?" — and you could not write honest done-criteria today.
27
27
  - **Do not use** when you *can* write done-criteria — that is a `feature` initiative
28
- ([prd skill](/skills/core/prd.skill)). A spike that is secretly a small feature corrupts the lane.
28
+ ([prd skill](/skills/prd.skill)). A spike that is secretly a small feature corrupts the lane.
29
29
  - **Do not use** for urgent production fixes — that is the `incident` lane.
30
30
  - A spike is still **anchored work** (D21): no anchor — a parent KR, or a declared health budget
31
31
  `health:<id>` (D59) — no spike.
@@ -38,7 +38,7 @@ fake rubric, or escaped the record entirely.
38
38
  - Tool form: `create_initiative({id, title, parent_kr, type: 'discovery'})`. The argument keeps
39
39
  its name. Read it as "record the equivalent in file-mode state" (AGENTS.md).
40
40
  1b. **Existing surface? Understand it first.** If answering the question honestly requires reading
41
- code that already exists, run an [explain-surface](/build-skills/explain-surface.skill) pass on
41
+ code that already exists, run an [explain-surface](/skills/explain-surface.skill) pass on
42
42
  that region — in a fresh context, **before** writing the brief. A spike aimed at a system the PM
43
43
  does not understand produces a learning they cannot act on. Greenfield → skip.
44
44
  2. **Write the spike brief** from
@@ -12,11 +12,11 @@ scope: read:okf, write:planning/evals
12
12
 
13
13
  This skill produces an eval rubric instance for one initiative. In eval-driven PM the rubric **is the
14
14
  spec made measurable** ([eval-driven-pm](/okf/core/concepts/eval-driven-pm.md)): it is authored
15
- *before* the build, alongside the [PRD](/skills/core/prd.skill), and it defines how output will pass
15
+ *before* the build, alongside the [PRD](/skills/prd.skill), and it defines how output will pass
16
16
  through the [three-tier gate](/okf/core/concepts/output-eval.md).
17
17
 
18
18
  > **Format note:** three-level progressive disclosure
19
- > ([SKILL-FORMAT](/skills/core/SKILL-FORMAT.md)). Level 1 frontmatter above is the trigger and
19
+ > ([SKILL-FORMAT](/skills/SKILL-FORMAT.md)). Level 1 frontmatter above is the trigger and
20
20
  > carries only `name` + `description`. Note: the *rubric instances* this skill produces carry their
21
21
  > own frontmatter (including `eval_type`, below) — that is the rubric's frontmatter, not this skill
22
22
  > file's.
@@ -79,7 +79,7 @@ Before any work begins, the generator and evaluator **agree on done-criteria**
79
79
  contract stage of the [SDLC loop](/okf/core/concepts/sdlc-loop.md). A rubric is incomplete without
80
80
  one.
81
81
 
82
- **Reach the criteria by grilling, not assumption** (the [prd skill](/skills/core/prd.skill)'s step-0
82
+ **Reach the criteria by grilling, not assumption** (the [prd skill](/skills/prd.skill)'s step-0
83
83
  discipline applies here too): where the PM's intent is ambiguous, ask **one question at a time with a
84
84
  recommended answer**, stop at unambiguous — no cap, and a batch questionnaire is not compliant. If a
85
85
  fact can be found by exploring the environment, look it up rather than asking — the decisions are the
@@ -129,7 +129,7 @@ construction; no per-contract justification needed beyond naming the doc set.
129
129
 
130
130
  **Dimensions must be evidence-citable.** The Review Gate scores each dimension with a verbatim
131
131
  quote or precise pointer from the artifact as evidence (an unevidenced dimension is `unverified`,
132
- scored ≤ 0.5 — see [evaluator.skill](/build-skills/evaluator.skill)). Write every dimension as an
132
+ scored ≤ 0.5 — see [evaluator.skill](/skills/evaluator.skill)). Write every dimension as an
133
133
  **observable outcome a quote can prove** — "the dry-run verdict names its exemplars" — never an
134
134
  impression ("the verdict feels thorough"). If you cannot imagine the evidence quote for a
135
135
  dimension, rewrite the dimension
@@ -12,7 +12,7 @@ the **interactive agent runtime** — any capable coding agent; heavy judgment b
12
12
  an Edge Function ([D09](/DECISIONS.md)/[D34](/DECISIONS.md), amended 2026-07-23). PMOS-self runs it in
13
13
  Claude Code, but the runtime is not Claude-specific.
14
14
 
15
- > **Format:** three-level progressive disclosure ([SKILL-FORMAT](/skills/core/SKILL-FORMAT.md)). Level 1
15
+ > **Format:** three-level progressive disclosure ([SKILL-FORMAT](/skills/SKILL-FORMAT.md)). Level 1
16
16
  > frontmatter above is the trigger. This Level 2 body is the full procedure.
17
17
 
18
18
  ## Non-negotiable stance
@@ -21,7 +21,7 @@ accrues **cognitive debt** and makes the non-delegable Acceptance Gate ([D12](/D
21
21
  *formally* human — the PM as their own [watermelon](/okf/core/concepts/watermelon-flag.md)
22
22
  ([skills-and-understanding](/planning/research/skills-and-understanding-research.md)).
23
23
 
24
- > **Format:** three-level progressive disclosure ([SKILL-FORMAT](/skills/core/SKILL-FORMAT.md)).
24
+ > **Format:** three-level progressive disclosure ([SKILL-FORMAT](/skills/SKILL-FORMAT.md)).
25
25
  > Level 1 frontmatter above is the trigger; this Level 2 body is the full procedure.
26
26
 
27
27
  ## Non-negotiable stance
@@ -17,13 +17,13 @@ and it costs more there. A misread at acceptance costs one rejected run; a misre
17
17
  inherited by the contract, the build, the rubric and the evaluator, and **every gate passes over it**.
18
18
  A [watermelon](/okf/core/concepts/watermelon-flag.md) planted at the root.
19
19
 
20
- The gap is sharpest in the [prd skill](/skills/core/prd.skill)'s own grilling rule — *"if a fact can
20
+ The gap is sharpest in the [prd skill](/skills/prd.skill)'s own grilling rule — *"if a fact can
21
21
  be found by exploring the environment, look it up rather than asking."* That rule is right, and its
22
22
  side effect is that **the agent acquires the understanding and the PM does not.**
23
23
 
24
- > **Format:** three-level progressive disclosure ([SKILL-FORMAT](/skills/core/SKILL-FORMAT.md)).
24
+ > **Format:** three-level progressive disclosure ([SKILL-FORMAT](/skills/SKILL-FORMAT.md)).
25
25
  > L1 above is the trigger; this L2 body is the procedure.
26
- > **Not [`ingest-repo`](/build-skills/ingest-repo.skill).** That builds an *agent-facing*,
26
+ > **Not [`ingest-repo`](/skills/ingest-repo.skill).** That builds an *agent-facing*,
27
27
  > whole-repo, one-time OKF reference layer — a capability ledger is a lookup table, not a teaching
28
28
  > artifact. This is PM-facing, one region, per-initiative, and pedagogically ordered. Read whatever
29
29
  > layer `ingest-repo` produced as substrate; **treat the code as ground truth.**
@@ -74,8 +74,8 @@ explainer output decayed to *never* by the second run.
74
74
 
75
75
  | Subject shape | Organise by | Reuse |
76
76
  |---|---|---|
77
- | A **user-facing surface** | the surface: views, states, controls, current copy | [design-reconcile](/skills/core/design-reconcile.skill)'s capability ledger §A–D — every control **backed vs. watermelon** — and the product's design system ([D38](/DECISIONS.md), D60(c)) |
78
- | A **subsystem / flow** | execution and logical flow, never file order | the [explain-run](/build-skills/explain-run.skill) convention |
77
+ | A **user-facing surface** | the surface: views, states, controls, current copy | [design-reconcile](/skills/design-reconcile.skill)'s capability ledger §A–D — every control **backed vs. watermelon** — and the product's design system ([D38](/DECISIONS.md), D60(c)) |
78
+ | A **subsystem / flow** | execution and logical flow, never file order | the [explain-run](/skills/explain-run.skill) convention |
79
79
  | A **data model** | entities, their invariants, what a migration must preserve | `ingest-repo`'s per-table `Reference` docs |
80
80
  | A **gate / policy surface** | the decision it makes, its fail-closed behaviour, its escape hatches | the gate's scripts + the decisions locking it |
81
81
  | A **harness surface** (skill, protocol) | trigger → procedure → stance, and what depends on it | SKILL-FORMAT's three levels |
@@ -94,7 +94,7 @@ explainer output decayed to *never* by the second run.
94
94
  - **Constraints** — what will **bite a change here**: coupling, invariants, gates, migrations,
95
95
  backfill, fail-closed behaviour. The cheapest possible place to learn these.
96
96
  - **Open questions** — what you **could not determine from the code**. Genuine unknowns, never
97
- rhetorical ones. This section feeds the [prd skill](/skills/core/prd.skill)'s grilling directly
97
+ rhetorical ones. This section feeds the [prd skill](/skills/prd.skill)'s grilling directly
98
98
  and is the honest alternative to a confident guess.
99
99
  - **Quiz** — five questions (rules below), then a `---` divider, then **Answers**.
100
100
  4. **Force counterfactuals in the BODY, not only in the quiz.** As you walk the region, repeatedly
@@ -88,7 +88,7 @@ field's "agents improving their own tools" loop while staying at the **L3 ceilin
88
88
  - The sibling detect→draft→PR loop: [okf-author](/build-skills/okf-author.skill),
89
89
  [D42](/okf/products/pmos/adr/d42-okf-author-on-demand.md).
90
90
  - The evidence surfaces: recurring friction themes and self-reported context pressure, read from
91
- file-mode state ([discoveries.md](/discoveries.md) and `runs.jsonl`; the hosted
91
+ file-mode state ([discoveries.md](/state/discoveries.md) and `runs.jsonl`; the hosted
92
92
  `v_friction_recurrence` / `v_context_budget` views went with the backend in D66) plus
93
93
  [eval-calibration.md](/planning/evals/eval-calibration.md).
94
94
  - The cadence it serves: [D23](/okf/products/pmos/adr/d23-quarterly-cadence.md) (harness-assumption
@@ -17,7 +17,7 @@ the stack, assuming wrong, producing work the PM must heavily correct — the no
17
17
  ingest-repo goes **code → what the product *is***. It emits a **draft** OKF
18
18
  reference layer + a repo registration and then **stops for PM approval**.
19
19
 
20
- > **Format:** three-level progressive disclosure ([SKILL-FORMAT](/skills/core/SKILL-FORMAT.md)). L1
20
+ > **Format:** three-level progressive disclosure ([SKILL-FORMAT](/skills/SKILL-FORMAT.md)). L1
21
21
  > frontmatter above is the trigger; this L2 body is the full procedure.
22
22
  > **Where it runs:** in **the interactive agent runtime** (any capable coding agent; PMOS-self uses
23
23
  > Claude Code), in the tenant's own conversation, read-only — never a server-side scanner
@@ -33,7 +33,7 @@ reference layer + a repo registration and then **stops for PM approval**.
33
33
  var *names* (`.env.example`, config keys). **Secret-scan the repo content AND git history** (real
34
34
  repos leak keys in committed code/history); if a secret value is found, **never** place it in a doc,
35
35
  a log, or the registry — note only that a leak exists and where, by path, so the PM can rotate it.
36
- - **Default-to-refute on capability** (the [design-reconcile](/skills/core/design-reconcile.skill)
36
+ - **Default-to-refute on capability** (the [design-reconcile](/skills/design-reconcile.skill)
37
37
  stance applied to code): a control/route is **watermelon** until a concrete backend read/write path
38
38
  is found in the code. Unbacked controls land in **§D**, not invented as capability.
39
39
  - **Everything is a DRAFT ([D37](/okf/products/pmos/adr/d37-tenant-okf-db-native.md)/[D12](/okf/products/pmos/adr/d12-three-tier-eval-gate.md)).**
@@ -65,7 +65,7 @@ reference layer + a repo registration and then **stops for PM approval**.
65
65
  **no** value; record only "possible secret at `<path>`" for the PM.
66
66
  4. **Emit the reference layer** — via `save_okf_doc(..., status:'draft', product_slug:<slug>)`,
67
67
  namespaced `products/<slug>/...`, **each body carrying situating context + the exact repo path(s)**
68
- it was derived from (the [source-to-concept](/skills/core/source-to-concept.skill) rule + the
68
+ it was derived from (the [source-to-concept](/skills/source-to-concept.skill) rule + the
69
69
  analyze-product-infra cited-line bar, applied to code). Minimum six kinds:
70
70
 
71
71
  | Doc | OKF `type` | id (under `products/<slug>/`) | derived from |
@@ -78,7 +78,7 @@ reference layer + a repo registration and then **stops for PM approval**.
78
78
  | ADR seed(s) | `ADR` | `adr/<slug>-<n>-<topic>` | README / ADRs / commit history |
79
79
 
80
80
  5. **Bootstrap the per-tenant capability ledger** (the artifact
81
- [design-reconcile](/skills/core/design-reconcile.skill) consumes, [D50](/okf/products/pmos/adr/d50-design-loop-product-scoped.md)
81
+ [design-reconcile](/skills/design-reconcile.skill) consumes, [D50](/okf/products/pmos/adr/d50-design-loop-product-scoped.md)
82
82
  pattern — generic method, per-tenant instance). Classify **every** user-facing control/route into
83
83
  exactly one bucket; each §A/§B entry **names the concrete read/write path** in the code:
84
84
  - **§A — read-backed:** maps to a real backend read (query/endpoint) — cite it.
@@ -111,8 +111,8 @@ reference layer + a repo registration and then **stops for PM approval**.
111
111
  approve them in PR review — the file-mode form of draft→approved. (D37's database-native tenant OKF
112
112
  and its `save_okf_doc` tool were retired by [D66](/DECISIONS.md).)
113
113
  - The ledger method: [design-loop concept](/okf/core/concepts/design-loop.md),
114
- [design-reconcile](/skills/core/design-reconcile.skill), [watermelon-flag](/okf/core/concepts/watermelon-flag.md).
115
- - Extraction discipline: [source-to-concept](/skills/core/source-to-concept.skill).
114
+ [design-reconcile](/skills/design-reconcile.skill), [watermelon-flag](/okf/core/concepts/watermelon-flag.md).
115
+ - Extraction discipline: [source-to-concept](/skills/source-to-concept.skill).
116
116
  - Scan location: [D51](/okf/products/pmos/adr/d51-scan-location.md). There is no longer a central
117
117
  registry to register into — the hosted `product_repos` table and `register_product_repo` went with
118
118
  the backend (D66); the product repo that carries the kit IS the registration.
@@ -15,14 +15,14 @@ measurable key results, structured as a tree that initiatives and agent runs lad
15
15
  operationalizes the [okr-tree](/okf/core/concepts/okr-tree.md) concept.
16
16
 
17
17
  > **Format note:** three-level progressive disclosure
18
- > ([SKILL-FORMAT](/skills/core/SKILL-FORMAT.md)). Level 1 above is the trigger; this body is the
18
+ > ([SKILL-FORMAT](/skills/SKILL-FORMAT.md)). Level 1 above is the trigger; this body is the
19
19
  > procedure; rare variants go to Level 3 sub-files.
20
20
 
21
21
  ## When to use vs. not
22
22
 
23
23
  - **Use** to set strategy for a planning period, or to re-anchor an initiative that has drifted from
24
24
  its parent KR.
25
- - **Do not use** to spec a single piece of work — that is the [PRD skill](/skills/core/prd.skill).
25
+ - **Do not use** to spec a single piece of work — that is the [PRD skill](/skills/prd.skill).
26
26
  OKRs sit one level above initiatives.
27
27
 
28
28
  ## Procedure