create-pmos 0.501.0 → 0.503.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 +39 -27
- 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/briefing-premium-calendar-2-test.js +4 -3
- package/workspace/briefing-premium-calendar-3-test.js +2 -3
- package/workspace/changelog.json +38 -0
- package/workspace/fixtures/premium-viewer-fixtures.js +3 -2
- package/workspace/guide/briefing-premium.md +1 -1
- package/workspace/guide/home.md +2 -1
- package/workspace/guide/notes.md +21 -1
- package/workspace/lib/meeting-terms.js +53 -0
- package/workspace/lib/routes-meeting-points.js +7 -4
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/premium-chat-line-test.js +3 -3
- package/workspace/premium-vw-meet-test.js +28 -17
- package/workspace/public/pm/bd-cols.js +5 -2
- package/workspace/public/pm/bd-deliveries.js +3 -3
- package/workspace/public/pm/bd-pin-card.js +3 -3
- package/workspace/public/pm/bd-pins.js +10 -0
- package/workspace/public/pm/bd-span.js +11 -9
- package/workspace/public/pm/cozy-vw-mtg.css +81 -0
- package/workspace/public/pm/home-line.css +2 -2
- package/workspace/public/pm/home-line.js +42 -13
- package/workspace/public/pm/ini-view.css +10 -0
- package/workspace/public/pm/premium-mtg-replay.js +155 -67
- package/workspace/public/pm/premium-vw-mtg.js +119 -25
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.503.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,44 @@
|
|
|
32
32
|
"audit"
|
|
33
33
|
],
|
|
34
34
|
"pmosChanges": [
|
|
35
|
+
{
|
|
36
|
+
"v": "0.503.0",
|
|
37
|
+
"at": "2026-11-03T05:42:41.752Z",
|
|
38
|
+
"f": 3,
|
|
39
|
+
"prs": [
|
|
40
|
+
{
|
|
41
|
+
"n": 1186,
|
|
42
|
+
"t": "A timeline end ◇ names the initiative's own workflow, never a fixed Deliver",
|
|
43
|
+
"s": "A timeline end ◇ names the initiative's own workflow, never a fixed Deliver",
|
|
44
|
+
"k": "fix"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"n": 1187,
|
|
48
|
+
"t": "Working card leads with the todo or turn goal, not the raw command",
|
|
49
|
+
"s": "Working card leads with the todo or turn goal, not the raw command",
|
|
50
|
+
"k": "new"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"n": 1188,
|
|
54
|
+
"t": "New meeting recording view, word fix in place (batch 25 item 11)",
|
|
55
|
+
"s": "New meeting recording view, word fix in place (batch 25 item 11)",
|
|
56
|
+
"k": "new"
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"v": "0.502.0",
|
|
62
|
+
"at": "2026-11-03T04:51:26.562Z",
|
|
63
|
+
"f": 3,
|
|
64
|
+
"prs": [
|
|
65
|
+
{
|
|
66
|
+
"n": 1185,
|
|
67
|
+
"t": "Full task names in the Timeline too",
|
|
68
|
+
"s": "Full task names in the Timeline too",
|
|
69
|
+
"k": "fix"
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
},
|
|
35
73
|
{
|
|
36
74
|
"v": "0.501.0",
|
|
37
75
|
"at": "2026-11-03T04:44:45.129Z",
|
|
@@ -397,32 +435,6 @@
|
|
|
397
435
|
"k": "fix"
|
|
398
436
|
}
|
|
399
437
|
]
|
|
400
|
-
},
|
|
401
|
-
{
|
|
402
|
-
"v": "0.483.0",
|
|
403
|
-
"at": "2026-11-02T00:26:37.780Z",
|
|
404
|
-
"f": 3,
|
|
405
|
-
"prs": [
|
|
406
|
-
{
|
|
407
|
-
"n": 1144,
|
|
408
|
-
"t": "Report building flow \"A+ on Home\" (batch 22 item 14)",
|
|
409
|
-
"s": "Report building flow \"A+ on Home\" (batch 22 item 14)",
|
|
410
|
-
"k": "new"
|
|
411
|
-
}
|
|
412
|
-
]
|
|
413
|
-
},
|
|
414
|
-
{
|
|
415
|
-
"v": "0.482.0",
|
|
416
|
-
"at": "2026-11-01T23:25:19.509Z",
|
|
417
|
-
"f": 3,
|
|
418
|
-
"prs": [
|
|
419
|
-
{
|
|
420
|
-
"n": 1142,
|
|
421
|
-
"t": "PDF takes the Viewer's whole width; a queued chat line leaves once read (batch 22 items 15, 16)",
|
|
422
|
-
"s": "PDF takes the Viewer's whole width; a queued chat line leaves once read (batch 22 items 15, 16)",
|
|
423
|
-
"k": "fix"
|
|
424
|
-
}
|
|
425
|
-
]
|
|
426
438
|
}
|
|
427
439
|
]
|
|
428
440
|
}
|
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-11-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-11-03T05:42:38Z"}
|
|
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-03T05:42:38Z"}
|
|
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-03T05:42:38Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -144,9 +144,10 @@ const json = (r, b, status = 200) => r.fulfill({ status, contentType: 'applicati
|
|
|
144
144
|
const q = B.page;
|
|
145
145
|
await until(() => q.evaluate(() => !!document.querySelector('#czViewer [data-a="maketask"]')), 15000);
|
|
146
146
|
// shell A (XvAReplay): Next steps is a fold; open it so what the press says can be read
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
await
|
|
147
|
+
// batch 25 item 11 (MvC): Next steps is a part of the one strip under the kept points; open it so what the press says can be read
|
|
148
|
+
await q.evaluate(() => { const f = document.querySelector('#czViewer .vzr-fold [data-a="strip"][data-k="steps"]'); if (f && f.getAttribute('aria-expanded') !== 'true') f.click(); });
|
|
149
|
+
ok('M4 each next step has Make task, and decisions and questions have none', await until(() => q.evaluate(() => document.querySelectorAll('#czViewer .vzr-panel [data-a="maketask"]').length === 3)));
|
|
150
|
+
await tap(q, '.vzr-panel [data-a="maketask"][data-i="0"]');
|
|
150
151
|
const pl = await until(() => posted(B.S, '/api/plan-items')[0]);
|
|
151
152
|
const ev = pl && pl.body.events && pl.body.events[0];
|
|
152
153
|
ok('M4 Make task posts one planned event in the first stage with the owner', ev && ev.ev === 'planned' && ev.initiative === 'viewer' && ev.stage === 'st1' && ev.owner === 'person:Ana' && ev.title === 'Add a print view for documents' && pl.body.ws === 'w1', JSON.stringify(pl && pl.body));
|
|
@@ -143,10 +143,9 @@ const json = (r, b, status = 200) => r.fulfill({ status, contentType: 'applicati
|
|
|
143
143
|
/* R7 Replay */
|
|
144
144
|
const C = await open('/premium-viewer.html?open=meeting¬e=n1');
|
|
145
145
|
const r = C.page;
|
|
146
|
-
//
|
|
147
|
-
const unfold = async (id) => { if (await r.evaluate((k) => document.querySelector(`#czViewer [data-
|
|
146
|
+
// batch 25 item 11 (MvC): no tabs; the kept points sit left, the timeline is docked, the lines are the Transcript part of the one strip
|
|
147
|
+
const unfold = async (id) => { if (await r.evaluate((k) => document.querySelector(`#czViewer .vzr-fold [data-a="strip"][data-k="${k}"]`).getAttribute('aria-expanded') !== 'true', id)) await tap(r, `.vzr-fold [data-a="strip"][data-k="${id}"]`); };
|
|
148
148
|
ok('R7 a kept recording has the replay card under the summary', await until(() => r.evaluate(() => !!document.querySelector('#czViewer .vzp-card .vzp-pl') && !document.querySelector('#czViewer [role="tablist"]')), 15000));
|
|
149
|
-
await unfold('kept');
|
|
150
149
|
const playing = () => r.evaluate(() => /^Pause/.test((document.querySelector('#czViewer .vzp-dt [data-a="play"]') || { getAttribute: () => '' }).getAttribute('aria-label') || ''));
|
|
151
150
|
ok('R7 Replay shows the player, the strip and the kept points (MtReplay)', await until(() => r.evaluate(() => !!document.querySelector('#czViewer .vzp-dt .vzp-strip') && document.querySelectorAll('#czViewer .vzp-kr').length > 0)));
|
|
152
151
|
await tap(r, '.vzp-dt [data-a="play"]');
|
package/workspace/changelog.json
CHANGED
|
@@ -1,4 +1,42 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"v": "0.503.0",
|
|
4
|
+
"at": "2026-11-03T05:42:41.752Z",
|
|
5
|
+
"f": 3,
|
|
6
|
+
"prs": [
|
|
7
|
+
{
|
|
8
|
+
"n": 1186,
|
|
9
|
+
"t": "A timeline end ◇ names the initiative's own workflow, never a fixed Deliver",
|
|
10
|
+
"s": "A timeline end ◇ names the initiative's own workflow, never a fixed Deliver",
|
|
11
|
+
"k": "fix"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"n": 1187,
|
|
15
|
+
"t": "Working card leads with the todo or turn goal, not the raw command",
|
|
16
|
+
"s": "Working card leads with the todo or turn goal, not the raw command",
|
|
17
|
+
"k": "new"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"n": 1188,
|
|
21
|
+
"t": "New meeting recording view, word fix in place (batch 25 item 11)",
|
|
22
|
+
"s": "New meeting recording view, word fix in place (batch 25 item 11)",
|
|
23
|
+
"k": "new"
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"v": "0.502.0",
|
|
29
|
+
"at": "2026-11-03T04:51:26.562Z",
|
|
30
|
+
"f": 3,
|
|
31
|
+
"prs": [
|
|
32
|
+
{
|
|
33
|
+
"n": 1185,
|
|
34
|
+
"t": "Full task names in the Timeline too",
|
|
35
|
+
"s": "Full task names in the Timeline too",
|
|
36
|
+
"k": "fix"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
},
|
|
2
40
|
{
|
|
3
41
|
"v": "0.501.0",
|
|
4
42
|
"at": "2026-11-03T04:44:45.129Z",
|
|
@@ -83,14 +83,15 @@ function install(page, o = {}) {
|
|
|
83
83
|
page.route('**/api/notes/series?*', (r) => json(r, { ok: true, series: { key: 'viewer-sync', meetings: [{ id: 'n0', at: '2026-10-15T10:00:00Z', decided: 1, steps: 3 }, { id: 'n1', at: W.NOTE.meeting.start, decided: 2, steps: 3 }] } })),
|
|
84
84
|
page.route('**/api/notes/search*', (r) => json(r, { ok: true, ids: ['n4'], hits: [{ id: 'n4', snip: '…which teams use saved views for teams every week…', open: true }] })),
|
|
85
85
|
page.route('**/api/meeting-points/*', (r) => { // a correction and its Undo (the answers lib/meeting-learn.js gives)
|
|
86
|
-
const b = post(r); if (/forget$/.test(new URL(r.request().url()).pathname)) return json(r, { ok: true });
|
|
86
|
+
const b = post(r); if (/forget$/.test(new URL(r.request().url()).pathname)) { S.terms = (S.terms || []).filter((x) => x.id !== b.lesson); return json(r, { ok: true }); }
|
|
87
|
+
if (/term$/.test(new URL(r.request().url()).pathname)) { const t = { id: 't0123456789', note: b.note, heard: b.heard, means: b.means }; S.terms = (S.terms || []).concat([t]); return json(r, { ok: true, term: t, terms: S.terms }); }
|
|
87
88
|
const phrase = require('../lib/meeting-learn.js').phraseOf(b.orig && b.orig.text, b.text);
|
|
88
89
|
S.edits = { ...(S.edits || {}), [b.id]: { kind: b.kind, text: b.text, orig: b.orig } };
|
|
89
90
|
return json(r, { ok: true, edits: S.edits, lesson: b.learn === false ? null : { id: 'l0123456789', phrase, kind: b.kind, meaning: b.text, orig: b.orig }, check: [] });
|
|
90
91
|
}),
|
|
91
92
|
page.route('**/api/meeting-points*', (r) => {
|
|
92
93
|
if (r.request().method() === 'POST') { const b = post(r); (b.ids || []).forEach((id) => { if (b.verdict) S.points[id] = b.verdict; else delete S.points[id]; }); return json(r, { ok: true, points: S.points }); }
|
|
93
|
-
return json(r, { ok: true, points: S.points, edits: S.edits || {}, lessons: [] });
|
|
94
|
+
return json(r, { ok: true, points: S.points, edits: S.edits || {}, lessons: [], terms: S.terms || [] });
|
|
94
95
|
}),
|
|
95
96
|
page.route('**/api/notes/new', (r) => {
|
|
96
97
|
const b = post(r), id = 'new' + (++S.made);
|
|
@@ -331,7 +331,7 @@ Board is the fourth place on the rail, after Chat, at `/premium-board.html`. It
|
|
|
331
331
|
- **The view chip** in the Initiatives head says which view is shown (List, By stage or Timeline); press it to pick another.
|
|
332
332
|
- **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.
|
|
333
333
|
- **By stage** has one column for each stage of the board's workflow with the board's own count and its first five initiatives.
|
|
334
|
-
- **List** and **Timeline** show ten tasks a page with Previous and Next; **By stage** shows six cards a column with Show more. **Timeline** draws a bar for each initiative that has dates, with the days left or over, and counts the rest. A bar runs from the earliest start to the latest due date of the initiative's breakdown items and its tasks. Every stage shows, Monitor and Evaluate too. Under a program, every initiative of the program has a row; one with no dates says **No dates** beside today. A product whose initiatives all have no dates keeps its row too, with each initiative saying **No dates**, so the product is never missing from the Board. In each group the rows run by start date, then end date, and the ones with no dates come last; the status dot still marks a late or at-risk initiative. With no dates on record it says so and draws nothing. A gap in the plan that overlaps a hold still leaves out the days outside the hold, also when the hold has no return date.
|
|
334
|
+
- **List** and **Timeline** show ten tasks a page with Previous and Next; **By stage** shows six cards a column with Show more. **Timeline** draws a bar for each initiative that has dates, with the days left or over, and counts the rest. A bar runs from the earliest start to the latest due date of the initiative's breakdown items and its tasks. Every stage shows, Monitor and Evaluate too. The ◇ at the end of a bar names a dated milestone task of the initiative. When the initiative has no dated milestone task, the ◇ names the ◆ stage of the initiative's own workflow: on Product feature that is Deliver, or Evaluate after Deliver. When the workflow has no ◆ stage, the bar ends with no ◇ and no label. A bar never shows Deliver for a workflow that has no Deliver stage, and never shows Deliver next to a named milestone. Under a program, every initiative of the program has a row; one with no dates says **No dates** beside today. A product whose initiatives all have no dates keeps its row too, with each initiative saying **No dates**, so the product is never missing from the Board. In each group the rows run by start date, then end date, and the ones with no dates come last; the status dot still marks a late or at-risk initiative. With no dates on record it says so and draws nothing. A gap in the plan that overlaps a hold still leaves out the days outside the hold, also when the hold has no return date.
|
|
335
335
|
- **Sprint** appears when an agent session has read Jira for the board. Otherwise it says there is none. Press it to open the sprint's facts, with the sprint as a board in a second tab: **To do**, **In progress** and **Done** columns from the last Jira read, with the dates, the days left and the points. The sprint goal is the one the Jira status check writes from the sprint's tickets, as on the console's Sprints tab, with its summary; Jira's own goal shows under it only as a reference (**In Jira: "…"**). Until the check has written one, the tab says **No goal yet**. The active and next sprints are buttons at the top. The closed sprints are behind **Past sprints**, which is shut until you press it. Press any card to open its ticket in another tab. A ticket that is on no initiative of this board shows what the sprint read holds (title, status, points and its Jira link). The board is read only: Jira owns each ticket's status and sprint, so you move tickets in Jira. Tickets with no sprint are not in this read. **Coming up** shows the next release, from the releases ledger.
|
|
336
336
|
- **Open the backlog** is in the Sprint dialog and on its sprint tab. It opens the board's Jira backlog in a tab beside the dialog: the open sprints (active, then next) and then **Backlog**, the tickets in no sprint and not done. A row shows the key, title, status, points and initiative, and why a blocked ticket is blocked. **All**, **Blocked** and **Bugs** filter it (Bugs appears only when the Jira read holds the issue type). A row opens its ticket. The list holds only the tickets the board's initiatives pushed to Jira or hold under their epics, in key order: the Jira read has no rank, assignee or capacity. You move and rank tickets in Jira. With no Jira read, the tab says so.
|
|
337
337
|
- **Next release** shows the next release that carries an initiative of this board, the one planned after it, and the three latest past releases (**Released** when accepted, else **Past**). It reads the release records (`releases/releases.jsonl`, written from the console's Releases view); Jira fix versions alone do not make a release. With no release planned it says so and still lists the past ones. With none at all it says "No release is on record for this board, past or planned."
|
package/workspace/guide/home.md
CHANGED
|
@@ -103,7 +103,7 @@ Press an initiative's name in the list, or open `?ini=<id>`. The list stays as i
|
|
|
103
103
|
- **Set a Jira ticket's dates.** Press the dates of a Jira ticket in the List, or drag its bar on the Timeline, as you do for a task. PMOS keeps the start and due days and shows them in place of the sprint end. Jira does not change. The status and the name stay Jira's.
|
|
104
104
|
- **Tasks** show one row (one group) for each stage of the workflow, in order. Finished stages and later stages are never merged into one row; they start folded, and the current stage and any stage with a late task start open.
|
|
105
105
|
- **Change a task where it is.** In the List, press a task's name. The row opens in place: change the name (Enter saves), the status, and the dates. Only one row is open at a time. Esc closes it. The ⋯ after the dates has **Milestone** (or **Make it a plain task**), **Move to a stage**, **Open the task card** and **Delete task**. A milestone has one date: its due day. Each change shows **Undo**.
|
|
106
|
-
- In the List, a task's full name always shows: a long name wraps onto more lines. A short prefix such as "Bus shelter:" stays in the name, in grey. The dates show at the right. In a narrow panel or on a phone, the dates show under the name. A task with no dates shows no date; open the row to set them.
|
|
106
|
+
- In the List, a task's full name always shows: a long name wraps onto more lines. A short prefix such as "Bus shelter:" stays in the name, in grey. The dates show at the right. In a narrow panel or on a phone, the dates show under the name. On a computer, the Timeline's name column wraps a long name too, and its row grows; on a phone, each title stays on one line above its bar. A task with no dates shows no date; open the row to set them.
|
|
107
107
|
- **Add a task to a stage.** Press **+** on an open stage. The field opens at the top of that stage, with one row of chips under it. **Stage** is the stage you pressed + on; press it to pick another stage of the workflow. **Task** can change to **Milestone**: the box becomes ◇ and **Date** takes one day. **Dates** and **Owner** are optional; with no owner, the task is yours. Enter adds the task and keeps the field open for the next one. Esc closes the field.
|
|
108
108
|
- **Move a task between stages.** On a computer, drag the grip at the left of a row. On a phone, press and hold the task's name, then drag. Drop it on a stage name to put it last in that stage, or between two rows to put it there. The status and the dates do not change. The message shows **Undo**.
|
|
109
109
|
- A linked Jira ticket opens in place too. Its name and status stay Jira's ("In Progress in Jira · read 4 min ago"). Its dates can change, and it can be a milestone. It has no **Delete**.
|
|
@@ -188,6 +188,7 @@ There is no My work page. The list on Home is your list:
|
|
|
188
188
|
- **Initiative.** The Board opens with **New initiative**, in the open product.
|
|
189
189
|
- **Sessions.** A session row names its agent. The coloured dot beside it is its state: blue waits on you, amber may be blocked, green is running. A session PMOS started itself (a skill or a background job) shows on Home only while it works or asks you something. Once it finishes its prompt, it leaves the list. On an initiative's page, a session that waits on you shows once, under **Needs you**, not again under **Sessions**. **Finished sessions** does not list an old run of a chat that is open again.
|
|
190
190
|
- **One chat box.** Every conversation has the same box: **+** (attach a file, notes, the terminal, and **Link to an initiative** for a loose chat), **/** for commands, **@** inside an initiative, the To or In chip, the mic, and Send. A chat opens at its newest message.
|
|
191
|
+
- **While Claude works.** A line at the top of the chat shows what Claude is trying to do. If Claude keeps a to-do list, the line shows the to-do in progress and how many are done, for example **2 of 5 done**. Tap the line to see the full list. If Claude has no to-do list, the line shows Claude's newest one-line status and the time since your message. The line never shows raw commands. To see the commands, tap the terminal icon.
|
|
191
192
|
- **A session's terminal.** **Watch its terminal** in **+**, or a session row such as **Claude sign-in**, opens the session's terminal in the Viewer. While the session runs, it is a real terminal: type in it to answer Claude or paste a code. When the session has ended, the Viewer shows it read only and says so.
|
|
192
193
|
- **Setting up your first project opens by itself** until a registered folder holds an initiative or setup has finished. The `PMOS` folder that the one-line installer makes holds no initiative yet, so a new user who opens PMOS from the Start menu or the Dock still gets the setup conversation, not an empty Home.
|
|
193
194
|
- **Signing in to Claude when you set up your first project.** If the login you choose is signed in already, PMOS does not open a terminal. It tests the login at once. Otherwise PMOS runs `claude auth login`: Claude opens your browser to sign in, and the terminal under the question shows only Claude's request for a code, if it needs one. Claude's theme and trust-this-folder screens do not appear. When the sign-in ends, PMOS tests the login by itself. **The Claude login on this computer** is the login your usual terminal uses (`~/.claude.json`); PMOS no longer points it at a different file. You choose between two logins only: **The Claude login on this computer** and **A separate login just for PMOS**. The test passes for a login that works in your terminal. Claude updates its own files when it runs, and the test does not count that as a changed setup.
|
package/workspace/guide/notes.md
CHANGED
|
@@ -238,9 +238,29 @@ Press **Replay** beside the player (in Calendar › Notes, or on the meeting's c
|
|
|
238
238
|
|
|
239
239
|
The page reads only records on this machine: the note, the file Focus kept for the meeting, and the screen shots.
|
|
240
240
|
|
|
241
|
+
### A recorded meeting in the Viewer
|
|
242
|
+
|
|
243
|
+
A meeting with a recording opens wide, in two columns. Press **Back to the side** in the header to make it narrow again: the columns then stack.
|
|
244
|
+
|
|
245
|
+
- **Left:** the summary (two lines; press **Read it all** for the rest), then the kept points. Each point is one line. The icon shows its kind: a green dot is a decision, a box is a next step, a question mark is an open question. The header counts each kind. A next step said within two minutes after a decision, in the same topic, shows under that decision, with **Make a task**.
|
|
246
|
+
- **Confirm:** press **✓** on a row to confirm it (press it again to undo). **Confirm N** in the header confirms every point that is left.
|
|
247
|
+
- **Right:** press a row to see its evidence: the screen at that moment, the quote, and the screen captures near it. Press **▶** to play from the point. The pen opens the edit card. **Not right** moves the point to **Left out**.
|
|
248
|
+
- **Under the points:** one row of parts. Press one to open it: **Left out**, **Next steps**, **Since the last one**, **Facts mentioned**, **Names and terms**, **Prep and material** and **Transcript**.
|
|
249
|
+
- **Bottom:** the timeline stays in view, with the full name of each chapter and a dot for each kept point.
|
|
250
|
+
|
|
251
|
+
The captures row shows every kept screen capture. It does not yet show only the shared screen: that needs Jev for Images, which PMOS does not have yet.
|
|
252
|
+
|
|
253
|
+
### Fix a word that was heard wrong
|
|
254
|
+
|
|
255
|
+
In the summary, a quote, **Left out** or the transcript, select the word (double-click it, or drag over up to four words). The word becomes a field. Type the right spelling and press **✓** or Enter. The word shows fixed in every place this meeting shows it, and the message says how many places. **Undo** puts it back. **Names and terms** lists the people of the invite and the words you fixed.
|
|
256
|
+
|
|
257
|
+
Later notes spell the word your way: the fix goes to your agent with the notes it writes. In other meetings, the same heard words are underlined. Press one to fix it there too, with your fix already typed.
|
|
258
|
+
|
|
259
|
+
The note's own files do not change. The fix shows where PMOS draws the note.
|
|
260
|
+
|
|
241
261
|
### Fix or delete a key point
|
|
242
262
|
|
|
243
|
-
In a meeting's Viewer, press a decision, a question or an action to open its edit card. Change the kind or the words, then press **Save**. PMOS remembers what you meant for the next notes, unless you press **Only fix this one**.
|
|
263
|
+
In a meeting's Viewer, press a decision, a question or an action, then press the pen to open its edit card. **Not right** is on the edit card too. Change the kind or the words, then press **Save**. PMOS remembers what you meant for the next notes, unless you press **Only fix this one**.
|
|
244
264
|
|
|
245
265
|
To delete the point, press the trash on the edit card. The point leaves at once, with no question first. **Undo** shows for 8 seconds and puts it back. A deleted point stays deleted after a reload. It is not in **Left out** and it is not counted. A delete does not teach PMOS anything, and nothing is sent outside PMOS.
|
|
246
266
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* meeting-terms.js — a word heard wrong, fixed in place (batch 25 item 11, "Meeting recording view C": MvF, MvN).
|
|
3
|
+
*
|
|
4
|
+
* The PM selects a word in a meeting's summary, a quote or the transcript ("More engaged") and types what it is ("MoEngage"). It is kept
|
|
5
|
+
* as one of the lessons of lib/meeting-learn.js, of kind 'term': its phrase is the heard words, its meaning is the fix. So, with no new
|
|
6
|
+
* record, the agent's handoff already carries it for later notes (meeting-learn.js handoffLines: "“more engaged”: MoEngage") and its
|
|
7
|
+
* Undo is the lesson's forget. The page shows the fix in every place this note shows the words, and underlines the heard words in other
|
|
8
|
+
* notes with the fix as the guess. The note's own files are NOT rewritten: rewriting every stored copy of a word is not proven.
|
|
9
|
+
*
|
|
10
|
+
* term(dir, { note, heard, means, now }) → { ok, term, terms } | { ok: false, status, error }
|
|
11
|
+
* terms(dir) → [{ id, heard, means, note, at }] the word fixes that hold now; the latest fix of the same words in a note wins
|
|
12
|
+
* (forget: lib/meeting-learn.js forget({ lesson: <term id> }))
|
|
13
|
+
* One line of <runtime>/meeting-lessons.jsonl per fix, append-only, file mode. Nothing here sends anything. */
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const crypto = require('crypto');
|
|
17
|
+
const ML = require('./meeting-learn.js');
|
|
18
|
+
|
|
19
|
+
const NOTE_ID = /^[A-Za-z0-9_-]{1,80}$/;
|
|
20
|
+
const MAX_WORD = 60;
|
|
21
|
+
const clean = (s) => String(s == null ? '' : s).replace(/\s+/g, ' ').trim();
|
|
22
|
+
/** The heard words as a lesson's phrase: lower case, the same words meeting-learn.js mentions() matches. */
|
|
23
|
+
const phraseOf = (s) => (clean(s).toLowerCase().match(/[\p{L}\p{N}][\p{L}\p{N}.'-]*/gu) || []).map((w) => w.replace(/[.'-]+$/, '')).filter(Boolean).join(' ');
|
|
24
|
+
|
|
25
|
+
function terms(dir) {
|
|
26
|
+
const out = [];
|
|
27
|
+
for (const l of ML.lessons(dir)) {
|
|
28
|
+
if (l.kind !== 'term' || !l.phrase || !l.meaning) continue;
|
|
29
|
+
const i = out.findIndex((x) => x.note === l.note && x.heard === l.phrase);
|
|
30
|
+
if (i >= 0) out.splice(i, 1);
|
|
31
|
+
out.push({ id: l.id, heard: l.phrase, means: l.meaning, note: l.note, at: l.at });
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function term(dir, body = {}) {
|
|
37
|
+
if (!dir) return { ok: false, status: 500, error: 'no runtime folder' };
|
|
38
|
+
const note = String(body.note || '');
|
|
39
|
+
if (!NOTE_ID.test(note)) return { ok: false, status: 400, error: 'note is the id of a meeting note' };
|
|
40
|
+
const raw = clean(body.heard), means = clean(body.means);
|
|
41
|
+
if (!raw || !means) return { ok: false, status: 400, error: 'heard and means are the words before and after the fix' };
|
|
42
|
+
if (raw.length > MAX_WORD || means.length > MAX_WORD) return { ok: false, status: 400, error: 'a word fix is a name or a term of at most ' + MAX_WORD + ' characters' };
|
|
43
|
+
const heard = phraseOf(raw);
|
|
44
|
+
if (!heard) return { ok: false, status: 400, error: 'heard names no word' };
|
|
45
|
+
if (heard === means) return { ok: false, status: 400, error: 'the fix is the same as what was heard' };
|
|
46
|
+
const at = new Date(body.now || Date.now()).toISOString();
|
|
47
|
+
const id = 'l' + crypto.createHash('sha1').update('term|' + note + '|' + heard + '|' + means + '|' + at).digest('hex').slice(0, 10);
|
|
48
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
49
|
+
fs.appendFileSync(path.join(dir, ML.FILE), JSON.stringify({ v: 1, t: 'lesson', id, phrase: heard, kind: 'term', meaning: means, orig: null, note, point: '', at }) + '\n');
|
|
50
|
+
return { ok: true, term: { id, heard, means, note, at }, terms: terms(dir) };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { term, terms, MAX_WORD };
|
|
@@ -7,10 +7,13 @@
|
|
|
7
7
|
* The GET also answers { edits, lessons }: the PM's corrections to the note's points and what PMOS learned (lib/meeting-learn.js).
|
|
8
8
|
* POST /api/meeting-points/edit { note, id, kind, text, orig: { kind, text }, learn, items: [{ id, text }] } → { ok, edits, lesson, check }
|
|
9
9
|
* a corrected point (XvAReplayEdit). learn false is "Only fix this one". check: the other points of the note that use the learned words.
|
|
10
|
-
* POST /api/meeting-points/forget { lesson } → { ok } Undo of what PMOS learned
|
|
10
|
+
* POST /api/meeting-points/forget { lesson } → { ok } Undo of what PMOS learned, and of a word fix (its term id)
|
|
11
|
+
* POST /api/meeting-points/term { note, heard, means } → { ok, term, terms } a word fixed in place, in every place the note shows it
|
|
12
|
+
* The GET answers { terms } too: the word fixes that hold now (lib/meeting-terms.js).
|
|
11
13
|
* The POSTs are page presses (lib/pm-guard.js requirePagePress). Handlers are pure over (q | body, ctx) so a test needs no server. */
|
|
12
14
|
const MP = require('./meeting-points.js');
|
|
13
15
|
const ML = require('./meeting-learn.js');
|
|
16
|
+
const MT = require('./meeting-terms.js');
|
|
14
17
|
const guard = require('./pm-guard.js');
|
|
15
18
|
|
|
16
19
|
const fail = (status, error) => ({ status, body: { ok: false, error } });
|
|
@@ -18,12 +21,12 @@ const fail = (status, error) => ({ status, body: { ok: false, error } });
|
|
|
18
21
|
function read(q, ctx) {
|
|
19
22
|
if (!/^[A-Za-z0-9_-]{1,80}$/.test(String(q.note || ''))) return fail(400, 'note is the id of a meeting note');
|
|
20
23
|
const note = String(q.note);
|
|
21
|
-
return { status: 200, body: { ok: true, points: MP.pointsOf(ctx.runtimeDir, note), edits: ML.editsOf(ctx.runtimeDir, note), lessons: ML.lessons(ctx.runtimeDir).filter((x) => x.phrase).slice(-30) } };
|
|
24
|
+
return { status: 200, body: { ok: true, points: MP.pointsOf(ctx.runtimeDir, note), edits: ML.editsOf(ctx.runtimeDir, note), lessons: ML.lessons(ctx.runtimeDir).filter((x) => x.phrase && x.kind !== 'term').slice(-30), terms: MT.terms(ctx.runtimeDir).slice(-200) } };
|
|
22
25
|
}
|
|
23
26
|
function learn(req, body, ctx, op) {
|
|
24
27
|
const g = guard.requirePagePress(req, body);
|
|
25
28
|
if (!g.ok) return fail(g.status, g.error);
|
|
26
|
-
const r = op === 'forget' ? ML.forget(ctx.runtimeDir, body) : ML.edit(ctx.runtimeDir, body);
|
|
29
|
+
const r = op === 'forget' ? ML.forget(ctx.runtimeDir, body) : op === 'term' ? MT.term(ctx.runtimeDir, body) : ML.edit(ctx.runtimeDir, body);
|
|
27
30
|
if (!r.ok) return fail(r.status || 400, r.error);
|
|
28
31
|
return { status: 200, body: r };
|
|
29
32
|
}
|
|
@@ -40,7 +43,7 @@ module.exports = {
|
|
|
40
43
|
prefixes: ['/api/meeting-points'],
|
|
41
44
|
read, write, learn,
|
|
42
45
|
handle: async (req, res, url, ctx) => {
|
|
43
|
-
const op = url.pathname === '/api/meeting-points/edit' ? 'edit' : url.pathname === '/api/meeting-points/forget' ? 'forget' : '';
|
|
46
|
+
const op = url.pathname === '/api/meeting-points/edit' ? 'edit' : url.pathname === '/api/meeting-points/forget' ? 'forget' : url.pathname === '/api/meeting-points/term' ? 'term' : '';
|
|
44
47
|
if (op) {
|
|
45
48
|
if (req.method !== 'POST') return respond(ctx, res, fail(405, 'POST only'));
|
|
46
49
|
const got = await ctx.readJson(req);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.503.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.503.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@anthropic-ai/sdk": "^1.3.0",
|
package/workspace/package.json
CHANGED
|
@@ -99,8 +99,8 @@ const look = () => {
|
|
|
99
99
|
if (shots) await p.screenshot({ path: `${shots}/line-390.png` });
|
|
100
100
|
await p.click('#hmPl [data-pl="toggle"]');
|
|
101
101
|
const s1 = await until(async () => { const v = await p.evaluate(look); return v.open ? v : null; }) || await p.evaluate(look);
|
|
102
|
-
ok('P2 a tap opens it in place: the goal, "1 of 4 points met · round 2 of 12", every point, Now and the terminal',
|
|
103
|
-
/Bring the descope-feature-parity PRD/.test(s1.line || '') && /1 of 4 points met · round 2 of 12/.test(s1.line || '') && /Ready for Dita's review/.test(s1.line || '') && /Now\s*Edit · planning
|
|
102
|
+
ok('P2 a tap opens it in place: the goal, "1 of 4 points met · round 2 of 12", every point, Now in plain words (never the raw call) and the terminal',
|
|
103
|
+
/Bring the descope-feature-parity PRD/.test(s1.line || '') && /1 of 4 points met · round 2 of 12/.test(s1.line || '') && /Ready for Dita's review/.test(s1.line || '') && /Now\s*SSO goes on the out-of-scope list/.test(s1.line || '') && !/Edit · planning/.test(s1.line || '') && s1.term && !s1.vw, JSON.stringify(s1));
|
|
104
104
|
if (shots) await p.screenshot({ path: `${shots}/line-390-open.png` });
|
|
105
105
|
await p.click('#hmPl [data-pl="toggle"]');
|
|
106
106
|
await p.evaluate(() => { const se = document.scrollingElement; se.scrollTop = 0; });
|
|
@@ -117,7 +117,7 @@ const look = () => {
|
|
|
117
117
|
{ const st = { conv: { turn: 'agent', messages: BASE } };
|
|
118
118
|
const { p, ctx, errs } = await open(390, st);
|
|
119
119
|
const s0 = await until(async () => { const v = await p.evaluate(look); return v.line ? v : null; }) || await p.evaluate(look);
|
|
120
|
-
ok('P3 a session with no step list:
|
|
120
|
+
ok('P3 a session with no step list: Claude\'s newest status line and the clock, never the raw call or an invented count (batch 25 item 14)', /SSO goes on the out-of-scope list; the date stays\.\s*\d+:\d\d/.test(s0.line || '') && !/Edit · planning/.test(s0.line || '') && !/ of \d+ done/.test(s0.line || ''), JSON.stringify(s0));
|
|
121
121
|
st.conv = { turn: 'agent', messages: BASE, steps: [{ text: 'Read the funnel export', state: 'done' }, { text: 'Find the step that moved', state: 'now', call: 'Read · funnel.csv' }, { text: 'Write it up', state: 'next' }] };
|
|
122
122
|
const s1 = await until(async () => { const v = await p.evaluate(look); return /Find the step/.test(v.line || '') ? v : null; }) || await p.evaluate(look);
|
|
123
123
|
ok('P3 a session with its own steps: its step and "1 of 3 done"', /Find the step that moved/.test(s1.line || '') && /1 of 3 done/.test(s1.line || ''), JSON.stringify(s1));
|
|
@@ -43,7 +43,7 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
43
43
|
const press = (p, sel) => p.$eval('#czViewer ' + sel, (b) => b.click());
|
|
44
44
|
const folds = (p) => p.evaluate(() => [...document.querySelectorAll('#czViewer [data-czs-fold]')].map((x) => x.dataset.czsFold + (x.getAttribute('aria-expanded') === 'true' ? '*' : '')).join());
|
|
45
45
|
const unfold = async (p, id) => { if (await p.evaluate((k) => document.querySelector(`#czViewer [data-czs-fold="${k}"]`).getAttribute('aria-expanded') !== 'true', id)) await press(p, `[data-czs-fold="${id}"]`); };
|
|
46
|
-
const sum = (p) => p.evaluate(() => { const s = document.querySelector('#czViewer .czs-sum'); return s ? { line: s.querySelector('.czs-line').innerText, text: (s.querySelector('.czs-p') || {}).innerText || '', meta: (s.querySelector('.czs-meta') || {}).innerText || '', pri: (s.querySelector('[data-czs-act]') || {}).textContent || '' } : null; });
|
|
46
|
+
const sum = (p) => p.evaluate(() => { const s = [...document.querySelectorAll('#czViewer .czs-sum')].find((x) => x.offsetParent) || document.querySelector('#czViewer .czs-sum'); return s ? { line: (s.querySelector('.czs-line') || {}).innerText || '', text: (s.querySelector('.czs-p') || {}).innerText || '', meta: (s.querySelector('.czs-meta') || {}).innerText || '', pri: (s.querySelector('[data-czs-act]') || {}).textContent || '' } : null; });
|
|
47
47
|
|
|
48
48
|
/* an ended meeting with notes and a recording */
|
|
49
49
|
const A = await F.open(browser, BASE, '/premium-viewer.html?open=meeting¬e=n1');
|
|
@@ -54,9 +54,12 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
54
54
|
|
|
55
55
|
/* S1 summary first */
|
|
56
56
|
const s1 = await sum(p);
|
|
57
|
-
|
|
58
|
-
ok('S1 the summary
|
|
59
|
-
ok('S1
|
|
57
|
+
/* batch 25 item 11 (MvC): the summary first in the left column, the kept points with Confirm N in their header, the evidence, one strip, the docked timeline */
|
|
58
|
+
ok('S1 the summary: the overview and the meta line, no primary (Confirm sits in the kept header)', /opens next to the chat/.test(s1.text) && !s1.pri, JSON.stringify(s1));
|
|
59
|
+
ok('S1 the left column starts with the summary, then the kept points: "N kept" and Confirm N; the evidence and the docked timeline', await p.evaluate(() => { const l = document.querySelector('#czViewer .vzr-l'); return !!l && l.firstElementChild.classList.contains('czs-sum') && /^\d+ kept$/.test(l.querySelector('.vzr-ph h3').textContent) && /^Confirm \d+$/.test(l.querySelector('[data-a="confirmall"]').textContent) && !!document.querySelector('#czViewer .vzr-ev') && !!document.querySelector('#czViewer .vzp-card .vzr-dock.vzp-pl'); }));
|
|
60
|
+
const chips = await p.evaluate(() => [...document.querySelectorAll('#czViewer .vzr-fold [data-a="strip"]')].map((x) => x.dataset.k).join());
|
|
61
|
+
ok('S1 one strip: Left out, Next steps, Since the last one, Facts, Names and terms, Prep and material, Transcript; no tabs, no checkbox, no filter', chips === 'left,steps,since,facts,names,prep,tr' && !(await p.$('#czViewer [role="tablist"]')) && !(await p.$('#czViewer .vzr [role="checkbox"]')) && !(await p.$('#czViewer [data-a="pf"]')), chips);
|
|
62
|
+
ok('S1 the Viewer opens wide for the two columns', await p.evaluate(() => document.getElementById('czViewer').offsetWidth >= 900 && getComputedStyle(document.querySelector('#czViewer .vzr-cols')).gridTemplateColumns.split(' ').length === 2));
|
|
60
63
|
ok('S1 no Detail or Conversation switch, no #dlg; the note was read from GET /api/notes/one', await p.evaluate(() => !document.querySelector('#dlg, #dTabs, .stream')) && A.S.gets.some((g) => g.startsWith('/api/notes/one?id=n1')));
|
|
61
64
|
|
|
62
65
|
/* S2 the header and ⋯ */
|
|
@@ -70,17 +73,17 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
70
73
|
ok('S2 Copy as Markdown copies the notes', /^# Viewer sync/.test(clip || '') && /Diffs can be approved/.test(clip || ''), String(clip).slice(0, 200));
|
|
71
74
|
|
|
72
75
|
/* S3 After */
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
ok('S3 Since the last one and Facts mentioned, Matches and Not checked', /Raised again/.test(
|
|
76
|
-
ok('S3 the
|
|
76
|
+
const strip = (k) => press(p, `.vzr-fold [data-a="strip"][data-k="${k}"]`);
|
|
77
|
+
await strip('since'); const since = await body(p); await strip('facts'); const facts = await body(p);
|
|
78
|
+
ok('S3 Since the last one and Facts mentioned, Matches and Not checked', /Raised again/.test(since) && /Matches/.test(facts) && /Not checked/.test(facts), facts.slice(0, 600));
|
|
79
|
+
ok('S3 the evidence has the screen captures, with no Waiting for design marker', await until(() => p.evaluate(() => document.querySelectorAll('#czViewer .vzp-card .vzp-th').length > 0 && !document.querySelector('#czViewer [data-cz-placeholder]'))));
|
|
77
80
|
await press(p, '.vzp-card .vzp-th[data-a="seekto"]');
|
|
78
81
|
ok('S3 a thumbnail moves the replay to its time', await until(() => p.evaluate(() => /^03:10/.test((document.querySelector('#czViewer [data-clk="pos"]') || {}).textContent || ''))), await p.evaluate(() => (document.querySelector('#czViewer [data-clk="pos"]') || {}).textContent));
|
|
79
|
-
await
|
|
80
|
-
ok('S3 the Transcript
|
|
82
|
+
await strip('tr');
|
|
83
|
+
ok('S3 the Transcript in the strip shows the lines, Dita named', await until(async () => /The Viewer sits beside the chat now/.test(await body(p)) && /Dita/.test(await body(p))));
|
|
81
84
|
|
|
82
85
|
/* S4 Make task: a work-item becomes V.4 Drafted in PMOS */
|
|
83
|
-
await
|
|
86
|
+
await strip('steps');
|
|
84
87
|
await until(() => p.evaluate(() => !!document.querySelector('#czViewer [data-a="makewi"][data-i="0"]')));
|
|
85
88
|
ok('S4 each step offers Make task and an icon-only work-item press', await p.evaluate(() => !!document.querySelector('#czViewer [data-a="maketask"][data-i="0"]') && !document.querySelector('#czViewer [data-a="makewi"][data-i="0"]').textContent.trim()));
|
|
86
89
|
await press(p, '[data-a="makewi"][data-i="0"]');
|
|
@@ -90,9 +93,10 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
90
93
|
ok('S4 Make task writes one plan item for the step', await until(() => A.S.posts.some((x) => x.path === '/api/plan-items' && JSON.stringify(x.body).includes('Move saved views'))));
|
|
91
94
|
|
|
92
95
|
/* S5 a correction, and what PMOS learns (XvAReplayEdit) */
|
|
93
|
-
await
|
|
94
|
-
|
|
95
|
-
|
|
96
|
+
const pid = await p.evaluate(() => { const r = document.querySelectorAll('#czViewer .vzr-row [data-a="psel"]')[1]; r.click(); return r.dataset.id; });
|
|
97
|
+
ok('S5 pressing a row selects it and the evidence follows it: its time, its quote', await until(() => p.evaluate((id) => !!document.querySelector(`#czViewer .vzr-row.sel [data-a="psel"][data-id="${id}"]`) && !!document.querySelector(`#czViewer .vzr-ev [data-a="pedit"][data-id="${id}"]`), pid)), pid);
|
|
98
|
+
await press(p, '.vzr-ev [data-a="pedit"]');
|
|
99
|
+
ok('S5 Fix in the evidence opens the editor in place: the kinds, the words, what PMOS will remember', !!pid && await until(() => p.evaluate(() => !!document.querySelector('#czViewer .vzp-ed [data-a="etext"]') && document.querySelectorAll('#czViewer .vzp-ed [data-a="ekind"]').length === 3 && document.querySelector('#czViewer .vzp-ed [data-a="esave"]').disabled)), pid);
|
|
96
100
|
const kNow = await p.evaluate(() => document.querySelector('#czViewer .vzp-ed [data-a="ekind"][aria-checked="true"]').dataset.k);
|
|
97
101
|
const kNew = kNow === 'question' ? 'action' : 'question';
|
|
98
102
|
await press(p, `.vzp-ed [data-a="ekind"][data-k="${kNew}"]`);
|
|
@@ -103,15 +107,22 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
103
107
|
ok('S5 the page says Fixed and what it will remember, with Undo', await until(async () => /Fixed\./.test(await body(p)) && !!(await p.$('#czViewer .vz-msg [data-a="msgundo"]'))), (await body(p)).slice(0, 300));
|
|
104
108
|
await press(p, '.vz-msg [data-a="msgundo"]');
|
|
105
109
|
ok('S5 Undo forgets the lesson (POST /api/meeting-points/forget)', await until(() => A.S.posts.some((x) => x.path === '/api/meeting-points/forget' && x.body.lesson === 'l0123456789')), JSON.stringify(A.S.posts.map((x) => x.path)));
|
|
106
|
-
|
|
107
|
-
|
|
110
|
+
/* MvF: a word heard wrong, selected in the quote, fixed in every place; Undo forgets it */
|
|
111
|
+
await p.evaluate(() => { const q = document.querySelector('#czViewer .vzr-ev .vzr-q'); const tn = [...q.childNodes].find((x) => x.nodeType === 3 && /print/.test(x.textContent)) || q.firstChild; const i = tn.textContent.indexOf('print'); const r = document.createRange(); r.setStart(tn, i); r.setEnd(tn, i + 5); const s = getSelection(); s.removeAllRanges(); s.addRange(r); q.dispatchEvent(new MouseEvent('mouseup', { bubbles: true })); });
|
|
112
|
+
ok('S5 a selected word becomes a field with ✓', await until(() => p.evaluate(() => { const f = document.querySelector('#czViewer .vzr-fe'); return !!f && f.dataset.w === 'print' && document.activeElement === f.querySelector('input'); })));
|
|
113
|
+
await p.fill('#czViewer .vzr-fe input', 'Print'); await p.press('#czViewer .vzr-fe input', 'Enter');
|
|
114
|
+
ok('S5 Enter posts the word fix and it shows fixed, with Undo', await until(() => A.S.posts.some((x) => x.path === '/api/meeting-points/term' && x.body.heard === 'print' && x.body.means === 'Print')) && await until(async () => !!(await p.$('#czViewer .vzr-fx')) && /Fixed/.test(await body(p)) && !!(await p.$('#czViewer .vz-msg [data-a="msgundo"]'))));
|
|
115
|
+
await press(p, '.vz-msg [data-a="msgundo"]');
|
|
116
|
+
ok('S5 Undo forgets the word fix', await until(() => A.S.posts.some((x) => x.path === '/api/meeting-points/forget' && x.body.lesson === 't0123456789')) && await until(async () => !(await p.$('#czViewer .vzr-fx'))));
|
|
117
|
+
await press(p, '[data-a="confirmall"]');
|
|
118
|
+
ok('S5 Confirm N in the kept header confirms what is left to confirm', await until(() => A.S.posts.some((x) => x.path === '/api/meeting-points' && x.body.verdict === 'confirmed' && x.body.ids.length > 0)), JSON.stringify(A.S.posts.filter((x) => x.path === '/api/meeting-points')));
|
|
108
119
|
|
|
109
120
|
/* S8 series membership */
|
|
110
121
|
await F.more(p, 'Not part of this series');
|
|
111
122
|
ok('S8 ⋯ Not part of this series posts key off', await until(() => A.S.posts.some((x) => x.path === '/api/notes/series-member' && x.body.key === 'off')));
|
|
112
123
|
|
|
113
124
|
/* S9 writes: only the presses */
|
|
114
|
-
const allowed = ['/api/viewer-meeting/work-item', '/api/plan-items', '/api/notes/series-member', '/api/meeting-points/edit', '/api/meeting-points/forget', '/api/meeting-points'];
|
|
125
|
+
const allowed = ['/api/meeting-points/term', '/api/viewer-meeting/work-item', '/api/plan-items', '/api/notes/series-member', '/api/meeting-points/edit', '/api/meeting-points/forget', '/api/meeting-points'];
|
|
115
126
|
ok('S9 the page wrote only what was pressed', A.S.posts.every((x) => allowed.includes(x.path)), JSON.stringify(A.S.posts.map((x) => x.path)));
|
|
116
127
|
ok('S9 no page error', A.errors.length === 0, A.errors.join(' | '));
|
|
117
128
|
await A.ctx.close();
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* · On a narrow Board the columns drop from right to left until the timeline keeps MIN_TRACK px. A phone has no columns (StsA3Phone).
|
|
11
11
|
* Pure markup and state; pm/bd-span.js draws with it, pm/premium-board.js wires the menu and the sort. */
|
|
12
12
|
import { esc, store } from './premium-shell.js';
|
|
13
|
+
import { endStage } from './bd-pins.js';
|
|
13
14
|
import { stageOfRow, wfOf, wfIcon, catOfRow, catLabel, CAT_ORDER } from './bd-wf.js';
|
|
14
15
|
import { offTone, md, dy } from './bd-span.js';
|
|
15
16
|
|
|
@@ -58,13 +59,15 @@ export function setSort(k) { sortBy = byK[k] ? k : null; }
|
|
|
58
59
|
const midnight = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
59
60
|
const day = (v) => { const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(v || '')); return m ? new Date(+m[1], +m[2] - 1, +m[3]) : null; };
|
|
60
61
|
/** A row's next milestone: the earliest milestone not done (a missed one first, it is the earliest), else the end the row stands for
|
|
61
|
-
* (Deliver, or Evaluate past it). → { name, d, late, over } or null. over: days past the date. */
|
|
62
|
+
* (its workflow's ◆ stage: Deliver, or Evaluate past it, on Product feature; none on a workflow with no ◆ stage). → { name, d, late, over } or null. over: days past the date. */
|
|
62
63
|
export function nextMs(r, now = new Date()) {
|
|
63
64
|
const t = midnight(now);
|
|
64
65
|
const M = (r.milestones || []).filter((m) => !m.done && day(m.date)).sort((a, b) => day(a.date) - day(b.date));
|
|
65
66
|
let x = M.length ? { name: M[0].name, d: day(M[0].date) } : null;
|
|
66
67
|
const k = r.track ? r.track.k : -1;
|
|
67
|
-
|
|
68
|
+
const es = endStage(r);
|
|
69
|
+
// a row with dated milestones is named by them (batch 25 item 13: never Deliver beside a named milestone); the plain end only without any
|
|
70
|
+
if (!x && es && !(r.milestones || []).some((m) => day(m.date)) && day(r.end) && k < 8 && !(r.track && r.track.prog && r.track.prog.total > 0 && r.track.prog.done >= r.track.prog.total)) x = { name: es, d: day(r.end) };
|
|
68
71
|
if (!x) return null;
|
|
69
72
|
const over = Math.round((t - x.d) / 864e5);
|
|
70
73
|
return { ...x, late: over > 0, over: Math.max(0, over) };
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* · A phone shows the next 3 above the rows (missed first), with "All".
|
|
13
13
|
* · Nothing to show (Filter = None, nothing pinned): no lane at all.
|
|
14
14
|
* Pure apart from the remembered fold and wire(), which hooks the hover and the fold onto the Board's body. */
|
|
15
|
-
import { shownMs, msMode } from './bd-pins.js';
|
|
15
|
+
import { shownMs, msMode, endStage } from './bd-pins.js';
|
|
16
16
|
import { faceOr } from './cozy-adler.js'; // Adler once in the lane's header when a delivery is missed (batch 17 item 4-A, canvas AdM2)
|
|
17
17
|
const TICK = '<svg aria-hidden="true" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12l5 5 9-10"/></svg>'; // the delivered mark, drawn (conformance R28-13: no glyph controls)
|
|
18
18
|
|
|
@@ -54,7 +54,7 @@ export function setFolded(on) {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
const openHold = (r) => (Array.isArray(r.holds) ? r.holds : []).find((h) => h && day(h.from) && !day(h.to)) || null;
|
|
57
|
-
const endName =
|
|
57
|
+
const endName = endStage; // its workflow's ◆ stage; '' when the workflow has none (batch 25 item 13)
|
|
58
58
|
/** The slip in words: "+1 wk", "+3d". */
|
|
59
59
|
const slipWords = (n) => (n >= 7 ? '+' + Math.round(n / 7) + ' wk' : '+' + n + 'd');
|
|
60
60
|
|
|
@@ -83,7 +83,7 @@ export function entriesOf(o) {
|
|
|
83
83
|
};
|
|
84
84
|
const dated = (r.milestones || []).filter((m) => m && day(m.date));
|
|
85
85
|
if (dated.length) { for (const m of shownMs(r, mode)) one(m.id, m.name, day(m.date), m.done, atEnd(day(m.date))); continue; }
|
|
86
|
-
if (e) { one('', endName(r), e, !!(r.track && (r.track.k >= 6 && endName(r) === 'Deliver')), true); continue; }
|
|
86
|
+
if (e && endName(r)) { one('', endName(r), e, !!(r.track && (r.track.k >= 6 && endName(r) === 'Deliver')), true); continue; }
|
|
87
87
|
// only Jira has a date (XcalmA6Grill 8): "≈" and the forecast; PMOS never presents a forecast as a commitment
|
|
88
88
|
if (fc) out.push({ ...base, id: '', name: r.title, date: fc, done: false, late: false, risk: false, fc: true, planned: null, slip: 0 });
|
|
89
89
|
}
|