create-pmos 0.123.0 → 0.125.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 (127) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +2 -2
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/scripts/nine-stages.js +81 -0
  6. package/payload/scripts/workflow-state.js +2 -0
  7. package/payload/state/discoveries.md +1 -1
  8. package/payload/state/log.md +1 -1
  9. package/workspace/c3-one-home-test.js +131 -0
  10. package/workspace/fixtures/initiatives-api/list.json +171 -3
  11. package/workspace/fixtures/initiatives-api/roadmap.json +228 -4
  12. package/workspace/fixtures/initiatives-api/sheet.json +57 -1
  13. package/workspace/fixtures/one-home-baseline.json +83 -0
  14. package/workspace/guide/layout.md +2 -0
  15. package/workspace/guide/notes.md +1 -0
  16. package/workspace/lib/engine/nine-stages.js +81 -0
  17. package/workspace/lib/engine/workflow-state.js +2 -0
  18. package/workspace/lib/facts-async.js +51 -0
  19. package/workspace/lib/item-steps.js +52 -0
  20. package/workspace/lib/jira-status.js +8 -3
  21. package/workspace/lib/jobs-to-be-done.js +52 -0
  22. package/workspace/lib/outbox-store.js +87 -0
  23. package/workspace/lib/pm-items.js +115 -0
  24. package/workspace/lib/route-modules.js +21 -0
  25. package/workspace/lib/routes-briefing.js +100 -0
  26. package/workspace/lib/routes-c3-init.js +53 -0
  27. package/workspace/lib/routes-c3-life.js +119 -0
  28. package/workspace/lib/routes-c3-strategy.js +169 -0
  29. package/workspace/lib/routes-c3-timeline.js +191 -0
  30. package/workspace/lib/routes-calendar-import.js +60 -0
  31. package/workspace/lib/routes-calendar-views.js +61 -0
  32. package/workspace/lib/routes-doc-health.js +79 -0
  33. package/workspace/lib/routes-doc-sheet.js +169 -0
  34. package/workspace/lib/routes-forecast.js +85 -0
  35. package/workspace/lib/routes-hooks.js +121 -0
  36. package/workspace/lib/routes-initiatives.js +3 -0
  37. package/workspace/lib/routes-intake.js +24 -0
  38. package/workspace/lib/routes-outbox.js +97 -0
  39. package/workspace/lib/routes-pm-items.js +216 -0
  40. package/workspace/lib/routes-records.js +61 -0
  41. package/workspace/lib/routes-releases.js +129 -0
  42. package/workspace/lib/routes-report-facts.js +84 -0
  43. package/workspace/lib/routes-report-families.js +32 -0
  44. package/workspace/lib/routes-roadmap-ops.js +141 -0
  45. package/workspace/lib/routes-runs.js +119 -0
  46. package/workspace/lib/routes-signals.js +50 -0
  47. package/workspace/lib/run-answers.js +69 -0
  48. package/workspace/package-lock.json +2 -2
  49. package/workspace/package.json +1 -1
  50. package/workspace/public/c3.html +26 -0
  51. package/workspace/public/pm/briefing-deck.js +415 -0
  52. package/workspace/public/pm/briefing-model.js +176 -0
  53. package/workspace/public/pm/c3-breakdown.js +240 -0
  54. package/workspace/public/pm/c3-brief.js +278 -0
  55. package/workspace/public/pm/c3-cal-strategy.js +528 -0
  56. package/workspace/public/pm/c3-case.js +173 -0
  57. package/workspace/public/pm/c3-demo-brief-ix.js +350 -0
  58. package/workspace/public/pm/c3-demo-cal-ix.js +74 -0
  59. package/workspace/public/pm/c3-demo-cal2.js +66 -0
  60. package/workspace/public/pm/c3-demo-cal3.js +173 -0
  61. package/workspace/public/pm/c3-demo-data.js +560 -0
  62. package/workspace/public/pm/c3-demo-init.js +373 -0
  63. package/workspace/public/pm/c3-demo-life.js +77 -0
  64. package/workspace/public/pm/c3-demo-misc2.js +46 -0
  65. package/workspace/public/pm/c3-demo-plan2.js +30 -0
  66. package/workspace/public/pm/c3-demo-rep-ix.js +126 -0
  67. package/workspace/public/pm/c3-demo-rep2.js +92 -0
  68. package/workspace/public/pm/c3-demo-term.js +153 -0
  69. package/workspace/public/pm/c3-demo-time.js +49 -0
  70. package/workspace/public/pm/c3-demo-work-ix.js +80 -0
  71. package/workspace/public/pm/c3-demo-work2.js +13 -0
  72. package/workspace/public/pm/c3-demo.js +113 -0
  73. package/workspace/public/pm/c3-doc-sheet.js +314 -0
  74. package/workspace/public/pm/c3-dock.js +180 -0
  75. package/workspace/public/pm/c3-docs.js +290 -0
  76. package/workspace/public/pm/c3-expand.js +96 -0
  77. package/workspace/public/pm/c3-init-extras.js +170 -0
  78. package/workspace/public/pm/c3-lifecycle-panels.js +476 -0
  79. package/workspace/public/pm/c3-misc-panels.js +89 -0
  80. package/workspace/public/pm/c3-plan-panels.js +163 -0
  81. package/workspace/public/pm/c3-plan.js +192 -0
  82. package/workspace/public/pm/c3-roadmap.js +249 -0
  83. package/workspace/public/pm/c3-sheet.js +107 -0
  84. package/workspace/public/pm/c3-shell.js +637 -0
  85. package/workspace/public/pm/c3-signals.js +196 -0
  86. package/workspace/public/pm/c3-strategy-panels.js +45 -0
  87. package/workspace/public/pm/c3-swipe.js +138 -0
  88. package/workspace/public/pm/c3-terminal-data.js +26 -0
  89. package/workspace/public/pm/c3-terminal-edge.js +91 -0
  90. package/workspace/public/pm/c3-terminal-icons.js +41 -0
  91. package/workspace/public/pm/c3-terminal-results.js +141 -0
  92. package/workspace/public/pm/c3-terminal.js +52 -0
  93. package/workspace/public/pm/c3-tickets.js +163 -0
  94. package/workspace/public/pm/c3-timeline.js +201 -0
  95. package/workspace/public/pm/c3-toast.js +16 -0
  96. package/workspace/public/pm/calendar-views.js +1015 -0
  97. package/workspace/public/pm/console3-lift.md +220 -0
  98. package/workspace/public/pm/console3.css +1480 -0
  99. package/workspace/public/pm/dest-initiatives.js +16 -3
  100. package/workspace/public/pm/dest-mywork.js +8 -1
  101. package/workspace/public/pm/dest-notes.js +25 -3
  102. package/workspace/public/pm/dest-reports.js +47 -1
  103. package/workspace/public/pm/dest-today.js +55 -1
  104. package/workspace/public/pm/doc-health-panel.js +110 -0
  105. package/workspace/public/pm/doc-rail.js +9 -3
  106. package/workspace/public/pm/lifecycle-lens-model.js +228 -0
  107. package/workspace/public/pm/lifecycle-lens.js +261 -0
  108. package/workspace/public/pm/new-initiative-sheet.js +116 -21
  109. package/workspace/public/pm/outbox-panel.js +125 -0
  110. package/workspace/public/pm/pm-work-view.js +919 -0
  111. package/workspace/public/pm/releases-view.js +248 -0
  112. package/workspace/public/pm/report-families-view.js +503 -0
  113. package/workspace/public/pm/run-card.js +244 -0
  114. package/workspace/public/pm/sessions-panel.js +149 -0
  115. package/workspace/public/pm/shell-bell.js +162 -0
  116. package/workspace/public/pm/shell-model.js +83 -0
  117. package/workspace/public/pm/shell-phone.js +124 -0
  118. package/workspace/public/pm/shell-readaloud.js +53 -0
  119. package/workspace/public/pm/shell-recording.js +110 -0
  120. package/workspace/public/pm/shell-ticker.js +90 -0
  121. package/workspace/public/pm/slide-confirm.js +163 -0
  122. package/workspace/public/pm/timeline-forecast-view.js +164 -0
  123. package/workspace/public/pm/today-day.js +8 -2
  124. package/workspace/public/pm/tokens.css +1 -0
  125. package/workspace/server.js +5 -2
  126. package/workspace/test-files.json +8 -0
  127. package/workspace/test-lists/browser-suites-green.json +5 -0
@@ -0,0 +1,196 @@
1
+ /* pm/c3-signals.js — the Signals sub-view of Initiatives on /c3.html (console3.html SIGS line 2861, sigRow 2881, viewLeft and viewRight at lines
2
+ * 3833 and 3857, creepSVG and driftSVG at 3715 and 3724, the "What the agent watches" card).
3
+ * REAL: GET /api/signals ({ rules, readings (latest per metric), trips, open }) and GET /api/records/kr-metrics (KR rows: metric, baseline, target,
4
+ * by). A rule is { id, metric, op, threshold, severity: watch|act|critical }; it is tripping when it is in `open`. The page draws one row per rule:
5
+ * the headline is the rule, the evidence is the latest reading, the right side is open or not tripped. The ledger keeps only the latest reading
6
+ * per metric, so a real row has no sparkline. Severity maps to the mockup's chips: critical Pivot, act Drift, watch Watch.
7
+ * DEMO ONLY (optional fields the real rule does not carry): title, evidence, action, when, spark, sev (up for upside), initiative, and the response's
8
+ * head and charts (scope creep, delay). They are drawn when present; real mode never shows them.
9
+ * Writes (page presses, through pmPost): POST /api/signals/rule and POST /api/signals/reading, from the two small forms of the empty state and the
10
+ * rules card. A press the server refuses shows its reason. No dialog is used. */
11
+ import { el } from './dom.js';
12
+ import { pmGet, pmPost } from './http.js';
13
+ import { c3ic } from './pm-work-view.js';
14
+ import { makeView, frame, card, listCard, emptyRow, short, wireRow, caseNode, markDone, say } from './c3-init-extras.js';
15
+
16
+ const NS = 'http://www.w3.org/2000/svg';
17
+ const sv = (tag, a, ...kids) => { const n = document.createElementNS(NS, tag); for (const k of Object.keys(a || {})) if (a[k] != null) n.setAttribute(k, a[k]); for (const c of kids) if (c) n.appendChild(c); return n; };
18
+ const svt = (x, y, t, a) => { const n = sv('text', { x, y, ...(a || {}) }); n.textContent = t; return n; };
19
+ const SEV = { watch: ['Watch', 'var(--acc)', 'eye'], drift: ['Drift', 'var(--warning)', 'clock'], pivot: ['Pivot', 'var(--destructive)', 'alert'], up: ['Upside', 'var(--success)', 'up'] };
20
+ const ORDER = ['pivot', 'drift', 'watch', 'up'];
21
+ const OF_SEV = { critical: 'pivot', act: 'drift', watch: 'watch' };
22
+
23
+ function spark(a, c) {
24
+ const W = 64; const H = 26; const mn = Math.min(...a); const mx = Math.max(...a); const X = (i) => 3 + i * (W - 6) / (a.length - 1); const Y = (v) => H - 4 - (v - mn) / (mx - mn || 1) * (H - 8);
25
+ return sv('svg', { class: 'spk', viewBox: `0 0 ${W} ${H}`, 'aria-hidden': 'true' }, sv('polyline', { points: a.map((v, i) => X(i) + ',' + Y(v)).join(' '), fill: 'none', stroke: c, 'stroke-width': 2, 'stroke-linecap': 'round', 'stroke-linejoin': 'round' }), sv('circle', { cx: X(a.length - 1), cy: Y(a[a.length - 1]), r: 3, fill: c }));
26
+ }
27
+
28
+ /** The signals the page draws: one per rule of the ledger, with the demo's optional narrative when the rule carries it. */
29
+ export function signalsOf(j) {
30
+ const open = new Set(((j && j.open) || []).map((t) => t.rule_id));
31
+ const read = new Map(((j && j.readings) || []).map((r) => [r.metric, r]));
32
+ return ((j && j.rules) || []).map((r) => {
33
+ const rd = read.get(r.metric); const on = open.has(r.id);
34
+ return { id: r.id, sev: r.sev || OF_SEV[r.severity] || 'watch', on, initiative: r.initiative || '',
35
+ raw: r, read: rd || null, t: r.title || r.metric + ' ' + r.op + ' ' + r.threshold, ev: r.evidence || (rd ? 'Latest reading of ' + r.metric + ': ' + rd.value + ' (' + String(rd.at).slice(0, 10) + ').' : 'No reading of ' + r.metric + ' yet.'),
36
+ thr: r.action || 'Trips when ' + r.metric + ' ' + r.op + ' ' + r.threshold, ttt: r.when || (on ? 'tripped' : rd ? 'not tripped' : 'waiting for a reading'), sp: r.spark || null };
37
+ });
38
+ }
39
+ /** The count on the Signals button: the signals that drift or call for a pivot. */
40
+ export async function badge() {
41
+ const j = await pmGet('/api/signals');
42
+ if (!j || !j.ok) return null;
43
+ return signalsOf(j).filter((s) => s.sev === 'drift' || s.sev === 'pivot').length;
44
+ }
45
+
46
+ const NEXT_SEV = { watch: 'act', act: 'critical' };
47
+ const PREV_SEV = { critical: 'act', act: 'watch' };
48
+ const SEVW = { watch: 'Watch', act: 'Drift', critical: 'Pivot' };
49
+
50
+ /** The case file of a signal (console3.html SIGC, line 2864). A demo rule carries the mockup's own `case`; a real rule gets the rule, its latest reading and its
51
+ * trips from the ledger, and its moves are the ones the page may make: keep watching, or change the severity (a page press on /api/signals/rule). */
52
+ function caseOf(V, s, S) {
53
+ if (s.raw && s.raw.case) return s.raw.case;
54
+ const r = s.raw; const trips = ((S && S.trips) || []).filter((t) => t.rule_id === s.id); const rd = s.read;
55
+ const chg = [['target', 'The rule: ' + r.metric + ' ' + r.op + ' ' + r.threshold, 'rule'],
56
+ rd ? ['clock', 'Latest reading ' + rd.value + ' on ' + String(rd.at).slice(0, 10), 'ledger', s.on ? 'bad' : 'ok'] : ['clock', 'No reading of ' + r.metric + ' yet', 'ledger', 'warn']];
57
+ trips.slice(-3).forEach((t) => chg.push(['alert', 'Tripped ' + String(t.at).slice(0, 10) + (t.reading ? ' at ' + t.reading.value : ''), 'trip', 'bad']));
58
+ chg.push(['info', trips.length + ' trip' + (trips.length === 1 ? '' : 's') + ' recorded, severity ' + (SEVW[r.severity] || r.severity), 'ledger']);
59
+ const opts = [{ id: 'w', l: s.on ? 'Keep watching' : 'Keep watching it', rec: !s.on || !NEXT_SEV[r.severity], fx: [['eye', 'The rule stays as it is', 'ok']] }];
60
+ if (NEXT_SEV[r.severity]) opts.push({ id: 'up', l: 'Raise it to ' + SEVW[NEXT_SEV[r.severity]], rec: s.on, fx: [['alert', 'A trip is treated as more urgent', 'warn']] });
61
+ if (PREV_SEV[r.severity]) opts.push({ id: 'dn', l: 'Lower it to ' + SEVW[PREV_SEV[r.severity]], fx: [['clock', 'A trip is treated as less urgent', 'warn']] });
62
+ return { sit: s.on ? 'This rule is tripping now. ' + s.ev : 'This rule is not tripping. ' + s.ev, conf: s.on ? 74 : 80, chg, opts, cta: 'Slide to confirm', unsure: 'A rule is a number against a threshold. It does not know why the number moved.' };
63
+ }
64
+
65
+ async function setSeverity(V, s, sev) {
66
+ const r = s.raw; const was = r.severity;
67
+ const put = (v) => pmPost('/api/signals/rule', { id: r.id, metric: r.metric, op: r.op, threshold: r.threshold, severity: v });
68
+ const out = await put(sev);
69
+ if (!out || !out.ok) { say('Not saved: ' + ((out && out.error) || 'the server refused it.')); return null; }
70
+ return async () => { await put(was); reload(V); };
71
+ }
72
+
73
+ function row(V, s, S) {
74
+ const SV = SEV[s.sev] || SEV.watch; const key = 'sg-' + s.id;
75
+ const xp = el('div', { class: 'xp' }, el('div', { class: 'xi' }, caseNode(V, 'sig:' + s.id, caseOf(V, s, S), {
76
+ onConfirm: async (o) => {
77
+ if (s.raw && s.raw.case) { const m = (s.raw.case.opts || []).find((q) => q.id === o.id); markDone(V, 'sig:' + s.id, (m && m.msg) || 'Recorded.'); say((m && m.msg) || 'Recorded.'); return; }
78
+ if (o.id === 'w') { markDone(V, 'sig:' + s.id, 'Watching. No change to the rule.'); return; }
79
+ const to = o.id === 'up' ? NEXT_SEV[s.raw.severity] : PREV_SEV[s.raw.severity];
80
+ const undo = await setSeverity(V, s, to);
81
+ if (undo) { say('Severity is now ' + SEVW[to] + '.', undo); await reload(V); }
82
+ } })));
83
+ const li = el('li', { class: 'sgr', style: '--c:' + SV[1], 'data-in-signal': s.id, 'data-id': s.id, tabindex: '0' },
84
+ el('span', { class: 'ki', style: 'color:' + SV[1] }, c3ic(SV[2])),
85
+ el('div', { style: 'min-width:0' }, el('div', { class: 'st1 tt' }, s.t), el('div', { class: 'st2' }, s.ev),
86
+ el('div', { class: 'st3' }, el('span', { class: 'sv' }, SV[0]), el('span', null, c3ic('target', 'sm'), s.thr), el('span', null, c3ic('clock', 'sm'), s.ttt)), xp),
87
+ s.sp && s.sp.length > 1 ? spark(s.sp, SV[1]) : el('span'));
88
+ wireRow(V, li, xp, key, null);
89
+ return li;
90
+ }
91
+
92
+ /* the mockup's creepSVG and driftSVG, from the response's `charts` */
93
+ function creepChart(C) {
94
+ const P = C.points; const W = 560; const H = 170; const x0 = 36; const x1 = W - 20; const y0 = 16; const y1 = H - 32; const mn = 15; const mx = 38;
95
+ const X = (i) => x0 + i * (x1 - x0) / (P.length - 1); const Y = (v) => y1 - (v - mn) / (mx - mn) * (y1 - y0);
96
+ const s = sv('svg', { class: 'mat', viewBox: `0 0 ${W} ${H}`, role: 'img', 'aria-label': 'Scope in points against the baseline of ' + C.baseline + ' points' });
97
+ [20, 30].forEach((v) => s.append(sv('line', { x1: x0, x2: x1, y1: Y(v), y2: Y(v), stroke: 'var(--border)' }), svt(x0 - 5, Y(v) + 4, v, { 'text-anchor': 'end' })));
98
+ const pts = []; P.forEach((p, i) => { if (i) pts.push(X(i).toFixed(1) + ',' + Y(P[i - 1][1]).toFixed(1)); pts.push(X(i).toFixed(1) + ',' + Y(p[1]).toFixed(1)); });
99
+ const base = []; for (let i = P.length - 1; i >= 0; i--) base.push(X(i).toFixed(1) + ',' + Y(C.baseline).toFixed(1));
100
+ s.append(sv('polygon', { points: pts.join(' ') + ' ' + base.join(' '), fill: 'var(--destructive)', 'fill-opacity': '.12' }),
101
+ sv('line', { x1: x0, x2: x1, y1: Y(C.baseline), y2: Y(C.baseline), stroke: 'var(--ring)', 'stroke-dasharray': '5 4', 'stroke-width': 2 }),
102
+ sv('polyline', { points: pts.join(' '), fill: 'none', stroke: 'var(--destructive)', 'stroke-width': 2.5, 'stroke-linejoin': 'round' }));
103
+ P.forEach((p, i) => {
104
+ s.append(sv('circle', { cx: X(i), cy: Y(p[1]), r: 4, fill: 'var(--destructive)', stroke: 'var(--card)', 'stroke-width': 2 }), svt(X(i), H - 16, p[0], { 'text-anchor': 'middle' }));
105
+ if (p[2] && i) s.append(svt(X(i), Y(p[1]) - 9, '+' + (p[1] - P[i - 1][1]) + ' ' + short(p[2], 16), { 'text-anchor': i === P.length - 1 ? 'end' : 'middle' }));
106
+ });
107
+ s.append(svt(x1, Y(C.baseline) + 14, 'baseline ' + C.baseline + ' points', { 'text-anchor': 'end' }));
108
+ return s;
109
+ }
110
+ function delayChart(C) {
111
+ const P = C.points; const W = 560; const H = 160; const x0 = 58; const x1 = W - 20; const y0 = 16; const y1 = H - 30; const mn = 22; const mx = 33;
112
+ const X = (i) => x0 + i * (x1 - x0) / (P.length - 1); const Y = (v) => y1 - (v - mn) / (mx - mn) * (y1 - y0);
113
+ const s = sv('svg', { class: 'mat', viewBox: `0 0 ${W} ${H}`, role: 'img', 'aria-label': 'Forecast ship date at each weekly check against the promised ' + C.promised + ' Oct' });
114
+ [24, 28, 32].forEach((v) => s.append(sv('line', { x1: x0, x2: x1, y1: Y(v), y2: Y(v), stroke: 'var(--border)' }), svt(x0 - 5, Y(v) + 4, v > 31 ? (v - 31) + ' Nov' : v + ' Oct', { 'text-anchor': 'end' })));
115
+ s.append(sv('line', { x1: x0, x2: x1, y1: Y(C.promised), y2: Y(C.promised), stroke: 'var(--success)', 'stroke-dasharray': '5 4', 'stroke-width': 2 }),
116
+ sv('polyline', { points: P.map((p, i) => X(i).toFixed(1) + ',' + Y(p[1]).toFixed(1)).join(' '), fill: 'none', stroke: 'var(--destructive)', 'stroke-width': 2.5, 'stroke-linejoin': 'round' }));
117
+ P.forEach((p, i) => s.append(sv('circle', { cx: X(i), cy: Y(p[1]), r: 4, fill: 'var(--destructive)', stroke: 'var(--card)', 'stroke-width': 2 }), svt(X(i), H - 12, p[0], { 'text-anchor': 'middle' })));
118
+ s.append(svt(x1, Y(C.promised) - 6, 'promised ' + C.promised + ' Oct', { 'text-anchor': 'end', fill: 'var(--success)' }), svt(X(P.length - 1), Y(P[P.length - 1][1]) - 9, P[P.length - 1][1] + ' Oct', { 'text-anchor': 'end' }));
119
+ return s;
120
+ }
121
+
122
+ /* a small form of the page's own: a rule or a reading, pressed through pmPost; the server's refusal is shown as it is */
123
+ const field = (label, attrs) => el('label', { class: 'dim mono', style: 'display:grid;gap:4px;font-size:var(--fs-2)' }, label, el('input', { ...attrs, style: 'min-height:44px;width:100%;box-sizing:border-box;font:inherit;padding:8px 10px;border:1px solid var(--ring);border-radius:8px;background:var(--card);color:var(--foreground)' }));
124
+ function ruleForm(V) {
125
+ const id = field('Rule id', { name: 'id', placeholder: 'conversion-floor', autocomplete: 'off', 'data-in-f': 'id' });
126
+ const metric = field('Metric', { name: 'metric', placeholder: 'checkout.conversion', autocomplete: 'off', 'data-in-f': 'metric' });
127
+ const op = el('select', { 'data-in-f': 'op', 'aria-label': 'Operator', style: 'min-height:44px;font:inherit;padding:8px;border:1px solid var(--ring);border-radius:8px;background:var(--card);color:var(--foreground)' }, ['<', '<=', '>', '>=', '=='].map((o) => el('option', { value: o }, o)));
128
+ const th = field('Threshold', { name: 'threshold', type: 'number', step: 'any', 'data-in-f': 'threshold' });
129
+ const sev = el('select', { 'data-in-f': 'severity', 'aria-label': 'Severity', style: 'min-height:44px;font:inherit;padding:8px;border:1px solid var(--ring);border-radius:8px;background:var(--card);color:var(--foreground)' }, ['watch', 'act', 'critical'].map((o) => el('option', { value: o }, o)));
130
+ const msg = el('p', { class: 'dim mono', style: 'font-size:var(--fs-2);margin:0', role: 'status', 'data-in-msg': '' });
131
+ const go = el('button', { type: 'button', class: 'pill', 'data-in-add-rule': '', style: 'min-height:44px', onClick: async () => {
132
+ const r = await pmPost('/api/signals/rule', { id: id.querySelector('input').value.trim(), metric: metric.querySelector('input').value.trim(), op: op.value, threshold: Number(th.querySelector('input').value), severity: sev.value });
133
+ if (r && r.ok) { msg.textContent = ''; reload(V); } else msg.textContent = 'Not saved: ' + ((r && r.error) || 'the server refused it.');
134
+ } }, c3ic('plus', 'sm'), 'Add the rule');
135
+ return el('div', { class: 'form', style: 'display:grid;gap:8px', 'data-in-rule-form': '' }, id, metric, el('div', { style: 'display:grid;grid-template-columns:auto 1fr auto;gap:8px;align-items:end' }, op, th, sev), el('div', { class: 'chips2' }, go), msg);
136
+ }
137
+ function readingForm(V, rules) {
138
+ const metrics = [...new Set(rules.map((r) => r.metric))];
139
+ const metric = el('select', { 'data-in-f': 'rmetric', 'aria-label': 'Metric', style: 'min-height:44px;font:inherit;padding:8px;border:1px solid var(--ring);border-radius:8px;background:var(--card);color:var(--foreground)' }, metrics.map((m) => el('option', { value: m }, m)));
140
+ const val = field('Reading', { name: 'value', type: 'number', step: 'any', 'data-in-f': 'rvalue' });
141
+ const msg = el('p', { class: 'dim mono', style: 'font-size:var(--fs-2);margin:0', role: 'status' });
142
+ const go = el('button', { type: 'button', class: 'pill', 'data-in-add-reading': '', style: 'min-height:44px', onClick: async () => {
143
+ const r = await pmPost('/api/signals/reading', { metric: metric.value, value: Number(val.querySelector('input').value) });
144
+ if (r && r.ok) { msg.textContent = ''; reload(V); } else msg.textContent = 'Not saved: ' + ((r && r.error) || 'the server refused it.');
145
+ } }, c3ic('plus', 'sm'), 'Record the reading');
146
+ return el('div', { class: 'form', style: 'display:grid;gap:8px', 'data-in-reading-form': '' }, metric, val, el('div', { class: 'chips2' }, go), msg);
147
+ }
148
+
149
+ function reload(V) {
150
+ return Promise.all([pmGet('/api/signals'), pmGet('/api/records/kr-metrics')]).then(([s, k]) => {
151
+ if (V.disposed) return;
152
+ V.data = { s: s && s.ok ? s : null, k: k && k.ok ? k.records || [] : [], err: s && s.ok ? null : (s && s.error) || 'The signals could not be read.' };
153
+ V.render();
154
+ }, () => { V.data = { s: null, k: [], err: 'The signals could not be read.' }; V.render(); });
155
+ }
156
+
157
+ function render(V) {
158
+ const D = V.data;
159
+ const S = D && D.s ? D.s : null;
160
+ const all = S ? signalsOf(S) : [];
161
+ const scoped = V.scope && all.some((s) => s.initiative);
162
+ const list = (scoped ? all.filter((s) => (s.initiative || '').split(',').includes(V.scope)) : all).sort((a, b) => ORDER.indexOf(a.sev) - ORDER.indexOf(b.sev));
163
+ const head = !D ? { icon: 'alert', h: 'Reading the signals', s: 'A signal is a rule on a measured number.', ring: [0, 1] }
164
+ : S && S.head ? { icon: 'alert', ...S.head }
165
+ : all.length ? { icon: 'alert', h: S.open.length + ' of ' + all.length + ' rule' + (all.length === 1 ? ' is' : 's are') + ' tripping', s: 'Each row is a rule, with its latest reading. The agent watches them; you decide what to do.', ring: [all.length - S.open.length, all.length] }
166
+ : { icon: 'alert', h: D.err ? 'The signals could not be read' : 'No signal rules yet', s: D.err || 'A signal is a rule on a measured number, such as “conversion below 3.0%”. Add one to start watching.', ring: [0, 1] };
167
+
168
+ const left = [];
169
+ if (!D) left.push(listCard('alert', 'Signals', null, '', [emptyRow('clock', 'Reading the signals.', { role: 'status' })]));
170
+ else {
171
+ left.push(el('section', { class: 'card sect', 'data-in-list': 'signals' }, el('div', { class: 'sh2' }, el('h2', null, c3ic('alert', 'sm'), 'Signals ', el('span', { class: 'bdg g' }, String(list.length)))),
172
+ list.length ? el('ul', { class: 'sgl', style: 'list-style:none;margin:0;padding:0' }, list.map((x) => row(V, x, S))) : el('ul', { class: 'list' }, emptyRow('moon', all.length ? 'No signal belongs to this initiative.' : 'No signal rules yet, so nothing is being watched.', { 'data-in-empty': '' }))));
173
+ const krs = D.k || [];
174
+ if (krs.length) left.push(listCard('target', 'Key results', krs.length, 'baseline to target', krs.map((k) => el('li', { class: 'nrow', 'data-in-kr': k.rid },
175
+ el('span', { class: 'ki' }, c3ic('target')), el('div', { class: 'nb' }, el('span', { class: 'tt' }, k.metric),
176
+ el('div', { class: 'chips' }, el('span', { class: 'chip2 mono' }, k.baseline + ' to ' + k.target), el('span', { class: 'chip2' }, c3ic('cal', 'sm'), 'by ' + k.by)))))));
177
+ }
178
+ const right = [];
179
+ const ch = S && S.charts;
180
+ if (ch && ch.creep) right.push(card('alert', 'Scope creep', 'points against baseline', creepChart(ch.creep)));
181
+ if (ch && ch.delay) right.push(card('clock', 'Delay', 'forecast ship date, each weekly check', delayChart(ch.delay)));
182
+ if (D && S) {
183
+ const watch = S.watch || S.rules.map((r) => ({ tone: S.open.some((t) => t.rule_id === r.id) ? 'bad' : 'ok', text: r.metric + ' ' + r.op + ' ' + r.threshold + ' (' + r.severity + ')', note: (() => { const rd = S.readings.find((x) => x.metric === r.metric); return rd ? 'latest ' + rd.value : 'no reading yet'; })() }));
184
+ right.push(card('shield', 'What the agent watches', '',
185
+ watch.length ? el('ul', { class: 'chgl' }, watch.map((r) => el('li', { class: r.tone, 'data-in-rule': '' }, c3ic(r.tone === 'ok' ? 'check' : 'alert', 'sm'), el('span', null, r.text + (r.note ? ' · ' + r.note : ''))))) : el('p', { class: 'dim', style: 'font-size:var(--fs-2);margin:0' }, 'Nothing yet. A rule names a metric, a comparison, a threshold and a severity.')));
186
+ if (!ch) { // real mode: the two page presses (the demo shows the mockup's charts instead and saves nothing)
187
+ right.push(card('plus', 'Add a rule', 'a number to watch', ruleForm(V)));
188
+ if (S.rules.length) right.push(card('plus', 'Record a reading', 'the latest value of a metric', readingForm(V, S.rules)));
189
+ }
190
+ } else if (D && D.err) right.push(card('alert', 'Not read', '', el('p', { class: 'dim', style: 'font-size:var(--fs-2);margin:0' }, D.err)));
191
+ frame(V, { head, left, right });
192
+ }
193
+
194
+ const view = makeView('signals', { init: reload, render });
195
+ export const mount = view.mount;
196
+ export const dispose = view.dispose;
@@ -0,0 +1,45 @@
1
+ /* pm/c3-strategy-panels.js — the strategy panels of the Calendar's Quarter and Year panes (c3-cal-strategy.js), mounted where the console3 mockup draws the same
2
+ * panel a second time: Initiatives > Lifecycle > Strategize (maturity 3582, bets, north star 3595, early signals, synergy) and Reports > Product (north star,
3
+ * quarter outcomes, console3.html line 4345). The builders are the Calendar's own, so the two places cannot drift; this file only supplies the read.
4
+ *
5
+ * strategyPanel(name, ctx, opts) -> a node. name: north | outcomes | signals | synergy | maturity | bets.
6
+ * The node is a `display:contents` wrapper, so the card sits in its column as if it were a direct child. It shows a one-line reading note until
7
+ * GET /api/c3-strategy answers (read once, kept a minute by c3-cal-strategy.js), then it holds the card. A panel with no source draws its honest
8
+ * empty card (what is missing and what fills it); an unreadable route says so. Nothing is invented.
9
+ * ctx = { goInit(id, title), today?, data? } opts = { view: 'year'|'quarter', n: signals shown, plain: heading "North star" only }
10
+ * DATA: the same route as the Calendar (workspace/lib/routes-c3-strategy.js); in demo mode c3-demo-cal3.js answers it with the mockup's own sample. */
11
+ import { el } from './dom.js';
12
+ import { ensureC3Css } from './pm-work-view.js';
13
+ import { PANEL_BUILDERS, readModel, cachedModel, todayStr } from './c3-cal-strategy.js';
14
+
15
+ function build(name, M0, ctx, opts) {
16
+ const M = M0.signalsAll ? M0 : { ...M0, signalsAll: (M0.signals || []).map((s) => ({ ...s })) }; // the Calendar adds the late initiatives from its gantt rows; this page has none, so the rule signals stand alone
17
+ const live = !M.demo; const today = ctx.today || M.today || todayStr(Date.now());
18
+ switch (name) {
19
+ case 'north': return PANEL_BUILDERS.north(M, opts.view || 'year', today, !!opts.plain);
20
+ case 'outcomes': return PANEL_BUILDERS.outcomes(M);
21
+ case 'signals': return PANEL_BUILDERS.signals(M, opts.n || 4, ctx, live);
22
+ case 'synergy': return PANEL_BUILDERS.synergy(M);
23
+ case 'maturity': return PANEL_BUILDERS.maturity(M);
24
+ case 'bets': return PANEL_BUILDERS.bets(M, ctx);
25
+ default: return null;
26
+ }
27
+ }
28
+
29
+ export function strategyPanel(name, ctx, opts) {
30
+ ensureC3Css();
31
+ const c = ctx || {}; const o = opts || {};
32
+ const host = el('div', { style: 'display:contents', 'data-sp': name });
33
+ const fill = (M) => {
34
+ if (!M || !M.ok) { host.replaceChildren(el('p', { class: 'dim mono', style: 'font-size:var(--fs-2)', role: 'status', 'data-sp-error': name }, 'The strategy view could not be read: ' + ((M && M.error) || 'not available'))); return; }
35
+ const node = build(name, M, c, o);
36
+ if (node) host.replaceChildren(node); else host.replaceChildren();
37
+ };
38
+ const cached = cachedModel();
39
+ if (cached) fill(cached);
40
+ else {
41
+ host.replaceChildren(el('p', { class: 'dim mono', style: 'font-size:var(--fs-2)', role: 'status', 'data-sp-loading': name }, 'Reading the repository'));
42
+ readModel(!(typeof window !== 'undefined' && window.__c3demo)).then(fill);
43
+ }
44
+ return host;
45
+ }
@@ -0,0 +1,138 @@
1
+ /* pm/c3-swipe.js — the console3 row swipe, as a primitive (console3.html lines 2420 to 2438, rowAct 2421).
2
+ *
3
+ * attachSwipe(el, { onRight, onLeft, labelRight, labelLeft, iconRight, iconLeft, disabled, threshold }) -> detach()
4
+ *
5
+ * The mockup's gesture, kept as it is: a pointer drag that starts on the row (not on a button, link, field or an open row's
6
+ * case file) and moves mostly sideways. It locks to horizontal after 8px; a vertical move of more than 12px first hands the touch
7
+ * back to the page (scroll). The row follows the pointer (`--dx`; its children move at 0.55 of it), the `.rv` strip on the
8
+ * side being pulled fades in with the distance (`--d`, full at 90px), and the row takes `.dr` while it is held, `.swr` or `.swl`
9
+ * for the side. Releasing past 90px calls onRight (dx > 90) or onLeft (dx < -90); a shorter pull snaps back and does nothing.
10
+ * The click that a pull would end with is swallowed for 80ms, as the mockup does (`swSup`).
11
+ *
12
+ * Added: the label of the action is drawn beside its icon in the strip (`.rv .rvl`), so a swipe says what it will do; the same
13
+ * two actions work from the keyboard on the focused row (ArrowRight = onRight, ArrowLeft = onLeft; the row must have the focus
14
+ * itself, not a field inside it); under prefers-reduced-motion the row does not slide, the strip and its label show at the
15
+ * threshold only. The row carries tabindex=0, data-swipe, and an aria-description naming both actions.
16
+ *
17
+ * Pure parts (Node-testable): swipeStep, swipeEnd, revealOf, keyAction, SWIPE.
18
+ */
19
+ export const SWIPE = { threshold: 90, lock: 8, cancelY: 12, suppressMs: 80, tracking: 0.55 };
20
+
21
+ /** One pointer-move. state: null | { x, y, on, dx }. Returns the next state (null = the gesture is cancelled: it is a scroll). */
22
+ export function swipeStep(state, dxRaw, dyRaw, cfg = SWIPE) {
23
+ if (!state) return null;
24
+ const dx = dxRaw; const dy = dyRaw;
25
+ if (!state.on) {
26
+ if (Math.abs(dx) < cfg.lock || Math.abs(dy) > Math.abs(dx)) return Math.abs(dy) > cfg.cancelY ? null : { ...state, dx: 0 };
27
+ return { ...state, on: true, dx };
28
+ }
29
+ return { ...state, dx };
30
+ }
31
+
32
+ /** The pointer is released (or cancelled). 'right' | 'left' | null. */
33
+ export function swipeEnd(state, cancelled = false, cfg = SWIPE) {
34
+ if (!state || cancelled || !state.on) return null;
35
+ if (state.dx > cfg.threshold) return 'right';
36
+ if (state.dx < -cfg.threshold) return 'left';
37
+ return null;
38
+ }
39
+
40
+ /** How far the strip is revealed: { right: 0..1, left: 0..1 } for a drag of dx pixels (the mockup's `--d`). */
41
+ export function revealOf(dx, cfg = SWIPE) {
42
+ const d = Math.min(1, Math.abs(dx) / cfg.threshold);
43
+ return { right: dx > 0 ? d : 0, left: dx < 0 ? d : 0 };
44
+ }
45
+
46
+ /** The keyboard twin of a swipe: a key on the focused row maps to a side, or null. */
47
+ export function keyAction(key, { shift = false, meta = false, ctrl = false, alt = false } = {}) {
48
+ if (shift || meta || ctrl || alt) return null;
49
+ if (key === 'ArrowRight') return 'right';
50
+ if (key === 'ArrowLeft') return 'left';
51
+ return null;
52
+ }
53
+
54
+ const reduced = () => { try { return typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches; } catch { return false; } };
55
+ const IGNORE = 'button,a,select,input,textarea,.xp,.slt,[data-noswipe]';
56
+
57
+ function strip(side, icon, label) {
58
+ const s = document.createElement('span');
59
+ s.className = 'rv ' + (side === 'right' ? 'r' : 'l');
60
+ s.setAttribute('aria-hidden', 'true');
61
+ if (icon) {
62
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
63
+ svg.setAttribute('class', 'ic'); svg.setAttribute('aria-hidden', 'true');
64
+ const u = document.createElementNS('http://www.w3.org/2000/svg', 'use'); u.setAttribute('href', '#c3i-' + icon); svg.appendChild(u); s.appendChild(svg);
65
+ }
66
+ const t = document.createElement('span'); t.className = 'rvl'; t.textContent = label || ''; s.appendChild(t);
67
+ return s;
68
+ }
69
+
70
+ export function attachSwipe(el, opts = {}) {
71
+ const { onRight, onLeft, labelRight = '', labelLeft = '', iconRight = 'check', iconLeft = 'moon', disabled, threshold } = opts;
72
+ const cfg = threshold ? { ...SWIPE, threshold } : SWIPE;
73
+ const off = () => typeof disabled === 'function' && disabled();
74
+ el.setAttribute('data-swipe', '');
75
+ if (!el.hasAttribute('tabindex')) el.setAttribute('tabindex', '0');
76
+ el.setAttribute('aria-description', (labelRight ? 'Swipe right or press the right arrow: ' + labelRight + '. ' : '') + (labelLeft ? 'Swipe left or press the left arrow: ' + labelLeft + '.' : ''));
77
+ let rvR = el.querySelector(':scope > .rv.r'); let rvL = el.querySelector(':scope > .rv.l');
78
+ if (!rvR && onRight) { rvR = strip('right', iconRight, labelRight); el.prepend(rvR); }
79
+ if (!rvL && onLeft) { rvL = strip('left', iconLeft, labelLeft); el.prepend(rvL); }
80
+ if (rvR && !rvR.querySelector('.rvl') && labelRight) { const t = document.createElement('span'); t.className = 'rvl'; t.textContent = labelRight; rvR.appendChild(t); }
81
+ if (rvL && !rvL.querySelector('.rvl') && labelLeft) { const t = document.createElement('span'); t.className = 'rvl'; t.textContent = labelLeft; rvL.appendChild(t); }
82
+
83
+ let st = null; let suppress = false; let tmr = null;
84
+ const paint = (dx) => {
85
+ const r = revealOf(dx, cfg);
86
+ if (!reduced()) el.style.setProperty('--dx', dx + 'px');
87
+ el.classList.toggle('swr', dx > 0); el.classList.toggle('swl', dx < 0);
88
+ const past = Math.abs(dx) > cfg.threshold;
89
+ if (rvR) rvR.style.setProperty('--d', String(reduced() ? (past && dx > 0 ? 1 : 0) : r.right));
90
+ if (rvL) rvL.style.setProperty('--d', String(reduced() ? (past && dx < 0 ? 1 : 0) : r.left));
91
+ };
92
+ const clear = () => {
93
+ el.classList.remove('dr', 'swr', 'swl'); el.style.removeProperty('--dx');
94
+ if (rvR) rvR.style.removeProperty('--d'); if (rvL) rvL.style.removeProperty('--d');
95
+ };
96
+ const fire = (side) => { const fn = side === 'right' ? onRight : onLeft; if (fn) fn(el, side); };
97
+
98
+ const onMove = (e) => {
99
+ if (!st) return;
100
+ const was = st.on;
101
+ st = swipeStep(st, e.clientX - st.x, e.clientY - st.y, cfg);
102
+ if (!st) { teardown(); clear(); return; }
103
+ if (st.on && !was) { el.classList.add('dr'); try { const g = getSelection(); if (g) g.removeAllRanges(); } catch { /* no selection API */ } }
104
+ if (st.on) paint(st.dx);
105
+ };
106
+ const onUp = (e) => finish(e && e.type === 'pointercancel');
107
+ function finish(cancelled) {
108
+ const s = st; st = null; teardown(); clear();
109
+ if (s && s.on) { suppress = true; clearTimeout(tmr); tmr = setTimeout(() => { suppress = false; }, SWIPE.suppressMs); }
110
+ const side = swipeEnd(s, cancelled, cfg);
111
+ if (side) fire(side);
112
+ }
113
+ function teardown() {
114
+ document.removeEventListener('pointermove', onMove); document.removeEventListener('pointerup', onUp); document.removeEventListener('pointercancel', onUp);
115
+ }
116
+ const onDown = (e) => {
117
+ if (e.button > 0 || off()) return;
118
+ if (e.target.closest && e.target.closest(IGNORE) && e.target.closest(IGNORE) !== el) return;
119
+ if (el.getAttribute('aria-expanded') === 'true') return; // an open row is read, not swiped (the mockup does the same)
120
+ st = { x: e.clientX, y: e.clientY, on: false, dx: 0 };
121
+ document.addEventListener('pointermove', onMove); document.addEventListener('pointerup', onUp); document.addEventListener('pointercancel', onUp);
122
+ };
123
+ const onClickCapture = (e) => { if (suppress) { suppress = false; e.stopPropagation(); e.preventDefault(); } };
124
+ const onKey = (e) => {
125
+ if (e.target !== el || off()) return;
126
+ const side = keyAction(e.key, { shift: e.shiftKey, meta: e.metaKey, ctrl: e.ctrlKey, alt: e.altKey });
127
+ if (!side || (side === 'right' ? !onRight : !onLeft)) return;
128
+ e.preventDefault(); e.stopPropagation(); fire(side);
129
+ };
130
+ el.addEventListener('pointerdown', onDown);
131
+ el.addEventListener('click', onClickCapture, true);
132
+ el.addEventListener('keydown', onKey);
133
+ return function detach() {
134
+ teardown(); clear(); clearTimeout(tmr);
135
+ el.removeEventListener('pointerdown', onDown); el.removeEventListener('click', onClickCapture, true); el.removeEventListener('keydown', onKey);
136
+ el.removeAttribute('data-swipe'); el.removeAttribute('aria-description');
137
+ };
138
+ }
@@ -0,0 +1,26 @@
1
+ /* pm/c3-terminal-data.js — where the agent surfaces read and press, in one place.
2
+ * source() -> { demo, getRuns(), answer(body), correct(body), canPrompt, why, ... }
3
+ * REAL: GET /api/runs; POST /api/runs/answer and /api/runs/correction (a page press; the server refuses any other caller). Nothing here sends
4
+ * a prompt to a session: no page route does. `canPrompt` is false and `why` says so; every control that would send one is drawn disabled with it.
5
+ * DEMO: the fixtures of c3-demo-term.js, in the real response shapes, answers from memory and advances its own state; nothing is sent.
6
+ *
7
+ * needsYou(runs) is the one count for the needs-you badge: a run that asks, holds a permission, needs the terminal or did not start. */
8
+ import { pmGet, pmPost } from './http.js';
9
+
10
+ export const WHY_PROMPT = 'Sending a prompt to a session is not available on this page: no safe route sends one. The session\'s terminal is in the main app.';
11
+
12
+ const real = {
13
+ demo: false, canPrompt: false, why: WHY_PROMPT,
14
+ getRuns: () => pmGet('/api/runs'),
15
+ answer: (b) => pmPost('/api/runs/answer', b),
16
+ correct: (b) => pmPost('/api/runs/correction', b),
17
+ };
18
+ /* demo: c3-demo.js registers the fixtures' factory (window.__c3termfx_make) by importing c3-demo-term.js; the page is in demo when installDemo() ran (window.__c3demo). */
19
+ export const isDemo = () => typeof window !== 'undefined' && !!window.__c3demo && typeof window.__c3termfx_make === 'function';
20
+ export const source = () => (isDemo() ? (window.__c3termfx || (window.__c3termfx = window.__c3termfx_make())) : real);
21
+
22
+ export function needsYou(r) {
23
+ if (!r || !r.ok || !r.cards) return 0;
24
+ const live = new Set([].concat((r.groups && r.groups.onCard) || [], (r.groups && r.groups.startedByMe) || [], (r.groups && r.groups.automatic) || []).filter((s) => s && s.endedAt == null).map((s) => s.id));
25
+ return Object.entries(r.cards).filter(([id, c]) => live.has(id) && c && ((c.permissions || []).length || ['asking', 'permission', 'attention', 'not_started'].includes(c.state))).length;
26
+ }
@@ -0,0 +1,91 @@
1
+ /* pm/c3-terminal-edge.js — the "Edge cases" tab of agent-surfaces.html: each case shows the CLI event that triggers it, what you see, what you can do, and what
2
+ * happens if that fails. The cards are run-card.js cards over folded runs (scripts/card-state.js cardFor over reply lines; the rich results through the
3
+ * contract's own validators). EXAMPLE DATA, in real mode too: no run is attached, so a press answers "this is an example" and records nothing.
4
+ * mountEdge(host) disposeEdge(host)
5
+ * The mockup's twelve cases, plus a group for the agent's reply failing (the mockup's "Next reply" switch: no result, unreadable, crashed, quiet,
6
+ * unlabelled claim, plain text), so every failure state of the contract can be looked at. */
7
+ import { el } from './dom.js';
8
+ import { ic } from './c3-terminal-icons.js';
9
+ import { mountRunCard, disposeRunCard, openBtn } from './run-card.js';
10
+ import { mdNode } from './c3-terminal-results.js';
11
+ import { toast } from './c3-toast.js';
12
+
13
+ const TESTED = 'Logic tested';
14
+ const UNV = 'Logic tested · CLI behaviour unverified';
15
+ const CASES = [
16
+ { g: 'The CLI asks you', id: 'q', name: 'Its own question', status: UNV, ev: 'PreToolUse on the question tool. The workspace holds the hook open, shows this, then denies the tool with your answer as the reason.', fb: 'If the hold fails, the hook denies at once and tells the agent to use the format; the question then arrives as a normal ask. If both fail: Open the session.' },
17
+ { g: 'The CLI asks you', id: 'plan', name: 'Plan approval', status: UNV, ev: 'PermissionRequest for the plan-exit tool. The plan text arrives with it; allow or deny goes back.', fb: 'If the workspace is down the CLI shows its own approval in the terminal. Nothing is approved by default.' },
18
+ { g: 'The CLI cannot continue', id: 'start', name: 'Not started', status: UNV, fail: 'notstarted', ev: 'No SessionStart hook within 10 seconds of launch. Usually a trust-folder or update prompt that only the terminal shows.', fb: 'Prevention: worktrees are made under a folder the CLI already trusts, and its auto-update is switched off for sessions (setting to be confirmed).' },
19
+ { g: 'The CLI cannot continue', id: 'auth', name: 'Sign-in needed', status: UNV, fail: 'attention', ev: 'A notification of an attention type (dialog, sign-in). Only the terminal can show it.', fb: 'This is the lowest tier on purpose: the interface says what kind of thing it is and opens the real CLI.' },
20
+ { g: 'The CLI cannot continue', id: 'pause', name: 'Usage limit', status: UNV, fail: 'pause', ev: 'A quota notification (auto-resume fired, stale or disabled). The run is paused, not failed.', fb: 'If the notice never comes, the quiet timer applies after 2 minutes and offers Open the session.' },
21
+ { g: 'The CLI cannot continue', id: 'api', name: 'API error', status: UNV, fail: 'api', ev: 'The StopFailure hook: the turn ended on an API error.', fb: 'Retry is one tap. Three failures in a row stop retrying and offer Open the session.' },
22
+ { g: 'The agent\'s reply fails', id: 'noresult', name: 'Ends with no result', status: TESTED, fail: 'noresult', ev: 'The Stop hook told the agent to report, twice. The session still ended with no result, question or reason.', fb: 'The card says so and keeps its content. Retry or Open the session; nothing on the card changed.' },
23
+ { g: 'The agent\'s reply fails', id: 'rejected', name: 'Unreadable reply', status: TESTED, fail: 'rejected', ev: '`pmos reply result` refused the body (for example: headline is required). The agent is told why and can send it again.', fb: 'If it cannot, the run ends as "ended without a usable result" and the refusal is in the case file.' },
24
+ { g: 'The agent\'s reply fails', id: 'crashed', name: 'Crashed', status: TESTED, fail: 'crashed', ev: 'SessionEnd with a non-zero exit code and no result.', fb: 'The card says it ended with an error. Retry or Open the session.' },
25
+ { g: 'The agent\'s reply fails', id: 'quiet', name: 'Goes quiet', status: TESTED, fail: 'quiet', ev: 'No output for 2 minutes and no result. The quiet timer is the interface\'s own.', fb: 'It may be stuck. Open the session to see it.' },
26
+ { g: 'The agent\'s reply fails', id: 'unlabelled', name: 'Unlabelled claim', status: TESTED, fail: 'unlabelled', ev: 'A claim with no valid label (fact needs a source, estimate needs a range). The strict command refuses it; the interface folds leniently as a second layer.', fb: 'The claim is kept and marked "unlabelled". It cannot back a message, and an unlabelled draft cannot be slid to send.' },
27
+ { g: 'The agent\'s reply fails', id: 'prose', name: 'Plain text', status: TESTED, fail: 'prose', ev: 'The Stop hook\'s last assistant message, when no result was reported. Tier 2.', fb: 'Shown as unchecked text, never as a result. Ask again in the format, or Open the session.' },
28
+ { g: 'Richer results', id: 'deck', name: 'Slides', status: TESTED, edge: 'deck', ev: 'A deck result: up to 20 slides, 6 bullets each. Pptx stays available as an export file with no preview.', fb: 'A bullet without a label is counted. The deck cannot be shared until none is left.' },
29
+ { g: 'Richer results', id: 'tbl', name: 'Table', status: TESTED, edge: 'table', ev: 'A table result: up to 12 columns and 200 rows. More than that becomes a csv file.', fb: 'A row with the wrong number of cells rejects the whole table, and the agent is told why.' },
30
+ { g: 'Richer results', id: 'dg', name: 'Diagram and image', status: TESTED, edge: 'diagram', ev: 'A diagram result (mermaid or svg) or a png, jpg or svg file. Drawn locally; no network, no scripts.', fb: 'An svg with a script, a handler, an external link or a foreignObject is refused with the reason. The agent gets the error and can resend.' },
31
+ { g: 'Richer results', id: 'big', name: 'Long answer', status: TESTED, edge: 'file', ev: 'The reply command refuses a result over 8 KB and tells the agent to write a file. The agent fixes it on its own.', fb: 'If the agent cannot, the run ends as plain text (Tier 2) and you see the text.' },
32
+ { g: 'Runs and history', id: 'sub', name: 'Helpers (subagents)', status: UNV, ev: 'SubagentStart and SubagentStop hooks. Only the helper type and whether it is running are kept.', fb: 'A helper\'s own text is never shown or trusted. Its work reaches you only through the parent\'s result.' },
33
+ { g: 'Runs and history', id: 'ver', name: 'Resumed or newer format', status: TESTED, fail: 'format', ev: 'Every line carries a format number. A line newer than the workspace reads is ignored and counted. At every start, resume and compact, the CLI is given the reply instructions again.', fb: 'A run that cannot be read says so and offers Open the session. It is never shown half-read.' },
34
+ ];
35
+ const stub = { demo: false, canPrompt: false, why: 'This is an example: no run is attached, so nothing is sent.', answer: async () => ({ ok: false, error: 'this is an example, no run is attached' }), correct: async () => ({ ok: false, error: 'this is an example, no run is attached' }), send: async () => ({ ok: false, error: 'this is an example' }) };
36
+ const ctxFor = (fx, id, name, card, extra) => ({ id: 'edge-' + id, name, card, src: stub, origin: 'example', refresh() {}, openSession: () => toast('In the real workspace this opens the CLI session.'), glossary: fx.glossary, fileBody: (p) => fx.edge.fileBody && fx.edge.fileBody[p], ...(extra || {}) });
37
+
38
+ function questionCase(fx, st) {
39
+ const qs = fx.edge.question.questions; const Q = st.q || (st.q = { a: [null, []], sent: false });
40
+ const draw = () => st.redraw();
41
+ const pill = (on, l, f) => el('button', { type: 'button', class: 'pill' + (on ? ' on' : ''), 'aria-pressed': String(on), onclick: f }, l);
42
+ const ready = Q.a[0] !== null && Q.a[1].length;
43
+ return el('div', { class: 'c3t-card' },
44
+ qs.map((q, qi) => el('div', { class: 'ask' }, el('b', null, q.q), q.multi ? dim('Pick any') : null, el('div', { class: 'c3t-tray' }, q.options.map((o, oi) => pill(qi === 0 ? Q.a[0] === oi : Q.a[1].includes(oi), o.label, () => { if (qi === 0) Q.a[0] = oi; else { const k = Q.a[1].indexOf(oi); if (k < 0) Q.a[1].push(oi); else Q.a[1].splice(k, 1); } draw(); }))))),
45
+ Q.sent ? el('div', { class: 'chips2' }, el('span', { class: 'chip2 ok' }, 'Sent to the CLI: ' + qs[0].options[Q.a[0]].label + ' · ' + Q.a[1].map((i) => qs[1].options[i].label).join(', ') + '. It will not ask again.'))
46
+ : el('div', { class: 'c3t-tray' }, dim('Two questions from the CLI'), el('span', { style: 'flex:1' }), el('button', { type: 'button', class: 'pill rec', disabled: !ready, onclick: () => { Q.sent = true; toast('Example: answered. In a run the CLI continues with your choices.'); draw(); } }, 'Answer')));
47
+ }
48
+ const dim = (t) => el('span', { class: 'dim mono', style: 'font-size:var(--fs-2)' }, t);
49
+ function planCase(fx, st) {
50
+ const P = st.p || (st.p = { sub: null, out: null }); const draw = () => st.redraw();
51
+ const set = (decision, note, label) => { P.out = label; toast('Example: counted as a correction would be.'); draw(); };
52
+ return el('div', { class: 'c3t-card' }, el('div', { class: 'top' }, el('span', { class: 'tg', style: '--tc:var(--acc)' }, 'Plan to approve'), dim('read-only run')), el('div', { class: 'c3t-file' }, el('div', { class: 'fb' }, mdNode(fx.edge.plan.plan))),
53
+ P.out ? el('div', { class: 'chips2' }, el('span', { class: 'chip2 ' + (P.out[0] === 'A' ? 'ok' : '') }, P.out))
54
+ : [el('div', { class: 'c3t-tray' }, el('button', { type: 'button', class: 'pill rec', onclick: () => { P.out = 'Approved: allowed in the CLI'; toast('Example: plan approved. The CLI continues.'); draw(); } }, ic('check', 'sm'), 'Approve'),
55
+ el('button', { type: 'button', class: 'pill' + (P.sub === 'r' ? ' on' : ''), onclick: () => { P.sub = P.sub === 'r' ? null : 'r'; draw(); } }, ic('edit', 'sm'), 'Revise'),
56
+ el('button', { type: 'button', class: 'pill' + (P.sub === 'x' ? ' on' : ''), onclick: () => { P.sub = P.sub === 'x' ? null : 'x'; draw(); } }, ic('x', 'sm'), 'Reject')),
57
+ P.sub === 'r' ? el('div', { class: 'c3t-tray' }, fx.moves.revise.map((m) => el('button', { type: 'button', class: 'pill', onclick: () => set('revise', m, 'Revise sent: "' + fx.edge.planReplies.revise.hookSpecificOutput.decision.message.replace('Make it shorter.', m) + '"') }, m))) : null,
58
+ P.sub === 'x' ? el('div', { class: 'c3t-tray' }, dim('Why?'), fx.moves.reject.map((m) => el('button', { type: 'button', class: 'pill', onclick: () => set('reject', m, 'Rejected sent: "The PM rejected the plan: ' + m + '."') }, m))) : null]);
59
+ }
60
+ function subCase() {
61
+ return el('div', { class: 'c3t-card' }, el('div', { class: 'c3t-run' }, el('div', { class: 'line' }, el('span', { class: 'c3spin' }, ic('run', 'sm')), el('span', { class: 't' }, 'Reading the plan with 2 helpers')),
62
+ el('div', { class: 'c3t-tray' }, el('span', { class: 'chip2' }, ic('check', 'sm'), 'Explore · done'), el('span', { class: 'chip2', style: 'color:var(--acc)' }, ic('run', 'sm'), 'Plan · working')), el('div', { class: 'prog' }, el('i', { class: 'd' }), el('i', { class: 'd' }), el('i'))));
63
+ }
64
+
65
+ export function mountEdge(host, fx) {
66
+ disposeEdge(host);
67
+ const st = { cur: 'q', ui: {} };
68
+ const list = el('div', { class: 'elist', 'data-c3t': 'edge-list' }); const detail = el('div', { style: 'min-width:0', 'data-c3t': 'edge-detail' });
69
+ st.redraw = () => draw();
70
+ function draw() {
71
+ let last = '';
72
+ list.replaceChildren(...CASES.map((c) => [c.g !== last ? (last = c.g, el('div', { class: 'eyebrow eh' }, c.g)) : null, el('button', { type: 'button', 'aria-pressed': String(c.id === st.cur), 'data-edge': c.id, onclick: () => { st.cur = c.id; draw(); } }, c.name)]).flat().filter(Boolean));
73
+ const c = CASES.find((x) => x.id === st.cur);
74
+ let body;
75
+ const slot = el('div', { 'data-run-host': 'edge' });
76
+ if (c.id === 'q') body = questionCase(fx, st);
77
+ else if (c.id === 'plan') body = planCase(fx, st);
78
+ else if (c.id === 'sub') body = subCase();
79
+ else {
80
+ const rec = c.fail ? fx.fail[c.fail] : c.id === 'big' ? fx.edge.file : fx.edge[c.edge];
81
+ mountRunCard(slot, ctxFor(fx, c.id, c.name, rec.card, { observed: null }));
82
+ body = slot;
83
+ if (c.id === 'big') body = el('div', { class: 'c3t-bigwrap' }, el('div', { class: 'tm' }, el('div', { class: 'raw' }, 'pmos reply result --type evidence --file big.json'), el('div', { class: 'raw', style: 'color:var(--destructive)' }, 'error: result too large · hint: write it to a file in the run folder and send --type file'), el('div', { class: 'raw' }, 'pmos reply result --type file --media md --file outputs/r12/report.md')), slot, dim('You never saw the failed attempt. It is in the case file.'));
84
+ }
85
+ detail.replaceChildren(el('div', { class: 'node c3t-node' }, el('div', { class: 'top' }, el('b', { style: 'font-size:var(--fs-4)' }, c.name), el('span', { class: 'chip2 ' + (c.status === TESTED ? 'ok' : 'warn'), style: 'margin-left:auto' }, c.status)),
86
+ el('div', { class: 'dim mono' }, el('b', { style: 'color:var(--foreground)' }, 'CLI event'), ' · ' + c.ev), body, el('div', { class: 'dim mono' }, el('b', { style: 'color:var(--foreground)' }, 'If this fails'), ' · ' + c.fb)));
87
+ }
88
+ host.replaceChildren(el('div', { class: 'dim mono', style: 'font-size:var(--fs-2)' }, 'Example data, not from your runs. Each case shows the CLI event that triggers it, what you see, what you can do, and what happens if that fails.'), el('div', { class: 'c3t-edge' }, list, detail));
89
+ draw();
90
+ }
91
+ export function disposeEdge(host) { host.querySelectorAll('[data-run-host]').forEach((h) => { try { disposeRunCard(h); } catch { /* gone */ } }); host.replaceChildren(); }
@@ -0,0 +1,41 @@
1
+ /* pm/c3-terminal-icons.js — the icon language of the agent surfaces, and the few glyphs the console3 sprite does not hold.
2
+ *
3
+ * Two glyphs, never swapped (the mockups draw them apart: agent-surfaces.html and terminal-layout.html):
4
+ * AGENT `pulse` (the mockup's i-pulse) the agent surfaces: sessions that need you, results to accept. Header button (with the needs-you badge) and the
5
+ * dock's "Agent surfaces" button. It also means "the agent acts" everywhere else on the page (swipe right, suggested move).
6
+ * TERMINAL `term` (`>_`) a real session's terminal: the dock rail button, a session's "Open the session" link, a session row's mark.
7
+ * AGENT = "the interface that reads sessions for you". TERMINAL = "the raw CLI session". A button shows one or the other, with its own tooltip.
8
+ *
9
+ * The extra symbols are the mockup's own paths (agent-surfaces.html lines 168 to 184), added once under the c3i- ids c3ic() reads. A symbol
10
+ * another module already added is not added again. */
11
+ import { c3ic } from './pm-work-view.js';
12
+
13
+ export const ICON = { agent: 'pulse', terminal: 'term' }; // the mockup's header Sessions pill draws the pulse glyph; the rail draws the terminal glyph
14
+ export const TIPS = {
15
+ agentHeader: (n) => (n ? 'Agent surfaces: ' + n + (n === 1 ? ' session needs you' : ' sessions need you') : 'Agent surfaces: sessions, results to accept, questions for you'),
16
+ agentDock: 'Agent surfaces: the full page of sessions that need you',
17
+ terminalRail: 'Sessions dock (Ctrl J): the terminals of your branches',
18
+ terminalOpen: 'Open the session: its real terminal, in the main app',
19
+ };
20
+
21
+ const NS = 'http://www.w3.org/2000/svg';
22
+ const PATHS = {
23
+ edit: 'M4 20h4L19 9l-4-4L4 16zM13 7l4 4', book: 'M5 4h10a3 3 0 0 1 3 3v13H8a3 3 0 0 1-3-3zM5 17a3 3 0 0 1 3-3h10', flip: 'M4 8h13l-3-3M20 16H7l3 3',
24
+ run: 'M12 3a9 9 0 1 0 9 9', pulse: 'M3 12h4l3-7 4 14 3-7h4', chev: 'm9 6 6 6-6 6', chevl: 'm15 6-6 6 6 6', chat: 'M4 5h16v11H9l-5 4z',
25
+ };
26
+ let done = false;
27
+ export function ensureTermIcons() {
28
+ if (done || typeof document === 'undefined') return;
29
+ done = true;
30
+ const missing = Object.keys(PATHS).filter((k) => !document.getElementById('c3i-' + k));
31
+ if (!missing.length) return;
32
+ const svg = document.createElementNS(NS, 'svg');
33
+ svg.setAttribute('width', '0'); svg.setAttribute('height', '0'); svg.setAttribute('aria-hidden', 'true'); svg.style.position = 'absolute';
34
+ for (const k of missing) {
35
+ const sym = document.createElementNS(NS, 'symbol'); sym.setAttribute('id', 'c3i-' + k); sym.setAttribute('viewBox', '0 0 24 24');
36
+ const p = document.createElementNS(NS, 'path'); p.setAttribute('d', PATHS[k]); sym.appendChild(p); svg.appendChild(sym);
37
+ }
38
+ document.body.appendChild(svg);
39
+ }
40
+ /** An icon element (the c3 sprite plus the extras above). */
41
+ export function ic(name, cls) { ensureTermIcons(); return c3ic(name, cls); }