@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.
Files changed (38) 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 +83 -210
  5. package/README.md +1 -1
  6. package/dist/agent-src/commands/analyze/inbox.md +172 -6
  7. package/dist/agent-src/commands/fix/commit-messages.md +336 -0
  8. package/dist/agent-src/commands/fix.md +5 -3
  9. package/dist/agent-src/rules/commit-conventions.md +1 -1
  10. package/dist/agent-src/skills/aws-infrastructure/SKILL.md +41 -0
  11. package/dist/agent-src/skills/conventional-commits-writing/SKILL.md +234 -4
  12. package/dist/agent-src/skills/terraform/SKILL.md +29 -0
  13. package/dist/agent-src/skills/threat-modeling/data/reasoning.csv +1 -0
  14. package/dist/agent-src/skills/threat-modeling/data/surfaces.csv +1 -0
  15. package/dist/agent-src/skills/threat-modeling/data/threats.csv +5 -0
  16. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  17. package/dist/cli-delegate/cmd_explain.js +24 -112
  18. package/dist/discovery/deprecation-report.md +1 -1
  19. package/dist/discovery/discovery-manifest.json +42 -16
  20. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  21. package/dist/discovery/discovery-manifest.summary.md +3 -3
  22. package/dist/discovery/dormancy-report.md +1 -1
  23. package/dist/discovery/orphan-report.md +1 -1
  24. package/dist/discovery/packs.json +6 -5
  25. package/dist/discovery/trust-report.md +2 -2
  26. package/dist/discovery/workspaces.json +5 -4
  27. package/dist/mcp/registry-manifest.json +3 -3
  28. package/dist/mcp/server.json +2 -2
  29. package/package.json +1 -1
  30. package/src/config/web-launch-readiness.json +1 -1
  31. package/src/scripts/_cli/explain_last/index.ts +1 -1
  32. package/src/scripts/_cli/explain_last/memory.ts +8 -104
  33. package/src/scripts/check_artefact_count_messaging.ts +9 -0
  34. package/src/scripts/check_release_highlights.ts +77 -4
  35. package/src/scripts/check_web_launch_readiness.ts +295 -15
  36. package/src/scripts/measure_release_mix.ts +384 -0
  37. package/src/scripts/release_mix_taxonomy.json +172 -0
  38. 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 messages or squash-merge titles — `feat:`, `fix:`, `chore:`, scopes, breaking changes — even when the user just says 'commit this' without naming Conventional Commits."
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. Recommended commit message(s)
114
- 2. Brief rationale for type choice
115
- 3. Split suggestion if the change should be multiple commits
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.16.0"
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 fs7 from "node:fs";
30
- import * as path6 from "node:path";
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 fs6 from "node:fs";
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 _splitlines(s) {
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 fs5 from "node:fs";
1066
- import * as path5 from "node:path";
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 = path5.join("dist", ROUTER_FILENAME);
980
+ var ROUTER_RELATIVE = path4.join("dist", ROUTER_FILENAME);
1069
981
  function _load_router(project_root) {
1070
- const p = path5.join(project_root, ROUTER_RELATIVE);
1071
- if (!fs5.existsSync(p)) {
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(fs5.readFileSync(p, "utf-8"));
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 = fs6.statSync(state_file).mtimeMs / 1e3;
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(project_root, state),
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 = path6.join("dist", ROUTER_FILENAME2);
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
- fs7.statSync(p);
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 = path6.join(project_root, DEFAULT_PROJECT_FILE);
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 = path6.join(project_root, ROUTER_RELATIVE2);
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(fs7.readFileSync(p, { encoding: "utf-8" }));
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 || path6.join(project_root, ".work-state.json");
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 = path6.basename(target_state);
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 = path6.relative(base, p);
2030
+ const rel = path5.relative(base, p);
2119
2031
  if (rel === "") return rel;
2120
- if (rel.startsWith("..") || path6.isAbsolute(rel)) {
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(path6.resolve(process2.argv[1])).href;
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 = fs7.realpathSync(fileURLToPath(import.meta.url));
2293
- const argv = fs7.realpathSync(path6.resolve(process2.argv[1]));
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;
@@ -1,6 +1,6 @@
1
1
  # Discovery — Deprecation Report
2
2
 
3
- - Generated: `2026-09-04T03:43:39Z`
3
+ - Generated: `2026-09-05T05:33:47Z`
4
4
  - Deprecated artefacts: **0**
5
5
 
6
6
  _None. Tree is clean._