create-pmos 0.147.0 → 0.149.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.
@@ -533,7 +533,7 @@ function openRef(ui, ref, extra) {
533
533
 
534
534
  /* the home chip: where the record lives (the mockup's UI.homeChip) */
535
535
  const homeChip = (name) => el('span', { class: 'chip2', title: 'Where this lives', 'data-cal3': 'home' }, c3ic('repo', 'sm'), name || 'its home');
536
- const peopleOf = (e) => [].concat(e.people || e.with || []).map((p) => (p && typeof p === 'object' ? p.name || p.id : p)).filter(Boolean).map(String);
536
+ const peopleOf = (e) => [].concat(e.people || e.with || e.attendees || []).map((p) => (p && typeof p === 'object' ? p.name || p.id : p)).filter(Boolean).map(String);
537
537
 
538
538
  function closeBtn(ui) {
539
539
  return button({ label: 'Close', variant: 'ghost', size: 'sm', attrs: { class: 'cal3-close', 'data-cal3': 'close' }, onClick: () => { ui.st.sel = null; ui.redraw('[data-cal3-chip="meetings"]'); } });
@@ -581,22 +581,78 @@ function meetingDetail(ui, hit) {
581
581
  };
582
582
  let body;
583
583
  if (!M.ctx) body = [fold('info', 'The calendar sent no meeting context, so PMOS shows nothing for this meeting.', { 'data-cal3': 'no-context' })];
584
- else if (!rec || rec.held === false) body = [fold('moon', past ? 'Nothing held for this meeting: no note, decisions or linked items yet.' : 'Nothing to prepare for this meeting.', { 'data-cal3': 'nothing-held' })];
584
+ else if (past && (!rec || rec.held === false)) body = [fold('moon', 'Nothing held for this meeting: no note, decisions or linked items yet.', { 'data-cal3': 'nothing-held' })];
585
585
  else if (past) body = [list(['decision', 'action'], 'What came out of it', 'out', 'No decision or action is recorded.'), list(['moved'], 'What moved after it', 'moved', 'Nothing moved within 48 hours that refers to it.')];
586
- else if (!items.some((x) => x.kind === 'prepared' || x.kind === 'todo')) body = [el('p', { class: 'dim cal3-note', 'data-cal3': 'nothing-to-prepare' }, 'Nothing to prepare for this meeting.')];
587
- else body = [list(['prepared'], 'Prepared', 'prepared', 'Nothing is prepared yet.'), list(['todo'], 'To prepare', 'todo', 'Nothing is left to prepare.')];
586
+ else body = upcomingBody(ui, rec, items);
587
+ const b = !past && rec && rec.brief ? rec.brief : null;
588
+ const iniId = typeof e.initiative === 'string' ? e.initiative : b && b.initiative ? b.initiative.id : null;
589
+ const iniTitle = iniId ? (b && b.initiative && b.initiative.id === iniId && b.initiative.title) || (((ui.d.gantt || []).find((g) => g.id === iniId) || {}).title) || iniId : null;
590
+ const iniStatus = b && b.initiative && b.initiative.id === iniId ? b.initiative.status : (((ui.d.gantt || []).find((g) => g.id === iniId) || {}).status || null);
591
+ const sources = b && b.sources && b.sources.length ? 'Put together from ' + listWords(b.sources) + '. Each line opens its source. ' : '';
588
592
  return [
589
593
  el('div', { class: 'cal3-dt' }, el('h3', null, c3ic('cal'), 'Meeting'), el('span', { class: 'tag' + (past ? '' : ' cal3-up') }, past ? 'past' : 'upcoming'), closeBtn(ui)),
590
594
  el('b', { class: 'cal3-title' }, e.title || e.id),
591
595
  el('div', { class: 'dim mono cal3-when' }, h.dayLabel(hit.date) + (s === null ? ', all day' : ' · ' + h.hhmm(s) + ' to ' + h.hhmm(f))),
592
- el('div', { class: 'chips2', 'data-cal3': 'meeting-chips' }, homeChip('Calendar'), ...peopleOf(e).map((n) => el('span', { class: 'tag' }, n)),
593
- e.initiative ? el('span', { class: 'tag' }, 'Initiative: ' + (((ui.d.gantt || []).find((g) => g.id === e.initiative) || {}).title || e.initiative)) : null),
596
+ el('div', { class: 'chips2', 'data-cal3': 'meeting-chips' }, homeChip('Calendar'), ...peopleOf(e).map((n) => el('span', { class: 'tag', 'data-cal3': 'person' }, n)),
597
+ iniId ? el('span', { class: 'tag', 'data-cal3': 'meeting-initiative' }, 'Initiative: ' + iniTitle) : null,
598
+ iniStatus && STATUS_TONE[iniStatus] ? el('span', { class: 'tag cal3-tone-' + STATUS_TONE[iniStatus] }, iniStatus) : null),
594
599
  noteLink(ui, e),
595
600
  ...body,
596
- el('p', { class: 'dim cal3-note' }, past ? 'Only what PMOS holds a record for: items created or closed within 48 hours of the meeting. Each line opens its source.' : 'PMOS proposes changes. It never edits your calendar.'),
601
+ el('p', { class: 'dim cal3-note', 'data-cal3': past ? 'past-note' : 'sources' }, past ? 'Only what PMOS holds a record for: items created or closed within 48 hours of the meeting. Each line opens its source.' : sources + 'PMOS proposes changes. It never edits your calendar.'),
597
602
  ];
598
603
  }
599
604
 
605
+ /* the card before a meeting (initiative c3-meeting-card, the mockup the PM approved on 2026-10-12): what it is for, what
606
+ * matters today, what happened last time and what to bring. Every part comes from the server's `brief` (lib/calendar-layers.js
607
+ * briefOf) or the prep items; a part with no record is left out. When PMOS knows little, it says what is missing. */
608
+ const STATUS_TONE = { 'at risk': 'warn', delayed: 'bad', overdue: 'bad' };
609
+ const listWords = (a) => (a.length < 2 ? a.join('') : a.slice(0, -1).join(', ') + ' and ' + a[a.length - 1]);
610
+ const ctxBox = (key, title, ...kids) => el('div', { class: 'cal3-ctx', 'data-cal3-ctx': key }, el('h4', null, ...[].concat(title)), ...kids);
611
+ const ul = (rows) => el('ul', { class: 'cal3-cl', role: 'list' }, ...rows);
612
+ const ci = (attrs, ...kids) => el('li', { class: 'cal3-ci', ...attrs }, ...kids);
613
+
614
+ function upcomingBody(ui, rec, items) {
615
+ const { h } = ui;
616
+ const b = rec && rec.brief ? rec.brief : null;
617
+ const prep = items.filter((x) => x.kind === 'prepared' || x.kind === 'todo');
618
+ const out = [];
619
+ if (b && b.gist && b.gist.length) out.push(el('div', { class: 'cal3-ctx cal3-gist', 'data-cal3-ctx': 'gist' }, el('h4', null, c3ic('spark', 'sm'), 'In short'), el('p', { class: 'cal3-p' }, b.gist.join(' '))));
620
+ if (b && b.matters && b.matters.length) out.push(ctxBox('matters', 'What matters today', ul(b.matters.map((x) => ci({ 'data-cal3-matter': x.tag },
621
+ el('span', { class: 'tag cal3-tone-' + x.tone }, x.tag), el('span', { class: 'cal3-ct' }, x.text), x.meta ? el('span', { class: 'dim mono cal3-at' }, x.meta) : null,
622
+ refLink(ui, x.ref, { initiative: x.ref.initiative, title: x.text }))))));
623
+ if (b && b.last) {
624
+ const l = b.last;
625
+ const open = ui.r.ctx.overlay ? null : el('a', { href: '#', class: 'cal3-ref', 'data-cal3': 'open-last-note', title: 'Open the note: ' + (l.title || 'Untitled note'),
626
+ onClick: (ev) => { ev.preventDefault(); openNote(ui.r, l.id); ui.redraw(); } }, c3ic('notes', 'sm'), 'Open the last note');
627
+ out.push(ctxBox('last', 'Since the last one · ' + h.dayLabel(String(l.at).slice(0, 10)),
628
+ l.private ? el('p', { class: 'dim cal3-note' }, 'That note is private, so only its title shows: ' + (l.title || 'Untitled note') + '.')
629
+ : l.overview ? el('p', { class: 'cal3-p' }, l.overview) : el('p', { class: 'dim cal3-note' }, 'That note has no summary yet.'),
630
+ !l.private && ((l.decisions || []).length || (l.actions || []).length) ? ul([
631
+ ...(l.decisions || []).map((t) => ci({ 'data-cal3-item': 'last-decision' }, el('span', { class: 'tag' }, 'Decision'), el('span', { class: 'cal3-ct' }, t))),
632
+ ...(l.actions || []).map((a) => ci({ 'data-cal3-item': 'last-action' }, el('span', { class: 'tag cal3-tone-warn' }, 'From last time'), el('span', { class: 'cal3-ct' }, a.text),
633
+ a.owner || a.due ? el('span', { class: 'dim mono cal3-at' }, [a.owner, a.due && 'due ' + a.due].filter(Boolean).join(' · ')) : null))]) : null,
634
+ el('div', { class: 'chips2' }, open, b.earlier > 1 ? el('span', { class: 'dim', 'data-cal3': 'earlier' }, b.earlier + ' earlier notes of this meeting') : null)));
635
+ }
636
+ if (prep.length || (b && b.prep)) out.push(ctxBox('prepare', 'To prepare', ul([
637
+ ...(b && b.prep ? [ci({ 'data-cal3-item': 'ready' }, el('span', { class: 'tag' }, 'To be ready'), el('span', { class: 'cal3-ct' }, b.prep))] : []),
638
+ ...prep.filter((x) => !(b && b.prep && /^Prep notes from your agent/.test(x.text))).map((x) => ci({ 'data-cal3-item': x.kind },
639
+ el('span', { class: 'tag' + (x.done ? ' cal3-up' : '') }, CTXWORD[x.kind] || x.kind), el('span', { class: 'cal3-ct' }, x.text),
640
+ x.at ? el('span', { class: 'dim mono cal3-at' }, h.dayLabel(x.at) + (h.timeOf(x.at) !== null ? ' ' + h.hhmm(h.timeOf(x.at)) : '')) : null,
641
+ x.ref && REF[x.ref.type] ? refLink(ui, x.ref, { title: x.text }) : null))])));
642
+ if (b && ((b.links || []).length || (b.refs || []).length)) out.push(el('div', { class: 'chips2', 'data-cal3': 'meeting-links' },
643
+ ...(b.links || []).map((l) => el('a', { class: 'chip2', href: l.url, target: '_blank', rel: 'noopener noreferrer' }, c3ic('out', 'sm'), l.label)),
644
+ ...(b.refs || []).map((r) => r.link ? el('a', { class: 'chip2', href: r.link, target: '_blank', rel: 'noopener noreferrer' }, c3ic('out', 'sm'), r.label, r.source ? el('span', { class: 'dim' }, ' ' + r.source) : null)
645
+ : el('span', { class: 'chip2' }, r.label, r.source ? el('span', { class: 'dim' }, ' ' + r.source) : null))));
646
+ const noIni = !(b && b.initiative); const noLast = !(b && b.last);
647
+ if (noIni || noLast) {
648
+ const missing = noIni && noLast ? 'No initiative is linked and there is no earlier note from this meeting.' : noIni ? 'No initiative is linked to this meeting.' : 'There is no earlier note from this meeting.';
649
+ out.push(ctxBox('knows', 'What PMOS knows', fold('info', missing + (out.length ? '' : ' Nothing is recorded to prepare.'), { 'data-cal3': 'knows' }),
650
+ ul([noIni ? ci({}, el('span', { class: 'tag' }, 'Link'), el('span', { class: 'cal3-ct' }, 'Link an initiative, and its blocked, late and due items show here.')) : null,
651
+ ci({}, el('span', { class: 'tag' }, 'Note'), el('span', { class: 'cal3-ct' }, 'Take a note in this meeting, and the next one opens with its summary and next steps.'))].filter(Boolean))));
652
+ }
653
+ return out;
654
+ }
655
+
600
656
  /* the free blocks of the range in words, with the suggested use or none; nothing is written */
601
657
  function freeList(ui, range) {
602
658
  const { M, h } = ui;
@@ -103,7 +103,21 @@ const CONTEXT = {
103
103
  'm4@2026-10-08': { phase: 'upcoming', held: true, items: [
104
104
  { kind: 'prepared', text: 'Three scope options with their dates', ref: init('checkout') },
105
105
  { kind: 'todo', text: 'Cut Checkout v2 to 3 core flows to hold 24 Oct?', ref: item('item-demo010') },
106
- ] },
106
+ ],
107
+ // the card before a meeting (initiative c3-meeting-card): what the server's briefOf puts together
108
+ brief: {
109
+ initiative: { id: 'checkout', title: 'Checkout v2', status: 'at risk' },
110
+ gist: ['For Checkout v2, which is at risk.', '1 item blocked, 1 item due today.'],
111
+ matters: [
112
+ { tone: 'bad', tag: 'Blocked', text: 'Sprint scope: waiting on the scope decision', meta: 'Dara · due 2026-10-08', ref: { type: 'plan-item', id: 'checkout-scope', initiative: 'checkout' } },
113
+ { tone: 'warn', tag: 'Due today', text: 'Confirm the 1 Oct numbers', meta: 'yours · due 2026-10-08', ref: item('item-demo001') },
114
+ ],
115
+ last: { id: 'note-demo-sp1', title: 'Sprint planning', at: '2026-10-01T14:00:00', private: false,
116
+ overview: 'The team split the checkout flows into 3 core and 2 later. Dara asked for a scope decision before this sprint.',
117
+ decisions: ['Payments stays in the first release.'], actions: [{ text: 'Send the 3 scope options', owner: 'You', due: 'Wed 7 Oct' }] },
118
+ earlier: 3, purpose: null, prep: null, links: [], refs: [],
119
+ sources: ['the calendar', '3 earlier notes of this meeting', 'the checkout plan', 'your items'],
120
+ } },
107
121
  'm5@2026-10-08': { phase: 'upcoming', held: false, items: [] },
108
122
  };
109
123
 
@@ -251,11 +251,11 @@ const MEET = [
251
251
  { id: 'm1', t: 'Standup', s: 9.5, e: 10, kind: 'ready', routine: true },
252
252
  { id: 'm2', t: 'Checkout sync', s: 10.5, e: 11.25, kind: 'prep ready' },
253
253
  { id: 'm3', t: 'Customer interview', s: 13, e: 14, kind: 'review prep' },
254
- { id: 'm4', t: 'Sprint planning', s: 14, e: 15, kind: 'blocked on your decision', hot: true, decide_by: '14:00' },
254
+ { id: 'm4', t: 'Sprint planning', s: 14, e: 15, kind: 'blocked on your decision', hot: true, decide_by: '14:00', att: ['Dara', 'Priya', 'Sam'] },
255
255
  { id: 'm5', t: 'Leadership update', s: 16.5, e: 17, kind: 'ready' },
256
256
  ];
257
257
  const hh = (x) => pad(Math.floor(x)) + ':' + pad(Math.round((x % 1) * 60));
258
- const ev = (date, m) => ({ id: m.id + '@' + date, title: m.t, at: date + 'T' + hh(m.s) + ':00', end: date + 'T' + hh(m.e) + ':00', kind: m.kind, ...(m.routine ? { mock_routine: true } : {}), ...(m.hot ? { mock_hot: true, mock_decide_by: m.decide_by } : {}) });
258
+ const ev = (date, m) => ({ id: m.id + '@' + date, title: m.t, at: date + 'T' + hh(m.s) + ':00', end: date + 'T' + hh(m.e) + ':00', kind: m.kind, ...(m.att ? { attendees: m.att } : {}), ...(m.routine ? { mock_routine: true } : {}), ...(m.hot ? { mock_hot: true, mock_decide_by: m.decide_by } : {}) });
259
259
  function eventsOn(date) {
260
260
  const dow = new Date(utcOf(date)).getUTCDay();
261
261
  if (date === TODAY) return MEET.map((m) => ev(date, m));
@@ -7,9 +7,10 @@
7
7
  * POST /api/doc-sheet/edit), Ask the agent (the word diff of what the agent proposes and Accept as a slide bound to the proposal's hash: POST /api/doc-sheet/accept),
8
8
  * History (GET /api/doc-sheet/revisions and /at: the revisions and a compare of any one with the one before), Health (POST /api/doc-health/check: the six checks, N of M),
9
9
  * Relations (the document graph around this document, from the library's edges).
10
- * What the page cannot do, it says so: the agent runs in a terminal session, not here, and there is no route that lists its proposals. So in real mode Ask the agent shows
11
- * the request and offers a copy of it, with the Ask button disabled and the reason beside it; the demo (?demo=1) answers the propose and accept routes from fixtures, so the
12
- * whole flow can be seen. Nothing is invented: an edit writes the file through the route, and every save keeps its why in the revisions.
10
+ * Ask the agent (page-starts-agent): in real mode the button starts a background agent run from the page (POST /api/doc-sheet/ask). The run may only read; its
11
+ * answer is recorded as a proposal (GET /api/doc-sheet/ask?id= is polled until it is), and the proposal is accepted only by the PM's slide, bound to its hash.
12
+ * Copy the request stays as the way to give it to a session the PM already has. The demo (?demo=1) answers the propose and accept routes from fixtures.
13
+ * Nothing is invented: an edit writes the file through the route, and every save keeps its why in the revisions.
13
14
  * Pure parts (Node-testable): splitDoc, joinDoc, changedSections, wordDiffTokens, byName, dayName. */
14
15
  import { el } from './dom.js';
15
16
  import { pmGet, pmPost } from './http.js';
@@ -17,6 +18,7 @@ import { c3ic, c3ib } from './pm-work-view.js';
17
18
  import { openSheet } from './c3-sheet.js';
18
19
  import { renderCase } from './c3-case.js';
19
20
  import { say } from './c3-init-extras.js';
21
+ import { c3btn } from './page-agent.js';
20
22
 
21
23
  /* ── pure: sections, round trip, word diff ── */
22
24
 
@@ -115,7 +117,7 @@ const sectionsToText = (before, drafted) => {
115
117
  * need a stored document (why), History says there is none, Health shows `health` ({ passed, total, checks }) as given.
116
118
  * secExtra(name) a node under each section in Read: the report's claims and where they come from. */
117
119
  export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, onChanged, generated, secExtra }) {
118
- const S = { draft: {}, mode, prompt, text: null, err: '', revs: null, texts: {}, cmp: null, health: null, hAsk: false, prop: null, busy: false, changed: false, why: '', metaFor: null };
120
+ const S = { draft: {}, mode, prompt, text: null, err: '', revs: null, texts: {}, cmp: null, health: null, hAsk: false, prop: null, busy: false, changed: false, why: '', metaFor: null, asking: null, closed: false };
119
121
  const demo = isDemo();
120
122
  const base = { ws, path: doc.path };
121
123
  let ui = null;
@@ -217,11 +219,16 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
217
219
  const sug = [...new Set(failing().map((c) => c.fix).filter(Boolean))];
218
220
  const copy = async () => { const t = 'Change ' + doc.path + ': ' + (S.prompt || '').trim(); try { await navigator.clipboard.writeText(t); say('The request is copied. Give it to your agent session.'); } catch { say('Copying is blocked here. Select the request and copy it.'); } };
219
221
  const kids = [ta, el('div', { class: 'chips2' },
220
- demo ? c3ib('Ask the agent', 'spark', { 'data-doc-ask': '', onClick: () => askDemo() }, '') : el('button', { type: 'button', class: 'pill', disabled: true, 'aria-disabled': 'true', 'data-doc-ask': '', title: 'The agent runs in your terminal session. This page cannot start it.' }, c3ic('lock', 'sm'), 'Ask the agent'),
222
+ demo ? c3ib('Ask the agent', 'spark', { 'data-doc-ask': '', onClick: () => askDemo() }, '')
223
+ : S.asking && S.asking.state === 'working' ? c3btn('pill', { disabled: true, 'aria-disabled': 'true', 'data-doc-ask': '' }, [c3ic('clock', 'sm'), 'The agent is drafting'])
224
+ : c3btn('pill', { 'data-doc-ask': '' }, [c3ic('spark', 'sm'), 'Ask the agent'], () => askReal()),
221
225
  !demo ? el('button', { type: 'button', class: 'chip2 lk', 'data-doc-copy': '', onclick: copy }, c3ic('doc', 'sm'), 'Copy the request') : null,
222
226
  ...sug.map((t, i) => el('button', { type: 'button', class: 'chip2 lk', 'data-doc-chip': String(i), onclick: () => { S.prompt = t; draw(); } }, c3ic('spark', 'sm'), t)))];
223
- if (!demo) kids.push(el('div', { class: 'fold', role: 'note', 'data-doc-ask-why': '' }, c3ic('lock', 'sm'), 'The agent works in your terminal session, so this page cannot start it, and no route lists what it proposes. Copy the request to the session; its proposal is recorded through the proposal routes and is accepted here only by your slide, bound to its hash.'));
227
+ const A = S.asking;
228
+ if (!demo && A && A.state === 'working') kids.push(el('div', { class: 'fold', role: 'status', 'data-doc-asking': '' }, c3ic('clock', 'sm'), 'The agent is reading the document and drafting the change. It runs in the background and can only read; you can close this sheet and come back. Its run is listed in Sessions.'));
229
+ else if (!demo && A && A.state === 'failed') kids.push(el('div', { class: 'fold w', role: 'alert', 'data-doc-ask-err': '' }, c3ic('alert', 'sm'), A.error || 'The agent did not answer.'));
224
230
  if (S.prop) kids.push(propCase());
231
+ else if (!demo && !A) kids.push(el('div', { class: 'fold', role: 'note', 'data-doc-ask-why': '' }, c3ic('info', 'sm'), 'The agent drafts the change in the background and shows you the difference. It can only read: nothing is saved until you slide to accept it.'));
225
232
  else if (demo) kids.push(el('div', { class: 'dim', style: 'font-size:var(--fs-2)' }, 'The agent drafts the change as a new version and shows you the difference. Nothing is saved or shared until you accept it.'));
226
233
  return el('div', { class: 'dpn' }, kids);
227
234
  };
@@ -232,6 +239,28 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
232
239
  if (!r || !r.ok) { say((r && r.error) || 'The agent did not answer.'); return; }
233
240
  S.prop = { id: r.id, hash: r.hash, prompt: pr, before, after, nextV: nextV(doc.v) }; draw();
234
241
  }
242
+ /* real mode: start the run from the page, then read its state until the proposal is there */
243
+ async function askReal() {
244
+ const pr = (S.prompt || '').trim(); if (!pr) { say('Tell the agent what to change'); return; }
245
+ S.prop = null; S.asking = { state: 'working' }; draw();
246
+ const r = await pmPost('/api/doc-sheet/ask', { ...base, prompt: pr });
247
+ if (!r || !r.ok) { S.asking = { state: 'failed', error: (r && r.error) || 'The agent could not start.' }; draw(); return; }
248
+ S.asking = { id: r.id, state: 'working' }; draw();
249
+ pollAsk(r.id, pr);
250
+ }
251
+ async function pollAsk(id, pr) {
252
+ while (!S.closed && S.asking && S.asking.id === id) {
253
+ await new Promise((ok) => setTimeout(ok, 3000));
254
+ if (S.closed || !S.asking || S.asking.id !== id) return;
255
+ const r = await pmGet('/api/doc-sheet/ask?id=' + encodeURIComponent(id) + (ws ? '&ws=' + encodeURIComponent(ws) : '')).catch(() => null);
256
+ if (!r || !r.ok || r.state === 'working') continue;
257
+ if (r.state === 'proposed' && r.proposal) { S.asking = null; S.prop = { id: r.proposal.id, hash: r.proposal.hash, prompt: r.proposal.prompt || pr, before: r.proposal.before, after: r.proposal.after, nextV: nextV(doc.v) }; }
258
+ else S.asking = { id, state: 'failed', error: r.error };
259
+ if (S.mode === 'ask') draw();
260
+ say(S.prop ? 'The agent’s draft is ready. Read it, then slide to accept.' : 'The agent did not draft the change.');
261
+ return;
262
+ }
263
+ }
235
264
  function propCase() {
236
265
  const P = S.prop; const D = changedSections(P.before, P.after);
237
266
  const c = renderCase({ id: 'docedit:' + doc.path, sit: 'You asked: “' + P.prompt + '”. The draft changes ' + D.length + ' section' + (D.length === 1 ? '' : 's') + ' and saves as ' + P.nextV + '. Nothing is shared until you send it.', conf: 76,
@@ -299,7 +328,7 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
299
328
  if (had && had.id) { const n = ui.body.querySelector('#' + had.id); if (n) { n.focus(); try { n.setSelectionRange(had.s, had.s); } catch { /* not a text field */ } } }
300
329
  }
301
330
 
302
- ui = openSheet({ title: doc.name, icon: 'doc', dw: true, body: el('div', { class: 'dpn' }), onClose: () => { if (S.changed && onChanged) onChanged(); } });
331
+ ui = openSheet({ title: doc.name, icon: 'doc', dw: true, body: el('div', { class: 'dpn' }), onClose: () => { S.closed = true; if (S.changed && onChanged) onChanged(); } });
303
332
  S.open = (d) => { ui.close(); if (S.openOther) S.openOther(d); };
304
333
  ui.setOpener = (fn) => { S.openOther = fn; };
305
334
  draw();
@@ -23,6 +23,7 @@ import { card, hBars, rowShell, caseNode, markDone, say, short, dayLabel } from
23
23
  import { progressCard } from './c3-plan-panels.js';
24
24
  import { releasePanels } from './releases-view.js';
25
25
  import { ideaScore, normIdea, rankIdeas, capacityUsed, normHyp, hypStatus, HYP_LABEL, GATES, gateLadder, gateCell, normEval, evalVerdict, correctionRate, nineOf, stageBucket } from './lifecycle-lens-model.js';
26
+ import { startAgent, startedLine } from './page-agent.js';
26
27
 
27
28
  const isDemo = () => !!window.__c3demo;
28
29
  const SVGNS = 'http://www.w3.org/2000/svg';
@@ -262,12 +263,12 @@ function ideaCase(V, x, ranked) {
262
263
  const spike = x.conf < 0.6 && x.imp >= 7;
263
264
  return { sit: x.why || 'No reason is recorded for this idea.', conf: pct(x.conf), chg, cta: 'Slide to confirm',
264
265
  opts: [{ id: 'sh', l: 'Shortlist it', rec: top, fx: [['check', 'Joins the prioritization', 'ok']] }, { id: 'cm', l: 'Commit it to the plan', fx: [['flag', 'Gets scope and an owner', 'ok']] },
265
- { id: 'sp', l: 'Run a 1-day spike first', rec: spike, outward: !isDemo(), fx: [['clock', 'Cheap test of the assumption', 'ok']] }, { id: 'pk', l: 'Park it', rec: !top && x.conf < 0.5 && x.imp < 6, fx: [['moon', 'Out of the way', 'warn']] }],
266
- outboxReason: 'A spike is scoped by an agent session, and this page cannot start one. Ask for it in your terminal.' };
266
+ { id: 'sp', l: 'Run a 1-day spike first', rec: spike, fx: [['clock', 'Cheap test of the assumption', 'ok'], ['spark', isDemo() ? 'An agent scopes it' : 'Starts an agent session that scopes it', '']] }, { id: 'pk', l: 'Park it', rec: !top && x.conf < 0.5 && x.imp < 6, fx: [['moon', 'Out of the way', 'warn']] }] };
267
267
  }
268
268
  function ideaRow(V, x, ranked, lens) {
269
269
  const sc = ideaScore(x); const st = ST_COLOR[x.st] || ST_COLOR.new;
270
270
  const decide = (o) => { const to = { sh: ['short', 'Shortlisted.'], cm: ['committed', 'Committed. It appears in Plan as work to break down.'], pk: ['parked', 'Parked.'] }[o.id];
271
+ if (!to && !isDemo()) { startAgent({ ws: V.ctx.ws, task: 'spike', initiative: x.init || '', what: x.n }).then((r) => { say(startedLine(r, 'the spike')); if (r.ok) markDone(V, 'idea:' + x.id, 'Spike started in Sessions'); }); return; }
271
272
  if (!to) { say('Spike scoped as a draft.'); markDone(V, 'idea:' + x.id, 'Spike scoped as a draft.'); return; }
272
273
  setIdeaStatus(V, x, to[0], to[1]).then((u) => { if (u) markDone(V, 'idea:' + x.id, to[1], async () => { await u(); }); }); };
273
274
  const swipe = (status, msg) => () => { setIdeaStatus(V, x, status, msg); };
@@ -307,15 +308,17 @@ function gateCase(V, id, lad) {
307
308
  const chg = lad.gates.map((g) => [g.state === 'done' ? 'check' : g.state === 'na' ? 'moon' : g.state === 'unknown' ? 'info' : 'clock', g.name + ': ' + (g.state === 'done' ? 'passed' : g.state === 'next' ? 'next' : g.state === 'na' ? 'not required' : g.state === 'unknown' ? 'not claimed by the engine' : 'not yet') + (g.evidence ? '. ' + g.evidence : ''), g.key, g.state === 'done' ? 'ok' : g.state === 'next' ? 'warn' : '']);
308
309
  lad.violations.forEach((v) => chg.push(['alert', 'Order violation: ' + v, 'order', 'bad']));
309
310
  return { sit: nx ? (lad.done ? lad.done + ' gate' + (lad.done === 1 ? '' : 's') + ' passed, so ' + nx.name + ' is next. ' : '') + 'Asking for the ' + nx.name + ' gate early surfaces problems while they are cheap.' : 'Every gate that applies is passed.', conf: 72, chg, cta: 'Slide to request',
310
- opts: nx ? [{ id: 'r', l: 'Request an early ' + nx.name, rec: 1, outward: !isDemo(), fx: [['check', 'Finds problems early', 'ok']] }, { id: 'w', l: 'Wait for ' + (lad.gates.find((g) => g.key === 'build' && g.state === 'next') ? 'build to finish' : 'the work to reach it'), fx: [['clock', 'Later feedback', 'warn']] }] : [],
311
- outboxReason: 'A gate is requested from an agent session, and this page cannot start one. Ask for it in your terminal.', go: ['open', id, 'Open ' + nm] };
311
+ opts: nx ? [{ id: 'r', l: 'Request an early ' + nx.name, rec: 1, fx: [['check', 'Finds problems early', 'ok'], ['spark', isDemo() ? 'An agent reviews it' : 'Starts an agent session that reviews it', '']] }, { id: 'w', l: 'Wait for ' + (lad.gates.find((g) => g.key === 'build' && g.state === 'next') ? 'build to finish' : 'the work to reach it'), fx: [['clock', 'Later feedback', 'warn']] }] : [],
312
+ go: ['open', id, 'Open ' + nm] };
312
313
  }
313
314
  function gateRow(V, id, slot) {
314
315
  const nm = nameOf(V, id);
315
316
  if (slot.state === 'loading') return el('div', { class: 'gtr', 'data-life-gate': id }, el('b', null, nm), el('span', { class: 'dim' }, c3ic('clock', 'sm'), 'Reading the gates…'), el('span'));
316
317
  if (!slot.row) return el('div', { class: 'gtr', 'data-life-gate': id }, el('b', null, nm), el('span', { class: 'dim' }, c3ic('alert', 'sm'), slot.err || 'No record for this initiative, so no gate is claimed.'), el('span', { class: 'chip-s', style: '--c:var(--muted-foreground)' }, 'unknown'));
317
318
  const lad = gateLadder(slot.row);
318
- const open = () => { const model = gateCase(V, id, lad); const sh = openSheet({ title: nm + ': gates', icon: 'shield', body: () => renderCase({ ...model, id: 'gate:' + id }, { outbox: false, onConfirm: (r) => { say(r.option.id === 'r' ? 'Early review requested.' : 'Waiting.'); sh.close(); }, onGo: () => { sh.close(); if (V.ctx.openInitiative) V.ctx.openInitiative(V.ctx.repo, id); } }) }); };
319
+ const open = () => { const model = gateCase(V, id, lad); const sh = openSheet({ title: nm + ': gates', icon: 'shield', body: () => renderCase({ ...model, id: 'gate:' + id }, { outbox: false, onConfirm: (r) => {
320
+ if (r.option.id === 'r' && !isDemo() && lad.next) { startAgent({ ws: V.ctx.ws, task: 'gate', initiative: id, what: lad.next.name }).then((x) => say(startedLine(x, 'the early ' + lad.next.name + ' review'))); sh.close(); return; }
321
+ say(r.option.id === 'r' ? 'Early review requested.' : 'Waiting.'); sh.close(); }, onGo: () => { sh.close(); if (V.ctx.openInitiative) V.ctx.openInitiative(V.ctx.repo, id); } }) }); };
319
322
  const bar = el('div', { class: 'hbs', style: 'gap:2px' }, el('div', { class: 'gt', role: 'img', 'aria-label': 'Quality gates', 'aria-description': lad.gates.map((g) => g.name + ' ' + g.state).join(', ') }, lad.gates.map((g) => el('i', { class: gateCell(g), title: g.name + ': ' + g.state + (g.evidence ? '. ' + g.evidence : '') }))),
320
323
  el('div', { class: 'gtl' }, GATES.map(([n]) => el('span', null, n))));
321
324
  return el('div', { class: 'gtr', 'data-life-gate': id, role: 'button', tabindex: '0', style: 'cursor:pointer;min-height:44px', title: 'Open the case', onClick: open, onKeydown: (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(); } } },
@@ -11,7 +11,7 @@
11
11
  * links and references, people. A note with no meeting says so and offers the main app's link pickers.
12
12
  * During the main app's live block while the note is recorded or paused (view-notes.js renderLive), else the transcript.
13
13
  * After the Notes card (decisions with why, next steps with owner, due and Make task, questions, conflicts, highlights,
14
- * the folded transcript) from final_notes, else from what was caught live (dest-notes.js partsOf).
14
+ * the screenshots of the shared screen, the folded transcript) from final_notes, else from what was caught live (dest-notes.js partsOf).
15
15
  *
16
16
  * Nothing is invented: a part the note does not hold is left out or says so. The mockup's Rejected column, Settle, told and
17
17
  * not told, Route actions and the drafts have no data or route yet, so they are not drawn.
@@ -30,6 +30,7 @@ import { groupNotes, partsOf, conflictsOf, whenShort } from './dest-notes.js';
30
30
  import * as dock from './note-dock.js';
31
31
  import * as V from './view-notes.js';
32
32
  import { makeTask } from './notes-task.js';
33
+ import { screenStrip } from './notes-screen-view.js';
33
34
 
34
35
  const PHASES = [['before', 'clip', 'Before'], ['during', 'mic', 'During'], ['after', 'notes', 'After']];
35
36
  const startOf = (n) => (n.meeting && n.meeting.start) || n.recorded_from || n.created_at || n.updated_at;
@@ -91,13 +92,13 @@ async function read(s) {
91
92
  if (seq !== s.seq) return;
92
93
  if (l && l.ok) { s.rows = l.notes || []; s.err = null; } else { s.rows = s.rows || []; s.err = (l && l.error) || 'The notes could not be read.'; }
93
94
  if (!s.open && s.rows.length) { s.open = s.rows[0].id; return read(s); }
94
- if (one && one.ok && one.note && one.note.id === s.open) s.note = one.note;
95
+ if (one && one.ok && one.note && one.note.id === s.open) { s.note = one.note; s.shots = (one.screen && one.screen.frames) || []; s.rec = one.recording || null; }
95
96
  draw(s);
96
97
  }
97
98
 
98
99
  function choose(s, id) {
99
100
  if (s.open === id) return;
100
- s.open = id; s.note = null; s.phase = null;
101
+ s.open = id; s.note = null; s.phase = null; s.shots = []; s.rec = null;
101
102
  draw(s); read(s);
102
103
  }
103
104
 
@@ -164,14 +165,45 @@ function noteCol(s) {
164
165
  el('div', { class: 'stg', role: 'group', 'aria-label': 'Note stage', 'data-cv': 'phase' },
165
166
  PHASES.map(([k, ic, l]) => btn('sgb', { 'data-cal3-phase': k, 'aria-pressed': String(ph === k), title: l }, [c3ic(ic), el('span', null, l)], () => { s.phase = k; draw(s); })))),
166
167
  el('div', { class: 'chips2 dim cal3-sm' },
167
- [dayOf(startOf(n)), len, iniName(n.initiative) || 'No initiative'].filter(Boolean).join(' · '),
168
- ev ? btn('chip2 lk', { 'data-cal3-notes': 'show-meeting', title: 'Show the meeting in the calendar' }, [c3ic('cal', 'sm'), 'Show in the calendar'], () => showMeeting(s, ev))
169
- : linked(n) ? null : el('span', { class: 'tag' }, 'no meeting'),
170
- btn('ib sm', { title: 'Copy as Markdown (with the meeting context)', 'aria-label': 'Copy as Markdown', 'data-cal3-notes': 'copy' }, c3ic('clip'), () => copy(n))));
168
+ [dayOf(startOf(n)), len].filter(Boolean).join(' · '),
169
+ btn('ib sm', { title: 'Copy as Markdown (with the meeting context)', 'aria-label': 'Copy as Markdown', 'data-cal3-notes': 'copy' }, c3ic('clip'), () => copy(n))),
170
+ links(s, n, ev));
171
171
  const body = ph === 'before' ? before(s, n) : ph === 'during' ? during(s, n, mode) : after(s, n, mode);
172
172
  return el('div', { class: 'cal3-ncol', 'data-cal3-notes': 'note', 'data-cal3-note': n.id, 'data-cal3-stage': ph }, head, ...[].concat(body));
173
173
  }
174
174
 
175
+ /* the note's meeting and initiative, each with its link: what it is linked to, then Link or Change (view-notes.js pickers) */
176
+ function links(s, n, ev) {
177
+ const adhoc = n.meeting && n.meeting.kind === 'adhoc';
178
+ const when = ev ? [dayOf(ev.start), hm(ev.start) + (ev.end ? ' to ' + hm(ev.end) : '')].filter(Boolean).join(' ') : '';
179
+ return el('div', { class: 'cal3-nlinks cal3-sm', 'data-cal3-notes': 'links' },
180
+ el('span', { class: 'chips2' }, c3ic('cal', 'sm'),
181
+ ev ? el('span', { 'data-cal3-notes': 'meeting' }, el('b', null, ev.title), ' · ' + when)
182
+ : el('span', { class: 'dim', 'data-cal3-notes': 'meeting' }, adhoc ? 'A meeting that is not on the calendar' : 'No meeting'),
183
+ !ev && !adhoc ? el('span', { class: 'tag' }, 'no meeting') : null,
184
+ btn('chip2 lk', { 'data-cal3-notes': 'meeting-link', title: ev || adhoc ? 'Link the note to another meeting' : 'Link the note to a meeting on the calendar' }, ev || adhoc ? 'Change' : 'Link to a meeting', () => V.linkMeeting(vctx(s), n.id)),
185
+ ev ? btn('chip2 lk', { 'data-cal3-notes': 'show-meeting', title: 'Show the meeting in the calendar' }, [c3ic('next', 'sm'), 'Show in the calendar'], () => showMeeting(s, ev)) : null),
186
+ el('span', { class: 'chips2' }, c3ic('init', 'sm'),
187
+ n.initiative ? el('span', { 'data-cal3-notes': 'initiative' }, el('b', null, iniName(n.initiative))) : el('span', { class: 'dim', 'data-cal3-notes': 'initiative' }, 'No initiative'),
188
+ btn('chip2 lk', { 'data-cal3-notes': 'initiative-link', title: n.initiative ? 'Link the note to another initiative' : 'Link the note to an initiative' }, n.initiative ? 'Change' : 'Link to an initiative', () => V.linkInitiative(vctx(s), n.id))));
189
+ }
190
+
191
+ /* the replay of the note's kept audio: one <audio> per note, kept across redraws so playback goes on (as dest-notes.js does) */
192
+ function replayOf(s, n) {
193
+ const rec = s.rec;
194
+ if (!rec || n.private) { s.audio = null; return null; }
195
+ const key = n.id + ':' + rec.bytes + ':' + (rec.cleaned ? 1 : 0);
196
+ if (s.audio && (s.audio.key === key || !s.audio.el.paused)) return s.audio.el;
197
+ const a = el('audio', { controls: true, preload: 'metadata', class: 'nt-audio cal3-audio', 'data-cal3-notes': 'audio', src: '/api/notes/recording?id=' + encodeURIComponent(n.id) + '&b=' + rec.bytes + (rec.cleaned ? '&c=1' : '') });
198
+ s.audio = { key, el: a };
199
+ return a;
200
+ }
201
+ function seek(s, ms) {
202
+ const a = s.audio && s.audio.el;
203
+ if (!a) return;
204
+ try { a.currentTime = Math.max(0, ms / 1000); a.play(); } catch { /* the audio is not ready */ }
205
+ }
206
+
175
207
  const vctx = (s) => ({ state: {}, setState: () => { if (s.box.isConnected) { read(s); } } });
176
208
  const linkButtons = (s, n) => el('div', { class: 'chips2' },
177
209
  btn('chip2 lk', { 'data-cal3-notes': 'link-meeting' }, [c3ic('cal', 'sm'), 'Link to a meeting'], () => V.linkMeeting(vctx(s), n.id)),
@@ -206,7 +238,9 @@ function before(s, n) {
206
238
  if (names.length) rows.push(prepRow('user', 'People', el('span', { class: 'chips2 cal3-wrapl' }, names.map(avatar))));
207
239
  else if (c && c.attendees) rows.push(prepRow('user', 'People', c.attendees + ' attendees. Names are not kept while calendar names are off.'));
208
240
  const when = (ev.start ? dayOf(ev.start) + ' ' + hm(ev.start) : '') + (ev.end ? ' to ' + hm(ev.end) : '');
209
- rows.push(prepRow('cal', 'Meeting', [ev.title, when, c && c.series ? 'series: ' + c.series : null, c && c.role ? 'your role: ' + c.role : null].filter(Boolean).join(' · ')));
241
+ rows.push(prepRow('cal', 'Meeting', [ev.title, when, c && c.where ? 'where: ' + c.where : null, c && c.organizer ? 'organizer: ' + c.organizer : null,
242
+ c && c.series ? 'series: ' + c.series : null, c && c.role ? 'your role: ' + c.role : null, c && c.external ? 'with people outside the company' : null,
243
+ c && c.attendees && names.length ? c.attendees + ' attendees' : null].filter(Boolean).join(' · ')));
210
244
  return el('section', { class: 'card', 'data-cv': 'stage-before' }, h3, ...rows,
211
245
  c ? null : el('p', { class: 'dim cal3-sm' }, 'The calendar gave no more about this meeting.'));
212
246
  }
@@ -252,6 +286,7 @@ function after(s, n, mode) {
252
286
  const kids = [
253
287
  el('div', { class: 'c3cv-head' }, el('h3', null, c3ic('notes'), 'Notes'), recordButton(s, n, mode)),
254
288
  el('div', { class: 'chips2' }, el('b', null, n.title || 'Untitled note'), el('span', { class: 'dim mono cal3-sm' }, [dayOf(startOf(n)), mins(lenOf(n))].filter(Boolean).join(' · ')), ...names.map(avatar)),
289
+ ...(() => { const rp = mode ? null : replayOf(s, n); return rp ? [el('div', { class: 'cal3-replay', 'data-cal3-notes': 'replay' }, el('h4', null, 'Replay'), rp, el('p', { class: 'dim cal3-sm' }, 'Kept on this machine.'))] : []; })(),
255
290
  ...cf.map((c) => el('div', { class: 'fold w', 'data-cal3-notes': 'conflict' }, c3ic('alert', 'sm'), '“' + c.text + '” differs from ', el('span', { class: 'mono' }, c.doc + (c.line ? ':' + c.line : '')))),
256
291
  p.overview ? editable(p.from === 'final', 'p', p.overview, 'Overview', (t) => saveFinal(s, n, 'overview', 0, t), { class: 'cal3-ov', 'data-cal3-notes': 'overview' })
257
292
  : el('p', { class: 'dim cal3-ov', 'data-cal3-notes': 'no-overview' }, mode ? 'The overview is written when you stop recording.' : 'No overview yet.'),
@@ -268,7 +303,10 @@ function after(s, n, mode) {
268
303
  const hl = n.highlights || [];
269
304
  if (hl.length) kids.push(el('div', { class: 'cal3-ctx', 'data-cal3-notes': 'highlights' }, el('h4', null, 'Highlights'),
270
305
  el('ul', { class: 'cal3-cl', role: 'list' }, hl.map((x) => el('li', { class: 'cal3-ci' }, el('span', { class: 'dim mono cal3-at' }, typeof x.t === 'number' ? V.fmtTime(x.t) : ''), el('span', { class: 'tag' }, x.label || 'Highlight'), el('span', { class: 'cal3-ct' }, x.text))))));
271
- if (!p.overview && !p.decisions.length && !p.actions.length && !p.questions.length && !hl.length && !cf.length) kids.push(fold('moon', mode ? 'Decisions and next steps show here as they are caught, and in full after you stop.' : 'No decisions, next steps or questions are held for this note.', { 'data-cal3-notes': 'nothing' }));
306
+ if (!p.overview && !p.decisions.length && !p.actions.length && !p.questions.length && !hl.length && !cf.length && !(s.shots || []).length) kids.push(fold('moon', mode ? 'Decisions and next steps show here as they are caught, and in full after you stop.' : 'No decisions, next steps or questions are held for this note.', { 'data-cal3-notes': 'nothing' }));
307
+ // the screenshots of the shared screen, after Stop, as the main app's Notes shows them (notes-screen-view.js)
308
+ const shots = mode || n.private ? [] : s.shots || [];
309
+ if (shots.length) kids.push(el('div', { 'data-cal3-notes': 'screen' }, screenStrip({ id: n.id, frames: shots, seek: s.audio ? (ms) => seek(s, ms) : null, replay: s.audio ? s.audio.el : null })));
272
310
  const tr = transcriptOf(n);
273
311
  if (!mode && tr.length) kids.push(el('details', { 'data-cal3-notes': 'transcript-fold' }, el('summary', { class: 'dim cal3-sm' }, 'Transcript · ' + tr.length + (tr.length === 1 ? ' line' : ' lines')),
274
312
  el('div', { class: 'term cal3-term' }, tr.map((x) => V.fmtTime(x.t || 0) + ' ' + x.text).join('\n'))));
@@ -529,6 +529,9 @@
529
529
  .c3 .cal3-ov{margin:0}
530
530
  .c3 .cal3-tdr{text-align:right}
531
531
  .c3 .cal3-ncard{display:grid;gap:var(--sp-2)}
532
+ .c3 .cal3-nlinks{display:grid;gap:var(--sp-1);margin-top:var(--sp-1)}
533
+ .c3 .cal3-replay{display:grid;gap:var(--sp-1)}
534
+ .c3 .cal3-replay audio{width:100%;max-width:100%}
532
535
  /* a primitives.js button drawn as a console3 control: the primitive's own height, padding, nowrap and hover come off */
533
536
  .c3 .cal3-b{height:auto;min-height:0;line-height:inherit;font-weight:inherit;justify-content:flex-start;text-align:left}
534
537
  .c3 .cal3-b.chip2{padding:0;border:0;background:transparent;white-space:nowrap}
@@ -1900,3 +1903,7 @@
1900
1903
  .c3 .kp.k-foreground{--c:var(--foreground)}
1901
1904
  .c3 .kp.k-muted-foreground{--c:var(--muted-foreground)}
1902
1905
  .c3 .psess{display:inline-flex;gap:6px;align-items:center}
1906
+ /* the card before a meeting (initiative c3-meeting-card) */
1907
+ .c3 .cal3-ctx .cal3-p{margin:0;font-size:var(--fs-2);line-height:1.5;color:var(--foreground)}
1908
+ .c3 .cal3-ctx.cal3-gist{padding:10px 12px;border:1px solid var(--border);border-radius:8px;background:var(--muted)}
1909
+ .c3 .cal3-ctx h4{display:flex;align-items:center;gap:6px}
@@ -15,8 +15,8 @@
15
15
  * the Build and Deliver lists of Jira tickets and releases (Releases is its own view).
16
16
  * What a stage needs (c3needs, line 4713): one row per requirement of the stage (REQ in the model), for each initiative at it, each with a
17
17
  * case file in place: an agent's requirement offers "Have the agent: draft it" and "Not yet"; the PM's decision offers its two answers;
18
- * the factory's is read-only. This page cannot start an agent or record a stage decision, so in real mode those slides are drawn
19
- * disabled with the reason; the demo (?demo=1) lets the slide mark it done. Park or Kill goes through the lifecycle route with the reason
18
+ * the factory's is read-only. In real mode the agent's slide starts an agent session for the draft (page-agent.js, page-starts-agent),
19
+ * listed in Sessions; the PM's decision has no route yet, so that slide is drawn disabled with the reason. The demo (?demo=1) lets the slide mark it done. Park or Kill goes through the lifecycle route with the reason
20
20
  * as the option. Swipe right or left and the keys e, n, j, k work on every row. A deck card per initiative at the stage (five at most; the rest are in
21
21
  * the list) replaces the mockup's five invented cards.
22
22
  * Self-registers as the 'lifecycle' Initiatives view, like Timeline and Releases.
@@ -28,6 +28,7 @@ import { setLifecycle } from './initiatives-api.js';
28
28
  import { STAGES, LABELS, bucketRows, stageNote, nineOf, REQ, reqWho, prevPending, DRAFT_TEXT, DECIDE_TEXT } from './lifecycle-lens-model.js';
29
29
  import { rowShell, caseNode, markDone, say } from './c3-init-extras.js';
30
30
  import { lensPanels } from './c3-lifecycle-panels.js';
31
+ import { startAgent, startedLine } from './page-agent.js';
31
32
  import { strategyPanel } from './c3-strategy-panels.js';
32
33
  import { ensureC3Css, c3ic, c3ring, c3Phone } from './pm-work-view.js';
33
34
 
@@ -106,8 +107,7 @@ function rqCase(V, row, L, r) {
106
107
  if (who === 'agent') {
107
108
  const t = DRAFT_TEXT[key] || ['Draft it', ''];
108
109
  return { sit: 'Nothing has this yet. An agent drafts it and you read it before it counts. ' + nm + ' stays at ' + stg + ' until it exists.', conf: 82, chg: base, cta: 'Slide to start the draft', go,
109
- outboxReason: 'An agent session does the drafting, and this page cannot start one. Ask it in your terminal; the draft comes back for you to read.',
110
- opts: [{ id: 'go', l: 'Have the agent: ' + t[0].toLowerCase(), rec: 1, outward: !demo, fx: [['spark', t[1], 'ok'], ['shield', 'Inside the repo; nothing goes outside', 'ok']] }, { id: 'nt', l: 'Not yet', fx: [['clock', 'The stage stays at ' + stg, 'warn']] }] };
110
+ opts: [{ id: 'go', l: 'Have the agent: ' + t[0].toLowerCase(), rec: 1, fx: [['spark', t[1], 'ok'], ['shield', 'Inside the repo; nothing goes outside', 'ok']] }, { id: 'nt', l: 'Not yet', fx: [['clock', 'The stage stays at ' + stg, 'warn']] }] };
111
111
  }
112
112
  const pend = prevPending(L, key, (k) => reqDone(V, row, L, k));
113
113
  if (pend.length) return { sit: 'This is your decision, and it comes after the agent’s drafts. Read them first: approving without reading is how errors go out.', conf: 70, chg: [['clock', 'Waiting for: ' + pend.map((q) => q[1]).join('; '), 'agent', 'warn']], opts: [], go };
@@ -121,10 +121,14 @@ function rqCase(V, row, L, r) {
121
121
  function rqRow(V, row, L, r) {
122
122
  const key = row.id + '|' + r[0]; const w = reqWho(r[2]); const done = reqDone(V, row, L, r[0]); const scope = V.scope;
123
123
  const decide = (o) => {
124
+ if (o.id === 'go' && !isDemo()) {
125
+ startAgent({ ws: V.ctx.ws, task: 'draft', initiative: row.id, what: r[1] }).then((res) => { say(startedLine(res, r[1])); if (res.ok) markDone(V, 'rq:' + key, 'Started in Sessions'); });
126
+ return;
127
+ }
124
128
  if (o.id === 'go' || o.id === 'ok') { V.rq = V.rq || {}; V.rq[key] = 1; const m = o.id === 'go' ? 'Drafted. It waits in Review for you.' : o.l + '. Recorded as your decision.'; say(m, () => { delete V.rq[key]; V.render(); }); markDone(V, 'rq:' + key, m, () => { delete V.rq[key]; }); } else markDone(V, 'rq:' + key, 'Left as it is.');
125
129
  };
126
130
  const sw = { labelRight: r[2] === 'agent' ? 'start the draft' : 'open it first', labelLeft: 'leave it', iconRight: 'spark', iconLeft: 'moon',
127
- onRight: () => say(r[2] === 'agent' && !done ? (isDemo() ? 'Open it, pick the draft, then slide: a draft starts only from its case file.' : 'An agent session does the drafting. This page cannot start one.') : done ? 'Already done.' : 'Open it first: a decision needs its case file.'),
131
+ onRight: () => say(r[2] === 'agent' && !done ? 'Open it, pick the draft, then slide: a draft starts only from its case file.' : done ? 'Already done.' : 'Open it first: a decision needs its case file.'),
128
132
  onLeft: () => say('Left as it is.') };
129
133
  const pend = !done && r[2] === 'pm' && prevPending(L, r[0], (k) => reqDone(V, row, L, k)).length;
130
134
  return rowShell(V, { key: 'rq' + key, attrs: { 'data-rq': key, 'data-id': 'rq|' + key, 'data-lc-card': row.id }, icon: done ? 'check' : w[0], iconColor: done ? 'var(--success)' : null, sw,
@@ -8,8 +8,8 @@
8
8
  *
9
9
  * PAUSE keeps the meeting open and resumable for an hour (the server ends it by itself after that and writes the final
10
10
  * notes). STOP ends it for good, asks first, and writes the final notes now. A stopped meeting cannot be resumed. A note
11
- * that is stopped within a minute, or before anything was heard, with nothing typed, is deleted instead (a recording started
12
- * by mistake must not take the place of the meeting before it). Resume applies to the most recent meeting only.
11
+ * that is stopped before anything was heard, with nothing typed, is deleted instead (a recording started by mistake must not
12
+ * take the place of the meeting before it). A short note with speech in it is kept. Resume applies to the most recent meeting only.
13
13
  *
14
14
  * What is held is read, not remembered: the listening note comes from pm/view-notes.js, and a paused meeting from the note
15
15
  * rows (GET /api/notes: state, resume_until, sessions), read again every REREAD_MS and after each action.
@@ -28,7 +28,6 @@ import { state, setState, subscribe, FIXTURE } from './state.js';
28
28
  import { meetingOf, refreshNotes, noteById, notesRows, onNotes } from './notes-links.js';
29
29
 
30
30
  const REREAD_MS = 30000;
31
- const MIN = 60000;
32
31
  let V = null; // pm/view-notes.js, read the first time it is needed
33
32
  let mounted = false;
34
33
  const viewCtx = { state, setState, FIXTURE };
@@ -163,14 +162,12 @@ export async function resumeNow() {
163
162
  return ok;
164
163
  }
165
164
 
166
- /** Is this note one that was started by mistake? Within a minute, or nothing heard, nothing typed, no highlight, one stretch. Pure. */
167
- export function discardable(n, now = Date.now()) {
165
+ /** Is this note one that was started by mistake? Nothing heard, nothing typed, no highlight, one stretch. A short note with speech is kept. Pure. */
166
+ export function discardable(n) {
168
167
  if (!n || String(n.body || '').trim() || (n.highlights || []).length) return false;
169
168
  const s = n.sessions || [];
170
169
  if (s.length !== 1) return false;
171
- const from = Date.parse(s[0].from); const to = s[0].to ? Date.parse(s[0].to) : now;
172
- const heard = (n.transcript || []).some((x) => !x.dropped);
173
- return !heard || to - from < MIN;
170
+ return !(n.transcript || []).some((x) => !x.dropped);
174
171
  }
175
172
 
176
173
  /** The "Stop for good?" pop-up. Resolves 'stop', 'pause' (only while recording) or null. */
@@ -0,0 +1,25 @@
1
+ /* pm/page-agent.js — start an agent session from a console3 press (initiative page-starts-agent).
2
+ *
3
+ * startAgent({ ws, task, initiative, what }) → Promise of { ok, session?, error? }
4
+ *
5
+ * POST /api/agent/start: the server writes the prompt for the task ('draft', 'spike' or 'gate'; lib/page-agent.js) and starts
6
+ * the session the way Next step › Go does, so it is listed in Sessions. The page sends no prompt text of its own. */
7
+ import { pmPost } from './http.js';
8
+ import { button } from './primitives.js';
9
+
10
+ /** A native button from primitives.js drawn as the console3 control `cls` (pill, chip2 lk); cal3-b takes the primitive's own
11
+ * height, padding and hover off (console3.css), as c3-notes.js does. */
12
+ export function c3btn(cls, attrs, kids, onClick) {
13
+ const { disabled, ...rest } = attrs || {};
14
+ const b = button({ variant: 'ghost', onClick, disabled: !!disabled, attrs: { ...rest, class: 'cal3-b ' + cls } });
15
+ b.replaceChildren(...[].concat(kids).filter((k) => k != null && k !== false));
16
+ return b;
17
+ }
18
+
19
+ export async function startAgent({ ws, task, initiative, what }) {
20
+ const r = await pmPost('/api/agent/start', { ws, task, initiative: initiative || '', what: String(what || '') }).catch(() => null);
21
+ return r && r.ok ? r : { ok: false, error: (r && r.error) || 'The agent could not start.' };
22
+ }
23
+
24
+ /** The line the page says after a start: where the session is, or why it did not start. */
25
+ export const startedLine = (r, what) => (r.ok ? 'The agent started on ' + what + '. Follow it in Sessions; its draft waits for you to read.' : 'Not started: ' + r.error);