@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,381 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Is the INSTALLED copy of this repository's git hooks still what the installer
|
|
4
|
+
* would write?
|
|
5
|
+
*
|
|
6
|
+
* WHY. `src/scripts/install-hooks.sh` writes six hooks into the shared
|
|
7
|
+
* `.git/hooks` directory. Afterwards it runs only on the package manager's
|
|
8
|
+
* `prepare` lifecycle step (git clones only) or when a human runs
|
|
9
|
+
* `task install-hooks`. Between those two events the installed copy drifts from
|
|
10
|
+
* the source that writes it, silently and without bound. Measured in this
|
|
11
|
+
* repository on 2026-09-05, before this gate existed: the installed `pre-push`
|
|
12
|
+
* was 146 lines against a 189-line source body, and the 43 missing lines were
|
|
13
|
+
* the entire base-freshness gate merged five days earlier. In the source, in
|
|
14
|
+
* CI, and in the skill documenting it, that gate read as live. On the checkout
|
|
15
|
+
* that actually runs it, it did not exist.
|
|
16
|
+
*
|
|
17
|
+
* That is the general shape of an INSTALLED copy of a generated file: the
|
|
18
|
+
* source is gated, the copy is not, and the copy is what runs.
|
|
19
|
+
*
|
|
20
|
+
* HOW. Not by re-parsing the heredocs. `install-hooks.sh` writes `post-merge`
|
|
21
|
+
* and `post-checkout` as a heredoc PLUS an appended auto-sync block, and it
|
|
22
|
+
* interpolates the hook name into both — so no slice of the source file equals
|
|
23
|
+
* a hook body, and a comparison built on slicing would be wrong for two of six
|
|
24
|
+
* hooks and brittle for the rest. Instead the gate runs the real installer with
|
|
25
|
+
* `AGENT_CONFIG_HOOKS_DIR` pointed at a scratch directory and byte-compares
|
|
26
|
+
* what it produced against what is installed. Anything the installer does
|
|
27
|
+
* deterministically — interpolation, appends, future additions — compares equal
|
|
28
|
+
* by construction, which is what keeps this from being the too-strict check its
|
|
29
|
+
* roadmap's risk register warned about.
|
|
30
|
+
*
|
|
31
|
+
* WHERE IT IS BOUND. Two carriers, and NEITHER REFUSES. The pre-push hook body
|
|
32
|
+
* calls it after the base-freshness gate, advisory (silence it with
|
|
33
|
+
* `AGENT_CONFIG_SKIP_PREPUSH_HOOKFRESH=1`); the `post-merge` / `post-checkout`
|
|
34
|
+
* auto-sync block calls it at the event that CAUSES the staleness. The
|
|
35
|
+
* predicate is "installed == what THIS checkout renders", and `.git/hooks` is
|
|
36
|
+
* shared by every linked worktree, so it has no unique referent and a refusal
|
|
37
|
+
* would fire on ordinary parallel work — council 2026-09-05, 2 of 2 seats.
|
|
38
|
+
*
|
|
39
|
+
* WHAT THE EVENT CARRIER DOES NOT REACH. `git pull --rebase` fires neither
|
|
40
|
+
* `post-merge` nor `post-checkout` — it fires `post-rewrite`, which carries no
|
|
41
|
+
* detector. A rebase-pull that moves the installer is therefore reported at the
|
|
42
|
+
* next push and not before. Named rather than closed: `post-rewrite` also fires
|
|
43
|
+
* on every `git commit --amend`, so wiring it there trades a real gap for a
|
|
44
|
+
* notice on an operation that cannot have moved the installer.
|
|
45
|
+
*
|
|
46
|
+
* WHY NOT IN CI. Deliberately NOT in `task ci` or `task preflight`: CI cannot
|
|
47
|
+
* observe a contributor's `.git/hooks`, so a CI-registered run would scan
|
|
48
|
+
* nothing and report a green that means "no data" — the vacuous-pass class this
|
|
49
|
+
* repository audited in 2026-07. Its correctness is covered by
|
|
50
|
+
* `tests/scripts/check_installed_hooks_fresh.test.ts`, which does run in CI,
|
|
51
|
+
* and which asserts BOTH directions.
|
|
52
|
+
*
|
|
53
|
+
* EMPTY IS A REAL STATE. A checkout that never ran the installer has no managed
|
|
54
|
+
* hook to compare. That exits 0 and says so in one line, rather than pretending
|
|
55
|
+
* to have checked something.
|
|
56
|
+
*
|
|
57
|
+
* CLI contract: exit 0 = every installed managed hook matches, or none is
|
|
58
|
+
* installed; 1 = at least one is missing or differs; 2 = the installer could
|
|
59
|
+
* not be rendered (usage error, or the installer itself failed).
|
|
60
|
+
*
|
|
61
|
+
* Usage:
|
|
62
|
+
* ./scripts-run src/scripts/check_installed_hooks_fresh
|
|
63
|
+
* ./scripts-run src/scripts/check_installed_hooks_fresh --quiet
|
|
64
|
+
* ./scripts-run src/scripts/check_installed_hooks_fresh --hooks-dir <path>
|
|
65
|
+
*/
|
|
66
|
+
// ledger-exempt: this gate has one target — the installed hook set — and it
|
|
67
|
+
// reports every managed hook by name with a fingerprint on both the clean and
|
|
68
|
+
// the failing path, so per-target accounting is already the whole output.
|
|
69
|
+
|
|
70
|
+
import { spawnSync } from 'node:child_process';
|
|
71
|
+
import * as crypto from 'node:crypto';
|
|
72
|
+
import * as fs from 'node:fs';
|
|
73
|
+
import * as os from 'node:os';
|
|
74
|
+
import * as path from 'node:path';
|
|
75
|
+
import process from 'node:process';
|
|
76
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
77
|
+
|
|
78
|
+
import { DeadScopeError, reportScanned } from './_lib/scan_scope.js';
|
|
79
|
+
|
|
80
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
81
|
+
const REPO = path.resolve(path.dirname(_HERE), '..', '..');
|
|
82
|
+
const INSTALLER_REL = 'src/scripts/install-hooks.sh';
|
|
83
|
+
const FIX = 'task install-hooks';
|
|
84
|
+
const SKIP_ENV = 'AGENT_CONFIG_SKIP_PREPUSH_HOOKFRESH';
|
|
85
|
+
// Both child processes run on the push path. Unbounded, a wedged `git` or an
|
|
86
|
+
// unwritable TMPDIR leaves the push hanging with no diagnostic, and the
|
|
87
|
+
// "could not render" branch is only reached if the child returns at all.
|
|
88
|
+
const GIT_TIMEOUT_MS = 5_000;
|
|
89
|
+
const INSTALLER_TIMEOUT_MS = 30_000;
|
|
90
|
+
|
|
91
|
+
export interface HookVerdict {
|
|
92
|
+
name: string;
|
|
93
|
+
/**
|
|
94
|
+
* `not-executable` is its own state rather than a flavour of `differs`:
|
|
95
|
+
* git SKIPS a hook without the executable bit, so a byte-identical file
|
|
96
|
+
* that lost +x is exactly the installed-but-inert case this gate exists
|
|
97
|
+
* to find, and it needs a different sentence than "the content drifted".
|
|
98
|
+
* `unreadable` is separated from `missing` for the same reason — EACCES
|
|
99
|
+
* and ENOENT need different fixes (#7).
|
|
100
|
+
*/
|
|
101
|
+
state: 'match' | 'differs' | 'missing' | 'not-executable' | 'unreadable';
|
|
102
|
+
expectedSha: string;
|
|
103
|
+
installedSha: string | null;
|
|
104
|
+
/** Error code when `state` is `unreadable`. */
|
|
105
|
+
readError?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface FreshnessReport {
|
|
109
|
+
hooksDir: string;
|
|
110
|
+
/** Managed hooks the installer would write. */
|
|
111
|
+
verdicts: HookVerdict[];
|
|
112
|
+
/** True when NONE of the managed hooks is installed — a fresh clone. */
|
|
113
|
+
neverInstalled: boolean;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function sha(buf: Buffer): string {
|
|
117
|
+
return crypto.createHash('sha256').update(buf).digest('hex').slice(0, 12);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The hooks directory git will actually execute from.
|
|
122
|
+
*
|
|
123
|
+
* `--git-path hooks` rather than `--git-common-dir` + "/hooks": the two agree
|
|
124
|
+
* on the ordinary case AND on a linked worktree (verified here — from a
|
|
125
|
+
* worktree both resolve to the SHARED common-dir hooks, which is the property
|
|
126
|
+
* the whole shared-state argument rests on), and they diverge when
|
|
127
|
+
* `core.hooksPath` is set, where only `--git-path` follows it. Composing the
|
|
128
|
+
* path by hand would compare a directory git never runs and report a match over
|
|
129
|
+
* hooks that are unrelated — the vacuous green this gate exists to refuse.
|
|
130
|
+
*
|
|
131
|
+
* Honest limit: the `core.hooksPath` branch is git-documented and NOT exercised
|
|
132
|
+
* here. This repository's own `block-no-verify` guard refuses any command
|
|
133
|
+
* carrying `core.hooksPath`, read-only probes included, so it cannot be tested
|
|
134
|
+
* from inside a session. The worktree and default cases are verified.
|
|
135
|
+
*/
|
|
136
|
+
export function resolveHooksDir(repoRoot: string): string {
|
|
137
|
+
const res = spawnSync('git', ['-C', repoRoot, 'rev-parse', '--git-path', 'hooks'], {
|
|
138
|
+
encoding: 'utf-8',
|
|
139
|
+
timeout: GIT_TIMEOUT_MS,
|
|
140
|
+
});
|
|
141
|
+
const out = (res.stdout ?? '').trim();
|
|
142
|
+
if (res.status !== 0 || out === '') return path.join(repoRoot, '.git', 'hooks');
|
|
143
|
+
return path.isAbsolute(out) ? out : path.join(repoRoot, out);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Render every hook the installer would write, into a scratch directory.
|
|
148
|
+
*
|
|
149
|
+
* Shells out to the real installer rather than reimplementing it: an
|
|
150
|
+
* independent renderer would be a second source of truth, and the drift between
|
|
151
|
+
* the two would be invisible in exactly the way this gate exists to catch.
|
|
152
|
+
*/
|
|
153
|
+
export function renderExpected(repoRoot: string): Map<string, Buffer> {
|
|
154
|
+
const stage = fs.mkdtempSync(path.join(os.tmpdir(), 'hookfresh-'));
|
|
155
|
+
// The seam FAILS OPEN by construction: install-hooks.sh treats an empty or
|
|
156
|
+
// absent AGENT_CONFIG_HOOKS_DIR as "install normally", so a redirect that
|
|
157
|
+
// does not take effect turns this read-only inspection into a real install
|
|
158
|
+
// over the pre-push that is currently executing it.
|
|
159
|
+
//
|
|
160
|
+
// THIS CHECK IS THE WEAKER HALF, and saying so matters on a path whose
|
|
161
|
+
// stated risk is that overwrite. It inspects `stage` — a value mkdtempSync
|
|
162
|
+
// produced two lines up — and therefore catches a caller that supplies an
|
|
163
|
+
// unusable path. It CANNOT observe whether the variable reached the child.
|
|
164
|
+
// The defence that can is the installer's own refusal of a set-but-empty
|
|
165
|
+
// value (install-hooks.sh), which is where the fall-through happens.
|
|
166
|
+
if (!path.isAbsolute(stage) || stage.trim() === '') {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`refusing to run the installer: scratch dir ${JSON.stringify(stage)} is not an absolute path, ` +
|
|
169
|
+
'and an unredirected run would overwrite the real .git/hooks.',
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
const res = spawnSync('bash', [path.join(repoRoot, INSTALLER_REL)], {
|
|
174
|
+
cwd: repoRoot,
|
|
175
|
+
encoding: 'buffer',
|
|
176
|
+
timeout: INSTALLER_TIMEOUT_MS,
|
|
177
|
+
env: { ...process.env, AGENT_CONFIG_HOOKS_DIR: stage },
|
|
178
|
+
});
|
|
179
|
+
if (res.error !== undefined) {
|
|
180
|
+
throw new Error(`${INSTALLER_REL} did not complete: ${res.error.message}`);
|
|
181
|
+
}
|
|
182
|
+
if (res.status !== 0) {
|
|
183
|
+
const err = res.stderr ? res.stderr.toString('utf-8') : '';
|
|
184
|
+
throw new Error(`${INSTALLER_REL} exited ${String(res.status)}\n${err}`);
|
|
185
|
+
}
|
|
186
|
+
const out = new Map<string, Buffer>();
|
|
187
|
+
for (const name of fs.readdirSync(stage).sort()) {
|
|
188
|
+
out.set(name, fs.readFileSync(path.join(stage, name)));
|
|
189
|
+
}
|
|
190
|
+
return out;
|
|
191
|
+
} finally {
|
|
192
|
+
fs.rmSync(stage, { recursive: true, force: true });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function inspect(repoRoot: string, hooksDirOverride?: string): FreshnessReport {
|
|
197
|
+
const hooksDir = hooksDirOverride ?? resolveHooksDir(repoRoot);
|
|
198
|
+
const expected = renderExpected(repoRoot);
|
|
199
|
+
if (expected.size === 0) {
|
|
200
|
+
// The installer produced nothing — it cannot have been read correctly.
|
|
201
|
+
throw new DeadScopeError(
|
|
202
|
+
'check_installed_hooks_fresh',
|
|
203
|
+
`${INSTALLER_REL} wrote no hooks — the installer, not the installed copy, is the problem.`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
const verdicts: HookVerdict[] = [];
|
|
207
|
+
for (const [name, body] of expected) {
|
|
208
|
+
const target = path.join(hooksDir, name);
|
|
209
|
+
let installed: Buffer | null = null;
|
|
210
|
+
let executable = false;
|
|
211
|
+
let readError: string | undefined;
|
|
212
|
+
try {
|
|
213
|
+
installed = fs.readFileSync(target);
|
|
214
|
+
// The installer chmod +x's every hook. Git silently skips one that
|
|
215
|
+
// is not executable, so mode is part of what "installed" means.
|
|
216
|
+
//
|
|
217
|
+
// POSIX only. Windows reports 0o666 / 0o444 and has no executable
|
|
218
|
+
// bit, so every hook would resolve `not-executable` and the gate
|
|
219
|
+
// would exit 1 forever — a permanent false red on the platform the
|
|
220
|
+
// test suite explicitly accommodates.
|
|
221
|
+
executable =
|
|
222
|
+
process.platform === 'win32' || (fs.statSync(target).mode & 0o111) !== 0;
|
|
223
|
+
} catch (e) {
|
|
224
|
+
const code = (e as NodeJS.ErrnoException).code ?? 'UNKNOWN';
|
|
225
|
+
installed = null;
|
|
226
|
+
// ENOENT is absence; anything else is a file that exists and could
|
|
227
|
+
// not be read, which a re-install will not fix on its own.
|
|
228
|
+
if (code !== 'ENOENT') readError = code;
|
|
229
|
+
}
|
|
230
|
+
const state: HookVerdict['state'] =
|
|
231
|
+
installed === null
|
|
232
|
+
? readError === undefined
|
|
233
|
+
? 'missing'
|
|
234
|
+
: 'unreadable'
|
|
235
|
+
: !installed.equals(body)
|
|
236
|
+
? 'differs'
|
|
237
|
+
: executable
|
|
238
|
+
? 'match'
|
|
239
|
+
: 'not-executable';
|
|
240
|
+
verdicts.push({
|
|
241
|
+
name,
|
|
242
|
+
state,
|
|
243
|
+
expectedSha: sha(body),
|
|
244
|
+
installedSha: installed === null ? null : sha(installed),
|
|
245
|
+
...(readError === undefined ? {} : { readError }),
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
hooksDir,
|
|
250
|
+
verdicts,
|
|
251
|
+
// "Never installed" means nothing is THERE. An unreadable hook is
|
|
252
|
+
// there, so it must not be swallowed by the fresh-clone branch.
|
|
253
|
+
neverInstalled: verdicts.every((v) => v.state === 'missing'),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function render(report: FreshnessReport, quiet: boolean, write: (s: string) => void): number {
|
|
258
|
+
// Outside the quiet guard on purpose: CI-shaped callers pass --quiet, and a
|
|
259
|
+
// gate that reports no count there reads as silent.
|
|
260
|
+
const collect = ((chunk: string | Uint8Array): boolean => {
|
|
261
|
+
write(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf-8'));
|
|
262
|
+
return true;
|
|
263
|
+
}) as typeof process.stdout.write;
|
|
264
|
+
reportScanned(
|
|
265
|
+
{
|
|
266
|
+
gate: 'check_installed_hooks_fresh',
|
|
267
|
+
scanned: report.verdicts.length,
|
|
268
|
+
units: 'managed hook(s)',
|
|
269
|
+
roots: [INSTALLER_REL],
|
|
270
|
+
},
|
|
271
|
+
collect,
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
if (report.neverInstalled) {
|
|
275
|
+
write(
|
|
276
|
+
`✅ check_installed_hooks_fresh: no managed hook is installed at ${report.hooksDir} — ` +
|
|
277
|
+
`nothing to compare. Run '${FIX}' to install them.\n`,
|
|
278
|
+
);
|
|
279
|
+
return 0;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const stale = report.verdicts.filter((v) => v.state !== 'match');
|
|
283
|
+
if (stale.length === 0) {
|
|
284
|
+
if (!quiet) {
|
|
285
|
+
for (const v of report.verdicts) {
|
|
286
|
+
write(` ${v.name} ${v.expectedSha}\n`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
write(
|
|
290
|
+
`✅ check_installed_hooks_fresh: ${String(report.verdicts.length)} installed hook(s) match ${INSTALLER_REL}.\n`,
|
|
291
|
+
);
|
|
292
|
+
return 0;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
write(`⚠️ check_installed_hooks_fresh: .git/hooks does not match this checkout.\n`);
|
|
296
|
+
write(` hooks dir: ${report.hooksDir} (shared by every linked worktree)\n`);
|
|
297
|
+
write(` worktree: ${REPO}\n`);
|
|
298
|
+
for (const v of stale) {
|
|
299
|
+
switch (v.state) {
|
|
300
|
+
case 'missing':
|
|
301
|
+
write(` ${v.name} MISSING (this checkout renders ${v.expectedSha})\n`);
|
|
302
|
+
break;
|
|
303
|
+
case 'unreadable':
|
|
304
|
+
write(` ${v.name} UNREADABLE (${String(v.readError)}) — exists but cannot be read\n`);
|
|
305
|
+
break;
|
|
306
|
+
case 'not-executable':
|
|
307
|
+
write(` ${v.name} NOT EXECUTABLE — byte-identical, and git skips it\n`);
|
|
308
|
+
break;
|
|
309
|
+
default:
|
|
310
|
+
write(
|
|
311
|
+
` ${v.name} installed ${String(v.installedSha)} ≠ this checkout ${v.expectedSha}\n`,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// Deliberately NOT "run the installer". A mismatch proves difference, never
|
|
316
|
+
// that THIS checkout is the newer side: linked worktrees share one hooks
|
|
317
|
+
// directory, so on a sibling-worktree mismatch a re-install only moves the
|
|
318
|
+
// mismatch to the other worktree, and on a behind-base checkout it writes
|
|
319
|
+
// the OLDER hook set over the shared one.
|
|
320
|
+
write(`\n A mismatch does not by itself say which side is newer.\n`);
|
|
321
|
+
write(` · behind the base? merge it first — the installer moves with it\n`);
|
|
322
|
+
write(` · sibling worktree on a different branch? re-installing only moves the mismatch\n`);
|
|
323
|
+
write(` · otherwise ${FIX}\n`);
|
|
324
|
+
write(` Gates the installed hooks are missing are inert on this checkout.\n`);
|
|
325
|
+
write(` Silence this notice with ${SKIP_ENV}=1.\n`);
|
|
326
|
+
return 1;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
330
|
+
let quiet = false;
|
|
331
|
+
let hooksDir: string | undefined;
|
|
332
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
333
|
+
const a = argv[i];
|
|
334
|
+
if (a === '--quiet') quiet = true;
|
|
335
|
+
else if (a === '-h' || a === '--help') {
|
|
336
|
+
process.stdout.write(
|
|
337
|
+
'usage: check_installed_hooks_fresh [--quiet] [--hooks-dir PATH]\n',
|
|
338
|
+
);
|
|
339
|
+
return 0;
|
|
340
|
+
} else if (a === '--hooks-dir') {
|
|
341
|
+
i += 1;
|
|
342
|
+
const next = argv[i];
|
|
343
|
+
// `--hooks-dir --quiet` must be a usage error, not a hooks
|
|
344
|
+
// directory literally named "--quiet" — which would report every
|
|
345
|
+
// managed hook missing and exit 1 with a staleness verdict, the
|
|
346
|
+
// exact misdiagnosis the undefined check below was written against.
|
|
347
|
+
if (next === undefined || next.startsWith('--')) {
|
|
348
|
+
process.stderr.write(
|
|
349
|
+
'check_installed_hooks_fresh: --hooks-dir needs a path, got ' +
|
|
350
|
+
`${next === undefined ? 'nothing' : JSON.stringify(next)}\n`,
|
|
351
|
+
);
|
|
352
|
+
return 2;
|
|
353
|
+
}
|
|
354
|
+
hooksDir = next;
|
|
355
|
+
} else {
|
|
356
|
+
process.stderr.write(`check_installed_hooks_fresh: unknown argument ${a}\n`);
|
|
357
|
+
return 2;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
let report: FreshnessReport;
|
|
362
|
+
try {
|
|
363
|
+
report = inspect(REPO, hooksDir);
|
|
364
|
+
} catch (e) {
|
|
365
|
+
process.stderr.write(
|
|
366
|
+
`❌ check_installed_hooks_fresh: ${e instanceof Error ? e.message : String(e)}\n`,
|
|
367
|
+
);
|
|
368
|
+
return 2;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const chunks: string[] = [];
|
|
372
|
+
const code = render(report, quiet, (s) => chunks.push(s));
|
|
373
|
+
const text = chunks.join('');
|
|
374
|
+
if (code === 0) process.stdout.write(text);
|
|
375
|
+
else process.stderr.write(text);
|
|
376
|
+
return code;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
380
|
+
process.exit(main());
|
|
381
|
+
}
|
|
@@ -71,13 +71,18 @@ import {
|
|
|
71
71
|
collect_span_commits,
|
|
72
72
|
derive_categories,
|
|
73
73
|
parse_git_log,
|
|
74
|
+
mix_response_blockers,
|
|
75
|
+
previous_promise,
|
|
74
76
|
previous_release_tag,
|
|
77
|
+
promise_readback_blockers,
|
|
75
78
|
stale_draft_labels,
|
|
76
79
|
} from './_lib/release_highlights.js';
|
|
77
80
|
import { loadTaxonomy, measureRange } from './measure_release_mix.js';
|
|
78
81
|
import {
|
|
79
82
|
CURATED_HEAD_INSTRUCTION,
|
|
83
|
+
PROMISE_READBACK_MARKER,
|
|
80
84
|
extract_changelog_section,
|
|
85
|
+
previous_changelog_version,
|
|
81
86
|
} from './_lib/release_material.js';
|
|
82
87
|
import { assertScanned, assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
|
|
83
88
|
|
|
@@ -206,7 +211,8 @@ export function main(argv: readonly string[]): number {
|
|
|
206
211
|
}
|
|
207
212
|
throw err;
|
|
208
213
|
}
|
|
209
|
-
const
|
|
214
|
+
const changelogText = fs.readFileSync(changelogPath, 'utf-8');
|
|
215
|
+
const section = extract_changelog_section(changelogText, version);
|
|
210
216
|
if (!section) {
|
|
211
217
|
process.stderr.write(`CHANGELOG carries no section for ${version}\n`);
|
|
212
218
|
return 2;
|
|
@@ -238,6 +244,23 @@ export function main(argv: readonly string[]): number {
|
|
|
238
244
|
);
|
|
239
245
|
return 1;
|
|
240
246
|
}
|
|
247
|
+
// Hoisted above the curated-head branches on purpose. It used to sit inside
|
|
248
|
+
// `contradictions.length === 0`, so CI reached it only for a section that
|
|
249
|
+
// HAS a head, carries no unrewritten draft and contradicts nothing — while
|
|
250
|
+
// the local guards call the same predicate unconditionally. A section
|
|
251
|
+
// without a curated head was therefore refused locally and waved through by
|
|
252
|
+
// CI: one predicate, two different gatekeepers, which is the drift this
|
|
253
|
+
// change exists to remove.
|
|
254
|
+
const mix = check_governance_mix_response(section.body, from, to, version);
|
|
255
|
+
if (mix !== 0) {
|
|
256
|
+
return mix;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const readback = check_previous_promise_readback(changelogText, section.body, version);
|
|
260
|
+
if (readback !== 0) {
|
|
261
|
+
return readback;
|
|
262
|
+
}
|
|
263
|
+
|
|
241
264
|
const curated = parse_curated_head(section.body);
|
|
242
265
|
if (!curated) {
|
|
243
266
|
process.stdout.write(`ℹ️ no curated head in the ${version} section — nothing to check\n`);
|
|
@@ -314,10 +337,6 @@ export function main(argv: readonly string[]): number {
|
|
|
314
337
|
}
|
|
315
338
|
const contradictions = highlight_contradictions(curated, derived);
|
|
316
339
|
if (contradictions.length === 0) {
|
|
317
|
-
const mix = check_governance_mix_response(section.body, from, to, version);
|
|
318
|
-
if (mix !== 0) {
|
|
319
|
-
return mix;
|
|
320
|
-
}
|
|
321
340
|
process.stdout.write(`✅ curated head plausible for ${version} (span ${from}..${to})\n`);
|
|
322
341
|
return 0;
|
|
323
342
|
}
|
|
@@ -361,9 +380,6 @@ export function main(argv: readonly string[]): number {
|
|
|
361
380
|
* shallow clone or a missing tag on the release path would otherwise turn an
|
|
362
381
|
* unrelated environment fact into a blocked release.
|
|
363
382
|
*/
|
|
364
|
-
export const MIX_RESPONSE_MARKER = '**Governance mix:**';
|
|
365
|
-
const MIX_RESPONSE_MIN_CHARS = 40;
|
|
366
|
-
|
|
367
383
|
export function check_governance_mix_response(
|
|
368
384
|
body: string,
|
|
369
385
|
from: string,
|
|
@@ -385,22 +401,91 @@ export function check_governance_mix_response(
|
|
|
385
401
|
process.stdout.write(`✅ governance mix for ${version}: ${level} — no response owed\n`);
|
|
386
402
|
return 0;
|
|
387
403
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
404
|
+
// ONE predicate, shared with `guard_release_branch_push` — see
|
|
405
|
+
// `mix_response_blockers`. This side prints and exits; the local side dies
|
|
406
|
+
// before anything leaves the machine. Neither owns the rule.
|
|
407
|
+
const blockers = mix_response_blockers(body, version, `\`release/${version}\``, {
|
|
408
|
+
triggered: true,
|
|
409
|
+
level,
|
|
410
|
+
});
|
|
411
|
+
if (blockers.length === 0) {
|
|
391
412
|
process.stdout.write(`✅ governance mix for ${version}: ${level} — response present\n`);
|
|
392
413
|
return 0;
|
|
393
414
|
}
|
|
394
415
|
process.stderr.write(
|
|
395
416
|
`❌ the ${version} section owes a governance-versus-product response: ${level}.\n` +
|
|
417
|
+
blockers.map((b) => ` - ${b}\n`).join('') +
|
|
396
418
|
' Governance-only commits outnumber consumer-only commits over the release span, so\n' +
|
|
397
419
|
' the section carries a written response naming either the next cycle\'s consumer work\n' +
|
|
398
420
|
' or a maintainer justification (docs/contracts/CHANGELOG-conventions.md § Governance-\n' +
|
|
399
421
|
' versus-product response; the decline this replaces is ADR-253).\n' +
|
|
400
|
-
` Add one line under the ${version} curated head, for example:\n` +
|
|
401
|
-
` > ${MIX_RESPONSE_MARKER} ${level}. Next cycle ships <the consumer work>.\n` +
|
|
402
422
|
' Only that section is read; historical sections are out of scope. No ratio is\n' +
|
|
403
|
-
' enforced — this blocks a MISSING answer, never a particular number.\n'
|
|
423
|
+
' enforced — this blocks a MISSING answer, never a particular number.\n' +
|
|
424
|
+
' Reproduce locally, before any push:\n' +
|
|
425
|
+
` ./scripts-run src/scripts/check_release_highlights --version ${version}\n`,
|
|
426
|
+
);
|
|
427
|
+
return 1;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* The previous release's promise, read back at the next release.
|
|
432
|
+
*
|
|
433
|
+
* `_lib/release_material.ts` generates `Next cycle ships …` and, until this
|
|
434
|
+
* check existed, nothing ever asked what became of it: 14.18.0 promised the MCP
|
|
435
|
+
* bridge repair, and no gate in the tree would have noticed the same promise
|
|
436
|
+
* being reprinted in 14.19.0 and 14.20.0. A commitment nothing reads back is
|
|
437
|
+
* free to restate, and one that is free to restate is not a commitment.
|
|
438
|
+
*
|
|
439
|
+
* Only the section under release is read, and only the previous section's
|
|
440
|
+
* governance-response block — the same scope every other check here uses, so a
|
|
441
|
+
* historical section cannot red a future release.
|
|
442
|
+
*
|
|
443
|
+
* The check is about ANSWERING, never about having kept the promise. A gate
|
|
444
|
+
* that refused an unkept promise would price honesty above silence and teach
|
|
445
|
+
* the next author to promise nothing.
|
|
446
|
+
*/
|
|
447
|
+
export function check_previous_promise_readback(
|
|
448
|
+
changelogText: string,
|
|
449
|
+
body: string,
|
|
450
|
+
version: string,
|
|
451
|
+
): number {
|
|
452
|
+
const previousVersion = previous_changelog_version(changelogText, version);
|
|
453
|
+
if (previousVersion === null) {
|
|
454
|
+
process.stdout.write(`✅ no previous CHANGELOG section before ${version} — no promise to read back\n`);
|
|
455
|
+
return 0;
|
|
456
|
+
}
|
|
457
|
+
const promise = previous_promise(changelogText, previousVersion);
|
|
458
|
+
if (promise === null) {
|
|
459
|
+
process.stdout.write(
|
|
460
|
+
`✅ the ${previousVersion} section made no next-cycle promise — nothing owed by ${version}\n`,
|
|
461
|
+
);
|
|
462
|
+
return 0;
|
|
463
|
+
}
|
|
464
|
+
const blockers = promise_readback_blockers(
|
|
465
|
+
body,
|
|
466
|
+
version,
|
|
467
|
+
previousVersion,
|
|
468
|
+
promise,
|
|
469
|
+
`\`release/${version}\``,
|
|
470
|
+
);
|
|
471
|
+
if (blockers.length === 0) {
|
|
472
|
+
process.stdout.write(`✅ ${version} answers the ${previousVersion} next-cycle promise\n`);
|
|
473
|
+
return 0;
|
|
474
|
+
}
|
|
475
|
+
process.stderr.write(
|
|
476
|
+
`❌ the ${version} section owes an answer to the ${previousVersion} next-cycle promise.\n` +
|
|
477
|
+
blockers.map((b) => ` - ${b}\n`).join('') +
|
|
478
|
+
` The ${previousVersion} promise reads:\n` +
|
|
479
|
+
promise
|
|
480
|
+
.split('\n')
|
|
481
|
+
.map((l) => ` ${l}\n`)
|
|
482
|
+
.join('') +
|
|
483
|
+
` Answering it is one blockquote line under the curated head, e.g.\n` +
|
|
484
|
+
` > ${PROMISE_READBACK_MARKER} the <promise> did not ship; <what happened>.\n` +
|
|
485
|
+
' Shipped, did not ship, or withdrawn with a reason — the check refuses a MISSING\n' +
|
|
486
|
+
' answer, never a particular outcome. An unkept promise answered honestly passes.\n' +
|
|
487
|
+
' Reproduce locally, before any push:\n' +
|
|
488
|
+
` ./scripts-run src/scripts/check_release_highlights --version ${version}\n`,
|
|
404
489
|
);
|
|
405
490
|
return 1;
|
|
406
491
|
}
|