@markjaquith/agency 2.67.0 → 2.69.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.
Files changed (36) hide show
  1. package/README.md +61 -33
  2. package/cli-main.ts +2 -2
  3. package/fixtures/protocol/orchestration-recipes.json +2 -2
  4. package/package.json +1 -1
  5. package/schemas/agency-graph-v1.schema.json +2 -2
  6. package/src/cli-parser.test.ts +7 -7
  7. package/src/cli-parser.ts +11 -11
  8. package/src/cli.test.ts +11 -9
  9. package/src/commands/claim.ts +6 -6
  10. package/src/commands/doctor.test.ts +3 -3
  11. package/src/commands/init.test.ts +6 -0
  12. package/src/commands/integration.test.ts +20 -2
  13. package/src/commands/integration.ts +6 -4
  14. package/src/commands/work.test.ts +74 -22
  15. package/src/commands/work.ts +32 -38
  16. package/src/protocol.test.ts +2 -2
  17. package/src/services/ArchiveBulkService.test.ts +2 -2
  18. package/src/services/ClaimService.test.ts +3 -3
  19. package/src/services/ClaimService.ts +4 -4
  20. package/src/services/DoctorService.ts +24 -24
  21. package/src/services/IntegrationService.test.ts +158 -2
  22. package/src/services/IntegrationService.ts +51 -1
  23. package/src/services/ReadinessService.test.ts +1 -1
  24. package/src/services/ReviewService.test.ts +2 -2
  25. package/src/services/SyncService.test.ts +2 -2
  26. package/src/services/VcsMigrationService.test.ts +1 -1
  27. package/src/services/WorkbaseService.test.ts +33 -2
  28. package/src/services/WorkbaseService.ts +39 -3
  29. package/src/workbase/AGENTS.md +7 -7
  30. package/src/workbase/{runner-command.test.ts → agent-command.test.ts} +33 -23
  31. package/src/workbase/{runner-command.ts → agent-command.ts} +33 -29
  32. package/src/workbase/kickoff-contract.ts +4 -4
  33. package/src/workbase/opencode-file.ts +1 -1
  34. package/src/workbase/pi-extension-file.ts +158 -0
  35. package/src/workbase/schemas.test.ts +5 -5
  36. package/src/workbase/schemas.ts +8 -2
@@ -1,10 +1,10 @@
1
1
  import { describe, expect, test } from "bun:test"
2
2
  import {
3
3
  printableEnvironment,
4
- resolveRunnerCommand,
5
- runnerEnvironment,
6
- validateRunners,
7
- } from "./runner-command"
4
+ resolveAgentCommand,
5
+ agentEnvironment,
6
+ validateAgents,
7
+ } from "./agent-command"
8
8
 
9
9
  const variables = {
10
10
  prompt: "Read the task.",
@@ -17,45 +17,55 @@ const variables = {
17
17
  claimRevision: "revision-1",
18
18
  }
19
19
 
20
- describe("runner commands", () => {
20
+ describe("agent commands", () => {
21
21
  test("uses promptless interactive commands for built-in presets", () => {
22
22
  expect(
23
- resolveRunnerCommand("opencode2", undefined, variables, false).argv,
23
+ resolveAgentCommand("opencode2", undefined, variables, false).argv,
24
24
  ).toEqual(["opencode2"])
25
25
  expect(
26
- resolveRunnerCommand("opencode2", undefined, variables, true).argv,
26
+ resolveAgentCommand("opencode2", undefined, variables, true).argv,
27
27
  ).toEqual(["opencode2", "--continue"])
28
28
  expect(
29
- resolveRunnerCommand("opencode", undefined, variables, false).argv,
29
+ resolveAgentCommand("opencode", undefined, variables, false).argv,
30
30
  ).toEqual(["opencode"])
31
31
  expect(
32
- resolveRunnerCommand("opencode", undefined, variables, true).argv,
32
+ resolveAgentCommand("opencode", undefined, variables, true).argv,
33
33
  ).toEqual(["opencode", "--continue"])
34
+ expect(resolveAgentCommand("pi", undefined, variables, false).argv).toEqual(
35
+ ["pi"],
36
+ )
37
+ expect(resolveAgentCommand("pi", undefined, variables, true).argv).toEqual([
38
+ "pi",
39
+ "--continue",
40
+ ])
34
41
  expect(
35
- resolveRunnerCommand("claude", undefined, variables, true).argv,
42
+ resolveAgentCommand("claude", undefined, variables, true).argv,
36
43
  ).toEqual(["claude", "--continue"])
37
44
  })
38
45
 
39
46
  test("uses autonomous commands when a prompt is requested", () => {
40
47
  expect(
41
- resolveRunnerCommand("opencode2", undefined, variables, false, true).argv,
48
+ resolveAgentCommand("opencode2", undefined, variables, false, true).argv,
42
49
  ).toEqual(["opencode2", "--prompt", "Read the task."])
43
50
  expect(
44
- resolveRunnerCommand("opencode2", undefined, variables, true, true).argv,
51
+ resolveAgentCommand("opencode2", undefined, variables, true, true).argv,
45
52
  ).toEqual(["opencode2", "--continue", "--prompt", "Read the task."])
46
53
  expect(
47
- resolveRunnerCommand("opencode", undefined, variables, false, true).argv,
54
+ resolveAgentCommand("opencode", undefined, variables, false, true).argv,
48
55
  ).toEqual(["opencode", "--prompt", "Read the task."])
49
56
  expect(
50
- resolveRunnerCommand("opencode", undefined, variables, true, true).argv,
57
+ resolveAgentCommand("opencode", undefined, variables, true, true).argv,
51
58
  ).toEqual(["opencode", "--continue", "--prompt", "Read the task."])
52
59
  expect(
53
- resolveRunnerCommand("claude", undefined, variables, true, true).argv,
60
+ resolveAgentCommand("pi", undefined, variables, false, true).argv,
61
+ ).toEqual(["pi", "Read the task."])
62
+ expect(
63
+ resolveAgentCommand("claude", undefined, variables, true, true).argv,
54
64
  ).toEqual(["claude", "--continue", "Read the task."])
55
65
  })
56
66
 
57
67
  test("expands configured argv and environment without a shell", () => {
58
- const resolved = resolveRunnerCommand(
68
+ const resolved = resolveAgentCommand(
59
69
  "custom",
60
70
  {
61
71
  custom: {
@@ -79,33 +89,33 @@ describe("runner commands", () => {
79
89
  })
80
90
  })
81
91
 
82
- test("rejects --auto for configured runners without an auto command", () => {
92
+ test("rejects --auto for configured agents without an auto command", () => {
83
93
  expect(() =>
84
- resolveRunnerCommand(
94
+ resolveAgentCommand(
85
95
  "custom",
86
96
  { custom: { command: ["agent"] } },
87
97
  variables,
88
98
  false,
89
99
  true,
90
100
  ),
91
- ).toThrow("Runner 'custom' does not support --auto")
101
+ ).toThrow("Agent 'custom' does not support --auto")
92
102
  })
93
103
 
94
104
  test("rejects unknown placeholders", () => {
95
105
  expect(() =>
96
- validateRunners({ custom: { command: ["agent", "{unknown}"] } }),
97
- ).toThrow("Unknown runner 'custom' placeholder: {unknown}")
106
+ validateAgents({ custom: { command: ["agent", "{unknown}"] } }),
107
+ ).toThrow("Unknown agent 'custom' placeholder: {unknown}")
98
108
  })
99
109
 
100
110
  test("provides normalized Agency environment and filters secret values", () => {
101
111
  const environment = {
102
- ...runnerEnvironment("custom", variables),
112
+ ...agentEnvironment("custom", variables),
103
113
  VISIBLE: "yes",
104
114
  ACCESS_TOKEN: "secret",
105
115
  }
106
116
 
107
117
  expect(environment).toMatchObject({
108
- AGENCY_RUNNER: "custom",
118
+ AGENCY_AGENT: "custom",
109
119
  AGENCY_CLAIMANT: "orchestrator",
110
120
  AGENCY_SESSION_ID: "session-1",
111
121
  AGENCY_WORKBASE: "/workbase",
@@ -1,6 +1,6 @@
1
1
  import type { WorkbaseConfig } from "./schemas"
2
2
 
3
- export interface RunnerCommandVariables {
3
+ export interface AgentCommandVariables {
4
4
  readonly prompt: string
5
5
  readonly workbase: string
6
6
  readonly target: string
@@ -11,7 +11,7 @@ export interface RunnerCommandVariables {
11
11
  readonly claimRevision: string
12
12
  }
13
13
 
14
- interface RunnerDefinition {
14
+ interface AgentDefinition {
15
15
  readonly command: readonly string[]
16
16
  readonly autoCommand?: readonly string[]
17
17
  readonly resumeCommand?: readonly string[]
@@ -19,7 +19,7 @@ interface RunnerDefinition {
19
19
  readonly environment?: Readonly<Record<string, string>>
20
20
  }
21
21
 
22
- const PLACEHOLDERS = new Set<keyof RunnerCommandVariables>([
22
+ const PLACEHOLDERS = new Set<keyof AgentCommandVariables>([
23
23
  "prompt",
24
24
  "workbase",
25
25
  "target",
@@ -30,7 +30,7 @@ const PLACEHOLDERS = new Set<keyof RunnerCommandVariables>([
30
30
  "claimRevision",
31
31
  ])
32
32
 
33
- const BUILTIN_RUNNERS: Readonly<Record<string, RunnerDefinition>> = {
33
+ const BUILTIN_AGENTS: Readonly<Record<string, AgentDefinition>> = {
34
34
  opencode2: {
35
35
  command: ["opencode2"],
36
36
  autoCommand: ["opencode2", "--prompt", "{prompt}"],
@@ -43,6 +43,12 @@ const BUILTIN_RUNNERS: Readonly<Record<string, RunnerDefinition>> = {
43
43
  resumeCommand: ["opencode", "--continue"],
44
44
  autoResumeCommand: ["opencode", "--continue", "--prompt", "{prompt}"],
45
45
  },
46
+ pi: {
47
+ command: ["pi"],
48
+ autoCommand: ["pi", "{prompt}"],
49
+ resumeCommand: ["pi", "--continue"],
50
+ autoResumeCommand: ["pi", "--continue", "{prompt}"],
51
+ },
46
52
  claude: {
47
53
  command: ["claude"],
48
54
  autoCommand: ["claude", "{prompt}"],
@@ -51,48 +57,46 @@ const BUILTIN_RUNNERS: Readonly<Record<string, RunnerDefinition>> = {
51
57
  },
52
58
  }
53
59
 
54
- const validateTemplate = (runner: string, value: string) => {
60
+ const validateTemplate = (agent: string, value: string) => {
55
61
  for (const match of value.matchAll(/\{([^{}]+)\}/g)) {
56
62
  const placeholder = match[1]!
57
- if (!PLACEHOLDERS.has(placeholder as keyof RunnerCommandVariables)) {
58
- throw new Error(
59
- `Unknown runner '${runner}' placeholder: {${placeholder}}`,
60
- )
63
+ if (!PLACEHOLDERS.has(placeholder as keyof AgentCommandVariables)) {
64
+ throw new Error(`Unknown agent '${agent}' placeholder: {${placeholder}}`)
61
65
  }
62
66
  }
63
67
  }
64
68
 
65
- export const validateRunners = (runners: WorkbaseConfig["runners"]): void => {
66
- for (const [name, runner] of Object.entries(runners ?? {})) {
69
+ export const validateAgents = (agents: WorkbaseConfig["agents"]): void => {
70
+ for (const [name, agent] of Object.entries(agents ?? {})) {
67
71
  for (const value of [
68
- ...runner.command,
69
- ...(runner.autoCommand ?? []),
70
- ...(runner.resumeCommand ?? []),
71
- ...(runner.autoResumeCommand ?? []),
72
- ...Object.values(runner.environment ?? {}),
72
+ ...agent.command,
73
+ ...(agent.autoCommand ?? []),
74
+ ...(agent.resumeCommand ?? []),
75
+ ...(agent.autoResumeCommand ?? []),
76
+ ...Object.values(agent.environment ?? {}),
73
77
  ]) {
74
78
  validateTemplate(name, value)
75
79
  }
76
80
  }
77
81
  }
78
82
 
79
- const expand = (value: string, variables: RunnerCommandVariables) =>
83
+ const expand = (value: string, variables: AgentCommandVariables) =>
80
84
  value.replaceAll(
81
85
  /\{([^{}]+)\}/g,
82
86
  (match, placeholder: string) =>
83
- variables[placeholder as keyof RunnerCommandVariables] ?? match,
87
+ variables[placeholder as keyof AgentCommandVariables] ?? match,
84
88
  )
85
89
 
86
- export const resolveRunnerCommand = (
90
+ export const resolveAgentCommand = (
87
91
  name: string,
88
- configured: WorkbaseConfig["runners"],
89
- variables: RunnerCommandVariables,
92
+ configured: WorkbaseConfig["agents"],
93
+ variables: AgentCommandVariables,
90
94
  resume: boolean,
91
95
  auto = false,
92
96
  ) => {
93
- validateRunners(configured)
94
- const definition = configured?.[name] ?? BUILTIN_RUNNERS[name]
95
- if (!definition) throw new Error(`Unknown runner: ${name}`)
97
+ validateAgents(configured)
98
+ const definition = configured?.[name] ?? BUILTIN_AGENTS[name]
99
+ if (!definition) throw new Error(`Unknown agent: ${name}`)
96
100
  const template = auto
97
101
  ? resume
98
102
  ? (definition.autoResumeCommand ?? definition.autoCommand)
@@ -101,7 +105,7 @@ export const resolveRunnerCommand = (
101
105
  ? definition.resumeCommand
102
106
  : definition.command
103
107
  if (!template) {
104
- throw new Error(`Runner '${name}' does not support --auto`)
108
+ throw new Error(`Agent '${name}' does not support --auto`)
105
109
  }
106
110
  const argv = template.map((argument) => expand(argument, variables))
107
111
  const environment = Object.fromEntries(
@@ -113,11 +117,11 @@ export const resolveRunnerCommand = (
113
117
  return { argv, environment }
114
118
  }
115
119
 
116
- export const runnerEnvironment = (
117
- runner: string,
118
- variables: RunnerCommandVariables,
120
+ export const agentEnvironment = (
121
+ agent: string,
122
+ variables: AgentCommandVariables,
119
123
  ): Record<string, string> => ({
120
- AGENCY_RUNNER: runner,
124
+ AGENCY_AGENT: agent,
121
125
  AGENCY_CLAIMANT: variables.claimant,
122
126
  AGENCY_SESSION_ID: variables.sessionId,
123
127
  AGENCY_CLAIM_REVISION: variables.claimRevision,
@@ -325,11 +325,11 @@ export const buildKickoffPlan = (input: {
325
325
  recovery: "Reuse the existing split when present.",
326
326
  },
327
327
  {
328
- id: "runner-start",
328
+ id: "agent-start",
329
329
  cwd: taskDirectory,
330
330
  argv: ["agency", "work", ".", "--auto"],
331
331
  recovery:
332
- "Inspect the recorded tab before retrying; a working runner must not be duplicated.",
332
+ "Inspect the recorded tab before retrying; a working agent must not be duplicated.",
333
333
  },
334
334
  {
335
335
  id: "final-context-verification",
@@ -341,7 +341,7 @@ export const buildKickoffPlan = (input: {
341
341
  ],
342
342
  exactlyOnce: true,
343
343
  recovery:
344
- "If verification fails, inspect the existing tab; do not launch another runner.",
344
+ "If verification fails, inspect the existing tab; do not launch another agent.",
345
345
  },
346
346
  ],
347
347
  successFields: [
@@ -351,7 +351,7 @@ export const buildKickoffPlan = (input: {
351
351
  "preparedCheckout",
352
352
  "herdrWorkspace",
353
353
  "herdrTab",
354
- "runnerStart",
354
+ "agentStart",
355
355
  "contextVerification",
356
356
  ],
357
357
  }
@@ -31,7 +31,7 @@ const body = () =>
31
31
  "Handles Agency workbase orchestration and workflow operations with the Agency CLI",
32
32
  mode: "subagent",
33
33
  prompt:
34
- "You are the Agency workflow specialist. Use the Agency CLI to handle delegated workbase orchestration and workflow operations. Always start with `agency context . --json`, follow the managed Agency instructions and reported authority, use Agency commands for durable mutations, and report the resulting state concisely. When delegated to start or kick off work in another agent, launch it, verify that the runner started successfully, and return without waiting for the task to finish.",
34
+ "You are the Agency workflow specialist. Use the Agency CLI to handle delegated workbase orchestration and workflow operations. Always start with `agency context . --json`, follow the managed Agency instructions and reported authority, use Agency commands for durable mutations, and report the resulting state concisely. When delegated to start or kick off work in another agent, launch it, verify that the agent started successfully, and return without waiting for the task to finish.",
35
35
  },
36
36
  plan: {
37
37
  disable: true,
@@ -0,0 +1,158 @@
1
+ import { createHash } from "node:crypto"
2
+
3
+ const managedHeaderPattern =
4
+ /^\/\/ agency-managed: sha256=([a-f0-9]{64})\r?\n\r?\n/
5
+
6
+ const checksum = (content: string) =>
7
+ createHash("sha256").update(content).digest("hex")
8
+
9
+ const body = `import { existsSync, readFileSync } from "node:fs"
10
+ import { dirname, join, resolve } from "node:path"
11
+ import { CONFIG_DIR_NAME, type ExtensionAPI } from "@earendil-works/pi-coding-agent"
12
+
13
+ type AgencyContext = {
14
+ root?: string
15
+ checkout?: string
16
+ target?: string
17
+ }
18
+
19
+ const contextTarget = (result: Record<string, any>): string | undefined => {
20
+ const target = result.target
21
+ if (target?.kind === "epic") return \`epic:\${target.epicId}\`
22
+ if (target?.kind === "phase") {
23
+ return \`execution-unit:phase/\${target.taskId}/\${target.phaseId}\`
24
+ }
25
+ if (target?.kind === "task") {
26
+ return result.authority?.mode === "execution"
27
+ ? \`execution-unit:task/\${target.taskId}\`
28
+ : \`task:\${target.taskId}\`
29
+ }
30
+ }
31
+
32
+ const discoverWorkbase = (directory: string) => {
33
+ let current = directory
34
+ while (true) {
35
+ if (existsSync(join(current, "agency.json"))) return current
36
+ const parent = dirname(current)
37
+ if (parent === current) return
38
+ current = parent
39
+ }
40
+ }
41
+
42
+ const discoverCheckout = (directory: string, root: string | undefined) => {
43
+ if (!root) return
44
+ let current = directory
45
+ while (current.startsWith(root)) {
46
+ for (const name of ["PHASE.md", "TASK.md"]) {
47
+ const document = join(current, name)
48
+ if (!existsSync(document)) continue
49
+ const repo = readFileSync(document, "utf8").match(/^repo:\\s*([^\\s]+)\\s*$/m)?.[1]
50
+ if (!repo) return
51
+ const checkout = join(current, "code", repo.replace(/^['\"]|['\"]$/g, ""))
52
+ return existsSync(checkout) ? checkout : undefined
53
+ }
54
+ if (current === root) return
55
+ current = dirname(current)
56
+ }
57
+ }
58
+
59
+ const agencyContext = async (
60
+ pi: ExtensionAPI,
61
+ directory: string,
62
+ ): Promise<AgencyContext | undefined> => {
63
+ const task = process.env.AGENCY_TASK_ID
64
+ const phase = process.env.AGENCY_PHASE_ID
65
+ const args = task
66
+ ? ["context", "--task", task, ...(phase ? ["--phase", phase] : []), "--compact", "--json"]
67
+ : ["context", ".", "--compact", "--json"]
68
+ const command = await pi.exec("agency", args, { timeout: 5000 })
69
+ if (command.code !== 0) return
70
+ const envelope = JSON.parse(command.stdout)
71
+ if (envelope.ok !== true) return
72
+ const result = envelope.result ?? {}
73
+ const target = contextTarget(result)
74
+ const document = result.target?.path
75
+ const status = result.documents?.phase?.data?.status ?? result.documents?.task?.data?.status
76
+ if (
77
+ result.validation?.valid !== true ||
78
+ !target ||
79
+ !document ||
80
+ dirname(document) !== resolve(directory) ||
81
+ (target.startsWith("execution-unit:") &&
82
+ (!result.authority?.writable?.checkoutPath || status !== "working"))
83
+ ) return
84
+ return {
85
+ root: result.workbase?.root,
86
+ checkout: result.authority?.writable?.checkoutPath,
87
+ target,
88
+ }
89
+ }
90
+
91
+ const extension = (pi: ExtensionAPI) => {
92
+ const contexts = new Map<string, Promise<AgencyContext | undefined>>()
93
+ const runtimeContext = (directory: string) => {
94
+ let context = contexts.get(directory)
95
+ if (!context) {
96
+ context = agencyContext(pi, directory).catch(() => undefined)
97
+ contexts.set(directory, context)
98
+ }
99
+ return context
100
+ }
101
+
102
+ pi.on("resources_discover", async (event) => {
103
+ const context = await runtimeContext(event.cwd)
104
+ const root = process.env.AGENCY_WORKBASE ?? context?.root ?? discoverWorkbase(event.cwd)
105
+ const checkout = process.env.AGENCY_WRITABLE_CHECKOUT ?? context?.checkout ?? discoverCheckout(event.cwd, root)
106
+ if (!checkout) return
107
+
108
+ const skillPaths = [
109
+ join(checkout, ".claude", "skills"),
110
+ join(checkout, ".agents", "skills"),
111
+ join(checkout, ".opencode", "skill"),
112
+ join(checkout, ".opencode", "skills"),
113
+ join(checkout, CONFIG_DIR_NAME, "skills"),
114
+ ].filter(existsSync)
115
+ if (skillPaths.length === 0) return
116
+ return { skillPaths: [...new Set(skillPaths)] }
117
+ })
118
+
119
+ pi.on("before_agent_start", async (event, ctx) => {
120
+ const context = await runtimeContext(ctx.cwd)
121
+ const root = process.env.AGENCY_WORKBASE ?? context?.root ?? discoverWorkbase(ctx.cwd)
122
+ if (!root) return
123
+ const checkout = process.env.AGENCY_WRITABLE_CHECKOUT ?? context?.checkout ?? discoverCheckout(ctx.cwd, root)
124
+ const instructionsPath = join(root, ".agency", "AGENTS.md")
125
+ const instructions = existsSync(instructionsPath)
126
+ ? readFileSync(instructionsPath, "utf8").trim()
127
+ : undefined
128
+ const activeTarget = process.env.AGENCY_TARGET
129
+ const worker = activeTarget && context?.target === activeTarget
130
+ ? \`Agency verified this Pi session as the active worker for \${activeTarget}. Perform the assigned work directly. Do not invoke agency work for this target or launch a replacement worker.\`
131
+ : undefined
132
+ const access = \`The complete Agency workbase is available at \${root}. Use absolute paths under that root when workbase context is needed. Agency context remains the authority for writes.\`
133
+ const implementation = checkout
134
+ ? \`Pi remains rooted in the task or phase directory for Agency instructions and context. Treat \${checkout} as the default implementation directory for source reads, edits, repository status, builds, tests, formatting, and other repository-local commands. Run Agency lifecycle and context commands from the task or phase directory. Any reference checkouts reported by Agency context are read-only.\`
135
+ : undefined
136
+
137
+ return {
138
+ systemPrompt: [event.systemPrompt, instructions, access, worker, implementation]
139
+ .filter(Boolean)
140
+ .join("\\n\\n"),
141
+ }
142
+ })
143
+ }
144
+
145
+ export default extension
146
+ `
147
+
148
+ const renderManagedWorkbasePiExtension = (content: string) =>
149
+ `// agency-managed: sha256=${checksum(content)}\n\n${content}`
150
+
151
+ export const managedWorkbasePiExtension = renderManagedWorkbasePiExtension(body)
152
+
153
+ export const canUpdateManagedWorkbasePiExtension = (content: string) => {
154
+ const match = content.match(managedHeaderPattern)
155
+ if (!match?.[1]) return false
156
+
157
+ return checksum(content.slice(match[0].length)) === match[1]
158
+ }
@@ -276,11 +276,11 @@ describe("workspace creation configuration", () => {
276
276
  })
277
277
  })
278
278
 
279
- describe("runner configuration", () => {
279
+ describe("agent configuration", () => {
280
280
  test("accepts named argv commands with resume commands and environment", () => {
281
281
  const config = Schema.decodeUnknownSync(WorkbaseConfig)({
282
282
  version: 2,
283
- runners: {
283
+ agents: {
284
284
  custom: {
285
285
  command: ["agent"],
286
286
  autoCommand: ["agent", "{prompt}"],
@@ -291,14 +291,14 @@ describe("runner configuration", () => {
291
291
  },
292
292
  })
293
293
 
294
- expect(config.runners?.custom?.autoCommand).toEqual(["agent", "{prompt}"])
294
+ expect(config.agents?.custom?.autoCommand).toEqual(["agent", "{prompt}"])
295
295
  })
296
296
 
297
297
  test("rejects shell strings in place of argv arrays", () => {
298
298
  expect(() =>
299
299
  Schema.decodeUnknownSync(WorkbaseConfig)({
300
300
  version: 2,
301
- runners: { custom: { command: "agent {prompt}" } },
301
+ agents: { custom: { command: "agent {prompt}" } },
302
302
  }),
303
303
  ).toThrow()
304
304
  })
@@ -423,7 +423,7 @@ describe("work status", () => {
423
423
  describe("claim records", () => {
424
424
  const record = {
425
425
  claimant: "orchestrator",
426
- runner: "agent",
426
+ agent: "agent",
427
427
  sessionId: "job-1",
428
428
  startedAt: "2026-07-17T12:00:00.000Z",
429
429
  targetRevision: "0".repeat(64),
@@ -52,7 +52,7 @@ export const DocumentRevision = Schema.String.pipe(
52
52
 
53
53
  export const ClaimRecord = Schema.Struct({
54
54
  claimant: NonEmptyString,
55
- runner: NonEmptyString,
55
+ agent: NonEmptyString,
56
56
  sessionId: NonEmptyString,
57
57
  startedAt: IsoTimestamp,
58
58
  targetRevision: DocumentRevision,
@@ -110,7 +110,7 @@ export const WorkbaseConfig = Schema.Struct({
110
110
  chooserCommand: Schema.optional(Schema.NonEmptyArray(NonEmptyString)),
111
111
  worktreeCreateCommand: Schema.optional(Schema.NonEmptyArray(NonEmptyString)),
112
112
  workspaceCreateCommand: Schema.optional(Schema.NonEmptyArray(NonEmptyString)),
113
- runners: Schema.optional(
113
+ agents: Schema.optional(
114
114
  Schema.Record({
115
115
  key: EntityId,
116
116
  value: Schema.Struct({
@@ -146,6 +146,12 @@ export const WorkbaseRegistry = Schema.Struct({
146
146
  defaultId: Schema.optional(EntityId),
147
147
  })
148
148
 
149
+ export const GlobalConfig = Schema.Struct({
150
+ agent: Schema.optional(
151
+ Schema.Literal("opencode2", "opencode", "pi", "claude"),
152
+ ),
153
+ })
154
+
149
155
  export const Dependency = Schema.Struct({
150
156
  id: EntityId,
151
157
  dependsOn: Schema.optional(Schema.Array(EntityId)),