@cleocode/worktree 2026.9.20 → 2026.9.21
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/dist/git.d.ts +14 -0
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +28 -0
- package/dist/git.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/worktree-create.d.ts.map +1 -1
- package/dist/worktree-create.js +222 -72
- package/dist/worktree-create.js.map +1 -1
- package/dist/worktree-destroy.d.ts.map +1 -1
- package/dist/worktree-destroy.js +15 -2
- package/dist/worktree-destroy.js.map +1 -1
- package/dist/worktree-hooks.d.ts +2 -1
- package/dist/worktree-hooks.d.ts.map +1 -1
- package/dist/worktree-hooks.js +6 -2
- package/dist/worktree-hooks.js.map +1 -1
- package/dist/worktree-lock.d.ts +211 -0
- package/dist/worktree-lock.d.ts.map +1 -0
- package/dist/worktree-lock.js +619 -0
- package/dist/worktree-lock.js.map +1 -0
- package/dist/worktree-prune.d.ts.map +1 -1
- package/dist/worktree-prune.js +3 -0
- package/dist/worktree-prune.js.map +1 -1
- package/native/worktree-napi.win32-x64-msvc.node +0 -0
- package/package.json +4 -4
package/dist/worktree-destroy.js
CHANGED
|
@@ -10,12 +10,13 @@
|
|
|
10
10
|
* @adr ADR-062
|
|
11
11
|
*/
|
|
12
12
|
import { existsSync, rmSync } from 'node:fs';
|
|
13
|
-
import { getGitRoot, gitSilent, gitSync } from './git.js';
|
|
13
|
+
import { countUnmergedCommits, getGitRoot, gitSilent, gitSync } from './git.js';
|
|
14
14
|
import { destroyWorktree as napiDestroyWorktree } from './napi-binding.js';
|
|
15
15
|
import { computeProjectHash, resolveTaskWorktreePath } from './paths.js';
|
|
16
16
|
import { appendWorktreeAuditLog, removeWorktreeFromSentinelIndex } from './worktree-audit.js';
|
|
17
17
|
import { runWorktreeHooks } from './worktree-hooks.js';
|
|
18
18
|
import { locateTaskWorktree } from './worktree-locate.js';
|
|
19
|
+
import { releaseWorktreeTaskLock } from './worktree-lock.js';
|
|
19
20
|
/**
|
|
20
21
|
* Destroy the git worktree for a task.
|
|
21
22
|
*
|
|
@@ -151,11 +152,23 @@ export async function destroyWorktree(projectRoot, options) {
|
|
|
151
152
|
else {
|
|
152
153
|
worktreeRemoved = true; // genuinely absent: not on disk, not registered
|
|
153
154
|
}
|
|
155
|
+
// T12506: the worktree is gone, so its per-task lock guards nothing — free
|
|
156
|
+
// it so the next spawn of this task provisions without waiting for a TTL.
|
|
157
|
+
if (worktreeRemoved)
|
|
158
|
+
releaseWorktreeTaskLock(projectHash, taskId);
|
|
154
159
|
// Step 4: Optionally delete the branch.
|
|
155
160
|
if (deleteBranch) {
|
|
156
161
|
try {
|
|
157
162
|
const branchExists = gitSync(['branch', '--list', branch], gitRoot);
|
|
158
|
-
|
|
163
|
+
const unmerged = branchExists ? countUnmergedCommits(gitRoot, branch, ['HEAD']) : 0;
|
|
164
|
+
if (branchExists && unmerged > 0 && options.forceDeleteUnmergedBranch !== true) {
|
|
165
|
+
// T12506: never delete history that exists nowhere else implicitly.
|
|
166
|
+
branchDeleted = false;
|
|
167
|
+
if (!error) {
|
|
168
|
+
error = `Branch '${branch}' kept: ${unmerged} commit(s) are on neither origin/main nor HEAD. Merge it, or pass forceDeleteUnmergedBranch to delete it anyway.`;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else if (branchExists) {
|
|
159
172
|
gitSync(['branch', '-D', branch], gitRoot);
|
|
160
173
|
branchDeleted = true;
|
|
161
174
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worktree-destroy.js","sourceRoot":"","sources":["../src/worktree-destroy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAE7C,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"worktree-destroy.js","sourceRoot":"","sources":["../src/worktree-destroy.ts"],"names":[],"mappings":"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"}
|
package/dist/worktree-hooks.d.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* feature per D030.
|
|
4
4
|
*
|
|
5
5
|
* Executes `post-create` and `post-start` hooks in the worktree directory.
|
|
6
|
-
* Each hook runs
|
|
6
|
+
* Each hook runs through the platform shell ({@link shellInvocation}: `/bin/sh -c`
|
|
7
|
+
* on POSIX, cmd.exe on Windows) with the worktree path as CWD (T12604).
|
|
7
8
|
*
|
|
8
9
|
* @task T1161
|
|
9
10
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worktree-hooks.d.ts","sourceRoot":"","sources":["../src/worktree-hooks.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"worktree-hooks.d.ts","sourceRoot":"","sources":["../src/worktree-hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAK5E;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,YAAY,EAClB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,kBAAkB,CAAC,CAgC7B;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,SAAS,YAAY,EAAE,EAC9B,KAAK,EAAE,YAAY,CAAC,OAAO,CAAC,EAC5B,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAiB/B"}
|
package/dist/worktree-hooks.js
CHANGED
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
* feature per D030.
|
|
4
4
|
*
|
|
5
5
|
* Executes `post-create` and `post-start` hooks in the worktree directory.
|
|
6
|
-
* Each hook runs
|
|
6
|
+
* Each hook runs through the platform shell ({@link shellInvocation}: `/bin/sh -c`
|
|
7
|
+
* on POSIX, cmd.exe on Windows) with the worktree path as CWD (T12604).
|
|
7
8
|
*
|
|
8
9
|
* @task T1161
|
|
9
10
|
*/
|
|
10
11
|
import { execFile } from 'node:child_process';
|
|
12
|
+
import { shellInvocation } from '@cleocode/paths';
|
|
11
13
|
const DEFAULT_HOOK_TIMEOUT_MS = 30_000;
|
|
12
14
|
/**
|
|
13
15
|
* Execute a single declarative hook in the given worktree directory.
|
|
@@ -19,11 +21,13 @@ const DEFAULT_HOOK_TIMEOUT_MS = 30_000;
|
|
|
19
21
|
export async function runSingleHook(hook, worktreePath) {
|
|
20
22
|
const timeoutMs = hook.timeoutMs ?? DEFAULT_HOOK_TIMEOUT_MS;
|
|
21
23
|
const start = Date.now();
|
|
24
|
+
const shell = shellInvocation(hook.command);
|
|
22
25
|
return new Promise((resolve) => {
|
|
23
|
-
const child = execFile(
|
|
26
|
+
const child = execFile(shell.file, shell.args, {
|
|
24
27
|
cwd: worktreePath,
|
|
25
28
|
timeout: timeoutMs,
|
|
26
29
|
encoding: 'utf-8',
|
|
30
|
+
windowsVerbatimArguments: shell.windowsVerbatimArguments,
|
|
27
31
|
}, (_err, stdout, stderr) => {
|
|
28
32
|
const durationMs = Date.now() - start;
|
|
29
33
|
const exitCode = child.exitCode;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worktree-hooks.js","sourceRoot":"","sources":["../src/worktree-hooks.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"worktree-hooks.js","sourceRoot":"","sources":["../src/worktree-hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAE9C,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAElD,MAAM,uBAAuB,GAAG,MAAM,CAAC;AAEvC;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,IAAkB,EAClB,YAAoB;IAEpB,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,uBAAuB,CAAC;IAC5D,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAEzB,MAAM,KAAK,GAAG,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAE5C,OAAO,IAAI,OAAO,CAAqB,CAAC,OAAO,EAAE,EAAE;QACjD,MAAM,KAAK,GAAG,QAAQ,CACpB,KAAK,CAAC,IAAI,EACV,KAAK,CAAC,IAAI,EACV;YACE,GAAG,EAAE,YAAY;YACjB,OAAO,EAAE,SAAS;YAClB,QAAQ,EAAE,OAAO;YACjB,wBAAwB,EAAE,KAAK,CAAC,wBAAwB;SACzD,EACD,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE;YACvB,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC;YACtC,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC;YAChC,MAAM,QAAQ,GAAG,QAAQ,KAAK,IAAI,IAAI,UAAU,IAAI,SAAS,CAAC;YAE9D,OAAO,CAAC;gBACN,IAAI;gBACJ,OAAO,EAAE,QAAQ,KAAK,CAAC;gBACvB,MAAM,EAAE,CAAC,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE;gBACzD,MAAM,EAAE,CAAC,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE;gBACzD,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC;gBAC3C,UAAU;aACX,CAAC,CAAC;QACL,CAAC,CACF,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,KAA8B,EAC9B,KAA4B,EAC5B,YAAoB;IAEpB,MAAM,OAAO,GAAyB,EAAE,CAAC;IACzC,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC;IAExD,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;QAC5B,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;QACvD,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAErB,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACxC,MAAM,IAAI,KAAK,CACb,4CAA4C,IAAI,CAAC,OAAO,IAAI;gBAC1D,SAAS,MAAM,CAAC,QAAQ,aAAa,MAAM,CAAC,MAAM,EAAE,CACvD,CAAC;QACJ,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-task worktree lock (T12506 · epic T12498 E-WORKTREE-MULTI-AGENT).
|
|
3
|
+
*
|
|
4
|
+
* `createWorktree` used to be check-then-act: when the task's worktree
|
|
5
|
+
* directory existed and `git status --porcelain` was empty it force-removed the
|
|
6
|
+
* worktree and ran `git branch -D task/<id>`. A LIVE agent's worktree looks
|
|
7
|
+
* clean whenever its only changes are gitignored (`.cleo/cleo.db`) or already
|
|
8
|
+
* committed, so a second `cleo orchestrate spawn <id>` deleted files out from
|
|
9
|
+
* under the running agent.
|
|
10
|
+
*
|
|
11
|
+
* This module is the mutual-exclusion half of the fix. Before any
|
|
12
|
+
* `git worktree add`, the spawner takes an atomic per-task lock:
|
|
13
|
+
*
|
|
14
|
+
* - **Location:** `<cleoHome>/locks/worktrees/<projectHash>/<taskId>.lock`
|
|
15
|
+
* (via `@cleocode/paths`), shared by every spawn of the task on this machine.
|
|
16
|
+
* - **Atomic publish:** the record is written to a private temp file and then
|
|
17
|
+
* hard-linked into place. `link(2)` fails with `EEXIST` when the lock already
|
|
18
|
+
* exists, so exactly one contender wins and a reader never sees a partially
|
|
19
|
+
* written record. Filesystems without hard links (exFAT, some network
|
|
20
|
+
* mounts) fall back to `open(O_CREAT|O_EXCL)` on the final path; a reader
|
|
21
|
+
* that meets a young, not-yet-parseable record waits instead of reclaiming.
|
|
22
|
+
* - **Every acquisition failure is a refusal.** An I/O error (EACCES, EROFS,
|
|
23
|
+
* ENOSPC, ...) surfaces as `E_WORKTREE_LOCKED` with `reason:
|
|
24
|
+
* 'lock-unavailable'` — never as a generic error a caller might answer with
|
|
25
|
+
* worktree cleanup.
|
|
26
|
+
* - **Holder identity:** session id, agent id, device id, owner pid plus the
|
|
27
|
+
* owner's `ps lstart` start time (rendered under `PS_STABLE_ENV`), host name
|
|
28
|
+
* and a heartbeat timestamp.
|
|
29
|
+
* - **Liveness:** a held lock is reclaimable only when its holder is provably
|
|
30
|
+
* dead (pid gone, or alive with a different start time = recycled pid). The
|
|
31
|
+
* heartbeat TTL applies ONLY to holders whose pid cannot be verified (another
|
|
32
|
+
* device, or no recorded start time); a verified live holder is never
|
|
33
|
+
* reclaimed on age. Anything else is `E_WORKTREE_LOCKED`.
|
|
34
|
+
* - **Re-entry:** the same caller (session id, agent id, owner pid and start
|
|
35
|
+
* time all equal) re-enters its own lock, so an orchestrator can retry.
|
|
36
|
+
* - **Reclaim:** serialised through a second O_EXCL "reclaim mutex" file so two
|
|
37
|
+
* contenders that both saw the same dead holder cannot both win; the winner
|
|
38
|
+
* re-reads the lock and only replaces it if it is still the record it judged.
|
|
39
|
+
*
|
|
40
|
+
* @module worktree-lock
|
|
41
|
+
* @task T12506
|
|
42
|
+
*/
|
|
43
|
+
import type { WorktreeLockAcquisition, WorktreeLockHolder, WorktreeLockRecord } from '@cleocode/contracts';
|
|
44
|
+
import { BRANCH_LOCK_ERROR_CODES } from '@cleocode/contracts';
|
|
45
|
+
/** Default heartbeat TTL: 4 hours. Override with `CLEO_WORKTREE_LOCK_TTL_MS`. */
|
|
46
|
+
export declare const DEFAULT_WORKTREE_LOCK_TTL_MS: number;
|
|
47
|
+
/** Environment variable overriding {@link DEFAULT_WORKTREE_LOCK_TTL_MS}. */
|
|
48
|
+
export declare const WORKTREE_LOCK_TTL_ENV = "CLEO_WORKTREE_LOCK_TTL_MS";
|
|
49
|
+
/** Liveness snapshot of one process. */
|
|
50
|
+
export interface ProcessLiveness {
|
|
51
|
+
/** True when the pid exists (signal 0 succeeded or was refused with EPERM). */
|
|
52
|
+
alive: boolean;
|
|
53
|
+
/** `ps -o lstart=` under {@link PS_STABLE_ENV}, or `null` when unprobeable. */
|
|
54
|
+
startedAt: string | null;
|
|
55
|
+
}
|
|
56
|
+
/** Reads the liveness of a pid; tests inject fakes. */
|
|
57
|
+
export type ProcessProbe = (pid: number) => ProcessLiveness;
|
|
58
|
+
/** Verdict on an existing lock record. */
|
|
59
|
+
export interface WorktreeLockAssessment {
|
|
60
|
+
/** True when the holder must be treated as live (lock not reclaimable). */
|
|
61
|
+
live: boolean;
|
|
62
|
+
/** Why a non-live holder is reclaimable. */
|
|
63
|
+
reason?: 'pid-gone' | 'pid-recycled' | 'heartbeat-stale';
|
|
64
|
+
/** True when the holder's pid + start time were verified live on this device. */
|
|
65
|
+
verified?: boolean;
|
|
66
|
+
}
|
|
67
|
+
/** Options for {@link acquireWorktreeTaskLock}. */
|
|
68
|
+
export interface AcquireWorktreeTaskLockOptions {
|
|
69
|
+
/** Project hash scoping the worktree. */
|
|
70
|
+
projectHash: string;
|
|
71
|
+
/** Task the lock guards. */
|
|
72
|
+
taskId: string;
|
|
73
|
+
/** Holder identity; missing pid/start default to the calling process. */
|
|
74
|
+
holder?: WorktreeLockHolder;
|
|
75
|
+
/** Heartbeat TTL in ms (default {@link resolveWorktreeLockTtlMs}). */
|
|
76
|
+
ttlMs?: number;
|
|
77
|
+
/** Process probe (default {@link probeProcess}). */
|
|
78
|
+
probe?: ProcessProbe;
|
|
79
|
+
/** Clock (default `Date.now`). */
|
|
80
|
+
now?: () => number;
|
|
81
|
+
/** This device's id (default: the persisted `<cleoHome>/device-id`, if any). */
|
|
82
|
+
deviceId?: string | null;
|
|
83
|
+
}
|
|
84
|
+
/** Error thrown when the lock is held by a live holder. */
|
|
85
|
+
export interface WorktreeLockedError extends Error {
|
|
86
|
+
/** Always `E_WORKTREE_LOCKED`. */
|
|
87
|
+
code: typeof BRANCH_LOCK_ERROR_CODES.E_WORKTREE_LOCKED;
|
|
88
|
+
/** Numeric exit code (`ExitCode.WORKTREE_LOCKED`). */
|
|
89
|
+
exitCode: number;
|
|
90
|
+
/** The live holder's record; `null` when the lock itself could not be taken. */
|
|
91
|
+
holder: WorktreeLockRecord | null;
|
|
92
|
+
/** `held` = a live holder owns it; `lock-unavailable` = the lock file could not be created or read. */
|
|
93
|
+
reason: 'held' | 'lock-unavailable';
|
|
94
|
+
/** Absolute path of the lock file. */
|
|
95
|
+
lockPath: string;
|
|
96
|
+
/** Remediation hint. */
|
|
97
|
+
fix: string;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Resolve the heartbeat TTL: a positive integer `CLEO_WORKTREE_LOCK_TTL_MS`
|
|
101
|
+
* wins, anything else falls back to {@link DEFAULT_WORKTREE_LOCK_TTL_MS}.
|
|
102
|
+
*
|
|
103
|
+
* @param env - Environment to read (defaults to `process.env`).
|
|
104
|
+
* @returns TTL in milliseconds.
|
|
105
|
+
*/
|
|
106
|
+
export declare function resolveWorktreeLockTtlMs(env?: NodeJS.ProcessEnv): number;
|
|
107
|
+
/**
|
|
108
|
+
* Read a process's start time via `ps -o lstart=` with the stable env
|
|
109
|
+
* (UTC, C locale). Returns `null` on Windows or when `ps` fails.
|
|
110
|
+
*
|
|
111
|
+
* @param pid - Process id.
|
|
112
|
+
* @returns Whitespace-normalised `lstart` string, or `null`.
|
|
113
|
+
*/
|
|
114
|
+
export declare function readProcessStartTime(pid: number): string | null;
|
|
115
|
+
/**
|
|
116
|
+
* Probe whether a pid is alive and, when it is, read its start time.
|
|
117
|
+
*
|
|
118
|
+
* `process.kill(pid, 0)` delivers no signal: `ESRCH` means no such process,
|
|
119
|
+
* `EPERM` means it exists but belongs to another user (still alive).
|
|
120
|
+
*
|
|
121
|
+
* @param pid - Process id.
|
|
122
|
+
* @returns Liveness snapshot.
|
|
123
|
+
*/
|
|
124
|
+
export declare function probeProcess(pid: number): ProcessLiveness;
|
|
125
|
+
/**
|
|
126
|
+
* Read this device's persisted stable id (`<cleoHome>/device-id`), or `null`
|
|
127
|
+
* when core has not created it yet. Never creates it.
|
|
128
|
+
*
|
|
129
|
+
* @returns The device id, or `null`.
|
|
130
|
+
*/
|
|
131
|
+
export declare function readStableDeviceId(): string | null;
|
|
132
|
+
/**
|
|
133
|
+
* Decide whether an existing lock's holder is live.
|
|
134
|
+
*
|
|
135
|
+
* - Same device (device ids equal; host names equal when either id is
|
|
136
|
+
* unknown): the pid is probed. Gone → `pid-gone`; alive with a different
|
|
137
|
+
* start time → `pid-recycled`; alive with the SAME start time → live and
|
|
138
|
+
* verified, whatever the heartbeat age (a verified live holder is never
|
|
139
|
+
* reclaimed on age — T12506 review).
|
|
140
|
+
* - Unverifiable (another device, or no start time on either side): only the
|
|
141
|
+
* heartbeat TTL can release it.
|
|
142
|
+
*
|
|
143
|
+
* @param record - The lock record on disk.
|
|
144
|
+
* @param opts - TTL, clock, probe and this device's id.
|
|
145
|
+
* @returns The assessment.
|
|
146
|
+
*/
|
|
147
|
+
export declare function assessWorktreeLockHolder(record: WorktreeLockRecord, opts: {
|
|
148
|
+
ttlMs: number;
|
|
149
|
+
now: number;
|
|
150
|
+
probe: ProcessProbe;
|
|
151
|
+
deviceId: string | null;
|
|
152
|
+
}): WorktreeLockAssessment;
|
|
153
|
+
/**
|
|
154
|
+
* True when `candidate` is the same caller as the lock's `holder`: session id
|
|
155
|
+
* and agent id both present and equal, and the same owner process (pid and
|
|
156
|
+
* start time). Only then may a spawn re-enter an existing lock.
|
|
157
|
+
*
|
|
158
|
+
* @param holder - Record on disk.
|
|
159
|
+
* @param candidate - Record the caller would publish.
|
|
160
|
+
* @returns Whether re-entry is allowed.
|
|
161
|
+
*/
|
|
162
|
+
export declare function isSameLockCaller(holder: WorktreeLockRecord, candidate: WorktreeLockRecord): boolean;
|
|
163
|
+
/**
|
|
164
|
+
* Read the current lock record for a task, if any.
|
|
165
|
+
*
|
|
166
|
+
* @param projectHash - Project hash.
|
|
167
|
+
* @param taskId - Task id.
|
|
168
|
+
* @returns The record, or `null` when no valid lock exists.
|
|
169
|
+
*/
|
|
170
|
+
export declare function readWorktreeTaskLock(projectHash: string, taskId: string): WorktreeLockRecord | null;
|
|
171
|
+
/**
|
|
172
|
+
* Type guard for the error thrown by {@link acquireWorktreeTaskLock}.
|
|
173
|
+
*
|
|
174
|
+
* @param err - Any thrown value.
|
|
175
|
+
* @returns True when `err` is an `E_WORKTREE_LOCKED` error.
|
|
176
|
+
*/
|
|
177
|
+
export declare function isWorktreeLockedError(err: unknown): err is WorktreeLockedError;
|
|
178
|
+
/**
|
|
179
|
+
* Atomically acquire the per-task worktree lock: create it, re-enter it (same
|
|
180
|
+
* caller), or reclaim it from a provably dead / unverifiable-and-stale holder.
|
|
181
|
+
*
|
|
182
|
+
* @param options - Task, holder identity, TTL and test seams.
|
|
183
|
+
* @returns How the lock was obtained plus the record now on disk.
|
|
184
|
+
* @throws {@link WorktreeLockedError} (`E_WORKTREE_LOCKED`) when a live holder
|
|
185
|
+
* owns the lock OR the lock could not be taken at all. Every failure of this
|
|
186
|
+
* function is a refusal: callers must never answer it with cleanup.
|
|
187
|
+
*/
|
|
188
|
+
export declare function acquireWorktreeTaskLock(options: AcquireWorktreeTaskLockOptions): WorktreeLockAcquisition;
|
|
189
|
+
/**
|
|
190
|
+
* Refresh the holder's heartbeat. Only the holder that owns `token` may beat,
|
|
191
|
+
* and the rewrite happens under the lock mutex after re-checking the token, so
|
|
192
|
+
* it can never clobber a lock another contender reclaimed in the meantime.
|
|
193
|
+
*
|
|
194
|
+
* @param projectHash - Project hash.
|
|
195
|
+
* @param taskId - Task id.
|
|
196
|
+
* @param token - Token of the acquisition to refresh.
|
|
197
|
+
* @param now - Clock (default `Date.now`).
|
|
198
|
+
* @returns The refreshed record, or `null` when `token` no longer holds the lock.
|
|
199
|
+
*/
|
|
200
|
+
export declare function heartbeatWorktreeTaskLock(projectHash: string, taskId: string, token: string, now?: () => number): WorktreeLockRecord | null;
|
|
201
|
+
/**
|
|
202
|
+
* Release the per-task lock. With `token`, only that acquisition's lock is
|
|
203
|
+
* removed (a lock since reclaimed or re-entered by someone else is left alone).
|
|
204
|
+
*
|
|
205
|
+
* @param projectHash - Project hash.
|
|
206
|
+
* @param taskId - Task id.
|
|
207
|
+
* @param token - Optional token of the acquisition to release.
|
|
208
|
+
* @returns True when a lock file was removed.
|
|
209
|
+
*/
|
|
210
|
+
export declare function releaseWorktreeTaskLock(projectHash: string, taskId: string, token?: string): boolean;
|
|
211
|
+
//# sourceMappingURL=worktree-lock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worktree-lock.d.ts","sourceRoot":"","sources":["../src/worktree-lock.ts"],"names":[],"mappings":"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"}
|