mandrel 2.7.0 → 2.9.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/.agents/README.md +104 -330
- package/.agents/agents/auditor.md +135 -0
- package/.agents/agents/plan-critic.md +80 -0
- package/.agents/audit-checklists/dependencies.md +7 -0
- package/.agents/audit-checklists/documentation.md +1 -0
- package/.agents/docs/SDLC.md +69 -162
- package/.agents/docs/configuration.md +159 -499
- package/.agents/docs/quality-gates.md +59 -180
- package/.agents/instructions.md +170 -295
- package/.agents/rules/changelog-style.md +8 -66
- package/.agents/rules/ci-remediation.md +65 -124
- package/.agents/rules/gherkin-standards.md +10 -31
- package/.agents/rules/git-conventions-reference.md +28 -61
- package/.agents/rules/git-conventions.md +1 -1
- package/.agents/rules/orchestration-error-handling.md +5 -15
- package/.agents/rules/security-baseline.md +7 -13
- package/.agents/rules/shell-conventions.md +4 -13
- package/.agents/rules/test-seams.md +2 -2
- package/.agents/rules/testing-standards.md +7 -17
- package/.agents/schemas/agentrc.schema.json +21 -0
- package/.agents/scripts/audit-to-stories.js +51 -0
- package/.agents/scripts/lib/audit-to-stories/dedupe-against-github.js +120 -55
- package/.agents/scripts/lib/config-settings-schema.js +32 -0
- package/.agents/scripts/lib/findings/semantic-issue-search.js +43 -5
- package/.agents/scripts/lib/observability/terse-result.js +114 -0
- package/.agents/scripts/lib/orchestration/complexity-gate.js +207 -0
- package/.agents/scripts/lib/orchestration/plan-context.js +3 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +221 -8
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +55 -14
- package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +9 -3
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +4 -1
- package/.agents/scripts/lib/orchestration/task-body-validator.js +13 -40
- package/.agents/scripts/lib/story-body/body-format-lints.js +215 -0
- package/.agents/scripts/lib/story-body/story-body.js +18 -2
- package/.agents/scripts/lib/templates/decomposer-prompts.js +16 -0
- package/.agents/scripts/providers/github/issues.js +54 -7
- package/.agents/scripts/providers/github/search-budget.js +124 -0
- package/.agents/scripts/providers/github/search-query.js +71 -0
- package/.agents/scripts/single-story-confirm-merge.js +14 -5
- package/.agents/scripts/single-story-init.js +19 -3
- package/.agents/scripts/sync-branch-from-base.js +9 -3
- package/.agents/skills/core/api-and-interface-design/SKILL.md +23 -297
- package/.agents/skills/core/api-and-interface-design/reference.md +76 -0
- package/.agents/skills/core/browser-testing-with-devtools/SKILL.md +20 -327
- package/.agents/skills/core/browser-testing-with-devtools/reference.md +74 -0
- package/.agents/skills/core/code-review-and-quality/reference.md +16 -398
- package/.agents/skills/core/debugging-and-error-recovery/reference.md +14 -281
- package/.agents/skills/core/documentation-and-adrs/reference.md +42 -338
- package/.agents/skills/core/git-workflow-and-versioning/SKILL.md +28 -326
- package/.agents/skills/core/idea-refinement/SKILL.md +67 -193
- package/.agents/skills/core/security-and-hardening/SKILL.md +15 -31
- package/.agents/skills/core/security-and-hardening/reference.md +15 -273
- package/.agents/skills/skills.index.json +5 -5
- package/.agents/skills/stack/qa/gherkin-authoring/SKILL.md +16 -222
- package/.agents/skills/stack/qa/gherkin-authoring/reference.md +157 -0
- package/.agents/skills/stack/qa/playwright/SKILL.md +0 -29
- package/.agents/skills/stack/qa/qa-explore-driving/SKILL.md +19 -23
- package/.agents/skills/stack/qa/qa-harness/SKILL.md +35 -53
- package/.agents/skills/stack/qa/vitest/SKILL.md +0 -29
- package/.agents/workflows/audit-accessibility.md +34 -108
- package/.agents/workflows/audit-architecture.md +35 -123
- package/.agents/workflows/audit-clean-code.md +19 -78
- package/.agents/workflows/audit-data-model.md +32 -100
- package/.agents/workflows/audit-dependencies.md +47 -111
- package/.agents/workflows/audit-devops.md +16 -83
- package/.agents/workflows/audit-documentation.md +46 -93
- package/.agents/workflows/audit-navigability.md +26 -80
- package/.agents/workflows/audit-performance.md +40 -106
- package/.agents/workflows/audit-privacy.md +17 -80
- package/.agents/workflows/audit-quality.md +35 -99
- package/.agents/workflows/audit-security.md +20 -78
- package/.agents/workflows/audit-seo.md +20 -98
- package/.agents/workflows/audit-sre.md +20 -88
- package/.agents/workflows/audit-to-stories.md +1 -8
- package/.agents/workflows/audit-ux-ui.md +17 -80
- package/.agents/workflows/deliver.md +54 -9
- package/.agents/workflows/git-cleanup.md +50 -275
- package/.agents/workflows/helpers/audit-lens-core.md +230 -0
- package/.agents/workflows/helpers/code-review.md +11 -23
- package/.agents/workflows/helpers/deliver-story-reference.md +114 -17
- package/.agents/workflows/helpers/deliver-story.md +36 -186
- package/.agents/workflows/helpers/qa-core.md +174 -0
- package/.agents/workflows/helpers/qa-run-scenario-reference.md +35 -0
- package/.agents/workflows/helpers/qa-run-scenario.md +11 -25
- package/.agents/workflows/helpers/worktree-lifecycle.md +6 -67
- package/.agents/workflows/mandrel-update.md +7 -13
- package/.agents/workflows/plan.md +44 -17
- package/.agents/workflows/qa-assist.md +140 -269
- package/.agents/workflows/qa-explore.md +125 -316
- package/.agents/workflows/qa-run.md +180 -380
- package/docs/CHANGELOG.md +30 -0
- package/package.json +1 -1
- package/.agents/workflows/helpers/audit-dual-path.md +0 -59
- package/.agents/workflows/helpers/audit-self-check.md +0 -70
- package/.agents/workflows/helpers/audit-severity-scale.md +0 -19
|
@@ -5,299 +5,74 @@ description: >-
|
|
|
5
5
|
`git stash` entries — each step gated by operator confirmation.
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
#
|
|
8
|
+
# /git-cleanup [--fast-forward-main] [--prune-remotes] [--branches] [--stashes] [--execute] [--remote] [--yes] [--drop-stashes <ref>] [--exclude <pattern>] [--json]
|
|
9
9
|
|
|
10
|
-
`/git-cleanup` folds the four cleanup steps
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
`/git-cleanup` folds the four cleanup steps operators routinely run by hand
|
|
11
|
+
after a busy session into a single pipeline with per-step confirmation. It is a
|
|
12
|
+
**recovery tool**, not a routine chore: the delivering flows already reap their
|
|
13
|
+
own merged refs and fast-forward the base branch (see
|
|
14
|
+
[`rules/git-conventions.md` § Local checkout hygiene](../rules/git-conventions.md)).
|
|
15
|
+
Reach for it when the automated hygiene left an unusual state behind.
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
2. **prune stale remote-tracking refs** — `git fetch --prune origin`
|
|
17
|
-
to drop `refs/remotes/origin/*` entries that GitHub already deleted.
|
|
18
|
-
3. **reap merged local branches** — the existing squash-aware
|
|
19
|
-
`gh pr list --state merged` + `git branch --merged <base>` sweep,
|
|
20
|
-
with attached worktrees removed first. Optionally also deletes the
|
|
21
|
-
`origin/<branch>` ref when `--remote` is passed. Every default run
|
|
22
|
-
also **enumerates** `refs/remotes/origin/*` and reports any
|
|
23
|
-
**remote-only** merged branches — branches whose local ref is
|
|
24
|
-
already gone (or never existed) but whose `origin/<branch>` still
|
|
25
|
-
points at a merged PR — even without `--remote`; `--remote` is still
|
|
26
|
-
required to *delete* them. A third branch, whose content already
|
|
27
|
-
landed in `<base>` by another route (a squash-merged PR, a
|
|
28
|
-
cherry-pick, a manual `merge --squash`), is caught by a
|
|
29
|
-
**content-equivalence probe** (`git merge-tree --write-tree`,
|
|
30
|
-
git ≥ 2.38) even when it has no merged PR of its own and is not a
|
|
31
|
-
git ancestor of `<base>`.
|
|
32
|
-
4. **triage `git stash` entries** — list every stash and prompt for
|
|
33
|
-
`drop / keep / quit` per entry (or pass `--drop-stashes <ref>` for
|
|
34
|
-
non-interactive use).
|
|
35
|
-
|
|
36
|
-
The enumeration + reap logic lives in
|
|
37
|
-
[`git-cleanup.js`](../scripts/git-cleanup.js). When no phase flag is
|
|
38
|
-
passed, **all four phases run** sequentially. Pass any of
|
|
39
|
-
`--fast-forward-main`, `--prune-remotes`, `--branches`, `--stashes` to
|
|
40
|
-
narrow the run.
|
|
41
|
-
|
|
42
|
-
> **When to run**: After a session that landed several PRs, or before
|
|
43
|
-
> starting a new Story, to put the local checkout into a known
|
|
44
|
-
> tidy state.
|
|
17
|
+
> **When to run**: after a session that landed several PRs, or before starting a
|
|
18
|
+
> new Story, to put the local checkout into a known tidy state.
|
|
45
19
|
>
|
|
46
20
|
> **Persona**: `devops-engineer` · **Skills**:
|
|
47
21
|
> `core/git-workflow-and-versioning`
|
|
48
22
|
|
|
49
|
-
|
|
23
|
+
The enumeration + reap logic lives in
|
|
24
|
+
[`git-cleanup.js`](../scripts/git-cleanup.js) — it computes the candidate list,
|
|
25
|
+
the skip taxonomy, the detection signals, and the JSON envelope (add `--json`),
|
|
26
|
+
and prints them itself. Without `--execute` the script is a **dry-run preview**; nothing is
|
|
27
|
+
mutated. When no phase flag is passed, **all four phases run** sequentially; pass
|
|
28
|
+
any of `--fast-forward-main`, `--prune-remotes`, `--branches`, `--stashes` to
|
|
29
|
+
narrow the run. A failure in one phase does not short-circuit the others — each
|
|
30
|
+
runs and reports independently.
|
|
31
|
+
|
|
32
|
+
## Phases
|
|
33
|
+
|
|
34
|
+
| Phase | What it does | Safety contract |
|
|
35
|
+
| --- | --- | --- |
|
|
36
|
+
| **fast-forward-main** | `git fetch origin <base>` then `git merge --ff-only origin/<base>`. | Skipped silently on a dirty tree or a non-fast-forward; otherwise prompts `Fast-forward main by N commit(s)?`. Checks out `<base>` first when HEAD is elsewhere and does **not** restore the prior branch. |
|
|
37
|
+
| **prune-remotes** | `git fetch --prune origin` to drop `refs/remotes/origin/*` GitHub already deleted. | Prompts before pruning. Runs as its own phase regardless of `--remote`. |
|
|
38
|
+
| **branches** | Reaps merged local branches (squash-aware: merged-PR, git-ancestry, and content-equivalence signals), removing an attached worktree first. Also enumerates **remote-only** merged branches. | Prints the candidate list, then prompts `Reap N merged branch(es)?`. `--remote` is required **on top of** `--execute` to delete any `origin/<branch>`. `content-merged` candidates carry a weaker-signal warning. |
|
|
39
|
+
| **stashes** | Lists every stash and triages it. | Interactive: `drop / keep / quit` per entry (default `keep`). Under `--yes` / `--json`, drops require an explicit `--drop-stashes <ref>` allowlist (repeatable) — there is no "drop all". |
|
|
50
40
|
|
|
51
|
-
|
|
52
|
-
> `main`, delete local branches, delete remote refs (with `--remote`),
|
|
53
|
-
> and drop stashes. Without `--execute` the script is a dry-run preview.
|
|
41
|
+
## Constraint
|
|
54
42
|
|
|
55
|
-
|
|
43
|
+
> [!WARNING] `--execute` mutates state: it can fast-forward `main`, delete local
|
|
44
|
+
> branches, delete remote refs (with `--remote`), and drop stashes. Without
|
|
45
|
+
> `--execute` the script only previews.
|
|
56
46
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
47
|
+
- **`--execute`** — the master gate. Omit it for a preview of all four phases.
|
|
48
|
+
- **`--remote`** — extends the branches phase to delete the matching
|
|
49
|
+
`origin/<branch>` ref (and to delete remote-only merged branches). Cannot be
|
|
50
|
+
undone without re-pushing.
|
|
51
|
+
- **`--yes`** — bypass every per-step prompt (CI / non-interactive). Under it,
|
|
52
|
+
stash drops still require `--drop-stashes <ref>`.
|
|
53
|
+
- **`--exclude '<pattern>'`** — carve a branch out of the reap. This is the only
|
|
54
|
+
way to protect an in-scope merged-PR branch you want to keep.
|
|
60
55
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
list. The stashes phase lists every stash with its created-at and
|
|
65
|
-
message.
|
|
56
|
+
Do **not** run with `--execute` if there is unmerged work that needs saving. The
|
|
57
|
+
fast-forward phase skips on a dirty tree (safe), but the branches phase reaps any
|
|
58
|
+
merged-PR branch in scope unless `--exclude`d.
|
|
66
59
|
|
|
67
|
-
|
|
68
|
-
candidates, … }` envelope.
|
|
60
|
+
## Examples
|
|
69
61
|
|
|
70
|
-
|
|
62
|
+
```bash
|
|
63
|
+
# Preview all four phases (no mutation).
|
|
64
|
+
node .agents/scripts/git-cleanup.js
|
|
71
65
|
|
|
72
|
-
|
|
73
|
-
|
|
66
|
+
# Run everything non-interactively, including origin refs.
|
|
67
|
+
node .agents/scripts/git-cleanup.js --execute --remote --yes
|
|
74
68
|
|
|
75
|
-
|
|
76
|
-
# Only fast-forward main (skip everything else)
|
|
69
|
+
# Only fast-forward main.
|
|
77
70
|
node .agents/scripts/git-cleanup.js --fast-forward-main --execute
|
|
78
71
|
|
|
79
|
-
# Only sweep merged branches + their origin refs
|
|
72
|
+
# Only sweep merged branches + their origin refs.
|
|
80
73
|
node .agents/scripts/git-cleanup.js --branches --execute --remote
|
|
81
74
|
|
|
82
|
-
#
|
|
83
|
-
node .agents/scripts/git-cleanup.js --prune-remotes --execute
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
## Step 3 — Run all four phases
|
|
87
|
-
|
|
88
|
-
The default. Confirms each destructive step independently:
|
|
89
|
-
|
|
90
|
-
```powershell
|
|
91
|
-
node .agents/scripts/git-cleanup.js --execute
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
The script prompts before each mutation:
|
|
95
|
-
|
|
96
|
-
- **fast-forward-main**: skipped silently when the tree is dirty or the
|
|
97
|
-
FF would not be a fast-forward; otherwise prompts
|
|
98
|
-
`Fast-forward main by N commit(s)? [y/N]`.
|
|
99
|
-
- **prune-remotes**: prompts before running
|
|
100
|
-
`git fetch --prune origin`.
|
|
101
|
-
- **branches**: prints the dry-run candidate list, then prompts
|
|
102
|
-
`Reap N merged branch(es)? [y/N]`.
|
|
103
|
-
- **stashes**: prompts per-stash with `drop / keep / quit`. A `quit`
|
|
104
|
-
reply stops the per-stash loop without dropping anything further.
|
|
105
|
-
|
|
106
|
-
Add `--remote` to also reap `origin/<branch>` during the branches phase.
|
|
107
|
-
|
|
108
|
-
## Step 4 — Non-interactive / CI
|
|
109
|
-
|
|
110
|
-
Pass `--yes` to bypass every per-step prompt:
|
|
111
|
-
|
|
112
|
-
```powershell
|
|
113
|
-
node .agents/scripts/git-cleanup.js --execute --remote --yes
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
For the stash phase under `--yes` or `--json`, drops require an explicit
|
|
117
|
-
allowlist via `--drop-stashes <ref>` (repeatable). Without that flag,
|
|
118
|
-
stashes are listed but never dropped:
|
|
119
|
-
|
|
120
|
-
```powershell
|
|
75
|
+
# Drop specific stashes under --yes.
|
|
121
76
|
node .agents/scripts/git-cleanup.js --stashes --execute --yes \
|
|
122
77
|
--drop-stashes 'stash@{0}' --drop-stashes 'stash@{2}'
|
|
123
78
|
```
|
|
124
|
-
|
|
125
|
-
This keeps the JSON / CI invocation contract explicit — the operator
|
|
126
|
-
names exactly which stashes to drop, by ref.
|
|
127
|
-
|
|
128
|
-
## Step 5 — JSON envelope
|
|
129
|
-
|
|
130
|
-
`--json` emits a single line of structured output suitable for
|
|
131
|
-
programmatic consumption:
|
|
132
|
-
|
|
133
|
-
```json
|
|
134
|
-
{
|
|
135
|
-
"dryRun": false,
|
|
136
|
-
"baseBranch": "main",
|
|
137
|
-
"fastForward": {
|
|
138
|
-
"ok": true,
|
|
139
|
-
"applied": true,
|
|
140
|
-
"skipped": false,
|
|
141
|
-
"behind": 2
|
|
142
|
-
},
|
|
143
|
-
"prune": {
|
|
144
|
-
"ok": true,
|
|
145
|
-
"attempted": true,
|
|
146
|
-
"remote": "origin",
|
|
147
|
-
"pruned": ["fix/old"]
|
|
148
|
-
},
|
|
149
|
-
"candidates": [
|
|
150
|
-
{
|
|
151
|
-
"branch": "fix/foo",
|
|
152
|
-
"prNumber": 1471,
|
|
153
|
-
"mergedAt": "2026-05-09T12:00:00Z",
|
|
154
|
-
"hasWorktree": true,
|
|
155
|
-
"worktreePath": "C:/repo/.worktrees/fix-foo",
|
|
156
|
-
"detectedBy": "gh"
|
|
157
|
-
}
|
|
158
|
-
],
|
|
159
|
-
"skipped": [
|
|
160
|
-
{ "branch": "story-4200", "reason": "not-merged", "lastCommitAt": "2026-05-01T00:00:00Z" }
|
|
161
|
-
],
|
|
162
|
-
"ghDegraded": false,
|
|
163
|
-
"worktrees": [{ "path": "C:/repo/.worktrees/fix-foo", "ok": true, "dirty": false }],
|
|
164
|
-
"local": [{ "branch": "fix/foo", "ok": true, "alreadyGone": false }],
|
|
165
|
-
"remote": [{ "branch": "fix/foo", "ok": true, "alreadyGone": true }],
|
|
166
|
-
"stashes": {
|
|
167
|
-
"ok": true,
|
|
168
|
-
"actions": [
|
|
169
|
-
{ "ref": "stash@{0}", "action": "drop", "dropped": true }
|
|
170
|
-
],
|
|
171
|
-
"failures": []
|
|
172
|
-
},
|
|
173
|
-
"failures": [],
|
|
174
|
-
"ok": true
|
|
175
|
-
}
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
## Exit codes
|
|
179
|
-
|
|
180
|
-
- `0` — clean: dry-run preview, or at least one phase produced work and
|
|
181
|
-
no phase failed.
|
|
182
|
-
- `1` — at least one phase reported a failure (fast-forward, prune,
|
|
183
|
-
branch reap, or stash drop). A failure in one phase **does not**
|
|
184
|
-
short-circuit later phases — each runs and reports independently.
|
|
185
|
-
- `2` — every active phase produced nothing to do (informational; the
|
|
186
|
-
repo is already tidy).
|
|
187
|
-
|
|
188
|
-
## Phase-specific behaviour
|
|
189
|
-
|
|
190
|
-
### fast-forward-main
|
|
191
|
-
|
|
192
|
-
- Skip reasons surfaced in `fastForward.reason`:
|
|
193
|
-
- `dirty-tree` — `git status --porcelain` returned non-empty.
|
|
194
|
-
- `not-fast-forward` — local `<base>` has commits the remote does
|
|
195
|
-
not (`git rev-list --left-right --count` shows local-ahead > 0).
|
|
196
|
-
- `already-up-to-date` — local and remote `<base>` point at the same
|
|
197
|
-
commit.
|
|
198
|
-
- `fetch-failed` / `merge-failed` — surface git's stderr verbatim.
|
|
199
|
-
- When the current HEAD is not `<base>`, the script runs
|
|
200
|
-
`git checkout <base>` before merging. The original branch is **not**
|
|
201
|
-
restored at the end — operators on a feature branch should run
|
|
202
|
-
`git checkout -` after.
|
|
203
|
-
|
|
204
|
-
### prune-remotes
|
|
205
|
-
|
|
206
|
-
Runs as its own phase regardless of whether `--remote` was passed
|
|
207
|
-
during the branches phase. The branches phase still runs its own
|
|
208
|
-
follow-up prune when `--remote` is set, so passing both is idempotent
|
|
209
|
-
(the second prune just reports `pruned: []`).
|
|
210
|
-
|
|
211
|
-
### branches
|
|
212
|
-
|
|
213
|
-
The merged-branch sweep recognizes three detection signals, in order:
|
|
214
|
-
|
|
215
|
-
1. **`detectedBy: 'gh'`** — the branch has a merged PR
|
|
216
|
-
(`gh pr list --head <branch> --state all`, classified by the
|
|
217
|
-
**latest** PR's state).
|
|
218
|
-
2. **`detectedBy: 'git-merged'`** — the branch is a git ancestor of
|
|
219
|
-
`<base>` (`git branch --merged <base>`), or of `origin/<base>` when
|
|
220
|
-
that remote-tracking ref exists (unioned so a stale local `<base>` —
|
|
221
|
-
fast-forward phase skipped, or `--branches` run alone — no longer
|
|
222
|
-
hides a branch already merged on the remote).
|
|
223
|
-
3. **`detectedBy: 'content-merged'`** (Story #4395) — the branch has no
|
|
224
|
-
reapable PR verdict and is not an ancestor of `<base>` under either
|
|
225
|
-
anchor, but simulating the merge via
|
|
226
|
-
`git merge-tree --write-tree <base> <branch>` (git ≥ 2.38) produces a
|
|
227
|
-
tree identical to `<base>`'s own tree — i.e. applying the branch's
|
|
228
|
-
changes on top of `<base>` is a content no-op. This catches
|
|
229
|
-
`story-<id>` branches whose PR **squash-merged** to `main` (the story
|
|
230
|
-
commits are not ancestors of `main`), and any other branch whose content
|
|
231
|
-
landed via a different route (a renamed
|
|
232
|
-
head, a cherry-pick, a manual `merge --squash`). When git rejects
|
|
233
|
-
`--write-tree` (git < 2.38) or the simulated merge conflicts, the
|
|
234
|
-
probe is inconclusive and the branch keeps its existing `not-merged`
|
|
235
|
-
skip — the signal never guesses. `content-merged` candidates render
|
|
236
|
-
with a "weaker signal — verify before deleting" annotation in the
|
|
237
|
-
dry-run list and are called out separately in the confirmation
|
|
238
|
-
prompt, since — unlike a merged PR or git ancestry — no CI or GitHub
|
|
239
|
-
merge check ever validated this branch's exact diff.
|
|
240
|
-
|
|
241
|
-
Other candidate semantics:
|
|
242
|
-
|
|
243
|
-
- A branch is a candidate iff it is not `<base>`, not the current HEAD,
|
|
244
|
-
not in `git config branch.protectedBranches`, and matches one of the
|
|
245
|
-
three signals above.
|
|
246
|
-
- When a candidate has an attached worktree, the worktree is removed
|
|
247
|
-
(force if dirty) **before** `git branch -D`, mirroring the pattern in
|
|
248
|
-
[`worktree-lifecycle.md`](helpers/worktree-lifecycle.md).
|
|
249
|
-
- `--remote` is required on top of `--execute` to touch `origin/`.
|
|
250
|
-
- A throwing `gh` runner (auth failure, rate limit, missing binary) no
|
|
251
|
-
longer aborts the run: the branches phase logs one warning and
|
|
252
|
-
continues with the git-only signals (ancestry + content-equivalence).
|
|
253
|
-
The JSON envelope's `ghDegraded: true` records that this happened for
|
|
254
|
-
the run, and the dry-run text carries a matching warning line.
|
|
255
|
-
|
|
256
|
-
The skip taxonomy:
|
|
257
|
-
|
|
258
|
-
- `reason: 'protected'` — the base branch or a name in
|
|
259
|
-
`git config branch.protectedBranches`. Not reapable; ignore.
|
|
260
|
-
- `reason: 'current-head'` — the current branch. Reapable after
|
|
261
|
-
`git checkout <base>`. The dry-run output surfaces a remediation
|
|
262
|
-
hint so the operator sees the recovery path without having to look
|
|
263
|
-
in the JSON envelope.
|
|
264
|
-
- `reason: 'tip-diverged-from-merge'` — the latest PR merged, but the
|
|
265
|
-
branch's tip has since moved past the merged commit (a post-merge
|
|
266
|
-
force-push). The dry-run line names both SHAs and a remediation hint
|
|
267
|
-
(delete manually via `git branch -D <branch>`, or push the follow-up
|
|
268
|
-
commit).
|
|
269
|
-
- `reason: 'not-merged'` — none of the three detection signals matched.
|
|
270
|
-
Previously silent; the dry-run output now lists every surviving
|
|
271
|
-
`not-merged` branch as a one-line-per-branch summary with its
|
|
272
|
-
last-commit age, so the operator can see why a leftover branch isn't
|
|
273
|
-
reaped instead of hunting for it by hand.
|
|
274
|
-
|
|
275
|
-
Every default run also opts the planner into a **remote-only
|
|
276
|
-
enumeration pass**: in addition to walking `refs/heads/*`, the planner
|
|
277
|
-
also walks `refs/remotes/origin/*` and emits candidates for any branch
|
|
278
|
-
that exists on `origin` with a merged PR but has no local ref. These
|
|
279
|
-
candidates carry `detectedBy: 'remote-only'` and `localExists: false`
|
|
280
|
-
and are always shown in the dry-run list; **deleting** them (via the
|
|
281
|
-
`git push --delete origin/<branch>` path — no local `git branch -D` is
|
|
282
|
-
attempted, since there is no local branch) still requires `--remote` on
|
|
283
|
-
top of `--execute`, unchanged.
|
|
284
|
-
|
|
285
|
-
### stashes
|
|
286
|
-
|
|
287
|
-
- Stashes are dropped high-index-first so the indices of remaining
|
|
288
|
-
stashes stay stable across consecutive drops (git renumbers from the
|
|
289
|
-
top of the stack).
|
|
290
|
-
- In interactive mode (no `--yes`, no `--json`), each stash gets a
|
|
291
|
-
prompt: `drop / keep / quit`. The default on bare ENTER is `keep`.
|
|
292
|
-
- In `--yes` or `--json` mode, only refs explicitly named via
|
|
293
|
-
`--drop-stashes <ref>` are dropped. Everything else is kept. There is
|
|
294
|
-
intentionally no "drop all" shorthand.
|
|
295
|
-
|
|
296
|
-
## Constraint
|
|
297
|
-
|
|
298
|
-
Do **not** run with `--execute` if there is unmerged work that needs
|
|
299
|
-
saving. The fast-forward phase skips on a dirty tree (safe), but the
|
|
300
|
-
branches phase will reap any merged-PR branch in scope — passing
|
|
301
|
-
`--exclude '<pattern>'` is the only way to carve out exceptions. The
|
|
302
|
-
remote reap (`--remote`) crosses `origin/` and cannot be undone without
|
|
303
|
-
re-pushing.
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# Audit lens core (shared contract)
|
|
2
|
+
|
|
3
|
+
> **Single source of truth for every audit lens's shared machinery
|
|
4
|
+
> (Story #4665).** Each `audit-<lens>.md` workflow references this file instead
|
|
5
|
+
> of re-stating the blocks below. A lens carries only its own frontmatter, a
|
|
6
|
+
> short preamble, its `{{changedFiles}}` fence, and its genuinely lens-specific
|
|
7
|
+
> dimensions / detection batteries / applicability gates / boundary
|
|
8
|
+
> demarcations. Everything a lens shares with every other lens — the read-only
|
|
9
|
+
> constraint, scope interpretation, the report envelope and finding-block
|
|
10
|
+
> skeleton, the severity scale, the self-cross-check, and the execution
|
|
11
|
+
> strategy — lives here.
|
|
12
|
+
>
|
|
13
|
+
> This file absorbs the three retired helpers `audit-severity-scale.md`,
|
|
14
|
+
> `audit-self-check.md`, and `audit-dual-path.md`.
|
|
15
|
+
|
|
16
|
+
## Read-only constraint {#read-only}
|
|
17
|
+
|
|
18
|
+
Every audit lens is a **read-only** analysis. Do **not** modify application
|
|
19
|
+
code, styles, configuration, dependencies, branches, or labels. Surfacing the
|
|
20
|
+
findings is the deliverable; fixing them is a separate pass. The **only** write
|
|
21
|
+
a lens performs is its report artifact (plus, where a lens explicitly declares
|
|
22
|
+
it, a single measurement/baseline artifact named in that lens's own
|
|
23
|
+
Constraint). A lens that names a stricter or looser read-only carve-out in its
|
|
24
|
+
own body (e.g. performance's non-mutating measurements, quality's permitted
|
|
25
|
+
committed-baseline reads, data-model's no-database rule) refines this shared
|
|
26
|
+
constraint for that lens only.
|
|
27
|
+
|
|
28
|
+
## Scope interpretation (Story / plan-run mode) {#scope-interpretation}
|
|
29
|
+
|
|
30
|
+
Each lens carries its own `{{changedFiles}}` fence — the substitution anchor
|
|
31
|
+
consumed by `.agents/scripts/lib/audit-suite/` — and reads it as follows:
|
|
32
|
+
|
|
33
|
+
- When this lens is invoked from `/deliver` close lenses (or a plan-run audit),
|
|
34
|
+
the fence is populated with the Story (or plan-run) change-set file list.
|
|
35
|
+
**Restrict analysis to those files** (and their direct dependencies when the
|
|
36
|
+
lens explicitly calls for cross-file reasoning).
|
|
37
|
+
- Otherwise — for any manual `/audit-<lens>` invocation — the fence renders the
|
|
38
|
+
literal string `{{changedFiles}}`. Treat that as **no scope filter — run the
|
|
39
|
+
lens codebase-wide** exactly as if the block were absent.
|
|
40
|
+
|
|
41
|
+
A handful of lenses deliberately deviate (documentation intersects the fence
|
|
42
|
+
with its config-driven target set; navigability always evaluates the whole
|
|
43
|
+
route tree regardless of the fence). Those lenses state their deviation in
|
|
44
|
+
their own Scope section; every other lens follows the rule above verbatim.
|
|
45
|
+
|
|
46
|
+
## Report envelope & finding-block skeleton {#report-envelope}
|
|
47
|
+
|
|
48
|
+
Each lens writes exactly one structured Markdown report to
|
|
49
|
+
`{{auditOutputDir}}/audit-<lens>-results.md` (the lens preamble names its own
|
|
50
|
+
path). The report MUST include every section its lens template mandates — write
|
|
51
|
+
`_No findings._` rather than omitting a section — and always an
|
|
52
|
+
`## Executive Summary` and a `## Detailed Findings` section. The Executive
|
|
53
|
+
Summary carries the self-cross-check `kept <k> / dropped <d>` line
|
|
54
|
+
([below](#self-cross-check)).
|
|
55
|
+
|
|
56
|
+
Every finding under `## Detailed Findings` uses the shared 7-field skeleton
|
|
57
|
+
below. A lens may **add** fields (e.g. a WCAG success criterion, a CWE ID, a
|
|
58
|
+
`Baseline MUST`, a `Route / Door` + `Persona(s)` pair, an `Evidence` tag) and
|
|
59
|
+
may **relabel** the two normalized axes — `Severity` ↔ `Impact`, `Dimension` ↔
|
|
60
|
+
`Category` ↔ `Type` — as its preamble declares; the parser
|
|
61
|
+
(`lib/audit-to-stories/parse-audit-md.js`) recognizes every variant. It never
|
|
62
|
+
drops or renames a shared field.
|
|
63
|
+
|
|
64
|
+
```markdown
|
|
65
|
+
## Executive Summary
|
|
66
|
+
|
|
67
|
+
[The lens's headline read plus the self-cross-check `kept <k> / dropped <d>`
|
|
68
|
+
line. A lens may mandate additional report sections between here and the
|
|
69
|
+
findings — its own body names them.]
|
|
70
|
+
|
|
71
|
+
## Detailed Findings
|
|
72
|
+
|
|
73
|
+
[For every finding, use the following strict structure. Lead each title with
|
|
74
|
+
the primary file the finding lives in:]
|
|
75
|
+
|
|
76
|
+
### `path/to/primary-file.ext` — [Short title of the issue]
|
|
77
|
+
|
|
78
|
+
- **Dimension:** [the lens-specific dimension — see the lens's own list]
|
|
79
|
+
- **Severity:** [Critical | High | Medium | Low]
|
|
80
|
+
- **Location:** `path/to/primary-file.ext:line`
|
|
81
|
+
- **Current State:** [the specific file/line/module and why it is problematic]
|
|
82
|
+
- **Recommendation & Rationale:** [how to remediate and why it matters]
|
|
83
|
+
- **Acceptance signal:** [the command or observable that proves this finding is
|
|
84
|
+
remediated — e.g. a grep that now returns empty, an added regression test, or
|
|
85
|
+
a re-run of this lens]
|
|
86
|
+
- **Agent Prompt:**
|
|
87
|
+
`[A copy-pasteable, highly specific prompt to execute this remediation independently]`
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Severity scale {#severity-scale}
|
|
91
|
+
|
|
92
|
+
Every finding grades its severity (labelled `Severity` or `Impact`) on this
|
|
93
|
+
ordered scale. `parse-audit-md.js` recognizes every level, and a surviving
|
|
94
|
+
**Critical** finding halts the delivery gate
|
|
95
|
+
(`lib/audit-suite/findings.js#hasSurvivingCritical`).
|
|
96
|
+
|
|
97
|
+
- **Critical** — an active, exploitable, or data-losing defect that must be
|
|
98
|
+
fixed before the change can ship (e.g. a leaked secret, an auth bypass, a
|
|
99
|
+
guaranteed production outage or data-loss path).
|
|
100
|
+
- **High** — a serious correctness, security, or maintainability risk that
|
|
101
|
+
should be fixed promptly, but does not by itself block the release.
|
|
102
|
+
- **Medium** — a real problem worth scheduling; contained blast radius, or a
|
|
103
|
+
reasonable workaround exists.
|
|
104
|
+
- **Low** — minor or cosmetic; fix opportunistically.
|
|
105
|
+
|
|
106
|
+
## Self-cross-check (mandatory — filter false positives before you finalize) {#self-cross-check}
|
|
107
|
+
|
|
108
|
+
You are your own adversarial reviewer. After you have drafted the Detailed
|
|
109
|
+
Findings but **before** you write the report artifact, re-open every finding
|
|
110
|
+
and hold it to the bar below. This pass is **read-only** — it filters and
|
|
111
|
+
tightens the findings you already have; it never invents new ones. It gives the
|
|
112
|
+
sequential single-pass path the same false-positive filter the orchestrated
|
|
113
|
+
path's independent adversarial reviewer applies.
|
|
114
|
+
|
|
115
|
+
### Per-finding evidence bar (keep or drop)
|
|
116
|
+
|
|
117
|
+
Keep a finding only when **all** of the following hold. Drop it otherwise.
|
|
118
|
+
|
|
119
|
+
- **Grounded location** — it names a concrete `path:line` (or a concrete
|
|
120
|
+
symbol / config key) that you have actually read, not a hypothetical or a
|
|
121
|
+
"somewhere in the codebase" claim.
|
|
122
|
+
- **Reproducible evidence** — the problem is backed by an observable: a tool
|
|
123
|
+
reading (a baseline row, a complexity/MI/duplication number, a failing
|
|
124
|
+
command), a quoted code snippet, or a specific standard it violates. A
|
|
125
|
+
finding whose entire basis is "this looks wrong" does not clear the bar.
|
|
126
|
+
- **In-scope** — when a change-set scope filter was supplied (the fence
|
|
127
|
+
resolved to a file list), the finding lives in that scope or a direct
|
|
128
|
+
dependency the lens explicitly reasons across. A finding outside the scope
|
|
129
|
+
filter is dropped, not reported.
|
|
130
|
+
- **Actionable** — the recommendation is specific enough to execute. Drop
|
|
131
|
+
vague exhortations ("improve error handling generally") that carry no
|
|
132
|
+
concrete change.
|
|
133
|
+
|
|
134
|
+
### Exclusion list (never a finding)
|
|
135
|
+
|
|
136
|
+
Treat the following as **out of scope by construction** and drop any finding
|
|
137
|
+
that rests on one of them:
|
|
138
|
+
|
|
139
|
+
- **Sanctioned test seams** — exports consumed only by tests, and other
|
|
140
|
+
patterns the `test-seams` rule blesses. Never a production defect.
|
|
141
|
+
- **Entry points & public API surface** — CLI mains, `bin/` scripts, declared
|
|
142
|
+
`exports` / `bin` / `main`, and barrel contracts consumed out-of-tree. A
|
|
143
|
+
zero in-repo consumer count is not death.
|
|
144
|
+
- **Dynamic / framework reachability** — symbols reached via `import()`,
|
|
145
|
+
string-keyed dispatch, decorators, lifecycle listeners, or convention-loaded
|
|
146
|
+
plugin directories. Invisible to static analysis, not dead.
|
|
147
|
+
- **Intentional, documented deviations** — a pattern an in-tree comment, ADR,
|
|
148
|
+
or config explicitly sanctions. Cite it and drop the finding.
|
|
149
|
+
- **Style-only nits already enforced by a formatter/linter** — do not
|
|
150
|
+
re-litigate what the committed tooling already governs.
|
|
151
|
+
|
|
152
|
+
### Final re-open-and-drop pass (mandatory)
|
|
153
|
+
|
|
154
|
+
1. Walk your Detailed Findings once more, applying the bar and the exclusion
|
|
155
|
+
list above. Remove every finding that fails.
|
|
156
|
+
2. Count what you kept (`k`) and what you dropped (`d`).
|
|
157
|
+
3. Record the outcome in the report's **Executive Summary** as a single line:
|
|
158
|
+
|
|
159
|
+
```text
|
|
160
|
+
Self-cross-check: kept <k> / dropped <d>.
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
When `d > 0`, name the dropped findings (title + the bar/exclusion reason)
|
|
164
|
+
in one short list under that line, so the filtering is auditable and never
|
|
165
|
+
silent.
|
|
166
|
+
|
|
167
|
+
A lens that keeps every finding still records `dropped 0` — the line's absence
|
|
168
|
+
is itself a defect (it means the pass did not run).
|
|
169
|
+
|
|
170
|
+
## Execution strategy {#execution-strategy}
|
|
171
|
+
|
|
172
|
+
A lens is a self-contained, read-only unit of work — exactly the shape a
|
|
173
|
+
role-scoped subagent is for. Run it along the first path below that is
|
|
174
|
+
available; every path emits the **identical** report contract (the finding-block
|
|
175
|
+
skeleton above), so downstream consumers (`audit-to-stories`) are agnostic to
|
|
176
|
+
which path produced it.
|
|
177
|
+
|
|
178
|
+
1. **Subagent dispatch (first-class).** Dispatch the lens as a single
|
|
179
|
+
`subagent_type: auditor` call — the standalone boot context in
|
|
180
|
+
[`../../agents/auditor.md`](../../agents/auditor.md) carries the read-only
|
|
181
|
+
MUSTs, the finding-block skeleton, the severity scale, and the
|
|
182
|
+
self-cross-check bar, so the child needs only the lens's own dimensions to
|
|
183
|
+
run. The subagent returns the **report path plus the Executive Summary**
|
|
184
|
+
(including the self-cross-check line); the parent never needs the full
|
|
185
|
+
findings inline. This is the default: the auditor boots without the full
|
|
186
|
+
project closure, so the spawn is cheap relative to running the lens inline
|
|
187
|
+
in the parent's context.
|
|
188
|
+
|
|
189
|
+
- **Per-dimension fan-out (heavyweight lenses).** `audit-architecture`,
|
|
190
|
+
`audit-performance`, and `audit-documentation` carry enough independent
|
|
191
|
+
dimensions to be worth fanning out: dispatch one `subagent_type: auditor`
|
|
192
|
+
call **per dimension** in a single turn via
|
|
193
|
+
[`parallel-tooling.md`](parallel-tooling.md) Rule 3, then **merge** the
|
|
194
|
+
per-dimension findings under this file's self-cross-check (the merge is
|
|
195
|
+
where cross-dimension duplicates and false positives are dropped). Respect
|
|
196
|
+
the nesting-depth budget and the concurrency cap that Rule 3 documents.
|
|
197
|
+
|
|
198
|
+
2. **Sequential inline execution (documented fallback).** When subagent
|
|
199
|
+
dispatch is unavailable, run the lens's steps turn-by-turn in the current
|
|
200
|
+
context exactly as written, ending with the self-cross-check. This changes
|
|
201
|
+
nothing about the report contract.
|
|
202
|
+
|
|
203
|
+
> **Orchestrated dynamic-workflow path (optimization note).** Six lenses ship a
|
|
204
|
+
> saved project workflow at `.claude/workflows/audit-<lens>.workflow.js` that,
|
|
205
|
+
> **when Claude Code dynamic workflows are available** (runtime is Claude Code,
|
|
206
|
+
> `disableWorkflows` unset, version `>= 2.1.154`), fans the dimensions out as
|
|
207
|
+
> parallel read-only subagents and runs an independent adversarial cross-check
|
|
208
|
+
> stage before synthesising the report. It derives its per-dimension prompts
|
|
209
|
+
> from the *lens* markdown at run time — the lens stays the single source of
|
|
210
|
+
> truth. This is a performance optimization over path 1, **not** a separate
|
|
211
|
+
> contract: strategy selection lives in
|
|
212
|
+
> [`../../scripts/lib/dynamic-workflow/capability.js`](../../scripts/lib/dynamic-workflow/capability.js)
|
|
213
|
+
> (`selectAuditStrategy`), and it is not covered by the No-Shim / hard-cutover
|
|
214
|
+
> rule in [`../../rules/git-conventions.md`](../../rules/git-conventions.md)
|
|
215
|
+
> because there is one report contract and only the execution strategy varies —
|
|
216
|
+
> the same capability-degradation pattern the protocol endorses for live-docs
|
|
217
|
+
> fallback. Force a path for testing with `MANDREL_AUDIT_STRATEGY=sequential`
|
|
218
|
+
> or `MANDREL_AUDIT_STRATEGY=orchestrated`; exercise the real disable signals
|
|
219
|
+
> with `CLAUDE_CODE_DISABLE_WORKFLOWS=1` or `disableWorkflows: true` in
|
|
220
|
+
> `.claude/settings.json`. On the orchestrated path the analysis subagents are
|
|
221
|
+
> granted only read/search tools (`Read`, `Grep`, `Glob`) — the single write is
|
|
222
|
+
> the final report artifact.
|
|
223
|
+
|
|
224
|
+
## Parallel tooling {#parallel-tooling}
|
|
225
|
+
|
|
226
|
+
When a lens batches independent reads/greps, runs a long shell (a scanner, a
|
|
227
|
+
profiler, a suite time), or fans out per-dimension, apply
|
|
228
|
+
[`parallel-tooling.md`](parallel-tooling.md): batch independent reads in one
|
|
229
|
+
turn (Rule 1), run long shells via `run_in_background` + `Monitor` (Rule 2),
|
|
230
|
+
and dispatch N independent units as N `Agent` calls in one turn (Rule 3).
|
|
@@ -18,10 +18,6 @@ is merged to `main`. The live v2 path is **Story scope only**:
|
|
|
18
18
|
after the PR opens and before auto-merge. Findings post to the PR;
|
|
19
19
|
critical findings block close (`agent::blocked`).
|
|
20
20
|
|
|
21
|
-
Legacy `scope: epic` / Epic-branch review procedure (including
|
|
22
|
-
`epic-audit-prepare.js` / `epic-audit-recheck.js`) was removed with the
|
|
23
|
-
v2 Story-only cutover.
|
|
24
|
-
|
|
25
21
|
**Invariant — Story-scope review runs outside the maker's LLM context.**
|
|
26
22
|
The Story-scope review executes inside the `single-story-close.js` close
|
|
27
23
|
subprocess, **not** in the delivering child's (maker agent's) LLM context.
|
|
@@ -45,28 +41,21 @@ The caller passes the following arguments (`/deliver` passes
|
|
|
45
41
|
|
|
46
42
|
| Argument | Type | Required | Meaning |
|
|
47
43
|
| ----------- | ------------------------------------- | -------- | ------------------------------------------------------------------------------------------------ |
|
|
48
|
-
| `scope` | `"story"`
|
|
49
|
-
| `ticketId` | integer | yes | GitHub issue number of the Story
|
|
50
|
-
| `baseRef` | string (git ref) | yes | The diff base
|
|
51
|
-
| `headRef` | string (git ref) | yes | The branch tip under review
|
|
44
|
+
| `scope` | `"story"` | yes | The live delivery path. |
|
|
45
|
+
| `ticketId` | integer | yes | GitHub issue number of the Story. |
|
|
46
|
+
| `baseRef` | string (git ref) | yes | The diff base — `main` (or `project.baseBranch`). |
|
|
47
|
+
| `headRef` | string (git ref) | yes | The branch tip under review — `story-<storyId>`. |
|
|
52
48
|
| `depth` | `"light"` \| `"standard"` \| `"deep"` | no | Risk-derived review thoroughness lever. Absent → `standard`. See **Review depth** below. |
|
|
53
49
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
the argument envelope.
|
|
50
|
+
Do not hard-code branch names or ticket types — read them from the argument
|
|
51
|
+
envelope.
|
|
57
52
|
|
|
58
53
|
### Review depth (`depth`)
|
|
59
54
|
|
|
60
|
-
`depth` is the thoroughness lever
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
changed files' intersection with the `sensitivePaths` classes registered in
|
|
65
|
-
`audit-rules.json` gives the level, their count gives the width, and
|
|
66
|
-
`resolveDepth` folds the two (a sensitive path OR a wide diff → `deep`; neither,
|
|
67
|
-
on a small diff → `light`; an unenumerable diff → `standard`). It takes no
|
|
68
|
-
planner-authored input and reads no checkpoint. `runCodeReview` forwards `depth`
|
|
69
|
-
to every provider's `runReview` input.
|
|
55
|
+
`depth` is the thoroughness lever: `runCodeReview` derives it from the diff via
|
|
56
|
+
[`review-depth.js`](../../scripts/lib/orchestration/review-depth.js) (Story #4542
|
|
57
|
+
re-based it on that observable signal, so it takes no planner-authored input) and
|
|
58
|
+
forwards it to every provider's `runReview` input.
|
|
70
59
|
|
|
71
60
|
It is an **input-only** signal: it changes *how thorough* the review is, never
|
|
72
61
|
the findings envelope (`{ status, severity, posted, report, halted,
|
|
@@ -270,8 +259,7 @@ prior baseline before merging.
|
|
|
270
259
|
Findings are **persisted as a `verification-results` structured comment on
|
|
271
260
|
the `[TICKET_ID]` issue** by `runCodeReview` (the unified findings contract of
|
|
272
261
|
Story #4411; this single comment carries the
|
|
273
|
-
|
|
274
|
-
`scope === 'story'` and the Epic when `scope === 'epic'`. The comment
|
|
262
|
+
Story-scope lens findings). The target ticket is the Story. The comment
|
|
275
263
|
is idempotent — re-runs replace the prior one — and its body includes
|
|
276
264
|
severity-tier counts plus the full findings list so downstream workflows
|
|
277
265
|
(notably the retro helper) can summarise blockers/high findings without
|