create-pmos 0.134.0 → 0.135.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 (209) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +1 -1
  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/workspace/bin/notes-replay.js +235 -0
  7. package/workspace/c3-briefing-test.js +130 -0
  8. package/workspace/c3-foundation-test.js +159 -0
  9. package/workspace/c3-ib-items-test.js +105 -0
  10. package/workspace/c3-ib-more-test.js +120 -0
  11. package/workspace/c3-ib-triage-backlog-test.js +119 -0
  12. package/workspace/c3-ini-harness.js +66 -0
  13. package/workspace/c3-initiatives-test.js +140 -0
  14. package/workspace/c3-one-home-test.js +14 -1
  15. package/workspace/c3-reports-test.js +57 -18
  16. package/workspace/c3-settings-test.js +139 -0
  17. package/workspace/c3-work-test.js +42 -10
  18. package/workspace/fixtures/one-home-baseline.json +1 -9
  19. package/workspace/guide/briefing-cards.md +15 -3
  20. package/workspace/guide/briefing.md +12 -8
  21. package/workspace/guide/calendar-popup.md +2 -2
  22. package/workspace/guide/console-calendar.md +39 -10
  23. package/workspace/guide/index.md +1 -0
  24. package/workspace/guide/initiative-boards.md +65 -0
  25. package/workspace/guide/jira.md +3 -1
  26. package/workspace/guide/layout.md +2 -0
  27. package/workspace/guide/my-work.md +5 -3
  28. package/workspace/guide/notes.md +16 -1
  29. package/workspace/guide/reports-dashboards.md +29 -5
  30. package/workspace/guide/settings.md +68 -0
  31. package/workspace/guide/welcome.md +10 -10
  32. package/workspace/lib/calendar-cli.js +3 -2
  33. package/workspace/lib/calendar-layers.js +21 -10
  34. package/workspace/lib/calendar-tidy.js +230 -0
  35. package/workspace/lib/engine/boards.js +542 -0
  36. package/workspace/lib/engine/breakdown-ops.js +180 -0
  37. package/workspace/lib/engine/calendar-import.js +84 -0
  38. package/workspace/lib/engine/calendar-model.js +185 -0
  39. package/workspace/lib/engine/card-state.js +92 -0
  40. package/workspace/lib/engine/derived/derive.js +92 -0
  41. package/workspace/lib/engine/derived/plan.js +3 -0
  42. package/workspace/lib/engine/derived/trust.js +3 -0
  43. package/workspace/lib/engine/doc-health.js +142 -0
  44. package/workspace/lib/engine/doc-sheet.js +75 -0
  45. package/workspace/lib/engine/intake.js +78 -0
  46. package/workspace/lib/engine/outbox.js +86 -0
  47. package/workspace/lib/engine/plan-forecast.js +342 -0
  48. package/workspace/lib/engine/promises.js +29 -0
  49. package/workspace/lib/engine/records/index.js +114 -0
  50. package/workspace/lib/engine/releases.js +96 -0
  51. package/workspace/lib/engine/replies.js +99 -0
  52. package/workspace/lib/engine/reply-contract/reply-contract.js +372 -0
  53. package/workspace/lib/engine/report-families.js +175 -0
  54. package/workspace/lib/engine/roadmap-ops.js +150 -0
  55. package/workspace/lib/engine/sessions-list.js +27 -0
  56. package/workspace/lib/engine/signal-rules.js +95 -0
  57. package/workspace/lib/engine/signals/cards.js +57 -0
  58. package/workspace/lib/engine/signals/detectors.js +370 -0
  59. package/workspace/lib/engine/signals/facts-cli.js +8 -0
  60. package/workspace/lib/engine/signals/facts.js +197 -0
  61. package/workspace/lib/engine/signals/index.js +33 -0
  62. package/workspace/lib/engine/signals/kinds.js +110 -0
  63. package/workspace/lib/engine/signals/layout-paths.js +44 -0
  64. package/workspace/lib/engine/signals/options.js +24 -0
  65. package/workspace/lib/engine/signals/score.js +72 -0
  66. package/workspace/lib/engine/signals/select.js +47 -0
  67. package/workspace/lib/engine/step-planner.js +65 -0
  68. package/workspace/lib/engine/trust/claim-rules.json +93 -0
  69. package/workspace/lib/engine/trust/import-calendar.js +21 -0
  70. package/workspace/lib/engine/trust/import-guard.js +36 -0
  71. package/workspace/lib/engine/trust/ledger.js +53 -0
  72. package/workspace/lib/engine/trust/outbox.js +45 -0
  73. package/workspace/lib/engine/trust/plan.js +5 -0
  74. package/workspace/lib/engine/trust/trust.js +21 -0
  75. package/workspace/lib/engine/truth-table.js +57 -0
  76. package/workspace/lib/engine/workspace-lib.js +24 -0
  77. package/workspace/lib/factory-join.js +1 -1
  78. package/workspace/lib/facts-async.js +4 -4
  79. package/workspace/lib/headless.js +95 -0
  80. package/workspace/lib/import-overlay.js +2 -1
  81. package/workspace/lib/jira-status.js +30 -3
  82. package/workspace/lib/notes-capture.js +9 -1
  83. package/workspace/lib/notes-final.js +38 -13
  84. package/workspace/lib/notes-replay.js +125 -0
  85. package/workspace/lib/notes-screen.js +180 -0
  86. package/workspace/lib/notes-whisper.js +10 -3
  87. package/workspace/lib/outbox-store.js +10 -6
  88. package/workspace/lib/pmos-verbs.js +3 -1
  89. package/workspace/lib/proactive-cli.js +7 -0
  90. package/workspace/lib/proactive.js +15 -2
  91. package/workspace/lib/repo-script.js +36 -0
  92. package/workspace/lib/report-dashboards.js +5 -2
  93. package/workspace/lib/report-frame.js +122 -0
  94. package/workspace/lib/report-schedules.js +113 -0
  95. package/workspace/lib/route-modules.js +9 -2
  96. package/workspace/lib/routes-boards.js +734 -0
  97. package/workspace/lib/routes-brief-page.js +13 -4
  98. package/workspace/lib/routes-briefing.js +22 -11
  99. package/workspace/lib/routes-c3-init.js +4 -3
  100. package/workspace/lib/routes-c3-life.js +2 -1
  101. package/workspace/lib/routes-c3-reports.js +96 -13
  102. package/workspace/lib/routes-c3-strategy.js +4 -3
  103. package/workspace/lib/routes-c3-timeline.js +3 -2
  104. package/workspace/lib/routes-calendar-import.js +3 -2
  105. package/workspace/lib/routes-calendar-views.js +9 -4
  106. package/workspace/lib/routes-doc-health.js +2 -1
  107. package/workspace/lib/routes-doc-sheet.js +2 -1
  108. package/workspace/lib/routes-forecast.js +2 -1
  109. package/workspace/lib/routes-hooks.js +3 -2
  110. package/workspace/lib/routes-initiatives.js +2 -1
  111. package/workspace/lib/routes-intake.js +2 -1
  112. package/workspace/lib/routes-notes.js +55 -9
  113. package/workspace/lib/routes-outbox.js +23 -3
  114. package/workspace/lib/routes-pm-items.js +19 -6
  115. package/workspace/lib/routes-records.js +3 -2
  116. package/workspace/lib/routes-releases.js +2 -1
  117. package/workspace/lib/routes-report-facts.js +4 -3
  118. package/workspace/lib/routes-report-families.js +2 -1
  119. package/workspace/lib/routes-roadmap-ops.js +5 -4
  120. package/workspace/lib/routes-runs.js +6 -5
  121. package/workspace/lib/routes-settings.js +51 -0
  122. package/workspace/lib/routes-signals.js +3 -2
  123. package/workspace/lib/routes-stage-tools.js +220 -0
  124. package/workspace/lib/run-answers.js +2 -1
  125. package/workspace/lib/settings-store.js +226 -0
  126. package/workspace/lib/stage-tools.js +208 -0
  127. package/workspace/lib/vendor/scripts/signals/layout-paths.js +2 -1
  128. package/workspace/lib/vendor/scripts/workspace-lib.js +24 -0
  129. package/workspace/lib/work-evidence.js +73 -0
  130. package/workspace/package-lock.json +2 -2
  131. package/workspace/package.json +1 -1
  132. package/workspace/public/pm/briefing-deck.js +63 -17
  133. package/workspace/public/pm/briefing-model.js +36 -7
  134. package/workspace/public/pm/c3-ambient.js +100 -0
  135. package/workspace/public/pm/c3-brief-needs.js +26 -6
  136. package/workspace/public/pm/c3-brief-pulse.js +12 -4
  137. package/workspace/public/pm/c3-brief-today.js +56 -21
  138. package/workspace/public/pm/c3-brief.js +2 -1
  139. package/workspace/public/pm/c3-calendar.js +161 -42
  140. package/workspace/public/pm/c3-calpop.js +5 -5
  141. package/workspace/public/pm/c3-demo-boards.js +649 -0
  142. package/workspace/public/pm/c3-demo-brief-page.js +1 -1
  143. package/workspace/public/pm/c3-demo-cal4.js +3 -2
  144. package/workspace/public/pm/c3-demo-cal5.js +52 -0
  145. package/workspace/public/pm/c3-demo-data.js +17 -27
  146. package/workspace/public/pm/c3-demo-rep3.js +46 -5
  147. package/workspace/public/pm/c3-demo.js +2 -1
  148. package/workspace/public/pm/c3-dock.js +1 -1
  149. package/workspace/public/pm/c3-ib-docs.js +80 -0
  150. package/workspace/public/pm/c3-ib-initiatives.js +134 -0
  151. package/workspace/public/pm/c3-ib-items.js +201 -0
  152. package/workspace/public/pm/c3-ib-releases.js +51 -0
  153. package/workspace/public/pm/c3-ib-sprints.js +55 -0
  154. package/workspace/public/pm/c3-ib-triage.js +178 -0
  155. package/workspace/public/pm/c3-ii-tabs.js +240 -0
  156. package/workspace/public/pm/c3-ini-records.js +197 -0
  157. package/workspace/public/pm/c3-ini-timeline.js +278 -0
  158. package/workspace/public/pm/c3-ini-ui.js +229 -0
  159. package/workspace/public/pm/c3-ini-work.js +139 -0
  160. package/workspace/public/pm/c3-initiatives.css +218 -0
  161. package/workspace/public/pm/c3-initiatives.js +741 -0
  162. package/workspace/public/pm/c3-plan.js +2 -2
  163. package/workspace/public/pm/c3-reports.css +47 -47
  164. package/workspace/public/pm/c3-reports.js +223 -185
  165. package/workspace/public/pm/c3-settings.css +74 -0
  166. package/workspace/public/pm/c3-settings.js +703 -0
  167. package/workspace/public/pm/c3-shell.js +102 -36
  168. package/workspace/public/pm/c3-terminal-icons.js +2 -2
  169. package/workspace/public/pm/c3-terminal.js +2 -2
  170. package/workspace/public/pm/calendar-views.js +1 -1
  171. package/workspace/public/pm/components.css +319 -213
  172. package/workspace/public/pm/console3.css +99 -23
  173. package/workspace/public/pm/dest-notes.js +26 -4
  174. package/workspace/public/pm/notes-screen-capture.js +165 -0
  175. package/workspace/public/pm/notes-screen-view.js +98 -0
  176. package/workspace/public/pm/panel-trees.js +5 -2
  177. package/workspace/public/pm/pm-work-view.js +233 -146
  178. package/workspace/public/pm/shell-bell.js +3 -2
  179. package/workspace/public/pm/shell-recording.js +29 -8
  180. package/workspace/public/pm/tokens.css +2 -2
  181. package/workspace/public/pm/view-notes.js +46 -9
  182. package/workspace/public/pm/welcome.js +197 -89
  183. package/workspace/public/welcome.html +6 -4
  184. package/workspace/server.js +105 -6
  185. package/workspace/skills/day-check.skill +1 -1
  186. package/workspace/skills/jira-sync.skill +6 -4
  187. package/workspace/skills/morning-brief.skill +1 -1
  188. package/workspace/test-files.json +12 -3
  189. package/workspace/test-lists/c3-ib-items.json +9 -0
  190. package/workspace/test-lists/c3-ib-more.json +9 -0
  191. package/workspace/test-lists/c3-ib-triage-backlog.json +9 -0
  192. package/workspace/test-lists/c3-initiative-boards.json +6 -0
  193. package/workspace/test-lists/calendar-tidy-up.json +6 -0
  194. package/workspace/test-lists/console3-briefing.json +9 -0
  195. package/workspace/test-lists/console3-foundation.json +9 -0
  196. package/workspace/test-lists/console3-reports.json +3 -3
  197. package/workspace/test-lists/meeting-notes.json +10 -4
  198. package/workspace/test-lists/workspace-headless-jobs.json +5 -0
  199. package/workspace/test-lists/workspace-package-script-closure.json +6 -0
  200. package/workspace/lib/vendor/workspace/lib/actor.js +0 -91
  201. package/workspace/lib/vendor/workspace/lib/board-cache.js +0 -156
  202. package/workspace/lib/vendor/workspace/lib/console-state.js +0 -295
  203. package/workspace/lib/vendor/workspace/lib/factory-join.js +0 -158
  204. package/workspace/lib/vendor/workspace/lib/git-info.js +0 -306
  205. package/workspace/lib/vendor/workspace/lib/plan-items.js +0 -922
  206. package/workspace/lib/vendor/workspace/lib/plan-tree.js +0 -419
  207. package/workspace/lib/vendor/workspace/lib/review-gate.js +0 -39
  208. package/workspace/lib/vendor/workspace/lib/sink.js +0 -321
  209. package/workspace/lib/vendor/workspace/lib/state-root.js +0 -587
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
  /* routes-brief-page.js — GET /api/brief-page[?ws=<id>&now=ISO] (console3 Briefing as the brief, PRD briefing-brief R2, R3, R8).
3
3
  *
4
- * → { ok, date, today: [meeting], needs: [row], decisions_error? }
4
+ * → { ok, date, me?, today: [meeting], needs: [row], decisions_error? } me: the PM's first name from git config user.name, or null
5
5
  *
6
6
  * READ ONLY, and built only from what PMOS already holds. Nothing is invented.
7
7
  * today one row per meeting of the local day, in time order, from the cached calendar events (routes-calendar-import.js readEvents,
@@ -21,6 +21,7 @@
21
21
  * has it. A deferred item ('Not yet') is left out until its defer date or time. Writes go through the existing pm-items routes. */
22
22
  const path = require('path');
23
23
  const pmItems = require('./pm-items.js');
24
+ const { repoScript } = require('./repo-script.js');
24
25
 
25
26
  const pad = (n) => String(n).padStart(2, '0');
26
27
  const localDate = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
@@ -153,9 +154,17 @@ function notesAdapter(runtimeDir) {
153
154
  };
154
155
  }
155
156
 
157
+ /** The PM's first name for the greeting: the first word of `git config user.name` where PMOS runs. null when git holds none (the page then greets without a name). */
158
+ function firstName(cwd) {
159
+ try {
160
+ const n = String(require('child_process').execFileSync('git', ['config', 'user.name'], { cwd, encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'] })).trim().split(/\s+/)[0];
161
+ return n && n.length <= 40 ? n : null;
162
+ } catch { return null; }
163
+ }
164
+
156
165
  module.exports = {
157
166
  prefixes: ['/api/brief-page'],
158
- build, todayAgenda, needsOf, evidenceFor, deferred,
167
+ build, todayAgenda, needsOf, evidenceFor, deferred, firstName,
159
168
  handle: async (req, res, url, ctx) => {
160
169
  if (url.pathname !== '/api/brief-page') return false;
161
170
  if (req.method !== 'GET') { ctx.json(res, { ok: false, error: 'GET only' }, 405); return true; }
@@ -174,10 +183,10 @@ module.exports = {
174
183
  const root = (w && w.root) || path.resolve(__dirname, '..', '..');
175
184
  const iso = localDate(now);
176
185
  const facts = await require('./facts-async.js').readFactsAsync(root, { today: iso });
177
- const sig = path.resolve(__dirname, '..', '..', 'scripts', 'signals');
178
- cards = require(path.join(sig, 'index.js')).present(facts, { lens: 'Unblock', zoom: 'week', today: iso }).cards || [];
186
+ cards = require(repoScript('signals/index.js')).present(facts, { lens: 'Unblock', zoom: 'week', today: iso }).cards || [];
179
187
  } catch (e) { decisionsError = 'the decisions could not be read: ' + (e && e.message ? e.message : String(e)); }
180
188
  const out = build({ now: now.toISOString() }, { events, items, cards, decisionsError, notes: dir ? notesAdapter(dir) : {} });
189
+ if (out.body && out.body.ok) out.body.me = firstName(path.resolve(__dirname, '..', '..'));
181
190
  ctx.json(res, out.body, out.status);
182
191
  } catch (e) {
183
192
  ctx.json(res, { ok: false, error: 'the brief page could not be built: ' + (e && e.message ? e.message : String(e)) }, 500);
@@ -1,8 +1,12 @@
1
1
  'use strict';
2
- /* routes-briefing.js — GET /api/briefing?lens=Plan|Unblock|Close|Reflect&zoom=day|week|month[&now=ISO] (console3 wave C1).
2
+ /* routes-briefing.js — GET /api/briefing?lens=Plan|Unblock|Close|Reflect&zoom=day|week|month[&scope=brief|rep][&now=ISO] (console3 wave C1).
3
3
  *
4
- * { ok, lens, zoom, cards, ticker, banner, own_needs }
5
- * own_needs: the PM's own items overdue or due today, as needs rows (lib/needs-overdue.js), overdue first. Additive; the page shows them above the cards.
4
+ * { ok, scope, lens, zoom, cards, ticker, banner, own_needs }
5
+ * scope: whose deck asks. brief (or none) runs the engine's briefing detectors, rep its reports detectors (console3-deck-flow-fixes).
6
+ * Any other scope answers no cards, so a page never shows another page's cards.
7
+ * own_needs: the PM's own items overdue or due today, as needs rows (lib/needs-overdue.js), overdue first. Briefing scope only; the page shows them above the cards.
8
+ * notices: the drafts a dashboard schedule made in the last 7 days that are still in the outbox (lib/report-schedules.js noticesOf).
9
+ * Additive; the bell lists them after the cards, and each opens Reports > Drafts.
6
10
  *
7
11
  * `cards` are the engine's selected cards (scripts/signals present() over readRepoFacts of the repo root), with the card
8
12
  * anatomy untouched. `ticker` is one line per top card, built from the card's own headline and deadline. `banner` is the
@@ -10,9 +14,12 @@
10
14
  * { ok:false, error } with 500 and never throws out of the handler.
11
15
  */
12
16
  const path = require('path');
17
+ const { repoScript } = require('./repo-script.js');
13
18
 
14
19
  const LENSES = ['Plan', 'Unblock', 'Close', 'Reflect'];
15
20
  const ZOOMS = ['day', 'week', 'month'];
21
+ // the engine's detector pages for each deck that asks (scripts/signals/detectors.js `page`)
22
+ const SCOPE_PAGES = { brief: ['briefing'], rep: ['reports'] };
16
23
 
17
24
  function lensFor(date) {
18
25
  const h = date.getHours();
@@ -38,9 +45,8 @@ function factsOnce(eng, root, iso) {
38
45
 
39
46
  function loadEngine() {
40
47
  // the engine is this repo's; `root` only says which repo's initiatives it reads
41
- const sig = path.resolve(__dirname, '..', '..', 'scripts', 'signals');
42
- const { present } = require(path.join(sig, 'index.js'));
43
- const { readRepoFacts } = require(path.join(sig, 'facts.js'));
48
+ const { present } = require(repoScript('signals/index.js'));
49
+ const { readRepoFacts } = require(repoScript('signals/facts.js'));
44
50
  return { present, readRepoFacts };
45
51
  }
46
52
 
@@ -52,12 +58,15 @@ function briefing(query, opts = {}) {
52
58
  if (isNaN(now.getTime())) now = new Date();
53
59
  const lens = LENSES.includes(q.lens) ? q.lens : lensFor(now);
54
60
  const zoom = ZOOMS.includes(q.zoom) ? q.zoom : 'week';
61
+ const scope = q.scope ? String(q.scope) : 'brief';
62
+ const pages = SCOPE_PAGES[scope];
63
+ if (!pages) return { code: 200, body: { ok: true, scope, lens, zoom, cards: [], ticker: [], banner: null } };
55
64
  try {
56
65
  const eng = (opts.load || loadEngine)(root);
57
66
  const iso = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0') + '-' + String(now.getDate()).padStart(2, '0');
58
67
  const facts = opts.load ? eng.readRepoFacts(root, { today: iso }) : factsOnce(eng, root, iso);
59
68
  let useLens = lens;
60
- let cards = eng.present(facts, { lens, zoom, today: iso }).cards || [];
69
+ let cards = eng.present(facts, { lens, zoom, today: iso, pages }).cards || [];
61
70
  // When the clock's lens has no card, show the lens that has the most (ties keep the clock's lens). On the real repo
62
71
  // this is usually all four empty: the briefing detectors read promised dates, forecasts, holders and the like, and no
63
72
  // initiative record carries them yet. The answer then stays empty and says so by fallback_lens being absent.
@@ -65,12 +74,12 @@ function briefing(query, opts = {}) {
65
74
  if (!cards.length && !q.lens) {
66
75
  for (const l of LENSES) {
67
76
  if (l === lens) continue;
68
- const c = eng.present(facts, { lens: l, zoom, today: iso }).cards || [];
77
+ const c = eng.present(facts, { lens: l, zoom, today: iso, pages }).cards || [];
69
78
  if (c.length > cards.length) { cards = c; useLens = l; fallback = true; }
70
79
  }
71
80
  }
72
81
  const banner = cards.find((c) => c.pinned && c.tag === 'Critical') || null;
73
- return { code: 200, body: { ok: true, lens: useLens, zoom, cards, ticker: tickerOf(cards), banner, ...(fallback ? { fallback_lens: true } : {}) } };
82
+ return { code: 200, body: { ok: true, scope, lens: useLens, zoom, cards, ticker: tickerOf(cards), banner, ...(fallback ? { fallback_lens: true } : {}) } };
74
83
  } catch (e) {
75
84
  return { code: 500, body: { ok: false, error: 'the signal engine failed: ' + (e && e.message ? e.message : String(e)) } };
76
85
  }
@@ -78,7 +87,7 @@ function briefing(query, opts = {}) {
78
87
 
79
88
  module.exports = {
80
89
  prefixes: ['/api/briefing'],
81
- briefing, lensFor, tickerOf,
90
+ briefing, lensFor, tickerOf, SCOPE_PAGES,
82
91
  handle: async (req, res, url, ctx) => {
83
92
  if (url.pathname !== '/api/briefing') return false;
84
93
  if (req.method !== 'GET') { ctx.json(res, { ok: false, error: 'GET only' }, 405); return true; }
@@ -96,12 +105,14 @@ module.exports = {
96
105
  const eng = loadEngine();
97
106
  const out = briefing(q, { root: useRoot, load: () => ({ present: eng.present, readRepoFacts: () => facts }) });
98
107
  // additive: the PM's own overdue and due-today items, as needs rows beside the engine's cards (lib/needs-overdue.js); a read failure leaves the list empty
99
- if (out.code === 200) {
108
+ if (out.code === 200 && out.body.scope === 'brief') {
100
109
  try {
101
110
  const pmItems = require('./pm-items.js');
102
111
  const file = path.join(ctx.runtimeDir, 'pm-items', 'items.jsonl');
103
112
  out.body.own_needs = require('./needs-overdue.js').overdueNeeds(pmItems.fold(pmItems.readLines(file)), iso);
104
113
  } catch { out.body.own_needs = []; }
114
+ // additive: a draft a dashboard schedule made in the last 7 days, still in the outbox, is a notice on the bell (lib/report-schedules.js). Nothing was sent.
115
+ try { out.body.notices = require('./report-schedules.js').noticesOf(require('./outbox-store.js').readOutbox(ctx.runtimeDir)); } catch { out.body.notices = []; }
105
116
  }
106
117
  ctx.json(res, out.body, out.code);
107
118
  return true;
@@ -10,9 +10,10 @@
10
10
  const fs = require('fs');
11
11
  const path = require('path');
12
12
  const roadmapOps = require('./routes-roadmap-ops.js');
13
- const records = require(path.join(__dirname, '..', '..', 'scripts', 'records', 'index.js'));
14
- const roadmap = require(path.join(__dirname, '..', '..', 'scripts', 'roadmap-ops.js'));
15
- const breakdown = require(path.join(__dirname, '..', '..', 'scripts', 'breakdown-ops.js'));
13
+ const { repoScript } = require('./repo-script.js');
14
+ const records = require(repoScript('records/index.js'));
15
+ const roadmap = require(repoScript('roadmap-ops.js'));
16
+ const breakdown = require(repoScript('breakdown-ops.js'));
16
17
 
17
18
  function readPeople(runtimeDir) {
18
19
  try {
@@ -13,7 +13,8 @@
13
13
  * Repo facts are not read here (readRepoFacts is synchronous and slow); only two small JSONL files and the existence of named files are. */
14
14
  const fs = require('fs');
15
15
  const path = require('path');
16
- const records = require(path.join(__dirname, '..', '..', 'scripts', 'records', 'index.js'));
16
+ const { repoScript } = require('./repo-script.js');
17
+ const records = require(repoScript('records/index.js'));
17
18
 
18
19
  const REPO = path.join(__dirname, '..', '..');
19
20
  const DAY = 86400000;
@@ -6,6 +6,9 @@
6
6
  * and scope, id, today echoed. 400 on a bad scope or a missing id; 404 for an id nothing holds.
7
7
  * POST /api/report-dashboards/layout { key, reports } → { ok, key, reports, layouts } reports: null resets the key to its default.
8
8
  * 400 on an unknown key or report id; 403 without a page press.
9
+ * POST /api/report-dashboards/schedule { key, plan, audience } → { ok, key, schedule, schedules } plan: none|weekly|monthly; none removes it.
10
+ * 400 on a bad key, plan or audience; 403 without a page press. (lib/report-schedules.js)
11
+ * runDue(ctx, now) the server tick: each due schedule appends ONE draft to the outbox. Nothing is sent.
9
12
  *
10
13
  * Initiatives come from the workspace's facts (lib/facts-async.js), like routes-calendar-views.js. Key results, bets and hypotheses
11
14
  * come from the same records as routes-c3-strategy.js. The handlers are pure over their inputs so a test needs no server.
@@ -14,24 +17,32 @@ const path = require('path');
14
17
  const guard = require('./pm-guard.js');
15
18
  const strategy = require('./routes-c3-strategy.js');
16
19
  const rd = require('./report-dashboards.js');
20
+ const rs = require('./report-schedules.js');
21
+ const RF = require('./report-frame.js');
22
+ const { repoScript } = require('./repo-script.js');
17
23
 
18
24
  const fail = (status, error, extra) => ({ status, body: { ok: false, error, ...(extra || {}) } });
19
25
  const pad = (n) => String(n).padStart(2, '0');
20
26
  const todayStr = () => { const d = new Date(); return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()); };
21
27
  const cap = (s) => (s ? s.charAt(0).toUpperCase() + s.slice(1) : s);
22
28
 
23
- /** boards and initiatives from the facts. A board is boardOf(ini, slug); its title is its id, capitalised. */
24
- function catalogue(facts, slug) {
29
+ /** boards and initiatives from the facts. With `index` (the Initiatives board model, routes-boards.js boardIndex) an
30
+ * initiative sits on the board the Initiatives page shows it on, and a board keeps its name there. Without it a board is
31
+ * boardOf(ini, slug) and its title is its id, capitalised. */
32
+ function catalogue(facts, slug, index) {
25
33
  const inis = ((facts && facts.inis) || []).filter((i) => i && i.id);
26
- const initiatives = inis.map((i) => ({ id: String(i.id), title: String(i.title || i.id), board: rd.boardOf(i, slug) }));
34
+ const boardOf = (i) => (index ? index.of(String(i.id)) : rd.boardOf(i, slug));
35
+ const initiatives = inis.map((i) => ({ id: String(i.id), title: String(i.title || i.id), board: boardOf(i) }));
36
+ const names = new Map(index ? index.boards.map((b) => [b.id, b.title]) : []);
27
37
  const seen = new Map();
28
- initiatives.forEach((i) => { if (i.board && !seen.has(i.board)) seen.set(i.board, { id: i.board, title: cap(i.board) }); });
38
+ initiatives.forEach((i) => { if (i.board && !seen.has(i.board)) seen.set(i.board, { id: i.board, title: names.get(i.board) || cap(i.board) }); });
29
39
  return { boards: [...seen.values()], initiatives };
30
40
  }
31
41
 
32
- function list({ facts, runtimeDir, slug }) {
33
- const c = catalogue(facts, slug);
34
- return { status: 200, body: { ok: true, library: rd.LIBRARY, layouts: rd.readLayouts(runtimeDir), defaults: rd.DEFAULTS, boards: c.boards, initiatives: c.initiatives } };
42
+ function list({ facts, runtimeDir, slug, index, now }) {
43
+ const c = catalogue(facts, slug, index);
44
+ return { status: 200, body: { ok: true, library: rd.LIBRARY, layouts: rd.readLayouts(runtimeDir), defaults: rd.DEFAULTS, boards: c.boards, initiatives: c.initiatives,
45
+ schedules: rs.viewOf(rs.readSchedules(runtimeDir), now || new Date()), plans: rs.PLANS } };
35
46
  }
36
47
 
37
48
  /** query { scope, id }; deps { facts, build() → the c3-strategy body, today, slug, hypotheses }. */
@@ -41,14 +52,14 @@ function data(query, deps) {
41
52
  if (scope !== 'portfolio') {
42
53
  if (typeof id !== 'string' || !id) return fail(400, 'id is required for the ' + scope + ' scope');
43
54
  if (!rd.validId(id)) return fail(400, 'id is not a valid id');
44
- const c = catalogue(deps.facts, deps.slug);
55
+ const c = catalogue(deps.facts, deps.slug, deps.index);
45
56
  const found = scope === 'board' ? c.boards.some((b) => b.id === id) : c.initiatives.some((i) => i.id === id);
46
57
  if (!found) return fail(404, 'no ' + scope + ' has the id ' + id);
47
58
  }
48
59
  const today = deps.today || todayStr();
49
60
  try {
50
61
  const body = deps.build(today);
51
- return { status: 200, body: rd.scopeFilter(body, deps.facts, scope, id || null, today, { slug: deps.slug, hypotheses: deps.hypotheses }) };
62
+ return { status: 200, body: rd.scopeFilter(body, deps.facts, scope, id || null, today, { slug: deps.slug, hypotheses: deps.hypotheses, boardOf: deps.index ? (i) => deps.index.of(String(i.id)) : undefined }) };
52
63
  } catch (e) { return fail(500, String((e && e.message) || e)); }
53
64
  }
54
65
 
@@ -62,6 +73,70 @@ function layout(req, body, runtimeDir) {
62
73
  return { status: 200, body: { ok: true, key: body.key, reports: r.layouts[body.key] || null, layouts: r.layouts } };
63
74
  }
64
75
 
76
+ function schedule(req, body, runtimeDir, now = new Date()) {
77
+ const g = guard.requirePagePress(req, body);
78
+ if (!g.ok) return fail(g.status, g.error);
79
+ const r = rs.saveSchedule(runtimeDir, { key: body.key, plan: body.plan, audience: body.audience, ws: body.ws == null || body.ws === '' ? null : body.ws }, now);
80
+ if (!r.ok) return fail(/could not be saved|no runtime/.test(r.error) ? 500 : 400, r.error);
81
+ const view = rs.viewOf(r.schedules, now);
82
+ return { status: 200, body: { ok: true, key: body.key, schedule: view[body.key] || null, schedules: view } };
83
+ }
84
+
85
+ /* The dashboard's title as the page names it (c3-reports.js dashTitle): Portfolio, Board: <title>, Initiative: <title>. */
86
+ function titleOf(key, c) {
87
+ if (key === 'portfolio') return 'Portfolio';
88
+ const i = key.indexOf(':'); const scope = key.slice(0, i); const id = key.slice(i + 1);
89
+ const x = (scope === 'board' ? c.boards : c.initiatives).find((b) => b.id === id);
90
+ return (scope === 'board' ? 'Board: ' : 'Initiative: ') + (x ? x.title || x.id : id);
91
+ }
92
+
93
+ /**
94
+ * Run one schedule: the dashboard's data for its scope, its current layout (or the scope's default), the frame and text the Share sheet
95
+ * makes, then ONE outbox draft. deps { facts, slug, build(today), hypotheses, runtimeDir, today }. Returns { ok, entry } | { ok:false, error }.
96
+ */
97
+ function runOne(sched, deps, now = new Date()) {
98
+ const key = sched.key;
99
+ const scope = key === 'portfolio' ? 'portfolio' : key.slice(0, key.indexOf(':'));
100
+ const id = scope === 'portfolio' ? null : key.slice(key.indexOf(':') + 1);
101
+ const r = data({ scope, id }, deps);
102
+ if (r.status !== 200) return { ok: false, error: r.body.error || 'the dashboard could not be read' };
103
+ const layouts = rd.readLayouts(deps.runtimeDir);
104
+ const ids = Object.prototype.hasOwnProperty.call(layouts, key) ? layouts[key] : rd.DEFAULTS[scope];
105
+ const reports = ids.map((x) => rd.LIBRARY.find((l) => l.id === x)).filter(Boolean);
106
+ const c = catalogue(deps.facts, deps.slug, deps.index);
107
+ const F = RF.frameOf({ title: titleOf(key, c), reports, raw: r.body, audience: sched.audience, today: r.body.today });
108
+ try { return { ok: true, entry: rs.makeDraft(deps.runtimeDir, sched, F, RF.draftBody(F), now) }; }
109
+ catch (e) { return { ok: false, error: 'the draft could not be saved: ' + String((e && e.message) || e) }; }
110
+ }
111
+
112
+ /** The tick. lastRun moves first, so a slow read never runs the same slot twice; the outcome is written after. */
113
+ let running = false;
114
+ async function runDue(ctx, now = new Date()) {
115
+ if (running || !ctx || !ctx.runtimeDir) return [];
116
+ running = true;
117
+ const done = [];
118
+ try {
119
+ for (const s of rs.dueOf(rs.readSchedules(ctx.runtimeDir), now)) {
120
+ rs.markRun(ctx.runtimeDir, s.key, { lastRun: now.toISOString() });
121
+ let out;
122
+ try {
123
+ const w = ctx.getWs(s.ws || null);
124
+ if (!w) throw new Error('no workspace is registered');
125
+ const slug = w.id || (w.root ? path.basename(path.resolve(w.root)) : null);
126
+ const facts = await (ctx.readFacts || readFacts)(w);
127
+ const root = rootOf(ctx);
128
+ let hypotheses = [];
129
+ try { hypotheses = require(repoScript('records/index.js')).list(root, 'hypotheses'); } catch { hypotheses = []; }
130
+ out = runOne(s, { facts, slug, hypotheses, runtimeDir: ctx.runtimeDir, today: ctx.today,
131
+ build: ctx.build || ((today) => strategy.build(root, path.join(ctx.runtimeDir, 'signals', 'signals.jsonl'), today)) }, now);
132
+ } catch (e) { out = { ok: false, error: String((e && e.message) || e) }; }
133
+ rs.markRun(ctx.runtimeDir, s.key, out.ok ? { last_status: 'ok', last_error: null, last_draft: out.entry.id } : { last_status: 'failed', last_error: out.error });
134
+ done.push({ key: s.key, ...out });
135
+ }
136
+ } finally { running = false; }
137
+ return done;
138
+ }
139
+
65
140
  const rootOf = (ctx) => (ctx && ctx.recordsDir) || path.join((ctx && ctx.repoRoot) || path.join(__dirname, '..', '..'), 'planning', 'records');
66
141
 
67
142
  async function readFacts(w) {
@@ -70,7 +145,7 @@ async function readFacts(w) {
70
145
 
71
146
  module.exports = {
72
147
  prefixes: ['/api/report-dashboards'],
73
- catalogue, list, data, layout,
148
+ catalogue, list, data, layout, schedule, runOne, runDue,
74
149
  handle: async (req, res, url, ctx) => {
75
150
  const p = url.pathname;
76
151
  const send = (r) => { ctx.json(res, r.body, r.status); return true; };
@@ -88,12 +163,14 @@ module.exports = {
88
163
  let facts;
89
164
  try { facts = await readFacts(w); }
90
165
  catch (e) { return send(fail(500, 'the dashboards could not read the repository: ' + (e && e.message ? e.message : String(e)))); }
91
- if (p === '/api/report-dashboards') return send(list({ facts, runtimeDir: ctx.runtimeDir, slug }));
166
+ let index = null; // the Initiatives board model; without it (no module, or a read error) boardOf decides
167
+ try { index = require('./routes-boards.js').boardIndex(ctx, w); } catch { index = null; }
168
+ if (p === '/api/report-dashboards') return send(list({ facts, runtimeDir: ctx.runtimeDir, slug, index }));
92
169
  const root = rootOf(ctx);
93
170
  let hypotheses = [];
94
- try { hypotheses = require('../../scripts/records/index.js').list(root, 'hypotheses'); } catch { hypotheses = []; }
171
+ try { hypotheses = require(repoScript('records/index.js')).list(root, 'hypotheses'); } catch { hypotheses = []; }
95
172
  return send(data({ scope: url.searchParams.get('scope'), id: url.searchParams.get('id') }, {
96
- facts, slug, hypotheses,
173
+ facts, slug, hypotheses, index,
97
174
  build: (today) => strategy.build(root, path.join(ctx.runtimeDir || '.', 'signals', 'signals.jsonl'), today),
98
175
  }));
99
176
  }
@@ -103,6 +180,12 @@ module.exports = {
103
180
  if (!got.ok) return send(fail(400, 'invalid JSON body'));
104
181
  return send(layout(req, got.body || {}, ctx.runtimeDir));
105
182
  }
183
+ if (p === '/api/report-dashboards/schedule') {
184
+ if (req.method !== 'POST') return send(fail(405, 'POST only'));
185
+ const got = await ctx.readJson(req);
186
+ if (!got.ok) return send(fail(400, 'invalid JSON body'));
187
+ return send(schedule(req, got.body || {}, ctx.runtimeDir));
188
+ }
106
189
  return false;
107
190
  },
108
191
  };
@@ -17,9 +17,10 @@
17
17
  * reader is used, so the route never starts a cold repository read. A number that no record holds is null, never invented. */
18
18
  const path = require('path');
19
19
  const fs = require('fs');
20
- const records = require(path.join(__dirname, '..', '..', 'scripts', 'records', 'index.js'));
21
- const sigRules = require(path.join(__dirname, '..', '..', 'scripts', 'signal-rules.js'));
22
- const ledger = require(path.join(__dirname, '..', '..', 'scripts', 'trust', 'ledger.js'));
20
+ const { repoScript } = require('./repo-script.js');
21
+ const records = require(repoScript('records/index.js'));
22
+ const sigRules = require(repoScript('signal-rules.js'));
23
+ const ledger = require(repoScript('trust/ledger.js'));
23
24
 
24
25
  const EARLY = 0.10; // the repository's early-warning share (scripts/calendar-model.js EARLY_WARNING)
25
26
  const OFF = 0.50; // behind the line by half the span or more reads as off course
@@ -23,9 +23,10 @@
23
23
  */
24
24
  const path = require('path');
25
25
  const planItems = require('./plan-items.js');
26
- const F = require('../../scripts/plan-forecast.js');
26
+ const { repoScript } = require('./repo-script.js');
27
+ const F = require(repoScript('plan-forecast.js'));
27
28
  const roadmapOps = require('./routes-roadmap-ops.js');
28
- const breakdown = require(path.join(__dirname, '..', '..', 'scripts', 'breakdown-ops.js'));
29
+ const breakdown = require(repoScript('breakdown-ops.js'));
29
30
 
30
31
  const localYmd = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
31
32
  const DAY = 86400000;
@@ -11,8 +11,9 @@
11
11
  * File: <runtime>/calendar-import/events.jsonl (append only, scripts/trust/ledger.js). */
12
12
  const path = require('path');
13
13
  const guard = require('./pm-guard.js');
14
- const CI = require('../../scripts/calendar-import.js');
15
- const L = require('../../scripts/trust/ledger.js');
14
+ const { repoScript } = require('./repo-script.js');
15
+ const CI = require(repoScript('calendar-import.js'));
16
+ const L = require(repoScript('trust/ledger.js'));
16
17
 
17
18
  const fileOf = (runtimeDir) => path.join(runtimeDir, 'calendar-import', 'events.jsonl');
18
19
  const fail = (status, error) => ({ status, body: { ok: false, error } });
@@ -2,7 +2,8 @@
2
2
  /* routes-calendar-views.js — GET /api/calendar-views?view=day|week|month|quarter|year&date=YYYY-MM-DD&ws=<id> (console3 wave C2).
3
3
  *
4
4
  * → { ok, view, range, cells, unit, gantt, strategy, events_source, today,
5
- * layers, layers_total, layers_note, free, context, context_total } (the last six: lib/calendar-layers.js)
5
+ * layers, layers_total, layers_note, free, context, context_total, (lib/calendar-layers.js)
6
+ * tidy, tidy_total, tidy_note } (lib/calendar-tidy.js: suggestions in words, no write)
6
7
  *
7
8
  * READ ONLY. Initiatives come from the signal engine's repo reader (scripts/signals/facts.js readRepoFacts), required inside
8
9
  * the handler so a failure answers 500 { ok:false } and never reaches the server. The date and the plan come from the records:
@@ -13,8 +14,10 @@
13
14
  * Until one is, events = [] and events_source is 'none'. KRS: no KR metric series is stored in the repo, so the strategy lens
14
15
  * gets none and says so. */
15
16
  const path = require('path');
16
- const model = require('../../scripts/calendar-model.js');
17
+ const { repoScript } = require('./repo-script.js');
18
+ const model = require(repoScript('calendar-model.js'));
17
19
  const layersLib = require('./calendar-layers.js');
20
+ const tidyLib = require('./calendar-tidy.js');
18
21
 
19
22
  const today = () => { const d = new Date(); const p = (n) => String(n).padStart(2, '0'); return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`; };
20
23
 
@@ -44,7 +47,9 @@ function handle(query, deps = {}) {
44
47
  const src = typeof deps.readContextSources === 'function' ? (deps.readContextSources() || {}) : {};
45
48
  const nowMs = Number.isFinite(deps.nowMs) ? deps.nowMs : (deps.today ? undefined : Date.now());
46
49
  const extra = layersLib.build({ range, today: nowDay, nowMs, events, dated, src, own: items.own, planItemsDue: items.planItems });
47
- return { status: 200, body: { ok: true, view, range, unit: b.unit, cells: b.cells, gantt, strategy, events_source: source, today: nowDay, ...extra } };
50
+ const tidy = tidyLib.tidyFrom({ range, today: nowDay, nowMs, events, eventsSource: source, inis, gantt,
51
+ planItems: dated.planItems || items.planItems || [] });
52
+ return { status: 200, body: { ok: true, view, range, unit: b.unit, cells: b.cells, gantt, strategy, events_source: source, today: nowDay, ...extra, ...tidy } };
48
53
  } catch (e) {
49
54
  return { status: 500, body: { ok: false, error: String(e && e.message || e) } };
50
55
  }
@@ -70,7 +75,7 @@ function readers(root, runtimeDir) {
70
75
  const out = { unavailable };
71
76
  try { out.planItems = planRows().map(planItem); } catch (e) { unavailable.plan_items = 'the plan ledger could not be read: ' + reason(e); }
72
77
  try {
73
- const R = require('../../scripts/releases.js');
78
+ const R = require(repoScript('releases.js'));
74
79
  const f = path.join(runtimeDir, 'releases', 'releases.jsonl');
75
80
  let lines = []; try { lines = require('fs').readFileSync(f, 'utf8').split('\n').filter(Boolean); } catch { lines = []; }
76
81
  out.releases = R.foldReleases(lines);
@@ -12,7 +12,8 @@
12
12
  const fs = require('fs');
13
13
  const path = require('path');
14
14
  const { spawnSync } = require('child_process');
15
- const H = require('../../scripts/doc-health.js');
15
+ const { repoScript } = require('./repo-script.js');
16
+ const H = require(repoScript('doc-health.js'));
16
17
 
17
18
  const ROOT = path.resolve(__dirname, '..', '..');
18
19
  const bad = (error, status = 400) => ({ status, body: { ok: false, error } });
@@ -19,7 +19,8 @@ const fs = require('fs');
19
19
  const path = require('path');
20
20
  const guard = require('./pm-guard.js');
21
21
  const applyChange = require('./apply-change.js');
22
- const S = require('../../scripts/doc-sheet.js');
22
+ const { repoScript } = require('./repo-script.js');
23
+ const S = require(repoScript('doc-sheet.js'));
23
24
 
24
25
  const ROOT = path.resolve(__dirname, '..', '..');
25
26
  const fail = (status, error, extra) => ({ status, body: { ok: false, error, ...(extra || {}) } });
@@ -15,7 +15,8 @@
15
15
  const fs = require('fs');
16
16
  const path = require('path');
17
17
  const planItems = require('./plan-items.js');
18
- const F = require('../../scripts/plan-forecast.js');
18
+ const { repoScript } = require('./repo-script.js');
19
+ const F = require(repoScript('plan-forecast.js'));
19
20
 
20
21
  const DEFAULT_PPW = 5;
21
22
  const localYmd = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
@@ -14,8 +14,9 @@ const fs = require('fs');
14
14
  const path = require('path');
15
15
  const actor = require('./actor.js');
16
16
  const runAnswers = require('./run-answers.js');
17
- const C = require('../../scripts/reply-contract/reply-contract.js');
18
- const R = require('../../scripts/replies.js');
17
+ const { repoScript } = require('./repo-script.js');
18
+ const C = require(repoScript('reply-contract/reply-contract.js'));
19
+ const R = require(repoScript('replies.js'));
19
20
 
20
21
  const EVENTS = ['SessionStart', 'PreToolUse', 'PermissionRequest', 'Stop', 'Notification', 'SessionEnd'];
21
22
  const ALIASES = { 'session-start': 'SessionStart', 'pre-tool': 'PreToolUse', 'permission-request': 'PermissionRequest', stop: 'Stop', notification: 'Notification', 'session-end': 'SessionEnd' };
@@ -57,7 +57,8 @@ const tidy = require('./tidy-documents.js');
57
57
  const jevDocs = require('./jev-documents.js');
58
58
  const tsSettings = require('./typesafe-settings');
59
59
  const docCatCorrections = require('./doc-category-corrections');
60
- const { nineStages } = require('../../scripts/nine-stages.js'); // pure label layer; never workflow-state.js (it runs its CLI on require)
60
+ const { repoScript } = require('./repo-script.js');
61
+ const { nineStages } = require(repoScript('nine-stages.js')); // pure label layer; never workflow-state.js (it runs its CLI on require)
61
62
 
62
63
  const HORIZON_RANK = { now: 0, next: 1, later: 2 };
63
64
  const FILE_KEYS = [['brief_path', 'brief', 'Brief'], ['prd_path', 'prd', 'PRD'], ['rubric_path', 'rubric', 'Rubric'], ['doc_path', 'doc', 'Document']];
@@ -4,7 +4,8 @@
4
4
  * Drafts only. Creating the initiative stays with createInitiative, which refuses an unanchored create. */
5
5
  const path = require('path');
6
6
  const guard = require('./pm-guard.js');
7
- const intake = require('../../scripts/intake.js');
7
+ const { repoScript } = require('./repo-script.js');
8
+ const intake = require(repoScript('intake.js'));
8
9
 
9
10
  const ROOT = path.resolve(__dirname, '..', '..');
10
11