@markjaquith/agency 2.65.2 → 2.66.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 CHANGED
@@ -347,8 +347,10 @@ external effects the failed invocation may have completed.
347
347
 
348
348
  ### Agent Runners
349
349
 
350
- OpenCode and Claude Code are built-in runner presets. Select either preset or a
351
- configured runner with `agency work --runner <name>`. A launch is fresh unless
350
+ OpenCode (`opencode2` and `opencode`) and Claude Code are built-in runner presets.
351
+ Without an explicit runner, Agency uses the first available executable in this
352
+ order: `opencode2`, `opencode`, then `claude`. Select a preset or configured
353
+ runner with `agency work --runner <name>`. A launch is fresh unless
352
354
  `AGENCY_SESSION_ID` is already set; resumed launches use the runner's
353
355
  `resumeCommand` when configured. The built-in presets use `--continue` only for
354
356
  resumed launches. By default Agency opens the runner without a prompt. `--auto`
@@ -399,8 +401,9 @@ to the receiving OpenCode session, injects an explicit active-worker system
399
401
  instruction, and restores Agency identity for that session's shell environment.
400
402
  This session bridge is necessary because an OpenCode client can attach to a
401
403
  long-lived server process that did not inherit the client's launch environment.
402
- The `opencode` runner remains rooted in its task or epic working directory so
403
- the workbase `AGENTS.md` and managed OpenCode config are discovered normally.
404
+ The `opencode2` and `opencode` runners remain rooted in their task or epic
405
+ working directory so the workbase `AGENTS.md` and managed OpenCode config are
406
+ discovered normally.
404
407
  Agency's managed OpenCode plugin grants the active workbase external-directory
405
408
  access and adds existing checkout-local `.claude/skills`, `.agents/skills`, and
406
409
  `.opencode/{skill,skills}` directories to `skills.paths`.
@@ -1063,10 +1066,10 @@ configured external chooser. Pass a directory, including `.` for the current
1063
1066
  directory, to infer its epic, task, or phase. Outside a workbase, Agency first
1064
1067
  presents the registered workbases, then the selected workbase's hierarchy.
1065
1068
 
1066
- OpenCode is the default runner, with automatic Claude fallback when neither is
1067
- explicitly selected. `--opencode` and `--claude` remain aliases for requiring
1068
- their built-in presets. Launches are interactive and promptless by default; use
1069
- `--auto` to send Agency's generated context prompt.
1069
+ Agency automatically uses the first available runner in this order: `opencode2`,
1070
+ `opencode`, then `claude`. `--opencode` and `--claude` remain aliases for
1071
+ requiring their corresponding built-in presets. Launches are interactive and
1072
+ promptless by default; use `--auto` to send Agency's generated context prompt.
1070
1073
 
1071
1074
  `agency work prepare` resolves an execution unit and creates or reuses its
1072
1075
  writable and reference worktrees, or its single pinned review checkout, without
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "2.65.2",
3
+ "version": "2.66.0",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -1050,18 +1050,45 @@ describe("work command", () => {
1050
1050
  ).toBeUndefined()
1051
1051
  })
1052
1052
 
1053
- test("automatically falls back to Claude", async () => {
1054
- const harness = createHarness({ available: { opencode: false } })
1053
+ test("automatically prefers opencode2", async () => {
1054
+ const harness = createHarness()
1055
+
1056
+ await harness.run({ taskId: "example" })
1057
+
1058
+ expect(harness.probes).toEqual(["opencode2"])
1059
+ expect(harness.launches[0]).toEqual({
1060
+ cli: "opencode2",
1061
+ args: ["opencode2"],
1062
+ cwd: taskDirectory,
1063
+ })
1064
+ })
1065
+
1066
+ test("automatically falls back from opencode2 to opencode", async () => {
1067
+ const harness = createHarness({ available: { opencode2: false } })
1055
1068
 
1056
1069
  await harness.run({ taskId: "example" })
1057
1070
 
1058
- expect(harness.probes).toEqual(["opencode", "claude"])
1071
+ expect(harness.probes).toEqual(["opencode2", "opencode"])
1072
+ expect(harness.launches[0]).toEqual({
1073
+ cli: "opencode",
1074
+ args: ["opencode"],
1075
+ cwd: taskDirectory,
1076
+ })
1077
+ })
1078
+
1079
+ test("automatically falls back from OpenCode to Claude", async () => {
1080
+ const harness = createHarness({
1081
+ available: { opencode2: false, opencode: false },
1082
+ })
1083
+
1084
+ await harness.run({ taskId: "example" })
1085
+
1086
+ expect(harness.probes).toEqual(["opencode2", "opencode", "claude"])
1059
1087
  expect(harness.launches[0]).toEqual({
1060
1088
  cli: "claude",
1061
1089
  args: ["claude"],
1062
1090
  cwd: taskDirectory,
1063
1091
  })
1064
- expect(harness.launchEnvironments[0]?.OPENCODE_CONFIG).toBeUndefined()
1065
1092
  })
1066
1093
 
1067
1094
  test("does not fall back when OpenCode is explicitly required", async () => {
@@ -1090,13 +1117,13 @@ describe("work command", () => {
1090
1117
 
1091
1118
  test("fails when neither agent tool is available", async () => {
1092
1119
  const harness = createHarness({
1093
- available: { opencode: false, claude: false },
1120
+ available: { opencode2: false, opencode: false, claude: false },
1094
1121
  })
1095
1122
 
1096
1123
  await expect(harness.run({ taskId: "example" })).rejects.toThrow(
1097
1124
  "claude CLI tool not found",
1098
1125
  )
1099
- expect(harness.probes).toEqual(["opencode", "claude"])
1126
+ expect(harness.probes).toEqual(["opencode2", "opencode", "claude"])
1100
1127
  expect(harness.launches).toEqual([])
1101
1128
  })
1102
1129
 
@@ -1150,7 +1177,7 @@ describe("work command", () => {
1150
1177
  verboseHarness.run({ taskId: "example", verbose: true }),
1151
1178
  )
1152
1179
  expect(verboseLogs).toEqual([
1153
- "Launching command: opencode (cwd: /workbase/tasks/example)",
1180
+ "Launching command: opencode2 (cwd: /workbase/tasks/example)",
1154
1181
  ])
1155
1182
  expect(verboseHarness.materializeOptions[0]?.verbose).toBe(true)
1156
1183
 
@@ -280,10 +280,16 @@ export const work = (
280
280
  options.claude ||
281
281
  process.env.AGENCY_RUNNER,
282
282
  )
283
- let runner =
283
+ const defaultRunners = ["opencode2", "opencode", "claude"] as const
284
+ let defaultRunnerIndex = 0
285
+ let runner: string =
284
286
  options.runner ??
285
287
  process.env.AGENCY_RUNNER ??
286
- (options.claude ? "claude" : "opencode")
288
+ (options.claude
289
+ ? "claude"
290
+ : options.opencode
291
+ ? "opencode"
292
+ : defaultRunners[defaultRunnerIndex]!)
287
293
  const claimant = process.env.AGENCY_CLAIMANT ?? process.env.USER ?? "agency"
288
294
  const sessionId =
289
295
  process.env.AGENCY_SESSION_ID ?? `${process.pid}-${Date.now()}`
@@ -311,12 +317,12 @@ export const work = (
311
317
  let available = yield* fs.runCommand(["which", cli], {
312
318
  captureOutput: true,
313
319
  })
314
- if (
320
+ while (
315
321
  available.exitCode !== 0 &&
316
322
  !explicitlyRequested &&
317
- runner === "opencode"
323
+ defaultRunnerIndex < defaultRunners.length - 1
318
324
  ) {
319
- runner = "claude"
325
+ runner = defaultRunners[++defaultRunnerIndex]!
320
326
  resolved = resolveRunnerCommand(
321
327
  runner,
322
328
  config.runners,
@@ -580,7 +586,8 @@ Launch an agent for an epic, task, or phase. With no directory, select one
580
586
  interactively. A positional argument resolves as a directory first, then as a task
581
587
  ID. Use '.' for the current directory. Outside a workbase, select a registered
582
588
  workbase first. Managed OpenCode launches receive whole-workbase access through
583
- Agency's project plugin; Agency context remains authoritative for writes.
589
+ Agency's project plugin; Agency context remains authoritative for writes. Automatic
590
+ runner discovery checks opencode2, opencode, then claude.
584
591
 
585
592
  The prepare subcommand resolves and materializes an execution workspace without
586
593
  launching an agent or changing lifecycle status. --dry-run reports planned Git
@@ -19,6 +19,12 @@ const variables = {
19
19
 
20
20
  describe("runner commands", () => {
21
21
  test("uses promptless interactive commands for built-in presets", () => {
22
+ expect(
23
+ resolveRunnerCommand("opencode2", undefined, variables, false).argv,
24
+ ).toEqual(["opencode2"])
25
+ expect(
26
+ resolveRunnerCommand("opencode2", undefined, variables, true).argv,
27
+ ).toEqual(["opencode2", "--continue"])
22
28
  expect(
23
29
  resolveRunnerCommand("opencode", undefined, variables, false).argv,
24
30
  ).toEqual(["opencode"])
@@ -31,6 +37,12 @@ describe("runner commands", () => {
31
37
  })
32
38
 
33
39
  test("uses autonomous commands when a prompt is requested", () => {
40
+ expect(
41
+ resolveRunnerCommand("opencode2", undefined, variables, false, true).argv,
42
+ ).toEqual(["opencode2", "--prompt", "Read the task."])
43
+ expect(
44
+ resolveRunnerCommand("opencode2", undefined, variables, true, true).argv,
45
+ ).toEqual(["opencode2", "--continue", "--prompt", "Read the task."])
34
46
  expect(
35
47
  resolveRunnerCommand("opencode", undefined, variables, false, true).argv,
36
48
  ).toEqual(["opencode", "--prompt", "Read the task."])
@@ -31,6 +31,12 @@ const PLACEHOLDERS = new Set<keyof RunnerCommandVariables>([
31
31
  ])
32
32
 
33
33
  const BUILTIN_RUNNERS: Readonly<Record<string, RunnerDefinition>> = {
34
+ opencode2: {
35
+ command: ["opencode2"],
36
+ autoCommand: ["opencode2", "--prompt", "{prompt}"],
37
+ resumeCommand: ["opencode2", "--continue"],
38
+ autoResumeCommand: ["opencode2", "--continue", "--prompt", "{prompt}"],
39
+ },
34
40
  opencode: {
35
41
  command: ["opencode"],
36
42
  autoCommand: ["opencode", "--prompt", "{prompt}"],