create-pmos 0.78.0 → 0.80.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.78.0",
3
+ "version": "0.80.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": {
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@1dbce98
1
+ kernel-source: pmos@f4177c6
2
2
  assembled: 2026-10-07
3
3
  mode: vendor
@@ -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-07T10:39:13Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-07T13:06:30Z"}
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-07T10:39:13Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-07T13:06:30Z"}
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: an existing surface is explained BEFORE it is specified ─────────────────
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 (fRoute !== 'existing') {
882
- steps.push(step('understand', 'understand', 'na',
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
- const eIdx = firstAppearance(root, range, expl);
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
- const needsRubric = effLane === 'feature';
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) and the rubric alongside it (D13), then set prd_path + rubric_path'
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: the episodic read is CITED, and the citation is checked ─────────────────────
974
- // AGENTS.md step 3 mandates reading this initiative's earlier runs BEFORE acting — the one kernel
975
- // invariant that was still asserted only in prose. Nothing can derive that a file was READ, and
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 before the build. Reported, never CI-blocking ────
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 (runs.length) {
1047
- steps.push(step('contract', 'contract', legacy ? 'pending' : 'blocked',
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 BEFORE building and append the run to ${runsRel} with PM_approved: true`,
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', 'pending', label,
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', 'blocked', label,
1152
- 'the Review Gate FAILED — remediate the findings and record a new verdict', { rule: 'R3' }));
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', 'pending', 'no Review-Gate verdict recorded',
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: the run is explained AFTER the build and BEFORE acceptance ─────────────────
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
- if (found) {
1171
- steps.push(step('explain', 'explain', 'done', found));
1172
- } else if (!runs.length) {
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-07T10:39:13Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-07T13:06:30Z"}
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": "on",
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,
@@ -42,7 +42,7 @@ After **Stop**, the final notes replace this live draft. See [Final notes after
42
42
 
43
43
  The notes taken live are a draft. After **Stop**, the workspace transcribes the whole recording again on this machine, with more time for each sentence, and then your agent session writes the final notes from that transcript and your typed lines.
44
44
 
45
- 1. The workspace transcribes your microphone and the meeting apart. It uses **Parakeet** when it is set up and the meeting is in English, else Whisper with beam search, speech detection and the names of this meeting.
45
+ 1. The workspace transcribes your microphone and the meeting apart. It uses **Qwen3-ASR** when it is set up, in any of the languages. Else it uses **Parakeet** when it is set up and the meeting is in English, else Whisper with beam search, speech detection and the names of this meeting. When one engine fails, the next one set up is used.
46
46
  2. It tells the speakers apart. Your microphone is you. The meeting's voices are **Speaker 1**, **Speaker 2** and so on. When no meeting audio was captured, the voices on your microphone are told apart instead.
47
47
  3. Your agent session writes the purpose, the overview, the topics, the decisions, the next steps with an owner, and the open questions. Each item shows its meeting time and **Said:** with the words that support it. An item with no supporting words in the transcript is left out, and the note says how many were left out.
48
48
 
@@ -50,9 +50,9 @@ For a 30-minute meeting on an M1, this takes about 3 to 10 minutes. Until it is
50
50
 
51
51
  - To name a speaker, press the name in the transcript, type the name and press **Save**. The name is kept when the meeting is transcribed again.
52
52
  - **Show the live transcript** shows what was heard while listening. **Show the final transcript** goes back.
53
- - **Transcribe again** in the settings runs the final pass again, for example after you set up Parakeet.
53
+ - **Transcribe again** in the settings runs the final pass again, for example after you set up Qwen3-ASR.
54
54
 
55
- To set up Parakeet, speech detection and speakers, open the settings and press **Set up in a terminal** under **Final notes after Stop**. A terminal opens with the commands typed in. They install ffmpeg and Python with Homebrew, then Parakeet and sherpa-onnx in the workspace's own folder, and download the models (up to about 2.5 GB). Read the commands and press Enter. Without them, the final pass uses Whisper and does not tell speakers apart.
55
+ To set up Qwen3-ASR, Parakeet, speech detection and speakers, open the settings and press **Set up in a terminal** under **Final notes after Stop**. A terminal opens with the commands typed in. They install ffmpeg and Python with Homebrew, then Parakeet, sherpa-onnx and mlx-qwen3-asr in the workspace's own folder, and download the models (about 6 GB in all, of which Qwen3-ASR is about 3 GB). Read the commands and press Enter. Without them, the final pass uses Whisper and does not tell speakers apart.
56
56
 
57
57
  The kept audio holds your microphone and the meeting apart until the final pass is done. When **Keep the audio for replay** is off, both are deleted after the final pass.
58
58
 
@@ -77,7 +77,11 @@ Local transcription uses whisper.cpp, a free program that runs on your machine.
77
77
 
78
78
  If lines come slowly, choose **Faster (about 550 MB)** under **Model** in the settings, press **Set up in a terminal** and press Enter to download it. It is the same model in a smaller form: it starts and runs faster on an M1 and makes a few more mistakes. **Most accurate (1.6 GB)** is the default.
79
79
 
80
- Whisper writes in the language you choose in the settings under **Language spoken in the meeting**: English unless you change it, or Indonesian. **Detect for each line** lets Whisper guess for each few seconds of speech; it can guess wrong and write a line in another language, so choose the language when you know it.
80
+ The transcript is written in the language you choose in the settings under **Language spoken in the meeting**: English unless you change it, Indonesian or Chinese. **Detect for each line** lets the engine find the language of each few seconds of speech. Whisper can guess wrong and write a line in another language, so with Whisper choose the language when you know it. Qwen3-ASR detects the language better, so with it **Detect for each line** is the choice for a meeting that mixes languages.
81
+
82
+ ### Qwen3-ASR for the live transcript
83
+
84
+ When Qwen3-ASR is set up (see [Final notes after Stop](#final-notes-after-stop)), the live transcript uses it instead of Whisper. It is more accurate in English and Chinese, and it understands Indonesian and speech that mixes these languages. The workspace starts it once, keeps the model loaded while you listen, and stops it after 10 minutes with nothing to transcribe, which frees the memory. The live transcript uses the smaller model (about 800 MB) and the final pass the larger one (about 2 GB). The settings say which engine the live transcript uses. To go back to Whisper, start the workspace with `PMOS_NOTES_QWEN=off`.
81
85
 
82
86
  If the download stops, press **Set up in a terminal** again and press Enter. The download continues where it stopped, and a part-downloaded model is never used.
83
87
 
@@ -148,6 +152,6 @@ To take notes for a calendar event, open the event's detail on the Agenda and pr
148
152
  - Audio never leaves this machine. The page sends it to the workspace on this machine, which gives it to whisper.cpp in a temporary file and deletes that file at once. For replay, the workspace keeps one recording of each note in `notes/recordings/` in the state folder, until you delete the audio, the note, or mark the note private. The browser's own recognition is used only on the device, never in the cloud.
149
153
  - Notes are files in the workspace's own folder on this machine (`notes/` in the state folder). They are never written into a repository.
150
154
  - Only Jev, with the switches on, and your agent session (the final notes, **Summary by agent**, and the Wispr Flow search) receive any text from a note. The Wispr Flow search receives the meeting's time and title only.
151
- - The final pass runs Parakeet, Whisper and the speaker models on this machine, from temporary files that it deletes when it is done.
155
+ - The final pass runs Qwen3-ASR, Parakeet, Whisper and the speaker models on this machine, from temporary files that it deletes when it is done.
152
156
 
153
157
  Live notes is a different tool: it maps each talking point to an initiative. See [Live notes](live-notes.md).
@@ -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: an existing surface is explained BEFORE it is specified ─────────────────
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 (fRoute !== 'existing') {
882
- steps.push(step('understand', 'understand', 'na',
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
- const eIdx = firstAppearance(root, range, expl);
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
- const needsRubric = effLane === 'feature';
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) and the rubric alongside it (D13), then set prd_path + rubric_path'
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: the episodic read is CITED, and the citation is checked ─────────────────────
974
- // AGENTS.md step 3 mandates reading this initiative's earlier runs BEFORE acting — the one kernel
975
- // invariant that was still asserted only in prose. Nothing can derive that a file was READ, and
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 before the build. Reported, never CI-blocking ────
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 (runs.length) {
1047
- steps.push(step('contract', 'contract', legacy ? 'pending' : 'blocked',
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 BEFORE building and append the run to ${runsRel} with PM_approved: true`,
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', 'pending', label,
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', 'blocked', label,
1152
- 'the Review Gate FAILED — remediate the findings and record a new verdict', { rule: 'R3' }));
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', 'pending', 'no Review-Gate verdict recorded',
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: the run is explained AFTER the build and BEFORE acceptance ─────────────────
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
- if (found) {
1171
- steps.push(step('explain', 'explain', 'done', found));
1172
- } else if (!runs.length) {
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) ─────────────────────────────────────