create-pmos 0.133.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 (212) 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 +35 -2
  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-audio.js +28 -9
  83. package/workspace/lib/notes-capture.js +136 -0
  84. package/workspace/lib/notes-final.js +38 -13
  85. package/workspace/lib/notes-live-guard.js +117 -0
  86. package/workspace/lib/notes-replay.js +125 -0
  87. package/workspace/lib/notes-screen.js +180 -0
  88. package/workspace/lib/notes-whisper.js +139 -16
  89. package/workspace/lib/notes.js +4 -2
  90. package/workspace/lib/outbox-store.js +10 -6
  91. package/workspace/lib/pmos-verbs.js +3 -1
  92. package/workspace/lib/proactive-cli.js +7 -0
  93. package/workspace/lib/proactive.js +15 -2
  94. package/workspace/lib/repo-script.js +36 -0
  95. package/workspace/lib/report-dashboards.js +5 -2
  96. package/workspace/lib/report-frame.js +122 -0
  97. package/workspace/lib/report-schedules.js +113 -0
  98. package/workspace/lib/route-modules.js +9 -2
  99. package/workspace/lib/routes-boards.js +734 -0
  100. package/workspace/lib/routes-brief-page.js +13 -4
  101. package/workspace/lib/routes-briefing.js +22 -11
  102. package/workspace/lib/routes-c3-init.js +4 -3
  103. package/workspace/lib/routes-c3-life.js +2 -1
  104. package/workspace/lib/routes-c3-reports.js +96 -13
  105. package/workspace/lib/routes-c3-strategy.js +4 -3
  106. package/workspace/lib/routes-c3-timeline.js +3 -2
  107. package/workspace/lib/routes-calendar-import.js +3 -2
  108. package/workspace/lib/routes-calendar-views.js +9 -4
  109. package/workspace/lib/routes-doc-health.js +2 -1
  110. package/workspace/lib/routes-doc-sheet.js +2 -1
  111. package/workspace/lib/routes-forecast.js +2 -1
  112. package/workspace/lib/routes-hooks.js +3 -2
  113. package/workspace/lib/routes-initiatives.js +2 -1
  114. package/workspace/lib/routes-intake.js +2 -1
  115. package/workspace/lib/routes-notes.js +113 -15
  116. package/workspace/lib/routes-outbox.js +23 -3
  117. package/workspace/lib/routes-pm-items.js +19 -6
  118. package/workspace/lib/routes-records.js +3 -2
  119. package/workspace/lib/routes-releases.js +2 -1
  120. package/workspace/lib/routes-report-facts.js +4 -3
  121. package/workspace/lib/routes-report-families.js +2 -1
  122. package/workspace/lib/routes-roadmap-ops.js +5 -4
  123. package/workspace/lib/routes-runs.js +6 -5
  124. package/workspace/lib/routes-settings.js +51 -0
  125. package/workspace/lib/routes-signals.js +3 -2
  126. package/workspace/lib/routes-stage-tools.js +220 -0
  127. package/workspace/lib/run-answers.js +2 -1
  128. package/workspace/lib/settings-store.js +226 -0
  129. package/workspace/lib/stage-tools.js +208 -0
  130. package/workspace/lib/vendor/scripts/signals/layout-paths.js +2 -1
  131. package/workspace/lib/vendor/scripts/workspace-lib.js +24 -0
  132. package/workspace/lib/work-evidence.js +73 -0
  133. package/workspace/package-lock.json +2 -2
  134. package/workspace/package.json +1 -1
  135. package/workspace/public/pm/briefing-deck.js +63 -17
  136. package/workspace/public/pm/briefing-model.js +36 -7
  137. package/workspace/public/pm/c3-ambient.js +100 -0
  138. package/workspace/public/pm/c3-brief-needs.js +26 -6
  139. package/workspace/public/pm/c3-brief-pulse.js +12 -4
  140. package/workspace/public/pm/c3-brief-today.js +56 -21
  141. package/workspace/public/pm/c3-brief.js +2 -1
  142. package/workspace/public/pm/c3-calendar.js +161 -42
  143. package/workspace/public/pm/c3-calpop.js +5 -5
  144. package/workspace/public/pm/c3-demo-boards.js +649 -0
  145. package/workspace/public/pm/c3-demo-brief-page.js +1 -1
  146. package/workspace/public/pm/c3-demo-cal4.js +3 -2
  147. package/workspace/public/pm/c3-demo-cal5.js +52 -0
  148. package/workspace/public/pm/c3-demo-data.js +17 -27
  149. package/workspace/public/pm/c3-demo-rep3.js +46 -5
  150. package/workspace/public/pm/c3-demo.js +2 -1
  151. package/workspace/public/pm/c3-dock.js +1 -1
  152. package/workspace/public/pm/c3-ib-docs.js +80 -0
  153. package/workspace/public/pm/c3-ib-initiatives.js +134 -0
  154. package/workspace/public/pm/c3-ib-items.js +201 -0
  155. package/workspace/public/pm/c3-ib-releases.js +51 -0
  156. package/workspace/public/pm/c3-ib-sprints.js +55 -0
  157. package/workspace/public/pm/c3-ib-triage.js +178 -0
  158. package/workspace/public/pm/c3-ii-tabs.js +240 -0
  159. package/workspace/public/pm/c3-ini-records.js +197 -0
  160. package/workspace/public/pm/c3-ini-timeline.js +278 -0
  161. package/workspace/public/pm/c3-ini-ui.js +229 -0
  162. package/workspace/public/pm/c3-ini-work.js +139 -0
  163. package/workspace/public/pm/c3-initiatives.css +218 -0
  164. package/workspace/public/pm/c3-initiatives.js +741 -0
  165. package/workspace/public/pm/c3-plan.js +2 -2
  166. package/workspace/public/pm/c3-reports.css +47 -47
  167. package/workspace/public/pm/c3-reports.js +223 -185
  168. package/workspace/public/pm/c3-settings.css +74 -0
  169. package/workspace/public/pm/c3-settings.js +703 -0
  170. package/workspace/public/pm/c3-shell.js +102 -36
  171. package/workspace/public/pm/c3-terminal-icons.js +2 -2
  172. package/workspace/public/pm/c3-terminal.js +2 -2
  173. package/workspace/public/pm/calendar-views.js +26 -13
  174. package/workspace/public/pm/components.css +319 -213
  175. package/workspace/public/pm/console3.css +118 -23
  176. package/workspace/public/pm/dest-notes.js +26 -4
  177. package/workspace/public/pm/notes-screen-capture.js +165 -0
  178. package/workspace/public/pm/notes-screen-view.js +98 -0
  179. package/workspace/public/pm/panel-trees.js +5 -2
  180. package/workspace/public/pm/pm-work-view.js +233 -146
  181. package/workspace/public/pm/shell-bell.js +3 -2
  182. package/workspace/public/pm/shell-recording.js +148 -17
  183. package/workspace/public/pm/tokens.css +2 -2
  184. package/workspace/public/pm/view-notes.js +53 -11
  185. package/workspace/public/pm/welcome.js +197 -89
  186. package/workspace/public/welcome.html +6 -4
  187. package/workspace/server.js +105 -6
  188. package/workspace/skills/day-check.skill +1 -1
  189. package/workspace/skills/jira-sync.skill +6 -4
  190. package/workspace/skills/morning-brief.skill +1 -1
  191. package/workspace/test-files.json +14 -3
  192. package/workspace/test-lists/c3-ib-items.json +9 -0
  193. package/workspace/test-lists/c3-ib-more.json +9 -0
  194. package/workspace/test-lists/c3-ib-triage-backlog.json +9 -0
  195. package/workspace/test-lists/c3-initiative-boards.json +6 -0
  196. package/workspace/test-lists/calendar-tidy-up.json +6 -0
  197. package/workspace/test-lists/console3-briefing.json +9 -0
  198. package/workspace/test-lists/console3-foundation.json +9 -0
  199. package/workspace/test-lists/console3-reports.json +3 -3
  200. package/workspace/test-lists/meeting-notes.json +12 -4
  201. package/workspace/test-lists/workspace-headless-jobs.json +5 -0
  202. package/workspace/test-lists/workspace-package-script-closure.json +6 -0
  203. package/workspace/lib/vendor/workspace/lib/actor.js +0 -91
  204. package/workspace/lib/vendor/workspace/lib/board-cache.js +0 -156
  205. package/workspace/lib/vendor/workspace/lib/console-state.js +0 -295
  206. package/workspace/lib/vendor/workspace/lib/factory-join.js +0 -158
  207. package/workspace/lib/vendor/workspace/lib/git-info.js +0 -306
  208. package/workspace/lib/vendor/workspace/lib/plan-items.js +0 -922
  209. package/workspace/lib/vendor/workspace/lib/plan-tree.js +0 -419
  210. package/workspace/lib/vendor/workspace/lib/review-gate.js +0 -39
  211. package/workspace/lib/vendor/workspace/lib/sink.js +0 -321
  212. package/workspace/lib/vendor/workspace/lib/state-root.js +0 -587
@@ -76,6 +76,7 @@ const jevRoute = require('./lib/jev-route.js'); // prompt-model-routing —
76
76
  const jevClaims = require('./lib/jev-claims.js'); // D80 — the bridge between the claims ledger and the one TypeSafe module
77
77
  const jevCategorize = require('./lib/jev-categorize.js'); // D82 — the bridge between the document categorizer and the one TypeSafe module
78
78
  const validateLib = require('./lib/validate.js');
79
+ const headlessLib = require('./lib/headless.js'); // workspace-headless-jobs — which jobs run as `claude -p`, and with which tools
79
80
  const liveNotes = require('./lib/live-notes.js'); // jev-live-notes — reads and appends a repository's live-notes ledger; no network
80
81
  const jevLive = require('./lib/jev-live.js'); // jev-live-notes Run 7 (D85) — the live view's one bridge to Jev, behind three gates
81
82
  const claimsLib = require('./lib/claims.js');
@@ -2512,7 +2513,13 @@ function checkDueSchedules() {
2512
2513
  // PMOS_TICK_MS exists for the suites (same footing as PMOS_LIVENESS_MS): proactive-prepare.test.js
2513
2514
  // used to wait on two real minutes of this clock. Unset, the interval is the 60 seconds it always was.
2514
2515
  const TICK_MS = Math.max(200, Number(process.env.PMOS_TICK_MS) || 60000);
2515
- try { const _tick = setInterval(() => { checkDueSchedules(); proactiveTick(); try { prepareTick({ manual: false }); } catch { /* the next tick tries again */ } try { dayTick(); } catch (e) { warnWriteFail('day run tick', e); } autoSync.tick().catch(() => {}); }, TICK_MS); if (_tick.unref) _tick.unref(); } catch {}
2516
+ // console3-dashboard-schedules: a scheduled Reports dashboard makes ONE ready draft in the outbox per slot. Nothing is sent.
2517
+ // Loaded on the first tick, as route-modules.js loads it: a checkout without the repository's scripts/ still starts.
2518
+ const dashboardScheduleTick = () => {
2519
+ let m; try { m = require('./lib/routes-c3-reports.js'); } catch { return; }
2520
+ m.runDue({ getWs, runtimeDir: STATE }).catch((e) => warnWriteFail('dashboard schedule tick', e));
2521
+ };
2522
+ try { const _tick = setInterval(() => { checkDueSchedules(); proactiveTick(); try { prepareTick({ manual: false }); } catch { /* the next tick tries again */ } try { dayTick(); } catch (e) { warnWriteFail('day run tick', e); } autoSync.tick().catch(() => {}); dashboardScheduleTick(); }, TICK_MS); if (_tick.unref) _tick.unref(); } catch {}
2516
2523
 
2517
2524
  // ── Sessions: real PTYs, owned by the server, with disk-backed scrollback + metadata. ──
2518
2525
  /** @type {Map<string, {id,ws,name,agent,cwd,state,note,created,pty,buf,cols,rows,clients:Set}>} */
@@ -2552,6 +2559,7 @@ function persist(s) {
2552
2559
  state: s.state, note: s.note || '', created: s.created, tmux: s.tmux || null, origin: s.origin || null };
2553
2560
  if (s.autoDelete) meta.autoDelete = s.autoDelete;
2554
2561
  if (s.autoEnded) meta.autoEnded = true;
2562
+ if (s.headless) meta.headless = s.headless;
2555
2563
  if (s.exitedAt) meta.exitedAt = s.exitedAt;
2556
2564
  // The breakdown item a Hand to agent session is bound to, so a restart keeps the binding (pm-os-absorb Run 1).
2557
2565
  if (s.plan_item) meta.plan_item = s.plan_item;
@@ -2838,6 +2846,7 @@ function sessionList() {
2838
2846
  cwd: path.relative(s.root, s.cwd) || '.', note: s.note || '', origin: s.origin || null,
2839
2847
  automatic: sessionOrigin.isAutomatic(s.origin), // the workspace started it, not the PM: the dock hides these by default
2840
2848
  auto_delete: s.autoDelete || null,
2849
+ headless: s.headless || null, // workspace-headless-jobs: a background run with no terminal ('notes' or 'skill')
2841
2850
  plan_item: s.plan_item || null, // Hand to agent: the breakdown item this session is bound to (pm-os-absorb Run 1)
2842
2851
  // No `token` and no `tokenHash` here, ever: the page never sees a session token (F5; r25-guards.test.js checks it).
2843
2852
  // Derived presentation state, JOINED from both sources: the cadence clock above and the
@@ -3363,7 +3372,80 @@ function sendToSession(id, text) {
3363
3372
  try { firstInput.deliver(s, safeFirstInput(t) + '\r'); return true; } catch { return false; }
3364
3373
  }
3365
3374
 
3366
- function spawnSession({ name, agent, cwd, ws, origin, initialInput, profile, initiative, modelAsk, planItem, workType, autoDelete, launchFlags }) {
3375
+ /* A headless run (workspace-headless-jobs). The row is an ordinary session record, so every caller that checks a
3376
+ * session's state keeps working; it has no pty and no tmux, and `child` is the process. The log is read-only. */
3377
+ function startHeadless(base, workdir, kind, launchArgs, env, initialInput, addDirs) {
3378
+ const prompt = safeFirstInput(String(initialInput).replace(/\r+$/, ''));
3379
+ const args = headlessLib.argsFor(kind, { modelArgs: launchArgs, addDirs: Array.isArray(addDirs) ? addDirs : [] });
3380
+ const s = { ...base, cwd: workdir, state: 'working', pty: null, tmux: null, headless: kind, child: null,
3381
+ note: '', buf: '', cols: 80, rows: 24, clients: new Set(), created: nowISO(), lastOutputAt: Date.now() };
3382
+ const say = (text) => {
3383
+ const d = String(text).replace(/\r?\n/g, '\r\n');
3384
+ appendBuf(s, d);
3385
+ for (const c of s.clients) if (c.readyState === 1) c.send(JSON.stringify({ t: 'data', id: s.id, data: d }));
3386
+ };
3387
+ sessions.set(s.id, s);
3388
+ say(`\x1b[38;5;244mRunning in the background (no terminal). The result shows here when it finishes.\x1b[0m\n`);
3389
+ let child;
3390
+ // detached: its own process group, so stopping it also stops what it started (an MCP server, a script) and no
3391
+ // grandchild keeps the output pipes open after the run is stopped.
3392
+ try { child = spawn(base.agent, args, { cwd: workdir, env, stdio: ['pipe', 'pipe', 'pipe'], detached: true }); }
3393
+ catch (e) {
3394
+ s.state = 'exited'; s.note = spawnFailureAdvice(e, base.agent, false);
3395
+ say(`\x1b[38;5;203m✗ ${s.note}\x1b[0m\n`); persist(s); pushSessions(); pushWorkspaces(); return s;
3396
+ }
3397
+ s.child = child;
3398
+ let out = '', err = '', timedOut = false;
3399
+ const CAP = 5 * 1024 * 1024;
3400
+ child.stdout.on('data', (d) => { if (out.length < CAP) out += d; s.lastOutputAt = Date.now(); });
3401
+ child.stderr.on('data', (d) => { if (err.length < 65536) err += d; });
3402
+ child.on('error', (e) => { err += String(e && e.message || e); });
3403
+ const timer = setTimeout(() => {
3404
+ timedOut = true;
3405
+ killHeadless(child, 'SIGTERM');
3406
+ const hard = setTimeout(() => killHeadless(child, 'SIGKILL'), 5000);
3407
+ if (hard.unref) hard.unref();
3408
+ }, headlessLib.timeoutMs());
3409
+ if (timer.unref) timer.unref();
3410
+ // A headless run prints nothing until it ends, and a quiet row reads as waiting on the PM (lib/harness.js deriveState).
3411
+ // While the process lives, it is working: keep its output clock current.
3412
+ const beat = setInterval(() => { s.lastOutputAt = Date.now(); }, 5000);
3413
+ if (beat.unref) beat.unref();
3414
+ child.on('close', (code, signal) => {
3415
+ clearTimeout(timer); clearInterval(beat);
3416
+ s.child = null; s.state = 'exited';
3417
+ const r = headlessLib.parseResult(out);
3418
+ if (r.ok) say(headlessLib.logLines(r, code).join('\n') + '\n');
3419
+ else if (out.trim()) say(r.text + '\n');
3420
+ if (err.trim()) say(`\x1b[38;5;244m${err.trim().split('\n').slice(-20).join('\n')}\x1b[0m\n`);
3421
+ s.note = timedOut ? `stopped: still running after ${Math.max(1, Math.round(headlessLib.timeoutMs() / 60000))} minutes`
3422
+ : s.stoppedByPm ? 'ended by the PM'
3423
+ : signal ? `exited (signal ${signal})`
3424
+ : (code === 0 && r.ok && r.isError) ? 'exited (the run reported an error)'
3425
+ : `exited (code ${code})`;
3426
+ if (s.note !== 'exited (code 0)') say(`\x1b[38;5;203m[${s.note}]\x1b[0m\n`);
3427
+ persist(s); pushSessions();
3428
+ });
3429
+ child.stdin.on('error', () => {});
3430
+ child.stdin.end(prompt);
3431
+ persist(s); pushSessions(); pushWorkspaces();
3432
+ return s;
3433
+ }
3434
+ /** Stop a headless run; its close handler marks the row exited. → true when there was a process to stop. */
3435
+ function stopHeadless(s) {
3436
+ if (!s || !s.child) return false;
3437
+ s.stoppedByPm = true;
3438
+ killHeadless(s.child, 'SIGTERM');
3439
+ const hard = setTimeout(() => { if (s.child) killHeadless(s.child, 'SIGKILL'); }, 5000);
3440
+ if (hard.unref) hard.unref();
3441
+ return true;
3442
+ }
3443
+ /** Signal a headless run's whole process group; fall back to the process alone. */
3444
+ function killHeadless(child, sig) {
3445
+ try { process.kill(-child.pid, sig); } catch { try { child.kill(sig); } catch {} }
3446
+ }
3447
+
3448
+ function spawnSession({ name, agent, cwd, ws, origin, initialInput, profile, initiative, modelAsk, planItem, workType, autoDelete, launchFlags, addDirs }) {
3367
3449
  const id = nextSessionId();
3368
3450
  /* pm-os-absorb Run 1 — every AGENT session gets a token, `<id>.<8 hex>`, in its environment as PMOS_SESSION, and
3369
3451
  * PMOS_BIN, this workspace's own bin/pmos.js. `pmos item` run inside it is then attributed `agent:<token>` (the token
@@ -3473,6 +3555,11 @@ function spawnSession({ name, agent, cwd, ws, origin, initialInput, profile, ini
3473
3555
  spawnFailureAdvice(Object.assign(new Error(`'${agent}' not found on PATH`), { code: 'ENOENT' }), agent, false));
3474
3556
  }
3475
3557
 
3558
+ // ── the headless path (workspace-headless-jobs): a background job that needs no human runs as `claude -p`, with
3559
+ // no pane, the same environment and model, a fixed tool list and the prompt on stdin (lib/headless.js says which).
3560
+ const hkind = initialInput ? headlessLib.kindFor({ agent, origin: tag, skill: modelAsk && modelAsk.skill }) : null;
3561
+ if (hkind) return startHeadless(base, workdir, hkind, argv, penv, initialInput, addDirs);
3562
+
3476
3563
  // ── the tmux path: the process outlives this server, and we re-attach to it live (R7) ──
3477
3564
  if (TMUX) {
3478
3565
  const tname = freshTmuxName(id);
@@ -4489,7 +4576,7 @@ function writeLayouts(w, doc) {
4489
4576
  // a live one must be killed first (its onExit flips it to exited).
4490
4577
  function removeSession(id) {
4491
4578
  const s = sessions.get(id);
4492
- if (!s || s.pty) return false;
4579
+ if (!s || s.pty || s.child) return false;
4493
4580
  if (s.tmux) tmuxKillSession(s.tmux); // best-effort: never orphan a detached tmux session
4494
4581
  try { recordConversation(s); } catch {} // the conversation outlives the session (surface-batch R5)
4495
4582
  forgetScratchpad(s); // the tab's staging text goes with the tab
@@ -5570,6 +5657,8 @@ async function handleRequest(req, res) {
5570
5657
  // disagree about what a normalized document is (markdown-newlines).
5571
5658
  if (p === '/lib/md-normalize.js') return serveFile(res, path.join(__dirname, 'lib', 'md-normalize.js'));
5572
5659
  if (p === '/lib/layout.js') return serveFile(res, path.join(__dirname, 'lib', 'layout.js'));
5660
+ // The report frame and draft text: the Reports page imports it, and the scheduled dashboard run requires it (console3-dashboard-schedules R6).
5661
+ if (p === '/lib/report-frame.js') return serveFile(res, path.join(__dirname, 'lib', 'report-frame.js'));
5573
5662
  if (p === '/lib/mobile-nav.js') return serveFile(res, path.join(__dirname, 'lib', 'mobile-nav.js'));
5574
5663
  if (p === '/lib/session-state.js') return serveFile(res, path.join(__dirname, 'lib', 'session-state.js'));
5575
5664
  if (p === '/lib/tree-share.js') return serveFile(res, path.join(__dirname, 'lib', 'tree-share.js'));
@@ -5776,6 +5865,11 @@ async function handleRequest(req, res) {
5776
5865
  try { body = JSON.parse((await readBody(req)) || '{}'); } catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
5777
5866
  const sess = sessions.get(String(body.id || ''));
5778
5867
  if (!sess) return json(res, { ok: false, error: 'no such session' }, 404);
5868
+ if (sess.child) {
5869
+ stopHeadless(sess);
5870
+ for (let i = 0; i < 30 && sess.child; i++) await new Promise((r) => setTimeout(r, 100));
5871
+ if (sess.child) return json(res, { ok: true, removed: false, note: 'asked it to end; it has not exited yet — press again to remove it' });
5872
+ }
5779
5873
  if (sess.pty) {
5780
5874
  if (sess.tmux) tmuxKillSession(sess.tmux);
5781
5875
  try { sess.pty.kill(); } catch {}
@@ -8285,7 +8379,7 @@ async function handleRequest(req, res) {
8285
8379
  const dayCheck = body.dayCheck && typeof body.dayCheck === 'object' && !Array.isArray(body.dayCheck) ? { ...cur.dayCheck, ...body.dayCheck } : (body.dayCheck === undefined ? cur.dayCheck : body.dayCheck);
8286
8380
  const bpr = body.placeRepos && typeof body.placeRepos === 'object' && !Array.isArray(body.placeRepos) ? body.placeRepos : null;
8287
8381
  const placeRepos = bpr ? { ...cur.placeRepos, ...Object.fromEntries(Object.entries(bpr).map(([k, v]) => [k, v && typeof v === 'object' && !Array.isArray(v) ? { ...((cur.placeRepos || {})[k] || {}), ...v } : v])) } : cur.placeRepos;
8288
- const n = proactiveLib.normalizeSettings({ ...cur, ...body, placeRepos, triggers: { ...cur.triggers, ...(body.triggers || {}) }, briefSources: { ...cur.briefSources, ...(body.briefSources || {}) }, scopes, dayCheck });
8382
+ const n = proactiveLib.normalizeSettings({ ...cur, ...body, placeRepos, triggers: { ...cur.triggers, ...(body.triggers || {}) }, briefSources: { ...cur.briefSources, ...(body.briefSources || {}) }, about: { ...(cur.about || {}), ...(body.about && typeof body.about === 'object' && !Array.isArray(body.about) ? body.about : {}) }, scopes, dayCheck });
8289
8383
  if (!n.ok) return json(res, { ok: false, error: n.error }, 400);
8290
8384
  try { fs.mkdirSync(STATE, { recursive: true }); fs.writeFileSync(PRO_SETTINGS(), JSON.stringify(n.settings) + '\n'); }
8291
8385
  catch (e) { warnWriteFail('proactive-settings.json', e); return json(res, { ok: false, error: 'could not save — nothing changed' }, 500); }
@@ -8688,10 +8782,14 @@ async function handleRequest(req, res) {
8688
8782
  // meeting-notes Run 1: the Notes routes keep notes in the per-user runtime directory and start the agent-summary session.
8689
8783
  if (await routeModules.dispatch(req, res, url, { json, readBody, getWs, invalidateNeedsAgg, boardCache, consoleState, stateDirFor, sessionList, initiativeIndex,
8690
8784
  runtimeDir: STATE, spawnSession, jevModelOpts, sendToSession,
8785
+ // workspace-headless-jobs R6: is this session a headless run that is still working?
8786
+ headlessBusy: (id) => { const x = sessions.get(String(id || '')); return !!(x && x.headless && x.child); },
8691
8787
  // initiative my-work: every registered repository, the day runs, and the two presses that start sessions
8692
8788
  workspaceList: () => [...workspaces.values()], readDayRun, myWorkBrief, myWorkRead,
8693
8789
  // workspace-data-freshness: the source table, the facts rebuild and Refresh now's skill sessions
8694
- freshness, factsRefresh: (root) => factsAsync.refresh(root, { force: true }), startSkill: startSkillSession })) return;
8790
+ freshness, factsRefresh: (root) => factsAsync.refresh(root, { force: true }), startSkill: startSkillSession,
8791
+ // c3-initiative-boards: the PMOS checkout this server is embedded in, whose default board keeps its derived ladder
8792
+ selfRoot: EMBEDDED_SELF ? SELF_ROOT : null })) return;
8695
8793
  res.writeHead(404); res.end('not found');
8696
8794
  }
8697
8795
 
@@ -8979,7 +9077,8 @@ wss.on('connection', (ws) => {
8979
9077
  ws.send(JSON.stringify({ t: 'created', id: s.id }));
8980
9078
  } else if (m.t === 'kill') {
8981
9079
  const s = sessions.get(m.id); if (!s) return;
8982
- if (s.pty) {
9080
+ if (s.child) stopHeadless(s); // a headless run → stop its process (its close handler flips state)
9081
+ else if (s.pty) {
8983
9082
  // A tmux client's .kill() only DETACHES — the process would live on and we'd reattach.
8984
9083
  // To actually end a live session, kill the tmux session itself (onExit then flips state).
8985
9084
  if (s.tmux) tmuxKillSession(s.tmux);
@@ -22,7 +22,7 @@ Rules that always hold:
22
22
  7. AT MOST 30 ITEMS in the stored brief after the merge. Resolve what is done before you add. The workspace refuses a merge that would pass 30.
23
23
 
24
24
  Procedure:
25
- 1. Run `pmos proactive day`. It shows the stage, the tasks, and the check state (when the last check was, and why this one runs). Run `pmos proactive brief` to read the stored brief: its items with their ids, its events with their ids, and its digests. Run `pmos proactive settings` for the sources, the scopes and the websites. Run `pmos proactive goals` for the goal ids.
25
+ 1. Run `pmos proactive day`. It shows the stage, the tasks, and the check state (when the last check was, and why this one runs). Run `pmos proactive brief` to read the stored brief: its items with their ids, its events with their ids, and its digests. Run `pmos proactive settings` for the sources, the scopes, the websites and, when it prints them, the PM's role, work and job description: use those to judge what matters to this PM. Run `pmos proactive goals` for the goal ids.
26
26
  2. Read what changed, from the sources that are on and reachable:
27
27
  - Calendar: today's events. Note a new event, a moved one, a cancelled one, and a new link or agenda in one.
28
28
  - Mentions of the PM since the last check, in Slack, Jira and Confluence, in every place that is not ignored. Keep where, who, the link to the conversation itself and a short paraphrase of what is asked. To tie a Jira issue to an initiative, run `pmos jira which <KEY>` (it reads local files only) and use the initiative it names. A status change of a ticket that `pmos jira which` names is not an item: the Jira status check shows it on the initiative's Jira tickets tab.
@@ -15,17 +15,19 @@ output:
15
15
  Rules that always hold:
16
16
  1. READ-ONLY on Jira. Use only the Jira search tool. Never create, edit, transition, assign, comment on, link, close or delete an issue. Never write to Confluence or to any other source.
17
17
  2. THE ONLY WRITE is `pmos jira sync`, and `pmos people record` once at the end (step 5). Never edit a repository file. Never run `pmos item`. Never write a ledger by hand. Never commit; the PM's Sync carries the files.
18
- 3. LIGHT ON CONTENT. Ask Jira only for these fields: summary, status, the sprint field, subtasks, the Flagged field, and the display names of the assignee and the reporter. Never ask for or copy a description, a comment, an attachment, an email address, any other detail of a person, or a token.
18
+ 3. LIGHT ON CONTENT. Ask Jira only for these fields: summary, status, the sprint field, subtasks, the Flagged field, the story points field, the resolution date, and the display names of the assignee and the reporter. Never ask for or copy a description, a comment, an attachment, an email address, any other detail of a person, or a token.
19
19
  4. TEMPORARY FILES OUTSIDE THE REPOSITORY. Save each search result in this session's scratch folder, never in the repository, and delete it after the sync.
20
20
  5. NOTHING INVENTED. Give `pmos jira sync` exactly what Jira returned. If Jira cannot be reached, list the initiative under "Not reached" and write nothing for it.
21
21
 
22
22
  Procedure:
23
23
  1. If the task names one initiative ("This check: initiative …"), check that one only: run `pmos jira query <initiative>`. Otherwise run `pmos jira query`: each line is an initiative id, a tab and the Jira search (JQL) to run for it. If no initiative has a ticket in Jira, say so in one line and stop.
24
- 2. For each initiative, run its search with the Jira search tool and the fields in rule 3. The sprint is usually a custom field named "Sprint" (often `customfield_10020`); ask for it by name. Fetch every page.
24
+ 2. For each initiative, run its search with the Jira search tool and the fields in rule 3. The sprint is usually a custom field named "Sprint" (often `customfield_10020`); ask for it by name. Story points are also a custom field, named "Story Points" or "Story point estimate" (often `customfield_10016`); ask for it by name. Fetch every page.
25
25
  3. Save the result as JSON in the scratch folder, for example `<scratch>/jira-<initiative>.json`, as a list with one object per issue:
26
- `{"key", "summary", "status", "category", "sprint", "subtasks", "flagged", "reason", "url"}`
26
+ `{"key", "summary", "status", "category", "sprint", "subtasks", "points", "resolved", "flagged", "reason", "url"}`
27
27
  - `category`: the status category, `new`, `indeterminate` or `done`.
28
- - `sprint`: `{"name": "Sprint 42", "state": "active"}` for the active sprint, `"future"` for a planned one, or `null` when the issue is in the backlog.
28
+ - `sprint`: `{"id": "42", "name": "Sprint 42", "state": "active", "start": "2026-10-01", "end": "2026-10-14"}` for the active sprint, `"future"` for a planned one, or `null` when the issue is in the backlog. Copy `id`, `start`, `end` and `completed` (a closed sprint's completion date) from Jira's sprint field; leave out any that Jira does not give.
29
+ - `points`: the story points as a number, or `null` when none are set.
30
+ - `resolved`: the resolution date as `YYYY-MM-DD`, or `null` when the issue is not resolved.
29
31
  - `subtasks`: `{"done": 2, "total": 5}`.
30
32
  - `flagged`: `true` when the issue is flagged; `reason`: the flag's comment or the blocking status, in a few words.
31
33
  - `url`: the issue's browse link, `https://<site>.atlassian.net/browse/<KEY>`.
@@ -17,7 +17,7 @@ output:
17
17
 
18
18
  Rules that always hold:
19
19
  1. READ-ONLY on every source. Never send, reply, forward, post, react, comment, create, edit, accept, decline, transition, label, archive, mark read or delete anything in mail, Slack, Jira, Confluence, GitHub, Drive, Figma, a website, calendar, Wispr Flow or any other source.
20
- 2. SETTINGS DECIDE THE SOURCES AND THE SCOPES. Run `pmos proactive settings`. Read a source only if its line under "brief sources" says on. A source that is off is reported as "off". Mail is off unless the PM turned it on. The same command lists the websites and, for Slack, Jira and Confluence, the scope of each channel, project and space (see step 2).
20
+ 2. SETTINGS DECIDE THE SOURCES AND THE SCOPES. Run `pmos proactive settings`. Read a source only if its line under "brief sources" says on. A source that is off is reported as "off". Mail is off unless the PM turned it on. When the same command prints "about the PM" and "job description", use the PM's role, work and job description to judge what matters to them and to word why an item is theirs. Never copy that text into the brief. The same command lists the websites and, for Slack, Jira and Confluence, the scope of each channel, project and space (see step 2).
21
21
  3. LIGHT ON CONTENT. Never copy a message body, an attachment, a token or a credential. Write what a message says in your own words, in the limits of the field. Skip anything marked private, personal, confidential or HR. At most one short evidence line per item.
22
22
  4. NOTHING INVENTED. A source you cannot reach in this session is reported as "not-reached". If nothing is waiting, submit a brief with no items. An honest short brief is correct.
23
23
  5. THE ONLY WRITE is `pmos proactive brief submit`, with one exception: when `pmos people settings` prints `calendar names: on`, step 6 records who attends the PM's meetings with `pmos people record`. When it prints `calendar names: off`, record nobody from the calendar and leave "with" out of the memory events. Do not edit repository files and do not commit.
@@ -8,6 +8,7 @@
8
8
  "test.js"
9
9
  ],
10
10
  "modules": [
11
+ "stage-tools.test.js",
11
12
  "report-template-cadence.test.js",
12
13
  "proactive.test.js",
13
14
  "goals.test.js",
@@ -26,6 +27,7 @@
26
27
  "jobs-to-be-done.test.js",
27
28
  "pm-items-routes.test.js",
28
29
  "work-focus.test.js",
30
+ "work-evidence.test.js",
29
31
  "brief-page.test.js",
30
32
  "item-steps.test.js",
31
33
  "status-facts-panel.test.js",
@@ -65,7 +67,9 @@
65
67
  "glossary.test.js",
66
68
  "c3-daypart.test.js",
67
69
  "briefing-icons.test.js",
68
- "needs-overdue.test.js"
70
+ "needs-overdue.test.js",
71
+ "briefing-scope.test.js",
72
+ "settings-store.test.js"
69
73
  ],
70
74
  "panels": [
71
75
  "pm-shell-test.js",
@@ -119,13 +123,15 @@
119
123
  "pm-live-notes-test.js",
120
124
  "pm-notes-menu-test.js",
121
125
  "pm-record-control-test.js",
126
+ "pm-c3-record-test.js",
122
127
  "pm-r30-test.js",
123
128
  "pm-breakdown-test.js",
124
129
  "pm-breakdown-phone-test.js",
125
130
  "first-paint-count.test.js",
126
131
  "pm-card-layout-test.js",
127
132
  "pm-portfolio-inline-test.js",
128
- "pm-brief-page-test.js"
133
+ "pm-brief-page-test.js",
134
+ "c3-settings-test.js"
129
135
  ],
130
136
  "ui": [
131
137
  "ui-test.js"
@@ -149,6 +155,7 @@
149
155
  "jobs-to-be-done.test.js": 1,
150
156
  "pm-items-routes.test.js": 1,
151
157
  "work-focus.test.js": 1,
158
+ "work-evidence.test.js": 1,
152
159
  "brief-page.test.js": 1,
153
160
  "pm-brief-page-test.js": 30,
154
161
  "item-steps.test.js": 1,
@@ -234,6 +241,7 @@
234
241
  "pm-live-notes-test.js": 24,
235
242
  "pm-notes-menu-test.js": 10,
236
243
  "pm-record-control-test.js": 12,
244
+ "pm-c3-record-test.js": 12,
237
245
  "pm-r30-test.js": 4,
238
246
  "ui-test.js": 30,
239
247
  "plan-items.test.js": 1,
@@ -247,8 +255,11 @@
247
255
  "c3-daypart.test.js": 1,
248
256
  "briefing-icons.test.js": 1,
249
257
  "needs-overdue.test.js": 1,
258
+ "briefing-scope.test.js": 1,
250
259
  "pm-card-layout-test.js": 20,
251
- "pm-portfolio-inline-test.js": 15
260
+ "pm-portfolio-inline-test.js": 15,
261
+ "settings-store.test.js": 1,
262
+ "c3-settings-test.js": 25
252
263
  },
253
264
  "_comment_seconds": "Measured wall time of each suite run alone (4-CPU sandbox, 2026-10-05). Only the ORDER uses it: the slowest suites start first, and --shard deals suites to the least-loaded shard so the shards finish together. A wrong or missing number never skips a suite; a missing one counts as 10."
254
265
  }
@@ -0,0 +1,9 @@
1
+ {
2
+ "_comment": "c3-ib-items: the Backlog, Board and Timeline tabs of a board on the console3 Initiatives page (Jira items and PMOS tasks mixed, shared filters, reschedule), in demo mode (browser).",
3
+ "panels": [
4
+ "c3-ib-items-test.js"
5
+ ],
6
+ "seconds": {
7
+ "c3-ib-items-test.js": 25
8
+ }
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "_comment": "c3-ib-more: the Releases, Docs and Sprints tabs of a board, the six tabs of an initiative and the stage records on its Overview (problem, RICE, launch, kill, learnings, burndown; quiet on a 404) on the console3 Initiatives page, in demo mode (browser).",
3
+ "panels": [
4
+ "c3-ib-more-test.js"
5
+ ],
6
+ "seconds": {
7
+ "c3-ib-more-test.js": 25
8
+ }
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "_comment": "c3-ib-triage-backlog: the Triage and Backlog tabs of a board on the console3 Initiatives page, in demo mode (browser), with the B2 timings.",
3
+ "panels": [
4
+ "c3-ib-triage-backlog-test.js"
5
+ ],
6
+ "seconds": {
7
+ "c3-ib-triage-backlog-test.js": 20
8
+ }
9
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "_comment": "c3-initiative-boards: the pure board model (scripts/boards.js), the /api/boards routes on a scratch server with synthetic repositories, and the console3 Initiatives page (Portfolio, Board, Initiative, workflow editor) in demo mode (browser).",
3
+ "modules": ["boards-model.test.js", "routes-boards.test.js"],
4
+ "panels": ["c3-initiatives-test.js"],
5
+ "seconds": { "boards-model.test.js": 1, "routes-boards.test.js": 6, "c3-initiatives-test.js": 12 }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "_comment": "Initiative calendar-tidy-up: GET /api/calendar-views tidy-up suggestions (rules in code, read only) and the Calendar page's Tidy-up chip and panel. Demo fixture /c3.html?demo=1.",
3
+ "modules": ["calendar-tidy.test.js"],
4
+ "panels": ["pm-calendar-tidy-test.js"],
5
+ "seconds": { "calendar-tidy.test.js": 2, "pm-calendar-tidy-test.js": 30 }
6
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "_comment": "Initiative console3-briefing-parity: the Briefing page browser suite.",
3
+ "panels": [
4
+ "c3-briefing-test.js"
5
+ ],
6
+ "seconds": {
7
+ "c3-briefing-test.js": 40
8
+ }
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "_comment": "Initiative console3-foundation: the shell foundation browser suite.",
3
+ "panels": [
4
+ "c3-foundation-test.js"
5
+ ],
6
+ "seconds": {
7
+ "c3-foundation-test.js": 40
8
+ }
9
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
- "_comment": "console3 Reports dashboards: the server half (library, defaults, scope filter, layout store, page-press layout route) and the browser suite (scopes, edit, share, library, Drafts tab).",
3
- "modules": ["report-dashboards.test.js"],
2
+ "_comment": "console3 Reports dashboards: the server half (library, defaults, scope filter, layout store, page-press layout route), the outbox discard route Drafts uses, and the browser suite (scopes, edit, share, library, Drafts tab).",
3
+ "modules": ["report-dashboards.test.js", "outbox-discard.test.js"],
4
4
  "panels": ["c3-reports-test.js"],
5
- "seconds": { "report-dashboards.test.js": 3, "c3-reports-test.js": 30 }
5
+ "seconds": { "report-dashboards.test.js": 3, "outbox-discard.test.js": 2, "c3-reports-test.js": 30 }
6
6
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "_comment": "meeting-notes Runs 1 to 10: the Notes engine and routes (notes.test.js), local Whisper, line labels and repository checks (notes-listen.test.js), the final pass after Stop and the Wispr Flow fetch (notes-final.test.js), Qwen3-ASR kept loaded (notes-qwen.test.js), and the Notes view in a browser (pm-notes-test.js), the echo canceller for the replay (notes-echo.test.js, notes-audio-quality Run 2) and the optional cleaned copy of the replay (notes-polish.test.js, Run 3) and the 24 kHz replay (notes-hires.test.js, Run 4), plus the note pop-up patched in place while it listens (pm-note-dock-test.js).",
2
+ "_comment": "meeting-notes Runs 1 to 10: the Notes engine and routes (notes.test.js), local Whisper, line labels and repository checks (notes-listen.test.js), the final pass after Stop and the Wispr Flow fetch (notes-final.test.js), Qwen3-ASR kept loaded (notes-qwen.test.js), and the Notes view in a browser (pm-notes-test.js), the echo canceller for the replay (notes-echo.test.js, notes-audio-quality Run 2) and the optional cleaned copy of the replay (notes-polish.test.js, Run 3) and the 24 kHz replay (notes-hires.test.js, Run 4), plus the note pop-up patched in place while it listens (pm-note-dock-test.js). notes-recording-reliability adds the duplication fixture (notes-duplication.test.js): duplicated audio and transcript across retry, restart, two tabs and a late chunk, and the live guards. notes-screen-capture adds the screenshots a note keeps of the shared screen (notes-screen.test.js): who may leave a shot, which pictures, listing and serving, deleting, and the optional on-screen text. notes-live-settle adds the replay of a kept meeting and its report (notes-replay.test.js): lines said twice, the live lines' lag, and the settle tier's busy share. notes-screen-capture also adds the screenshots of the shared screen in a browser (pm-notes-screen-test.js).",
3
3
  "modules": [
4
4
  "notes.test.js",
5
5
  "notes-listen.test.js",
@@ -7,11 +7,15 @@
7
7
  "notes-qwen.test.js",
8
8
  "notes-echo.test.js",
9
9
  "notes-polish.test.js",
10
- "notes-hires.test.js"
10
+ "notes-hires.test.js",
11
+ "notes-duplication.test.js",
12
+ "notes-screen.test.js",
13
+ "notes-replay.test.js"
11
14
  ],
12
15
  "panels": [
13
16
  "pm-notes-test.js",
14
- "pm-note-dock-test.js"
17
+ "pm-note-dock-test.js",
18
+ "pm-notes-screen-test.js"
15
19
  ],
16
20
  "seconds": {
17
21
  "notes.test.js": 12,
@@ -22,6 +26,10 @@
22
26
  "pm-note-dock-test.js": 20,
23
27
  "notes-echo.test.js": 25,
24
28
  "notes-polish.test.js": 3,
25
- "notes-hires.test.js": 10
29
+ "notes-hires.test.js": 10,
30
+ "notes-duplication.test.js": 25,
31
+ "pm-notes-screen-test.js": 60,
32
+ "notes-screen.test.js": 6,
33
+ "notes-replay.test.js": 20
26
34
  }
27
35
  }
@@ -0,0 +1,5 @@
1
+ {
2
+ "_comment": "Initiative workspace-headless-jobs: background agent jobs run as `claude -p` with no terminal pane (lib/headless.js and spawnSession's headless path), against a stub claude.",
3
+ "modules": ["headless.test.js"],
4
+ "seconds": { "headless.test.js": 12 }
5
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "_comment": "workspace-package-script-closure: the built create-pmos package, unpacked, loads every route module, runs pmos calendar status and reads facts with no repo scripts/ beside it.",
3
+ "modules": ["package-closure.test.js"],
4
+ "panels": [],
5
+ "seconds": { "package-closure.test.js": 60 }
6
+ }
@@ -1,91 +0,0 @@
1
- 'use strict';
2
- /* actor.js — who a write is, read from the CALL PATH, never from a field (pm-os-absorb, PRD R23 and R25).
3
- *
4
- * fromUi() → { by: 'pm:wawan', via: 'ui' } the page, after the route's own checks
5
- * fromCli({isTTY, env}) → { by, via: 'cli' } a bound agent session, else a terminal (the PM), else unknown
6
- * isPm(actor) · isAgent(actor) · sessionOf(actor)
7
- * pageName(by) → 'agent:sess-<n>' for 'agent:sess-<n>.<8hex>', else `by` unchanged
8
- *
9
- * F5: a ledger line's `by` carries the session token; the page must never see it. Every GET route and every needs
10
- * provider maps each `by` it answers through pageName (proposals, moves, item owners, report attributions).
11
- *
12
- * The placements, measurements and issues writers take their actor from here, so the four ledgers attribute one way.
13
- * A caller never passes `by` or `via`: a writer that accepted them would make attribution something a caller chooses.
14
- *
15
- * THE SESSION-TOKEN MAP (Run 2, slice 2A). server.js gives each agent session a token, `sess-<n>.<8 hex>`, in its PTY
16
- * environment as PMOS_SESSION. The token itself is never written to disk or sent to the page (D08, F5). server.js
17
- * persists its SHA-256 (`tokenHash`) in the session's own meta file, so after a restart the map still answers which
18
- * session a token belongs to and which initiative that session is bound to. The map is read from the files each time
19
- * (no cache, D33), so a CLI with no server reads the same answer as the server.
20
- *
21
- * tokenHash(token) → 64 hex characters, or null for a value that is not a token
22
- * sessionForToken(token, { sessDir }) → { session, initiative } | null
23
- *
24
- * HONEST LIMIT. A token is an attribution, not a credential: a process that unsets PMOS_SESSION is no longer an agent
25
- * to fromCli. pm-guard.js requirePmCli asks such a caller to type the id of a PM-only act (X12); CI detection is the
26
- * backstop for the rest.
27
- */
28
- const fs = require('fs');
29
- const path = require('path');
30
- const crypto = require('crypto');
31
-
32
- const PM_SLUG = 'wawan';
33
- const SESSION_RE = /^sess-\d+\.[0-9a-f]{8}$/;
34
- const BY_RE = /^(?:pm:[a-z0-9_-]{1,40}|agent:unknown|agent:sess-\d+\.[0-9a-f]{8})$/;
35
- const VIA = ['ui', 'cli'];
36
-
37
- const PM = Object.freeze({ by: `pm:${PM_SLUG}`, via: 'ui' });
38
-
39
- /** The page. Call it only after the route has run its checks (pm-guard.js requirePmPress for a PM-only act). */
40
- function fromUi() { return { by: PM.by, via: 'ui' }; }
41
-
42
- /**
43
- * A `pmos <verb>` call. A bound agent session FIRST, by its token: an agent that runs its commands in a PTY (or wraps
44
- * them with `script`) still has a terminal, and a terminal must not turn its writes into the PM's. Only agent sessions
45
- * get PMOS_SESSION (server.js spawnSession), so the PM's own shell is a terminal with no token: the PM. Else unknown.
46
- */
47
- function fromCli({ isTTY = !!(process.stdin.isTTY && process.stdout.isTTY), env = process.env } = {}) {
48
- const tok = env && env.PMOS_SESSION;
49
- if (typeof tok === 'string' && SESSION_RE.test(tok)) return { by: `agent:${tok}`, via: 'cli' };
50
- if (isTTY) return { by: `pm:${PM_SLUG}`, via: 'cli' };
51
- return { by: 'agent:unknown', via: 'cli' };
52
- }
53
-
54
- const valid = (a) => !!a && typeof a.by === 'string' && BY_RE.test(a.by) && VIA.includes(a.via);
55
- const isPm = (a) => valid(a) && a.by.startsWith('pm:');
56
- const isAgent = (a) => valid(a) && a.by.startsWith('agent:');
57
- /** The session token of an agent actor, or null. */
58
- const sessionOf = (a) => (isAgent(a) && a.by !== 'agent:unknown' ? a.by.slice('agent:'.length) : null);
59
-
60
- /** The name the page may show. Strips the session token's secret suffix; anything else passes through. */
61
- function pageName(by) {
62
- if (typeof by !== 'string') return by;
63
- const m = /^(agent:sess-\d+)\.[0-9a-f]{8}$/.exec(by);
64
- return m ? m[1] : by;
65
- }
66
-
67
- /** The SHA-256 of a session token. The map stores this, never the token. */
68
- function tokenHash(token) {
69
- if (typeof token !== 'string' || !SESSION_RE.test(token)) return null;
70
- return crypto.createHash('sha256').update(token).digest('hex');
71
- }
72
-
73
- /**
74
- * The session a token belongs to, read from the session meta files server.js persists (`<sessDir>/<id>.json`). The id
75
- * part of the token names the file, and the stored hash must match: a token with the right id and a wrong suffix is not
76
- * that session. Null when there is no such session or no hash on record. `plan_item` is the item Hand to agent gave the
77
- * session (null for any other session).
78
- */
79
- function sessionForToken(token, { sessDir } = {}) {
80
- const h = tokenHash(token);
81
- if (!h || typeof sessDir !== 'string' || !sessDir) return null;
82
- const id = token.slice(0, token.indexOf('.'));
83
- let meta;
84
- try { meta = JSON.parse(fs.readFileSync(path.join(sessDir, id + '.json'), 'utf8')); } catch { return null; }
85
- if (!meta || meta.id !== id || typeof meta.tokenHash !== 'string' || meta.tokenHash.length !== h.length) return null;
86
- if (!crypto.timingSafeEqual(Buffer.from(meta.tokenHash), Buffer.from(h))) return null;
87
- const str = (v) => (typeof v === 'string' && v ? v : null);
88
- return { session: id, initiative: str(meta.initiative), plan_item: str(meta.plan_item) };
89
- }
90
-
91
- module.exports = { tokenHash, sessionForToken, PM_SLUG, SESSION_RE, BY_RE, VIA, fromUi, fromCli, valid, isPm, isAgent, sessionOf, pageName };