create-pmos 0.518.0 → 0.519.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 +26 -32
- 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/board-columns-test.js +1 -1
- package/workspace/board-programs-test.js +26 -7
- package/workspace/briefing-premium-board-test.js +3 -3
- package/workspace/calm-timeline-test.js +2 -2
- package/workspace/changelog.json +25 -0
- package/workspace/guide/index.md +2 -2
- package/workspace/guide/next-prompt.md +4 -2
- package/workspace/guide/programs.md +2 -1
- package/workspace/guide/settings.md +1 -1
- package/workspace/lib/headless.js +12 -2
- package/workspace/lib/routes-next-prompt.js +67 -27
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/bd-span.js +13 -17
- package/workspace/public/pm/cozy-settings.js +8 -7
- package/workspace/public/pm/premium-vw-guide.css +7 -2
- package/workspace/server.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.519.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,31 @@
|
|
|
32
32
|
"audit"
|
|
33
33
|
],
|
|
34
34
|
"pmosChanges": [
|
|
35
|
+
{
|
|
36
|
+
"v": "0.519.0",
|
|
37
|
+
"at": "2026-11-04T01:26:27.710Z",
|
|
38
|
+
"f": 4,
|
|
39
|
+
"prs": [
|
|
40
|
+
{
|
|
41
|
+
"n": 1213,
|
|
42
|
+
"t": "Every undated initiative is a Timeline row with No dates",
|
|
43
|
+
"s": "Every undated initiative is a Timeline row with No dates",
|
|
44
|
+
"k": "fix"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"n": 1214,
|
|
48
|
+
"t": "Chat prompt suggestions run on claude -p by default, and never fail silently",
|
|
49
|
+
"s": "Chat prompt suggestions run on claude -p by default, and never fail silently",
|
|
50
|
+
"k": "fix"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"n": 1215,
|
|
54
|
+
"t": "Background runs read long connector results; guide fills the wide Viewer",
|
|
55
|
+
"s": "Background runs read long connector results; guide fills the wide Viewer",
|
|
56
|
+
"k": "fix"
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
},
|
|
35
60
|
{
|
|
36
61
|
"v": "0.518.0",
|
|
37
62
|
"at": "2026-11-04T00:33:13.219Z",
|
|
@@ -373,37 +398,6 @@
|
|
|
373
398
|
"d": "A point captured from a meeting could not be removed. Now you can delete it from its edit card."
|
|
374
399
|
}
|
|
375
400
|
]
|
|
376
|
-
},
|
|
377
|
-
{
|
|
378
|
-
"v": "0.499.0",
|
|
379
|
-
"at": "2026-11-03T03:30:26.339Z",
|
|
380
|
-
"f": 4,
|
|
381
|
-
"prs": [
|
|
382
|
-
{
|
|
383
|
-
"n": 1180,
|
|
384
|
-
"t": "The context meter knows the default model is 1M",
|
|
385
|
-
"s": "The context meter knows the default model is 1M",
|
|
386
|
-
"k": "fix",
|
|
387
|
-
"h": "The context meter reads right",
|
|
388
|
-
"d": "The context meter assumed a smaller model and filled up too early. Now it knows the default model has a much larger window."
|
|
389
|
-
},
|
|
390
|
-
{
|
|
391
|
-
"n": 1181,
|
|
392
|
-
"t": "`pmos initiative list` names records on a document lane",
|
|
393
|
-
"s": "pmos initiative list names records on a document lane",
|
|
394
|
-
"k": "fix",
|
|
395
|
-
"h": "Initiative lists name every record",
|
|
396
|
-
"d": "Listing initiatives missed the records of document work. Now it names them too."
|
|
397
|
-
},
|
|
398
|
-
{
|
|
399
|
-
"n": 1182,
|
|
400
|
-
"t": "Claude login test passes for a signed-in new user; picker shows each login once",
|
|
401
|
-
"s": "Claude login test passes for a signed-in new user; picker shows each login once",
|
|
402
|
-
"k": "fix",
|
|
403
|
-
"h": "Claude sign-in is checked properly",
|
|
404
|
-
"d": "The sign-in check could fail a user who was signed in, and the picker showed one login twice. Now it checks the real login and lists each one once."
|
|
405
|
-
}
|
|
406
|
-
]
|
|
407
401
|
}
|
|
408
402
|
]
|
|
409
403
|
}
|
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-04T01:26:24Z"}
|
|
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-04T01:26:24Z"}
|
|
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-04T01:26:24Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -113,7 +113,7 @@ const ITEMS = [
|
|
|
113
113
|
const before = await rowsIn(p);
|
|
114
114
|
await p.click('#ibBody [data-tsort="status"]'); await p.waitForTimeout(200);
|
|
115
115
|
const byStatus = await rowsIn(p), aria = await p.evaluate(() => document.querySelector('#ibBody [data-tsort="status"]').getAttribute('aria-sort'));
|
|
116
|
-
ok('S pressing Status puts late, at risk and on hold first', byStatus.slice(0,
|
|
116
|
+
ok('S pressing Status puts late, at risk and on hold first (the undated usage-billing is a row since batch 26 item 8)', byStatus.slice(0, 4).join() === 'lightbox,magw,usage-billing,email-otp' && aria === 'ascending', JSON.stringify(byStatus));
|
|
117
117
|
await p.click('#ibBody [data-tsort="status"]'); await p.waitForTimeout(200);
|
|
118
118
|
ok('S a second press goes back to time order', (await rowsIn(p)).join() === before.join(), JSON.stringify([before, await rowsIn(p)]));
|
|
119
119
|
// P: the Columns menu
|
|
@@ -218,9 +218,9 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
218
218
|
const row = [...d.querySelectorAll('.tg-r[data-row]')].find((r) => /Mobile OTP login/.test(r.textContent));
|
|
219
219
|
return { member: !!row, nd: row ? (row.querySelector('.tg-nd') || {}).textContent : '', loose: [...d.querySelectorAll('.tg-r[data-row]')].some((r) => /Loose undated/.test(r.textContent)), none: T.none.length };
|
|
220
220
|
});
|
|
221
|
-
ok('T an undated initiative in a program keeps its row under the program and says "No dates"; an undated one outside a program is
|
|
221
|
+
ok('T an undated initiative in a program keeps its row under the program and says "No dates"; an undated one outside a program is a row too (batch 8 item 4, batch 26 item 8)', nd.member && nd.nd === 'No dates' && nd.loose && nd.none === 2, JSON.stringify(nd));
|
|
222
222
|
// batch 11 item 19: By product, a program's undated initiative keeps its "No dates" row under its product (it disappeared once grouped by
|
|
223
|
-
// product); an undated one in no program is
|
|
223
|
+
// product); since batch 26 item 8 an undated one in no program is a "No dates" row too
|
|
224
224
|
const bnd = await p.evaluate(async () => {
|
|
225
225
|
const S = await import('/pm/bd-span.js'), now = new Date(2026, 9, 26);
|
|
226
226
|
const a = { key: 'board:a', id: 'a', title: 'Dated one', start: '2026-10-01', end: '2026-11-20', track: { key: 'build' } };
|
|
@@ -232,11 +232,11 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
232
232
|
const d = document.createElement('div'); d.innerHTML = T.html;
|
|
233
233
|
const rs = [...d.querySelectorAll('[data-row]')], row = rs.find((r) => r.dataset.row === 'board:b');
|
|
234
234
|
const out = { phone, rows: rs.map((r) => r.dataset.row), nd: row ? (row.querySelector('.tg-nd') || {}).textContent : '', none: T.none.length };
|
|
235
|
-
if (!(out.nd === 'No dates' && out.rows.join() === 'board:a,board:b' && out.none === 2)) return out;
|
|
235
|
+
if (!(out.nd === 'No dates' && out.rows.join() === 'board:a,board:b,board:c' && out.none === 2)) return out;
|
|
236
236
|
}
|
|
237
237
|
return true;
|
|
238
238
|
});
|
|
239
|
-
ok('T By product: a program\'s undated initiative keeps its "No dates" row under its product, desktop and phone; one in no program is
|
|
239
|
+
ok('T By product: a program\'s undated initiative keeps its "No dates" row under its product, desktop and phone; one in no program is a row too (batch 11 item 19, batch 26 item 8)', bnd === true, JSON.stringify(bnd));
|
|
240
240
|
// batch 14 item 2: a product whose initiatives are all undated (outside a program) kept no group, so it was missing from the Board
|
|
241
241
|
const pnd = await p.evaluate(async () => {
|
|
242
242
|
const S = await import('/pm/bd-span.js'), now = new Date(2026, 9, 26);
|
|
@@ -249,8 +249,27 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
249
249
|
const row = d.querySelector('[data-row="board:m"]');
|
|
250
250
|
return { heads: [...d.querySelectorAll('[data-prod]')].map((x) => x.dataset.prod).join(), nd: row ? (row.querySelector('.tg-nd') || {}).textContent : '', loose: !!d.querySelector('[data-row="board:c"]'), foot: ((d.querySelector('.tg-ft .tg-none span') || {}).textContent || '') };
|
|
251
251
|
});
|
|
252
|
-
ok('T a product with only undated initiatives keeps its head and its "No dates" rows
|
|
253
|
-
pnd.heads === 'p:x,p:ms' && pnd.nd === 'No dates' &&
|
|
252
|
+
ok('T a product with only undated initiatives keeps its head and its "No dates" rows (batch 14 item 2)',
|
|
253
|
+
pnd.heads === 'p:x,p:ms' && pnd.nd === 'No dates' && pnd.loose, JSON.stringify(pnd));
|
|
254
|
+
// batch 26 item 8 (Wawan: "8 have no dates" sat in the foot and none was a row): every undated initiative is a "No dates" row beside today, in
|
|
255
|
+
// every grouping (Programs first, By product, no groups), desktop and phone; the foot keeps the count and "Ask to propose dates", with no names
|
|
256
|
+
const all = await p.evaluate(async () => {
|
|
257
|
+
const S = await import('/pm/bd-span.js'), now = new Date(2026, 9, 26);
|
|
258
|
+
const r = (k, s, e) => ({ key: 'board:' + k, id: k, title: 'Row ' + k, start: s, end: e, track: { key: 'discover' } });
|
|
259
|
+
const a = r('a', '2026-10-01', '2026-11-20'), u1 = r('u1', null, null), u2 = r('u2', null, null), u3 = r('u3', null, null);
|
|
260
|
+
const prog = [{ kind: 'program', key: 'p', program: { id: 'p', title: 'P', products: [] }, products: [{ name: 'X', rows: [u1] }], rows: [u1] }, { kind: 'product', key: 'x', rows: [a, u2, u3] }];
|
|
261
|
+
const byProd = [{ kind: 'product', key: 'x', rows: [a, { ...u1, inProgram: 'p' }, u2] }, { kind: 'product', key: 'y', rows: [u3] }];
|
|
262
|
+
const bad = [];
|
|
263
|
+
for (const [name, groups] of [['programs', prog], ['product', byProd], ['none', undefined]]) for (const phone of [false, true]) {
|
|
264
|
+
const T = S.timelineHtml([a, u1, u2, u3], { jira: null }, 'quarter', { now, groups, phone, askHref: () => '#ask' });
|
|
265
|
+
const d = document.createElement('div'); d.innerHTML = T.html;
|
|
266
|
+
const nd = ['u1', 'u2', 'u3'].map((k) => d.querySelector(`[data-row="board:${k}"] .tg-nd`)).filter((x) => x && x.textContent === 'No dates').length;
|
|
267
|
+
const ft = d.querySelector(phone ? '.tg-none' : '.tg-ft .tg-none'), txt = ft ? ft.textContent : '';
|
|
268
|
+
if (nd !== 3 || !d.querySelector('#askDates') || !/3 have no dates/.test(txt) || /Row u/.test(txt)) bad.push({ name, phone, nd, txt });
|
|
269
|
+
}
|
|
270
|
+
return bad;
|
|
271
|
+
});
|
|
272
|
+
ok('T every undated initiative is a "No dates" row in every grouping, desktop and phone, in no program too; the foot keeps the count and Ask to propose dates (batch 26 item 8)', !all.length, JSON.stringify(all));
|
|
254
273
|
ok('T a legend in the chip row names done, planned, Milestone (Deliver, Evaluate and milestone tasks, batch 11 item 3) and today; no program milestone key while a program draws none (batch 13 item 4)', /Done in Jira/.test(t.lgd) && /To do/.test(t.lgd) && !/Program milestone/.test(t.lgd) && /Milestone/.test(t.lgd) && /Today/.test(t.lgd), t.lgd);
|
|
255
274
|
await p.click('#ibBody .tg-pg .tg-pgo');
|
|
256
275
|
// the program opens in the floating Viewer (shell A), which sits over the right of the timeline; close it before the next press
|
|
@@ -319,7 +338,7 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
319
338
|
// the phone (TsCPhone, TsCPhoneSpan): the timeline is a view; the span buttons become one picker beside the product picker
|
|
320
339
|
const { ctx, page: p, errors } = await open({ view: 'time', w: 390, h: 844, store: { 'pmos.board.pview': 'time', 'pmos.board.span': 'quarter' } });
|
|
321
340
|
const ph = await p.evaluate(() => ({ spans: !document.querySelector('#bdSpans').hidden, pick: (document.querySelector('#bdSpanBtn') || {}).textContent || '', rows: [...document.querySelectorAll('#ibBody .tgq-r .tgq-n b')].map((b) => b.textContent), names: document.querySelectorAll('#ibBody .tgq-r[data-row] .tgq-n').length }));
|
|
322
|
-
ok('S phone: one span picker beside the product, no span row; each row carries its name above its bar, Monitor and Evaluate rows too (batch 8 item 2)', !ph.spans && ph.pick === 'Quarter' && ph.names ===
|
|
341
|
+
ok('S phone: one span picker beside the product, no span row; each row carries its name above its bar, Monitor and Evaluate rows too (batch 8 item 2), and the undated one as a "No dates" row (batch 26 item 8)', !ph.spans && ph.pick === 'Quarter' && ph.names === 4 && ph.rows.includes('Usage-based billing') && ph.rows.includes('Legacy IdP sunset') && ph.rows.includes('Inline login for eShop checkout'), JSON.stringify(ph));
|
|
323
342
|
await p.click('#bdSpanBtn'); await p.click('#bdSpanMenu [data-spick="year"]'); await p.waitForTimeout(200);
|
|
324
343
|
ok('S phone: the picker changes the span', await p.evaluate(() => document.querySelector('#ibBody .tg-s').dataset.span === 'year' && document.querySelector('#bdSpanBtn').textContent === 'Year' && localStorage.getItem('pmos.board.span') === 'year'));
|
|
325
344
|
await p.evaluate(() => { const sc = document.querySelector('#ibBody .tg-sc'); sc.scrollLeft = sc.scrollWidth; }); await p.waitForTimeout(200);
|
|
@@ -337,7 +337,7 @@ const IVREAD = () => {
|
|
|
337
337
|
* timeline spans C: TsAllSpans); the scroll itself is checked in board-programs-test.js (T) */
|
|
338
338
|
await view(p, 'time');
|
|
339
339
|
const tl = await p.evaluate(() => ({ spans: [...document.querySelectorAll('#bdSpans [data-sp]')].map((b) => b.textContent + (b.getAttribute('aria-checked') === 'true' ? '*' : '')).join(), shown: !document.querySelector('#bdSpans').hidden, rows: [...document.querySelectorAll('#ibBody .tg-r .tg-n.ini2 b')].map((b) => b.textContent), bars: document.querySelectorAll('#ibBody .tg-bar').length, none: (document.querySelector('.tg-none') || {}).textContent || '', ask: (document.querySelector('#askDates') || {}).href || '', hid: (document.querySelector('.tg-hid') || {}).textContent || '', lgd: (document.querySelector('.tg-lgd') || {}).textContent || '', today: (document.querySelector('#ibBody .tg-tpill') || {}).textContent === 'Today' }));
|
|
340
|
-
ok('B6 the timeline has six spans and starts on Quarter, one row per initiative with dates
|
|
340
|
+
ok('B6 the timeline has six spans and starts on Quarter, one row per initiative; the 4 with no dates are rows too (batch 26 item 8), counted in the foot with an ask to propose dates', tl.shown && tl.spans === 'Sprint,Quarter*,Half,Year,2 yr,5 yr' && tl.rows.length === 7 && ['Usage-based billing', 'Audit log export', 'Refund reason codes', 'Docs search'].every((n) => tl.rows.includes(n)) && tl.bars === 3 && /^4 have no dates/.test(tl.none) && !/Docs search/.test(tl.none) && /\/premium\.html\?ask=board%3Aw1&q=Propose/.test(tl.ask), JSON.stringify(tl));
|
|
341
341
|
ok('B6 Monitor and Evaluate show on the Quarter, with no hidden count in the footer (batch 8 item 2); the legend (PgTimeline: done, planned, the Jira forecast, Milestone (batch 11 item 3), today) and Today are drawn', tl.hid === '' && /Done in Jira/.test(tl.lgd) && /To do/.test(tl.lgd) && /Jira forecast/.test(tl.lgd) && /Milestone/.test(tl.lgd) && /Today/.test(tl.lgd) && tl.today, JSON.stringify(tl));
|
|
342
342
|
ok('B6 the spans help popover went with the theme grouping (TsCGrill): the first head is a plain "Initiative"', await p.evaluate(() => !document.querySelector('#popRules') && !document.querySelector('#ibBody [data-rules]') && /^Initiative/.test(document.querySelector('#ibBody .tg-h .tg-n').textContent)));
|
|
343
343
|
await p.click('#ibBody .tg-n[data-key="board:partner-rate-limits"]');
|
|
@@ -361,7 +361,7 @@ const IVREAD = () => {
|
|
|
361
361
|
ok('B6 Year draws Quarter\'s rows (TsAllSpans): one row per initiative with its bar, months as columns; no themes, no Group chip; the chips and the legend stay', yr.rows.join() === tl.rows.join() && yr.bars === 3 && !yr.themes && !yr.group && yr.lgd && yr.jira && yr.heads.length >= 12 && yr.heads.every((h) => /^[A-Z][a-z]{2}$/.test(h)), JSON.stringify(yr));
|
|
362
362
|
await spanTo(p, 'y5');
|
|
363
363
|
const y5 = await p.evaluate(() => ({ rows: document.querySelectorAll('#ibBody .tg-q[data-span="y5"] .tg-n.ini2').length, heads: [...document.querySelectorAll('#ibBody .tg-h .tg-p > span[data-y] b')].map((b) => b.textContent), dates: [...document.querySelectorAll('#ibBody .tg-endl, #ibBody .tg-lab, #ibBody .tg-cms')].map((e) => e.innerText.replace(/\s+/g, ' ').trim()) })); // with the Next milestone column (batch 19 item 5) the column says it
|
|
364
|
-
ok('B6 5 yr draws the same rows with years as columns, and each bar ends with what lands, then when (batch 13 item 4)', y5.rows ===
|
|
364
|
+
ok('B6 5 yr draws the same rows with years as columns, and each bar ends with what lands, then when (batch 13 item 4)', y5.rows === 7 && y5.heads.length >= 5 && y5.heads.every((h) => /^\d{4}$/.test(h)) && y5.dates.length === 3 && y5.dates.every((d) => /^[A-Z][\w ]* [A-Z][a-z]{2} \d{1,2}( ’\d\d)?( \+\d+d)?$/.test(d)), JSON.stringify(y5));
|
|
365
365
|
const sp = [];
|
|
366
366
|
for (const [k, sel, re] of [['sprint', '.sp[data-span="sprint"]', /Initiative and ticket.*Backlog · \d+ not in/], ['half', '.tg-q[data-span="half"]', /Self-serve onboarding v2/], ['y2', '.tg-q[data-span="y2"]', /Self-serve onboarding v2/]]) { await spanTo(p, k); sp.push(await p.evaluate(([q2, r]) => { const e = document.querySelector('#ibBody ' + q2); return !!e && !document.querySelector('#ibBody .bd-wtag') && new RegExp(r).test(e.textContent); }, [sel, re.source])); }
|
|
367
367
|
ok('B6 Sprint (BdSpans: tickets and the backlog row), Half and 2 yr (Quarter\'s rows) are drawn, with no "Waiting for design" tag', sp.every(Boolean), JSON.stringify(sp));
|
|
@@ -369,7 +369,7 @@ const IVREAD = () => {
|
|
|
369
369
|
{
|
|
370
370
|
const f = F.facts(); f.rows = f.rows.map((r) => ({ ...r, start: null, end: null }));
|
|
371
371
|
const R = await open({ model: BP.build(f) }); await view(R.page, 'time');
|
|
372
|
-
ok('B6 with no dates anywhere it draws
|
|
372
|
+
ok('B6 with no dates anywhere it draws no bar; every initiative is a "No dates" row (batch 26 item 8)', await R.page.evaluate(() => document.querySelectorAll('#ibBody .tg-bar').length === 0 && document.querySelectorAll('#ibBody .tg-r[data-row]').length > 0 && [...document.querySelectorAll('#ibBody .tg-r[data-row]')].every((r) => !!r.querySelector('.tg-nd'))));
|
|
373
373
|
await R.ctx.close();
|
|
374
374
|
}
|
|
375
375
|
await view(p, 'track');
|
|
@@ -166,7 +166,7 @@ const ITEMS = [
|
|
|
166
166
|
lane: [...document.querySelectorAll('#ibBody .tg-lane.closed')].map((x) => ({ span: x.querySelectorAll('.tg-p .tg-span').length, end: (x.querySelector('.tg-p .tg-endl') || {}).textContent || '', dots: x.querySelectorAll('.tg-offd .cz-dot').length })),
|
|
167
167
|
}));
|
|
168
168
|
ok('G folded, a program shows the roll-up line, its ◇s and its end date', g.prog.length > 0 && g.prog.every((x) => x.span > 0 && x.dm > 0 && /’\d\d$/.test(x.end)), JSON.stringify(g));
|
|
169
|
-
ok('G folded, a product shows the roll-up, its end date and one dot per initiative off track', g.lane.length > 0 && g.lane.some((x) => x.span > 0 && x.end && x.dots ===
|
|
169
|
+
ok('G folded, a product shows the roll-up, its end date and one dot per initiative off track (its undated one counts: it is a row since batch 26 item 8)', g.lane.length > 0 && g.lane.some((x) => x.span > 0 && x.end && x.dots === 4), JSON.stringify(g));
|
|
170
170
|
await c2.close();
|
|
171
171
|
}
|
|
172
172
|
|
|
@@ -179,7 +179,7 @@ const ITEMS = [
|
|
|
179
179
|
dots: [...document.querySelectorAll('#ibBody .tgq-r[data-row] .tgq-n')].map((b) => (b.querySelector('.cz-dot') || {}).className || ''),
|
|
180
180
|
scroll: document.documentElement.scrollWidth <= innerWidth + 1,
|
|
181
181
|
}));
|
|
182
|
-
ok('M the phone keeps the end labels, a dot only off track, and no sideways page scroll', m.labels >= 5 && m.dots.filter(Boolean).length ===
|
|
182
|
+
ok('M the phone keeps the end labels, a dot only off track (the undated row too, batch 26 item 8), and no sideways page scroll', m.labels >= 5 && m.dots.filter(Boolean).length === 4 && m.scroll, JSON.stringify(m));
|
|
183
183
|
ok('M no page error on the phone', errors.length === 0, errors.join(' | '));
|
|
184
184
|
await ctx.close();
|
|
185
185
|
}
|
package/workspace/changelog.json
CHANGED
|
@@ -1,4 +1,29 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"v": "0.519.0",
|
|
4
|
+
"at": "2026-11-04T01:26:27.710Z",
|
|
5
|
+
"f": 4,
|
|
6
|
+
"prs": [
|
|
7
|
+
{
|
|
8
|
+
"n": 1213,
|
|
9
|
+
"t": "Every undated initiative is a Timeline row with No dates",
|
|
10
|
+
"s": "Every undated initiative is a Timeline row with No dates",
|
|
11
|
+
"k": "fix"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"n": 1214,
|
|
15
|
+
"t": "Chat prompt suggestions run on claude -p by default, and never fail silently",
|
|
16
|
+
"s": "Chat prompt suggestions run on claude -p by default, and never fail silently",
|
|
17
|
+
"k": "fix"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"n": 1215,
|
|
21
|
+
"t": "Background runs read long connector results; guide fills the wide Viewer",
|
|
22
|
+
"s": "Background runs read long connector results; guide fills the wide Viewer",
|
|
23
|
+
"k": "fix"
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
},
|
|
2
27
|
{
|
|
3
28
|
"v": "0.518.0",
|
|
4
29
|
"at": "2026-11-04T00:33:13.219Z",
|
package/workspace/guide/index.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Guide: start here"
|
|
4
|
-
timestamp: 2026-11-
|
|
4
|
+
timestamp: 2026-11-04
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Guide: start here
|
|
8
8
|
|
|
9
9
|
This guide describes the PMOS workspace as it works today. It ships with the app. When a change alters how the workspace looks or behaves, the same change updates this guide.
|
|
10
10
|
|
|
11
|
-
To open the guide from any page, press the round menu button at the bottom of the page, then the **?** icon (Guide). The guide opens wide in the Viewer. Every page is in the list on the left, by group, with **Find a page** on top; a group folds and stays folded. Above each page, one chip for each of its sections takes you there. A link to another guide page opens that page in the same place. Press ← to go back. On a phone, or when the Viewer is narrow, the list icon beside the title shows the list.
|
|
11
|
+
To open the guide from any page, press the round menu button at the bottom of the page, then the **?** icon (Guide). The guide opens wide in the Viewer and uses its full width. Every page is in the list on the left, by group, with **Find a page** on top; a group folds and stays folded. Above each page, one chip for each of its sections takes you there. A link to another guide page opens that page in the same place. Press ← to go back. On a phone, or when the Viewer is narrow, the list icon beside the title shows the list.
|
|
12
12
|
|
|
13
13
|
## The screen
|
|
14
14
|
|
|
@@ -30,7 +30,7 @@ Each card names only the initiatives, tasks and PRs that the reply or your messa
|
|
|
30
30
|
|
|
31
31
|
## While you type
|
|
32
32
|
|
|
33
|
-
When you stop typing for about 1.5 seconds on a rough prompt of three or more words, a small spinner card shows while Claude sharpens it. Then one lit **Sharper** card shows. Tap it to put the sharper text in the box. ↶ in the box puts back what you wrote. When the sharper version says the same as yours, no card shows.
|
|
33
|
+
When you stop typing for about 1.5 seconds on a rough prompt of three or more words, a small spinner card shows while Claude sharpens it. This takes a few seconds. Then one lit **Sharper** card shows. Tap it to put the sharper text in the box. ↶ in the box puts back what you wrote. When the sharper version says the same as yours, no card shows.
|
|
34
34
|
|
|
35
35
|
The sliders icon on the Sharper card sets how Claude sharpens:
|
|
36
36
|
|
|
@@ -45,7 +45,7 @@ The same menu has the **Sharpen as I type** switch. Off means no spinner and no
|
|
|
45
45
|
| Row | What it does |
|
|
46
46
|
|---|---|
|
|
47
47
|
| Suggest the next prompt | On by default. Off: no cards after a reply. |
|
|
48
|
-
| Sharpen as I type | On
|
|
48
|
+
| Sharpen as I type | On by default. Off: no spinner and no Sharper card. |
|
|
49
49
|
| Sharpen as | Polish, Prompt engineer or Your own. |
|
|
50
50
|
| Your own rules | The rules for Your own, up to 1000 characters. |
|
|
51
51
|
|
|
@@ -57,6 +57,8 @@ The suggestions and the sharper prompt go through the same guardrail as every AI
|
|
|
57
57
|
- **Proven only.** Each suggested move points to the line of PMOS code that makes it work, and PMOS checks that line before the card shows. An answer to the reply points to the line of the reply it answers. A card that PMOS cannot check is not shown.
|
|
58
58
|
- **Replay-tested.** A new version shows nothing until its recorded cases pass, and those include cases that must be dropped.
|
|
59
59
|
- **Audited.** Each run writes a line to the AI audit, with no text of your conversation in it.
|
|
60
|
+
- **Your own Claude session.** Both run as a short background `claude -p` run on your own Claude login, which can only read files. Your API key is not used unless you set `PMOS_NEXT_PROMPT_API=on`. Then, when a call on the key fails, the same request goes to the background run instead.
|
|
61
|
+
- **No silent failure.** When a run gives no card, the audit says why. `pmos ai log next-prompt` and `pmos ai log sharpen-prompt` show the last lines.
|
|
60
62
|
- **Off switch.** `pmos ai off next-prompt` stops the suggestions after a reply. `pmos ai off sharpen-prompt` stops the Sharper card. `pmos ai off` stops every AI job.
|
|
61
63
|
|
|
62
64
|
The reply and the words a card refers to are kept in your PMOS state folder (`next-prompt/`, the newest 30 files), outside every repository. Your choices for these rows are kept there too, in `next-prompt/prefs.json`.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Programs on the Board: All products, groupings and the program panel"
|
|
4
4
|
description: "A program groups initiatives across products. How it shows on the Board (stage track, Kanban, timeline and List), the All products mode, the By product grouping, the program panel and the side panel width."
|
|
5
|
-
timestamp: 2026-11-
|
|
5
|
+
timestamp: 2026-11-04
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Programs on the Board
|
|
@@ -22,6 +22,7 @@ A cancelled or archived initiative leaves its program's views: the band, the pro
|
|
|
22
22
|
- To group by product only, open **Filter** and choose **By product**. A program member then shows a small layers icon and stays under its product in every view, the timeline included. A chip shows while By product is on.
|
|
23
23
|
- **Kanban** shows programs as swimlanes, then products. Press a column header to fold the column to a strip with its count. An empty column starts folded. The Board remembers each fold.
|
|
24
24
|
- The **timeline** draws a program row that brackets its initiatives' bars, on every span. Program milestones are diamonds. Milestones that are close together merge into one diamond with a count.
|
|
25
|
+
- On the timeline, **every initiative has a row**, in a program or not, in every grouping. An initiative with no dates says **No dates** beside today; in each group it comes after the dated ones. Under the chart, a line gives how many have no dates, with **Ask to propose dates**.
|
|
25
26
|
- On the timeline, a **product's head row** also shows milestones. Nothing is added by hand: the row shows every milestone of the product's initiatives, which are each milestone task and each initiative's end diamond (its Deliver or Evaluate date). The diamonds are the same as on the initiative rows: open, filled when done, orange when late. Diamonds that are close together merge into one diamond with a count and the date range under it. Press it to see a list with the milestone, its initiative and the date. Press a line to open that initiative. Hold the pointer on a single diamond to see its milestone and initiative. A thin line goes from the first start to the last end of the product's initiatives. When the product is folded, the row stays the same, so it is a summary of the product. On a phone the diamonds have no dates; tap the row to see all the milestones in date order. A product inside a program has no such row, because the program row shows its milestones.
|
|
26
27
|
- On the timeline, an initiative row shows only its **pinned** milestones. By default that is its last milestone. Press a diamond to see every milestone of the initiative, with how many are on the timeline. Press the pin beside a milestone to show it or to hide it. To see every milestone or none, open **Filter** and pick **Milestones on the timeline: Pinned**, **All** or **None**. The Board keeps your pick. From a terminal: `pmos task set <task> --pin`, `--no-pin`, or `--pin-default` to go back to the default.
|
|
27
28
|
- The label at the end of an initiative row says what lands and when. When a milestone task is in the last milestone stage, or is on the end date, or is in the Deliver stage, or is so close to the end date that its diamond would sit on the end diamond, the label shows its name and its own date, and the end date of the initiative shows when you hold the pointer on the label. Otherwise the latest milestone task of the initiative names the end in the same way, even when it is months before the end date. Only an initiative with no dated milestone task shows **Deliver** (or **Evaluate**) and the end date. Press that end diamond: the card has the same words as the label, then the milestones of the initiative.
|
|
@@ -49,7 +49,7 @@ Under the update card, **Updates** shows what changed in the published kits. **T
|
|
|
49
49
|
|
|
50
50
|
**API key.** Some runs of Claude, for example Grill, can use the Claude API and not a Claude session. Paste an Anthropic API key and press **Save** to turn this on. The key stays on this computer, in a file only you can read, outside every repository. PMOS never shows the key again: the row only says that a key is set. Press **Remove** to delete it. A key saved here is used before `ANTHROPIC_API_KEY`. A command that a run starts does not get the key. Only the computer that runs the workspace can change it. Set `PMOS_GRILL_API=off` to stop runs on the API.
|
|
51
51
|
|
|
52
|
-
**Sharpen prompts.** Suggest the next prompt (on by default) shows up to two prompts above the chat box after a reply. Sharpen as I type offers a sharper version of a rough prompt. It is on
|
|
52
|
+
**Sharpen prompts.** Suggest the next prompt (on by default) shows up to two prompts above the chat box after a reply. Sharpen as I type offers a sharper version of a rough prompt. It is on by default and runs in your own Claude session. Sharpen as picks Polish, Prompt engineer (the default) or Your own, and Your own rules holds the rules for Your own. The same style and switch are in the menu on the chat's Sharper card. See [The next prompt, suggested and sharpened](next-prompt.md).
|
|
53
53
|
|
|
54
54
|
## Claude's checks: when Claude grills, and what it may spend
|
|
55
55
|
|
|
@@ -250,7 +250,17 @@ function initTools(line) {
|
|
|
250
250
|
return o.tools.filter((t) => typeof t === 'string' && t.startsWith('mcp__'));
|
|
251
251
|
}
|
|
252
252
|
// The one line every run gets about a refused tool (fix 2026-11-02: a Jira refresh called the same denied tool again and again).
|
|
253
|
-
const DENIED_RULE = 'A tool call that is refused in this run stays refused: never call that tool again. Carry on with the tools you have, and say in your answer what you could not read.';
|
|
253
|
+
const DENIED_RULE = 'A tool call that is refused in this run stays refused: never call that tool again. Carry on with the tools you have, and say in your answer what you could not read. When a tool result is too long and is saved to a file, open that file with Read, and use offset and limit for a long file. Never read it with a shell command: that is refused.';
|
|
254
|
+
// Hotfix 2026-11-04: a Jira search on the Mac returned more than Claude Code keeps inline, so its result went to a tool-results file; the run then
|
|
255
|
+
// tried `Bash F=<file> …` to parse it, Bash was refused, and the run failed. A background run keeps connector results inline up to this limit,
|
|
256
|
+
// and the rule above sends a saved result to Read. The PM's own MAX_MCP_OUTPUT_TOKENS, when set, wins.
|
|
257
|
+
const HEADLESS_ENV = Object.freeze({ MAX_MCP_OUTPUT_TOKENS: '100000' });
|
|
258
|
+
/** The run's environment with HEADLESS_ENV added where the PM has not set the variable. */
|
|
259
|
+
function headlessEnv(env) {
|
|
260
|
+
const out = { ...(env || {}) };
|
|
261
|
+
for (const [k, v] of Object.entries(HEADLESS_ENV)) if (!out[k]) out[k] = v;
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
254
264
|
const DENIED_RE = /Permission to use ([\w.-]+) has been denied|requested permissions to use ([\w.-]+)/;
|
|
255
265
|
/** Follows a run's stream and says when it calls a refused tool a second time: → { see(line) → tool name | null, denied: Map }. */
|
|
256
266
|
function denialGuard(limit = 2) {
|
|
@@ -488,4 +498,4 @@ function runEnvOf(extra) {
|
|
|
488
498
|
return out;
|
|
489
499
|
}
|
|
490
500
|
|
|
491
|
-
module.exports = { CONNECTOR_READ_KINDS, CONNECTOR_READ_SEED, DENIED_RULE, connectorReads, initTools, denialGuard, 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, METRIC_DRAFT_ORIGINS, IMPACT_ORIGINS, TOOLS, SKILL_WRITE_DIRS, DEFAULT_TIMEOUT_MS, kindFor, argsFor, skillWriteDirs, timeoutMs, parseResult, streamLines, outcomeOf, signedOut, SIGNED_OUT_WHY, logLines, switchedOff, RUN_ENV, runEnvOf };
|
|
501
|
+
module.exports = { CONNECTOR_READ_KINDS, CONNECTOR_READ_SEED, DENIED_RULE, HEADLESS_ENV, headlessEnv, connectorReads, initTools, denialGuard, 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, METRIC_DRAFT_ORIGINS, IMPACT_ORIGINS, TOOLS, SKILL_WRITE_DIRS, DEFAULT_TIMEOUT_MS, kindFor, argsFor, skillWriteDirs, timeoutMs, parseResult, streamLines, outcomeOf, signedOut, SIGNED_OUT_WHY, logLines, switchedOff, RUN_ENV, runEnvOf };
|
|
@@ -2,16 +2,17 @@
|
|
|
2
2
|
/* routes-next-prompt.js — the chat's suggested next prompt and its Sharper card over HTTP (batch 26 item 2, design B; lib/next-prompt.js).
|
|
3
3
|
*
|
|
4
4
|
* GET /api/next-prompt/prefs → { ok, next, sharpen, sharpenSet, style, rules, api }
|
|
5
|
-
* next: suggestions after a reply are on. sharpen: Sharpen as I type is on now (
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* next: suggestions after a reply are on. sharpen: Sharpen as I type is on now (on unless the PM turned it off; 'auto' is off only
|
|
6
|
+
* while an opted-in API path is failing). sharpenSet: 'auto'|'on'|'off'. style: 'polish'|'engineer'|'own'. rules: whether Your own
|
|
7
|
+
* rules are written. api: the opted-in API path is in use (PMOS_NEXT_PROMPT_API=on and a key). apiDown: { why, auth } while it fails.
|
|
8
8
|
* POST /api/next-prompt/prefs { key: 'next'|'sharpen'|'style'|'rules', value } → { ok } (saved in <runtime>/next-prompt/prefs.json)
|
|
9
9
|
* POST /api/next-prompt/next { key, asked, reply, initiative?, stage?, product?, title? } → { ok, on, cards: [{ text, move, rec }] }
|
|
10
10
|
* POST /api/next-prompt/sharpen { key, text, reply?, initiative?, title? } → { ok, on, text | null }
|
|
11
11
|
*
|
|
12
12
|
* Both POSTs are page presses (lib/pm-guard.js requirePagePress; an agent session header is refused) and run ONE short Claude call:
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* a background read-only `claude -p` run on the PM's own Claude login (lib/headless.js kind 'ask', origin briefing-ask:next-prompt:…), the
|
|
14
|
+
* default (Wawan 2026-11-04). Only with PMOS_NEXT_PROMPT_API=on does it go to the API with the PM's key (lib/anthropic-key.js, model
|
|
15
|
+
* PMOS_NEXT_PROMPT_MODEL or claude-sonnet-5-5), and a failed API call then falls back to the background run. The answer goes through the shared door, lib/ai-guard.js file():
|
|
15
16
|
* the kill switch (`pmos ai off next-prompt`, `pmos ai off sharpen-prompt`, PMOS_AI=off), the replay gate (fixtures/next-prompt-replay.json,
|
|
16
17
|
* recorded answers, never a model), the citation check and the audit (<runtime>/ai-audit.jsonl). A card with no verified source is never
|
|
17
18
|
* shown. The reply and the typed text a card cites are kept in <runtime>/next-prompt/ (the newest 30 files), outside every repository.
|
|
@@ -61,14 +62,29 @@ function setPref(runtimeDir, key, value) {
|
|
|
61
62
|
const get = (ctx, k) => { const v = readPrefs(ctx.runtimeDir)[k]; return PREFS[k](v) ? v : undefined; };
|
|
62
63
|
const envOf = (ctx) => ctx.env || process.env;
|
|
63
64
|
|
|
65
|
+
/* Wawan 2026-11-04: "just default to normal claude session with -p". Both calls run as a background `claude -p` on the PM's own Claude login.
|
|
66
|
+
* The API key is used only when PMOS_NEXT_PROMPT_API=on opts in. */
|
|
64
67
|
function apiKey(ctx) {
|
|
65
68
|
const env = envOf(ctx);
|
|
66
|
-
if (
|
|
69
|
+
if (!/^(on|1|true|yes)$/i.test(String(env.PMOS_NEXT_PROMPT_API || ''))) return null;
|
|
67
70
|
try { return require('./anthropic-key.js').getKey(ctx.runtimeDir, env); } catch { return null; }
|
|
68
71
|
}
|
|
72
|
+
/* The API's health, per state folder, in memory: after a failed call PMOS stops calling the API for DOWN_MS and uses the background run,
|
|
73
|
+
* so a spent balance or a refused key does not fail every reply again. A server restart, or a new key saved, tries the API again. */
|
|
74
|
+
const DOWN_MS = 15 * 60 * 1000;
|
|
75
|
+
const apiDown = new Map(); // runtimeDir → { until, why, auth, key }
|
|
76
|
+
function downOf(ctx, key) {
|
|
77
|
+
const d = apiDown.get(ctx.runtimeDir);
|
|
78
|
+
if (!d) return null;
|
|
79
|
+
if (d.until <= Date.now() || d.key !== keyId(key)) { apiDown.delete(ctx.runtimeDir); return null; }
|
|
80
|
+
return d;
|
|
81
|
+
}
|
|
82
|
+
const keyId = (k) => (k ? crypto.createHash('sha1').update(String(k)).digest('hex').slice(0, 12) : '');
|
|
69
83
|
function prefs(ctx) {
|
|
70
|
-
const set = get(ctx, 'sharpen') || 'auto',
|
|
71
|
-
|
|
84
|
+
const set = get(ctx, 'sharpen') || 'auto', k = apiKey(ctx), down = k ? downOf(ctx, k) : null, api = !!k && !down;
|
|
85
|
+
const style = get(ctx, 'style') || 'engineer', rules = String(get(ctx, 'rules') || '');
|
|
86
|
+
return { next: get(ctx, 'next') !== false, sharpen: set !== 'off' && !(set === 'auto' && k && down), sharpenSet: set, style, rules: !!rules.trim(), rulesText: rules, api,
|
|
87
|
+
...(down ? { apiDown: { why: down.why, auth: down.auth } } : {}) };
|
|
72
88
|
}
|
|
73
89
|
function offWhy(ctx, job) {
|
|
74
90
|
const why = AG.off(job, { env: envOf(ctx), settingsDir: ctx.runtimeDir });
|
|
@@ -81,31 +97,55 @@ function offWhy(ctx, job) {
|
|
|
81
97
|
|
|
82
98
|
/* ── the call ────────────────────────────────────────────────────────────────────────────── */
|
|
83
99
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
100
|
+
// A model the account cannot use (404) tries the next one before the call counts as failed.
|
|
101
|
+
const MODELS = ['claude-sonnet-5-5', 'claude-sonnet-5', 'claude-haiku-5-5'];
|
|
102
|
+
async function viaApi(key, prompt, env, create = null) {
|
|
103
|
+
const make = create || ((o) => { const Anthropic = require('@anthropic-ai/sdk'); return new Anthropic({ apiKey: key }).messages.create(o); });
|
|
104
|
+
const want = String(env.PMOS_NEXT_PROMPT_MODEL || '').trim();
|
|
105
|
+
const models = want ? [want] : MODELS;
|
|
106
|
+
for (let i = 0; i < models.length; i++) {
|
|
107
|
+
try {
|
|
108
|
+
const res = await make({ model: models[i], max_tokens: 800, messages: [{ role: 'user', content: prompt }] });
|
|
109
|
+
return (res.content || []).filter((b) => b.type === 'text').map((b) => b.text).join('\n');
|
|
110
|
+
} catch (e) { if (!(e && e.status === 404) || i === models.length - 1) throw e; }
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
89
113
|
}
|
|
90
|
-
|
|
114
|
+
/** A background read-only Claude run → its answer text, or null after `fail(why)` says why it gave none. */
|
|
115
|
+
function viaHeadless(ctx, prompt, origin, fail = () => {}) {
|
|
91
116
|
const headless = require('./headless.js');
|
|
92
|
-
if (typeof ctx.spawnSession !== 'function'
|
|
117
|
+
if (typeof ctx.spawnSession !== 'function') { fail('no background run here'); return Promise.resolve(null); }
|
|
118
|
+
if (headless.kindFor({ agent: 'claude', origin }) !== 'ask') { fail('the run would not be read-only'); return Promise.resolve(null); }
|
|
93
119
|
return new Promise((resolve) => {
|
|
94
|
-
let settled = false; const done = (v) => { if (
|
|
95
|
-
const t = setTimeout(() => done(null), TIMEOUT_MS);
|
|
120
|
+
let settled = false; const done = (v, why) => { if (settled) return; settled = true; clearTimeout(t); if (v == null) fail(why); resolve(v); };
|
|
121
|
+
const t = setTimeout(() => done(null, 'the run took longer than ' + Math.round(TIMEOUT_MS / 1000) + ' s'), TIMEOUT_MS);
|
|
96
122
|
let s = null;
|
|
97
123
|
try {
|
|
98
124
|
s = ctx.spawnSession({ name: 'chat: next prompt', agent: 'claude', origin, initialInput: prompt,
|
|
99
|
-
onDone: (d) => { const r = d && d.result; done(r && r.ok && !r.isError ? String(r.text || '') : null); } });
|
|
100
|
-
} catch { s = null; }
|
|
101
|
-
if (!s || s.state === 'exited') done(null);
|
|
125
|
+
onDone: (d) => { const r = d && d.result; done(r && r.ok && !r.isError ? String(r.text || '') : null, 'the run ended without an answer' + (d && d.note ? ': ' + String(d.note).slice(0, 80) : '')); } });
|
|
126
|
+
} catch (e) { s = null; }
|
|
127
|
+
if (!s || s.state === 'exited') done(null, 'the run did not start' + (s && s.note ? ': ' + String(s.note).slice(0, 80) : ''));
|
|
102
128
|
});
|
|
103
129
|
}
|
|
104
|
-
|
|
130
|
+
/** Why an API call failed, in words fit for the audit: the HTTP status and the error's type, never the prompt or the answer. */
|
|
131
|
+
function apiWhy(e) {
|
|
132
|
+
const st = e && (e.status || (e.response && e.response.status));
|
|
133
|
+
const kind = e && ((e.error && e.error.error && e.error.error.type) || e.name || e.code);
|
|
134
|
+
return ['api', st ? String(st) : null, kind ? String(kind).slice(0, 60) : null].filter(Boolean).join(' ');
|
|
135
|
+
}
|
|
136
|
+
/* One short Claude call. With a key it goes to the API; when that call fails (a spent balance, a model the account cannot use, no network),
|
|
137
|
+
* it does not end in silence: the same prompt goes to a background read-only Claude run, and the audit records why the API failed
|
|
138
|
+
* (`pmos ai log next-prompt`); for 15 minutes after, PMOS goes straight to the background run. `ctx.createMessage` replaces the SDK in tests. */
|
|
139
|
+
async function ask(ctx, prompt, { job, key, run }) {
|
|
105
140
|
if (typeof ctx.askClaude === 'function') return ctx.askClaude(prompt, { job, key });
|
|
106
141
|
const k = apiKey(ctx);
|
|
107
|
-
if (k
|
|
108
|
-
|
|
142
|
+
if (k && !downOf(ctx, k)) {
|
|
143
|
+
const failed = (why, auth) => { apiDown.set(ctx.runtimeDir, { until: Date.now() + DOWN_MS, why, auth: !!auth, key: keyId(k) }); AG.audit(ctx.runtimeDir, { job, version: VERSION, run, event: 'api-error', error: why }); };
|
|
144
|
+
try { const t = await viaApi(k, prompt, envOf(ctx), ctx.createMessage || null); if (t && String(t).trim()) return t; failed('api empty answer', false); }
|
|
145
|
+
catch (e) { failed(apiWhy(e), e && (e.status === 401 || e.status === 403)); }
|
|
146
|
+
}
|
|
147
|
+
return viaHeadless(ctx, prompt, `briefing-ask:next-prompt:${job}:${crypto.createHash('sha1').update(String(key)).digest('hex').slice(0, 10)}`,
|
|
148
|
+
(why) => AG.audit(ctx.runtimeDir, { job, version: VERSION, run, event: 'run-error', error: String(why).slice(0, 160) }));
|
|
109
149
|
}
|
|
110
150
|
|
|
111
151
|
/* ── what a card cites ───────────────────────────────────────────────────────────────────── */
|
|
@@ -182,7 +222,7 @@ async function next(ctx, b) {
|
|
|
182
222
|
const k = 'n:' + key + ':' + crypto.createHash('sha1').update(reply + '\n' + P.style + '\n' + P.rulesText).digest('hex');
|
|
183
223
|
return once(k, async () => {
|
|
184
224
|
AG.audit(ctx.runtimeDir, { job, version: VERSION, run, event: 'start' });
|
|
185
|
-
const answer = await ask(ctx, NP.nextPrompt({ pm: text(b.asked, 2000), reply, place, style: P.style, rules: P.rulesText, moves }), { job, key });
|
|
225
|
+
const answer = await ask(ctx, NP.nextPrompt({ pm: text(b.asked, 2000), reply, place, style: P.style, rules: P.rulesText, moves }), { job, key, run });
|
|
186
226
|
const cards = NP.parseNext(answer, { moves, place });
|
|
187
227
|
if (!cards.length) { AG.audit(ctx.runtimeDir, { job, version: VERSION, run, event: 'end', filed: 0, dropped: 0, ...(answer == null ? { error: 'no answer' } : {}) }); return { ok: true, on: true, cards: [] }; }
|
|
188
228
|
const rel = keep(ctx.runtimeDir, 'reply', reply);
|
|
@@ -200,7 +240,7 @@ async function sharpen(ctx, b) {
|
|
|
200
240
|
const k = 's:' + key + ':' + crypto.createHash('sha1').update(typed + '\n' + P.style + '\n' + P.rulesText).digest('hex');
|
|
201
241
|
return once(k, async () => {
|
|
202
242
|
AG.audit(ctx.runtimeDir, { job, version: VERSION, run, event: 'start' });
|
|
203
|
-
const answer = await ask(ctx, NP.sharpenPrompt({ text: typed, reply: text(b.reply, 3000), place: placeOf(b), style: P.style, rules: P.rulesText }), { job, key });
|
|
243
|
+
const answer = await ask(ctx, NP.sharpenPrompt({ text: typed, reply: text(b.reply, 3000), place: placeOf(b), style: P.style, rules: P.rulesText }), { job, key, run });
|
|
204
244
|
const out = NP.parseSharpen(answer, typed);
|
|
205
245
|
if (!out) { AG.audit(ctx.runtimeDir, { job, version: VERSION, run, event: 'end', filed: 0, dropped: 0, ...(answer == null ? { error: 'no answer' } : { same: true }) }); return { ok: true, on: true, text: null }; }
|
|
206
246
|
const first = typed.split('\n')[0], rel = keep(ctx.runtimeDir, 'typed', typed);
|
|
@@ -227,7 +267,7 @@ async function handle(req, res, url, ctx) {
|
|
|
227
267
|
if (req.method !== 'GET') return send(bad(405, 'GET or POST only'));
|
|
228
268
|
const P = prefs(ctx), env = envOf(ctx);
|
|
229
269
|
const next = P.next && !AG.off(NP.JOB_NEXT, { env, settingsDir: ctx.runtimeDir }), sh = P.sharpen && !AG.off(NP.JOB_SHARPEN, { env, settingsDir: ctx.runtimeDir });
|
|
230
|
-
return send({ status: 200, body: { ok: true, next, nextSet: P.next, sharpen: sh, sharpenSet: P.sharpenSet, style: P.style, rules: P.rules, rulesText: P.rulesText, api: P.api } });
|
|
270
|
+
return send({ status: 200, body: { ok: true, next, nextSet: P.next, sharpen: sh, sharpenSet: P.sharpenSet, style: P.style, rules: P.rules, rulesText: P.rulesText, api: P.api, ...(P.apiDown ? { apiDown: P.apiDown } : {}) } });
|
|
231
271
|
}
|
|
232
272
|
if (what !== 'next' && what !== 'sharpen') return send(bad(404, 'no such next-prompt route'));
|
|
233
273
|
if (req.method !== 'POST') return send(bad(405, 'POST only'));
|
|
@@ -245,4 +285,4 @@ async function handle(req, res, url, ctx) {
|
|
|
245
285
|
catch (e) { return send(bad(500, 'The suggestion failed: ' + String((e && e.message) || e).slice(0, 200))); }
|
|
246
286
|
}
|
|
247
287
|
|
|
248
|
-
module.exports = { prefixes: ['/api/next-prompt'], handle, VERSION, FIXTURE, prefs, setPref, lineRef, replayCases, next, sharpen };
|
|
288
|
+
module.exports = { prefixes: ['/api/next-prompt'], handle, VERSION, FIXTURE, MODELS, prefs, setPref, lineRef, replayCases, next, sharpen };
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.519.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.519.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@anthropic-ai/sdk": "^1.3.0",
|
package/workspace/package.json
CHANGED
|
@@ -524,7 +524,7 @@ function scaled(rows, M, o) {
|
|
|
524
524
|
const msO = { sp, reach: Math.max(1, Math.round(40 / pxDay)), mode: o.msMode || 'pinned', noLabel: !!(C && C.msOn) }; // a row's milestone ◇s closer than ~40 px merge (batch 11 item 3); the Filter's milestones (batch 13 item 4)
|
|
525
525
|
const ov = overlay(W, X, now, sprint, rel), pxPct = (tpx * (W.fixed ? 1 : ratio)) / 100;
|
|
526
526
|
const ra = rel && day(rel.iso), sa = sprint && day(sprint.start), sb = sprint && day(sprint.end);
|
|
527
|
-
// an undated
|
|
527
|
+
// an undated initiative's track says so beside today, where the view opens
|
|
528
528
|
const ND = `<span class="tg-nd" style="left:${P(Math.min(90, Math.max(0, X(now))))}">No dates</span>`;
|
|
529
529
|
const askBtn = none.length ? `<a class="bd-chip tg-ask" id="askDates" href="${esc(o.askHref(none))}">${SV('M4 5h16v11h-9l-5 4v-4H4z')}Ask to propose dates</a>` : '';
|
|
530
530
|
const ticks = W.ticks.map((k) => `<span style="left:${P(X(k.at))}" data-y="${k.y}" data-base="${esc(k.base)}"${k.plain ? ' data-plain' : ''}><b>${esc(k.label)}</b><small>${esc(k.sub)}</small></span>`).join('');
|
|
@@ -533,23 +533,19 @@ function scaled(rows, M, o) {
|
|
|
533
533
|
const back = `<button type="button" class="tg-tback" data-today hidden aria-label="Back to today">${SV(CHL)}<span>Today</span>${SV(CHR)}</button>`;
|
|
534
534
|
const attrs = `data-span="${sp}" data-from="${+W.from}" data-to="${+W.to}" data-ratio="${W.fixed ? 1 : ratio.toFixed(5)}" data-now="${+now}"${W.fixed ? ' data-fixed' : ''}`;
|
|
535
535
|
const style = `--tw:${Math.round(tpx * (W.fixed ? 1 : ratio))}px`;
|
|
536
|
-
//
|
|
537
|
-
//
|
|
538
|
-
//
|
|
539
|
-
|
|
540
|
-
//
|
|
541
|
-
const
|
|
542
|
-
// a product with no dated initiative keeps its head and its initiatives as "No dates" rows, so the product is not missing from the Board
|
|
543
|
-
// (batch 14 item 2: pmos-microsites, all undated outside its program, had no group at all); those rows leave the foot's names
|
|
544
|
-
const keepOf = (R) => { const k = R.filter((r) => onList.has(r.key) || r.inProgram); return k.length ? k : R.slice(); };
|
|
545
|
-
const shownNd = new Set(o.groups ? o.groups.filter((g) => g.kind !== 'program' && !g.rows.some((r) => onList.has(r.key) || r.inProgram)).flatMap((g) => g.rows.map((r) => r.key)) : []);
|
|
546
|
-
const footNone = none.filter((r) => !shownNd.has(r.key));
|
|
536
|
+
// every undated initiative is a row that says "No dates" beside today, in every grouping and in no program too (batch 26 item 8, Wawan:
|
|
537
|
+
// the foot alone hid them; this replaces batch 8 item 4's program-only rule and design B6). By product copies a program's row
|
|
538
|
+
// ({ ...r, inProgram }), so a row is matched by key, not by identity (batch 11 item 12)
|
|
539
|
+
const shownKeys = new Set(o.groups ? o.groups.flatMap((g) => (g.kind === 'program' ? g.products.flatMap((x) => x.rows) : g.rows).map((r) => r.key)) : rows.map((r) => r.key));
|
|
540
|
+
// the foot keeps the count and "Ask to propose dates"; it names only an undated initiative no group draws (none, as the Board groups today)
|
|
541
|
+
const footNone = none.filter((r) => !shownKeys.has(r.key));
|
|
547
542
|
// a timeline reads in time (batch 11 item 22): each group's rows by start, then end; undated ones last. The health dot still says late or at risk
|
|
548
543
|
const at = (r) => { const x = extentOf(r, now); return x ? [+x.a, +x.b] : [Infinity, Infinity]; };
|
|
549
544
|
// a pressed column title sorts each group by that column instead (StsA8Board, as in the List)
|
|
550
545
|
const byTime = (L) => (C && C.order ? L.slice().sort(C.order) : L.map((r) => [at(r), r]).sort((p, q) => p[0][0] - q[0][0] || p[0][1] - q[0][1]).map((x) => x[1]));
|
|
551
546
|
list.splice(0, list.length, ...byTime(list));
|
|
552
|
-
const
|
|
547
|
+
const flat = list.concat(none);
|
|
548
|
+
const G = o.groups ? o.groups.map((g) => (g.kind === 'program' ? { ...g, products: g.products.filter((x) => x.rows.length).map((x) => ({ ...x, rows: byTime(x.rows) })) } : { ...g, rows: byTime(g.rows) })).filter((g) => g.rows.length || g.kind === 'program') : null;
|
|
553
549
|
const empty = '<p class="tg-empty">No initiative on screen has a date on record.</p>';
|
|
554
550
|
const ext = (r) => { const x = extentOf(r, now); return x ? ` data-a="${X(x.a).toFixed(3)}" data-b="${X(x.b).toFixed(3)}"` : ''; };
|
|
555
551
|
// a product's head carries its lane from Quarter on (batch 11 item 17); Sprint on a phone, and a page that gives no head cell, keep the plain head
|
|
@@ -561,9 +557,9 @@ function scaled(rows, M, o) {
|
|
|
561
557
|
if (g.kind !== 'program') return lane(g) + (o.folded(g.key) ? '' : g.rows.map(one).join(''));
|
|
562
558
|
const st = o.progState(g.program), shut = o.folded(g.key), tr = shut ? rollUp(g.rows, progSpan(g), X, W, o, msO, pxPct, now) : '';
|
|
563
559
|
return `<div class="tgq-r tg-pg${shut ? ' closed' : ''}" data-prow="${esc(g.program.id)}"><div class="tgq-n tg-pgn">${progName(g, st, o, shut ? offDots(g.rows, now) : '')}</div><div class="tg-p">${ov}${tr}</div></div>` + (shut ? '' : g.products.map((x) => x.rows.map(one).join('')).join(''));
|
|
564
|
-
}).join('') :
|
|
560
|
+
}).join('') : flat.map(one).join('');
|
|
565
561
|
const top = `<div class="tgq-h"><div class="tg-p">${ticks}${pill}</div></div>`;
|
|
566
|
-
const below = none.length ? `<div class="tg-none"
|
|
562
|
+
const below = none.length ? `<div class="tg-none"><span>${none.length} ${none.length === 1 ? 'has' : 'have'} no dates</span>${askBtn}</div>` : '';
|
|
567
563
|
// the Deliveries lane (batch 13 item 4, XcalmA6Phone): a phone shows the next deliveries above the rows (o.lane draws it; none: nothing)
|
|
568
564
|
const lanePh = o.lane ? o.lane({ W, X, rows: list, groups: G, now, phone: true, pxPct }) || '' : '';
|
|
569
565
|
return { html: lanePh + `<div class="tg tg-s tgq" ${attrs}><div class="tg-scw"><div class="tg-sc" tabindex="0" role="group" aria-label="Timeline" style="${style}"><div class="tg-in">${top}<div class="tg-body">${body || empty}</div></div></div>${back}<i class="tg-fade l" aria-hidden="true"></i><i class="tg-fade r" aria-hidden="true"></i></div></div>${below}`, none };
|
|
@@ -581,11 +577,11 @@ function scaled(rows, M, o) {
|
|
|
581
577
|
if (g.kind !== 'program') return lane(g) + (o.folded(g.key) ? '' : g.rows.map((r) => one(r, ' tg-gr')).join(''));
|
|
582
578
|
const lp = g.products[g.products.length - 1], last = lp ? lp.rows[lp.rows.length - 1] : null;
|
|
583
579
|
return progRow(g, o.progState(g.program), X, W, ov, o, msO, pxPct, now) + (o.folded(g.key) ? '' : g.products.map((x) => (g.products.length > 1 ? o.subHtml(x.name) : '') + x.rows.map((r) => one(r, ' tg-kid' + (r === last ? ' last' : ''))).join('')).join(''));
|
|
584
|
-
}).join('') :
|
|
580
|
+
}).join('') : flat.map((r) => one(r)).join('');
|
|
585
581
|
// the sprint band and the release keep their labels under the axis on Quarter, where a week is wide enough to read them
|
|
586
582
|
const axl = sp === 'quarter' ? `${sa && sb && sb > W.from && sa < W.to ? `<span class="tg-axl sb" style="left:${P(X(sa))}">Jira ${esc(sprint.name.toLowerCase())} · ${esc(md(sa))} – ${esc(md(sb))}</span>` : ''}${ra && ra > W.from && ra < W.to ? `<span class="tg-axl rl" style="left:${P(X(ra))}">${SV(FLAG)}${esc(rel.name)} · ${esc(md(ra))}</span>` : ''}` : '';
|
|
587
583
|
const axis = axl ? `<div class="tg-ax"><span class="tg-n"></span><div class="tg-p">${axl}</div></div>` : '';
|
|
588
|
-
const foot = none.length ? `<div class="tg-ft"
|
|
584
|
+
const foot = none.length ? `<div class="tg-ft"><span class="tg-none"><b>${none.length} ${none.length === 1 ? 'has' : 'have'} no dates${footNone.length ? ':' : ''}</b>${footNone.length ? ` <span>${esc(footNone.slice(0, 4).map((r) => r.title).join(', '))}${footNone.length > 4 ? ' and ' + (footNone.length - 4) + ' more' : ''}</span>` : ''}${askBtn}</span></div>` : '';
|
|
589
585
|
// the Deliveries lane (batch 13 item 4, XcalmA6Lane): one row under the axis, a list per column (o.lane draws it; none: nothing)
|
|
590
586
|
const laneH = o.lane ? o.lane({ W, X, rows: list, groups: G, now, phone: false, pxPct }) || '' : '';
|
|
591
587
|
// the columns' width is the name column's (--nw: the sticky block, the left fade and the Deliveries fold all read it)
|
|
@@ -270,18 +270,19 @@ function claudeSec() {
|
|
|
270
270
|
return '<h4>Claude login and profile</h4><div data-claude-setup></div>'
|
|
271
271
|
+ trow('API key', keySub, keyRight)
|
|
272
272
|
+ (k.present && C.off ? trow('Runs on the API', 'Off: PMOS_GRILL_API is off in the environment.', '<span class="ev-pill">Off</span>') : '')
|
|
273
|
-
+ sharpenRows(
|
|
273
|
+
+ sharpenRows();
|
|
274
274
|
}
|
|
275
275
|
/* Settings › Claude › Sharpen prompts (batch 26 item 2; pm/next-prompt-ui.js, lib/routes-next-prompt.js): the suggestions after a reply, Sharpen as I
|
|
276
|
-
* type (
|
|
276
|
+
* type (on by default: both run as a background `claude -p` in the PM's own Claude session, Wawan 2026-11-04), the style, and Your own rules.
|
|
277
277
|
* The same style and switch are in the chat's Sharper card menu; a save here tells the chat (the event pmos:prompt-prefs). */
|
|
278
278
|
const NPK = { 'prompt.next': ['nextSet', 'next'], 'prompt.sharpen': ['sharpenSet', 'sharpen'], 'prompt.style': ['style', 'style'], 'prompt.rules': ['rulesText', 'rules'] }; // row key → [GET field, POST key]
|
|
279
279
|
const setVal = (k, d) => { const v = T.np ? T.np[NPK[k][0]] : undefined; return v !== undefined && v !== null ? v : d; };
|
|
280
|
-
function sharpenRows(
|
|
280
|
+
function sharpenRows() {
|
|
281
281
|
const next = setVal('prompt.next', true) !== false, sh = setVal('prompt.sharpen', 'auto'), style = setVal('prompt.style', 'engineer'), rules = String(setVal('prompt.rules', '') || '');
|
|
282
|
-
const on = sh
|
|
283
|
-
const
|
|
284
|
-
|
|
282
|
+
const on = sh !== 'off' && !(T.np && T.np.sharpen === false && sh === 'auto');
|
|
283
|
+
const down = T.np && T.np.apiDown;
|
|
284
|
+
const shSub = !on ? (down && down.auth ? 'Off for now: your API key was refused. Replace it under API key above.' : 'Off. No spinner and no Sharper card.')
|
|
285
|
+
: 'When you pause on a rough prompt, Claude writes a sharper version above the box. It runs in your Claude session and takes a few seconds.';
|
|
285
286
|
const styleSub = style === 'polish' ? 'Clearer and shorter, your meaning kept.' : style === 'own' ? (rules.trim() ? 'Your rules below.' : 'Write your rules below first.') : 'Builds the best prompt for the job.';
|
|
286
287
|
return '<h4>Sharpen prompts</h4>'
|
|
287
288
|
+ trow('Suggest the next prompt', 'After a reply, up to two prompts above the box. A tap puts one in the box. Nothing sends.', sw(next, 'data-ev-np="prompt.next"', 'Suggest the next prompt'))
|
|
@@ -380,7 +381,7 @@ async function press(e) {
|
|
|
380
381
|
}
|
|
381
382
|
if (t.dataset.evSeg === 'prompt.style') { await savePrompt('prompt.style', t.dataset.v, 'Saved. Sharper prompts follow this style.'); return; }
|
|
382
383
|
if (t.dataset.evNp === 'prompt.next') { const on = setVal('prompt.next', true) === false; await savePrompt('prompt.next', on, on ? 'Saved. Claude suggests the next prompt after a reply.' : 'Saved. No suggestions after a reply.'); return; }
|
|
383
|
-
if (t.dataset.evNp === 'prompt.sharpen') { const
|
|
384
|
+
if (t.dataset.evNp === 'prompt.sharpen') { const sh = setVal('prompt.sharpen', 'auto'), was = sh !== 'off' && !(T.np && T.np.sharpen === false && sh === 'auto'); await savePrompt('prompt.sharpen', was ? 'off' : 'on', was ? 'Saved. Sharpen as I type is off.' : 'Saved. Sharpen as I type is on.'); return; }
|
|
384
385
|
if ('evRulesave' in t.dataset) { const ta = T.el.querySelector('#evRules'); await savePrompt('prompt.rules', ta ? ta.value.slice(0, 1000) : '', 'Saved. Your own rules are used when Sharpen as is Your own.'); return; }
|
|
385
386
|
if (t.dataset.evSeg && t.dataset.evSeg.startsWith('adler.')) { const r = await AD.savePref(pmPost, t.dataset.evSeg, t.dataset.v); T.msg = r && r.ok ? 'Saved.' : (r && r.error) || 'Not saved.'; draw(); return; }
|
|
386
387
|
if (t.dataset.evDress) {
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
@font-face{font-family:"Space Grotesk";src:url(/vendor/space-grotesk-var.woff2) format("woff2");font-weight:300 700;font-display:swap}
|
|
2
|
+
@font-face{font-family:"IBM Plex Mono";src:url(/vendor/ibm-plex-mono-400.woff2) format("woff2");font-weight:400;font-display:swap}
|
|
3
|
+
@font-face{font-family:"IBM Plex Mono";src:url(/vendor/ibm-plex-mono-500.woff2) format("woff2");font-weight:500;font-display:swap}
|
|
4
|
+
@font-face{font-family:"IBM Plex Mono";src:url(/vendor/ibm-plex-mono-700.woff2) format("woff2");font-weight:600 700;font-display:swap}
|
|
5
|
+
:root{--fw-regular:400;--fw-strong:600;--fs-field:16px;--fs-display:26px;--fs-page:26px;--fs-panel:18px;--fs-section:15px;--fs-row:15px;--fs-body:15px;--fs-meta:13px;--fs-label:13px;--fs-badge:13px;--lh-display:1.2;--lh-page:1.2;--lh-panel:1.3;--lh-section:1.35;--lh-row:1.4;--lh-body:1.6;--lh-meta:1.5;--lh-label:1.35;--lh-badge:1.2}
|
|
1
6
|
/* premium-vw-guide.css — the guide in the Viewer (premium-vw-guide.js; canvas Exploring › User guide, option B): a page rail on the left,
|
|
2
7
|
* the page on the right with its section chips. Under 720 px the rail hides and the list icon shows it over the page. Quiet: hairlines, one
|
|
3
8
|
* well for the open page, no colour but the chips' hover. */
|
|
4
|
-
.vwdoc.gd{padding:0}
|
|
9
|
+
.vwdoc.gd{padding:0;max-width:none}
|
|
5
10
|
.gd-wrap{container-type:inline-size;display:flex;align-items:flex-start;gap:20px;position:relative}
|
|
6
11
|
.gd-rail{width:248px;flex:none;position:sticky;top:0;max-height:calc(100vh - 140px);overflow:auto;padding:4px 4px 16px 0;display:flex;flex-direction:column;gap:2px}
|
|
7
|
-
.gd-read{flex:1;min-width:0}
|
|
12
|
+
.gd-read{flex:1;min-width:0;max-width:none}
|
|
8
13
|
.gd-tog{display:none}
|
|
9
14
|
.gd-find{display:flex;align-items:center;gap:8px;height:40px;padding:0 10px;margin:0 0 6px;border-radius:10px;background:var(--well);color:var(--ink-3)}
|
|
10
15
|
.gd-find .i{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;flex:none}
|
package/workspace/server.js
CHANGED
|
@@ -4475,7 +4475,7 @@ function startHeadless(base, workdir, kind, launchArgs, env, initialInput, addDi
|
|
|
4475
4475
|
// grandchild keeps the output pipes open after the run is stopped.
|
|
4476
4476
|
// Windows: `claude` installed by npm is claude.cmd, which spawn() cannot start by name (lib/win-command.js).
|
|
4477
4477
|
const run = winCommand.resolve(base.agent, { env });
|
|
4478
|
-
try { child = spawn(run.file, [...run.prefix, ...args], { cwd: workdir, env, stdio: ['pipe', 'pipe', 'pipe'], detached: true }); }
|
|
4478
|
+
try { child = spawn(run.file, [...run.prefix, ...args], { cwd: workdir, env: headlessLib.headlessEnv(env), stdio: ['pipe', 'pipe', 'pipe'], detached: true }); }
|
|
4479
4479
|
catch (e) {
|
|
4480
4480
|
s.state = 'exited'; s.note = spawnFailureAdvice(e, base.agent, false);
|
|
4481
4481
|
say(`\x1b[38;5;203m✗ ${s.note}\x1b[0m\n`); persist(s); pushSessions(); pushWorkspaces(); return s;
|