create-pmos 0.186.0 → 0.188.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 (43) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +1 -1
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/scripts/workflow-state.js +2 -1
  6. package/payload/state/discoveries.md +1 -1
  7. package/workspace/c3-ib-triage-backlog-test.js +20 -13
  8. package/workspace/c3-ini-life-test.js +167 -0
  9. package/workspace/c3-initiatives-test.js +6 -6
  10. package/workspace/c3-phone-test.js +67 -16
  11. package/workspace/c3-quiet-test.js +2 -1
  12. package/workspace/guide/initiative-boards.md +37 -2
  13. package/workspace/guide/my-work.md +1 -0
  14. package/workspace/guide/phone-companion.md +19 -4
  15. package/workspace/lib/day-run.js +4 -3
  16. package/workspace/lib/engine/boards.js +73 -18
  17. package/workspace/lib/engine/workflow-state.js +2 -1
  18. package/workspace/lib/initiative-rows.js +2 -2
  19. package/workspace/lib/my-work.js +2 -1
  20. package/workspace/lib/routes-boards.js +119 -6
  21. package/workspace/lib/routes-meeting-focus.js +19 -7
  22. package/workspace/lib/vendor/scripts/workflow-state.js +2 -1
  23. package/workspace/lib/work-assigned.js +1 -1
  24. package/workspace/package-lock.json +2 -2
  25. package/workspace/package.json +1 -1
  26. package/workspace/public/pm/c3-demo-boards.js +82 -17
  27. package/workspace/public/pm/c3-demo-focus.js +39 -1
  28. package/workspace/public/pm/c3-focus.css +53 -0
  29. package/workspace/public/pm/c3-focus.js +188 -44
  30. package/workspace/public/pm/c3-ib-initiatives.js +127 -60
  31. package/workspace/public/pm/c3-ib-items.js +2 -2
  32. package/workspace/public/pm/c3-ini-life.css +32 -0
  33. package/workspace/public/pm/c3-ini-life.js +379 -0
  34. package/workspace/public/pm/c3-ini-ui.js +12 -7
  35. package/workspace/public/pm/c3-initiatives.js +14 -4
  36. package/workspace/public/pm/c3-phone.css +3 -1
  37. package/workspace/public/pm/c3-phone.js +28 -8
  38. package/workspace/public/pm/console3.css +3 -0
  39. package/workspace/public/pm/pm-work-view.js +11 -5
  40. package/workspace/public/pm/run-card.js +12 -3
  41. package/workspace/public/pm/sessions-panel.js +2 -2
  42. package/workspace/server.js +25 -3
  43. package/workspace/test-lists/c3-ini-life.json +9 -0
@@ -1583,6 +1583,7 @@
1583
1583
  .c3 .raw{background:var(--muted);border-radius:8px;padding:var(--sp-2) var(--sp-3);font:var(--fs-1)/1.6 var(--mono);color:var(--muted-foreground);overflow-wrap:anywhere;white-space:pre-wrap}
1584
1584
  .c3 .c3t-card .draft{background:var(--muted);border-radius:10px;padding:var(--sp-3);font-size:var(--fs-3);white-space:pre-wrap;overflow-wrap:anywhere}
1585
1585
  .c3 .c3t-tray{display:flex;gap:var(--sp-2);flex-wrap:wrap;align-items:center}
1586
+ .c3 .c3t-grow{flex:1}
1586
1587
  .c3 .c3t-tray input,.c3 .c3t-panel input{flex:1;min-width:120px;border:1px solid var(--border);border-radius:99px;padding:6px 12px;background:var(--card);color:var(--foreground);font:inherit;font-size:var(--fs-2)}
1587
1588
  .c3 .c3t-tray input[readonly],.c3 .c3t-panel input[readonly]{opacity:.6;cursor:not-allowed}
1588
1589
  .c3 .c3t-card .pill.rec{background:var(--acc);color:var(--on-accent);border-color:var(--acc)}
@@ -1884,6 +1885,8 @@
1884
1885
  .c3 .wk-agent-ask input{flex:1;min-width:0;font:inherit;font-size:var(--fs-2);padding:4px 8px;border:1px solid var(--border);background:var(--background);color:var(--foreground);border-radius:6px}
1885
1886
  .c3 .wk-agent-ask[hidden]{display:none}
1886
1887
  .c3 .wk-agent-sheet{display:grid;gap:var(--sp-3)}
1888
+ .c3 .wk-agent-st{display:inline-flex;align-items:center;gap:4px;max-width:100%;min-width:0}
1889
+ .c3 .wk-agent-tx{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1887
1890
  .c3 .wk-agent-will{font-size:var(--fs-2)}
1888
1891
  .c3 .wk-agent-sum{margin:0}
1889
1892
  .c3 .wk-agent-body{--tc:var(--acc)}
@@ -707,14 +707,15 @@ const minsAgo = (iso) => { const t = Date.parse(iso); if (!Number.isFinite(t)) r
707
707
  const whereOf = (item) => (item.origin === 'jira' ? 'Jira' : 'its source');
708
708
  const AG = {
709
709
  can: (t) => 'Agent can: ' + t, queued: 'Agent: queued', working: (ago) => 'Agent: working' + (ago ? ' · started ' + ago : ''), ready: 'Agent: draft ready',
710
- sending: 'Agent: posting', sent: (w, at) => 'Posted to ' + w + (at ? ' ' + at : ''), failed: (why) => 'Agent stopped' + (why ? ': ' + why : ''),
710
+ sending: 'Agent: posting', sent: (w, at) => 'Posted to ' + w + (at ? ' ' + at : ''), failed: (asks) => (asks ? 'Agent: needs you' : 'Agent stopped'),
711
711
  sitSuggest: (w) => `The agent can draft this for you. You read it here first. Nothing goes to ${w} until you post it.`,
712
712
  sitQueued: 'Queued. The agent starts as soon as a session is free. You can close this page.',
713
713
  sitWorking: 'The agent is working on it. You can close this page: the item keeps its state.',
714
714
  sitReady: (at, w) => `Draft ready${at ? ' at ' + at : ''}. Nothing went to ${w}.`,
715
715
  sitSending: (k) => `Posting to ${k}. A session does exactly the step you approved and nothing else.`,
716
716
  sitSent: (at, w) => `You posted it to ${w}${at ? ' at ' + at : ''}.`,
717
- sitFailed: (why) => `The agent stopped${why ? ': ' + why : '.'} Nothing was sent.`,
717
+ sitFailed: (why, asks) => (asks && why ? why.charAt(0).toUpperCase() + why.slice(1) + ' Nothing was sent.' : `The agent stopped${why ? ': ' + why : '.'} Nothing was sent.`),
718
+ openSession: 'Open the session',
718
719
  willDo: 'What the agent will do: ', change: 'Change what the agent does', askLabel: 'What the agent will do',
719
720
  draftIt: 'Let the agent draft this', tryAgain: 'Try again', post: (k) => 'Post it to ' + k, back: 'Send it back with a note', keepDraft: 'Keep it as a draft',
720
721
  noteLabel: 'Tell the agent what to change', openDraft: 'Open the draft', draftSheet: (k) => 'Draft for ' + k, stop: 'stop',
@@ -725,14 +726,14 @@ const AG = {
725
726
 
726
727
  function agentChip(item) {
727
728
  const a = item.agent; if (!a || item.mock_row) return null;
728
- const st = (cls, ic, text, spin) => el('span', { class: cls, 'data-pm-agent-state': a.state, 'data-pm-suggest': item.id }, spin ? el('span', { class: 'c3spin' }, c3ic(ic, 'sm')) : c3ic(ic, 'sm'), text);
729
+ const st = (cls, ic, text, spin) => el('span', { class: cls + ' wk-agent-st', 'data-pm-agent-state': a.state, 'data-pm-suggest': item.id, title: text }, spin ? el('span', { class: 'c3spin' }, c3ic(ic, 'sm')) : c3ic(ic, 'sm'), el('span', { class: 'wk-agent-tx' }, text));
729
730
  if (a.state === 'suggest') return st('chip2', 'spark', AG.can(a.instruction || a.suggest || ''));
730
731
  if (a.state === 'queued') return st('st', 'clock', AG.queued);
731
732
  if (a.state === 'working') return st('st c3strip', 'spark', AG.working(minsAgo(a.started_at)), true);
732
733
  if (a.state === 'ready') return st('st ok', 'check', AG.ready);
733
734
  if (a.state === 'sending') return st('st c3strip', 'out', AG.sending, true);
734
735
  if (a.state === 'sent') return st('st ok', 'out', AG.sent(whereOf(item), a.sent && a.sent.at ? hhmm(a.sent.at) : ''));
735
- if (a.state === 'failed') return st('tag warn', 'alert', AG.failed(a.error));
736
+ if (a.state === 'failed') return st('tag warn', 'alert', AG.failed(a.asks));
736
737
  return null;
737
738
  }
738
739
 
@@ -755,7 +756,8 @@ function agentCase(inst, item, L) {
755
756
  const strip = (spin, ic, text) => el('div', { class: 'c3ag', 'data-pm-agent-strip': item.id }, el('div', { class: 'fold a', role: 'status' }, spin ? el('span', { class: 'c3spin' }, c3ic(ic, 'sm')) : c3ic(ic, 'sm'), ' ' + text + ' ',
756
757
  c3btn('chip2 lk', { 'data-pm-agent-stop': item.id }, [c3ic('stop', 'sm'), AG.stop], () => agentPress(inst, '/api/my-work/stop', { item: item.id }, AG.stopped))));
757
758
  if (a.state === 'suggest' || a.state === 'failed') {
758
- sit = a.state === 'failed' ? AG.sitFailed(a.error) : AG.sitSuggest(w);
759
+ sit = a.state === 'failed' ? AG.sitFailed(a.error, a.asks) : AG.sitSuggest(w);
760
+ if (a.state === 'failed' && a.session) extras.push(el('div', { class: 'chips2' }, c3btn('chip2 lk', { 'data-pm-agent-session': a.session }, [c3ic('term', 'sm'), AG.openSession], () => openAgentSession(a.session, item))));
759
761
  const brief = () => agentPress(inst, '/api/my-work/act', { action: 'brief', item: item.id, instruction: ask(), ws: inst.ctx.ws }, AG.handed(w));
760
762
  const retry = () => agentPress(inst, '/api/my-work/decide', { item: item.id, action: 'retry' }, AG.retried);
761
763
  opts = [o('agent', a.state === 'failed' ? AG.tryAgain : AG.draftIt, [fx('spark', 'Drafts it here in PMOS', 'ok'), fx('lock', 'Nothing goes to ' + w, 'ok')], a.state === 'failed' ? retry : brief), ...own];
@@ -805,6 +807,10 @@ function agentCase(inst, item, L) {
805
807
  return { model, run };
806
808
  }
807
809
 
810
+ function openAgentSession(id, item) {
811
+ try { window.dispatchEvent(new CustomEvent('pmos:open-session', { detail: { id, name: 'Job · ' + (item.key || item.title || '') } })); } catch { /* no event */ }
812
+ }
813
+
808
814
  function openDraft(item) {
809
815
  const a = item.agent || {}; const d = a.draft || {};
810
816
  const body = el('div', { class: 'wk-agent-sheet' },
@@ -103,6 +103,11 @@ function pausedBox(ctx, card) {
103
103
  }
104
104
  // a session that has not started may be at the CLI's trust-the-folder question, where a typed Enter would answer it
105
105
  const NOSTART = 'Open the session to see what it waits for: typing into it now could answer a question you have not read.';
106
+ function flipBtn(ui, ctx) {
107
+ const b = button({ variant: 'ghost', onClick: () => { ui.flip = true; ctx.redraw(); }, attrs: { class: 'ib', 'aria-label': 'Flip to the case file', title: 'Case file' } });
108
+ b.replaceChildren(ic('flip', 'sm'));
109
+ return b;
110
+ }
106
111
  export function openBtn(ctx) {
107
112
  const b = button({ label: 'Open the session', onClick: () => ctx.openSession && ctx.openSession(ctx.id), attrs: { class: 'pill c3t-open', 'data-open-session': '', title: 'Open the session: its real terminal, on this page' } });
108
113
  b.prepend(ic('term', 'sm'));
@@ -210,10 +215,12 @@ function draw(host, ctx) {
210
215
  else {
211
216
  kids.push(el('h3', null, done && card.result.headline ? card.result.headline : (ctx.name || card.title)));
212
217
  const strip = stripNode(card, ctx.observed); if (strip) kids.push(strip);
218
+ // every box but the question and the permission carries Open the session; a card with none of them gets its own below
219
+ let opened = false;
213
220
  if (perms.length) perms.forEach((p) => kids.push(permissionBox(ctx, p)));
214
221
  else if (card.state === 'asking' && card.ask) kids.push(questionBox(ctx, card));
215
- else if (card.state === 'paused') kids.push(pausedBox(ctx, card));
216
- else if (ui.hidden !== card.state + ':' + ((card.failure && card.failure.code) || '')) { const f = failBox(ctx, card, ui); if (f) kids.push(f); }
222
+ else if (card.state === 'paused') { kids.push(pausedBox(ctx, card)); opened = true; }
223
+ else if (ui.hidden !== card.state + ':' + ((card.failure && card.failure.code) || '')) { const f = failBox(ctx, card, ui); if (f) { kids.push(f); opened = true; } }
217
224
  if (card.state === 'permission' && !perms.length) kids.push(dim('Waiting for your permission. Open the session to see it.'));
218
225
  if (done) {
219
226
  kids.push(...resultNodes(card.result, { ui, redraw: ctx.redraw, fileBody: ctx.fileBody }).filter(Boolean));
@@ -225,7 +232,9 @@ function draw(host, ctx) {
225
232
  if (ui.voice) kids.push(voiceNode(ctx, ui));
226
233
  else if (ui.tray) kids.push(trayNode(ctx, card, ui));
227
234
  if (!done && !ui.tray && !ui.voice && !perms.length && card.state !== 'asking' && !['working', 'queued'].includes(card.state)) { /* a failure box carries its own moves */ }
228
- if (!done && !ctx.readonly) kids.push(el('div', { class: 'c3t-tray' }, el('span', { style: 'flex:1' }), el('button', { type: 'button', class: 'ib', 'aria-label': 'Flip to the case file', title: 'Case file', onclick: () => { ui.flip = true; ctx.redraw(); } }, ic('flip', 'sm'))));
235
+ // a starting or working run shows Open the session on its face: a read-only card (a job PMOS started) has no case file to flip to
236
+ if (!done && (!ctx.readonly || (!opened && ctx.openSession && !ctx.ended))) kids.push(el('div', { class: 'c3t-tray' }, !opened && ctx.openSession && !ctx.ended ? openBtn(ctx) : null, el('span', { class: 'c3t-grow' }),
237
+ ctx.readonly ? null : flipBtn(ui, ctx)));
229
238
  }
230
239
  const art = el('article', { class: 'c3t-card' + (['working', 'queued'].includes(card.state) ? ' busy' : '') + (ui.flash ? ' flash' : ''), 'data-run-card': card.state || '', 'data-run-id': ctx.id, 'aria-label': ctx.name || ctx.id }, ...kids);
231
240
  host.replaceChildren(el('div', { class: 'c3 c3e' }, art));
@@ -39,7 +39,7 @@ function outcomeOf(card, meta) {
39
39
  function cardHost(st, s, card, extra) {
40
40
  const host = el('div', { 'data-run-host': s.id });
41
41
  const ctx = { id: s.id, name: s.name, card, observed: st.data.demo && st.data.demo.observed ? st.data.demo.observed[s.id] : null, origin: s.origin === 'manual' ? (s.cardId ? 'on a card' : 'started by you') : s.origin, src: st.src,
42
- refresh: st.refresh, openSession: st.openSession, glossary: st.data.demo && st.data.demo.glossary, fileBody: st.data.demo && st.data.demo.fileBody, accepted: false, ...(extra || {}) };
42
+ refresh: st.refresh, openSession: st.openSession, ended: s.endedAt != null, glossary: st.data.demo && st.data.demo.glossary, fileBody: st.data.demo && st.data.demo.fileBody, accepted: false, ...(extra || {}) };
43
43
  mountRunCard(host, ctx);
44
44
  return host;
45
45
  }
@@ -172,7 +172,7 @@ function draw(host, st) {
172
172
  const running = auto.filter((s) => s.endedAt == null).length;
173
173
  const sw = el('button', { type: 'button', class: 'c3t-sw', role: 'switch', 'aria-checked': String(st.showAuto), 'aria-label': 'Show automatic sessions', 'data-sessions-auto': '', onclick: () => { st.showAuto = !st.showAuto; st.redraw(); } }, el('i'));
174
174
  root.append(section('bolt', 'Automatic', auto.length, el('span', { class: 'chips2' }, dim(auto.length + ' today' + (running ? ', ' + running + ' running' : '')), sw), st.showAuto
175
- ? (auto.length ? el('ul', { class: 'list', 'data-c3t': 'auto' }, auto.map((s) => { const c = cards[s.id]; return row(st, s, c, { sub: s.origin + ' · ' + (s.endedAt == null ? 'running' : 'ended') + ' · ' + ago(s.startedAt) + ' · ' + outcomeOf(c), body: c ? [cardHost(st, s, c, { readonly: true })] : [dim('No card yet.')] }); })) : none('None.'))
175
+ ? (auto.length ? el('ul', { class: 'list', 'data-c3t': 'auto' }, auto.map((s) => { const c = cards[s.id]; return row(st, s, c, { sub: s.origin + ' · ' + (s.endedAt == null ? 'running' : 'ended') + ' · ' + ago(s.startedAt) + ' · ' + outcomeOf(c), body: c ? [cardHost(st, s, c, { readonly: true })] : [dim('No card yet.'), s.endedAt == null ? el('div', { class: 'c3t-tray' }, openBtn({ id: s.id, openSession: st.openSession })) : null].filter(Boolean) }); })) : none('None.'))
176
176
  : el('div', { class: 'dim', style: 'font-size:var(--fs-2)' }, 'Hidden. They run, write their result onto a card and clean up. Switch on to see them.')));
177
177
  host.append(root);
178
178
  }
@@ -1969,7 +1969,8 @@ function readLifecycle(w) {
1969
1969
  for (const r of readLedger(w, 'lifecycle.jsonl')) {
1970
1970
  if (!r || typeof r.initiative_id !== 'string') continue;
1971
1971
  if (r.state !== 'active' && !STOPPED_STATES.includes(r.state)) continue;
1972
- latest.set(r.initiative_id, { state: r.state, reason: r.reason || '', pm: r.pm || '', at: r.at || '' });
1972
+ // `back` (c3-initiative-lifecycle): the day an on-hold initiative comes back to the PM, when the hold named one.
1973
+ latest.set(r.initiative_id, { state: r.state, reason: r.reason || '', pm: r.pm || '', at: r.at || '', ...(r.state === 'on-hold' && /^\d{4}-\d{2}-\d{2}$/.test(r.back || '') ? { back: r.back } : {}) });
1973
1974
  }
1974
1975
  return latest;
1975
1976
  }
@@ -4134,10 +4135,27 @@ function stallQuietTasks(run, now) {
4134
4135
  if ((t.status !== 'working' && t.status !== 'sending') || !t.session_id) continue;
4135
4136
  const s = sessions.get(t.session_id);
4136
4137
  if (!s || s.state === 'exited') continue;
4137
- if (now - Math.max(s.lastOutputAt || 0, Date.parse(t.started_at || 0) || 0) >= TASK_IDLE_MS) run = dayRunLib.sessionStalled(run, t.session_id, TASK_STALLED_WHY, now).run;
4138
+ if (now - Math.max(s.lastOutputAt || 0, Date.parse(t.started_at || 0) || 0) < TASK_IDLE_MS) continue;
4139
+ const q = taskStallReason(s);
4140
+ run = dayRunLib.sessionStalled(run, t.session_id, q.why, now, { asks: q.asks }).run;
4138
4141
  }
4139
4142
  return run;
4140
4143
  }
4144
+ // Why a quiet task session is quiet, from its transcript: a tool call that waits on the PM's OK, a turn that ended with
4145
+ // a question for the PM, or (no transcript answer) only that it printed nothing. The session stays open in every case.
4146
+ function taskStallReason(s) {
4147
+ let hx = null; try { hx = transcriptHarness(s); } catch { hx = null; }
4148
+ const shown = sessionState.presented('quiet', hx && hx.turn);
4149
+ const said = String(((harnessOf(s) || {}).lastWords) || '').replace(/\s+/g, ' ').trim().slice(0, 140);
4150
+ if (shown === 'blocked') {
4151
+ const verb = hx && hx.openCall && hx.openCall.verb ? String(hx.openCall.verb).slice(0, 60) : '';
4152
+ return { asks: 'ok', why: `the agent waits for your OK${verb ? ' to ' + verb : ''} in its session. Open the session and answer it: the job carries on from there.` };
4153
+ }
4154
+ if (shown === 'needs') return { asks: 'answer', why: `the agent stopped to ask you something in its session${said ? ': "' + said + '"' : ''}. Open the session and answer it: the job carries on from there.` };
4155
+ // no transcript at all: the agent never began its turn, which is what a CLI held at its trust-the-folder or sign-in screen looks like
4156
+ if (hx && hx.missing && hx.missing.all && s.agent === 'claude') return { asks: 'start', why: 'the session has printed nothing for five minutes and shows no sign that it began the job: Claude Code may be waiting for you to trust the folder or to sign in. Open the session to see it.' };
4157
+ return { asks: null, why: TASK_STALLED_WHY };
4158
+ }
4141
4159
  // The skill's own body is the prompt, as fireSchedule hands it over; the line after it names this run's task and mode.
4142
4160
  function daySkillPrompt(run, skillId, wsId) {
4143
4161
  const w = getWs(wsId || run.ws);
@@ -8648,6 +8666,10 @@ async function handleRequest(req, res) {
8648
8666
  if (body.state !== 'active' && !STOPPED_STATES.includes(body.state)) {
8649
8667
  return json(res, { ok: false, error: `state must be one of ${STOPPED_STATES.concat('active').join(', ')}` }, 400);
8650
8668
  }
8669
+ // A hold may name the day it comes back to the PM (c3-initiative-lifecycle). Only on-hold carries it.
8670
+ if (body.back != null && (body.state !== 'on-hold' || !/^\d{4}-\d{2}-\d{2}$/.test(String(body.back)))) {
8671
+ return json(res, { ok: false, error: 'back is a day (YYYY-MM-DD) and only goes with on-hold' }, 400);
8672
+ }
8651
8673
  const w = getWs(body.ws);
8652
8674
  if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
8653
8675
  const known = new Set(initiativeIndex(w.root).initiatives.map((x) => x.id));
@@ -8662,7 +8684,7 @@ async function handleRequest(req, res) {
8662
8684
  grant = m.grant_id;
8663
8685
  }
8664
8686
  const at = nowISO();
8665
- const recs = ids.map((x) => ({ initiative_id: x, state: body.state, reason, pm: 'wawan', by: g.actor.by, via: g.actor.via, at, ...(grant ? { grant } : {}) }));
8687
+ const recs = ids.map((x) => ({ initiative_id: x, state: body.state, reason, ...(body.back ? { back: String(body.back) } : {}), pm: 'wawan', by: g.actor.by, via: g.actor.via, at, ...(grant ? { grant } : {}) }));
8666
8688
  const rec = recs[0];
8667
8689
  // The ledger the ENGINE reads for this repository's layout (scripts/workflow-state.js#layout): a repository
8668
8690
  // with planning/initiatives/ keeps its ledgers in planning/runs/, a kit in pmos/state/. Writing anywhere else
@@ -0,0 +1,9 @@
1
+ {
2
+ "_comment": "c3-initiative-lifecycle: On hold, Cancelled, merge and archive on the console3 Initiatives page, and the two-row board header, in demo mode (browser).",
3
+ "panels": [
4
+ "c3-ini-life-test.js"
5
+ ],
6
+ "seconds": {
7
+ "c3-ini-life-test.js": 30
8
+ }
9
+ }