@event4u/agent-config 9.20.0 → 9.22.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 (239) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +86 -0
  3. package/dist/agent-src/commands/worktree/cleanup.md +17 -0
  4. package/dist/agent-src/commands/worktree/status.md +6 -0
  5. package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +22 -0
  6. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  7. package/dist/cli-delegate/{chunk-XGA2NTU4.js → chunk-3EWYC3YK.js} +40 -0
  8. package/dist/cli-delegate/cmd_handoff.js +1 -1
  9. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  10. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  11. package/dist/discovery/deprecation-report.md +1 -1
  12. package/dist/discovery/discovery-manifest.json +5 -5
  13. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  14. package/dist/discovery/discovery-manifest.summary.md +1 -1
  15. package/dist/discovery/orphan-report.md +1 -1
  16. package/dist/discovery/packs.json +2 -2
  17. package/dist/discovery/trust-report.md +1 -1
  18. package/dist/discovery/workspaces.json +2 -2
  19. package/dist/hooks/dispatch.js +58 -2
  20. package/dist/mcp/registry-manifest.json +2 -2
  21. package/dist/mcp/server.json +2 -2
  22. package/dist/mcp/server.mjs +47 -29
  23. package/package.json +1 -1
  24. package/src/config/gate-coverage.yml +22 -4
  25. package/src/config/gate-violation-baselines.json +3 -8
  26. package/src/scripts/_lib/bench_ab_activation.ts +368 -0
  27. package/src/scripts/_lib/gate_population.ts +70 -0
  28. package/src/scripts/_lib/roadmap_granularity.ts +157 -0
  29. package/src/scripts/_lib/scan_scope.ts +44 -0
  30. package/src/scripts/_lib/stdin.ts +82 -0
  31. package/src/scripts/audit_adr_coverage.ts +38 -0
  32. package/src/scripts/audit_auto_rules.ts +17 -3
  33. package/src/scripts/audit_command_surface.ts +51 -0
  34. package/src/scripts/audit_initial_context.ts +39 -3
  35. package/src/scripts/audit_mcp_tools.ts +25 -0
  36. package/src/scripts/audit_overlap.ts +24 -1
  37. package/src/scripts/audit_skill_descriptions.ts +19 -0
  38. package/src/scripts/{verify_before_complete_hook.ts → before_complete_hook.ts} +16 -3
  39. package/src/scripts/bench_ab_task_runner.ts +18 -0
  40. package/src/scripts/bench_ab_v2_run.ts +147 -6
  41. package/src/scripts/bench_ab_v2_stats.ts +74 -2
  42. package/src/scripts/check_adr_frontmatter.ts +23 -3
  43. package/src/scripts/check_always_budget.ts +24 -0
  44. package/src/scripts/check_artefact_checksums.ts +24 -1
  45. package/src/scripts/check_artefact_count_messaging.ts +20 -0
  46. package/src/scripts/check_augmentignore.ts +22 -15
  47. package/src/scripts/check_backstop_debt.ts +22 -0
  48. package/src/scripts/check_beta_review_markers.ts +22 -1
  49. package/src/scripts/check_bridge_derivation.ts +22 -1
  50. package/src/scripts/check_bundle_path_leakage.ts +26 -0
  51. package/src/scripts/check_ci_strict_superset.ts +21 -3
  52. package/src/scripts/check_claims.ts +22 -0
  53. package/src/scripts/check_cli_registry_budget_sync.ts +33 -4
  54. package/src/scripts/check_cluster_patterns.ts +26 -4
  55. package/src/scripts/check_command_count_messaging.ts +20 -4
  56. package/src/scripts/check_comparison.ts +18 -0
  57. package/src/scripts/check_condensed_paths.ts +23 -2
  58. package/src/scripts/check_consumer_scope_flip.ts +21 -2
  59. package/src/scripts/check_council_config_location.ts +20 -0
  60. package/src/scripts/check_council_layout.ts +22 -2
  61. package/src/scripts/check_council_references.ts +23 -1
  62. package/src/scripts/check_dependency_floors.ts +19 -3
  63. package/src/scripts/check_discovery_determinism.ts +26 -0
  64. package/src/scripts/check_enforcement_coverage.ts +20 -0
  65. package/src/scripts/check_evaluator_budgets.ts +23 -0
  66. package/src/scripts/check_finding_dispositions.ts +29 -0
  67. package/src/scripts/check_gate_coverage.ts +63 -22
  68. package/src/scripts/check_gate_paths.ts +30 -5
  69. package/src/scripts/check_generated_artefact_headers.ts +21 -0
  70. package/src/scripts/check_generator_output_coverage.ts +23 -0
  71. package/src/scripts/check_gitignore_freshness.ts +23 -0
  72. package/src/scripts/check_host_loadability.ts +34 -8
  73. package/src/scripts/check_installer_import_purity.ts +22 -8
  74. package/src/scripts/check_kernel_prefix_stability.ts +19 -1
  75. package/src/scripts/check_kernel_rule_bundle.ts +23 -0
  76. package/src/scripts/check_knowledge_cards.ts +13 -0
  77. package/src/scripts/check_knowledge_pages.ts +23 -0
  78. package/src/scripts/check_knowledge_sharing.ts +27 -0
  79. package/src/scripts/check_md_language.ts +24 -0
  80. package/src/scripts/check_media_deps.ts +23 -0
  81. package/src/scripts/check_memory.ts +26 -1
  82. package/src/scripts/check_memory_contradiction.ts +16 -1
  83. package/src/scripts/check_memory_proposal.ts +15 -0
  84. package/src/scripts/check_memory_similarity.ts +16 -0
  85. package/src/scripts/check_module_management_neutral.ts +19 -11
  86. package/src/scripts/check_no_conflict_markers.ts +22 -1
  87. package/src/scripts/check_no_external_sources.ts +26 -1
  88. package/src/scripts/check_no_local_settings_committed.ts +37 -10
  89. package/src/scripts/check_no_new_legacy_path.ts +30 -1
  90. package/src/scripts/check_one_off_location.ts +43 -0
  91. package/src/scripts/check_originality_freshness.ts +25 -1
  92. package/src/scripts/check_overlay_cascade_subdirs.ts +21 -0
  93. package/src/scripts/check_pack_size.ts +22 -0
  94. package/src/scripts/check_preamble_payload_budget.ts +35 -5
  95. package/src/scripts/check_proposal.ts +24 -1
  96. package/src/scripts/check_public_catalog_links.ts +27 -0
  97. package/src/scripts/check_public_links.ts +19 -0
  98. package/src/scripts/check_quality_regression.ts +19 -0
  99. package/src/scripts/check_reach_channels.ts +44 -0
  100. package/src/scripts/check_reach_staleness.ts +27 -0
  101. package/src/scripts/check_release_adjacent_health.ts +28 -0
  102. package/src/scripts/check_release_highlights.ts +34 -0
  103. package/src/scripts/check_release_includes_discovery.ts +25 -6
  104. package/src/scripts/check_release_pr_shape.ts +18 -1
  105. package/src/scripts/check_release_published.ts +21 -0
  106. package/src/scripts/check_release_surface_equality.ts +22 -0
  107. package/src/scripts/check_release_trunk_sync.ts +19 -0
  108. package/src/scripts/check_reply_consistency.ts +22 -0
  109. package/src/scripts/check_roadmap_trackable.ts +20 -0
  110. package/src/scripts/check_role_doc_links.ts +23 -0
  111. package/src/scripts/check_rule_invariants.ts +14 -0
  112. package/src/scripts/check_secret_leak.ts +47 -0
  113. package/src/scripts/check_skill_gaps.ts +32 -0
  114. package/src/scripts/check_skill_requires.ts +19 -0
  115. package/src/scripts/check_source_pointer_freshness.ts +20 -0
  116. package/src/scripts/check_structural_breaking.ts +29 -2
  117. package/src/scripts/check_surface_tiers.ts +34 -0
  118. package/src/scripts/check_template_pin_drift.ts +23 -1
  119. package/src/scripts/check_test_coverage_diff.ts +17 -1
  120. package/src/scripts/check_token_optimizer_freshness.ts +18 -3
  121. package/src/scripts/check_token_quality_golden.ts +21 -0
  122. package/src/scripts/check_token_regression.ts +25 -0
  123. package/src/scripts/check_tracked_but_ignored.ts +26 -0
  124. package/src/scripts/check_trigger_eval_presence.ts +23 -1
  125. package/src/scripts/check_trigger_evals.ts +36 -0
  126. package/src/scripts/check_trunk_drift.ts +23 -0
  127. package/src/scripts/check_update_banner.ts +24 -0
  128. package/src/scripts/gate_scope_classify.ts +513 -0
  129. package/src/scripts/hook_manifest.yaml +1 -1
  130. package/src/scripts/hooks/block_kernel_rule_writes.ts +1 -1
  131. package/src/scripts/hooks/concern_registry.ts +2 -2
  132. package/src/scripts/lint_agent_security.ts +19 -0
  133. package/src/scripts/lint_agent_skill_names.ts +30 -0
  134. package/src/scripts/lint_agents_layout.ts +20 -0
  135. package/src/scripts/lint_agents_md.ts +20 -0
  136. package/src/scripts/lint_archived_skills.ts +24 -3
  137. package/src/scripts/lint_behavioural_eval_freshness.ts +29 -3
  138. package/src/scripts/lint_bench_ab.ts +25 -0
  139. package/src/scripts/lint_bench_corpus.ts +20 -4
  140. package/src/scripts/lint_breaking_changes_index.ts +24 -0
  141. package/src/scripts/lint_budget_ownership.ts +20 -3
  142. package/src/scripts/lint_changelog_rollback.ts +24 -0
  143. package/src/scripts/lint_command_flow_coverage.ts +22 -0
  144. package/src/scripts/lint_command_tiers.ts +29 -5
  145. package/src/scripts/lint_commit_subjects.ts +15 -0
  146. package/src/scripts/lint_confusables.ts +24 -0
  147. package/src/scripts/lint_consumer_internal_refs.ts +24 -0
  148. package/src/scripts/lint_design_quality.ts +22 -0
  149. package/src/scripts/lint_design_slop.ts +23 -0
  150. package/src/scripts/lint_discovery_manifest.ts +23 -0
  151. package/src/scripts/lint_discovery_vocabulary.ts +19 -0
  152. package/src/scripts/lint_documented_commands.ts +23 -0
  153. package/src/scripts/lint_empty_roadmaps.ts +23 -0
  154. package/src/scripts/lint_eval_fixture_citations.ts +19 -0
  155. package/src/scripts/lint_eval_freshness.ts +35 -0
  156. package/src/scripts/lint_examples.ts +18 -3
  157. package/src/scripts/lint_explain_trace.ts +23 -0
  158. package/src/scripts/lint_featured_skills.ts +26 -1
  159. package/src/scripts/lint_flows.ts +19 -0
  160. package/src/scripts/lint_frontmatter_boilerplate.ts +20 -0
  161. package/src/scripts/lint_ghostwriter_source.ts +26 -6
  162. package/src/scripts/lint_glama_drift.ts +24 -0
  163. package/src/scripts/lint_global_paths.ts +24 -0
  164. package/src/scripts/lint_governed_writes.ts +20 -0
  165. package/src/scripts/lint_hidden_unicode.ts +25 -0
  166. package/src/scripts/lint_hook_concern_budget.ts +39 -6
  167. package/src/scripts/lint_hook_manifest.ts +22 -0
  168. package/src/scripts/lint_instruction_smuggling.ts +23 -0
  169. package/src/scripts/lint_knowledge_scale.ts +32 -2
  170. package/src/scripts/lint_legal_pack.ts +31 -0
  171. package/src/scripts/lint_marketplace.ts +26 -0
  172. package/src/scripts/lint_marketplace_install_completeness.ts +23 -0
  173. package/src/scripts/lint_mcp_config_security.ts +24 -0
  174. package/src/scripts/lint_mcp_registry_manifest.ts +22 -1
  175. package/src/scripts/lint_memory_tripwire.ts +35 -0
  176. package/src/scripts/lint_missions.ts +23 -7
  177. package/src/scripts/lint_model_tier_coverage.ts +18 -0
  178. package/src/scripts/lint_namespace_collisions.ts +19 -0
  179. package/src/scripts/lint_no_new_atomic_commands.ts +33 -0
  180. package/src/scripts/lint_no_python_twin_rationale.ts +18 -0
  181. package/src/scripts/lint_one_off_age.ts +38 -2
  182. package/src/scripts/lint_orchestration_dsl.ts +23 -3
  183. package/src/scripts/lint_orchestrator_auto_detect.ts +19 -12
  184. package/src/scripts/lint_originality.ts +22 -4
  185. package/src/scripts/lint_originality_shingles.ts +28 -3
  186. package/src/scripts/lint_output_slop.ts +19 -0
  187. package/src/scripts/lint_override_kernel_guard.ts +30 -0
  188. package/src/scripts/lint_pack_first_win.ts +23 -0
  189. package/src/scripts/lint_pack_risk_class.ts +19 -3
  190. package/src/scripts/lint_persistence.ts +19 -0
  191. package/src/scripts/lint_persona_governance.ts +18 -3
  192. package/src/scripts/lint_positioning.ts +23 -0
  193. package/src/scripts/lint_pre_migration_refs.ts +24 -0
  194. package/src/scripts/lint_profile_overlay_set_only.ts +24 -0
  195. package/src/scripts/lint_provenance.ts +21 -0
  196. package/src/scripts/lint_provenance_vocabulary.ts +24 -1
  197. package/src/scripts/lint_readme_jargon.ts +21 -3
  198. package/src/scripts/lint_readme_serial_comma.ts +21 -3
  199. package/src/scripts/lint_readme_size.ts +21 -3
  200. package/src/scripts/lint_regression.ts +28 -0
  201. package/src/scripts/lint_roadmap_blockers.ts +43 -0
  202. package/src/scripts/lint_roadmap_ci_steps.ts +45 -0
  203. package/src/scripts/lint_roadmap_complexity.ts +47 -0
  204. package/src/scripts/lint_roadmap_later_disposition.ts +23 -0
  205. package/src/scripts/lint_rule_interactions.ts +16 -2
  206. package/src/scripts/lint_rule_tiers.ts +19 -6
  207. package/src/scripts/lint_showcase_sessions.ts +19 -3
  208. package/src/scripts/lint_skill_descriptions.ts +19 -0
  209. package/src/scripts/lint_skill_frontmatter_safety.ts +24 -0
  210. package/src/scripts/lint_skill_originality.ts +18 -3
  211. package/src/scripts/lint_skill_scripts_readonly.ts +20 -0
  212. package/src/scripts/lint_skill_tools.ts +84 -145
  213. package/src/scripts/lint_spawn_payload.ts +27 -1
  214. package/src/scripts/lint_store_boundary.ts +19 -0
  215. package/src/scripts/lint_subagent_determinism.ts +23 -1
  216. package/src/scripts/lint_supported_tools_matrix.ts +18 -0
  217. package/src/scripts/lint_surface_matrix.ts +19 -3
  218. package/src/scripts/lint_ticket_buildable.ts +23 -0
  219. package/src/scripts/lint_topics_yaml.ts +19 -12
  220. package/src/scripts/lint_trigger_precision.ts +27 -0
  221. package/src/scripts/lint_trust_coherence.ts +21 -0
  222. package/src/scripts/lint_ui_stack_bundles.ts +17 -3
  223. package/src/scripts/lint_value_dashboard.ts +19 -3
  224. package/src/scripts/lint_versioned_cache.ts +19 -1
  225. package/src/scripts/lint_workflow_security.ts +22 -14
  226. package/src/scripts/pr_url_reminder_hook.ts +1 -1
  227. package/src/scripts/skill_collision_clusters.ts +37 -3
  228. package/src/scripts/skill_discovery.ts +20 -0
  229. package/src/scripts/skill_eval_coverage.ts +22 -0
  230. package/src/scripts/skill_overlap.ts +32 -6
  231. package/src/scripts/skill_preview.ts +31 -0
  232. package/src/scripts/skill_trigger_eval.ts +18 -0
  233. package/src/scripts/skill_usage_collect.ts +21 -0
  234. package/src/scripts/skill_usage_report.ts +21 -0
  235. package/src/scripts/sweep_dead_scan_roots.ts +11 -3
  236. package/src/scripts/verify_physical_move.ts +24 -0
  237. package/src/scripts/worktree_cleanup_check.ts +319 -4
  238. package/src/scripts/check_bite_sized_granularity.ts +0 -116
  239. package/src/scripts/lint_workspace_boundary.ts +0 -267
@@ -25,6 +25,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
25
25
  import { parse as parseYaml } from 'yaml';
26
26
 
27
27
  import { project_settings_path } from './_lib/agent_settings.js';
28
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
28
29
 
29
30
  const _HERE = fileURLToPath(import.meta.url);
30
31
  const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
@@ -148,14 +149,16 @@ function _isPlainDict(v: unknown): v is Record<string, unknown> {
148
149
  return typeof v === 'object' && v !== null && !Array.isArray(v);
149
150
  }
150
151
 
152
+ /** @returns the number of platform/event concern lists inspected. */
151
153
  function _check_concern_counts(
152
154
  manifest: Manifest,
153
155
  max_per_event: number,
154
156
  warnings: string[],
155
- ): void {
157
+ ): number {
158
+ let inspected = 0;
156
159
  const platforms = manifest['platforms'] ?? {};
157
160
  if (!_isPlainDict(platforms)) {
158
- return;
161
+ return inspected;
159
162
  }
160
163
  for (const [plat, block] of Object.entries(platforms)) {
161
164
  if (!_isPlainDict(block) || block['fallback_only']) {
@@ -165,6 +168,7 @@ function _check_concern_counts(
165
168
  if (!Array.isArray(names)) {
166
169
  continue;
167
170
  }
171
+ inspected += 1;
168
172
  const count = names.length;
169
173
  if (count > max_per_event) {
170
174
  warnings.push(
@@ -175,18 +179,22 @@ function _check_concern_counts(
175
179
  }
176
180
  }
177
181
  }
182
+ return inspected;
178
183
  }
179
184
 
180
- function _check_fail_closed_tier(manifest: Manifest, tier1: string[], errors: string[]): void {
185
+ /** @returns the number of concern declarations inspected. */
186
+ function _check_fail_closed_tier(manifest: Manifest, tier1: string[], errors: string[]): number {
187
+ let inspected = 0;
181
188
  const concerns = manifest['concerns'] ?? {};
182
189
  if (!_isPlainDict(concerns)) {
183
- return;
190
+ return inspected;
184
191
  }
185
192
  const allowed = new Set(tier1);
186
193
  for (const [name, spec] of Object.entries(concerns)) {
187
194
  if (!_isPlainDict(spec)) {
188
195
  continue;
189
196
  }
197
+ inspected += 1;
190
198
  if (spec['fail_closed'] === true && !allowed.has(name)) {
191
199
  errors.push(
192
200
  `concerns.${name}: fail_closed=true but not declared in ` +
@@ -195,6 +203,7 @@ function _check_fail_closed_tier(manifest: Manifest, tier1: string[], errors: st
195
203
  );
196
204
  }
197
205
  }
206
+ return inspected;
198
207
  }
199
208
 
200
209
  export function lint(
@@ -227,8 +236,32 @@ export function lint(
227
236
 
228
237
  const warnings: string[] = [];
229
238
  const errors: string[] = [];
230
- _check_concern_counts(manifest, max_per_event, warnings);
231
- _check_fail_closed_tier(manifest, tier1, errors);
239
+ const inspected =
240
+ _check_concern_counts(manifest, max_per_event, warnings) +
241
+ _check_fail_closed_tier(manifest, tier1, errors);
242
+
243
+ // The manifest loads and parses, then every walk below silently tolerates a
244
+ // shape it does not recognise — no `platforms:`, no `concerns:`, a renamed
245
+ // top-level key — and the gate exits 0 having budgeted nothing. Counts units
246
+ // INSPECTED (platform/event concern lists + concern declarations), not
247
+ // warnings or errors. Exit 1 is the load-failure code and is the right one
248
+ // here: the manifest was readable but carried nothing this gate governs; 2
249
+ // is reserved for a hard-fail WITH violations.
250
+ try {
251
+ assertScanned({
252
+ gate: 'lint_hook_concern_budget',
253
+ scanned: inspected,
254
+ units: 'manifest concern unit(s)',
255
+ roots: [manifest_path],
256
+ });
257
+ } catch (exc) {
258
+ if (exc instanceof DeadScopeError) {
259
+ // The message already carries the gate name as its prefix.
260
+ process.stderr.write(`${exc.message}\n`);
261
+ return 1;
262
+ }
263
+ throw exc;
264
+ }
232
265
 
233
266
  for (const w of warnings) {
234
267
  process.stderr.write(`warn: ${w}\n`);
@@ -28,6 +28,8 @@ import path from "node:path";
28
28
  import { fileURLToPath, pathToFileURL } from "node:url";
29
29
  import { parse as parseYaml } from "yaml";
30
30
 
31
+ import { assertScanned, DeadScopeError } from "./_lib/scan_scope.js";
32
+
31
33
  // src/scripts/lint_hook_manifest.ts → two levels up is the repo root.
32
34
  const REPO_ROOT = path.resolve(
33
35
  path.dirname(fileURLToPath(import.meta.url)),
@@ -322,6 +324,26 @@ export function lint(manifestPath: string, strict: boolean): number {
322
324
  process.stderr.write(`error: ${e}\n`);
323
325
  }
324
326
 
327
+ // Concerns are the units read — each one resolves a script path on disk.
328
+ // Runs after the findings are printed so the existing `'concerns' must be a
329
+ // non-empty mapping` line still reaches the caller. Exit 1, not 2: the
330
+ // manifest loaded fine (2 is reserved for a file that could not be read or
331
+ // parsed); what is empty is its declared scope.
332
+ try {
333
+ assertScanned({
334
+ gate: "lint_hook_manifest",
335
+ scanned: concernNames.size,
336
+ units: "concern(s)",
337
+ roots: [manifestPath],
338
+ });
339
+ } catch (exc) {
340
+ if (exc instanceof DeadScopeError) {
341
+ process.stderr.write(`error: ${exc.message}\n`);
342
+ return 1;
343
+ }
344
+ throw exc;
345
+ }
346
+
325
347
  if (errors.length > 0) {
326
348
  return 1;
327
349
  }
@@ -28,6 +28,7 @@ import * as fs from 'node:fs';
28
28
  import { fileURLToPath, pathToFileURL } from 'node:url';
29
29
 
30
30
  import * as sl from './_lib/security_lint.js';
31
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
31
32
 
32
33
  export const CHECK = 'instruction-smuggling';
33
34
 
@@ -152,12 +153,34 @@ export function main(argv: readonly string[] | null = null): number {
152
153
  const args = parse_args(argv ?? process.argv.slice(2));
153
154
 
154
155
  const findings: sl.Finding[] = [];
156
+ let corpusFiles = 0;
155
157
  for (const sf of sl.iter_corpus()) {
158
+ corpusFiles += 1;
156
159
  for (const h of _scan(sf)) {
157
160
  findings.push(h);
158
161
  }
159
162
  }
160
163
 
164
+ // `iter_corpus` skips a root that does not exist, so a moved corpus yields
165
+ // zero files and this security lint reports "clean" — and the umbrella
166
+ // aggregate reads that as 0 blocking findings. Count files READ, not
167
+ // findings. Exit 1 is this CLI's only failure code (2 is reserved for the
168
+ // pinned argparse usage error), so it carries "could not run" here.
169
+ try {
170
+ assertScanned({
171
+ gate: 'lint_instruction_smuggling',
172
+ scanned: corpusFiles,
173
+ units: 'corpus file(s)',
174
+ roots: sl.DEFAULT_SCAN_ROOTS,
175
+ });
176
+ } catch (exc) {
177
+ if (exc instanceof DeadScopeError) {
178
+ process.stderr.write(`❌ ${exc.message}\n`);
179
+ return 1;
180
+ }
181
+ throw exc;
182
+ }
183
+
161
184
  if (args.json) {
162
185
  const payload = sl.py_json_dumps_indent2(findings.map((f) => f.toDict()));
163
186
  process.stdout.write(payload + '\n');
@@ -31,6 +31,8 @@ import * as path from 'node:path';
31
31
  import process from 'node:process';
32
32
  import { fileURLToPath } from 'node:url';
33
33
 
34
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
35
+
34
36
  const PROG = 'lint_knowledge_scale.ts';
35
37
 
36
38
  export const INTAKE_EVENTS_MAX = 2000;
@@ -109,7 +111,12 @@ export function collectTypeCounts(root: string): Map<string, number> {
109
111
  return counts;
110
112
  }
111
113
 
112
- export function runChecks(root: string): Warning[] {
114
+ /** Out-param so the caller can assert the scope it just walked. */
115
+ export interface ScanStats {
116
+ corpusFiles: number;
117
+ }
118
+
119
+ export function runChecks(root: string, stats?: ScanStats): Warning[] {
113
120
  const warnings: Warning[] = [];
114
121
 
115
122
  // 1. Intake scale.
@@ -156,6 +163,9 @@ export function runChecks(root: string): Warning[] {
156
163
  });
157
164
  }
158
165
  }
166
+ if (stats) {
167
+ stats.corpusFiles = total;
168
+ }
159
169
  if (total > CORPUS_FILES_MAX) {
160
170
  warnings.push({
161
171
  rule: 'corpus-scale',
@@ -252,12 +262,32 @@ function main(argv: string[]): number {
252
262
  }
253
263
 
254
264
  let warnings: Warning[];
265
+ const stats: ScanStats = { corpusFiles: 0 };
255
266
  try {
256
- warnings = runChecks(path.resolve(dir));
267
+ warnings = runChecks(path.resolve(dir), stats);
257
268
  } catch (exc) {
258
269
  process.stderr.write(`${PROG}: internal error: ${String(exc)}\n`);
259
270
  return 3;
260
271
  }
272
+ try {
273
+ assertScanned({
274
+ gate: 'lint_knowledge_scale',
275
+ scanned: stats.corpusFiles,
276
+ units: 'knowledge/memory file(s)',
277
+ roots: ['agents/knowledge', 'agents/memory'],
278
+ allowEmpty:
279
+ 'OPTIONAL_INPUT: the memory/knowledge substrate accumulates in the consumer ' +
280
+ 'tree over time — a project that has recorded nothing yet has no corpus, and ' +
281
+ 'the tripwires it would trip are all upper bounds. Delete both roots and zero ' +
282
+ 'still means "nothing recorded", never "the scale checks stopped looking".',
283
+ });
284
+ } catch (exc) {
285
+ if (exc instanceof DeadScopeError) {
286
+ process.stderr.write(`${PROG}: ${exc.message}\n`);
287
+ return 3;
288
+ }
289
+ throw exc;
290
+ }
261
291
 
262
292
  if (format === 'json') {
263
293
  process.stdout.write(JSON.stringify({ warnings }, null, 2) + '\n');
@@ -24,6 +24,8 @@ import * as fs from 'node:fs';
24
24
  import * as path from 'node:path';
25
25
  import { fileURLToPath, pathToFileURL } from 'node:url';
26
26
 
27
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
28
+
27
29
  const QUIET = process.argv.includes('--quiet');
28
30
 
29
31
  const _HERE = fileURLToPath(import.meta.url);
@@ -246,7 +248,36 @@ export function lintLegalPack(skillsDir: string = SKILLS_DIR): Violation[] {
246
248
  return violations;
247
249
  }
248
250
 
251
+ /** SKILL.md files under `skillsDir` — the corpus `lintLegalPack` walks. */
252
+ function countSkillFiles(skillsDir: string = SKILLS_DIR): number {
253
+ try {
254
+ return fs
255
+ .readdirSync(skillsDir)
256
+ .filter((d) => fs.existsSync(path.join(skillsDir, d, 'SKILL.md'))).length;
257
+ } catch {
258
+ return 0;
259
+ }
260
+ }
261
+
249
262
  export function main(): number {
263
+ // The unit is every SKILL.md, not the `packs: [legal-review-prep]` subset:
264
+ // the pack is opt-in, so a zero-legal-pack-skills count is a normal state
265
+ // and asserting on it would prove nothing. Over a vanished src/skills the
266
+ // subset is zero too — indistinguishable from "the pack is not installed".
267
+ try {
268
+ assertScanned({
269
+ gate: 'lint_legal_pack',
270
+ scanned: countSkillFiles(),
271
+ units: 'SKILL.md file(s)',
272
+ roots: ['src/skills'],
273
+ });
274
+ } catch (exc) {
275
+ if (exc instanceof DeadScopeError) {
276
+ process.stderr.write(`❌ ${exc.message}\n`);
277
+ return 1;
278
+ }
279
+ throw exc;
280
+ }
250
281
  const violations = lintLegalPack();
251
282
  if (violations.length === 0) {
252
283
  _print('✅ legal pack — all legal-pack skills carry the attorney-review line + Jurisdiction tag');
@@ -27,6 +27,8 @@ import fs from "node:fs";
27
27
  import path from "node:path";
28
28
  import { fileURLToPath, pathToFileURL } from "node:url";
29
29
 
30
+ import { assertWatchlistResolves, DeadScopeError } from "./_lib/scan_scope.js";
31
+
30
32
  const ROOT = ".";
31
33
  const MARKETPLACE = path.join(ROOT, ".claude-plugin", "marketplace.json");
32
34
  const PACKAGE_JSON = path.join(ROOT, "package.json");
@@ -87,6 +89,30 @@ function require_key(
87
89
  }
88
90
 
89
91
  export function main(): number {
92
+ // No corpus to count — the shim is exactly one manifest plus one pointer
93
+ // skill, and every count the shape offers (plugins, skill entries, dirs under
94
+ // .claude-plugin/skills/) is a value an existing negative case drives to zero
95
+ // on purpose. What CAN vanish is the fixed set of paths this resolves against
96
+ // cwd, so the watch list is the honest guard: run from the wrong directory
97
+ // and the gate now says so instead of reporting a missing manifest.
98
+ try {
99
+ assertWatchlistResolves({
100
+ gate: "lint_marketplace",
101
+ candidates: [
102
+ ".claude-plugin/marketplace.json",
103
+ "package.json",
104
+ `${POINTER_SKILL_ENTRY.replace(/^\.\//, "")}/SKILL.md`,
105
+ ],
106
+ repoRoot: path.resolve(ROOT),
107
+ });
108
+ } catch (exc) {
109
+ if (exc instanceof DeadScopeError) {
110
+ process.stdout.write(`❌ ${exc.message}\n`);
111
+ return 1;
112
+ }
113
+ throw exc;
114
+ }
115
+
90
116
  if (!_exists(MARKETPLACE)) {
91
117
  process.stdout.write(`❌ ${MARKETPLACE} not found\n`);
92
118
  return 1;
@@ -25,6 +25,8 @@ import * as fs from 'node:fs';
25
25
  import * as path from 'node:path';
26
26
  import { fileURLToPath, pathToFileURL } from 'node:url';
27
27
 
28
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
29
+
28
30
  const _HERE = fileURLToPath(import.meta.url);
29
31
 
30
32
  // src/scripts/lint_marketplace_install_completeness.ts → two dirs up is the
@@ -150,6 +152,27 @@ function lint(
150
152
  const commands = load_hook_commands(hooks_path);
151
153
  const known = load_dispatcher_subcommands(dispatch_path);
152
154
 
155
+ // `load_hook_commands` skips every non-conforming `hooks` shape silently, so
156
+ // a restructured hooks.json yields an empty command list and the gate
157
+ // announces "0 hook command(s) checked" — green. Assert the UNFILTERED list:
158
+ // `checked` below excludes entries whose command line failed to parse, and
159
+ // those are exactly the ones this gate must still report on. Exit 2 is the
160
+ // schema/file-error code ("could not run"); 1 means an unknown subcommand.
161
+ try {
162
+ assertScanned({
163
+ gate: 'lint-marketplace-install',
164
+ scanned: commands.length,
165
+ units: 'hook command(s)',
166
+ roots: [hooks_path],
167
+ });
168
+ } catch (exc) {
169
+ if (exc instanceof DeadScopeError) {
170
+ process.stderr.write(`${exc.message}\n`);
171
+ return 2;
172
+ }
173
+ throw exc;
174
+ }
175
+
153
176
  const issues: string[] = [];
154
177
  let checked = 0;
155
178
  for (const [event, cmd] of commands) {
@@ -24,6 +24,7 @@ import * as fs from 'node:fs';
24
24
  import { fileURLToPath, pathToFileURL } from 'node:url';
25
25
 
26
26
  import * as sl from './_lib/security_lint.js';
27
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
27
28
 
28
29
  export const CHECK = 'mcp-config-security';
29
30
 
@@ -204,12 +205,35 @@ export function main(argv: readonly string[] | null = null): number {
204
205
  // scan .md (fenced examples) under the default roots PLUS named MCP configs
205
206
  // under src/templates (where the shipped claude_desktop_config template lives).
206
207
  const roots = [...sl.DEFAULT_SCAN_ROOTS, 'src/templates'];
208
+ let corpusFiles = 0;
207
209
  for (const sf of sl.iter_corpus(roots, ['.md', '.json', '.template'])) {
210
+ corpusFiles += 1;
208
211
  for (const h of _scan(sf)) {
209
212
  findings.push(h);
210
213
  }
211
214
  }
212
215
 
216
+ // `iter_corpus` skips a root that does not exist, so a moved
217
+ // `src/templates` (where the only shipped MCP config template lives) leaves
218
+ // this lint scanning `.md` fences alone — or nothing at all — while still
219
+ // printing "clean". Count files READ, not findings. Exit 1 is this CLI's
220
+ // only failure code (2 is the pinned argparse usage error), so it carries
221
+ // "could not run" here.
222
+ try {
223
+ assertScanned({
224
+ gate: 'lint_mcp_config_security',
225
+ scanned: corpusFiles,
226
+ units: 'corpus file(s)',
227
+ roots,
228
+ });
229
+ } catch (exc) {
230
+ if (exc instanceof DeadScopeError) {
231
+ process.stderr.write(`❌ ${exc.message}\n`);
232
+ return 1;
233
+ }
234
+ throw exc;
235
+ }
236
+
213
237
  if (args.json) {
214
238
  const payload = sl.py_json_dumps_indent2(findings.map((f) => f.toDict()));
215
239
  process.stdout.write(payload + '\n');
@@ -26,6 +26,8 @@ import * as fs from 'node:fs';
26
26
  import * as path from 'node:path';
27
27
  import { fileURLToPath, pathToFileURL } from 'node:url';
28
28
 
29
+ import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
30
+
29
31
  const _HERE = path.resolve(fileURLToPath(import.meta.url));
30
32
  // ROOT = Path(__file__).resolve().parents[2]
31
33
  const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
@@ -295,7 +297,26 @@ function main(argv: readonly string[]): number {
295
297
  throw e;
296
298
  }
297
299
 
298
- for (const p of [SCHEMA, MANIFEST, ROW_MD, CF_JSON, SERVER_JSON]) {
300
+ const artefacts = [SCHEMA, MANIFEST, ROW_MD, CF_JSON, SERVER_JSON];
301
+ // The per-artefact loop below stays authoritative — it names the FIRST
302
+ // missing file and is the pinned `missing: <path>` contract. This assertion
303
+ // covers the case that loop reports misleadingly: all five gone at once,
304
+ // which is a relocated ROOT rather than an unbuilt artefact, and deserves a
305
+ // message that says so. Exit 1 is the gate's only failure code.
306
+ try {
307
+ assertWatchlistResolves({
308
+ gate: 'lint_mcp_registry_manifest',
309
+ candidates: artefacts.map(_relToRoot),
310
+ repoRoot: ROOT,
311
+ });
312
+ } catch (e) {
313
+ if (e instanceof DeadScopeError) {
314
+ process.stderr.write(`❌ ${e.message}\n`);
315
+ return 1;
316
+ }
317
+ throw e;
318
+ }
319
+ for (const p of artefacts) {
299
320
  if (!fs.existsSync(p)) {
300
321
  return _fail(`missing: ${_relToRoot(p)}`);
301
322
  }
@@ -24,6 +24,8 @@ import fs from 'node:fs';
24
24
  import path from 'node:path';
25
25
  import { fileURLToPath, pathToFileURL } from 'node:url';
26
26
 
27
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
28
+
27
29
  const _HERE = fileURLToPath(import.meta.url);
28
30
  export const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
29
31
 
@@ -73,6 +75,15 @@ function _git(root: string, args: string[]): string {
73
75
  return execFileSync('git', args, { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
74
76
  }
75
77
 
78
+ /** How many tracked memory files `scanWorktree` has to read (0 when git fails). */
79
+ function _trackedCount(root: string): number {
80
+ try {
81
+ return _git(root, ['ls-files', TRACKED_GLOB_ROOT]).split('\n').filter(Boolean).length;
82
+ } catch {
83
+ return 0;
84
+ }
85
+ }
86
+
76
87
  /** Scan the current worktree's tracked memory files. */
77
88
  export function scanWorktree(root: string): Hit[] {
78
89
  const out: Hit[] = [];
@@ -147,6 +158,15 @@ export function main(argv: string[] = process.argv.slice(2)): number {
147
158
  process.stdout.write(
148
159
  `scanned ${commits} commit(s), ${blobs} unique blob(s) under ${TRACKED_GLOB_ROOT}\n`,
149
160
  );
161
+ // The honest-null verdict rests on this scan having covered the
162
+ // full history; a "zero fires" certification over zero blobs would
163
+ // silently re-certify nothing at all.
164
+ assertScanned({
165
+ gate: 'lint_memory_tripwire',
166
+ scanned: blobs,
167
+ units: 'historical blob(s)',
168
+ roots: [TRACKED_GLOB_ROOT],
169
+ });
150
170
  if (hits.length === 0) {
151
171
  process.stdout.write('✅ zero tripwire fires across the full history.\n');
152
172
  return 0;
@@ -157,6 +177,15 @@ export function main(argv: string[] = process.argv.slice(2)): number {
157
177
  process.stdout.write(`❌ ${hits.length} fire(s). HALT — never rewrite; a human decides.\n`);
158
178
  return 1;
159
179
  }
180
+ // Count the corpus separately from the hits: `scanWorktree` returns
181
+ // only fires, and "no fires" is the pass state, so an unlistable or
182
+ // moved agents/memory tree would otherwise read as clean.
183
+ assertScanned({
184
+ gate: 'lint_memory_tripwire',
185
+ scanned: _trackedCount(root),
186
+ units: 'tracked memory file(s)',
187
+ roots: [TRACKED_GLOB_ROOT],
188
+ });
160
189
  const hits = scanWorktree(root);
161
190
  if (hits.length === 0) {
162
191
  process.stdout.write('✅ tripwire clean on tracked memory files.\n');
@@ -168,6 +197,12 @@ export function main(argv: string[] = process.argv.slice(2)): number {
168
197
  process.stdout.write(`❌ ${hits.length} fire(s). HALT — never rewrite; a human decides.\n`);
169
198
  return 1;
170
199
  } catch (exc) {
200
+ if (exc instanceof DeadScopeError) {
201
+ // A dead scope is a tripwire failure, not an internal fault: the
202
+ // gate reports the same HALT code it uses for a fire.
203
+ process.stderr.write(`❌ ${exc.message}\n`);
204
+ return 1;
205
+ }
171
206
  process.stderr.write(`lint_memory_tripwire: internal error: ${String(exc)}\n`);
172
207
  return 3;
173
208
  }
@@ -35,6 +35,8 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
35
35
 
36
36
  import YAML from 'yaml';
37
37
 
38
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
39
+
38
40
  const _HERE = fileURLToPath(import.meta.url);
39
41
 
40
42
  const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
@@ -684,16 +686,30 @@ export function main(argv?: string[]): number {
684
686
  const mission_schema = _load_schema(MISSION_SCHEMA_PATH);
685
687
  const catalog_schema = _load_schema(CATALOG_SCHEMA_PATH);
686
688
 
687
- if (!_isDir(MISSIONS_ROOT)) {
688
- if (!args.quiet) {
689
- process.stderr.write(`No missions directory found at ${MISSIONS_ROOT}\n`);
690
- }
691
- return 0;
692
- }
693
-
694
689
  const all_findings: Finding[] = [];
695
690
  const mission_dirs = _sortedMissionDirs();
696
691
 
692
+ // Supersedes the `No missions directory found → exit 0` short-circuit: this
693
+ // gate is warn-only about what a mission SAYS, never about whether there are
694
+ // missions to read, and a missing root produced the same silent 0 as a clean
695
+ // run. Exit 1 is the hard-failure code (2 is reserved for argparse and
696
+ // schema-load errors), and it fires regardless of --strict — a scope that
697
+ // read nothing is not a warning about a manifest.
698
+ try {
699
+ assertScanned({
700
+ gate: 'lint_missions',
701
+ scanned: mission_dirs.length,
702
+ units: 'mission(s)',
703
+ roots: ['src/missions'],
704
+ });
705
+ } catch (exc) {
706
+ if (exc instanceof DeadScopeError) {
707
+ process.stderr.write(`❌ ${exc.message}\n`);
708
+ return 1;
709
+ }
710
+ throw exc;
711
+ }
712
+
697
713
  for (const mission_dir of mission_dirs) {
698
714
  const findings = validate_mission(mission_dir, mission_schema, catalog_schema);
699
715
  all_findings.push(...findings);
@@ -23,6 +23,7 @@ import * as path from 'node:path';
23
23
  import { fileURLToPath, pathToFileURL } from 'node:url';
24
24
  import { parse_frontmatter } from './validate_frontmatter.js';
25
25
  import { artefact_roots, iter_commands } from './_lib/agent_src.js';
26
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
26
27
 
27
28
  const _HERE = fileURLToPath(import.meta.url);
28
29
  const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
@@ -99,6 +100,23 @@ function main(argv: readonly string[]): number {
99
100
  }
100
101
  }
101
102
 
103
+ // `total` is the artefacts read, `missing` the verdict — a coverage gate
104
+ // whose enumerator yields nothing reports full coverage of nothing.
105
+ try {
106
+ assertScanned({
107
+ gate: 'lint_model_tier_coverage',
108
+ scanned: total,
109
+ units: 'artefact(s)',
110
+ roots: ['src/skills', 'src/agent-src/commands', 'src/domains'],
111
+ });
112
+ } catch (exc) {
113
+ if (exc instanceof DeadScopeError) {
114
+ process.stderr.write(`❌ ${exc.message}\n`);
115
+ return 1;
116
+ }
117
+ throw exc;
118
+ }
119
+
102
120
  if (missing.length > 0) {
103
121
  for (const [kind, rel] of missing.slice(0, 40)) {
104
122
  process.stdout.write(
@@ -29,6 +29,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
29
29
  import { parse as parseYaml } from 'yaml';
30
30
 
31
31
  import { iter_all_sources } from './_lib/agent_src.js';
32
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
32
33
 
33
34
  const _HERE = fileURLToPath(import.meta.url);
34
35
 
@@ -135,6 +136,24 @@ function main(): number {
135
136
  }
136
137
  }
137
138
 
139
+ // `total` is the names read, not the collisions found: uniqueness holds
140
+ // vacuously over an empty name space, so only the corpus count can tell
141
+ // "no collisions" apart from "no artefacts reached the normalizer".
142
+ try {
143
+ assertScanned({
144
+ gate: 'lint_namespace_collisions',
145
+ scanned: total,
146
+ units: 'artefact name(s)',
147
+ roots: ['src/skills', 'src/rules', 'src/agent-src/commands', 'src/domains'],
148
+ });
149
+ } catch (exc) {
150
+ if (exc instanceof DeadScopeError) {
151
+ process.stderr.write(`❌ ${exc.message}\n`);
152
+ return 1;
153
+ }
154
+ throw exc;
155
+ }
156
+
138
157
  const collisions = [...seen.entries()].filter(([, v]) => v.length > 1);
139
158
  if (collisions.length) {
140
159
  // sorted(collisions) — by (namespace, name) tuple, which the key