create-pmos 0.134.0 → 0.136.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 (211) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +2 -2
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/state/discoveries.md +1 -1
  6. package/payload/state/log.md +1 -1
  7. package/workspace/bin/notes-replay.js +235 -0
  8. package/workspace/c3-briefing-test.js +130 -0
  9. package/workspace/c3-foundation-test.js +159 -0
  10. package/workspace/c3-ib-items-test.js +105 -0
  11. package/workspace/c3-ib-more-test.js +120 -0
  12. package/workspace/c3-ib-triage-backlog-test.js +119 -0
  13. package/workspace/c3-ini-harness.js +66 -0
  14. package/workspace/c3-initiatives-test.js +140 -0
  15. package/workspace/c3-one-home-test.js +14 -1
  16. package/workspace/c3-reports-test.js +57 -18
  17. package/workspace/c3-settings-test.js +139 -0
  18. package/workspace/c3-work-test.js +42 -10
  19. package/workspace/fixtures/one-home-baseline.json +1 -9
  20. package/workspace/guide/briefing-cards.md +15 -3
  21. package/workspace/guide/briefing.md +12 -8
  22. package/workspace/guide/calendar-popup.md +2 -2
  23. package/workspace/guide/console-calendar.md +39 -10
  24. package/workspace/guide/index.md +1 -0
  25. package/workspace/guide/initiative-boards.md +65 -0
  26. package/workspace/guide/jira.md +3 -1
  27. package/workspace/guide/layout.md +2 -0
  28. package/workspace/guide/my-work.md +5 -3
  29. package/workspace/guide/notes.md +16 -1
  30. package/workspace/guide/reports-dashboards.md +29 -5
  31. package/workspace/guide/settings.md +68 -0
  32. package/workspace/guide/welcome.md +10 -10
  33. package/workspace/lib/calendar-cli.js +3 -2
  34. package/workspace/lib/calendar-layers.js +21 -10
  35. package/workspace/lib/calendar-tidy.js +230 -0
  36. package/workspace/lib/engine/boards.js +542 -0
  37. package/workspace/lib/engine/breakdown-ops.js +180 -0
  38. package/workspace/lib/engine/calendar-import.js +84 -0
  39. package/workspace/lib/engine/calendar-model.js +185 -0
  40. package/workspace/lib/engine/card-state.js +92 -0
  41. package/workspace/lib/engine/derived/derive.js +92 -0
  42. package/workspace/lib/engine/derived/plan.js +3 -0
  43. package/workspace/lib/engine/derived/trust.js +3 -0
  44. package/workspace/lib/engine/doc-health.js +142 -0
  45. package/workspace/lib/engine/doc-sheet.js +75 -0
  46. package/workspace/lib/engine/intake.js +78 -0
  47. package/workspace/lib/engine/outbox.js +86 -0
  48. package/workspace/lib/engine/plan-forecast.js +342 -0
  49. package/workspace/lib/engine/promises.js +29 -0
  50. package/workspace/lib/engine/records/index.js +114 -0
  51. package/workspace/lib/engine/releases.js +96 -0
  52. package/workspace/lib/engine/replies.js +99 -0
  53. package/workspace/lib/engine/reply-contract/reply-contract.js +372 -0
  54. package/workspace/lib/engine/report-families.js +175 -0
  55. package/workspace/lib/engine/roadmap-ops.js +150 -0
  56. package/workspace/lib/engine/sessions-list.js +27 -0
  57. package/workspace/lib/engine/signal-rules.js +95 -0
  58. package/workspace/lib/engine/signals/cards.js +57 -0
  59. package/workspace/lib/engine/signals/detectors.js +370 -0
  60. package/workspace/lib/engine/signals/facts-cli.js +8 -0
  61. package/workspace/lib/engine/signals/facts.js +197 -0
  62. package/workspace/lib/engine/signals/index.js +33 -0
  63. package/workspace/lib/engine/signals/kinds.js +110 -0
  64. package/workspace/lib/engine/signals/layout-paths.js +44 -0
  65. package/workspace/lib/engine/signals/options.js +24 -0
  66. package/workspace/lib/engine/signals/score.js +72 -0
  67. package/workspace/lib/engine/signals/select.js +47 -0
  68. package/workspace/lib/engine/step-planner.js +65 -0
  69. package/workspace/lib/engine/trust/claim-rules.json +93 -0
  70. package/workspace/lib/engine/trust/import-calendar.js +21 -0
  71. package/workspace/lib/engine/trust/import-guard.js +36 -0
  72. package/workspace/lib/engine/trust/ledger.js +53 -0
  73. package/workspace/lib/engine/trust/outbox.js +45 -0
  74. package/workspace/lib/engine/trust/plan.js +5 -0
  75. package/workspace/lib/engine/trust/trust.js +21 -0
  76. package/workspace/lib/engine/truth-table.js +57 -0
  77. package/workspace/lib/engine/workspace-lib.js +24 -0
  78. package/workspace/lib/factory-join.js +1 -1
  79. package/workspace/lib/facts-async.js +4 -4
  80. package/workspace/lib/headless.js +95 -0
  81. package/workspace/lib/import-overlay.js +2 -1
  82. package/workspace/lib/jira-status.js +30 -3
  83. package/workspace/lib/notes-capture.js +9 -1
  84. package/workspace/lib/notes-final.js +38 -13
  85. package/workspace/lib/notes-replay.js +125 -0
  86. package/workspace/lib/notes-screen.js +180 -0
  87. package/workspace/lib/notes-whisper.js +10 -3
  88. package/workspace/lib/outbox-store.js +10 -6
  89. package/workspace/lib/pmos-verbs.js +3 -1
  90. package/workspace/lib/proactive-cli.js +7 -0
  91. package/workspace/lib/proactive.js +15 -2
  92. package/workspace/lib/records-dir.js +29 -0
  93. package/workspace/lib/repo-script.js +36 -0
  94. package/workspace/lib/report-dashboards.js +5 -2
  95. package/workspace/lib/report-frame.js +122 -0
  96. package/workspace/lib/report-schedules.js +113 -0
  97. package/workspace/lib/route-modules.js +9 -2
  98. package/workspace/lib/routes-boards.js +734 -0
  99. package/workspace/lib/routes-brief-page.js +13 -4
  100. package/workspace/lib/routes-briefing.js +22 -11
  101. package/workspace/lib/routes-c3-init.js +9 -8
  102. package/workspace/lib/routes-c3-life.js +2 -1
  103. package/workspace/lib/routes-c3-reports.js +99 -15
  104. package/workspace/lib/routes-c3-strategy.js +7 -5
  105. package/workspace/lib/routes-c3-timeline.js +3 -2
  106. package/workspace/lib/routes-calendar-import.js +3 -2
  107. package/workspace/lib/routes-calendar-views.js +9 -4
  108. package/workspace/lib/routes-doc-health.js +2 -1
  109. package/workspace/lib/routes-doc-sheet.js +2 -1
  110. package/workspace/lib/routes-forecast.js +2 -1
  111. package/workspace/lib/routes-hooks.js +3 -2
  112. package/workspace/lib/routes-initiatives.js +2 -1
  113. package/workspace/lib/routes-intake.js +2 -1
  114. package/workspace/lib/routes-notes.js +55 -9
  115. package/workspace/lib/routes-outbox.js +23 -3
  116. package/workspace/lib/routes-pm-items.js +19 -6
  117. package/workspace/lib/routes-records.js +10 -7
  118. package/workspace/lib/routes-releases.js +2 -1
  119. package/workspace/lib/routes-report-facts.js +4 -3
  120. package/workspace/lib/routes-report-families.js +2 -1
  121. package/workspace/lib/routes-roadmap-ops.js +17 -14
  122. package/workspace/lib/routes-runs.js +6 -5
  123. package/workspace/lib/routes-settings.js +51 -0
  124. package/workspace/lib/routes-signals.js +3 -2
  125. package/workspace/lib/routes-stage-tools.js +220 -0
  126. package/workspace/lib/run-answers.js +2 -1
  127. package/workspace/lib/settings-store.js +226 -0
  128. package/workspace/lib/stage-tools.js +208 -0
  129. package/workspace/lib/vendor/scripts/signals/layout-paths.js +2 -1
  130. package/workspace/lib/vendor/scripts/workspace-lib.js +24 -0
  131. package/workspace/lib/work-evidence.js +73 -0
  132. package/workspace/package-lock.json +2 -2
  133. package/workspace/package.json +1 -1
  134. package/workspace/public/pm/briefing-deck.js +63 -17
  135. package/workspace/public/pm/briefing-model.js +36 -7
  136. package/workspace/public/pm/c3-ambient.js +100 -0
  137. package/workspace/public/pm/c3-brief-needs.js +26 -6
  138. package/workspace/public/pm/c3-brief-pulse.js +12 -4
  139. package/workspace/public/pm/c3-brief-today.js +56 -21
  140. package/workspace/public/pm/c3-brief.js +2 -1
  141. package/workspace/public/pm/c3-calendar.js +161 -42
  142. package/workspace/public/pm/c3-calpop.js +5 -5
  143. package/workspace/public/pm/c3-demo-boards.js +649 -0
  144. package/workspace/public/pm/c3-demo-brief-page.js +1 -1
  145. package/workspace/public/pm/c3-demo-cal4.js +3 -2
  146. package/workspace/public/pm/c3-demo-cal5.js +52 -0
  147. package/workspace/public/pm/c3-demo-data.js +17 -27
  148. package/workspace/public/pm/c3-demo-rep3.js +46 -5
  149. package/workspace/public/pm/c3-demo.js +2 -1
  150. package/workspace/public/pm/c3-dock.js +1 -1
  151. package/workspace/public/pm/c3-ib-docs.js +80 -0
  152. package/workspace/public/pm/c3-ib-initiatives.js +134 -0
  153. package/workspace/public/pm/c3-ib-items.js +201 -0
  154. package/workspace/public/pm/c3-ib-releases.js +51 -0
  155. package/workspace/public/pm/c3-ib-sprints.js +55 -0
  156. package/workspace/public/pm/c3-ib-triage.js +178 -0
  157. package/workspace/public/pm/c3-ii-tabs.js +240 -0
  158. package/workspace/public/pm/c3-ini-records.js +197 -0
  159. package/workspace/public/pm/c3-ini-timeline.js +278 -0
  160. package/workspace/public/pm/c3-ini-ui.js +229 -0
  161. package/workspace/public/pm/c3-ini-work.js +139 -0
  162. package/workspace/public/pm/c3-initiatives.css +218 -0
  163. package/workspace/public/pm/c3-initiatives.js +741 -0
  164. package/workspace/public/pm/c3-plan.js +2 -2
  165. package/workspace/public/pm/c3-reports.css +47 -47
  166. package/workspace/public/pm/c3-reports.js +223 -185
  167. package/workspace/public/pm/c3-settings.css +74 -0
  168. package/workspace/public/pm/c3-settings.js +703 -0
  169. package/workspace/public/pm/c3-shell.js +102 -36
  170. package/workspace/public/pm/c3-terminal-icons.js +2 -2
  171. package/workspace/public/pm/c3-terminal.js +2 -2
  172. package/workspace/public/pm/calendar-views.js +1 -1
  173. package/workspace/public/pm/components.css +319 -213
  174. package/workspace/public/pm/console3.css +99 -23
  175. package/workspace/public/pm/dest-notes.js +26 -4
  176. package/workspace/public/pm/notes-screen-capture.js +165 -0
  177. package/workspace/public/pm/notes-screen-view.js +98 -0
  178. package/workspace/public/pm/panel-trees.js +5 -2
  179. package/workspace/public/pm/pm-work-view.js +233 -146
  180. package/workspace/public/pm/shell-bell.js +3 -2
  181. package/workspace/public/pm/shell-recording.js +29 -8
  182. package/workspace/public/pm/tokens.css +2 -2
  183. package/workspace/public/pm/view-notes.js +46 -9
  184. package/workspace/public/pm/welcome.js +197 -89
  185. package/workspace/public/welcome.html +6 -4
  186. package/workspace/server.js +105 -6
  187. package/workspace/skills/day-check.skill +1 -1
  188. package/workspace/skills/jira-sync.skill +6 -4
  189. package/workspace/skills/morning-brief.skill +1 -1
  190. package/workspace/test-files.json +12 -3
  191. package/workspace/test-lists/c3-ib-items.json +9 -0
  192. package/workspace/test-lists/c3-ib-more.json +9 -0
  193. package/workspace/test-lists/c3-ib-triage-backlog.json +9 -0
  194. package/workspace/test-lists/c3-initiative-boards.json +6 -0
  195. package/workspace/test-lists/calendar-tidy-up.json +6 -0
  196. package/workspace/test-lists/console3-briefing.json +9 -0
  197. package/workspace/test-lists/console3-foundation.json +9 -0
  198. package/workspace/test-lists/console3-reports.json +3 -3
  199. package/workspace/test-lists/meeting-notes.json +10 -4
  200. package/workspace/test-lists/workspace-headless-jobs.json +5 -0
  201. package/workspace/test-lists/workspace-package-script-closure.json +6 -0
  202. package/workspace/lib/vendor/workspace/lib/actor.js +0 -91
  203. package/workspace/lib/vendor/workspace/lib/board-cache.js +0 -156
  204. package/workspace/lib/vendor/workspace/lib/console-state.js +0 -295
  205. package/workspace/lib/vendor/workspace/lib/factory-join.js +0 -158
  206. package/workspace/lib/vendor/workspace/lib/git-info.js +0 -306
  207. package/workspace/lib/vendor/workspace/lib/plan-items.js +0 -922
  208. package/workspace/lib/vendor/workspace/lib/plan-tree.js +0 -419
  209. package/workspace/lib/vendor/workspace/lib/review-gate.js +0 -39
  210. package/workspace/lib/vendor/workspace/lib/sink.js +0 -321
  211. package/workspace/lib/vendor/workspace/lib/state-root.js +0 -587
@@ -1,419 +0,0 @@
1
- 'use strict';
2
- /* plan-tree.js — the stages an initiative's breakdown hangs under, and the layout the Breakdown pane draws.
3
- *
4
- * pm-os-absorb Run 1 (PRD R4, R5, R6, R18, R19). Pure apart from reading the initiative record and the
5
- * repository's profile: the caller passes the folded ledger (lib/plan-items.js) and the ladder
6
- * (lib/console-state.js `ladder()`), and this module groups them. Nothing here writes, and nothing is cached
7
- * (D33): every answer is derived from what the caller read on this request.
8
- *
9
- * WHY THE STAGES ARE NOT A LIST OF THEIR OWN. A stage header groups the steps `scripts/workflow-state.js`
10
- * derives for the initiative's lane and profile (R4). The step keys come from the engine (lane-defs
11
- * RUNG_RULES), and the suite asserts that STAGE_GROUPS partitions them exactly, so a rung added to the
12
- * engine without a stage here fails a test instead of disappearing from the tree.
13
- *
14
- * The owed row of a stage is the first step of its group that is still open — not done, n/a or unknown —
15
- * and it is DERIVED: it ticks itself when the ladder sees its evidence, and nobody can edit it.
16
- */
17
- const fs = require('fs');
18
- const path = require('path');
19
- const consoleState = require('./console-state');
20
- const factoryJoin = require('./factory-join.js');
21
-
22
- const STAGE_GROUPS = Object.freeze({
23
- 'product-code': [
24
- { id: 'intake', label: 'Intake', steps: ['route', 'anchor', 'prototype', 'understand'] },
25
- { id: 'spec', label: 'Spec', steps: ['spec', 'priors'] },
26
- { id: 'contract', label: 'Contract', steps: ['contract'] },
27
- { id: 'build', label: 'Build', steps: ['build', 'quality'] },
28
- { id: 'evaluate', label: 'Evaluate', steps: ['review', 'explain', 'accept', 'outcome'] },
29
- ],
30
- 'pm-artifacts': [
31
- { id: 'intake', label: 'Intake', steps: ['route', 'anchor'] },
32
- { id: 'brief', label: null, steps: ['brief'] }, // the lane's brief_label, capitalised: "Problem brief"
33
- { id: 'draft', label: null, steps: ['draft'] }, // the lane's draft_label: "PRD", "Change specification"
34
- { id: 'evaluate', label: 'Evaluate', steps: ['review', 'approved'] },
35
- { id: 'handoff', label: 'Handoff', steps: ['handoff'] }, // dropped for a lane whose rungs have no handoff
36
- ],
37
- });
38
- // The noun an owed row names. `brief` and `draft` read the lane's own label at run time.
39
- const STEP_NOUN = Object.freeze({
40
- route: 'routing', anchor: 'anchor', prototype: 'prototype', understand: 'explainer', spec: 'spec', priors: 'priors',
41
- contract: 'contract', build: 'build', quality: 'checks', review: 'review', explain: 'run explainer', accept: 'acceptance',
42
- outcome: 'outcome check', brief: '<brief_label>', draft: '<draft_label>', approved: 'approval', handoff: 'handoff',
43
- });
44
- // Who owes the step. Everything not named here is the agent's.
45
- const OWED_ACTOR = Object.freeze({ accept: 'you', approved: 'you', outcome: 'you', handoff: 'you' });
46
- const OPEN = (s) => !['done', 'na', 'unknown'].includes(s);
47
- const DONE_WINDOW_MS = 14 * 86400000;
48
- // An initiative id is a record file name. Shared with lib/plan-items.js, which re-exports it.
49
- const INIT_RE = /^[A-Za-z0-9][\w.-]{0,119}$/;
50
- const STATE_LABEL = { todo: 'Todo', doing: 'Doing', review: 'Review', done: 'Done' };
51
-
52
- function engine() {
53
- const loaded = consoleState.loadEngine();
54
- if (!loaded.ok) throw new Error(loaded.error);
55
- return loaded.engine;
56
- }
57
-
58
- /**
59
- * The main working tree of the repository `root` sits in. A linked worktree (an agent started by Hand to
60
- * agent on a new branch runs in one) has a `.git` FILE naming its gitdir, whose `commondir` leads back to the
61
- * main repository's `.git`. Filesystem reads only — no git process, because this runs on every poll.
62
- *
63
- * A `.git` FILE is followed ONLY for a real linked worktree of a non-bare repository whose main tree exists.
64
- * Everything else keeps `root`: a submodule and a `--separate-git-dir` repository have no `commondir`; a worktree
65
- * of a bare repository has a common directory that is not named `.git`. Following those would put the ledger
66
- * outside the repository (or inside a git directory), where git, CI and every other clone never see it.
67
- */
68
- function mainTree(root) {
69
- const dotgit = path.join(root, '.git');
70
- let st;
71
- try { st = fs.statSync(dotgit); } catch { return root; }
72
- if (st.isDirectory()) return root;
73
- let m = null;
74
- try { m = /^gitdir:\s*(.+)$/m.exec(fs.readFileSync(dotgit, 'utf8')); } catch { return root; }
75
- if (!m) return root;
76
- const gitdir = path.resolve(root, m[1].trim());
77
- let common;
78
- try { common = path.resolve(gitdir, fs.readFileSync(path.join(gitdir, 'commondir'), 'utf8').trim()); }
79
- catch { return root; } // a submodule or a separate git directory
80
- if (path.basename(common) !== '.git') return root; // a worktree of a bare repository
81
- const main = path.dirname(common);
82
- try { if (!fs.statSync(path.join(main, '.git')).isDirectory()) return root; } catch { return root; }
83
- return main;
84
- }
85
-
86
- /** The initiative record, looked up in this tree first and then in the main tree. null when neither has it. */
87
- function recordPath(root, initiative) {
88
- if (typeof initiative !== 'string' || !INIT_RE.test(initiative)) return null;
89
- // The folder's README.md and index.md are not records: the same rule as the engine's listInitiatives().
90
- if (initiative === 'README' || initiative === 'index') return null;
91
- const eng = engine();
92
- const roots = [root];
93
- const main = mainTree(root);
94
- if (main !== root) roots.push(main);
95
- for (const r of roots) {
96
- const file = path.join(eng.layout(r).initiatives, `${initiative}.md`);
97
- try { if (fs.statSync(file).isFile()) return file; } catch { /* try the next tree */ }
98
- }
99
- return null;
100
- }
101
-
102
- /** The repository's profile, the initiative's lane (as workflow-state.js resolves it) and the lane definitions. */
103
- function profileAndLane(root, initiative) {
104
- const eng = engine();
105
- const { profile, defs } = eng.lanesFor(root);
106
- const file = recordPath(root, initiative);
107
- let fm = {};
108
- if (file) { try { fm = eng.parseLeadingFrontmatter(fs.readFileSync(file, 'utf8')) || {}; } catch { fm = {}; } }
109
- const declared = String(fm.lane || '').trim();
110
- // An unknown or missing lane is the profile's strictest, exactly as workflow-state.js computes `effLane`.
111
- const fallback = profile === 'pm-artifacts' ? 'prd' : 'feature';
112
- const lane = declared && defs[declared] ? declared : fallback;
113
- return { profile, lane, defs, record: file, group: viewGroup(defs, declared) };
114
- }
115
-
116
- /**
117
- * Whether an initiative is a work card or a document in the planning views (D86 point 4), from its DECLARED lane.
118
- *
119
- * Only a record that declares a document lane is a document. A missing or unknown lane is work: the fallback above
120
- * picks the strictest ladder for the gate, and in a pm-artifacts repository that fallback is `prd`, a document lane.
121
- * Grouping by the fallback hid every record written before lanes were declared (0.53.0: most of a kit repository's
122
- * initiatives were missing from Initiatives, Board and Reports).
123
- */
124
- function viewGroup(defs, declared) {
125
- const d = String(declared || '').trim();
126
- return d && defs && defs[d] && defs[d].group ? defs[d].group : 'work';
127
- }
128
-
129
- const cap = (s) => (s && /^[a-z]/.test(s) ? s[0].toUpperCase() + s.slice(1) : s);
130
-
131
- /** The stages of this initiative, in order: [{ id, label, steps }]. */
132
- function stagesFor(root, initiative) {
133
- const { profile, lane, defs } = profileAndLane(root, initiative);
134
- if (profile !== 'pm-artifacts') return STAGE_GROUPS['product-code'].map((g) => ({ ...g, steps: g.steps.slice() }));
135
- const def = defs[lane] || defs.prd;
136
- const out = [];
137
- for (const g of STAGE_GROUPS['pm-artifacts']) {
138
- if (g.id === 'handoff' && !(def.rungs || []).includes('handoff')) continue;
139
- const label = g.id === 'brief' ? cap(def.brief_label) : g.id === 'draft' ? cap(def.draft_label) : g.label;
140
- out.push({ id: g.id, label, steps: g.steps.slice() });
141
- }
142
- return out;
143
- }
144
- function stageIdsFor(root, initiative) { return stagesFor(root, initiative).map((s) => s.id); }
145
-
146
- /** A stage id from an id or a label, case- and space-insensitive. null when it names no stage of this initiative. */
147
- function resolveStage(root, initiative, idOrLabel) {
148
- const norm = (s) => String(s == null ? '' : s).toLowerCase().replace(/\s+/g, ' ').trim();
149
- const want = norm(idOrLabel);
150
- if (!want) return null;
151
- for (const s of stagesFor(root, initiative)) if (norm(s.id) === want || norm(s.label) === want) return s.id;
152
- return null;
153
- }
154
-
155
- /** The noun an owed row uses for a step, with the lane's own brief and draft labels filled in. */
156
- function nounFor(key, def) {
157
- if (key === 'brief') return (def && def.brief_label) || 'brief';
158
- if (key === 'draft') return (def && def.draft_label) || 'draft';
159
- return STEP_NOUN[key] || key;
160
- }
161
-
162
- /**
163
- * The owed row of a stage: the first step of its group that is still open. null when every step is done,
164
- * n/a or unknown — those neither block nor count as owed (R3, R4).
165
- */
166
- function owedFor(group, steps, def) {
167
- const byKey = new Map((steps || []).map((s) => [s.key, s]));
168
- for (const key of group.steps) {
169
- const s = byKey.get(key);
170
- if (!s || !OPEN(s.status)) continue;
171
- const noun = nounFor(key, def);
172
- const actor = OWED_ACTOR[key] || 'agent';
173
- return { key, noun, actor, status: s.status, next: s.next || null, label: `Owed: ${noun} · ${actor}` };
174
- }
175
- return null;
176
- }
177
-
178
- /**
179
- * The index of the furthest stage with evidence (scan-consistency R2): the last stage that holds a step the ladder
180
- * reads `done`. -1 when no stage has one.
181
- *
182
- * WHY THE STEPPER NEEDS IT. The current stage used to be the FIRST stage with an open step. A step skipped long ago — a
183
- * `priors` citation nobody wrote, a contract never logged before the build — then pinned the initiative at Spec or
184
- * Contract for ever, while the build had merged and the PM had accepted it: 90 built initiatives read Spec on this
185
- * repository, and 51 accepted ones read Intake, Spec, Contract or Build. Work moves forward past a gap; the gap stays a
186
- * fact. So the current stage is the first stage with an open step AT OR AFTER the frontier, and an earlier stage that
187
- * still has an open step reads `skipped` (its owed row is kept), never `done`.
188
- */
189
- function frontierOf(stages, steps) {
190
- const done = new Set((steps || []).filter((s) => s && s.status === 'done').map((s) => s.key));
191
- // Accepted work is finished whatever its leftover steps (initiative-display.js rule 2): the frontier is past the end,
192
- // so no stage is current and every stage with an open step reads `skipped`. A document lane is finished once it is
193
- // approved and no handoff is still open.
194
- const by = new Map((steps || []).filter(Boolean).map((s) => [s.key, s.status]));
195
- if (by.get('accept') === 'done' || (by.get('approved') === 'done' && !OPEN(by.get('handoff') || 'na'))) return stages.length;
196
- let at = -1;
197
- stages.forEach((s, i) => { if (s.steps.some((k) => done.has(k))) at = i; });
198
- return at;
199
- }
200
-
201
- /**
202
- * The stage an initiative is in now, from its ladder steps, by the same rule the Breakdown and the sheet's stepper use:
203
- * { id, label } of the first stage at or after the frontier with an open step, or null when no stage has one.
204
- */
205
- function currentStage(root, initiative, steps, def) {
206
- const stages = stagesFor(root, initiative);
207
- const front = frontierOf(stages, steps);
208
- for (let i = Math.max(front, 0); i < stages.length; i++) {
209
- if (owedFor(stages[i], steps, def) !== null) return { id: stages[i].id, label: stages[i].label };
210
- }
211
- return null;
212
- }
213
-
214
- /**
215
- * The progress label (R6), shared by the layout, the fold's progressOf and the CLI. Proposed and withdrawn items never
216
- * count; when nothing is kept yet but an agent proposed items, it says so instead of a bare "no breakdown".
217
- */
218
- function progressLabel(done, total, proposed) {
219
- if (total) return `${done} of ${total}`;
220
- return proposed ? `no breakdown · ${proposed} proposed` : 'no breakdown';
221
- }
222
-
223
- const byLabel = (by) => {
224
- const s = String(by || '');
225
- if (/^pm:/.test(s)) return 'you';
226
- const m = /^agent:(sess-\d+)\./.exec(s);
227
- if (m) return `agent · ${m[1]}`;
228
- if (s === 'agent:unknown') return 'agent (unknown)';
229
- return s || 'unknown';
230
- };
231
-
232
- /** One history entry as a sentence. `stageLabelOf(id)` names a stage. */
233
- function historyText(h, stageLabelOf) {
234
- const lab = (id) => (stageLabelOf && stageLabelOf(id)) || id;
235
- const st = (s) => STATE_LABEL[s] || s;
236
- if (h.ev === 'planned') return `added in ${lab(h.stage)}`;
237
- if (h.ev === 'moved') {
238
- const ev = h.evidence ? ` with evidence ${h.evidence.kind}:${h.evidence.ref}` : '';
239
- return `moved to ${st(h.to)}${ev}`;
240
- }
241
- if (h.ev === 'flagged') return h.blocked ? `blocked: ${h.reason}` : 'unblocked';
242
- if (h.ev === 'withdrawn') return 'withdrawn';
243
- if (h.ev === 'kept') return 'kept';
244
- if (h.ev === 'set') {
245
- const f = h.fields || {};
246
- const parts = [];
247
- for (const k of Object.keys(f)) {
248
- const v = f[k];
249
- if (k === 'stage') parts.push(`moved to stage ${lab(v)}`);
250
- else if (k === 'order') parts.push('reordered');
251
- else if (k === 'title') parts.push(`renamed to "${v}"`);
252
- else if (k === 'due' || k === 'start') parts.push(v ? `${k} set to ${v}` : `${k} cleared`);
253
- else if (k === 'owner') parts.push(`owner set to ${v}`);
254
- else if (k === 'after') parts.push(Array.isArray(v) && v.length ? `after ${v.join(', ')}` : 'predecessors cleared');
255
- else if (k === 'milestone') parts.push(v ? 'marked a milestone' : 'no longer a milestone');
256
- else if (k === 'note') parts.push(v ? 'note changed' : 'note cleared');
257
- else parts.push(`${k} changed`);
258
- }
259
- return parts.join('; ') || 'changed';
260
- }
261
- return h.ev;
262
- }
263
-
264
- const treeOrder = (a, b) => (a.order - b.order) || String(a.createdAt).localeCompare(String(b.createdAt)) || a.id.localeCompare(b.id);
265
-
266
- /** What a plan item reads when it was dragged to Done and the factory has not integrated its work item (R7). */
267
- const NOT_MERGED_LABEL = 'Done · your call · not merged';
268
-
269
- /**
270
- * The factory join applied to the items of one initiative (PRD R7, D86 point 1; pm-os-absorb Run 4).
271
- *
272
- * A plan item that a work item materializes (its `declared` line names it as `plan_item`) carries
273
- * `factory: { item, state, integrated, pr, notMerged, label }`. Once that work item is integrated (`pmos verify-work`
274
- * saw its trailer on the default branch), the plan item reads Done with no hand edit: state `done`, and its Done time
275
- * is the integration's unless it was already Done. A plan item moved to Done whose work item is NOT integrated stays
276
- * Done, the PM's call, and says so (`notMerged`, `label`). The plan-items ledger is not written: the factory's own
277
- * `integrated` line is the evidence (D86 point 1), so this is applied on every read. A withdrawn work item is not a
278
- * join. An item no work item names is returned unchanged and gains no key, so the layout's shape holds for it.
279
- */
280
- function withFactory(root, initiative, fold, items) {
281
- let entry = null;
282
- try { entry = factoryJoin.factoryOf(factoryJoin.joinFactory(root, { initiative, fold }), initiative); } catch { entry = null; }
283
- if (!entry || !entry.items.length) return items;
284
- const byPlan = new Map();
285
- for (const w of entry.items) if (w.plan_item && w.state !== 'withdrawn' && !byPlan.has(w.plan_item)) byPlan.set(w.plan_item, w);
286
- return items.map((it) => {
287
- const w = byPlan.get(it.id);
288
- if (!w) return it;
289
- const notMerged = !w.integrated && it.state === 'done';
290
- const factory = { item: w.id, state: w.state, integrated: !!w.integrated, pr: w.pr || null, notMerged, label: notMerged ? NOT_MERGED_LABEL : null };
291
- if (!w.integrated) return { ...it, factory };
292
- const doneAt = it.state === 'done' ? it.doneAt : (w.integrated_at || it.doneAt || null);
293
- return { ...it, state: 'done', doneAt, noEvidence: false, late: false, factory };
294
- });
295
- }
296
-
297
- /**
298
- * The Breakdown layout of one initiative (the shape the pane draws; see the Run 1 plan §2.2).
299
- *
300
- * @param {object} o
301
- * @param {string} o.root the workspace root
302
- * @param {string} o.initiative the initiative id
303
- * @param {object} o.fold planItems.fold() result for the repository's ledger
304
- * @param {object|null} o.ladder consoleState.ladder() result, or null for the fast path (no owed rows)
305
- * @param {number} [o.now]
306
- */
307
- function layout({ root, initiative, fold, ladder, now }) {
308
- const t = Number.isFinite(now) ? now : Date.now();
309
- const { profile, lane, defs } = profileAndLane(root, initiative);
310
- const def = profile === 'pm-artifacts' ? (defs[lane] || defs.prd) : null;
311
- const stages = stagesFor(root, initiative);
312
- const stageIds = stages.map((s) => s.id);
313
- const labelOf = (id) => { const s = stages.find((x) => x.id === id); return s ? s.label : id; };
314
- const ladderIncluded = ladder !== null && ladder !== undefined;
315
- let steps = null;
316
- let ladderState = null;
317
- if (ladderIncluded) {
318
- if (ladder && ladder.ok && ladder.row && Array.isArray(ladder.row.steps)) {
319
- steps = ladder.row.steps;
320
- ladderState = { ok: true, error: null };
321
- } else {
322
- ladderState = { ok: false, error: (ladder && (ladder.error || (ladder.empty ? `no ladder for ${initiative}` : null))) || 'the ladder could not be derived' };
323
- }
324
- }
325
-
326
- const mine = withFactory(root, initiative, fold,
327
- (fold.order || []).map((id) => fold.items[id]).filter((it) => it && it.initiative === initiative));
328
- const gone = new Set(fold.withdrawnIds || []);
329
- const items = {};
330
- const flags = [];
331
- for (const it of mine) {
332
- const stageKnown = stageIds.includes(it.stage);
333
- if (!stageKnown) flags.push({ kind: 'unknown-stage', line: null, eid: null, item: it.id, message: `${it.id} names stage "${it.stage}", which this initiative's lane does not have — shown under Other` });
334
- const { movedVia, ...shown } = it; // movedVia stays in the fold (needsFor reads it); the pane does not
335
- items[it.id] = {
336
- ...shown, stageKnown, byLabel: byLabel(it.updatedBy),
337
- // A withdrawn predecessor is not shown: the sheet then sends the list without it, and the writer lets it go.
338
- after: it.after.filter((x) => !gone.has(x)),
339
- // A planned entry carries its stage in the fold so its sentence can say where it was added; the pane gets
340
- // the §1.4 history keys and the sentence, not the stage.
341
- history: it.history.map(({ stage, ...h }) => ({ ...h, byLabel: byLabel(h.by), text: historyText({ ...h, stage }, labelOf) })),
342
- };
343
- }
344
- const visibleItems = mine;
345
- const counted = (list) => list.filter((it) => !it.proposed);
346
-
347
- let firstOpenSeen = false;
348
- const frontier = steps ? frontierOf(stages, steps) : -1;
349
- const stageRows = stages.map((s, i) => {
350
- const inStage = visibleItems.filter((it) => it.stage === s.id).sort(treeOrder);
351
- const real = counted(inStage);
352
- const total = real.length;
353
- const done = real.filter((it) => it.state === 'done').length;
354
- const proposed = inStage.length - real.length;
355
- const row = { id: s.id, label: s.label };
356
- let owed = null;
357
- if (steps) owed = owedFor(s, steps, def);
358
- row.visible = total > 0 || proposed > 0 || owed !== null;
359
- // A stage before the first open step is `done`; every stage after it is `later`, even one whose own steps
360
- // are all done, because the work reaches it in ladder order. Without the ladder (`ladder=0`) `current`,
361
- // `state` and `owed` are not derived, so they are left out: the client keeps them from its last full
362
- // response. A ladder that was asked for but failed gives `current:false`, `state:null`, `owed:null`.
363
- if (steps) {
364
- // A stage behind the frontier with a step still open was passed over, not finished (see frontierOf).
365
- const skipped = owed !== null && i < frontier;
366
- const current = owed !== null && !skipped && !firstOpenSeen;
367
- row.current = current;
368
- row.state = current ? 'current' : skipped ? 'skipped' : (firstOpenSeen ? 'later' : 'done');
369
- if (owed !== null && !skipped) firstOpenSeen = true;
370
- } else if (ladderIncluded) {
371
- row.current = false;
372
- row.state = null;
373
- }
374
- Object.assign(row, { done, total, proposed, count: total > 0 ? `${done} of ${total}` : null });
375
- if (ladderIncluded) row.owed = owed;
376
- row.items = inStage.map((it) => it.id);
377
- return row;
378
- });
379
- const otherItems = visibleItems.filter((it) => !stageIds.includes(it.stage)).sort(treeOrder);
380
-
381
- const real = counted(visibleItems);
382
- const pDone = real.filter((it) => it.state === 'done').length;
383
- const progress = { done: pDone, total: real.length, proposed: visibleItems.length - real.length,
384
- label: progressLabel(pDone, real.length, visibleItems.length - real.length) };
385
-
386
- const stageIdx = (id) => { const i = stageIds.indexOf(id); return i === -1 ? stageIds.length : i; };
387
- const boardSort = (a, b) => (stageIdx(a.stage) - stageIdx(b.stage)) || treeOrder(a, b);
388
- const board = { todo: [], doing: [], review: [], done: [], doneOlder: 0 };
389
- for (const st of ['todo', 'doing', 'review', 'done']) {
390
- let list = visibleItems.filter((it) => it.state === st);
391
- if (st === 'done') {
392
- const recent = list.filter((it) => it.doneAt && Date.parse(it.doneAt) >= t - DONE_WINDOW_MS);
393
- board.doneOlder = list.length - recent.length;
394
- list = recent;
395
- }
396
- board[st] = list.sort(boardSort).map((it) => it.id);
397
- }
398
-
399
- const batches = (fold.batches || []).filter((b) => b.initiative === initiative)
400
- .map((b) => ({ key: b.by, by: b.by, byLabel: b.byLabel, count: b.count, items: b.items.slice(), firstAt: b.firstAt, lastAt: b.lastAt }));
401
- for (const f of fold.flags || []) {
402
- if (f.initiative && f.initiative !== initiative) continue;
403
- flags.push({ kind: f.kind, line: f.line, eid: f.eid, item: f.item, message: f.message });
404
- }
405
- const withdrawnCount = (fold.withdrawnIds || []).filter((id) => id.startsWith(initiative + '.')
406
- && /^[0-9a-z]{10}$/.test(id.slice(initiative.length + 1))).length;
407
-
408
- const out = { profile, lane, ladderIncluded };
409
- if (ladderIncluded) out.ladder = ladderState;
410
- Object.assign(out, {
411
- progress, stages: stageRows, other: { items: otherItems.map((it) => it.id) }, items, board, batches, flags, withdrawnCount,
412
- });
413
- return out;
414
- }
415
-
416
- module.exports = {
417
- STAGE_GROUPS, STEP_NOUN, OWED_ACTOR, OPEN, DONE_WINDOW_MS, INIT_RE, NOT_MERGED_LABEL, withFactory,
418
- mainTree, recordPath, profileAndLane, viewGroup, stagesFor, stageIdsFor, resolveStage, owedFor, frontierOf, currentStage, nounFor, byLabel, progressLabel, layout, historyText,
419
- };
@@ -1,39 +0,0 @@
1
- 'use strict';
2
- /*
3
- * CANONICAL Review-Gate pass rule — the single source of truth (D64 / pivot-workspace-first, Track D2).
4
- *
5
- * The rule is: an explicit verdict DECIDES (pass iff it starts with "pass"); the score is a
6
- * fallback used ONLY when no verdict was recorded (score >= 0.85).
7
- *
8
- * AMENDED 2026-07-26 (blind-plant round 2 finding). The rule was previously
9
- * `verdict ILIKE 'pass%' OR score >= 0.85`, and that disjunction was a real leniency hole: a run the
10
- * evaluator explicitly FAILED still passed the gate if its weighted score happened to clear 0.85.
11
- * Round 2 hit it live — specimen `be58` came back verdict=FAIL score=0.93 and the old rule counted it
12
- * pass-side, overriding a FAIL issued under the two-axis rule ("a FAIL on either axis cannot become a
13
- * PASS"). A gate whose plumbing can overturn its own judge is not a gate. See
14
- * planning/evals/blind-plants/2026-07-26-round2/RESULTS.md.
15
- *
16
- * Note the fix is NOT a blunt OR->AND: that would wrongly fail a legitimate `PASS` verdict carrying no
17
- * recorded score. Verdict-authoritative-with-score-fallback preserves every other case and changes
18
- * exactly the one that was broken.
19
- *
20
- * It was previously duplicated verbatim across SQL views, TS, the workspace JS, and metrics.py, so a
21
- * change in one place silently drifted the others. It now lives HERE and is mirrored in exactly one
22
- * other place — kit/scripts/metrics.py's `review_gate_pass()` — with a comment pointing back here.
23
- * kit/fixtures/test_parsers.py runs BOTH implementations against kit/fixtures/review-gate-pass-rule.json
24
- * and fails CI if they ever disagree, so the mirror can never drift unnoticed.
25
- */
26
- const PASS_VERDICT_PREFIX = 'pass'; // ILIKE 'pass%' → lowercased startsWith('pass')
27
- const SCORE_THRESHOLD = 0.85;
28
-
29
- // A recorded verdict is AUTHORITATIVE: the run passes iff that verdict starts with "pass"
30
- // (PASS / PASS-WITH-FINDINGS / …). An explicit FAIL is never overridden by a high score.
31
- // Only when NO verdict was recorded (null / "" / whitespace) does the numeric score decide.
32
- function isReviewPass(verdict, score) {
33
- const v = String(verdict == null ? '' : verdict).trim().toLowerCase();
34
- if (v) return v.startsWith(PASS_VERDICT_PREFIX);
35
- const s = Number(score);
36
- return Number.isFinite(s) && s >= SCORE_THRESHOLD;
37
- }
38
-
39
- module.exports = { isReviewPass, PASS_VERDICT_PREFIX, SCORE_THRESHOLD };