@codyswann/lisa 2.45.0 → 2.46.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.
Files changed (27) hide show
  1. package/package.json +1 -1
  2. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  3. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  4. package/plugins/lisa/skills/confluence-prd-intake/SKILL.md +81 -6
  5. package/plugins/lisa/skills/github-prd-intake/SKILL.md +1 -1
  6. package/plugins/lisa/skills/linear-prd-intake/SKILL.md +80 -6
  7. package/plugins/lisa/skills/notion-prd-intake/SKILL.md +75 -4
  8. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  9. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  10. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  11. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  12. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  13. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  14. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  15. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  16. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  17. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  18. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  19. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  20. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  22. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  24. package/plugins/src/base/skills/confluence-prd-intake/SKILL.md +81 -6
  25. package/plugins/src/base/skills/github-prd-intake/SKILL.md +1 -1
  26. package/plugins/src/base/skills/linear-prd-intake/SKILL.md +80 -6
  27. package/plugins/src/base/skills/notion-prd-intake/SKILL.md +75 -4
package/package.json CHANGED
@@ -82,7 +82,7 @@
82
82
  "lodash": ">=4.18.1"
83
83
  },
84
84
  "name": "@codyswann/lisa",
85
- "version": "2.45.0",
85
+ "version": "2.46.0",
86
86
  "description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
87
87
  "main": "dist/index.js",
88
88
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.45.0",
3
+ "version": "2.46.0",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.45.0",
3
+ "version": "2.46.0",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -59,11 +59,26 @@ current_role_for_prd() {
59
59
  done
60
60
  echo "unknown"
61
61
  }
62
+
63
+ # Resolve a boolean rollup flag. Local overrides global per-key; default when unset.
64
+ # NOTE: Confluence rollup config lives under `confluence.rollup` (NOT
65
+ # `confluence.parents.rollup`) — see the config-resolution rule.
66
+ read_rollup_flag() {
67
+ local key="$1" default="$2"
68
+ local local_v global_v
69
+ local_v=$(jq -r ".confluence.rollup.${key} // empty" .lisa.config.local.json 2>/dev/null)
70
+ global_v=$(jq -r ".confluence.rollup.${key} // empty" .lisa.config.json 2>/dev/null)
71
+ echo "${local_v:-${global_v:-$default}}"
72
+ }
73
+
74
+ CLOSE_ON_SHIPPED=$(read_rollup_flag closeOnShipped false)
62
75
  ```
63
76
 
64
77
  In prose below, the role names refer to the resolved parent-page IDs: e.g. "the `ready` parent" means whatever `confluence.parents.ready` resolves to.
65
78
 
66
- This skill is the Confluence counterpart of `lisa:notion-prd-intake`. The phases, gates, comment templates, and rules are identical — the only differences are (1) the lifecycle is encoded as **parent-page placement** instead of a status property, and (2) the fetch / comment / update tools route through `lisa:atlassian-access`. Keep the two skills behaviorally aligned: when changing intake logic, change BOTH skills together.
79
+ This skill is the Confluence counterpart of `lisa:notion-prd-intake`, and shares its PRD closure rollup phase (3f) with `lisa:github-prd-intake` and `lisa:linear-prd-intake`. The phases, gates, comment templates, and rules are identical — the only differences are (1) the lifecycle is encoded as **parent-page placement** instead of a status property, and (2) the fetch / comment / update tools route through `lisa:atlassian-access`. Keep all four intake skills behaviorally aligned: when changing intake logic — including the rollup phase — change them together.
80
+
81
+ The **PRD closure rollup phase (3f)** re-parents a `ticketed` PRD to the `shipped` parent (and optionally archives it) once all its generated top-level work is terminal, per the `prd-lifecycle-rollup` rule. This is the Confluence leg of the same vendor-neutral rollup that `lisa:github-prd-intake` implements for GitHub (LPC-1.3 #584); only the vendor surface (parent-page placement + documented generated-work section, since Confluence has no native ticket hierarchy) differs.
67
82
 
68
83
  ## Confirmation policy
69
84
 
@@ -93,13 +108,15 @@ draft → ready → in_review → blocked | ticketed → shipped
93
108
 
94
109
  A PRD's current state is determined entirely by which lifecycle parent it sits under. Re-parenting is the transition.
95
110
 
96
- This skill ONLY transitions:
111
+ This skill transitions:
97
112
 
98
113
  - `ready` → `in_review` (claim)
99
114
  - `in_review` → `blocked` (gate failures or coverage gaps)
100
115
  - `in_review` → `ticketed` (success)
116
+ - `ticketed` → `blocked` (post-write coverage gaps from Phase 3e)
117
+ - `ticketed` → `shipped` (PRD closure rollup, Phase 3f — only when **all** generated top-level children are terminal)
101
118
 
102
- It never re-parents PRDs into or out of the `draft` or `shipped` parents. Those parents are owned by product.
119
+ It never re-parents PRDs into or out of the `draft` parent — that parent is owned by product. The `shipped` parent is set by this skill's **rollup phase (3f)** when, and only when, the PRD's generated top-level work is all terminal — per the `prd-lifecycle-rollup` rule; product may also re-parent there by hand. Rollup never advances a PRD to `shipped` on partial completion, and never archives a PRD page unless `confluence.rollup.closeOnShipped` is configured `true` (default `false` → re-parent under `shipped`, leave the page active).
103
120
 
104
121
  A "transition" means: update the PRD's `parentId` to the new role's parent-page id via `lisa:atlassian-access` `operation: write-page payload: { id, parentId, title, version: { number: <next> } }`. The v2 PUT endpoint requires the next version number and the page title in the payload; the body content is not strictly required for a re-parent-only edit, but some Atlassian deployments reject PUTs without a body. The skill MUST therefore GET the page first via `read-page`, capture title + current version + current body, then PUT with `parentId` swapped and `version.number` bumped — preserving body content is non-negotiable, this skill never edits PRD content. See `transition_prd` helper in Phase 3a for the canonical implementation.
105
122
 
@@ -276,6 +293,61 @@ Per-ticket gates prove each ticket is well-formed; they do NOT prove the *set* o
276
293
 
277
294
  3. The created tickets remain in the destination tracker regardless of the verdict — they are valid in their own right. The audit only tells us whether *more* are needed.
278
295
 
296
+ #### 3f. PRD closure rollup (config-gated)
297
+
298
+ A PRD's lifecycle terminal state (`shipped`) is **derived** from whether the work it generated is done — it is never set by hand here on its own authority. This phase implements the Confluence leg of that derivation, per the `prd-lifecycle-rollup` rule (cite it by slug; do not restate its taxonomy or terminal-state semantics here). It is behaviorally identical to `lisa:github-prd-intake`'s Phase 3f — only the vendor surface (parent-page re-parenting via `lisa:atlassian-access` + the documented generated-work section) differs from GitHub's (issue close + labels via `gh`).
299
+
300
+ Rollup runs over PRD pages that are already under the `ticketed` parent (the only state from which a PRD can ship): the freshly-ticketed PRD from Phase 3c, and — because rollup also catches PRDs whose children finished in a *later* cycle — every page currently parented under `$TICKETED_PARENT`. (Re-read its direct children via `lisa:atlassian-access` `operation: read-page-descendants id: $TICKETED_PARENT`.) Process each independently; one PRD never blocks another's rollup.
301
+
302
+ ##### 3f.0 Resolve closure config
303
+
304
+ Closure is gated on `confluence.rollup.closeOnShipped` (default `false`), resolved via `read_rollup_flag` (defined in the Workflow resolution block, same local-overrides-global precedence the lifecycle parents use). Note the config lives under `confluence.rollup` (NOT `confluence.parents.rollup`):
305
+
306
+ ```bash
307
+ CLOSE_ON_SHIPPED=$(read_rollup_flag closeOnShipped false)
308
+ ```
309
+
310
+ When `false` (the default), rollup re-parents the PRD under `$SHIPPED_PARENT` but leaves the page **active** for a human to archive. When `true`, rollup also archives the page (where the deployment supports archival) after the `shipped` re-parent. Closure NEVER happens before all generated top-level work is terminal (`prd-lifecycle-rollup` rule; PRD #525 non-goal).
311
+
312
+ ##### 3f.1 Idempotency guard (no-op if already shipped)
313
+
314
+ Rollup is keyed by the PRD's current state. If the PRD is already parented under `$SHIPPED_PARENT` (and is already archived, when `$CLOSE_ON_SHIPPED` is `true`), it is a **no-op** — do not re-parent, do not re-archive, do not re-comment. Record it as `already shipped (no-op)` in the cycle summary and move on. This is what makes re-running intake safe.
315
+
316
+ ##### 3f.2 Read the generated top-level child set
317
+
318
+ Read the PRD's **generated top-level work** — its created Epics and any top-level Stories created directly under it, **excluding** leaf Sub-tasks and any Story nested under a generated Epic (`prd-lifecycle-rollup` rule, generated-top-level-work contract). Confluence has **no native ticket hierarchy**, so the child set comes from the documented section only:
319
+
320
+ 1. **Documented `## Tickets` section (primary and only source).** Parse the machine-readable generated-work section `lisa:prd-backlink` writes to the PRD body (`## Tickets`, alias `## Generated Work`; see #582) via `lisa:atlassian-access` `operation: read-page id: <PRD-ID>`. Top-level children are the `### <Epic key>: <title>` group headers' first line (`- [<ref>](<url>) — Epic`) plus any top-level Story listed directly under `### Unparented items`. Lines nested deeper (` - ... — Story:` under an Epic, ` - ... — Sub-task:`) are descendants, NOT top-level children — skip them.
321
+
322
+ Dedupe the resulting child set by **child-ref identity** — the destination ticket ref recorded in each generated-work entry (the entry is keyed by that ref, not by list position) — per the `prd-lifecycle-rollup` idempotency dedupe key. If the section yields no child (the PRD generated nothing, or the relationship was never recorded), record `no generated top-level children — rollup skipped` and leave the PRD under `$TICKETED_PARENT`; do not ship an empty PRD.
323
+
324
+ ##### 3f.3 Apply the terminal-state predicate
325
+
326
+ For each top-level child, classify per the `prd-lifecycle-rollup` Confluence/Notion predicate:
327
+
328
+ - **Terminal (shipped).** The documented generated-work entry for the child is marked **done** in the PRD's machine-readable section (the durable equivalent of a closed ticket, since Confluence has no native ticket state). A child Epic is terminal only when it has itself rolled up to its own terminal state per `leaf-only-lifecycle` — read the child's own recorded state; do not re-derive it from its leaves here.
329
+ - **Terminal-but-dropped.** The entry is marked won't-do / canceled. Like a not-planned leaf, it does not hold the PRD open and is excluded from the shipped set.
330
+ - **Incomplete / blocked.** Anything else: the entry is not yet marked done. Holds the PRD open.
331
+
332
+ The set of **required** children for the all-terminal check is the top-level children minus the terminal-but-dropped ones.
333
+
334
+ ##### 3f.4 Branch on the rollup verdict
335
+
336
+ **All required children terminal** (every required top-level child is terminal; at least one required child exists):
337
+
338
+ 1. Re-parent to `shipped`: `transition_prd "$PRD_ID" shipped` (GET-then-PUT via `lisa:atlassian-access` preserving the body verbatim). After the re-parent, re-read the page and confirm `parentId` matches `$SHIPPED_PARENT` (the single-parent invariant).
339
+ 2. **If `$CLOSE_ON_SHIPPED` is `true`**, archive the PRD page where the deployment supports archival. When `false`, leave it active.
340
+ 3. Post a short rollup footer comment via `lisa:atlassian-access` `operation: comment-page kind: footer` naming the terminal child set and (when dropped children exist) the dropped set, so the audit trail records *why* the PRD shipped. Lead with `"Shipped by Claude — all generated top-level work is complete."`
341
+
342
+ **Any required child incomplete / blocked**:
343
+
344
+ 1. Leave the PRD under `$TICKETED_PARENT` and leave the page **active**. Do NOT re-parent to `shipped`. Do NOT archive.
345
+ 2. Report the incomplete child set — both in the cycle summary and, when at least one cycle has previously ticketed this PRD, as a single advisory footer comment listing the still-open children (`- <ref> "<title>" — <state>`), so product can see what's blocking the rollup. Keep it idempotent: regenerate the advisory rather than appending a fresh one each cycle.
346
+
347
+ ##### 3f.5 Rollup cites the rule
348
+
349
+ This phase implements exactly one PRD-lifecycle hop — `ticketed → shipped` — and the optional config-gated archive that follows it. All terminal-state semantics, the generated-top-level-work boundary, and the dedupe-by-child-ref idempotency come from the `prd-lifecycle-rollup` rule; this skill is its Confluence implementation, not a second source of truth.
350
+
279
351
  ### Phase 4 — Summary report
280
352
 
281
353
  After processing every ready PRD, emit a summary:
@@ -304,10 +376,11 @@ Print to the agent's output. Do not write this summary to Confluence or the dest
304
376
  ## Idempotency & safety
305
377
 
306
378
  - **Single-cycle scope**: this skill processes the ready set as it exists at the start of Phase 2. New PRDs moved under the `ready` parent mid-cycle are picked up next run.
307
- - **No writes outside the lifecycle**: this skill only ever writes to the destination tracker via `lisa:confluence-to-tracker` (which delegates to `lisa:tracker-write`), and only ever re-parents PRDs among `in_review`, `blocked`, and `ticketed` via `lisa:atlassian-access` `operation: write-page`. It never edits PRD body content, never re-parents into or out of `draft` / `shipped`, never deletes pages.
379
+ - **No writes outside the lifecycle**: this skill only ever writes to the destination tracker via `lisa:confluence-to-tracker` (which delegates to `lisa:tracker-write`), and only ever re-parents PRDs among `in_review`, `blocked`, `ticketed`, and `shipped` (the last via the rollup phase 3f only) via `lisa:atlassian-access` `operation: write-page`. It never edits PRD body content, never re-parents into or out of `draft`, never deletes pages. It re-parents under `shipped` and may archive the PRD page **only** through the config-gated rollup phase (3f).
308
380
  - **Claim-first ordering**: the re-parent to `in_review` happens BEFORE validation runs, so a re-entrant call won't double-process.
309
381
  - **Failure isolation**: an exception processing one PRD must not stop the cycle. Catch, record under "Errors" in the summary, continue to the next PRD. The PRD that errored is left under whatever parent it currently occupies (usually `in_review` if claim succeeded) — the human investigates from there.
310
382
  - **Single-parent invariant**: a page has exactly one parent by construction in Confluence — the multi-state ambiguity that label-based systems can hit (two `prd-*` labels simultaneously) cannot occur here. After every transition, re-read the page and confirm `parentId` matches the expected role; if not, surface as an Error and skip.
383
+ - **Rollup idempotency**: rollup (Phase 3f) is a no-op on a PRD already parented under `$SHIPPED_PARENT` (and already archived when `closeOnShipped` is `true`) — no duplicate re-parent, no duplicate archive, no duplicate comment. The all-terminal condition is a pure function of the children's current states (deduped by child-ref identity), so recomputing it is safe to re-run. Archival NEVER precedes the all-terminal condition.
311
384
 
312
385
  ## Configuration
313
386
 
@@ -325,12 +398,14 @@ Destination tracker config (jira / github / linear) is consumed by `lisa:tracker
325
398
  | `.lisa.config.json` `confluence.parents.blocked` | yes | Parent page id for "validation failure" |
326
399
  | `.lisa.config.json` `confluence.parents.ticketed` | yes | Parent page id for "successfully ticketed" |
327
400
  | `.lisa.config.json` `confluence.parents.draft` | recommended | Parent page id for PRDs still being drafted by product |
328
- | `.lisa.config.json` `confluence.parents.shipped` | recommended | Parent page id for delivered PRDs |
401
+ | `.lisa.config.json` `confluence.parents.shipped` | recommended | Parent page id the rollup phase (3f) re-parents delivered PRDs under; product may also use it by hand |
402
+ | `.lisa.config.json` `confluence.rollup.closeOnShipped` | no (default `false`) | When `true`, rollup archives the PRD page after the `shipped` re-parent; when `false`, re-parents under `shipped` and leaves the page active |
329
403
 
330
404
  ## Rules
331
405
 
332
406
  - Never write to the destination tracker outside of `lisa:confluence-to-tracker` → `lisa:tracker-write`. The validator's verdict gates progress; bypassing it produces broken tickets.
333
- - Never re-parent a PRD into a lifecycle parent this skill doesn't own (`in_review`, `blocked`, `ticketed`). Product owns `draft`, `ready` (as the entry signal), and `shipped`.
407
+ - Never re-parent a PRD into a lifecycle parent this skill doesn't own (`in_review`, `blocked`, `ticketed`, and `shipped` via the rollup phase only). Product owns `draft` and `ready` (as the entry signal); product and the rollup phase (3f) both re-parent under `shipped`.
408
+ - Re-parent under `shipped` (and archive the PRD page when `closeOnShipped` is configured) only from the rollup phase, and only when all generated top-level children are terminal per the `prd-lifecycle-rollup` rule. Never ship or archive on partial completion.
334
409
  - Never edit the PRD's body. Communication with product happens only through Confluence comments. The `write-page` call preserves the body verbatim — fetch then PUT with body unchanged.
335
410
  - Never post a single page-level dump of all gate failures. One inline comment per `prd_anchor` group (or one footer summary for unanchored failures only). Comments must be inline-anchored where possible, categorized, plain-language, and contain a concrete recommendation.
336
411
  - Never include a gate ID, internal skill name, or engineering shorthand in a comment body.
@@ -39,7 +39,7 @@ In prose below, the role names refer to the resolved labels: e.g. "the `ready` l
39
39
 
40
40
  This skill is the GitHub counterpart of `lisa:notion-prd-intake`, `lisa:confluence-prd-intake`, and `lisa:linear-prd-intake`. Phases, gates, comment templates, and rules are identical — the only differences are (1) the lifecycle is encoded as **issue labels** (mirroring Linear's project labels and Confluence's page labels), (2) the fetch / update tools are the `gh` CLI, and (3) clarifying-question comments land directly on the source PRD issue (because GitHub Issues *do* have native comments — no sentinel issue required, unlike Linear). Keep all four skills behaviorally aligned: when changing intake logic, change them together.
41
41
 
42
- The **PRD closure rollup phase (3f)** is, for now, GitHub-only: it transitions a `$TICKETED` PRD to `$SHIPPED` (and optionally closes it) once all its generated top-level work is terminal, per the `prd-lifecycle-rollup` rule. The Linear / Confluence / Notion counterparts gain the same rollup in sibling sub-task #584; until then, those three intake skills do not roll up to `shipped`, so this alignment note covers the shared intake/validation phases (1–3e), not yet the rollup phase.
42
+ The **PRD closure rollup phase (3f)** transitions a `$TICKETED` PRD to `$SHIPPED` (and optionally closes it) once all its generated top-level work is terminal, per the `prd-lifecycle-rollup` rule. This phase is GitHub-only here because its vendor surface (issue close + labels via `gh`) is GitHub-specific; the Linear / Confluence / Notion intake skills carry the **same** vendor-neutral rollup with their own surfaces (sibling sub-task #584, now landed). All four intake skills are behaviorally aligned across the rollup phase too keep them in sync when changing rollup logic.
43
43
 
44
44
  ## Confirmation policy
45
45
 
@@ -34,11 +34,24 @@ BLOCKED=$(read_role blocked "prd-blocked")
34
34
  TICKETED=$(read_role ticketed "prd-ticketed")
35
35
  SHIPPED=$(read_role shipped "prd-shipped")
36
36
  SENTINEL=$(read_role sentinel "prd-intake-feedback")
37
+
38
+ # Resolve a boolean rollup flag. Local overrides global per-key; default when unset.
39
+ read_rollup_flag() {
40
+ local key="$1" default="$2"
41
+ local local_v global_v
42
+ local_v=$(jq -r ".linear.labels.prd.rollup.${key} // empty" .lisa.config.local.json 2>/dev/null)
43
+ global_v=$(jq -r ".linear.labels.prd.rollup.${key} // empty" .lisa.config.json 2>/dev/null)
44
+ echo "${local_v:-${global_v:-$default}}"
45
+ }
46
+
47
+ CLOSE_ON_SHIPPED=$(read_rollup_flag closeOnShipped false)
37
48
  ```
38
49
 
39
50
  In prose below, the role names refer to the resolved labels: e.g. "the `ready` label" means whatever `linear.labels.prd.ready` resolves to (default: `prd-ready`).
40
51
 
41
- This skill is the Linear counterpart of `lisa:notion-prd-intake` and `lisa:confluence-prd-intake`. The phases, gates, comment templates, and rules are identical — the only differences are (1) the lifecycle is encoded as **project labels** instead of a status property, (2) the fetch / update tools are Linear MCP, and (3) clarifying-question comments land on a sentinel feedback Issue under the project (because Linear's MCP does not expose project-level comments). Keep all three skills behaviorally aligned: when changing intake logic, change them together.
52
+ This skill is the Linear counterpart of `lisa:notion-prd-intake` and `lisa:confluence-prd-intake`, and shares its PRD closure rollup phase (3f) with `lisa:github-prd-intake`. The phases, gates, comment templates, and rules are identical — the only differences are (1) the lifecycle is encoded as **project labels** instead of a status property, (2) the fetch / update tools are Linear MCP, and (3) clarifying-question comments land on a sentinel feedback Issue under the project (because Linear's MCP does not expose project-level comments). Keep all four intake skills behaviorally aligned: when changing intake logic — including the rollup phase — change them together.
53
+
54
+ The **PRD closure rollup phase (3f)** transitions a `$TICKETED` PRD project to `$SHIPPED` (and optionally closes/archives it) once all its generated top-level work is terminal, per the `prd-lifecycle-rollup` rule. This is the Linear leg of the same vendor-neutral rollup that `lisa:github-prd-intake` implements for GitHub (LPC-1.3 #584); only the vendor surface (Linear workflow states + project labels) differs.
42
55
 
43
56
  ## Confirmation policy
44
57
 
@@ -68,14 +81,15 @@ draft → ready → in_review → blocked | ticketed → shipped
68
81
 
69
82
  (Defaults: `prd-draft` / `prd-ready` / `prd-in-review` / `prd-blocked` / `prd-ticketed` / `prd-shipped`.)
70
83
 
71
- This skill ONLY transitions:
84
+ This skill transitions:
72
85
 
73
86
  - `$READY` → `$IN_REVIEW` (claim)
74
87
  - `$IN_REVIEW` → `$BLOCKED` (gate failures or coverage gaps)
75
88
  - `$IN_REVIEW` → `$TICKETED` (success)
76
89
  - `$TICKETED` → `$BLOCKED` (post-write coverage gaps from Phase 3e)
90
+ - `$TICKETED` → `$SHIPPED` (PRD closure rollup, Phase 3f — only when **all** generated top-level children are terminal)
77
91
 
78
- It never adds, removes, or touches the `draft` or `shipped` labels. Those labels are owned by product.
92
+ It never touches the `draft` label — that label is owned by product. The `shipped` label is set by this skill's **rollup phase (3f)** when, and only when, the PRD project's generated top-level work is all terminal — per the `prd-lifecycle-rollup` rule; product may also set it by hand. Rollup never advances a PRD to `shipped` on partial completion, and never archives a PRD project unless `linear.labels.prd.rollup.closeOnShipped` is configured `true` (default `false` → set `shipped`, leave the project active).
79
93
 
80
94
  A "transition" means: remove the old lifecycle label and add the new one in a single `save_project` call (passing the full new label set in the `labels` array). The skill MUST verify that exactly one lifecycle label exists on the project after the update — having two simultaneously breaks idempotency.
81
95
 
@@ -223,6 +237,63 @@ Per-ticket gates prove each ticket is well-formed; they do NOT prove the *set* o
223
237
 
224
238
  3. The created tickets remain in the destination tracker regardless of the verdict — they are valid in their own right. The audit only tells us whether *more* are needed.
225
239
 
240
+ #### 3f. PRD closure rollup (config-gated)
241
+
242
+ A PRD's lifecycle terminal state (`shipped`) is **derived** from whether the work it generated is done — it is never set by hand here on its own authority. This phase implements the Linear leg of that derivation, per the `prd-lifecycle-rollup` rule (cite it by slug; do not restate its taxonomy or terminal-state semantics here). It is behaviorally identical to `lisa:github-prd-intake`'s Phase 3f — only the vendor surface (Linear workflow states + project labels via Linear MCP) differs from GitHub's (issue close + labels via `gh`).
243
+
244
+ Rollup runs over PRD projects that are already `$TICKETED` (the only state from which a PRD can ship): the freshly-ticketed project from Phase 3c, and — because rollup also catches PRDs whose children finished in a *later* cycle — every project currently carrying `$TICKETED`. Process each independently; one PRD never blocks another's rollup.
245
+
246
+ ##### 3f.0 Resolve closure config
247
+
248
+ Closure is gated on `linear.labels.prd.rollup.closeOnShipped` (default `false`), resolved via `read_rollup_flag` (defined in the Workflow resolution block, same local-overrides-global precedence the lifecycle labels use):
249
+
250
+ ```bash
251
+ CLOSE_ON_SHIPPED=$(read_rollup_flag closeOnShipped false)
252
+ ```
253
+
254
+ When `false` (the default), rollup sets `$SHIPPED` but leaves the PRD project **active** for a human to archive. When `true`, rollup also moves the project to a closed/archived state after the `$SHIPPED` transition. Closure NEVER happens before all generated top-level work is terminal (`prd-lifecycle-rollup` rule; PRD #525 non-goal).
255
+
256
+ ##### 3f.1 Idempotency guard (no-op if already shipped)
257
+
258
+ Rollup is keyed by the PRD's current state. If the PRD project already carries `$SHIPPED` (and is already archived, when `$CLOSE_ON_SHIPPED` is `true`), it is a **no-op** — do not re-transition, do not re-archive, do not re-comment. Record it as `already shipped (no-op)` in the cycle summary and move on. This is what makes re-running intake safe.
259
+
260
+ ##### 3f.2 Read the generated top-level child set
261
+
262
+ Read the PRD's **generated top-level work** — its created Epics and any top-level Stories created directly under it, **excluding** leaf Sub-tasks and any Story nested under a generated Epic (`prd-lifecycle-rollup` rule, generated-top-level-work contract). Use two sources, native first:
263
+
264
+ 1. **Native parent / project relationships (primary).** Linear records the PRD→child relationship natively where the PRD also lives in Linear: a generated top-level Issue uses `parentId`, or a generated Project groups the generated Issues. Read the PRD project's generated top-level Issues via `mcp__linear-server__list_issues({project: <id>})` and take the **top-level** ones (no `parentId`, or whose parent is the PRD itself) — those are the PRD's direct children. Fetch each with `mcp__linear-server__get_issue` for its workflow state.
265
+
266
+ 2. **Documented `## Tickets` section (fallback).** When the native relationship is unavailable (the destination tracker is a *different* system — e.g. Linear PRD → JIRA tracker — so the children were never linked as Linear issues), parse the machine-readable generated-work section `lisa:prd-backlink` writes to the PRD (`## Tickets`, alias `## Generated Work`; see #582). Top-level children are the `### <Epic key>: <title>` group headers' first line (`- [<ref>](<url>) — Epic`) plus any top-level Story listed directly under `### Unparented items`. Lines nested deeper (` - ... — Story:` under an Epic, ` - ... — Sub-task:`) are descendants, NOT top-level children — skip them.
267
+
268
+ Dedupe the resulting child set by **child-ref identity** (the Linear issue/project identifier, e.g. `TEAM-123` or its UUID) so a child that appears both as a native relationship and in the documented section is counted once (`prd-lifecycle-rollup` idempotency dedupe key). If neither source yields any child (the PRD generated nothing, or the relationship was never recorded), record `no generated top-level children — rollup skipped` and leave the PRD as `$TICKETED`; do not ship an empty PRD.
269
+
270
+ ##### 3f.3 Apply the terminal-state predicate
271
+
272
+ For each top-level child, fetch its workflow state and classify per the `prd-lifecycle-rollup` Linear predicate:
273
+
274
+ - **Terminal (shipped).** The child Issue/Project is in a **completed** workflow state (the `completed` / `done`-category state). A child Epic is terminal only when it has itself rolled up to its own terminal state per `leaf-only-lifecycle` — read the child's own resolved state; do not re-derive it from its leaves here.
275
+ - **Terminal-but-dropped.** The child is in a **canceled** workflow state (the `canceled`-category state). Like a not-planned leaf, it does not hold the PRD open and is excluded from the shipped set.
276
+ - **Incomplete / blocked.** Anything else: any backlog / unstarted / started / triage workflow state. Holds the PRD open.
277
+
278
+ The set of **required** children for the all-terminal check is the top-level children minus the canceled (terminal-but-dropped) ones.
279
+
280
+ ##### 3f.4 Branch on the rollup verdict
281
+
282
+ **All required children terminal** (every required top-level child is terminal; at least one required child exists):
283
+
284
+ 1. Transition labels: remove `$TICKETED`, add `$SHIPPED` via `mcp__linear-server__save_project({id, labels})`. Verify exactly one lifecycle label remains (the single-label invariant).
285
+ 2. **If `$CLOSE_ON_SHIPPED` is `true`**, move the PRD project to a closed/archived state via `save_project` (set the project to a completed/canceled state or archive it). When `false`, leave it active.
286
+ 3. Post a short rollup comment on the sentinel feedback issue naming the terminal child set and (when dropped children exist) the dropped set, so the audit trail records *why* the PRD shipped. Lead with `"Shipped by Claude — all generated top-level work is complete."`
287
+
288
+ **Any required child incomplete / blocked**:
289
+
290
+ 1. Leave the PRD label as `$TICKETED` and leave the project **active**. Do NOT add `$SHIPPED`. Do NOT archive.
291
+ 2. Report the incomplete child set — both in the cycle summary and, when at least one cycle has previously ticketed this PRD, as a single advisory comment on the sentinel feedback issue listing the still-open children (`- <ref> "<title>" — <state>`), so product can see what's blocking the rollup. Keep it idempotent: regenerate the advisory rather than appending a fresh one each cycle.
292
+
293
+ ##### 3f.5 Rollup cites the rule
294
+
295
+ This phase implements exactly one PRD-lifecycle hop — `$TICKETED → $SHIPPED` — and the optional config-gated archive that follows it. All terminal-state semantics, the generated-top-level-work boundary, and the dedupe-by-child-ref idempotency come from the `prd-lifecycle-rollup` rule; this skill is its Linear implementation, not a second source of truth.
296
+
226
297
  ### Phase 4 — Summary report
227
298
 
228
299
  After processing every ready PRD, emit a summary:
@@ -269,10 +340,11 @@ Idempotency: the helper finds-or-creates. Re-runs of the cycle reuse the same se
269
340
  ## Idempotency & safety
270
341
 
271
342
  - **Single-cycle scope**: this skill processes the ready set as it exists at the start of Phase 2. New `$READY` projects added mid-cycle are picked up next run.
272
- - **No writes outside the lifecycle**: this skill only ever writes to the destination tracker via `lisa:linear-to-tracker` (which delegates to `lisa:tracker-write`), only ever changes Linear project labels among `$IN_REVIEW`, `$BLOCKED`, `$TICKETED`, only ever creates/comments on the sentinel feedback issue (never any other Linear issue). It never edits project descriptions, never edits Linear documents, never touches the `draft` or `shipped` labels, never archives or deletes projects.
343
+ - **No writes outside the lifecycle**: this skill only ever writes to the destination tracker via `lisa:linear-to-tracker` (which delegates to `lisa:tracker-write`), only ever changes Linear project labels among `$IN_REVIEW`, `$BLOCKED`, `$TICKETED`, and `$SHIPPED` (the last via the rollup phase 3f only), only ever creates/comments on the sentinel feedback issue (never any other Linear issue). It never edits project descriptions, never edits Linear documents, never touches the `draft` label, and never deletes projects. It sets the `$SHIPPED` label and may archive the PRD project **only** through the config-gated rollup phase (3f).
273
344
  - **Claim-first ordering**: the label flip to `$IN_REVIEW` happens BEFORE validation runs, so a re-entrant call won't double-process.
274
345
  - **Failure isolation**: an exception processing one project must not stop the cycle. Catch, record under "Errors" in the summary, continue to the next project. The project that errored is left labelled `$IN_REVIEW` — the human investigates from there.
275
346
  - **Single-label invariant**: after every transition, verify exactly one lifecycle label is present on the project. If two are present (rare race), surface as an Error and skip — do NOT auto-resolve, the human decides.
347
+ - **Rollup idempotency**: rollup (Phase 3f) is a no-op on a PRD project already carrying `$SHIPPED` (and already archived when `closeOnShipped` is `true`) — no duplicate transition, no duplicate archive, no duplicate comment. The all-terminal condition is a pure function of the children's current states (deduped by child-ref identity), so recomputing it is safe to re-run. Archival NEVER precedes the all-terminal condition.
276
348
 
277
349
  ## Configuration
278
350
 
@@ -289,13 +361,15 @@ Destination tracker config (jira / github / linear) is consumed by `lisa:tracker
289
361
  | `.lisa.config.json` `linear.labels.prd.in_review` | `prd-in-review` | Project label set on claim |
290
362
  | `.lisa.config.json` `linear.labels.prd.blocked` | `prd-blocked` | Project label set on validation failure |
291
363
  | `.lisa.config.json` `linear.labels.prd.ticketed` | `prd-ticketed` | Project label set on success |
292
- | `.lisa.config.json` `linear.labels.prd.shipped` | `prd-shipped` | Project label product sets after delivery |
364
+ | `.lisa.config.json` `linear.labels.prd.shipped` | `prd-shipped` | Project label set by the rollup phase (3f) when all generated top-level work is terminal; product may also set it by hand |
365
+ | `.lisa.config.json` `linear.labels.prd.rollup.closeOnShipped` | `false` | When `true`, rollup archives the PRD project after the `$SHIPPED` transition; when `false`, sets `$SHIPPED` and leaves it active |
293
366
  | `.lisa.config.json` `linear.labels.prd.sentinel` | `prd-intake-feedback` | Issue-level label marking the sentinel feedback issue |
294
367
 
295
368
  ## Rules
296
369
 
297
370
  - Never write to the destination tracker outside of `lisa:linear-to-tracker` → `lisa:tracker-write`. The validator's verdict gates progress; bypassing it produces broken tickets.
298
- - Never add or remove a label this skill doesn't own (`$IN_REVIEW`, `$BLOCKED`, `$TICKETED`). Product owns the `draft`, `ready`, and `shipped` labels. The issue-level `$SENTINEL` label is owned by this skill but is not a lifecycle label.
371
+ - Never add or remove a label this skill doesn't own (`$IN_REVIEW`, `$BLOCKED`, `$TICKETED`, and `$SHIPPED` via the rollup phase only). Product owns the `draft` and `ready` labels; product and the rollup phase (3f) both set `shipped`. The issue-level `$SENTINEL` label is owned by this skill but is not a lifecycle label.
372
+ - Set `$SHIPPED` (and archive the PRD project when `closeOnShipped` is configured) only from the rollup phase, and only when all generated top-level children are terminal per the `prd-lifecycle-rollup` rule. Never ship or archive on partial completion.
299
373
  - Never edit a project's description or any attached Linear document. Communication with product happens only through comments on sub-issues or on the sentinel feedback issue.
300
374
  - Never post a single dump of all gate failures on one comment. One comment per `prd_anchor` group on the relevant sub-issue (or one comment on the sentinel feedback issue for unanchored failures only). Comments must be sub-issue-anchored where possible, categorized, plain-language, and contain a concrete recommendation.
301
375
  - Never include a gate ID, internal skill name, or engineering shorthand in a comment body.
@@ -37,10 +37,23 @@ BLOCKED=$(read_role blocked "Blocked")
37
37
  TICKETED=$(read_role ticketed "Ticketed")
38
38
  SHIPPED=$(read_role shipped "Shipped")
39
39
  STATUS_PROP=$(jq -r '.notion.statusProperty // "Status"' .lisa.config.json 2>/dev/null)
40
+
41
+ # Resolve a boolean rollup flag. Local overrides global per-key; default when unset.
42
+ read_rollup_flag() {
43
+ local key="$1" default="$2"
44
+ local local_v global_v
45
+ local_v=$(jq -r ".notion.rollup.${key} // empty" .lisa.config.local.json 2>/dev/null)
46
+ global_v=$(jq -r ".notion.rollup.${key} // empty" .lisa.config.json 2>/dev/null)
47
+ echo "${local_v:-${global_v:-$default}}"
48
+ }
49
+
50
+ CLOSE_ON_SHIPPED=$(read_rollup_flag closeOnShipped false)
40
51
  ```
41
52
 
42
53
  In prose below, the role names refer to the resolved values: e.g. "the `ready` status" means whatever `notion.values.ready` resolves to (default: `Ready`).
43
54
 
55
+ This skill shares its PRD closure rollup phase (3f) with `lisa:github-prd-intake`, `lisa:linear-prd-intake`, and `lisa:confluence-prd-intake`. The phases, gates, comment templates, and rollup behavior are identical across all four intake skills — only the vendor surface differs. Keep all four behaviorally aligned: when changing intake logic — including the rollup phase — change them together. The **PRD closure rollup phase (3f)** transitions a `$TICKETED` PRD to `$SHIPPED` (and optionally archives it) once all its generated top-level work is terminal, per the `prd-lifecycle-rollup` rule; this is the Notion leg of the same vendor-neutral rollup (LPC-1.3 #584), using the documented generated-work section since Notion has no native ticket hierarchy.
56
+
44
57
  ## Confirmation policy
45
58
 
46
59
  Do NOT ask the caller whether to proceed. Once invoked with a database URL, run the cycle to completion — claim, validate, branch to `blocked` or `ticketed`, write the summary. The caller (a human or a cron) has already authorized the run by invoking the skill; re-prompting defeats the purpose of a background batch.
@@ -67,7 +80,7 @@ draft → ready → in_review → blocked | ticketed → shipped
67
80
  (product) (us) (us) (product)
68
81
  ```
69
82
 
70
- This skill ONLY transitions `ready → in_review`, then `in_review → blocked` or `in_review → ticketed`. Never touches `draft` or `shipped`.
83
+ This skill transitions `ready → in_review`, then `in_review → blocked` or `in_review → ticketed`, then (via the rollup phase 3f) `ticketed → shipped`. It never touches `draft` that status is owned by product. The `shipped` status is set by this skill's **rollup phase (3f)** when, and only when, the PRD's generated top-level work is all terminal — per the `prd-lifecycle-rollup` rule; product may also set it by hand. Rollup never advances a PRD to `shipped` on partial completion, and never archives a PRD page unless `notion.rollup.closeOnShipped` is configured `true` (default `false` → set `$SHIPPED`, leave the page active).
71
84
 
72
85
  ## Phases
73
86
 
@@ -226,6 +239,61 @@ The access skill resolves a `prd_anchor` substring to the matching block ID by p
226
239
 
227
240
  Move to the next ready PRD. One PRD failing does not affect others.
228
241
 
242
+ #### 3f. PRD closure rollup (config-gated)
243
+
244
+ A PRD's lifecycle terminal state (`shipped`) is **derived** from whether the work it generated is done — it is never set by hand here on its own authority. This phase implements the Notion leg of that derivation, per the `prd-lifecycle-rollup` rule (cite it by slug; do not restate its taxonomy or terminal-state semantics here). It is behaviorally identical to `lisa:github-prd-intake`'s Phase 3f — only the vendor surface (a Notion status property via `lisa:notion-access` + the documented generated-work section) differs from GitHub's (issue close + labels via `gh`).
245
+
246
+ Rollup runs over PRD pages that are already `$TICKETED` (the only state from which a PRD can ship): the freshly-ticketed PRD from Phase 3c, and — because rollup also catches PRDs whose children finished in a *later* cycle — every page currently in `$STATUS_PROP = $TICKETED` (re-query the database with operation `query-database` filtered on the ticketed status). Process each independently; one PRD never blocks another's rollup.
247
+
248
+ ##### 3f.0 Resolve closure config
249
+
250
+ Closure is gated on `notion.rollup.closeOnShipped` (default `false`), resolved via `read_rollup_flag` (defined in the Workflow resolution block, same local-overrides-global precedence the status values use):
251
+
252
+ ```bash
253
+ CLOSE_ON_SHIPPED=$(read_rollup_flag closeOnShipped false)
254
+ ```
255
+
256
+ When `false` (the default), rollup sets `$STATUS_PROP = $SHIPPED` but leaves the page **active** for a human to archive. When `true`, rollup also archives the page (where Notion supports archival) after the `shipped` transition. Closure NEVER happens before all generated top-level work is terminal (`prd-lifecycle-rollup` rule; PRD #525 non-goal).
257
+
258
+ ##### 3f.1 Idempotency guard (no-op if already shipped)
259
+
260
+ Rollup is keyed by the PRD's current state. If the PRD already has `$STATUS_PROP = $SHIPPED` (and is already archived, when `$CLOSE_ON_SHIPPED` is `true`), it is a **no-op** — do not re-transition, do not re-archive, do not re-comment. Record it as `already shipped (no-op)` in the cycle summary and move on. This is what makes re-running intake safe.
261
+
262
+ ##### 3f.2 Read the generated top-level child set
263
+
264
+ Read the PRD's **generated top-level work** — its created Epics and any top-level Stories created directly under it, **excluding** leaf Sub-tasks and any Story nested under a generated Epic (`prd-lifecycle-rollup` rule, generated-top-level-work contract). Notion has **no native ticket hierarchy**, so the child set comes from the documented section only:
265
+
266
+ 1. **Documented `## Tickets` section (primary and only source).** Parse the machine-readable generated-work section `lisa:prd-backlink` writes to the PRD body (`## Tickets`, alias `## Generated Work`; see #582) by invoking `lisa:notion-access` operation `read-page` on the PRD. Top-level children are the `### <Epic key>: <title>` group headers' first line (`- [<ref>](<url>) — Epic`) plus any top-level Story listed directly under `### Unparented items`. Lines nested deeper (` - ... — Story:` under an Epic, ` - ... — Sub-task:`) are descendants, NOT top-level children — skip them.
267
+
268
+ Dedupe the resulting child set by **child-ref identity** — the destination ticket ref recorded in each generated-work entry (the entry is keyed by that ref, not by list position) — per the `prd-lifecycle-rollup` idempotency dedupe key. If the section yields no child (the PRD generated nothing, or the relationship was never recorded), record `no generated top-level children — rollup skipped` and leave the PRD as `$TICKETED`; do not ship an empty PRD.
269
+
270
+ ##### 3f.3 Apply the terminal-state predicate
271
+
272
+ For each top-level child, classify per the `prd-lifecycle-rollup` Confluence/Notion predicate:
273
+
274
+ - **Terminal (shipped).** The documented generated-work entry for the child is marked **done** in the PRD's machine-readable section (the durable equivalent of a closed ticket, since Notion has no native ticket state). A child Epic is terminal only when it has itself rolled up to its own terminal state per `leaf-only-lifecycle` — read the child's own recorded state; do not re-derive it from its leaves here.
275
+ - **Terminal-but-dropped.** The entry is marked won't-do / canceled. Like a not-planned leaf, it does not hold the PRD open and is excluded from the shipped set.
276
+ - **Incomplete / blocked.** Anything else: the entry is not yet marked done. Holds the PRD open.
277
+
278
+ The set of **required** children for the all-terminal check is the top-level children minus the terminal-but-dropped ones.
279
+
280
+ ##### 3f.4 Branch on the rollup verdict
281
+
282
+ **All required children terminal** (every required top-level child is terminal; at least one required child exists):
283
+
284
+ 1. Set `$STATUS_PROP = $SHIPPED` by invoking `lisa:notion-access` operation `write-page` with payload `{ "id": "<PRD-page-id>", "properties": { "<STATUS_PROP>": { "status": { "name": "<SHIPPED>" } } } }` (use `"select"` instead of `"status"` if the property is a select).
285
+ 2. **If `$CLOSE_ON_SHIPPED` is `true`**, archive the PRD page via `lisa:notion-access` (set `archived: true` where supported). When `false`, leave it active.
286
+ 3. Post a short rollup Notion comment naming the terminal child set and (when dropped children exist) the dropped set, so the audit trail records *why* the PRD shipped. Lead with `"Shipped by Claude — all generated top-level work is complete."`
287
+
288
+ **Any required child incomplete / blocked**:
289
+
290
+ 1. Leave `$STATUS_PROP = $TICKETED` and leave the page **active**. Do NOT set `$SHIPPED`. Do NOT archive.
291
+ 2. Report the incomplete child set — both in the cycle summary and, when at least one cycle has previously ticketed this PRD, as a single advisory Notion comment listing the still-open children (`- <ref> "<title>" — <state>`), so product can see what's blocking the rollup. Keep it idempotent: regenerate the advisory rather than appending a fresh one each cycle.
292
+
293
+ ##### 3f.5 Rollup cites the rule
294
+
295
+ This phase implements exactly one PRD-lifecycle hop — `$TICKETED → $SHIPPED` — and the optional config-gated archive that follows it. All terminal-state semantics, the generated-top-level-work boundary, and the dedupe-by-child-ref idempotency come from the `prd-lifecycle-rollup` rule; this skill is its Notion implementation, not a second source of truth.
296
+
229
297
  ### Phase 4 — Summary report
230
298
 
231
299
  After processing every ready PRD, emit a summary:
@@ -254,9 +322,10 @@ Print to the agent's output. Do not write this summary to Notion or the destinat
254
322
  ## Idempotency & safety
255
323
 
256
324
  - **Single-cycle scope**: this skill processes the ready set as it exists at the start of Phase 2. New ready PRDs added mid-cycle are picked up next run.
257
- - **No writes outside the lifecycle**: this skill only ever writes to the destination tracker via `lisa:notion-to-tracker` (which delegates to `lisa:tracker-write`), and only ever changes the Notion status property to `$IN_REVIEW`, `$BLOCKED`, or `$TICKETED`. It never edits PRD content, never touches `$DRAFT` or `$SHIPPED`, never deletes pages.
325
+ - **No writes outside the lifecycle**: this skill only ever writes to the destination tracker via `lisa:notion-to-tracker` (which delegates to `lisa:tracker-write`), and only ever changes the Notion status property to `$IN_REVIEW`, `$BLOCKED`, `$TICKETED`, or `$SHIPPED` (the last via the rollup phase 3f only). It never edits PRD content, never touches `$DRAFT`, never deletes pages. It sets `$SHIPPED` and may archive the PRD page **only** through the config-gated rollup phase (3f).
258
326
  - **Claim-first ordering**: the status flip to `$IN_REVIEW` is set BEFORE validation runs, so a re-entrant call won't double-process.
259
327
  - **Failure isolation**: an exception processing one PRD must not stop the cycle. Catch, record under "Errors" in the summary, continue to the next PRD. The PRD that errored is left in `$IN_REVIEW` — the human investigates from there.
328
+ - **Rollup idempotency**: rollup (Phase 3f) is a no-op on a PRD already in `$STATUS_PROP = $SHIPPED` (and already archived when `closeOnShipped` is `true`) — no duplicate transition, no duplicate archive, no duplicate comment. The all-terminal condition is a pure function of the children's current states (deduped by child-ref identity), so recomputing it is safe to re-run. Archival NEVER precedes the all-terminal condition.
260
329
 
261
330
  ## Configuration
262
331
 
@@ -273,7 +342,8 @@ This skill reads project configuration from `.lisa.config.json` (with `.lisa.con
273
342
  | `notion.values.in_review` | `In Review` | Value the agent sets on claim |
274
343
  | `notion.values.blocked` | `Blocked` | Value the agent sets on validation failure |
275
344
  | `notion.values.ticketed` | `Ticketed` | Value the agent sets on success |
276
- | `notion.values.shipped` | `Shipped` | Value product sets after delivery (agent never writes) |
345
+ | `notion.values.shipped` | `Shipped` | Value the rollup phase (3f) sets when all generated top-level work is terminal; product may also set it by hand |
346
+ | `notion.rollup.closeOnShipped` | `false` | When `true`, rollup archives the PRD page after the `$SHIPPED` transition; when `false`, sets `$SHIPPED` and leaves the page active |
277
347
 
278
348
  ### From environment variables
279
349
 
@@ -286,7 +356,8 @@ This skill reads project configuration from `.lisa.config.json` (with `.lisa.con
286
356
  ## Rules
287
357
 
288
358
  - Never write to the destination tracker outside of `lisa:notion-to-tracker` → `lisa:tracker-write`. The validator's verdict gates progress; bypassing it produces broken tickets.
289
- - Never set the Notion status to a value this skill doesn't own (`$IN_REVIEW`, `$BLOCKED`, `$TICKETED`). Product owns `$DRAFT`, `$READY`, `$SHIPPED`.
359
+ - Never set the Notion status to a value this skill doesn't own (`$IN_REVIEW`, `$BLOCKED`, `$TICKETED`, and `$SHIPPED` via the rollup phase only). Product owns `$DRAFT` and `$READY`; product and the rollup phase (3f) both set `$SHIPPED`.
360
+ - Set `$SHIPPED` (and archive the PRD page when `closeOnShipped` is configured) only from the rollup phase, and only when all generated top-level children are terminal per the `prd-lifecycle-rollup` rule. Never ship or archive on partial completion.
290
361
  - Never edit the PRD's body. Communication with product happens only through Notion comments.
291
362
  - Never post a single page-level dump of all gate failures. One comment per `prd_anchor` group (or one page-level summary for unanchored failures only). The audience is product, not engineers — comments must be block-anchored, categorized, plain-language, and contain a concrete recommendation. See Phase 3c.3 for the required template and Phase 3c.5 for forbidden language.
292
363
  - Never include a gate ID, internal skill name, or engineering shorthand in a Notion comment body. If the validator's `what` or `recommendation` field uses one, paraphrase before posting.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.45.0",
3
+ "version": "2.46.0",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.45.0",
3
+ "version": "2.46.0",
4
4
  "description": "AWS CDK-specific Lisa plugin.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "2.45.0",
3
+ "version": "2.46.0",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "2.45.0",
3
+ "version": "2.46.0",
4
4
  "description": "Expo and React Native-specific skills, agents, rules, and MCP servers.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "2.45.0",
3
+ "version": "2.46.0",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "2.45.0",
3
+ "version": "2.46.0",
4
4
  "description": "Harper/Fabric-specific Lisa rules for TypeScript component apps.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "2.45.0",
3
+ "version": "2.46.0",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "2.45.0",
3
+ "version": "2.46.0",
4
4
  "description": "NestJS-specific skills and migration write-protection hooks.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "2.45.0",
3
+ "version": "2.46.0",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "2.45.0",
3
+ "version": "2.46.0",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "2.45.0",
3
+ "version": "2.46.0",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "2.45.0",
3
+ "version": "2.46.0",
4
4
  "description": "Ruby on Rails-specific skills and hooks for RuboCop and ast-grep scanning on edit.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "2.45.0",
3
+ "version": "2.46.0",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "2.45.0",
3
+ "version": "2.46.0",
4
4
  "description": "TypeScript-specific hooks for formatting, linting, and ast-grep scanning on edit.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.45.0",
3
+ "version": "2.46.0",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.45.0",
3
+ "version": "2.46.0",
4
4
  "description": "Distributable LLM Wiki kernel — ingest, query, lint, and maintain a git-native markdown knowledge base across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -59,11 +59,26 @@ current_role_for_prd() {
59
59
  done
60
60
  echo "unknown"
61
61
  }
62
+
63
+ # Resolve a boolean rollup flag. Local overrides global per-key; default when unset.
64
+ # NOTE: Confluence rollup config lives under `confluence.rollup` (NOT
65
+ # `confluence.parents.rollup`) — see the config-resolution rule.
66
+ read_rollup_flag() {
67
+ local key="$1" default="$2"
68
+ local local_v global_v
69
+ local_v=$(jq -r ".confluence.rollup.${key} // empty" .lisa.config.local.json 2>/dev/null)
70
+ global_v=$(jq -r ".confluence.rollup.${key} // empty" .lisa.config.json 2>/dev/null)
71
+ echo "${local_v:-${global_v:-$default}}"
72
+ }
73
+
74
+ CLOSE_ON_SHIPPED=$(read_rollup_flag closeOnShipped false)
62
75
  ```
63
76
 
64
77
  In prose below, the role names refer to the resolved parent-page IDs: e.g. "the `ready` parent" means whatever `confluence.parents.ready` resolves to.
65
78
 
66
- This skill is the Confluence counterpart of `lisa:notion-prd-intake`. The phases, gates, comment templates, and rules are identical — the only differences are (1) the lifecycle is encoded as **parent-page placement** instead of a status property, and (2) the fetch / comment / update tools route through `lisa:atlassian-access`. Keep the two skills behaviorally aligned: when changing intake logic, change BOTH skills together.
79
+ This skill is the Confluence counterpart of `lisa:notion-prd-intake`, and shares its PRD closure rollup phase (3f) with `lisa:github-prd-intake` and `lisa:linear-prd-intake`. The phases, gates, comment templates, and rules are identical — the only differences are (1) the lifecycle is encoded as **parent-page placement** instead of a status property, and (2) the fetch / comment / update tools route through `lisa:atlassian-access`. Keep all four intake skills behaviorally aligned: when changing intake logic — including the rollup phase — change them together.
80
+
81
+ The **PRD closure rollup phase (3f)** re-parents a `ticketed` PRD to the `shipped` parent (and optionally archives it) once all its generated top-level work is terminal, per the `prd-lifecycle-rollup` rule. This is the Confluence leg of the same vendor-neutral rollup that `lisa:github-prd-intake` implements for GitHub (LPC-1.3 #584); only the vendor surface (parent-page placement + documented generated-work section, since Confluence has no native ticket hierarchy) differs.
67
82
 
68
83
  ## Confirmation policy
69
84
 
@@ -93,13 +108,15 @@ draft → ready → in_review → blocked | ticketed → shipped
93
108
 
94
109
  A PRD's current state is determined entirely by which lifecycle parent it sits under. Re-parenting is the transition.
95
110
 
96
- This skill ONLY transitions:
111
+ This skill transitions:
97
112
 
98
113
  - `ready` → `in_review` (claim)
99
114
  - `in_review` → `blocked` (gate failures or coverage gaps)
100
115
  - `in_review` → `ticketed` (success)
116
+ - `ticketed` → `blocked` (post-write coverage gaps from Phase 3e)
117
+ - `ticketed` → `shipped` (PRD closure rollup, Phase 3f — only when **all** generated top-level children are terminal)
101
118
 
102
- It never re-parents PRDs into or out of the `draft` or `shipped` parents. Those parents are owned by product.
119
+ It never re-parents PRDs into or out of the `draft` parent — that parent is owned by product. The `shipped` parent is set by this skill's **rollup phase (3f)** when, and only when, the PRD's generated top-level work is all terminal — per the `prd-lifecycle-rollup` rule; product may also re-parent there by hand. Rollup never advances a PRD to `shipped` on partial completion, and never archives a PRD page unless `confluence.rollup.closeOnShipped` is configured `true` (default `false` → re-parent under `shipped`, leave the page active).
103
120
 
104
121
  A "transition" means: update the PRD's `parentId` to the new role's parent-page id via `lisa:atlassian-access` `operation: write-page payload: { id, parentId, title, version: { number: <next> } }`. The v2 PUT endpoint requires the next version number and the page title in the payload; the body content is not strictly required for a re-parent-only edit, but some Atlassian deployments reject PUTs without a body. The skill MUST therefore GET the page first via `read-page`, capture title + current version + current body, then PUT with `parentId` swapped and `version.number` bumped — preserving body content is non-negotiable, this skill never edits PRD content. See `transition_prd` helper in Phase 3a for the canonical implementation.
105
122
 
@@ -276,6 +293,61 @@ Per-ticket gates prove each ticket is well-formed; they do NOT prove the *set* o
276
293
 
277
294
  3. The created tickets remain in the destination tracker regardless of the verdict — they are valid in their own right. The audit only tells us whether *more* are needed.
278
295
 
296
+ #### 3f. PRD closure rollup (config-gated)
297
+
298
+ A PRD's lifecycle terminal state (`shipped`) is **derived** from whether the work it generated is done — it is never set by hand here on its own authority. This phase implements the Confluence leg of that derivation, per the `prd-lifecycle-rollup` rule (cite it by slug; do not restate its taxonomy or terminal-state semantics here). It is behaviorally identical to `lisa:github-prd-intake`'s Phase 3f — only the vendor surface (parent-page re-parenting via `lisa:atlassian-access` + the documented generated-work section) differs from GitHub's (issue close + labels via `gh`).
299
+
300
+ Rollup runs over PRD pages that are already under the `ticketed` parent (the only state from which a PRD can ship): the freshly-ticketed PRD from Phase 3c, and — because rollup also catches PRDs whose children finished in a *later* cycle — every page currently parented under `$TICKETED_PARENT`. (Re-read its direct children via `lisa:atlassian-access` `operation: read-page-descendants id: $TICKETED_PARENT`.) Process each independently; one PRD never blocks another's rollup.
301
+
302
+ ##### 3f.0 Resolve closure config
303
+
304
+ Closure is gated on `confluence.rollup.closeOnShipped` (default `false`), resolved via `read_rollup_flag` (defined in the Workflow resolution block, same local-overrides-global precedence the lifecycle parents use). Note the config lives under `confluence.rollup` (NOT `confluence.parents.rollup`):
305
+
306
+ ```bash
307
+ CLOSE_ON_SHIPPED=$(read_rollup_flag closeOnShipped false)
308
+ ```
309
+
310
+ When `false` (the default), rollup re-parents the PRD under `$SHIPPED_PARENT` but leaves the page **active** for a human to archive. When `true`, rollup also archives the page (where the deployment supports archival) after the `shipped` re-parent. Closure NEVER happens before all generated top-level work is terminal (`prd-lifecycle-rollup` rule; PRD #525 non-goal).
311
+
312
+ ##### 3f.1 Idempotency guard (no-op if already shipped)
313
+
314
+ Rollup is keyed by the PRD's current state. If the PRD is already parented under `$SHIPPED_PARENT` (and is already archived, when `$CLOSE_ON_SHIPPED` is `true`), it is a **no-op** — do not re-parent, do not re-archive, do not re-comment. Record it as `already shipped (no-op)` in the cycle summary and move on. This is what makes re-running intake safe.
315
+
316
+ ##### 3f.2 Read the generated top-level child set
317
+
318
+ Read the PRD's **generated top-level work** — its created Epics and any top-level Stories created directly under it, **excluding** leaf Sub-tasks and any Story nested under a generated Epic (`prd-lifecycle-rollup` rule, generated-top-level-work contract). Confluence has **no native ticket hierarchy**, so the child set comes from the documented section only:
319
+
320
+ 1. **Documented `## Tickets` section (primary and only source).** Parse the machine-readable generated-work section `lisa:prd-backlink` writes to the PRD body (`## Tickets`, alias `## Generated Work`; see #582) via `lisa:atlassian-access` `operation: read-page id: <PRD-ID>`. Top-level children are the `### <Epic key>: <title>` group headers' first line (`- [<ref>](<url>) — Epic`) plus any top-level Story listed directly under `### Unparented items`. Lines nested deeper (` - ... — Story:` under an Epic, ` - ... — Sub-task:`) are descendants, NOT top-level children — skip them.
321
+
322
+ Dedupe the resulting child set by **child-ref identity** — the destination ticket ref recorded in each generated-work entry (the entry is keyed by that ref, not by list position) — per the `prd-lifecycle-rollup` idempotency dedupe key. If the section yields no child (the PRD generated nothing, or the relationship was never recorded), record `no generated top-level children — rollup skipped` and leave the PRD under `$TICKETED_PARENT`; do not ship an empty PRD.
323
+
324
+ ##### 3f.3 Apply the terminal-state predicate
325
+
326
+ For each top-level child, classify per the `prd-lifecycle-rollup` Confluence/Notion predicate:
327
+
328
+ - **Terminal (shipped).** The documented generated-work entry for the child is marked **done** in the PRD's machine-readable section (the durable equivalent of a closed ticket, since Confluence has no native ticket state). A child Epic is terminal only when it has itself rolled up to its own terminal state per `leaf-only-lifecycle` — read the child's own recorded state; do not re-derive it from its leaves here.
329
+ - **Terminal-but-dropped.** The entry is marked won't-do / canceled. Like a not-planned leaf, it does not hold the PRD open and is excluded from the shipped set.
330
+ - **Incomplete / blocked.** Anything else: the entry is not yet marked done. Holds the PRD open.
331
+
332
+ The set of **required** children for the all-terminal check is the top-level children minus the terminal-but-dropped ones.
333
+
334
+ ##### 3f.4 Branch on the rollup verdict
335
+
336
+ **All required children terminal** (every required top-level child is terminal; at least one required child exists):
337
+
338
+ 1. Re-parent to `shipped`: `transition_prd "$PRD_ID" shipped` (GET-then-PUT via `lisa:atlassian-access` preserving the body verbatim). After the re-parent, re-read the page and confirm `parentId` matches `$SHIPPED_PARENT` (the single-parent invariant).
339
+ 2. **If `$CLOSE_ON_SHIPPED` is `true`**, archive the PRD page where the deployment supports archival. When `false`, leave it active.
340
+ 3. Post a short rollup footer comment via `lisa:atlassian-access` `operation: comment-page kind: footer` naming the terminal child set and (when dropped children exist) the dropped set, so the audit trail records *why* the PRD shipped. Lead with `"Shipped by Claude — all generated top-level work is complete."`
341
+
342
+ **Any required child incomplete / blocked**:
343
+
344
+ 1. Leave the PRD under `$TICKETED_PARENT` and leave the page **active**. Do NOT re-parent to `shipped`. Do NOT archive.
345
+ 2. Report the incomplete child set — both in the cycle summary and, when at least one cycle has previously ticketed this PRD, as a single advisory footer comment listing the still-open children (`- <ref> "<title>" — <state>`), so product can see what's blocking the rollup. Keep it idempotent: regenerate the advisory rather than appending a fresh one each cycle.
346
+
347
+ ##### 3f.5 Rollup cites the rule
348
+
349
+ This phase implements exactly one PRD-lifecycle hop — `ticketed → shipped` — and the optional config-gated archive that follows it. All terminal-state semantics, the generated-top-level-work boundary, and the dedupe-by-child-ref idempotency come from the `prd-lifecycle-rollup` rule; this skill is its Confluence implementation, not a second source of truth.
350
+
279
351
  ### Phase 4 — Summary report
280
352
 
281
353
  After processing every ready PRD, emit a summary:
@@ -304,10 +376,11 @@ Print to the agent's output. Do not write this summary to Confluence or the dest
304
376
  ## Idempotency & safety
305
377
 
306
378
  - **Single-cycle scope**: this skill processes the ready set as it exists at the start of Phase 2. New PRDs moved under the `ready` parent mid-cycle are picked up next run.
307
- - **No writes outside the lifecycle**: this skill only ever writes to the destination tracker via `lisa:confluence-to-tracker` (which delegates to `lisa:tracker-write`), and only ever re-parents PRDs among `in_review`, `blocked`, and `ticketed` via `lisa:atlassian-access` `operation: write-page`. It never edits PRD body content, never re-parents into or out of `draft` / `shipped`, never deletes pages.
379
+ - **No writes outside the lifecycle**: this skill only ever writes to the destination tracker via `lisa:confluence-to-tracker` (which delegates to `lisa:tracker-write`), and only ever re-parents PRDs among `in_review`, `blocked`, `ticketed`, and `shipped` (the last via the rollup phase 3f only) via `lisa:atlassian-access` `operation: write-page`. It never edits PRD body content, never re-parents into or out of `draft`, never deletes pages. It re-parents under `shipped` and may archive the PRD page **only** through the config-gated rollup phase (3f).
308
380
  - **Claim-first ordering**: the re-parent to `in_review` happens BEFORE validation runs, so a re-entrant call won't double-process.
309
381
  - **Failure isolation**: an exception processing one PRD must not stop the cycle. Catch, record under "Errors" in the summary, continue to the next PRD. The PRD that errored is left under whatever parent it currently occupies (usually `in_review` if claim succeeded) — the human investigates from there.
310
382
  - **Single-parent invariant**: a page has exactly one parent by construction in Confluence — the multi-state ambiguity that label-based systems can hit (two `prd-*` labels simultaneously) cannot occur here. After every transition, re-read the page and confirm `parentId` matches the expected role; if not, surface as an Error and skip.
383
+ - **Rollup idempotency**: rollup (Phase 3f) is a no-op on a PRD already parented under `$SHIPPED_PARENT` (and already archived when `closeOnShipped` is `true`) — no duplicate re-parent, no duplicate archive, no duplicate comment. The all-terminal condition is a pure function of the children's current states (deduped by child-ref identity), so recomputing it is safe to re-run. Archival NEVER precedes the all-terminal condition.
311
384
 
312
385
  ## Configuration
313
386
 
@@ -325,12 +398,14 @@ Destination tracker config (jira / github / linear) is consumed by `lisa:tracker
325
398
  | `.lisa.config.json` `confluence.parents.blocked` | yes | Parent page id for "validation failure" |
326
399
  | `.lisa.config.json` `confluence.parents.ticketed` | yes | Parent page id for "successfully ticketed" |
327
400
  | `.lisa.config.json` `confluence.parents.draft` | recommended | Parent page id for PRDs still being drafted by product |
328
- | `.lisa.config.json` `confluence.parents.shipped` | recommended | Parent page id for delivered PRDs |
401
+ | `.lisa.config.json` `confluence.parents.shipped` | recommended | Parent page id the rollup phase (3f) re-parents delivered PRDs under; product may also use it by hand |
402
+ | `.lisa.config.json` `confluence.rollup.closeOnShipped` | no (default `false`) | When `true`, rollup archives the PRD page after the `shipped` re-parent; when `false`, re-parents under `shipped` and leaves the page active |
329
403
 
330
404
  ## Rules
331
405
 
332
406
  - Never write to the destination tracker outside of `lisa:confluence-to-tracker` → `lisa:tracker-write`. The validator's verdict gates progress; bypassing it produces broken tickets.
333
- - Never re-parent a PRD into a lifecycle parent this skill doesn't own (`in_review`, `blocked`, `ticketed`). Product owns `draft`, `ready` (as the entry signal), and `shipped`.
407
+ - Never re-parent a PRD into a lifecycle parent this skill doesn't own (`in_review`, `blocked`, `ticketed`, and `shipped` via the rollup phase only). Product owns `draft` and `ready` (as the entry signal); product and the rollup phase (3f) both re-parent under `shipped`.
408
+ - Re-parent under `shipped` (and archive the PRD page when `closeOnShipped` is configured) only from the rollup phase, and only when all generated top-level children are terminal per the `prd-lifecycle-rollup` rule. Never ship or archive on partial completion.
334
409
  - Never edit the PRD's body. Communication with product happens only through Confluence comments. The `write-page` call preserves the body verbatim — fetch then PUT with body unchanged.
335
410
  - Never post a single page-level dump of all gate failures. One inline comment per `prd_anchor` group (or one footer summary for unanchored failures only). Comments must be inline-anchored where possible, categorized, plain-language, and contain a concrete recommendation.
336
411
  - Never include a gate ID, internal skill name, or engineering shorthand in a comment body.
@@ -39,7 +39,7 @@ In prose below, the role names refer to the resolved labels: e.g. "the `ready` l
39
39
 
40
40
  This skill is the GitHub counterpart of `lisa:notion-prd-intake`, `lisa:confluence-prd-intake`, and `lisa:linear-prd-intake`. Phases, gates, comment templates, and rules are identical — the only differences are (1) the lifecycle is encoded as **issue labels** (mirroring Linear's project labels and Confluence's page labels), (2) the fetch / update tools are the `gh` CLI, and (3) clarifying-question comments land directly on the source PRD issue (because GitHub Issues *do* have native comments — no sentinel issue required, unlike Linear). Keep all four skills behaviorally aligned: when changing intake logic, change them together.
41
41
 
42
- The **PRD closure rollup phase (3f)** is, for now, GitHub-only: it transitions a `$TICKETED` PRD to `$SHIPPED` (and optionally closes it) once all its generated top-level work is terminal, per the `prd-lifecycle-rollup` rule. The Linear / Confluence / Notion counterparts gain the same rollup in sibling sub-task #584; until then, those three intake skills do not roll up to `shipped`, so this alignment note covers the shared intake/validation phases (1–3e), not yet the rollup phase.
42
+ The **PRD closure rollup phase (3f)** transitions a `$TICKETED` PRD to `$SHIPPED` (and optionally closes it) once all its generated top-level work is terminal, per the `prd-lifecycle-rollup` rule. This phase is GitHub-only here because its vendor surface (issue close + labels via `gh`) is GitHub-specific; the Linear / Confluence / Notion intake skills carry the **same** vendor-neutral rollup with their own surfaces (sibling sub-task #584, now landed). All four intake skills are behaviorally aligned across the rollup phase too keep them in sync when changing rollup logic.
43
43
 
44
44
  ## Confirmation policy
45
45
 
@@ -34,11 +34,24 @@ BLOCKED=$(read_role blocked "prd-blocked")
34
34
  TICKETED=$(read_role ticketed "prd-ticketed")
35
35
  SHIPPED=$(read_role shipped "prd-shipped")
36
36
  SENTINEL=$(read_role sentinel "prd-intake-feedback")
37
+
38
+ # Resolve a boolean rollup flag. Local overrides global per-key; default when unset.
39
+ read_rollup_flag() {
40
+ local key="$1" default="$2"
41
+ local local_v global_v
42
+ local_v=$(jq -r ".linear.labels.prd.rollup.${key} // empty" .lisa.config.local.json 2>/dev/null)
43
+ global_v=$(jq -r ".linear.labels.prd.rollup.${key} // empty" .lisa.config.json 2>/dev/null)
44
+ echo "${local_v:-${global_v:-$default}}"
45
+ }
46
+
47
+ CLOSE_ON_SHIPPED=$(read_rollup_flag closeOnShipped false)
37
48
  ```
38
49
 
39
50
  In prose below, the role names refer to the resolved labels: e.g. "the `ready` label" means whatever `linear.labels.prd.ready` resolves to (default: `prd-ready`).
40
51
 
41
- This skill is the Linear counterpart of `lisa:notion-prd-intake` and `lisa:confluence-prd-intake`. The phases, gates, comment templates, and rules are identical — the only differences are (1) the lifecycle is encoded as **project labels** instead of a status property, (2) the fetch / update tools are Linear MCP, and (3) clarifying-question comments land on a sentinel feedback Issue under the project (because Linear's MCP does not expose project-level comments). Keep all three skills behaviorally aligned: when changing intake logic, change them together.
52
+ This skill is the Linear counterpart of `lisa:notion-prd-intake` and `lisa:confluence-prd-intake`, and shares its PRD closure rollup phase (3f) with `lisa:github-prd-intake`. The phases, gates, comment templates, and rules are identical — the only differences are (1) the lifecycle is encoded as **project labels** instead of a status property, (2) the fetch / update tools are Linear MCP, and (3) clarifying-question comments land on a sentinel feedback Issue under the project (because Linear's MCP does not expose project-level comments). Keep all four intake skills behaviorally aligned: when changing intake logic — including the rollup phase — change them together.
53
+
54
+ The **PRD closure rollup phase (3f)** transitions a `$TICKETED` PRD project to `$SHIPPED` (and optionally closes/archives it) once all its generated top-level work is terminal, per the `prd-lifecycle-rollup` rule. This is the Linear leg of the same vendor-neutral rollup that `lisa:github-prd-intake` implements for GitHub (LPC-1.3 #584); only the vendor surface (Linear workflow states + project labels) differs.
42
55
 
43
56
  ## Confirmation policy
44
57
 
@@ -68,14 +81,15 @@ draft → ready → in_review → blocked | ticketed → shipped
68
81
 
69
82
  (Defaults: `prd-draft` / `prd-ready` / `prd-in-review` / `prd-blocked` / `prd-ticketed` / `prd-shipped`.)
70
83
 
71
- This skill ONLY transitions:
84
+ This skill transitions:
72
85
 
73
86
  - `$READY` → `$IN_REVIEW` (claim)
74
87
  - `$IN_REVIEW` → `$BLOCKED` (gate failures or coverage gaps)
75
88
  - `$IN_REVIEW` → `$TICKETED` (success)
76
89
  - `$TICKETED` → `$BLOCKED` (post-write coverage gaps from Phase 3e)
90
+ - `$TICKETED` → `$SHIPPED` (PRD closure rollup, Phase 3f — only when **all** generated top-level children are terminal)
77
91
 
78
- It never adds, removes, or touches the `draft` or `shipped` labels. Those labels are owned by product.
92
+ It never touches the `draft` label — that label is owned by product. The `shipped` label is set by this skill's **rollup phase (3f)** when, and only when, the PRD project's generated top-level work is all terminal — per the `prd-lifecycle-rollup` rule; product may also set it by hand. Rollup never advances a PRD to `shipped` on partial completion, and never archives a PRD project unless `linear.labels.prd.rollup.closeOnShipped` is configured `true` (default `false` → set `shipped`, leave the project active).
79
93
 
80
94
  A "transition" means: remove the old lifecycle label and add the new one in a single `save_project` call (passing the full new label set in the `labels` array). The skill MUST verify that exactly one lifecycle label exists on the project after the update — having two simultaneously breaks idempotency.
81
95
 
@@ -223,6 +237,63 @@ Per-ticket gates prove each ticket is well-formed; they do NOT prove the *set* o
223
237
 
224
238
  3. The created tickets remain in the destination tracker regardless of the verdict — they are valid in their own right. The audit only tells us whether *more* are needed.
225
239
 
240
+ #### 3f. PRD closure rollup (config-gated)
241
+
242
+ A PRD's lifecycle terminal state (`shipped`) is **derived** from whether the work it generated is done — it is never set by hand here on its own authority. This phase implements the Linear leg of that derivation, per the `prd-lifecycle-rollup` rule (cite it by slug; do not restate its taxonomy or terminal-state semantics here). It is behaviorally identical to `lisa:github-prd-intake`'s Phase 3f — only the vendor surface (Linear workflow states + project labels via Linear MCP) differs from GitHub's (issue close + labels via `gh`).
243
+
244
+ Rollup runs over PRD projects that are already `$TICKETED` (the only state from which a PRD can ship): the freshly-ticketed project from Phase 3c, and — because rollup also catches PRDs whose children finished in a *later* cycle — every project currently carrying `$TICKETED`. Process each independently; one PRD never blocks another's rollup.
245
+
246
+ ##### 3f.0 Resolve closure config
247
+
248
+ Closure is gated on `linear.labels.prd.rollup.closeOnShipped` (default `false`), resolved via `read_rollup_flag` (defined in the Workflow resolution block, same local-overrides-global precedence the lifecycle labels use):
249
+
250
+ ```bash
251
+ CLOSE_ON_SHIPPED=$(read_rollup_flag closeOnShipped false)
252
+ ```
253
+
254
+ When `false` (the default), rollup sets `$SHIPPED` but leaves the PRD project **active** for a human to archive. When `true`, rollup also moves the project to a closed/archived state after the `$SHIPPED` transition. Closure NEVER happens before all generated top-level work is terminal (`prd-lifecycle-rollup` rule; PRD #525 non-goal).
255
+
256
+ ##### 3f.1 Idempotency guard (no-op if already shipped)
257
+
258
+ Rollup is keyed by the PRD's current state. If the PRD project already carries `$SHIPPED` (and is already archived, when `$CLOSE_ON_SHIPPED` is `true`), it is a **no-op** — do not re-transition, do not re-archive, do not re-comment. Record it as `already shipped (no-op)` in the cycle summary and move on. This is what makes re-running intake safe.
259
+
260
+ ##### 3f.2 Read the generated top-level child set
261
+
262
+ Read the PRD's **generated top-level work** — its created Epics and any top-level Stories created directly under it, **excluding** leaf Sub-tasks and any Story nested under a generated Epic (`prd-lifecycle-rollup` rule, generated-top-level-work contract). Use two sources, native first:
263
+
264
+ 1. **Native parent / project relationships (primary).** Linear records the PRD→child relationship natively where the PRD also lives in Linear: a generated top-level Issue uses `parentId`, or a generated Project groups the generated Issues. Read the PRD project's generated top-level Issues via `mcp__linear-server__list_issues({project: <id>})` and take the **top-level** ones (no `parentId`, or whose parent is the PRD itself) — those are the PRD's direct children. Fetch each with `mcp__linear-server__get_issue` for its workflow state.
265
+
266
+ 2. **Documented `## Tickets` section (fallback).** When the native relationship is unavailable (the destination tracker is a *different* system — e.g. Linear PRD → JIRA tracker — so the children were never linked as Linear issues), parse the machine-readable generated-work section `lisa:prd-backlink` writes to the PRD (`## Tickets`, alias `## Generated Work`; see #582). Top-level children are the `### <Epic key>: <title>` group headers' first line (`- [<ref>](<url>) — Epic`) plus any top-level Story listed directly under `### Unparented items`. Lines nested deeper (` - ... — Story:` under an Epic, ` - ... — Sub-task:`) are descendants, NOT top-level children — skip them.
267
+
268
+ Dedupe the resulting child set by **child-ref identity** (the Linear issue/project identifier, e.g. `TEAM-123` or its UUID) so a child that appears both as a native relationship and in the documented section is counted once (`prd-lifecycle-rollup` idempotency dedupe key). If neither source yields any child (the PRD generated nothing, or the relationship was never recorded), record `no generated top-level children — rollup skipped` and leave the PRD as `$TICKETED`; do not ship an empty PRD.
269
+
270
+ ##### 3f.3 Apply the terminal-state predicate
271
+
272
+ For each top-level child, fetch its workflow state and classify per the `prd-lifecycle-rollup` Linear predicate:
273
+
274
+ - **Terminal (shipped).** The child Issue/Project is in a **completed** workflow state (the `completed` / `done`-category state). A child Epic is terminal only when it has itself rolled up to its own terminal state per `leaf-only-lifecycle` — read the child's own resolved state; do not re-derive it from its leaves here.
275
+ - **Terminal-but-dropped.** The child is in a **canceled** workflow state (the `canceled`-category state). Like a not-planned leaf, it does not hold the PRD open and is excluded from the shipped set.
276
+ - **Incomplete / blocked.** Anything else: any backlog / unstarted / started / triage workflow state. Holds the PRD open.
277
+
278
+ The set of **required** children for the all-terminal check is the top-level children minus the canceled (terminal-but-dropped) ones.
279
+
280
+ ##### 3f.4 Branch on the rollup verdict
281
+
282
+ **All required children terminal** (every required top-level child is terminal; at least one required child exists):
283
+
284
+ 1. Transition labels: remove `$TICKETED`, add `$SHIPPED` via `mcp__linear-server__save_project({id, labels})`. Verify exactly one lifecycle label remains (the single-label invariant).
285
+ 2. **If `$CLOSE_ON_SHIPPED` is `true`**, move the PRD project to a closed/archived state via `save_project` (set the project to a completed/canceled state or archive it). When `false`, leave it active.
286
+ 3. Post a short rollup comment on the sentinel feedback issue naming the terminal child set and (when dropped children exist) the dropped set, so the audit trail records *why* the PRD shipped. Lead with `"Shipped by Claude — all generated top-level work is complete."`
287
+
288
+ **Any required child incomplete / blocked**:
289
+
290
+ 1. Leave the PRD label as `$TICKETED` and leave the project **active**. Do NOT add `$SHIPPED`. Do NOT archive.
291
+ 2. Report the incomplete child set — both in the cycle summary and, when at least one cycle has previously ticketed this PRD, as a single advisory comment on the sentinel feedback issue listing the still-open children (`- <ref> "<title>" — <state>`), so product can see what's blocking the rollup. Keep it idempotent: regenerate the advisory rather than appending a fresh one each cycle.
292
+
293
+ ##### 3f.5 Rollup cites the rule
294
+
295
+ This phase implements exactly one PRD-lifecycle hop — `$TICKETED → $SHIPPED` — and the optional config-gated archive that follows it. All terminal-state semantics, the generated-top-level-work boundary, and the dedupe-by-child-ref idempotency come from the `prd-lifecycle-rollup` rule; this skill is its Linear implementation, not a second source of truth.
296
+
226
297
  ### Phase 4 — Summary report
227
298
 
228
299
  After processing every ready PRD, emit a summary:
@@ -269,10 +340,11 @@ Idempotency: the helper finds-or-creates. Re-runs of the cycle reuse the same se
269
340
  ## Idempotency & safety
270
341
 
271
342
  - **Single-cycle scope**: this skill processes the ready set as it exists at the start of Phase 2. New `$READY` projects added mid-cycle are picked up next run.
272
- - **No writes outside the lifecycle**: this skill only ever writes to the destination tracker via `lisa:linear-to-tracker` (which delegates to `lisa:tracker-write`), only ever changes Linear project labels among `$IN_REVIEW`, `$BLOCKED`, `$TICKETED`, only ever creates/comments on the sentinel feedback issue (never any other Linear issue). It never edits project descriptions, never edits Linear documents, never touches the `draft` or `shipped` labels, never archives or deletes projects.
343
+ - **No writes outside the lifecycle**: this skill only ever writes to the destination tracker via `lisa:linear-to-tracker` (which delegates to `lisa:tracker-write`), only ever changes Linear project labels among `$IN_REVIEW`, `$BLOCKED`, `$TICKETED`, and `$SHIPPED` (the last via the rollup phase 3f only), only ever creates/comments on the sentinel feedback issue (never any other Linear issue). It never edits project descriptions, never edits Linear documents, never touches the `draft` label, and never deletes projects. It sets the `$SHIPPED` label and may archive the PRD project **only** through the config-gated rollup phase (3f).
273
344
  - **Claim-first ordering**: the label flip to `$IN_REVIEW` happens BEFORE validation runs, so a re-entrant call won't double-process.
274
345
  - **Failure isolation**: an exception processing one project must not stop the cycle. Catch, record under "Errors" in the summary, continue to the next project. The project that errored is left labelled `$IN_REVIEW` — the human investigates from there.
275
346
  - **Single-label invariant**: after every transition, verify exactly one lifecycle label is present on the project. If two are present (rare race), surface as an Error and skip — do NOT auto-resolve, the human decides.
347
+ - **Rollup idempotency**: rollup (Phase 3f) is a no-op on a PRD project already carrying `$SHIPPED` (and already archived when `closeOnShipped` is `true`) — no duplicate transition, no duplicate archive, no duplicate comment. The all-terminal condition is a pure function of the children's current states (deduped by child-ref identity), so recomputing it is safe to re-run. Archival NEVER precedes the all-terminal condition.
276
348
 
277
349
  ## Configuration
278
350
 
@@ -289,13 +361,15 @@ Destination tracker config (jira / github / linear) is consumed by `lisa:tracker
289
361
  | `.lisa.config.json` `linear.labels.prd.in_review` | `prd-in-review` | Project label set on claim |
290
362
  | `.lisa.config.json` `linear.labels.prd.blocked` | `prd-blocked` | Project label set on validation failure |
291
363
  | `.lisa.config.json` `linear.labels.prd.ticketed` | `prd-ticketed` | Project label set on success |
292
- | `.lisa.config.json` `linear.labels.prd.shipped` | `prd-shipped` | Project label product sets after delivery |
364
+ | `.lisa.config.json` `linear.labels.prd.shipped` | `prd-shipped` | Project label set by the rollup phase (3f) when all generated top-level work is terminal; product may also set it by hand |
365
+ | `.lisa.config.json` `linear.labels.prd.rollup.closeOnShipped` | `false` | When `true`, rollup archives the PRD project after the `$SHIPPED` transition; when `false`, sets `$SHIPPED` and leaves it active |
293
366
  | `.lisa.config.json` `linear.labels.prd.sentinel` | `prd-intake-feedback` | Issue-level label marking the sentinel feedback issue |
294
367
 
295
368
  ## Rules
296
369
 
297
370
  - Never write to the destination tracker outside of `lisa:linear-to-tracker` → `lisa:tracker-write`. The validator's verdict gates progress; bypassing it produces broken tickets.
298
- - Never add or remove a label this skill doesn't own (`$IN_REVIEW`, `$BLOCKED`, `$TICKETED`). Product owns the `draft`, `ready`, and `shipped` labels. The issue-level `$SENTINEL` label is owned by this skill but is not a lifecycle label.
371
+ - Never add or remove a label this skill doesn't own (`$IN_REVIEW`, `$BLOCKED`, `$TICKETED`, and `$SHIPPED` via the rollup phase only). Product owns the `draft` and `ready` labels; product and the rollup phase (3f) both set `shipped`. The issue-level `$SENTINEL` label is owned by this skill but is not a lifecycle label.
372
+ - Set `$SHIPPED` (and archive the PRD project when `closeOnShipped` is configured) only from the rollup phase, and only when all generated top-level children are terminal per the `prd-lifecycle-rollup` rule. Never ship or archive on partial completion.
299
373
  - Never edit a project's description or any attached Linear document. Communication with product happens only through comments on sub-issues or on the sentinel feedback issue.
300
374
  - Never post a single dump of all gate failures on one comment. One comment per `prd_anchor` group on the relevant sub-issue (or one comment on the sentinel feedback issue for unanchored failures only). Comments must be sub-issue-anchored where possible, categorized, plain-language, and contain a concrete recommendation.
301
375
  - Never include a gate ID, internal skill name, or engineering shorthand in a comment body.
@@ -37,10 +37,23 @@ BLOCKED=$(read_role blocked "Blocked")
37
37
  TICKETED=$(read_role ticketed "Ticketed")
38
38
  SHIPPED=$(read_role shipped "Shipped")
39
39
  STATUS_PROP=$(jq -r '.notion.statusProperty // "Status"' .lisa.config.json 2>/dev/null)
40
+
41
+ # Resolve a boolean rollup flag. Local overrides global per-key; default when unset.
42
+ read_rollup_flag() {
43
+ local key="$1" default="$2"
44
+ local local_v global_v
45
+ local_v=$(jq -r ".notion.rollup.${key} // empty" .lisa.config.local.json 2>/dev/null)
46
+ global_v=$(jq -r ".notion.rollup.${key} // empty" .lisa.config.json 2>/dev/null)
47
+ echo "${local_v:-${global_v:-$default}}"
48
+ }
49
+
50
+ CLOSE_ON_SHIPPED=$(read_rollup_flag closeOnShipped false)
40
51
  ```
41
52
 
42
53
  In prose below, the role names refer to the resolved values: e.g. "the `ready` status" means whatever `notion.values.ready` resolves to (default: `Ready`).
43
54
 
55
+ This skill shares its PRD closure rollup phase (3f) with `lisa:github-prd-intake`, `lisa:linear-prd-intake`, and `lisa:confluence-prd-intake`. The phases, gates, comment templates, and rollup behavior are identical across all four intake skills — only the vendor surface differs. Keep all four behaviorally aligned: when changing intake logic — including the rollup phase — change them together. The **PRD closure rollup phase (3f)** transitions a `$TICKETED` PRD to `$SHIPPED` (and optionally archives it) once all its generated top-level work is terminal, per the `prd-lifecycle-rollup` rule; this is the Notion leg of the same vendor-neutral rollup (LPC-1.3 #584), using the documented generated-work section since Notion has no native ticket hierarchy.
56
+
44
57
  ## Confirmation policy
45
58
 
46
59
  Do NOT ask the caller whether to proceed. Once invoked with a database URL, run the cycle to completion — claim, validate, branch to `blocked` or `ticketed`, write the summary. The caller (a human or a cron) has already authorized the run by invoking the skill; re-prompting defeats the purpose of a background batch.
@@ -67,7 +80,7 @@ draft → ready → in_review → blocked | ticketed → shipped
67
80
  (product) (us) (us) (product)
68
81
  ```
69
82
 
70
- This skill ONLY transitions `ready → in_review`, then `in_review → blocked` or `in_review → ticketed`. Never touches `draft` or `shipped`.
83
+ This skill transitions `ready → in_review`, then `in_review → blocked` or `in_review → ticketed`, then (via the rollup phase 3f) `ticketed → shipped`. It never touches `draft` that status is owned by product. The `shipped` status is set by this skill's **rollup phase (3f)** when, and only when, the PRD's generated top-level work is all terminal — per the `prd-lifecycle-rollup` rule; product may also set it by hand. Rollup never advances a PRD to `shipped` on partial completion, and never archives a PRD page unless `notion.rollup.closeOnShipped` is configured `true` (default `false` → set `$SHIPPED`, leave the page active).
71
84
 
72
85
  ## Phases
73
86
 
@@ -226,6 +239,61 @@ The access skill resolves a `prd_anchor` substring to the matching block ID by p
226
239
 
227
240
  Move to the next ready PRD. One PRD failing does not affect others.
228
241
 
242
+ #### 3f. PRD closure rollup (config-gated)
243
+
244
+ A PRD's lifecycle terminal state (`shipped`) is **derived** from whether the work it generated is done — it is never set by hand here on its own authority. This phase implements the Notion leg of that derivation, per the `prd-lifecycle-rollup` rule (cite it by slug; do not restate its taxonomy or terminal-state semantics here). It is behaviorally identical to `lisa:github-prd-intake`'s Phase 3f — only the vendor surface (a Notion status property via `lisa:notion-access` + the documented generated-work section) differs from GitHub's (issue close + labels via `gh`).
245
+
246
+ Rollup runs over PRD pages that are already `$TICKETED` (the only state from which a PRD can ship): the freshly-ticketed PRD from Phase 3c, and — because rollup also catches PRDs whose children finished in a *later* cycle — every page currently in `$STATUS_PROP = $TICKETED` (re-query the database with operation `query-database` filtered on the ticketed status). Process each independently; one PRD never blocks another's rollup.
247
+
248
+ ##### 3f.0 Resolve closure config
249
+
250
+ Closure is gated on `notion.rollup.closeOnShipped` (default `false`), resolved via `read_rollup_flag` (defined in the Workflow resolution block, same local-overrides-global precedence the status values use):
251
+
252
+ ```bash
253
+ CLOSE_ON_SHIPPED=$(read_rollup_flag closeOnShipped false)
254
+ ```
255
+
256
+ When `false` (the default), rollup sets `$STATUS_PROP = $SHIPPED` but leaves the page **active** for a human to archive. When `true`, rollup also archives the page (where Notion supports archival) after the `shipped` transition. Closure NEVER happens before all generated top-level work is terminal (`prd-lifecycle-rollup` rule; PRD #525 non-goal).
257
+
258
+ ##### 3f.1 Idempotency guard (no-op if already shipped)
259
+
260
+ Rollup is keyed by the PRD's current state. If the PRD already has `$STATUS_PROP = $SHIPPED` (and is already archived, when `$CLOSE_ON_SHIPPED` is `true`), it is a **no-op** — do not re-transition, do not re-archive, do not re-comment. Record it as `already shipped (no-op)` in the cycle summary and move on. This is what makes re-running intake safe.
261
+
262
+ ##### 3f.2 Read the generated top-level child set
263
+
264
+ Read the PRD's **generated top-level work** — its created Epics and any top-level Stories created directly under it, **excluding** leaf Sub-tasks and any Story nested under a generated Epic (`prd-lifecycle-rollup` rule, generated-top-level-work contract). Notion has **no native ticket hierarchy**, so the child set comes from the documented section only:
265
+
266
+ 1. **Documented `## Tickets` section (primary and only source).** Parse the machine-readable generated-work section `lisa:prd-backlink` writes to the PRD body (`## Tickets`, alias `## Generated Work`; see #582) by invoking `lisa:notion-access` operation `read-page` on the PRD. Top-level children are the `### <Epic key>: <title>` group headers' first line (`- [<ref>](<url>) — Epic`) plus any top-level Story listed directly under `### Unparented items`. Lines nested deeper (` - ... — Story:` under an Epic, ` - ... — Sub-task:`) are descendants, NOT top-level children — skip them.
267
+
268
+ Dedupe the resulting child set by **child-ref identity** — the destination ticket ref recorded in each generated-work entry (the entry is keyed by that ref, not by list position) — per the `prd-lifecycle-rollup` idempotency dedupe key. If the section yields no child (the PRD generated nothing, or the relationship was never recorded), record `no generated top-level children — rollup skipped` and leave the PRD as `$TICKETED`; do not ship an empty PRD.
269
+
270
+ ##### 3f.3 Apply the terminal-state predicate
271
+
272
+ For each top-level child, classify per the `prd-lifecycle-rollup` Confluence/Notion predicate:
273
+
274
+ - **Terminal (shipped).** The documented generated-work entry for the child is marked **done** in the PRD's machine-readable section (the durable equivalent of a closed ticket, since Notion has no native ticket state). A child Epic is terminal only when it has itself rolled up to its own terminal state per `leaf-only-lifecycle` — read the child's own recorded state; do not re-derive it from its leaves here.
275
+ - **Terminal-but-dropped.** The entry is marked won't-do / canceled. Like a not-planned leaf, it does not hold the PRD open and is excluded from the shipped set.
276
+ - **Incomplete / blocked.** Anything else: the entry is not yet marked done. Holds the PRD open.
277
+
278
+ The set of **required** children for the all-terminal check is the top-level children minus the terminal-but-dropped ones.
279
+
280
+ ##### 3f.4 Branch on the rollup verdict
281
+
282
+ **All required children terminal** (every required top-level child is terminal; at least one required child exists):
283
+
284
+ 1. Set `$STATUS_PROP = $SHIPPED` by invoking `lisa:notion-access` operation `write-page` with payload `{ "id": "<PRD-page-id>", "properties": { "<STATUS_PROP>": { "status": { "name": "<SHIPPED>" } } } }` (use `"select"` instead of `"status"` if the property is a select).
285
+ 2. **If `$CLOSE_ON_SHIPPED` is `true`**, archive the PRD page via `lisa:notion-access` (set `archived: true` where supported). When `false`, leave it active.
286
+ 3. Post a short rollup Notion comment naming the terminal child set and (when dropped children exist) the dropped set, so the audit trail records *why* the PRD shipped. Lead with `"Shipped by Claude — all generated top-level work is complete."`
287
+
288
+ **Any required child incomplete / blocked**:
289
+
290
+ 1. Leave `$STATUS_PROP = $TICKETED` and leave the page **active**. Do NOT set `$SHIPPED`. Do NOT archive.
291
+ 2. Report the incomplete child set — both in the cycle summary and, when at least one cycle has previously ticketed this PRD, as a single advisory Notion comment listing the still-open children (`- <ref> "<title>" — <state>`), so product can see what's blocking the rollup. Keep it idempotent: regenerate the advisory rather than appending a fresh one each cycle.
292
+
293
+ ##### 3f.5 Rollup cites the rule
294
+
295
+ This phase implements exactly one PRD-lifecycle hop — `$TICKETED → $SHIPPED` — and the optional config-gated archive that follows it. All terminal-state semantics, the generated-top-level-work boundary, and the dedupe-by-child-ref idempotency come from the `prd-lifecycle-rollup` rule; this skill is its Notion implementation, not a second source of truth.
296
+
229
297
  ### Phase 4 — Summary report
230
298
 
231
299
  After processing every ready PRD, emit a summary:
@@ -254,9 +322,10 @@ Print to the agent's output. Do not write this summary to Notion or the destinat
254
322
  ## Idempotency & safety
255
323
 
256
324
  - **Single-cycle scope**: this skill processes the ready set as it exists at the start of Phase 2. New ready PRDs added mid-cycle are picked up next run.
257
- - **No writes outside the lifecycle**: this skill only ever writes to the destination tracker via `lisa:notion-to-tracker` (which delegates to `lisa:tracker-write`), and only ever changes the Notion status property to `$IN_REVIEW`, `$BLOCKED`, or `$TICKETED`. It never edits PRD content, never touches `$DRAFT` or `$SHIPPED`, never deletes pages.
325
+ - **No writes outside the lifecycle**: this skill only ever writes to the destination tracker via `lisa:notion-to-tracker` (which delegates to `lisa:tracker-write`), and only ever changes the Notion status property to `$IN_REVIEW`, `$BLOCKED`, `$TICKETED`, or `$SHIPPED` (the last via the rollup phase 3f only). It never edits PRD content, never touches `$DRAFT`, never deletes pages. It sets `$SHIPPED` and may archive the PRD page **only** through the config-gated rollup phase (3f).
258
326
  - **Claim-first ordering**: the status flip to `$IN_REVIEW` is set BEFORE validation runs, so a re-entrant call won't double-process.
259
327
  - **Failure isolation**: an exception processing one PRD must not stop the cycle. Catch, record under "Errors" in the summary, continue to the next PRD. The PRD that errored is left in `$IN_REVIEW` — the human investigates from there.
328
+ - **Rollup idempotency**: rollup (Phase 3f) is a no-op on a PRD already in `$STATUS_PROP = $SHIPPED` (and already archived when `closeOnShipped` is `true`) — no duplicate transition, no duplicate archive, no duplicate comment. The all-terminal condition is a pure function of the children's current states (deduped by child-ref identity), so recomputing it is safe to re-run. Archival NEVER precedes the all-terminal condition.
260
329
 
261
330
  ## Configuration
262
331
 
@@ -273,7 +342,8 @@ This skill reads project configuration from `.lisa.config.json` (with `.lisa.con
273
342
  | `notion.values.in_review` | `In Review` | Value the agent sets on claim |
274
343
  | `notion.values.blocked` | `Blocked` | Value the agent sets on validation failure |
275
344
  | `notion.values.ticketed` | `Ticketed` | Value the agent sets on success |
276
- | `notion.values.shipped` | `Shipped` | Value product sets after delivery (agent never writes) |
345
+ | `notion.values.shipped` | `Shipped` | Value the rollup phase (3f) sets when all generated top-level work is terminal; product may also set it by hand |
346
+ | `notion.rollup.closeOnShipped` | `false` | When `true`, rollup archives the PRD page after the `$SHIPPED` transition; when `false`, sets `$SHIPPED` and leaves the page active |
277
347
 
278
348
  ### From environment variables
279
349
 
@@ -286,7 +356,8 @@ This skill reads project configuration from `.lisa.config.json` (with `.lisa.con
286
356
  ## Rules
287
357
 
288
358
  - Never write to the destination tracker outside of `lisa:notion-to-tracker` → `lisa:tracker-write`. The validator's verdict gates progress; bypassing it produces broken tickets.
289
- - Never set the Notion status to a value this skill doesn't own (`$IN_REVIEW`, `$BLOCKED`, `$TICKETED`). Product owns `$DRAFT`, `$READY`, `$SHIPPED`.
359
+ - Never set the Notion status to a value this skill doesn't own (`$IN_REVIEW`, `$BLOCKED`, `$TICKETED`, and `$SHIPPED` via the rollup phase only). Product owns `$DRAFT` and `$READY`; product and the rollup phase (3f) both set `$SHIPPED`.
360
+ - Set `$SHIPPED` (and archive the PRD page when `closeOnShipped` is configured) only from the rollup phase, and only when all generated top-level children are terminal per the `prd-lifecycle-rollup` rule. Never ship or archive on partial completion.
290
361
  - Never edit the PRD's body. Communication with product happens only through Notion comments.
291
362
  - Never post a single page-level dump of all gate failures. One comment per `prd_anchor` group (or one page-level summary for unanchored failures only). The audience is product, not engineers — comments must be block-anchored, categorized, plain-language, and contain a concrete recommendation. See Phase 3c.3 for the required template and Phase 3c.5 for forbidden language.
292
363
  - Never include a gate ID, internal skill name, or engineering shorthand in a Notion comment body. If the validator's `what` or `recommendation` field uses one, paraphrase before posting.