@codyswann/lisa 4.45.5 → 4.45.7
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-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 +24 -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 +15 -8
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/parity-safety-net.sh +74 -0
- 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/cleanup-stale-worktrees.sh +42 -1
- package/plugins/lisa/hooks/parity-safety-net.sh +74 -0
- 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-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-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/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
|
@@ -28,6 +28,12 @@
|
|
|
28
28
|
* proof that none is takeable *right now*. Deleting a worktree that holds
|
|
29
29
|
* uncommitted work permanently forecloses the only window in which its own
|
|
30
30
|
* case was detectable.
|
|
31
|
+
*
|
|
32
|
+
* That limit is now held open from the other end by
|
|
33
|
+
* `lisa-worktree-guard.mjs` (CodySwannGT/lisa#3863), which refuses to remove a
|
|
34
|
+
* worktree whose uncommitted bytes exist in no commit. The two are one control
|
|
35
|
+
* read in two directions: this file needs the original to still be there, and
|
|
36
|
+
* that one is what keeps it there.
|
|
31
37
|
*/
|
|
32
38
|
|
|
33
39
|
import { execFileSync } from "node:child_process";
|
|
@@ -2033,6 +2033,77 @@ readonly GIT_CONTROL_PLANE='(^|[[:space:]='"$qc"'./])\.git([/[:space:]'"$qc"']|$
|
|
|
2033
2033
|
# that could not run produced no lines, the loop body never executed, and the
|
|
2034
2034
|
# guard was skipped with the command ALLOWED. A segmentation that cannot answer
|
|
2035
2035
|
# must refuse, exactly like a scan that cannot answer.
|
|
2036
|
+
# 14b. Linked worktrees, which are the same principle one level out. `.git` is
|
|
2037
|
+
# protected because nothing in the working tree can rebuild it; a linked
|
|
2038
|
+
# worktree is protected because nothing OUTSIDE it can rebuild what it holds.
|
|
2039
|
+
# The asymmetry is exact and it is invisible from the prompt: `.git/refs` and
|
|
2040
|
+
# the object store are shared by every worktree of a repository, while
|
|
2041
|
+
# `.git/index` and the working files are per-worktree. So `git commit` moves
|
|
2042
|
+
# work somewhere a deletion cannot reach, and `git add` does not — a staged file
|
|
2043
|
+
# shows green in `git status`, which reads as saved, and dies with the
|
|
2044
|
+
# directory.
|
|
2045
|
+
#
|
|
2046
|
+
# Measured twice in one evening (issue #3863): one worktree lost a 284-line
|
|
2047
|
+
# staged test file, and a second lost nothing from the identical event purely
|
|
2048
|
+
# because a gate had forced its work into a commit first. Neither deletion
|
|
2049
|
+
# announced itself. That is the second reason this is a block rather than a
|
|
2050
|
+
# warning — a worktree deletion destroys the only evidence its own case could
|
|
2051
|
+
# leave, so there is no after-the-fact detection to fall back on.
|
|
2052
|
+
#
|
|
2053
|
+
# `git worktree remove` (guard 12) already refuses a dirty tree, and
|
|
2054
|
+
# `--force` is already blocked. This closes the spelling that walks around both:
|
|
2055
|
+
# `rm -rf` on the directory, which git never sees.
|
|
2056
|
+
#
|
|
2057
|
+
# Matching is by REGISTERED path — `git worktree list` names them — so an
|
|
2058
|
+
# ordinary directory that merely lives near one is untouched. A repository that
|
|
2059
|
+
# cannot be read answers "no": this guard adds protection to a shape that was
|
|
2060
|
+
# fully allowed a moment ago, and failing it closed would deny every recursive
|
|
2061
|
+
# delete on every path outside a repository.
|
|
2062
|
+
#
|
|
2063
|
+
# The comparison is on WHOLE TOKENS, which is the only spelling that gets both
|
|
2064
|
+
# error directions right. Substring containment would let a worktree named
|
|
2065
|
+
# `wt-1` claim `rm -rf wt-10`, an over-block on an unrelated directory; and
|
|
2066
|
+
# hand-written boundary classes leak the other way, because `rm -rf <wt>/`
|
|
2067
|
+
# with one trailing slash ends on a character every such class treats as
|
|
2068
|
+
# "inside a path". So every non-path character — quote, space, parenthesis,
|
|
2069
|
+
# redirection — is folded to a space first, which makes each argument a
|
|
2070
|
+
# space-delimited token regardless of how it was quoted, and the match is then
|
|
2071
|
+
# an equality test on that token. A SUBPATH (`rm -rf <wt>/node_modules`) is
|
|
2072
|
+
# deliberately not this guard's business: it is not a worktree deletion, and
|
|
2073
|
+
# blocking routine cleanup inside a worktree is how a guard gets switched off.
|
|
2074
|
+
names_linked_worktree() {
|
|
2075
|
+
local stmt="$1" probe root listed line primary wt rel
|
|
2076
|
+
root="$(git -C "${CLAUDE_PROJECT_DIR:-$PWD}" rev-parse --show-toplevel 2> /dev/null)" || return 1
|
|
2077
|
+
[ -n "$root" ] || return 1
|
|
2078
|
+
listed="$(git -C "$root" worktree list --porcelain 2> /dev/null)" || return 1
|
|
2079
|
+
probe=" $(printf '%s' "$stmt" | tr -c 'A-Za-z0-9_./-' ' ') "
|
|
2080
|
+
primary=""
|
|
2081
|
+
while IFS= read -r line; do
|
|
2082
|
+
case "$line" in
|
|
2083
|
+
worktree\ *) wt="${line#worktree }" ;;
|
|
2084
|
+
*) continue ;;
|
|
2085
|
+
esac
|
|
2086
|
+
# First entry is the primary checkout, which is not a linked worktree.
|
|
2087
|
+
if [ -z "$primary" ]; then
|
|
2088
|
+
primary="$wt"
|
|
2089
|
+
continue
|
|
2090
|
+
fi
|
|
2091
|
+
case "$probe" in
|
|
2092
|
+
*" $wt "* | *" $wt/ "*) return 0 ;;
|
|
2093
|
+
esac
|
|
2094
|
+
rel="${wt#"$primary"/}"
|
|
2095
|
+
[ "$rel" != "$wt" ] || continue
|
|
2096
|
+
# `./` is the same path spelled the way an agent in the project root types
|
|
2097
|
+
# it, and a token comparison is exact, so it has to be spelled here too.
|
|
2098
|
+
case "$probe" in
|
|
2099
|
+
*" $rel "* | *" $rel/ "* | *" ./$rel "* | *" ./$rel/ "*) return 0 ;;
|
|
2100
|
+
esac
|
|
2101
|
+
done <<EOF
|
|
2102
|
+
$listed
|
|
2103
|
+
EOF
|
|
2104
|
+
return 1
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2036
2107
|
git_segments_status=0
|
|
2037
2108
|
git_segments="$(printf '%s' "$normalized_command_str" \
|
|
2038
2109
|
| awk '{ if (/\|[ \t]*$/) printf "%s ", $0; else print }' \
|
|
@@ -2055,6 +2126,9 @@ while IFS= read -r git_stmt; do
|
|
|
2055
2126
|
if scan -E "$git_stmt" "$GIT_CONTROL_PLANE"; then
|
|
2056
2127
|
block "recursive forced delete of the git control plane (.git holds every commit, branch and stash not already pushed; nothing in the working tree can rebuild it). Delete a specific ignored artifact instead. Re-cloning is NOT a like-for-like replacement: every linked worktree stores a pointer into this .git, so discarding it discards each of them and any uncommitted work they hold — check \`git worktree list\` before treating the checkout as disposable."
|
|
2057
2128
|
fi
|
|
2129
|
+
if names_linked_worktree "$git_stmt"; then
|
|
2130
|
+
block "recursive forced delete of a linked git worktree. A worktree's index and working files are private to it — only commits are shared — so this destroys any staged or unwritten-to-a-commit work another agent left there, and destroys the evidence that it did (CodySwannGT/lisa#3863). Use 'node scripts/lisa-worktree-guard.mjs remove <path>' instead: it deletes clean and already-committed trees without ceremony, names the files when they exist nowhere else, and records an explicit --force override."
|
|
2131
|
+
fi
|
|
2058
2132
|
done <<<"$git_segments"
|
|
2059
2133
|
|
|
2060
2134
|
# 15. Credential stores. Reading one into an agent transcript is disclosure even
|
|
@@ -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",
|
|
@@ -1019,6 +1025,12 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
|
|
|
1019
1025
|
"fc0d92739e620d8fd70f7aa4a12d3e0b3a8f790d47c4a8322040f32c30b9445a",
|
|
1020
1026
|
"fe064a2bd11200ed256fb36e203978f858fccbcaf40708496c066383345900ef",
|
|
1021
1027
|
]),
|
|
1028
|
+
"scripts/lisa-worktree-guard.mjs": Object.freeze([
|
|
1029
|
+
"2535ef2a60b3413dacb8491c008ab28f0a07929196277d1d1fed54a04740c025",
|
|
1030
|
+
"71b67f940338e66864eec970b6881aba639176c9d262ab0ecf7203006d9f7cbf",
|
|
1031
|
+
"80ff38d47bdf125526bdbf4f41c1df6154baff2834dad4ec5a11a3dd3df5747e",
|
|
1032
|
+
"ebcea40e1484501f257128c9c92b8a3e2c4cefaeae397b96d267ef878d962da4",
|
|
1033
|
+
]),
|
|
1022
1034
|
"scripts/nightly-e2e-github-tracking.mjs": Object.freeze([
|
|
1023
1035
|
"2606caa9d31b2488aa7e468e584c9f2afda8581fa1941f4da9ec8b3cab09c1de",
|
|
1024
1036
|
"6fc4edbb8707a90ee878567d88f05dd2d7c2df062f81983c5419c8d4036b203f",
|
|
@@ -1516,11 +1528,14 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
|
|
|
1516
1528
|
"5d587fd849cef02e9706d084ac2b508f3885da0ba59b76311e3b666f4fbff01e",
|
|
1517
1529
|
]),
|
|
1518
1530
|
"scripts/lisa-cross-worktree-guard.mjs": Object.freeze([
|
|
1531
|
+
"2ca521021c535be700caaaf4755a54363f685a9a85e435624833cb838a072c5d",
|
|
1519
1532
|
"3967e4eb189c043e8702ca5cff40c4612ed77027a053e23635eeb367a436cb9d",
|
|
1520
1533
|
"41b92e1e94a8ee9ad0235359dbd83566a49b17db60da933835057dc77008ab87",
|
|
1521
1534
|
"592aed14ae21fea08be73497f5cb5d8c94a0a1915792632265fe2717a8817176",
|
|
1522
1535
|
"d8625e2d339ac512350203e29e70f7b20546bf5248cf952aafd997389feb230f",
|
|
1536
|
+
"da6ec451ca6ea7d890874f8f54bc56f41e541a1190bb170a5167ea104637aba6",
|
|
1523
1537
|
"e0ef99ef1df32f76601a2106a2b68b3c31a126e0b48e8b8ceaba1a7476c32900",
|
|
1538
|
+
"e3422f6177b6d6eb5509f17673ff8e10e7ed91bf59dd1630e9c5f77d2b56f327",
|
|
1524
1539
|
]),
|
|
1525
1540
|
"scripts/lisa-destructive-guard.mjs": Object.freeze([
|
|
1526
1541
|
"4fb431a3c58f443e303d613934d023d9e95ddae5c42710bb7009f7c080c82a64",
|
|
@@ -1785,9 +1800,11 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
|
|
|
1785
1800
|
"1bf3512bb94923a6cb25b1e7a619a3fc8e1336bffa16f499b459aaee3eaed66d",
|
|
1786
1801
|
"1d4fbe135f1bb4e861d286a6512ed201d4522250dfe3f9eba6584bc5ff166a95",
|
|
1787
1802
|
"2bdba97c9a1ccc0e70247ee02b5eff02c042aba83b8e7fed3dfbf29a8a83b394",
|
|
1803
|
+
"2da3ad6d7680b61e20b48022e57b8960e063266cb26c197c2c8daa89adf4b648",
|
|
1788
1804
|
"388cf8847581d96a1ff8ea98290a33a5f96d67cdd502ea807a0b8d88efe90e19",
|
|
1789
1805
|
"4242b39169bb05aaac4113569125a2c3a351c5dde2d12fd7dacc303dd9a2b5a4",
|
|
1790
1806
|
"450338257697a75a8fce9f06f3fc00bf3417947e50d601794da41f9c520a7cc3",
|
|
1807
|
+
"57051a1913665b34acc342002d799c703258650a19c9b9894e1911ddf3bd519f",
|
|
1791
1808
|
"5ef7c10bee0110c4af1e429fac01332433102c495ec467966ddd7b1494dc899d",
|
|
1792
1809
|
"64546813f168fb446f39e34bc674fd3af85b15bd5e7f4f6bf4503463e701a7b9",
|
|
1793
1810
|
"74b5b90fcf15dbeb14a69677cf74e339154421cd056fdaf603d1167b34a74105",
|
|
@@ -1803,6 +1820,7 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
|
|
|
1803
1820
|
"a211f1a06aecddb841851463da302f694ab0eafff6bab75ee944b0a58aac431d",
|
|
1804
1821
|
"a22fd1b7c483f1a538202353ee85c82be17a8910e03c0a930635ce8cb3595c64",
|
|
1805
1822
|
"a28779ba2e6e4e999014d50734ea0b7e36af886bdb08d144bd4274acd642c902",
|
|
1823
|
+
"afe82473fe2a08af298997cbce62417fdaefa37f4a963f720ed38b992a65b8d3",
|
|
1806
1824
|
"b5505f55a5092f1d451ae5a01ada85a031e5b063d08fc0ce5527d94b67191a60",
|
|
1807
1825
|
"b6fbbda29d897a7c39cf0524c91d5c66a47054df32e036f15cdabefdc34ac72a",
|
|
1808
1826
|
"b83c5043a0f29b2d8851258acfe6906aab7922cd80f486e25220e55f26a8c0a5",
|
|
@@ -2040,6 +2058,12 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
|
|
|
2040
2058
|
"fc0d92739e620d8fd70f7aa4a12d3e0b3a8f790d47c4a8322040f32c30b9445a",
|
|
2041
2059
|
"fe064a2bd11200ed256fb36e203978f858fccbcaf40708496c066383345900ef",
|
|
2042
2060
|
]),
|
|
2061
|
+
"scripts/lisa-worktree-guard.mjs": Object.freeze([
|
|
2062
|
+
"2535ef2a60b3413dacb8491c008ab28f0a07929196277d1d1fed54a04740c025",
|
|
2063
|
+
"71b67f940338e66864eec970b6881aba639176c9d262ab0ecf7203006d9f7cbf",
|
|
2064
|
+
"80ff38d47bdf125526bdbf4f41c1df6154baff2834dad4ec5a11a3dd3df5747e",
|
|
2065
|
+
"ebcea40e1484501f257128c9c92b8a3e2c4cefaeae397b96d267ef878d962da4",
|
|
2066
|
+
]),
|
|
2043
2067
|
"scripts/nightly-e2e-github-tracking.mjs": Object.freeze([
|
|
2044
2068
|
"2606caa9d31b2488aa7e468e584c9f2afda8581fa1941f4da9ec8b3cab09c1de",
|
|
2045
2069
|
"6fc4edbb8707a90ee878567d88f05dd2d7c2df062f81983c5419c8d4036b203f",
|