mandrel 2.39.0 → 2.40.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 +6 -3
- package/.agents/agents/auditor.md +5 -0
- package/.agents/docs/SDLC.md +21 -12
- package/.agents/instructions.md +17 -16
- package/.agents/scripts/audit-to-stories.js +510 -66
- package/.agents/scripts/lib/audit-to-stories/epic-grouping-directive.js +39 -0
- package/.agents/scripts/lib/audit-to-stories/ledger-commit.js +290 -0
- package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +94 -3
- package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +10 -0
- package/.agents/scripts/lib/label-constants.js +18 -0
- package/.agents/scripts/lib/label-taxonomy.js +18 -5
- package/.agents/scripts/lib/orchestration/epic-container.js +186 -0
- package/.agents/scripts/lib/orchestration/epic-expansion.js +148 -0
- package/.agents/scripts/lib/orchestration/plan-persist/epic-ops.js +320 -0
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +18 -0
- package/.agents/scripts/lib/orchestration/run-epilogue.js +130 -1
- package/.agents/scripts/plan-persist.js +39 -1
- package/.agents/scripts/providers/github/sub-issue-add.js +218 -0
- package/.agents/scripts/resolve-stories.js +42 -2
- package/.agents/templates/docs/audit-sweep-runbook.md +169 -0
- package/.agents/workflows/audit-to-stories.md +85 -7
- package/.agents/workflows/helpers/audit-lens-core.md +24 -4
- package/.agents/workflows/helpers/deliver-reference.md +8 -0
- package/.agents/workflows/helpers/plan-reference.md +28 -0
- package/.agents/workflows/mandrel-deliver.md +47 -43
- package/.agents/workflows/mandrel-plan.md +44 -38
- package/docs/CHANGELOG.md +16 -0
- package/package.json +1 -1
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# Audit sweep runbook
|
|
2
|
+
|
|
3
|
+
> **Template generated by Mandrel.** Copy it into your own docs tree (e.g.
|
|
4
|
+
> `docs/audit-sweep-runbook.md`), then localise every bracketed placeholder:
|
|
5
|
+
> the cadence, the lens list, who reviews the ledger PR, and the label
|
|
6
|
+
> conventions your repository actually uses. The steps themselves are the
|
|
7
|
+
> contract — the values around them are yours.
|
|
8
|
+
>
|
|
9
|
+
> The workflow this runbook drives is
|
|
10
|
+
> [`/audit-to-stories`](../../workflows/audit-to-stories.md); the CLI it calls
|
|
11
|
+
> is [`audit-to-stories.js`](../../scripts/audit-to-stories.js). Run the CLI
|
|
12
|
+
> with `--help` for the authoritative flag list.
|
|
13
|
+
|
|
14
|
+
## What this runbook is for
|
|
15
|
+
|
|
16
|
+
A maintenance **sweep** runs the `audit-*` lenses full-scope, folds their
|
|
17
|
+
findings onto the cross-run ledger, and turns what is genuinely new into
|
|
18
|
+
Stories. It is the unattended sibling of an interactive `/audit-to-stories`
|
|
19
|
+
run: no HITL gates, so every judgement call the interactive path asks a human
|
|
20
|
+
has to be settled here instead.
|
|
21
|
+
|
|
22
|
+
| Setting | Value for this repository |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| Cadence | _e.g. weekly, Sunday 02:00_ |
|
|
25
|
+
| Lenses in scope | _e.g. security, clean-code, quality, dependencies_ |
|
|
26
|
+
| Severity floor | _`delivery.auditToStories.severityFloor`, default `high`_ |
|
|
27
|
+
| Ledger reviewer | _e.g. @your-handle_ |
|
|
28
|
+
| Story triage owner | _e.g. the on-call maintainer_ |
|
|
29
|
+
|
|
30
|
+
## Step 1 — Run the lenses full-scope
|
|
31
|
+
|
|
32
|
+
Run each `audit-*` workflow with **no** `--paths` and no change-set filter, so
|
|
33
|
+
the whole target-set union is audited rather than whatever a recent branch
|
|
34
|
+
happened to touch. Each lens writes its report to
|
|
35
|
+
`temp/audits/audit-<lens>-results.md`.
|
|
36
|
+
|
|
37
|
+
A sweep scoped to a change set is not a sweep: it re-reports the same recent
|
|
38
|
+
files every cycle and never reaches the code nobody has touched in a year,
|
|
39
|
+
which is exactly where audit findings accumulate.
|
|
40
|
+
|
|
41
|
+
## Step 2 — Cross-check the severity tally
|
|
42
|
+
|
|
43
|
+
Every lens report ends its executive summary with a machine-readable line:
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
Severity tally: Critical 0 / High 3 / Medium 7 / Low 2
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The CLI re-counts the `### Finding` blocks it actually parsed and compares them
|
|
50
|
+
to that declared tally. A mismatch — or a missing tally line — means the report
|
|
51
|
+
is not trustworthy: a finding was malformed, a severity did not resolve onto the
|
|
52
|
+
canonical scale, or the lens truncated its own output.
|
|
53
|
+
|
|
54
|
+
`--auto` **fails closed** on any such failure. It exits non-zero having opened
|
|
55
|
+
no Issue and written no ledger, and names the offending report in
|
|
56
|
+
`summary.reportFailures[]`. `--allow-missing-tally` is a `--scan` affordance
|
|
57
|
+
only; `--auto` ignores it by design, because an unattended run has no operator
|
|
58
|
+
to read a warning.
|
|
59
|
+
|
|
60
|
+
**When the sweep goes red here, re-run the lens.** Do not reach for
|
|
61
|
+
`--allow-missing-tally` and do not hand-edit the report to make the numbers
|
|
62
|
+
agree — the tally is the only signal that the parse saw what the lens wrote.
|
|
63
|
+
|
|
64
|
+
## Step 3 — Dry-run the first cycles
|
|
65
|
+
|
|
66
|
+
Start every new sweep in report-only mode, and stay there until the tallies
|
|
67
|
+
stop surprising you:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
node .agents/scripts/audit-to-stories.js --auto --dry-run
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`--dry-run` performs zero GitHub writes and skips the ledger write, printing
|
|
74
|
+
only the run summary. Read `totals.create` before you let the sweep file
|
|
75
|
+
anything: a first full-scope run over an un-audited repository can propose more
|
|
76
|
+
Stories than your team can triage in a quarter. Raise `--severity` (or
|
|
77
|
+
`delivery.auditToStories.severityFloor`) until the create count is a batch you
|
|
78
|
+
would actually take on, then go live.
|
|
79
|
+
|
|
80
|
+
## Step 4 — Go live, and persist the ledger
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
node .agents/scripts/audit-to-stories.js --auto --ledger-commit
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The cross-run ledger (`baselines/audit-ledger.json`) is **consumer state, not
|
|
87
|
+
scratch output**. It is what lets the next sweep tell a re-detection from a
|
|
88
|
+
fresh finding, and a deliberately-rejected finding from an unseen one. A
|
|
89
|
+
scheduled job normally runs on an ephemeral checkout, so unless the ledger is
|
|
90
|
+
committed back it dies with the clone and every later sweep starts amnesiac —
|
|
91
|
+
re-proposing findings already filed and re-surfacing findings a human already
|
|
92
|
+
rejected.
|
|
93
|
+
|
|
94
|
+
`--ledger-commit` closes that loop. After the run summary has printed, and only
|
|
95
|
+
when the ledger actually changed, it:
|
|
96
|
+
|
|
97
|
+
1. creates `chore/audit-ledger-<YYYY-MM-DD>` from the current HEAD,
|
|
98
|
+
2. commits **only** the ledger file, subject
|
|
99
|
+
`chore(audit): reconcile audit ledger <date>`,
|
|
100
|
+
3. pushes the branch, and
|
|
101
|
+
4. opens a PR against your base branch.
|
|
102
|
+
|
|
103
|
+
**Auto-merge is never requested.** The ledger records machine-derived lifecycle
|
|
104
|
+
state, so a human glance before it lands is the point — nominate that reviewer
|
|
105
|
+
in the table above. Review the PR for entries flipping to `accepted-risk` or
|
|
106
|
+
`regressed`; those two are the ledger telling you something about your backlog,
|
|
107
|
+
not about itself.
|
|
108
|
+
|
|
109
|
+
Any git or `gh` failure in that sequence is fatal and names the step it broke
|
|
110
|
+
on — but it happens _after_ the summary is printed, so a broken remote never
|
|
111
|
+
costs you the sweep's findings.
|
|
112
|
+
|
|
113
|
+
Without the flag, a run whose ledger changed on a checkout that cannot persist
|
|
114
|
+
it — no `origin` remote, or HEAD parked off the base branch — sets
|
|
115
|
+
`ledger.unpersisted: true` in the summary and warns on stderr naming the file.
|
|
116
|
+
Treat that warning as a red sweep: the findings are fine, but the memory is
|
|
117
|
+
about to be thrown away.
|
|
118
|
+
|
|
119
|
+
## Step 5 — Enrich before you deliver
|
|
120
|
+
|
|
121
|
+
`--emit-stories` renders `{ title, body, labels }` payloads from audit findings.
|
|
122
|
+
Those bodies are **audit prose**, not delivery-ready Specs: they describe a
|
|
123
|
+
symptom and a recommendation, not a scoped change with acceptance criteria a
|
|
124
|
+
worker can verify against.
|
|
125
|
+
|
|
126
|
+
Do not point `/mandrel-deliver` at a freshly-filed audit Story. Route it through
|
|
127
|
+
`/mandrel-plan` first — the planning pass is where the finding becomes a
|
|
128
|
+
capability slice with a `## Spec`, real `acceptance[]` items and runnable
|
|
129
|
+
`verify[]` lines. Planning is deliberately not automated here: deciding what a
|
|
130
|
+
finding is worth, and how far the fix should reach, is the judgement the sweep
|
|
131
|
+
exists to surface rather than to make.
|
|
132
|
+
|
|
133
|
+
The wiring pass is the exception — it _is_ mechanical and it is **required**.
|
|
134
|
+
After opening the Issues, replay their numbers through `--wire-edges` so the
|
|
135
|
+
cohort's declared ordering exists as `blocked by #N` footers and native
|
|
136
|
+
`blocked_by` relations. An unwired cohort is genuinely unordered, and
|
|
137
|
+
`/mandrel-deliver` will co-dispatch Stories the edges say must follow one
|
|
138
|
+
another.
|
|
139
|
+
|
|
140
|
+
## Step 6 — Label convention
|
|
141
|
+
|
|
142
|
+
Audit-sourced Stories carry a closed label set, so they can be filtered out of
|
|
143
|
+
(or into) ordinary planning at a glance:
|
|
144
|
+
|
|
145
|
+
| Label | Meaning |
|
|
146
|
+
| --- | --- |
|
|
147
|
+
| `type::story` | Every emitted Story — the sweep never opens Epics or tasks. |
|
|
148
|
+
| `agent::ready` | Filed and available for pickup. |
|
|
149
|
+
| `audit::<lens>` | One per lens represented in the group; a cross-audit merge carries several. |
|
|
150
|
+
| `risk::high` | Added when any finding in the group is Critical. |
|
|
151
|
+
|
|
152
|
+
The lens labels are a **closed taxonomy**: only the canonical `audit::<lens>`
|
|
153
|
+
names are valid, and the filer refuses to emit a label the repository has never
|
|
154
|
+
created. Create them once with `audit-labels-bootstrap.js` before the first live
|
|
155
|
+
sweep — a generated label that does not exist makes every Issue create fail.
|
|
156
|
+
|
|
157
|
+
Do not invent per-finding labels. If you need another axis (a team, a
|
|
158
|
+
component), add it in triage on top of this set rather than teaching the sweep
|
|
159
|
+
to mint labels no taxonomy defines.
|
|
160
|
+
|
|
161
|
+
## Failure triage
|
|
162
|
+
|
|
163
|
+
| Symptom | Cause | Action |
|
|
164
|
+
| --- | --- | --- |
|
|
165
|
+
| Non-zero exit, `summary.reportFailures[]` populated | A lens report's tally is missing or disagrees with the parse | Re-run that lens; never downgrade with `--allow-missing-tally` |
|
|
166
|
+
| `ledger.unpersisted: true` in the summary | No `origin`, or HEAD off the base branch | Re-run with `--ledger-commit`, or commit the ledger by hand |
|
|
167
|
+
| `--ledger-commit failed at step "..."` | git or `gh` failed at the named step | Fix the remote/auth and re-run; the summary above it is still valid |
|
|
168
|
+
| Same findings re-proposed every cycle | The ledger is not being committed | Adopt Step 4 |
|
|
169
|
+
| `totals.create` far larger than the team can absorb | Severity floor too low for a first full-scope run | Raise `--severity` and re-dry-run |
|
|
@@ -44,10 +44,12 @@ They remain read-only emitters of audit reports.
|
|
|
44
44
|
## Phase 1 — Discover & parse
|
|
45
45
|
|
|
46
46
|
Run the CLI in `--scan` mode against the resolved glob. It parses every
|
|
47
|
-
|
|
47
|
+
finding block, normalises the fields (`Severity` / `Impact` are
|
|
48
48
|
both recognised; `Dimension` / `Category` likewise), and extracts file
|
|
49
|
-
paths mentioned in the body.
|
|
50
|
-
|
|
49
|
+
paths mentioned in the body. A `###` heading that carries no severity axis and
|
|
50
|
+
holds `####` blocks is read as a **grouping header**: its `####` children are
|
|
51
|
+
the findings, and the header itself never becomes one. It then stamps each
|
|
52
|
+
finding with a stable sha1 fingerprint via the shared
|
|
51
53
|
[`lib/findings/route-finding.js`](../scripts/lib/findings/route-finding.js)
|
|
52
54
|
helper (`fingerprintFinding`) — the single dedup/route implementation
|
|
53
55
|
shared with `qa-explore`. The workflow carries **no** separate inline
|
|
@@ -64,6 +66,19 @@ The emitted plan envelope carries `findings`, `groups`, `edges`,
|
|
|
64
66
|
`classifications`, and `summary`. Subsequent phases consume the file
|
|
65
67
|
rather than re-parsing the reports.
|
|
66
68
|
|
|
69
|
+
**The tally cross-check is automatic.** Every report declares
|
|
70
|
+
`Severity tally: Critical <n> / High <n> / Medium <n> / Low <n>` in its
|
|
71
|
+
Executive Summary; the scan compares that line with what it parsed and carries
|
|
72
|
+
each disagreement on `summary.reportFailures[]` as
|
|
73
|
+
`{ sourceReport, kind, reported, parsed }`. The kinds are `missing-tally` (no
|
|
74
|
+
line), `tally-mismatch` (line and parse disagree), and `unresolved-severity` (a
|
|
75
|
+
finding whose severity did not resolve — dropped from grouping, never filed as
|
|
76
|
+
an `unknown` group). They print to stderr before `--scan` returns its plan, so
|
|
77
|
+
a mis-parsed report is never read as a clean audit: re-run the lens rather than
|
|
78
|
+
file from it. Over older reports predating the mandate,
|
|
79
|
+
`--scan --allow-missing-tally` downgrades **only** `missing-tally` to a
|
|
80
|
+
warning.
|
|
81
|
+
|
|
67
82
|
## Phase 2 — HITL: severity gate
|
|
68
83
|
|
|
69
84
|
Read the plan envelope's `summary.tally`. Present the operator with the
|
|
@@ -117,6 +132,11 @@ Ask:
|
|
|
117
132
|
> default-single policy.
|
|
118
133
|
> - **Individual standalone Stories** — opens one GitHub Issue per
|
|
119
134
|
> group directly (no plan ceremony).
|
|
135
|
+
>
|
|
136
|
+
> Either way, if the sweep proposes **more than 2** Stories they are grouped
|
|
137
|
+
> under a **container Epic** by default — a title, a one-paragraph goal and a
|
|
138
|
+
> child checklist, carrying nothing a child does not already carry. Say so if
|
|
139
|
+
> you would rather file them flat.
|
|
120
140
|
|
|
121
141
|
**STOP** until the operator picks.
|
|
122
142
|
|
|
@@ -133,7 +153,14 @@ node .agents/scripts/audit-to-stories.js --emit-plan-seed \
|
|
|
133
153
|
The seed renders the canonical one-pager sections — Problem Statement,
|
|
134
154
|
Recommended Direction, Key Assumptions (with links to every source
|
|
135
155
|
report), MVP Scope (the M proposed Stories), Key Files (so `/mandrel-plan`'s
|
|
136
|
-
authoring step has concrete anchors), Not Doing.
|
|
156
|
+
authoring step has concrete anchors), Grouping, Not Doing.
|
|
157
|
+
|
|
158
|
+
**Grouping is the container-Epic directive.** Above 2 proposed Stories the
|
|
159
|
+
seed instructs `/mandrel-plan` to group them under one Epic — a sweep is the
|
|
160
|
+
clearest case for a container, since every Story shares a provenance and an
|
|
161
|
+
operator usually delivers them together. It is a directive in the text, not an
|
|
162
|
+
automatic write: Phase 4 above is where an operator declines it. Below the
|
|
163
|
+
threshold the section says so and asks for nothing.
|
|
137
164
|
|
|
138
165
|
Chain into the existing planning entrypoint:
|
|
139
166
|
|
|
@@ -215,6 +242,14 @@ its footprint guard ignores the shared provenance footers, so an unwired cohort
|
|
|
215
242
|
is genuinely unordered and `/mandrel-deliver` will co-dispatch Stories the edges say
|
|
216
243
|
must follow one another.
|
|
217
244
|
|
|
245
|
+
**Preconditions.** The pass writes through the configured provider, so it needs
|
|
246
|
+
`github.owner` **and** `github.repo` in `.agentrc.json` plus working `gh` auth
|
|
247
|
+
(`GH_TOKEN`/`gh auth status`) — the same two things Phase 1's dedup needs. When
|
|
248
|
+
either is missing the command refuses and names which one; fix that and re-run
|
|
249
|
+
the exact command above. Do not transcribe the footers by hand: `/mandrel-deliver`
|
|
250
|
+
reads them, but the native `blocked_by` relations only exist if this pass wrote
|
|
251
|
+
them.
|
|
252
|
+
|
|
218
253
|
## Phase 6 — Idempotency (folded into Phase 1 scan)
|
|
219
254
|
|
|
220
255
|
The `--scan` step routes each group's findings through the shared
|
|
@@ -289,6 +324,8 @@ summarising the run:
|
|
|
289
324
|
|
|
290
325
|
When the single-plan path ran, link the Story (or plan-run) the chained
|
|
291
326
|
`/mandrel-plan` opened. When the Standalone-Stories path ran, list every Issue URL.
|
|
327
|
+
Either way, name the container Epic if one was created — it is the single id
|
|
328
|
+
that delivers the whole sweep (`/mandrel-deliver <epicId>`).
|
|
292
329
|
|
|
293
330
|
## Constraints
|
|
294
331
|
|
|
@@ -330,18 +367,59 @@ writing their `temp/audits/audit-*-results.md` reports, then (2) invokes the
|
|
|
330
367
|
CLI's **`--auto` mode** over those results:
|
|
331
368
|
|
|
332
369
|
```bash
|
|
333
|
-
node .agents/scripts/audit-to-stories.js --auto [--dry-run] \
|
|
370
|
+
node .agents/scripts/audit-to-stories.js --auto [--dry-run] [--ledger-commit] \
|
|
334
371
|
[--glob "temp/audits/audit-*-results.md"] [--severity <floor>]
|
|
335
372
|
```
|
|
336
373
|
|
|
374
|
+
The routine shape is **lenses full-scope → dry-run → live with a ledger PR**:
|
|
375
|
+
|
|
376
|
+
1. Run the `audit-*` lenses with no `--paths` and no change-set filter. A
|
|
377
|
+
sweep scoped to a change set re-reports the same recent files every cycle
|
|
378
|
+
and never reaches the untouched code where findings accumulate.
|
|
379
|
+
2. `--auto --dry-run` for the first cycles — zero writes, summary only. Read
|
|
380
|
+
`totals.create` and raise the severity floor until it is a batch the team
|
|
381
|
+
would actually take on.
|
|
382
|
+
3. `--auto --ledger-commit` once the tallies stop surprising you.
|
|
383
|
+
|
|
337
384
|
`--auto` runs with **no interactive gates**: it resolves the severity floor
|
|
338
385
|
from `delivery.auditToStories.severityFloor` (default `high`, overridable with
|
|
339
386
|
`--severity`), applies the two-stage dedup, reconciles the cross-run ledger,
|
|
340
387
|
and prints a run-summary JSON (create / skip-open / skip-reoccurring /
|
|
341
388
|
suppressed-by-ledger tallies, plus the re-detected open Issue numbers an
|
|
342
389
|
operator may want a "re-detected" comment on). `--dry-run` performs zero GitHub
|
|
343
|
-
writes and skips the ledger write, emitting only the summary.
|
|
344
|
-
|
|
390
|
+
writes and skips the ledger write, emitting only the summary.
|
|
391
|
+
|
|
392
|
+
`--auto` **fails closed on any `summary.reportFailures[]` entry** (Phase 1): an
|
|
393
|
+
unattended sweep has no operator to read a warning, so a missing or mismatched
|
|
394
|
+
`Severity tally:` line — or a finding whose severity did not resolve — exits
|
|
395
|
+
non-zero having opened no Issue and written no ledger. `--allow-missing-tally`
|
|
396
|
+
is a `--scan` affordance that `--auto` ignores. A red sweep means the report is
|
|
397
|
+
untrustworthy: re-run the lens. The host scheduler owns the cadence; this
|
|
398
|
+
workflow owns the routing.
|
|
399
|
+
|
|
400
|
+
### The ledger is consumer state — commit it
|
|
401
|
+
|
|
402
|
+
`baselines/audit-ledger.json` is **committed consumer state, not scratch
|
|
403
|
+
output**. A scheduled sweep normally runs on an ephemeral checkout, so unless
|
|
404
|
+
the reconciled ledger is committed back it dies with the clone: every later
|
|
405
|
+
sweep starts amnesiac, re-proposing findings already filed and re-surfacing
|
|
406
|
+
findings a human already rejected.
|
|
407
|
+
|
|
408
|
+
`--ledger-commit` closes that loop. After the summary prints — and only when
|
|
409
|
+
the ledger changed — it creates `chore/audit-ledger-<YYYY-MM-DD>` from HEAD,
|
|
410
|
+
commits **only** the ledger file, pushes it, and opens a PR against
|
|
411
|
+
`project.baseBranch`. **Auto-merge is never requested**: a human glance at the
|
|
412
|
+
`accepted-risk` / `regressed` flips before it lands is the point. A git or `gh`
|
|
413
|
+
failure is fatal and names its step, but only after the summary is printed, so
|
|
414
|
+
a broken remote never costs the operator the run's findings. `--dry-run` skips
|
|
415
|
+
the tail. Without the flag, a changed ledger on a checkout that cannot persist
|
|
416
|
+
it — no `origin`, or HEAD off the base branch — sets `ledger.unpersisted: true`
|
|
417
|
+
in the summary and warns on stderr naming the file.
|
|
418
|
+
|
|
419
|
+
The full sweep procedure — tally cross-check, the ledger PR, the
|
|
420
|
+
enrich-before-deliver step and the label convention — ships as a
|
|
421
|
+
consumer-copyable template at
|
|
422
|
+
[`templates/docs/audit-sweep-runbook.md`](../templates/docs/audit-sweep-runbook.md).
|
|
345
423
|
|
|
346
424
|
## See also
|
|
347
425
|
|
|
@@ -51,7 +51,8 @@ path). The report MUST include every section its lens template mandates — writ
|
|
|
51
51
|
`_No findings._` rather than omitting a section — and always an
|
|
52
52
|
`## Executive Summary` and a `## Detailed Findings` section. The Executive
|
|
53
53
|
Summary carries the self-cross-check `kept <k> / dropped <d>` line
|
|
54
|
-
([below](#self-cross-check))
|
|
54
|
+
([below](#self-cross-check)) and, beside it, the machine-readable severity
|
|
55
|
+
tally ([below](#severity-tally)).
|
|
55
56
|
|
|
56
57
|
Every finding under `## Detailed Findings` uses the shared 7-field skeleton
|
|
57
58
|
below. A lens may **add** fields (e.g. a WCAG success criterion, a CWE ID, a
|
|
@@ -64,9 +65,11 @@ drops or renames a shared field.
|
|
|
64
65
|
```markdown
|
|
65
66
|
## Executive Summary
|
|
66
67
|
|
|
67
|
-
[The lens's headline read
|
|
68
|
-
line. A lens may mandate additional report
|
|
69
|
-
findings — its own body names them.]
|
|
68
|
+
[The lens's headline read, the self-cross-check `kept <k> / dropped <d>` line,
|
|
69
|
+
and the severity tally line below. A lens may mandate additional report
|
|
70
|
+
sections between here and the findings — its own body names them.]
|
|
71
|
+
|
|
72
|
+
Severity tally: Critical <n> / High <n> / Medium <n> / Low <n>
|
|
70
73
|
|
|
71
74
|
## Detailed Findings
|
|
72
75
|
|
|
@@ -188,6 +191,23 @@ that rests on one of them:
|
|
|
188
191
|
A lens that keeps every finding still records `dropped 0` — the line's absence
|
|
189
192
|
is itself a defect (it means the pass did not run).
|
|
190
193
|
|
|
194
|
+
## Severity tally (mandatory, machine-readable) {#severity-tally}
|
|
195
|
+
|
|
196
|
+
The Executive Summary MUST also carry exactly one tally line, counting the
|
|
197
|
+
findings you kept:
|
|
198
|
+
|
|
199
|
+
```text
|
|
200
|
+
Severity tally: Critical <n> / High <n> / Medium <n> / Low <n>
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Write every bucket, zeros included, and never count `Info` — the scale already
|
|
204
|
+
excludes it from scheduled work. This line is the report's own checksum:
|
|
205
|
+
`audit-to-stories --scan` cross-checks it against the findings its parser
|
|
206
|
+
extracted and names any disagreement as a **report failure**
|
|
207
|
+
(`summary.reportFailures[]`), and `--auto` refuses to file anything from a
|
|
208
|
+
report whose line is missing or wrong. A parse that silently drops findings is
|
|
209
|
+
otherwise indistinguishable from a clean audit.
|
|
210
|
+
|
|
191
211
|
## Execution strategy {#execution-strategy}
|
|
192
212
|
|
|
193
213
|
A lens is a self-contained, read-only unit of work — exactly the shape a
|
|
@@ -292,6 +292,14 @@ This executes, in order:
|
|
|
292
292
|
(files issues when auto-file is on; posts `follow-ups`).
|
|
293
293
|
- `sibling-coherence` — Spec/Acceptance coherence check across sibling bodies
|
|
294
294
|
(`plan-run-sibling-coherence`).
|
|
295
|
+
- `epic-close` — closes a container Epic once **every** child Story is
|
|
296
|
+
`agent::done`, as `completed`. This is the only completion cascade v2 has:
|
|
297
|
+
it closes the container and nothing else — no child status roll-up, no label
|
|
298
|
+
inheritance, no reopening. Because linkage is parent→child only, the parent
|
|
299
|
+
is found by scanning open `type::epic` issues, and only an Epic containing
|
|
300
|
+
one of *this run's* Stories is considered, so an unrelated container is
|
|
301
|
+
never swept. An Epic with an outstanding child is reported `pending` and
|
|
302
|
+
left open.
|
|
295
303
|
|
|
296
304
|
A single-Story run skips the epilogue — follow-ups are captured on merge
|
|
297
305
|
confirm instead (`captureStoryFollowUps`).
|
|
@@ -414,6 +414,34 @@ That is the whole point of running it first: a dry run that comes back clean
|
|
|
414
414
|
has already paid for every deterministic refusal, so the real persist has
|
|
415
415
|
nothing left to discover except network failure.
|
|
416
416
|
|
|
417
|
+
## The container Epic (Gate #3, N>2)
|
|
418
|
+
|
|
419
|
+
Above two Stories, `/mandrel-plan` offers to group them under one `type::epic`
|
|
420
|
+
container. Confirmed, persist opens it **after** the Stories — its body embeds
|
|
421
|
+
their issue numbers and its sub-issue edges need their database ids — and
|
|
422
|
+
links every created Story both ways it can: a `- [ ] #N` body checklist and a
|
|
423
|
+
native GitHub sub-issue edge. Both are written because each survives what the
|
|
424
|
+
other does not; the delivery-side reader unions them.
|
|
425
|
+
|
|
426
|
+
What the Epic must never carry: an `agent::*` label (that absence keeps it out
|
|
427
|
+
of the bare `/mandrel-deliver` ready list and outside the `type::story`-scoped
|
|
428
|
+
body lint), a `## Spec`, an `acceptance[]` / `verify[]`, or any path, finding
|
|
429
|
+
or rationale a child does not already hold. It is a container; unique content
|
|
430
|
+
here is content no delivering agent reads.
|
|
431
|
+
|
|
432
|
+
What the **Stories** never gain is an `Epic: #N` footer. Linkage is
|
|
433
|
+
parent→child only, which is exactly why every existing refusal of that footer
|
|
434
|
+
still stands and each Story stays independently deliverable (ADR
|
|
435
|
+
`20260905-5139`).
|
|
436
|
+
|
|
437
|
+
Degradation is deliberate: an unensurable `type::epic` label skips the Epic
|
|
438
|
+
entirely (an unlabelled container is not a container), while a failed
|
|
439
|
+
sub-issue edge only warns — the checklist still lists every child. Either way
|
|
440
|
+
the Stories are untouched and deliver by id. A resumed persist adopts an
|
|
441
|
+
existing Epic carrying the same fingerprint, which is keyed on the title **and
|
|
442
|
+
the exact child set**, so a run grouping different Stories never adopts the
|
|
443
|
+
wrong container.
|
|
444
|
+
|
|
417
445
|
## Ready means fully persisted
|
|
418
446
|
|
|
419
447
|
`agent::ready` is the **terminal** step, not part of the creating POST.
|
|
@@ -7,8 +7,8 @@ description:
|
|
|
7
7
|
|
|
8
8
|
# /mandrel-deliver
|
|
9
9
|
|
|
10
|
-
> **Lean spine.** Happy path + gate list. Sequencing, dispatch mechanics,
|
|
11
|
-
>
|
|
10
|
+
> **Lean spine.** Happy path + gate list. Sequencing, dispatch mechanics, intent
|
|
11
|
+
> phrases, ceremony and the epilogue live in on-demand
|
|
12
12
|
> [`helpers/deliver-reference.md`](helpers/deliver-reference.md) ("reference"
|
|
13
13
|
> below); the unplanned path in
|
|
14
14
|
> [`helpers/deliver-light.md`](helpers/deliver-light.md). Every delivery reads
|
|
@@ -21,9 +21,8 @@ close-and-land tail; Stories are implemented via
|
|
|
21
21
|
[`helpers/deliver-story.md`](helpers/deliver-story.md).
|
|
22
22
|
|
|
23
23
|
The dependency graph is **discovered, not declared** — `resolve-stories.js`
|
|
24
|
-
reads it from live state, so you can deliver Stories **across plan runs and
|
|
25
|
-
|
|
26
|
-
`route::lite` a body-derived hint only.
|
|
24
|
+
reads it from live state, so you can deliver Stories **across plan runs and over
|
|
25
|
+
time**. `plan-run::<id>` is filter metadata, never a resolution input.
|
|
27
26
|
|
|
28
27
|
## Inputs
|
|
29
28
|
|
|
@@ -36,25 +35,28 @@ you read:
|
|
|
36
35
|
| `/mandrel-deliver 4712` | ids | One Story via `helpers/deliver-story.md`, **inline in this session** — no `story-worker` spawn. |
|
|
37
36
|
| `/mandrel-deliver 4712 4713 …` | ids | Resolve the set, sequence by the discovered graph via `stories-wave-tick.js`, dispatch sub-agents. |
|
|
38
37
|
| `/mandrel-deliver 4712 - 4716` | ids | A **range** — every id in the inclusive span. |
|
|
38
|
+
| `/mandrel-deliver 4700` (a `type::epic`) | ids | The Epic's **open** child Stories. Mixes with Story ids. |
|
|
39
39
|
| `/mandrel-deliver add a --json flag to doctor` | prompt | Unplanned work: gate, author a receipt Story, land it — [`helpers/deliver-light.md`](helpers/deliver-light.md). |
|
|
40
40
|
|
|
41
41
|
**The discriminator is lexical and total.** An argument matching `^#?\d+$` is an
|
|
42
42
|
id, and `^#?\d+\s*[-–—]\s*#?\d+$` an inclusive **range** — pass one on as a
|
|
43
|
-
single unspaced token, never hand-expanded (reference
|
|
43
|
+
single unspaced token, never hand-expanded (reference). Either shape
|
|
44
44
|
means ids; anything else means a prompt. A **mixed** invocation (ids *and*
|
|
45
|
-
prose) is a **hard error** — refuse it and ask which was meant. A ticket
|
|
46
|
-
`type::story`, or
|
|
45
|
+
prose) is a **hard error** — refuse it and ask which was meant. A ticket that
|
|
46
|
+
is neither `type::story` nor `type::epic`, or that carries an `Epic: #N`
|
|
47
|
+
footer, is a hard error too — container Epics link parent→child only, so that
|
|
48
|
+
footer stays refused.
|
|
47
49
|
|
|
48
50
|
## Saying what you want
|
|
49
51
|
|
|
50
|
-
No flags to remember: state intent — *"…but I'll merge it myself"*, *"…one at
|
|
51
|
-
|
|
52
|
+
No flags to remember: state intent — *"…but I'll merge it myself"*, *"…one at a
|
|
53
|
+
time"* — and announce what you read. Phrasings and the flags they fill in:
|
|
52
54
|
reference § Intent phrases.
|
|
53
55
|
|
|
54
56
|
`--yes` is **runner-set, never operator-typed**: cron, `/loop` and headless
|
|
55
57
|
dispatch set it to mean *nobody is at the keyboard*, which fails the unplanned
|
|
56
|
-
path's over-scope stop closed to an envelope instead of a question. Never
|
|
57
|
-
|
|
58
|
+
path's over-scope stop closed to an envelope instead of a question. Never add it
|
|
59
|
+
to an attended run.
|
|
58
60
|
|
|
59
61
|
## Procedure
|
|
60
62
|
|
|
@@ -66,9 +68,11 @@ it to an operator or add it to an attended run.
|
|
|
66
68
|
`node .agents/scripts/resolve-stories.js --ids <id,id,...>`. It validates the
|
|
67
69
|
set and shows what will run: read `stories[]`, `dag[]` and `done[]` to
|
|
68
70
|
present the order in step 2, but do **not** thread them into step 3 — the
|
|
69
|
-
tick re-resolves the graph every beat.
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
tick re-resolves the graph every beat. An Epic id expands to its open child
|
|
72
|
+
Stories first — **announce it**. It hard-errors (exit 1) on an id that is
|
|
73
|
+
neither a Story nor an Epic, on an `Epic: #N` footer, on an Epic with no
|
|
74
|
+
open children, or on edges it cannot read — a missing gate would co-dispatch
|
|
75
|
+
against an unlanded blocker.
|
|
72
76
|
|
|
73
77
|
2. **Confirm (N>1).** Present the order; wait unless `--yes`.
|
|
74
78
|
|
|
@@ -87,62 +91,62 @@ it to an operator or add it to an attended run.
|
|
|
87
91
|
Each beat re-probes live state to derive done / in-flight itself; you never
|
|
88
92
|
compute them. `--dispatched` is the one thing you must supply — the
|
|
89
93
|
append-only list of every id you spawned this run. Cross-run de-confliction
|
|
90
|
-
via the assignee lease is automatic
|
|
91
|
-
Dispatch mechanics). Branch on the exit code:
|
|
94
|
+
via the assignee lease is automatic. Branch on the exit code:
|
|
92
95
|
- **0** — dispatch each `ready` id (already capped and overlap-free); an
|
|
93
96
|
empty `ready` with work in flight means "waiting", so keep looping;
|
|
94
97
|
`epilogueDue: true` means every Story is done — step 4.
|
|
95
98
|
- **2 / 3 / 4** — `cycleError` / `wedged` / `blocked`: stop the loop and
|
|
96
|
-
route per reference
|
|
97
|
-
|
|
98
|
-
|
|
99
|
+
route per reference. **4** is the protocol's HITL pause
|
|
100
|
+
([`instructions.md` § 1.J](../instructions.md)) — surface it and wait for
|
|
101
|
+
the operator; never poll.
|
|
99
102
|
|
|
100
103
|
4. **Close each hand-off** (§ Closing what the workers hand back), then, with
|
|
101
104
|
every Story landed, run the **per-run epilogue (N>1)**:
|
|
102
|
-
`node .agents/scripts/plan-run-epilogue.js --stories 101,102
|
|
103
|
-
|
|
105
|
+
`node .agents/scripts/plan-run-epilogue.js --stories 101,102`, which also
|
|
106
|
+
closes a container Epic whose children all landed. N=1 skips it
|
|
107
|
+
([reference](helpers/deliver-reference.md)).
|
|
104
108
|
|
|
105
109
|
5. **Correct what the change invalidated.** If a memory you recalled this
|
|
106
|
-
session is now wrong — a trap this landed, a budget it moved — fix that
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
+
session is now wrong — a trap this landed, a budget it moved — fix that entry
|
|
111
|
+
now, while both the old belief and the new fact are in context, and say so
|
|
112
|
+
when you report. No memory substrate → skip silently. Sweeping the whole pool
|
|
113
|
+
is [`/memory-consolidate`](memory-consolidate.md), not this step.
|
|
110
114
|
|
|
111
115
|
## Closing what the workers hand back {#tail}
|
|
112
116
|
|
|
113
117
|
**The tail is the orchestrator's, not the worker's.** A dispatched
|
|
114
118
|
`story-worker` stops at a pushed branch and returns a hand-off; **you** run
|
|
115
119
|
[`helpers/deliver-story.md`](helpers/deliver-story.md) Step 3
|
|
116
|
-
(`single-story-close.js`) for it, foreground, and relay
|
|
120
|
+
(`single-story-close.js`) for it, foreground, and relay its envelope.
|
|
117
121
|
|
|
118
|
-
**Serialize the tail.** Implementation runs in parallel; closing does not.
|
|
119
|
-
one Story at a time — closes contend on the base branch, the merge queue
|
|
120
|
-
the checkout. A worker handing back mid-close waits its turn.
|
|
122
|
+
**Serialize the tail.** Implementation runs in parallel; closing does not.
|
|
123
|
+
Close one Story at a time — closes contend on the base branch, the merge queue
|
|
124
|
+
and the checkout. A worker handing back mid-close waits its turn.
|
|
121
125
|
|
|
122
|
-
**A worker returning no terminal envelope is
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
+
**A worker returning no terminal envelope is expected**, not a failure to answer
|
|
127
|
+
with a re-dispatch: only close mints one. Close the pushed branch, or probe
|
|
128
|
+
read-only with `node .agents/scripts/deliver-recover.js --story <storyId>` and
|
|
129
|
+
resume what it names.
|
|
126
130
|
|
|
127
131
|
**Reading the outcome.** Each close ends the Story in one schema-validated
|
|
128
132
|
envelope — `landed` | `pending` | `blocked` | `failed`; statuses, exits and
|
|
129
|
-
fields are digest § 5. `pending` is **not** a failure — `nextCommand
|
|
130
|
-
it; run that, do not re-dispatch.
|
|
133
|
+
fields are digest § 5. `pending` is **not** a failure — run its `nextCommand`.
|
|
131
134
|
|
|
132
135
|
**Branch model (authoritative).** `story-<id>` → PR → `main` (squash +
|
|
133
136
|
required checks), per digest § 2; dependent Stories land sequentially. Ceremony
|
|
134
|
-
depth (profiles + derived level via `ceremony-routing.js`, review
|
|
135
|
-
|
|
137
|
+
depth (profiles + the derived level via `ceremony-routing.js`, which review
|
|
138
|
+
depth reads): reference § Ceremony.
|
|
136
139
|
|
|
137
140
|
## Constraints
|
|
138
141
|
|
|
139
142
|
- **Land or block — never a silent local build** (digest § 2). Attended delivers
|
|
140
143
|
default to close-and-land (`delivery.routing.closeAndLand: true`); rest at
|
|
141
|
-
`agent::closing` only when a human owns
|
|
142
|
-
- **`/mandrel-deliver` never plans.** Planned tickets come from
|
|
143
|
-
an over-scope prompt **escalates and
|
|
144
|
-
session to rescue it
|
|
145
|
-
§ Escalation is
|
|
144
|
+
`agent::closing` only when a human owns the merge.
|
|
145
|
+
- **`/mandrel-deliver` never plans.** Planned tickets come from
|
|
146
|
+
[`/mandrel-plan`](mandrel-plan.md), and an over-scope prompt **escalates and
|
|
147
|
+
ends** — never invoke `/mandrel-plan` in this session to rescue it
|
|
148
|
+
([`helpers/deliver-light.md`](helpers/deliver-light.md) § Escalation is
|
|
149
|
+
terminal).
|
|
146
150
|
|
|
147
151
|
## See also
|
|
148
152
|
|