@markjaquith/agency 1.11.1 → 2.0.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 +302 -113
- package/cli.ts +123 -420
- package/index.ts +1 -1
- package/package.json +12 -17
- package/skills/agency/SKILL.md +258 -0
- package/src/cli.test.ts +290 -0
- package/src/commands/description.test.ts +103 -0
- package/src/commands/epic.test.ts +103 -0
- package/src/commands/epic.ts +101 -0
- package/src/commands/init.test.ts +42 -290
- package/src/commands/init.ts +15 -123
- package/src/commands/phase.ts +120 -0
- package/src/commands/pr.test.ts +19 -170
- package/src/commands/pr.ts +23 -179
- package/src/commands/repo.test.ts +80 -0
- package/src/commands/repo.ts +83 -0
- package/src/commands/status.test.ts +35 -477
- package/src/commands/status.ts +24 -213
- package/src/commands/task-phase.test.ts +206 -0
- package/src/commands/task.ts +83 -1285
- package/src/commands/validate.test.ts +91 -0
- package/src/commands/validate.ts +50 -0
- package/src/commands/work.test.ts +172 -419
- package/src/commands/work.ts +47 -166
- package/src/services/EpicService.test.ts +71 -0
- package/src/services/EpicService.ts +135 -0
- package/src/services/FileSystemService.ts +61 -92
- package/src/services/PhaseService.ts +323 -0
- package/src/services/PullRequestService.test.ts +386 -0
- package/src/services/PullRequestService.ts +143 -0
- package/src/services/RepositoryService.test.ts +106 -0
- package/src/services/RepositoryService.ts +159 -0
- package/src/services/TaskPhaseService.test.ts +217 -0
- package/src/services/TaskService.ts +192 -0
- package/src/services/WorkbaseService.test.ts +446 -0
- package/src/services/WorkbaseService.ts +493 -0
- package/src/services/WorktreeService.test.ts +599 -0
- package/src/services/WorktreeService.ts +357 -0
- package/src/test-utils.ts +41 -438
- package/src/types.ts +1 -215
- package/src/utils/command.ts +1 -5
- package/src/utils/effect.test.ts +24 -0
- package/src/utils/effect.ts +3 -273
- package/src/utils/process.test.ts +0 -15
- package/src/utils/process.ts +1 -60
- package/src/workbase/frontmatter.test.ts +50 -0
- package/src/workbase/frontmatter.ts +77 -0
- package/src/workbase/repository-reference.test.ts +25 -0
- package/src/workbase/repository-reference.ts +18 -0
- package/src/workbase/schemas.test.ts +146 -0
- package/src/workbase/schemas.ts +78 -0
- package/src/workbase/worktree-command.test.ts +61 -0
- package/src/workbase/worktree-command.ts +52 -0
- package/src/commands/base.test.ts +0 -198
- package/src/commands/base.ts +0 -198
- package/src/commands/clean.test.ts +0 -299
- package/src/commands/clean.ts +0 -274
- package/src/commands/completions.test.ts +0 -51
- package/src/commands/completions.ts +0 -308
- package/src/commands/emit.integration.test.ts +0 -549
- package/src/commands/emit.test.ts +0 -576
- package/src/commands/emit.ts +0 -700
- package/src/commands/emitted.test.ts +0 -226
- package/src/commands/emitted.ts +0 -57
- package/src/commands/loop.test.ts +0 -621
- package/src/commands/loop.ts +0 -406
- package/src/commands/merge.integration.test.ts +0 -195
- package/src/commands/merge.test.ts +0 -246
- package/src/commands/merge.ts +0 -244
- package/src/commands/pull.test.ts +0 -395
- package/src/commands/pull.ts +0 -205
- package/src/commands/push.test.ts +0 -641
- package/src/commands/push.ts +0 -482
- package/src/commands/rebase.test.ts +0 -521
- package/src/commands/rebase.ts +0 -232
- package/src/commands/save.test.ts +0 -247
- package/src/commands/save.ts +0 -162
- package/src/commands/source.test.ts +0 -195
- package/src/commands/source.ts +0 -72
- package/src/commands/switch.test.ts +0 -194
- package/src/commands/switch.ts +0 -84
- package/src/commands/task-branching.test.ts +0 -633
- package/src/commands/task-continue.test.ts +0 -311
- package/src/commands/task-edit.test.ts +0 -235
- package/src/commands/task-main.test.ts +0 -1066
- package/src/commands/task-squash.test.ts +0 -266
- package/src/commands/tasks.test.ts +0 -391
- package/src/commands/tasks.ts +0 -161
- package/src/commands/template-delete.test.ts +0 -178
- package/src/commands/template-delete.ts +0 -98
- package/src/commands/template-list.test.ts +0 -135
- package/src/commands/template-list.ts +0 -87
- package/src/commands/template-view.test.ts +0 -158
- package/src/commands/template-view.ts +0 -86
- package/src/commands/template.test.ts +0 -32
- package/src/commands/template.ts +0 -96
- package/src/commands/use.test.ts +0 -87
- package/src/commands/use.ts +0 -97
- package/src/constants.ts +0 -10
- package/src/errors.ts +0 -17
- package/src/schemas.ts +0 -33
- package/src/services/AgencyMetadataService.ts +0 -311
- package/src/services/ClaudeService.test.ts +0 -184
- package/src/services/ClaudeService.ts +0 -91
- package/src/services/ConfigService.ts +0 -115
- package/src/services/FilterRepoService.ts +0 -164
- package/src/services/FormatterService.test.ts +0 -432
- package/src/services/FormatterService.ts +0 -219
- package/src/services/GitService.ts +0 -1187
- package/src/services/MockFilterRepoService.ts +0 -140
- package/src/services/OpencodeService.ts +0 -263
- package/src/services/PromptService.ts +0 -183
- package/src/services/TemplateService.ts +0 -81
- package/src/types/native-exec.d.ts +0 -8
- package/src/utils/colors.ts +0 -178
- package/src/utils/git-path.ts +0 -6
- package/src/utils/glob.test.ts +0 -161
- package/src/utils/glob.ts +0 -78
- package/src/utils/paths.ts +0 -51
- package/src/utils/pr-branch.test.ts +0 -376
- package/src/utils/pr-branch.ts +0 -508
- package/src/utils/spinner.test.ts +0 -45
- package/src/utils/spinner.ts +0 -96
- package/src/utils/task-parser.test.ts +0 -172
- package/src/utils/task-parser.ts +0 -93
- package/templates/AGENCY.md +0 -22
- package/templates/AGENTS.md +0 -11
- package/templates/CLAUDE.md +0 -3
- package/templates/TASK.md +0 -5
- package/templates/opencode.json +0 -4
|
@@ -1,462 +1,215 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { Effect } from "effect"
|
|
3
|
+
import { FileSystemService } from "../services/FileSystemService"
|
|
4
|
+
import { WorktreeService } from "../services/WorktreeService"
|
|
5
|
+
import { captureLogs } from "../test-utils"
|
|
3
6
|
import { work } from "./work"
|
|
4
|
-
import {
|
|
5
|
-
createTempDir,
|
|
6
|
-
cleanupTempDir,
|
|
7
|
-
initGitRepo,
|
|
8
|
-
initAgency,
|
|
9
|
-
runTestEffect,
|
|
10
|
-
createCommit,
|
|
11
|
-
} from "../test-utils"
|
|
12
|
-
import { writeFileSync } from "node:fs"
|
|
13
7
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
hasClaude?: boolean
|
|
28
|
-
onSpawn?: (args: string[], options: any) => any
|
|
29
|
-
} = {},
|
|
30
|
-
) {
|
|
31
|
-
const { hasOpencode = true, hasClaude = false, onSpawn } = options
|
|
32
|
-
|
|
33
|
-
const originalSpawn = Bun.spawn
|
|
34
|
-
const originalSpawnSync = Bun.spawnSync
|
|
35
|
-
|
|
36
|
-
// @ts-ignore - mocking for test
|
|
37
|
-
Bun.spawnSync = (args: any, options: any) => {
|
|
38
|
-
// Mock which command to return success/failure based on config
|
|
39
|
-
if (Array.isArray(args) && args[0] === "which") {
|
|
40
|
-
if (args[1] === "opencode") {
|
|
41
|
-
return { exitCode: hasOpencode ? 0 : 1 }
|
|
42
|
-
}
|
|
43
|
-
if (args[1] === "claude") {
|
|
44
|
-
return { exitCode: hasClaude ? 0 : 1 }
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return originalSpawnSync(args, options)
|
|
48
|
-
}
|
|
8
|
+
type ExecutionWorkspace = Effect.Effect.Success<
|
|
9
|
+
ReturnType<WorktreeService["materialize"]>
|
|
10
|
+
>
|
|
11
|
+
|
|
12
|
+
const singlePhaseWorkspace: ExecutionWorkspace = {
|
|
13
|
+
root: "/workbase",
|
|
14
|
+
taskPath: "/workbase/tasks/example/TASK.md",
|
|
15
|
+
phasePath: null,
|
|
16
|
+
codePath: "/workbase/tasks/example/code",
|
|
17
|
+
writablePath: "/workbase/tasks/example/code/agency",
|
|
18
|
+
repo: "agency",
|
|
19
|
+
repos: [],
|
|
20
|
+
}
|
|
49
21
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
22
|
+
const multiPhaseWorkspace: ExecutionWorkspace = {
|
|
23
|
+
root: "/workbase",
|
|
24
|
+
taskPath: "/workbase/tasks/example/TASK.md",
|
|
25
|
+
phasePath: "/workbase/tasks/example/phases/implementation/PHASE.md",
|
|
26
|
+
codePath: "/workbase/tasks/example/phases/implementation/code",
|
|
27
|
+
writablePath: "/workbase/tasks/example/phases/implementation/code/agency",
|
|
28
|
+
repo: "agency",
|
|
29
|
+
repos: [],
|
|
30
|
+
}
|
|
56
31
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
32
|
+
interface HarnessOptions {
|
|
33
|
+
readonly workspace?: ExecutionWorkspace
|
|
34
|
+
readonly materializeError?: Error
|
|
35
|
+
readonly available?: Partial<Record<"opencode" | "claude", boolean>>
|
|
36
|
+
}
|
|
61
37
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
38
|
+
const createHarness = (options: HarnessOptions = {}) => {
|
|
39
|
+
const events: string[] = []
|
|
40
|
+
const probes: string[] = []
|
|
41
|
+
const launches: Array<{
|
|
42
|
+
cli: string
|
|
43
|
+
args: readonly string[]
|
|
44
|
+
cwd: string
|
|
45
|
+
}> = []
|
|
46
|
+
const worktrees = {
|
|
47
|
+
materialize: () => {
|
|
48
|
+
events.push("materialize")
|
|
49
|
+
return options.materializeError
|
|
50
|
+
? Effect.fail(options.materializeError)
|
|
51
|
+
: Effect.succeed(options.workspace ?? singlePhaseWorkspace)
|
|
52
|
+
},
|
|
69
53
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
54
|
+
const fs = {
|
|
55
|
+
runCommand: (args: readonly string[]) => {
|
|
56
|
+
const cli = args[1] as "opencode" | "claude"
|
|
57
|
+
events.push(`probe:${cli}`)
|
|
58
|
+
probes.push(cli)
|
|
59
|
+
return Effect.succeed({
|
|
60
|
+
exitCode: options.available?.[cli] === false ? 1 : 0,
|
|
61
|
+
stdout: "",
|
|
62
|
+
stderr: "",
|
|
63
|
+
})
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
const launch = (cli: string, args: readonly string[], cwd: string) => {
|
|
67
|
+
events.push(`launch:${cli}`)
|
|
68
|
+
launches.push({ cli, args, cwd })
|
|
77
69
|
}
|
|
70
|
+
const run = (commandOptions: Parameters<typeof work>[0]) =>
|
|
71
|
+
Effect.runPromise(
|
|
72
|
+
work(commandOptions, launch).pipe(
|
|
73
|
+
Effect.provideService(WorktreeService, worktrees as never),
|
|
74
|
+
Effect.provideService(FileSystemService, fs as never),
|
|
75
|
+
) as Effect.Effect<void, unknown, never>,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
return { events, probes, launches, run }
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
describe("work command", () => {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
beforeEach(async () => {
|
|
85
|
-
tempDir = await createTempDir()
|
|
86
|
-
originalCwd = process.cwd()
|
|
87
|
-
process.chdir(tempDir)
|
|
88
|
-
|
|
89
|
-
// Initialize git repo
|
|
90
|
-
await initGitRepo(tempDir)
|
|
91
|
-
await createCommit(tempDir, "Initial commit")
|
|
92
|
-
})
|
|
82
|
+
test("requires a task ID before materialization", async () => {
|
|
83
|
+
const harness = createHarness()
|
|
93
84
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
await cleanupTempDir(tempDir)
|
|
85
|
+
await expect(harness.run({})).rejects.toThrow("Task ID is required")
|
|
86
|
+
expect(harness.events).toEqual([])
|
|
97
87
|
})
|
|
98
88
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
expect(
|
|
102
|
-
runTestEffect(work({ silent: true, _noExec: true })),
|
|
103
|
-
).rejects.toThrow(
|
|
104
|
-
"TASK.md not found. Run 'agency task' first to create it.",
|
|
105
|
-
)
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
test("throws error when not in a git repository", async () => {
|
|
109
|
-
const nonGitDir = await createTempDir()
|
|
110
|
-
process.chdir(nonGitDir)
|
|
89
|
+
test("rejects conflicting agent flags before materialization", async () => {
|
|
90
|
+
const harness = createHarness()
|
|
111
91
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
await cleanupTempDir(nonGitDir)
|
|
117
|
-
})
|
|
92
|
+
await expect(
|
|
93
|
+
harness.run({ taskId: "example", opencode: true, claude: true }),
|
|
94
|
+
).rejects.toThrow("Cannot use both --opencode and --claude")
|
|
95
|
+
expect(harness.events).toEqual([])
|
|
118
96
|
})
|
|
119
97
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
restore()
|
|
142
|
-
|
|
143
|
-
expect(spawnCalled).toBe(true)
|
|
144
|
-
expect(spawnArgs).toEqual(["opencode", "--prompt", "Start the task"])
|
|
145
|
-
})
|
|
98
|
+
test("launches OpenCode in the writable checkout with the single-phase prompt", async () => {
|
|
99
|
+
const harness = createHarness()
|
|
100
|
+
|
|
101
|
+
await harness.run({ taskId: "example", opencode: true })
|
|
102
|
+
|
|
103
|
+
expect(harness.events).toEqual([
|
|
104
|
+
"materialize",
|
|
105
|
+
"probe:opencode",
|
|
106
|
+
"launch:opencode",
|
|
107
|
+
])
|
|
108
|
+
expect(harness.launches).toEqual([
|
|
109
|
+
{
|
|
110
|
+
cli: "opencode",
|
|
111
|
+
args: [
|
|
112
|
+
"opencode",
|
|
113
|
+
"--prompt",
|
|
114
|
+
"Start the task. Read /workbase/tasks/example/TASK.md.",
|
|
115
|
+
],
|
|
116
|
+
cwd: "/workbase/tasks/example/code/agency",
|
|
117
|
+
},
|
|
118
|
+
])
|
|
146
119
|
})
|
|
147
120
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
// Create TASK.md
|
|
151
|
-
const taskPath = join(tempDir, "TASK.md")
|
|
152
|
-
writeFileSync(taskPath, "# Test Task\n\nSome task content")
|
|
153
|
-
|
|
154
|
-
let capturedArgs: string[] = []
|
|
155
|
-
let capturedOptions: any = null
|
|
156
|
-
|
|
157
|
-
const restore = mockCliTools({
|
|
158
|
-
onSpawn: (args, options) => {
|
|
159
|
-
capturedArgs = args
|
|
160
|
-
capturedOptions = options
|
|
161
|
-
return {
|
|
162
|
-
exited: Promise.resolve(0),
|
|
163
|
-
exitCode: 0,
|
|
164
|
-
stdout: new ReadableStream(),
|
|
165
|
-
stderr: new ReadableStream(),
|
|
166
|
-
}
|
|
167
|
-
},
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
await runTestEffect(work({ silent: true, _noExec: true }))
|
|
171
|
-
|
|
172
|
-
restore()
|
|
121
|
+
test("includes absolute task and phase paths in a multi-phase prompt", async () => {
|
|
122
|
+
const harness = createHarness({ workspace: multiPhaseWorkspace })
|
|
173
123
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
capturedOptions.cwd === `/private${tempDir}`,
|
|
179
|
-
).toBe(true)
|
|
180
|
-
expect(capturedOptions.stdout).toEqual("inherit")
|
|
181
|
-
expect(capturedOptions.stderr).toEqual("inherit")
|
|
124
|
+
await harness.run({
|
|
125
|
+
taskId: "example",
|
|
126
|
+
phaseId: "implementation",
|
|
127
|
+
opencode: true,
|
|
182
128
|
})
|
|
183
129
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const restore = mockCliTools({
|
|
190
|
-
onSpawn: () => ({
|
|
191
|
-
exited: Promise.resolve(1),
|
|
192
|
-
exitCode: 1,
|
|
193
|
-
stdout: new ReadableStream(),
|
|
194
|
-
stderr: new ReadableStream(),
|
|
195
|
-
}),
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
expect(
|
|
199
|
-
runTestEffect(work({ silent: true, _noExec: true })),
|
|
200
|
-
).rejects.toThrow("opencode exited with code 1")
|
|
201
|
-
|
|
202
|
-
restore()
|
|
203
|
-
})
|
|
130
|
+
expect(harness.launches[0]?.args).toEqual([
|
|
131
|
+
"opencode",
|
|
132
|
+
"--prompt",
|
|
133
|
+
"Start the task. Read /workbase/tasks/example/TASK.md and /workbase/tasks/example/phases/implementation/PHASE.md.",
|
|
134
|
+
])
|
|
204
135
|
})
|
|
205
136
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
// Create TASK.md
|
|
209
|
-
const taskPath = join(tempDir, "TASK.md")
|
|
210
|
-
writeFileSync(taskPath, "# Test Task\n\nSome task content")
|
|
211
|
-
|
|
212
|
-
const restore = mockCliTools()
|
|
213
|
-
|
|
214
|
-
// Capture console.log
|
|
215
|
-
const originalLog = console.log
|
|
216
|
-
let logMessages: string[] = []
|
|
217
|
-
console.log = (msg: string) => {
|
|
218
|
-
logMessages.push(msg)
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
await runTestEffect(work({ silent: false, verbose: true, _noExec: true }))
|
|
222
|
-
|
|
223
|
-
console.log = originalLog
|
|
224
|
-
restore()
|
|
225
|
-
|
|
226
|
-
expect(logMessages.some((msg) => msg.includes("Found TASK.md"))).toBe(
|
|
227
|
-
true,
|
|
228
|
-
)
|
|
229
|
-
expect(logMessages.some((msg) => msg.includes("Running opencode"))).toBe(
|
|
230
|
-
true,
|
|
231
|
-
)
|
|
232
|
-
})
|
|
233
|
-
|
|
234
|
-
test("silent flag suppresses verbose output", async () => {
|
|
235
|
-
// Create TASK.md
|
|
236
|
-
const taskPath = join(tempDir, "TASK.md")
|
|
237
|
-
writeFileSync(taskPath, "# Test Task\n\nSome task content")
|
|
238
|
-
|
|
239
|
-
const restore = mockCliTools()
|
|
240
|
-
|
|
241
|
-
// Capture console.log
|
|
242
|
-
const originalLog = console.log
|
|
243
|
-
let logCalled = false
|
|
244
|
-
console.log = () => {
|
|
245
|
-
logCalled = true
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
await work({ silent: true, verbose: true, _noExec: true })
|
|
137
|
+
test("automatically falls back to Claude", async () => {
|
|
138
|
+
const harness = createHarness({ available: { opencode: false } })
|
|
249
139
|
|
|
250
|
-
|
|
251
|
-
restore()
|
|
140
|
+
await harness.run({ taskId: "example" })
|
|
252
141
|
|
|
253
|
-
|
|
142
|
+
expect(harness.probes).toEqual(["opencode", "claude"])
|
|
143
|
+
expect(harness.launches[0]).toEqual({
|
|
144
|
+
cli: "claude",
|
|
145
|
+
args: ["claude", "Start the task. Read /workbase/tasks/example/TASK.md."],
|
|
146
|
+
cwd: "/workbase/tasks/example/code/agency",
|
|
254
147
|
})
|
|
255
148
|
})
|
|
256
149
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
// Create TASK.md
|
|
260
|
-
const taskPath = join(tempDir, "TASK.md")
|
|
261
|
-
writeFileSync(taskPath, "# Test Task\n\nSome task content")
|
|
150
|
+
test("does not fall back when OpenCode is explicitly required", async () => {
|
|
151
|
+
const harness = createHarness({ available: { opencode: false } })
|
|
262
152
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
exited: Promise.resolve(0),
|
|
270
|
-
exitCode: 0,
|
|
271
|
-
stdout: new ReadableStream(),
|
|
272
|
-
stderr: new ReadableStream(),
|
|
273
|
-
}
|
|
274
|
-
},
|
|
275
|
-
})
|
|
276
|
-
|
|
277
|
-
await runTestEffect(work({ silent: true, _noExec: true, opencode: true }))
|
|
278
|
-
|
|
279
|
-
restore()
|
|
280
|
-
|
|
281
|
-
expect(capturedArgs).toEqual(["opencode", "--prompt", "Start the task"])
|
|
282
|
-
})
|
|
283
|
-
|
|
284
|
-
test("--claude flag forces use of Claude Code", async () => {
|
|
285
|
-
// Create TASK.md
|
|
286
|
-
const taskPath = join(tempDir, "TASK.md")
|
|
287
|
-
writeFileSync(taskPath, "# Test Task\n\nSome task content")
|
|
288
|
-
|
|
289
|
-
let capturedArgs: string[] = []
|
|
290
|
-
|
|
291
|
-
const restore = mockCliTools({
|
|
292
|
-
hasClaude: true,
|
|
293
|
-
onSpawn: (args) => {
|
|
294
|
-
capturedArgs = args
|
|
295
|
-
return {
|
|
296
|
-
exited: Promise.resolve(0),
|
|
297
|
-
exitCode: 0,
|
|
298
|
-
stdout: new ReadableStream(),
|
|
299
|
-
stderr: new ReadableStream(),
|
|
300
|
-
}
|
|
301
|
-
},
|
|
302
|
-
})
|
|
303
|
-
|
|
304
|
-
await runTestEffect(work({ silent: true, _noExec: true, claude: true }))
|
|
305
|
-
|
|
306
|
-
restore()
|
|
307
|
-
|
|
308
|
-
expect(capturedArgs).toEqual(["claude", "Start the task"])
|
|
309
|
-
})
|
|
310
|
-
|
|
311
|
-
test("throws error when both --opencode and --claude flags are used", async () => {
|
|
312
|
-
// Create TASK.md
|
|
313
|
-
const taskPath = join(tempDir, "TASK.md")
|
|
314
|
-
writeFileSync(taskPath, "# Test Task\n\nSome task content")
|
|
315
|
-
|
|
316
|
-
expect(
|
|
317
|
-
runTestEffect(
|
|
318
|
-
work({ silent: true, _noExec: true, opencode: true, claude: true }),
|
|
319
|
-
),
|
|
320
|
-
).rejects.toThrow(
|
|
321
|
-
"Cannot use both --opencode and --claude flags together. Choose one.",
|
|
322
|
-
)
|
|
323
|
-
})
|
|
324
|
-
|
|
325
|
-
test("throws error when --opencode is used but opencode is not installed", async () => {
|
|
326
|
-
// Create TASK.md
|
|
327
|
-
const taskPath = join(tempDir, "TASK.md")
|
|
328
|
-
writeFileSync(taskPath, "# Test Task\n\nSome task content")
|
|
329
|
-
|
|
330
|
-
const restore = mockCliTools({ hasOpencode: false })
|
|
331
|
-
|
|
332
|
-
expect(
|
|
333
|
-
runTestEffect(work({ silent: true, _noExec: true, opencode: true })),
|
|
334
|
-
).rejects.toThrow(
|
|
335
|
-
"opencode CLI tool not found. Please install OpenCode or remove the --opencode flag.",
|
|
336
|
-
)
|
|
337
|
-
|
|
338
|
-
restore()
|
|
339
|
-
})
|
|
340
|
-
|
|
341
|
-
test("throws error when --claude is used but claude is not installed", async () => {
|
|
342
|
-
// Create TASK.md
|
|
343
|
-
const taskPath = join(tempDir, "TASK.md")
|
|
344
|
-
writeFileSync(taskPath, "# Test Task\n\nSome task content")
|
|
153
|
+
await expect(
|
|
154
|
+
harness.run({ taskId: "example", opencode: true }),
|
|
155
|
+
).rejects.toThrow("opencode CLI tool not found")
|
|
156
|
+
expect(harness.probes).toEqual(["opencode"])
|
|
157
|
+
expect(harness.launches).toEqual([])
|
|
158
|
+
})
|
|
345
159
|
|
|
346
|
-
|
|
160
|
+
test("launches explicitly requested Claude", async () => {
|
|
161
|
+
const harness = createHarness()
|
|
347
162
|
|
|
348
|
-
|
|
349
|
-
runTestEffect(work({ silent: true, _noExec: true, claude: true })),
|
|
350
|
-
).rejects.toThrow(
|
|
351
|
-
"claude CLI tool not found. Please install Claude Code or remove the --claude flag.",
|
|
352
|
-
)
|
|
163
|
+
await harness.run({ taskId: "example", claude: true })
|
|
353
164
|
|
|
354
|
-
|
|
165
|
+
expect(harness.probes).toEqual(["claude"])
|
|
166
|
+
expect(harness.launches[0]).toEqual({
|
|
167
|
+
cli: "claude",
|
|
168
|
+
args: ["claude", "Start the task. Read /workbase/tasks/example/TASK.md."],
|
|
169
|
+
cwd: "/workbase/tasks/example/code/agency",
|
|
355
170
|
})
|
|
356
171
|
})
|
|
357
172
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const taskPath = join(tempDir, "TASK.md")
|
|
362
|
-
writeFileSync(taskPath, "# Test Task\n\nSome task content")
|
|
363
|
-
|
|
364
|
-
let capturedArgs: string[] = []
|
|
365
|
-
|
|
366
|
-
const restore = mockCliTools({
|
|
367
|
-
onSpawn: (args) => {
|
|
368
|
-
capturedArgs = args
|
|
369
|
-
return {
|
|
370
|
-
exited: Promise.resolve(0),
|
|
371
|
-
exitCode: 0,
|
|
372
|
-
stdout: new ReadableStream(),
|
|
373
|
-
stderr: new ReadableStream(),
|
|
374
|
-
}
|
|
375
|
-
},
|
|
376
|
-
})
|
|
377
|
-
|
|
378
|
-
await runTestEffect(
|
|
379
|
-
work({
|
|
380
|
-
silent: true,
|
|
381
|
-
_noExec: true,
|
|
382
|
-
extraArgs: ["--model", "claude-sonnet-4-20250514"],
|
|
383
|
-
}),
|
|
384
|
-
)
|
|
385
|
-
|
|
386
|
-
restore()
|
|
387
|
-
|
|
388
|
-
expect(capturedArgs).toEqual([
|
|
389
|
-
"opencode",
|
|
390
|
-
"--prompt",
|
|
391
|
-
"Start the task",
|
|
392
|
-
"--model",
|
|
393
|
-
"claude-sonnet-4-20250514",
|
|
394
|
-
])
|
|
173
|
+
test("fails when neither agent tool is available", async () => {
|
|
174
|
+
const harness = createHarness({
|
|
175
|
+
available: { opencode: false, claude: false },
|
|
395
176
|
})
|
|
396
177
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
const restore = mockCliTools({
|
|
405
|
-
hasClaude: true,
|
|
406
|
-
onSpawn: (args) => {
|
|
407
|
-
capturedArgs = args
|
|
408
|
-
return {
|
|
409
|
-
exited: Promise.resolve(0),
|
|
410
|
-
exitCode: 0,
|
|
411
|
-
stdout: new ReadableStream(),
|
|
412
|
-
stderr: new ReadableStream(),
|
|
413
|
-
}
|
|
414
|
-
},
|
|
415
|
-
})
|
|
416
|
-
|
|
417
|
-
await runTestEffect(
|
|
418
|
-
work({
|
|
419
|
-
silent: true,
|
|
420
|
-
_noExec: true,
|
|
421
|
-
claude: true,
|
|
422
|
-
extraArgs: ["--arbitrary", "switches"],
|
|
423
|
-
}),
|
|
424
|
-
)
|
|
425
|
-
|
|
426
|
-
restore()
|
|
178
|
+
await expect(harness.run({ taskId: "example" })).rejects.toThrow(
|
|
179
|
+
"claude CLI tool not found",
|
|
180
|
+
)
|
|
181
|
+
expect(harness.probes).toEqual(["opencode", "claude"])
|
|
182
|
+
expect(harness.launches).toEqual([])
|
|
183
|
+
})
|
|
427
184
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
"--arbitrary",
|
|
432
|
-
"switches",
|
|
433
|
-
])
|
|
185
|
+
test("does not probe or launch when materialization fails", async () => {
|
|
186
|
+
const harness = createHarness({
|
|
187
|
+
materializeError: new Error("materialization failed"),
|
|
434
188
|
})
|
|
435
189
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
let capturedArgs: string[] = []
|
|
442
|
-
|
|
443
|
-
const restore = mockCliTools({
|
|
444
|
-
onSpawn: (args) => {
|
|
445
|
-
capturedArgs = args
|
|
446
|
-
return {
|
|
447
|
-
exited: Promise.resolve(0),
|
|
448
|
-
exitCode: 0,
|
|
449
|
-
stdout: new ReadableStream(),
|
|
450
|
-
stderr: new ReadableStream(),
|
|
451
|
-
}
|
|
452
|
-
},
|
|
453
|
-
})
|
|
454
|
-
|
|
455
|
-
await runTestEffect(work({ silent: true, _noExec: true }))
|
|
456
|
-
|
|
457
|
-
restore()
|
|
190
|
+
await expect(harness.run({ taskId: "example" })).rejects.toThrow(
|
|
191
|
+
"materialization failed",
|
|
192
|
+
)
|
|
193
|
+
expect(harness.events).toEqual(["materialize"])
|
|
194
|
+
})
|
|
458
195
|
|
|
459
|
-
|
|
460
|
-
|
|
196
|
+
test("respects silent and verbose logging options", async () => {
|
|
197
|
+
const verboseHarness = createHarness()
|
|
198
|
+
const verboseLogs = await captureLogs(() =>
|
|
199
|
+
verboseHarness.run({ taskId: "example", verbose: true }),
|
|
200
|
+
)
|
|
201
|
+
expect(verboseLogs).toEqual([
|
|
202
|
+
"Launching opencode in /workbase/tasks/example/code/agency",
|
|
203
|
+
])
|
|
204
|
+
|
|
205
|
+
const silentHarness = createHarness()
|
|
206
|
+
const silentLogs = await captureLogs(() =>
|
|
207
|
+
silentHarness.run({
|
|
208
|
+
taskId: "example",
|
|
209
|
+
verbose: true,
|
|
210
|
+
silent: true,
|
|
211
|
+
}),
|
|
212
|
+
)
|
|
213
|
+
expect(silentLogs).toEqual([])
|
|
461
214
|
})
|
|
462
215
|
})
|