create-pmos 0.52.0 → 0.53.1

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 (229) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +2 -2
  3. package/payload/mods/pmos-cockpit/README.md +13 -0
  4. package/payload/mods/pmos-cockpit/hooks/lib/commands.mjs +2 -1
  5. package/payload/mods/pmos-cockpit/hooks/lib/grants.mjs +41 -11
  6. package/payload/mods/pmos-cockpit/hooks/lib/guard.mjs +228 -8
  7. package/payload/mods/pmos-cockpit/hooks/register.tsx +11 -3
  8. package/payload/planning/evals/eval-calibration.md +1 -1
  9. package/payload/planning/okrs/health-budgets.md +1 -1
  10. package/payload/scripts/plan-items-stage.js +271 -0
  11. package/payload/scripts/pmos-log.sh +18 -7
  12. package/payload/scripts/workflow-state.js +28 -2
  13. package/payload/state/discoveries.md +1 -1
  14. package/payload/state/log.md +1 -1
  15. package/workspace/README.md +2 -0
  16. package/workspace/REMOTE.md +24 -63
  17. package/workspace/bin/pmos.js +19 -3
  18. package/workspace/cut-stub.js +40 -0
  19. package/workspace/first-paint-audit.js +145 -0
  20. package/workspace/fixtures/capture/requirements/capture-2026-10-04-1.md +10 -0
  21. package/workspace/fixtures/initiatives-api/list.json +176 -0
  22. package/workspace/fixtures/initiatives-api/post-403.json +5 -0
  23. package/workspace/fixtures/initiatives-api/post-ok.json +10 -0
  24. package/workspace/fixtures/initiatives-api/roadmap.json +208 -0
  25. package/workspace/fixtures/initiatives-api/sheet.json +112 -0
  26. package/workspace/fixtures/report-api/draft.json +177 -0
  27. package/workspace/fixtures/reports-page/draft-biweekly-2026-10-16.md +97 -0
  28. package/workspace/fixtures/today-api/provider-rows.json +204 -0
  29. package/workspace/fixtures/today-api/seam-probe-kinds.json +5 -0
  30. package/workspace/guide/board.md +81 -0
  31. package/workspace/guide/breakdown.md +154 -0
  32. package/workspace/guide/command-palette.md +75 -0
  33. package/workspace/guide/index.md +28 -17
  34. package/workspace/guide/initiatives-roadmap.md +130 -0
  35. package/workspace/guide/initiatives-screen.md +89 -0
  36. package/workspace/guide/initiatives-timeline.md +57 -0
  37. package/workspace/guide/integrated-work.md +94 -0
  38. package/workspace/guide/knowledge-graph.md +3 -68
  39. package/workspace/guide/launcher.md +51 -0
  40. package/workspace/guide/layout.md +102 -32
  41. package/workspace/guide/live-notes.md +3 -3
  42. package/workspace/guide/pm-only-acts.md +89 -0
  43. package/workspace/guide/pm-only-lines.md +61 -0
  44. package/workspace/guide/proactive-engine.md +21 -5
  45. package/workspace/guide/reports-page.md +80 -0
  46. package/workspace/guide/reports.md +73 -0
  47. package/workspace/guide/timeline.md +70 -0
  48. package/workspace/guide/today.md +85 -0
  49. package/workspace/guide/validate-with-typesafe.md +1 -1
  50. package/workspace/lib/actor.js +91 -0
  51. package/workspace/lib/board-cache.js +32 -2
  52. package/workspace/lib/board-cli.js +108 -0
  53. package/workspace/lib/board-fold.js +232 -0
  54. package/workspace/lib/breakdown-export.js +75 -0
  55. package/workspace/lib/brief.js +20 -2
  56. package/workspace/lib/capture.js +98 -0
  57. package/workspace/lib/engine/plan-items-stage.js +271 -0
  58. package/workspace/lib/engine/workflow-state.js +28 -2
  59. package/workspace/lib/factory-join.js +158 -0
  60. package/workspace/lib/goals.js +165 -0
  61. package/workspace/lib/grant-mint.js +113 -0
  62. package/workspace/lib/health-rules-factory.js +92 -0
  63. package/workspace/lib/health-rules-plan.js +164 -0
  64. package/workspace/lib/health.js +79 -0
  65. package/workspace/lib/initiative-cli.js +222 -0
  66. package/workspace/lib/initiative-display.js +183 -0
  67. package/workspace/lib/initiative-needs.js +127 -0
  68. package/workspace/lib/initiative-rows.js +210 -0
  69. package/workspace/lib/initiative-writer.js +141 -0
  70. package/workspace/lib/issues.js +206 -0
  71. package/workspace/lib/item-cli.js +195 -0
  72. package/workspace/lib/item-spans.js +137 -0
  73. package/workspace/lib/launcher-cli.js +207 -0
  74. package/workspace/lib/measure-cli.js +100 -0
  75. package/workspace/lib/measurements.js +189 -0
  76. package/workspace/lib/mobile-nav.js +57 -53
  77. package/workspace/lib/needs-providers.js +120 -0
  78. package/workspace/lib/placements.js +440 -0
  79. package/workspace/lib/plan-items.js +922 -0
  80. package/workspace/lib/plan-ledgers.js +85 -0
  81. package/workspace/lib/plan-rules.js +72 -0
  82. package/workspace/lib/plan-tree.js +380 -0
  83. package/workspace/lib/pm-guard.js +174 -0
  84. package/workspace/lib/pm-lines-needs.js +331 -0
  85. package/workspace/lib/pmos-verbs.js +69 -0
  86. package/workspace/lib/proactive-cli.js +58 -2
  87. package/workspace/lib/proactive.js +9 -6
  88. package/workspace/lib/qr.js +293 -0
  89. package/workspace/lib/remote-access.js +93 -1
  90. package/workspace/lib/report-cli.js +152 -0
  91. package/workspace/lib/report-draft.js +578 -0
  92. package/workspace/lib/report-needs.js +213 -0
  93. package/workspace/lib/route-modules.js +128 -0
  94. package/workspace/lib/routes-board.js +91 -0
  95. package/workspace/lib/routes-capture.js +52 -0
  96. package/workspace/lib/routes-initiatives.js +371 -0
  97. package/workspace/lib/routes-reports.js +163 -0
  98. package/workspace/lib/routes-timeline.js +197 -0
  99. package/workspace/lib/routes-today.js +32 -0
  100. package/workspace/lib/today-cli.js +99 -0
  101. package/workspace/lib/today.js +333 -0
  102. package/workspace/lib/triage.js +163 -0
  103. package/workspace/lib/verify-work-cli.js +81 -0
  104. package/workspace/lib/work-check-needs.js +99 -0
  105. package/workspace/package-lock.json +1 -1
  106. package/workspace/package.json +4 -5
  107. package/workspace/public/icons/apple-touch-icon.png +0 -0
  108. package/workspace/public/icons/icon-192.png +0 -0
  109. package/workspace/public/icons/icon-512.png +0 -0
  110. package/workspace/public/index.html +40 -19
  111. package/workspace/public/pm/app.js +330 -30
  112. package/workspace/public/pm/bd-board.js +113 -0
  113. package/workspace/public/pm/bd-drag.js +59 -0
  114. package/workspace/public/pm/bd-model.js +152 -0
  115. package/workspace/public/pm/bd-timeline.js +233 -0
  116. package/workspace/public/pm/bd-tree.js +244 -0
  117. package/workspace/public/pm/board-copy.js +57 -0
  118. package/workspace/public/pm/breakdown-api.js +58 -0
  119. package/workspace/public/pm/breakdown-copy.js +138 -0
  120. package/workspace/public/pm/components.css +3135 -0
  121. package/workspace/public/pm/cycle-review.js +125 -0
  122. package/workspace/public/pm/data.js +40 -187
  123. package/workspace/public/pm/dest-board.js +438 -0
  124. package/workspace/public/pm/dest-initiatives.js +134 -0
  125. package/workspace/public/pm/dest-reports.js +270 -0
  126. package/workspace/public/pm/dest-settings.js +217 -0
  127. package/workspace/public/pm/dest-today.js +182 -0
  128. package/workspace/public/pm/dialogs.js +88 -0
  129. package/workspace/public/pm/display-word.js +56 -0
  130. package/workspace/public/pm/dock.js +175 -0
  131. package/workspace/public/pm/fixture.js +8 -227
  132. package/workspace/public/pm/frame-copy.js +99 -0
  133. package/workspace/public/pm/handoff-box.js +82 -0
  134. package/workspace/public/pm/http.js +88 -0
  135. package/workspace/public/pm/icons.js +53 -0
  136. package/workspace/public/pm/initiative-list.js +218 -0
  137. package/workspace/public/pm/initiative-sheet.js +261 -0
  138. package/workspace/public/pm/initiatives-api.js +59 -0
  139. package/workspace/public/pm/initiatives-copy.js +215 -0
  140. package/workspace/public/pm/initiatives-timeline-copy.js +59 -0
  141. package/workspace/public/pm/initiatives-timeline.js +410 -0
  142. package/workspace/public/pm/initiatives-ui.js +123 -0
  143. package/workspace/public/pm/item-sheet.js +251 -0
  144. package/workspace/public/pm/keep-card.js +204 -0
  145. package/workspace/public/pm/lifecycle.js +7 -1
  146. package/workspace/public/pm/mobile.css +49 -20
  147. package/workspace/public/pm/mobile.js +157 -29
  148. package/workspace/public/pm/modes-live.js +7 -883
  149. package/workspace/public/pm/modes.js +32 -1024
  150. package/workspace/public/pm/new-initiative-sheet.js +74 -0
  151. package/workspace/public/pm/next-step-ui.js +3 -2
  152. package/workspace/public/pm/palette-copy.js +51 -0
  153. package/workspace/public/pm/palette.js +312 -0
  154. package/workspace/public/pm/panel-breakdown.js +702 -0
  155. package/workspace/public/pm/panel-cockpit.js +63 -295
  156. package/workspace/public/pm/panel-detail.js +25 -172
  157. package/workspace/public/pm/panel-initiatives.js +1 -19
  158. package/workspace/public/pm/panel-repos.js +31 -81
  159. package/workspace/public/pm/panel-tabs.js +200 -111
  160. package/workspace/public/pm/panel-trees.js +5 -2
  161. package/workspace/public/pm/portfolio-report.js +61 -0
  162. package/workspace/public/pm/primitives.js +656 -0
  163. package/workspace/public/pm/print.css +33 -0
  164. package/workspace/public/pm/provider-cards.js +136 -0
  165. package/workspace/public/pm/registry.js +194 -0
  166. package/workspace/public/pm/release-card.js +110 -0
  167. package/workspace/public/pm/report-view.js +309 -0
  168. package/workspace/public/pm/reports-copy.js +114 -0
  169. package/workspace/public/pm/roadmap.js +271 -0
  170. package/workspace/public/pm/sig-watch.js +41 -0
  171. package/workspace/public/pm/state.js +8 -4
  172. package/workspace/public/pm/status-banner.js +13 -5
  173. package/workspace/public/pm/tab-doc.js +2 -4
  174. package/workspace/public/pm/tab-term.js +12 -2
  175. package/workspace/public/pm/tab-web.js +3 -2
  176. package/workspace/public/pm/timeline-copy.js +48 -0
  177. package/workspace/public/pm/today-cards.js +244 -0
  178. package/workspace/public/pm/today-copy.js +126 -0
  179. package/workspace/public/pm/today-goals.js +172 -0
  180. package/workspace/public/pm/tokens.css +16 -18
  181. package/workspace/public/pm/topbar.js +102 -0
  182. package/workspace/public/pm/triage-card.js +60 -0
  183. package/workspace/public/pm/undo-hold.js +103 -0
  184. package/workspace/public/pm/view-addrepo.js +2 -1
  185. package/workspace/public/pm/view-livenotes.js +3 -1
  186. package/workspace/public/pm/view-today.js +7 -162
  187. package/workspace/public/vendor/Sortable.min.js +2 -0
  188. package/workspace/public/vendor/lucide-sprite.js +79 -0
  189. package/workspace/replaced-tests.json +2877 -0
  190. package/workspace/run-tests.js +56 -5
  191. package/workspace/server.js +446 -40
  192. package/workspace/sheet-answer.js +61 -0
  193. package/workspace/skills/measure.skill +41 -0
  194. package/workspace/skills/morning-brief.skill +15 -9
  195. package/workspace/skills/report-biweekly.skill +23 -25
  196. package/workspace/skills/tracker-refresh.skill +50 -0
  197. package/workspace/test-files.json +21 -3
  198. package/workspace/test-lists/base.json +6 -0
  199. package/workspace/test-lists/run2-a.json +6 -0
  200. package/workspace/test-lists/run2-b.json +13 -0
  201. package/workspace/test-lists/run2-c.json +19 -0
  202. package/workspace/test-lists/run2-d.json +22 -0
  203. package/workspace/test-lists/run3-0.json +5 -0
  204. package/workspace/test-lists/run3-a.json +5 -0
  205. package/workspace/test-lists/run3-b.json +6 -0
  206. package/workspace/test-lists/run3-c.json +15 -0
  207. package/workspace/test-lists/run3-d.json +6 -0
  208. package/workspace/test-lists/run4-a.json +8 -0
  209. package/workspace/test-lists/run4-b.json +6 -0
  210. package/workspace/test-lists/run4-c.json +13 -0
  211. package/workspace/test-lists/run4-d.json +9 -0
  212. package/workspace/test-lists/run5-a.json +6 -0
  213. package/workspace/test-lists/run5-b.json +6 -0
  214. package/workspace/test-lists/run5-c.json +15 -0
  215. package/workspace/test-lists/workspace-launcher.json +5 -0
  216. package/workspace/test-lists/workspace-phone-app.json +5 -0
  217. package/workspace/test-run.js +27 -1
  218. package/workspace/third-party/README.md +44 -0
  219. package/workspace/third-party/lucide-static-1.52.0.LICENSE +43 -0
  220. package/workspace/third-party/sortablejs-1.15.7.LICENSE +21 -0
  221. package/workspace/design-parity.js +0 -673
  222. package/workspace/public/pm/tab-task.js +0 -86
  223. package/workspace/public/pm/view-guide.js +0 -141
  224. package/workspace/public/pm/view-knowledge.js +0 -225
  225. package/workspace/public/pm/view-memory.js +0 -122
  226. package/workspace/public/pm/view-portfolio.js +0 -153
  227. package/workspace/public/pm/view-reports.js +0 -190
  228. package/workspace/public/pm/view-scheduled.js +0 -161
  229. package/workspace/public/pm/view-skills.js +0 -216
@@ -0,0 +1,222 @@
1
+ 'use strict';
2
+ /* initiative-cli.js — `pmos initiative …`: initiatives and their place on the Roadmap from a terminal (pm-os-absorb
3
+ * Run 2, PRD R16, R20 A, R23, R24, R25; D86 point 4).
4
+ *
5
+ * pmos initiative new "<title>" --lane <lane> --anchor <KR|health:id> [--intent "<text>"] [--id <id>]
6
+ * pmos initiative new --from capture-YYYY-MM-DD-<n> [--anchor …] [--lane …] [--title "…"]
7
+ * pmos initiative now <id> PM only: type the id to confirm
8
+ * pmos initiative next|later <id> [--top] to the end of that horizon, or its top
9
+ * pmos initiative top|up|down <id> reorder inside its horizon (in Now: PM only)
10
+ * pmos initiative check-by <id> <YYYY-MM-DD|none>
11
+ * pmos initiative adopt <proposal-eid> PM only: type the eid to confirm
12
+ * pmos initiative drop <proposal-eid>
13
+ * pmos initiative list
14
+ * pmos initiative export <id> the breakdown as markdown on stdout; nothing is written
15
+ * every command: [--json] [--root <dir>]
16
+ *
17
+ * The SAME writers the workspace page uses (lib/initiative-writer.js, lib/placements.js) — no server is needed. Who
18
+ * wrote a line comes from the call path, never from an argument: a PM-only act (Move to Now, adopt, and any line into
19
+ * Now) goes through lib/pm-guard.js requirePmCli — an interactive terminal with no agent session, and the PM types the
20
+ * confirmation; every other command takes lib/actor.js fromCli: the PM at a terminal, the bound agent session from
21
+ * PMOS_SESSION, `agent:unknown` otherwise. An agent's placement is written as a proposal (`proposed: true`).
22
+ * Work lanes only (D86 point 4): `list` and `export` leave document-lane initiatives out.
23
+ * Exit codes: 0 done, 1 refused, 2 usage.
24
+ */
25
+ const path = require('path');
26
+ const { execFileSync } = require('child_process');
27
+ const guard = require('./pm-guard.js');
28
+ const actorLib = require('./actor.js');
29
+ const placements = require('./placements.js');
30
+ const writer = require('./initiative-writer.js');
31
+ const rowsLib = require('./initiative-rows.js');
32
+ const planRules = require('./plan-rules.js');
33
+ const breakdownExport = require('./breakdown-export.js');
34
+ const triage = require('./triage.js');
35
+ const { parseArgs } = require('./schedule-cli');
36
+
37
+ const COMMANDS = { initiative: ['new', 'now', 'next', 'later', 'top', 'up', 'down', 'check-by', 'adopt', 'drop', 'list', 'export'] };
38
+ const USAGE = [
39
+ 'usage: pmos initiative new|now|next|later|top|up|down|check-by|adopt|drop|list|export — initiatives on the Roadmap',
40
+ ' pmos initiative new "<title>" --lane <lane> --anchor <KR|health:id> [--intent "<text>"] [--id <id>]',
41
+ ' pmos initiative new --from capture-YYYY-MM-DD-<n> [--anchor <anchor>] [--lane <lane>] [--title "<title>"]',
42
+ ' pmos initiative now <id> move it into Now (you only; type the id to confirm)',
43
+ ' pmos initiative next|later <id> [--top] move it to the end of Next or Later, or to its top',
44
+ ' pmos initiative top|up|down <id> reorder it inside its horizon',
45
+ ' pmos initiative check-by <id> <YYYY-MM-DD|none>',
46
+ ' pmos initiative adopt <proposal> keep an agent proposal (you only; type the proposal to confirm)',
47
+ ' pmos initiative drop <proposal> close an agent proposal',
48
+ ' pmos initiative list Now, Next and Later, in order',
49
+ ' pmos initiative export <id> the breakdown as markdown on stdout',
50
+ ' every command: [--json] [--root <dir>]',
51
+ 'An agent session writes a placement as a proposal for you to keep or drop. A new initiative lands in Later.',
52
+ ];
53
+ const BOOL = new Set(['json', 'top', 'help']);
54
+ const VALUE = new Set(['lane', 'anchor', 'intent', 'id', 'from', 'title', 'root']);
55
+ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
56
+
57
+ function parse(argv) {
58
+ const rest = []; const bools = {};
59
+ for (const a of argv) {
60
+ if (a.startsWith('--') && BOOL.has(a.slice(2))) bools[a.slice(2)] = true;
61
+ else if (a === '-h') bools.help = true;
62
+ else rest.push(a);
63
+ }
64
+ const { pos, flags } = parseArgs(rest);
65
+ return { pos, flags: { ...flags, ...bools } };
66
+ }
67
+ function repoRoot(cwd) {
68
+ try { return execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || cwd; }
69
+ catch { return cwd; }
70
+ }
71
+
72
+ async function run(argv, { say = (s) => console.log(s), cwd = process.cwd(), isTTY, env, now, ask } = {}) {
73
+ const args = argv[0] === 'initiative' ? argv.slice(1) : argv.slice();
74
+ const verb = args[0];
75
+ const { pos, flags } = parse(args.slice(1));
76
+ const json = !!flags.json;
77
+ if (!verb || verb === 'help' || verb === '--help' || verb === '-h' || flags.help) {
78
+ for (const l of USAGE) say(l);
79
+ return verb ? 0 : 1;
80
+ }
81
+ if (!COMMANDS.initiative.includes(verb)) { say(`unknown initiative command "${verb}" — one of ${COMMANDS.initiative.join(', ')}`); return 2; }
82
+ if ('by' in flags || 'via' in flags) { say(`attribution is set from the call path; --${'by' in flags ? 'by' : 'via'} is not accepted`); return 2; }
83
+ for (const k of Object.keys(flags)) {
84
+ if (!BOOL.has(k) && !VALUE.has(k)) { say(`unknown option --${k}`); return 2; }
85
+ if (VALUE.has(k) && flags[k] === '') { say(`--${k} needs a value`); return 2; }
86
+ }
87
+ const root = flags.root ? path.resolve(cwd, flags.root) : repoRoot(cwd);
88
+ const who = { isTTY: isTTY === undefined ? !!(process.stdin.isTTY && process.stdout.isTTY) : isTTY, env: env === undefined ? process.env : env };
89
+ const fail = (error, code = 1) => { if (json) say(JSON.stringify({ ok: false, error })); else say(`✗ ${error}`); return code; };
90
+ const done = (obj, line) => { if (json) say(JSON.stringify({ ok: true, ...obj })); else say(line); return 0; };
91
+ const pmPress = (act, expect) => guard.requirePmCli({ act, expect, isTTY: who.isTTY, env: who.env, ask: ask || guard.askAtTerminal });
92
+ const order = () => {
93
+ const d = rowsLib.deriveSync(root);
94
+ if (!d.ok) return { error: d.error };
95
+ return { placed: placements.readPlacements(root, { now: now == null ? Date.now() : now, rules: planRules.readRules(root), records: d.rows }), rows: d.rows };
96
+ };
97
+ const write = (events, actor) => placements.appendFromCli(root, events, { actor, now });
98
+ const said = (r, what) => (r.lines && r.lines[0] && r.lines[0].proposed ? `${what} — written as a proposal for the PM to keep or drop` : what);
99
+
100
+ if (verb === 'new') {
101
+ let input = {};
102
+ if (flags.from) {
103
+ const p = triage.proposalFor(root, flags.from);
104
+ if (!p) return fail(`no capture ${flags.from} in this repository`);
105
+ if (p.done) return fail(`${flags.from} was added already, as ${p.initiative}`);
106
+ if (p.empty) return fail(`${flags.from} holds no text; write the idea in the note first`);
107
+ input = { title: p.title, lane: p.lane, from: p.capture };
108
+ if (p.anchor) input.anchor = p.anchor;
109
+ if (p.rejection && !json) say(`! ${flags.from} matches an earlier rejection: ${p.rejection.path}. Read it before you add this.`);
110
+ } else {
111
+ if (!pos[0]) { say('usage: pmos initiative new "<title>" --lane <lane> --anchor <anchor> [--intent "<text>"] [--id <id>]'); return 2; }
112
+ input.title = pos[0];
113
+ }
114
+ if (flags.title) input.title = flags.title;
115
+ for (const k of ['lane', 'anchor', 'intent', 'id']) if (flags[k]) input[k] = flags[k];
116
+ const r = writer.createInitiative(root, input, { actor: actorLib.fromCli(who), now });
117
+ if (!r.ok) return fail(r.error);
118
+ return done({ id: r.id, path: r.path }, `✓ ${r.id} added to Later (${r.path}). Commit the record to keep it.`);
119
+ }
120
+
121
+ if (verb === 'list') {
122
+ const o = order();
123
+ if (o.error) return fail(o.error);
124
+ const titles = new Map(o.rows.map((r) => [r.id, r.title || r.id]));
125
+ if (json) { say(JSON.stringify({ ok: true, order: o.placed.order, byInit: o.placed.byInit, proposals: o.placed.proposals.map((p) => ({ ...p, by: actorLib.pageName(p.by) })) })); return 0; }
126
+ for (const h of placements.HORIZONS) {
127
+ const ids = o.placed.order[h];
128
+ const dormant = h === 'later' ? ids.filter((id) => o.placed.byInit[id].dormant).length : 0;
129
+ say(`${h[0].toUpperCase()}${h.slice(1)} · ${ids.length}${dormant ? ` (${dormant} dormant)` : ''}`);
130
+ for (const id of ids) {
131
+ const p = o.placed.byInit[id];
132
+ const bits = [];
133
+ if (h === 'now' && !p.confirmed) bits.push('unconfirmed');
134
+ if (p.check_by) bits.push(`check by ${p.check_by}`);
135
+ if (p.dormant) bits.push('dormant');
136
+ say(` ${p.rank + 1}. ${id} — ${titles.get(id)}${bits.length ? ' · ' + bits.join(' · ') : ''}`);
137
+ }
138
+ }
139
+ for (const p of o.placed.proposals) say(`proposal ${p.eid}: ${p.init} to ${p.horizon} · ${actorLib.pageName(p.by)}`);
140
+ return 0;
141
+ }
142
+
143
+ if (verb === 'export') {
144
+ const id = pos[0];
145
+ if (!id) { say('usage: pmos initiative export <id>'); return 2; }
146
+ const r = breakdownExport.exportBreakdown(root, id);
147
+ if (!r.ok) return fail(r.error);
148
+ if (json) say(JSON.stringify({ ok: true, id, markdown: r.markdown })); else say(r.markdown);
149
+ return 0;
150
+ }
151
+
152
+ if (verb === 'adopt' || verb === 'drop') {
153
+ const eid = pos[0];
154
+ if (!eid) { say(`usage: pmos initiative ${verb} <proposal>`); return 2; }
155
+ let actor;
156
+ if (verb === 'adopt') {
157
+ const g = await pmPress('adopt', eid);
158
+ if (!g.ok) return fail(g.error);
159
+ actor = g.actor;
160
+ } else actor = actorLib.fromCli(who);
161
+ const r = write([{ ev: verb === 'adopt' ? 'adopted' : 'dropped', proposal: eid }], actor);
162
+ if (!r.ok) return fail(r.error);
163
+ return done({ written: r.written }, `✓ proposal ${eid} ${verb === 'adopt' ? 'kept' : 'dropped'}`);
164
+ }
165
+
166
+ const id = pos[0];
167
+ if (!id) { say(`usage: pmos initiative ${verb} <id>${verb === 'check-by' ? ' <YYYY-MM-DD|none>' : ''}`); return 2; }
168
+
169
+ if (verb === 'now' || verb === 'next' || verb === 'later') {
170
+ // After the last line of that horizon in the LEDGER's order, so no workflow derivation is needed (fast). Day one's
171
+ // rows are seeded before the lines (placements.js), so the ones no line was placed after sort below it. --top puts
172
+ // it first.
173
+ const ledgerOnly = placements.readPlacements(root);
174
+ const list = ledgerOnly.order[verb].filter((x) => x !== id);
175
+ const after = flags.top ? null : (list.length ? list[list.length - 1] : null);
176
+ let actor;
177
+ if (verb === 'now') {
178
+ const g = await pmPress('now', id);
179
+ if (!g.ok) return fail(g.error);
180
+ actor = g.actor;
181
+ } else actor = actorLib.fromCli(who);
182
+ const r = write([{ ev: 'placed', initiative: id, horizon: verb, after }], actor);
183
+ if (!r.ok) return fail(r.error);
184
+ return done({ written: r.written, proposed: r.lines[0].proposed }, said(r, `✓ ${id} moved to ${verb[0].toUpperCase()}${verb.slice(1)}${flags.top ? ', at the top' : ''}`));
185
+ }
186
+
187
+ // top | up | down | check-by: the whole order, day one included.
188
+ const o = order();
189
+ if (o.error) return fail(o.error);
190
+ const cur = o.placed.byInit[id];
191
+ if (!cur) return fail(`no work-lane initiative ${id} in this repository`);
192
+ const list = o.placed.order[cur.horizon];
193
+ const i = list.indexOf(id);
194
+ const ev = { ev: 'placed', initiative: id, horizon: cur.horizon };
195
+ let what;
196
+ if (verb === 'check-by') {
197
+ const v = pos[1];
198
+ if (!v || !(v === 'none' || DATE_RE.test(v))) { say('usage: pmos initiative check-by <id> <YYYY-MM-DD|none>'); return 2; }
199
+ ev.after = i > 0 ? list[i - 1] : null;
200
+ ev.check_by = v === 'none' ? null : v;
201
+ what = v === 'none' ? `✓ ${id}: check-by cleared` : `✓ ${id}: check by ${v}`;
202
+ } else if (verb === 'top' || verb === 'up') {
203
+ if (i === 0) return done({ written: 0 }, `${id} is at the top of ${cur.horizon} already`);
204
+ ev.after = verb === 'top' || i === 1 ? null : list[i - 2];
205
+ what = `✓ ${id} moved ${verb === 'top' ? 'to the top' : 'up'}`;
206
+ } else {
207
+ if (i === list.length - 1) return done({ written: 0 }, `${id} is at the end of ${cur.horizon} already`);
208
+ ev.after = list[i + 1];
209
+ what = `✓ ${id} moved down`;
210
+ }
211
+ let actor;
212
+ if (cur.horizon === 'now') {
213
+ const g = await pmPress('now', id);
214
+ if (!g.ok) return fail(g.error);
215
+ actor = g.actor;
216
+ } else actor = actorLib.fromCli(who);
217
+ const r = write([ev], actor);
218
+ if (!r.ok) return fail(r.error);
219
+ return done({ written: r.written, proposed: r.lines[0].proposed }, said(r, what));
220
+ }
221
+
222
+ module.exports = { run, COMMANDS, USAGE };
@@ -0,0 +1,183 @@
1
+ 'use strict';
2
+ // initiative-display.js — the words an initiative shows: one derived word, the stage, who it waits on and line 2
3
+ // (pm-os-absorb, PRD R3, R5, R13). Nobody types a status: every field is read from a workflow row and the ledgers.
4
+ //
5
+ // displayOf(row, ctx) → { word, stage, waitingOn: 'pm'|'agent'|null, act, idleDays, line2 }
6
+ // idleDays(lastActivity, now) → whole days since the last evidence, or null when there is none
7
+ //
8
+ // `row` is a workflow row (scripts/workflow-state.js computeWorkflow). `ctx` may carry
9
+ // { lifecycle, acceptance, handoff, fold, placement, rules, now }; a missing field is read as unknown, never as a state.
10
+ // THE ctx SUB-SHAPES (frozen; the producers 2C and 5A assemble them, 2D's rules read them):
11
+ // lifecycle : { state: 'active'|'held'|'closed'|'cancelled', reason, at } | null the server's lifecycle fold for this
12
+ // id (the same object lib/next-steps.js reads as `lifecycle`); null = active
13
+ // acceptance : { at, by } | null the PM's Acceptance record for this id (by mapped via actor.pageName)
14
+ // handoff : { item, session } | null a live Hand-to-agent session for one of this id's plan items:
15
+ // item = plan item id, session = 'agent:sess-<n>' (actor.pageName)
16
+ // fold : { items, order, batches, flags } lib/plan-items.js readFold(root) narrowed to this initiative
17
+ // placement : placements.readPlacements(root).byInit[id] | null
18
+ // rules : lib/plan-rules.js readRules(root)
19
+ // now : epoch milliseconds
20
+ // Two optional keys let a caller that knows the lane word the stage and the document step the way the Breakdown does:
21
+ // stageLabel : the stage's label ("PRD") for `word`; without it the stage id, capitalised ("Build")
22
+ // stepNoun : { brief, draft } the lane's own nouns for those two steps, as lib/plan-tree.js nounFor reads them
23
+ //
24
+ // One mapping for every surface: the Initiatives views and the report blocks (Runs 2 and 5) and, when Run 3 swaps the
25
+ // old surfaces, the cockpit, Today and the status banner. Classic script (a UMD setting window.PMOSInitiativeDisplay)
26
+ // like status-groups.js, served from /lib/, so the same words are walkable in node over every record.
27
+ //
28
+ // THE RULES (PRD R3), in the order they are tried:
29
+ // 1. a lifecycle line that is not `active` reads On hold, Closed or Cancelled, and nothing else is shown;
30
+ // 2. an initiative with an acceptance reads Accepted (code profile) or Handed off (pm-artifacts, once no Handoff step is
31
+ // open), whatever its next step: leftover ladder steps are History notes (leftoverSteps), never an act it owes;
32
+ // 3. anything else reads stage · waiting on · act ("Build · waiting on you: approval"). The owed step is the first open
33
+ // step of the ladder: a step that reads `na`, `unknown` or `done` neither blocks nor counts as owed.
34
+ // An initiative counts as moving only with evidence within `stall_days`: a live session, an item in Doing or Review, or a
35
+ // commit or ledger event (the row's last_activity, or an item's last write). Without it, line 2 ends "idle 12 d".
36
+ // Nothing here reads a `status:` line; chores keep writing it because the gate reads it, and it never reaches the display.
37
+ //
38
+ // Used by 2C's route rows and 5A's blocks, and by the Initiatives views through those rows. Run 3 points the old surfaces
39
+ // here. `STEP_ACT` and `PM_STEPS` mirror lib/plan-tree.js (STEP_NOUN, OWED_ACTOR) and lib/next-steps.js (PM_STEPS):
40
+ // this file is served to the page and cannot require them, and r3-display.test.js fails when the three drift apart.
41
+ (function (root, factory) {
42
+ const api = factory();
43
+ if (typeof module !== 'undefined' && module.exports) module.exports = api;
44
+ else root.PMOSInitiativeDisplay = api;
45
+ }(typeof self !== 'undefined' ? self : this, function () {
46
+ const DAY = 86400000;
47
+ const DEFAULT_STALL_DAYS = 5;
48
+
49
+ // The noun of the owed step, as the Breakdown's owed row names it (lib/plan-tree.js STEP_NOUN).
50
+ const STEP_ACT = Object.freeze({
51
+ route: 'routing', anchor: 'anchor', prototype: 'prototype', understand: 'explainer', spec: 'spec', priors: 'priors',
52
+ contract: 'contract', build: 'build', quality: 'checks', review: 'review', explain: 'run explainer', accept: 'acceptance',
53
+ outcome: 'outcome check', brief: 'brief', draft: 'draft', approved: 'approval', handoff: 'handoff',
54
+ });
55
+ // The steps only the PM can do: the gate on a contract, a document or an acceptance, an outcome check, a handoff.
56
+ const PM_STEPS = Object.freeze(['accept', 'contract', 'approved', 'outcome', 'handoff']);
57
+ const LIFECYCLE_WORD = Object.freeze({ held: 'On hold', 'on-hold': 'On hold', closed: 'Closed', cancelled: 'Cancelled' });
58
+ const OPEN_STATUS = (s) => s !== 'done' && s !== 'na' && s !== 'unknown';
59
+
60
+ /** A time as epoch milliseconds: an ISO string, a number of milliseconds, or the engine's epoch seconds. */
61
+ function toMs(t) {
62
+ if (typeof t === 'number' && Number.isFinite(t)) return t < 1e11 ? t * 1000 : t;
63
+ const n = Date.parse(String(t || ''));
64
+ return Number.isFinite(n) ? n : null;
65
+ }
66
+
67
+ function idleDays(lastActivity, now) {
68
+ const t = toMs(lastActivity);
69
+ const n = typeof now === 'number' ? now : Date.now();
70
+ if (t === null) return null;
71
+ return Math.max(0, Math.floor((n - t) / DAY));
72
+ }
73
+
74
+ const itemsOf = (fold) => {
75
+ const it = fold && fold.items;
76
+ return Array.isArray(it) ? it : it && typeof it === 'object' ? Object.values(it) : [];
77
+ };
78
+ const mine = (it, id) => !id || !it.initiative || it.initiative === id;
79
+
80
+ /**
81
+ * The evidence an initiative has: { at, moving }. `at` is the latest time of a commit or ledger event (the row's
82
+ * last_activity, an item's last write), in epoch milliseconds, or null. `moving` is true for a live session or an item
83
+ * in Doing or Review, which is evidence now whatever the clock says.
84
+ */
85
+ function evidenceOf(row, ctx) {
86
+ const r = row || {};
87
+ const c = ctx || {};
88
+ let at = toMs(r.last_activity);
89
+ let moving = !!c.handoff;
90
+ for (const it of itemsOf(c.fold)) {
91
+ if (!it || !mine(it, r.id) || it.state === 'withdrawn' || it.proposed) continue;
92
+ if (it.state === 'doing' || it.state === 'review') moving = true;
93
+ const t = toMs(it.updatedAt);
94
+ if (t !== null && (at === null || t > at)) at = t;
95
+ }
96
+ return { at, moving };
97
+ }
98
+
99
+ const stallDays = (ctx) => {
100
+ const n = ctx && ctx.rules && ctx.rules.stall_days;
101
+ return Number.isInteger(n) && n > 0 ? n : DEFAULT_STALL_DAYS;
102
+ };
103
+
104
+ /** True when an initiative has no evidence within stall_days (R3). One with no evidence at all is idle too. */
105
+ function isIdle(row, ctx) {
106
+ const ev = evidenceOf(row, ctx);
107
+ if (ev.moving) return false;
108
+ const days = idleDays(ev.at, ctx && typeof ctx.now === 'number' ? ctx.now : undefined);
109
+ return days === null || days >= stallDays(ctx);
110
+ }
111
+
112
+ /** "idle 12 d", or "idle" when there is no evidence at all, or null when the initiative has evidence within stall_days. */
113
+ function idleLabel(row, ctx) {
114
+ if (!isIdle(row, ctx)) return null;
115
+ const ev = evidenceOf(row, ctx);
116
+ const d = idleDays(ev.at, ctx && typeof ctx.now === 'number' ? ctx.now : undefined);
117
+ return d === null ? 'idle' : `idle ${d} d`;
118
+ }
119
+
120
+ /** The steps still owed, in ladder order: not done, `na` or `unknown`. */
121
+ function owedSteps(row) {
122
+ const steps = row && Array.isArray(row.steps) ? row.steps : [];
123
+ return steps.filter((s) => s && OPEN_STATUS(s.status));
124
+ }
125
+
126
+ /** The owed step the engine names as next, else the first open one, or null. */
127
+ function owedStep(row) {
128
+ const open = owedSteps(row);
129
+ const named = row && row.next && row.next.step;
130
+ return open.find((s) => s.key === named) || open[0] || null;
131
+ }
132
+
133
+ /** What an accepted or handed-off initiative still carries on its ladder: History notes, never owed acts. */
134
+ function leftoverSteps(row) { return owedSteps(row).map((s) => s.key); }
135
+
136
+ const isPmProfile = (row) => !!row && row.profile === 'pm-artifacts';
137
+ const capital = (s) => (s ? s.charAt(0).toUpperCase() + s.slice(1) : s);
138
+
139
+ function finished(row, ctx) {
140
+ const accepted = !!(row && row.accepted) || !!(ctx && ctx.acceptance);
141
+ if (!accepted) return null;
142
+ if (!isPmProfile(row)) return 'Accepted';
143
+ const handoff = (row.steps || []).find((s) => s && s.key === 'handoff');
144
+ return handoff && OPEN_STATUS(handoff.status) ? null : 'Handed off';
145
+ }
146
+
147
+ /** "Waiting on you: approval", "Waiting on agent: build", or '' when nothing is owed. */
148
+ function waitingLine(on, act) {
149
+ return on && act ? `Waiting on ${on === 'pm' ? 'you' : 'agent'}: ${act}` : '';
150
+ }
151
+
152
+ function displayOf(row, ctx) {
153
+ const r = row || {};
154
+ const c = ctx || {};
155
+ const stage = typeof r.stage === 'string' && r.stage ? r.stage : null;
156
+ const out = { word: '', stage, waitingOn: null, act: null, idleDays: null, line2: '' };
157
+ const ev = evidenceOf(r, c);
158
+ out.idleDays = idleDays(ev.at, c.now); // days since the last commit or ledger event; a live session does not reset it
159
+
160
+ const lc = c.lifecycle && c.lifecycle.state ? c.lifecycle : null;
161
+ if (lc && LIFECYCLE_WORD[lc.state]) {
162
+ out.word = LIFECYCLE_WORD[lc.state];
163
+ out.line2 = lc.reason ? String(lc.reason) : '';
164
+ return out;
165
+ }
166
+ const done = finished(r, c);
167
+ if (done) { out.word = done; return out; }
168
+
169
+ out.word = (typeof c.stageLabel === 'string' && c.stageLabel) || capital(stage) || 'Unknown';
170
+ const step = owedStep(r);
171
+ if (step) {
172
+ out.waitingOn = PM_STEPS.includes(step.key) ? 'pm' : 'agent';
173
+ out.act = (step.key === 'brief' || step.key === 'draft') && c.stepNoun && c.stepNoun[step.key] ? c.stepNoun[step.key] : (STEP_ACT[step.key] || step.key);
174
+ }
175
+ const parts = [waitingLine(out.waitingOn, out.act)];
176
+ const idle = idleLabel(r, c);
177
+ if (idle) parts.push(idle);
178
+ out.line2 = parts.filter(Boolean).join(' · ');
179
+ return out;
180
+ }
181
+
182
+ return { displayOf, idleDays, idleLabel, evidenceOf, isIdle, owedSteps, owedStep, leftoverSteps, STEP_ACT, PM_STEPS };
183
+ }));
@@ -0,0 +1,127 @@
1
+ 'use strict';
2
+ /* initiative-needs.js — the needs provider of the Initiatives layer (pm-os-absorb Run 2, slice 2C; PRD R12 kinds 6, 7
3
+ * and 8, R16, R26). lib/needs-providers.js folds these rows into server.js deriveNeeds, and from there into /api/needs
4
+ * and Today. The row shapes are held to fixtures/today-api/provider-rows.json.
5
+ *
6
+ * kinds: triage (6), cycle-review (7), no-anchor (8)
7
+ * rowsFor(w, { lead }) → rows · sig(w) → string
8
+ *
9
+ * triage one row per capture note not yet added (lib/triage.js): "Proposed: <title> · <lane> · <anchor>".
10
+ * A capture whose words match a file under planning/out-of-scope/ says "matches an earlier rejection"
11
+ * in today.cause and names that file in today.evidence. Act: Add (`pmos initiative new --from …`).
12
+ * cycle-review ONE row (R12 kind 7) while the cycle holds something to confirm: day one's unconfirmed Now
13
+ * initiatives (R26), agent proposals not kept or dropped (R23), and passed check-by dates. 2D draws it.
14
+ * no-anchor one row per work-lane initiative record with no parent_kr (R12 kind 8, "old records have no anchor").
15
+ *
16
+ * Every row is repository-wide, so a tree that is not the lead returns []. A row never carries a session token: no
17
+ * `by` is answered at all. The day-one and no-anchor rows need the workflow rows. They are read with
18
+ * lib/initiative-rows.js rowsFor(): it answers from the cache at once and starts a derivation in a child process (about
19
+ * 260 ms per record) when the cache is empty or stale, so no request waits for it. A failed refresh keeps the last good
20
+ * answer, so once the rows are in they stay on every read. Until the first derivation lands, banner(w) puts a pending
21
+ * line in Today's state-health banner (lib/needs-providers.js bannersFor), so the rows never pop in unannounced.
22
+ * sig(w) folds the ledgerSig of placements, measurements and plan-items, the records signature, the cached derivation
23
+ * (cachedSig: it changes when a derivation lands) and the capture folder, so /api/needs recounts when any of them
24
+ * changes (seams §4.9).
25
+ */
26
+ const fs = require('fs');
27
+ const path = require('path');
28
+ const crypto = require('crypto');
29
+ const ledgers = require('./plan-ledgers.js');
30
+ const placements = require('./placements.js');
31
+ const planRules = require('./plan-rules.js');
32
+ const rowsLib = require('./initiative-rows.js');
33
+ const triage = require('./triage.js');
34
+
35
+ const NAME = 'initiative-needs';
36
+ const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
37
+
38
+ function ledgerRel(root, name) {
39
+ try { return path.relative(root, ledgers.ledgerPath(root, name)).split(path.sep).join('/'); } catch { return `planning/runs/${ledgers.LEDGERS[name].file}`; }
40
+ }
41
+
42
+ function triageRows(root) {
43
+ const t = triage.triage(root);
44
+ return t.proposals.map((p) => {
45
+ const anchor = p.anchor || 'no anchor yet';
46
+ return {
47
+ run_id: `${NAME}:triage:${p.capture}`, kind: 'triage', initiative_id: null,
48
+ question: `Proposed: ${p.title} · ${p.laneLabel} · ${anchor}`, doc: null,
49
+ today: {
50
+ kind: 6, source: NAME,
51
+ cause: p.rejection ? `A capture was triaged; it matches an earlier rejection: ${p.rejection.title}` : 'A capture was triaged',
52
+ evidence: p.rejection ? p.rejection.path : p.path,
53
+ act: { label: 'Add', cli: `pmos initiative new --from ${p.capture}` },
54
+ },
55
+ };
56
+ });
57
+ }
58
+
59
+ function cycleRow(root, records, now) {
60
+ const rules = planRules.readRules(root);
61
+ const placed = placements.readPlacements(root, { now, rules, records });
62
+ const today = new Date(now).toISOString().slice(0, 10);
63
+ const unconfirmed = placed.order.now.filter((id) => placed.byInit[id].source === 'day-one');
64
+ const passed = Object.entries(placed.byInit).filter(([, p]) => p.check_by && p.check_by < today).map(([id]) => id);
65
+ const proposals = placed.proposals.length;
66
+ if (!unconfirmed.length && !passed.length && !proposals) return [];
67
+ const parts = [];
68
+ if (unconfirmed.length) parts.push(`Day one: ${plural(unconfirmed.length, 'initiative reads', 'initiatives read')} Now, unconfirmed`);
69
+ if (proposals) parts.push(`${plural(proposals, 'agent proposal', 'agent proposals')} to keep or drop`);
70
+ if (passed.length) parts.push(`${plural(passed.length, 'check-by date', 'check-by dates')} passed`);
71
+ return [{
72
+ run_id: `${NAME}:cycle:${today}`, kind: 'cycle-review', initiative_id: null,
73
+ question: unconfirmed.length ? 'Confirm what is in Now' : 'Review the cycle', doc: null,
74
+ today: { kind: 7, source: NAME, cause: parts.join('; '), evidence: ledgerRel(root, 'placements'), act: { label: 'Review', cli: null } },
75
+ }];
76
+ }
77
+
78
+ function noAnchorRows(root, records) {
79
+ return records.filter((r) => r.ok !== false && r.group === 'work' && !String(r.anchor || '').trim()).map((r) => ({
80
+ run_id: `${NAME}:no-anchor:${r.id}`, kind: 'no-anchor', initiative_id: r.id,
81
+ question: `${r.title || r.id} has no anchor`, doc: null,
82
+ today: { kind: 8, source: NAME, cause: 'An initiative record names no KR or health budget',
83
+ evidence: r.record ? String(r.record) : `planning/initiatives/${r.id}.md`, act: { label: 'Add anchor', cli: null } },
84
+ }));
85
+ }
86
+
87
+ function rowsFor(w, { lead } = {}) {
88
+ if (!lead || !w || !w.root) return [];
89
+ const root = w.root;
90
+ const out = triageRows(root);
91
+ // rowsFor() answers at once from the cache and starts the derivation in a child process when there is none or it is
92
+ // stale; it never blocks this read. A failed refresh keeps the last good answer, so once the rows are in they stay.
93
+ // Until the first derivation lands, banner(w) says so on Today instead of rows popping in unannounced.
94
+ const r = rowsLib.rowsFor(root);
95
+ if (r.ok) {
96
+ out.push(...cycleRow(root, r.rows, Date.now()));
97
+ out.push(...noAnchorRows(root, r.rows));
98
+ }
99
+ return out;
100
+ }
101
+
102
+ /** Today's state-health line while the first derivation of the repository is still running (lead tree only). */
103
+ function banner(w, { lead } = {}) {
104
+ if (!lead || !w || !w.root) return [];
105
+ // a repository with no initiative record has nothing to read: no notice
106
+ const hasRecords = ['planning/initiatives', 'pmos/state/initiatives'].some((d) => { try { return fs.readdirSync(path.join(w.root, d)).some((n) => n.endsWith('.md')); } catch { return false; } });
107
+ if (!hasRecords) return [];
108
+ const r = rowsLib.peek(w.root);
109
+ return r.pending ? [{ source: 'initiative rows', error: 'still reading the initiative records; the Cycle review and missing-anchor cards show when that is done' }] : [];
110
+ }
111
+
112
+ function sig(w) {
113
+ if (!w || !w.root) return 'none';
114
+ const root = w.root;
115
+ let notes = '';
116
+ try {
117
+ const dir = path.join(triage.stateDir(root), 'requirements');
118
+ notes = fs.readdirSync(dir).filter((n) => triage.NOTE_RE.test(n)).sort()
119
+ .map((n) => { try { const st = fs.statSync(path.join(dir, n)); return `${n}:${st.size}:${st.mtimeMs}`; } catch { return n; } }).join(',');
120
+ } catch { notes = 'none'; }
121
+ let oos = '';
122
+ try { oos = fs.readdirSync(path.join(root, 'planning', 'out-of-scope')).sort().join(','); } catch { oos = 'none'; }
123
+ const parts = ['placements', 'measurements', 'plan-items'].map((n) => { try { return ledgers.ledgerSig(root, n); } catch { return 'none'; } });
124
+ return crypto.createHash('sha1').update([...parts, rowsLib.recordsSig(root), rowsLib.cachedSig(root), notes, oos, new Date().toISOString().slice(0, 10)].join('|')).digest('hex');
125
+ }
126
+
127
+ module.exports = { kinds: ['triage', 'cycle-review', 'no-anchor'], rowsFor, banner, sig, triageRows, cycleRow, noAnchorRows };