@event4u/agent-config 9.20.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 (231) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +51 -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 +22 -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_condensed_paths.ts +23 -2
  51. package/src/scripts/check_consumer_scope_flip.ts +21 -2
  52. package/src/scripts/check_council_config_location.ts +20 -0
  53. package/src/scripts/check_council_layout.ts +22 -2
  54. package/src/scripts/check_council_references.ts +23 -1
  55. package/src/scripts/check_dependency_floors.ts +19 -3
  56. package/src/scripts/check_discovery_determinism.ts +26 -0
  57. package/src/scripts/check_enforcement_coverage.ts +20 -0
  58. package/src/scripts/check_evaluator_budgets.ts +23 -0
  59. package/src/scripts/check_finding_dispositions.ts +29 -0
  60. package/src/scripts/check_gate_coverage.ts +63 -22
  61. package/src/scripts/check_gate_paths.ts +30 -5
  62. package/src/scripts/check_generated_artefact_headers.ts +21 -0
  63. package/src/scripts/check_generator_output_coverage.ts +23 -0
  64. package/src/scripts/check_gitignore_freshness.ts +23 -0
  65. package/src/scripts/check_host_loadability.ts +34 -8
  66. package/src/scripts/check_installer_import_purity.ts +22 -8
  67. package/src/scripts/check_kernel_prefix_stability.ts +19 -1
  68. package/src/scripts/check_kernel_rule_bundle.ts +23 -0
  69. package/src/scripts/check_knowledge_cards.ts +13 -0
  70. package/src/scripts/check_knowledge_pages.ts +23 -0
  71. package/src/scripts/check_knowledge_sharing.ts +27 -0
  72. package/src/scripts/check_md_language.ts +24 -0
  73. package/src/scripts/check_media_deps.ts +23 -0
  74. package/src/scripts/check_memory.ts +26 -1
  75. package/src/scripts/check_memory_contradiction.ts +16 -1
  76. package/src/scripts/check_memory_proposal.ts +15 -0
  77. package/src/scripts/check_memory_similarity.ts +16 -0
  78. package/src/scripts/check_module_management_neutral.ts +19 -11
  79. package/src/scripts/check_no_conflict_markers.ts +22 -1
  80. package/src/scripts/check_no_external_sources.ts +26 -1
  81. package/src/scripts/check_no_local_settings_committed.ts +37 -10
  82. package/src/scripts/check_no_new_legacy_path.ts +30 -1
  83. package/src/scripts/check_one_off_location.ts +43 -0
  84. package/src/scripts/check_originality_freshness.ts +25 -1
  85. package/src/scripts/check_overlay_cascade_subdirs.ts +21 -0
  86. package/src/scripts/check_pack_size.ts +22 -0
  87. package/src/scripts/check_preamble_payload_budget.ts +35 -5
  88. package/src/scripts/check_proposal.ts +24 -1
  89. package/src/scripts/check_public_catalog_links.ts +27 -0
  90. package/src/scripts/check_public_links.ts +19 -0
  91. package/src/scripts/check_quality_regression.ts +19 -0
  92. package/src/scripts/check_reach_channels.ts +44 -0
  93. package/src/scripts/check_reach_staleness.ts +27 -0
  94. package/src/scripts/check_release_adjacent_health.ts +28 -0
  95. package/src/scripts/check_release_highlights.ts +34 -0
  96. package/src/scripts/check_release_includes_discovery.ts +25 -6
  97. package/src/scripts/check_release_pr_shape.ts +18 -1
  98. package/src/scripts/check_release_published.ts +21 -0
  99. package/src/scripts/check_release_surface_equality.ts +22 -0
  100. package/src/scripts/check_release_trunk_sync.ts +19 -0
  101. package/src/scripts/check_reply_consistency.ts +22 -0
  102. package/src/scripts/check_roadmap_trackable.ts +20 -0
  103. package/src/scripts/check_role_doc_links.ts +23 -0
  104. package/src/scripts/check_rule_invariants.ts +14 -0
  105. package/src/scripts/check_secret_leak.ts +47 -0
  106. package/src/scripts/check_skill_gaps.ts +32 -0
  107. package/src/scripts/check_skill_requires.ts +19 -0
  108. package/src/scripts/check_source_pointer_freshness.ts +20 -0
  109. package/src/scripts/check_structural_breaking.ts +29 -2
  110. package/src/scripts/check_surface_tiers.ts +34 -0
  111. package/src/scripts/check_template_pin_drift.ts +23 -1
  112. package/src/scripts/check_test_coverage_diff.ts +17 -1
  113. package/src/scripts/check_token_optimizer_freshness.ts +18 -3
  114. package/src/scripts/check_token_quality_golden.ts +21 -0
  115. package/src/scripts/check_token_regression.ts +25 -0
  116. package/src/scripts/check_tracked_but_ignored.ts +26 -0
  117. package/src/scripts/check_trigger_eval_presence.ts +23 -1
  118. package/src/scripts/check_trigger_evals.ts +36 -0
  119. package/src/scripts/check_trunk_drift.ts +23 -0
  120. package/src/scripts/check_update_banner.ts +24 -0
  121. package/src/scripts/gate_scope_classify.ts +513 -0
  122. package/src/scripts/hook_manifest.yaml +1 -1
  123. package/src/scripts/hooks/block_kernel_rule_writes.ts +1 -1
  124. package/src/scripts/hooks/concern_registry.ts +2 -2
  125. package/src/scripts/lint_agent_security.ts +19 -0
  126. package/src/scripts/lint_agent_skill_names.ts +30 -0
  127. package/src/scripts/lint_agents_layout.ts +20 -0
  128. package/src/scripts/lint_agents_md.ts +20 -0
  129. package/src/scripts/lint_archived_skills.ts +24 -3
  130. package/src/scripts/lint_behavioural_eval_freshness.ts +29 -3
  131. package/src/scripts/lint_bench_ab.ts +25 -0
  132. package/src/scripts/lint_bench_corpus.ts +20 -4
  133. package/src/scripts/lint_breaking_changes_index.ts +24 -0
  134. package/src/scripts/lint_budget_ownership.ts +20 -3
  135. package/src/scripts/lint_changelog_rollback.ts +24 -0
  136. package/src/scripts/lint_command_flow_coverage.ts +22 -0
  137. package/src/scripts/lint_command_tiers.ts +29 -5
  138. package/src/scripts/lint_commit_subjects.ts +15 -0
  139. package/src/scripts/lint_confusables.ts +24 -0
  140. package/src/scripts/lint_consumer_internal_refs.ts +24 -0
  141. package/src/scripts/lint_design_quality.ts +22 -0
  142. package/src/scripts/lint_design_slop.ts +23 -0
  143. package/src/scripts/lint_discovery_manifest.ts +23 -0
  144. package/src/scripts/lint_discovery_vocabulary.ts +19 -0
  145. package/src/scripts/lint_documented_commands.ts +23 -0
  146. package/src/scripts/lint_empty_roadmaps.ts +23 -0
  147. package/src/scripts/lint_eval_fixture_citations.ts +19 -0
  148. package/src/scripts/lint_eval_freshness.ts +35 -0
  149. package/src/scripts/lint_examples.ts +18 -3
  150. package/src/scripts/lint_explain_trace.ts +23 -0
  151. package/src/scripts/lint_featured_skills.ts +26 -1
  152. package/src/scripts/lint_flows.ts +19 -0
  153. package/src/scripts/lint_frontmatter_boilerplate.ts +20 -0
  154. package/src/scripts/lint_ghostwriter_source.ts +26 -6
  155. package/src/scripts/lint_glama_drift.ts +24 -0
  156. package/src/scripts/lint_global_paths.ts +24 -0
  157. package/src/scripts/lint_governed_writes.ts +20 -0
  158. package/src/scripts/lint_hidden_unicode.ts +25 -0
  159. package/src/scripts/lint_hook_concern_budget.ts +39 -6
  160. package/src/scripts/lint_hook_manifest.ts +22 -0
  161. package/src/scripts/lint_instruction_smuggling.ts +23 -0
  162. package/src/scripts/lint_knowledge_scale.ts +32 -2
  163. package/src/scripts/lint_legal_pack.ts +31 -0
  164. package/src/scripts/lint_marketplace.ts +26 -0
  165. package/src/scripts/lint_marketplace_install_completeness.ts +23 -0
  166. package/src/scripts/lint_mcp_config_security.ts +24 -0
  167. package/src/scripts/lint_mcp_registry_manifest.ts +22 -1
  168. package/src/scripts/lint_memory_tripwire.ts +35 -0
  169. package/src/scripts/lint_missions.ts +23 -7
  170. package/src/scripts/lint_model_tier_coverage.ts +18 -0
  171. package/src/scripts/lint_namespace_collisions.ts +19 -0
  172. package/src/scripts/lint_no_new_atomic_commands.ts +33 -0
  173. package/src/scripts/lint_no_python_twin_rationale.ts +18 -0
  174. package/src/scripts/lint_one_off_age.ts +38 -2
  175. package/src/scripts/lint_orchestration_dsl.ts +23 -3
  176. package/src/scripts/lint_orchestrator_auto_detect.ts +19 -12
  177. package/src/scripts/lint_originality.ts +22 -4
  178. package/src/scripts/lint_originality_shingles.ts +28 -3
  179. package/src/scripts/lint_output_slop.ts +19 -0
  180. package/src/scripts/lint_override_kernel_guard.ts +30 -0
  181. package/src/scripts/lint_pack_first_win.ts +23 -0
  182. package/src/scripts/lint_pack_risk_class.ts +19 -3
  183. package/src/scripts/lint_persistence.ts +19 -0
  184. package/src/scripts/lint_persona_governance.ts +18 -3
  185. package/src/scripts/lint_positioning.ts +23 -0
  186. package/src/scripts/lint_pre_migration_refs.ts +24 -0
  187. package/src/scripts/lint_profile_overlay_set_only.ts +24 -0
  188. package/src/scripts/lint_provenance.ts +21 -0
  189. package/src/scripts/lint_provenance_vocabulary.ts +24 -1
  190. package/src/scripts/lint_readme_jargon.ts +21 -3
  191. package/src/scripts/lint_readme_serial_comma.ts +21 -3
  192. package/src/scripts/lint_readme_size.ts +21 -3
  193. package/src/scripts/lint_regression.ts +28 -0
  194. package/src/scripts/lint_roadmap_blockers.ts +43 -0
  195. package/src/scripts/lint_roadmap_ci_steps.ts +45 -0
  196. package/src/scripts/lint_roadmap_complexity.ts +47 -0
  197. package/src/scripts/lint_roadmap_later_disposition.ts +23 -0
  198. package/src/scripts/lint_rule_interactions.ts +16 -2
  199. package/src/scripts/lint_rule_tiers.ts +19 -6
  200. package/src/scripts/lint_showcase_sessions.ts +19 -3
  201. package/src/scripts/lint_skill_descriptions.ts +19 -0
  202. package/src/scripts/lint_skill_frontmatter_safety.ts +24 -0
  203. package/src/scripts/lint_skill_originality.ts +18 -3
  204. package/src/scripts/lint_skill_scripts_readonly.ts +20 -0
  205. package/src/scripts/lint_skill_tools.ts +84 -145
  206. package/src/scripts/lint_spawn_payload.ts +27 -1
  207. package/src/scripts/lint_store_boundary.ts +19 -0
  208. package/src/scripts/lint_subagent_determinism.ts +23 -1
  209. package/src/scripts/lint_supported_tools_matrix.ts +18 -0
  210. package/src/scripts/lint_surface_matrix.ts +19 -3
  211. package/src/scripts/lint_ticket_buildable.ts +23 -0
  212. package/src/scripts/lint_topics_yaml.ts +19 -12
  213. package/src/scripts/lint_trigger_precision.ts +27 -0
  214. package/src/scripts/lint_trust_coherence.ts +21 -0
  215. package/src/scripts/lint_ui_stack_bundles.ts +17 -3
  216. package/src/scripts/lint_value_dashboard.ts +19 -3
  217. package/src/scripts/lint_versioned_cache.ts +19 -1
  218. package/src/scripts/lint_workflow_security.ts +22 -14
  219. package/src/scripts/pr_url_reminder_hook.ts +1 -1
  220. package/src/scripts/skill_collision_clusters.ts +37 -3
  221. package/src/scripts/skill_discovery.ts +20 -0
  222. package/src/scripts/skill_eval_coverage.ts +22 -0
  223. package/src/scripts/skill_overlap.ts +32 -6
  224. package/src/scripts/skill_preview.ts +31 -0
  225. package/src/scripts/skill_trigger_eval.ts +18 -0
  226. package/src/scripts/skill_usage_collect.ts +21 -0
  227. package/src/scripts/skill_usage_report.ts +21 -0
  228. package/src/scripts/sweep_dead_scan_roots.ts +11 -3
  229. package/src/scripts/verify_physical_move.ts +24 -0
  230. package/src/scripts/check_bite_sized_granularity.ts +0 -116
  231. package/src/scripts/lint_workspace_boundary.ts +0 -267
@@ -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();
@@ -19,6 +19,7 @@
19
19
  import * as fs from 'node:fs';
20
20
  import * as path from 'node:path';
21
21
 
22
+ import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
22
23
  import { main as runOriginality } from './lint_originality.js';
23
24
 
24
25
  const REPO_ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', '..');
@@ -136,9 +137,32 @@ export function main(): number {
136
137
  );
137
138
  return 0;
138
139
  }
139
- const { drifted, missing } = checkFreshness();
140
140
  const rel = (f: string): string => path.relative(REPO_ROOT, f);
141
141
 
142
+ // `REPORTS` is the entire scope. One of the two absent is a real state the
143
+ // `missing` branch below reports; BOTH absent is the reports directory
144
+ // having moved, and the sweep would then WRITE two files, call them missing,
145
+ // and delete them again — a warning about the wrong thing. Exit stays 0:
146
+ // this surface is an advisory by design and has no failure code.
147
+ try {
148
+ assertWatchlistResolves({
149
+ gate: 'check_originality_freshness',
150
+ candidates: REPORTS.map(rel),
151
+ repoRoot: REPO_ROOT,
152
+ });
153
+ } catch (err) {
154
+ if (err instanceof DeadScopeError) {
155
+ process.stderr.write(
156
+ `⚠️ ${err.message}\n` +
157
+ ' Run `./scripts-run src/scripts/lint_originality` and commit the result.\n',
158
+ );
159
+ return 0;
160
+ }
161
+ throw err;
162
+ }
163
+
164
+ const { drifted, missing } = checkFreshness();
165
+
142
166
  if (missing.length > 0) {
143
167
  process.stdout.write(
144
168
  `⚠️ originality report is not committed yet: ${missing.map(rel).join(', ')}\n` +
@@ -30,6 +30,7 @@ import {
30
30
  CASCADE_ELIGIBLE_KINDS,
31
31
  USER_GLOBAL_OVERLAY_KINDS,
32
32
  } from './_lib/agents_overlay.js';
33
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
33
34
 
34
35
  const _HERE = fileURLToPath(import.meta.url);
35
36
  // src/scripts/check_overlay_cascade_subdirs.ts → two dirs up is the repo root.
@@ -96,6 +97,26 @@ export function main(): number {
96
97
  }
97
98
  const text = fs.readFileSync(DOCS_PATH, 'utf-8');
98
99
  const parsed = _parse_doc_table(text);
100
+ // The doc side of this comparison is a table PARSE, so a restructured,
101
+ // renamed, or reformatted table yields zero rows while the file still
102
+ // exists — and the set comparison then reports drift against phantoms, or
103
+ // nothing at all once both lists are empty. Exit 3, not 1: an unreadable
104
+ // table means the gate could not run, which is the same class as the
105
+ // `not found` path above; 1 is reserved for real code↔docs drift.
106
+ try {
107
+ assertScanned({
108
+ gate: 'check_overlay_cascade_subdirs',
109
+ scanned: parsed.all_kinds.size,
110
+ units: 'overlay table row(s)',
111
+ roots: ['docs/customization.md'],
112
+ });
113
+ } catch (exc) {
114
+ if (exc instanceof DeadScopeError) {
115
+ process.stderr.write(`❌ ${exc.message}\n`);
116
+ return 3;
117
+ }
118
+ throw exc;
119
+ }
99
120
  const doc_cascade = parsed.cascade_yes;
100
121
  const doc_user_global = parsed.user_global_yes;
101
122
 
@@ -27,6 +27,8 @@ import * as fs from 'node:fs';
27
27
  import * as path from 'node:path';
28
28
  import { fileURLToPath } from 'node:url';
29
29
 
30
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
31
+
30
32
  const REPO_ROOT = path.resolve(fileURLToPath(import.meta.url), '..', '..', '..');
31
33
  const BUDGET_PATH = path.join(REPO_ROOT, 'src', 'config', 'pack-size-budget.json');
32
34
  const SKILL_PREFIX = 'dist/agent-src/skills/';
@@ -176,6 +178,26 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
176
178
  process.stderr.write(`❌ pack size: npm pack failed: ${String(err)}\n`);
177
179
  return 2;
178
180
  }
181
+ // The pack manifest IS the corpus. `evaluate` refuses a vacuous per-skill
182
+ // share, but the packed-size arm has no such floor: an empty payload — a
183
+ // botched `files[]`, a pack that resolved nothing — measures 0 MB and sits
184
+ // comfortably under every budget.
185
+ try {
186
+ assertScanned({
187
+ gate: 'check_pack_size',
188
+ scanned: pack.files.length,
189
+ units: 'packed file(s)',
190
+ roots: ['npm pack --dry-run payload (package.json files[])'],
191
+ });
192
+ } catch (err) {
193
+ if (err instanceof DeadScopeError) {
194
+ // 2, not 1: the documented meaning of 1 is "over budget", and an
195
+ // empty manifest is unreadable input, not a budget violation.
196
+ process.stderr.write(`❌ pack size: ${err.message}\n`);
197
+ return 2;
198
+ }
199
+ throw err;
200
+ }
179
201
 
180
202
  const errors = evaluate(budget, pack);
181
203
  const { perSkill, total } = skillBytes(pack.files);
@@ -23,6 +23,7 @@
23
23
  import * as fs from 'node:fs';
24
24
  import * as path from 'node:path';
25
25
 
26
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
26
27
  import { censusClaudeMdHierarchy, censusRuleDir, censusSkillsCatalog } from './preamble_byte_census.js';
27
28
 
28
29
  const HERE = path.dirname(new URL(import.meta.url).pathname);
@@ -40,7 +41,7 @@ export interface BudgetVerdict {
40
41
  baseline: number;
41
42
  ceiling: number;
42
43
  withinBudget: boolean;
43
- buckets: Array<{ name: string; tokens: number }>;
44
+ buckets: Array<{ name: string; tokens: number; files: number }>;
44
45
  }
45
46
 
46
47
  function tokens(chars: number): number {
@@ -65,7 +66,7 @@ export function readBudget(file: string = BUDGET_FILE): Budget {
65
66
  /** Measure only what the repo tree determines — no `~`, no transcripts, no network. */
66
67
  export function measureDeterministicPayload(
67
68
  repoRoot: string = REPO_ROOT,
68
- ): Array<{ name: string; tokens: number }> {
69
+ ): Array<{ name: string; tokens: number; files: number }> {
69
70
  const projectRules = censusRuleDir(path.join(repoRoot, 'dist', 'agent-src', 'rules'));
70
71
  const skills = censusSkillsCatalog(path.join(repoRoot, 'dist', 'agent-src', 'skills'));
71
72
  // Only the PROJECT half of the CLAUDE.md hierarchy is deterministic — the
@@ -73,10 +74,20 @@ export function measureDeterministicPayload(
73
74
  const claudeMd = censusClaudeMdHierarchy(repoRoot, path.join(repoRoot, '.no-such-home'));
74
75
  const projectClaudeMdChars =
75
76
  claudeMd.project_claude_md_chars + claudeMd.project_claude_local_md_chars;
77
+ // `files` carries the count of sources each census actually read. A token
78
+ // total cannot tell "the root moved" from "the payload is genuinely tiny",
79
+ // so the scope assertion in `main` needs the count, not the bytes.
80
+ const claudeMdFiles =
81
+ (claudeMd.project_claude_md_present ? 1 : 0) +
82
+ (claudeMd.project_claude_local_md_present ? 1 : 0);
76
83
  return [
77
- { name: 'project-scope rules', tokens: tokens(projectRules.chars) },
78
- { name: 'preloaded skills catalog', tokens: tokens(skills.chars) },
79
- { name: 'CLAUDE.md hierarchy (project only)', tokens: tokens(projectClaudeMdChars) },
84
+ { name: 'project-scope rules', tokens: tokens(projectRules.chars), files: projectRules.files },
85
+ { name: 'preloaded skills catalog', tokens: tokens(skills.chars), files: skills.skills },
86
+ {
87
+ name: 'CLAUDE.md hierarchy (project only)',
88
+ tokens: tokens(projectClaudeMdChars),
89
+ files: claudeMdFiles,
90
+ },
80
91
  ];
81
92
  }
82
93
 
@@ -98,6 +109,25 @@ export function main(argv: string[] = process.argv.slice(2)): number {
98
109
  return 2;
99
110
  }
100
111
 
112
+ // A ratchet over a measurement of nothing always passes: move
113
+ // `dist/agent-src/` and every census returns zero, which is trivially under
114
+ // any ceiling. Exit 2 (misuse / unreadable budget — the could-not-run
115
+ // code), never 1, which asserts the payload actually grew.
116
+ try {
117
+ assertScanned({
118
+ gate: 'check_preamble_payload_budget',
119
+ scanned: verdict.buckets.reduce((n, b) => n + b.files, 0),
120
+ units: 'payload source file(s)',
121
+ roots: ['dist/agent-src/rules', 'dist/agent-src/skills', 'CLAUDE.md'],
122
+ });
123
+ } catch (err) {
124
+ if (err instanceof DeadScopeError) {
125
+ process.stderr.write(`❌ preamble-payload budget: ${err.message}\n`);
126
+ return 2;
127
+ }
128
+ throw err;
129
+ }
130
+
101
131
  if (json) {
102
132
  process.stdout.write(JSON.stringify(verdict, null, 2) + '\n');
103
133
  return verdict.withinBudget ? 0 : 1;
@@ -37,6 +37,8 @@ import process from 'node:process';
37
37
  import { fileURLToPath, pathToFileURL } from 'node:url';
38
38
  import { parse as parseYaml } from 'yaml';
39
39
 
40
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
41
+
40
42
  type Severity = 'error' | 'warning';
41
43
 
42
44
  const REQUIRED_FRONTMATTER: ReadonlySet<string> = new Set([
@@ -456,7 +458,28 @@ function main(): number {
456
458
  process.stderr.write(`error: ${p} not found\n`);
457
459
  return 3;
458
460
  }
459
- const findings = _runChecks(fs.readFileSync(p, 'utf-8'), p);
461
+ const text = fs.readFileSync(p, 'utf-8');
462
+ // No corpus walk: this gate's scope is the single document named on argv.
463
+ // The unit is therefore the content lines actually read — every check below
464
+ // is a search over this text, so a zero-byte target makes the negative ones
465
+ // ("no TODO/TBD markers") vacuously true against nothing.
466
+ try {
467
+ assertScanned({
468
+ gate: 'check_proposal',
469
+ scanned: text.split('\n').filter((l) => l.trim() !== '').length,
470
+ units: 'content line(s)',
471
+ roots: [p],
472
+ });
473
+ } catch (exc) {
474
+ if (exc instanceof DeadScopeError) {
475
+ // 1 = gate failure, which is what an unusable proposal already
476
+ // returns today (3 is reserved for the not-found path above).
477
+ process.stderr.write(`❌ ${exc.message}\n`);
478
+ return 1;
479
+ }
480
+ throw exc;
481
+ }
482
+ const findings = _runChecks(text, p);
460
483
  const errors = findings.filter((f) => f.severity === 'error');
461
484
  if (args.format === 'json') {
462
485
  process.stdout.write(
@@ -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 { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
30
+
29
31
  const QUIET = process.argv.slice(2).includes('--quiet');
30
32
 
31
33
  const _HERE = fileURLToPath(import.meta.url);
@@ -117,6 +119,10 @@ function main(): number {
117
119
  const missing: Array<[number, string]> = [];
118
120
  const unshipped: Array<[number, string]> = [];
119
121
 
122
+ // Counted BEFORE any filtering — external hrefs and anchors included — so
123
+ // the number answers "did the catalog still carry links", not "how many
124
+ // survived the checks".
125
+ let links_seen = 0;
120
126
  const lines = _splitlines(text);
121
127
  for (let i = 0; i < lines.length; i++) {
122
128
  const lineno = i + 1;
@@ -124,6 +130,7 @@ function main(): number {
124
130
  LINK_RE.lastIndex = 0;
125
131
  let m: RegExpExecArray | null;
126
132
  while ((m = LINK_RE.exec(line)) !== null) {
133
+ links_seen += 1;
127
134
  const href = m[1]!;
128
135
  if (href.includes(FORBIDDEN_PREFIX)) {
129
136
  forbidden.push([lineno, href]);
@@ -143,6 +150,26 @@ function main(): number {
143
150
  }
144
151
  }
145
152
 
153
+ // The catalog is generated. A renderer that stops emitting links leaves a
154
+ // file that still exists and still parses, so the three violation buckets
155
+ // stay empty and this reports "all links resolve" over no links at all.
156
+ // Exit 1 is the only failure code here, and it already carries the
157
+ // could-not-run meaning (the missing-catalog path above returns it too).
158
+ try {
159
+ assertScanned({
160
+ gate: 'check_public_catalog_links',
161
+ scanned: links_seen,
162
+ units: 'markdown link(s)',
163
+ roots: ['docs/catalog.md'],
164
+ });
165
+ } catch (exc) {
166
+ if (exc instanceof DeadScopeError) {
167
+ process.stdout.write(`❌ ${exc.message}\n`);
168
+ return 1;
169
+ }
170
+ throw exc;
171
+ }
172
+
146
173
  const total_violations = forbidden.length + missing.length + unshipped.length;
147
174
  if (total_violations === 0) {
148
175
  if (!QUIET) {