mandrel 1.78.0 → 1.80.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.
@@ -107,6 +107,18 @@ rejected by `pre-push` hooks):
107
107
  4. **Never bypass hooks**: Do not use `--no-verify`, `--no-gpg-sign`, or
108
108
  other hook-skipping flags unless the operator explicitly authorizes it.
109
109
  If a hook fails, investigate the underlying cause.
110
+ - **Known false-negative signature**: a `pre-push`/`pre-commit` failure
111
+ whose message is a _zero-match_ error (e.g. Biome's
112
+ `No files were processed in the specified paths`) rather than a
113
+ reported violation, combined with an agent CWD under a harness-managed
114
+ worktree path a consumer's lint config ignores (e.g.
115
+ `.claude/worktrees/<name>/` against a `files.includes` glob like
116
+ `"!**/.claude"`), is a **consumer-tooling gap**, not a real lint
117
+ failure. It does not authorize `--no-verify`. See
118
+ [`worktree-lifecycle.md` § Harness-worktree ⇄ consumer-lint-ignore interaction](../workflows/helpers/worktree-lifecycle.md#harness-worktree-consumer-lint-ignore-interaction-story-152)
119
+ for the recognition signature and the sanctioned consumer-side fix
120
+ (`--no-errors-on-unmatched` or equivalent) before escalating via
121
+ `agent::blocked`.
110
122
 
111
123
  ## Meta Labels (Retrospective Signal Routing)
112
124
 
@@ -22,9 +22,13 @@ import { applyBudget } from '../../planning-context-budget.js';
22
22
 
23
23
  export function buildDecomposerSystemPrompt(
24
24
  heuristics = [],
25
- { maxTickets, maxTokenBudget } = {},
25
+ { maxTickets, maxTokenBudget, epicId } = {},
26
26
  ) {
27
- const base = renderDecomposerSystemPrompt({ maxTickets, maxTokenBudget });
27
+ const base = renderDecomposerSystemPrompt({
28
+ maxTickets,
29
+ maxTokenBudget,
30
+ epicId,
31
+ });
28
32
  const heuristicsStr =
29
33
  heuristics.length > 0
30
34
  ? `### RISK HEURISTICS (planning metadata if any apply):\n- ${heuristics.join('\n- ')}`
@@ -121,6 +125,7 @@ export async function buildDecompositionContext(
121
125
  const systemPrompt = buildDecomposerSystemPrompt(heuristics, {
122
126
  maxTickets,
123
127
  maxTokenBudget,
128
+ epicId,
124
129
  });
125
130
 
126
131
  const budgeted = applyBudget(
@@ -80,6 +80,7 @@
80
80
  * issue with an empty body.
81
81
  */
82
82
 
83
+ import { composeStoryBody } from '../../providers/github/tickets.js';
83
84
  import { assertPlanLabelAllowList } from './epic-spec-reconciler-discriminator.js';
84
85
  import {
85
86
  closeOp,
@@ -208,34 +209,6 @@ function stripFooter(body) {
208
209
  return value.replace(ORCHESTRATOR_FOOTER_RE, '').replace(/\s+$/, '');
209
210
  }
210
211
 
211
- /**
212
- * Render the canonical orchestrator footer (no leading newline). Format
213
- * matches the byte-stable shape that the cascade-reading consumers
214
- * (story-init, dispatcher, manifest, close-gate) parse line-anchored:
215
- *
216
- * ---
217
- * parent: #<parentId>
218
- * [Epic: #<epicId>] // only when epicId !== parentId
219
- *
220
- * [blocked by #<dep>] // one per dependency
221
- *
222
- * @param {{parentId: number, epicId?: number, dependencies?: number[]}} opts
223
- * @returns {string}
224
- */
225
- function renderFooter({ parentId, epicId, dependencies = [] }) {
226
- const lines = ['---', `parent: #${parentId}`];
227
- if (epicId !== undefined && epicId !== null && epicId !== parentId) {
228
- lines.push(`Epic: #${epicId}`);
229
- }
230
- if (dependencies.length > 0) {
231
- lines.push('');
232
- for (const dep of dependencies) {
233
- lines.push(`blocked by #${dep}`);
234
- }
235
- }
236
- return lines.join('\n');
237
- }
238
-
239
212
  /**
240
213
  * Compose the canonical orchestrator footer onto a spec body for non-epic
241
214
  * entities. Resolves `parentSlug`/`dependsOn` slugs against the running
@@ -246,12 +219,19 @@ function renderFooter({ parentId, epicId, dependencies = [] }) {
246
219
  * the YAML spec writes just the description, silently stripping
247
220
  * `parent: #N` / `Epic: #M` / `blocked by #X` and breaking the cascade.
248
221
  *
249
- * Story #3185 — the footer compose/strip logic is inlined here rather
250
- * than reused from the legacy Task-body renderer module. That renderer
251
- * was removed, so the diff engine carries its own footer shape. The shape
252
- * is byte-identical to the legacy renderer's `parent: #<n>` /
253
- * `Epic: #<m>` / `blocked by #<x>` output so cascade-readers continue
254
- * to parse it unchanged.
222
+ * Story #4300 — the footer rendering is single-sourced from
223
+ * `composeStoryBody` (`providers/github/tickets.js`), the same helper the
224
+ * CREATE path (`epic-spec-reconciler-apply.js` `provider.createTicket`)
225
+ * uses. Story #3185 previously inlined a parallel `renderFooter` here to
226
+ * avoid depending on the (now-removed) legacy Task-body renderer; that
227
+ * inlined copy silently diverged from `composeStoryBody` by gating the
228
+ * `Epic: #<id>` line on `epicId !== parentId` — a 3-tier-era condition
229
+ * that is always false under the 2-tier hierarchy (a Story's parent IS
230
+ * the Epic), so force re-decompose (`/plan --force`, which routes through
231
+ * this UPDATE path) silently dropped `Epic: #<id>` from every refreshed
232
+ * Story body and broke `story-init.js`'s hierarchy resolution. Importing
233
+ * `composeStoryBody` directly makes that divergence structurally
234
+ * impossible going forward.
255
235
  *
256
236
  * @param {{entity: string, parentSlug?: string|null, dependsOn?: string[]}} specEntity
257
237
  * @param {string} specBody
@@ -284,8 +264,7 @@ function composeBodyWithFooter(specEntity, specBody, ctx) {
284
264
  // included) or emits a canonical-form body. With the strip, the
285
265
  // function is idempotent against its own output.
286
266
  const head = stripFooter(specBody);
287
- const footer = renderFooter({ parentId, epicId, dependencies });
288
- return `${head}\n\n${footer}`;
267
+ return composeStoryBody({ body: head, parentId, epicId, dependencies });
289
268
  }
290
269
 
291
270
  /**
@@ -23,6 +23,15 @@ import {
23
23
  /**
24
24
  * Parse the `Epic: #N` and `parent: #N` references from a Story body.
25
25
  *
26
+ * Story #4300 (defense-in-depth): under the 2-tier hierarchy
27
+ * (Epic → Story) a Story's `parent: #N` marker always IS the parent
28
+ * Epic, so when the `Epic: #N` line is missing — e.g. a Story body
29
+ * refreshed by the reconciler's UPDATE op before Story #4300's
30
+ * single-sourced footer rendering landed — `epicId` falls back to the
31
+ * resolved `parentId` rather than reporting `null` and aborting
32
+ * delivery. A body that carries neither marker still resolves
33
+ * `epicId: null` (no parent to fall back to).
34
+ *
26
35
  * @param {string} body Raw Story body Markdown.
27
36
  * @returns {{ epicId: number|null, parentId: number|null }}
28
37
  */
@@ -30,10 +39,9 @@ export function resolveStoryHierarchy(body) {
30
39
  const source = body ?? '';
31
40
  const epicMatch = source.match(/(?:^\s*epic:\s*#(\d+))/im);
32
41
  const parentMatch = source.match(/(?:^\s*parent:\s*#(\d+))/im);
33
- return {
34
- epicId: epicMatch ? Number.parseInt(epicMatch[1], 10) : null,
35
- parentId: parentMatch ? Number.parseInt(parentMatch[1], 10) : null,
36
- };
42
+ const parentId = parentMatch ? Number.parseInt(parentMatch[1], 10) : null;
43
+ const epicId = epicMatch ? Number.parseInt(epicMatch[1], 10) : parentId;
44
+ return { epicId, parentId };
37
45
  }
38
46
 
39
47
  /**
@@ -36,8 +36,9 @@ import {
36
36
  export function renderDecomposerSystemPrompt({
37
37
  maxTickets = LIMITS_DEFAULTS.maxTickets,
38
38
  maxTokenBudget = LIMITS_DEFAULTS.maxTokenBudget,
39
+ epicId = null,
39
40
  } = {}) {
40
- return render2TierPrompt({ maxTickets, maxTokenBudget });
41
+ return render2TierPrompt({ maxTickets, maxTokenBudget, epicId });
41
42
  }
42
43
 
43
44
  /**
@@ -46,7 +47,7 @@ export function renderDecomposerSystemPrompt({
46
47
  * on the Story body so the executing agent has everything it needs in one
47
48
  * ticket. Thematic grouping lives as prose in the Epic body / Tech Spec.
48
49
  */
49
- function render2TierPrompt({ maxTickets, maxTokenBudget }) {
50
+ function render2TierPrompt({ maxTickets, maxTokenBudget, epicId = null }) {
50
51
  // Sizing thresholds are sourced from the single DEFAULT_TASK_SIZING constant
51
52
  // (ticket-validator-sizing.js) so the prompt and the validator cannot drift.
52
53
  const { softFiles, hardFiles, maxAcceptance, softAcceptanceCount } =
@@ -66,6 +67,13 @@ function render2TierPrompt({ maxTickets, maxTokenBudget }) {
66
67
  advisoryCaveat,
67
68
  newFileContract,
68
69
  } = AUTHORING_ALTITUDE_GUIDANCE;
70
+ // The namespaced AC-tag token the wave-0 BDD scaffold section below must
71
+ // require on every scaffolded scenario (Story #4301). When the Epic ID is
72
+ // known at render time, interpolate the concrete tag so the author has no
73
+ // placeholder to get wrong; otherwise fall back to the documented pattern.
74
+ const acTagExample = Number.isInteger(epicId)
75
+ ? `@epic-${epicId}-ac-1`
76
+ : '@epic-<id>-ac-N';
69
77
  return `You are an expert Senior Project Manager and Orchestrator.
70
78
  Your job is to take a Product Requirements Document (PRD) and a Technical Specification and decompose them into a flat list of Story tickets for an AI Agent to execute.
71
79
 
@@ -96,7 +104,7 @@ You MUST respond ONLY with a valid JSON array of objects. No prose, no markdown
96
104
  }
97
105
  ]
98
106
 
99
- **Slug format**: \`^[a-z0-9][a-z0-9-]*\$\` — hyphen-case only. Underscores are rejected by the validator.
107
+ **Slug format**: \`^[a-z0-9][a-z0-9-]*$\` — hyphen-case only. Underscores are rejected by the validator.
100
108
 
101
109
  ### STORY BODY SCHEMA (REQUIRED FOR EVERY STORY):
102
110
  \`body\` MUST be a **string** — the serialized markdown produced by \`serialize()\` from \`lib/story-body/story-body.js\`. Do NOT emit \`body\` as a JSON object: an object body throws \`StoryBodyParseError\` in the reconciler (Story #3302) and is discarded by the GitHub provider, producing an empty issue body. Stories are consumed by non-interactive sub-agents that must self-verify from the Story ticket alone — so the ticket must carry everything an agent needs to execute and self-verify.
@@ -196,8 +204,9 @@ When the Acceptance Spec contains **one or more \`Disposition: new\` rows**, you
196
204
  - **goal**: contains the literal token \`bdd-scaffold\` (e.g. "bdd-scaffold: create the @skip-tagged feature files the implementation Stories verify against").
197
205
  - **depends_on**: EMPTY (\`[]\`) — it runs first, in wave 0.
198
206
  - **changes**: one entry per distinct \`.feature\` file named in a \`new\` row, each \`{ "path": "<feature file path>", "assumption": "creates" }\`.
199
- - **acceptance**: MUST assert (a) every new \`.feature\` file exists, and (b) every new scenario within them carries an \`@skip\` tag. Keep these observable (a grep/validate command exits 0, a file exists at a path).
200
- - **verify**: a grep/validate command (tier \`validate\`), NOT an e2e runner verifying that a file exists with a tag needs no browser/playwright run. Example: \`grep -rL '@skip' tests/features/<area>/*.feature (validate)\` paired with an existence check.
207
+ - **acceptance**: MUST assert (a) every new \`.feature\` file exists, (b) every new scenario within them carries an \`@skip\` tag, AND (c) every new scenario also carries its **namespaced per-Epic AC tag** \`${acTagExample}\` (one tag per AC ID the scenario satisfies — see below). Keep these observable (a grep/validate command exits 0, a file exists at a path).
208
+ - **Namespaced AC tag is REQUIRED at scaffold time, not only at de-skip time.** Phase 7 finalize's \`acceptance-spec-reconciler.js\` matches AC IDs only against \`@epic-<id>-ac-*\` / \`@pending\` tags in \`tests/features/**\` a bare \`@ac-N\` tag is deliberately ignored to prevent cross-Epic collision. A scaffolded scenario that carries \`@skip\` but omits \`@epic-<id>-ac-N\` reads as \`missing[]\` at finalize and aborts the close even after the implementation Story de-skips it, because the tag was never added. Tag each scenario with both \`@skip\` AND \`${acTagExample}\` (substituting the AC's own number) in this SAME wave-0 pass — do not defer the AC tag to the later de-skip edit.
209
+ - **verify**: a grep/validate command (tier \`validate\`), NOT an e2e runner — verifying that a file exists with the required tags needs no browser/playwright run. Example: \`grep -rL '@skip' tests/features/<area>/*.feature (validate)\` paired with an existence check, AND a check that every new AC ID's namespaced tag (\`${acTagExample}\`) appears in the scaffolded files, e.g. \`grep -q '${acTagExample}' tests/features/<area>/<file>.feature (validate)\` for each new AC row.
201
210
  - Each implementation Story whose \`verify[]\` references one of these scaffolded \`.feature\` paths MUST \`depends_on\` the scaffold Story (so the scaffold lands in an earlier wave). Omitting the link trips the soft \`missing-bdd-scaffold\` validator finding.
202
211
 
203
212
  When the Acceptance Spec contains **zero \`new\`-disposition rows** (every row is \`updated\` or \`unchanged\`), do NOT emit a scaffold Story — there is nothing to create.
@@ -290,13 +290,14 @@ When the Acceptance Spec contains **one or more `Disposition: new` rows**, you M
290
290
  - **goal** (in body string): contains the literal token `bdd-scaffold`.
291
291
  - **depends_on**: EMPTY (`[]`) — the scaffold runs first, in wave 0.
292
292
  - **changes** (in body string): one `{ path, assumption: "creates" }` entry per distinct `.feature` file named in a `new` row.
293
- - **acceptance** (top-level array): MUST assert (a) every new `.feature` file exists, and (b) every new scenario within them carries an `@skip` tag. Keep items observable (a command exits 0; a file exists at a path).
294
- - **verify** (top-level array): a grep/validate command (tier `validate`), NOT an e2e runnerverifying that a file exists with a tag needs no browser/playwright run.
293
+ - **acceptance** (top-level array): MUST assert (a) every new `.feature` file exists, (b) every new scenario within them carries an `@skip` tag, AND (c) every new scenario also carries its **namespaced per-Epic AC tag** `@epic-<id>-ac-N` (one tag per AC ID the scenario satisfies). Keep items observable (a command exits 0; a file exists at a path).
294
+ - **The namespaced AC tag is REQUIRED at scaffold time, not only at de-skip time.** Phase 7 finalize's `acceptance-spec-reconciler.js` matches AC IDs only against `@epic-<id>-ac-*` / `@pending` tags under `tests/features/**` — a bare `@ac-N` tag is deliberately ignored to prevent cross-Epic collision (Story #3362). A scaffolded scenario carrying `@skip` but no `@epic-<id>-ac-N` tag reads as `missing[]` at finalize and throws, aborting close, even after the implementation Story de-skips it the tag was never added in either pass. Tag each scenario with both `@skip` AND `@epic-<id>-ac-N` (substituting the Epic's real ID and the scenario's own AC number) in this SAME wave-0 commit; do not defer the AC tag to the later de-skip edit.
295
+ - **verify** (top-level array): a grep/validate command (tier `validate`), NOT an e2e runner — verifying that a file exists with the required tags needs no browser/playwright run. Include a check that each new AC ID's namespaced tag is present in the scaffolded files, alongside the `@skip` check.
295
296
  - Each implementation Story whose `verify[]` references a scaffolded `.feature` path MUST add `depends_on: ["<scaffold-slug>"]` so the scaffold lands in an earlier wave. Omitting the link trips the soft `missing-bdd-scaffold` finding in `ticket-validator-conflicts.js` (advisory, not a hard block).
296
297
 
297
298
  When the Acceptance Spec contains **zero `new`-disposition rows** (every row is `updated` or `unchanged`), do NOT emit a scaffold Story — there is nothing to create.
298
299
 
299
- **Worked example.** Acceptance Spec with two `new` rows (`AC-1` -> `tests/features/billing/invoice.feature`, `AC-2` -> `tests/features/billing/refund.feature`). The scaffold Story below uses a serialized string `body`, top-level `acceptance`/`verify` arrays, and an empty `depends_on`:
300
+ **Worked example.** Epic #42, Acceptance Spec with two `new` rows (`AC-1` -> `tests/features/billing/invoice.feature`, `AC-2` -> `tests/features/billing/refund.feature`). The scaffold Story below uses a serialized string `body`, top-level `acceptance`/`verify` arrays, an empty `depends_on`, and tags each scenario with both `@skip` and its namespaced `@epic-42-ac-N` tag:
300
301
 
301
302
  {
302
303
  "slug": "scaffold-billing-feature-files",
@@ -306,16 +307,18 @@ When the Acceptance Spec contains **zero `new`-disposition rows** (every row is
306
307
  "labels": ["type::story", "persona::qa-engineer"],
307
308
  "acceptance": [
308
309
  "tests/features/billing/invoice.feature and tests/features/billing/refund.feature both exist on the branch",
309
- "every Scenario in the two new feature files is preceded by an @skip tag (grep for un-skipped scenarios returns zero matches)"
310
+ "every Scenario in the two new feature files is preceded by an @skip tag (grep for un-skipped scenarios returns zero matches)",
311
+ "the invoice.feature scenario carries @epic-42-ac-1 and the refund.feature scenario carries @epic-42-ac-2"
310
312
  ],
311
313
  "verify": [
312
314
  "test -f tests/features/billing/invoice.feature && test -f tests/features/billing/refund.feature (validate)",
313
- "test -z \"$(grep -rL '@skip' tests/features/billing/*.feature)\" (validate)"
315
+ "test -z \"$(grep -rL '@skip' tests/features/billing/*.feature)\" (validate)",
316
+ "grep -q '@epic-42-ac-1' tests/features/billing/invoice.feature && grep -q '@epic-42-ac-2' tests/features/billing/refund.feature (validate)"
314
317
  ],
315
- "body": "## Goal\nbdd-scaffold: create the @skip-tagged feature files the billing-flows implementation Stories verify against, so wave-0 lands them before any implementation Story runs.\n\n## Changes\n- {\"path\": \"tests/features/billing/invoice.feature\", \"assumption\": \"creates\"}\n- {\"path\": \"tests/features/billing/refund.feature\", \"assumption\": \"creates\"}\n\n## Acceptance\n- [ ] tests/features/billing/invoice.feature and tests/features/billing/refund.feature both exist on the branch\n- [ ] every Scenario in the two new feature files is preceded by an @skip tag\n\n## Verify\n- test -f tests/features/billing/invoice.feature && test -f tests/features/billing/refund.feature (validate)\n- test -z \"$(grep -rL '@skip' tests/features/billing/*.feature)\" (validate)\n"
318
+ "body": "## Goal\nbdd-scaffold: create the @skip-tagged, @epic-42-ac-N-tagged feature files the billing-flows implementation Stories verify against, so wave-0 lands them before any implementation Story runs.\n\n## Changes\n- {\"path\": \"tests/features/billing/invoice.feature\", \"assumption\": \"creates\"}\n- {\"path\": \"tests/features/billing/refund.feature\", \"assumption\": \"creates\"}\n\n## Acceptance\n- [ ] tests/features/billing/invoice.feature and tests/features/billing/refund.feature both exist on the branch\n- [ ] every Scenario in the two new feature files is preceded by an @skip tag\n- [ ] the invoice.feature scenario carries @epic-42-ac-1 and the refund.feature scenario carries @epic-42-ac-2\n\n## Verify\n- test -f tests/features/billing/invoice.feature && test -f tests/features/billing/refund.feature (validate)\n- test -z \"$(grep -rL '@skip' tests/features/billing/*.feature)\" (validate)\n- grep -q '@epic-42-ac-1' tests/features/billing/invoice.feature && grep -q '@epic-42-ac-2' tests/features/billing/refund.feature (validate)\n"
316
319
  }
317
320
 
318
- The implementation Stories that later un-skip and flesh out these scenarios each carry `depends_on: ["scaffold-billing-feature-files"]`, placing them in a later wave than the scaffold.
321
+ The implementation Stories that later un-skip and flesh out these scenarios each carry `depends_on: ["scaffold-billing-feature-files"]`, placing them in a later wave than the scaffold. They MUST NOT add the `@epic-42-ac-N` tag themselves — it is already present from the scaffold pass; their job is to remove `@skip` once the scenario passes.
319
322
 
320
323
  ### SCOPE-OVERLAP FLAGGING (docs/runbook downstream of config work)
321
324
 
@@ -260,6 +260,74 @@ Symlink strategy:
260
260
  specific failure up to 3 times with 250/500/1000 ms backoff. Unrelated fetch
261
261
  failures surface immediately — no retry.
262
262
 
263
+ ## Harness-worktree ⇄ consumer-lint-ignore interaction (Story #152)
264
+
265
+ Mandrel's own worktree isolation (above) always roots story worktrees at
266
+ `delivery.worktreeIsolation.root` (default `.worktrees/` at the repo root).
267
+ That path is separate from **the host IDE/CLI harness's own worktree
268
+ mechanism** — for example Claude Code, when it manages an agent session as a
269
+ git worktree, nests it at `.claude/worktrees/<name>/`. A mandrel delivery
270
+ agent can be invoked from *either* location depending on how the operator's
271
+ harness composes with `/deliver`: mandrel's own `.worktrees/story-<id>/` when
272
+ `worktreeIsolation.enabled` drives the checkout, or a harness-level
273
+ `.claude/worktrees/<name>/` when the harness itself provides the isolated
274
+ working directory mandrel runs inside.
275
+
276
+ This matters because a consumer's `pre-push` (or `pre-commit`) lint step is
277
+ commonly configured with an ignore glob that excludes noisy agent-tooling
278
+ directories, e.g. a Biome `files.includes` entry like `"!**/.claude"`. When
279
+ the *agent's CWD itself* resolves under `.claude/worktrees/<name>/`, a
280
+ lint invocation scoped to `.` (`biome check .`, or equivalent) resolves
281
+ every candidate path as living under the ignored `.claude` prefix — the glob
282
+ matches zero files, and tools that treat zero-match as failure (Biome's
283
+ default `check` behavior without `--no-errors-on-unmatched`) exit non-zero
284
+ with something like `No files were processed in the specified paths`. This
285
+ is a **false negative**: the changed files were never actually linted
286
+ against, and the hook is not reporting a real defect. It is functionally
287
+ distinct from a `pre-push` rejection caused by a genuine lint violation, and
288
+ must not be treated the same way.
289
+
290
+ **Do not resolve this by bypassing the push hook.**
291
+ [`rules/git-conventions.md`](../../rules/git-conventions.md) § "Push
292
+ Validation & Reliability" prohibits skipping hooks without explicit operator
293
+ authorization, and that prohibition is not weakened by this interaction —
294
+ the zero-match failure is a **consumer-tooling gap**, not a framework
295
+ authorization the agent gets to grant itself.
296
+
297
+ **Sanctioned resolution path:**
298
+
299
+ 1. **Recognize the signature.** A `pre-push`/`pre-commit` failure whose
300
+ message is a zero-match error (`No files were processed`, `0 files
301
+ matched`, or equivalent for the consumer's linter) — not a reported
302
+ violation in a specific file — combined with an agent CWD under
303
+ `.claude/worktrees/` (or any other harness-managed path a consumer's lint
304
+ config ignores) is this known interaction, not a real lint failure.
305
+ 2. **Fix it in the consumer, not the agent invocation.** The remedy lives in
306
+ the consumer's own lint command, mirroring what its `lint-staged` config
307
+ (if present) likely already does for the same reason: make the zero-match
308
+ case a no-op instead of a failure. For Biome:
309
+ `biome check --no-errors-on-unmatched .`. Other linters have an
310
+ equivalent flag (e.g. ESLint's `--no-error-on-unmatched-pattern`). This is
311
+ a one-line consumer-side change, typically to `.husky/pre-push` or the
312
+ `package.json` script it invokes.
313
+ 3. **Escalate through the normal HITL path**, per
314
+ [`.agents/instructions.md` § 1.J](../../instructions.md), if the agent
315
+ cannot edit the consumer's hook/lint config directly (e.g. it sits outside
316
+ the Story's scope). Transition to `agent::blocked`, name the zero-match
317
+ signature and the one-line remedy in the blocker summary, and let the
318
+ operator apply the consumer-side fix or explicitly authorize a one-time
319
+ hook-skip per [`rules/git-conventions.md`](../../rules/git-conventions.md)
320
+ § "Push Validation & Reliability". Explicit operator authorization is the
321
+ *only* circumstance under which a hook may be skipped — never as an
322
+ agent's unilateral default when this signature is recognized.
323
+ 4. **Do not relocate mandrel's own worktrees to work around a harness-level
324
+ path.** `delivery.worktreeIsolation.root` controls where *mandrel*
325
+ materializes `story-<id>` worktrees (default `.worktrees/`, already
326
+ outside `.claude/`) and is unrelated to where the host harness places its
327
+ own session worktree. Changing `worktreeIsolation.root` does not fix this
328
+ interaction when the false negative originates from the harness's path,
329
+ not mandrel's.
330
+
263
331
  ## Fallback: single-tree mode
264
332
 
265
333
  Set `delivery.worktreeIsolation.enabled: false` (or omit the block) to
package/docs/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [1.80.0](https://github.com/dsj1984/mandrel/compare/mandrel-v1.79.0...mandrel-v1.80.0) (2026-07-01)
6
+
7
+
8
+ ### Chores
9
+
10
+ * **release:** force a release to ship the [#4306](https://github.com/dsj1984/mandrel/issues/4306) worktree-lifecycle doc fix ([#4307](https://github.com/dsj1984/mandrel/issues/4307)) ([fa54ebb](https://github.com/dsj1984/mandrel/commit/fa54ebb52fc8fe264bab2f4c6dea180973023c28))
11
+
12
+ ## [1.79.0](https://github.com/dsj1984/mandrel/compare/mandrel-v1.78.0...mandrel-v1.79.0) (2026-06-30)
13
+
14
+
15
+ ### Fixed
16
+
17
+ * **decompose-author:** require namespaced [@epic](https://github.com/epic)-&lt;id&gt;-ac-N tag on wave-0 BDD scaffold scenarios (refs [#4301](https://github.com/dsj1984/mandrel/issues/4301)) ([#4304](https://github.com/dsj1984/mandrel/issues/4304)) ([c97d45e](https://github.com/dsj1984/mandrel/commit/c97d45e806a72a8511995d8f606b5be2ef486315))
18
+ * **reconciler:** single-source the parent/Epic body trailer so force re-decompose retains Epic: #&lt;id&gt; (refs [#4300](https://github.com/dsj1984/mandrel/issues/4300)) ([#4303](https://github.com/dsj1984/mandrel/issues/4303)) ([a8e78a9](https://github.com/dsj1984/mandrel/commit/a8e78a9fd946e6f532a59df9cd37237e0d046b2d))
19
+
5
20
  ## [1.78.0](https://github.com/dsj1984/mandrel/compare/mandrel-v1.77.0...mandrel-v1.78.0) (2026-06-24)
6
21
 
7
22
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mandrel",
3
- "version": "1.78.0",
3
+ "version": "1.80.0",
4
4
  "description": "Claude Code-first opinionated workflow framework: instructions, personas, skills, and SDLC workflows that govern AI coding assistants.",
5
5
  "files": [
6
6
  ".agents/",