@markjaquith/agency 2.4.0 → 2.5.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 +45 -16
- package/cli.ts +21 -0
- package/package.json +1 -1
- package/skills/agency/SKILL.md +31 -4
- package/src/cli.test.ts +1 -0
- package/src/commands/archive.test.ts +69 -0
- package/src/commands/archive.ts +70 -0
- package/src/commands/init.test.ts +3 -0
- package/src/commands/phase.ts +23 -1
- package/src/commands/task-phase.test.ts +46 -1
- package/src/commands/task.test.ts +94 -0
- package/src/commands/task.ts +160 -17
- package/src/commands/work.test.ts +52 -33
- package/src/commands/work.ts +20 -8
- package/src/services/ArchiveService.test.ts +334 -0
- package/src/services/ArchiveService.ts +246 -0
- package/src/services/PhaseService.ts +28 -0
- package/src/services/TaskPhaseService.test.ts +79 -0
- package/src/services/TaskService.ts +31 -1
- package/src/services/WorkbaseService.test.ts +66 -0
- package/src/services/WorkbaseService.ts +32 -2
- package/src/services/WorktreeService.test.ts +150 -1
- package/src/services/WorktreeService.ts +123 -1
- package/src/test-utils.ts +2 -0
- package/src/utils/progress.test.ts +37 -0
- package/src/utils/progress.ts +36 -0
- package/src/workbase/AGENTS.md +3 -0
- package/src/workbase/opencode-file.ts +37 -0
- package/src/workbase/schemas.test.ts +50 -0
- package/src/workbase/schemas.ts +6 -2
- package/src/workbase/work-target.test.ts +9 -9
- package/src/workbase/work-target.ts +36 -6
package/src/commands/task.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { Effect } from "effect"
|
|
2
|
+
import { createInterface } from "node:readline/promises"
|
|
2
3
|
import type { BaseCommandOptions } from "../utils/command"
|
|
3
4
|
import { TaskService } from "../services/TaskService"
|
|
5
|
+
import { EpicService } from "../services/EpicService"
|
|
6
|
+
import { RepositoryService } from "../services/RepositoryService"
|
|
4
7
|
import { createLoggers } from "../utils/effect"
|
|
5
8
|
import { parseRepositoryReferences } from "../workbase/repository-reference"
|
|
6
9
|
|
|
@@ -18,31 +21,151 @@ interface TaskOptions extends BaseCommandOptions {
|
|
|
18
21
|
readonly json?: boolean
|
|
19
22
|
}
|
|
20
23
|
|
|
21
|
-
export
|
|
24
|
+
export interface TaskInteraction {
|
|
25
|
+
readonly text: (prompt: string) => Effect.Effect<string, Error>
|
|
26
|
+
readonly select: (
|
|
27
|
+
prompt: string,
|
|
28
|
+
choices: readonly string[],
|
|
29
|
+
) => Effect.Effect<string | null, Error>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const defaultInteraction: TaskInteraction = {
|
|
33
|
+
text: (prompt) =>
|
|
34
|
+
Effect.tryPromise({
|
|
35
|
+
try: async () => {
|
|
36
|
+
const input = createInterface({
|
|
37
|
+
input: process.stdin,
|
|
38
|
+
output: process.stderr,
|
|
39
|
+
})
|
|
40
|
+
try {
|
|
41
|
+
return await input.question(prompt)
|
|
42
|
+
} finally {
|
|
43
|
+
input.close()
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
catch: (cause) => new Error("Failed to read task input", { cause }),
|
|
47
|
+
}),
|
|
48
|
+
select: (prompt, choices) =>
|
|
49
|
+
Effect.tryPromise({
|
|
50
|
+
try: async () => {
|
|
51
|
+
const process = Bun.spawn(
|
|
52
|
+
["fzf", `--prompt=${prompt}> `, "--height=~40%", "--reverse"],
|
|
53
|
+
{
|
|
54
|
+
stdin: new Blob([choices.join("\n")]),
|
|
55
|
+
stdout: "pipe",
|
|
56
|
+
stderr: "inherit",
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
const [exitCode, output] = await Promise.all([
|
|
60
|
+
process.exited,
|
|
61
|
+
new Response(process.stdout).text(),
|
|
62
|
+
])
|
|
63
|
+
if (exitCode === 1 || exitCode === 130) return null
|
|
64
|
+
if (exitCode !== 0) throw new Error(`fzf exited with code ${exitCode}`)
|
|
65
|
+
return output.trim() || null
|
|
66
|
+
},
|
|
67
|
+
catch: (cause) =>
|
|
68
|
+
new Error("Failed to select task input with fzf", { cause }),
|
|
69
|
+
}),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const task = (
|
|
73
|
+
options: TaskOptions,
|
|
74
|
+
interaction: TaskInteraction = defaultInteraction,
|
|
75
|
+
) =>
|
|
22
76
|
Effect.gen(function* () {
|
|
23
77
|
const tasks = yield* TaskService
|
|
78
|
+
const epics = yield* EpicService
|
|
79
|
+
const repositories = yield* RepositoryService
|
|
24
80
|
const { log } = createLoggers(options)
|
|
25
81
|
const cwd = options.cwd ?? process.cwd()
|
|
26
82
|
|
|
27
83
|
switch (options.subcommand) {
|
|
84
|
+
case "new":
|
|
28
85
|
case "create": {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
86
|
+
const interactive = options.subcommand === "new" || !options.args[0]
|
|
87
|
+
const id =
|
|
88
|
+
options.args[0] ??
|
|
89
|
+
(interactive
|
|
90
|
+
? (yield* interaction.text("Task ID: ")).trim()
|
|
91
|
+
: undefined)
|
|
92
|
+
if (!id) {
|
|
93
|
+
return yield* Effect.fail(new Error("Task ID is required"))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let ticketUrl = options.ticketUrl?.trim() || null
|
|
97
|
+
let description = options.description?.trim() || undefined
|
|
98
|
+
let epic = options.epic
|
|
99
|
+
let multiPhase = options.multiPhase ?? false
|
|
100
|
+
let repo = options.repo
|
|
101
|
+
|
|
102
|
+
if (interactive) {
|
|
103
|
+
if (options.ticketUrl === undefined) {
|
|
104
|
+
ticketUrl =
|
|
105
|
+
(yield* interaction.text("Ticket URL (optional): ")).trim() ||
|
|
106
|
+
null
|
|
107
|
+
}
|
|
108
|
+
if (options.description === undefined) {
|
|
109
|
+
description =
|
|
110
|
+
(yield* interaction.text("Description (optional): ")).trim() ||
|
|
111
|
+
undefined
|
|
112
|
+
}
|
|
113
|
+
if (options.epic === undefined) {
|
|
114
|
+
const epicRecords = yield* epics.list(cwd)
|
|
115
|
+
if (epicRecords.length > 0) {
|
|
116
|
+
const none = "(none)"
|
|
117
|
+
const selected = yield* interaction.select("Parent epic", [
|
|
118
|
+
none,
|
|
119
|
+
...epicRecords.map((record) => record.id),
|
|
120
|
+
])
|
|
121
|
+
if (selected === null) {
|
|
122
|
+
return yield* Effect.fail(new Error("Task creation cancelled"))
|
|
123
|
+
}
|
|
124
|
+
epic = selected === none ? undefined : selected
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (options.multiPhase === undefined) {
|
|
128
|
+
const selected = yield* interaction.select("Task type", [
|
|
129
|
+
"single-phase",
|
|
130
|
+
"multi-phase",
|
|
131
|
+
])
|
|
132
|
+
if (selected === null) {
|
|
133
|
+
return yield* Effect.fail(new Error("Task creation cancelled"))
|
|
134
|
+
}
|
|
135
|
+
multiPhase = selected === "multi-phase"
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!multiPhase && !repo) {
|
|
140
|
+
const records = yield* repositories.list(cwd)
|
|
141
|
+
if (records.length === 0) {
|
|
142
|
+
return yield* Effect.fail(
|
|
143
|
+
new Error(
|
|
144
|
+
"No repositories found; add or link a repository first",
|
|
145
|
+
),
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
const selected = yield* interaction.select(
|
|
149
|
+
"Writable repository",
|
|
150
|
+
records.map((record) => record.alias),
|
|
33
151
|
)
|
|
152
|
+
if (!selected) {
|
|
153
|
+
return yield* Effect.fail(new Error("Task creation cancelled"))
|
|
154
|
+
}
|
|
155
|
+
repo = selected
|
|
34
156
|
}
|
|
157
|
+
|
|
35
158
|
const record = yield* tasks.create(
|
|
36
159
|
{
|
|
37
160
|
id,
|
|
38
|
-
ticketUrl
|
|
39
|
-
description
|
|
40
|
-
epic
|
|
41
|
-
multiPhase
|
|
42
|
-
repo
|
|
161
|
+
ticketUrl,
|
|
162
|
+
description,
|
|
163
|
+
epic,
|
|
164
|
+
multiPhase,
|
|
165
|
+
repo,
|
|
43
166
|
repos: parseRepositoryReferences(options.references),
|
|
44
|
-
branch: options.branch,
|
|
45
|
-
base: options.base,
|
|
167
|
+
branch: multiPhase ? undefined : (options.branch ?? `task/${id}`),
|
|
168
|
+
base: multiPhase ? undefined : (options.base ?? "main"),
|
|
46
169
|
},
|
|
47
170
|
cwd,
|
|
48
171
|
)
|
|
@@ -81,9 +204,27 @@ export const task = (options: TaskOptions) =>
|
|
|
81
204
|
)
|
|
82
205
|
return
|
|
83
206
|
}
|
|
207
|
+
case "status": {
|
|
208
|
+
const [id, status] = options.args
|
|
209
|
+
if (!id || !status) {
|
|
210
|
+
return yield* Effect.fail(
|
|
211
|
+
new Error("Usage: agency task status <id> <status>"),
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
const record = yield* tasks.setStatus(id, status, cwd)
|
|
215
|
+
const { content: _, ...output } = record
|
|
216
|
+
log(
|
|
217
|
+
options.json
|
|
218
|
+
? JSON.stringify(output, null, 2)
|
|
219
|
+
: `Marked task '${id}' as ${record.data.status}`,
|
|
220
|
+
)
|
|
221
|
+
return
|
|
222
|
+
}
|
|
84
223
|
default:
|
|
85
224
|
return yield* Effect.fail(
|
|
86
|
-
new Error(
|
|
225
|
+
new Error(
|
|
226
|
+
"Subcommand is required. Available: new, create, list, show, status",
|
|
227
|
+
),
|
|
87
228
|
)
|
|
88
229
|
}
|
|
89
230
|
})
|
|
@@ -92,19 +233,21 @@ export const help = `
|
|
|
92
233
|
Usage: agency task <subcommand>
|
|
93
234
|
|
|
94
235
|
Subcommands:
|
|
95
|
-
|
|
236
|
+
new [id] Create a task with guided input
|
|
237
|
+
create <id> Create a task; omitted metadata uses defaults
|
|
96
238
|
list List tasks
|
|
97
239
|
show <id> Show a task
|
|
240
|
+
status <id> <status> Set open, working, done, or dropped
|
|
98
241
|
|
|
99
242
|
Create options:
|
|
100
|
-
--ticket-url <url> External ticket URL
|
|
243
|
+
--ticket-url <url> External ticket URL (optional)
|
|
101
244
|
--description <text> Short description of the task
|
|
102
245
|
--epic <id> Parent epic
|
|
103
246
|
--repo <alias> Writable repository
|
|
104
247
|
--reference <alias>:<ref>
|
|
105
248
|
Read-only repository reference; repeatable
|
|
106
|
-
--branch <name> Working branch
|
|
107
|
-
--base <name> Base branch
|
|
249
|
+
--branch <name> Working branch (default: task/<id>)
|
|
250
|
+
--base <name> Base branch (default: main)
|
|
108
251
|
--multi-phase Create a task container for phases
|
|
109
252
|
|
|
110
253
|
Options:
|
|
@@ -9,6 +9,7 @@ import { WorktreeService } from "../services/WorktreeService"
|
|
|
9
9
|
import { captureLogs } from "../test-utils"
|
|
10
10
|
import { work } from "./work"
|
|
11
11
|
import type { PickWorkTarget } from "../workbase/work-target"
|
|
12
|
+
import type { Progress } from "../utils/progress"
|
|
12
13
|
|
|
13
14
|
type ExecutionWorkspace = Effect.Effect.Success<
|
|
14
15
|
ReturnType<WorktreeService["materialize"]>
|
|
@@ -47,6 +48,8 @@ interface HarnessOptions {
|
|
|
47
48
|
const createHarness = (options: HarnessOptions = {}) => {
|
|
48
49
|
const events: string[] = []
|
|
49
50
|
const probes: string[] = []
|
|
51
|
+
const statusUpdates: string[] = []
|
|
52
|
+
const progressUpdates: string[] = []
|
|
50
53
|
const launches: Array<{
|
|
51
54
|
cli: string
|
|
52
55
|
args: readonly string[]
|
|
@@ -91,6 +94,10 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
91
94
|
: { repo: "agency", branch: `task/${id}`, base: "main" },
|
|
92
95
|
}),
|
|
93
96
|
list: () => Effect.succeed(options.taskRecords ?? []),
|
|
97
|
+
setStatus: (id: string, status: string) => {
|
|
98
|
+
statusUpdates.push(`task:${id}:${status}`)
|
|
99
|
+
return Effect.void
|
|
100
|
+
},
|
|
94
101
|
}
|
|
95
102
|
const phases = {
|
|
96
103
|
show: (taskId: string, id: string) =>
|
|
@@ -101,6 +108,10 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
101
108
|
data: { repo: "agency", branch: `task/${id}`, base: "main" },
|
|
102
109
|
}),
|
|
103
110
|
list: () => Effect.succeed(options.phaseRecords ?? []),
|
|
111
|
+
setStatus: (taskId: string, id: string, status: string) => {
|
|
112
|
+
statusUpdates.push(`phase:${taskId}:${id}:${status}`)
|
|
113
|
+
return Effect.void
|
|
114
|
+
},
|
|
104
115
|
}
|
|
105
116
|
const fs = {
|
|
106
117
|
runCommand: (args: readonly string[]) => {
|
|
@@ -119,12 +130,17 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
119
130
|
launches.push({ cli, args, cwd })
|
|
120
131
|
}
|
|
121
132
|
const defaultPick: PickWorkTarget = () => Effect.succeed(null)
|
|
133
|
+
const progress: Progress = {
|
|
134
|
+
start: (message) => progressUpdates.push(`start:${message}`),
|
|
135
|
+
succeed: (message) => progressUpdates.push(`succeed:${message}`),
|
|
136
|
+
fail: (message) => progressUpdates.push(`fail:${message}`),
|
|
137
|
+
}
|
|
122
138
|
const run = (
|
|
123
139
|
commandOptions: Parameters<typeof work>[0],
|
|
124
140
|
pick: PickWorkTarget = defaultPick,
|
|
125
141
|
) =>
|
|
126
142
|
Effect.runPromise(
|
|
127
|
-
work(commandOptions, launch, pick).pipe(
|
|
143
|
+
work(commandOptions, launch, pick, progress).pipe(
|
|
128
144
|
Effect.provideService(WorktreeService, worktrees as never),
|
|
129
145
|
Effect.provideService(FileSystemService, fs as never),
|
|
130
146
|
Effect.provideService(WorkbaseService, workbase as never),
|
|
@@ -134,7 +150,15 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
134
150
|
) as Effect.Effect<void, unknown, never>,
|
|
135
151
|
)
|
|
136
152
|
|
|
137
|
-
return {
|
|
153
|
+
return {
|
|
154
|
+
events,
|
|
155
|
+
probes,
|
|
156
|
+
launches,
|
|
157
|
+
materializeOptions,
|
|
158
|
+
statusUpdates,
|
|
159
|
+
progressUpdates,
|
|
160
|
+
run,
|
|
161
|
+
}
|
|
138
162
|
}
|
|
139
163
|
|
|
140
164
|
describe("work command", () => {
|
|
@@ -146,11 +170,7 @@ describe("work command", () => {
|
|
|
146
170
|
expect(harness.events).toEqual(["probe:opencode", "launch:opencode"])
|
|
147
171
|
expect(harness.launches[0]).toEqual({
|
|
148
172
|
cli: "opencode",
|
|
149
|
-
args: [
|
|
150
|
-
"opencode",
|
|
151
|
-
"--prompt",
|
|
152
|
-
"Work on the epic. Read /workbase/epics/delivery/EPIC.md.",
|
|
153
|
-
],
|
|
173
|
+
args: ["opencode", "--continue"],
|
|
154
174
|
cwd: "/workbase/epics/delivery",
|
|
155
175
|
})
|
|
156
176
|
})
|
|
@@ -163,11 +183,7 @@ describe("work command", () => {
|
|
|
163
183
|
expect(harness.events).toEqual(["probe:opencode", "launch:opencode"])
|
|
164
184
|
expect(harness.launches[0]).toEqual({
|
|
165
185
|
cli: "opencode",
|
|
166
|
-
args: [
|
|
167
|
-
"opencode",
|
|
168
|
-
"--prompt",
|
|
169
|
-
"Work on the task. Read /workbase/tasks/delivery/TASK.md.",
|
|
170
|
-
],
|
|
186
|
+
args: ["opencode", "--continue"],
|
|
171
187
|
cwd: "/workbase/tasks/delivery",
|
|
172
188
|
})
|
|
173
189
|
})
|
|
@@ -185,9 +201,10 @@ describe("work command", () => {
|
|
|
185
201
|
"probe:opencode",
|
|
186
202
|
"launch:opencode",
|
|
187
203
|
])
|
|
188
|
-
expect(harness.launches[0]?.args).
|
|
189
|
-
|
|
190
|
-
|
|
204
|
+
expect(harness.launches[0]?.args).toEqual(["opencode", "--continue"])
|
|
205
|
+
expect(harness.statusUpdates).toEqual([
|
|
206
|
+
"phase:example:implementation:working",
|
|
207
|
+
])
|
|
191
208
|
})
|
|
192
209
|
|
|
193
210
|
test("infers a single-phase task from a nested checkout directory", async () => {
|
|
@@ -199,7 +216,7 @@ describe("work command", () => {
|
|
|
199
216
|
})
|
|
200
217
|
|
|
201
218
|
expect(harness.events[0]).toBe("materialize")
|
|
202
|
-
expect(harness.launches[0]?.cwd).toBe("/workbase/tasks/example
|
|
219
|
+
expect(harness.launches[0]?.cwd).toBe("/workbase/tasks/example")
|
|
203
220
|
})
|
|
204
221
|
|
|
205
222
|
test("selects a target with fzf outside an entity directory", async () => {
|
|
@@ -234,7 +251,7 @@ describe("work command", () => {
|
|
|
234
251
|
"launch:opencode",
|
|
235
252
|
])
|
|
236
253
|
expect(harness.launches[0]?.cwd).toBe(
|
|
237
|
-
"/workbase/tasks/
|
|
254
|
+
"/workbase/tasks/delivery/phases/build",
|
|
238
255
|
)
|
|
239
256
|
})
|
|
240
257
|
|
|
@@ -278,7 +295,7 @@ describe("work command", () => {
|
|
|
278
295
|
expect(harness.events).toEqual([])
|
|
279
296
|
})
|
|
280
297
|
|
|
281
|
-
test("launches OpenCode in the
|
|
298
|
+
test("launches OpenCode in the task directory and continues its session", async () => {
|
|
282
299
|
const harness = createHarness()
|
|
283
300
|
|
|
284
301
|
await harness.run({ taskId: "example", opencode: true })
|
|
@@ -291,17 +308,18 @@ describe("work command", () => {
|
|
|
291
308
|
expect(harness.launches).toEqual([
|
|
292
309
|
{
|
|
293
310
|
cli: "opencode",
|
|
294
|
-
args: [
|
|
295
|
-
|
|
296
|
-
"--prompt",
|
|
297
|
-
"Start the task. Read /workbase/tasks/example/TASK.md.",
|
|
298
|
-
],
|
|
299
|
-
cwd: "/workbase/tasks/example/code/agency",
|
|
311
|
+
args: ["opencode", "--continue"],
|
|
312
|
+
cwd: "/workbase/tasks/example",
|
|
300
313
|
},
|
|
301
314
|
])
|
|
315
|
+
expect(harness.statusUpdates).toEqual(["task:example:working"])
|
|
316
|
+
expect(harness.progressUpdates).toEqual([
|
|
317
|
+
"start:Preparing workspace...",
|
|
318
|
+
"succeed:Workspace ready",
|
|
319
|
+
])
|
|
302
320
|
})
|
|
303
321
|
|
|
304
|
-
test("
|
|
322
|
+
test("continues OpenCode for a multi-phase task", async () => {
|
|
305
323
|
const harness = createHarness({ workspace: multiPhaseWorkspace })
|
|
306
324
|
|
|
307
325
|
await harness.run({
|
|
@@ -310,11 +328,7 @@ describe("work command", () => {
|
|
|
310
328
|
opencode: true,
|
|
311
329
|
})
|
|
312
330
|
|
|
313
|
-
expect(harness.launches[0]?.args).toEqual([
|
|
314
|
-
"opencode",
|
|
315
|
-
"--prompt",
|
|
316
|
-
"Start the task. Read /workbase/tasks/example/TASK.md and /workbase/tasks/example/phases/implementation/PHASE.md.",
|
|
317
|
-
])
|
|
331
|
+
expect(harness.launches[0]?.args).toEqual(["opencode", "--continue"])
|
|
318
332
|
})
|
|
319
333
|
|
|
320
334
|
test("automatically falls back to Claude", async () => {
|
|
@@ -326,7 +340,7 @@ describe("work command", () => {
|
|
|
326
340
|
expect(harness.launches[0]).toEqual({
|
|
327
341
|
cli: "claude",
|
|
328
342
|
args: ["claude", "Start the task. Read /workbase/tasks/example/TASK.md."],
|
|
329
|
-
cwd: "/workbase/tasks/example
|
|
343
|
+
cwd: "/workbase/tasks/example",
|
|
330
344
|
})
|
|
331
345
|
})
|
|
332
346
|
|
|
@@ -338,6 +352,7 @@ describe("work command", () => {
|
|
|
338
352
|
).rejects.toThrow("opencode CLI tool not found")
|
|
339
353
|
expect(harness.probes).toEqual(["opencode"])
|
|
340
354
|
expect(harness.launches).toEqual([])
|
|
355
|
+
expect(harness.statusUpdates).toEqual([])
|
|
341
356
|
})
|
|
342
357
|
|
|
343
358
|
test("launches explicitly requested Claude", async () => {
|
|
@@ -349,7 +364,7 @@ describe("work command", () => {
|
|
|
349
364
|
expect(harness.launches[0]).toEqual({
|
|
350
365
|
cli: "claude",
|
|
351
366
|
args: ["claude", "Start the task. Read /workbase/tasks/example/TASK.md."],
|
|
352
|
-
cwd: "/workbase/tasks/example
|
|
367
|
+
cwd: "/workbase/tasks/example",
|
|
353
368
|
})
|
|
354
369
|
})
|
|
355
370
|
|
|
@@ -374,6 +389,10 @@ describe("work command", () => {
|
|
|
374
389
|
"materialization failed",
|
|
375
390
|
)
|
|
376
391
|
expect(harness.events).toEqual(["materialize"])
|
|
392
|
+
expect(harness.progressUpdates).toEqual([
|
|
393
|
+
"start:Preparing workspace...",
|
|
394
|
+
"fail:Workspace preparation failed",
|
|
395
|
+
])
|
|
377
396
|
})
|
|
378
397
|
|
|
379
398
|
test("respects silent and verbose logging options", async () => {
|
|
@@ -382,7 +401,7 @@ describe("work command", () => {
|
|
|
382
401
|
verboseHarness.run({ taskId: "example", verbose: true }),
|
|
383
402
|
)
|
|
384
403
|
expect(verboseLogs).toEqual([
|
|
385
|
-
"Launching opencode in /workbase/tasks/example
|
|
404
|
+
"Launching opencode in /workbase/tasks/example",
|
|
386
405
|
])
|
|
387
406
|
expect(verboseHarness.materializeOptions[0]?.verbose).toBe(true)
|
|
388
407
|
|
package/src/commands/work.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { TaskService } from "../services/TaskService"
|
|
|
9
9
|
import { PhaseService } from "../services/PhaseService"
|
|
10
10
|
import { createLoggers } from "../utils/effect"
|
|
11
11
|
import { execvp } from "../utils/exec"
|
|
12
|
+
import { createProgress, type Progress } from "../utils/progress"
|
|
12
13
|
import {
|
|
13
14
|
buildWorkTargetChoices,
|
|
14
15
|
pickWorkTarget,
|
|
@@ -35,6 +36,7 @@ export const work = (
|
|
|
35
36
|
options: WorkOptions = {},
|
|
36
37
|
launch: LaunchAgent = launchAgent,
|
|
37
38
|
pick: PickWorkTarget = pickWorkTarget,
|
|
39
|
+
progress: Progress = createProgress(options),
|
|
38
40
|
) =>
|
|
39
41
|
Effect.gen(function* () {
|
|
40
42
|
if (options.opencode && options.claude) {
|
|
@@ -155,16 +157,21 @@ export const work = (
|
|
|
155
157
|
} else {
|
|
156
158
|
const taskId = target.taskId
|
|
157
159
|
const phaseId = target.kind === "phase" ? target.phaseId : undefined
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
phaseId,
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
160
|
+
progress.start("Preparing workspace...")
|
|
161
|
+
const workspace = yield* worktrees
|
|
162
|
+
.materialize(taskId, phaseId, root, options)
|
|
163
|
+
.pipe(
|
|
164
|
+
Effect.tap(() =>
|
|
165
|
+
Effect.sync(() => progress.succeed("Workspace ready")),
|
|
166
|
+
),
|
|
167
|
+
Effect.tapError(() =>
|
|
168
|
+
Effect.sync(() => progress.fail("Workspace preparation failed")),
|
|
169
|
+
),
|
|
170
|
+
)
|
|
164
171
|
prompt = workspace.phasePath
|
|
165
172
|
? `Start the task. Read ${workspace.taskPath} and ${workspace.phasePath}.`
|
|
166
173
|
: `Start the task. Read ${workspace.taskPath}.`
|
|
167
|
-
launchPath =
|
|
174
|
+
launchPath = dirname(target.path)
|
|
168
175
|
}
|
|
169
176
|
|
|
170
177
|
const requested = options.claude ? "claude" : "opencode"
|
|
@@ -179,9 +186,14 @@ export const work = (
|
|
|
179
186
|
if (available.exitCode !== 0) {
|
|
180
187
|
return yield* Effect.fail(new Error(`${cli} CLI tool not found`))
|
|
181
188
|
}
|
|
189
|
+
if (target.kind === "phase") {
|
|
190
|
+
yield* phases.setStatus(target.taskId, target.phaseId, "working", root)
|
|
191
|
+
} else if (target.kind === "task" && !target.multiPhase) {
|
|
192
|
+
yield* tasks.setStatus(target.taskId, "working", root)
|
|
193
|
+
}
|
|
182
194
|
|
|
183
195
|
verboseLog(`Launching ${cli} in ${launchPath}`)
|
|
184
|
-
const args = cli === "opencode" ? ["--
|
|
196
|
+
const args = cli === "opencode" ? ["--continue"] : [prompt]
|
|
185
197
|
launch(cli, [cli, ...args], launchPath)
|
|
186
198
|
})
|
|
187
199
|
|