@event4u/agent-config 9.14.0 → 9.16.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 +118 -99
- package/README.md +8 -0
- package/dist/agent-src/commands/agents/audit.md +7 -7
- package/dist/agent-src/commands/agents/optimize.md +1 -1
- package/dist/agent-src/commands/analyze/reference-repo.md +1 -1
- package/dist/agent-src/commands/chat-history.md +1 -0
- package/dist/agent-src/commands/context.md +1 -0
- package/dist/agent-src/commands/council.md +1 -1
- package/dist/agent-src/commands/ghostwriter.md +1 -1
- package/dist/agent-src/commands/memory.md +2 -1
- package/dist/agent-src/commands/module.md +1 -0
- package/dist/agent-src/commands/optimize/agents-dir.md +1 -1
- package/dist/agent-src/commands/optimize/deep.md +5 -2
- package/dist/agent-src/commands/optimize/skills.md +5 -5
- package/dist/agent-src/commands/override.md +1 -0
- package/dist/agent-src/commands/prediction-pool.md +4 -4
- package/dist/agent-src/commands/research/deep.md +1 -1
- package/dist/agent-src/commands/roadmap.md +2 -1
- package/dist/agent-src/commands/rule-compliance-audit.md +1 -1
- package/dist/agent-src/commands/tdd.md +1 -0
- package/dist/agent-src/commands/tests.md +1 -0
- package/dist/agent-src/commands/upstream-contribute.md +5 -5
- package/dist/agent-src/commands/video/from-script.md +1 -1
- package/dist/agent-src/commands/video/from-song.md +1 -1
- package/dist/agent-src/commands/video/scene.md +1 -1
- package/dist/agent-src/commands/video/stitch.md +1 -1
- package/dist/agent-src/commands/video/storyboard.md +1 -1
- package/dist/agent-src/commands/worktree/create.md +2 -1
- package/dist/agent-src/contexts/augment-infrastructure.md +1 -1
- package/dist/agent-src/contexts/contracts/artifact-engagement-flow.md +5 -5
- package/dist/agent-src/contexts/contracts/command-suggestion-flow.md +3 -3
- package/dist/agent-src/contexts/contracts/emergency-triage-block.md +2 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +4 -3
- package/dist/agent-src/contexts/execution/subagent-routing.md +19 -0
- package/dist/agent-src/contexts/override-system.md +1 -1
- package/dist/agent-src/ghostwriter/fictional-fixture-v1.md +1 -1
- package/dist/agent-src/rules/augment-edit-discipline.md +1 -1
- package/dist/agent-src/rules/autonomous-execution.md +2 -1
- package/dist/agent-src/rules/brand-consistency.md +1 -0
- package/dist/agent-src/rules/brand-source-of-truth.md +1 -0
- package/dist/agent-src/rules/code-comment-discipline.md +2 -0
- package/dist/agent-src/rules/communication-through-line.md +1 -0
- package/dist/agent-src/rules/content-quoting-floor.md +1 -0
- package/dist/agent-src/rules/cross-source-consistency.md +0 -1
- package/dist/agent-src/rules/downstream-changes.md +1 -0
- package/dist/agent-src/rules/external-code-graph-interop.md +1 -0
- package/dist/agent-src/rules/external-reference-deep-dive.md +1 -0
- package/dist/agent-src/rules/fast-path-marker-visibility.md +1 -0
- package/dist/agent-src/rules/finance-safety-floor.md +0 -1
- package/dist/agent-src/rules/icon-consistency.md +1 -0
- package/dist/agent-src/rules/image-likeness-and-rights.md +1 -0
- package/dist/agent-src/rules/improve-before-implement.md +1 -1
- package/dist/agent-src/rules/invite-challenge.md +1 -0
- package/dist/agent-src/rules/lethal-trifecta-guard.md +1 -0
- package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +1 -0
- package/dist/agent-src/rules/markdown-safe-codeblocks.md +1 -0
- package/dist/agent-src/rules/media-governance-routing.md +4 -3
- package/dist/agent-src/rules/media-sync-ground-truth.md +1 -0
- package/dist/agent-src/rules/no-attribution-footers.md +1 -0
- package/dist/agent-src/rules/no-decorative-emojis-in-git-surfaces.md +1 -0
- package/dist/agent-src/rules/no-pr-progress-comments.md +1 -0
- package/dist/agent-src/rules/onboarding-gate.md +1 -0
- package/dist/agent-src/rules/output-discipline.md +1 -0
- package/dist/agent-src/rules/prefer-enums-over-literals.md +1 -0
- package/dist/agent-src/rules/preservation-guard.md +1 -0
- package/dist/agent-src/rules/question-not-instruction.md +1 -0
- package/dist/agent-src/rules/roadmap-ci-steps-policy.md +1 -0
- package/dist/agent-src/rules/role-mode-adherence.md +1 -0
- package/dist/agent-src/rules/runtime-safety.md +1 -0
- package/dist/agent-src/rules/security-sensitive-stop.md +1 -0
- package/dist/agent-src/rules/session-canary.md +9 -2
- package/dist/agent-src/rules/source-confidentiality.md +1 -0
- package/dist/agent-src/rules/source-of-truth.md +1 -0
- package/dist/agent-src/rules/spreadsheet-source-quality.md +1 -0
- package/dist/agent-src/rules/think-before-action.md +1 -0
- package/dist/agent-src/rules/token-budget-discipline.md +1 -0
- package/dist/agent-src/rules/token-efficiency.md +1 -0
- package/dist/agent-src/rules/token-optimizer-maintenance.md +5 -5
- package/dist/agent-src/rules/tool-safety.md +1 -0
- package/dist/agent-src/rules/untrusted-input-defense.md +2 -0
- package/dist/agent-src/rules/user-interaction.md +1 -0
- package/dist/agent-src/rules/user-interrupt-priority.md +1 -0
- package/dist/agent-src/skills/adr-create/SKILL.md +1 -1
- package/dist/agent-src/skills/check-refs/SKILL.md +1 -1
- package/dist/agent-src/skills/competitive-positioning/SKILL.md +1 -1
- package/dist/agent-src/skills/condense-memory/SKILL.md +1 -1
- package/dist/agent-src/skills/context-authoring/SKILL.md +1 -1
- package/dist/agent-src/skills/copilot-agents-optimization/SKILL.md +1 -1
- package/dist/agent-src/skills/description-assist/SKILL.md +1 -1
- package/dist/agent-src/skills/github-ci/SKILL.md +1 -0
- package/dist/agent-src/skills/guideline-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/lint-skills/SKILL.md +1 -1
- package/dist/agent-src/skills/readme-reviewer/SKILL.md +1 -1
- package/dist/agent-src/skills/roadmap-management/SKILL.md +8 -1
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-writing/SKILL.md +4 -4
- package/dist/agent-src/skills/token-optimizer/SKILL.md +4 -4
- package/dist/agent-src/skills/upstream-contribute/SKILL.md +6 -6
- package/dist/agent-src/skills/using-git-worktrees/SKILL.md +25 -0
- package/dist/agent-src/templates/AGENTS.md +1 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/command.md +1 -1
- package/dist/agent-src/templates/copilot-instructions.md +3 -3
- package/dist/agent-src/templates/rule.md +2 -2
- package/dist/agent-src/templates/scripts/work_engine/orchestration.ts +1 -1
- package/dist/agent-src/templates/skill-archive-note.md +5 -5
- package/dist/agent-src/templates/skill.md +1 -1
- package/dist/cli/agent-config.js +66 -19
- package/dist/cli/agent-config.js.map +1 -1
- package/dist/cli/main.js +9 -4
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/mcp/content.js +1 -1
- package/dist/cli/mcp/content.js.map +1 -1
- package/dist/cli/python/workspace_drive.js +2 -0
- package/dist/cli/python/workspace_drive.js.map +1 -1
- package/dist/cli/python/workspace_drive_health.js +2 -0
- package/dist/cli/python/workspace_drive_health.js.map +1 -1
- package/dist/cli/python/workspace_hosts.js +2 -0
- package/dist/cli/python/workspace_hosts.js.map +1 -1
- package/dist/cli/python/workspace_sessions.js +2 -0
- package/dist/cli/python/workspace_sessions.js.map +1 -1
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-A3HJ6NTI.js → chunk-63WSP6YZ.js} +2 -1
- package/dist/cli-delegate/{chunk-EJYIGZ3T.js → chunk-BKO2AZ7Z.js} +1 -1
- package/dist/cli-delegate/{chunk-GCVFLBP2.js → chunk-H574KOUH.js} +338 -494
- package/dist/cli-delegate/chunk-NXFWFAYV.js +339 -0
- package/dist/cli-delegate/{chunk-TEGJU2JX.js → chunk-ZXXJLL5K.js} +2 -2
- package/dist/cli-delegate/cmd_conformance.js +29 -23
- package/dist/cli-delegate/cmd_doctor.js +3 -3
- package/dist/cli-delegate/cmd_export.js +4 -4
- package/dist/cli-delegate/cmd_preflight.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +4 -4
- package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
- package/dist/cli-delegate/cmd_sync.js +4 -4
- package/dist/cli-delegate/cmd_uninstall.js +4 -4
- package/dist/cli-delegate/cmd_validate.js +4 -4
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +127 -95
- 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 +729 -430
- package/dist/install/install.mjs +52 -11
- package/dist/install/rule_scope.js +18 -2
- package/dist/install/rule_scope.js.map +1 -1
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +518 -411
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/claude_settings_hooks.js +30 -5
- package/dist/scripts/_lib/claude_settings_hooks.js.map +1 -1
- package/dist/scripts/_lib/scoped_projection.js +223 -0
- package/dist/scripts/_lib/scoped_projection.js.map +1 -0
- package/dist/scripts/_lib/surface_tiers.js +125 -0
- package/dist/scripts/_lib/surface_tiers.js.map +1 -0
- package/dist/scripts/_lib/token_count.js +166 -0
- package/dist/scripts/_lib/token_count.js.map +1 -0
- package/dist/scripts/_lib/trigger_eval_floors.js +8 -0
- package/dist/scripts/_lib/trigger_eval_floors.js.map +1 -1
- package/dist/scripts/condense.js +139 -9
- package/dist/scripts/condense.js.map +1 -1
- package/dist/scripts/project_thin_rules.js +450 -0
- package/dist/scripts/project_thin_rules.js.map +1 -0
- package/dist/server/routes/workspace.js +3 -1
- package/dist/server/routes/workspace.js.map +1 -1
- package/dist/server/schemas/settings.js +9 -3
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/shared/userMd/utils.js +1 -1
- package/dist/shared/userMd/utils.js.map +1 -1
- package/dist/ui/assets/index-DmR0mbk4.js +26 -0
- package/dist/ui/assets/index-DmR0mbk4.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
- package/package.json +14 -14
- package/src/agent-src/templates/AGENTS.md +1 -1
- package/src/agent-src/templates/scripts/work_engine/orchestration.ts +1 -1
- package/src/config/agent-settings.template.yml +61 -11
- package/src/config/budget-routing.json +7 -0
- package/src/config/ci-local-parity.yml +8 -0
- package/src/config/discovery/unassigned-artefacts.yml +23 -25
- package/src/config/evaluator-budgets.json +35 -5
- package/src/config/gate-coverage.yml +147 -0
- package/src/config/gate-violation-baselines.json +25 -0
- package/src/install/rule_scope.ts +27 -2
- package/src/scripts/_cli/cmd_conformance.ts +13 -24
- package/src/scripts/_dispatch.bash +10 -0
- package/src/scripts/_lib/claude_settings_hooks.ts +30 -4
- package/src/scripts/_lib/gate_baseline.ts +213 -0
- package/src/scripts/_lib/scoped_projection.ts +50 -0
- package/src/scripts/_lib/tier_budget_routing.ts +360 -0
- package/src/scripts/_lib/tool_adapter_registry.ts +8 -1
- package/src/scripts/_lib/trigger_eval_floors.ts +9 -0
- package/src/scripts/_lib/trigger_routers.ts +17 -5
- package/src/scripts/adr/regenerate_index.ts +4 -3
- package/src/scripts/ai-video/lib/parse-blueprint.sh +1 -1
- package/src/scripts/ai_council/compile_corpus.ts +1 -1
- package/src/scripts/anchor_scoring_run.ts +1 -1
- package/src/scripts/audit_cloud_compatibility.ts +37 -2
- package/src/scripts/audit_initial_context.ts +53 -19
- package/src/scripts/audit_likelihood.ts +31 -2
- package/src/scripts/audit_user_type_axis.ts +58 -11
- package/src/scripts/bench_hook_latency.ts +172 -41
- package/src/scripts/bench_persona_placebo.ts +1 -1
- package/src/scripts/bench_quality_run.ts +1 -1
- package/src/scripts/build_mcp_catalog.ts +12 -1
- package/src/scripts/check_always_budget.ts +122 -11
- package/src/scripts/check_augment_description_cap.ts +24 -3
- package/src/scripts/check_ci_strict_superset.ts +150 -0
- package/src/scripts/check_cli_registry_budget_sync.ts +168 -0
- package/src/scripts/check_cluster_patterns.ts +100 -0
- package/src/scripts/check_consumer_scope_flip.ts +26 -4
- package/src/scripts/check_evaluator_budgets.ts +198 -19
- package/src/scripts/check_gate_coverage.ts +338 -2
- package/src/scripts/check_generator_output_coverage.ts +2 -1
- package/src/scripts/check_iron_law_prominence.ts +1 -1
- package/src/scripts/check_kernel_rule_bundle.ts +1 -1
- package/src/scripts/check_no_new_legacy_path.ts +167 -3
- package/src/scripts/check_pack_size.ts +18 -4
- package/src/scripts/check_portability.ts +37 -7
- package/src/scripts/check_references.ts +19 -3
- package/src/scripts/check_release_pr_shape.ts +13 -0
- package/src/scripts/check_token_quality_golden.ts +1 -1
- package/src/scripts/check_token_regression.ts +9 -2
- package/src/scripts/condense.ts +155 -12
- package/src/scripts/cost/budget.mjs +77 -2
- package/src/scripts/evaluator_umbrella.sh +34 -1
- package/src/scripts/generate_capability_matrix.ts +8 -0
- package/src/scripts/generate_router_coverage_from_matrix.ts +155 -0
- package/src/scripts/hook_manifest.yaml +17 -3
- package/src/scripts/hooks/block_config_weakening.ts +337 -0
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/probe_throttle.ts +75 -0
- package/src/scripts/install-hooks.sh +24 -2
- package/src/scripts/install.ts +2 -2
- package/src/scripts/lint_agents_md.ts +9 -3
- package/src/scripts/lint_archived_skills.ts +1 -1
- package/src/scripts/lint_artefact_frontmatter.ts +185 -37
- package/src/scripts/lint_command_routing.ts +21 -52
- package/src/scripts/lint_command_verbs.ts +47 -28
- package/src/scripts/lint_context_spine_usage.ts +38 -12
- package/src/scripts/lint_framework_leakage.ts +25 -6
- package/src/scripts/lint_handoffs.ts +49 -2
- package/src/scripts/lint_media_policy_linkage.ts +62 -30
- package/src/scripts/lint_namespace.ts +92 -7
- package/src/scripts/lint_new_skill_gate.ts +39 -31
- package/src/scripts/lint_originality_shingles.ts +7 -1
- package/src/scripts/lint_override_kernel_guard.ts +21 -3
- package/src/scripts/lint_pack_boundaries.ts +461 -91
- package/src/scripts/lint_pack_dependencies.ts +24 -57
- package/src/scripts/lint_persona_governance.ts +1 -1
- package/src/scripts/lint_role_experiences.ts +34 -5
- package/src/scripts/lint_trigger_precision.ts +142 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +12 -201
- package/src/scripts/mcp_server/tools.ts +1 -1
- package/src/scripts/prepack-check.mjs +40 -1
- package/src/scripts/prepack_router_targets.d.mts +7 -0
- package/src/scripts/prepack_router_targets.mjs +93 -0
- package/src/scripts/print_required_checks.ts +72 -18
- package/src/scripts/profile_staleness_hook.ts +12 -0
- package/src/scripts/record_evaluator_measurements.mjs +105 -0
- package/src/scripts/recruit_preflight.sh +1 -1
- package/src/scripts/release.ts +214 -7
- package/src/scripts/router_target_paths.d.mts +4 -0
- package/src/scripts/router_target_paths.mjs +69 -0
- package/src/scripts/router_telemetry.ts +52 -1
- package/src/scripts/routing_doctor.ts +560 -0
- package/src/scripts/rule_trigger_eval.ts +403 -0
- package/src/scripts/schemas/rule.schema.json +5 -1
- package/src/scripts/session_canary_hook.ts +96 -5
- package/src/scripts/skill_linter.ts +9 -28
- package/src/scripts/smoke/skills.sh +1 -1
- package/src/scripts/sweep_dead_scan_roots.ts +356 -10
- package/src/scripts/trigger_coverage.ts +13 -2
- package/src/scripts/update_counts.ts +1 -1
- package/src/scripts/validate_telegraph_carveouts.ts +1 -1
- package/src/scripts/wrapper_freshness_hook.ts +14 -0
- package/src/server/schemas/settings.ts +19 -5
- package/src/shared/userMd/utils.ts +1 -1
- package/dist/cli-delegate/chunk-TJIAM5FW.js +0 -133
- package/dist/ui/assets/index-BpSQt5vm.js +0 -40
- package/dist/ui/assets/index-BpSQt5vm.js.map +0 -1
|
@@ -36,7 +36,42 @@ const RULES_DIR = path.join(REPO_ROOT, 'dist/agent-src', 'rules');
|
|
|
36
36
|
const SRC_PREFIX = '.agent-src.uncondensed/';
|
|
37
37
|
const COMP_PREFIX = 'dist/agent-src/';
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* TWO BUDGETS, TWO DIMENSIONS (road-to-renewal-foundation Phase 1; council
|
|
41
|
+
* 2026-08-02 option B, 2/2).
|
|
42
|
+
*
|
|
43
|
+
* `TOTAL_CAP` governs the **raw** dimension — the always-loaded kernel rule
|
|
44
|
+
* bodies themselves. That is the dimension this cap was calibrated for and the
|
|
45
|
+
* only one it ever actually enforced: until the dead-root repair in this same
|
|
46
|
+
* change, `srcToCondensed()` resolved every `load_context` entry to a path that
|
|
47
|
+
* could not exist, so the walker counted ZERO context files and `ext` always
|
|
48
|
+
* equalled `raw`. The gate printed a confident `60.1%` of a dimension it was
|
|
49
|
+
* not measuring.
|
|
50
|
+
*
|
|
51
|
+
* `EXT_TOTAL_CAP` governs the **extended** dimension — raw plus the transitive
|
|
52
|
+
* closure of `load_context` / `load_context_eager` (depth ≤ 2). It is seeded at
|
|
53
|
+
* the FIRST REAL MEASUREMENT, taken 2026-08-02 after the repair, and is a hard
|
|
54
|
+
* gate (the council rejected an advisory cycle: an advisory number creates no
|
|
55
|
+
* pressure and just defers the collision).
|
|
56
|
+
*
|
|
57
|
+
* The seed is a baseline of revealed debt, NOT a target and NOT an approval.
|
|
58
|
+
* It exists so the number is visible and ratcheted; it may only move DOWN. The
|
|
59
|
+
* Phase 2 token work of this roadmap is what pays it down.
|
|
60
|
+
*
|
|
61
|
+
* Deliberately NOT done: raising `TOTAL_CAP` to swallow the ext number. That
|
|
62
|
+
* would silently redefine the kernel budget to ~2× what the maintainer approved
|
|
63
|
+
* and remove exactly the pressure Phase 2 exists to apply.
|
|
64
|
+
*
|
|
65
|
+
* Deliberately DEFERRED: splitting lazy `load_context` from eager
|
|
66
|
+
* `load_context_eager` (only the latter is unconditionally always-loaded). The
|
|
67
|
+
* distinction is real and encoded in the schema, but narrowing an aggregate on
|
|
68
|
+
* a semantic argument is a separate decision from repairing its measurement —
|
|
69
|
+
* and it cannot claim the "live and green" protection the house discriminator
|
|
70
|
+
* grants an enforced contract, because this dimension was never enforced.
|
|
71
|
+
*/
|
|
39
72
|
const TOTAL_CAP = 49_000;
|
|
73
|
+
const EXT_TOTAL_CAP = 60_254;
|
|
74
|
+
const EXT_MEASURED_AT = '2026-08-02';
|
|
40
75
|
const WARN_THRESHOLD = 0.8;
|
|
41
76
|
const FAIL_THRESHOLD = 0.9;
|
|
42
77
|
|
|
@@ -63,16 +98,36 @@ const TREND_LOG = path.join(REPO_ROOT, '.github', 'budget-trend.jsonl');
|
|
|
63
98
|
const TREND_LOG_MAX_RECORDS = 500;
|
|
64
99
|
const TOLERANCE_BAND = 0.02;
|
|
65
100
|
const PER_RULE_CAP = 6_000;
|
|
66
|
-
|
|
101
|
+
// Top-3 is an EXT measure, so it is capped against the ext budget, not the raw
|
|
102
|
+
// one. Seeded 2026-08-02 at the measured 37,855 rather than the derived
|
|
103
|
+
// EXT_TOTAL_CAP/2 (30,127): the derived value was never met and would fail on
|
|
104
|
+
// day one, which is a baseline the gate cannot hold. Ratchet — moves DOWN only.
|
|
105
|
+
const TOP3_CAP = 37_855;
|
|
67
106
|
const MAX_DEPTH = 2;
|
|
68
107
|
const MAX_CONTEXTS_PER_RULE = 3;
|
|
69
108
|
|
|
70
109
|
const RECOVERY_BAND_ENABLED = true;
|
|
71
110
|
const BASELINE_FILE = path.join(REPO_ROOT, '.github', 'budget-baseline.txt');
|
|
72
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Per-rule EXT ceilings, reseeded 2026-08-02 at the first real measurement.
|
|
114
|
+
*
|
|
115
|
+
* The previous values (`non-destructive-by-default` 7,908 · `scope-control`
|
|
116
|
+
* 8,550) were ~2× those rules' raw sizes, which is why they are evidence the
|
|
117
|
+
* ext dimension once counted contexts — but they have been unenforceable for as
|
|
118
|
+
* long as the resolver was broken, so they describe a tree that no longer
|
|
119
|
+
* exists. Reseeding them to the measured values is a baseline of revealed debt,
|
|
120
|
+
* not an approval: these are ratchets and may only move DOWN.
|
|
121
|
+
*
|
|
122
|
+
* `commit-policy` and `verify-before-complete` are new entries for the same
|
|
123
|
+
* reason — they breach the 6,000 per-rule cap only now that their context
|
|
124
|
+
* closures are actually counted.
|
|
125
|
+
*/
|
|
73
126
|
const KNOWN_PER_RULE_BREACHES: Record<string, number> = {
|
|
74
|
-
'
|
|
75
|
-
'
|
|
127
|
+
'scope-control.md': 19_332,
|
|
128
|
+
'non-destructive-by-default.md': 9_657,
|
|
129
|
+
'commit-policy.md': 8_866,
|
|
130
|
+
'verify-before-complete.md': 7_140,
|
|
76
131
|
};
|
|
77
132
|
|
|
78
133
|
const PROG = 'check_always_budget.py';
|
|
@@ -198,11 +253,51 @@ function loadContextPaths(filePath: string): string[] {
|
|
|
198
253
|
return out;
|
|
199
254
|
}
|
|
200
255
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
256
|
+
/**
|
|
257
|
+
* Resolve one `load_context` entry to the projected file it names.
|
|
258
|
+
*
|
|
259
|
+
* DEAD-ROOT REPAIR (road-to-renewal-foundation Phase 1). This used to be:
|
|
260
|
+
*
|
|
261
|
+
* if (entry.startsWith(<retired authoring root>)) → dist/agent-src/<rest>
|
|
262
|
+
* else → REPO_ROOT/<entry>
|
|
263
|
+
*
|
|
264
|
+
* The retired authoring root was deleted by ADR-051, so the first branch was
|
|
265
|
+
* unreachable. The second branch was wrong for every real entry: `load_context`
|
|
266
|
+
* values are relative to the file that declares them
|
|
267
|
+
* (`../contexts/execution/autonomy-mechanics.md` from a rule in
|
|
268
|
+
* `dist/agent-src/rules/`), never repo-root-relative. Joining them to the repo
|
|
269
|
+
* root produced `<repo>/contexts/…`, which never exists — and `walkContexts`
|
|
270
|
+
* silently `continue`s on a missing path.
|
|
271
|
+
*
|
|
272
|
+
* Net effect: the transitive-context dimension of this budget counted ZERO
|
|
273
|
+
* files while the gate printed a confident percentage. It reported ~60% of a
|
|
274
|
+
* cap it was not measuring, and would have reported the same had every rule
|
|
275
|
+
* pulled in a megabyte of context.
|
|
276
|
+
*
|
|
277
|
+
* `skill:` / `guideline:` entries are scheme references, not paths — they have
|
|
278
|
+
* no file to size and are skipped explicitly rather than by accidental
|
|
279
|
+
* existsSync failure.
|
|
280
|
+
*
|
|
281
|
+
* @param entry the raw frontmatter value
|
|
282
|
+
* @param fromFile absolute path of the file that declared it
|
|
283
|
+
* @returns absolute path, or `null` when the entry names no file
|
|
284
|
+
*/
|
|
285
|
+
function resolveContextEntry(entry: string, fromFile: string): string | null {
|
|
286
|
+
const trimmed = entry.trim();
|
|
287
|
+
if (trimmed === '' || /^[a-z][a-z0-9+.-]*:/i.test(trimmed)) {
|
|
288
|
+
// `skill:foo`, `guideline:bar/baz`, or any other scheme reference.
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
if (trimmed.startsWith(SRC_PREFIX)) {
|
|
292
|
+
// Legacy authoring-root form, kept so a stale entry still resolves to
|
|
293
|
+
// its projected twin instead of silently counting zero.
|
|
294
|
+
return path.join(REPO_ROOT, COMP_PREFIX + trimmed.slice(SRC_PREFIX.length));
|
|
204
295
|
}
|
|
205
|
-
|
|
296
|
+
if (trimmed.startsWith(COMP_PREFIX) || path.isAbsolute(trimmed)) {
|
|
297
|
+
return path.resolve(REPO_ROOT, trimmed);
|
|
298
|
+
}
|
|
299
|
+
// The real shape: relative to the declaring file's directory.
|
|
300
|
+
return path.resolve(path.dirname(fromFile), trimmed);
|
|
206
301
|
}
|
|
207
302
|
|
|
208
303
|
/** Return [set of context files counted, list of depth-violation chains]. */
|
|
@@ -214,8 +309,9 @@ function walkContexts(rule: string): [Set<string>, Array<[string, string]>] {
|
|
|
214
309
|
while (stack.length > 0) {
|
|
215
310
|
const [node, depth, chain] = stack.pop() as [string, number, string];
|
|
216
311
|
for (const entry of loadContextPaths(node)) {
|
|
217
|
-
const comp =
|
|
312
|
+
const comp = resolveContextEntry(entry, node);
|
|
218
313
|
const newChain = `${chain} → ${entry}`;
|
|
314
|
+
if (comp === null) continue;
|
|
219
315
|
if (depth + 1 > MAX_DEPTH) {
|
|
220
316
|
violations.push([ruleName, newChain]);
|
|
221
317
|
continue;
|
|
@@ -473,8 +569,16 @@ function main(): number {
|
|
|
473
569
|
// sizes.sort(key=lambda x: -x[2]) — stable descending by ext.
|
|
474
570
|
sizes.sort((a, b) => b[2] - a[2]);
|
|
475
571
|
let totalExt = 0;
|
|
476
|
-
|
|
477
|
-
const
|
|
572
|
+
let totalRaw = 0;
|
|
573
|
+
for (const s of sizes) {
|
|
574
|
+
totalRaw += s[1];
|
|
575
|
+
totalExt += s[2];
|
|
576
|
+
}
|
|
577
|
+
// TOTAL_CAP is the RAW budget (see the two-budgets note at the top); the
|
|
578
|
+
// extended dimension has its own seeded cap.
|
|
579
|
+
const pct = totalRaw / TOTAL_CAP;
|
|
580
|
+
const extPct = totalExt / EXT_TOTAL_CAP;
|
|
581
|
+
const extBreach = totalExt > EXT_TOTAL_CAP;
|
|
478
582
|
let top3 = 0;
|
|
479
583
|
for (const s of sizes.slice(0, 3)) top3 += s[2];
|
|
480
584
|
const top3Breach = top3 > TOP3_CAP;
|
|
@@ -503,6 +607,7 @@ function main(): number {
|
|
|
503
607
|
const failing =
|
|
504
608
|
(pct >= FAIL_THRESHOLD && !inTolerance && !inRecoveryBand && pct < 1.0) ||
|
|
505
609
|
pct > 1.0 + TOLERANCE_BAND ||
|
|
610
|
+
extBreach ||
|
|
506
611
|
overPerRule.length > 0 ||
|
|
507
612
|
grewOverCeiling.length > 0 ||
|
|
508
613
|
top3Breach ||
|
|
@@ -532,9 +637,15 @@ function main(): number {
|
|
|
532
637
|
|
|
533
638
|
const out: string[] = [];
|
|
534
639
|
out.push(
|
|
535
|
-
`${status} always-rule
|
|
640
|
+
`${status} always-rule raw budget: ${comma(totalRaw)} / ` +
|
|
536
641
|
`${comma(TOTAL_CAP)} chars (${fixed(pct * 100, 1)}%) across ${rules.length} rule(s)`,
|
|
537
642
|
);
|
|
643
|
+
out.push(
|
|
644
|
+
` extended (raw + load_context closure): ${comma(totalExt)} / ` +
|
|
645
|
+
`${comma(EXT_TOTAL_CAP)} chars (${fixed(extPct * 100, 1)}%)` +
|
|
646
|
+
`${extBreach ? ' ❌ over the seeded ext cap' : ''}` +
|
|
647
|
+
` — ratchet seeded at the first real measurement ${EXT_MEASURED_AT}; may only move DOWN`,
|
|
648
|
+
);
|
|
538
649
|
out.push(
|
|
539
650
|
` thresholds: warn ${fixed(WARN_THRESHOLD * 100, 0)}% · ` +
|
|
540
651
|
`fail ${fixed(FAIL_THRESHOLD * 100, 0)}% · ` +
|
|
@@ -91,11 +91,32 @@ function _globMdSorted(dir: string): string[] {
|
|
|
91
91
|
return out;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Injection seam for tests: `--root <dir>` overrides the scan root.
|
|
96
|
+
*
|
|
97
|
+
* Additive — no flag means the CI-identical bare call scans `RULES_DIR`, so the
|
|
98
|
+
* pinned CLI contract is unchanged. Without a seam the only way to exercise a
|
|
99
|
+
* real over-cap violation was to tamper with a committed rule, which is why this
|
|
100
|
+
* gate shipped with tests that never called `main()` at all.
|
|
101
|
+
*/
|
|
102
|
+
function _resolve_root(argv: readonly string[]): string {
|
|
103
|
+
for (let i = 0; i < argv.length; i++) {
|
|
104
|
+
const a = argv[i] as string;
|
|
105
|
+
if (a === '--root') {
|
|
106
|
+
const v = argv[i + 1];
|
|
107
|
+
if (v !== undefined) return path.resolve(v);
|
|
108
|
+
} else if (a.startsWith('--root=')) {
|
|
109
|
+
return path.resolve(a.slice('--root='.length));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return RULES_DIR;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function main(argv: readonly string[] = []): number {
|
|
95
116
|
const failures: Array<[string, number, string]> = [];
|
|
96
117
|
let checked = 0;
|
|
97
118
|
|
|
98
|
-
for (const p of _globMdSorted(
|
|
119
|
+
for (const p of _globMdSorted(_resolve_root(argv))) {
|
|
99
120
|
const text = fs.readFileSync(p, 'utf-8');
|
|
100
121
|
const fm = parse_frontmatter(text);
|
|
101
122
|
if (fm['type'] !== 'auto') {
|
|
@@ -157,7 +178,7 @@ function _isCliEntry(): boolean {
|
|
|
157
178
|
}
|
|
158
179
|
|
|
159
180
|
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
160
|
-
process.exit(main());
|
|
181
|
+
process.exit(main(process.argv.slice(2)));
|
|
161
182
|
}
|
|
162
183
|
|
|
163
184
|
export { REPO_ROOT, RULES_DIR, DESC_CAP };
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Assert `ci-strict` is a superset of `ci` BY CONSTRUCTION.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS (road-to-renewal-foundation Phase 1, council 2026-08-02
|
|
6
|
+
* decision A1, 2/2):
|
|
7
|
+
*
|
|
8
|
+
* `Taskfile.yml` used to carry `ci` (202 gate entries) and `ci-strict` (197)
|
|
9
|
+
* as two independently maintained literal lists. They drifted, and nothing
|
|
10
|
+
* detected it — `check_ci_local_parity.ts` deliberately scopes itself to
|
|
11
|
+
* `LOCAL_ROOTS = ['ci', 'consistency']`, so `ci-strict` was outside every
|
|
12
|
+
* assertion in the repo. Six gates ran in `ci` with no counterpart in
|
|
13
|
+
* `ci-strict` at all:
|
|
14
|
+
*
|
|
15
|
+
* preflight (fans out to 11 scripts, incl. check_no_new_legacy_path and
|
|
16
|
+
* check_kernel_rule_bundle), check-ci-local-parity,
|
|
17
|
+
* check-gitignore-freshness, check-generator-output-coverage,
|
|
18
|
+
* check-tracked-but-ignored, check-generated-artefact-headers
|
|
19
|
+
*
|
|
20
|
+
* — so the "release-tag gate" was strictly WEAKER than the everyday gate on
|
|
21
|
+
* ~16 concrete script runs. A stricter tier that checks less is the worst
|
|
22
|
+
* possible shape for a release gate: it is trusted more and proves less.
|
|
23
|
+
*
|
|
24
|
+
* The fix was structural, not a bigger checker: `ci-strict` now delegates to
|
|
25
|
+
* `ci` and adds only strict-only entries on top, so the superset property is
|
|
26
|
+
* tautological. This gate guards the ONE invariant that keeps it tautological
|
|
27
|
+
* — the first gate entry of `ci-strict` is `- task: ci`.
|
|
28
|
+
*
|
|
29
|
+
* It deliberately does NOT re-parse and diff both lists. That would recreate
|
|
30
|
+
* the very "maintain a second model of the same thing" problem the delegation
|
|
31
|
+
* removed, and it would pass happily on a re-forked pair of lists that merely
|
|
32
|
+
* happened to match on the day it ran.
|
|
33
|
+
*
|
|
34
|
+
* Exit codes: 0 ok · 1 invariant violated · 2 usage / environment error.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import * as fs from 'node:fs';
|
|
38
|
+
import * as path from 'node:path';
|
|
39
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
40
|
+
|
|
41
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
42
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
43
|
+
const TASKFILE = path.join(REPO_ROOT, 'Taskfile.yml');
|
|
44
|
+
|
|
45
|
+
/** The delegation entry that makes the superset property tautological. */
|
|
46
|
+
const DELEGATION = '- task: ci';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Extract the `cmds:` entries of a top-level task, in order.
|
|
50
|
+
*
|
|
51
|
+
* Minimal line scan rather than a YAML dependency: the shape is fixed
|
|
52
|
+
* (` <name>:` at two-space indent, ` cmds:` at four, ` - ...` at
|
|
53
|
+
* six) and the repo has no YAML anchors anywhere, so a parser buys nothing
|
|
54
|
+
* here and adds an import to a gate that must stay cheap.
|
|
55
|
+
*/
|
|
56
|
+
function taskCmds(source: string, taskName: string): string[] | null {
|
|
57
|
+
const lines = source.split('\n');
|
|
58
|
+
const header = ` ${taskName}:`;
|
|
59
|
+
let i = lines.findIndex((l) => l === header);
|
|
60
|
+
if (i === -1) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const out: string[] = [];
|
|
64
|
+
let inCmds = false;
|
|
65
|
+
for (i += 1; i < lines.length; i++) {
|
|
66
|
+
const line = lines[i]!;
|
|
67
|
+
if (line.trim() === '') {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
// Any line at two-space indent that is not deeper ends this task.
|
|
71
|
+
if (/^ {2}\S/.test(line)) {
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
if (/^ {4}cmds:\s*$/.test(line)) {
|
|
75
|
+
inCmds = true;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (!inCmds) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const m = /^ {6}(- .*)$/.exec(line);
|
|
82
|
+
if (m) {
|
|
83
|
+
out.push(m[1]!.trim());
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function main(): number {
|
|
90
|
+
let source: string;
|
|
91
|
+
try {
|
|
92
|
+
source = fs.readFileSync(TASKFILE, 'utf-8');
|
|
93
|
+
} catch (err) {
|
|
94
|
+
process.stderr.write(`❌ check_ci_strict_superset: cannot read ${TASKFILE}: ${String(err)}\n`);
|
|
95
|
+
return 2;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const strict = taskCmds(source, 'ci-strict');
|
|
99
|
+
if (strict === null) {
|
|
100
|
+
process.stderr.write(
|
|
101
|
+
'❌ check_ci_strict_superset: no `ci-strict:` task found in Taskfile.yml.\n' +
|
|
102
|
+
' The gate cannot verify an invariant on a task that does not exist.\n',
|
|
103
|
+
);
|
|
104
|
+
return 2;
|
|
105
|
+
}
|
|
106
|
+
if (taskCmds(source, 'ci') === null) {
|
|
107
|
+
process.stderr.write(
|
|
108
|
+
'❌ check_ci_strict_superset: no `ci:` task found in Taskfile.yml.\n',
|
|
109
|
+
);
|
|
110
|
+
return 2;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Skip the internal timing wrappers; they carry no gate semantics and
|
|
114
|
+
// `ci-strict` intentionally does not repeat them (`ci` already brackets
|
|
115
|
+
// the run, so the duration line prints once).
|
|
116
|
+
const gateEntries = strict.filter(
|
|
117
|
+
(e) => !e.startsWith('- defer:') && e !== '- task: _ci-start' && e !== '- task: _ci-end',
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
if (gateEntries.length === 0) {
|
|
121
|
+
process.stderr.write('❌ check_ci_strict_superset: `ci-strict` has no gate entries.\n');
|
|
122
|
+
return 1;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (gateEntries[0] !== DELEGATION) {
|
|
126
|
+
process.stderr.write(
|
|
127
|
+
'❌ check_ci_strict_superset: `ci-strict` no longer delegates to `ci`.\n' +
|
|
128
|
+
` expected first gate entry: \`${DELEGATION}\`\n` +
|
|
129
|
+
` found: \`${gateEntries[0]}\`\n\n` +
|
|
130
|
+
' `ci-strict` must run every `ci` gate plus strict-only extras. It used\n' +
|
|
131
|
+
' to be an independently maintained copy of the list and silently drifted\n' +
|
|
132
|
+
' to SIX fewer gates than `ci` — a release gate that proved less than the\n' +
|
|
133
|
+
' everyday gate. Delegation is what makes the superset property structural\n' +
|
|
134
|
+
' instead of asserted. If you need a strict variant of a gate, add the\n' +
|
|
135
|
+
' `-strict` task AFTER the delegation entry; never re-inline the list.\n',
|
|
136
|
+
);
|
|
137
|
+
return 1;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const extras = gateEntries.slice(1);
|
|
141
|
+
process.stdout.write(
|
|
142
|
+
`✅ ci-strict ⊇ ci by construction (delegates to \`ci\`, plus ${extras.length} strict-only ` +
|
|
143
|
+
`entr${extras.length === 1 ? 'y' : 'ies'}).\n`,
|
|
144
|
+
);
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
149
|
+
process.exit(main());
|
|
150
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* CLI-registry ↔ evaluator-budget sync gate.
|
|
4
|
+
*
|
|
5
|
+
* The evaluator budget `cli_help_command_count` (src/config/evaluator-budgets.json)
|
|
6
|
+
* and the committed measurement record (agents/evidence/metrics/
|
|
7
|
+
* evaluator-measurements.json) are derived from `src/cli/registry.ts`, but until
|
|
8
|
+
* this gate nothing forced the PR that changes the registry to move them: the
|
|
9
|
+
* breach only WARNED on main and BLOCKED the next release. That is how 9.15.0
|
|
10
|
+
* went red — f02db54c2 registered `routing:doctor` (80 → 81 entries) and the
|
|
11
|
+
* drift surfaced days later, at release time, on an unrelated branch.
|
|
12
|
+
*
|
|
13
|
+
* This gate runs on every PR (Static Checks + `task preflight`) and fails the
|
|
14
|
+
* moment the three numbers disagree, so the registering PR carries the budget
|
|
15
|
+
* move itself.
|
|
16
|
+
*
|
|
17
|
+
* The count uses the SAME pinned method as evaluator_umbrella.sh — the number
|
|
18
|
+
* of `{ name: ` entry openers in src/cli/registry.ts (NOT --help prose
|
|
19
|
+
* parsing; methods historically disagreed 74 vs 76 vs 79).
|
|
20
|
+
*
|
|
21
|
+
* Exit codes: 0 in sync · 1 out of sync · 2 a required file is missing or
|
|
22
|
+
* unparseable (fail-closed: a gate that cannot read its inputs must not
|
|
23
|
+
* certify them).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import * as fs from 'node:fs';
|
|
27
|
+
import * as path from 'node:path';
|
|
28
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
29
|
+
|
|
30
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
31
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
32
|
+
|
|
33
|
+
const REGISTRY_PATH = path.join(REPO_ROOT, 'src/cli/registry.ts');
|
|
34
|
+
const BUDGETS_PATH = path.join(REPO_ROOT, 'src/config/evaluator-budgets.json');
|
|
35
|
+
const RECORD_PATH = path.join(REPO_ROOT, 'agents/evidence/metrics/evaluator-measurements.json');
|
|
36
|
+
|
|
37
|
+
const METRIC = 'cli_help_command_count';
|
|
38
|
+
|
|
39
|
+
/** Pinned method — byte-for-byte the umbrella's CLI_COUNT probe. */
|
|
40
|
+
function count_registry_entries(source: string): number {
|
|
41
|
+
return (source.match(/\{ name: /g) || []).length;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface BudgetEntry {
|
|
45
|
+
max?: unknown;
|
|
46
|
+
last_measured?: unknown;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface SyncInputs {
|
|
50
|
+
count: number;
|
|
51
|
+
budget: BudgetEntry | undefined;
|
|
52
|
+
recorded: unknown;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const REMEDY =
|
|
56
|
+
'Fix, in THIS PR (the one that moves src/cli/registry.ts):\n' +
|
|
57
|
+
` 1. src/config/evaluator-budgets.json → budgets.${METRIC}: set max +\n` +
|
|
58
|
+
' last_measured to the new count, bump last_measured_at, and extend\n' +
|
|
59
|
+
' baseline_note naming the commit that moves the surface.\n' +
|
|
60
|
+
' 2. Regenerate the committed record (never hand-edit):\n' +
|
|
61
|
+
' node src/scripts/record_evaluator_measurements.mjs \\\n' +
|
|
62
|
+
' --measurements <file with the re-measured metrics> \\\n' +
|
|
63
|
+
' --out agents/evidence/metrics/evaluator-measurements.json \\\n' +
|
|
64
|
+
' --sha "$(git rev-parse HEAD)"\n' +
|
|
65
|
+
'Otherwise the drift stays invisible until the next release, where the\n' +
|
|
66
|
+
'evaluator gate blocks it (the 9.15.0 failure mode this gate exists to stop).\n';
|
|
67
|
+
|
|
68
|
+
/** Pure comparison — the CLI owns the filesystem. */
|
|
69
|
+
function collect_findings(inputs: SyncInputs): string[] {
|
|
70
|
+
const findings: string[] = [];
|
|
71
|
+
const { count, budget, recorded } = inputs;
|
|
72
|
+
|
|
73
|
+
if (budget === undefined) {
|
|
74
|
+
findings.push(`budgets.${METRIC} is missing from evaluator-budgets.json`);
|
|
75
|
+
return findings;
|
|
76
|
+
}
|
|
77
|
+
if (budget.last_measured !== count) {
|
|
78
|
+
findings.push(
|
|
79
|
+
`registry has ${count} entries but budgets.${METRIC}.last_measured says ` +
|
|
80
|
+
`${String(budget.last_measured)}`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
if (typeof budget.max !== 'number' || count > budget.max) {
|
|
84
|
+
findings.push(
|
|
85
|
+
`registry has ${count} entries but budgets.${METRIC}.max is ` +
|
|
86
|
+
`${String(budget.max)}`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if (recorded === undefined) {
|
|
90
|
+
findings.push(
|
|
91
|
+
`measurements.${METRIC} is missing from the committed record ` +
|
|
92
|
+
'(agents/evidence/metrics/evaluator-measurements.json)',
|
|
93
|
+
);
|
|
94
|
+
} else if (recorded !== count) {
|
|
95
|
+
findings.push(
|
|
96
|
+
`registry has ${count} entries but the committed record says ` +
|
|
97
|
+
`${String(recorded)}`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return findings;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function _read_json(p: string): Record<string, unknown> {
|
|
104
|
+
return JSON.parse(fs.readFileSync(p, 'utf-8')) as Record<string, unknown>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function main(): number {
|
|
108
|
+
let registrySource: string;
|
|
109
|
+
let budgetsDoc: Record<string, unknown>;
|
|
110
|
+
let recordDoc: Record<string, unknown>;
|
|
111
|
+
try {
|
|
112
|
+
registrySource = fs.readFileSync(REGISTRY_PATH, 'utf-8');
|
|
113
|
+
budgetsDoc = _read_json(BUDGETS_PATH);
|
|
114
|
+
recordDoc = _read_json(RECORD_PATH);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
process.stderr.write(
|
|
117
|
+
`ERROR: cannot read a gate input: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
118
|
+
);
|
|
119
|
+
return 2;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const count = count_registry_entries(registrySource);
|
|
123
|
+
const budgets = budgetsDoc['budgets'] as Record<string, BudgetEntry> | undefined;
|
|
124
|
+
const measurements = recordDoc['measurements'] as Record<string, unknown> | undefined;
|
|
125
|
+
const findings = collect_findings({
|
|
126
|
+
count,
|
|
127
|
+
budget: budgets?.[METRIC],
|
|
128
|
+
recorded: measurements?.[METRIC],
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
process.stdout.write('scanned: 3\n');
|
|
132
|
+
if (findings.length > 0) {
|
|
133
|
+
for (const f of findings) {
|
|
134
|
+
process.stdout.write(`❌ ${METRIC} out of sync: ${f}\n`);
|
|
135
|
+
}
|
|
136
|
+
process.stdout.write('\n' + REMEDY);
|
|
137
|
+
return 1;
|
|
138
|
+
}
|
|
139
|
+
process.stdout.write(
|
|
140
|
+
`✅ ${METRIC} in sync — registry (${count}) == budget == committed record.\n`,
|
|
141
|
+
);
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function _isCliEntry(): boolean {
|
|
146
|
+
if (process.argv[1] === undefined) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
150
|
+
if (import.meta.url === argvUrl) {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
// Symlinked invocations (installed projections, macOS /var → /private/var)
|
|
154
|
+
// make the raw URLs differ — compare realpaths so the entry guard fires.
|
|
155
|
+
try {
|
|
156
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
157
|
+
const argvPath = fs.realpathSync(path.resolve(process.argv[1]));
|
|
158
|
+
return here === argvPath;
|
|
159
|
+
} catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
165
|
+
process.exit(main());
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export { collect_findings, count_registry_entries, main };
|
|
@@ -18,6 +18,19 @@
|
|
|
18
18
|
* must document the bare-invocation behaviour (menu / default route /
|
|
19
19
|
* detection fallback) per command-clusters.md § Bare invocation.
|
|
20
20
|
*
|
|
21
|
+
* Filesystem-enumeration addition (road-to-renewal-leverage, 2026-08-02): the
|
|
22
|
+
* checks above all read the CONTRACT and ask "does what it names exist?".
|
|
23
|
+
* Nothing asked the inverse — "does what exists get named?" — so a
|
|
24
|
+
* sub-command could sit on disk while appearing in neither its hub's
|
|
25
|
+
* `## Sub-commands` table nor its `routes_to:`, invisibly. Two instances were
|
|
26
|
+
* live when this was added (`/roadmap:materialize`, `/memory:learn-low-impact`)
|
|
27
|
+
* plus incomplete `routes_to` on 12 of 25 dispatch clusters.
|
|
28
|
+
* `check_completeness` closes that direction: for a `type: orchestrator`
|
|
29
|
+
* dispatcher, every on-disk `<sub>/command.md` sibling directory must be named
|
|
30
|
+
* in the table and listed in `routes_to`. Scoped to orchestrators on purpose —
|
|
31
|
+
* a few contract rows point at full working commands that merely happen to
|
|
32
|
+
* carry one nested sub-command, and those own no dispatch table.
|
|
33
|
+
*
|
|
21
34
|
* Exit codes: 0 = clean, 1 = pattern violations, 3 = internal error.
|
|
22
35
|
*/
|
|
23
36
|
|
|
@@ -170,6 +183,90 @@ function _repr(v: string | undefined): string {
|
|
|
170
183
|
return `'${v.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
171
184
|
}
|
|
172
185
|
|
|
186
|
+
/** Parse a `routes_to: [a, b]` frontmatter value into its entries. */
|
|
187
|
+
export function parse_routes_to(raw: string | undefined): string[] {
|
|
188
|
+
if (raw === undefined) {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
const m = /^\[(.*)\]$/.exec(raw.trim());
|
|
192
|
+
if (!m) {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
return (m[1] as string)
|
|
196
|
+
.split(',')
|
|
197
|
+
.map((s) => s.trim().replace(/^['"]|['"]$/g, ''))
|
|
198
|
+
.filter((s) => s.length > 0);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Sub-command directory names that carry a `command.md`, sorted. */
|
|
202
|
+
export function on_disk_subs(dispatcher_path: string): string[] {
|
|
203
|
+
const dir = path.dirname(dispatcher_path);
|
|
204
|
+
let entries: fs.Dirent[];
|
|
205
|
+
try {
|
|
206
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
207
|
+
} catch {
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
return entries
|
|
211
|
+
.filter((e) => e.isDirectory() && _isFile(path.join(dir, e.name, 'command.md')))
|
|
212
|
+
.map((e) => e.name)
|
|
213
|
+
.sort();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Does the body name `<sub>` as a sub-command of `<cluster>`?
|
|
218
|
+
*
|
|
219
|
+
* Both `` `/cluster:sub` `` and `` `/cluster sub` `` count — command-clusters.md
|
|
220
|
+
* § 157 makes the space form a first-class equivalent of the colon form, so
|
|
221
|
+
* requiring one of them would be inventing a rule the contract does not carry.
|
|
222
|
+
* The trailing class allows a row that decorates the name inside the same
|
|
223
|
+
* backticks (`` `/team review [--background]` ``).
|
|
224
|
+
*/
|
|
225
|
+
export function names_sub(body: string, cluster: string, sub: string): boolean {
|
|
226
|
+
const re = new RegExp('`/' + cluster + '[: ]' + sub + '(?![a-z0-9-])');
|
|
227
|
+
return re.test(body);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Every on-disk sub-command of an orchestrator must be named in its
|
|
232
|
+
* `## Sub-commands` table and listed in `routes_to:`.
|
|
233
|
+
*
|
|
234
|
+
* Non-orchestrator dispatchers are skipped: a handful of contract rows point
|
|
235
|
+
* at full working commands that merely happen to carry a nested sub-command,
|
|
236
|
+
* and those own no dispatch table to be complete against.
|
|
237
|
+
*/
|
|
238
|
+
export function check_completeness(
|
|
239
|
+
cluster: string,
|
|
240
|
+
dispatcher_path: string,
|
|
241
|
+
fm: Record<string, string>,
|
|
242
|
+
body: string,
|
|
243
|
+
): string[] {
|
|
244
|
+
if (fm['type'] !== 'orchestrator') {
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
const subs = on_disk_subs(dispatcher_path);
|
|
248
|
+
if (subs.length === 0) {
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
251
|
+
const errors: string[] = [];
|
|
252
|
+
const routes = parse_routes_to(fm['routes_to']);
|
|
253
|
+
for (const sub of subs) {
|
|
254
|
+
if (!names_sub(body, cluster, sub)) {
|
|
255
|
+
errors.push(
|
|
256
|
+
`sub-command \`${sub}\` exists on disk but is not named in the ` +
|
|
257
|
+
'`## Sub-commands` table',
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
if (!routes.includes(`${cluster}-${sub}`) && !routes.includes(sub)) {
|
|
261
|
+
errors.push(
|
|
262
|
+
`sub-command \`${sub}\` exists on disk but is missing from ` +
|
|
263
|
+
'`routes_to:`',
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return errors;
|
|
268
|
+
}
|
|
269
|
+
|
|
173
270
|
export function check_dispatcher(cluster: string, slug_map: Map<string, string>): FileReport {
|
|
174
271
|
const p = _resolve_command(cluster, slug_map);
|
|
175
272
|
if (p === null) {
|
|
@@ -241,6 +338,9 @@ export function check_dispatcher(cluster: string, slug_map: Map<string, string>)
|
|
|
241
338
|
}
|
|
242
339
|
}
|
|
243
340
|
|
|
341
|
+
// Completeness (2026-08-02): every sub-command that EXISTS must be named.
|
|
342
|
+
rep.errors.push(...check_completeness(cluster, p, fm, body));
|
|
343
|
+
|
|
244
344
|
// Bare-invocation story (Phase 4 / ADR-115): the Dispatch section must say
|
|
245
345
|
// what a bare `/<cluster>` does — a menu, a default route, or detection.
|
|
246
346
|
const dispatchIdx = body.indexOf('## Dispatch');
|