@codyswann/lisa 4.10.14 → 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 (121) hide show
  1. package/all/copy-overwrite/scripts/lisa-hooks/block-no-verify.sh +4 -1
  2. package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +132 -43
  3. package/dist/cli/doctor-worktree-hygiene.d.ts +5 -2
  4. package/dist/cli/doctor-worktree-hygiene.d.ts.map +1 -1
  5. package/dist/cli/doctor-worktree-hygiene.js +12 -6
  6. package/dist/cli/doctor-worktree-hygiene.js.map +1 -1
  7. package/dist/cli/index.d.ts.map +1 -1
  8. package/dist/cli/index.js +2 -0
  9. package/dist/cli/index.js.map +1 -1
  10. package/dist/cli/prune-commands.d.ts +52 -0
  11. package/dist/cli/prune-commands.d.ts.map +1 -0
  12. package/dist/cli/prune-commands.js +158 -0
  13. package/dist/cli/prune-commands.js.map +1 -0
  14. package/dist/cli/prune-report.d.ts +40 -0
  15. package/dist/cli/prune-report.d.ts.map +1 -0
  16. package/dist/cli/prune-report.js +81 -0
  17. package/dist/cli/prune-report.js.map +1 -0
  18. package/dist/cli/stash-prune-policy.d.ts +72 -0
  19. package/dist/cli/stash-prune-policy.d.ts.map +1 -0
  20. package/dist/cli/stash-prune-policy.js +90 -0
  21. package/dist/cli/stash-prune-policy.js.map +1 -0
  22. package/dist/cli/stash-prune.d.ts +104 -0
  23. package/dist/cli/stash-prune.d.ts.map +1 -0
  24. package/dist/cli/stash-prune.js +206 -0
  25. package/dist/cli/stash-prune.js.map +1 -0
  26. package/dist/cli/worktree-inventory.d.ts +82 -0
  27. package/dist/cli/worktree-inventory.d.ts.map +1 -0
  28. package/dist/cli/worktree-inventory.js +195 -0
  29. package/dist/cli/worktree-inventory.js.map +1 -0
  30. package/dist/cli/worktree-liveness.d.ts +52 -0
  31. package/dist/cli/worktree-liveness.d.ts.map +1 -0
  32. package/dist/cli/worktree-liveness.js +124 -0
  33. package/dist/cli/worktree-liveness.js.map +1 -0
  34. package/dist/cli/worktree-ownership.d.ts +65 -0
  35. package/dist/cli/worktree-ownership.d.ts.map +1 -0
  36. package/dist/cli/worktree-ownership.js +142 -0
  37. package/dist/cli/worktree-ownership.js.map +1 -0
  38. package/dist/cli/worktree-prune-policy.d.ts +76 -0
  39. package/dist/cli/worktree-prune-policy.d.ts.map +1 -0
  40. package/dist/cli/worktree-prune-policy.js +114 -0
  41. package/dist/cli/worktree-prune-policy.js.map +1 -0
  42. package/dist/cli/worktree-prune.d.ts +96 -0
  43. package/dist/cli/worktree-prune.d.ts.map +1 -0
  44. package/dist/cli/worktree-prune.js +153 -0
  45. package/dist/cli/worktree-prune.js.map +1 -0
  46. package/dist/codex/scripts/block-no-verify.sh +4 -1
  47. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  48. package/dist/core/lisa-owned-hash-ledger.js +4 -0
  49. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  50. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  51. package/dist/core/upstream-evidence-manifest.js +23 -6
  52. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  53. package/dist/opencode/plugin-templates/block-no-verify.sh +4 -1
  54. package/dist/opencode/plugin-templates/parity-safety-net.sh +132 -43
  55. package/package.json +1 -1
  56. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  58. package/plugins/lisa/hooks/block-no-verify.agy.sh +4 -1
  59. package/plugins/lisa/hooks/block-no-verify.sh +4 -1
  60. package/plugins/lisa/hooks/parity-safety-net.sh +132 -43
  61. package/plugins/lisa/rules/reference/base-rules.md +1 -1
  62. package/plugins/lisa-agy/hooks/block-no-verify.agy.sh +4 -1
  63. package/plugins/lisa-agy/hooks/parity-safety-net.sh +132 -43
  64. package/plugins/lisa-agy/plugin.json +1 -1
  65. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  67. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  68. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-copilot/hooks/block-no-verify.sh +4 -1
  72. package/plugins/lisa-copilot/hooks/parity-safety-net.sh +132 -43
  73. package/plugins/lisa-copilot/rules/reference/base-rules.md +1 -1
  74. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-cursor/hooks/block-no-verify.sh +4 -1
  76. package/plugins/lisa-cursor/hooks/parity-safety-net.sh +132 -43
  77. package/plugins/lisa-cursor/rules/base-rules-reference.mdc +1 -1
  78. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  80. package/plugins/lisa-expo-agy/plugin.json +1 -1
  81. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  82. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  83. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  84. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  85. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  86. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  87. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  88. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  90. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  91. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  92. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  93. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  94. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  95. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  96. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  97. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  98. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  99. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  100. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  101. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  102. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  103. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  104. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  105. package/plugins/lisa-rails-agy/plugin.json +1 -1
  106. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  107. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  108. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  109. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  110. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  111. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  112. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  113. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  114. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  115. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  116. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  117. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  118. package/plugins/src/base/hooks/block-no-verify.agy.sh +4 -1
  119. package/plugins/src/base/hooks/block-no-verify.sh +4 -1
  120. package/plugins/src/base/hooks/parity-safety-net.sh +132 -43
  121. package/plugins/src/base/rules/reference/base-rules.md +1 -1
@@ -0,0 +1,82 @@
1
+ import type { WorktreeFacts } from "./worktree-prune-policy.js";
2
+ /** One entry as git itself reports it. */
3
+ export interface WorktreeEntry {
4
+ /** Absolute worktree path. */
5
+ readonly path: string;
6
+ /** Checked-out branch, or undefined when detached. */
7
+ readonly branch?: string;
8
+ /** Whether git reports the entry locked. */
9
+ readonly locked: boolean;
10
+ /** Whether git reports the entry's directory as gone. */
11
+ readonly prunable: boolean;
12
+ /** Whether this is the primary checkout (always the first entry). */
13
+ readonly isPrimary: boolean;
14
+ }
15
+ /**
16
+ * Parse `git worktree list --porcelain` output.
17
+ *
18
+ * The first record is always the primary checkout, which is how the primary is
19
+ * identified — not by comparing it against a path the caller supplied, which
20
+ * would be wrong the moment the caller ran from a worktree.
21
+ * @param stdout - Porcelain output
22
+ * @returns Parsed entries in git's order
23
+ */
24
+ export declare function parseWorktreeList(stdout: string): readonly WorktreeEntry[];
25
+ /**
26
+ * Run one bounded git command.
27
+ * @param args - Git arguments
28
+ * @param cwd - Directory to run git in
29
+ * @returns Trimmed stdout
30
+ */
31
+ export declare function git(args: readonly string[], cwd: string): Promise<string>;
32
+ /**
33
+ * List every worktree registered for a checkout.
34
+ * @param repoPath - Any path inside the repository
35
+ * @returns Registered entries
36
+ */
37
+ export declare function listWorktrees(repoPath: string): Promise<readonly WorktreeEntry[]>;
38
+ /** The half of the facts that only git and the filesystem can answer. */
39
+ export interface WorktreeGitState {
40
+ /** Ownership verdict from the receipt check. */
41
+ readonly ownership: WorktreeFacts["ownership"];
42
+ /** Seconds since the worktree last showed activity. */
43
+ readonly idleSeconds: number;
44
+ /** Modified, staged, added, deleted, or renamed TRACKED files. */
45
+ readonly trackedChanges: number;
46
+ /** Untracked, non-ignored files. */
47
+ readonly untrackedFiles: number;
48
+ /** Commits present locally and on no remote. */
49
+ readonly unpushedCommits: number;
50
+ }
51
+ /** Inputs shared by every fact-gathering call in one run. */
52
+ export interface GatherContext {
53
+ /** Live working directories, or undefined when the probe could not run. */
54
+ readonly liveWorkingDirectories: readonly string[] | undefined;
55
+ /** Owner id of the process running the cleaner. */
56
+ readonly callerOwnerId: string | undefined;
57
+ /** Absolute path the cleaner is running from. */
58
+ readonly currentPath: string;
59
+ /** Clock in epoch milliseconds. */
60
+ readonly nowMs: number;
61
+ }
62
+ /**
63
+ * Gather every fact the entitlement rule is allowed to consider.
64
+ *
65
+ * A worktree git cannot report on yields `readable: false` with zeroed counts.
66
+ * The zeros are never read as "holds nothing" because `readable: false` is
67
+ * itself a blocker — the same reason the doctor check returns undefined for an
68
+ * unreadable worktree instead of a zeroed exposure.
69
+ * @param entry - Registered worktree entry
70
+ * @param context - Shared run inputs
71
+ * @returns Facts for the entitlement rule
72
+ */
73
+ export declare function gatherWorktreeFacts(entry: WorktreeEntry, context: GatherContext): Promise<WorktreeFacts>;
74
+ /**
75
+ * Gather facts for many worktrees without spawning git for all of them at once.
76
+ * @param entries - Registered worktree entries
77
+ * @param context - Shared run inputs
78
+ * @param start - Batch start index
79
+ * @returns Facts in the original entry order
80
+ */
81
+ export declare function gatherAllWorktreeFacts(entries: readonly WorktreeEntry[], context: GatherContext, start?: number): Promise<readonly WorktreeFacts[]>;
82
+ //# sourceMappingURL=worktree-inventory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worktree-inventory.d.ts","sourceRoot":"","sources":["../../src/cli/worktree-inventory.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAOhE,0CAA0C;AAC1C,MAAM,WAAW,aAAa;IAC5B,8BAA8B;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,sDAAsD;IACtD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,4CAA4C;IAC5C,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,yDAAyD;IACzD,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,qEAAqE;IACrE,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,aAAa,EAAE,CAM1E;AA+BD;;;;;GAKG;AACH,wBAAsB,GAAG,CACvB,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,MAAM,CAAC,CAOjB;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC,CAInC;AAED,yEAAyE;AACzE,MAAM,WAAW,gBAAgB;IAC/B,gDAAgD;IAChD,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC,WAAW,CAAC,CAAC;IAC/C,uDAAuD;IACvD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,kEAAkE;IAClE,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,oCAAoC;IACpC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,gDAAgD;IAChD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC;AAED,6DAA6D;AAC7D,MAAM,WAAW,aAAa;IAC5B,2EAA2E;IAC3E,QAAQ,CAAC,sBAAsB,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IAC/D,mDAAmD;IACnD,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3C,iDAAiD;IACjD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,mCAAmC;IACnC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,aAAa,EACpB,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,aAAa,CAAC,CAgCxB;AAuFD;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,SAAS,aAAa,EAAE,EACjC,OAAO,EAAE,aAAa,EACtB,KAAK,SAAI,GACR,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC,CAanC"}
@@ -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"}