@event4u/agent-config 14.17.0 → 14.18.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 (74) 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 +170 -79
  5. package/README.md +1 -1
  6. package/dist/agent-src/commands/pr/merge.md +35 -44
  7. package/dist/agent-src/skills/accessibility-auditor/SKILL.md +2 -0
  8. package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +27 -1
  9. package/dist/agent-src/skills/code-intelligence/SKILL.md +60 -12
  10. package/dist/agent-src/skills/complexity-first-planning/SKILL.md +27 -0
  11. package/dist/agent-src/skills/design-review/SKILL.md +2 -0
  12. package/dist/agent-src/skills/feature-planning/SKILL.md +34 -0
  13. package/dist/agent-src/skills/git-workflow/SKILL.md +37 -12
  14. package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +176 -0
  15. package/dist/agent-src/skills/laravel-mail/SKILL.md +44 -0
  16. package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +40 -0
  17. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +12 -0
  18. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +53 -8
  19. package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +3 -1
  20. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  21. package/dist/cli-delegate/cmd_conformance.js +335 -659
  22. package/dist/discovery/deprecation-report.md +1 -1
  23. package/dist/discovery/discovery-manifest.json +15 -15
  24. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  25. package/dist/discovery/discovery-manifest.summary.md +1 -1
  26. package/dist/discovery/dormancy-report.md +1 -1
  27. package/dist/discovery/orphan-report.md +1 -1
  28. package/dist/discovery/packs.json +2 -2
  29. package/dist/discovery/trust-report.md +1 -1
  30. package/dist/discovery/workspaces.json +2 -2
  31. package/dist/hooks/dispatch.js +959 -1304
  32. package/dist/mcp/registry-manifest.json +2 -2
  33. package/dist/mcp/server.json +2 -2
  34. package/dist/mcp/server.mjs +671 -1002
  35. package/dist/shared/settingsConsent.js +1 -1
  36. package/package.json +1 -1
  37. package/src/config/conformance-claim-baseline.json +9 -0
  38. package/src/config/gate-violation-baselines.json +3 -3
  39. package/src/config/release-gate-locality.yml +129 -0
  40. package/src/scripts/_lib/release_highlights.ts +172 -1
  41. package/src/scripts/_lib/release_material.ts +66 -0
  42. package/src/scripts/_lib/repeated_run.ts +77 -0
  43. package/src/scripts/_lib/tamper_vocabulary.ts +242 -0
  44. package/src/scripts/_lib/unified_diff.ts +121 -0
  45. package/src/scripts/check_finding_dispositions.ts +179 -3
  46. package/src/scripts/check_installed_hooks_fresh.ts +381 -0
  47. package/src/scripts/check_release_highlights.ts +25 -13
  48. package/src/scripts/check_suppression_hygiene.ts +11 -0
  49. package/src/scripts/code_graph/build.ts +230 -33
  50. package/src/scripts/code_graph/extract.ts +168 -33
  51. package/src/scripts/code_graph/types.ts +37 -2
  52. package/src/scripts/code_graph/validate.ts +12 -1
  53. package/src/scripts/conformance_scan.ts +2 -2
  54. package/src/scripts/detect_silent_catch.ts +403 -0
  55. package/src/scripts/detect_verification_tampering.ts +538 -0
  56. package/src/scripts/git_authorization_hook.ts +153 -17
  57. package/src/scripts/hook_manifest.json +1 -1
  58. package/src/scripts/hook_manifest.yaml +33 -19
  59. package/src/scripts/hooks/block_no_verify.ts +3 -3
  60. package/src/scripts/hooks/concern_registry.ts +2 -2
  61. package/src/scripts/hooks/evidence_independence.ts +21 -2
  62. package/src/scripts/hooks/{block_unauthorized_git.ts → git_command_classifier.ts} +25 -369
  63. package/src/scripts/hooks/push_settle_hook.ts +250 -0
  64. package/src/scripts/hooks/ship_diff_volume_hook.ts +1 -1
  65. package/src/scripts/install-hooks.sh +134 -1
  66. package/src/scripts/language_mirror_hook.ts +27 -5
  67. package/src/scripts/release.ts +31 -143
  68. package/src/scripts/release_drill.ts +71 -1
  69. package/src/scripts/release_publication.ts +243 -3
  70. package/src/scripts/release_verify.ts +220 -0
  71. package/src/scripts/schemas/review-findings.schema.json +1 -1
  72. package/src/scripts/schemas/skill.schema.json +11 -1
  73. package/src/scripts/self_review_gate.ts +10 -1
  74. package/src/shared/settingsConsent.ts +1 -1
@@ -0,0 +1,381 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Is the INSTALLED copy of this repository's git hooks still what the installer
4
+ * would write?
5
+ *
6
+ * WHY. `src/scripts/install-hooks.sh` writes six hooks into the shared
7
+ * `.git/hooks` directory. Afterwards it runs only on the package manager's
8
+ * `prepare` lifecycle step (git clones only) or when a human runs
9
+ * `task install-hooks`. Between those two events the installed copy drifts from
10
+ * the source that writes it, silently and without bound. Measured in this
11
+ * repository on 2026-09-05, before this gate existed: the installed `pre-push`
12
+ * was 146 lines against a 189-line source body, and the 43 missing lines were
13
+ * the entire base-freshness gate merged five days earlier. In the source, in
14
+ * CI, and in the skill documenting it, that gate read as live. On the checkout
15
+ * that actually runs it, it did not exist.
16
+ *
17
+ * That is the general shape of an INSTALLED copy of a generated file: the
18
+ * source is gated, the copy is not, and the copy is what runs.
19
+ *
20
+ * HOW. Not by re-parsing the heredocs. `install-hooks.sh` writes `post-merge`
21
+ * and `post-checkout` as a heredoc PLUS an appended auto-sync block, and it
22
+ * interpolates the hook name into both — so no slice of the source file equals
23
+ * a hook body, and a comparison built on slicing would be wrong for two of six
24
+ * hooks and brittle for the rest. Instead the gate runs the real installer with
25
+ * `AGENT_CONFIG_HOOKS_DIR` pointed at a scratch directory and byte-compares
26
+ * what it produced against what is installed. Anything the installer does
27
+ * deterministically — interpolation, appends, future additions — compares equal
28
+ * by construction, which is what keeps this from being the too-strict check its
29
+ * roadmap's risk register warned about.
30
+ *
31
+ * WHERE IT IS BOUND. Two carriers, and NEITHER REFUSES. The pre-push hook body
32
+ * calls it after the base-freshness gate, advisory (silence it with
33
+ * `AGENT_CONFIG_SKIP_PREPUSH_HOOKFRESH=1`); the `post-merge` / `post-checkout`
34
+ * auto-sync block calls it at the event that CAUSES the staleness. The
35
+ * predicate is "installed == what THIS checkout renders", and `.git/hooks` is
36
+ * shared by every linked worktree, so it has no unique referent and a refusal
37
+ * would fire on ordinary parallel work — council 2026-09-05, 2 of 2 seats.
38
+ *
39
+ * WHAT THE EVENT CARRIER DOES NOT REACH. `git pull --rebase` fires neither
40
+ * `post-merge` nor `post-checkout` — it fires `post-rewrite`, which carries no
41
+ * detector. A rebase-pull that moves the installer is therefore reported at the
42
+ * next push and not before. Named rather than closed: `post-rewrite` also fires
43
+ * on every `git commit --amend`, so wiring it there trades a real gap for a
44
+ * notice on an operation that cannot have moved the installer.
45
+ *
46
+ * WHY NOT IN CI. Deliberately NOT in `task ci` or `task preflight`: CI cannot
47
+ * observe a contributor's `.git/hooks`, so a CI-registered run would scan
48
+ * nothing and report a green that means "no data" — the vacuous-pass class this
49
+ * repository audited in 2026-07. Its correctness is covered by
50
+ * `tests/scripts/check_installed_hooks_fresh.test.ts`, which does run in CI,
51
+ * and which asserts BOTH directions.
52
+ *
53
+ * EMPTY IS A REAL STATE. A checkout that never ran the installer has no managed
54
+ * hook to compare. That exits 0 and says so in one line, rather than pretending
55
+ * to have checked something.
56
+ *
57
+ * CLI contract: exit 0 = every installed managed hook matches, or none is
58
+ * installed; 1 = at least one is missing or differs; 2 = the installer could
59
+ * not be rendered (usage error, or the installer itself failed).
60
+ *
61
+ * Usage:
62
+ * ./scripts-run src/scripts/check_installed_hooks_fresh
63
+ * ./scripts-run src/scripts/check_installed_hooks_fresh --quiet
64
+ * ./scripts-run src/scripts/check_installed_hooks_fresh --hooks-dir <path>
65
+ */
66
+ // ledger-exempt: this gate has one target — the installed hook set — and it
67
+ // reports every managed hook by name with a fingerprint on both the clean and
68
+ // the failing path, so per-target accounting is already the whole output.
69
+
70
+ import { spawnSync } from 'node:child_process';
71
+ import * as crypto from 'node:crypto';
72
+ import * as fs from 'node:fs';
73
+ import * as os from 'node:os';
74
+ import * as path from 'node:path';
75
+ import process from 'node:process';
76
+ import { fileURLToPath, pathToFileURL } from 'node:url';
77
+
78
+ import { DeadScopeError, reportScanned } from './_lib/scan_scope.js';
79
+
80
+ const _HERE = fileURLToPath(import.meta.url);
81
+ const REPO = path.resolve(path.dirname(_HERE), '..', '..');
82
+ const INSTALLER_REL = 'src/scripts/install-hooks.sh';
83
+ const FIX = 'task install-hooks';
84
+ const SKIP_ENV = 'AGENT_CONFIG_SKIP_PREPUSH_HOOKFRESH';
85
+ // Both child processes run on the push path. Unbounded, a wedged `git` or an
86
+ // unwritable TMPDIR leaves the push hanging with no diagnostic, and the
87
+ // "could not render" branch is only reached if the child returns at all.
88
+ const GIT_TIMEOUT_MS = 5_000;
89
+ const INSTALLER_TIMEOUT_MS = 30_000;
90
+
91
+ export interface HookVerdict {
92
+ name: string;
93
+ /**
94
+ * `not-executable` is its own state rather than a flavour of `differs`:
95
+ * git SKIPS a hook without the executable bit, so a byte-identical file
96
+ * that lost +x is exactly the installed-but-inert case this gate exists
97
+ * to find, and it needs a different sentence than "the content drifted".
98
+ * `unreadable` is separated from `missing` for the same reason — EACCES
99
+ * and ENOENT need different fixes (#7).
100
+ */
101
+ state: 'match' | 'differs' | 'missing' | 'not-executable' | 'unreadable';
102
+ expectedSha: string;
103
+ installedSha: string | null;
104
+ /** Error code when `state` is `unreadable`. */
105
+ readError?: string;
106
+ }
107
+
108
+ export interface FreshnessReport {
109
+ hooksDir: string;
110
+ /** Managed hooks the installer would write. */
111
+ verdicts: HookVerdict[];
112
+ /** True when NONE of the managed hooks is installed — a fresh clone. */
113
+ neverInstalled: boolean;
114
+ }
115
+
116
+ function sha(buf: Buffer): string {
117
+ return crypto.createHash('sha256').update(buf).digest('hex').slice(0, 12);
118
+ }
119
+
120
+ /**
121
+ * The hooks directory git will actually execute from.
122
+ *
123
+ * `--git-path hooks` rather than `--git-common-dir` + "/hooks": the two agree
124
+ * on the ordinary case AND on a linked worktree (verified here — from a
125
+ * worktree both resolve to the SHARED common-dir hooks, which is the property
126
+ * the whole shared-state argument rests on), and they diverge when
127
+ * `core.hooksPath` is set, where only `--git-path` follows it. Composing the
128
+ * path by hand would compare a directory git never runs and report a match over
129
+ * hooks that are unrelated — the vacuous green this gate exists to refuse.
130
+ *
131
+ * Honest limit: the `core.hooksPath` branch is git-documented and NOT exercised
132
+ * here. This repository's own `block-no-verify` guard refuses any command
133
+ * carrying `core.hooksPath`, read-only probes included, so it cannot be tested
134
+ * from inside a session. The worktree and default cases are verified.
135
+ */
136
+ export function resolveHooksDir(repoRoot: string): string {
137
+ const res = spawnSync('git', ['-C', repoRoot, 'rev-parse', '--git-path', 'hooks'], {
138
+ encoding: 'utf-8',
139
+ timeout: GIT_TIMEOUT_MS,
140
+ });
141
+ const out = (res.stdout ?? '').trim();
142
+ if (res.status !== 0 || out === '') return path.join(repoRoot, '.git', 'hooks');
143
+ return path.isAbsolute(out) ? out : path.join(repoRoot, out);
144
+ }
145
+
146
+ /**
147
+ * Render every hook the installer would write, into a scratch directory.
148
+ *
149
+ * Shells out to the real installer rather than reimplementing it: an
150
+ * independent renderer would be a second source of truth, and the drift between
151
+ * the two would be invisible in exactly the way this gate exists to catch.
152
+ */
153
+ export function renderExpected(repoRoot: string): Map<string, Buffer> {
154
+ const stage = fs.mkdtempSync(path.join(os.tmpdir(), 'hookfresh-'));
155
+ // The seam FAILS OPEN by construction: install-hooks.sh treats an empty or
156
+ // absent AGENT_CONFIG_HOOKS_DIR as "install normally", so a redirect that
157
+ // does not take effect turns this read-only inspection into a real install
158
+ // over the pre-push that is currently executing it.
159
+ //
160
+ // THIS CHECK IS THE WEAKER HALF, and saying so matters on a path whose
161
+ // stated risk is that overwrite. It inspects `stage` — a value mkdtempSync
162
+ // produced two lines up — and therefore catches a caller that supplies an
163
+ // unusable path. It CANNOT observe whether the variable reached the child.
164
+ // The defence that can is the installer's own refusal of a set-but-empty
165
+ // value (install-hooks.sh), which is where the fall-through happens.
166
+ if (!path.isAbsolute(stage) || stage.trim() === '') {
167
+ throw new Error(
168
+ `refusing to run the installer: scratch dir ${JSON.stringify(stage)} is not an absolute path, ` +
169
+ 'and an unredirected run would overwrite the real .git/hooks.',
170
+ );
171
+ }
172
+ try {
173
+ const res = spawnSync('bash', [path.join(repoRoot, INSTALLER_REL)], {
174
+ cwd: repoRoot,
175
+ encoding: 'buffer',
176
+ timeout: INSTALLER_TIMEOUT_MS,
177
+ env: { ...process.env, AGENT_CONFIG_HOOKS_DIR: stage },
178
+ });
179
+ if (res.error !== undefined) {
180
+ throw new Error(`${INSTALLER_REL} did not complete: ${res.error.message}`);
181
+ }
182
+ if (res.status !== 0) {
183
+ const err = res.stderr ? res.stderr.toString('utf-8') : '';
184
+ throw new Error(`${INSTALLER_REL} exited ${String(res.status)}\n${err}`);
185
+ }
186
+ const out = new Map<string, Buffer>();
187
+ for (const name of fs.readdirSync(stage).sort()) {
188
+ out.set(name, fs.readFileSync(path.join(stage, name)));
189
+ }
190
+ return out;
191
+ } finally {
192
+ fs.rmSync(stage, { recursive: true, force: true });
193
+ }
194
+ }
195
+
196
+ export function inspect(repoRoot: string, hooksDirOverride?: string): FreshnessReport {
197
+ const hooksDir = hooksDirOverride ?? resolveHooksDir(repoRoot);
198
+ const expected = renderExpected(repoRoot);
199
+ if (expected.size === 0) {
200
+ // The installer produced nothing — it cannot have been read correctly.
201
+ throw new DeadScopeError(
202
+ 'check_installed_hooks_fresh',
203
+ `${INSTALLER_REL} wrote no hooks — the installer, not the installed copy, is the problem.`,
204
+ );
205
+ }
206
+ const verdicts: HookVerdict[] = [];
207
+ for (const [name, body] of expected) {
208
+ const target = path.join(hooksDir, name);
209
+ let installed: Buffer | null = null;
210
+ let executable = false;
211
+ let readError: string | undefined;
212
+ try {
213
+ installed = fs.readFileSync(target);
214
+ // The installer chmod +x's every hook. Git silently skips one that
215
+ // is not executable, so mode is part of what "installed" means.
216
+ //
217
+ // POSIX only. Windows reports 0o666 / 0o444 and has no executable
218
+ // bit, so every hook would resolve `not-executable` and the gate
219
+ // would exit 1 forever — a permanent false red on the platform the
220
+ // test suite explicitly accommodates.
221
+ executable =
222
+ process.platform === 'win32' || (fs.statSync(target).mode & 0o111) !== 0;
223
+ } catch (e) {
224
+ const code = (e as NodeJS.ErrnoException).code ?? 'UNKNOWN';
225
+ installed = null;
226
+ // ENOENT is absence; anything else is a file that exists and could
227
+ // not be read, which a re-install will not fix on its own.
228
+ if (code !== 'ENOENT') readError = code;
229
+ }
230
+ const state: HookVerdict['state'] =
231
+ installed === null
232
+ ? readError === undefined
233
+ ? 'missing'
234
+ : 'unreadable'
235
+ : !installed.equals(body)
236
+ ? 'differs'
237
+ : executable
238
+ ? 'match'
239
+ : 'not-executable';
240
+ verdicts.push({
241
+ name,
242
+ state,
243
+ expectedSha: sha(body),
244
+ installedSha: installed === null ? null : sha(installed),
245
+ ...(readError === undefined ? {} : { readError }),
246
+ });
247
+ }
248
+ return {
249
+ hooksDir,
250
+ verdicts,
251
+ // "Never installed" means nothing is THERE. An unreadable hook is
252
+ // there, so it must not be swallowed by the fresh-clone branch.
253
+ neverInstalled: verdicts.every((v) => v.state === 'missing'),
254
+ };
255
+ }
256
+
257
+ function render(report: FreshnessReport, quiet: boolean, write: (s: string) => void): number {
258
+ // Outside the quiet guard on purpose: CI-shaped callers pass --quiet, and a
259
+ // gate that reports no count there reads as silent.
260
+ const collect = ((chunk: string | Uint8Array): boolean => {
261
+ write(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf-8'));
262
+ return true;
263
+ }) as typeof process.stdout.write;
264
+ reportScanned(
265
+ {
266
+ gate: 'check_installed_hooks_fresh',
267
+ scanned: report.verdicts.length,
268
+ units: 'managed hook(s)',
269
+ roots: [INSTALLER_REL],
270
+ },
271
+ collect,
272
+ );
273
+
274
+ if (report.neverInstalled) {
275
+ write(
276
+ `✅ check_installed_hooks_fresh: no managed hook is installed at ${report.hooksDir} — ` +
277
+ `nothing to compare. Run '${FIX}' to install them.\n`,
278
+ );
279
+ return 0;
280
+ }
281
+
282
+ const stale = report.verdicts.filter((v) => v.state !== 'match');
283
+ if (stale.length === 0) {
284
+ if (!quiet) {
285
+ for (const v of report.verdicts) {
286
+ write(` ${v.name} ${v.expectedSha}\n`);
287
+ }
288
+ }
289
+ write(
290
+ `✅ check_installed_hooks_fresh: ${String(report.verdicts.length)} installed hook(s) match ${INSTALLER_REL}.\n`,
291
+ );
292
+ return 0;
293
+ }
294
+
295
+ write(`⚠️ check_installed_hooks_fresh: .git/hooks does not match this checkout.\n`);
296
+ write(` hooks dir: ${report.hooksDir} (shared by every linked worktree)\n`);
297
+ write(` worktree: ${REPO}\n`);
298
+ for (const v of stale) {
299
+ switch (v.state) {
300
+ case 'missing':
301
+ write(` ${v.name} MISSING (this checkout renders ${v.expectedSha})\n`);
302
+ break;
303
+ case 'unreadable':
304
+ write(` ${v.name} UNREADABLE (${String(v.readError)}) — exists but cannot be read\n`);
305
+ break;
306
+ case 'not-executable':
307
+ write(` ${v.name} NOT EXECUTABLE — byte-identical, and git skips it\n`);
308
+ break;
309
+ default:
310
+ write(
311
+ ` ${v.name} installed ${String(v.installedSha)} ≠ this checkout ${v.expectedSha}\n`,
312
+ );
313
+ }
314
+ }
315
+ // Deliberately NOT "run the installer". A mismatch proves difference, never
316
+ // that THIS checkout is the newer side: linked worktrees share one hooks
317
+ // directory, so on a sibling-worktree mismatch a re-install only moves the
318
+ // mismatch to the other worktree, and on a behind-base checkout it writes
319
+ // the OLDER hook set over the shared one.
320
+ write(`\n A mismatch does not by itself say which side is newer.\n`);
321
+ write(` · behind the base? merge it first — the installer moves with it\n`);
322
+ write(` · sibling worktree on a different branch? re-installing only moves the mismatch\n`);
323
+ write(` · otherwise ${FIX}\n`);
324
+ write(` Gates the installed hooks are missing are inert on this checkout.\n`);
325
+ write(` Silence this notice with ${SKIP_ENV}=1.\n`);
326
+ return 1;
327
+ }
328
+
329
+ export function main(argv: readonly string[] = process.argv.slice(2)): number {
330
+ let quiet = false;
331
+ let hooksDir: string | undefined;
332
+ for (let i = 0; i < argv.length; i += 1) {
333
+ const a = argv[i];
334
+ if (a === '--quiet') quiet = true;
335
+ else if (a === '-h' || a === '--help') {
336
+ process.stdout.write(
337
+ 'usage: check_installed_hooks_fresh [--quiet] [--hooks-dir PATH]\n',
338
+ );
339
+ return 0;
340
+ } else if (a === '--hooks-dir') {
341
+ i += 1;
342
+ const next = argv[i];
343
+ // `--hooks-dir --quiet` must be a usage error, not a hooks
344
+ // directory literally named "--quiet" — which would report every
345
+ // managed hook missing and exit 1 with a staleness verdict, the
346
+ // exact misdiagnosis the undefined check below was written against.
347
+ if (next === undefined || next.startsWith('--')) {
348
+ process.stderr.write(
349
+ 'check_installed_hooks_fresh: --hooks-dir needs a path, got ' +
350
+ `${next === undefined ? 'nothing' : JSON.stringify(next)}\n`,
351
+ );
352
+ return 2;
353
+ }
354
+ hooksDir = next;
355
+ } else {
356
+ process.stderr.write(`check_installed_hooks_fresh: unknown argument ${a}\n`);
357
+ return 2;
358
+ }
359
+ }
360
+
361
+ let report: FreshnessReport;
362
+ try {
363
+ report = inspect(REPO, hooksDir);
364
+ } catch (e) {
365
+ process.stderr.write(
366
+ `❌ check_installed_hooks_fresh: ${e instanceof Error ? e.message : String(e)}\n`,
367
+ );
368
+ return 2;
369
+ }
370
+
371
+ const chunks: string[] = [];
372
+ const code = render(report, quiet, (s) => chunks.push(s));
373
+ const text = chunks.join('');
374
+ if (code === 0) process.stdout.write(text);
375
+ else process.stderr.write(text);
376
+ return code;
377
+ }
378
+
379
+ if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
380
+ process.exit(main());
381
+ }
@@ -71,6 +71,7 @@ import {
71
71
  collect_span_commits,
72
72
  derive_categories,
73
73
  parse_git_log,
74
+ mix_response_blockers,
74
75
  previous_release_tag,
75
76
  stale_draft_labels,
76
77
  } from './_lib/release_highlights.js';
@@ -238,6 +239,18 @@ export function main(argv: readonly string[]): number {
238
239
  );
239
240
  return 1;
240
241
  }
242
+ // Hoisted above the curated-head branches on purpose. It used to sit inside
243
+ // `contradictions.length === 0`, so CI reached it only for a section that
244
+ // HAS a head, carries no unrewritten draft and contradicts nothing — while
245
+ // the local guards call the same predicate unconditionally. A section
246
+ // without a curated head was therefore refused locally and waved through by
247
+ // CI: one predicate, two different gatekeepers, which is the drift this
248
+ // change exists to remove.
249
+ const mix = check_governance_mix_response(section.body, from, to, version);
250
+ if (mix !== 0) {
251
+ return mix;
252
+ }
253
+
241
254
  const curated = parse_curated_head(section.body);
242
255
  if (!curated) {
243
256
  process.stdout.write(`ℹ️ no curated head in the ${version} section — nothing to check\n`);
@@ -314,10 +327,6 @@ export function main(argv: readonly string[]): number {
314
327
  }
315
328
  const contradictions = highlight_contradictions(curated, derived);
316
329
  if (contradictions.length === 0) {
317
- const mix = check_governance_mix_response(section.body, from, to, version);
318
- if (mix !== 0) {
319
- return mix;
320
- }
321
330
  process.stdout.write(`✅ curated head plausible for ${version} (span ${from}..${to})\n`);
322
331
  return 0;
323
332
  }
@@ -361,9 +370,6 @@ export function main(argv: readonly string[]): number {
361
370
  * shallow clone or a missing tag on the release path would otherwise turn an
362
371
  * unrelated environment fact into a blocked release.
363
372
  */
364
- export const MIX_RESPONSE_MARKER = '**Governance mix:**';
365
- const MIX_RESPONSE_MIN_CHARS = 40;
366
-
367
373
  export function check_governance_mix_response(
368
374
  body: string,
369
375
  from: string,
@@ -385,22 +391,28 @@ export function check_governance_mix_response(
385
391
  process.stdout.write(`✅ governance mix for ${version}: ${level} — no response owed\n`);
386
392
  return 0;
387
393
  }
388
- const idx = body.indexOf(MIX_RESPONSE_MARKER);
389
- const written = idx === -1 ? '' : body.slice(idx + MIX_RESPONSE_MARKER.length).split('\n')[0]!.trim();
390
- if (written.length >= MIX_RESPONSE_MIN_CHARS) {
394
+ // ONE predicate, shared with `guard_release_branch_push` — see
395
+ // `mix_response_blockers`. This side prints and exits; the local side dies
396
+ // before anything leaves the machine. Neither owns the rule.
397
+ const blockers = mix_response_blockers(body, version, `\`release/${version}\``, {
398
+ triggered: true,
399
+ level,
400
+ });
401
+ if (blockers.length === 0) {
391
402
  process.stdout.write(`✅ governance mix for ${version}: ${level} — response present\n`);
392
403
  return 0;
393
404
  }
394
405
  process.stderr.write(
395
406
  `❌ the ${version} section owes a governance-versus-product response: ${level}.\n` +
407
+ blockers.map((b) => ` - ${b}\n`).join('') +
396
408
  ' Governance-only commits outnumber consumer-only commits over the release span, so\n' +
397
409
  ' the section carries a written response naming either the next cycle\'s consumer work\n' +
398
410
  ' or a maintainer justification (docs/contracts/CHANGELOG-conventions.md § Governance-\n' +
399
411
  ' versus-product response; the decline this replaces is ADR-253).\n' +
400
- ` Add one line under the ${version} curated head, for example:\n` +
401
- ` > ${MIX_RESPONSE_MARKER} ${level}. Next cycle ships <the consumer work>.\n` +
402
412
  ' Only that section is read; historical sections are out of scope. No ratio is\n' +
403
- ' enforced — this blocks a MISSING answer, never a particular number.\n',
413
+ ' enforced — this blocks a MISSING answer, never a particular number.\n' +
414
+ ' Reproduce locally, before any push:\n' +
415
+ ` ./scripts-run src/scripts/check_release_highlights --version ${version}\n`,
404
416
  );
405
417
  return 1;
406
418
  }
@@ -260,6 +260,17 @@ export const SUPPRESSION_INVENTORY: readonly SuppressionSpec[] = [
260
260
  // mistyped path as a fresh baseline.
261
261
  what: 'review prompts whose recorded prompt_hash does not re-derive',
262
262
  },
263
+ {
264
+ file: 'src/config/conformance-claim-baseline.json',
265
+ listKey: 'undeclared',
266
+ tier: 'string_list',
267
+ // Absent at the base ref because this change introduces it. Remove the
268
+ // flag on the next change once the introducing commit is an ancestor of
269
+ // main — the scan below fails while it is set and the file resolves,
270
+ // so the flag closes itself rather than needing to be remembered.
271
+ newInThisChange: true,
272
+ what: 'skill and rule artefacts carrying a standard-conformance claim with no enforced_by declaration',
273
+ },
263
274
  ];
264
275
 
265
276
  export interface SuppressionFinding {