create-pmos 0.217.0 → 0.219.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/guide/my-work.md +8 -4
- package/workspace/lib/day-run.js +13 -4
- package/workspace/lib/engine/boards.js +5 -2
- package/workspace/lib/jira-status.js +10 -5
- package/workspace/lib/my-work.js +1 -1
- package/workspace/lib/routes-my-work.js +4 -1
- package/workspace/lib/work-assigned.js +3 -3
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/console3.css +21 -0
- package/workspace/public/pm/pm-work-view.js +110 -32
- package/workspace/public/pm/today-copy.js +1 -2
- package/workspace/public/pm/today-day.js +2 -3
- package/workspace/skills/day-task.skill +1 -1
- package/workspace/skills/jira-sync.skill +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.219.0",
|
|
4
4
|
"description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
package/payload/KIT-VERSION
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Eval calibration log"
|
|
4
4
|
description: "Append-only record of every PM override of an evaluator verdict."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-14T04:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-14T04:55:02Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Health budgets"
|
|
4
4
|
description: "Closed registry of health-budget anchors (D59), edited only by the PM."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-14T04:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-14T04:55:02Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Discoveries and friction log"
|
|
4
4
|
description: "Append-only record of what runs discovered and where the process rubbed."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-14T04:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-14T04:55:02Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -121,15 +121,19 @@ The row chip then shows the agent's state:
|
|
|
121
121
|
|
|
122
122
|
The page reads the state again every 15 seconds while an agent is queued, working or posting.
|
|
123
123
|
|
|
124
|
-
When the draft is ready, the case file shows it as a document, with its headings, lists and tables. Select **Open
|
|
124
|
+
When the draft is ready, the case file shows it as a document card, with its headings, lists and tables. Select **Open** to read all of it, or **Copy** to copy the text.
|
|
125
|
+
|
|
126
|
+
Under the draft is the conversation with the agent: your ask, each draft, each note you sent back and each edit. Select **View round** on an earlier draft to read that version. The conversation stays on the item on the next days too.
|
|
127
|
+
|
|
128
|
+
To ask the agent for changes, type in the box under the conversation and press Enter or select **Send to the agent**. Shift+Enter adds a line. The agent drafts it again, and the draft and the conversation stay on the item while it works. You can send a draft back as many times as you want.
|
|
129
|
+
|
|
130
|
+
Or choose one option:
|
|
125
131
|
|
|
126
132
|
- **Post it to** the ticket posts the exact text. Nothing goes out until you hold to confirm.
|
|
127
|
-
- **
|
|
133
|
+
- **Edit the draft** opens the text to change. Select **Save the draft**. The card then says **Edited by you**, and **Post it to** the ticket posts exactly your text.
|
|
128
134
|
- **Keep it as a draft** leaves the draft as it is.
|
|
129
135
|
- **Mark it done** closes the item. Nothing is posted.
|
|
130
136
|
|
|
131
|
-
To change the text yourself before you post it, select **Edit the draft**, change the text and select **Save the draft**. The case file then says **Edited by you**, and **Post it to** the ticket posts exactly your text.
|
|
132
|
-
|
|
133
137
|
The agent only drafts. It never posts to Jira until you approve the draft.
|
|
134
138
|
|
|
135
139
|
### Defer and flag
|
package/workspace/lib/day-run.js
CHANGED
|
@@ -32,7 +32,6 @@
|
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
34
|
const MAX_PARALLEL = 5;
|
|
35
|
-
const MAX_REVISIONS = 3;
|
|
36
35
|
const MAX_CHECK_LOG = 20;
|
|
37
36
|
const MAX_INTERVAL_MIN = 120;
|
|
38
37
|
const BEFORE_MEETING_MIN = 15;
|
|
@@ -47,6 +46,10 @@ const iso = (ms) => new Date(ms).toISOString();
|
|
|
47
46
|
const clone = (x) => JSON.parse(JSON.stringify(x));
|
|
48
47
|
const isUrl = (v) => typeof v === 'string' && v.length <= 500 && /^https?:\/\/\S+$/.test(v);
|
|
49
48
|
const text = (v) => (typeof v === 'string' ? v.trim() : '');
|
|
49
|
+
/* The conversation on a task (c3-work-agent amendment 15): the agent's drafts, the PM's notes, edits and post, in order, so
|
|
50
|
+
* Work can show every round. Each draft keeps its body, so an earlier round can be read again. At most MAX_LOG turns. */
|
|
51
|
+
const MAX_LOG = 30;
|
|
52
|
+
function logTurn(t, turn) { t.log = [...(Array.isArray(t.log) ? t.log : []), turn].slice(-MAX_LOG); }
|
|
50
53
|
const pad = (n) => String(n).padStart(2, '0');
|
|
51
54
|
const localDate = (ms) => { const d = new Date(ms); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; };
|
|
52
55
|
/** ms of HH:MM on the local day of `ms`. */
|
|
@@ -129,7 +132,8 @@ function addBriefed(run, spec) {
|
|
|
129
132
|
const next = clone(run);
|
|
130
133
|
next.tasks.push({ id: s.id, item_id: /^[0-9a-f]{12}$/.test(String(s.item_id || '')) ? s.item_id : null, title: text(s.title).slice(0, 250) || s.id, kind: 'approve', source: s.source || 'my-work', link: isUrl(s.link) ? s.link : null,
|
|
131
134
|
goal: null, my_work: s.my_work || null, brief_at: s.brief_at || null, instruction: text(s.instruction), context: s.context && typeof s.context === 'object' ? clone(s.context) : null,
|
|
132
|
-
status: 'waiting', session_id: null, started_at: null, attempts: 0, revisions: [], draft: null, decided: null, error: null
|
|
135
|
+
status: 'waiting', session_id: null, started_at: null, attempts: 0, revisions: [], draft: null, decided: null, error: null,
|
|
136
|
+
...(Array.isArray(s.log) && s.log.length ? { log: clone(s.log).slice(-MAX_LOG) } : {}) });
|
|
133
137
|
return { ok: true, run: next, added: s.id };
|
|
134
138
|
}
|
|
135
139
|
|
|
@@ -201,6 +205,7 @@ function deliver(run, taskId, d, now) {
|
|
|
201
205
|
if (body.length > 8000) return 'the body is over 8000 characters';
|
|
202
206
|
if (b.link !== undefined && b.link !== null && b.link !== '' && !isUrl(b.link)) return 'the link must be an http(s) address of at most 500 characters';
|
|
203
207
|
t.draft = { summary, body, link: b.link || null, at: iso(now) };
|
|
208
|
+
logTurn(t, { who: 'agent', kind: 'draft', at: iso(now), text: summary, body });
|
|
204
209
|
t.status = t.kind === 'auto' ? 'done' : 'review';
|
|
205
210
|
t.error = null;
|
|
206
211
|
return null;
|
|
@@ -229,14 +234,16 @@ function decide(run, taskId, op, now) {
|
|
|
229
234
|
if (t.status === 'review' && t.kind === 'approve') { t.status = 'sending'; keepSession(t); t.started_at = null; }
|
|
230
235
|
else if (!(t.status === 'done' && t.kind === 'auto')) return `task ${taskId} is ${t.status} — only a draft waiting for you can be approved`;
|
|
231
236
|
t.decided = { action: 'approve', at, ...(note ? { note: note.slice(0, 500) } : {}) };
|
|
237
|
+
logTurn(t, { who: 'pm', kind: 'post', at });
|
|
232
238
|
return null;
|
|
233
239
|
}
|
|
234
240
|
if (o.action === 'revise') {
|
|
235
241
|
if (t.status !== 'review' && t.status !== 'done') return `task ${taskId} is ${t.status} — only a delivered draft can be revised`;
|
|
236
242
|
if (!note) return 'say what to change: a revise needs a note';
|
|
237
243
|
if (note.length > 500) return 'the note is over 500 characters';
|
|
238
|
-
|
|
244
|
+
// no cap on rounds: a conversation with the agent takes as many as the PM wants (c3-work-agent amendment 15; was 3)
|
|
239
245
|
t.revisions.push({ note, at });
|
|
246
|
+
logTurn(t, { who: 'pm', kind: 'note', at, text: note });
|
|
240
247
|
t.status = 'waiting'; keepSession(t); t.decided = { action: 'revise', at, note };
|
|
241
248
|
return null;
|
|
242
249
|
}
|
|
@@ -260,6 +267,7 @@ function edit(run, taskId, body, now) {
|
|
|
260
267
|
if (b === t.draft.body) return null;
|
|
261
268
|
if (t.draft.agent_body === undefined) t.draft.agent_body = t.draft.body;
|
|
262
269
|
t.draft.body = b; t.draft.edited_at = iso(now);
|
|
270
|
+
logTurn(t, { who: 'pm', kind: 'edit', at: iso(now), body: b });
|
|
263
271
|
return null;
|
|
264
272
|
});
|
|
265
273
|
}
|
|
@@ -274,6 +282,7 @@ function carry(run, task, now) {
|
|
|
274
282
|
if (findTask(next, task.id)) return { ok: false, error: `task ${task.id} is already in today's run` };
|
|
275
283
|
const t = clone(task);
|
|
276
284
|
Object.assign(t, { status: 'sending', session_id: null, prev_session: null, started_at: null, error: null, decided: { action: 'approve', at: iso(now) } });
|
|
285
|
+
logTurn(t, { who: 'pm', kind: 'post', at: iso(now) });
|
|
277
286
|
next.tasks.push(t);
|
|
278
287
|
return { ok: true, run: next, task: task.id };
|
|
279
288
|
}
|
|
@@ -441,6 +450,6 @@ function checkDone(run, { found = 0, resolved = 0 } = {}, now) {
|
|
|
441
450
|
return { ok: true, run: next };
|
|
442
451
|
}
|
|
443
452
|
|
|
444
|
-
module.exports = { MAX_PARALLEL,
|
|
453
|
+
module.exports = { MAX_PARALLEL, MAX_CHECK_LOG, MAX_INTERVAL_MIN, STATUSES, KINDS, ACTIONS, TASK_RE,
|
|
445
454
|
newRun, readRun, findTask, modeOf, addTasks, addBriefed, addSend, toStart, started, deliver, decide, edit, carry, sent, fail, stop, withdraw, sessionEnded, sessionStalled, stage, intervalMin,
|
|
446
455
|
checkDue, checkStarted, checkDone };
|
|
@@ -681,7 +681,7 @@ function sprintsOf(snapshots, onBoard = () => true, whole = null, notes = null,
|
|
|
681
681
|
sp.summarisedTickets = Number.isInteger(n.tickets) ? n.tickets : null;
|
|
682
682
|
sp.closed = sp.state === 'closed' && n.closed ? { ...n.closed } : null;
|
|
683
683
|
sp.frozen = sp.state === 'closed' && n.state === 'closed';
|
|
684
|
-
sp.brief = sp.state
|
|
684
|
+
sp.brief = sp.state === 'active' && n.brief ? n.brief : null; // the sprint brief is the active sprint's only
|
|
685
685
|
sp.hit = sp.state === 'closed' && n.hit ? n.hit : null;
|
|
686
686
|
sp.got = sp.state === 'closed' && n.got ? n.got : null;
|
|
687
687
|
} else {
|
|
@@ -691,7 +691,10 @@ function sprintsOf(snapshots, onBoard = () => true, whole = null, notes = null,
|
|
|
691
691
|
}
|
|
692
692
|
}
|
|
693
693
|
const order = { active: 0, future: 1, closed: 2 };
|
|
694
|
-
|
|
694
|
+
// Two active sprints: the one Jira dated and gave an id comes first, then the larger one; the name only breaks a tie.
|
|
695
|
+
const real = (sp) => (sp.start || sp.end || wt.some((t) => t && t.sprint && t.sprint.id && String(t.sprint.name) === sp.name) ? 1 : 0);
|
|
696
|
+
const sprints = [...byName.values()].sort((a, b) => ((order[a.state] ?? 3) - (order[b.state] ?? 3))
|
|
697
|
+
|| (a.state === 'active' ? (real(b) - real(a)) || (b.issues.length - a.issues.length) : 0) || a.name.localeCompare(b.name));
|
|
695
698
|
for (const sp of sprints) sp.issues.sort((a, b) => a.key.localeCompare(b.key, undefined, { numeric: true }));
|
|
696
699
|
// Counted by PMOS, never written by the agent: each sprint's points and status flow, the velocity of the last four closed
|
|
697
700
|
// sprints, and the active sprint's pace and forecast (the sprint brief).
|
|
@@ -89,20 +89,24 @@ function fixVersionsOf(v) {
|
|
|
89
89
|
return [...new Set(list.map((x) => clip(nameOf(x), 80)).filter(Boolean))];
|
|
90
90
|
}
|
|
91
91
|
const SPRINT_STATES = ['active', 'future', 'closed'];
|
|
92
|
-
/**
|
|
92
|
+
/** A name that says "no sprint" ("Future", "Backlog", "No sprint"): a ticket planned for later, which is in no sprint. */
|
|
93
|
+
const NOT_A_SPRINT = /^(future|next|later|planned|unplanned|unscheduled|backlog|icebox|none|null|no sprint|not in a sprint|unassigned)( sprints?)?$/i;
|
|
94
|
+
/** One sprint, from an object { name, state } or a bare name. Only Jira's own state makes a sprint active: a bare name or an
|
|
95
|
+
* unknown state is a planned sprint, and a "Future" or "Backlog" with no Jira id is no sprint (the backlog). */
|
|
93
96
|
function sprintOne(v) {
|
|
94
97
|
if (!v) return null;
|
|
95
|
-
if (isStr(v)) { const name = clip(oneLine(v), 80); return name ? { name, state: '
|
|
98
|
+
if (isStr(v)) { const name = clip(oneLine(v), 80); return name && !NOT_A_SPRINT.test(name) ? { name, state: 'future' } : null; }
|
|
96
99
|
if (typeof v !== 'object') return null;
|
|
97
100
|
const name = clip(oneLine(v.name), 80);
|
|
98
101
|
const state = String(v.state || '').toLowerCase();
|
|
99
102
|
if (!name) return null;
|
|
100
|
-
const out = { name, state: SPRINT_STATES.includes(state) ? state : '
|
|
103
|
+
const out = { name, state: SPRINT_STATES.includes(state) ? state : 'future' };
|
|
101
104
|
if ((Number.isInteger(v.id) && v.id > 0) || (isStr(v.id) && /^\d{1,12}$/.test(v.id))) out.id = String(v.id);
|
|
102
105
|
for (const [k, from] of [['start', ['startDate', 'start']], ['end', ['endDate', 'end']], ['completed', ['completeDate', 'completed']]]) {
|
|
103
106
|
const d = dayOf(from.map((f) => v[f]).find((x) => x != null));
|
|
104
107
|
if (d) out[k] = d;
|
|
105
108
|
}
|
|
109
|
+
if (!out.id && NOT_A_SPRINT.test(name)) return null;
|
|
106
110
|
const goal = clip(oneLine(v.goal), 300); // Jira's sprint goal, often empty or broad (sprint-goal-summary)
|
|
107
111
|
if (goal) out.goal = goal;
|
|
108
112
|
return out;
|
|
@@ -235,7 +239,8 @@ function readSnapshot(root, initiative) {
|
|
|
235
239
|
let data;
|
|
236
240
|
try { data = JSON.parse(m[1]); } catch { return null; }
|
|
237
241
|
const tickets = new Map();
|
|
238
|
-
|
|
242
|
+
// The sprint is read again, so a reading saved before the "Future" fix is no longer an active sprint.
|
|
243
|
+
for (const r of Array.isArray(data.tickets) ? data.tickets : []) if (r && KEY_RE.test(String(r.key || ''))) tickets.set(r.key, { ...r, sprint: r.sprint ? sprintOne(r.sprint) : null });
|
|
239
244
|
return { fetchedAt: isStr(data.fetched_at) ? data.fetched_at : null, query: isStr(data.query) ? data.query : null, tickets };
|
|
240
245
|
}
|
|
241
246
|
|
|
@@ -415,7 +420,7 @@ function noteOne(x) {
|
|
|
415
420
|
const note = { name, state, goal, summary, themes };
|
|
416
421
|
// The sprint brief (sprint-goal-summary, mockup-c3-sprint-brief): where the sprint stands, the issues to resolve with a proposed
|
|
417
422
|
// plan each, what is done and the asks for the PM. Written by the agent from the tickets; optional, so a note without one still loads.
|
|
418
|
-
if (state
|
|
423
|
+
if (state === 'active' && x.brief && typeof x.brief === 'object') {
|
|
419
424
|
const r = briefOf(x.brief, name);
|
|
420
425
|
if (r.error) return r;
|
|
421
426
|
note.brief = r.brief;
|
package/workspace/lib/my-work.js
CHANGED
|
@@ -196,7 +196,7 @@ function stateOf(item, presses, runs) {
|
|
|
196
196
|
const t = taskOf(runs, last);
|
|
197
197
|
const task = t ? { id: t.id, status: t.status, error: t.error || null, draft: t.draft ? t.draft.summary : null, body: t.draft ? t.draft.body || null : null, edited_at: t.draft ? t.draft.edited_at || null : null,
|
|
198
198
|
sent: t.sent || null, started_at: t.started_at || null, revisions: Array.isArray(t.revisions) ? t.revisions.length : 0,
|
|
199
|
-
session: t.stalled_session || null, asks: t.asks || null } : null;
|
|
199
|
+
session: t.stalled_session || null, asks: t.asks || null, log: Array.isArray(t.log) ? t.log : null } : null;
|
|
200
200
|
const base = { task, instruction: last.instruction, decided_at: last.at };
|
|
201
201
|
if (!t) return { state: 'agent', sub: 'waiting', ...base };
|
|
202
202
|
if (t.status === 'review') return { state: 'ready', sub: 'review', ...base };
|
|
@@ -279,7 +279,10 @@ function agentItemPress(ctx, res, verb, body, itemId, now) {
|
|
|
279
279
|
if (body.action === 'revise' && !note) { ctx.json(res, { ok: false, error: 'say what to change: a revise needs a note' }, 400); return; }
|
|
280
280
|
const prev = t.draft && t.draft.body ? String(t.draft.body).slice(0, 4000) : '';
|
|
281
281
|
const own = body.action === 'revise' ? `Draft this item again. The PM's note on your earlier draft: ${note}${prev ? `\n\nYour earlier draft, to change:\n${prev}\n` : ''}` : '';
|
|
282
|
-
|
|
282
|
+
// the conversation goes on in today's task: the earlier rounds and this note (amendment 15)
|
|
283
|
+
const prior = Array.isArray(t.log) && t.log.length ? t.log : t.draft ? [{ who: 'agent', kind: 'draft', at: t.draft.at || null, text: t.draft.summary || '', body: t.draft.body || '' }] : [];
|
|
284
|
+
const log = body.action === 'revise' ? [...prior, { who: 'pm', kind: 'note', at: new Date(now).toISOString(), text: note }] : prior;
|
|
285
|
+
const b = ctx.myWorkBrief({ ...g.spec(own), log }, String(body.ws || run.ws || ''));
|
|
283
286
|
if (!b.ok) { ctx.json(res, { ok: false, error: b.error }, b.status || 500); return; }
|
|
284
287
|
after(b.run, b.task); return;
|
|
285
288
|
}
|
|
@@ -33,7 +33,7 @@ function agentOf(x) {
|
|
|
33
33
|
state, suggest: x.suggest || null, instruction: x.instruction || null,
|
|
34
34
|
draft: t && (t.draft || t.body) ? { summary: t.draft || null, body: t.body || null, edited_at: t.edited_at || null } : null,
|
|
35
35
|
sent: t && t.sent ? { link: t.sent.link || null, at: t.sent.at || null } : null,
|
|
36
|
-
error: t ? t.error || null : null, session: t && t.session ? String(t.session) : null, asks: t ? t.asks || null : null, revisions: t ? t.revisions || 0 : 0, started_at: t ? t.started_at || null : null, decided_at: x.decided_at || null,
|
|
36
|
+
error: t ? t.error || null : null, session: t && t.session ? String(t.session) : null, asks: t ? t.asks || null : null, revisions: t ? t.revisions || 0 : 0, started_at: t ? t.started_at || null : null, decided_at: x.decided_at || null, log: t ? t.log || null : null,
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -74,13 +74,13 @@ const TASK_STATE = { waiting: 'queued', working: 'working', sending: 'sending',
|
|
|
74
74
|
/** The agent field of an item whose day task is `taskId` in `run` (or none yet: the offer `suggest`). */
|
|
75
75
|
function taskAgent(taskId, suggest, run) {
|
|
76
76
|
const t = run && Array.isArray(run.tasks) ? run.tasks.find((y) => y && y.id === taskId) || null : null;
|
|
77
|
-
const base = { suggest, instruction: null, draft: null, sent: null, error: null, session: null, asks: null, revisions: 0, started_at: null, decided_at: null };
|
|
77
|
+
const base = { suggest, instruction: null, draft: null, sent: null, error: null, session: null, asks: null, revisions: 0, started_at: null, decided_at: null, log: null };
|
|
78
78
|
if (!t) return { state: 'suggest', ...base };
|
|
79
79
|
return { ...base, state: TASK_STATE[t.status] || 'queued', instruction: t.instruction || null,
|
|
80
80
|
draft: t.draft ? { summary: t.draft.summary || null, body: t.draft.body || null, edited_at: t.draft.edited_at || null } : null,
|
|
81
81
|
sent: t.sent ? { link: t.sent.link || null, at: t.sent.at || null } : null, error: t.error || null,
|
|
82
82
|
session: t.stalled_session ? String(t.stalled_session) : null, asks: t.asks || null, revisions: Array.isArray(t.revisions) ? t.revisions.length : 0,
|
|
83
|
-
started_at: t.started_at || null, decided_at: (t.decided && t.decided.at) || null };
|
|
83
|
+
started_at: t.started_at || null, decided_at: (t.decided && t.decided.at) || null, log: Array.isArray(t.log) ? t.log : null };
|
|
84
84
|
}
|
|
85
85
|
const briefAgent = (x, run) => taskAgent('t-' + x.id, briefSuggest(x), run);
|
|
86
86
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.219.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pmos-workspace",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.219.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -1957,6 +1957,27 @@
|
|
|
1957
1957
|
.c3 .wk-agent-body{--tc:var(--acc)}
|
|
1958
1958
|
.c3 .wk-agent-body .sit{white-space:pre-line}
|
|
1959
1959
|
.c3 .wk-agent-doc .wk-agent-md{font-size:var(--fs-4);max-width:72ch}
|
|
1960
|
+
.c3 .wk-doc{display:grid;gap:var(--sp-2);box-shadow:none}
|
|
1961
|
+
.c3 .wk-doc-h{display:flex;flex-wrap:wrap;gap:6px 12px;align-items:center;justify-content:space-between}
|
|
1962
|
+
.c3 .wk-doc-to{font-size:var(--fs-3);font-weight:600}
|
|
1963
|
+
.c3 .wk-doc .ds-md h3{display:block;color:var(--foreground);text-transform:none;letter-spacing:0}
|
|
1964
|
+
.c3 .wk-doc-clip{max-height:22rem;overflow:hidden;-webkit-mask-image:linear-gradient(to bottom,var(--foreground) 65%,transparent);mask-image:linear-gradient(to bottom,var(--foreground) 65%,transparent)}
|
|
1965
|
+
.c3 .wk-doc>.chip2{justify-self:start}
|
|
1966
|
+
.c3 .wk-conv{display:grid;gap:var(--sp-2)}
|
|
1967
|
+
.c3 .wk-conv-h{display:flex;flex-wrap:wrap;gap:6px 10px;align-items:center}
|
|
1968
|
+
.c3 .wk-conv-h>.dim{display:inline-flex;gap:6px;align-items:center;font-size:var(--fs-2)}
|
|
1969
|
+
.c3 .wk-thr li{grid-template-columns:auto minmax(0,1fr);align-items:start}
|
|
1970
|
+
.c3 .wk-thr li>.ic{margin-top:3px}
|
|
1971
|
+
.c3 .wk-thr li.ag>.ic{color:var(--acc)}
|
|
1972
|
+
.c3 .wk-turn{display:grid;gap:4px;min-width:0}
|
|
1973
|
+
.c3 .wk-turn p{margin:0}
|
|
1974
|
+
.c3 .wk-who{display:flex;flex-wrap:wrap;gap:8px;align-items:baseline;font-size:var(--fs-2)}
|
|
1975
|
+
.c3 .wk-reply{display:grid;gap:6px}
|
|
1976
|
+
.c3 .wk-reply textarea{width:100%;box-sizing:border-box;font:inherit;font-size:var(--fs-3);line-height:1.45;padding:8px 10px;border:1px solid var(--border);background:var(--background);color:var(--foreground);border-radius:8px;resize:vertical;min-height:3.2em}
|
|
1977
|
+
.c3 .wk-reply textarea:focus-visible{outline:2px solid var(--ring);outline-offset:1px}
|
|
1978
|
+
.c3 .wk-reply-f{display:flex;flex-wrap:wrap;gap:6px 12px;align-items:center;justify-content:space-between}
|
|
1979
|
+
.c3 .wk-reply-f>.dim{display:inline-flex;gap:6px;align-items:center;font-size:var(--fs-2)}
|
|
1980
|
+
.c3 .case.wk-ag>.wkq{border-top:1px solid var(--border);padding-top:var(--sp-3)}
|
|
1960
1981
|
.c3 .wk-agent-editbox{display:grid;gap:var(--sp-2)}
|
|
1961
1982
|
.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}
|
|
1962
1983
|
.c3 .tag.wk-off{background:var(--accw);color:var(--acc)}
|
|
@@ -725,6 +725,11 @@ const AG = {
|
|
|
725
725
|
openSession: 'Open the session',
|
|
726
726
|
willDo: 'What the agent will do: ', change: 'Change what the agent does', askLabel: 'What the agent will do',
|
|
727
727
|
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',
|
|
728
|
+
sitRedraft: (w) => `The agent is redrafting with your note. Nothing went to ${w}.`, sitRedraftMine: 'The agent is redrafting with your note. The draft stays here in PMOS.',
|
|
729
|
+
orDecide: 'Or decide', conv: 'Conversation with the agent', show: 'Show', hide: 'Hide', showAll: 'Show all', showLess: 'Show less', open: 'Open', copy: 'Copy', copied: 'Copied.',
|
|
730
|
+
send: 'Send to the agent', replyWhat: (w) => `The agent drafts it again. ${w ? `Nothing goes to ${w} or to anyone.` : 'Nothing leaves PMOS.'}`,
|
|
731
|
+
theDraft: 'the draft above', beingRedrafted: 'being redrafted',
|
|
732
|
+
round: (n) => 'Agent draft, round ' + n, yours: 'Edited by you', viewRound: (n) => 'View round ' + n, viewEdit: 'View your edit', earlier: 'earlier version',
|
|
728
733
|
noteLabel: 'Tell the agent what to change', openDraft: 'Open the draft', editDraft: 'Edit the draft', edited: 'Edited by you. Post sends your text.',
|
|
729
734
|
saveDraft: 'Save the draft', cancel: 'Cancel', savedDraft: (w) => `Saved. Post sends your text to ${w}.`, savedMine: 'Saved. The draft stays here.', bodyLabel: 'The draft',
|
|
730
735
|
doneIt: 'Mark it done', doneFx: 'Closes the item. Nothing is posted', doneToast: 'Done. Nothing was posted.', draftSheet: (k) => 'Draft for ' + k, stop: 'stop',
|
|
@@ -760,7 +765,7 @@ function agentCase(inst, item, L) {
|
|
|
760
765
|
const o = (id, l, f, run, extra) => ({ id, l, rec: 0, fx: f, run, ...(extra || {}) });
|
|
761
766
|
const own = L.opts.slice(0, 2).map((x) => ({ ...x, rec: 0 }));
|
|
762
767
|
const ask = () => inst.asks.get(item.id) || a.instruction || a.suggest || '';
|
|
763
|
-
let sit = ''; let opts = [];
|
|
768
|
+
let sit = ''; let opts = []; const quote = ''; const chg = []; let outbox = false; let cta = ''; let agentAsk = null; let before = null;
|
|
764
769
|
const extras = [];
|
|
765
770
|
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 + ' ',
|
|
766
771
|
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))));
|
|
@@ -780,45 +785,117 @@ function agentCase(inst, item, L) {
|
|
|
780
785
|
editing ? el('div', { class: 'form wk-agent-ask' }, input, button({ label: COPY.save, variant: 'outline', size: 'sm', onClick: save })) : null));
|
|
781
786
|
if (!editing) agentAsk = c3btn('chip2 lk', { 'data-pm-agent-change': item.id }, [c3ic('edit', 'sm'), AG.change], () => { inst.editing.add('ask:' + item.id); draw(inst); });
|
|
782
787
|
}
|
|
783
|
-
} else if (a.state === 'queued') {
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
sit =
|
|
788
|
+
} else if (a.state === 'queued' || a.state === 'working') {
|
|
789
|
+
// a redraft after the PM's note shows the draft and the conversation above the strip (amendment 15)
|
|
790
|
+
const redraft = !!(a.draft && a.draft.body);
|
|
791
|
+
sit = redraft ? (isMine(item) ? AG.sitRedraftMine : AG.sitRedraft(w)) : a.state === 'queued' ? AG.sitQueued : AG.sitWorking;
|
|
792
|
+
if (redraft) extras.push(draftCard(inst, item, k, true), conversation(inst, item, true));
|
|
793
|
+
extras.push(a.state === 'queued' ? strip(false, 'clock', 'Queued' + (a.decided_at ? ' at ' + hhmm(a.decided_at) : ''))
|
|
794
|
+
: strip(true, 'spark', (redraft ? 'Redrafting with your note' : 'Working') + (a.started_at ? ' · started ' + minsAgo(a.started_at) : '')));
|
|
787
795
|
} else if (a.state === 'ready') {
|
|
788
|
-
|
|
789
|
-
quote = d.body || ''; sit = AG.sitReady('', w);
|
|
790
|
-
if (d.summary) chg.push(['doc', d.summary, 'draft']);
|
|
791
|
-
if (d.edited_at) chg.push(['edit', AG.edited, '']);
|
|
792
|
-
if (a.revisions) chg.push(['reply', 'Redrafted after your note' + (a.revisions > 1 ? 's (' + a.revisions + ')' : ''), '']);
|
|
796
|
+
sit = AG.sitReady('', w);
|
|
793
797
|
outbox = true; cta = 'Hold to post';
|
|
794
|
-
const
|
|
795
|
-
const back = () => { const v = note.value.trim(); if (!v) { say({ text: AG.needNote, timeout: 3500 }); note.focus(); return; } agentPress(inst, '/api/my-work/decide', { item: item.id, action: 'revise', note: v }, AG.sentBack); };
|
|
796
|
-
note.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.isComposing) back(); });
|
|
798
|
+
const mine = isMine(item);
|
|
797
799
|
opts = [
|
|
798
|
-
|
|
799
|
-
o('
|
|
800
|
-
o('
|
|
801
|
-
o('
|
|
800
|
+
mine ? 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 }),
|
|
801
|
+
mine ? o('done', AG.doneIt, [fx('check', AG.doneFx, 'ok')], (r) => doneAct(inst, item, r, AG.doneToast)) : null,
|
|
802
|
+
o('edit', AG.editDraft, [fx('edit', mine ? 'Your version replaces the draft here' : 'Post then sends your version', 'ok')], () => openDraft(item, inst, true)),
|
|
803
|
+
o('keep', AG.keepDraft, [fx('check', 'Nothing leaves PMOS', 'ok')], () => say({ text: mine ? AG.keptMine : AG.kept, timeout: 3000 })),
|
|
804
|
+
mine ? null : o('done', AG.doneIt, [fx('check', AG.doneFx, 'ok')], (r) => doneAct(inst, item, r, AG.doneToast)),
|
|
802
805
|
].filter(Boolean);
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
c3btn('chip2 lk', { 'data-pm-agent-edit': item.id }, [c3ic('edit', 'sm'), AG.editDraft], () => openDraft(item, inst, true))));
|
|
806
|
-
after = noteBox;
|
|
806
|
+
extras.push(draftCard(inst, item, k, false), conversation(inst, item, false, w));
|
|
807
|
+
before = el('div', { class: 'wk-conv-h', 'data-pm-agent-decide': item.id }, el('span', { class: 'dim mono' }, c3ic('check', 'sm'), AG.orDecide));
|
|
807
808
|
} else if (a.state === 'sending') {
|
|
808
809
|
sit = AG.sitSending(k);
|
|
810
|
+
if (a.draft && a.draft.body) extras.push(draftCard(inst, item, k, false), conversation(inst, item, false));
|
|
809
811
|
} else if (a.state === 'sent') {
|
|
810
812
|
sit = AG.sitSent(a.sent && a.sent.at ? hhmm(a.sent.at) : '', w);
|
|
811
813
|
opts = L.opts.map((x) => ({ ...x }));
|
|
812
|
-
if (a.draft && a.draft.body) extras.push(
|
|
813
|
-
|
|
814
|
+
if (a.draft && a.draft.body) extras.push(draftCard(inst, item, k, false), conversation(inst, item, false));
|
|
815
|
+
if (a.sent && a.sent.link) extras.push(el('div', { class: 'chips2' }, el('a', { class: 'chip2 lk', href: a.sent.link, target: '_blank', rel: 'noopener noreferrer' }, c3ic('out', 'sm'), AG.link)));
|
|
814
816
|
}
|
|
815
817
|
if (opts.length) opts[0].rec = 1;
|
|
816
818
|
const model = { id: 'task:' + item.id, sit, quote, chg, conf: 0, cta, opts: opts.map((x) => ({ id: x.id, l: x.l, rec: x.rec, fx: x.fx, outward: !!x.outward })), why: '', outbox,
|
|
817
|
-
outboxReason: OUT_REASON, agentAsk, agentExtras: () => extras,
|
|
819
|
+
outboxReason: OUT_REASON, agentAsk, agentExtras: () => extras, agentBeforeOpts: before };
|
|
818
820
|
const run = {}; opts.forEach((x) => { if (x.run) run[x.id] = x.run; });
|
|
819
821
|
return { model, run };
|
|
820
822
|
}
|
|
821
823
|
|
|
824
|
+
/* ── the draft and the conversation with the agent (c3-work-agent amendment 15, mockup "Agent Draft Conversation") ──────── */
|
|
825
|
+
|
|
826
|
+
/** The turns on an item's task, oldest first: the PM's ask, then the log (drafts, notes, edits, the post). A task from before
|
|
827
|
+
* the log has its current draft only. Each draft or edit carries its round label and body. */
|
|
828
|
+
function turnsOf(item) {
|
|
829
|
+
const a = item.agent || {}; const d = a.draft || {};
|
|
830
|
+
let log = Array.isArray(a.log) && a.log.length ? a.log.slice() : d.body ? [{ who: 'agent', kind: 'draft', at: null, text: d.summary || '', body: d.body }] : [];
|
|
831
|
+
// an edit made before the log, or the draft body that the log does not hold: the current text is the last version
|
|
832
|
+
const last = [...log].reverse().find((t) => t.body);
|
|
833
|
+
if (d.body && last && last.body !== d.body) log.push({ who: 'pm', kind: 'edit', at: d.edited_at || null, body: d.body });
|
|
834
|
+
const ask = a.instruction || a.suggest || '';
|
|
835
|
+
const out = ask ? [{ who: 'pm', kind: 'ask', at: null, text: ask }] : [];
|
|
836
|
+
let round = 0;
|
|
837
|
+
for (const t of log) out.push(t.body ? { ...t, label: t.who === 'agent' ? AG.round(++round) : AG.yours, round: t.who === 'agent' ? round : 0 } : t);
|
|
838
|
+
return out;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const copyText = (text) => { try { navigator.clipboard.writeText(String(text || '')).then(() => say({ text: AG.copied, timeout: 2000 }), () => {}); } catch { /* no clipboard */ } };
|
|
842
|
+
|
|
843
|
+
/** The draft as a document card: what it is, which version, Open and Copy, the document clipped with Show all. */
|
|
844
|
+
function draftCard(inst, item, k, busy) {
|
|
845
|
+
const a = item.agent; const d = a.draft || {};
|
|
846
|
+
const cur = [...turnsOf(item)].reverse().find((t) => t.body) || { label: AG.round(1), at: d.at || null };
|
|
847
|
+
const all = inst.editing.has('all:' + item.id);
|
|
848
|
+
const long = String(d.body || '').length > 900 || String(d.body || '').split('\n').length > 16;
|
|
849
|
+
const body = el('div', { class: 'ds-md wk-agent-md' + (long && !all ? ' wk-doc-clip' : ''), 'data-pm-agent-doc-body': item.id }, toDom(parseMd(String(d.body || '')), document));
|
|
850
|
+
return el('section', { class: 'card wk-doc', 'data-pm-agent-doc': item.id, 'aria-label': 'The draft' },
|
|
851
|
+
el('div', { class: 'wk-doc-h' },
|
|
852
|
+
el('div', { class: 'chips' }, el('span', { class: 'tag' }, a.state === 'sent' ? 'posted' : 'draft'), busy ? el('span', { class: 'tag' }, AG.beingRedrafted) : null,
|
|
853
|
+
el('b', { class: 'wk-doc-to' }, isMine(item) ? 'Draft for you' : AG.draftSheet(k)),
|
|
854
|
+
el('span', { class: 'chip2', 'data-pm-agent-version': item.id }, c3ic(cur.kind === 'edit' ? 'edit' : 'spark', 'sm'), cur.label + (cur.at ? ' · ' + hhmm(cur.at) : ''))),
|
|
855
|
+
el('div', { class: 'chips2' }, c3btn('chip2 lk', { 'data-pm-agent-draft': item.id }, [c3ic('eye', 'sm'), AG.open], () => openDraft(item, inst)),
|
|
856
|
+
c3btn('chip2 lk', { 'data-pm-agent-copy': item.id }, [c3ic('copy', 'sm'), AG.copy], () => copyText(d.body)))),
|
|
857
|
+
body,
|
|
858
|
+
long ? c3btn('chip2 lk', { 'data-pm-agent-all': item.id, 'aria-expanded': String(all) }, [c3ic(all ? 'zout' : 'zin', 'sm'), all ? AG.showLess : AG.showAll],
|
|
859
|
+
() => { if (all) inst.editing.delete('all:' + item.id); else inst.editing.add('all:' + item.id); draw(inst); }) : null);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/** The conversation: one line per turn, then the reply box that sends the draft back (only while a draft waits). */
|
|
863
|
+
function conversation(inst, item, busy, w) {
|
|
864
|
+
const a = item.agent; const turns = turnsOf(item); const n = turns.length;
|
|
865
|
+
const only = n <= 2 && !busy; const show = !only || inst.editing.has('thr:' + item.id);
|
|
866
|
+
const cur = [...turns].reverse().find((t) => t.body);
|
|
867
|
+
const lead = { ask: isMine(item) ? 'added' : 'asked', note: 'sent it back', edit: 'edited the draft', post: 'posted it' };
|
|
868
|
+
const line = (t) => {
|
|
869
|
+
const you = t.who !== 'agent';
|
|
870
|
+
const text = t.kind === 'note' ? el('blockquote', { class: 'qd' }, t.text || '') : t.kind === 'draft' ? el('p', null, t.text || 'Drafted it') : t.kind === 'ask' ? el('p', null, t.text) : null;
|
|
871
|
+
const ver = !t.body ? null : t === cur ? el('span', { class: 'tag' }, a.state === 'sent' ? 'posted' : AG.theDraft)
|
|
872
|
+
: c3btn('chip2 lk', { 'data-pm-agent-round': String(t.round || 0) }, [c3ic('eye', 'sm'), t.kind === 'edit' ? AG.viewEdit : AG.viewRound(t.round)], () => openDraft(item, inst, false, t));
|
|
873
|
+
return el('li', { class: you ? 'you' : 'ag' }, c3ic(you ? 'user' : 'spark', 'sm'),
|
|
874
|
+
el('div', { class: 'wk-turn' }, el('div', { class: 'wk-who' }, el('b', null, you ? 'You' : 'Agent'), el('span', { class: 'dim mono' }, [t.at ? hhmm(t.at) : '', lead[t.kind] || ''].filter(Boolean).join(' · '))),
|
|
875
|
+
text, ver ? el('div', { class: 'chips2' }, ver) : null));
|
|
876
|
+
};
|
|
877
|
+
const head = el('div', { class: 'wk-conv-h' }, el('span', { class: 'dim mono' }, c3ic('reply', 'sm'), AG.conv), el('span', { class: 'tag' }, String(n)),
|
|
878
|
+
only ? c3btn('chip2 lk', { 'data-pm-agent-thread': item.id, 'aria-expanded': String(show) }, [c3ic(show ? 'zout' : 'zin', 'sm'), show ? AG.hide : AG.show],
|
|
879
|
+
() => { if (show) inst.editing.delete('thr:' + item.id); else inst.editing.add('thr:' + item.id); draw(inst); }) : null);
|
|
880
|
+
let reply = null;
|
|
881
|
+
if (a.state === 'ready') {
|
|
882
|
+
const ta = el('textarea', { rows: '2', placeholder: AG.noteLabel, 'aria-label': AG.noteLabel, 'data-pm-agent-note-in': item.id });
|
|
883
|
+
ta.value = inst.asks.get('note:' + item.id) || '';
|
|
884
|
+
ta.addEventListener('input', () => inst.asks.set('note:' + item.id, ta.value));
|
|
885
|
+
const send = () => {
|
|
886
|
+
const v = ta.value.trim();
|
|
887
|
+
if (!v) { say({ text: AG.needNote, timeout: 3500 }); ta.focus(); return; }
|
|
888
|
+
inst.asks.delete('note:' + item.id);
|
|
889
|
+
agentPress(inst, '/api/my-work/decide', { item: item.id, action: 'revise', note: v }, AG.sentBack);
|
|
890
|
+
};
|
|
891
|
+
ta.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); send(); } });
|
|
892
|
+
reply = el('div', { class: 'wk-reply', 'data-pm-agent-note': item.id }, ta,
|
|
893
|
+
el('div', { class: 'wk-reply-f' }, el('span', { class: 'dim' }, c3ic('lock', 'sm'), ' ' + AG.replyWhat(isMine(item) ? '' : w)),
|
|
894
|
+
button({ label: AG.send, variant: 'primary', size: 'sm', onClick: send, attrs: { 'data-pm-agent-send': item.id } })));
|
|
895
|
+
}
|
|
896
|
+
return el('div', { class: 'wk-conv', 'data-pm-agent-conv': item.id }, head, show ? el('ol', { class: 'chgl wk-thr', role: 'list' }, ...turns.map(line)) : null, reply);
|
|
897
|
+
}
|
|
898
|
+
|
|
822
899
|
function openAgentSession(id, item) {
|
|
823
900
|
try { window.dispatchEvent(new CustomEvent('pmos:open-session', { detail: { id, name: 'Job · ' + (item.key || item.title || '') } })); } catch { /* no event */ }
|
|
824
901
|
}
|
|
@@ -828,9 +905,9 @@ const draftMd = (text) => el('div', { class: 'ds-md wk-agent-md' }, toDom(parseM
|
|
|
828
905
|
|
|
829
906
|
/* 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
|
|
830
907
|
* as the draft, so Post sends exactly that text. */
|
|
831
|
-
function openDraft(item, inst, editing) {
|
|
832
|
-
const a = item.agent || {}; const d = a.draft || {};
|
|
833
|
-
const canEdit = !!inst && a.state === 'ready';
|
|
908
|
+
function openDraft(item, inst, editing, ver) {
|
|
909
|
+
const a = item.agent || {}; const d = ver ? { body: ver.body, summary: ver.kind === 'draft' ? ver.text : '' } : a.draft || {};
|
|
910
|
+
const canEdit = !!inst && a.state === 'ready' && !ver;
|
|
834
911
|
const area = el('textarea', { class: 'wk-agent-edit', rows: '18', 'aria-label': AG.bodyLabel, 'data-pm-agent-edit-in': item.id });
|
|
835
912
|
area.value = d.body || '';
|
|
836
913
|
const save = async () => {
|
|
@@ -848,12 +925,12 @@ function openDraft(item, inst, editing) {
|
|
|
848
925
|
button({ label: AG.cancel, variant: 'outline', size: 'sm', onClick: () => openDraft(item, inst, false) })))
|
|
849
926
|
: el('div', { class: 'wk-agent-doc' }, draftMd(d.body || ''));
|
|
850
927
|
const body = el('div', { class: 'wk-agent-sheet' },
|
|
851
|
-
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),
|
|
928
|
+
el('div', { class: 'chips' }, el('span', { class: 'tag' }, ver ? AG.earlier : 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),
|
|
852
929
|
d.summary ? el('p', { class: 'dim wk-agent-sum' }, d.summary) : null,
|
|
853
930
|
text,
|
|
854
|
-
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.')));
|
|
931
|
+
el('div', { class: 'fold' }, c3ic('info', 'sm'), ' ' + (ver ? 'This is an earlier version. The draft on the item is the newest one.' : a.state === 'sent' ? 'This is the text that was posted.' : isMine(item) ? 'This stays here in PMOS.' : 'Nothing is in ' + whereOf(item) + ' yet. Post it from the item, after you read it.')));
|
|
855
932
|
const actions = canEdit && !editing ? [c3btn('chip2 lk', { 'data-pm-agent-edit-sheet': item.id }, [c3ic('edit', 'sm'), AG.editDraft], () => openDraft(item, inst, true))] : null;
|
|
856
|
-
openSheet({ title: AG.draftSheet(item.key || item.title), icon: 'doc', body, actions });
|
|
933
|
+
openSheet({ title: ver ? ver.label + (ver.at ? ' · ' + hhmm(ver.at) : '') : AG.draftSheet(item.key || item.title), icon: 'doc', body, actions });
|
|
857
934
|
if (editing && canEdit) { try { area.focus(); } catch { /* not focusable yet */ } }
|
|
858
935
|
}
|
|
859
936
|
|
|
@@ -863,7 +940,7 @@ function caseFile(inst, item, lens) {
|
|
|
863
940
|
const root = renderCase(model, {
|
|
864
941
|
picked: inst.picks.get(item.id), outbox: !!model.outbox, noConf: !item.mock_row,
|
|
865
942
|
renderQuote: item.agent && !item.mock_row ? draftMd : undefined,
|
|
866
|
-
onPick: (id) => { inst.picks.set(item.id, id);
|
|
943
|
+
onPick: (id) => { inst.picks.set(item.id, id); },
|
|
867
944
|
onConfirm: ({ option }) => {
|
|
868
945
|
const fn = run[option.id];
|
|
869
946
|
if (fn) fn(rowOf(inst, item));
|
|
@@ -875,7 +952,8 @@ function caseFile(inst, item, lens) {
|
|
|
875
952
|
const agent = item.mock_row && item.agent_move && (item.ix || {}).move;
|
|
876
953
|
if (item.kind === 'offline') extras.push(offlineBlock(inst, item));
|
|
877
954
|
if (model.agentExtras) extras.push(...model.agentExtras());
|
|
878
|
-
if (model.
|
|
955
|
+
if (model.agentBeforeOpts) { const op = root.querySelector('.opts'); if (op) op.before(model.agentBeforeOpts); }
|
|
956
|
+
if (item.agent && !item.mock_row && ['ready', 'sending', 'sent', 'queued', 'working'].includes(item.agent.state)) root.classList.add('wk-ag');
|
|
879
957
|
if (rs && rs.phase === 'work') {
|
|
880
958
|
extras.push(el('div', { class: 'c3ag', 'data-ag': item.id }, el('div', { class: 'fold a', role: 'status' }, el('span', { class: 'c3spin' }, c3ic('spark', 'sm')), rs.steps[Math.min(rs.i, rs.steps.length - 1)] + ' · ' + (Math.min(rs.i, rs.steps.length - 1) + 1) + ' of ' + rs.steps.length,
|
|
881
959
|
el('button', { type: 'button', class: 'chip2 lk', 'data-pm-agent-stop': item.id, onclick: () => { inst.runs.delete(item.id); say({ text: 'Stopped. Nothing was kept.', timeout: 2500 }); draw(inst); } }, c3ic('stop', 'sm'), 'stop'))));
|
|
@@ -253,8 +253,7 @@ export const COPY = {
|
|
|
253
253
|
sendNote: 'Run it again with this note',
|
|
254
254
|
cancel: 'Cancel',
|
|
255
255
|
noteNeeded: 'Write what should change, in 1 to 500 characters.',
|
|
256
|
-
revisions: (n
|
|
257
|
-
noMoreRevisions: 'This job used all its revisions. Do the rest yourself.',
|
|
256
|
+
revisions: (n) => `Sent back ${n} time${n === 1 ? '' : 's'}`,
|
|
258
257
|
openConversation: 'Open the conversation ↗',
|
|
259
258
|
openDraft: 'Open the draft ↗',
|
|
260
259
|
openSent: 'Open what was sent ↗',
|
|
@@ -40,7 +40,6 @@ const D = COPY.day;
|
|
|
40
40
|
const A = COPY.act;
|
|
41
41
|
const SLOW_MS = 30000;
|
|
42
42
|
const FAST_MS = 4000;
|
|
43
|
-
const MAX_REVISIONS = 3;
|
|
44
43
|
const SCOPED = ['slack', 'jira', 'confluence'];
|
|
45
44
|
const NARROW = 860; // below this width of the section the detail goes under the list (components.css @container td-day)
|
|
46
45
|
|
|
@@ -999,7 +998,7 @@ function jobCard(ctx, j) {
|
|
|
999
998
|
t.draft.summary ? el('p', null, t.draft.summary) : null,
|
|
1000
999
|
t.draft.body ? el('pre', { class: 'td-day__draft' }, t.draft.body) : null) : null;
|
|
1001
1000
|
const acts = [];
|
|
1002
|
-
const canRevise = t && (j.status === 'review' || j.status === 'done')
|
|
1001
|
+
const canRevise = t && (j.status === 'review' || j.status === 'done') ;
|
|
1003
1002
|
if (t && j.status === 'review' && j.kind === 'approve') acts.push(button({ label: tagged(D.approve, 'you'), variant: 'primary', size: 'sm', disabled: busy, attrs: { 'data-day-act': 'approve' }, onClick: () => decide(ws, t, 'approve') }));
|
|
1004
1003
|
if (t && j.kind === 'auto' && (j.status === 'review' || (j.status === 'done' && !t.decided))) acts.push(button({ label: tagged(D.approveAuto, 'you'), size: 'sm', disabled: busy, attrs: { 'data-day-act': 'approve' }, onClick: () => decide(ws, t, 'approve') }));
|
|
1005
1004
|
if (canRevise && !ui.revising.has(t.id)) acts.push(button({ label: tagged(D.revise, 'agent'), size: 'sm', disabled: busy, attrs: { 'data-day-act': 'revise' }, onClick: () => { ui.revising.add(t.id); rerender(); } }));
|
|
@@ -1029,7 +1028,7 @@ function jobCard(ctx, j) {
|
|
|
1029
1028
|
t && j.status === 'failed' && t.error ? el('p', { class: 'td-day__run td-day__run--failed', role: 'alert' }, D.failedWhy(t.error)) : null,
|
|
1030
1029
|
j.status === 'sent' && t && ((t.sent && t.sent.at) || (t.decided && t.decided.at)) ? note(D.sentAt(hhmm((t.sent && t.sent.at) || t.decided.at))) : null,
|
|
1031
1030
|
draft,
|
|
1032
|
-
revs ? note(D.revisions(revs
|
|
1031
|
+
revs ? note(D.revisions(revs)) : null,
|
|
1033
1032
|
el('div', { class: 'td-day__acts' }, ...acts.filter(Boolean)),
|
|
1034
1033
|
form);
|
|
1035
1034
|
}
|
|
@@ -32,7 +32,7 @@ Procedure:
|
|
|
32
32
|
3. If MODE is revise:
|
|
33
33
|
a. Read the draft so far and the LAST note of the PM in the output of `day show`. The note says what to change. Earlier notes still hold unless the last one cancels them.
|
|
34
34
|
b. Change the draft to address the note, and only what the note asks. Read a source again only if the note needs it.
|
|
35
|
-
c. Submit the new draft as in step 2d. State in the summary what changed. The
|
|
35
|
+
c. Submit the new draft as in step 2d. State in the summary what changed. The PM can send the draft back as often as they want. Make this one count.
|
|
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.
|
|
@@ -27,7 +27,7 @@ Procedure:
|
|
|
27
27
|
- `type`: the issue type's name, for example `Epic`, `Story`, `Task` or `Bug`.
|
|
28
28
|
- `parent`: the parent issue's key (the epic of a story), or `null`.
|
|
29
29
|
- `category`: the status category, `new`, `indeterminate` or `done`.
|
|
30
|
-
- `sprint`: `{"id": "42", "name": "Sprint 42", "state": "active", "start": "2026-10-01", "end": "2026-10-14"}`
|
|
30
|
+
- `sprint`: Jira's sprint, as `{"id": "42", "name": "Sprint 42", "state": "active", "start": "2026-10-01", "end": "2026-10-14"}`. Copy `state` as Jira gives it: `"active"`, `"future"` (a planned sprint) or `"closed"`. Use `null` when the issue is in no sprint: never write a made-up sprint such as `"Future"` or `"Backlog"` for it. Copy `id`, `start`, `end`, `completed` (a closed sprint's completion date) and `goal` (the sprint goal, as Jira writes it) from Jira's sprint field; leave out any that Jira does not give.
|
|
31
31
|
- `points`: the story points as a number, or `null` when none are set.
|
|
32
32
|
- `resolved`: the resolution date as `YYYY-MM-DD`, or `null` when the issue is not resolved.
|
|
33
33
|
- `subtasks`: `{"done": 2, "total": 5}`.
|