@agentproto/worktree 0.4.2 → 0.5.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/README.md +25 -0
- package/dist/bin/worktree-agent.mjs +2 -2
- package/dist/{chunk-AJJPABO3.mjs → chunk-PSNNWGAF.mjs} +3 -3
- package/dist/{chunk-AJJPABO3.mjs.map → chunk-PSNNWGAF.mjs.map} +1 -1
- package/dist/{chunk-ZRTFECIP.mjs → chunk-S5ARFUTK.mjs} +59 -32
- package/dist/chunk-S5ARFUTK.mjs.map +1 -0
- package/dist/index.d.ts +123 -13
- package/dist/index.mjs +67 -20
- package/dist/index.mjs.map +1 -1
- package/dist/provider/index.mjs +1 -1
- package/package.json +7 -5
- package/routines/README.md +113 -0
- package/routines/worktree-gc/ROUTINE.md +96 -0
- package/routines/worktree-gc-notify/ROUTINE.md +70 -0
- package/routines/worktree-gc-notify/WORKFLOW.md +70 -0
- package/routines/worktree-gc-notify/entry.mjs +77 -0
- package/dist/chunk-ZRTFECIP.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -125,3 +125,28 @@ It connects to the daemon's MCP endpoint (`http://127.0.0.1:18790/mcp`, or
|
|
|
125
125
|
step reads a y/n answer from `/dev/tty`; `--yes` auto-approves, and a
|
|
126
126
|
non-interactive run (no TTY) defaults to NOT approving — the worktree is left
|
|
127
127
|
in place rather than silently cleaned up.
|
|
128
|
+
|
|
129
|
+
## `worktree-gc` routine (AIP-41, opt-in)
|
|
130
|
+
|
|
131
|
+
`routines/worktree-gc/ROUTINE.md` is a reference AIP-41 routine that reaps
|
|
132
|
+
merged worktrees on a schedule, so a long-running workspace doesn't accumulate
|
|
133
|
+
stale `_worktrees/*` trees and dead `wt/*` branches. It fires the `worktree_gc`
|
|
134
|
+
tool (the daemon's MCP/HTTP surface over `planGc` / `applyGc` in `src/gc.ts`)
|
|
135
|
+
with `apply: true`, `salvageDirty: false` on a daily cron (`0 4 * * *`, UTC).
|
|
136
|
+
|
|
137
|
+
**It ships disabled (`enabled: false`)** — it registers but never fires until
|
|
138
|
+
you turn it on. To activate it in a workspace:
|
|
139
|
+
|
|
140
|
+
1. Copy `routines/worktree-gc/` to the workspace's routine library at
|
|
141
|
+
`<workspace>/.routines/worktree-gc/ROUTINE.md` (the path
|
|
142
|
+
`@agentproto/routine`'s `routineSpec.pathOf` expects).
|
|
143
|
+
2. Set `enabled: true` in the frontmatter. To pin a specific repo, add
|
|
144
|
+
`repoRoot: <abs path>` or `workspaceSlug: <slug>` to `target.inputs`;
|
|
145
|
+
otherwise the daemon resolves the repo from the **active workspace**.
|
|
146
|
+
3. Reload routines so the daemon registers the schedule.
|
|
147
|
+
|
|
148
|
+
The safety invariants are enforced by the engine and cannot be weakened by the
|
|
149
|
+
routine: reclaim is **merge-gated** (integration ∈ {merged, fresh} **and** the
|
|
150
|
+
tree is clean), an **open** PR or live-session worktree is always **held**, and
|
|
151
|
+
a **dirty** integrated worktree is only ever archived — never discarded — and
|
|
152
|
+
only when `salvageDirty` is `true`.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { connectDaemonAgentSessionHost, worktreeAgentWorkflow } from '../chunk-
|
|
2
|
+
import { connectDaemonAgentSessionHost, worktreeAgentWorkflow } from '../chunk-PSNNWGAF.mjs';
|
|
3
3
|
import '../chunk-E3EM24BS.mjs';
|
|
4
|
-
import '../chunk-
|
|
4
|
+
import '../chunk-S5ARFUTK.mjs';
|
|
5
5
|
import '../chunk-CM3XGWTY.mjs';
|
|
6
6
|
import { runWorkflow } from '@agentproto/workflow-runtime';
|
|
7
7
|
import { resolve } from 'path';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { worktreeProvider } from './chunk-
|
|
1
|
+
import { worktreeProvider } from './chunk-S5ARFUTK.mjs';
|
|
2
2
|
import { provisionWorktreeTool, runGateTool, cleanupWorktreeTool } from './chunk-CM3XGWTY.mjs';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { resolveMcpUrl, HarnessClient } from '@agentproto/harness';
|
|
@@ -180,5 +180,5 @@ async function waitForSettled(client, sessionId) {
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
export { connectDaemonAgentSessionHost, makeDaemonAgentSessionHost, worktreeAgentInputSchema, worktreeAgentWorkflow };
|
|
183
|
-
//# sourceMappingURL=chunk-
|
|
184
|
-
//# sourceMappingURL=chunk-
|
|
183
|
+
//# sourceMappingURL=chunk-PSNNWGAF.mjs.map
|
|
184
|
+
//# sourceMappingURL=chunk-PSNNWGAF.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/workflow.ts","../src/agent-session-host.ts"],"names":[],"mappings":"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","file":"chunk-AJJPABO3.mjs","sourcesContent":["import { z } from \"zod\"\nimport type { Bindings, RuntimeWorkflow } from \"@agentproto/workflow-runtime\"\nimport { provisionWorktreeTool, cleanupWorktreeTool, runGateTool } from \"./tools/index.js\"\nimport { worktreeProvider } from \"./provider/index.js\"\n\n/**\n * Input to the worktree-agent workflow: launch a coding agent inside a\n * fresh git worktree, gate its work, and — on approval — clean up.\n */\nexport const worktreeAgentInputSchema = z.object({\n repoRoot: z.string().describe(\"Absolute path to the git repository root.\"),\n slug: z.string().describe(\"Worktree/branch identifier, e.g. 'fix-flaky-test'.\"),\n base: z.string().optional().describe(\"Ref to cut the worktree branch from. Default 'origin/main'.\"),\n depsCmd: z.string().optional().describe(\"Command to install deps inside the worktree, e.g. 'pnpm install --prefer-offline'.\"),\n copyGlobs: z.array(z.string()).optional().describe(\"Gitignored files to copy into the worktree, e.g. secrets.\"),\n linkPaths: z.array(z.string()).optional().describe(\"Gitignored dirs/files symlinked from the host repo into the worktree before depsCmd (node_modules, sibling workspace repos) so the graph resolves without a full reinstall.\"),\n task: z.string().describe(\"The prompt sent to the coding agent.\"),\n adapter: z.string().optional().describe(\"Agent adapter slug. Default 'claude-code'.\"),\n gateCmd: z.string().describe(\"Command run inside the worktree to check the agent's work, e.g. 'pnpm test'.\"),\n deleteBranch: z.boolean().optional().describe(\"Delete the worktree's branch on cleanup. Default true.\"),\n})\n\nexport type WorktreeAgentInput = z.infer<typeof worktreeAgentInputSchema>\n\nconst candidates = [worktreeProvider]\n\nfunction input(b: { input: unknown }): WorktreeAgentInput {\n return b.input as WorktreeAgentInput\n}\n\n/**\n * provision (tool) → code (AgentStep, cwd bound to provision's output) →\n * gate (tool) → on pass: approval → cleanup (tool); on fail: leave the\n * worktree in place for inspection.\n */\nexport const worktreeAgentWorkflow: RuntimeWorkflow = {\n id: \"worktree-agent\",\n description:\n \"Launch a coding agent inside a fresh git worktree, gate its work with a \" +\n \"check command, get a human ack, then clean up. On gate failure the \" +\n \"worktree is left in place for inspection instead of being cleaned up.\",\n steps: [\n {\n kind: \"tool\",\n id: \"provision\",\n tool: provisionWorktreeTool,\n candidates,\n input: (b: Bindings) => {\n const i = input(b)\n return {\n repoRoot: i.repoRoot,\n slug: i.slug,\n base: i.base,\n depsCmd: i.depsCmd,\n copyGlobs: i.copyGlobs,\n linkPaths: i.linkPaths,\n }\n },\n },\n {\n kind: \"agent\",\n id: \"code\",\n adapter: (b: Bindings) => input(b).adapter ?? \"claude-code\",\n cwd: (b: Bindings) => (b.steps.provision as { cwd: string }).cwd,\n prompt: (b: Bindings) => input(b).task,\n },\n {\n kind: \"tool\",\n id: \"gate\",\n tool: runGateTool,\n candidates,\n input: (b: Bindings) => ({\n cwd: (b.steps.provision as { cwd: string }).cwd,\n cmd: input(b).gateCmd,\n }),\n },\n {\n kind: \"branch\",\n id: \"route\",\n cond: (b: Bindings) => (b.steps.gate as { passed: boolean }).passed,\n then: [\n {\n kind: \"approval\",\n id: \"approval\",\n prompt: () => \"Gate passed. Approve worktree cleanup?\",\n onApprove: [\n {\n kind: \"tool\",\n id: \"cleanup\",\n tool: cleanupWorktreeTool,\n candidates,\n // discardUntracked/discardModified: worktree.cleanup refuses a\n // dirty tree by default (PLAN.md §5.2 layer 3). This workflow's\n // own approval step, just above, is that guard already — a\n // human explicitly approved tearing this worktree down, so\n // both flags are granted here rather than surfacing a second,\n // redundant refusal.\n input: (b: Bindings) => ({\n repoRoot: input(b).repoRoot,\n cwd: (b.steps.provision as { cwd: string }).cwd,\n branch: (b.steps.provision as { branch: string }).branch,\n deleteBranch: input(b).deleteBranch ?? true,\n discardUntracked: true,\n discardModified: true,\n }),\n },\n ],\n onReject: [\n {\n kind: \"transform\",\n id: \"held\",\n compute: () => ({ removed: false, reason: \"cleanup_rejected\" as const }),\n },\n ],\n },\n ],\n otherwise: [\n {\n kind: \"transform\",\n id: \"gate-failed\",\n compute: () => ({ removed: false, reason: \"gate_failed\" as const }),\n },\n ],\n },\n ],\n output: (b: Bindings) => ({\n cwd: (b.steps.provision as { cwd: string }).cwd,\n branch: (b.steps.provision as { branch: string }).branch,\n gate: b.steps.gate,\n cleanup: b.steps.cleanup ?? b.steps.held ?? b.steps[\"gate-failed\"],\n }),\n}\n","import {\n HarnessClient,\n resolveMcpUrl,\n type ConnectHarnessOptions,\n type SessionDescriptor,\n type StartAgentArgs,\n type TurnEvent,\n type TurnResult,\n} from \"@agentproto/harness\"\nimport type { AgentSessionHost } from \"@agentproto/workflow-runtime\"\n\n/** `session_monitor`'s max accepted long-poll window (`orchestration-tools.ts`). */\nconst MAX_POLL_MS = 49_000\n\n/** The subset of `HarnessClient` the host drives — narrowed for fake-client tests. */\nexport type DaemonClient = Pick<\n HarnessClient,\n \"start\" | \"prompt\" | \"output\" | \"kill\" | \"waitForAny\" | \"close\"\n>\n\nexport interface DaemonAgentSessionHost extends AgentSessionHost {\n close(): Promise<void>\n /** Full `agent_start`, returning the raw descriptor and forwarding every\n * `StartAgentArgs` field (`mcpServers`, `model`, `effort`, `label`, …) —\n * unlike `spawn()` above (the narrow AgentStep-shaped convenience\n * wrapper), which only takes `cwd`/`workspaceSlug`/`stepId`. Consumers\n * that need finer per-turn control than the AgentStep contract provides\n * (e.g. `@agentproto/sandbox`'s sandbox agent-session proxy) use this\n * plus `prompt`/`output`/`kill`/`waitForAny` directly instead of\n * `spawn`/`sendPromptAndWait`. */\n start(args: StartAgentArgs): Promise<SessionDescriptor>\n /** Send a follow-up turn — `opts.interrupt` cancels an in-flight turn and\n * redirects the session onto this prompt (mirrors `agent_prompt`). */\n prompt(sessionId: string, prompt: string, opts?: { interrupt?: boolean }): Promise<void>\n /** Tail the ring buffer (`agent_output`). */\n output(sessionId: string, lastN?: number): Promise<string>\n /** SIGTERM the session (`agent_kill`). */\n kill(sessionId: string): Promise<void>\n /** Multiplexed long-poll (`session_monitor`) — see `HarnessClient.waitForAny`. */\n waitForAny(sessionIds: string[], opts?: { timeoutMs?: number; event?: TurnEvent }): Promise<TurnResult>\n}\n\n/**\n * Connect to the agentproto daemon's MCP endpoint and return a real,\n * supervisable `AgentSessionHost` — every `AgentStep` becomes an `agent_start`\n * daemon session rather than a bare subprocess. Fails loudly (no silent\n * fallback) if the daemon isn't reachable.\n */\nexport async function connectDaemonAgentSessionHost(\n opts?: ConnectHarnessOptions,\n): Promise<DaemonAgentSessionHost> {\n const url = resolveMcpUrl(opts)\n let client: HarnessClient\n try {\n client = await HarnessClient.connect({ url })\n } catch (err) {\n throw new Error(\n `worktree-agent: could not reach the agentproto daemon's MCP endpoint at ${url}. ` +\n \"Start the daemon (`agentproto daemon start`), or point at a running one via \" +\n `AGENTPROTO_MCP_URL, before running this command.\\n` +\n `(${err instanceof Error ? err.message : String(err)})`,\n )\n }\n return makeDaemonAgentSessionHost(client)\n}\n\n/** Build the host over an already-connected client (exported for tests with a fake client). */\nexport function makeDaemonAgentSessionHost(client: DaemonClient): DaemonAgentSessionHost {\n const sessionByStepId = new Map<string, string>()\n\n return {\n async spawn(adapter, opts): Promise<string> {\n const desc = await client.start({\n adapter,\n ...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),\n ...(opts.workspaceSlug !== undefined ? { workspaceSlug: opts.workspaceSlug } : {}),\n ...(opts.stepId !== undefined ? { label: opts.stepId } : {}),\n })\n if (opts.stepId) sessionByStepId.set(opts.stepId, desc.id)\n return desc.id\n },\n\n async sendPromptAndWait(sessionId, prompt): Promise<void> {\n // Subscribe before sending: an extremely fast turn could otherwise settle\n // before the wait call subscribes (same race `harness`'s `ask()` avoids).\n const waitPromise = waitForSettled(client, sessionId)\n await client.prompt(sessionId, prompt)\n await waitPromise\n },\n\n resolveByLabel(stepId): string | undefined {\n return sessionByStepId.get(stepId)\n },\n\n async start(args): Promise<SessionDescriptor> {\n return client.start(args)\n },\n\n async prompt(sessionId, prompt, opts): Promise<void> {\n await client.prompt(sessionId, prompt, opts)\n },\n\n async output(sessionId, lastN): Promise<string> {\n return client.output(sessionId, lastN)\n },\n\n async kill(sessionId): Promise<void> {\n await client.kill(sessionId)\n },\n\n async waitForAny(sessionIds, opts): Promise<TurnResult> {\n return client.waitForAny(sessionIds, opts)\n },\n\n async close(): Promise<void> {\n await client.close()\n },\n }\n}\n\n/**\n * `session_monitor` long-polls cap at 49s; loop past timeouts for longer\n * turns. A clean timeout comes back as `{ timedOut: true, sessionIds }` —\n * there is no `event: \"timeout\"` on the wire, `event` is only ever set on a\n * real match. Keep polling while `timedOut` is true; stop once it's\n * falsy/absent (a real turn-end/awaiting-input/exited match).\n */\nasync function waitForSettled(client: DaemonClient, sessionId: string): Promise<void> {\n for (;;) {\n const result = await client.waitForAny([sessionId], { event: \"any\", timeoutMs: MAX_POLL_MS })\n if (!result.timedOut) return\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/workflow.ts","../src/agent-session-host.ts"],"names":[],"mappings":"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","file":"chunk-PSNNWGAF.mjs","sourcesContent":["import { z } from \"zod\"\nimport type { Bindings, RuntimeWorkflow } from \"@agentproto/workflow-runtime\"\nimport { provisionWorktreeTool, cleanupWorktreeTool, runGateTool } from \"./tools/index.js\"\nimport { worktreeProvider } from \"./provider/index.js\"\n\n/**\n * Input to the worktree-agent workflow: launch a coding agent inside a\n * fresh git worktree, gate its work, and — on approval — clean up.\n */\nexport const worktreeAgentInputSchema = z.object({\n repoRoot: z.string().describe(\"Absolute path to the git repository root.\"),\n slug: z.string().describe(\"Worktree/branch identifier, e.g. 'fix-flaky-test'.\"),\n base: z.string().optional().describe(\"Ref to cut the worktree branch from. Default 'origin/main'.\"),\n depsCmd: z.string().optional().describe(\"Command to install deps inside the worktree, e.g. 'pnpm install --prefer-offline'.\"),\n copyGlobs: z.array(z.string()).optional().describe(\"Gitignored files to copy into the worktree, e.g. secrets.\"),\n linkPaths: z.array(z.string()).optional().describe(\"Gitignored dirs/files symlinked from the host repo into the worktree before depsCmd (node_modules, sibling workspace repos) so the graph resolves without a full reinstall.\"),\n task: z.string().describe(\"The prompt sent to the coding agent.\"),\n adapter: z.string().optional().describe(\"Agent adapter slug. Default 'claude-code'.\"),\n gateCmd: z.string().describe(\"Command run inside the worktree to check the agent's work, e.g. 'pnpm test'.\"),\n deleteBranch: z.boolean().optional().describe(\"Delete the worktree's branch on cleanup. Default true.\"),\n})\n\nexport type WorktreeAgentInput = z.infer<typeof worktreeAgentInputSchema>\n\nconst candidates = [worktreeProvider]\n\nfunction input(b: { input: unknown }): WorktreeAgentInput {\n return b.input as WorktreeAgentInput\n}\n\n/**\n * provision (tool) → code (AgentStep, cwd bound to provision's output) →\n * gate (tool) → on pass: approval → cleanup (tool); on fail: leave the\n * worktree in place for inspection.\n */\nexport const worktreeAgentWorkflow: RuntimeWorkflow = {\n id: \"worktree-agent\",\n description:\n \"Launch a coding agent inside a fresh git worktree, gate its work with a \" +\n \"check command, get a human ack, then clean up. On gate failure the \" +\n \"worktree is left in place for inspection instead of being cleaned up.\",\n steps: [\n {\n kind: \"tool\",\n id: \"provision\",\n tool: provisionWorktreeTool,\n candidates,\n input: (b: Bindings) => {\n const i = input(b)\n return {\n repoRoot: i.repoRoot,\n slug: i.slug,\n base: i.base,\n depsCmd: i.depsCmd,\n copyGlobs: i.copyGlobs,\n linkPaths: i.linkPaths,\n }\n },\n },\n {\n kind: \"agent\",\n id: \"code\",\n adapter: (b: Bindings) => input(b).adapter ?? \"claude-code\",\n cwd: (b: Bindings) => (b.steps.provision as { cwd: string }).cwd,\n prompt: (b: Bindings) => input(b).task,\n },\n {\n kind: \"tool\",\n id: \"gate\",\n tool: runGateTool,\n candidates,\n input: (b: Bindings) => ({\n cwd: (b.steps.provision as { cwd: string }).cwd,\n cmd: input(b).gateCmd,\n }),\n },\n {\n kind: \"branch\",\n id: \"route\",\n cond: (b: Bindings) => (b.steps.gate as { passed: boolean }).passed,\n then: [\n {\n kind: \"approval\",\n id: \"approval\",\n prompt: () => \"Gate passed. Approve worktree cleanup?\",\n onApprove: [\n {\n kind: \"tool\",\n id: \"cleanup\",\n tool: cleanupWorktreeTool,\n candidates,\n // discardUntracked/discardModified: worktree.cleanup refuses a\n // dirty tree by default (PLAN.md §5.2 layer 3). This workflow's\n // own approval step, just above, is that guard already — a\n // human explicitly approved tearing this worktree down, so\n // both flags are granted here rather than surfacing a second,\n // redundant refusal.\n input: (b: Bindings) => ({\n repoRoot: input(b).repoRoot,\n cwd: (b.steps.provision as { cwd: string }).cwd,\n branch: (b.steps.provision as { branch: string }).branch,\n deleteBranch: input(b).deleteBranch ?? true,\n discardUntracked: true,\n discardModified: true,\n }),\n },\n ],\n onReject: [\n {\n kind: \"transform\",\n id: \"held\",\n compute: () => ({ removed: false, reason: \"cleanup_rejected\" as const }),\n },\n ],\n },\n ],\n otherwise: [\n {\n kind: \"transform\",\n id: \"gate-failed\",\n compute: () => ({ removed: false, reason: \"gate_failed\" as const }),\n },\n ],\n },\n ],\n output: (b: Bindings) => ({\n cwd: (b.steps.provision as { cwd: string }).cwd,\n branch: (b.steps.provision as { branch: string }).branch,\n gate: b.steps.gate,\n cleanup: b.steps.cleanup ?? b.steps.held ?? b.steps[\"gate-failed\"],\n }),\n}\n","import {\n HarnessClient,\n resolveMcpUrl,\n type ConnectHarnessOptions,\n type SessionDescriptor,\n type StartAgentArgs,\n type TurnEvent,\n type TurnResult,\n} from \"@agentproto/harness\"\nimport type { AgentSessionHost } from \"@agentproto/workflow-runtime\"\n\n/** `session_monitor`'s max accepted long-poll window (`orchestration-tools.ts`). */\nconst MAX_POLL_MS = 49_000\n\n/** The subset of `HarnessClient` the host drives — narrowed for fake-client tests. */\nexport type DaemonClient = Pick<\n HarnessClient,\n \"start\" | \"prompt\" | \"output\" | \"kill\" | \"waitForAny\" | \"close\"\n>\n\nexport interface DaemonAgentSessionHost extends AgentSessionHost {\n close(): Promise<void>\n /** Full `agent_start`, returning the raw descriptor and forwarding every\n * `StartAgentArgs` field (`mcpServers`, `model`, `effort`, `label`, …) —\n * unlike `spawn()` above (the narrow AgentStep-shaped convenience\n * wrapper), which only takes `cwd`/`workspaceSlug`/`stepId`. Consumers\n * that need finer per-turn control than the AgentStep contract provides\n * (e.g. `@agentproto/sandbox`'s sandbox agent-session proxy) use this\n * plus `prompt`/`output`/`kill`/`waitForAny` directly instead of\n * `spawn`/`sendPromptAndWait`. */\n start(args: StartAgentArgs): Promise<SessionDescriptor>\n /** Send a follow-up turn — `opts.interrupt` cancels an in-flight turn and\n * redirects the session onto this prompt (mirrors `agent_prompt`). */\n prompt(sessionId: string, prompt: string, opts?: { interrupt?: boolean }): Promise<void>\n /** Tail the ring buffer (`agent_output`). */\n output(sessionId: string, lastN?: number): Promise<string>\n /** SIGTERM the session (`agent_kill`). */\n kill(sessionId: string): Promise<void>\n /** Multiplexed long-poll (`session_monitor`) — see `HarnessClient.waitForAny`. */\n waitForAny(sessionIds: string[], opts?: { timeoutMs?: number; event?: TurnEvent }): Promise<TurnResult>\n}\n\n/**\n * Connect to the agentproto daemon's MCP endpoint and return a real,\n * supervisable `AgentSessionHost` — every `AgentStep` becomes an `agent_start`\n * daemon session rather than a bare subprocess. Fails loudly (no silent\n * fallback) if the daemon isn't reachable.\n */\nexport async function connectDaemonAgentSessionHost(\n opts?: ConnectHarnessOptions,\n): Promise<DaemonAgentSessionHost> {\n const url = resolveMcpUrl(opts)\n let client: HarnessClient\n try {\n client = await HarnessClient.connect({ url })\n } catch (err) {\n throw new Error(\n `worktree-agent: could not reach the agentproto daemon's MCP endpoint at ${url}. ` +\n \"Start the daemon (`agentproto daemon start`), or point at a running one via \" +\n `AGENTPROTO_MCP_URL, before running this command.\\n` +\n `(${err instanceof Error ? err.message : String(err)})`,\n )\n }\n return makeDaemonAgentSessionHost(client)\n}\n\n/** Build the host over an already-connected client (exported for tests with a fake client). */\nexport function makeDaemonAgentSessionHost(client: DaemonClient): DaemonAgentSessionHost {\n const sessionByStepId = new Map<string, string>()\n\n return {\n async spawn(adapter, opts): Promise<string> {\n const desc = await client.start({\n adapter,\n ...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),\n ...(opts.workspaceSlug !== undefined ? { workspaceSlug: opts.workspaceSlug } : {}),\n ...(opts.stepId !== undefined ? { label: opts.stepId } : {}),\n })\n if (opts.stepId) sessionByStepId.set(opts.stepId, desc.id)\n return desc.id\n },\n\n async sendPromptAndWait(sessionId, prompt): Promise<void> {\n // Subscribe before sending: an extremely fast turn could otherwise settle\n // before the wait call subscribes (same race `harness`'s `ask()` avoids).\n const waitPromise = waitForSettled(client, sessionId)\n await client.prompt(sessionId, prompt)\n await waitPromise\n },\n\n resolveByLabel(stepId): string | undefined {\n return sessionByStepId.get(stepId)\n },\n\n async start(args): Promise<SessionDescriptor> {\n return client.start(args)\n },\n\n async prompt(sessionId, prompt, opts): Promise<void> {\n await client.prompt(sessionId, prompt, opts)\n },\n\n async output(sessionId, lastN): Promise<string> {\n return client.output(sessionId, lastN)\n },\n\n async kill(sessionId): Promise<void> {\n await client.kill(sessionId)\n },\n\n async waitForAny(sessionIds, opts): Promise<TurnResult> {\n return client.waitForAny(sessionIds, opts)\n },\n\n async close(): Promise<void> {\n await client.close()\n },\n }\n}\n\n/**\n * `session_monitor` long-polls cap at 49s; loop past timeouts for longer\n * turns. A clean timeout comes back as `{ timedOut: true, sessionIds }` —\n * there is no `event: \"timeout\"` on the wire, `event` is only ever set on a\n * real match. Keep polling while `timedOut` is true; stop once it's\n * falsy/absent (a real turn-end/awaiting-input/exited match).\n */\nasync function waitForSettled(client: DaemonClient, sessionId: string): Promise<void> {\n for (;;) {\n const result = await client.waitForAny([sessionId], { event: \"any\", timeoutMs: MAX_POLL_MS })\n if (!result.timedOut) return\n }\n}\n"]}
|
|
@@ -169,8 +169,6 @@ async function loadConfigFromBase(repoRoot, base = "origin/main") {
|
|
|
169
169
|
}
|
|
170
170
|
return parseConfig(result.stdout);
|
|
171
171
|
}
|
|
172
|
-
|
|
173
|
-
// src/env.ts
|
|
174
172
|
var ENV_VARS = {
|
|
175
173
|
/** Absolute path to the original repo checkout the worktree was cut from. */
|
|
176
174
|
sourceCheckoutPath: "AGENTPROTO_SOURCE_CHECKOUT_PATH",
|
|
@@ -212,6 +210,14 @@ function serviceEnv(args) {
|
|
|
212
210
|
...peerEnv(args.peers)
|
|
213
211
|
};
|
|
214
212
|
}
|
|
213
|
+
var WORKTREES_TURBO_CACHE_DIR_ENV = "AGENTPROTO_WORKTREES_TURBO_CACHE_DIR";
|
|
214
|
+
function resolveWorktreesTurboCacheDir() {
|
|
215
|
+
const pinned = process.env[WORKTREES_TURBO_CACHE_DIR_ENV];
|
|
216
|
+
if (pinned) return resolve(pinned);
|
|
217
|
+
const ambient = process.env.TURBO_CACHE_DIR;
|
|
218
|
+
if (ambient) return ambient;
|
|
219
|
+
return join(homedir(), ".agentproto", "turbo-cache");
|
|
220
|
+
}
|
|
215
221
|
|
|
216
222
|
// src/lifecycle.ts
|
|
217
223
|
var HookError = class extends Error {
|
|
@@ -230,7 +236,7 @@ var HookError = class extends Error {
|
|
|
230
236
|
async function runSetup(config, ctx) {
|
|
231
237
|
const commands = normalizeHook(config.worktree?.setup);
|
|
232
238
|
const runs = [];
|
|
233
|
-
const env = hookEnv(ctx);
|
|
239
|
+
const env = { ...hookEnv(ctx), TURBO_CACHE_DIR: resolveWorktreesTurboCacheDir() };
|
|
234
240
|
for (const command of commands) {
|
|
235
241
|
const result = await execShell(command, ctx.worktreePath, { env });
|
|
236
242
|
const run = { command, result };
|
|
@@ -257,7 +263,16 @@ var sessionRefSchema = z.object({
|
|
|
257
263
|
startedAt: z.string(),
|
|
258
264
|
endedAt: z.string().optional(),
|
|
259
265
|
status: z.string(),
|
|
260
|
-
cwd: z.string().optional()
|
|
266
|
+
cwd: z.string().optional(),
|
|
267
|
+
// Optional echoes from SessionDescriptor; kept optional for back-compat with
|
|
268
|
+
// registry rows written before these fields were persisted. The GC logic
|
|
269
|
+
// ignores them; they are surfaced in local provenance footers.
|
|
270
|
+
adapterSlug: z.string().optional(),
|
|
271
|
+
model: z.string().optional(),
|
|
272
|
+
authMode: z.string().optional(),
|
|
273
|
+
costUsd: z.number().optional(),
|
|
274
|
+
tokensIn: z.number().optional(),
|
|
275
|
+
tokensOut: z.number().optional()
|
|
261
276
|
});
|
|
262
277
|
var sessionsFileSchema = z.object({ sessions: z.array(z.unknown()) });
|
|
263
278
|
async function readSessionsRegistry(path) {
|
|
@@ -300,7 +315,13 @@ async function readSessionsFile(path) {
|
|
|
300
315
|
if (!file.success) return null;
|
|
301
316
|
const refs = [];
|
|
302
317
|
for (const entry of file.data.sessions) {
|
|
303
|
-
const
|
|
318
|
+
const rawEntry = entry;
|
|
319
|
+
const ref = sessionRefSchema.safeParse({
|
|
320
|
+
...rawEntry,
|
|
321
|
+
// SessionDescriptor stores billing-auth mode under `auth.mode`; mirror it
|
|
322
|
+
// onto the flattened `authMode` field for footer consumers.
|
|
323
|
+
authMode: rawEntry?.authMode ?? rawEntry?.auth?.mode
|
|
324
|
+
});
|
|
304
325
|
if (ref.success) refs.push(ref.data);
|
|
305
326
|
}
|
|
306
327
|
return refs;
|
|
@@ -314,11 +335,11 @@ var worktreeMarkerSchema = z.object({
|
|
|
314
335
|
createdAt: z.string(),
|
|
315
336
|
createdBySessionId: z.string().optional()
|
|
316
337
|
});
|
|
317
|
-
async function readWorktreeMarker(worktreePath) {
|
|
338
|
+
async function readWorktreeMarker(repoRoot, worktreePath) {
|
|
318
339
|
const gitDirRes = await execArgv(
|
|
319
340
|
"git",
|
|
320
341
|
["-C", worktreePath, "rev-parse", "--path-format=absolute", "--git-dir"],
|
|
321
|
-
|
|
342
|
+
repoRoot
|
|
322
343
|
);
|
|
323
344
|
if (gitDirRes.exitCode !== 0) return null;
|
|
324
345
|
const gitDir = gitDirRes.stdout.trim();
|
|
@@ -338,11 +359,11 @@ async function readWorktreeMarker(worktreePath) {
|
|
|
338
359
|
const result = worktreeMarkerSchema.safeParse(parsed);
|
|
339
360
|
return result.success ? result.data : null;
|
|
340
361
|
}
|
|
341
|
-
async function writeWorktreeMarker(worktreePath, marker) {
|
|
362
|
+
async function writeWorktreeMarker(repoRoot, worktreePath, marker) {
|
|
342
363
|
const gitDirRes = await execArgv(
|
|
343
364
|
"git",
|
|
344
365
|
["-C", worktreePath, "rev-parse", "--path-format=absolute", "--git-dir"],
|
|
345
|
-
|
|
366
|
+
repoRoot
|
|
346
367
|
);
|
|
347
368
|
if (gitDirRes.exitCode !== 0) {
|
|
348
369
|
throw new Error(
|
|
@@ -352,13 +373,13 @@ async function writeWorktreeMarker(worktreePath, marker) {
|
|
|
352
373
|
const gitDir = gitDirRes.stdout.trim();
|
|
353
374
|
await writeFile(resolve(gitDir, "agentproto-worktree.json"), JSON.stringify(marker, null, 2) + "\n", "utf8");
|
|
354
375
|
}
|
|
355
|
-
async function computeProvenance(worktreePath, options = {}) {
|
|
376
|
+
async function computeProvenance(repoRoot, worktreePath, options = {}) {
|
|
356
377
|
const [registry, marker] = await Promise.all([
|
|
357
378
|
// No `sessionsPath` ⇒ the bucket ∪ legacy union, NOT the legacy file
|
|
358
379
|
// alone — pinning it here would hide every partitioned session from
|
|
359
380
|
// the GC join. See this file's header.
|
|
360
381
|
readSessionsRegistry(options.sessionsPath),
|
|
361
|
-
readWorktreeMarker(worktreePath)
|
|
382
|
+
readWorktreeMarker(repoRoot, worktreePath)
|
|
362
383
|
]);
|
|
363
384
|
const sessions = registry ?? [];
|
|
364
385
|
const matched = sessions.filter((s) => sessionInWorktree(s, worktreePath)).filter((s) => marker ? s.startedAt >= marker.createdAt : true).sort((a, b) => a.startedAt.localeCompare(b.startedAt));
|
|
@@ -376,7 +397,7 @@ var provisionWorktreeBuiltin = implementTool(
|
|
|
376
397
|
const branch = input.branch ?? `wt/${input.slug}`;
|
|
377
398
|
const cwd = input.dir ? resolve(input.dir) : resolve(input.repoRoot, "..", "_worktrees", input.slug);
|
|
378
399
|
await execGit(input.repoRoot, ["worktree", "add", "-b", branch, cwd, base]);
|
|
379
|
-
await writeWorktreeMarker(cwd, {
|
|
400
|
+
await writeWorktreeMarker(input.repoRoot, cwd, {
|
|
380
401
|
worktreeId: `wt_${randomUUID().slice(0, 8)}`,
|
|
381
402
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
382
403
|
});
|
|
@@ -426,24 +447,24 @@ var provisionWorktreeBuiltin = implementTool(
|
|
|
426
447
|
}
|
|
427
448
|
);
|
|
428
449
|
function isPortFree(port) {
|
|
429
|
-
return new Promise((
|
|
450
|
+
return new Promise((resolve5) => {
|
|
430
451
|
const server = createServer();
|
|
431
|
-
server.once("error", () =>
|
|
452
|
+
server.once("error", () => resolve5(false));
|
|
432
453
|
server.once("listening", () => {
|
|
433
|
-
server.close(() =>
|
|
454
|
+
server.close(() => resolve5(true));
|
|
434
455
|
});
|
|
435
456
|
server.listen(port, "127.0.0.1");
|
|
436
457
|
});
|
|
437
458
|
}
|
|
438
459
|
function ephemeralPort() {
|
|
439
|
-
return new Promise((
|
|
460
|
+
return new Promise((resolve5, reject) => {
|
|
440
461
|
const server = createServer();
|
|
441
462
|
server.once("error", reject);
|
|
442
463
|
server.listen(0, "127.0.0.1", () => {
|
|
443
464
|
const address = server.address();
|
|
444
465
|
if (address && typeof address === "object") {
|
|
445
466
|
const port = address.port;
|
|
446
|
-
server.close(() =>
|
|
467
|
+
server.close(() => resolve5(port));
|
|
447
468
|
} else {
|
|
448
469
|
server.close(() => reject(new Error("could not determine an ephemeral port")));
|
|
449
470
|
}
|
|
@@ -578,10 +599,10 @@ var ServiceSupervisor = class _ServiceSupervisor {
|
|
|
578
599
|
this.opts.proxyTable?.delete(t.runtime.hostname);
|
|
579
600
|
const child = t.child;
|
|
580
601
|
if (!child || t.status !== "running") return false;
|
|
581
|
-
return new Promise((
|
|
602
|
+
return new Promise((resolve5) => {
|
|
582
603
|
const done = () => {
|
|
583
604
|
t.status = "exited";
|
|
584
|
-
|
|
605
|
+
resolve5(true);
|
|
585
606
|
};
|
|
586
607
|
child.once("exit", done);
|
|
587
608
|
child.kill("SIGTERM");
|
|
@@ -704,8 +725,8 @@ async function disposeSupervisor(worktreePath) {
|
|
|
704
725
|
await supervisor.stopAll();
|
|
705
726
|
supervisors.delete(worktreePath);
|
|
706
727
|
}
|
|
707
|
-
async function computeTreeState(worktreePath) {
|
|
708
|
-
const res = await execArgv("git", ["-C", worktreePath, "status", "--porcelain=v2"],
|
|
728
|
+
async function computeTreeState(repoRoot, worktreePath) {
|
|
729
|
+
const res = await execArgv("git", ["-C", worktreePath, "status", "--porcelain=v2"], repoRoot);
|
|
709
730
|
if (res.exitCode !== 0) {
|
|
710
731
|
throw new Error(
|
|
711
732
|
`git status --porcelain=v2 failed in ${worktreePath} (exit ${res.exitCode}): ${res.stderr.trim() || res.stdout.trim()}`
|
|
@@ -727,13 +748,19 @@ async function computeTreeState(worktreePath) {
|
|
|
727
748
|
}
|
|
728
749
|
}
|
|
729
750
|
if (modified === 0 && staged === 0 && untracked === 0) return { state: "clean" };
|
|
730
|
-
return {
|
|
751
|
+
return {
|
|
752
|
+
state: "dirty",
|
|
753
|
+
modified,
|
|
754
|
+
staged,
|
|
755
|
+
untracked,
|
|
756
|
+
newestMtimeMs: await newestDirtyMtimeMs(repoRoot, worktreePath)
|
|
757
|
+
};
|
|
731
758
|
}
|
|
732
|
-
async function newestDirtyMtimeMs(worktreePath) {
|
|
759
|
+
async function newestDirtyMtimeMs(repoRoot, worktreePath) {
|
|
733
760
|
const [unstaged, staged, untracked] = await Promise.all([
|
|
734
|
-
execArgv("git", ["-C", worktreePath, "diff", "--name-only", "-z"],
|
|
735
|
-
execArgv("git", ["-C", worktreePath, "diff", "--cached", "--name-only", "-z"],
|
|
736
|
-
execArgv("git", ["-C", worktreePath, "ls-files", "--others", "--exclude-standard", "-z"],
|
|
761
|
+
execArgv("git", ["-C", worktreePath, "diff", "--name-only", "-z"], repoRoot),
|
|
762
|
+
execArgv("git", ["-C", worktreePath, "diff", "--cached", "--name-only", "-z"], repoRoot),
|
|
763
|
+
execArgv("git", ["-C", worktreePath, "ls-files", "--others", "--exclude-standard", "-z"], repoRoot)
|
|
737
764
|
]);
|
|
738
765
|
const paths = /* @__PURE__ */ new Set();
|
|
739
766
|
for (const res of [unstaged, staged, untracked]) {
|
|
@@ -996,7 +1023,7 @@ function classify(tree, integration, liveness, nowMs = Date.now()) {
|
|
|
996
1023
|
}
|
|
997
1024
|
async function computeWorktreeStatus(input) {
|
|
998
1025
|
const [tree, integration, liveness, provenance] = await Promise.all([
|
|
999
|
-
computeTreeState(input.worktree.path),
|
|
1026
|
+
computeTreeState(input.repoRoot, input.worktree.path),
|
|
1000
1027
|
reconcileIntegration({
|
|
1001
1028
|
repoRoot: input.repoRoot,
|
|
1002
1029
|
repoName: input.repoName,
|
|
@@ -1008,7 +1035,7 @@ async function computeWorktreeStatus(input) {
|
|
|
1008
1035
|
now: input.now
|
|
1009
1036
|
}),
|
|
1010
1037
|
computeLiveness(input.worktree.path, { sessionsPath: input.sessionsPath }),
|
|
1011
|
-
computeProvenance(input.worktree.path, { sessionsPath: input.sessionsPath })
|
|
1038
|
+
computeProvenance(input.repoRoot, input.worktree.path, { sessionsPath: input.sessionsPath })
|
|
1012
1039
|
]);
|
|
1013
1040
|
const { reclaimable, class: cls } = classify(tree, integration, liveness, input.nowMs);
|
|
1014
1041
|
return {
|
|
@@ -1093,7 +1120,7 @@ var cleanupWorktreeBuiltin = implementTool(
|
|
|
1093
1120
|
const discardUntracked = input.discardUntracked === true;
|
|
1094
1121
|
const discardModified = input.discardModified === true;
|
|
1095
1122
|
if (discardUntracked || discardModified) {
|
|
1096
|
-
const tree = await computeTreeState(input.cwd);
|
|
1123
|
+
const tree = await computeTreeState(input.repoRoot, input.cwd);
|
|
1097
1124
|
if (tree.state === "dirty") {
|
|
1098
1125
|
const blocked = [];
|
|
1099
1126
|
if (tree.untracked > 0 && !discardUntracked) blocked.push("untracked");
|
|
@@ -1197,6 +1224,6 @@ var worktreeProvider = defineDriver({
|
|
|
1197
1224
|
]
|
|
1198
1225
|
});
|
|
1199
1226
|
|
|
1200
|
-
export { CONFIG_FILENAME, ConfigError, DEFAULT_PROXY_PORT, ENV_VARS, FileVerdictMemoStore, HookError, InMemoryVerdictMemoStore, ProxyTable, SESSIONS_BUCKETS_ROOT, SESSIONS_FILE_PATH, ServiceSupervisor, VERDICT_MEMO_PATH, WorktreeNotRemovableError, allocatePort, classify, computeLiveness, computeProvenance, computeTreeState, computeWorktreeStatus, detectDefaultBranch, disposeSupervisor, ephemeralPort, execArgv, execGit, execShell, expandGlob, getScript, getSupervisor, globToRegExp, hookEnv, isPortFree, listGitWorktrees, listScripts, listServices, listWorktreeStatuses, loadConfigFromBase, normalizeHook, parseConfig, peerEnv, peerEnvKey, readSessionsRegistry, readWorktreeMarker, reconcileIntegration, repoLabel, resolveSupervisor, runSetup, runTeardown, serviceEnv, serviceEnvToken, serviceHostname, serviceUrl, sessionInWorktree, sharedProxyTable, slugify, stripPort, worktreeProvider, writeWorktreeMarker };
|
|
1201
|
-
//# sourceMappingURL=chunk-
|
|
1202
|
-
//# sourceMappingURL=chunk-
|
|
1227
|
+
export { CONFIG_FILENAME, ConfigError, DEFAULT_PROXY_PORT, ENV_VARS, FileVerdictMemoStore, HookError, InMemoryVerdictMemoStore, ProxyTable, SESSIONS_BUCKETS_ROOT, SESSIONS_FILE_PATH, ServiceSupervisor, VERDICT_MEMO_PATH, WORKTREES_TURBO_CACHE_DIR_ENV, WorktreeNotRemovableError, allocatePort, classify, computeLiveness, computeProvenance, computeTreeState, computeWorktreeStatus, detectDefaultBranch, disposeSupervisor, ephemeralPort, execArgv, execGit, execShell, expandGlob, getScript, getSupervisor, globToRegExp, hookEnv, isPortFree, listGitWorktrees, listScripts, listServices, listWorktreeStatuses, loadConfigFromBase, normalizeHook, parseConfig, peerEnv, peerEnvKey, readSessionsRegistry, readWorktreeMarker, reconcileIntegration, repoLabel, resolveSupervisor, resolveWorktreesTurboCacheDir, runSetup, runTeardown, serviceEnv, serviceEnvToken, serviceHostname, serviceUrl, sessionInWorktree, sharedProxyTable, slugify, stripPort, worktreeProvider, writeWorktreeMarker };
|
|
1228
|
+
//# sourceMappingURL=chunk-S5ARFUTK.mjs.map
|
|
1229
|
+
//# sourceMappingURL=chunk-S5ARFUTK.mjs.map
|