create-pmos 0.183.0 → 0.184.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.183.0",
3
+ "version": "0.184.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": {
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@ab3a131
1
+ kernel-source: pmos@14635e7
2
2
  assembled: 2026-10-13
3
3
  mode: vendor
@@ -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-13T03:53:55Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-13T04:43:18Z"}
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-13T03:53:55Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-13T04:43:18Z"}
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-13T03:53:55Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-13T04:43:18Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -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-12
5
+ timestamp: 2026-10-13
6
6
  ---
7
7
 
8
8
  # Jira: tickets drafted from the PRD, and their status
@@ -110,6 +110,15 @@ pmos jira query [<initiative>]
110
110
  pmos jira sync <initiative> --from <file|-> [--query "<jql>"] [--dry-run]
111
111
  ```
112
112
 
113
+ ## The board's Sprints tab shows the whole sprint
114
+
115
+ After it reads the initiatives, the check reads every issue in the sprints their tickets are in. It reads every issue, not only the issues of an initiative and not only the issues assigned to you. The board's **Sprints** tab and the sprint burndown show all of them. An issue that is in no initiative of the board shows **No initiative**. The check writes `<state>/tracker/jira/_sprints.md` with:
116
+
117
+ ```
118
+ pmos jira query --sprints
119
+ pmos jira sync --sprints --from <file|-> [--query "<jql>"]
120
+ ```
121
+
113
122
  ## Link an initiative to its Jira epics
114
123
 
115
124
  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.
@@ -152,6 +161,8 @@ The morning brief, the day check and requirement gathering use it to tie a Jira
152
161
  - `pmos jira record <initiative> [ID=KEY …] [--epic KEY] [--updated ID,ID]`
153
162
  - `pmos jira query [<initiative>]`
154
163
  - `pmos jira sync <initiative> --from <file|-> [--query "<jql>"] [--dry-run]`
164
+ - `pmos jira query --sprints`
165
+ - `pmos jira sync --sprints --from <file|-> [--query "<jql>"] [--dry-run]`
155
166
  - `pmos jira which <KEY>`
156
167
 
157
168
  Every command takes `--json` and `--root <dir>`.
@@ -15,7 +15,7 @@
15
15
  * filterRows(rows, query) → { total, offset, limit, rows, facets }
16
16
  * health(rows) → 'ok' | 'warn' | 'bad'
17
17
  * counts(rows, workflow) → { total, open, byCat, byStage, byStatus, risk, late }
18
- * sprintsOf(snapshots, onBoard) → { source, readAt, sprints }
18
+ * sprintsOf(snapshots, onBoard, whole?) → { source, readAt, sprints }
19
19
  * INI_TEMPLATES · isBuildStage · templateBuilds · boardSettings(board) the per-initiative workflows (console3-initiatives-parity)
20
20
  * workflowOf(board, id, folded) → the stages an initiative runs: its own (ini-workflow), else the board's
21
21
  * applyIniWorkflow(board, wfNow, stage, template, stages, moveTo) → { ok, template, workflow, moved } | { ok:false, status, error }
@@ -523,8 +523,11 @@ function counts(rows, workflow) {
523
523
  * The Jira sprints of a board from the cached status snapshots (lib/jira-status.js readSnapshot), read-only.
524
524
  * snapshots: [{ initiative, fetchedAt, tickets: Map|[] of readings }]; onBoard(initiativeId) → boolean.
525
525
  * A ticket with no sprint (the backlog) is in no sprint. source is 'none' when no snapshot of the board was read.
526
+ * whole: the whole-sprint read (lib/jira-status.js readSprints), { fetchedAt, tickets }. A sprint the board's tickets are in then
527
+ * lists EVERY issue Jira holds in it, whoever it is assigned to; an issue no initiative of the board holds has initiative null
528
+ * (jira-sprint-scope). Without it, a sprint lists only the board's own tickets.
526
529
  */
527
- function sprintsOf(snapshots, onBoard = () => true) {
530
+ function sprintsOf(snapshots, onBoard = () => true, whole = null) {
528
531
  const byName = new Map();
529
532
  let readAt = null;
530
533
  let any = false;
@@ -545,6 +548,28 @@ function sprintsOf(snapshots, onBoard = () => true) {
545
548
  sp.counts[category]++;
546
549
  }
547
550
  }
551
+ const wt = whole && whole.tickets ? (whole.tickets instanceof Map ? [...whole.tickets.values()] : Array.isArray(whole.tickets) ? whole.tickets : []) : [];
552
+ let widened = false;
553
+ for (const sp of byName.values()) {
554
+ const all = wt.filter((t) => t && t.key && t.sprint && String(t.sprint.name) === sp.name);
555
+ if (!all.length) continue;
556
+ widened = true;
557
+ const own = new Map(sp.issues.map((i) => [i.key, i]));
558
+ const keys = new Set(all.map((t) => t.key));
559
+ if (!sp.start && typeof all[0].sprint.start === 'string' && all[0].sprint.start) sp.start = all[0].sprint.start;
560
+ if (!sp.end && typeof all[0].sprint.end === 'string' && all[0].sprint.end) sp.end = all[0].sprint.end;
561
+ sp.issues = [
562
+ ...all.map((t) => {
563
+ const category = ['todo', 'doing', 'done'].includes(t.category) ? t.category : 'todo';
564
+ const o = own.get(t.key);
565
+ return { key: t.key, summary: t.summary || '', status: t.status || '', category, initiative: o ? o.initiative : null, flagged: !!t.flagged, reason: t.flagged && t.reason ? String(t.reason) : null };
566
+ }),
567
+ ...sp.issues.filter((i) => !keys.has(i.key)), // a board ticket the sprint read has not seen yet keeps its own reading
568
+ ];
569
+ sp.counts = { todo: 0, doing: 0, done: 0 };
570
+ for (const i of sp.issues) sp.counts[i.category]++;
571
+ }
572
+ if (widened && whole.fetchedAt && (!readAt || whole.fetchedAt < readAt)) readAt = whole.fetchedAt; // the oldest read is the honest age
548
573
  const order = { active: 0, future: 1, closed: 2 };
549
574
  const sprints = [...byName.values()].sort((a, b) => ((order[a.state] ?? 3) - (order[b.state] ?? 3)) || a.name.localeCompare(b.name));
550
575
  for (const sp of sprints) sp.issues.sort((a, b) => a.key.localeCompare(b.key, undefined, { numeric: true }));
@@ -10,6 +10,8 @@
10
10
  * pmos jira record <initiative> [ID=KEY …] [--epic KEY] [--updated ID,ID] the keys the agent created
11
11
  * pmos jira query [<initiative>] the Jira search for the tickets an initiative pushed
12
12
  * pmos jira sync <initiative> --from <file|-> [--query "<jql>"] [--dry-run] a saved search → the status snapshot
13
+ * pmos jira query --sprints the Jira search for every issue in the sprints the tickets are in
14
+ * pmos jira sync --sprints --from <file|-> [--query "<jql>"] a saved whole-sprint search → the Sprints tab (jira-sprint-scope)
13
15
  * pmos jira which <KEY> the initiative a Jira key belongs to
14
16
  * every command: [--json] [--root <dir>]
15
17
  *
@@ -36,11 +38,13 @@ const USAGE = [
36
38
  ' pmos jira record <initiative> [ID=KEY …] [--epic KEY] [--updated ID,ID] the keys created in Jira',
37
39
  ' pmos jira query [<initiative>] the Jira search for the pushed tickets',
38
40
  ' pmos jira sync <initiative> --from <file|-> [--query "<jql>"] [--dry-run] save the status read from Jira',
41
+ ' pmos jira query --sprints the Jira search for every issue in those tickets\' sprints',
42
+ ' pmos jira sync --sprints --from <file|-> [--query "<jql>"] save every issue of the sprints, for the Sprints tab',
39
43
  ' pmos jira which <KEY> the initiative a Jira key belongs to',
40
44
  ' every command: [--json] [--root <dir>]',
41
45
  'This command never calls Jira. Your agent reads and writes Jira with your own connection.',
42
46
  ];
43
- const BOOL = new Set(['json', 'dry-run', 'help']);
47
+ const BOOL = new Set(['json', 'dry-run', 'help', 'sprints']);
44
48
  const VALUE = new Set(['from', 'query', 'root', 'epic', 'updated']);
45
49
  const FLAG_TEXT = { 'prd-changed': 'PRD changed', removed: 'Removed from PRD', blocked: 'Blocked', incomplete: 'Missing text', pushing: 'Selected, waiting for the push', edited: 'Edited, not in Jira yet' };
46
50
 
@@ -92,6 +96,23 @@ async function run(argv, { say = (s) => console.log(s), cwd = process.cwd(), isT
92
96
  const known = () => planItems.INIT_RE.test(initiative || '') && !!jiraTickets.recordFm(root, initiative);
93
97
 
94
98
  try {
99
+ if (flags.sprints && verb !== 'query' && verb !== 'sync') return fail('--sprints goes with query or sync', 2);
100
+ if (verb === 'query' && flags.sprints) {
101
+ const q = jiraStatus.sprintQuery(root);
102
+ return out(q, [q.linked ? q.query : 'no Jira reading has a sprint yet — run the initiatives\' searches first']);
103
+ }
104
+ if (verb === 'sync' && flags.sprints) {
105
+ if (!flags.from) return need('--sprints --from <file|->');
106
+ let raw;
107
+ try { raw = JSON.parse(readInput(flags.from, cwd, stdin)); }
108
+ catch (e) { return fail(`could not read ${flags.from} as JSON — ${String((e && e.message) || e).slice(0, 160)}`); }
109
+ const r = jiraStatus.syncSprints(root, raw, { query: flags.query || undefined, dryRun: !!flags['dry-run'], now });
110
+ if (!r.ok) return fail(r.error);
111
+ const lines = [`sprints · read ${r.read} ${r.read === 1 ? 'issue' : 'issues'}${r.ignored ? ` · ${r.ignored} in no sprint, ignored` : ''}${r.skipped ? ` · ${r.skipped} skipped (no key or summary)` : ''}${r.dryRun ? ' · dry run, nothing written' : ''}`];
112
+ for (const [name, n] of Object.entries(r.sprints)) lines.push(` ${name} · ${n} ${n === 1 ? 'issue' : 'issues'}`);
113
+ if (!r.dryRun) lines.push(`snapshot ${r.snapshot}`);
114
+ return out(r, lines);
115
+ }
95
116
  if (verb === 'query') {
96
117
  if (initiative) {
97
118
  const q = jiraStatus.queryFor(root, initiative);
@@ -13,6 +13,10 @@
13
13
  * sync(root, initiative, raw, opts) → the snapshot, written to <state>/tracker/jira/<initiative>.md
14
14
  * readSnapshot(root, initiative) → { fetchedAt, query, tickets: Map<key, reading> } | null
15
15
  * which(root, key) → the initiative and ticket a key belongs to
16
+ * sprintQuery(root) → { linked, sprints, query } the JQL for EVERY issue in the sprints the initiatives' tickets
17
+ * are in, whoever holds them (jira-sprint-scope); linked false when no reading has a sprint
18
+ * syncSprints(root, raw, opts) → the whole-sprint snapshot, written to <state>/tracker/jira/_sprints.md
19
+ * readSprints(root) → { fetchedAt, query, tickets: Map<key, reading> } | null
16
20
  *
17
21
  * A READING is { key, summary, status, category: todo|doing|done, sprint: { name, state: active|future|closed, id?, start?, end?,
18
22
  * completed? } | null, subtasks: { done, total }, fixVersion: [names], points: number | null, resolved: YYYY-MM-DD | null,
@@ -206,12 +210,13 @@ function snapshotRel(root, initiative) { return jiraTickets.relOf(root, snapshot
206
210
  const cell = (s) => oneLine(s).replace(/\\/g, '\\\\').replace(/\|/g, '\\|') || '—';
207
211
  const SPRINT_WORD = { active: 'Active sprint', future: 'Next sprint', closed: 'Closed sprint' };
208
212
  function sprintLabel(s) { return s ? `${SPRINT_WORD[s.state] || 'Sprint'}: ${s.name}` : 'Backlog'; }
209
- function renderSnapshot({ initiative, fetchedAt, query, readings, ids }) {
213
+ function renderSnapshot({ initiative, fetchedAt, query, readings, ids, title }) {
214
+ const name = title || initiative;
210
215
  const rows = readings.map((r) => `| ${cell(ids.get(r.key) || '')} | ${cell(r.key)} | ${cell(r.summary)} | ${cell(r.status)} | ${cell(r.category === 'done' ? 'Done' : sprintLabel(r.sprint))} | ${r.subtasks.total ? `${r.subtasks.done} of ${r.subtasks.total}` : '—'} | ${cell(r.flagged ? r.reason : '')} |${r.stale ? ' not in the last read |' : ''}`);
211
216
  return [
212
- '---', 'type: Reference', `title: "Jira status: ${initiative}"`, `timestamp: ${fetchedAt}`, `initiative: ${initiative}`, '---', '',
213
- `# Jira status: ${initiative}`, '',
214
- `Read from Jira at ${fetchedAt} by the Jira status check, read-only. The Jira tickets tab of the initiative shows it. Written by \`pmos jira sync\`; do not edit by hand.`, '',
217
+ '---', 'type: Reference', `title: "Jira status: ${name}"`, `timestamp: ${fetchedAt}`, ...(title ? [] : [`initiative: ${initiative}`]), '---', '',
218
+ `# Jira status: ${name}`, '',
219
+ `Read from Jira at ${fetchedAt} by the Jira status check, read-only. ${title ? 'The Sprints tab and the sprint burndown of a board show it.' : 'The Jira tickets tab of the initiative shows it.'} Written by \`pmos jira sync\`; do not edit by hand.`, '',
215
220
  query ? `Search: \`${query.replace(/`/g, "'")}\`` : 'Search: not recorded', '',
216
221
  '| Ticket | Key | Summary | Status | Sprint | Subtasks | Blocked |', '|---|---|---|---|---|---|---|', ...rows, '',
217
222
  '```json', JSON.stringify({ fetched_at: fetchedAt, query: query || null, tickets: readings }, null, 0), '```', '',
@@ -275,6 +280,48 @@ function sync(root, initiative, raw, { query, dryRun = false, now } = {}) {
275
280
  return { ...result, fetchedAt };
276
281
  }
277
282
 
283
+ // ── the whole sprint (jira-sprint-scope) ─────────────────────────────────────────────────────────────
284
+ // An initiative's read holds only its own tickets, so a sprint drawn from those reads lists only the tickets PMOS knows,
285
+ // not the sprint. This second read asks Jira for every issue in the sprints those tickets are in, whoever holds them.
286
+ const SPRINTS_FILE = '_sprints';
287
+ const jqlName = (n) => `"${String(n).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
288
+ function sprintQuery(root) {
289
+ const seen = new Map();
290
+ for (const q of linkedInitiatives(root).filter((x) => x.linked)) {
291
+ const snap = readSnapshot(root, q.initiative);
292
+ if (!snap) continue;
293
+ for (const t of snap.tickets.values()) {
294
+ const sp = t && t.sprint;
295
+ if (!sp || !sp.name) continue;
296
+ const k = sp.id || sp.name;
297
+ if (!seen.has(k)) seen.set(k, { name: sp.name, state: sp.state || null, ...(sp.id ? { id: sp.id } : {}) });
298
+ }
299
+ }
300
+ const sprints = [...seen.values()].sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true }));
301
+ if (!sprints.length) return { ok: true, linked: false, sprints, query: null };
302
+ return { ok: true, linked: true, sprints, query: `sprint in (${sprints.map((s) => s.id || jqlName(s.name)).join(', ')}) ORDER BY key` };
303
+ }
304
+ /** Save a whole-sprint read. The read replaces the last one whole: an issue moved out of a sprint is gone from it. */
305
+ function syncSprints(root, raw, { query, dryRun = false, now } = {}) {
306
+ const n = normalize(raw);
307
+ if (n.error) return { ok: false, error: n.error };
308
+ const kept = n.issues.filter((i) => i.sprint && i.sprint.name);
309
+ const fetchedAt = nowIso(now);
310
+ const prev = readSprints(root);
311
+ const bySprint = {};
312
+ for (const i of kept) bySprint[i.sprint.name] = (bySprint[i.sprint.name] || 0) + 1;
313
+ const result = { ok: true, read: kept.length, ignored: n.issues.length - kept.length, skipped: n.skipped, sprints: bySprint,
314
+ was: prev ? prev.tickets.size : null, snapshot: snapshotRel(root, SPRINTS_FILE), dryRun: !!dryRun };
315
+ if (dryRun) return result;
316
+ const file = snapshotPath(root, SPRINTS_FILE);
317
+ fs.mkdirSync(path.dirname(file), { recursive: true });
318
+ const tmp = `${file}.${process.pid}.tmp`;
319
+ fs.writeFileSync(tmp, renderSnapshot({ initiative: SPRINTS_FILE, title: 'every issue in the sprints', fetchedAt, query: query || (prev && prev.query) || null, readings: kept, ids: new Map() }));
320
+ fs.renameSync(tmp, file);
321
+ return { ...result, fetchedAt };
322
+ }
323
+ function readSprints(root) { return readSnapshot(root, SPRINTS_FILE); }
324
+
278
325
  // ── J13 · which initiative ─────────────────────────────────────────────────────────────────────────
279
326
  function which(root, key) {
280
327
  const k = String(key || '').trim().toUpperCase();
@@ -288,4 +335,4 @@ function which(root, key) {
288
335
  return { ok: true, key: k, initiatives: hits };
289
336
  }
290
337
 
291
- module.exports = { queryFor, linkedInitiatives, normalize, sync, readSnapshot, which, sprintLabel, snapshotPath, snapshotRel };
338
+ module.exports = { queryFor, linkedInitiatives, normalize, sync, readSnapshot, which, sprintLabel, snapshotPath, snapshotRel, sprintQuery, syncSprints, readSprints, SPRINTS_FILE };
@@ -489,7 +489,9 @@ const READS = {
489
489
  if (s) snaps.push({ initiative: q.initiative, fetchedAt: s.fetchedAt, tickets: s.tickets });
490
490
  }
491
491
  } catch { /* no snapshot yet */ }
492
- ctx.json(res, { ok: true, ws: r.w.id, board: r.board.id, ...B.sprintsOf(snaps, (id) => ids.has(id)) });
492
+ let whole = null;
493
+ try { whole = jiraStatus.readSprints(r.w.root); } catch { whole = null; }
494
+ ctx.json(res, { ok: true, ws: r.w.id, board: r.board.id, ...B.sprintsOf(snaps, (id) => ids.has(id), whole) });
493
495
  },
494
496
  };
495
497
 
@@ -866,7 +868,9 @@ function meetingBoards(ctx) {
866
868
  if (s) snaps.push({ initiative: q.initiative, fetchedAt: s.fetchedAt, tickets: s.tickets });
867
869
  }
868
870
  } catch { /* no snapshot yet */ }
869
- sprints = B.sprintsOf(snaps, (id) => ids.has(id));
871
+ let whole = null;
872
+ try { whole = jiraStatus.readSprints(w.root); } catch { whole = null; }
873
+ sprints = B.sprintsOf(snaps, (id) => ids.has(id), whole);
870
874
  }
871
875
  return { ...head, health: B.health(rows), counts: { total: c.total, open: c.open, risk: c.risk, late: c.late },
872
876
  rows: rows.filter((r) => r.status === 'late' || r.status === 'at risk').map((r) => ({ id: r.id, title: r.title, status: r.status, end: r.end || null, owner: r.owner || null })),
@@ -122,13 +122,13 @@ function readers(root, runtimeDir) {
122
122
  try { out.own = ownRows().map((o) => ({ id: o.id, title: o.title, due: o.due, done: !!o.done, priority: o.priority || null })); } catch { /* likewise */ }
123
123
  return out;
124
124
  },
125
- /* each initiative's Jira epic progress, from the snapshots in the tracker folder (one file per initiative; the -tickets.md drafts are not snapshots) */
125
+ /* each initiative's Jira epic progress, from the snapshots in the tracker folder (one file per initiative; the -tickets.md drafts and the _sprints.md whole-sprint read are not) */
126
126
  readJira() {
127
127
  const fs = require('fs');
128
128
  const status = require('./jira-status.js');
129
129
  const out = new Map();
130
130
  let names = [];
131
- try { names = fs.readdirSync(require('./jira-tickets.js').jiraDir(root)).filter((n) => n.endsWith('.md') && !n.endsWith('-tickets.md')); } catch { return out; }
131
+ try { names = fs.readdirSync(require('./jira-tickets.js').jiraDir(root)).filter((n) => n.endsWith('.md') && !n.endsWith('-tickets.md') && !n.startsWith('_')); } catch { return out; }
132
132
  for (const n of names) {
133
133
  const id = n.slice(0, -3);
134
134
  try { const p = require('./briefing-real.js').jiraProgress(status.readSnapshot(root, id)); if (p) out.set(id, p); } catch { /* one unreadable snapshot is left out */ }
@@ -164,6 +164,21 @@ function burndownFor(ctx, w, ids, board, sprint) {
164
164
  if (snap.readAt && (!readAt || snap.readAt < readAt)) readAt = snap.readAt; // the oldest read is the honest age
165
165
  }
166
166
  if (!tickets.length) return { ok: true, available: false, reason: 'no Jira read for these initiatives yet' };
167
+ // jira-sprint-scope: a sprint burns down every issue in it, not only the initiatives' own tickets.
168
+ let whole = null;
169
+ try { whole = require('./jira-status.js').readSprints(w.root); } catch { whole = null; }
170
+ if (whole && whole.tickets.size) {
171
+ const names = new Set(tickets.filter((t) => t.sprint && t.sprint.name).map((t) => t.sprint.name));
172
+ const own = new Map(tickets.map((t) => [t.key, t]));
173
+ const add = [...whole.tickets.values()].filter((t) => t.sprint && names.has(t.sprint.name));
174
+ if (add.length) {
175
+ const keys = new Set(add.map((t) => t.key));
176
+ const merged = [...add.map((t) => ({ ...t, initiative: own.has(t.key) ? own.get(t.key).initiative : null })), ...tickets.filter((t) => !keys.has(t.key))];
177
+ tickets.length = 0;
178
+ tickets.push(...merged);
179
+ if (whole.fetchedAt && (!readAt || whole.fetchedAt < readAt)) readAt = whole.fetchedAt;
180
+ }
181
+ }
167
182
  return { ok: true, ...S.burndown(tickets, { sprint: sprint || null }), readAt };
168
183
  }
169
184
 
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.183.0",
3
+ "version": "0.184.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.183.0",
9
+ "version": "0.184.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@xterm/addon-fit": "^0.11.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.183.0",
3
+ "version": "0.184.0",
4
4
  "private": true,
5
5
  "description": "PMOS Terminal Workspace — local companion hosting real PTYs beside the library and gates (D62)",
6
6
  "license": "MIT",
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: jira-sync
3
3
  label: "Jira status check"
4
- description: "Read, read-only, the status, sprint and subtasks of the Jira tickets each initiative pushed, or of its Jira epics and the issues under them, and save them in PMOS for the initiative's Jira tickets tab. It writes nothing to Jira."
4
+ description: "Read, read-only, the status, sprint and subtasks of the Jira tickets each initiative pushed, or of its Jira epics and the issues under them, and every issue in the sprints those tickets are in, and save them in PMOS for the initiative's Jira tickets tab and the board's Sprints tab. It writes nothing to Jira."
5
5
  run: agent
6
6
  agent: claude
7
7
  output:
@@ -35,10 +35,11 @@ Procedure:
35
35
  - `url`: the issue's browse link, `https://<site>.atlassian.net/browse/<KEY>`.
36
36
  The shape Jira returned (`{"issues": [{"key", "self", "fields": {...}}]}`) is also accepted.
37
37
  4. Run `pmos jira sync <initiative> --from <the file> --query "<the JQL>"`. Then delete the file.
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", skip this step. 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).
38
39
  5. Record the people in People. For each assignee and reporter other than the PM, write one sighting per issue (People adds each new person at once): {"source": "jira", "handle": {"kind": "jira", "value": "<their Jira display name>"}, "name": "<display name>", "interaction": {"date": "<the issue's last update, YYYY-MM-DD>", "kind": "Jira", "text": "<Assignee|Reporter> of <KEY>: <summary, at most 150 characters>", "pointer": "jira · <KEY> · <url>", "initiative": "<the initiative id>"}}. Put all of them in one file {"sightings": [...]} in the scratch folder, run `pmos people record --file <that file>` once, and delete the file. An issue with no assignee gives no assignee sighting.
39
40
  6. Finish with the three sections of the output contract, one short line each:
40
- - Checked: the initiative, the number of tickets read, and each change (status, sprint, blocked). If nothing changed, say "no change". Add how many people you recorded.
41
- - Not reached: each initiative whose search failed, with the reason.
41
+ - Checked: the initiative, the number of tickets read, and each change (status, sprint, blocked). If nothing changed, say "no change". Add how many people you recorded, and how many issues the sprint read saved per sprint.
42
+ - Not reached: each initiative whose search failed, with the reason, and the sprint read if it failed.
42
43
  - Needs you: tickets newly blocked, tickets the search did not return, and any refusal.
43
44
 
44
45
  Output contract — produce EXACTLY these sections, the same shape every run: