@codyswann/lisa 4.39.0 → 4.40.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/all/copy-overwrite/scripts/lisa-cross-worktree-guard.mjs +333 -0
- package/all/copy-overwrite/scripts/lisa-hooks/block-direct-issue-create.sh +17 -0
- package/all/copy-overwrite/scripts/lisa-hooks/block-managed-file-edits.sh +14 -0
- package/all/copy-overwrite/scripts/lisa-hooks/block-no-verify.sh +28 -0
- package/dist/codex/scripts/block-no-verify.sh +21 -0
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +22 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +19 -13
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/block-managed-file-edits.sh +14 -0
- package/dist/opencode/plugin-templates/block-no-verify.sh +28 -0
- package/npm-package/create-only/.github/workflows/publish-to-npm.yml +48 -19
- 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/hooks/block-direct-issue-create.sh +17 -0
- package/plugins/lisa/hooks/block-managed-file-edits.sh +14 -0
- package/plugins/lisa/hooks/block-no-verify.agy.sh +21 -0
- package/plugins/lisa/hooks/block-no-verify.sh +28 -0
- package/plugins/lisa-agy/hooks/block-direct-issue-create.sh +17 -0
- package/plugins/lisa-agy/hooks/block-managed-file-edits.sh +14 -0
- package/plugins/lisa-agy/hooks/block-no-verify.agy.sh +21 -0
- package/plugins/lisa-agy/plugin.json +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/block-direct-issue-create.sh +17 -0
- package/plugins/lisa-copilot/hooks/block-managed-file-edits.sh +14 -0
- package/plugins/lisa-copilot/hooks/block-no-verify.sh +28 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/block-direct-issue-create.sh +17 -0
- package/plugins/lisa-cursor/hooks/block-managed-file-edits.sh +14 -0
- package/plugins/lisa-cursor/hooks/block-no-verify.sh +28 -0
- 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/block-direct-issue-create.sh +17 -0
- package/plugins/src/base/hooks/block-managed-file-edits.sh +14 -0
- package/plugins/src/base/hooks/block-no-verify.agy.sh +21 -0
- package/plugins/src/base/hooks/block-no-verify.sh +28 -0
- package/rails/copy-contents/scripts/lisa-mutation.sh +8 -1
- package/scripts/two-channel-couplings.json +1 -1
- package/typescript/copy-contents/.husky/pre-commit +40 -0
- package/typescript/copy-overwrite/.prettierignore +1 -0
- package/typescript/copy-overwrite/scripts/lisa-mutation.mjs +20 -14
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// This file is managed by Lisa and IS replaced on each `lisa` run.
|
|
3
|
+
// Do not edit directly — durable changes belong upstream in Lisa.
|
|
4
|
+
/**
|
|
5
|
+
* Refuse a commit that stages a NEW file byte-identical to uncommitted work
|
|
6
|
+
* living in a different linked worktree.
|
|
7
|
+
*
|
|
8
|
+
* WHAT THIS IS FOR. When several agents share one repository through linked
|
|
9
|
+
* worktrees, one agent's uncommitted file can be committed by another — the
|
|
10
|
+
* file is real, the branch is real, the commit succeeds, every gate passes,
|
|
11
|
+
* and the history is quietly wrong about who did what. One confirmed case:
|
|
12
|
+
* a 284-line test file staged in one worktree was committed an hour later
|
|
13
|
+
* from elsewhere, byte-identical, under a subtly different subject line.
|
|
14
|
+
*
|
|
15
|
+
* WHY IT MUST BE A HOOK AND NOT A HABIT. The failure mode is that nobody
|
|
16
|
+
* notices. A check an agent has to remember to run is a check that does not
|
|
17
|
+
* run on the commit where it mattered. This runs on every commit, needs no
|
|
18
|
+
* cooperation, and costs one `stat` per linked worktree per newly added file.
|
|
19
|
+
*
|
|
20
|
+
* WHY CONTENT AND NOT MESSAGE. The observed case had a rewritten subject
|
|
21
|
+
* line, so anything keyed on the message would have missed it. Blob identity
|
|
22
|
+
* cannot be edited away.
|
|
23
|
+
*
|
|
24
|
+
* THE LIMIT, STATED PLAINLY. This needs BOTH halves — the file being staged
|
|
25
|
+
* here and the original still present over there. Once the other worktree is
|
|
26
|
+
* removed, the comparison has nothing to match against and this guard is
|
|
27
|
+
* silent. A clean run is therefore not proof that no work was taken; it is
|
|
28
|
+
* proof that none is takeable *right now*. Deleting a worktree that holds
|
|
29
|
+
* uncommitted work permanently forecloses the only window in which its own
|
|
30
|
+
* case was detectable.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { execFileSync } from "node:child_process";
|
|
34
|
+
import { existsSync, statSync } from "node:fs";
|
|
35
|
+
import { join } from "node:path";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Paths that are ambient session furniture rather than authored work.
|
|
39
|
+
*
|
|
40
|
+
* Matched with `startsWith` rather than a constructed RegExp: a literal prefix
|
|
41
|
+
* needs no pattern, and building one invites escaping that the shipped fixer
|
|
42
|
+
* then rewrites, which makes this file stop being a fixed point of Lisa's own
|
|
43
|
+
* formatter (#2788).
|
|
44
|
+
*/
|
|
45
|
+
const IGNORED_PREFIXES = [".codex/"];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Content below this many bytes is not evidence of anything. A short or empty
|
|
49
|
+
* file collides across worktrees by coincidence — an empty `__init__`, a
|
|
50
|
+
* one-line barrel re-export, a placeholder — and a guard that fires on those
|
|
51
|
+
* teaches people to bypass it, which costs more than the cases it would catch.
|
|
52
|
+
*/
|
|
53
|
+
const MIN_INTERESTING_BYTES = 64;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Header markers that identify a file as GENERATED rather than authored.
|
|
57
|
+
*
|
|
58
|
+
* Two agents running the same deterministic generator against the same source
|
|
59
|
+
* produce byte-identical output, in different worktrees, that is not yet on the
|
|
60
|
+
* integration branch. That is this guard's trigger condition exactly — and it
|
|
61
|
+
* is legitimate concurrent work rather than one agent taking another's.
|
|
62
|
+
*
|
|
63
|
+
* Measured against the real repository with two worktrees and the real
|
|
64
|
+
* generators: for an artifact that already EXISTS on the integration branch the
|
|
65
|
+
* case is excluded twice over — a regenerated file stages as a modification,
|
|
66
|
+
* not an addition, and it is tracked in the other worktree. Disabling either
|
|
67
|
+
* clause alone still accepted; only disabling BOTH made it refuse, which is how
|
|
68
|
+
* that redundancy was established rather than assumed.
|
|
69
|
+
*
|
|
70
|
+
* A generator that emits a file NOT yet on the integration branch has neither
|
|
71
|
+
* protection: it stages as an addition here and sits untracked over there. That
|
|
72
|
+
* case was reproduced and DID refuse, so this marker check is what closes it.
|
|
73
|
+
*
|
|
74
|
+
* Keyed on the file's own declaration rather than on a path list, because the
|
|
75
|
+
* distinction that matters is "identical because both were generated from the
|
|
76
|
+
* same source" versus "identical because it is the same authored file" — and a
|
|
77
|
+
* generated file says so at the top. A path list would need updating every time
|
|
78
|
+
* a generator learns a new output, and would be wrong silently until it was.
|
|
79
|
+
*/
|
|
80
|
+
const GENERATED_MARKERS = Object.freeze([
|
|
81
|
+
"@generated",
|
|
82
|
+
"Generated by",
|
|
83
|
+
"DO NOT EDIT",
|
|
84
|
+
"Do not edit",
|
|
85
|
+
"managed by Lisa",
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* How far into a file the declaration must appear.
|
|
90
|
+
*
|
|
91
|
+
* A generated file announces itself in its header. Scanning the whole file
|
|
92
|
+
* would let the words "do not edit" occurring in ordinary prose — a rule, a
|
|
93
|
+
* skill, a comment quoting a banner — silence the guard for an authored file.
|
|
94
|
+
*/
|
|
95
|
+
const GENERATED_HEADER_BYTES = 2048;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Run a command, returning its trimmed stdout, or `null` when it exits
|
|
99
|
+
* non-zero. Used for probes whose failure is a legitimate answer ("this path
|
|
100
|
+
* is not on main", "this path is not tracked there") rather than an error.
|
|
101
|
+
*/
|
|
102
|
+
function probe(args, cwd) {
|
|
103
|
+
try {
|
|
104
|
+
return execFileSync("git", args, {
|
|
105
|
+
cwd,
|
|
106
|
+
encoding: "utf8",
|
|
107
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
108
|
+
}).trim();
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function must(args, cwd) {
|
|
115
|
+
return execFileSync("git", args, {
|
|
116
|
+
cwd,
|
|
117
|
+
encoding: "utf8",
|
|
118
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
119
|
+
}).trim();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Absolute paths of every registered linked worktree, this one included. */
|
|
123
|
+
function worktrees(repo) {
|
|
124
|
+
const out = probe(["worktree", "list", "--porcelain"], repo) ?? "";
|
|
125
|
+
return out
|
|
126
|
+
.split("\n")
|
|
127
|
+
.filter(line => line.startsWith("worktree "))
|
|
128
|
+
.map(line => line.slice("worktree ".length).trim())
|
|
129
|
+
.filter(Boolean);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Paths staged as ADDED in this commit.
|
|
134
|
+
*
|
|
135
|
+
* Deliberately only `A`. A modified tracked file is frequently identical
|
|
136
|
+
* across worktrees for honest reasons — a regenerated manifest is the common
|
|
137
|
+
* one, and it is deterministic, so two agents regenerating it produce the same
|
|
138
|
+
* bytes while differing from main. Firing on that would make the guard noise
|
|
139
|
+
* on nearly every commit. A brand-new file with identical content in two
|
|
140
|
+
* worktrees has no such innocent explanation.
|
|
141
|
+
*/
|
|
142
|
+
function stagedAdditions(repo) {
|
|
143
|
+
const out = probe(
|
|
144
|
+
["diff", "--cached", "--name-only", "--diff-filter=A", "-z"],
|
|
145
|
+
repo
|
|
146
|
+
);
|
|
147
|
+
if (out === null) return [];
|
|
148
|
+
return out.split("\0").filter(Boolean);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The blob this commit is staging for `path`, or null when unreadable. */
|
|
152
|
+
function stagedBlob(repo, path) {
|
|
153
|
+
return probe(["rev-parse", `:${path}`], repo);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Whether `blob` is simply what the integration branch already holds at
|
|
158
|
+
* `path`.
|
|
159
|
+
*
|
|
160
|
+
* THIS IS THE REJECTION CONTROL, and without it the guard is worthless rather
|
|
161
|
+
* than merely weak. Measured while designing this: a first pass without it
|
|
162
|
+
* fired on four files in one worktree that were byte-identical to `origin/main`
|
|
163
|
+
* — content every worktree carries for the ordinary reason. A guard that
|
|
164
|
+
* reports shared history as contamination fires on every worktree and is
|
|
165
|
+
* switched off within the hour.
|
|
166
|
+
*/
|
|
167
|
+
function isIntegrationContent(repo, defaultRef, path, blob) {
|
|
168
|
+
if (!defaultRef) return false;
|
|
169
|
+
return probe(["rev-parse", `${defaultRef}:${path}`], repo) === blob;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Whether the blob's header declares it generated.
|
|
174
|
+
* @param {string} repo Repository root.
|
|
175
|
+
* @param {string} blob Blob sha to read.
|
|
176
|
+
* @returns {boolean} True when a generated marker appears in the header.
|
|
177
|
+
*/
|
|
178
|
+
function declaresItselfGenerated(repo, blob) {
|
|
179
|
+
const body = probe(["cat-file", "blob", blob], repo);
|
|
180
|
+
if (body === null) return false;
|
|
181
|
+
const header = body.slice(0, GENERATED_HEADER_BYTES);
|
|
182
|
+
return GENERATED_MARKERS.some(marker => header.includes(marker));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The remote-tracking integration ref, or null when it cannot be resolved. */
|
|
186
|
+
function integrationRef(repo) {
|
|
187
|
+
const symref = probe(
|
|
188
|
+
["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"],
|
|
189
|
+
repo
|
|
190
|
+
);
|
|
191
|
+
if (symref) return symref.replace(/^refs\/remotes\//, "");
|
|
192
|
+
for (const candidate of ["origin/main", "origin/master"]) {
|
|
193
|
+
if (probe(["rev-parse", "--verify", "--quiet", candidate], repo))
|
|
194
|
+
return candidate;
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Paths the operator has explicitly cleared, as a comma-separated list.
|
|
201
|
+
*
|
|
202
|
+
* Deliberately path-scoped rather than a boolean. A blanket "off" switch on a
|
|
203
|
+
* guard like this gets set once, in frustration, and never unset — and the
|
|
204
|
+
* next real case passes silently. Naming the file keeps the override as narrow
|
|
205
|
+
* as the situation that justified it.
|
|
206
|
+
*/
|
|
207
|
+
function clearedPaths() {
|
|
208
|
+
const raw = process.env.LISA_ALLOW_CROSS_WORKTREE_BLOB;
|
|
209
|
+
if (!raw) return new Set();
|
|
210
|
+
return new Set(
|
|
211
|
+
raw
|
|
212
|
+
.split(",")
|
|
213
|
+
.map(entry => entry.trim())
|
|
214
|
+
.filter(Boolean)
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function findCollisions(repo, self) {
|
|
219
|
+
const additions = stagedAdditions(repo);
|
|
220
|
+
if (additions.length === 0) return [];
|
|
221
|
+
|
|
222
|
+
const others = worktrees(repo).filter(tree => tree !== self);
|
|
223
|
+
if (others.length === 0) return [];
|
|
224
|
+
|
|
225
|
+
const defaultRef = integrationRef(repo);
|
|
226
|
+
const cleared = clearedPaths();
|
|
227
|
+
const collisions = [];
|
|
228
|
+
|
|
229
|
+
for (const path of additions) {
|
|
230
|
+
if (cleared.has(path)) continue;
|
|
231
|
+
if (IGNORED_PREFIXES.some(prefix => path.startsWith(prefix))) continue;
|
|
232
|
+
|
|
233
|
+
const blob = stagedBlob(repo, path);
|
|
234
|
+
if (!blob) continue;
|
|
235
|
+
|
|
236
|
+
const size = Number(probe(["cat-file", "-s", blob], repo));
|
|
237
|
+
if (!Number.isFinite(size) || size < MIN_INTERESTING_BYTES) continue;
|
|
238
|
+
|
|
239
|
+
if (isIntegrationContent(repo, defaultRef, path, blob)) continue;
|
|
240
|
+
|
|
241
|
+
if (declaresItselfGenerated(repo, blob)) continue;
|
|
242
|
+
|
|
243
|
+
for (const tree of others) {
|
|
244
|
+
const candidate = join(tree, path);
|
|
245
|
+
// The stat is the cheap filter: almost every worktree lacks the path,
|
|
246
|
+
// so almost every pair costs one failed stat and nothing else.
|
|
247
|
+
if (!existsSync(candidate)) continue;
|
|
248
|
+
let stats;
|
|
249
|
+
try {
|
|
250
|
+
stats = statSync(candidate);
|
|
251
|
+
} catch {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (!stats.isFile()) continue;
|
|
255
|
+
|
|
256
|
+
const theirs = probe(["hash-object", candidate], repo);
|
|
257
|
+
if (theirs !== blob) continue;
|
|
258
|
+
|
|
259
|
+
// Tracked over there means it is shared history reaching that worktree
|
|
260
|
+
// through a branch, not that agent's work in progress.
|
|
261
|
+
const tracked = probe(["-C", tree, "ls-files", "--error-unmatch", path]);
|
|
262
|
+
if (tracked !== null) continue;
|
|
263
|
+
|
|
264
|
+
collisions.push({ path, tree });
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return collisions;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function report(collisions) {
|
|
273
|
+
const lines = [
|
|
274
|
+
"",
|
|
275
|
+
"❌ Commit blocked: staged content is also uncommitted in another worktree.",
|
|
276
|
+
"",
|
|
277
|
+
];
|
|
278
|
+
for (const { path, tree } of collisions) {
|
|
279
|
+
lines.push(` ${path}`);
|
|
280
|
+
lines.push(` byte-identical to an uncommitted copy in ${tree}`);
|
|
281
|
+
}
|
|
282
|
+
lines.push("");
|
|
283
|
+
lines.push(" Each file above is being added here while the same bytes sit");
|
|
284
|
+
lines.push(
|
|
285
|
+
" uncommitted in another worktree. Either another agent authored it and"
|
|
286
|
+
);
|
|
287
|
+
lines.push(
|
|
288
|
+
" this commit would take credit for work in progress, or the same work"
|
|
289
|
+
);
|
|
290
|
+
lines.push(" is about to land twice on two branches.");
|
|
291
|
+
lines.push("");
|
|
292
|
+
lines.push(" Before retrying, establish which worktree authored it:");
|
|
293
|
+
lines.push(
|
|
294
|
+
" git log --all --oneline --find-object=$(git rev-parse :<path>)"
|
|
295
|
+
);
|
|
296
|
+
lines.push(
|
|
297
|
+
" Inspect the other branch through shared refs — git log, git show,"
|
|
298
|
+
);
|
|
299
|
+
lines.push(
|
|
300
|
+
" git cat-file all reach it from here. Do NOT enter another agent's"
|
|
301
|
+
);
|
|
302
|
+
lines.push(
|
|
303
|
+
" working tree, and do not delete it: it may hold the only copy."
|
|
304
|
+
);
|
|
305
|
+
lines.push("");
|
|
306
|
+
lines.push(" If this really is your own file, clear it by name:");
|
|
307
|
+
lines.push(
|
|
308
|
+
` LISA_ALLOW_CROSS_WORKTREE_BLOB='${collisions.map(c => c.path).join(",")}' git commit ...`
|
|
309
|
+
);
|
|
310
|
+
lines.push("");
|
|
311
|
+
return lines.join("\n");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function main() {
|
|
315
|
+
let repo;
|
|
316
|
+
let self;
|
|
317
|
+
try {
|
|
318
|
+
repo = must(["rev-parse", "--show-toplevel"]);
|
|
319
|
+
self = repo;
|
|
320
|
+
} catch {
|
|
321
|
+
// Not a git repository, or git is unavailable. A commit is not happening
|
|
322
|
+
// here in any case; there is nothing for this guard to say.
|
|
323
|
+
return 0;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const collisions = findCollisions(repo, self);
|
|
327
|
+
if (collisions.length === 0) return 0;
|
|
328
|
+
|
|
329
|
+
process.stderr.write(report(collisions));
|
|
330
|
+
return 1;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
process.exit(main());
|
|
@@ -1711,6 +1711,23 @@ def executed_operand(argv):
|
|
|
1711
1711
|
# ordinary shell options and do not consume the script path.
|
|
1712
1712
|
if not head.startswith("--") and "c" in head[1:]:
|
|
1713
1713
|
return None
|
|
1714
|
+
# `-n` is noexec: the shell READS and parses the file and then
|
|
1715
|
+
# exits without running a line of it. `bash -n <file>` is the
|
|
1716
|
+
# one shape that puts a path at a command position while
|
|
1717
|
+
# provably executing nothing, so it is the exact seam between
|
|
1718
|
+
# reading and running — and this walk got it wrong in BOTH
|
|
1719
|
+
# directions (CodySwannGT/lisa#3781). A creation-shaped file
|
|
1720
|
+
# earned a false refusal; a file carrying a human-gate marker
|
|
1721
|
+
# earned a false ALLOW, adjudicating as "declared" a command
|
|
1722
|
+
# that filed nothing. The silent direction is the worse one.
|
|
1723
|
+
#
|
|
1724
|
+
# Not a read-only-command exemption: `bash <file>` still runs
|
|
1725
|
+
# the file and is still refused. The distinction is noexec, not
|
|
1726
|
+
# the program.
|
|
1727
|
+
if head == "--noexec" or (
|
|
1728
|
+
not head.startswith("--") and "n" in head[1:]
|
|
1729
|
+
):
|
|
1730
|
+
return None
|
|
1714
1731
|
elif head in NO_SCRIPT_OPTIONS.get(program, set()):
|
|
1715
1732
|
return None
|
|
1716
1733
|
takes_value = head in INTERPRETER_VALUE_OPTIONS.get(program, set())
|
|
@@ -722,6 +722,20 @@ def shell_script_operand(args):
|
|
|
722
722
|
# not a script file.
|
|
723
723
|
if not token.startswith("--") and "c" in token[1:]:
|
|
724
724
|
return None
|
|
725
|
+
# `-n` is noexec: the shell READS and parses the file, then exits
|
|
726
|
+
# without executing a line of it. `bash -n <file>` is the one shape
|
|
727
|
+
# that puts a path at a command position while provably running
|
|
728
|
+
# nothing — the exact seam between reading and running, and this
|
|
729
|
+
# walk got it wrong (CodySwannGT/lisa#3781). Here that meant a
|
|
730
|
+
# syntax check of any script mentioning a managed path was refused
|
|
731
|
+
# as though it were about to write one.
|
|
732
|
+
#
|
|
733
|
+
# `bash <file>` still executes and is still refused. Noexec is the
|
|
734
|
+
# distinction, not the program.
|
|
735
|
+
if token == "--noexec" or (
|
|
736
|
+
not token.startswith("--") and "n" in token[1:]
|
|
737
|
+
):
|
|
738
|
+
return None
|
|
725
739
|
continue
|
|
726
740
|
return token
|
|
727
741
|
return None
|
|
@@ -27,6 +27,13 @@
|
|
|
27
27
|
# `source`, `.`, `bash < nv.sh`, and an interpreter a `find -exec` or
|
|
28
28
|
# `xargs` puts at a command position.
|
|
29
29
|
#
|
|
30
|
+
# NOT vector 11: `bash -n nv.sh`. `-n` is noexec — the shell reads and parses
|
|
31
|
+
# the file, then exits without running a line — so a syntax check is the one
|
|
32
|
+
# shape that puts a path at a command position while executing nothing. It used
|
|
33
|
+
# to be refused, which made `bash -n` unusable on this file, on every husky
|
|
34
|
+
# hook, and on most of this repository (#3781). The distinction is noexec, not
|
|
35
|
+
# the program: `bash nv.sh` still executes and is still refused.
|
|
36
|
+
#
|
|
30
37
|
# The line below is what lets `lisa apply` tell a downstream copy of this guard
|
|
31
38
|
# that is BEHIND from one that is AHEAD. Byte comparison cannot: both look like
|
|
32
39
|
# "differs from mine", and guessing "behind" is how a fork's stronger guard gets
|
|
@@ -1034,6 +1041,10 @@ def resolve_script(token):
|
|
|
1034
1041
|
|
|
1035
1042
|
SHELL_COMMAND_STRING = object()
|
|
1036
1043
|
SHELL_STDIN = object()
|
|
1044
|
+
# `-n` / `--noexec`: the shell READS and parses its operand, then exits without
|
|
1045
|
+
# executing a line of it. Distinct from the two above because nothing runs at
|
|
1046
|
+
# all — there is no payload to scan and no stdin script to wait for.
|
|
1047
|
+
SHELL_NOEXEC = object()
|
|
1037
1048
|
|
|
1038
1049
|
|
|
1039
1050
|
def shell_execution_operand(args):
|
|
@@ -1064,12 +1075,25 @@ def shell_execution_operand(args):
|
|
|
1064
1075
|
index += 1
|
|
1065
1076
|
continue
|
|
1066
1077
|
if token.startswith("--"):
|
|
1078
|
+
if token == "--noexec":
|
|
1079
|
+
return SHELL_NOEXEC
|
|
1067
1080
|
if token not in SHELL_LONG_OPTIONS_NO_VALUE:
|
|
1068
1081
|
opaque_option_seen = True
|
|
1069
1082
|
index += 1
|
|
1070
1083
|
continue
|
|
1071
1084
|
if "c" in token[1:]:
|
|
1072
1085
|
return SHELL_COMMAND_STRING
|
|
1086
|
+
# `-n` is noexec. `bash -n <file>` is the one shape that puts a path at
|
|
1087
|
+
# a command position while provably executing nothing, so it is the
|
|
1088
|
+
# exact seam between reading and running — and reach got it wrong in
|
|
1089
|
+
# BOTH directions (CodySwannGT/lisa#3781). Here it meant a syntax check
|
|
1090
|
+
# of any file merely DISCUSSING the bypass was refused, which is most of
|
|
1091
|
+
# this repository's own guards and every husky hook.
|
|
1092
|
+
#
|
|
1093
|
+
# `bash <file>` still executes and is still refused. Noexec is the
|
|
1094
|
+
# distinction, not the program: this is not a read-only exemption.
|
|
1095
|
+
if "n" in token[1:]:
|
|
1096
|
+
return SHELL_NOEXEC
|
|
1073
1097
|
index += 1
|
|
1074
1098
|
return args[index] if index < len(args) else SHELL_STDIN
|
|
1075
1099
|
|
|
@@ -1114,6 +1138,10 @@ def executed_scripts(tokens):
|
|
|
1114
1138
|
continue
|
|
1115
1139
|
if operand is SHELL_COMMAND_STRING:
|
|
1116
1140
|
continue
|
|
1141
|
+
if operand is SHELL_NOEXEC:
|
|
1142
|
+
# Nothing runs, so there is nothing to follow and nothing
|
|
1143
|
+
# to fail closed about.
|
|
1144
|
+
continue
|
|
1117
1145
|
else:
|
|
1118
1146
|
continue
|
|
1119
1147
|
if operand is None:
|
|
@@ -937,6 +937,10 @@ def resolve_script(token):
|
|
|
937
937
|
|
|
938
938
|
SHELL_COMMAND_STRING = object()
|
|
939
939
|
SHELL_STDIN = object()
|
|
940
|
+
# `-n` / `--noexec`: the shell READS and parses its operand, then exits without
|
|
941
|
+
# executing a line of it. Distinct from the two above because nothing runs at
|
|
942
|
+
# all — there is no payload to scan and no stdin script to wait for.
|
|
943
|
+
SHELL_NOEXEC = object()
|
|
940
944
|
|
|
941
945
|
|
|
942
946
|
def shell_execution_operand(args):
|
|
@@ -967,12 +971,25 @@ def shell_execution_operand(args):
|
|
|
967
971
|
index += 1
|
|
968
972
|
continue
|
|
969
973
|
if token.startswith("--"):
|
|
974
|
+
if token == "--noexec":
|
|
975
|
+
return SHELL_NOEXEC
|
|
970
976
|
if token not in SHELL_LONG_OPTIONS_NO_VALUE:
|
|
971
977
|
opaque_option_seen = True
|
|
972
978
|
index += 1
|
|
973
979
|
continue
|
|
974
980
|
if "c" in token[1:]:
|
|
975
981
|
return SHELL_COMMAND_STRING
|
|
982
|
+
# `-n` is noexec. `bash -n <file>` is the one shape that puts a path at
|
|
983
|
+
# a command position while provably executing nothing, so it is the
|
|
984
|
+
# exact seam between reading and running — and reach got it wrong in
|
|
985
|
+
# BOTH directions (CodySwannGT/lisa#3781). Here it meant a syntax check
|
|
986
|
+
# of any file merely DISCUSSING the bypass was refused, which is most of
|
|
987
|
+
# this repository's own guards and every husky hook.
|
|
988
|
+
#
|
|
989
|
+
# `bash <file>` still executes and is still refused. Noexec is the
|
|
990
|
+
# distinction, not the program: this is not a read-only exemption.
|
|
991
|
+
if "n" in token[1:]:
|
|
992
|
+
return SHELL_NOEXEC
|
|
976
993
|
index += 1
|
|
977
994
|
return args[index] if index < len(args) else SHELL_STDIN
|
|
978
995
|
|
|
@@ -1017,6 +1034,10 @@ def executed_scripts(tokens):
|
|
|
1017
1034
|
continue
|
|
1018
1035
|
if operand is SHELL_COMMAND_STRING:
|
|
1019
1036
|
continue
|
|
1037
|
+
if operand is SHELL_NOEXEC:
|
|
1038
|
+
# Nothing runs, so there is nothing to follow and nothing
|
|
1039
|
+
# to fail closed about.
|
|
1040
|
+
continue
|
|
1020
1041
|
else:
|
|
1021
1042
|
continue
|
|
1022
1043
|
if operand is None:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lisa-owned-hash-ledger.d.ts","sourceRoot":"","sources":["../../src/core/lisa-owned-hash-ledger.ts"],"names":[],"mappings":"AACA,6EAA6E;AAE7E;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,EAAE,QAAQ,CAC3C,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"lisa-owned-hash-ledger.d.ts","sourceRoot":"","sources":["../../src/core/lisa-owned-hash-ledger.ts"],"names":[],"mappings":"AACA,6EAA6E;AAE7E;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,EAAE,QAAQ,CAC3C,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CA4gCjC,CAAC;AAEH;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,+BAA+B,EAAE,QAAQ,CACpD,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CA+6BjC,CAAC"}
|
|
@@ -404,6 +404,12 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
|
|
|
404
404
|
"5d587fd849cef02e9706d084ac2b508f3885da0ba59b76311e3b666f4fbff01e",
|
|
405
405
|
"99422b4912cd660ddeb5ec5ab9fc333e57fa5763e711117a00b06dd2eda0bbb7",
|
|
406
406
|
]),
|
|
407
|
+
"scripts/lisa-cross-worktree-guard.mjs": Object.freeze([
|
|
408
|
+
"3967e4eb189c043e8702ca5cff40c4612ed77027a053e23635eeb367a436cb9d",
|
|
409
|
+
"41b92e1e94a8ee9ad0235359dbd83566a49b17db60da933835057dc77008ab87",
|
|
410
|
+
"d8625e2d339ac512350203e29e70f7b20546bf5248cf952aafd997389feb230f",
|
|
411
|
+
"e0ef99ef1df32f76601a2106a2b68b3c31a126e0b48e8b8ceaba1a7476c32900",
|
|
412
|
+
]),
|
|
407
413
|
"scripts/lisa-destructive-guard.mjs": Object.freeze([
|
|
408
414
|
"4fb431a3c58f443e303d613934d023d9e95ddae5c42710bb7009f7c080c82a64",
|
|
409
415
|
"66ade2c9cc0554c4934ed94a44563fd9d648b37cacfa9a5d31f17ac4f181e5a3",
|
|
@@ -602,6 +608,8 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
|
|
|
602
608
|
"2832037d6c8d7930a111519dee0cca0ed0110deba3988f8e713bb9203de254df",
|
|
603
609
|
"2e030d5a394d3edb48c391db49661fb3d45545c85f88ea03d1ab07ad3787c089",
|
|
604
610
|
"412209ea2329a64737eeffa4ffc9b6f9371f14cf86419fd0626361a99188a3f8",
|
|
611
|
+
"49fb071d5e062200b09babdc56002bc6bd8aceefd8508c49763c05d505f5083e",
|
|
612
|
+
"500b5d5770cf7a6bf5588effe6db462fe0a4646d1de3e8b799115279af1b2662",
|
|
605
613
|
"5b3bd8a3460524457435f51a0994fb12009c97fffb921375bc79db3798acbabf",
|
|
606
614
|
"5c5a9f1c39f99e493a575784f70379eb857c4963da5d400b6ee86f0e69fc5402",
|
|
607
615
|
"60a4f94ec671b2372f7492680ab327d97aff686588ce85791f54e0fbd098b0ca",
|
|
@@ -627,6 +635,7 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
|
|
|
627
635
|
"scripts/lisa-hooks/block-managed-file-edits.sh": Object.freeze([
|
|
628
636
|
"133acf329582c5d4df9deb6a297df13216993c1bfec0b7df0663c69a1e2bb0db",
|
|
629
637
|
"18aebaef5ea9bf6af220dc9beef80a5cfb37282c6a53047783b4cc96d8daa4e3",
|
|
638
|
+
"2bc98db04ded8aa64af5d894f485173fbfad81976cbd0c6938de6910acfbd577",
|
|
630
639
|
"387a3f7f7dfbde9823532435fc1c7dbca687c8f3d1b63d9a435812b4a33e0425",
|
|
631
640
|
"3882bd338e65b9db9e97aa9e70426f0f7a0d191539df51636e1a5e93a2501137",
|
|
632
641
|
"38a52af7c9d5f36e9e68553bf68a7d1ced9e9f9e6cecdf615efc122d42c8b89e",
|
|
@@ -669,6 +678,7 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
|
|
|
669
678
|
"dd1aea1b9fdd0da023bf704b331b350bc14620f4364afa5001c6690fcbc940cc",
|
|
670
679
|
"e3407d63eae16662923d46b75b6a41e7ca7ab206b428ea2cbb0b4fee226f6516",
|
|
671
680
|
"e5070ad1408dcd30f1ec44d8c4b9b914a57a9930c0fc9345bd6f28941b52afe3",
|
|
681
|
+
"e987dd924dbbdc35eddf607a27372bfac6cf743c9ddb888d8a81b4b128c10908",
|
|
672
682
|
"efd20725ffa4a613823084bc00f1ab584353f8d7ed1c11672b0a0193aefe658c",
|
|
673
683
|
]),
|
|
674
684
|
"scripts/lisa-hooks/block-shell-json-parsing.sh": Object.freeze([
|
|
@@ -752,6 +762,7 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
|
|
|
752
762
|
"03dda43b5813b5b6a083c1e0302ce3e9fec13818864ed38dce180cbdaa4e2de2",
|
|
753
763
|
"0cd013a51a81d8eeebced9f2646bdb3fe0a9247fcd74b57c03e95cc2db3fe7de",
|
|
754
764
|
"0f5d9027e95d47745827adcd979e3c2355271066b6b84d661215dc6e895b3ac2",
|
|
765
|
+
"0fe73e162ece6c4a8270b0d5d675d6927a62962e0ae9275239c6acfacd0ef6c8",
|
|
755
766
|
"11bc91eb0ab1f669fc6531201f16747dc8b5bfcebb1650ad8c29038e9ec8f89e",
|
|
756
767
|
"19c77dd5ff81caf3ed92b5263b662bc16f9b43308993c6450cb6611bb037b180",
|
|
757
768
|
"20bc2e58a54d7ef3c99b91393b85aa2f59d42984cbcbdb0a710b2c8d12d46b39",
|
|
@@ -1422,6 +1433,12 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
|
|
|
1422
1433
|
"scripts/lisa-commit-msg-gates.mjs": Object.freeze([
|
|
1423
1434
|
"5d587fd849cef02e9706d084ac2b508f3885da0ba59b76311e3b666f4fbff01e",
|
|
1424
1435
|
]),
|
|
1436
|
+
"scripts/lisa-cross-worktree-guard.mjs": Object.freeze([
|
|
1437
|
+
"3967e4eb189c043e8702ca5cff40c4612ed77027a053e23635eeb367a436cb9d",
|
|
1438
|
+
"41b92e1e94a8ee9ad0235359dbd83566a49b17db60da933835057dc77008ab87",
|
|
1439
|
+
"d8625e2d339ac512350203e29e70f7b20546bf5248cf952aafd997389feb230f",
|
|
1440
|
+
"e0ef99ef1df32f76601a2106a2b68b3c31a126e0b48e8b8ceaba1a7476c32900",
|
|
1441
|
+
]),
|
|
1425
1442
|
"scripts/lisa-destructive-guard.mjs": Object.freeze([
|
|
1426
1443
|
"4fb431a3c58f443e303d613934d023d9e95ddae5c42710bb7009f7c080c82a64",
|
|
1427
1444
|
"66ade2c9cc0554c4934ed94a44563fd9d648b37cacfa9a5d31f17ac4f181e5a3",
|
|
@@ -1576,6 +1593,8 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
|
|
|
1576
1593
|
"2832037d6c8d7930a111519dee0cca0ed0110deba3988f8e713bb9203de254df",
|
|
1577
1594
|
"2e030d5a394d3edb48c391db49661fb3d45545c85f88ea03d1ab07ad3787c089",
|
|
1578
1595
|
"412209ea2329a64737eeffa4ffc9b6f9371f14cf86419fd0626361a99188a3f8",
|
|
1596
|
+
"49fb071d5e062200b09babdc56002bc6bd8aceefd8508c49763c05d505f5083e",
|
|
1597
|
+
"500b5d5770cf7a6bf5588effe6db462fe0a4646d1de3e8b799115279af1b2662",
|
|
1579
1598
|
"5b3bd8a3460524457435f51a0994fb12009c97fffb921375bc79db3798acbabf",
|
|
1580
1599
|
"5c5a9f1c39f99e493a575784f70379eb857c4963da5d400b6ee86f0e69fc5402",
|
|
1581
1600
|
"60a4f94ec671b2372f7492680ab327d97aff686588ce85791f54e0fbd098b0ca",
|
|
@@ -1601,6 +1620,7 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
|
|
|
1601
1620
|
"scripts/lisa-hooks/block-managed-file-edits.sh": Object.freeze([
|
|
1602
1621
|
"133acf329582c5d4df9deb6a297df13216993c1bfec0b7df0663c69a1e2bb0db",
|
|
1603
1622
|
"18aebaef5ea9bf6af220dc9beef80a5cfb37282c6a53047783b4cc96d8daa4e3",
|
|
1623
|
+
"2bc98db04ded8aa64af5d894f485173fbfad81976cbd0c6938de6910acfbd577",
|
|
1604
1624
|
"387a3f7f7dfbde9823532435fc1c7dbca687c8f3d1b63d9a435812b4a33e0425",
|
|
1605
1625
|
"3882bd338e65b9db9e97aa9e70426f0f7a0d191539df51636e1a5e93a2501137",
|
|
1606
1626
|
"38a52af7c9d5f36e9e68553bf68a7d1ced9e9f9e6cecdf615efc122d42c8b89e",
|
|
@@ -1643,6 +1663,7 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
|
|
|
1643
1663
|
"dd1aea1b9fdd0da023bf704b331b350bc14620f4364afa5001c6690fcbc940cc",
|
|
1644
1664
|
"e3407d63eae16662923d46b75b6a41e7ca7ab206b428ea2cbb0b4fee226f6516",
|
|
1645
1665
|
"e5070ad1408dcd30f1ec44d8c4b9b914a57a9930c0fc9345bd6f28941b52afe3",
|
|
1666
|
+
"e987dd924dbbdc35eddf607a27372bfac6cf743c9ddb888d8a81b4b128c10908",
|
|
1646
1667
|
"efd20725ffa4a613823084bc00f1ab584353f8d7ed1c11672b0a0193aefe658c",
|
|
1647
1668
|
]),
|
|
1648
1669
|
"scripts/lisa-hooks/block-shell-json-parsing.sh": Object.freeze([
|
|
@@ -1723,6 +1744,7 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
|
|
|
1723
1744
|
"scripts/lisa-mutation.mjs": Object.freeze([
|
|
1724
1745
|
"0cd013a51a81d8eeebced9f2646bdb3fe0a9247fcd74b57c03e95cc2db3fe7de",
|
|
1725
1746
|
"0f5d9027e95d47745827adcd979e3c2355271066b6b84d661215dc6e895b3ac2",
|
|
1747
|
+
"0fe73e162ece6c4a8270b0d5d675d6927a62962e0ae9275239c6acfacd0ef6c8",
|
|
1726
1748
|
"19c77dd5ff81caf3ed92b5263b662bc16f9b43308993c6450cb6611bb037b180",
|
|
1727
1749
|
"227dfa273802d6a5c03c3bf67136b743de98370127c7417db454f2e7424e98a0",
|
|
1728
1750
|
"48394f771485db4b7863d2307f9ca0750cba4ce3750c615dd3c54fa47178a50e",
|