@agentproto/worktree 0.2.0 → 0.3.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/LICENSE +202 -21
- package/README.md +86 -4
- package/dist/bin/worktree-agent.mjs +4 -4
- package/dist/{chunk-P7FOHOQ3.mjs → chunk-CBEWP5OT.mjs} +13 -5
- package/dist/chunk-CBEWP5OT.mjs.map +1 -0
- package/dist/chunk-CM3XGWTY.mjs +155 -0
- package/dist/chunk-CM3XGWTY.mjs.map +1 -0
- package/dist/chunk-E3EM24BS.mjs +3 -0
- package/dist/{chunk-PSFICTF7.mjs.map → chunk-E3EM24BS.mjs.map} +1 -1
- package/dist/chunk-SEVCUXPY.mjs +1176 -0
- package/dist/chunk-SEVCUXPY.mjs.map +1 -0
- package/dist/index.d.ts +1076 -6
- package/dist/index.mjs +595 -4
- package/dist/index.mjs.map +1 -1
- package/dist/provider/index.mjs +2 -2
- package/dist/tools/index.d.ts +114 -2
- package/dist/tools/index.mjs +2 -2
- package/package.json +6 -6
- package/dist/chunk-BBAH3VLQ.mjs +0 -170
- package/dist/chunk-BBAH3VLQ.mjs.map +0 -1
- package/dist/chunk-FRVPZTQN.mjs +0 -67
- package/dist/chunk-FRVPZTQN.mjs.map +0 -1
- package/dist/chunk-P7FOHOQ3.mjs.map +0 -1
- package/dist/chunk-PSFICTF7.mjs +0 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,24 +1,189 @@
|
|
|
1
|
-
export { cleanupWorktreeTool, provisionWorktreeTool, runGateTool } from './tools/index.js';
|
|
1
|
+
export { ServiceStatusOutput, cleanupWorktreeTool, listServicesTool, provisionWorktreeTool, runGateTool, runScriptTool, serviceStatusSchema, startServiceTool, stopServiceTool } from './tools/index.js';
|
|
2
2
|
export { worktreeProvider } from './provider/index.js';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
+
import http from 'node:http';
|
|
4
5
|
import { RuntimeWorkflow, AgentSessionHost } from '@agentproto/workflow-runtime';
|
|
5
6
|
import { StartAgentArgs, SessionDescriptor, TurnEvent, TurnResult, HarnessClient, ConnectHarnessOptions } from '@agentproto/harness';
|
|
6
7
|
import '@agentproto/tool';
|
|
7
8
|
import '@agentproto/driver';
|
|
8
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Raised when `worktree.cleanup` can't remove the worktree: either we
|
|
12
|
+
* categorized the tree ourselves and found a dirty class the caller didn't
|
|
13
|
+
* authorize (`blocked` names it), or — the no-flags default path — git's own
|
|
14
|
+
* `worktree remove` refused and we never asked why (`blocked` is empty,
|
|
15
|
+
* `message` carries git's stderr verbatim).
|
|
16
|
+
*/
|
|
17
|
+
declare class WorktreeNotRemovableError extends Error {
|
|
18
|
+
readonly cwd: string;
|
|
19
|
+
readonly blocked: readonly ("untracked" | "modified")[];
|
|
20
|
+
/**
|
|
21
|
+
* `blocked` is empty when git itself refused without us categorizing the
|
|
22
|
+
* tree first (the no-flags default path, PLAN.md §5.2 layer 3) — `detail`
|
|
23
|
+
* then carries git's own stderr instead of a named class.
|
|
24
|
+
*/
|
|
25
|
+
constructor(cwd: string, blocked: readonly ("untracked" | "modified")[], detail?: string);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Provenance — which session(s) worked in this worktree, historically,
|
|
30
|
+
* including dead ones (PLAN.md §1.5). Not a fourth status axis: it never
|
|
31
|
+
* changes `class`, only triage ("what was the session doing when it
|
|
32
|
+
* stranded these files?").
|
|
33
|
+
*
|
|
34
|
+
* The join is over the EXISTING sessions registry
|
|
35
|
+
* (`~/.agentproto/sessions.json`, `packages/runtime/src/sessions.ts`) —
|
|
36
|
+
* upholding the no-registry principle (PLAN.md §1.4): nothing new is
|
|
37
|
+
* mirrored, this only reads a file another package already owns and
|
|
38
|
+
* persists. Read directly off disk (not via a running daemon) so `worktree
|
|
39
|
+
* ls --status` works with no daemon required, matching `worktree check`'s
|
|
40
|
+
* posture (PLAN.md §3.3).
|
|
41
|
+
*
|
|
42
|
+
* Two known limits, both designed in rather than hidden (PLAN.md §1.5):
|
|
43
|
+
* - `sessions.json` is a recency window (`HISTORY_CAP = 200`,
|
|
44
|
+
* `packages/runtime/src/sessions.ts:583`), not an archive — an old
|
|
45
|
+
* worktree's sessions may already be evicted.
|
|
46
|
+
* - Without a per-worktree creation marker, the join can span
|
|
47
|
+
* generations (a worktree removed and recreated at the same path
|
|
48
|
+
* inherits the old generation's sessions). PR-B writes that marker
|
|
49
|
+
* (`agentproto-worktree.json` in the worktree's private gitdir); this PR
|
|
50
|
+
* only reads it if present and is honest — `confidence: "best-effort"`
|
|
51
|
+
* — when it's absent, which is every worktree today.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
declare const SESSIONS_FILE_PATH: () => string;
|
|
55
|
+
/** The subset of `SessionDescriptor` (`packages/runtime/src/sessions.ts`) provenance needs. */
|
|
56
|
+
declare const sessionRefSchema: z.ZodObject<{
|
|
57
|
+
id: z.ZodString;
|
|
58
|
+
label: z.ZodOptional<z.ZodString>;
|
|
59
|
+
startedAt: z.ZodString;
|
|
60
|
+
endedAt: z.ZodOptional<z.ZodString>;
|
|
61
|
+
status: z.ZodString;
|
|
62
|
+
cwd: z.ZodOptional<z.ZodString>;
|
|
63
|
+
}, z.core.$strip>;
|
|
64
|
+
type SessionRef = z.infer<typeof sessionRefSchema>;
|
|
65
|
+
type ProvenanceConfidence = "exact" | "best-effort";
|
|
66
|
+
interface ProvenanceInfo {
|
|
67
|
+
confidence: ProvenanceConfidence;
|
|
68
|
+
sessions: SessionRef[];
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Read the persisted sessions snapshot. Missing file (no daemon has ever
|
|
72
|
+
* run) is a legitimate empty registry, not an error — mirrors
|
|
73
|
+
* `loadHistorySnapshot`'s own ENOENT handling in `sessions.ts`. A malformed
|
|
74
|
+
* file (present but unparseable) is surfaced by returning `null` so callers
|
|
75
|
+
* can distinguish "no history" from "history is unreadable".
|
|
76
|
+
*/
|
|
77
|
+
declare function readSessionsRegistry(path?: string): Promise<SessionRef[] | null>;
|
|
78
|
+
/** `session.cwd == wtPath || session.cwd.startsWith(wtPath + sep)` (PLAN.md §1.5 — containment, not equality). */
|
|
79
|
+
declare function sessionInWorktree(session: SessionRef, worktreePath: string): boolean;
|
|
80
|
+
/** The creation marker PR-B writes into `$(git rev-parse --git-dir)/agentproto-worktree.json`. */
|
|
81
|
+
declare const worktreeMarkerSchema: z.ZodObject<{
|
|
82
|
+
worktreeId: z.ZodString;
|
|
83
|
+
createdAt: z.ZodString;
|
|
84
|
+
createdBySessionId: z.ZodOptional<z.ZodString>;
|
|
85
|
+
}, z.core.$strip>;
|
|
86
|
+
type WorktreeMarker = z.infer<typeof worktreeMarkerSchema>;
|
|
87
|
+
/**
|
|
88
|
+
* Read the per-worktree creation marker, if PR-B has written one. This PR
|
|
89
|
+
* never writes it — only reads, so a worktree created before PR-B shipped
|
|
90
|
+
* (all of them, today) honestly reports `confidence: "best-effort"`.
|
|
91
|
+
*/
|
|
92
|
+
declare function readWorktreeMarker(worktreePath: string): Promise<WorktreeMarker | null>;
|
|
93
|
+
/**
|
|
94
|
+
* Write the creation marker into `$(git rev-parse --git-dir)/agentproto-
|
|
95
|
+
* worktree.json` — the worktree's own private gitdir, not the shared
|
|
96
|
+
* `.git/config` (PLAN.md §1.5 rejected `git config --local` for exactly
|
|
97
|
+
* this reason: in a linked worktree it reads/writes the main repo's shared
|
|
98
|
+
* config, so it wouldn't die with the worktree and would collide across
|
|
99
|
+
* worktrees). Called once, by `worktree.provision`, right after the
|
|
100
|
+
* worktree is created; nothing else ever rewrites it.
|
|
101
|
+
*/
|
|
102
|
+
declare function writeWorktreeMarker(worktreePath: string, marker: WorktreeMarker): Promise<void>;
|
|
103
|
+
interface ComputeProvenanceOptions {
|
|
104
|
+
sessionsPath?: string;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The §1.5 join: sessions whose `cwd` is contained in `worktreePath`,
|
|
108
|
+
* bounded by the marker's `createdAt` when present (filters out sessions
|
|
109
|
+
* from a prior generation at the same path). `confidence` is `"exact"` only
|
|
110
|
+
* when that marker exists; otherwise `"best-effort"` — the join may span
|
|
111
|
+
* generations and is bounded by `HISTORY_CAP`'s recency window.
|
|
112
|
+
*/
|
|
113
|
+
declare function computeProvenance(worktreePath: string, options?: ComputeProvenanceOptions): Promise<ProvenanceInfo>;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Salvage-before-discard (PLAN.md §5.2 layer 4). `worktree archive` re-earns
|
|
117
|
+
* its name by snapshotting a worktree's uncommitted state before it removes
|
|
118
|
+
* anything: `git diff HEAD` → `changes.patch`, plus a byte-copy of every
|
|
119
|
+
* unignored untracked file, into
|
|
120
|
+
* `~/.agentproto/worktree-salvage/<repoName>/<slug>-<tipSha7>-<date>/` with a
|
|
121
|
+
* `MANIFEST.json`. Every file is fsynced before this returns — only once the
|
|
122
|
+
* snapshot is durable does the caller proceed to force-remove.
|
|
123
|
+
*
|
|
124
|
+
* Deliberately forge-independent: no PR lookup here. This writer's only job
|
|
125
|
+
* is "don't lose bytes before deletion," not classification — `ls --status`
|
|
126
|
+
* (status.ts) already owns the forge-derived `integration` axis, and gc
|
|
127
|
+
* (PR-D) is where a salvage manifest and a status entry would be cross-
|
|
128
|
+
* referenced, not here.
|
|
129
|
+
*/
|
|
130
|
+
|
|
131
|
+
declare const SALVAGE_ROOT: () => string;
|
|
132
|
+
interface SalvageManifest {
|
|
133
|
+
repo: string;
|
|
134
|
+
/** `null` for a detached HEAD. */
|
|
135
|
+
branch: string | null;
|
|
136
|
+
tipSha: string;
|
|
137
|
+
createdAt: string;
|
|
138
|
+
/** Relative paths of every unignored untracked file that was copied in. */
|
|
139
|
+
untrackedFiles: string[];
|
|
140
|
+
/** Whether `changes.patch` has any content (tracked-file changes existed). */
|
|
141
|
+
hasPatch: boolean;
|
|
142
|
+
provenance: ProvenanceInfo;
|
|
143
|
+
}
|
|
144
|
+
interface SalvageWorktreeInput {
|
|
145
|
+
repoName: string;
|
|
146
|
+
worktreePath: string;
|
|
147
|
+
branch: string | null;
|
|
148
|
+
tipSha: string;
|
|
149
|
+
/** Identifies this worktree in the salvage directory name, e.g. its slug or branch. */
|
|
150
|
+
slug: string;
|
|
151
|
+
sessionsPath?: string;
|
|
152
|
+
/** Injectable clock so tests can freeze `createdAt` / the directory name. */
|
|
153
|
+
now?: () => string;
|
|
154
|
+
/** Override for `~/.agentproto/worktree-salvage` — tests use a temp dir. */
|
|
155
|
+
salvageRoot?: string;
|
|
156
|
+
}
|
|
157
|
+
interface SalvageResult {
|
|
158
|
+
dir: string;
|
|
159
|
+
manifest: SalvageManifest;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Snapshot `worktreePath`'s uncommitted state into a fresh salvage
|
|
163
|
+
* directory. Safe to call on a clean tree (writes an empty patch, no
|
|
164
|
+
* untracked files, still a manifest) — every path this function returns is
|
|
165
|
+
* fsynced before it resolves.
|
|
166
|
+
*/
|
|
167
|
+
declare function salvageWorktree(input: SalvageWorktreeInput): Promise<SalvageResult>;
|
|
168
|
+
|
|
9
169
|
interface ExecResult {
|
|
10
170
|
exitCode: number;
|
|
11
171
|
stdout: string;
|
|
12
172
|
stderr: string;
|
|
13
173
|
}
|
|
174
|
+
/** Extra `AGENTPROTO_*` (or any) vars, merged onto `process.env` for the child. */
|
|
175
|
+
interface ExecOptions {
|
|
176
|
+
env?: Record<string, string>;
|
|
177
|
+
}
|
|
14
178
|
/** Run an argv (no shell — args pass through verbatim, no injection risk). */
|
|
15
|
-
declare function execArgv(command: string, args: readonly string[], cwd: string): Promise<ExecResult>;
|
|
179
|
+
declare function execArgv(command: string, args: readonly string[], cwd: string, opts?: ExecOptions): Promise<ExecResult>;
|
|
16
180
|
/**
|
|
17
181
|
* Run a single command string through a shell. Callers pass `depsCmd` /
|
|
18
|
-
* `gateCmd` as a trusted, developer-authored
|
|
19
|
-
* model as a CI job's `script:` line) — not untrusted
|
|
182
|
+
* `gateCmd` / a config `setup` line as a trusted, developer-authored value
|
|
183
|
+
* (the same trust model as a CI job's `script:` line) — not untrusted
|
|
184
|
+
* end-user input. `opts.env` layers `AGENTPROTO_*` vars onto the child.
|
|
20
185
|
*/
|
|
21
|
-
declare function execShell(cmd: string, cwd: string): Promise<ExecResult>;
|
|
186
|
+
declare function execShell(cmd: string, cwd: string, opts?: ExecOptions): Promise<ExecResult>;
|
|
22
187
|
declare function execGit(repoRoot: string, args: readonly string[]): Promise<ExecResult>;
|
|
23
188
|
|
|
24
189
|
/** Compile a `/`-separated glob (supporting `*`, `?`, `**`) into an anchored regex. */
|
|
@@ -26,6 +191,359 @@ declare function globToRegExp(pattern: string): RegExp;
|
|
|
26
191
|
/** Expand a glob against `root`, returning root-relative posix paths of matching files. */
|
|
27
192
|
declare function expandGlob(root: string, pattern: string): Promise<string[]>;
|
|
28
193
|
|
|
194
|
+
/**
|
|
195
|
+
* `agentproto.json` — the declarative per-repo worktree lifecycle.
|
|
196
|
+
*
|
|
197
|
+
* SECURITY: this file is always read from the **committed tree of the base
|
|
198
|
+
* ref** (`git show <base>:agentproto.json`), never from a worktree's working
|
|
199
|
+
* tree. A branch (or an agent editing files inside a worktree) therefore
|
|
200
|
+
* cannot inject setup/teardown hooks or service commands that run on the host
|
|
201
|
+
* — only what a reviewer merged into the base branch executes.
|
|
202
|
+
*/
|
|
203
|
+
declare const CONFIG_FILENAME = "agentproto.json";
|
|
204
|
+
/** `scripts.<name>` — a command, optionally a long-running service with a port. */
|
|
205
|
+
declare const scriptSchema: z.ZodObject<{
|
|
206
|
+
command: z.ZodString;
|
|
207
|
+
type: z.ZodOptional<z.ZodLiteral<"service">>;
|
|
208
|
+
port: z.ZodOptional<z.ZodNumber>;
|
|
209
|
+
}, z.core.$strip>;
|
|
210
|
+
declare const configSchema: z.ZodObject<{
|
|
211
|
+
worktree: z.ZodOptional<z.ZodObject<{
|
|
212
|
+
setup: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
213
|
+
teardown: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
214
|
+
}, z.core.$strip>>;
|
|
215
|
+
scripts: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
216
|
+
command: z.ZodString;
|
|
217
|
+
type: z.ZodOptional<z.ZodLiteral<"service">>;
|
|
218
|
+
port: z.ZodOptional<z.ZodNumber>;
|
|
219
|
+
}, z.core.$strip>>>;
|
|
220
|
+
}, z.core.$strip>;
|
|
221
|
+
type ScriptConfig = z.infer<typeof scriptSchema>;
|
|
222
|
+
type AgentprotoConfig = z.infer<typeof configSchema>;
|
|
223
|
+
/** A validation failure with the offending file surfaced for error messages. */
|
|
224
|
+
declare class ConfigError extends Error {
|
|
225
|
+
constructor(message: string);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Parse + validate raw `agentproto.json` text. Throws {@link ConfigError} on
|
|
229
|
+
* malformed JSON or a schema violation.
|
|
230
|
+
*/
|
|
231
|
+
declare function parseConfig(raw: string): AgentprotoConfig;
|
|
232
|
+
/** Normalise a hook (string | string[] | undefined) into an ordered command list. */
|
|
233
|
+
declare function normalizeHook(hook: string | readonly string[] | undefined): string[];
|
|
234
|
+
/** A named script, with its config attached — the shape supervisor/tools consume. */
|
|
235
|
+
interface NamedScript extends ScriptConfig {
|
|
236
|
+
name: string;
|
|
237
|
+
}
|
|
238
|
+
/** All declared scripts as a list. */
|
|
239
|
+
declare function listScripts(config: AgentprotoConfig): NamedScript[];
|
|
240
|
+
/** Only the `type: "service"` scripts. */
|
|
241
|
+
declare function listServices(config: AgentprotoConfig): NamedScript[];
|
|
242
|
+
/** Look up a single named script, or `undefined` if it isn't declared. */
|
|
243
|
+
declare function getScript(config: AgentprotoConfig, name: string): NamedScript | undefined;
|
|
244
|
+
/**
|
|
245
|
+
* Load `agentproto.json` from the **committed tree** of `base` (default
|
|
246
|
+
* `origin/main`) via `git show <base>:agentproto.json`. Returns `null` when
|
|
247
|
+
* the base tree has no such file (the common "repo hasn't opted in" case).
|
|
248
|
+
* Throws {@link ConfigError} on a present-but-invalid file.
|
|
249
|
+
*/
|
|
250
|
+
declare function loadConfigFromBase(repoRoot: string, base?: string): Promise<AgentprotoConfig | null>;
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Typed env surface for worktree lifecycle hooks, scripts, and services.
|
|
254
|
+
*
|
|
255
|
+
* Every `AGENTPROTO_*` variable the lifecycle feature injects is named here
|
|
256
|
+
* once — bodies compose env via {@link hookEnv} / {@link serviceEnv} rather
|
|
257
|
+
* than reading `process.env` (or writing literal `"AGENTPROTO_..."` keys)
|
|
258
|
+
* ad hoc. Keeps the contract in one place and the derivations testable.
|
|
259
|
+
*/
|
|
260
|
+
/** The `AGENTPROTO_*` variables injected into every hook / script / service. */
|
|
261
|
+
declare const ENV_VARS: {
|
|
262
|
+
/** Absolute path to the original repo checkout the worktree was cut from. */
|
|
263
|
+
readonly sourceCheckoutPath: "AGENTPROTO_SOURCE_CHECKOUT_PATH";
|
|
264
|
+
/** Absolute path to the worktree directory. */
|
|
265
|
+
readonly worktreePath: "AGENTPROTO_WORKTREE_PATH";
|
|
266
|
+
/** The worktree's branch name. */
|
|
267
|
+
readonly branchName: "AGENTPROTO_BRANCH_NAME";
|
|
268
|
+
/** A service's own allocated port. */
|
|
269
|
+
readonly port: "AGENTPROTO_PORT";
|
|
270
|
+
/** A service's own reverse-proxy URL. */
|
|
271
|
+
readonly url: "AGENTPROTO_URL";
|
|
272
|
+
};
|
|
273
|
+
/** The shared context every worktree lifecycle command runs against. */
|
|
274
|
+
interface WorktreeEnvContext {
|
|
275
|
+
/** Absolute path to the original repo checkout. */
|
|
276
|
+
sourceCheckoutPath: string;
|
|
277
|
+
/** Absolute path to the worktree directory. */
|
|
278
|
+
worktreePath: string;
|
|
279
|
+
/** The worktree's branch name. */
|
|
280
|
+
branchName: string;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Base env shared by hooks, scripts, and services — the worktree location and
|
|
284
|
+
* the branch/checkout it derives from. Values are strings only (never
|
|
285
|
+
* `undefined`), so the result merges cleanly onto `process.env`.
|
|
286
|
+
*/
|
|
287
|
+
declare function hookEnv(ctx: WorktreeEnvContext): Record<string, string>;
|
|
288
|
+
/** A sibling service, as seen by a peer's env. */
|
|
289
|
+
interface PeerService {
|
|
290
|
+
/** The declared script name (e.g. `"web"`). */
|
|
291
|
+
name: string;
|
|
292
|
+
/** The port the service listens on. */
|
|
293
|
+
port: number;
|
|
294
|
+
/** The service's reverse-proxy URL. */
|
|
295
|
+
url: string;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Upper-case a service name and replace every non-alphanumeric run with a
|
|
299
|
+
* single `_`, for use inside an env var key (`web-api` → `WEB_API`).
|
|
300
|
+
*/
|
|
301
|
+
declare function serviceEnvToken(name: string): string;
|
|
302
|
+
/** `AGENTPROTO_SERVICE_<TOKEN>_PORT` / `..._URL` for a named peer. */
|
|
303
|
+
declare function peerEnvKey(name: string, suffix: "PORT" | "URL"): string;
|
|
304
|
+
/**
|
|
305
|
+
* Peer-discovery env for a set of sibling services: each contributes
|
|
306
|
+
* `AGENTPROTO_SERVICE_<NAME>_PORT` and `AGENTPROTO_SERVICE_<NAME>_URL`.
|
|
307
|
+
*/
|
|
308
|
+
declare function peerEnv(peers: readonly PeerService[]): Record<string, string>;
|
|
309
|
+
/**
|
|
310
|
+
* Full env for a running service: the shared {@link hookEnv}, the service's
|
|
311
|
+
* own `AGENTPROTO_PORT` / `AGENTPROTO_URL`, and {@link peerEnv} for every
|
|
312
|
+
* sibling.
|
|
313
|
+
*/
|
|
314
|
+
declare function serviceEnv(args: {
|
|
315
|
+
ctx: WorktreeEnvContext;
|
|
316
|
+
self: PeerService;
|
|
317
|
+
peers: readonly PeerService[];
|
|
318
|
+
}): Record<string, string>;
|
|
319
|
+
|
|
320
|
+
/** One hook command's result, kept for error reporting / logging. */
|
|
321
|
+
interface HookRun {
|
|
322
|
+
command: string;
|
|
323
|
+
result: ExecResult;
|
|
324
|
+
}
|
|
325
|
+
/** Raised when a `setup` hook exits non-zero — carries the captured output. */
|
|
326
|
+
declare class HookError extends Error {
|
|
327
|
+
readonly command: string;
|
|
328
|
+
readonly result: ExecResult;
|
|
329
|
+
constructor(phase: "setup" | "teardown", run: HookRun);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Run the `worktree.setup` hooks sequentially in the worktree, with the
|
|
333
|
+
* `AGENTPROTO_*` context env injected. The first non-zero exit throws
|
|
334
|
+
* {@link HookError} (setup failure must fail provisioning). No-op when the
|
|
335
|
+
* repo declares no setup.
|
|
336
|
+
*/
|
|
337
|
+
declare function runSetup(config: AgentprotoConfig, ctx: WorktreeEnvContext): Promise<HookRun[]>;
|
|
338
|
+
/**
|
|
339
|
+
* Run the `worktree.teardown` hooks sequentially. Unlike setup, a failing
|
|
340
|
+
* teardown hook does NOT throw — cleanup must proceed — so failures are
|
|
341
|
+
* returned for the caller to log. No-op when no teardown is declared.
|
|
342
|
+
*/
|
|
343
|
+
declare function runTeardown(config: AgentprotoConfig, ctx: WorktreeEnvContext): Promise<HookRun[]>;
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* The proxy routing table: `<hostname>` → local service port.
|
|
347
|
+
*
|
|
348
|
+
* Kept separate from the HTTP server (see `proxy-server.ts`) so the supervisor
|
|
349
|
+
* can register/unregister routes without owning a live socket, and so routing
|
|
350
|
+
* can be unit-tested without binding a port.
|
|
351
|
+
*/
|
|
352
|
+
declare class ProxyTable {
|
|
353
|
+
private readonly routes;
|
|
354
|
+
/** Register (or replace) the target port for a hostname. */
|
|
355
|
+
set(hostname: string, port: number): void;
|
|
356
|
+
/** Remove a hostname's route. Returns whether it was present. */
|
|
357
|
+
delete(hostname: string): boolean;
|
|
358
|
+
/** The target port registered for an exact hostname, if any. */
|
|
359
|
+
get(hostname: string): number | undefined;
|
|
360
|
+
/**
|
|
361
|
+
* Resolve a raw `Host` header (which may carry a `:port` suffix, and for
|
|
362
|
+
* IPv6 a bracketed host) to a target port.
|
|
363
|
+
*/
|
|
364
|
+
lookup(hostHeader: string | undefined): number | undefined;
|
|
365
|
+
/** All `[hostname, port]` routes currently registered. */
|
|
366
|
+
entries(): Array<[string, number]>;
|
|
367
|
+
/** Number of registered routes. */
|
|
368
|
+
get size(): number;
|
|
369
|
+
}
|
|
370
|
+
/** Strip a trailing `:port` from a Host header, honouring `[ipv6]:port`. */
|
|
371
|
+
declare function stripPort(hostHeader: string): string;
|
|
372
|
+
|
|
373
|
+
/** A declared service with its resolved port / hostname / proxy URL. */
|
|
374
|
+
interface ServiceRuntime {
|
|
375
|
+
name: string;
|
|
376
|
+
command: string;
|
|
377
|
+
hostname: string;
|
|
378
|
+
port: number;
|
|
379
|
+
url: string;
|
|
380
|
+
}
|
|
381
|
+
/** The observable state of a service (v1: no restart — just track the exit). */
|
|
382
|
+
interface ServiceStatus {
|
|
383
|
+
name: string;
|
|
384
|
+
hostname: string;
|
|
385
|
+
port: number;
|
|
386
|
+
url: string;
|
|
387
|
+
pid: number | null;
|
|
388
|
+
status: "running" | "exited";
|
|
389
|
+
exitCode: number | null;
|
|
390
|
+
startedAt: string | null;
|
|
391
|
+
}
|
|
392
|
+
interface SupervisorOptions {
|
|
393
|
+
ctx: WorktreeEnvContext;
|
|
394
|
+
/** The `type: "service"` scripts from the repo's `agentproto.json`. */
|
|
395
|
+
services: readonly NamedScript[];
|
|
396
|
+
/** Repo label for the hostname (typically the repo dir basename). */
|
|
397
|
+
repo: string;
|
|
398
|
+
/** Whether the worktree's branch is the repo default (drops the branch label). */
|
|
399
|
+
isDefaultBranch: boolean;
|
|
400
|
+
/** Port the reverse proxy listens on — used to build each service's URL. */
|
|
401
|
+
proxyPort: number;
|
|
402
|
+
/** When given, service hostnames are (de)registered here as they start/stop. */
|
|
403
|
+
proxyTable?: ProxyTable;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Supervises the long-running services declared for one worktree. Ports are
|
|
407
|
+
* allocated up front for every declared service so peer discovery is complete
|
|
408
|
+
* (a service started later is visible to one started earlier only if its port
|
|
409
|
+
* was reserved — which it is), and hostnames register with the proxy table as
|
|
410
|
+
* services come up.
|
|
411
|
+
*
|
|
412
|
+
* v1 scope: no crash-restart loop. A service that exits is marked `exited`
|
|
413
|
+
* with its code; callers decide what to do.
|
|
414
|
+
*/
|
|
415
|
+
declare class ServiceSupervisor {
|
|
416
|
+
private readonly opts;
|
|
417
|
+
private readonly tracked;
|
|
418
|
+
private constructor();
|
|
419
|
+
/**
|
|
420
|
+
* Build a supervisor, allocating a port for every declared service (declared
|
|
421
|
+
* port if free, else ephemeral) and resolving each one's hostname + URL.
|
|
422
|
+
* Nothing is spawned yet.
|
|
423
|
+
*/
|
|
424
|
+
static create(opts: SupervisorOptions): Promise<ServiceSupervisor>;
|
|
425
|
+
/** Every declared service's resolved runtime (ports/urls), regardless of state. */
|
|
426
|
+
runtimes(): ServiceRuntime[];
|
|
427
|
+
/** Peers of `name` — every other declared service, as `PeerService` records. */
|
|
428
|
+
private peersOf;
|
|
429
|
+
private toStatus;
|
|
430
|
+
/**
|
|
431
|
+
* Start a declared service. Idempotent: returns the current status if it's
|
|
432
|
+
* already running. Injects `AGENTPROTO_PORT` / `AGENTPROTO_URL` for itself
|
|
433
|
+
* plus `AGENTPROTO_SERVICE_<PEER>_PORT|URL` for every sibling.
|
|
434
|
+
*/
|
|
435
|
+
start(name: string): ServiceStatus;
|
|
436
|
+
/** Stop a running service (SIGTERM). Returns whether one was running. */
|
|
437
|
+
stop(name: string): Promise<boolean>;
|
|
438
|
+
/** Stop every running service. */
|
|
439
|
+
stopAll(): Promise<void>;
|
|
440
|
+
/** Current status of a single service, or `undefined` if not declared. */
|
|
441
|
+
get(name: string): ServiceStatus | undefined;
|
|
442
|
+
/** Current status of every declared service. */
|
|
443
|
+
list(): ServiceStatus[];
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** Build (but don't start) the proxy HTTP server backed by `table`. */
|
|
447
|
+
declare function createProxyServer(table: ProxyTable): http.Server;
|
|
448
|
+
interface RunningProxy {
|
|
449
|
+
/** The port the proxy is listening on. */
|
|
450
|
+
port: number;
|
|
451
|
+
/** The underlying server. */
|
|
452
|
+
server: http.Server;
|
|
453
|
+
/** Stop the proxy. */
|
|
454
|
+
close(): Promise<void>;
|
|
455
|
+
}
|
|
456
|
+
/** Start the proxy server on `port` (0 → OS-assigned). Resolves once listening. */
|
|
457
|
+
declare function startProxy(table: ProxyTable, port?: number): Promise<RunningProxy>;
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Hostname derivation for the `*.localhost` reverse proxy.
|
|
461
|
+
*
|
|
462
|
+
* A service is reachable at:
|
|
463
|
+
* http://<script>--<branch-slug>--<repo-slug>.localhost:<proxy-port>
|
|
464
|
+
* except on the repo's default branch, where the branch label is dropped:
|
|
465
|
+
* http://<script>--<repo-slug>.localhost:<proxy-port>
|
|
466
|
+
*
|
|
467
|
+
* `*.localhost` resolves to 127.0.0.1 on modern systems, so no DNS work is
|
|
468
|
+
* needed — the proxy just matches on the Host header.
|
|
469
|
+
*/
|
|
470
|
+
/** Lowercase, non-alphanumerics → `-`, collapse repeats, trim leading/trailing `-`. */
|
|
471
|
+
declare function slugify(input: string): string;
|
|
472
|
+
interface HostnameParts {
|
|
473
|
+
/** The script/service name (e.g. `"web"`). */
|
|
474
|
+
script: string;
|
|
475
|
+
/** The worktree's branch (e.g. `"wt/fix-flaky"`). */
|
|
476
|
+
branch: string;
|
|
477
|
+
/** The repo identifier (e.g. its directory basename). */
|
|
478
|
+
repo: string;
|
|
479
|
+
/** True when `branch` is the repo's default branch — drops the branch label. */
|
|
480
|
+
isDefaultBranch: boolean;
|
|
481
|
+
}
|
|
482
|
+
/** The bare hostname (no scheme, no port) a service is proxied under. */
|
|
483
|
+
declare function serviceHostname(parts: HostnameParts): string;
|
|
484
|
+
/** The full proxy URL (`http://<hostname>:<proxy-port>`) for a service. */
|
|
485
|
+
declare function serviceUrl(parts: HostnameParts, proxyPort: number): string;
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Port allocation for supervised services.
|
|
489
|
+
*
|
|
490
|
+
* A service declares a preferred `port`; we use it if free, otherwise we fall
|
|
491
|
+
* back to an OS-assigned ephemeral port. Allocation binds a probe socket on
|
|
492
|
+
* 127.0.0.1 to decide — there's an inherent TOCTOU window between the probe
|
|
493
|
+
* closing and the service binding, but for local dev supervision that's the
|
|
494
|
+
* same race every "is this port free" check has, and the service will simply
|
|
495
|
+
* fail to bind if something grabbed it in between.
|
|
496
|
+
*/
|
|
497
|
+
/** Resolve to `true` if a TCP listen on `127.0.0.1:<port>` succeeds. */
|
|
498
|
+
declare function isPortFree(port: number): Promise<boolean>;
|
|
499
|
+
/** Ask the OS for a free ephemeral port (bind to 0, read the assigned port). */
|
|
500
|
+
declare function ephemeralPort(): Promise<number>;
|
|
501
|
+
/**
|
|
502
|
+
* Allocate a port for a service: the declared `preferred` if it's free,
|
|
503
|
+
* otherwise an OS-assigned ephemeral port. Ports in `reserved` (already handed
|
|
504
|
+
* out to sibling services this session) are treated as taken.
|
|
505
|
+
*/
|
|
506
|
+
declare function allocatePort(preferred: number | undefined, reserved?: ReadonlySet<number>): Promise<number>;
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Derive the hostname-facing repo label from a repo root path — its directory
|
|
510
|
+
* basename (the proxy slugs it further).
|
|
511
|
+
*/
|
|
512
|
+
declare function repoLabel(repoRoot: string): string;
|
|
513
|
+
/**
|
|
514
|
+
* Best-effort detection of a repo's default branch, for deciding whether the
|
|
515
|
+
* branch label is dropped from a service hostname. Prefers the symbolic
|
|
516
|
+
* `origin/HEAD` ref, falls back to `main` / `master` presence, then `main`.
|
|
517
|
+
*/
|
|
518
|
+
declare function detectDefaultBranch(repoRoot: string): Promise<string>;
|
|
519
|
+
|
|
520
|
+
/** The shared reverse-proxy routing table (hostname → service port). */
|
|
521
|
+
declare const sharedProxyTable: ProxyTable;
|
|
522
|
+
/** Default reverse-proxy port when a caller doesn't pin one. */
|
|
523
|
+
declare const DEFAULT_PROXY_PORT = 18780;
|
|
524
|
+
interface ResolveSupervisorInput {
|
|
525
|
+
repoRoot: string;
|
|
526
|
+
worktreePath: string;
|
|
527
|
+
branch: string;
|
|
528
|
+
base?: string;
|
|
529
|
+
proxyPort?: number;
|
|
530
|
+
}
|
|
531
|
+
/** The context the service tools resolve before touching the supervisor. */
|
|
532
|
+
interface ResolvedWorktreeRuntime {
|
|
533
|
+
supervisor: ServiceSupervisor;
|
|
534
|
+
config: AgentprotoConfig;
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Resolve (creating on first use) the supervisor for a worktree: read the
|
|
538
|
+
* committed `agentproto.json` from `base`, build service runtimes with
|
|
539
|
+
* allocated ports, and cache it keyed by worktree path.
|
|
540
|
+
*/
|
|
541
|
+
declare function resolveSupervisor(input: ResolveSupervisorInput): Promise<ResolvedWorktreeRuntime>;
|
|
542
|
+
/** The cached supervisor for a worktree, if one has been resolved. */
|
|
543
|
+
declare function getSupervisor(worktreePath: string): ServiceSupervisor | undefined;
|
|
544
|
+
/** Stop all services for a worktree and drop its supervisor (used by cleanup). */
|
|
545
|
+
declare function disposeSupervisor(worktreePath: string): Promise<void>;
|
|
546
|
+
|
|
29
547
|
/**
|
|
30
548
|
* Input to the worktree-agent workflow: launch a coding agent inside a
|
|
31
549
|
* fresh git worktree, gate its work, and — on approval — clean up.
|
|
@@ -50,6 +568,410 @@ type WorktreeAgentInput = z.infer<typeof worktreeAgentInputSchema>;
|
|
|
50
568
|
*/
|
|
51
569
|
declare const worktreeAgentWorkflow: RuntimeWorkflow;
|
|
52
570
|
|
|
571
|
+
/**
|
|
572
|
+
* `ForgeClient` — the only network-facing surface the reconciliation rule
|
|
573
|
+
* (`status.ts`) depends on. Kept as an interface (not a concrete `gh` call)
|
|
574
|
+
* so:
|
|
575
|
+
* 1. `status.ts` stays testable with a fake — the fixture-repo tests never
|
|
576
|
+
* shell out to a real forge or touch the network.
|
|
577
|
+
* 2. The shape is forge-portable by construction (PLAN.md §7.1): GitLab has
|
|
578
|
+
* the same two facts — "PRs/MRs whose head is this branch" and "PRs/MRs
|
|
579
|
+
* that contain this commit" — under different verbs
|
|
580
|
+
* (`refs/merge-requests/<n>/head`, `GET /merge_requests?…`). Only
|
|
581
|
+
* GitHub ships today; a `GitlabForgeClient` is a second implementation
|
|
582
|
+
* of this same interface, not a redesign.
|
|
583
|
+
*
|
|
584
|
+
* Two implementations ship: `GhCliForgeClient` (shells out to the `gh` CLI,
|
|
585
|
+
* inheriting the caller's own `gh auth` session — zero config) and
|
|
586
|
+
* `RestForgeClient` (plain `fetch` against the GitHub REST API using
|
|
587
|
+
* `GITHUB_TOKEN` — no new npm dependency). `createForgeClient` picks
|
|
588
|
+
* whichever is usable; when neither is, callers get a client that always
|
|
589
|
+
* reports itself unreachable, which the reconciliation rule already knows
|
|
590
|
+
* how to fall back from (PLAN.md §1.3 step 5: memo, else `unknown(offline)`,
|
|
591
|
+
* never a guess).
|
|
592
|
+
*/
|
|
593
|
+
/** A pull/merge request as the reconciliation rule needs to see it. */
|
|
594
|
+
interface ForgePullRequestRef {
|
|
595
|
+
number: number;
|
|
596
|
+
state: "open" | "closed";
|
|
597
|
+
merged: boolean;
|
|
598
|
+
mergedAt: string | null;
|
|
599
|
+
headRefName: string;
|
|
600
|
+
headRefOid: string;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Thrown by a `ForgeClient` method when the forge could not be reached or
|
|
604
|
+
* queried at all (network failure, missing/unauthenticated CLI, non-2xx HTTP
|
|
605
|
+
* response). Never thrown for a legitimate empty result (a branch/commit
|
|
606
|
+
* with no PRs is a valid, resolvable answer — `[]`, not an error). Callers
|
|
607
|
+
* treat this as PLAN.md §1.3 step 5: "never guess."
|
|
608
|
+
*/
|
|
609
|
+
declare class ForgeUnavailableError extends Error {
|
|
610
|
+
constructor(message: string, options?: {
|
|
611
|
+
cause?: unknown;
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
interface ForgeClient {
|
|
615
|
+
/** PRs/MRs (any state) whose head branch is exactly `branch`. */
|
|
616
|
+
pullRequestsForBranch(branch: string): Promise<ForgePullRequestRef[]>;
|
|
617
|
+
/**
|
|
618
|
+
* PRs/MRs associated with a specific commit SHA — the rename/retarget-proof
|
|
619
|
+
* signal (PLAN.md §1.3 step 2b): finds the PR even if the branch was
|
|
620
|
+
* renamed or deleted before merge.
|
|
621
|
+
*/
|
|
622
|
+
pullRequestsForCommit(sha: string): Promise<ForgePullRequestRef[]>;
|
|
623
|
+
/**
|
|
624
|
+
* Ensure `oid` (a PR's `headRefOid`) is present in the local object
|
|
625
|
+
* database, fetching `refs/pull/<number>/head` if it isn't. Always
|
|
626
|
+
* fetchable even after the head branch was deleted on the remote
|
|
627
|
+
* (PLAN.md §1.3 step 2, verified for #273/#312/#325/#271).
|
|
628
|
+
*/
|
|
629
|
+
ensurePullHeadFetched(prNumber: number, oid: string): Promise<void>;
|
|
630
|
+
}
|
|
631
|
+
/** A client that always reports itself unreachable — the "no gh, no token" case. */
|
|
632
|
+
declare class UnreachableForgeClient implements ForgeClient {
|
|
633
|
+
private readonly reason;
|
|
634
|
+
constructor(reason: string);
|
|
635
|
+
pullRequestsForBranch(): Promise<ForgePullRequestRef[]>;
|
|
636
|
+
pullRequestsForCommit(): Promise<ForgePullRequestRef[]>;
|
|
637
|
+
ensurePullHeadFetched(): Promise<void>;
|
|
638
|
+
}
|
|
639
|
+
/** Runs `gh` in `repoRoot` so it auto-detects owner/repo from the `origin` remote there. */
|
|
640
|
+
declare class GhCliForgeClient implements ForgeClient {
|
|
641
|
+
private readonly repoRoot;
|
|
642
|
+
constructor(repoRoot: string);
|
|
643
|
+
private runRaw;
|
|
644
|
+
private run;
|
|
645
|
+
private parseOutput;
|
|
646
|
+
pullRequestsForBranch(branch: string): Promise<ForgePullRequestRef[]>;
|
|
647
|
+
pullRequestsForCommit(sha: string): Promise<ForgePullRequestRef[]>;
|
|
648
|
+
ensurePullHeadFetched(prNumber: number, oid: string): Promise<void>;
|
|
649
|
+
}
|
|
650
|
+
/** Parse `owner/repo` out of a `git remote get-url origin` value (ssh or https form). */
|
|
651
|
+
declare function parseGithubOwnerRepo(remoteUrl: string): {
|
|
652
|
+
owner: string;
|
|
653
|
+
repo: string;
|
|
654
|
+
} | null;
|
|
655
|
+
declare class RestForgeClient implements ForgeClient {
|
|
656
|
+
private readonly owner;
|
|
657
|
+
private readonly repo;
|
|
658
|
+
private readonly repoRoot;
|
|
659
|
+
private readonly token;
|
|
660
|
+
private readonly fetchFn;
|
|
661
|
+
constructor(owner: string, repo: string, repoRoot: string, token: string, fetchFn?: typeof fetch);
|
|
662
|
+
private headers;
|
|
663
|
+
private get;
|
|
664
|
+
private pullRequestList;
|
|
665
|
+
pullRequestsForBranch(branch: string): Promise<ForgePullRequestRef[]>;
|
|
666
|
+
pullRequestsForCommit(sha: string): Promise<ForgePullRequestRef[]>;
|
|
667
|
+
ensurePullHeadFetched(prNumber: number, oid: string): Promise<void>;
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Pick the best available `ForgeClient` for `repoRoot`: `gh` CLI first (zero
|
|
671
|
+
* config, inherits the caller's own auth), else `GITHUB_TOKEN` REST, else a
|
|
672
|
+
* client that always reports unreachable — the reconciliation rule already
|
|
673
|
+
* knows how to fall back from that (memo, else `unknown(offline)`).
|
|
674
|
+
*/
|
|
675
|
+
declare function createForgeClient(repoRoot: string): Promise<ForgeClient>;
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* The status engine (PLAN.md §1–§1.5): a worktree's status is derived at
|
|
679
|
+
* read time from three orthogonal axes — `tree`, `integration`, `liveness`
|
|
680
|
+
* — plus a `provenance` field that never affects classification. Nothing
|
|
681
|
+
* here is a registry: every value is recomputed from git, the forge, and
|
|
682
|
+
* the sessions snapshot on every call (PLAN.md §1.4's no-registry
|
|
683
|
+
* principle). The one piece of durable state, the verdict memo
|
|
684
|
+
* (`~/.agentproto/worktree-verdicts.json`), is a memo of *immutable* facts
|
|
685
|
+
* only (a merged PR stays merged) — deleting it changes nothing but speed
|
|
686
|
+
* and offline honesty, the acid test a real registry fails.
|
|
687
|
+
*
|
|
688
|
+
* The reconciliation rule (`reconcileIntegration`, PLAN.md §1.3) is the
|
|
689
|
+
* heart of this module: it is why ad-hoc shell answers to "how many
|
|
690
|
+
* worktrees are stale?" went 5 → 22 → 21+8 in one session, and why this is
|
|
691
|
+
* code with tests instead. Every step is a pure function of `(T,
|
|
692
|
+
* origin/main, the forge's merged-PR records)` — content-addressed inputs,
|
|
693
|
+
* so two runs at the same repo state produce byte-identical answers.
|
|
694
|
+
*/
|
|
695
|
+
|
|
696
|
+
type TreeState = {
|
|
697
|
+
state: "clean";
|
|
698
|
+
} | {
|
|
699
|
+
state: "dirty";
|
|
700
|
+
modified: number;
|
|
701
|
+
staged: number;
|
|
702
|
+
untracked: number;
|
|
703
|
+
/**
|
|
704
|
+
* Newest mtime (epoch ms) among the worktree's own dirty paths
|
|
705
|
+
* (unstaged, staged, and untracked), or `null` if it couldn't be
|
|
706
|
+
* determined. Feeds `classify`'s `RECENT_WRITE_HOLD_WINDOW_MS` guard —
|
|
707
|
+
* a liveness-independent "someone touched this a moment ago" signal.
|
|
708
|
+
*/
|
|
709
|
+
newestMtimeMs: number | null;
|
|
710
|
+
};
|
|
711
|
+
/**
|
|
712
|
+
* `git status --porcelain=v2` in the worktree itself (not `repoRoot` — tree
|
|
713
|
+
* state is per-worktree). Gitignored files never appear here (no
|
|
714
|
+
* `--ignored` flag passed), matching git's own non-`--force` `worktree
|
|
715
|
+
* remove` tolerance for them (PLAN.md §0.5) — the two notions of "clean"
|
|
716
|
+
* agree by construction.
|
|
717
|
+
*/
|
|
718
|
+
declare function computeTreeState(worktreePath: string): Promise<TreeState>;
|
|
719
|
+
declare const integrationStateSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
720
|
+
state: z.ZodLiteral<"fresh">;
|
|
721
|
+
checkedAt: z.ZodString;
|
|
722
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
723
|
+
state: z.ZodLiteral<"merged">;
|
|
724
|
+
via: z.ZodLiteral<"squash">;
|
|
725
|
+
pr: z.ZodNumber;
|
|
726
|
+
offline: z.ZodBoolean;
|
|
727
|
+
checkedAt: z.ZodString;
|
|
728
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
729
|
+
state: z.ZodLiteral<"partial">;
|
|
730
|
+
pr: z.ZodNumber;
|
|
731
|
+
aheadBy: z.ZodNumber;
|
|
732
|
+
offline: z.ZodBoolean;
|
|
733
|
+
checkedAt: z.ZodString;
|
|
734
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
735
|
+
state: z.ZodLiteral<"open">;
|
|
736
|
+
pr: z.ZodNumber;
|
|
737
|
+
offline: z.ZodBoolean;
|
|
738
|
+
checkedAt: z.ZodString;
|
|
739
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
740
|
+
state: z.ZodLiteral<"pushed-no-pr">;
|
|
741
|
+
checkedAt: z.ZodString;
|
|
742
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
743
|
+
state: z.ZodLiteral<"unpushed">;
|
|
744
|
+
aheadBy: z.ZodNumber;
|
|
745
|
+
checkedAt: z.ZodString;
|
|
746
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
747
|
+
state: z.ZodLiteral<"local-only">;
|
|
748
|
+
checkedAt: z.ZodString;
|
|
749
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
750
|
+
state: z.ZodLiteral<"gone-unexplained">;
|
|
751
|
+
checkedAt: z.ZodString;
|
|
752
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
753
|
+
state: z.ZodLiteral<"diverged">;
|
|
754
|
+
offline: z.ZodBoolean;
|
|
755
|
+
checkedAt: z.ZodString;
|
|
756
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
757
|
+
state: z.ZodLiteral<"detached">;
|
|
758
|
+
checkedAt: z.ZodString;
|
|
759
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
760
|
+
state: z.ZodLiteral<"unknown">;
|
|
761
|
+
reason: z.ZodLiteral<"offline">;
|
|
762
|
+
checkedAt: z.ZodString;
|
|
763
|
+
}, z.core.$strip>]>;
|
|
764
|
+
/** PLAN.md §1.2's `integration` axis, in full — every state the rule can produce. */
|
|
765
|
+
type IntegrationState = z.infer<typeof integrationStateSchema>;
|
|
766
|
+
declare const verdictMemoRecordSchema: z.ZodObject<{
|
|
767
|
+
repo: z.ZodString;
|
|
768
|
+
branch: z.ZodString;
|
|
769
|
+
tipSha: z.ZodString;
|
|
770
|
+
verdict: z.ZodUnion<readonly [z.ZodObject<{
|
|
771
|
+
state: z.ZodLiteral<"fresh">;
|
|
772
|
+
checkedAt: z.ZodString;
|
|
773
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
774
|
+
state: z.ZodLiteral<"merged">;
|
|
775
|
+
via: z.ZodLiteral<"squash">;
|
|
776
|
+
pr: z.ZodNumber;
|
|
777
|
+
offline: z.ZodBoolean;
|
|
778
|
+
checkedAt: z.ZodString;
|
|
779
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
780
|
+
state: z.ZodLiteral<"partial">;
|
|
781
|
+
pr: z.ZodNumber;
|
|
782
|
+
aheadBy: z.ZodNumber;
|
|
783
|
+
offline: z.ZodBoolean;
|
|
784
|
+
checkedAt: z.ZodString;
|
|
785
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
786
|
+
state: z.ZodLiteral<"open">;
|
|
787
|
+
pr: z.ZodNumber;
|
|
788
|
+
offline: z.ZodBoolean;
|
|
789
|
+
checkedAt: z.ZodString;
|
|
790
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
791
|
+
state: z.ZodLiteral<"pushed-no-pr">;
|
|
792
|
+
checkedAt: z.ZodString;
|
|
793
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
794
|
+
state: z.ZodLiteral<"unpushed">;
|
|
795
|
+
aheadBy: z.ZodNumber;
|
|
796
|
+
checkedAt: z.ZodString;
|
|
797
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
798
|
+
state: z.ZodLiteral<"local-only">;
|
|
799
|
+
checkedAt: z.ZodString;
|
|
800
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
801
|
+
state: z.ZodLiteral<"gone-unexplained">;
|
|
802
|
+
checkedAt: z.ZodString;
|
|
803
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
804
|
+
state: z.ZodLiteral<"diverged">;
|
|
805
|
+
offline: z.ZodBoolean;
|
|
806
|
+
checkedAt: z.ZodString;
|
|
807
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
808
|
+
state: z.ZodLiteral<"detached">;
|
|
809
|
+
checkedAt: z.ZodString;
|
|
810
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
811
|
+
state: z.ZodLiteral<"unknown">;
|
|
812
|
+
reason: z.ZodLiteral<"offline">;
|
|
813
|
+
checkedAt: z.ZodString;
|
|
814
|
+
}, z.core.$strip>]>;
|
|
815
|
+
checkedAt: z.ZodString;
|
|
816
|
+
}, z.core.$strip>;
|
|
817
|
+
type VerdictMemoRecord = z.infer<typeof verdictMemoRecordSchema>;
|
|
818
|
+
declare const VERDICT_MEMO_PATH: () => string;
|
|
819
|
+
/** Only forge-derived verdicts are ever memoised (PLAN.md §1.4/§2) — local-only classifications need no cache. */
|
|
820
|
+
interface VerdictMemoStore {
|
|
821
|
+
get(repo: string, branch: string, tipSha: string): Promise<VerdictMemoRecord | null>;
|
|
822
|
+
set(record: VerdictMemoRecord): Promise<void>;
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* `~/.agentproto/worktree-verdicts.json` — entries keyed by `(repo, branch,
|
|
826
|
+
* tipSha)`. This is NOT a third status authority (PLAN.md §1.4): every
|
|
827
|
+
* entry memoises an immutable fact (a containment check between two fixed
|
|
828
|
+
* SHAs has one eternal answer; a merged PR stays merged). Deleting the file
|
|
829
|
+
* changes nothing but speed and offline honesty.
|
|
830
|
+
*/
|
|
831
|
+
declare class FileVerdictMemoStore implements VerdictMemoStore {
|
|
832
|
+
private readonly path;
|
|
833
|
+
private cache;
|
|
834
|
+
constructor(path?: string);
|
|
835
|
+
private load;
|
|
836
|
+
get(repo: string, branch: string, tipSha: string): Promise<VerdictMemoRecord | null>;
|
|
837
|
+
set(record: VerdictMemoRecord): Promise<void>;
|
|
838
|
+
}
|
|
839
|
+
/** In-memory-only store — for tests, or callers that don't want a durable memo. */
|
|
840
|
+
declare class InMemoryVerdictMemoStore implements VerdictMemoStore {
|
|
841
|
+
private readonly map;
|
|
842
|
+
get(repo: string, branch: string, tipSha: string): Promise<VerdictMemoRecord | null>;
|
|
843
|
+
set(record: VerdictMemoRecord): Promise<void>;
|
|
844
|
+
}
|
|
845
|
+
interface GitWorktreeRef {
|
|
846
|
+
path: string;
|
|
847
|
+
/** `null` for a detached HEAD. */
|
|
848
|
+
branch: string | null;
|
|
849
|
+
head: string;
|
|
850
|
+
}
|
|
851
|
+
/** Parses `git worktree list --porcelain` — every linked worktree of `repoRoot`, wherever it lives. */
|
|
852
|
+
declare function listGitWorktrees(repoRoot: string): Promise<GitWorktreeRef[]>;
|
|
853
|
+
interface ReconcileIntegrationInput {
|
|
854
|
+
repoRoot: string;
|
|
855
|
+
repoName: string;
|
|
856
|
+
/** `null` for a detached HEAD — short-circuits to `{ state: "detached" }` (step 0). */
|
|
857
|
+
branch: string | null;
|
|
858
|
+
tipSha: string;
|
|
859
|
+
forge: ForgeClient;
|
|
860
|
+
memo: VerdictMemoStore;
|
|
861
|
+
/** Default `"origin/main"` — matches `loadConfigFromBase`'s own default. */
|
|
862
|
+
defaultBranchRef?: string;
|
|
863
|
+
/** Injectable clock so callers (the determinism test) can freeze `checkedAt`. */
|
|
864
|
+
now?: () => string;
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* PLAN.md §1.3 — the heart of the design. Implements the six-step rule
|
|
868
|
+
* exactly: local ancestry first (cheap, no forge), then the forge's
|
|
869
|
+
* merged-PR containment check (the squash-proof signal), then open PRs,
|
|
870
|
+
* then local upstream tracking, with the verdict memo as the only fallback
|
|
871
|
+
* when the forge is unreachable. Never guesses (step 5): a memo miss while
|
|
872
|
+
* offline is `unknown(offline)`, full stop.
|
|
873
|
+
*/
|
|
874
|
+
declare function reconcileIntegration(input: ReconcileIntegrationInput): Promise<IntegrationState>;
|
|
875
|
+
/** `services` is always empty in PR-A — service liveness needs the daemon's in-process `ServiceSupervisor` (out of scope, see below). */
|
|
876
|
+
type LivenessState = {
|
|
877
|
+
state: "idle";
|
|
878
|
+
sessions: ProvenanceInfo["sessions"];
|
|
879
|
+
services: string[];
|
|
880
|
+
} | {
|
|
881
|
+
state: "sessions";
|
|
882
|
+
sessions: ProvenanceInfo["sessions"];
|
|
883
|
+
services: string[];
|
|
884
|
+
} | {
|
|
885
|
+
state: "daemon-unreachable";
|
|
886
|
+
sessions: ProvenanceInfo["sessions"];
|
|
887
|
+
services: string[];
|
|
888
|
+
};
|
|
889
|
+
/**
|
|
890
|
+
* Live sessions whose `cwd` is contained in this worktree, read off the
|
|
891
|
+
* same persisted sessions snapshot as provenance (PLAN.md §1.5) — no
|
|
892
|
+
* running daemon required, matching `worktree check`'s no-daemon posture
|
|
893
|
+
* (PLAN.md §3.3). Service liveness needs the daemon's in-process
|
|
894
|
+
* `ServiceSupervisor` and is out of scope here (`services` is always `[]`);
|
|
895
|
+
* `daemon-unreachable` fires only when the sessions snapshot itself is
|
|
896
|
+
* present but unreadable (corrupt/permission-denied), not merely absent —
|
|
897
|
+
* "no daemon has ever run" is a legitimate empty registry, not an error.
|
|
898
|
+
*/
|
|
899
|
+
declare function computeLiveness(worktreePath: string, options?: ComputeProvenanceOptions): Promise<LivenessState>;
|
|
900
|
+
type GateState = {
|
|
901
|
+
state: "none";
|
|
902
|
+
};
|
|
903
|
+
type WorktreeClass = "reclaim" | "salvage" | "hold";
|
|
904
|
+
interface Classification {
|
|
905
|
+
reclaimable: boolean;
|
|
906
|
+
class: WorktreeClass;
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* `reclaimable ⇔ tree=clean ∧ integration ∈ {merged, fresh} ∧ liveness ∈
|
|
910
|
+
* {idle, daemon-unreachable}` (PLAN.md §1.2). `daemon-unreachable` is
|
|
911
|
+
* allowed because the hard guarantee against losing work is git's own
|
|
912
|
+
* non-`--force` removal refusal, not the liveness probe. `fresh` reclaims
|
|
913
|
+
* when clean for the same reason: a clean tree has nothing uncommitted to
|
|
914
|
+
* lose, so it doesn't matter that `fresh` can't distinguish "never had
|
|
915
|
+
* commits" from "real commits, fast-forwarded into main" (see
|
|
916
|
+
* `reconcileIntegration` step 1) — either way there's nothing at risk.
|
|
917
|
+
*
|
|
918
|
+
* Salvage is narrower than reclaim on purpose: only `merged` (always
|
|
919
|
+
* forge-confirmed — see `reconcileIntegration`) ever licenses destroying
|
|
920
|
+
* dirty, uncommitted work — `fresh` never does, clean or not, because the
|
|
921
|
+
* ambiguity in what `fresh` can prove is exactly what destroyed a live
|
|
922
|
+
* worktree via `gc --apply --salvage-dirty` on 2026-07-15: a branch with
|
|
923
|
+
* zero commits of its own read as "merged" and its uncommitted work was
|
|
924
|
+
* salvaged-then-removed out from under a running agent. A `merged ∧ dirty`
|
|
925
|
+
* worktree written to inside `RECENT_WRITE_HOLD_WINDOW_MS` also holds
|
|
926
|
+
* instead of salvaging — see that constant's doc. Everything else is
|
|
927
|
+
* `hold`.
|
|
928
|
+
*/
|
|
929
|
+
declare function classify(tree: TreeState, integration: IntegrationState, liveness: LivenessState, nowMs?: number): Classification;
|
|
930
|
+
interface WorktreeStatusEntry {
|
|
931
|
+
path: string;
|
|
932
|
+
branch: string | null;
|
|
933
|
+
head: string;
|
|
934
|
+
tree: TreeState;
|
|
935
|
+
integration: IntegrationState;
|
|
936
|
+
liveness: LivenessState;
|
|
937
|
+
provenance: ProvenanceInfo;
|
|
938
|
+
gate: GateState;
|
|
939
|
+
reclaimable: boolean;
|
|
940
|
+
class: WorktreeClass;
|
|
941
|
+
}
|
|
942
|
+
interface ComputeWorktreeStatusInput {
|
|
943
|
+
repoRoot: string;
|
|
944
|
+
repoName: string;
|
|
945
|
+
worktree: GitWorktreeRef;
|
|
946
|
+
forge: ForgeClient;
|
|
947
|
+
memo: VerdictMemoStore;
|
|
948
|
+
defaultBranchRef?: string;
|
|
949
|
+
sessionsPath?: string;
|
|
950
|
+
now?: () => string;
|
|
951
|
+
/**
|
|
952
|
+
* Clock for `classify`'s `RECENT_WRITE_HOLD_WINDOW_MS` guard — deliberately
|
|
953
|
+
* separate from `now`. `now` freezes `checkedAt` stamps for deterministic
|
|
954
|
+
* tests and is often a fixed past instant; file mtimes are always real
|
|
955
|
+
* wall-clock, so comparing them against a frozen-past `now` would make
|
|
956
|
+
* every dirty fixture look like it was written in the future (a negative,
|
|
957
|
+
* and therefore always-"recent", delta). Defaults to `Date.now()`.
|
|
958
|
+
*/
|
|
959
|
+
nowMs?: number;
|
|
960
|
+
}
|
|
961
|
+
/** Computes all three axes + provenance + classification for one worktree. */
|
|
962
|
+
declare function computeWorktreeStatus(input: ComputeWorktreeStatusInput): Promise<WorktreeStatusEntry>;
|
|
963
|
+
interface ListWorktreeStatusesInput {
|
|
964
|
+
repoRoot: string;
|
|
965
|
+
repoName: string;
|
|
966
|
+
forge: ForgeClient;
|
|
967
|
+
memo: VerdictMemoStore;
|
|
968
|
+
defaultBranchRef?: string;
|
|
969
|
+
sessionsPath?: string;
|
|
970
|
+
now?: () => string;
|
|
971
|
+
}
|
|
972
|
+
/** Enumerates every linked worktree of `repoRoot` (`git worktree list`) and computes its status. */
|
|
973
|
+
declare function listWorktreeStatuses(input: ListWorktreeStatusesInput): Promise<WorktreeStatusEntry[]>;
|
|
974
|
+
|
|
53
975
|
/** The subset of `HarnessClient` the host drives — narrowed for fake-client tests. */
|
|
54
976
|
type DaemonClient = Pick<HarnessClient, "start" | "prompt" | "output" | "kill" | "waitForAny" | "close">;
|
|
55
977
|
interface DaemonAgentSessionHost extends AgentSessionHost {
|
|
@@ -88,4 +1010,152 @@ declare function connectDaemonAgentSessionHost(opts?: ConnectHarnessOptions): Pr
|
|
|
88
1010
|
/** Build the host over an already-connected client (exported for tests with a fake client). */
|
|
89
1011
|
declare function makeDaemonAgentSessionHost(client: DaemonClient): DaemonAgentSessionHost;
|
|
90
1012
|
|
|
91
|
-
|
|
1013
|
+
/**
|
|
1014
|
+
* `gc` (PLAN.md §5, PR-D): plan → apply → salvage over the status engine's
|
|
1015
|
+
* classification (`status.ts`, PR-A). This module owns no new git knowledge
|
|
1016
|
+
* — it reuses `listGitWorktrees` / `computeWorktreeStatus` / `classify` for
|
|
1017
|
+
* every fact and `salvageWorktree` / `worktree.cleanup` (PR-C) for every
|
|
1018
|
+
* mutation. Its only original logic is sequencing the four safety layers
|
|
1019
|
+
* (PLAN.md §5.2):
|
|
1020
|
+
*
|
|
1021
|
+
* 1. `planGc` is pure read: it classifies every worktree and returns a
|
|
1022
|
+
* plan. Nothing is mutated by calling it — the CLI's dry-run default
|
|
1023
|
+
* *is* just "call planGc, print it, stop."
|
|
1024
|
+
* 2. `applyGc` re-derives each worktree's class from scratch immediately
|
|
1025
|
+
* before touching it (a fresh `listGitWorktrees` + `computeWorktreeStatus`,
|
|
1026
|
+
* not a reuse of the plan's stale snapshot) and refuses to act if the
|
|
1027
|
+
* class changed since the plan was made.
|
|
1028
|
+
* 3. The actual removal is `worktree.cleanup` with NO discard flags for a
|
|
1029
|
+
* reclaim-class entry — git's own non-`--force` refusal is the backstop
|
|
1030
|
+
* this module leans on, never a force flag "just in case."
|
|
1031
|
+
* 4. A salvage-class entry is only ever touched via `salvageWorktree`
|
|
1032
|
+
* followed by `worktree.cleanup` with both discard flags — snapshot
|
|
1033
|
+
* first, removal only after the snapshot is durable (mirrors `worktree
|
|
1034
|
+
* archive`'s CLI verb, PR-C).
|
|
1035
|
+
*
|
|
1036
|
+
* The main worktree (`repoRoot` itself) is never part of the plan: `git
|
|
1037
|
+
* worktree list`'s first entry is always the main checkout, and a repo
|
|
1038
|
+
* sitting on an up-to-date default branch is trivially `fresh ∧ clean` —
|
|
1039
|
+
* exactly what `reclaim` looks for. Git itself refuses to remove
|
|
1040
|
+
* the main working tree ("is a main working tree", verified empirically),
|
|
1041
|
+
* but this module doesn't lean on that as the only guard: the main worktree
|
|
1042
|
+
* is filtered out before classification even runs, so it can never appear
|
|
1043
|
+
* in a plan, let alone be attempted.
|
|
1044
|
+
*/
|
|
1045
|
+
|
|
1046
|
+
type GcClass = "reclaim" | "salvage" | "hold";
|
|
1047
|
+
interface ClassifyForGcOptions {
|
|
1048
|
+
/**
|
|
1049
|
+
* A clean, idle/daemon-unreachable DETACHED worktree moves from `hold` to
|
|
1050
|
+
* `reclaim` (PLAN.md §5.1). This is the only reclassification power any
|
|
1051
|
+
* flag has: every other hold reason — partial, open, unknown(offline),
|
|
1052
|
+
* gone-unexplained, diverged — is untouched by any flag, ever. Default
|
|
1053
|
+
* false.
|
|
1054
|
+
*/
|
|
1055
|
+
includeDetached?: boolean;
|
|
1056
|
+
/** Threaded straight through to `classify`'s `RECENT_WRITE_HOLD_WINDOW_MS` guard. Defaults to `Date.now()`. */
|
|
1057
|
+
nowMs?: number;
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* gc's own classifier, layered on top of `classify` (status.ts) rather than
|
|
1061
|
+
* reimplementing it — reuse first (AGENTS.md, PLAN.md §0.6). The one thing
|
|
1062
|
+
* `classify` cannot know is `--include-detached`, a gc-specific flag; that
|
|
1063
|
+
* override lives here, not in the read-only status engine `ls --status`
|
|
1064
|
+
* also depends on.
|
|
1065
|
+
*/
|
|
1066
|
+
declare function classifyForGc(tree: TreeState, integration: IntegrationState, liveness: LivenessState, options?: ClassifyForGcOptions): GcClass;
|
|
1067
|
+
interface GcPlanEntry {
|
|
1068
|
+
path: string;
|
|
1069
|
+
branch: string | null;
|
|
1070
|
+
head: string;
|
|
1071
|
+
tree: TreeState;
|
|
1072
|
+
integration: IntegrationState;
|
|
1073
|
+
liveness: LivenessState;
|
|
1074
|
+
/** Classification per PLAN.md §5.1, after `--include-detached` (if set). */
|
|
1075
|
+
class: GcClass;
|
|
1076
|
+
}
|
|
1077
|
+
interface PlanGcInput {
|
|
1078
|
+
repoRoot: string;
|
|
1079
|
+
repoName: string;
|
|
1080
|
+
forge: ForgeClient;
|
|
1081
|
+
memo: VerdictMemoStore;
|
|
1082
|
+
defaultBranchRef?: string;
|
|
1083
|
+
sessionsPath?: string;
|
|
1084
|
+
includeDetached?: boolean;
|
|
1085
|
+
now?: () => string;
|
|
1086
|
+
/** See `ComputeWorktreeStatusInput.nowMs` (status.ts) — kept separate from `now`. Defaults to `Date.now()`. */
|
|
1087
|
+
nowMs?: number;
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* The dry-run plan (PLAN.md §5.2 layer 1): classify every linked worktree,
|
|
1091
|
+
* mutate nothing. This is the human's decision surface — `gc` with no flags
|
|
1092
|
+
* is exactly "call this, print it, stop."
|
|
1093
|
+
*/
|
|
1094
|
+
declare function planGc(input: PlanGcInput): Promise<GcPlanEntry[]>;
|
|
1095
|
+
interface ApplyGcOptions {
|
|
1096
|
+
repoRoot: string;
|
|
1097
|
+
repoName: string;
|
|
1098
|
+
forge: ForgeClient;
|
|
1099
|
+
memo: VerdictMemoStore;
|
|
1100
|
+
defaultBranchRef?: string;
|
|
1101
|
+
sessionsPath?: string;
|
|
1102
|
+
includeDetached?: boolean;
|
|
1103
|
+
/** Archive (salvage-then-remove) every salvage-class entry. Default false — salvage entries are left untouched. */
|
|
1104
|
+
salvageDirty?: boolean;
|
|
1105
|
+
now?: () => string;
|
|
1106
|
+
/** See `ComputeWorktreeStatusInput.nowMs` (status.ts) — kept separate from `now`. Defaults to `Date.now()`. */
|
|
1107
|
+
nowMs?: number;
|
|
1108
|
+
/** Override for `~/.agentproto/worktree-salvage` — tests use a temp dir. */
|
|
1109
|
+
salvageRoot?: string;
|
|
1110
|
+
}
|
|
1111
|
+
type GcApplyOutcome = {
|
|
1112
|
+
path: string;
|
|
1113
|
+
branch: string | null;
|
|
1114
|
+
result: "reclaimed";
|
|
1115
|
+
} | {
|
|
1116
|
+
path: string;
|
|
1117
|
+
branch: string | null;
|
|
1118
|
+
result: "salvaged";
|
|
1119
|
+
salvageDir: string;
|
|
1120
|
+
} | {
|
|
1121
|
+
path: string;
|
|
1122
|
+
branch: string | null;
|
|
1123
|
+
result: "held";
|
|
1124
|
+
}
|
|
1125
|
+
/** Salvage-class, but `--salvage-dirty` was not passed: left untouched by design. */
|
|
1126
|
+
| {
|
|
1127
|
+
path: string;
|
|
1128
|
+
branch: string | null;
|
|
1129
|
+
result: "skipped-dirty";
|
|
1130
|
+
}
|
|
1131
|
+
/** Layer 2: re-classified at apply time to something other than the plan said — refuses rather than acting on a stale plan. */
|
|
1132
|
+
| {
|
|
1133
|
+
path: string;
|
|
1134
|
+
branch: string | null;
|
|
1135
|
+
result: "aborted-reclassified";
|
|
1136
|
+
from: GcClass;
|
|
1137
|
+
to: GcClass;
|
|
1138
|
+
}
|
|
1139
|
+
/** The worktree named in the plan no longer exists (already removed by something else since the plan was made). */
|
|
1140
|
+
| {
|
|
1141
|
+
path: string;
|
|
1142
|
+
branch: string | null;
|
|
1143
|
+
result: "aborted-vanished";
|
|
1144
|
+
}
|
|
1145
|
+
/** git itself (or the salvage snapshot) refused — the TOCTOU backstop actually firing, or a real I/O failure. */
|
|
1146
|
+
| {
|
|
1147
|
+
path: string;
|
|
1148
|
+
branch: string | null;
|
|
1149
|
+
result: "failed";
|
|
1150
|
+
message: string;
|
|
1151
|
+
};
|
|
1152
|
+
/**
|
|
1153
|
+
* Execute a plan (PLAN.md §5.2 layers 2–4). Every entry is re-classified
|
|
1154
|
+
* from scratch immediately before it's touched; a `hold` entry is never
|
|
1155
|
+
* touched, a `salvage` entry is only touched when `salvageDirty` is true,
|
|
1156
|
+
* and every removal goes through `worktree.cleanup` — never a hand-rolled
|
|
1157
|
+
* `git worktree remove`.
|
|
1158
|
+
*/
|
|
1159
|
+
declare function applyGc(plan: readonly GcPlanEntry[], options: ApplyGcOptions): Promise<GcApplyOutcome[]>;
|
|
1160
|
+
|
|
1161
|
+
export { type AgentprotoConfig, type ApplyGcOptions, CONFIG_FILENAME, type Classification, type ClassifyForGcOptions, type ComputeProvenanceOptions, type ComputeWorktreeStatusInput, ConfigError, DEFAULT_PROXY_PORT, type DaemonAgentSessionHost, type DaemonClient, ENV_VARS, type ExecOptions, type ExecResult, FileVerdictMemoStore, type ForgeClient, type ForgePullRequestRef, ForgeUnavailableError, type GateState, type GcApplyOutcome, type GcClass, type GcPlanEntry, GhCliForgeClient, type GitWorktreeRef, HookError, type HookRun, type HostnameParts, InMemoryVerdictMemoStore, type IntegrationState, type ListWorktreeStatusesInput, type LivenessState, type NamedScript, type PeerService, type PlanGcInput, type ProvenanceConfidence, type ProvenanceInfo, ProxyTable, type ReconcileIntegrationInput, type ResolveSupervisorInput, type ResolvedWorktreeRuntime, RestForgeClient, type RunningProxy, SALVAGE_ROOT, SESSIONS_FILE_PATH, type SalvageManifest, type SalvageResult, type SalvageWorktreeInput, type ScriptConfig, type ServiceRuntime, type ServiceStatus, ServiceSupervisor, type SessionRef, type SupervisorOptions, type TreeState, UnreachableForgeClient, VERDICT_MEMO_PATH, type VerdictMemoRecord, type VerdictMemoStore, type WorktreeAgentInput, type WorktreeClass, type WorktreeEnvContext, type WorktreeMarker, WorktreeNotRemovableError, type WorktreeStatusEntry, allocatePort, applyGc, classify, classifyForGc, computeLiveness, computeProvenance, computeTreeState, computeWorktreeStatus, connectDaemonAgentSessionHost, createForgeClient, createProxyServer, detectDefaultBranch, disposeSupervisor, ephemeralPort, execArgv, execGit, execShell, expandGlob, getScript, getSupervisor, globToRegExp, hookEnv, isPortFree, listGitWorktrees, listScripts, listServices, listWorktreeStatuses, loadConfigFromBase, makeDaemonAgentSessionHost, normalizeHook, parseConfig, parseGithubOwnerRepo, peerEnv, peerEnvKey, planGc, readSessionsRegistry, readWorktreeMarker, reconcileIntegration, repoLabel, resolveSupervisor, runSetup, runTeardown, salvageWorktree, serviceEnv, serviceEnvToken, serviceHostname, serviceUrl, sessionInWorktree, sharedProxyTable, slugify, startProxy, stripPort, worktreeAgentInputSchema, worktreeAgentWorkflow, writeWorktreeMarker };
|