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.
@@ -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
- gitSync(cwd, 'push', '-u', 'origin', storyBranch);
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
- pushStoryBranch({ cwd, storyBranch, gitSync, progress });
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-02T11:35:53.522Z",
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": 7,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mandrel",
3
- "version": "2.30.0",
3
+ "version": "2.31.0",
4
4
  "description": "Claude Code-first opinionated workflow framework: instructions, skills, rules, and SDLC workflows that govern AI coding assistants.",
5
5
  "files": [
6
6
  ".agents/",