@event4u/agent-config 9.20.0 → 9.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +86 -0
- package/dist/agent-src/commands/worktree/cleanup.md +17 -0
- package/dist/agent-src/commands/worktree/status.md +6 -0
- package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +22 -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 +5 -5
- 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 +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- 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/bench_ab_activation.ts +368 -0
- 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/bench_ab_task_runner.ts +18 -0
- package/src/scripts/bench_ab_v2_run.ts +147 -6
- package/src/scripts/bench_ab_v2_stats.ts +74 -2
- 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/worktree_cleanup_check.ts +319 -4
- package/src/scripts/check_bite_sized_granularity.ts +0 -116
- package/src/scripts/lint_workspace_boundary.ts +0 -267
|
@@ -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(
|
|
@@ -25,6 +25,7 @@ import * as path from 'node:path';
|
|
|
25
25
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
26
26
|
|
|
27
27
|
import { artefact_roots } from './_lib/agent_src.js';
|
|
28
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
28
29
|
|
|
29
30
|
export const MIN_LENGTH = 150;
|
|
30
31
|
// Mirrors scripts/skill_linter.py `description_too_long` threshold.
|
|
@@ -386,6 +387,24 @@ export function main(argv: string[] | null = null): number {
|
|
|
386
387
|
for (const r of roots) {
|
|
387
388
|
findings.push(...collect_findings(r));
|
|
388
389
|
}
|
|
390
|
+
// The roots check above only proves a `skills/` DIRECTORY exists; roots that
|
|
391
|
+
// hold no SKILL.md still render "Audited 0 skills … ✅ All descriptions look
|
|
392
|
+
// reasonable." Exit 2 is this gate's only failure code, and it already
|
|
393
|
+
// covers the sibling "no skills/ directories found" case.
|
|
394
|
+
try {
|
|
395
|
+
assertScanned({
|
|
396
|
+
gate: 'audit_skill_descriptions',
|
|
397
|
+
scanned: findings.length,
|
|
398
|
+
units: 'SKILL.md file(s)',
|
|
399
|
+
roots,
|
|
400
|
+
});
|
|
401
|
+
} catch (exc) {
|
|
402
|
+
if (exc instanceof DeadScopeError) {
|
|
403
|
+
process.stderr.write(`error: ${exc.message}\n`);
|
|
404
|
+
return 2;
|
|
405
|
+
}
|
|
406
|
+
throw exc;
|
|
407
|
+
}
|
|
389
408
|
if (args.json) {
|
|
390
409
|
process.stdout.write(pyJsonDumpsIndent2(findings.map(_asdict)) + '\n');
|
|
391
410
|
} else {
|
|
@@ -2,9 +2,22 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Platform-agnostic hook for the `verify-before-complete` rule.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Named `before_complete_hook`, not `verify_before_complete_hook`: `verify_` is
|
|
6
|
+
* one of the gate-shaped prefixes in `_lib/gate_population.ts`, so the old name
|
|
7
|
+
* put an observability hook into the gate population, where the scan-scope
|
|
8
|
+
* ratchet counted it as a gate that cannot assert a scan scope (it scans no
|
|
9
|
+
* corpus — its only inputs are its stdin envelope and the state file it writes).
|
|
10
|
+
* AI council 2026-08-05 rejected the alternative of excluding `_hook.ts` in the
|
|
11
|
+
* population filter as population-shrinking: `.d.ts` / `.test.ts` are excluded
|
|
12
|
+
* because the language and the test runner make them structurally
|
|
13
|
+
* non-executable, whereas "a hook never blocks" is an operational property you
|
|
14
|
+
* can only learn by reading `hook_manifest.yaml` — and manifest-driven
|
|
15
|
+
* classification was already rejected once. Renaming keeps classification
|
|
16
|
+
* structural and needs no exclusion rule.
|
|
17
|
+
*
|
|
18
|
+
* Ported from the retired Python `src/scripts/verify_before_complete_hook.py`
|
|
19
|
+
* (ADR-200 — Python→TS migration, Phase 6 / hooks). Public API mirrors the
|
|
20
|
+
* Python module exactly (snake_case kept deliberately — fidelity over TS idiom).
|
|
8
21
|
*
|
|
9
22
|
* Records observable evidence that a verification command (tests, quality
|
|
10
23
|
* tools, build) ran. The rule body cites the resulting state file as the
|
|
@@ -194,6 +194,12 @@ interface RunResult {
|
|
|
194
194
|
tokens?: number;
|
|
195
195
|
/** Empty object `{}` on dry-run (key absent in Python); breakdown otherwise. */
|
|
196
196
|
tokens_breakdown?: TokensBreakdown | Record<string, never>;
|
|
197
|
+
/**
|
|
198
|
+
* Model ids the envelope actually billed (`modelUsage` keys) — the only
|
|
199
|
+
* place a run states which model answered it. Empty when the envelope
|
|
200
|
+
* omits the block. Feeds the delta-#3 model-id check.
|
|
201
|
+
*/
|
|
202
|
+
models_seen?: string[];
|
|
197
203
|
errored?: boolean;
|
|
198
204
|
num_turns?: number;
|
|
199
205
|
subtype?: string;
|
|
@@ -295,6 +301,7 @@ export function run_live(
|
|
|
295
301
|
const returncode = result.status ?? -1;
|
|
296
302
|
let transcript: string = stdout;
|
|
297
303
|
let tokens = 0;
|
|
304
|
+
let modelsSeen: string[] = [];
|
|
298
305
|
let isError = false;
|
|
299
306
|
let errReason = 'ok';
|
|
300
307
|
let numTurns = 0;
|
|
@@ -337,6 +344,16 @@ export function run_live(
|
|
|
337
344
|
breakdown.output_tokens +
|
|
338
345
|
breakdown.cache_read_input_tokens +
|
|
339
346
|
breakdown.cache_creation_input_tokens;
|
|
347
|
+
// `modelUsage` is keyed by the model ids the run actually billed. Record
|
|
348
|
+
// them unconditionally: the delta-#3 check needs the id even on a run
|
|
349
|
+
// whose top-level `usage` was fine, and a mid-sweep model swap is exactly
|
|
350
|
+
// the case where the totals look ordinary.
|
|
351
|
+
modelsSeen = Object.keys(
|
|
352
|
+
(_isPlainObj(obj['modelUsage']) ? (obj['modelUsage'] as Record<string, unknown>) : {}) as Record<
|
|
353
|
+
string,
|
|
354
|
+
unknown
|
|
355
|
+
>,
|
|
356
|
+
);
|
|
340
357
|
// The top-level `usage` block is zeroed on a budget-capped / errored run
|
|
341
358
|
// (and unreliable even on some completions). `modelUsage` carries the
|
|
342
359
|
// authoritative per-model counts — sum it as the fallback so token deltas
|
|
@@ -387,6 +404,7 @@ export function run_live(
|
|
|
387
404
|
wall_time_seconds: _pyRound(duration, 3),
|
|
388
405
|
tokens,
|
|
389
406
|
tokens_breakdown: breakdown,
|
|
407
|
+
models_seen: modelsSeen,
|
|
390
408
|
errored: isError || returncode !== 0,
|
|
391
409
|
num_turns: numTurns,
|
|
392
410
|
subtype,
|