@event4u/agent-config 14.17.0 → 14.18.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.
Files changed (74) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +170 -79
  5. package/README.md +1 -1
  6. package/dist/agent-src/commands/pr/merge.md +35 -44
  7. package/dist/agent-src/skills/accessibility-auditor/SKILL.md +2 -0
  8. package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +27 -1
  9. package/dist/agent-src/skills/code-intelligence/SKILL.md +60 -12
  10. package/dist/agent-src/skills/complexity-first-planning/SKILL.md +27 -0
  11. package/dist/agent-src/skills/design-review/SKILL.md +2 -0
  12. package/dist/agent-src/skills/feature-planning/SKILL.md +34 -0
  13. package/dist/agent-src/skills/git-workflow/SKILL.md +37 -12
  14. package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +176 -0
  15. package/dist/agent-src/skills/laravel-mail/SKILL.md +44 -0
  16. package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +40 -0
  17. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +12 -0
  18. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +53 -8
  19. package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +3 -1
  20. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  21. package/dist/cli-delegate/cmd_conformance.js +335 -659
  22. package/dist/discovery/deprecation-report.md +1 -1
  23. package/dist/discovery/discovery-manifest.json +15 -15
  24. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  25. package/dist/discovery/discovery-manifest.summary.md +1 -1
  26. package/dist/discovery/dormancy-report.md +1 -1
  27. package/dist/discovery/orphan-report.md +1 -1
  28. package/dist/discovery/packs.json +2 -2
  29. package/dist/discovery/trust-report.md +1 -1
  30. package/dist/discovery/workspaces.json +2 -2
  31. package/dist/hooks/dispatch.js +959 -1304
  32. package/dist/mcp/registry-manifest.json +2 -2
  33. package/dist/mcp/server.json +2 -2
  34. package/dist/mcp/server.mjs +671 -1002
  35. package/dist/shared/settingsConsent.js +1 -1
  36. package/package.json +1 -1
  37. package/src/config/conformance-claim-baseline.json +9 -0
  38. package/src/config/gate-violation-baselines.json +3 -3
  39. package/src/config/release-gate-locality.yml +129 -0
  40. package/src/scripts/_lib/release_highlights.ts +172 -1
  41. package/src/scripts/_lib/release_material.ts +66 -0
  42. package/src/scripts/_lib/repeated_run.ts +77 -0
  43. package/src/scripts/_lib/tamper_vocabulary.ts +242 -0
  44. package/src/scripts/_lib/unified_diff.ts +121 -0
  45. package/src/scripts/check_finding_dispositions.ts +179 -3
  46. package/src/scripts/check_installed_hooks_fresh.ts +381 -0
  47. package/src/scripts/check_release_highlights.ts +25 -13
  48. package/src/scripts/check_suppression_hygiene.ts +11 -0
  49. package/src/scripts/code_graph/build.ts +230 -33
  50. package/src/scripts/code_graph/extract.ts +168 -33
  51. package/src/scripts/code_graph/types.ts +37 -2
  52. package/src/scripts/code_graph/validate.ts +12 -1
  53. package/src/scripts/conformance_scan.ts +2 -2
  54. package/src/scripts/detect_silent_catch.ts +403 -0
  55. package/src/scripts/detect_verification_tampering.ts +538 -0
  56. package/src/scripts/git_authorization_hook.ts +153 -17
  57. package/src/scripts/hook_manifest.json +1 -1
  58. package/src/scripts/hook_manifest.yaml +33 -19
  59. package/src/scripts/hooks/block_no_verify.ts +3 -3
  60. package/src/scripts/hooks/concern_registry.ts +2 -2
  61. package/src/scripts/hooks/evidence_independence.ts +21 -2
  62. package/src/scripts/hooks/{block_unauthorized_git.ts → git_command_classifier.ts} +25 -369
  63. package/src/scripts/hooks/push_settle_hook.ts +250 -0
  64. package/src/scripts/hooks/ship_diff_volume_hook.ts +1 -1
  65. package/src/scripts/install-hooks.sh +134 -1
  66. package/src/scripts/language_mirror_hook.ts +27 -5
  67. package/src/scripts/release.ts +31 -143
  68. package/src/scripts/release_drill.ts +71 -1
  69. package/src/scripts/release_publication.ts +243 -3
  70. package/src/scripts/release_verify.ts +220 -0
  71. package/src/scripts/schemas/review-findings.schema.json +1 -1
  72. package/src/scripts/schemas/skill.schema.json +11 -1
  73. package/src/scripts/self_review_gate.ts +10 -1
  74. package/src/shared/settingsConsent.ts +1 -1
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  model_tier: inherit
3
3
  name: code-intelligence
4
- description: "Route codebase-structure questions (who calls X, where used, what imports, change-impact) to a code-graph first, grep fallback. Triggers 'who calls', 'where is this used', 'call graph'."
4
+ description: "Route codebase-structure questions (who calls X, where is this used, what imports, change-impact) to an existing code-graph first: cheaper, never more precise; grep stays routine. Also 'call graph'."
5
5
  domain: engineering
6
6
  workspaces:
7
7
  - engineering
@@ -14,11 +14,14 @@ requires_skills:
14
14
  # code-intelligence
15
15
 
16
16
  > For a **structure** question — *who calls X*, *where is Y used*, *what
17
- > imports Z*, *what breaks if I change this symbol* — a code-graph answers far
18
- > more precisely than a blind `grep`, at a fraction of the tokens. This skill
19
- > routes such questions to the native code-graph engine (ADR-124, Class A) or a
20
- > consumer-shipped index first, and falls back to grep **with a stated reason**
21
- > when the graph cannot answer. It is the executable side of
17
+ > imports Z*, *what breaks if I change this symbol* — ask an index that already
18
+ > exists before rebuilding the relationship by hand: it is already built, its
19
+ > answer is structured, and it costs a fraction of the tokens. It is **not** more
20
+ > precise than a blind `grep` — measured on this repository's own source, zero
21
+ > classes met the win bar (§ Measured twice) — so grep is the arm you fall back
22
+ > to routinely and **with a stated reason**, not grudgingly. This skill routes
23
+ > such questions to the native code-graph engine (ADR-124, Class A) or a
24
+ > consumer-shipped index first. It is the executable side of
22
25
  > [`external-code-graph-interop`](../../rules/external-code-graph-interop.md):
23
26
  > *orchestrator first, owner where it wins*.
24
27
 
@@ -50,9 +53,15 @@ code-graph and no appetite to build one — plain `grep`/read is right there.
50
53
  - `agent-config code-graph path <a> <b>` — how two symbols connect.
51
54
  - `agent-config code-graph explain <symbol>` — 2-hop neighbourhood.
52
55
  Pass `--budget <tokens>` to cap output.
53
- 4. **Read the confidence.** `EXTRACTED` = syntactic fact; `INFERRED` =
54
- hierarchy-resolved; `AMBIGUOUS` = dynamic dispatch / facade, carries
56
+ 4. **Read the confidence.** `EXTRACTED` = syntactic fact — a symbol declared in
57
+ the file, or a name bound to the module specifier the file names; `INFERRED`
58
+ = resolved by hierarchy, or by a repo-wide same-name lookup with no binding
59
+ in the file to justify it; `AMBIGUOUS` = dynamic dispatch / facade, carries
55
60
  candidates — treat its target as *one of* the candidates, never as certain.
61
+ Two target shapes are **not nodes** and resolve to nothing you can read:
62
+ `symbol:<name>` (a name this repository does not declare) and
63
+ `external:<module>` / `external:<module>#<name>` (a name imported from
64
+ outside the indexed root — `external:node:path`).
56
65
  5. **Fall back honestly.** If the graph has no entry for the symbol, grep — and
57
66
  say so: *"the graph has no entry for X, so I grepped."* If this command is
58
67
  not available, grep and say so.
@@ -72,10 +81,22 @@ Every answer built with this skill MUST:
72
81
  - **A stale graph lies confidently.** If `detect` reports the index is N commits
73
82
  behind, rebuild (`agent-config code-graph build --update`) before trusting
74
83
  relationship answers, or say the answer is from a stale index.
75
- - **Dynamic dispatch is honestly ambiguous.** On a Laravel/JS codebase most
76
- method-call edges are `AMBIGUOUS` (facades, injected services, `$obj->m()`).
77
- That is the engine being honest, not broken — do not "resolve" them yourself
78
- by guessing.
84
+ - **Dynamic dispatch is honestly ambiguous — where there is anything to be
85
+ ambiguous ABOUT.** A `$obj->m()` whose name matches a method declared
86
+ somewhere in the repo is `AMBIGUOUS` and carries those candidates: the engine
87
+ being honest, not broken — do not "resolve" them yourself by guessing. A
88
+ dynamic call whose name matches **no** in-repo method (`xs.push()`,
89
+ `map.get()`) is not emitted at all, and the count is published as
90
+ `suppressed_edge_counts.dynamic_no_candidate`. It used to be 39 % of this
91
+ engine's own graph, pointing at `symbol:push`, which is not a node and which
92
+ no query verb could reach.
93
+ - **The AMBIGUOUS share is a property of the code, not a constant.** Measured
94
+ 2026-09-04 over the three roots the v2 benchmark uses: `AMBIGUOUS` is 0 of 495
95
+ edges under the package's own graph-engine root, 0 of 181 under `src/shared`, and 121 of
96
+ 4,002 (3.0 %) under its council root — class-free TypeScript has
97
+ almost no in-repo method to be ambiguous between. A Laravel codebase, where
98
+ facades and injected services dispatch onto real in-repo methods, is the
99
+ opposite case. Read the counts in the graph rather than a remembered ratio.
79
100
 
80
101
  ## Measured twice, and it has not won (v2, 2026-08-29)
81
102
 
@@ -113,6 +134,33 @@ engine on this repository's own code. The rule now gives the reasons that surviv
113
134
  measurement — an index that exists is already built and structured, so it is the
114
135
  cheap first question — and drops the precision claim its own benchmark refuted.
115
136
 
137
+ ## Re-run after the extractor repair (2026-09-04) — still no class won
138
+
139
+ `internal/bench/reports/code-graph-vs-grep-inrepo-v2-rerun-2026-09-04.md`. The
140
+ SAME registration, the same corpus SHA, the same per-class bars and the same
141
+ arm-B verbs, re-run after the import-binding repair. The 2026-08-29 report is
142
+ untouched; this is a second report beside it, which is how v1 was handled too.
143
+
144
+ | Class | graph R then | graph R now | graph P then | graph P now | verdict then → now |
145
+ |---|---|---|---|---|---|
146
+ | `callers` | 1.000 | 1.000 | 0.667 | 0.667 | TIE → TIE |
147
+ | `transitive-impact` | 0.500 | **0.611** | 0.667 | **1.000** | **NULL → TIE** |
148
+ | `path-between` | 1.000 | 1.000 | 1.000 | 1.000 | TIE → TIE |
149
+ | `references` | 0.333 | **1.000** | 0.333 | **1.000** | **NULL → TIE** |
150
+
151
+ **Zero classes met the win bar, again.** Two NULLs became TIEs and nothing
152
+ regressed, so the routing verdict below is unchanged — a TIE is not a win, and
153
+ no bar was renegotiated after the repair.
154
+
155
+ Read with two caveats the report states in full. A measured root is live source:
156
+ `src/shared` is byte-identical between the runs, the council root moved
157
+ by 27 files on `main` in between, and the graph-engine root IS the engine, so
158
+ its content necessarily moves whenever the engine does — which is also why the
159
+ GREP arm's macro precision moved (0.806 → 0.764) in a run that changed nothing
160
+ about grep. Both classes that changed verdict did so on rows whose root did not
161
+ drift: `references` moved on the graph-engine and shared roots, `transitive-impact` on
162
+ `shared` alone.
163
+
116
164
  **No class is graph-first.** Query the index first because an index that already
117
165
  exists is cheap to ask and its answer is structured — not because it answers
118
166
  better. When it returns nothing, that is the common case, and grep remains the
@@ -62,6 +62,33 @@ unknown to resolve), or when the user has already fixed the sequence.
62
62
  A short ordered plan that leads with the load-bearing unknown + how it will be
63
63
  proven, then the dependent steps. One recommendation, not a survey.
64
64
 
65
+ ## The four plan criteria — what a reviewer fails the ordered plan on
66
+
67
+ Risk-first ordering is one of four things a plan is judged on. All four are
68
+ stated so a reviewer can **fail** a plan on them — each names what failing looks
69
+ like. This skill owns the third; the other three are stated here because a plan
70
+ reviewed on ordering alone passes while being unnecessary, insufficient, or
71
+ built on verbs that do not exist. The same four are carried in
72
+ `feature-planning` (pack `product-basic`), which is pruned under `projection.mode: scoped` and so is named rather than linked.
73
+
74
+ 1. **Necessity** — every step traces to a stated problem or acceptance criterion.
75
+ **Fails when** the plan carries work nothing asked for — a spike for an
76
+ unknown that is not load-bearing is the version of this failure this skill
77
+ causes.
78
+ 2. **Sufficiency** — every requirement is discharged by at least one step.
79
+ **Fails when** a requirement owns no step, or its only step has no verifiable
80
+ outcome. Walk the requirements, not the steps.
81
+ 3. **Ordering** — the load-bearing unknown is resolved before anything that
82
+ assumes it, and no step consumes what a later step creates. **Fails when** the
83
+ plan front-loads the easy parts, or ranks by effort instead of dependency
84
+ blast radius (see Gotchas). This is the criterion this skill exists for.
85
+ 4. **Groundedness** — every command, skill and tool the plan tells someone to run
86
+ exists. **Fails when** the plan names a verb that is not in the index. Check
87
+ it against [`CAPABILITIES.yaml`](../../../CAPABILITIES.yaml) at the repository
88
+ root — the generated capability index of every shipped skill and command —
89
+ plus the project's own script/task entry points. It is the only one of the
90
+ four decidable without judgement, so it is never waved through.
91
+
65
92
  ## Do NOT
66
93
 
67
94
  - Build the easy parts first to show progress, then discover the hard part breaks them.
@@ -3,6 +3,8 @@ model_tier: high
3
3
  name: design-review
4
4
  description: "When the user says \"review the design\", \"check the UI\", or wants a comprehensive UI/UX review. Uses a 7-phase methodology covering interaction, responsiveness, accessibility and more."
5
5
  domain: quality
6
+ enforced_by:
7
+ - "instruction-only: the Phase 4 sweep claims WCAG 2.1 AA and its criteria table is a deliberate SUBSET — depth is delegated to accessibility-auditor, which claims 2.2 AA and carries the resolver. No resolver checks this table against 2.1, so the claim is model-carried and the gap is recorded rather than implied."
6
8
  workspaces:
7
9
  - engineering
8
10
  packs:
@@ -176,6 +176,40 @@ complexity: lightweight # default — skips bite-sized granularity
176
176
 
177
177
  Source: adapted from an external reference; complexity-gating is our addition (Council Round 1, Q4 — mitigates UX pushback for senior engineers on well-scoped work).
178
178
 
179
+ ## The four plan criteria — what a reviewer fails a plan on
180
+
181
+ The 3-scan self-review below catches mechanical gaps. These four catch the plan
182
+ itself. Each is written so a reviewer can **fail** on it: the criterion states
183
+ what failing looks like, not what passing feels like. A plan that cannot be
184
+ failed on any of the four has not been reviewed, it has been read.
185
+
186
+ 1. **Necessity** — every phase, step and artifact in the plan is traceable to a
187
+ stated problem or acceptance criterion. **Fails when** the plan contains work
188
+ no requirement asked for: a new abstraction with one caller, a migration for a
189
+ field nothing reads, a "while we're in there" refactor, a config surface
190
+ nobody requested. Name the orphan step and the requirement it does not map to.
191
+ 2. **Sufficiency** — every stated requirement and acceptance criterion is
192
+ discharged by at least one step. **Fails when** a requirement has no owning
193
+ step, or when the only step that claims it is a heading with no verifiable
194
+ outcome under it. This is the mirror of Necessity and is checked in the
195
+ opposite direction: walk the requirements, not the steps.
196
+ 3. **Ordering** — no step depends on the outcome of a step that comes after it,
197
+ and the load-bearing unknown is resolved before the work that assumes it (see
198
+ [`complexity-first-planning`](../complexity-first-planning/SKILL.md)).
199
+ **Fails when** step N consumes something step N+2 creates, or when the plan
200
+ builds three cheap phases on an integration nobody has proven yet.
201
+ 4. **Groundedness** — every command, skill, command-verb and tool the plan tells
202
+ someone to run **exists**. **Fails when** the plan names a verb, skill or
203
+ command that is not in the index. Check it against
204
+ [`CAPABILITIES.yaml`](../../../CAPABILITIES.yaml) at the repository root — the
205
+ generated capability index listing every shipped skill and command per
206
+ capability area — plus the project's own script/task entry points for anything
207
+ the index does not cover. This is the one of the four that is decidable
208
+ without judgement, so it is the one that should never be waved through.
209
+
210
+ A failure on any of the four is a blocker, not a note: fix the plan and re-run
211
+ the criterion, or move the item to *Open questions* with the reason.
212
+
179
213
  ## Self-review (3-scan checklist)
180
214
 
181
215
  Before presenting any plan, run these three scans in order. Each is a fast pass — not a deep review. Failures block presentation; fix and re-scan.
@@ -100,21 +100,46 @@ Work complete. What would you like to do?
100
100
  ### Option 1: Push and create PR
101
101
 
102
102
  1. Run quality pipeline + tests (only when `quality.local_auto_run: true`; default `false` → skip, remote CI is the gate).
103
- 2. `git push -u origin <branch>`.
104
- 3. `gh pr create` using PR template.
103
+ 2. **The push-ready sequence** — fetch → integrate the base SET → regenerate →
104
+ verify → re-check freshness (this repo wires it as a `push-ready` task target;
105
+ a consumer wires its own). Not optional housekeeping: see § A push closes its
106
+ own loop. A stale push is refused, so skipping this buys the refusal.
107
+ 3. `git push -u origin <branch>`.
108
+ 4. `gh pr create` using PR template.
109
+ 5. **Settle it** — the turn is not over at step 4:
110
+ `npx tsx node_modules/@event4u/agent-config/src/scripts/ci_settle.ts <pr>`
105
111
 
106
- ### Option 2: Keep as-is
112
+ ## A push closes its own loop
107
113
 
108
- Report: "Branch `<name>` preserved locally." — do nothing.
109
-
110
- ### Option 3: Discard
111
-
112
- **Confirm first** — list branch name and commit count.
113
- Wait for explicit confirmation. Then:
114
- ```bash
115
- git checkout main
116
- git branch -D <feature-branch>
117
114
  ```
115
+ A PUSH IS NOT A DELIVERY. THE EVIDENCE FOR A PUSH IS THE CI VERDICT,
116
+ NEVER THE PUSH'S OWN EXIT CODE.
117
+ BEHIND THE BASE → INTEGRATE BEFORE PUSHING, NEVER AFTER THE PR IS RED.
118
+ RED AFTER PUSHING → FIX IT IN THE SAME TURN, OR SAY PLAINLY THAT YOU DID NOT.
119
+ NEVER HAND THE USER A RED PR WITH ITS CAUSE NAMED AND UNFIXED.
120
+ ```
121
+
122
+ Two halves, failing differently, over the 30 sessions and 50 PRs before
123
+ 2026-09-04. **Stale base**: 25 of 50 PRs carried a `Merge branch 'main' into …`
124
+ commit (52 in total), and the three most-failing workflows are the base-relative
125
+ ones — a branch pushed behind its base was verified against a base it no longer
126
+ merges into. **Unsettled push**: 22 of 30 sessions ran `gh run view
127
+ --log-failed`, and 20 of 50 PRs carried a follow-up `fix(ci|gates|budget)`
128
+ commit. Only 19 of 50 landed with neither. Each half now has a deterministic
129
+ carrier — the pre-push hook refuses a verified-behind branch and points at the
130
+ push-ready sequence; the `push-settle` PostToolUse concern fires the moment git
131
+ reports a ref advanced. Neither replaces the discipline: the hook is skippable
132
+ with `AGENT_CONFIG_SKIP_PREPUSH_FRESHNESS=1` for a genuine WIP push, and the
133
+ settle reminder is advisory, because leaving a push deliberately unsettled is
134
+ legitimate — **ending the turn silently on one is not**.
135
+
136
+ `ci_settle` non-zero → read only the failing part
137
+ (`gh run view --job <id> --log-failed | grep -E '×|FAIL|Error'`), fix, push
138
+ again; the author of the red is irrelevant (`fix-what-you-see`). Three failed
139
+ attempts on one target → stop and surface them (`autonomous-execution` N=3).
140
+ Exit `2` is **not** a verdict — the wait timed out or the API could not be read.
141
+ Counts, carrier limits and the honest cost:
142
+ [`references/push-closes-its-loop.md`](references/push-closes-its-loop.md).
118
143
 
119
144
  ## PR template
120
145
 
@@ -0,0 +1,176 @@
1
+ # A push closes its own loop — the measurement
2
+
3
+ Reference for [`git-workflow`](../SKILL.md) § A push closes its own loop. The
4
+ obligation lives in the skill; this file carries the evidence behind it and the
5
+ two deterministic carriers that now exist, so neither has to be re-derived and
6
+ neither inflates the skill body.
7
+
8
+ ## What was measured
9
+
10
+ Corpus: the 30 most recent local session transcripts and the 50 most recent
11
+ pull requests on this repository, read on 2026-09-04. Session events are counted
12
+ from git's and `gh`'s own literal output in the transcripts, never from prose
13
+ about them; PR workflow runs come from `actions/runs?event=pull_request`, which
14
+ paginates out at 1,000 runs and therefore covers **36** of the 50 PRs — the
15
+ older 14 are unmeasured, not clean, and are excluded from that row rather than
16
+ counted as green.
17
+
18
+ ### Half one — the stale base
19
+
20
+ | Signal | Count |
21
+ |---|---|
22
+ | PRs carrying ≥ 1 `Merge branch 'main' into <branch>` commit | **25 / 50** |
23
+ | Such merge commits in total | **52** |
24
+ | Sessions hitting a literal `CONFLICT (content)` / `Automatic merge failed` | **10 / 30** |
25
+ | Sessions hitting a rejected push (`Updates were rejected` / non-fast-forward) | **20 / 30** |
26
+
27
+ The worst single PRs needed four and five separate base merges. What that
28
+ buys is not only conflict work: the three workflows that failed most —
29
+ **Tests (18), Consistency (16), Rule Backstops (13)** — are the base-relative
30
+ ones. Ratchets, budgets and generated counts are green against the base a branch
31
+ forked from and red against the base it merges into, so a branch pushed behind
32
+ its base was verified against a question nobody asked.
33
+
34
+ ### Half two — the unsettled push
35
+
36
+ | Signal | Count |
37
+ |---|---|
38
+ | Sessions running `gh run view --log-failed` (a red job was investigated) | **22 / 30** |
39
+ | PRs with ≥ 1 FAILED workflow run (of the 36 readable) | **17** |
40
+ | Failed runs in total | **56** |
41
+ | PRs carrying a follow-up `fix(ci\|gates\|budget\|deps\|tests)` commit | **20 / 50** |
42
+ | Such repair commits in total | **41** |
43
+
44
+ Every one of those pushes SUCCEEDED. What failed was the ending: the push
45
+ returned 0, the turn closed, and the red arrived where the user found it.
46
+
47
+ **Only 19 of 50 PRs landed with neither half.**
48
+
49
+ ## The two carriers
50
+
51
+ | Half | Carrier | Behavior |
52
+ |---|---|---|
53
+ | Stale base | `check_branch_freshness` in the pre-push hook (`src/scripts/install-hooks.sh`) | Asks the REMOTE, one `ls-remote`, ~4.5 s. Refuses only on a VERIFIED behind state and names the push-ready sequence. Never merges. |
54
+ | Unsettled push | the `push-settle` PostToolUse concern (`src/scripts/hooks/push_settle_hook.ts`) | Fires on git's own ref-advance report, resolves the PR number, and names the literal `ci_settle` command. Warn only. |
55
+
56
+ ### What each one does NOT do
57
+
58
+ - **The hook never merges.** Resolution belongs to a step run with the result in
59
+ front of you; a hook that rewrites the tree at the moment the contributor
60
+ believes their work is finished turns one refused push into an unreviewed
61
+ commit. It refuses and names the push-ready sequence instead.
62
+ - **The hook cannot see an unverifiable base.** Offline, in CI, on a detached
63
+ HEAD, or standing on the base itself, `check_branch_freshness` exits 0 and
64
+ says which — so an unreachable network never blocks a push, and a green there
65
+ means "did not refuse", not "current".
66
+ - **The concern cannot block.** It is `severity: advisory`,
67
+ `fail_closed: false`. Leaving a push deliberately unsettled is legitimate;
68
+ ending the turn silently on one is what the reminder exists to stop. Whether
69
+ the reminder changes behavior is unmeasured — it ships as a carrier, not as a
70
+ claim.
71
+ - **Neither reaches a host without the slot.** `agent-config hooks:status`
72
+ answers which slots are bound where you actually are; `post_tool_use` is
73
+ unbound on windsurf and copilot, and there the settle obligation is
74
+ model-carried by the skill alone.
75
+
76
+ ## Cost, stated rather than implied
77
+
78
+ The pre-push gate set was already measured at 36.05 s against the 25 s
79
+ `pre_push_budget_seconds` ceiling in `src/config/ci-local-parity.yml`, and the
80
+ freshness gate adds ~4.5 s on the green path. That is a real cost on an already
81
+ over-budget hook, and it is recorded here rather than hidden: the trade is 4.5 s
82
+ per push against 52 base merges and 56 failed runs across 50 PRs. On the RED
83
+ path the hook gets *cheaper*, not dearer — the refusal exits before the ~10 s
84
+ regeneration and the preflight set, because everything after it would be
85
+ answered against a base that is about to move.
86
+
87
+ ## The gap this change does NOT close
88
+
89
+ **The pre-push hook is INSTALLED, not read from source.** `install-hooks.sh`
90
+ writes `.git/hooks/pre-push`, and afterwards it is rewritten only by the
91
+ package manager's post-install lifecycle step or by running the installer
92
+ again. Measured in this repository on 2026-09-04, before this change: the
93
+ installed hook was
94
+ **113 lines** against a source body of ~146 — it was missing header revisions
95
+ merged on 2026-08-30, so it had been stale for days with no signal anywhere.
96
+
97
+ The freshness gate therefore does nothing on a checkout whose hook predates it,
98
+ and the contributor is not told. That is a real hole in a change whose whole
99
+ subject is "the agent should not leave work for the user to discover", and it is
100
+ recorded rather than papered over:
101
+
102
+ task install-hooks # rewrites .git/hooks/* from source
103
+
104
+ ### Closed 2026-09-05 — the contributor is now told
105
+
106
+ `check_installed_hooks_fresh` renders what `install-hooks.sh` would write into a
107
+ scratch directory and compares content AND the executable bit against
108
+ `.git/hooks`. It runs in the pre-push body — advisory, and after the
109
+ base-freshness gate — and again from the `post-merge` / `post-checkout`
110
+ auto-sync block, at the moment a pull causes the drift.
111
+
112
+ **It reports and never refuses, and the ordering is part of the answer.** The
113
+ predicate is "installed == what THIS checkout renders", and `.git/hooks` is
114
+ shared by every linked worktree, so with two worktrees whose installer bodies
115
+ differ at most one satisfies it at a time — a refusal would fire on ordinary
116
+ parallel work and the skip variable would become routine, which retires the
117
+ check rather than enforcing it. And being behind a base that moved the installer
118
+ is the commonest cause of a mismatch, whose repair is to MERGE: re-installing
119
+ from a behind-base checkout writes the older hook set over the shared directory.
120
+ Base freshness therefore answers first, and the notice never prescribes a
121
+ re-install as universally correct. On the checkout that closed the gap it found `pre-push`, `post-merge` and
122
+ `post-checkout` all stale — the same drift, five days on and one hook wider.
123
+
124
+ It compares rendered output rather than slicing these heredocs, because
125
+ `post-merge` and `post-checkout` are a heredoc PLUS an appended block: no slice
126
+ of the installer equals what it installs, so a slice-based comparison would have
127
+ been wrong for two of six hooks.
128
+
129
+ **What was deliberately NOT built: the repair.** The roadmap asked for
130
+ `post-merge` to re-run the installer so the hooks fixed themselves with no
131
+ command. Two findings stopped it, and both are worth keeping.
132
+
133
+ 1. **Measured 2026-09-05:** a bash script that `cat >`-overwrites its own path
134
+ mid-run stops executing at that point and **exits 0**. Every statement after
135
+ it is skipped, silently. A repair placed in `post-merge` would truncate
136
+ `post-merge`'s own remaining body — the projection sync and the CLI rebuild —
137
+ and report success. Any future attempt needs the installer to stage and
138
+ `mv` into place first.
139
+ 2. **Linked worktrees share one `.git/hooks`** through the common dir. "The
140
+ installed hooks match the checked-out tree" has no unique referent when eight
141
+ worktrees hold eight versions of the installer: a repair would let whichever
142
+ worktree checked out last redefine the gates all the others run, and a
143
+ checkout of an older branch would reinstall older hooks over newer ones.
144
+
145
+ An AI council (`claude-sonnet-4-5` + `codex-default`, 2026-09-05, two rounds,
146
+ 2 of 2 seats present in both) reached the same verdict independently in both
147
+ seats: report, do not repair. *Reopen* on either per-worktree hook isolation
148
+ (`core.hooksPath`) or a branch-independent dispatcher installed once in the
149
+ common dir — both make "which version is authoritative" answerable, which is the
150
+ question the repair cannot currently answer.
151
+
152
+ ### Answered 2026-09-05 — consumers get no git hooks, by decision
153
+
154
+ A consumer install writes **no git hooks at all** (`src/install/` references
155
+ `.git/hooks` nowhere, and the package manager's post-install lifecycle step does
156
+ not run for a registry dependency), so the pre-push gate is a maintainer-only
157
+ mechanism. The same council, unanimously, made that the decision rather than the
158
+ status quo: the pre-push chain runs this repository's own consistency and
159
+ preflight gate chains, which depend on its Taskfile, its `./scripts-run` shim
160
+ and its generated trees — none of which exist in a consumer project — and a
161
+ dependency install should not establish persistent repository execution. *Revisit-if* a
162
+ consumer-native gate set is designed with its own opt-in command and consent
163
+ step; that is a product feature, not an extension of this installer. Stated for
164
+ consumers in `docs/development.md` § "Git hooks are maintainer-only". Named
165
+ rather than linked: this file is projected byte-identically into
166
+ `dist/agent-src/`, which sits one level deeper, so a repo-root-relative link
167
+ resolves in one copy and not in the other.
168
+
169
+ ## Revisit-if
170
+
171
+ - A later corpus shows base merges and post-push repair commits at a rate the
172
+ carriers cannot explain — then the carriers are firing and not landing, and
173
+ the next lever is delivery, not another reminder.
174
+ - `pre_push_budget_seconds` is enforced by something. Today nothing measures the
175
+ hook, which is how the set grew past its own ceiling with no signal; once it
176
+ is measured, this gate is one of the entries that has to justify its slice.
@@ -167,6 +167,50 @@ Route::get('/mail-preview', function () {
167
167
  });
168
168
  ```
169
169
 
170
+ ## Surviving the mail client
171
+
172
+ An email is not a web page. Mail clients strip, rewrite, and ignore CSS that
173
+ every browser honors, so a template that renders correctly in
174
+ `/mail-preview` tells you nothing about the inbox. Markdown templates (above)
175
+ give you a tested baseline for free — this section is what to hold to when a
176
+ design forces you off them.
177
+
178
+ **Four requirements, in order of what breaks first:**
179
+
180
+ 1. **Table-based layout, not flexbox or grid.** Use nested `<table>` elements
181
+ with `role="presentation"`, a fixed outer width of 600px, and `cellpadding`
182
+ / `cellspacing` / `border` set to `0`. `display: flex` and
183
+ `display: grid` are unsupported or partially supported in the Windows
184
+ Outlook family and collapse to a single stacked column.
185
+ 2. **Inline styles, not a `<style>` block.** Write `style="…"` on the element.
186
+ A `<head><style>` block is stripped outright by some webmail clients, and
187
+ class selectors then match nothing. Use a CSS inliner at build time if the
188
+ template is authored with classes — never ship the classes unresolved. Keep
189
+ a `<style>` block only for what cannot be inlined (media queries), and treat
190
+ everything in it as optional.
191
+ 3. **No web fonts, no background images, no external JS.** Declare a font stack
192
+ ending in a system fallback; a remote font silently degrades. Background
193
+ images require the `v:fill` VML fallback in Outlook, so put the color on
194
+ `bgcolor` and treat the image as decoration.
195
+ 4. **Explicit width and alt text on every image, and no image-only content.**
196
+ Images are blocked by default in several clients, so an email whose call to
197
+ action is an image is an email with no call to action.
198
+
199
+ **The client list worth testing, and what breaks in each:**
200
+
201
+ | Client | What breaks |
202
+ |---|---|
203
+ | **Outlook 2016-2019 / Windows (Word engine)** | `flex`, `grid`, `float`, `max-width`, `border-radius`, background images, `padding` on `<div>`; the strictest target — if it renders, most others do |
204
+ | **Outlook.com / Outlook 365 web** | strips `<style>` blocks in some views; rewrites `class` attributes; ignores `margin` on several elements |
205
+ | **Gmail web** | clips the message past ~102 KB of HTML with a "view entire message" link — anything below the clip, including the unsubscribe link, is not seen; strips `<style>` when the message is clipped |
206
+ | **Gmail app (iOS / Android)** | no support for embedded `<style>` on non-Gmail accounts; media queries ignored there, so the mobile layout must be the fluid default |
207
+ | **Apple Mail / iOS Mail** | the most permissive; auto-scales small text and auto-links dates and addresses unless suppressed — a false green if it is the only client you check |
208
+ | **Dark mode (Apple Mail, Outlook, Gmail)** | colors are force-inverted; a logo on a hardcoded white background becomes a white box on dark, and `#000` text on a transparent background becomes invisible |
209
+
210
+ **Verify against a real client, not a preview route.** `/mail-preview` renders in
211
+ a browser and proves none of the above. Send to real accounts, or use a
212
+ rendering service, before the template ships.
213
+
170
214
  ## Core rules
171
215
 
172
216
  - **Always queue emails** — implement `ShouldQueue` to avoid blocking requests.
@@ -208,6 +208,46 @@ rg -n '\b(it|test|describe)\.skip\b|\.only\b|xit\(|xdescribe\(|@pytest\.mark\.(s
208
208
 
209
209
  This is the test-surface instance of the `autonomous-execution` N=3 / allowlist-growth antipattern (bulk `skip`/`xfail` to force green counts as the tool being wrong, not the content) and the `verify-before-complete` floor (green must be *earned*, not manufactured).
210
210
 
211
+ ### The named smells — canonical vocabulary, and what it does NOT cover
212
+
213
+ The behaviors above are described; two of them also have field names a
214
+ reviewer can cite, and this table is where the name meets the behavior. The
215
+ grep column follows the shape [`ai-code-blindspots`](../ai-code-blindspots/SKILL.md)
216
+ uses: an authoring-time backstop, zero results is the pass, a hit is a prompt
217
+ to read that line — never an auto-fix.
218
+
219
+ | Canonical name | The behavior, as already described above | Backstop grep |
220
+ |---|---|---|
221
+ | **Magic-number test** | Anti-Pattern 6 — "a hardcoded expected value the code will always emit". The expectation is a literal rather than something derived from the input, so the test cannot disagree with the code. | `rg -n '\b(toBe\|toEqual\|assertEquals\|assertSame)\(\s*-?[0-9]{2,}' tests/` |
222
+ | **Assertion roulette** | The multi-case stack Anti-Pattern 6 *requires* (boundary, error, abuse in one test), written without per-assertion messages — so a failure names a line number and not a reason. The stack is right; the silence is the smell. | `rg -c '\b(expect\|assert\w*\|self\.assert\w*\|\$this->assert\w*)\(' tests/ \| awk -F: '$2 >= 8'` |
223
+
224
+ Both greps are scoped to **the test file you just wrote**, not to a tree —
225
+ `tests/` above is a placeholder for that path. Run over a whole repository
226
+ they are useless as verdicts: measured on this repository on 2026-09-04 the
227
+ first returns 585 lines and the second 1189 files, because an exact assertion
228
+ against a two-digit literal and a file with eight assertions are both ordinary.
229
+ They are authoring-time prompts to reread one file, exactly as
230
+ `ai-code-blindspots` states, and neither is a gate.
231
+
232
+ The second grep counts per **file**, not per test — no regex sees a test
233
+ boundary.
234
+
235
+ **Three further names are deliberately NOT in the table**, recorded here so a
236
+ later round meets a record rather than a fresh argument. Measured 2026-09-04
237
+ over both files of this skill: `roulette`, `eager`, `lazy` and `duplicate` each
238
+ return **0** occurrences, and `hardcoded` returns **6**.
239
+
240
+ - **Eager test** (one test exercising several behaviors) would carry the same
241
+ behavior and the same grep as assertion roulette above, and Anti-Pattern 6
242
+ *prescribes* the shape on purpose. A row for it would be one entry under two
243
+ names, which is the drift this table exists to prevent.
244
+ - **Lazy test** (several tests over one fixture, differing only in name) is
245
+ described nowhere above. A row would be new guidance wearing a naming step's
246
+ clothes.
247
+ - **Duplicate assert** (the same condition asserted twice in one test) is
248
+ likewise undescribed, and is not greppable without parsing test boundaries —
249
+ so it could not carry the backstop column even if it were.
250
+
211
251
  ## Output format
212
252
 
213
253
  1. The mocking decision recorded as a one-line comment in the test file (`// mock at <seam>: <reason>`).
@@ -109,6 +109,18 @@ Ask format:
109
109
 
110
110
  **Recommendation: 1 — `.worktrees/`** — project-local keeps the worktree next to the repo (easy cleanup), and the leading dot keeps it out of `ls`. Caveat: pick 3 if multiple repos must share a single worktree root.
111
111
 
112
+ > **A worktree outside the repository root costs the persistent shell cwd.**
113
+ > It sits outside the host's working-directory boundary, so a `cd` into it does
114
+ > not survive the call — Claude Code appends `Shell cwd was reset to <root>` and
115
+ > the next command runs in the main checkout. Every command then repeats its own
116
+ > `cd`, and one that forgets edits the wrong tree silently. Measured 2026-09-05
117
+ > on this repository: 2,128 transcript records carrying that line across 20
118
+ > sessions, alongside 1,694 whose `cd` targeted a worktree placed beside the
119
+ > repo. Choosing option 3 — or any path outside the root — means also declaring
120
+ > the parent directory in the host's working-directory allowlist
121
+ > (`permissions.additionalDirectories` on Claude Code). Options 1 and 2 are
122
+ > inside the root and need nothing.
123
+
112
124
  ### 3. Verify ignore-safety (project-local only)
113
125
 
114
126
  ```bash