mandrel 2.30.0 → 2.31.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/rules/ci-remediation.md +44 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +17 -2
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +7 -1
- package/.agents/skills/skills.index.json +2 -2
- package/.agents/skills/stack/qa/playwright/SKILL.md +48 -0
- package/docs/CHANGELOG.md +7 -0
- package/package.json +1 -1
|
@@ -51,6 +51,7 @@ the two options above. Name the verdict you reached in the `friction` comment.
|
|
|
51
51
|
| **defect-in-diff** | The failure reproduces on the branch and not on an unmodified `main` | Option 1 — fix at source |
|
|
52
52
|
| **pre-existing** | The same check fails on an unmodified `main` too | Option 2 — file `meta::framework-gap`; remediate here only if it blocks this delivery |
|
|
53
53
|
| **capacity** | Proven exhaustion of a runner resource, not a property of the diff (see below) | Option 2 — file `meta::framework-gap` **and** escalate to the operator |
|
|
54
|
+
| **unreproducible-tier** | The tier cannot be exercised in this sandbox at all, proven by an attempted attach (see below) | Option 2 — file `meta::framework-gap` **and** escalate on first encounter |
|
|
54
55
|
|
|
55
56
|
### The `capacity` verdict
|
|
56
57
|
|
|
@@ -82,10 +83,46 @@ the operator, who owns the runner pool. Do not sit in a retry loop waiting for
|
|
|
82
83
|
capacity to return.
|
|
83
84
|
|
|
84
85
|
**Rerunning a failed job to reach green stays forbidden under every verdict,
|
|
85
|
-
`capacity` included.** The verdict changes who owns the fix and where it is
|
|
86
|
+
`capacity` and `unreproducible-tier` included.** The verdict changes who owns the fix and where it is
|
|
86
87
|
filed; it never licenses a re-run, and it is not a route to a green bar. A
|
|
87
88
|
capacity-blocked delivery ends `agent::blocked` — not merged.
|
|
88
89
|
|
|
90
|
+
### The `unreproducible-tier` verdict
|
|
91
|
+
|
|
92
|
+
A check can fail on a tier the sandbox cannot run at all — most often a
|
|
93
|
+
browser suite whose Playwright `webServer` block supervises a dev server the
|
|
94
|
+
local process manager daemonizes, which aborts the run with
|
|
95
|
+
`Process from config.webServer exited early` before any test executes. The
|
|
96
|
+
failure is a property of the sandbox's ability to *host* the suite, not of the
|
|
97
|
+
diff.
|
|
98
|
+
|
|
99
|
+
This is the same structural hole the `capacity` verdict was added to fill, one
|
|
100
|
+
step earlier in the loop. Without it the honest reading is `flaky`, which routes
|
|
101
|
+
to Option 1 — and fix-at-source requires reproducing the failure, which is the
|
|
102
|
+
one thing that cannot be done. The agent then spends the full timebox
|
|
103
|
+
rediscovering that before escalating anyway, and any fix it does author is
|
|
104
|
+
written blind against a tier it never ran.
|
|
105
|
+
|
|
106
|
+
**Unreproducible must be proven, not inferred.** "The suite did not run for me"
|
|
107
|
+
is not the verdict — it is the symptom every misconfiguration produces. Cite
|
|
108
|
+
both:
|
|
109
|
+
|
|
110
|
+
- **The attempted attach.** Work the attach-don't-boot seam in the
|
|
111
|
+
[`playwright`](../skills/stack/qa/playwright/SKILL.md) skill — boot the server
|
|
112
|
+
out-of-band, point the suite at the running origin, set `reuseExistingServer`
|
|
113
|
+
— and name which step failed and how. A tier that runs once attached was never
|
|
114
|
+
unreproducible.
|
|
115
|
+
- **The observed signature.** The verbatim line the runner aborted on, so a
|
|
116
|
+
later reader can tell a lifetime-ownership mismatch from a genuine boot
|
|
117
|
+
failure in the app under test.
|
|
118
|
+
|
|
119
|
+
Absent both readings the verdict is unavailable and the failure routes as it did
|
|
120
|
+
before. On the verdict: file the `meta::framework-gap` issue with the run link,
|
|
121
|
+
the failure signature, and the attach attempt; flip the Story to
|
|
122
|
+
`agent::blocked` with a `friction` comment naming the verdict; and hand back to
|
|
123
|
+
the operator, who owns the sandbox. Do not author a fix for a tier you could not
|
|
124
|
+
run — a blind fix to a suite nobody exercised is how the gap compounds.
|
|
125
|
+
|
|
89
126
|
## Verifier
|
|
90
127
|
|
|
91
128
|
The check is resolved only when it is **green with zero reruns of the failed
|
|
@@ -131,3 +168,9 @@ operator under **any** of:
|
|
|
131
168
|
signature) and escalate on the first encounter rather than burning iterations
|
|
132
169
|
trying to code around it. A proven-capacity failure is this case: reach the
|
|
133
170
|
`capacity` verdict above and escalate on the first encounter.
|
|
171
|
+
- **Unrunnable tier → escalate immediately.** A tier the sandbox cannot host at
|
|
172
|
+
all is this case too: work the attach seam once, reach the
|
|
173
|
+
`unreproducible-tier` verdict above with its two readings, and escalate on the
|
|
174
|
+
**first encounter**. The 30-minute timebox is a ceiling here, never a budget
|
|
175
|
+
to spend — every minute past the failed attach buys nothing, because no
|
|
176
|
+
iteration can make an unhostable suite run.
|
|
@@ -15,6 +15,18 @@
|
|
|
15
15
|
* added because it would re-ask a question the push exit code already
|
|
16
16
|
* answered authoritatively.
|
|
17
17
|
*
|
|
18
|
+
* Issue #4990 — the push runs in the Story worktree, not the main
|
|
19
|
+
* checkout. `core.hooksPath` is the relative `.husky/_`, so the
|
|
20
|
+
* invocation directory is what selects which tree `pre-push` resolves
|
|
21
|
+
* and measures. Pushing from the main checkout therefore validated a
|
|
22
|
+
* tree other than the one being sent — false red when that checkout sat
|
|
23
|
+
* on unrelated work, and worse, false green whenever it happened to be
|
|
24
|
+
* clean. `worktreePath` falls back to `cwd` because single-tree mode and
|
|
25
|
+
* a disabled `delivery.worktreeIsolation` both legitimately have no
|
|
26
|
+
* worktree, and there the main checkout IS the tree being pushed. The
|
|
27
|
+
* two sibling phases resolve their tree the same way
|
|
28
|
+
* (`base-sync.js`, `close-validation.js`).
|
|
29
|
+
*
|
|
18
30
|
* `gitSync` is accepted as an injected dependency rather than statically
|
|
19
31
|
* imported so the caller's (cache-busted) binding wins. The
|
|
20
32
|
* `single-story-close.js` orchestrator owns the static import; test
|
|
@@ -29,6 +41,7 @@ import { gitSync as defaultGitSync } from '../../../git-utils.js';
|
|
|
29
41
|
*
|
|
30
42
|
* @param {{
|
|
31
43
|
* cwd: string,
|
|
44
|
+
* worktreePath?: string|null,
|
|
32
45
|
* storyBranch: string,
|
|
33
46
|
* gitSync?: typeof defaultGitSync,
|
|
34
47
|
* progress: (tag: string, msg: string) => void,
|
|
@@ -36,6 +49,7 @@ import { gitSync as defaultGitSync } from '../../../git-utils.js';
|
|
|
36
49
|
*/
|
|
37
50
|
export function pushStoryBranch({
|
|
38
51
|
cwd,
|
|
52
|
+
worktreePath = null,
|
|
39
53
|
storyBranch,
|
|
40
54
|
gitSync = defaultGitSync,
|
|
41
55
|
progress,
|
|
@@ -46,8 +60,9 @@ export function pushStoryBranch({
|
|
|
46
60
|
// before this point, but `--skip-validation` skips that chain, and the
|
|
47
61
|
// bypass then left nothing running at all. `pre-push` is the backstop,
|
|
48
62
|
// and it only became reachable once hooks were materialized into
|
|
49
|
-
// worktrees — which is where every Story branch is built
|
|
50
|
-
|
|
63
|
+
// worktrees — which is where every Story branch is built, and where the
|
|
64
|
+
// cwd below now resolves so the hook reads that tree.
|
|
65
|
+
gitSync(worktreePath ?? cwd, 'push', '-u', 'origin', storyBranch);
|
|
51
66
|
progress('GIT', `✅ Pushed ${storyBranch}.`);
|
|
52
67
|
} catch (err) {
|
|
53
68
|
throw new Error(
|
|
@@ -201,6 +201,7 @@ async function runPrePushPhases({
|
|
|
201
201
|
|
|
202
202
|
async function openAndReviewPr({
|
|
203
203
|
cwd,
|
|
204
|
+
worktreePath,
|
|
204
205
|
story,
|
|
205
206
|
storyId,
|
|
206
207
|
storyBranch,
|
|
@@ -211,7 +212,11 @@ async function openAndReviewPr({
|
|
|
211
212
|
setPhase = () => {},
|
|
212
213
|
}) {
|
|
213
214
|
setPhase('push');
|
|
214
|
-
|
|
215
|
+
// Issue #4990 — push from the Story worktree so `pre-push` measures the
|
|
216
|
+
// tree being sent. `gh` and the review's ref-based diffs below keep the
|
|
217
|
+
// caller's `cwd`: they read the shared `.git`, so they resolve identically
|
|
218
|
+
// from either tree.
|
|
219
|
+
pushStoryBranch({ cwd, worktreePath, storyBranch, gitSync, progress });
|
|
215
220
|
setPhase('pull-request');
|
|
216
221
|
const { url: prUrl, alreadyMerged } = await ensurePullRequestWith({
|
|
217
222
|
cwd,
|
|
@@ -729,6 +734,7 @@ async function runClosePipeline({
|
|
|
729
734
|
() =>
|
|
730
735
|
openAndReviewPr({
|
|
731
736
|
cwd: options.cwd,
|
|
737
|
+
worktreePath,
|
|
732
738
|
story,
|
|
733
739
|
storyId: options.storyId,
|
|
734
740
|
storyBranch,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-08-
|
|
2
|
+
"generatedAt": "2026-08-05T00:59:17.768Z",
|
|
3
3
|
"generator": "generate-skills-index.js@1",
|
|
4
4
|
"skills": [
|
|
5
5
|
{
|
|
@@ -148,7 +148,7 @@
|
|
|
148
148
|
"category": "qa",
|
|
149
149
|
"path": ".agents/skills/stack/qa/playwright/SKILL.md",
|
|
150
150
|
"description": "Robust E2E browser testing with Playwright. Use when writing browser-driven tests — leverage auto-waiting (no `waitForTimeout`), prefer user-visible locators (`getByRole`, `getByText`, `getByLabel`) over CSS/XPath, reuse `storageState` for auth, and enable trace-on-first-retry for CI debugging.",
|
|
151
|
-
"policyCapsuleBullets":
|
|
151
|
+
"policyCapsuleBullets": 8,
|
|
152
152
|
"allowedTools": null,
|
|
153
153
|
"vendor": "playwright"
|
|
154
154
|
},
|
|
@@ -19,3 +19,51 @@ vendor: playwright
|
|
|
19
19
|
- Write tests independent of one another so they run in parallel; clean up shared state in fixtures, not afterwards.
|
|
20
20
|
- Enable `trace: 'on-first-retry'` (or `'retain-on-failure'`) so CI failures are debuggable in the Trace Viewer.
|
|
21
21
|
- Use a unique data set per test run, or tear down state explicitly, to prevent cross-test contamination.
|
|
22
|
+
- Never let Playwright own the lifetime of a dev server it did not start: boot the server out-of-band, point the suite at the running origin, and set `reuseExistingServer` so `webServer` only probes readiness.
|
|
23
|
+
|
|
24
|
+
## Running a `webServer`-backed suite outside CI
|
|
25
|
+
|
|
26
|
+
Playwright's `webServer` block **watches the process it spawned**. That
|
|
27
|
+
assumption holds for a dev server that stays in the foreground, and breaks for
|
|
28
|
+
any manager that daemonizes one — the foreground process exits `0` while the
|
|
29
|
+
server keeps serving, Playwright reads the exit as a crash, and the run aborts
|
|
30
|
+
before a single test executes:
|
|
31
|
+
|
|
32
|
+
```text
|
|
33
|
+
Process from config.webServer exited early
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Read that line as a **lifetime-ownership mismatch, not a flake**. It reproduces
|
|
37
|
+
on every invocation, clean tree or not, and no amount of retrying, tree-cleaning
|
|
38
|
+
or timeout-raising changes it. Agent sandboxes and IDE harnesses commonly manage
|
|
39
|
+
dev servers this way (`Dev server already running at … (pid N)`), so an agent
|
|
40
|
+
meets this far more often than a developer does.
|
|
41
|
+
|
|
42
|
+
### Attach, don't boot
|
|
43
|
+
|
|
44
|
+
Invert the ownership instead of fighting it — the manager owns the process,
|
|
45
|
+
Playwright owns only the probe:
|
|
46
|
+
|
|
47
|
+
1. **Boot the server out-of-band** through whatever manages it, and confirm it
|
|
48
|
+
is serving. Its lifetime is now the manager's concern, not the runner's.
|
|
49
|
+
2. **Point the suite at the already-running origin** — set the config's
|
|
50
|
+
`baseURL` (or the `webServer.url` the block probes) to that origin, via the
|
|
51
|
+
project's own environment seam rather than an edit to committed config.
|
|
52
|
+
3. **Set `reuseExistingServer: true`** so Playwright probes the URL, finds it
|
|
53
|
+
live, and never spawns or supervises a process of its own.
|
|
54
|
+
|
|
55
|
+
This is the same convention the QA harness already encodes as
|
|
56
|
+
`qa.environments[].baseUrl`: attach to a running origin, never boot one. A suite
|
|
57
|
+
run this way exercises identical browser behavior — only the process supervision
|
|
58
|
+
differs.
|
|
59
|
+
|
|
60
|
+
### When no attachable origin exists
|
|
61
|
+
|
|
62
|
+
Some apps genuinely cannot be reached this way — the server is unreachable from
|
|
63
|
+
the sandbox, or the suite depends on a build step the sandbox cannot run. Do
|
|
64
|
+
**not** burn a timebox rediscovering that. Record the observed signature, state
|
|
65
|
+
which of the three steps above failed, and escalate on the first encounter:
|
|
66
|
+
that evidence is exactly what the `unreproducible-tier` verdict in
|
|
67
|
+
[`ci-remediation.md`](../../../../rules/ci-remediation.md) requires, and it is
|
|
68
|
+
the only verdict that lets an unrunnable tier route somewhere other than a dead
|
|
69
|
+
end.
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,13 @@ All notable changes to this project will be documented in this file.
|
|
|
15
15
|
-->
|
|
16
16
|
<!-- markdownlint-disable-file MD004 MD012 MD037 -->
|
|
17
17
|
|
|
18
|
+
## [2.31.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.30.0...mandrel-v2.31.0) (2026-08-05)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
* **qa:** add an attach-don't-boot seam and an unreproducible-tier verdict (refs [#4994](https://github.com/dsj1984/mandrel/issues/4994)) ([#4995](https://github.com/dsj1984/mandrel/issues/4995)) ([dac7986](https://github.com/dsj1984/mandrel/commit/dac79868a305e34db01005f06f638fc384c21c17)), closes [#4993](https://github.com/dsj1984/mandrel/issues/4993)
|
|
24
|
+
|
|
18
25
|
## [2.30.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.29.0...mandrel-v2.30.0) (2026-08-04)
|
|
19
26
|
|
|
20
27
|
|
package/package.json
CHANGED