@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.
- package/README.md +22 -0
- package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +44 -1
- package/all/copy-overwrite/scripts/lisa-work-item.mjs +361 -10
- package/dist/cli/apply.d.ts.map +1 -1
- package/dist/cli/apply.js +8 -1
- package/dist/cli/apply.js.map +1 -1
- package/dist/configs/vitest/base.d.ts +76 -11
- package/dist/configs/vitest/base.d.ts.map +1 -1
- package/dist/configs/vitest/base.js +121 -14
- package/dist/configs/vitest/base.js.map +1 -1
- package/dist/core/apply-receipt.d.ts +18 -0
- package/dist/core/apply-receipt.d.ts.map +1 -1
- package/dist/core/apply-receipt.js +7 -5
- package/dist/core/apply-receipt.js.map +1 -1
- package/dist/core/config.d.ts +34 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js.map +1 -1
- 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/lisa.d.ts +75 -0
- package/dist/core/lisa.d.ts.map +1 -1
- package/dist/core/lisa.js +125 -6
- package/dist/core/lisa.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 +31 -13
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/core/workflow-deletion-ownership.d.ts +69 -0
- package/dist/core/workflow-deletion-ownership.d.ts.map +1 -0
- package/dist/core/workflow-deletion-ownership.js +156 -0
- package/dist/core/workflow-deletion-ownership.js.map +1 -0
- package/dist/opencode/plugin-templates/parity-safety-net.sh +44 -1
- package/package.json +4 -4
- 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-drive-pr-to-merge/SKILL.md +17 -2
- package/plugins/lisa/.codex-plugin/skills/lisa-github-build-intake/SKILL.md +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-intake-explain/SKILL.md +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-repair-intake/SKILL.md +32 -20
- package/plugins/lisa/.codex-plugin/skills/lisa-ticket-triage/SKILL.md +1 -1
- package/plugins/lisa/hooks/parity-safety-net.sh +44 -1
- package/plugins/lisa/rules/eager/blocker-containment.md +44 -0
- package/plugins/lisa/rules/reference/blocker-containment.md +92 -0
- package/plugins/lisa/skills/lisa-drive-pr-to-merge/SKILL.md +17 -2
- package/plugins/lisa/skills/lisa-github-build-intake/SKILL.md +1 -1
- package/plugins/lisa/skills/lisa-intake-explain/SKILL.md +1 -1
- package/plugins/lisa/skills/lisa-repair-intake/SKILL.md +32 -20
- package/plugins/lisa/skills/lisa-ticket-triage/SKILL.md +1 -1
- package/plugins/lisa-agy/hooks/parity-safety-net.sh +44 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-drive-pr-to-merge/SKILL.md +17 -2
- package/plugins/lisa-agy/skills/lisa-github-build-intake/SKILL.md +1 -1
- package/plugins/lisa-agy/skills/lisa-intake-explain/SKILL.md +1 -1
- package/plugins/lisa-agy/skills/lisa-repair-intake/SKILL.md +32 -20
- package/plugins/lisa-agy/skills/lisa-ticket-triage/SKILL.md +1 -1
- 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/hooks/parity-safety-net.sh +44 -1
- package/plugins/lisa-copilot/rules/eager/blocker-containment.md +44 -0
- package/plugins/lisa-copilot/rules/reference/blocker-containment.md +92 -0
- package/plugins/lisa-copilot/skills/lisa-drive-pr-to-merge/SKILL.md +17 -2
- package/plugins/lisa-copilot/skills/lisa-github-build-intake/SKILL.md +1 -1
- package/plugins/lisa-copilot/skills/lisa-intake-explain/SKILL.md +1 -1
- package/plugins/lisa-copilot/skills/lisa-repair-intake/SKILL.md +32 -20
- package/plugins/lisa-copilot/skills/lisa-ticket-triage/SKILL.md +1 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/parity-safety-net.sh +44 -1
- package/plugins/lisa-cursor/rules/blocker-containment-reference.mdc +97 -0
- package/plugins/lisa-cursor/rules/blocker-containment.mdc +49 -0
- package/plugins/lisa-cursor/skills/lisa-drive-pr-to-merge/SKILL.md +17 -2
- package/plugins/lisa-cursor/skills/lisa-github-build-intake/SKILL.md +1 -1
- package/plugins/lisa-cursor/skills/lisa-intake-explain/SKILL.md +1 -1
- package/plugins/lisa-cursor/skills/lisa-repair-intake/SKILL.md +32 -20
- package/plugins/lisa-cursor/skills/lisa-ticket-triage/SKILL.md +1 -1
- 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/hooks/parity-safety-net.sh +44 -1
- package/plugins/src/base/rules/eager/blocker-containment.md +44 -0
- package/plugins/src/base/rules/reference/blocker-containment.md +92 -0
- package/plugins/src/base/skills/lisa-drive-pr-to-merge/SKILL.md +17 -2
- package/plugins/src/base/skills/lisa-github-build-intake/SKILL.md +1 -1
- package/plugins/src/base/skills/lisa-intake-explain/SKILL.md +1 -1
- package/plugins/src/base/skills/lisa-repair-intake/SKILL.md +32 -20
- package/plugins/src/base/skills/lisa-ticket-triage/SKILL.md +1 -1
- package/scripts/two-channel-couplings.json +1 -1
- package/typescript/copy-overwrite/.github/GITHUB_ACTIONS.md +2 -2
- package/typescript/copy-overwrite/scripts/lisa-mutation.mjs +96 -12
- package/typescript/copy-overwrite/scripts/reconcile-nightly-e2e-tracking.mjs +3 -2
- 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
|
-
|
|
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.
|
|
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": "
|
|
317
|
-
"gitHead": "
|
|
318
|
-
"lisaReleaseTag": "v4.33.
|
|
316
|
+
"lisaReleaseCommit": "e27b5463d29df52d8d5ffc9bae219cd970ca0323",
|
|
317
|
+
"gitHead": "e27b5463d29df52d8d5ffc9bae219cd970ca0323",
|
|
318
|
+
"lisaReleaseTag": "v4.33.9"
|
|
319
319
|
}
|
|
@@ -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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
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
|
|
1029
|
-
|
|
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`)**:
|
|
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
|
-
|
|
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).
|