@event4u/agent-config 14.17.0 → 14.19.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 (184) 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 +207 -77
  5. package/README.md +72 -150
  6. package/dist/agent-src/README.md +1 -1
  7. package/dist/agent-src/commands/agent-handoff.md +2 -2
  8. package/dist/agent-src/commands/pr/merge.md +35 -44
  9. package/dist/agent-src/commands/review/changes.md +19 -19
  10. package/dist/agent-src/commands/review.md +2 -2
  11. package/dist/agent-src/contexts/execution/host-capability-manifest.md +30 -3
  12. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  13. package/dist/agent-src/rules/git-history-discipline.md +2 -2
  14. package/dist/agent-src/rules/guidelines.md +1 -1
  15. package/dist/agent-src/rules/session-canary.md +2 -2
  16. package/dist/agent-src/rules/token-efficiency.md +1 -1
  17. package/dist/agent-src/skills/accessibility-auditor/SKILL.md +2 -0
  18. package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
  19. package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +27 -1
  20. package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
  21. package/dist/agent-src/skills/code-intelligence/SKILL.md +60 -12
  22. package/dist/agent-src/skills/complexity-first-planning/SKILL.md +27 -0
  23. package/dist/agent-src/skills/design-intelligence/SKILL.md +1 -1
  24. package/dist/agent-src/skills/design-intelligence/data/app-interface.csv +1 -1
  25. package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -17
  26. package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +2 -2
  27. package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +17 -0
  28. package/dist/agent-src/skills/design-intelligence/references/design-rules-checklist.md +4 -4
  29. package/dist/agent-src/skills/design-review/SKILL.md +3 -1
  30. package/dist/agent-src/skills/design-variations/SKILL.md +1 -1
  31. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
  32. package/dist/agent-src/skills/fe-design/SKILL.md +32 -5
  33. package/dist/agent-src/skills/fe-design/references/craft-floor.md +7 -0
  34. package/dist/agent-src/skills/fe-design/references/design-patterns.md +47 -2
  35. package/dist/agent-src/skills/feature-planning/SKILL.md +34 -0
  36. package/dist/agent-src/skills/git-workflow/SKILL.md +37 -12
  37. package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +176 -0
  38. package/dist/agent-src/skills/laravel-mail/SKILL.md +44 -0
  39. package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -1
  40. package/dist/agent-src/skills/project-analyzer/SKILL.md +0 -6
  41. package/dist/agent-src/skills/react-native-setup/SKILL.md +3 -3
  42. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
  43. package/dist/agent-src/skills/sequential-thinking/SKILL.md +18 -8
  44. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
  45. package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +40 -0
  46. package/dist/agent-src/skills/ui-apply-generic/SKILL.md +1 -1
  47. package/dist/agent-src/skills/ui-component-architect/SKILL.md +1 -1
  48. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +29 -2
  49. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +53 -8
  50. package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +3 -1
  51. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  52. package/dist/cli/commands/recordTriggerEval.js +2 -2
  53. package/dist/cli/main.js +1 -1
  54. package/dist/cli-delegate/{chunk-AKJGUQNO.js → chunk-GZTMRYCN.js} +405 -341
  55. package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-IDHUQIXP.js} +283 -132
  56. package/dist/cli-delegate/cmd_adr_effective.js +318 -81
  57. package/dist/cli-delegate/cmd_conformance.js +749 -669
  58. package/dist/cli-delegate/cmd_doctor.js +1 -1
  59. package/dist/cli-delegate/cmd_export.js +1 -1
  60. package/dist/cli-delegate/cmd_handoff.js +1 -1
  61. package/dist/cli-delegate/cmd_prune.js +1 -1
  62. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  63. package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
  64. package/dist/cli-delegate/cmd_sync.js +1 -1
  65. package/dist/cli-delegate/cmd_uninstall.js +1 -1
  66. package/dist/cli-delegate/cmd_validate.js +1 -1
  67. package/dist/discovery/deprecation-report.md +1 -1
  68. package/dist/discovery/discovery-manifest.json +39 -38
  69. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  70. package/dist/discovery/discovery-manifest.summary.md +1 -1
  71. package/dist/discovery/dormancy-report.md +1 -1
  72. package/dist/discovery/orphan-report.md +1 -1
  73. package/dist/discovery/packs.json +2 -2
  74. package/dist/discovery/trust-report.md +1 -1
  75. package/dist/discovery/workspaces.json +2 -2
  76. package/dist/hooks/dispatch.js +2764 -2439
  77. package/dist/install/install.mjs +588 -527
  78. package/dist/mcp/registry-manifest.json +2 -2
  79. package/dist/mcp/server.json +2 -2
  80. package/dist/mcp/server.mjs +1381 -1138
  81. package/dist/scripts/_lib/model_tier.js +1 -1
  82. package/dist/shared/settingsConsent.js +1 -1
  83. package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
  84. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
  85. package/docs/guidelines/design-antipatterns.md +87 -64
  86. package/package.json +1 -1
  87. package/src/agent-src/README.md +1 -1
  88. package/src/config/ci-local-parity.yml +6 -0
  89. package/src/config/conformance-claim-baseline.json +9 -0
  90. package/src/config/gate-coverage.yml +170 -0
  91. package/src/config/gate-violation-baselines.json +4 -4
  92. package/src/config/release-gate-locality.yml +141 -0
  93. package/src/scripts/_cli/cmd_doctor.ts +2 -2
  94. package/src/scripts/_cli/cmd_handoff.ts +1 -1
  95. package/src/scripts/_cli/cmd_update.ts +1 -1
  96. package/src/scripts/_cli/cmd_versions.ts +1 -1
  97. package/src/scripts/_lib/adr_frontmatter.ts +44 -0
  98. package/src/scripts/_lib/consequence_objects.ts +152 -0
  99. package/src/scripts/_lib/exec_evidence.ts +7 -0
  100. package/src/scripts/_lib/global_deploy_inventory.ts +2 -2
  101. package/src/scripts/_lib/host_permission_checks.ts +251 -0
  102. package/src/scripts/_lib/model_tier.ts +1 -1
  103. package/src/scripts/_lib/obligation_frequency.ts +2 -2
  104. package/src/scripts/_lib/release_highlights.ts +265 -1
  105. package/src/scripts/_lib/release_material.ts +129 -0
  106. package/src/scripts/_lib/repeated_run.ts +77 -0
  107. package/src/scripts/_lib/runtime_journal.ts +26 -3
  108. package/src/scripts/_lib/runtime_wiring_checks.ts +12 -1
  109. package/src/scripts/_lib/session_register.ts +11 -6
  110. package/src/scripts/_lib/sqlite_schema.ts +22 -0
  111. package/src/scripts/_lib/surface.ts +86 -0
  112. package/src/scripts/_lib/tamper_vocabulary.ts +242 -0
  113. package/src/scripts/_lib/ui_authority.ts +116 -0
  114. package/src/scripts/_lib/unified_diff.ts +121 -0
  115. package/src/scripts/adr/regenerate_index.ts +54 -1
  116. package/src/scripts/adr_cite_check.ts +71 -15
  117. package/src/scripts/ai_council/clients.ts +1 -1
  118. package/src/scripts/autonomy_friction_corpus.ts +229 -0
  119. package/src/scripts/build_proof.ts +15 -2
  120. package/src/scripts/check_beta_review_markers.ts +141 -17
  121. package/src/scripts/check_enforcement_coverage.ts +1 -1
  122. package/src/scripts/check_enforcement_denominator.ts +121 -11
  123. package/src/scripts/check_finding_dispositions.ts +239 -6
  124. package/src/scripts/check_gate_coverage.ts +96 -0
  125. package/src/scripts/check_installed_hooks_fresh.ts +381 -0
  126. package/src/scripts/check_release_highlights.ts +99 -14
  127. package/src/scripts/check_skill_activation_claim.ts +179 -0
  128. package/src/scripts/check_suppression_hygiene.ts +11 -0
  129. package/src/scripts/check_trigger_evals.ts +1 -1
  130. package/src/scripts/code_graph/build.ts +230 -33
  131. package/src/scripts/code_graph/extract.ts +168 -33
  132. package/src/scripts/code_graph/types.ts +37 -2
  133. package/src/scripts/code_graph/validate.ts +12 -1
  134. package/src/scripts/conformance_scan.ts +45 -4
  135. package/src/scripts/detect_silent_catch.ts +403 -0
  136. package/src/scripts/detect_verification_tampering.ts +538 -0
  137. package/src/scripts/explain_run.ts +1 -1
  138. package/src/scripts/generate_capability_matrix.ts +1 -1
  139. package/src/scripts/git_authorization_hook.ts +228 -18
  140. package/src/scripts/hook_manifest.json +1 -1
  141. package/src/scripts/hook_manifest.yaml +47 -30
  142. package/src/scripts/hooks/augment-dispatcher.sh +1 -1
  143. package/src/scripts/hooks/block_no_verify.ts +3 -3
  144. package/src/scripts/hooks/category_a.ts +291 -0
  145. package/src/scripts/hooks/cline-dispatcher.sh +1 -1
  146. package/src/scripts/hooks/concern_registry.ts +2 -2
  147. package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
  148. package/src/scripts/hooks/dispatch_hook.ts +17 -22
  149. package/src/scripts/hooks/evidence_independence.ts +21 -2
  150. package/src/scripts/hooks/{block_unauthorized_git.ts → git_command_classifier.ts} +175 -364
  151. package/src/scripts/hooks/host_lowering.ts +215 -0
  152. package/src/scripts/hooks/host_lowering.yaml +185 -0
  153. package/src/scripts/hooks/host_semantics.ts +105 -37
  154. package/src/scripts/hooks/journal_record_hook.ts +10 -0
  155. package/src/scripts/hooks/permission_gate.ts +117 -0
  156. package/src/scripts/hooks/push_settle_hook.ts +250 -0
  157. package/src/scripts/hooks/ship_diff_volume_hook.ts +1 -1
  158. package/src/scripts/hooks_status.ts +12 -10
  159. package/src/scripts/install-hooks.sh +134 -1
  160. package/src/scripts/install.ts +53 -82
  161. package/src/scripts/install_anthropic_key.sh +2 -2
  162. package/src/scripts/language_mirror_hook.ts +27 -5
  163. package/src/scripts/lint_design_antipattern_parity.ts +80 -0
  164. package/src/scripts/lint_evidence_artifacts.ts +45 -1
  165. package/src/scripts/lint_hook_manifest.ts +87 -3
  166. package/src/scripts/lint_motion_authority_drift.ts +731 -0
  167. package/src/scripts/lint_readme_size.ts +158 -12
  168. package/src/scripts/lint_reasoning_fixtures.ts +216 -0
  169. package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
  170. package/src/scripts/measure_sibling_naming.ts +150 -0
  171. package/src/scripts/redact_hook_capture.ts +1 -1
  172. package/src/scripts/release.ts +31 -143
  173. package/src/scripts/release_drill.ts +71 -1
  174. package/src/scripts/release_publication.ts +243 -3
  175. package/src/scripts/release_verify.ts +220 -0
  176. package/src/scripts/report_skill_activation.ts +319 -7
  177. package/src/scripts/schemas/review-findings.schema.json +6 -1
  178. package/src/scripts/schemas/skill.schema.json +12 -2
  179. package/src/scripts/self_review_gate.ts +10 -1
  180. package/src/scripts/session_canary_hook.ts +1 -1
  181. package/src/scripts/session_register_hook.ts +4 -3
  182. package/src/scripts/trigger_eval_grandfather.json +5 -1
  183. package/src/scripts/validate_agent_settings.ts +1 -1
  184. package/src/shared/settingsConsent.ts +1 -1
@@ -0,0 +1,150 @@
1
+ /**
2
+ * measure_sibling_naming — how many skill descriptions name another skill.
3
+ *
4
+ * `road-to-one-motion-authority` step 5.2. An inbox census claimed "44/299
5
+ * descriptions name a sibling" and shipped no definition of *naming* and no
6
+ * script, so the figure could be neither reproduced nor refuted. A companion
7
+ * claim of "218/299 bodies" reproduced under no reading of the tree at all.
8
+ * This script exists so the next such number is falsifiable.
9
+ *
10
+ * WHAT "NAMES A SIBLING" MEANS HERE, stated because the census did not.
11
+ *
12
+ * A description names a sibling when it contains the exact `name:` value of a
13
+ * DIFFERENT shipped skill, delimited so it is not a fragment of a longer
14
+ * identifier. `\b` is not enough: it puts a boundary between `brand` and the
15
+ * hyphen in `brand-identity`, so a bare `\b`-matched `brand` would score every
16
+ * mention of `brand-identity` as naming `brand` too. The delimiter here is
17
+ * "not `[A-Za-z0-9-]`" on both sides, which is the smallest rule that keeps
18
+ * `mcp` out of `mcp-builder` and `laravel` out of `laravel-dto`.
19
+ *
20
+ * A skill naming ITSELF does not count — that is a description, not a pointer.
21
+ *
22
+ * WHAT THE NUMBER IS AND IS NOT.
23
+ *
24
+ * A KNOWN AND UNFIXABLE FALSE POSITIVE, named rather than left for a reader to
25
+ * find: some skill names are ordinary English words, so a description can score
26
+ * a match by accident. `canvas-design` scores `brand` on the phrase "brand
27
+ * assets", which is not a pointer to the `brand` skill. Excluding short or
28
+ * common names would trade this for a false negative on the skills whose names
29
+ * really are those words, and there is no rule that gets both. The count is
30
+ * therefore an upper bound; the per-skill list is printed so a reader can see
31
+ * which matches are real.
32
+ *
33
+ * It is a count of descriptions that mention a sibling by name. It is NOT a
34
+ * measure of routing quality: a description can name a sibling uselessly, and a
35
+ * description that names none can be perfectly routable on its own keywords.
36
+ * The reason the count is worth having is narrower — the router reads
37
+ * descriptions and nothing else, so a family whose members never point at each
38
+ * other is a family the router cannot traverse.
39
+ *
40
+ * Exit code is 0 unless `--min <n>` is given and the count falls below it.
41
+ */
42
+
43
+ import fs from 'node:fs';
44
+ import path from 'node:path';
45
+ import { fileURLToPath, pathToFileURL } from 'node:url';
46
+
47
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
48
+ const REPO_ROOT = path.resolve(HERE, '..', '..');
49
+ const SKILLS_REL = path.join('src', 'skills');
50
+
51
+ export interface SkillRecord { name: string; description: string }
52
+
53
+ /** Read `name:` and `description:` out of a SKILL.md frontmatter block. */
54
+ export function parseSkill(text: string): SkillRecord | null {
55
+ if (!text.startsWith('---')) return null;
56
+ const end = text.indexOf('\n---', 3);
57
+ if (end < 0) return null;
58
+ const fm = text.slice(3, end);
59
+ const name = /^name:\s*(.+)$/m.exec(fm)?.[1]?.trim() ?? '';
60
+ let description = /^description:\s*(.+)$/m.exec(fm)?.[1]?.trim() ?? '';
61
+ if ((description.startsWith('"') && description.endsWith('"'))
62
+ || (description.startsWith("'") && description.endsWith("'"))) {
63
+ description = description.slice(1, -1);
64
+ }
65
+ description = description.replace(/\\"/g, '"');
66
+ if (name === '') return null;
67
+ return { name, description };
68
+ }
69
+
70
+ export function loadSkills(root: string): SkillRecord[] {
71
+ const dir = path.join(root, SKILLS_REL);
72
+ if (!fs.existsSync(dir)) return [];
73
+ const out: SkillRecord[] = [];
74
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
75
+ if (!entry.isDirectory()) continue;
76
+ const file = path.join(dir, entry.name, 'SKILL.md');
77
+ if (!fs.existsSync(file)) continue;
78
+ const rec = parseSkill(fs.readFileSync(file, 'utf-8'));
79
+ if (rec !== null) out.push(rec);
80
+ }
81
+ return out.sort((a, b) => a.name.localeCompare(b.name));
82
+ }
83
+
84
+ /** Identifier-boundary containment: `mcp` is not found inside `mcp-builder`. */
85
+ export function namesIdentifier(haystack: string, id: string): boolean {
86
+ let from = 0;
87
+ for (;;) {
88
+ const i = haystack.indexOf(id, from);
89
+ if (i < 0) return false;
90
+ const before = i === 0 ? '' : haystack[i - 1] ?? '';
91
+ const after = haystack[i + id.length] ?? '';
92
+ const isPart = (c: string): boolean => c !== '' && /[A-Za-z0-9-]/.test(c);
93
+ if (!isPart(before) && !isPart(after)) return true;
94
+ from = i + 1;
95
+ }
96
+ }
97
+
98
+ /** For each skill, the OTHER skills its description names. Sorted, deterministic. */
99
+ export function siblingsNamed(skills: readonly SkillRecord[]): Map<string, string[]> {
100
+ const names = skills.map((s) => s.name);
101
+ const out = new Map<string, string[]>();
102
+ for (const s of skills) {
103
+ const hits = names
104
+ .filter((n) => n !== s.name && namesIdentifier(s.description, n))
105
+ .sort();
106
+ out.set(s.name, hits);
107
+ }
108
+ return out;
109
+ }
110
+
111
+ export function main(argv: readonly string[] = process.argv.slice(2)): number {
112
+ const rootFlag = argv.indexOf('--root');
113
+ const root = rootFlag >= 0 ? (argv[rootFlag + 1] ?? REPO_ROOT) : REPO_ROOT;
114
+ const minFlag = argv.indexOf('--min');
115
+ const min = minFlag >= 0 ? Number(argv[minFlag + 1]) : null;
116
+ const json = argv.includes('--json');
117
+
118
+ const skills = loadSkills(root);
119
+ const map = siblingsNamed(skills);
120
+ const naming = [...map].filter(([, v]) => v.length > 0);
121
+
122
+ if (json) {
123
+ process.stdout.write(`${JSON.stringify({
124
+ total: skills.length,
125
+ naming: naming.length,
126
+ entries: Object.fromEntries(naming),
127
+ }, null, 1)}\n`);
128
+ } else {
129
+ process.stdout.write(`scanned: ${String(skills.length)}\n`);
130
+ process.stdout.write(
131
+ `sibling-naming: ${String(naming.length)}/${String(skills.length)} description(s) name at least one other shipped skill\n\n`,
132
+ );
133
+ for (const [name, hits] of naming) {
134
+ process.stdout.write(` ${name} → ${hits.join(', ')}\n`);
135
+ }
136
+ }
137
+
138
+ if (min !== null && Number.isFinite(min) && naming.length < min) {
139
+ process.stderr.write(
140
+ `\n❌ measure_sibling_naming: ${String(naming.length)} below the declared floor of ${String(min)}\n`,
141
+ );
142
+ return 1;
143
+ }
144
+ return 0;
145
+ }
146
+
147
+ if (process.argv[1] !== undefined) {
148
+ const invoked = pathToFileURL(path.resolve(process.argv[1])).href;
149
+ if (invoked === import.meta.url) process.exitCode = main();
150
+ }
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Ported from the retired Python `redact_hook_capture.py` (Phase 8 / Wave 8g).
6
6
  *
7
- * Reads JSON capture files written by `dispatch_hook.py` (when
7
+ * Reads JSON capture files written by `dispatch_hook.ts` (when
8
8
  * `AGENT_HOOK_CAPTURE_DIR` is set) and produces a redacted version
9
9
  * suitable for pasting into the verified-platforms roadmap.
10
10
  *
@@ -102,7 +102,6 @@
102
102
  * - `_lib.changelog_eras` imports resolve to the `.ts` twin, never a `.py`.
103
103
  */
104
104
 
105
- import { spawnSync } from 'node:child_process';
106
105
  import * as fs from 'node:fs';
107
106
  import * as path from 'node:path';
108
107
  import process from 'node:process';
@@ -120,16 +119,17 @@ import {
120
119
  } from './_lib/changelog_eras.js';
121
120
  import {
122
121
  HEAD_LABELS,
122
+ type MixObligation,
123
123
  collect_span_commits,
124
124
  derive_category_hits,
125
125
  render_derived_head_values,
126
126
  } from './_lib/release_highlights.js';
127
127
  import {
128
- NEXT_SECTION_RE,
129
128
  RELEASE_HEAD_DEFAULT,
130
129
  extract_changelog_section,
131
130
  pr_body_from_section,
132
131
  release_notes_from_section,
132
+ render_mix_response,
133
133
  } from './_lib/release_material.js';
134
134
 
135
135
  // `__doc__.splitlines()[0]` in `_parse_args` — the argparse description. Kept
@@ -164,11 +164,16 @@ import {
164
164
  _splitlines,
165
165
  } from './release_env.js';
166
166
  import type { RunResult } from './release_publication.js';
167
- // Re-export surface for the six names tests import from `release.js` and that
168
- // moved into the publication unit. `export ... from` rather than a bare import,
167
+ // Re-export surface for the names tests import from `release.js` and that moved
168
+ // into the publication unit — eleven since the test-count-trend unit followed. `export ... from` rather than a bare import,
169
169
  // because these are not USED here — an unused import would be dropped and the
170
170
  // test import path would break silently.
171
171
  export {
172
+ _TEST_LIST_MAX_BUFFER,
173
+ _count_from_list_result,
174
+ _count_tests_current,
175
+ _previous_test_count_from_changelog,
176
+ _render_test_trend_line,
172
177
  _failed_check_names,
173
178
  _failed_checks_report,
174
179
  _is_non_fast_forward,
@@ -176,7 +181,7 @@ export {
176
181
  _no_checks_action,
177
182
  _required_contexts_from_rules,
178
183
  } from './release_publication.js';
179
- import { _cap_body, jsonDumpsIndent, reEscape } from './release_env.js';
184
+ import { _cap_body, jsonDumpsIndent } from './release_env.js';
180
185
  import {
181
186
  _MERGE_UPDATE_ROUNDS,
182
187
  _branch_exists_local,
@@ -195,7 +200,10 @@ import {
195
200
  create_and_push_annotated_tag,
196
201
  guard_publication,
197
202
  checkout_release_branch,
203
+ _render_test_trend_line,
198
204
  guard_release_curation,
205
+ local_release_gate_argv,
206
+ measure_mix_obligation,
199
207
  die,
200
208
  gh,
201
209
  git,
@@ -500,7 +508,11 @@ function render_changelog_entry(
500
508
  prev: string | null,
501
509
  commits: readonly Commit[],
502
510
  today: string,
503
- opts: { test_trend_line?: string | null; head?: Readonly<Record<string, string>> } = {},
511
+ opts: {
512
+ test_trend_line?: string | null;
513
+ head?: Readonly<Record<string, string>>;
514
+ mix?: MixObligation | null;
515
+ } = {},
504
516
  ): [string, string] {
505
517
  const test_trend_line = opts.test_trend_line ?? null;
506
518
  let heading: string;
@@ -539,6 +551,11 @@ function render_changelog_entry(
539
551
 
540
552
  // The curated head sits above the generated log, which stays unchanged.
541
553
  const body_lines: string[] = [...render_release_head(opts.head ?? {})];
554
+ // Under the head and outside it — rationale on `MIX_RESPONSE_PLACEHOLDER`.
555
+ if (opts.mix?.triggered) {
556
+ body_lines.push('');
557
+ body_lines.push(...render_mix_response(opts.mix.level));
558
+ }
542
559
  const ordered_labels = ['BREAKING CHANGES', ...SECTIONS.map(([label]) => label)];
543
560
  for (const label of ordered_labels) {
544
561
  const bucket = grouped[label] ?? [];
@@ -581,139 +598,6 @@ function _changelog_line(c: Commit): string {
581
598
  return `* ${scope}${c.subject} ([${short}](${link}))`;
582
599
  }
583
600
 
584
- // ─── test-count trend (road-to-feedback-followups P3.2) ───────────────────────
585
-
586
- const _TEST_COUNT_LINE_RE = /^Tests:\s+(\d+)/m;
587
-
588
- /**
589
- * Buffer ceiling for the `vitest list` probe, in bytes.
590
- *
591
- * `spawnSync` buffers the child's whole stdout in memory and fails with
592
- * ENOBUFS past `maxBuffer`, whose default is 1 MiB. The probe emits one line
593
- * per test case, so its output grows with the suite: at 9470 cases the listing
594
- * is ~1.25 MB, i.e. already past the default. That is what silently dropped
595
- * the footer from the 9.10.0 notes and turned the `changelog-entry` gate red.
596
- * 64 MiB is ~50× the current listing — headroom for a suite many times this
597
- * size, at no cost when the output is small.
598
- */
599
- const _TEST_LIST_MAX_BUFFER = 64 * 1024 * 1024;
600
-
601
- /**
602
- * Count test cases from a finished `vitest list` spawn result, or return null
603
- * when the probe did not produce a usable listing.
604
- *
605
- * Split out from `_count_tests_current` so the failure modes are testable
606
- * without spawning the real (~14s, >1 MB) collection. `warn` is injected for
607
- * the same reason; it defaults to stderr.
608
- */
609
- function _count_from_list_result(
610
- res: {
611
- error?: (Error & { code?: string }) | undefined;
612
- status: number | null;
613
- stdout: string | null;
614
- },
615
- warn: (msg: string) => void = (msg) => void process.stderr.write(msg),
616
- ): number | null {
617
- // The trend line is informational and never blocks a release — but a
618
- // SILENT drop is what shipped 9.8.0 and 9.10.0 without a footer, so every
619
- // degradation says why. The `changelog-entry` CI gate treats a missing
620
- // footer as fatal, so this warning is the difference between fixing the
621
- // probe now and finding out from a red release PR.
622
- if (res.error) {
623
- // ENOENT (no npx) · ETIMEDOUT · ENOBUFS (listing past maxBuffer).
624
- const code = res.error.code ?? res.error.message;
625
- warn(
626
- `⚠️ test-count probe failed (${code}) — the \`Tests:\` footer will ` +
627
- `be omitted from the release notes, which fails the ` +
628
- `\`CHANGELOG entry exists for head version\` gate.\n`,
629
- );
630
- return null;
631
- }
632
- if ((res.status ?? 1) !== 0) {
633
- warn(
634
- `⚠️ test-count probe exited ${res.status ?? '(null)'} — the ` +
635
- `\`Tests:\` footer will be omitted from the release notes.\n`,
636
- );
637
- return null;
638
- }
639
- const lines = (res.stdout ?? '').split('\n').filter((l) => l.trim().length > 0);
640
- if (lines.length === 0) {
641
- warn('⚠️ test-count probe listed 0 cases — the `Tests:` footer will be omitted.\n');
642
- return null;
643
- }
644
- return lines.length;
645
- }
646
-
647
- /**
648
- * Return the collected vitest test-case count on the current tree
649
- * (`npx vitest list`, one line per case; ~14s wall). Returns null when
650
- * collection fails — the trend line is informational, never a release
651
- * blocker, but every failure warns (see `_count_from_list_result`).
652
- * (Replaced the dead `pytest --collect-only` probe on 2026-07-08,
653
- * road-to-truth-and-reference-hygiene Phase 3: the Python suite was retired
654
- * with ADR-200, so the old probe always degraded to null and the `Tests:`
655
- * footer silently vanished from release notes.)
656
- */
657
- function _count_tests_current(): number | null {
658
- // Recursion/cost guard: `npx vitest list` inside a vitest-driven release
659
- // test would collect the whole suite from within the suite (the child
660
- // inherits VITEST=… from the runner) — the exact recursion the caller
661
- // comment warns about. Degrade to null there, same as any other
662
- // collection failure.
663
- if (process.env['VITEST'] !== undefined) {
664
- return null;
665
- }
666
- const res = spawnSync('npx', ['vitest', 'list'], {
667
- cwd: REPO_ROOT,
668
- encoding: 'utf-8',
669
- stdio: ['ignore', 'pipe', 'pipe'],
670
- timeout: 180_000,
671
- maxBuffer: _TEST_LIST_MAX_BUFFER,
672
- });
673
- return _count_from_list_result(res);
674
- }
675
-
676
- /**
677
- * Read CHANGELOG.md and return the most recent `Tests: N` footer under the
678
- * `prev_tag` heading, or null when not found.
679
- */
680
- function _previous_test_count_from_changelog(prev_tag: string | null): number | null {
681
- if (!prev_tag || !fs.existsSync(CHANGELOG)) {
682
- return null;
683
- }
684
- const text = fs.readFileSync(CHANGELOG, 'utf-8');
685
- const heading_re = new RegExp(`^##\\s+\\[?${reEscape(prev_tag)}\\b`, 'm');
686
- const m = heading_re.exec(text);
687
- if (!m) {
688
- return null;
689
- }
690
- const headEnd = m.index + m[0].length;
691
- const rest = text.slice(headEnd);
692
- const next_heading = NEXT_SECTION_RE.exec(rest);
693
- const sectionEnd = headEnd + (next_heading ? next_heading.index : rest.length);
694
- const section = text.slice(headEnd, sectionEnd);
695
- const count_match = _TEST_COUNT_LINE_RE.exec(section);
696
- return count_match ? Number.parseInt(count_match[1] as string, 10) : null;
697
- }
698
-
699
- /**
700
- * Return the `Tests: N (+M since X.Y.Z)` footer line, or null when the current
701
- * count cannot be determined. Silent on collection errors.
702
- */
703
- function _render_test_trend_line(prev_tag: string | null): string | null {
704
- const current = _count_tests_current();
705
- if (current === null) {
706
- return null;
707
- }
708
- const previous = _previous_test_count_from_changelog(prev_tag);
709
- if (previous === null || !prev_tag) {
710
- return `Tests: ${current}`;
711
- }
712
- const delta = current - previous;
713
- const sign = delta >= 0 ? '+' : '';
714
- return `Tests: ${current} (${sign}${delta} since ${prev_tag})`;
715
- }
716
-
717
601
  /**
718
602
  * Insert `entry` inside the current era block.
719
603
  *
@@ -1287,6 +1171,10 @@ function execute(
1287
1171
  // `git push -u` is naturally idempotent — it prints "Everything
1288
1172
  // up-to-date" when remote already matches. push_release_branch
1289
1173
  // additionally absorbs a remote that moved under us.
1174
+ // Not a `_step`: a second `[4/10]` makes the cited evidence anchors ambiguous.
1175
+ process.stdout.write(' · verifying release gates locally (`task release:verify -- --cheap`)\n');
1176
+ run(local_release_gate_argv());
1177
+
1290
1178
  _step(4, total, `Push ${branch} to ${REMOTE}`);
1291
1179
  push_release_branch(branch);
1292
1180
  }
@@ -1834,9 +1722,13 @@ function main(argv: readonly string[] | null = null): number {
1834
1722
  // made the preview print the `_none_` skeleton while the real run wrote
1835
1723
  // pre-filled lines — a preview that contradicts its own output, which is
1836
1724
  // the exact class of surface disagreement this pre-fill exists to end.
1725
+ // Unconditional for the same preview-parity reason as `_derive_head_prefill`,
1726
+ // at a different cost: one `git show` per commit — measured 3.2 s over a
1727
+ // 120-commit span, paid 3-4× per release. Not the ~25 ms of the line above.
1837
1728
  const [full, body] = render_changelog_entry(target, prev, commits, today, {
1838
1729
  test_trend_line,
1839
1730
  head: _derive_head_prefill(prev),
1731
+ mix: measure_mix_obligation(target, prev),
1840
1732
  });
1841
1733
 
1842
1734
  // Era-split planning — gate on the POST-release view (2026-07-07 fix).
@@ -1954,10 +1846,6 @@ export {
1954
1846
  _detect_in_flight_target,
1955
1847
  print_preview,
1956
1848
  latest_tag,
1957
- _render_test_trend_line,
1958
- _previous_test_count_from_changelog,
1959
- _count_from_list_result,
1960
- _TEST_LIST_MAX_BUFFER,
1961
1849
  Commit,
1962
1850
  Plan,
1963
1851
  SystemExitError,
@@ -209,6 +209,18 @@ interface WorldConfig {
209
209
  * it fakes is one every resumed release actually passes through.
210
210
  */
211
211
  pr_already_merged?: boolean;
212
+
213
+ /**
214
+ * Make `task release:verify` fail, as a red local release gate would.
215
+ *
216
+ * Without this the drill returns OK for EVERY `task` invocation, so the
217
+ * pre-push gate run added 2026-09-05 would be exercised only on its happy
218
+ * path — wired but never proven to stop anything. The whole point of that
219
+ * step is that a red local gate costs one edit instead of a branch, a PR
220
+ * and a CI run, and that property is only real if a scenario watches it
221
+ * hold.
222
+ */
223
+ release_verify_fails?: boolean;
212
224
  }
213
225
 
214
226
  /**
@@ -232,6 +244,20 @@ export function defaultChangelogFixture(target: string): string {
232
244
  '',
233
245
  '- fixture entry for the release drill',
234
246
  '',
247
+ // Both lines are section-level publication obligations
248
+ // (`section_publication_blockers`), so a fixture without them refuses
249
+ // at the curation guard and every sequencing scenario dies at step 2 —
250
+ // the same "fixture trips the guard the drill exists to sequence"
251
+ // failure this builder's docstring already warns about, for the two
252
+ // obligations added 2026-09-05. The mix answer is written OUT rather
253
+ // than measured: measuring would couple every scenario to the live
254
+ // repository's commit span, which is the coupling `changelog` was
255
+ // introduced to remove.
256
+ '> **Governance mix:** governance-only 2 vs consumer-only 1 (taxonomy 1.0.0).',
257
+ '> Next cycle ships the fixture consumer work, tracked in the drill.',
258
+ '',
259
+ 'Tests: 1000 (+0 since 0.0.0)',
260
+ '',
235
261
  '## [0.0.0]',
236
262
  '',
237
263
  '- earlier release, present so the section boundary is exercised',
@@ -267,6 +293,8 @@ class FakeWorld {
267
293
  pr_merged = false;
268
294
  release_created = false;
269
295
  branch_exists_remote = true;
296
+ /** Whether the simulated `task release:verify` reports a red local gate. */
297
+ private readonly release_verify_fails: boolean;
270
298
  /** Live PR body (release-truth Phase 1: refreshed from the changelog at head). */
271
299
  pr_body = '';
272
300
 
@@ -296,6 +324,7 @@ class FakeWorld {
296
324
  this.merge_fails_hard = cfg.merge_fails_hard ?? false;
297
325
  this.checks_fail = cfg.checks_fail ?? false;
298
326
  this.pr_merged = cfg.pr_already_merged ?? false;
327
+ this.release_verify_fails = cfg.release_verify_fails ?? false;
299
328
  }
300
329
 
301
330
  exec(args: readonly string[]): ExecResult {
@@ -374,7 +403,14 @@ class FakeWorld {
374
403
 
375
404
  // ── task ─────────────────────────────────────────────────────────
376
405
  if (args[0] === 'task') {
377
- return OK; // release-prepare — regeneration is out of drill scope
406
+ // `release-prepare` — regeneration is out of drill scope.
407
+ // `release:verify` — the pre-push local gate run; simulated
408
+ // red only when a scenario asks for it, so the step's stopping
409
+ // power is exercised rather than assumed.
410
+ if (args[1] === 'release:verify' && this.release_verify_fails) {
411
+ return { status: 1, stdout: '', stderr: '❌ 1 release gate(s) would fail the release PR: release-shape.\n' };
412
+ }
413
+ return OK;
378
414
  }
379
415
 
380
416
  // ── gh ───────────────────────────────────────────────────────────
@@ -541,6 +577,40 @@ const SCENARIOS: Record<string, Scenario> = {
541
577
  return f;
542
578
  },
543
579
  },
580
+ 'local-gates-refuse-before-push': {
581
+ summary:
582
+ 'step 4: a red local release gate stops the run BEFORE the branch is pushed — no branch on origin, no PR, no CI',
583
+ config: { release_verify_fails: true },
584
+ expect_success: false,
585
+ verify: (w, err) => {
586
+ const f: string[] = [];
587
+ _expect(
588
+ (err ?? '').includes('release:verify'),
589
+ `died for the wrong reason: ${err ?? '(no error)'}`,
590
+ f,
591
+ );
592
+ // The three remote side effects the 14.17.0 run actually produced
593
+ // before `check_release_highlights` refused it on the PR. Each is
594
+ // a separate assertion because each was a separate cost.
595
+ _expect(
596
+ !w.calls.some((c) => c.startsWith('git push')),
597
+ 'the release branch was pushed after the local gates refused',
598
+ f,
599
+ );
600
+ _expect(
601
+ !w.calls.some((c) => c.startsWith('gh pr create')),
602
+ 'a pull request was opened after the local gates refused',
603
+ f,
604
+ );
605
+ _expect(
606
+ !w.calls.some((c) => c.includes('pr checks')),
607
+ 'CI was watched after the local gates refused',
608
+ f,
609
+ );
610
+ return f;
611
+ },
612
+ },
613
+
544
614
  'marker-refuses-before-commit': {
545
615
  summary:
546
616
  'step 3: a draft marker refuses BEFORE the release commit — no local release state, and therefore no remote state either',