@event4u/agent-config 14.16.0 → 14.17.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 +83 -210
- package/README.md +1 -1
- 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/rules/commit-conventions.md +1 -1
- package/dist/agent-src/skills/aws-infrastructure/SKILL.md +41 -0
- package/dist/agent-src/skills/conventional-commits-writing/SKILL.md +234 -4
- package/dist/agent-src/skills/terraform/SKILL.md +29 -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/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli-delegate/cmd_explain.js +24 -112
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +42 -16
- 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/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/package.json +1 -1
- 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/check_artefact_count_messaging.ts +9 -0
- package/src/scripts/check_release_highlights.ts +77 -4
- package/src/scripts/check_web_launch_readiness.ts +295 -15
- package/src/scripts/measure_release_mix.ts +384 -0
- package/src/scripts/release_mix_taxonomy.json +172 -0
- 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
|
|
@@ -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)
|
|
@@ -8,3 +8,4 @@ public-endpoint,Rate limit + cache + minimal payload,"{""if_search_endpoint"": "
|
|
|
8
8
|
billing,Ledger + idempotency + server-side amounts,"{""if_refunds"": ""one-way-state-machine"", ""if_webhooks_from_psp"": ""verify-signature-and-amount""}",Mutable balance column updates,CRITICAL
|
|
9
9
|
secrets,Manager + redaction + scan,"{""if_third_party_keys"": ""scope-and-rotate-quarterly"", ""if_ci_pipeline"": ""masked-vars-only""}",Secrets in env committed to VCS,HIGH
|
|
10
10
|
data-exposure,Allowlist serializers + contract tests,"{""if_admin_fields"": ""separate-serializer"", ""if_logs_contain_payloads"": ""logger-redaction-keys""}",Spreading model attributes into responses,MEDIUM
|
|
11
|
+
infrastructure,Deny-by-default ingress + encryption on + scoped IAM,"{""if_management_or_db_port"": ""no-internet-cidr-use-bastion-or-private-link"", ""if_object_storage"": ""block-public-access-at-account-and-bucket"", ""if_iam_policy"": ""enumerate-actions-and-resource-arns""}",Permissive defaults left in place because the resource works either way,HIGH
|
|
@@ -8,3 +8,4 @@ Public unauthenticated endpoint,public endpoint health status unauthenticated an
|
|
|
8
8
|
Billing / payment / balance mutation,billing payment charge refund invoice subscription balance credit,billing,money movement,Anything that changes an amount owed/paid
|
|
9
9
|
Secrets / key handling,secret api-key signing key env vault kms rotation,secrets,credentials,"Storage, rotation, and logging paths"
|
|
10
10
|
Data export / serializer / log channel,export serializer api-resource log audit csv download report,data-exposure,"PII, internal fields",Response shaping and log sinks
|
|
11
|
+
Infrastructure / IaC resource definition,terraform iac infrastructure provisioning security group ingress egress cidr bucket storage iam role subnet vpc encryption,infrastructure,"network reachability, data at rest, cloud credentials",Any IaC change to networking storage IAM or encryption defaults
|
|
@@ -14,3 +14,8 @@ billing,Race condition on balance / double-spend,CWE-362,Parallel requests redee
|
|
|
14
14
|
billing,Price/amount tampering,CWE-602,Client-supplied price or currency trusted at checkout,Recompute amounts server-side from catalog; signed cart or server-held cart only,Test: tampered client amount → server total unchanged,CRITICAL
|
|
15
15
|
secrets,Secret leakage via logs / errors / VCS,"CWE-532, ATT&CK T1552","Key appears in exception payload, debug log, or committed .env",Centralized secret manager; logger redaction list; pre-commit secret scan; scoped short-lived tokens,Test: forced exception on auth path → no credential substring in log sink,HIGH
|
|
16
16
|
data-exposure,Over-broad serializer / API resource,"CWE-213, OWASP API3","Serializer returns internal fields (cost, flags, foreign tenant refs) the UI ignores but attackers read",Explicit field allowlist per resource; separate admin vs public serializers; contract test on response shape,Test: schema-diff response against documented contract → no undocumented fields,MEDIUM
|
|
17
|
+
infrastructure,Unrestricted ingress to a management or database port,"CWE-1327, ATT&CK T1190",Attacker reaches SSH/RDP or a database engine straight from the internet because a security-group or firewall rule allows 0.0.0.0/0,"Deny-by-default ingress; management and database ports reachable only via bastion, VPN, or private link; CIDR scoped to known ranges; never 0.0.0.0/0 on 22/3389/3306/5432/6379/27017","Test: a rule with cidr_blocks 0.0.0.0/0 on a management or database port is rejected. Decided by: NO CHECK IN THIS REPOSITORY - nothing here parses HCL or cloud config; the deciding check is the consumer's own IaC policy scanner",CRITICAL
|
|
18
|
+
infrastructure,Object storage public by default,"CWE-732, OWASP A05",Bucket or container created without a block-public-access setting so any object URL is world-readable,Block public access at account AND bucket level; explicit policy allowlist instead of a public ACL; serve objects via signed URLs or a CDN origin identity,"Test: storage created with no explicit public-access block is rejected; anonymous GET of a stored object returns 403. Decided by: NO CHECK IN THIS REPOSITORY - the deciding check is the consumer's own IaC policy scanner or a cloud posture tool",HIGH
|
|
19
|
+
infrastructure,Missing encryption at rest or in transit,"CWE-311, CWE-319",Volume snapshot bucket or database instance created with no encryption block; endpoint accepts a plaintext connection,Encryption block mandatory on every storage and database resource; customer-managed key where the data class requires it; TLS-only bucket policy and listener; reject plaintext listeners,"Test: a storage or database resource with no encryption block is rejected; a plaintext connection to the endpoint is refused. Decided by: NO CHECK IN THIS REPOSITORY - the deciding check is the consumer's own IaC policy scanner",HIGH
|
|
20
|
+
infrastructure,Wildcard IAM action or resource,"CWE-269, ATT&CK T1078.004",A role policy grants Action * or Resource * so one compromised workload credential reaches every service and object in the account,Enumerate actions and resource ARNs; one role per workload; permission boundary or service control policy on privileged roles; no wildcard outside a named reviewed exception,"Test: a policy document containing Action ""*"" or Resource ""*"" is rejected or carries a named exception. Decided by: NO CHECK IN THIS REPOSITORY - nothing here parses IAM policy documents; the deciding check is the consumer's own IaC policy scanner",CRITICAL
|
|
21
|
+
infrastructure,Credential in an IaC or vars file instead of a secret manager,"CWE-798, ATT&CK T1552.001",Password token or key written as a literal into a .tf .tfvars or .yaml file and committed,Secret-manager reference or runtime-injected env var; never a literal in IaC or vars files; treat the state file as sensitive and store it encrypted with restricted access,"Test: a literal credential in a tracked IaC or vars file fails the build. Decided by: check_secret_leak (src/scripts/check_secret_leak.ts) inside THIS repository - it has no extension allowlist so .tf and .yaml are in scope; for a CONSUMER the deciding check is the gitleaks workflow the package ships as a template that the consumer must copy",HIGH
|
|
@@ -39,7 +39,7 @@ schema_version: 1
|
|
|
39
39
|
# CI guard: a release bump of `package.json` must update this value
|
|
40
40
|
# in lockstep — see scripts/check_template_pin_drift.ts (road-to-
|
|
41
41
|
# portable-runtime-and-update-check P3.3).
|
|
42
|
-
agent_config_version: "14.
|
|
42
|
+
agent_config_version: "14.17.0"
|
|
43
43
|
|
|
44
44
|
# --- Project identity ---
|
|
45
45
|
project:
|
|
@@ -26,8 +26,8 @@ import {
|
|
|
26
26
|
} from "./chunk-J55WI3RR.js";
|
|
27
27
|
|
|
28
28
|
// src/scripts/_cli/cmd_explain.ts
|
|
29
|
-
import * as
|
|
30
|
-
import * as
|
|
29
|
+
import * as fs6 from "node:fs";
|
|
30
|
+
import * as path5 from "node:path";
|
|
31
31
|
import process2 from "node:process";
|
|
32
32
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
33
33
|
|
|
@@ -303,7 +303,7 @@ function _repr(s) {
|
|
|
303
303
|
}
|
|
304
304
|
|
|
305
305
|
// src/scripts/_cli/explain_last/index.ts
|
|
306
|
-
import * as
|
|
306
|
+
import * as fs5 from "node:fs";
|
|
307
307
|
|
|
308
308
|
// src/scripts/_cli/explain_last/scrubber.ts
|
|
309
309
|
var LONG_STRING_THRESHOLD = 200;
|
|
@@ -817,15 +817,12 @@ function _isOsError(exc) {
|
|
|
817
817
|
}
|
|
818
818
|
|
|
819
819
|
// src/scripts/_cli/explain_last/memory.ts
|
|
820
|
-
import * as fs4 from "node:fs";
|
|
821
|
-
import * as path4 from "node:path";
|
|
822
820
|
var PyFloat = class {
|
|
823
821
|
constructor(value) {
|
|
824
822
|
this.value = value;
|
|
825
823
|
}
|
|
826
824
|
value;
|
|
827
825
|
};
|
|
828
|
-
var MEMORY_SIDECAR = path4.join(".agent-memory", "hits.jsonl");
|
|
829
826
|
function _pyTruthy5(value) {
|
|
830
827
|
if (value === null || value === void 0) {
|
|
831
828
|
return false;
|
|
@@ -908,93 +905,8 @@ function _from_state(state) {
|
|
|
908
905
|
}
|
|
909
906
|
return entries;
|
|
910
907
|
}
|
|
911
|
-
function
|
|
912
|
-
if (s === "") {
|
|
913
|
-
return [];
|
|
914
|
-
}
|
|
915
|
-
const breaks = /* @__PURE__ */ new Set([
|
|
916
|
-
"\n",
|
|
917
|
-
"\r",
|
|
918
|
-
"\v",
|
|
919
|
-
"\f",
|
|
920
|
-
"",
|
|
921
|
-
"",
|
|
922
|
-
"",
|
|
923
|
-
"\x85",
|
|
924
|
-
"\u2028",
|
|
925
|
-
"\u2029"
|
|
926
|
-
]);
|
|
927
|
-
const out = [];
|
|
928
|
-
let cur = "";
|
|
929
|
-
for (let i = 0; i < s.length; i += 1) {
|
|
930
|
-
const ch = s[i];
|
|
931
|
-
if (ch === "\r" && s[i + 1] === "\n") {
|
|
932
|
-
out.push(cur);
|
|
933
|
-
cur = "";
|
|
934
|
-
i += 1;
|
|
935
|
-
continue;
|
|
936
|
-
}
|
|
937
|
-
if (breaks.has(ch)) {
|
|
938
|
-
out.push(cur);
|
|
939
|
-
cur = "";
|
|
940
|
-
continue;
|
|
941
|
-
}
|
|
942
|
-
cur += ch;
|
|
943
|
-
}
|
|
944
|
-
if (cur !== "") {
|
|
945
|
-
out.push(cur);
|
|
946
|
-
}
|
|
947
|
-
return out;
|
|
948
|
-
}
|
|
949
|
-
function _from_sidecar(project_root, run_id) {
|
|
950
|
-
const p = path4.join(project_root, MEMORY_SIDECAR);
|
|
951
|
-
if (!fs4.existsSync(p)) {
|
|
952
|
-
return [];
|
|
953
|
-
}
|
|
954
|
-
const entries = [];
|
|
955
|
-
let content;
|
|
956
|
-
try {
|
|
957
|
-
content = fs4.readFileSync(p, "utf-8");
|
|
958
|
-
} catch {
|
|
959
|
-
return [];
|
|
960
|
-
}
|
|
961
|
-
for (let line of _splitlines(content)) {
|
|
962
|
-
line = line.trim();
|
|
963
|
-
if (!line) {
|
|
964
|
-
continue;
|
|
965
|
-
}
|
|
966
|
-
let raw;
|
|
967
|
-
try {
|
|
968
|
-
raw = JSON.parse(line);
|
|
969
|
-
} catch {
|
|
970
|
-
continue;
|
|
971
|
-
}
|
|
972
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
973
|
-
continue;
|
|
974
|
-
}
|
|
975
|
-
const rawDict = raw;
|
|
976
|
-
if (run_id) {
|
|
977
|
-
const got = rawDict.run_id ?? null;
|
|
978
|
-
if (got !== null && got !== run_id) {
|
|
979
|
-
continue;
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
const entry = _coerce_entry(rawDict);
|
|
983
|
-
if (entry !== null) {
|
|
984
|
-
entries.push(entry);
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
return entries;
|
|
988
|
-
}
|
|
989
|
-
function build5(project_root, state) {
|
|
990
|
-
const input = _pyTruthy5(state.input) ? state.input : {};
|
|
991
|
-
const dataRaw = "data" in input ? input.data : {};
|
|
992
|
-
const data = typeof dataRaw === "object" && dataRaw !== null && !Array.isArray(dataRaw) ? dataRaw : {};
|
|
993
|
-
const run_id = data.id;
|
|
908
|
+
function build5(state) {
|
|
994
909
|
const entries = _from_state(state);
|
|
995
|
-
entries.push(
|
|
996
|
-
..._from_sidecar(project_root, typeof run_id === "string" ? run_id : null)
|
|
997
|
-
);
|
|
998
910
|
return entries.length > 0 ? entries : null;
|
|
999
911
|
}
|
|
1000
912
|
|
|
@@ -1062,17 +974,17 @@ function build6(state) {
|
|
|
1062
974
|
}
|
|
1063
975
|
|
|
1064
976
|
// src/scripts/_cli/explain_last/route.ts
|
|
1065
|
-
import * as
|
|
1066
|
-
import * as
|
|
977
|
+
import * as fs4 from "node:fs";
|
|
978
|
+
import * as path4 from "node:path";
|
|
1067
979
|
var ROUTER_FILENAME = "router.json";
|
|
1068
|
-
var ROUTER_RELATIVE =
|
|
980
|
+
var ROUTER_RELATIVE = path4.join("dist", ROUTER_FILENAME);
|
|
1069
981
|
function _load_router(project_root) {
|
|
1070
|
-
const p =
|
|
1071
|
-
if (!
|
|
982
|
+
const p = path4.join(project_root, ROUTER_RELATIVE);
|
|
983
|
+
if (!fs4.existsSync(p)) {
|
|
1072
984
|
return null;
|
|
1073
985
|
}
|
|
1074
986
|
try {
|
|
1075
|
-
const parsed = JSON.parse(
|
|
987
|
+
const parsed = JSON.parse(fs4.readFileSync(p, "utf-8"));
|
|
1076
988
|
return parsed;
|
|
1077
989
|
} catch {
|
|
1078
990
|
return null;
|
|
@@ -1175,7 +1087,7 @@ function _derive_run_id(state, state_file) {
|
|
|
1175
1087
|
}
|
|
1176
1088
|
let mtime = 0;
|
|
1177
1089
|
try {
|
|
1178
|
-
mtime =
|
|
1090
|
+
mtime = fs5.statSync(state_file).mtimeMs / 1e3;
|
|
1179
1091
|
} catch {
|
|
1180
1092
|
mtime = 0;
|
|
1181
1093
|
}
|
|
@@ -1193,7 +1105,7 @@ function build_trace(project_root, state_file, options = {}) {
|
|
|
1193
1105
|
inputs: build4(project_root),
|
|
1194
1106
|
route: build7(project_root, state),
|
|
1195
1107
|
council: build2(project_root, state_file),
|
|
1196
|
-
memory: build5(
|
|
1108
|
+
memory: build5(state),
|
|
1197
1109
|
pack: build_pack(project_root),
|
|
1198
1110
|
assumptions: build(state),
|
|
1199
1111
|
halt: build3(state),
|
|
@@ -1722,7 +1634,7 @@ ${TIP_FOOTER}`;
|
|
|
1722
1634
|
|
|
1723
1635
|
// src/scripts/_cli/cmd_explain.ts
|
|
1724
1636
|
var ROUTER_FILENAME2 = "router.json";
|
|
1725
|
-
var ROUTER_RELATIVE2 =
|
|
1637
|
+
var ROUTER_RELATIVE2 = path5.join("dist", ROUTER_FILENAME2);
|
|
1726
1638
|
var _HERE = fileURLToPath(import.meta.url);
|
|
1727
1639
|
var ArgparseExit = class extends Error {
|
|
1728
1640
|
constructor(code) {
|
|
@@ -1834,7 +1746,7 @@ function _dumpJson(payload) {
|
|
|
1834
1746
|
}
|
|
1835
1747
|
function _exists2(p) {
|
|
1836
1748
|
try {
|
|
1837
|
-
|
|
1749
|
+
fs6.statSync(p);
|
|
1838
1750
|
return true;
|
|
1839
1751
|
} catch {
|
|
1840
1752
|
return false;
|
|
@@ -1859,19 +1771,19 @@ function _resolve_root(arg) {
|
|
|
1859
1771
|
}
|
|
1860
1772
|
}
|
|
1861
1773
|
function _load_user_settings(project_root) {
|
|
1862
|
-
const p =
|
|
1774
|
+
const p = path5.join(project_root, DEFAULT_PROJECT_FILE);
|
|
1863
1775
|
if (!_exists2(p)) {
|
|
1864
1776
|
return {};
|
|
1865
1777
|
}
|
|
1866
1778
|
return load_agent_settings({ project_path: p }) || {};
|
|
1867
1779
|
}
|
|
1868
1780
|
function _load_router2(project_root) {
|
|
1869
|
-
const p =
|
|
1781
|
+
const p = path5.join(project_root, ROUTER_RELATIVE2);
|
|
1870
1782
|
if (!_exists2(p)) {
|
|
1871
1783
|
return {};
|
|
1872
1784
|
}
|
|
1873
1785
|
try {
|
|
1874
|
-
return JSON.parse(
|
|
1786
|
+
return JSON.parse(fs6.readFileSync(p, { encoding: "utf-8" }));
|
|
1875
1787
|
} catch (exc) {
|
|
1876
1788
|
eprint(`\u274C explain: cannot read ${p}: ${osErrorStr(exc)}`);
|
|
1877
1789
|
throw new SystemExitError(2);
|
|
@@ -2088,7 +2000,7 @@ function _explain_last(project_root, state_file, as_json, quiet) {
|
|
|
2088
2000
|
print("explain last disabled by settings (explain.enable_last)");
|
|
2089
2001
|
return 0;
|
|
2090
2002
|
}
|
|
2091
|
-
const target_state = state_file ||
|
|
2003
|
+
const target_state = state_file || path5.join(project_root, ".work-state.json");
|
|
2092
2004
|
let trace;
|
|
2093
2005
|
try {
|
|
2094
2006
|
trace = build_trace(project_root, target_state);
|
|
@@ -2099,7 +2011,7 @@ function _explain_last(project_root, state_file, as_json, quiet) {
|
|
|
2099
2011
|
if (relativeTo !== null) {
|
|
2100
2012
|
rel = relativeTo;
|
|
2101
2013
|
} else {
|
|
2102
|
-
rel =
|
|
2014
|
+
rel = path5.basename(target_state);
|
|
2103
2015
|
}
|
|
2104
2016
|
const msg = exc.message.split(target_state).join(rel);
|
|
2105
2017
|
eprint(`\u274C explain last: ${msg}`);
|
|
@@ -2115,9 +2027,9 @@ function _explain_last(project_root, state_file, as_json, quiet) {
|
|
|
2115
2027
|
return 0;
|
|
2116
2028
|
}
|
|
2117
2029
|
function relativeToOrNull(p, base) {
|
|
2118
|
-
const rel =
|
|
2030
|
+
const rel = path5.relative(base, p);
|
|
2119
2031
|
if (rel === "") return rel;
|
|
2120
|
-
if (rel.startsWith("..") ||
|
|
2032
|
+
if (rel.startsWith("..") || path5.isAbsolute(rel)) {
|
|
2121
2033
|
return null;
|
|
2122
2034
|
}
|
|
2123
2035
|
return rel;
|
|
@@ -2284,13 +2196,13 @@ function _isCliEntry() {
|
|
|
2284
2196
|
if (process2.argv[1] === void 0) {
|
|
2285
2197
|
return false;
|
|
2286
2198
|
}
|
|
2287
|
-
const argvUrl = pathToFileURL(
|
|
2199
|
+
const argvUrl = pathToFileURL(path5.resolve(process2.argv[1])).href;
|
|
2288
2200
|
if (import.meta.url === argvUrl) {
|
|
2289
2201
|
return true;
|
|
2290
2202
|
}
|
|
2291
2203
|
try {
|
|
2292
|
-
const here =
|
|
2293
|
-
const argv =
|
|
2204
|
+
const here = fs6.realpathSync(fileURLToPath(import.meta.url));
|
|
2205
|
+
const argv = fs6.realpathSync(path5.resolve(process2.argv[1]));
|
|
2294
2206
|
return here === argv;
|
|
2295
2207
|
} catch {
|
|
2296
2208
|
return false;
|