@event4u/agent-config 14.17.0 → 14.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +207 -77
  5. package/README.md +72 -150
  6. package/dist/agent-src/README.md +1 -1
  7. package/dist/agent-src/commands/agent-handoff.md +2 -2
  8. package/dist/agent-src/commands/pr/merge.md +35 -44
  9. package/dist/agent-src/commands/review/changes.md +19 -19
  10. package/dist/agent-src/commands/review.md +2 -2
  11. package/dist/agent-src/contexts/execution/host-capability-manifest.md +30 -3
  12. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  13. package/dist/agent-src/rules/git-history-discipline.md +2 -2
  14. package/dist/agent-src/rules/guidelines.md +1 -1
  15. package/dist/agent-src/rules/session-canary.md +2 -2
  16. package/dist/agent-src/rules/token-efficiency.md +1 -1
  17. package/dist/agent-src/skills/accessibility-auditor/SKILL.md +2 -0
  18. package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
  19. package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +27 -1
  20. package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
  21. package/dist/agent-src/skills/code-intelligence/SKILL.md +60 -12
  22. package/dist/agent-src/skills/complexity-first-planning/SKILL.md +27 -0
  23. package/dist/agent-src/skills/design-intelligence/SKILL.md +1 -1
  24. package/dist/agent-src/skills/design-intelligence/data/app-interface.csv +1 -1
  25. package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -17
  26. package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +2 -2
  27. package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +17 -0
  28. package/dist/agent-src/skills/design-intelligence/references/design-rules-checklist.md +4 -4
  29. package/dist/agent-src/skills/design-review/SKILL.md +3 -1
  30. package/dist/agent-src/skills/design-variations/SKILL.md +1 -1
  31. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
  32. package/dist/agent-src/skills/fe-design/SKILL.md +32 -5
  33. package/dist/agent-src/skills/fe-design/references/craft-floor.md +7 -0
  34. package/dist/agent-src/skills/fe-design/references/design-patterns.md +47 -2
  35. package/dist/agent-src/skills/feature-planning/SKILL.md +34 -0
  36. package/dist/agent-src/skills/git-workflow/SKILL.md +37 -12
  37. package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +176 -0
  38. package/dist/agent-src/skills/laravel-mail/SKILL.md +44 -0
  39. package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -1
  40. package/dist/agent-src/skills/project-analyzer/SKILL.md +0 -6
  41. package/dist/agent-src/skills/react-native-setup/SKILL.md +3 -3
  42. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
  43. package/dist/agent-src/skills/sequential-thinking/SKILL.md +18 -8
  44. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
  45. package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +40 -0
  46. package/dist/agent-src/skills/ui-apply-generic/SKILL.md +1 -1
  47. package/dist/agent-src/skills/ui-component-architect/SKILL.md +1 -1
  48. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +29 -2
  49. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +53 -8
  50. package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +3 -1
  51. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  52. package/dist/cli/commands/recordTriggerEval.js +2 -2
  53. package/dist/cli/main.js +1 -1
  54. package/dist/cli-delegate/{chunk-AKJGUQNO.js → chunk-GZTMRYCN.js} +405 -341
  55. package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-IDHUQIXP.js} +283 -132
  56. package/dist/cli-delegate/cmd_adr_effective.js +318 -81
  57. package/dist/cli-delegate/cmd_conformance.js +749 -669
  58. package/dist/cli-delegate/cmd_doctor.js +1 -1
  59. package/dist/cli-delegate/cmd_export.js +1 -1
  60. package/dist/cli-delegate/cmd_handoff.js +1 -1
  61. package/dist/cli-delegate/cmd_prune.js +1 -1
  62. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  63. package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
  64. package/dist/cli-delegate/cmd_sync.js +1 -1
  65. package/dist/cli-delegate/cmd_uninstall.js +1 -1
  66. package/dist/cli-delegate/cmd_validate.js +1 -1
  67. package/dist/discovery/deprecation-report.md +1 -1
  68. package/dist/discovery/discovery-manifest.json +39 -38
  69. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  70. package/dist/discovery/discovery-manifest.summary.md +1 -1
  71. package/dist/discovery/dormancy-report.md +1 -1
  72. package/dist/discovery/orphan-report.md +1 -1
  73. package/dist/discovery/packs.json +2 -2
  74. package/dist/discovery/trust-report.md +1 -1
  75. package/dist/discovery/workspaces.json +2 -2
  76. package/dist/hooks/dispatch.js +2764 -2439
  77. package/dist/install/install.mjs +588 -527
  78. package/dist/mcp/registry-manifest.json +2 -2
  79. package/dist/mcp/server.json +2 -2
  80. package/dist/mcp/server.mjs +1381 -1138
  81. package/dist/scripts/_lib/model_tier.js +1 -1
  82. package/dist/shared/settingsConsent.js +1 -1
  83. package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
  84. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
  85. package/docs/guidelines/design-antipatterns.md +87 -64
  86. package/package.json +1 -1
  87. package/src/agent-src/README.md +1 -1
  88. package/src/config/ci-local-parity.yml +6 -0
  89. package/src/config/conformance-claim-baseline.json +9 -0
  90. package/src/config/gate-coverage.yml +170 -0
  91. package/src/config/gate-violation-baselines.json +4 -4
  92. package/src/config/release-gate-locality.yml +141 -0
  93. package/src/scripts/_cli/cmd_doctor.ts +2 -2
  94. package/src/scripts/_cli/cmd_handoff.ts +1 -1
  95. package/src/scripts/_cli/cmd_update.ts +1 -1
  96. package/src/scripts/_cli/cmd_versions.ts +1 -1
  97. package/src/scripts/_lib/adr_frontmatter.ts +44 -0
  98. package/src/scripts/_lib/consequence_objects.ts +152 -0
  99. package/src/scripts/_lib/exec_evidence.ts +7 -0
  100. package/src/scripts/_lib/global_deploy_inventory.ts +2 -2
  101. package/src/scripts/_lib/host_permission_checks.ts +251 -0
  102. package/src/scripts/_lib/model_tier.ts +1 -1
  103. package/src/scripts/_lib/obligation_frequency.ts +2 -2
  104. package/src/scripts/_lib/release_highlights.ts +265 -1
  105. package/src/scripts/_lib/release_material.ts +129 -0
  106. package/src/scripts/_lib/repeated_run.ts +77 -0
  107. package/src/scripts/_lib/runtime_journal.ts +26 -3
  108. package/src/scripts/_lib/runtime_wiring_checks.ts +12 -1
  109. package/src/scripts/_lib/session_register.ts +11 -6
  110. package/src/scripts/_lib/sqlite_schema.ts +22 -0
  111. package/src/scripts/_lib/surface.ts +86 -0
  112. package/src/scripts/_lib/tamper_vocabulary.ts +242 -0
  113. package/src/scripts/_lib/ui_authority.ts +116 -0
  114. package/src/scripts/_lib/unified_diff.ts +121 -0
  115. package/src/scripts/adr/regenerate_index.ts +54 -1
  116. package/src/scripts/adr_cite_check.ts +71 -15
  117. package/src/scripts/ai_council/clients.ts +1 -1
  118. package/src/scripts/autonomy_friction_corpus.ts +229 -0
  119. package/src/scripts/build_proof.ts +15 -2
  120. package/src/scripts/check_beta_review_markers.ts +141 -17
  121. package/src/scripts/check_enforcement_coverage.ts +1 -1
  122. package/src/scripts/check_enforcement_denominator.ts +121 -11
  123. package/src/scripts/check_finding_dispositions.ts +239 -6
  124. package/src/scripts/check_gate_coverage.ts +96 -0
  125. package/src/scripts/check_installed_hooks_fresh.ts +381 -0
  126. package/src/scripts/check_release_highlights.ts +99 -14
  127. package/src/scripts/check_skill_activation_claim.ts +179 -0
  128. package/src/scripts/check_suppression_hygiene.ts +11 -0
  129. package/src/scripts/check_trigger_evals.ts +1 -1
  130. package/src/scripts/code_graph/build.ts +230 -33
  131. package/src/scripts/code_graph/extract.ts +168 -33
  132. package/src/scripts/code_graph/types.ts +37 -2
  133. package/src/scripts/code_graph/validate.ts +12 -1
  134. package/src/scripts/conformance_scan.ts +45 -4
  135. package/src/scripts/detect_silent_catch.ts +403 -0
  136. package/src/scripts/detect_verification_tampering.ts +538 -0
  137. package/src/scripts/explain_run.ts +1 -1
  138. package/src/scripts/generate_capability_matrix.ts +1 -1
  139. package/src/scripts/git_authorization_hook.ts +228 -18
  140. package/src/scripts/hook_manifest.json +1 -1
  141. package/src/scripts/hook_manifest.yaml +47 -30
  142. package/src/scripts/hooks/augment-dispatcher.sh +1 -1
  143. package/src/scripts/hooks/block_no_verify.ts +3 -3
  144. package/src/scripts/hooks/category_a.ts +291 -0
  145. package/src/scripts/hooks/cline-dispatcher.sh +1 -1
  146. package/src/scripts/hooks/concern_registry.ts +2 -2
  147. package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
  148. package/src/scripts/hooks/dispatch_hook.ts +17 -22
  149. package/src/scripts/hooks/evidence_independence.ts +21 -2
  150. package/src/scripts/hooks/{block_unauthorized_git.ts → git_command_classifier.ts} +175 -364
  151. package/src/scripts/hooks/host_lowering.ts +215 -0
  152. package/src/scripts/hooks/host_lowering.yaml +185 -0
  153. package/src/scripts/hooks/host_semantics.ts +105 -37
  154. package/src/scripts/hooks/journal_record_hook.ts +10 -0
  155. package/src/scripts/hooks/permission_gate.ts +117 -0
  156. package/src/scripts/hooks/push_settle_hook.ts +250 -0
  157. package/src/scripts/hooks/ship_diff_volume_hook.ts +1 -1
  158. package/src/scripts/hooks_status.ts +12 -10
  159. package/src/scripts/install-hooks.sh +134 -1
  160. package/src/scripts/install.ts +53 -82
  161. package/src/scripts/install_anthropic_key.sh +2 -2
  162. package/src/scripts/language_mirror_hook.ts +27 -5
  163. package/src/scripts/lint_design_antipattern_parity.ts +80 -0
  164. package/src/scripts/lint_evidence_artifacts.ts +45 -1
  165. package/src/scripts/lint_hook_manifest.ts +87 -3
  166. package/src/scripts/lint_motion_authority_drift.ts +731 -0
  167. package/src/scripts/lint_readme_size.ts +158 -12
  168. package/src/scripts/lint_reasoning_fixtures.ts +216 -0
  169. package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
  170. package/src/scripts/measure_sibling_naming.ts +150 -0
  171. package/src/scripts/redact_hook_capture.ts +1 -1
  172. package/src/scripts/release.ts +31 -143
  173. package/src/scripts/release_drill.ts +71 -1
  174. package/src/scripts/release_publication.ts +243 -3
  175. package/src/scripts/release_verify.ts +220 -0
  176. package/src/scripts/report_skill_activation.ts +319 -7
  177. package/src/scripts/schemas/review-findings.schema.json +6 -1
  178. package/src/scripts/schemas/skill.schema.json +12 -2
  179. package/src/scripts/self_review_gate.ts +10 -1
  180. package/src/scripts/session_canary_hook.ts +1 -1
  181. package/src/scripts/session_register_hook.ts +4 -3
  182. package/src/scripts/trigger_eval_grandfather.json +5 -1
  183. package/src/scripts/validate_agent_settings.ts +1 -1
  184. package/src/shared/settingsConsent.ts +1 -1
@@ -44,7 +44,19 @@
44
44
  * count descriptions against bare names. Until that runs, "most skills reach the
45
45
  * model without a description" is a suspicion.
46
46
  *
47
- * Exit: 0 always, except a usage/IO error (1). Deliberate.
47
+ * Exit: the REPORT is 0 always, except a usage/IO error (1). Deliberate — it
48
+ * gates on nothing and must not acquire a threshold.
49
+ *
50
+ * One later mode DOES carry a verdict, and it is not a threshold on the census:
51
+ * `--emit` exits 1 rather than writing a record from an empty store, because a
52
+ * zero-session reading is "no transcripts at this path" and publishing it as an
53
+ * activation figure is the defect this file exists to end.
54
+ *
55
+ * The assertion that the PUBLISHED figure still matches that record is a gate
56
+ * and lives in `check_skill_activation_claim.ts`. It is a separate script on
57
+ * purpose: `src/config/gate-coverage.yml` is a manifest of gates and
58
+ * `_lib/gate_population.ts` classifies one by prefix, so a `report_*` id
59
+ * registered there forks that population.
48
60
  */
49
61
 
50
62
  import * as fs from 'node:fs';
@@ -53,6 +65,7 @@ import * as path from 'node:path';
53
65
  import { fileURLToPath, pathToFileURL } from 'node:url';
54
66
 
55
67
  import { projectStoreSlug } from './_lib/cc_transcript.js';
68
+ import { reportScanned } from './_lib/scan_scope.js';
56
69
 
57
70
  const _HERE = fileURLToPath(import.meta.url);
58
71
  const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
@@ -73,11 +86,17 @@ export const DETERMINISTIC_RE = /^\s*(?:[-*]\s*)?(?:\*\*)?(MUST|NEVER|ALWAYS)\b/
73
86
  export interface SkillCensus {
74
87
  total: number;
75
88
  withTriggerKey: string[];
89
+ withTriggerCorpus: string[];
76
90
  withDeterministicObligation: string[];
77
91
  }
78
92
 
79
93
  export function censusSkills(root: string): SkillCensus {
80
- const out: SkillCensus = { total: 0, withTriggerKey: [], withDeterministicObligation: [] };
94
+ const out: SkillCensus = {
95
+ total: 0,
96
+ withTriggerKey: [],
97
+ withTriggerCorpus: [],
98
+ withDeterministicObligation: [],
99
+ };
81
100
  if (!fs.existsSync(root)) {
82
101
  return out;
83
102
  }
@@ -98,6 +117,12 @@ export function censusSkills(root: string): SkillCensus {
98
117
  if (TRIGGER_KEYS.some((k) => new RegExp(`^${k}`, 'm').test(fm))) {
99
118
  out.withTriggerKey.push(entry.name);
100
119
  }
120
+ // A test fixture, not a routing input: no host reads it at selection time.
121
+ // Counted because it is the second of the three populations the surface
122
+ // splits into, and it is the one most often mistaken for the first.
123
+ if (fs.existsSync(path.join(root, entry.name, 'evals', 'triggers.json'))) {
124
+ out.withTriggerCorpus.push(entry.name);
125
+ }
101
126
  const body = fmEnd === -1 ? text : text.slice(fmEnd + 4);
102
127
  if (DETERMINISTIC_RE.test(body)) {
103
128
  out.withDeterministicObligation.push(entry.name);
@@ -178,7 +203,185 @@ function _pct(n: number, d: number): string {
178
203
  return d === 0 ? 'n/a' : `${((100 * n) / d).toFixed(1)}%`;
179
204
  }
180
205
 
181
- export function render(census: SkillCensus, usage: UsageReport[]): string {
206
+ /** Repo-relative path of the record the published claim is derived from. */
207
+ export const RECORD_PATH = 'agents/evidence/metrics/skill-activation-census.json';
208
+ export const CLAIMS_PATH = 'docs/CLAIMS.md';
209
+ /** The one ledger entry this census backs. */
210
+ export const CLAIM_ID = 'skill-activation-census-zero';
211
+
212
+ /**
213
+ * The three populations the shipped surface splits into, plus their overlap.
214
+ *
215
+ * They are computed from one pass rather than three greps because the whole
216
+ * point of 2.1 is that they reconcile: `humanNamedOnly` is a remainder, so it
217
+ * cannot be stated independently and then found to disagree.
218
+ */
219
+ export interface Populations {
220
+ shipped: number;
221
+ triggerKey: number;
222
+ triggerCorpus: number;
223
+ bothKeyAndCorpus: number;
224
+ humanNamedOnly: number;
225
+ }
226
+
227
+ export function populations(census: SkillCensus): Populations {
228
+ const corpus = new Set(census.withTriggerCorpus);
229
+ const both = census.withTriggerKey.filter((s) => corpus.has(s));
230
+ const eitherOne = new Set([...census.withTriggerKey, ...census.withTriggerCorpus]);
231
+ return {
232
+ shipped: census.total,
233
+ triggerKey: census.withTriggerKey.length,
234
+ triggerCorpus: census.withTriggerCorpus.length,
235
+ bothKeyAndCorpus: both.length,
236
+ humanNamedOnly: census.total - eitherOne.size,
237
+ };
238
+ }
239
+
240
+ export interface CensusRecord {
241
+ schema_version: 1;
242
+ measured_at: string;
243
+ skills_shipped: number;
244
+ with_trigger_key: number;
245
+ with_trigger_corpus: number;
246
+ trigger_key_and_corpus: number;
247
+ human_named_only: number;
248
+ sessions: number;
249
+ assistant_turns: number;
250
+ invocations: number;
251
+ distinct_skills_invoked: number;
252
+ stores: string[];
253
+ }
254
+
255
+ export function buildRecord(census: SkillCensus, usage: UsageReport[], today: string): CensusRecord {
256
+ const pop = populations(census);
257
+ const distinct = new Set<string>();
258
+ for (const u of usage) {
259
+ for (const k of Object.keys(u.bySkill)) {
260
+ distinct.add(k);
261
+ }
262
+ }
263
+ return {
264
+ schema_version: 1,
265
+ measured_at: today,
266
+ skills_shipped: pop.shipped,
267
+ with_trigger_key: pop.triggerKey,
268
+ with_trigger_corpus: pop.triggerCorpus,
269
+ trigger_key_and_corpus: pop.bothKeyAndCorpus,
270
+ human_named_only: pop.humanNamedOnly,
271
+ sessions: usage.reduce((a, u) => a + u.sessions, 0),
272
+ assistant_turns: usage.reduce((a, u) => a + u.assistantTurns, 0),
273
+ invocations: usage.reduce((a, u) => a + u.invocations, 0),
274
+ distinct_skills_invoked: distinct.size,
275
+ stores: usage.map((u) => path.basename(u.store)),
276
+ };
277
+ }
278
+
279
+ export function readRecord(repoRoot: string): CensusRecord | null {
280
+ const file = path.join(repoRoot, RECORD_PATH);
281
+ if (!fs.existsSync(file)) {
282
+ return null;
283
+ }
284
+ return JSON.parse(fs.readFileSync(file, 'utf8')) as CensusRecord;
285
+ }
286
+
287
+ /**
288
+ * Numbers the published claim asserts, pulled back out of the claim text.
289
+ *
290
+ * Read from prose on purpose: the claim is the surface a reader believes, so
291
+ * comparing the record against a separate machine field would leave the
292
+ * sentence free to drift from both. `null` for any field the claim no longer
293
+ * states in the expected shape — which `checkClaim` reports as a defect rather
294
+ * than passing over, because a claim that stopped stating its figures is not a
295
+ * claim that agrees with the record.
296
+ */
297
+ export interface ClaimFigures {
298
+ sessions: number | null;
299
+ assistantTurns: number | null;
300
+ invocations: number | null;
301
+ distinct: number | null;
302
+ shipped: number | null;
303
+ triggerKey: number | null;
304
+ triggerCorpus: number | null;
305
+ bothKeyAndCorpus: number | null;
306
+ humanNamedOnly: number | null;
307
+ measuredAt: string | null;
308
+ lastVerified: string | null;
309
+ }
310
+
311
+ function _num(text: string, re: RegExp): number | null {
312
+ const m = re.exec(text);
313
+ return m?.[1] === undefined ? null : Number.parseInt(m[1].replace(/,/gu, ''), 10);
314
+ }
315
+
316
+ export function claimBlock(claimsText: string, id: string): string | null {
317
+ const start = claimsText.indexOf(`### claim: ${id}\n`);
318
+ if (start === -1) {
319
+ return null;
320
+ }
321
+ const rest = claimsText.slice(start + 1);
322
+ const end = rest.indexOf('\n### claim: ');
323
+ return end === -1 ? claimsText.slice(start) : claimsText.slice(start, start + 1 + end);
324
+ }
325
+
326
+ export function parseClaimFigures(block: string): ClaimFigures {
327
+ return {
328
+ sessions: _num(block, /over ([\d,]+) sessions and/u),
329
+ assistantTurns: _num(block, /sessions and ([\d,]+) assistant turns/u),
330
+ invocations: _num(block, /records ([\d,]+) Skill invocations/u),
331
+ distinct: _num(block, /Skill invocations and ([\d,]+) of [\d,]+ distinct skills/u),
332
+ shipped: _num(block, /Skill invocations and [\d,]+ of ([\d,]+) distinct skills/u),
333
+ triggerKey: _num(block, /([\d,]+) declare a machine-matchable trigger key/u),
334
+ triggerCorpus: _num(block, /([\d,]+) carry an `evals\/triggers\.json` corpus/u),
335
+ bothKeyAndCorpus: _num(block, /([\d,]+) do both/u),
336
+ humanNamedOnly: _num(block, /([\d,]+) are reachable only by a human naming them/u),
337
+ measuredAt: /measured (\d{4}-\d{2}-\d{2})/u.exec(block)?.[1] ?? null,
338
+ lastVerified: /^- last_verified: (\d{4}-\d{2}-\d{2})/mu.exec(block)?.[1] ?? null,
339
+ };
340
+ }
341
+
342
+ /**
343
+ * Disagreements between the committed record and the published claim.
344
+ *
345
+ * This is the whole of what 1.3 buys, so it is worth being exact about what it
346
+ * does NOT buy: it cannot tell whether the RECORD still describes the world,
347
+ * because the transcript store it was taken from is one machine's and is absent
348
+ * from CI. Re-taking the measurement is `--emit`; this only forbids the
349
+ * published sentence and the record from telling two different stories.
350
+ */
351
+ export function claimProblems(rec: CensusRecord, fig: ClaimFigures): string[] {
352
+ const out: string[] = [];
353
+ const pairs: Array<[string, number, number | null]> = [
354
+ ['sessions', rec.sessions, fig.sessions],
355
+ ['assistant turns', rec.assistant_turns, fig.assistantTurns],
356
+ ['Skill invocations', rec.invocations, fig.invocations],
357
+ ['distinct skills invoked', rec.distinct_skills_invoked, fig.distinct],
358
+ ['skills shipped', rec.skills_shipped, fig.shipped],
359
+ ['skills with a trigger key', rec.with_trigger_key, fig.triggerKey],
360
+ ['skills with a triggers.json corpus', rec.with_trigger_corpus, fig.triggerCorpus],
361
+ ['skills in both populations', rec.trigger_key_and_corpus, fig.bothKeyAndCorpus],
362
+ ['human-named-only skills', rec.human_named_only, fig.humanNamedOnly],
363
+ ];
364
+ for (const [label, recorded, claimed] of pairs) {
365
+ if (claimed === null) {
366
+ out.push(`${label}: the claim no longer states this figure; the record says ${String(recorded)}`);
367
+ } else if (claimed !== recorded) {
368
+ out.push(`${label}: claim says ${String(claimed)}, record says ${String(recorded)}`);
369
+ }
370
+ }
371
+ for (const [label, claimed] of [
372
+ ['measurement date', fig.measuredAt],
373
+ ['last_verified', fig.lastVerified],
374
+ ] as Array<[string, string | null]>) {
375
+ if (claimed === null) {
376
+ out.push(`${label}: the claim no longer states it; the record was measured ${rec.measured_at}`);
377
+ } else if (claimed !== rec.measured_at) {
378
+ out.push(`${label}: claim says ${claimed}, record was measured ${rec.measured_at}`);
379
+ }
380
+ }
381
+ return out;
382
+ }
383
+
384
+ export function render(census: SkillCensus, usage: UsageReport[], published: CensusRecord | null = null): string {
182
385
  const lines: string[] = [];
183
386
  lines.push('skill activation census — advisory, gates on nothing');
184
387
  lines.push('');
@@ -193,6 +396,16 @@ export function render(census: SkillCensus, usage: UsageReport[]): string {
193
396
  lines.push(` ${census.withDeterministicObligation.join(', ')}`);
194
397
  }
195
398
  lines.push('');
399
+ const pop = populations(census);
400
+ lines.push(' populations — the three ways a skill can be reached, reconciled');
401
+ lines.push(` machine-matchable trigger key ${pop.triggerKey}`);
402
+ lines.push(` evals/triggers.json corpus ${pop.triggerCorpus} (a test fixture; no host reads it at routing time)`);
403
+ lines.push(` both of the above ${pop.bothKeyAndCorpus}`);
404
+ lines.push(` human-named only (remainder) ${pop.humanNamedOnly}`);
405
+ lines.push(
406
+ ` reconciles ${pop.triggerKey} + ${pop.triggerCorpus} - ${pop.bothKeyAndCorpus} + ${pop.humanNamedOnly} = ${String(pop.triggerKey + pop.triggerCorpus - pop.bothKeyAndCorpus + pop.humanNamedOnly)} of ${pop.shipped}`,
407
+ );
408
+ lines.push('');
196
409
  let totalInv = 0;
197
410
  const distinct = new Set<string>();
198
411
  for (const u of usage) {
@@ -213,6 +426,65 @@ export function render(census: SkillCensus, usage: UsageReport[]): string {
213
426
  lines.push(` ${[...distinct].sort().join(', ')}`);
214
427
  }
215
428
  lines.push('');
429
+ const sessionsRead = usage.reduce((a, u) => a + u.sessions, 0);
430
+ if (sessionsRead === 0) {
431
+ // The store is keyed on the directory the process runs in, so a checkout
432
+ // that never hosted a session — a fresh worktree, a CI runner — yields a
433
+ // zero that means "no transcripts at this path". Publishing that as the
434
+ // activation reading would be the defect this census exists to end, so the
435
+ // two zeros are separated here rather than left to the reader.
436
+ lines.push(' EMPTY STORE: 0 sessions were read, so the zero above is "no transcripts at');
437
+ lines.push(' this path", NOT "no activations". It is not a measurement of the surface and');
438
+ lines.push(' must not be published as one. Run this where the store lives, or pass --store.');
439
+ lines.push('');
440
+ }
441
+ if (published !== null) {
442
+ // Staleness has to be readable from the run itself. Without this block the
443
+ // only way to tell a fresh published figure from a rotted one is to open
444
+ // the git log of `docs/CLAIMS.md`, which is exactly the thing 1.3 forbids.
445
+ const live = buildRecord(census, usage, published.measured_at);
446
+ // The two halves drift for different reasons and a single verdict over both
447
+ // would be worthless: the store is rolling, so its counts move between any
448
+ // two runs — including a run in the session that is adding to it. A reader
449
+ // who is told STALE every time stops reading the line. Only the repository
450
+ // half is a defect when it moves, because the published population split
451
+ // then no longer describes the tree it claims to count.
452
+ const REPO_FIELDS: Array<keyof CensusRecord> = [
453
+ 'skills_shipped',
454
+ 'with_trigger_key',
455
+ 'with_trigger_corpus',
456
+ 'trigger_key_and_corpus',
457
+ 'human_named_only',
458
+ ];
459
+ const STORE_FIELDS: Array<keyof CensusRecord> = [
460
+ 'sessions',
461
+ 'assistant_turns',
462
+ 'invocations',
463
+ 'distinct_skills_invoked',
464
+ ];
465
+ const repoDrift = REPO_FIELDS.filter((k) => live[k] !== published[k]);
466
+ const storeDrift = STORE_FIELDS.filter((k) => live[k] !== published[k]);
467
+ lines.push(` PUBLISHED RECORD (${RECORD_PATH}), measured ${published.measured_at}:`);
468
+ lines.push(
469
+ ` sessions=${String(published.sessions)} asst=${String(published.assistant_turns)} invocations=${String(published.invocations)} distinct=${String(published.distinct_skills_invoked)}`,
470
+ );
471
+ if (repoDrift.length > 0) {
472
+ lines.push(` STALE: the published population split no longer describes this tree —`);
473
+ lines.push(` ${repoDrift.join(', ')} moved. Re-take with --emit.`);
474
+ } else {
475
+ lines.push(' population split still describes this tree.');
476
+ }
477
+ if (sessionsRead === 0) {
478
+ lines.push(' Store reading: UNDECIDED — this run read no store, so it cannot say whether');
479
+ lines.push(' the published figures still hold. Re-run where the store lives.');
480
+ } else if (storeDrift.length === 0) {
481
+ lines.push(' Store reading: reproduces the published figures exactly.');
482
+ } else {
483
+ lines.push(` Store reading: moved since the record (${storeDrift.join(', ')}). Expected —`);
484
+ lines.push(' the store is rolling, not append-only, and a later reading can be lower.');
485
+ }
486
+ lines.push('');
487
+ }
216
488
  if (census.withTriggerKey.length === 0) {
217
489
  lines.push(' VERDICT: activation is not measurable as a rate. No skill declares a');
218
490
  lines.push(' machine-matchable trigger, so a missed-activation detector has nothing to');
@@ -227,19 +499,32 @@ export function render(census: SkillCensus, usage: UsageReport[]): string {
227
499
  return lines.join('\n');
228
500
  }
229
501
 
502
+ const USAGE =
503
+ 'usage: report_skill_activation [--root DIR] [--limit N] [--store PATH]... [--emit] [--allow-empty-store]\n';
504
+
230
505
  function main(argv: string[]): number {
231
506
  let limit = 30;
507
+ let emit = false;
508
+ let allowEmpty = false;
509
+ const rootIdx = argv.indexOf('--root');
510
+ const root = rootIdx === -1 ? REPO_ROOT : (argv[rootIdx + 1] ?? REPO_ROOT);
232
511
  const stores: string[] = [];
233
512
  for (let i = 0; i < argv.length; i += 1) {
234
513
  const a = argv[i];
235
- if (a === '--limit' && argv[i + 1] !== undefined) {
514
+ if (a === '--root' && argv[i + 1] !== undefined) {
515
+ i += 1;
516
+ } else if (a === '--limit' && argv[i + 1] !== undefined) {
236
517
  limit = Number.parseInt(argv[i + 1] as string, 10);
237
518
  i += 1;
238
519
  } else if (a === '--store' && argv[i + 1] !== undefined) {
239
520
  stores.push(argv[i + 1] as string);
240
521
  i += 1;
522
+ } else if (a === '--emit') {
523
+ emit = true;
524
+ } else if (a === '--allow-empty-store') {
525
+ allowEmpty = true;
241
526
  } else if (a === '--help' || a === '-h') {
242
- process.stdout.write('usage: report_skill_activation [--limit N] [--store PATH]...\n');
527
+ process.stdout.write(USAGE);
243
528
  return 0;
244
529
  }
245
530
  }
@@ -250,9 +535,36 @@ function main(argv: string[]): number {
250
535
  if (stores.length === 0) {
251
536
  stores.push(defaultStore(REPO_ROOT));
252
537
  }
253
- const census = censusSkills(path.join(REPO_ROOT, SKILLS_ROOT));
538
+ const census = censusSkills(path.join(root, SKILLS_ROOT));
254
539
  const usage = stores.map((s) => measureUsage(s, limit));
255
- process.stdout.write(`${render(census, usage)}\n`);
540
+ process.stdout.write(`${render(census, usage, readRecord(root))}\n`);
541
+ // The skills are the corpus this floor can hold: they are in the repository,
542
+ // so the count is the same everywhere. The transcript half is path-scoped and
543
+ // legitimately zero in CI, which is why it is reported above and not here — a
544
+ // floor over it would either red every CI run or be vacuous.
545
+ reportScanned({
546
+ gate: 'report_skill_activation',
547
+ scanned: census.total,
548
+ units: 'SKILL.md file(s)',
549
+ roots: [SKILLS_ROOT],
550
+ });
551
+ if (!emit) {
552
+ return 0;
553
+ }
554
+ const sessionsRead = usage.reduce((a, u) => a + u.sessions, 0);
555
+ if (sessionsRead === 0 && !allowEmpty) {
556
+ process.stderr.write(
557
+ 'report_skill_activation --emit: refusing to write a record from 0 sessions — that zero\n' +
558
+ 'is "no transcripts at this path", not an activation reading. Run where the store lives,\n' +
559
+ 'pass --store, or pass --allow-empty-store if an empty store is genuinely the finding.\n',
560
+ );
561
+ return 1;
562
+ }
563
+ const today = new Date().toISOString().slice(0, 10);
564
+ const file = path.join(root, RECORD_PATH);
565
+ fs.mkdirSync(path.dirname(file), { recursive: true });
566
+ fs.writeFileSync(file, `${JSON.stringify(buildRecord(census, usage, today), null, 2)}\n`);
567
+ process.stdout.write(`\nwrote ${RECORD_PATH} (measured ${today})\n`);
256
568
  return 0;
257
569
  }
258
570
 
@@ -51,6 +51,11 @@
51
51
  "type": "string"
52
52
  }
53
53
  },
54
+ "no_findings_reason": {
55
+ "type": "string",
56
+ "minLength": 1,
57
+ "description": "Why the finding set is empty. REQUIRED whenever a ledger file exists and `findings` is empty — enforced in check_finding_dispositions.empty_ledger_problem rather than here, because the requirement is conditional on the array length and this repo's Draft-07 subset validator resolves no `if`/`then`. The field exists because `findings: []` and a missing file made the same claim to every reader while collapsing two states that differ entirely: a review that ran and reported nothing, and a review that never ran. The reason must pick one and say how a reader can check it."
58
+ },
54
59
  "findings": {
55
60
  "type": "array",
56
61
  "description": "May be empty. An empty array is an honest null, not a missing field — the distinction the Markdown track spells with its own §2.3 line. The item shape is INLINED rather than `$ref`-ed for the same measured reason: the subset validator does not resolve `$ref`, so a referenced definition would have validated nothing.",
@@ -66,7 +71,7 @@
66
71
  "finding_id": {
67
72
  "type": "string",
68
73
  "pattern": "^[0-9a-f]{12}$",
69
- "description": "First 12 hex of sha256(`kind|title|file`), per findingId() in self_review_gate.ts. Stable across runs, which is what lets the disposition ledger key on it. NOT required: an in-flight finding may not have been keyed yet, and requiring it would make the schema reject the shape the model returns before the gate stamps it."
74
+ "description": "First 12 hex of sha256(`kind|title|file`), per findingId() in self_review_gate.ts. Stable across runs, which is what lets the disposition ledger key on it. NOT required: an in-flight finding may not have been keyed yet, and requiring it would make the schema reject the shape the model returns before the gate stamps it. It is a FINDING id, NOT a commit SHA — a 12-hex string looks exactly like a short SHA, and searching the commit log for it finds nothing by design; the disposition lives in agents/evidence/release-findings/<version>.json."
70
75
  },
71
76
  "severity": {
72
77
  "enum": [
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "$id": "https://github.com/event4u-app/agent-config/scripts/schemas/skill.schema.json",
4
- "x-schemaVersion": "1.0.0",
4
+ "x-schemaVersion": "1.1.1",
5
5
  "title": "Skill frontmatter",
6
6
  "$comment": "Source: agents/reference/docs/frontmatter-contract.md#skills. Keep in sync with inventory_frontmatter.py and the linter's lint_skill().",
7
7
  "type": "object",
@@ -276,7 +276,7 @@
276
276
  },
277
277
  "requires_confirmation": {
278
278
  "type": "boolean",
279
- "description": "Optional, additive: the skill's action STAGES rather than executes, so an irreversible step waits for an explicit human confirmation naming the exact object (non-destructive-by-default § Never act while asking). A declaration only — it names the action class, never the object, which exists at stage time and not in frontmatter. The mechanism is stageAction/confirmOnce in work_engine/hooks/builtin/staged_confirmation.ts; enumerate what is pending with `agent-config gates --pending`. Nothing BINDS on this flag yet: only claude both binds pre_tool_use and honours a deny (docs/contracts/hook-architecture-v1.md, section Which hosts carry pre_tool_use — 2 further hosts bind it and ignore the verdict, 3 alias it unbound, 2 have no pre-tool surface), so a guard would degrade to prose on 7 of 8 hosts while the flag reads like a control — that decision is deferred behind blocker confirmation-degraded-host-semantics."
279
+ "description": "Optional, additive: the skill's action STAGES rather than executes, so an irreversible step waits for an explicit human confirmation naming the exact object (non-destructive-by-default § Never act while asking). A declaration only — it names the action class, never the object, which exists at stage time and not in frontmatter. The mechanism is stageAction/confirmOnce in work_engine/hooks/builtin/staged_confirmation.ts; enumerate what is pending with `agent-config gates --pending`. Nothing BINDS on this flag yet: only claude both binds pre_tool_use and honours a deny (docs/contracts/hook-architecture-v1.md, section Which hosts carry pre_tool_use — 2 further hosts bind it and ignore the verdict, 3 alias it unbound, 2 carry no pre-tool alias row), so a guard would degrade to prose on 7 of 8 hosts while the flag reads like a control — that decision is deferred behind blocker confirmation-degraded-host-semantics."
280
280
  },
281
281
  "command": {
282
282
  "type": "array",
@@ -472,6 +472,16 @@
472
472
  ],
473
473
  "description": "Classifier signal for auto-dispatch. 'steps' = ordered multi-step plan → do-in-steps. 'files' = N independent files of the same shape → do-in-parallel. 'independent' = N independent same-shape targets (not files) → do-in-parallel. Omit for skills that are always single-pass. Read by auto-dispatch-classification.md classifier signal 1."
474
474
  },
475
+ "enforced_by": {
476
+ "type": "array",
477
+ "minItems": 1,
478
+ "uniqueItems": true,
479
+ "items": {
480
+ "type": "string",
481
+ "pattern": "^(hook:[a-z0-9_-]+|validator:[A-Za-z0-9_./-]+|test:[A-Za-z0-9_./-]+|observer:[a-z0-9 _-]+|instruction-only: *[^ ].*|none)$"
482
+ },
483
+ "description": "What resolves a CHECKABLE CLAIM this skill makes about itself \u2014 a standard-conformance claim of the form `<standard> <version> <level>` (WCAG 2.2 AA). Deliberately the SAME key and the same value vocabulary as rule.schema.json's `enforced_by`, per council 2026-09-04 (anthropic + openai, 2/2 for frontmatter over body markers; the residual split between reusing this key and minting a new `claim_resolvers:` was resolved on measurement \u2014 reuse costs ONE schema addition where a new key costs two, and adds no second vocabulary for one idea). `test:<path>` names the resolver; `instruction-only: <reason>` declares an honest gap, and the reason is MANDATORY because a bare marker is a claim with no triage record behind it. Resolved by tests/contracts/conformance_claim_declaration.test.ts. NOT read by check_enforcement_coverage.ts or lint_rule_enforcement_declaration.ts \u2014 both scan `src/rules/` only, so a skill carrying this key does not join the rule-enforcement ratchet."
484
+ },
475
485
  "gaps": {
476
486
  "type": "array",
477
487
  "description": "B6 — honest-null known limitations. Human-curated (NOT auto-discovered), not all skills. Each entry documents a real limitation and cites a witness test that reproduces it. Rendered on the proof page's 'Known Limits'; check_skill_gaps enforces every witness path exists. The offensive-transparency surface a headline-benchmark cannot ship.",
@@ -356,6 +356,15 @@ export function renderReview(findings: Finding[], enforce: boolean, escalation:
356
356
  ? `❌ ${blocking.length} merge-blocking finding(s) (security/claim × high+).`
357
357
  : `⚠️ ${blocking.length} finding(s) WOULD block merge under an enforced gate (advisory now).`
358
358
  : '✅ No merge-blocking findings (style/correctness advise only).';
359
+ // Three reviewers in the 2026-09 round searched the commit log for a finding
360
+ // id's first eight characters, found nothing, and reported "no fix found". A
361
+ // 12-hex id looks exactly like a short SHA, so the rendered comment now says
362
+ // what it is and where its disposition lives. This is the only place a reader
363
+ // of the comment can learn it without opening the source.
364
+ const idNote =
365
+ '\n\n`id` above is a FINDING id — the first 12 hex of sha256(kind|title|file), '
366
+ + '**not a commit SHA**. Searching the git log for it finds nothing, by design. '
367
+ + 'Its disposition is recorded in `agents/evidence/release-findings/<version>.json`.';
359
368
  // Machine-readable block (release-truth Phase 3): invisible in the rendered
360
369
  // comment; `check_finding_dispositions --pr` reads it as the TRIGGER that
361
370
  // demands committed dispositions. The comment is transport, never the
@@ -363,7 +372,7 @@ export function renderReview(findings: Finding[], enforce: boolean, escalation:
363
372
  const machine = `\n\n<!-- release-findings-json: ${JSON.stringify(
364
373
  findings.map((f) => ({ finding_id: findingId(f), severity: f.severity, kind: f.kind, title: f.title, file: f.file ?? null })),
365
374
  )} -->`;
366
- return `${banner}\n\n${verdictLine}\n\n| id | severity | kind | file | finding |\n|---|---|---|---|---|\n${rows}${esc}${machine}`;
375
+ return `${banner}\n\n${verdictLine}\n\n| id | severity | kind | file | finding |\n|---|---|---|---|---|\n${rows}${idNote}${esc}${machine}`;
367
376
  }
368
377
 
369
378
  function postReview(body: string): void {
@@ -16,7 +16,7 @@
16
16
  * rule alone is exactly the kind of surface that is dropped in fresh
17
17
  * conversations (found RED baseline on the reply-close PR-URL contract,
18
18
  * 2026-07-27); the session_start injection makes the contract present in every
19
- * new conversation on hook-capable hosts. Copilot (no hook surface) falls back
19
+ * new conversation on hook-capable hosts. Copilot (nothing bound here) falls back
20
20
  * to the rule alone.
21
21
  *
22
22
  * Gate + name resolution (first non-empty wins — the canary is a PERSONAL,
@@ -37,9 +37,10 @@
37
37
  *
38
38
  * ## Honest coverage
39
39
  *
40
- * The heartbeat is a per-turn obligation and does not reach every host. `copilot`
41
- * has no hook surface at all; `cursor`'s per-turn slots are IDE-only and do not
42
- * fire in its CLI; `cowork`'s lifecycle events are wired but inert. Those hosts
40
+ * The heartbeat is a per-turn obligation and does not reach every host. This
41
+ * package binds nothing on `copilot`; `cursor`'s per-turn reachability is not
42
+ * established here, so it is excluded rather than assumed; `cowork`'s lifecycle
43
+ * events are wired but inert. Those hosts
43
44
  * are named in `HEARTBEAT_REACHABLE_PLATFORMS` by their absence, and a session
44
45
  * there simply stops being visible after its TTL rather than being reported as
45
46
  * covered.
@@ -1,5 +1,9 @@
1
1
  {
2
- "_note": "Shrink-only grandfather allowlist for check_trigger_eval_presence (road-to-composition-ratchet). Skills listed here predate the trigger-eval presence gate and may lack evals/triggers.json. Entries may ONLY be removed (when the skill gains a triggers.json or is deleted) \u2014 never added. Frozen 2026-07-08 at 221 of 264 skills.",
2
+ "_note": "Shrink-only grandfather allowlist for check_trigger_eval_presence (road-to-composition-ratchet). Skills listed here predate the trigger-eval presence gate and may lack evals/triggers.json. Entries may ONLY be removed (when the skill gains a triggers.json or is deleted) — never added. Frozen 2026-07-08 at 221 of 264 skills.",
3
+ "_no_obligation_note": "Entries in `_no_obligation` are on the `skills` allowlist for a DIFFERENT reason from the rest. The list above records skills that predate the gate and owe a triggers.json; these owe none, because their description says explicit-request-only and they publish no auto-trigger keywords — there is no trigger to evaluate. Listing them without this distinction reads as a waived obligation when the obligation is absent. Read by humans only: check_trigger_eval_presence parses `skills` and ignores every other key, so this changes no verdict. Removing one from `skills` WOULD change a verdict — the gate would then demand a triggers.json for a skill that has nothing to trigger on.",
4
+ "_no_obligation": [
5
+ "sequential-thinking"
6
+ ],
3
7
  "skills": [
4
8
  "accessibility-auditor",
5
9
  "activation-design",
@@ -42,7 +42,7 @@ const SCHEMA_PATH = path.join(REPO_ROOT, 'src', 'scripts', 'schemas', 'agent-set
42
42
  const TEMPLATE_PATH = path.join(REPO_ROOT, 'src', 'config', 'agent-settings.template.yml');
43
43
  const LOCAL_PATHS = [project_settings_path(REPO_ROOT)];
44
44
 
45
- // Installer-default substitutions, mirroring scripts/install.py so the
45
+ // Installer-default substitutions, mirroring the installer so the
46
46
  // template validates as it would after a fresh `balanced` install.
47
47
  const PLACEHOLDERS: Record<string, string> = {
48
48
  __RULE_LOADING_TIER__: 'balanced',
@@ -15,7 +15,7 @@
15
15
  * would mean the agent could arrive at its own permission by observing the
16
16
  * world, which is the confused-deputy shape in miniature.
17
17
  *
18
- * Modelled on `src/scripts/hooks/block_unauthorized_git.ts`: answer from a
18
+ * Modelled on `src/scripts/hooks/git_command_classifier.ts`: answer from a
19
19
  * recorded fact rather than from the model's recollection, and treat a missing
20
20
  * record as NOT granted rather than as probably-fine.
21
21
  *