create-pmos 0.214.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.
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/c3-sessions-panel-test.js +69 -11
- package/workspace/c3-work-test.js +2 -2
- package/workspace/guide/console3-sessions.md +33 -9
- package/workspace/guide/my-work.md +4 -1
- package/workspace/lib/day-run.js +30 -1
- package/workspace/lib/my-work.js +1 -1
- package/workspace/lib/proactive-cli.js +8 -2
- package/workspace/lib/routes-my-work.js +44 -17
- package/workspace/lib/routes-pm-items.js +8 -0
- package/workspace/lib/transcript-read.js +93 -10
- package/workspace/lib/work-assigned.js +23 -11
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-session-conv.js +164 -0
- package/workspace/public/pm/c3-session-term.js +63 -16
- package/workspace/public/pm/c3-settings.js +1 -1
- package/workspace/public/pm/console3.css +62 -0
- package/workspace/public/pm/pm-work-view.js +37 -11
- package/workspace/public/pm/tab-term.js +9 -0
- package/workspace/server.js +32 -12
- package/workspace/skills/day-task.skill +1 -0
- package/workspace/test-files.json +1 -1
|
@@ -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',
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
*
|
package/workspace/server.js
CHANGED
|
@@ -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
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
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,
|
|
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.
|
|
@@ -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":
|
|
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,
|