mandrel 2.54.0 → 2.55.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/agents/story-worker.md +24 -23
- package/.agents/audit-checklists/accessibility.md +0 -3
- package/.agents/audit-checklists/mobile.md +0 -4
- package/.agents/docs/agentrc-reference.json +4 -2
- package/.agents/docs/configuration.md +2 -0
- package/.agents/schemas/agentrc.schema.json +15 -1
- package/.agents/schemas/lifecycle/merge.unlanded.schema.json +2 -1
- package/.agents/schemas/story-deliver-terminal.schema.json +1 -0
- package/.agents/scripts/audit-to-stories.js +158 -7
- package/.agents/scripts/check-audit-attribution.js +119 -62
- package/.agents/scripts/check-test-portability.js +512 -0
- package/.agents/scripts/coverage-capture.js +17 -10
- package/.agents/scripts/evidence-gate.js +31 -4
- package/.agents/scripts/generate-workflows-doc.js +65 -14
- package/.agents/scripts/git-cleanup.js +4 -0
- package/.agents/scripts/lib/ITicketingProvider.js +78 -0
- package/.agents/scripts/lib/audit-advisories.js +195 -0
- package/.agents/scripts/lib/audit-attribution.js +22 -0
- package/.agents/scripts/lib/audit-to-stories/dedupe-against-github.js +68 -5
- package/.agents/scripts/lib/audit-to-stories/issue-index.js +83 -0
- package/.agents/scripts/lib/audit-to-stories/ledger-commit.js +60 -114
- package/.agents/scripts/lib/audit-to-stories/ledger-pr.js +347 -0
- package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +169 -44
- package/.agents/scripts/lib/baselines/merge-envelopes.js +298 -32
- package/.agents/scripts/lib/bootstrap/baseline-merge-driver.js +180 -14
- package/.agents/scripts/lib/cli-args.js +26 -0
- package/.agents/scripts/lib/close-validation/gates.js +113 -7
- package/.agents/scripts/lib/close-validation/process.js +7 -3
- package/.agents/scripts/lib/close-validation/runner.js +62 -11
- package/.agents/scripts/lib/config/ci.js +28 -9
- package/.agents/scripts/lib/config-settings-schema-delivery.js +7 -0
- package/.agents/scripts/lib/config-settings-schema.js +19 -1
- package/.agents/scripts/lib/coverage-capture-fullscope.js +23 -11
- package/.agents/scripts/lib/coverage-capture-incremental.js +22 -16
- package/.agents/scripts/lib/coverage-capture-usage.js +5 -1
- package/.agents/scripts/lib/coverage-capture.js +77 -3
- package/.agents/scripts/lib/findings/route-finding.js +4 -2
- package/.agents/scripts/lib/full-suite-lock.js +232 -6
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/git/sync-from-base.js +130 -13
- package/.agents/scripts/lib/observability/source-classifier.js +1 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +10 -2
- package/.agents/scripts/lib/orchestration/check-baselines/phases/refresh-ack.js +75 -15
- package/.agents/scripts/lib/orchestration/deliver-recover.js +82 -43
- package/.agents/scripts/lib/orchestration/dependency-candidates.js +8 -4
- package/.agents/scripts/lib/orchestration/epic-candidates.js +9 -4
- package/.agents/scripts/lib/orchestration/epic-container.js +66 -4
- package/.agents/scripts/lib/orchestration/epic-rollup.js +233 -84
- package/.agents/scripts/lib/orchestration/file-assumptions.js +218 -16
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/branches.js +93 -7
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/git-probes.js +22 -6
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/parse-args.js +26 -5
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/phase-drivers.js +13 -2
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +35 -5
- package/.agents/scripts/lib/orchestration/merge-block-class.js +18 -3
- package/.agents/scripts/lib/orchestration/merge-poll.js +284 -40
- package/.agents/scripts/lib/orchestration/plan-persist/epic-adoption.js +49 -2
- package/.agents/scripts/lib/orchestration/plan-persist/epic-ops.js +43 -7
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +24 -1
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +5 -0
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +3 -0
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +63 -0
- package/.agents/scripts/lib/orchestration/plan-persist/wave-serialisation.js +110 -0
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +130 -40
- package/.agents/scripts/lib/orchestration/resolve-stories.js +44 -1
- package/.agents/scripts/lib/orchestration/review-providers/native.js +31 -11
- package/.agents/scripts/lib/orchestration/review-providers/scoped-lint.js +27 -24
- package/.agents/scripts/lib/orchestration/run-epilogue.js +59 -38
- package/.agents/scripts/lib/orchestration/single-story-close/close-note.js +81 -0
- package/.agents/scripts/lib/orchestration/single-story-close/failed-terminal.js +40 -51
- package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +10 -2
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +101 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +351 -28
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +27 -6
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +117 -22
- package/.agents/scripts/lib/orchestration/story-close/baseline-upward-writeback.js +94 -12
- package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +6 -1
- package/.agents/scripts/lib/orchestration/ticket-validator.js +25 -14
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +30 -0
- package/.agents/scripts/lib/orchestration/verify-credit.js +37 -0
- package/.agents/scripts/lib/pinned-override-notes.js +41 -53
- package/.agents/scripts/lib/pinned-override-resolve.js +212 -0
- package/.agents/scripts/lib/qa/resolve-qa-contract.js +18 -0
- package/.agents/scripts/lib/single-story-sweep/sweep-lock.js +173 -9
- package/.agents/scripts/lib/skills/walk-skill-files.js +24 -7
- package/.agents/scripts/lib/test-temp.js +167 -30
- package/.agents/scripts/lib/validation-evidence.js +37 -0
- package/.agents/scripts/lib/wave-runner/footprint.js +167 -14
- package/.agents/scripts/lib/wave-runner/live-probe.js +7 -1
- package/.agents/scripts/lib/wave-runner/ready-set.js +1 -1
- package/.agents/scripts/merge-baseline.js +175 -21
- package/.agents/scripts/providers/github/errors.js +22 -1
- package/.agents/scripts/providers/github/issues.js +106 -1
- package/.agents/scripts/providers/github/sub-issue-add.js +18 -1
- package/.agents/scripts/providers/github.js +6 -0
- package/.agents/scripts/resolve-stories.js +44 -34
- package/.agents/scripts/single-story-close.js +5 -0
- package/.agents/scripts/stories-wave-tick.js +37 -13
- package/.agents/templates/docs/audit-sweep-runbook.md +41 -7
- package/.agents/workflows/audit-accessibility.md +16 -31
- package/.agents/workflows/audit-mobile.md +20 -37
- package/.agents/workflows/git-cleanup.md +17 -3
- package/.agents/workflows/helpers/audit-lens-core.md +45 -0
- package/.agents/workflows/helpers/deliver-digest.md +7 -6
- package/.agents/workflows/helpers/deliver-reference.md +35 -14
- package/.agents/workflows/helpers/deliver-story-reference.md +7 -4
- package/.agents/workflows/helpers/deliver-story.md +15 -12
- package/.agents/workflows/helpers/plan-reference.md +7 -0
- package/.agents/workflows/mandrel-plan.md +4 -7
- package/.agents/workflows/memory-consolidate.md +14 -9
- package/docs/CHANGELOG.md +27 -0
- package/lib/cli/registry.js +64 -21
- package/lib/cli/sync.js +27 -2
- package/package.json +7 -4
|
@@ -48,20 +48,19 @@ the step-by-step. This shared core binds every role:
|
|
|
48
48
|
You are a **Story delivery worker**: you take one Story from init through
|
|
49
49
|
implementation to a **pushed branch**, then return. You do **not** close it —
|
|
50
50
|
your caller owns the close-and-land tail. Follow the `helpers/deliver-story`
|
|
51
|
-
prose your caller hands you; this delta states the
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
come.
|
|
51
|
+
prose your caller hands you; this delta states the MUSTs. Treat a
|
|
52
|
+
blocking tool-permission prompt as a harness condition — flip to
|
|
53
|
+
`agent::blocked` rather than waiting on an approval that cannot come.
|
|
55
54
|
|
|
56
55
|
## Worktree discipline (MUST)
|
|
57
56
|
|
|
58
57
|
1. Initialize with
|
|
59
58
|
`node .agents/scripts/single-story-init.js --story <storyId>` from the
|
|
60
|
-
**main checkout**, synchronously at max Bash timeout —
|
|
61
|
-
|
|
59
|
+
**main checkout**, synchronously at max Bash timeout — the install
|
|
60
|
+
can take minutes; never background it.
|
|
62
61
|
2. Capture `workCwd` and `dependenciesInstalled` from the envelope.
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
Anchor every path at the absolute `workCwd` and work only there; never
|
|
63
|
+
move the main checkout HEAD.
|
|
65
64
|
|
|
66
65
|
## Verify branch before every commit (MUST)
|
|
67
66
|
|
|
@@ -81,8 +80,8 @@ follow-up commit, never amend.
|
|
|
81
80
|
## Docs context — digest first
|
|
82
81
|
|
|
83
82
|
Do **not** re-read every file in `project.docsContextFiles`. Read the
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
digest your caller passes, then pull files on demand at the lines it
|
|
84
|
+
names. A null digest path means no docs mandate.
|
|
86
85
|
|
|
87
86
|
## Close gates — one credited run
|
|
88
87
|
|
|
@@ -90,7 +89,7 @@ the lines it names. A null `docsDigestPath` means no mandate.
|
|
|
90
89
|
(**typecheck, lint, test, format, maintainability, coverage, crap**) and is
|
|
91
90
|
the authoritative gate — do not pre-run it. The **one** exception is
|
|
92
91
|
the full suite: run it exactly once, after the self-eval loop's last fix
|
|
93
|
-
commit and immediately
|
|
92
|
+
commit and immediately after the push, in the shape close credits. A bare
|
|
94
93
|
`npm test` / `pnpm run test` deposits **no** credit:
|
|
95
94
|
|
|
96
95
|
```bash
|
|
@@ -102,8 +101,9 @@ node <main-repo>/.agents/scripts/evidence-gate.js --standalone \
|
|
|
102
101
|
```
|
|
103
102
|
|
|
104
103
|
Dispatch it in the **background**: it routinely outruns the host's sync
|
|
105
|
-
Bash ceiling, and its completion re-invokes you —
|
|
106
|
-
|
|
104
|
+
Bash ceiling, and its completion re-invokes you — which is why the push
|
|
105
|
+
comes first. Never spawn a task to poll or `sleep`-loop
|
|
106
|
+
against it; a waiter with a wrong condition outlives the agent. Share
|
|
107
107
|
`lint` / `typecheck` evidence with close via `evidence-gate.js`; never
|
|
108
108
|
stamp coverage / CRAP fresh any other way.
|
|
109
109
|
|
|
@@ -117,13 +117,12 @@ Waiter traps: [`parallel-tooling.md`](../workflows/helpers/parallel-tooling.md)
|
|
|
117
117
|
|
|
118
118
|
## Acceptance self-eval before close (MUST)
|
|
119
119
|
|
|
120
|
-
|
|
121
|
-
run the bounded acceptance self-eval loop
|
|
120
|
+
**Before** flipping to `closing`, run the bounded self-eval loop
|
|
122
121
|
([`acceptance-self-eval.md`](../workflows/helpers/acceptance-self-eval.md)).
|
|
123
122
|
It scores the change set you computed **once** and injected into the critic
|
|
124
123
|
— never one it re-derives — against each `acceptance[]` item,
|
|
125
124
|
consuming `verify[]` output as evidence. **proceed** → flip to `closing`,
|
|
126
|
-
push, hand off; **redraft** → fix the
|
|
125
|
+
push, capture, hand off; **redraft** → fix the criteria, commit, re-eval;
|
|
127
126
|
**block** → take the blocked path below. Never hand off an unscored
|
|
128
127
|
branch.
|
|
129
128
|
|
|
@@ -141,20 +140,22 @@ branch.
|
|
|
141
140
|
|
|
142
141
|
The init envelope carries `remoteVerified` + `remoteProbe`. When
|
|
143
142
|
`remoteVerified` is `false`, flip to `agent::blocked` quoting
|
|
144
|
-
`remoteProbe.detail` and stop. A PR opened by
|
|
145
|
-
|
|
143
|
+
`remoteProbe.detail` and stop. A PR opened by `single-story-close.js` is
|
|
144
|
+
the only sanctioned landing.
|
|
146
145
|
|
|
147
146
|
## Your turn ends at a pushed branch (MUST)
|
|
148
147
|
|
|
149
148
|
You do **not** run close. Push `story-<storyId>` to `origin` — confirming
|
|
150
|
-
the remote ref moved —
|
|
149
|
+
the remote ref moved — **before** the credited capture, then return: the
|
|
150
|
+
capture is backgrounded, so its completion ends your turn, and a turn that
|
|
151
|
+
ends unpushed reads as unfinished work. The orchestrator runs
|
|
151
152
|
`single-story-close.js` in its own session, serialized against your
|
|
152
|
-
siblings. Do not open the PR, flip `agent::done`, or spawn a child to
|
|
153
|
-
|
|
153
|
+
siblings. Do not open the PR, flip `agent::done`, or spawn a child to
|
|
154
|
+
close for you. If the push fails, take the blocked path above.
|
|
154
155
|
|
|
155
156
|
## Return contract — the hand-off report
|
|
156
157
|
|
|
157
158
|
A short, literal hand-off your caller can act on: Story id, `workCwd`,
|
|
158
|
-
branch, pushed head SHA, self-eval verdict, `verify[]` evidence. Say
|
|
159
|
-
|
|
159
|
+
branch, pushed head SHA, self-eval verdict, `verify[]` evidence. Say the
|
|
160
|
+
branch is pushed and unclosed. Never hand-compose a terminal envelope —
|
|
160
161
|
inventing one makes an unlanded Story look landed.
|
|
@@ -24,7 +24,4 @@ Self-check your change against this lens's concerns before you ship:
|
|
|
24
24
|
- [ ] Media alternatives
|
|
25
25
|
- [ ] Contrast where statically derivable
|
|
26
26
|
- [ ] Raw-element census
|
|
27
|
-
- [ ] Resolve the target from config — never a hardcoded URL.
|
|
28
|
-
- [ ] Sample routes from the navigability SSOT.
|
|
29
27
|
- [ ] Run an accessibility engine per sampled route.
|
|
30
|
-
- [ ] Median-of-3 or provisional.
|
|
@@ -28,8 +28,4 @@ Self-check your change against this lens's concerns before you ship:
|
|
|
28
28
|
- [ ] Typography and spacing
|
|
29
29
|
- [ ] Coverage — is anything exercised at a non-desktop viewport?
|
|
30
30
|
- [ ] Effectiveness — does that exercise assert anything mobile-specific?
|
|
31
|
-
- [ ] Resolve the target from config — never a hardcoded URL.
|
|
32
|
-
- [ ] Sample routes from the navigability SSOT.
|
|
33
31
|
- [ ] Drive two form factors per route.
|
|
34
|
-
- [ ] Median-of-3 or provisional.
|
|
35
|
-
- [ ] Leave the viewport as you found it.
|
|
@@ -75,7 +75,8 @@
|
|
|
75
75
|
],
|
|
76
76
|
"memoryPool": {
|
|
77
77
|
"staleAfterDays": 30,
|
|
78
|
-
"growthDelta": 25
|
|
78
|
+
"growthDelta": 25,
|
|
79
|
+
"indexByteCeiling": 24576
|
|
79
80
|
},
|
|
80
81
|
"failOnSharedEditors": false,
|
|
81
82
|
"requireExplicitCrossStoryDeps": false,
|
|
@@ -353,7 +354,8 @@
|
|
|
353
354
|
},
|
|
354
355
|
"autoMerge": "trust-ci",
|
|
355
356
|
"blockOnAdvisoryFailure": true,
|
|
356
|
-
"advisoryAllowlist": []
|
|
357
|
+
"advisoryAllowlist": [],
|
|
358
|
+
"rerunAdvisory": 0
|
|
357
359
|
},
|
|
358
360
|
"routing": {
|
|
359
361
|
"roleScopedAgents": true,
|
|
@@ -130,6 +130,7 @@ Inputs to `/mandrel-plan`: risk escalation heuristics, ceremony-lite routing, th
|
|
|
130
130
|
| `memoryPool` | No | `object` | — | Thresholds for the memory-hygiene advisory `/mandrel-plan` surfaces at Gate #1. Advisory only: it recommends `/memory-consolidate` and never gates, reroutes, or mutates the memory pool. |
|
|
131
131
|
| `memoryPool.staleAfterDays` | No | `integer` | `30` | Recommend a consolidation pass once the pool's stamp is older than this many days. Default 30. |
|
|
132
132
|
| `memoryPool.growthDelta` | No | `integer` | `25` | Recommend a consolidation pass once this many entries have been written since the last one. Measured against the entry count the last pass stamped, so a stamp predating that field leaves growth unmeasured and only the age threshold applies. Default 25. |
|
|
133
|
+
| `memoryPool.indexByteCeiling` | No | `integer` | `24576` | Recommend a consolidation pass once the pool's `MEMORY.md` index exceeds this many bytes. Independent of the age and growth thresholds: the harness truncates the index it loads into each session at its own byte cap, so an oversized index is a loss already happening — every entry listed after the cut is invisible — rather than a hygiene forecast. Default 24576, the harness cap itself. |
|
|
133
134
|
| `failOnSharedEditors` | No | `boolean` | `false` | When true, upgrade shared-editor conflict findings to hard errors (default false — advisory soft findings only). |
|
|
134
135
|
| `requireExplicitCrossStoryDeps` | No | `boolean` | `false` | When true, upgrade implicit cross-Story dependency findings to hard errors (default false — advisory soft findings only). |
|
|
135
136
|
| `crossCuttingRegistries` | No | `string[]` or `{ append?, prepend? }` | `["lib/orchestration/lifecycle/listeners/index.js","**/listeners/index.js","**/handlers/index.js"]` | Registry path patterns whose concurrent edits across Stories are flagged as conflicts. Defaults to the framework listener/handler index patterns when omitted. |
|
|
@@ -330,6 +331,7 @@ Everything `/mandrel-deliver` and `single-story-close` consume: execution timeou
|
|
|
330
331
|
| `ci.autoMerge` | No | `"trust-ci"` \| `"strict"` | `"trust-ci"` | Story #4356 (Epic #4355). Merge posture. 'trust-ci' (default) merges once required checks pass; 'strict' additionally requires a clean review gate. |
|
|
331
332
|
| `ci.blockOnAdvisoryFailure` | No | `boolean` | `true` | Story #5096. When true (default), delivery refuses to arm — and disarms — GitHub native auto-merge while a non-required (advisory) check is genuinely red on the PR head and GitHub reports the PR mergeable anyway (mergeStateStatus=UNSTABLE). `--auto` waits on REQUIRED contexts only, so without this a red advisory quality gate merges unattended. Set false to restore the pre-#5096 behaviour verbatim. |
|
|
332
333
|
| `ci.advisoryAllowlist` | No | `array<string>` | `[]` | Story #5096. Check-run names exempt from blockOnAdvisoryFailure — a red run whose name matches exactly never blocks arming. Matching is exact; an unnamed run can never match and always blocks. |
|
|
334
|
+
| `ci.rerunAdvisory` | No | `integer` | `0` | Story #5266. How many times close may re-run a failed advisory workflow run before blocking on it, per close invocation. Default 0: close spends no CI minutes and issues no GitHub mutation on an advisory red unless asked. At n > 0 the failed run(s) are re-run within that allowance and the merge wait re-polls inside its existing budget, landing or blocking on the re-run verdict. Overridden per invocation by --rerun-advisory <n>. |
|
|
333
335
|
| `routing` | No | `object` | — | v2 delivery-spawn routing: role-scoped boot contexts and maker-checker sampling. The v1 singleDelivery epic-route kill-switch was removed in Stage 6. |
|
|
334
336
|
| `routing.roleScopedAgents` | No | `boolean` | `true` | Epic #4478 (M7-B). Kill-switch for the role-scoped boot contexts. When true (default), a converted delivery spawn (`story-worker`, `acceptance-critic`) boots on its own `.claude/agents/<role>.md` system prompt instead of re-paying the full CLAUDE.md @-import closure. When false, every converted spawn falls back to `subagent_type: general-purpose` — the instant, code-rollback-free per-consumer revert, and the universal escape for hosts that ignore `.claude/agents/`. The fallback is the full-closure agent that ran before M7-B, so flipping it off never drops a gate. |
|
|
335
337
|
| `routing.freshCriticSampleRate` | No | `number` | `0.2` | Epic #4478 (M7-B, Part 2). Maker-checker sampling floor. Under the standard profile, a change set touching no sensitive path routes its acceptance clusters down the contract-identical inline critic path, but this fraction of them is still forced through a fresh-context critic so a low derived level never means zero independent checking. Clamped to [0, 1]; 0 disables the floor, 1 forces every cluster fresh. Consumed by resolveCeremonyForRisk (lib/orchestration/ceremony-routing.js). |
|
|
@@ -377,6 +377,12 @@
|
|
|
377
377
|
"minimum": 1,
|
|
378
378
|
"description": "Recommend a consolidation pass once this many entries have been written since the last one. Measured against the entry count the last pass stamped, so a stamp predating that field leaves growth unmeasured and only the age threshold applies. Default 25.",
|
|
379
379
|
"default": 25
|
|
380
|
+
},
|
|
381
|
+
"indexByteCeiling": {
|
|
382
|
+
"type": "integer",
|
|
383
|
+
"minimum": 1,
|
|
384
|
+
"description": "Recommend a consolidation pass once the pool's `MEMORY.md` index exceeds this many bytes. Independent of the age and growth thresholds: the harness truncates the index it loads into each session at its own byte cap, so an oversized index is a loss already happening — every entry listed after the cut is invisible — rather than a hygiene forecast. Default 24576, the harness cap itself.",
|
|
385
|
+
"default": 24576
|
|
380
386
|
}
|
|
381
387
|
},
|
|
382
388
|
"additionalProperties": false
|
|
@@ -1975,6 +1981,12 @@
|
|
|
1975
1981
|
},
|
|
1976
1982
|
"description": "Story #5096. Check-run names exempt from blockOnAdvisoryFailure — a red run whose name matches exactly never blocks arming. Matching is exact; an unnamed run can never match and always blocks.",
|
|
1977
1983
|
"default": []
|
|
1984
|
+
},
|
|
1985
|
+
"rerunAdvisory": {
|
|
1986
|
+
"type": "integer",
|
|
1987
|
+
"minimum": 0,
|
|
1988
|
+
"description": "Story #5266. How many times close may re-run a failed advisory workflow run before blocking on it, per close invocation. Default 0: close spends no CI minutes and issues no GitHub mutation on an advisory red unless asked. At n > 0 the failed run(s) are re-run within that allowance and the merge wait re-polls inside its existing budget, landing or blocking on the re-run verdict. Overridden per invocation by --rerun-advisory <n>.",
|
|
1989
|
+
"default": 0
|
|
1978
1990
|
}
|
|
1979
1991
|
},
|
|
1980
1992
|
"additionalProperties": false
|
|
@@ -2084,7 +2096,9 @@
|
|
|
2084
2096
|
"not": {
|
|
2085
2097
|
"pattern": "([;&|`]|\\$\\()"
|
|
2086
2098
|
},
|
|
2087
|
-
"minLength": 1
|
|
2099
|
+
"minLength": 1,
|
|
2100
|
+
"pattern": "^[a-z0-9][a-z0-9._-]*(?:\\/[a-z0-9][a-z0-9._-]*)+$",
|
|
2101
|
+
"description": "Tier-relative skill id, e.g. `stack/qa/acme-sso`: lowercase segments of letters, digits, `.`, `_` or `-`, at least two of them, separated by `/`. A traversal (`../..`), an absolute path, a backslash or an uppercase segment is rejected here rather than normalized."
|
|
2088
2102
|
}
|
|
2089
2103
|
},
|
|
2090
2104
|
"required": ["skill"],
|
|
@@ -50,13 +50,17 @@ import {
|
|
|
50
50
|
} from './lib/audit-to-stories/ledger-commit.js';
|
|
51
51
|
import {
|
|
52
52
|
parseAuditReports,
|
|
53
|
-
|
|
53
|
+
readSeverityTally,
|
|
54
54
|
} from './lib/audit-to-stories/parse-audit-md.js';
|
|
55
55
|
import { buildPlanSeedMarkdown } from './lib/audit-to-stories/seed-from-findings.js';
|
|
56
56
|
import { wireAuditStoryEdges } from './lib/audit-to-stories/wire-dependencies.js';
|
|
57
57
|
import { runAsCli } from './lib/cli-utils.js';
|
|
58
58
|
import { searchSemanticCandidates } from './lib/findings/semantic-issue-search.js';
|
|
59
|
-
import {
|
|
59
|
+
import {
|
|
60
|
+
normalizeSeverity,
|
|
61
|
+
SEVERITIES,
|
|
62
|
+
SEVERITY_RANK,
|
|
63
|
+
} from './lib/findings/severity.js';
|
|
60
64
|
import { Logger } from './lib/Logger.js';
|
|
61
65
|
import { parse as parseStoryBody } from './lib/story-body/story-body.js';
|
|
62
66
|
|
|
@@ -209,6 +213,11 @@ function auditReportFailures({
|
|
|
209
213
|
* hand-written report). `allowMissingTally` downgrades ONLY this kind to a
|
|
210
214
|
* warning, for an interactive `--scan` over legacy reports.
|
|
211
215
|
* - `tally-mismatch` — the report says one thing and the parse says another.
|
|
216
|
+
* - `duplicate-tally` — the report declares the tally more than once, so there
|
|
217
|
+
* is no single number to check against. Adopting whichever line the scan
|
|
218
|
+
* reached first would compare the parse against an arbitrary one of two
|
|
219
|
+
* declarations, which is a cross-check in name only. `allowMissingTally`
|
|
220
|
+
* does NOT downgrade it: the report is contradictory, not merely old.
|
|
212
221
|
* - `unresolved-severity` — a finding parsed with no resolvable severity. It
|
|
213
222
|
* is a report defect, never an `unknown` group.
|
|
214
223
|
*
|
|
@@ -232,7 +241,11 @@ function crossCheckReports({ reports, findings, allowMissingTally }) {
|
|
|
232
241
|
for (const report of reports) {
|
|
233
242
|
const own = byReport.get(report.sourceReport) ?? [];
|
|
234
243
|
const parsed = comparableTally(own);
|
|
235
|
-
const
|
|
244
|
+
const {
|
|
245
|
+
tally: reported,
|
|
246
|
+
matches: tallyLines,
|
|
247
|
+
duplicate,
|
|
248
|
+
} = readSeverityTally(report.markdown);
|
|
236
249
|
const sourceReport = report.sourceReport;
|
|
237
250
|
const unresolved = own.filter((f) => !f.severity);
|
|
238
251
|
if (unresolved.length > 0) {
|
|
@@ -244,6 +257,16 @@ function crossCheckReports({ reports, findings, allowMissingTally }) {
|
|
|
244
257
|
titles: unresolved.map((f) => f.title),
|
|
245
258
|
});
|
|
246
259
|
}
|
|
260
|
+
if (duplicate) {
|
|
261
|
+
failures.push({
|
|
262
|
+
sourceReport,
|
|
263
|
+
kind: 'duplicate-tally',
|
|
264
|
+
reported: null,
|
|
265
|
+
parsed,
|
|
266
|
+
tallyLines,
|
|
267
|
+
});
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
247
270
|
if (!reported) {
|
|
248
271
|
const failure = {
|
|
249
272
|
sourceReport,
|
|
@@ -282,7 +305,7 @@ function missingTallyWarning(failure) {
|
|
|
282
305
|
*
|
|
283
306
|
* Pure: returns the message string so the caller owns the single `Logger.warn`.
|
|
284
307
|
*
|
|
285
|
-
* @param {Array<{ sourceReport: string, kind: string, reported: object|null, parsed: object, titles?: string[] }>} failures
|
|
308
|
+
* @param {Array<{ sourceReport: string, kind: string, reported: object|null, parsed: object, titles?: string[], tallyLines?: string[] }>} failures
|
|
286
309
|
* @returns {string}
|
|
287
310
|
*/
|
|
288
311
|
function reportFailureWarning(failures) {
|
|
@@ -290,7 +313,13 @@ function reportFailureWarning(failures) {
|
|
|
290
313
|
const titles = f.titles?.length
|
|
291
314
|
? ` findings=${f.titles.map((t) => `"${t}"`).join(', ')}`
|
|
292
315
|
: '';
|
|
293
|
-
|
|
316
|
+
// A duplicate names the competing lines rather than a tally: there is no
|
|
317
|
+
// single `reported` number to print, and "which two lines" is the whole
|
|
318
|
+
// remedy.
|
|
319
|
+
const declared = f.tallyLines?.length
|
|
320
|
+
? ` declared=${f.tallyLines.map((t) => `"${t}"`).join(' | ')}`
|
|
321
|
+
: '';
|
|
322
|
+
return ` - ${f.sourceReport} [${f.kind}] reported=${formatTally(f.reported)} parsed=${formatTally(f.parsed)}${titles}${declared}`;
|
|
294
323
|
});
|
|
295
324
|
return [
|
|
296
325
|
`audit report cross-check FAILED for ${failures.length} report(s) — the declared severity tally does not match the parsed findings:`,
|
|
@@ -359,6 +388,34 @@ function normaliseIssueHit(hit) {
|
|
|
359
388
|
};
|
|
360
389
|
}
|
|
361
390
|
|
|
391
|
+
/**
|
|
392
|
+
* Walk the list endpoint once per label and merge the pages into one
|
|
393
|
+
* deduplicated, normalised issue list.
|
|
394
|
+
*
|
|
395
|
+
* `labels` is an OR across the run's lenses, which the REST list endpoint
|
|
396
|
+
* cannot express in one query (its `labels` parameter is an AND), so one call
|
|
397
|
+
* per label is the narrowest honest read. Each is a paginated **list**, not a
|
|
398
|
+
* search — a different, far larger rate-limit budget.
|
|
399
|
+
*
|
|
400
|
+
* @param {object} provider
|
|
401
|
+
* @param {string[]} labels
|
|
402
|
+
* @returns {Promise<Array<object>>}
|
|
403
|
+
*/
|
|
404
|
+
async function listIssuesForLabels(provider, labels) {
|
|
405
|
+
const seen = new Map();
|
|
406
|
+
for (const label of labels) {
|
|
407
|
+
const issues = await provider.listIssuesByLabel({
|
|
408
|
+
state: 'all',
|
|
409
|
+
labels: label,
|
|
410
|
+
});
|
|
411
|
+
for (const raw of issues ?? []) {
|
|
412
|
+
const hit = normaliseIssueHit(raw);
|
|
413
|
+
if (!seen.has(hit.number)) seen.set(hit.number, hit);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return [...seen.values()];
|
|
417
|
+
}
|
|
418
|
+
|
|
362
419
|
/**
|
|
363
420
|
* The two read ports the dedupe module consumes, adapted off the provider's
|
|
364
421
|
* one full-text `searchIssues` call: `findIssuesByFingerprint(sha)` for the
|
|
@@ -368,7 +425,8 @@ function normaliseIssueHit(hit) {
|
|
|
368
425
|
*
|
|
369
426
|
* @param {object} provider
|
|
370
427
|
* @param {{ owner: string, repo: string }} coords
|
|
371
|
-
* @returns {{ findIssuesByFingerprint: Function,
|
|
428
|
+
* @returns {{ findIssuesByFingerprint: Function, listAuditIssues: Function,
|
|
429
|
+
* searchCandidates: Function }}
|
|
372
430
|
*/
|
|
373
431
|
function buildDedupPorts(provider, { owner, repo }) {
|
|
374
432
|
return {
|
|
@@ -376,6 +434,21 @@ function buildDedupPorts(provider, { owner, repo }) {
|
|
|
376
434
|
const hits = await provider.searchIssues({ query: sha, owner, repo });
|
|
377
435
|
return (hits ?? []).map(normaliseIssueHit);
|
|
378
436
|
},
|
|
437
|
+
/**
|
|
438
|
+
* List every Issue carrying one of the run's `audit::*` labels, once, off
|
|
439
|
+
* the REST list endpoint. The dedup module indexes the result and answers
|
|
440
|
+
* every exact-fingerprint lookup from it, so the rate-limited search API is
|
|
441
|
+
* spent only on findings it has never seen. A provider without the list
|
|
442
|
+
* port yields `null`, which returns dedup to the per-finding search path
|
|
443
|
+
* rather than silently skipping it.
|
|
444
|
+
*
|
|
445
|
+
* @param {string[]} labels
|
|
446
|
+
* @returns {Promise<Array<object>|null>}
|
|
447
|
+
*/
|
|
448
|
+
async listAuditIssues(labels) {
|
|
449
|
+
if (typeof provider.listIssuesByLabel !== 'function') return null;
|
|
450
|
+
return listIssuesForLabels(provider, labels);
|
|
451
|
+
},
|
|
379
452
|
async searchCandidates(finding) {
|
|
380
453
|
// Wire the shared semantic search onto the provider's full-text
|
|
381
454
|
// issue search (open + closed) so route-finding's Stage-1 pass runs.
|
|
@@ -667,6 +740,7 @@ async function buildPlan(
|
|
|
667
740
|
groups,
|
|
668
741
|
provider,
|
|
669
742
|
searchCandidates: provider.searchCandidates,
|
|
743
|
+
listAuditIssues: provider.listAuditIssues,
|
|
670
744
|
});
|
|
671
745
|
classifications = result.classifications;
|
|
672
746
|
summary = result.summary;
|
|
@@ -1117,6 +1191,73 @@ export const __testing = {
|
|
|
1117
1191
|
* }} [deps]
|
|
1118
1192
|
* @returns {Promise<void>}
|
|
1119
1193
|
*/
|
|
1194
|
+
/**
|
|
1195
|
+
* The one-line `--ledger-commit` outcome, for stderr.
|
|
1196
|
+
*
|
|
1197
|
+
* Names the branch and the PR on success — the two things an operator needs to
|
|
1198
|
+
* go look at it — and the skip reason otherwise, because "nothing happened" and
|
|
1199
|
+
* "the ledger was already clean" are different facts and only one of them is
|
|
1200
|
+
* fine.
|
|
1201
|
+
*
|
|
1202
|
+
* @param {{ committed?: boolean, reason?: string, branch?: string,
|
|
1203
|
+
* prUrl?: string|null, resumed?: boolean, ledgerPath?: string }} [result]
|
|
1204
|
+
* @returns {string}
|
|
1205
|
+
*/
|
|
1206
|
+
function describeLedgerCommit(result) {
|
|
1207
|
+
return result?.committed
|
|
1208
|
+
? ledgerCommittedLine(result)
|
|
1209
|
+
: ledgerSkippedLine(result);
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
/**
|
|
1213
|
+
* The success half: the branch, whether it resumed a half-finished one, and
|
|
1214
|
+
* the PR to go look at.
|
|
1215
|
+
* @param {object} result
|
|
1216
|
+
* @returns {string}
|
|
1217
|
+
*/
|
|
1218
|
+
function ledgerCommittedLine(result) {
|
|
1219
|
+
const resumed = result.resumed ? ' (resumed an unpushed ledger branch)' : '';
|
|
1220
|
+
const pr = result.prUrl ?? '(no URL reported by gh)';
|
|
1221
|
+
return `--ledger-commit: pushed ${result.branch}${resumed} and opened ${pr}.`;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
/**
|
|
1225
|
+
* The skip half. It names the ledger file, because the fact that matters is
|
|
1226
|
+
* which state is still only in the working tree.
|
|
1227
|
+
* @param {object} [result]
|
|
1228
|
+
* @returns {string}
|
|
1229
|
+
*/
|
|
1230
|
+
function ledgerSkippedLine(result) {
|
|
1231
|
+
const reason = result?.reason ?? 'no result';
|
|
1232
|
+
const ledgerPath = result?.ledgerPath ?? 'the ledger';
|
|
1233
|
+
return `--ledger-commit: skipped (${reason}) — ${ledgerPath} was not committed.`;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
/**
|
|
1237
|
+
* Validate `--severity` against the canonical scale, failing on a value the
|
|
1238
|
+
* filter would silently ignore.
|
|
1239
|
+
*
|
|
1240
|
+
* `meetsSeverity` reads an unknown threshold as rank `0`, so a typo — the
|
|
1241
|
+
* classic being `--severity Hgh` — quietly widened the run to every finding
|
|
1242
|
+
* instead of narrowing it. On an unattended sweep that is the difference
|
|
1243
|
+
* between filing a batch and filing the backlog, with nothing on stderr to say
|
|
1244
|
+
* so. An absent flag stays absent: the floor then resolves from config.
|
|
1245
|
+
*
|
|
1246
|
+
* @param {string|undefined} raw
|
|
1247
|
+
* @returns {string|undefined} the canonical level.
|
|
1248
|
+
*/
|
|
1249
|
+
function validateSeverityFlag(raw) {
|
|
1250
|
+
if (raw === undefined || raw === null || raw === '') return undefined;
|
|
1251
|
+
if (String(raw).toLowerCase() === 'all') return 'all';
|
|
1252
|
+
const level = normalizeSeverity(String(raw), null);
|
|
1253
|
+
if (!level) {
|
|
1254
|
+
throw new Error(
|
|
1255
|
+
`[audit-to-stories] --severity "${raw}" is not a severity. Accepted: ${SEVERITIES.join(', ')} (or "all").`,
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
return level;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1120
1261
|
export async function runAuditToStories(
|
|
1121
1262
|
argv = process.argv.slice(2),
|
|
1122
1263
|
deps = {},
|
|
@@ -1156,6 +1297,8 @@ export async function runAuditToStories(
|
|
|
1156
1297
|
strict: false,
|
|
1157
1298
|
});
|
|
1158
1299
|
|
|
1300
|
+
values.severity = validateSeverityFlag(values.severity);
|
|
1301
|
+
|
|
1159
1302
|
const json = (value) => JSON.stringify(value, null, 2);
|
|
1160
1303
|
|
|
1161
1304
|
const runAutoSummary = async () =>
|
|
@@ -1175,7 +1318,15 @@ export async function runAuditToStories(
|
|
|
1175
1318
|
// findings, so the PR attempt is the last thing the run does (Story #5145).
|
|
1176
1319
|
const commitLedger = async () => {
|
|
1177
1320
|
if (!values['ledger-commit'] || values['dry-run']) return;
|
|
1178
|
-
|
|
1321
|
+
// Say what happened. The tail used to run silently, so an operator could
|
|
1322
|
+
// not tell a ledger PR from a skip without going to look for the branch —
|
|
1323
|
+
// and a skip is the outcome that matters, because it means the sweep's
|
|
1324
|
+
// memory is still only in the working tree.
|
|
1325
|
+
Logger.warn(
|
|
1326
|
+
describeLedgerCommit(
|
|
1327
|
+
await runLedgerCommitImpl({ ledgerPath: values.ledger }),
|
|
1328
|
+
),
|
|
1329
|
+
);
|
|
1179
1330
|
};
|
|
1180
1331
|
|
|
1181
1332
|
const scanPlan = () =>
|