create-pmos 0.502.0 → 0.504.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 +1 -0
- 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/lib/transcript-read.js +38 -3
- 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/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.504.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.504.0",
|
|
37
|
+
"at": "2026-11-03T05:59:07.712Z",
|
|
38
|
+
"f": 3,
|
|
39
|
+
"prs": [
|
|
40
|
+
{
|
|
41
|
+
"n": 1189,
|
|
42
|
+
"t": "The Working card ends when Claude's answer ends",
|
|
43
|
+
"s": "The Working card ends when Claude's answer ends",
|
|
44
|
+
"k": "fix"
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"v": "0.503.0",
|
|
50
|
+
"at": "2026-11-03T05:42:41.752Z",
|
|
51
|
+
"f": 3,
|
|
52
|
+
"prs": [
|
|
53
|
+
{
|
|
54
|
+
"n": 1186,
|
|
55
|
+
"t": "A timeline end ◇ names the initiative's own workflow, never a fixed Deliver",
|
|
56
|
+
"s": "A timeline end ◇ names the initiative's own workflow, never a fixed Deliver",
|
|
57
|
+
"k": "fix"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"n": 1187,
|
|
61
|
+
"t": "Working card leads with the todo or turn goal, not the raw command",
|
|
62
|
+
"s": "Working card leads with the todo or turn goal, not the raw command",
|
|
63
|
+
"k": "new"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"n": 1188,
|
|
67
|
+
"t": "New meeting recording view, word fix in place (batch 25 item 11)",
|
|
68
|
+
"s": "New meeting recording view, word fix in place (batch 25 item 11)",
|
|
69
|
+
"k": "new"
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
},
|
|
35
73
|
{
|
|
36
74
|
"v": "0.502.0",
|
|
37
75
|
"at": "2026-11-03T04:51:26.562Z",
|
|
@@ -397,32 +435,6 @@
|
|
|
397
435
|
"k": "fix"
|
|
398
436
|
}
|
|
399
437
|
]
|
|
400
|
-
},
|
|
401
|
-
{
|
|
402
|
-
"v": "0.484.0",
|
|
403
|
-
"at": "2026-11-02T02:00:11.221Z",
|
|
404
|
-
"f": 3,
|
|
405
|
-
"prs": [
|
|
406
|
-
{
|
|
407
|
-
"n": 1143,
|
|
408
|
-
"t": "Program chats get the header pill; a phone shows every PDF page (batch 22 items 17, 18)",
|
|
409
|
-
"s": "Program chats get the header pill; a phone shows every PDF page (batch 22 items 17, 18)",
|
|
410
|
-
"k": "fix"
|
|
411
|
-
}
|
|
412
|
-
]
|
|
413
|
-
},
|
|
414
|
-
{
|
|
415
|
-
"v": "0.483.0",
|
|
416
|
-
"at": "2026-11-02T00:26:37.780Z",
|
|
417
|
-
"f": 3,
|
|
418
|
-
"prs": [
|
|
419
|
-
{
|
|
420
|
-
"n": 1144,
|
|
421
|
-
"t": "Report building flow \"A+ on Home\" (batch 22 item 14)",
|
|
422
|
-
"s": "Report building flow \"A+ on Home\" (batch 22 item 14)",
|
|
423
|
-
"k": "new"
|
|
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:59:04Z"}
|
|
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:59:04Z"}
|
|
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:59:04Z"}
|
|
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.504.0",
|
|
4
|
+
"at": "2026-11-03T05:59:07.712Z",
|
|
5
|
+
"f": 3,
|
|
6
|
+
"prs": [
|
|
7
|
+
{
|
|
8
|
+
"n": 1189,
|
|
9
|
+
"t": "The Working card ends when Claude's answer ends",
|
|
10
|
+
"s": "The Working card ends when Claude's answer ends",
|
|
11
|
+
"k": "fix"
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"v": "0.503.0",
|
|
17
|
+
"at": "2026-11-03T05:42:41.752Z",
|
|
18
|
+
"f": 3,
|
|
19
|
+
"prs": [
|
|
20
|
+
{
|
|
21
|
+
"n": 1186,
|
|
22
|
+
"t": "A timeline end ◇ names the initiative's own workflow, never a fixed Deliver",
|
|
23
|
+
"s": "A timeline end ◇ names the initiative's own workflow, never a fixed Deliver",
|
|
24
|
+
"k": "fix"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"n": 1187,
|
|
28
|
+
"t": "Working card leads with the todo or turn goal, not the raw command",
|
|
29
|
+
"s": "Working card leads with the todo or turn goal, not the raw command",
|
|
30
|
+
"k": "new"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"n": 1188,
|
|
34
|
+
"t": "New meeting recording view, word fix in place (batch 25 item 11)",
|
|
35
|
+
"s": "New meeting recording view, word fix in place (batch 25 item 11)",
|
|
36
|
+
"k": "new"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
},
|
|
2
40
|
{
|
|
3
41
|
"v": "0.502.0",
|
|
4
42
|
"at": "2026-11-03T04:51:26.562Z",
|
|
@@ -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
|
@@ -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);
|
|
@@ -236,9 +236,20 @@ function newHarnessAcc() {
|
|
|
236
236
|
// more. `ai-title` and `attachment` records are CLI bookkeeping and are deliberately not
|
|
237
237
|
// turn-taking, and neither is a SYNTHETIC assistant record: counting one as "the assistant
|
|
238
238
|
// finished speaking" is exactly how a false `needs you` would be manufactured.
|
|
239
|
-
lastRole: null
|
|
239
|
+
lastRole: null, // 'assistant' | 'user' | null
|
|
240
|
+
msgId: null, // the newest real model message's id: a new one closes the older messages' calls (feedHarness)
|
|
241
|
+
cmdFrom: undefined }; // the turn before a /command record, given back when the CLI answers the command itself
|
|
240
242
|
}
|
|
241
243
|
|
|
244
|
+
// The text of a user record, for the turn rules below: string content, or its text blocks joined.
|
|
245
|
+
function userRecordText(um) {
|
|
246
|
+
if (typeof um.content === 'string') return um.content;
|
|
247
|
+
return (Array.isArray(um.content) ? um.content : []).filter((c) => c && c.type === 'text' && typeof c.text === 'string').map((c) => c.text).join('\n');
|
|
248
|
+
}
|
|
249
|
+
const INTERRUPTED = /^\s*\[Request interrupted by user/;
|
|
250
|
+
const LOCAL_OUT = /^\s*<(local-command-stdout|local-command-stderr)>/;
|
|
251
|
+
const LOCAL_IN = /^\s*<(bash-input|bash-stdout|bash-stderr)>|^\s*Caveat: The messages below/;
|
|
252
|
+
|
|
242
253
|
function feedHarness(acc, raw, clean) {
|
|
243
254
|
for (const line of raw.split('\n')) {
|
|
244
255
|
if (!line) continue;
|
|
@@ -253,13 +264,27 @@ function feedHarness(acc, raw, clean) {
|
|
|
253
264
|
}
|
|
254
265
|
// tool_result blocks live inside `user` records; harvest the ids they resolve.
|
|
255
266
|
if (r.type === 'user') {
|
|
267
|
+
if (r.isSidechain) continue; // a subagent's own records: its calls and results never hold the main session's turn
|
|
256
268
|
const um = r.message || {};
|
|
269
|
+
let result = false;
|
|
257
270
|
for (const c of (Array.isArray(um.content) ? um.content : [])) {
|
|
258
271
|
if (c && c.type === 'tool_result' && c.tool_use_id) {
|
|
272
|
+
result = true;
|
|
259
273
|
if (!acc.openUse.delete(c.tool_use_id)) acc.resolved.add(c.tool_use_id);
|
|
260
274
|
}
|
|
261
275
|
}
|
|
262
|
-
|
|
276
|
+
const said = result ? '' : userRecordText(um);
|
|
277
|
+
// batch 25 item 15: only a prompt or a tool result gives the model the turn. The CLI's own records after a finished
|
|
278
|
+
// answer (a meta note, a compact summary, a local command such as /model and its output, a `!` shell line) start no
|
|
279
|
+
// model turn, so counting them left the chat on "Working" for hours after Claude had answered.
|
|
280
|
+
if (result) acc.lastRole = 'user';
|
|
281
|
+
else if (INTERRUPTED.test(said)) { acc.lastRole = 'assistant'; acc.openUse.clear(); } // Esc / Stop: the model stopped, the PM is up
|
|
282
|
+
else if (LOCAL_OUT.test(said)) { if (acc.cmdFrom !== undefined) acc.lastRole = acc.cmdFrom; acc.cmdFrom = undefined; } // a local command answered itself
|
|
283
|
+
else if (r.isMeta || r.isCompactSummary || r.isVisibleInTranscriptOnly || LOCAL_IN.test(said)) { /* bookkeeping: not turn-taking */ }
|
|
284
|
+
else {
|
|
285
|
+
if (/<command-name>/.test(said)) acc.cmdFrom = acc.lastRole; // /name: a prompt command takes the turn; a local one gives it back above
|
|
286
|
+
acc.lastRole = 'user'; // a prompt was submitted
|
|
287
|
+
}
|
|
263
288
|
continue;
|
|
264
289
|
}
|
|
265
290
|
if (r.type === 'attachment') {
|
|
@@ -274,6 +299,13 @@ function feedHarness(acc, raw, clean) {
|
|
|
274
299
|
}
|
|
275
300
|
if (r.type !== 'assistant') continue;
|
|
276
301
|
const m = r.message || {};
|
|
302
|
+
if (m.model && m.model !== SYNTHETIC && !r.isSidechain && m.id && m.id !== acc.msgId) {
|
|
303
|
+
// A new model message starts only once every call of the one before it has its result: the model cannot go on without
|
|
304
|
+
// them. So a call of an older message that is still unresolved is a phantom (a crash, a killed pane, a resumed chat), never
|
|
305
|
+
// a call in flight, and it must not hold the turn (batch 25 item 15: a chat showed "Working · 169:28" under its answer).
|
|
306
|
+
for (const [id, info] of acc.openUse) if (info.msg !== m.id) acc.openUse.delete(id);
|
|
307
|
+
acc.msgId = m.id;
|
|
308
|
+
}
|
|
277
309
|
if (m.model && m.model !== SYNTHETIC) {
|
|
278
310
|
acc.model = clean(m.model);
|
|
279
311
|
acc.turns++; // only real model turns count
|
|
@@ -298,7 +330,7 @@ function feedHarness(acc, raw, clean) {
|
|
|
298
330
|
const name = clean(prettyTool(c.name));
|
|
299
331
|
acc.toolCounts[name] = (acc.toolCounts[name] || 0) + 1;
|
|
300
332
|
acc.toolTotal++;
|
|
301
|
-
if (c.id && !acc.resolved.delete(c.id)) acc.openUse.set(c.id, { verb: name, ts: r.timestamp || null });
|
|
333
|
+
if (c.id && !r.isSidechain && !acc.resolved.delete(c.id)) acc.openUse.set(c.id, { verb: name, ts: r.timestamp || null, msg: m.id || null });
|
|
302
334
|
}
|
|
303
335
|
}
|
|
304
336
|
}
|
|
@@ -320,6 +352,9 @@ function harnessOf(acc) {
|
|
|
320
352
|
// the strong `needs you` claim can be built) is UNREACHABLE for it. Not a guard that rejects
|
|
321
353
|
// the phantom: a value it cannot construct. Reordering these branches silently converts a
|
|
322
354
|
// corpse into "waiting on you", which is the one lie this surface must not tell.
|
|
355
|
+
// A phantom stays only while nothing came after it: once a NEWER model message exists, the session demonstrably went
|
|
356
|
+
// on past that call, so feedHarness drops it (batch 25 item 15). A crash with the call as its newest record still lands
|
|
357
|
+
// on `tool`, exactly as above.
|
|
323
358
|
if (out.openCall) out.turn = 'tool'; // awaiting a tool — OR the PM's approval of one
|
|
324
359
|
else if (acc.lastRole === 'assistant') out.turn = 'user'; // the model stopped; the human is up
|
|
325
360
|
else if (acc.lastRole === 'user') out.turn = 'agent'; // a prompt/result landed; the model is up
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.504.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.504.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));
|