@codyswann/lisa 4.10.15 → 4.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/dist/cli/doctor-worktree-hygiene.d.ts +5 -2
  2. package/dist/cli/doctor-worktree-hygiene.d.ts.map +1 -1
  3. package/dist/cli/doctor-worktree-hygiene.js +12 -6
  4. package/dist/cli/doctor-worktree-hygiene.js.map +1 -1
  5. package/dist/cli/index.d.ts.map +1 -1
  6. package/dist/cli/index.js +2 -0
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/cli/prune-commands.d.ts +52 -0
  9. package/dist/cli/prune-commands.d.ts.map +1 -0
  10. package/dist/cli/prune-commands.js +158 -0
  11. package/dist/cli/prune-commands.js.map +1 -0
  12. package/dist/cli/prune-report.d.ts +40 -0
  13. package/dist/cli/prune-report.d.ts.map +1 -0
  14. package/dist/cli/prune-report.js +81 -0
  15. package/dist/cli/prune-report.js.map +1 -0
  16. package/dist/cli/stash-prune-policy.d.ts +72 -0
  17. package/dist/cli/stash-prune-policy.d.ts.map +1 -0
  18. package/dist/cli/stash-prune-policy.js +90 -0
  19. package/dist/cli/stash-prune-policy.js.map +1 -0
  20. package/dist/cli/stash-prune.d.ts +104 -0
  21. package/dist/cli/stash-prune.d.ts.map +1 -0
  22. package/dist/cli/stash-prune.js +206 -0
  23. package/dist/cli/stash-prune.js.map +1 -0
  24. package/dist/cli/worktree-inventory.d.ts +82 -0
  25. package/dist/cli/worktree-inventory.d.ts.map +1 -0
  26. package/dist/cli/worktree-inventory.js +195 -0
  27. package/dist/cli/worktree-inventory.js.map +1 -0
  28. package/dist/cli/worktree-liveness.d.ts +52 -0
  29. package/dist/cli/worktree-liveness.d.ts.map +1 -0
  30. package/dist/cli/worktree-liveness.js +124 -0
  31. package/dist/cli/worktree-liveness.js.map +1 -0
  32. package/dist/cli/worktree-ownership.d.ts +65 -0
  33. package/dist/cli/worktree-ownership.d.ts.map +1 -0
  34. package/dist/cli/worktree-ownership.js +142 -0
  35. package/dist/cli/worktree-ownership.js.map +1 -0
  36. package/dist/cli/worktree-prune-policy.d.ts +76 -0
  37. package/dist/cli/worktree-prune-policy.d.ts.map +1 -0
  38. package/dist/cli/worktree-prune-policy.js +114 -0
  39. package/dist/cli/worktree-prune-policy.js.map +1 -0
  40. package/dist/cli/worktree-prune.d.ts +96 -0
  41. package/dist/cli/worktree-prune.d.ts.map +1 -0
  42. package/dist/cli/worktree-prune.js +153 -0
  43. package/dist/cli/worktree-prune.js.map +1 -0
  44. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  45. package/dist/core/upstream-evidence-manifest.js +16 -0
  46. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  47. package/package.json +1 -1
  48. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  50. package/plugins/lisa-agy/plugin.json +1 -1
  51. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  53. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  54. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  60. package/plugins/lisa-expo-agy/plugin.json +1 -1
  61. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  65. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  66. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  70. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  71. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  73. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  75. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  76. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  78. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  80. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  81. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  82. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  83. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  84. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  85. package/plugins/lisa-rails-agy/plugin.json +1 -1
  86. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  87. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  88. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  90. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  91. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  92. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  93. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  94. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  95. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  96. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  97. 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":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC,MAAM,GAAG,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAEhC,qEAAqE;AACrE,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAEhC,oCAAoC;AACpC,MAAM,sBAAsB,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAEhD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,eAAe,GAAsB,MAAM,CAAC,MAAM,CAAC;IAC9D,gBAAgB;IAChB,eAAe;IACf,WAAW;IACX,wBAAwB;CACzB,CAAC,CAAC;AAUH;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CAAC,MAAc,EAAE,KAAa;IACpD,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;IAC1E,OAAO,CACL,QAAQ,KAAK,EAAE;QACf,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,KAAK,IAAI,CAAC,GAAG,EAAE,CAAC;YACpC,QAAQ,KAAK,IAAI;YACjB,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAC9B,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAC9B,QAAgB,EAChB,kBAAqC;IAErC,OAAO,kBAAkB,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;SACzE,MAAM,CAAC;AACZ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,2BAA2B,CAAC,MAAc;IACxD,OAAO;QACL,GAAG,IAAI,GAAG,CACR,MAAM;aACH,KAAK,CAAC,IAAI,CAAC;aACX,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;aACrC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAC9B;KACF,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,2BAA2B,CAC/C,KAAK,GAAG,CAAC;IAET,MAAM,SAAS,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;IACzC,IAAI,SAAS,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC9C,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,SAAS,CAAC,CAAC;IACvC,OAAO,KAAK,IAAI,CAAC,MAAM,2BAA2B,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC;AACjE,CAAC;AAED;;;;GAIG;AACH,KAAK,UAAU,OAAO,CACpB,UAAkB;IAElB,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,EAAE;QACzE,SAAS,EAAE,sBAAsB;QACjC,OAAO,EAAE,gBAAgB;KAC1B,CAAC,CAAC,IAAI,CACL,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,EACvB,CAAC,KAAc,EAAE,EAAE,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAC7C,CAAC;IACF,MAAM,WAAW,GAAG,2BAA2B,CAAC,MAAM,CAAC,CAAC;IACxD,OAAO,WAAW,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC;AAC5D,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,iBAAiB,CAAC,KAAc;IACvC,OAAO,OAAO,KAAK,KAAK,QAAQ;QAC9B,KAAK,KAAK,IAAI;QACd,QAAQ,IAAI,KAAK;QACjB,OAAQ,KAA8B,CAAC,MAAM,KAAK,QAAQ;QAC1D,CAAC,CAAE,KAA4B,CAAC,MAAM;QACtC,CAAC,CAAC,EAAE,CAAC;AACT,CAAC"}
@@ -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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"}