@codyswann/lisa 4.26.3 → 4.26.5
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/all/copy-overwrite/scripts/lisa-work-item.mjs +248 -21
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +12 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +14 -6
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +3 -3
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-github-build-intake/SKILL.md +40 -6
- package/plugins/lisa/rules/eager/leaf-only-lifecycle.md +16 -0
- package/plugins/lisa/skills/lisa-github-build-intake/SKILL.md +40 -6
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-github-build-intake/SKILL.md +40 -6
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/rules/eager/leaf-only-lifecycle.md +16 -0
- package/plugins/lisa-copilot/skills/lisa-github-build-intake/SKILL.md +40 -6
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/rules/leaf-only-lifecycle.mdc +16 -0
- package/plugins/lisa-cursor/skills/lisa-github-build-intake/SKILL.md +40 -6
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/rules/eager/leaf-only-lifecycle.md +16 -0
- package/plugins/src/base/skills/lisa-github-build-intake/SKILL.md +40 -6
- package/rails/copy-contents/scripts/lisa-mutation.sh +8 -0
- package/rails/copy-overwrite/lefthook.yml +7 -2
- package/rails/copy-overwrite/scripts/lisa-scratch-run.sh +1030 -0
- package/scripts/two-channel-couplings.json +17 -4
package/package.json
CHANGED
|
@@ -161,7 +161,7 @@
|
|
|
161
161
|
"zod-validation-error": "^4.0.0"
|
|
162
162
|
},
|
|
163
163
|
"name": "@codyswann/lisa",
|
|
164
|
-
"version": "4.26.
|
|
164
|
+
"version": "4.26.5",
|
|
165
165
|
"description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
|
|
166
166
|
"main": "dist/index.js",
|
|
167
167
|
"exports": {
|
|
@@ -305,6 +305,6 @@
|
|
|
305
305
|
"test": "tests"
|
|
306
306
|
},
|
|
307
307
|
"types": "./dist/index.d.ts",
|
|
308
|
-
"lisaReleaseCommit": "
|
|
309
|
-
"gitHead": "
|
|
308
|
+
"lisaReleaseCommit": "8f93055a16a87378f7e2c179744e6ff6297dbc5a",
|
|
309
|
+
"gitHead": "8f93055a16a87378f7e2c179744e6ff6297dbc5a"
|
|
310
310
|
}
|
|
@@ -431,14 +431,26 @@ Run this only when the returned triage verdict is exactly `DUPLICATE_ALREADY_FIX
|
|
|
431
431
|
2. Post or preserve the triage-finding comment that explains why this issue is a duplicate and names the canonical issue.
|
|
432
432
|
3. Ensure a native `duplicates <canonical>` link exists when GitHub exposes issue relationships; if this installation cannot create that relationship, leave an explicit issue cross-reference comment/body link and record the limitation in the summary.
|
|
433
433
|
4. Resolve terminal `$DONE` exactly as in Phase 3d. For a single-env repo, `$DONE` is terminal; for env-keyed config, only the production/final value is terminal.
|
|
434
|
-
5.
|
|
434
|
+
5. Apply `$DONE` and retire every competing lifecycle role the issue currently carries — not just `$CLAIMED` — per the `leaf-only-lifecycle` rule's "Exactly one lifecycle role survives closure". Then close the issue as duplicate/not-planned:
|
|
435
435
|
|
|
436
436
|
```bash
|
|
437
|
-
gh issue
|
|
438
|
-
gh issue
|
|
437
|
+
gh issue view <number> --repo <org>/<repo> --json labels -q '[.labels[].name] | join(" ")'
|
|
438
|
+
gh issue edit <number> --repo <org>/<repo> --add-label "$DONE" <one --remove-label "<role>" per competing role the issue CURRENTLY carries>
|
|
439
439
|
gh issue close <number> --repo <org>/<repo> --reason "not planned"
|
|
440
440
|
```
|
|
441
441
|
|
|
442
|
+
6. **Confirm with an independent readback, and only then announce** — the same obligation as Phase 3d step 5, for the same reason. Re-read the issue instead of trusting what the edit and close reported:
|
|
443
|
+
|
|
444
|
+
```bash
|
|
445
|
+
gh issue view <number> --repo <org>/<repo> --json state,stateReason,labels
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
It must read `state: CLOSED` with `stateReason: NOT_PLANNED`, carry `$DONE`, and carry **no** other lifecycle role. Anything else is an Error naming what is still present. Post the closeout comment only after that passes:
|
|
449
|
+
|
|
450
|
+
```bash
|
|
451
|
+
gh issue comment <number> --repo <org>/<repo> --body "[claude-build-intake] Closed as duplicate of <canonical>. Canonical fix: <PR-or-commit>. Evidence: <base-branch-proof>."
|
|
452
|
+
```
|
|
453
|
+
|
|
442
454
|
If the canonical fix is merged but not yet present on the production branch, append the production-promotion caveat to the close comment: the production error can recur until the canonical issue promotes, and recurrence is tracked by the canonical issue rather than by reopening this duplicate.
|
|
443
455
|
|
|
444
456
|
This path is distinct from `BLOCKED`: ambiguity, open blockers, and duplicate-of-open findings remain held for human action and must not be auto-closed.
|
|
@@ -475,11 +487,15 @@ If the lifecycle run returned Success:
|
|
|
475
487
|
- If `github.labels.build.done` is an object, only the production/final environment value is terminal (default: `status:done`). Intermediate env values such as `status:on-dev` and `status:on-stg` are not terminal and must stay open.
|
|
476
488
|
- If the project uses a different final environment name, resolve it from the configured deployment topology; if ambiguous, record an Error and do not close.
|
|
477
489
|
|
|
490
|
+
4. **Retire every competing lifecycle role**, per the `leaf-only-lifecycle` rule's "Exactly one lifecycle role survives closure" — cite it, do not restate it. Read the issue's current labels, intersect them with the configured role set (`$READY`, `$CLAIMED`, `$BLOCKED`, `$REVIEW` where bound, and every env-keyed done value), and remove each one that is present except `$DONE`. Removing only `$CLAIMED` leaves a stale `$READY` on any item that reached completion without passing through the claimed lane, and a closed item still reading ready is re-dispatched by this very scanner on its next cycle.
|
|
491
|
+
|
|
478
492
|
```bash
|
|
479
|
-
gh issue
|
|
480
|
-
gh issue
|
|
493
|
+
gh issue view <number> --repo <org>/<repo> --json labels -q '[.labels[].name] | join(" ")'
|
|
494
|
+
gh issue edit <number> --repo <org>/<repo> --add-label "$DONE" <one --remove-label "<role>" per competing role the issue CURRENTLY carries>
|
|
481
495
|
```
|
|
482
496
|
|
|
497
|
+
Name only roles the read actually returned: `--remove-label` on a label the issue does not carry is a 404, which turns a clean transition into an error and makes a repeat cycle fail where the first succeeded.
|
|
498
|
+
|
|
483
499
|
If `$DONE` is terminal, immediately close the native GitHub issue:
|
|
484
500
|
|
|
485
501
|
```bash
|
|
@@ -488,6 +504,24 @@ gh issue close <number> --repo <org>/<repo> --reason completed
|
|
|
488
504
|
|
|
489
505
|
This close is idempotent: if the issue is already closed, record that native closure was already satisfied and continue. If `$DONE` is an intermediate env state, leave the issue open by design.
|
|
490
506
|
|
|
507
|
+
5. **Confirm with an independent readback, and only then announce.** Re-read the issue rather than trusting what the edit and close commands reported about themselves:
|
|
508
|
+
|
|
509
|
+
```bash
|
|
510
|
+
gh issue view <number> --repo <org>/<repo> --json state,stateReason,labels
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
The item must carry `$DONE` and **no** other lifecycle role, and — when `$DONE` is terminal — read `state: CLOSED`. Anything else is recorded as an Error naming the roles still present; do not report the transition as applied on the strength of a write that said it worked.
|
|
514
|
+
|
|
515
|
+
Post the completion comment **only after** that readback passes:
|
|
516
|
+
|
|
517
|
+
```bash
|
|
518
|
+
gh issue comment <number> --repo <org>/<repo> --body "[claude-build-intake] Build complete. PR <URL> merged. Transitioned to $DONE."
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
The ordering is the point. Announcing before verifying leaves a public "Build complete" on an item whose lifecycle state was never confirmed, and a reader has no way to tell that claim apart from a verified one — the same indistinguishability the readback exists to prevent. On a failed readback, comment the Error instead.
|
|
522
|
+
|
|
523
|
+
`node scripts/lisa-work-item.mjs complete --ref <org>/<repo>#<number>` performs this whole terminal sequence — merged-PR evidence check, full role reconciliation, close, independent readback — in one step, and is the preferred path whenever `$DONE` is the terminal value. Use the manual commands above for intermediate env transitions, which must not close the issue.
|
|
524
|
+
|
|
491
525
|
For any non-Success outcome, do NOT transition. The issue sits in `$CLAIMED` (or wherever the lifecycle left it) — humans take it from there.
|
|
492
526
|
|
|
493
527
|
#### 3d.1 Roll up the parent chain (forward rollup)
|
|
@@ -541,7 +575,7 @@ Total PRs opened: <n>
|
|
|
541
575
|
- **Claim-first ordering**: `$CLAIMED` set BEFORE the lifecycle dispatch for leaves; containers are also moved to `$CLAIMED` to leave the ready pickup queue, but are not dispatched.
|
|
542
576
|
- **No writes outside the lifecycle**: this skill only relabels `$READY → $CLAIMED` and `$CLAIMED → $DONE`. For containers, `$READY → $CLAIMED` is a lifecycle repair, not a direct build claim. Every other label change is owned by the per-issue lifecycle (github-agent workflow).
|
|
543
577
|
- **Duplicate terminal exception**: `DUPLICATE_ALREADY_FIXED` is the only triage outcome that may close a claimed item without a PR from this cycle. It must include a canonical issue reference and empirical base-branch evidence, and it closes as duplicate/not-planned rather than as completed build work.
|
|
544
|
-
- **Terminal native closure**: after `$
|
|
578
|
+
- **Terminal native closure**: after applying `$DONE` and retiring every competing lifecycle role (`leaf-only-lifecycle`, "Exactly one lifecycle role survives closure"), close the GitHub issue only when `$DONE` is the true terminal done value per `leaf-only-lifecycle`; intermediate env labels stay open. Confirm the result with an independent readback, never with the write's own response.
|
|
545
579
|
- **One item per cycle**: per-issue exceptions are caught and recorded, then the cycle exits. The scheduler owns retrying or moving on to the next ready item.
|
|
546
580
|
- **Single cycle per repo**: do not run two `lisa-github-build-intake` cycles in parallel against the same repo — concurrent claims could race. The scheduling layer is responsible for serialization.
|
|
547
581
|
- **Single-label invariant**: after every transition, verify exactly one `status:*` label is present on the issue. If two are present (rare race), surface as an Error and skip — do NOT auto-resolve.
|
|
@@ -49,6 +49,22 @@ When a leaf reaches the true terminal `done` (the production / final-env value),
|
|
|
49
49
|
|
|
50
50
|
Intermediate env-keyed states (`status:on-dev`, `On Stg`, etc.) remain open. Idempotent — if already closed, report and continue.
|
|
51
51
|
|
|
52
|
+
### Exactly one lifecycle role survives closure
|
|
53
|
+
|
|
54
|
+
The terminal transition retires **every** other lifecycle role the item carries, not only the one it was last known to hold. Resolve the complete configured set — `ready`, `claimed`, `review` where bound, `blocked`, and every env-keyed `done` value — and remove each one that is present, except the terminal role being applied. Type, component, priority, and provenance labels are untouched.
|
|
55
|
+
|
|
56
|
+
Retiring only the claimed role assumes the item travelled the happy path, and items skip stages: one reaches completion straight from `ready` having never been claimed, another is unblocked and closed still carrying `blocked`. Measured on a live tracker: 34 closed issues carried an active lifecycle role, six of them carrying `ready` and the terminal role at once.
|
|
57
|
+
|
|
58
|
+
**This is not cosmetic.** A queue scan filters on the synthesized role, not the native closed state, so a closed item still reading `ready` is handed back out — one already-shipped fix was independently rebuilt end to end before a push-time gate caught it.
|
|
59
|
+
|
|
60
|
+
Three constraints on how it is done:
|
|
61
|
+
|
|
62
|
+
- **Remove only roles the item actually carries.** Naming an absent label is a 404, which fails a clean closure and makes a repeat run fail where the first one succeeded.
|
|
63
|
+
- **Verify by re-reading.** Confirm the final closed state and the unique terminal role from a **fresh read of the item**, never from the write's own response. A write that reports success is not evidence of the state it intended to produce — asserting otherwise is the same defect as the drift being repaired, one layer up.
|
|
64
|
+
- **Never re-complete an abandoned item.** Applying a terminal `done` role to an item someone already closed as not-planned rewrites their decision that the work would not be done, and afterwards the two are indistinguishable. Refuse and say so. This does not restrict the duplicate closeout below, which is a flow deliberately closing an open item as not-planned rather than overwriting a closure that already exists.
|
|
65
|
+
|
|
66
|
+
Trackers with a native lifecycle field (JIRA, Linear) get exclusivity for free: the field that closes the item is the field a queue scan filters on, so the two cannot disagree. GitHub Issues has no lifecycle beyond open/closed, so Lisa synthesizes one in labels and inherits a reconciliation duty the others do not have. That asymmetry is tracked separately in `CodySwannGT/lisa#3479`; this section is the obligation, not the redesign.
|
|
67
|
+
|
|
52
68
|
Duplicate closeout is a narrow terminal exception: build intake may close a claimed item without a PR only when `ticket-triage` returns `DUPLICATE_ALREADY_FIXED` with a canonical item reference and empirical base-branch proof. Close it through provider duplicate semantics, not as completed build work. `BLOCKED`, ambiguous, duplicate-of-open, and other human-owned dispositions are not auto-closed.
|
|
53
69
|
|
|
54
70
|
Full vendor mechanics + the state machine in prose: [reference/leaf-only-lifecycle.md](../reference/leaf-only-lifecycle.md).
|
|
@@ -431,14 +431,26 @@ Run this only when the returned triage verdict is exactly `DUPLICATE_ALREADY_FIX
|
|
|
431
431
|
2. Post or preserve the triage-finding comment that explains why this issue is a duplicate and names the canonical issue.
|
|
432
432
|
3. Ensure a native `duplicates <canonical>` link exists when GitHub exposes issue relationships; if this installation cannot create that relationship, leave an explicit issue cross-reference comment/body link and record the limitation in the summary.
|
|
433
433
|
4. Resolve terminal `$DONE` exactly as in Phase 3d. For a single-env repo, `$DONE` is terminal; for env-keyed config, only the production/final value is terminal.
|
|
434
|
-
5.
|
|
434
|
+
5. Apply `$DONE` and retire every competing lifecycle role the issue currently carries — not just `$CLAIMED` — per the `leaf-only-lifecycle` rule's "Exactly one lifecycle role survives closure". Then close the issue as duplicate/not-planned:
|
|
435
435
|
|
|
436
436
|
```bash
|
|
437
|
-
gh issue
|
|
438
|
-
gh issue
|
|
437
|
+
gh issue view <number> --repo <org>/<repo> --json labels -q '[.labels[].name] | join(" ")'
|
|
438
|
+
gh issue edit <number> --repo <org>/<repo> --add-label "$DONE" <one --remove-label "<role>" per competing role the issue CURRENTLY carries>
|
|
439
439
|
gh issue close <number> --repo <org>/<repo> --reason "not planned"
|
|
440
440
|
```
|
|
441
441
|
|
|
442
|
+
6. **Confirm with an independent readback, and only then announce** — the same obligation as Phase 3d step 5, for the same reason. Re-read the issue instead of trusting what the edit and close reported:
|
|
443
|
+
|
|
444
|
+
```bash
|
|
445
|
+
gh issue view <number> --repo <org>/<repo> --json state,stateReason,labels
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
It must read `state: CLOSED` with `stateReason: NOT_PLANNED`, carry `$DONE`, and carry **no** other lifecycle role. Anything else is an Error naming what is still present. Post the closeout comment only after that passes:
|
|
449
|
+
|
|
450
|
+
```bash
|
|
451
|
+
gh issue comment <number> --repo <org>/<repo> --body "[claude-build-intake] Closed as duplicate of <canonical>. Canonical fix: <PR-or-commit>. Evidence: <base-branch-proof>."
|
|
452
|
+
```
|
|
453
|
+
|
|
442
454
|
If the canonical fix is merged but not yet present on the production branch, append the production-promotion caveat to the close comment: the production error can recur until the canonical issue promotes, and recurrence is tracked by the canonical issue rather than by reopening this duplicate.
|
|
443
455
|
|
|
444
456
|
This path is distinct from `BLOCKED`: ambiguity, open blockers, and duplicate-of-open findings remain held for human action and must not be auto-closed.
|
|
@@ -475,11 +487,15 @@ If the lifecycle run returned Success:
|
|
|
475
487
|
- If `github.labels.build.done` is an object, only the production/final environment value is terminal (default: `status:done`). Intermediate env values such as `status:on-dev` and `status:on-stg` are not terminal and must stay open.
|
|
476
488
|
- If the project uses a different final environment name, resolve it from the configured deployment topology; if ambiguous, record an Error and do not close.
|
|
477
489
|
|
|
490
|
+
4. **Retire every competing lifecycle role**, per the `leaf-only-lifecycle` rule's "Exactly one lifecycle role survives closure" — cite it, do not restate it. Read the issue's current labels, intersect them with the configured role set (`$READY`, `$CLAIMED`, `$BLOCKED`, `$REVIEW` where bound, and every env-keyed done value), and remove each one that is present except `$DONE`. Removing only `$CLAIMED` leaves a stale `$READY` on any item that reached completion without passing through the claimed lane, and a closed item still reading ready is re-dispatched by this very scanner on its next cycle.
|
|
491
|
+
|
|
478
492
|
```bash
|
|
479
|
-
gh issue
|
|
480
|
-
gh issue
|
|
493
|
+
gh issue view <number> --repo <org>/<repo> --json labels -q '[.labels[].name] | join(" ")'
|
|
494
|
+
gh issue edit <number> --repo <org>/<repo> --add-label "$DONE" <one --remove-label "<role>" per competing role the issue CURRENTLY carries>
|
|
481
495
|
```
|
|
482
496
|
|
|
497
|
+
Name only roles the read actually returned: `--remove-label` on a label the issue does not carry is a 404, which turns a clean transition into an error and makes a repeat cycle fail where the first succeeded.
|
|
498
|
+
|
|
483
499
|
If `$DONE` is terminal, immediately close the native GitHub issue:
|
|
484
500
|
|
|
485
501
|
```bash
|
|
@@ -488,6 +504,24 @@ gh issue close <number> --repo <org>/<repo> --reason completed
|
|
|
488
504
|
|
|
489
505
|
This close is idempotent: if the issue is already closed, record that native closure was already satisfied and continue. If `$DONE` is an intermediate env state, leave the issue open by design.
|
|
490
506
|
|
|
507
|
+
5. **Confirm with an independent readback, and only then announce.** Re-read the issue rather than trusting what the edit and close commands reported about themselves:
|
|
508
|
+
|
|
509
|
+
```bash
|
|
510
|
+
gh issue view <number> --repo <org>/<repo> --json state,stateReason,labels
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
The item must carry `$DONE` and **no** other lifecycle role, and — when `$DONE` is terminal — read `state: CLOSED`. Anything else is recorded as an Error naming the roles still present; do not report the transition as applied on the strength of a write that said it worked.
|
|
514
|
+
|
|
515
|
+
Post the completion comment **only after** that readback passes:
|
|
516
|
+
|
|
517
|
+
```bash
|
|
518
|
+
gh issue comment <number> --repo <org>/<repo> --body "[claude-build-intake] Build complete. PR <URL> merged. Transitioned to $DONE."
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
The ordering is the point. Announcing before verifying leaves a public "Build complete" on an item whose lifecycle state was never confirmed, and a reader has no way to tell that claim apart from a verified one — the same indistinguishability the readback exists to prevent. On a failed readback, comment the Error instead.
|
|
522
|
+
|
|
523
|
+
`node scripts/lisa-work-item.mjs complete --ref <org>/<repo>#<number>` performs this whole terminal sequence — merged-PR evidence check, full role reconciliation, close, independent readback — in one step, and is the preferred path whenever `$DONE` is the terminal value. Use the manual commands above for intermediate env transitions, which must not close the issue.
|
|
524
|
+
|
|
491
525
|
For any non-Success outcome, do NOT transition. The issue sits in `$CLAIMED` (or wherever the lifecycle left it) — humans take it from there.
|
|
492
526
|
|
|
493
527
|
#### 3d.1 Roll up the parent chain (forward rollup)
|
|
@@ -541,7 +575,7 @@ Total PRs opened: <n>
|
|
|
541
575
|
- **Claim-first ordering**: `$CLAIMED` set BEFORE the lifecycle dispatch for leaves; containers are also moved to `$CLAIMED` to leave the ready pickup queue, but are not dispatched.
|
|
542
576
|
- **No writes outside the lifecycle**: this skill only relabels `$READY → $CLAIMED` and `$CLAIMED → $DONE`. For containers, `$READY → $CLAIMED` is a lifecycle repair, not a direct build claim. Every other label change is owned by the per-issue lifecycle (github-agent workflow).
|
|
543
577
|
- **Duplicate terminal exception**: `DUPLICATE_ALREADY_FIXED` is the only triage outcome that may close a claimed item without a PR from this cycle. It must include a canonical issue reference and empirical base-branch evidence, and it closes as duplicate/not-planned rather than as completed build work.
|
|
544
|
-
- **Terminal native closure**: after `$
|
|
578
|
+
- **Terminal native closure**: after applying `$DONE` and retiring every competing lifecycle role (`leaf-only-lifecycle`, "Exactly one lifecycle role survives closure"), close the GitHub issue only when `$DONE` is the true terminal done value per `leaf-only-lifecycle`; intermediate env labels stay open. Confirm the result with an independent readback, never with the write's own response.
|
|
545
579
|
- **One item per cycle**: per-issue exceptions are caught and recorded, then the cycle exits. The scheduler owns retrying or moving on to the next ready item.
|
|
546
580
|
- **Single cycle per repo**: do not run two `lisa-github-build-intake` cycles in parallel against the same repo — concurrent claims could race. The scheduling layer is responsible for serialization.
|
|
547
581
|
- **Single-label invariant**: after every transition, verify exactly one `status:*` label is present on the issue. If two are present (rare race), surface as an Error and skip — do NOT auto-resolve.
|
|
@@ -431,14 +431,26 @@ Run this only when the returned triage verdict is exactly `DUPLICATE_ALREADY_FIX
|
|
|
431
431
|
2. Post or preserve the triage-finding comment that explains why this issue is a duplicate and names the canonical issue.
|
|
432
432
|
3. Ensure a native `duplicates <canonical>` link exists when GitHub exposes issue relationships; if this installation cannot create that relationship, leave an explicit issue cross-reference comment/body link and record the limitation in the summary.
|
|
433
433
|
4. Resolve terminal `$DONE` exactly as in Phase 3d. For a single-env repo, `$DONE` is terminal; for env-keyed config, only the production/final value is terminal.
|
|
434
|
-
5.
|
|
434
|
+
5. Apply `$DONE` and retire every competing lifecycle role the issue currently carries — not just `$CLAIMED` — per the `leaf-only-lifecycle` rule's "Exactly one lifecycle role survives closure". Then close the issue as duplicate/not-planned:
|
|
435
435
|
|
|
436
436
|
```bash
|
|
437
|
-
gh issue
|
|
438
|
-
gh issue
|
|
437
|
+
gh issue view <number> --repo <org>/<repo> --json labels -q '[.labels[].name] | join(" ")'
|
|
438
|
+
gh issue edit <number> --repo <org>/<repo> --add-label "$DONE" <one --remove-label "<role>" per competing role the issue CURRENTLY carries>
|
|
439
439
|
gh issue close <number> --repo <org>/<repo> --reason "not planned"
|
|
440
440
|
```
|
|
441
441
|
|
|
442
|
+
6. **Confirm with an independent readback, and only then announce** — the same obligation as Phase 3d step 5, for the same reason. Re-read the issue instead of trusting what the edit and close reported:
|
|
443
|
+
|
|
444
|
+
```bash
|
|
445
|
+
gh issue view <number> --repo <org>/<repo> --json state,stateReason,labels
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
It must read `state: CLOSED` with `stateReason: NOT_PLANNED`, carry `$DONE`, and carry **no** other lifecycle role. Anything else is an Error naming what is still present. Post the closeout comment only after that passes:
|
|
449
|
+
|
|
450
|
+
```bash
|
|
451
|
+
gh issue comment <number> --repo <org>/<repo> --body "[claude-build-intake] Closed as duplicate of <canonical>. Canonical fix: <PR-or-commit>. Evidence: <base-branch-proof>."
|
|
452
|
+
```
|
|
453
|
+
|
|
442
454
|
If the canonical fix is merged but not yet present on the production branch, append the production-promotion caveat to the close comment: the production error can recur until the canonical issue promotes, and recurrence is tracked by the canonical issue rather than by reopening this duplicate.
|
|
443
455
|
|
|
444
456
|
This path is distinct from `BLOCKED`: ambiguity, open blockers, and duplicate-of-open findings remain held for human action and must not be auto-closed.
|
|
@@ -475,11 +487,15 @@ If the lifecycle run returned Success:
|
|
|
475
487
|
- If `github.labels.build.done` is an object, only the production/final environment value is terminal (default: `status:done`). Intermediate env values such as `status:on-dev` and `status:on-stg` are not terminal and must stay open.
|
|
476
488
|
- If the project uses a different final environment name, resolve it from the configured deployment topology; if ambiguous, record an Error and do not close.
|
|
477
489
|
|
|
490
|
+
4. **Retire every competing lifecycle role**, per the `leaf-only-lifecycle` rule's "Exactly one lifecycle role survives closure" — cite it, do not restate it. Read the issue's current labels, intersect them with the configured role set (`$READY`, `$CLAIMED`, `$BLOCKED`, `$REVIEW` where bound, and every env-keyed done value), and remove each one that is present except `$DONE`. Removing only `$CLAIMED` leaves a stale `$READY` on any item that reached completion without passing through the claimed lane, and a closed item still reading ready is re-dispatched by this very scanner on its next cycle.
|
|
491
|
+
|
|
478
492
|
```bash
|
|
479
|
-
gh issue
|
|
480
|
-
gh issue
|
|
493
|
+
gh issue view <number> --repo <org>/<repo> --json labels -q '[.labels[].name] | join(" ")'
|
|
494
|
+
gh issue edit <number> --repo <org>/<repo> --add-label "$DONE" <one --remove-label "<role>" per competing role the issue CURRENTLY carries>
|
|
481
495
|
```
|
|
482
496
|
|
|
497
|
+
Name only roles the read actually returned: `--remove-label` on a label the issue does not carry is a 404, which turns a clean transition into an error and makes a repeat cycle fail where the first succeeded.
|
|
498
|
+
|
|
483
499
|
If `$DONE` is terminal, immediately close the native GitHub issue:
|
|
484
500
|
|
|
485
501
|
```bash
|
|
@@ -488,6 +504,24 @@ gh issue close <number> --repo <org>/<repo> --reason completed
|
|
|
488
504
|
|
|
489
505
|
This close is idempotent: if the issue is already closed, record that native closure was already satisfied and continue. If `$DONE` is an intermediate env state, leave the issue open by design.
|
|
490
506
|
|
|
507
|
+
5. **Confirm with an independent readback, and only then announce.** Re-read the issue rather than trusting what the edit and close commands reported about themselves:
|
|
508
|
+
|
|
509
|
+
```bash
|
|
510
|
+
gh issue view <number> --repo <org>/<repo> --json state,stateReason,labels
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
The item must carry `$DONE` and **no** other lifecycle role, and — when `$DONE` is terminal — read `state: CLOSED`. Anything else is recorded as an Error naming the roles still present; do not report the transition as applied on the strength of a write that said it worked.
|
|
514
|
+
|
|
515
|
+
Post the completion comment **only after** that readback passes:
|
|
516
|
+
|
|
517
|
+
```bash
|
|
518
|
+
gh issue comment <number> --repo <org>/<repo> --body "[claude-build-intake] Build complete. PR <URL> merged. Transitioned to $DONE."
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
The ordering is the point. Announcing before verifying leaves a public "Build complete" on an item whose lifecycle state was never confirmed, and a reader has no way to tell that claim apart from a verified one — the same indistinguishability the readback exists to prevent. On a failed readback, comment the Error instead.
|
|
522
|
+
|
|
523
|
+
`node scripts/lisa-work-item.mjs complete --ref <org>/<repo>#<number>` performs this whole terminal sequence — merged-PR evidence check, full role reconciliation, close, independent readback — in one step, and is the preferred path whenever `$DONE` is the terminal value. Use the manual commands above for intermediate env transitions, which must not close the issue.
|
|
524
|
+
|
|
491
525
|
For any non-Success outcome, do NOT transition. The issue sits in `$CLAIMED` (or wherever the lifecycle left it) — humans take it from there.
|
|
492
526
|
|
|
493
527
|
#### 3d.1 Roll up the parent chain (forward rollup)
|
|
@@ -541,7 +575,7 @@ Total PRs opened: <n>
|
|
|
541
575
|
- **Claim-first ordering**: `$CLAIMED` set BEFORE the lifecycle dispatch for leaves; containers are also moved to `$CLAIMED` to leave the ready pickup queue, but are not dispatched.
|
|
542
576
|
- **No writes outside the lifecycle**: this skill only relabels `$READY → $CLAIMED` and `$CLAIMED → $DONE`. For containers, `$READY → $CLAIMED` is a lifecycle repair, not a direct build claim. Every other label change is owned by the per-issue lifecycle (github-agent workflow).
|
|
543
577
|
- **Duplicate terminal exception**: `DUPLICATE_ALREADY_FIXED` is the only triage outcome that may close a claimed item without a PR from this cycle. It must include a canonical issue reference and empirical base-branch evidence, and it closes as duplicate/not-planned rather than as completed build work.
|
|
544
|
-
- **Terminal native closure**: after `$
|
|
578
|
+
- **Terminal native closure**: after applying `$DONE` and retiring every competing lifecycle role (`leaf-only-lifecycle`, "Exactly one lifecycle role survives closure"), close the GitHub issue only when `$DONE` is the true terminal done value per `leaf-only-lifecycle`; intermediate env labels stay open. Confirm the result with an independent readback, never with the write's own response.
|
|
545
579
|
- **One item per cycle**: per-issue exceptions are caught and recorded, then the cycle exits. The scheduler owns retrying or moving on to the next ready item.
|
|
546
580
|
- **Single cycle per repo**: do not run two `lisa-github-build-intake` cycles in parallel against the same repo — concurrent claims could race. The scheduling layer is responsible for serialization.
|
|
547
581
|
- **Single-label invariant**: after every transition, verify exactly one `status:*` label is present on the issue. If two are present (rare race), surface as an Error and skip — do NOT auto-resolve.
|
|
@@ -49,6 +49,22 @@ When a leaf reaches the true terminal `done` (the production / final-env value),
|
|
|
49
49
|
|
|
50
50
|
Intermediate env-keyed states (`status:on-dev`, `On Stg`, etc.) remain open. Idempotent — if already closed, report and continue.
|
|
51
51
|
|
|
52
|
+
### Exactly one lifecycle role survives closure
|
|
53
|
+
|
|
54
|
+
The terminal transition retires **every** other lifecycle role the item carries, not only the one it was last known to hold. Resolve the complete configured set — `ready`, `claimed`, `review` where bound, `blocked`, and every env-keyed `done` value — and remove each one that is present, except the terminal role being applied. Type, component, priority, and provenance labels are untouched.
|
|
55
|
+
|
|
56
|
+
Retiring only the claimed role assumes the item travelled the happy path, and items skip stages: one reaches completion straight from `ready` having never been claimed, another is unblocked and closed still carrying `blocked`. Measured on a live tracker: 34 closed issues carried an active lifecycle role, six of them carrying `ready` and the terminal role at once.
|
|
57
|
+
|
|
58
|
+
**This is not cosmetic.** A queue scan filters on the synthesized role, not the native closed state, so a closed item still reading `ready` is handed back out — one already-shipped fix was independently rebuilt end to end before a push-time gate caught it.
|
|
59
|
+
|
|
60
|
+
Three constraints on how it is done:
|
|
61
|
+
|
|
62
|
+
- **Remove only roles the item actually carries.** Naming an absent label is a 404, which fails a clean closure and makes a repeat run fail where the first one succeeded.
|
|
63
|
+
- **Verify by re-reading.** Confirm the final closed state and the unique terminal role from a **fresh read of the item**, never from the write's own response. A write that reports success is not evidence of the state it intended to produce — asserting otherwise is the same defect as the drift being repaired, one layer up.
|
|
64
|
+
- **Never re-complete an abandoned item.** Applying a terminal `done` role to an item someone already closed as not-planned rewrites their decision that the work would not be done, and afterwards the two are indistinguishable. Refuse and say so. This does not restrict the duplicate closeout below, which is a flow deliberately closing an open item as not-planned rather than overwriting a closure that already exists.
|
|
65
|
+
|
|
66
|
+
Trackers with a native lifecycle field (JIRA, Linear) get exclusivity for free: the field that closes the item is the field a queue scan filters on, so the two cannot disagree. GitHub Issues has no lifecycle beyond open/closed, so Lisa synthesizes one in labels and inherits a reconciliation duty the others do not have. That asymmetry is tracked separately in `CodySwannGT/lisa#3479`; this section is the obligation, not the redesign.
|
|
67
|
+
|
|
52
68
|
Duplicate closeout is a narrow terminal exception: build intake may close a claimed item without a PR only when `ticket-triage` returns `DUPLICATE_ALREADY_FIXED` with a canonical item reference and empirical base-branch proof. Close it through provider duplicate semantics, not as completed build work. `BLOCKED`, ambiguous, duplicate-of-open, and other human-owned dispositions are not auto-closed.
|
|
53
69
|
|
|
54
70
|
Full vendor mechanics + the state machine in prose: [reference/leaf-only-lifecycle.md](../reference/leaf-only-lifecycle.md).
|
|
@@ -431,14 +431,26 @@ Run this only when the returned triage verdict is exactly `DUPLICATE_ALREADY_FIX
|
|
|
431
431
|
2. Post or preserve the triage-finding comment that explains why this issue is a duplicate and names the canonical issue.
|
|
432
432
|
3. Ensure a native `duplicates <canonical>` link exists when GitHub exposes issue relationships; if this installation cannot create that relationship, leave an explicit issue cross-reference comment/body link and record the limitation in the summary.
|
|
433
433
|
4. Resolve terminal `$DONE` exactly as in Phase 3d. For a single-env repo, `$DONE` is terminal; for env-keyed config, only the production/final value is terminal.
|
|
434
|
-
5.
|
|
434
|
+
5. Apply `$DONE` and retire every competing lifecycle role the issue currently carries — not just `$CLAIMED` — per the `leaf-only-lifecycle` rule's "Exactly one lifecycle role survives closure". Then close the issue as duplicate/not-planned:
|
|
435
435
|
|
|
436
436
|
```bash
|
|
437
|
-
gh issue
|
|
438
|
-
gh issue
|
|
437
|
+
gh issue view <number> --repo <org>/<repo> --json labels -q '[.labels[].name] | join(" ")'
|
|
438
|
+
gh issue edit <number> --repo <org>/<repo> --add-label "$DONE" <one --remove-label "<role>" per competing role the issue CURRENTLY carries>
|
|
439
439
|
gh issue close <number> --repo <org>/<repo> --reason "not planned"
|
|
440
440
|
```
|
|
441
441
|
|
|
442
|
+
6. **Confirm with an independent readback, and only then announce** — the same obligation as Phase 3d step 5, for the same reason. Re-read the issue instead of trusting what the edit and close reported:
|
|
443
|
+
|
|
444
|
+
```bash
|
|
445
|
+
gh issue view <number> --repo <org>/<repo> --json state,stateReason,labels
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
It must read `state: CLOSED` with `stateReason: NOT_PLANNED`, carry `$DONE`, and carry **no** other lifecycle role. Anything else is an Error naming what is still present. Post the closeout comment only after that passes:
|
|
449
|
+
|
|
450
|
+
```bash
|
|
451
|
+
gh issue comment <number> --repo <org>/<repo> --body "[claude-build-intake] Closed as duplicate of <canonical>. Canonical fix: <PR-or-commit>. Evidence: <base-branch-proof>."
|
|
452
|
+
```
|
|
453
|
+
|
|
442
454
|
If the canonical fix is merged but not yet present on the production branch, append the production-promotion caveat to the close comment: the production error can recur until the canonical issue promotes, and recurrence is tracked by the canonical issue rather than by reopening this duplicate.
|
|
443
455
|
|
|
444
456
|
This path is distinct from `BLOCKED`: ambiguity, open blockers, and duplicate-of-open findings remain held for human action and must not be auto-closed.
|
|
@@ -475,11 +487,15 @@ If the lifecycle run returned Success:
|
|
|
475
487
|
- If `github.labels.build.done` is an object, only the production/final environment value is terminal (default: `status:done`). Intermediate env values such as `status:on-dev` and `status:on-stg` are not terminal and must stay open.
|
|
476
488
|
- If the project uses a different final environment name, resolve it from the configured deployment topology; if ambiguous, record an Error and do not close.
|
|
477
489
|
|
|
490
|
+
4. **Retire every competing lifecycle role**, per the `leaf-only-lifecycle` rule's "Exactly one lifecycle role survives closure" — cite it, do not restate it. Read the issue's current labels, intersect them with the configured role set (`$READY`, `$CLAIMED`, `$BLOCKED`, `$REVIEW` where bound, and every env-keyed done value), and remove each one that is present except `$DONE`. Removing only `$CLAIMED` leaves a stale `$READY` on any item that reached completion without passing through the claimed lane, and a closed item still reading ready is re-dispatched by this very scanner on its next cycle.
|
|
491
|
+
|
|
478
492
|
```bash
|
|
479
|
-
gh issue
|
|
480
|
-
gh issue
|
|
493
|
+
gh issue view <number> --repo <org>/<repo> --json labels -q '[.labels[].name] | join(" ")'
|
|
494
|
+
gh issue edit <number> --repo <org>/<repo> --add-label "$DONE" <one --remove-label "<role>" per competing role the issue CURRENTLY carries>
|
|
481
495
|
```
|
|
482
496
|
|
|
497
|
+
Name only roles the read actually returned: `--remove-label` on a label the issue does not carry is a 404, which turns a clean transition into an error and makes a repeat cycle fail where the first succeeded.
|
|
498
|
+
|
|
483
499
|
If `$DONE` is terminal, immediately close the native GitHub issue:
|
|
484
500
|
|
|
485
501
|
```bash
|
|
@@ -488,6 +504,24 @@ gh issue close <number> --repo <org>/<repo> --reason completed
|
|
|
488
504
|
|
|
489
505
|
This close is idempotent: if the issue is already closed, record that native closure was already satisfied and continue. If `$DONE` is an intermediate env state, leave the issue open by design.
|
|
490
506
|
|
|
507
|
+
5. **Confirm with an independent readback, and only then announce.** Re-read the issue rather than trusting what the edit and close commands reported about themselves:
|
|
508
|
+
|
|
509
|
+
```bash
|
|
510
|
+
gh issue view <number> --repo <org>/<repo> --json state,stateReason,labels
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
The item must carry `$DONE` and **no** other lifecycle role, and — when `$DONE` is terminal — read `state: CLOSED`. Anything else is recorded as an Error naming the roles still present; do not report the transition as applied on the strength of a write that said it worked.
|
|
514
|
+
|
|
515
|
+
Post the completion comment **only after** that readback passes:
|
|
516
|
+
|
|
517
|
+
```bash
|
|
518
|
+
gh issue comment <number> --repo <org>/<repo> --body "[claude-build-intake] Build complete. PR <URL> merged. Transitioned to $DONE."
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
The ordering is the point. Announcing before verifying leaves a public "Build complete" on an item whose lifecycle state was never confirmed, and a reader has no way to tell that claim apart from a verified one — the same indistinguishability the readback exists to prevent. On a failed readback, comment the Error instead.
|
|
522
|
+
|
|
523
|
+
`node scripts/lisa-work-item.mjs complete --ref <org>/<repo>#<number>` performs this whole terminal sequence — merged-PR evidence check, full role reconciliation, close, independent readback — in one step, and is the preferred path whenever `$DONE` is the terminal value. Use the manual commands above for intermediate env transitions, which must not close the issue.
|
|
524
|
+
|
|
491
525
|
For any non-Success outcome, do NOT transition. The issue sits in `$CLAIMED` (or wherever the lifecycle left it) — humans take it from there.
|
|
492
526
|
|
|
493
527
|
#### 3d.1 Roll up the parent chain (forward rollup)
|
|
@@ -541,7 +575,7 @@ Total PRs opened: <n>
|
|
|
541
575
|
- **Claim-first ordering**: `$CLAIMED` set BEFORE the lifecycle dispatch for leaves; containers are also moved to `$CLAIMED` to leave the ready pickup queue, but are not dispatched.
|
|
542
576
|
- **No writes outside the lifecycle**: this skill only relabels `$READY → $CLAIMED` and `$CLAIMED → $DONE`. For containers, `$READY → $CLAIMED` is a lifecycle repair, not a direct build claim. Every other label change is owned by the per-issue lifecycle (github-agent workflow).
|
|
543
577
|
- **Duplicate terminal exception**: `DUPLICATE_ALREADY_FIXED` is the only triage outcome that may close a claimed item without a PR from this cycle. It must include a canonical issue reference and empirical base-branch evidence, and it closes as duplicate/not-planned rather than as completed build work.
|
|
544
|
-
- **Terminal native closure**: after `$
|
|
578
|
+
- **Terminal native closure**: after applying `$DONE` and retiring every competing lifecycle role (`leaf-only-lifecycle`, "Exactly one lifecycle role survives closure"), close the GitHub issue only when `$DONE` is the true terminal done value per `leaf-only-lifecycle`; intermediate env labels stay open. Confirm the result with an independent readback, never with the write's own response.
|
|
545
579
|
- **One item per cycle**: per-issue exceptions are caught and recorded, then the cycle exits. The scheduler owns retrying or moving on to the next ready item.
|
|
546
580
|
- **Single cycle per repo**: do not run two `lisa-github-build-intake` cycles in parallel against the same repo — concurrent claims could race. The scheduling layer is responsible for serialization.
|
|
547
581
|
- **Single-label invariant**: after every transition, verify exactly one `status:*` label is present on the issue. If two are present (rare race), surface as an Error and skip — do NOT auto-resolve.
|
|
@@ -54,6 +54,22 @@ When a leaf reaches the true terminal `done` (the production / final-env value),
|
|
|
54
54
|
|
|
55
55
|
Intermediate env-keyed states (`status:on-dev`, `On Stg`, etc.) remain open. Idempotent — if already closed, report and continue.
|
|
56
56
|
|
|
57
|
+
### Exactly one lifecycle role survives closure
|
|
58
|
+
|
|
59
|
+
The terminal transition retires **every** other lifecycle role the item carries, not only the one it was last known to hold. Resolve the complete configured set — `ready`, `claimed`, `review` where bound, `blocked`, and every env-keyed `done` value — and remove each one that is present, except the terminal role being applied. Type, component, priority, and provenance labels are untouched.
|
|
60
|
+
|
|
61
|
+
Retiring only the claimed role assumes the item travelled the happy path, and items skip stages: one reaches completion straight from `ready` having never been claimed, another is unblocked and closed still carrying `blocked`. Measured on a live tracker: 34 closed issues carried an active lifecycle role, six of them carrying `ready` and the terminal role at once.
|
|
62
|
+
|
|
63
|
+
**This is not cosmetic.** A queue scan filters on the synthesized role, not the native closed state, so a closed item still reading `ready` is handed back out — one already-shipped fix was independently rebuilt end to end before a push-time gate caught it.
|
|
64
|
+
|
|
65
|
+
Three constraints on how it is done:
|
|
66
|
+
|
|
67
|
+
- **Remove only roles the item actually carries.** Naming an absent label is a 404, which fails a clean closure and makes a repeat run fail where the first one succeeded.
|
|
68
|
+
- **Verify by re-reading.** Confirm the final closed state and the unique terminal role from a **fresh read of the item**, never from the write's own response. A write that reports success is not evidence of the state it intended to produce — asserting otherwise is the same defect as the drift being repaired, one layer up.
|
|
69
|
+
- **Never re-complete an abandoned item.** Applying a terminal `done` role to an item someone already closed as not-planned rewrites their decision that the work would not be done, and afterwards the two are indistinguishable. Refuse and say so. This does not restrict the duplicate closeout below, which is a flow deliberately closing an open item as not-planned rather than overwriting a closure that already exists.
|
|
70
|
+
|
|
71
|
+
Trackers with a native lifecycle field (JIRA, Linear) get exclusivity for free: the field that closes the item is the field a queue scan filters on, so the two cannot disagree. GitHub Issues has no lifecycle beyond open/closed, so Lisa synthesizes one in labels and inherits a reconciliation duty the others do not have. That asymmetry is tracked separately in `CodySwannGT/lisa#3479`; this section is the obligation, not the redesign.
|
|
72
|
+
|
|
57
73
|
Duplicate closeout is a narrow terminal exception: build intake may close a claimed item without a PR only when `ticket-triage` returns `DUPLICATE_ALREADY_FIXED` with a canonical item reference and empirical base-branch proof. Close it through provider duplicate semantics, not as completed build work. `BLOCKED`, ambiguous, duplicate-of-open, and other human-owned dispositions are not auto-closed.
|
|
58
74
|
|
|
59
75
|
Full vendor mechanics + the state machine in prose: [reference/leaf-only-lifecycle.md](leaf-only-lifecycle-reference.mdc).
|