@event4u/agent-config 12.1.0 → 14.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 (185) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +164 -186
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/cost/report.md +23 -0
  5. package/dist/agent-src/commands/evals/roadmap.json +8 -0
  6. package/dist/agent-src/commands/fix/ci.md +39 -1
  7. package/dist/agent-src/commands/pr/create.md +65 -4
  8. package/dist/agent-src/commands/roadmap/next.md +28 -5
  9. package/dist/agent-src/commands/work.md +16 -0
  10. package/dist/agent-src/contexts/contracts/command-suggestion-flow.md +47 -0
  11. package/dist/agent-src/contexts/execution/contract-decision-sheet.md +94 -0
  12. package/dist/agent-src/contexts/execution/plan-confidence-gate.md +30 -0
  13. package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +21 -8
  14. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +55 -8
  15. package/dist/agent-src/contexts/execution/subagent-response-contract.md +42 -0
  16. package/dist/agent-src/contexts/execution/subagent-routing.md +148 -33
  17. package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +29 -0
  18. package/dist/agent-src/contexts/subagent-configuration.md +69 -7
  19. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  20. package/dist/agent-src/rules/command-suggestion-policy.md +33 -0
  21. package/dist/agent-src/rules/design-fidelity.md +1 -1
  22. package/dist/agent-src/rules/design-review-after-ui-write.md +36 -14
  23. package/dist/agent-src/rules/evaluator-independence.md +27 -5
  24. package/dist/agent-src/rules/git-history-discipline.md +5 -1
  25. package/dist/agent-src/rules/missing-skill-recovery.md +89 -0
  26. package/dist/agent-src/rules/ui-audit-gate.md +34 -12
  27. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
  28. package/dist/agent-src/scripts/gate_execute.ts +395 -0
  29. package/dist/agent-src/scripts/resume_probe.ts +422 -0
  30. package/dist/agent-src/scripts/roadmap_gates.ts +138 -4
  31. package/dist/agent-src/scripts/update_roadmap_progress.ts +70 -4
  32. package/dist/agent-src/skills/ai-council/SKILL.md +61 -0
  33. package/dist/agent-src/skills/authz-review/SKILL.md +4 -0
  34. package/dist/agent-src/skills/design-system-capture/SKILL.md +4 -0
  35. package/dist/agent-src/skills/design-system-capture/references/snapshot-preference-order.md +77 -0
  36. package/dist/agent-src/skills/experiment-loop/SKILL.md +116 -0
  37. package/dist/agent-src/skills/experiment-loop/evals/output-schema.yml +27 -0
  38. package/dist/agent-src/skills/experiment-loop/evals/triggers.json +17 -0
  39. package/dist/agent-src/skills/experiment-loop/references/pivot-ladder.md +54 -0
  40. package/dist/agent-src/skills/experiment-loop/references/protocol.md +61 -0
  41. package/dist/agent-src/skills/experiment-loop/references/register-format.md +66 -0
  42. package/dist/agent-src/skills/merge-conflicts/SKILL.md +4 -0
  43. package/dist/agent-src/skills/prompt-optimizer/SKILL.md +17 -1
  44. package/dist/agent-src/skills/roadmap-management/SKILL.md +2 -2
  45. package/dist/agent-src/skills/roadmap-writing/SKILL.md +5 -5
  46. package/dist/agent-src/skills/subagent-orchestration/schemas/team-review-status.json +8 -3
  47. package/dist/agent-src/skills/systematic-debugging/SKILL.md +4 -0
  48. package/dist/agent-src/skills/threat-modeling/SKILL.md +3 -0
  49. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  50. package/dist/agent-src/templates/roadmaps.md +43 -3
  51. package/dist/cli/registry.js +1 -0
  52. package/dist/cli/registry.js.map +1 -1
  53. package/dist/cli-delegate/{chunk-K34QTSD5.js → chunk-7PZ4FSZB.js} +368 -191
  54. package/dist/cli-delegate/{chunk-55WZN5VF.js → chunk-EVTY7ISR.js} +2 -2
  55. package/dist/cli-delegate/{chunk-O52B4F5S.js → chunk-F3YUF7SA.js} +1 -1
  56. package/dist/cli-delegate/{chunk-LKZ5DOYW.js → chunk-XURJ4AQC.js} +11 -3
  57. package/dist/cli-delegate/cmd_conformance.js +224 -104
  58. package/dist/cli-delegate/cmd_doctor.js +2 -2
  59. package/dist/cli-delegate/cmd_export.js +2 -2
  60. package/dist/cli-delegate/cmd_handoff.js +1 -1
  61. package/dist/cli-delegate/cmd_prune.js +2 -2
  62. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  63. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  64. package/dist/cli-delegate/cmd_sync.js +2 -2
  65. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  66. package/dist/cli-delegate/cmd_validate.js +2 -2
  67. package/dist/discovery/deprecation-report.md +1 -1
  68. package/dist/discovery/discovery-manifest.json +73 -30
  69. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  70. package/dist/discovery/discovery-manifest.summary.md +3 -3
  71. package/dist/discovery/dormancy-report.md +1 -1
  72. package/dist/discovery/orphan-report.md +1 -1
  73. package/dist/discovery/packs.json +7 -5
  74. package/dist/discovery/trust-report.md +3 -3
  75. package/dist/discovery/workspaces.json +6 -4
  76. package/dist/hooks/dispatch.js +2653 -1365
  77. package/dist/install/install.mjs +12 -4
  78. package/dist/mcp/registry-manifest.json +3 -3
  79. package/dist/mcp/server.json +2 -2
  80. package/dist/mcp/server.mjs +2679 -887
  81. package/dist/router.json +1 -1
  82. package/dist/scripts/condense.js +1 -1
  83. package/dist/scripts/condense.js.map +1 -1
  84. package/dist/server/schemas/settings.js +2 -2
  85. package/dist/server/schemas/settings.js.map +1 -1
  86. package/dist/ui/assets/{index-B0aMJnuz.js → index-C1pBRm1f.js} +2 -2
  87. package/dist/ui/assets/index-C1pBRm1f.js.map +1 -0
  88. package/dist/ui/index.html +1 -1
  89. package/docs/guidelines/design-fidelity-mechanics.md +21 -0
  90. package/docs/guidelines/design-handover-extraction.md +87 -0
  91. package/docs/guidelines/prompt-templates.md +24 -1
  92. package/package.json +1 -1
  93. package/src/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
  94. package/src/agent-src/scripts/gate_execute.ts +395 -0
  95. package/src/agent-src/scripts/resume_probe.ts +422 -0
  96. package/src/agent-src/scripts/roadmap_gates.ts +138 -4
  97. package/src/agent-src/scripts/update_roadmap_progress.ts +70 -4
  98. package/src/config/ci-local-parity.yml +13 -0
  99. package/src/config/evaluator-budgets.json +9 -8
  100. package/src/config/gate-coverage.yml +140 -12
  101. package/src/config/gate-violation-baselines.json +9 -3
  102. package/src/config/hook-token-budget.json +8 -1
  103. package/src/config/rule-activation-census.json +41 -0
  104. package/src/config/rule-stub-ceilings.json +52 -0
  105. package/src/scripts/_dispatch.bash +4 -0
  106. package/src/scripts/_lib/bench_ab_complexity.ts +439 -0
  107. package/src/scripts/_lib/bench_ab_safety_tier.ts +142 -0
  108. package/src/scripts/_lib/bench_ab_scoring_v2.ts +28 -3
  109. package/src/scripts/_lib/bench_ab_search_adherence.ts +423 -0
  110. package/src/scripts/_lib/bench_ab_size_claim.ts +206 -0
  111. package/src/scripts/_lib/bench_ab_workspace.ts +111 -0
  112. package/src/scripts/_lib/evaluator_contract.ts +159 -0
  113. package/src/scripts/_lib/gh_transient.ts +80 -0
  114. package/src/scripts/_lib/release_material.ts +16 -5
  115. package/src/scripts/_lib/review_independence.ts +111 -0
  116. package/src/scripts/_lib/session_register.ts +14 -0
  117. package/src/scripts/_lib/skill_catalogue.ts +278 -5
  118. package/src/scripts/_lib/skill_catalogue_series.ts +411 -0
  119. package/src/scripts/_lib/tier_budget_routing.ts +31 -331
  120. package/src/scripts/_lib/turn_end_refusals.ts +482 -0
  121. package/src/scripts/ai_council/_py_json.ts +96 -0
  122. package/src/scripts/ai_council/cli_call_budget.ts +357 -0
  123. package/src/scripts/ai_council/clients.ts +101 -164
  124. package/src/scripts/ai_council/config.ts +25 -11
  125. package/src/scripts/ai_council/probe_store.ts +196 -0
  126. package/src/scripts/ai_council/qualification.ts +425 -0
  127. package/src/scripts/ai_council/qualification_wiring.ts +243 -0
  128. package/src/scripts/ai_council/quorum_wiring.ts +211 -0
  129. package/src/scripts/ai_team/team_dispatch.ts +40 -6
  130. package/src/scripts/bench_ab_v2_complexity.ts +252 -0
  131. package/src/scripts/bench_ab_v2_run.ts +3 -42
  132. package/src/scripts/bench_ab_v2_safety.ts +296 -0
  133. package/src/scripts/bench_ab_v2_search.ts +343 -0
  134. package/src/scripts/bench_ab_v2_stats.ts +191 -0
  135. package/src/scripts/build_archive_index.ts +383 -0
  136. package/src/scripts/capture_skill_catalogue.ts +369 -4
  137. package/src/scripts/check_evaluator_schema.ts +249 -0
  138. package/src/scripts/check_pr_ci_current.ts +409 -0
  139. package/src/scripts/check_review_schema.ts +195 -0
  140. package/src/scripts/check_rule_activation_census.ts +451 -0
  141. package/src/scripts/check_rule_stub_ceiling.ts +1128 -0
  142. package/src/scripts/check_source_size_budget.ts +428 -0
  143. package/src/scripts/check_suppression_hygiene.ts +5 -1
  144. package/src/scripts/condense.ts +1 -1
  145. package/src/scripts/cost/backfill_rates.mjs +298 -0
  146. package/src/scripts/cost/budget.mjs +12 -32
  147. package/src/scripts/cost_summary.ts +25 -0
  148. package/src/scripts/council_cli.ts +141 -191
  149. package/src/scripts/dispatch_r2_reviewer.ts +8 -1
  150. package/src/scripts/external_sources_denylist.json +1 -0
  151. package/src/scripts/hook_manifest.yaml +99 -6
  152. package/src/scripts/hooks/concern_registry.ts +6 -0
  153. package/src/scripts/hooks/dispatch_hook.ts +43 -1
  154. package/src/scripts/hooks/interruption_ledger_hook.ts +256 -0
  155. package/src/scripts/hooks/ship_diff_volume_hook.ts +180 -0
  156. package/src/scripts/hooks/skill_route_hook.ts +226 -0
  157. package/src/scripts/hooks/turn_end_gate_hook.ts +119 -26
  158. package/src/scripts/hooks/ui_route_nudge_hook.ts +6 -3
  159. package/src/scripts/hooks_doctor.ts +112 -0
  160. package/src/scripts/interruption_report.ts +443 -0
  161. package/src/scripts/lint_design_antipattern_parity.ts +40 -6
  162. package/src/scripts/lint_evidence_artifacts.ts +359 -0
  163. package/src/scripts/lint_harvest_provenance.ts +35 -1
  164. package/src/scripts/lint_roadmap_blockers.ts +65 -0
  165. package/src/scripts/lint_roadmap_complexity.ts +7 -1
  166. package/src/scripts/lint_rule_skill_pack_reach.ts +43 -0
  167. package/src/scripts/lint_skill_router_head.ts +21 -0
  168. package/src/scripts/mcp_server/consumer_tool_catalog.json +26 -3
  169. package/src/scripts/mcp_server/tool_catalog_source.ts +6 -24
  170. package/src/scripts/mcp_server/tools.ts +87 -0
  171. package/src/scripts/release.ts +43 -58
  172. package/src/scripts/routing_doctor.ts +50 -13
  173. package/src/scripts/rule_activation_census.ts +292 -0
  174. package/src/scripts/schemas/command.schema.json +1 -1
  175. package/src/scripts/schemas/evaluator-output.schema.json +60 -0
  176. package/src/scripts/schemas/review-findings.schema.json +31 -0
  177. package/src/scripts/schemas/skill.schema.json +18 -1
  178. package/src/scripts/self_review_gate.ts +6 -0
  179. package/src/scripts/session_register_hook.ts +30 -1
  180. package/src/scripts/sync_pr_branch.ts +271 -0
  181. package/src/scripts/test_council_qualification.ts +213 -0
  182. package/src/scripts/trigger_coverage.ts +224 -3
  183. package/src/server/schemas/settings.ts +2 -2
  184. package/dist/ui/assets/index-B0aMJnuz.js.map +0 -1
  185. 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
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * The evaluator-output contract: types, the registry of wrapped verifiers, and
3
+ * the validator both the gate and any future loop share.
4
+ *
5
+ * Contract: `docs/contracts/evaluator-output.md`.
6
+ * Schema: `src/scripts/schemas/evaluator-output.schema.json`.
7
+ *
8
+ * The adapters below are PURE — each maps a verifier's already-captured
9
+ * `{stdout, stderr, exitCode}` onto the contract shape. That is deliberate and
10
+ * it is what makes the gate cheap: the mapping is the part that can be wrong,
11
+ * and it can be tested against recorded fixtures without paying to re-run three
12
+ * verifiers CI already runs once. Spike s02 established that none of the three
13
+ * needs a change on its own side for this to work.
14
+ */
15
+ import { load_schema, validate, type YamlValue } from '../validate_frontmatter.js';
16
+
17
+ export type MetricState = 'present' | 'absent' | 'unreadable';
18
+ export type Direction = 'maximize' | 'minimize';
19
+
20
+ export interface EvaluatorOutput {
21
+ readonly schema_version: 1;
22
+ readonly name: string;
23
+ readonly pass: boolean;
24
+ readonly score: number;
25
+ readonly metric?: number;
26
+ readonly metric_state?: MetricState;
27
+ readonly direction?: Direction;
28
+ readonly error?: string;
29
+ }
30
+
31
+ /** What a verifier invocation produced, before any interpretation. */
32
+ export interface RawRun {
33
+ readonly stdout: string;
34
+ readonly stderr: string;
35
+ readonly exitCode: number;
36
+ }
37
+
38
+ export interface EvaluatorAdapter {
39
+ readonly name: string;
40
+ /** The verifier this adapter wraps, as its CI-identical script name. */
41
+ readonly wraps: string;
42
+ readonly direction: Direction;
43
+ readonly parse: (raw: RawRun) => EvaluatorOutput;
44
+ }
45
+
46
+ /**
47
+ * Read a count off either stream.
48
+ *
49
+ * Both streams on purpose: `check_references` writes its count to stderr
50
+ * (measured, spike s02), and a stdout-only reader returned a null metric beside
51
+ * `pass: true` — a degraded reading that looked clean. A wrapper reads both; it
52
+ * still emits its own verdict on stdout alone, per the contract.
53
+ */
54
+ function readCount(raw: RawRun, re: RegExp): number | null {
55
+ const m = `${raw.stdout}\n${raw.stderr}`.match(re);
56
+ if (m === null || m[1] === undefined) return null;
57
+ const n = Number(m[1]);
58
+ return Number.isFinite(n) ? n : null;
59
+ }
60
+
61
+ /**
62
+ * Fold a possibly-missing count into the three contract fields.
63
+ *
64
+ * `expected` says whether this verifier is supposed to have a number at all.
65
+ * When it is and the number is missing, the result is `unreadable` — never
66
+ * `absent`, and never a silent zero. That collapse is the exact defect the
67
+ * `metric_state` field exists to prevent.
68
+ */
69
+ function withMetric(
70
+ base: Omit<EvaluatorOutput, 'metric' | 'metric_state' | 'score'>,
71
+ count: number | null,
72
+ expected: boolean,
73
+ ): EvaluatorOutput {
74
+ if (count !== null) {
75
+ return { ...base, metric: count, metric_state: 'present', score: -count };
76
+ }
77
+ // `metric` is OMITTED rather than set to null: the subset validator this
78
+ // estate ships rejects an array-typed `type`, so the schema spells the field
79
+ // as an optional number. Absence is the null.
80
+ if (!expected) {
81
+ return { ...base, metric_state: 'absent', score: base.pass ? 0 : -1 };
82
+ }
83
+ return {
84
+ ...base,
85
+ metric_state: 'unreadable',
86
+ score: -1,
87
+ error: 'expected a count in the verifier output and found none',
88
+ };
89
+ }
90
+
91
+ export const EVALUATORS: readonly EvaluatorAdapter[] = [
92
+ {
93
+ name: 'validate_frontmatter',
94
+ wraps: 'validate_frontmatter',
95
+ direction: 'minimize',
96
+ parse: (raw) =>
97
+ withMetric(
98
+ { schema_version: 1, name: 'validate_frontmatter', pass: raw.exitCode === 0, direction: 'minimize' },
99
+ // Group 2 is the failing count; group 1 is the corpus size, which
100
+ // is the denominator and not the thing being minimized.
101
+ readCount(raw, /\d+\s+artefacts,\s+(\d+)\s+failing/),
102
+ true,
103
+ ),
104
+ },
105
+ {
106
+ name: 'lint_output_slop',
107
+ wraps: 'lint_output_slop',
108
+ direction: 'minimize',
109
+ parse: (raw) => {
110
+ const text = `${raw.stdout}\n${raw.stderr}`;
111
+ const clean = /clean\s+—\s+no placeholder-prose patterns found/.test(text);
112
+ const count = clean ? 0 : (text.match(/^\s*\S+:\d+/gm) ?? []).length;
113
+ return withMetric(
114
+ { schema_version: 1, name: 'lint_output_slop', pass: raw.exitCode === 0, direction: 'minimize' },
115
+ count,
116
+ true,
117
+ );
118
+ },
119
+ },
120
+ {
121
+ name: 'check_references',
122
+ wraps: 'check_references',
123
+ direction: 'minimize',
124
+ parse: (raw) => {
125
+ const text = `${raw.stdout}\n${raw.stderr}`;
126
+ const scanned = readCount(raw, /scanned:\s*(\d+)/);
127
+ // A gate that scanned nothing has not measured a clean tree — it has
128
+ // not measured. Report that as unreadable rather than as zero broken
129
+ // references, which is the manufactured-green shape this estate names.
130
+ if (scanned === null || scanned === 0) {
131
+ return withMetric(
132
+ { schema_version: 1, name: 'check_references', pass: raw.exitCode === 0, direction: 'minimize' },
133
+ null,
134
+ true,
135
+ );
136
+ }
137
+ const clean = /No broken references found/.test(text);
138
+ return withMetric(
139
+ { schema_version: 1, name: 'check_references', pass: raw.exitCode === 0, direction: 'minimize' },
140
+ clean ? 0 : (text.match(/^\s*broken:/gim) ?? []).length || 1,
141
+ true,
142
+ );
143
+ },
144
+ },
145
+ ];
146
+
147
+ /**
148
+ * Schema violations for one evaluator output. Empty array = conformant.
149
+ *
150
+ * `SchemaError.format()` rather than `String(e)`: the class carries no
151
+ * `toString`, so stringifying it yields `[object Object]` — a violation list
152
+ * that names nothing, which is indistinguishable from a broken validator.
153
+ */
154
+ export function validateEvaluatorOutput(value: unknown): string[] {
155
+ const schema = load_schema('evaluator-output');
156
+ return validate(value as YamlValue, schema)
157
+ .filter((e) => e.severity !== 'warning')
158
+ .map((e) => e.format());
159
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Transient-failure handling for `gh` calls in the release pipeline.
3
+ *
4
+ * `gh` does not retry GitHub's 5xx responses — including the pre-flight
5
+ * "does a PR already exist" GraphQL query it runs before every `pr create`.
6
+ * The release run therefore dies at whichever step happens to catch the
7
+ * outage, after the commit and the push already landed.
8
+ *
9
+ * Pure over strings and an injected executor so the classifier and the retry
10
+ * loop are exercised by the fixture tests without a network or a real sleep.
11
+ */
12
+
13
+ export interface GhRunResult {
14
+ returncode: number;
15
+ stdout: string;
16
+ stderr: string;
17
+ }
18
+
19
+ /** Backoff between attempts. Length is the retry budget: three retries, ~23s. */
20
+ export const GH_RETRY_DELAYS_MS = [2_000, 6_000, 15_000] as const;
21
+
22
+ /**
23
+ * Retry-worthy failures only. A 4xx is the caller's fault and repeating it
24
+ * just delays the real message — the one exception is 429, which is the same
25
+ * wait-and-repeat shape as a 5xx.
26
+ */
27
+ const _TRANSIENT_RE =
28
+ /HTTP 5\d\d|HTTP 429|No server is currently available|Something went wrong while executing your query|exceeded a secondary rate limit|\b(?:ETIMEDOUT|ECONNRESET|EAI_AGAIN)\b|TLS handshake timeout/;
29
+
30
+ /**
31
+ * Whether `stderr` describes a transient GitHub failure.
32
+ *
33
+ * Deliberately reads stderr ALONE. `gh` writes errors there, while stdout on a
34
+ * failing call can still carry payload the caller asked for (`pr view --json
35
+ * body`) — and a changelog body that happens to quote `HTTP 500` must not turn
36
+ * a permanent failure into three pointless retries.
37
+ */
38
+ export function is_transient_gh_failure(stderr: string): boolean {
39
+ return _TRANSIENT_RE.test(stderr);
40
+ }
41
+
42
+ /**
43
+ * `gh` argv rendered for an error line, long values elided.
44
+ *
45
+ * The release-PR body is passed as a single `--body` argument and runs to tens
46
+ * of thousands of characters. Rendering argv verbatim is what buried the real
47
+ * one-line `gh` error under a 22k dump on 14.0.0.
48
+ */
49
+ export function gh_argv_label(args: readonly string[], max = 60): string {
50
+ return ['gh', ...args].map((a) => (a.length > max ? `${a.slice(0, max - 1)}…` : a)).join(' ');
51
+ }
52
+
53
+ /**
54
+ * Run `gh <args>` through `exec`, repeating while the failure is transient and
55
+ * the budget lasts. Returns the last result — classifying it is the caller's
56
+ * job, so a `check: false` probe keeps its own fallback behaviour.
57
+ *
58
+ * `sleep` and `notify` are injected rather than called directly so the loop is
59
+ * testable without a real 23-second wait, and so a caller running under a test
60
+ * exec-override can pass a no-op sleep while the delay sequence itself still
61
+ * runs and can be asserted.
62
+ */
63
+ export function gh_retry(
64
+ args: readonly string[],
65
+ exec: (argv: readonly string[]) => GhRunResult,
66
+ opts: { sleep?: (ms: number) => void; notify?: (message: string) => void } = {},
67
+ ): GhRunResult {
68
+ let result = exec(['gh', ...args]);
69
+ for (let i = 0; result.returncode !== 0 && i < GH_RETRY_DELAYS_MS.length; i++) {
70
+ if (!is_transient_gh_failure(result.stderr)) {
71
+ break;
72
+ }
73
+ const ms = GH_RETRY_DELAYS_MS[i]!;
74
+ const first = result.stderr.trim().split('\n')[0] ?? '';
75
+ opts.notify?.(`↻ ${gh_argv_label(args, 24)} — transient GitHub failure, retry in ${ms / 1_000}s: ${first}`);
76
+ opts.sleep?.(ms);
77
+ result = exec(['gh', ...args]);
78
+ }
79
+ return result;
80
+ }
@@ -21,7 +21,18 @@ export interface ChangelogSection {
21
21
  body: string;
22
22
  }
23
23
 
24
- const _NEXT_HEADING_RE = /^##\s+\[?\d+\.\d+\.\d+/m;
24
+ /**
25
+ * Where a changelog section ends: the next release entry OR the next era
26
+ * banner. Era banners are level 1 and release entries level 2+ — the same
27
+ * invariant the release-validation `changelog-entry` gate relies on, and the
28
+ * one `lint_changelog_rollback` / `lint_breaking_changes_index` already encode
29
+ * as `/^##? /`. Without the era arm the newest entry runs to end-of-file after
30
+ * a split, which leaves it as the last `##` heading above archived banners.
31
+ *
32
+ * Exported because release.ts bounds the previous entry the same way, and two
33
+ * copies of a boundary is how one of them stays wrong.
34
+ */
35
+ export const NEXT_SECTION_RE = /^(?:##\s+\[?\d+\.\d+\.\d+|# Era:)/m;
25
36
 
26
37
  function _reEscape(s: string): string {
27
38
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -29,9 +40,9 @@ function _reEscape(s: string): string {
29
40
 
30
41
  /**
31
42
  * Extract the CHANGELOG section for `version` — heading plus body up to the
32
- * next version heading (or end of file). Returns null when no heading for
33
- * `version` exists. Matches both heading shapes the generator emits
34
- * (`## X.Y.Z (date)` and `## [X.Y.Z](compare-url) (date)`).
43
+ * next version heading or era banner (or end of file). Returns null when no
44
+ * heading for `version` exists. Matches both heading shapes the generator
45
+ * emits (`## X.Y.Z (date)` and `## [X.Y.Z](compare-url) (date)`).
35
46
  */
36
47
  export function extract_changelog_section(text: string, version: string): ChangelogSection | null {
37
48
  const headingRe = new RegExp(`^##\\s+\\[?${_reEscape(version)}\\b.*$`, 'm');
@@ -41,7 +52,7 @@ export function extract_changelog_section(text: string, version: string): Change
41
52
  }
42
53
  const bodyStart = m.index + m[0].length;
43
54
  const rest = text.slice(bodyStart);
44
- const next = _NEXT_HEADING_RE.exec(rest);
55
+ const next = NEXT_SECTION_RE.exec(rest);
45
56
  const sectionEnd = bodyStart + (next ? next.index : rest.length);
46
57
  const body = text
47
58
  .slice(bodyStart, sectionEnd)
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Review independence — a recorded property of a review, never a promise.
3
+ *
4
+ * The failure this closes: a review whose members all share a model family
5
+ * produces an artifact byte-indistinguishable from one backed by independent
6
+ * models, so a consumer reads "no findings" as acceptance either way. The
7
+ * self-review gate is the live instance — it calls ONE Anthropic client, and
8
+ * the ledger it writes says nothing about that.
9
+ *
10
+ * Derived, never set by hand: `acceptance_status` follows from
11
+ * `review_independence`, so a same-family set carrying `accepted` is
12
+ * unrepresentable rather than merely forbidden. Two fields a producer can set
13
+ * inconsistently would reintroduce the ambiguity the pair exists to remove.
14
+ *
15
+ * Evidence: agents/evidence/eval-findings/metric-loop-s03.md.
16
+ */
17
+
18
+ /** The five providers `ai_council/config.ts::_VALID_PROVIDERS` admits. */
19
+ const PROVIDER_FAMILY: Readonly<Record<string, string>> = {
20
+ anthropic: 'anthropic',
21
+ openai: 'openai',
22
+ gemini: 'google',
23
+ xai: 'xai',
24
+ perplexity: 'perplexity',
25
+ };
26
+
27
+ export type ReviewIndependence = 'cross-family' | 'same-family' | 'single-member' | 'unknown';
28
+ export type AcceptanceStatus = 'accepted' | 'provisional';
29
+
30
+ /**
31
+ * How much INDEPENDENT evidence backs the verdict — deliberately orthogonal to
32
+ * how much effort the reviewer spent.
33
+ *
34
+ * Collapsing the two is what lets a long, careful, single-model pass read as
35
+ * acceptance: effort is visible in the artifact (findings, detail, rationale)
36
+ * and independence is not, so a reader with only one axis substitutes the one
37
+ * they can see. A five-hour same-family review is `single-pass`, and a terse
38
+ * two-family one is `independent`. That inversion is the point.
39
+ */
40
+ export type Assurance = 'unreviewed' | 'single-pass' | 'independent';
41
+
42
+ export function reviewIndependence(memberNames: readonly string[]): ReviewIndependence {
43
+ const present = memberNames.map((n) => n.trim()).filter((n) => n.length > 0);
44
+ if (present.length === 0) return 'unknown';
45
+ if (present.length === 1) return 'single-member';
46
+ const families = new Set(present.map((n) => PROVIDER_FAMILY[n] ?? `unknown:${n}`));
47
+ return families.size > 1 ? 'cross-family' : 'same-family';
48
+ }
49
+
50
+ /**
51
+ * Only a cross-family set may be described as acceptance.
52
+ *
53
+ * `unknown` maps to `provisional`, not to a third value: an absent member set
54
+ * is not evidence of independence, and the safe direction on an integrity field
55
+ * is the weaker claim.
56
+ */
57
+ export function acceptanceStatus(independence: ReviewIndependence): AcceptanceStatus {
58
+ return independence === 'cross-family' ? 'accepted' : 'provisional';
59
+ }
60
+
61
+ export function assuranceFor(independence: ReviewIndependence): Assurance {
62
+ if (independence === 'cross-family') return 'independent';
63
+ if (independence === 'unknown') return 'unreviewed';
64
+ return 'single-pass';
65
+ }
66
+
67
+ export interface IndependenceFields {
68
+ readonly review_independence: ReviewIndependence;
69
+ readonly acceptance_status: AcceptanceStatus;
70
+ readonly assurance: Assurance;
71
+ readonly reviewers: readonly string[];
72
+ }
73
+
74
+ /** The three fields plus the member set they were derived from. */
75
+ export function independenceFields(memberNames: readonly string[]): IndependenceFields {
76
+ const independence = reviewIndependence(memberNames);
77
+ return {
78
+ review_independence: independence,
79
+ acceptance_status: acceptanceStatus(independence),
80
+ assurance: assuranceFor(independence),
81
+ reviewers: [...memberNames],
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Consistency violations for an artifact that already carries the fields.
87
+ *
88
+ * The gate's teeth: a producer that hand-set `accepted` beside a same-family
89
+ * independence is caught here rather than trusted.
90
+ */
91
+ export function independenceViolations(doc: Record<string, unknown>): string[] {
92
+ const out: string[] = [];
93
+ const indep = doc['review_independence'];
94
+ const accept = doc['acceptance_status'];
95
+ const assur = doc['assurance'];
96
+ if (indep === undefined) {
97
+ out.push('review_independence is absent — an artifact that does not declare its independence reads as acceptance by default');
98
+ return out;
99
+ }
100
+ const expectedAccept = acceptanceStatus(indep as ReviewIndependence);
101
+ if (accept !== expectedAccept) {
102
+ out.push(
103
+ `acceptance_status is ${String(accept)} but review_independence ${String(indep)} derives ${expectedAccept}`,
104
+ );
105
+ }
106
+ const expectedAssur = assuranceFor(indep as ReviewIndependence);
107
+ if (assur !== undefined && assur !== expectedAssur) {
108
+ out.push(`assurance is ${String(assur)} but review_independence ${String(indep)} derives ${expectedAssur}`);
109
+ }
110
+ return out;
111
+ }
@@ -212,6 +212,20 @@ export interface SessionRecord {
212
212
  started_at: string;
213
213
  /** ISO-8601 UTC, rewritten every heartbeat. The liveness signal. */
214
214
  last_seen: string;
215
+ /**
216
+ * Per-detector turn-end refusals THIS session has taken, or absent when it
217
+ * has taken none.
218
+ *
219
+ * `road-to-stop-gate-honesty` § D-2: the gate refuses turn-ends with "no
220
+ * per-session visibility into how often it happens", and a refused turn-end
221
+ * costs the user at least one extra model turn. The counts ride this
222
+ * existing write — the reader is one small `readFileSync` of a record the
223
+ * gate already maintains, so the heartbeat gains no spawn.
224
+ *
225
+ * Optional on purpose: a record written before this field existed, or by a
226
+ * session that was never refused, is still a valid record.
227
+ */
228
+ turn_end_refusals?: Record<string, number>;
215
229
  }
216
230
 
217
231
  /**