@codyswann/lisa 4.45.4 → 4.45.6
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 +6 -0
- package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +74 -0
- package/all/copy-overwrite/scripts/lisa-hooks/worktree-binding-guard.mjs +115 -23
- package/all/copy-overwrite/scripts/lisa-worktree-guard.mjs +456 -0
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +28 -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 +21 -10
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/parity-safety-net.sh +74 -0
- package/package.json +5 -4
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/hooks/cleanup-stale-worktrees.sh +42 -1
- package/plugins/lisa/hooks/parity-safety-net.sh +74 -0
- package/plugins/lisa/hooks/worktree-binding-guard.mjs +115 -23
- package/plugins/lisa-agy/hooks/parity-safety-net.sh +74 -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/cleanup-stale-worktrees.sh +42 -1
- package/plugins/lisa-copilot/hooks/parity-safety-net.sh +74 -0
- package/plugins/lisa-copilot/hooks/worktree-binding-guard.mjs +115 -23
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/cleanup-stale-worktrees.sh +42 -1
- package/plugins/lisa-cursor/hooks/parity-safety-net.sh +74 -0
- package/plugins/lisa-cursor/hooks/worktree-binding-guard.mjs +115 -23
- 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/cleanup-stale-worktrees.sh +42 -1
- package/plugins/src/base/hooks/parity-safety-net.sh +74 -0
- package/plugins/src/base/hooks/worktree-binding-guard.mjs +115 -23
- package/scripts/check-cwd-resolution-corpus.mjs +163 -0
- package/scripts/cleanup-worktrees.sh +36 -3
- package/scripts/two-channel-couplings.json +1 -1
- package/typescript/copy-overwrite/.prettierignore +1 -0
- package/typescript/create-only/.github/workflows/review-evidence.yml +20 -1
|
@@ -0,0 +1,456 @@
|
|
|
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
|
+
/**
|
|
6
|
+
* lisa-worktree-guard — refuse to delete a linked worktree whose uncommitted
|
|
7
|
+
* work exists in no commit (CodySwannGT/lisa#3863).
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* ## The asymmetry that makes this necessary
|
|
11
|
+
*
|
|
12
|
+
* `.git/refs` and the object store are **shared** across every linked worktree
|
|
13
|
+
* of a repository; `.git/index` and the working files are **per-worktree**. So
|
|
14
|
+
* deleting a worktree spares everything that was committed and destroys
|
|
15
|
+
* everything that was only staged or only written. Two agents hit the same
|
|
16
|
+
* deletion event on the same evening: the one whose work was in a commit lost
|
|
17
|
+
* nothing, the one whose work was staged lost the whole deliverable. Nothing
|
|
18
|
+
* about the two situations looked different from the outside — `git status`
|
|
19
|
+
* reports staged work in green, which reads as "saved".
|
|
20
|
+
*
|
|
21
|
+
* `lisa-cross-worktree-guard.mjs` is the same control read from the other end:
|
|
22
|
+
* it detects one agent committing another agent's uncommitted file, and it
|
|
23
|
+
* needs BOTH halves — the commit and the surviving original — to see anything
|
|
24
|
+
* at all. This guard is what keeps the second half in existence.
|
|
25
|
+
*
|
|
26
|
+
* ## Why the deletion cannot be allowed to report success quietly
|
|
27
|
+
*
|
|
28
|
+
* A worktree deletion **destroys the only evidence its own case could leave**.
|
|
29
|
+
* Once the directory is gone the staged blob is unreferenced and the incident
|
|
30
|
+
* is indistinguishable from ordinary cleanup: no per-agent identifier appears
|
|
31
|
+
* in any git object, the branch survives, and the aggregate worktree count is
|
|
32
|
+
* flat whenever deletions are matched by creations. Any detection has to happen
|
|
33
|
+
* *before* the removal, which is why this is a pre-flight refusal rather than a
|
|
34
|
+
* monitor.
|
|
35
|
+
*
|
|
36
|
+
* ## What is refused, and what is not
|
|
37
|
+
*
|
|
38
|
+
* The question asked of every uncommitted file is narrow and answerable:
|
|
39
|
+
* **does a commit anywhere in this repository already contain these exact
|
|
40
|
+
* bytes?** If yes, deleting the worktree costs nothing recoverable and the
|
|
41
|
+
* removal proceeds without ceremony — a worktree full of untracked build
|
|
42
|
+
* output, or holding a file byte-identical to one already pushed, is not work
|
|
43
|
+
* at risk. If no, the removal is refused and the at-risk files are named.
|
|
44
|
+
*
|
|
45
|
+
* Reachability is answered from `git rev-list --objects --all`, which walks
|
|
46
|
+
* every ref rather than only the current branch, so work committed on a
|
|
47
|
+
* different branch (or already pushed and merged) reads as safe. A blob that
|
|
48
|
+
* exists only in the object database — which is exactly what `git add` writes —
|
|
49
|
+
* is NOT reachable and is exactly the case this guard exists to catch.
|
|
50
|
+
*
|
|
51
|
+
* ## The override is real, and it is recorded
|
|
52
|
+
*
|
|
53
|
+
* An operator who genuinely wants the directory gone must not be stranded, so
|
|
54
|
+
* `--force` (or `LISA_WORKTREE_REMOVE_OVERRIDE=1`) proceeds. Both the refusal
|
|
55
|
+
* and the override append a line to a JSONL ledger in the **common** git
|
|
56
|
+
* directory — never inside the worktree, which is about to stop existing. The
|
|
57
|
+
* ledger is the only artifact that survives to say a detectable case was
|
|
58
|
+
* knowingly made undetectable.
|
|
59
|
+
*
|
|
60
|
+
* Failing closed is deliberate: an unreadable repository, a `rev-list` that
|
|
61
|
+
* cannot be buffered, or a worktree git does not know about all resolve to
|
|
62
|
+
* refusal. "I could not tell" must never be cheaper than "there is work here".
|
|
63
|
+
* @module scripts/lisa-worktree-guard
|
|
64
|
+
*/
|
|
65
|
+
import { execFileSync } from "node:child_process";
|
|
66
|
+
import { appendFileSync, existsSync } from "node:fs";
|
|
67
|
+
import path from "node:path";
|
|
68
|
+
|
|
69
|
+
import { invokedAsScript } from "./lib/invoked-as-script.mjs";
|
|
70
|
+
|
|
71
|
+
/** Exit code meaning "safe, or removed". */
|
|
72
|
+
const OK = 0;
|
|
73
|
+
/** Exit code meaning "usage or environment error". */
|
|
74
|
+
const ERROR = 1;
|
|
75
|
+
/** Exit code meaning "refused: uncommitted work would be destroyed". */
|
|
76
|
+
const REFUSED = 2;
|
|
77
|
+
|
|
78
|
+
/** Ledger filename, written beside the shared object store. */
|
|
79
|
+
const LEDGER = "lisa-worktree-removals.jsonl";
|
|
80
|
+
|
|
81
|
+
/** Environment variable spelling of `--force`. */
|
|
82
|
+
const OVERRIDE_ENV = "LISA_WORKTREE_REMOVE_OVERRIDE";
|
|
83
|
+
|
|
84
|
+
/** Verdict reason: the path is not a git worktree at all. */
|
|
85
|
+
const NOT_A_WORKTREE = "not-a-worktree";
|
|
86
|
+
|
|
87
|
+
/** Verdict reason: the path is the primary checkout, which is never removable. */
|
|
88
|
+
const PRIMARY_CHECKOUT = "primary-checkout";
|
|
89
|
+
|
|
90
|
+
/** Verdict reason: the repository's objects could not be enumerated. */
|
|
91
|
+
const UNREADABLE = "unreadable";
|
|
92
|
+
|
|
93
|
+
/** Largest `git rev-list` payload buffered before the guard fails closed. */
|
|
94
|
+
const MAX_BUFFER = 1 << 28;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Run git, returning stdout.
|
|
98
|
+
* @param {string[]} args - Arguments after `git`.
|
|
99
|
+
* @param {string} cwd - Directory to run in.
|
|
100
|
+
* @returns {string} Captured stdout.
|
|
101
|
+
*/
|
|
102
|
+
function git(args, cwd) {
|
|
103
|
+
return execFileSync("git", args, {
|
|
104
|
+
cwd,
|
|
105
|
+
encoding: "utf8",
|
|
106
|
+
maxBuffer: MAX_BUFFER,
|
|
107
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Run git, returning `null` instead of throwing.
|
|
113
|
+
* @param {string[]} args - Arguments after `git`.
|
|
114
|
+
* @param {string} cwd - Directory to run in.
|
|
115
|
+
* @returns {string | null} Captured stdout, or `null` when git failed.
|
|
116
|
+
*/
|
|
117
|
+
function gitOrNull(args, cwd) {
|
|
118
|
+
try {
|
|
119
|
+
return git(args, cwd);
|
|
120
|
+
} catch {
|
|
121
|
+
// probe-direction: fail-closed — no caller spends this null as an answer.
|
|
122
|
+
// `candidatePaths`, `indexBlobs` and `candidateBlobs` each propagate it,
|
|
123
|
+
// and `classifyWorktree` turns it into UNREADABLE, which REFUSES the
|
|
124
|
+
// removal. A git that cannot answer costs a false refusal, never a
|
|
125
|
+
// silent deletion.
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Split NUL-delimited git output into entries.
|
|
132
|
+
* @param {string | null} out - Raw git output.
|
|
133
|
+
* @returns {string[]} Non-empty entries.
|
|
134
|
+
*/
|
|
135
|
+
function splitZ(out) {
|
|
136
|
+
return (out ?? "").split("\0").filter(Boolean);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Repo-relative paths whose current bytes may differ from every commit.
|
|
141
|
+
*
|
|
142
|
+
* Staged additions, unstaged modifications and untracked files are all at risk;
|
|
143
|
+
* ignored files are not (that is what makes build output cheap to delete).
|
|
144
|
+
*
|
|
145
|
+
* Returns `null` when any of the three probes failed. An empty list means
|
|
146
|
+
* "git looked and found nothing"; `null` means "git could not look", and the
|
|
147
|
+
* two must never collapse into each other — collapsing them is what lets an
|
|
148
|
+
* unreadable worktree be deleted as though it were empty.
|
|
149
|
+
* @param {string} wt - Worktree path.
|
|
150
|
+
* @returns {string[] | null} Candidate repo-relative paths, or `null`.
|
|
151
|
+
*/
|
|
152
|
+
function candidatePaths(wt) {
|
|
153
|
+
const staged = gitOrNull(
|
|
154
|
+
["diff", "--cached", "--name-only", "--no-renames", "-z"],
|
|
155
|
+
wt
|
|
156
|
+
);
|
|
157
|
+
const unstaged = gitOrNull(["diff", "--name-only", "--no-renames", "-z"], wt);
|
|
158
|
+
const untracked = gitOrNull(
|
|
159
|
+
["ls-files", "--others", "--exclude-standard", "-z"],
|
|
160
|
+
wt
|
|
161
|
+
);
|
|
162
|
+
if (staged === null || unstaged === null || untracked === null) return null;
|
|
163
|
+
return [
|
|
164
|
+
...new Set([...splitZ(staged), ...splitZ(unstaged), ...splitZ(untracked)]),
|
|
165
|
+
];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Index blob ids for the given paths, keyed by path.
|
|
170
|
+
* @param {string} wt - Worktree path.
|
|
171
|
+
* @param {string[]} paths - Candidate repo-relative paths.
|
|
172
|
+
* @returns {Map<string, string> | null} Path to staged blob id, or `null`.
|
|
173
|
+
*/
|
|
174
|
+
function indexBlobs(wt, paths) {
|
|
175
|
+
const entries = new Map();
|
|
176
|
+
if (paths.length === 0) return entries;
|
|
177
|
+
const raw = gitOrNull(["ls-files", "-s", "-z", "--", ...paths], wt);
|
|
178
|
+
if (raw === null) return null;
|
|
179
|
+
for (const line of splitZ(raw)) {
|
|
180
|
+
const tab = line.indexOf("\t");
|
|
181
|
+
if (tab === -1) continue;
|
|
182
|
+
const fields = line.slice(0, tab).split(" ");
|
|
183
|
+
if (fields.length >= 2) entries.set(line.slice(tab + 1), fields[1]);
|
|
184
|
+
}
|
|
185
|
+
return entries;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Every blob a worktree holds that no commit may contain, as path/blob pairs.
|
|
190
|
+
*
|
|
191
|
+
* Both sides of a path are collected: the INDEX blob (what `git add` wrote) and
|
|
192
|
+
* the WORKING blob (what is on disk). They differ for a file staged and then
|
|
193
|
+
* edited again, and losing either is losing work.
|
|
194
|
+
* @param {string} wt - Worktree path.
|
|
195
|
+
* @returns {{path: string, blob: string, source: string}[] | null} Candidate
|
|
196
|
+
* blobs, or `null` when the worktree could not be read.
|
|
197
|
+
*/
|
|
198
|
+
function candidateBlobs(wt) {
|
|
199
|
+
const paths = candidatePaths(wt);
|
|
200
|
+
if (paths === null) return null;
|
|
201
|
+
const staged = indexBlobs(wt, paths);
|
|
202
|
+
if (staged === null) return null;
|
|
203
|
+
const found = [];
|
|
204
|
+
for (const rel of paths) {
|
|
205
|
+
const stagedBlob = staged.get(rel);
|
|
206
|
+
if (stagedBlob)
|
|
207
|
+
found.push({ path: rel, blob: stagedBlob, source: "index" });
|
|
208
|
+
if (!existsSync(path.join(wt, rel))) continue;
|
|
209
|
+
const hashed = gitOrNull(["hash-object", "--", rel], wt);
|
|
210
|
+
// A file that is on disk but cannot be hashed is unread, not unchanged.
|
|
211
|
+
if (hashed === null) return null;
|
|
212
|
+
const id = hashed.trim();
|
|
213
|
+
if (id && id !== stagedBlob) {
|
|
214
|
+
found.push({ path: rel, blob: id, source: "worktree" });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return found;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The subset of `wanted` blob ids that some commit already contains.
|
|
222
|
+
*
|
|
223
|
+
* Returns `null` when the answer could not be computed, which callers must
|
|
224
|
+
* treat as "nothing is reachable" rather than as "everything is".
|
|
225
|
+
* @param {string} wt - Worktree path.
|
|
226
|
+
* @param {Set<string>} wanted - Blob ids to look for.
|
|
227
|
+
* @returns {Set<string> | null} Reachable ids, or `null` on failure.
|
|
228
|
+
*/
|
|
229
|
+
function reachableBlobs(wt, wanted) {
|
|
230
|
+
if (wanted.size === 0) return new Set();
|
|
231
|
+
const raw = gitOrNull(["rev-list", "--objects", "--all"], wt);
|
|
232
|
+
if (raw === null) return null;
|
|
233
|
+
const reachable = new Set();
|
|
234
|
+
for (const line of raw.split("\n")) {
|
|
235
|
+
const space = line.indexOf(" ");
|
|
236
|
+
const id = space === -1 ? line : line.slice(0, space);
|
|
237
|
+
if (wanted.has(id)) reachable.add(id);
|
|
238
|
+
}
|
|
239
|
+
return reachable;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Resolve the worktree's identity: its branch, the primary checkout, and
|
|
244
|
+
* whether the path is a linked worktree at all.
|
|
245
|
+
* @param {string} target - Path to inspect.
|
|
246
|
+
* @returns {{worktree: string, primary: string, common: string, branch: string, linked: boolean} | null} Identity, or `null` when not a repository.
|
|
247
|
+
*/
|
|
248
|
+
function identify(target) {
|
|
249
|
+
const top = gitOrNull(["rev-parse", "--show-toplevel"], target)?.trim();
|
|
250
|
+
if (!top) return null;
|
|
251
|
+
const gitDir = gitOrNull(["rev-parse", "--absolute-git-dir"], target)?.trim();
|
|
252
|
+
const commonRaw = gitOrNull(
|
|
253
|
+
["rev-parse", "--git-common-dir"],
|
|
254
|
+
target
|
|
255
|
+
)?.trim();
|
|
256
|
+
if (!gitDir || !commonRaw) return null;
|
|
257
|
+
const common = path.resolve(top, commonRaw);
|
|
258
|
+
const listed = gitOrNull(["worktree", "list", "--porcelain"], target) ?? "";
|
|
259
|
+
const first = listed.split("\n")[0] ?? "";
|
|
260
|
+
return {
|
|
261
|
+
worktree: top,
|
|
262
|
+
primary: first.startsWith("worktree ")
|
|
263
|
+
? first.slice(9)
|
|
264
|
+
: path.dirname(common),
|
|
265
|
+
common,
|
|
266
|
+
branch:
|
|
267
|
+
gitOrNull(["rev-parse", "--abbrev-ref", "HEAD"], target)?.trim() ?? "",
|
|
268
|
+
linked: path.resolve(gitDir) !== common,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Classify a worktree for removal safety.
|
|
274
|
+
* @param {string} target - Path to inspect.
|
|
275
|
+
* @returns {{ok: boolean, reason: string, identity: object | null, atRisk: object[]}} Verdict.
|
|
276
|
+
*/
|
|
277
|
+
export function classifyWorktree(target) {
|
|
278
|
+
const identity = identify(target);
|
|
279
|
+
if (!identity) {
|
|
280
|
+
return { ok: false, reason: NOT_A_WORKTREE, identity: null, atRisk: [] };
|
|
281
|
+
}
|
|
282
|
+
if (!identity.linked) {
|
|
283
|
+
return { ok: false, reason: PRIMARY_CHECKOUT, identity, atRisk: [] };
|
|
284
|
+
}
|
|
285
|
+
const blobs = candidateBlobs(identity.worktree);
|
|
286
|
+
if (blobs === null) {
|
|
287
|
+
return { ok: false, reason: UNREADABLE, identity, atRisk: [] };
|
|
288
|
+
}
|
|
289
|
+
const reachable = reachableBlobs(
|
|
290
|
+
identity.worktree,
|
|
291
|
+
new Set(blobs.map(entry => entry.blob))
|
|
292
|
+
);
|
|
293
|
+
if (reachable === null) {
|
|
294
|
+
return { ok: false, reason: UNREADABLE, identity, atRisk: blobs };
|
|
295
|
+
}
|
|
296
|
+
const atRisk = blobs.filter(entry => !reachable.has(entry.blob));
|
|
297
|
+
return {
|
|
298
|
+
ok: atRisk.length === 0,
|
|
299
|
+
reason: atRisk.length === 0 ? "safe" : "work-at-risk",
|
|
300
|
+
identity,
|
|
301
|
+
atRisk,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Append one line to the removal ledger in the shared git directory.
|
|
307
|
+
*
|
|
308
|
+
* Best-effort by construction: a ledger that cannot be written must not turn a
|
|
309
|
+
* safe removal into a failure, and must not turn a refusal into a pass.
|
|
310
|
+
* @param {string} common - Absolute path to the common git directory.
|
|
311
|
+
* @param {object} row - The record to append.
|
|
312
|
+
* @returns {string | null} Ledger path when written, else `null`.
|
|
313
|
+
*/
|
|
314
|
+
export function recordRemoval(common, row) {
|
|
315
|
+
const file = path.join(common, LEDGER);
|
|
316
|
+
try {
|
|
317
|
+
appendFileSync(file, `${JSON.stringify(row)}\n`, "utf8");
|
|
318
|
+
return file;
|
|
319
|
+
} catch {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Human-readable refusal, naming every file that exists nowhere else.
|
|
326
|
+
* @param {object} verdict - Result of `classifyWorktree`.
|
|
327
|
+
* @returns {string} Message for stderr.
|
|
328
|
+
*/
|
|
329
|
+
function refusalMessage(verdict) {
|
|
330
|
+
const lines = verdict.atRisk.map(
|
|
331
|
+
entry => ` ${entry.source.padEnd(8)} ${entry.path} (blob ${entry.blob})`
|
|
332
|
+
);
|
|
333
|
+
return [
|
|
334
|
+
`REFUSED: ${verdict.identity.worktree}`,
|
|
335
|
+
`${verdict.atRisk.length} file(s) here exist in no commit, in this repository or any other:`,
|
|
336
|
+
...lines,
|
|
337
|
+
"",
|
|
338
|
+
"A worktree's index and working files are per-worktree; only commits are shared.",
|
|
339
|
+
"Deleting this directory destroys the bytes above and leaves no trace that it did.",
|
|
340
|
+
"Commit them (they need no push to survive), or re-run with --force to delete them",
|
|
341
|
+
`anyway — an override is recorded in ${LEDGER}.`,
|
|
342
|
+
].join("\n");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Reasons that describe a broken request rather than work at risk.
|
|
347
|
+
* @param {string} reason - Verdict reason.
|
|
348
|
+
* @returns {string} Operator-readable explanation.
|
|
349
|
+
*/
|
|
350
|
+
function hardFailure(reason) {
|
|
351
|
+
if (reason === NOT_A_WORKTREE) return "not a git worktree";
|
|
352
|
+
if (reason === PRIMARY_CHECKOUT) {
|
|
353
|
+
return "refusing to touch the primary checkout (no override exists)";
|
|
354
|
+
}
|
|
355
|
+
return "could not read the repository's objects; refusing fail-closed";
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Parse argv into a verb, a target and flags.
|
|
360
|
+
* @param {string[]} argv - Arguments after the script name.
|
|
361
|
+
* @returns {{verb: string, target: string, force: boolean, json: boolean}} Options.
|
|
362
|
+
*/
|
|
363
|
+
function parseArgs(argv) {
|
|
364
|
+
const rest = argv.filter(arg => !arg.startsWith("--"));
|
|
365
|
+
const verb = rest[0] === "remove" || rest[0] === "check" ? rest[0] : "check";
|
|
366
|
+
const positional = rest[0] === verb && rest.length > 1 ? rest[1] : rest[0];
|
|
367
|
+
return {
|
|
368
|
+
verb,
|
|
369
|
+
target: positional ?? "",
|
|
370
|
+
force: argv.includes("--force") || process.env[OVERRIDE_ENV] === "1",
|
|
371
|
+
json: argv.includes("--json"),
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Entry point.
|
|
377
|
+
* @param {string[]} argv - Arguments after the script name.
|
|
378
|
+
* @param {{out?: NodeJS.WritableStream, err?: NodeJS.WritableStream}} [io] - Streams.
|
|
379
|
+
* @returns {number} Process exit code.
|
|
380
|
+
*/
|
|
381
|
+
export function main(argv, io = {}) {
|
|
382
|
+
const out = io.out ?? process.stdout;
|
|
383
|
+
const err = io.err ?? process.stderr;
|
|
384
|
+
const opts = parseArgs(argv);
|
|
385
|
+
if (!opts.target) {
|
|
386
|
+
err.write(
|
|
387
|
+
"usage: lisa-worktree-guard [check|remove] <worktree> [--force]\n"
|
|
388
|
+
);
|
|
389
|
+
return ERROR;
|
|
390
|
+
}
|
|
391
|
+
const verdict = classifyWorktree(opts.target);
|
|
392
|
+
if (opts.json) out.write(`${JSON.stringify(verdict)}\n`);
|
|
393
|
+
if (!verdict.identity || verdict.reason === PRIMARY_CHECKOUT) {
|
|
394
|
+
err.write(`${hardFailure(verdict.reason)}\n`);
|
|
395
|
+
return ERROR;
|
|
396
|
+
}
|
|
397
|
+
return finish(verdict, opts, out, err);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Apply the verdict: refuse, record an override, or remove.
|
|
402
|
+
* @param {object} verdict - Result of `classifyWorktree`.
|
|
403
|
+
* @param {object} opts - Parsed options.
|
|
404
|
+
* @param {NodeJS.WritableStream} out - Stdout.
|
|
405
|
+
* @param {NodeJS.WritableStream} err - Stderr.
|
|
406
|
+
* @returns {number} Process exit code.
|
|
407
|
+
*/
|
|
408
|
+
function finish(verdict, opts, out, err) {
|
|
409
|
+
const { identity } = verdict;
|
|
410
|
+
const base = {
|
|
411
|
+
at: new Date().toISOString(),
|
|
412
|
+
worktree: identity.worktree,
|
|
413
|
+
branch: identity.branch,
|
|
414
|
+
verb: opts.verb,
|
|
415
|
+
atRisk: verdict.atRisk,
|
|
416
|
+
};
|
|
417
|
+
if (!verdict.ok && !opts.force) {
|
|
418
|
+
recordRemoval(identity.common, { ...base, action: "refused" });
|
|
419
|
+
if (verdict.reason === UNREADABLE) {
|
|
420
|
+
err.write(`${hardFailure(verdict.reason)}\n`);
|
|
421
|
+
return REFUSED;
|
|
422
|
+
}
|
|
423
|
+
err.write(`${refusalMessage(verdict)}\n`);
|
|
424
|
+
return REFUSED;
|
|
425
|
+
}
|
|
426
|
+
const action = verdict.ok ? "removed" : "override";
|
|
427
|
+
if (opts.verb === "check") {
|
|
428
|
+
if (!verdict.ok) recordRemoval(identity.common, { ...base, action });
|
|
429
|
+
return OK;
|
|
430
|
+
}
|
|
431
|
+
return remove(identity, { ...base, action }, out, err);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Remove the worktree and record it.
|
|
436
|
+
* @param {object} identity - Worktree identity.
|
|
437
|
+
* @param {object} row - Ledger row.
|
|
438
|
+
* @param {NodeJS.WritableStream} out - Stdout.
|
|
439
|
+
* @param {NodeJS.WritableStream} err - Stderr.
|
|
440
|
+
* @returns {number} Process exit code.
|
|
441
|
+
*/
|
|
442
|
+
function remove(identity, row, out, err) {
|
|
443
|
+
recordRemoval(identity.common, row);
|
|
444
|
+
try {
|
|
445
|
+
git(["worktree", "remove", "--force", identity.worktree], identity.primary);
|
|
446
|
+
} catch (error) {
|
|
447
|
+
err.write(`error: git worktree remove failed: ${error.message}\n`);
|
|
448
|
+
return ERROR;
|
|
449
|
+
}
|
|
450
|
+
out.write(`removed ${identity.worktree}\n`);
|
|
451
|
+
return OK;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (invokedAsScript(import.meta.url)) {
|
|
455
|
+
process.exitCode = main(process.argv.slice(2));
|
|
456
|
+
}
|
|
@@ -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,CAqlCjC,CAAC;AAEH;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,+BAA+B,EAAE,QAAQ,CACpD,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAw/BjC,CAAC"}
|
|
@@ -426,11 +426,14 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
|
|
|
426
426
|
"99422b4912cd660ddeb5ec5ab9fc333e57fa5763e711117a00b06dd2eda0bbb7",
|
|
427
427
|
]),
|
|
428
428
|
"scripts/lisa-cross-worktree-guard.mjs": Object.freeze([
|
|
429
|
+
"2ca521021c535be700caaaf4755a54363f685a9a85e435624833cb838a072c5d",
|
|
429
430
|
"3967e4eb189c043e8702ca5cff40c4612ed77027a053e23635eeb367a436cb9d",
|
|
430
431
|
"41b92e1e94a8ee9ad0235359dbd83566a49b17db60da933835057dc77008ab87",
|
|
431
432
|
"592aed14ae21fea08be73497f5cb5d8c94a0a1915792632265fe2717a8817176",
|
|
432
433
|
"d8625e2d339ac512350203e29e70f7b20546bf5248cf952aafd997389feb230f",
|
|
434
|
+
"da6ec451ca6ea7d890874f8f54bc56f41e541a1190bb170a5167ea104637aba6",
|
|
433
435
|
"e0ef99ef1df32f76601a2106a2b68b3c31a126e0b48e8b8ceaba1a7476c32900",
|
|
436
|
+
"e3422f6177b6d6eb5509f17673ff8e10e7ed91bf59dd1630e9c5f77d2b56f327",
|
|
434
437
|
]),
|
|
435
438
|
"scripts/lisa-destructive-guard.mjs": Object.freeze([
|
|
436
439
|
"4fb431a3c58f443e303d613934d023d9e95ddae5c42710bb7009f7c080c82a64",
|
|
@@ -740,9 +743,11 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
|
|
|
740
743
|
"1d4fbe135f1bb4e861d286a6512ed201d4522250dfe3f9eba6584bc5ff166a95",
|
|
741
744
|
"2b1ca7d317dea3af106df1a74b1718e3df9c18e76e9d9d769aa3271b7abf8a3f",
|
|
742
745
|
"2bdba97c9a1ccc0e70247ee02b5eff02c042aba83b8e7fed3dfbf29a8a83b394",
|
|
746
|
+
"2da3ad6d7680b61e20b48022e57b8960e063266cb26c197c2c8daa89adf4b648",
|
|
743
747
|
"388cf8847581d96a1ff8ea98290a33a5f96d67cdd502ea807a0b8d88efe90e19",
|
|
744
748
|
"4242b39169bb05aaac4113569125a2c3a351c5dde2d12fd7dacc303dd9a2b5a4",
|
|
745
749
|
"450338257697a75a8fce9f06f3fc00bf3417947e50d601794da41f9c520a7cc3",
|
|
750
|
+
"57051a1913665b34acc342002d799c703258650a19c9b9894e1911ddf3bd519f",
|
|
746
751
|
"5ef7c10bee0110c4af1e429fac01332433102c495ec467966ddd7b1494dc899d",
|
|
747
752
|
"64546813f168fb446f39e34bc674fd3af85b15bd5e7f4f6bf4503463e701a7b9",
|
|
748
753
|
"74b5b90fcf15dbeb14a69677cf74e339154421cd056fdaf603d1167b34a74105",
|
|
@@ -758,6 +763,7 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
|
|
|
758
763
|
"a211f1a06aecddb841851463da302f694ab0eafff6bab75ee944b0a58aac431d",
|
|
759
764
|
"a22fd1b7c483f1a538202353ee85c82be17a8910e03c0a930635ce8cb3595c64",
|
|
760
765
|
"a28779ba2e6e4e999014d50734ea0b7e36af886bdb08d144bd4274acd642c902",
|
|
766
|
+
"afe82473fe2a08af298997cbce62417fdaefa37f4a963f720ed38b992a65b8d3",
|
|
761
767
|
"b5505f55a5092f1d451ae5a01ada85a031e5b063d08fc0ce5527d94b67191a60",
|
|
762
768
|
"b6fbbda29d897a7c39cf0524c91d5c66a47054df32e036f15cdabefdc34ac72a",
|
|
763
769
|
"b83c5043a0f29b2d8851258acfe6906aab7922cd80f486e25220e55f26a8c0a5",
|
|
@@ -791,11 +797,13 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
|
|
|
791
797
|
"scripts/lisa-hooks/worktree-binding-guard.mjs": Object.freeze([
|
|
792
798
|
"248939a111ad7d03d19ae210e5d76cb13613815719610ef22df9a5199f0631e8",
|
|
793
799
|
"2d094120f2c1748144f1c0e5757a60ecc7c6ae3e4f57fa54ab6a15c5a9e1868f",
|
|
800
|
+
"5706ed8cfb4c75a3439080d114e61fa5a59e60aa09f7394dea7d322854a6e314",
|
|
794
801
|
"5fe074e29a332c8c4c83a1bdf3bdc766675203178810e0977e7d270ef680636a",
|
|
795
802
|
"6b0c13bf89dd62577baebb7e6e8ff7a0bbcb1e83b32c5a1b878a64c76d83d822",
|
|
796
803
|
"822e4f9d2c3ad8fb184ff7fc880f6811004852fb731c7878b5a8a1a9532bceb8",
|
|
797
804
|
"bc1118b05eaea3fc7b3409c96979d64e3dee0907118f3e84d735163f473f632a",
|
|
798
805
|
"e121219bbee1960a6bf88d1e2ba0b50587d63013e7872c56619c04182e8a7561",
|
|
806
|
+
"efbf0d770c5547e9b7465eac4650c4ff01d427dee86fdb623a463a32023bcabf",
|
|
799
807
|
]),
|
|
800
808
|
"scripts/lisa-hooks/worktree-binding-guard.sh": Object.freeze([
|
|
801
809
|
"662652c09e5eb70f14db61136ea1206e9b337fce49dcabc774a45a07a0fd718f",
|
|
@@ -1017,6 +1025,12 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
|
|
|
1017
1025
|
"fc0d92739e620d8fd70f7aa4a12d3e0b3a8f790d47c4a8322040f32c30b9445a",
|
|
1018
1026
|
"fe064a2bd11200ed256fb36e203978f858fccbcaf40708496c066383345900ef",
|
|
1019
1027
|
]),
|
|
1028
|
+
"scripts/lisa-worktree-guard.mjs": Object.freeze([
|
|
1029
|
+
"2535ef2a60b3413dacb8491c008ab28f0a07929196277d1d1fed54a04740c025",
|
|
1030
|
+
"71b67f940338e66864eec970b6881aba639176c9d262ab0ecf7203006d9f7cbf",
|
|
1031
|
+
"80ff38d47bdf125526bdbf4f41c1df6154baff2834dad4ec5a11a3dd3df5747e",
|
|
1032
|
+
"ebcea40e1484501f257128c9c92b8a3e2c4cefaeae397b96d267ef878d962da4",
|
|
1033
|
+
]),
|
|
1020
1034
|
"scripts/nightly-e2e-github-tracking.mjs": Object.freeze([
|
|
1021
1035
|
"2606caa9d31b2488aa7e468e584c9f2afda8581fa1941f4da9ec8b3cab09c1de",
|
|
1022
1036
|
"6fc4edbb8707a90ee878567d88f05dd2d7c2df062f81983c5419c8d4036b203f",
|
|
@@ -1514,11 +1528,14 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
|
|
|
1514
1528
|
"5d587fd849cef02e9706d084ac2b508f3885da0ba59b76311e3b666f4fbff01e",
|
|
1515
1529
|
]),
|
|
1516
1530
|
"scripts/lisa-cross-worktree-guard.mjs": Object.freeze([
|
|
1531
|
+
"2ca521021c535be700caaaf4755a54363f685a9a85e435624833cb838a072c5d",
|
|
1517
1532
|
"3967e4eb189c043e8702ca5cff40c4612ed77027a053e23635eeb367a436cb9d",
|
|
1518
1533
|
"41b92e1e94a8ee9ad0235359dbd83566a49b17db60da933835057dc77008ab87",
|
|
1519
1534
|
"592aed14ae21fea08be73497f5cb5d8c94a0a1915792632265fe2717a8817176",
|
|
1520
1535
|
"d8625e2d339ac512350203e29e70f7b20546bf5248cf952aafd997389feb230f",
|
|
1536
|
+
"da6ec451ca6ea7d890874f8f54bc56f41e541a1190bb170a5167ea104637aba6",
|
|
1521
1537
|
"e0ef99ef1df32f76601a2106a2b68b3c31a126e0b48e8b8ceaba1a7476c32900",
|
|
1538
|
+
"e3422f6177b6d6eb5509f17673ff8e10e7ed91bf59dd1630e9c5f77d2b56f327",
|
|
1522
1539
|
]),
|
|
1523
1540
|
"scripts/lisa-destructive-guard.mjs": Object.freeze([
|
|
1524
1541
|
"4fb431a3c58f443e303d613934d023d9e95ddae5c42710bb7009f7c080c82a64",
|
|
@@ -1783,9 +1800,11 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
|
|
|
1783
1800
|
"1bf3512bb94923a6cb25b1e7a619a3fc8e1336bffa16f499b459aaee3eaed66d",
|
|
1784
1801
|
"1d4fbe135f1bb4e861d286a6512ed201d4522250dfe3f9eba6584bc5ff166a95",
|
|
1785
1802
|
"2bdba97c9a1ccc0e70247ee02b5eff02c042aba83b8e7fed3dfbf29a8a83b394",
|
|
1803
|
+
"2da3ad6d7680b61e20b48022e57b8960e063266cb26c197c2c8daa89adf4b648",
|
|
1786
1804
|
"388cf8847581d96a1ff8ea98290a33a5f96d67cdd502ea807a0b8d88efe90e19",
|
|
1787
1805
|
"4242b39169bb05aaac4113569125a2c3a351c5dde2d12fd7dacc303dd9a2b5a4",
|
|
1788
1806
|
"450338257697a75a8fce9f06f3fc00bf3417947e50d601794da41f9c520a7cc3",
|
|
1807
|
+
"57051a1913665b34acc342002d799c703258650a19c9b9894e1911ddf3bd519f",
|
|
1789
1808
|
"5ef7c10bee0110c4af1e429fac01332433102c495ec467966ddd7b1494dc899d",
|
|
1790
1809
|
"64546813f168fb446f39e34bc674fd3af85b15bd5e7f4f6bf4503463e701a7b9",
|
|
1791
1810
|
"74b5b90fcf15dbeb14a69677cf74e339154421cd056fdaf603d1167b34a74105",
|
|
@@ -1801,6 +1820,7 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
|
|
|
1801
1820
|
"a211f1a06aecddb841851463da302f694ab0eafff6bab75ee944b0a58aac431d",
|
|
1802
1821
|
"a22fd1b7c483f1a538202353ee85c82be17a8910e03c0a930635ce8cb3595c64",
|
|
1803
1822
|
"a28779ba2e6e4e999014d50734ea0b7e36af886bdb08d144bd4274acd642c902",
|
|
1823
|
+
"afe82473fe2a08af298997cbce62417fdaefa37f4a963f720ed38b992a65b8d3",
|
|
1804
1824
|
"b5505f55a5092f1d451ae5a01ada85a031e5b063d08fc0ce5527d94b67191a60",
|
|
1805
1825
|
"b6fbbda29d897a7c39cf0524c91d5c66a47054df32e036f15cdabefdc34ac72a",
|
|
1806
1826
|
"b83c5043a0f29b2d8851258acfe6906aab7922cd80f486e25220e55f26a8c0a5",
|
|
@@ -1833,11 +1853,13 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
|
|
|
1833
1853
|
"scripts/lisa-hooks/worktree-binding-guard.mjs": Object.freeze([
|
|
1834
1854
|
"248939a111ad7d03d19ae210e5d76cb13613815719610ef22df9a5199f0631e8",
|
|
1835
1855
|
"2d094120f2c1748144f1c0e5757a60ecc7c6ae3e4f57fa54ab6a15c5a9e1868f",
|
|
1856
|
+
"5706ed8cfb4c75a3439080d114e61fa5a59e60aa09f7394dea7d322854a6e314",
|
|
1836
1857
|
"5fe074e29a332c8c4c83a1bdf3bdc766675203178810e0977e7d270ef680636a",
|
|
1837
1858
|
"6b0c13bf89dd62577baebb7e6e8ff7a0bbcb1e83b32c5a1b878a64c76d83d822",
|
|
1838
1859
|
"822e4f9d2c3ad8fb184ff7fc880f6811004852fb731c7878b5a8a1a9532bceb8",
|
|
1839
1860
|
"bc1118b05eaea3fc7b3409c96979d64e3dee0907118f3e84d735163f473f632a",
|
|
1840
1861
|
"e121219bbee1960a6bf88d1e2ba0b50587d63013e7872c56619c04182e8a7561",
|
|
1862
|
+
"efbf0d770c5547e9b7465eac4650c4ff01d427dee86fdb623a463a32023bcabf",
|
|
1841
1863
|
]),
|
|
1842
1864
|
"scripts/lisa-hooks/worktree-binding-guard.sh": Object.freeze([
|
|
1843
1865
|
"662652c09e5eb70f14db61136ea1206e9b337fce49dcabc774a45a07a0fd718f",
|
|
@@ -2036,6 +2058,12 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
|
|
|
2036
2058
|
"fc0d92739e620d8fd70f7aa4a12d3e0b3a8f790d47c4a8322040f32c30b9445a",
|
|
2037
2059
|
"fe064a2bd11200ed256fb36e203978f858fccbcaf40708496c066383345900ef",
|
|
2038
2060
|
]),
|
|
2061
|
+
"scripts/lisa-worktree-guard.mjs": Object.freeze([
|
|
2062
|
+
"2535ef2a60b3413dacb8491c008ab28f0a07929196277d1d1fed54a04740c025",
|
|
2063
|
+
"71b67f940338e66864eec970b6881aba639176c9d262ab0ecf7203006d9f7cbf",
|
|
2064
|
+
"80ff38d47bdf125526bdbf4f41c1df6154baff2834dad4ec5a11a3dd3df5747e",
|
|
2065
|
+
"ebcea40e1484501f257128c9c92b8a3e2c4cefaeae397b96d267ef878d962da4",
|
|
2066
|
+
]),
|
|
2039
2067
|
"scripts/nightly-e2e-github-tracking.mjs": Object.freeze([
|
|
2040
2068
|
"2606caa9d31b2488aa7e468e584c9f2afda8581fa1941f4da9ec8b3cab09c1de",
|
|
2041
2069
|
"6fc4edbb8707a90ee878567d88f05dd2d7c2df062f81983c5419c8d4036b203f",
|