@event4u/agent-config 12.0.0 → 13.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 (148) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +273 -0
  3. package/README.md +1 -1
  4. package/agents/templates/.ai-council.yml.example +46 -3
  5. package/dist/agent-src/commands/cost/report.md +23 -0
  6. package/dist/agent-src/contexts/execution/subagent-routing.md +148 -33
  7. package/dist/agent-src/contexts/model-recommendations.md +6 -4
  8. package/dist/agent-src/contexts/subagent-configuration.md +69 -7
  9. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  10. package/dist/agent-src/rules/design-fidelity.md +1 -1
  11. package/dist/agent-src/rules/design-review-after-ui-write.md +10 -5
  12. package/dist/agent-src/rules/evaluator-independence.md +27 -5
  13. package/dist/agent-src/rules/git-history-discipline.md +5 -1
  14. package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +1 -0
  15. package/dist/agent-src/rules/missing-skill-recovery.md +89 -0
  16. package/dist/agent-src/rules/ui-audit-gate.md +9 -4
  17. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
  18. package/dist/agent-src/scripts/roadmap_gates.ts +30 -2
  19. package/dist/agent-src/scripts/update_roadmap_progress.ts +47 -1
  20. package/dist/agent-src/skills/authz-review/SKILL.md +4 -0
  21. package/dist/agent-src/skills/design-system-capture/SKILL.md +4 -0
  22. package/dist/agent-src/skills/design-system-capture/references/snapshot-preference-order.md +77 -0
  23. package/dist/agent-src/skills/merge-conflicts/SKILL.md +4 -0
  24. package/dist/agent-src/skills/prompt-optimizer/SKILL.md +17 -1
  25. package/dist/agent-src/skills/roadmap-writing/SKILL.md +11 -11
  26. package/dist/agent-src/skills/systematic-debugging/SKILL.md +4 -0
  27. package/dist/agent-src/skills/threat-modeling/SKILL.md +3 -0
  28. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  29. package/dist/agent-src/templates/roadmaps.md +41 -3
  30. package/dist/cli/registry.js +1 -0
  31. package/dist/cli/registry.js.map +1 -1
  32. package/dist/cli-delegate/{chunk-NN4KW4HZ.js → chunk-3B72LSZL.js} +123 -2338
  33. package/dist/cli-delegate/{chunk-55WZN5VF.js → chunk-EVTY7ISR.js} +2 -2
  34. package/dist/cli-delegate/chunk-N7FEST6D.js +3895 -0
  35. package/dist/cli-delegate/{chunk-L565YFXH.js → chunk-XURJ4AQC.js} +566 -298
  36. package/dist/cli-delegate/cmd_conformance.js +4 -4
  37. package/dist/cli-delegate/cmd_doctor.js +2 -2
  38. package/dist/cli-delegate/cmd_export.js +2 -2
  39. package/dist/cli-delegate/cmd_handoff.js +1 -1
  40. package/dist/cli-delegate/cmd_prune.js +2 -2
  41. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  42. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  43. package/dist/cli-delegate/cmd_sync.js +2 -2
  44. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  45. package/dist/cli-delegate/cmd_validate.js +2 -2
  46. package/dist/discovery/deprecation-report.md +1 -1
  47. package/dist/discovery/discovery-manifest.json +44 -23
  48. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  49. package/dist/discovery/discovery-manifest.summary.md +3 -3
  50. package/dist/discovery/dormancy-report.md +1 -1
  51. package/dist/discovery/orphan-report.md +1 -1
  52. package/dist/discovery/packs.json +6 -5
  53. package/dist/discovery/trust-report.md +2 -2
  54. package/dist/discovery/workspaces.json +5 -4
  55. package/dist/hooks/dispatch.js +10625 -8541
  56. package/dist/install/install.mjs +754 -484
  57. package/dist/mcp/registry-manifest.json +3 -3
  58. package/dist/mcp/server.json +2 -2
  59. package/dist/mcp/server.mjs +1302 -167
  60. package/dist/router.json +1 -1
  61. package/dist/scripts/_lib/model_tier.js +9 -0
  62. package/dist/scripts/_lib/model_tier.js.map +1 -1
  63. package/dist/scripts/condense.js +50 -0
  64. package/dist/scripts/condense.js.map +1 -1
  65. package/dist/server/schemas/settings.js +2 -2
  66. package/dist/server/schemas/settings.js.map +1 -1
  67. package/docs/MIGRATION.md +20 -1
  68. package/docs/guidelines/design-fidelity-mechanics.md +21 -0
  69. package/docs/guidelines/design-handover-extraction.md +87 -0
  70. package/docs/guidelines/prompt-templates.md +24 -1
  71. package/package.json +1 -1
  72. package/src/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
  73. package/src/agent-src/scripts/roadmap_gates.ts +30 -2
  74. package/src/agent-src/scripts/update_roadmap_progress.ts +47 -1
  75. package/src/config/evaluator-budgets.json +9 -8
  76. package/src/config/gate-coverage.yml +63 -11
  77. package/src/config/gate-violation-baselines.json +12 -0
  78. package/src/config/hook-token-budget.json +15 -2
  79. package/src/config/preamble-payload-budget.json +28 -0
  80. package/src/scripts/_dispatch.bash +10 -0
  81. package/src/scripts/_lib/bench_ab_complexity.ts +439 -0
  82. package/src/scripts/_lib/bench_ab_safety_tier.ts +142 -0
  83. package/src/scripts/_lib/bench_ab_scoring_v2.ts +28 -3
  84. package/src/scripts/_lib/bench_ab_search_adherence.ts +423 -0
  85. package/src/scripts/_lib/bench_ab_size_claim.ts +206 -0
  86. package/src/scripts/_lib/bench_ab_workspace.ts +111 -0
  87. package/src/scripts/_lib/git_common_dir.ts +173 -0
  88. package/src/scripts/_lib/model_tier.ts +9 -0
  89. package/src/scripts/_lib/release_highlights.ts +108 -7
  90. package/src/scripts/_lib/skill_catalogue.ts +1115 -0
  91. package/src/scripts/_lib/tier_budget_routing.ts +31 -331
  92. package/src/scripts/ai_council/clients.ts +197 -18
  93. package/src/scripts/ai_council/config.ts +16 -1
  94. package/src/scripts/ai_council/low_impact.ts +14 -1
  95. package/src/scripts/ai_council/orchestrator.ts +14 -1
  96. package/src/scripts/ai_council/transport_resolver.ts +32 -2
  97. package/src/scripts/bench_ab_v2_complexity.ts +252 -0
  98. package/src/scripts/bench_ab_v2_run.ts +3 -42
  99. package/src/scripts/bench_ab_v2_safety.ts +296 -0
  100. package/src/scripts/bench_ab_v2_search.ts +343 -0
  101. package/src/scripts/bench_ab_v2_stats.ts +191 -0
  102. package/src/scripts/build_archive_index.ts +383 -0
  103. package/src/scripts/capture_skill_catalogue.ts +279 -317
  104. package/src/scripts/check_branch_freshness.ts +7 -1
  105. package/src/scripts/check_release_published.ts +6 -2
  106. package/src/scripts/check_release_trunk_sync.ts +18 -2
  107. package/src/scripts/check_source_size_budget.ts +428 -0
  108. package/src/scripts/check_suppression_hygiene.ts +5 -1
  109. package/src/scripts/condense.ts +50 -1
  110. package/src/scripts/cost/backfill_rates.mjs +298 -0
  111. package/src/scripts/cost/budget.mjs +12 -32
  112. package/src/scripts/cost_summary.ts +25 -0
  113. package/src/scripts/dispatch_r2_reviewer.ts +15 -5
  114. package/src/scripts/evidence_report.ts +15 -4
  115. package/src/scripts/external_sources_denylist.json +1 -0
  116. package/src/scripts/hook_manifest.yaml +75 -15
  117. package/src/scripts/hooks/concern_registry.ts +4 -0
  118. package/src/scripts/hooks/orchestration_record_hook.ts +21 -0
  119. package/src/scripts/hooks/skill_route_hook.ts +226 -0
  120. package/src/scripts/hooks/tool_result_bytes_hook.ts +219 -0
  121. package/src/scripts/hooks/ui_route_nudge_hook.ts +6 -3
  122. package/src/scripts/hot_context_hook.ts +13 -1
  123. package/src/scripts/install.ts +129 -0
  124. package/src/scripts/janitor.ts +77 -2
  125. package/src/scripts/lint_design_antipattern_parity.ts +40 -6
  126. package/src/scripts/lint_harvest_provenance.ts +35 -1
  127. package/src/scripts/lint_plan_risk_register.ts +22 -9
  128. package/src/scripts/lint_roadmap_blockers.ts +109 -6
  129. package/src/scripts/lint_rule_skill_pack_reach.ts +43 -0
  130. package/src/scripts/lint_scheduled_deprecations.ts +738 -0
  131. package/src/scripts/lint_skill_router_head.ts +21 -0
  132. package/src/scripts/mcp_server/consumer_tool_catalog.json +26 -3
  133. package/src/scripts/mcp_server/tool_catalog_source.ts +6 -24
  134. package/src/scripts/mcp_server/tools.ts +87 -0
  135. package/src/scripts/migration_status.ts +17 -1
  136. package/src/scripts/probe_review_binding_drift.ts +731 -0
  137. package/src/scripts/release.ts +100 -6
  138. package/src/scripts/routing_doctor.ts +50 -13
  139. package/src/scripts/schemas/command.schema.json +3 -3
  140. package/src/scripts/schemas/skill.schema.json +20 -2
  141. package/src/scripts/schemas/subagent.schema.json +2 -2
  142. package/src/scripts/session_eol_report.ts +81 -0
  143. package/src/scripts/trigger_coverage.ts +224 -3
  144. package/src/scripts/workspace_doctor.ts +464 -0
  145. package/src/scripts/worktree_cleanup_check.ts +2 -1
  146. package/src/server/schemas/settings.ts +2 -2
  147. package/dist/cli-delegate/chunk-AYKQGIBG.js +0 -1594
  148. package/src/config/budget-routing.json +0 -7
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Per-trial workspace lifecycle — the clone, and the transcript beside it.
3
+ *
4
+ * The two belong together: `reset_fixture` re-creates the clone AND drops the
5
+ * sibling transcript, and both are the delta-#7 evidence an offline re-scorer
6
+ * reads. They moved out of `bench_ab_v2_run.ts` because the source-size ratchet
7
+ * is shrink-only and extraction is its intended answer — raising the baseline
8
+ * is a defect, and trimming the prose out of load-bearing comments is worse.
9
+ *
10
+ * ## Transcript preservation — delta #7's sibling, for T5.
11
+ *
12
+ * T5 scores what a run did BEFORE it wrote, which leaves no residue in the
13
+ * final tree: the transcript is the only evidence, and the report discards it.
14
+ * These two functions are what make a completed sweep re-scorable for search
15
+ * adherence instead of needing a re-run.
16
+ *
17
+ * BESIDE the clone, never inside it. `changed_files` diffs the clone against
18
+ * the pristine fixture, so a transcript written into the workspace would appear
19
+ * as a file the run created: T5's evidence would corrupt T1's, and the size
20
+ * endpoint would gain one added file per trial for a reason no reader could see.
21
+ *
22
+ * A file rather than a field on the trial record because transcripts are the
23
+ * largest thing a sweep produces, and inlining them would multiply a pinned
24
+ * report's size for data only the T5 re-scorer ever reads. Truncating instead
25
+ * was the other option and is worse: a rubric scored on a clipped transcript
26
+ * measures the clip.
27
+ *
28
+ * KNOWN COVERAGE GAP, stated rather than silently carried: the
29
+ * `package-recursive` arm records neither `workspace` nor `transcript_path`, so
30
+ * T1/T2/T4 and now T5 all skip it. The workspace half predates this module and
31
+ * closing it would alter what the existing endpoints cover, which is a
32
+ * different decision from adding one — so it is named here and in the T5
33
+ * re-scorer's "no transcript recorded" reason instead of being half-fixed.
34
+ */
35
+ import * as fs from 'node:fs';
36
+ import * as os from 'node:os';
37
+ import * as path from 'node:path';
38
+ import { fileURLToPath } from 'node:url';
39
+
40
+ const _HERE = fileURLToPath(import.meta.url);
41
+ const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..', '..');
42
+ const FIXTURES_ROOT = path.join(REPO_ROOT, 'internal', 'bench', 'ab');
43
+ const WORK_ROOT = path.join(os.tmpdir(), 'agent-config-bench-v2-clones');
44
+
45
+ type Dict = Record<string, unknown>;
46
+
47
+ /** Where this trial's transcript is preserved — a sibling of the clone. */
48
+ export function transcript_path_for(clone: string): string {
49
+ return `${clone}.transcript.txt`;
50
+ }
51
+
52
+ /**
53
+ * Write the transcript beside the clone; return the path, or `null` when there
54
+ * was nothing to preserve or the write failed.
55
+ *
56
+ * A null propagates onto the trial as `transcript_path: null`, which the T5
57
+ * re-scorer reads as "not measured on this trial" — never as a zero score. A
58
+ * failed write must not be able to look like a run that searched for nothing.
59
+ */
60
+ export function preserve_transcript(clone: string, transcript: string): string | null {
61
+ if (!transcript) return null;
62
+ const dest = transcript_path_for(clone);
63
+ try {
64
+ fs.writeFileSync(dest, transcript, { encoding: 'utf-8' });
65
+ } catch {
66
+ return null;
67
+ }
68
+ return dest;
69
+ }
70
+
71
+
72
+ /**
73
+ * Workspace directory for one trial — delta #7 of the S0.3 spike.
74
+ *
75
+ * Keyed by `task|arm|seed`, not by task alone. The old task-only key meant every
76
+ * arm and every seed of a task reused ONE directory that the next trial deleted,
77
+ * so at the end of a sweep exactly one workspace survived per task — the last one
78
+ * written, with no record of which arm or seed it belonged to.
79
+ *
80
+ * That is not a tidiness problem. Phase 3's anti-golfing gate is specified as
81
+ * retro-fittable onto already-completed runs by offline re-scoring, and the
82
+ * roadmap calls that gate cheap *because* the workspaces are preserved. Under the
83
+ * old key there was nothing to re-score, so the claim was false. A distinct
84
+ * directory per trial is what makes it true.
85
+ *
86
+ * `arm` is sanitised because arm names are used verbatim as a path segment and
87
+ * one of them would otherwise be free to escape the root.
88
+ */
89
+ export function workspace_dir(task_id: string, arm: string, seed: number): string {
90
+ const safe_arm = arm.replace(/[^A-Za-z0-9._-]/g, '_');
91
+ return path.join(WORK_ROOT, `${task_id}__${safe_arm}__seed${seed}`);
92
+ }
93
+
94
+ /**
95
+ * Copy the task's pristine fixture into this trial's own working clone.
96
+ *
97
+ * The clone is re-created from the fixture on every call, so a resumed or
98
+ * repeated trial still starts pristine — the per-trial key changes *which*
99
+ * directory that is, never whether it is clean.
100
+ */
101
+ export function reset_fixture(task: Dict, arm: string, seed: number): [string, string] {
102
+ const fixture = path.join(FIXTURES_ROOT, String(task['fixture']));
103
+ const dest = workspace_dir(String(task['id']), arm, seed);
104
+ if (fs.existsSync(dest)) {
105
+ fs.rmSync(dest, { recursive: true, force: true });
106
+ }
107
+ fs.rmSync(transcript_path_for(dest), { force: true });
108
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
109
+ fs.cpSync(fixture, dest, { recursive: true });
110
+ return [dest, fixture];
111
+ }
@@ -126,3 +126,176 @@ export function git_common_dir(project_root: string): string | null {
126
126
  return null;
127
127
  }
128
128
  }
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // Workspace identity
132
+ // ---------------------------------------------------------------------------
133
+
134
+ /**
135
+ * One answer to one identity question, and never a silent default.
136
+ *
137
+ * The census that motivated this type (`agents/evidence/analysis/
138
+ * workspace-identity-census.md`) found three of eight repo-root sites falling
139
+ * back to a value that is *plausible but wrong* when git cannot answer —
140
+ * `process.cwd()`, a path derived from the script's own location, an
141
+ * exception. A caller cannot distinguish those from a real answer, which is
142
+ * how a gate ends up scanning the wrong tree and exiting green.
143
+ */
144
+ export type IdentityField =
145
+ | { readonly resolved: true; readonly value: string; readonly provenance: string }
146
+ | { readonly resolved: false; readonly reason: string };
147
+
148
+ /** The five workspace-identity questions, each independently resolvable. */
149
+ export interface WorkspaceIdentity {
150
+ /**
151
+ * Top level of the working tree the caller is in — the drop-in for
152
+ * `git rev-parse --show-toplevel`. Inside a linked worktree this is the
153
+ * worktree's own root, exactly as git reports it.
154
+ */
155
+ readonly repoRoot: IdentityField;
156
+ /**
157
+ * The main (non-linked) checkout. Derived from the common git dir, so it
158
+ * is the same value from every worktree of the repo. This is the field the
159
+ * two shipped misclassification defects needed and did not have.
160
+ */
161
+ readonly mainWorktree: IdentityField;
162
+ /**
163
+ * The checkout the caller is in. Its `value` equals `repoRoot` by git's own
164
+ * definition; the two fields exist separately because the *provenance*
165
+ * differs and that is the part callers need — `main-checkout` versus
166
+ * `linked-worktree` is precisely the question `isStandardLocation` got
167
+ * wrong twice.
168
+ */
169
+ readonly currentWorktree: IdentityField;
170
+ /** Branch of the current checkout; unresolved on a detached HEAD. */
171
+ readonly branch: IdentityField;
172
+ /**
173
+ * The remote's default branch, as a `refs/remotes/<remote>/<name>` ref —
174
+ * the offline notion of "what a PR from here would target".
175
+ *
176
+ * Deliberately NOT the `gh`-derived base of an actual open PR: that needs
177
+ * network and auth, and `check_branch_freshness.ts` already owns it. This
178
+ * field answers the question a gate can answer without leaving the disk,
179
+ * and reports `unresolved` rather than guessing `main` when the repo has
180
+ * no recorded default.
181
+ */
182
+ readonly prBase: IdentityField;
183
+ }
184
+
185
+ function resolved(value: string, provenance: string): IdentityField {
186
+ return { resolved: true, value, provenance };
187
+ }
188
+
189
+ function unresolved(reason: string): IdentityField {
190
+ return { resolved: false, reason };
191
+ }
192
+
193
+ /**
194
+ * Walk up from `start` to the first directory holding a `.git` entry.
195
+ *
196
+ * File-based on purpose, like everything else here: `rev-parse --show-toplevel`
197
+ * would be redirected by an inherited `GIT_DIR`, and the symptom is a wrong
198
+ * path rather than an error.
199
+ */
200
+ function discover_root(start: string): string | null {
201
+ let dir = realpath_or_self(path.resolve(start));
202
+ for (;;) {
203
+ if (fs.existsSync(path.join(dir, '.git'))) return dir;
204
+ const parent = path.dirname(dir);
205
+ if (parent === dir) return null;
206
+ dir = parent;
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Is `project_root`'s `.git` a linked-worktree pointer file rather than a
212
+ * directory? Returns `null` when there is no `.git` entry at all.
213
+ */
214
+ function is_linked_worktree(project_root: string): boolean | null {
215
+ try {
216
+ return !fs.statSync(path.join(project_root, '.git')).isDirectory();
217
+ } catch {
218
+ return null;
219
+ }
220
+ }
221
+
222
+ /** The remote default branch ref, read from disk. `null` when unrecorded. */
223
+ function remote_head_ref(common_dir: string, remote: string): string | null {
224
+ // A symbolic ref is stored loose; `git pack-refs` never packs one, so
225
+ // packed-refs is not a fallback for this specific ref — a repo that has
226
+ // never run `git remote set-head` simply has no answer on disk.
227
+ try {
228
+ const raw = fs.readFileSync(path.join(common_dir, 'refs', 'remotes', remote, 'HEAD'), 'utf-8');
229
+ const m = raw.match(/^ref:\s*(refs\/remotes\/.+?)\s*$/m);
230
+ return m === null ? null : m[1]!.trim();
231
+ } catch {
232
+ return null;
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Resolve all five workspace-identity questions from `start` (default: the
238
+ * process cwd), reading files only.
239
+ *
240
+ * Every field is either a value with its provenance or an explicit reason it
241
+ * could not be resolved. Nothing here throws and nothing guesses.
242
+ */
243
+ export function workspaceIdentity(
244
+ start: string = process.cwd(),
245
+ remote = 'origin',
246
+ ): WorkspaceIdentity {
247
+ const root = discover_root(start);
248
+ if (root === null) {
249
+ const why = `no .git entry in ${path.resolve(start)} or any ancestor`;
250
+ return {
251
+ repoRoot: unresolved(why),
252
+ mainWorktree: unresolved(why),
253
+ currentWorktree: unresolved(why),
254
+ branch: unresolved(why),
255
+ prBase: unresolved(why),
256
+ };
257
+ }
258
+
259
+ const linked = is_linked_worktree(root);
260
+ const repoRoot = resolved(root, 'upward .git walk from cwd');
261
+ const currentWorktree = resolved(
262
+ root,
263
+ linked === true
264
+ ? 'linked-worktree (.git is a gitdir pointer file)'
265
+ : 'main-checkout (.git is a directory)',
266
+ );
267
+
268
+ const common = git_common_dir(root);
269
+ let mainWorktree: IdentityField;
270
+ if (common === null) {
271
+ mainWorktree = unresolved('no common git dir resolvable from the repo root');
272
+ } else if (path.basename(common) === '.git') {
273
+ mainWorktree = resolved(path.dirname(common), 'dirname of the common git dir');
274
+ } else {
275
+ // Bare repo, `core.worktree`, or a separate-gitdir layout: the common
276
+ // dir is not `<main>/.git`, so `dirname` would name a directory that is
277
+ // not a checkout. Refusing is the point of the type.
278
+ mainWorktree = unresolved(
279
+ `common git dir ${common} is not named .git — bare or separate-gitdir layout has no main worktree to derive`,
280
+ );
281
+ }
282
+
283
+ const br = current_branch(root);
284
+ const branch = br === null
285
+ ? unresolved('detached HEAD, or HEAD unreadable in the per-worktree git dir')
286
+ : resolved(br, 'ref line of <git-dir>/HEAD');
287
+
288
+ let prBase: IdentityField;
289
+ if (common === null) {
290
+ prBase = unresolved('no common git dir resolvable from the repo root');
291
+ } else {
292
+ const ref = remote_head_ref(common, remote);
293
+ prBase = ref === null
294
+ ? unresolved(
295
+ `no refs/remotes/${remote}/HEAD on disk — run \`git remote set-head ${remote} -a\`; not guessed`,
296
+ )
297
+ : resolved(ref, `symbolic ref refs/remotes/${remote}/HEAD`);
298
+ }
299
+
300
+ return { repoRoot, mainWorktree, currentWorktree, branch, prBase };
301
+ }
@@ -25,7 +25,16 @@ import fs from "node:fs";
25
25
 
26
26
  // Tier → Claude model. The ONLY per-vendor mapping the package maintains
27
27
  // (ADR-035 § 3); other agents resolve the tier band to their own line-up.
28
+ //
29
+ // `frontier` was added by ADR-232, which amends ADR-035 § Decision 1 on that
30
+ // record's own reopen condition. The asymmetry that triggered it is worth
31
+ // keeping visible here: `MODEL_FAMILIES` in the orchestration-record hook has
32
+ // enumerated four families since it shipped, while this map named three — so a
33
+ // dispatch on the fourth band was recordable but not declarable. Adding the row
34
+ // here rather than a second table elsewhere is what keeps ADR-035 § 3 intact:
35
+ // one mapping, one owner, no two-clocks drift.
28
36
  export const TIER_TO_CLAUDE_MODEL: Record<string, string> = {
37
+ frontier: "fable",
29
38
  high: "opus",
30
39
  medium: "sonnet",
31
40
  lite: "haiku",
@@ -76,6 +76,91 @@ const _PUBLIC_SURFACE_PREFIXES = [
76
76
 
77
77
  const _RULE_OR_SCHEMA_PREFIXES = ['src/rules/', 'src/scripts/schemas/'];
78
78
 
79
+ /**
80
+ * Paths whose repair is a correctness fix to shipped *behaviour* rather than a
81
+ * record of one. The distinction is load-bearing: 43 % of `fix(...)` commits
82
+ * over the six spans measured in
83
+ * `agents/evidence/analysis/release-head-derivation-recall.md` touch no
84
+ * executable file at all — `52d7fe1 fix(worktrees): the inventory
85
+ * misclassifies from inside a worktree, totally` changes two markdown files and
86
+ * repairs nothing. Keying on the touched surface is what separates a bug fix
87
+ * from a bug report, and it moves hand-judged precision from 54 % to 96 %.
88
+ */
89
+ const _EXECUTABLE_SUFFIX = /\.(?:ts|tsx|js|mjs|cjs|sh|py)$/u;
90
+ const _EXECUTABLE_PREFIXES = ['.github/workflows/', '.github/actions/'];
91
+
92
+ /**
93
+ * Shebang entry points with no extension, which a suffix test cannot see. An
94
+ * exact set rather than a heuristic: the alternative is reading file contents,
95
+ * which a span commit does not carry.
96
+ *
97
+ * The recall limit that remains, named rather than left for a reader to
98
+ * discover: a NEW extensionless entry point is invisible until it is added
99
+ * here. Precision was measured over six spans; this predicate's recall was not,
100
+ * and adding to this set is the maintenance that follows a new entry point.
101
+ */
102
+ const _EXECUTABLE_EXACT: ReadonlySet<string> = new Set([
103
+ 'scripts-run',
104
+ 'src/scripts/agent-config',
105
+ ]);
106
+
107
+ function _touches_executable(files: SpanCommit['files']): boolean {
108
+ return files.some(
109
+ (f) =>
110
+ _EXECUTABLE_SUFFIX.test(f.path) ||
111
+ _EXECUTABLE_EXACT.has(f.path) ||
112
+ _EXECUTABLE_PREFIXES.some((p) => f.path.startsWith(p)),
113
+ );
114
+ }
115
+
116
+ /**
117
+ * Conventional-commit type, lower-cased, or `''` when the subject carries no
118
+ * conventional prefix.
119
+ *
120
+ * Two things this deliberately does, both of which an earlier version got
121
+ * wrong. It requires the conventional `:` separator, so `fix the flaky runner`
122
+ * is not classified as type `fix` — otherwise this function disagrees with the
123
+ * scope regex three lines below it and with every prose description of the
124
+ * rule. And it recognises `Revert "<subject>"`, the form `git revert` writes by
125
+ * default, which is not conventional at all: without it the documented revert
126
+ * half only ever applied to a hand-written `revert(scope):`, and a revert of
127
+ * shipped behaviour is the commit class most likely to be a correctness repair.
128
+ */
129
+ function _commit_type(subject: string): string {
130
+ if (/^Revert\s+"/u.test(subject)) {
131
+ return 'revert';
132
+ }
133
+ const m = /^(\w+)(?:\([^)]*\))?!?:/u.exec(subject);
134
+ return m ? m[1]!.toLowerCase() : '';
135
+ }
136
+
137
+ /**
138
+ * Recorded-null forms beyond the literal marker, taken from real subjects and
139
+ * bodies over the six measured spans rather than invented: a waived-rather-than-
140
+ * met condition, a published or recorded null, an archival on a roadmap's own
141
+ * falsifier. All six commits these add across the six spans are true positives.
142
+ */
143
+ const _NULL_FORMS: readonly RegExp[] = [
144
+ /honest[ -]null/iu,
145
+ /\bwaived,? (?:rather than|not) met\b/iu,
146
+ /\bsoak was waived\b/iu,
147
+ // The trailing lookahead keeps a recorded *result* apart from a field that
148
+ // merely holds null. This pattern is applied to the body as well as the
149
+ // subject, and a body is where "records null token_delta" appears — a
150
+ // telemetry field, not a null result. Excluding an identifier-shaped token
151
+ // after `null` costs nothing real: "records the null result" and "closed as
152
+ // a published null" both still derive. The lookahead is deliberately
153
+ // SAME-LINE (`[ \t]`, never `\s`): the first version used `\s+` and dropped
154
+ // a true positive, because 92f9b9a ends its subject on "closed as a
155
+ // published null" and its body then opens with `count_open reached 0` — an
156
+ // identifier on the NEXT line, which `\s+` happily crossed. The residual
157
+ // limit, named rather than left to be discovered: a field name with no
158
+ // underscore ("records null delta") is still caught.
159
+ /\b(?:publish(?:es|ed|ing)?|record(?:s|ed|ing)?|report(?:s|ed|ing)?) (?:the |an |a |its )?(?:honest )?null\b(?![ \t]+`?[a-z][a-z0-9]*_[a-z0-9_]*)/iu,
160
+ /\bnull(?: result)? (?:stands|is the answer)\b/iu,
161
+ /\bon (?:its|the) own falsifier\b/iu,
162
+ ];
163
+
79
164
  /**
80
165
  * Why a label fired, in generator-facing prose. Keyed by label; a label absent
81
166
  * here is never derived (`Known limitations` is pure prose, not checkable).
@@ -83,25 +168,37 @@ const _RULE_OR_SCHEMA_PREFIXES = ['src/rules/', 'src/scripts/schemas/'];
83
168
  const _DERIVED_REASON: Readonly<Record<string, string>> = {
84
169
  'Behaviour changes': 'rule/schema diffs, breaking commits or removed public surface',
85
170
  'Default changes + migration': 'commits naming a default, migration or migrate',
86
- 'Security and correctness': 'security-scoped commits',
87
- 'Honest nulls': 'commits carrying an honest-null marker',
171
+ 'Security and correctness': 'security-scoped commits or fixes to executable surface',
172
+ 'Honest nulls': 'commits recording a null, waived or falsified result',
88
173
  };
89
174
 
90
175
  /**
91
176
  * Derive evidence per curated label from the span. Rules per label:
92
177
  *
93
178
  * - Security and correctness: conventional scope matching /secur/i, or the
94
- * whole word "security" in the subject.
179
+ * whole word "security" in the subject — plus the *correctness* half the
180
+ * label names: a `fix` or `revert` commit touching executable surface.
95
181
  * - Behaviour changes: breaking (`!` / BREAKING CHANGE) commits, diffs
96
182
  * touching `src/rules/` or `src/scripts/schemas/`, and deletions under the
97
183
  * public artefact trees (removed public surface).
98
184
  * - Default changes + migration: subject carrying the whole word "default",
99
185
  * "migration", or "migrate".
100
- * - Honest nulls: subject or body carrying an "honest null" marker.
186
+ * - Honest nulls: subject or body carrying one of the recorded-null forms —
187
+ * the literal marker, a waived-rather-than-met condition, a published or
188
+ * recorded null, an archival on a roadmap's own falsifier.
101
189
  * - Known limitations: never derived — pure prose, not gate-checkable.
102
190
  *
103
191
  * Derivation is deliberately conservative: a false red makes every release
104
- * annoying, a miss only returns the head to the pre-gate state.
192
+ * annoying, a miss only returns the head to the pre-gate state. Conservative
193
+ * is not the same as silent, and two labels had crossed that line: measured
194
+ * over the six spans in
195
+ * `agents/evidence/analysis/release-head-derivation-recall.md`, `Security and
196
+ * correctness` fired **1 of 45** hand-confirmed in-category commits and
197
+ * `Honest nulls` **3 of 9**, so the
198
+ * curated `_none_` shipped uncontested where the span carried the evidence.
199
+ * The widening is aimed at those two and measured before it landed; the naive
200
+ * "any `fix(` counts" alternative was rejected on data (54 % precision against
201
+ * 96 %), not on taste.
105
202
  */
106
203
  export function derive_category_hits(
107
204
  commits: readonly SpanCommit[],
@@ -113,7 +210,10 @@ export function derive_category_hits(
113
210
  for (const c of commits) {
114
211
  const scopeMatch = /^\w+\(([^)]*)\)!?:/u.exec(c.subject);
115
212
  const scope = scopeMatch ? scopeMatch[1]! : '';
116
- if (/secur/iu.test(scope) || /\bsecurity\b/iu.test(c.subject)) {
213
+ const type = _commit_type(c.subject);
214
+ const correctnessFix =
215
+ (type === 'fix' || type === 'revert') && _touches_executable(c.files);
216
+ if (/secur/iu.test(scope) || /\bsecurity\b/iu.test(c.subject) || correctnessFix) {
117
217
  out['Security and correctness']!.push({ sha: c.sha, text: c.subject });
118
218
  }
119
219
  const removedPublic = c.files.filter(
@@ -134,7 +234,8 @@ export function derive_category_hits(
134
234
  if (/\b(default|migration|migrate)\b/iu.test(c.subject)) {
135
235
  out['Default changes + migration']!.push({ sha: c.sha, text: c.subject });
136
236
  }
137
- if (/honest[ -]null/iu.test(`${c.subject}\n${c.body}`)) {
237
+ const subjectAndBody = `${c.subject}\n${c.body}`;
238
+ if (_NULL_FORMS.some((re) => re.test(subjectAndBody))) {
138
239
  out['Honest nulls']!.push({ sha: c.sha, text: c.subject });
139
240
  }
140
241
  }