@codyswann/lisa 2.43.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.43.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.43.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.43.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: prd-backlink
3
- description: "Update a source PRD with a `## Tickets` section linking back to every work item created from it. Vendor-aware on the source side (Notion / Confluence / Linear / GitHub Issue / file) and tracker-agnostic on the ticket side. Idempotent — regenerates the section on each run rather than appending, so re-planning never accumulates stale links. Invoked by the *-to-tracker skills at the end of their pipeline and standalone if a PRD's Tickets section needs to be refreshed."
3
+ description: "Update a source PRD with an always-written, machine-readable `## Tickets` (alias `## Generated Work`) section linking back to every work item created from it. Each entry carries a parseable ref + URL + type + parent token so the generated child set is readable without scraping prose. Vendor-aware on the source side (Notion / Confluence / Linear / GitHub Issue / file) and tracker-agnostic on the ticket side; the documented section is written for every vendor, additive to native hierarchy linking. Idempotent — regenerates the section on each run rather than appending, so re-planning never accumulates stale links. Invoked by the *-to-tracker skills at the end of their pipeline and standalone if a PRD's Tickets section needs to be refreshed."
4
4
  allowed-tools: ["Skill", "Bash", "Read", "Edit", "Write", "Glob", "Grep"]
5
5
  ---
6
6
 
@@ -8,6 +8,8 @@ allowed-tools: ["Skill", "Bash", "Read", "Edit", "Write", "Glob", "Grep"]
8
8
 
9
9
  Write or update the `## Tickets` section of a source PRD so it links to every work item created from that PRD. The Debrief flow (and a human reading the PRD months later) uses this section as the canonical work-item set for the initiative.
10
10
 
11
+ This documented section is the **always-written, machine-readable record** of the generated child set. It is written for **every** `source_type` — including the vendors that also get a native hierarchy link (`github` / `linear` / `jira`) — so the generated top-level work is readable later from the PRD body alone, without parsing free-form comments or depending on a native relationship that may be unavailable on older hosts or across vendors. Native hierarchy linking (see the per-vendor sections below) is **additive** to this section, never a substitute for it. This is the documented-section leg of the `prd-lifecycle-rollup` rule (cited by slug; its taxonomy is not restated here).
12
+
11
13
  ## Input
12
14
 
13
15
  Pass `$ARGUMENTS` as a single JSON-style block:
@@ -31,8 +33,8 @@ Pass `$ARGUMENTS` as a single JSON-style block:
31
33
  - `linear` → Linear MCP project / issue read
32
34
  - `github` → `gh issue view`
33
35
  - `file` → `Read` tool on the absolute path
34
- 2. **Locate the existing section.** Search for `section_heading` (default `## Tickets`). If present, you will replace it. If not, you will append a new section just before any closing footer / sign-off / signature block, otherwise at the end.
35
- 3. **Render the section.** Use the format below. Group by Epic. Within an Epic, group by Story. Sub-tasks nest under their Story. Bugs and Spikes that are not under a Story go in a flat list at the bottom.
36
+ 2. **Locate the existing section.** Search for `section_heading` (default `## Tickets`). The canonical heading is `## Tickets`; `## Generated Work` is an accepted alias that the reader recognizes — match either heading when locating an existing section so a PRD authored with either name is found and regenerated in place (never duplicated under the other name). If present under either name, you will replace it (keeping whichever heading was already there, or the explicit `section_heading` override). If not present, you will append a new section just before any closing footer / sign-off / signature block, otherwise at the end.
37
+ 3. **Render the section.** Always render it — for every `source_type`, even when a native hierarchy link was also made (additive, not exclusive). Use the format below. Group by Epic. Within an Epic, group by Story. Sub-tasks nest under their Story. Bugs and Spikes that are not under a Story go in a flat list at the bottom. Each entry carries a machine-readable token (ref + URL + type + parent) so the generated child set is parseable without reading prose — see [Format](#format).
36
38
  4. **Write the updated PRD back** using the source's native write tool:
37
39
  - `notion` → `notion-update-page`
38
40
  - `confluence` → `updateConfluencePage`
@@ -49,7 +51,7 @@ Pass `$ARGUMENTS` as a single JSON-style block:
49
51
 
50
52
  ## Format
51
53
 
52
- The rendered section must be deterministic — same inputs produce identical output bytes. This is what makes idempotency reliable.
54
+ The rendered section must be deterministic — same inputs produce identical output bytes. This is what makes idempotency reliable. Every entry is simultaneously **human-readable** (a nested markdown link) and **machine-readable** (a trailing structured token), so the generated child set can be enumerated by parsing this section alone — the contract LPC-1.3 rollup (`github-prd-intake` / `*-prd-intake`) reads against, with no need to scrape free-form comments.
53
55
 
54
56
  ```markdown
55
57
  ## Tickets
@@ -58,28 +60,45 @@ _Generated by `lisa:prd-backlink`. Regenerated on every Plan run; do not edit by
58
60
 
59
61
  ### <Epic key>: <Epic title>
60
62
 
61
- - [<Epic key>](<url>) — Epic
62
- - [<Story key>](<url>) — Story: <title>
63
- - [<Sub-task key>](<url>) — Sub-task: <title>
64
- - [<Sub-task key>](<url>) — Sub-task: <title>
65
- - [<Story key>](<url>) — Story: <title>
63
+ - [<Epic key>](<url>) — Epic <!-- lisa:gw ref=<ref> url=<url> type=Epic parent= -->
64
+ - [<Story key>](<url>) — Story: <title> <!-- lisa:gw ref=<ref> url=<url> type=Story parent=<Epic ref> -->
65
+ - [<Sub-task key>](<url>) — Sub-task: <title> <!-- lisa:gw ref=<ref> url=<url> type=Sub-task parent=<Story ref> -->
66
+ - [<Sub-task key>](<url>) — Sub-task: <title> <!-- lisa:gw ref=<ref> url=<url> type=Sub-task parent=<Story ref> -->
67
+ - [<Story key>](<url>) — Story: <title> <!-- lisa:gw ref=<ref> url=<url> type=Story parent=<Epic ref> -->
66
68
 
67
69
  ### <Epic key>: <Epic title>
68
70
  ...
69
71
 
70
72
  ### Unparented items
71
73
 
72
- - [<Bug key>](<url>) — Bug: <title>
73
- - [<Spike key>](<url>) — Spike: <title>
74
+ - [<Bug key>](<url>) — Bug: <title> <!-- lisa:gw ref=<ref> url=<url> type=Bug parent= -->
75
+ - [<Spike key>](<url>) — Spike: <title> <!-- lisa:gw ref=<ref> url=<url> type=Spike parent= -->
76
+ ```
77
+
78
+ ### Machine-readable entry token
79
+
80
+ Every list entry ends with a single-line HTML comment — invisible in rendered markdown, so the section stays clean for humans, but a stable, greppable record for machines:
81
+
82
+ ```text
83
+ <!-- lisa:gw ref=<ref> url=<url> type=<type> parent=<parent-ref or empty> -->
74
84
  ```
75
85
 
76
- If the input contains zero items, write the section header with a single line: `_No tickets created Plan flow may not have completed._` Do not omit the section; presence-of-section is itself a signal to Debrief.
86
+ - **`lisa:gw`** a fixed sentinel (`gw` = generated work). A reader enumerates the generated child set by matching `<!-- lisa:gw ` lines; it never has to parse the surrounding prose, headings, or indentation.
87
+ - **`ref`** — the child-ref identity from the `prd-lifecycle-rollup` rule: `<org>/<repo>#<n>` for GitHub, the issue/project identifier (e.g. `TEAM-123`) for Linear, the issue key (e.g. `PROJ-123`) for JIRA. This is the same dedupe key native linking uses, so the documented record and the native record agree.
88
+ - **`url`** — the canonical URL of the work item.
89
+ - **`type`** — `Epic | Story | Task | Sub-task | Bug | Spike` (verbatim from the ticket's `type`).
90
+ - **`parent`** — the **`ref`** of this entry's parent (its `parent_key` resolved to the parent's ref), or **empty** when the entry is top-level (`parent_key` null/empty). A reader selects the **generated top-level child set** — exactly what the PRD owns per the rule — as every `lisa:gw` line whose `parent` is empty.
91
+
92
+ The visible markdown link and the token always carry the same `ref`/`url`/`type`; the token is the authoritative machine field (the prose may wrap or be reflowed by a host editor, the comment line will not). Field order within the token is fixed (`ref`, `url`, `type`, `parent`) so output is byte-stable.
93
+
94
+ If the input contains zero items, write the section header with a single line: `_No tickets created — Plan flow may not have completed._` Do not omit the section; presence-of-section is itself a signal to Debrief, and the always-written guarantee means a reader can always distinguish "ran, produced nothing" from "never ran."
77
95
 
78
96
  ## Idempotency
79
97
 
80
98
  Rendering rules:
81
- - Sort epics by key (lexical). Sort stories within an epic by key. Sort sub-tasks within a story by key. Sort the unparented list by `(type, key)`.
99
+ - Sort epics by key (lexical). Sort stories within an epic by key. Sort sub-tasks within a story by key. Sort the unparented list by `(type, key)`. The same sort applies to the `lisa:gw` tokens (each token sits on its entry's line), so the machine-readable order is identical across runs.
82
100
  - The line `_Generated by ..._` is fixed text — does not include a timestamp. A timestamp would defeat the diff-equality check Debrief relies on.
101
+ - Regenerate the whole section from the current ticket set on every run — **never append**. Dedupe is by **child-ref** (the token's `ref`, per the `prd-lifecycle-rollup` idempotency dedupe key): the same ticket set produces a byte-identical section with no duplicate entries, and re-running over an existing section is a no-op diff. A ticket present in a prior run but absent now simply does not reappear (stale links never accumulate).
83
102
 
84
103
  ## Native parent linking (GitHub)
85
104
 
@@ -242,4 +261,4 @@ Omit the `Native parent links` line when no native linking applies (Notion / Con
242
261
 
243
262
  ## Related rules
244
263
 
245
- - `prd-lifecycle-rollup` — the vendor-neutral source of truth for PRD→generated-top-level-work ownership, the per-vendor terminal predicate, PRD `shipped` rollup, and the child-ref idempotency dedupe key. This skill implements the GitHub, Linear, and JIRA native-linking legs of that rule; it cites the rule by slug rather than restating its taxonomy.
264
+ - `prd-lifecycle-rollup` — the vendor-neutral source of truth for PRD→generated-top-level-work ownership, the per-vendor terminal predicate, PRD `shipped` rollup, and the child-ref idempotency dedupe key. This skill implements both the **documented always-written, machine-readable generated-work section** leg (the universal fallback written for every vendor) and the GitHub, Linear, and JIRA native-linking legs of that rule; it cites the rule by slug rather than restating its taxonomy.
@@ -1,4 +1,4 @@
1
1
  display_name: "PRD Backlink"
2
- short_description: "Update a source PRD with a `## Tickets` section linking back to every work item created from it"
2
+ short_description: "Update a source PRD with an always-written, machine-readable `## Tickets` (alias `## Generated Work`) section linking back to every work"
3
3
  default_prompt:
4
- - "Use $prd-backlink: Update a source PRD with a `## Tickets` section linking back to every work item created from it."
4
+ - "Use $prd-backlink: Update a source PRD with an always-written, machine-readable `## Tickets` (alias `## Generated Work`) section linking back to every work…."
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.43.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.43.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.43.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.43.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.43.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.43.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.43.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.43.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.43.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.43.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.43.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.43.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.43.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.43.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.43.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.43.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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: prd-backlink
3
- description: "Update a source PRD with a `## Tickets` section linking back to every work item created from it. Vendor-aware on the source side (Notion / Confluence / Linear / GitHub Issue / file) and tracker-agnostic on the ticket side. Idempotent — regenerates the section on each run rather than appending, so re-planning never accumulates stale links. Invoked by the *-to-tracker skills at the end of their pipeline and standalone if a PRD's Tickets section needs to be refreshed."
3
+ description: "Update a source PRD with an always-written, machine-readable `## Tickets` (alias `## Generated Work`) section linking back to every work item created from it. Each entry carries a parseable ref + URL + type + parent token so the generated child set is readable without scraping prose. Vendor-aware on the source side (Notion / Confluence / Linear / GitHub Issue / file) and tracker-agnostic on the ticket side; the documented section is written for every vendor, additive to native hierarchy linking. Idempotent — regenerates the section on each run rather than appending, so re-planning never accumulates stale links. Invoked by the *-to-tracker skills at the end of their pipeline and standalone if a PRD's Tickets section needs to be refreshed."
4
4
  allowed-tools: ["Skill", "Bash", "Read", "Edit", "Write", "Glob", "Grep"]
5
5
  ---
6
6
 
@@ -8,6 +8,8 @@ allowed-tools: ["Skill", "Bash", "Read", "Edit", "Write", "Glob", "Grep"]
8
8
 
9
9
  Write or update the `## Tickets` section of a source PRD so it links to every work item created from that PRD. The Debrief flow (and a human reading the PRD months later) uses this section as the canonical work-item set for the initiative.
10
10
 
11
+ This documented section is the **always-written, machine-readable record** of the generated child set. It is written for **every** `source_type` — including the vendors that also get a native hierarchy link (`github` / `linear` / `jira`) — so the generated top-level work is readable later from the PRD body alone, without parsing free-form comments or depending on a native relationship that may be unavailable on older hosts or across vendors. Native hierarchy linking (see the per-vendor sections below) is **additive** to this section, never a substitute for it. This is the documented-section leg of the `prd-lifecycle-rollup` rule (cited by slug; its taxonomy is not restated here).
12
+
11
13
  ## Input
12
14
 
13
15
  Pass `$ARGUMENTS` as a single JSON-style block:
@@ -31,8 +33,8 @@ Pass `$ARGUMENTS` as a single JSON-style block:
31
33
  - `linear` → Linear MCP project / issue read
32
34
  - `github` → `gh issue view`
33
35
  - `file` → `Read` tool on the absolute path
34
- 2. **Locate the existing section.** Search for `section_heading` (default `## Tickets`). If present, you will replace it. If not, you will append a new section just before any closing footer / sign-off / signature block, otherwise at the end.
35
- 3. **Render the section.** Use the format below. Group by Epic. Within an Epic, group by Story. Sub-tasks nest under their Story. Bugs and Spikes that are not under a Story go in a flat list at the bottom.
36
+ 2. **Locate the existing section.** Search for `section_heading` (default `## Tickets`). The canonical heading is `## Tickets`; `## Generated Work` is an accepted alias that the reader recognizes — match either heading when locating an existing section so a PRD authored with either name is found and regenerated in place (never duplicated under the other name). If present under either name, you will replace it (keeping whichever heading was already there, or the explicit `section_heading` override). If not present, you will append a new section just before any closing footer / sign-off / signature block, otherwise at the end.
37
+ 3. **Render the section.** Always render it — for every `source_type`, even when a native hierarchy link was also made (additive, not exclusive). Use the format below. Group by Epic. Within an Epic, group by Story. Sub-tasks nest under their Story. Bugs and Spikes that are not under a Story go in a flat list at the bottom. Each entry carries a machine-readable token (ref + URL + type + parent) so the generated child set is parseable without reading prose — see [Format](#format).
36
38
  4. **Write the updated PRD back** using the source's native write tool:
37
39
  - `notion` → `notion-update-page`
38
40
  - `confluence` → `updateConfluencePage`
@@ -49,7 +51,7 @@ Pass `$ARGUMENTS` as a single JSON-style block:
49
51
 
50
52
  ## Format
51
53
 
52
- The rendered section must be deterministic — same inputs produce identical output bytes. This is what makes idempotency reliable.
54
+ The rendered section must be deterministic — same inputs produce identical output bytes. This is what makes idempotency reliable. Every entry is simultaneously **human-readable** (a nested markdown link) and **machine-readable** (a trailing structured token), so the generated child set can be enumerated by parsing this section alone — the contract LPC-1.3 rollup (`github-prd-intake` / `*-prd-intake`) reads against, with no need to scrape free-form comments.
53
55
 
54
56
  ```markdown
55
57
  ## Tickets
@@ -58,28 +60,45 @@ _Generated by `lisa:prd-backlink`. Regenerated on every Plan run; do not edit by
58
60
 
59
61
  ### <Epic key>: <Epic title>
60
62
 
61
- - [<Epic key>](<url>) — Epic
62
- - [<Story key>](<url>) — Story: <title>
63
- - [<Sub-task key>](<url>) — Sub-task: <title>
64
- - [<Sub-task key>](<url>) — Sub-task: <title>
65
- - [<Story key>](<url>) — Story: <title>
63
+ - [<Epic key>](<url>) — Epic <!-- lisa:gw ref=<ref> url=<url> type=Epic parent= -->
64
+ - [<Story key>](<url>) — Story: <title> <!-- lisa:gw ref=<ref> url=<url> type=Story parent=<Epic ref> -->
65
+ - [<Sub-task key>](<url>) — Sub-task: <title> <!-- lisa:gw ref=<ref> url=<url> type=Sub-task parent=<Story ref> -->
66
+ - [<Sub-task key>](<url>) — Sub-task: <title> <!-- lisa:gw ref=<ref> url=<url> type=Sub-task parent=<Story ref> -->
67
+ - [<Story key>](<url>) — Story: <title> <!-- lisa:gw ref=<ref> url=<url> type=Story parent=<Epic ref> -->
66
68
 
67
69
  ### <Epic key>: <Epic title>
68
70
  ...
69
71
 
70
72
  ### Unparented items
71
73
 
72
- - [<Bug key>](<url>) — Bug: <title>
73
- - [<Spike key>](<url>) — Spike: <title>
74
+ - [<Bug key>](<url>) — Bug: <title> <!-- lisa:gw ref=<ref> url=<url> type=Bug parent= -->
75
+ - [<Spike key>](<url>) — Spike: <title> <!-- lisa:gw ref=<ref> url=<url> type=Spike parent= -->
76
+ ```
77
+
78
+ ### Machine-readable entry token
79
+
80
+ Every list entry ends with a single-line HTML comment — invisible in rendered markdown, so the section stays clean for humans, but a stable, greppable record for machines:
81
+
82
+ ```text
83
+ <!-- lisa:gw ref=<ref> url=<url> type=<type> parent=<parent-ref or empty> -->
74
84
  ```
75
85
 
76
- If the input contains zero items, write the section header with a single line: `_No tickets created Plan flow may not have completed._` Do not omit the section; presence-of-section is itself a signal to Debrief.
86
+ - **`lisa:gw`** a fixed sentinel (`gw` = generated work). A reader enumerates the generated child set by matching `<!-- lisa:gw ` lines; it never has to parse the surrounding prose, headings, or indentation.
87
+ - **`ref`** — the child-ref identity from the `prd-lifecycle-rollup` rule: `<org>/<repo>#<n>` for GitHub, the issue/project identifier (e.g. `TEAM-123`) for Linear, the issue key (e.g. `PROJ-123`) for JIRA. This is the same dedupe key native linking uses, so the documented record and the native record agree.
88
+ - **`url`** — the canonical URL of the work item.
89
+ - **`type`** — `Epic | Story | Task | Sub-task | Bug | Spike` (verbatim from the ticket's `type`).
90
+ - **`parent`** — the **`ref`** of this entry's parent (its `parent_key` resolved to the parent's ref), or **empty** when the entry is top-level (`parent_key` null/empty). A reader selects the **generated top-level child set** — exactly what the PRD owns per the rule — as every `lisa:gw` line whose `parent` is empty.
91
+
92
+ The visible markdown link and the token always carry the same `ref`/`url`/`type`; the token is the authoritative machine field (the prose may wrap or be reflowed by a host editor, the comment line will not). Field order within the token is fixed (`ref`, `url`, `type`, `parent`) so output is byte-stable.
93
+
94
+ If the input contains zero items, write the section header with a single line: `_No tickets created — Plan flow may not have completed._` Do not omit the section; presence-of-section is itself a signal to Debrief, and the always-written guarantee means a reader can always distinguish "ran, produced nothing" from "never ran."
77
95
 
78
96
  ## Idempotency
79
97
 
80
98
  Rendering rules:
81
- - Sort epics by key (lexical). Sort stories within an epic by key. Sort sub-tasks within a story by key. Sort the unparented list by `(type, key)`.
99
+ - Sort epics by key (lexical). Sort stories within an epic by key. Sort sub-tasks within a story by key. Sort the unparented list by `(type, key)`. The same sort applies to the `lisa:gw` tokens (each token sits on its entry's line), so the machine-readable order is identical across runs.
82
100
  - The line `_Generated by ..._` is fixed text — does not include a timestamp. A timestamp would defeat the diff-equality check Debrief relies on.
101
+ - Regenerate the whole section from the current ticket set on every run — **never append**. Dedupe is by **child-ref** (the token's `ref`, per the `prd-lifecycle-rollup` idempotency dedupe key): the same ticket set produces a byte-identical section with no duplicate entries, and re-running over an existing section is a no-op diff. A ticket present in a prior run but absent now simply does not reappear (stale links never accumulate).
83
102
 
84
103
  ## Native parent linking (GitHub)
85
104
 
@@ -242,4 +261,4 @@ Omit the `Native parent links` line when no native linking applies (Notion / Con
242
261
 
243
262
  ## Related rules
244
263
 
245
- - `prd-lifecycle-rollup` — the vendor-neutral source of truth for PRD→generated-top-level-work ownership, the per-vendor terminal predicate, PRD `shipped` rollup, and the child-ref idempotency dedupe key. This skill implements the GitHub, Linear, and JIRA native-linking legs of that rule; it cites the rule by slug rather than restating its taxonomy.
264
+ - `prd-lifecycle-rollup` — the vendor-neutral source of truth for PRD→generated-top-level-work ownership, the per-vendor terminal predicate, PRD `shipped` rollup, and the child-ref idempotency dedupe key. This skill implements both the **documented always-written, machine-readable generated-work section** leg (the universal fallback written for every vendor) and the GitHub, Linear, and JIRA native-linking legs of that rule; it cites the rule by slug rather than restating its taxonomy.