create-pmos 0.78.0 → 0.79.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/KIT-VERSION +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 -123
- package/payload/state/discoveries.md +1 -1
- package/workspace/ci-checks.sh +37 -0
- package/workspace/fixtures/initiatives-api/roadmap.json +9 -2
- package/workspace/lib/engine/workflow-state.js +35 -123
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/replaced-tests.json +39 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.79.0",
|
|
4
4
|
"description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
package/payload/KIT-VERSION
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Eval calibration log"
|
|
4
4
|
description: "Append-only record of every PM override of an evaluator verdict."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-07T12:01:33Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Health budgets"
|
|
4
4
|
description: "Closed registry of health-budget anchors (D59), edited only by the PM."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-07T12:01:33Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -720,6 +720,8 @@ function computeWorkflowDerived(id, opts) {
|
|
|
720
720
|
return Number.isFinite(t) ? t : null;
|
|
721
721
|
})();
|
|
722
722
|
const steps = [];
|
|
723
|
+
// Steps the PM cut or made optional on 2026-10-07 (planning/initiatives/sdlc-step-cuts.md).
|
|
724
|
+
const CUT_NOTE = 'cut by the PM 2026-10-07 (sdlc-step-cuts)';
|
|
723
725
|
|
|
724
726
|
// ── route ─ the lane is chosen and the two intake questions are answered ─────────────────────
|
|
725
727
|
const sRoute = specimenRoute(fm.specimen_test);
|
|
@@ -731,6 +733,9 @@ function computeWorkflowDerived(id, opts) {
|
|
|
731
733
|
if (!missing.length) {
|
|
732
734
|
steps.push(step('route', 'route', 'done',
|
|
733
735
|
`${lane} lane · specimen test ${sRoute} · surface ${fRoute}`));
|
|
736
|
+
} else if (effLane !== 'feature') {
|
|
737
|
+
// The routing questions are asked of feature work only.
|
|
738
|
+
steps.push(step('route', 'route', 'done', `${lane} lane — routing questions are for features only (${CUT_NOTE})`));
|
|
734
739
|
} else if (legacy) {
|
|
735
740
|
steps.push(step('route', 'route', 'unknown',
|
|
736
741
|
`${lane} lane · ${missing.join(' + ')} not declared (record predates ${ENFORCED_FROM})`));
|
|
@@ -875,41 +880,21 @@ function computeWorkflowDerived(id, opts) {
|
|
|
875
880
|
}
|
|
876
881
|
}
|
|
877
882
|
|
|
878
|
-
// ── understand ─ O4
|
|
883
|
+
// ── understand ─ O4, made optional (sdlc-step-cuts) ───────────────────────────────────────
|
|
884
|
+
// An explain-surface pass that exists is shown; a missing one never blocks.
|
|
879
885
|
{
|
|
880
886
|
const expl = String(fm.explainer_path || '').trim();
|
|
881
|
-
if (
|
|
882
|
-
steps.push(step('understand', 'understand', '
|
|
883
|
-
fRoute === 'greenfield' ? 'greenfield — nothing existing to explain first' : 'surface check not declared'));
|
|
884
|
-
} else if (!expl) {
|
|
885
|
-
steps.push(step('understand', 'understand', legacy ? 'unknown' : 'blocked',
|
|
886
|
-
'surface_check says existing but no explainer_path is set',
|
|
887
|
-
'run build-skills/explain-surface.skill in a FRESH context (not the agent that will write the spec), then set explainer_path:',
|
|
888
|
-
{ blocking: !legacy, rule: 'O4' }));
|
|
889
|
-
} else if (!fs.existsSync(path.join(root, expl))) {
|
|
890
|
-
steps.push(step('understand', 'understand', legacy ? 'unknown' : 'blocked',
|
|
891
|
-
`explainer_path '${expl}' does not exist in the repo`,
|
|
892
|
-
'set explainer_path to a real explain-surface output',
|
|
893
|
-
{ blocking: !legacy, rule: 'O4' }));
|
|
887
|
+
if (expl && fs.existsSync(path.join(root, expl))) {
|
|
888
|
+
steps.push(step('understand', 'understand', 'done', `${expl} — optional surface explainer`));
|
|
894
889
|
} else {
|
|
895
|
-
|
|
896
|
-
const ok = ordered(eIdx, prdIdx);
|
|
897
|
-
if (ok === null) {
|
|
898
|
-
steps.push(step('understand', 'understand', 'unknown',
|
|
899
|
-
`${expl} present; order undecidable (${range.ok ? 'no spec artifact to order against' : range.reason})`));
|
|
900
|
-
} else if (ok) {
|
|
901
|
-
steps.push(step('understand', 'understand', 'done', `${expl} — first-appeared before ${prd}`));
|
|
902
|
-
} else {
|
|
903
|
-
steps.push(step('understand', 'understand', legacy ? 'unknown' : 'blocked',
|
|
904
|
-
`ORDER VIOLATION — the spec (${prd}) first-appeared BEFORE the explainer (${expl})`,
|
|
905
|
-
'without the pass first, the AGENT acquires the understanding and the PM approves a spec built on facts they never saw',
|
|
906
|
-
{ blocking: !legacy, rule: 'O4' }));
|
|
907
|
-
}
|
|
890
|
+
steps.push(step('understand', 'understand', 'na', `the surface explainer is optional — ${CUT_NOTE}`));
|
|
908
891
|
}
|
|
909
892
|
}
|
|
910
893
|
|
|
911
894
|
// ── spec ─ the lane's artifacts, and O1/O2: they precede the build ───────────────────────────
|
|
912
|
-
|
|
895
|
+
// A feature PRD's done-criteria section can serve as its rubric (sdlc-step-cuts), so a separate
|
|
896
|
+
// rubric file is optional in every lane. One that is declared must still exist.
|
|
897
|
+
const needsRubric = false;
|
|
913
898
|
{
|
|
914
899
|
const wants = {
|
|
915
900
|
feature: 'a PRD', discovery: 'a spike brief', incident: 'an incident record',
|
|
@@ -925,7 +910,7 @@ function computeWorkflowDerived(id, opts) {
|
|
|
925
910
|
} else if (!prd) {
|
|
926
911
|
steps.push(step('spec', 'spec', 'blocked', `no prd_path — the ${effLane} lane needs ${wants}`,
|
|
927
912
|
effLane === 'feature'
|
|
928
|
-
? 'author the PRD (skills/core/prd.skill)
|
|
913
|
+
? 'author the PRD (skills/core/prd.skill) with its done-criteria (D13), then set prd_path'
|
|
929
914
|
: `author ${wants} and set prd_path in ${recRel}`,
|
|
930
915
|
{ blocking: true, rule: 'R2' }));
|
|
931
916
|
} else if (prdIdx === null) {
|
|
@@ -970,88 +955,24 @@ function computeWorkflowDerived(id, opts) {
|
|
|
970
955
|
const runs = cachedJsonl(path.join(L.runsDir, L.runsFile)).filter((r) => r && r.initiative_id === id);
|
|
971
956
|
const runsRel = path.relative(root, path.join(L.runsDir, L.runsFile));
|
|
972
957
|
|
|
973
|
-
// ── priors ─ O7
|
|
974
|
-
//
|
|
975
|
-
|
|
976
|
-
// this rule does not pretend to: what it derives is the three things that ARE decidable from the
|
|
977
|
-
// record, and together they are what separates it from a checkbox.
|
|
978
|
-
// 1. whether earlier runs existed at all — the rule is vacuous on a first run, and says so
|
|
979
|
-
// 2. whether the run declared what it consulted
|
|
980
|
-
// 3. whether the ids it named are REAL and actually EARLIER
|
|
981
|
-
// (3) is the part that matters. O5's `PM_approved: true` is a bare boolean, satisfiable by typing
|
|
982
|
-
// it; a fabricated `priors` list fails against the corpus it claims to have read. The rule is
|
|
983
|
-
// NON-BLOCKING for the same reason O5/O6 are: it cannot be true before a run exists, and a rule
|
|
984
|
-
// enforced at a moment it cannot be satisfied only teaches people to reach for the escape hatch.
|
|
985
|
-
{
|
|
986
|
-
const latest = runs.length ? runs[runs.length - 1] : null;
|
|
987
|
-
const earlier = runs.slice(0, -1);
|
|
988
|
-
const cite = priorsCitation(latest);
|
|
989
|
-
const how = `consult this initiative's earlier runs BEFORE acting (AGENTS.md step 3) and cite them as \`priors\` on the run in ${runsRel}`;
|
|
990
|
-
if (!latest) {
|
|
991
|
-
steps.push(step('priors', 'priors', 'pending', 'no run logged for this initiative yet', how, { rule: 'O7' }));
|
|
992
|
-
} else if (!earlier.length) {
|
|
993
|
-
// Not a pass — there was nothing to pass. A tick here would be the watermelon this engine exists to prevent.
|
|
994
|
-
steps.push(step('priors', 'priors', 'na',
|
|
995
|
-
`${latest.run_id} is the first run on this initiative — no priors to consult`));
|
|
996
|
-
} else if (cite.malformed) {
|
|
997
|
-
steps.push(step('priors', 'priors', legacy ? 'pending' : 'blocked',
|
|
998
|
-
`${latest.run_id} has a malformed priors citation — ${cite.malformed}`,
|
|
999
|
-
`use \`priors: ["<run_id>", …]\` or \`priors: {consulted: [...], note: "…"}\` in ${runsRel}`,
|
|
1000
|
-
{ rule: 'O7' }));
|
|
1001
|
-
} else if (!cite.declared) {
|
|
1002
|
-
steps.push(step('priors', 'priors', legacy ? 'pending' : 'blocked',
|
|
1003
|
-
`${latest.run_id} follows ${earlier.length} earlier run(s) on this initiative and cites no priors`,
|
|
1004
|
-
how, { rule: 'O7' }));
|
|
1005
|
-
} else {
|
|
1006
|
-
const byId = new Map(runs.map((r) => [String(r.run_id), r]));
|
|
1007
|
-
const earlierIds = new Set(earlier.map((r) => String(r.run_id)));
|
|
1008
|
-
const missing = cite.consulted.filter((x) => !byId.has(x));
|
|
1009
|
-
const notEarlier = cite.consulted.filter((x) => byId.has(x) && !earlierIds.has(x));
|
|
1010
|
-
if (missing.length) {
|
|
1011
|
-
// The check a declared field would otherwise never get. A citation naming a run that does not
|
|
1012
|
-
// exist reports a read that cannot have happened — worse than an absent citation, because it
|
|
1013
|
-
// reads as compliance on the board.
|
|
1014
|
-
steps.push(step('priors', 'priors', legacy ? 'pending' : 'blocked',
|
|
1015
|
-
`${latest.run_id} cites prior run(s) that do not exist on this initiative: ${missing.join(', ')}`,
|
|
1016
|
-
`cite run_ids that are actually in ${runsRel} — a citation naming nothing reports a read that cannot have happened`,
|
|
1017
|
-
{ rule: 'O7' }));
|
|
1018
|
-
} else if (notEarlier.length) {
|
|
1019
|
-
steps.push(step('priors', 'priors', legacy ? 'pending' : 'blocked',
|
|
1020
|
-
`${latest.run_id} cites ${notEarlier.join(', ')}, which ${notEarlier.length > 1 ? 'are' : 'is'} not EARLIER than it`,
|
|
1021
|
-
'a run cannot consult itself or a run that came after it — cite only runs logged before this one',
|
|
1022
|
-
{ rule: 'O7' }));
|
|
1023
|
-
} else if (!cite.consulted.length && !cite.note) {
|
|
1024
|
-
steps.push(step('priors', 'priors', legacy ? 'pending' : 'blocked',
|
|
1025
|
-
`${latest.run_id} declares an empty priors list with no note, over ${earlier.length} earlier run(s)`,
|
|
1026
|
-
'an empty list with no reason is indistinguishable from not looking — say which priors were read, or say in `note` why none applied',
|
|
1027
|
-
{ rule: 'O7' }));
|
|
1028
|
-
} else if (!cite.consulted.length) {
|
|
1029
|
-
// Allowed, and deliberately not silent: the machine cannot judge whether the priors were
|
|
1030
|
-
// genuinely irrelevant, so it surfaces the claim verbatim for the PM instead of ticking.
|
|
1031
|
-
steps.push(step('priors', 'priors', 'done',
|
|
1032
|
-
`${latest.run_id} consulted none of ${earlier.length} earlier run(s) — declared irrelevant: ${cite.note}`));
|
|
1033
|
-
} else {
|
|
1034
|
-
steps.push(step('priors', 'priors', 'done',
|
|
1035
|
-
`${latest.run_id} cites ${cite.consulted.length} prior run(s): ${cite.consulted.join(', ')}${cite.note ? ` — ${cite.note}` : ''}`));
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
958
|
+
// ── priors ─ O7, cut (sdlc-step-cuts) ───────────────────────────────────────────────────────
|
|
959
|
+
// Reading earlier runs is still good practice; citing them is no longer checked.
|
|
960
|
+
steps.push(step('priors', 'priors', 'na', `prior-run citation is not required — ${CUT_NOTE}`));
|
|
1039
961
|
|
|
1040
|
-
// ── contract ─ O5: PM-approved done-criteria
|
|
962
|
+
// ── contract ─ O5: PM-approved done-criteria. Reported, never blocking ─────────────────────
|
|
963
|
+
// Since sdlc-step-cuts only the feature lane asks for it, and a missing sign-off is pending, not
|
|
964
|
+
// blocked: the PM's approval of the PRD and the acceptance at the end are the sign-offs that count.
|
|
1041
965
|
const approved = runs.find((r) => r.pre_run_contract && r.pre_run_contract.PM_approved === true);
|
|
1042
966
|
{
|
|
1043
967
|
if (approved) {
|
|
1044
968
|
steps.push(step('contract', 'contract', 'done',
|
|
1045
969
|
`PM-approved pre_run_contract on run ${approved.run_id}${approved.pre_run_contract.approved_by ? ` (by ${approved.pre_run_contract.approved_by})` : ''}`));
|
|
1046
|
-
} else if (
|
|
1047
|
-
steps.push(step('contract', 'contract',
|
|
1048
|
-
`${runs.length} run(s) logged, none with pre_run_contract.PM_approved: true`,
|
|
1049
|
-
`agree numbered done_criteria + test_method with the PM and record the approval in ${runsRel}`,
|
|
1050
|
-
{ rule: 'O5' }));
|
|
970
|
+
} else if (effLane !== 'feature') {
|
|
971
|
+
steps.push(step('contract', 'contract', 'na', `${effLane} lane — the PM's acceptance stands in for a contract sign-off (${CUT_NOTE})`));
|
|
1051
972
|
} else {
|
|
1052
973
|
steps.push(step('contract', 'contract', 'pending',
|
|
1053
|
-
'no run logged for this initiative yet',
|
|
1054
|
-
`agree done_criteria + test_method
|
|
974
|
+
runs.length ? `${runs.length} run(s) logged, none with pre_run_contract.PM_approved: true` : 'no run logged for this initiative yet',
|
|
975
|
+
`agree done_criteria + test_method with the PM (approving the PRD counts) and record it in ${runsRel}`,
|
|
1055
976
|
{ rule: 'O5' }));
|
|
1056
977
|
}
|
|
1057
978
|
}
|
|
@@ -1143,41 +1064,32 @@ function computeWorkflowDerived(id, opts) {
|
|
|
1143
1064
|
* fix that made a chore's gate visible ADDED one. The module's own rule, repeated in three
|
|
1144
1065
|
* files — "a skip is an honest RECORD that verification did not happen; it is not
|
|
1145
1066
|
* verification" — now applies to the surface that reports it. */
|
|
1067
|
+
// The Review Gate runs on request since sdlc-step-cuts: a skip is n/a, and a FAIL is reported
|
|
1068
|
+
// for the PM to weigh at acceptance rather than holding the gate shut.
|
|
1146
1069
|
if (verdict === 'SKIPPED') {
|
|
1147
|
-
steps.push(step('review', 'review', '
|
|
1148
|
-
'the gate was skipped, so it is still owed — commission an independent evaluator and record a real verdict',
|
|
1149
|
-
{ rule: 'R3' }));
|
|
1070
|
+
steps.push(step('review', 'review', 'na', `${label} — skipped; the Review Gate runs on request (${CUT_NOTE})`));
|
|
1150
1071
|
} else if (verdict === 'FAIL') {
|
|
1151
|
-
steps.push(step('review', 'review', '
|
|
1152
|
-
'the Review Gate FAILED —
|
|
1072
|
+
steps.push(step('review', 'review', 'pending', label,
|
|
1073
|
+
'the Review Gate FAILED — weigh the findings at acceptance, or remediate and record a new verdict', { rule: 'R3' }));
|
|
1153
1074
|
} else {
|
|
1154
1075
|
steps.push(step('review', 'review', 'done', label));
|
|
1155
1076
|
}
|
|
1156
1077
|
} else {
|
|
1157
|
-
steps.push(step('review', 'review', '
|
|
1158
|
-
'score the build against the rubric (build-skills/evaluator.skill), then append to evals.jsonl',
|
|
1159
|
-
{ rule: 'R3' }));
|
|
1078
|
+
steps.push(step('review', 'review', 'na', `no Review-Gate verdict — it runs on request (${CUT_NOTE})`));
|
|
1160
1079
|
}
|
|
1161
1080
|
}
|
|
1162
1081
|
|
|
1163
|
-
// ── explain ─ O6
|
|
1082
|
+
// ── explain ─ O6, made optional (sdlc-step-cuts) ────────────────────────────────────────────
|
|
1083
|
+
// A run explainer that exists is shown; a missing one never blocks acceptance.
|
|
1164
1084
|
{
|
|
1165
1085
|
let found = null;
|
|
1166
1086
|
for (const r of runs) {
|
|
1167
1087
|
const p = path.join(L.explainers, `${r.run_id}-explainer.md`);
|
|
1168
1088
|
if (fs.existsSync(p)) { found = path.relative(root, p); break; }
|
|
1169
1089
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
steps.push(step('explain', 'explain', 'pending', 'no run to explain yet',
|
|
1174
|
-
'log the run first, then run build-skills/explain-run.skill against its diff', { rule: 'O6' }));
|
|
1175
|
-
} else {
|
|
1176
|
-
steps.push(step('explain', 'explain', legacy ? 'pending' : 'blocked',
|
|
1177
|
-
`no explainer at ${path.relative(root, L.explainers)}/${runs[runs.length - 1].run_id}-explainer.md`,
|
|
1178
|
-
'run build-skills/explain-run.skill — raw-transcript vigilance degrades within ~30 min, so the explainer is what the PM actually reviews',
|
|
1179
|
-
{ rule: 'O6' }));
|
|
1180
|
-
}
|
|
1090
|
+
steps.push(found
|
|
1091
|
+
? step('explain', 'explain', 'done', found)
|
|
1092
|
+
: step('explain', 'explain', 'na', `the run explainer is optional — ${CUT_NOTE}`));
|
|
1181
1093
|
}
|
|
1182
1094
|
|
|
1183
1095
|
// ── accept ─ the human PM, final and non-delegable (D12) ─────────────────────────────────────
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Discoveries and friction log"
|
|
4
4
|
description: "Append-only record of what runs discovered and where the process rubbed."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-07T12:01:33Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# The fast workspace checks, run first in the Workspace CI test job (hosted or self-hosted).
|
|
3
|
+
# Usage: bash ci-checks.sh [BASE_REF] — run from workspace/. With BASE_REF (a pull request's base
|
|
4
|
+
# branch) the guide-sync and conformance checks also read the PR's diff.
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
base="${1:-}"
|
|
7
|
+
cd "$(dirname "$0")"
|
|
8
|
+
|
|
9
|
+
if [ -n "$base" ]; then
|
|
10
|
+
git fetch --no-tags -q origin "$base"
|
|
11
|
+
# guide-sync: a pull request that changes the workspace front end also changes the guide.
|
|
12
|
+
changed=$(git -C .. diff --name-only "origin/$base"...HEAD)
|
|
13
|
+
if echo "$changed" | grep -q '^workspace/public/pm/' && ! echo "$changed" | grep -q '^workspace/guide/'; then
|
|
14
|
+
echo "❌ workspace/public/pm/** changed but workspace/guide/** did not. Update the guide page that describes this behaviour (AGENTS.md, 'The user guide')."; exit 1
|
|
15
|
+
fi
|
|
16
|
+
echo "✅ guide-sync holds."
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
# token hygiene: no raw hex outside the token-definition blocks.
|
|
20
|
+
shopt -s nullglob
|
|
21
|
+
files=(public/pm/*.css)
|
|
22
|
+
[ ${#files[@]} -gt 0 ] || { echo "❌ no stylesheet found under public/pm/ — this check would silently pass"; exit 1; }
|
|
23
|
+
hits=$(awk '/^:root\{/,/^\}/{next} /^\[data-pm-theme="light"\]\{/,/^\}/{next} 1' "${files[@]}" | grep -nE '#[0-9a-fA-F]{3,6}' || true)
|
|
24
|
+
[ -z "$hits" ] || { echo "❌ raw hex in a component rule (use a semantic token):"; echo "$hits"; exit 1; }
|
|
25
|
+
echo "✅ token hygiene holds across ${#files[@]} stylesheet(s)."
|
|
26
|
+
|
|
27
|
+
# conformance gate (R28): every line of new files, the added lines of changed ones.
|
|
28
|
+
node conformance-gate.js --self-test
|
|
29
|
+
[ -z "$base" ] || node conformance-gate.js --base "origin/$base"
|
|
30
|
+
|
|
31
|
+
# isolation: workspace/ never imports from web/.
|
|
32
|
+
hits=$(grep -rnE "(from|import|require).*['\"][^'\"]*/?web/" server.js public test.js 2>/dev/null || true)
|
|
33
|
+
[ -z "$hits" ] || { echo "❌ workspace/ reaches into web/ (behavior-preservation floor):"; echo "$hits"; exit 1; }
|
|
34
|
+
echo "✅ isolation holds."
|
|
35
|
+
|
|
36
|
+
# test lists: every suite is listed once and the shards cover them all.
|
|
37
|
+
node run-tests.js --check
|
|
@@ -175,8 +175,15 @@
|
|
|
175
175
|
"line2": ""
|
|
176
176
|
},
|
|
177
177
|
"health": {
|
|
178
|
-
"level": "
|
|
179
|
-
"reasons": [
|
|
178
|
+
"level": "at-risk",
|
|
179
|
+
"reasons": [
|
|
180
|
+
{
|
|
181
|
+
"level": "at-risk",
|
|
182
|
+
"code": "pm-step-owed",
|
|
183
|
+
"text": "Your acceptance is owed. The last evidence is 128 d old",
|
|
184
|
+
"source": "planning/initiatives/fx-old.md"
|
|
185
|
+
}
|
|
186
|
+
]
|
|
180
187
|
},
|
|
181
188
|
"progress": {
|
|
182
189
|
"done": 0,
|
|
@@ -720,6 +720,8 @@ function computeWorkflowDerived(id, opts) {
|
|
|
720
720
|
return Number.isFinite(t) ? t : null;
|
|
721
721
|
})();
|
|
722
722
|
const steps = [];
|
|
723
|
+
// Steps the PM cut or made optional on 2026-10-07 (planning/initiatives/sdlc-step-cuts.md).
|
|
724
|
+
const CUT_NOTE = 'cut by the PM 2026-10-07 (sdlc-step-cuts)';
|
|
723
725
|
|
|
724
726
|
// ── route ─ the lane is chosen and the two intake questions are answered ─────────────────────
|
|
725
727
|
const sRoute = specimenRoute(fm.specimen_test);
|
|
@@ -731,6 +733,9 @@ function computeWorkflowDerived(id, opts) {
|
|
|
731
733
|
if (!missing.length) {
|
|
732
734
|
steps.push(step('route', 'route', 'done',
|
|
733
735
|
`${lane} lane · specimen test ${sRoute} · surface ${fRoute}`));
|
|
736
|
+
} else if (effLane !== 'feature') {
|
|
737
|
+
// The routing questions are asked of feature work only.
|
|
738
|
+
steps.push(step('route', 'route', 'done', `${lane} lane — routing questions are for features only (${CUT_NOTE})`));
|
|
734
739
|
} else if (legacy) {
|
|
735
740
|
steps.push(step('route', 'route', 'unknown',
|
|
736
741
|
`${lane} lane · ${missing.join(' + ')} not declared (record predates ${ENFORCED_FROM})`));
|
|
@@ -875,41 +880,21 @@ function computeWorkflowDerived(id, opts) {
|
|
|
875
880
|
}
|
|
876
881
|
}
|
|
877
882
|
|
|
878
|
-
// ── understand ─ O4
|
|
883
|
+
// ── understand ─ O4, made optional (sdlc-step-cuts) ───────────────────────────────────────
|
|
884
|
+
// An explain-surface pass that exists is shown; a missing one never blocks.
|
|
879
885
|
{
|
|
880
886
|
const expl = String(fm.explainer_path || '').trim();
|
|
881
|
-
if (
|
|
882
|
-
steps.push(step('understand', 'understand', '
|
|
883
|
-
fRoute === 'greenfield' ? 'greenfield — nothing existing to explain first' : 'surface check not declared'));
|
|
884
|
-
} else if (!expl) {
|
|
885
|
-
steps.push(step('understand', 'understand', legacy ? 'unknown' : 'blocked',
|
|
886
|
-
'surface_check says existing but no explainer_path is set',
|
|
887
|
-
'run build-skills/explain-surface.skill in a FRESH context (not the agent that will write the spec), then set explainer_path:',
|
|
888
|
-
{ blocking: !legacy, rule: 'O4' }));
|
|
889
|
-
} else if (!fs.existsSync(path.join(root, expl))) {
|
|
890
|
-
steps.push(step('understand', 'understand', legacy ? 'unknown' : 'blocked',
|
|
891
|
-
`explainer_path '${expl}' does not exist in the repo`,
|
|
892
|
-
'set explainer_path to a real explain-surface output',
|
|
893
|
-
{ blocking: !legacy, rule: 'O4' }));
|
|
887
|
+
if (expl && fs.existsSync(path.join(root, expl))) {
|
|
888
|
+
steps.push(step('understand', 'understand', 'done', `${expl} — optional surface explainer`));
|
|
894
889
|
} else {
|
|
895
|
-
|
|
896
|
-
const ok = ordered(eIdx, prdIdx);
|
|
897
|
-
if (ok === null) {
|
|
898
|
-
steps.push(step('understand', 'understand', 'unknown',
|
|
899
|
-
`${expl} present; order undecidable (${range.ok ? 'no spec artifact to order against' : range.reason})`));
|
|
900
|
-
} else if (ok) {
|
|
901
|
-
steps.push(step('understand', 'understand', 'done', `${expl} — first-appeared before ${prd}`));
|
|
902
|
-
} else {
|
|
903
|
-
steps.push(step('understand', 'understand', legacy ? 'unknown' : 'blocked',
|
|
904
|
-
`ORDER VIOLATION — the spec (${prd}) first-appeared BEFORE the explainer (${expl})`,
|
|
905
|
-
'without the pass first, the AGENT acquires the understanding and the PM approves a spec built on facts they never saw',
|
|
906
|
-
{ blocking: !legacy, rule: 'O4' }));
|
|
907
|
-
}
|
|
890
|
+
steps.push(step('understand', 'understand', 'na', `the surface explainer is optional — ${CUT_NOTE}`));
|
|
908
891
|
}
|
|
909
892
|
}
|
|
910
893
|
|
|
911
894
|
// ── spec ─ the lane's artifacts, and O1/O2: they precede the build ───────────────────────────
|
|
912
|
-
|
|
895
|
+
// A feature PRD's done-criteria section can serve as its rubric (sdlc-step-cuts), so a separate
|
|
896
|
+
// rubric file is optional in every lane. One that is declared must still exist.
|
|
897
|
+
const needsRubric = false;
|
|
913
898
|
{
|
|
914
899
|
const wants = {
|
|
915
900
|
feature: 'a PRD', discovery: 'a spike brief', incident: 'an incident record',
|
|
@@ -925,7 +910,7 @@ function computeWorkflowDerived(id, opts) {
|
|
|
925
910
|
} else if (!prd) {
|
|
926
911
|
steps.push(step('spec', 'spec', 'blocked', `no prd_path — the ${effLane} lane needs ${wants}`,
|
|
927
912
|
effLane === 'feature'
|
|
928
|
-
? 'author the PRD (skills/core/prd.skill)
|
|
913
|
+
? 'author the PRD (skills/core/prd.skill) with its done-criteria (D13), then set prd_path'
|
|
929
914
|
: `author ${wants} and set prd_path in ${recRel}`,
|
|
930
915
|
{ blocking: true, rule: 'R2' }));
|
|
931
916
|
} else if (prdIdx === null) {
|
|
@@ -970,88 +955,24 @@ function computeWorkflowDerived(id, opts) {
|
|
|
970
955
|
const runs = cachedJsonl(path.join(L.runsDir, L.runsFile)).filter((r) => r && r.initiative_id === id);
|
|
971
956
|
const runsRel = path.relative(root, path.join(L.runsDir, L.runsFile));
|
|
972
957
|
|
|
973
|
-
// ── priors ─ O7
|
|
974
|
-
//
|
|
975
|
-
|
|
976
|
-
// this rule does not pretend to: what it derives is the three things that ARE decidable from the
|
|
977
|
-
// record, and together they are what separates it from a checkbox.
|
|
978
|
-
// 1. whether earlier runs existed at all — the rule is vacuous on a first run, and says so
|
|
979
|
-
// 2. whether the run declared what it consulted
|
|
980
|
-
// 3. whether the ids it named are REAL and actually EARLIER
|
|
981
|
-
// (3) is the part that matters. O5's `PM_approved: true` is a bare boolean, satisfiable by typing
|
|
982
|
-
// it; a fabricated `priors` list fails against the corpus it claims to have read. The rule is
|
|
983
|
-
// NON-BLOCKING for the same reason O5/O6 are: it cannot be true before a run exists, and a rule
|
|
984
|
-
// enforced at a moment it cannot be satisfied only teaches people to reach for the escape hatch.
|
|
985
|
-
{
|
|
986
|
-
const latest = runs.length ? runs[runs.length - 1] : null;
|
|
987
|
-
const earlier = runs.slice(0, -1);
|
|
988
|
-
const cite = priorsCitation(latest);
|
|
989
|
-
const how = `consult this initiative's earlier runs BEFORE acting (AGENTS.md step 3) and cite them as \`priors\` on the run in ${runsRel}`;
|
|
990
|
-
if (!latest) {
|
|
991
|
-
steps.push(step('priors', 'priors', 'pending', 'no run logged for this initiative yet', how, { rule: 'O7' }));
|
|
992
|
-
} else if (!earlier.length) {
|
|
993
|
-
// Not a pass — there was nothing to pass. A tick here would be the watermelon this engine exists to prevent.
|
|
994
|
-
steps.push(step('priors', 'priors', 'na',
|
|
995
|
-
`${latest.run_id} is the first run on this initiative — no priors to consult`));
|
|
996
|
-
} else if (cite.malformed) {
|
|
997
|
-
steps.push(step('priors', 'priors', legacy ? 'pending' : 'blocked',
|
|
998
|
-
`${latest.run_id} has a malformed priors citation — ${cite.malformed}`,
|
|
999
|
-
`use \`priors: ["<run_id>", …]\` or \`priors: {consulted: [...], note: "…"}\` in ${runsRel}`,
|
|
1000
|
-
{ rule: 'O7' }));
|
|
1001
|
-
} else if (!cite.declared) {
|
|
1002
|
-
steps.push(step('priors', 'priors', legacy ? 'pending' : 'blocked',
|
|
1003
|
-
`${latest.run_id} follows ${earlier.length} earlier run(s) on this initiative and cites no priors`,
|
|
1004
|
-
how, { rule: 'O7' }));
|
|
1005
|
-
} else {
|
|
1006
|
-
const byId = new Map(runs.map((r) => [String(r.run_id), r]));
|
|
1007
|
-
const earlierIds = new Set(earlier.map((r) => String(r.run_id)));
|
|
1008
|
-
const missing = cite.consulted.filter((x) => !byId.has(x));
|
|
1009
|
-
const notEarlier = cite.consulted.filter((x) => byId.has(x) && !earlierIds.has(x));
|
|
1010
|
-
if (missing.length) {
|
|
1011
|
-
// The check a declared field would otherwise never get. A citation naming a run that does not
|
|
1012
|
-
// exist reports a read that cannot have happened — worse than an absent citation, because it
|
|
1013
|
-
// reads as compliance on the board.
|
|
1014
|
-
steps.push(step('priors', 'priors', legacy ? 'pending' : 'blocked',
|
|
1015
|
-
`${latest.run_id} cites prior run(s) that do not exist on this initiative: ${missing.join(', ')}`,
|
|
1016
|
-
`cite run_ids that are actually in ${runsRel} — a citation naming nothing reports a read that cannot have happened`,
|
|
1017
|
-
{ rule: 'O7' }));
|
|
1018
|
-
} else if (notEarlier.length) {
|
|
1019
|
-
steps.push(step('priors', 'priors', legacy ? 'pending' : 'blocked',
|
|
1020
|
-
`${latest.run_id} cites ${notEarlier.join(', ')}, which ${notEarlier.length > 1 ? 'are' : 'is'} not EARLIER than it`,
|
|
1021
|
-
'a run cannot consult itself or a run that came after it — cite only runs logged before this one',
|
|
1022
|
-
{ rule: 'O7' }));
|
|
1023
|
-
} else if (!cite.consulted.length && !cite.note) {
|
|
1024
|
-
steps.push(step('priors', 'priors', legacy ? 'pending' : 'blocked',
|
|
1025
|
-
`${latest.run_id} declares an empty priors list with no note, over ${earlier.length} earlier run(s)`,
|
|
1026
|
-
'an empty list with no reason is indistinguishable from not looking — say which priors were read, or say in `note` why none applied',
|
|
1027
|
-
{ rule: 'O7' }));
|
|
1028
|
-
} else if (!cite.consulted.length) {
|
|
1029
|
-
// Allowed, and deliberately not silent: the machine cannot judge whether the priors were
|
|
1030
|
-
// genuinely irrelevant, so it surfaces the claim verbatim for the PM instead of ticking.
|
|
1031
|
-
steps.push(step('priors', 'priors', 'done',
|
|
1032
|
-
`${latest.run_id} consulted none of ${earlier.length} earlier run(s) — declared irrelevant: ${cite.note}`));
|
|
1033
|
-
} else {
|
|
1034
|
-
steps.push(step('priors', 'priors', 'done',
|
|
1035
|
-
`${latest.run_id} cites ${cite.consulted.length} prior run(s): ${cite.consulted.join(', ')}${cite.note ? ` — ${cite.note}` : ''}`));
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
958
|
+
// ── priors ─ O7, cut (sdlc-step-cuts) ───────────────────────────────────────────────────────
|
|
959
|
+
// Reading earlier runs is still good practice; citing them is no longer checked.
|
|
960
|
+
steps.push(step('priors', 'priors', 'na', `prior-run citation is not required — ${CUT_NOTE}`));
|
|
1039
961
|
|
|
1040
|
-
// ── contract ─ O5: PM-approved done-criteria
|
|
962
|
+
// ── contract ─ O5: PM-approved done-criteria. Reported, never blocking ─────────────────────
|
|
963
|
+
// Since sdlc-step-cuts only the feature lane asks for it, and a missing sign-off is pending, not
|
|
964
|
+
// blocked: the PM's approval of the PRD and the acceptance at the end are the sign-offs that count.
|
|
1041
965
|
const approved = runs.find((r) => r.pre_run_contract && r.pre_run_contract.PM_approved === true);
|
|
1042
966
|
{
|
|
1043
967
|
if (approved) {
|
|
1044
968
|
steps.push(step('contract', 'contract', 'done',
|
|
1045
969
|
`PM-approved pre_run_contract on run ${approved.run_id}${approved.pre_run_contract.approved_by ? ` (by ${approved.pre_run_contract.approved_by})` : ''}`));
|
|
1046
|
-
} else if (
|
|
1047
|
-
steps.push(step('contract', 'contract',
|
|
1048
|
-
`${runs.length} run(s) logged, none with pre_run_contract.PM_approved: true`,
|
|
1049
|
-
`agree numbered done_criteria + test_method with the PM and record the approval in ${runsRel}`,
|
|
1050
|
-
{ rule: 'O5' }));
|
|
970
|
+
} else if (effLane !== 'feature') {
|
|
971
|
+
steps.push(step('contract', 'contract', 'na', `${effLane} lane — the PM's acceptance stands in for a contract sign-off (${CUT_NOTE})`));
|
|
1051
972
|
} else {
|
|
1052
973
|
steps.push(step('contract', 'contract', 'pending',
|
|
1053
|
-
'no run logged for this initiative yet',
|
|
1054
|
-
`agree done_criteria + test_method
|
|
974
|
+
runs.length ? `${runs.length} run(s) logged, none with pre_run_contract.PM_approved: true` : 'no run logged for this initiative yet',
|
|
975
|
+
`agree done_criteria + test_method with the PM (approving the PRD counts) and record it in ${runsRel}`,
|
|
1055
976
|
{ rule: 'O5' }));
|
|
1056
977
|
}
|
|
1057
978
|
}
|
|
@@ -1143,41 +1064,32 @@ function computeWorkflowDerived(id, opts) {
|
|
|
1143
1064
|
* fix that made a chore's gate visible ADDED one. The module's own rule, repeated in three
|
|
1144
1065
|
* files — "a skip is an honest RECORD that verification did not happen; it is not
|
|
1145
1066
|
* verification" — now applies to the surface that reports it. */
|
|
1067
|
+
// The Review Gate runs on request since sdlc-step-cuts: a skip is n/a, and a FAIL is reported
|
|
1068
|
+
// for the PM to weigh at acceptance rather than holding the gate shut.
|
|
1146
1069
|
if (verdict === 'SKIPPED') {
|
|
1147
|
-
steps.push(step('review', 'review', '
|
|
1148
|
-
'the gate was skipped, so it is still owed — commission an independent evaluator and record a real verdict',
|
|
1149
|
-
{ rule: 'R3' }));
|
|
1070
|
+
steps.push(step('review', 'review', 'na', `${label} — skipped; the Review Gate runs on request (${CUT_NOTE})`));
|
|
1150
1071
|
} else if (verdict === 'FAIL') {
|
|
1151
|
-
steps.push(step('review', 'review', '
|
|
1152
|
-
'the Review Gate FAILED —
|
|
1072
|
+
steps.push(step('review', 'review', 'pending', label,
|
|
1073
|
+
'the Review Gate FAILED — weigh the findings at acceptance, or remediate and record a new verdict', { rule: 'R3' }));
|
|
1153
1074
|
} else {
|
|
1154
1075
|
steps.push(step('review', 'review', 'done', label));
|
|
1155
1076
|
}
|
|
1156
1077
|
} else {
|
|
1157
|
-
steps.push(step('review', 'review', '
|
|
1158
|
-
'score the build against the rubric (build-skills/evaluator.skill), then append to evals.jsonl',
|
|
1159
|
-
{ rule: 'R3' }));
|
|
1078
|
+
steps.push(step('review', 'review', 'na', `no Review-Gate verdict — it runs on request (${CUT_NOTE})`));
|
|
1160
1079
|
}
|
|
1161
1080
|
}
|
|
1162
1081
|
|
|
1163
|
-
// ── explain ─ O6
|
|
1082
|
+
// ── explain ─ O6, made optional (sdlc-step-cuts) ────────────────────────────────────────────
|
|
1083
|
+
// A run explainer that exists is shown; a missing one never blocks acceptance.
|
|
1164
1084
|
{
|
|
1165
1085
|
let found = null;
|
|
1166
1086
|
for (const r of runs) {
|
|
1167
1087
|
const p = path.join(L.explainers, `${r.run_id}-explainer.md`);
|
|
1168
1088
|
if (fs.existsSync(p)) { found = path.relative(root, p); break; }
|
|
1169
1089
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
steps.push(step('explain', 'explain', 'pending', 'no run to explain yet',
|
|
1174
|
-
'log the run first, then run build-skills/explain-run.skill against its diff', { rule: 'O6' }));
|
|
1175
|
-
} else {
|
|
1176
|
-
steps.push(step('explain', 'explain', legacy ? 'pending' : 'blocked',
|
|
1177
|
-
`no explainer at ${path.relative(root, L.explainers)}/${runs[runs.length - 1].run_id}-explainer.md`,
|
|
1178
|
-
'run build-skills/explain-run.skill — raw-transcript vigilance degrades within ~30 min, so the explainer is what the PM actually reviews',
|
|
1179
|
-
{ rule: 'O6' }));
|
|
1180
|
-
}
|
|
1090
|
+
steps.push(found
|
|
1091
|
+
? step('explain', 'explain', 'done', found)
|
|
1092
|
+
: step('explain', 'explain', 'na', `the run explainer is optional — ${CUT_NOTE}`));
|
|
1181
1093
|
}
|
|
1182
1094
|
|
|
1183
1095
|
// ── accept ─ the human PM, final and non-delegable (D12) ─────────────────────────────────────
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.79.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.79.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -123,6 +123,45 @@
|
|
|
123
123
|
"DOC4: raw shows the file as written, frontmatter fence and all",
|
|
124
124
|
"DOC2: view renders the FILE ON DISK as markdown — a heading, its frontmatter card, its list"
|
|
125
125
|
]
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"id": "STD-NEXT",
|
|
129
|
+
"file": "workspace/next-step-actions.test.js",
|
|
130
|
+
"run": "sdlc-step-cuts",
|
|
131
|
+
"ref": "DECISIONS.md D93 and planning/initiatives/sdlc-step-cuts.md (PM-approved 2026-10-07)",
|
|
132
|
+
"reason": "D93 cut the priors step and made the contract optional outside the feature lane, so a chore's reached step is build, not a contract gate. E3, E5, E10 and E11 asserted the cut steps. E2 and E19 keep their intent and now read the build and accept steps of the new ladder; scripts/tests/workflow-order.sh asserts the ladder itself.",
|
|
133
|
+
"names": [
|
|
134
|
+
"E3 the reached step is work, and the contract and acceptance are gates",
|
|
135
|
+
"E5 the sources are the profile-declared ones that suit the step (priors → Confluence)",
|
|
136
|
+
"E10 the build is refused while the contract is not approved",
|
|
137
|
+
"E11 the contract is refused as work",
|
|
138
|
+
"E2 /api/initiative serves an act on every next-step item",
|
|
139
|
+
"E19 go on a refused step starts nothing"
|
|
140
|
+
]
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"id": "STD-PREP",
|
|
144
|
+
"file": "workspace/proactive-prepare.test.js",
|
|
145
|
+
"run": "sdlc-step-cuts",
|
|
146
|
+
"ref": "DECISIONS.md D93 and planning/initiatives/sdlc-step-cuts.md (PM-approved 2026-10-07)",
|
|
147
|
+
"reason": "D93 made the contract optional outside the feature lane, so a chore no longer waits on a contract gate before build. B7 and B8 asserted that gate. B2, D1 and D6 keep their intent and now name the build step the new ladder reaches.",
|
|
148
|
+
"names": [
|
|
149
|
+
"B7 a step the ladder has not reached is refused with the reason (the build waits for the contract)",
|
|
150
|
+
"B8 a gate step (the contract) can never be queued",
|
|
151
|
+
"B2 the plan names the agent step the ladder has reached, with the exact prompt, a 16-hex hash and the switch state",
|
|
152
|
+
"D1 `prepare plan` shows the step, the budget, that the switch is off, the exact prompt and the command to queue it",
|
|
153
|
+
"D6 `prepare status` lists the entry and the budget"
|
|
154
|
+
]
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"id": "STD-TREE",
|
|
158
|
+
"file": "workspace/first-paint-count.test.js",
|
|
159
|
+
"run": "sdlc-step-cuts",
|
|
160
|
+
"ref": "DECISIONS.md D93 and planning/initiatives/sdlc-step-cuts.md (PM-approved 2026-10-07)",
|
|
161
|
+
"reason": "D93 cut the priors step, so the fixture's finished Spec stage no longer owes a row and folds. The freed space shows the fifth open Build row on first paint: 25 controls against specimen A's 24. The assertion keeps specimen A's count as its base and allows that one row's worth (TREE_PANE_BUDGET = TREE_BUDGET + 1); the (d) setup assertion still pins specimen A at 24.",
|
|
162
|
+
"names": [
|
|
163
|
+
"(d) the Tree pane shows at most ${TREE_BUDGET} controls on first paint (specimen A's own Tree view)"
|
|
164
|
+
]
|
|
126
165
|
}
|
|
127
166
|
],
|
|
128
167
|
"entries": [
|