create-pmos 0.213.0 → 0.215.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 (35) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +1 -1
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/state/discoveries.md +1 -1
  6. package/workspace/c3-ib-more-test.js +23 -4
  7. package/workspace/c3-sessions-panel-test.js +69 -11
  8. package/workspace/c3-work-test.js +2 -2
  9. package/workspace/guide/console3-sessions.md +33 -9
  10. package/workspace/guide/jira.md +13 -1
  11. package/workspace/guide/my-work.md +4 -1
  12. package/workspace/lib/day-run.js +30 -1
  13. package/workspace/lib/engine/boards.js +51 -7
  14. package/workspace/lib/jira-status.js +46 -1
  15. package/workspace/lib/my-work.js +1 -1
  16. package/workspace/lib/proactive-cli.js +8 -2
  17. package/workspace/lib/routes-my-work.js +44 -17
  18. package/workspace/lib/routes-pm-items.js +8 -0
  19. package/workspace/lib/transcript-read.js +93 -10
  20. package/workspace/lib/work-assigned.js +23 -11
  21. package/workspace/package-lock.json +2 -2
  22. package/workspace/package.json +1 -1
  23. package/workspace/public/pm/c3-demo-boards.js +12 -3
  24. package/workspace/public/pm/c3-ib-sprints.js +142 -39
  25. package/workspace/public/pm/c3-initiatives.css +18 -0
  26. package/workspace/public/pm/c3-session-conv.js +164 -0
  27. package/workspace/public/pm/c3-session-term.js +63 -16
  28. package/workspace/public/pm/c3-settings.js +1 -1
  29. package/workspace/public/pm/console3.css +62 -0
  30. package/workspace/public/pm/pm-work-view.js +37 -11
  31. package/workspace/public/pm/tab-term.js +9 -0
  32. package/workspace/server.js +32 -12
  33. package/workspace/skills/day-task.skill +1 -0
  34. package/workspace/skills/jira-sync.skill +9 -3
  35. package/workspace/test-files.json +1 -1
@@ -1689,6 +1689,66 @@
1689
1689
  /* terminal sheet sizes, as the classic dock has them; the phone sheet is already full width */
1690
1690
  .c3 .c3t-size{flex:none}
1691
1691
  .c3 .c3t-size button{padding:4px 8px}
1692
+ /* the session sheet (c3-session-term.js): Conversation | Terminal, a list of running sessions at 2/3 and Whole, and the conversation (c3-session-conv.js) */
1693
+ .c3 .c3t-views button .ic{flex:none}
1694
+ .c3 .c3t-cv{display:grid;grid-template-columns:minmax(0,1fr);height:calc(100vh - 150px);min-height:240px;min-width:0}
1695
+ .c3 .c3t-cv-panes{display:flex;flex-direction:column;min-height:0;min-width:0}
1696
+ .c3 .c3t-cv-panes>[hidden]{display:none}
1697
+ .c3 .c3t-cv-panes .c3t-tw{flex:1 1 0;height:auto;min-height:0}
1698
+ .c3 .c3t-cv-list{display:none;border-right:1px solid var(--border);overflow:auto;padding:0 var(--sp-2) 0 0;margin-right:var(--sp-3);gap:2px;align-content:start}
1699
+ .c3 .c3t-cv-list .eyebrow{padding:var(--sp-2) 10px 4px;font:500 var(--fs-1) var(--mono);letter-spacing:.06em;text-transform:uppercase;color:var(--muted-foreground)}
1700
+ .c3 .c3t-cv-list button{display:grid;gap:2px;text-align:left;border:0;background:none;padding:8px 10px;border-radius:8px;cursor:pointer;color:var(--foreground);font:inherit;font-size:var(--fs-3);width:100%;min-width:0}
1701
+ .c3 .c3t-cv-list button .nm,.c3 .c3t-cv-list button .sub{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1702
+ .c3 .c3t-cv-list button .sub{font:var(--fs-1) var(--mono);color:var(--muted-foreground)}
1703
+ .c3 .c3t-cv-list button[aria-current="true"]{background:color-mix(in oklab,var(--acc) 12%,transparent)}
1704
+ .c3 .c3t-cv-list button[aria-current="true"] .nm{color:var(--acc)}
1705
+ .c3 .c3t-cv-main{display:flex;flex-direction:column;flex:1 1 0;min-height:0;min-width:0}
1706
+ .c3 .c3t-cv-meta{display:flex;gap:var(--sp-2);align-items:center;flex-wrap:wrap;padding-bottom:var(--sp-2);border-bottom:1px solid var(--border);font:var(--fs-1) var(--mono);color:var(--muted-foreground)}
1707
+ .c3 .c3t-cv-meta .tg{--tc:var(--acc)}
1708
+ .c3 .c3t-cv-meta .tg[data-tone="muted"]{--tc:var(--muted-foreground)}
1709
+ .c3 .c3t-cv-meta .tg[data-tone="warning"]{--tc:var(--warning)}
1710
+ .c3 .c3t-cv-feed{flex:1 1 0;min-height:0;overflow:auto;padding:var(--sp-3) 2px}
1711
+ .c3 .c3t-cv-feed>.wrap{max-width:760px;margin:0 auto;display:flex;flex-direction:column;gap:var(--sp-2)}
1712
+ .c3 .c3t-cv .m{padding:8px 14px;border-radius:8px;max-width:85%;overflow-wrap:anywhere;font-size:var(--fs-3);line-height:1.5}
1713
+ .c3 .c3t-cv .m.me{align-self:flex-end;background:var(--acc);color:var(--on-accent);white-space:pre-wrap}
1714
+ .c3 .c3t-cv .m.bot{align-self:flex-start;background:var(--muted)}
1715
+ .c3 .c3t-cv .m.st{color:var(--muted-foreground);font:var(--fs-2) var(--mono);padding:0}
1716
+ .c3 .c3t-cv .m.bot .c3t-md{display:grid;gap:6px}
1717
+ .c3 .c3t-cv .m.bot .c3t-md>*{margin:0}
1718
+ .c3 .c3t-cv .m.bot ul,.c3 .c3t-cv .m.bot ol{padding-left:18px}
1719
+ .c3 .c3t-cv .m.bot code{font:var(--fs-2) var(--mono)}
1720
+ .c3 .c3t-cv .m.bot pre{margin:0;padding:8px 10px;border-radius:6px;background:var(--background);border:1px solid var(--border);overflow:auto;max-height:240px}
1721
+ .c3 .c3t-cv .who{display:flex;gap:6px;align-items:center;margin-top:var(--sp-2);font:500 var(--fs-1) var(--mono);color:var(--muted-foreground)}
1722
+ .c3 .c3t-cv .who.me{align-self:flex-end}
1723
+ .c3 .c3t-cv details.tools{align-self:flex-start;max-width:85%;font:var(--fs-2) var(--mono);color:var(--muted-foreground)}
1724
+ .c3 .c3t-cv details.tools summary{cursor:pointer;display:flex;gap:6px;align-items:center;list-style:none;padding:2px 0}
1725
+ .c3 .c3t-cv details.tools summary::-webkit-details-marker{display:none}
1726
+ .c3 .c3t-cv details.tools[open] summary .car{display:inline-flex;transform:rotate(90deg)}
1727
+ .c3 .c3t-cv details.tools ul{list-style:none;margin:4px 0 0;padding:0 0 0 18px;display:grid;gap:4px;border-left:2px solid var(--border)}
1728
+ .c3 .c3t-cv details.tools li{display:flex;gap:6px;align-items:baseline;min-width:0}
1729
+ .c3 .c3t-cv details.tools li b{color:var(--foreground);font-weight:500}
1730
+ .c3 .c3t-cv details.tools li span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
1731
+ .c3 .c3t-cv .live{align-self:flex-start;display:flex;gap:8px;align-items:center;font:var(--fs-2) var(--mono);color:var(--muted-foreground)}
1732
+ .c3 .c3t-cv .perm{border:1px solid var(--warning);border-radius:8px;padding:var(--sp-3);display:grid;gap:var(--sp-2);background:color-mix(in oklab,var(--warning) 6%,var(--card))}
1733
+ .c3 .c3t-cv .perm .q{font:var(--fs-2) var(--mono)}
1734
+ .c3 .c3t-cv .perm .c3t-tray{display:flex;gap:var(--sp-2);flex-wrap:wrap;align-items:center}
1735
+ .c3 .c3t-cv .none{margin:var(--sp-4) auto;max-width:420px;text-align:center;display:grid;gap:var(--sp-3);justify-items:center}
1736
+ .c3 .c3t-cv .none>.ic{width:28px;height:28px;color:var(--muted-foreground)}
1737
+ .c3 .c3t-cv .none .dim{font-size:var(--fs-2)}
1738
+ .c3 .c3t-cv-comp{border-top:1px solid var(--border);padding-top:var(--sp-2);display:grid;gap:6px}
1739
+ .c3 .c3t-cv-comp[hidden],.c3 .c3t-cv-comp .box>[hidden]{display:none}
1740
+ .c3 .c3t-cv-comp .box{display:flex;gap:var(--sp-2);align-items:flex-end;border:1px solid var(--border);border-radius:8px;padding:6px 6px 6px 12px;background:var(--card)}
1741
+ .c3 .c3t-cv-comp .box:focus-within{border-color:var(--ring)}
1742
+ .c3 .c3t-cv-comp textarea{flex:1;border:0;background:none;resize:none;font:inherit;font-size:var(--fs-3);color:var(--foreground);padding:5px 0;outline:none;min-width:0}
1743
+ .c3 .c3t-cv-comp .hint{display:flex;gap:var(--sp-2);justify-content:space-between;align-items:center;flex-wrap:wrap;font:var(--fs-1) var(--mono);color:var(--muted-foreground)}
1744
+ .c3 .c3t-cv-comp .hint .chips2[hidden]{display:none}
1745
+ @media (min-width:761px){
1746
+ .c3:not(.ph) .sheet.c3t-ss.tsz-two-thirds .c3t-cv,.c3:not(.ph) .sheet.c3t-ss.tsz-whole .c3t-cv{grid-template-columns:220px minmax(0,1fr)}
1747
+ .c3:not(.ph) .sheet.c3t-ss.tsz-two-thirds .c3t-cv-list,.c3:not(.ph) .sheet.c3t-ss.tsz-whole .c3t-cv-list{display:grid}
1748
+ }
1749
+ .c3.ph .c3t-views .lb,.c3.ph .c3t-all .lb{display:none}
1750
+ .c3.ph .c3t-cv-comp .hint .kb{display:none}
1751
+ .c3.ph .c3t-cv .m,.c3.ph .c3t-cv details.tools{max-width:92%}
1692
1752
  .c3.ph .c3t-size{display:none}
1693
1753
  @media (min-width:761px){
1694
1754
  .c3 .sheet.wide.tsz-half{width:50vw}
@@ -1891,6 +1951,8 @@
1891
1951
  .c3 .wk-agent-sum{margin:0}
1892
1952
  .c3 .wk-agent-body{--tc:var(--acc)}
1893
1953
  .c3 .wk-agent-body .sit{white-space:pre-line}
1954
+ .c3 .wk-agent-editbox{display:grid;gap:var(--sp-2)}
1955
+ .c3 .wk-agent-edit{width:100%;box-sizing:border-box;font:inherit;font-size:var(--fs-2);line-height:1.5;padding:8px 10px;border:1px solid var(--border);background:var(--background);color:var(--foreground);border-radius:6px;resize:vertical}
1894
1956
  .c3 .tag.wk-off{background:var(--accw);color:var(--acc)}
1895
1957
  .c3 .tag.wk-nb{background:var(--muted);color:var(--foreground)}
1896
1958
  .c3 .tag.wk-on{background:var(--acc);color:var(--background)}
@@ -33,7 +33,7 @@ import { renderCase } from './c3-case.js';
33
33
  import { makeXp, attachExpand } from './c3-expand.js';
34
34
  import { attachSwipe } from './c3-swipe.js';
35
35
  import { toast } from './c3-toast.js';
36
- import { openSheet } from './c3-sheet.js';
36
+ import { openSheet, closeSheet } from './c3-sheet.js';
37
37
  import { c3btn } from './page-agent.js';
38
38
  import { onBriefing, loadBriefing } from './shell-ticker.js';
39
39
  import { reviewSection } from './c3-brief.js';
@@ -724,7 +724,9 @@ const AG = {
724
724
  openSession: 'Open the session',
725
725
  willDo: 'What the agent will do: ', change: 'Change what the agent does', askLabel: 'What the agent will do',
726
726
  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',
727
- noteLabel: 'Tell the agent what to change', openDraft: 'Open the draft', draftSheet: (k) => 'Draft for ' + k, stop: 'stop',
727
+ noteLabel: 'Tell the agent what to change', openDraft: 'Open the draft', editDraft: 'Edit the draft', edited: 'Edited by you. Post sends your text.',
728
+ saveDraft: 'Save the draft', cancel: 'Cancel', savedDraft: (w) => `Saved. Post sends your text to ${w}.`, savedMine: 'Saved. The draft stays here.', bodyLabel: 'The draft',
729
+ doneIt: 'Mark it done', doneFx: 'Closes the item. Nothing is posted', doneToast: 'Done. Nothing was posted.', draftSheet: (k) => 'Draft for ' + k, stop: 'stop',
728
730
  handed: (w) => `Handed to the agent. Nothing goes to ${w} until you post it.`, stopped: 'Stopped. Nothing was kept.', kept: 'Kept as a draft. Post it when you are ready.',
729
731
  posting: 'Approved. A session posts exactly this text.', sentBack: 'Sent back with your note. The agent drafts it again.', retried: 'The agent tries again.',
730
732
  needNote: 'Say what to change first: type a note, then send it back.', link: 'Open what was posted',
@@ -785,6 +787,7 @@ function agentCase(inst, item, L) {
785
787
  const d = a.draft || {};
786
788
  quote = d.body || ''; sit = AG.sitReady('', w);
787
789
  if (d.summary) chg.push(['doc', d.summary, 'draft']);
790
+ if (d.edited_at) chg.push(['edit', AG.edited, '']);
788
791
  if (a.revisions) chg.push(['reply', 'Redrafted after your note' + (a.revisions > 1 ? 's (' + a.revisions + ')' : ''), '']);
789
792
  outbox = true; cta = 'Hold to post';
790
793
  const note = el('input', { type: 'text', placeholder: AG.noteLabel, 'aria-label': AG.noteLabel, 'data-pm-agent-note-in': item.id, autocomplete: 'off' });
@@ -794,16 +797,18 @@ function agentCase(inst, item, L) {
794
797
  isMine(item) ? null : o('post', AG.post(k), [fx('lock', 'Outward: exactly this text', 'warn'), fx('out', 'Goes to ' + w + ' as you', 'warn')], () => agentPress(inst, '/api/my-work/decide', { item: item.id, action: 'approve' }, AG.posting), { outward: true }),
795
798
  o('back', AG.back, [fx('reply', 'The agent drafts it again', 'ok')], back),
796
799
  o('keep', AG.keepDraft, [fx('check', 'Nothing leaves PMOS', 'ok')], () => say({ text: isMine(item) ? AG.keptMine : AG.kept, timeout: 3000 })),
800
+ o('done', AG.doneIt, [fx('check', AG.doneFx, 'ok')], (r) => doneAct(inst, item, r, AG.doneToast)),
797
801
  ].filter(Boolean);
798
802
  const noteBox = el('div', { class: 'form wk-agent-ask', 'data-pm-agent-note': item.id }, note); noteBox.hidden = inst.picks.get(item.id) !== 'back';
799
- extras.push(el('div', { class: 'chips2' }, c3btn('chip2 lk', { 'data-pm-agent-draft': item.id }, [c3ic('eye', 'sm'), AG.openDraft], () => openDraft(item))));
803
+ extras.push(el('div', { class: 'chips2' }, c3btn('chip2 lk', { 'data-pm-agent-draft': item.id }, [c3ic('eye', 'sm'), AG.openDraft], () => openDraft(item, inst)),
804
+ c3btn('chip2 lk', { 'data-pm-agent-edit': item.id }, [c3ic('edit', 'sm'), AG.editDraft], () => openDraft(item, inst, true))));
800
805
  after = noteBox;
801
806
  } else if (a.state === 'sending') {
802
807
  sit = AG.sitSending(k);
803
808
  } else if (a.state === 'sent') {
804
809
  sit = AG.sitSent(a.sent && a.sent.at ? hhmm(a.sent.at) : '', w);
805
810
  opts = L.opts.map((x) => ({ ...x }));
806
- if (a.draft && a.draft.body) extras.push(el('div', { class: 'chips2' }, c3btn('chip2 lk', { 'data-pm-agent-draft': item.id }, [c3ic('eye', 'sm'), AG.openDraft], () => openDraft(item)),
811
+ if (a.draft && a.draft.body) extras.push(el('div', { class: 'chips2' }, c3btn('chip2 lk', { 'data-pm-agent-draft': item.id }, [c3ic('eye', 'sm'), AG.openDraft], () => openDraft(item, inst)),
807
812
  a.sent && a.sent.link ? el('a', { class: 'chip2 lk', href: a.sent.link, target: '_blank', rel: 'noopener noreferrer' }, c3ic('out', 'sm'), AG.link) : null));
808
813
  }
809
814
  if (opts.length) opts[0].rec = 1;
@@ -817,14 +822,35 @@ function openAgentSession(id, item) {
817
822
  try { window.dispatchEvent(new CustomEvent('pmos:open-session', { detail: { id, name: 'Job · ' + (item.key || item.title || '') } })); } catch { /* no event */ }
818
823
  }
819
824
 
820
- function openDraft(item) {
825
+ /* The agent's draft in a sheet. A draft that waits for the PM can be edited there (amendment 13): Save keeps the PM's text
826
+ * as the draft, so Post sends exactly that text. */
827
+ function openDraft(item, inst, editing) {
821
828
  const a = item.agent || {}; const d = a.draft || {};
829
+ const canEdit = !!inst && a.state === 'ready';
830
+ const area = el('textarea', { class: 'wk-agent-edit', rows: '18', 'aria-label': AG.bodyLabel, 'data-pm-agent-edit-in': item.id });
831
+ area.value = d.body || '';
832
+ const save = async () => {
833
+ const v = area.value.trim();
834
+ if (!v) { say({ text: 'The draft cannot be empty. Send it back with a note instead.', timeout: 3500 }); area.focus(); return; }
835
+ const r = await pmPost('/api/my-work/decide', { item: item.id, action: 'edit', body: v });
836
+ if (!r || !r.ok) { say({ text: (r && r.error) || COPY.saveError, tone: 'error' }); return; }
837
+ closeSheet();
838
+ say({ text: isMine(item) ? AG.savedMine : AG.savedDraft(whereOf(item)), timeout: 4000 });
839
+ await load(inst);
840
+ };
841
+ const text = editing && canEdit
842
+ ? el('div', { class: 'wk-agent-editbox' }, area, el('div', { class: 'chips2' },
843
+ button({ label: AG.saveDraft, variant: 'primary', size: 'sm', onClick: save, attrs: { 'data-pm-agent-save': item.id } }),
844
+ button({ label: AG.cancel, variant: 'outline', size: 'sm', onClick: () => openDraft(item, inst, false) })))
845
+ : el('div', { class: 'case wk-agent-body' }, el('p', { class: 'sit' }, d.body || ''));
822
846
  const body = el('div', { class: 'wk-agent-sheet' },
823
- el('div', { class: 'chips' }, el('span', { class: 'tag' }, a.state === 'sent' ? 'posted' : 'draft'), item.key ? el('span', { class: 'chip2' }, c3ic('org', 'sm'), (item.origin === 'jira' ? 'Jira ' : '') + item.key) : null),
847
+ el('div', { class: 'chips' }, el('span', { class: 'tag' }, a.state === 'sent' ? 'posted' : d.edited_at ? 'draft · edited by you' : 'draft'), item.key ? el('span', { class: 'chip2' }, c3ic('org', 'sm'), (item.origin === 'jira' ? 'Jira ' : '') + item.key) : null),
824
848
  d.summary ? el('p', { class: 'dim wk-agent-sum' }, d.summary) : null,
825
- el('div', { class: 'case wk-agent-body' }, el('p', { class: 'sit' }, d.body || '')),
849
+ text,
826
850
  el('div', { class: 'fold' }, c3ic('info', 'sm'), ' ' + (a.state === 'sent' ? 'This is the text that was posted.' : 'Nothing is in ' + whereOf(item) + ' yet. Post it from the item, after you read it.')));
827
- openSheet({ title: AG.draftSheet(item.key || item.title), icon: 'doc', body });
851
+ const actions = canEdit && !editing ? [c3btn('chip2 lk', { 'data-pm-agent-edit-sheet': item.id }, [c3ic('edit', 'sm'), AG.editDraft], () => openDraft(item, inst, true))] : null;
852
+ openSheet({ title: AG.draftSheet(item.key || item.title), icon: 'doc', body, actions });
853
+ if (editing && canEdit) { try { area.focus(); } catch { /* not focusable yet */ } }
828
854
  }
829
855
 
830
856
  /** The case file element of an item: c3-case.js plus what is Work's own (offline work, the shared actions, the steps, the agent tray, the edit fields). */
@@ -840,7 +866,7 @@ function caseFile(inst, item, lens) {
840
866
  });
841
867
  root.setAttribute('data-pm-case', item.id);
842
868
  if (!item.mock_row) { // real items carry no confidence figure: never show a made-up percentage
843
- root.querySelectorAll('.opt .rk').forEach((n) => { n.replaceChildren(c3ic('spark', 'sm'), 'Suggested'); n.removeAttribute('title'); });
869
+ root.querySelectorAll('.opt .rk').forEach((n) => { if (n.childNodes.length) n.replaceChildren(c3ic('spark', 'sm'), 'Suggested'); else n.remove(); n.removeAttribute('title'); });
844
870
  root.querySelectorAll('.opt .conf').forEach((n) => n.remove());
845
871
  }
846
872
  const extras = [];
@@ -863,7 +889,7 @@ function caseFile(inst, item, lens) {
863
889
  c3ib('Speak is off on this line: nothing on this page records. Type the reply here.', 'mic', { 'data-off': '', onclick: () => say({ text: 'The microphone needs the app. Nothing was recorded: type the reply here.', timeout: 4200 }) }, 'sm'),
864
890
  c3ib('Close the reply', 'x', { onClick: () => { inst.trays.delete(item.id); draw(inst); } }, 'sm')));
865
891
  }
866
- const showSteps = !(rs && rs.phase) && item.kind !== 'offline' && !(item.agent && !item.mock_row && item.agent.state !== 'sent' && !isMine(item)) && (inst.planning.has(item.id) || (item.steps || []).length || (!agent && (!item.mock_row || item.src === 'You')));
892
+ const showSteps = !(rs && rs.phase) && item.kind !== 'offline' && !(item.agent && !item.mock_row && item.agent.state !== 'sent' && !isMine(item) && !(item.steps || []).length) && (inst.planning.has(item.id) || (item.steps || []).length || (!agent && (!item.mock_row || item.src === 'You')));
867
893
  if (showSteps) extras.push(stepsBlock(inst, item));
868
894
  const anchor = root.querySelector('.c3ag') || root.querySelector('.sit');
869
895
  if (anchor) anchor.after(...extras); else root.prepend(...extras);
@@ -1227,7 +1253,7 @@ function jobNewCase(inst, st, close) {
1227
1253
  if (r) { say({ text: 'Job created. Add work to it by saying which job it serves.', timeout: 3000 }); close(); await load(inst); }
1228
1254
  } });
1229
1255
  if (!demo) {
1230
- root.querySelectorAll('.opt .rk').forEach((n) => { n.replaceChildren(c3ic('spark', 'sm'), 'Suggested'); n.removeAttribute('title'); });
1256
+ root.querySelectorAll('.opt .rk').forEach((n) => { if (n.childNodes.length) n.replaceChildren(c3ic('spark', 'sm'), 'Suggested'); else n.remove(); n.removeAttribute('title'); });
1231
1257
  root.querySelectorAll('.opt .conf').forEach((n) => n.remove());
1232
1258
  }
1233
1259
  root.querySelector('.sit').after(el('div', { class: 'c3jf' }, nameIn ? field('Job', nameIn) : null, ...['when', 'want', 'so'].map((k, i) => field(k === 'when' ? 'When' : k === 'want' ? 'I want' : 'So', inputs[i]))));
@@ -1098,6 +1098,15 @@ export async function typeIntoSession(id, text) {
1098
1098
  return send({ t: 'input', id, data: '\r' });
1099
1099
  }
1100
1100
 
1101
+ /** Keys typed into a session as they are, with no Enter after them: a menu choice ('1') or Esc ('\x1b') for a running agent.
1102
+ * The same frame and socket as typeIntoSession. Returns false when nothing was sent. */
1103
+ export async function pressKeys(id, data) {
1104
+ if (FIXTURE || !id || !data) return false;
1105
+ connect();
1106
+ for (let i = 0; i < 50 && !socketOpen(); i++) await new Promise((r) => setTimeout(r, 100));
1107
+ return send({ t: 'input', id, data });
1108
+ }
1109
+
1101
1110
  /**
1102
1111
  * End a running session, by id — the `+ session` chooser's one destructive action.
1103
1112
  *
@@ -2939,22 +2939,41 @@ function txFound(s, now = Date.now()) {
2939
2939
  }
2940
2940
  // console3's session conversation: the PM's prompts and the agent's words from the transcript (transcript-read.readConversation).
2941
2941
  // Re-parsed only when the file moves; `stamp` lets the page skip a redraw when nothing changed. Read-only and local.
2942
+ // `state` says what the page can show: 'conv' (a conversation), 'shell' (a shell with no agent in it yet), 'unlinked' (an agent
2943
+ // whose record cannot be found or read), 'other' (an agent that keeps no Claude transcript). `ask` is Claude Code's permission
2944
+ // prompt while it is on the pane's screen (permissionAsk).
2942
2945
  function sessionConversation(s) {
2943
- if (s.agent !== 'claude' && s.agent !== 'shell') return { messages: [], missing: 'Only Claude sessions show a conversation here.' };
2946
+ if (s.agent !== 'claude' && s.agent !== 'shell') return { messages: [], state: 'other', missing: 'Only Claude sessions show a conversation here.' };
2944
2947
  const found = txFound(s);
2945
2948
  if (found.missing) {
2946
- if (s.agent === 'shell' && /wrote no session record/.test(found.missing)) return { messages: [], missing: 'No agent is running in this shell yet.' };
2947
- return { messages: [], missing: found.missing };
2949
+ if (s.agent === 'shell' && /wrote no session record/.test(found.missing)) return { messages: [], state: 'shell', missing: 'No agent is running in this shell yet.' };
2950
+ return { messages: [], state: 'unlinked', missing: found.missing };
2948
2951
  }
2949
2952
  let stamp = null;
2950
2953
  try { const st = fs.statSync(found.file); stamp = `${st.size}:${st.mtimeMs}`; } catch {}
2951
- if (s.conv && s.convStamp === stamp) return s.conv;
2952
- const c = transcript.readConversation(found.file, scrub);
2953
- const h = transcriptHarness(s);
2954
- c.turn = h && h.turn ? h.turn : null;
2955
- c.stamp = stamp;
2956
- s.conv = c; s.convStamp = stamp;
2957
- return c;
2954
+ let c = s.conv;
2955
+ if (!(c && s.convStamp === stamp)) {
2956
+ c = transcript.readConversation(found.file, scrub, transcript.CONV_LIMIT, s.root);
2957
+ c.state = 'conv';
2958
+ c.stamp = stamp;
2959
+ s.conv = c; s.convStamp = stamp;
2960
+ }
2961
+ // whose turn comes from the harness read, which has its own re-read floor: taken on every call, so a turn that the floor
2962
+ // held back shows on the next poll even when the file has not moved since
2963
+ const h = transcriptHarness(s) || {};
2964
+ const out = { ...c, turn: h.turn || null, openCall: h.openCall ? h.openCall.verb : null, model: h.model || null };
2965
+ // the prompt is on the screen, not in the transcript: read it only while a tool call is open
2966
+ const ask = out.turn === 'tool' ? paneAsk(s) : null;
2967
+ return ask ? { ...out, ask } : out;
2968
+ }
2969
+ // The pane's visible screen, plain text, read at most once every 1.5s per session (a tmux capture is a blocking spawn).
2970
+ function paneAsk(s, now = Date.now()) {
2971
+ if (!s.tmux || !TMUX) return null;
2972
+ if (s.askAt && now - s.askAt < 1500) return s.ask || null;
2973
+ s.askAt = now;
2974
+ const r = tmuxCtl(['capture-pane', '-p', '-t', s.tmux]);
2975
+ s.ask = r.status === 0 && r.stdout ? transcript.permissionAsk(r.stdout, scrub) : null;
2976
+ return s.ask;
2958
2977
  }
2959
2978
  function transcriptHarness(s) {
2960
2979
  if (s.state === 'exited') return { missing: { all: 'the session has exited' } };
@@ -6682,9 +6701,10 @@ async function handleRequest(req, res) {
6682
6701
  if (p === '/api/sessions/conversation' && req.method === 'GET') {
6683
6702
  const sess = sessions.get(String(url.searchParams.get('id') || ''));
6684
6703
  if (!sess) return json(res, { ok: false, error: 'no such session' }, 404);
6685
- if (sess.state === 'exited') return json(res, { ok: true, id: sess.id, messages: [], missing: 'The session has ended.', turn: null, stamp: null });
6704
+ if (sess.state === 'exited') return json(res, { ok: true, id: sess.id, agent: sess.agent, messages: [], state: 'ended', missing: 'The session has ended.', turn: null, stamp: null });
6686
6705
  const c = sessionConversation(sess);
6687
- return json(res, { ok: true, id: sess.id, messages: c.messages, missing: c.missing || null, turn: c.turn || null, stamp: c.stamp || null });
6706
+ return json(res, { ok: true, id: sess.id, agent: sess.agent, messages: c.messages, state: c.state || null, missing: c.missing || null, turn: c.turn || null,
6707
+ openCall: c.openCall || null, model: c.model || null, ask: c.ask || null, stamp: c.stamp || null });
6688
6708
  }
6689
6709
  if (p === '/api/sessions/transcript' && req.method === 'GET') {
6690
6710
  const sess = sessions.get(String(url.searchParams.get('id') || ''));
@@ -36,6 +36,7 @@ Procedure:
36
36
  4. If MODE is send:
37
37
  a. The task is approved. Read the approved draft and its link in the output of `day show`. The approved draft is the only text you may send. Do not read it as a draft to improve.
38
38
  A draft whose summary starts "The PM's own" was written by the PM in the Your day card. Post it exactly as written, as the PM, to the place the summary and the link name. Do not add a greeting, a signature or a line of your own.
39
+ If `day show` prints "edited by the PM", the PM changed the draft in Work. The body is the PM's text. Post it exactly as written, the same way.
39
40
  b. Do the one outward step the summary names, with the connector that fits, to the place the link names. Examples: post the approved reply in the linked Slack thread, add the approved comment to the linked Jira issue, reply to the linked mail thread. If the summary names more than one step, do only the first and report the rest with `day fail`.
40
41
  c. If the place does not match the link, the thread is closed, or the connector is not available, do not try another route. Run `pmos proactive day fail TASK --reason "<what stopped it>"`.
41
42
  d. When it is done, run `pmos proactive day sent TASK --link <URL of what you posted or sent>`. Leave out `--link` only if the connector returns none.
@@ -40,11 +40,17 @@ Procedure:
40
40
  - `goal`: `{"text": "<the goal>", "jira": "<Jira's goal, or null>"}`. Always write the goal yourself from the sprint's tickets, in one sentence of at most 240 characters: what the sprint delivers when its tickets are done, naming the two or three largest outcomes. Do not copy Jira's sprint goal: it is often empty, too broad or incomplete. Put Jira's own goal in `"jira"` only as a reference, exactly as Jira writes it.
41
41
  - `summary`: two to four plain sentences on what the team is working on: what is done, what is moving, what is blocked. For a closed sprint, say if the goal was met and what moved to the next sprint.
42
42
  - `themes`: one entry per group of related tickets, at most 8, largest first: `{"title", "line", "tickets", "done", "blocked", "people", "initiative"}`. Group by initiative when the tickets belong to one (the initiative id from step 1); put tickets with no initiative in one theme such as "Fixes and upkeep" with `"initiative": null`. `line` is one sentence; `people` is the assignees' first names, comma-separated.
43
- - For a closed sprint, also `closed`: `{"done", "total", "carried", "carriedTo", "points"}` (`points` as "31 of 38" when the tickets have points).
44
- Use only what the tickets say. Never invent work, a date or a person. Save `{"sprints": [{"name", "id", "state", "goal", "summary", "themes", "closed"}]}` in the scratch folder, run `pmos jira summary --from <the file>`, and delete the file.
43
+ - For a closed sprint, also `closed`: `{"done", "total", "carried", "carriedTo", "points"}` (`points` as "31 of 38" when the tickets have points), `hit`: `"met"`, `"partly"` or `"missed"` (was the goal delivered), and `got`: one sentence on what got in the way, or leave it out when nothing did.
44
+ - For the active sprint, also `brief`, the sprint brief the PM reads first. Write it for a product manager: what matters, what is stuck, and what to do about it. PMOS counts the points, the flow, the velocity and the forecast itself, so do not repeat those as a list.
45
+ - `health`: `"ok"` (on track), `"warn"` (at risk) or `"bad"` (off track), from the share of points done against the share of the sprint gone, the blocked tickets and the velocity of the closed sprints.
46
+ - `verdict`: two or three sentences on where the sprint stands: on track or not, about how much will carry over at the team's usual pace, and which part of the work is exposed.
47
+ - `issues`: at most 6, worst first. One entry per blocked ticket (flagged, or a blocked or on-hold status) and per ticket that looks stuck (in progress or in review with no status change across reads, or a review queue that is building up), plus tickets with no estimate as one entry. Each is `{"sev": "bad" for blocked or "warn" for watch, "title", "keys", "initiative", "why", "by", "plan", "doneWhen"}`. `why` says why it matters for the goal. `plan` is two to four steps the PM can take to move the ticket to done: who to ask, the decision to make, the fallback if it does not work. `by` is when to act ("Decide today", "Check by Wed 15 Oct"). `doneWhen` is the outcome that closes it. Do not only report a problem: always propose the plan.
48
+ - `done`: at most 6 outcomes completed in the sprint, `{"title", "keys", "initiative", "line", "waiting"}`. Write the outcome, not the ticket title. Set `"waiting": true` when the ticket is done but not yet in production (a status such as "Ready for Prod" or a release that has not gone out).
49
+ - `asks`: at most 3 decisions or actions only the PM can take this week, each one sentence.
50
+ Use only what the tickets say. Never invent work, a date or a person. Save `{"sprints": [{"name", "id", "state", "goal", "summary", "themes", "closed", "hit", "got", "brief"}]}` in the scratch folder, run `pmos jira summary --from <the file>`, and delete the file.
45
51
  5. Record the people in People. For each assignee and reporter other than the PM, write one sighting per issue (People adds each new person at once): {"source": "jira", "handle": {"kind": "jira", "value": "<their Jira display name>"}, "name": "<display name>", "interaction": {"date": "<the issue's last update, YYYY-MM-DD>", "kind": "Jira", "text": "<Assignee|Reporter> of <KEY>: <summary, at most 150 characters>", "pointer": "jira · <KEY> · <url>", "initiative": "<the initiative id>"}}. Put all of them in one file {"sightings": [...]} in the scratch folder, run `pmos people record --file <that file>` once, and delete the file. An issue with no assignee gives no assignee sighting.
46
52
  6. Finish with the three sections of the output contract, one short line each:
47
- - Checked: the initiative, the number of tickets read, and each change (status, sprint, blocked). If nothing changed, say "no change". Add how many people you recorded, how many issues the sprint read saved per sprint, and which sprints got a goal and summary (and whose goal you wrote from the tickets).
53
+ - Checked: the initiative, the number of tickets read, and each change (status, sprint, blocked). If nothing changed, say "no change". Add how many people you recorded, how many issues the sprint read saved per sprint, and which sprints got a goal, summary and brief.
48
54
  - Not reached: each initiative whose search failed, with the reason, and the sprint read if it failed.
49
55
  - Needs you: tickets newly blocked, tickets the search did not return, and any refusal.
50
56
 
@@ -299,7 +299,7 @@
299
299
  "pm-portfolio-inline-test.js": 15,
300
300
  "settings-store.test.js": 1,
301
301
  "c3-settings-test.js": 25,
302
- "c3-sessions-panel-test.js": 20,
302
+ "c3-sessions-panel-test.js": 70,
303
303
  "c3-updates-test.js": 20,
304
304
  "c3-quiet-test.js": 30,
305
305
  "c3-phone-model.test.js": 1,