@codyswann/lisa 4.43.0 → 4.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/all/copy-overwrite/scripts/lisa-hooks/worktree-binding-guard.mjs +263 -1
- package/all/copy-overwrite/scripts/lisa-work-item.mjs +103 -7
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +20 -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 +9 -3
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +7 -6
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/hooks/worktree-binding-guard.mjs +263 -1
- 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/worktree-binding-guard.mjs +263 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/worktree-binding-guard.mjs +263 -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/worktree-binding-guard.mjs +263 -1
- package/scripts/check-generated-artifact-merge-coverage.mjs +301 -0
|
@@ -50,7 +50,13 @@
|
|
|
50
50
|
*/
|
|
51
51
|
|
|
52
52
|
import { execFileSync } from "node:child_process";
|
|
53
|
-
import {
|
|
53
|
+
import {
|
|
54
|
+
mkdirSync,
|
|
55
|
+
readFileSync,
|
|
56
|
+
realpathSync,
|
|
57
|
+
statSync,
|
|
58
|
+
writeFileSync,
|
|
59
|
+
} from "node:fs";
|
|
54
60
|
import { homedir } from "node:os";
|
|
55
61
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
56
62
|
|
|
@@ -151,6 +157,258 @@ function mainCheckout(cwd) {
|
|
|
151
157
|
return null;
|
|
152
158
|
}
|
|
153
159
|
|
|
160
|
+
/**
|
|
161
|
+
* How much of an executed script this guard will read, in bytes.
|
|
162
|
+
*
|
|
163
|
+
* A generated fixture can be megabytes, and a guard that reads all of it on
|
|
164
|
+
* every Bash call is a guard someone switches off.
|
|
165
|
+
*/
|
|
166
|
+
const SCRIPT_READ_LIMIT = 256 * 1024;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Programs that take their operands as DATA and never execute them.
|
|
170
|
+
*
|
|
171
|
+
* The same split `block-direct-issue-create.sh` makes, and for the same reason
|
|
172
|
+
* it had to: a guard that opens every file a command names, then judges the
|
|
173
|
+
* COMMAND by that FILE's contents, refuses ordinary inspection. Reading a
|
|
174
|
+
* script that visits another worktree is not visiting it.
|
|
175
|
+
*
|
|
176
|
+
* The list is an exemption, not an allowlist. Anything unrecognised is
|
|
177
|
+
* FOLLOWED, because an interpreter roster fails open on the runner nobody
|
|
178
|
+
* enumerated.
|
|
179
|
+
*/
|
|
180
|
+
const READ_ONLY_PROGRAMS = new Set([
|
|
181
|
+
"awk",
|
|
182
|
+
"basename",
|
|
183
|
+
"cat",
|
|
184
|
+
"cksum",
|
|
185
|
+
"cmp",
|
|
186
|
+
"comm",
|
|
187
|
+
"cut",
|
|
188
|
+
"diff",
|
|
189
|
+
"dirname",
|
|
190
|
+
"du",
|
|
191
|
+
"file",
|
|
192
|
+
"find",
|
|
193
|
+
"grep",
|
|
194
|
+
"egrep",
|
|
195
|
+
"fgrep",
|
|
196
|
+
"head",
|
|
197
|
+
"less",
|
|
198
|
+
"ls",
|
|
199
|
+
"md5",
|
|
200
|
+
"md5sum",
|
|
201
|
+
"more",
|
|
202
|
+
"nl",
|
|
203
|
+
"od",
|
|
204
|
+
"realpath",
|
|
205
|
+
"rg",
|
|
206
|
+
"sed",
|
|
207
|
+
"sha1sum",
|
|
208
|
+
"sha256sum",
|
|
209
|
+
"sort",
|
|
210
|
+
"stat",
|
|
211
|
+
"strings",
|
|
212
|
+
"tail",
|
|
213
|
+
"tee",
|
|
214
|
+
"tr",
|
|
215
|
+
"uniq",
|
|
216
|
+
"wc",
|
|
217
|
+
"xxd",
|
|
218
|
+
]);
|
|
219
|
+
|
|
220
|
+
/** Interpreters whose first non-option operand is the file they run. */
|
|
221
|
+
const EXECUTING_INTERPRETERS = new Set([
|
|
222
|
+
"bash",
|
|
223
|
+
"dash",
|
|
224
|
+
"ksh",
|
|
225
|
+
"node",
|
|
226
|
+
"perl",
|
|
227
|
+
"python",
|
|
228
|
+
"python3",
|
|
229
|
+
"ruby",
|
|
230
|
+
"sh",
|
|
231
|
+
"zsh",
|
|
232
|
+
"bun",
|
|
233
|
+
"deno",
|
|
234
|
+
"tsx",
|
|
235
|
+
]);
|
|
236
|
+
|
|
237
|
+
/** Shell builtins that run a file in the CURRENT shell. */
|
|
238
|
+
const SOURCE_BUILTINS = new Set([".", "source"]);
|
|
239
|
+
|
|
240
|
+
/** Where one command ends and the next begins. */
|
|
241
|
+
const COMMAND_SEPARATORS = /\|\||&&|[;|&\n]/u;
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Split a command into tokens, honouring simple quoting.
|
|
245
|
+
*
|
|
246
|
+
* Deliberately simple, and the guard fails OPEN when it is not enough — see
|
|
247
|
+
* {@link scriptReachingForeignTree}. A tokeniser that guessed would be worse
|
|
248
|
+
* than one that says it does not know.
|
|
249
|
+
*/
|
|
250
|
+
function tokenise(segment) {
|
|
251
|
+
const tokens = segment.match(/"[^"]*"|'[^']*'|[^\s]+/gu) ?? [];
|
|
252
|
+
return tokens.map(token => token.replace(/^["']|["']$/gu, ""));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* The path this command EXECUTES, or null.
|
|
257
|
+
*
|
|
258
|
+
* The command-position question. A path anywhere else is an argument, and an
|
|
259
|
+
* argument is data.
|
|
260
|
+
*/
|
|
261
|
+
function executedPath(segment) {
|
|
262
|
+
const tokens = tokenise(segment);
|
|
263
|
+
if (tokens.length === 0) return null;
|
|
264
|
+
const [head, ...rest] = tokens;
|
|
265
|
+
const program = head.split("/").pop() ?? "";
|
|
266
|
+
if (READ_ONLY_PROGRAMS.has(program)) return null;
|
|
267
|
+
if (SOURCE_BUILTINS.has(program)) return rest[0] ?? null;
|
|
268
|
+
if (!EXECUTING_INTERPRETERS.has(program)) {
|
|
269
|
+
// A command word that is itself a file runs by its shebang.
|
|
270
|
+
return head.includes("/") ? head : null;
|
|
271
|
+
}
|
|
272
|
+
// A known interpreter: the first operand that is not an option.
|
|
273
|
+
const operand = rest.find(token => !token.startsWith("-"));
|
|
274
|
+
return operand ?? null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* A worktree root other than `bound` that this text reaches into, or null.
|
|
279
|
+
*
|
|
280
|
+
* ## Why this does not enumerate `cd`
|
|
281
|
+
*
|
|
282
|
+
* The redirect spellings — `cd`, `git -C`, `--work-tree`, `--git-dir`, a
|
|
283
|
+
* subshell, `env -C`, a variable holding the path — are an open set, and
|
|
284
|
+
* enumerating them is the failure this arm exists to end
|
|
285
|
+
* (CodySwannGT/lisa#3927 states it for the sibling guard). So the question is
|
|
286
|
+
* inverted the same way the filing guard inverted its own: not *which syntax
|
|
287
|
+
* moves*, which is unbounded, but *which tokens name a directory in another
|
|
288
|
+
* worktree*, which is bounded by the file.
|
|
289
|
+
*
|
|
290
|
+
* A path only has to RESOLVE. How it arrived does not matter, which is what
|
|
291
|
+
* makes an unenumerated spelling reach the same verdict as `cd`.
|
|
292
|
+
*
|
|
293
|
+
* ## Why comments are stripped first
|
|
294
|
+
*
|
|
295
|
+
* A script that merely MENTIONS a sibling worktree — a usage line, a comment,
|
|
296
|
+
* a log message — would otherwise be refused for describing the thing rather
|
|
297
|
+
* than doing it. Prose about a subject is the likeliest text to contain the
|
|
298
|
+
* subject's shapes, and the population that writes it is the people
|
|
299
|
+
* documenting the guard. A parity classifier elsewhere in this repository was
|
|
300
|
+
* fooled by exactly that and was caught only by a known-answer control.
|
|
301
|
+
*/
|
|
302
|
+
function foreignTreeIn(text, bound, cwd) {
|
|
303
|
+
const code = text
|
|
304
|
+
.split("\n")
|
|
305
|
+
.map(line => line.replace(/^\s*#.*$/u, ""))
|
|
306
|
+
.join("\n");
|
|
307
|
+
for (const raw of code.match(/[^\s'";|&()<>]+/gu) ?? []) {
|
|
308
|
+
const token = raw.replace(/^["']|["']$/gu, "");
|
|
309
|
+
if (!token.includes("/")) continue;
|
|
310
|
+
const candidate = isAbsolute(token) ? token : join(cwd, token);
|
|
311
|
+
let root;
|
|
312
|
+
try {
|
|
313
|
+
if (!statSync(candidate).isDirectory()) continue;
|
|
314
|
+
root = worktreeRoot(realpathSync(candidate));
|
|
315
|
+
} catch {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (root && root !== bound) return root;
|
|
319
|
+
}
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* The foreign worktree an executed script reaches, or null.
|
|
325
|
+
*
|
|
326
|
+
* ## Why this arm exists at all
|
|
327
|
+
*
|
|
328
|
+
* The check above compares `payload.cwd` against the bound root, and that is
|
|
329
|
+
* the right comparison — but **`payload.cwd` is sampled before the child
|
|
330
|
+
* process runs**. A script that changes directory internally moves AFTER the
|
|
331
|
+
* guard has measured. The guard is not wrong about the target; it is right
|
|
332
|
+
* about a target that stops being the target one instruction later.
|
|
333
|
+
*
|
|
334
|
+
* A measurement taken before execution cannot bind what execution does
|
|
335
|
+
* (CodySwannGT/lisa#3924). So the only thing left is to read what is about to
|
|
336
|
+
* run.
|
|
337
|
+
*
|
|
338
|
+
* ## What this arm does NOT cover, stated rather than implied
|
|
339
|
+
*
|
|
340
|
+
* An INLINE redirect — `cd B && git status`, `git -C B status` — is not
|
|
341
|
+
* refused here. Those are already refused by the runtime's own worktree
|
|
342
|
+
* isolation, and duplicating a control is how two controls drift into
|
|
343
|
+
* disagreeing. The scripted form is the one neither covers, and it is the only
|
|
344
|
+
* cell this arm fills.
|
|
345
|
+
*
|
|
346
|
+
* ## A relative script path is resolved against `payload.cwd`, and that is a
|
|
347
|
+
* KNOWN limitation shared with `parity-safety-net.sh`
|
|
348
|
+
*
|
|
349
|
+
* CodySwannGT/lisa#3933, filed against that guard, is the same defect: a
|
|
350
|
+
* relative token is resolved against the hook process's working directory, not
|
|
351
|
+
* against the directory the command will have once it runs its own leading
|
|
352
|
+
* `cd`. So `cd <other tree> && bash scripts/foo.sh` reads THIS tree's copy of
|
|
353
|
+
* `scripts/foo.sh` and reports on it with full confidence.
|
|
354
|
+
*
|
|
355
|
+
* It matters here for the same reason it matters there: on a machine running
|
|
356
|
+
* many worktrees of one repository, every tree holds its own copy of every
|
|
357
|
+
* script at the same relative path, and they differ by whatever each lane is
|
|
358
|
+
* doing. A confident scan of the wrong copy is wrong in both directions, and
|
|
359
|
+
* the silent one — the copy that will run reaches out, the copy that was read
|
|
360
|
+
* does not — is the direction this arm exists to prevent.
|
|
361
|
+
*
|
|
362
|
+
* NOT fixed here on purpose. #3933 is assigned and its options (honour a
|
|
363
|
+
* leading `cd`, or fail closed when the effective cwd is uncertain) belong in
|
|
364
|
+
* one place rather than two: a second implementation of the same resolution is
|
|
365
|
+
* how two guards drift into disagreeing about which file a command runs.
|
|
366
|
+
* When that lands, this call site should adopt whatever it produces.
|
|
367
|
+
*
|
|
368
|
+
* The exposure here is narrower than there. This arm is reached only after the
|
|
369
|
+
* session has been confirmed to be in its bound worktree, so the common case
|
|
370
|
+
* is a relative path resolving inside the tree the session is actually in.
|
|
371
|
+
*
|
|
372
|
+
* Fails OPEN and says so, like every other undecidable case in this file.
|
|
373
|
+
*/
|
|
374
|
+
function scriptReachingForeignTree(payload, bound) {
|
|
375
|
+
const command = payload.tool_input?.command;
|
|
376
|
+
if (typeof command !== "string" || !command) return null;
|
|
377
|
+
for (const segment of command.split(COMMAND_SEPARATORS)) {
|
|
378
|
+
const named = executedPath(segment);
|
|
379
|
+
if (!named) continue;
|
|
380
|
+
const path = isAbsolute(named) ? named : join(payload.cwd, named);
|
|
381
|
+
let text;
|
|
382
|
+
try {
|
|
383
|
+
if (statSync(path).size > SCRIPT_READ_LIMIT) {
|
|
384
|
+
say(`${path} is too large to inspect; NOT enforcing on it`);
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
text = readFileSync(path, "utf8");
|
|
388
|
+
} catch {
|
|
389
|
+
// Not a readable file: nothing was executed that this can read.
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
const foreign = foreignTreeIn(text, bound, payload.cwd);
|
|
393
|
+
if (foreign) return { script: path, foreign };
|
|
394
|
+
}
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function reachesOut(script, foreign, bound) {
|
|
399
|
+
return refuse([
|
|
400
|
+
"worktree-binding-guard: this command runs a script that reaches into",
|
|
401
|
+
"another worktree.",
|
|
402
|
+
` script: ${script}`,
|
|
403
|
+
` reaches: ${foreign}`,
|
|
404
|
+
` bound to: ${bound}`,
|
|
405
|
+
"The directory change lives inside the file, so it happens after this",
|
|
406
|
+
"guard has measured where the session is. Run the work from the tree it",
|
|
407
|
+
"belongs to, or acknowledge the move:",
|
|
408
|
+
acceptanceLine(foreign),
|
|
409
|
+
]);
|
|
410
|
+
}
|
|
411
|
+
|
|
154
412
|
function stateFile(sessionId) {
|
|
155
413
|
const home = process.env.LISA_STATE_HOME || join(homedir(), ".lisa");
|
|
156
414
|
return join(home, "worktree-binding", `${sessionId}.json`);
|
|
@@ -331,6 +589,10 @@ function evaluate(payload) {
|
|
|
331
589
|
return 0;
|
|
332
590
|
}
|
|
333
591
|
if (state.boundRoot !== observed) return displaced(state.boundRoot, observed);
|
|
592
|
+
// Last, and only once the session is provably where it should be: a script
|
|
593
|
+
// that moves somewhere else after this check has already run.
|
|
594
|
+
const reach = scriptReachingForeignTree(payload, observed);
|
|
595
|
+
if (reach) return reachesOut(reach.script, reach.foreign, observed);
|
|
334
596
|
return 0;
|
|
335
597
|
}
|
|
336
598
|
|
|
@@ -2460,8 +2460,11 @@ function remoteDefaultRef(remote) {
|
|
|
2460
2460
|
* the rule is unchanged and now applied to the unit it was always about.
|
|
2461
2461
|
* @param {string} input The pre-push stdin stream.
|
|
2462
2462
|
* @param {string} remote Remote being pushed to.
|
|
2463
|
-
* @returns {{localRef: string|undefined, commits: string[]}[]}
|
|
2464
|
-
* ref update that introduces commits.
|
|
2463
|
+
* @returns {{localRef: string|undefined, commits: string[], scope: string[]}[]}
|
|
2464
|
+
* One entry per ref update that introduces commits. `scope` is the rev-list
|
|
2465
|
+
* argument vector the commit list came from, kept so a later question about
|
|
2466
|
+
* the same range — see {@link ancestryUnreachable} — is asked of the range
|
|
2467
|
+
* itself rather than re-derived from a list that has already lost its bounds.
|
|
2465
2468
|
*/
|
|
2466
2469
|
function parsePushGroups(input, remote) {
|
|
2467
2470
|
const groups = [];
|
|
@@ -2488,14 +2491,15 @@ function parsePushGroups(input, remote) {
|
|
|
2488
2491
|
groups.push({
|
|
2489
2492
|
localRef,
|
|
2490
2493
|
commits: git(args).split("\n").filter(Boolean),
|
|
2494
|
+
scope: args,
|
|
2491
2495
|
});
|
|
2492
2496
|
}
|
|
2493
2497
|
if (groups.length === 0 && input.trim() === "") {
|
|
2498
|
+
const args = ["rev-list", "HEAD", "--not", `--remotes=${remote}`];
|
|
2494
2499
|
groups.push({
|
|
2495
2500
|
localRef: undefined,
|
|
2496
|
-
commits: git(
|
|
2497
|
-
|
|
2498
|
-
.filter(Boolean),
|
|
2501
|
+
commits: git(args).split("\n").filter(Boolean),
|
|
2502
|
+
scope: args,
|
|
2499
2503
|
});
|
|
2500
2504
|
}
|
|
2501
2505
|
return groups;
|
|
@@ -4236,14 +4240,100 @@ function validateCommit(args) {
|
|
|
4236
4240
|
* that answer into silence.
|
|
4237
4241
|
* @param {string} input The pre-push stdin stream.
|
|
4238
4242
|
* @param {string} remote Remote being pushed to.
|
|
4239
|
-
* @returns {{localRef: string|undefined, commits: string[]}[]}
|
|
4243
|
+
* @returns {{localRef: string|undefined, commits: string[], scope: string[]}[]}
|
|
4244
|
+
* Groups to check.
|
|
4240
4245
|
*/
|
|
4241
4246
|
function pushGroups(input, remote) {
|
|
4242
4247
|
const parsed = parsePushGroups(input, remote);
|
|
4243
4248
|
const carrying = parsed.filter(group => group.commits.length > 0);
|
|
4244
4249
|
return carrying.length > 0
|
|
4245
4250
|
? carrying
|
|
4246
|
-
: [{ localRef: parsed[0]?.localRef, commits: [] }];
|
|
4251
|
+
: [{ localRef: parsed[0]?.localRef, commits: [], scope: [] }];
|
|
4252
|
+
}
|
|
4253
|
+
|
|
4254
|
+
/**
|
|
4255
|
+
* Whether this range reaches back to the beginning of history.
|
|
4256
|
+
*
|
|
4257
|
+
* THE QUESTION THIS ANSWERS IS "IS MY SCOPE STILL VALID", not "is this commit
|
|
4258
|
+
* traceable", and that is the whole of CodySwannGT/lisa#3719. The new-branch
|
|
4259
|
+
* lane bounds itself with `--not --remotes=<remote>`, which is exact while
|
|
4260
|
+
* reachability holds and silently unbounded the moment it does not. A history
|
|
4261
|
+
* rewrite gives every commit a new object id, so a branch created before one
|
|
4262
|
+
* shares NOTHING with the rewritten remote: the exclusion set removes nothing,
|
|
4263
|
+
* the entire history falls into the range, and the gate then refuses on a
|
|
4264
|
+
* stranger's years-old commit whose work item closed long ago. It computes a
|
|
4265
|
+
* correct answer to the wrong question, and nothing in the refusal says so.
|
|
4266
|
+
*
|
|
4267
|
+
* A ROOT COMMIT IN RANGE IS THE DISCRIMINATOR, and it is deliberately not a
|
|
4268
|
+
* size heuristic. A range that legitimately belongs to one branch begins at a
|
|
4269
|
+
* base the remote can still reach, so the walk stops there — reaching a
|
|
4270
|
+
* parentless commit means it did not stop anywhere, which is the unbounded case
|
|
4271
|
+
* itself rather than a proxy for it. A long branch stays a long branch.
|
|
4272
|
+
*
|
|
4273
|
+
* `--max-parents=0` re-asks the range's OWN rev-list, so the answer costs one
|
|
4274
|
+
* git call whatever the history's size. Filtering the returned commit list
|
|
4275
|
+
* instead would spawn one child per commit — thousands of them in exactly the
|
|
4276
|
+
* case this detects, inside a push gate.
|
|
4277
|
+
* @param {{commits: string[], scope: string[]}} group One pushed ref's range.
|
|
4278
|
+
* @param {string} remote Remote being pushed to.
|
|
4279
|
+
* @returns {boolean} True when the range runs to a parentless commit.
|
|
4280
|
+
*/
|
|
4281
|
+
export function ancestryUnreachable(group, remote) {
|
|
4282
|
+
if (group.commits.length === 0 || group.scope.length === 0) return false;
|
|
4283
|
+
// A repository's FIRST push carries its root commit legitimately: nothing is
|
|
4284
|
+
// published yet, so the exclusion set is empty because there is nothing to
|
|
4285
|
+
// exclude, not because history moved. Reading that as a rewrite would make
|
|
4286
|
+
// every new repository unpushable — the same defect, inverted.
|
|
4287
|
+
// `--format` is presentation only, and deliberately not asserted on: this
|
|
4288
|
+
// reads the ANSWER's emptiness, not its shape, so emptying the format still
|
|
4289
|
+
// prints for-each-ref's default line for each ref and still prints nothing
|
|
4290
|
+
// for none. A hand-run mutant emptying it survives, and is equivalent rather
|
|
4291
|
+
// than a test gap — the ref pattern beside it is the argument that decides
|
|
4292
|
+
// the answer, and a mutant emptying THAT is killed.
|
|
4293
|
+
const published = git([
|
|
4294
|
+
"for-each-ref",
|
|
4295
|
+
"--count=1",
|
|
4296
|
+
"--format=%(refname)",
|
|
4297
|
+
`refs/remotes/${remote}/`,
|
|
4298
|
+
]);
|
|
4299
|
+
if (published === "") return false;
|
|
4300
|
+
return git([...group.scope, "--max-parents=0"]) !== "";
|
|
4301
|
+
}
|
|
4302
|
+
|
|
4303
|
+
/**
|
|
4304
|
+
* The refusal for a range whose base the remote can no longer reach.
|
|
4305
|
+
*
|
|
4306
|
+
* Deliberately says NOTHING about work items. The defect being repaired is that
|
|
4307
|
+
* this push was refused over a stranger's closed ticket, so a message that
|
|
4308
|
+
* still argues about tickets — even the right ones — reproduces the
|
|
4309
|
+
* misdirection at a different address. `selfExplanatory` suppresses the
|
|
4310
|
+
* five-gate checklist for the same reason it is suppressed on the
|
|
4311
|
+
* push-destination refusal: no gate here is cleared by naming a ticket.
|
|
4312
|
+
* @param {string|undefined} localRef The ref being pushed.
|
|
4313
|
+
* @returns {TrackingError} The refusal to raise.
|
|
4314
|
+
*/
|
|
4315
|
+
export function unreachableAncestryRefusal(localRef) {
|
|
4316
|
+
const branch = localRef
|
|
4317
|
+
? pushedBranchName(localRef) || localRef
|
|
4318
|
+
: "this branch";
|
|
4319
|
+
const error = new TrackingError(
|
|
4320
|
+
`The commits on ${branch} are no longer reachable from "origin", so this ` +
|
|
4321
|
+
`push cannot tell which of them the branch actually adds.\n\n` +
|
|
4322
|
+
`WHAT HAPPENED: the shared history was rewritten after this branch was ` +
|
|
4323
|
+
`created. Rewriting gives every commit a new identity, so nothing this ` +
|
|
4324
|
+
`branch was built on still exists on the remote — and the check that ` +
|
|
4325
|
+
`normally asks "what is new here?" gets the whole history back instead.\n\n` +
|
|
4326
|
+
`WHY IT IS NOT REFUSING YOUR WORK: the commits it would otherwise ` +
|
|
4327
|
+
`complain about are other people's, from before the rewrite. Nothing is ` +
|
|
4328
|
+
`wrong with what you wrote.\n\n` +
|
|
4329
|
+
`WHAT TO DO: copy your commits onto the current branch instead of ` +
|
|
4330
|
+
`pushing this one as it stands — \`git cherry-pick\` each of your own ` +
|
|
4331
|
+
`commits onto a fresh branch taken from an up-to-date "main", then push ` +
|
|
4332
|
+
`that. Merging "main" into this branch does NOT help: a merge adds a ` +
|
|
4333
|
+
`parent, it cannot make the old commits exist on the remote again.`
|
|
4334
|
+
);
|
|
4335
|
+
error.selfExplanatory = true;
|
|
4336
|
+
return error;
|
|
4247
4337
|
}
|
|
4248
4338
|
|
|
4249
4339
|
/**
|
|
@@ -4392,6 +4482,12 @@ function validatePush(args) {
|
|
|
4392
4482
|
const pr = currentPullRequest();
|
|
4393
4483
|
const target = prTargetGroup(groups, pr);
|
|
4394
4484
|
for (const group of groups) {
|
|
4485
|
+
// BEFORE the commits are judged, not after: every verdict below is computed
|
|
4486
|
+
// over a range, and a range this wide is not evidence about anything. The
|
|
4487
|
+
// order is the fix — judging first and diagnosing afterwards is what
|
|
4488
|
+
// produced a refusal naming a stranger's closed ticket (#3719).
|
|
4489
|
+
if (ancestryUnreachable(group, remote))
|
|
4490
|
+
throw unreachableAncestryRefusal(group.localRef);
|
|
4395
4491
|
const outcome = commitOutcome(group.commits, configRef, remote);
|
|
4396
4492
|
reportPushGroup(
|
|
4397
4493
|
outcome,
|
|
@@ -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,CA+jCjC,CAAC;AAEH;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,+BAA+B,EAAE,QAAQ,CACpD,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAk+BjC,CAAC"}
|
|
@@ -781,6 +781,10 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
|
|
|
781
781
|
"f21cdcb54b353714a92c2d1e25fd6b76318e8e8d7cb90cdb016f174923c532ef",
|
|
782
782
|
]),
|
|
783
783
|
"scripts/lisa-hooks/worktree-binding-guard.mjs": Object.freeze([
|
|
784
|
+
"248939a111ad7d03d19ae210e5d76cb13613815719610ef22df9a5199f0631e8",
|
|
785
|
+
"2d094120f2c1748144f1c0e5757a60ecc7c6ae3e4f57fa54ab6a15c5a9e1868f",
|
|
786
|
+
"5fe074e29a332c8c4c83a1bdf3bdc766675203178810e0977e7d270ef680636a",
|
|
787
|
+
"6b0c13bf89dd62577baebb7e6e8ff7a0bbcb1e83b32c5a1b878a64c76d83d822",
|
|
784
788
|
"822e4f9d2c3ad8fb184ff7fc880f6811004852fb731c7878b5a8a1a9532bceb8",
|
|
785
789
|
"bc1118b05eaea3fc7b3409c96979d64e3dee0907118f3e84d735163f473f632a",
|
|
786
790
|
"e121219bbee1960a6bf88d1e2ba0b50587d63013e7872c56619c04182e8a7561",
|
|
@@ -921,6 +925,7 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
|
|
|
921
925
|
"scripts/lisa-work-item.mjs": Object.freeze([
|
|
922
926
|
"04401ae05bf9464e6929180ffdfff80afc528a1df20733b032a0342252ca1d4a",
|
|
923
927
|
"04f78df45465d01601c8b5711b74749b2a2c2b70b9c916d6790bd69e1c11865d",
|
|
928
|
+
"06f82a176755c7ff464e263c8a0a6249e02bed710504acf1129ff8fc4c2c0326",
|
|
924
929
|
"08ec9a37d66fb35443a5aac72e09d7ab5478cbd516f72e248d01b3befabddfe5",
|
|
925
930
|
"0b6eff0a306ccd8396d6e3d71ac9c54d3a1c8c29449a9146e0c2e5726b74feb9",
|
|
926
931
|
"1365fd1272eac2d046bbc54d50b0ebdce5b1d9863c832d93c9d4671c3fa13e3c",
|
|
@@ -937,10 +942,13 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
|
|
|
937
942
|
"3a9ec1ff863693a0fa0fef0ade6fa5534485d0249fb78302d37cdac3781348f1",
|
|
938
943
|
"3f8c59e0f4ad43211f6964b4889f3fc673bee0496ec3613bdbdc4544424340f6",
|
|
939
944
|
"41b967f736d7fcf9d8928ad7c8cc767c5beb9544e2fbfa3317c65734aa02fcfd",
|
|
945
|
+
"42176fa5ee6df11d2337a91b2eeeb31a9ed7cc1e16acb0cec11739ba5a6849ed",
|
|
940
946
|
"4372872a448d3b943dea5541167ed5be6155703b57b9f8224d19ddefd3b80898",
|
|
941
947
|
"475cb9ef86c2ac54c8fd8cb6d63e9a55ea8cd0017e26efbe3c44391a4aeeab41",
|
|
942
948
|
"4c263fef48c03b3b6c623ea8a6cd55505bfaf28197e5072226ea35f9f3d26885",
|
|
943
949
|
"4da6624ff233f5ea781c5cb1f2e13d0062fd071e66c4b866fcefb89a6e6247d4",
|
|
950
|
+
"50e3a04a3e5c961b7f70d1a23cab1a6b560ec463a4b76727fe64e6e773d42b0e",
|
|
951
|
+
"56d3718b1243506965b5898c3eb81cb3ff3c6233a2a5185be80fb099933a5a26",
|
|
944
952
|
"5a0b82045d9795284a109c28e5e9dd97a5488ff540bb7034b84077d1447ffd7c",
|
|
945
953
|
"5ba15f6126e253ce9554579ddde86fc0bd744cd0c5c1395cb4e4b8f0472cc134",
|
|
946
954
|
"5bc1466a79c18e565e62305d4adeca0c7de94a1e5a22b2c2a75c84f634fcec86",
|
|
@@ -958,6 +966,7 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
|
|
|
958
966
|
"7573b5170fcba45ccd6123cd0712baba8faa52f063b639140be829d9ce87327d",
|
|
959
967
|
"7ab29cc836e026e4f71a65df9cf5340b7830fb45eec191354ef227d60f8230db",
|
|
960
968
|
"83e7f1257bfee293bcdcdafef4272f40fcdca2d9e43ceb9df647c13c8277423d",
|
|
969
|
+
"89a9c4c2f93ed1e318e61f15f66b89e924c624eaf3fbaae8eeb4a94ca7158be5",
|
|
961
970
|
"8c3effe804dfbea75011271f9d96eeb125fcbb97dc6397cc2c2dffd935f3aef0",
|
|
962
971
|
"8cd8d102d4b3276f92bb4706274d280c302269fb2f564ca93b1d79cb3ff26841",
|
|
963
972
|
"9223856be4618fb1e3e731b259ae5bf5328decc322f0b4a0d0fd18d6b12cd45a",
|
|
@@ -965,6 +974,7 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
|
|
|
965
974
|
"96a87c131606b3edd43e52485e68f8ffbf5c528cb4beb7a1185263519a9632ce",
|
|
966
975
|
"9a470f2590772e144b6ed08512e51244fa1293dfd08d5c42daaa38fed22bdafb",
|
|
967
976
|
"9b3dda4a697bc23d8e71a0f00cf6204c80f5670724b843764af4fe8ecd9b2df2",
|
|
977
|
+
"9c4f6cd0aec2598d1563fa56d87fd05aceaf28752cb811a280e30d369119885e",
|
|
968
978
|
"9e1db1f28cc7ee4b66b08c2fd2ee0ebf98054786d5902245941641e98783bf47",
|
|
969
979
|
"9e5db767677bc9fb07a97109e9036d1ffeda6018c6e678f5c28c55a6c516c234",
|
|
970
980
|
"9fc4267020b6341f74dabebb17123206293280b58cf114932f05a8234426445c",
|
|
@@ -1805,6 +1815,10 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
|
|
|
1805
1815
|
"f21cdcb54b353714a92c2d1e25fd6b76318e8e8d7cb90cdb016f174923c532ef",
|
|
1806
1816
|
]),
|
|
1807
1817
|
"scripts/lisa-hooks/worktree-binding-guard.mjs": Object.freeze([
|
|
1818
|
+
"248939a111ad7d03d19ae210e5d76cb13613815719610ef22df9a5199f0631e8",
|
|
1819
|
+
"2d094120f2c1748144f1c0e5757a60ecc7c6ae3e4f57fa54ab6a15c5a9e1868f",
|
|
1820
|
+
"5fe074e29a332c8c4c83a1bdf3bdc766675203178810e0977e7d270ef680636a",
|
|
1821
|
+
"6b0c13bf89dd62577baebb7e6e8ff7a0bbcb1e83b32c5a1b878a64c76d83d822",
|
|
1808
1822
|
"822e4f9d2c3ad8fb184ff7fc880f6811004852fb731c7878b5a8a1a9532bceb8",
|
|
1809
1823
|
"bc1118b05eaea3fc7b3409c96979d64e3dee0907118f3e84d735163f473f632a",
|
|
1810
1824
|
"e121219bbee1960a6bf88d1e2ba0b50587d63013e7872c56619c04182e8a7561",
|
|
@@ -1924,6 +1938,7 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
|
|
|
1924
1938
|
]),
|
|
1925
1939
|
"scripts/lisa-work-item.mjs": Object.freeze([
|
|
1926
1940
|
"04401ae05bf9464e6929180ffdfff80afc528a1df20733b032a0342252ca1d4a",
|
|
1941
|
+
"06f82a176755c7ff464e263c8a0a6249e02bed710504acf1129ff8fc4c2c0326",
|
|
1927
1942
|
"08ec9a37d66fb35443a5aac72e09d7ab5478cbd516f72e248d01b3befabddfe5",
|
|
1928
1943
|
"0b6eff0a306ccd8396d6e3d71ac9c54d3a1c8c29449a9146e0c2e5726b74feb9",
|
|
1929
1944
|
"1365fd1272eac2d046bbc54d50b0ebdce5b1d9863c832d93c9d4671c3fa13e3c",
|
|
@@ -1940,10 +1955,13 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
|
|
|
1940
1955
|
"3a9ec1ff863693a0fa0fef0ade6fa5534485d0249fb78302d37cdac3781348f1",
|
|
1941
1956
|
"3f8c59e0f4ad43211f6964b4889f3fc673bee0496ec3613bdbdc4544424340f6",
|
|
1942
1957
|
"41b967f736d7fcf9d8928ad7c8cc767c5beb9544e2fbfa3317c65734aa02fcfd",
|
|
1958
|
+
"42176fa5ee6df11d2337a91b2eeeb31a9ed7cc1e16acb0cec11739ba5a6849ed",
|
|
1943
1959
|
"4372872a448d3b943dea5541167ed5be6155703b57b9f8224d19ddefd3b80898",
|
|
1944
1960
|
"475cb9ef86c2ac54c8fd8cb6d63e9a55ea8cd0017e26efbe3c44391a4aeeab41",
|
|
1945
1961
|
"4c263fef48c03b3b6c623ea8a6cd55505bfaf28197e5072226ea35f9f3d26885",
|
|
1946
1962
|
"4da6624ff233f5ea781c5cb1f2e13d0062fd071e66c4b866fcefb89a6e6247d4",
|
|
1963
|
+
"50e3a04a3e5c961b7f70d1a23cab1a6b560ec463a4b76727fe64e6e773d42b0e",
|
|
1964
|
+
"56d3718b1243506965b5898c3eb81cb3ff3c6233a2a5185be80fb099933a5a26",
|
|
1947
1965
|
"5a0b82045d9795284a109c28e5e9dd97a5488ff540bb7034b84077d1447ffd7c",
|
|
1948
1966
|
"5ba15f6126e253ce9554579ddde86fc0bd744cd0c5c1395cb4e4b8f0472cc134",
|
|
1949
1967
|
"5bc1466a79c18e565e62305d4adeca0c7de94a1e5a22b2c2a75c84f634fcec86",
|
|
@@ -1960,6 +1978,7 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
|
|
|
1960
1978
|
"7573b5170fcba45ccd6123cd0712baba8faa52f063b639140be829d9ce87327d",
|
|
1961
1979
|
"7ab29cc836e026e4f71a65df9cf5340b7830fb45eec191354ef227d60f8230db",
|
|
1962
1980
|
"83e7f1257bfee293bcdcdafef4272f40fcdca2d9e43ceb9df647c13c8277423d",
|
|
1981
|
+
"89a9c4c2f93ed1e318e61f15f66b89e924c624eaf3fbaae8eeb4a94ca7158be5",
|
|
1963
1982
|
"8c3effe804dfbea75011271f9d96eeb125fcbb97dc6397cc2c2dffd935f3aef0",
|
|
1964
1983
|
"8cd8d102d4b3276f92bb4706274d280c302269fb2f564ca93b1d79cb3ff26841",
|
|
1965
1984
|
"9223856be4618fb1e3e731b259ae5bf5328decc322f0b4a0d0fd18d6b12cd45a",
|
|
@@ -1967,6 +1986,7 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
|
|
|
1967
1986
|
"96a87c131606b3edd43e52485e68f8ffbf5c528cb4beb7a1185263519a9632ce",
|
|
1968
1987
|
"9a470f2590772e144b6ed08512e51244fa1293dfd08d5c42daaa38fed22bdafb",
|
|
1969
1988
|
"9b3dda4a697bc23d8e71a0f00cf6204c80f5670724b843764af4fe8ecd9b2df2",
|
|
1989
|
+
"9c4f6cd0aec2598d1563fa56d87fd05aceaf28752cb811a280e30d369119885e",
|
|
1970
1990
|
"9e1db1f28cc7ee4b66b08c2fd2ee0ebf98054786d5902245941641e98783bf47",
|
|
1971
1991
|
"9e5db767677bc9fb07a97109e9036d1ffeda6018c6e678f5c28c55a6c516c234",
|
|
1972
1992
|
"9fc4267020b6341f74dabebb17123206293280b58cf114932f05a8234426445c",
|