@codyswann/lisa 4.33.6 → 4.33.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/README.md +22 -0
  2. package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +44 -1
  3. package/all/copy-overwrite/scripts/lisa-work-item.mjs +361 -10
  4. package/dist/cli/apply.d.ts.map +1 -1
  5. package/dist/cli/apply.js +8 -1
  6. package/dist/cli/apply.js.map +1 -1
  7. package/dist/configs/vitest/base.d.ts +76 -11
  8. package/dist/configs/vitest/base.d.ts.map +1 -1
  9. package/dist/configs/vitest/base.js +121 -14
  10. package/dist/configs/vitest/base.js.map +1 -1
  11. package/dist/core/apply-receipt.d.ts +18 -0
  12. package/dist/core/apply-receipt.d.ts.map +1 -1
  13. package/dist/core/apply-receipt.js +7 -5
  14. package/dist/core/apply-receipt.js.map +1 -1
  15. package/dist/core/config.d.ts +34 -0
  16. package/dist/core/config.d.ts.map +1 -1
  17. package/dist/core/config.js.map +1 -1
  18. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  19. package/dist/core/lisa-owned-hash-ledger.js +12 -0
  20. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  21. package/dist/core/lisa.d.ts +75 -0
  22. package/dist/core/lisa.d.ts.map +1 -1
  23. package/dist/core/lisa.js +125 -6
  24. package/dist/core/lisa.js.map +1 -1
  25. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  26. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  27. package/dist/core/upstream-evidence-manifest.js +31 -13
  28. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  29. package/dist/core/workflow-deletion-ownership.d.ts +69 -0
  30. package/dist/core/workflow-deletion-ownership.d.ts.map +1 -0
  31. package/dist/core/workflow-deletion-ownership.js +156 -0
  32. package/dist/core/workflow-deletion-ownership.js.map +1 -0
  33. package/dist/opencode/plugin-templates/parity-safety-net.sh +44 -1
  34. package/package.json +4 -4
  35. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  37. package/plugins/lisa/.codex-plugin/skills/lisa-drive-pr-to-merge/SKILL.md +17 -2
  38. package/plugins/lisa/.codex-plugin/skills/lisa-github-build-intake/SKILL.md +1 -1
  39. package/plugins/lisa/.codex-plugin/skills/lisa-intake-explain/SKILL.md +1 -1
  40. package/plugins/lisa/.codex-plugin/skills/lisa-repair-intake/SKILL.md +32 -20
  41. package/plugins/lisa/.codex-plugin/skills/lisa-ticket-triage/SKILL.md +1 -1
  42. package/plugins/lisa/hooks/parity-safety-net.sh +44 -1
  43. package/plugins/lisa/rules/eager/blocker-containment.md +44 -0
  44. package/plugins/lisa/rules/reference/blocker-containment.md +92 -0
  45. package/plugins/lisa/skills/lisa-drive-pr-to-merge/SKILL.md +17 -2
  46. package/plugins/lisa/skills/lisa-github-build-intake/SKILL.md +1 -1
  47. package/plugins/lisa/skills/lisa-intake-explain/SKILL.md +1 -1
  48. package/plugins/lisa/skills/lisa-repair-intake/SKILL.md +32 -20
  49. package/plugins/lisa/skills/lisa-ticket-triage/SKILL.md +1 -1
  50. package/plugins/lisa-agy/hooks/parity-safety-net.sh +44 -1
  51. package/plugins/lisa-agy/plugin.json +1 -1
  52. package/plugins/lisa-agy/skills/lisa-drive-pr-to-merge/SKILL.md +17 -2
  53. package/plugins/lisa-agy/skills/lisa-github-build-intake/SKILL.md +1 -1
  54. package/plugins/lisa-agy/skills/lisa-intake-explain/SKILL.md +1 -1
  55. package/plugins/lisa-agy/skills/lisa-repair-intake/SKILL.md +32 -20
  56. package/plugins/lisa-agy/skills/lisa-ticket-triage/SKILL.md +1 -1
  57. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  59. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  60. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-copilot/hooks/parity-safety-net.sh +44 -1
  64. package/plugins/lisa-copilot/rules/eager/blocker-containment.md +44 -0
  65. package/plugins/lisa-copilot/rules/reference/blocker-containment.md +92 -0
  66. package/plugins/lisa-copilot/skills/lisa-drive-pr-to-merge/SKILL.md +17 -2
  67. package/plugins/lisa-copilot/skills/lisa-github-build-intake/SKILL.md +1 -1
  68. package/plugins/lisa-copilot/skills/lisa-intake-explain/SKILL.md +1 -1
  69. package/plugins/lisa-copilot/skills/lisa-repair-intake/SKILL.md +32 -20
  70. package/plugins/lisa-copilot/skills/lisa-ticket-triage/SKILL.md +1 -1
  71. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-cursor/hooks/parity-safety-net.sh +44 -1
  73. package/plugins/lisa-cursor/rules/blocker-containment-reference.mdc +97 -0
  74. package/plugins/lisa-cursor/rules/blocker-containment.mdc +49 -0
  75. package/plugins/lisa-cursor/skills/lisa-drive-pr-to-merge/SKILL.md +17 -2
  76. package/plugins/lisa-cursor/skills/lisa-github-build-intake/SKILL.md +1 -1
  77. package/plugins/lisa-cursor/skills/lisa-intake-explain/SKILL.md +1 -1
  78. package/plugins/lisa-cursor/skills/lisa-repair-intake/SKILL.md +32 -20
  79. package/plugins/lisa-cursor/skills/lisa-ticket-triage/SKILL.md +1 -1
  80. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  82. package/plugins/lisa-expo-agy/plugin.json +1 -1
  83. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  84. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  86. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  87. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  88. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  90. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  91. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  92. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  93. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  94. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  95. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  96. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  97. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  98. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  99. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  100. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  101. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  102. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  103. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  104. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  105. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  106. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  107. package/plugins/lisa-rails-agy/plugin.json +1 -1
  108. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  109. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  110. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  111. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  112. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  113. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  114. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  115. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  116. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  117. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  118. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  119. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  120. package/plugins/src/base/hooks/parity-safety-net.sh +44 -1
  121. package/plugins/src/base/rules/eager/blocker-containment.md +44 -0
  122. package/plugins/src/base/rules/reference/blocker-containment.md +92 -0
  123. package/plugins/src/base/skills/lisa-drive-pr-to-merge/SKILL.md +17 -2
  124. package/plugins/src/base/skills/lisa-github-build-intake/SKILL.md +1 -1
  125. package/plugins/src/base/skills/lisa-intake-explain/SKILL.md +1 -1
  126. package/plugins/src/base/skills/lisa-repair-intake/SKILL.md +32 -20
  127. package/plugins/src/base/skills/lisa-ticket-triage/SKILL.md +1 -1
  128. package/scripts/two-channel-couplings.json +1 -1
  129. package/typescript/copy-overwrite/.github/GITHUB_ACTIONS.md +2 -2
  130. package/typescript/copy-overwrite/scripts/lisa-mutation.mjs +96 -12
  131. package/typescript/copy-overwrite/scripts/reconcile-nightly-e2e-tracking.mjs +3 -2
  132. package/typescript/deletions.json +8 -1
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The sentence a `copy-overwrite` template carries, minus its comment prefix.
3
+ *
4
+ * Matched as a substring rather than a whole line because the prefix differs by
5
+ * file type (`#`, `//`, ` *`) and the wording has been revised once already —
6
+ * the retired header said "changes will be overwritten on the next `lisa` run".
7
+ * Both spellings contain this phrase, so a consumer still holding a
8
+ * years-old Lisa workflow is still correctly attributed.
9
+ */
10
+ export declare const LISA_MANAGED_MARKER = "managed by Lisa";
11
+ /**
12
+ * The word a `create-only` seed header shouts, minus its comment prefix.
13
+ *
14
+ * "YOURS" is the load-bearing token of that contract and the one the header
15
+ * test asserts. A file carrying it came out of Lisa's template tree AND was
16
+ * handed to the consumer, which is exactly the pair of facts that makes it
17
+ * undeletable.
18
+ */
19
+ export declare const LISA_SEEDED_MARKER = "YOURS";
20
+ /** What Lisa can prove about a workflow file it has been asked to delete. */
21
+ export type WorkflowDeletionVerdict =
22
+ /** Header asserts Lisa replaces this file wholesale. Lisa may retire it. */
23
+ {
24
+ readonly kind: "lisa-managed";
25
+ }
26
+ /** Header seeded it as the consumer's. Lisa promised not to touch it. */
27
+ | {
28
+ readonly kind: "host-owned-seed";
29
+ }
30
+ /** No Lisa header at all. Lisa has no record of installing this. */
31
+ | {
32
+ readonly kind: "unattributable";
33
+ };
34
+ /**
35
+ * Whether a deletion target lives under `.github/workflows/`.
36
+ *
37
+ * Only this tree is gated. The manifests name 200-odd other paths whose
38
+ * deletion is unchanged by #3656, and narrowing the guard is deliberate: a
39
+ * workflow's absence is undetectable by construction, which is not true of a
40
+ * missing config file or skill directory.
41
+ * @param relativePath - Repo-relative path a deletions manifest wants removed
42
+ * @returns Whether the ownership gate applies to it
43
+ */
44
+ export declare function isWorkflowDeletionPath(relativePath: string): boolean;
45
+ /**
46
+ * Read the ownership contract a workflow file states about itself.
47
+ *
48
+ * Precedence is seed-before-managed. A `create-only` header mentions
49
+ * `copy-overwrite` in its own second line, so a naive managed-first check would
50
+ * read every seeded workflow as Lisa-managed and delete the very files #3656
51
+ * lost. Ordering it this way means the strongest claim a file can make is the
52
+ * one that keeps it.
53
+ * @param contents - Raw text of the workflow file on disk
54
+ * @returns The verdict its header supports
55
+ */
56
+ export declare function classifyWorkflowForDeletion(contents: string): WorkflowDeletionVerdict;
57
+ /**
58
+ * The operator-readable reason a workflow deletion was refused.
59
+ *
60
+ * Written for whoever is staring at an install log wondering why a file they
61
+ * expected to disappear is still there — so it names the contract, not the
62
+ * implementation, and says what to do about it.
63
+ * @param verdict - The verdict that refused the deletion
64
+ * @returns A sentence fragment naming why the file was kept
65
+ */
66
+ export declare function describeRefusal(verdict: Exclude<WorkflowDeletionVerdict, {
67
+ kind: "lisa-managed";
68
+ }>): string;
69
+ //# sourceMappingURL=workflow-deletion-ownership.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflow-deletion-ownership.d.ts","sourceRoot":"","sources":["../../src/core/workflow-deletion-ownership.ts"],"names":[],"mappings":"AAuEA;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AAErD;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,UAAU,CAAC;AAc1C,6EAA6E;AAC7E,MAAM,MAAM,uBAAuB;AACjC,4EAA4E;AAC1E;IAAE,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAA;CAAE;AACnC,yEAAyE;GACvE;IAAE,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAA;CAAE;AACtC,oEAAoE;GAClE;IAAE,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAA;CAAE,CAAC;AAYxC;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAEpE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,MAAM,GACf,uBAAuB,CAQzB;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,OAAO,CAAC,uBAAuB,EAAE;IAAE,IAAI,EAAE,cAAc,CAAA;CAAE,CAAC,GAClE,MAAM,CAIR"}
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Whether Lisa may delete a workflow file it finds in a consumer repository.
3
+ *
4
+ * A deletions manifest names DESTINATION PATHS. It cannot name bytes, and until
5
+ * CodySwannGT/lisa#3656 nothing between the manifest and `fse.remove` looked at
6
+ * any. So a manifest entry retiring one of Lisa's own workflows also removed a
7
+ * completely unrelated file a consumer happened to author at the same path —
8
+ * three of them at once, on a routine version bump, with nothing in the install
9
+ * output anyone could read afterwards.
10
+ *
11
+ * The damage shape is what makes path-only deletion unacceptable here rather
12
+ * than merely untidy. **A workflow that no longer exists cannot fail.** Removing
13
+ * one does not redden a check; it removes the check. Two capabilities stopped in
14
+ * that repo — PR branches were no longer auto-updated and required-checks drift
15
+ * was no longer policed — and both losses presented as a quiet, healthy repo.
16
+ * The only reason anyone found out was an unrelated test that happened to read
17
+ * one of the files off disk and got `ENOENT`.
18
+ *
19
+ * ## Ownership is read from the file's own header, and the header already exists
20
+ *
21
+ * Every template Lisa ships carries an ownership header, and which header it
22
+ * carries is enforced repo-wide by `tests/unit/templates/template-ownership-header.test.ts`.
23
+ * The two headers are not decoration — they are opposite contracts, and this
24
+ * module simply honours the one the file on disk is carrying:
25
+ *
26
+ * - **`copy-overwrite`**: "This file is managed by Lisa and IS replaced on each
27
+ * `lisa` run." Lisa asserts ownership of the bytes and has always replaced
28
+ * them wholesale. Retiring such a file is Lisa deleting its own artifact,
29
+ * which is squarely inside the mandate.
30
+ * - **`create-only`**: "Seeded by Lisa on first setup — this file is YOURS.
31
+ * Lisa will not overwrite it." Lisa explicitly DISCLAIMED ownership at seed
32
+ * time. Whatever is in that file now may be entirely the consumer's work, and
33
+ * deleting it is the one operation the header promised would never happen.
34
+ * Overwriting it was already refused; removing it must be too.
35
+ *
36
+ * All three files lost in #3656 were `create-only` seeds. So was every workflow
37
+ * template Lisa currently ships.
38
+ *
39
+ * ## Why a header and not the hash ledger
40
+ *
41
+ * `core/lisa-owned-provenance` answers a neighbouring question — are these bytes
42
+ * some past Lisa release? — from a generated hash ledger, and it would be the
43
+ * stronger signal if it applied. It does not: the generator
44
+ * (`scripts/generate-lisa-owned-hash-ledger.mjs`) enrols only `copy-overwrite`
45
+ * sources under `scripts/`, so `LISA_OWNED_HASH_LEDGER` holds no
46
+ * `.github/workflows/` key at all and every workflow would classify as
47
+ * `unenrolled` — the verdict whose documented meaning is "behave exactly as
48
+ * before", which is the bug. Enrolling workflows would mean walking the history
49
+ * of retired template paths, and it still could not vouch for a `create-only`
50
+ * seed a consumer has since edited. The header answers the question that is
51
+ * actually being asked, for every workflow Lisa has ever shipped, with no
52
+ * generated artifact to keep in step.
53
+ *
54
+ * ## The residual risk, stated
55
+ *
56
+ * A consumer who deletes the `copy-overwrite` header out of a Lisa-managed
57
+ * workflow makes it unattributable, and Lisa will then decline to retire it.
58
+ * That is the safe direction: a stale reusable workflow nobody calls costs
59
+ * nothing, and `lisa doctor` is not made worse by its presence. The unsafe
60
+ * direction — deleting a file Lisa cannot account for — is the one this module
61
+ * closes.
62
+ * @module core/workflow-deletion-ownership
63
+ */
64
+ import * as path from "node:path";
65
+ /**
66
+ * Repo-relative directory GitHub Actions reads workflow definitions from.
67
+ * Deletion of anything inside it is what this module governs.
68
+ */
69
+ const WORKFLOWS_DIR = ".github/workflows";
70
+ /**
71
+ * The sentence a `copy-overwrite` template carries, minus its comment prefix.
72
+ *
73
+ * Matched as a substring rather than a whole line because the prefix differs by
74
+ * file type (`#`, `//`, ` *`) and the wording has been revised once already —
75
+ * the retired header said "changes will be overwritten on the next `lisa` run".
76
+ * Both spellings contain this phrase, so a consumer still holding a
77
+ * years-old Lisa workflow is still correctly attributed.
78
+ */
79
+ export const LISA_MANAGED_MARKER = "managed by Lisa";
80
+ /**
81
+ * The word a `create-only` seed header shouts, minus its comment prefix.
82
+ *
83
+ * "YOURS" is the load-bearing token of that contract and the one the header
84
+ * test asserts. A file carrying it came out of Lisa's template tree AND was
85
+ * handed to the consumer, which is exactly the pair of facts that makes it
86
+ * undeletable.
87
+ */
88
+ export const LISA_SEEDED_MARKER = "YOURS";
89
+ /**
90
+ * How many leading lines of a file count as its ownership header.
91
+ *
92
+ * Bounded on purpose. A whole-file search would let any mention of Lisa
93
+ * anywhere in a consumer's workflow — a step name, a comment about a Lisa
94
+ * command — authorise that file's deletion, which is the false-negative
95
+ * direction that reintroduces #3656. Every shipped template puts its header on
96
+ * the first two lines; four covers a leading `---` document marker or a blank
97
+ * line above it and still stops short of any workflow body.
98
+ */
99
+ const HEADER_LINES = 4;
100
+ /**
101
+ * Normalize a path for comparison: POSIX separators, no leading `./`.
102
+ * @param value - Raw path from a deletions manifest
103
+ * @returns Comparable POSIX-style repo-relative path
104
+ */
105
+ function normalizeRepoPath(value) {
106
+ const posix = value.split(path.sep).join("/");
107
+ return posix.startsWith("./") ? posix.slice(2) : posix;
108
+ }
109
+ /**
110
+ * Whether a deletion target lives under `.github/workflows/`.
111
+ *
112
+ * Only this tree is gated. The manifests name 200-odd other paths whose
113
+ * deletion is unchanged by #3656, and narrowing the guard is deliberate: a
114
+ * workflow's absence is undetectable by construction, which is not true of a
115
+ * missing config file or skill directory.
116
+ * @param relativePath - Repo-relative path a deletions manifest wants removed
117
+ * @returns Whether the ownership gate applies to it
118
+ */
119
+ export function isWorkflowDeletionPath(relativePath) {
120
+ return normalizeRepoPath(relativePath).startsWith(`${WORKFLOWS_DIR}/`);
121
+ }
122
+ /**
123
+ * Read the ownership contract a workflow file states about itself.
124
+ *
125
+ * Precedence is seed-before-managed. A `create-only` header mentions
126
+ * `copy-overwrite` in its own second line, so a naive managed-first check would
127
+ * read every seeded workflow as Lisa-managed and delete the very files #3656
128
+ * lost. Ordering it this way means the strongest claim a file can make is the
129
+ * one that keeps it.
130
+ * @param contents - Raw text of the workflow file on disk
131
+ * @returns The verdict its header supports
132
+ */
133
+ export function classifyWorkflowForDeletion(contents) {
134
+ const header = contents.split("\n").slice(0, HEADER_LINES).join("\n");
135
+ if (header.includes(LISA_SEEDED_MARKER)) {
136
+ return { kind: "host-owned-seed" };
137
+ }
138
+ return header.includes(LISA_MANAGED_MARKER)
139
+ ? { kind: "lisa-managed" }
140
+ : { kind: "unattributable" };
141
+ }
142
+ /**
143
+ * The operator-readable reason a workflow deletion was refused.
144
+ *
145
+ * Written for whoever is staring at an install log wondering why a file they
146
+ * expected to disappear is still there — so it names the contract, not the
147
+ * implementation, and says what to do about it.
148
+ * @param verdict - The verdict that refused the deletion
149
+ * @returns A sentence fragment naming why the file was kept
150
+ */
151
+ export function describeRefusal(verdict) {
152
+ return verdict.kind === "host-owned-seed"
153
+ ? "Lisa seeded this workflow as yours and will not remove it"
154
+ : "Lisa cannot prove it installed this workflow";
155
+ }
156
+ //# sourceMappingURL=workflow-deletion-ownership.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflow-deletion-ownership.js","sourceRoot":"","sources":["../../src/core/workflow-deletion-ownership.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8DG;AACH,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC;;;GAGG;AACH,MAAM,aAAa,GAAG,mBAAmB,CAAC;AAE1C;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,iBAAiB,CAAC;AAErD;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,OAAO,CAAC;AAE1C;;;;;;;;;GASG;AACH,MAAM,YAAY,GAAG,CAAC,CAAC;AAWvB;;;;GAIG;AACH,SAAS,iBAAiB,CAAC,KAAa;IACtC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC9C,OAAO,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;AACzD,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,sBAAsB,CAAC,YAAoB;IACzD,OAAO,iBAAiB,CAAC,YAAY,CAAC,CAAC,UAAU,CAAC,GAAG,aAAa,GAAG,CAAC,CAAC;AACzE,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,2BAA2B,CACzC,QAAgB;IAEhB,MAAM,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,YAAY,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtE,IAAI,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,CAAC;QACxC,OAAO,EAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC;IACrC,CAAC;IACD,OAAO,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAC;QACzC,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE;QAC1B,CAAC,CAAC,EAAE,IAAI,EAAE,gBAAgB,EAAE,CAAC;AACjC,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAC7B,OAAmE;IAEnE,OAAO,OAAO,CAAC,IAAI,KAAK,iBAAiB;QACvC,CAAC,CAAC,2DAA2D;QAC7D,CAAC,CAAC,8CAA8C,CAAC;AACrD,CAAC"}
@@ -1687,7 +1687,50 @@ fi
1687
1687
 
1688
1688
  # 13. Dropping or truncating a database / schema / table (Lisa-only guard —
1689
1689
  # upstream has no SQL protection; keep).
1690
- if matches '\b(drop[[:space:]]+(database|schema|table)|truncate[[:space:]]+(table[[:space:]]+)?[[:alnum:]_."`]+)\b'; then
1690
+ #
1691
+ # The two arms used to be asymmetric, and only the lopsided one produced false
1692
+ # positives (issue #3530). The `drop` arm requires a following keyword —
1693
+ # `drop database|schema|table` — so `drop the ball` never matched it. The
1694
+ # `truncate` arm made that keyword OPTIONAL:
1695
+ #
1696
+ # truncate[[:space:]]+(table[[:space:]]+)?[[:alnum:]_."`]+
1697
+ #
1698
+ # which reduces to "`truncate`, whitespace, any identifier-ish word", so
1699
+ # `truncate the board`, `truncate results` and `truncate output` all matched.
1700
+ # A session was refused for a bug-report TITLE describing a connection that
1701
+ # truncates results — and a title has no escape hatch, because the remedy this
1702
+ # hook prints at the wall (write the text to a file, pass it BY PATH) covers a
1703
+ # body, while the title flag of an issue-filing CLI is necessarily inline. The
1704
+ # only exit left was to reword, which is what trains people to route around a
1705
+ # guard.
1706
+ #
1707
+ # The fix keeps the coverage and drops the English, in three arms.
1708
+ #
1709
+ # ARM 1 — `drop`, byte-identical to before. It was already correct, and a
1710
+ # rewrite that unified the arms could only make it permissive.
1711
+ readonly SQL_DROP='\bdrop[[:space:]]+(database|schema|table)\b'
1712
+ # ARM 2 — `truncate` carrying its own evidence that it is a statement rather
1713
+ # than a verb: the TABLE keyword (what the drop arm demands), a
1714
+ # schema-qualified name, a quoted identifier, a trailing `;`, or one of the
1715
+ # tail keywords that only ever follow a TRUNCATE. English has none of these.
1716
+ readonly SQL_TRUNCATE_TAIL='(cascade|restrict|restart[[:space:]]+identity|continue[[:space:]]+identity)([^[:alnum:]_]|$)'
1717
+ readonly SQL_TRUNCATE_EVIDENCE='\btruncate[[:space:]]+(table[[:space:]]+[[:alnum:]_."`]+|(only[[:space:]]+)?([[:alnum:]_]+\.[[:alnum:]_."`]+|["`][[:alnum:]_. ]+["`]|[[:alnum:]_]+[[:space:]]*(;|'"$SQL_TRUNCATE_TAIL"')))'
1718
+ # ARM 3 — the permissive-dialect `TRUNCATE <name>` with none of that evidence.
1719
+ # Deleting this shape is what a lazy fix does: it passes every prose scenario
1720
+ # while silently removing the coverage a regression fixture already asserts
1721
+ # (`mysql -e 'TRUNCATE sessions'`). So it is recognized by CONTEXT instead —
1722
+ # the same command must also invoke a command-line database client. Prose in a
1723
+ # tracker filing or a commit message invokes none, which is exactly the
1724
+ # difference the text scan could not see on its own.
1725
+ readonly SQL_CLIENT='(^|[^[:alnum:]_./-])([[:alnum:]_./-]*/)?(psql|pgcli|mysql|mysqladmin|mysqlsh|mariadb|mycli|sqlite3|sqlcmd|cqlsh|clickhouse-client|cockroach|mongosh|duckdb|usql)([[:space:]]|$)'
1726
+ readonly SQL_TRUNCATE_BARE='\btruncate[[:space:]]+(only[[:space:]]+)?[[:alnum:]_."`]'
1727
+ # Residual, accepted and stated: a bare `TRUNCATE <name>` with no terminator,
1728
+ # no qualification and no recognized client in the same command goes unmatched
1729
+ # — the same trade the `drop` arm has always made for `drop the ball`. No DROP
1730
+ # shape is weakened, and the guard still fails in the safe direction.
1731
+ if matches "$SQL_DROP" \
1732
+ || matches "$SQL_TRUNCATE_EVIDENCE" \
1733
+ || { matches "$SQL_CLIENT" && matches "$SQL_TRUNCATE_BARE"; }; then
1691
1734
  block "destructive SQL (DROP/TRUNCATE) detected"
1692
1735
  fi
1693
1736
 
package/package.json CHANGED
@@ -165,7 +165,7 @@
165
165
  "zod-validation-error": "^4.0.0"
166
166
  },
167
167
  "name": "@codyswann/lisa",
168
- "version": "4.33.6",
168
+ "version": "4.33.9",
169
169
  "description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
170
170
  "main": "dist/index.js",
171
171
  "exports": {
@@ -313,7 +313,7 @@
313
313
  "test": "tests"
314
314
  },
315
315
  "types": "./dist/index.d.ts",
316
- "lisaReleaseCommit": "384b68c7af30a0c6a27ea50978b723b470f6aaaa",
317
- "gitHead": "384b68c7af30a0c6a27ea50978b723b470f6aaaa",
318
- "lisaReleaseTag": "v4.33.6"
316
+ "lisaReleaseCommit": "e27b5463d29df52d8d5ffc9bae219cd970ca0323",
317
+ "gitHead": "e27b5463d29df52d8d5ffc9bae219cd970ca0323",
318
+ "lisaReleaseTag": "v4.33.9"
319
319
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.33.6",
3
+ "version": "4.33.9",
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": "4.33.6",
3
+ "version": "4.33.9",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -652,7 +652,7 @@ result: **27 of 27** open items carrying the claimed role in this repository had
652
652
  a merged pull request — the claimed lane reported 27 things in flight when the
653
653
  real number was one.
654
654
 
655
- Three properties make the command safe to run unconditionally:
655
+ Four properties make the command safe to run unconditionally:
656
656
 
657
657
  1. **The terminal role is RESOLVED, never assumed.** `lifecycleContract` reads
658
658
  `github.labels.build.done` (or the Jira/Linear equivalent), which is
@@ -666,7 +666,22 @@ Three properties make the command safe to run unconditionally:
666
666
  a real one afterwards. Cross-repository references do not count: a downstream
667
667
  consumer's PR mentioning an upstream issue is not evidence the upstream issue
668
668
  shipped.
669
- 3. **It is idempotent**, so re-running after a retry converges rather than
669
+ 3. **It weighs the merged pull request's BASE BRANCH**, the same way step (c)
670
+ above does, and for the same reason. A merged pull request is not evidence
671
+ on its own: the writer resolves `<baseRefName>` through `.lisa.config.json`
672
+ `deploy.branches` and applies only the role that base earned. A merge into
673
+ the production deploy branch is terminal and closes the item; a merge into a
674
+ branch mapping to a lower env records that env's role and leaves the item
675
+ open; a merge into a branch the project does not deploy at all — an
676
+ integration/stack branch — records nothing, closes nothing, and reports the
677
+ base it observed. **Measured**: on a stacked-PR queue the previous writer
678
+ stamped the production terminal role and closed every ticket in the batch at
679
+ the moment its PR landed on the stacking branch, and one of those false
680
+ completions then made push gate 1 refuse a follow-up commit on the same
681
+ ticket, because there was no longer an open work item to bind. When the
682
+ command reports `work-item NOT completed`, that is the correct answer for a
683
+ stacked merge — re-run it after the stack reaches the deploy branch.
684
+ 4. **It is idempotent**, so re-running after a retry converges rather than
670
685
  accumulating.
671
686
 
672
687
  It also removes the claimed role rather than only adding the terminal one.
@@ -539,7 +539,7 @@ gh issue comment <number> --repo <org>/<repo> --body "[claude-build-intake] Buil
539
539
 
540
540
  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.
541
541
 
542
- `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.
542
+ `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. It also decides for itself whether `$DONE` is terminal here: it resolves each merged PR's base branch through `.lisa.config.json` `deploy.branches`, so a merge into an intermediate env branch records that env's role WITHOUT closing the issue, and a merge into a branch the project does not deploy records nothing and says which base it saw. The manual commands above remain available for an intermediate env transition performed without merge evidence.
543
543
 
544
544
  For any non-Success outcome, do NOT transition. The issue sits in `$CLAIMED` (or wherever the lifecycle left it) — humans take it from there.
545
545
 
@@ -190,7 +190,7 @@ Apply gate verdicts in the same order as build intake:
190
190
 
191
191
  1. **Repo-scope gate.** A build item carrying `repo:<other>` and not `repo:<current>` is outside this repo's pickup lane. Return `MISCONFIGURED` when repo scope is absent or contradictory enough that the current repo cannot be determined confidently; otherwise explain the repo-scope mismatch and recommend running intake in the target repo or fixing the `repo:<name>` label. For an unlabeled item whose target repo is obvious from the item body, report the inferred repo signal but stay read-only: execution intake would stamp `repo:<name>`, while intake-explain only says it would do so. A multi-repo leaf is not directly buildable; explain that execution intake would split it per `repo-scope-split`, but this read-only diagnosis does not split.
192
192
  2. **Leaf-only gate.** If the item has open child work from native GitHub sub-issues or body parentage, return `NON_LEAF_CONTAINER` and explain that direct build pickup is leaf-only per `leaf-only-lifecycle`. If it has no open children but carries `type:Epic`, also return `NON_LEAF_CONTAINER` because a childless Epic is a pure rollup container that still needs decomposition or reclassification. (A childless `type:Story` or `type:Spike` is a valid leaf per `leaf-only-lifecycle` and is NOT a `NON_LEAF_CONTAINER` — it is directly buildable.) The next action for a childless Epic is decomposition, moving `status:ready` to leaf children, or correcting the issue type. Execution build intake would move such a stale ready container out of the pickup queue; this read-only diagnosis must not perform that repair.
193
- 3. **Dependency hold gate.** If a single-repo leaf for the current repo has explicit blockers, read each blocker. Closed blockers are clear. Open blockers are clear only when they carry a cleared build status such as `status:code-review`, `status:on-dev`, `status:on-stg`, `status:done`, or the configured done-equivalent labels. Open blockers with `status:ready`, `status:in-progress`, no cleared status label, or inaccessible state are active. Return `HELD_BY_BLOCKERS`, list the active blocker refs, and make the next action blocker resolution rather than `/lisa:intake`.
193
+ 3. **Dependency hold gate.** If a single-repo leaf for the current repo has explicit blockers, read each blocker and clear-check it **by containment, per the `blocker-containment` rule** — cite that slug for the full contract rather than restating it. A blocker is clear only when its merged PRs are ancestors of the branch this leaf will be built from (resolved from the leaf's `## Target Backend Environment` through `.lisa.config.json` `deploy.branches`, never from a rendered `Branch Plan`), or when the rule's narrow no-code carve-out or human override applies. **A build status label is never the test** — `status:code-review`, `status:on-dev`, `status:on-stg` and `status:done` are name matches that hold in a single-trunk workflow and come apart in a promotion workflow, where `main` is the trailing branch. Every other state, and every case where containment is not computable, is active: fail closed. Return `HELD_BY_BLOCKERS`, list the active blocker refs with the rule's reason key for each, and make the next action blocker resolution rather than `/lisa:intake`.
194
194
  4. **Ready leaf.** A build item in the configured ready role, scoped to the current repo, with no open child work and no active blockers returns `ELIGIBLE_FOR_INTAKE`. The `Why:` line should say it is a single-repo leaf for the current repo and that leaf-only, repo-scope, and dependency gates all pass.
195
195
 
196
196
  Relevant `Signals:` should include the decisive context, not every field: for example `repo:lisa; type:Sub-task; no open children`, `open children #12/#13`, `repo:api but current repo is web`, or `active blockers CodySwannGT/lisa#123`.
@@ -1005,28 +1005,40 @@ is no normalized `is blocked by` field. Read the bundle, then extract blockers p
1005
1005
  - **Linear**: inspect the native issue **relations** from Linear MCP `get_issue` and select
1006
1006
  blocker relations.
1007
1007
 
1008
- Then classify each blocker:
1009
-
1010
- - **Closed, or shipped to any environment** → **cleared**. A blocker is cleared at **any**
1011
- env-staged `done` role — not only the production terminal. The `done` role is configured
1012
- per-env as a `{dev, staging, production}` map (`config-resolution`), so `On Dev`, `On Stg`,
1013
- **and** production `Done` all mean the blocker's code is merged and deployed to ≥1 environment.
1014
- A post-build `review` role (e.g. `Code Review`) is likewise cleared — the change exists and is
1015
- in flight. An `is blocked by` link is a **development** dependency: it is satisfied once the
1016
- blocker's code is in trunk; it must NOT wait for the blocker to reach production. (This matches
1017
- the intake-path dependency-hold gate in `lisa-intake-explain`, which already treats
1018
- `code-review` / `on-dev` / `on-stg` / `done` as cleared — repair-intake must not be stricter.
1019
- In an env-staged workflow where `Done` means "in production", a strict production-terminal
1020
- check strands every dependent forever behind a blocker that is already merged and sitting at
1021
- `On Stg` / `On Dev`.)
1022
- - **Open** in a pre-merge role (`ready` / `claimed` / unknown — code not yet in trunk) → **still
1023
- blocking**.
1024
- - **Inaccessible** (deleted, cross-org, permission denied) → **still blocking**, unless the item
1025
- body or a newer human comment explicitly states the dependency is resolved.
1008
+ Then classify each blocker **by containment, per the `blocker-containment` rule** — cite that
1009
+ slug for the full contract; do not restate its resolution steps or its fail-closed table here.
1010
+
1011
+ The one question is whether the blocker's code is on the branch **this item** will be built from:
1012
+ resolve this item's base branch from its `## Target Backend Environment` through
1013
+ `.lisa.config.json` `deploy.branches` (`derived-branch-plan`, `config-resolution` — never by
1014
+ reading a rendered `Branch Plan`), resolve the blocker's merge commit through the vendor access
1015
+ layer, and test ancestry against the remote.
1016
+
1017
+ - **Contained** — every merged PR on the blocker is an ancestor of this item's base branch →
1018
+ **cleared**.
1019
+ - **Not contained**, or containment **not computable** for any reason the rule enumerates (no
1020
+ linked PR, PR not merged, blocker inaccessible, base branch underivable or absent from the
1021
+ remote, ancestry query errored) → **still blocking**, with the rule's reason key in the run
1022
+ record.
1023
+ - **Ships no code** — the blocker is closed as completed, positively declares
1024
+ `Target Backend Environment: None — no runtime behavior change`, and has no merged PR →
1025
+ **cleared** under the rule's carve-out. This is a positive determination, never an absence:
1026
+ "no PR found" is `no-pr` and stays blocking.
1027
+ - **Human override** — an explicit human statement on this item that the dependency is satisfied
1028
+ clears it and outranks a failed containment check. Name it in the run record.
1029
+
1030
+ **A lifecycle status name is never the test.** `done`, `on-stg`, `on-dev` and `code-review` are
1031
+ labels that correlate with containment in a single-trunk workflow and come apart from it in a
1032
+ promotion workflow, where `main` is the trailing branch — a blocker at `On Stg` is on `staging`
1033
+ and absent from `main`, and a blocker in `Code Review` is merged to nothing. Containment gets
1034
+ both the loose and the strict failure right at once: a blocker merged into the branch this item
1035
+ builds from clears **immediately**, with no wait for production, so a dependent is never stranded
1036
+ behind work already merged where it needs it.
1026
1037
 
1027
1038
  Only re-dispatch when **every** parsed blocker is cleared. When in doubt, stay blocked — a
1028
- false-negative (left blocked) is cheap; a false-positive (re-dispatched into a real blocker)
1029
- wastes a build cycle.
1039
+ false-negative (left blocked) costs one cycle and is visible; a false-positive is **invisible**,
1040
+ because the item moves to `ready`, which looks exactly like a repair that worked, and the failure
1041
+ surfaces days later as work begun before its dependency existed.
1030
1042
 
1031
1043
  ### Class B — validation / quality-gate self-block
1032
1044
 
@@ -46,7 +46,7 @@ If relevant code IS found, proceed to Phase 2.
46
46
 
47
47
  From the context bundle, evaluate relationships before analyzing this ticket in isolation:
48
48
 
49
- - **Open blockers (`is blocked by`)**: if any blocker is not `Done` or its linked PR is not merged, raise an ambiguity: "Blocker {KEY} is not shipped — work cannot meaningfully start." This is an automatic `BLOCKED` verdict unless the human confirms the blocker state is acceptable.
49
+ - **Open blockers (`is blocked by`)**: clear-check each blocker **by containment, per the `blocker-containment` rule** — cite that slug for the full contract rather than restating it. A blocker is satisfied when its merged PRs are ancestors of the branch this ticket will be built from (resolved from this ticket's `## Target Backend Environment` through `.lisa.config.json` `deploy.branches`, never from a rendered `Branch Plan`), or when the rule's narrow no-code carve-out or human override applies. **`Done` is not the test** — a state name holds in a single-trunk workflow and comes apart in a promotion workflow, and requiring the production terminal strands every dependent behind work already merged where this ticket needs it. This is the same predicate the `DUPLICATE_ALREADY_FIXED` bullet below already requires ("present on the relevant base branch… never from a name/label match alone"), applied here too. For a blocker that is not contained, or where containment is not computable, raise an ambiguity naming the rule's reason key: "Blocker {KEY} is not on {BRANCH} ({REASON}) — work cannot meaningfully start." That is an automatic `BLOCKED` verdict unless the human confirms the blocker state is acceptable.
50
50
  - **Epic siblings in progress**: if a sibling under the same epic is `In Progress` / `In Review` with a different assignee and overlapping scope, raise it as an edge case in Phase 4 ("Duplicate-work risk with {KEY}").
51
51
  - **`duplicates` / `is duplicated by` links**:
52
52
  - If this ticket is a duplicate of an open canonical ticket whose fix is not yet merged into the base branch, verdict is `BLOCKED` with the recommendation to close as duplicate manually rather than implement.
@@ -1687,7 +1687,50 @@ fi
1687
1687
 
1688
1688
  # 13. Dropping or truncating a database / schema / table (Lisa-only guard —
1689
1689
  # upstream has no SQL protection; keep).
1690
- if matches '\b(drop[[:space:]]+(database|schema|table)|truncate[[:space:]]+(table[[:space:]]+)?[[:alnum:]_."`]+)\b'; then
1690
+ #
1691
+ # The two arms used to be asymmetric, and only the lopsided one produced false
1692
+ # positives (issue #3530). The `drop` arm requires a following keyword —
1693
+ # `drop database|schema|table` — so `drop the ball` never matched it. The
1694
+ # `truncate` arm made that keyword OPTIONAL:
1695
+ #
1696
+ # truncate[[:space:]]+(table[[:space:]]+)?[[:alnum:]_."`]+
1697
+ #
1698
+ # which reduces to "`truncate`, whitespace, any identifier-ish word", so
1699
+ # `truncate the board`, `truncate results` and `truncate output` all matched.
1700
+ # A session was refused for a bug-report TITLE describing a connection that
1701
+ # truncates results — and a title has no escape hatch, because the remedy this
1702
+ # hook prints at the wall (write the text to a file, pass it BY PATH) covers a
1703
+ # body, while the title flag of an issue-filing CLI is necessarily inline. The
1704
+ # only exit left was to reword, which is what trains people to route around a
1705
+ # guard.
1706
+ #
1707
+ # The fix keeps the coverage and drops the English, in three arms.
1708
+ #
1709
+ # ARM 1 — `drop`, byte-identical to before. It was already correct, and a
1710
+ # rewrite that unified the arms could only make it permissive.
1711
+ readonly SQL_DROP='\bdrop[[:space:]]+(database|schema|table)\b'
1712
+ # ARM 2 — `truncate` carrying its own evidence that it is a statement rather
1713
+ # than a verb: the TABLE keyword (what the drop arm demands), a
1714
+ # schema-qualified name, a quoted identifier, a trailing `;`, or one of the
1715
+ # tail keywords that only ever follow a TRUNCATE. English has none of these.
1716
+ readonly SQL_TRUNCATE_TAIL='(cascade|restrict|restart[[:space:]]+identity|continue[[:space:]]+identity)([^[:alnum:]_]|$)'
1717
+ readonly SQL_TRUNCATE_EVIDENCE='\btruncate[[:space:]]+(table[[:space:]]+[[:alnum:]_."`]+|(only[[:space:]]+)?([[:alnum:]_]+\.[[:alnum:]_."`]+|["`][[:alnum:]_. ]+["`]|[[:alnum:]_]+[[:space:]]*(;|'"$SQL_TRUNCATE_TAIL"')))'
1718
+ # ARM 3 — the permissive-dialect `TRUNCATE <name>` with none of that evidence.
1719
+ # Deleting this shape is what a lazy fix does: it passes every prose scenario
1720
+ # while silently removing the coverage a regression fixture already asserts
1721
+ # (`mysql -e 'TRUNCATE sessions'`). So it is recognized by CONTEXT instead —
1722
+ # the same command must also invoke a command-line database client. Prose in a
1723
+ # tracker filing or a commit message invokes none, which is exactly the
1724
+ # difference the text scan could not see on its own.
1725
+ readonly SQL_CLIENT='(^|[^[:alnum:]_./-])([[:alnum:]_./-]*/)?(psql|pgcli|mysql|mysqladmin|mysqlsh|mariadb|mycli|sqlite3|sqlcmd|cqlsh|clickhouse-client|cockroach|mongosh|duckdb|usql)([[:space:]]|$)'
1726
+ readonly SQL_TRUNCATE_BARE='\btruncate[[:space:]]+(only[[:space:]]+)?[[:alnum:]_."`]'
1727
+ # Residual, accepted and stated: a bare `TRUNCATE <name>` with no terminator,
1728
+ # no qualification and no recognized client in the same command goes unmatched
1729
+ # — the same trade the `drop` arm has always made for `drop the ball`. No DROP
1730
+ # shape is weakened, and the guard still fails in the safe direction.
1731
+ if matches "$SQL_DROP" \
1732
+ || matches "$SQL_TRUNCATE_EVIDENCE" \
1733
+ || { matches "$SQL_CLIENT" && matches "$SQL_TRUNCATE_BARE"; }; then
1691
1734
  block "destructive SQL (DROP/TRUNCATE) detected"
1692
1735
  fi
1693
1736
 
@@ -0,0 +1,44 @@
1
+ # Blocker Containment (load-bearing)
2
+
3
+ **An `is blocked by` dependency is cleared by one fact only: the blocker's code is present on the branch the dependent will be built from.** A lifecycle status name — `done`, `on-stg`, `on-dev`, `code-review`, or any configured equivalent — is never that fact. It is a label that correlates with the fact in a single-trunk workflow and comes apart from it in a promotion workflow.
4
+
5
+ **One vendor-neutral contract, cited by** `lisa-repair-intake` (Class A dependency clearing), `lisa-intake-explain` (dependency hold gate), and `lisa-ticket-triage` (Phase 1.5 open-blocker gate) — the `leaf-only-lifecycle` / `repo-scope-split` precedent: one shared slug, never divergent per-consumer prose.
6
+
7
+ ## Why a status name is the wrong test
8
+
9
+ Where promotion runs `dev → staging → main`, `main` is the **trailing** branch. A blocker at `On Stg` is merged to `staging` and absent from `main`, so a dependent that builds from `main` does not have it. A blocker in `Code Review` is merged to nothing at all. The two failures are symmetric and both are live in the tree:
10
+
11
+ - Test too **loosely** (any env-staged `done` clears) and a dependent is dispatched into work whose prerequisite is not on its branch. Nothing goes red; the item moves to `ready`, which is indistinguishable from a repair that worked.
12
+ - Test too **strictly** (only the production terminal clears) and every dependent strands behind a blocker that is already merged where it matters.
13
+
14
+ Containment is the one predicate that gets both right, because it asks the question the status name was standing in for.
15
+
16
+ ## The predicate
17
+
18
+ 1. **Resolve the dependent's base branch.** Take the dependent's `## Target Backend Environment` and map that exact key forward through `.lisa.config.json` `deploy.branches`, per `derived-branch-plan` and `config-resolution`. **Never read the rendered `Branch Plan` section as input** — `derived-branch-plan` forbids it; a surface needing the base branch derives it again.
19
+ 2. **Resolve the blocker's merge commit** — the commit that landed the blocker's PR.
20
+ 3. **Test ancestry on the remote:** is that commit an ancestor of the resolved base branch (`git merge-base --is-ancestor <sha> origin/<base>`)?
21
+ 4. **Cleared** iff the answer is yes. Otherwise **still blocking**.
22
+
23
+ ## Fail closed
24
+
25
+ **Never fail open on an unresolvable ancestry check.** Any step that cannot be positively resolved leaves the dependency **blocking**, and the run record says which step failed:
26
+
27
+ - no linked PR, or a PR that is not merged;
28
+ - the blocker is inaccessible (deleted, cross-org, permission denied) — containment across repositories is not defined;
29
+ - the dependent's environment is not derivable, maps non-uniquely, or maps to a branch absent from the remote;
30
+ - the ancestry query itself fails.
31
+
32
+ A false negative (left blocked) costs a cycle and is visible. A false positive (dispatched into a missing prerequisite) is invisible and surfaces days later as work begun before its dependency existed. Prefer the visible failure.
33
+
34
+ ## The one carve-out: a blocker with no code
35
+
36
+ A dependency need not deliver code. A blocker **closed as completed** that positively declares no code deliverable — `Target Backend Environment: None — no runtime behavior change` (doc-only / config-only / type-only), no linked PR, no merge commit — is **cleared**, because there is no artifact whose location could matter.
37
+
38
+ This requires a **positive determination**, never an absence. "I could not find a PR" is fail-closed under the rule above; "the item declares it ships no code" is this carve-out. Collapsing the two turns the carve-out into a universal bypass.
39
+
40
+ ## Human override
41
+
42
+ A human statement on the item that the dependency is satisfied clears it, and outranks a failed containment check. Record it as the reason in the run record. This is the escape hatch for cross-repo and untracked dependencies, and it is the only one. An automation may never synthesize one on its own behalf.
43
+
44
+ Full contract (resolution steps, per-vendor merge-commit bindings, the fail-closed reason-key table, the no-code carve-out's three conditions, and what this rule deliberately does not do): [reference/blocker-containment.md](../reference/blocker-containment.md).