@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,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":"AAAA;;;;;;;;;;;;;;;GAeG;AA2DH,yDAAyD;AACzD,MAAM,CAAC,MAAM,wBAAwB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;AAErD,sDAAsD;AACtD,MAAM,CAAC,MAAM,oBAAoB,GAC/B,MAAM,CAAC,MAAM,CAAC;IACZ,kBAAkB,EAAE,2CAA2C;IAC/D,kBAAkB,EAAE,kCAAkC;IACtD,YAAY,EAAE,wDAAwD;IACtE,0BAA0B,EACxB,6EAA6E;IAC/E,kBAAkB,EAChB,uEAAuE;IACzE,QAAQ,EAAE,+CAA+C;IACzD,iBAAiB,EAAE,wDAAwD;IAC3E,kBAAkB,EAChB,4DAA4D;IAC9D,qBAAqB,EACnB,iEAAiE;IACnE,UAAU,EAAE,4DAA4D;CACzE,CAAC,CAAC;AAEL;;;;;;;;;;GAUG;AACH,MAAM,UAAU,gBAAgB,CAC9B,KAAoB,EACpB,MAA2B;IAE3B,MAAM,QAAQ,GAA+B;QAC3C,GAAG,kBAAkB,CAAC,KAAK,CAAC;QAC5B,GAAG,gBAAgB,CAAC,KAAK,EAAE,MAAM,CAAC;QAClC,GAAG,kBAAkB,CAAC,KAAK,CAAC;KAC7B,CAAC;IACF,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,QAAQ,EAAE,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,QAAQ,EAAE,CAAC;AACzE,CAAC;AAED;;;;GAIG;AACH,SAAS,kBAAkB,CAAC,KAAoB;IAC9C,MAAM,UAAU,GAA6C;QAC3D,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAE,YAAsB;QACpD,KAAK,CAAC,SAAS,CAAC,CAAC,CAAE,kBAA4B,CAAC,CAAC,CAAC,SAAS;QAC3D,KAAK,CAAC,SAAS,CAAC,CAAC,CAAE,kBAA4B,CAAC,CAAC,CAAC,SAAS;QAC3D,KAAK,CAAC,MAAM,CAAC,CAAC,CAAE,YAAsB,CAAC,CAAC,CAAC,SAAS;QAClD,KAAK,CAAC,SAAS,KAAK,QAAQ;YAC1B,CAAC,CAAE,0BAAoC;YACvC,CAAC,CAAC,SAAS;KACd,CAAC;IACF,OAAO,UAAU,CAAC,MAAM,CACtB,CAAC,OAAO,EAA8B,EAAE,CAAC,OAAO,KAAK,SAAS,CAC/D,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAS,gBAAgB,CACvB,KAAoB,EACpB,MAA2B;IAE3B,MAAM,QAAQ,GACZ,KAAK,CAAC,WAAW,KAAK,SAAS;QAC7B,CAAC,CAAE,kBAA4B;QAC/B,CAAC,CAAC,KAAK,CAAC,WAAW,GAAG,CAAC;YACrB,CAAC,CAAE,QAAkB;YACrB,CAAC,CAAC,SAAS,CAAC;IAClB,MAAM,KAAK,GACT,KAAK,CAAC,SAAS,KAAK,MAAM,IAAI,KAAK,CAAC,WAAW,IAAI,MAAM,CAAC,cAAc;QACtE,CAAC,CAAC,SAAS;QACX,CAAC,CAAE,iBAA2B,CAAC;IACnC,MAAM,UAAU,GAA6C;QAC3D,QAAQ;QACR,KAAK;KACN,CAAC;IACF,OAAO,UAAU,CAAC,MAAM,CACtB,CAAC,OAAO,EAA8B,EAAE,CAAC,OAAO,KAAK,SAAS,CAC/D,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAS,kBAAkB,CAAC,KAAoB;IAC9C,MAAM,UAAU,GAA6C;QAC3D,KAAK,CAAC,eAAe,GAAG,CAAC,CAAC,CAAC,CAAE,kBAA4B,CAAC,CAAC,CAAC,SAAS;QACrE,KAAK,CAAC,cAAc,GAAG,CAAC,IAAI,KAAK,CAAC,cAAc,GAAG,CAAC;YAClD,CAAC,CAAE,qBAA+B;YAClC,CAAC,CAAC,SAAS;KACd,CAAC;IACF,OAAO,UAAU,CAAC,MAAM,CACtB,CAAC,OAAO,EAA8B,EAAE,CAAC,OAAO,KAAK,SAAS,CAC/D,CAAC;AACJ,CAAC"}
@@ -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
- return words[0] if len(words) == 1 and words[0] else None
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,CA6pBjC,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"}