@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
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Derive the routing-matrix router-coverage corpus (road-to-tested-routing
|
|
4
|
+
* Phase 2, PREREG P2 corpus expansion).
|
|
5
|
+
*
|
|
6
|
+
* Reads every per-rule routing matrix under `tests/eval/routing-matrix/`
|
|
7
|
+
* and emits ONE generated corpus file,
|
|
8
|
+
* `internal/bench/corpora/router-coverage/routing-matrix-derived.yaml`,
|
|
9
|
+
* with one `router-coverage` prompt per matrix POSITIVE, labelled
|
|
10
|
+
* `intended_triggers: [<rule>]`. Near-misses are deliberately NOT emitted —
|
|
11
|
+
* the corpus measures intended activation; the matrices' negative half is
|
|
12
|
+
* enforced by `tests/scripts/routing_matrix.test.ts`.
|
|
13
|
+
*
|
|
14
|
+
* Ownership (two-gate pattern): this generator is the only writer;
|
|
15
|
+
* `--check` re-derives in memory and exits 1 on drift, so the derived file
|
|
16
|
+
* can never be hand-edited into disagreement with its source matrices.
|
|
17
|
+
*/
|
|
18
|
+
import * as fs from "node:fs";
|
|
19
|
+
import * as path from "node:path";
|
|
20
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
21
|
+
import { parse as parseYaml } from "yaml";
|
|
22
|
+
|
|
23
|
+
const REPO_ROOT = path.resolve(
|
|
24
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
25
|
+
"..",
|
|
26
|
+
"..",
|
|
27
|
+
);
|
|
28
|
+
const MATRIX_DIR = path.join(REPO_ROOT, "tests", "eval", "routing-matrix");
|
|
29
|
+
export const OUT_FILE = path.join(
|
|
30
|
+
REPO_ROOT,
|
|
31
|
+
"internal",
|
|
32
|
+
"bench",
|
|
33
|
+
"corpora",
|
|
34
|
+
"router-coverage",
|
|
35
|
+
"routing-matrix-derived.yaml",
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
interface MatrixCase {
|
|
39
|
+
prompt: string;
|
|
40
|
+
open_files?: string[];
|
|
41
|
+
command?: string;
|
|
42
|
+
}
|
|
43
|
+
interface MatrixFile {
|
|
44
|
+
rule: string;
|
|
45
|
+
positives: MatrixCase[];
|
|
46
|
+
near_misses: MatrixCase[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Cheap language tag for the corpus `language:` field. */
|
|
50
|
+
export function detect_language(prompt: string): "de" | "en" {
|
|
51
|
+
if (/[äöüßÄÖÜ]/.test(prompt)) {
|
|
52
|
+
return "de";
|
|
53
|
+
}
|
|
54
|
+
return /(^|\s)(der|die|das|und|nicht|bitte|keine?|mach|prüfe|erstelle|für)(\s|$)/i.test(prompt)
|
|
55
|
+
? "de"
|
|
56
|
+
: "en";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function yamlQuote(s: string): string {
|
|
60
|
+
return JSON.stringify(s);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function derive(): string {
|
|
64
|
+
const files = fs
|
|
65
|
+
.readdirSync(MATRIX_DIR)
|
|
66
|
+
.filter((f) => f.endsWith(".yaml"))
|
|
67
|
+
.sort();
|
|
68
|
+
const lines: string[] = [];
|
|
69
|
+
lines.push("# Router-coverage corpus — DERIVED FROM the per-rule routing matrices.");
|
|
70
|
+
lines.push("#");
|
|
71
|
+
lines.push("# GENERATED FILE — do not hand-edit. Source of truth:");
|
|
72
|
+
lines.push("# tests/eval/routing-matrix/*.yaml");
|
|
73
|
+
lines.push("# Regenerate: npx tsx src/scripts/generate_router_coverage_from_matrix.ts");
|
|
74
|
+
lines.push("# Drift gate: npx tsx src/scripts/generate_router_coverage_from_matrix.ts --check");
|
|
75
|
+
lines.push("");
|
|
76
|
+
lines.push("version: 1");
|
|
77
|
+
lines.push("corpus_id: router-coverage-routing-matrix-derived");
|
|
78
|
+
lines.push("selection_accuracy_target: 0.0");
|
|
79
|
+
lines.push("");
|
|
80
|
+
lines.push("prompts:");
|
|
81
|
+
for (const f of files) {
|
|
82
|
+
const parsed = parseYaml(fs.readFileSync(path.join(MATRIX_DIR, f), "utf-8")) as MatrixFile;
|
|
83
|
+
parsed.positives.forEach((c, i) => {
|
|
84
|
+
const id = `rm-${parsed.rule}-${String(i + 1).padStart(2, "0")}`;
|
|
85
|
+
lines.push(` - id: ${id}`);
|
|
86
|
+
lines.push(" category: router-coverage");
|
|
87
|
+
lines.push(` language: ${detect_language(c.prompt)}`);
|
|
88
|
+
lines.push(` prompt: ${yamlQuote(c.prompt)}`);
|
|
89
|
+
lines.push(" expected_skills: []");
|
|
90
|
+
lines.push(` intended_triggers: [${parsed.rule}]`);
|
|
91
|
+
if (c.open_files && c.open_files.length > 0) {
|
|
92
|
+
lines.push(` open_files: [${c.open_files.map(yamlQuote).join(", ")}]`);
|
|
93
|
+
}
|
|
94
|
+
lines.push("");
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function main(argv?: string[]): number {
|
|
101
|
+
const args = argv ?? process.argv.slice(2);
|
|
102
|
+
const check = args.includes("--check");
|
|
103
|
+
const derived = derive();
|
|
104
|
+
if (check) {
|
|
105
|
+
let current = "";
|
|
106
|
+
try {
|
|
107
|
+
current = fs.readFileSync(OUT_FILE, "utf-8");
|
|
108
|
+
} catch {
|
|
109
|
+
process.stderr.write(`missing derived corpus: ${OUT_FILE} — run the generator\n`);
|
|
110
|
+
return 1;
|
|
111
|
+
}
|
|
112
|
+
if (current !== derived) {
|
|
113
|
+
process.stderr.write(
|
|
114
|
+
"routing-matrix-derived.yaml is stale — regenerate via generate_router_coverage_from_matrix\n",
|
|
115
|
+
);
|
|
116
|
+
return 1;
|
|
117
|
+
}
|
|
118
|
+
process.stdout.write("routing-matrix-derived corpus: fresh\n");
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
fs.writeFileSync(OUT_FILE, derived, "utf-8");
|
|
122
|
+
const promptCount = (derived.match(/^ {2}- id: /gm) ?? []).length;
|
|
123
|
+
const ruleCount = new Set(
|
|
124
|
+
[...derived.matchAll(/intended_triggers: \[([^\]]+)\]/g)].map((m) => m[1]),
|
|
125
|
+
).size;
|
|
126
|
+
process.stdout.write(
|
|
127
|
+
`wrote ${path.relative(REPO_ROOT, OUT_FILE)} — ${promptCount} prompts, ${ruleCount} distinct rule ids\n`,
|
|
128
|
+
);
|
|
129
|
+
return 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
|
|
133
|
+
function _isCliEntry(): boolean {
|
|
134
|
+
if (typeof __AGENT_CONFIG_BUNDLE__ !== "undefined" && __AGENT_CONFIG_BUNDLE__) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
if (process.argv[1] === undefined) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
141
|
+
if (import.meta.url === argvUrl) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
146
|
+
const argv1 = fs.realpathSync(path.resolve(process.argv[1]));
|
|
147
|
+
return here === argv1;
|
|
148
|
+
} catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (_isCliEntry()) {
|
|
154
|
+
process.exit(main());
|
|
155
|
+
}
|
|
@@ -91,6 +91,20 @@ concerns:
|
|
|
91
91
|
script: src/scripts/hooks/block_kernel_rule_writes.ts
|
|
92
92
|
args: []
|
|
93
93
|
fail_closed: true
|
|
94
|
+
# road-to-renewal-leverage Phase 2 — "fix the code, not the config".
|
|
95
|
+
# PreToolUse guard over the recorded allowlist-growth antipattern
|
|
96
|
+
# (autonomous-execution § Antipattern): warns as an allowlist grows within a
|
|
97
|
+
# session and BLOCKS past the cap that rule already states in words (>20
|
|
98
|
+
# entries in one session = the linter is wrong). Baselines and budget
|
|
99
|
+
# thresholds warn only — whether a rising count is weakening or a legitimate
|
|
100
|
+
# ratchet reset is not decidable from the edit alone. Council 2026-08-02
|
|
101
|
+
# rejected inventing a "fix-loop is active" predicate; the SESSION is the
|
|
102
|
+
# window the recorded rule already names. fail_closed: false — a malformed
|
|
103
|
+
# envelope must never block an unrelated edit.
|
|
104
|
+
block-config-weakening:
|
|
105
|
+
script: src/scripts/hooks/block_config_weakening.ts
|
|
106
|
+
args: []
|
|
107
|
+
fail_closed: false
|
|
94
108
|
# token-saving Phase 3 — PreToolUse RTK-wrap nudge. DETERMINISTIC: keys off a
|
|
95
109
|
# live `which rtk` probe (not the self-reported flag). When rtk is installed
|
|
96
110
|
# and the agent is about to run a single verbose CLI command that is NOT
|
|
@@ -211,7 +225,7 @@ platforms:
|
|
|
211
225
|
session_start: [chat-history, hot-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe]
|
|
212
226
|
session_end: [chat-history, memory-learn]
|
|
213
227
|
stop: [chat-history, hot-context, verify-before-complete]
|
|
214
|
-
pre_tool_use: [block-no-verify, block-kernel-rule-writes, rtk-wrap, design-slop, code-graph-nudge]
|
|
228
|
+
pre_tool_use: [block-no-verify, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, code-graph-nudge]
|
|
215
229
|
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
|
|
216
230
|
|
|
217
231
|
claude:
|
|
@@ -219,7 +233,7 @@ platforms:
|
|
|
219
233
|
session_end: [chat-history, memory-learn]
|
|
220
234
|
stop: [chat-history, hot-context, verify-before-complete, team-review-gate]
|
|
221
235
|
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff]
|
|
222
|
-
pre_tool_use: [block-no-verify, block-kernel-rule-writes, rtk-wrap, design-slop, code-graph-nudge]
|
|
236
|
+
pre_tool_use: [block-no-verify, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, code-graph-nudge]
|
|
223
237
|
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
|
|
224
238
|
|
|
225
239
|
# Cowork — the Claude desktop app's local-agent-mode runtime, built
|
|
@@ -241,7 +255,7 @@ platforms:
|
|
|
241
255
|
session_end: [chat-history, memory-learn]
|
|
242
256
|
stop: [chat-history, hot-context, verify-before-complete]
|
|
243
257
|
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff]
|
|
244
|
-
pre_tool_use: [block-no-verify, block-kernel-rule-writes, rtk-wrap, design-slop, code-graph-nudge]
|
|
258
|
+
pre_tool_use: [block-no-verify, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, code-graph-nudge]
|
|
245
259
|
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
|
|
246
260
|
|
|
247
261
|
# Phase 7.5 — Cursor. `.cursor/hooks.json` (project) is read by the
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// MIGRATE: precompiled-hook-layer — carry this hook when road-to-credible-install
|
|
3
|
+
// Phase 1 precompiles the hook path (touch-once preserved at migration).
|
|
4
|
+
/**
|
|
5
|
+
* PreToolUse guard: "fix the code, not the config."
|
|
6
|
+
*
|
|
7
|
+
* The recorded antipattern (`autonomous-execution` § Antipattern —
|
|
8
|
+
* allowlist-growth as silent budget bypass): during a fix loop the agent
|
|
9
|
+
* weakens the gate instead of the code, growing a lint allowlist entry by
|
|
10
|
+
* entry until the linter no longer objects. The rule already states the
|
|
11
|
+
* threshold in words —
|
|
12
|
+
*
|
|
13
|
+
* ALLOWLIST > 20 ENTRIES IN ONE SESSION = THE LINTER IS WRONG.
|
|
14
|
+
* STOP. PROPOSE LINTER REDESIGN OR REMOVAL.
|
|
15
|
+
*
|
|
16
|
+
* — and two linters cite it in their own failure messages. Nothing enforced
|
|
17
|
+
* it: by the time CI sees the diff the session has already spent its budget
|
|
18
|
+
* on the wrong fix. This hook moves that stated threshold to tool-call time.
|
|
19
|
+
*
|
|
20
|
+
* Deliberately NOT built (council 2026-08-02, anthropic/claude-sonnet-4-5 +
|
|
21
|
+
* openai/gpt-4o):
|
|
22
|
+
*
|
|
23
|
+
* - **No invented "fix-loop is active" predicate.** Both members rejected it
|
|
24
|
+
* as speculative — no such state exists in the repo and defining entry/exit
|
|
25
|
+
* invites false positives in both directions. The SESSION is the window the
|
|
26
|
+
* recorded rule already names, so the session id off the hook envelope is
|
|
27
|
+
* the predicate, and it is not an invention.
|
|
28
|
+
* - **No blocking on baselines or budgets.** Whether a violation-baseline
|
|
29
|
+
* count rising is weakening or a legitimate ratchet reset after a refactor
|
|
30
|
+
* is not mechanically decidable from the edit alone. Those surfaces warn
|
|
31
|
+
* (exit 2) and stay a review decision; only allowlist growth — mechanical,
|
|
32
|
+
* countable, and already carrying a stated numeric threshold — blocks.
|
|
33
|
+
*
|
|
34
|
+
* Cumulative within a session, per allowlist file. Small additions warn from
|
|
35
|
+
* the first one so the count is visible while it is still cheap to change
|
|
36
|
+
* course; crossing the recorded cap blocks.
|
|
37
|
+
*
|
|
38
|
+
* Exit codes (docs/contracts/hook-architecture-v1.md):
|
|
39
|
+
* 0 — allow
|
|
40
|
+
* 1 — block
|
|
41
|
+
* 2 — warn
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import * as fs from 'node:fs';
|
|
45
|
+
import * as path from 'node:path';
|
|
46
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
47
|
+
|
|
48
|
+
import { EDIT_TOOLS } from '../minimal_safe_diff_hook.js';
|
|
49
|
+
import { readHookStdin } from './hook_stdin.js';
|
|
50
|
+
|
|
51
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
52
|
+
|
|
53
|
+
const EXIT_ALLOW = 0;
|
|
54
|
+
const EXIT_BLOCK = 1;
|
|
55
|
+
const EXIT_WARN = 2;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The stated cap from `autonomous-execution` § Antipattern. Crossing it in one
|
|
59
|
+
* session is defined there as the third validation-target failure — the point
|
|
60
|
+
* at which the linter, not the content, is wrong.
|
|
61
|
+
*/
|
|
62
|
+
export const SESSION_ENTRY_CAP = 20;
|
|
63
|
+
|
|
64
|
+
/** Warn from this many cumulative added entries so the count stays visible. */
|
|
65
|
+
export const SESSION_WARN_AT = 5;
|
|
66
|
+
|
|
67
|
+
type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
|
|
68
|
+
type JsonObject = { [k: string]: JsonValue };
|
|
69
|
+
|
|
70
|
+
function _isObject(v: JsonValue | undefined): v is JsonObject {
|
|
71
|
+
return v !== null && v !== undefined && typeof v === 'object' && !Array.isArray(v);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const _PATH_KEYS: readonly string[] = ['file_path', 'path', 'target_file', 'filePath', 'notebook_path'];
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Config surfaces this guard recognises.
|
|
78
|
+
*
|
|
79
|
+
* `allowlist` is the blocking class — an entry count is a plain number and the
|
|
80
|
+
* recorded rule already fixes its threshold. `advisory` covers the surfaces
|
|
81
|
+
* where "weakening" needs context a tool call does not carry (a baseline count
|
|
82
|
+
* may legitimately reset after a refactor); those warn and never block.
|
|
83
|
+
*/
|
|
84
|
+
export type ConfigKind = 'allowlist' | 'advisory' | null;
|
|
85
|
+
|
|
86
|
+
/** Classify a target path into a config surface, or null for everything else. */
|
|
87
|
+
export function classify_target(p: string): ConfigKind {
|
|
88
|
+
const posix = p.split(path.sep).join('/');
|
|
89
|
+
const base = posix.split('/').pop() ?? '';
|
|
90
|
+
if (/_allowlist\.(json|txt)$/.test(base) || /^allowlist[-_.]/.test(base)) {
|
|
91
|
+
return 'allowlist';
|
|
92
|
+
}
|
|
93
|
+
if (/(^|-)baselines?\.json$/.test(base)) {
|
|
94
|
+
return 'advisory';
|
|
95
|
+
}
|
|
96
|
+
if (/-budget(s)?\.(json|ya?ml)$/.test(base) || base === 'budgets.yml') {
|
|
97
|
+
return 'advisory';
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Count allowlist entries in a blob.
|
|
104
|
+
*
|
|
105
|
+
* JSON: every string leaf in the structure (arrays of paths, and
|
|
106
|
+
* `{"rule": ["path", …]}` maps alike) — parsing beats a line count because a
|
|
107
|
+
* reformat would otherwise read as growth. Falls back to non-blank,
|
|
108
|
+
* non-comment lines for `.txt` allowlists and for JSON fragments that do not
|
|
109
|
+
* parse on their own (an `Edit` payload is rarely a whole document).
|
|
110
|
+
*/
|
|
111
|
+
export function count_entries(text: string): number {
|
|
112
|
+
const trimmed = text.trim();
|
|
113
|
+
if (trimmed) {
|
|
114
|
+
try {
|
|
115
|
+
let n = 0;
|
|
116
|
+
const walk = (v: unknown): void => {
|
|
117
|
+
if (typeof v === 'string') {
|
|
118
|
+
n += 1;
|
|
119
|
+
} else if (Array.isArray(v)) {
|
|
120
|
+
v.forEach(walk);
|
|
121
|
+
} else if (v !== null && typeof v === 'object') {
|
|
122
|
+
Object.values(v as Record<string, unknown>).forEach(walk);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
walk(JSON.parse(trimmed));
|
|
126
|
+
return n;
|
|
127
|
+
} catch {
|
|
128
|
+
/* not a standalone JSON document — fall through */
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return text
|
|
132
|
+
.split(/\r?\n/)
|
|
133
|
+
.map((l) => l.trim())
|
|
134
|
+
.filter((l) => l.length > 0 && !l.startsWith('#') && !l.startsWith('//')).length;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Entries this single tool call adds to an allowlist.
|
|
139
|
+
*
|
|
140
|
+
* `Write` carries the whole new document, so the delta is measured against
|
|
141
|
+
* what is on disk. `Edit` carries a replacement pair, so the delta is the pair
|
|
142
|
+
* itself. Negative results (a shrinking allowlist — the direction this guard
|
|
143
|
+
* wants) collapse to 0: removals must never bank credit against a later
|
|
144
|
+
* addition.
|
|
145
|
+
*/
|
|
146
|
+
export function added_entries(
|
|
147
|
+
tool_input: JsonObject,
|
|
148
|
+
on_disk: string | null,
|
|
149
|
+
): number {
|
|
150
|
+
const content = tool_input['content'];
|
|
151
|
+
if (typeof content === 'string') {
|
|
152
|
+
const before = on_disk === null ? 0 : count_entries(on_disk);
|
|
153
|
+
return Math.max(0, count_entries(content) - before);
|
|
154
|
+
}
|
|
155
|
+
const oldS = tool_input['old_string'] ?? tool_input['oldStr'] ?? tool_input['old_str'];
|
|
156
|
+
const newS = tool_input['new_string'] ?? tool_input['newStr'] ?? tool_input['new_str'];
|
|
157
|
+
if (typeof newS === 'string') {
|
|
158
|
+
const before = typeof oldS === 'string' ? count_entries(oldS) : 0;
|
|
159
|
+
return Math.max(0, count_entries(newS) - before);
|
|
160
|
+
}
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface Decision {
|
|
165
|
+
action: 'allow' | 'warn' | 'block';
|
|
166
|
+
reason: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Decide from the cumulative session total for one allowlist file. */
|
|
170
|
+
export function decide(kind: ConfigKind, rel_path: string, session_total: number, added: number): Decision {
|
|
171
|
+
if (kind === null || added <= 0) {
|
|
172
|
+
return { action: 'allow', reason: '' };
|
|
173
|
+
}
|
|
174
|
+
if (kind === 'advisory') {
|
|
175
|
+
return {
|
|
176
|
+
action: 'warn',
|
|
177
|
+
reason:
|
|
178
|
+
`${rel_path}: this edit loosens a gate threshold. Whether that is a ` +
|
|
179
|
+
'legitimate ratchet reset or the config being bent around a failing ' +
|
|
180
|
+
'check is a review decision — state which in the diff.',
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
if (session_total > SESSION_ENTRY_CAP) {
|
|
184
|
+
return {
|
|
185
|
+
action: 'block',
|
|
186
|
+
reason:
|
|
187
|
+
`${rel_path}: ${session_total} allowlist entries added this session ` +
|
|
188
|
+
`(cap ${SESSION_ENTRY_CAP}). Per autonomous-execution § Antipattern — ` +
|
|
189
|
+
'allowlist-growth as silent budget bypass, crossing this cap means the ' +
|
|
190
|
+
'LINTER is wrong, not the content. Fix the code or redesign the check; ' +
|
|
191
|
+
'do not expand the allowlist further.\n' +
|
|
192
|
+
' Legitimate override is a human action outside this session: land the ' +
|
|
193
|
+
'linter redesign, or remove the `block-config-weakening` entry in ' +
|
|
194
|
+
'src/scripts/hook_manifest.yaml.',
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
if (session_total >= SESSION_WARN_AT) {
|
|
198
|
+
return {
|
|
199
|
+
action: 'warn',
|
|
200
|
+
reason:
|
|
201
|
+
`${rel_path}: ${session_total} allowlist entries added this session ` +
|
|
202
|
+
`(cap ${SESSION_ENTRY_CAP}). Fix the code, not the config.`,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
return { action: 'allow', reason: '' };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// --- session state -------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
function _state_file(root: string): string {
|
|
211
|
+
return path.join(root, 'agents', 'runtime', 'state', 'config-weakening.json');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Read the cumulative count, add `added`, persist, and return the new total. */
|
|
215
|
+
export function bump_session(root: string, session: string, key: string, added: number): number {
|
|
216
|
+
const f = _state_file(root);
|
|
217
|
+
let data: Record<string, Record<string, number>> = {};
|
|
218
|
+
try {
|
|
219
|
+
data = JSON.parse(fs.readFileSync(f, 'utf-8')) as Record<string, Record<string, number>>;
|
|
220
|
+
} catch {
|
|
221
|
+
data = {};
|
|
222
|
+
}
|
|
223
|
+
const bucket = data[session] ?? {};
|
|
224
|
+
const total = (bucket[key] ?? 0) + added;
|
|
225
|
+
bucket[key] = total;
|
|
226
|
+
data[session] = bucket;
|
|
227
|
+
try {
|
|
228
|
+
fs.mkdirSync(path.dirname(f), { recursive: true });
|
|
229
|
+
fs.writeFileSync(f, `${JSON.stringify(data, null, 2)}\n`, 'utf-8');
|
|
230
|
+
} catch {
|
|
231
|
+
/* state is an optimisation, never a gate — a read-only tree still decides */
|
|
232
|
+
}
|
|
233
|
+
return total;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// --- entry ---------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
function _extract(envelope: JsonObject): { tool: string; paths: string[]; ti: JsonObject | null } {
|
|
239
|
+
const payload = _isObject(envelope['payload']) ? envelope['payload'] : envelope;
|
|
240
|
+
const nameVal =
|
|
241
|
+
payload['tool_name'] ?? payload['toolName'] ?? payload['tool'] ?? envelope['tool_name'] ?? envelope['tool'];
|
|
242
|
+
const tool = typeof nameVal === 'string' ? nameVal : '';
|
|
243
|
+
const ti = _isObject(payload['tool_input'])
|
|
244
|
+
? payload['tool_input']
|
|
245
|
+
: _isObject(envelope['tool_input'])
|
|
246
|
+
? envelope['tool_input']
|
|
247
|
+
: null;
|
|
248
|
+
const paths: string[] = [];
|
|
249
|
+
if (ti !== null) {
|
|
250
|
+
for (const key of _PATH_KEYS) {
|
|
251
|
+
const v = ti[key];
|
|
252
|
+
if (typeof v === 'string' && v) {
|
|
253
|
+
paths.push(v);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return { tool, paths, ti };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function main(): number {
|
|
261
|
+
let envelope: JsonValue;
|
|
262
|
+
try {
|
|
263
|
+
const raw = readHookStdin();
|
|
264
|
+
envelope = raw.trim() ? (JSON.parse(raw) as JsonValue) : {};
|
|
265
|
+
} catch {
|
|
266
|
+
return EXIT_ALLOW;
|
|
267
|
+
}
|
|
268
|
+
if (!_isObject(envelope)) {
|
|
269
|
+
return EXIT_ALLOW;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const { tool, paths, ti } = _extract(envelope);
|
|
273
|
+
if (!tool || !EDIT_TOOLS.has(tool) || ti === null) {
|
|
274
|
+
return EXIT_ALLOW;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const cwd = envelope['cwd'];
|
|
278
|
+
const pr = envelope['workspace_root'] ?? envelope['project_root'];
|
|
279
|
+
const root = typeof cwd === 'string' && cwd ? cwd : typeof pr === 'string' && pr ? pr : '.';
|
|
280
|
+
const sidRaw = envelope['session_id'] ?? envelope['sessionId'];
|
|
281
|
+
const session = typeof sidRaw === 'string' && sidRaw ? sidRaw : 'default';
|
|
282
|
+
|
|
283
|
+
for (const p of paths) {
|
|
284
|
+
const kind = classify_target(p);
|
|
285
|
+
if (kind === null) {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const abs = path.isAbsolute(p) ? p : path.join(root, p);
|
|
289
|
+
let on_disk: string | null = null;
|
|
290
|
+
try {
|
|
291
|
+
on_disk = fs.readFileSync(abs, 'utf-8');
|
|
292
|
+
} catch {
|
|
293
|
+
on_disk = null;
|
|
294
|
+
}
|
|
295
|
+
const added = added_entries(ti, on_disk);
|
|
296
|
+
if (added <= 0) {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
const rel = path.relative(root, abs) || p;
|
|
300
|
+
const total = kind === 'allowlist' ? bump_session(root, session, rel, added) : added;
|
|
301
|
+
const d = decide(kind, rel, total, added);
|
|
302
|
+
if (d.action === 'block') {
|
|
303
|
+
process.stderr.write(`block-config-weakening: BLOCKED — ${d.reason}\n`);
|
|
304
|
+
return EXIT_BLOCK;
|
|
305
|
+
}
|
|
306
|
+
if (d.action === 'warn') {
|
|
307
|
+
process.stdout.write(`${JSON.stringify({ decision: 'warn', reason: d.reason })}\n`);
|
|
308
|
+
return EXIT_WARN;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return EXIT_ALLOW;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Bundle-safety: never auto-run when inlined into an esbuild bundle, where
|
|
315
|
+
// every module shares the bundle's `import.meta.url`.
|
|
316
|
+
declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
|
|
317
|
+
function _isCliEntry(): boolean {
|
|
318
|
+
if (typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__) {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
if (process.argv[1] === undefined) {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
325
|
+
if (import.meta.url === argvUrl) {
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
330
|
+
} catch {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
336
|
+
process.exit(main());
|
|
337
|
+
}
|
|
@@ -42,6 +42,7 @@ import { main as prUrlReminderMain } from '../pr_url_reminder_hook.js';
|
|
|
42
42
|
import { main as sessionCanaryMain } from '../session_canary_hook.js';
|
|
43
43
|
import { main as blockNoVerifyMain } from './block_no_verify.js';
|
|
44
44
|
import { main as blockKernelRuleWritesMain } from './block_kernel_rule_writes.js';
|
|
45
|
+
import { main as blockConfigWeakeningMain } from './block_config_weakening.js';
|
|
45
46
|
import { main as rtkWrapMain } from './rtk_wrap_hook.js';
|
|
46
47
|
import { main as designSlopMain } from './design_slop_hook.js';
|
|
47
48
|
import { main as codeGraphNudgeMain } from './code_graph_nudge_hook.js';
|
|
@@ -69,6 +70,7 @@ export const CONCERN_REGISTRY: Readonly<Record<string, ConcernMain>> = {
|
|
|
69
70
|
'src/scripts/session_canary_hook.ts': sessionCanaryMain as ConcernMain,
|
|
70
71
|
'src/scripts/hooks/block_no_verify.ts': blockNoVerifyMain as ConcernMain,
|
|
71
72
|
'src/scripts/hooks/block_kernel_rule_writes.ts': blockKernelRuleWritesMain as ConcernMain,
|
|
73
|
+
'src/scripts/hooks/block_config_weakening.ts': blockConfigWeakeningMain as ConcernMain,
|
|
72
74
|
'src/scripts/hooks/rtk_wrap_hook.ts': rtkWrapMain as ConcernMain,
|
|
73
75
|
'src/scripts/hooks/design_slop_hook.ts': designSlopMain as ConcernMain,
|
|
74
76
|
'src/scripts/hooks/code_graph_nudge_hook.ts': codeGraphNudgeMain as ConcernMain,
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* probe_throttle — daily throttle for session_start probe concerns
|
|
3
|
+
* (road-to-hook-latency-repair Phase 2, SessionStart fan-out lever).
|
|
4
|
+
*
|
|
5
|
+
* `profile-staleness`, `wrapper-freshness`, and `surface-probe` are
|
|
6
|
+
* defense-in-depth probes whose findings do not change within a day; running
|
|
7
|
+
* them on EVERY session_start pays their cost per session for no new
|
|
8
|
+
* information. Each probe checks a per-concern stamp file under
|
|
9
|
+
* `agents/runtime/state/probe-throttle/` and skips when the stamp's mtime is
|
|
10
|
+
* from the same LOCAL calendar day (a skipped probe re-runs tomorrow).
|
|
11
|
+
*
|
|
12
|
+
* Deliberate simplicity (roadmap lock — "an mtime file, not a daemon"):
|
|
13
|
+
* - The stamp carries NO semantics beyond its mtime. Deleting the file or
|
|
14
|
+
* the directory only changes WHEN a probe re-runs, never what it answers.
|
|
15
|
+
* - Same-day equality is skew-safe: a stamp dated in the future or the past
|
|
16
|
+
* is simply "not today", so the probe runs and re-stamps — there is no
|
|
17
|
+
* lockout state (council 2026-08-03).
|
|
18
|
+
* - Fail-open both ways: an unreadable stamp means "run the probe"; an
|
|
19
|
+
* unwritable state dir means the probe simply runs again next session.
|
|
20
|
+
* - Replay runs (AGENT_CONFIG_REPLAY=1) bypass the throttle and never
|
|
21
|
+
* write stamps — fixture re-execution and the latency bench must be
|
|
22
|
+
* deterministic, not dependent on yesterday's stamps (state_io replay
|
|
23
|
+
* contract).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import fs from 'node:fs';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
|
|
29
|
+
import { is_replay_mode } from './state_io.js';
|
|
30
|
+
|
|
31
|
+
const STATE_SUBDIR = ['agents', 'runtime', 'state', 'probe-throttle'] as const;
|
|
32
|
+
|
|
33
|
+
function _stampPath(projectRoot: string, concern: string): string {
|
|
34
|
+
return path.join(projectRoot, ...STATE_SUBDIR, `${concern}.stamp`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Local-calendar-day key (YYYY-MM-DD in local time, not UTC). */
|
|
38
|
+
function _localDay(d: Date): string {
|
|
39
|
+
const y = d.getFullYear();
|
|
40
|
+
const m = String(d.getMonth() + 1).padStart(2, '0');
|
|
41
|
+
const day = String(d.getDate()).padStart(2, '0');
|
|
42
|
+
return `${y}-${m}-${day}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* True when the probe already ran today (same LOCAL calendar day as the
|
|
47
|
+
* stamp's mtime) and may be skipped. Replay mode and any read problem
|
|
48
|
+
* return false — the probe runs.
|
|
49
|
+
*/
|
|
50
|
+
export function probeRanToday(projectRoot: string, concern: string): boolean {
|
|
51
|
+
if (is_replay_mode()) return false;
|
|
52
|
+
try {
|
|
53
|
+
const st = fs.statSync(_stampPath(projectRoot, concern));
|
|
54
|
+
return _localDay(st.mtime) === _localDay(new Date());
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Record that the probe ran now. Fail-silent — an unwritable state dir just
|
|
62
|
+
* means the probe runs again next session. No-op in replay mode.
|
|
63
|
+
*/
|
|
64
|
+
export function stampProbeRun(projectRoot: string, concern: string): void {
|
|
65
|
+
if (is_replay_mode()) return;
|
|
66
|
+
try {
|
|
67
|
+
const p = _stampPath(projectRoot, concern);
|
|
68
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
69
|
+
// Content is a human-readable timestamp for debugging only — the
|
|
70
|
+
// mtime is the sole semantic carrier.
|
|
71
|
+
fs.writeFileSync(p, `${new Date().toISOString()}\n`);
|
|
72
|
+
} catch {
|
|
73
|
+
/* fail-silent */
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -266,10 +266,20 @@ write_chat_history_hook "post-rewrite" "git:post-rewrite"
|
|
|
266
266
|
# `.augment/`, etc. projections until they remember to run `task sync`.
|
|
267
267
|
# These hooks bridge that gap: fast idempotent re-projection.
|
|
268
268
|
#
|
|
269
|
+
# The same staleness bites the compiled CLI: the Claude/agent hooks run
|
|
270
|
+
# `./agent-config` → `dist/cli/` (gitignored tsc output). A pull that adds
|
|
271
|
+
# or moves a TS module leaves `dist/` stale and EVERY subsequent tool-use
|
|
272
|
+
# hook fails with ERR_MODULE_NOT_FOUND until someone rebuilds. So when the
|
|
273
|
+
# range touches TS sources, rebuild via `npm run build:cli` — tsc only,
|
|
274
|
+
# deliberately NOT the full `npm run build`: the install-bundle step
|
|
275
|
+
# rewrites the committed `dist/install/install.mjs` and has produced
|
|
276
|
+
# spurious tracked diffs from worktrees before.
|
|
277
|
+
#
|
|
269
278
|
# Bypass: `git pull --no-verify` does not exist, but devs can disable the
|
|
270
279
|
# hooks per-command via `git -c core.hooksPath=/dev/null ...` or by
|
|
271
280
|
# editing the file. Runtime ~200 ms when nothing relevant changed
|
|
272
|
-
# (path-diff check exits early); ~2 s on full re-projection
|
|
281
|
+
# (path-diff check exits early); ~2 s on full re-projection; ~2 s extra
|
|
282
|
+
# for the tsc rebuild when TS sources changed.
|
|
273
283
|
|
|
274
284
|
append_auto_sync_block() {
|
|
275
285
|
local name="$1"
|
|
@@ -294,13 +304,25 @@ elif [ "$name" = "post-checkout" ]; then
|
|
|
294
304
|
fi
|
|
295
305
|
|
|
296
306
|
if [ -n "\$prev" ] && [ -n "\$new" ] && [ "\$prev" != "\$new" ]; then
|
|
297
|
-
|
|
307
|
+
changed="\$(git diff --name-only "\$prev" "\$new" 2>/dev/null)"
|
|
308
|
+
if printf '%s\n' "\$changed" | \\
|
|
298
309
|
grep -qE '^(dist/agent-src/|\\.agent-src\\.uncondensed/|src/scripts/condense\\.ts|\\.agent-tools\\.yml|Taskfile\\.yml)'; then
|
|
299
310
|
if command -v task >/dev/null 2>&1; then
|
|
300
311
|
task sync >/dev/null 2>&1 || true
|
|
301
312
|
task generate-tools >/dev/null 2>&1 || true
|
|
302
313
|
fi
|
|
303
314
|
fi
|
|
315
|
+
# TS sources changed → the gitignored tsc output (dist/cli, dist/scripts,
|
|
316
|
+
# …) is stale and the ./agent-config hook chain breaks with
|
|
317
|
+
# ERR_MODULE_NOT_FOUND. Rebuild tsc output only (build:cli), never the
|
|
318
|
+
# full build (install-bundle rewrites committed dist/install/install.mjs).
|
|
319
|
+
# Skip silently without node_modules (fresh clone / bare worktree).
|
|
320
|
+
if printf '%s\n' "\$changed" | \\
|
|
321
|
+
grep -qE '^(src/(cli|server|shared|install|scripts)/|tsconfig\\.json|package\\.json)'; then
|
|
322
|
+
if command -v npm >/dev/null 2>&1 && [ -d node_modules ]; then
|
|
323
|
+
npm run build:cli >/dev/null 2>&1 || true
|
|
324
|
+
fi
|
|
325
|
+
fi
|
|
304
326
|
fi
|
|
305
327
|
EOF
|
|
306
328
|
}
|