@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
@@ -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.",
@@ -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.1.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",
@@ -48,6 +48,7 @@ import {
48
48
  attachReleaseErrors,
49
49
  listRecords,
50
50
  markReleased,
51
+ writeTargetIndex,
51
52
  openRecords,
52
53
  readOverflow,
53
54
  readRecord,
@@ -503,6 +504,30 @@ function releaseCmd(root: string, argv: string[]): number {
503
504
  return EXIT_OK;
504
505
  }
505
506
 
507
+ /**
508
+ * Rebuild the per-target occurrence index and print it.
509
+ *
510
+ * A DERIVED read: the record files are the source, the index is regenerable
511
+ * and gitignored with the rest of `agents/runtime/`. It exists because
512
+ * `occurrences` counts a fingerprint (class + normalised evidence shape), so a
513
+ * rule's weakness showing up in three different shapes was three records with
514
+ * one occurrence each — and the third-recurrence escalation had nothing that
515
+ * could ever establish its own precondition.
516
+ */
517
+ function targetsCmd(root: string): number {
518
+ const counts = writeTargetIndex(root);
519
+ if (counts.length === 0) {
520
+ process.stdout.write('self-repair: no record carries a resolved target yet.\n');
521
+ return EXIT_OK;
522
+ }
523
+ for (const c of counts) {
524
+ process.stdout.write(
525
+ ` ${c.target} ${c.occurrences} occurrence(s) across ${c.records.length} record(s)\n`,
526
+ );
527
+ }
528
+ return EXIT_OK;
529
+ }
530
+
506
531
  export function main(argv: readonly string[]): number {
507
532
  const root = process.cwd();
508
533
  const sub = argv[0] ?? 'status';
@@ -512,7 +537,12 @@ export function main(argv: readonly string[]): number {
512
537
  if (sub === 'release') {
513
538
  return releaseCmd(root, [...argv.slice(1)]);
514
539
  }
515
- process.stderr.write(`self-repair: unknown subcommand '${sub}' (status | release).\n`);
540
+ if (sub === 'targets') {
541
+ return targetsCmd(root);
542
+ }
543
+ process.stderr.write(
544
+ `self-repair: unknown subcommand '${sub}' (status | release | targets).\n`,
545
+ );
516
546
  return EXIT_FAIL;
517
547
  }
518
548
 
@@ -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.
@@ -48,6 +48,12 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
48
48
 
49
49
  import { collect, _keyword_vector, _cosine } from './audit_skill_overlap.js';
50
50
  import { reportScanned, DeadScopeError } from './_lib/scan_scope.js';
51
+ import * as sl from './_lib/security_lint.js';
52
+ import { _scan as _scanHiddenUnicode } from './lint_hidden_unicode.js';
53
+ import { _scan as _scanConfusables } from './lint_confusables.js';
54
+ import { _scan as _scanInstructionSmuggling } from './lint_instruction_smuggling.js';
55
+ import { _scan as _scanMcpConfigSecurity } from './lint_mcp_config_security.js';
56
+ import { _scan as _scanFrontmatterSafety } from './lint_skill_frontmatter_safety.js';
51
57
 
52
58
  const _HERE = fileURLToPath(import.meta.url);
53
59
  const _DEFAULT_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
@@ -262,6 +268,60 @@ export function resolveCandidateDir(
262
268
  return { dir, refusal: null };
263
269
  }
264
270
 
271
+ /**
272
+ * The five content linters, by the check id each one reports under.
273
+ *
274
+ * The SAME functions the `lint_agent_security` umbrella spawns as processes.
275
+ * Called in-process here rather than shelled out: five `tsx` cold starts per
276
+ * candidate is a cost with no benefit, and the thing that must be identical is
277
+ * the DETECTION, which is these functions.
278
+ */
279
+ const CONTENT_LINTERS: ReadonlyArray<readonly [string, (sf: sl.ScannedFile) => sl.Finding[]]> = [
280
+ ['hidden-unicode', _scanHiddenUnicode],
281
+ ['mixed-script-confusable', _scanConfusables],
282
+ ['instruction-smuggling', _scanInstructionSmuggling],
283
+ ['mcp-config-security', _scanMcpConfigSecurity],
284
+ ['dangerous-frontmatter', _scanFrontmatterSafety],
285
+ ];
286
+
287
+ /**
288
+ * Read what a quarantined candidate SAYS, and refuse on a payload.
289
+ *
290
+ * `intake`'s other checks are properties of the file — symlink, extension, exec
291
+ * bit, size. Every one of them can be true of a file whose text tells an agent
292
+ * to hide its output from the user. "Inert" was a claim about the container that
293
+ * the gate read as a claim about the contents.
294
+ *
295
+ * TWO DELIBERATE DIFFERENCES FROM HOW THESE LINTERS RUN OVER THIS PACKAGE:
296
+ *
297
+ * 1. **Any HIGH refuses, whatever its weight.** The 0.25 confidence weighting is
298
+ * a false-positive containment device for THIS repository's own corpus, where
299
+ * a path containing `docs` or `examples` really is documentation somebody
300
+ * here wrote. A quarantined third-party candidate carries no such provenance,
301
+ * and a directory named `examples/` in it is an attacker's word for it. A
302
+ * refusal is also cheaper than a false pass in exactly this direction: the
303
+ * candidate is rejected and a human reads it.
304
+ * 2. **Every text extension is scanned, not just `.md`.** The candidate
305
+ * allow-list already bounds what may be present; a payload in the `.yml` a
306
+ * candidate ships is a payload.
307
+ *
308
+ * The refusal NAMES the linter, because "the content scan refused this" sends a
309
+ * reader to five detectors.
310
+ */
311
+ export function scanCandidateContent(candidateDir: string): string[] {
312
+ const out: string[] = [];
313
+ for (const sf of sl.iter_corpus(['.'], [...ALLOWED_EXT], candidateDir)) {
314
+ for (const [check, scan] of CONTENT_LINTERS) {
315
+ for (const finding of scan(sf)) {
316
+ if (finding.severity !== 'HIGH') continue;
317
+ const loc = finding.line ? `${sf.rel}:${String(finding.line)}` : sf.rel;
318
+ out.push(`${loc}: flagged by ${check} — ${finding.message}`);
319
+ }
320
+ }
321
+ }
322
+ return out;
323
+ }
324
+
265
325
  /**
266
326
  * Refuse a candidate that is not inert.
267
327
  *
@@ -296,6 +356,15 @@ export function intake(candidateDir: string): IntakeResult {
296
356
  if (files.length === 0) {
297
357
  refusals.push('candidate directory is empty');
298
358
  }
359
+ // Content, after the file properties. Ordering is not cosmetic: a candidate
360
+ // that fails an inertness check may not be safe to READ (a symlink out of
361
+ // quarantine is the case the read-time re-check exists for), so the bytes
362
+ // are only opened once the container has been judged.
363
+ if (refusals.length === 0) {
364
+ for (const flagged of scanCandidateContent(candidateDir)) {
365
+ refusals.push(flagged);
366
+ }
367
+ }
299
368
  return { accepted: refusals.length === 0, refusals, files_seen: files.length };
300
369
  }
301
370
 
@@ -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',