@event4u/agent-config 14.14.0 → 14.15.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.
Files changed (72) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +63 -0
  5. package/README.md +1 -1
  6. package/dist/agent-src/commands/pr/create.md +16 -1
  7. package/dist/agent-src/contexts/execution/terminal-states.md +25 -4
  8. package/dist/agent-src/rules/missing-skill-recovery.md +2 -2
  9. package/dist/agent-src/rules/token-budget-discipline.md +2 -2
  10. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  11. package/dist/cli/registry.js +1 -0
  12. package/dist/cli-delegate/{chunk-ILMUPLGW.js → chunk-7RXIKF62.js} +159 -86
  13. package/dist/cli-delegate/cmd_conformance.js +513 -17
  14. package/dist/cli-delegate/cmd_doctor.js +1 -1
  15. package/dist/discovery/deprecation-report.md +1 -1
  16. package/dist/discovery/discovery-manifest.json +5 -5
  17. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  18. package/dist/discovery/discovery-manifest.summary.md +1 -1
  19. package/dist/discovery/dormancy-report.md +1 -1
  20. package/dist/discovery/orphan-report.md +1 -1
  21. package/dist/discovery/packs.json +2 -2
  22. package/dist/discovery/trust-report.md +1 -1
  23. package/dist/discovery/workspaces.json +2 -2
  24. package/dist/hooks/dispatch.js +1043 -390
  25. package/dist/mcp/registry-manifest.json +2 -2
  26. package/dist/mcp/server.json +2 -2
  27. package/dist/mcp/server.mjs +703 -115
  28. package/package.json +1 -1
  29. package/src/config/assurance-capability-registry.json +1 -1
  30. package/src/config/evaluator-budgets.json +4 -4
  31. package/src/config/gate-coverage.yml +62 -2
  32. package/src/config/gate-reachability-exemptions.json +1 -0
  33. package/src/config/gate-violation-baselines.json +4 -3
  34. package/src/config/rule-enforcement-baseline.json +0 -1
  35. package/src/config/web-launch-readiness.json +4 -2
  36. package/src/scripts/_cli/_doctor_emit.ts +71 -0
  37. package/src/scripts/_cli/_doctor_strict.ts +163 -0
  38. package/src/scripts/_cli/cmd_doctor.ts +16 -37
  39. package/src/scripts/_dispatch.bash +11 -0
  40. package/src/scripts/_lib/branch_convergence.ts +186 -0
  41. package/src/scripts/_lib/context_observation.ts +132 -0
  42. package/src/scripts/_lib/continuation_ladder.ts +188 -0
  43. package/src/scripts/_lib/experiment_binding.ts +95 -0
  44. package/src/scripts/_lib/ignored_blocker.ts +8 -1
  45. package/src/scripts/_lib/outcome_envelope.ts +6 -1
  46. package/src/scripts/_lib/outcome_vocabularies.ts +135 -5
  47. package/src/scripts/_lib/release_highlights.ts +75 -10
  48. package/src/scripts/_lib/repeated_failure.ts +19 -2
  49. package/src/scripts/_lib/runtime_journal.ts +11 -4
  50. package/src/scripts/check_agent_artifact_location.ts +337 -0
  51. package/src/scripts/check_artefact_count_messaging.ts +74 -8
  52. package/src/scripts/check_enforcement_coverage.ts +40 -1
  53. package/src/scripts/check_release_highlights.ts +17 -1
  54. package/src/scripts/check_web_launch_readiness.ts +310 -20
  55. package/src/scripts/evolution_lab.ts +53 -1
  56. package/src/scripts/generate_capability_matrix.ts +16 -9
  57. package/src/scripts/git_authorization_hook.ts +476 -6
  58. package/src/scripts/grade_target_readiness.ts +221 -2
  59. package/src/scripts/hook_effect_doctor.ts +47 -4
  60. package/src/scripts/hooks/block_unauthorized_git.ts +238 -9
  61. package/src/scripts/hooks/merge_impact.ts +207 -0
  62. package/src/scripts/hooks/run_continuation_hook.ts +78 -88
  63. package/src/scripts/hooks/session_eol_hook.ts +12 -1
  64. package/src/scripts/install-hooks.sh +18 -0
  65. package/src/scripts/lint_code_comments.ts +106 -1
  66. package/src/scripts/release.ts +5 -17
  67. package/src/scripts/release_drill.ts +76 -1
  68. package/src/scripts/release_env.ts +31 -0
  69. package/src/scripts/release_publication.ts +125 -1
  70. package/src/scripts/roadmap_context.ts +15 -0
  71. package/src/scripts/skill_scout.ts +633 -0
  72. package/src/scripts/sync_pr_branch.ts +521 -42
@@ -13,7 +13,7 @@
13
13
  * |---|---|---|---|
14
14
  * | phase | one audit-log line — a `/work` PHASE | here, consumed by `orchestration_record` | 4 |
15
15
  * | step | one work-engine STEP | `work_engine/delivery_state.ts` (template tree) | 3 |
16
- * | run | one command or gate RUN | here, consumed by `outcome_envelope` | 6 |
16
+ * | run | one command or gate RUN | here, consumed by `outcome_envelope` | 7 | code-comment-allow report-comment -- one row of a pre-existing three-row table documenting the three vocabularies; the digit changed from 6 to 7 with the vocabulary, and the table is the registry's whole point
17
17
  *
18
18
  * All three are declared in code AND emitted today. `skipped` and `error` are
19
19
  * not aspirational: `envelopeOutcome` in `orchestration_record.ts` returns
@@ -33,16 +33,42 @@
33
33
  * `partial`. So: separate vocabularies, one registry, mappings only where
34
34
  * execution actually crosses.
35
35
  *
36
- * **Mappings are not written for symmetry.** Exactly one crossing exists
37
- * (`DispatchOutcome` -> phase, in `orchestration_record.ts`). A total
38
- * phase<->step<->run mapping would be nine relations of which one is real, and
39
- * the eight invented ones would read as contract.
36
+ * **Mappings are not written for symmetry.** Two crossings exist, and both are
37
+ * places where execution actually translates: `DispatchOutcome` -> phase in
38
+ * `orchestration_record.ts`, and `LadderAction` -> run in
39
+ * `continuation_ladder.ts` (added 2026-09-03 with the seventh run state, so the
40
+ * state has a producer rather than being a word nothing emits). A total
41
+ * phase<->step<->run mapping would be nine relations of which two are real, and
42
+ * the seven invented ones would read as contract.
40
43
  *
41
44
  * `revisit-if`: a producer needs a value its vocabulary cannot express, or a
42
45
  * second real crossing appears, or producer analysis shows two of the three
43
46
  * subjects share identical terminal semantics with a lossless mapping — at
44
47
  * which point unifying those two becomes the cheaper answer.
45
48
  *
49
+ * **The run vocabulary grew to seven on 2026-09-03** — exactly the first branch
50
+ * of the reopening condition above: the continuation ladder needed a word for a
51
+ * run whose plan premise moved, and had none. AI council (anthropic/claude-sonnet-4-5 +
52
+ * openai/codex-default, three rounds, blind chairman) resolved it unanimously
53
+ * as option (a), extend rather than overload `blocked` with a reason field. The
54
+ * seats attached the same prerequisites, discharged here: a versioned value
55
+ * domain ({@link RUN_TERMINAL_VOCABULARY_VERSION}), a tolerant read
56
+ * ({@link readRunTerminalState}), a declared downgrade
57
+ * ({@link RUN_TERMINAL_STATE_DOWNGRADE}) and the rollback trigger below.
58
+ *
59
+ * **Rollback trigger, stated rather than instrumented.** Withdraw
60
+ * `premise-invalidated` — remove it from {@link RUN_TERMINAL_STATES} and let the
61
+ * ladder rung report its declared downgrade `blocked` instead — on EITHER of:
62
+ * (1) any consumer is observed failing (a throw, a refused write, a dropped
63
+ * record) on encountering the value, or (2) `readRunTerminalState` returns
64
+ * `null` for a persisted value in normal operation, which would mean a writer
65
+ * emitted something this registry does not know. Both are single-occurrence
66
+ * triggers, not rates: the downgrade mapping already exists, so withdrawal
67
+ * costs one commit and no migration, and there is no reason to tolerate a
68
+ * budget of failures before paying that. No telemetry is built for this and
69
+ * none is claimed — condition (1) surfaces as a failing run and (2) as a null
70
+ * where a value was written.
71
+ *
46
72
  * Type-only + literal arrays. No `node:` import, so any surface may read it.
47
73
  */
48
74
 
@@ -82,9 +108,99 @@ export const RUN_TERMINAL_STATES = [
82
108
  'approval-required',
83
109
  'exhausted',
84
110
  'stagnated',
111
+ /**
112
+ * The plan premise this run was built on no longer holds — the situational
113
+ * -awareness fingerprint the run engaged under (`origin/main` plus every open
114
+ * PR head) was re-observed and differs.
115
+ *
116
+ * Added by `road-to-wired-instruments` Phase 2 under the AI-council decision
117
+ * of 2026-09-03 (option (a), unanimous). It is NOT `blocked` with a reason
118
+ * field: premise invalidation is operationally distinct from a missing
119
+ * precondition and from an exhausted budget, and folding it into either loses
120
+ * both aggregation by state and type-enforced exhaustive handling. The
121
+ * remedy differs too — re-probe and re-plan, where `blocked` says obtain the
122
+ * missing thing and `exhausted` says the budget was too small.
123
+ */
124
+ 'premise-invalidated',
85
125
  ] as const;
86
126
  export type RunTerminalState = (typeof RUN_TERMINAL_STATES)[number];
87
127
 
128
+ /**
129
+ * Version of the RUN vocabulary's VALUE DOMAIN — bumped when a member is added
130
+ * or removed, never for prose.
131
+ *
132
+ * Distinct from any table version: `runtime_journal.JOURNAL_SCHEMA_VERSION`
133
+ * covers tables and columns and is unaffected by a widening of what one column
134
+ * may contain, so a value-domain change needs its own number or it travels
135
+ * unversioned. A persisted shape carrying a `RunTerminalState` stamps THIS
136
+ * number beside the value, so a reader can tell a value it does not know from a
137
+ * value that is corrupt.
138
+ *
139
+ * v1 — the six original states.
140
+ * v2 — adds `premise-invalidated` (2026-09-03).
141
+ */
142
+ export const RUN_TERMINAL_VOCABULARY_VERSION = 2;
143
+
144
+ /** The vocabulary version each member first appeared in. */
145
+ export const RUN_TERMINAL_STATE_SINCE: Readonly<Record<RunTerminalState, number>> = {
146
+ success: 1,
147
+ 'clean-no-op': 1,
148
+ blocked: 1,
149
+ 'approval-required': 1,
150
+ exhausted: 1,
151
+ stagnated: 1,
152
+ 'premise-invalidated': 2,
153
+ };
154
+
155
+ /**
156
+ * What a consumer pinned to an older vocabulary version reports instead.
157
+ *
158
+ * `premise-invalidated` -> `blocked`: of the six v1 states it is the only one
159
+ * that is honest for a reader that cannot represent it — the run stopped on an
160
+ * external condition it cannot resolve by iterating. Mapping it to `exhausted`
161
+ * would invite the wrong remedy (raise the budget), which is the exact
162
+ * confusion the contract's own `exhausted`/`stagnated` split exists to prevent.
163
+ *
164
+ * Every member introduced after v1 MUST have a row here; `outcome_vocabularies`
165
+ * contract tests assert that, so a future eighth value cannot ship undowngradable.
166
+ */
167
+ export const RUN_TERMINAL_STATE_DOWNGRADE: Readonly<Partial<Record<RunTerminalState, RunTerminalState>>> = {
168
+ 'premise-invalidated': 'blocked',
169
+ };
170
+
171
+ /**
172
+ * The state a consumer pinned at `toVersion` should report for `state`.
173
+ *
174
+ * Returns the state unchanged when it already existed at that version, the
175
+ * declared downgrade when it did not, and `null` for a string this vocabulary
176
+ * does not know at all — never a throw, because this is the function a reader
177
+ * of PERSISTED data calls, and persisted data outlives the code that wrote it.
178
+ */
179
+ export function downgradeRunTerminalState(
180
+ state: string,
181
+ toVersion: number,
182
+ ): RunTerminalState | null {
183
+ if (!isRunTerminalState(state)) return null;
184
+ if ((RUN_TERMINAL_STATE_SINCE[state] ?? 1) <= toVersion) return state;
185
+ return RUN_TERMINAL_STATE_DOWNGRADE[state] ?? null;
186
+ }
187
+
188
+ /**
189
+ * Tolerant read of a persisted terminal state: a member returns itself,
190
+ * anything else returns `null`. Never throws.
191
+ *
192
+ * `null` rather than a fabricated fallback is the load-bearing choice. This
193
+ * vocabulary's own consumers already treat a null terminal state as "not
194
+ * recorded" (`repeated_failure` counts it under `unknown`), and that is exactly
195
+ * what an unrecognised value IS to a reader that cannot name it. Substituting
196
+ * `blocked` here would manufacture a measurement out of an absence — the
197
+ * downgrade above is for a value the reader KNOWS is newer, which is a
198
+ * different fact from a value it cannot place.
199
+ */
200
+ export function readRunTerminalState(v: unknown): RunTerminalState | null {
201
+ return isRunTerminalState(v) ? v : null;
202
+ }
203
+
88
204
  /**
89
205
  * The crossings that exist. One row, because one crossing is real.
90
206
  *
@@ -112,6 +228,20 @@ export const CROSS_DOMAIN_MAPPINGS = [
112
228
  */
113
229
  semantics_version: 2,
114
230
  },
231
+ {
232
+ source: 'LadderAction',
233
+ target: 'run',
234
+ at: 'src/scripts/_lib/continuation_ladder.ts',
235
+ fn: 'terminalStateFor',
236
+ /**
237
+ * Version 1, and the mapping is TOTAL over `LadderAction` by
238
+ * construction: `TERMINAL_STATE_BY_ACTION` is a `Record<LadderAction,
239
+ * RunTerminalState | null>`, so adding a rung without deciding its
240
+ * terminal state does not compile. `engage` maps to null because it is
241
+ * the one action that is not terminal.
242
+ */
243
+ semantics_version: 1,
244
+ },
115
245
  ] as const;
116
246
 
117
247
  /** Is this string a phase outcome? The guard the audit-log consumer was missing. */
@@ -279,9 +279,57 @@ export function derive_categories(commits: readonly SpanCommit[]): Record<string
279
279
  *
280
280
  * A label with no evidence is omitted, so the caller's `_none_` default still
281
281
  * applies where `_none_` is the true answer. One line per label keeps the
282
- * rendered head inside its cap; SHA citations are capped at `DERIVED_SHA_CAP`
283
- * with an explicit remainder count rather than a silent truncation.
282
+ * rendered head inside its cap; citations are capped at `DERIVED_SHA_CAP` with
283
+ * an explicit remainder count rather than a silent truncation.
284
+ *
285
+ * **PUBLISHABLE BY DEFAULT since 2026-09-03, and this is the third position
286
+ * this line has held.** It shipped `DERIVED_MARKER` plus a CATEGORY
287
+ * DESCRIPTION — "rule/schema diffs, breaking commits or removed public surface
288
+ * in 5a3b7c5". The 2026-09-01 flip then made that marker a hard refusal, and
289
+ * the combination had a consequence nobody priced: every release of this
290
+ * package touches `src/rules/` or `src/scripts/schemas/`, so **Behaviour
291
+ * changes** is always substantiated, so the generator always wrote a line the
292
+ * gate always refused. Every release halted, by construction. That was
293
+ * recorded as "the cadence the flip chose"; the maintainer's instruction of
294
+ * 2026-09-03 — *"fix the bug so this stops happening"* — is the authority for
295
+ * removing it.
296
+ *
297
+ * The fix is not a weaker gate, and it is not the marker moving somewhere
298
+ * cheaper (that was 2026-09-03's first attempt, `guard_release_curation`,
299
+ * which relocated the halt without removing it). It is the writer no longer
300
+ * emitting text that cannot ship. Two changes, and the first is what makes the
301
+ * second honest:
302
+ *
303
+ * 1. The line now states the COMMIT SUBJECTS behind each category, not the
304
+ * name of the category's own detection rule. "hooks:effect reports whether
305
+ * a bound concern fires (b0a03a7)" is a claim a reader can use; "rule/schema
306
+ * diffs in b0a03a7" is a restatement of why the classifier fired.
307
+ * 2. `DERIVED_MARKER` is gone from the emission, so nothing unpublishable is
308
+ * ever written and no release halts on the writer's own output.
309
+ *
310
+ * What is deliberately NOT relaxed: `highlight_contradictions` still refuses a
311
+ * human editing a substantiated line down to `_none_`, which is the failure the
312
+ * false 9.13.0 and 9.14.0 heads actually were; and the marker constant and its
313
+ * four guard sites stay, now covering only a marker somebody writes BY HAND.
314
+ * The honest cost of this change, stated rather than left to be discovered: the
315
+ * head is a categorised view of the span rather than curated prose, and it
316
+ * ships without a human having read it. An operator who wants better prose
317
+ * still edits it — they are no longer STOPPED until they do.
284
318
  */
319
+ /**
320
+ * A commit subject reduced to the claim inside it.
321
+ *
322
+ * Drops the conventional-commit `type(scope):` prefix, because "feat(hooks):"
323
+ * is metadata about the commit and the changelog's own commit lists already
324
+ * carry it — what a highlight line needs is the sentence after the colon. Keeps
325
+ * the subject otherwise verbatim: rewording it here would make the head a
326
+ * paraphrase of the span rather than a citation of it, and the whole point of
327
+ * this module is that the two cannot disagree.
328
+ */
329
+ function _claim_text(subject: string): string {
330
+ return subject.replace(/^[a-z]+(?:\([^)]*\))?!?:\s*/u, '').trim();
331
+ }
332
+
285
333
  export function render_derived_head_values(
286
334
  hits: Readonly<Record<string, readonly CategoryHit[]>>,
287
335
  ): Record<string, string> {
@@ -292,11 +340,19 @@ export function render_derived_head_values(
292
340
  if (!reason || ev.length === 0) {
293
341
  continue;
294
342
  }
295
- const shas = ev.map((h) => h.sha.slice(0, 7));
296
- const shown = shas.slice(0, DERIVED_SHA_CAP);
297
- const remainder = shas.length - shown.length;
298
- const more = remainder > 0 ? ` +${remainder} more` : '';
299
- out[label] = `${DERIVED_MARKER} ${reason} in ${shown.join(', ')}${more}.`;
343
+ const shown = ev.slice(0, DERIVED_SHA_CAP);
344
+ const remainder = ev.length - shown.length;
345
+ const more = remainder > 0 ? `; +${String(remainder)} more` : '';
346
+ const cited = shown
347
+ .map((h) => `${_claim_text(h.text)} (${h.sha.slice(0, 7)})`)
348
+ .join('; ');
349
+ // `reason` still names the classifier that fired, kept as the fallback
350
+ // for a hit whose subject renders empty after trimming — a subject that
351
+ // is nothing but a conventional-commit prefix. Without it the line
352
+ // would read "( b0a03a7)".
353
+ out[label] = cited ? `${cited}${more}.` : `${reason} in ${shown
354
+ .map((h) => h.sha.slice(0, 7))
355
+ .join(', ')}${more}.`;
300
356
  }
301
357
  return out;
302
358
  }
@@ -331,14 +387,23 @@ export function render_derived_head_values(
331
387
  * `CURATED_HEAD_INSTRUCTION` is a named sentinel imported from the module that
332
388
  * defines it — never a shape match over comment prose, which would reject
333
389
  * unrelated legitimate comments and miss this one after a reword.
390
+ *
391
+ * `where` names the branch the remedy edit belongs on, because the same
392
+ * blockers are now read at a FOURTH site that is not on `main`:
393
+ * `guard_release_branch_push` refuses the release branch's first push, where
394
+ * the file to edit is on `release/X.Y.Z`. One definition of the blockers with a
395
+ * parameterised remedy beats a second copy of the message that drifts.
334
396
  */
335
- export function publication_blockers(sectionBody: string, version: string): string[] {
397
+ export function publication_blockers(
398
+ sectionBody: string,
399
+ version: string,
400
+ where = '`main`',
401
+ ): string[] {
336
402
  const out: string[] = [];
337
403
  if (sectionBody.includes(DERIVED_MARKER)) {
338
404
  out.push(
339
405
  `the ${version} section still carries \`${DERIVED_MARKER}\` — the generator's ` +
340
- 'draft head, not a curated claim. Rewrite those line(s) on ' +
341
- `${'`main`'} and re-run.`,
406
+ `draft head, not a curated claim. Rewrite those line(s) on ${where} and re-run.`,
342
407
  );
343
408
  }
344
409
  if (sectionBody.includes(CURATED_HEAD_INSTRUCTION)) {
@@ -39,9 +39,26 @@ export const SUCCESS_TERMINALS: ReadonlySet<RunTerminalState> = new Set([
39
39
  'clean-no-op',
40
40
  ]);
41
41
 
42
+ /**
43
+ * Neither a failure nor a success for this metric, and each excluded for its own
44
+ * reason rather than by omission.
45
+ *
46
+ * `approval-required` is the safety floor working; counting a floor doing its
47
+ * job as a failure would make the metric fall every time governance fires.
48
+ *
49
+ * `premise-invalidated` is the same shape one layer out: the run stopped because
50
+ * the world it planned against moved, which is the drift detector working, not
51
+ * the work failing. It is also the state whose base rate is set by OTHER people's
52
+ * pushes rather than by this run's quality — `origin/main` moves constantly — so
53
+ * counting it as a failure would make the repeated-failure rate track repository
54
+ * traffic. That is the concrete form of this roadmap's Risk 1, arriving through
55
+ * the metric instead of through the ladder.
56
+ */
57
+ const EXCLUDED: ReadonlySet<RunTerminalState> = new Set(['approval-required', 'premise-invalidated']);
58
+
42
59
  // Every terminal state is classified or deliberately excluded -- silence about
43
60
  // one is how a metric quietly changes meaning when the vocabulary grows.
44
- const CLASSIFIED = new Set<string>([...FAILURE_TERMINALS, ...SUCCESS_TERMINALS, 'approval-required']);
61
+ const CLASSIFIED = new Set<string>([...FAILURE_TERMINALS, ...SUCCESS_TERMINALS, ...EXCLUDED]);
45
62
  const _unclassified = RUN_TERMINAL_STATES.filter((t) => !CLASSIFIED.has(t));
46
63
  if (_unclassified.length > 0) {
47
64
  throw new Error(
@@ -86,7 +103,7 @@ export function repeatedFailureRate(
86
103
  // `ep.terminal_state` is already the AMENDED verdict: reconstructEpisode
87
104
  // drops superseded rows and takes the LAST effective terminal state.
88
105
  const t = ep.terminal_state;
89
- if (t === null || !CLASSIFIED.has(t) || t === 'approval-required') {
106
+ if (t === null || !CLASSIFIED.has(t) || EXCLUDED.has(t)) {
90
107
  unknown += 1;
91
108
  continue;
92
109
  }
@@ -173,7 +173,7 @@ import type { DatabaseSync } from 'node:sqlite';
173
173
 
174
174
  import { current_branch, git_common_dir, git_dir } from './git_common_dir.js';
175
175
  import type { TerminalState } from './outcome_envelope.js';
176
- import { RUN_TERMINAL_STATES } from './outcome_vocabularies.js';
176
+ import { readRunTerminalState, RUN_TERMINAL_STATES } from './outcome_vocabularies.js';
177
177
  import {
178
178
  isSqliteAvailableSync,
179
179
  loadSqliteSync,
@@ -303,7 +303,7 @@ export const CONSUMPTION_STATES = ['consumed', 'partially-consumed', 'rejected-w
303
303
  export type Consumption = (typeof CONSUMPTION_STATES)[number];
304
304
 
305
305
  /**
306
- * The six terminal states as a runtime array.
306
+ * The run terminal states as a runtime array.
307
307
  *
308
308
  * Re-export of `RUN_TERMINAL_STATES` from `outcome_vocabularies.ts`. It used to
309
309
  * be a second literal list here, guarded by two type-level assertions that
@@ -316,6 +316,12 @@ export type Consumption = (typeof CONSUMPTION_STATES)[number];
316
316
  *
317
317
  * The name is kept because it is exported and pinned by
318
318
  * `tests/scripts/runtime_journal.test.ts:205,207`.
319
+ *
320
+ * Growing this list does NOT move {@link JOURNAL_SCHEMA_VERSION}: that covers
321
+ * tables and columns and a mismatch discards the store, while a widened value
322
+ * domain changes neither. The domain carries its own version in the registry
323
+ * (`RUN_TERMINAL_VOCABULARY_VERSION`), and the read path tolerates a value it
324
+ * does not know rather than trusting the column — see `toEvent`.
319
325
  */
320
326
  export const TERMINAL_STATES = RUN_TERMINAL_STATES;
321
327
 
@@ -1019,7 +1025,7 @@ export function recordEvent(h: JournalHandle, input: RecordEventInput): JournalE
1019
1025
  !(TERMINAL_STATES as readonly string[]).includes(input.terminal_state)
1020
1026
  ) {
1021
1027
  throw new JournalContractError(
1022
- `terminal_state ${JSON.stringify(input.terminal_state)} is not one of the six.`,
1028
+ `terminal_state ${JSON.stringify(input.terminal_state)} is not in the run vocabulary.`,
1023
1029
  );
1024
1030
  }
1025
1031
  if (
@@ -1105,7 +1111,8 @@ function toEvent(r: Record<string, unknown>): JournalEvent {
1105
1111
  worktree_id: String(r['worktree_id']),
1106
1112
  at: String(r['at']),
1107
1113
  capability: String(r['capability']),
1108
- terminal_state: (r['terminal_state'] as TerminalState | null) ?? null,
1114
+ // Tolerant, not a cast: a value this build cannot place reads as null.
1115
+ terminal_state: readRunTerminalState(r['terminal_state']),
1109
1116
  return_ref: (r['return_ref'] as string | null) ?? null,
1110
1117
  verification_ref: (r['verification_ref'] as string | null) ?? null,
1111
1118
  consumption: (r['consumption'] as Consumption | null) ?? null,