create-pmos 0.216.0 → 0.218.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/c3-sessions-panel-test.js +18 -0
- package/workspace/guide/console3-sessions.md +1 -1
- package/workspace/lib/engine/boards.js +5 -2
- package/workspace/lib/jira-status.js +10 -5
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/console3.css +3 -2
- 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.218.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:40:51Z"}
|
|
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:40:51Z"}
|
|
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:40:51Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -261,6 +261,24 @@ function startShell(name) {
|
|
|
261
261
|
}, cvSel, 10000));
|
|
262
262
|
ok('S2 the tool line lists each call with its target', await page.evaluate((s) => [...document.querySelectorAll(s + ' details[data-chat="tool"] li')].map((li) => li.querySelector('b').textContent + ' ' + ((li.querySelector('span') || {}).textContent || '')).join() === 'Bash git log', cvSel));
|
|
263
263
|
ok('S3 the status reads Your turn and the meta names the model', await page.evaluate(() => (document.querySelector('[data-cv="status"]') || {}).textContent === 'Your turn' && /claude-test/.test((document.querySelector('[data-cv="meta"]') || {}).textContent || '')));
|
|
264
|
+
// at Whole the list of sessions shows: each row's title sits above its subtitle, both at the row's left, and rows don't overlap
|
|
265
|
+
await page.click('[data-c3-size="whole"]');
|
|
266
|
+
await sleep(250);
|
|
267
|
+
const lay = await page.evaluate((s) => {
|
|
268
|
+
const rows = [...document.querySelectorAll('[data-c3-sess-list] [data-c3-sess-pick]')].map((b) => {
|
|
269
|
+
const r = b.getBoundingClientRect(), n = b.querySelector('.nm').getBoundingClientRect(), u = b.querySelector('.sub').getBoundingClientRect();
|
|
270
|
+
return { top: r.top, bottom: r.bottom, left: r.left, nl: n.left, nb: n.bottom, ul: u.left, ut: u.top, ub: u.bottom };
|
|
271
|
+
});
|
|
272
|
+
const f = document.querySelector(s + ' [data-cv="feed"]').getBoundingClientRect();
|
|
273
|
+
const m = document.querySelector(s + ' [data-chat="me"],' + s + ' [data-chat="bot"]');
|
|
274
|
+
const w = document.querySelector(s + ' [data-cv="feed"] > .wrap').getBoundingClientRect();
|
|
275
|
+
return { rows, gap: w.left - f.left, shown: !!m && m.getBoundingClientRect().height > 0 };
|
|
276
|
+
}, cvSel);
|
|
277
|
+
ok('S3b the list rows are laid out cleanly (title over subtitle, left aligned, no overlap)', lay.rows.length > 0 && lay.rows.every((r, i) =>
|
|
278
|
+
r.nb <= r.ut + 1 && r.ub <= r.bottom && Math.abs(r.nl - r.ul) < 1 && r.nl - r.left < 14 && (i === 0 || r.top >= lay.rows[i - 1].bottom - 1)), JSON.stringify(lay.rows));
|
|
279
|
+
ok('S3c at Whole the conversation starts at the left of its pane, next to the list', lay.shown && lay.gap < 16, String(lay.gap));
|
|
280
|
+
await page.click('[data-c3-size="half"]');
|
|
281
|
+
await sleep(150);
|
|
264
282
|
await page.fill('[data-cv="msg"]', 'echo c3-conv-$((5*5))');
|
|
265
283
|
await page.press('[data-cv="msg"]', 'Enter');
|
|
266
284
|
ok('S4 a sent message shows at once', await waitFor(page, () => [...document.querySelectorAll('[data-c3-conv] [data-chat="me"]')].some((n) => /c3-conv-/.test(n.textContent)), null, 3000));
|
|
@@ -71,7 +71,7 @@ A shell where Claude has not started says "No Claude in this shell yet". Press *
|
|
|
71
71
|
|
|
72
72
|
If PMOS cannot find the transcript of a Claude session, the view says so, and it does not show the transcript of a different session. Read the session in the terminal. PMOS looks for transcripts in `~/.claude`, in each `~/.claude-*` profile folder, and in the folder that `CLAUDE_CONFIG_DIR` names.
|
|
73
73
|
|
|
74
|
-
At **2/3** and **Whole**, the sheet also shows a list of your running sessions at the left. Press a session to open it.
|
|
74
|
+
At **2/3** and **Whole**, the sheet also shows a list of your running sessions at the left. Each row shows the session name and, below it, the agent and its state. Press a session to open it. The conversation starts next to the list.
|
|
75
75
|
|
|
76
76
|
### The scratchpad
|
|
77
77
|
|
|
@@ -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;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.218.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.218.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -1701,7 +1701,8 @@
|
|
|
1701
1701
|
.c3 .c3t-cv-panes .c3t-tw{flex:1 1 0;height:auto;min-height:0}
|
|
1702
1702
|
.c3 .c3t-cv-list{display:none;border-right:1px solid var(--border);overflow:auto;padding:0 var(--sp-2) 0 0;margin-right:var(--sp-3);gap:2px;align-content:start}
|
|
1703
1703
|
.c3 .c3t-cv-list .eyebrow{padding:var(--sp-2) 10px 4px;font:500 var(--fs-1) var(--mono);letter-spacing:.06em;text-transform:uppercase;color:var(--muted-foreground)}
|
|
1704
|
-
.c3 .c3t-cv-list button{display:grid;gap:2px;text-align:left;border:0;background:none;padding:8px 10px;border-radius:8px;cursor:pointer;color:var(--foreground);font:inherit;font-size:var(--fs-3);width:100%;min-width:0}
|
|
1704
|
+
.c3 .c3t-cv-list button{display:grid;gap:2px;justify-content:stretch;justify-items:start;align-content:start;text-align:left;border:0;background:none;padding:8px 10px;border-radius:8px;cursor:pointer;color:var(--foreground);font:inherit;font-size:var(--fs-3);line-height:1.3;width:100%;min-width:0;height:auto;min-height:0}
|
|
1705
|
+
.c3 .c3t-cv-list button>span{max-width:100%}
|
|
1705
1706
|
.c3 .c3t-cv-list button .nm,.c3 .c3t-cv-list button .sub{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1706
1707
|
.c3 .c3t-cv-list button .sub{font:var(--fs-1) var(--mono);color:var(--muted-foreground)}
|
|
1707
1708
|
.c3 .c3t-cv-list button[aria-current="true"]{background:color-mix(in oklab,var(--acc) 12%,transparent)}
|
|
@@ -1712,7 +1713,7 @@
|
|
|
1712
1713
|
.c3 .c3t-cv-meta .tg[data-tone="muted"]{--tc:var(--muted-foreground)}
|
|
1713
1714
|
.c3 .c3t-cv-meta .tg[data-tone="warning"]{--tc:var(--warning)}
|
|
1714
1715
|
.c3 .c3t-cv-feed{flex:1 1 0;min-height:0;overflow:auto;padding:var(--sp-3) 2px}
|
|
1715
|
-
.c3 .c3t-cv-feed>.wrap{max-width:
|
|
1716
|
+
.c3 .c3t-cv-feed>.wrap{max-width:820px;margin:0;display:flex;flex-direction:column;gap:var(--sp-2)}
|
|
1716
1717
|
.c3 .c3t-cv .m{padding:8px 14px;border-radius:8px;max-width:85%;overflow-wrap:anywhere;font-size:var(--fs-3);line-height:1.5}
|
|
1717
1718
|
.c3 .c3t-cv .m.me{align-self:flex-end;background:var(--acc);color:var(--on-accent);white-space:pre-wrap}
|
|
1718
1719
|
.c3 .c3t-cv .m.bot{align-self:flex-start;background:var(--muted)}
|
|
@@ -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}`.
|