@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.
- package/all/copy-overwrite/scripts/lisa-hooks/block-no-verify.sh +4 -1
- package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +132 -43
- 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 +40 -0
- package/dist/cli/prune-report.d.ts.map +1 -0
- package/dist/cli/prune-report.js +81 -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/codex/scripts/block-no-verify.sh +4 -1
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +4 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +23 -6
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/block-no-verify.sh +4 -1
- package/dist/opencode/plugin-templates/parity-safety-net.sh +132 -43
- 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/hooks/block-no-verify.agy.sh +4 -1
- package/plugins/lisa/hooks/block-no-verify.sh +4 -1
- package/plugins/lisa/hooks/parity-safety-net.sh +132 -43
- package/plugins/lisa/rules/reference/base-rules.md +1 -1
- package/plugins/lisa-agy/hooks/block-no-verify.agy.sh +4 -1
- package/plugins/lisa-agy/hooks/parity-safety-net.sh +132 -43
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/hooks/block-no-verify.sh +4 -1
- package/plugins/lisa-copilot/hooks/parity-safety-net.sh +132 -43
- package/plugins/lisa-copilot/rules/reference/base-rules.md +1 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/block-no-verify.sh +4 -1
- package/plugins/lisa-cursor/hooks/parity-safety-net.sh +132 -43
- package/plugins/lisa-cursor/rules/base-rules-reference.mdc +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
- package/plugins/src/base/hooks/block-no-verify.agy.sh +4 -1
- package/plugins/src/base/hooks/block-no-verify.sh +4 -1
- package/plugins/src/base/hooks/parity-safety-net.sh +132 -43
- package/plugins/src/base/rules/reference/base-rules.md +1 -1
|
@@ -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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"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The entitlement rule for removing an agent worktree.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately pure: every fact is gathered elsewhere and handed in, so the
|
|
5
|
+
* decision that authorizes a deletion is a function that can be exhaustively
|
|
6
|
+
* tested and mutation-tested rather than a side effect of a shell pipeline.
|
|
7
|
+
*
|
|
8
|
+
* The rule is **entitlement by proof, never by location.** Nothing here looks
|
|
9
|
+
* at where a worktree lives. `$TMPDIR` and the worktree roots are shared by
|
|
10
|
+
* every agent on the machine, so a path prefix identifies a neighbourhood, not
|
|
11
|
+
* an owner; reaping on one deletes siblings' live work. A worktree is the
|
|
12
|
+
* caller's to remove only when it is claimed by the caller, or when no other
|
|
13
|
+
* party can possibly have a claim on it — no live process inside it, no recent
|
|
14
|
+
* activity, and nothing in it that exists nowhere else.
|
|
15
|
+
* @module cli/worktree-prune-policy
|
|
16
|
+
*/
|
|
17
|
+
/** Why one worktree was refused. Every value names a specific unmet proof. */
|
|
18
|
+
export type WorktreeBlocker = "primary-checkout" | "current-worktree" | "git-locked" | "claimed-by-another-owner" | "liveness-unknown" | "in-use" | "recently-active" | "unpushed-commits" | "uncommitted-changes" | "unreadable";
|
|
19
|
+
/** Everything the rule is allowed to consider about one worktree. */
|
|
20
|
+
export interface WorktreeFacts {
|
|
21
|
+
/** Absolute worktree path. */
|
|
22
|
+
readonly path: string;
|
|
23
|
+
/** Checked-out branch, or undefined when detached. */
|
|
24
|
+
readonly branch?: string;
|
|
25
|
+
/** Whether this is the repository's primary checkout. */
|
|
26
|
+
readonly isPrimary: boolean;
|
|
27
|
+
/** Whether the cleaner itself is running inside this worktree. */
|
|
28
|
+
readonly isCurrent: boolean;
|
|
29
|
+
/** Whether git reports the worktree as locked. */
|
|
30
|
+
readonly locked: boolean;
|
|
31
|
+
/** Whether git could report on the worktree at all. */
|
|
32
|
+
readonly readable: boolean;
|
|
33
|
+
/** Ownership verdict from the receipt check. */
|
|
34
|
+
readonly ownership: "mine" | "theirs" | "unclaimed";
|
|
35
|
+
/** Live processes whose working directory is inside, or undefined if unknown. */
|
|
36
|
+
readonly liveHolders: number | undefined;
|
|
37
|
+
/** Seconds since the worktree last showed activity. */
|
|
38
|
+
readonly idleSeconds: number;
|
|
39
|
+
/** Modified, staged, added, deleted, or renamed TRACKED files. */
|
|
40
|
+
readonly trackedChanges: number;
|
|
41
|
+
/** Untracked, non-ignored files. */
|
|
42
|
+
readonly untrackedFiles: number;
|
|
43
|
+
/** Commits present locally and on no remote. */
|
|
44
|
+
readonly unpushedCommits: number;
|
|
45
|
+
}
|
|
46
|
+
/** Tunable part of the rule. */
|
|
47
|
+
export interface WorktreePrunePolicy {
|
|
48
|
+
/** Quiescence a worktree must show before an unclaimed one is eligible. */
|
|
49
|
+
readonly minIdleSeconds: number;
|
|
50
|
+
}
|
|
51
|
+
/** Result of applying the rule to one worktree. */
|
|
52
|
+
export interface WorktreeVerdict {
|
|
53
|
+
/** Absolute worktree path. */
|
|
54
|
+
readonly path: string;
|
|
55
|
+
/** Whether the worktree may be removed. */
|
|
56
|
+
readonly eligible: boolean;
|
|
57
|
+
/** Every unmet proof, most severe first. */
|
|
58
|
+
readonly blockers: readonly WorktreeBlocker[];
|
|
59
|
+
}
|
|
60
|
+
/** Default quiescence window: one day of no activity. */
|
|
61
|
+
export declare const DEFAULT_MIN_IDLE_SECONDS: number;
|
|
62
|
+
/** Operator-readable explanation for each blocker. */
|
|
63
|
+
export declare const BLOCKER_EXPLANATIONS: Readonly<Record<WorktreeBlocker, string>>;
|
|
64
|
+
/**
|
|
65
|
+
* Decide whether one worktree may be removed, and say why not when it may not.
|
|
66
|
+
*
|
|
67
|
+
* Blockers are returned in full rather than short-circuiting at the first
|
|
68
|
+
* failure. A worktree refused for three reasons and a worktree refused for one
|
|
69
|
+
* need different remedies, and reporting only the first trains a reader to fix
|
|
70
|
+
* one thing and re-run into the next refusal.
|
|
71
|
+
* @param facts - Gathered facts about the worktree
|
|
72
|
+
* @param policy - Tunable thresholds
|
|
73
|
+
* @returns Verdict with every unmet proof
|
|
74
|
+
*/
|
|
75
|
+
export declare function classifyWorktree(facts: WorktreeFacts, policy: WorktreePrunePolicy): WorktreeVerdict;
|
|
76
|
+
//# sourceMappingURL=worktree-prune-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worktree-prune-policy.d.ts","sourceRoot":"","sources":["../../src/cli/worktree-prune-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,8EAA8E;AAC9E,MAAM,MAAM,eAAe,GACvB,kBAAkB,GAClB,kBAAkB,GAClB,YAAY,GACZ,0BAA0B,GAC1B,kBAAkB,GAClB,QAAQ,GACR,iBAAiB,GACjB,kBAAkB,GAClB,qBAAqB,GACrB,YAAY,CAAC;AAEjB,qEAAqE;AACrE,MAAM,WAAW,aAAa;IAC5B,8BAA8B;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,sDAAsD;IACtD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,yDAAyD;IACzD,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,kEAAkE;IAClE,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,kDAAkD;IAClD,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,uDAAuD;IACvD,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,gDAAgD;IAChD,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,QAAQ,GAAG,WAAW,CAAC;IACpD,iFAAiF;IACjF,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IACzC,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,gCAAgC;AAChC,MAAM,WAAW,mBAAmB;IAClC,2EAA2E;IAC3E,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAED,mDAAmD;AACnD,MAAM,WAAW,eAAe;IAC9B,8BAA8B;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,2CAA2C;IAC3C,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,4CAA4C;IAC5C,QAAQ,CAAC,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAC;CAC/C;AAED,yDAAyD;AACzD,eAAO,MAAM,wBAAwB,QAAe,CAAC;AAErD,sDAAsD;AACtD,eAAO,MAAM,oBAAoB,EAAE,QAAQ,CAAC,MAAM,CAAC,eAAe,EAAE,MAAM,CAAC,CAgBvE,CAAC;AAEL;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,aAAa,EACpB,MAAM,EAAE,mBAAmB,GAC1B,eAAe,CAOjB"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The entitlement rule for removing an agent worktree.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately pure: every fact is gathered elsewhere and handed in, so the
|
|
5
|
+
* decision that authorizes a deletion is a function that can be exhaustively
|
|
6
|
+
* tested and mutation-tested rather than a side effect of a shell pipeline.
|
|
7
|
+
*
|
|
8
|
+
* The rule is **entitlement by proof, never by location.** Nothing here looks
|
|
9
|
+
* at where a worktree lives. `$TMPDIR` and the worktree roots are shared by
|
|
10
|
+
* every agent on the machine, so a path prefix identifies a neighbourhood, not
|
|
11
|
+
* an owner; reaping on one deletes siblings' live work. A worktree is the
|
|
12
|
+
* caller's to remove only when it is claimed by the caller, or when no other
|
|
13
|
+
* party can possibly have a claim on it — no live process inside it, no recent
|
|
14
|
+
* activity, and nothing in it that exists nowhere else.
|
|
15
|
+
* @module cli/worktree-prune-policy
|
|
16
|
+
*/
|
|
17
|
+
/** Default quiescence window: one day of no activity. */
|
|
18
|
+
export const DEFAULT_MIN_IDLE_SECONDS = 24 * 60 * 60;
|
|
19
|
+
/** Operator-readable explanation for each blocker. */
|
|
20
|
+
export const BLOCKER_EXPLANATIONS = Object.freeze({
|
|
21
|
+
"primary-checkout": "this is the repository's primary checkout",
|
|
22
|
+
"current-worktree": "the cleaner is running inside it",
|
|
23
|
+
"git-locked": "git reports it locked, which means somebody claimed it",
|
|
24
|
+
"claimed-by-another-owner": "its ownership receipt names a different agent, so it is not yours to remove",
|
|
25
|
+
"liveness-unknown": "the live-process probe could not run, so nothing could be proven idle",
|
|
26
|
+
"in-use": "a live process is working inside it right now",
|
|
27
|
+
"recently-active": "it showed activity too recently to be called abandoned",
|
|
28
|
+
"unpushed-commits": "it holds commits that exist on no remote — push them first",
|
|
29
|
+
"uncommitted-changes": "it holds uncommitted work — commit and push, or stash it, first",
|
|
30
|
+
unreadable: "git could not report on it, so nothing about it was proven",
|
|
31
|
+
});
|
|
32
|
+
/**
|
|
33
|
+
* Decide whether one worktree may be removed, and say why not when it may not.
|
|
34
|
+
*
|
|
35
|
+
* Blockers are returned in full rather than short-circuiting at the first
|
|
36
|
+
* failure. A worktree refused for three reasons and a worktree refused for one
|
|
37
|
+
* need different remedies, and reporting only the first trains a reader to fix
|
|
38
|
+
* one thing and re-run into the next refusal.
|
|
39
|
+
* @param facts - Gathered facts about the worktree
|
|
40
|
+
* @param policy - Tunable thresholds
|
|
41
|
+
* @returns Verdict with every unmet proof
|
|
42
|
+
*/
|
|
43
|
+
export function classifyWorktree(facts, policy) {
|
|
44
|
+
const blockers = [
|
|
45
|
+
...structuralBlockers(facts),
|
|
46
|
+
...idlenessBlockers(facts, policy),
|
|
47
|
+
...workAtRiskBlockers(facts),
|
|
48
|
+
];
|
|
49
|
+
return { path: facts.path, eligible: blockers.length === 0, blockers };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Blockers that make a worktree ineligible regardless of its contents.
|
|
53
|
+
* @param facts - Gathered facts about the worktree
|
|
54
|
+
* @returns Structural blockers
|
|
55
|
+
*/
|
|
56
|
+
function structuralBlockers(facts) {
|
|
57
|
+
const candidates = [
|
|
58
|
+
facts.readable ? undefined : "unreadable",
|
|
59
|
+
facts.isPrimary ? "primary-checkout" : undefined,
|
|
60
|
+
facts.isCurrent ? "current-worktree" : undefined,
|
|
61
|
+
facts.locked ? "git-locked" : undefined,
|
|
62
|
+
facts.ownership === "theirs"
|
|
63
|
+
? "claimed-by-another-owner"
|
|
64
|
+
: undefined,
|
|
65
|
+
];
|
|
66
|
+
return candidates.filter((blocker) => blocker !== undefined);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Blockers derived from whether anybody could still be using the worktree.
|
|
70
|
+
*
|
|
71
|
+
* A matching ownership receipt waives the quiescence window and NOTHING else.
|
|
72
|
+
* An owner asking to clean its own worktree has said what the window is there
|
|
73
|
+
* to infer. It never waives `in-use`: a claim says the work is finished, not
|
|
74
|
+
* that no process is still standing in the directory.
|
|
75
|
+
* @param facts - Gathered facts about the worktree
|
|
76
|
+
* @param policy - Tunable thresholds
|
|
77
|
+
* @returns Non-use blockers
|
|
78
|
+
*/
|
|
79
|
+
function idlenessBlockers(facts, policy) {
|
|
80
|
+
const liveness = facts.liveHolders === undefined
|
|
81
|
+
? "liveness-unknown"
|
|
82
|
+
: facts.liveHolders > 0
|
|
83
|
+
? "in-use"
|
|
84
|
+
: undefined;
|
|
85
|
+
const stale = facts.ownership === "mine" || facts.idleSeconds >= policy.minIdleSeconds
|
|
86
|
+
? undefined
|
|
87
|
+
: "recently-active";
|
|
88
|
+
const candidates = [
|
|
89
|
+
liveness,
|
|
90
|
+
stale,
|
|
91
|
+
];
|
|
92
|
+
return candidates.filter((blocker) => blocker !== undefined);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Blockers derived from work that would not survive the removal.
|
|
96
|
+
*
|
|
97
|
+
* Untracked files count. The existing doctor check deliberately excludes them
|
|
98
|
+
* from its "work at risk" signal because untracked droppings make every
|
|
99
|
+
* worktree look catastrophic in a REPORT. A report is not a deletion: here the
|
|
100
|
+
* files are about to stop existing, and an untracked file is in no object
|
|
101
|
+
* database at all, so it is the least recoverable thing in the tree.
|
|
102
|
+
* @param facts - Gathered facts about the worktree
|
|
103
|
+
* @returns Work-at-risk blockers
|
|
104
|
+
*/
|
|
105
|
+
function workAtRiskBlockers(facts) {
|
|
106
|
+
const candidates = [
|
|
107
|
+
facts.unpushedCommits > 0 ? "unpushed-commits" : undefined,
|
|
108
|
+
facts.trackedChanges > 0 || facts.untrackedFiles > 0
|
|
109
|
+
? "uncommitted-changes"
|
|
110
|
+
: undefined,
|
|
111
|
+
];
|
|
112
|
+
return candidates.filter((blocker) => blocker !== undefined);
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=worktree-prune-policy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worktree-prune-policy.js","sourceRoot":"","sources":["../../src/cli/worktree-prune-policy.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { type LivenessProbe } from "./worktree-liveness.js";
|
|
2
|
+
import { type WorktreeFacts, type WorktreeVerdict } from "./worktree-prune-policy.js";
|
|
3
|
+
/** Command-line options for the prune verb. */
|
|
4
|
+
export interface WorktreePruneOptions {
|
|
5
|
+
/** Perform the removals instead of reporting them. */
|
|
6
|
+
readonly apply?: boolean;
|
|
7
|
+
/** Quiescence window in hours before an unclaimed worktree is eligible. */
|
|
8
|
+
readonly idleHours?: string;
|
|
9
|
+
/** Emit machine-readable output. */
|
|
10
|
+
readonly json?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/** What one run decided, before anything is removed. */
|
|
13
|
+
export interface WorktreePrunePlan {
|
|
14
|
+
/** Repository the plan was computed for. */
|
|
15
|
+
readonly repoPath: string;
|
|
16
|
+
/** Verdict for every registered worktree. */
|
|
17
|
+
readonly verdicts: readonly WorktreeVerdict[];
|
|
18
|
+
/** Facts behind each verdict, in the same order. */
|
|
19
|
+
readonly facts: readonly WorktreeFacts[];
|
|
20
|
+
/** Whether the live-process probe produced an answer. */
|
|
21
|
+
readonly livenessAvailable: boolean;
|
|
22
|
+
/** Registrations whose directory is already gone. */
|
|
23
|
+
readonly prunableRegistrations: readonly string[];
|
|
24
|
+
}
|
|
25
|
+
/** Injectable collaborators, overridden by tests. */
|
|
26
|
+
export interface WorktreePruneDependencies {
|
|
27
|
+
/** Machine-wide live working directory probe. */
|
|
28
|
+
readonly probeLive: LivenessProbe;
|
|
29
|
+
/** Owner id of the running process. */
|
|
30
|
+
readonly callerOwnerId: string | undefined;
|
|
31
|
+
/** Clock in epoch milliseconds. */
|
|
32
|
+
readonly now: () => number;
|
|
33
|
+
/** Path the cleaner is running from. */
|
|
34
|
+
readonly currentPath: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Arguments used to remove one worktree.
|
|
38
|
+
*
|
|
39
|
+
* Exported so a test can pin the absence of `--force` by value rather than by
|
|
40
|
+
* reading the source. The plain form is not a stylistic preference: it is the
|
|
41
|
+
* backstop that makes a bug in the entitlement rule non-destructive, because
|
|
42
|
+
* git still refuses a dirty tree on its own.
|
|
43
|
+
* @param worktreePath - Absolute worktree path
|
|
44
|
+
* @returns Git arguments
|
|
45
|
+
*/
|
|
46
|
+
export declare function buildWorktreeRemoveArgs(worktreePath: string): readonly string[];
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the quiescence window from the command line.
|
|
49
|
+
* @param idleHours - Raw `--idle-hours` value
|
|
50
|
+
* @returns Window in seconds
|
|
51
|
+
*/
|
|
52
|
+
export declare function resolveMinIdleSeconds(idleHours: string | undefined): number;
|
|
53
|
+
/**
|
|
54
|
+
* Compute what a run would do, without doing any of it.
|
|
55
|
+
* @param repoPath - Any path inside the repository
|
|
56
|
+
* @param options - Command-line options
|
|
57
|
+
* @param dependencies - Injectable collaborators
|
|
58
|
+
* @returns The plan
|
|
59
|
+
*/
|
|
60
|
+
export declare function planWorktreePrune(repoPath: string, options: WorktreePruneOptions, dependencies: WorktreePruneDependencies): Promise<WorktreePrunePlan>;
|
|
61
|
+
/** Outcome of removing one worktree. */
|
|
62
|
+
export interface RemovalOutcome {
|
|
63
|
+
/** Absolute worktree path. */
|
|
64
|
+
readonly path: string;
|
|
65
|
+
/** Whether git removed it. */
|
|
66
|
+
readonly removed: boolean;
|
|
67
|
+
/** Git's message when it refused. */
|
|
68
|
+
readonly error?: string;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Remove every eligible worktree in a plan.
|
|
72
|
+
*
|
|
73
|
+
* Removals run one at a time rather than concurrently. `git worktree remove`
|
|
74
|
+
* takes the repository's worktree lock, and a batch of parallel removals on a
|
|
75
|
+
* machine where other agents are also running git turns a contended lock into
|
|
76
|
+
* a spurious failure that reads like a refusal.
|
|
77
|
+
* @param plan - Plan produced by {@link planWorktreePrune}
|
|
78
|
+
* @param index - Index of the eligible worktree to remove next
|
|
79
|
+
* @returns One outcome per eligible worktree
|
|
80
|
+
*/
|
|
81
|
+
export declare function applyWorktreePrune(plan: WorktreePrunePlan, index?: number): Promise<readonly RemovalOutcome[]>;
|
|
82
|
+
/**
|
|
83
|
+
* Drop registrations whose directory has already disappeared.
|
|
84
|
+
*
|
|
85
|
+
* Separate from removal because it destroys nothing: git is only forgetting a
|
|
86
|
+
* pointer to a directory that no longer exists.
|
|
87
|
+
* @param plan - Plan produced by {@link planWorktreePrune}
|
|
88
|
+
* @returns Number of registrations git was asked to forget
|
|
89
|
+
*/
|
|
90
|
+
export declare function pruneMissingRegistrations(plan: WorktreePrunePlan): Promise<number>;
|
|
91
|
+
/**
|
|
92
|
+
* Default collaborators for a real run.
|
|
93
|
+
* @returns Dependencies wired to the real probe, environment, and clock
|
|
94
|
+
*/
|
|
95
|
+
export declare function defaultWorktreePruneDependencies(): WorktreePruneDependencies;
|
|
96
|
+
//# sourceMappingURL=worktree-prune.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worktree-prune.d.ts","sourceRoot":"","sources":["../../src/cli/worktree-prune.ts"],"names":[],"mappings":"AAyBA,OAAO,EAEL,KAAK,aAAa,EACnB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,eAAe,EACrB,MAAM,4BAA4B,CAAC;AAEpC,+CAA+C;AAC/C,MAAM,WAAW,oBAAoB;IACnC,sDAAsD;IACtD,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,2EAA2E;IAC3E,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,oCAAoC;IACpC,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,wDAAwD;AACxD,MAAM,WAAW,iBAAiB;IAChC,4CAA4C;IAC5C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,6CAA6C;IAC7C,QAAQ,CAAC,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAC;IAC9C,oDAAoD;IACpD,QAAQ,CAAC,KAAK,EAAE,SAAS,aAAa,EAAE,CAAC;IACzC,yDAAyD;IACzD,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;IACpC,qDAAqD;IACrD,QAAQ,CAAC,qBAAqB,EAAE,SAAS,MAAM,EAAE,CAAC;CACnD;AAED,qDAAqD;AACrD,MAAM,WAAW,yBAAyB;IACxC,iDAAiD;IACjD,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC;IAClC,uCAAuC;IACvC,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3C,mCAAmC;IACnC,QAAQ,CAAC,GAAG,EAAE,MAAM,MAAM,CAAC;IAC3B,wCAAwC;IACxC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CACrC,YAAY,EAAE,MAAM,GACnB,SAAS,MAAM,EAAE,CAEnB;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAO3E;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,oBAAoB,EAC7B,YAAY,EAAE,yBAAyB,GACtC,OAAO,CAAC,iBAAiB,CAAC,CAoB5B;AAED,wCAAwC;AACxC,MAAM,WAAW,cAAc;IAC7B,8BAA8B;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8BAA8B;IAC9B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,qCAAqC;IACrC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,iBAAiB,EACvB,KAAK,SAAI,GACR,OAAO,CAAC,SAAS,cAAc,EAAE,CAAC,CAgBpC;AAED;;;;;;;GAOG;AACH,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,iBAAiB,GACtB,OAAO,CAAC,MAAM,CAAC,CAIjB;AAED;;;GAGG;AACH,wBAAgB,gCAAgC,IAAI,yBAAyB,CAO5E"}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `lisa worktree prune` — the vetted verb for retiring agent worktrees.
|
|
3
|
+
*
|
|
4
|
+
* The guard in `parity-safety-net.sh` blocks `git worktree remove --force`
|
|
5
|
+
* because it discards a dirty tree, and that block is correct and stays. This
|
|
6
|
+
* command does not relax it, is not an override for it, and never passes
|
|
7
|
+
* `--force` to git: it performs the safety checks itself and then uses the
|
|
8
|
+
* plain `git worktree remove`, whose own refusal of a dirty tree remains in
|
|
9
|
+
* place underneath as a last line of defence.
|
|
10
|
+
*
|
|
11
|
+
* Dry-run is the default. The command is meant to be called by unattended
|
|
12
|
+
* loops on a machine shared with other agents' live work, so the first contact
|
|
13
|
+
* a mis-scoped invocation makes must be a report and not a deletion. `--apply`
|
|
14
|
+
* is the whole opt-in; there is no flag anywhere that widens the entitlement
|
|
15
|
+
* rule itself.
|
|
16
|
+
* @module cli/worktree-prune
|
|
17
|
+
*/
|
|
18
|
+
import { realpathSync } from "node:fs";
|
|
19
|
+
import * as path from "node:path";
|
|
20
|
+
import { gatherAllWorktreeFacts, git, listWorktrees, } from "./worktree-inventory.js";
|
|
21
|
+
import { probeLiveWorkingDirectories, } from "./worktree-liveness.js";
|
|
22
|
+
import { resolveCallerOwnerId } from "./worktree-ownership.js";
|
|
23
|
+
import { classifyWorktree, DEFAULT_MIN_IDLE_SECONDS, } from "./worktree-prune-policy.js";
|
|
24
|
+
/**
|
|
25
|
+
* Arguments used to remove one worktree.
|
|
26
|
+
*
|
|
27
|
+
* Exported so a test can pin the absence of `--force` by value rather than by
|
|
28
|
+
* reading the source. The plain form is not a stylistic preference: it is the
|
|
29
|
+
* backstop that makes a bug in the entitlement rule non-destructive, because
|
|
30
|
+
* git still refuses a dirty tree on its own.
|
|
31
|
+
* @param worktreePath - Absolute worktree path
|
|
32
|
+
* @returns Git arguments
|
|
33
|
+
*/
|
|
34
|
+
export function buildWorktreeRemoveArgs(worktreePath) {
|
|
35
|
+
return ["worktree", "remove", worktreePath];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the quiescence window from the command line.
|
|
39
|
+
* @param idleHours - Raw `--idle-hours` value
|
|
40
|
+
* @returns Window in seconds
|
|
41
|
+
*/
|
|
42
|
+
export function resolveMinIdleSeconds(idleHours) {
|
|
43
|
+
if (idleHours === undefined)
|
|
44
|
+
return DEFAULT_MIN_IDLE_SECONDS;
|
|
45
|
+
const parsed = Number.parseFloat(idleHours);
|
|
46
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
47
|
+
throw new Error("--idle-hours must be a non-negative number");
|
|
48
|
+
}
|
|
49
|
+
return Math.floor(parsed * 3600);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Compute what a run would do, without doing any of it.
|
|
53
|
+
* @param repoPath - Any path inside the repository
|
|
54
|
+
* @param options - Command-line options
|
|
55
|
+
* @param dependencies - Injectable collaborators
|
|
56
|
+
* @returns The plan
|
|
57
|
+
*/
|
|
58
|
+
export async function planWorktreePrune(repoPath, options, dependencies) {
|
|
59
|
+
const minIdleSeconds = resolveMinIdleSeconds(options.idleHours);
|
|
60
|
+
const entries = await listWorktrees(repoPath);
|
|
61
|
+
const live = await dependencies.probeLive();
|
|
62
|
+
const present = entries.filter(entry => !entry.prunable);
|
|
63
|
+
const facts = await gatherAllWorktreeFacts(present, {
|
|
64
|
+
liveWorkingDirectories: live,
|
|
65
|
+
callerOwnerId: dependencies.callerOwnerId,
|
|
66
|
+
currentPath: dependencies.currentPath,
|
|
67
|
+
nowMs: dependencies.now(),
|
|
68
|
+
});
|
|
69
|
+
return {
|
|
70
|
+
repoPath,
|
|
71
|
+
verdicts: facts.map(fact => classifyWorktree(fact, { minIdleSeconds })),
|
|
72
|
+
facts,
|
|
73
|
+
livenessAvailable: live !== undefined,
|
|
74
|
+
prunableRegistrations: entries
|
|
75
|
+
.filter((entry) => entry.prunable)
|
|
76
|
+
.map(entry => entry.path),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Remove every eligible worktree in a plan.
|
|
81
|
+
*
|
|
82
|
+
* Removals run one at a time rather than concurrently. `git worktree remove`
|
|
83
|
+
* takes the repository's worktree lock, and a batch of parallel removals on a
|
|
84
|
+
* machine where other agents are also running git turns a contended lock into
|
|
85
|
+
* a spurious failure that reads like a refusal.
|
|
86
|
+
* @param plan - Plan produced by {@link planWorktreePrune}
|
|
87
|
+
* @param index - Index of the eligible worktree to remove next
|
|
88
|
+
* @returns One outcome per eligible worktree
|
|
89
|
+
*/
|
|
90
|
+
export async function applyWorktreePrune(plan, index = 0) {
|
|
91
|
+
const eligible = plan.verdicts.filter(verdict => verdict.eligible);
|
|
92
|
+
if (index >= eligible.length)
|
|
93
|
+
return [];
|
|
94
|
+
const target = eligible[index];
|
|
95
|
+
const outcome = await git(buildWorktreeRemoveArgs(target.path), plan.repoPath).then(() => ({ path: target.path, removed: true }), (error) => ({
|
|
96
|
+
path: target.path,
|
|
97
|
+
removed: false,
|
|
98
|
+
error: describeError(error),
|
|
99
|
+
}));
|
|
100
|
+
return [outcome, ...(await applyWorktreePrune(plan, index + 1))];
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Drop registrations whose directory has already disappeared.
|
|
104
|
+
*
|
|
105
|
+
* Separate from removal because it destroys nothing: git is only forgetting a
|
|
106
|
+
* pointer to a directory that no longer exists.
|
|
107
|
+
* @param plan - Plan produced by {@link planWorktreePrune}
|
|
108
|
+
* @returns Number of registrations git was asked to forget
|
|
109
|
+
*/
|
|
110
|
+
export async function pruneMissingRegistrations(plan) {
|
|
111
|
+
if (plan.prunableRegistrations.length === 0)
|
|
112
|
+
return 0;
|
|
113
|
+
await git(["worktree", "prune"], plan.repoPath).catch(() => "");
|
|
114
|
+
return plan.prunableRegistrations.length;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Default collaborators for a real run.
|
|
118
|
+
* @returns Dependencies wired to the real probe, environment, and clock
|
|
119
|
+
*/
|
|
120
|
+
export function defaultWorktreePruneDependencies() {
|
|
121
|
+
return {
|
|
122
|
+
probeLive: probeLiveWorkingDirectories,
|
|
123
|
+
callerOwnerId: resolveCallerOwnerId(),
|
|
124
|
+
now: () => Date.now(),
|
|
125
|
+
currentPath: resolvePhysicalCwd(),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Resolve the directory the cleaner runs from, following symlinks.
|
|
130
|
+
*
|
|
131
|
+
* The physical form is what the liveness probe reports, so the two have to be
|
|
132
|
+
* in the same spelling or the cleaner cannot recognise its own worktree.
|
|
133
|
+
* @returns Absolute physical path of the current working directory
|
|
134
|
+
*/
|
|
135
|
+
function resolvePhysicalCwd() {
|
|
136
|
+
const resolved = path.resolve(process.cwd());
|
|
137
|
+
try {
|
|
138
|
+
return realpathSync.native(resolved);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return resolved;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Render an error for the report.
|
|
146
|
+
* @param error - Rejection from git
|
|
147
|
+
* @returns Single-line message
|
|
148
|
+
*/
|
|
149
|
+
function describeError(error) {
|
|
150
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
151
|
+
return message.split("\n").filter(line => line.trim() !== "")[0] ?? "unknown";
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=worktree-prune.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worktree-prune.js","sourceRoot":"","sources":["../../src/cli/worktree-prune.ts"],"names":[],"mappings":"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"}
|
|
@@ -44,7 +44,10 @@ def strip_heredocs(text: str) -> str:
|
|
|
44
44
|
words = shlex.split(raw, posix=True)
|
|
45
45
|
except ValueError:
|
|
46
46
|
return None
|
|
47
|
-
|
|
47
|
+
# An explicitly quoted empty word is a valid heredoc delimiter. Bash
|
|
48
|
+
# terminates that heredoc on the next empty line, so preserve "" as a
|
|
49
|
+
# marker instead of treating it as a parse failure.
|
|
50
|
+
return words[0] if len(words) == 1 else None
|
|
48
51
|
|
|
49
52
|
index = 0
|
|
50
53
|
while index < len(lines):
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lisa-owned-hash-ledger.d.ts","sourceRoot":"","sources":["../../src/core/lisa-owned-hash-ledger.ts"],"names":[],"mappings":"AACA,6EAA6E;AAE7E;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,EAAE,QAAQ,CAC3C,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"lisa-owned-hash-ledger.d.ts","sourceRoot":"","sources":["../../src/core/lisa-owned-hash-ledger.ts"],"names":[],"mappings":"AACA,6EAA6E;AAE7E;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,EAAE,QAAQ,CAC3C,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAiqBjC,CAAC"}
|