create-pmos 0.496.0 → 0.498.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 +51 -33
- package/payload/AGENTS.md +1 -0
- 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 +35 -19
- package/payload/state/discoveries.md +1 -1
- package/workspace/briefing-premium-test.js +2 -2
- package/workspace/changelog.json +50 -0
- package/workspace/guide/briefing-premium.md +4 -2
- package/workspace/guide/data-freshness.md +5 -1
- package/workspace/guide/home.md +3 -3
- package/workspace/guide/launcher.md +7 -1
- package/workspace/lib/briefing-premium.js +38 -12
- package/workspace/lib/engine/workflow-state.js +35 -19
- package/workspace/lib/git-identity-cli.js +67 -0
- package/workspace/lib/git-identity.js +93 -0
- package/workspace/lib/pmos-verbs.js +3 -0
- package/workspace/lib/routes-boards.js +8 -3
- package/workspace/lib/vendor/scripts/workflow-state.js +35 -19
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/bd-ini-panel.js +1 -1
- package/workspace/public/pm/chat-turn-end.js +19 -0
- package/workspace/public/pm/home-app.js +13 -3
- package/workspace/public/pm/home-chat.js +7 -2
- package/workspace/public/pm/home-ini.js +8 -1
- package/workspace/public/pm/home-need.js +1 -1
- package/workspace/public/pm/ini-edit.js +82 -19
- package/workspace/public/pm/ini-tasks.js +6 -1
- package/workspace/public/pm/ini-view-model.js +3 -1
- package/workspace/public/pm/ini-view.css +31 -7
- package/workspace/public/pm/premium-board.js +2 -2
- package/workspace/stage-card-tasks-test.js +4 -3
- package/workspace/test-lists/chat-turn-end.json +9 -0
- package/workspace/test-lists/git-identity.json +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.498.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,56 @@
|
|
|
32
32
|
"audit"
|
|
33
33
|
],
|
|
34
34
|
"pmosChanges": [
|
|
35
|
+
{
|
|
36
|
+
"v": "0.498.0",
|
|
37
|
+
"at": "2026-11-03T02:50:09.805Z",
|
|
38
|
+
"f": 3,
|
|
39
|
+
"prs": [
|
|
40
|
+
{
|
|
41
|
+
"n": 1176,
|
|
42
|
+
"t": "Commit as the repo's own git identity; ask the PM once when it has none",
|
|
43
|
+
"s": "Commit as the repo's own git identity; ask the PM once when it has none",
|
|
44
|
+
"k": "fix"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"n": 1178,
|
|
48
|
+
"t": "Full task names wrap; add-task chips for stage, type, dates, owner",
|
|
49
|
+
"s": "Full task names wrap; add-task chips for stage, type, dates, owner",
|
|
50
|
+
"k": "new"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"n": 1179,
|
|
54
|
+
"t": "Each product repo pushes with its own GitHub login",
|
|
55
|
+
"s": "Each product repo pushes with its own GitHub login",
|
|
56
|
+
"k": "new"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"n": 1177,
|
|
60
|
+
"t": "A workflow routes the initiative; blocked steps ask only as ✦ proposals",
|
|
61
|
+
"s": "A workflow routes the initiative; blocked steps ask only as ✦ proposals",
|
|
62
|
+
"k": "fix"
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"v": "0.497.0",
|
|
68
|
+
"at": "2026-11-03T02:04:11.405Z",
|
|
69
|
+
"f": 3,
|
|
70
|
+
"prs": [
|
|
71
|
+
{
|
|
72
|
+
"n": 1174,
|
|
73
|
+
"t": "Count only a real Jira handoff record, not keys in body text",
|
|
74
|
+
"s": "Count only a real Jira handoff record, not keys in body text",
|
|
75
|
+
"k": "fix"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"n": 1175,
|
|
79
|
+
"t": "The Viewer shows what a chat turn changed, with no refresh",
|
|
80
|
+
"s": "The Viewer shows what a chat turn changed, with no refresh",
|
|
81
|
+
"k": "fix"
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
},
|
|
35
85
|
{
|
|
36
86
|
"v": "0.496.0",
|
|
37
87
|
"at": "2026-11-03T00:20:24.723Z",
|
|
@@ -359,38 +409,6 @@
|
|
|
359
409
|
"s": "Batch 21: feedback fixes (held for one merge)"
|
|
360
410
|
}
|
|
361
411
|
]
|
|
362
|
-
},
|
|
363
|
-
{
|
|
364
|
-
"v": "0.478.0",
|
|
365
|
-
"at": "2026-11-01T08:32:21.933Z",
|
|
366
|
-
"f": 3,
|
|
367
|
-
"prs": [
|
|
368
|
-
{
|
|
369
|
-
"n": 1138,
|
|
370
|
-
"t": "Shared-board milestone keeps its place; CI runs only the suites a PR reaches",
|
|
371
|
-
"s": "Shared-board milestone keeps its place; CI runs only the suites a PR reaches",
|
|
372
|
-
"k": "fix"
|
|
373
|
-
}
|
|
374
|
-
]
|
|
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
412
|
}
|
|
395
413
|
]
|
|
396
414
|
}
|
package/payload/AGENTS.md
CHANGED
|
@@ -89,6 +89,7 @@ Every initiative carries a `type` — the lane picks what the gate requires **be
|
|
|
89
89
|
- **Existing surface → the PM needs the understanding too.** That retrieval warms *you*. When the work touches something that already exists — the test is *would specifying this honestly require reading code that already exists?* — run `skills/explain-surface.skill` on that region **before** the brief/PRD is authored, in a fresh context and **not** by the agent that will write it. Standard for existing surfaces, skipped for greenfield. Without it, the agent acquires the understanding and the **PM approves a spec built on facts they never saw**.
|
|
90
90
|
4. Anchor: the initiative file must exist at stage `intake`/`spec` with its **lane's** spec/eval artifacts authored **before** build (eval-first, D13, generalized per lane — see the lane table above; `feature` = PRD + rubric; the incident lane alone records post-hoc). Contract per lane: `feature` = the rubric's sprint-contract section with the PM's explicit `PM_approved: true`; `discovery` = the PM-approved spike brief (question + timebox); `chore` = the stated intent; `content` = the PM-approved content brief; `prototype` = the PM-approved quick brief (the judgement exercised + the specimen format) — and **no rubric**, which is the decision, not an omission. No work before the lane's approval. When you question the PM to reach any of these, follow `skills/requirements-elicitation.skill`. Feature-lane `test_method`s SHOULD be executable checks written before build (D58) — wire `templates/product-ci-starter.yml` so tier 1 is never silently empty.
|
|
91
91
|
5. Branch `initiative/<id>`; execute at control altitude; one commit per artifact.
|
|
92
|
+
- **Commit as the repository's own git identity, and push with its own GitHub login.** Never pass an author on the command line (`-c user.name=…`, `-c user.email=…`, `--author`, `GIT_AUTHOR_*` or `GIT_COMMITTER_*`), and never use a name, email or login from your account, the session or another repository. The PM uses different identities and GitHub logins for different products. Run `pmos git-identity` (or `node "$PMOS_BIN" git-identity`) before the first commit or push in a repository, and again if `git commit` fails with "Author identity unknown" or `git push` fails with "Repository not found". It says what this repository uses, or lists what the PM uses in the other repositories. If something is missing, ask the PM once which identity and GitHub login this product uses, offering those. Save the answer with `pmos git-identity set --name "…" --email "…" --login <login>` (this repository only, never `--global`), then commit and push. After that, `git push` uses that login by itself; run `gh` in this repository as `GH_TOKEN="$(gh auth token --user <login>)" gh …`. Never run `gh auth switch`. Do not leave the PM's files staged.
|
|
92
93
|
6. Self-evaluate against the contract — self-eval is unreliable; it is an input to the gates, never a verdict.
|
|
93
94
|
7. Close the gates: **Quality** = this repo's CI plus the kit gate (`pmos/scripts/gate-initiative.sh`
|
|
94
95
|
+ `pmos/planning/evals/check_rubrics.py`); **Review** = score against the rubric with the evaluator skill (adversarial, outcome-only, default-to-refute) and append to `evals.jsonl`; **Acceptance** = the human PM merges and disposes — final and non-delegable. Never merge your own PR.
|
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:50:05Z"}
|
|
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:50:05Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -556,6 +556,29 @@ function profileOf(root) {
|
|
|
556
556
|
} catch { return 'product-code'; }
|
|
557
557
|
}
|
|
558
558
|
|
|
559
|
+
/*
|
|
560
|
+
* workflowOf(root, id) — the workflow an initiative runs, when its repository runs initiatives on workflows (Boards, Settings ›
|
|
561
|
+
* Workflows). The workspace gives every repository it keeps state for a default board, so every initiative there runs a workflow:
|
|
562
|
+
* an `ini-wf` line in the boards ledger (<state dir>/boards.jsonl, the dirs workspace/server.js stateDirFor reads) names its
|
|
563
|
+
* own, else it runs its board's (Product feature by default). A repository runs on workflows when it holds that ledger or a
|
|
564
|
+
* pmos/state/ dir. PMOS itself holds neither (its state is the workspace's own), so it still routes by lane and R0 still binds it.
|
|
565
|
+
* → the workflow's id, '' (its board's), or null (no workflows). The ledger is read again when its size or mtime changes.
|
|
566
|
+
*/
|
|
567
|
+
const wfMemo = new Map();
|
|
568
|
+
function workflowOf(root, id) {
|
|
569
|
+
const f = [['pmos', 'state'], ['state'], ['.pmos']].map((rel) => path.join(root, ...rel, 'boards.jsonl')).find((x) => fs.existsSync(x));
|
|
570
|
+
const on = !!f || fs.existsSync(path.join(root, 'pmos', 'state'));
|
|
571
|
+
if (!on) return null;
|
|
572
|
+
let key = '';
|
|
573
|
+
try { const st = f ? fs.statSync(f) : null; key = st ? st.size + ':' + st.mtimeMs : '-'; } catch { key = '?'; }
|
|
574
|
+
let m = wfMemo.get(root);
|
|
575
|
+
if (!m || m.key !== key) { // a held module sees a new line on the next read (the in-process freshness rule)
|
|
576
|
+
m = { key, wf: new Map() };
|
|
577
|
+
for (const l of f ? jsonl.readJsonl(f).rows : []) if (l && l.kind === 'ini-wf' && typeof l.id === 'string' && typeof l.wf === 'string' && l.wf) m.wf.set(l.id, l.wf);
|
|
578
|
+
wfMemo.set(root, m);
|
|
579
|
+
}
|
|
580
|
+
return m.wf.get(id) || '';
|
|
581
|
+
}
|
|
559
582
|
/** The lanes a repository's initiatives may declare — what the lane picker offers and the lane write accepts. */
|
|
560
583
|
function laneNames(profile) { return laneDefs.laneNames(profile === 'pm-artifacts' ? 'pm-artifacts' : 'product-code'); }
|
|
561
584
|
function lanesFor(root) {
|
|
@@ -735,6 +758,7 @@ function computeWorkflowDerived(id, opts) {
|
|
|
735
758
|
// ── route ─ the lane is chosen and the two intake questions are answered ─────────────────────
|
|
736
759
|
const sRoute = specimenRoute(fm.specimen_test);
|
|
737
760
|
const fRoute = surfaceRoute(fm.surface_check);
|
|
761
|
+
const onWf = workflowOf(root, id);
|
|
738
762
|
{
|
|
739
763
|
const missing = [];
|
|
740
764
|
if (sRoute === 'undeclared' || sRoute === 'malformed') missing.push('specimen_test');
|
|
@@ -742,6 +766,10 @@ function computeWorkflowDerived(id, opts) {
|
|
|
742
766
|
if (!missing.length) {
|
|
743
767
|
steps.push(step('route', 'route', 'done',
|
|
744
768
|
`${lane} lane · specimen test ${sRoute} · surface ${fRoute}`));
|
|
769
|
+
} else if (onWf !== null) {
|
|
770
|
+
// Batch 25 item 6: an initiative on a workflow is routed. The workflow's stages say what comes first, and PMOS proposes a
|
|
771
|
+
// switch when the BRD says another workflow fits, so the two intake questions are not asked of it again.
|
|
772
|
+
steps.push(step('route', 'route', 'done', `${onWf ? `runs the ${onWf} workflow` : 'runs its board\'s workflow'} — the workflow routes it`));
|
|
745
773
|
} else if (effLane !== 'feature') {
|
|
746
774
|
// The routing questions are asked of feature work only.
|
|
747
775
|
steps.push(step('route', 'route', 'done', `${lane} lane — routing questions are for features only (${CUT_NOTE})`));
|
|
@@ -1438,11 +1466,11 @@ function documentDuplicates(root, opts) {
|
|
|
1438
1466
|
return { ok: true, profile, archive, groups };
|
|
1439
1467
|
}
|
|
1440
1468
|
/**
|
|
1441
|
-
* Jira keys
|
|
1442
|
-
* record's
|
|
1443
|
-
*
|
|
1469
|
+
* Jira keys a handoff record holds for an initiative (S4): the Jira tab's draft (`- jira: KEY` lines) and the
|
|
1470
|
+
* record's `jira:` / `jira_key:` / `jira_keys:` / `jira_epic:` / `jira_issue_key:` frontmatter. Body text and found documents are not
|
|
1471
|
+
* 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
1472
|
*/
|
|
1445
|
-
function jiraKeysOf(
|
|
1473
|
+
function jiraKeysOf(L, id, fm) {
|
|
1446
1474
|
const keys = [];
|
|
1447
1475
|
const draft = path.join(path.dirname(L.initiatives), 'tracker', 'jira', `${id}-tickets.md`);
|
|
1448
1476
|
try {
|
|
@@ -1451,21 +1479,9 @@ function jiraKeysOf(root, L, id, fm, found) {
|
|
|
1451
1479
|
if (m && JIRA_KEY.test(m[1].toUpperCase())) keys.push(m[1].toUpperCase());
|
|
1452
1480
|
}
|
|
1453
1481
|
} catch { /* no draft */ }
|
|
1454
|
-
for (const k of ['jira', 'jira_key', 'jira_keys', 'jira_epic']) {
|
|
1482
|
+
for (const k of ['jira', 'jira_key', 'jira_keys', 'jira_epic', 'jira_issue_key']) {
|
|
1455
1483
|
for (const m of String(fm[k] || '').toUpperCase().matchAll(/\b([A-Z][A-Z0-9_]+-\d+)\b/g)) keys.push(m[1]);
|
|
1456
1484
|
}
|
|
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
1485
|
return [...new Set(keys)];
|
|
1470
1486
|
}
|
|
1471
1487
|
|
|
@@ -1483,7 +1499,7 @@ function jiraKeysOf(root, L, id, fm, found) {
|
|
|
1483
1499
|
* handoff — `handed_off:` names where it went; n/a for decision records, reports and chores
|
|
1484
1500
|
* When the record names no file for brief, draft or review, or names one that does not exist, a
|
|
1485
1501
|
* document the repository scan found for the id completes the step (scan-documents S3); Jira keys
|
|
1486
|
-
* in the
|
|
1502
|
+
* 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
1503
|
* Track: `light` (guidance — only route and anchor can fail CI) or `governed` (brief and draft
|
|
1488
1504
|
* existence and their order fail CI for a non-legacy record). Review, approval and handoff are never
|
|
1489
1505
|
* CI-blocking in either track: none can be true before the PR exists.
|
|
@@ -1564,7 +1580,7 @@ function pmLadder({ id, fm, lane, effLane, knownLane, legacy, steps, root, L, re
|
|
|
1564
1580
|
if (!flows('handoff')) steps.push(step('handoff', 'handed off', 'na', `${effLane} — nothing to hand off`));
|
|
1565
1581
|
else {
|
|
1566
1582
|
const to = String(fm.handed_off || '').trim();
|
|
1567
|
-
const keys = to ? [] : jiraKeysOf(
|
|
1583
|
+
const keys = to ? [] : jiraKeysOf(L, id, fm);
|
|
1568
1584
|
steps.push(to ? step('handoff', 'handed off', 'done', `handed off — ${to}`)
|
|
1569
1585
|
: 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
1586
|
: 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:50:05Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -172,11 +172,11 @@ const vw = () => { const v = document.querySelector('#czViewer:not([hidden])');
|
|
|
172
172
|
await p.click(`.hm-r[data-id="n:${k4}"] .hm-rm`);
|
|
173
173
|
await until(() => p.evaluate(() => !!document.querySelector('#czViewer:not([hidden]) [data-czs-act="start"]')));
|
|
174
174
|
const s0 = await p.evaluate(() => { const v = document.querySelector('#czViewer'); return { line: (v.querySelector('.czs-line') || {}).textContent || '', offer: (v.querySelector('.nv-offer') || {}).textContent || '', cost: (v.querySelector('.nv-cost') || {}).textContent || '', pri: (v.querySelector('.czs-pri') || {}).textContent || '', own: (v.querySelector('.nv-own') || {}).textContent || '' }; });
|
|
175
|
-
ok('W8 a blocked row says what is missing, offers ✦ what Claude drafts, says what waiting costs, and holds ONE verb', s0.line === 'Blocked: the spec is missing' && /^I can
|
|
175
|
+
ok('W8 a blocked row says what is missing, offers ✦ what Claude drafts (batch 25 item 6: a concrete proposal), says what waiting costs, and holds ONE verb', s0.line === 'Blocked: the spec is missing' && /^I can draft the PRD/.test(s0.offer) && /^If it waits: /.test(s0.cost) && s0.pri === 'Draft the PRD' && /Yours to unblock · New feature/.test(s0.own), JSON.stringify(s0));
|
|
176
176
|
await p.click('[data-czs-act="start"]');
|
|
177
177
|
const sp = await until(() => { const l = A.posts.filter((x) => x.path === '/api/agent/start'); return l.length ? l : null; });
|
|
178
178
|
ok('W8 Start posts one agent start for that row', !!sp && sp.length === 1, JSON.stringify(A.posts.map((x) => [x.path, x.body])));
|
|
179
|
-
const s1 = await until(() => p.evaluate(() => { const v = document.querySelector('#czViewer:not([hidden])'); const l = v && v.querySelector('.czs-line'); return l && /^
|
|
179
|
+
const s1 = await until(() => p.evaluate(() => { const v = document.querySelector('#czViewer:not([hidden])'); const l = v && v.querySelector('.czs-line'); return l && /^Drafting the PRD/.test(l.textContent) ? { stop: !!v.querySelector('[data-czs-act="stop"][aria-label="Stop the agent"]'), chat: !!v.querySelector('[data-czs-act="talk"]'), pri: !!v.querySelector('.czs-pri') } : null; }));
|
|
180
180
|
ok('W8 after Start the same card says what the agent does now, with Stop and its chat and no other verb (OvA1After)', !!s1 && s1.stop && s1.chat && !s1.pri, JSON.stringify(s1));
|
|
181
181
|
const mint = await T.nonce(BASE);
|
|
182
182
|
const writes = A.posts.filter((x) => /briefing-premium\/(decide|put-off)|\/api\/agent\/start/.test(x.path));
|
package/workspace/changelog.json
CHANGED
|
@@ -1,4 +1,54 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"v": "0.498.0",
|
|
4
|
+
"at": "2026-11-03T02:50:09.805Z",
|
|
5
|
+
"f": 3,
|
|
6
|
+
"prs": [
|
|
7
|
+
{
|
|
8
|
+
"n": 1176,
|
|
9
|
+
"t": "Commit as the repo's own git identity; ask the PM once when it has none",
|
|
10
|
+
"s": "Commit as the repo's own git identity; ask the PM once when it has none",
|
|
11
|
+
"k": "fix"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"n": 1178,
|
|
15
|
+
"t": "Full task names wrap; add-task chips for stage, type, dates, owner",
|
|
16
|
+
"s": "Full task names wrap; add-task chips for stage, type, dates, owner",
|
|
17
|
+
"k": "new"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"n": 1179,
|
|
21
|
+
"t": "Each product repo pushes with its own GitHub login",
|
|
22
|
+
"s": "Each product repo pushes with its own GitHub login",
|
|
23
|
+
"k": "new"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"n": 1177,
|
|
27
|
+
"t": "A workflow routes the initiative; blocked steps ask only as ✦ proposals",
|
|
28
|
+
"s": "A workflow routes the initiative; blocked steps ask only as ✦ proposals",
|
|
29
|
+
"k": "fix"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"v": "0.497.0",
|
|
35
|
+
"at": "2026-11-03T02:04:11.405Z",
|
|
36
|
+
"f": 3,
|
|
37
|
+
"prs": [
|
|
38
|
+
{
|
|
39
|
+
"n": 1174,
|
|
40
|
+
"t": "Count only a real Jira handoff record, not keys in body text",
|
|
41
|
+
"s": "Count only a real Jira handoff record, not keys in body text",
|
|
42
|
+
"k": "fix"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"n": 1175,
|
|
46
|
+
"t": "The Viewer shows what a chat turn changed, with no refresh",
|
|
47
|
+
"s": "The Viewer shows what a chat turn changed, with no refresh",
|
|
48
|
+
"k": "fix"
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
},
|
|
2
52
|
{
|
|
3
53
|
"v": "0.496.0",
|
|
4
54
|
"at": "2026-11-03T00:20:24.723Z",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "The premium Briefing (/premium.html)"
|
|
4
4
|
description: "The Briefing page at /premium.html: the hero sentence, What you need to do, Today, the dock, and what each one reads from the repository."
|
|
5
|
-
timestamp: 2026-11-
|
|
5
|
+
timestamp: 2026-11-03
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# The premium Briefing
|
|
@@ -133,7 +133,9 @@ The list is the console's **Needs you**: the same rows, in the same order, from
|
|
|
133
133
|
|
|
134
134
|
Each row is a thing that waits for you. The order is fixed: blocked work first, then decisions only you can make, then what has waited longest. The first row is tagged "Do this first".
|
|
135
135
|
|
|
136
|
-
A row has two lines: a verb-first title, then the subject and what the repository says is next, joined by a dot.
|
|
136
|
+
A row has two lines: a verb-first title, then the subject and what the repository says is next, joined by a dot.
|
|
137
|
+
|
|
138
|
+
**Blocked work is Claude's ✦ proposal.** A blocked initiative shows as a row only when Claude can propose a way through. The title names the proposal (for example, **Draft the PRD**, **Anchor it** or **Start a prototype**), and the second line says what Claude will do: "✦ I'll draft the PRD with its done-criteria…". The initiative page and the chat show the same proposal ("Also waiting on you: ✦ Draft the PRD · Review"), not a question. Press the proposal to approve it, and Claude starts an agent session that does it. Two blocks are never a row: routing, because the initiative's workflow routes it, and an order violation in git history, which no draft can fix. When the line is too long it ends in "…"; the full text is in the row's dialog. "If you skip", which says what stays blocked, is in the dialog too. Press a row to open it. Detail opens first, and the conversation sits beside it. On a phone, the Detail tab is first.
|
|
137
139
|
|
|
138
140
|
The header counts "N of M done". A row counts as done when the record behind it changes, so a row clears by itself. Nothing on this page presses Done for you. The count starts again each day and is kept on this device.
|
|
139
141
|
|
|
@@ -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**:
|
package/workspace/guide/home.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Home (/premium.html) and the designed features"
|
|
4
4
|
description: "Home in the Agreed design: the list grouped by product, the Viewer, the place menu, Your day and its New for you fold, Search, and the initiative, meeting, report, Board and Insights features designed after it."
|
|
5
|
-
timestamp: 2026-11-
|
|
5
|
+
timestamp: 2026-11-03
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Home
|
|
@@ -103,8 +103,8 @@ Press an initiative's name in the list, or open `?ini=<id>`. The list stays as i
|
|
|
103
103
|
- **Set a Jira ticket's dates.** Press the dates of a Jira ticket in the List, or drag its bar on the Timeline, as you do for a task. PMOS keeps the start and due days and shows them in place of the sprint end. Jira does not change. The status and the name stay Jira's.
|
|
104
104
|
- **Tasks** show one row (one group) for each stage of the workflow, in order. Finished stages and later stages are never merged into one row; they start folded, and the current stage and any stage with a late task start open.
|
|
105
105
|
- **Change a task where it is.** In the List, press a task's name. The row opens in place: change the name (Enter saves), the status, and the dates. Only one row is open at a time. Esc closes it. The ⋯ after the dates has **Milestone** (or **Make it a plain task**), **Move to a stage**, **Open the task card** and **Delete task**. A milestone has one date: its due day. Each change shows **Undo**.
|
|
106
|
-
- In the List, a
|
|
107
|
-
- **Add a task to a stage.** Press **+** on an open stage. The field opens at the top of that stage. Enter adds the task and keeps the field open for the next one.
|
|
106
|
+
- In the List, a task's full name always shows: a long name wraps onto more lines. A short prefix such as "Bus shelter:" stays in the name, in grey. The dates show at the right. In a narrow panel or on a phone, the dates show under the name. A task with no dates shows no date; open the row to set them.
|
|
107
|
+
- **Add a task to a stage.** Press **+** on an open stage. The field opens at the top of that stage, with one row of chips under it. **Stage** is the stage you pressed + on; press it to pick another stage of the workflow. **Task** can change to **Milestone**: the box becomes ◇ and **Date** takes one day. **Dates** and **Owner** are optional; with no owner, the task is yours. Enter adds the task and keeps the field open for the next one. Esc closes the field.
|
|
108
108
|
- **Move a task between stages.** On a computer, drag the grip at the left of a row. On a phone, press and hold the task's name, then drag. Drop it on a stage name to put it last in that stage, or between two rows to put it there. The status and the dates do not change. The message shows **Undo**.
|
|
109
109
|
- A linked Jira ticket opens in place too. Its name and status stay Jira's ("In Progress in Jira · read 4 min ago"). Its dates can change, and it can be a milestone. It has no **Delete**.
|
|
110
110
|
- **Delete a task.** Press the caret under a task's box (on a phone, press and hold the box) and press the trash at the end of the status picker. The task goes at once and the message shows **Undo**. In the Task Viewer, the trash is the last icon of the Status row, and **Delete** is the last entry of its ⋯ menu. You can delete every PMOS task, also a task that Claude planned in the breakdown and a task that a stage's To finish list made. A deleted To finish task does not come back when the stage is read again or when the initiative moves back to that stage. A Jira ticket has no Delete: change it in Jira. A task deleted in a draft leaves only the draft.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Start the workspace without a terminal"
|
|
4
|
-
timestamp: 2026-
|
|
4
|
+
timestamp: 2026-11-03
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Start the workspace without a terminal
|
|
@@ -31,3 +31,9 @@ Type `pmos launcher off`. This removes only launcher files that PMOS owns. It le
|
|
|
31
31
|
## Limits
|
|
32
32
|
|
|
33
33
|
The login item starts the server once at login. Open the app if the server stops. Login does not enable remote access. Mac and Linux launcher output goes to `launcher.log` in the runtime state folder. Windows launchers run without a command window and use the server's normal logs.
|
|
34
|
+
|
|
35
|
+
## Choose which git identity and GitHub login a repository uses
|
|
36
|
+
|
|
37
|
+
Some computers have no global git name and email, so each repository keeps its own. A computer can also have more than one GitHub login, and a private repository of one login is not found by the other. PMOS does not choose for you, because you can use a different identity and login for each product. When a repository has none, Claude asks you once and offers the ones from your other repositories.
|
|
38
|
+
|
|
39
|
+
Type `pmos git-identity` in the repository to see the identity it commits as and the GitHub login it pushes with. When one is missing, the result lists the ones your other repositories use. To save your choice for this repository only, type `pmos git-identity set --name "Your Name" --email you@example.com --login your-github-login`. To save only the login, type `pmos git-identity set --login your-github-login`. After that, `git push` in the repository uses that login, whichever login `gh` has active. Your global git settings do not change.
|
|
@@ -54,8 +54,32 @@ const STEP = {
|
|
|
54
54
|
spec: { title: 'Put the spec ahead of the build', skip: 'the build stays ahead of the spec, so the order check keeps failing.' },
|
|
55
55
|
contract: { title: 'Agree the contract', skip: 'the contract stays unagreed, so the run has no done-criteria to check.' },
|
|
56
56
|
anchor: { title: 'Anchor the initiative', skip: 'the initiative stays unanchored, and unanchored work is rejected.' },
|
|
57
|
-
route: { title: 'Route the initiative', skip: 'the initiative has no
|
|
57
|
+
route: { title: 'Route the initiative', skip: 'the initiative has no workflow, so its stages cannot run.' },
|
|
58
58
|
};
|
|
59
|
+
|
|
60
|
+
/* What Claude proposes for a blocked step (batch 25 item 6). A blocked step reaches the PM only as a ✦ proposal: what Claude will do, in
|
|
61
|
+
* one sentence, that the PM approves (the Start write opens the agent session that does it). A step Claude cannot move by itself is not
|
|
62
|
+
* an ask: route (the initiative's workflow routes it, scripts/workflow-state.js), and an order violation (git history; no draft fixes it).
|
|
63
|
+
* `what` is the agent's task and starts with the verb the press names (pm/home-need.js plainStep makes the label from it).
|
|
64
|
+
* → { label, say, what } | null */
|
|
65
|
+
const PROPOSE = {
|
|
66
|
+
route: () => null,
|
|
67
|
+
anchor: (st) => (/only the PM adds/.test(st.next || '')
|
|
68
|
+
? { label: 'Anchor it again', say: 'I’ll anchor it again to a goal or health budget you already have, and say which and why.', what: 'anchor it again to a KR or health budget the OKR documents already declare, set parent_kr in the record, and say which and why' }
|
|
69
|
+
: { label: 'Anchor it', say: 'I’ll pick the goal it moves from your goals and add it as its anchor, and say which and why.', what: 'anchor it to the KR or health budget it moves, from the OKR documents: set parent_kr in the record and say which and why' }),
|
|
70
|
+
prototype: (st) => (/cannot follow it/.test(st.next || '') ? null
|
|
71
|
+
: /prototype lane first/.test(st.next || '')
|
|
72
|
+
? { label: 'Start a prototype', say: 'I’ll start a prototype: a quick brief and two or three specimens you can look at.', what: 'start a prototype with a quick brief and two or three divergent specimens (skills/core/prototype.skill), then set predecessor in the record' }
|
|
73
|
+
: { label: 'Link its prototype', say: 'I’ll link it to its prototype, or start one if there is none.', what: 'link its prototype by setting predecessor in the record to the prototype initiative, or start a prototype if there is none' }),
|
|
74
|
+
spec: (st) => (/^O[12]$/.test(st.code || st.rule || '') || /earlier commit than the build/.test(st.next || '') ? null
|
|
75
|
+
: /intent/.test(st.next || '')
|
|
76
|
+
? { label: 'Draft the intent', say: 'I’ll write the one-paragraph intent from what the initiative already says.', what: 'draft the one-paragraph intent from what the initiative already says and put it in the record\'s status' }
|
|
77
|
+
: /rubric/.test(st.next || '')
|
|
78
|
+
? { label: 'Draft the rubric', say: 'I’ll draft the done-criteria rubric from the PRD, for you to review.', what: 'draft the rubric from the PRD (skills/core/eval-rubric.skill), then set rubric_path' }
|
|
79
|
+
: { label: 'Draft the PRD', say: 'I’ll draft the PRD with its done-criteria from what the initiative already says, for you to review.', what: 'draft the PRD with its done-criteria (skills/core/prd.skill) from what the initiative already says, then set prd_path' }),
|
|
80
|
+
contract: () => ({ label: 'Draft the contract', say: 'I’ll draft the done-criteria and how we test them, for you to approve.', what: 'draft the contract with its done-criteria and test method, for the PM to approve, and record it in the runs ledger' }),
|
|
81
|
+
};
|
|
82
|
+
const proposalOf = (st) => { try { return PROPOSE[st.key] ? PROPOSE[st.key](st) : null; } catch { return null; } };
|
|
59
83
|
const stepOf = (key) => STEP[key] || { title: 'Unblock the ' + key + ' step', skip: 'the ' + key + ' step stays blocked.' };
|
|
60
84
|
|
|
61
85
|
/* The options a decision offers. They name the choices the repository's own rules give (D41 for a tripwire, D60 for a prototype); none is marked
|
|
@@ -99,26 +123,28 @@ function blockedRows(f) {
|
|
|
99
123
|
for (const i of f.initiatives) {
|
|
100
124
|
try {
|
|
101
125
|
if (!live(i)) continue;
|
|
102
|
-
|
|
126
|
+
// the first blocked step Claude can propose a way through, the blocking ones first; a step with no proposal is not an ask
|
|
127
|
+
const blocked = (i.steps || []).filter((s) => s.status === 'blocked');
|
|
128
|
+
const st = [...blocked.filter((s) => s.blocking !== false), ...blocked.filter((s) => s.blocking === false)].find((s) => proposalOf(s));
|
|
103
129
|
if (!st) continue;
|
|
104
|
-
const m = stepOf(st.key);
|
|
130
|
+
const m = stepOf(st.key), pr = proposalOf(st);
|
|
105
131
|
out.push({
|
|
106
|
-
kind: 'blocked', key: 'blocked:' + i.id, title:
|
|
107
|
-
prepared:
|
|
132
|
+
kind: 'blocked', key: 'blocked:' + i.id, title: pr.label, subject: clip(i.title, 90),
|
|
133
|
+
prepared: '✦ ' + pr.say, propose: { label: pr.label, say: pr.say },
|
|
108
134
|
skip: 'If you skip: ' + m.skip,
|
|
109
135
|
from: [i.id], stepKey: st.key, order: [-(i.blockers || []).length, i.id],
|
|
110
|
-
detail: { id: i.id, step: st.key, rule: st.rule || null, next:
|
|
136
|
+
detail: { id: i.id, step: st.key, rule: st.rule || null, next: pr.what, record: i.record || null, ...whoOf(i) },
|
|
111
137
|
dialog: tryD(() => ({
|
|
112
138
|
eye: 'Unblock', logTitle: 'Read from the repository',
|
|
113
139
|
log: ['Read the initiative record' + (i.record ? ' (' + i.record + ')' : ''), 'Ran the workflow order rules on it', 'Found the step that blocks it: ' + st.key],
|
|
114
140
|
say: i.id + ' is blocked at the ' + st.key + ' step.' + (st.rule ? ' The rule it does not meet: ' + clip(st.rule, 200) : ''),
|
|
115
141
|
hold: START_NOTE, head: 'The way through',
|
|
116
142
|
facts: [['Blocked at', st.key], ['Initiative', i.id], ['The check', st.rule && clip(st.rule, 200)], ['Record', i.record]].filter((x) => x[1]),
|
|
117
|
-
opts:
|
|
118
|
-
sel:
|
|
119
|
-
nodef:
|
|
120
|
-
write:
|
|
121
|
-
prim: { next: '
|
|
143
|
+
opts: [{ k: 'next', t: pr.label, d: '✦ ' + pr.say }],
|
|
144
|
+
sel: 'next',
|
|
145
|
+
nodef: '',
|
|
146
|
+
write: { kind: 'start', initiative: i.id, what: clip(pr.what, 200), ...(i.ws ? { ws: i.ws } : {}) },
|
|
147
|
+
prim: { next: '✦ ' + pr.label },
|
|
122
148
|
pane: { label: 'Initiative record', file: i.record || i.id, h: clip(i.title, 90), rows: fmRows(i.fm) },
|
|
123
149
|
})),
|
|
124
150
|
});
|
|
@@ -528,7 +554,7 @@ function goalOf(A, raw) {
|
|
|
528
554
|
async function readInitiatives({ root, engine }) {
|
|
529
555
|
const wf = engine ? await run(process.execPath, [engine, '--json', '--root', root], { cwd: root, nice: 10 }) : null;
|
|
530
556
|
let list;
|
|
531
|
-
try { list = JSON.parse(wf).map((x) => ({ id: x.id, title: x.title, lane: x.lane, stage: x.stage, accepted: !!x.accepted, stopped: !!x.stopped, legacy: !!x.legacy, anchor: x.anchor, last_activity: x.last_activity, blockers: x.blockers || [], next: x.next, record: x.record, steps: (x.steps || []).map((s) => ({ key: s.key, status: s.status, next: s.next, rule: (s.basis && s.basis.rule) || s.rule, blocking: s.blocking })) })); } catch { return null; }
|
|
557
|
+
try { list = JSON.parse(wf).map((x) => ({ id: x.id, title: x.title, lane: x.lane, stage: x.stage, accepted: !!x.accepted, stopped: !!x.stopped, legacy: !!x.legacy, anchor: x.anchor, last_activity: x.last_activity, blockers: x.blockers || [], next: x.next, record: x.record, steps: (x.steps || []).map((s) => ({ key: s.key, status: s.status, next: s.next, rule: (s.basis && s.basis.rule) || s.rule, code: s.rule || null, blocking: s.blocking })) })); } catch { return null; }
|
|
532
558
|
let A = null;
|
|
533
559
|
try { A = require('./initiative-rows.js').anchorsOf(root); } catch { A = null; }
|
|
534
560
|
for (const i of list) {
|
|
@@ -556,6 +556,29 @@ function profileOf(root) {
|
|
|
556
556
|
} catch { return 'product-code'; }
|
|
557
557
|
}
|
|
558
558
|
|
|
559
|
+
/*
|
|
560
|
+
* workflowOf(root, id) — the workflow an initiative runs, when its repository runs initiatives on workflows (Boards, Settings ›
|
|
561
|
+
* Workflows). The workspace gives every repository it keeps state for a default board, so every initiative there runs a workflow:
|
|
562
|
+
* an `ini-wf` line in the boards ledger (<state dir>/boards.jsonl, the dirs workspace/server.js stateDirFor reads) names its
|
|
563
|
+
* own, else it runs its board's (Product feature by default). A repository runs on workflows when it holds that ledger or a
|
|
564
|
+
* pmos/state/ dir. PMOS itself holds neither (its state is the workspace's own), so it still routes by lane and R0 still binds it.
|
|
565
|
+
* → the workflow's id, '' (its board's), or null (no workflows). The ledger is read again when its size or mtime changes.
|
|
566
|
+
*/
|
|
567
|
+
const wfMemo = new Map();
|
|
568
|
+
function workflowOf(root, id) {
|
|
569
|
+
const f = [['pmos', 'state'], ['state'], ['.pmos']].map((rel) => path.join(root, ...rel, 'boards.jsonl')).find((x) => fs.existsSync(x));
|
|
570
|
+
const on = !!f || fs.existsSync(path.join(root, 'pmos', 'state'));
|
|
571
|
+
if (!on) return null;
|
|
572
|
+
let key = '';
|
|
573
|
+
try { const st = f ? fs.statSync(f) : null; key = st ? st.size + ':' + st.mtimeMs : '-'; } catch { key = '?'; }
|
|
574
|
+
let m = wfMemo.get(root);
|
|
575
|
+
if (!m || m.key !== key) { // a held module sees a new line on the next read (the in-process freshness rule)
|
|
576
|
+
m = { key, wf: new Map() };
|
|
577
|
+
for (const l of f ? jsonl.readJsonl(f).rows : []) if (l && l.kind === 'ini-wf' && typeof l.id === 'string' && typeof l.wf === 'string' && l.wf) m.wf.set(l.id, l.wf);
|
|
578
|
+
wfMemo.set(root, m);
|
|
579
|
+
}
|
|
580
|
+
return m.wf.get(id) || '';
|
|
581
|
+
}
|
|
559
582
|
/** The lanes a repository's initiatives may declare — what the lane picker offers and the lane write accepts. */
|
|
560
583
|
function laneNames(profile) { return laneDefs.laneNames(profile === 'pm-artifacts' ? 'pm-artifacts' : 'product-code'); }
|
|
561
584
|
function lanesFor(root) {
|
|
@@ -735,6 +758,7 @@ function computeWorkflowDerived(id, opts) {
|
|
|
735
758
|
// ── route ─ the lane is chosen and the two intake questions are answered ─────────────────────
|
|
736
759
|
const sRoute = specimenRoute(fm.specimen_test);
|
|
737
760
|
const fRoute = surfaceRoute(fm.surface_check);
|
|
761
|
+
const onWf = workflowOf(root, id);
|
|
738
762
|
{
|
|
739
763
|
const missing = [];
|
|
740
764
|
if (sRoute === 'undeclared' || sRoute === 'malformed') missing.push('specimen_test');
|
|
@@ -742,6 +766,10 @@ function computeWorkflowDerived(id, opts) {
|
|
|
742
766
|
if (!missing.length) {
|
|
743
767
|
steps.push(step('route', 'route', 'done',
|
|
744
768
|
`${lane} lane · specimen test ${sRoute} · surface ${fRoute}`));
|
|
769
|
+
} else if (onWf !== null) {
|
|
770
|
+
// Batch 25 item 6: an initiative on a workflow is routed. The workflow's stages say what comes first, and PMOS proposes a
|
|
771
|
+
// switch when the BRD says another workflow fits, so the two intake questions are not asked of it again.
|
|
772
|
+
steps.push(step('route', 'route', 'done', `${onWf ? `runs the ${onWf} workflow` : 'runs its board\'s workflow'} — the workflow routes it`));
|
|
745
773
|
} else if (effLane !== 'feature') {
|
|
746
774
|
// The routing questions are asked of feature work only.
|
|
747
775
|
steps.push(step('route', 'route', 'done', `${lane} lane — routing questions are for features only (${CUT_NOTE})`));
|
|
@@ -1438,11 +1466,11 @@ function documentDuplicates(root, opts) {
|
|
|
1438
1466
|
return { ok: true, profile, archive, groups };
|
|
1439
1467
|
}
|
|
1440
1468
|
/**
|
|
1441
|
-
* Jira keys
|
|
1442
|
-
* record's
|
|
1443
|
-
*
|
|
1469
|
+
* Jira keys a handoff record holds for an initiative (S4): the Jira tab's draft (`- jira: KEY` lines) and the
|
|
1470
|
+
* record's `jira:` / `jira_key:` / `jira_keys:` / `jira_epic:` / `jira_issue_key:` frontmatter. Body text and found documents are not
|
|
1471
|
+
* 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
1472
|
*/
|
|
1445
|
-
function jiraKeysOf(
|
|
1473
|
+
function jiraKeysOf(L, id, fm) {
|
|
1446
1474
|
const keys = [];
|
|
1447
1475
|
const draft = path.join(path.dirname(L.initiatives), 'tracker', 'jira', `${id}-tickets.md`);
|
|
1448
1476
|
try {
|
|
@@ -1451,21 +1479,9 @@ function jiraKeysOf(root, L, id, fm, found) {
|
|
|
1451
1479
|
if (m && JIRA_KEY.test(m[1].toUpperCase())) keys.push(m[1].toUpperCase());
|
|
1452
1480
|
}
|
|
1453
1481
|
} catch { /* no draft */ }
|
|
1454
|
-
for (const k of ['jira', 'jira_key', 'jira_keys', 'jira_epic']) {
|
|
1482
|
+
for (const k of ['jira', 'jira_key', 'jira_keys', 'jira_epic', 'jira_issue_key']) {
|
|
1455
1483
|
for (const m of String(fm[k] || '').toUpperCase().matchAll(/\b([A-Z][A-Z0-9_]+-\d+)\b/g)) keys.push(m[1]);
|
|
1456
1484
|
}
|
|
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
1485
|
return [...new Set(keys)];
|
|
1470
1486
|
}
|
|
1471
1487
|
|
|
@@ -1483,7 +1499,7 @@ function jiraKeysOf(root, L, id, fm, found) {
|
|
|
1483
1499
|
* handoff — `handed_off:` names where it went; n/a for decision records, reports and chores
|
|
1484
1500
|
* When the record names no file for brief, draft or review, or names one that does not exist, a
|
|
1485
1501
|
* document the repository scan found for the id completes the step (scan-documents S3); Jira keys
|
|
1486
|
-
* in the
|
|
1502
|
+
* 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
1503
|
* Track: `light` (guidance — only route and anchor can fail CI) or `governed` (brief and draft
|
|
1488
1504
|
* existence and their order fail CI for a non-legacy record). Review, approval and handoff are never
|
|
1489
1505
|
* CI-blocking in either track: none can be true before the PR exists.
|
|
@@ -1564,7 +1580,7 @@ function pmLadder({ id, fm, lane, effLane, knownLane, legacy, steps, root, L, re
|
|
|
1564
1580
|
if (!flows('handoff')) steps.push(step('handoff', 'handed off', 'na', `${effLane} — nothing to hand off`));
|
|
1565
1581
|
else {
|
|
1566
1582
|
const to = String(fm.handed_off || '').trim();
|
|
1567
|
-
const keys = to ? [] : jiraKeysOf(
|
|
1583
|
+
const keys = to ? [] : jiraKeysOf(L, id, fm);
|
|
1568
1584
|
steps.push(to ? step('handoff', 'handed off', 'done', `handed off — ${to}`)
|
|
1569
1585
|
: 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
1586
|
: foundOf('handoff') ? step('handoff', 'handed off', 'done', `${foundOf('handoff')} — handoff found by the repository scan`)
|