@event4u/agent-config 14.18.0 → 14.20.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 (210) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +120 -129
  5. package/README.md +71 -149
  6. package/dist/agent-src/README.md +1 -1
  7. package/dist/agent-src/commands/agent-handoff.md +44 -43
  8. package/dist/agent-src/commands/review/changes.md +19 -19
  9. package/dist/agent-src/commands/review.md +2 -2
  10. package/dist/agent-src/contexts/execution/host-capability-manifest.md +37 -4
  11. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  12. package/dist/agent-src/rules/git-history-discipline.md +2 -2
  13. package/dist/agent-src/rules/guidelines.md +1 -1
  14. package/dist/agent-src/rules/session-canary.md +2 -2
  15. package/dist/agent-src/rules/token-efficiency.md +1 -1
  16. package/dist/agent-src/rules/untrusted-input-defense.md +0 -2
  17. package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
  18. package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
  19. package/dist/agent-src/skills/design-intelligence/SKILL.md +1 -1
  20. package/dist/agent-src/skills/design-intelligence/data/app-interface.csv +1 -1
  21. package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -17
  22. package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +2 -2
  23. package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +17 -0
  24. package/dist/agent-src/skills/design-intelligence/references/design-rules-checklist.md +4 -4
  25. package/dist/agent-src/skills/design-review/SKILL.md +1 -1
  26. package/dist/agent-src/skills/design-variations/SKILL.md +1 -1
  27. package/dist/agent-src/skills/evaluate-llm-feature/SKILL.md +1 -1
  28. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
  29. package/dist/agent-src/skills/fe-design/SKILL.md +32 -5
  30. package/dist/agent-src/skills/fe-design/references/craft-floor.md +7 -0
  31. package/dist/agent-src/skills/fe-design/references/design-patterns.md +47 -2
  32. package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -1
  33. package/dist/agent-src/skills/markitdown/SKILL.md +1 -1
  34. package/dist/agent-src/skills/project-analyzer/SKILL.md +0 -6
  35. package/dist/agent-src/skills/react-native-setup/SKILL.md +3 -3
  36. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
  37. package/dist/agent-src/skills/sequential-thinking/SKILL.md +18 -8
  38. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +28 -6
  39. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
  40. package/dist/agent-src/skills/test-driven-development/SKILL.md +1 -3
  41. package/dist/agent-src/skills/ui-apply-generic/SKILL.md +1 -1
  42. package/dist/agent-src/skills/ui-component-architect/SKILL.md +1 -1
  43. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +17 -2
  44. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  45. package/dist/agent-src/templates/roadmaps.md +14 -1
  46. package/dist/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
  47. package/dist/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
  48. package/dist/agent-src/templates/scripts/telemetry_report.ts +22 -2
  49. package/dist/cli/commands/recordTriggerEval.js +2 -2
  50. package/dist/cli/main.js +1 -1
  51. package/dist/cli/mcp/dispatch.js +10 -0
  52. package/dist/cli/mcp/stdio.js +33 -2
  53. package/dist/cli/mcp/telemetry.js +227 -0
  54. package/dist/cli-delegate/{chunk-AKJGUQNO.js → chunk-C6IPZRDA.js} +484 -353
  55. package/dist/cli-delegate/{chunk-CYH5SP6M.js → chunk-I6RHFQEG.js} +151 -16
  56. package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-NUMQHLS5.js} +304 -136
  57. package/dist/cli-delegate/{chunk-PMEKGDED.js → chunk-PI2GU3DP.js} +163 -1
  58. package/dist/cli-delegate/{chunk-7PRE6ZV2.js → chunk-YS6XTLEK.js} +199 -4
  59. package/dist/cli-delegate/chunk-ZVBOHSVF.js +1109 -0
  60. package/dist/cli-delegate/cmd_adr_effective.js +318 -81
  61. package/dist/cli-delegate/cmd_conformance.js +489 -121
  62. package/dist/cli-delegate/cmd_doctor.js +2 -2
  63. package/dist/cli-delegate/cmd_export.js +2 -2
  64. package/dist/cli-delegate/cmd_handoff.js +42 -12
  65. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  66. package/dist/cli-delegate/cmd_prune.js +2 -2
  67. package/dist/cli-delegate/cmd_route_audit.js +2 -2
  68. package/dist/cli-delegate/cmd_session_recycle.js +59 -902
  69. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  70. package/dist/cli-delegate/cmd_sync.js +2 -2
  71. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  72. package/dist/cli-delegate/cmd_validate.js +2 -2
  73. package/dist/discovery/deprecation-report.md +1 -1
  74. package/dist/discovery/discovery-manifest.json +34 -33
  75. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  76. package/dist/discovery/discovery-manifest.summary.md +1 -1
  77. package/dist/discovery/dormancy-report.md +1 -1
  78. package/dist/discovery/orphan-report.md +1 -1
  79. package/dist/discovery/packs.json +2 -2
  80. package/dist/discovery/trust-report.md +1 -1
  81. package/dist/discovery/workspaces.json +2 -2
  82. package/dist/hooks/dispatch.js +4455 -3025
  83. package/dist/install/install.mjs +975 -689
  84. package/dist/mcp/registry-manifest.json +2 -2
  85. package/dist/mcp/server.json +2 -2
  86. package/dist/mcp/server.mjs +2087 -1134
  87. package/dist/scripts/_lib/model_tier.js +1 -1
  88. package/dist/scripts/mcp_server/telemetry.js +148 -0
  89. package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
  90. package/docs/guidelines/agent-infra/naming.md +1 -1
  91. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
  92. package/docs/guidelines/agent-infra/security-lint-containment.md +46 -6
  93. package/docs/guidelines/agent-infra/self-improvement-pipeline.md +4 -4
  94. package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +1 -1
  95. package/docs/guidelines/design-antipatterns.md +87 -64
  96. package/docs/guidelines/redundancy-taxonomy.md +28 -0
  97. package/package.json +1 -1
  98. package/src/agent-src/README.md +1 -1
  99. package/src/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
  100. package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
  101. package/src/agent-src/templates/scripts/telemetry_report.ts +22 -2
  102. package/src/config/assurance-capability-registry.json +9 -0
  103. package/src/config/ci-local-parity.yml +6 -0
  104. package/src/config/gate-coverage.yml +225 -0
  105. package/src/config/gate-violation-baselines.json +18 -6
  106. package/src/config/release-gate-locality.yml +12 -0
  107. package/src/scripts/_cli/cmd_doctor.ts +2 -2
  108. package/src/scripts/_cli/cmd_handoff.ts +1 -1
  109. package/src/scripts/_cli/cmd_session_recycle.ts +51 -3
  110. package/src/scripts/_cli/cmd_update.ts +1 -1
  111. package/src/scripts/_cli/cmd_versions.ts +1 -1
  112. package/src/scripts/_lib/adr_frontmatter.ts +44 -0
  113. package/src/scripts/_lib/audit_field_provenance.ts +44 -3
  114. package/src/scripts/_lib/consequence_objects.ts +152 -0
  115. package/src/scripts/_lib/exec_evidence.ts +7 -0
  116. package/src/scripts/_lib/global_deploy_inventory.ts +2 -2
  117. package/src/scripts/_lib/host_capability.ts +38 -0
  118. package/src/scripts/_lib/host_permission_checks.ts +251 -0
  119. package/src/scripts/_lib/install_reach_checks.ts +33 -1
  120. package/src/scripts/_lib/mcp_bridge.ts +222 -10
  121. package/src/scripts/_lib/mcp_consent_residual.ts +173 -0
  122. package/src/scripts/_lib/model_tier.ts +1 -1
  123. package/src/scripts/_lib/obligation_frequency.ts +2 -2
  124. package/src/scripts/_lib/orchestration_record.ts +23 -1
  125. package/src/scripts/_lib/recycle_envelope_paths.ts +190 -0
  126. package/src/scripts/_lib/release_highlights.ts +93 -0
  127. package/src/scripts/_lib/release_material.ts +64 -1
  128. package/src/scripts/_lib/release_position.ts +96 -0
  129. package/src/scripts/_lib/review_skipped_record.ts +17 -1
  130. package/src/scripts/_lib/runtime_journal.ts +26 -3
  131. package/src/scripts/_lib/runtime_wiring_checks.ts +12 -1
  132. package/src/scripts/_lib/security_lint.ts +179 -7
  133. package/src/scripts/_lib/self_repair.ts +141 -6
  134. package/src/scripts/_lib/self_repair_store.ts +273 -7
  135. package/src/scripts/_lib/session_register.ts +11 -6
  136. package/src/scripts/_lib/sqlite_schema.ts +22 -0
  137. package/src/scripts/_lib/subagent_capsule.ts +155 -3
  138. package/src/scripts/_lib/surface.ts +86 -0
  139. package/src/scripts/_lib/ui_authority.ts +116 -0
  140. package/src/scripts/adr/regenerate_index.ts +54 -1
  141. package/src/scripts/adr_cite_check.ts +71 -15
  142. package/src/scripts/ai_council/clients.ts +1 -1
  143. package/src/scripts/autonomy_friction_corpus.ts +229 -0
  144. package/src/scripts/build_proof.ts +15 -2
  145. package/src/scripts/build_rule_trigger_matrix.ts +5 -0
  146. package/src/scripts/check_beta_review_markers.ts +141 -17
  147. package/src/scripts/check_enforcement_coverage.ts +1 -1
  148. package/src/scripts/check_enforcement_denominator.ts +121 -11
  149. package/src/scripts/check_finding_dispositions.ts +66 -9
  150. package/src/scripts/check_gate_coverage.ts +96 -0
  151. package/src/scripts/check_mcp_doc_drift.ts +217 -0
  152. package/src/scripts/check_pack_size.ts +321 -3
  153. package/src/scripts/check_release_highlights.ts +74 -1
  154. package/src/scripts/check_skill_activation_claim.ts +179 -0
  155. package/src/scripts/check_trigger_evals.ts +1 -1
  156. package/src/scripts/conformance_scan.ts +44 -3
  157. package/src/scripts/explain_run.ts +1 -1
  158. package/src/scripts/generate_capability_matrix.ts +1 -1
  159. package/src/scripts/git_authorization_hook.ts +75 -1
  160. package/src/scripts/handoff_context_hook.ts +54 -7
  161. package/src/scripts/hook_manifest.json +1 -1
  162. package/src/scripts/hook_manifest.yaml +14 -11
  163. package/src/scripts/hooks/augment-dispatcher.sh +1 -1
  164. package/src/scripts/hooks/category_a.ts +291 -0
  165. package/src/scripts/hooks/cline-dispatcher.sh +1 -1
  166. package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
  167. package/src/scripts/hooks/dispatch_hook.ts +17 -22
  168. package/src/scripts/hooks/git_command_classifier.ts +156 -1
  169. package/src/scripts/hooks/host_lowering.ts +215 -0
  170. package/src/scripts/hooks/host_lowering.yaml +185 -0
  171. package/src/scripts/hooks/host_semantics.ts +105 -37
  172. package/src/scripts/hooks/journal_record_hook.ts +10 -0
  173. package/src/scripts/hooks/permission_gate.ts +117 -0
  174. package/src/scripts/hooks/session_eol_hook.ts +18 -7
  175. package/src/scripts/hooks_status.ts +12 -10
  176. package/src/scripts/install.ts +57 -86
  177. package/src/scripts/install_anthropic_key.sh +2 -2
  178. package/src/scripts/lint_agent_security.ts +316 -25
  179. package/src/scripts/lint_confusables.ts +42 -6
  180. package/src/scripts/lint_design_antipattern_parity.ts +80 -0
  181. package/src/scripts/lint_evidence_artifacts.ts +45 -1
  182. package/src/scripts/lint_handoffs.ts +0 -132
  183. package/src/scripts/lint_hidden_unicode.ts +49 -7
  184. package/src/scripts/lint_hook_manifest.ts +87 -3
  185. package/src/scripts/lint_instruction_smuggling.ts +42 -6
  186. package/src/scripts/lint_mcp_config_security.ts +42 -6
  187. package/src/scripts/lint_motion_authority_drift.ts +731 -0
  188. package/src/scripts/lint_readme_size.ts +158 -12
  189. package/src/scripts/lint_reasoning_fixtures.ts +216 -0
  190. package/src/scripts/lint_roadmap_later_disposition.ts +217 -27
  191. package/src/scripts/lint_skill_frontmatter_safety.ts +43 -7
  192. package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
  193. package/src/scripts/mcp_server/resources.ts +41 -0
  194. package/src/scripts/mcp_server/telemetry.ts +19 -4
  195. package/src/scripts/measure_sibling_naming.ts +150 -0
  196. package/src/scripts/orchestration_record.ts +12 -1
  197. package/src/scripts/redact_hook_capture.ts +1 -1
  198. package/src/scripts/release.ts +24 -24
  199. package/src/scripts/release_publication.ts +6 -2
  200. package/src/scripts/report_skill_activation.ts +319 -7
  201. package/src/scripts/schemas/review-findings.schema.json +5 -0
  202. package/src/scripts/schemas/skill.schema.json +2 -2
  203. package/src/scripts/self_repair_cli.ts +31 -1
  204. package/src/scripts/session_canary_hook.ts +1 -1
  205. package/src/scripts/session_register_hook.ts +4 -3
  206. package/src/scripts/skill_scout.ts +69 -0
  207. package/src/scripts/trigger_eval_grandfather.json +5 -1
  208. package/src/scripts/validate_agent_settings.ts +1 -1
  209. package/dist/cli-delegate/chunk-2GNWGMAL.js +0 -172
  210. package/dist/cli-delegate/chunk-LPPWO5CE.js +0 -161
@@ -0,0 +1,229 @@
1
+ /**
2
+ * autonomy_friction_corpus — how many host confirmations each payload class
3
+ * costs, measured through the real dispatcher.
4
+ *
5
+ * WHY A CONFIRMATION COUNT AND NOT A GATE COUNT. "Three gates fire" says
6
+ * nothing about what the operator experiences; "this call costs one
7
+ * confirmation" is the quantity the round is about. A corpus counting gates
8
+ * would go green on a change that fired fewer gates and prompted just as often.
9
+ *
10
+ * WHAT IT MEASURES, STATED NARROWLY. The number of confirmations THIS PACKAGE
11
+ * leaves on the table for a call — 0 when the dispatcher hands the host an
12
+ * explicit `permissionDecision: allow`, 1 when it emits no permission field and
13
+ * the host's own machinery decides. It is NOT a measurement of what the host
14
+ * finally does: a consumer whose settings already allow the call is not
15
+ * prompted either way, and a confined session ignores our allow entirely
16
+ * (`permissionDecision=allow ignored: a confined session takes grants only
17
+ * from its command line`). Reading this as a host-behaviour measurement is the
18
+ * fact-about-our-plumbing-written-as-a-fact-about-the-host collapse the hook
19
+ * contract warns about three times.
20
+ *
21
+ * A REFUSAL IS NOT A CONFIRMATION. A case the dispatcher blocks (exit 2) is
22
+ * reported as `refused` and scores no confirmations — counting a refusal as
23
+ * friction would reward removing a gate, which is the opposite of the point.
24
+ *
25
+ * Usage:
26
+ * ./scripts-run src/scripts/autonomy_friction_corpus [--json]
27
+ * ./agent-config … (no verb — this is a package-internal measurement)
28
+ */
29
+ import { spawnSync } from "node:child_process";
30
+ import * as fs from "node:fs";
31
+ import * as os from "node:os";
32
+ import * as path from "node:path";
33
+ import process from "node:process";
34
+ import { fileURLToPath } from "node:url";
35
+
36
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
37
+ const REPO_ROOT = path.resolve(HERE, "..", "..");
38
+ const ENTRY = path.join(REPO_ROOT, "src", "scripts", "hooks", "dispatch_entry.ts");
39
+ const TSX_BIN = path.join(
40
+ REPO_ROOT,
41
+ "node_modules",
42
+ ".bin",
43
+ process.platform === "win32" ? "tsx.cmd" : "tsx",
44
+ );
45
+
46
+ /** One payload class, with the confirmations it is expected to cost. */
47
+ export interface FrictionCase {
48
+ id: string;
49
+ /** The class the owner named, in their words. */
50
+ klass: string;
51
+ tool: string;
52
+ input: Record<string, unknown>;
53
+ /** Confirmations this call SHOULD cost. */
54
+ expected: number;
55
+ /** Why that number, in one line. */
56
+ because: string;
57
+ }
58
+
59
+ /** One measured result. */
60
+ export interface FrictionResult extends FrictionCase {
61
+ observed: number;
62
+ refused: boolean;
63
+ pass: boolean;
64
+ }
65
+
66
+ /**
67
+ * The six classes the round named, plus the directory-flag form.
68
+ *
69
+ * The seventh is not padding: cases 1 and 2 are the shape the canon stopped
70
+ * teaching, and without the shape that replaced them the corpus would record
71
+ * the cost and not the fix.
72
+ */
73
+ export const CASES: readonly FrictionCase[] = [
74
+ {
75
+ id: "directory-change",
76
+ klass: "directory change",
77
+ tool: "Bash",
78
+ input: { command: "cd sub" },
79
+ expected: 1,
80
+ because:
81
+ "a bare `cd` is not category A — the canon's answer is a directory flag, " +
82
+ "not an allow for changing directory",
83
+ },
84
+ {
85
+ id: "directory-change-compounded",
86
+ klass: "directory change compounded with a status read",
87
+ tool: "Bash",
88
+ input: { command: "cd sub && git status" },
89
+ expected: 1,
90
+ because:
91
+ "a compound command is refused before its argv is read — a safe head " +
92
+ "token must not be able to carry a second command",
93
+ },
94
+ {
95
+ id: "directory-flag-status",
96
+ klass: "the directory-flag form of the case above",
97
+ tool: "Bash",
98
+ input: { command: "git -C sub status" },
99
+ expected: 0,
100
+ because: "one simple command, a read-only git subcommand, inside the working tree",
101
+ },
102
+ {
103
+ id: "test-runner-in-subdirectory",
104
+ klass: "a test runner invoked from a subdirectory",
105
+ tool: "Bash",
106
+ input: { command: "vitest run sub" },
107
+ expected: 0,
108
+ because: "a test runner admitted by name — the name IS the operation",
109
+ },
110
+ {
111
+ id: "workspace-package-manager",
112
+ klass: "a workspace package manager",
113
+ tool: "Bash",
114
+ input: { command: "npm run test" },
115
+ expected: 0,
116
+ because: "a declared subcommand with a declared script name",
117
+ },
118
+ {
119
+ id: "consequence-authorized",
120
+ klass: "a consequence operation with the authorizing phrase present",
121
+ tool: "Bash",
122
+ input: { command: "terraform apply" },
123
+ expected: 1,
124
+ because:
125
+ "the phrase changes the RECORD, never the prompt — after ADR-254 the " +
126
+ "Hard Floor is model-held and this package grants nothing",
127
+ },
128
+ {
129
+ id: "consequence-unauthorized",
130
+ klass: "the same operation with the authorizing phrase absent",
131
+ tool: "Bash",
132
+ input: { command: "terraform apply" },
133
+ expected: 1,
134
+ because:
135
+ "identical to the case above BY DESIGN — that the two are the same " +
136
+ "number is the finding, not a defect in the corpus",
137
+ },
138
+ ];
139
+
140
+ /** Run one case through the real dispatcher and count its confirmations. */
141
+ export function measure(c: FrictionCase, workspace: string): FrictionResult {
142
+ const payload = JSON.stringify({
143
+ session_id: "autonomy-friction-corpus",
144
+ cwd: workspace,
145
+ hook_event_name: "PreToolUse",
146
+ tool_name: c.tool,
147
+ tool_input: c.input,
148
+ });
149
+ const r = spawnSync(
150
+ TSX_BIN,
151
+ [ENTRY, "--platform", "claude", "--event", "pre_tool_use", "--project-dir", workspace],
152
+ {
153
+ encoding: "utf-8",
154
+ cwd: workspace,
155
+ input: payload,
156
+ timeout: 120_000,
157
+ env: { ...process.env, AGENT_CONFIG_PACKAGE_ROOT: REPO_ROOT },
158
+ },
159
+ );
160
+ const refused = r.status === 2;
161
+ const allowed = !refused && (r.stdout ?? "").includes('"permissionDecision":"allow"');
162
+ const observed = refused || allowed ? 0 : 1;
163
+ return { ...c, observed, refused, pass: refused || observed === c.expected };
164
+ }
165
+
166
+ export function runCorpus(): FrictionResult[] {
167
+ const workspace = fs.mkdtempSync(path.join(os.tmpdir(), "ac-friction-"));
168
+ fs.mkdirSync(path.join(workspace, "sub"), { recursive: true });
169
+ try {
170
+ return CASES.map((c) => measure(c, workspace));
171
+ } finally {
172
+ fs.rmSync(workspace, { recursive: true, force: true });
173
+ }
174
+ }
175
+
176
+ export function render(results: readonly FrictionResult[]): string {
177
+ const out: string[] = [];
178
+ out.push("autonomy_friction_corpus — confirmations per payload class");
179
+ out.push("");
180
+ out.push(" measured through the real dispatcher; a confirmation is one this");
181
+ out.push(" package leaves to the host, NOT one the host necessarily shows.");
182
+ out.push("");
183
+ for (const r of results) {
184
+ const mark = r.pass ? "✅" : "❌";
185
+ const obs = r.refused ? "refused" : String(r.observed);
186
+ out.push(` ${mark} ${r.id.padEnd(30)} expected ${r.expected} observed ${obs}`);
187
+ out.push(` ${r.klass}`);
188
+ out.push(` ${r.because}`);
189
+ }
190
+ const failures = results.filter((r) => !r.pass);
191
+ out.push("");
192
+ out.push(
193
+ failures.length === 0
194
+ ? ` ${results.length} case(s), every observation matches its expectation`
195
+ : ` ${failures.length} of ${results.length} case(s) miss their expectation: ` +
196
+ failures.map((f) => f.id).join(", "),
197
+ );
198
+ return out.join("\n") + "\n";
199
+ }
200
+
201
+ export function main(argv: string[] = process.argv.slice(2)): number {
202
+ const json = argv.includes("--json");
203
+ if (argv.includes("--help") || argv.includes("-h")) {
204
+ process.stdout.write(
205
+ "usage: autonomy_friction_corpus [--json]\n" +
206
+ " Reports the confirmations each payload class costs, measured\n" +
207
+ " through the real dispatcher. Exit 1 when an observation misses\n" +
208
+ " its expectation.\n",
209
+ );
210
+ return 0;
211
+ }
212
+ const results = runCorpus();
213
+ process.stdout.write(json ? `${JSON.stringify(results, null, 2)}\n` : render(results));
214
+ return results.every((r) => r.pass) ? 0 : 1;
215
+ }
216
+
217
+ function _isCliEntry(): boolean {
218
+ const argv1 = process.argv[1];
219
+ if (argv1 === undefined) return false;
220
+ try {
221
+ return fs.realpathSync(argv1) === fs.realpathSync(fileURLToPath(import.meta.url));
222
+ } catch {
223
+ return false;
224
+ }
225
+ }
226
+
227
+ if (_isCliEntry()) {
228
+ process.exit(main());
229
+ }
@@ -413,10 +413,20 @@ function render(): string {
413
413
  {
414
414
  const rows = collectEnforcement();
415
415
  const s = summariseEnforcement(rows);
416
+ // The split ships WITH the figure, and the word `yet` travels only with
417
+ // the half it is true of. `kernel_denied` rules cannot carry an
418
+ // `enforced_by` field at all — `block_kernel_rule_writes` refuses the
419
+ // write with no agent-accessible override — so publishing them as "not
420
+ // declared yet" promises a future that does not exist for them. They
421
+ // still count as uncovered — the counting policy is unchanged; only the
422
+ // claim about their future is.
416
423
  L.push(
417
424
  `**Axis 1 — enforcement level per rule.** ${s.total} rules · ` +
418
425
  `${s.blocking} blocking (${s.blocking_pct}%) · ${s.observer} observer · ` +
419
- `${s.local_only} local-only · ${s.undeclared} undeclared (no \`enforced_by\` yet).`,
426
+ `${s.local_only} local-only · ${s.undeclared} undeclared — of which ` +
427
+ `${s.kernel_denied} kernel-denied (\`block_kernel_rule_writes\` refuses an ` +
428
+ `\`enforced_by\` write on a kernel rule, so no declaration is reachable ` +
429
+ `for them at all) and ${s.undeclared - s.kernel_denied} not declared yet.`,
420
430
  );
421
431
  L.push('');
422
432
  // The denominator, WITH the frame that produced it. Until 2026-08-23 the
@@ -436,7 +446,10 @@ function render(): string {
436
446
  L.push(`| \`${r.id}\` | ${r.effective} | ${decl} |`);
437
447
  }
438
448
  L.push('');
439
- L.push(`Undeclared rules (${s.undeclared}) carry no row — an honest gap beats a false claim.`);
449
+ L.push(
450
+ `Undeclared rules (${s.undeclared}, of which ${s.kernel_denied} kernel-denied) ` +
451
+ 'carry no row — an honest gap beats a false claim.',
452
+ );
440
453
  }
441
454
  L.push('');
442
455
  {
@@ -126,6 +126,11 @@ add('artifact-engagement-recording.md', 'phase-step / task end', 'mechanical-alr
126
126
  add('model-recommendation.md', 'task-start / topic-shift', 'hook',
127
127
  'output', 'low', '2a', 'no',
128
128
  'Phase 5 prototype target. Marker injection at first user msg + topic-change detection.');
129
+ // HISTORICAL row. `src/rules/capture-learnings.md` no longer exists, so this
130
+ // classification is never emitted (the generator keys on files actually present
131
+ // in src/rules/); the live rule for this trigger is `skill-improvement-trigger`,
132
+ // classified two rows below. Kept rather than deleted so the inventory this
133
+ // table records stays readable against the round it was taken in.
129
134
  add('capture-learnings.md', 'task completion', 'hook', 'output',
130
135
  'medium', '2a', 'no', 'Post-task marker; learning detection is fuzzy');
131
136
  add('skill-improvement-trigger.md', 'task completion (settings.skill_improvement)',
@@ -63,28 +63,34 @@ interface LapsedBaseline {
63
63
  contracts: string[];
64
64
  }
65
65
 
66
- let _baselineCache: Set<string> | null = null;
66
+ // Keyed by root: an unkeyed cache returns the REPO baseline for a fixture root
67
+ // whenever the repo was read first, which silently judges a fixture against
68
+ // production data and is invisible in the result.
69
+ const _baselineCache = new Map<string, Set<string>>();
67
70
 
68
71
  export function loadLapsedBaseline(root: string = ROOT): Set<string> {
69
- if (_baselineCache !== null) {
70
- return _baselineCache;
72
+ const hit = _baselineCache.get(root);
73
+ if (hit !== undefined) {
74
+ return hit;
71
75
  }
72
76
  const p = path.join(root, BASELINE_REL);
73
77
  if (!fs.existsSync(p)) {
74
78
  // The baseline reaching zero is the SUCCESS state: the file is deleted
75
79
  // in the same change that flips the severity. An absent file therefore
76
80
  // means "no inherited debt", i.e. every lapse is fresh and errors.
77
- _baselineCache = new Set<string>();
78
- return _baselineCache;
81
+ const empty = new Set<string>();
82
+ _baselineCache.set(root, empty);
83
+ return empty;
79
84
  }
80
85
  const parsed = JSON.parse(fs.readFileSync(p, 'utf-8')) as LapsedBaseline;
81
- _baselineCache = new Set(parsed.contracts);
82
- return _baselineCache;
86
+ const loaded = new Set(parsed.contracts);
87
+ _baselineCache.set(root, loaded);
88
+ return loaded;
83
89
  }
84
90
 
85
91
  /** Test seam — the module-level cache would otherwise outlive a fixture. */
86
92
  export function _resetLapsedBaseline(): void {
87
- _baselineCache = null;
93
+ _baselineCache.clear();
88
94
  }
89
95
 
90
96
  /**
@@ -115,12 +121,43 @@ const SUPERSEDED_RE = /^superseded-by:\s*\S+\s*$/m;
115
121
 
116
122
  const MAX_REVIEW_WINDOW_DAYS = 90;
117
123
 
124
+ /**
125
+ * Forward horizon for the upcoming-lapse report.
126
+ *
127
+ * The gate reports a contract on the day it lapses, which is the day it is
128
+ * already too late to decide anything: a FRESH lapse errors, and every pull
129
+ * request in the repository reds until someone edits a contract under time
130
+ * pressure. The horizon names the set that is ABOUT to enter that branch.
131
+ *
132
+ * The number is bounded on both sides: too wide and the report names most of
133
+ * the forward-dated corpus on every run, which is the same as naming none; too
134
+ * narrow and it arrives after the decision needed to be made.
135
+ */
136
+ const HORIZON_DAYS = 14;
137
+
118
138
  interface Violation {
119
139
  file: string;
120
140
  reason: string;
121
141
  severity: 'error' | 'warning';
122
142
  }
123
143
 
144
+ /**
145
+ * A contract whose window closes within {@link HORIZON_DAYS} and which is NOT
146
+ * in the frozen baseline — i.e. one that will enter the ERROR branch, not the
147
+ * inherited-warning one.
148
+ *
149
+ * Deliberately NOT a {@link Violation}: it carries no severity and never moves
150
+ * the exit code. A third severity class printed into the same stream as the 84
151
+ * standing warnings is the way this work changes nothing (risk 1 on the
152
+ * roadmap's own register), so the horizon is a separate, clearly-labelled
153
+ * section and the exit code stays a function of real violations alone.
154
+ */
155
+ interface Upcoming {
156
+ file: string;
157
+ date: string;
158
+ daysOut: number;
159
+ }
160
+
124
161
  function _exists(p: string): boolean {
125
162
  try {
126
163
  fs.statSync(p);
@@ -184,7 +221,7 @@ function _ordinalToISO(ordinal: number): string {
184
221
  return `${y}-${m}-${d}`;
185
222
  }
186
223
 
187
- function check_one(p: string, todayOrdinal: number): Violation[] {
224
+ function check_one(p: string, todayOrdinal: number, root: string = ROOT): Violation[] {
188
225
  const fm = read_frontmatter(p);
189
226
  if (fm === null) {
190
227
  return [];
@@ -199,7 +236,7 @@ function check_one(p: string, todayOrdinal: number): Violation[] {
199
236
  ['superseded-by', SUPERSEDED_RE.test(fm)],
200
237
  ];
201
238
  const setMarkers = markers.filter(([, present]) => present).map(([name]) => name);
202
- const rel = _relPosix(p, ROOT);
239
+ const rel = _relPosix(p, root);
203
240
  if (setMarkers.length === 0) {
204
241
  return [
205
242
  {
@@ -231,7 +268,7 @@ function check_one(p: string, todayOrdinal: number): Violation[] {
231
268
  // bound because a lapsed date can never also exceed the forward window,
232
269
  // so the order is a readability choice rather than a precedence one.
233
270
  if (reviewOrdinal < todayOrdinal) {
234
- const inherited = loadLapsedBaseline().has(rel);
271
+ const inherited = loadLapsedBaseline(root).has(rel);
235
272
  const age = todayOrdinal - reviewOrdinal;
236
273
  return [
237
274
  {
@@ -263,6 +300,34 @@ function check_one(p: string, todayOrdinal: number): Violation[] {
263
300
  return [];
264
301
  }
265
302
 
303
+ /**
304
+ * The upcoming-lapse finding for one contract, or `null`.
305
+ *
306
+ * Baselined contracts are excluded EXPLICITLY rather than by relying on the
307
+ * fact that a baselined contract is already lapsed and so cannot also carry a
308
+ * future date. That implication holds today and is not a property anything
309
+ * enforces; a guard that is merely vacuous cannot be distinguished from a
310
+ * guard that is absent, and the difference shows up the first time a baseline
311
+ * entry is re-dated forward.
312
+ */
313
+ export function upcoming_one(
314
+ p: string,
315
+ todayOrdinal: number,
316
+ horizonDays: number = HORIZON_DAYS,
317
+ root: string = ROOT,
318
+ ): Upcoming | null {
319
+ const fm = read_frontmatter(p);
320
+ if (fm === null || !/^stability:\s*beta\s*$/m.test(fm)) return null;
321
+ const km = KEEP_RE.exec(fm);
322
+ if (!km) return null;
323
+ const rel = _relPosix(p, root);
324
+ if (loadLapsedBaseline(root).has(rel)) return null;
325
+ const [ry, rm, rd] = _parseISODate(km[1]!);
326
+ const reviewOrdinal = _dateOrdinal(ry, rm, rd);
327
+ const daysOut = reviewOrdinal - todayOrdinal;
328
+ if (daysOut < 0 || daysOut > horizonDays) return null;
329
+ return { file: rel, date: _ordinalToISO(reviewOrdinal), daysOut };
330
+ }
266
331
 
267
332
  /**
268
333
  * STABILITY.md's own re-audit condition, as a command instead of as prose.
@@ -302,10 +367,16 @@ interface ParsedArgs {
302
367
  trigger: boolean;
303
368
  /** Fixture root, for `--self-test`. A fixture is never judged against the repo baseline. */
304
369
  root: string | null;
370
+ horizon: number;
305
371
  }
306
372
 
307
373
  function parse_args(argv: readonly string[]): ParsedArgs {
308
- const args: ParsedArgs = { json: false, trigger: false, root: null };
374
+ const args: ParsedArgs = {
375
+ json: false,
376
+ trigger: false,
377
+ root: null,
378
+ horizon: HORIZON_DAYS,
379
+ };
309
380
  for (let i = 0; i < argv.length; i += 1) {
310
381
  const arg = argv[i] as string;
311
382
  if (arg === '--json') {
@@ -313,14 +384,27 @@ function parse_args(argv: readonly string[]): ParsedArgs {
313
384
  } else if (arg === '--root') {
314
385
  args.root = argv[i + 1] ?? null;
315
386
  i += 1;
387
+ } else if (arg === '--horizon') {
388
+ const raw = argv[i + 1] ?? '';
389
+ const n = Number.parseInt(raw, 10);
390
+ if (!/^\d+$/.test(raw) || Number.isNaN(n)) {
391
+ process.stderr.write(
392
+ `check_beta_review_markers: error: --horizon needs a non-negative integer, got: ${raw || '(nothing)'}\n`,
393
+ );
394
+ process.exit(2);
395
+ }
396
+ args.horizon = n;
397
+ i += 1;
316
398
  } else if (arg === '--trigger') {
317
399
  args.trigger = true;
318
400
  } else if (arg === '-h' || arg === '--help') {
319
401
  process.stdout.write(
320
- 'usage: check_beta_review_markers [-h] [--json] [--trigger]\n' +
402
+ 'usage: check_beta_review_markers [-h] [--json] [--trigger] [--horizon N]\n' +
321
403
  ' --trigger print the lapsed percentage and whether STABILITY.md\n' +
322
404
  ' re-audit condition (>= 25 %) has fired. Reports; never fails.\n' +
323
- ' --root DIR judge a fixture tree instead of the repo (self-test seam).\n',
405
+ ' --root DIR judge a fixture tree instead of the repo (self-test seam).\n' +
406
+ ` --horizon N days ahead to report upcoming fresh lapses (default ${String(HORIZON_DAYS)}).\n` +
407
+ ' Advisory: never changes the exit code.\n',
324
408
  );
325
409
  process.exit(0);
326
410
  } else {
@@ -400,11 +484,15 @@ function main(): number {
400
484
  const scanRoot = args.root ?? ROOT;
401
485
  const contracts = _globMdSorted(path.join(scanRoot, CONTRACTS_DIR));
402
486
  let betaContracts = 0;
487
+ const upcoming: Upcoming[] = [];
403
488
  for (const p of contracts) {
404
489
  const fm = read_frontmatter(p);
405
490
  if (fm !== null && /^stability:\s*beta\s*$/m.test(fm)) betaContracts += 1;
406
- violations.push(...check_one(p, todayOrdinal));
491
+ violations.push(...check_one(p, todayOrdinal, scanRoot));
492
+ const due = upcoming_one(p, todayOrdinal, args.horizon, scanRoot);
493
+ if (due !== null) upcoming.push(due);
407
494
  }
495
+ upcoming.sort((a, b) => a.daysOut - b.daysOut || a.file.localeCompare(b.file));
408
496
  // Count every contract read, not the `stability: beta` subset: over a moved
409
497
  // `docs/contracts/` "no beta contracts" and "no contracts at all" produce
410
498
  // the same clean line, and only the second is a dead gate. Exit 1 is the
@@ -446,7 +534,9 @@ function main(): number {
446
534
  return 0;
447
535
  }
448
536
  if (args.json) {
449
- process.stdout.write(JSON.stringify({ violations }, null, 2) + '\n');
537
+ process.stdout.write(
538
+ JSON.stringify({ violations, upcoming, horizonDays: args.horizon }, null, 2) + '\n',
539
+ );
450
540
  } else {
451
541
  if (violations.length === 0) {
452
542
  process.stdout.write('✅ All beta contracts carry a valid review marker.\n');
@@ -455,7 +545,39 @@ function main(): number {
455
545
  const icon = v.severity === 'error' ? '❌' : '⚠️ ';
456
546
  process.stdout.write(`${icon} ${v.file}: ${v.reason}\n`);
457
547
  }
458
- process.stdout.write(`\n${violations.length} violation(s).\n`);
548
+ // Both counts, and the relationship between them, because a
549
+ // violation count alone cannot answer "how many contracts is this".
550
+ // Today they are equal — check_one returns at most one finding per
551
+ // contract — and printing only one of them hides the fact that it
552
+ // is the size of the decision, not just the size of the output.
553
+ const distinct = new Set(violations.map((v) => v.file)).size;
554
+ const baselineSize = loadLapsedBaseline(scanRoot).size;
555
+ const reportedInherited = violations.filter((v) =>
556
+ loadLapsedBaseline(scanRoot).has(v.file),
557
+ ).length;
558
+ process.stdout.write(
559
+ `\n${String(violations.length)} violation(s) across ` +
560
+ `${String(distinct)} distinct contract(s) — one line per finding, so the ` +
561
+ 'two differ only where a contract carries more than one.\n',
562
+ );
563
+ process.stdout.write(
564
+ `Frozen baseline: ${String(baselineSize)} entries · ` +
565
+ `${String(reportedInherited)} still reported as lapsed · ` +
566
+ `${String(baselineSize - reportedInherited)} inert ` +
567
+ '(left the lapsed set by promotion, supersession, or re-dating).\n',
568
+ );
569
+ }
570
+ if (upcoming.length > 0) {
571
+ process.stdout.write(
572
+ `\nUpcoming FRESH lapses within ${String(args.horizon)} day(s) — advisory, ` +
573
+ 'exit code unchanged. These are absent from the frozen baseline, so each\n' +
574
+ 'becomes an ERROR on its date rather than an inherited warning:\n',
575
+ );
576
+ for (const u of upcoming) {
577
+ process.stdout.write(
578
+ ` ${u.file}: keep-beta-until=${u.date} in ${String(u.daysOut)} day(s) [fresh]\n`,
579
+ );
580
+ }
459
581
  }
460
582
  }
461
583
  return violations.some((v) => v.severity === 'error') ? 1 : 0;
@@ -495,9 +617,11 @@ if (_isCliEntry() || process.argv[1] === _HERE) {
495
617
 
496
618
  export {
497
619
  type Violation,
620
+ type Upcoming,
498
621
  ROOT,
499
622
  CONTRACTS_DIR,
500
623
  MAX_REVIEW_WINDOW_DAYS,
624
+ HORIZON_DAYS,
501
625
  read_frontmatter,
502
626
  check_one,
503
627
  main,
@@ -118,7 +118,7 @@ export interface RuleCoverage {
118
118
  /**
119
119
  * Carrier period per hook-capable platform — never a single scalar.
120
120
  *
121
- * A scalar is wrong twice over: copilot has no hook surface at all, so a
121
+ * A scalar is wrong twice over: this package binds nothing on copilot, so a
122
122
  * weakest-platform collapse turns one platform property into a finding on
123
123
  * every hook-carried rule; and cline maps `stop` from `TaskCancel`, so the
124
124
  * same slot means per-turn on six platforms and per-interruption there. Both