@event4u/agent-config 15.0.0 → 16.0.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 (78) 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 +142 -348
  5. package/dist/agent-src/commands/roadmap/create.md +3 -2
  6. package/dist/agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +50 -0
  7. package/dist/agent-src/rules/user-interaction.md +31 -30
  8. package/dist/agent-src/skills/error-handling-patterns/SKILL.md +9 -0
  9. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  10. package/dist/cli-delegate/{chunk-HZ52BRGG.js → chunk-H32DGUJH.js} +14 -89
  11. package/dist/cli-delegate/chunk-NUHLI2RX.js +80 -0
  12. package/dist/cli-delegate/{chunk-TYSY33MD.js → chunk-QJWAUMJW.js} +12 -2
  13. package/dist/cli-delegate/{chunk-2KKXJW25.js → chunk-S3VFXMTM.js} +1 -1
  14. package/dist/cli-delegate/cmd_conformance.js +831 -272
  15. package/dist/cli-delegate/cmd_explain.js +4 -2
  16. package/dist/cli-delegate/cmd_handoff.js +2 -2
  17. package/dist/cli-delegate/cmd_packs_active.js +2 -1
  18. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  19. package/dist/cli-delegate/cmd_settings_set.js +13 -0
  20. package/dist/discovery/deprecation-report.md +1 -1
  21. package/dist/discovery/discovery-manifest.json +5 -5
  22. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  23. package/dist/discovery/discovery-manifest.summary.md +1 -1
  24. package/dist/discovery/dormancy-report.md +1 -1
  25. package/dist/discovery/orphan-report.md +1 -1
  26. package/dist/discovery/packs.json +2 -2
  27. package/dist/discovery/trust-report.md +1 -1
  28. package/dist/discovery/workspaces.json +2 -2
  29. package/dist/hooks/dispatch.js +1274 -571
  30. package/dist/install/install.mjs +1 -0
  31. package/dist/mcp/registry-manifest.json +2 -2
  32. package/dist/mcp/server.json +2 -2
  33. package/dist/mcp/server.mjs +1218 -632
  34. package/dist/scripts/_lib/installed_lock.js +1 -1
  35. package/dist/server/io/yamlIO.js +50 -0
  36. package/package.json +1 -1
  37. package/src/config/continuity-surface.json +7 -0
  38. package/src/config/gate-violation-baselines.json +3 -3
  39. package/src/config/host-injection-effect.json +1 -1
  40. package/src/config/preamble-payload-budget.json +12 -7
  41. package/src/config/preamble-payload-exceptions.json +28 -0
  42. package/src/scripts/_dispatch.bash +3 -1
  43. package/src/scripts/_lib/base_ref_payload.ts +315 -0
  44. package/src/scripts/_lib/dropped_decision.ts +142 -0
  45. package/src/scripts/_lib/installed_lock.ts +1 -1
  46. package/src/scripts/_lib/measured_payload_ceiling.ts +424 -0
  47. package/src/scripts/_lib/payload_catalogue_completeness.ts +211 -0
  48. package/src/scripts/_lib/release_findings_ingest.ts +324 -0
  49. package/src/scripts/_lib/release_tag_dispatch.ts +63 -0
  50. package/src/scripts/_lib/review_baseline.ts +254 -0
  51. package/src/scripts/_lib/review_skipped_record.ts +61 -1
  52. package/src/scripts/_lib/standing_bound_ratchet.ts +216 -103
  53. package/src/scripts/_lib/turn_end_refusals.ts +51 -4
  54. package/src/scripts/check_beta_review_markers.ts +35 -4
  55. package/src/scripts/check_finding_dispositions.ts +167 -16
  56. package/src/scripts/check_held_object_arrivals.ts +381 -0
  57. package/src/scripts/check_preamble_payload_budget.ts +399 -122
  58. package/src/scripts/check_release_published.ts +1 -1
  59. package/src/scripts/check_reply_consistency.ts +86 -17
  60. package/src/scripts/hook_manifest.json +1 -1
  61. package/src/scripts/hook_manifest.yaml +39 -3
  62. package/src/scripts/hooks/block_kernel_rule_writes.ts +12 -2
  63. package/src/scripts/hooks/concern_registry.ts +2 -0
  64. package/src/scripts/hooks/end_review_nudge_hook.ts +101 -21
  65. package/src/scripts/hooks/review_baseline_hook.ts +130 -0
  66. package/src/scripts/hooks/turn_end_gate_hook.ts +232 -14
  67. package/src/scripts/hooks_doctor.ts +4 -1
  68. package/src/scripts/install.ts +1 -1
  69. package/src/scripts/interruption_report.ts +8 -2
  70. package/src/scripts/janitor.ts +8 -1
  71. package/src/scripts/measure_turn_end_gate.ts +302 -5
  72. package/src/scripts/release.ts +52 -64
  73. package/src/scripts/release_drill.ts +193 -14
  74. package/src/scripts/release_env.ts +1 -1
  75. package/src/scripts/release_publication.ts +1 -1
  76. package/src/scripts/report_held_object_arrivals.ts +456 -0
  77. package/src/scripts/sync_agent_settings.ts +140 -18
  78. package/src/server/io/yamlIO.ts +48 -0
@@ -3,38 +3,63 @@
3
3
  *
4
4
  * WHY THIS EXISTS
5
5
  * ---------------
6
- * `src/config/preamble-payload-budget.json` says of its grace ceiling: *"It may
7
- * never move UP."* `grace_ceiling_history` in the same file records it moving up
8
- * twice — 138,212 → 138,273 on 2026-09-02 and 138,273 → 138,490 on 2026-09-08 —
9
- * each with a careful prose justification. ADR-264 resolved that contradiction
10
- * against the practice: the sentence stands, the two raises are violations caused
11
- * by contradictory wording, and standing-rule growth needs a compensating
12
- * reduction elsewhere.
6
+ * `src/config/preamble-payload-budget.json` said of its grace ceiling: *"It may
7
+ * never move UP."* `grace_ceiling_history` in the same file recorded it moving
8
+ * up twice — 138,212 → 138,273 on 2026-09-02 and 138,273 → 138,490 on
9
+ * 2026-09-08 — each with a careful prose justification. ADR-264 resolved that
10
+ * contradiction against the practice: the sentence stands, the two raises are
11
+ * violations caused by contradictory wording, and standing-rule growth needs a
12
+ * compensating reduction elsewhere.
13
13
  *
14
14
  * That decision then had no enforcement. A sentence in a JSON string is not a
15
- * gate, and the file's own history is the evidence: nothing objected either time.
16
- * This module is the enforcement — and it is the ONLY thing
17
- * `road-to-a-standing-budget-with-headroom` step 1.3 ships, because the 128-token
18
- * Iron Law reserve that step designed was refused by an AI council 2/2 on
19
- * 2026-09-08. See ADR-265.
15
+ * gate, and the file's own history was the evidence: nothing objected either
16
+ * time. This module is the enforcement.
17
+ *
18
+ * WHAT IT BOUNDS NOW — AND WHY THE SUBJECT CHANGED
19
+ * -------------------------------------------------
20
+ * It used to bound one number, `ci_delivery.grace_ceiling`. That key is GONE
21
+ * (`road-to-delivery-for-every-host` 4.4): the ceiling is no longer stored, it
22
+ * is MEASURED at the base ref as `max(design_ceiling, base + active grants)`.
23
+ * A measured ceiling captures every merged reduction automatically, which a
24
+ * stored one never did.
25
+ *
26
+ * Moving the ceiling out of the config did not remove the need for a
27
+ * shrink-only bound — it moved the attack surface. Three numbers in the tree
28
+ * can still widen the effective ceiling without adding a single token of
29
+ * payload, and all three are now bounded:
30
+ *
31
+ * · `design_ceiling`, derived from `baseline_tokens` × `headroom_pct`. It is
32
+ * the FLOOR of the measured ceiling, so raising it raises the ceiling
33
+ * directly. ADR-264's sentence applies to it verbatim.
34
+ * · every existing exception's `granted_tokens` — enlarging a live grant is
35
+ * a raise wearing a break-glass label.
36
+ * · every existing exception's `watermark` — the watermark is what pins the
37
+ * base while a grant is live, so raising it launders the grant into the
38
+ * next base, which is precisely the failure the watermark exists to stop.
39
+ *
40
+ * A NEW exception id is deliberately NOT a violation: recording a fresh,
41
+ * approved, dated grant is the break-glass path working. Editing one that
42
+ * already existed is not.
20
43
  *
21
44
  * WHAT IT CANNOT DO, STATED PLAINLY
22
45
  * ---------------------------------
23
46
  * The change under review can edit this file. Both council seats named that as
24
- * the defect fatal to the reserve — "the checker, its imports, token census,
25
- * workflow, and status context are all part of the trusted computing base", so a
26
- * protected record read by an unprotected verifier grants nothing. It is fatal to
27
- * a mechanism that GRANTS budget and it is not fatal to one that only REFUSES,
28
- * which is the whole reason the reserve stayed unshipped and this ratchet did
29
- * not: disabling this check requires visibly editing it, and every gate in this
30
- * repository already rests on that same review. A ratchet whose only failure mode
31
- * is "someone deleted the ratchet" is strictly better than the sentence it
32
- * replaces, which had no failure mode because it did nothing.
47
+ * the defect fatal to a mechanism that GRANTS budget, and it is not fatal to
48
+ * one that only REFUSES: disabling this check requires visibly editing it, and
49
+ * every gate in this repository already rests on that same review. A ratchet
50
+ * whose only failure mode is "someone deleted the ratchet" is strictly better
51
+ * than the sentence it replaces, which had no failure mode because it did
52
+ * nothing. The 2026-09-10 verdict added one mitigation on top and named its
53
+ * limit in the same breath: CI runs this code AS IT EXISTS AT THE BASE REF, so
54
+ * a same-PR weakening cannot measure its own change — *"the mechanism does not
55
+ * prevent the exploit; it makes the exploit auditable."*
33
56
  */
34
57
  import { spawnSync } from 'node:child_process';
35
58
 
36
- /** Repo-relative path of the budget config the bound lives in. */
59
+ /** Repo-relative path of the budget config the bounds live in. */
37
60
  export const BUDGET_CONFIG_PATH = 'src/config/preamble-payload-budget.json';
61
+ /** Repo-relative path of the break-glass ledger the grant bounds live in. */
62
+ export const EXCEPTIONS_CONFIG_PATH = 'src/config/preamble-payload-exceptions.json';
38
63
 
39
64
  /** Injection seam: run git, capture output, never throw on a non-zero exit. */
40
65
  export type GitRunner = (args: readonly string[], cwd: string) => {
@@ -48,28 +73,68 @@ export const realGit: GitRunner = (args, cwd) => {
48
73
  return { ok: r.status === 0, stdout: r.stdout ?? '', stderr: r.stderr ?? '' };
49
74
  };
50
75
 
76
+ /** Named shrink-only numbers, keyed by a stable bound id. */
77
+ export type BoundSet = Record<string, number>;
78
+
51
79
  export interface BoundsRatchetVerdict {
52
80
  ok: boolean;
53
81
  violations: string[];
54
- /** The bound read at the base ref, or `null` when it could not be read. */
55
- baseGraceCeiling: number | null;
82
+ /** The bounds read at the base ref, or `null` when they could not be read. */
83
+ baseBounds: BoundSet | null;
56
84
  /** The ref compared against, or `null` when none resolved. */
57
85
  baseRef: string | null;
58
86
  /** Why the comparison was skipped, when it was. Printed, never silent. */
59
87
  note: string | null;
60
88
  /**
61
- * `false` means the bound could not be READ, not that it rose.
89
+ * `false` means the bounds could not be READ, not that one rose.
62
90
  *
63
91
  * Two refusals reach a caller with `ok: false` and they need opposite
64
- * actions: a risen ceiling is fixed by lowering the addition, an
65
- * unverifiable one by repairing the checkout. A completion review caught
66
- * both being rendered as "ROSE", which would send an operator to shrink a
67
- * rule over what was a fetch problem. Absent means verified — the only
68
- * `ok: false` that predates this field is a genuine rise.
92
+ * actions: a risen bound is fixed by lowering the addition, an unverifiable
93
+ * one by repairing the checkout. A completion review caught both being
94
+ * rendered as "ROSE", which would send an operator to shrink a rule over
95
+ * what was a fetch problem.
69
96
  */
70
97
  verified: boolean;
71
98
  }
72
99
 
100
+ /**
101
+ * The bound map, derived identically from head and base configs.
102
+ *
103
+ * ONE function for both sides, on purpose: two derivations of one boundary is
104
+ * the drift shape that let the grace ceiling rise twice under a sentence
105
+ * forbidding it. `budget` and `exceptions` are parsed JSON, or `null` when the
106
+ * file is absent — an absent exceptions ledger contributes no bounds, which is
107
+ * correct: there are no grants to protect.
108
+ */
109
+ export function boundsFrom(budget: unknown, exceptions: unknown): BoundSet | null {
110
+ const b = (budget ?? null) as Record<string, unknown> | null;
111
+ if (b === null) return null;
112
+ const baseline = Number(b['baseline_tokens']);
113
+ const headroom = Number(b['headroom_pct']);
114
+ if (!Number.isFinite(baseline) || !Number.isFinite(headroom)) return null;
115
+ // `design_ceiling` is the whole config-side bound now. The retained stored
116
+ // allowance stage 1 kept beside it was deleted in stage 2 (ADR-276), so a
117
+ // tree at or below design is bounded by this number and a tree above it is
118
+ // bounded by its own base measurement — neither of which lives in a field
119
+ // anyone can edit upward.
120
+ const out: BoundSet = { design_ceiling: Math.round(baseline * (1 + headroom / 100)) };
121
+
122
+ const ex = (exceptions ?? null) as Record<string, unknown> | null;
123
+ const list = ex === null ? null : ex['exceptions'];
124
+ if (Array.isArray(list)) {
125
+ for (const item of list) {
126
+ const e = (item ?? {}) as Record<string, unknown>;
127
+ const id = e['id'];
128
+ if (typeof id !== 'string' || id.trim() === '') continue;
129
+ const granted = Number(e['granted_tokens']);
130
+ const watermark = Number(e['watermark']);
131
+ if (Number.isFinite(granted)) out[`exception:${id}:granted_tokens`] = granted;
132
+ if (Number.isFinite(watermark)) out[`exception:${id}:watermark`] = watermark;
133
+ }
134
+ }
135
+ return out;
136
+ }
137
+
73
138
  function showAtRef(git: GitRunner, repoRoot: string, ref: string, relPath: string): string | null {
74
139
  const r = git(['show', `${ref}:${relPath}`], repoRoot);
75
140
  return r.ok ? r.stdout : null;
@@ -78,29 +143,24 @@ function showAtRef(git: GitRunner, repoRoot: string, ref: string, relPath: strin
78
143
  /**
79
144
  * A condition that stops the comparison: skip when advisory, refuse when not.
80
145
  *
81
- * One helper for all four causes rather than four inline branches, so the
82
- * posture cannot drift between them — the failure this closes was four
83
- * independent `ok: true` returns that all had to be found before any of them
84
- * could be trusted.
146
+ * One helper for all causes rather than an inline branch each, so the posture
147
+ * cannot drift between them — the failure this closes was four independent
148
+ * `ok: true` returns that all had to be found before any of them could be
149
+ * trusted.
85
150
  */
86
- function unverified(
87
- reason: string,
88
- requireBase: boolean,
89
- baseGraceCeiling: number | null,
90
- baseRef: string | null,
91
- ): BoundsRatchetVerdict {
151
+ function unverified(reason: string, requireBase: boolean, baseRef: string | null): BoundsRatchetVerdict {
92
152
  if (requireBase) {
93
153
  return {
94
154
  ok: false,
95
155
  verified: false,
96
- baseGraceCeiling,
156
+ baseBounds: null,
97
157
  baseRef,
98
- note: `${reason}, so the shrink-only bound was NOT verified in this run`,
158
+ note: `${reason}, so the shrink-only bounds were NOT verified in this run`,
99
159
  violations: [
100
- `the shrink-only bound could not be verified: ${reason}. This run is `
160
+ `the shrink-only bounds could not be verified: ${reason}. This run is `
101
161
  + 'ENFORCING, so an unverifiable bound refuses rather than skips — an '
102
162
  + 'unreadable base would otherwise grant budget on an infrastructure '
103
- + 'failure. This is NOT a ceiling rise: fix the checkout (a full fetch '
163
+ + 'failure. This is NOT a bound rise: fix the checkout (a full fetch '
104
164
  + 'of the base ref) rather than lowering the addition.',
105
165
  ],
106
166
  };
@@ -109,33 +169,29 @@ function unverified(
109
169
  ok: true,
110
170
  verified: false,
111
171
  violations: [],
112
- baseGraceCeiling,
172
+ baseBounds: null,
113
173
  baseRef,
114
- note: `${reason}, so the shrink-only bound was NOT verified in this run`,
174
+ note: `${reason}, so the shrink-only bounds were NOT verified in this run`,
115
175
  };
116
176
  }
117
177
 
118
178
  /**
119
- * Compare the grace ceiling in this tree against the one at the base ref.
179
+ * Compare every shrink-only bound in this tree against the one at the base ref.
120
180
  *
121
181
  * TWO POSTURES, AND WHICH ONE APPLIES IS THE CALLER'S TO DECLARE.
122
182
  *
123
- * Advisory (the default, and what every caller got before `requireBase`): a
124
- * base ref or base config that cannot be read SKIPS, reported in both the human
125
- * and JSON output so it cannot pass for a check. That default is not laziness
126
- * and the original reasoning still holds — a shallow clone, a first commit and
127
- * a detached build all legitimately have no base, and a gate that reds on them
128
- * gets switched off, which is how a repository loses the enforcement it just
129
- * built.
183
+ * Advisory (the default): a base ref or base config that cannot be read SKIPS,
184
+ * reported in both the human and JSON output so it cannot pass for a check.
185
+ * That default is not laziness and the original reasoning still holds — a
186
+ * shallow clone, a first commit and a detached build all legitimately have no
187
+ * base, and a gate that reds on them gets switched off, which is how a
188
+ * repository loses the enforcement it just built.
130
189
  *
131
190
  * Enforcing (`requireBase: true`): the same conditions REFUSE. An AI council
132
191
  * (2/2, 2026-09-10) made this blocking for the move to a base-measured ceiling,
133
- * and the reason is that the asymmetry above inverts once the base ref stops
134
- * being a cross-check and becomes the ceiling itself. Today an unreadable base
135
- * costs a comparison; there it would grant an unbounded budget on an
136
- * infrastructure failure — the exact fail-open the reserve decision refused.
137
- * anthropic's wording was "replaced or mode-gated"; this is the mode gate, so
138
- * that no existing caller silently changes behaviour.
192
+ * because the asymmetry inverts once the base ref stops being a cross-check and
193
+ * becomes the ceiling itself. Today an unreadable base costs a comparison;
194
+ * there it would grant an unbounded budget on an infrastructure failure.
139
195
  *
140
196
  * The flag is the caller's rather than derived from `GITHUB_ACTIONS` here: this
141
197
  * module cannot tell an enforcing CI run from a diagnostic one on the same
@@ -154,67 +210,124 @@ export function assertBoundsDidNotRise(opts: {
154
210
  */
155
211
  requireBase?: boolean;
156
212
  /**
157
- * The ceiling this run actually compares the payload against.
213
+ * The shrink-only numbers as they stand in THIS tree.
158
214
  *
159
- * The CI step passes `--ceiling <grace_ceiling>`, so the effective bound can
160
- * be raised either by editing the config or by passing a bigger number. Both
161
- * arrive here as one value, which closes the second path without a second
162
- * check.
215
+ * Passed in rather than re-read here so head and base go through
216
+ * `boundsFrom` from the caller's own configuration objects — including an
217
+ * effective value the caller may have taken from a flag rather than the
218
+ * file, which is the second path a bound could otherwise be widened by.
163
219
  */
164
- headGraceCeiling: number;
220
+ headBounds: BoundSet;
165
221
  }): BoundsRatchetVerdict {
166
222
  const git = opts.git ?? realGit;
167
223
  const requireBase = opts.requireBase === true;
168
224
  if (opts.baseRef === null || opts.baseRef.trim() === '') {
169
- return unverified('no base ref resolved', requireBase, null, null);
225
+ return unverified('no base ref resolved', requireBase, null);
170
226
  }
171
227
  const cfg = showAtRef(git, opts.repoRoot, opts.baseRef, BUDGET_CONFIG_PATH);
172
228
  if (cfg === null) {
173
- return unverified(
174
- `${BUDGET_CONFIG_PATH} could not be read at ${opts.baseRef}`,
175
- requireBase,
176
- null,
177
- opts.baseRef,
178
- );
229
+ return unverified(`${BUDGET_CONFIG_PATH} could not be read at ${opts.baseRef}`, requireBase, opts.baseRef);
179
230
  }
180
- let baseGrace: number | null = null;
231
+ let baseBudget: unknown;
181
232
  try {
182
- const parsed = JSON.parse(cfg) as Record<string, unknown>;
183
- const delivery = (parsed['ci_delivery'] ?? {}) as Record<string, unknown>;
184
- const n = Number(delivery['grace_ceiling']);
185
- baseGrace = Number.isFinite(n) ? n : null;
233
+ baseBudget = JSON.parse(cfg);
186
234
  } catch {
235
+ return unverified(`${BUDGET_CONFIG_PATH} at ${opts.baseRef} is not parseable`, requireBase, opts.baseRef);
236
+ }
237
+ // An absent exceptions ledger at the base ref is normal — it did not exist
238
+ // before this mechanism landed, and a ref predating it has no grants to
239
+ // protect. An unparseable one is not, and refuses like a bad budget.
240
+ const exRaw = showAtRef(git, opts.repoRoot, opts.baseRef, EXCEPTIONS_CONFIG_PATH);
241
+ let baseExceptions: unknown = null;
242
+ if (exRaw !== null) {
243
+ try {
244
+ baseExceptions = JSON.parse(exRaw);
245
+ } catch {
246
+ return unverified(
247
+ `${EXCEPTIONS_CONFIG_PATH} at ${opts.baseRef} is not parseable`,
248
+ requireBase,
249
+ opts.baseRef,
250
+ );
251
+ }
252
+ }
253
+
254
+ const baseBounds = boundsFrom(baseBudget, baseExceptions);
255
+ if (baseBounds === null) {
187
256
  return unverified(
188
- `${BUDGET_CONFIG_PATH} at ${opts.baseRef} is not parseable`,
257
+ `there are no shrink-only bounds at ${opts.baseRef} to ratchet against`,
189
258
  requireBase,
190
- null,
191
259
  opts.baseRef,
192
260
  );
193
261
  }
194
- if (baseGrace === null) {
195
- return unverified(
196
- `there is no ci_delivery.grace_ceiling at ${opts.baseRef} to ratchet against`,
197
- requireBase,
198
- null,
199
- opts.baseRef,
262
+
263
+ const violations: string[] = [];
264
+ for (const [name, head] of Object.entries(opts.headBounds)) {
265
+ const base = baseBounds[name];
266
+ // A bound absent at the base is a NEW one — a freshly recorded grant,
267
+ // which is the break-glass path working rather than a rise.
268
+ if (base === undefined) continue;
269
+ if (head > base) violations.push(explain(name, base, head, opts.baseRef));
270
+ }
271
+
272
+ // DELETION CANNOT DISCHARGE DEBT. A grant that existed at the base and is
273
+ // gone at head was not repaid, it was erased — the 2026-09-11 council's
274
+ // blocking finding #3, and the one failure a shrink-only comparison misses
275
+ // by construction, because a removed number is not a smaller number.
276
+ // Only `exception:` bounds are checked for deletion: `design_ceiling` is
277
+ // always present, and the stored allowance that used to need an exemption
278
+ // here no longer exists.
279
+ const reportedDeletions = new Set<string>();
280
+ for (const name of Object.keys(baseBounds)) {
281
+ if (!name.startsWith('exception:')) continue;
282
+ if (opts.headBounds[name] !== undefined) continue;
283
+ const m = /^exception:(.+):(?:granted_tokens|watermark)$/.exec(name);
284
+ const id = m === null ? name : String(m[1]);
285
+ // One record carries two bounds; report the record once.
286
+ if (reportedDeletions.has(id)) continue;
287
+ reportedDeletions.add(id);
288
+ violations.push(
289
+ `standing-payload exception '${id}' existed at ${opts.baseRef} and is gone at head. A deleted ` +
290
+ 'record is not a repaid one: repayment is established by the payload returning to the ' +
291
+ 'watermark, and erasing the entry erases the debt along with the evidence of it. Mark it ' +
292
+ 'repaid once the measurement supports that, or leave it in place.',
200
293
  );
201
294
  }
202
- if (opts.headGraceCeiling > baseGrace) {
203
- return {
204
- ok: false,
205
- verified: true,
206
- baseGraceCeiling: baseGrace,
207
- baseRef: opts.baseRef,
208
- note: null,
209
- violations: [
210
- `the standing-payload grace ceiling rose from ${String(baseGrace)} to ` +
211
- `${String(opts.headGraceCeiling)} against ${opts.baseRef}. ` +
212
- `${BUDGET_CONFIG_PATH} says it may not move upward and ADR-264 decided that ` +
213
- 'the sentence stands: standing-rule growth needs a compensating reduction ' +
214
- 'elsewhere, not a bigger ceiling. Lower the addition, migrate the prose out of ' +
215
- 'the standing rule, or reduce another standing asset by the same amount.',
216
- ],
217
- };
295
+
296
+ if (violations.length > 0) {
297
+ return { ok: false, verified: true, baseBounds, baseRef: opts.baseRef, note: null, violations };
298
+ }
299
+ return { ok: true, verified: true, violations: [], baseBounds, baseRef: opts.baseRef, note: null };
300
+ }
301
+
302
+ /** One refusal message per bound kind — a generic "a bound rose" sends the
303
+ * reader nowhere, and the three kinds have three different fixes. */
304
+ function explain(name: string, base: number, head: number, baseRef: string): string {
305
+ const rose = `rose from ${String(base)} to ${String(head)} against ${baseRef}`;
306
+ if (name === 'design_ceiling') {
307
+ return (
308
+ `the standing-payload design ceiling ${rose}. It derives from baseline_tokens × ` +
309
+ `headroom_pct in ${BUDGET_CONFIG_PATH}, and it is the FLOOR of the measured ceiling, so ` +
310
+ 'raising it widens every future budget without adding a token of payload. ADR-264 decided ' +
311
+ 'that standing-rule growth needs a compensating reduction elsewhere, not a bigger ceiling. ' +
312
+ 'Lower the addition, migrate the prose out of the standing rule, or reduce another standing ' +
313
+ 'asset by the same amount.'
314
+ );
315
+ }
316
+ const m = /^exception:(.+):(granted_tokens|watermark)$/.exec(name);
317
+ if (m !== null && m[2] === 'granted_tokens') {
318
+ return (
319
+ `standing-payload exception '${String(m[1])}' had its grant ${rose}. Enlarging a grant that ` +
320
+ 'already exists is a ceiling raise wearing a break-glass label. Record a NEW approved, dated ' +
321
+ 'exception instead, so the enlargement carries its own approver, reason and expiry.'
322
+ );
323
+ }
324
+ if (m !== null) {
325
+ return (
326
+ `standing-payload exception '${String(m[1])}' had its watermark ${rose}. The watermark pins the ` +
327
+ 'base while the grant is live, and raising it launders the granted tokens into the next base — ' +
328
+ 'which is exactly the failure the watermark exists to prevent. It is the payload measured ' +
329
+ 'BEFORE the grant and does not change afterwards.'
330
+ );
218
331
  }
219
- return { ok: true, verified: true, violations: [], baseGraceCeiling: baseGrace, baseRef: opts.baseRef, note: null };
332
+ return `the shrink-only bound '${name}' ${rose}, and ${BUDGET_CONFIG_PATH} says it may not move upward.`;
220
333
  }
@@ -41,15 +41,21 @@ import * as path from 'node:path';
41
41
  import { read_lockfile } from './installed_lock.js';
42
42
 
43
43
  /**
44
- * The four detectors, in the order the gate runs them.
44
+ * The five detectors, in the order the gate runs them.
45
45
  *
46
46
  * The roadmap's § 0 names three (A promissory, B language, C verification).
47
47
  * That was true of the draft and is not true of the tree: detector D
48
- * (`completion`) landed under round 7 § Phase 1 and runs in the same detector
49
- * list as the other three. Counting three would silently drop a detector's
48
+ * (`completion`) landed under round 7 § Phase 1, detector E
49
+ * (`pending-decision`) under road-to-a-question-that-survives-the-turn, and
50
+ * detector F (`untested`) on 2026-09-11 — all of which run in the same detector
51
+ * list as the first three. Counting three would silently drop a detector's
50
52
  * refusals, so the set is read off `DetectorId` in the gate rather than off the
51
53
  * prose.
52
54
  *
55
+ * F is the one that asks whether a change is TESTED rather than whether
56
+ * something ran: C is satisfied by `eslint`, which is how a feature with zero
57
+ * test lines passed every guard in that file.
58
+ *
53
59
  * This said "the same UNCONDITIONAL list" until 2026-08-18, which was wrong and
54
60
  * is worth naming rather than quietly rewording: `main()` runs A and D only when
55
61
  * no subagent dispatch is open. It does not change this set — every detector
@@ -63,6 +69,8 @@ export const DETECTOR_IDS = [
63
69
  'language',
64
70
  'verification',
65
71
  'completion',
72
+ 'pending-decision',
73
+ 'untested',
66
74
  ] as const;
67
75
 
68
76
  export type RefusalDetectorId = (typeof DETECTOR_IDS)[number];
@@ -138,10 +146,39 @@ export interface RefusalRecord {
138
146
  counts?: Partial<DetectorCounts>;
139
147
  /** Package version recorded at the most recent refusal, when readable. */
140
148
  agent_config_version?: string;
149
+ /**
150
+ * The host's own id for the prompt being processed at the most recent
151
+ * refusal, when the payload carries one.
152
+ *
153
+ * Recorded BESIDE `refused_turn`, never instead of it. The ordinal stays the
154
+ * re-entrancy key — its drift history is why both guard layers exist, and
155
+ * swapping the key of a wedge-critical guard on an axis nothing measures is
156
+ * a behaviour change dressed as a correction. What the pair buys is the
157
+ * measurement: two refusals carrying the same `refused_prompt_id` and
158
+ * different `refused_turn` are one prompt whose ordinal drifted, which is
159
+ * the failure this file's own header describes and which the ordinal alone
160
+ * cannot distinguish from two genuine turns.
161
+ */
162
+ refused_prompt_id?: string;
141
163
  }
142
164
 
165
+ /**
166
+ * A zeroed counter per detector, DERIVED from `DETECTOR_IDS` rather than
167
+ * written out.
168
+ *
169
+ * It was an object literal of four keys until detector E landed, and the
170
+ * literal is what made adding a detector a silent arithmetic defect rather than
171
+ * a type error: `DetectorCounts` is `Record<RefusalDetectorId, number>`, the
172
+ * literal satisfied it for the union of the day, and a fifth id then read back
173
+ * as `undefined` — so every `+=` over it produced `NaN` and the whole rollup
174
+ * went quiet. The sibling test asserts that the id list and the gate's union
175
+ * agree; nothing asserted that the COUNTER covered the list, which is the half
176
+ * this construction removes the need to assert.
177
+ */
143
178
  export function emptyCounts(): DetectorCounts {
144
- return { promissory: 0, language: 0, verification: 0, completion: 0 };
179
+ const out = {} as DetectorCounts;
180
+ for (const id of DETECTOR_IDS) out[id] = 0;
181
+ return out;
145
182
  }
146
183
 
147
184
  function isDetector(v: unknown): v is RefusalDetectorId {
@@ -174,6 +211,9 @@ export function parseRecord(raw: string): RefusalRecord | null {
174
211
  if (typeof o['agent_config_version'] === 'string') {
175
212
  rec.agent_config_version = o['agent_config_version'];
176
213
  }
214
+ if (typeof o['refused_prompt_id'] === 'string') {
215
+ rec.refused_prompt_id = o['refused_prompt_id'];
216
+ }
177
217
  const counts = o['counts'];
178
218
  if (typeof counts === 'object' && counts !== null && !Array.isArray(counts)) {
179
219
  const parsed: Partial<DetectorCounts> = {};
@@ -231,6 +271,7 @@ export function foldRefusal(
231
271
  turnOrdinal: number;
232
272
  at: string;
233
273
  version?: string | undefined;
274
+ promptId?: string | undefined;
234
275
  },
235
276
  ): RefusalRecord {
236
277
  const counts = prev === null ? emptyCounts() : countsOf(prev);
@@ -247,6 +288,12 @@ export function foldRefusal(
247
288
  else if (prev?.agent_config_version !== undefined) {
248
289
  rec.agent_config_version = prev.agent_config_version;
249
290
  }
291
+ // Not inherited from `prev` when this refusal carries none: the field
292
+ // describes THIS refusal's prompt, and carrying a previous one forward would
293
+ // read as an identity the payload never supplied.
294
+ if (input.promptId !== undefined && input.promptId !== '') {
295
+ rec.refused_prompt_id = input.promptId;
296
+ }
250
297
  return rec;
251
298
  }
252
299
 
@@ -42,6 +42,12 @@ const KEEP_RE = /^keep-beta-until:\s*(\d{4}-\d{2}-\d{2})\s*$/m;
42
42
  * - a lapsed contract **in** the baseline -> warning (inherited debt)
43
43
  * - a lapsed contract **not** in it -> ERROR (a fresh lapse, today)
44
44
  *
45
+ * SUPERSEDED 2026-09-11 on the second line: a fresh lapse is a warning too, and
46
+ * the reason is at {@link LAPSED_SEVERITY_FRESH}. The council's choice of a
47
+ * ratchet over a flat error stands as the record of what was decided then; what
48
+ * changed is the owner's ruling that no date may red CI by itself. The baseline
49
+ * therefore labels rather than gates.
50
+ *
45
51
  * So new work is enforced immediately while the 86-contract cohort does not red
46
52
  * an arbitrary future PR whose author caused none of it. The cohort is real:
47
53
  * 44 of the 86 lapsed on the same day and 64 within four days, which is one past
@@ -116,7 +122,31 @@ export function _resetLapsedBaseline(): void {
116
122
  * whether this becomes an error is that roadmap's step 0.2 and is not taken here.
117
123
  */
118
124
  const LAPSED_SEVERITY_IN_BASELINE: 'error' | 'warning' = 'warning';
119
- const LAPSED_SEVERITY_FRESH: 'error' | 'warning' = 'error';
125
+ /**
126
+ * A fresh lapse is a WARNING since 2026-09-11, by owner decision.
127
+ *
128
+ * It was an error, and the error said: this contract must still be beta,
129
+ * because a date passed. That is the one thing a date cannot establish.
130
+ * Stability is decided on observable criteria — active enforcement, consumer
131
+ * reliance, no known pending incompatible change — and none of them contains an
132
+ * elapsed-time term. `keep-beta-until` SCHEDULES A REVIEW. It does not prohibit
133
+ * promotion before that date, does not require continued beta after it, and
134
+ * must not turn CI red on its own.
135
+ *
136
+ * What this gate still errors on is unchanged and is the part that was never
137
+ * about time: a beta contract declaring NO disposition at all, one declaring
138
+ * two, and a window set further out than the maximum — a contract that says
139
+ * nothing about its own maturity, or parks the question indefinitely. Those are
140
+ * statements about missing or contradictory evidence, which CI may absolutely
141
+ * refuse. "The window expired" is not one of them.
142
+ *
143
+ * The inherited/fresh distinction stays in the REPORT because it still tells a
144
+ * reader which lapses are cohort debt and which arrived since. It no longer
145
+ * gates anything, so the frozen baseline is now a label rather than a ratchet;
146
+ * retiring it is the remaining step, recorded in
147
+ * `stubs/road-to-evidence-driven-stability.md`.
148
+ */
149
+ const LAPSED_SEVERITY_FRESH: 'error' | 'warning' = 'warning';
120
150
  const SUPERSEDED_RE = /^superseded-by:\s*\S+\s*$/m;
121
151
 
122
152
  const MAX_REVIEW_WINDOW_DAYS = 90;
@@ -569,9 +599,10 @@ function main(): number {
569
599
  }
570
600
  if (upcoming.length > 0) {
571
601
  process.stdout.write(
572
- `\nUpcoming FRESH lapses within ${String(args.horizon)} day(s) — advisory, ` +
573
- 'exit code unchanged. These are absent from the frozen baseline, so each\n' +
574
- 'becomes an ERROR on its date rather than an inherited warning:\n',
602
+ `\nReviews falling due within ${String(args.horizon)} day(s) — advisory, ` +
603
+ 'exit code unchanged, and it stays unchanged when the date passes:\n' +
604
+ 'the window schedules a maturity DECISION, it does not require continued\n' +
605
+ 'beta and it never reds CI on its own. Evidence decides; the date only asks:\n',
575
606
  );
576
607
  for (const u of upcoming) {
577
608
  process.stdout.write(