mandrel 1.75.0 → 1.76.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.
@@ -341,11 +341,20 @@ export async function assertNoOpenPlanChildren({
341
341
  const openChildren = (children ?? []).filter((t) => {
342
342
  const labels = Array.isArray(t.labels) ? t.labels : [];
343
343
  const isOpen = t.state === undefined || t.state === 'open';
344
- // Any open typed plan ticket counts `type::story` plus pre-v4
345
- // `type::feature` leftovers. The prefix check is legacy-data
346
- // detection, not compat support: the guard only refuses, it never
347
- // processes the legacy tier. Context tickets (`context::*`) are
348
- // untouched.
344
+ // Context spec tickets (PRD / Tech Spec / Acceptance Spec) carry a
345
+ // `context::*` label and since the `createTicket` factory injects
346
+ // `type::story` by default also `type::story`. They are reference
347
+ // artifacts that stay open across delivery, NOT plan children, so they
348
+ // MUST be excluded here: counting them would make every first decompose
349
+ // (where the three context tickets are the only open children) refuse.
350
+ const isContext = labels.some(
351
+ (l) => typeof l === 'string' && l.startsWith('context::'),
352
+ );
353
+ if (isContext) return false;
354
+ // Any remaining open typed plan ticket counts — `type::story` plus pre-v4
355
+ // `type::feature` leftovers. The prefix check is legacy-data detection,
356
+ // not compat support: the guard only refuses, it never processes the
357
+ // legacy tier.
349
358
  return (
350
359
  isOpen &&
351
360
  labels.some((l) => typeof l === 'string' && l.startsWith('type::'))
@@ -35,6 +35,17 @@ export async function discoverOpenStories({ epicId, provider }) {
35
35
  for (const t of descendants) {
36
36
  const labels = t.labels ?? [];
37
37
  if (!labels.includes(TYPE_LABELS.STORY)) continue;
38
+ // Defense-in-depth: never enumerate a context spec ticket (PRD / Tech
39
+ // Spec / Acceptance Spec) as a deliverable Story even if it carries
40
+ // `type::story`. The `createTicket` factory no longer stamps context
41
+ // tickets that way, but a context ticket mislabelled out-of-band must
42
+ // still never reach a delivery wave (it has no `story-<id>` branch or
43
+ // acceptance contract to deliver against).
44
+ if (
45
+ labels.some((l) => typeof l === 'string' && l.startsWith('context::'))
46
+ ) {
47
+ continue;
48
+ }
38
49
  const rawState = t.state ?? 'open';
39
50
  const norm = typeof rawState === 'string' ? rawState.toLowerCase() : 'open';
40
51
  if (norm !== 'open') continue;
@@ -320,13 +320,26 @@ export class TicketGateway {
320
320
  });
321
321
 
322
322
  // Mirror the Epic create path (issues.js:160 → `labels: TYPE_LABELS.EPIC`):
323
- // always inject TYPE_LABELS.STORY so a spec that omits the labels array
324
- // cannot produce an unlabeled, undispatchable Story. Dedupe to avoid
325
- // duplicates when the caller already carries the label.
323
+ // inject TYPE_LABELS.STORY so a spec that omits the labels array cannot
324
+ // produce an unlabeled, undispatchable Story. Dedupe to avoid duplicates
325
+ // when the caller already carries the label.
326
+ //
327
+ // Context spec tickets (PRD / Tech Spec / Acceptance Spec) are a distinct
328
+ // ticket class created through this same factory carrying a `context::*`
329
+ // label (and no `type::story`). They MUST NOT be stamped `type::story`:
330
+ // doing so makes every `type::story`-counting consumer — the decompose
331
+ // open-children guard (`assertNoOpenPlanChildren`), the delivery wave
332
+ // builder (`discoverOpenStories`), the plan healthcheck — mis-classify
333
+ // them as deliverable Stories. Skip the injection when the caller's labels
334
+ // already classify the ticket as context.
326
335
  const callerLabels = ticketData.labels ?? [];
327
- const labels = callerLabels.includes(TYPE_LABELS.STORY)
328
- ? callerLabels
329
- : [TYPE_LABELS.STORY, ...callerLabels];
336
+ const isContextTicket = callerLabels.some(
337
+ (l) => typeof l === 'string' && l.startsWith('context::'),
338
+ );
339
+ const labels =
340
+ isContextTicket || callerLabels.includes(TYPE_LABELS.STORY)
341
+ ? callerLabels
342
+ : [TYPE_LABELS.STORY, ...callerLabels];
330
343
  const result = await this._gh.api({
331
344
  method: 'POST',
332
345
  endpoint: `/repos/${this.owner}/${this.repo}/issues`,
package/docs/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [1.76.0](https://github.com/dsj1984/mandrel/compare/mandrel-v1.75.0...mandrel-v1.76.0) (2026-06-20)
6
+
7
+
8
+ ### Fixed
9
+
10
+ * **plan:** stop stamping context spec tickets as type::story so first decompose isn't blocked ([#4246](https://github.com/dsj1984/mandrel/issues/4246)) ([#4247](https://github.com/dsj1984/mandrel/issues/4247)) ([564cfb8](https://github.com/dsj1984/mandrel/commit/564cfb81876f87ad5f61f3c670efb7cfc2c95540))
11
+
5
12
  ## [1.75.0](https://github.com/dsj1984/mandrel/compare/mandrel-v1.74.0...mandrel-v1.75.0) (2026-06-19)
6
13
 
7
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mandrel",
3
- "version": "1.75.0",
3
+ "version": "1.76.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/",