@event4u/agent-config 9.19.0 → 9.21.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 (234) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +89 -0
  3. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  4. package/dist/cli-delegate/{chunk-XGA2NTU4.js → chunk-3EWYC3YK.js} +40 -0
  5. package/dist/cli-delegate/cmd_handoff.js +1 -1
  6. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  7. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  8. package/dist/discovery/deprecation-report.md +1 -1
  9. package/dist/discovery/discovery-manifest.json +1 -1
  10. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  11. package/dist/discovery/discovery-manifest.summary.md +1 -1
  12. package/dist/discovery/orphan-report.md +1 -1
  13. package/dist/discovery/packs.json +1 -1
  14. package/dist/discovery/trust-report.md +1 -1
  15. package/dist/discovery/workspaces.json +1 -1
  16. package/dist/hooks/dispatch.js +58 -2
  17. package/dist/mcp/registry-manifest.json +2 -2
  18. package/dist/mcp/server.json +2 -2
  19. package/dist/mcp/server.mjs +47 -29
  20. package/package.json +1 -1
  21. package/src/config/gate-coverage.yml +38 -4
  22. package/src/config/gate-violation-baselines.json +3 -8
  23. package/src/scripts/_lib/gate_population.ts +70 -0
  24. package/src/scripts/_lib/roadmap_granularity.ts +157 -0
  25. package/src/scripts/_lib/scan_scope.ts +44 -0
  26. package/src/scripts/_lib/stdin.ts +82 -0
  27. package/src/scripts/audit_adr_coverage.ts +38 -0
  28. package/src/scripts/audit_auto_rules.ts +17 -3
  29. package/src/scripts/audit_command_surface.ts +51 -0
  30. package/src/scripts/audit_initial_context.ts +39 -3
  31. package/src/scripts/audit_mcp_tools.ts +25 -0
  32. package/src/scripts/audit_overlap.ts +24 -1
  33. package/src/scripts/audit_skill_descriptions.ts +19 -0
  34. package/src/scripts/{verify_before_complete_hook.ts → before_complete_hook.ts} +16 -3
  35. package/src/scripts/check_adr_frontmatter.ts +23 -3
  36. package/src/scripts/check_always_budget.ts +24 -0
  37. package/src/scripts/check_artefact_checksums.ts +24 -1
  38. package/src/scripts/check_artefact_count_messaging.ts +20 -0
  39. package/src/scripts/check_augmentignore.ts +22 -15
  40. package/src/scripts/check_backstop_debt.ts +22 -0
  41. package/src/scripts/check_beta_review_markers.ts +22 -1
  42. package/src/scripts/check_bridge_derivation.ts +22 -1
  43. package/src/scripts/check_bundle_path_leakage.ts +26 -0
  44. package/src/scripts/check_ci_strict_superset.ts +21 -3
  45. package/src/scripts/check_claims.ts +22 -0
  46. package/src/scripts/check_cli_registry_budget_sync.ts +33 -4
  47. package/src/scripts/check_cluster_patterns.ts +26 -4
  48. package/src/scripts/check_command_count_messaging.ts +20 -4
  49. package/src/scripts/check_comparison.ts +18 -0
  50. package/src/scripts/check_completion_review.ts +19 -4
  51. package/src/scripts/check_condensed_paths.ts +23 -2
  52. package/src/scripts/check_consumer_scope_flip.ts +21 -2
  53. package/src/scripts/check_council_config_location.ts +20 -0
  54. package/src/scripts/check_council_layout.ts +22 -2
  55. package/src/scripts/check_council_references.ts +23 -1
  56. package/src/scripts/check_dependency_floors.ts +19 -3
  57. package/src/scripts/check_discovery_determinism.ts +26 -0
  58. package/src/scripts/check_enforcement_coverage.ts +20 -0
  59. package/src/scripts/check_evaluator_budgets.ts +23 -0
  60. package/src/scripts/check_finding_dispositions.ts +29 -0
  61. package/src/scripts/check_gate_coverage.ts +63 -22
  62. package/src/scripts/check_gate_paths.ts +30 -5
  63. package/src/scripts/check_generated_artefact_headers.ts +21 -0
  64. package/src/scripts/check_generator_output_coverage.ts +23 -0
  65. package/src/scripts/check_gitignore_freshness.ts +23 -0
  66. package/src/scripts/check_host_loadability.ts +34 -8
  67. package/src/scripts/check_installer_import_purity.ts +22 -8
  68. package/src/scripts/check_kernel_prefix_stability.ts +19 -1
  69. package/src/scripts/check_kernel_rule_bundle.ts +23 -0
  70. package/src/scripts/check_knowledge_cards.ts +13 -0
  71. package/src/scripts/check_knowledge_pages.ts +23 -0
  72. package/src/scripts/check_knowledge_sharing.ts +27 -0
  73. package/src/scripts/check_md_language.ts +24 -0
  74. package/src/scripts/check_media_deps.ts +23 -0
  75. package/src/scripts/check_memory.ts +26 -1
  76. package/src/scripts/check_memory_contradiction.ts +16 -1
  77. package/src/scripts/check_memory_proposal.ts +15 -0
  78. package/src/scripts/check_memory_similarity.ts +16 -0
  79. package/src/scripts/check_module_management_neutral.ts +19 -11
  80. package/src/scripts/check_no_conflict_markers.ts +22 -1
  81. package/src/scripts/check_no_external_sources.ts +26 -1
  82. package/src/scripts/check_no_local_settings_committed.ts +37 -10
  83. package/src/scripts/check_no_new_legacy_path.ts +30 -1
  84. package/src/scripts/check_one_off_location.ts +43 -0
  85. package/src/scripts/check_originality_freshness.ts +25 -1
  86. package/src/scripts/check_overlay_cascade_subdirs.ts +21 -0
  87. package/src/scripts/check_pack_size.ts +22 -0
  88. package/src/scripts/check_preamble_payload_budget.ts +35 -5
  89. package/src/scripts/check_proposal.ts +24 -1
  90. package/src/scripts/check_public_catalog_links.ts +27 -0
  91. package/src/scripts/check_public_links.ts +19 -0
  92. package/src/scripts/check_quality_regression.ts +19 -0
  93. package/src/scripts/check_reach_channels.ts +44 -0
  94. package/src/scripts/check_reach_staleness.ts +27 -0
  95. package/src/scripts/check_release_adjacent_health.ts +28 -0
  96. package/src/scripts/check_release_highlights.ts +34 -0
  97. package/src/scripts/check_release_includes_discovery.ts +25 -6
  98. package/src/scripts/check_release_pr_shape.ts +18 -1
  99. package/src/scripts/check_release_published.ts +21 -0
  100. package/src/scripts/check_release_surface_equality.ts +22 -0
  101. package/src/scripts/check_release_trunk_sync.ts +19 -0
  102. package/src/scripts/check_reply_consistency.ts +22 -0
  103. package/src/scripts/check_review_dispositions.ts +188 -0
  104. package/src/scripts/check_roadmap_trackable.ts +20 -0
  105. package/src/scripts/check_role_doc_links.ts +23 -0
  106. package/src/scripts/check_rule_invariants.ts +14 -0
  107. package/src/scripts/check_secret_leak.ts +47 -0
  108. package/src/scripts/check_skill_gaps.ts +32 -0
  109. package/src/scripts/check_skill_requires.ts +19 -0
  110. package/src/scripts/check_source_pointer_freshness.ts +20 -0
  111. package/src/scripts/check_structural_breaking.ts +29 -2
  112. package/src/scripts/check_surface_tiers.ts +34 -0
  113. package/src/scripts/check_template_pin_drift.ts +23 -1
  114. package/src/scripts/check_test_coverage_diff.ts +17 -1
  115. package/src/scripts/check_token_optimizer_freshness.ts +18 -3
  116. package/src/scripts/check_token_quality_golden.ts +21 -0
  117. package/src/scripts/check_token_regression.ts +25 -0
  118. package/src/scripts/check_tracked_but_ignored.ts +26 -0
  119. package/src/scripts/check_trigger_eval_presence.ts +23 -1
  120. package/src/scripts/check_trigger_evals.ts +36 -0
  121. package/src/scripts/check_trunk_drift.ts +23 -0
  122. package/src/scripts/check_update_banner.ts +24 -0
  123. package/src/scripts/dispatch_r2_reviewer.ts +3 -0
  124. package/src/scripts/gate_scope_classify.ts +513 -0
  125. package/src/scripts/hook_manifest.yaml +1 -1
  126. package/src/scripts/hooks/block_kernel_rule_writes.ts +1 -1
  127. package/src/scripts/hooks/concern_registry.ts +2 -2
  128. package/src/scripts/lint_agent_security.ts +19 -0
  129. package/src/scripts/lint_agent_skill_names.ts +30 -0
  130. package/src/scripts/lint_agents_layout.ts +20 -0
  131. package/src/scripts/lint_agents_md.ts +20 -0
  132. package/src/scripts/lint_archived_skills.ts +24 -3
  133. package/src/scripts/lint_behavioural_eval_freshness.ts +29 -3
  134. package/src/scripts/lint_bench_ab.ts +25 -0
  135. package/src/scripts/lint_bench_corpus.ts +20 -4
  136. package/src/scripts/lint_breaking_changes_index.ts +24 -0
  137. package/src/scripts/lint_budget_ownership.ts +20 -3
  138. package/src/scripts/lint_changelog_rollback.ts +24 -0
  139. package/src/scripts/lint_command_flow_coverage.ts +22 -0
  140. package/src/scripts/lint_command_tiers.ts +29 -5
  141. package/src/scripts/lint_commit_subjects.ts +15 -0
  142. package/src/scripts/lint_confusables.ts +24 -0
  143. package/src/scripts/lint_consumer_internal_refs.ts +24 -0
  144. package/src/scripts/lint_design_quality.ts +22 -0
  145. package/src/scripts/lint_design_slop.ts +23 -0
  146. package/src/scripts/lint_discovery_manifest.ts +23 -0
  147. package/src/scripts/lint_discovery_vocabulary.ts +19 -0
  148. package/src/scripts/lint_documented_commands.ts +23 -0
  149. package/src/scripts/lint_empty_roadmaps.ts +23 -0
  150. package/src/scripts/lint_eval_fixture_citations.ts +19 -0
  151. package/src/scripts/lint_eval_freshness.ts +35 -0
  152. package/src/scripts/lint_examples.ts +18 -3
  153. package/src/scripts/lint_explain_trace.ts +23 -0
  154. package/src/scripts/lint_featured_skills.ts +26 -1
  155. package/src/scripts/lint_flows.ts +19 -0
  156. package/src/scripts/lint_frontmatter_boilerplate.ts +20 -0
  157. package/src/scripts/lint_ghostwriter_source.ts +26 -6
  158. package/src/scripts/lint_glama_drift.ts +24 -0
  159. package/src/scripts/lint_global_paths.ts +24 -0
  160. package/src/scripts/lint_governed_writes.ts +20 -0
  161. package/src/scripts/lint_hidden_unicode.ts +25 -0
  162. package/src/scripts/lint_hook_concern_budget.ts +39 -6
  163. package/src/scripts/lint_hook_manifest.ts +22 -0
  164. package/src/scripts/lint_instruction_smuggling.ts +23 -0
  165. package/src/scripts/lint_knowledge_scale.ts +32 -2
  166. package/src/scripts/lint_legal_pack.ts +31 -0
  167. package/src/scripts/lint_marketplace.ts +26 -0
  168. package/src/scripts/lint_marketplace_install_completeness.ts +23 -0
  169. package/src/scripts/lint_mcp_config_security.ts +24 -0
  170. package/src/scripts/lint_mcp_registry_manifest.ts +22 -1
  171. package/src/scripts/lint_memory_tripwire.ts +35 -0
  172. package/src/scripts/lint_missions.ts +23 -7
  173. package/src/scripts/lint_model_tier_coverage.ts +18 -0
  174. package/src/scripts/lint_namespace_collisions.ts +19 -0
  175. package/src/scripts/lint_no_new_atomic_commands.ts +33 -0
  176. package/src/scripts/lint_no_python_twin_rationale.ts +18 -0
  177. package/src/scripts/lint_one_off_age.ts +38 -2
  178. package/src/scripts/lint_orchestration_dsl.ts +23 -3
  179. package/src/scripts/lint_orchestrator_auto_detect.ts +19 -12
  180. package/src/scripts/lint_originality.ts +22 -4
  181. package/src/scripts/lint_originality_shingles.ts +28 -3
  182. package/src/scripts/lint_output_slop.ts +19 -0
  183. package/src/scripts/lint_override_kernel_guard.ts +30 -0
  184. package/src/scripts/lint_pack_first_win.ts +23 -0
  185. package/src/scripts/lint_pack_risk_class.ts +19 -3
  186. package/src/scripts/lint_persistence.ts +19 -0
  187. package/src/scripts/lint_persona_governance.ts +18 -3
  188. package/src/scripts/lint_positioning.ts +23 -0
  189. package/src/scripts/lint_pre_migration_refs.ts +24 -0
  190. package/src/scripts/lint_profile_overlay_set_only.ts +24 -0
  191. package/src/scripts/lint_provenance.ts +21 -0
  192. package/src/scripts/lint_provenance_vocabulary.ts +24 -1
  193. package/src/scripts/lint_readme_jargon.ts +21 -3
  194. package/src/scripts/lint_readme_serial_comma.ts +21 -3
  195. package/src/scripts/lint_readme_size.ts +21 -3
  196. package/src/scripts/lint_regression.ts +28 -0
  197. package/src/scripts/lint_roadmap_blockers.ts +43 -0
  198. package/src/scripts/lint_roadmap_ci_steps.ts +45 -0
  199. package/src/scripts/lint_roadmap_complexity.ts +47 -0
  200. package/src/scripts/lint_roadmap_later_disposition.ts +23 -0
  201. package/src/scripts/lint_rule_interactions.ts +16 -2
  202. package/src/scripts/lint_rule_tiers.ts +19 -6
  203. package/src/scripts/lint_showcase_sessions.ts +19 -3
  204. package/src/scripts/lint_skill_descriptions.ts +19 -0
  205. package/src/scripts/lint_skill_frontmatter_safety.ts +24 -0
  206. package/src/scripts/lint_skill_originality.ts +18 -3
  207. package/src/scripts/lint_skill_scripts_readonly.ts +20 -0
  208. package/src/scripts/lint_skill_tools.ts +84 -145
  209. package/src/scripts/lint_spawn_payload.ts +27 -1
  210. package/src/scripts/lint_store_boundary.ts +19 -0
  211. package/src/scripts/lint_subagent_determinism.ts +23 -1
  212. package/src/scripts/lint_supported_tools_matrix.ts +18 -0
  213. package/src/scripts/lint_surface_matrix.ts +19 -3
  214. package/src/scripts/lint_ticket_buildable.ts +23 -0
  215. package/src/scripts/lint_topics_yaml.ts +19 -12
  216. package/src/scripts/lint_trigger_precision.ts +27 -0
  217. package/src/scripts/lint_trust_coherence.ts +21 -0
  218. package/src/scripts/lint_ui_stack_bundles.ts +17 -3
  219. package/src/scripts/lint_value_dashboard.ts +19 -3
  220. package/src/scripts/lint_versioned_cache.ts +19 -1
  221. package/src/scripts/lint_workflow_security.ts +22 -14
  222. package/src/scripts/pr_url_reminder_hook.ts +1 -1
  223. package/src/scripts/skill_collision_clusters.ts +37 -3
  224. package/src/scripts/skill_discovery.ts +20 -0
  225. package/src/scripts/skill_eval_coverage.ts +22 -0
  226. package/src/scripts/skill_overlap.ts +32 -6
  227. package/src/scripts/skill_preview.ts +31 -0
  228. package/src/scripts/skill_trigger_eval.ts +18 -0
  229. package/src/scripts/skill_usage_collect.ts +21 -0
  230. package/src/scripts/skill_usage_report.ts +21 -0
  231. package/src/scripts/sweep_dead_scan_roots.ts +11 -3
  232. package/src/scripts/verify_physical_move.ts +24 -0
  233. package/src/scripts/check_bite_sized_granularity.ts +0 -116
  234. package/src/scripts/lint_workspace_boundary.ts +0 -267
@@ -31,7 +31,9 @@
31
31
  * a few contract rows point at full working commands that merely happen to
32
32
  * carry one nested sub-command, and those own no dispatch table.
33
33
  *
34
- * Exit codes: 0 = clean, 1 = pattern violations, 3 = internal error.
34
+ * Exit codes: 0 = clean, 1 = pattern violations, 3 = internal error (which now
35
+ * includes a dead scan scope — an unparseable cluster table — reported through
36
+ * `_lib/scan_scope` so the message names the root).
35
37
  */
36
38
 
37
39
  import * as fs from 'node:fs';
@@ -39,6 +41,7 @@ import * as path from 'node:path';
39
41
  import { fileURLToPath, pathToFileURL } from 'node:url';
40
42
 
41
43
  import { SRC_DOMAINS, command_slug, resolve_logical } from './_lib/agent_src.js';
44
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
42
45
 
43
46
  const _HERE = fileURLToPath(import.meta.url);
44
47
  // src/scripts/check_cluster_patterns.ts → two dirs up is the repo root.
@@ -369,10 +372,29 @@ function _relativeToRootOrAbs(p: string): string {
369
372
  }
370
373
 
371
374
  export function main(): number {
375
+ // The locked-clusters table IS the corpus — every row drives one dispatcher
376
+ // read. A renamed contract file or a renamed `## Locked clusters` heading
377
+ // parses zero rows, which used to exit 3 (internal error, advisory at call
378
+ // sites) rather than fail: a blind gate, not a broken one.
372
379
  const rows = load_cluster_table();
373
- if (rows.length === 0) {
374
- process.stderr.write(`❌ No clusters parsed from ${_relativeToRootOrAbs(CONTRACT)}\n`);
375
- return 3;
380
+ try {
381
+ assertScanned({
382
+ gate: 'check_cluster_patterns',
383
+ scanned: rows.length,
384
+ units: 'locked-cluster row(s)',
385
+ roots: [_relativeToRootOrAbs(CONTRACT)],
386
+ });
387
+ } catch (exc) {
388
+ if (exc instanceof DeadScopeError) {
389
+ // Exit 3, not 1: this gate's CLI contract is pinned by its py2ts
390
+ // port, and an empty table was ALREADY loud here (3 is non-zero, and
391
+ // the Taskfile fails on any non-zero). Hardening means routing the
392
+ // check through the shared assertion so the message names the root —
393
+ // not renumbering an exit code that was never a silent green.
394
+ process.stderr.write(`❌ ${exc.message}\n`);
395
+ return 3;
396
+ }
397
+ throw exc;
376
398
  }
377
399
 
378
400
  const slug_map = build_slug_map();
@@ -8,7 +8,8 @@
8
8
  * 1 drift, 1 no-commands-dir via stderr), stdout/stderr split,
9
9
  * byte-identical messages, same canonical-count derivation
10
10
  * (iter_commands union), same per-file pattern checks in the same order.
11
- * No behaviour changes.
11
+ * No behaviour changes — except that the no-commands-dir stderr line now comes
12
+ * from `_lib/scan_scope` and names the empty roots; the exit code is unchanged.
12
13
  *
13
14
  * Sources canonical counts from the command frontmatter and fails when any
14
15
  * documented number drifts (README hero badge, getting-started browse line;
@@ -20,6 +21,7 @@ import * as path from 'node:path';
20
21
  import { fileURLToPath, pathToFileURL } from 'node:url';
21
22
 
22
23
  import { iter_commands } from './_lib/agent_src.js';
24
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
23
25
 
24
26
  const _HERE = fileURLToPath(import.meta.url);
25
27
  const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
@@ -62,9 +64,23 @@ function _command_files(): string[] {
62
64
 
63
65
  function canonical_counts(): [number, number, number] {
64
66
  const files = _command_files();
65
- if (files.length === 0) {
66
- process.stderr.write('❌ no commands/ directory found under any artefact root\n');
67
- process.exit(1);
67
+ try {
68
+ assertScanned({
69
+ gate: 'check_command_count_messaging',
70
+ scanned: files.length,
71
+ units: 'command file(s)',
72
+ roots: ['src/domains/*/**/command.md', 'src/agent-src/commands'],
73
+ });
74
+ } catch (exc) {
75
+ if (exc instanceof DeadScopeError) {
76
+ // Exit 1 stays — the documented "no commands dir" code, and the
77
+ // Taskfile fails on any non-zero, so this was never a silent green.
78
+ // The assertion replaces a bare "no commands/ directory found" with
79
+ // a message that names the roots that came back empty.
80
+ process.stderr.write(`❌ ${exc.message}\n`);
81
+ process.exit(1);
82
+ }
83
+ throw exc;
68
84
  }
69
85
  let total = 0;
70
86
  let shims = 0;
@@ -20,6 +20,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
20
20
  import { parse as parseYaml } from 'yaml';
21
21
 
22
22
  import { REPO, pointer_unresolved } from './check_claims.js';
23
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
23
24
 
24
25
  const _HERE = fileURLToPath(import.meta.url);
25
26
  export const DATA_REL = 'docs/comparison.yaml';
@@ -71,6 +72,23 @@ export function findErrors(rows: Row[]): string[] {
71
72
  export function main(argv: string[] = process.argv.slice(2)): number {
72
73
  const quiet = argv.includes('--quiet');
73
74
  const rows = loadRows();
75
+ // `loadRows` returns [] for a missing or rows-less `docs/comparison.yaml`,
76
+ // so a renamed data file made this gate certify "all our_evidence pointers
77
+ // resolve" over zero rows — the falsifiability lock passing vacuously.
78
+ try {
79
+ assertScanned({
80
+ gate: 'check_comparison',
81
+ scanned: rows.length,
82
+ units: 'comparison row(s)',
83
+ roots: [DATA_REL],
84
+ });
85
+ } catch (exc) {
86
+ if (exc instanceof DeadScopeError) {
87
+ process.stderr.write(`❌ ${exc.message}\n`);
88
+ return 1;
89
+ }
90
+ throw exc;
91
+ }
74
92
  const errs = findErrors(rows);
75
93
  if (errs.length > 0) {
76
94
  process.stdout.write('❌ comparison-honesty:\n');
@@ -495,10 +495,18 @@ export function parseArtifact(text: string): ParsedArtifact {
495
495
  // LABELLED closed fence is illustrative content. A stray fence (bare, or
496
496
  // an unclosed opener) skips nothing (see scanFences) — the lines around
497
497
  // it are parsed, and the artefact is reported.
498
- if (FENCE_RE.test(raw) || fences.fenced.has(i)) {
498
+ const trimmed = raw.trim();
499
+ // §2.2 row-liveness rule: fences govern the marker / honest-null / skip
500
+ // lines, which a fenced illustration may legitimately quote — but NOT
501
+ // findings rows. Hiding a marker or an honest-null fails CLOSED (the
502
+ // artefact reads as absent and the gate blocks); hiding a row fails OPEN
503
+ // (the finding silently ceases to exist). So the one line type whose
504
+ // concealment is dangerous is the one fences may not conceal, and a row
505
+ // is live wherever it appears. Only its own Status cell can excuse it.
506
+ const isCandidateRow = trimmed.startsWith('|');
507
+ if (FENCE_RE.test(raw) || (fences.fenced.has(i) && !isCandidateRow)) {
499
508
  continue;
500
509
  }
501
- const trimmed = raw.trim();
502
510
  // Marker candidates must LOOK like the §2.1 HTML comment: prose that
503
511
  // merely mentions the grammar (`the \`completion-review:\` marker …`,
504
512
  // or a reviewer note quoting it in backticks — real artefacts in this
@@ -544,6 +552,12 @@ export function parseArtifact(text: string): ParsedArtifact {
544
552
  if (first === '#' || second === 'severity') {
545
553
  continue; // header row
546
554
  }
555
+ // §2.2: `example` is the ONE illustrative marker. It is read from the
556
+ // Status cell of a six-cell row only — a short row cannot excuse
557
+ // itself, or `malformed-row` would become opt-out.
558
+ if (cells.length === 6 && (cells[4] ?? '').trim().toLowerCase() === 'example') {
559
+ continue;
560
+ }
547
561
  // EXACTLY six, per contract §2.2 — not `>= 6`. An over-long row is
548
562
  // as malformed as a short one and the surplus is usually the tell:
549
563
  // an unescaped `|` inside a cell (the shape §2.2 requires escaping)
@@ -797,8 +811,9 @@ function evaluate(input: EvalInput): Violation[] {
797
811
  detail:
798
812
  `${at}: stray \`\`\` fence — a bare fence delimits no region, and a labelled fence must ` +
799
813
  'be closed. An artefact whose ``` fences do not resolve is malformed (contract §2.2). ' +
800
- 'Label the opener (```markdown) and close it, or inline-escape an illustrative ```; ' +
801
- 'the lines around a stray fence are still parsed, so any finding row there is validated too.',
814
+ 'Close the fence, or inline-escape an illustrative ```. NOTE: fences never hide a ' +
815
+ 'findings row — a row is live wherever it appears, and the ONLY way to mark one ' +
816
+ 'illustrative is the status `example` in its own Status cell.',
802
817
  });
803
818
  }
804
819
  }
@@ -47,6 +47,8 @@ import process from 'node:process';
47
47
  import { fileURLToPath, pathToFileURL } from 'node:url';
48
48
  import type * as YamlModule from 'yaml';
49
49
 
50
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
51
+
50
52
  // ESM-standard `require` shim. The bare `require` global is present when this
51
53
  // module is *imported* (tsx injects it) but absent when the module is the
52
54
  // directly-executed CLI entry, so resolve it explicitly here to preserve the
@@ -359,7 +361,26 @@ function main(): number {
359
361
  }
360
362
  const viols: Violation[] = [];
361
363
  const audited: Array<[string, IgnoreEntry]> = [];
362
- for (const ruleFile of _globMdSorted(RULES_DIR)) {
364
+ const ruleFiles = _globMdSorted(RULES_DIR);
365
+ // The guard above only proves the directory node exists; an empty one still
366
+ // renders "clean (0 rules, 0 ignore(s) audited)". Exit 3 is the code that
367
+ // guard already uses for "there was nothing to check" — 1 means violations
368
+ // were found, which is a different claim.
369
+ try {
370
+ assertScanned({
371
+ gate: 'check_condensed_paths',
372
+ scanned: ruleFiles.length,
373
+ units: 'rule file(s)',
374
+ roots: ['dist/agent-src/rules'],
375
+ });
376
+ } catch (exc) {
377
+ if (exc instanceof DeadScopeError) {
378
+ process.stderr.write(`❌ ${exc.message}\n`);
379
+ return 3;
380
+ }
381
+ throw exc;
382
+ }
383
+ for (const ruleFile of ruleFiles) {
363
384
  const text = fs.readFileSync(ruleFile, 'utf-8');
364
385
  const [fm, body] = _split_frontmatter(text);
365
386
  const ignores: IgnoreEntry[] = fm !== null ? _parse_ignores(fm) : [];
@@ -383,7 +404,7 @@ function main(): number {
383
404
  process.stdout.write(`\n${viols.length} violation(s) in dist/agent-src/rules/\n`);
384
405
  return 1;
385
406
  }
386
- const ruleCount = _globMdSorted(RULES_DIR).length;
407
+ const ruleCount = ruleFiles.length;
387
408
  if (!QUIET) {
388
409
  process.stdout.write(
389
410
  `✅ condensed-path check clean (${ruleCount} rules, ${audited.length} ignore(s) audited)\n`,
@@ -32,6 +32,7 @@ import * as path from 'node:path';
32
32
  import { parse as parseYaml } from 'yaml';
33
33
 
34
34
  import { excludedRuleBasenames, ruleScopeFromSettings } from '../install/rule_scope.js';
35
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
35
36
 
36
37
  export const RULES_DIR = 'dist/agent-src/rules';
37
38
  export const TEMPLATE = 'src/config/agent-settings.template.yml';
@@ -112,8 +113,26 @@ async function main(): Promise<number> {
112
113
  let legacyTok = 0;
113
114
  let scopedRules = 0;
114
115
  let scopedTok = 0;
115
- for (const name of fs.readdirSync(RULES_DIR).sort()) {
116
- if (!name.endsWith('.md')) continue;
116
+ // The shipped rule tree is the corpus the before/after evidence is measured
117
+ // over. An emptied `dist/agent-src/rules` keeps every verdict vacuously true
118
+ // — nothing excluded, nothing dropped — so `pass` stays true and the flip
119
+ // reports as "verified" against a zero-rule baseline (with a NaN delta).
120
+ const ruleFiles = fs.readdirSync(RULES_DIR).sort().filter((n) => n.endsWith('.md'));
121
+ try {
122
+ assertScanned({
123
+ gate: 'check_consumer_scope_flip',
124
+ scanned: ruleFiles.length,
125
+ units: 'projected rule file(s)',
126
+ roots: [RULES_DIR],
127
+ });
128
+ } catch (exc) {
129
+ if (exc instanceof DeadScopeError) {
130
+ process.stderr.write(`❌ ${exc.message}\n`);
131
+ return 1;
132
+ }
133
+ throw exc;
134
+ }
135
+ for (const name of ruleFiles) {
117
136
  const body = fs.readFileSync(path.join(RULES_DIR, name), 'utf-8');
118
137
  const tok = gpt_tokens(body).tokens;
119
138
  // legacy-all baseline mirrors the 2026-07-08 report: compat exclusion applies on both arms
@@ -54,6 +54,8 @@ import * as fs from 'node:fs';
54
54
  import * as path from 'node:path';
55
55
  import { fileURLToPath, pathToFileURL } from 'node:url';
56
56
 
57
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
58
+
57
59
  const QUIET = process.argv.includes('--quiet');
58
60
 
59
61
  // Agent-facing surfaces where council config must resolve to `.ai-council.yml`.
@@ -318,6 +320,24 @@ function find_path_violations(root: string): string[] {
318
320
 
319
321
  function main(): number {
320
322
  const root = process.cwd();
323
+ // PATH_CHECK_GLOBS is the superset of both passes, so its resolution is the
324
+ // whole corpus either check can see. Run from the wrong cwd — or after the
325
+ // council surfaces move — every glob resolves to nothing and the gate
326
+ // reports "clean". Exit 1 is its only failure code.
327
+ try {
328
+ assertScanned({
329
+ gate: 'check_council_config_location',
330
+ scanned: [...iter_files(root, PATH_CHECK_GLOBS)].length,
331
+ units: 'council-surface file(s)',
332
+ roots: PATH_CHECK_GLOBS,
333
+ });
334
+ } catch (exc) {
335
+ if (exc instanceof DeadScopeError) {
336
+ process.stderr.write(`❌ ${exc.message}\n`);
337
+ return 1;
338
+ }
339
+ throw exc;
340
+ }
321
341
  const findings = [...find_violations(root), ...find_path_violations(root)];
322
342
  if (findings.length) {
323
343
  process.stdout.write('❌ Council config-location violations:\n\n');
@@ -19,6 +19,8 @@ import * as fs from 'node:fs';
19
19
  import * as path from 'node:path';
20
20
  import { fileURLToPath, pathToFileURL } from 'node:url';
21
21
 
22
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
23
+
22
24
  const _HERE = fileURLToPath(import.meta.url);
23
25
 
24
26
  const AGENTS_ROOT = 'agents';
@@ -88,6 +90,16 @@ function _isFile(p: string): boolean {
88
90
 
89
91
  function find_violations(root: string): string[] {
90
92
  const findings: string[] = [];
93
+ // `agents/` is resolved relative to the cwd, so a run from anywhere but the
94
+ // repo root walks nothing and reports a clean layout. Thrown, not returned —
95
+ // `findings` is the violation channel, and a dead scope is not a violation.
96
+ const descendants = _isDir(root) ? _rglobAllSorted(root) : [];
97
+ assertScanned({
98
+ gate: 'check_council_layout',
99
+ scanned: descendants.length,
100
+ units: 'agents/ entries',
101
+ roots: [AGENTS_ROOT],
102
+ });
91
103
  if (!_isDir(root)) {
92
104
  return findings;
93
105
  }
@@ -110,7 +122,7 @@ function find_violations(root: string): string[] {
110
122
  }
111
123
 
112
124
  // 2. Council artefacts in non-canonical subdirectories.
113
- for (const p of _rglobAllSorted(root)) {
125
+ for (const p of descendants) {
114
126
  if (!_isFile(p) || !is_council_artefact(path.basename(p))) {
115
127
  continue;
116
128
  }
@@ -144,7 +156,15 @@ function _posix(p: string): string {
144
156
 
145
157
  function main(argv: readonly string[]): number {
146
158
  const quiet = argv.includes('--quiet');
147
- const findings = find_violations(AGENTS_ROOT);
159
+ let findings: string[];
160
+ try {
161
+ findings = find_violations(AGENTS_ROOT);
162
+ } catch (exc) {
163
+ if (!(exc instanceof DeadScopeError)) throw exc;
164
+ // Exit 1 is this gate's only failure code.
165
+ process.stderr.write(`❌ ${exc.message}\n`);
166
+ return 1;
167
+ }
148
168
  if (findings.length > 0) {
149
169
  process.stdout.write('❌ Council layout violations:\n\n');
150
170
  for (const f of findings) {
@@ -40,6 +40,7 @@ import * as path from 'node:path';
40
40
  import { fileURLToPath, pathToFileURL } from 'node:url';
41
41
 
42
42
  import { artefact_roots, strip_source_prefix } from './_lib/agent_src.js';
43
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
43
44
 
44
45
  const QUIET = process.argv.includes('--quiet');
45
46
 
@@ -284,7 +285,14 @@ function* _iter_files(roots: readonly string[]): Generator<string> {
284
285
 
285
286
  function main(): number {
286
287
  const violations: Array<[string, number, string]> = [];
287
- for (const p of _iter_files(_scan_roots())) {
288
+ const roots = _scan_roots();
289
+ // Counted before the allowlist filter — the question is what the walk
290
+ // reached, not what survived it. Every scan root is skipped when absent, so
291
+ // a relocated `docs/` or `agents/` leaves the linter reporting "no forbidden
292
+ // references" over an empty corpus. Exit 1 is its only failure code.
293
+ let scanned = 0;
294
+ for (const p of _iter_files(roots)) {
295
+ scanned += 1;
288
296
  const rel = p.split(path.sep).join('/');
289
297
  if (_is_allowlisted(rel)) {
290
298
  continue;
@@ -293,6 +301,20 @@ function main(): number {
293
301
  violations.push([p, ln, ref]);
294
302
  }
295
303
  }
304
+ try {
305
+ assertScanned({
306
+ gate: 'check_council_references',
307
+ scanned,
308
+ units: 'durable artefact file(s)',
309
+ roots,
310
+ });
311
+ } catch (exc) {
312
+ if (exc instanceof DeadScopeError) {
313
+ process.stdout.write(`❌ ${exc.message}\n`);
314
+ return 1;
315
+ }
316
+ throw exc;
317
+ }
296
318
 
297
319
  if (violations.length === 0) {
298
320
  if (!QUIET) {
@@ -37,6 +37,8 @@ import * as fs from 'node:fs';
37
37
  import * as path from 'node:path';
38
38
  import { fileURLToPath } from 'node:url';
39
39
 
40
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
41
+
40
42
  const REPO_ROOT = path.resolve(fileURLToPath(import.meta.url), '..', '..', '..');
41
43
 
42
44
  /**
@@ -137,9 +139,23 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
137
139
  process.stderr.write(`❌ dependency floors: cannot read ${manifestPath}: ${String(err)}\n`);
138
140
  return 2;
139
141
  }
140
- if (Object.keys(dependencies).length === 0) {
141
- process.stderr.write('❌ dependency floors: no runtime dependencies found — gate would pass vacuously\n');
142
- return 2;
142
+ // Replaces the ad-hoc "gate would pass vacuously" guard with the shared
143
+ // assertion — same condition, same exit code (2 = the manifest yielded no
144
+ // corpus, vs 1 = a floor violates the rule), now counted on the unfiltered
145
+ // read rather than on what `evaluate` judges.
146
+ try {
147
+ assertScanned({
148
+ gate: 'check_dependency_floors',
149
+ scanned: Object.keys(dependencies).length,
150
+ units: 'runtime dependency floor(s)',
151
+ roots: ['package.json#dependencies'],
152
+ });
153
+ } catch (err) {
154
+ if (err instanceof DeadScopeError) {
155
+ process.stderr.write(`❌ ${err.message}\n`);
156
+ return 2;
157
+ }
158
+ throw err;
143
159
  }
144
160
  const errors = evaluate(dependencies);
145
161
  if (asJson) {
@@ -19,6 +19,8 @@ import * as path from 'node:path';
19
19
  import * as fs from 'node:fs';
20
20
  import { fileURLToPath, pathToFileURL } from 'node:url';
21
21
 
22
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
23
+
22
24
  const _HERE = fileURLToPath(import.meta.url);
23
25
  const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
24
26
  const SCANNER = path.join(ROOT, 'src', 'scripts', 'build_discovery_manifest.ts');
@@ -76,6 +78,30 @@ class ExitError extends Error {}
76
78
  function main(): number {
77
79
  const a = _normalise(_run());
78
80
  const b = _normalise(_run());
81
+
82
+ // Determinism over an EMPTY manifest is vacuously true: if the scanner's
83
+ // artefact roots moved, both runs emit zero artefacts, the two payloads
84
+ // compare equal, and this gate reports "OK: deterministic" having compared
85
+ // nothing. Assert the corpus the comparison ran over, not the comparison.
86
+ // Deletion test: wiping the artefact tree would give 0 — which is blindness
87
+ // here, never success — so no `allowEmpty`. Exit 1 is this pinned CLI's
88
+ // only failure code (the scanner-failed path also exits 1, via ExitError).
89
+ const artefacts = a['artefacts'];
90
+ try {
91
+ assertScanned({
92
+ gate: 'check_discovery_determinism',
93
+ scanned: Array.isArray(artefacts) ? artefacts.length : 0,
94
+ units: 'manifest artefact(s)',
95
+ roots: ['src/scripts/build_discovery_manifest.ts → manifest.artefacts'],
96
+ });
97
+ } catch (exc) {
98
+ if (exc instanceof DeadScopeError) {
99
+ process.stderr.write(`${exc.message}\n`);
100
+ return 1;
101
+ }
102
+ throw exc;
103
+ }
104
+
79
105
  const sa = _dumps(a);
80
106
  const sb = _dumps(b);
81
107
  if (sa !== sb) {
@@ -37,6 +37,8 @@ import * as fs from 'node:fs';
37
37
  import * as path from 'node:path';
38
38
  import { fileURLToPath, pathToFileURL } from 'node:url';
39
39
 
40
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
41
+
40
42
  const _HERE = path.dirname(fileURLToPath(import.meta.url));
41
43
  const REPO_ROOT = path.resolve(_HERE, '..', '..');
42
44
  const RULES_DIR = path.join(REPO_ROOT, 'src', 'rules');
@@ -478,6 +480,24 @@ function main(argv: string[]): number {
478
480
  }
479
481
 
480
482
  const rows = collect();
483
+ // Count every rule read, not the declared subset: with an empty rules dir
484
+ // the report renders "0/0 rules (0%)" and exits green, and `summarise`
485
+ // special-cases the zero denominator rather than rejecting it. Exit 2 is
486
+ // the code the missing-rules-dir guard above already uses for this class.
487
+ try {
488
+ assertScanned({
489
+ gate: 'check_enforcement_coverage',
490
+ scanned: rows.length,
491
+ units: 'rule file(s)',
492
+ roots: ['src/rules'],
493
+ });
494
+ } catch (exc) {
495
+ if (exc instanceof DeadScopeError) {
496
+ process.stderr.write(`❌ ${exc.message}\n`);
497
+ return 2;
498
+ }
499
+ throw exc;
500
+ }
481
501
  const summary = summarise(rows);
482
502
 
483
503
  if (as_json) {
@@ -40,6 +40,8 @@ import * as fs from 'node:fs';
40
40
  import * as path from 'node:path';
41
41
  import { fileURLToPath } from 'node:url';
42
42
 
43
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
44
+
43
45
  const REPO_ROOT = path.resolve(fileURLToPath(import.meta.url), '..', '..', '..');
44
46
  const BUDGETS_PATH = path.join(REPO_ROOT, 'src', 'config', 'evaluator-budgets.json');
45
47
 
@@ -230,6 +232,27 @@ export function main(argv: string[] = process.argv.slice(2)): number {
230
232
  return 2;
231
233
  }
232
234
 
235
+ // The budget document IS the corpus: every finding below is produced by
236
+ // iterating it, so a budgets file that parses to no entries reports
237
+ // "✅ evaluator budgets met (0 metrics)" — green over nothing, the exact
238
+ // shape this harness exists to end. A missing `budgets` key counts as 0 for
239
+ // the same reason. Exit 2 (documented internal error — the gate could not
240
+ // run), never 1, which means a metric actually breached.
241
+ try {
242
+ assertScanned({
243
+ gate: 'check_evaluator_budgets',
244
+ scanned: Object.keys(budgetsDoc.budgets ?? {}).length,
245
+ units: 'budgeted metric(s)',
246
+ roots: ['src/config/evaluator-budgets.json (budgets)'],
247
+ });
248
+ } catch (e) {
249
+ if (e instanceof DeadScopeError) {
250
+ process.stderr.write(`❌ ${e.message}\n`);
251
+ return 2;
252
+ }
253
+ throw e;
254
+ }
255
+
233
256
  // The committed record is optional: a fresh checkout before the first
234
257
  // nightly has none, and a missing record must not fake a green drift check.
235
258
  const recordPath = argValue(argv, '--recorded') ?? path.join(REPO_ROOT, RECORD_REL);
@@ -34,6 +34,8 @@ import * as path from 'node:path';
34
34
  import process from 'node:process';
35
35
  import { fileURLToPath } from 'node:url';
36
36
 
37
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
38
+
37
39
  const _HERE = fileURLToPath(import.meta.url);
38
40
  const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
39
41
 
@@ -235,6 +237,33 @@ function main(argv: readonly string[]): number {
235
237
  }
236
238
  }
237
239
 
240
+ // Scope declaration, not a scope guard. The ledger is written per release by
241
+ // `--ingest`, so "no <release>.json" is the normal state of a release whose
242
+ // self-review reported nothing — the gate's own success line says so. That
243
+ // makes an absent file indistinguishable from a moved ledger dir here, and
244
+ // the honest reading is that the corpus count cannot carry that signal: the
245
+ // durable trigger for an un-ingested finding is `--pr` mode, which compares
246
+ // the ledger against what the self-review actually reported.
247
+ try {
248
+ assertScanned({
249
+ gate: 'check_finding_dispositions',
250
+ scanned: ledger.findings.length,
251
+ units: 'recorded finding(s)',
252
+ roots: [path.relative(REPO_ROOT, ledgerPath)],
253
+ allowEmpty:
254
+ 'EMPTY_VALID: zero recorded findings IS the pass state for a release whose '
255
+ + 'self-review reported none — the ledger file is created on first --ingest, so a '
256
+ + 'clean release legitimately has no <version>.json. Un-ingested findings are '
257
+ + 'caught by --pr (unrecorded_findings), never by this count.',
258
+ });
259
+ } catch (exc) {
260
+ if (exc instanceof DeadScopeError) {
261
+ process.stderr.write(`❌ ${exc.message}\n`);
262
+ return 1;
263
+ }
264
+ throw exc;
265
+ }
266
+
238
267
  const problems = missing_dispositions(ledger.findings);
239
268
  if (pr) {
240
269
  const reported = parse_comment_findings(_gh_comment_bodies(pr));