@markjaquith/agency 2.68.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.
@@ -8,6 +8,7 @@ import { parseFrontmatter } from "../workbase/frontmatter"
8
8
  import {
9
9
  EntityId,
10
10
  EpicFrontmatter,
11
+ GlobalConfig,
11
12
  LegacyWorkbaseRegistry,
12
13
  PhaseFrontmatter,
13
14
  TaskFrontmatter,
@@ -22,7 +23,7 @@ import {
22
23
  import { validateWorktreeCreateCommand } from "../workbase/worktree-command"
23
24
  import { validateWorkspaceCreateCommand } from "../workbase/workspace-command"
24
25
  import { validatePostCheckoutCommand } from "../workbase/checkout-command"
25
- import { validateRunners } from "../workbase/runner-command"
26
+ import { validateAgents } from "../workbase/agent-command"
26
27
  import { findDependencyCycles } from "../workbase/dependency-graph"
27
28
  import { validateDelivery } from "../workbase/delivery-command"
28
29
  import { preferredVersionControl } from "../workbase/version-control"
@@ -90,6 +91,15 @@ const registryPath = (configDirectory?: string) =>
90
91
  "workbases.json",
91
92
  )
92
93
 
94
+ const globalConfigPath = (configDirectory?: string) =>
95
+ join(
96
+ configDirectory ||
97
+ process.env.XDG_CONFIG_HOME ||
98
+ join(homedir(), ".config"),
99
+ "agency",
100
+ "agency.json",
101
+ )
102
+
93
103
  const registrationId = (path: string) =>
94
104
  `wb-${new Bun.CryptoHasher("sha256").update(path).digest("hex").slice(0, 12)}`
95
105
 
@@ -178,6 +188,32 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
178
188
  "WorkbaseService",
179
189
  {
180
190
  sync: () => ({
191
+ loadGlobalConfig: (configDirectory?: string) =>
192
+ Effect.gen(function* () {
193
+ const fs = yield* FileSystemService
194
+ const path = globalConfigPath(configDirectory)
195
+ if (!(yield* fs.exists(path))) return {}
196
+ const content = yield* fs.readFile(path)
197
+ let input: unknown
198
+ try {
199
+ input = JSON.parse(content)
200
+ } catch (cause) {
201
+ return yield* new WorkbaseConfigError({
202
+ path,
203
+ message: `Invalid JSON in global Agency configuration ${path}`,
204
+ cause,
205
+ })
206
+ }
207
+ const decoded = decode(GlobalConfig, input)
208
+ if (!decoded.success) {
209
+ return yield* new WorkbaseConfigError({
210
+ path,
211
+ message: `Invalid global Agency configuration in ${path}:\n${decoded.error}`,
212
+ })
213
+ }
214
+ return decoded.value
215
+ }),
216
+
181
217
  initialize: (path: string = process.cwd()) =>
182
218
  Effect.gen(function* () {
183
219
  const fs = yield* FileSystemService
@@ -309,7 +345,7 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
309
345
  }
310
346
  }
311
347
  try {
312
- validateRunners(decoded.value.runners)
348
+ validateAgents(decoded.value.agents)
313
349
  validateDelivery(decoded.value.delivery)
314
350
  } catch (cause) {
315
351
  return yield* new WorkbaseConfigError({
@@ -317,7 +353,7 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
317
353
  message:
318
354
  cause instanceof Error
319
355
  ? cause.message
320
- : "Invalid runner configuration",
356
+ : "Invalid agent configuration",
321
357
  })
322
358
  }
323
359
  return current
@@ -121,7 +121,7 @@ Do not rediscover commands that this recipe or a known-current
121
121
  prepare the task with
122
122
  `agency work prepare <slug> --evidence <creation-json> --dry-run --json`, then
123
123
  execute the returned plan through `task-document-split` to prepare the
124
- checkout and open or reuse the background tab. Stop before `runner-start`.
124
+ checkout and open or reuse the background tab. Stop before `agent-start`.
125
125
  3. For work/launch/start/kickoff intent, run that same preflight and execute its
126
126
  ordered kickoff steps. The plan owns worktree dry-run/preparation, a
127
127
  retry-safe background Herdr tab, the side-by-side task document,
@@ -131,8 +131,8 @@ Do not rediscover commands that this recipe or a known-current
131
131
  `agency-kickoff-v1` capability, execute the supplied actions directly. Do not
132
132
  call Herdr help, skill, or CLI discovery. If capability/version evidence is
133
133
  absent or stale, discovery is the compatibility path; then resume the same
134
- idempotency key rather than creating another tab, checkout, or runner.
135
- 5. After the one final context verification succeeds, leave the runner in the
134
+ idempotency key rather than creating another tab, checkout, or agent.
135
+ 5. After the one final context verification succeeds, leave the agent in the
136
136
  background and stop. Do not inspect, poll, or babysit it unless the user asks.
137
137
 
138
138
  Validation evidence is a local, auditable optimization, not authority. Preflight
@@ -153,12 +153,12 @@ IDs and remediation commands before retrying.
153
153
 
154
154
  `agency work` is the human launch flow: it reconciles managed integration,
155
155
  selects work, checks readiness, prepares checkouts, marks execution work
156
- `working` without creating a claim, and starts the runner. Epic and multi-phase
156
+ `working` without creating a claim, and starts the agent. Epic and multi-phase
157
157
  task launches remain orchestration-only. External orchestrators instead claim
158
- an execution unit, launch and monitor their runner separately, and finish or
158
+ an execution unit, launch and monitor their agent separately, and finish or
159
159
  release the claim with the current document revision.
160
160
 
161
- An Agency-launched runner receives process-local worker identity through both
161
+ An Agency-launched agent receives process-local worker identity through both
162
162
  the `AGENCY_SESSION_ID` and `AGENCY_TARGET` environment variables and a generated
163
163
  prompt beginning `Agency worker launch target: <target>.` Treat either form as
164
164
  launch evidence only after `agency context . --json` confirms the same target,
@@ -166,7 +166,7 @@ document paths, valid context, and expected write authority. Once confirmed,
166
166
  perform the assigned work directly and never invoke `agency work` to start the
167
167
  same target again.
168
168
 
169
- Some runner clients attach to a long-lived process and may not preserve launch
169
+ Some agent clients attach to a long-lived process and may not preserve launch
170
170
  environment variables. If the variables and prompt marker are absent, fail safe
171
171
  when the initial instruction is a generated `Start`, `Continue`, or `Work on`
172
172
  prompt whose absolute document paths match the current directory and the active,
@@ -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,
@@ -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)),