fix(): agent-safe leased worktree create/release pair with seat policy (Refs #340)

This commit is contained in:
John Smith committed 2026-10-12 00:34:27 -04:00
1 parent 71e42150de
commit f480221743
4 files changed
+327 -1

No files matched your search

+1
View File
@@ -49,6 +49,7 @@ spike as proof).
(repoints node_modules symlinks; `bun install` restores them)
- `es <status|runs|watch|approve|trust|waive|gates|factory|research|brainstorm|harvest|design|config|lsp|hooks|audit|scout|mcp|improve>`
- `es factory run --headless [--driver opencode] [--max-turns N] [--log-level quiet|info|debug] [--events jsonl] [--events-file <path>] [--turn-timeout S] [--cwd <dir>]` (SIGINT/SIGTERM cancel, exit 130; detached HEADs and other runs' seat worktrees refused; launching one stays human-only, spend-bearing like audit/scout runs; `docs/HEADLESS.md`)
- Human-process worktrees live under `.worktrees/<slug>` under a heartbeat lease (`es worktree create|release|prune`, agent-safe); never coordinate them with shell `flock` or `.git_lock`. Phase worktrees (`.factory/worktrees/`) and task worktrees (`.fleet/worktrees/`) are held by the engine — seats manage neither from the shell.
- `es mcp` pins caller identity to the adapter environment (`ES_MCP_AGENT`, else legacy `ES_AGENT`): the per-call `agent` argument is ignored; without either the caller is `mcp` (no seat)
- The CLI grammar lives in core (`packages/core/src/util/args.ts`): `parseArgs`, `flag`, `Args`, `BOOLEAN_FLAGS` — the human-only policy reads the same grammar (argv parity, #97)
- One package list (`bun scripts/packages.ts [--release] [--tsconfig] [--browser] [--no-browser]`): the typecheck loop, `pack-smoke.sh`, `publish.yml` and `deps.ts` all read it; new packages declare `esRelease.order` and `BOUNDARIES` (#102); the `bun --conditions=browser` test split comes from each manifest's `esTest.conditions`
+116 -1
View File
@@ -6,6 +6,7 @@ import { appendFile, readFile, writeFile } from "node:fs/promises"
import path from "node:path"
import {
type Args,
addWorktree,
aliasReuseRatio,
atomicWrite,
BOOLEAN_FLAGS,
@@ -50,6 +51,7 @@ import {
readProbes,
readTokens,
recordConsent,
removeWorktree,
renderBrainstorm,
renderGuide,
renderResearchRun,
@@ -63,6 +65,7 @@ import {
validateTokens,
verifyAuditChain,
verifyBrief,
WorktreeLeaseManager,
writeArtifacts,
writeProfile,
} from "@heretek-ai/es-core"
@@ -171,6 +174,10 @@ Other
scout show [--json] The scout's ranked verdicts
mcp Serve the factory tools over MCP (stdio)
worktree prune Drop stale worktree leases and remove orphaned .worktrees/ checkouts
worktree create <slug> [--branch <branch>] [--from <ref>]
Create a .worktrees/ checkout under a heartbeat lease (agent-safe)
worktree release <slug> Release the lease and remove the checkout (never a live
holder's checkout, never the branch; agent-safe)
rebase cascade --from <branch> --to <branch> [--no-verify]
Replay <to>'s commits onto <from> with TS/JSON conflict
resolution; <to> moves only when post-rebase checks pass
@@ -209,6 +216,24 @@ async function projectRoot(cwd: string): Promise<string> {
return top.code === 0 ? top.stdout.trim() : cwd
}
/**
* Heartbeat lease managers for the agent-safe worktree pair (#340): one per
* repo root, shared process-wide so a create in one call is releasable in
* the next. A release from another process never steals: a live lease held
* elsewhere is refused, a dead or expired one reclaimed.
*/
const worktreeLeaseManagers = new Map<string, WorktreeLeaseManager>()
const worktreeLeases = (root: string): WorktreeLeaseManager => {
const cached = worktreeLeaseManagers.get(root)
if (cached) return cached
const manager = new WorktreeLeaseManager(root)
worktreeLeaseManagers.set(root, manager)
return manager
}
const WORKTREE_USAGE =
"Usage: es worktree prune | es worktree create <slug> [--branch <branch>] [--from <ref>] | es worktree release <slug>"
export async function main(argv: readonly string[], io: MainIO): Promise<number> {
const args = parseArgs(argv, BOOLEAN_FLAGS)
const [command, sub, ...rest] = args.positionals
@@ -853,7 +878,97 @@ export async function main(argv: readonly string[], io: MainIO): Promise<number>
)
return 0
}
io.print("Usage: es worktree prune")
if (sub === "create") {
const slug = rest[0]
if (!slug) {
io.print("Usage: es worktree create <slug> [--branch <branch>] [--from <ref>]")
return 2
}
const branch = flag(args, "branch") ?? slug
const fromFlag = flag(args, "from")
const head = fromFlag ? undefined : await git(root, ["rev-parse", "HEAD"], { allowFail: true })
if (!fromFlag && head!.code !== 0) {
io.print(`Error: cannot resolve a base commit in ${root} (pass --from <ref>).`)
return 1
}
const from = fromFlag ?? head!.stdout.trim()
const dir = path.join(root, ".worktrees", slug)
const manager = worktreeLeases(root)
let ttlMs: number
try {
ttlMs = (await manager.acquire(slug, { branch, dir })).ttlMs
} catch (error) {
io.print(`Error: ${error instanceof Error ? error.message : String(error)}`)
return 1
}
try {
await addWorktree(root, dir, branch, from)
} catch (error) {
await manager.release(slug).catch(() => undefined)
io.print(`Error: ${error instanceof Error ? error.message : String(error)}`)
return 1
}
io.print(
`Created ${dir} on branch ${branch} under lease ${JSON.stringify(slug)} (heartbeat every ${ttlMs}ms).`,
)
return 0
}
if (sub === "release") {
const slug = rest[0]
if (!slug) {
io.print("Usage: es worktree release <slug>")
return 2
}
const manager = worktreeLeases(root)
const record = (await manager.list()).find((lease) => lease.slug === slug)
if (record && manager.isLive(record)) {
// A live lease may be ours (same process, token held) or another
// holder's: heartbeat proves ownership, and a failed heartbeat
// writes nothing, so refusal has no side effects.
const owned = await manager.heartbeat(slug).then(
() => true,
() => false,
)
if (!owned) {
io.print(
`Refusing: worktree ${JSON.stringify(slug)} is leased by a live holder (pid ${record.pid} on ${record.host}, heartbeat ${record.heartbeatAt}). Only a stale (expired or dead-holder) lease can be reclaimed.`,
)
return 1
}
}
const dir = record?.dir ?? path.join(root, ".worktrees", slug)
const branch = record?.branch ?? slug
let acquiredHere = false
if (record && !manager.isLive(record)) {
// Reclaim the dead lease so this process holds the token. A lease
// that turned live in the meantime refuses here instead of stealing.
try {
await manager.acquire(slug, { branch, dir })
acquiredHere = true
} catch (error) {
io.print(`Refusing: ${error instanceof Error ? error.message : String(error)}`)
return 1
}
}
try {
await removeWorktree(root, dir).catch(async () => {
await git(root, ["worktree", "remove", "--force", dir])
})
} catch (error) {
// A checkout we cannot remove keeps its lease: drop only a lease
// reclaimed above, never one we already held.
if (acquiredHere) await manager.release(slug).catch(() => undefined)
io.print(`Error: ${error instanceof Error ? error.message : String(error)}`)
return 1
}
// Prune invariant: the checkout goes, the branch stays. Our own
// lease (held or just reclaimed) is dropped; another holder's live
// lease was refused above and is never touched.
if (record) await manager.release(slug).catch(() => undefined)
io.print(`Released ${JSON.stringify(slug)}: removed ${dir} (branch ${branch} kept).`)
return 0
}
io.print(WORKTREE_USAGE)
return 2
}
case "factory": {
+174
View File
@@ -0,0 +1,174 @@
// Agent-safe leased worktree CLI pair (#340): `es worktree create` takes a
// heartbeat lease around a `.worktrees/<slug>` checkout and
// `es worktree release` drops it. Release never removes another holder's live
// checkout and never deletes branches.
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"
import { tmpdir } from "node:os"
import path from "node:path"
import { evaluateShell, exists, invokesHumanOnly, WorktreeLeaseManager } from "@heretek-ai/es-core"
import { main } from "../src/main.ts"
import type { ConfirmIO } from "../src/tty.ts"
let root: string
let state: string
const realXdgConfig = process.env.XDG_CONFIG_HOME
const git = async (...args: string[]) => {
const proc = Bun.spawn(["git", ...args], { cwd: root, stdout: "pipe", stderr: "pipe" })
await proc.exited
return proc
}
beforeEach(async () => {
root = await mkdtemp(path.join(tmpdir(), "es-worktree-cli-"))
state = await mkdtemp(path.join(tmpdir(), "es-worktree-cli-state-"))
process.env.XDG_CONFIG_HOME = path.join(state, "xdg-config")
await git("init", "-q", "-b", "main")
await writeFile(path.join(root, "README.md"), "# x\n")
await git("add", "-A")
await git("-c", "user.name=t", "-c", "user.email=t@t", "commit", "-q", "-m", "init")
})
afterEach(async () => {
if (realXdgConfig === undefined) delete process.env.XDG_CONFIG_HOME
else process.env.XDG_CONFIG_HOME = realXdgConfig
await rm(root, { recursive: true, force: true })
await rm(state, { recursive: true, force: true })
})
const pipe: ConfirmIO = { interactive: false, write: () => {}, readLine: async () => "y", readSecret: async () => "" }
async function run(argv: string[]) {
const printed: string[] = []
const code = await main(argv, { print: (text) => printed.push(text), confirm: pipe, cwd: root, stateDir: state })
return { code, out: printed.join("\n") }
}
const branchExists = async (branch: string) => {
const proc = Bun.spawn(["git", "show-ref", "--verify", "--quiet", `refs/heads/${branch}`], {
cwd: root,
stdout: "pipe",
stderr: "pipe",
})
await proc.exited
return proc.exitCode === 0
}
const leaseFile = (slug: string) => path.join(root, ".worktrees", ".leases", `${slug}.json`)
describe("es worktree create/release (#340)", () => {
test("create takes a live lease; release drops it and removes the checkout", async () => {
const created = await run(["worktree", "create", "demo", "--branch", "demo-branch"])
expect(created.code).toBe(0)
expect(await exists(path.join(root, ".worktrees", "demo"))).toBe(true)
const manager = new WorktreeLeaseManager(root)
const record = (await manager.list()).find((lease) => lease.slug === "demo")
expect(record && manager.isLive(record)).toBe(true)
const released = await run(["worktree", "release", "demo"])
expect(released.code).toBe(0)
expect(await exists(path.join(root, ".worktrees", "demo"))).toBe(false)
expect(await Bun.file(leaseFile("demo")).exists()).toBe(false)
// Prune invariant: checkouts go, branches stay.
expect(await branchExists("demo-branch")).toBe(true)
})
test("creating twice refuses the second holder", async () => {
expect((await run(["worktree", "create", "demo", "--branch", "demo-branch"])).code).toBe(0)
const second = await run(["worktree", "create", "demo", "--branch", "demo-branch"])
expect(second.code).toBe(1)
expect(second.out).toMatch(/leas|held/i)
})
test("release refuses another holder's live checkout and leaves it alone", async () => {
const dir = path.join(root, ".worktrees", "demo")
const add = Bun.spawn(["git", "worktree", "add", "-B", "demo-branch", dir, "HEAD"], {
cwd: root,
stdout: "pipe",
stderr: "pipe",
})
await add.exited
expect(add.exitCode).toBe(0)
// Another holder object (no shared token): the lease is live, pid alive.
const holder = new WorktreeLeaseManager(root)
await holder.acquire("demo", { branch: "demo-branch", dir })
const refused = await run(["worktree", "release", "demo"])
expect(refused.code).toBe(1)
expect(refused.out).toMatch(/held|live/i)
expect(await exists(dir)).toBe(true)
expect(await Bun.file(leaseFile("demo")).exists()).toBe(true)
await holder.release("demo")
const released = await run(["worktree", "release", "demo"])
expect(released.code).toBe(0)
expect(await exists(dir)).toBe(false)
expect(await branchExists("demo-branch")).toBe(true)
})
test("release reclaims a crashed holder and keeps the branch", async () => {
const dir = path.join(root, ".worktrees", "doomed")
const add = Bun.spawn(["git", "worktree", "add", "-B", "doomed-branch", dir, "HEAD"], {
cwd: root,
stdout: "pipe",
stderr: "pipe",
})
await add.exited
expect(add.exitCode).toBe(0)
const holder = new WorktreeLeaseManager(root)
await holder.acquire("doomed", { branch: "doomed-branch", dir })
// Simulate SIGKILL: the lease file keeps a fresh heartbeat but a dead pid.
const raw = JSON.parse(await readFile(leaseFile("doomed"), "utf8")) as Record<string, unknown>
await writeFile(leaseFile("doomed"), `${JSON.stringify({ ...raw, pid: 2 ** 30 })}\n`)
const released = await run(["worktree", "release", "doomed"])
expect(released.code).toBe(0)
expect(await exists(dir)).toBe(false)
expect(await branchExists("doomed-branch")).toBe(true)
await holder.release("doomed").catch(() => undefined)
})
test("release of an orphaned checkout (no lease) removes it and keeps the branch", async () => {
const add = Bun.spawn(
["git", "worktree", "add", "-B", "orphan-branch", path.join(root, ".worktrees", "orphan"), "HEAD"],
{
cwd: root,
stdout: "pipe",
stderr: "pipe",
},
)
await add.exited
expect(add.exitCode).toBe(0)
const released = await run(["worktree", "release", "orphan"])
expect(released.code).toBe(0)
expect(await exists(path.join(root, ".worktrees", "orphan"))).toBe(false)
expect(await branchExists("orphan-branch")).toBe(true)
})
test("missing slug is usage, not an error", async () => {
expect((await run(["worktree", "create"])).code).toBe(2)
expect((await run(["worktree", "release"])).code).toBe(2)
})
})
describe("worktree CLI pair policy (#340)", () => {
const ctx = () => ({ root, stateDir: state })
const shell = (agent: string | undefined, command: string) =>
evaluateShell(ctx(), agent, command, { sandboxAvailable: true })
test("the pair is agent-safe (not human-only)", () => {
expect(invokesHumanOnly("es worktree create demo --branch demo-branch")).toBe(false)
expect(invokesHumanOnly("es worktree release demo")).toBe(false)
expect(invokesHumanOnly("es worktree prune")).toBe(false)
})
test("seats may not manage worktrees from the shell; user agents may", () => {
expect(shell("es-programmer", "es worktree create demo --branch b").effect).toBe("deny")
expect(shell("es-programmer", "es worktree release demo").effect).toBe("deny")
expect(shell("factory", "es worktree release demo").effect).toBe("deny")
expect(shell(undefined, "es worktree create demo --branch b").effect).toBe("allow")
expect(shell(undefined, "es worktree release demo").effect).toBe("allow")
})
test("seats keep the agent-safe prune and plain --help", () => {
expect(shell("es-programmer", "es worktree prune").effect).toBe("allow")
expect(shell("es-programmer", "es worktree create demo --help").effect).toBe("allow")
})
})
+36
View File
@@ -384,6 +384,30 @@ const seatForbiddenGit = (command: string) =>
(sub === "branch" && args.some((arg) => /^-[a-zA-Z]*D/.test(arg))),
)
/** `es worktree` verbs that create or drop checkouts (prune stays agent-safe, #160). */
const ES_WORKTREE_MANAGING = new Set(["create", "release"])
/**
* `es worktree create|release` calls in a command, however flagged
* (`es --cwd . worktree create …`). Reads the same argv grammar as the CLI
* (#97): unknown flags count both as taking a value and not, like the
* human-only check above.
*/
function seatForbiddenEsWorktree(command: string): { verb: string } | undefined {
for (const segment of segments(unquoteShell(command))) {
const tokens = words(segment)
for (let i = 0; i < tokens.length; i++) {
if (!ES_BINARY.test(tokens[i]!)) continue
for (const verb of nextPositionals(tokens, { at: i + 1, literal: false })) {
if (tokens[verb.at] !== "worktree") continue
for (const sub of nextPositionals(tokens, { at: verb.at + 1, literal: verb.literal }))
if (ES_WORKTREE_MANAGING.has(tokens[sub.at] ?? "")) return { verb: tokens[sub.at]! }
}
}
}
return undefined
}
/**
* Commands that only read (each pipeline segment). No `env` (it runs any
* program), and no tool with an output-file option: `sort -o`, `uniq IN OUT`,
@@ -447,6 +471,18 @@ export function evaluateShell(
reason:
"Factory seats may not reference other tasks' fleet worktrees (.fleet/) from the shell; work only inside your own task worktree.",
}
// Leased worktrees (#340): phase worktrees are held by the engine and
// `.worktrees/` checkouts by whoever ran `es worktree create`. Seats
// manage neither from the shell; user agents may use the agent-safe
// create/release pair (a live checkout held elsewhere is refused there).
if (!isPlainHelp(command)) {
const managed = seatForbiddenEsWorktree(command)
if (managed)
return {
effect: "deny",
reason: `Factory seats may not manage worktrees from the shell (es worktree ${managed.verb}); phase worktrees are held under a heartbeat lease by the engine.`,
}
}
const git = seatForbiddenGit(command)
if (git)
return {