create-pmos 0.496.0 → 0.497.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 +20 -20
- 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/scripts/workflow-state.js +7 -19
- package/payload/state/discoveries.md +1 -1
- package/workspace/changelog.json +19 -0
- package/workspace/guide/data-freshness.md +5 -1
- package/workspace/lib/engine/workflow-state.js +7 -19
- package/workspace/lib/vendor/scripts/workflow-state.js +7 -19
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/chat-turn-end.js +19 -0
- package/workspace/public/pm/home-app.js +9 -1
- package/workspace/public/pm/home-chat.js +5 -0
- package/workspace/public/pm/home-ini.js +7 -0
- package/workspace/test-lists/chat-turn-end.json +9 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.497.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,25 @@
|
|
|
32
32
|
"audit"
|
|
33
33
|
],
|
|
34
34
|
"pmosChanges": [
|
|
35
|
+
{
|
|
36
|
+
"v": "0.497.0",
|
|
37
|
+
"at": "2026-11-03T02:04:11.405Z",
|
|
38
|
+
"f": 3,
|
|
39
|
+
"prs": [
|
|
40
|
+
{
|
|
41
|
+
"n": 1174,
|
|
42
|
+
"t": "Count only a real Jira handoff record, not keys in body text",
|
|
43
|
+
"s": "Count only a real Jira handoff record, not keys in body text",
|
|
44
|
+
"k": "fix"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"n": 1175,
|
|
48
|
+
"t": "The Viewer shows what a chat turn changed, with no refresh",
|
|
49
|
+
"s": "The Viewer shows what a chat turn changed, with no refresh",
|
|
50
|
+
"k": "fix"
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
},
|
|
35
54
|
{
|
|
36
55
|
"v": "0.496.0",
|
|
37
56
|
"at": "2026-11-03T00:20:24.723Z",
|
|
@@ -372,25 +391,6 @@
|
|
|
372
391
|
"k": "fix"
|
|
373
392
|
}
|
|
374
393
|
]
|
|
375
|
-
},
|
|
376
|
-
{
|
|
377
|
-
"v": "0.477.0",
|
|
378
|
-
"at": "2026-11-01T08:07:47.579Z",
|
|
379
|
-
"f": 3,
|
|
380
|
-
"prs": [
|
|
381
|
-
{
|
|
382
|
-
"n": 1130,
|
|
383
|
-
"t": "Chase a late owner without a Jira epic",
|
|
384
|
-
"s": "Chase a late owner without a Jira epic",
|
|
385
|
-
"k": "fix"
|
|
386
|
-
},
|
|
387
|
-
{
|
|
388
|
-
"n": 1135,
|
|
389
|
-
"t": "Feedback batch 20",
|
|
390
|
-
"s": "Feedback batch 20",
|
|
391
|
-
"k": "fix"
|
|
392
|
-
}
|
|
393
|
-
]
|
|
394
394
|
}
|
|
395
395
|
]
|
|
396
396
|
}
|
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-03T02:04:07Z"}
|
|
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-03T02:04:07Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -1438,11 +1438,11 @@ function documentDuplicates(root, opts) {
|
|
|
1438
1438
|
return { ok: true, profile, archive, groups };
|
|
1439
1439
|
}
|
|
1440
1440
|
/**
|
|
1441
|
-
* Jira keys
|
|
1442
|
-
* record's
|
|
1443
|
-
*
|
|
1441
|
+
* Jira keys a handoff record holds for an initiative (S4): the Jira tab's draft (`- jira: KEY` lines) and the
|
|
1442
|
+
* record's `jira:` / `jira_key:` / `jira_keys:` / `jira_epic:` / `jira_issue_key:` frontmatter. Body text and found documents are not
|
|
1443
|
+
* read: a key a PM mentions in prose ("see Jira LOY-41", an epic link in a spec) is a reference, not a handoff.
|
|
1444
1444
|
*/
|
|
1445
|
-
function jiraKeysOf(
|
|
1445
|
+
function jiraKeysOf(L, id, fm) {
|
|
1446
1446
|
const keys = [];
|
|
1447
1447
|
const draft = path.join(path.dirname(L.initiatives), 'tracker', 'jira', `${id}-tickets.md`);
|
|
1448
1448
|
try {
|
|
@@ -1451,21 +1451,9 @@ function jiraKeysOf(root, L, id, fm, found) {
|
|
|
1451
1451
|
if (m && JIRA_KEY.test(m[1].toUpperCase())) keys.push(m[1].toUpperCase());
|
|
1452
1452
|
}
|
|
1453
1453
|
} catch { /* no draft */ }
|
|
1454
|
-
for (const k of ['jira', 'jira_key', 'jira_keys', 'jira_epic']) {
|
|
1454
|
+
for (const k of ['jira', 'jira_key', 'jira_keys', 'jira_epic', 'jira_issue_key']) {
|
|
1455
1455
|
for (const m of String(fm[k] || '').toUpperCase().matchAll(/\b([A-Z][A-Z0-9_]+-\d+)\b/g)) keys.push(m[1]);
|
|
1456
1456
|
}
|
|
1457
|
-
const fromText = (text, lines) => {
|
|
1458
|
-
for (const m of String(text).matchAll(/\/browse\/([A-Z][A-Z0-9_]+-\d+)\b/g)) keys.push(m[1]);
|
|
1459
|
-
if (lines) for (const line of String(text).split('\n')) {
|
|
1460
|
-
if (!/jira/i.test(line)) continue;
|
|
1461
|
-
for (const m of line.matchAll(/\b([A-Z][A-Z0-9_]+-\d+)\b/g)) keys.push(m[1]);
|
|
1462
|
-
}
|
|
1463
|
-
};
|
|
1464
|
-
try { fromText(fs.readFileSync(path.join(L.initiatives, `${id}.md`), 'utf8'), true); } catch { /* no record text */ }
|
|
1465
|
-
for (const rel of found ? [].concat(found.brief, found.draft, found.review, found.handoff) : []) {
|
|
1466
|
-
if (!SCAN_TEXT.test(rel)) continue;
|
|
1467
|
-
try { fromText(fs.readFileSync(path.join(root, rel), 'utf8').slice(0, 200000), false); } catch { /* unreadable */ }
|
|
1468
|
-
}
|
|
1469
1457
|
return [...new Set(keys)];
|
|
1470
1458
|
}
|
|
1471
1459
|
|
|
@@ -1483,7 +1471,7 @@ function jiraKeysOf(root, L, id, fm, found) {
|
|
|
1483
1471
|
* handoff — `handed_off:` names where it went; n/a for decision records, reports and chores
|
|
1484
1472
|
* When the record names no file for brief, draft or review, or names one that does not exist, a
|
|
1485
1473
|
* document the repository scan found for the id completes the step (scan-documents S3); Jira keys
|
|
1486
|
-
* in the
|
|
1474
|
+
* in the Jira tab's draft or the record's `jira:` frontmatter complete the handoff (S4), never a key in body text. Found evidence never blocks and never fails CI.
|
|
1487
1475
|
* Track: `light` (guidance — only route and anchor can fail CI) or `governed` (brief and draft
|
|
1488
1476
|
* existence and their order fail CI for a non-legacy record). Review, approval and handoff are never
|
|
1489
1477
|
* CI-blocking in either track: none can be true before the PR exists.
|
|
@@ -1564,7 +1552,7 @@ function pmLadder({ id, fm, lane, effLane, knownLane, legacy, steps, root, L, re
|
|
|
1564
1552
|
if (!flows('handoff')) steps.push(step('handoff', 'handed off', 'na', `${effLane} — nothing to hand off`));
|
|
1565
1553
|
else {
|
|
1566
1554
|
const to = String(fm.handed_off || '').trim();
|
|
1567
|
-
const keys = to ? [] : jiraKeysOf(
|
|
1555
|
+
const keys = to ? [] : jiraKeysOf(L, id, fm);
|
|
1568
1556
|
steps.push(to ? step('handoff', 'handed off', 'done', `handed off — ${to}`)
|
|
1569
1557
|
: keys.length ? step('handoff', 'handed off', 'done', `handed off to Jira — ${keys.slice(0, 5).join(', ')}${keys.length > 5 ? ` and ${keys.length - 5} more` : ''}`)
|
|
1570
1558
|
: foundOf('handoff') ? step('handoff', 'handed off', 'done', `${foundOf('handoff')} — handoff found by the repository scan`)
|
|
@@ -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-03T02:04:07Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
package/workspace/changelog.json
CHANGED
|
@@ -1,4 +1,23 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"v": "0.497.0",
|
|
4
|
+
"at": "2026-11-03T02:04:11.405Z",
|
|
5
|
+
"f": 3,
|
|
6
|
+
"prs": [
|
|
7
|
+
{
|
|
8
|
+
"n": 1174,
|
|
9
|
+
"t": "Count only a real Jira handoff record, not keys in body text",
|
|
10
|
+
"s": "Count only a real Jira handoff record, not keys in body text",
|
|
11
|
+
"k": "fix"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"n": 1175,
|
|
15
|
+
"t": "The Viewer shows what a chat turn changed, with no refresh",
|
|
16
|
+
"s": "The Viewer shows what a chat turn changed, with no refresh",
|
|
17
|
+
"k": "fix"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
},
|
|
2
21
|
{
|
|
3
22
|
"v": "0.496.0",
|
|
4
23
|
"at": "2026-11-03T00:20:24.723Z",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Data freshness: how the workspace keeps up to date, and how old each source is"
|
|
4
|
-
timestamp: 2026-11-
|
|
4
|
+
timestamp: 2026-11-03
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Data freshness
|
|
@@ -17,6 +17,10 @@ The workspace checks each registered repository every 10 seconds for a change: a
|
|
|
17
17
|
|
|
18
18
|
To stop the checks, turn off **Repository and initiatives** in Settings › Data refresh. This stops them for every registered repository. The pages then show what was read last, and **Refresh now** reads it again. Turn it on to start the checks again.
|
|
19
19
|
|
|
20
|
+
## After a chat turn
|
|
21
|
+
|
|
22
|
+
When Claude finishes a turn in a Home chat, Home reads its records again at once, so the list and the Viewer show what Claude changed: a renamed task, a new date, a moved stage. You do not need to refresh the page. Open folds, the Viewer's scroll and the Timeline's place stay as they were. A document open in the Viewer already reads its file again when the file changes on disk.
|
|
23
|
+
|
|
20
24
|
## Jira, calendar, My work and tracker items
|
|
21
25
|
|
|
22
26
|
The workspace holds no password or token, and it never calls these services. An agent session reads each one with your own connection, on the schedule set in **Settings › Data refresh**, or when you press **Refresh**:
|
|
@@ -1438,11 +1438,11 @@ function documentDuplicates(root, opts) {
|
|
|
1438
1438
|
return { ok: true, profile, archive, groups };
|
|
1439
1439
|
}
|
|
1440
1440
|
/**
|
|
1441
|
-
* Jira keys
|
|
1442
|
-
* record's
|
|
1443
|
-
*
|
|
1441
|
+
* Jira keys a handoff record holds for an initiative (S4): the Jira tab's draft (`- jira: KEY` lines) and the
|
|
1442
|
+
* record's `jira:` / `jira_key:` / `jira_keys:` / `jira_epic:` / `jira_issue_key:` frontmatter. Body text and found documents are not
|
|
1443
|
+
* read: a key a PM mentions in prose ("see Jira LOY-41", an epic link in a spec) is a reference, not a handoff.
|
|
1444
1444
|
*/
|
|
1445
|
-
function jiraKeysOf(
|
|
1445
|
+
function jiraKeysOf(L, id, fm) {
|
|
1446
1446
|
const keys = [];
|
|
1447
1447
|
const draft = path.join(path.dirname(L.initiatives), 'tracker', 'jira', `${id}-tickets.md`);
|
|
1448
1448
|
try {
|
|
@@ -1451,21 +1451,9 @@ function jiraKeysOf(root, L, id, fm, found) {
|
|
|
1451
1451
|
if (m && JIRA_KEY.test(m[1].toUpperCase())) keys.push(m[1].toUpperCase());
|
|
1452
1452
|
}
|
|
1453
1453
|
} catch { /* no draft */ }
|
|
1454
|
-
for (const k of ['jira', 'jira_key', 'jira_keys', 'jira_epic']) {
|
|
1454
|
+
for (const k of ['jira', 'jira_key', 'jira_keys', 'jira_epic', 'jira_issue_key']) {
|
|
1455
1455
|
for (const m of String(fm[k] || '').toUpperCase().matchAll(/\b([A-Z][A-Z0-9_]+-\d+)\b/g)) keys.push(m[1]);
|
|
1456
1456
|
}
|
|
1457
|
-
const fromText = (text, lines) => {
|
|
1458
|
-
for (const m of String(text).matchAll(/\/browse\/([A-Z][A-Z0-9_]+-\d+)\b/g)) keys.push(m[1]);
|
|
1459
|
-
if (lines) for (const line of String(text).split('\n')) {
|
|
1460
|
-
if (!/jira/i.test(line)) continue;
|
|
1461
|
-
for (const m of line.matchAll(/\b([A-Z][A-Z0-9_]+-\d+)\b/g)) keys.push(m[1]);
|
|
1462
|
-
}
|
|
1463
|
-
};
|
|
1464
|
-
try { fromText(fs.readFileSync(path.join(L.initiatives, `${id}.md`), 'utf8'), true); } catch { /* no record text */ }
|
|
1465
|
-
for (const rel of found ? [].concat(found.brief, found.draft, found.review, found.handoff) : []) {
|
|
1466
|
-
if (!SCAN_TEXT.test(rel)) continue;
|
|
1467
|
-
try { fromText(fs.readFileSync(path.join(root, rel), 'utf8').slice(0, 200000), false); } catch { /* unreadable */ }
|
|
1468
|
-
}
|
|
1469
1457
|
return [...new Set(keys)];
|
|
1470
1458
|
}
|
|
1471
1459
|
|
|
@@ -1483,7 +1471,7 @@ function jiraKeysOf(root, L, id, fm, found) {
|
|
|
1483
1471
|
* handoff — `handed_off:` names where it went; n/a for decision records, reports and chores
|
|
1484
1472
|
* When the record names no file for brief, draft or review, or names one that does not exist, a
|
|
1485
1473
|
* document the repository scan found for the id completes the step (scan-documents S3); Jira keys
|
|
1486
|
-
* in the
|
|
1474
|
+
* in the Jira tab's draft or the record's `jira:` frontmatter complete the handoff (S4), never a key in body text. Found evidence never blocks and never fails CI.
|
|
1487
1475
|
* Track: `light` (guidance — only route and anchor can fail CI) or `governed` (brief and draft
|
|
1488
1476
|
* existence and their order fail CI for a non-legacy record). Review, approval and handoff are never
|
|
1489
1477
|
* CI-blocking in either track: none can be true before the PR exists.
|
|
@@ -1564,7 +1552,7 @@ function pmLadder({ id, fm, lane, effLane, knownLane, legacy, steps, root, L, re
|
|
|
1564
1552
|
if (!flows('handoff')) steps.push(step('handoff', 'handed off', 'na', `${effLane} — nothing to hand off`));
|
|
1565
1553
|
else {
|
|
1566
1554
|
const to = String(fm.handed_off || '').trim();
|
|
1567
|
-
const keys = to ? [] : jiraKeysOf(
|
|
1555
|
+
const keys = to ? [] : jiraKeysOf(L, id, fm);
|
|
1568
1556
|
steps.push(to ? step('handoff', 'handed off', 'done', `handed off — ${to}`)
|
|
1569
1557
|
: keys.length ? step('handoff', 'handed off', 'done', `handed off to Jira — ${keys.slice(0, 5).join(', ')}${keys.length > 5 ? ` and ${keys.length - 5} more` : ''}`)
|
|
1570
1558
|
: foundOf('handoff') ? step('handoff', 'handed off', 'done', `${foundOf('handoff')} — handoff found by the repository scan`)
|
|
@@ -1438,11 +1438,11 @@ function documentDuplicates(root, opts) {
|
|
|
1438
1438
|
return { ok: true, profile, archive, groups };
|
|
1439
1439
|
}
|
|
1440
1440
|
/**
|
|
1441
|
-
* Jira keys
|
|
1442
|
-
* record's
|
|
1443
|
-
*
|
|
1441
|
+
* Jira keys a handoff record holds for an initiative (S4): the Jira tab's draft (`- jira: KEY` lines) and the
|
|
1442
|
+
* record's `jira:` / `jira_key:` / `jira_keys:` / `jira_epic:` / `jira_issue_key:` frontmatter. Body text and found documents are not
|
|
1443
|
+
* read: a key a PM mentions in prose ("see Jira LOY-41", an epic link in a spec) is a reference, not a handoff.
|
|
1444
1444
|
*/
|
|
1445
|
-
function jiraKeysOf(
|
|
1445
|
+
function jiraKeysOf(L, id, fm) {
|
|
1446
1446
|
const keys = [];
|
|
1447
1447
|
const draft = path.join(path.dirname(L.initiatives), 'tracker', 'jira', `${id}-tickets.md`);
|
|
1448
1448
|
try {
|
|
@@ -1451,21 +1451,9 @@ function jiraKeysOf(root, L, id, fm, found) {
|
|
|
1451
1451
|
if (m && JIRA_KEY.test(m[1].toUpperCase())) keys.push(m[1].toUpperCase());
|
|
1452
1452
|
}
|
|
1453
1453
|
} catch { /* no draft */ }
|
|
1454
|
-
for (const k of ['jira', 'jira_key', 'jira_keys', 'jira_epic']) {
|
|
1454
|
+
for (const k of ['jira', 'jira_key', 'jira_keys', 'jira_epic', 'jira_issue_key']) {
|
|
1455
1455
|
for (const m of String(fm[k] || '').toUpperCase().matchAll(/\b([A-Z][A-Z0-9_]+-\d+)\b/g)) keys.push(m[1]);
|
|
1456
1456
|
}
|
|
1457
|
-
const fromText = (text, lines) => {
|
|
1458
|
-
for (const m of String(text).matchAll(/\/browse\/([A-Z][A-Z0-9_]+-\d+)\b/g)) keys.push(m[1]);
|
|
1459
|
-
if (lines) for (const line of String(text).split('\n')) {
|
|
1460
|
-
if (!/jira/i.test(line)) continue;
|
|
1461
|
-
for (const m of line.matchAll(/\b([A-Z][A-Z0-9_]+-\d+)\b/g)) keys.push(m[1]);
|
|
1462
|
-
}
|
|
1463
|
-
};
|
|
1464
|
-
try { fromText(fs.readFileSync(path.join(L.initiatives, `${id}.md`), 'utf8'), true); } catch { /* no record text */ }
|
|
1465
|
-
for (const rel of found ? [].concat(found.brief, found.draft, found.review, found.handoff) : []) {
|
|
1466
|
-
if (!SCAN_TEXT.test(rel)) continue;
|
|
1467
|
-
try { fromText(fs.readFileSync(path.join(root, rel), 'utf8').slice(0, 200000), false); } catch { /* unreadable */ }
|
|
1468
|
-
}
|
|
1469
1457
|
return [...new Set(keys)];
|
|
1470
1458
|
}
|
|
1471
1459
|
|
|
@@ -1483,7 +1471,7 @@ function jiraKeysOf(root, L, id, fm, found) {
|
|
|
1483
1471
|
* handoff — `handed_off:` names where it went; n/a for decision records, reports and chores
|
|
1484
1472
|
* When the record names no file for brief, draft or review, or names one that does not exist, a
|
|
1485
1473
|
* document the repository scan found for the id completes the step (scan-documents S3); Jira keys
|
|
1486
|
-
* in the
|
|
1474
|
+
* in the Jira tab's draft or the record's `jira:` frontmatter complete the handoff (S4), never a key in body text. Found evidence never blocks and never fails CI.
|
|
1487
1475
|
* Track: `light` (guidance — only route and anchor can fail CI) or `governed` (brief and draft
|
|
1488
1476
|
* existence and their order fail CI for a non-legacy record). Review, approval and handoff are never
|
|
1489
1477
|
* CI-blocking in either track: none can be true before the PR exists.
|
|
@@ -1564,7 +1552,7 @@ function pmLadder({ id, fm, lane, effLane, knownLane, legacy, steps, root, L, re
|
|
|
1564
1552
|
if (!flows('handoff')) steps.push(step('handoff', 'handed off', 'na', `${effLane} — nothing to hand off`));
|
|
1565
1553
|
else {
|
|
1566
1554
|
const to = String(fm.handed_off || '').trim();
|
|
1567
|
-
const keys = to ? [] : jiraKeysOf(
|
|
1555
|
+
const keys = to ? [] : jiraKeysOf(L, id, fm);
|
|
1568
1556
|
steps.push(to ? step('handoff', 'handed off', 'done', `handed off — ${to}`)
|
|
1569
1557
|
: keys.length ? step('handoff', 'handed off', 'done', `handed off to Jira — ${keys.slice(0, 5).join(', ')}${keys.length > 5 ? ` and ${keys.length - 5} more` : ''}`)
|
|
1570
1558
|
: foundOf('handoff') ? step('handoff', 'handed off', 'done', `${foundOf('handoff')} — handoff found by the repository scan`)
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.497.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.497.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@anthropic-ai/sdk": "^1.3.0",
|
package/workspace/package.json
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/* pm/chat-turn-end.js — tells when a chat turn ended, so the page beside the chat reads its records again (batch 25 item 3: "When chat
|
|
2
|
+
* said it has made changes to the doc or content shown on the viewer, the content on the viewer should automatically update without
|
|
3
|
+
* the user needing to refresh"). home-chat.js takes a snapshot on each read of the conversation; the host re-reads what the Viewer shows
|
|
4
|
+
* when a turn ends.
|
|
5
|
+
*
|
|
6
|
+
* snapOf(key, busy, messages) { key, busy, agents }: the conversation's place, whether Claude is still at work, and how many of its
|
|
7
|
+
* messages are not the PM's.
|
|
8
|
+
* turnEnded(prev, cur) true once per finished turn: the same conversation, not at work now, and either at work on the last read
|
|
9
|
+
* or with more of Claude's messages than then (a turn that started and finished between two reads).
|
|
10
|
+
* The first read of a conversation is never a finished turn: opening a chat does not re-read the page. */
|
|
11
|
+
export function snapOf(key, busy, messages) {
|
|
12
|
+
const agents = (Array.isArray(messages) ? messages : []).filter((m) => m && m.role !== 'user').length;
|
|
13
|
+
return { key: String(key || ''), busy: !!busy, agents };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function turnEnded(prev, cur) {
|
|
17
|
+
if (!prev || !cur || !cur.key || prev.key !== cur.key || cur.busy) return false;
|
|
18
|
+
return prev.busy || cur.agents > prev.agents;
|
|
19
|
+
}
|
|
@@ -47,7 +47,7 @@ import * as YD from './home-day.js';
|
|
|
47
47
|
import * as AD from './cozy-adler.js'; // Adler, Claude's face (batch 16 item 2)
|
|
48
48
|
import * as OKL from './okr-list.js'; // the product page's Goals line (batch 16 item 6, GsProduct)
|
|
49
49
|
import { canDictate, dictate } from './home-dictate.js';
|
|
50
|
-
import { mountIni, iniList, iniOverview, iniPage, iniAddTask } from './home-ini.js'; // the initiative's page, its stage move, Tasks and Core documents (premium-uncovered-features, HbC)
|
|
50
|
+
import { mountIni, iniList, iniOverview, iniPage, iniAddTask, iniFresh } from './home-ini.js'; // the initiative's page, its stage move, Tasks and Core documents (premium-uncovered-features, HbC)
|
|
51
51
|
import * as MEN from './home-mentions.js';
|
|
52
52
|
import * as LS from './load-shapes.js'; // the last view, the grey shapes and the held Viewer column while Home reads (page-open-speed) // @Mentions, the permanent row at the top of Home and its Viewer (Viewer kit, XvANew)
|
|
53
53
|
import { mountRecRow, clockOf as recClock } from './home-rec-row.js';
|
|
@@ -756,7 +756,15 @@ const chat = mountChat($('#hmChat'), {
|
|
|
756
756
|
desk: () => !isPhone(),
|
|
757
757
|
reopenViewer: () => reopenViewer(),
|
|
758
758
|
head: (t) => (isPhone() ? '' : TD.chatHeadHtml(chatHead(t))), // the phone names it in its top bar (#hmTop)
|
|
759
|
+
// a chat turn finished: read the records again, so the list and the Viewer show what Claude changed with no refresh (batch 25 item 3).
|
|
760
|
+
// load() keeps the open folds (ini-view.js FOLD) and the Viewer's scroll (home-ini.js redrawView); the initiative's layout is read again too
|
|
761
|
+
turnEnded: () => turnEnded(),
|
|
759
762
|
});
|
|
763
|
+
let turnT = null;
|
|
764
|
+
function turnEnded() {
|
|
765
|
+
clearTimeout(turnT);
|
|
766
|
+
turnT = setTimeout(async () => { await load(); iniFresh(); }, 400); // two chats that finish together read once
|
|
767
|
+
}
|
|
760
768
|
/* claude-onboarding-build: set up a project as a conversation in this chat's place (cozy-onboard.js). It opens on ?onboard=1, shows the
|
|
761
769
|
* finished project's next steps on ?onboarded=1, and opens by itself on a first run: setup not finished and no registered folder holds an
|
|
762
770
|
* initiative. A named workspace does not stop it: the installer opens its own empty ~/PMOS folder by name (bug 2026-11-02, Windows). */
|
|
@@ -33,6 +33,7 @@ import { splitFiles, filesHtml, sizeText, MAX_BYTES, MAX_FILES, setAttachSink }
|
|
|
33
33
|
import * as VWC from './cozy-viewer.js';
|
|
34
34
|
import { canDictate, dictate } from './home-dictate.js';
|
|
35
35
|
import { adler } from './cozy-adler.js';
|
|
36
|
+
import { snapOf, turnEnded } from './chat-turn-end.js'; // a finished turn re-reads what the Viewer shows (batch 25 item 3)
|
|
36
37
|
/* The views a chat opens go in the Viewer: on a page with no dialog the old viewer's views draw in the cozy card (premium-viewer.js). */
|
|
37
38
|
const views = {
|
|
38
39
|
ctx: () => import('./premium-vw-ctx.js'), file: () => import('./premium-vw-file.js'), attach: () => import('./premium-attach.js'),
|
|
@@ -92,7 +93,11 @@ export function mountChat(el, host) {
|
|
|
92
93
|
C.th = th && th.ok ? th : null; C.conv = conv && conv.ok ? conv : null;
|
|
93
94
|
if (host.touch && sid() && C.touched !== sid()) { C.touched = sid(); host.touch(C.touched); } // the Live pill: the PM opened it now
|
|
94
95
|
C.sess = host.session ? host.session(t.sid || (C.th && C.th.session)) : null;
|
|
96
|
+
// a turn that finished: Claude may have changed what the Viewer shows (tasks, documents), so the host reads it again (batch 25 item 3)
|
|
97
|
+
const snap = snapOf(t.key || t.sid, running() || !!(C.th && C.th.working), C.conv && C.conv.messages);
|
|
98
|
+
const ended = turnEnded(C.turn, snap); C.turn = snap;
|
|
95
99
|
draw();
|
|
100
|
+
if (ended && host.turnEnded) host.turnEnded(t);
|
|
96
101
|
clearTimeout(C.timer);
|
|
97
102
|
const live = (C.th && (C.th.session || C.th.working)) || (t.sid && C.conv && C.conv.state !== 'ended');
|
|
98
103
|
C.timer = setTimeout(() => { if (!document.hidden) read(); else C.timer = setTimeout(read, IDLE_POLL); }, live ? POLL : IDLE_POLL);
|
|
@@ -201,6 +201,13 @@ function ask(anchor, label, value, go, o = {}) { // one line of words, inline
|
|
|
201
201
|
i.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); const v = i.value.trim(); if (v) { closePop(); go(v); } } });
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
/** The open initiative's records changed elsewhere (a chat turn finished, batch 25 item 3): read its layout again, as a press does.
|
|
205
|
+
* The page redraws in place; the folds and the scroll stay. */
|
|
206
|
+
export function iniFresh() {
|
|
207
|
+
if (!X.ctx) return;
|
|
208
|
+
const id = X.ctx.S.ini, M = curM();
|
|
209
|
+
if (M) readL(id, M, true);
|
|
210
|
+
}
|
|
204
211
|
export function iniList(sc) {
|
|
205
212
|
if (!X.ctx || !sc) return;
|
|
206
213
|
sc.querySelectorAll('[data-hi-slot]').forEach((n) => n.remove());
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Batch 25 item 3: a finished chat turn re-reads what the Viewer shows, with no manual refresh (pm/chat-turn-end.js, home-chat.js, home-app.js). Pure builder plus the wiring.",
|
|
3
|
+
"modules": [
|
|
4
|
+
"chat-turn-end.test.js"
|
|
5
|
+
],
|
|
6
|
+
"seconds": {
|
|
7
|
+
"chat-turn-end.test.js": 1
|
|
8
|
+
}
|
|
9
|
+
}
|