@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
@@ -39,6 +39,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
39
39
  // byte-faithful for behaviour (the only observable effect is the --global path).
40
40
  import * as _kg from './_lib/knowledge_global.js';
41
41
  import * as _kgr from './_lib/knowledge_global_redaction.js';
42
+ import { assertScanned } from './_lib/scan_scope.js';
42
43
 
43
44
  // ROOT = Path(__file__).resolve().parent.parent.parent — src/scripts/ → repo root.
44
45
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
@@ -373,6 +374,18 @@ function main(): number {
373
374
  }
374
375
 
375
376
  const cards = _sortedGlobMd(card_dir).filter((p) => path.basename(p).toLowerCase() !== 'readme.md');
377
+ assertScanned({
378
+ gate: 'check_knowledge_cards',
379
+ scanned: cards.length,
380
+ units: 'knowledge card(s)',
381
+ roots: [path.relative(ROOT, card_dir) || card_dir],
382
+ allowEmpty:
383
+ 'OPTIONAL_INPUT: cards are project-authored runtime content under agents/knowledge/ '
384
+ + '(or a --dir / --global store), never shipped package source. Delete the root and '
385
+ + 'zero still reads as "this project has captured no cards yet" — which is why both '
386
+ + 'the missing-dir and empty-dir paths are pinned as exit 0 by the CLI contract '
387
+ + 'below. A stale root cannot hide here: --dir is caller-supplied per invocation.',
388
+ });
376
389
  if (cards.length === 0) {
377
390
  process.stdout.write('No knowledge cards found — nothing to check.\n');
378
391
  return 0;
@@ -29,6 +29,8 @@ import process from 'node:process';
29
29
  import { fileURLToPath } from 'node:url';
30
30
  import YAML from 'yaml';
31
31
 
32
+ import { assertScanned } from './_lib/scan_scope.js';
33
+
32
34
  const PROG = 'check_knowledge_pages.ts';
33
35
  const PAGE_LINE_BUDGET = 200;
34
36
  const CONTESTED_WARN_COUNT = 2;
@@ -135,6 +137,14 @@ function listMarkdownFiles(dirPath: string): string[] {
135
137
  .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
136
138
  }
137
139
 
140
+ /** Pages `lintAll` reads — the corpus, independent of how many warn. */
141
+ function countPages(knowledgeRoot: string): number {
142
+ return TYPED_DIRS.reduce(
143
+ (n, dir) => n + listMarkdownFiles(path.join(knowledgeRoot, dir)).length,
144
+ 0,
145
+ );
146
+ }
147
+
138
148
  export function lintAll(knowledgeRoot: string, today: Date = new Date()): Warning[] {
139
149
  const warnings: Warning[] = [];
140
150
  for (const dir of TYPED_DIRS) {
@@ -187,6 +197,19 @@ export function main(argv: string[]): number {
187
197
 
188
198
  const knowledgeRoot = path.join(dir, 'agents', 'knowledge');
189
199
  const warnings = lintAll(knowledgeRoot);
200
+ // Pages read, not warnings emitted — this lint never fails the build, so a
201
+ // moved root would print "no warnings" indefinitely with nothing behind it.
202
+ assertScanned({
203
+ gate: 'check_knowledge_pages',
204
+ scanned: countPages(knowledgeRoot),
205
+ units: 'knowledge page(s)',
206
+ roots: TYPED_DIRS.map((d) => path.join('agents', 'knowledge', d)),
207
+ allowEmpty:
208
+ 'OPTIONAL_INPUT: the lifecycle-typed pages are project-authored runtime content, '
209
+ + 'not shipped package source, and the four typed subdirs are created on first '
210
+ + 'capture. Delete them and zero still reads as "this project has written no '
211
+ + 'knowledge pages yet" — the state every consumer starts in.',
212
+ });
190
213
 
191
214
  if (format === 'json') {
192
215
  process.stdout.write(JSON.stringify(warnings, null, 2) + '\n');
@@ -29,6 +29,8 @@ import process from 'node:process';
29
29
  import { fileURLToPath } from 'node:url';
30
30
  import YAML from 'yaml';
31
31
 
32
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
33
+
32
34
  const PROG = 'check_knowledge_sharing.ts';
33
35
  const CREATION_BUDGET_WARN = 5;
34
36
  const FRONTMATTER_RE = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n/;
@@ -150,6 +152,31 @@ export function main(argv: string[]): number {
150
152
  return 3;
151
153
  }
152
154
 
155
+ // Diff-driven: the corpus is whatever is staged, and an empty index is the
156
+ // normal state of a clean tree — so zero is allowed here. The dangerous
157
+ // empty (git itself failing) is NOT swallowed: it exits 3 above rather than
158
+ // reaching this point with an empty list. Exit 3 (internal error) is the
159
+ // code a dead scope would share, for the same "could not run" reason.
160
+ try {
161
+ assertScanned({
162
+ gate: 'check_knowledge_sharing',
163
+ scanned: staged.length,
164
+ units: 'staged path(s)',
165
+ roots: ['git diff --cached --name-status'],
166
+ allowEmpty:
167
+ 'EMPTY_VALID: nothing staged means there is no commit to judge — an absent ' +
168
+ 'question, not an empty corpus. Deletion test: deleting agents/knowledge/ ' +
169
+ 'would not produce this zero (the staged deletions would themselves show up ' +
170
+ 'in the diff); only an empty index does, and that is a clean tree.',
171
+ });
172
+ } catch (err) {
173
+ if (err instanceof DeadScopeError) {
174
+ process.stderr.write(`❌ ${err.message}\n`);
175
+ return 3;
176
+ }
177
+ throw err;
178
+ }
179
+
153
180
  const { blocked, warnings } = checkSharing(staged, gitStagedContent);
154
181
 
155
182
  for (const w of warnings) {
@@ -20,6 +20,8 @@ import * as fs from 'node:fs';
20
20
  import * as path from 'node:path';
21
21
  import { fileURLToPath, pathToFileURL } from 'node:url';
22
22
 
23
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
24
+
23
25
  // Umlauts and German-only characters.
24
26
  const UMLAUT_RE = /[äöüÄÖÜß]/g;
25
27
 
@@ -247,6 +249,7 @@ export function main(argv?: readonly string[]): number {
247
249
  const args = parse_args(argv ?? process.argv.slice(2));
248
250
 
249
251
  const all_violations: Violation[] = [];
252
+ let scanned = 0;
250
253
  for (const raw_path of args.paths) {
251
254
  if (!_exists(raw_path)) {
252
255
  process.stderr.write(`⚠️ Not found: ${raw_path}\n`);
@@ -256,9 +259,30 @@ export function main(argv?: readonly string[]): number {
256
259
  process.stderr.write(`⚠️ Skipping non-.md: ${raw_path}\n`);
257
260
  continue;
258
261
  }
262
+ scanned += 1;
259
263
  all_violations.push(...scan_file(raw_path));
260
264
  }
261
265
 
266
+ // The caller expands the path list (`find docs -name '*.md' …`), so a moved
267
+ // docs tree hands this gate paths that every skip above swallows into a
268
+ // stderr warning while stdout still reports "No German content detected".
269
+ // Exit 3 is the documented "internal error" code — the gate could not run;
270
+ // 1 means it read files and found German in them.
271
+ try {
272
+ assertScanned({
273
+ gate: 'check_md_language',
274
+ scanned,
275
+ units: '.md file(s)',
276
+ roots: [`${String(args.paths.length)} caller-supplied path(s)`],
277
+ });
278
+ } catch (exc) {
279
+ if (exc instanceof DeadScopeError) {
280
+ process.stderr.write(`❌ ${exc.message}\n`);
281
+ return 3;
282
+ }
283
+ throw exc;
284
+ }
285
+
262
286
  if (args.format === 'json') {
263
287
  process.stdout.write(JSON.stringify(all_violations, null, 2) + '\n');
264
288
  } else if (all_violations.length === 0) {
@@ -21,6 +21,8 @@ import * as path from 'node:path';
21
21
  import * as fs from 'node:fs';
22
22
  import { fileURLToPath, pathToFileURL } from 'node:url';
23
23
 
24
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
25
+
24
26
  const _FILE = fileURLToPath(import.meta.url);
25
27
 
26
28
  export interface MediaTool {
@@ -71,6 +73,27 @@ export function missingTools(tools: MediaTool[] = MEDIA_TOOLS): MediaTool[] {
71
73
 
72
74
  export function main(argv: string[] = process.argv.slice(2)): number {
73
75
  const quiet = argv.includes('--quiet');
76
+
77
+ // The scan scope is the declared tool list, not a directory: an empty
78
+ // MEDIA_TOOLS probes nothing and still prints the ✅ "all present" line.
79
+ // Deletion test: emptying the declaration would legitimately yield 0 only
80
+ // if "no tooling is required", which is never true for a prereq check.
81
+ // Exit 1 is the only failure code this CLI has, so a dead scope shares it.
82
+ try {
83
+ assertScanned({
84
+ gate: 'check_media_deps',
85
+ scanned: MEDIA_TOOLS.length,
86
+ units: 'declared media tool(s)',
87
+ roots: ['MEDIA_TOOLS (declared in src/scripts/check_media_deps.ts)'],
88
+ });
89
+ } catch (exc) {
90
+ if (exc instanceof DeadScopeError) {
91
+ process.stderr.write(`❌ ${exc.message}\n`);
92
+ return 1;
93
+ }
94
+ throw exc;
95
+ }
96
+
74
97
  const missing = missingTools();
75
98
  if (missing.length === 0) {
76
99
  if (!quiet) {
@@ -53,6 +53,8 @@ import process from 'node:process';
53
53
  import { fileURLToPath, pathToFileURL } from 'node:url';
54
54
  import YAML, { parseDocument } from 'yaml';
55
55
 
56
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
57
+
56
58
  type Severity = 'error' | 'warning' | 'info';
57
59
 
58
60
  const REQUIRED_KEYS: ReadonlySet<string> = new Set([
@@ -722,11 +724,34 @@ function main(): number {
722
724
  }
723
725
  return 0;
724
726
  }
727
+ // An ABSENT root stays a pinned exit-0 no-op (the branch above; the memory
728
+ // tree is an optional surface and `task check-memory` short-circuits it).
729
+ // A root that EXISTS but walks to zero `*.yml` is the case nothing covered:
730
+ // entries moved to a subtree this walk does not reach, and the validator
731
+ // reported "0 error(s)" over an empty read. No `allowEmpty` for that half —
732
+ // a present-but-unreadable corpus is blindness, not a clean bill of health.
733
+ // Exit 1 is this gate's only failure code (see `_emit`).
734
+ const ymls = _rglobYmlSorted(root);
735
+ try {
736
+ assertScanned({
737
+ gate: 'check_memory',
738
+ scanned: ymls.length,
739
+ units: 'memory entry file(s)',
740
+ roots: [`${root}/**/*.yml`],
741
+ });
742
+ } catch (exc) {
743
+ if (exc instanceof DeadScopeError) {
744
+ process.stderr.write(`❌ ${exc.message}\n`);
745
+ return 1;
746
+ }
747
+ throw exc;
748
+ }
749
+
725
750
  const criticalCounts: Record<string, number> = {};
726
751
  for (const key of Object.keys(_TYPE_COUNTS)) {
727
752
  delete _TYPE_COUNTS[key];
728
753
  }
729
- for (const yml of _rglobYmlSorted(root)) {
754
+ for (const yml of ymls) {
730
755
  _validateFile(yml, findings, criticalCounts);
731
756
  }
732
757
  // Tier-0 inflation warning — soft cap on `priority: critical` per type.
@@ -34,6 +34,7 @@ import process from 'node:process';
34
34
  import { fileURLToPath } from 'node:url';
35
35
  import YAML from 'yaml';
36
36
 
37
+ import { assertScanned } from './_lib/scan_scope.js';
37
38
  import { jaccardSimilarity } from './_lib/text_similarity.js';
38
39
 
39
40
  const PROG = 'check_memory_contradiction.ts';
@@ -234,7 +235,21 @@ function main(argv: string[]): number {
234
235
 
235
236
  let hit: ContradictionHit | null;
236
237
  try {
237
- hit = findContradiction(key, body, loadCuratedEntries(path.resolve(memoryRoot), type));
238
+ const existing = loadCuratedEntries(path.resolve(memoryRoot), type);
239
+ // Curated entries READ — the corpus every verdict below is decided
240
+ // against. Zero of them makes "no contradiction" unfalsifiable.
241
+ assertScanned({
242
+ gate: 'check_memory_contradiction',
243
+ scanned: existing.length,
244
+ units: 'curated entry(ies)',
245
+ roots: [path.join(memoryRoot, type), path.join(memoryRoot, `${type}.yml`)],
246
+ allowEmpty:
247
+ 'OPTIONAL_INPUT: the per-type curated store is created by the first '
248
+ + '/memory promote of that type. Delete it and zero still reads as "nothing '
249
+ + 'of this type has been curated yet" — a state every consumer starts in, and '
250
+ + 'one in which no contradiction can exist by definition.',
251
+ });
252
+ hit = findContradiction(key, body, existing);
238
253
  } catch (exc) {
239
254
  process.stderr.write(`${PROG}: internal error: ${String(exc)}\n`);
240
255
  return 3;
@@ -16,6 +16,8 @@ import * as path from 'node:path';
16
16
  import { fileURLToPath, pathToFileURL } from 'node:url';
17
17
  import YAML from 'yaml';
18
18
 
19
+ import { assertScanned } from './_lib/scan_scope.js';
20
+
19
21
  const _HERE = fileURLToPath(import.meta.url);
20
22
 
21
23
  // Relative to cwd, mirroring Python `Path("agents/memory/intake")`.
@@ -296,6 +298,19 @@ function _json_dumps_ascii(obj: unknown): string {
296
298
 
297
299
  function main(argv?: readonly string[]): number {
298
300
  const args = parse_args(argv ?? process.argv.slice(2));
301
+ // Intake FILES walked — the scope both `_find_intake` (record lookup) and
302
+ // `_count_sibling_paths` (pattern evidence) share.
303
+ assertScanned({
304
+ gate: 'check_memory_proposal',
305
+ scanned: _jsonlFilesSorted().length,
306
+ units: 'intake signal file(s)',
307
+ roots: [INTAKE_ROOT],
308
+ allowEmpty:
309
+ 'OPTIONAL_INPUT: the intake dir is local, gitignored capture written by '
310
+ + '/memory propose. Delete it and zero still reads as "nothing captured yet". '
311
+ + 'An empty scope also tightens rather than relaxes this gate: zero sibling '
312
+ + 'paths forces the future_decisions branch, and --intake-id already fails 1.',
313
+ });
299
314
  let record: Record_;
300
315
  let source: string;
301
316
  if (args.intake_id !== null) {
@@ -22,6 +22,7 @@ import * as path from 'node:path';
22
22
  import process from 'node:process';
23
23
  import { fileURLToPath } from 'node:url';
24
24
 
25
+ import { assertScanned } from './_lib/scan_scope.js';
25
26
  import { findMostSimilar, type Candidate } from './_lib/text_similarity.js';
26
27
 
27
28
  const PROG = 'check_memory_similarity.ts';
@@ -106,6 +107,21 @@ export function main(argv: string[]): number {
106
107
  return 2;
107
108
  }
108
109
 
110
+ // The unit is the signal FILES the glob walks, not the type-filtered
111
+ // candidates: the first entry of a given type legitimately has zero
112
+ // candidates, so only the unfiltered walk distinguishes that from a root
113
+ // that moved and turned every proposal into a silent "create".
114
+ assertScanned({
115
+ gate: 'check_memory_similarity',
116
+ scanned: globSignals(intakeRoot).length,
117
+ units: 'intake signal file(s)',
118
+ roots: [intakeRoot],
119
+ allowEmpty:
120
+ 'OPTIONAL_INPUT: intake signals are local, gitignored capture written by '
121
+ + '/memory propose — never shipped source. Delete the root and zero still reads '
122
+ + 'as "nothing has been captured in this project yet", which is exactly the '
123
+ + 'state of the very first proposal. Advisory check: it never blocks a create.',
124
+ });
109
125
  const candidates = loadCandidates(intakeRoot, type);
110
126
  const match = findMostSimilar(body, candidates);
111
127
 
@@ -27,6 +27,8 @@ import * as fs from 'node:fs';
27
27
  import * as 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 _HERE = fileURLToPath(import.meta.url);
31
33
  const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
32
34
  const SKILL_PATH = path.join(
@@ -55,14 +57,6 @@ const BODY_BANNED_PATTERNS: readonly BannedPattern[] = [
55
57
  { re: /App\\\\Modules\\\\/, source: 'App\\\\\\\\Modules\\\\\\\\' },
56
58
  ];
57
59
 
58
- function _isFile(p: string): boolean {
59
- try {
60
- return fs.statSync(p).isFile();
61
- } catch {
62
- return false;
63
- }
64
- }
65
-
66
60
  /** POSIX relative path of `child` under `root` (mirrors relative_to().as_posix-ish for display). */
67
61
  function _relToPosix(child: string, root: string): string {
68
62
  return path.relative(root, child).split(path.sep).join('/');
@@ -193,9 +187,23 @@ function _scan_frontmatter(fm: string): string[] {
193
187
  }
194
188
 
195
189
  function main(): number {
196
- if (!_isFile(SKILL_PATH)) {
197
- process.stderr.write(`error: SKILL.md not found at ${SKILL_PATH}\n`);
198
- return 2;
190
+ // One named file is the entire scan scope, so `_isFile` was this gate's
191
+ // zero-check; the shared assertion replaces it and names the scope. The
192
+ // ported message is kept verbatim ahead of it (the py2ts CLI contract pins
193
+ // it) and exit 2 is unchanged.
194
+ try {
195
+ assertWatchlistResolves({
196
+ gate: 'check_module_management_neutral',
197
+ candidates: [_relToPosix(SKILL_PATH, REPO_ROOT)],
198
+ repoRoot: REPO_ROOT,
199
+ });
200
+ } catch (exc) {
201
+ if (exc instanceof DeadScopeError) {
202
+ process.stderr.write(`error: SKILL.md not found at ${SKILL_PATH}\n`);
203
+ process.stderr.write(` ${exc.message}\n`);
204
+ return 2;
205
+ }
206
+ throw exc;
199
207
  }
200
208
  const text = fs.readFileSync(SKILL_PATH, 'utf-8');
201
209
  const [fm, body] = _split_frontmatter(text);
@@ -44,6 +44,8 @@ import * as path from 'node:path';
44
44
  import process from 'node:process';
45
45
  import { fileURLToPath, pathToFileURL } from 'node:url';
46
46
 
47
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
48
+
47
49
  const _HERE = fileURLToPath(import.meta.url);
48
50
  // Path(__file__).resolve().parents[2] — repo root, two dirs up from src/scripts.
49
51
  const REPO = path.resolve(path.dirname(_HERE), '..', '..');
@@ -174,6 +176,25 @@ function main(argv?: readonly string[]): number {
174
176
  const args = parse_args(argv ?? process.argv.slice(2));
175
177
 
176
178
  const allow = load_allowlist();
179
+ const tracked = tracked_text_files();
180
+ // `_git` swallows every failure and returns "", so a run outside a work
181
+ // tree — or against a repo whose index this process cannot read — walks an
182
+ // empty file list, finds no markers, and reports the tree clean. The count
183
+ // the success line already prints was the evidence; nothing asserted on it.
184
+ try {
185
+ assertScanned({
186
+ gate: 'check_no_conflict_markers',
187
+ scanned: tracked.length,
188
+ units: 'tracked file(s)',
189
+ roots: [`${REPO} (git ls-files)`],
190
+ });
191
+ } catch (exc) {
192
+ if (exc instanceof DeadScopeError) {
193
+ process.stderr.write(`❌ ${exc.message}\n`);
194
+ return 1;
195
+ }
196
+ throw exc;
197
+ }
177
198
  const unmerged = unmerged_paths();
178
199
  const markerHits = scan_markers(allow);
179
200
 
@@ -208,7 +229,7 @@ function main(argv?: readonly string[]): number {
208
229
  if (!args.quiet) {
209
230
  process.stdout.write(
210
231
  `✅ check_no_conflict_markers: no conflicted index entries, no markers ` +
211
- `(${tracked_text_files().length} tracked files scanned).\n`,
232
+ `(${tracked.length} tracked files scanned).\n`,
212
233
  );
213
234
  }
214
235
  return 0;
@@ -36,6 +36,8 @@ import * as path from 'node:path';
36
36
  import process from 'node:process';
37
37
  import { fileURLToPath, pathToFileURL } from 'node:url';
38
38
 
39
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
40
+
39
41
  const _HERE = fileURLToPath(import.meta.url);
40
42
  // parents[2] of src/scripts/<file> is the repo root.
41
43
  const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
@@ -272,8 +274,31 @@ function main(argv: readonly string[]): number {
272
274
  const patterns: Array<[string, RegExp]> = cfg.deny.map((p) => [p, new RegExp(p, 'i')]);
273
275
  const skipGlobs = cfg.skip_paths ?? [];
274
276
 
277
+ // `_tracked_files` raises when git itself fails, but a git that succeeds
278
+ // with empty output (not a repo, nothing tracked) yields the same empty
279
+ // loop and the ✅ "no external-source references" line. Not a diff — this
280
+ // is the whole tracked tree, so zero is never a legitimate state; no
281
+ // `allowEmpty`. Assert the unfiltered enumeration, before the extension /
282
+ // skip_paths filters, so a filter change can never mask a dead scope.
283
+ // Exit 1 is the only failure code this pinned CLI reaches.
284
+ const tracked = _tracked_files();
285
+ try {
286
+ assertScanned({
287
+ gate: 'check_no_external_sources',
288
+ scanned: tracked.length,
289
+ units: 'tracked file(s)',
290
+ roots: ['git ls-files'],
291
+ });
292
+ } catch (exc) {
293
+ if (exc instanceof DeadScopeError) {
294
+ process.stderr.write(`❌ ${exc.message}\n`);
295
+ return 1;
296
+ }
297
+ throw exc;
298
+ }
299
+
275
300
  const hits: Hit[] = [];
276
- for (const rel of _tracked_files()) {
301
+ for (const rel of tracked) {
277
302
  if (_SKIP_EXT.has(_suffixLower(rel))) {
278
303
  continue;
279
304
  }
@@ -21,29 +21,56 @@ import * as path from 'node:path';
21
21
  import * as fs from 'node:fs';
22
22
  import { fileURLToPath, pathToFileURL } from 'node:url';
23
23
 
24
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
25
+
24
26
  const _HERE = fileURLToPath(import.meta.url);
25
27
 
26
28
  const LOCAL_FILE = '.agent-settings.local.yml';
27
29
 
28
- function tracked_local_settings(): string[] {
30
+ /** Every tracked path — the unfiltered corpus the basename match runs over. */
31
+ function _tracked_files(): string[] {
29
32
  const res = spawnSync('git', ['ls-files'], { encoding: 'utf-8' });
30
33
  // Mirror Python: CalledProcessError (non-zero) or FileNotFoundError
31
34
  // (git missing) → return [] (nothing to enforce).
32
35
  if (res.error !== undefined || res.status !== 0 || typeof res.stdout !== 'string') {
33
36
  return [];
34
37
  }
35
- const out = res.stdout;
36
- return out
37
- .split('\n')
38
- .filter((line) => line !== '')
39
- .filter((line) => {
40
- const parts = line.split('/');
41
- return parts[parts.length - 1] === LOCAL_FILE;
42
- });
38
+ return res.stdout.split('\n').filter((line) => line !== '');
39
+ }
40
+
41
+ function _isLocalSettings(line: string): boolean {
42
+ const parts = line.split('/');
43
+ return parts[parts.length - 1] === LOCAL_FILE;
44
+ }
45
+
46
+ function tracked_local_settings(): string[] {
47
+ return _tracked_files().filter(_isLocalSettings);
43
48
  }
44
49
 
45
50
  function main(): number {
46
- const offenders = tracked_local_settings();
51
+ // Not a diff — `git ls-files` enumerates the WHOLE tracked tree, so zero
52
+ // lines means git failed or this is not a repo, not "nothing committed".
53
+ // The swallow above turns both into an empty list, and the gate then prints
54
+ // the ✅ line. Deletion test: a repo with zero tracked files is never a
55
+ // legitimate state here, so no `allowEmpty`. Exit 1 is the only failure
56
+ // code this pinned CLI has.
57
+ const tracked = _tracked_files();
58
+ try {
59
+ assertScanned({
60
+ gate: 'check_no_local_settings_committed',
61
+ scanned: tracked.length,
62
+ units: 'tracked file(s)',
63
+ roots: ['git ls-files'],
64
+ });
65
+ } catch (exc) {
66
+ if (exc instanceof DeadScopeError) {
67
+ process.stderr.write(`❌ ${exc.message}\n`);
68
+ return 1;
69
+ }
70
+ throw exc;
71
+ }
72
+
73
+ const offenders = tracked.filter(_isLocalSettings);
47
74
  if (offenders.length === 0) {
48
75
  process.stdout.write(`✅ No tracked ${LOCAL_FILE} files.\n`);
49
76
  return 0;
@@ -46,6 +46,8 @@ import * as path from 'node:path';
46
46
  import { fileURLToPath, pathToFileURL } from 'node:url';
47
47
 
48
48
  import { checkRatchet } from './_lib/gate_baseline.js';
49
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
50
+ import { readStdinText } from './_lib/stdin.js';
49
51
 
50
52
  const LEGACY = '.agent-src.uncondensed/';
51
53
  const EXEMPT: ReadonlySet<string> = new Set([
@@ -263,6 +265,31 @@ export function find_hardcoded_scan_roots(
263
265
  * live number, which is the whole point of committing it.
264
266
  */
265
267
  export function checkHardcodedScanRoots(repoRoot: string = process.cwd()): number {
268
+ // The scanned unit is script FILES walked, never `findings.length` — this
269
+ // pass is a downward ratchet, so zero findings is the goal state and can
270
+ // never distinguish "clean" from "the walk root moved". `_walkScripts`
271
+ // swallows an unreadable directory, so a relocated `src/scripts/` yields an
272
+ // empty walk, an actual of 0, a verdict under baseline, and a ✅ — this gate
273
+ // failing the same way it exists to detect. No `allowEmpty`: a repo with no
274
+ // scripts under the scan dir is blindness, not cleanliness.
275
+ // Exit 2 (git/environment failure) over 1 (above baseline) — a dead scope
276
+ // means the pass could not run; `main` propagates it via Math.max.
277
+ const walked = [..._walkScripts(path.join(repoRoot, HARDCODED_ROOT_SCAN_DIR))];
278
+ try {
279
+ assertScanned({
280
+ gate: HARDCODED_ROOT_GATE,
281
+ scanned: walked.length,
282
+ units: 'script file(s)',
283
+ roots: [`${HARDCODED_ROOT_SCAN_DIR}/**/*.{ts,mts,mjs}`],
284
+ });
285
+ } catch (exc) {
286
+ if (exc instanceof DeadScopeError) {
287
+ process.stdout.write(`❌ ${exc.message}\n`);
288
+ return 2;
289
+ }
290
+ throw exc;
291
+ }
292
+
266
293
  const findings = find_hardcoded_scan_roots(repoRoot);
267
294
  const verdict = checkRatchet({
268
295
  gate: HARDCODED_ROOT_GATE,
@@ -293,7 +320,9 @@ function main(): number {
293
320
  // documented failure the kernel-bundle step sidesteps the same way).
294
321
  let offenders: string[];
295
322
  if (process.argv.includes('--stdin')) {
296
- const stdin = fs.readFileSync(0, 'utf-8') as string;
323
+ // `readFileSync(0)` crashes with EAGAIN once the diff outgrows the pipe
324
+ // buffer — this gate's own PR was the first big enough to prove it.
325
+ const stdin = readStdinText();
297
326
  offenders = find_offenders(stdin);
298
327
  } else {
299
328
  const base = _base();
@@ -29,6 +29,8 @@ import * as fs from 'node:fs';
29
29
  import * as path from 'node:path';
30
30
  import { fileURLToPath, pathToFileURL } from 'node:url';
31
31
 
32
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
33
+
32
34
  // src/scripts/check_one_off_location.ts → two dirs up is the repo root.
33
35
  // Mirrors the Python `Path(__file__).resolve().parent.parent.parent`.
34
36
  const _HERE = fileURLToPath(import.meta.url);
@@ -73,6 +75,28 @@ function _rglobOneOff(root: string): string[] {
73
75
  return out;
74
76
  }
75
77
 
78
+ /** Files the guard's walk visits under `root` — the scope, not the matches. */
79
+ function _countFilesUnder(root: string): number {
80
+ let n = 0;
81
+ const walk = (dir: string): void => {
82
+ let entries: fs.Dirent[];
83
+ try {
84
+ entries = fs.readdirSync(dir, { withFileTypes: true });
85
+ } catch {
86
+ return;
87
+ }
88
+ for (const ent of entries) {
89
+ if (ent.isDirectory()) {
90
+ walk(path.join(dir, ent.name));
91
+ } else {
92
+ n += 1;
93
+ }
94
+ }
95
+ };
96
+ walk(root);
97
+ return n;
98
+ }
99
+
76
100
  function _isFile(p: string): boolean {
77
101
  try {
78
102
  return fs.statSync(p).isFile();
@@ -126,6 +150,25 @@ function parse_args(argv: readonly string[]): Args {
126
150
  export function main(argv?: readonly string[]): number {
127
151
  const args = parse_args(argv ?? process.argv.slice(2));
128
152
 
153
+ // The unit is every file the walk visits, NOT the `_one_off_*.py` matches:
154
+ // zero matches is the normal clean state (the archive is empty today), so
155
+ // only the unfiltered walk can tell "nothing to move" from "nothing read".
156
+ try {
157
+ assertScanned({
158
+ gate: 'check_one_off_location',
159
+ scanned: _countFilesUnder(SCRIPTS),
160
+ units: 'file(s)',
161
+ roots: ['src/scripts'],
162
+ });
163
+ } catch (exc) {
164
+ if (exc instanceof DeadScopeError) {
165
+ // 1 = violation (3 is reserved for an internal error below).
166
+ process.stderr.write(`❌ ${exc.message}\n`);
167
+ return 1;
168
+ }
169
+ throw exc;
170
+ }
171
+
129
172
  let violations: string[];
130
173
  try {
131
174
  violations = find_violations();