create-pmos 0.242.0 → 0.244.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/KIT-VERSION +2 -2
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/payload/state/log.md +1 -1
- package/workspace/c3-report-design-real-test.js +13 -2
- package/workspace/c3-report-design-test.js +25 -3
- package/workspace/c3-report-structure-real-test.js +38 -1
- package/workspace/c3-report-structure-test.js +21 -6
- package/workspace/guide/console-calendar.md +2 -2
- package/workspace/guide/notes.md +5 -1
- package/workspace/guide/report-structure.md +3 -3
- package/workspace/guide/reports-page.md +3 -1
- package/workspace/guide/reports-set-up.md +5 -1
- package/workspace/lib/headless.js +16 -4
- package/workspace/lib/meeting-focus.js +6 -4
- package/workspace/lib/meeting-replay.js +15 -1
- package/workspace/lib/report-structure.js +49 -5
- package/workspace/lib/routes-meeting-focus.js +1 -0
- package/workspace/lib/routes-report-setups.js +16 -2
- package/workspace/lib/routes-report-structure.js +176 -5
- package/workspace/lib/routes-reports.js +10 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-audience-link.js +21 -0
- package/workspace/public/pm/c3-audience.js +46 -0
- package/workspace/public/pm/c3-boot.js +2 -1
- package/workspace/public/pm/c3-material.js +11 -1
- package/workspace/public/pm/c3-replay.css +6 -0
- package/workspace/public/pm/c3-replay.js +19 -3
- package/workspace/public/pm/c3-report-deck.css +5 -0
- package/workspace/public/pm/c3-report-deck.js +41 -5
- package/workspace/public/pm/c3-report-setup.css +5 -0
- package/workspace/public/pm/c3-report-setup.js +20 -2
- package/workspace/public/pm/c3-report-store.js +24 -2
- package/workspace/public/pm/c3-report-structure.css +4 -0
- package/workspace/public/pm/c3-report-structure.js +26 -8
- package/workspace/public/pm/c3-shell.js +2 -0
- package/workspace/public/pm/dest-reports.js +10 -1
- package/workspace/public/pm/report-view.js +23 -1
- package/workspace/public/pm/reports-copy.js +5 -0
- package/workspace/server.js +8 -1
- package/workspace/skills/report-structure-pull.skill +45 -0
- package/workspace/test-lists/report-structure-gaps.json +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.244.0",
|
|
4
4
|
"description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
package/payload/KIT-VERSION
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
kernel-source: pmos@
|
|
2
|
-
assembled: 2026-10-
|
|
1
|
+
kernel-source: pmos@c924d0d
|
|
2
|
+
assembled: 2026-10-15
|
|
3
3
|
mode: vendor
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Eval calibration log"
|
|
4
4
|
description: "Append-only record of every PM override of an evaluator verdict."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-15T02:53:25Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Health budgets"
|
|
4
4
|
description: "Closed registry of health-budget anchors (D59), edited only by the PM."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-15T02:53:25Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Discoveries and friction log"
|
|
4
4
|
description: "Append-only record of what runs discovered and where the process rubbed."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-15T02:53:25Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
package/payload/state/log.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* frame shows the render; a point-and-say marker goes with the second turn and lands D2; Compare two; This is it; the Design brief
|
|
10
10
|
* cycle Draft now on the report: the cycle hook renders the cycle to match the saved design
|
|
11
11
|
* deck the Read tab shows the deck from /api/report-design/cycle-html in frames with sandbox exactly "allow-same-origin"; a source
|
|
12
|
-
* popover; a speaker note and a pin are saved; the pin is in the Edit log; Present answers ArrowRight and Escape
|
|
12
|
+
* popover; a speaker note and a pin are saved; the pin is in the Edit log; Present answers ArrowRight and Escape; Audience view opens a window of its own that follows the presenter and closes with it
|
|
13
13
|
* No page error and no failed request may be raised. Synthetic repositories only; the stub is the only "model"; nothing reaches the network. */
|
|
14
14
|
const fs = require('fs');
|
|
15
15
|
const path = require('path');
|
|
@@ -244,7 +244,18 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
244
244
|
ok('Present opens the presenter view with the note of the slide', /1 \/ 4/.test(await text('[data-rd-pn]')) && /Open with the release/.test(await text('[data-rd-ovnote]')));
|
|
245
245
|
await page.keyboard.press('ArrowRight');
|
|
246
246
|
ok('ArrowRight goes to the next slide', !!(await until(async () => /2 \/ 4/.test(await text('[data-rd-pn]')))));
|
|
247
|
-
|
|
247
|
+
/* Audience view: a window of its own, slides only, that follows the presenter and closes with it (initiative meeting-material-replay-gaps) */
|
|
248
|
+
const popening = page.waitForEvent('popup');
|
|
249
|
+
await page.click('[data-rd-ovact="aud"]');
|
|
250
|
+
const pop = await popening;
|
|
251
|
+
await pop.waitForSelector('[data-rd-ov].rd-aud [data-rd-ovslide] iframe', { timeout: 30000 });
|
|
252
|
+
ok('Audience view opens a window of its own with the slide the presenter is on, no notes and no toolbar', /2 \/ 4/.test(await pop.textContent('[data-rd-count]')) && !(await pop.$('[data-rd-ovnote]')) && !(await pop.isVisible('.rp-deck__bar')) && /^PMOS · Audience view · /.test(await pop.title()) && !!(await page.$('[data-rd-ov].rd-pres')));
|
|
253
|
+
await page.bringToFront(); await page.keyboard.press('ArrowRight');
|
|
254
|
+
ok('the audience window follows the presenter to the next slide', !!(await until(async () => /3 \/ 4/.test((await pop.textContent('[data-rd-count]')) || ''))));
|
|
255
|
+
await pop.bringToFront(); await pop.keyboard.press('ArrowLeft');
|
|
256
|
+
ok('a key in the audience window moves the presenter view back', !!(await until(async () => /2 \/ 4/.test(await text('[data-rd-pn]')))));
|
|
257
|
+
await page.bringToFront(); await page.keyboard.press('Escape');
|
|
258
|
+
ok('closing the presenter view closes the audience window', !!(await until(async () => pop.isClosed())));
|
|
248
259
|
ok('Escape closes the view and leaves the document open', !!(await until(async () => !(await page.$('[data-rd-ov]')))) && !!(await page.$('[data-rd-big]')));
|
|
249
260
|
|
|
250
261
|
await sleep(400);
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* Differs from the design with Keep for this cycle, Make it part of the design, Match the design and Show the slide; Matches the design
|
|
13
13
|
* notes speaker notes save and carry forward (Keep, Clear); pins: yours blue, PMOS dashed purple, Keep the note, a new pin, the Edit log,
|
|
14
14
|
* Edit this opens the Edit tab at its section
|
|
15
|
-
* present the presenter view (slide, note, next, timer, n / N, Jump to) and the audience view: arrows, Page keys, Space, Home, End,
|
|
15
|
+
* present the presenter view (slide, note, next, timer, n / N, Jump to) and the audience view (a window of its own that follows the presenter; the full-screen view in the page when the window is blocked): arrows, Page keys, Space, Home, End,
|
|
16
16
|
* swipe, Escape
|
|
17
17
|
* print Print / PDF writes the slides only: no speaker note and no pin; Download .pptx reads the existing export
|
|
18
18
|
* document a report designed as a document reads as an interactive document (pmos-sec) with its notes keyed by section
|
|
@@ -331,7 +331,24 @@ const DOC = 'planning/reports/rs1/2026-10-21.md';
|
|
|
331
331
|
await page.keyboard.press('End'); ok('End goes to the last slide', !!(await until(async () => /11 \/ 11/.test(await pn()))));
|
|
332
332
|
await page.keyboard.press('Home'); ok('Home goes to the first slide', !!(await until(async () => /1 \/ 11/.test(await pn()))));
|
|
333
333
|
await page.click('[data-rd-jump="4"]'); ok('Jump to goes to that slide', !!(await until(async () => /5 \/ 11/.test(await pn()))));
|
|
334
|
+
/* Audience view opens a window of its own (initiative meeting-material-replay-gaps). This page is in demo mode, whose sample state lives in this page's memory, so the new window
|
|
335
|
+
* cannot read the deck here: it says so. What the window shows, follows and closes with is tested against a real deck in pm-c3-meeting-material-test.js. */
|
|
336
|
+
const popening = page.waitForEvent('popup');
|
|
334
337
|
await page.click('[data-rd-ovact="aud"]');
|
|
338
|
+
const pop = await popening;
|
|
339
|
+
await pop.waitForSelector('[data-aud="message"]', { timeout: 30000 });
|
|
340
|
+
const pu = new URL(pop.url());
|
|
341
|
+
ok('Audience view opens a window of its own: the audience page, the report path, the slide the presenter is on, and a title that names the deck',
|
|
342
|
+
pu.pathname === '/c3.html' && pu.searchParams.get('audience') === '1' && /rs1\/[\d-]+\.md$/.test(pu.searchParams.get('path') || '') && pu.searchParams.get('start') === '4' && /^PMOS · Audience view · /.test(await pop.title()) && !!(await page.$('[data-rd-ov]')), pop.url());
|
|
343
|
+
ok('the presenter view stays on this page while the window is open', !!(await page.$('[data-rd-ov].rd-pres')));
|
|
344
|
+
await page.bringToFront(); await page.keyboard.press('Escape');
|
|
345
|
+
ok('closing the presenter view closes the audience window', !!(await until(async () => pop.isClosed())) && !(await page.$('[data-rd-ov]')));
|
|
346
|
+
/* a browser that blocks the window: a notice that says so, and the full-screen view in this window */
|
|
347
|
+
await page.evaluate(() => { window.__open = window.open; window.open = () => null; });
|
|
348
|
+
await page.click('[data-rd-act="present"]'); await page.waitForSelector('[data-rd-ov]');
|
|
349
|
+
await page.click('[data-rd-ovact="aud"]');
|
|
350
|
+
ok('a blocked window says so and offers the full-screen view here', !!(await until(async () => /The audience window was blocked/.test(await text('[data-c3-toast]') || ''))) && /Full-screen view here/.test(await text('[data-c3-toast]')));
|
|
351
|
+
await page.click('[data-c3-toast-action]');
|
|
335
352
|
ok('Audience view shows only the slide and n / N', !!(await until(() => page.$('[data-rd-ov].rd-aud, [data-rd-ov] [data-rd-count]'))) && !(await page.$('[data-rd-ovnote]')) && /5 \/ 11/.test(await text('[data-rd-count]')));
|
|
336
353
|
await shot(page, 'deck-audience-1440');
|
|
337
354
|
await page.keyboard.press(' '); ok('Space goes on', !!(await until(async () => /6 \/ 11/.test(await text('[data-rd-count]')))));
|
|
@@ -339,9 +356,14 @@ const DOC = 'planning/reports/rs1/2026-10-21.md';
|
|
|
339
356
|
ok('a swipe to the left goes on', !!(await until(async () => /7 \/ 11/.test(await text('[data-rd-count]')))));
|
|
340
357
|
await page.keyboard.press('Escape');
|
|
341
358
|
ok('Escape closes the view and leaves the document open', !!(await until(async () => !(await page.$('[data-rd-ov]')))) && !!(await page.$('[data-rd-big]')));
|
|
359
|
+
await page.evaluate(() => { window.open = window.__open; });
|
|
360
|
+
const popening2 = page.waitForEvent('popup');
|
|
342
361
|
await page.click('[data-rd-act="audience"]');
|
|
343
|
-
|
|
344
|
-
await
|
|
362
|
+
const pop2 = await popening2;
|
|
363
|
+
await pop2.waitForSelector('[data-aud="message"]', { timeout: 30000 });
|
|
364
|
+
ok('Audience view opens straight from the bar, in a window of its own, and not as an overlay on this page', /audience=1/.test(pop2.url()) && !(await page.$('[data-rd-ov]')));
|
|
365
|
+
await pop2.close();
|
|
366
|
+
ok('the audience window can be closed', !!(await until(async () => pop2.isClosed())));
|
|
345
367
|
|
|
346
368
|
/* print and download */
|
|
347
369
|
await page.evaluate(() => { window.__printed = []; window.__printCalls = 0; window.open = () => ({ document: { open() {}, write(h) { window.__printed.push(h); }, close() {} }, focus() {}, print() { window.__printCalls++; }, close() {}, addEventListener() {}, onload: null }); });
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* source a Chrome source is added with the check stubbed to pass: the check view shows its steps
|
|
13
13
|
* accept Use this structure: the setup sheet shows the saved structure, brief.md is on disk, charts.jsonl holds the new charts
|
|
14
14
|
* charts the Charts tab lists a data chart with its source and Used in naming the report
|
|
15
|
+
* pull Test pull reads a source into its row (Reading…, then Pulled), adds nothing to the conversation and fills the chart's TEST PULL
|
|
16
|
+
* drafting a report row while a session drafts (Drafting…), when the file lands (Draft ready), and when it waits for a check
|
|
15
17
|
* No page error and no failed request may be raised. Synthetic repositories only; the stub is the only "model"; nothing reaches the network. */
|
|
16
18
|
const fs = require('fs');
|
|
17
19
|
const path = require('path');
|
|
@@ -47,6 +49,14 @@ let input = ''; process.stdin.on('data', (d) => { input += d; }); process.stdin.
|
|
|
47
49
|
const line = (k) => { const m = new RegExp('^- ' + k + ': (.*)$', 'm').exec(input); return m ? m[1].trim() : null; };
|
|
48
50
|
const mode = line('Mode'); const out = line('Output path'); const turn = line('Turn') || '';
|
|
49
51
|
const done = (o) => { process.stdout.write(JSON.stringify({ type: 'result', subtype: 'success', is_error: false, result: typeof o === 'string' ? o : JSON.stringify(o), num_turns: 2, duration_ms: 500, permission_denials: [] })); process.exit(0); };
|
|
52
|
+
const pullSrc = /^- Source: ([a-z0-9-]+) /m.exec(input);
|
|
53
|
+
if (pullSrc && out) { // a Test pull: one source, one result file (initiative report-structure-gaps)
|
|
54
|
+
const vm = /^- Version: S(\\d+) \\((.*)\\)$/m.exec(input);
|
|
55
|
+
const plan = JSON.parse(fs.readFileSync(vm[2], 'utf8'));
|
|
56
|
+
const mine = plan.charts.filter((c) => c.source === pullSrc[1]);
|
|
57
|
+
fs.writeFileSync(out, JSON.stringify({ source: pullSrc[1], n: Number(vm[1]), period: '2026-09', reached: true, reason: '', values: mine.map((c, i) => ({ chart: c.id, value: i ? '3' : '48,210', was: '52,930', delta: '-8.9%', flag: !i, note: '' })) }));
|
|
58
|
+
return setTimeout(() => done({ ok: true, note: mine.length + ' charts' }), 1200);
|
|
59
|
+
}
|
|
50
60
|
if (!mode || !out) return done('Drafted.');
|
|
51
61
|
const base = () => ({
|
|
52
62
|
purpose: { readers: 'Leadership', decision: 'Where to invest next quarter', scope: 'The app', cadence: 'monthly', questions: ['Are we growing?'] },
|
|
@@ -130,7 +140,7 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
130
140
|
ok('the set-up row has Shape the report', !!(await page.$('[data-rs-reg="' + id + '"] [data-rs-act="shape"]')));
|
|
131
141
|
await page.click('[data-rs-reg="' + id + '"] [data-rs-act="shape"]');
|
|
132
142
|
await page.waitForSelector('[data-st-studio]');
|
|
133
|
-
ok('the Structure step opens with nothing saved yet and the Use button waiting', /Not saved yet/.test(await text('[data-st-studio]')) && await page.$eval('[data-st-act="save"]', (b) => b.disabled));
|
|
143
|
+
ok('the Structure step opens with nothing saved yet and the Use button waiting', !!(await until(async () => /Not saved yet/.test(await text('[data-st-studio]')))) && await page.$eval('[data-st-act="save"]', (b) => b.disabled));
|
|
134
144
|
|
|
135
145
|
/* ── a purpose and the first turn: S1 ── */
|
|
136
146
|
await page.fill('[data-st-in]', 'Monthly product performance for leadership, numbers from Google Analytics.');
|
|
@@ -184,6 +194,18 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
184
194
|
ok('what it could not get names the Chrome source', /Looker Studio/.test(await text('[data-st-dry-missing]')) && /Needs Chrome/.test(await text('[data-st-dry-missing]')));
|
|
185
195
|
await page.click('[data-st-view="plan"]');
|
|
186
196
|
|
|
197
|
+
/* ── Test pull: one source read into its row, not a turn (report-structure-gaps) ── */
|
|
198
|
+
const logLen = async () => (await call('GET', `/api/report-structure?ws=${ws}&id=${id}`)).body.structure.log.length;
|
|
199
|
+
const logBefore = await logLen();
|
|
200
|
+
await page.waitForSelector('[data-st-src="ga"] [data-st-act="test"]');
|
|
201
|
+
await page.click('[data-st-src="ga"] [data-st-act="test"]');
|
|
202
|
+
ok('Test pull shows Reading… on the source row while the read runs', !!(await until(() => page.$('[data-st-src="ga"] [data-st-pulling]'), 8000, 50)));
|
|
203
|
+
ok('the row then says Pulled with the period and the charts that returned a number', !!(await until(() => page.$('[data-st-src="ga"] [data-st-pulled="ok"]'), 30000, 100)) && /2026-09/.test(await text('[data-st-src="ga"]')) && /1 of 1 chart returned a number/.test(await text('[data-st-src="ga"]')), await text('[data-st-src="ga"]'));
|
|
204
|
+
ok('the pull added no line to the conversation', (await logLen()) === logBefore);
|
|
205
|
+
await page.click('[data-st-chart="users"] [data-st-act="chart"]');
|
|
206
|
+
ok('the chart row shows the pulled value, its source and when', !!(await until(async () => /48,210/.test(await text('[data-st-testpull]')))) && /Pulled from Google Analytics at \d\d:\d\d, 2026-09/.test(await text('[data-st-pulled-from]')), await text('[data-st-testpull]'));
|
|
207
|
+
ok('the pull is on disk as structure/pull/ga.json', fs.existsSync(path.join(P.pull, 'ga.json')));
|
|
208
|
+
|
|
187
209
|
/* ── Use this structure ── */
|
|
188
210
|
await idle();
|
|
189
211
|
await page.click('[data-st-act="save"]');
|
|
@@ -205,6 +227,21 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
205
227
|
ok('the Charts tab lists a data chart beside the built-in ones', !!(await page.$('[data-rp3-lib] [data-home="north"]')) && /Data chart/.test(await text('[data-rp3-data-chart="' + newUsersId + '"]')));
|
|
206
228
|
ok('the data chart shows its source and Used in naming the report', /Google Analytics/.test(await text('[data-rp3-source="' + newUsersId + '"]')) && /Used in: .*MySPH app performance/.test(await text('[data-rp3-data-chart="' + newUsersId + '"] [data-rp3-used]')), await text('[data-rp3-data-chart="' + newUsersId + '"]'));
|
|
207
229
|
|
|
230
|
+
/* ── a report whose draft a session is making (report-structure-gaps) ── */
|
|
231
|
+
const dFile = path.join(P.dir, 'drafting.json');
|
|
232
|
+
fs.writeFileSync(dFile, JSON.stringify({ since: new Date().toISOString(), sched: null, session: null, saved: 3 }) + '\n');
|
|
233
|
+
await goReports();
|
|
234
|
+
const row = '[data-rs-reg="' + id + '"]';
|
|
235
|
+
ok('the report row says Drafting… with the saved structure it follows, and its button is disabled', (await page.getAttribute(row, 'data-rs-draft')) === 'drafting' && /Following saved structure S3/.test(await text(row + ' [data-rs-drafting]')) && /Drafting…/.test(await text(row + ' .acts'))
|
|
236
|
+
&& await page.isDisabled(row + ' [data-rs-act="draft-now"]'), await text(row));
|
|
237
|
+
{ const acts = await text(row + ' .acts'); ok('Draft it now and Open are not offered while it drafts', !/Draft it now/.test(acts) && !/\bOpen\b/.test(acts), acts); }
|
|
238
|
+
fs.writeFileSync(path.join(P.folder, '2026-11-03.md'), '---\ntype: Report\n---\n# draft\n');
|
|
239
|
+
ok('when the file lands the row returns to normal and a notice says the draft is ready, with Open', !!(await until(async () => !(await page.getAttribute(row, 'data-rs-draft')), 15000)) && !!(await until(async () => /Draft ready/.test(await text('[data-c3-toast]')), 8000)), await text('[data-c3-toast]'));
|
|
240
|
+
fs.writeFileSync(path.join(P.dir, 'hold.json'), JSON.stringify({ since: new Date().toISOString(), sched: null, sources: ['looker-studio-paid-acquisition'], saved: 3 }) + '\n');
|
|
241
|
+
await goReports();
|
|
242
|
+
ok('a draft held for a check says Waiting for a check and names the source', (await page.getAttribute(row, 'data-rs-draft')) === 'held' && /Waiting for a check/.test(await text(row)) && /Looker Studio/.test(await text(row + ' [data-rs-drafting]')), await text(row));
|
|
243
|
+
fs.unlinkSync(path.join(P.dir, 'hold.json'));
|
|
244
|
+
|
|
208
245
|
await sleep(400);
|
|
209
246
|
ok('no page error and no failed request', errors.length === 0, errors.slice(0, 6).join(' | '));
|
|
210
247
|
return finish();
|
|
@@ -68,12 +68,14 @@ const plan1 = () => ({ purpose: { readers: 'Leadership: Gita Rahma and the produ
|
|
|
68
68
|
const Q1 = [{ q: 'Who reads it, and what do they decide with it?', chips: ['Leadership: where to invest next quarter', 'Product team: what to fix next', 'Both'] }, { q: 'Anything that must be in it?', chips: ['Revenue', 'Releases this month', 'Nothing more'] }];
|
|
69
69
|
|
|
70
70
|
function makeStub() {
|
|
71
|
-
const S = { versions: [], plans: {}, log: [], current: null, saved: null, running: null, ticks: 0, dry: null, dryResult: null, checks: {}, posts: [], gets: [], chartsRead: 0, polls: 0, setupMade: 0, designStep: 0 };
|
|
71
|
+
const S = { versions: [], plans: {}, log: [], current: null, saved: null, running: null, ticks: 0, dry: null, dryResult: null, checks: {}, pulls: {}, pulling: [], pullTicks: 0, posts: [], gets: [], chartsRead: 0, polls: 0, setupMade: 0, designStep: 0 };
|
|
72
72
|
const now = () => '2026-10-12T22:' + String(10 + S.versions.length * 3 + S.log.length).padStart(2, '0') + ':00.000Z';
|
|
73
73
|
const land = (turn, plan, answer, extra) => { const n = S.versions.length + 1; S.versions.push({ n, at: now(), turn }); S.plans[n] = plan; S.current = n; S.log.push({ who: 'pmos', text: answer, at: now(), n, questions: [], options: [], ...(extra || {}) }); };
|
|
74
74
|
const view = () => {
|
|
75
75
|
if (S.running) { S.ticks++; if (S.ticks >= 2) { const r = S.running; S.running = null; r.done(); } }
|
|
76
|
-
|
|
76
|
+
if (S.pulling.length) { S.pullTicks++; if (S.pullTicks >= 2) { const src = S.pulling.shift(); S.pulls[src] = { source: src, n: S.current, period: '2026-09', reached: true, ok: true, reason: '', charts: 2, got: 1, at: now(),
|
|
77
|
+
values: [{ chart: 'new-users', value: '48,210', was: '52,930', delta: '-8.9%', flag: true, note: '' }] }; } }
|
|
78
|
+
return { ok: true, setup: { id: SETUP.id, name: SETUP.name, cadence: 'monthly' }, structure: { pulls: S.pulls, pulling: S.pulling.slice(), running: !!S.running, job: S.running ? { kind: S.running.kind } : null, current: S.current, saved: S.saved, versions: S.versions.slice(), log: S.log.slice(),
|
|
77
79
|
plan: S.current ? S.plans[S.current] : null, dry: S.dry, checks: S.checks, errors: [] } };
|
|
78
80
|
};
|
|
79
81
|
const chromeChecks = (pass) => ({ source: 'looker', kind: 'chrome', at: now(), running: false, ok: pass, steps: [
|
|
@@ -102,6 +104,7 @@ function makeStub() {
|
|
|
102
104
|
S.running = { kind: 'report-structure', done };
|
|
103
105
|
return json({ ok: true, running: true });
|
|
104
106
|
}
|
|
107
|
+
if (m === 'POST' && p === '/api/report-structure/pull') { if (S.pulling.includes(body.source)) return json({ ok: false, error: 'PMOS is already reading this source. Wait for the answer.' }, 409); S.pullTicks = 0; S.pulling.push(body.source); return json({ ok: true, running: true }); }
|
|
105
108
|
if (m === 'POST' && p === '/api/report-structure/revert') { S.current = body.n; return json({ ok: true, structure: view().structure }); }
|
|
106
109
|
if (m === 'POST' && p === '/api/report-structure/remove') { const pl = fixedPlan(S.current); if (body.kind === 'chart') { pl.charts = pl.charts.filter((c) => c.id !== body.ref); pl.outline.forEach((o) => { o.charts = o.charts.filter((c) => c !== body.ref); }); } else pl.sources = pl.sources.filter((s) => s.id !== body.ref); land('Remove ' + body.ref, pl, 'Removed ' + body.ref + '.'); return json({ ok: true, structure: view().structure }); }
|
|
107
110
|
if (m === 'POST' && p === '/api/report-structure/source') {
|
|
@@ -169,14 +172,14 @@ function makeStub() {
|
|
|
169
172
|
await goReports();
|
|
170
173
|
await page.click('[data-rs-reg="' + SETUP.id + '"] [data-rs-act="shape"]');
|
|
171
174
|
await page.waitForSelector('[data-st-studio]');
|
|
172
|
-
ok('Shape the report on the row opens the same studio', /Not saved yet/.test(await text('[data-st-studio]')));
|
|
175
|
+
ok('Shape the report on the row opens the same studio', !!(await until(async () => /Not saved yet/.test(await text('[data-st-studio]')))));
|
|
173
176
|
await closeAll();
|
|
174
177
|
await page.click('[data-rs-reg="' + SETUP.id + '"] [data-rs-act="design"]');
|
|
175
178
|
await page.waitForSelector('[data-rd-studio]');
|
|
176
179
|
ok('the Design studio shows the step bar with 2 Design pressed', await page.getAttribute('[data-rd-step="design"]', 'aria-pressed') === 'true' && await page.getAttribute('[data-rd-step="structure"]', 'aria-pressed') === 'false');
|
|
177
180
|
await page.click('[data-rd-step="structure"]');
|
|
178
181
|
await page.waitForSelector('[data-st-studio]');
|
|
179
|
-
ok('1 Structure in the Design studio opens the Structure step', !!(await page.$('[data-st-steps]')));
|
|
182
|
+
ok('1 Structure in the Design studio opens the Structure step', !!(await until(() => page.$('[data-st-steps]'))));
|
|
180
183
|
await page.click('[data-st-step-btn="design"]');
|
|
181
184
|
await page.waitForSelector('[data-rd-studio]');
|
|
182
185
|
ok('2 Design in the Structure step opens the Design studio', true);
|
|
@@ -207,7 +210,15 @@ function makeStub() {
|
|
|
207
210
|
ok('the actions fit the source: Open, Test pull, How to connect', /Open/.test(await text('[data-st-src="board"] .acts')) && /Test pull/.test(await text('[data-st-src="ga"] .acts')) && /How to connect/.test(await text('[data-st-src="bq"] .acts')));
|
|
208
211
|
ok('a source says how many charts use it', /2 charts/.test(await text('[data-st-src="ga"]')) && !/ chart/.test(await text('[data-st-src="board"] .chips')));
|
|
209
212
|
await page.click('[data-st-src="ga"] [data-st-act="test"]');
|
|
210
|
-
ok('Test pull
|
|
213
|
+
ok('Test pull starts a read of that source, not a turn', !!(await until(() => sent('/api/report-structure/pull').length === 1)) && sent('/api/report-structure/pull')[0].body.source === 'ga' && sent('/api/report-structure/turn').length === 2, JSON.stringify(sent('/api/report-structure/pull')));
|
|
214
|
+
const reading = await until(() => page.$('[data-st-src="ga"] [data-st-pulling]'), 5000, 50);
|
|
215
|
+
const readingOff = reading ? await page.isDisabled('[data-st-src="ga"] [data-st-act="test"]') : null;
|
|
216
|
+
const readingNote = reading ? await text('[data-st-src="ga"] [data-st-pullnote]') : '';
|
|
217
|
+
ok('the source row says Reading… with a disabled button while it reads', !!reading && readingOff === true && /Read only/.test(readingNote), JSON.stringify({ reading: !!reading, readingOff, readingNote }));
|
|
218
|
+
const pulledEl = await until(() => page.$('[data-st-src="ga"] [data-st-pulled="ok"]'), 8000, 100);
|
|
219
|
+
const pulledRow = await text('[data-st-src="ga"]'); const pulledActs = await text('[data-st-src="ga"] .acts');
|
|
220
|
+
ok('the row then says Pulled with the period and how many charts returned a number, and offers Test pull again', !!pulledEl && /2026-09/.test(pulledRow) && /1 of 2 charts returned a number/.test(pulledRow) && /Test pull again/.test(pulledActs), JSON.stringify({ pulledEl: !!pulledEl, pulledRow, pulledActs, tags: await page.$$eval('[data-st-src="ga"] .tag', (a) => a.map((x) => x.className + '|' + x.getAttributeNames().join(',') + '|' + x.getAttribute('data-st-pulled') + '|' + x.textContent)), q: !!(await page.$('[data-st-pulled]')) }));
|
|
221
|
+
ok('the conversation got no line from the pull', !/Test pull from/.test(await text('.rd-dks-log')));
|
|
211
222
|
await idle();
|
|
212
223
|
await page.click('[data-st-src="bq"] [data-st-act="connect"]');
|
|
213
224
|
ok('How to connect says where to connect it', /Settings, Connectors/.test(await text('[data-c3-toast]')));
|
|
@@ -221,7 +232,11 @@ function makeStub() {
|
|
|
221
232
|
ok('a chart from Charts has Open in Charts, a new chart does not', !!(await page.$('[data-st-detail="new-users"] [data-st-act="opencharts"]')));
|
|
222
233
|
await page.click('[data-st-chart="cpa"] [data-st-act="chart"]');
|
|
223
234
|
ok('only one row is open and a new chart says it is saved in Charts when you use the structure', !!(await page.$('[data-st-detail="cpa"]')) && !(await page.$('[data-st-detail="new-users"]')) && /Saved in Charts when you use this structure/.test(await text('[data-st-used]')) && !(await page.$('[data-st-detail="cpa"] [data-st-act="opencharts"]')));
|
|
224
|
-
ok('the
|
|
235
|
+
ok('a chart the pull did not return says it is not tried yet', /Not tried on this version/.test(await text('[data-st-testpull]')));
|
|
236
|
+
await page.click('[data-st-chart="new-users"] [data-st-act="chart"]');
|
|
237
|
+
ok('a chart the pull returned shows its value, comparison, flag, source and time', !!(await until(async () => /48,210/.test(await text('[data-st-testpull]')))) && /was 52,930/.test(await text('[data-st-testpull]')) && /-8\.9%/.test(await text('[data-st-testpull]')) && /Flagged/.test(await text('[data-st-testpull]'))
|
|
238
|
+
&& /Pulled from Google Analytics 4: MySPH web \+ app at \d\d:\d\d, 2026-09\. Not saved in the report\./.test(await text('[data-st-pulled-from]')), await text('[data-st-testpull]'));
|
|
239
|
+
await page.click('[data-st-chart="cpa"] [data-st-act="chart"]');
|
|
225
240
|
await shot(page, 'structure-logic-1440');
|
|
226
241
|
await page.click('[data-st-detail="cpa"] [data-st-act="discuss"]');
|
|
227
242
|
ok('Discuss this chart fills the box with the chart name and focuses it', (await page.inputValue('[data-st-in]')) === 'About Cost per new user: ' && await page.evaluate(() => document.activeElement && document.activeElement.hasAttribute('data-st-in')));
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Guide: the Calendar tab of the console page"
|
|
4
4
|
description: "The Calendar tab of the console page (/): Calendar and Notes, the five zooms, the highlights, the layer chips, the markers, the context of a meeting, free time, the tidy-up suggestions and the empty states."
|
|
5
|
-
timestamp: 2026-10-
|
|
5
|
+
timestamp: 2026-10-15
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Guide: the Calendar tab of the console page
|
|
@@ -168,7 +168,7 @@ Press **Add material** to open the sheet:
|
|
|
168
168
|
|
|
169
169
|
After you pick an item, choose **For** (this meeting, or every meeting in the series), **Topic** (one of the topics of the meeting, or the whole meeting) and **You** (**Present it** or **Only read it**, for a PMOS deck or a Google Slides link).
|
|
170
170
|
|
|
171
|
-
In **Focus**, the material shows next to the links of the invite. An item for a topic comes up on the stage when the room is on that topic. When the room moves to another topic, the stage goes back to the first link. Press an item in the **Material** switch to keep it on the stage. A PMOS deck or report opens in PMOS. A PMOS deck has **Present**, which opens the presenter view with your speaker notes, and **Audience view**, which
|
|
171
|
+
In **Focus**, the material shows next to the links of the invite. An item for a topic comes up on the stage when the room is on that topic. When the room moves to another topic, the stage goes back to the first link. Press an item in the **Material** switch to keep it on the stage. A PMOS deck or report opens in PMOS. A PMOS deck has **Present**, which opens the presenter view with your speaker notes, and **Audience view**, which opens a window of its own with the slides only. Drag that window to the screen or the projector that you share. In the presenter view, the window follows your slide, and a key in the window moves your slide. Closing the presenter view closes the window. If the browser blocks the window, a notice says so and **Full-screen view here** shows the slides full screen in this window. A Google Slides deck opens in its own tab, as before. When nothing is linked, the Material part of Focus has **Add material**.
|
|
172
172
|
|
|
173
173
|
### Meeting focus over Google Meet
|
|
174
174
|
|
package/workspace/guide/notes.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Guide: notes"
|
|
4
4
|
description: "How to take notes in a meeting: one note top to bottom, typed notes as timed lines, key points written each minute and labelled by Jev, delete a note; final notes written after Stop from the whole recording, with speakers and a quote for each item, decisions with their why and who, next steps with an open question, a discussion paragraph, and Wispr Flow's notes merged in and fetched by hand when the fetch fails; listen to your microphone and the meeting on this machine, see decisions, actions and questions caught as they are said, check them against the repository for conflicts, capture highlights during the meeting, create tasks from next steps, correct and tag lines, see what changed in your notes, replay the meeting, see screenshots of the shared screen beside the transcript, link a note to its meeting and its initiative, and get a summary."
|
|
5
|
-
timestamp: 2026-10-
|
|
5
|
+
timestamp: 2026-10-15
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Guide: notes
|
|
@@ -211,8 +211,12 @@ Press **Replay** beside the player (in Calendar › Notes, or on the meeting's c
|
|
|
211
211
|
- **Key points** lists the decisions, next steps and questions with the time each was said. A key point whose line is not found has no time and no play button. **Confirm all** makes a task of each next step on the note's initiative.
|
|
212
212
|
- **Topics** shows the minutes each topic took and the first time it came up. **Conversation** shows the lines and PMOS's notes in time order. The line being played is marked and kept in view. Choose **All**, **Key points** or **PMOS**, or search for a word.
|
|
213
213
|
|
|
214
|
+
- A meeting that you stopped and started again has **2 recordings** (or more) beside its title. A hatched part of the track is a time when nothing was recorded, and a line under the track gives its times and length. A press on the hatched part, or playing across it, goes on from where the recording began again. The sound keeps one clock, so every line, mark and screen shot stays at its own time.
|
|
215
|
+
|
|
214
216
|
The page reads only records on this machine: the note, the file Focus kept for the meeting, and the screen shots.
|
|
215
217
|
|
|
218
|
+
You can open the replay from a link. The link is the address of the console with `?replay=` and the id of the note, for example `/c3.html?replay=n-abcd-1234`. It works on the computer that holds the note. On another computer, or for a note that you deleted, the page says that the link does not match a note on this computer.
|
|
219
|
+
|
|
216
220
|
### Clean up the replay sound
|
|
217
221
|
|
|
218
222
|
The replay can play a cleaned copy of the sound. The copy has less rumble and hiss, and an even loudness. It needs **ffmpeg** on your machine. To install it, run `brew install ffmpeg` in a terminal.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Shape what a report says: the Structure step"
|
|
4
4
|
description: "How to decide with PMOS what a report answers, where each number comes from and how it is worked out, in the Structure step of the report studio, and how to try the plan on real data before you design it."
|
|
5
|
-
timestamp: 2026-10-
|
|
5
|
+
timestamp: 2026-10-15
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Shape what a report says
|
|
@@ -40,8 +40,8 @@ The plan has these parts:
|
|
|
40
40
|
- **Sources.** Where the numbers come from. They are in two groups.
|
|
41
41
|
- **From PMOS.** A portfolio, board or initiative, a dashboard, a chart, a document, or a past report. A document gives PMOS context for its notes, never numbers. A past report gives last cycle's numbers. In the first cycle there is nothing to read.
|
|
42
42
|
- **Outside PMOS.** A connector you have (for example Google Analytics through Supermetrics), a link, a file you upload each cycle, a person who gives a number each cycle, a page read in Chrome, or an app on your Mac.
|
|
43
|
-
- Each source shows its state: **Reached**, **Not connected**, **First cycle: nothing to read** or **Needs you**. It also shows how it is read and how many charts use it. Press **Test pull** to
|
|
44
|
-
- **Charts.** Every number is a chart: one question, its source, how it is worked out, what it is compared with, and when it is flagged. A chart that PMOS proposes is tagged **New chart**. A chart that already exists in Reports is tagged **From Charts**. Press a chart to see the question, where it is used, exactly what PMOS reads, why PMOS proposed it and its test value. Press **Discuss this chart** to put its name in the box. Press **Open in Charts** to see a chart from Charts. Press **Remove it** to make the next version without the chart.
|
|
43
|
+
- Each source shows its state: **Reached**, **Not connected**, **First cycle: nothing to read** or **Needs you**. It also shows how it is read and how many charts use it. Press **Test pull** to have PMOS read that one source now, for the last complete period. The row shows **Reading…**, then **Pulled** with the period and how many charts returned a number. If the source returns nothing, the row says **Returned nothing** and why. A test pull adds nothing to the conversation and does not make a new version. It works while PMOS makes a version. Press **Test pull again** to read it again. Press **Check it works** on a Chrome page or an app on your Mac. Press **How to connect** for a connector that is not connected. Press **Open** for a source in PMOS.
|
|
44
|
+
- **Charts.** Every number is a chart: one question, its source, how it is worked out, what it is compared with, and when it is flagged. A chart that PMOS proposes is tagged **New chart**. A chart that already exists in Reports is tagged **From Charts**. Press a chart to see the question, where it is used, exactly what PMOS reads, why PMOS proposed it and its test pull. After a test pull of its source, the test pull shows the value, what it was before, the change, a **Flagged** tag, where PMOS pulled it and when. A test pull is not saved in the report. Press **Discuss this chart** to put its name in the box. Press **Open in Charts** to see a chart from Charts. Press **Remove it** to make the next version without the chart.
|
|
45
45
|
- **Outline.** The sections in order. Each section lists its charts and says if PMOS writes notes on them. **Headline**, **Needs your call** and **Sources** are in every report. You decide everything between them.
|
|
46
46
|
|
|
47
47
|
Press **Other structures** to read the structures PMOS proposed side by side. The one PMOS recommends is first. Press **Use this instead** to put the choice in the box, then press **Make the next version**.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Reports: draft, read and issue"
|
|
4
4
|
description: "How to draft a report, add your notes, issue it, and print or copy it. How the report schedules and the Today cards for reports work."
|
|
5
|
-
timestamp: 2026-10-
|
|
5
|
+
timestamp: 2026-10-15
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Reports: draft, read and issue
|
|
@@ -31,6 +31,8 @@ The parts that this guide describes below, such as Draft now, Edit notes and the
|
|
|
31
31
|
1. Press **Draft now**. The workspace writes the draft of the cadence you have selected, for example biweekly. A cadence is one kind of report: daily, weekly, biweekly or monthly.
|
|
32
32
|
2. The new draft is selected. Every number in it comes from the repository's ledgers. The page never changes a number.
|
|
33
33
|
|
|
34
|
+
For a report with a saved structure, **Draft now** starts a session that follows the structure. The button shows **Drafting…** until the draft is ready, and a notice says when it is. A **Redraft**, and a draft at a slot you name, stay instant. A report with no saved structure is drafted at once.
|
|
35
|
+
|
|
34
36
|
A second **Draft now** for the same slot is refused. Press **Redraft** to write the draft again. A Redraft keeps every note. It moves a note with its initiative, even when the order of the initiatives changed. A note of an initiative that left the window stays at the end, under "Notes kept from an earlier draft".
|
|
35
37
|
|
|
36
38
|
To draft another cadence, open the menu with the three dots and choose **Draft now** with the name of the cadence.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Set up a report by asking, and the report finder"
|
|
4
4
|
description: "How to ask PMOS to draft a report you send, how the report finder finds reports you already send, how to set one up, and how a cycle becomes a document you issue."
|
|
5
|
-
timestamp: 2026-10-
|
|
5
|
+
timestamp: 2026-10-15
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Set up a report by asking, and the report finder
|
|
@@ -41,6 +41,10 @@ The most likely finding also shows on the Briefing as **Report found**. Press **
|
|
|
41
41
|
|
|
42
42
|
- **Open** opens the current draft.
|
|
43
43
|
- **Draft it now** shows in place of Open when the report has no draft. PMOS writes the draft of the next cycle and opens it.
|
|
44
|
+
- A report with a saved structure is drafted by a session, as the schedule drafts it, so the draft follows the structure. The row shows **Drafting…** with the saved structure it follows. You can leave the page. A notice with **Open** appears when the draft is ready. Press **Watch the session** to see the session work. A second press while it drafts does nothing.
|
|
45
|
+
- If a Chrome page or an app on your Mac needs a check, the row shows **Waiting for a check** and names the source. The draft starts when the check passes, or after 4 hours without that source.
|
|
46
|
+
- If the drafting session ends before it writes a draft, the row says **Draft did not finish**. Press **Draft it now** to try again.
|
|
47
|
+
- A report with no saved structure is drafted at once, as before.
|
|
44
48
|
- **All cycles** lists every cycle of the report.
|
|
45
49
|
- **Pause** stops the drafts. **Resume** starts them again.
|
|
46
50
|
- **Shape the report** opens the Structure step of the report studio. There you decide what the report answers, where each number comes from and how it is worked out. See [Shape what a report says](report-structure.md). **Shape it with PMOS** on an answer sets the report up and opens the same step.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* (initiative workspace-headless-jobs; PM decision 2026-10-11: keep Jev, no direct Claude API, background
|
|
4
4
|
* jobs that need no human run headless).
|
|
5
5
|
*
|
|
6
|
-
* kindFor({ agent, origin, skill, env }) → 'notes' | 'skill' | 'ask' | 'drive' | 'scan-web' | 'scan-repo' | 'scan-research-web' | 'scan-research-repo' | 'report-setup' | 'report-find' | 'core-doc' | 'report-design' | 'report-sample-fetch' | 'report-structure' | 'report-structure-dry' | null (null: open a pane, as before)
|
|
6
|
+
* kindFor({ agent, origin, skill, env }) → 'notes' | 'skill' | 'ask' | 'drive' | 'scan-web' | 'scan-repo' | 'scan-research-web' | 'scan-research-repo' | 'report-setup' | 'report-find' | 'core-doc' | 'report-design' | 'report-structure-pull' | 'report-sample-fetch' | 'report-structure' | 'report-structure-dry' | null (null: open a pane, as before)
|
|
7
7
|
* argsFor(kind, { modelArgs, addDirs, writePaths }) → the argv after `claude` (writePaths: the one render file a report-design job may write)
|
|
8
8
|
* parseResult(stdout) → { ok, text, isError, turns, durationMs, costUsd, denied[] }
|
|
9
9
|
* streamLines(line) → { log: [lines], result: object|null } one stream-json event, for the live log
|
|
@@ -90,7 +90,9 @@ const REPORT_SAMPLE_FETCH_ORIGINS = Object.freeze(['report-sample-fetch:']);
|
|
|
90
90
|
// report-structure: a structure turn and a dry run on real data (origins `report-structure:<id>`, `report-structure-dry:<id>`).
|
|
91
91
|
const REPORT_STRUCTURE_ORIGINS = Object.freeze(['report-structure:']);
|
|
92
92
|
const REPORT_STRUCTURE_DRY_ORIGINS = Object.freeze(['report-structure-dry:']);
|
|
93
|
-
|
|
93
|
+
// report-structure-gaps: Test pull reads ONE source for the last complete period (origin `report-structure-pull:<id>:<source>`).
|
|
94
|
+
const REPORT_STRUCTURE_PULL_ORIGINS = Object.freeze(['report-structure-pull:']);
|
|
95
|
+
const WRITE_PATH_KINDS = Object.freeze(['report-design', 'report-sample-fetch', 'report-structure', 'report-structure-dry', 'report-structure-pull']); // kinds whose one writable file the caller names (argsFor writePaths)
|
|
94
96
|
|
|
95
97
|
// What each kind may use. Read-only search is always allowed. The notes passes write their JSON (and rename the .tmp
|
|
96
98
|
// file) in their notes folder, which --add-dir opens to them; the skills also run `pmos` verbs and repository scripts.
|
|
@@ -125,6 +127,8 @@ const TOOLS = Object.freeze({
|
|
|
125
127
|
// The structure runs: read and search; a connector read runs only where the PM's own settings allow it. Their one write, the JSON file, is added per run by argsFor.
|
|
126
128
|
'report-structure': Object.freeze(['Read', 'Glob', 'Grep']),
|
|
127
129
|
'report-structure-dry': Object.freeze(['Read', 'Glob', 'Grep']),
|
|
130
|
+
// The pull run: the same read tools as the dry run. One source, one JSON file, no Bash, no web fetch, nothing that sends.
|
|
131
|
+
'report-structure-pull': Object.freeze(['Read', 'Glob', 'Grep']),
|
|
128
132
|
skill: Object.freeze(['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash(pmos *)', 'Bash(node *)',
|
|
129
133
|
'Bash(git log *)', 'Bash(git status *)', 'Bash(git diff *)', 'Bash(ls *)', 'Bash(date *)']),
|
|
130
134
|
});
|
|
@@ -152,6 +156,7 @@ function kindFor({ agent, origin, skill, env } = {}) {
|
|
|
152
156
|
if (REPORT_SAMPLE_FETCH_ORIGINS.some((p) => o.startsWith(p))) return 'report-sample-fetch';
|
|
153
157
|
// 'report-structure-dry:' is not a prefix of 'report-structure:' (the colon differs), so the two are told apart by their prefix alone
|
|
154
158
|
if (REPORT_STRUCTURE_DRY_ORIGINS.some((p) => o.startsWith(p))) return 'report-structure-dry';
|
|
159
|
+
if (REPORT_STRUCTURE_PULL_ORIGINS.some((p) => o.startsWith(p))) return 'report-structure-pull';
|
|
155
160
|
if (REPORT_STRUCTURE_ORIGINS.some((p) => o.startsWith(p))) return 'report-structure';
|
|
156
161
|
if (skill && HEADLESS_SKILLS.includes(String(skill))) return 'skill';
|
|
157
162
|
return null;
|
|
@@ -212,6 +217,11 @@ function parseResult(stdout) {
|
|
|
212
217
|
};
|
|
213
218
|
}
|
|
214
219
|
|
|
220
|
+
/** Did the run fail because Claude is signed out on this computer? The CLI prints "Failed to authenticate: OAuth session expired…" and exits 1. */
|
|
221
|
+
const SIGNED_OUT_RE = /failed to authenticate|oauth (?:session|token) (?:has )?expired|could not be refreshed|please run \/login|not logged in|invalid (?:api key|bearer token)|authentication_error/i;
|
|
222
|
+
const signedOut = (text) => SIGNED_OUT_RE.test(String(text || ''));
|
|
223
|
+
const SIGNED_OUT_WHY = 'Claude is signed out on this computer. Open Terminal, run `claude`, type /login and sign in. PMOS carries on by itself afterwards.';
|
|
224
|
+
|
|
215
225
|
const oneLine = (v, max) => { const t = String(v == null ? '' : v).replace(/\s+/g, ' ').trim(); return t.length > max ? t.slice(0, max - 1) + '…' : t; };
|
|
216
226
|
// The one argument that says what a tool call does: the command, the file, the pattern, the address.
|
|
217
227
|
function toolWhat(input) {
|
|
@@ -255,17 +265,19 @@ function outcomeOf(r, { code = null, signal = null, timedOut = false, stopped =
|
|
|
255
265
|
const first = String(res.text || '').split('\n').map((x) => x.replace(/^[#>*\-\s]+/, '').trim()).find(Boolean) || '';
|
|
256
266
|
const denied = Array.isArray(res.denied) ? res.denied : [];
|
|
257
267
|
const base = { turns: res.turns == null ? null : res.turns, durationMs: res.durationMs == null ? null : res.durationMs, denied };
|
|
268
|
+
const auth = !timedOut && !stopped && signedOut(res.text);
|
|
258
269
|
const ok = !timedOut && !stopped && !signal && code === 0 && res.ok && !res.isError;
|
|
259
270
|
if (ok) return { ok: true, headline: oneLine(first || 'Finished. It wrote no summary.', 160), why: null, ...base };
|
|
260
271
|
let why;
|
|
261
272
|
if (stopped) why = 'Stopped by you.';
|
|
262
273
|
else if (timedOut) why = `Stopped: still running after ${timeoutMin || Math.round(DEFAULT_TIMEOUT_MS / 60000)} minutes.`;
|
|
263
274
|
else if (signal) why = `The process was ended (${signal}).`;
|
|
275
|
+
else if (signedOut(res.text)) why = SIGNED_OUT_WHY;
|
|
264
276
|
else if (!res.ok) why = `It exited with code ${code} and printed no result${res.text ? ': ' + oneLine(res.text.split('\n').filter(Boolean).pop(), 200) : '.'}`;
|
|
265
277
|
else if (res.isError) why = `The run reported an error${first ? ': ' + oneLine(first, 200) : '.'}`;
|
|
266
278
|
else why = `It exited with code ${code}.`;
|
|
267
279
|
if (denied.length) why += ` Not allowed in a background run: ${denied.join(', ')}.`;
|
|
268
|
-
return { ok: false, headline: null, why, ...base };
|
|
280
|
+
return { ok: false, headline: null, why, ...(auth ? { auth: true } : {}), ...base };
|
|
269
281
|
}
|
|
270
282
|
|
|
271
283
|
function logLines(r, exitCode) {
|
|
@@ -279,4 +291,4 @@ function logLines(r, exitCode) {
|
|
|
279
291
|
return out;
|
|
280
292
|
}
|
|
281
293
|
|
|
282
|
-
module.exports = { HEADLESS_SKILLS, HEADLESS_ORIGINS, ASK_ORIGINS, DRIVE_ORIGINS, SCAN_WEB_ORIGINS, SCAN_REPO_ORIGINS, SCAN_RESEARCH_WEB_ORIGINS, SCAN_RESEARCH_REPO_ORIGINS, WEB_KINDS, REPORT_SETUP_ORIGINS, REPORT_FIND_ORIGINS, CORE_DOC_ORIGINS, REPORT_DESIGN_ORIGINS, REPORT_SAMPLE_FETCH_ORIGINS, REPORT_STRUCTURE_ORIGINS, REPORT_STRUCTURE_DRY_ORIGINS, TOOLS, DEFAULT_TIMEOUT_MS, kindFor, argsFor, timeoutMs, parseResult, streamLines, outcomeOf, logLines, switchedOff };
|
|
294
|
+
module.exports = { HEADLESS_SKILLS, HEADLESS_ORIGINS, ASK_ORIGINS, DRIVE_ORIGINS, SCAN_WEB_ORIGINS, SCAN_REPO_ORIGINS, SCAN_RESEARCH_WEB_ORIGINS, SCAN_RESEARCH_REPO_ORIGINS, WEB_KINDS, REPORT_SETUP_ORIGINS, REPORT_FIND_ORIGINS, CORE_DOC_ORIGINS, REPORT_DESIGN_ORIGINS, REPORT_SAMPLE_FETCH_ORIGINS, REPORT_STRUCTURE_ORIGINS, REPORT_STRUCTURE_DRY_ORIGINS, TOOLS, DEFAULT_TIMEOUT_MS, kindFor, argsFor, timeoutMs, parseResult, streamLines, outcomeOf, signedOut, SIGNED_OUT_WHY, logLines, switchedOff };
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
const CATS = Object.freeze(['Answer', 'Fact check', 'Decision', 'Time', 'Topics', 'Context', 'Prep', 'Follow-up']);
|
|
25
25
|
const REF_TYPES = Object.freeze(['initiative', 'plan-item', 'release', 'item', 'jira', 'ask', 'report', 'note', 'decision', 'portfolio', 'event']);
|
|
26
26
|
const MIN_GAP_MS = 40000; // the companion runs at most this often per note
|
|
27
|
+
const AUTH_WAIT_MS = 120000; // after a run found Claude signed out, wait this long before the next try
|
|
27
28
|
const TAIL_MINUTES = 5; // how much of the talk the companion reads
|
|
28
29
|
const TAIL_MAX = 40; // and at most this many lines
|
|
29
30
|
const TOPICS_MAX = 8;
|
|
@@ -231,7 +232,7 @@ function foldState(lines, { topics = [], startMs = null, nowMs = Date.now() } =
|
|
|
231
232
|
return byKey.get(k);
|
|
232
233
|
};
|
|
233
234
|
for (const t of topics) addTopic(t.text || t, t.src || 'steps');
|
|
234
|
-
let current = null; const runs = []; let gist = ''; let chips = []; const notes = []; let seen = 0; let lastRunAt = 0; let pending = false; let error = null;
|
|
235
|
+
let current = null; const runs = []; let gist = ''; let chips = []; const notes = []; let seen = 0; let lastRunAt = 0; let pending = false; let error = null; let authAt = 0;
|
|
235
236
|
const asks = new Map();
|
|
236
237
|
const close = (at) => { const r = runs[runs.length - 1]; if (r && r.to == null) r.to = at; };
|
|
237
238
|
const switchTo = (row, at) => { if (!row || (current && current.id === row.id && runs.length && runs[runs.length - 1].to == null)) return; close(at); row.parked = false; current = row; runs.push({ topic: row.id, from: at, to: null }); };
|
|
@@ -240,9 +241,9 @@ function foldState(lines, { topics = [], startMs = null, nowMs = Date.now() } =
|
|
|
240
241
|
if (!l || typeof l !== 'object') continue;
|
|
241
242
|
const at = Number.isFinite(l.at) ? l.at : 0;
|
|
242
243
|
if (l.k === 'run') { lastRunAt = at; seen = Number.isFinite(l.lines) ? l.lines : seen; pending = true; }
|
|
243
|
-
else if (l.k === 'fail') { if (!l.id) { pending = false; error = str(l.error, 200) || 'The companion did not answer.'; } else if (asks.has(l.id)) asks.get(l.id).state = 'failed', asks.get(l.id).error = str(l.error, 200); }
|
|
244
|
+
else if (l.k === 'fail') { if (!l.id) { pending = false; error = str(l.error, 200) || 'The companion did not answer.'; authAt = l.auth ? at : 0; } else if (asks.has(l.id)) asks.get(l.id).state = 'failed', asks.get(l.id).error = str(l.error, 200); }
|
|
244
245
|
else if (l.k === 'out') {
|
|
245
|
-
pending = false; error = null;
|
|
246
|
+
pending = false; error = null; authAt = 0;
|
|
246
247
|
if (l.gist) gist = l.gist;
|
|
247
248
|
if (Array.isArray(l.chips)) chips = l.chips;
|
|
248
249
|
const tp = l.topic || {};
|
|
@@ -271,7 +272,7 @@ function foldState(lines, { topics = [], startMs = null, nowMs = Date.now() } =
|
|
|
271
272
|
const state = current && current.id === r.id ? 'now' : r.parked ? 'parked' : spent(r.id) > 0 ? 'discussed' : 'notyet';
|
|
272
273
|
return { id: r.id, text: r.text, src: r.src, state, minutes, ...(r.at ? { at: r.at } : {}), ...(r.by ? { by: r.by } : {}) };
|
|
273
274
|
});
|
|
274
|
-
return { gist, chips, current: current ? current.id : null, topics: outTopics, runs: outRuns, notes, seen, last_run_at: lastRunAt, pending, asks: [...asks.values()], error };
|
|
275
|
+
return { gist, chips, current: current ? current.id : null, topics: outTopics, runs: outRuns, notes, seen, last_run_at: lastRunAt, pending, asks: [...asks.values()], error, auth_at: authAt };
|
|
275
276
|
}
|
|
276
277
|
|
|
277
278
|
/* ── when the companion runs ────────────────────────────────────────────────────────────────────────────────────── */
|
|
@@ -283,6 +284,7 @@ const off = (env) => /^(off|0|false|no)$/i.test(String((env || process.env).PMOS
|
|
|
283
284
|
function shouldRun(now, state, note, opts = {}) {
|
|
284
285
|
const st = state || {};
|
|
285
286
|
if (off(opts.env)) return { run: false, reason: 'off' };
|
|
287
|
+
if (st.auth_at && now - st.auth_at < AUTH_WAIT_MS) return { run: false, reason: 'signed out' };
|
|
286
288
|
if (!note || note.state !== 'recording') return { run: false, reason: 'not recording' };
|
|
287
289
|
if (st.pending && st.last_run_at && now - st.last_run_at < (opts.timeoutMs || 20 * 60000)) return { run: false, reason: 'busy' };
|
|
288
290
|
if (!(note.lines > (st.seen || 0))) return { run: false, reason: 'no new lines' };
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
* runs: [{ topic, n, from_ms, to_ms }]
|
|
25
25
|
* notes: [{ id, t, cat, body, heard, why, src }] PMOS's notes with the time they were made
|
|
26
26
|
* shots: [{ t, w, h, why?, text? }] kept shots only
|
|
27
|
+
* recordings: how many times the recording was started (1 for most notes)
|
|
28
|
+
* gaps: [{ from_ms, to_ms }] where the recording was off between two starts (empty when there was one)
|
|
27
29
|
* aside_count }
|
|
28
30
|
*/
|
|
29
31
|
const N = require('./notes.js');
|
|
@@ -154,6 +156,18 @@ function build({ note, event = null, stateLines = [], frames = [], audio = null,
|
|
|
154
156
|
const topicRows = [...visited, ...fold.topics.filter((t) => !firstOf.has(t.id))].map((t) => ({
|
|
155
157
|
id: t.id, text: t.text, src: t.src, state: t.state, minutes: minutesOf(t.id), n: numOf.get(t.id) || null, first_t: firstOf.has(t.id) ? firstOf.get(t.id) : null,
|
|
156
158
|
stretches: stretches(t.id), parked: t.state === 'parked', by: t.by || null }));
|
|
159
|
+
// A recording that was stopped and started again (a Pause then Listen, or a new Listen on the same meeting) has one stretch per start. The audio keeps one clock
|
|
160
|
+
// (lib/notes-audio.js places a late chunk at its own time and leaves the gap silent), so the replay needs only to show where the recording was off.
|
|
161
|
+
const sessions = (Array.isArray(n.sessions) ? n.sessions : []).filter((x) => x && Number.isFinite(Date.parse(x.from))).sort((a, b) => Date.parse(a.from) - Date.parse(b.from));
|
|
162
|
+
const gaps = [];
|
|
163
|
+
if (start != null) {
|
|
164
|
+
for (let i = 1; i < sessions.length; i++) {
|
|
165
|
+
const off = Date.parse(sessions[i - 1].to); const on = Date.parse(sessions[i].from);
|
|
166
|
+
if (!Number.isFinite(off) || !Number.isFinite(on) || on <= off) continue;
|
|
167
|
+
const from_ms = Math.max(0, Math.round(off - start)); const to_ms = Math.min(length, Math.round(on - start));
|
|
168
|
+
if (to_ms > from_ms) gaps.push({ from_ms, to_ms });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
157
171
|
const ev = event || {};
|
|
158
172
|
const m = n.meeting || {};
|
|
159
173
|
return {
|
|
@@ -162,7 +176,7 @@ function build({ note, event = null, stateLines = [], frames = [], audio = null,
|
|
|
162
176
|
meeting: { title: ev.title || m.title || n.title || 'Untitled note', at: ev.at || m.start || n.recorded_from || n.created_at || null, end: ev.end || m.end || null,
|
|
163
177
|
attendees: [].concat(ev.attendees || ev.people || []).map((p) => (p && typeof p === 'object' ? p.name || p.id : p)).filter(Boolean).map(String) },
|
|
164
178
|
audio: audio && audio.url ? { url: audio.url, ms: finite(audio.ms) ? audio.ms : null } : null,
|
|
165
|
-
start_ms: start, length_ms: length,
|
|
179
|
+
start_ms: start, length_ms: length, recordings: Math.max(1, sessions.length), gaps,
|
|
166
180
|
lines, keys, topics: topicRows, runs: runs.map((r) => ({ ...r, n: numOf.get(r.topic) || null })), notes: pmos, shots, aside_count: asideCount,
|
|
167
181
|
};
|
|
168
182
|
}
|