geneseed 3.4.0 → 3.6.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/CHANGELOG.md +316 -0
- package/DESIGN.md +11 -14
- package/QUICKSTART.md +3 -1
- package/README.md +51 -42
- package/SETUP.md +25 -27
- package/SHIPPED.md +26 -32
- package/adapters/bob/README.md +12 -9
- package/adapters/copilot/README.md +26 -25
- package/adapters/opencode/GLOBAL-HARNESS-SPEC.md +1 -1
- package/adapters/opencode/README.md +2 -2
- package/adapters/opencode/plugins/geneseed-context.js +5 -3
- package/adapters/opencode/plugins/geneseed-workflow.js +30 -3
- package/adapters/opencode/workflows/_runtime.js +97 -7
- package/bin/build-driver.mjs +5 -1248
- package/bin/geneseed-cli.mjs +11 -6
- package/bin/geneseed-hook.mjs +4 -2
- package/docs/candidate-hosts.md +1 -1
- package/docs/design-history.md +37 -37
- package/docs/extending.md +49 -24
- package/docs/token-footprint.md +52 -26
- package/docs/web/claude-hooks.md +3 -2
- package/docs/web/collaboration.md +2 -2
- package/docs/web/first-session.md +1 -1
- package/docs/web/footprint.md +2 -2
- package/docs/web/harnesses.md +1 -1
- package/docs/web/model.md +1 -1
- package/docs/web/plugin-workflow.md +1 -0
- package/docs/web/rules.md +3 -3
- package/docs/web/setup-choices.md +2 -2
- package/docs/web/themes-author.md +1 -1
- package/docs/web-ui.md +18 -10
- package/harness.config.json +1 -1
- package/js/README.md +6 -5
- package/js/build/driver.mjs +1191 -0
- package/js/build/emit-claude.mjs +17 -10
- package/js/build/emit-common.mjs +4 -4
- package/js/build/generate.mjs +1 -1
- package/js/build/render.mjs +48 -44
- package/js/build/stubs.mjs +1 -1
- package/js/build/themes.mjs +4 -4
- package/js/build/version.mjs +12 -0
- package/js/cli-table.json +21 -6
- package/js/hosts/hooks.mjs +89 -74
- package/js/hosts/hosts.mjs +19 -2
- package/js/hosts/mcp.mjs +14 -20
- package/js/hosts/native.mjs +23 -17
- package/js/hosts/settings.mjs +52 -104
- package/js/inspect/checks-authoring.mjs +117 -3
- package/js/inspect/diff.mjs +1 -1
- package/js/inspect/doctor.mjs +1 -1
- package/js/inspect/excludes.mjs +1 -1
- package/js/inspect/scan.mjs +3 -14
- package/js/inspect/status.mjs +2 -2
- package/js/inspect/validate.mjs +1 -1
- package/js/lib/fs.mjs +23 -8
- package/js/lib/json.mjs +7 -1
- package/js/lib/proc.mjs +6 -4
- package/js/maintain/migrate.mjs +1 -1
- package/js/maintain/setup.mjs +1 -1
- package/js/maintain/uninstall.mjs +17 -15
- package/js/ui/cli.mjs +19 -0
- package/js/ui/menu.mjs +1 -1
- package/js/web/actions.mjs +4 -4
- package/js/web/activity.mjs +4 -4
- package/js/web/api.mjs +14 -11
- package/js/web/daemon.mjs +1 -1
- package/js/web/docs.mjs +12 -12
- package/js/web/handler.mjs +12 -5
- package/js/web/jobs.mjs +1 -1
- package/package.json +1 -1
- package/src/AGENT.md.tmpl +74 -38
- package/src/agents/developer.md +2 -2
- package/src/agents/reviewer.md +5 -2
- package/src/doctrines/README.md +26 -3
- package/src/doctrines/craft.md +78 -5
- package/src/doctrines/ops.md +62 -0
- package/src/doctrines/process.md +91 -4
- package/src/doctrines/rigor.md +40 -0
- package/src/laws/universal.md +27 -21
- package/src/memory/README.md +4 -2
- package/src/modes/foreman.md +5 -5
- package/src/notebook/README.md +2 -1
- package/src/ontology/universal.md +71 -67
- package/src/postures/peer.md +3 -0
- package/src/skills/bruno-collection-generator.md +1 -1
- package/src/skills/bruno-test-writer.md +2 -2
- package/src/skills/daydream/LICENSE +21 -0
- package/src/skills/daydream/VENDOR.md +3 -1
- package/src/skills/debug.md +1 -1
- package/src/skills/document-project.md +127 -25
- package/src/skills/geneseed-code-review.md +6 -2
- package/src/skills/parallel-agents.md +3 -3
- package/src/skills/pipeline.md +4 -4
- package/src/skills/react-view-transitions/VENDOR.md +7 -1
- package/src/skills/workflow.md +12 -4
- package/themes/_TEMPLATE.json +7 -5
- package/themes/biker.json +6 -4
- package/themes/commentator.json +6 -4
- package/themes/cyberpunk.json +6 -4
- package/themes/gamer.json +6 -4
- package/themes/imperial.json +6 -4
- package/themes/joker.json +6 -4
- package/themes/marvin.json +6 -4
- package/themes/mean.json +6 -4
- package/themes/military.json +6 -4
- package/themes/neutral.json +7 -5
- package/themes/opencode/README.md +2 -2
- package/themes/pirate.json +6 -4
- package/themes/sports.json +6 -4
- package/themes/verstappen.json +6 -4
- package/themes/wizard.json +6 -4
- package/web/dist/assets/Activity-Flgf4kHy.js +1 -0
- package/web/dist/assets/ActivityDetail-DIpr85nh.js +1 -0
- package/web/dist/assets/CatalogRow-CjyLjaLb.js +1 -0
- package/web/dist/assets/Diff-Deisdha8.js +2 -0
- package/web/dist/assets/Docs-DGUdY4l4.js +2 -0
- package/web/dist/assets/Doctor-F74lJ69P.js +1 -0
- package/web/dist/assets/FilterInput-BakJLvaV.js +1 -0
- package/web/dist/assets/GreenhouseView-D072lBiD.js +2 -0
- package/web/dist/assets/Harness-DNRP2Va7.js +1 -0
- package/web/dist/assets/JournalView-DWFqZ0Iw.js +1 -0
- package/web/dist/assets/Laws-BN6g3HU7.js +3 -0
- package/web/dist/assets/Library-CDOv4emM.js +1 -0
- package/web/dist/assets/Markdown-BQAvN8xx.js +38 -0
- package/web/dist/assets/OperatorHudView-CwNIUVX8.js +2 -0
- package/web/dist/assets/Profile-CpP9BTne.js +1 -0
- package/web/dist/assets/Rules-DFXsUCB7.js +1 -0
- package/web/dist/assets/Settings-BqZVUtJ5.js +1 -0
- package/web/dist/assets/Skills-Cu6kBgfF.js +1 -0
- package/web/dist/assets/StatusBadge-Cg6j-NJL.js +1 -0
- package/web/dist/assets/activity-iWaWVtsR.js +1 -0
- package/web/dist/assets/drawable-BgfrEBer.js +1 -0
- package/web/dist/assets/index-CAqrWhKl.js +2 -0
- package/web/dist/assets/index-Ruh93A3A.css +1 -0
- package/web/dist/assets/jobBuckets-Dbq8ZlWW.js +1 -0
- package/web/dist/assets/lawCats-BLgxl151.js +1 -0
- package/web/dist/assets/react-DbkvxWG5.js +8 -0
- package/web/dist/assets/rolldown-runtime-CbXtAM7H.js +1 -0
- package/web/dist/index.html +4 -3
- package/web/src/pages/Laws.jsx +22 -8
- package/src/skills/react-view-transitions/README.md +0 -42
- package/web/dist/assets/Activity-DMGhhjQc.js +0 -1
- package/web/dist/assets/ActivityDetail-CeQcDA7e.js +0 -1
- package/web/dist/assets/CatalogRow-CnSbDPyg.js +0 -1
- package/web/dist/assets/Diff-D_ksMBD1.js +0 -5
- package/web/dist/assets/Doctor-Brb3_oCT.js +0 -1
- package/web/dist/assets/FilterInput-DbRWPhtT.js +0 -1
- package/web/dist/assets/GreenhouseView-F3VNFr73.js +0 -2
- package/web/dist/assets/Harness-DJSwfYeF.js +0 -1
- package/web/dist/assets/JournalView-BLRYRnB3.js +0 -1
- package/web/dist/assets/Laws-CKYns9qe.js +0 -3
- package/web/dist/assets/Library-BK0YloSv.js +0 -1
- package/web/dist/assets/Markdown-BzTRzc7a.js +0 -46
- package/web/dist/assets/OperatorHudView-D3OQSGp5.js +0 -2
- package/web/dist/assets/Profile-CPA2u_0g.js +0 -1
- package/web/dist/assets/Rules-Lukel-Ru.js +0 -1
- package/web/dist/assets/Skills-BKRLHH7m.js +0 -1
- package/web/dist/assets/StatusBadge-DI3weRKd.js +0 -1
- package/web/dist/assets/activity-C_H2Bx1P.js +0 -1
- package/web/dist/assets/index-B7_Wga23.js +0 -2
- package/web/dist/assets/index-DzdQjvM1.css +0 -1
- package/web/dist/assets/index-N8dTrVro.js +0 -1
- package/web/dist/assets/index-Py_IQjgK.js +0 -10
- package/web/dist/assets/jobBuckets-DfTUJUxR.js +0 -1
- package/web/dist/assets/lawCats-DUS9UTH4.js +0 -1
- package/web/dist/assets/react-DOpiTn5v.js +0 -32
|
@@ -185,7 +185,7 @@ repo's layout defeats the convention.
|
|
|
185
185
|
|
|
186
186
|
### 3.5 Injection block format (mirror `harness.py cmd_context`)
|
|
187
187
|
|
|
188
|
-
Byte-identical structure to
|
|
188
|
+
Byte-identical structure to the retired Python `harness.py` (lines 237-263) so both
|
|
189
189
|
enforcement paths read the same. Adds a machine marker (first line) for §4.
|
|
190
190
|
|
|
191
191
|
```
|
|
@@ -380,8 +380,8 @@ behaviour** — nothing changes the machine's current agent/model unless you opt
|
|
|
380
380
|
`tokyonight`, `rosepine`, `gruvbox`, `nord`, `everforest`, `kanagawa`, `dracula` — each
|
|
381
381
|
in a **solid** (opaque) and a **transparent** (terminal-background-through) flavour.
|
|
382
382
|
These are decoupled from the voice theme: any voice theme can run any colour theme.
|
|
383
|
-
Authored under `themes/opencode/` (see its README
|
|
384
|
-
|
|
383
|
+
Authored under `themes/opencode/` (see its README; the design record was the
|
|
384
|
+
`2026-06-17-opencode-color-themes` spec).
|
|
385
385
|
- **Runtime guard plugin** (`geneseed-guard.js`, installed with the others). Enforces
|
|
386
386
|
the safety Laws at the tool boundary: **blocks** writes to private-key/credential
|
|
387
387
|
files (Law I), catastrophic shell like `rm -rf /` (Law IV), and any mutation under
|
|
@@ -138,10 +138,12 @@ let fallbackVisible = false
|
|
|
138
138
|
function log(msg) { if (DEBUG) console.error(`[geneseed-context] ${msg}`) }
|
|
139
139
|
|
|
140
140
|
// ---- convention --------------------------------------------------------------
|
|
141
|
-
// Root-level files injected in full.
|
|
141
|
+
// Root-level files injected in full. Canonical entry docs + agent-directed rules OpenCode
|
|
142
|
+
// does not read by itself. AGENT.md (opencode.json `instructions`), AGENTS.md and CLAUDE.md
|
|
143
|
+
// (OpenCode's own native roots) are deliberately absent: OpenCode already has them, and
|
|
144
|
+
// injecting them again re-sent the whole harness on every request until 2026-09.
|
|
142
145
|
const EAGER_ROOT = new Set([
|
|
143
|
-
"
|
|
144
|
-
"README.md", "CONTRIBUTING.md", "user-rules.md", "PROFILE.md",
|
|
146
|
+
".cursorrules", "README.md", "CONTRIBUTING.md", "user-rules.md", "PROFILE.md",
|
|
145
147
|
])
|
|
146
148
|
// Doc trees walked recursively; everything found is lazy (listed, not injected).
|
|
147
149
|
const LAZY_DIRS = ["docs", "doc", "documentation", "architecture", "adr", "ADR"]
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
// Quiet by default; GENESEED_DEBUG=1 logs to stderr. GENESEED_WORKFLOWS_DIR overrides
|
|
18
18
|
// where saved workflows are loaded from.
|
|
19
19
|
|
|
20
|
+
import { execFile } from "node:child_process"
|
|
20
21
|
import { promises as fs } from "node:fs"
|
|
21
22
|
import * as path from "node:path"
|
|
22
23
|
import { fileURLToPath, pathToFileURL } from "node:url"
|
|
@@ -63,6 +64,17 @@ function nowStamp() {
|
|
|
63
64
|
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
// The runtime's git executor, for `isolation: "worktree"`. argv only, never a shell string:
|
|
68
|
+
// branch and directory names derive from agent labels a script author wrote.
|
|
69
|
+
function git(argv, cwd) {
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
execFile("git", argv, { cwd, windowsHide: true, maxBuffer: 16 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
72
|
+
if (err) reject(new Error(`git ${argv[0]}: ${String(stderr || err.message).trim()}`))
|
|
73
|
+
else resolve(String(stdout))
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
66
78
|
function safeJson(v) {
|
|
67
79
|
try { return JSON.stringify(v, null, 2) } catch { return String(v) }
|
|
68
80
|
}
|
|
@@ -73,7 +85,22 @@ function summarize(name, result, stats, tracePath) {
|
|
|
73
85
|
(stats.phases.length ? `, phases: ${stats.phases.join(" → ")}` : "")
|
|
74
86
|
let body = safeJson(result)
|
|
75
87
|
if (body.length > 2000) body = body.slice(0, 2000) + `\n… (truncated — full result in ${tracePath})`
|
|
76
|
-
return `${head}\n\nResult:\n${body}\n\nTrace: ${tracePath}`
|
|
88
|
+
return `${head}\n\nResult:\n${body}${isolationReport(stats)}\n\nTrace: ${tracePath}`
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Kept worktrees are work awaiting review, and an overlap is a conflict awaiting ONE
|
|
92
|
+
// merger (process 8) — both are said out loud, never left for the trace to hold.
|
|
93
|
+
function isolationReport(stats) {
|
|
94
|
+
const kept = stats.worktrees || []
|
|
95
|
+
if (!kept.length) return ""
|
|
96
|
+
const lines = ["", "", `Worktrees kept (${kept.length}) — review and merge them yourself; nothing was merged:`]
|
|
97
|
+
for (const w of kept) lines.push(`- ${w.label}: ${w.branch} at ${w.dir} — ${w.files.length} file(s)`)
|
|
98
|
+
const over = stats.overlaps || []
|
|
99
|
+
if (over.length) {
|
|
100
|
+
lines.push(`CONFLICTS — ${over.length} file(s) changed by more than one agent; merge these by hand or through one merge agent:`)
|
|
101
|
+
for (const o of over) lines.push(`- ${o.file}: ${o.labels.join(", ")}`)
|
|
102
|
+
}
|
|
103
|
+
return lines.join("\n")
|
|
77
104
|
}
|
|
78
105
|
|
|
79
106
|
async function writeTrace(rootDir, runId, lines) {
|
|
@@ -142,7 +169,7 @@ export const GeneseedWorkflow = async (ctx) => {
|
|
|
142
169
|
try { ({ createRuntime } = await import(pathToFileURL(path.join(WORKFLOWS_DIR, "_runtime.js")).href + "?t=" + Date.now())) }
|
|
143
170
|
catch (e) { return `Workflow runtime unavailable: ${e?.message || e}` }
|
|
144
171
|
|
|
145
|
-
const rt = createRuntime({ client, directory, worktree, args: normalizeArgs(argv?.args), log: sink })
|
|
172
|
+
const rt = createRuntime({ client, directory, worktree, git, runId, args: normalizeArgs(argv?.args), log: sink })
|
|
146
173
|
let result, failed = null
|
|
147
174
|
try { result = await wf.run(rt) }
|
|
148
175
|
catch (e) { failed = e; sink(`✗ workflow error: ${e?.stack || e?.message || e}`) }
|
|
@@ -152,7 +179,7 @@ export const GeneseedWorkflow = async (ctx) => {
|
|
|
152
179
|
if (!failed) sink(`# result:\n${safeJson(result)}`)
|
|
153
180
|
const tracePath = await writeTrace(rootDir, runId, lines)
|
|
154
181
|
|
|
155
|
-
if (failed) return `Workflow "${name}" failed: ${failed?.message || failed}\nTrace: ${tracePath}`
|
|
182
|
+
if (failed) return `Workflow "${name}" failed: ${failed?.message || failed}${isolationReport(stats)}\nTrace: ${tracePath}`
|
|
156
183
|
return summarize(name, result, stats, tracePath)
|
|
157
184
|
}
|
|
158
185
|
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
// hard sandbox guard. See docs/specs/2026-06-09-opencode-workflow-primitive.md.
|
|
18
18
|
|
|
19
19
|
import * as os from "node:os"
|
|
20
|
+
import * as path from "node:path"
|
|
20
21
|
|
|
21
22
|
const DEBUG = !!process.env.GENESEED_DEBUG
|
|
22
23
|
function dlog(m) { if (DEBUG) console.error(`[geneseed-workflow] ${m}`) }
|
|
@@ -150,13 +151,17 @@ function usageFrom(res) {
|
|
|
150
151
|
return { input: Number(tok?.input ?? 0) || 0, output: Number(tok?.output ?? 0) || 0 }
|
|
151
152
|
}
|
|
152
153
|
|
|
153
|
-
async function runChildSession(client, { prompt, agent, model, title }) {
|
|
154
|
+
async function runChildSession(client, { prompt, agent, model, title, directory }) {
|
|
154
155
|
if (!client?.session?.create || !client?.session?.prompt) {
|
|
155
156
|
throw new Error("OpenCode SDK session API unavailable")
|
|
156
157
|
}
|
|
157
158
|
let created
|
|
158
|
-
|
|
159
|
-
|
|
159
|
+
// `query.directory` routes the request to that directory's instance, and the session keeps
|
|
160
|
+
// it — so an isolated child runs in its worktree. Never retried without it: a child that
|
|
161
|
+
// asked for a worktree and silently ran in the shared tree is the failure isolation exists for.
|
|
162
|
+
const q = directory ? { query: { directory } } : {}
|
|
163
|
+
try { created = await client.session.create({ body: { title: title || "geneseed-workflow" }, ...q }) }
|
|
164
|
+
catch (e) { if (directory) throw e; created = await client.session.create() }
|
|
160
165
|
const sid = sessionId(created)
|
|
161
166
|
if (!sid) throw new Error("could not create child session")
|
|
162
167
|
try {
|
|
@@ -164,7 +169,7 @@ async function runChildSession(client, { prompt, agent, model, title }) {
|
|
|
164
169
|
if (agent) body.agent = agent
|
|
165
170
|
if (model) body.model = model
|
|
166
171
|
let res = null
|
|
167
|
-
try { res = await client.session.prompt({ path: { id: sid }, body }) }
|
|
172
|
+
try { res = await client.session.prompt({ path: { id: sid }, body, ...q }) }
|
|
168
173
|
catch (e) { dlog(`prompt failed: ${e?.message || e}`) }
|
|
169
174
|
const r = unwrap(res)
|
|
170
175
|
let text = partsText(r?.parts ?? r?.info?.parts ?? [])
|
|
@@ -175,9 +180,52 @@ async function runChildSession(client, { prompt, agent, model, title }) {
|
|
|
175
180
|
}
|
|
176
181
|
}
|
|
177
182
|
|
|
183
|
+
// ---- worktree isolation ------------------------------------------------------
|
|
184
|
+
// `agent(prompt, { isolation: "worktree" })` — the same option Claude Code's Workflow
|
|
185
|
+
// takes. Parallel editors in one tree lose each other's writes (process 8, one writer per
|
|
186
|
+
// file), so each isolated agent gets its own branch + worktree off the root's HEAD, and
|
|
187
|
+
// the files it changed are recorded for `overlaps()`. A worktree with no change is
|
|
188
|
+
// removed with its branch; one with changes is KEPT for the parent to review and merge —
|
|
189
|
+
// the runtime never merges, commits, or deletes work. `git(argv, cwd)` is injected by the
|
|
190
|
+
// plugin so this file stays pure and testable. The worktree has no untracked files from
|
|
191
|
+
// the root (no node_modules, no .env): an agent that must run the project installs first.
|
|
192
|
+
const slug = (t) => String(t).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 32) || "agent"
|
|
193
|
+
|
|
194
|
+
export async function isolate(git, root, name) {
|
|
195
|
+
if (typeof git !== "function") throw new Error("worktree isolation unavailable — no git executor")
|
|
196
|
+
if (!root) throw new Error("worktree isolation unavailable — unknown working dir")
|
|
197
|
+
const base = (await git(["rev-parse", "HEAD"], root)).trim()
|
|
198
|
+
const branch = `geneseed-wf/${name}`
|
|
199
|
+
const dir = path.join(os.tmpdir(), "geneseed-wf", name)
|
|
200
|
+
await git(["worktree", "add", "-b", branch, dir, base], root)
|
|
201
|
+
return { dir, branch, base }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function changedFiles(git, w) {
|
|
205
|
+
const lines = (out) => String(out).split("\n").map((l) => l.trim()).filter(Boolean)
|
|
206
|
+
const tracked = lines(await git(["diff", "--name-only", w.base], w.dir))
|
|
207
|
+
const untracked = lines(await git(["ls-files", "--others", "--exclude-standard"], w.dir))
|
|
208
|
+
return [...new Set([...tracked, ...untracked])].sort()
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Files more than one isolated agent changed — the conflicts a merge would otherwise
|
|
212
|
+
// settle by whoever landed last. [{ file, labels }], sorted by file.
|
|
213
|
+
export function overlaps(worktrees) {
|
|
214
|
+
const by = new Map()
|
|
215
|
+
for (const w of worktrees || []) for (const f of w.files || []) {
|
|
216
|
+
if (!by.has(f)) by.set(f, [])
|
|
217
|
+
by.get(f).push(w.label)
|
|
218
|
+
}
|
|
219
|
+
return [...by].filter(([, l]) => l.length > 1).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
220
|
+
.map(([file, labels]) => ({ file, labels }))
|
|
221
|
+
}
|
|
222
|
+
|
|
178
223
|
// ---- the runtime ------------------------------------------------------------
|
|
179
224
|
export function createRuntime(ctx) {
|
|
180
|
-
const { client, directory, worktree, log: sink } = ctx
|
|
225
|
+
const { client, directory, worktree, log: sink, git } = ctx
|
|
226
|
+
const root = worktree || directory
|
|
227
|
+
const runTag = slug(ctx.runId || "run")
|
|
228
|
+
const worktrees = []
|
|
181
229
|
const args = ctx.args || {}
|
|
182
230
|
const cap = ctx.concurrency || concurrencyCap()
|
|
183
231
|
const limit = createLimiter(cap)
|
|
@@ -202,9 +250,32 @@ export function createRuntime(ctx) {
|
|
|
202
250
|
async function spawn(prompt, opts) {
|
|
203
251
|
return limit(() => runChildSession(client, {
|
|
204
252
|
prompt, agent: opts.agent, model: opts.model, title: `geneseed-wf:${opts.label}`,
|
|
253
|
+
directory: opts.directory,
|
|
205
254
|
}))
|
|
206
255
|
}
|
|
207
256
|
|
|
257
|
+
// Git calls are serialised: concurrent `worktree add -b` calls contend for the same ref
|
|
258
|
+
// and worktree locks, and a lost lock fails an agent that did nothing wrong.
|
|
259
|
+
const gitQ = createLimiter(1)
|
|
260
|
+
const g = typeof git === "function" ? (argv, cwd) => gitQ(() => git(argv, cwd)) : undefined
|
|
261
|
+
|
|
262
|
+
// An isolated agent that changed nothing has its worktree and branch released; one that
|
|
263
|
+
// changed files is KEPT and recorded. A failed release is logged, never thrown.
|
|
264
|
+
async function settle(w, label) {
|
|
265
|
+
let files
|
|
266
|
+
try { files = await changedFiles(g, w) }
|
|
267
|
+
catch (e) { emit(` ⚠ ${label}: could not list changes — worktree kept at ${w.dir}: ${e?.message || e}`); return }
|
|
268
|
+
if (files.length) {
|
|
269
|
+
worktrees.push({ label, dir: w.dir, branch: w.branch, base: w.base, files })
|
|
270
|
+
emit(` ⎇ ${label}: ${files.length} file(s) changed — kept at ${w.dir} (branch ${w.branch})`)
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
await g(["worktree", "remove", w.dir], root)
|
|
275
|
+
await g(["branch", "-D", w.branch], root)
|
|
276
|
+
} catch (e) { emit(` ⚠ ${label}: could not release unchanged worktree — ${e?.message || e}`) }
|
|
277
|
+
}
|
|
278
|
+
|
|
208
279
|
async function agent(prompt, opts = {}) {
|
|
209
280
|
if (agentCount >= MAX_AGENTS) throw new Error(`workflow agent cap (${MAX_AGENTS}) exceeded`)
|
|
210
281
|
if (budgetTotal != null && spent >= budgetTotal) {
|
|
@@ -213,8 +284,21 @@ export function createRuntime(ctx) {
|
|
|
213
284
|
agentCount++
|
|
214
285
|
const label = opts.label || String(prompt).slice(0, 40)
|
|
215
286
|
emit(`· agent: ${label}`)
|
|
287
|
+
if (opts.isolation !== "worktree") return runAgent(prompt, { ...opts, label })
|
|
288
|
+
// The worktree wraps EVERY attempt, schema retries included — a retry that ran in the
|
|
289
|
+
// shared tree would undo the isolation it was asked for.
|
|
290
|
+
let w
|
|
291
|
+
try { w = await isolate(g, root, `${runTag}-${agentCount}-${slug(label)}`) }
|
|
292
|
+
catch (e) { emit(`✗ agent failed: ${label} — ${e?.message || e}`); return null }
|
|
293
|
+
emit(` ⎇ ${label}: worktree ${w.dir}`)
|
|
294
|
+
try { return await runAgent(prompt, { ...opts, label, directory: w.dir }) }
|
|
295
|
+
finally { await settle(w, label) }
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function runAgent(prompt, opts) {
|
|
299
|
+
const { label } = opts
|
|
216
300
|
let out
|
|
217
|
-
try { out = await spawn(prompt,
|
|
301
|
+
try { out = await spawn(prompt, opts) }
|
|
218
302
|
catch (e) { emit(`✗ agent failed: ${label} — ${e?.message || e}`); return null }
|
|
219
303
|
spent += out.usage?.output || 0
|
|
220
304
|
if (!opts.schema) return out.text
|
|
@@ -270,7 +354,13 @@ export function createRuntime(ctx) {
|
|
|
270
354
|
|
|
271
355
|
return {
|
|
272
356
|
agent, parallel, pipeline, phase, log, budget, args, directory, worktree,
|
|
273
|
-
|
|
357
|
+
// Isolated agents that changed something, and the files two or more of them touched.
|
|
358
|
+
worktrees: () => worktrees.map((w) => ({ ...w, files: [...w.files] })),
|
|
359
|
+
overlaps: () => overlaps(worktrees),
|
|
360
|
+
_stats: () => ({
|
|
361
|
+
agentCount, spent, cap, phases: [...phases], trace: [...trace],
|
|
362
|
+
worktrees: worktrees.map((w) => ({ ...w, files: [...w.files] })), overlaps: overlaps(worktrees),
|
|
363
|
+
}),
|
|
274
364
|
}
|
|
275
365
|
}
|
|
276
366
|
|