@event4u/agent-config 14.16.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.
- package/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +177 -213
- package/README.md +2 -2
- package/dist/agent-src/commands/analyze/inbox.md +172 -6
- package/dist/agent-src/commands/fix/commit-messages.md +336 -0
- package/dist/agent-src/commands/fix.md +5 -3
- package/dist/agent-src/commands/pr/merge.md +35 -44
- package/dist/agent-src/rules/commit-conventions.md +1 -1
- package/dist/agent-src/skills/accessibility-auditor/SKILL.md +2 -0
- package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +27 -1
- package/dist/agent-src/skills/aws-infrastructure/SKILL.md +41 -0
- 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/conventional-commits-writing/SKILL.md +234 -4
- package/dist/agent-src/skills/design-review/SKILL.md +2 -0
- 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/terraform/SKILL.md +29 -0
- package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +40 -0
- package/dist/agent-src/skills/threat-modeling/data/reasoning.csv +1 -0
- package/dist/agent-src/skills/threat-modeling/data/surfaces.csv +1 -0
- package/dist/agent-src/skills/threat-modeling/data/threats.csv +5 -0
- package/dist/agent-src/skills/using-git-worktrees/SKILL.md +12 -0
- 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-delegate/cmd_conformance.js +335 -659
- package/dist/cli-delegate/cmd_explain.js +24 -112
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +55 -29
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +6 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +5 -4
- package/dist/hooks/dispatch.js +959 -1304
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +671 -1002
- package/dist/shared/settingsConsent.js +1 -1
- package/package.json +1 -1
- package/src/config/conformance-claim-baseline.json +9 -0
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/config/release-gate-locality.yml +129 -0
- package/src/config/web-launch-readiness.json +1 -1
- package/src/scripts/_cli/explain_last/index.ts +1 -1
- package/src/scripts/_cli/explain_last/memory.ts +8 -104
- package/src/scripts/_lib/release_highlights.ts +172 -1
- package/src/scripts/_lib/release_material.ts +66 -0
- package/src/scripts/_lib/repeated_run.ts +77 -0
- package/src/scripts/_lib/tamper_vocabulary.ts +242 -0
- package/src/scripts/_lib/unified_diff.ts +121 -0
- package/src/scripts/check_artefact_count_messaging.ts +9 -0
- package/src/scripts/check_finding_dispositions.ts +179 -3
- package/src/scripts/check_installed_hooks_fresh.ts +381 -0
- package/src/scripts/check_release_highlights.ts +89 -4
- package/src/scripts/check_suppression_hygiene.ts +11 -0
- package/src/scripts/check_web_launch_readiness.ts +295 -15
- 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 +2 -2
- package/src/scripts/detect_silent_catch.ts +403 -0
- package/src/scripts/detect_verification_tampering.ts +538 -0
- package/src/scripts/git_authorization_hook.ts +153 -17
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +33 -19
- package/src/scripts/hooks/block_no_verify.ts +3 -3
- package/src/scripts/hooks/concern_registry.ts +2 -2
- package/src/scripts/hooks/evidence_independence.ts +21 -2
- package/src/scripts/hooks/{block_unauthorized_git.ts → git_command_classifier.ts} +25 -369
- package/src/scripts/hooks/push_settle_hook.ts +250 -0
- package/src/scripts/hooks/ship_diff_volume_hook.ts +1 -1
- package/src/scripts/install-hooks.sh +134 -1
- package/src/scripts/language_mirror_hook.ts +27 -5
- package/src/scripts/measure_release_mix.ts +384 -0
- package/src/scripts/release.ts +31 -143
- package/src/scripts/release_drill.ts +71 -1
- package/src/scripts/release_mix_taxonomy.json +172 -0
- package/src/scripts/release_publication.ts +243 -3
- package/src/scripts/release_verify.ts +220 -0
- package/src/scripts/schemas/review-findings.schema.json +1 -1
- package/src/scripts/schemas/skill.schema.json +11 -1
- package/src/scripts/self_review_gate.ts +10 -1
- package/src/shared/settingsConsent.ts +1 -1
- package/src/scripts/attest_artifact.ts +0 -193
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
model_tier: medium
|
|
3
3
|
name: conventional-commits-writing
|
|
4
|
-
description: "When writing commit
|
|
4
|
+
description: "When writing a commit message, branch name or squash title — measure the repo's own convention first, Conventional Commits as fallback — even on a bare 'commit this'."
|
|
5
5
|
domain: process
|
|
6
6
|
scope:
|
|
7
7
|
write: []
|
|
@@ -22,6 +22,8 @@ packs:
|
|
|
22
22
|
|
|
23
23
|
Use this skill when:
|
|
24
24
|
|
|
25
|
+
- Committing in a repository whose conventions you have not established this
|
|
26
|
+
session — run `## Procedure: Establish the house convention` first
|
|
25
27
|
- Generating a commit message from staged changes
|
|
26
28
|
- Generating a squash merge title from a PR
|
|
27
29
|
- Deciding the correct Conventional Commit type for a change
|
|
@@ -34,6 +36,217 @@ Do NOT use when:
|
|
|
34
36
|
- The message is already correct and does not need review
|
|
35
37
|
- Following the Git workflow (use `git-workflow` skill)
|
|
36
38
|
|
|
39
|
+
## Procedure: Establish the house convention (run FIRST, before any message)
|
|
40
|
+
|
|
41
|
+
Conventional Commits is this suite's **fallback**, not a universal truth. A
|
|
42
|
+
repository that has demonstrably converged on another grammar gets that grammar;
|
|
43
|
+
imposing the shipped default there produces commits that read as foreign in
|
|
44
|
+
`git log` and in review. The precedence, highest first:
|
|
45
|
+
|
|
46
|
+
| Tier | Source | Binding? |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| **1 — configured** | `commitlint.config.*` · `.gitmessage` · a `commit-msg` hook (husky / lefthook / `.git/hooks`) · `CONTRIBUTING.md` § Commits · a CI job that validates subjects · release automation that PARSES subjects (semantic-release, changesets, git-cliff, conventional-changelog) | yes — Class A, no approval needed |
|
|
49
|
+
| **2 — measured + approved** | the consensus pass below, after the user says yes | yes, for this repository |
|
|
50
|
+
| **3 — measured, unapproved** | the same pass before the user answers | **no — advisory**; report the mismatch, write Conventional |
|
|
51
|
+
| **4 — default** | Conventional Commits | yes |
|
|
52
|
+
|
|
53
|
+
Release automation is the trap that makes tier 1 outrank tier 2 even when the
|
|
54
|
+
history disagrees: a repo whose `git log` is 85 % `[JIRA-123] Fix thing` but
|
|
55
|
+
whose manifest (`package.json`, `composer.json`, `pyproject.toml`, `Cargo.toml`)
|
|
56
|
+
runs `semantic-release` or its ecosystem equivalent is a repo **migrating toward**
|
|
57
|
+
Conventional Commits, and the measurement is reading its past, not its intent.
|
|
58
|
+
Check for the parser before you trust the prevalence.
|
|
59
|
+
|
|
60
|
+
### 1. Look for tier 1 before measuring anything
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
ls commitlint.config.* .commitlintrc* .gitmessage .czrc 2>/dev/null
|
|
64
|
+
git config --get commit.template
|
|
65
|
+
ls .husky/commit-msg .git/hooks/commit-msg 2>/dev/null
|
|
66
|
+
grep -rniE 'semantic-release|git-cliff|conventional-changelog|release-please|commitlint' \
|
|
67
|
+
package.json composer.json pyproject.toml Cargo.toml .github/workflows/ .gitlab-ci.yml \
|
|
68
|
+
2>/dev/null | head
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Every one of these produces candidates, not verdicts — open the file before
|
|
72
|
+
you act on the hit.** A `commit.template` proves a template exists, not that it
|
|
73
|
+
constrains the subject: a `.gitmessage` carrying only a body checklist ends
|
|
74
|
+
nothing. `CONTRIBUTING.md` is prose and cannot be grepped for a convention —
|
|
75
|
+
`grep -i commit` matches "please commit early and often" — so read its commits
|
|
76
|
+
section if it has one and take the grammar from the sentences, not from the
|
|
77
|
+
match. Only a **confirmed** source ends the procedure, and when it does, say
|
|
78
|
+
which file answered.
|
|
79
|
+
|
|
80
|
+
`changesets` is deliberately not in the parser list: it reads `.changeset/*.md`
|
|
81
|
+
files and never parses a commit subject, so its presence says nothing about the
|
|
82
|
+
convention. The four that do parse subjects — semantic-release, git-cliff,
|
|
83
|
+
conventional-changelog, release-please — count only when the hit is a real
|
|
84
|
+
dependency or a wired job, not a mention in a comment.
|
|
85
|
+
|
|
86
|
+
The tier table above calls a confirmed hit Class A. That is exact for a machine
|
|
87
|
+
-readable config and loose for `CONTRIBUTING.md`, which carries no digest and is
|
|
88
|
+
read by a human judgement — treat a prose source as tier 1 in **precedence** and
|
|
89
|
+
as a quoted sentence in the report, so the next reader can check it.
|
|
90
|
+
|
|
91
|
+
### 2. Sample the history — native source per dimension
|
|
92
|
+
|
|
93
|
+
Only **commit subjects** are reliably measurable from a clone. Branch names,
|
|
94
|
+
PR titles and commit granularity have different native sources, and `unknown`
|
|
95
|
+
is a valid, honest result for each:
|
|
96
|
+
|
|
97
|
+
| Dimension | Native source | Measurable from a clone? |
|
|
98
|
+
|---|---|---|
|
|
99
|
+
| Subject grammar | `git log` | yes |
|
|
100
|
+
| Branch naming | forge refs / `git branch -r` | partially — merged branches are often deleted |
|
|
101
|
+
| PR title shape | forge API (`gh pr list`) | only with forge access |
|
|
102
|
+
| Granularity (atomic vs batched) | per-commit diff stat | **no** where the repo squash-merges — the evidence was destroyed at merge |
|
|
103
|
+
|
|
104
|
+
Resolve the trunk explicitly. `origin/HEAD` is set by `git clone` and is absent
|
|
105
|
+
in a `git init` + `git remote add` checkout, in many CI checkouts and in
|
|
106
|
+
worktrees, where the sample command would die rather than return nothing:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
TRUNK=$(git symbolic-ref -q --short refs/remotes/origin/HEAD)
|
|
110
|
+
[ -n "$TRUNK" ] || TRUNK=$(git rev-parse -q --verify origin/main >/dev/null && echo origin/main)
|
|
111
|
+
[ -n "$TRUNK" ] || TRUNK=$(git rev-parse -q --verify origin/master >/dev/null && echo origin/master)
|
|
112
|
+
[ -n "$TRUNK" ] || TRUNK=HEAD # no remote at all: this checkout is the trunk
|
|
113
|
+
git log "$TRUNK" --no-merges -n 200 --since='24 months ago' \
|
|
114
|
+
--pretty=format:'%H%x09%aN%x09%aE%x09%s'
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**`--first-parent` is deliberately absent.** On a merge-commit workflow the
|
|
118
|
+
trunk's first-parent chain is merge commits, which `--no-merges` then drops —
|
|
119
|
+
the two together leave only what was committed straight to the trunk, which on
|
|
120
|
+
a PR-based repo is nearly nothing. The convention lives in the feature commits,
|
|
121
|
+
so walk the whole reachable history and exclude merges alone.
|
|
122
|
+
|
|
123
|
+
The terminal `HEAD` matters as much as the first line: a chain that ends with an
|
|
124
|
+
unset `TRUNK` hands `git log` an empty argument, which walks `HEAD` anyway —
|
|
125
|
+
silently doing the right thing on a local repo while hiding that the resolution
|
|
126
|
+
failed on one where it should not have.
|
|
127
|
+
|
|
128
|
+
`%H` leads the format because two of the exclusions below need the commit, not
|
|
129
|
+
just its subject. Drop, from the sample:
|
|
130
|
+
|
|
131
|
+
- **bot authors** — `[bot]`, `dependabot`, `renovate`, `github-actions`,
|
|
132
|
+
`semantic-release`, `release-please`;
|
|
133
|
+
- **automation subjects** — `^Revert `, `^Merge `, `chore(release)`,
|
|
134
|
+
`^Bump `, a bare `v?\d+\.\d+\.\d+`;
|
|
135
|
+
- **bulk imports** — resolve with the hash the format carries:
|
|
136
|
+
`git show --stat --oneline <sha> | tail -1` and drop anything over 500 files.
|
|
137
|
+
Check this only for the handful of subjects that read like an import; running
|
|
138
|
+
it per commit costs 200 subprocesses to remove two rows.
|
|
139
|
+
|
|
140
|
+
### 3. Classify each surviving subject
|
|
141
|
+
|
|
142
|
+
Match in order, first hit wins. These are POSIX extended regular expressions,
|
|
143
|
+
given outside a table because a markdown cell would need the alternation pipes
|
|
144
|
+
escaped and `\|` in ERE is a literal pipe, not an alternation:
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
conventional ^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([^)]+\))?!?:
|
|
148
|
+
ticket-prefix ^\[[A-Z][A-Z0-9]+-[0-9]+\][: ]|^[A-Z][A-Z0-9]+-[0-9]+[: ]
|
|
149
|
+
gitmoji ^:[a-z0-9_+-]+:[[:space:]]|^[^[:ascii:][:space:]]
|
|
150
|
+
imperative-plain ^[A-Z][a-z]+[[:space:]].*[^.]$
|
|
151
|
+
other everything else
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`imperative-plain` is deliberately mechanical — capitalised first word, no
|
|
155
|
+
trailing period — and does **not** test for the imperative mood. Mood needs a
|
|
156
|
+
verb lexicon this procedure does not ship, and `Update` versus `Updated` is
|
|
157
|
+
exactly the pair a lexicon-free test cannot separate. That is a stated limit of
|
|
158
|
+
the classifier, not a gap to fill by guessing: a repo whose only distinction
|
|
159
|
+
from Conventional is mood will read as `imperative-plain` either way, and the
|
|
160
|
+
mood question belongs in the ask at step 6.
|
|
161
|
+
|
|
162
|
+
`classifier_version` in step 7 names the revision of THIS section — the five
|
|
163
|
+
patterns above plus the exclusions in step 2. It is a provenance stamp so a
|
|
164
|
+
later measurement can be compared against a like one; it does not claim an
|
|
165
|
+
executable classifier ships anywhere in the tree.
|
|
166
|
+
|
|
167
|
+
Record the runner-up family too — a near-tie is itself the finding.
|
|
168
|
+
|
|
169
|
+
### 4. Aggregate, capped per author, per half
|
|
170
|
+
|
|
171
|
+
Raw commit counts let one prolific author or an unfiltered bot define the house
|
|
172
|
+
style; one-vote-per-author lets a drive-by contributor with two commits veto it.
|
|
173
|
+
Cap instead: split the sample into a newer and an older half by position first,
|
|
174
|
+
then let **each author contribute at most 20 commits to each half**. Capping the
|
|
175
|
+
sample as a whole would let one author's newest 20 fill their entire quota and
|
|
176
|
+
empty the older half, which is precisely where step 5's coherence check has to
|
|
177
|
+
look.
|
|
178
|
+
|
|
179
|
+
Compute the dominant family's share over the **capped** total, and report that
|
|
180
|
+
total — a percentage quoted against the raw eligible count describes a statistic
|
|
181
|
+
nobody computed.
|
|
182
|
+
|
|
183
|
+
With fewer than three authors the cap is **off**: there is no dominance to guard
|
|
184
|
+
against, and a cap of 20 would put the single-maintainer bar's own `n ≥ 30`
|
|
185
|
+
out of reach of its denominator.
|
|
186
|
+
|
|
187
|
+
### 5. The evidence bar
|
|
188
|
+
|
|
189
|
+
A measured convention is eligible to be proposed only when **all** hold:
|
|
190
|
+
|
|
191
|
+
- **n ≥ 30** eligible commits after exclusions;
|
|
192
|
+
- **≥ 80 %** capped-weighted share for the dominant family, over **≥ 3 human
|
|
193
|
+
authors** — or, with **one or two** authors, an uncapped **≥ 90 %** share over
|
|
194
|
+
n ≥ 30. Two authors take the stricter branch rather than falling between the
|
|
195
|
+
two: a two-person repo is a small-team repo, and leaving it unreachable by
|
|
196
|
+
both branches would make the measurement permanently advisory there;
|
|
197
|
+
- **temporal coherence** — the same family is dominant in the newer half of
|
|
198
|
+
the sample *and* in the older half. If the halves disagree materially the
|
|
199
|
+
repository is **migrating**: abstain, name the change point, and use the
|
|
200
|
+
newer half's family only if it independently clears the bar.
|
|
201
|
+
|
|
202
|
+
> The 80 % figure is the stricter of two council positions (2026-09-04,
|
|
203
|
+
> anthropic + openai, 2/2 quorum: 70 % vs 80 % + a two-thirds-of-authors
|
|
204
|
+
> clause). The per-author cap is why the separate author-share clause was not
|
|
205
|
+
> also adopted — it already answers the dominance concern the clause existed
|
|
206
|
+
> for. These are policy heuristics, not statistically derived thresholds.
|
|
207
|
+
> **Revisit-if:** a labelled corpus of repositories shows a false-positive or
|
|
208
|
+
> false-negative rate that a different bar would fix.
|
|
209
|
+
|
|
210
|
+
### 6. Ask — never adopt silently
|
|
211
|
+
|
|
212
|
+
Below the bar, or on a migrating repo: use Conventional Commits and say so
|
|
213
|
+
**once**, in one line, when a commit is actually requested. Do not ask.
|
|
214
|
+
|
|
215
|
+
At or above the bar, ask once, as numbered options (per `user-interaction`):
|
|
216
|
+
name the family, the share **and the capped total it was computed over**, the
|
|
217
|
+
author count, and — if a tier-1 parser exists — the tooling that would stop
|
|
218
|
+
parsing.
|
|
219
|
+
|
|
220
|
+
### 7. Persist the answer where the next session finds it
|
|
221
|
+
|
|
222
|
+
Write the measurement as a Class-B convention card under
|
|
223
|
+
`agents/memory/curated/conventions/quarantine/commit-subject.md`; the user's
|
|
224
|
+
approval is what moves it to `approved/`. Carry `observed_n`,
|
|
225
|
+
`dominant_share`, `author_count`, `sample_window`, `classifier_version`, and
|
|
226
|
+
`confirm_against` — aggregates only, never per-author identities. Re-measure
|
|
227
|
+
and re-review when the share falls below **70 %**, when a tier-1 source
|
|
228
|
+
appears, or when the newer half's family changes.
|
|
229
|
+
|
|
230
|
+
## What a measurement may never lower
|
|
231
|
+
|
|
232
|
+
Prevalence answers *what happened*, never *what the agent is permitted to do* —
|
|
233
|
+
so a house convention can change how a commit reads, and can never change what
|
|
234
|
+
the agent is allowed to commit. Three override policies:
|
|
235
|
+
|
|
236
|
+
| Policy | Applies to | Examples |
|
|
237
|
+
|---|---|---|
|
|
238
|
+
| `never` | authorization · security · integrity · destructive history · agent provenance | [`commit-policy`](../../rules/commit-policy.md) · [`secret-vcs-guard`](../../rules/secret-vcs-guard.md) · [`git-history-discipline`](../../rules/git-history-discipline.md) · [`non-destructive-by-default`](../../rules/non-destructive-by-default.md) · [`no-attribution-footers`](../../rules/no-attribution-footers.md) |
|
|
239
|
+
| `explicit-only` | governance preferences a maintainer may lift deliberately, that history may not | [`no-decorative-emojis-in-git-surfaces`](../../rules/no-decorative-emojis-in-git-surfaces.md) |
|
|
240
|
+
| `approved-observation` | representation — subject grammar, ticket prefixes, type vocabulary, capitalisation, mood, subject length, body wrapping | this procedure |
|
|
241
|
+
|
|
242
|
+
So a repository whose history is 70 % emoji subjects does **not** license emoji
|
|
243
|
+
subjects: that rule is `explicit-only`, and the lift is the maintainer's word,
|
|
244
|
+
not the log's. A repository whose history is 60 % attribution trailers licenses
|
|
245
|
+
nothing at all. The council split here (one member argued subject-line emoji is
|
|
246
|
+
a defect and absolute, the other that it is governance taste) resolves to
|
|
247
|
+
`explicit-only` because both positions agree on the operative half — **history
|
|
248
|
+
may not lift it**.
|
|
249
|
+
|
|
37
250
|
## Procedure: Generate commit message
|
|
38
251
|
|
|
39
252
|
### 1. Identify the actual intent
|
|
@@ -110,9 +323,12 @@ Or add `BREAKING CHANGE:` in the commit body/footer.
|
|
|
110
323
|
|
|
111
324
|
## Output format
|
|
112
325
|
|
|
113
|
-
1.
|
|
114
|
-
|
|
115
|
-
|
|
326
|
+
1. The convention in force and the tier that established it — `configured
|
|
327
|
+
(commitlint.config.js)`, `approved (ticket-prefix, 84% of 137)`, or
|
|
328
|
+
`default (Conventional Commits)`
|
|
329
|
+
2. Recommended commit message(s)
|
|
330
|
+
3. Brief rationale for type choice
|
|
331
|
+
4. Split suggestion if the change should be multiple commits
|
|
116
332
|
|
|
117
333
|
## Gotcha
|
|
118
334
|
|
|
@@ -120,6 +336,10 @@ Or add `BREAKING CHANGE:` in the commit body/footer.
|
|
|
120
336
|
- File type alone does not determine commit type (e.g. a `.md` change can be `feat` if it's a new feature doc)
|
|
121
337
|
- Squash merge titles should describe the net effect, not every internal detail
|
|
122
338
|
- `refactor` means NO behavior change — if behavior changes, use `feat` or `fix`
|
|
339
|
+
- A high dominant share over a long window can hide a format change six months
|
|
340
|
+
ago — the temporal-coherence check, not the share, is what catches it
|
|
341
|
+
- Squash-merging destroys commit-granularity evidence; report granularity as
|
|
342
|
+
`unknown` there rather than inferring it from the trunk
|
|
123
343
|
|
|
124
344
|
## Frugality Standards
|
|
125
345
|
|
|
@@ -146,6 +366,11 @@ to every commit message you author.
|
|
|
146
366
|
|
|
147
367
|
## Do NOT
|
|
148
368
|
|
|
369
|
+
- Do NOT impose Conventional Commits on a repository that has a configured or
|
|
370
|
+
approved convention of its own
|
|
371
|
+
- Do NOT adopt a measured convention without asking — below tier 2 it is
|
|
372
|
+
advisory, and silence is not approval
|
|
373
|
+
- Do NOT let prevalence lift a `never` or `explicit-only` floor
|
|
149
374
|
- Do NOT use vague messages: `update stuff`, `fix bug`, `changes`
|
|
150
375
|
- Do NOT use `refactor` for bug fixes
|
|
151
376
|
- Do NOT use `chore` for meaningful behavior changes
|
|
@@ -157,3 +382,8 @@ to every commit message you author.
|
|
|
157
382
|
- Rule: `commit-conventions` — base format, types, scope, examples
|
|
158
383
|
- Guideline: `docs/guidelines/php/git.md` — type selection rules, anti-patterns, decision checklist
|
|
159
384
|
- Command: `/commit` — uses this skill for message generation
|
|
385
|
+
- Command: `/fix commit-messages` — retro-fits past subjects to the convention
|
|
386
|
+
this procedure establishes
|
|
387
|
+
- Context: `contexts/execution/project-intelligence.md` — the Class-B
|
|
388
|
+
consensus-pass, quarantine→approved gate and deviation-staleness mechanism
|
|
389
|
+
this procedure instantiates for commit subjects
|
|
@@ -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.
|
|
104
|
-
|
|
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
|
-
|
|
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.
|
|
@@ -96,6 +96,35 @@ Check existing modules in the project for which registry modules are used.
|
|
|
96
96
|
- Security groups follow least-privilege: only allow traffic between known services.
|
|
97
97
|
- IAM policies use specific resource ARNs, not wildcards (except where unavoidable).
|
|
98
98
|
|
|
99
|
+
### Permissive defaults and missing security blocks
|
|
100
|
+
|
|
101
|
+
The failure that reaches production is almost never a pinned-version mistake;
|
|
102
|
+
it is a resource that works either way and was left on its permissive default.
|
|
103
|
+
Four constructs carry it. Each backstop grep is over the `.tf` tree you are
|
|
104
|
+
changing, not over this repository.
|
|
105
|
+
|
|
106
|
+
| Backstop grep | What a hit means | Override condition |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| `grep -rn '0\.0\.0\.0/0' --include='*.tf' .` | An ingress or egress rule whose CIDR is 0.0.0.0/0, i.e. open to the whole internet. On a management or database port stop and treat it as CRITICAL - the corpus row enumerates which ports those are. | A deliberately public listener - an ALB or CloudFront origin on 80/443. Nothing else. Record the reason next to the rule. |
|
|
109
|
+
| `grep -rn 'publicly_accessible\|block_public_acls\|ignore_public_acls' --include='*.tf' .` | Either `publicly_accessible = true` on a database, or a bucket with no public-access block present at all. Absence is the more common hit and the grep returning nothing is the failing case. | A bucket that genuinely serves public web assets, with the policy written explicitly rather than inherited from a default. |
|
|
110
|
+
| `grep -rnE '"(Action\|Resource)" *: *"\*"\|= *\["\*"\]' --include='*.tf' .` | A wildcard IAM action or resource. One compromised workload credential then reaches everything in the account. | A named, reviewed exception - `iam:PassRole` scoped by condition, or a bootstrap role. Never the default. |
|
|
111
|
+
| `grep -rL 'encrypt' --include='*.tf' .` (lists files with NO encryption mention; `-L` prints names only, so no `-n`) | A storage, volume, snapshot, or database resource with no encryption block. | None for data at rest. A resource holding no data at all. |
|
|
112
|
+
|
|
113
|
+
Read the surface class before the fix, not after: the mitigation text, the
|
|
114
|
+
abuse case, and the negative test live in the `infrastructure` rows of
|
|
115
|
+
[`threat-modeling`](../threat-modeling/SKILL.md)'s corpus, which is the single
|
|
116
|
+
authority for them. Ask that skill's grounded-corpus step for the
|
|
117
|
+
`infrastructure` surface class - it carries the invocation - and describe the
|
|
118
|
+
change you are making, e.g. *terraform security group ingress open to
|
|
119
|
+
0.0.0.0/0*. Do not copy the command here: one skill owns that interface, for
|
|
120
|
+
the same reason one file owns the control text.
|
|
121
|
+
|
|
122
|
+
Those rows also carry a `Decided by:` clause stating which check actually
|
|
123
|
+
decides each one. For all four constructs above that clause reads NO CHECK IN
|
|
124
|
+
THIS REPOSITORY - no HCL or IAM-policy parser ships here, so these greps and
|
|
125
|
+
your own IaC policy scanner are the whole enforcement story. Do not write or
|
|
126
|
+
imply otherwise.
|
|
127
|
+
|
|
99
128
|
## Common patterns
|
|
100
129
|
|
|
101
130
|
### ECS service with CodeDeploy (Blue/Green)
|