@event4u/agent-config 14.7.0 → 14.9.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 (230) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +175 -390
  3. package/README.md +1 -1
  4. package/agents/templates/.ai-council.yml.example +34 -4
  5. package/dist/agent-src/commands/evals/git-pr-merge.json +13 -0
  6. package/dist/agent-src/commands/evals/roadmap.json +17 -1
  7. package/dist/agent-src/commands/pr/create.md +14 -2
  8. package/dist/agent-src/commands/pr/merge.md +383 -0
  9. package/dist/agent-src/commands/review/changes.md +95 -14
  10. package/dist/agent-src/commands/roadmap/process-full.md +157 -3
  11. package/dist/agent-src/commands/roadmap.md +1 -1
  12. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +46 -36
  13. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +37 -2
  14. package/dist/agent-src/contexts/execution/host-capability-manifest.md +77 -0
  15. package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +100 -2
  16. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +154 -2
  17. package/dist/agent-src/contexts/execution/subagent-response-contract.md +80 -0
  18. package/dist/agent-src/rules/artifact-drafting-protocol.md +5 -1
  19. package/dist/agent-src/rules/decision-revisit-gate.md +65 -33
  20. package/dist/agent-src/rules/recurring-criticism.md +1 -1
  21. package/dist/agent-src/rules/roadmap-progress-sync.md +19 -0
  22. package/dist/agent-src/rules/source-confidentiality.md +1 -1
  23. package/dist/agent-src/rules/untrusted-input-defense.md +9 -3
  24. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +7 -4
  25. package/dist/agent-src/scripts/dashboard_mode.ts +212 -0
  26. package/dist/agent-src/scripts/update_roadmap_progress.ts +47 -48
  27. package/dist/agent-src/skills/adr-create/SKILL.md +124 -5
  28. package/dist/agent-src/skills/code-review/evals/evals.json +87 -46
  29. package/dist/agent-src/skills/decision-record/SKILL.md +61 -3
  30. package/dist/agent-src/skills/decision-review/SKILL.md +65 -0
  31. package/dist/agent-src/skills/humanizer/SKILL.md +39 -0
  32. package/dist/agent-src/skills/humanizer/evals/strip_fixtures.json +57 -0
  33. package/dist/agent-src/skills/humanizer/references/fixtures.md +54 -0
  34. package/dist/agent-src/skills/judge-spec-compliance/SKILL.md +154 -0
  35. package/dist/agent-src/skills/judge-synthesis/SKILL.md +45 -0
  36. package/dist/agent-src/skills/merge-conflicts/SKILL.md +26 -0
  37. package/dist/agent-src/skills/roadmap-writing/SKILL.md +18 -18
  38. package/dist/agent-src/skills/security/SKILL.md +8 -0
  39. package/dist/agent-src/skills/security-audit/SKILL.md +12 -1
  40. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  41. package/dist/agent-src/templates/roadmaps.md +28 -2
  42. package/dist/cli/registry.js +3 -0
  43. package/dist/cli/registry.js.map +1 -1
  44. package/dist/cli-delegate/{chunk-FBKL6JWT.js → chunk-77GR3VD4.js} +68 -43
  45. package/dist/cli-delegate/{chunk-JZ4NR2TW.js → chunk-7PRE6ZV2.js} +1 -1
  46. package/dist/cli-delegate/{chunk-UEZ7D35A.js → chunk-CYH5SP6M.js} +4 -23
  47. package/dist/cli-delegate/{chunk-Q26HT4QT.js → chunk-E46KPN6K.js} +251 -150
  48. package/dist/cli-delegate/{chunk-74E3TKWV.js → chunk-H4VIVD4C.js} +5 -5
  49. package/dist/cli-delegate/chunk-I3EIN2YG.js +36 -0
  50. package/dist/cli-delegate/{chunk-4VR4ORQ6.js → chunk-LPPWO5CE.js} +4 -3
  51. package/dist/cli-delegate/{chunk-7F3VYSM2.js → chunk-PLSG6UL5.js} +14 -14
  52. package/dist/cli-delegate/cmd_adr_effective.js +1162 -0
  53. package/dist/cli-delegate/cmd_conformance.js +101 -25
  54. package/dist/cli-delegate/cmd_converge.js +3 -3
  55. package/dist/cli-delegate/cmd_doctor.js +7 -7
  56. package/dist/cli-delegate/cmd_export.js +6 -6
  57. package/dist/cli-delegate/cmd_handoff.js +7 -6
  58. package/dist/cli-delegate/cmd_memory_get.js +2 -1
  59. package/dist/cli-delegate/cmd_migrate.js +2 -2
  60. package/dist/cli-delegate/cmd_preflight.js +5 -5
  61. package/dist/cli-delegate/cmd_prune.js +6 -6
  62. package/dist/cli-delegate/cmd_route_audit.js +6 -5
  63. package/dist/cli-delegate/cmd_route_explain.js +3 -3
  64. package/dist/cli-delegate/cmd_session_recycle.js +3 -3
  65. package/dist/cli-delegate/cmd_settings_get.js +4 -4
  66. package/dist/cli-delegate/cmd_settings_migrate.js +6 -6
  67. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  68. package/dist/cli-delegate/cmd_sync.js +6 -6
  69. package/dist/cli-delegate/cmd_uninstall.js +6 -6
  70. package/dist/cli-delegate/cmd_update.js +3 -3
  71. package/dist/cli-delegate/cmd_upgrade.js +3 -3
  72. package/dist/cli-delegate/cmd_validate.js +6 -6
  73. package/dist/cli-delegate/cmd_versions.js +3 -3
  74. package/dist/discovery/deprecation-report.md +1 -1
  75. package/dist/discovery/discovery-manifest.json +83 -30
  76. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  77. package/dist/discovery/discovery-manifest.summary.md +4 -4
  78. package/dist/discovery/dormancy-report.md +1 -1
  79. package/dist/discovery/orphan-report.md +1 -1
  80. package/dist/discovery/packs.json +11 -9
  81. package/dist/discovery/trust-report.md +2 -2
  82. package/dist/discovery/workspaces.json +9 -7
  83. package/dist/hooks/dispatch.js +1295 -812
  84. package/dist/install/globalRuleLayers.js +169 -0
  85. package/dist/install/globalRuleLayers.js.map +1 -0
  86. package/dist/install/partitionEligibility.js +156 -0
  87. package/dist/install/partitionEligibility.js.map +1 -1
  88. package/dist/install/ruleLayerPartition.js +157 -0
  89. package/dist/install/ruleLayerPartition.js.map +1 -0
  90. package/dist/mcp/registry-manifest.json +3 -3
  91. package/dist/mcp/server.json +2 -2
  92. package/dist/mcp/server.mjs +1358 -805
  93. package/dist/scripts/_lib/claude_plugin.js +162 -0
  94. package/dist/scripts/_lib/claude_plugin.js.map +1 -0
  95. package/dist/scripts/_lib/environment_detector.js +397 -0
  96. package/dist/scripts/_lib/environment_detector.js.map +1 -0
  97. package/dist/scripts/_lib/prune_empty_dirs.js +85 -0
  98. package/dist/scripts/_lib/prune_empty_dirs.js.map +1 -0
  99. package/dist/scripts/_lib/spawn_env.js +129 -0
  100. package/dist/scripts/_lib/spawn_env.js.map +1 -0
  101. package/dist/scripts/ai_council/modes.js +199 -0
  102. package/dist/scripts/ai_council/modes.js.map +1 -0
  103. package/dist/scripts/ai_council/transport_resolver.js +418 -0
  104. package/dist/scripts/ai_council/transport_resolver.js.map +1 -0
  105. package/dist/scripts/condense.js +52 -89
  106. package/dist/scripts/condense.js.map +1 -1
  107. package/dist/server/routes/wizard.js +3 -2
  108. package/dist/server/routes/wizard.js.map +1 -1
  109. package/dist/ui/assets/index-Cxgy63kz.js +26 -0
  110. package/dist/ui/assets/index-Cxgy63kz.js.map +1 -0
  111. package/dist/ui/index.html +1 -1
  112. package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +45 -1
  113. package/docs/guidelines/agent-infra/false-green.md +47 -1
  114. package/docs/guidelines/agent-infra/traceability-field-mechanics.md +50 -0
  115. package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +17 -0
  116. package/package.json +7 -3
  117. package/src/agent-src/scripts/archive_completed_roadmaps.ts +7 -4
  118. package/src/agent-src/scripts/dashboard_mode.ts +212 -0
  119. package/src/agent-src/scripts/update_roadmap_progress.ts +47 -48
  120. package/src/config/ci-local-parity.yml +21 -0
  121. package/src/config/estate-count-budget.json +13 -471
  122. package/src/config/evaluator-budgets.json +4 -4
  123. package/src/config/gate-coverage.yml +458 -12
  124. package/src/config/gate-violation-baselines.json +18 -8
  125. package/src/config/gitignore-block.txt +15 -2
  126. package/src/config/hook-latency-budget.json +5 -0
  127. package/src/config/publish-surface.json +60 -0
  128. package/src/config/requirements-trace-budget.json +32 -0
  129. package/src/config/rule-stub-ceilings.json +25 -4
  130. package/src/install/globalRuleLayers.ts +197 -0
  131. package/src/install/partitionEligibility.ts +177 -0
  132. package/src/install/ruleLayerPartition.ts +212 -0
  133. package/src/scripts/_cli/cmd_adr_effective.ts +484 -0
  134. package/src/scripts/_dispatch.bash +18 -0
  135. package/src/scripts/_lib/adr_frontmatter.test.ts +365 -0
  136. package/src/scripts/_lib/adr_frontmatter.ts +459 -0
  137. package/src/scripts/_lib/base_tree.ts +175 -0
  138. package/src/scripts/_lib/billing_grant.ts +165 -0
  139. package/src/scripts/_lib/billing_grant_cli.ts +123 -0
  140. package/src/scripts/_lib/council_fallback_posture.ts +53 -6
  141. package/src/scripts/_lib/council_fallback_wiring.ts +61 -1
  142. package/src/scripts/_lib/envelope_grounding.ts +11 -1
  143. package/src/scripts/_lib/host_capability.ts +59 -5
  144. package/src/scripts/_lib/layer_overlap_notice.ts +11 -2
  145. package/src/scripts/_lib/prune_empty_dirs.ts +80 -0
  146. package/src/scripts/_lib/quota_parked.ts +133 -0
  147. package/src/scripts/_lib/retrieval_sanitize.ts +62 -0
  148. package/src/scripts/_lib/review_telemetry.ts +196 -0
  149. package/src/scripts/_lib/subagent_response.ts +92 -0
  150. package/src/scripts/adr/evidence_census.ts +899 -0
  151. package/src/scripts/adr/regenerate_index.ts +25 -38
  152. package/src/scripts/adr_cite_check.ts +540 -39
  153. package/src/scripts/ai_council/clients.ts +12 -1
  154. package/src/scripts/ai_council/config.ts +72 -115
  155. package/src/scripts/ai_council/fallback_config.ts +19 -7
  156. package/src/scripts/ai_council/mid_flight_fallback.ts +88 -7
  157. package/src/scripts/ai_council/orchestrator.ts +2 -2
  158. package/src/scripts/ai_council/py_format.ts +124 -0
  159. package/src/scripts/ai_council/transport_resolver.ts +65 -2
  160. package/src/scripts/audit_adr_coverage.ts +134 -21
  161. package/src/scripts/bench_hook_latency.ts +204 -0
  162. package/src/scripts/build_archive_index.ts +34 -0
  163. package/src/scripts/check_adr_frontmatter.ts +574 -39
  164. package/src/scripts/check_council_pin_staleness.ts +300 -0
  165. package/src/scripts/check_estate_count.ts +420 -293
  166. package/src/scripts/check_gate_coverage.ts +91 -0
  167. package/src/scripts/check_hook_bundle_content.ts +219 -0
  168. package/src/scripts/check_new_adr_evidence.ts +529 -0
  169. package/src/scripts/check_no_stub_inventory_table.ts +292 -0
  170. package/src/scripts/check_pack_size.ts +234 -1
  171. package/src/scripts/check_publish_surface.ts +297 -0
  172. package/src/scripts/check_references.ts +18 -0
  173. package/src/scripts/check_requirements_trace.ts +378 -0
  174. package/src/scripts/check_rule_layer_partition.ts +531 -0
  175. package/src/scripts/check_secret_leak.ts +166 -12
  176. package/src/scripts/check_single_delivery.ts +137 -4
  177. package/src/scripts/check_standing_rule_delivery.ts +8 -2
  178. package/src/scripts/code_graph/cli.ts +44 -5
  179. package/src/scripts/code_graph/extract.ts +87 -0
  180. package/src/scripts/condense.ts +61 -91
  181. package/src/scripts/council_cli.ts +12 -16
  182. package/src/scripts/detect_ai_tells.ts +87 -0
  183. package/src/scripts/dispatch_r2_reviewer.ts +42 -0
  184. package/src/scripts/external_sources_denylist.json +4 -1
  185. package/src/scripts/generate_capabilities_index.ts +41 -0
  186. package/src/scripts/git_authorization_hook.ts +54 -4
  187. package/src/scripts/hooks/block_unauthorized_git.ts +41 -3
  188. package/src/scripts/hooks/dispatch_hook.ts +154 -187
  189. package/src/scripts/hooks/dispatch_issues.ts +38 -19
  190. package/src/scripts/hooks/fallback_yaml.ts +66 -0
  191. package/src/scripts/hooks/host_semantics.ts +21 -0
  192. package/src/scripts/hooks/injection_budget.ts +13 -3
  193. package/src/scripts/hooks/py_json_dumps.ts +103 -0
  194. package/src/scripts/hooks/run_continuation_hook.ts +3 -1
  195. package/src/scripts/hooks/state_io.ts +88 -1
  196. package/src/scripts/hooks/stdin_failure_policy.ts +96 -0
  197. package/src/scripts/hooks/subagent_ledger_hook.ts +76 -11
  198. package/src/scripts/injection_scan_hook.ts +257 -15
  199. package/src/scripts/lint_adr_sweep_routing.ts +365 -0
  200. package/src/scripts/lint_hook_manifest.ts +49 -0
  201. package/src/scripts/lint_provenance_vocabulary.ts +472 -4
  202. package/src/scripts/lint_workflow_security.ts +53 -10
  203. package/src/scripts/lint_workflow_security_allowlist.json +2 -18
  204. package/src/scripts/report_envelope_rate.ts +166 -0
  205. package/src/scripts/review_axis_report.ts +218 -0
  206. package/src/scripts/routing_doctor.ts +5 -1
  207. package/src/scripts/run_supervise.ts +46 -1
  208. package/src/scripts/schemas/evals.schema.json +5 -0
  209. package/src/scripts/sync_pr_branch.ts +170 -20
  210. package/dist/cli/commands/commands.candidates.test.js +0 -257
  211. package/dist/cli/commands/commands.candidates.test.js.map +0 -1
  212. package/dist/cli/commands/profiles.test.js +0 -88
  213. package/dist/cli/commands/profiles.test.js.map +0 -1
  214. package/dist/cli/commands/recordTriggerEval.test.js +0 -113
  215. package/dist/cli/commands/recordTriggerEval.test.js.map +0 -1
  216. package/dist/cli/configRoot.test.js +0 -97
  217. package/dist/cli/configRoot.test.js.map +0 -1
  218. package/dist/cli/mcp/content.test.js +0 -54
  219. package/dist/cli/mcp/content.test.js.map +0 -1
  220. package/dist/cli/mcp/dispatch.test.js +0 -108
  221. package/dist/cli/mcp/dispatch.test.js.map +0 -1
  222. package/dist/server/writeRoot.test.js +0 -91
  223. package/dist/server/writeRoot.test.js.map +0 -1
  224. package/dist/shared/capabilities.test.js +0 -39
  225. package/dist/shared/capabilities.test.js.map +0 -1
  226. package/dist/ui/assets/index-C1pBRm1f.js +0 -26
  227. package/dist/ui/assets/index-C1pBRm1f.js.map +0 -1
  228. package/dist/cli-delegate/{chunk-PPQO4NGR.js → chunk-6RJA2F5A.js} +3 -3
  229. package/dist/cli-delegate/{chunk-6EOCNQAM.js → chunk-E4XIZDIB.js} +0 -0
  230. package/dist/cli-delegate/{chunk-GJ5BVHXM.js → chunk-PMEKGDED.js} +3 -3
@@ -0,0 +1,300 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * A council model pin cannot notice its own age. This gate is what notices.
4
+ *
5
+ * THE DEFECT IT EXISTS FOR. The starter council config shipped a dated,
6
+ * several-generation-old model id on an **enabled** member — the seat a quality
7
+ * council actually reviews with — and nothing in the file could flag it: a grep
8
+ * for `verified_at` or `stale` over the template returned two prose comments and
9
+ * no key. The pin was unversioned and unstamped, so it rotted silently for as
10
+ * long as nobody happened to re-read a template.
11
+ *
12
+ * WHAT IT DOES NOT DO, and this is the honest half: it cannot tell whether a
13
+ * pinned id is current. That needs the provider's own listing, which lives
14
+ * outside this tree and changes on the provider's schedule. It reports how long
15
+ * ago a **human** last said they looked. A stamp is a claim about attention, not
16
+ * about correctness, and treating it as the second would be worse than no stamp.
17
+ *
18
+ * SENTINELS ARE EXEMPT, BY DESIGN. A member on `codex-default`, or on a vendor
19
+ * alias the provider documents as "the latest model" in a band (`claude --help`:
20
+ * 'fable', 'opus', 'sonnet'), delegates the version decision to the vendor CLI
21
+ * and cannot go stale. Demanding a date there would be asking for a refresh
22
+ * nobody would ever perform, which is how a gate teaches its reader to ignore
23
+ * it.
24
+ *
25
+ * Mirrors `check_corpus_staleness` rather than inventing a second idiom:
26
+ * offline, deterministic, and the clock is INJECTABLE via `--today YYYY-MM-DD`
27
+ * so a fixture cannot flip verdict with the calendar. A future date is its own
28
+ * violation class — a stamp ahead of today is not "very fresh", it is a typo or
29
+ * a fabrication, and collapsing it into the fresh path would hide exactly the
30
+ * case worth catching.
31
+ *
32
+ * Exit: 0 clean or advisory-only, 1 on a violation, 2 on a usage error
33
+ * (unparsable config, bad --today, or a dead scan scope).
34
+ */
35
+ import * as fs from 'node:fs';
36
+ import * as os from 'node:os';
37
+ import * as path from 'node:path';
38
+
39
+ import { DeadScopeError, reportScanned } from './_lib/scan_scope.js';
40
+ import { runSelfTest, type SelfTestCase } from './_lib/gate_self_test.js';
41
+
42
+ /** Days before a stamped pin is called stale. */
43
+ export const CADENCE_DAYS = 100;
44
+
45
+ /**
46
+ * Vendor sentinels and "latest in band" aliases — a member on one of these
47
+ * cannot go stale, so it needs no stamp.
48
+ *
49
+ * The alias set is READ FROM the provider's own CLI surface (`claude --help`
50
+ * documents 'fable', 'opus' and 'sonnet' as aliases for the latest model), not
51
+ * from the agent's recall. That distinction is the one the roadmap behind this
52
+ * gate names as its top risk: a plausible-sounding id typed from memory is
53
+ * indistinguishable from a real one at review time.
54
+ */
55
+ export const SENTINEL_MODELS: ReadonlySet<string> = new Set([
56
+ 'codex-default',
57
+ 'fable',
58
+ 'opus',
59
+ 'sonnet',
60
+ 'haiku',
61
+ ]);
62
+
63
+ export type Verdict = 'fresh' | 'sentinel' | 'unstamped' | 'stale' | 'future-date' | 'malformed';
64
+
65
+ export interface PinRow {
66
+ member: string;
67
+ model: string;
68
+ verified_at: string | null;
69
+ enabled: boolean;
70
+ verdict: Verdict;
71
+ age_days: number | null;
72
+ }
73
+
74
+ const DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
75
+
76
+ /** Parse a `YYYY-MM-DD` into a UTC epoch-day, or null when it is not a real date. */
77
+ export function epochDay(s: string): number | null {
78
+ const m = DATE_RE.exec(s);
79
+ if (m === null) return null;
80
+ const y = Number(m[1]);
81
+ const mo = Number(m[2]);
82
+ const d = Number(m[3]);
83
+ const t = Date.UTC(y, mo - 1, d);
84
+ const probe = new Date(t);
85
+ if (probe.getUTCFullYear() !== y || probe.getUTCMonth() + 1 !== mo || probe.getUTCDate() !== d) {
86
+ return null;
87
+ }
88
+ return Math.floor(t / 86400000);
89
+ }
90
+
91
+ /** Classify one member. Pure — the unit under `--self-test`. */
92
+ export function classify(
93
+ member: string,
94
+ model: string,
95
+ verified_at: string | null,
96
+ enabled: boolean,
97
+ today: string,
98
+ ): PinRow {
99
+ const base: Omit<PinRow, 'verdict' | 'age_days'> = { member, model, verified_at, enabled };
100
+ if (SENTINEL_MODELS.has(model.trim().toLowerCase())) {
101
+ return { ...base, verdict: 'sentinel', age_days: null };
102
+ }
103
+ if (verified_at === null || verified_at.trim() === '') {
104
+ return { ...base, verdict: 'unstamped', age_days: null };
105
+ }
106
+ const stamp = epochDay(verified_at);
107
+ if (stamp === null) return { ...base, verdict: 'malformed', age_days: null };
108
+ const now = epochDay(today);
109
+ if (now === null) throw new Error(`--today must be a real YYYY-MM-DD date (got ${today})`);
110
+ const age = now - stamp;
111
+ if (age < 0) return { ...base, verdict: 'future-date', age_days: age };
112
+ return { ...base, verdict: age > CADENCE_DAYS ? 'stale' : 'fresh', age_days: age };
113
+ }
114
+
115
+ /**
116
+ * Read the members out of a council YAML without a YAML dependency.
117
+ *
118
+ * Deliberately line-based and deliberately narrow: it reads `members:` two
119
+ * levels deep, which is the whole shape this gate needs. A full parser would
120
+ * pull a dependency into a gate whose entire job is to be runnable offline in
121
+ * any checkout.
122
+ */
123
+ export function parseMembers(text: string): { member: string; model: string; verified_at: string | null; enabled: boolean }[] {
124
+ const lines = text.split('\n');
125
+ const out: { member: string; model: string; verified_at: string | null; enabled: boolean }[] = [];
126
+ let inMembers = false;
127
+ let cur: { member: string; model: string; verified_at: string | null; enabled: boolean } | null = null;
128
+ for (const raw of lines) {
129
+ const line = raw.replace(/\t/g, ' ');
130
+ if (/^members:\s*$/.test(line)) {
131
+ inMembers = true;
132
+ continue;
133
+ }
134
+ if (!inMembers) continue;
135
+ // A non-indented, non-empty, non-comment line ends the block.
136
+ if (line.trim() !== '' && !line.startsWith(' ') && !line.trimStart().startsWith('#')) break;
137
+ const head = /^ {2}([A-Za-z0-9_-]+):\s*$/.exec(line);
138
+ if (head !== null) {
139
+ if (cur !== null) out.push(cur);
140
+ cur = { member: String(head[1]), model: '', verified_at: null, enabled: false };
141
+ continue;
142
+ }
143
+ if (cur === null) continue;
144
+ const kv = /^ {4}([a-z_]+):\s*(.+?)\s*$/.exec(line);
145
+ if (kv === null) continue;
146
+ const k = String(kv[1]);
147
+ const v = String(kv[2]).replace(/^['"]|['"]$/g, '');
148
+ if (k === 'model') cur.model = v;
149
+ else if (k === 'verified_at') cur.verified_at = v;
150
+ else if (k === 'enabled') cur.enabled = v === 'true';
151
+ }
152
+ if (cur !== null) out.push(cur);
153
+ return out.filter((m) => m.model !== '');
154
+ }
155
+
156
+ function selfTest(): number {
157
+ const T = '2026-08-22';
158
+ let seq = 0;
159
+ // Each case drives the REAL gate over a real fixture, so what is asserted is
160
+ // the exit code rather than an internal verdict. Reject == the gate refuses.
161
+ const probe = (body: string): number => {
162
+ seq += 1;
163
+ const file = path.join(os.tmpdir(), `pin-staleness-selftest-${String(process.pid)}-${String(seq)}.yml`);
164
+ fs.writeFileSync(file, body, 'utf-8');
165
+ try {
166
+ return main(['--file', file, '--today', T]);
167
+ } catch (err) {
168
+ // Mirror the entrypoint: a dead scan scope exits 2 there, so a case
169
+ // asserting "must not pass" has to observe the same code rather than
170
+ // an exception the harness would read as a broken test.
171
+ if (err instanceof DeadScopeError) return 2;
172
+ throw err;
173
+ } finally {
174
+ fs.rmSync(file, { force: true });
175
+ }
176
+ };
177
+ const member = (model: string, stamp: string | null): string =>
178
+ `members:\n m:\n enabled: true\n model: ${model}\n` +
179
+ (stamp === null ? '' : ` verified_at: "${stamp}"\n`);
180
+
181
+ const cases: SelfTestCase[] = [
182
+ { name: 'a sentinel needs no stamp', expect: 'accept', run: () => probe(member('codex-default', null)) },
183
+ { name: 'a vendor alias needs no stamp', expect: 'accept', run: () => probe(member('sonnet', null)) },
184
+ {
185
+ name: 'a dated pin with no stamp is advisory, not a failure',
186
+ expect: 'accept',
187
+ run: () => probe(member('gemini-2.5-pro', null)),
188
+ },
189
+ {
190
+ name: 'a stamp inside the cadence is fresh',
191
+ expect: 'accept',
192
+ run: () => probe(member('gemini-2.5-pro', '2026-08-01')),
193
+ },
194
+ // 100 exactly is fresh; 101 is not. A cadence met on the day it comes
195
+ // due is met — the same boundary `check_corpus_staleness` uses.
196
+ {
197
+ name: 'the cadence boundary is inclusive',
198
+ expect: 'accept',
199
+ run: () => probe(member('gemini-2.5-pro', '2026-05-14')),
200
+ },
201
+ {
202
+ name: 'a stamp past the cadence is refused',
203
+ expect: 'reject',
204
+ run: () => probe(member('gemini-2.5-pro', '2026-01-01')),
205
+ },
206
+ {
207
+ name: 'a future stamp is refused, not read as very fresh',
208
+ expect: 'reject',
209
+ run: () => probe(member('gemini-2.5-pro', '2027-01-01')),
210
+ },
211
+ {
212
+ name: 'an impossible date is refused, not treated as stale',
213
+ expect: 'reject',
214
+ run: () => probe(member('gemini-2.5-pro', '2026-02-30')),
215
+ },
216
+ { name: 'a non-date is refused', expect: 'reject', run: () => probe(member('gemini-2.5-pro', 'soon')) },
217
+ {
218
+ name: 'a member with no model: leaves an empty scan and must not pass',
219
+ expect: 'reject',
220
+ run: () => probe('members:\n m:\n enabled: true\n'),
221
+ },
222
+ ];
223
+ return runSelfTest({
224
+ gate: 'check_council_pin_staleness',
225
+ cases,
226
+ minCases: 10,
227
+ minRejectCases: 5,
228
+ });
229
+ }
230
+
231
+ function main(argv: string[]): number {
232
+ if (argv.includes('--self-test')) return selfTest();
233
+ const ti = argv.indexOf('--today');
234
+ const today =
235
+ ti >= 0 && argv[ti + 1] !== undefined
236
+ ? String(argv[ti + 1])
237
+ : new Date().toISOString().slice(0, 10);
238
+ if (epochDay(today) === null) {
239
+ process.stderr.write(`--today must be a real YYYY-MM-DD date (got ${today})\n`);
240
+ return 2;
241
+ }
242
+ const fi = argv.indexOf('--file');
243
+ const file =
244
+ fi >= 0 && argv[fi + 1] !== undefined
245
+ ? String(argv[fi + 1])
246
+ : path.join(process.cwd(), 'agents/templates/.ai-council.yml.example');
247
+ if (!fs.existsSync(file)) {
248
+ process.stderr.write(`no council config at ${file}\n`);
249
+ return 2;
250
+ }
251
+ const members = parseMembers(fs.readFileSync(file, 'utf-8'));
252
+ const rows = members.map((m) => classify(m.member, m.model, m.verified_at, m.enabled, today));
253
+ const bad = rows.filter((r) => r.verdict === 'stale' || r.verdict === 'future-date' || r.verdict === 'malformed');
254
+ const unstamped = rows.filter((r) => r.verdict === 'unstamped');
255
+
256
+ for (const r of rows) {
257
+ const age = r.age_days === null ? '—' : `${r.age_days}d`;
258
+ process.stdout.write(
259
+ ` ${r.verdict.padEnd(11)} ${r.member.padEnd(12)} ${r.model.padEnd(20)} ${age}${r.enabled ? '' : ' (disabled)'}\n`,
260
+ );
261
+ }
262
+ // Assert the scope and publish the count in one call: a dead scan scope is a
263
+ // usage error, never a pass, and "0 stale pins" over 0 members read is the
264
+ // failure this separates. reportScanned emits the number the assertion just
265
+ // accepted, so the published count cannot drift from the validated one.
266
+ reportScanned({
267
+ gate: 'check_council_pin_staleness',
268
+ scanned: rows.length,
269
+ units: 'council member(s) with a model:',
270
+ roots: [file],
271
+ });
272
+ if (bad.length > 0) {
273
+ process.stdout.write(
274
+ `❌ ${bad.length} pin(s) need attention (cadence ${CADENCE_DAYS}d). A stamp records when a human last\n` +
275
+ ` checked the pin against the provider's own surface — refresh the pin or the date, and prefer a\n` +
276
+ ` vendor sentinel where one exists: a sentinel cannot go stale, so it never needs a stamp.\n`,
277
+ );
278
+ return 1;
279
+ }
280
+ if (unstamped.length > 0) {
281
+ process.stdout.write(
282
+ `⚠️ ${unstamped.length} dated pin(s) carry no verified_at — advisory. Add a stamp, or move the member to a\n` +
283
+ ` vendor sentinel and it drops out of this gate entirely.\n`,
284
+ );
285
+ }
286
+ process.stdout.write('✅ council model pins within cadence.\n');
287
+ return 0;
288
+ }
289
+
290
+ if (process.argv[1] !== undefined && process.argv[1].includes('check_council_pin_staleness')) {
291
+ try {
292
+ process.exit(main(process.argv.slice(2)));
293
+ } catch (err) {
294
+ if (err instanceof DeadScopeError) {
295
+ process.stderr.write(`${err.message}\n`);
296
+ process.exit(2);
297
+ }
298
+ throw err;
299
+ }
300
+ }