create-pmos 0.495.0 → 0.496.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 +38 -14
- package/payload/AGENTS.md +2 -0
- package/payload/KIT-VERSION +2 -2
- 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/payload/state/log.md +1 -1
- package/workspace/adler-moments-test.js +3 -1
- package/workspace/board-programs-test.js +6 -6
- package/workspace/board-shared-ui-test.js +2 -2
- package/workspace/briefing-premium-adddoc-test.js +1 -48
- package/workspace/briefing-premium-board-test.js +18 -19
- package/workspace/changelog.json +37 -0
- package/workspace/guide/adler.md +1 -1
- package/workspace/guide/briefing-premium.md +1 -0
- package/workspace/guide/claude-proposes.md +6 -2
- package/workspace/guide/data-freshness.md +2 -0
- package/workspace/guide/home.md +1 -1
- package/workspace/guide/published-board.md +1 -1
- package/workspace/guide/workflows.md +4 -3
- package/workspace/lib/board-premium.js +17 -2
- package/workspace/lib/initiative-chat.js +4 -1
- package/workspace/lib/proposals.js +18 -4
- package/workspace/lib/routes-board-premium.js +2 -2
- package/workspace/lib/routes-proposals.js +14 -2
- package/workspace/lib/split-merge-suggest.js +37 -3
- package/workspace/lib/split-merge.js +10 -4
- package/workspace/lib/task-cli.js +5 -1
- package/workspace/lib/workflow-cli.js +70 -15
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/premium-board-actions-test.js +7 -9
- package/workspace/premium-core-doc-issue-test.js +9 -38
- package/workspace/public/pm/cozy-board.css +17 -5
- package/workspace/public/pm/home-app.js +12 -4
- package/workspace/public/pm/home-today.js +11 -4
- package/workspace/public/pm/load-shapes.css +57 -0
- package/workspace/public/pm/load-shapes.js +57 -0
- package/workspace/public/pm/premium-board.js +82 -57
- package/workspace/public/pm/readonly.js +3 -3
- package/workspace/public/pm/split-merge.js +3 -2
- package/workspace/public/premium-board.html +7 -10
- package/workspace/public/premium.html +1 -0
- package/workspace/split-merge-ui-test.js +1 -1
- package/workspace/test-lists/page-open-speed.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.496.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": {
|
|
@@ -32,6 +32,43 @@
|
|
|
32
32
|
"audit"
|
|
33
33
|
],
|
|
34
34
|
"pmosChanges": [
|
|
35
|
+
{
|
|
36
|
+
"v": "0.496.0",
|
|
37
|
+
"at": "2026-11-03T00:20:24.723Z",
|
|
38
|
+
"f": 3,
|
|
39
|
+
"prs": [
|
|
40
|
+
{
|
|
41
|
+
"n": 1172,
|
|
42
|
+
"t": "Initiative chats find the initiative's workflow through pmos, never a lane",
|
|
43
|
+
"s": "Initiative chats find the initiative's workflow through pmos, never a lane",
|
|
44
|
+
"k": "fix"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"n": 1170,
|
|
48
|
+
"t": "Keep a rejected ✦ suggestion rejected while its facts hold",
|
|
49
|
+
"s": "Keep a rejected ✦ suggestion rejected while its facts hold",
|
|
50
|
+
"k": "fix"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"n": 1171,
|
|
54
|
+
"t": "Tasks keep their mapped stage after a workflow switch on Home and Board",
|
|
55
|
+
"s": "Tasks keep their mapped stage after a workflow switch on Home and Board",
|
|
56
|
+
"k": "fix"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"n": 1173,
|
|
60
|
+
"t": "Calmer Board top bar (batch 24 item 7, design B)",
|
|
61
|
+
"s": "Calmer Board top bar (batch 24 item 7, design B)",
|
|
62
|
+
"k": "new"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"n": 1165,
|
|
66
|
+
"t": "Pages show something at once while they read",
|
|
67
|
+
"s": "Pages show something at once while they read",
|
|
68
|
+
"k": "new"
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
},
|
|
35
72
|
{
|
|
36
73
|
"v": "0.495.0",
|
|
37
74
|
"at": "2026-11-02T23:26:48.050Z",
|
|
@@ -354,19 +391,6 @@
|
|
|
354
391
|
"k": "fix"
|
|
355
392
|
}
|
|
356
393
|
]
|
|
357
|
-
},
|
|
358
|
-
{
|
|
359
|
-
"v": "0.476.0",
|
|
360
|
-
"at": "2026-11-01T06:51:29.850Z",
|
|
361
|
-
"f": 3,
|
|
362
|
-
"prs": [
|
|
363
|
-
{
|
|
364
|
-
"n": 1132,
|
|
365
|
-
"t": "Grill and Forecast screens: Claude's checks, grill findings, forecast ✦ line, Home line",
|
|
366
|
-
"s": "Grill and Forecast screens: Claude's checks, grill findings, forecast ✦ line, Home line",
|
|
367
|
-
"k": "new"
|
|
368
|
-
}
|
|
369
|
-
]
|
|
370
394
|
}
|
|
371
395
|
]
|
|
372
396
|
}
|
package/payload/AGENTS.md
CHANGED
|
@@ -58,6 +58,8 @@ State files are **append-only**: never rewrite or delete an existing line or ini
|
|
|
58
58
|
|
|
59
59
|
Every initiative carries a `type` — the lane picks what the gate requires **before** work; the kernel (anchor — a KR **or** `health:<id>` from `planning/okrs/health-budgets.md` (D59) — eval-before-work in lane-appropriate form, human Acceptance, append-only state) holds in every lane. Unknown/missing type = `feature`, the strictest lane. The enum is closed — never invent a lane; a work shape with no lane escalates to the PM.
|
|
60
60
|
|
|
61
|
+
**A lane is not a workflow (D99).** A lane sets what the gate requires before work. A workflow is the list of stages an initiative goes through on the PM's page (Triage, Discover, Plan…), and the PM picks it per initiative. Read workflows only through pmos: `pmos workflow of <initiative>` gives the initiative's workflow and its stages, `pmos workflow show <id>` gives one workflow's stages, and `pmos workflow list` names every workflow the PM can pick. The workflow library is not in this repository's files (Settings › Workflows saves it in the PM's home workspace), so never search the files for a workflow. When pmos cannot find a workflow, tell the PM so. Never use a lane in place of a workflow.
|
|
62
|
+
|
|
61
63
|
| type | spec artifact the gate requires | eval form |
|
|
62
64
|
|---|---|---|
|
|
63
65
|
| `feature` (default) | PRD + eval rubric before build | rubric + evaluator skill (unchanged) |
|
package/payload/KIT-VERSION
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
kernel-source: pmos@
|
|
2
|
-
assembled: 2026-11-
|
|
1
|
+
kernel-source: pmos@45dcbe29
|
|
2
|
+
assembled: 2026-11-03
|
|
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-11-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-11-03T00:20:21Z"}
|
|
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-11-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-11-03T00:20:21Z"}
|
|
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-11-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-11-03T00:20:21Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
package/payload/state/log.md
CHANGED
|
@@ -47,7 +47,9 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
47
47
|
browser = await chromium.launch({ executablePath: exe });
|
|
48
48
|
if (SHOTS) fs.mkdirSync(SHOTS, { recursive: true });
|
|
49
49
|
const shot = async (p, name) => { if (SHOTS) { await sleep(500); await p.screenshot({ path: path.join(SHOTS, name + '.png') }); } };
|
|
50
|
-
|
|
50
|
+
// F1 is the case where the failed source leads Your day, so no stuck call may lead it: the fixture's two blocked rows are left out here
|
|
51
|
+
// (with them, the honest card leads on "2 pieces of work are stuck" and Jira is one card under it, home-day.js).
|
|
52
|
+
const BRIEF = (() => { const b = BRP.build(BRF.facts()); if (b.list && Array.isArray(b.list.rows)) b.list.rows = b.list.rows.filter((r) => r.kind !== 'blocked'); return b; })();
|
|
51
53
|
const BOARD = BP.build(BF.facts());
|
|
52
54
|
const SOURCES = { ok: true, ws: '', sources: [{ id: 'calendar', name: 'Calendar', state: 'fresh', as_of: new Date().toISOString() }, { id: 'jira', name: 'Jira', state: 'failed', as_of: new Date(Date.now() - 90 * 60000).toISOString() }] };
|
|
53
55
|
|
|
@@ -280,7 +280,7 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
280
280
|
// from a place in the middle of the range (at the far end the next span's track can be shorter and stops short)
|
|
281
281
|
await p.evaluate(() => { const sc = document.querySelector('#ibBody .tg-sc'); sc.scrollLeft = (sc.scrollWidth - sc.clientWidth) / 2; }); await p.waitForTimeout(200);
|
|
282
282
|
const mid = (await geo()).mid;
|
|
283
|
-
await p.click('#bdSpans [data-sp="half"]'); await p.waitForTimeout(250);
|
|
283
|
+
await p.click('#bdSpansBtn'); await p.click('#bdSpans [data-sp="half"]'); await p.waitForTimeout(250);
|
|
284
284
|
g = await geo();
|
|
285
285
|
ok('S switching span keeps the date in the middle in the middle', g.span === 'half' && Math.abs(g.mid - mid) < 3 * DAY, JSON.stringify({ mid: new Date(mid), now: new Date(g.mid) }));
|
|
286
286
|
// the Monitor and Evaluate rows (batch 8 item 2) widen the range, so the middle can hold today: scroll back to the end, away from today
|
|
@@ -288,14 +288,14 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
288
288
|
await p.click('#ibBody .tg-tback'); await p.waitForTimeout(800);
|
|
289
289
|
g = await geo();
|
|
290
290
|
ok('S the Today button scrolls back to today, a quarter of the way in, and hides', Math.abs(g.at - 0.25) < 0.03 && !g.back.length, JSON.stringify(g));
|
|
291
|
-
await p.click('#bdSpans [data-sp="y2"]'); await p.waitForTimeout(250);
|
|
291
|
+
await p.click('#bdSpansBtn'); await p.click('#bdSpans [data-sp="y2"]'); await p.waitForTimeout(250);
|
|
292
292
|
g = await geo();
|
|
293
293
|
ok('S with today in view, a span switch keeps today in its place', g.span === 'y2' && (g.l === 0 || Math.abs(g.at - 0.25) < 0.03), JSON.stringify(g));
|
|
294
294
|
const lab2 = await p.evaluate(() => [...document.querySelectorAll('#ibBody .tg-endl, #ibBody .tg-pdl')].map((e) => e.textContent));
|
|
295
295
|
ok('S 2 yr labels say what lands, then when, with the year only when it is not this year (batch 13 item 4)', lab2.length > 0 && lab2.every((x) => /^[A-Z].* [A-Z][a-z]{2} \d{1,2}( ’\d\d)?$/.test(x)) && lab2.some((x) => / ’27$/.test(x)), JSON.stringify(lab2));
|
|
296
|
-
await p.click('#bdSpans [data-sp="sprint"]'); await p.waitForTimeout(200);
|
|
296
|
+
await p.click('#bdSpansBtn'); await p.click('#bdSpans [data-sp="sprint"]'); await p.waitForTimeout(200);
|
|
297
297
|
await p.evaluate(() => { const sc = document.querySelector('#ibBody .tg-sc'); if (sc) sc.scrollLeft = 0; });
|
|
298
|
-
await p.click('#bdSpans [data-sp="quarter"]'); await p.waitForTimeout(250);
|
|
298
|
+
await p.click('#bdSpansBtn'); await p.click('#bdSpans [data-sp="quarter"]'); await p.waitForTimeout(250);
|
|
299
299
|
g = await geo();
|
|
300
300
|
ok('S Sprint does not scroll, and going from Sprint to Quarter opens at today', !(await p.evaluate(() => !!document.querySelector('.sp .tg-sc'))) && Math.abs(g.at - 0.25) < 0.03, JSON.stringify(g));
|
|
301
301
|
// a drag on empty track scrolls; a press on a bar still opens the initiative
|
|
@@ -357,13 +357,13 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
357
357
|
subs: [...document.querySelectorAll('#ibBody .bt-psb')].map((x) => x.textContent.replace(/\s+/g, ' ').trim()),
|
|
358
358
|
kids: [...document.querySelectorAll('#ibBody .bt-kid .bt-nm b')].map((b) => b.textContent),
|
|
359
359
|
prods: [...document.querySelectorAll('#ibBody .bt-prd .bd-prdb span:not(.bd-ti):not(.bd-gd):not(.n)')].map((b) => b.textContent),
|
|
360
|
-
docs:
|
|
360
|
+
docs: !!document.querySelector('#tabDocs'), asks: !!document.querySelector('#chAsks'), nw: document.querySelector('#newIni').hidden,
|
|
361
361
|
chips: [...document.querySelectorAll('#bdChipset .bd-chip')].map((c) => c.id).filter(Boolean),
|
|
362
362
|
}));
|
|
363
363
|
ok('A All products is saved, the address stays plain and the chip says All', a.url === '' && /^All/.test(a.crumb) && a.label === 'Products: all 2' && JSON.stringify(pk.posts) === '[["idpf","microsites"]]', JSON.stringify({ a, posts: pk.posts }));
|
|
364
364
|
ok('A the program comes first and holds a sub-row per product with that product\'s initiatives', /Project Ascent/.test(a.band) && a.subs.length === 2 && /IDPF/.test(a.subs[0]) && a.kids.length === 3, JSON.stringify(a));
|
|
365
365
|
ok('A then one group per product with the rows no program holds', a.prods.length === 2 && a.prods[0] === 'IDPF', JSON.stringify(a));
|
|
366
|
-
ok('A
|
|
366
|
+
ok('A New initiative and Jira work across products (the Board has no Documents tab, batch 24 item 7); only the sprint waits for a product to be picked', !a.docs && !a.nw && a.chips.includes('chJira') && !a.chips.includes('chSprint'), JSON.stringify(a));
|
|
367
367
|
const other = await p.evaluate(() => { const b = [...document.querySelectorAll('#ibBody .bt-kid .bt-nm')].pop(); return b ? b.dataset.key : null; });
|
|
368
368
|
ok('A each row keeps its own product in its key', /^[a-z-]+\//.test(other || ''), other);
|
|
369
369
|
await p.click('#ibTabs [data-v="list"]');
|
|
@@ -62,7 +62,7 @@ const iso = (ms) => new Date(Date.now() + ms).toISOString();
|
|
|
62
62
|
let s = await open(1440, 900, '/premium-board.html?ws=idpf&board=idpf');
|
|
63
63
|
ok('A1 Shared is a tab beside Initiatives and Documents, and has no number when there is nothing', await s.p.locator('#tabShared').isVisible() && /^Shared\s*$/.test((await s.p.locator('#tabShared').textContent()).trim()));
|
|
64
64
|
ok('A2 opening the tab with nothing joined asks nothing of any host beyond the read, and shows one sentence and the invite field', await (async () => { await s.p.click('#tabShared'); await s.p.waitForSelector('#shInvite'); return /Boards your teams share show up here/.test(await s.p.locator('#cShared').textContent()) && (await s.p.locator('#cShared .bdsh-card').count()) === 0; })());
|
|
65
|
-
ok('A3 the other tabs are hidden while it is open, and Initiatives comes back', await s.p.locator('#cIni').isHidden() &&
|
|
65
|
+
ok('A3 the other tabs are hidden while it is open, and Initiatives comes back', await s.p.locator('#cIni').isHidden() && (await (async () => { await s.p.click('#tabIni'); return s.p.locator('#cShared').isHidden(); })()));
|
|
66
66
|
await s.ctx.close();
|
|
67
67
|
reset({ yours: MINE, names: ['Product'] }); s = await open(1440, 900, '/premium-board.html?ws=idpf&board=idpf');
|
|
68
68
|
ok('A4 at load the number is your own open links, from this computer only (no host was asked)', (await s.p.locator('#nShared').textContent()) === '2' && W.reads === 0, await s.p.locator('#nShared').textContent());
|
|
@@ -152,7 +152,7 @@ const iso = (ms) => new Date(Date.now() + ms).toISOString();
|
|
|
152
152
|
|
|
153
153
|
console.log('G a phone');
|
|
154
154
|
reset({ yours: MINE, names: ['Product'], teams: [JSON.parse(JSON.stringify(TEAM_OK))] }); s = await open(390, 844, '/premium-board.html?ws=idpf&board=idpf');
|
|
155
|
-
ok('G1 on a phone Shared is a chip after Docs, and it opens the tab', await (async () => { await s.p.waitForSelector('#chShared'); const after = await s.p.evaluate(() => document.getElementById('chShared').previousElementSibling && document.getElementById('chShared').previousElementSibling.id); await s.p.click('#chShared'); await s.p.waitForSelector('.bdsh-list .bdsh-card'); return after === '
|
|
155
|
+
ok('G1 on a phone Shared is a chip after the needs-a-look pill (no Docs chip, batch 24 item 7), and it opens the tab', await (async () => { await s.p.waitForSelector('#chShared'); const after = await s.p.evaluate(() => !document.getElementById('chDocs') && document.getElementById('chShared').previousElementSibling && document.getElementById('chShared').previousElementSibling.id); await s.p.click('#chShared'); await s.p.waitForSelector('.bdsh-list .bdsh-card'); return after === 'bdNeedw' || after === 'chJira'; })());
|
|
156
156
|
const g = await s.p.evaluate(() => ({ side: document.documentElement.scrollWidth > innerWidth + 1, cards: document.querySelectorAll('#cShared .bdsh-card').length }));
|
|
157
157
|
ok('G2 a row is a card that fits the phone, with no sideways scroll', g.cards === 4 && !g.side, JSON.stringify(g));
|
|
158
158
|
const small = await s.p.locator('#cShared button:visible').evaluateAll((els) => els.filter((e) => e.getBoundingClientRect().height < 34).map((e) => e.className + ':' + Math.round(e.getBoundingClientRect().height)));
|
|
@@ -8,9 +8,7 @@ const { spawn } = require('child_process');
|
|
|
8
8
|
const { findBrowser } = require('./lib/find-browser');
|
|
9
9
|
const { openMission } = require('./lib/open-mission');
|
|
10
10
|
const T = require('./test-run').configure({ name: 'briefing-premium-adddoc' });
|
|
11
|
-
const BP = require('./lib/board-premium.js');
|
|
12
11
|
const IP = require('./lib/initiative-premium.js');
|
|
13
|
-
const BF = require('./fixtures/board-premium-facts.js');
|
|
14
12
|
const IF = require('./fixtures/initiative-premium-facts.js');
|
|
15
13
|
|
|
16
14
|
const BASE = T.BASE;
|
|
@@ -34,7 +32,6 @@ const CUSTOM = [
|
|
|
34
32
|
{ id: 'cd-2', scope: 'board:w1', title: 'Contract', writer: 'drafts', phase: null, link: { kind: 'Confluence', url: 'https://example.atlassian.net/wiki/spaces/X/pages/1' }, at: '2026-10-18T09:05:00.000Z' },
|
|
35
33
|
];
|
|
36
34
|
const PAGES = {
|
|
37
|
-
board: { file: '/premium-board.html', api: '**/api/board-premium*', scope: 'board:w1', mk: (custom) => BP.build(BF.facts({ custom })), ready: '#ibBody .ini2', btn: '#addDoc', qs: '?theme=' },
|
|
38
35
|
// the initiative is a layout inside Home (premium-agreed-design): Add a document is in its Overview, a minimal form in the Viewer (Waiting for design)
|
|
39
36
|
initiative: { file: '/premium.html', api: '**/api/initiative-premium*', scope: 'initiative:self-serve-v2', mk: (custom) => IP.build(IF.facts({ custom })), ready: '#czViewer .iv-mc', btn: '#czViewer [data-hi-v="docs"]', qs: '?ini=self-serve-v2&ws=w1&board=w1&theme=' },
|
|
40
37
|
};
|
|
@@ -51,7 +48,6 @@ const PAGES = {
|
|
|
51
48
|
const open = async (kind, { w = 1280, h = 900, theme = 'light', custom = [], add = { status: 200, body: { ok: true } } } = {}) => {
|
|
52
49
|
const P = PAGES[kind];
|
|
53
50
|
const ctx = await openMission(await browser.newContext({ viewport: { width: w, height: h } }));
|
|
54
|
-
if (kind === 'board') await ctx.addInitScript(() => { try { localStorage.setItem('pmos.premium.openAs', 'popup'); } catch { /* none */ } });
|
|
55
51
|
const page = await ctx.newPage();
|
|
56
52
|
let current = custom, calls = 0, reply = add; const errors = [], posts = [];
|
|
57
53
|
page.on('pageerror', (e) => errors.push(String((e && e.message) || e)));
|
|
@@ -61,7 +57,6 @@ const PAGES = {
|
|
|
61
57
|
await page.goto(BASE + P.file + P.qs + theme);
|
|
62
58
|
let up = await until(() => page.evaluate((s) => !!document.querySelector(s), P.ready), 20000);
|
|
63
59
|
if (up && kind === 'initiative') { /* the page holds the Tasks (batch 11 item 20) */ await docsView(page); up = await until(() => page.evaluate((s) => !!document.querySelector(s), P.btn), 20000); } // the Overview holds the button
|
|
64
|
-
if (up && P.file === '/premium-board.html') await page.click(w <= 720 ? '#chDocs' : '#tabDocs'); // a phone reaches the documents from its Docs chip // the Agreed design shows the documents on the Documents tab (premium-agreed-design)
|
|
65
60
|
return { ctx, page, errors, posts, up, calls: () => calls, setCustom: (c) => { current = c; }, reply: (a) => { reply = a; } };
|
|
66
61
|
};
|
|
67
62
|
const txt = (p, s) => p.evaluate((q) => (document.querySelector(q) || {}).textContent || '', s);
|
|
@@ -72,49 +67,7 @@ const PAGES = {
|
|
|
72
67
|
// the initiative's page (canvas HbC): its Documents' "Show all N" drills into the Documents view
|
|
73
68
|
async function docsView(page) { if (await until(() => page.evaluate(() => !!document.querySelector('#czViewer:not([hidden]) [data-hi-v]')), 20000)) await (async (pg) => { if (await pg.$('#czViewer [data-hi-v="page"] [data-iv-docs]')) await pg.click('#czViewer [data-iv-docs]'); else if (await pg.$('#czViewer [data-hi-view="docs"]')) await pg.click('#czViewer [data-hi-view="docs"]'); else await viaMore(pg, 'Documents'); })(page); }
|
|
74
69
|
|
|
75
|
-
/* the Board
|
|
76
|
-
the same route, the same fields, the same refusals */
|
|
77
|
-
{
|
|
78
|
-
const A = await open('board'), p = A.page, P = PAGES.board;
|
|
79
|
-
ok('board 0 the page draws', A.up);
|
|
80
|
-
if (!A.up) return finish(1);
|
|
81
|
-
ok('board A1 the Documents tab has an Add a document button', await p.evaluate(() => { const b = document.querySelector('#docBody #addDoc'); return !!b && b.textContent === 'Add a document'; }));
|
|
82
|
-
await p.click('#addDoc');
|
|
83
|
-
const form = await until(() => p.evaluate(() => { const f = document.querySelector('#czViewer form[data-form="adddoc"]'); return f ? { title: document.querySelector('#czViewer .cz-vw-title').textContent, seg: [...f.querySelectorAll('.bd-rad span')].map((x) => x.textContent), kinds: [...f.kind.options].map((o) => o.textContent), uOff: f.url.disabled, note: f.querySelector('.bd-amut').textContent, dlg: !!document.querySelector('#dlg.on') } : null; }));
|
|
84
|
-
ok('board A1 the side panel has the dialog\'s fields, and the old dialog stays shut', !!form && form.title === 'Add a document' && form.seg.join() === 'PMOS drafts it,I write it' && form.kinds.join() === 'In PMOS,Confluence,Google Docs,Google Slides' && form.uOff && !form.dlg, JSON.stringify(form));
|
|
85
|
-
ok('board A7 it says the document is saved and the linked page is not read', !!form && /saved on the board's record/.test(form.note) && /does not read or sync/.test(form.note), form && form.note);
|
|
86
|
-
const go = '#czViewer form[data-form="adddoc"] [type="submit"]';
|
|
87
|
-
await p.click(go); await sleep(120);
|
|
88
|
-
ok('board A4 an empty title is refused in the panel and posts nothing', /Give the document a title/.test(await txt(p, '#formM')) && A.posts.length === 0);
|
|
89
|
-
await p.fill('#czViewer input[name="title"]', 'UAT plan'); await p.selectOption('#czViewer select[name="kind"]', 'Confluence');
|
|
90
|
-
ok('board A1 the address field opens for a link kind', await p.evaluate(() => !document.querySelector('#czViewer input[name="url"]').disabled));
|
|
91
|
-
await p.click(go); await sleep(120);
|
|
92
|
-
ok('board A4 a link kind with no address is refused in the panel and posts nothing', /Paste the address of the Confluence page/.test(await txt(p, '#formM')) && A.posts.length === 0);
|
|
93
|
-
A.reply({ status: 409, body: { ok: false, error: '“UAT plan” is already on this board.' } });
|
|
94
|
-
await p.fill('#czViewer input[name="url"]', 'https://example.atlassian.net/wiki/x'); await p.click(go); await sleep(300);
|
|
95
|
-
ok('board A4 a refusal from the route is shown in the panel, which stays open', /already on this/.test(await txt(p, '#formM')) && await p.evaluate((g) => !document.querySelector('#czViewer').hidden && !document.querySelector(g).disabled, go));
|
|
96
|
-
A.reply({ status: 200, body: { ok: true } }); const calls0 = A.calls();
|
|
97
|
-
await p.check('#czViewer input[name="writer"][value="you"]'); await p.click(go); await sleep(500);
|
|
98
|
-
const last = A.posts[A.posts.length - 1], mint = await T.nonce(BASE);
|
|
99
|
-
const want = { ws: 'w1', scope: P.scope, title: 'UAT plan', writer: 'you', kind: 'Confluence', url: 'https://example.atlassian.net/wiki/x' };
|
|
100
|
-
ok('board A3 the press posts the entered fields to the one route, with the launch nonce and no writer field', last && last.path === '/api/custom-docs/add' && JSON.stringify(Object.keys(last.body).sort()) === JSON.stringify(Object.keys(want).sort()) && Object.keys(want).every((k) => last.body[k] === want[k] || k === 'ws') && !!mint && last.headers['x-pmos-nonce'] === mint && !['by', 'via', 'at', 'actor', 'pm', 'nonce'].some((k) => k in last.body), JSON.stringify(last && last.body));
|
|
101
|
-
ok('board A3 nothing else was posted', A.posts.every((x) => x.path === '/api/custom-docs/add'));
|
|
102
|
-
ok('board A1 the panel closes, the page reads again and says it was added', A.calls() > calls0 && await p.evaluate(() => document.querySelector('#czViewer').hidden && /is added to the board's documents/.test(document.querySelector('#bdToast').textContent)));
|
|
103
|
-
A.setCustom(CUSTOM); await p.reload(); await until(() => p.evaluate((s) => !!document.querySelector(s), P.ready), 20000); await p.click('#tabDocs'); await sleep(300);
|
|
104
|
-
const rows = await p.evaluate(() => [...document.querySelectorAll('#docBody .bd-dt[data-docrow]')].map((r) => { const c = r.querySelectorAll('[role="cell"]'); return { id: r.dataset.docrow, t: r.querySelector('[data-opendoc]').textContent, s: c[1].textContent, when: c[2].textContent, by: c[3].textContent, btn: r.querySelectorAll('button').length }; }));
|
|
105
|
-
const uat = rows.find((r) => r.t === 'UAT plan'), con = rows.find((r) => r.t === 'Contract');
|
|
106
|
-
ok('board A5 an unlinked document reads Not started with the date it was added, who writes it, and no press but its title', uat && uat.s === 'Not started' && /18 Oct/.test(uat.when) && /You write it/.test(uat.by) && uat.btn === 1, JSON.stringify(uat));
|
|
107
|
-
ok('board A5 a linked document reads Linked and has no press but its title', con && con.s === 'Linked' && con.btn === 1, JSON.stringify(con));
|
|
108
|
-
await p.click(`#docBody [data-opendoc="${con.id}"]`);
|
|
109
|
-
ok('board A5 a linked document opens with its address as text', await until(() => p.evaluate(() => { const o = document.querySelector('#czViewer'); return !o.hidden && /Lives in Confluence: https:\/\/example\.atlassian\.net/.test(o.textContent) && !o.querySelector('a[href^="https://example"]'); })), await txt(p, '#czViewer'));
|
|
110
|
-
ok('board no page error', A.errors.length === 0, A.errors.join(' | '));
|
|
111
|
-
await A.ctx.close();
|
|
112
|
-
const N = await open('board', { w: 390, h: 844 }); await N.page.click('#addDoc'); await sleep(300);
|
|
113
|
-
const fl = await N.page.evaluate(() => ({ over: document.documentElement.scrollWidth > innerWidth + 1, add: Math.round(document.querySelector('#addDoc').getBoundingClientRect().height), go: Math.round(document.querySelector('#czViewer form[data-form="adddoc"] [type="submit"]').getBoundingClientRect().height) }));
|
|
114
|
-
ok('board A8 at 390 px no sideways scroll with the form open, and its buttons are 44 px', !fl.over && fl.add >= 44 && fl.go >= 44, JSON.stringify(fl));
|
|
115
|
-
await N.ctx.close();
|
|
116
|
-
}
|
|
117
|
-
|
|
70
|
+
/* the Board had its own Add a document on its Documents tab; batch 24 item 7 took that tab off the Board, so only the initiative's is checked */
|
|
118
71
|
for (const kind of ['initiative']) {
|
|
119
72
|
const P = PAGES[kind], ini = kind === 'initiative';
|
|
120
73
|
const A = await open(kind), p = A.page;
|
|
@@ -35,6 +35,8 @@ const four = async (p, name) => {
|
|
|
35
35
|
}
|
|
36
36
|
await p.setViewportSize(vp); await p.emulateMedia({ colorScheme: null }); await new Promise((r) => setTimeout(r, 250));
|
|
37
37
|
};
|
|
38
|
+
/* the one pill for what needs a look (batch 24 item 7, canvas BbB): its rows are pressed after the pill opens them */
|
|
39
|
+
const needOpen = async (pg) => { if (await pg.$('#bdNeedMenu[hidden]')) await pg.click('#chNeed'); };
|
|
38
40
|
const until = async (fn, ms = 10000, step = 100) => { const t0 = Date.now(); for (;;) { const v = await fn(); if (v) return v; if (Date.now() - t0 > ms) return v; await sleep(step); } };
|
|
39
41
|
const results = [];
|
|
40
42
|
const ok = (name, cond, extra = '') => { results.push({ name, cond: !!cond }); console.log(`${cond ? '✓' : '✗'} ${name}${!cond && extra !== '' ? ' — ' + extra : ''}`); };
|
|
@@ -109,7 +111,7 @@ const IVREAD = () => {
|
|
|
109
111
|
const txt = (s) => p.evaluate((q) => (document.querySelector(q) || {}).textContent || '', s);
|
|
110
112
|
|
|
111
113
|
const view = async (pg, v) => { await pg.evaluate((x) => document.querySelector(`#ibTabs [data-v="${x}"]`).click(), v); await sleep(150); };
|
|
112
|
-
const spanTo = async (pg, s) => { await pg.click(`#bdSpans [data-sp="${s}"]`); await sleep(150); };
|
|
114
|
+
const spanTo = async (pg, s) => { await pg.click('#bdSpansBtn'); await pg.click(`#bdSpans [data-sp="${s}"]`); await sleep(150); }; // one span picker (batch 24 item 7)
|
|
113
115
|
/* the Viewer's shell A (initiative viewer-shell-a, XvABoard): what it shows now, and a press in its ⋯ menu by label */
|
|
114
116
|
const vwOn = (pg, kind) => pg.evaluate((k) => { const v = document.querySelector('#czViewer'), c = window.czViewer && window.czViewer.current(); return !!v && !v.hidden && !!c && (!k || c.kind === k); }, kind || null);
|
|
115
117
|
const vwTitle = (pg) => pg.evaluate(() => { const v = document.querySelector('#czViewer'); return v && !v.hidden ? (v.querySelector('.cz-vw-title') || {}).textContent || '' : ''; });
|
|
@@ -200,16 +202,16 @@ const IVREAD = () => {
|
|
|
200
202
|
}
|
|
201
203
|
{
|
|
202
204
|
const R = await open({ model: BP.build(FJ.facts()) }), q = R.page;
|
|
203
|
-
const c = await q.evaluate(() => ({ chips: [...document.querySelectorAll('#bdChipset .bd-chip')].map((b) => b.textContent.replace(/\s+/g, ' ').trim() + (b.getAttribute('aria-pressed') === 'true' ? '*' : '')), ss: document.querySelector('.bt-l[data-row="board:self-serve-v2"] .bd-nx .tx').textContent, sso: document.querySelector('.bt-l[data-row="board:sso-partners"] .bd-nx .tx').textContent, red: !!document.querySelector('.bt-l[data-row="board:sso-partners"] .bd-nx em.red'), ssoTitle: document.querySelector('.bt-l[data-row="board:sso-partners"] .bd-nx .tx').title, st: document.querySelector('.bt-l[data-row="board:sso-partners"] .cs').textContent, ssoCount: document.querySelector('.bt-l[data-row="board:sso-partners"] .bd-nx .mu').textContent, ssCount: document.querySelector('.bt-l[data-row="board:self-serve-v2"] .bd-nx .mu').textContent }));
|
|
204
|
-
ok('B1 the chips read
|
|
205
|
+
const c = await q.evaluate(() => ({ chips: [...document.querySelectorAll('#bdChipset .bd-chip')].map((b) => b.textContent.replace(/\s+/g, ' ').trim() + (b.getAttribute('aria-pressed') === 'true' ? '*' : '')), need: document.querySelector('#chNeed').getAttribute('aria-label'), rows: [...document.querySelectorAll('#bdNeedMenu button')].map((b) => b.textContent.trim() + (b.getAttribute('aria-pressed') === 'true' ? '*' : '')).join('|'), jira: document.querySelector('#chJira').getAttribute('title'), ss: document.querySelector('.bt-l[data-row="board:self-serve-v2"] .bd-nx .tx').textContent, sso: document.querySelector('.bt-l[data-row="board:sso-partners"] .bd-nx .tx').textContent, red: !!document.querySelector('.bt-l[data-row="board:sso-partners"] .bd-nx em.red'), ssoTitle: document.querySelector('.bt-l[data-row="board:sso-partners"] .bd-nx .tx').title, st: document.querySelector('.bt-l[data-row="board:sso-partners"] .cs').textContent, ssoCount: document.querySelector('.bt-l[data-row="board:sso-partners"] .bd-nx .mu').textContent, ssCount: document.querySelector('.bt-l[data-row="board:self-serve-v2"] .bd-nx .mu').textContent }));
|
|
206
|
+
ok('B1 the chips read the sprint and the release, This sprint (on by default), one pill for the new asks, late, at risk and blocked, and Jira as a face whose words show on hover (batch 24 item 7)', c.chips[0] === 'Sprint 24 · v2.4 ships Nov 4' && c.chips[1] === 'Sprint 24 only*' && c.need === '1 new ask, 1 late, 3 at risk, 1 blocked. Show them' && c.rows === 'New ask1|Late1|At risk3|Blocked1' && /^Jira · read .*Press to read Jira again$/.test(c.jira) && c.chips[c.chips.length - 1] === 'Jira · read ' + c.jira.split('Jira · read ')[1].split('. Press')[0], JSON.stringify(c));
|
|
205
207
|
ok('B1 Build and Deliver say the ticket that matters, counted in points or tickets; a blocked ticket comes first, in red', c.ssCount === '5/13 pts' && c.ssoCount === '1/4 tickets' && /^Finish SSO-12/.test(c.sso) && c.red && /blocked/.test(c.ssoTitle) && /^Finish ONB-\d+ · 2 more$/.test(c.ss), JSON.stringify(c));
|
|
206
208
|
ok('B1 a Deliver row whose release has a date says when it ships', c.st === 'Ships Nov 4', c.st);
|
|
207
209
|
// a metric chip opens its trend (BdPanel); its "Show only" filters
|
|
208
|
-
await q.click('#bdChipset [data-f="blocked"]');
|
|
210
|
+
await needOpen(q); await q.click('#bdChipset [data-f="blocked"]');
|
|
209
211
|
ok('B1 a metric chip opens its trend, with Show only and See in Intelligence', await until(() => q.evaluate(() => { const p = document.querySelector('#bdPop'); return !!p && !p.hidden && /Blocked · 1 now/.test(p.textContent) && !!p.querySelector('[data-trshow="blocked"]') && /See in Intelligence/.test(p.textContent); })));
|
|
210
212
|
await q.click('#bdPop [data-trshow="blocked"]');
|
|
211
213
|
ok('B1 a chip filters the rows to what it counts', await q.evaluate(() => [...document.querySelectorAll('#ibBody .bt-l')].map((r) => r.dataset.row).join() === 'board:sso-partners'));
|
|
212
|
-
await q.click('#bdChipset [data-f="blocked"]');
|
|
214
|
+
await needOpen(q); await q.click('#bdChipset [data-f="blocked"]');
|
|
213
215
|
await q.click('.bt-l[data-row="board:sso-partners"] .bt-nm');
|
|
214
216
|
/* re-pointed (viewer-shell-a, XvABoard): the initiative opens in the Viewer's shell A, floating over the page's right edge: the summary card,
|
|
215
217
|
* the stage bar, then the folding rows. The Ask box is gone; Discuss in chat and ⋯ take its presses */
|
|
@@ -247,7 +249,7 @@ const IVREAD = () => {
|
|
|
247
249
|
/* the chips and tools open their designed popovers (BdAsks, BdSprint, BdFilter) and New initiative its panel (BdNew), with no "Waiting for design" tag */
|
|
248
250
|
const waits = [];
|
|
249
251
|
for (const [sel, name, body] of [['#chAsks', 'New asks', /1 new ask.*Finance wants a CSV of failed payouts.*Accept to Triage/], ['#chSprint', 'Sprints and releases', /Sprints · from Jira.*Sprint 24.*Releases.*Backlog/], ['#bdFind', 'Find', /Find stays in the Board/], ['#bdFilter', 'Filter', /Owner.*Dara.*Lee.*No owner.*Category.*In progress.*Done.*Applies as you pick/]]) {
|
|
250
|
-
await q.click(sel);
|
|
252
|
+
if (sel === '#chAsks') await needOpen(q); await q.click(sel);
|
|
251
253
|
waits.push(await until(() => q.evaluate(([nm, re]) => { const k = document.querySelector('#bdPop'); if (!k || k.hidden) return null; return k.getAttribute('aria-label') === nm && !k.querySelector('.bd-wtag') && new RegExp(re).test(k.textContent) ? nm : null; }, [name, body.source])) || 'missing ' + name);
|
|
252
254
|
await q.keyboard.press('Escape');
|
|
253
255
|
}
|
|
@@ -297,8 +299,8 @@ const IVREAD = () => {
|
|
|
297
299
|
}
|
|
298
300
|
|
|
299
301
|
/* B3 + B11 one count: the Initiatives tab, the track's head and the chips agree with the board's counts; B12 the status says what is real */
|
|
300
|
-
const head = await p.evaluate(() => ({ tab: document.querySelector('#nIni').textContent, n: document.querySelector('.bt-nmh span').textContent, chips: [...document.querySelectorAll('#
|
|
301
|
-
ok('B3 B11 the counts are the board\'s, once: 9 open in the tab and the head, 1 late and 3 at risk in the chips', head.tab === '9' && head.n === '9' && head.chips === '
|
|
302
|
+
const head = await p.evaluate(() => ({ tab: document.querySelector('#nIni').textContent, n: document.querySelector('.bt-nmh span').textContent, chips: [...document.querySelectorAll('#bdNeedMenu [data-f]')].map((b) => b.textContent.trim()).join('|'), snap: document.querySelector('#snap').textContent }));
|
|
303
|
+
ok('B3 B11 the counts are the board\'s, once: 9 open in the tab and the head, 1 late and 3 at risk in the chips', head.tab === '9' && head.n === '9' && head.chips === 'Late1|At risk3', JSON.stringify(head));
|
|
302
304
|
ok('B12 the status says what is real', /^Real data · as of 09:12$/.test(head.snap), JSON.stringify(head));
|
|
303
305
|
|
|
304
306
|
/* B4 B5 List (BdList) and By stage (BdByStage): rows and cards open the panel */
|
|
@@ -335,7 +337,7 @@ const IVREAD = () => {
|
|
|
335
337
|
/* B6 (premium-agreed-design B2) the timeline: plan against the Jira forecast; every span from Quarter on draws the same rows (feedback batch 7,
|
|
336
338
|
* timeline spans C: TsAllSpans); the scroll itself is checked in board-programs-test.js (T) */
|
|
337
339
|
await view(p, 'time');
|
|
338
|
-
const tl = await p.evaluate(() => ({ spans: [...document.querySelectorAll('#bdSpans
|
|
340
|
+
const tl = await p.evaluate(() => ({ spans: [...document.querySelectorAll('#bdSpans [data-sp]')].map((b) => b.textContent + (b.getAttribute('aria-checked') === 'true' ? '*' : '')).join(), shown: !document.querySelector('#bdSpans').hidden, rows: [...document.querySelectorAll('#ibBody .tg-r .tg-n.ini2 b')].map((b) => b.textContent), bars: document.querySelectorAll('#ibBody .tg-bar').length, none: (document.querySelector('.tg-none') || {}).textContent || '', ask: (document.querySelector('#askDates') || {}).href || '', hid: (document.querySelector('.tg-hid') || {}).textContent || '', lgd: (document.querySelector('.tg-lgd') || {}).textContent || '', today: (document.querySelector('#ibBody .tg-tpill') || {}).textContent === 'Today' }));
|
|
339
341
|
ok('B6 the timeline has six spans and starts on Quarter, one row per initiative with dates; the others are named, with an ask to propose dates', tl.shown && tl.spans === 'Sprint,Quarter*,Half,Year,2 yr,5 yr' && tl.rows.length === 3 && tl.bars === 3 && /^4 have no dates: Usage-based billing, Audit log export, Refund reason codes, Docs search/.test(tl.none) && /\/premium\.html\?ask=board%3Aw1&q=Propose/.test(tl.ask), JSON.stringify(tl));
|
|
340
342
|
ok('B6 Monitor and Evaluate show on the Quarter, with no hidden count in the footer (batch 8 item 2); the legend (PgTimeline: done, planned, the Jira forecast, Milestone (batch 11 item 3), today) and Today are drawn', tl.hid === '' && /Done in Jira/.test(tl.lgd) && /To do/.test(tl.lgd) && /Jira forecast/.test(tl.lgd) && /Milestone/.test(tl.lgd) && /Today/.test(tl.lgd) && tl.today, JSON.stringify(tl));
|
|
341
343
|
ok('B6 the spans help popover went with the theme grouping (TsCGrill): the first head is a plain "Initiative"', await p.evaluate(() => !document.querySelector('#popRules') && !document.querySelector('#ibBody [data-rules]') && /^Initiative/.test(document.querySelector('#ibBody .tg-h .tg-n').textContent)));
|
|
@@ -373,12 +375,8 @@ const IVREAD = () => {
|
|
|
373
375
|
}
|
|
374
376
|
await view(p, 'track');
|
|
375
377
|
|
|
376
|
-
/* B7 the Documents tab (
|
|
377
|
-
await p.
|
|
378
|
-
ok('B7 the Documents tab lists the documents with kind chips and hides the initiatives; the tab counts them', await p.evaluate(() => !document.querySelector('#cDocs').hidden && document.querySelector('#cIni').hidden && document.querySelector('#tabDocs').getAttribute('aria-selected') === 'true' && document.querySelector('#docCap').textContent === '6' && !document.querySelector('#docBody .bd-wtag') && document.querySelectorAll('#docBody .bd-dt[data-docrow]').length === 6 && document.querySelectorAll('#docBody [data-dkind]').length === 2 && document.querySelectorAll('#docBody [data-act="draft"]').length === 3 && /Board charter.*Not started/.test(document.querySelector('#docBody').textContent)));
|
|
379
|
-
{ const n2 = A.posts.length; await p.click('#docBody [data-act="draft"][data-key="charter"]');
|
|
380
|
-
ok('B7 Draft it asks the agent to draft the document (POST /api/core-docs/draft), and says nothing is sent', await until(() => A.posts.slice(n2).some((x) => x.path === '/api/core-docs/draft' && x.body.key === 'charter' && x.body.scope === 'board:w1')) && await until(() => p.evaluate(() => /drafting it/.test(document.querySelector('#bdToast').textContent))), JSON.stringify(A.posts.slice(n2))); }
|
|
381
|
-
await p.click('#tabIni');
|
|
378
|
+
/* B7 the Board has no Documents tab (batch 24 item 7, canvas BbB): an initiative's Documents fold and Search's Documents filter reach the documents */
|
|
379
|
+
ok('B7 the Board has no Documents tab and no Docs chip; its tabs are Initiatives and Shared', await p.evaluate(() => !document.querySelector('#tabDocs') && !document.querySelector('#cDocs') && !document.querySelector('#chDocs') && [...document.querySelectorAll('.bd-tab')].map((t) => t.id).join() === 'tabIni,tabShared'));
|
|
382
380
|
|
|
383
381
|
/* B13 focus: Escape closes the panel and gives the focus back to the row; the Board never opens the old dialog */
|
|
384
382
|
await p.locator('#ibBody .bt-l[data-row="board:self-serve-v2"] .bt-nm').click(); await until(() => vwOn(p, 'Initiative'));
|
|
@@ -421,7 +419,8 @@ const IVREAD = () => {
|
|
|
421
419
|
const ctx = await openMission(await browser.newContext({ viewport: { width: 1280, height: 900 } })), pg = await ctx.newPage(); let calls = 0;
|
|
422
420
|
await pg.route('**/api/board-premium*', (r) => { calls++; if (calls < 2) return r.fulfill({ status: 202, contentType: 'application/json', body: JSON.stringify({ ok: true, state: 'reading' }) }); return r.fulfill({ contentType: 'application/json', body: JSON.stringify({ ok: true, state: 'ready', model: mk(), as_of: Date.now() }) }); });
|
|
423
421
|
await pg.goto(BASE + '/premium-board.html');
|
|
424
|
-
|
|
422
|
+
// page-open-speed (SpD): grey rows that claim nothing stand in for the board; past 2 s only 'Still reading the board.' is said
|
|
423
|
+
ok('B2 while the board is read the page says so, with the seconds, and shows grey rows instead of a guess', await until(() => pg.evaluate(() => /Reading the board · \d+ s so far/.test(document.querySelector('#snap').textContent) && document.querySelectorAll('#ibBody .ini2').length === 0 && !!document.querySelector('#ibBody .ls-bd') && /^(Still reading the board\.)?$/.test(document.querySelector('#ibBody').textContent.trim())), 4000));
|
|
425
424
|
ok('B2 it asks again and draws when the answer is ready', await until(() => pg.evaluate(() => document.querySelectorAll('#ibBody .ini2').length === 7 && /^Real data/.test(document.querySelector('#snap').textContent)), 9000));
|
|
426
425
|
await ctx.close();
|
|
427
426
|
const c2 = await openMission(await browser.newContext({ viewport: { width: 1280, height: 900 } })), p2 = await c2.newPage();
|
|
@@ -436,7 +435,7 @@ const IVREAD = () => {
|
|
|
436
435
|
const R = await open({ w: 390, h: 844, theme, model: BP.build(FJ.facts()) }), q = R.page;
|
|
437
436
|
const m = await q.evaluate(() => { const small = [...document.querySelectorAll('#ibTabs button, #ibBody .bt-nm, #bdChipset .bd-chip, .bd-tab, #newIni, #bdFind, #bdView')].filter((b) => !b.closest('[hidden]') && b.getBoundingClientRect().height > 0 && b.getBoundingClientRect().height < 44).map((b) => b.className + ':' + Math.round(b.getBoundingClientRect().height)); const fab = document.querySelector('#czFab').getBoundingClientRect(); return { over: document.documentElement.scrollWidth > innerWidth + 1, small, fabRight: Math.round(innerWidth - fab.right), top: Math.round(document.querySelector('.bd-hd').getBoundingClientRect().height), chips: [...document.querySelectorAll('#bdChipset .bd-chip')].map((b) => b.textContent.replace(/\s+/g, ' ').trim()), title: getComputedStyle(document.querySelector('.bd-title')).display }; });
|
|
438
437
|
ok(`B13 at 390 px (${theme}) the page has no sideways scroll, the top bar is one line with "Board", the menu sits bottom right, and every control is 44 px`, !m.over && m.small.length === 0 && m.fabRight < 40 && m.top <= 56 && m.title !== 'none', JSON.stringify(m));
|
|
439
|
-
ok(`B13 at 390 px (${theme}) the chips lead with This sprint, then
|
|
438
|
+
ok(`B13 at 390 px (${theme}) the chips lead with This sprint, then Jira as a face, the one pill for what needs a look, and end with Shared (no Docs chip, batch 24 item 7)`, m.chips[0] === 'Sprint 24 only' && /^Jira · read /.test(m.chips[1]) && m.chips[2] === '1131' && m.chips[m.chips.length - 1] === 'Shared' && !m.chips.some((t) => /^Docs/.test(t)), JSON.stringify(m.chips));
|
|
440
439
|
const pl = await q.evaluate(() => ({ groups: [...document.querySelectorAll('#ibBody .bd-ghd')].map((g) => g.textContent.replace(/\s+/g, ' ').trim()), seg: [...document.querySelectorAll('#ibBody [data-pv]')].map((b) => b.textContent + (b.getAttribute('aria-pressed') === 'true' ? '*' : '')).join(), note: /Timeline needs a wider screen/.test(document.querySelector('#ibBody').textContent), small: [...document.querySelectorAll('#ibBody [data-pv], #ibBody .bd-lrow')].filter((b) => b.getBoundingClientRect().height < 44).length }));
|
|
441
440
|
ok(`B13 at 390 px (${theme}) the phone opens on the List, grouped your turn first, with List, By stage, Track and Timeline (BdPhone, TsCPhone)`, pl.groups.join('|') === 'At risk or late4|On track3' && pl.seg === 'List*,By stage,Track,Timeline' && !pl.note && pl.small === 0, JSON.stringify(pl));
|
|
442
441
|
await q.click('#bdView'); const s2 = await q.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1 && document.querySelectorAll('#ibBody .bd-scols.ph .bd-scol').length + (((document.querySelector('#ibBody .bd-sempty') || {}).textContent || '').replace(/^Nothing on screen in /, '').match(/[A-Z][a-z]+/g) || []).length === 8);
|
|
@@ -522,9 +521,9 @@ const IVREAD = () => {
|
|
|
522
521
|
await q.fill('#czViewer input[name="title"]', 'Per-key API usage'); await q.fill('#czViewer textarea[name="why"]', 'Partners ask which key used the quota.'); await q.selectOption('#czViewer select[name="anchor"]', 'q3-2026:o1:kr1'); await q.click('#czViewer form[data-form="new2"] [type="submit"]');
|
|
523
522
|
ok('T New initiative records an ask and accepts it into the first stage, anchored, with the Why as its intent and a work lane', await until(() => { const i = R.posts.findIndex((x) => x.path === '/api/boards/asks/add' && x.body.line === 'Per-key API usage'); return i >= 0 && R.posts.slice(i).some((x) => x.path === '/api/boards/asks/accept' && x.body.ask === 'n1' && x.body.anchor === 'q3-2026:o1:kr1' && typeof x.body.lane === 'string' && x.body.lane.length > 0 && x.body.intent === 'Partners ask which key used the quota.'); }), JSON.stringify(R.posts.slice(-3)));
|
|
524
523
|
await q.keyboard.press('Escape');
|
|
525
|
-
await q.click('#chAsks'); await q.click('#bdPop [data-askact="accept"][data-id="a1"]');
|
|
524
|
+
await needOpen(q); await q.click('#chAsks'); await q.click('#bdPop [data-askact="accept"][data-id="a1"]');
|
|
526
525
|
ok('T accepting an ask with no goal yet opens New initiative from its draft', await until(() => q.evaluate(() => (document.querySelector('#czViewer input[name="title"]') || {}).value === 'Failed payouts CSV' && (document.querySelector('#czViewer .cz-vw-title') || {}).textContent === 'Accept ask')));
|
|
527
|
-
await q.keyboard.press('Escape'); await q.click('#chAsks'); await q.click('#bdPop [data-askact="decline"][data-id="a1"]');
|
|
526
|
+
await q.keyboard.press('Escape'); await needOpen(q); await q.click('#chAsks'); await q.click('#bdPop [data-askact="decline"][data-id="a1"]');
|
|
528
527
|
await q.click('#bdPop [data-whynot="Out of scope"]'); await q.fill('#bdPop input[name="why"]', 'Finance owns this report'); await q.click('#bdPop [data-askact="decline-go"]');
|
|
529
528
|
ok('T declining an ask takes a reason, and keeps it', await until(() => R.posts.some((x) => x.path === '/api/boards/asks/decline' && x.body.ask === 'a1' && x.body.why === 'Out of scope: Finance owns this report')), JSON.stringify(R.posts.slice(-2)));
|
|
530
529
|
/* Find, Filter, Metrics, cancelled and archived */
|
package/workspace/changelog.json
CHANGED
|
@@ -1,4 +1,41 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"v": "0.496.0",
|
|
4
|
+
"at": "2026-11-03T00:20:24.723Z",
|
|
5
|
+
"f": 3,
|
|
6
|
+
"prs": [
|
|
7
|
+
{
|
|
8
|
+
"n": 1172,
|
|
9
|
+
"t": "Initiative chats find the initiative's workflow through pmos, never a lane",
|
|
10
|
+
"s": "Initiative chats find the initiative's workflow through pmos, never a lane",
|
|
11
|
+
"k": "fix"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"n": 1170,
|
|
15
|
+
"t": "Keep a rejected ✦ suggestion rejected while its facts hold",
|
|
16
|
+
"s": "Keep a rejected ✦ suggestion rejected while its facts hold",
|
|
17
|
+
"k": "fix"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"n": 1171,
|
|
21
|
+
"t": "Tasks keep their mapped stage after a workflow switch on Home and Board",
|
|
22
|
+
"s": "Tasks keep their mapped stage after a workflow switch on Home and Board",
|
|
23
|
+
"k": "fix"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"n": 1173,
|
|
27
|
+
"t": "Calmer Board top bar (batch 24 item 7, design B)",
|
|
28
|
+
"s": "Calmer Board top bar (batch 24 item 7, design B)",
|
|
29
|
+
"k": "new"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"n": 1165,
|
|
33
|
+
"t": "Pages show something at once while they read",
|
|
34
|
+
"s": "Pages show something at once while they read",
|
|
35
|
+
"k": "new"
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
},
|
|
2
39
|
{
|
|
3
40
|
"v": "0.495.0",
|
|
4
41
|
"at": "2026-11-02T23:26:48.050Z",
|
package/workspace/guide/adler.md
CHANGED
|
@@ -33,7 +33,7 @@ Each mood comes from real state. When nothing happens, Adler is calm or he rests
|
|
|
33
33
|
- **Search.** Adler with a telescope when nothing matches.
|
|
34
34
|
- **Initiative mission.** The telemetry card at the top of an initiative uses the same art: the rocket on its pad (a blueprint, then built, then lift-off), the planet, the dish and the clouds. The rocket flies a path that runs from left to right in time. While Discover is open, Adler stands at the telescope; when Discover is done, he is gone. On a festival, the day's things hang in the sky in place of the clouds. When the news is bad (the initiative is late) the festival goes. With **Face only** or **Off**, neither he nor the festival shows.
|
|
35
35
|
- **Welcome.** Adler says hello once on the first step, and shows his thinking face while PMOS reads your folder.
|
|
36
|
-
- **Board.** His small face
|
|
36
|
+
- **Board.** His small face is the Jira control at the end of the top bar: calm when Jira was read in the last day and a half, honest (with an amber dot) when it was not, and his thinking face while a press reads it again. Hold the pointer on it to see when Jira was read. He heads **New asks**: needs you while asks wait. When a delivery is missed, his honest face stands once in the Deliveries lane's header; hover it to see which. When you open a milestone that was delivered, he celebrates once in the panel (at most once a week); after that his face is calm. The diamond on the timeline stays plain, and nothing is sent to anyone.
|
|
37
37
|
- **Program charter.** His face on the check line, with his thinking face while Claude checks the milestones.
|
|
38
38
|
- **Stage card.** While a session works on a step, that step shows Adler with his thinking face instead of a spinner. When every step is done, Adler stands by the **Move to** button. Your own steps keep their rings.
|
|
39
39
|
- **Sessions.** Only the session that asks you something shows his face in the list. The other sessions keep their dots. In that session's card, he asks the question.
|
|
@@ -325,6 +325,7 @@ Not drawn, because no record backs them: answers and ideas an agent files by its
|
|
|
325
325
|
Board is the fourth place on the rail, after Chat, at `/premium-board.html`. It reads what the production Initiatives page reads, for one board at a time, and shows a switcher only when more than one board is registered. The first time it takes a few seconds, because the routes derive every initiative. The page says "Reading the board" with the seconds, shows nothing in place of a guess, and asks again.
|
|
326
326
|
|
|
327
327
|
- **The header** counts the board's open initiatives: how many are on track, at risk, late and on hold, with a bar. A zero is shown.
|
|
328
|
+
- **The top bar** has two tabs, **Initiatives** and **Shared**. The Board has no Documents tab: open an initiative's **Documents** fold, or use Search's **Documents** filter. On the Timeline, one chip shows the span (Sprint, Quarter, Half, Year, 2 yr or 5 yr); press it to pick another. One pill shows what needs a look: a dot and a number for new asks, late, at risk, blocked and idle. A count of zero is not shown, and the pill is not shown when all counts are zero. Press the pill to see the rows. Press a row to open the asks or that count's trend, where **Show only** filters the Board; press a row that filters to stop the filter. On the Timeline, the **(i)** icon shows what the bars mean. Jira is Adler's small face at the end of the row. It has an amber dot when Jira was not read in the last day and a half. Hold the pointer on it to see when Jira was read, and press it to read Jira again.
|
|
328
329
|
- **The view chip** in the Initiatives head says which view is shown (List, By stage or Timeline); press it to pick another.
|
|
329
330
|
- **List** shows the late and at-risk initiatives first, then the board's now view, up to 100 loaded and 25 on screen at a time. A row shows the owner and lane, a status chip when it is not on track, how far through the workflow it is (and "derived" when the workflow engine placed it), a progress bar only when a record holds progress, and the planned finish only when an end date was set ("Not planned" otherwise). "Do this first" appears only when the first row is late.
|
|
330
331
|
- **By stage** has one column for each stage of the board's workflow with the board's own count and its first five initiatives.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Claude proposes: the one ✦ on each initiative"
|
|
4
4
|
description: "What Claude finds when you add an initiative that already lives elsewhere, the new dates, done and tidy names it proposes for late tasks, the clean-up of the work that is not in an initiative, how you keep or drop them in the Viewer, Undo, the pmos propose commands, and how Claude grills an initiative by itself (pmos grill)."
|
|
5
|
-
timestamp: 2026-11-
|
|
5
|
+
timestamp: 2026-11-02
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Claude proposes: the one ✦ on each initiative
|
|
@@ -21,7 +21,11 @@ Press the ✦ to open "Claude proposes N" in the Viewer. On a desk the chat stay
|
|
|
21
21
|
|
|
22
22
|
✓ in the Viewer's header keeps everything Claude is sure of. Check these and Not placed are never part of it. Each proposal has its own ✓ and ✕, and ⌄ shows its items. ✕ on one item leaves just that item out. × closes the Viewer and keeps or drops nothing. One toast says what changed, with Undo for the whole batch.
|
|
23
23
|
|
|
24
|
-
✕ means gone: a dropped proposal comes back only when its evidence changes or the date passes again. A
|
|
24
|
+
✕ means gone: a dropped proposal comes back only when its evidence changes or the date passes again. A ✕ stays with the facts, not with how the work is laid out. These do not bring a dropped proposal back: you edit, rename or move a task, you switch the initiative's workflow, Claude words a grill finding again, or a new day starts. A new date proposal for the same late task comes back only when the task misses a new due date. A grill finding comes back only when its sources change.
|
|
25
|
+
|
|
26
|
+
The same is true for Claude's suggestion to split an initiative or to merge it into another. ✕ on "Split off …?" stays while most of the tasks it would move are the tasks you said no to. You can add, edit or re-date tasks and the suggestion stays away. Claude suggests a split again only when a different group of work stands on its own. ✕ on "Same work as …?" stays while the merge is into the same initiative.
|
|
27
|
+
|
|
28
|
+
A Jira ticket keeps Jira's name and dates: a proposal for one becomes a drafted comment in your outbox. A draft plan is never changed.
|
|
25
29
|
|
|
26
30
|
## The commands
|
|
27
31
|
|
|
@@ -111,6 +111,8 @@ A refresh waits for the sources it uses. The briefing waits for work assigned to
|
|
|
111
111
|
- **Quiet start**: a server starts no background run in its first 5 minutes, so the first pages load quickly. A missed refresh catches up after that.
|
|
112
112
|
- **Warm start**: 5 seconds after PMOS starts, it reads what Home and the Board show once, so your first page opens as fast as later ones. This read writes nothing you would see as read or seen. `PMOS_WARM=off` turns it off.
|
|
113
113
|
- **Fresh answers, asked once**: when two parts of a page ask for the same thing at the same moment, the server is asked once and both get the same answer. Nothing is kept after it arrives, so the next look always reads again.
|
|
114
|
+
- **Last view while Home reads**: Home shows the list from your last visit at once, with a hollow ring next to "Good morning". You can read that list, but you cannot use it until the new read is in. Then the ring goes away. Before a section has data, it shows grey shapes with no names and no counts. Each section fills in when its own data arrives. If the Viewer was open on your last visit, its space stays empty, so the chat does not change width. Adler's face next to the greeting thinks while Home reads, and changes to handled when the read is in. If the read fails, it changes to honest.
|
|
115
|
+
- **Board while it reads**: the toolbar and the tabs show at once, with grey rows below them. If the read takes more than 2 seconds, the line "Still reading the board." shows. There is no progress bar and no percent.
|
|
114
116
|
|
|
115
117
|
### The refresh log
|
|
116
118
|
|
package/workspace/guide/home.md
CHANGED
|
@@ -152,7 +152,7 @@ Some features are still not designed. Home keeps each one usable as a plain card
|
|
|
152
152
|
|
|
153
153
|
## The other pages
|
|
154
154
|
|
|
155
|
-
- **Board.** New initiative takes links and files, starts in Triage and asks only the questions that the material does not answer. **Later** keeps the questions as the next step. The Board also has List, By stage
|
|
155
|
+
- **Board.** New initiative takes links and files, starts in Triage and asks only the questions that the material does not answer. **Later** keeps the questions as the next step. The Board also has List, By stage and a span picker. Documents are in each initiative's Documents fold and in Search. On a phone, **Metrics** and **Cancelled and archived** are in every view.
|
|
156
156
|
- **Meetings.** Replay with screen captures and kept points, Focus, a presenter view, Add material, the After view, a Day, Week and Month calendar, and Notes.
|
|
157
157
|
- **Reports.** The Brief, the report states, a document reader and a live page reader.
|
|
158
158
|
- **Insights.** A goal's detail with its chart and readings, targets on each tile, Library flows and a Portfolio view.
|
|
@@ -37,7 +37,7 @@ Only you can publish, republish or turn off a link, with a press on the page. An
|
|
|
37
37
|
|
|
38
38
|
## The Shared tab
|
|
39
39
|
|
|
40
|
-
**Shared** is a tab of the Board, beside **Initiatives
|
|
40
|
+
**Shared** is a tab of the Board, beside **Initiatives**. It lists the boards that people published to your teams, and your own links. On a phone it is the last chip. The tab shows a number when there are open copies. Before you read a team, the number is your own open links.
|
|
41
41
|
|
|
42
42
|
**Join a team.** The person who makes a team gets an invite link and sends it to you. It looks like `https://boards.example.com/join#Team.password`. Open **Shared**, paste the link in the field and press **Join**. PMOS asks the host first. It keeps the team only if the password works. You do not need to set the host address first: the invite link has it.
|
|
43
43
|
|