@codyswann/lisa 2.44.0 → 2.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -82,7 +82,7 @@
82
82
  "lodash": ">=4.18.1"
83
83
  },
84
84
  "name": "@codyswann/lisa",
85
- "version": "2.44.0",
85
+ "version": "2.45.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.44.0",
3
+ "version": "2.45.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.44.0",
3
+ "version": "2.45.0",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -39,6 +39,8 @@ 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.
43
+
42
44
  ## Confirmation policy
43
45
 
44
46
  Do NOT ask the caller whether to proceed. Once invoked with a repo, run the cycle to completion — claim, validate, branch to `$BLOCKED` or `$TICKETED`, write the summary. The caller has already authorized the run by invoking the skill; re-prompting defeats the purpose of a background batch.
@@ -69,14 +71,15 @@ draft → ready → in_review → blocked | ticketed → shipped
69
71
 
70
72
  Exactly one of these labels is expected on a PRD issue at any time.
71
73
 
72
- This skill ONLY transitions:
74
+ This skill transitions:
73
75
 
74
76
  - `$READY` → `$IN_REVIEW` (claim)
75
77
  - `$IN_REVIEW` → `$BLOCKED` (gate failures or coverage gaps)
76
78
  - `$IN_REVIEW` → `$TICKETED` (success)
77
79
  - `$TICKETED` → `$BLOCKED` (post-write coverage gaps from Phase 3e)
80
+ - `$TICKETED` → `$SHIPPED` (PRD closure rollup, Phase 3f — only when **all** generated top-level children are terminal)
78
81
 
79
- It never adds, removes, or touches the `draft` or `shipped` labels. Those labels are owned by product.
82
+ The `draft` label is owned by product and is never touched here. The `shipped` label 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 closes a PRD issue unless `github.labels.prd.rollup.closeOnShipped` is configured `true` (default `false` → set `shipped`, leave open).
80
83
 
81
84
  A "transition" means: remove the old lifecycle label and add the new one (`gh issue edit <num> --remove-label <old> --add-label <new>`). The skill MUST verify exactly one lifecycle label is present after the update.
82
85
 
@@ -240,6 +243,99 @@ Per-ticket gates prove each ticket is well-formed; they do NOT prove the *set* o
240
243
 
241
244
  3. The created tickets remain in the destination tracker regardless of the verdict. The audit only tells us whether *more* are needed.
242
245
 
246
+ #### 3f. PRD closure rollup (config-gated)
247
+
248
+ 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 GitHub leg of that derivation, per the `prd-lifecycle-rollup` rule (cite it by slug; do not restate its taxonomy or terminal-state semantics here). Linear / Confluence / Notion rollup is a sibling sub-task (#584) and is out of scope for this skill.
249
+
250
+ Rollup runs over PRD issues 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 issue currently carrying `$TICKETED`. Process each independently; one PRD never blocks another's rollup.
251
+
252
+ ##### 3f.0 Resolve closure config
253
+
254
+ Closure is gated on `github.labels.prd.rollup.closeOnShipped` (default `false`). Resolve it with the same local-overrides-global precedence the lifecycle labels use:
255
+
256
+ ```bash
257
+ # Resolve a boolean rollup flag. Local overrides global per-key; default when unset.
258
+ read_rollup_flag() {
259
+ local key="$1" default="$2"
260
+ local local_v global_v
261
+ local_v=$(jq -r ".github.labels.prd.rollup.${key} // empty" .lisa.config.local.json 2>/dev/null)
262
+ global_v=$(jq -r ".github.labels.prd.rollup.${key} // empty" .lisa.config.json 2>/dev/null)
263
+ echo "${local_v:-${global_v:-$default}}"
264
+ }
265
+
266
+ CLOSE_ON_SHIPPED=$(read_rollup_flag closeOnShipped false)
267
+ ```
268
+
269
+ When `false` (the default), rollup sets `$SHIPPED` but leaves the PRD issue **open** for a human to close. When `true`, rollup also closes the PRD issue after the `$SHIPPED` transition. Closure NEVER happens before all generated top-level work is terminal (`prd-lifecycle-rollup` rule; PRD #525 non-goal).
270
+
271
+ ##### 3f.1 Idempotency guard (no-op if already shipped)
272
+
273
+ Rollup is keyed by the PRD's current state. If the PRD already carries `$SHIPPED` (and is already closed, when `$CLOSE_ON_SHIPPED` is `true`), it is a **no-op** — do not re-transition, do not re-close, 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.
274
+
275
+ ##### 3f.2 Read the generated top-level child set
276
+
277
+ 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:
278
+
279
+ 1. **Native sub-issues (primary).** Traverse the PRD issue's native sub-issue graph via the GraphQL `subIssues` query (the same query `lisa:github-read-issue` Phase 3 uses). The PRD's direct `subIssues` nodes are its top-level children:
280
+
281
+ ```bash
282
+ gh api graphql -f query='
283
+ query($org:String!,$repo:String!,$number:Int!){
284
+ repository(owner:$org,name:$repo){
285
+ issue(number:$number){
286
+ subIssues(first: 100) {
287
+ nodes {
288
+ number title state url
289
+ repository { nameWithOwner }
290
+ labels(first: 50) { nodes { name } }
291
+ }
292
+ }
293
+ }
294
+ }
295
+ }' -F org=<org> -F repo=<repo> -F number=<prd-num>
296
+ ```
297
+
298
+ 2. **Documented `## Tickets` section (fallback).** When native sub-issues are unavailable (older GHES, sub-issues feature off, or the source PRD and the destination tracker are different systems so the children were never linked as sub-issues), parse the machine-readable generated-work section `lisa:prd-backlink` writes to the PRD body (`## 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.
299
+
300
+ ```bash
301
+ # Top-level child refs = Epic lines (top indent) + Unparented top-level Stories.
302
+ # Sub-tasks and Stories nested under an Epic are descendants — excluded.
303
+ gh issue view <prd-num> --repo <org>/<repo> --json body --jq '.body' \
304
+ | awk '/^## (Tickets|Generated Work)/{insec=1;next} /^## /{insec=0}
305
+ insec && /^- \[.*\] — Epic/{print}
306
+ insec && /^### Unparented items/{unp=1;next}
307
+ insec && unp && /^- \[.*\] — Story/{print}'
308
+ ```
309
+
310
+ Dedupe the resulting child set by **child-ref identity** (`owner/repo#number`) so a child that appears both as a native sub-issue 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.
311
+
312
+ ##### 3f.3 Apply the terminal-state predicate
313
+
314
+ For each top-level child, fetch its state + labels (already present from the GraphQL nodes, or `gh issue view <child-num> --json state,labels`) and classify per the `prd-lifecycle-rollup` GitHub predicate:
315
+
316
+ - **Terminal (shipped).** The child issue is **CLOSED** *and* (where the build-status label is in use) carries the resolved build `done` role label (`status:done` by default). 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.
317
+ - **Terminal-but-dropped.** The child is closed **as not planned** (`stateReason == "not_planned"`). It does not hold the PRD open and is excluded from the shipped set — treated like a won't-do leaf.
318
+ - **Incomplete / blocked.** Anything else: still open, or closed without the `done` label. Holds the PRD open.
319
+
320
+ The set of **required** children for the all-terminal check is the top-level children minus the terminal-but-dropped ones.
321
+
322
+ ##### 3f.4 Branch on the rollup verdict
323
+
324
+ **All required children terminal** (every required top-level child is terminal; at least one required child exists):
325
+
326
+ 1. Transition labels: `gh issue edit <prd-num> --repo <org>/<repo> --remove-label "$TICKETED" --add-label "$SHIPPED"`. Verify exactly one lifecycle label remains (the single-label invariant).
327
+ 2. **If `$CLOSE_ON_SHIPPED` is `true`**, close the PRD issue: `gh issue close <prd-num> --repo <org>/<repo> --reason completed`. When `false`, leave it open.
328
+ 3. Post a short rollup 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."`
329
+
330
+ **Any required child incomplete / blocked**:
331
+
332
+ 1. Leave the PRD label as `$TICKETED` and leave the issue **open**. Do NOT add `$SHIPPED`. Do NOT close.
333
+ 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 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.
334
+
335
+ ##### 3f.5 Rollup is GitHub-only and cites the rule
336
+
337
+ This phase only touches GitHub PRD issues. It implements exactly one PRD-lifecycle hop — `$TICKETED → $SHIPPED` — and the optional config-gated close that follows it. All terminal-state semantics, the generated-top-level-work boundary, the env-keyed `done` resolution, and the dedupe-by-child-ref idempotency come from the `prd-lifecycle-rollup` rule; this skill is its GitHub implementation, not a second source of truth.
338
+
243
339
  ### Phase 4 — Summary report
244
340
 
245
341
  ```text
@@ -257,6 +353,14 @@ PRDs processed: <n>
257
353
  - Errors (claim failed, etc): <n>
258
354
  - <issue-ref> "<title>" — <reason>
259
355
 
356
+ Rollup (Phase 3f):
357
+ - $SHIPPED: <n>
358
+ - <issue-ref> "<title>" → all <child-count> top-level children terminal (<dropped-count> dropped); closed: <yes|no (closeOnShipped off)>
359
+ - Held open (incomplete children): <n>
360
+ - <issue-ref> "<title>" → <incomplete-count> of <child-count> top-level children still open
361
+ - Already shipped (no-op): <n>
362
+ - No generated children (rollup skipped): <n>
363
+
260
364
  Total tickets created: <n>
261
365
  Coverage audit summary: <n> COMPLETE / <n> COMPLETE_WITH_SCOPE_CREEP / <n> GAPS_FOUND
262
366
  ```
@@ -274,10 +378,11 @@ When the configured destination tracker is GitHub Issues AND the PRD repo is the
274
378
  ## Idempotency & safety
275
379
 
276
380
  - **Single-cycle scope**: this skill processes the ready set as it exists at the start of Phase 2. New ready issues added mid-cycle are picked up next run.
277
- - **No writes outside the lifecycle**: this skill only ever writes to the destination tracker via `lisa:github-to-tracker` (which delegates to `lisa:tracker-write`), only ever changes labels among `$IN_REVIEW`, `$BLOCKED`, `$TICKETED`, only ever comments on the source PRD issue. It never edits PRD bodies, never touches `draft` or `shipped` labels, never closes or deletes PRD issues.
381
+ - **No writes outside the lifecycle**: this skill only ever writes to the destination tracker via `lisa:github-to-tracker` (which delegates to `lisa:tracker-write`), only ever changes labels among `$IN_REVIEW`, `$BLOCKED`, `$TICKETED`, `$SHIPPED`, only ever comments on the source PRD issue. It never edits PRD bodies and never touches the `draft` label. It sets the `$SHIPPED` label and may close the PRD issue **only** through the config-gated rollup phase (3f), and never deletes any issue.
278
382
  - **Claim-first ordering**: the label flip to `$IN_REVIEW` happens BEFORE validation runs.
279
383
  - **Failure isolation**: an exception processing one PRD must not stop the cycle. Catch, record under "Errors" in the summary, continue. The PRD that errored is left labeled `$IN_REVIEW` — humans investigate from there.
280
384
  - **Single-label invariant**: after every transition, verify exactly one lifecycle label is present.
385
+ - **Rollup idempotency**: rollup (Phase 3f) is a no-op on a PRD already carrying `$SHIPPED` (and already closed when `closeOnShipped` is `true`) — no duplicate transition, no duplicate close, no duplicate comment. The all-terminal condition is a pure function of the children's current states, so recomputing it is safe to re-run. Closure NEVER precedes the all-terminal condition.
281
386
 
282
387
  ## Configuration
283
388
 
@@ -299,7 +404,8 @@ Destination tracker config (jira / github / linear) is consumed by `lisa:tracker
299
404
  ## Rules
300
405
 
301
406
  - Never write to the destination tracker outside of `lisa:github-to-tracker` → `lisa:tracker-write`.
302
- - Never add or remove a label this skill doesn't own (`$IN_REVIEW`, `$BLOCKED`, `$TICKETED`). Product owns the `draft`, `ready`, and `shipped` PRD labels.
407
+ - 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` PRD labels; product and the rollup phase (3f) both set `shipped`.
408
+ - Set `$SHIPPED` (and close the PRD 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 close on partial completion.
303
409
  - Never edit a PRD's body. Communication with product happens only via comments.
304
410
  - Never post a single dump of all gate failures on one comment. One comment per `prd_anchor` group, plus one rollup for unanchored failures.
305
411
  - Never include a gate ID, internal skill name, or engineering shorthand in a comment body.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.44.0",
3
+ "version": "2.45.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.44.0",
3
+ "version": "2.45.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.44.0",
3
+ "version": "2.45.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.44.0",
3
+ "version": "2.45.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.44.0",
3
+ "version": "2.45.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.44.0",
3
+ "version": "2.45.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.44.0",
3
+ "version": "2.45.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.44.0",
3
+ "version": "2.45.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.44.0",
3
+ "version": "2.45.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.44.0",
3
+ "version": "2.45.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.44.0",
3
+ "version": "2.45.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.44.0",
3
+ "version": "2.45.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.44.0",
3
+ "version": "2.45.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.44.0",
3
+ "version": "2.45.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.44.0",
3
+ "version": "2.45.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.44.0",
3
+ "version": "2.45.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"
@@ -39,6 +39,8 @@ 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.
43
+
42
44
  ## Confirmation policy
43
45
 
44
46
  Do NOT ask the caller whether to proceed. Once invoked with a repo, run the cycle to completion — claim, validate, branch to `$BLOCKED` or `$TICKETED`, write the summary. The caller has already authorized the run by invoking the skill; re-prompting defeats the purpose of a background batch.
@@ -69,14 +71,15 @@ draft → ready → in_review → blocked | ticketed → shipped
69
71
 
70
72
  Exactly one of these labels is expected on a PRD issue at any time.
71
73
 
72
- This skill ONLY transitions:
74
+ This skill transitions:
73
75
 
74
76
  - `$READY` → `$IN_REVIEW` (claim)
75
77
  - `$IN_REVIEW` → `$BLOCKED` (gate failures or coverage gaps)
76
78
  - `$IN_REVIEW` → `$TICKETED` (success)
77
79
  - `$TICKETED` → `$BLOCKED` (post-write coverage gaps from Phase 3e)
80
+ - `$TICKETED` → `$SHIPPED` (PRD closure rollup, Phase 3f — only when **all** generated top-level children are terminal)
78
81
 
79
- It never adds, removes, or touches the `draft` or `shipped` labels. Those labels are owned by product.
82
+ The `draft` label is owned by product and is never touched here. The `shipped` label 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 closes a PRD issue unless `github.labels.prd.rollup.closeOnShipped` is configured `true` (default `false` → set `shipped`, leave open).
80
83
 
81
84
  A "transition" means: remove the old lifecycle label and add the new one (`gh issue edit <num> --remove-label <old> --add-label <new>`). The skill MUST verify exactly one lifecycle label is present after the update.
82
85
 
@@ -240,6 +243,99 @@ Per-ticket gates prove each ticket is well-formed; they do NOT prove the *set* o
240
243
 
241
244
  3. The created tickets remain in the destination tracker regardless of the verdict. The audit only tells us whether *more* are needed.
242
245
 
246
+ #### 3f. PRD closure rollup (config-gated)
247
+
248
+ 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 GitHub leg of that derivation, per the `prd-lifecycle-rollup` rule (cite it by slug; do not restate its taxonomy or terminal-state semantics here). Linear / Confluence / Notion rollup is a sibling sub-task (#584) and is out of scope for this skill.
249
+
250
+ Rollup runs over PRD issues 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 issue currently carrying `$TICKETED`. Process each independently; one PRD never blocks another's rollup.
251
+
252
+ ##### 3f.0 Resolve closure config
253
+
254
+ Closure is gated on `github.labels.prd.rollup.closeOnShipped` (default `false`). Resolve it with the same local-overrides-global precedence the lifecycle labels use:
255
+
256
+ ```bash
257
+ # Resolve a boolean rollup flag. Local overrides global per-key; default when unset.
258
+ read_rollup_flag() {
259
+ local key="$1" default="$2"
260
+ local local_v global_v
261
+ local_v=$(jq -r ".github.labels.prd.rollup.${key} // empty" .lisa.config.local.json 2>/dev/null)
262
+ global_v=$(jq -r ".github.labels.prd.rollup.${key} // empty" .lisa.config.json 2>/dev/null)
263
+ echo "${local_v:-${global_v:-$default}}"
264
+ }
265
+
266
+ CLOSE_ON_SHIPPED=$(read_rollup_flag closeOnShipped false)
267
+ ```
268
+
269
+ When `false` (the default), rollup sets `$SHIPPED` but leaves the PRD issue **open** for a human to close. When `true`, rollup also closes the PRD issue after the `$SHIPPED` transition. Closure NEVER happens before all generated top-level work is terminal (`prd-lifecycle-rollup` rule; PRD #525 non-goal).
270
+
271
+ ##### 3f.1 Idempotency guard (no-op if already shipped)
272
+
273
+ Rollup is keyed by the PRD's current state. If the PRD already carries `$SHIPPED` (and is already closed, when `$CLOSE_ON_SHIPPED` is `true`), it is a **no-op** — do not re-transition, do not re-close, 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.
274
+
275
+ ##### 3f.2 Read the generated top-level child set
276
+
277
+ 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:
278
+
279
+ 1. **Native sub-issues (primary).** Traverse the PRD issue's native sub-issue graph via the GraphQL `subIssues` query (the same query `lisa:github-read-issue` Phase 3 uses). The PRD's direct `subIssues` nodes are its top-level children:
280
+
281
+ ```bash
282
+ gh api graphql -f query='
283
+ query($org:String!,$repo:String!,$number:Int!){
284
+ repository(owner:$org,name:$repo){
285
+ issue(number:$number){
286
+ subIssues(first: 100) {
287
+ nodes {
288
+ number title state url
289
+ repository { nameWithOwner }
290
+ labels(first: 50) { nodes { name } }
291
+ }
292
+ }
293
+ }
294
+ }
295
+ }' -F org=<org> -F repo=<repo> -F number=<prd-num>
296
+ ```
297
+
298
+ 2. **Documented `## Tickets` section (fallback).** When native sub-issues are unavailable (older GHES, sub-issues feature off, or the source PRD and the destination tracker are different systems so the children were never linked as sub-issues), parse the machine-readable generated-work section `lisa:prd-backlink` writes to the PRD body (`## 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.
299
+
300
+ ```bash
301
+ # Top-level child refs = Epic lines (top indent) + Unparented top-level Stories.
302
+ # Sub-tasks and Stories nested under an Epic are descendants — excluded.
303
+ gh issue view <prd-num> --repo <org>/<repo> --json body --jq '.body' \
304
+ | awk '/^## (Tickets|Generated Work)/{insec=1;next} /^## /{insec=0}
305
+ insec && /^- \[.*\] — Epic/{print}
306
+ insec && /^### Unparented items/{unp=1;next}
307
+ insec && unp && /^- \[.*\] — Story/{print}'
308
+ ```
309
+
310
+ Dedupe the resulting child set by **child-ref identity** (`owner/repo#number`) so a child that appears both as a native sub-issue 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.
311
+
312
+ ##### 3f.3 Apply the terminal-state predicate
313
+
314
+ For each top-level child, fetch its state + labels (already present from the GraphQL nodes, or `gh issue view <child-num> --json state,labels`) and classify per the `prd-lifecycle-rollup` GitHub predicate:
315
+
316
+ - **Terminal (shipped).** The child issue is **CLOSED** *and* (where the build-status label is in use) carries the resolved build `done` role label (`status:done` by default). 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.
317
+ - **Terminal-but-dropped.** The child is closed **as not planned** (`stateReason == "not_planned"`). It does not hold the PRD open and is excluded from the shipped set — treated like a won't-do leaf.
318
+ - **Incomplete / blocked.** Anything else: still open, or closed without the `done` label. Holds the PRD open.
319
+
320
+ The set of **required** children for the all-terminal check is the top-level children minus the terminal-but-dropped ones.
321
+
322
+ ##### 3f.4 Branch on the rollup verdict
323
+
324
+ **All required children terminal** (every required top-level child is terminal; at least one required child exists):
325
+
326
+ 1. Transition labels: `gh issue edit <prd-num> --repo <org>/<repo> --remove-label "$TICKETED" --add-label "$SHIPPED"`. Verify exactly one lifecycle label remains (the single-label invariant).
327
+ 2. **If `$CLOSE_ON_SHIPPED` is `true`**, close the PRD issue: `gh issue close <prd-num> --repo <org>/<repo> --reason completed`. When `false`, leave it open.
328
+ 3. Post a short rollup 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."`
329
+
330
+ **Any required child incomplete / blocked**:
331
+
332
+ 1. Leave the PRD label as `$TICKETED` and leave the issue **open**. Do NOT add `$SHIPPED`. Do NOT close.
333
+ 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 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.
334
+
335
+ ##### 3f.5 Rollup is GitHub-only and cites the rule
336
+
337
+ This phase only touches GitHub PRD issues. It implements exactly one PRD-lifecycle hop — `$TICKETED → $SHIPPED` — and the optional config-gated close that follows it. All terminal-state semantics, the generated-top-level-work boundary, the env-keyed `done` resolution, and the dedupe-by-child-ref idempotency come from the `prd-lifecycle-rollup` rule; this skill is its GitHub implementation, not a second source of truth.
338
+
243
339
  ### Phase 4 — Summary report
244
340
 
245
341
  ```text
@@ -257,6 +353,14 @@ PRDs processed: <n>
257
353
  - Errors (claim failed, etc): <n>
258
354
  - <issue-ref> "<title>" — <reason>
259
355
 
356
+ Rollup (Phase 3f):
357
+ - $SHIPPED: <n>
358
+ - <issue-ref> "<title>" → all <child-count> top-level children terminal (<dropped-count> dropped); closed: <yes|no (closeOnShipped off)>
359
+ - Held open (incomplete children): <n>
360
+ - <issue-ref> "<title>" → <incomplete-count> of <child-count> top-level children still open
361
+ - Already shipped (no-op): <n>
362
+ - No generated children (rollup skipped): <n>
363
+
260
364
  Total tickets created: <n>
261
365
  Coverage audit summary: <n> COMPLETE / <n> COMPLETE_WITH_SCOPE_CREEP / <n> GAPS_FOUND
262
366
  ```
@@ -274,10 +378,11 @@ When the configured destination tracker is GitHub Issues AND the PRD repo is the
274
378
  ## Idempotency & safety
275
379
 
276
380
  - **Single-cycle scope**: this skill processes the ready set as it exists at the start of Phase 2. New ready issues added mid-cycle are picked up next run.
277
- - **No writes outside the lifecycle**: this skill only ever writes to the destination tracker via `lisa:github-to-tracker` (which delegates to `lisa:tracker-write`), only ever changes labels among `$IN_REVIEW`, `$BLOCKED`, `$TICKETED`, only ever comments on the source PRD issue. It never edits PRD bodies, never touches `draft` or `shipped` labels, never closes or deletes PRD issues.
381
+ - **No writes outside the lifecycle**: this skill only ever writes to the destination tracker via `lisa:github-to-tracker` (which delegates to `lisa:tracker-write`), only ever changes labels among `$IN_REVIEW`, `$BLOCKED`, `$TICKETED`, `$SHIPPED`, only ever comments on the source PRD issue. It never edits PRD bodies and never touches the `draft` label. It sets the `$SHIPPED` label and may close the PRD issue **only** through the config-gated rollup phase (3f), and never deletes any issue.
278
382
  - **Claim-first ordering**: the label flip to `$IN_REVIEW` happens BEFORE validation runs.
279
383
  - **Failure isolation**: an exception processing one PRD must not stop the cycle. Catch, record under "Errors" in the summary, continue. The PRD that errored is left labeled `$IN_REVIEW` — humans investigate from there.
280
384
  - **Single-label invariant**: after every transition, verify exactly one lifecycle label is present.
385
+ - **Rollup idempotency**: rollup (Phase 3f) is a no-op on a PRD already carrying `$SHIPPED` (and already closed when `closeOnShipped` is `true`) — no duplicate transition, no duplicate close, no duplicate comment. The all-terminal condition is a pure function of the children's current states, so recomputing it is safe to re-run. Closure NEVER precedes the all-terminal condition.
281
386
 
282
387
  ## Configuration
283
388
 
@@ -299,7 +404,8 @@ Destination tracker config (jira / github / linear) is consumed by `lisa:tracker
299
404
  ## Rules
300
405
 
301
406
  - Never write to the destination tracker outside of `lisa:github-to-tracker` → `lisa:tracker-write`.
302
- - Never add or remove a label this skill doesn't own (`$IN_REVIEW`, `$BLOCKED`, `$TICKETED`). Product owns the `draft`, `ready`, and `shipped` PRD labels.
407
+ - 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` PRD labels; product and the rollup phase (3f) both set `shipped`.
408
+ - Set `$SHIPPED` (and close the PRD 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 close on partial completion.
303
409
  - Never edit a PRD's body. Communication with product happens only via comments.
304
410
  - Never post a single dump of all gate failures on one comment. One comment per `prd_anchor` group, plus one rollup for unanchored failures.
305
411
  - Never include a gate ID, internal skill name, or engineering shorthand in a comment body.