create-pmos 0.178.0 → 0.180.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 (59) 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/c3-report-setup-test.js +190 -0
  7. package/workspace/c3-reports-test.js +30 -97
  8. package/workspace/guide/console-calendar.md +17 -2
  9. package/workspace/guide/document-sheet.md +14 -0
  10. package/workspace/guide/index.md +3 -2
  11. package/workspace/guide/reports-dashboards.md +17 -28
  12. package/workspace/guide/reports-page.md +10 -5
  13. package/workspace/guide/reports-set-up.md +56 -0
  14. package/workspace/guide/reports.md +13 -1
  15. package/workspace/guide/settings.md +11 -2
  16. package/workspace/lib/doc-sync.js +28 -6
  17. package/workspace/lib/docs-cli.js +7 -11
  18. package/workspace/lib/four-panel.js +1 -1
  19. package/workspace/lib/headless.js +21 -2
  20. package/workspace/lib/notes-series.js +357 -0
  21. package/workspace/lib/notes.js +53 -4
  22. package/workspace/lib/report-checks.js +103 -0
  23. package/workspace/lib/report-cli.js +89 -4
  24. package/workspace/lib/report-export.js +347 -0
  25. package/workspace/lib/report-finder.js +156 -0
  26. package/workspace/lib/report-needs.js +1 -1
  27. package/workspace/lib/report-setups.js +743 -0
  28. package/workspace/lib/route-modules.js +1 -0
  29. package/workspace/lib/routes-c3-reports.js +35 -17
  30. package/workspace/lib/routes-doc-health.js +15 -1
  31. package/workspace/lib/routes-notes.js +27 -2
  32. package/workspace/lib/routes-report-setups.js +368 -0
  33. package/workspace/lib/schedule-logic.js +5 -2
  34. package/workspace/package-lock.json +2 -2
  35. package/workspace/package.json +1 -1
  36. package/workspace/public/pm/c3-demo-rep2.js +15 -0
  37. package/workspace/public/pm/c3-demo-rs.js +281 -0
  38. package/workspace/public/pm/c3-demo.js +1 -1
  39. package/workspace/public/pm/c3-doc-formats.js +152 -0
  40. package/workspace/public/pm/c3-doc-sheet.js +115 -25
  41. package/workspace/public/pm/c3-notes.js +299 -34
  42. package/workspace/public/pm/c3-quiet.js +2 -2
  43. package/workspace/public/pm/c3-report-print.css +20 -0
  44. package/workspace/public/pm/c3-report-setup.css +62 -0
  45. package/workspace/public/pm/c3-report-setup.js +260 -0
  46. package/workspace/public/pm/c3-report-store.js +208 -0
  47. package/workspace/public/pm/c3-reports.css +4 -0
  48. package/workspace/public/pm/c3-reports.js +47 -181
  49. package/workspace/public/pm/c3-settings.js +4 -3
  50. package/workspace/public/pm/c3-shell.js +15 -31
  51. package/workspace/public/pm/console3.css +13 -0
  52. package/workspace/run-tests.js +25 -1
  53. package/workspace/server.js +66 -5
  54. package/workspace/skills/doc-sync.skill +17 -8
  55. package/workspace/skills/report-find.skill +75 -0
  56. package/workspace/skills/report-setup.skill +67 -0
  57. package/workspace/test-files.json +12 -0
  58. package/workspace/test-lists/c3-notes-series.json +5 -0
  59. package/workspace/test-lists/report-workspace-ui.json +5 -0
@@ -0,0 +1,156 @@
1
+ 'use strict';
2
+ /* report-finder.js — the weekly look for the reports a PM already sends (initiative report-workspace, PRD section 4).
3
+ *
4
+ * off(env) → true when PMOS_REPORT_FIND=off (or 0, false, no)
5
+ * weekStart(ms) → the Monday 00:00 (local) of the week that holds `ms`
6
+ * dueNow(state, now, { open }) → { ok } | { ok: false, why } may the run start now?
7
+ * nextRun(state, now, { dayEnd }) → ms | null when the next run starts (null: off, or no source on)
8
+ * findPrompt(skill, run, ctx) → the prompt of the run
9
+ * create(deps) → { tick(opts), active() }
10
+ *
11
+ * THE RULE. Weekly means Monday 09:00 on this machine's clock, inside the working hours (lib/driver-run.js dayCheckOpen: the day-check window and
12
+ * the quiet hours). A run that missed 09:00 starts at the first tick inside the working hours of that Monday. A Monday the server did not run is
13
+ * not caught up. AT MOST ONE RUN PER WEEK: any run that started this week, whether it ended well or not, ends the week's turn. With every source
14
+ * off, or the finder set to Off, nothing runs. `PMOS_REPORT_FIND=off` does none of it. The run is headless and never a pane.
15
+ *
16
+ * ONE RUN FOR THE MACHINE. The PM's calendar, Slack and mail are the same whichever repository is open, so the run belongs to the HOME workspace
17
+ * (deps.homeId) and files its findings in that repository's report-setups.jsonl. The run reads (kind report-find, lib/headless.js) and files with
18
+ * `pmos report propose`; it has no tool that sends, posts or fetches the web. The server gives it PMOS_REPORT_FILE and PMOS_REPORT_RUN, which the
19
+ * model cannot change, so it files as the find run it is and nothing else.
20
+ *
21
+ * deps: { workspaces() → [w], isLead(w), homeId() → id | null, settings() → proactive settings, inQuiet(now, quiet) → bool, headlessOff() → bool, env,
22
+ * spawn(spec) → session row, stop(session), promptFor(skillId) → skill body | null, warn(what, err) }
23
+ * `spawn(spec)` gets { name, ws, origin, initialInput, env, onDone }; it returns the session row ({ id, state, headless, note }).
24
+ */
25
+ const RS = require('./report-setups.js');
26
+ const driverRun = require('./driver-run.js');
27
+
28
+ const flagOff = (v) => /^(off|0|false|no)$/i.test(String(v || '').trim());
29
+ const off = (env) => flagOff((env || process.env).PMOS_REPORT_FIND);
30
+ const SLOT = '09:00';
31
+ const oneLine = (s, max) => String(s == null ? '' : s).replace(/\s+/g, ' ').trim().slice(0, max);
32
+ const hhmm = (ms) => { const d = new Date(ms); return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`; };
33
+ const slotMs = (ms) => { const d = new Date(ms); const [h, m] = SLOT.split(':').map(Number); d.setHours(h, m, 0, 0); return d.getTime(); };
34
+
35
+ /** The Monday 00:00 (local) of the week that holds `ms`. */
36
+ function weekStart(ms) {
37
+ const d = new Date(ms);
38
+ d.setHours(0, 0, 0, 0);
39
+ d.setDate(d.getDate() - ((d.getDay() + 6) % 7));
40
+ return d.getTime();
41
+ }
42
+ const sourcesOn = (state) => RS.FINDER_SOURCES.filter((k) => state.finder.sources[k]);
43
+ const ranThisWeek = (state, now) => state.finds.some((f) => f.start && Date.parse(f.start) >= weekStart(now));
44
+ const isOpenRun = (state) => state.finds.some((f) => !f.end);
45
+
46
+ /** May a run start now? `open` is the day-check window and quiet hours as one boolean (lib/driver-run.js dayCheckOpen). */
47
+ function dueNow(state, now, { open = true } = {}) {
48
+ if (state.finder.cadence === 'off') return { ok: false, why: 'off' };
49
+ if (!sourcesOn(state).length) return { ok: false, why: 'no-sources' };
50
+ if (new Date(now).getDay() !== 1) return { ok: false, why: 'not-monday' };
51
+ if (now < slotMs(now)) return { ok: false, why: 'before-09' };
52
+ if (!open) return { ok: false, why: 'outside-working-hours' };
53
+ if (isOpenRun(state)) return { ok: false, why: 'running' };
54
+ if (ranThisWeek(state, now)) return { ok: false, why: 'done-this-week' };
55
+ return { ok: true };
56
+ }
57
+
58
+ /** When the next run starts, in ms, or null. A run that is due now and has not started starts at the next tick, so the answer is `now`. */
59
+ function nextRun(state, now, { dayEnd = '18:00' } = {}) {
60
+ if (state.finder.cadence === 'off' || !sourcesOn(state).length) return null;
61
+ const done = ranThisWeek(state, now);
62
+ for (let k = 0; k < 15; k++) {
63
+ const d = new Date(now); d.setDate(d.getDate() + k);
64
+ const day = d.getTime();
65
+ if (d.getDay() !== 1) continue;
66
+ if (k === 0) {
67
+ if (done) continue;
68
+ if (now < slotMs(day)) return slotMs(day);
69
+ if (hhmm(now) < dayEnd) return now;
70
+ continue;
71
+ }
72
+ return slotMs(day);
73
+ }
74
+ return null;
75
+ }
76
+
77
+ /** The prompt of the run: the skill, the run id, the sources that are on, and what the run must skip. Nothing of the repository's private text. */
78
+ function findPrompt(skill, run, { sources, setups = [], dismissed = [], now = Date.now() } = {}) {
79
+ const on = RS.FINDER_SOURCES.filter((k) => sources && sources[k]);
80
+ const offList = RS.FINDER_SOURCES.filter((k) => !(sources && sources[k]));
81
+ const list = (a, none) => (a.length ? a.map((x) => `- ${oneLine(x, 120)}`).join('\n') : `- ${none}`);
82
+ return `${skill}\n\nThis run:\n- Run: ${run}\n- Today: ${new Date(now).toISOString().slice(0, 10)}\n- Sources that are ON (read these only): ${on.join(', ') || 'none'}\n- Sources that are OFF (do not read them): ${offList.join(', ') || 'none'}\n`
83
+ + `- Reports already set up (do not propose them):\n${list(setups, 'none')}\n- Not a report (the PM said so; skip these patterns and names):\n${list(dismissed, 'none')}\n\n`
84
+ + 'File each proposal with `pmos report propose --json -`. File at most 5. Send nothing.';
85
+ }
86
+
87
+ function create(deps) {
88
+ const active = new Map(); // run id → { w, file, session }
89
+ const warn = deps.warn || (() => {});
90
+
91
+ /** The run ended (or never started): write its find-end line. */
92
+ function ended(run, file, r, now) {
93
+ const res = (r && r.result) || {};
94
+ const ok = !!(r && !r.timedOut && !r.stopped && r.code === 0 && res.ok && !res.isError);
95
+ try { RS.findEnd(file, { run, ok, error: ok ? null : (r && r.note) || 'the run failed', now }); } catch (e) { warn('report find end', e); }
96
+ active.delete(run);
97
+ }
98
+
99
+ function start(w, file, state, now) {
100
+ const skill = deps.promptFor('report-find');
101
+ const run = RS.findStart(file, { now });
102
+ active.set(run, { w, file, session: null });
103
+ if (!skill) { ended(run, file, { code: 1, note: 'the report-find skill is not installed', result: {} }, now); return; }
104
+ const st = RS.fold(RS.readLines(file));
105
+ const prompt = findPrompt(skill, run, { sources: st.finder.sources, now,
106
+ setups: [...st.setups.values()].map((s) => s.name),
107
+ dismissed: [...st.dismissed.patterns, ...[...st.dismissed.names].map((n) => `name:${n}`)] });
108
+ let session = null;
109
+ try {
110
+ session = deps.spawn({ name: 'Report finder', ws: w.id, origin: `report-find:${run}`, initialInput: prompt + '\r',
111
+ env: { PMOS_REPORT_FILE: file, PMOS_REPORT_RUN: `find:${run}` }, onDone: (r) => ended(run, file, r) });
112
+ } catch (e) { warn('report find', e); }
113
+ if (!session || session.state === 'exited' || session.headless !== 'report-find') {
114
+ // A start that did not become a headless run (no `claude` on the PATH, a refused model choice, a pane because headless is off) is a failed run.
115
+ if (session && session.headless !== 'report-find' && session.state !== 'exited' && deps.stop) { try { deps.stop(session); } catch { /* best effort */ } }
116
+ if (active.has(run)) ended(run, file, { code: 1, note: (session && session.note) || 'the run could not start as a background run', result: {} }, now);
117
+ return;
118
+ }
119
+ const cur = active.get(run);
120
+ if (cur) cur.session = session.id;
121
+ }
122
+
123
+ function tick({ now = Date.now(), env } = {}) {
124
+ if (off(env || deps.env)) return { off: true };
125
+ const summary = { closed: 0, started: 0 };
126
+ const settings = deps.settings();
127
+ const home = deps.homeId ? deps.homeId() : null;
128
+ for (const w of deps.workspaces()) {
129
+ let lead = false;
130
+ try { lead = !!deps.isLead(w) && (!home || w.id === home); } catch { lead = false; }
131
+ if (!lead) continue;
132
+ let file = null;
133
+ try { file = RS.ledgerFile(w.root); } catch { file = null; }
134
+ if (!file) continue;
135
+ try {
136
+ let state = RS.fold(RS.readLines(file));
137
+ // A run that was alive when the server stopped is over.
138
+ for (const f of state.finds) {
139
+ if (f.end || active.has(f.run)) continue;
140
+ RS.findEnd(file, { run: f.run, ok: false, error: 'interrupted: the workspace restarted', now });
141
+ summary.closed++;
142
+ }
143
+ if (deps.headlessOff()) continue; // the finder never runs in a pane
144
+ if (active.size) continue;
145
+ state = RS.fold(RS.readLines(file));
146
+ const open = driverRun.dayCheckOpen(settings, now, deps.inQuiet);
147
+ if (dueNow(state, now, { open }).ok) { start(w, file, state, now); summary.started++; break; }
148
+ } catch (e) { warn('report find tick', e); }
149
+ }
150
+ return summary;
151
+ }
152
+
153
+ return { tick, active: () => [...active.keys()] };
154
+ }
155
+
156
+ module.exports = { create, off, weekStart, dueNow, nextRun, findPrompt, sourcesOn, SLOT };
@@ -216,4 +216,4 @@ function sig(w) {
216
216
  return [reportsOf(w.root).sig, schedulesStamp(), kill, hour].join(':');
217
217
  }
218
218
 
219
- module.exports = { kinds, rowsFor, sig, rowsAt, reportsOf, readSchedules, cadenceOfSkill, cadenceOfSchedule, blobSha, reportsDir, KILL_FILE, GRACE_MS };
219
+ module.exports = { kinds, rowsFor, sig, rowsAt, reportsOf, frontmatter, readSchedules, cadenceOfSkill, cadenceOfSchedule, blobSha, reportsDir, KILL_FILE, GRACE_MS };