@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,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"}
|