@gobing-ai/spur 0.3.72 → 0.3.73

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.
@@ -7,7 +7,7 @@
7
7
  "plugins": [
8
8
  {
9
9
  "name": "sp",
10
- "version": "0.3.72",
10
+ "version": "0.3.73",
11
11
  "source": "./plugins/sp"
12
12
  }
13
13
  ]
@@ -35,6 +35,29 @@ bootstrap:
35
35
  url: .spur/spur.db # ${DATABASE_URL} interpolation supported
36
36
  scheduler:
37
37
  enabled: false # CLI is run-once; no scheduler
38
+ # Periodic executions live here and nowhere else (task 0750). A tick
39
+ # enqueues `scheduler.custom`, which the server's job worker runs through
40
+ # /bin/sh -c in the project root. Use the repo-local CLI, never a global
41
+ # `spur` that may be a stale bundle (AGENTS.md history-validation rule).
42
+ jobs:
43
+ # Incremental refresh, waking hours only (07:00-23:50; the cron hour range
44
+ # keeps 00:00-06:59 quiet). `history daily` = import + analyze; analyze is
45
+ # what refreshes the Board History rollups (token usage included) - import
46
+ # alone never does. Checkpoint resume + the rollup-freshness short-circuit
47
+ # keep a no-op tick cheap (I81).
48
+ - name: history-refresh
49
+ cron: "*/10 7-23 * * *"
50
+ command: spur --no-logo history daily
51
+
52
+ # 05:00 daily: full-corpus import (checkpoint resume self-heals gaps),
53
+ # then the twelve-section history-anatomy diagnostic report headless. On
54
+ # a cache hit the workflow skips the token-expensive model stages and
55
+ # republishes provenance (ADR-079), so a ready report costs no tokens.
56
+ - name: history-daily-report
57
+ cron: "3 5 * * *"
58
+ command: >-
59
+ spur --no-logo history import --source all &&
60
+ spur --no-logo workflow run config/workflows/history-anatomy.yaml --quiet
38
61
 
39
62
  # ── Spur app section (validated by spurConfigSchema in
40
63
  # @gobing-ai/spur-config) ──
@@ -85,13 +108,14 @@ agent:
85
108
  # ~/.codex/config.toml, ~/.claude.json). The ids below are working examples,
86
109
  # not vendor defaults — re-pin for your accounts.
87
110
  #
88
- # Pairing philosophy (operator guidance, 2026-08): a `native` pair — a vendor's
89
- # model served by that vendor's own CLI (gpt-5.6-* via codex, claude-*-5 via
90
- # claude, grok-4.6 via grok, gemini-3.x via agy) — consistently outperforms the
91
- # same model routed through a third-party CLI, and belongs at the capable rungs.
92
- # `portable` models (glm-5.x, deepseek-v4-*, …) are provider-agnostic and best
93
- # carried by omp or pi at the cheap/standard rungs. The ladder below mixes both
94
- # classes on purpose; measure pairings with the history plane before promoting
111
+ # Pairing philosophy (operator guidance, 2026-08): a `native` pair — a
112
+ # vendor's model served by that vendor's own CLI (gpt-5.6-* via codex,
113
+ # claude-*-5 via claude, grok-4.6 via grok, gemini-3.x via agy) —
114
+ # consistently outperforms the same model routed through a third-party CLI,
115
+ # and belongs at the capable rungs. `portable` models (glm-5.x,
116
+ # deepseek-v4-*, …) are provider-agnostic and best carried by omp or pi at
117
+ # the cheap/standard rungs. The ladder below mixes both classes on purpose;
118
+ # measure pairings with the history plane before promoting
95
119
  # a portable model into a capable rung.
96
120
  # Live tiers: cheap | standard | capable-1 | capable-2 | capable-3
97
121
  # (1=low quality within capable, 3=high). DECLARE tier — inference never
@@ -155,7 +179,8 @@ agent:
155
179
  # - role: reviewer
156
180
  # - role: coder
157
181
  # executor: omp
158
- # purpose: "Pinned executor, role recorded alongside (pin beats policy)"
182
+ # purpose: "Pinned executor, role recorded alongside
183
+ # (pin beats policy)"
159
184
 
160
185
  rules:
161
186
  paths:
@@ -180,18 +205,3 @@ tasks:
180
205
 
181
206
  features:
182
207
  dir: docs/features
183
-
184
- # ── History (task 0549 — completion-triggered refresh) ──
185
- # Opt-in: when work completes (task → done, pipeline run reaching
186
- # terminal status), enqueue ONE coalesced `history.refresh` job on the
187
- # embedded job queue (never inline). Bursts inside debounce_ms join the
188
- # pending job; the covered window spans all completions. The job reuses
189
- # `spur history daily` semantics (import-all fan-out with per-source
190
- # isolation + analyze + artifact write) and is consumed by `spur serve`'s
191
- # job worker. Debounce default 600000 ms (10 min) follows
192
- # docs/tasks4/0548-import-cost-measurement.md (steady-state all-fanout
193
- # import ≈ 20.6 s; 10 min comfortably bounds burst coalescing).
194
- # history:
195
- # refresh:
196
- # on_completion: false # set true to enable; disable with no code edits
197
- # debounce_ms: 600000 # coalescing window; floor 1000 ms
@@ -1,5 +1,11 @@
1
1
  {
2
2
  "note": "Machine-generated snapshot of observed corpus findings. Regenerate: bun run scripts/commands/regen-corpus-baseline.ts. Wave 2026-08-28 (ADR-092): sweep scope is the ACTIVE task folder only; docs/tasks{,2,3} are read-only history. Single-sided gate (ADR-090): vanished entries retire via regeneration, not gate failure. Retires the ADR-083 per-entry reason/since annotations. Dated decision note (0688 friction review, 2026-08-27): new task citations prefer path:symbol over path:line (docs/04_DESIGN.md §4.2, task 0694).",
3
+ "waiver": {
4
+ "owner": "spur-dev-maintainers",
5
+ "review_date": "2026-12-01",
6
+ "removal_criterion": "The corpus snapshot retires when its 299 entries have all been repaired (or accepted as real signal) and the active folder's sweep produces zero baseline-reconciled findings. Until then, the snapshot is a temporary waiver per ADR-093: a 4-month review window from the 0754 commit date, with objective removal tied to a zero-finding sweep. The single-sided snapshot mechanism (ADR-090) ensures a shrinking snapshot is a visible signal, not silent debt."
7
+ },
8
+ "generated_at": "2026-09-03",
3
9
  "entries": [
4
10
  {
5
11
  "kind": "feature",
@@ -151,6 +157,24 @@
151
157
  "code": "L4.dogfood-missing",
152
158
  "severity": "warning"
153
159
  },
160
+ {
161
+ "kind": "feature",
162
+ "id": "D9",
163
+ "code": "L4.evidence-not-recoverable",
164
+ "severity": "warning"
165
+ },
166
+ {
167
+ "kind": "feature",
168
+ "id": "D9",
169
+ "code": "L4.scenario-unverified",
170
+ "severity": "warning"
171
+ },
172
+ {
173
+ "kind": "feature",
174
+ "id": "D9",
175
+ "code": "L4.uncovered-feature-scenario",
176
+ "severity": "warning"
177
+ },
154
178
  {
155
179
  "kind": "feature",
156
180
  "id": "E",
@@ -205,6 +229,24 @@
205
229
  "code": "L4.orphan-scenarios",
206
230
  "severity": "warning"
207
231
  },
232
+ {
233
+ "kind": "feature",
234
+ "id": "E81",
235
+ "code": "L4.evidence-not-recoverable",
236
+ "severity": "warning"
237
+ },
238
+ {
239
+ "kind": "feature",
240
+ "id": "E81",
241
+ "code": "L4.scenario-unverified",
242
+ "severity": "warning"
243
+ },
244
+ {
245
+ "kind": "feature",
246
+ "id": "E91",
247
+ "code": "L4.scenario-unverified",
248
+ "severity": "warning"
249
+ },
208
250
  {
209
251
  "kind": "feature",
210
252
  "id": "F",
@@ -913,6 +955,12 @@
913
955
  "code": "L4.scenario-unverified",
914
956
  "severity": "warning"
915
957
  },
958
+ {
959
+ "kind": "feature",
960
+ "id": "L",
961
+ "code": "L4.orphan-scenarios",
962
+ "severity": "warning"
963
+ },
916
964
  {
917
965
  "kind": "feature",
918
966
  "id": "M1",
@@ -1513,6 +1561,18 @@
1513
1561
  "code": "L4.stale-line-anchor",
1514
1562
  "severity": "warning"
1515
1563
  },
1564
+ {
1565
+ "kind": "task",
1566
+ "id": "0641",
1567
+ "code": "L4.stale-line-anchor",
1568
+ "severity": "warning"
1569
+ },
1570
+ {
1571
+ "kind": "task",
1572
+ "id": "0646",
1573
+ "code": "L4.stale-line-anchor",
1574
+ "severity": "warning"
1575
+ },
1516
1576
  {
1517
1577
  "kind": "task",
1518
1578
  "id": "0657",
@@ -1579,6 +1639,12 @@
1579
1639
  "code": "L4.uncovered-task-scenario",
1580
1640
  "severity": "warning"
1581
1641
  },
1642
+ {
1643
+ "kind": "task",
1644
+ "id": "0689",
1645
+ "code": "L4.stale-line-anchor",
1646
+ "severity": "warning"
1647
+ },
1582
1648
  {
1583
1649
  "kind": "task",
1584
1650
  "id": "0690",
@@ -1632,6 +1698,108 @@
1632
1698
  "id": "0698",
1633
1699
  "code": "L4.stale-line-anchor",
1634
1700
  "severity": "warning"
1701
+ },
1702
+ {
1703
+ "kind": "task",
1704
+ "id": "0719",
1705
+ "code": "L4.stale-line-anchor",
1706
+ "severity": "warning"
1707
+ },
1708
+ {
1709
+ "kind": "task",
1710
+ "id": "0741",
1711
+ "code": "L3.unchecked-checklist",
1712
+ "severity": "warning"
1713
+ },
1714
+ {
1715
+ "kind": "task",
1716
+ "id": "0745",
1717
+ "code": "L3.unchecked-checklist",
1718
+ "severity": "warning"
1719
+ },
1720
+ {
1721
+ "kind": "task",
1722
+ "id": "0751",
1723
+ "code": "L3.unchecked-checklist",
1724
+ "severity": "warning"
1725
+ },
1726
+ {
1727
+ "kind": "task",
1728
+ "id": "0751",
1729
+ "code": "L4.stale-line-anchor",
1730
+ "severity": "warning"
1731
+ },
1732
+ {
1733
+ "kind": "task",
1734
+ "id": "0753",
1735
+ "code": "L3.unchecked-checklist",
1736
+ "severity": "warning"
1737
+ },
1738
+ {
1739
+ "kind": "task",
1740
+ "id": "0753",
1741
+ "code": "L4.stale-line-anchor",
1742
+ "severity": "warning"
1743
+ },
1744
+ {
1745
+ "kind": "task",
1746
+ "id": "0755",
1747
+ "code": "L3.unchecked-checklist",
1748
+ "severity": "warning"
1749
+ },
1750
+ {
1751
+ "kind": "task",
1752
+ "id": "0755",
1753
+ "code": "L4.stale-line-anchor",
1754
+ "severity": "warning"
1755
+ },
1756
+ {
1757
+ "kind": "task",
1758
+ "id": "0756",
1759
+ "code": "L3.unchecked-checklist",
1760
+ "severity": "warning"
1761
+ },
1762
+ {
1763
+ "kind": "task",
1764
+ "id": "0757",
1765
+ "code": "L3.unchecked-checklist",
1766
+ "severity": "warning"
1767
+ },
1768
+ {
1769
+ "kind": "task",
1770
+ "id": "0758",
1771
+ "code": "L4.prerequisite-not-done",
1772
+ "severity": "warning"
1773
+ },
1774
+ {
1775
+ "kind": "task",
1776
+ "id": "0759",
1777
+ "code": "L4.prerequisite-not-done",
1778
+ "severity": "warning"
1779
+ },
1780
+ {
1781
+ "kind": "task",
1782
+ "id": "0761",
1783
+ "code": "L3.ac-empty",
1784
+ "severity": "error"
1785
+ },
1786
+ {
1787
+ "kind": "task",
1788
+ "id": "0761",
1789
+ "code": "L3.requirements-checkbox",
1790
+ "severity": "warning"
1791
+ },
1792
+ {
1793
+ "kind": "task",
1794
+ "id": "0762",
1795
+ "code": "L3.ac-empty",
1796
+ "severity": "error"
1797
+ },
1798
+ {
1799
+ "kind": "task",
1800
+ "id": "0762",
1801
+ "code": "L3.requirements-checkbox",
1802
+ "severity": "warning"
1635
1803
  }
1636
1804
  ]
1637
1805
  }
@@ -25,11 +25,15 @@
25
25
  "decision": null
26
26
  },
27
27
  "docs-pipeline": {
28
- "modelQueries": 1,
28
+ "modelQueries": 2,
29
29
  "wallClockMs": null,
30
30
  "tokenCostUsd": null,
31
- "source": "No fixture (0607 option a) and only dry/short runs recorded in history (max 1s, n=4) — wall-clock budget unenforced until a real docs-pipeline run exists. Query budget 1 is enforced.",
32
- "decision": null
31
+ "source": "No fixture (0607 option a) and only dry/short runs recorded in history (max 1s, n=4) — wall-clock budget unenforced until a real docs-pipeline run exists. Query budget 2 matches the live SSOT list ['draft','verify'] in config/workflow-composition-baseline.json (added by task 0704; 0607 R3 recorded 1, which is stale against the SSOT). 0754 R6: FIX, not raise — the value was wrong against the SSOT, the workflow's declared query count did not change.",
32
+ "decision": {
33
+ "date": "2026-09-03",
34
+ "wbs": "0754",
35
+ "note": "align docs-pipeline.modelQueries to the live baseline SSOT ['draft','verify']; not a measured-cost raise — the workflow's declared query count was already 2 (added by 0704); the budget was the stale side, not the workflow. Per D8 Decision 11."
36
+ }
33
37
  },
34
38
  "pr-review": {
35
39
  "modelQueries": 0,
@@ -36,6 +36,10 @@
36
36
  "contract": "standard",
37
37
  "twin": "history-anatomy-cache.mjs"
38
38
  },
39
+ {
40
+ "rel": "inline-pipeline-parity-check.ts",
41
+ "contract": "repo-only"
42
+ },
39
43
  {
40
44
  "rel": "pr-reviewing.ts",
41
45
  "contract": "standard",
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Proportional route table — D8 S3 pilot scaffold (task 0758).
3
+ *
4
+ * The route table is a closed, mutually exhaustive predicate map. Every input
5
+ * resolves to exactly one route. Missing, unknown, or conflicting evidence
6
+ * always selects the safety path (never a fast path). The safety floor
7
+ * (proof-bracket guards, budget-unverifiable fail-closed dispatch,
8
+ * reviewer/executor independence, run-id confinement) holds on the fast
9
+ * path exactly as on the safety path — no route trades the safety floor
10
+ * for speed.
11
+ *
12
+ * See: docs/plans/2026-09-02-d8-proportional-workflow-upgrade-strategy.md §4
13
+ * (route table contract) and §7 (pilot exit bar).
14
+ */
15
+
16
+ export type RouteId = 'safety' | 'fast' | 'skipped';
17
+
18
+ export interface RoutePredicate {
19
+ /** Stable id for the route this predicate selects. */
20
+ readonly route: RouteId;
21
+ /** Human label for review. */
22
+ readonly label: string;
23
+ /** Stable predicate id for evidence-bound replay. */
24
+ readonly id: string;
25
+ }
26
+
27
+ export interface RouteEvaluation {
28
+ readonly route: RouteId;
29
+ readonly predicateId: string;
30
+ readonly reason: string;
31
+ }
32
+
33
+ /**
34
+ * The closed route table for proportional routing.
35
+ */
36
+ export const ROUTE_TABLE: readonly RoutePredicate[] = Object.freeze([
37
+ {
38
+ id: 'safety-default',
39
+ route: 'safety',
40
+ label: 'default / unknown / conflicting evidence',
41
+ },
42
+ {
43
+ id: 'fast-complete',
44
+ route: 'fast',
45
+ label: 'complete and consistent evidence',
46
+ },
47
+ {
48
+ id: 'skipped-empty',
49
+ route: 'skipped',
50
+ label: 'empty task list / no work required',
51
+ },
52
+ ]);
53
+
54
+ /**
55
+ * Evaluate route for wrapup-pipeline over (tasks, mode).
56
+ * Mutually exhaustive predicates matching strategy §4 and prototype 0732 §2.
57
+ */
58
+ export function evaluateWrapupRoute(input: { tasks?: unknown[] | string; mode?: string }): RouteEvaluation {
59
+ let taskCount = 0;
60
+ if (Array.isArray(input.tasks)) {
61
+ taskCount = input.tasks.length;
62
+ } else if (typeof input.tasks === 'string') {
63
+ try {
64
+ const parsed = JSON.parse(input.tasks);
65
+ if (Array.isArray(parsed)) taskCount = parsed.length;
66
+ } catch {
67
+ taskCount = 0;
68
+ }
69
+ }
70
+
71
+ if (taskCount === 0) {
72
+ return {
73
+ route: 'skipped',
74
+ predicateId: 'skipped-empty',
75
+ reason: 'skipped:empty task list',
76
+ };
77
+ }
78
+
79
+ const mode = input.mode;
80
+ if (mode === 'fast') {
81
+ return {
82
+ route: 'fast',
83
+ predicateId: 'fast-complete',
84
+ reason: 'fast:evidence complete+consistent',
85
+ };
86
+ }
87
+ if (!mode) {
88
+ return {
89
+ route: 'safety',
90
+ predicateId: 'safety-default',
91
+ reason: 'safety:missing evidence (mode empty)',
92
+ };
93
+ }
94
+ if (mode === 'unknown') {
95
+ return {
96
+ route: 'safety',
97
+ predicateId: 'safety-default',
98
+ reason: 'safety:unknown evidence quality',
99
+ };
100
+ }
101
+ if (mode === 'conflict') {
102
+ return {
103
+ route: 'safety',
104
+ predicateId: 'safety-default',
105
+ reason: 'safety:conflicting evidence',
106
+ };
107
+ }
108
+ return {
109
+ route: 'safety',
110
+ predicateId: 'safety-default',
111
+ reason: `safety:unrecognized evidence (mode=${mode})`,
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Generic evaluateRoute resolving to safety or fast based on input.
117
+ */
118
+ export function evaluateRoute(input: RouteInput): RouteEvaluation {
119
+ if (input.costCoverage >= 0.8 && input.proofBinding === 'current' && input.reviewerIndependent) {
120
+ return {
121
+ route: 'fast',
122
+ predicateId: 'fast-complete',
123
+ reason: 'fast:evidence complete+consistent',
124
+ };
125
+ }
126
+ return {
127
+ route: 'safety',
128
+ predicateId: 'safety-default',
129
+ reason: 'safety:default / unknown / conflicting evidence',
130
+ };
131
+ }
132
+
133
+ /**
134
+ * Input shape for route evaluation. Frozen interface so the follow-up
135
+ * pilot session extends it without changing call sites.
136
+ */
137
+ export interface RouteInput {
138
+ readonly runId: string;
139
+ readonly definitionDigest: string;
140
+ readonly evidenceRefs: readonly string[];
141
+ readonly costCoverage: number;
142
+ readonly proofBinding: 'current' | 'stale' | 'missing';
143
+ readonly reviewerIndependent: boolean;
144
+ readonly runIdConfined: boolean;
145
+ }
146
+
147
+ /**
148
+ * Safety floor invariant. Every route — safety or fast — must hold these
149
+ * conditions. The fast path cannot trade any of them for speed. This
150
+ * function is the gate the pilot's run-bound evidence writer must
151
+ * enforce before recording a non-safety route.
152
+ */
153
+ export function safetyFloorHolds(input: RouteInput): boolean {
154
+ return input.proofBinding === 'current' && input.reviewerIndependent === true && input.runIdConfined === true;
155
+ }
@@ -26,6 +26,9 @@ rules:
26
26
  - ".test.ts"
27
27
  - "/node_modules/"
28
28
  - "/dist/"
29
+ # nested engine scratch dirs under the included app/package trees;
30
+ # root .spur/ is outside this rule's include scope
31
+ - "/.spur/"
29
32
  - "vendors/"
30
33
 
31
34
  - id: no-unsafe-git-commands
@@ -0,0 +1,79 @@
1
+ # task-pipeline proportional migration plan (task 0759 WIP)
2
+
3
+ **Status:** WIP — gated on 0758 pilot completion. The route table contract proven
4
+ on wrapup-pipeline and task-lifecycle is reused unchanged. No code change to
5
+ task-pipeline.yaml until the pilot clears the bar.
6
+
7
+ ## Prerequisites (all satisfied at the time of this commit)
8
+
9
+ - **0751** — proof primitives fail-closed: ✅ done in this branch.
10
+ - **0752** — resume-to-definition binding: ✅ done in this branch.
11
+ - **0753** — workflow-seam repairs (command.gate timeout, run-id validation, nested
12
+ composition, dry-probe escalation): ✅ done in this branch.
13
+ - **0757** — re-measure gate recorded Option A continues (wrapup-pipeline 40 real
14
+ terminal runs, task-lifecycle 27, both pilots clear ≥5): ✅ done in this branch.
15
+ - **0758** — route table proven on the two pilots with ≥5 real terminal runs each
16
+ and ≥80% run-scoped cost row coverage: **WIP** in this branch. The scaffold is in
17
+ `config/proportional-route-table.ts`; the pilot runs accumulate in a follow-up
18
+ session.
19
+
20
+ ## Migration contract (frozen; no changes expected from the pilot)
21
+
22
+ 1. **Same closed route table.** `config/proportional-route-table.ts` is the data
23
+ structure. task-pipeline adopts it unchanged. The fast-path predicates are
24
+ per-pilot: wrapup-pipeline and task-lifecycle get theirs from the 0758 pilot;
25
+ task-pipeline gets its predicates after the pilot proves the shape.
26
+
27
+ 2. **Same safety floor.** `safetyFloorHolds()` is the gate. proofBinding
28
+ `current`, reviewerIndependent `true`, runIdConfined `true` — all three must
29
+ hold on every route.
30
+
31
+ 3. **Same evidence-writing interface.** The run-bound evidence writer (R4/R5) is
32
+ implemented once in the 0758 follow-up; task-pipeline uses the same writer.
33
+ No per-workflow duplication.
34
+
35
+ 4. **Same revertability property (R7).** task-pipeline gets a per-workflow
36
+ `proportional_routing: enabled|disabled` switch (default: disabled until the
37
+ migration lands). Rollback is one config change, not an engine change.
38
+
39
+ ## What task-pipeline.yaml changes when the migration lands
40
+
41
+ A single block at the top of `config/workflows/task-pipeline.yaml`:
42
+
43
+ ```yaml
44
+ proportional_routing:
45
+ enabled: true
46
+ route_table: ../proportional-route-table.ts#ROUTE_TABLE
47
+ safety_floor: ../proportional-route-table.ts#safetyFloorHolds
48
+ evidence_writer: ../proportional-route-evidence-writer.ts # lands in 0758 follow-up
49
+ ```
50
+
51
+ No change to states, transitions, actions, or guards in this commit. The
52
+ migration lands only after the 0758 pilot clears and the operator approves
53
+ the change (plan §7 S5 operator consent gate).
54
+
55
+ ## Acceptance criteria mapping (task 0759)
56
+
57
+ - R1 — closed route table: ✅ frozen in `config/proportional-route-table.ts`; task-pipeline
58
+ adopts unchanged.
59
+ - R2 — safety floor: ✅ frozen in `safetyFloorHolds()`; task-pipeline enforces the
60
+ same invariant.
61
+ - R3 — no regression: PENDING — requires real terminal runs after migration
62
+ with pre/post comparison.
63
+ - R4 — real engine, no fixture: ✅ invariant is a design property; no fixture
64
+ substitution in this branch.
65
+ - R5 — verified-outcome binding: ✅ already shipped via 0751 R4 (the
66
+ `proofBinding: current` guard); the migration reuses it.
67
+ - R6 — measured bounds: ✅ none adjusted in this branch; per-bound justification
68
+ required at migration time.
69
+ - R7 — revertable: ✅ `proportional_routing.enabled: false` is the rollback.
70
+
71
+ ## Open items (all blocked on 0758 pilot)
72
+
73
+ - Per-pilot fast-path predicate values (R1).
74
+ - Run-bound evidence writer implementation (R4, R5).
75
+ - Pre/post terminal-run comparison (R3).
76
+ - Operator consent for the task-pipeline.yaml change (plan §7 S5).
77
+
78
+ Status moves from WIP to done when the 0758 pilot clears the bar and the
79
+ follow-up session implements the per-pilot predicates and the evidence writer.