create-pmos 0.37.0 → 0.38.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/bin/create-pmos.js +22 -3
- package/package.json +1 -1
- package/payload/AGENTS.md +4 -0
- package/payload/KIT-VERSION +2 -2
- package/payload/okf/core/concepts/frontmatter-format.md +27 -61
- package/payload/okf/core/templates/decision-record-template.md +34 -0
- package/payload/okf/core/templates/index.md +7 -2
- package/payload/okf/core/templates/pm-brief-template.md +34 -0
- package/payload/okf/core/templates/project-plan-template.md +43 -0
- package/payload/okf/core/templates/report-weekly-template.md +35 -0
- package/payload/okf/core/templates/strategy-long-range-template.md +37 -0
- package/payload/okf/core/templates/strategy-one-pager-template.md +34 -0
- package/payload/planning/evals/eval-calibration.md +8 -0
- package/payload/planning/okrs/health-budgets.md +8 -0
- package/payload/scripts/anchor-resolve.js +56 -6
- package/payload/scripts/gate-initiative.sh +32 -0
- package/payload/scripts/okf-upgrade.js +232 -0
- package/payload/scripts/pmos-memory.js +375 -0
- package/payload/scripts/pmos-model.js +239 -0
- package/payload/scripts/pmos-skills.js +274 -0
- package/payload/scripts/workflow-state.js +178 -10
- package/payload/scripts/workflow-sweep.js +2 -1
- package/payload/state/discoveries.md +8 -0
- package/payload/state/index.md +7 -0
- package/payload/state/log.md +7 -0
- package/workspace/README.md +61 -204
- package/workspace/lib/board-cache.js +18 -8
- package/workspace/lib/console-state.js +2 -0
- package/workspace/lib/engine/anchor-resolve.js +56 -6
- package/workspace/lib/engine/pmos-memory.js +375 -0
- package/workspace/lib/engine/pmos-model.js +239 -0
- package/workspace/lib/engine/pmos-skills.js +274 -0
- package/workspace/lib/engine/workflow-state.js +178 -10
- package/workspace/lib/focus.js +5 -6
- package/workspace/lib/memory.js +54 -0
- package/workspace/lib/models.js +50 -0
- package/workspace/lib/preview-kind.js +12 -1
- package/workspace/lib/schedule-logic.js +31 -2
- package/workspace/lib/skill-store.js +5 -1
- package/workspace/lib/skills-registry.js +74 -0
- package/workspace/lib/sync.js +132 -0
- package/workspace/lib/tree-share.js +2 -0
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +3 -3
- package/workspace/public/pm/app.js +32 -2
- package/workspace/public/pm/data.js +160 -10
- package/workspace/public/pm/lifecycle.js +105 -0
- package/workspace/public/pm/markdown.js +10 -1
- package/workspace/public/pm/modes-live.js +371 -0
- package/workspace/public/pm/modes.js +7 -4
- package/workspace/public/pm/panel-detail.js +22 -2
- package/workspace/public/pm/panel-initiatives.js +57 -12
- package/workspace/public/pm/panel-newsess.js +25 -0
- package/workspace/public/pm/panel-repos.js +130 -4
- package/workspace/public/pm/panel-tabs.js +49 -2
- package/workspace/public/pm/state.js +11 -1
- package/workspace/public/pm/tab-doc.js +160 -26
- package/workspace/public/pm/tab-term.js +2 -0
- package/workspace/public/pm/tab-web.js +89 -0
- package/workspace/public/pm/tokens.css +4 -0
- package/workspace/public/pm/xlsx-read.js +117 -0
- package/workspace/server.js +690 -40
- package/workspace/skills/gather-requirements.skill +53 -0
- package/workspace/skills/memory-groom.skill +46 -0
- package/workspace/skills/report-biweekly.skill +53 -0
- package/workspace/skills/report-daily.skill +53 -0
- package/workspace/skills/report-monthly.skill +53 -0
- package/workspace/skills/report-special.skill +49 -0
- package/workspace/skills/report-weekly.skill +53 -0
- package/workspace/skills/skill-discover.skill +49 -0
- package/workspace/skills/skill-generate.skill +33 -0
|
@@ -94,7 +94,8 @@ function budgetsFor(root, L) {
|
|
|
94
94
|
catch { value = { ok: false, text: '', path: path.relative(root, L.budgets) }; }
|
|
95
95
|
_budCache.set(root, { stamp, value });
|
|
96
96
|
}
|
|
97
|
-
|
|
97
|
+
// D67: the domain registry rides on the budgets object, read fresh (a small, PM-edited file)
|
|
98
|
+
return anchors.withDomains(_budCache.get(root).value, path.dirname(L.budgets), root);
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
/*
|
|
@@ -527,6 +528,21 @@ function surfaceRoute(v) {
|
|
|
527
528
|
return 'malformed';
|
|
528
529
|
}
|
|
529
530
|
|
|
531
|
+
// ── Repository profile (D68) ──────────────────────────────────────────────────────────────────
|
|
532
|
+
// A repository declares `pm-artifacts` (a PM's documents, strategy and specifications — no code) or
|
|
533
|
+
// `product-code` (PMOS-self and code repositories) in a one-line `.pmos-profile` file at its root.
|
|
534
|
+
// No file, or anything else in it, is `product-code`: every existing repository behaves as before.
|
|
535
|
+
const PM_LANES = ['strategy', 'prd', 'project-plan', 'discovery', 'decision-record', 'report', 'chore'];
|
|
536
|
+
const PM_LIGHT_BY_DEFAULT = ['decision-record', 'report', 'chore'];
|
|
537
|
+
const PM_NO_HANDOFF = ['decision-record', 'report', 'chore'];
|
|
538
|
+
function profileOf(root) {
|
|
539
|
+
try {
|
|
540
|
+
const line = fs.readFileSync(path.join(root, '.pmos-profile'), 'utf8')
|
|
541
|
+
.split('\n').map((l) => l.replace(/#.*$/, '').trim()).find(Boolean);
|
|
542
|
+
return line === 'pm-artifacts' ? 'pm-artifacts' : 'product-code';
|
|
543
|
+
} catch { return 'product-code'; }
|
|
544
|
+
}
|
|
545
|
+
|
|
530
546
|
// ── Step model ─────────────────────────────────────────────────────────────────────────────────
|
|
531
547
|
// status ∈ done | current | blocked | pending | na | unknown
|
|
532
548
|
// `blocking` marks a step whose failure fails CI. O5/O6/O7 are deliberately NOT blocking: none can
|
|
@@ -539,7 +555,12 @@ function step(key, label, status, evidence, next, opts) {
|
|
|
539
555
|
};
|
|
540
556
|
}
|
|
541
557
|
|
|
542
|
-
|
|
558
|
+
// D74 — `generated: {by: x, at: "2026-10-02T01:00:00Z"}` on one line (the only form PMOS writes). Returns the `at`, or ''.
|
|
559
|
+
function generatedAt(v) {
|
|
560
|
+
const m = /\bat:\s*["']?([^"',}\s]+)/.exec(String(v || ''));
|
|
561
|
+
return m ? m[1] : '';
|
|
562
|
+
}
|
|
563
|
+
function computeWorkflowDerived(id, opts) {
|
|
543
564
|
const root = path.resolve((opts && opts.root) || process.cwd());
|
|
544
565
|
const L = layout(root);
|
|
545
566
|
const recRel = path.relative(root, path.join(L.initiatives, `${id}.md`));
|
|
@@ -548,9 +569,13 @@ function computeWorkflow(id, opts) {
|
|
|
548
569
|
return { ok: false, id, error: `no initiative record at ${recRel} — an initiative must exist before its work` };
|
|
549
570
|
}
|
|
550
571
|
const fm = readFrontmatter(recAbs) || {};
|
|
551
|
-
const
|
|
552
|
-
const
|
|
553
|
-
const
|
|
572
|
+
const profile = profileOf(root);
|
|
573
|
+
const pmProfile = profile === 'pm-artifacts';
|
|
574
|
+
const lane = (fm.lane || (pmProfile ? 'prd' : 'feature')).trim() || (pmProfile ? 'prd' : 'feature');
|
|
575
|
+
const knownLane = pmProfile ? PM_LANES.includes(lane)
|
|
576
|
+
: ['feature', 'discovery', 'chore', 'incident', 'content', 'prototype'].includes(lane);
|
|
577
|
+
// unknown lane = the profile's strictest (feature in product-code, a governed prd in pm-artifacts)
|
|
578
|
+
const effLane = knownLane ? lane : (pmProfile ? 'prd' : 'feature');
|
|
554
579
|
const range = (opts && opts.range) || commitRange(root);
|
|
555
580
|
// See ENFORCED_FROM: git decides, the frontmatter only breaks a tie git cannot see.
|
|
556
581
|
//
|
|
@@ -575,7 +600,7 @@ function computeWorkflow(id, opts) {
|
|
|
575
600
|
const legacy = recIdx === -1 ? true
|
|
576
601
|
: /^\d{4}-\d{2}-\d{2}$/.test(recon) ? recon < ENFORCED_FROM
|
|
577
602
|
: recAt ? new Date(recAt * 1000).toISOString().slice(0, 10) < ENFORCED_FROM
|
|
578
|
-
: String(fm.timestamp || '').slice(0, 10) < ENFORCED_FROM; // fallback — see ENFORCED_FROM
|
|
603
|
+
: String(fm.timestamp || generatedAt(fm.generated) || '').slice(0, 10) < ENFORCED_FROM; // fallback — see ENFORCED_FROM; D74: OKF v0.2 `generated.at` stands in for `timestamp`
|
|
579
604
|
/*
|
|
580
605
|
* `legacy` answers the ENFORCEMENT question — should this PR fail? It is NOT the visibility
|
|
581
606
|
* question. Filtering the board by it was a real bug: on `main`, every record is legacy, so the
|
|
@@ -673,6 +698,18 @@ function computeWorkflow(id, opts) {
|
|
|
673
698
|
`only the PM adds a budget — use an existing id from ${budRel} or anchor to a KR`,
|
|
674
699
|
{ blocking: !legacy, rule: 'R1' }));
|
|
675
700
|
}
|
|
701
|
+
} else if (r.kind === 'domain') {
|
|
702
|
+
// D67 — the domain's standing goal: resolved like a health budget, rejected like one when the
|
|
703
|
+
// id is not in the PM-edited registry.
|
|
704
|
+
const domRel = (budgetsFor(root, L).domains || {}).path || 'planning/okrs/domains.md';
|
|
705
|
+
if (r.status === 'resolved') {
|
|
706
|
+
steps.push(step('anchor', 'anchor', 'done', `domain:${r.domain} — ${r.domain_goal || 'standing goal not written'} (no OKR, D67)`));
|
|
707
|
+
} else {
|
|
708
|
+
steps.push(step('anchor', 'anchor', legacy ? 'unknown' : 'blocked',
|
|
709
|
+
`domain:${r.domain} is not a declared domain`,
|
|
710
|
+
`only the PM adds a domain — use an id from ${domRel}, or anchor to a KR or a health budget`,
|
|
711
|
+
{ blocking: !legacy, rule: 'R1' }));
|
|
712
|
+
}
|
|
676
713
|
} else if (r.status === 'resolved') {
|
|
677
714
|
steps.push(step('anchor', 'anchor', 'done',
|
|
678
715
|
`${anchors.canonical(r, (okrIndexFor(root) || {}).period) || anchor} — ${r.reason}`));
|
|
@@ -691,6 +728,11 @@ function computeWorkflow(id, opts) {
|
|
|
691
728
|
}
|
|
692
729
|
}
|
|
693
730
|
|
|
731
|
+
// ── D68: the pm-artifacts ladder replaces everything after the anchor ─────────────────────────
|
|
732
|
+
if (pmProfile) {
|
|
733
|
+
return pmLadder({ id, fm, lane, effLane, knownLane, legacy, steps, root, L, recRel, range, inChange, lastActivity });
|
|
734
|
+
}
|
|
735
|
+
|
|
694
736
|
const prd = String(fm.prd_path || '').trim();
|
|
695
737
|
const rubric = String(fm.rubric_path || '').trim();
|
|
696
738
|
const prdIdx = prd ? firstAppearance(root, range, prd) : null;
|
|
@@ -1133,7 +1175,7 @@ function computeWorkflow(id, opts) {
|
|
|
1133
1175
|
return {
|
|
1134
1176
|
ok: true, id, lane, laneKnown: knownLane, legacy, in_change: inChange,
|
|
1135
1177
|
last_activity: lastActivity, accepted: accepted.length > 0, stage, record: recRel,
|
|
1136
|
-
title: fm.title || id, anchor: fm.parent_kr || null,
|
|
1178
|
+
title: fm.title || id, anchor: fm.parent_kr || null, profile: 'product-code',
|
|
1137
1179
|
steps, blockers: blockers.map((s) => s.key), gate_failures: gateFailures.map((s) => s.rule || s.key),
|
|
1138
1180
|
next: next ? { step: next.key, why: next.evidence, action: next.next || next.evidence } : null,
|
|
1139
1181
|
range: range.ok ? { base: range.base, commits: range.shas.length, whole_history: !!range.wholeHistory } : { error: range.reason },
|
|
@@ -1141,6 +1183,122 @@ function computeWorkflow(id, opts) {
|
|
|
1141
1183
|
};
|
|
1142
1184
|
}
|
|
1143
1185
|
|
|
1186
|
+
/**
|
|
1187
|
+
* D68 — the pm-artifacts ladder: route → anchor → brief → draft → review → approved → handed off.
|
|
1188
|
+
*
|
|
1189
|
+
* Route and anchor are computed by the shared code above and keep their rules (anchoring holds in
|
|
1190
|
+
* every profile — D59/D67); route is re-stated here because its product-code form asks the
|
|
1191
|
+
* specimen/surface questions, which a PM document does not need. What follows is evidence-derived
|
|
1192
|
+
* like every other rung — nothing is declared done:
|
|
1193
|
+
* brief — the brief file (`brief_path`, or `prd_path`) exists
|
|
1194
|
+
* draft — the deliverable (`doc_path`) exists, and — governed only — did not appear before the brief
|
|
1195
|
+
* review — an evals.jsonl verdict for this id, or a named `reviewed_by:` (a rubric, an agent, a human)
|
|
1196
|
+
* approved — the PM's acceptance in acceptances.jsonl (D12, never automatic)
|
|
1197
|
+
* handoff — `handed_off:` names where it went; n/a for decision records, reports and chores
|
|
1198
|
+
* Track: `light` (guidance — only route and anchor can fail CI) or `governed` (brief and draft
|
|
1199
|
+
* existence and their order fail CI for a non-legacy record). Review, approval and handoff are never
|
|
1200
|
+
* CI-blocking in either track: none can be true before the PR exists.
|
|
1201
|
+
*/
|
|
1202
|
+
function pmLadder({ id, fm, lane, effLane, knownLane, legacy, steps, root, L, recRel, range, inChange, lastActivity }) {
|
|
1203
|
+
const declared = String(fm.track || '').trim().toLowerCase();
|
|
1204
|
+
const track = declared === 'light' || declared === 'governed' ? declared
|
|
1205
|
+
: (PM_LIGHT_BY_DEFAULT.includes(effLane) ? 'light' : 'governed');
|
|
1206
|
+
const enforce = track === 'governed' && !legacy;
|
|
1207
|
+
const exists = (p) => !!p && fs.existsSync(path.join(root, p));
|
|
1208
|
+
|
|
1209
|
+
// route — the lane is a pm-artifacts lane
|
|
1210
|
+
steps[0] = knownLane
|
|
1211
|
+
? step('route', 'route', 'done', `${lane} lane · ${track} track (pm-artifacts profile, D68)`)
|
|
1212
|
+
: step('route', 'route', 'done', `unknown lane '${lane}' — treated as a governed prd, the profile's strictest (D68)`,
|
|
1213
|
+
`set lane: to one of ${PM_LANES.join(', ')}`);
|
|
1214
|
+
|
|
1215
|
+
const briefPath = String(fm.brief_path || fm.prd_path || '').trim();
|
|
1216
|
+
const docPath = String(fm.doc_path || '').trim();
|
|
1217
|
+
const briefIdx = briefPath ? firstAppearance(root, range, briefPath) : null;
|
|
1218
|
+
const docIdx = docPath ? firstAppearance(root, range, docPath) : null;
|
|
1219
|
+
|
|
1220
|
+
if (exists(briefPath)) steps.push(step('brief', 'brief', 'done', `${briefPath} present`));
|
|
1221
|
+
else steps.push(step('brief', 'brief', enforce ? 'blocked' : 'pending',
|
|
1222
|
+
briefPath ? `brief_path '${briefPath}' does not exist` : 'no brief yet',
|
|
1223
|
+
`write a short brief (the question, the reader, the decision it informs) and set brief_path in ${recRel}`,
|
|
1224
|
+
{ blocking: enforce, rule: 'P1' }));
|
|
1225
|
+
|
|
1226
|
+
if (exists(docPath)) {
|
|
1227
|
+
const inOrder = briefIdx == null || docIdx == null ? null : docIdx >= briefIdx;
|
|
1228
|
+
if (enforce && inOrder === false) {
|
|
1229
|
+
steps.push(step('draft', 'draft', 'blocked', `${docPath} appeared before its brief ${briefPath}`,
|
|
1230
|
+
'a governed document is briefed before it is drafted — the brief must land first', { blocking: true, rule: 'P2' }));
|
|
1231
|
+
} else steps.push(step('draft', 'draft', 'done', `${docPath} present`));
|
|
1232
|
+
} else steps.push(step('draft', 'draft', enforce && docPath ? 'blocked' : 'pending',
|
|
1233
|
+
docPath ? `doc_path '${docPath}' does not exist` : 'no draft yet',
|
|
1234
|
+
`write the document and set doc_path in ${recRel}`, { blocking: enforce && !!docPath, rule: 'P2' }));
|
|
1235
|
+
|
|
1236
|
+
const evals = cachedJsonl(path.join(L.runsDir, 'evals.jsonl')).filter((e) => e && e.initiative_id === id);
|
|
1237
|
+
const reviewer = String(fm.reviewed_by || '').trim();
|
|
1238
|
+
if (evals.length) steps.push(step('review', 'review', 'done', `${evals[evals.length - 1].verdict || 'verdict'} recorded in evals.jsonl`));
|
|
1239
|
+
else if (reviewer) steps.push(step('review', 'review', 'done', `reviewed by ${reviewer}`));
|
|
1240
|
+
else steps.push(step('review', 'review', 'pending', 'not reviewed yet',
|
|
1241
|
+
'name the reviewer in reviewed_by: (a person, an agent, or a rubric verdict in evals.jsonl)'));
|
|
1242
|
+
|
|
1243
|
+
const accepted = cachedJsonl(path.join(L.runsDir, 'acceptances.jsonl'))
|
|
1244
|
+
.filter((a) => a && a.initiative_id === id && a.accepted === true);
|
|
1245
|
+
steps.push(accepted.length
|
|
1246
|
+
? step('approved', 'approved', 'done', `accepted by the PM${accepted[accepted.length - 1].accepted_at ? ' on ' + String(accepted[accepted.length - 1].accepted_at).slice(0, 10) : ''}`)
|
|
1247
|
+
: step('approved', 'approved', 'pending', 'awaiting the PM\'s acceptance (D12)', 'the PM accepts the document — never automatic'));
|
|
1248
|
+
|
|
1249
|
+
if (PM_NO_HANDOFF.includes(effLane)) steps.push(step('handoff', 'handed off', 'na', `${effLane} — nothing to hand off`));
|
|
1250
|
+
else {
|
|
1251
|
+
const to = String(fm.handed_off || '').trim();
|
|
1252
|
+
steps.push(to ? step('handoff', 'handed off', 'done', `handed off — ${to}`)
|
|
1253
|
+
: step('handoff', 'handed off', 'pending', 'not handed off yet',
|
|
1254
|
+
'record where it went in handed_off: (a tracker link, the team, or "accepted as the deliverable")'));
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
const by = Object.fromEntries(steps.map((x) => [x.key, x]));
|
|
1258
|
+
let stage = 'intake';
|
|
1259
|
+
if (by.brief.status === 'done') stage = 'brief';
|
|
1260
|
+
if (by.draft.status === 'done') stage = 'draft';
|
|
1261
|
+
if (by.review.status === 'done') stage = 'review';
|
|
1262
|
+
if (accepted.length) stage = 'accepted';
|
|
1263
|
+
const blockers = steps.filter((x) => x.status === 'blocked');
|
|
1264
|
+
const gateFailures = blockers.filter((x) => x.blocking);
|
|
1265
|
+
const next = steps.find((x) => x.status === 'blocked') || steps.find((x) => x.status === 'pending' && x.next) || null;
|
|
1266
|
+
return {
|
|
1267
|
+
ok: true, id, lane, laneKnown: knownLane, legacy, in_change: inChange,
|
|
1268
|
+
last_activity: lastActivity, accepted: accepted.length > 0, stage, record: recRel,
|
|
1269
|
+
title: fm.title || id, anchor: fm.parent_kr || null, profile: 'pm-artifacts', track,
|
|
1270
|
+
steps, blockers: blockers.map((x) => x.key), gate_failures: gateFailures.map((x) => x.rule || x.key),
|
|
1271
|
+
next: next ? { step: next.key, why: next.evidence, action: next.next || next.evidence } : null,
|
|
1272
|
+
range: range.ok ? { base: range.base, commits: range.shas.length, whole_history: !!range.wholeHistory } : { error: range.reason },
|
|
1273
|
+
enforced_from: ENFORCED_FROM,
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// ── Lifecycle (D70) — a stopped initiative is a ledger line, never a record field ───────────────
|
|
1278
|
+
// `lifecycle.jsonl` beside the other ledgers: {initiative_id, state, reason, pm, at}. The latest line
|
|
1279
|
+
// for an id wins. `active` reopens. The same reader exists in workspace/lib/lifecycle.js; a parity
|
|
1280
|
+
// fixture keeps the two in step. A lifecycle line is never an acceptance and never writes one.
|
|
1281
|
+
const STOPPED_STATES = ['closed', 'on-hold', 'cancelled'];
|
|
1282
|
+
function readLifecycle(root) {
|
|
1283
|
+
const L = layout(root);
|
|
1284
|
+
const latest = new Map();
|
|
1285
|
+
for (const r of cachedJsonl(path.join(L.runsDir, 'lifecycle.jsonl'))) {
|
|
1286
|
+
if (!r || typeof r.initiative_id !== 'string') continue;
|
|
1287
|
+
if (r.state !== 'active' && !STOPPED_STATES.includes(r.state)) continue;
|
|
1288
|
+
latest.set(r.initiative_id, { state: r.state, reason: r.reason || '', pm: r.pm || '', at: r.at || '' });
|
|
1289
|
+
}
|
|
1290
|
+
return latest;
|
|
1291
|
+
}
|
|
1292
|
+
function computeWorkflow(id, opts) {
|
|
1293
|
+
const w = computeWorkflowDerived(id, opts);
|
|
1294
|
+
if (!w.ok) return w;
|
|
1295
|
+
const lc = readLifecycle(path.resolve((opts && opts.root) || process.cwd())).get(id);
|
|
1296
|
+
if (!lc || lc.state === 'active') return w;
|
|
1297
|
+
// Stopped: leaves the board and stops gating. The ladder is kept so a reopen shows where it was.
|
|
1298
|
+
return { ...w, stopped: lc, blockers: [], gate_failures: [], next: null,
|
|
1299
|
+
steps: w.steps.map((x) => (x.status === 'blocked' ? { ...x, status: 'pending', blocking: false } : x)) };
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1144
1302
|
// ── Rendering ──────────────────────────────────────────────────────────────────────────────────
|
|
1145
1303
|
const COLOR = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
1146
1304
|
const c = (code, s) => (COLOR ? `\x1b[${code}m${s}\x1b[0m` : s);
|
|
@@ -1192,7 +1350,15 @@ function renderBoard(rows, opts) {
|
|
|
1192
1350
|
out.push(` ${c(1, 'PMOS workflow board')} · ${total} without a recorded acceptance · ${blocked ? c(31, `${blocked} blocked`) : c(32, 'none blocked')}${acceptedN ? c(90, ` · ${acceptedN} accepted`) : ''}`);
|
|
1193
1351
|
out.push(` ${c(90, 'most recently worked on first · ✗ blocked · ● in this change')}`);
|
|
1194
1352
|
out.push(` ${c(90, '─'.repeat(78))}`);
|
|
1195
|
-
|
|
1353
|
+
const stoppedList = () => {
|
|
1354
|
+
const stopped = rows.filter((r) => r.ok && r.stopped);
|
|
1355
|
+
if (stopped.length) {
|
|
1356
|
+
out.push(` ${c(90, `stopped (${stopped.length}) — off the board, not gating:`)}`);
|
|
1357
|
+
for (const r of stopped) out.push(` ${c(90, `─ ${r.id} ${r.stopped.state}${r.stopped.at ? ' ' + String(r.stopped.at).slice(0, 10) : ''}${r.stopped.reason ? ' — ' + brief(r.stopped.reason, 60) : ''}`)}`);
|
|
1358
|
+
out.push('');
|
|
1359
|
+
}
|
|
1360
|
+
};
|
|
1361
|
+
if (!shown.length) { out.push(` ${c(32, 'every initiative here has a recorded acceptance.')}`); out.push(''); stoppedList(); return out.join('\n'); }
|
|
1196
1362
|
const w1 = Math.min(34, Math.max(...shown.map((r) => r.id.length), 4));
|
|
1197
1363
|
for (const r of shown) {
|
|
1198
1364
|
if (!r.ok) { out.push(` ${c(31, '✗')} ${r.id.padEnd(w1)} ${c(31, r.error)}`); continue; }
|
|
@@ -1210,6 +1376,7 @@ function renderBoard(rows, opts) {
|
|
|
1210
1376
|
// The two ranges, said out loud. The board reads the whole trunk so a delivered initiative does
|
|
1211
1377
|
// not render as unstarted; the gate reads the PR so "spec before build" stays decidable. A rule
|
|
1212
1378
|
// can therefore read done here and still be evaluated on the PR — intended, and not a discrepancy.
|
|
1379
|
+
stoppedList();
|
|
1213
1380
|
out.push(` ${c(90, 'derived from the whole trunk history; the CI gate evaluates order within the PR only')}`);
|
|
1214
1381
|
out.push(` ${c(90, `one initiative in detail: node ${opts.self} <id>`)}`);
|
|
1215
1382
|
out.push('');
|
|
@@ -1232,7 +1399,7 @@ function renderBoard(rows, opts) {
|
|
|
1232
1399
|
const BOARD_CAP = 12;
|
|
1233
1400
|
function inFlight(rows, opts) {
|
|
1234
1401
|
const cap = (opts && opts.cap) || BOARD_CAP;
|
|
1235
|
-
const live = rows.filter((r) => !r.ok || !r.accepted);
|
|
1402
|
+
const live = rows.filter((r) => !r.ok || (!r.accepted && !r.stopped));
|
|
1236
1403
|
const rank = (r) => (!r.ok ? 0 : r.blockers.length ? 1 : r.in_change ? 2 : 3);
|
|
1237
1404
|
const sorted = [...live].sort((a, b) =>
|
|
1238
1405
|
rank(a) - rank(b)
|
|
@@ -1285,6 +1452,7 @@ function main(argv) {
|
|
|
1285
1452
|
|
|
1286
1453
|
// ── gate mode ──────────────────────────────────────────────────────────────────────────────
|
|
1287
1454
|
if (!w.ok) { process.stdout.write(`::error::workflow-gate: ${w.error}\n`); return 1; }
|
|
1455
|
+
if (w.stopped) { process.stdout.write(`ℹ️ '${w.id}' is ${w.stopped.state} — stopped initiatives do not gate (D70).\n✅ workflow gate PASSED — '${w.id}' is ${w.stopped.state}.\n`); return 0; }
|
|
1288
1456
|
const fails = w.steps.filter((s) => s.status === 'blocked' && s.blocking);
|
|
1289
1457
|
if (fails.length) {
|
|
1290
1458
|
for (const f of fails) {
|
|
@@ -1325,7 +1493,7 @@ function main(argv) {
|
|
|
1325
1493
|
}
|
|
1326
1494
|
|
|
1327
1495
|
module.exports = {
|
|
1328
|
-
computeWorkflow, renderLadder, renderBoard, listInitiatives, layout, inFlight, BOARD_CAP,
|
|
1496
|
+
computeWorkflow, readLifecycle, STOPPED_STATES, renderLadder, renderBoard, listInitiatives, layout, inFlight, BOARD_CAP,
|
|
1329
1497
|
parseLeadingFrontmatter, specimenRoute, surfaceRoute, priorsCitation, isBuildPath, commitRange,
|
|
1330
1498
|
// the board's trunk range, exported so the suite can assert the split: the same record is legacy
|
|
1331
1499
|
// under BOTH ranges, and `in this change` is false under the trunk one.
|
|
@@ -467,7 +467,8 @@ function budgetsFor(root, L) {
|
|
|
467
467
|
catch { value = { ok: false, text: '', path: path.relative(root, L.budgets) }; }
|
|
468
468
|
_bud.set(root, { stamp, value });
|
|
469
469
|
}
|
|
470
|
-
|
|
470
|
+
// D67: the domain registry rides on the budgets object, read fresh (a small, PM-edited file)
|
|
471
|
+
return anchors.withDomains(_bud.get(root).value, path.dirname(L.budgets), root);
|
|
471
472
|
}
|
|
472
473
|
|
|
473
474
|
/*
|
|
@@ -1,2 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Reference
|
|
3
|
+
title: "Discoveries and friction log"
|
|
4
|
+
description: "Append-only record of what runs discovered and where the process rubbed."
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-02T01:05:10Z"}
|
|
6
|
+
status: stable
|
|
7
|
+
---
|
|
8
|
+
|
|
1
9
|
# Discoveries & friction log (append-only; ISO dates; tag friction with #friction)
|
|
2
10
|
|