@event4u/agent-config 9.14.0 → 9.15.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 +110 -112
- 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/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 +2 -0
- 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/ui/assets/index-DJY_vQVn.js +40 -0
- package/dist/ui/assets/index-DJY_vQVn.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 +11 -11
- 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 +59 -11
- 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 +234 -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/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/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_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 +337 -1
- 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_regression.ts +9 -2
- package/src/scripts/condense.ts +155 -12
- package/src/scripts/cost/budget.mjs +63 -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 +105 -1
- 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 +485 -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/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
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* sweep_dead_scan_roots — which gate scripts walk a root that does not exist?
|
|
4
4
|
*
|
|
5
5
|
* Population expansion for `road-to-gates-that-can-fail` Phase 1. The committed
|
|
6
|
-
* census (`agents/evidence/reports/gate-scope-census.md`)
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* census (`agents/evidence/reports/gate-scope-census.md`) began as a manual audit
|
|
7
|
+
* of 14 gates; since 2026-08-02 this script GENERATES it over the full population
|
|
8
|
+
* via `--census`, so it is re-runnable and a stale row shows up in a diff.
|
|
9
9
|
*
|
|
10
10
|
* WHY IT EXISTS: two more dead gates were found OUTSIDE the censused 14 while
|
|
11
11
|
* doing thematically unrelated work — `audit_skill_overlap` (rooted at a
|
|
@@ -73,7 +73,9 @@
|
|
|
73
73
|
* ── EXIT CONTRACT (one meaning per code, so red stays informative) ─────────
|
|
74
74
|
*
|
|
75
75
|
* 0 clean
|
|
76
|
-
* 1
|
|
76
|
+
* 1 class-A finding(s) ABOVE the recorded ratchet baseline
|
|
77
|
+
* (`src/config/gate-violation-baselines.json`). With no baseline entry this
|
|
78
|
+
* is any class-A finding at all. Nothing else returns 1.
|
|
77
79
|
* 2 self-test failure — the extractor is blind; output is not trustworthy.
|
|
78
80
|
* 3 ledger hygiene — stale entry or over cap. The sweep ran fine; the LIST
|
|
79
81
|
* needs work.
|
|
@@ -85,7 +87,13 @@
|
|
|
85
87
|
*
|
|
86
88
|
* Usage:
|
|
87
89
|
* ./scripts-run src/scripts/sweep_dead_scan_roots [--quiet] [--json <path>]
|
|
88
|
-
* [--
|
|
90
|
+
* [--census <path>] [--root <dir>]
|
|
91
|
+
* [--ledger <path>]
|
|
92
|
+
*
|
|
93
|
+
* `--census` writes the full scan-scope census — every gate, every root it
|
|
94
|
+
* reads, and the unit count under that root today. The sweep answers "which
|
|
95
|
+
* roots are dead"; the census answers "what does each gate read, and how much",
|
|
96
|
+
* so the NEXT container move shows up as a count dropping in a diff.
|
|
89
97
|
*/
|
|
90
98
|
|
|
91
99
|
import * as fs from 'node:fs';
|
|
@@ -93,6 +101,9 @@ import * as path from 'node:path';
|
|
|
93
101
|
import process from 'node:process';
|
|
94
102
|
import { fileURLToPath } from 'node:url';
|
|
95
103
|
|
|
104
|
+
import * as agentSrc from './_lib/agent_src.js';
|
|
105
|
+
import { checkRatchet } from './_lib/gate_baseline.js';
|
|
106
|
+
|
|
96
107
|
const _HERE = fileURLToPath(import.meta.url);
|
|
97
108
|
const REPO = path.resolve(path.dirname(_HERE), '..', '..');
|
|
98
109
|
const DEFAULT_SCRIPTS = path.join(REPO, 'src', 'scripts');
|
|
@@ -135,9 +146,24 @@ export interface Finding {
|
|
|
135
146
|
evidence: string;
|
|
136
147
|
}
|
|
137
148
|
|
|
149
|
+
/** Every root the extractor saw for one gate, dead or alive — the census row source. */
|
|
150
|
+
export interface RootRef {
|
|
151
|
+
rel: string;
|
|
152
|
+
/** Declaring identifier(s), or `(inline)` / `(array)` / `(spec-table)`. */
|
|
153
|
+
names: string;
|
|
154
|
+
}
|
|
155
|
+
|
|
138
156
|
export interface Analysis {
|
|
139
157
|
confirmed: Finding[];
|
|
140
158
|
unproven: Finding[];
|
|
159
|
+
/**
|
|
160
|
+
* Census input: ALL extracted roots, not only the missing ones the sweep
|
|
161
|
+
* reports as findings. The sweep answers "which roots are dead"; the census
|
|
162
|
+
* answers "what does every gate read, and how much" — a root+count pair per
|
|
163
|
+
* gate is what makes the NEXT container move visible in a diff instead of
|
|
164
|
+
* silently disarming a gate for weeks.
|
|
165
|
+
*/
|
|
166
|
+
roots: RootRef[];
|
|
141
167
|
}
|
|
142
168
|
|
|
143
169
|
export interface Disposition {
|
|
@@ -345,9 +371,11 @@ export function analyze(src: string, repoRoot: string): Analysis {
|
|
|
345
371
|
|
|
346
372
|
const confirmed: Finding[] = [];
|
|
347
373
|
const unproven: Finding[] = [];
|
|
374
|
+
const roots = new Map<string, string>();
|
|
348
375
|
const missing = (rel: string): boolean => !fs.existsSync(path.join(repoRoot, rel));
|
|
349
376
|
|
|
350
377
|
for (const [rel, e] of found) {
|
|
378
|
+
roots.set(rel, [...e.names].join(',') || '(inline)');
|
|
351
379
|
if (!missing(rel)) continue;
|
|
352
380
|
let evidence: string | null =
|
|
353
381
|
e.inlineRead ? 'inline-read' : (arrayHits.get(rel) ?? specHits.get(rel) ?? null);
|
|
@@ -376,7 +404,30 @@ export function analyze(src: string, repoRoot: string): Analysis {
|
|
|
376
404
|
for (const [rel, ev] of arrayHits) push(confirmed, [], rel, '(array)', ev);
|
|
377
405
|
for (const rel of arrayUnproven) push(unproven, unproven, rel, '(array)', '—');
|
|
378
406
|
|
|
379
|
-
|
|
407
|
+
// Resolver-reached roots: a gate calling `SRC_SKILLS()` reads `src/skills`
|
|
408
|
+
// just as surely as one joining the literal, and the census must see both.
|
|
409
|
+
for (const [getter, rel] of resolverRoots(repoRoot)) {
|
|
410
|
+
if (new RegExp(`\\b${getter}\\s*\\(\\s*\\)`).test(src) && !roots.has(rel)) {
|
|
411
|
+
roots.set(rel, `${getter}()`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Census-only permissive pass — see `censusOnlyRoots` for why it may never
|
|
416
|
+
// feed the finding path.
|
|
417
|
+
for (const [rel, name] of censusOnlyRoots(src, repoRoot)) {
|
|
418
|
+
if (!roots.has(rel)) roots.set(rel, name);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
for (const rel of specHits.keys()) if (!roots.has(rel)) roots.set(rel, '(spec-table)');
|
|
422
|
+
for (const rel of specUnproven) if (!roots.has(rel)) roots.set(rel, '(spec-table)');
|
|
423
|
+
for (const rel of arrayHits.keys()) if (!roots.has(rel)) roots.set(rel, '(array)');
|
|
424
|
+
for (const rel of arrayUnproven) if (!roots.has(rel)) roots.set(rel, '(array)');
|
|
425
|
+
|
|
426
|
+
return {
|
|
427
|
+
confirmed,
|
|
428
|
+
unproven,
|
|
429
|
+
roots: [...roots].sort(([a], [b]) => a.localeCompare(b)).map(([rel, names]) => ({ rel, names })),
|
|
430
|
+
};
|
|
380
431
|
}
|
|
381
432
|
|
|
382
433
|
/**
|
|
@@ -411,19 +462,279 @@ export function loadLedger(file: string): Disposition[] {
|
|
|
411
462
|
return Array.isArray(data) ? data : [];
|
|
412
463
|
}
|
|
413
464
|
|
|
465
|
+
/**
|
|
466
|
+
* Roots reached through the shared resolver rather than a path literal.
|
|
467
|
+
*
|
|
468
|
+
* Without this, the census is blind to exactly the gates that did the right
|
|
469
|
+
* thing: a gate repaired from `path.join(ROOT, '.agent-src.uncondensed')` to
|
|
470
|
+
* `SRC_SKILLS()` would VANISH from the census, so adopting the resolver — which
|
|
471
|
+
* the roadmap explicitly prefers over new literals — would look like losing
|
|
472
|
+
* coverage. The map is built by calling the real getters, not by copying their
|
|
473
|
+
* values, so it cannot drift from `_lib/agent_src.ts`.
|
|
474
|
+
*/
|
|
475
|
+
function resolverRoots(repoRoot: string): ReadonlyMap<string, string> {
|
|
476
|
+
const getters: Record<string, () => string> = {
|
|
477
|
+
SRC: agentSrc.SRC,
|
|
478
|
+
SRC_SKILLS: agentSrc.SRC_SKILLS,
|
|
479
|
+
SRC_RULES: agentSrc.SRC_RULES,
|
|
480
|
+
SRC_AGENT: agentSrc.SRC_AGENT,
|
|
481
|
+
SRC_DOMAINS: agentSrc.SRC_DOMAINS,
|
|
482
|
+
LEGACY_SRC: agentSrc.LEGACY_SRC,
|
|
483
|
+
PACKAGES: agentSrc.PACKAGES,
|
|
484
|
+
};
|
|
485
|
+
const out = new Map<string, string>();
|
|
486
|
+
for (const [name, fn] of Object.entries(getters)) {
|
|
487
|
+
let abs: string;
|
|
488
|
+
try {
|
|
489
|
+
abs = fn();
|
|
490
|
+
} catch {
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
const rel = path.relative(repoRoot, abs).split(path.sep).join('/');
|
|
494
|
+
if (rel && !rel.startsWith('..')) out.set(name, rel);
|
|
495
|
+
}
|
|
496
|
+
return out;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Permissive root extraction — CENSUS ONLY, never the finding path.
|
|
501
|
+
*
|
|
502
|
+
* The finding extractor is deliberately strict: it reports a dead root only
|
|
503
|
+
* with positive read evidence, because a false "this gate is dead" turns CI red
|
|
504
|
+
* on a lie. That precision costs recall — a gate writing
|
|
505
|
+
* `const SOURCE_DIR = 'src'` and later `path.join(REPO, SOURCE_DIR)` is
|
|
506
|
+
* invisible to it, and roughly half the population is shaped that way.
|
|
507
|
+
*
|
|
508
|
+
* The census has the opposite trade: a missed root is a coverage hole, and a
|
|
509
|
+
* spurious row is cheap. So this pass substitutes single-literal string consts
|
|
510
|
+
* into `path.join(...)` calls — and then keeps ONLY roots that EXIST on disk.
|
|
511
|
+
* That filter is what makes it safe: a pass that cannot yield a missing path
|
|
512
|
+
* cannot manufacture a dead-root finding, no matter how loose its parsing.
|
|
513
|
+
* Precision stays where red exits are decided; recall goes where the record is.
|
|
514
|
+
*/
|
|
515
|
+
export function censusOnlyRoots(src: string, repoRoot: string): Map<string, string> {
|
|
516
|
+
const consts = new Map<string, string>();
|
|
517
|
+
const cre = /(?:const|let)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*'([^'\n]+)'/g;
|
|
518
|
+
let cm: RegExpExecArray | null;
|
|
519
|
+
while ((cm = cre.exec(src)) !== null) {
|
|
520
|
+
const [, name, value] = cm;
|
|
521
|
+
if (value !== undefined && !value.includes(' ') && value !== '') consts.set(name as string, value);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const out = new Map<string, string>();
|
|
525
|
+
const jre = /path\.join\(([^)]*)\)/g;
|
|
526
|
+
let jm: RegExpExecArray | null;
|
|
527
|
+
while ((jm = jre.exec(src)) !== null) {
|
|
528
|
+
const parts = (jm[1] as string).split(',').map((p) => p.trim()).filter((p) => p !== '');
|
|
529
|
+
if (parts.length < 2) continue;
|
|
530
|
+
const base = parts[0] as string;
|
|
531
|
+
if (!BASE_IDS.has(base)) continue;
|
|
532
|
+
|
|
533
|
+
const segs: string[] = [];
|
|
534
|
+
for (const raw of parts.slice(1)) {
|
|
535
|
+
const lit = /^'([^']*)'$/.exec(raw);
|
|
536
|
+
if (lit) {
|
|
537
|
+
segs.push(lit[1] as string);
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
const viaConst = consts.get(raw);
|
|
541
|
+
if (viaConst !== undefined) {
|
|
542
|
+
segs.push(viaConst);
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
break; // a segment we cannot resolve ends the path — a prefix is still a real root
|
|
546
|
+
}
|
|
547
|
+
if (segs.length === 0) continue;
|
|
548
|
+
const rel = segs.join('/').replace(/\/+$/, '');
|
|
549
|
+
if (rel === '' || rel.startsWith('..')) continue;
|
|
550
|
+
// `node_modules/.bin/tsx` is a tool path a gate SPAWNS, not a corpus it reads.
|
|
551
|
+
if (rel === 'node_modules' || rel.startsWith('node_modules/')) continue;
|
|
552
|
+
if (!fs.existsSync(path.join(repoRoot, rel))) continue; // the safety filter — see the doc comment
|
|
553
|
+
if (!out.has(rel)) out.set(rel, '(census-only)');
|
|
554
|
+
}
|
|
555
|
+
return out;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** Directories never worth walking for a unit count. */
|
|
559
|
+
const COUNT_SKIP: ReadonlySet<string> = new Set(['node_modules', '.git', '.venv', '__pycache__']);
|
|
560
|
+
|
|
561
|
+
/** Depth cap for the unit count — deep enough for every artefact tree here, cheap enough to re-run. */
|
|
562
|
+
const COUNT_MAX_DEPTH = 12;
|
|
563
|
+
|
|
564
|
+
export interface RootCount {
|
|
565
|
+
/** `dir` · `file` · `absent` — absent is the dead-root case the sweep reports separately. */
|
|
566
|
+
kind: 'dir' | 'file' | 'absent';
|
|
567
|
+
/** Files under the root (recursive), 1 for a file, 0 for absent. */
|
|
568
|
+
units: number;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Count what a root actually contains, on this tree, right now.
|
|
573
|
+
*
|
|
574
|
+
* The number is deliberately generic — files under the root — not the gate's
|
|
575
|
+
* own notion of a unit (rules, skills, declarers). A gate-specific count would
|
|
576
|
+
* mean reimplementing 200 gates' filters here and would rot the moment one
|
|
577
|
+
* changed. What the census needs is a value that MOVES when the tree moves, so
|
|
578
|
+
* a container migration shows up as a diff instead of as silence.
|
|
579
|
+
*/
|
|
580
|
+
export function countUnits(repoRoot: string, rel: string): RootCount {
|
|
581
|
+
const abs = path.join(repoRoot, rel);
|
|
582
|
+
let st: fs.Stats;
|
|
583
|
+
try {
|
|
584
|
+
st = fs.statSync(abs);
|
|
585
|
+
} catch {
|
|
586
|
+
return { kind: 'absent', units: 0 };
|
|
587
|
+
}
|
|
588
|
+
if (st.isFile()) return { kind: 'file', units: 1 };
|
|
589
|
+
if (!st.isDirectory()) return { kind: 'absent', units: 0 };
|
|
590
|
+
|
|
591
|
+
let units = 0;
|
|
592
|
+
const walk = (dir: string, depth: number): void => {
|
|
593
|
+
if (depth > COUNT_MAX_DEPTH) return;
|
|
594
|
+
let entries: fs.Dirent[];
|
|
595
|
+
try {
|
|
596
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
597
|
+
} catch {
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
for (const e of entries) {
|
|
601
|
+
if (e.isDirectory()) {
|
|
602
|
+
if (!COUNT_SKIP.has(e.name)) walk(path.join(dir, e.name), depth + 1);
|
|
603
|
+
} else if (e.isFile()) {
|
|
604
|
+
units += 1;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
walk(abs, 0);
|
|
609
|
+
return { kind: 'dir', units };
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
export interface CensusRow {
|
|
613
|
+
gate: string;
|
|
614
|
+
rel: string;
|
|
615
|
+
names: string;
|
|
616
|
+
kind: RootCount['kind'];
|
|
617
|
+
units: number;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Build the census: one row per (gate, extracted root), plus an explicit row
|
|
622
|
+
* for every gate the extractor found NO root in.
|
|
623
|
+
*
|
|
624
|
+
* The no-root rows are the honest half. A census that silently omits the gates
|
|
625
|
+
* it could not read would claim coverage it does not have — the same
|
|
626
|
+
* green-on-nothing shape this whole sweep exists to end.
|
|
627
|
+
*/
|
|
628
|
+
export function buildCensus(population: readonly string[], results: ReadonlyMap<string, Analysis>, repoRoot: string): CensusRow[] {
|
|
629
|
+
const rows: CensusRow[] = [];
|
|
630
|
+
for (const file of population) {
|
|
631
|
+
const gate = file.replace(/\.ts$/, '');
|
|
632
|
+
const analysis = results.get(file);
|
|
633
|
+
const refs = analysis?.roots ?? [];
|
|
634
|
+
if (refs.length === 0) {
|
|
635
|
+
rows.push({ gate, rel: '(no literal root extracted)', names: '—', kind: 'absent', units: 0 });
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
for (const ref of refs) {
|
|
639
|
+
const c = countUnits(repoRoot, ref.rel);
|
|
640
|
+
rows.push({ gate, rel: ref.rel, names: ref.names, kind: c.kind, units: c.units });
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return rows;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
export function renderCensus(rows: readonly CensusRow[], population: number, today: string): string {
|
|
647
|
+
const dead = rows.filter((r) => r.kind === 'absent' && r.rel !== '(no literal root extracted)');
|
|
648
|
+
const noRoot = rows.filter((r) => r.rel === '(no literal root extracted)');
|
|
649
|
+
const live = rows.filter((r) => r.kind !== 'absent');
|
|
650
|
+
const gatesWithRoots = new Set(live.map((r) => r.gate)).size;
|
|
651
|
+
|
|
652
|
+
const out: string[] = [];
|
|
653
|
+
out.push('# Gate scan-scope census');
|
|
654
|
+
out.push('');
|
|
655
|
+
out.push(`> Generated by \`./scripts-run src/scripts/sweep_dead_scan_roots --census <path>\` on ${today}.`);
|
|
656
|
+
out.push('> Do not hand-edit — re-run the command. A row that disagrees with a fresh');
|
|
657
|
+
out.push('> run is the signal this file exists to produce.');
|
|
658
|
+
out.push('');
|
|
659
|
+
out.push('## Why');
|
|
660
|
+
out.push('');
|
|
661
|
+
out.push('ADR-051 moved the source container. Gates carrying a hardcoded literal root');
|
|
662
|
+
out.push('kept exiting 0 while scanning zero files, for weeks, because nothing recorded');
|
|
663
|
+
out.push('what each gate was supposed to be reading. This census is that record: every');
|
|
664
|
+
out.push('gate, every root it reads, and how much is under that root **today**. The next');
|
|
665
|
+
out.push('container move shows up here as a count dropping to zero in a diff, instead of');
|
|
666
|
+
out.push('as a green checkmark over an empty directory.');
|
|
667
|
+
out.push('');
|
|
668
|
+
out.push('## Headline');
|
|
669
|
+
out.push('');
|
|
670
|
+
out.push('| Metric | Value |');
|
|
671
|
+
out.push('|---|---|');
|
|
672
|
+
out.push(`| Gate scripts in population | ${population} |`);
|
|
673
|
+
out.push(`| Gates with at least one resolvable root | ${gatesWithRoots} |`);
|
|
674
|
+
out.push(`| Gates with no literal root the extractor can see | ${noRoot.length} |`);
|
|
675
|
+
out.push(`| Roots resolved and counted | ${live.length} |`);
|
|
676
|
+
out.push(`| Roots that do not exist on this tree | ${dead.length} |`);
|
|
677
|
+
out.push('');
|
|
678
|
+
out.push('## Scope — what this census does NOT cover');
|
|
679
|
+
out.push('');
|
|
680
|
+
out.push('- **The unit count is files under the root, not the gate\'s own unit.** A gate');
|
|
681
|
+
out.push(' filtering to `*.md` reports fewer units than the row shows. The number is a');
|
|
682
|
+
out.push(' movement detector, not a gate-internal assertion — reimplementing 200 filters');
|
|
683
|
+
out.push(' here would rot on the first filter change.');
|
|
684
|
+
out.push('- **Roots the static extractor cannot see** are listed as');
|
|
685
|
+
out.push(' `(no literal root extracted)`, not omitted. Known blind spots: roots read from');
|
|
686
|
+
out.push(' a config file, bases outside the accepted repo-root identifier set, glob-library');
|
|
687
|
+
out.push(' walks, and template-literal paths. Those gates are uncovered, and the row says so.');
|
|
688
|
+
out.push('- **Nested helper directories** under `src/scripts/` are outside the population —');
|
|
689
|
+
out.push(' the sweep reads the top level only.');
|
|
690
|
+
out.push('- A dead root here is a *finding*, not a repair. Repairs are the roadmap\'s job;');
|
|
691
|
+
out.push(' this file only makes them visible.');
|
|
692
|
+
out.push('');
|
|
693
|
+
out.push('## Roots that do not exist on this tree');
|
|
694
|
+
out.push('');
|
|
695
|
+
if (dead.length === 0) {
|
|
696
|
+
out.push('None. Every extracted root resolves.');
|
|
697
|
+
} else {
|
|
698
|
+
out.push('| Gate | Declared root | Declared by |');
|
|
699
|
+
out.push('|---|---|---|');
|
|
700
|
+
for (const r of dead) out.push(`| \`${r.gate}\` | \`${r.rel}\` | \`${r.names}\` |`);
|
|
701
|
+
}
|
|
702
|
+
out.push('');
|
|
703
|
+
out.push('## Full census');
|
|
704
|
+
out.push('');
|
|
705
|
+
out.push('| Gate | Root | Kind | Units | Declared by |');
|
|
706
|
+
out.push('|---|---|---|---|---|');
|
|
707
|
+
for (const r of rows) {
|
|
708
|
+
const rootCell = r.rel === '(no literal root extracted)' ? '_(none extracted)_' : `\`${r.rel}\``;
|
|
709
|
+
const units = r.kind === 'absent' ? '**0**' : String(r.units);
|
|
710
|
+
out.push(`| \`${r.gate}\` | ${rootCell} | ${r.kind} | ${units} | \`${r.names}\` |`);
|
|
711
|
+
}
|
|
712
|
+
out.push('');
|
|
713
|
+
out.push('## Reproducing');
|
|
714
|
+
out.push('');
|
|
715
|
+
out.push('```bash');
|
|
716
|
+
out.push('./scripts-run src/scripts/sweep_dead_scan_roots --census agents/evidence/reports/gate-scope-census.md');
|
|
717
|
+
out.push('git diff --stat agents/evidence/reports/gate-scope-census.md # empty = the tree has not moved');
|
|
718
|
+
out.push('```');
|
|
719
|
+
out.push('');
|
|
720
|
+
return `${out.join('\n')}\n`;
|
|
721
|
+
}
|
|
722
|
+
|
|
414
723
|
interface Args {
|
|
415
724
|
quiet: boolean;
|
|
416
725
|
json: string | null;
|
|
726
|
+
census: string | null;
|
|
417
727
|
scripts: string;
|
|
418
728
|
ledger: string;
|
|
419
729
|
}
|
|
420
730
|
|
|
421
731
|
export function parseArgs(argv: string[]): Args {
|
|
422
|
-
const a: Args = { quiet: false, json: null, scripts: DEFAULT_SCRIPTS, ledger: DEFAULT_LEDGER };
|
|
732
|
+
const a: Args = { quiet: false, json: null, census: null, scripts: DEFAULT_SCRIPTS, ledger: DEFAULT_LEDGER };
|
|
423
733
|
for (let i = 0; i < argv.length; i += 1) {
|
|
424
734
|
const arg = argv[i];
|
|
425
735
|
if (arg === '--quiet') a.quiet = true;
|
|
426
736
|
else if (arg === '--json') a.json = argv[(i += 1)] ?? '';
|
|
737
|
+
else if (arg === '--census') a.census = argv[(i += 1)] ?? '';
|
|
427
738
|
else if (arg === '--root') a.scripts = path.resolve(REPO, argv[(i += 1)] ?? DEFAULT_SCRIPTS);
|
|
428
739
|
else if (arg === '--ledger') a.ledger = path.resolve(REPO, argv[(i += 1)] ?? DEFAULT_LEDGER);
|
|
429
740
|
else {
|
|
@@ -463,9 +774,14 @@ export function main(argv: string[]): number {
|
|
|
463
774
|
const pop = fs.existsSync(args.scripts)
|
|
464
775
|
? fs.readdirSync(args.scripts).filter((f) => GATE_RE.test(f)).sort()
|
|
465
776
|
: [];
|
|
777
|
+
// Census needs every gate's roots; the finding report needs only the ones
|
|
778
|
+
// with hits. Keep both, and filter at the point of use rather than dropping
|
|
779
|
+
// data here — `census` is the superset.
|
|
780
|
+
const census = new Map<string, Analysis>();
|
|
466
781
|
const results = new Map<string, Analysis>();
|
|
467
782
|
for (const f of pop) {
|
|
468
783
|
const r = analyze(fs.readFileSync(path.join(args.scripts, f), 'utf-8'), repoRoot);
|
|
784
|
+
census.set(f, r);
|
|
469
785
|
if (r.confirmed.length || r.unproven.length) results.set(f, r);
|
|
470
786
|
}
|
|
471
787
|
|
|
@@ -514,7 +830,11 @@ export function main(argv: string[]): number {
|
|
|
514
830
|
unproven: unprovenCount,
|
|
515
831
|
class_a: classA,
|
|
516
832
|
stale: stale.map((s) => `${s.script}:${s.rel}`),
|
|
517
|
-
findings: [...results].map(([script, r]) => ({
|
|
833
|
+
findings: [...results].map(([script, r]) => ({
|
|
834
|
+
script,
|
|
835
|
+
confirmed: r.confirmed,
|
|
836
|
+
unproven: r.unproven,
|
|
837
|
+
})),
|
|
518
838
|
},
|
|
519
839
|
null,
|
|
520
840
|
2,
|
|
@@ -522,6 +842,18 @@ export function main(argv: string[]): number {
|
|
|
522
842
|
);
|
|
523
843
|
}
|
|
524
844
|
|
|
845
|
+
if (args.census) {
|
|
846
|
+
const rows = buildCensus(pop, census, repoRoot);
|
|
847
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
848
|
+
fs.mkdirSync(path.dirname(args.census), { recursive: true });
|
|
849
|
+
fs.writeFileSync(args.census, renderCensus(rows, pop.length, today));
|
|
850
|
+
if (!args.quiet) {
|
|
851
|
+
process.stdout.write(
|
|
852
|
+
`sweep_dead_scan_roots: census → ${args.census} (${rows.length} row(s) over ${pop.length} gate(s))\n`,
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
525
857
|
if (!args.quiet) {
|
|
526
858
|
process.stdout.write(`sweep_dead_scan_roots: ${pop.length} gate script(s) scanned\n`);
|
|
527
859
|
for (const l of lines) process.stdout.write(`${l}\n`);
|
|
@@ -534,14 +866,28 @@ export function main(argv: string[]): number {
|
|
|
534
866
|
);
|
|
535
867
|
}
|
|
536
868
|
|
|
537
|
-
//
|
|
538
|
-
|
|
869
|
+
// Class-A findings are judged against the ratchet, not against zero.
|
|
870
|
+
//
|
|
871
|
+
// This gate shipped deliberately OUTSIDE `task ci` because it exits 1 on
|
|
872
|
+
// pre-existing class-A debt, and "a gate that turns CI red on debt it did
|
|
873
|
+
// not create" is the exact failure this track exists to avoid — its own
|
|
874
|
+
// Taskfile entry says so, and defers the wiring to the
|
|
875
|
+
// `dead-gate-finding-triage` disposition. That disposition is now decided
|
|
876
|
+
// (repair + ratchet, council 2026-08-02), so the deferral is spent: a count
|
|
877
|
+
// at or under the recorded baseline passes and the sweep can run in CI,
|
|
878
|
+
// while any RISE still exits 1. A tree with no baseline file behaves exactly
|
|
879
|
+
// as before — any class-A finding fails.
|
|
880
|
+
const ratchet = checkRatchet({ gate: 'sweep_dead_scan_roots', actual: classA, repoRoot });
|
|
881
|
+
const exit = !ratchet.ok ? 1 : stale.length > 0 ? 3 : 0;
|
|
539
882
|
if (!args.quiet || exit !== 0) {
|
|
540
883
|
process.stdout.write(
|
|
541
884
|
`sweep_dead_scan_roots: ${confirmedCount} confirmed, ${unprovenCount} unproven, ` +
|
|
542
885
|
`${classA} class-A, ${stale.length} stale → exit ${exit}\n`,
|
|
543
886
|
);
|
|
544
887
|
}
|
|
888
|
+
if (classA > 0 || ratchet.status !== 'unbaselined') {
|
|
889
|
+
(ratchet.ok ? process.stdout : process.stderr).write(`${ratchet.message}\n`);
|
|
890
|
+
}
|
|
545
891
|
return exit;
|
|
546
892
|
}
|
|
547
893
|
|
|
@@ -39,6 +39,8 @@ import process from 'node:process';
|
|
|
39
39
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
40
40
|
import { parse as parseYaml } from 'yaml';
|
|
41
41
|
|
|
42
|
+
import { keyword_matches_anchored } from './router_telemetry.js';
|
|
43
|
+
|
|
42
44
|
const _HERE = fileURLToPath(import.meta.url);
|
|
43
45
|
|
|
44
46
|
// src/scripts/trigger_coverage.ts → parents[2] of the .py file is repo root.
|
|
@@ -76,8 +78,17 @@ export function fired_rules(prompt: string, router: Router): Set<string> {
|
|
|
76
78
|
for (const tier of ['tier_1', 'tier_2'] as const) {
|
|
77
79
|
for (const entry of router[tier] ?? []) {
|
|
78
80
|
for (const trig of entry.triggers ?? []) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
// Single matcher source of truth (road-to-tested-routing
|
|
82
|
+
// Phase 3): `keyword` is word-boundary anchored via the
|
|
83
|
+
// shared helper; `phrase` stays unanchored substring.
|
|
84
|
+
if ('keyword' in trig) {
|
|
85
|
+
const needle = String(trig['keyword']).toLowerCase();
|
|
86
|
+
if (needle !== '' && keyword_matches_anchored(low, needle)) {
|
|
87
|
+
fired.add(entry.id);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
} else if ('phrase' in trig) {
|
|
91
|
+
const needle = String(trig['phrase']).toLowerCase();
|
|
81
92
|
if (needle !== '' && low.includes(needle)) {
|
|
82
93
|
fired.add(entry.id);
|
|
83
94
|
break;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* Historical quirks are preserved deliberately — tests and downstream consumers pin the exact behaviour.
|
|
17
17
|
*
|
|
18
|
-
* Source of truth:
|
|
18
|
+
* Source of truth: `src/`.
|
|
19
19
|
*
|
|
20
20
|
* Target files have explicit regex patterns for each count mention — no fuzzy
|
|
21
21
|
* matching, no risk of touching unrelated numbers.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* changes — historical quirks preserved (consumers pin the exact behaviour).
|
|
11
11
|
*
|
|
12
12
|
* Given a pre-condensation reply and a post-condensation reply, assert that
|
|
13
|
-
* every carve-out region from
|
|
13
|
+
* every carve-out region from `src/rules/telegraph-speak.md`
|
|
14
14
|
* § Carve-outs survived byte-for-byte:
|
|
15
15
|
*
|
|
16
16
|
* 1. Triple-backtick code blocks (any language).
|
|
@@ -39,6 +39,9 @@ import {
|
|
|
39
39
|
needs_refresh,
|
|
40
40
|
} from "./_lib/cli_wrapper.js";
|
|
41
41
|
import { readHookStdin } from "./hooks/hook_stdin.js";
|
|
42
|
+
import { probeRanToday, stampProbeRun } from "./hooks/probe_throttle.js";
|
|
43
|
+
|
|
44
|
+
const THROTTLE_CONCERN = "wrapper-freshness";
|
|
42
45
|
|
|
43
46
|
export const EXIT_ALLOW = 0;
|
|
44
47
|
|
|
@@ -202,6 +205,17 @@ export function main(argv?: string[]): number {
|
|
|
202
205
|
_readStdinIfNotTty();
|
|
203
206
|
|
|
204
207
|
const root = rootArg !== null ? rootArg : _project_root();
|
|
208
|
+
|
|
209
|
+
// Daily throttle (road-to-hook-latency-repair): a wrapper or pre-commit
|
|
210
|
+
// hook that was fresh this morning is still fresh this afternoon — one
|
|
211
|
+
// self-heal attempt per local calendar day. Skipped probe re-runs
|
|
212
|
+
// tomorrow; `agent-config upgrade` / `refresh --project` remain the
|
|
213
|
+
// immediate recovery paths.
|
|
214
|
+
if (probeRanToday(root, THROTTLE_CONCERN)) {
|
|
215
|
+
return EXIT_ALLOW;
|
|
216
|
+
}
|
|
217
|
+
stampProbeRun(root, THROTTLE_CONCERN);
|
|
218
|
+
|
|
205
219
|
let wrapper: string | null;
|
|
206
220
|
try {
|
|
207
221
|
if (_is_source_repo(root)) {
|
|
@@ -53,8 +53,8 @@ export const settingsSchema = z.object({
|
|
|
53
53
|
rule_workspaces: z.array(z.string()).default([]).describe(
|
|
54
54
|
'Workspace scope for the RULE layer only (road-to-request-scoped-rule-load P1/P1b, opt-in). Absent or empty = legacy-all: every rule projects AND installs. Non-empty = only rules whose workspaces frontmatter intersects this list are projected (condense) and installed (install.sh + global wizard payload). Kernel rules always ship; untagged rules fail safe. The default flip to a scoped value is a HUMAN release gate — do not set this from automation.',
|
|
55
55
|
),
|
|
56
|
-
rule_packs: z.array(z.string()).default([]).describe(
|
|
57
|
-
'Optional second scoping axis for the RULE layer, per pack ids (src/config/discovery/packs.yml). When set, a non-kernel rule also needs a packs frontmatter intersection to ship — e.g. deselecting frontend-design drops ui-audit-gate + design-fidelity. Same opt-in / human-gate semantics as rule_workspaces.',
|
|
56
|
+
rule_packs: z.union([z.literal('auto'), z.array(z.string())]).default([]).describe(
|
|
57
|
+
'Optional second scoping axis for the RULE layer, per pack ids (src/config/discovery/packs.yml). When set, a non-kernel rule also needs a packs frontmatter intersection to ship — e.g. deselecting frontend-design drops ui-audit-gate + design-fidelity. Same opt-in / human-gate semantics as rule_workspaces. The literal "auto" derives the id list from the active-pack set (the same set the skill/command prune uses), so a domain safety floor stops shipping into installs that do not have the pack it guards; an explicit list stays supported and wins over the derivation.',
|
|
58
58
|
),
|
|
59
59
|
}).default({ mode: 'legacy-all' }),
|
|
60
60
|
rule_loading_tier: ruleLoadingTier.default('balanced').describe(
|
|
@@ -79,6 +79,17 @@ export const settingsSchema = z.object({
|
|
|
79
79
|
monthly: z.number().min(0).default(0).describe(
|
|
80
80
|
'Calendar-month USD ceiling. Pairs with cost.enforcement = hard-stop for a hard cap on agent spend before the next billing cycle. Set 0 to disable.',
|
|
81
81
|
),
|
|
82
|
+
per_tier: z.object({
|
|
83
|
+
cheap: z.number().min(0).nullable().default(null).describe(
|
|
84
|
+
'USD ceiling for the cheap model tier under budget-aware delegation (docs/contracts/budget-routing.md). null = no separate tier cap; global ceilings still apply.',
|
|
85
|
+
),
|
|
86
|
+
medium: z.number().min(0).nullable().default(null).describe(
|
|
87
|
+
'USD ceiling for the medium model tier. null = no separate tier cap.',
|
|
88
|
+
),
|
|
89
|
+
strong: z.number().min(0).nullable().default(null).describe(
|
|
90
|
+
'USD ceiling for the strong model tier. null = no separate tier cap — with cheap exhausted and strong funded, budget routing uses the strong tier rather than blocking work.',
|
|
91
|
+
),
|
|
92
|
+
}).default({ cheap: null, medium: null, strong: null }),
|
|
82
93
|
}),
|
|
83
94
|
enforcement: enforcementMode.default('advisory').describe(
|
|
84
95
|
'What happens when a budget hits 100%. advisory = show a banner, keep working (default — never blocks an active task). hard-stop = refuse further model calls until the budget resets or you raise the ceiling.',
|
|
@@ -103,7 +114,7 @@ export const settingsSchema = z.object({
|
|
|
103
114
|
'Prefer short bullets and tables (true, default) vs verbose prose with rationale (false). Affects every chat reply; flip to false during debugging when you want the agent to think out loud.',
|
|
104
115
|
),
|
|
105
116
|
canary_name: z.string().default('').describe(
|
|
106
|
-
'Session canary — the name the agent addresses you with at the start of every new task (e.g. "Alex"). When the greeting silently disappears, the context window is degrading: start a fresh conversation. Also keeps the reply-close markers (end-summary, PR URL as literal last line) alive. Empty = off. See rules/session-canary.md.',
|
|
117
|
+
'Session canary — the name the agent addresses you with at the start of every new task (e.g. "Alex"). When the greeting silently disappears, the context window is degrading: start a fresh conversation. Also keeps the reply-close markers (end-summary, PR URL as literal last line) alive. Empty = fall back to the user-global canary_name, then to identity.name from the setup wizard; no name anywhere = off. See rules/session-canary.md.',
|
|
107
118
|
),
|
|
108
119
|
play_by_play: z.boolean().default(false).describe(
|
|
109
120
|
'Narrate intermediate findings between tool calls ("Found it.", "Let me check Y."). Off by default — most users find it noisy. Turn on when you want to follow the agent\'s reasoning step by step.',
|
|
@@ -187,8 +198,8 @@ export const settingsSchema = z.object({
|
|
|
187
198
|
quality_cadence: qualityCadence.default('end_of_roadmap').describe(
|
|
188
199
|
'When the agent runs the full quality / test suite during /roadmap:process-* runs. end_of_roadmap = once, after the last step (fastest, default). per_phase = after each phase boundary. per_step = after every single step (slowest, highest confidence).',
|
|
189
200
|
),
|
|
190
|
-
dashboard_regen_cadence: regenCadence.default('
|
|
191
|
-
'How often the agent regenerates agents/roadmaps/dashboard.md during a roadmap run. per_step = after every step (
|
|
201
|
+
dashboard_regen_cadence: regenCadence.default('every_5_steps').describe(
|
|
202
|
+
'How often the agent regenerates agents/roadmaps/dashboard.md during a roadmap run. every_5_steps = batch the regen (default). per_step = after every step (freshest dashboard, highest subprocess overhead). phase_boundary = only at phase edges. A rename, phase add, or archive always regenerates immediately regardless.',
|
|
192
203
|
),
|
|
193
204
|
horizon_weeks: z.number().int().min(0).default(0).describe(
|
|
194
205
|
'Optional planning horizon (weeks) the agent shows in roadmap framing ("next 4 weeks"). Set 0 to omit the horizon — most teams prefer to ship without a hardcoded window.',
|
|
@@ -288,6 +299,9 @@ export const settingsSchema = z.object({
|
|
|
288
299
|
downshift: z.boolean().default(true).describe(
|
|
289
300
|
'Route delegable sub-tasks to the lowest-capable model tier (cost + speed via model downshift). false = every subagent runs on the session tier.',
|
|
290
301
|
),
|
|
302
|
+
budget_routing: z.enum(['ask', 'auto', 'off']).default('ask').describe(
|
|
303
|
+
'Budget-aware cheap-request delegation (docs/contracts/budget-routing.md): pick the cheapest classifier-adequate tier WITH available budget (cost.budgets.per_tier); cheap exhausted but strong funded → strong tier; all exhausted → session model + notice. Work is never blocked to save money; the session model is never switched (delegation with a model override). ask = confirm the first budget-motivated downshift per session (default). auto = apply silently with telemetry. off = today\'s behavior (single-flip rollback). Distinct from downshift: that is the quality/cost knob, this is the resource-availability gate.',
|
|
304
|
+
),
|
|
291
305
|
quota_arbitrage: z.boolean().default(true).describe(
|
|
292
306
|
'Prefer a separate quota-pool model for delegable sub-tasks where the host manifest reports one. Optional bonus only — identical behaviour (minus the quota win) where unsupported. Never load-bearing.',
|
|
293
307
|
),
|