@event4u/agent-config 14.17.0 → 14.19.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/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +207 -77
- package/README.md +72 -150
- package/dist/agent-src/README.md +1 -1
- package/dist/agent-src/commands/agent-handoff.md +2 -2
- package/dist/agent-src/commands/pr/merge.md +35 -44
- package/dist/agent-src/commands/review/changes.md +19 -19
- package/dist/agent-src/commands/review.md +2 -2
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +30 -3
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/git-history-discipline.md +2 -2
- package/dist/agent-src/rules/guidelines.md +1 -1
- package/dist/agent-src/rules/session-canary.md +2 -2
- package/dist/agent-src/rules/token-efficiency.md +1 -1
- package/dist/agent-src/skills/accessibility-auditor/SKILL.md +2 -0
- package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
- package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +27 -1
- package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
- package/dist/agent-src/skills/code-intelligence/SKILL.md +60 -12
- package/dist/agent-src/skills/complexity-first-planning/SKILL.md +27 -0
- package/dist/agent-src/skills/design-intelligence/SKILL.md +1 -1
- package/dist/agent-src/skills/design-intelligence/data/app-interface.csv +1 -1
- package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -17
- package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +2 -2
- package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +17 -0
- package/dist/agent-src/skills/design-intelligence/references/design-rules-checklist.md +4 -4
- package/dist/agent-src/skills/design-review/SKILL.md +3 -1
- package/dist/agent-src/skills/design-variations/SKILL.md +1 -1
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
- package/dist/agent-src/skills/fe-design/SKILL.md +32 -5
- package/dist/agent-src/skills/fe-design/references/craft-floor.md +7 -0
- package/dist/agent-src/skills/fe-design/references/design-patterns.md +47 -2
- package/dist/agent-src/skills/feature-planning/SKILL.md +34 -0
- package/dist/agent-src/skills/git-workflow/SKILL.md +37 -12
- package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +176 -0
- package/dist/agent-src/skills/laravel-mail/SKILL.md +44 -0
- package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -1
- package/dist/agent-src/skills/project-analyzer/SKILL.md +0 -6
- package/dist/agent-src/skills/react-native-setup/SKILL.md +3 -3
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
- package/dist/agent-src/skills/sequential-thinking/SKILL.md +18 -8
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
- package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +40 -0
- package/dist/agent-src/skills/ui-apply-generic/SKILL.md +1 -1
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +1 -1
- package/dist/agent-src/skills/using-git-worktrees/SKILL.md +29 -2
- package/dist/agent-src/skills/verify-repair-loop/SKILL.md +53 -8
- package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +3 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/commands/recordTriggerEval.js +2 -2
- package/dist/cli/main.js +1 -1
- package/dist/cli-delegate/{chunk-AKJGUQNO.js → chunk-GZTMRYCN.js} +405 -341
- package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-IDHUQIXP.js} +283 -132
- package/dist/cli-delegate/cmd_adr_effective.js +318 -81
- package/dist/cli-delegate/cmd_conformance.js +749 -669
- package/dist/cli-delegate/cmd_doctor.js +1 -1
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +1 -1
- package/dist/cli-delegate/cmd_uninstall.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +39 -38
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.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 +2764 -2439
- package/dist/install/install.mjs +588 -527
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1381 -1138
- package/dist/scripts/_lib/model_tier.js +1 -1
- package/dist/shared/settingsConsent.js +1 -1
- package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
- package/docs/guidelines/design-antipatterns.md +87 -64
- package/package.json +1 -1
- package/src/agent-src/README.md +1 -1
- package/src/config/ci-local-parity.yml +6 -0
- package/src/config/conformance-claim-baseline.json +9 -0
- package/src/config/gate-coverage.yml +170 -0
- package/src/config/gate-violation-baselines.json +4 -4
- package/src/config/release-gate-locality.yml +141 -0
- package/src/scripts/_cli/cmd_doctor.ts +2 -2
- package/src/scripts/_cli/cmd_handoff.ts +1 -1
- package/src/scripts/_cli/cmd_update.ts +1 -1
- package/src/scripts/_cli/cmd_versions.ts +1 -1
- package/src/scripts/_lib/adr_frontmatter.ts +44 -0
- package/src/scripts/_lib/consequence_objects.ts +152 -0
- package/src/scripts/_lib/exec_evidence.ts +7 -0
- package/src/scripts/_lib/global_deploy_inventory.ts +2 -2
- package/src/scripts/_lib/host_permission_checks.ts +251 -0
- package/src/scripts/_lib/model_tier.ts +1 -1
- package/src/scripts/_lib/obligation_frequency.ts +2 -2
- package/src/scripts/_lib/release_highlights.ts +265 -1
- package/src/scripts/_lib/release_material.ts +129 -0
- package/src/scripts/_lib/repeated_run.ts +77 -0
- package/src/scripts/_lib/runtime_journal.ts +26 -3
- package/src/scripts/_lib/runtime_wiring_checks.ts +12 -1
- package/src/scripts/_lib/session_register.ts +11 -6
- package/src/scripts/_lib/sqlite_schema.ts +22 -0
- package/src/scripts/_lib/surface.ts +86 -0
- package/src/scripts/_lib/tamper_vocabulary.ts +242 -0
- package/src/scripts/_lib/ui_authority.ts +116 -0
- package/src/scripts/_lib/unified_diff.ts +121 -0
- package/src/scripts/adr/regenerate_index.ts +54 -1
- package/src/scripts/adr_cite_check.ts +71 -15
- package/src/scripts/ai_council/clients.ts +1 -1
- package/src/scripts/autonomy_friction_corpus.ts +229 -0
- package/src/scripts/build_proof.ts +15 -2
- package/src/scripts/check_beta_review_markers.ts +141 -17
- package/src/scripts/check_enforcement_coverage.ts +1 -1
- package/src/scripts/check_enforcement_denominator.ts +121 -11
- package/src/scripts/check_finding_dispositions.ts +239 -6
- package/src/scripts/check_gate_coverage.ts +96 -0
- package/src/scripts/check_installed_hooks_fresh.ts +381 -0
- package/src/scripts/check_release_highlights.ts +99 -14
- package/src/scripts/check_skill_activation_claim.ts +179 -0
- package/src/scripts/check_suppression_hygiene.ts +11 -0
- package/src/scripts/check_trigger_evals.ts +1 -1
- package/src/scripts/code_graph/build.ts +230 -33
- package/src/scripts/code_graph/extract.ts +168 -33
- package/src/scripts/code_graph/types.ts +37 -2
- package/src/scripts/code_graph/validate.ts +12 -1
- package/src/scripts/conformance_scan.ts +45 -4
- package/src/scripts/detect_silent_catch.ts +403 -0
- package/src/scripts/detect_verification_tampering.ts +538 -0
- package/src/scripts/explain_run.ts +1 -1
- package/src/scripts/generate_capability_matrix.ts +1 -1
- package/src/scripts/git_authorization_hook.ts +228 -18
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +47 -30
- package/src/scripts/hooks/augment-dispatcher.sh +1 -1
- package/src/scripts/hooks/block_no_verify.ts +3 -3
- package/src/scripts/hooks/category_a.ts +291 -0
- package/src/scripts/hooks/cline-dispatcher.sh +1 -1
- package/src/scripts/hooks/concern_registry.ts +2 -2
- package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
- package/src/scripts/hooks/dispatch_hook.ts +17 -22
- package/src/scripts/hooks/evidence_independence.ts +21 -2
- package/src/scripts/hooks/{block_unauthorized_git.ts → git_command_classifier.ts} +175 -364
- package/src/scripts/hooks/host_lowering.ts +215 -0
- package/src/scripts/hooks/host_lowering.yaml +185 -0
- package/src/scripts/hooks/host_semantics.ts +105 -37
- package/src/scripts/hooks/journal_record_hook.ts +10 -0
- package/src/scripts/hooks/permission_gate.ts +117 -0
- package/src/scripts/hooks/push_settle_hook.ts +250 -0
- package/src/scripts/hooks/ship_diff_volume_hook.ts +1 -1
- package/src/scripts/hooks_status.ts +12 -10
- package/src/scripts/install-hooks.sh +134 -1
- package/src/scripts/install.ts +53 -82
- package/src/scripts/install_anthropic_key.sh +2 -2
- package/src/scripts/language_mirror_hook.ts +27 -5
- package/src/scripts/lint_design_antipattern_parity.ts +80 -0
- package/src/scripts/lint_evidence_artifacts.ts +45 -1
- package/src/scripts/lint_hook_manifest.ts +87 -3
- package/src/scripts/lint_motion_authority_drift.ts +731 -0
- package/src/scripts/lint_readme_size.ts +158 -12
- package/src/scripts/lint_reasoning_fixtures.ts +216 -0
- package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
- package/src/scripts/measure_sibling_naming.ts +150 -0
- package/src/scripts/redact_hook_capture.ts +1 -1
- package/src/scripts/release.ts +31 -143
- package/src/scripts/release_drill.ts +71 -1
- package/src/scripts/release_publication.ts +243 -3
- package/src/scripts/release_verify.ts +220 -0
- package/src/scripts/report_skill_activation.ts +319 -7
- package/src/scripts/schemas/review-findings.schema.json +6 -1
- package/src/scripts/schemas/skill.schema.json +12 -2
- package/src/scripts/self_review_gate.ts +10 -1
- package/src/scripts/session_canary_hook.ts +1 -1
- package/src/scripts/session_register_hook.ts +4 -3
- package/src/scripts/trigger_eval_grandfather.json +5 -1
- package/src/scripts/validate_agent_settings.ts +1 -1
- package/src/shared/settingsConsent.ts +1 -1
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* permission_gate — whether a `pre_tool_use` call gets an explicit
|
|
3
|
+
* `permissionDecision`, and which one.
|
|
4
|
+
*
|
|
5
|
+
* Four independent conditions, each able to withhold an allow on its own: the
|
|
6
|
+
* host offers the field on `pre_tool_use` only; the dispatcher's reduced
|
|
7
|
+
* severity must already be `allow`; no concern may have voted `ask` or `deny`;
|
|
8
|
+
* and the call itself must be category A. An allow is emitted only when all
|
|
9
|
+
* four hold, so widening any one of them is a visible edit rather than an
|
|
10
|
+
* emergent effect.
|
|
11
|
+
*
|
|
12
|
+
* Separate from `dispatch_hook` because that file is against the per-file
|
|
13
|
+
* source-size budget, and because the seam is real: the dispatcher runs
|
|
14
|
+
* concerns and reduces exit codes, this module reads the result and decides
|
|
15
|
+
* what the host is told about permission.
|
|
16
|
+
*/
|
|
17
|
+
import { isCategoryA, type ToolInput } from "./category_a.js";
|
|
18
|
+
import {
|
|
19
|
+
composePermissionDecision,
|
|
20
|
+
type PermissionDecision,
|
|
21
|
+
type PermissionEmission,
|
|
22
|
+
} from "./host_semantics.js";
|
|
23
|
+
|
|
24
|
+
type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
|
|
25
|
+
type JsonObject = { [k: string]: JsonValue };
|
|
26
|
+
|
|
27
|
+
function _isObject(v: unknown): v is JsonObject {
|
|
28
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Mirrors `dispatch_hook.EXIT_ALLOW` / `EXIT_BLOCK`; imported would be a cycle. */
|
|
32
|
+
const EXIT_ALLOW = 0;
|
|
33
|
+
const EXIT_BLOCK = 1;
|
|
34
|
+
|
|
35
|
+
/** The host's `tool_name`, or `""` when the event carries none. */
|
|
36
|
+
export function _payload_tool_name(envelope: JsonObject): string {
|
|
37
|
+
const payload = envelope["payload"];
|
|
38
|
+
if (!_isObject(payload)) return "";
|
|
39
|
+
const raw = payload["tool_name"];
|
|
40
|
+
return typeof raw === "string" ? raw : "";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The host's `tool_input` object, or `{}` when the event carries none. */
|
|
44
|
+
export function _payload_tool_input(envelope: JsonObject): ToolInput {
|
|
45
|
+
const payload = envelope["payload"];
|
|
46
|
+
if (!_isObject(payload)) return {};
|
|
47
|
+
const raw = payload["tool_input"];
|
|
48
|
+
return _isObject(raw) ? (raw as ToolInput) : {};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The root every category-A path argument must resolve inside.
|
|
53
|
+
*
|
|
54
|
+
* The host's own `cwd` wins over `workspace_root` because the latter is the
|
|
55
|
+
* dispatcher's process cwd, which in a worktree is the parent checkout rather
|
|
56
|
+
* than the tree the user is working in — confining against it would admit a
|
|
57
|
+
* path outside the tree the call actually runs in.
|
|
58
|
+
*/
|
|
59
|
+
export function _working_tree_root(envelope: JsonObject): string {
|
|
60
|
+
const payload = envelope["payload"];
|
|
61
|
+
if (_isObject(payload) && typeof payload["cwd"] === "string" && payload["cwd"].trim()) {
|
|
62
|
+
return payload["cwd"];
|
|
63
|
+
}
|
|
64
|
+
const workspace = envelope["workspace_root"];
|
|
65
|
+
return typeof workspace === "string" ? workspace : "";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* One concern's permission verdict.
|
|
70
|
+
*
|
|
71
|
+
* A block is a deny. A concern may also say `{"decision":"ask"}` on stdout —
|
|
72
|
+
* the field has been recorded in the feedback file since v1 and read by
|
|
73
|
+
* nothing, so an advisory concern that wanted a confirmation got silence.
|
|
74
|
+
* `ask` is honoured regardless of the exit code: an advisory concern cannot
|
|
75
|
+
* block and must not have to in order to be heard. Everything else abstains,
|
|
76
|
+
* which composes as `allow`.
|
|
77
|
+
*/
|
|
78
|
+
export function _concern_permission_verdict(
|
|
79
|
+
rc: number,
|
|
80
|
+
reply: JsonObject,
|
|
81
|
+
): PermissionDecision {
|
|
82
|
+
if (rc === EXIT_BLOCK) return "deny";
|
|
83
|
+
const stated = String(reply["decision"] ?? "").trim().toLowerCase();
|
|
84
|
+
if (stated === "ask") return "ask";
|
|
85
|
+
if (stated === "deny") return "deny";
|
|
86
|
+
return "allow";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The permission field to hand the host, or `null` to keep the legacy envelope.
|
|
91
|
+
*
|
|
92
|
+
* Four independent conditions, each able to withhold the allow on its own:
|
|
93
|
+
* the host offers the field on `pre_tool_use` only; the reduced severity must
|
|
94
|
+
* already be `allow`; no concern may have voted `ask` or `deny`; and the call
|
|
95
|
+
* itself must be category A. An allow is emitted only when all four hold, so
|
|
96
|
+
* widening any one of them is a visible edit rather than an emergent effect.
|
|
97
|
+
*/
|
|
98
|
+
export function _permission_for(
|
|
99
|
+
event: string,
|
|
100
|
+
envelope: JsonObject,
|
|
101
|
+
final_rc: number,
|
|
102
|
+
verdicts: readonly PermissionDecision[],
|
|
103
|
+
): PermissionEmission | null {
|
|
104
|
+
if (event !== "pre_tool_use") return null;
|
|
105
|
+
if (final_rc !== EXIT_ALLOW) return null;
|
|
106
|
+
if (composePermissionDecision(verdicts) !== "allow") return null;
|
|
107
|
+
const tool_name = _payload_tool_name(envelope);
|
|
108
|
+
if (!isCategoryA(tool_name, _payload_tool_input(envelope), _working_tree_root(envelope))) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
decision: "allow",
|
|
113
|
+
reason:
|
|
114
|
+
`agent-config: category-A ${tool_name} inside the working tree — ` +
|
|
115
|
+
`no concern gates it and none asked to.`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PostToolUse push-settle reminder — the second half of "a push closes its own
|
|
4
|
+
* loop", and the half no gate could reach before.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS EXISTS — measured, not hypothetical.
|
|
7
|
+
*
|
|
8
|
+
* The pre-push hook answers "is this push safe to make". Nothing answered "is
|
|
9
|
+
* this push finished", and the difference is what reaches the user. Over the 30
|
|
10
|
+
* sessions and 50 PRs preceding 2026-09-04:
|
|
11
|
+
*
|
|
12
|
+
* · 22 of 30 sessions ran `gh run view --log-failed` — i.e. a red CI job was
|
|
13
|
+
* routine, not exceptional.
|
|
14
|
+
* · 17 of the 36 PRs whose workflow runs were still readable carried at least
|
|
15
|
+
* one FAILED run (56 failed runs in total).
|
|
16
|
+
* · 20 of 50 PRs carried a follow-up `fix(ci|gates|budget)…` commit — 41 of
|
|
17
|
+
* them — every one a repair made after a push that had looked done.
|
|
18
|
+
*
|
|
19
|
+
* The push itself always succeeded. What failed was the ending: the agent
|
|
20
|
+
* pushed, reported the work as delivered, and the red arrived afterwards, where
|
|
21
|
+
* the user found it. `verify-before-complete` already forbids that, and it is
|
|
22
|
+
* model-carried at exactly the moment a turn feels finished — which is the
|
|
23
|
+
* moment a behavior rule is least likely to fire.
|
|
24
|
+
*
|
|
25
|
+
* So this hook fires where the fact is deterministic: right after a shell
|
|
26
|
+
* command that ACTUALLY ADVANCED A REMOTE REF. It reads git's own push report
|
|
27
|
+
* rather than the exit code or the command text — a wrapper that pushes
|
|
28
|
+
* (`task push-ready`, a deploy script, a `git` alias) advances a ref without
|
|
29
|
+
* the words `git push` appearing anywhere, and those are the pushes furthest
|
|
30
|
+
* from a human's eye. So:
|
|
31
|
+
*
|
|
32
|
+
* · `Everything up-to-date` → nothing was pushed, nothing to settle, silent.
|
|
33
|
+
* · `! [rejected]` → the push did not happen; the agent already has
|
|
34
|
+
* a visible failure and does not need a second.
|
|
35
|
+
* · `--delete` → no ref advanced; silent.
|
|
36
|
+
* · `--dry-run` → prints the same report as a real push, so this
|
|
37
|
+
* one is caught on the COMMAND, not the output.
|
|
38
|
+
*
|
|
39
|
+
* It resolves the PR number for the pushed branch itself (one `gh` call, best
|
|
40
|
+
* effort) so the reminder carries the literal next command rather than a flag —
|
|
41
|
+
* per `active-remediation` § "The ask carries fixes, not a flag". With no `gh`,
|
|
42
|
+
* no auth, or no open PR, it names the branch and the two-step instead, and
|
|
43
|
+
* never claims a PR number it did not read.
|
|
44
|
+
*
|
|
45
|
+
* Warn only, never blocks (fail_closed: false). A push that is genuinely WIP is
|
|
46
|
+
* a legitimate thing to leave unsettled; what is not legitimate is ENDING THE
|
|
47
|
+
* TURN on one silently.
|
|
48
|
+
*
|
|
49
|
+
* Exit codes (dispatcher contract): 0 allow · 2 warn (+ `{decision,reason,
|
|
50
|
+
* additional_context}` JSON on stdout).
|
|
51
|
+
*/
|
|
52
|
+
import fs from "node:fs";
|
|
53
|
+
import path from "node:path";
|
|
54
|
+
import { execFileSync } from "node:child_process";
|
|
55
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
56
|
+
import { readHookStdin } from "./hook_stdin.js";
|
|
57
|
+
|
|
58
|
+
const EXIT_ALLOW = 0;
|
|
59
|
+
const EXIT_WARN = 2;
|
|
60
|
+
|
|
61
|
+
type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
|
|
62
|
+
type JsonObject = { [k: string]: JsonValue };
|
|
63
|
+
|
|
64
|
+
// Tool names across platforms whose command field carries a shell command
|
|
65
|
+
// (kept in lock-step with pr_url_reminder_hook.COMMAND_TOOLS).
|
|
66
|
+
const COMMAND_TOOLS: ReadonlySet<string> = new Set([
|
|
67
|
+
"launch-process",
|
|
68
|
+
"launch_process",
|
|
69
|
+
"Bash",
|
|
70
|
+
"BashTool",
|
|
71
|
+
"run-process",
|
|
72
|
+
"runProcess",
|
|
73
|
+
"shell",
|
|
74
|
+
"execute_shell",
|
|
75
|
+
"RunShellCommand",
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A push git was told NOT to actually make. `git push --dry-run` prints the SAME
|
|
80
|
+
* ref-advance report as a real push, so the command is where the two are told
|
|
81
|
+
* apart — and it is the ONLY thing the command is consulted for.
|
|
82
|
+
*
|
|
83
|
+
* Deliberately not the other way round. The first version required the command
|
|
84
|
+
* to match `git push`, and its own near-miss test exposed the cost: a wrapper
|
|
85
|
+
* that pushes — `task push-ready`, a deploy script, a `git` alias — advances a
|
|
86
|
+
* remote ref without the words `git push` appearing anywhere, and the hook
|
|
87
|
+
* would have been silent on exactly the pushes furthest from a human's eye.
|
|
88
|
+
* A false negative here is the failure this whole change exists to remove, so
|
|
89
|
+
* the authoritative condition is git's OWN report and the command only subtracts.
|
|
90
|
+
*/
|
|
91
|
+
const DRY_RUN = /(?:^|\s)(?:--dry-run|-n)(?:\s|$)/;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Git's own report that a ref MOVED. Both shapes it prints:
|
|
95
|
+
* ` b75d7f7..a1b2c3d feat/x -> feat/x` (fast-forward / forced)
|
|
96
|
+
* ` * [new branch] feat/x -> feat/x` (first push)
|
|
97
|
+
* The `->` is the load-bearing token: `Everything up-to-date` has none, and a
|
|
98
|
+
* `! [rejected]` line carries one but is excluded before this runs.
|
|
99
|
+
*/
|
|
100
|
+
const REF_ADVANCED = /(?:\[new branch\]|\[new tag\]|[0-9a-f]{7,}\.\.[0-9a-f]{7,})\s+(\S+)\s+->\s+(\S+)/;
|
|
101
|
+
|
|
102
|
+
/** A push git refused. Its `->` must never be read as an advance. */
|
|
103
|
+
const REJECTED = /^\s*!\s*\[(?:rejected|remote rejected)\]/m;
|
|
104
|
+
|
|
105
|
+
function isObject(v: unknown): v is JsonObject {
|
|
106
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Unwrap the dispatcher envelope `{event, platform, payload: <native>}`.
|
|
111
|
+
* Reading `tool_name` off the top level is the defect that left
|
|
112
|
+
* `pr-url-reminder` silently never firing in production (measured 2026-08-06).
|
|
113
|
+
*/
|
|
114
|
+
function unwrap(envelope: JsonObject): JsonObject {
|
|
115
|
+
const inner = envelope["payload"];
|
|
116
|
+
return isObject(inner) ? inner : envelope;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function extractCommand(payload: JsonObject): string | null {
|
|
120
|
+
const toolRaw = payload["tool_name"] ?? payload["toolName"] ?? payload["tool"];
|
|
121
|
+
const tool = typeof toolRaw === "string" ? toolRaw : null;
|
|
122
|
+
if (tool === null || !COMMAND_TOOLS.has(tool)) return null;
|
|
123
|
+
const ti = payload["tool_input"];
|
|
124
|
+
if (isObject(ti)) {
|
|
125
|
+
for (const key of ["command", "cmd", "shell_command"]) {
|
|
126
|
+
const v = ti[key];
|
|
127
|
+
if (typeof v === "string" && v) return v;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
for (const key of ["command", "cmd"]) {
|
|
131
|
+
const v = payload[key];
|
|
132
|
+
if (typeof v === "string" && v) return v;
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function toolOutput(payload: JsonObject): string {
|
|
138
|
+
for (const key of ["tool_response", "tool_result", "toolResponse", "output", "result", "stdout"]) {
|
|
139
|
+
const v = payload[key];
|
|
140
|
+
if (typeof v === "string") return v;
|
|
141
|
+
if (isObject(v) || Array.isArray(v)) return JSON.stringify(v);
|
|
142
|
+
}
|
|
143
|
+
return JSON.stringify(payload);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The pushed branch, taken from the REMOTE side of git's `local -> remote`
|
|
148
|
+
* report and stripped of any `refs/heads/` prefix. The remote side is the one
|
|
149
|
+
* that names the branch CI will run against; on a `HEAD:feat/x` push the local
|
|
150
|
+
* side says `HEAD` and would resolve to nothing.
|
|
151
|
+
*/
|
|
152
|
+
export function pushedBranch(output: string): string | null {
|
|
153
|
+
if (REJECTED.test(output)) return null;
|
|
154
|
+
const m = REF_ADVANCED.exec(output);
|
|
155
|
+
if (m === null) return null;
|
|
156
|
+
const remote = m[2];
|
|
157
|
+
if (remote === undefined) return null;
|
|
158
|
+
return remote.replace(/^refs\/heads\//, "") || null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Best-effort PR number for a branch. Never throws, never guesses. */
|
|
162
|
+
export function resolvePr(
|
|
163
|
+
branch: string,
|
|
164
|
+
run: (cmd: string, args: string[]) => string = (cmd, args) =>
|
|
165
|
+
execFileSync(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 15_000 }),
|
|
166
|
+
): number | null {
|
|
167
|
+
try {
|
|
168
|
+
const out = run("gh", ["pr", "view", branch, "--json", "number", "-q", ".number"]).trim();
|
|
169
|
+
const n = Number.parseInt(out, 10);
|
|
170
|
+
return Number.isInteger(n) && n > 0 ? n : null;
|
|
171
|
+
} catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** The reminder text. Split out so the test can read it without a `gh` on PATH. */
|
|
177
|
+
export function buildReminder(branch: string, pr: number | null): { reason: string; additional_context: string } {
|
|
178
|
+
const settle =
|
|
179
|
+
pr === null
|
|
180
|
+
? `gh pr view ${branch} --json number -q .number # then:\n` +
|
|
181
|
+
`./scripts-run src/scripts/ci_settle <that number>`
|
|
182
|
+
: `./scripts-run src/scripts/ci_settle ${pr}`;
|
|
183
|
+
const subject = pr === null ? `branch ${branch}` : `PR #${pr} (${branch})`;
|
|
184
|
+
const reason =
|
|
185
|
+
`Pushed ${subject} — the turn is not finished until its CI has settled green ` +
|
|
186
|
+
`(verify-before-complete). Run ci_settle before you report this as done.`;
|
|
187
|
+
const additional_context =
|
|
188
|
+
`You just advanced a remote ref: ${subject}.\n\n` +
|
|
189
|
+
`A push is not a delivery. Per verify-before-complete, a completion claim needs ` +
|
|
190
|
+
`fresh evidence, and the evidence for a push is the CI verdict — not the push's ` +
|
|
191
|
+
`own exit code.\n\n` +
|
|
192
|
+
`Before this turn ends:\n` +
|
|
193
|
+
` 1. Settle it:\n ${settle}\n` +
|
|
194
|
+
` Exit 0 = settled green · 1 = settled, something failed · 2 = NOT a verdict ` +
|
|
195
|
+
`(timed out or unreadable — never report 2 as green).\n` +
|
|
196
|
+
` 2. Red? Read only the failing part:\n` +
|
|
197
|
+
` gh run view --job <id> --log-failed | grep -E '×|FAIL|Error'\n` +
|
|
198
|
+
` then fix it and push again (fix-what-you-see: the author is irrelevant).\n` +
|
|
199
|
+
` 3. Still red after 3 attempts on the same target, or blocked on something ` +
|
|
200
|
+
`only the user can decide — stop and say so, with the three attempts named ` +
|
|
201
|
+
`(autonomous-execution N=3).\n\n` +
|
|
202
|
+
`Handing the user a red PR with its cause named and unfixed is not a delivery ` +
|
|
203
|
+
`either. If you are deliberately leaving this WIP, say that in the reply — ` +
|
|
204
|
+
`silence is what this hook exists to stop.`;
|
|
205
|
+
return { reason, additional_context };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function main(): number {
|
|
209
|
+
let envelope: JsonValue;
|
|
210
|
+
try {
|
|
211
|
+
const raw = readHookStdin();
|
|
212
|
+
envelope = raw.trim() ? (JSON.parse(raw) as JsonValue) : {};
|
|
213
|
+
} catch {
|
|
214
|
+
return EXIT_ALLOW; // never block on a malformed envelope
|
|
215
|
+
}
|
|
216
|
+
if (!isObject(envelope)) return EXIT_ALLOW;
|
|
217
|
+
|
|
218
|
+
const inner = unwrap(envelope);
|
|
219
|
+
// A shell tool is still required: the report below is git's own text, and a
|
|
220
|
+
// structured forge tool does not produce it.
|
|
221
|
+
const command = extractCommand(inner);
|
|
222
|
+
if (command === null) return EXIT_ALLOW;
|
|
223
|
+
if (DRY_RUN.test(command)) return EXIT_ALLOW;
|
|
224
|
+
|
|
225
|
+
const branch = pushedBranch(toolOutput(inner));
|
|
226
|
+
if (branch === null) return EXIT_ALLOW; // up-to-date, rejected, delete — nothing advanced
|
|
227
|
+
|
|
228
|
+
const { reason, additional_context } = buildReminder(branch, resolvePr(branch));
|
|
229
|
+
process.stdout.write(`${JSON.stringify({ decision: "warn", reason, additional_context })}\n`);
|
|
230
|
+
return EXIT_WARN;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Bundle-safety: never auto-run when inlined into an esbuild bundle, where
|
|
234
|
+
// every module shares the bundle's `import.meta.url` (see cmd_migrate.ts).
|
|
235
|
+
declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
|
|
236
|
+
function isCliEntry(): boolean {
|
|
237
|
+
if (typeof __AGENT_CONFIG_BUNDLE__ !== "undefined" && __AGENT_CONFIG_BUNDLE__) return false;
|
|
238
|
+
if (process.argv[1] === undefined) return false;
|
|
239
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
240
|
+
if (import.meta.url === argvUrl) return true;
|
|
241
|
+
try {
|
|
242
|
+
return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
243
|
+
} catch {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (isCliEntry()) {
|
|
249
|
+
process.exit(main());
|
|
250
|
+
}
|
|
@@ -91,7 +91,7 @@ export function correctedVolume(numstat: string): { volume: number; excluded: nu
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
/**
|
|
94
|
-
* Tools that carry a shell command. Mirrors `
|
|
94
|
+
* Tools that carry a shell command. Mirrors `git_command_classifier`'s set.
|
|
95
95
|
*
|
|
96
96
|
* `SHIP_PATTERNS` are unanchored, so without this gate a `grep` for the string
|
|
97
97
|
* `git push` spawns two `git` subprocesses on a blocking slot. ALLOW-list: a
|
|
@@ -45,7 +45,9 @@ const MANIFEST_PATH = path.join(
|
|
|
45
45
|
"hook_manifest.yaml",
|
|
46
46
|
);
|
|
47
47
|
|
|
48
|
-
// (
|
|
48
|
+
// (project-relative bridge path, one-line note shown when the bridge is not
|
|
49
|
+
// live). The note is printed verbatim, so a note that is not a command must
|
|
50
|
+
// not read as one.
|
|
49
51
|
// Path may be a directory (cline) — existence => any file inside.
|
|
50
52
|
//
|
|
51
53
|
// Cowork has no project-scope bridge path: the Claude desktop app's
|
|
@@ -58,17 +60,17 @@ const MANIFEST_PATH = path.join(
|
|
|
58
60
|
// Once upstream lands the fix and a stable settings location is
|
|
59
61
|
// documented, swap the empty path here for that location.
|
|
60
62
|
export const PLATFORM_BRIDGES: Record<string, [string, string]> = {
|
|
61
|
-
augment: [".augment/settings.json", "
|
|
62
|
-
claude: [".claude/settings.json", "
|
|
63
|
+
augment: [".augment/settings.json", "run `agent-config install`"],
|
|
64
|
+
claude: [".claude/settings.json", "run `agent-config install`"],
|
|
63
65
|
cowork: [
|
|
64
66
|
"",
|
|
65
67
|
"upstream-blocked: anthropics/claude-code#40495 + #27398 (settings.json ignored in Cowork sandbox)",
|
|
66
68
|
],
|
|
67
|
-
cursor: [".cursor/hooks.json", "
|
|
68
|
-
cline: [".clinerules/hooks", "
|
|
69
|
-
windsurf: [".windsurf/hooks.json", "
|
|
70
|
-
gemini: [".gemini/settings.json", "
|
|
71
|
-
copilot: ["", "rule-only fallback
|
|
69
|
+
cursor: [".cursor/hooks.json", "run `agent-config install`"],
|
|
70
|
+
cline: [".clinerules/hooks", "run `agent-config install`"],
|
|
71
|
+
windsurf: [".windsurf/hooks.json", "run `agent-config install`"],
|
|
72
|
+
gemini: [".gemini/settings.json", "run `agent-config install`"],
|
|
73
|
+
copilot: ["", "rule-only fallback — this package binds no hook here"],
|
|
72
74
|
};
|
|
73
75
|
|
|
74
76
|
export interface PlatformRow {
|
|
@@ -180,11 +182,11 @@ export function _render_table(matrix: StatusMatrix): string {
|
|
|
180
182
|
lines.push(` ${event.padEnd(22)} → ${concerns}`);
|
|
181
183
|
}
|
|
182
184
|
if (row.hint) {
|
|
183
|
-
lines.push(` hint:
|
|
185
|
+
lines.push(` hint: ${row.hint}`);
|
|
184
186
|
}
|
|
185
187
|
}
|
|
186
188
|
lines.push("");
|
|
187
|
-
lines.push("Source of truth: scripts/hook_manifest.yaml");
|
|
189
|
+
lines.push("Source of truth: src/scripts/hook_manifest.yaml");
|
|
188
190
|
lines.push("Contract: docs/contracts/hook-architecture-v1.md");
|
|
189
191
|
return lines.join("\n");
|
|
190
192
|
}
|
|
@@ -12,7 +12,25 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
|
12
12
|
# (where $PROJECT_ROOT/.git is a file, not a dir, and a literal .git/hooks path
|
|
13
13
|
# would break under `set -e`). --git-common-dir points at the main .git even from
|
|
14
14
|
# a worktree; fall back to the literal path only outside a git repo.
|
|
15
|
-
|
|
15
|
+
#
|
|
16
|
+
# AGENT_CONFIG_HOOKS_DIR overrides the destination. It exists so that
|
|
17
|
+
# `check_installed_hooks_fresh` can render what this installer WOULD write into
|
|
18
|
+
# a scratch directory and byte-compare it against what is installed — the only
|
|
19
|
+
# way to answer "is the installed copy stale?" without re-parsing these
|
|
20
|
+
# heredocs, which carry an appended block (post-merge / post-checkout) that no
|
|
21
|
+
# heredoc slice can reproduce. It is a test/inspection seam; a normal install
|
|
22
|
+
# never sets it.
|
|
23
|
+
if [ -n "${AGENT_CONFIG_HOOKS_DIR+set}" ]; then
|
|
24
|
+
# SET-BUT-EMPTY is refused rather than ignored. A caller that meant to
|
|
25
|
+
# redirect an inspection run and passed an empty value would otherwise fall
|
|
26
|
+
# through to the real .git/hooks and INSTALL over it — including over a
|
|
27
|
+
# pre-push that is running the inspection, which truncates it silently.
|
|
28
|
+
if [ -z "$AGENT_CONFIG_HOOKS_DIR" ]; then
|
|
29
|
+
echo "❌ AGENT_CONFIG_HOOKS_DIR is set but empty — refusing to fall back to the real .git/hooks." >&2
|
|
30
|
+
exit 2
|
|
31
|
+
fi
|
|
32
|
+
HOOKS_DIR="$AGENT_CONFIG_HOOKS_DIR"
|
|
33
|
+
elif COMMON_GIT_DIR="$(git -C "$PROJECT_ROOT" rev-parse --git-common-dir 2>/dev/null)"; then
|
|
16
34
|
case "$COMMON_GIT_DIR" in /*) : ;; *) COMMON_GIT_DIR="$PROJECT_ROOT/$COMMON_GIT_DIR" ;; esac
|
|
17
35
|
HOOKS_DIR="$COMMON_GIT_DIR/hooks"
|
|
18
36
|
else
|
|
@@ -20,6 +38,12 @@ else
|
|
|
20
38
|
fi
|
|
21
39
|
|
|
22
40
|
mkdir -p "$HOOKS_DIR"
|
|
41
|
+
# Name the destination. AGENT_CONFIG_HOOKS_DIR is a global override, and a
|
|
42
|
+
# stray non-empty value would otherwise redirect a real `task install-hooks`
|
|
43
|
+
# while it printed six "hook installed" lines that name no path at all — the
|
|
44
|
+
# freshness check would then report every hook missing with nothing pointing at
|
|
45
|
+
# the cause.
|
|
46
|
+
echo "→ installing git hooks into $HOOKS_DIR" >&2
|
|
23
47
|
|
|
24
48
|
cat > "$HOOKS_DIR/pre-push" << 'EOF'
|
|
25
49
|
#!/usr/bin/env bash
|
|
@@ -85,6 +109,96 @@ fi
|
|
|
85
109
|
|
|
86
110
|
fail=0
|
|
87
111
|
|
|
112
|
+
# BASE FRESHNESS — first, and it EXITS rather than setting fail=1.
|
|
113
|
+
#
|
|
114
|
+
# The header above has promised "when this hook refuses for staleness" since the
|
|
115
|
+
# hook was written, and nothing below ever asked. Measured over the 50 PRs
|
|
116
|
+
# preceding 2026-09-04: 25 of them carried at least one `Merge branch 'main'
|
|
117
|
+
# into <branch>` commit (52 such commits in total), and 17 of the 36 PRs whose
|
|
118
|
+
# workflow runs were still readable had at least one FAILED run — the three
|
|
119
|
+
# workflows that failed most (Tests 18, Consistency 16, Rule Backstops 13) are
|
|
120
|
+
# exactly the base-relative ones: ratchets, budgets and generated counts that
|
|
121
|
+
# were green against the base the branch forked from and red against the base it
|
|
122
|
+
# merges into. A branch pushed behind its base buys a CONFLICTING PR, a red run,
|
|
123
|
+
# or both, and the contributor pays for it after the work looks finished.
|
|
124
|
+
#
|
|
125
|
+
# It runs FIRST because everything after it is invalidated by the merge that has
|
|
126
|
+
# to happen anyway: `task consistency` regenerates derived files against the
|
|
127
|
+
# stale base, the merge then re-dirties them, and the ~10s is spent twice. And
|
|
128
|
+
# it EXITS instead of accumulating into `fail`, for the same reason — there is
|
|
129
|
+
# no useful second finding to collect from a tree that is about to move.
|
|
130
|
+
#
|
|
131
|
+
# It never merges. `check_branch_freshness` asks the REMOTE (one `ls-remote`,
|
|
132
|
+
# measured 4.5s) and returns 1 only on a VERIFIED behind state; offline, in CI,
|
|
133
|
+
# on a detached HEAD, or standing on the base itself it returns 0 and says so.
|
|
134
|
+
# So an unreachable network cannot block a push, and a stale local tracking ref
|
|
135
|
+
# cannot fake a green.
|
|
136
|
+
echo "🔍 Base freshness — is this branch behind the base it will merge into?"
|
|
137
|
+
if [ "${AGENT_CONFIG_SKIP_PREPUSH_FRESHNESS:-}" = "1" ]; then
|
|
138
|
+
echo "⏭️ skipped via AGENT_CONFIG_SKIP_PREPUSH_FRESHNESS=1"
|
|
139
|
+
elif [ ! -x ./scripts-run ]; then
|
|
140
|
+
echo "⚠️ ./scripts-run not found — skipping the freshness check for this push."
|
|
141
|
+
elif ! ./scripts-run src/scripts/check_branch_freshness --quiet; then
|
|
142
|
+
echo ""
|
|
143
|
+
echo " Push blocked — the branch is behind its base, and the gates you just"
|
|
144
|
+
echo " passed were answered against a base that no longer exists."
|
|
145
|
+
echo ""
|
|
146
|
+
echo " task push-ready # fetch → integrate the base SET → regenerate"
|
|
147
|
+
echo " # → verify → re-check freshness, then push"
|
|
148
|
+
echo " task push-ready DRY=1 # the same steps, read-only"
|
|
149
|
+
echo ""
|
|
150
|
+
echo " This hook refuses; it never merges. Bypass a genuine WIP push with"
|
|
151
|
+
echo " AGENT_CONFIG_SKIP_PREPUSH_FRESHNESS=1."
|
|
152
|
+
exit 1
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
# INSTALLED-HOOK FRESHNESS — ADVISORY, and deliberately AFTER base freshness.
|
|
156
|
+
#
|
|
157
|
+
# What it answers: are the hooks in .git/hooks still what THIS checkout's
|
|
158
|
+
# install-hooks.sh would write? Measured 2026-09-05: the installed pre-push here
|
|
159
|
+
# was 146 lines against a 189-line source body, missing the entire base-freshness
|
|
160
|
+
# gate above, merged five days earlier. It read as live in the source, in CI and
|
|
161
|
+
# in the skill documenting it, and did not run here.
|
|
162
|
+
#
|
|
163
|
+
# WHY IT DOES NOT REFUSE. The predicate is "installed == what this checkout
|
|
164
|
+
# renders", and .git/hooks is SHARED by every linked worktree through the common
|
|
165
|
+
# dir. With two worktrees whose installer bodies differ, at most one can satisfy
|
|
166
|
+
# it at a time and each re-install flips which — so a refusal would fire on
|
|
167
|
+
# ordinary parallel work, and the documented skip variable would become routine,
|
|
168
|
+
# which retires the gate rather than enforcing it. An AI council (claude-sonnet-4-5
|
|
169
|
+
# + codex-default, 2026-09-05, 2 of 2 seats) chose advisory unanimously after an
|
|
170
|
+
# independent review raised exactly that. Blocking returns only with a
|
|
171
|
+
# single-referent predicate — per-worktree hook isolation via core.hooksPath, or a
|
|
172
|
+
# branch-independent dispatcher installed once in the common dir.
|
|
173
|
+
#
|
|
174
|
+
# WHY AFTER BASE FRESHNESS. Being behind a base that moved the installer is the
|
|
175
|
+
# most common cause of a mismatch, and the repair for THAT is to merge, not to
|
|
176
|
+
# re-install: re-installing from a behind-base checkout writes the OLDER hook set
|
|
177
|
+
# over the shared directory. Base freshness exits first, so that contributor is
|
|
178
|
+
# told the correct thing and never reaches this notice.
|
|
179
|
+
echo "🔍 Installed-hook freshness — do .git/hooks still match this checkout?"
|
|
180
|
+
if [ "${AGENT_CONFIG_SKIP_PREPUSH_HOOKFRESH:-}" = "1" ]; then
|
|
181
|
+
echo "⏭️ skipped via AGENT_CONFIG_SKIP_PREPUSH_HOOKFRESH=1"
|
|
182
|
+
elif [ ! -x ./scripts-run ] || [ ! -d node_modules ] \
|
|
183
|
+
|| [ ! -f src/scripts/check_installed_hooks_fresh.ts ]; then
|
|
184
|
+
# Three guards, and each has been a real miss. `-d node_modules`: without
|
|
185
|
+
# dependencies ./scripts-run cannot start tsx and the non-zero exit would
|
|
186
|
+
# read as staleness. `-f`: .git/hooks is shared, so this file also runs in a
|
|
187
|
+
# worktree standing on a branch that predates the gate, and calling a script
|
|
188
|
+
# that is not on that checkout would report the wrong thing.
|
|
189
|
+
echo "⏭️ check_installed_hooks_fresh not runnable on this checkout — skipping."
|
|
190
|
+
else
|
|
191
|
+
./scripts-run src/scripts/check_installed_hooks_fresh --quiet
|
|
192
|
+
hookfresh_rc=$?
|
|
193
|
+
# The gate's three exit codes mean three different things, and collapsing
|
|
194
|
+
# them reports a false cause: 0 = match or nothing installed, 1 = mismatch,
|
|
195
|
+
# 2 = it could not render (installer failed, unwritable TMPDIR, usage error).
|
|
196
|
+
if [ "$hookfresh_rc" = "2" ]; then
|
|
197
|
+
echo "⚠️ could not compare the installed hooks (see above) — not a staleness verdict."
|
|
198
|
+
fi
|
|
199
|
+
# Advisory in both cases: nothing here sets fail=1 or exits.
|
|
200
|
+
fi
|
|
201
|
+
|
|
88
202
|
if ! command -v task >/dev/null 2>&1; then
|
|
89
203
|
echo "⚠️ 'task' (go-task) not found — cannot run the consistency gate locally."
|
|
90
204
|
echo " Install it (https://taskfile.dev) so pre-push can mirror the CI check;"
|
|
@@ -381,6 +495,25 @@ if [ -n "\$prev" ] && [ -n "\$new" ] && [ "\$prev" != "\$new" ]; then
|
|
|
381
495
|
if command -v npm >/dev/null 2>&1 && [ -d node_modules ]; then
|
|
382
496
|
npm run build:cli >/dev/null 2>&1 || true
|
|
383
497
|
fi
|
|
498
|
+
# Installed-hook staleness — REPORT it here, never repair it here.
|
|
499
|
+
#
|
|
500
|
+
# This is the event that CAUSES the staleness, so it is where the news
|
|
501
|
+
# belongs. It is NOT where the repair belongs: linked worktrees SHARE
|
|
502
|
+
# .git/hooks through the common dir, so re-running the installer from a
|
|
503
|
+
# checkout would let one worktree silently redefine the gates every
|
|
504
|
+
# other worktree runs, and an older branch would reinstall older hooks
|
|
505
|
+
# over newer ones. An AI council (claude-sonnet-4-5 + codex-default,
|
|
506
|
+
# 2026-09-05, 2 of 2 seats) refused that mutation unanimously.
|
|
507
|
+
#
|
|
508
|
+
# Output goes to stderr and is NOT swallowed: the rest of this block is
|
|
509
|
+
# "|| true" so a pull is never blocked, and a repair notice nobody can
|
|
510
|
+
# see is the same silence this check exists to remove.
|
|
511
|
+
# The -f guard is not belt-and-braces: this hook is shared across
|
|
512
|
+
# linked worktrees, so it also runs on checkouts that predate the gate.
|
|
513
|
+
if [ -x ./scripts-run ] && [ -d node_modules ] \
|
|
514
|
+
&& [ -f src/scripts/check_installed_hooks_fresh.ts ]; then
|
|
515
|
+
./scripts-run src/scripts/check_installed_hooks_fresh --quiet >&2 || true
|
|
516
|
+
fi
|
|
384
517
|
fi
|
|
385
518
|
fi
|
|
386
519
|
EOF
|