@codyswann/lisa 4.10.15 → 4.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/doctor-worktree-hygiene.d.ts +5 -2
- package/dist/cli/doctor-worktree-hygiene.d.ts.map +1 -1
- package/dist/cli/doctor-worktree-hygiene.js +12 -6
- package/dist/cli/doctor-worktree-hygiene.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/prune-commands.d.ts +52 -0
- package/dist/cli/prune-commands.d.ts.map +1 -0
- package/dist/cli/prune-commands.js +158 -0
- package/dist/cli/prune-commands.js.map +1 -0
- package/dist/cli/prune-report.d.ts +52 -0
- package/dist/cli/prune-report.d.ts.map +1 -0
- package/dist/cli/prune-report.js +143 -0
- package/dist/cli/prune-report.js.map +1 -0
- package/dist/cli/stash-prune-policy.d.ts +72 -0
- package/dist/cli/stash-prune-policy.d.ts.map +1 -0
- package/dist/cli/stash-prune-policy.js +90 -0
- package/dist/cli/stash-prune-policy.js.map +1 -0
- package/dist/cli/stash-prune.d.ts +104 -0
- package/dist/cli/stash-prune.d.ts.map +1 -0
- package/dist/cli/stash-prune.js +206 -0
- package/dist/cli/stash-prune.js.map +1 -0
- package/dist/cli/worktree-inventory.d.ts +82 -0
- package/dist/cli/worktree-inventory.d.ts.map +1 -0
- package/dist/cli/worktree-inventory.js +195 -0
- package/dist/cli/worktree-inventory.js.map +1 -0
- package/dist/cli/worktree-liveness.d.ts +52 -0
- package/dist/cli/worktree-liveness.d.ts.map +1 -0
- package/dist/cli/worktree-liveness.js +124 -0
- package/dist/cli/worktree-liveness.js.map +1 -0
- package/dist/cli/worktree-ownership.d.ts +65 -0
- package/dist/cli/worktree-ownership.d.ts.map +1 -0
- package/dist/cli/worktree-ownership.js +142 -0
- package/dist/cli/worktree-ownership.js.map +1 -0
- package/dist/cli/worktree-prune-policy.d.ts +76 -0
- package/dist/cli/worktree-prune-policy.d.ts.map +1 -0
- package/dist/cli/worktree-prune-policy.js +114 -0
- package/dist/cli/worktree-prune-policy.js.map +1 -0
- package/dist/cli/worktree-prune.d.ts +96 -0
- package/dist/cli/worktree-prune.d.ts.map +1 -0
- package/dist/cli/worktree-prune.js +153 -0
- package/dist/cli/worktree-prune.js.map +1 -0
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +18 -0
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -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-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enumeration and fact-gathering for registered git worktrees.
|
|
3
|
+
*
|
|
4
|
+
* Enumeration is `git worktree list --porcelain`, never a scan of the worktree
|
|
5
|
+
* roots, and that is load-bearing twice over. It is the git control plane's own
|
|
6
|
+
* record of which checkouts belong to this repository — ownership evidence
|
|
7
|
+
* rather than a path guess — and it reaches worktrees parked outside the tidy
|
|
8
|
+
* roots, which a root scan silently reports as absent.
|
|
9
|
+
* @module cli/worktree-inventory
|
|
10
|
+
*/
|
|
11
|
+
import { execFile } from "node:child_process";
|
|
12
|
+
import { realpath, stat } from "node:fs/promises";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import { promisify } from "node:util";
|
|
15
|
+
import { countLiveHolders } from "./worktree-liveness.js";
|
|
16
|
+
import { judgeOwnership, readOwnerReceipt } from "./worktree-ownership.js";
|
|
17
|
+
const run = promisify(execFile);
|
|
18
|
+
const GIT_TIMEOUT_MS = 15_000;
|
|
19
|
+
const GIT_MAX_BUFFER_BYTES = 4 * 1024 * 1024;
|
|
20
|
+
const INSPECTION_BATCH_SIZE = 8;
|
|
21
|
+
/**
|
|
22
|
+
* Parse `git worktree list --porcelain` output.
|
|
23
|
+
*
|
|
24
|
+
* The first record is always the primary checkout, which is how the primary is
|
|
25
|
+
* identified — not by comparing it against a path the caller supplied, which
|
|
26
|
+
* would be wrong the moment the caller ran from a worktree.
|
|
27
|
+
* @param stdout - Porcelain output
|
|
28
|
+
* @returns Parsed entries in git's order
|
|
29
|
+
*/
|
|
30
|
+
export function parseWorktreeList(stdout) {
|
|
31
|
+
return stdout
|
|
32
|
+
.split(/\n{2,}/)
|
|
33
|
+
.map(block => block.split("\n").filter(line => line !== ""))
|
|
34
|
+
.filter(lines => lines.some(line => line.startsWith("worktree ")))
|
|
35
|
+
.map((lines, index) => parseWorktreeBlock(lines, index === 0));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Parse one porcelain record.
|
|
39
|
+
* @param lines - Non-empty lines of the record
|
|
40
|
+
* @param isPrimary - Whether this record is the primary checkout
|
|
41
|
+
* @returns Parsed entry
|
|
42
|
+
*/
|
|
43
|
+
function parseWorktreeBlock(lines, isPrimary) {
|
|
44
|
+
const worktreePath = (lines.find(line => line.startsWith("worktree ")) ?? "worktree ").slice("worktree ".length);
|
|
45
|
+
const branchLine = lines.find(line => line.startsWith("branch "));
|
|
46
|
+
const branch = branchLine === undefined
|
|
47
|
+
? undefined
|
|
48
|
+
: branchLine.slice("branch ".length).replace(/^refs\/heads\//, "");
|
|
49
|
+
const base = {
|
|
50
|
+
path: worktreePath,
|
|
51
|
+
locked: lines.some(line => line === "locked" || line.startsWith("locked ")),
|
|
52
|
+
prunable: lines.some(line => line === "prunable" || line.startsWith("prunable ")),
|
|
53
|
+
isPrimary,
|
|
54
|
+
};
|
|
55
|
+
return branch === undefined ? base : { ...base, branch };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Run one bounded git command.
|
|
59
|
+
* @param args - Git arguments
|
|
60
|
+
* @param cwd - Directory to run git in
|
|
61
|
+
* @returns Trimmed stdout
|
|
62
|
+
*/
|
|
63
|
+
export async function git(args, cwd) {
|
|
64
|
+
const { stdout } = await run("git", [...args], {
|
|
65
|
+
cwd,
|
|
66
|
+
maxBuffer: GIT_MAX_BUFFER_BYTES,
|
|
67
|
+
timeout: GIT_TIMEOUT_MS,
|
|
68
|
+
});
|
|
69
|
+
return stdout;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* List every worktree registered for a checkout.
|
|
73
|
+
* @param repoPath - Any path inside the repository
|
|
74
|
+
* @returns Registered entries
|
|
75
|
+
*/
|
|
76
|
+
export async function listWorktrees(repoPath) {
|
|
77
|
+
return parseWorktreeList(await git(["worktree", "list", "--porcelain"], repoPath));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Gather every fact the entitlement rule is allowed to consider.
|
|
81
|
+
*
|
|
82
|
+
* A worktree git cannot report on yields `readable: false` with zeroed counts.
|
|
83
|
+
* The zeros are never read as "holds nothing" because `readable: false` is
|
|
84
|
+
* itself a blocker — the same reason the doctor check returns undefined for an
|
|
85
|
+
* unreadable worktree instead of a zeroed exposure.
|
|
86
|
+
* @param entry - Registered worktree entry
|
|
87
|
+
* @param context - Shared run inputs
|
|
88
|
+
* @returns Facts for the entitlement rule
|
|
89
|
+
*/
|
|
90
|
+
export async function gatherWorktreeFacts(entry, context) {
|
|
91
|
+
// Compared against the PHYSICAL path. `lsof` reports resolved paths, and on
|
|
92
|
+
// macOS the scratch and temp roots agents park worktrees under are symlinks
|
|
93
|
+
// (`/tmp` is `/private/tmp`). Comparing the registered spelling against a
|
|
94
|
+
// resolved one finds no live holder for a worktree somebody is sitting in —
|
|
95
|
+
// a false "abandoned" verdict on exactly the busiest case.
|
|
96
|
+
const physical = await realpath(entry.path).catch(() => entry.path);
|
|
97
|
+
const shell = {
|
|
98
|
+
path: entry.path,
|
|
99
|
+
...(entry.branch === undefined ? {} : { branch: entry.branch }),
|
|
100
|
+
isPrimary: entry.isPrimary,
|
|
101
|
+
isCurrent: isCurrentWorktree(physical, context.currentPath),
|
|
102
|
+
locked: entry.locked,
|
|
103
|
+
liveHolders: context.liveWorkingDirectories === undefined
|
|
104
|
+
? undefined
|
|
105
|
+
: countLiveHolders(physical, context.liveWorkingDirectories),
|
|
106
|
+
};
|
|
107
|
+
const state = await readWorktreeState(entry.path, context).catch(() => undefined);
|
|
108
|
+
return state === undefined
|
|
109
|
+
? {
|
|
110
|
+
...shell,
|
|
111
|
+
readable: false,
|
|
112
|
+
ownership: "unclaimed",
|
|
113
|
+
idleSeconds: 0,
|
|
114
|
+
trackedChanges: 0,
|
|
115
|
+
untrackedFiles: 0,
|
|
116
|
+
unpushedCommits: 0,
|
|
117
|
+
}
|
|
118
|
+
: { ...shell, readable: true, ...state };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Read the git-derived half of one worktree's facts.
|
|
122
|
+
* @param worktree - Absolute worktree path
|
|
123
|
+
* @param context - Shared run inputs
|
|
124
|
+
* @returns Ownership, idleness, and work-at-risk facts
|
|
125
|
+
*/
|
|
126
|
+
async function readWorktreeState(worktree, context) {
|
|
127
|
+
const [status, adminDirectory, unpushed] = await Promise.all([
|
|
128
|
+
git(["status", "--porcelain"], worktree),
|
|
129
|
+
git(["rev-parse", "--absolute-git-dir"], worktree).then(out => out.trim()),
|
|
130
|
+
git(["rev-list", "--count", "HEAD", "--not", "--remotes"], worktree),
|
|
131
|
+
]);
|
|
132
|
+
const lines = status.split("\n").filter(line => line !== "");
|
|
133
|
+
const untrackedFiles = lines.filter(line => line.startsWith("??")).length;
|
|
134
|
+
const receiptOwner = await readOwnerReceipt(adminDirectory);
|
|
135
|
+
return {
|
|
136
|
+
ownership: judgeOwnership(receiptOwner, context.callerOwnerId),
|
|
137
|
+
idleSeconds: await measureIdleSeconds(worktree, adminDirectory, context.nowMs),
|
|
138
|
+
trackedChanges: lines.length - untrackedFiles,
|
|
139
|
+
untrackedFiles,
|
|
140
|
+
unpushedCommits: Number.parseInt(unpushed.trim(), 10) || 0,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Report whether the cleaner is running inside a worktree.
|
|
145
|
+
* @param worktree - Absolute worktree path
|
|
146
|
+
* @param currentPath - Path the cleaner runs from
|
|
147
|
+
* @returns True when the cleaner is inside
|
|
148
|
+
*/
|
|
149
|
+
function isCurrentWorktree(worktree, currentPath) {
|
|
150
|
+
const relative = path.relative(path.resolve(worktree), path.resolve(currentPath));
|
|
151
|
+
return (relative === "" ||
|
|
152
|
+
(!relative.startsWith(`..${path.sep}`) &&
|
|
153
|
+
relative !== ".." &&
|
|
154
|
+
!path.isAbsolute(relative)));
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Measure how long a worktree has shown no activity.
|
|
158
|
+
*
|
|
159
|
+
* The probes are the worktree root and the control-plane files git touches on
|
|
160
|
+
* every command run inside it. A working-tree edit that misses all of them
|
|
161
|
+
* still cannot slip through, because an edit shows up as a tracked or untracked
|
|
162
|
+
* change, which is a blocker in its own right.
|
|
163
|
+
* @param worktree - Absolute worktree path
|
|
164
|
+
* @param adminDirectory - Worktree's git admin directory
|
|
165
|
+
* @param nowMs - Clock in epoch milliseconds
|
|
166
|
+
* @returns Seconds since the most recent observed activity
|
|
167
|
+
*/
|
|
168
|
+
async function measureIdleSeconds(worktree, adminDirectory, nowMs) {
|
|
169
|
+
const probes = [
|
|
170
|
+
worktree,
|
|
171
|
+
path.join(adminDirectory, "HEAD"),
|
|
172
|
+
path.join(adminDirectory, "index"),
|
|
173
|
+
path.join(adminDirectory, "gitdir"),
|
|
174
|
+
];
|
|
175
|
+
const times = await Promise.all(probes.map(probe => stat(probe).then(info => info.mtimeMs, () => 0)));
|
|
176
|
+
const newest = Math.max(0, ...times);
|
|
177
|
+
return newest === 0 ? 0 : Math.max(0, Math.floor((nowMs - newest) / 1000));
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Gather facts for many worktrees without spawning git for all of them at once.
|
|
181
|
+
* @param entries - Registered worktree entries
|
|
182
|
+
* @param context - Shared run inputs
|
|
183
|
+
* @param start - Batch start index
|
|
184
|
+
* @returns Facts in the original entry order
|
|
185
|
+
*/
|
|
186
|
+
export async function gatherAllWorktreeFacts(entries, context, start = 0) {
|
|
187
|
+
if (start >= entries.length)
|
|
188
|
+
return [];
|
|
189
|
+
const batch = entries.slice(start, start + INSPECTION_BATCH_SIZE);
|
|
190
|
+
return [
|
|
191
|
+
...(await Promise.all(batch.map(entry => gatherWorktreeFacts(entry, context)))),
|
|
192
|
+
...(await gatherAllWorktreeFacts(entries, context, start + INSPECTION_BATCH_SIZE)),
|
|
193
|
+
];
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=worktree-inventory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worktree-inventory.js","sourceRoot":"","sources":["../../src/cli/worktree-inventory.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixed absolute `lsof` locations, tried in order.
|
|
3
|
+
*
|
|
4
|
+
* Fixed paths rather than a bare command name so a writeable directory early on
|
|
5
|
+
* `PATH` cannot decide which binary answers a question whose answer authorizes
|
|
6
|
+
* a deletion.
|
|
7
|
+
*/
|
|
8
|
+
export declare const LSOF_CANDIDATES: readonly string[];
|
|
9
|
+
/**
|
|
10
|
+
* Machine-wide live working directories, or `undefined` when unknowable.
|
|
11
|
+
*
|
|
12
|
+
* `undefined` is not "none" — it is "not assessed", and every caller treats it
|
|
13
|
+
* as a refusal to proceed.
|
|
14
|
+
*/
|
|
15
|
+
export type LivenessProbe = () => Promise<readonly string[] | undefined>;
|
|
16
|
+
/**
|
|
17
|
+
* Report whether one path is the same as, or inside, another.
|
|
18
|
+
* @param parent - Containing directory
|
|
19
|
+
* @param child - Path being tested
|
|
20
|
+
* @returns True when `child` is `parent` or lives beneath it
|
|
21
|
+
*/
|
|
22
|
+
export declare function isInside(parent: string, child: string): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Count live processes whose working directory sits inside a worktree.
|
|
25
|
+
* @param worktree - Absolute worktree path
|
|
26
|
+
* @param workingDirectories - Live working directories from the probe
|
|
27
|
+
* @returns Number of processes holding the worktree open
|
|
28
|
+
*/
|
|
29
|
+
export declare function countLiveHolders(worktree: string, workingDirectories: readonly string[]): number;
|
|
30
|
+
/**
|
|
31
|
+
* Parse `lsof -F n` output into the working directories it reports.
|
|
32
|
+
* @param stdout - Raw `lsof` field output
|
|
33
|
+
* @returns Absolute directory paths, deduplicated
|
|
34
|
+
*/
|
|
35
|
+
export declare function parseLsofWorkingDirectories(stdout: string): readonly string[];
|
|
36
|
+
/**
|
|
37
|
+
* Probe the machine for every process working directory.
|
|
38
|
+
*
|
|
39
|
+
* `lsof` exits non-zero whenever any process could not be inspected, which is
|
|
40
|
+
* routine on a multi-user box, so the exit status is deliberately not the
|
|
41
|
+
* signal — the presence of parsed output is. That is the one place where
|
|
42
|
+
* reading a status would produce the wrong answer, and reading the content
|
|
43
|
+
* produces the right one.
|
|
44
|
+
*
|
|
45
|
+
* Candidates are tried one at a time rather than in parallel: a machine-wide
|
|
46
|
+
* `lsof` is a real cost, and four of them to answer one question is three too
|
|
47
|
+
* many on a box already under fleet load.
|
|
48
|
+
* @param index - Candidate to try, for the sequential fallback
|
|
49
|
+
* @returns Live working directories, or undefined when the probe could not run
|
|
50
|
+
*/
|
|
51
|
+
export declare function probeLiveWorkingDirectories(index?: number): Promise<readonly string[] | undefined>;
|
|
52
|
+
//# sourceMappingURL=worktree-liveness.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worktree-liveness.d.ts","sourceRoot":"","sources":["../../src/cli/worktree-liveness.ts"],"names":[],"mappings":"AA2BA;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,EAAE,SAAS,MAAM,EAK3C,CAAC;AAEH;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,OAAO,CAAC,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;AAEzE;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAQ/D;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,kBAAkB,EAAE,SAAS,MAAM,EAAE,GACpC,MAAM,CAGR;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAS7E;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,2BAA2B,CAC/C,KAAK,SAAI,GACR,OAAO,CAAC,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC,CAKxC"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live-process probe used to decide whether a worktree is still in use.
|
|
3
|
+
*
|
|
4
|
+
* This is the module that makes ownership provable rather than guessed. A
|
|
5
|
+
* worktree's path says nothing about who is using it — every agent on this
|
|
6
|
+
* machine parks its checkout under the same handful of roots — so a prefix
|
|
7
|
+
* match would reap a sibling's live work. What DOES distinguish them is whether
|
|
8
|
+
* a running process is sitting inside the directory right now.
|
|
9
|
+
*
|
|
10
|
+
* The probe is deliberately all-or-nothing. When it cannot run — no `lsof`, a
|
|
11
|
+
* timeout, an empty read — it reports `undefined` rather than an empty list,
|
|
12
|
+
* and the caller refuses every candidate. An empty list would mean "nothing is
|
|
13
|
+
* live", which is the false all-clear that turns a cleaner into a shredder.
|
|
14
|
+
* @module cli/worktree-liveness
|
|
15
|
+
*/
|
|
16
|
+
import { execFile } from "node:child_process";
|
|
17
|
+
import * as path from "node:path";
|
|
18
|
+
import { promisify } from "node:util";
|
|
19
|
+
const run = promisify(execFile);
|
|
20
|
+
/** Probe wall-clock budget. A machine-wide `lsof` is not instant. */
|
|
21
|
+
const PROBE_TIMEOUT_MS = 30_000;
|
|
22
|
+
/** Output ceiling for the probe. */
|
|
23
|
+
const PROBE_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
|
|
24
|
+
/**
|
|
25
|
+
* Fixed absolute `lsof` locations, tried in order.
|
|
26
|
+
*
|
|
27
|
+
* Fixed paths rather than a bare command name so a writeable directory early on
|
|
28
|
+
* `PATH` cannot decide which binary answers a question whose answer authorizes
|
|
29
|
+
* a deletion.
|
|
30
|
+
*/
|
|
31
|
+
export const LSOF_CANDIDATES = Object.freeze([
|
|
32
|
+
"/usr/sbin/lsof",
|
|
33
|
+
"/usr/bin/lsof",
|
|
34
|
+
"/bin/lsof",
|
|
35
|
+
"/opt/homebrew/bin/lsof",
|
|
36
|
+
]);
|
|
37
|
+
/**
|
|
38
|
+
* Report whether one path is the same as, or inside, another.
|
|
39
|
+
* @param parent - Containing directory
|
|
40
|
+
* @param child - Path being tested
|
|
41
|
+
* @returns True when `child` is `parent` or lives beneath it
|
|
42
|
+
*/
|
|
43
|
+
export function isInside(parent, child) {
|
|
44
|
+
const relative = path.relative(path.resolve(parent), path.resolve(child));
|
|
45
|
+
return (relative === "" ||
|
|
46
|
+
(!relative.startsWith(`..${path.sep}`) &&
|
|
47
|
+
relative !== ".." &&
|
|
48
|
+
!path.isAbsolute(relative)));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Count live processes whose working directory sits inside a worktree.
|
|
52
|
+
* @param worktree - Absolute worktree path
|
|
53
|
+
* @param workingDirectories - Live working directories from the probe
|
|
54
|
+
* @returns Number of processes holding the worktree open
|
|
55
|
+
*/
|
|
56
|
+
export function countLiveHolders(worktree, workingDirectories) {
|
|
57
|
+
return workingDirectories.filter(directory => isInside(worktree, directory))
|
|
58
|
+
.length;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Parse `lsof -F n` output into the working directories it reports.
|
|
62
|
+
* @param stdout - Raw `lsof` field output
|
|
63
|
+
* @returns Absolute directory paths, deduplicated
|
|
64
|
+
*/
|
|
65
|
+
export function parseLsofWorkingDirectories(stdout) {
|
|
66
|
+
return [
|
|
67
|
+
...new Set(stdout
|
|
68
|
+
.split("\n")
|
|
69
|
+
.filter(line => line.startsWith("n/"))
|
|
70
|
+
.map(line => line.slice(1))),
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Probe the machine for every process working directory.
|
|
75
|
+
*
|
|
76
|
+
* `lsof` exits non-zero whenever any process could not be inspected, which is
|
|
77
|
+
* routine on a multi-user box, so the exit status is deliberately not the
|
|
78
|
+
* signal — the presence of parsed output is. That is the one place where
|
|
79
|
+
* reading a status would produce the wrong answer, and reading the content
|
|
80
|
+
* produces the right one.
|
|
81
|
+
*
|
|
82
|
+
* Candidates are tried one at a time rather than in parallel: a machine-wide
|
|
83
|
+
* `lsof` is a real cost, and four of them to answer one question is three too
|
|
84
|
+
* many on a box already under fleet load.
|
|
85
|
+
* @param index - Candidate to try, for the sequential fallback
|
|
86
|
+
* @returns Live working directories, or undefined when the probe could not run
|
|
87
|
+
*/
|
|
88
|
+
export async function probeLiveWorkingDirectories(index = 0) {
|
|
89
|
+
const candidate = LSOF_CANDIDATES[index];
|
|
90
|
+
if (candidate === undefined)
|
|
91
|
+
return undefined;
|
|
92
|
+
const found = await runLsof(candidate);
|
|
93
|
+
return found ?? (await probeLiveWorkingDirectories(index + 1));
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Run one `lsof` candidate and parse its output.
|
|
97
|
+
* @param executable - Absolute `lsof` path to try
|
|
98
|
+
* @returns Parsed working directories, or undefined when this candidate failed
|
|
99
|
+
*/
|
|
100
|
+
async function runLsof(executable) {
|
|
101
|
+
const stdout = await run(executable, ["-a", "-d", "cwd", "-F", "n", "-w"], {
|
|
102
|
+
maxBuffer: PROBE_MAX_BUFFER_BYTES,
|
|
103
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
104
|
+
}).then(result => result.stdout, (error) => readPartialStdout(error));
|
|
105
|
+
const directories = parseLsofWorkingDirectories(stdout);
|
|
106
|
+
return directories.length === 0 ? undefined : directories;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Recover stdout from a failed `execFile` rejection.
|
|
110
|
+
*
|
|
111
|
+
* A timed-out child returns EMPTY streams, so this yields `""` and the caller
|
|
112
|
+
* treats the probe as unavailable rather than as a clean machine.
|
|
113
|
+
* @param error - Rejection from `execFile`
|
|
114
|
+
* @returns Whatever stdout the child produced before failing
|
|
115
|
+
*/
|
|
116
|
+
function readPartialStdout(error) {
|
|
117
|
+
return typeof error === "object" &&
|
|
118
|
+
error !== null &&
|
|
119
|
+
"stdout" in error &&
|
|
120
|
+
typeof error.stdout === "string"
|
|
121
|
+
? error.stdout
|
|
122
|
+
: "";
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=worktree-liveness.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worktree-liveness.js","sourceRoot":"","sources":["../../src/cli/worktree-liveness.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** Receipt filename inside a worktree's git admin directory. */
|
|
2
|
+
export declare const OWNER_RECEIPT_FILENAME = "lisa-worktree-owner.json";
|
|
3
|
+
/**
|
|
4
|
+
* Environment variables consulted, in order, for the caller's owner id.
|
|
5
|
+
*
|
|
6
|
+
* All of them are supplied by the surrounding agent runtime. None is invented
|
|
7
|
+
* by Lisa, because an id Lisa minted per invocation would never match a receipt
|
|
8
|
+
* written by an earlier invocation and would make every claim useless.
|
|
9
|
+
*/
|
|
10
|
+
export declare const OWNER_ID_VARIABLES: readonly string[];
|
|
11
|
+
/** What a receipt says about the caller's relationship to a worktree. */
|
|
12
|
+
export type OwnershipVerdict = "mine" | "theirs" | "unclaimed";
|
|
13
|
+
/** Persisted claim on one worktree. */
|
|
14
|
+
export interface OwnerReceipt {
|
|
15
|
+
/** Opaque id of the agent that claimed the worktree. */
|
|
16
|
+
readonly ownerId: string;
|
|
17
|
+
/** ISO timestamp the claim was written. */
|
|
18
|
+
readonly claimedAt: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the owner id of the process running the cleaner.
|
|
22
|
+
*
|
|
23
|
+
* Returns undefined when no runtime supplied one. Undefined is a real answer
|
|
24
|
+
* and a restrictive one: a caller with no id can never match a receipt, so
|
|
25
|
+
* every claimed worktree is somebody else's as far as it is concerned.
|
|
26
|
+
* @param env - Environment to read
|
|
27
|
+
* @returns Owner id, or undefined when none is available
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveCallerOwnerId(env?: NodeJS.ProcessEnv): string | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Judge a worktree's claim against the caller.
|
|
32
|
+
*
|
|
33
|
+
* A receipt naming somebody else is a hard "theirs" even when the caller has no
|
|
34
|
+
* id of its own — an unidentified caller proves nothing, and "I could not tell"
|
|
35
|
+
* must never resolve in favour of deletion.
|
|
36
|
+
* @param receiptOwner - Owner recorded in the receipt, if any
|
|
37
|
+
* @param callerOwner - Owner id of the running process, if any
|
|
38
|
+
* @returns Ownership verdict
|
|
39
|
+
*/
|
|
40
|
+
export declare function judgeOwnership(receiptOwner: string | undefined, callerOwner: string | undefined): OwnershipVerdict;
|
|
41
|
+
/**
|
|
42
|
+
* Absolute path of the receipt for one worktree admin directory.
|
|
43
|
+
* @param adminDirectory - Worktree's git admin directory
|
|
44
|
+
* @returns Absolute receipt path
|
|
45
|
+
*/
|
|
46
|
+
export declare function ownerReceiptPath(adminDirectory: string): string;
|
|
47
|
+
/**
|
|
48
|
+
* Read the owner recorded for one worktree.
|
|
49
|
+
*
|
|
50
|
+
* An unreadable or malformed receipt returns undefined rather than throwing:
|
|
51
|
+
* the caller then treats the worktree as unclaimed and falls back to the
|
|
52
|
+
* non-use gates, which are strictly more conservative than a claim.
|
|
53
|
+
* @param adminDirectory - Worktree's git admin directory
|
|
54
|
+
* @returns Recorded owner id, or undefined when there is no usable receipt
|
|
55
|
+
*/
|
|
56
|
+
export declare function readOwnerReceipt(adminDirectory: string): Promise<string | undefined>;
|
|
57
|
+
/**
|
|
58
|
+
* Write an ownership claim for one worktree.
|
|
59
|
+
* @param adminDirectory - Worktree's git admin directory
|
|
60
|
+
* @param ownerId - Owner id to record
|
|
61
|
+
* @param now - Clock, injectable for tests
|
|
62
|
+
* @returns Absolute path of the receipt written
|
|
63
|
+
*/
|
|
64
|
+
export declare function writeOwnerReceipt(adminDirectory: string, ownerId: string, now?: () => Date): Promise<string>;
|
|
65
|
+
//# sourceMappingURL=worktree-ownership.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worktree-ownership.d.ts","sourceRoot":"","sources":["../../src/cli/worktree-ownership.ts"],"names":[],"mappings":"AAkBA,gEAAgE;AAChE,eAAO,MAAM,sBAAsB,6BAA6B,CAAC;AAEjE;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,EAAE,SAAS,MAAM,EAI9C,CAAC;AAEH,yEAAyE;AACzE,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,QAAQ,GAAG,WAAW,CAAC;AAE/D,uCAAuC;AACvC,MAAM,WAAW,YAAY;IAC3B,wDAAwD;IACxD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,2CAA2C;IAC3C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,GAAE,MAAM,CAAC,UAAqC,GAChD,MAAM,GAAG,SAAS,CAMpB;AAeD;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,WAAW,EAAE,MAAM,GAAG,SAAS,GAC9B,gBAAgB,CAGlB;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAM7B;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,MAAM,EACf,GAAG,GAAE,MAAM,IAAuB,GACjC,OAAO,CAAC,MAAM,CAAC,CASjB"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ownership receipts for agent worktrees.
|
|
3
|
+
*
|
|
4
|
+
* A receipt is the only POSITIVE ownership evidence available: it records which
|
|
5
|
+
* agent claimed a worktree, so a cleaner can tell "mine" from "somebody
|
|
6
|
+
* else's" without inspecting a single path component.
|
|
7
|
+
*
|
|
8
|
+
* The receipt lives in the worktree's git ADMIN directory, never in the working
|
|
9
|
+
* tree. Two reasons, both load-bearing. A file in the working tree would dirty
|
|
10
|
+
* `git status`, which would make a claimed worktree permanently ineligible for
|
|
11
|
+
* the very cleanup the claim exists to authorize. And git removes the admin
|
|
12
|
+
* directory when the worktree is removed, so a receipt cannot outlive its
|
|
13
|
+
* subject and be inherited by an unrelated checkout at the same path.
|
|
14
|
+
* @module cli/worktree-ownership
|
|
15
|
+
*/
|
|
16
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
17
|
+
import * as path from "node:path";
|
|
18
|
+
/** Receipt filename inside a worktree's git admin directory. */
|
|
19
|
+
export const OWNER_RECEIPT_FILENAME = "lisa-worktree-owner.json";
|
|
20
|
+
/**
|
|
21
|
+
* Environment variables consulted, in order, for the caller's owner id.
|
|
22
|
+
*
|
|
23
|
+
* All of them are supplied by the surrounding agent runtime. None is invented
|
|
24
|
+
* by Lisa, because an id Lisa minted per invocation would never match a receipt
|
|
25
|
+
* written by an earlier invocation and would make every claim useless.
|
|
26
|
+
*/
|
|
27
|
+
export const OWNER_ID_VARIABLES = Object.freeze([
|
|
28
|
+
"LISA_OWNER_ID",
|
|
29
|
+
"CLAUDE_SESSION_ID",
|
|
30
|
+
"CODEX_SESSION_ID",
|
|
31
|
+
]);
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the owner id of the process running the cleaner.
|
|
34
|
+
*
|
|
35
|
+
* Returns undefined when no runtime supplied one. Undefined is a real answer
|
|
36
|
+
* and a restrictive one: a caller with no id can never match a receipt, so
|
|
37
|
+
* every claimed worktree is somebody else's as far as it is concerned.
|
|
38
|
+
* @param env - Environment to read
|
|
39
|
+
* @returns Owner id, or undefined when none is available
|
|
40
|
+
*/
|
|
41
|
+
export function resolveCallerOwnerId(env = readProcessEnvironment()) {
|
|
42
|
+
const name = OWNER_ID_VARIABLES.find(variable => {
|
|
43
|
+
const value = env[variable];
|
|
44
|
+
return typeof value === "string" && value.trim() !== "";
|
|
45
|
+
});
|
|
46
|
+
return name === undefined ? undefined : env[name].trim();
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Read the CLI process environment through one explicit, reviewable exception.
|
|
50
|
+
*
|
|
51
|
+
* The owner id is supplied by the surrounding agent runtime and by nothing
|
|
52
|
+
* else, so it has to be read from the externally-supplied environment. Every
|
|
53
|
+
* other function in this module takes the environment as a parameter.
|
|
54
|
+
* @returns Current process environment
|
|
55
|
+
*/
|
|
56
|
+
function readProcessEnvironment() {
|
|
57
|
+
// eslint-disable-next-line no-restricted-syntax -- the agent runtime supplies the owner id through process env and nowhere else
|
|
58
|
+
return process.env;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Judge a worktree's claim against the caller.
|
|
62
|
+
*
|
|
63
|
+
* A receipt naming somebody else is a hard "theirs" even when the caller has no
|
|
64
|
+
* id of its own — an unidentified caller proves nothing, and "I could not tell"
|
|
65
|
+
* must never resolve in favour of deletion.
|
|
66
|
+
* @param receiptOwner - Owner recorded in the receipt, if any
|
|
67
|
+
* @param callerOwner - Owner id of the running process, if any
|
|
68
|
+
* @returns Ownership verdict
|
|
69
|
+
*/
|
|
70
|
+
export function judgeOwnership(receiptOwner, callerOwner) {
|
|
71
|
+
if (receiptOwner === undefined || receiptOwner === "")
|
|
72
|
+
return "unclaimed";
|
|
73
|
+
return receiptOwner === callerOwner ? "mine" : "theirs";
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Absolute path of the receipt for one worktree admin directory.
|
|
77
|
+
* @param adminDirectory - Worktree's git admin directory
|
|
78
|
+
* @returns Absolute receipt path
|
|
79
|
+
*/
|
|
80
|
+
export function ownerReceiptPath(adminDirectory) {
|
|
81
|
+
return path.join(adminDirectory, OWNER_RECEIPT_FILENAME);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Read the owner recorded for one worktree.
|
|
85
|
+
*
|
|
86
|
+
* An unreadable or malformed receipt returns undefined rather than throwing:
|
|
87
|
+
* the caller then treats the worktree as unclaimed and falls back to the
|
|
88
|
+
* non-use gates, which are strictly more conservative than a claim.
|
|
89
|
+
* @param adminDirectory - Worktree's git admin directory
|
|
90
|
+
* @returns Recorded owner id, or undefined when there is no usable receipt
|
|
91
|
+
*/
|
|
92
|
+
export async function readOwnerReceipt(adminDirectory) {
|
|
93
|
+
const raw = await readFile(ownerReceiptPath(adminDirectory), "utf8").catch(() => undefined);
|
|
94
|
+
if (raw === undefined)
|
|
95
|
+
return undefined;
|
|
96
|
+
return parseOwnerId(raw);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Write an ownership claim for one worktree.
|
|
100
|
+
* @param adminDirectory - Worktree's git admin directory
|
|
101
|
+
* @param ownerId - Owner id to record
|
|
102
|
+
* @param now - Clock, injectable for tests
|
|
103
|
+
* @returns Absolute path of the receipt written
|
|
104
|
+
*/
|
|
105
|
+
export async function writeOwnerReceipt(adminDirectory, ownerId, now = () => new Date()) {
|
|
106
|
+
const receipt = {
|
|
107
|
+
ownerId,
|
|
108
|
+
claimedAt: now().toISOString(),
|
|
109
|
+
};
|
|
110
|
+
const target = ownerReceiptPath(adminDirectory);
|
|
111
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
112
|
+
await writeFile(target, `${JSON.stringify(receipt, undefined, 2)}\n`, "utf8");
|
|
113
|
+
return target;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Extract an owner id from raw receipt JSON.
|
|
117
|
+
* @param raw - Receipt file contents
|
|
118
|
+
* @returns Owner id, or undefined when the receipt is unusable
|
|
119
|
+
*/
|
|
120
|
+
function parseOwnerId(raw) {
|
|
121
|
+
const parsed = safeParse(raw);
|
|
122
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
123
|
+
return undefined;
|
|
124
|
+
const owner = parsed.ownerId;
|
|
125
|
+
return typeof owner === "string" && owner.trim() !== ""
|
|
126
|
+
? owner.trim()
|
|
127
|
+
: undefined;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Parse JSON without throwing.
|
|
131
|
+
* @param raw - Candidate JSON text
|
|
132
|
+
* @returns Parsed value, or undefined when parsing failed
|
|
133
|
+
*/
|
|
134
|
+
function safeParse(raw) {
|
|
135
|
+
try {
|
|
136
|
+
return JSON.parse(raw);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=worktree-ownership.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worktree-ownership.js","sourceRoot":"","sources":["../../src/cli/worktree-ownership.ts"],"names":[],"mappings":"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"}
|