@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +51 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli-delegate/{chunk-XGA2NTU4.js → chunk-3EWYC3YK.js} +40 -0
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +1 -1
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +1 -1
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +1 -1
- package/dist/hooks/dispatch.js +58 -2
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +47 -29
- package/package.json +1 -1
- package/src/config/gate-coverage.yml +22 -4
- package/src/config/gate-violation-baselines.json +3 -8
- package/src/scripts/_lib/gate_population.ts +70 -0
- package/src/scripts/_lib/roadmap_granularity.ts +157 -0
- package/src/scripts/_lib/scan_scope.ts +44 -0
- package/src/scripts/_lib/stdin.ts +82 -0
- package/src/scripts/audit_adr_coverage.ts +38 -0
- package/src/scripts/audit_auto_rules.ts +17 -3
- package/src/scripts/audit_command_surface.ts +51 -0
- package/src/scripts/audit_initial_context.ts +39 -3
- package/src/scripts/audit_mcp_tools.ts +25 -0
- package/src/scripts/audit_overlap.ts +24 -1
- package/src/scripts/audit_skill_descriptions.ts +19 -0
- package/src/scripts/{verify_before_complete_hook.ts → before_complete_hook.ts} +16 -3
- package/src/scripts/check_adr_frontmatter.ts +23 -3
- package/src/scripts/check_always_budget.ts +24 -0
- package/src/scripts/check_artefact_checksums.ts +24 -1
- package/src/scripts/check_artefact_count_messaging.ts +20 -0
- package/src/scripts/check_augmentignore.ts +22 -15
- package/src/scripts/check_backstop_debt.ts +22 -0
- package/src/scripts/check_beta_review_markers.ts +22 -1
- package/src/scripts/check_bridge_derivation.ts +22 -1
- package/src/scripts/check_bundle_path_leakage.ts +26 -0
- package/src/scripts/check_ci_strict_superset.ts +21 -3
- package/src/scripts/check_claims.ts +22 -0
- package/src/scripts/check_cli_registry_budget_sync.ts +33 -4
- package/src/scripts/check_cluster_patterns.ts +26 -4
- package/src/scripts/check_command_count_messaging.ts +20 -4
- package/src/scripts/check_comparison.ts +18 -0
- package/src/scripts/check_condensed_paths.ts +23 -2
- package/src/scripts/check_consumer_scope_flip.ts +21 -2
- package/src/scripts/check_council_config_location.ts +20 -0
- package/src/scripts/check_council_layout.ts +22 -2
- package/src/scripts/check_council_references.ts +23 -1
- package/src/scripts/check_dependency_floors.ts +19 -3
- package/src/scripts/check_discovery_determinism.ts +26 -0
- package/src/scripts/check_enforcement_coverage.ts +20 -0
- package/src/scripts/check_evaluator_budgets.ts +23 -0
- package/src/scripts/check_finding_dispositions.ts +29 -0
- package/src/scripts/check_gate_coverage.ts +63 -22
- package/src/scripts/check_gate_paths.ts +30 -5
- package/src/scripts/check_generated_artefact_headers.ts +21 -0
- package/src/scripts/check_generator_output_coverage.ts +23 -0
- package/src/scripts/check_gitignore_freshness.ts +23 -0
- package/src/scripts/check_host_loadability.ts +34 -8
- package/src/scripts/check_installer_import_purity.ts +22 -8
- package/src/scripts/check_kernel_prefix_stability.ts +19 -1
- package/src/scripts/check_kernel_rule_bundle.ts +23 -0
- package/src/scripts/check_knowledge_cards.ts +13 -0
- package/src/scripts/check_knowledge_pages.ts +23 -0
- package/src/scripts/check_knowledge_sharing.ts +27 -0
- package/src/scripts/check_md_language.ts +24 -0
- package/src/scripts/check_media_deps.ts +23 -0
- package/src/scripts/check_memory.ts +26 -1
- package/src/scripts/check_memory_contradiction.ts +16 -1
- package/src/scripts/check_memory_proposal.ts +15 -0
- package/src/scripts/check_memory_similarity.ts +16 -0
- package/src/scripts/check_module_management_neutral.ts +19 -11
- package/src/scripts/check_no_conflict_markers.ts +22 -1
- package/src/scripts/check_no_external_sources.ts +26 -1
- package/src/scripts/check_no_local_settings_committed.ts +37 -10
- package/src/scripts/check_no_new_legacy_path.ts +30 -1
- package/src/scripts/check_one_off_location.ts +43 -0
- package/src/scripts/check_originality_freshness.ts +25 -1
- package/src/scripts/check_overlay_cascade_subdirs.ts +21 -0
- package/src/scripts/check_pack_size.ts +22 -0
- package/src/scripts/check_preamble_payload_budget.ts +35 -5
- package/src/scripts/check_proposal.ts +24 -1
- package/src/scripts/check_public_catalog_links.ts +27 -0
- package/src/scripts/check_public_links.ts +19 -0
- package/src/scripts/check_quality_regression.ts +19 -0
- package/src/scripts/check_reach_channels.ts +44 -0
- package/src/scripts/check_reach_staleness.ts +27 -0
- package/src/scripts/check_release_adjacent_health.ts +28 -0
- package/src/scripts/check_release_highlights.ts +34 -0
- package/src/scripts/check_release_includes_discovery.ts +25 -6
- package/src/scripts/check_release_pr_shape.ts +18 -1
- package/src/scripts/check_release_published.ts +21 -0
- package/src/scripts/check_release_surface_equality.ts +22 -0
- package/src/scripts/check_release_trunk_sync.ts +19 -0
- package/src/scripts/check_reply_consistency.ts +22 -0
- package/src/scripts/check_roadmap_trackable.ts +20 -0
- package/src/scripts/check_role_doc_links.ts +23 -0
- package/src/scripts/check_rule_invariants.ts +14 -0
- package/src/scripts/check_secret_leak.ts +47 -0
- package/src/scripts/check_skill_gaps.ts +32 -0
- package/src/scripts/check_skill_requires.ts +19 -0
- package/src/scripts/check_source_pointer_freshness.ts +20 -0
- package/src/scripts/check_structural_breaking.ts +29 -2
- package/src/scripts/check_surface_tiers.ts +34 -0
- package/src/scripts/check_template_pin_drift.ts +23 -1
- package/src/scripts/check_test_coverage_diff.ts +17 -1
- package/src/scripts/check_token_optimizer_freshness.ts +18 -3
- package/src/scripts/check_token_quality_golden.ts +21 -0
- package/src/scripts/check_token_regression.ts +25 -0
- package/src/scripts/check_tracked_but_ignored.ts +26 -0
- package/src/scripts/check_trigger_eval_presence.ts +23 -1
- package/src/scripts/check_trigger_evals.ts +36 -0
- package/src/scripts/check_trunk_drift.ts +23 -0
- package/src/scripts/check_update_banner.ts +24 -0
- package/src/scripts/gate_scope_classify.ts +513 -0
- package/src/scripts/hook_manifest.yaml +1 -1
- package/src/scripts/hooks/block_kernel_rule_writes.ts +1 -1
- package/src/scripts/hooks/concern_registry.ts +2 -2
- package/src/scripts/lint_agent_security.ts +19 -0
- package/src/scripts/lint_agent_skill_names.ts +30 -0
- package/src/scripts/lint_agents_layout.ts +20 -0
- package/src/scripts/lint_agents_md.ts +20 -0
- package/src/scripts/lint_archived_skills.ts +24 -3
- package/src/scripts/lint_behavioural_eval_freshness.ts +29 -3
- package/src/scripts/lint_bench_ab.ts +25 -0
- package/src/scripts/lint_bench_corpus.ts +20 -4
- package/src/scripts/lint_breaking_changes_index.ts +24 -0
- package/src/scripts/lint_budget_ownership.ts +20 -3
- package/src/scripts/lint_changelog_rollback.ts +24 -0
- package/src/scripts/lint_command_flow_coverage.ts +22 -0
- package/src/scripts/lint_command_tiers.ts +29 -5
- package/src/scripts/lint_commit_subjects.ts +15 -0
- package/src/scripts/lint_confusables.ts +24 -0
- package/src/scripts/lint_consumer_internal_refs.ts +24 -0
- package/src/scripts/lint_design_quality.ts +22 -0
- package/src/scripts/lint_design_slop.ts +23 -0
- package/src/scripts/lint_discovery_manifest.ts +23 -0
- package/src/scripts/lint_discovery_vocabulary.ts +19 -0
- package/src/scripts/lint_documented_commands.ts +23 -0
- package/src/scripts/lint_empty_roadmaps.ts +23 -0
- package/src/scripts/lint_eval_fixture_citations.ts +19 -0
- package/src/scripts/lint_eval_freshness.ts +35 -0
- package/src/scripts/lint_examples.ts +18 -3
- package/src/scripts/lint_explain_trace.ts +23 -0
- package/src/scripts/lint_featured_skills.ts +26 -1
- package/src/scripts/lint_flows.ts +19 -0
- package/src/scripts/lint_frontmatter_boilerplate.ts +20 -0
- package/src/scripts/lint_ghostwriter_source.ts +26 -6
- package/src/scripts/lint_glama_drift.ts +24 -0
- package/src/scripts/lint_global_paths.ts +24 -0
- package/src/scripts/lint_governed_writes.ts +20 -0
- package/src/scripts/lint_hidden_unicode.ts +25 -0
- package/src/scripts/lint_hook_concern_budget.ts +39 -6
- package/src/scripts/lint_hook_manifest.ts +22 -0
- package/src/scripts/lint_instruction_smuggling.ts +23 -0
- package/src/scripts/lint_knowledge_scale.ts +32 -2
- package/src/scripts/lint_legal_pack.ts +31 -0
- package/src/scripts/lint_marketplace.ts +26 -0
- package/src/scripts/lint_marketplace_install_completeness.ts +23 -0
- package/src/scripts/lint_mcp_config_security.ts +24 -0
- package/src/scripts/lint_mcp_registry_manifest.ts +22 -1
- package/src/scripts/lint_memory_tripwire.ts +35 -0
- package/src/scripts/lint_missions.ts +23 -7
- package/src/scripts/lint_model_tier_coverage.ts +18 -0
- package/src/scripts/lint_namespace_collisions.ts +19 -0
- package/src/scripts/lint_no_new_atomic_commands.ts +33 -0
- package/src/scripts/lint_no_python_twin_rationale.ts +18 -0
- package/src/scripts/lint_one_off_age.ts +38 -2
- package/src/scripts/lint_orchestration_dsl.ts +23 -3
- package/src/scripts/lint_orchestrator_auto_detect.ts +19 -12
- package/src/scripts/lint_originality.ts +22 -4
- package/src/scripts/lint_originality_shingles.ts +28 -3
- package/src/scripts/lint_output_slop.ts +19 -0
- package/src/scripts/lint_override_kernel_guard.ts +30 -0
- package/src/scripts/lint_pack_first_win.ts +23 -0
- package/src/scripts/lint_pack_risk_class.ts +19 -3
- package/src/scripts/lint_persistence.ts +19 -0
- package/src/scripts/lint_persona_governance.ts +18 -3
- package/src/scripts/lint_positioning.ts +23 -0
- package/src/scripts/lint_pre_migration_refs.ts +24 -0
- package/src/scripts/lint_profile_overlay_set_only.ts +24 -0
- package/src/scripts/lint_provenance.ts +21 -0
- package/src/scripts/lint_provenance_vocabulary.ts +24 -1
- package/src/scripts/lint_readme_jargon.ts +21 -3
- package/src/scripts/lint_readme_serial_comma.ts +21 -3
- package/src/scripts/lint_readme_size.ts +21 -3
- package/src/scripts/lint_regression.ts +28 -0
- package/src/scripts/lint_roadmap_blockers.ts +43 -0
- package/src/scripts/lint_roadmap_ci_steps.ts +45 -0
- package/src/scripts/lint_roadmap_complexity.ts +47 -0
- package/src/scripts/lint_roadmap_later_disposition.ts +23 -0
- package/src/scripts/lint_rule_interactions.ts +16 -2
- package/src/scripts/lint_rule_tiers.ts +19 -6
- package/src/scripts/lint_showcase_sessions.ts +19 -3
- package/src/scripts/lint_skill_descriptions.ts +19 -0
- package/src/scripts/lint_skill_frontmatter_safety.ts +24 -0
- package/src/scripts/lint_skill_originality.ts +18 -3
- package/src/scripts/lint_skill_scripts_readonly.ts +20 -0
- package/src/scripts/lint_skill_tools.ts +84 -145
- package/src/scripts/lint_spawn_payload.ts +27 -1
- package/src/scripts/lint_store_boundary.ts +19 -0
- package/src/scripts/lint_subagent_determinism.ts +23 -1
- package/src/scripts/lint_supported_tools_matrix.ts +18 -0
- package/src/scripts/lint_surface_matrix.ts +19 -3
- package/src/scripts/lint_ticket_buildable.ts +23 -0
- package/src/scripts/lint_topics_yaml.ts +19 -12
- package/src/scripts/lint_trigger_precision.ts +27 -0
- package/src/scripts/lint_trust_coherence.ts +21 -0
- package/src/scripts/lint_ui_stack_bundles.ts +17 -3
- package/src/scripts/lint_value_dashboard.ts +19 -3
- package/src/scripts/lint_versioned_cache.ts +19 -1
- package/src/scripts/lint_workflow_security.ts +22 -14
- package/src/scripts/pr_url_reminder_hook.ts +1 -1
- package/src/scripts/skill_collision_clusters.ts +37 -3
- package/src/scripts/skill_discovery.ts +20 -0
- package/src/scripts/skill_eval_coverage.ts +22 -0
- package/src/scripts/skill_overlap.ts +32 -6
- package/src/scripts/skill_preview.ts +31 -0
- package/src/scripts/skill_trigger_eval.ts +18 -0
- package/src/scripts/skill_usage_collect.ts +21 -0
- package/src/scripts/skill_usage_report.ts +21 -0
- package/src/scripts/sweep_dead_scan_roots.ts +11 -3
- package/src/scripts/verify_physical_move.ts +24 -0
- package/src/scripts/check_bite_sized_granularity.ts +0 -116
- package/src/scripts/lint_workspace_boundary.ts +0 -267
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bite-sized task-granularity check for structural roadmaps.
|
|
3
|
+
*
|
|
4
|
+
* A `complexity: structural` roadmap's task bullets must name real work. A
|
|
5
|
+
* placeholder in a task bullet — `<something>`, TODO, FIXME, XXX, TBD, `???` —
|
|
6
|
+
* means the step was filed before it was decided, which is the failure the
|
|
7
|
+
* bite-sized standard exists to catch.
|
|
8
|
+
*
|
|
9
|
+
* ## Why this lives in `_lib/` and not in a gate of its own
|
|
10
|
+
*
|
|
11
|
+
* It used to be `src/scripts/check_bite_sized_granularity.ts`: a pure library
|
|
12
|
+
* with no CLI, no `main`, no exit code, and — measured — no production caller
|
|
13
|
+
* outside its own test. It was carried over from a retired Python module for
|
|
14
|
+
* API parity and never wired up, so a real check that no other gate covers ran
|
|
15
|
+
* nowhere, while the `check_` prefix kept it inside the gate population where
|
|
16
|
+
* the scan-scope ratchet counted it as a gate that could not assert a scan
|
|
17
|
+
* scope.
|
|
18
|
+
*
|
|
19
|
+
* AI council 2026-08-05 resolved it by folding rather than deleting: the checks
|
|
20
|
+
* are the only placeholder coverage in the tree (`lint_roadmap_complexity`
|
|
21
|
+
* covers the complexity tag, the lightweight caps, and the plate/horizon
|
|
22
|
+
* prohibition, and greps clean for every placeholder pattern), so deleting them
|
|
23
|
+
* would have discarded live value to reach a count. Folding into the gate that
|
|
24
|
+
* already owns the roadmap corpus adds no gate script, no CI job, and no
|
|
25
|
+
* manifest entry, and the population member disappears with the old file.
|
|
26
|
+
*
|
|
27
|
+
* ## Inline-code masking (the reason this is not a verbatim move)
|
|
28
|
+
*
|
|
29
|
+
* The original patterns matched inside backticked spans, which made CLI
|
|
30
|
+
* metasyntax a violation. Measured across 22 structural roadmaps, the only hit
|
|
31
|
+
* in the whole tree was `` `settings set <key> <value>` `` — argument
|
|
32
|
+
* metasyntax in a code span, on a step that is otherwise fully specified. The
|
|
33
|
+
* council flagged folding a check with an unaudited hit in an out-of-scope
|
|
34
|
+
* directory as a time-bomb: it would fire the day the gate's scope widened. So
|
|
35
|
+
* the fix is precision, not scope-limiting — inline code spans are masked
|
|
36
|
+
* before matching, the same technique `lint_readme_serial_comma` already uses
|
|
37
|
+
* on README prose. With masking the count is 0 across every structural roadmap,
|
|
38
|
+
* `later/` and `stubs/` included, so widening the gate's scope later cannot
|
|
39
|
+
* surprise anyone.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/** Placeholder patterns in (kind, regex) order. */
|
|
43
|
+
const PLACEHOLDER_PATTERNS: ReadonlyArray<[string, RegExp]> = [
|
|
44
|
+
['angle-placeholder', /<[a-z][a-z0-9 _\-/]*>/i],
|
|
45
|
+
['todo', /\bTODO\b/],
|
|
46
|
+
['fixme', /\bFIXME\b/],
|
|
47
|
+
['xxx', /\bXXX\b/],
|
|
48
|
+
['tbd', /\btbd\b/i],
|
|
49
|
+
['triple-question', /\?\?\?/],
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const COMPLEXITY_PAT = /^complexity:\s*(lightweight|structural)\s*$/m;
|
|
53
|
+
|
|
54
|
+
const TASK_PREFIXES = ['- [ ]', '- [x]', '- [/]', '- [-]'] as const;
|
|
55
|
+
|
|
56
|
+
export interface Placeholder {
|
|
57
|
+
kind: string;
|
|
58
|
+
line: number;
|
|
59
|
+
text: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface GranularityResult {
|
|
63
|
+
complexity: string | null;
|
|
64
|
+
gated: boolean;
|
|
65
|
+
violations: Placeholder[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function frontmatter(text: string): string {
|
|
69
|
+
if (!text.startsWith('---\n')) {
|
|
70
|
+
return '';
|
|
71
|
+
}
|
|
72
|
+
const end = text.indexOf('\n---\n', 4);
|
|
73
|
+
return end !== -1 ? text.slice(4, end) : '';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function read_complexity(text: string): string | null {
|
|
77
|
+
const fm = frontmatter(text);
|
|
78
|
+
if (fm === '') {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const m = COMPLEXITY_PAT.exec(fm);
|
|
82
|
+
return m ? m[1]! : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const ANGLE_PLACEHOLDER = /<[a-z][a-z0-9 _\-/]*>/gi;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Blank inline code spans that are COMMAND TEMPLATES, so CLI argument
|
|
89
|
+
* metasyntax is not read as an undecided step.
|
|
90
|
+
*
|
|
91
|
+
* Blanking every backtick span (the first attempt) was too blunt, and the ported
|
|
92
|
+
* test corpus proved it: `` `<file>` `` in *"Edit `<file>` and add the new
|
|
93
|
+
* method"* is a real placeholder — the file is genuinely undecided — while
|
|
94
|
+
* `` `settings set <key> <value>` `` on a step that goes on to specify
|
|
95
|
+
* zod-validation, atomicity and key-class refusal is a command signature.
|
|
96
|
+
*
|
|
97
|
+
* The discriminator is what else the span holds: strip the placeholders and if
|
|
98
|
+
* nothing but whitespace is left, the span IS the placeholder (violation); if
|
|
99
|
+
* real content remains (`settings set`), it is a command template naming its
|
|
100
|
+
* arguments (masked). Only the angle pattern gets this treatment — a literal
|
|
101
|
+
* TODO / FIXME / XXX / TBD / `???` in a task bullet is unfinished work wherever
|
|
102
|
+
* it sits, backticks included.
|
|
103
|
+
*/
|
|
104
|
+
function mask_command_templates(line: string): string {
|
|
105
|
+
return line.replace(/`[^`]*`/g, (span) => {
|
|
106
|
+
const withoutPlaceholders = span.slice(1, -1).replace(ANGLE_PLACEHOLDER, '');
|
|
107
|
+
return withoutPlaceholders.trim() === '' ? span : ' ';
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function scan_placeholders(text: string): Placeholder[] {
|
|
112
|
+
const hits: Placeholder[] = [];
|
|
113
|
+
const lines = text.split('\n');
|
|
114
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
115
|
+
const line = lines[idx]!;
|
|
116
|
+
const stripped = line.replace(/^\s+/, '');
|
|
117
|
+
if (!TASK_PREFIXES.some((p) => stripped.startsWith(p))) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
// Command-template masking applies to the angle pattern ONLY. A literal
|
|
121
|
+
// TODO / FIXME / XXX / TBD / `???` is unfinished work wherever it sits,
|
|
122
|
+
// so those patterns read the raw line.
|
|
123
|
+
const masked = mask_command_templates(line);
|
|
124
|
+
for (const [kind, pat] of PLACEHOLDER_PATTERNS) {
|
|
125
|
+
const probe = kind === 'angle-placeholder' ? masked : line;
|
|
126
|
+
if (pat.test(probe)) {
|
|
127
|
+
hits.push({ kind, line: idx + 1, text: line.replace(/\s+$/, '') });
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return hits;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Gated only for `complexity: structural`. Violations are empty when the gate
|
|
137
|
+
* is not active, regardless of placeholder presence.
|
|
138
|
+
*/
|
|
139
|
+
export function check_granularity(text: string): GranularityResult {
|
|
140
|
+
const complexity = read_complexity(text);
|
|
141
|
+
const gated = complexity === 'structural';
|
|
142
|
+
if (!gated) {
|
|
143
|
+
return { complexity, gated: false, violations: [] };
|
|
144
|
+
}
|
|
145
|
+
return { complexity, gated: true, violations: scan_placeholders(text) };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** One human-readable problem line per violation, for a gate's problem list. */
|
|
149
|
+
export function granularity_problems(text: string): string[] {
|
|
150
|
+
return check_granularity(text).violations.map(
|
|
151
|
+
(v) =>
|
|
152
|
+
`bite-sized granularity: line ${v.line} has a ${v.kind} placeholder in a ` +
|
|
153
|
+
`task bullet — a structural roadmap's steps name decided work: ${v.text.trim()}`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export { PLACEHOLDER_PATTERNS, COMPLEXITY_PAT };
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import * as fs from 'node:fs';
|
|
25
25
|
import * as path from 'node:path';
|
|
26
|
+
import process from 'node:process';
|
|
26
27
|
|
|
27
28
|
/** Raised when a gate's scope is empty — it cannot have checked anything. */
|
|
28
29
|
export class DeadScopeError extends Error {
|
|
@@ -49,6 +50,24 @@ export interface ScannedOptions {
|
|
|
49
50
|
* corpus is a real, expected state (an optional consumer surface that may
|
|
50
51
|
* genuinely be absent) — never to silence a moved root. The string is the
|
|
51
52
|
* reason and is printed, so it is reviewable in a diff.
|
|
53
|
+
*
|
|
54
|
+
* **Carry one of the three prefixes below.** A reason a reviewer cannot
|
|
55
|
+
* classify from the comment alone is how a justified exception becomes an
|
|
56
|
+
* allowlist of boilerplate (`road-to-gate-hardening-adoption`, AI council
|
|
57
|
+
* 2026-08-04). The prefix states which kind of emptiness is claimed:
|
|
58
|
+
*
|
|
59
|
+
* - `OPTIONAL_INPUT:` — the root belongs to a surface that may legitimately
|
|
60
|
+
* be absent in this project (a consumer tree, an opt-in pack).
|
|
61
|
+
* - `EMPTY_VALID:` — zero units IS the success state (`check_no_todos`: an
|
|
62
|
+
* empty tree genuinely has zero TODOs).
|
|
63
|
+
* - `WATCHLIST_DRIVEN:` — the gate has no corpus at all; it guards named
|
|
64
|
+
* files and its scope is asserted by {@link assertWatchlistResolves}.
|
|
65
|
+
*
|
|
66
|
+
* The operational test every reason must pass, applied from the comment
|
|
67
|
+
* alone: **if this gate's scan root were deleted, would the reason still
|
|
68
|
+
* make sense?** `check_no_todos` → yes. `check_frontmatter_valid` → no; no
|
|
69
|
+
* docs to validate is blindness, not cleanliness, and that gate must
|
|
70
|
+
* reclassify rather than claim `allowEmpty`.
|
|
52
71
|
*/
|
|
53
72
|
allowEmpty?: string;
|
|
54
73
|
}
|
|
@@ -75,6 +94,31 @@ export function assertScanned(opts: ScannedOptions): void {
|
|
|
75
94
|
);
|
|
76
95
|
}
|
|
77
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Assert a corpus gate's scope AND publish the count it just validated.
|
|
99
|
+
*
|
|
100
|
+
* The two halves of scope hardening drifted apart in practice: 14 gates emit
|
|
101
|
+
* the machine-readable `scanned:` line the coverage guard reads, 12 assert
|
|
102
|
+
* their scope, and only 8 do both. A gate that publishes a number it never
|
|
103
|
+
* asserted can print `scanned: 0` and exit green; a gate that asserts without
|
|
104
|
+
* publishing is invisible to `check_gate_coverage`. Splitting the two also
|
|
105
|
+
* lets the published number drift from the number that was validated — the
|
|
106
|
+
* invented-count failure that is mechanically undetectable downstream.
|
|
107
|
+
*
|
|
108
|
+
* One call closes both: the emitted number is, by construction, the number the
|
|
109
|
+
* assertion just accepted. Use this for any gate that should also carry a floor
|
|
110
|
+
* in `src/config/gate-coverage.yml`; plain {@link assertScanned} stays correct
|
|
111
|
+
* for a gate that only needs its scope guarded.
|
|
112
|
+
*
|
|
113
|
+
* The line goes to stdout unconditionally — a count only visible without
|
|
114
|
+
* `--quiet` is not a count, since CI passes `--quiet` (the `lint_handoffs`
|
|
115
|
+
* lesson, preserved here so no caller has to rediscover it).
|
|
116
|
+
*/
|
|
117
|
+
export function reportScanned(opts: ScannedOptions, write = process.stdout.write.bind(process.stdout)): void {
|
|
118
|
+
assertScanned(opts);
|
|
119
|
+
write(`scanned: ${String(opts.scanned)}\n`);
|
|
120
|
+
}
|
|
121
|
+
|
|
78
122
|
export interface WatchlistOptions {
|
|
79
123
|
/** Gate name, for the error message. */
|
|
80
124
|
gate: string;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read piped stdin completely — including payloads larger than the pipe buffer.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS (measured 2026-08-04, PR "take the unhardened-gate count from
|
|
5
|
+
* 189 to 4"): the CI step
|
|
6
|
+
*
|
|
7
|
+
* gh pr diff "$PR" | ./scripts-run src/scripts/check_no_new_legacy_path --stdin
|
|
8
|
+
*
|
|
9
|
+
* crashed with `EAGAIN: resource temporarily unavailable, read` at
|
|
10
|
+
* `fs.readFileSync(0)`. Not flaky — **reproducible on that PR and green on every
|
|
11
|
+
* PR before it.** A pipe holds ~64 KB; while the diff fit, the single read
|
|
12
|
+
* returned everything. That PR's diff is ~6,400 lines, so the writer is still
|
|
13
|
+
* filling the pipe when the reader arrives, and Node leaves the inherited fd 0
|
|
14
|
+
* in non-blocking mode: the read fails instead of waiting.
|
|
15
|
+
*
|
|
16
|
+
* A size-dependent gate failure is the same family of defect this suite's
|
|
17
|
+
* scan-scope work exists to kill — the gate works on the changes small enough
|
|
18
|
+
* not to matter and breaks on the ones big enough to. And the sibling
|
|
19
|
+
* workaround is worse than the crash: `check_release_pr_shape` wraps the same
|
|
20
|
+
* call in `catch { data = '' }`, so an oversized diff there reads as an empty
|
|
21
|
+
* one and the gate passes over nothing.
|
|
22
|
+
*
|
|
23
|
+
* So: retry on EAGAIN rather than swallow it, and never substitute an empty
|
|
24
|
+
* string for a failed read. A caller that gets `''` here got a genuinely empty
|
|
25
|
+
* stdin.
|
|
26
|
+
*/
|
|
27
|
+
import * as fs from 'node:fs';
|
|
28
|
+
|
|
29
|
+
const CHUNK = 64 * 1024;
|
|
30
|
+
/** Sleep between EAGAIN retries. Synchronous — these are sync CLI entry points. */
|
|
31
|
+
const RETRY_SLEEP_MS = 5;
|
|
32
|
+
/**
|
|
33
|
+
* Total patience before giving up, in retries. At 5 ms this is ~10 s, which is
|
|
34
|
+
* far beyond any `gh pr diff` and still terminates rather than spinning forever
|
|
35
|
+
* if fd 0 is a pipe nobody ever writes to.
|
|
36
|
+
*/
|
|
37
|
+
const MAX_RETRIES = 2000;
|
|
38
|
+
|
|
39
|
+
/** Sleep without a busy loop, in synchronous code. */
|
|
40
|
+
function _sleepSync(ms: number): void {
|
|
41
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Read all of fd 0 as UTF-8.
|
|
46
|
+
*
|
|
47
|
+
* @throws the underlying error for anything that is not EAGAIN, and after
|
|
48
|
+
* {@link MAX_RETRIES} consecutive EAGAINs — a caller must not be able to
|
|
49
|
+
* mistake a failed read for empty input.
|
|
50
|
+
*/
|
|
51
|
+
export function readStdinText(fd = 0): string {
|
|
52
|
+
const chunks: Buffer[] = [];
|
|
53
|
+
const buf = Buffer.alloc(CHUNK);
|
|
54
|
+
let retries = 0;
|
|
55
|
+
for (;;) {
|
|
56
|
+
let read: number;
|
|
57
|
+
try {
|
|
58
|
+
read = fs.readSync(fd, buf, 0, CHUNK, null);
|
|
59
|
+
retries = 0;
|
|
60
|
+
} catch (exc) {
|
|
61
|
+
const code = (exc as NodeJS.ErrnoException).code;
|
|
62
|
+
if (code === 'EAGAIN') {
|
|
63
|
+
retries += 1;
|
|
64
|
+
if (retries > MAX_RETRIES) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`readStdinText: stdin stayed unreadable across ${String(MAX_RETRIES)} retries ` +
|
|
67
|
+
`(~${String((MAX_RETRIES * RETRY_SLEEP_MS) / 1000)}s). Refusing to report an ` +
|
|
68
|
+
'empty read as empty input.',
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
_sleepSync(RETRY_SLEEP_MS);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
// EOF is how some platforms end a pipe read rather than returning 0.
|
|
75
|
+
if (code === 'EOF') break;
|
|
76
|
+
throw exc;
|
|
77
|
+
}
|
|
78
|
+
if (read === 0) break;
|
|
79
|
+
chunks.push(Buffer.from(buf.subarray(0, read)));
|
|
80
|
+
}
|
|
81
|
+
return Buffer.concat(chunks).toString('utf-8');
|
|
82
|
+
}
|
|
@@ -22,6 +22,8 @@ import * as path from 'node:path';
|
|
|
22
22
|
import process from 'node:process';
|
|
23
23
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
24
24
|
|
|
25
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
26
|
+
|
|
25
27
|
const _HERE = fileURLToPath(import.meta.url);
|
|
26
28
|
|
|
27
29
|
// src/scripts/audit_adr_coverage.ts → parents[2] of the .py file is repo root.
|
|
@@ -251,14 +253,40 @@ export function render_area_readme(area: string, meta: AreaMeta, adrs: AdrEntry[
|
|
|
251
253
|
return lines.join('\n') + '\n';
|
|
252
254
|
}
|
|
253
255
|
|
|
256
|
+
/**
|
|
257
|
+
* A vanished `docs/adrs/` makes every area report zero ADRs — "missing
|
|
258
|
+
* bootstrap" across the board, which both modes treat as a warning and exit 0.
|
|
259
|
+
* Exit 1 is the hard-failure code (`cmd_check`); 2 is reserved for argparse
|
|
260
|
+
* usage errors.
|
|
261
|
+
*/
|
|
262
|
+
function _dead_adr_scope(scanned: number): number | null {
|
|
263
|
+
try {
|
|
264
|
+
assertScanned({
|
|
265
|
+
gate: 'audit_adr_coverage',
|
|
266
|
+
scanned,
|
|
267
|
+
units: 'ADR file(s)',
|
|
268
|
+
roots: ['docs/adrs'],
|
|
269
|
+
});
|
|
270
|
+
return null;
|
|
271
|
+
} catch (exc) {
|
|
272
|
+
if (exc instanceof DeadScopeError) {
|
|
273
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
274
|
+
return 1;
|
|
275
|
+
}
|
|
276
|
+
throw exc;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
254
280
|
export function cmd_report(): number {
|
|
255
281
|
process.stdout.write('## ADR coverage report\n');
|
|
256
282
|
process.stdout.write('\n');
|
|
257
283
|
process.stdout.write('| Area | Contract | ADRs | README | Status |\n');
|
|
258
284
|
process.stdout.write('|---|---|---:|:---:|---|\n');
|
|
259
285
|
let missingBootstrap = 0;
|
|
286
|
+
let totalAdrs = 0;
|
|
260
287
|
for (const [area, meta] of Object.entries(AREAS)) {
|
|
261
288
|
const [adrs] = scan_area(area);
|
|
289
|
+
totalAdrs += adrs.length;
|
|
262
290
|
const readme = _exists(path.join(ADR_ROOT, area, 'README.md')) ? '✅' : '—';
|
|
263
291
|
const contractPresent = _exists(_contract_path(meta));
|
|
264
292
|
const status = adrs.length > 0 ? 'ok' : 'missing bootstrap';
|
|
@@ -268,6 +296,10 @@ export function cmd_report(): number {
|
|
|
268
296
|
const contractCell = contractPresent ? meta.contract : `_${meta.contract}_ (no contract)`;
|
|
269
297
|
process.stdout.write(`| \`${area}\` | ${contractCell} | ${adrs.length} | ${readme} | ${status} |\n`);
|
|
270
298
|
}
|
|
299
|
+
const dead = _dead_adr_scope(totalAdrs);
|
|
300
|
+
if (dead !== null) {
|
|
301
|
+
return dead;
|
|
302
|
+
}
|
|
271
303
|
process.stdout.write('\n');
|
|
272
304
|
process.stdout.write(
|
|
273
305
|
`BASELINE: ${Object.keys(AREAS).length} canonical areas · ${missingBootstrap} missing bootstrap ADR(s)\n`,
|
|
@@ -278,6 +310,7 @@ export function cmd_report(): number {
|
|
|
278
310
|
export function cmd_check(): number {
|
|
279
311
|
let hard = 0;
|
|
280
312
|
let warn = 0;
|
|
313
|
+
let totalAdrs = 0;
|
|
281
314
|
for (const dir of unlistedAreaDirs(ADR_ROOT)) {
|
|
282
315
|
process.stderr.write(
|
|
283
316
|
`❌ ${dir}/: area directory exists on disk but is absent from AREAS — it is not audited at all. ` +
|
|
@@ -287,6 +320,7 @@ export function cmd_check(): number {
|
|
|
287
320
|
}
|
|
288
321
|
for (const [area, meta] of Object.entries(AREAS)) {
|
|
289
322
|
const [adrs, errs] = scan_area(area);
|
|
323
|
+
totalAdrs += adrs.length;
|
|
290
324
|
for (const e of errs) {
|
|
291
325
|
process.stderr.write(`❌ ${e}\n`);
|
|
292
326
|
hard += 1;
|
|
@@ -300,6 +334,10 @@ export function cmd_check(): number {
|
|
|
300
334
|
warn += 1;
|
|
301
335
|
}
|
|
302
336
|
}
|
|
337
|
+
const dead = _dead_adr_scope(totalAdrs);
|
|
338
|
+
if (dead !== null) {
|
|
339
|
+
return dead;
|
|
340
|
+
}
|
|
303
341
|
process.stdout.write(`BASELINE: ${hard} hard fail(s) · ${warn} warn(s)\n`);
|
|
304
342
|
return hard ? 1 : 0;
|
|
305
343
|
}
|
|
@@ -28,6 +28,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
28
28
|
import { parse as parseYaml } from 'yaml';
|
|
29
29
|
|
|
30
30
|
import { artefact_roots } from './_lib/agent_src.js';
|
|
31
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
31
32
|
|
|
32
33
|
const _HERE = fileURLToPath(import.meta.url);
|
|
33
34
|
// src/scripts/audit_auto_rules.py → parent.parent.parent == repo root.
|
|
@@ -404,9 +405,22 @@ function _ruleToJson(r: RuleEntry): Record<string, Json> {
|
|
|
404
405
|
}
|
|
405
406
|
|
|
406
407
|
export function main(): number {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
408
|
+
// The scanned unit is every source rule, not the `type: auto` subset
|
|
409
|
+
// `collect()` keeps: over a vanished rules/ tree "no auto-rules" still reads
|
|
410
|
+
// true, and the budget report would be regenerated with rule_count 0.
|
|
411
|
+
try {
|
|
412
|
+
assertScanned({
|
|
413
|
+
gate: 'audit_auto_rules',
|
|
414
|
+
scanned: _src_rule_paths().length,
|
|
415
|
+
units: 'source rule file(s)',
|
|
416
|
+
roots: artefact_roots().map((r) => `${_relPosix(r, REPO_ROOT) || '.'}/rules`),
|
|
417
|
+
});
|
|
418
|
+
} catch (exc) {
|
|
419
|
+
if (exc instanceof DeadScopeError) {
|
|
420
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
421
|
+
return 1;
|
|
422
|
+
}
|
|
423
|
+
throw exc;
|
|
410
424
|
}
|
|
411
425
|
const rules = collect();
|
|
412
426
|
fs.mkdirSync(REPORT_DIR, { recursive: true });
|
|
@@ -27,6 +27,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
27
27
|
import { parse as parseYaml } from 'yaml';
|
|
28
28
|
|
|
29
29
|
import { SRC_DOMAINS } from './_lib/agent_src.js';
|
|
30
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
30
31
|
|
|
31
32
|
const _HERE = fileURLToPath(import.meta.url);
|
|
32
33
|
|
|
@@ -974,8 +975,50 @@ export function grown_packs(baseline: string, commands: Command[]): Map<string,
|
|
|
974
975
|
return grew;
|
|
975
976
|
}
|
|
976
977
|
|
|
978
|
+
/**
|
|
979
|
+
* Scope guard shared by all three modes.
|
|
980
|
+
*
|
|
981
|
+
* `_rglobMdSorted` yields nothing for a root that does not exist, and every
|
|
982
|
+
* mode's verdict is a comparison over the collected commands — so a moved
|
|
983
|
+
* `src/domains` leaves the per-pack budget trivially satisfied, the overlap
|
|
984
|
+
* scan pair-less, and the surface report regenerated with `total: 0`. Only
|
|
985
|
+
* `main`'s `_exists(args.root)` guard covered that, and it does not see a
|
|
986
|
+
* present-but-command-less tree.
|
|
987
|
+
*
|
|
988
|
+
* @returns the exit code to return, or null when the scope is live.
|
|
989
|
+
*/
|
|
990
|
+
function _dead_scope(commands: readonly Command[], roots: readonly string[]): number | null {
|
|
991
|
+
try {
|
|
992
|
+
assertScanned({
|
|
993
|
+
gate: 'audit_command_surface',
|
|
994
|
+
scanned: commands.length,
|
|
995
|
+
units: 'command file(s)',
|
|
996
|
+
roots,
|
|
997
|
+
});
|
|
998
|
+
return null;
|
|
999
|
+
} catch (exc) {
|
|
1000
|
+
if (exc instanceof DeadScopeError) {
|
|
1001
|
+
// Exit 2 — this file's existing "the root is not usable" code
|
|
1002
|
+
// (`error: <root> does not exist`), not 1, which means a pack
|
|
1003
|
+
// exceeded its budget.
|
|
1004
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
1005
|
+
return 2;
|
|
1006
|
+
}
|
|
1007
|
+
throw exc;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/** Repo-relative form of the roots `collect_all` walks, for the scope message. */
|
|
1012
|
+
function _command_roots_rel(): string[] {
|
|
1013
|
+
return _command_roots().map((r) => _relPosix(r, REPO_ROOT));
|
|
1014
|
+
}
|
|
1015
|
+
|
|
977
1016
|
export function check_new_budget(baseline: string, quiet: boolean): number {
|
|
978
1017
|
const commands = collect_all();
|
|
1018
|
+
const dead = _dead_scope(commands, _command_roots_rel());
|
|
1019
|
+
if (dead !== null) {
|
|
1020
|
+
return dead;
|
|
1021
|
+
}
|
|
979
1022
|
const audit = build_budget_audit(commands, load_size_classes());
|
|
980
1023
|
const byPack = new Map<string, PackEntry>();
|
|
981
1024
|
for (const p of audit.packs) {
|
|
@@ -1155,6 +1198,10 @@ export function main(argv: string[] | null = null): number {
|
|
|
1155
1198
|
|
|
1156
1199
|
if (args.budget) {
|
|
1157
1200
|
const commands = collect_all();
|
|
1201
|
+
const dead = _dead_scope(commands, _command_roots_rel());
|
|
1202
|
+
if (dead !== null) {
|
|
1203
|
+
return dead;
|
|
1204
|
+
}
|
|
1158
1205
|
const audit = build_budget_audit(commands, load_size_classes());
|
|
1159
1206
|
fs.writeFileSync(OUT_BUDGET_JSON, _jsonDumpsIndent2(audit), 'utf-8');
|
|
1160
1207
|
fs.writeFileSync(OUT_BUDGET_MD, render_budget_md(audit), 'utf-8');
|
|
@@ -1177,6 +1224,10 @@ export function main(argv: string[] | null = null): number {
|
|
|
1177
1224
|
}
|
|
1178
1225
|
|
|
1179
1226
|
const commands = collect(args.root);
|
|
1227
|
+
const dead = _dead_scope(commands, [_relPosix(args.root, REPO_ROOT)]);
|
|
1228
|
+
if (dead !== null) {
|
|
1229
|
+
return dead;
|
|
1230
|
+
}
|
|
1180
1231
|
const pairs = find_overlap_pairs(commands);
|
|
1181
1232
|
|
|
1182
1233
|
fs.writeFileSync(
|
|
@@ -32,6 +32,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
32
32
|
import { parse as parseYaml } from 'yaml';
|
|
33
33
|
|
|
34
34
|
import * as token_count from './_lib/token_count.js';
|
|
35
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
35
36
|
import { SRC_AGENT, SRC_DOMAINS } from './_lib/agent_src.js';
|
|
36
37
|
import * as projectThinRules from './project_thin_rules.js';
|
|
37
38
|
|
|
@@ -304,14 +305,30 @@ function _catalog(globPat: string): Measure {
|
|
|
304
305
|
return m;
|
|
305
306
|
}
|
|
306
307
|
|
|
308
|
+
const CATALOG_GLOBS = {
|
|
309
|
+
skills_projected: '.claude/skills/*/SKILL.md',
|
|
310
|
+
skills_core_source: 'src/skills/*/SKILL.md',
|
|
311
|
+
commands_core_source: 'src/domains/*/**/command.md',
|
|
312
|
+
} as const;
|
|
313
|
+
|
|
307
314
|
export function description_catalog(): Record<string, Measure> {
|
|
308
315
|
return {
|
|
309
|
-
skills_projected: _catalog(
|
|
310
|
-
skills_core_source: _catalog(
|
|
311
|
-
commands_core_source: _catalog(
|
|
316
|
+
skills_projected: _catalog(CATALOG_GLOBS.skills_projected),
|
|
317
|
+
skills_core_source: _catalog(CATALOG_GLOBS.skills_core_source),
|
|
318
|
+
commands_core_source: _catalog(CATALOG_GLOBS.commands_core_source),
|
|
312
319
|
};
|
|
313
320
|
}
|
|
314
321
|
|
|
322
|
+
/**
|
|
323
|
+
* Files the catalogs glob — counted UNFILTERED, unlike the `entries` the
|
|
324
|
+
* measurement keeps (only those carrying a description). A vanished skills or
|
|
325
|
+
* commands tree must read as zero files, not as "nothing declares a
|
|
326
|
+
* description", which is indistinguishable from a clean tree at zero tokens.
|
|
327
|
+
*/
|
|
328
|
+
function catalog_file_count(): number {
|
|
329
|
+
return Object.values(CATALOG_GLOBS).reduce((n, pat) => n + _glob(pat).length, 0);
|
|
330
|
+
}
|
|
331
|
+
|
|
315
332
|
interface LongestRule {
|
|
316
333
|
id: string;
|
|
317
334
|
tokens_gpt: number;
|
|
@@ -700,6 +717,25 @@ function parse_args(argv: string[]): ParsedArgs {
|
|
|
700
717
|
export function main(argv: string[] | null = null): number {
|
|
701
718
|
const args = parse_args(argv ?? process.argv.slice(2));
|
|
702
719
|
|
|
720
|
+
// Every budget in BUDGETS is a token count over these catalogs: an empty
|
|
721
|
+
// catalog measures zero tokens and passes every cap, so the surface has to
|
|
722
|
+
// be proved non-empty before the caps mean anything. Exit 1 is the gate's
|
|
723
|
+
// failure code (`--fail-if-over-budget`); 2 is reserved for usage errors.
|
|
724
|
+
try {
|
|
725
|
+
assertScanned({
|
|
726
|
+
gate: 'audit_initial_context',
|
|
727
|
+
scanned: catalog_file_count(),
|
|
728
|
+
units: 'catalog file(s)',
|
|
729
|
+
roots: Object.values(CATALOG_GLOBS),
|
|
730
|
+
});
|
|
731
|
+
} catch (exc) {
|
|
732
|
+
if (exc instanceof DeadScopeError) {
|
|
733
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
734
|
+
return 1;
|
|
735
|
+
}
|
|
736
|
+
throw exc;
|
|
737
|
+
}
|
|
738
|
+
|
|
703
739
|
const data = build();
|
|
704
740
|
|
|
705
741
|
if (args.fail_if_over_budget) {
|
|
@@ -21,6 +21,8 @@ import * as path from 'node:path';
|
|
|
21
21
|
import process from 'node:process';
|
|
22
22
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
23
23
|
|
|
24
|
+
import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
|
|
25
|
+
|
|
24
26
|
const _HERE = fileURLToPath(import.meta.url);
|
|
25
27
|
|
|
26
28
|
// src/scripts/audit_mcp_tools.ts → parent.parent.parent of the .py file = repo root.
|
|
@@ -228,6 +230,29 @@ function parse_args(argv: string[]): ParsedArgs {
|
|
|
228
230
|
export function main(argv: string[] | null = null): number {
|
|
229
231
|
const args = parse_args(argv ?? process.argv.slice(2));
|
|
230
232
|
|
|
233
|
+
// This generator walks no tree — it reads two named sources, and
|
|
234
|
+
// `_index_handlers` / `_index_catalog_lines` swallow a read failure as an
|
|
235
|
+
// empty index, so a moved `tools.ts` renders every tool `_stub-only_`
|
|
236
|
+
// rather than reporting that half the input vanished.
|
|
237
|
+
try {
|
|
238
|
+
assertWatchlistResolves({
|
|
239
|
+
gate: 'audit_mcp_tools',
|
|
240
|
+
candidates: [
|
|
241
|
+
path.posix.join('src', 'scripts', 'mcp_server', 'consumer_tool_catalog.json'),
|
|
242
|
+
path.posix.join('src', 'scripts', 'mcp_server', 'tools.ts'),
|
|
243
|
+
],
|
|
244
|
+
repoRoot: ROOT,
|
|
245
|
+
});
|
|
246
|
+
} catch (e) {
|
|
247
|
+
if (e instanceof DeadScopeError) {
|
|
248
|
+
// 2 (the usage / could-not-run code) over 1, which this CLI
|
|
249
|
+
// documents as "the inventory drifted from the generator".
|
|
250
|
+
process.stderr.write(`❌ ${e.message}\n`);
|
|
251
|
+
return 2;
|
|
252
|
+
}
|
|
253
|
+
throw e;
|
|
254
|
+
}
|
|
255
|
+
|
|
231
256
|
const catalog = JSON.parse(fs.readFileSync(CATALOG, 'utf-8')) as Catalog;
|
|
232
257
|
const handlers = _index_handlers();
|
|
233
258
|
const cat_lines = _index_catalog_lines();
|
|
@@ -28,6 +28,8 @@ import * as fs from 'node:fs';
|
|
|
28
28
|
import * as path from 'node:path';
|
|
29
29
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
30
30
|
|
|
31
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
32
|
+
|
|
31
33
|
const _HERE = fileURLToPath(import.meta.url);
|
|
32
34
|
// src/scripts/audit_overlap.ts → parents[2] is the repo root (mirrors
|
|
33
35
|
// `Path(__file__).resolve().parent.parent.parent` in the .py).
|
|
@@ -381,7 +383,28 @@ export function main(): number {
|
|
|
381
383
|
return 1;
|
|
382
384
|
}
|
|
383
385
|
const data = JSON.parse(fs.readFileSync(AUDIT_JSON, 'utf-8')) as { rules: RuleEntry[] };
|
|
384
|
-
|
|
386
|
+
// The existence check above does not cover a present-but-ruleless input:
|
|
387
|
+
// an upstream `audit_auto_rules` that itself scanned nothing yields a
|
|
388
|
+
// "0 pairs flagged" report, which is the answer a real clean run gives.
|
|
389
|
+
// The unit is the rules read, never the pairs flagged.
|
|
390
|
+
const rules = Array.isArray(data.rules) ? data.rules : [];
|
|
391
|
+
try {
|
|
392
|
+
assertScanned({
|
|
393
|
+
gate: 'audit_overlap',
|
|
394
|
+
scanned: rules.length,
|
|
395
|
+
units: 'rule(s)',
|
|
396
|
+
roots: ['agents/reports/auto-rules-audit.json'],
|
|
397
|
+
});
|
|
398
|
+
} catch (exc) {
|
|
399
|
+
if (exc instanceof DeadScopeError) {
|
|
400
|
+
// 1 is this gate's only non-zero code — same one the missing-input
|
|
401
|
+
// path uses, and for the same reason: it could not run.
|
|
402
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
403
|
+
return 1;
|
|
404
|
+
}
|
|
405
|
+
throw exc;
|
|
406
|
+
}
|
|
407
|
+
const pairs = analyse(rules);
|
|
385
408
|
// Build the JSON payload with the two `round(x, 3)` fields wrapped as
|
|
386
409
|
// PyFloat so 0.0 / 0.5 / 1.0 render with a trailing `.0` like json.dumps.
|
|
387
410
|
const pairsJson: Json = pairs.map(
|