create-pmos 0.206.0 → 0.208.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-ib-more-test.js +2 -2
- package/workspace/guide/jira.md +17 -1
- package/workspace/guide/my-work.md +3 -1
- package/workspace/lib/day-run.js +14 -1
- package/workspace/lib/engine/boards.js +3 -2
- package/workspace/lib/jira-status.js +10 -15
- package/workspace/lib/routes-my-work.js +44 -0
- package/workspace/lib/routes-pm-items.js +8 -4
- package/workspace/lib/work-assigned.js +32 -5
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-demo-boards.js +5 -5
- package/workspace/public/pm/c3-ib-sprints.js +12 -13
- package/workspace/public/pm/pm-work-view.js +5 -3
- package/workspace/server.js +2 -5
- 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.208.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-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-14T01:38:03Z"}
|
|
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-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-14T01:38:03Z"}
|
|
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-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-14T01:38:03Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -45,8 +45,8 @@ const { harness } = require('./c3-ini-harness');
|
|
|
45
45
|
ok('SP1 the active sprint header says Jira, read-only, and there is no banner', /Jira, read-only/.test(await text('[data-inb="sprints-source"]')) && await st(() => !!document.querySelector('[data-inb-state="active"] [data-inb="sprints-source"]') && !document.querySelector('.fold [data-inb="sprints-source"], .fold[data-inb="sprints-source"]')));
|
|
46
46
|
ok('SP2 the active sprint is drawn with its counts; future and closed sprints are listed', await st(() => !!document.querySelector('[data-inb-state="active"]') && !!document.querySelector('[data-inb-state="future"]') && !!document.querySelector('[data-inb-state="closed"]') && ((m) => !!m && Number(m[1]) === 12 && Number(m[2]) + Number(m[3]) + Number(m[4]) === 12)(/Items(\d+):(\d+)done,(\d+)inprogress,(\d+)todo/.exec(document.querySelector('[data-inb="sprint-counts"]').textContent.replace(/\s+/g, '')))));
|
|
47
47
|
// sprint-goal-summary: the goal (from Jira, or written from the tickets and why), the team summary, closed sprints read once
|
|
48
|
-
ok('SP4 the
|
|
49
|
-
&& /
|
|
48
|
+
ok('SP4 the sprint goal is written from the tickets, with Jira\'s goal only as a reference', await st(() => { const g = document.querySelector('[data-inb="sprint-summary"] [data-inb="sprint-goal"]'); return !!g && g.dataset.inbGoal === 'tickets' && /SPRINT GOAL · from the tickets/.test(g.textContent) && /In Jira: “Q4 delivery”/.test(g.textContent) && !/from Jira/.test(g.textContent); })
|
|
49
|
+
&& /from the tickets/.test(await text('[data-inb-state="future"] [data-inb="sprint-goal"]')) && !(await text('[data-inb-state="future"] [data-inb="sprint-goal"]')).includes('In Jira') && /from the tickets/.test(await text('[data-inb-state="closed"] [data-inb="sprint-goal"]')));
|
|
50
50
|
ok('SP5 what the team is working on: a row per theme, one with no initiative', await st(() => document.querySelectorAll('[data-inb="sprint-themes"] [data-inb-theme]').length === 2 && !!document.querySelector('[data-inb-theme="none"]') && /No initiative/.test(document.querySelector('[data-inb-theme="none"]').textContent)));
|
|
51
51
|
ok('SP6 a closed sprint shows done of total, carried, and that it was read and summarised once', /5 of 7 done/.test(await text('[data-inb-state="closed"]')) && /2 carried to/.test(await text('[data-inb-state="closed"]'))
|
|
52
52
|
&& /Read and summarised once on 29 Sep\. PMOS does not read a closed sprint again\./.test(await text('[data-inb-state="closed"] [data-inb="sprint-read-once"]')) && /Closed sprints/.test(await text('[data-inb="sprints-how"]')));
|
package/workspace/guide/jira.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Jira: tickets drafted from the PRD, and their status"
|
|
4
4
|
description: "How to draft Jira tickets from a PRD in your ticket template, push the ones that are ready, and see their status, sprint and progress on the initiative's Jira tickets tab."
|
|
5
|
-
timestamp: 2026-10-
|
|
5
|
+
timestamp: 2026-10-14
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Jira: tickets drafted from the PRD, and their status
|
|
@@ -119,6 +119,22 @@ pmos jira query --sprints
|
|
|
119
119
|
pmos jira sync --sprints --from <file|-> [--query "<jql>"]
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
+
## Each sprint's goal and summary
|
|
123
|
+
|
|
124
|
+
The check then writes a goal and a short summary for each sprint, from the tickets it read. The Sprints tab shows them above the sprint's items, with one row for each group of related work.
|
|
125
|
+
|
|
126
|
+
- The check always writes the goal from the sprint's tickets, because Jira's sprint goal is often empty, too broad (for example "Q4 delivery") or incomplete. The tab shows it as **from the tickets**.
|
|
127
|
+
- When Jira has a sprint goal, the tab shows it under the goal as a reference: **In Jira: "…"**.
|
|
128
|
+
- The active and the next sprint are summarised again at each check.
|
|
129
|
+
- A closed sprint is read and summarised once. After that, the check does not search it again and keeps its first summary.
|
|
130
|
+
|
|
131
|
+
The check writes `<state>/tracker/jira/_sprint-notes.md` with:
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
pmos jira summary
|
|
135
|
+
pmos jira summary --from <file|-> [--dry-run]
|
|
136
|
+
```
|
|
137
|
+
|
|
122
138
|
## Link an initiative to its Jira epics
|
|
123
139
|
|
|
124
140
|
Your team may already plan in Jira, with an epic for each piece of work. Then link the initiative to its epics. You do not have to draft or push tickets.
|
|
@@ -98,7 +98,9 @@ An item goes to the first view that fits these rules, in this order. Parked is S
|
|
|
98
98
|
|
|
99
99
|
### Let the agent draft an item
|
|
100
100
|
|
|
101
|
-
Open an
|
|
101
|
+
Open an item that shows **Agent can:**. The case file shows **What the agent will do**.
|
|
102
|
+
|
|
103
|
+
The offer is on Jira and WBS items with a suggestion, and on each item the morning brief suggests for you (for example a Slack reply or an email). On a brief item the offer is the reply or the note that the item asks for, and the draft goes back to the place the item came from, only when you post it.
|
|
102
104
|
|
|
103
105
|
1. To change the instruction, select **Change what the agent does** and type a new one.
|
|
104
106
|
2. Select **Let the agent draft this** and hold to confirm. This adds one job to today's day run, with the instruction.
|
package/workspace/lib/day-run.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* newRun(date, now) · readRun(raw, date)
|
|
16
16
|
* addTasks(run, brief) agent priorities (kind approve|auto) → waiting tasks
|
|
17
17
|
* addBriefed(run, spec) one task the PM briefed in My work (initiative my-work, PRD R5)
|
|
18
|
+
* withdraw(run, taskId) a failed task taken out, so its item can be briefed again
|
|
18
19
|
* addSend(run, spec, now) the PM's own text, written in the card, to post as written (today-act-prepare)
|
|
19
20
|
* toStart(run, limit) the tasks to start now, each with its mode (do | revise | send)
|
|
20
21
|
* started · deliver · decide · sent · fail · stop · sessionEnded · sessionStalled
|
|
@@ -283,6 +284,18 @@ function stop(run, taskId, reason, now) {
|
|
|
283
284
|
});
|
|
284
285
|
}
|
|
285
286
|
|
|
287
|
+
/** Take a failed task out of the run (c3-work-agent amendment 10): the PM stopped the agent on a brief item, so the item
|
|
288
|
+
* offers the draft again and a new press adds the task anew. */
|
|
289
|
+
function withdraw(run, taskId) {
|
|
290
|
+
if (!run) return { ok: false, error: 'no day run today' };
|
|
291
|
+
const t = findTask(run, taskId);
|
|
292
|
+
if (!t) return { ok: false, error: `no task ${taskId} in today's run` };
|
|
293
|
+
if (t.status !== 'failed') return { ok: false, error: `task ${taskId} is ${t.status} — only a stopped or failed task can be taken out` };
|
|
294
|
+
const next = clone(run);
|
|
295
|
+
next.tasks = next.tasks.filter((x) => x.id !== taskId);
|
|
296
|
+
return { ok: true, run: next };
|
|
297
|
+
}
|
|
298
|
+
|
|
286
299
|
/** A session ended. A task it was working on without delivering, or sending without reporting, has failed. */
|
|
287
300
|
function sessionEnded(run, sessionId, now) {
|
|
288
301
|
if (!run) return { ok: false, error: 'no day run today' };
|
|
@@ -400,5 +413,5 @@ function checkDone(run, { found = 0, resolved = 0 } = {}, now) {
|
|
|
400
413
|
}
|
|
401
414
|
|
|
402
415
|
module.exports = { MAX_PARALLEL, MAX_REVISIONS, MAX_CHECK_LOG, MAX_INTERVAL_MIN, STATUSES, KINDS, ACTIONS, TASK_RE,
|
|
403
|
-
newRun, readRun, findTask, modeOf, addTasks, addBriefed, addSend, toStart, started, deliver, decide, sent, fail, stop, sessionEnded, sessionStalled, stage, intervalMin,
|
|
416
|
+
newRun, readRun, findTask, modeOf, addTasks, addBriefed, addSend, toStart, started, deliver, decide, sent, fail, stop, withdraw, sessionEnded, sessionStalled, stage, intervalMin,
|
|
404
417
|
checkDue, checkStarted, checkDone };
|
|
@@ -656,7 +656,7 @@ function sprintsOf(snapshots, onBoard = () => true, whole = null, notes = null)
|
|
|
656
656
|
const n = nl.find((x) => x.name === sp.name);
|
|
657
657
|
const jg = jiraGoal(sp);
|
|
658
658
|
if (n) {
|
|
659
|
-
sp.goal = { from:
|
|
659
|
+
sp.goal = { from: 'tickets', text: n.goal.text, jira: n.goal.jira || jg || null }; // always from the tickets; Jira's goal is a reference
|
|
660
660
|
sp.summary = n.summary;
|
|
661
661
|
sp.themes = (n.themes || []).map((t) => ({ title: t.title, line: t.line || '', tickets: t.tickets || 0, done: t.done || 0, blocked: t.blocked || 0, people: t.people || '', initiative: t.initiative || null }));
|
|
662
662
|
sp.writtenAt = n.writtenAt || null;
|
|
@@ -664,7 +664,8 @@ function sprintsOf(snapshots, onBoard = () => true, whole = null, notes = null)
|
|
|
664
664
|
sp.closed = sp.state === 'closed' && n.closed ? { ...n.closed } : null;
|
|
665
665
|
sp.frozen = sp.state === 'closed' && n.state === 'closed';
|
|
666
666
|
} else {
|
|
667
|
-
sp.goal =
|
|
667
|
+
sp.goal = null;
|
|
668
|
+
sp.jiraGoal = jg;
|
|
668
669
|
sp.summary = null; sp.themes = []; sp.writtenAt = null; sp.summarisedTickets = null; sp.closed = null; sp.frozen = false;
|
|
669
670
|
}
|
|
670
671
|
}
|
|
@@ -336,11 +336,10 @@ function syncSprints(root, raw, { query, dryRun = false, now } = {}) {
|
|
|
336
336
|
function readSprints(root) { return readSnapshot(root, SPRINTS_FILE); }
|
|
337
337
|
|
|
338
338
|
// ── the goal and summary of each sprint (sprint-goal-summary) ─────────────────────────────────────────
|
|
339
|
-
// The Jira status check writes them from the tickets it read
|
|
340
|
-
//
|
|
341
|
-
//
|
|
339
|
+
// The Jira status check writes them from the tickets it read. The goal is always written from the tickets: Jira's sprint goal is
|
|
340
|
+
// often empty, too broad or incomplete (the PM, 2026-10-14), so it is kept only as a reference (goal.jira). A closed sprint's
|
|
341
|
+
// note is written once and kept; its sprint is not searched again.
|
|
342
342
|
const NOTES_FILE = '_sprint-notes';
|
|
343
|
-
const GOAL_FROM = ['jira', 'none', 'broad', 'partial'];
|
|
344
343
|
const INI_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,80}$/;
|
|
345
344
|
const sameSprint = (a, b) => !!a && !!b && ((a.id && b.id) ? String(a.id) === String(b.id) : String(a.name) === String(b.name));
|
|
346
345
|
function readSprintNotes(root) {
|
|
@@ -364,15 +363,11 @@ function noteOne(x) {
|
|
|
364
363
|
const state = String(x.state || '').toLowerCase();
|
|
365
364
|
if (!SPRINT_STATES.includes(state)) return { error: `${name}: state is active, future or closed` };
|
|
366
365
|
const g = x.goal && typeof x.goal === 'object' ? x.goal : null;
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const
|
|
371
|
-
if (
|
|
372
|
-
const j = text(g.jira, 300);
|
|
373
|
-
if (!j) return { error: `${name}: a ${g.from} goal carries Jira's own goal in goal.jira` };
|
|
374
|
-
goal.jira = j;
|
|
375
|
-
}
|
|
366
|
+
const gt = g ? text(g.text, 240) : '';
|
|
367
|
+
if (!gt) return { error: `${name}: the goal has no text — write it from the sprint's tickets` };
|
|
368
|
+
const goal = { from: 'tickets', text: gt };
|
|
369
|
+
const j = text(g.jira, 300);
|
|
370
|
+
if (j) goal.jira = j; // Jira's own sprint goal, kept for reference only
|
|
376
371
|
const summary = text(x.summary, 900);
|
|
377
372
|
if (!summary) return { error: `${name}: the summary is empty` };
|
|
378
373
|
const themes = [];
|
|
@@ -437,12 +432,12 @@ function saveSprintNotes(root, raw, { dryRun = false, now } = {}) {
|
|
|
437
432
|
if (dryRun || !saved.length) return result;
|
|
438
433
|
const file = snapshotPath(root, NOTES_FILE);
|
|
439
434
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
440
|
-
const rows = notes.map((o) => `| ${cell(o.name)} | ${cell(o.state)} | ${cell(o.goal.text)} | ${o.goal.
|
|
435
|
+
const rows = notes.map((o) => `| ${cell(o.name)} | ${cell(o.state)} | ${cell(o.goal.text)} | ${o.goal.jira ? cell(o.goal.jira) : '—'} | ${cell(o.writtenAt)} |`);
|
|
441
436
|
const body = [
|
|
442
437
|
'---', 'type: Reference', 'title: "Jira sprints: goal and summary"', `timestamp: ${writtenAt}`, '---', '',
|
|
443
438
|
'# Jira sprints: goal and summary', '',
|
|
444
439
|
'Written by the Jira status check from the tickets it read, read-only. The Sprints tab of a board shows it. A closed sprint is written once and kept. Written by `pmos jira summary`; do not edit by hand.', '',
|
|
445
|
-
'| Sprint | State | Goal | Goal
|
|
440
|
+
'| Sprint | State | Goal (from the tickets) | Goal in Jira | Written |', '|---|---|---|---|---|', ...rows, '',
|
|
446
441
|
'```json', JSON.stringify({ written_at: writtenAt, sprints: notes }, null, 0), '```', '',
|
|
447
442
|
].join('\n');
|
|
448
443
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
@@ -28,6 +28,7 @@ const myWork = require('./my-work.js');
|
|
|
28
28
|
const boardFold = require('./board-fold.js');
|
|
29
29
|
const offloop = require('./offloop.js');
|
|
30
30
|
const dayRun = require('./day-run.js');
|
|
31
|
+
const assigned = require('./work-assigned.js');
|
|
31
32
|
const { localDate } = require('./brief.js');
|
|
32
33
|
|
|
33
34
|
const dirOf = (ctx) => path.join(ctx.runtimeDir, 'my-work');
|
|
@@ -121,6 +122,9 @@ async function handle(req, res, url, ctx) {
|
|
|
121
122
|
return true;
|
|
122
123
|
}
|
|
123
124
|
|
|
125
|
+
const bm = BRIEF_ITEM.exec(String((body && body.item) || ''));
|
|
126
|
+
if (bm && p !== '/api/my-work/refresh') { briefPress(ctx, res, p.slice('/api/my-work/'.length), body, bm[1], now); return true; }
|
|
127
|
+
|
|
124
128
|
if (p === '/api/my-work/decide' || p === '/api/my-work/stop') { taskPress(ctx, res, p.slice('/api/my-work/'.length), body, now); return true; }
|
|
125
129
|
|
|
126
130
|
// act
|
|
@@ -196,4 +200,44 @@ function taskPress(ctx, res, verb, body, now) {
|
|
|
196
200
|
ctx.json(res, { ok: true, item: after });
|
|
197
201
|
}
|
|
198
202
|
|
|
203
|
+
/* ── the agent on an item the morning brief suggests for the PM (c3-work-agent amendment 10) ──────────────────────
|
|
204
|
+
* Item `brief:<id>`; its task is `t-<id>` in today's day run, added as Your day's "Have the agent draft it" adds it. */
|
|
205
|
+
const BRIEF_ITEM = /^brief:([0-9a-f]{12})$/;
|
|
206
|
+
|
|
207
|
+
function briefPress(ctx, res, verb, body, id, now) {
|
|
208
|
+
const today = localDate(now);
|
|
209
|
+
let brief = null; try { brief = JSON.parse(fs.readFileSync(path.join(ctx.runtimeDir, 'briefs', today + '.json'), 'utf8')); } catch { brief = null; }
|
|
210
|
+
const x = brief && brief.date === today ? [...(brief.priorities || []), ...(brief.items || [])].find((y) => y && y.id === id) : null;
|
|
211
|
+
if (!x || x.owner !== 'pm') { ctx.json(res, { ok: false, error: 'no item of yours with that id in today\'s brief' }, 404); return; }
|
|
212
|
+
const taskId = 't-' + id;
|
|
213
|
+
const after = (task) => ctx.json(res, { ok: true, item: { id: 'brief:' + id, agent: assigned.briefAgent(x, ctx.readDayRun ? ctx.readDayRun(today) : null) }, task: task || null });
|
|
214
|
+
if (verb === 'act') {
|
|
215
|
+
if (body.action !== 'brief') { ctx.json(res, { ok: false, error: 'a brief item takes only action brief here — mark it done or set it aside from Work' }, 400); return; }
|
|
216
|
+
// the offer as shown ("Draft your reply: …") is not a change: the task then gets the full ask, written for the agent
|
|
217
|
+
const own = typeof body.instruction === 'string' && body.instruction.trim() !== assigned.briefSuggest(x) ? body.instruction.trim().slice(0, 500) : '';
|
|
218
|
+
const b = ctx.myWorkBrief(assigned.briefDraftSpec(x, own), String(body.ws || ''));
|
|
219
|
+
if (!b.ok) { ctx.json(res, { ok: false, error: b.error }, b.status || 500); return; }
|
|
220
|
+
after(b.task); return;
|
|
221
|
+
}
|
|
222
|
+
const run = ctx.readDayRun ? ctx.readDayRun(today) : null;
|
|
223
|
+
const t = run ? dayRun.findTask(run, taskId) : null;
|
|
224
|
+
if (!t) { ctx.json(res, { ok: false, error: 'no agent works on this item — let the agent draft it first' }, 409); return; }
|
|
225
|
+
let next = null;
|
|
226
|
+
if (verb === 'decide') {
|
|
227
|
+
const r = dayRun.decide(run, taskId, { action: body.action, note: body.note }, now);
|
|
228
|
+
if (!r.ok) { ctx.json(res, { ok: false, error: r.error }, 400); return; }
|
|
229
|
+
next = ctx.startDayTasks ? ctx.startDayTasks(r.run, now) : r.run;
|
|
230
|
+
} else {
|
|
231
|
+
// stop: only a task that has not delivered; its session ends and the task leaves the run, so the item offers the draft again
|
|
232
|
+
if (t.status !== 'waiting' && t.status !== 'working' && t.status !== 'failed') { ctx.json(res, { ok: false, error: `the agent's task is ${t.status} — there is nothing to stop` }, 409); return; }
|
|
233
|
+
if (t.session_id && t.status === 'working' && ctx.endSession) { try { ctx.endSession(t.session_id); } catch { /* the session already ended */ } }
|
|
234
|
+
const f = t.status === 'failed' ? { ok: true, run } : dayRun.stop(run, taskId, 'You stopped it.', now);
|
|
235
|
+
const w = f.ok ? dayRun.withdraw(f.run, taskId) : f;
|
|
236
|
+
if (!w.ok) { ctx.json(res, { ok: false, error: w.error }, 409); return; }
|
|
237
|
+
next = w.run;
|
|
238
|
+
}
|
|
239
|
+
if (!ctx.writeDayRun || !ctx.writeDayRun(next)) { ctx.json(res, { ok: false, error: 'could not save — nothing changed' }, 500); return; }
|
|
240
|
+
after(dayRun.findTask(next, taskId));
|
|
241
|
+
}
|
|
242
|
+
|
|
199
243
|
module.exports = { prefixes: ['/api/my-work'], handle, listFor, listForAsync };
|
|
@@ -116,19 +116,23 @@ function readBrief(ctx, now) {
|
|
|
116
116
|
if (!ctx.runtimeDir) return null;
|
|
117
117
|
try { return JSON.parse(fs.readFileSync(path.join(ctx.runtimeDir, 'briefs', require('./brief.js').localDate(now.getTime()) + '.json'), 'utf8')); } catch { return null; }
|
|
118
118
|
}
|
|
119
|
-
|
|
119
|
+
// today's day run, for the agent state of a brief item (c3-work-agent amendment 10). Best effort: no run, no task.
|
|
120
|
+
function readRun(ctx, now) {
|
|
121
|
+
try { return typeof ctx.readDayRun === 'function' ? ctx.readDayRun(require('./brief.js').localDate(now.getTime())) : null; } catch { return null; }
|
|
122
|
+
}
|
|
123
|
+
function assignedFrom(myWork, brief, now, run) {
|
|
120
124
|
const todayIso = require('./brief.js').localDate(now.getTime());
|
|
121
|
-
return [...assigned.fromMyWork(myWork && myWork.ok ? myWork.items : []), ...assigned.fromBrief(brief, todayIso)];
|
|
125
|
+
return [...assigned.fromMyWork(myWork && myWork.ok ? myWork.items : []), ...assigned.fromBrief(brief, todayIso, run)];
|
|
122
126
|
}
|
|
123
127
|
async function assignedAsync(ctx, now) {
|
|
124
128
|
let mw = null;
|
|
125
129
|
try { mw = await require('./routes-my-work.js').listForAsync(ctx, now.getTime()); } catch { mw = null; }
|
|
126
|
-
return assignedFrom(mw, readBrief(ctx, now), now);
|
|
130
|
+
return assignedFrom(mw, readBrief(ctx, now), now, readRun(ctx, now));
|
|
127
131
|
}
|
|
128
132
|
function assignedSync(ctx, now) {
|
|
129
133
|
let mw = null;
|
|
130
134
|
try { mw = require('./routes-my-work.js').listFor(ctx, now.getTime()); } catch { mw = null; }
|
|
131
|
-
return assignedFrom(mw, readBrief(ctx, now), now);
|
|
135
|
+
return assignedFrom(mw, readBrief(ctx, now), now, readRun(ctx, now));
|
|
132
136
|
}
|
|
133
137
|
const VIRTUAL_ID = /^(jira:|wbs:|brief:)/;
|
|
134
138
|
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
const TIER_PRIORITY = { P1: 'P1', P2: 'P2', P3: 'P3' };
|
|
16
16
|
const AGENT = 'an agent';
|
|
17
|
-
const SHOWN = ['origin', 'key', 'url', 'status', 'suggest', 'why', 'my_work_state', 'repo', 'initiative', 'agent'];
|
|
17
|
+
const SHOWN = ['origin', 'key', 'url', 'status', 'suggest', 'why', 'my_work_state', 'repo', 'initiative', 'agent', 'place'];
|
|
18
18
|
|
|
19
19
|
/* The agent on a my-work item (initiative c3-work-agent): its state, read from lib/my-work.js stateOf.
|
|
20
20
|
* suggest nothing started (the suggestion, when there is one, is the offer) queued · working · sending · failed the day task
|
|
@@ -56,7 +56,33 @@ function fromMyWork(items) {
|
|
|
56
56
|
return out;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
/* An item the morning brief suggests for the PM (amendment 10): the agent drafts it on the PM's press, as Your day's "Have the
|
|
60
|
+
* agent draft it" does. Its task in today's day run is `t-<brief item id>`; the state is read from that task. */
|
|
61
|
+
const BRIEF_VERB = { reply: 'reply', review: 'review notes', decide: 'answer', 'sign-off': 'sign-off comment', read: 'summary', do: 'draft' };
|
|
62
|
+
const briefVerb = (x) => BRIEF_VERB[x.verb] || (x.reply_needed ? 'reply' : 'draft');
|
|
63
|
+
const briefWhat = (x) => String(x.todo || x.how || x.ask || x.title || '').trim();
|
|
64
|
+
/** The offer shown on the item: "Draft your reply: …". */
|
|
65
|
+
function briefSuggest(x) { return `Draft your ${briefVerb(x)}: ${briefWhat(x)}`.slice(0, 300); }
|
|
66
|
+
/** What lib/day-run.js addBriefed takes for brief item `x`; `instruction` is the PM's changed ask, or empty for the offer. */
|
|
67
|
+
function briefDraftSpec(x, instruction) {
|
|
68
|
+
const ask = String(instruction || '').trim() || `Draft the PM's ${briefVerb(x)} for this item: ${briefWhat(x)}`;
|
|
69
|
+
return { id: 't-' + x.id, item_id: x.id, title: x.title, source: x.source, link: x.link || null,
|
|
70
|
+
instruction: `${ask}${/[.!?]$/.test(ask) ? '' : '.'} Draft only. Send nothing: the PM approves it first.`,
|
|
71
|
+
context: { from: x.who || null, where: x.where || null, about: x.about || null, ask: x.ask || null, how: x.how || null, why_now: x.why_now || null } };
|
|
72
|
+
}
|
|
73
|
+
const BRIEF_STATE = { waiting: 'queued', working: 'working', sending: 'sending', failed: 'failed', review: 'ready', done: 'ready', sent: 'sent' };
|
|
74
|
+
function briefAgent(x, run) {
|
|
75
|
+
const t = run && Array.isArray(run.tasks) ? run.tasks.find((y) => y && y.id === 't-' + x.id) || null : null;
|
|
76
|
+
const base = { suggest: briefSuggest(x), instruction: null, draft: null, sent: null, error: null, session: null, asks: null, revisions: 0, started_at: null, decided_at: null };
|
|
77
|
+
if (!t) return { state: 'suggest', ...base };
|
|
78
|
+
return { ...base, state: BRIEF_STATE[t.status] || 'queued', instruction: t.instruction || null,
|
|
79
|
+
draft: t.draft ? { summary: t.draft.summary || null, body: t.draft.body || null } : null,
|
|
80
|
+
sent: t.sent ? { link: t.sent.link || null, at: t.sent.at || null } : null, error: t.error || null,
|
|
81
|
+
session: t.stalled_session ? String(t.stalled_session) : null, asks: t.asks || null, revisions: Array.isArray(t.revisions) ? t.revisions.length : 0,
|
|
82
|
+
started_at: t.started_at || null, decided_at: (t.decided && t.decided.at) || null };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function fromBrief(brief, todayIso, run = null) {
|
|
60
86
|
if (!brief || brief.date !== todayIso) return [];
|
|
61
87
|
const list = [...(Array.isArray(brief.priorities) ? brief.priorities : []), ...(Array.isArray(brief.items) ? brief.items : [])];
|
|
62
88
|
const seen = new Set();
|
|
@@ -65,7 +91,8 @@ function fromBrief(brief, todayIso) {
|
|
|
65
91
|
if (!x || !x.id || !x.title || x.owner !== 'pm' || x.worth_asking || seen.has(x.id)) continue;
|
|
66
92
|
seen.add(x.id);
|
|
67
93
|
out.push({ id: 'brief:' + x.id, title: String(x.title), source: 'brief', origin: 'brief', suggested: true, flagged: true,
|
|
68
|
-
due: null, priority: null, waiting_on: null, job: null, why: x.why_now || null, initiative: x.initiative_id || null
|
|
94
|
+
due: null, priority: null, waiting_on: null, job: null, why: x.why_now || null, initiative: x.initiative_id || null,
|
|
95
|
+
place: x.source || null, agent: briefAgent(x, run && run.date === todayIso ? run : null) });
|
|
69
96
|
}
|
|
70
97
|
return out;
|
|
71
98
|
}
|
|
@@ -77,7 +104,7 @@ function addable(virtuals, itemLines) {
|
|
|
77
104
|
}
|
|
78
105
|
|
|
79
106
|
function createdEvent(v) {
|
|
80
|
-
const { id, source, agent, ...fields } = v; // the agent state is read on every list, never stored
|
|
107
|
+
const { id, source, agent, place, ...fields } = v; // the agent state (and a brief item's place) is read on every list, never stored
|
|
81
108
|
return { event: 'created', id, fields, source };
|
|
82
109
|
}
|
|
83
110
|
|
|
@@ -90,4 +117,4 @@ function overlay(item, byId) {
|
|
|
90
117
|
return out;
|
|
91
118
|
}
|
|
92
119
|
|
|
93
|
-
module.exports = { fromMyWork, fromBrief, addable, createdEvent, overlay, agentOf, AGENT };
|
|
120
|
+
module.exports = { fromMyWork, fromBrief, addable, createdEvent, overlay, agentOf, briefAgent, briefSuggest, briefDraftSpec, AGENT };
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.208.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.208.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -385,15 +385,15 @@ export function handle(S, method, path, p, body) {
|
|
|
385
385
|
const counts = { todo: issues.filter((i) => i.category === 'todo').length, doing: issues.filter((i) => i.category === 'doing').length, done: issues.filter((i) => i.category === 'done').length };
|
|
386
386
|
const num = key === 'IDPF' ? 42 : 19;
|
|
387
387
|
const extra = (k, state, name, cat, n) => { const is = Array.from({ length: n }, (_, i) => ({ key: key + '-' + (1100 + k * 20 + i), summary: (own[i % Math.max(1, own.length)] || { title: 'Sprint work' }).title + ': part ' + (i + 1), status: cat === 'done' ? 'Done' : 'To Do', category: cat, initiative: (own[i % Math.max(1, own.length)] || {}).id || null })); return { name, state, issues: is, counts: { todo: cat === 'todo' ? n : 0, doing: 0, done: cat === 'done' ? n : 0 } }; };
|
|
388
|
-
// The goal and summary the Jira status check writes (sprint-goal-summary)
|
|
389
|
-
//
|
|
388
|
+
// The goal and summary the Jira status check writes (sprint-goal-summary). The goal is always written from the tickets;
|
|
389
|
+
// Jira's goal, when it has one, is a reference. The closed sprint was summarised once.
|
|
390
390
|
const ini0 = own[0] ? own[0].id : null;
|
|
391
391
|
const themes = [{ title: own[0] ? own[0].title : 'Sprint work', line: 'Most items are in progress or in review.', tickets: issues.filter((i) => i.initiative === ini0).length || 1, done: issues.filter((i) => i.initiative === ini0 && i.category === 'done').length, blocked: 1, people: 'Ana, Budi', initiative: ini0 },
|
|
392
392
|
{ title: 'Fixes and upkeep', line: 'Small fixes outside any initiative.', tickets: 2, done: 1, blocked: 0, people: 'Chen', initiative: null }];
|
|
393
|
-
const act = { name: key + ' Sprint ' + num, state: 'active', issues, counts, goal: { from: '
|
|
393
|
+
const act = { name: key + ' Sprint ' + num, state: 'active', issues, counts, goal: { from: 'tickets', text: 'Get the first initiative ready for internal users and clear the blocked work.', jira: 'Q4 delivery' },
|
|
394
394
|
summary: 'Most of the sprint is on the first initiative. One item is blocked. Two small fixes sit outside any initiative.', themes, writtenAt: TODAY + 'T06:30:00Z', summarisedTickets: issues.length, closed: null, frozen: false };
|
|
395
|
-
const fut = { ...extra(1, 'future', key + ' Sprint ' + (num + 1), 'todo', 3), goal: { from: '
|
|
396
|
-
const old = { ...extra(0, 'closed', key + ' Sprint ' + (num - 1), 'done', 5), goal: { from: '
|
|
395
|
+
const fut = { ...extra(1, 'future', key + ' Sprint ' + (num + 1), 'todo', 3), goal: { from: 'tickets', text: 'Finish the first initiative and start the beta.', jira: null }, summary: 'Planned so far: three follow-up items.', themes: [], writtenAt: TODAY + 'T06:30:00Z', closed: null, frozen: false };
|
|
396
|
+
const old = { ...extra(0, 'closed', key + ' Sprint ' + (num - 1), 'done', 5), goal: { from: 'tickets', text: 'Start the first initiative.', jira: null }, summary: 'Goal met. Five items shipped; two moved to the next sprint.', themes: [{ title: 'Sprint work', line: '', tickets: 5, done: 5, blocked: 0, people: '', initiative: ini0 }],
|
|
397
397
|
writtenAt: '2026-09-29T06:30:00Z', closed: { done: 5, total: 7, carried: 2, carriedTo: key + ' Sprint ' + num, points: '21 of 26' }, frozen: true };
|
|
398
398
|
return okb({ source: 'jira', readAt: TODAY + 'T08:10:00', sprints: [act, fut, old] });
|
|
399
399
|
}
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* and team summary from mockup-c3-sprint-goal v3, initiative sprint-goal-summary).
|
|
3
3
|
* registerBoardTab('sprints', (host, ctx) => ...)
|
|
4
4
|
* Reads GET /api/boards/sprints { source: jira|none, readAt, sprints: [{ name, state: active|future|closed, start, end, issues: [{ key, summary,
|
|
5
|
-
* status, category: todo|doing|done, initiative }], counts, goal: { from:
|
|
5
|
+
* status, category: todo|doing|done, initiative }], counts, goal: { from: 'tickets', text, jira } | null, jiraGoal?, summary,
|
|
6
6
|
* themes: [{ title, line, tickets, done, blocked, people, initiative }], writtenAt, closed: { done, total, carried, carriedTo, points } | null,
|
|
7
7
|
* frozen }] }. READ-ONLY: the sprints mirror Jira, so there is no create, edit or move control.
|
|
8
|
-
* Left: the active sprint's goal and summary (the goal
|
|
8
|
+
* Left: the active sprint's goal and summary (the goal, always written from the tickets, with Jira's goal as a reference; what the team is working on,
|
|
9
9
|
* one row per theme), its items (header chip: Jira, read-only and the read time; c3-board-declutter: no banner), then the next sprint
|
|
10
10
|
* with its goal and summary, and each closed sprint with its goal, summary, done and carried counts and the read-once note. Right: the
|
|
11
11
|
* active sprint's card, its burndown from GET /api/stage-tools/burndown (when that route is not on this server yet, the card says so
|
|
@@ -18,25 +18,24 @@ import { recordCards } from './c3-ini-records.js';
|
|
|
18
18
|
|
|
19
19
|
const CAT = { todo: 'To do', doing: 'In progress', done: 'Done' };
|
|
20
20
|
const readTime = (s) => { const m = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})/.exec(String(s || '')); return m ? day(m[1]) + ', ' + m[2] : ''; };
|
|
21
|
-
const WHY = { none: 'Jira has no goal for this sprint.', broad: 'The goal in Jira is too broad to tell what this sprint delivers.', partial: 'The goal in Jira leaves out part of the sprint.' };
|
|
22
21
|
const timeOf = (s) => { const m = /T(\d{2}:\d{2})/.exec(String(s || '')); return m ? m[1] : ''; };
|
|
23
22
|
/* Quiet pages (c3-quiet.css) hide .ini-note, .ini-lhm .dim and a row's third chip. The goal's reason, the theme chips and the
|
|
24
23
|
* read-once note are the content of this tab (mockup v3), so they use their own classes. */
|
|
25
|
-
/* The goal
|
|
26
|
-
|
|
24
|
+
/* The goal is always written from the sprint's tickets: Jira's sprint goal is often empty, too broad or incomplete (the PM,
|
|
25
|
+
* 2026-10-14). Jira's own text shows underneath as a reference. */
|
|
26
|
+
const jiraRef = (j) => (j ? el('div', { class: 'dim ini-sp-note', 'data-inb': 'sprint-goal-jira' }, 'In Jira: “' + j + '”') : null);
|
|
27
|
+
function goalBlock(g, jiraGoal) {
|
|
27
28
|
if (!g) return el('div', { class: 'lh', 'data-inb': 'sprint-goal', 'data-inb-goal': 'missing' }, el('span', { class: 'ki' }, icon('target')),
|
|
28
|
-
el('div', { class: 'ini-sp-goal' }, lbl('SPRINT GOAL'), el('div', { class: 'dim ini-sp-note' }, 'No goal yet. The Jira status check writes one from the tickets at its next check.')));
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
el('div', { class: 'ini-sp-goal' }, lbl(derived ? 'SPRINT GOAL · written from the tickets' : 'SPRINT GOAL · from Jira'), el('b', null, g.text),
|
|
32
|
-
derived ? el('div', { class: 'dim ini-sp-note', 'data-inb': 'sprint-goal-why' }, (WHY[g.from] || '') + (g.jira ? ' In Jira: “' + g.jira + '”' : '')) : null));
|
|
29
|
+
el('div', { class: 'ini-sp-goal' }, lbl('SPRINT GOAL'), el('div', { class: 'dim ini-sp-note' }, 'No goal yet. The Jira status check writes one from the tickets at its next check.'), jiraRef(jiraGoal)));
|
|
30
|
+
return el('div', { class: 'lh', 'data-inb': 'sprint-goal', 'data-inb-goal': 'tickets' }, el('span', { class: 'ki' }, icon('target')),
|
|
31
|
+
el('div', { class: 'ini-sp-goal' }, lbl('SPRINT GOAL · from the tickets'), el('b', null, g.text), jiraRef(g.jira)));
|
|
33
32
|
}
|
|
34
33
|
const themeRow = (t, ctx) => nrow({ ic: t.blocked ? 'alert' : t.tickets && t.done === t.tickets ? 'check' : t.initiative ? 'bolt' : 'task', hot: !t.blocked && t.done < t.tickets && !!t.initiative,
|
|
35
34
|
title: t.title, attrs: { 'data-inb-theme': t.initiative || 'none' },
|
|
36
35
|
after: el('div', null, t.line ? el('div', { class: 'dim ini-sp-note' }, t.line) : null, el('div', { class: 'chips ini-sp-chips' }, ...[chip(plural(t.tickets, 'ticket')), t.done ? st('ok', 'check', t.done + ' done') : null, t.blocked ? st('warn', 'alert', t.blocked + ' blocked') : null,
|
|
37
36
|
t.people ? chip(icon('user', 'sm'), t.people) : null,
|
|
38
37
|
t.initiative ? lk({ onclick: () => ctx.openInitiative(t.initiative), 'data-inb-theme-ini': t.initiative }, icon('init', 'sm'), t.initiative) : el('span', { class: 'chip2 dim' }, 'No initiative')].filter(Boolean))) });
|
|
39
|
-
const summaryBody = (sp, ctx) => [goalBlock(sp.goal), sp.summary ? el('p', { class: 'ini-sp-sum' }, sp.summary) : null,
|
|
38
|
+
const summaryBody = (sp, ctx) => [goalBlock(sp.goal, sp.jiraGoal), sp.summary ? el('p', { class: 'ini-sp-sum' }, sp.summary) : null,
|
|
40
39
|
sp.themes && sp.themes.length ? [lbl('WHAT THE TEAM IS WORKING ON'), list(sp.themes.map((t) => themeRow(t, ctx)), { 'data-inb': 'sprint-themes' })] : null];
|
|
41
40
|
const countsOf = (sp) => { const c = sp.counts || {}; const f = (k) => (c[k] != null ? Number(c[k]) : (sp.issues || []).filter((i) => i.category === k).length); return { todo: f('todo'), doing: f('doing'), done: f('done') }; };
|
|
42
41
|
|
|
@@ -78,7 +77,7 @@ registerBoardTab('sprints', async (host, ctx) => {
|
|
|
78
77
|
n ? list(active.issues.map(issueRow)) : empty('No items in this sprint yet.', { 'data-inb': 'sprint-no-issues' }))
|
|
79
78
|
: empty(sprints.length ? 'No sprint is active.' : 'Jira has no sprints for this board.', { 'data-inb': sprints.length ? 'sprints-no-active' : 'sprints-empty' }),
|
|
80
79
|
...others.filter((s) => s.state !== 'closed').map((s) => sect({ ic: 'cal', title: s.name, count: (s.issues || []).length, right: tag(s.state || 'sprint'), attrs: { 'data-inb-sprint': s.name, 'data-inb-state': s.state } },
|
|
81
|
-
goalBlock(s.goal), s.summary ? el('p', { class: 'ini-sp-sum' }, s.summary) : null, itemsRow(s))),
|
|
80
|
+
goalBlock(s.goal, s.jiraGoal), s.summary ? el('p', { class: 'ini-sp-sum' }, s.summary) : null, itemsRow(s))),
|
|
82
81
|
...others.filter((s) => s.state === 'closed').map(closedSect),
|
|
83
82
|
];
|
|
84
83
|
const right = [];
|
|
@@ -91,7 +90,7 @@ registerBoardTab('sprints', async (host, ctx) => {
|
|
|
91
90
|
right.push(...await recordCards(pseudo, ['burndown']));
|
|
92
91
|
}
|
|
93
92
|
if (sprints.length) right.push(card({ ic: 'redo', title: 'How sprints are read', attrs: { 'data-inb': 'sprints-how' } }, kv([
|
|
94
|
-
['Goal', '
|
|
93
|
+
['Goal', 'Written from the sprint\'s tickets. Jira\'s goal is shown for reference'], ['Active sprint', 'Read and summarised at each Jira status check'],
|
|
95
94
|
['Next sprint', 'Read at each check, until it starts'], ['Closed sprints', 'Read and summarised once, then kept'], ['Jira', 'Read-only. PMOS changes nothing there']])));
|
|
96
95
|
if (dead) return () => {};
|
|
97
96
|
put(host, cols(left, right));
|
|
@@ -696,7 +696,7 @@ function startRun(inst, item, intent) {
|
|
|
696
696
|
}
|
|
697
697
|
|
|
698
698
|
|
|
699
|
-
/* ── the agent on a Jira or
|
|
699
|
+
/* ── the agent on a Jira, WBS or morning-brief item (initiative c3-work-agent, planning/prd/c3-work-agent-prd.md) ────────
|
|
700
700
|
* item.agent = { state: suggest|queued|working|ready|sending|sent|failed, suggest, instruction, draft: { summary, body }, sent, error, … }
|
|
701
701
|
* from lib/work-assigned.js. The suggestion is an option the PM picks; the day task does the work; nothing goes outward
|
|
702
702
|
* until the PM holds Post (POST /api/my-work/decide approve). Stop ends a task that has not delivered (POST /api/my-work/stop). */
|
|
@@ -704,7 +704,9 @@ const AGENT_BUSY = ['queued', 'working', 'sending'];
|
|
|
704
704
|
const AGENT_POLL_MS = 15000;
|
|
705
705
|
const hhmm = (iso) => { const d = new Date(iso); return Number.isNaN(d.getTime()) ? '' : String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0'); };
|
|
706
706
|
const minsAgo = (iso) => { const t = Date.parse(iso); if (!Number.isFinite(t)) return ''; const m = Math.max(0, Math.round((Date.now() - t) / 60000)); return m < 1 ? 'just now' : m < 60 ? m + ' min ago' : Math.floor(m / 60) + ' h ago'; };
|
|
707
|
-
|
|
707
|
+
// where a post goes: Jira for a ticket; for an item the morning brief suggests, the place it came from (amendment 10)
|
|
708
|
+
const PLACE = { mail: 'email', slack: 'Slack', jira: 'Jira', confluence: 'Confluence', github: 'GitHub', drive: 'Drive', figma: 'Figma', calendar: 'the calendar', wispr: 'Wispr' };
|
|
709
|
+
const whereOf = (item) => (item.origin === 'jira' ? 'Jira' : PLACE[item.place] || 'its source');
|
|
708
710
|
const AG = {
|
|
709
711
|
can: (t) => 'Agent can: ' + t, queued: 'Agent: queued', working: (ago) => 'Agent: working' + (ago ? ' · started ' + ago : ''), ready: 'Agent: draft ready',
|
|
710
712
|
sending: 'Agent: posting', sent: (w, at) => 'Posted to ' + w + (at ? ' ' + at : ''), failed: (asks) => (asks ? 'Agent: needs you' : 'Agent stopped'),
|
|
@@ -747,7 +749,7 @@ async function agentPress(inst, path, body, text) {
|
|
|
747
749
|
}
|
|
748
750
|
|
|
749
751
|
function agentCase(inst, item, L) {
|
|
750
|
-
const a = item.agent; const w = whereOf(item); const k = item.key || item.title;
|
|
752
|
+
const a = item.agent; const w = whereOf(item); const k = item.key || (item.origin === 'brief' ? w : item.title);
|
|
751
753
|
const o = (id, l, f, run, extra) => ({ id, l, rec: 0, fx: f, run, ...(extra || {}) });
|
|
752
754
|
const own = L.opts.slice(0, 2).map((x) => ({ ...x, rec: 0 }));
|
|
753
755
|
const ask = () => inst.asks.get(item.id) || a.instruction || a.suggest || '';
|
package/workspace/server.js
CHANGED
|
@@ -8474,11 +8474,8 @@ async function handleRequest(req, res) {
|
|
|
8474
8474
|
const x = brief ? [...(brief.priorities || []), ...(brief.worth_asking || [])].find((y) => y.id === String(body.item || '')) : null;
|
|
8475
8475
|
if (!x) return json(res, { ok: false, error: 'no item with that id in today\'s brief' }, 404);
|
|
8476
8476
|
if (x.owner !== 'pm') return json(res, { ok: false, error: 'the agent already does this job: approve or revise its draft' }, 409);
|
|
8477
|
-
|
|
8478
|
-
const
|
|
8479
|
-
instruction: `Draft the PM's ${verb} for this item: ${x.todo || x.how || x.ask || x.title}. Draft only. Send nothing: the PM approves it first.`,
|
|
8480
|
-
context: { from: x.who || null, where: x.where || null, about: x.about || null, ask: x.ask || null, how: x.how || null, why_now: x.why_now || null } };
|
|
8481
|
-
const r = myWorkBrief(spec, body.ws);
|
|
8477
|
+
// the same ask as Work's "Let the agent draft this" on this item (lib/work-assigned.js, c3-work-agent amendment 10)
|
|
8478
|
+
const r = myWorkBrief(require('./lib/work-assigned.js').briefDraftSpec(x, ''), body.ws);
|
|
8482
8479
|
if (!r.ok) return json(res, { ok: false, error: r.error }, r.status || 400);
|
|
8483
8480
|
return json(res, { ok: true, task: r.task });
|
|
8484
8481
|
}
|
|
@@ -37,7 +37,7 @@ Procedure:
|
|
|
37
37
|
4. Run `pmos jira sync <initiative> --from <the file> --query "<the JQL>"`. Then delete the file.
|
|
38
38
|
4a. Read the whole sprints, after every initiative is synced. Run `pmos jira query --sprints`. It prints one Jira search for every issue in the sprints the tickets above are in, whoever the issue is assigned to. Do this even when the task names one initiative. If it prints "no Jira reading has a sprint yet" or "every sprint is closed and summarised once", skip to step 4b. Otherwise run that search with the same fields as step 2, fetch every page, save the result in the scratch folder in the same shape as step 3 (for example `<scratch>/jira-sprints.json`), run `pmos jira sync --sprints --from <the file> --query "<the JQL>"`, and delete the file. Do not record people from this search (step 5 is for the initiatives' own issues).
|
|
39
39
|
4b. Write each sprint's goal and summary, after step 4a. Run `pmos jira summary`. Each line is a sprint that needs one: its name, its state, its number of issues and Jira's goal. A closed sprint that is already summarised is listed as kept: do not write it again. If it prints "no sprint needs a summary", skip this step. For each listed sprint, read its issues in the sprint search result of step 4a and write:
|
|
40
|
-
- `goal`: `{"
|
|
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
43
|
- For a closed sprint, also `closed`: `{"done", "total", "carried", "carriedTo", "points"}` (`points` as "31 of 38" when the tickets have points).
|