@markjaquith/agency 2.18.0 → 2.20.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 +55 -9
- package/cli.ts +22 -0
- package/package.json +1 -1
- package/skills/agency/SKILL.md +4 -1
- package/src/cli-parser.test.ts +53 -2
- package/src/cli-parser.ts +78 -14
- package/src/cli.test.ts +11 -1
- package/src/commands/epic.test.ts +22 -0
- package/src/commands/epic.ts +38 -2
- package/src/commands/phase.ts +52 -2
- package/src/commands/status.test.ts +44 -0
- package/src/commands/status.ts +51 -2
- package/src/commands/task-phase.test.ts +20 -0
- package/src/commands/task.ts +49 -2
- package/src/commands/work.test.ts +105 -16
- package/src/commands/work.ts +99 -28
- package/src/services/WorkbaseService.test.ts +19 -0
- package/src/services/WorkbaseService.ts +12 -0
- package/src/utils/table.ts +19 -0
- package/src/work-view.test.ts +151 -0
- package/src/work-view.ts +253 -0
- package/src/workbase/runner-command.test.ts +79 -0
- package/src/workbase/runner-command.ts +118 -0
- package/src/workbase/schemas.test.ts +26 -0
- package/src/workbase/schemas.ts +15 -0
package/src/commands/phase.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { Effect } from "effect"
|
|
|
2
2
|
import type { BaseCommandOptions } from "../utils/command"
|
|
3
3
|
import { PhaseService } from "../services/PhaseService"
|
|
4
4
|
import { createLoggers } from "../utils/effect"
|
|
5
|
+
import { formatTable } from "../utils/table"
|
|
6
|
+
import { getWorkViews } from "../work-view"
|
|
5
7
|
import { parseRepositoryReferences } from "../workbase/repository-reference"
|
|
6
8
|
|
|
7
9
|
interface PhaseOptions extends BaseCommandOptions {
|
|
@@ -15,6 +17,11 @@ interface PhaseOptions extends BaseCommandOptions {
|
|
|
15
17
|
readonly dependsOn?: readonly string[]
|
|
16
18
|
readonly firstPhase?: string
|
|
17
19
|
readonly json?: boolean
|
|
20
|
+
readonly statuses?: readonly string[]
|
|
21
|
+
readonly repositories?: readonly string[]
|
|
22
|
+
readonly ready?: boolean
|
|
23
|
+
readonly blocked?: boolean
|
|
24
|
+
readonly pr?: boolean
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
export const phase = (options: PhaseOptions) =>
|
|
@@ -64,15 +71,53 @@ export const phase = (options: PhaseOptions) =>
|
|
|
64
71
|
case "list": {
|
|
65
72
|
if (!taskId) return yield* Effect.fail(new Error("Task ID is required"))
|
|
66
73
|
const records = yield* phases.list(taskId, cwd)
|
|
74
|
+
const { phaseRows } = yield* getWorkViews({
|
|
75
|
+
cwd,
|
|
76
|
+
statuses: options.statuses,
|
|
77
|
+
repositories: options.repositories,
|
|
78
|
+
ready: options.ready,
|
|
79
|
+
blocked: options.blocked,
|
|
80
|
+
pr: options.pr,
|
|
81
|
+
})
|
|
82
|
+
const rows = phaseRows.filter((row) => row.parent === taskId)
|
|
83
|
+
const ordered = rows.flatMap((row) => {
|
|
84
|
+
const record = records.find((item) => item.id === row.id)
|
|
85
|
+
return record ? [record] : []
|
|
86
|
+
})
|
|
67
87
|
if (options.json) {
|
|
68
88
|
log(
|
|
69
89
|
JSON.stringify(
|
|
70
|
-
|
|
90
|
+
ordered.map(({ content: _, ...record }) => record),
|
|
71
91
|
null,
|
|
72
92
|
2,
|
|
73
93
|
),
|
|
74
94
|
)
|
|
75
|
-
} else
|
|
95
|
+
} else {
|
|
96
|
+
log(
|
|
97
|
+
formatTable(
|
|
98
|
+
[
|
|
99
|
+
"PHASE",
|
|
100
|
+
"PARENT",
|
|
101
|
+
"STATUS",
|
|
102
|
+
"READINESS",
|
|
103
|
+
"REPOSITORIES",
|
|
104
|
+
"BRANCH",
|
|
105
|
+
"PR",
|
|
106
|
+
"WORKTREE",
|
|
107
|
+
],
|
|
108
|
+
rows.map((row) => [
|
|
109
|
+
row.id,
|
|
110
|
+
row.parent,
|
|
111
|
+
row.status,
|
|
112
|
+
row.readiness,
|
|
113
|
+
row.repositories,
|
|
114
|
+
row.branch,
|
|
115
|
+
row.pr,
|
|
116
|
+
row.worktree,
|
|
117
|
+
]),
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
}
|
|
76
121
|
return
|
|
77
122
|
}
|
|
78
123
|
case "show": {
|
|
@@ -139,4 +184,9 @@ Create options:
|
|
|
139
184
|
|
|
140
185
|
Options:
|
|
141
186
|
--json Output results as JSON
|
|
187
|
+
--status <status> Filter list by status; repeatable
|
|
188
|
+
--repository <alias> Filter list by repository; repeatable
|
|
189
|
+
--ready Include only ready phases
|
|
190
|
+
--blocked Include only blocked phases
|
|
191
|
+
--pr / --no-pr Filter by recorded PR presence
|
|
142
192
|
`
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
runTestEffect,
|
|
9
9
|
} from "../test-utils"
|
|
10
10
|
import { status } from "./status"
|
|
11
|
+
import { task } from "./task"
|
|
11
12
|
|
|
12
13
|
describe("status command", () => {
|
|
13
14
|
let root: string
|
|
@@ -44,4 +45,47 @@ describe("status command", () => {
|
|
|
44
45
|
},
|
|
45
46
|
])
|
|
46
47
|
})
|
|
48
|
+
|
|
49
|
+
test("renders and filters the execution dashboard", async () => {
|
|
50
|
+
await runTestEffect(
|
|
51
|
+
task({
|
|
52
|
+
subcommand: "create",
|
|
53
|
+
args: ["example"],
|
|
54
|
+
repo: "agency",
|
|
55
|
+
branch: "feat/example",
|
|
56
|
+
base: "main",
|
|
57
|
+
cwd: root,
|
|
58
|
+
silent: true,
|
|
59
|
+
}),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const logs = await captureLogs(() =>
|
|
63
|
+
runTestEffect(
|
|
64
|
+
status({ cwd: root, repositories: ["agency"], ready: true, pr: false }),
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
expect(logs).toContain("Repositories: 1")
|
|
68
|
+
expect(logs.at(-1)).toContain(
|
|
69
|
+
"KIND WORK PARENT STATUS READINESS REPOSITORIES BRANCH",
|
|
70
|
+
)
|
|
71
|
+
expect(logs.at(-1)).toContain(
|
|
72
|
+
"task example - open ready agency feat/example absent absent",
|
|
73
|
+
)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test("reports validation issues without requiring a decodable graph", async () => {
|
|
77
|
+
await mkdir(join(root, "tasks/broken"), { recursive: true })
|
|
78
|
+
await Bun.write(
|
|
79
|
+
join(root, "tasks/broken/TASK.md"),
|
|
80
|
+
"---\nrepo: agency\nstatus: invalid\n---\n",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const logs = await captureLogs(() =>
|
|
84
|
+
runTestEffect(status({ cwd: root, json: true })),
|
|
85
|
+
)
|
|
86
|
+
const output = JSON.parse(logs[0]!)
|
|
87
|
+
expect(output.valid).toBe(false)
|
|
88
|
+
expect(output.issues.length).toBeGreaterThan(0)
|
|
89
|
+
expect(output.work).toEqual([])
|
|
90
|
+
})
|
|
47
91
|
})
|
package/src/commands/status.ts
CHANGED
|
@@ -3,9 +3,16 @@ import type { BaseCommandOptions } from "../utils/command"
|
|
|
3
3
|
import { WorkbaseService } from "../services/WorkbaseService"
|
|
4
4
|
import { RepositoryService } from "../services/RepositoryService"
|
|
5
5
|
import { createLoggers } from "../utils/effect"
|
|
6
|
+
import { formatTable } from "../utils/table"
|
|
7
|
+
import { getWorkViews } from "../work-view"
|
|
6
8
|
|
|
7
9
|
interface StatusOptions extends BaseCommandOptions {
|
|
8
10
|
readonly json?: boolean
|
|
11
|
+
readonly statuses?: readonly string[]
|
|
12
|
+
readonly repositories?: readonly string[]
|
|
13
|
+
readonly ready?: boolean
|
|
14
|
+
readonly blocked?: boolean
|
|
15
|
+
readonly pr?: boolean
|
|
9
16
|
}
|
|
10
17
|
|
|
11
18
|
export const status = (options: StatusOptions = {}) =>
|
|
@@ -16,7 +23,17 @@ export const status = (options: StatusOptions = {}) =>
|
|
|
16
23
|
const cwd = options.cwd ?? process.cwd()
|
|
17
24
|
const report = yield* workbase.validate(cwd)
|
|
18
25
|
const repos = yield* repositories.list(report.root)
|
|
19
|
-
const
|
|
26
|
+
const executionRows = report.valid
|
|
27
|
+
? (yield* getWorkViews({
|
|
28
|
+
cwd,
|
|
29
|
+
statuses: options.statuses,
|
|
30
|
+
repositories: options.repositories,
|
|
31
|
+
ready: options.ready,
|
|
32
|
+
blocked: options.blocked,
|
|
33
|
+
pr: options.pr,
|
|
34
|
+
})).executionRows
|
|
35
|
+
: []
|
|
36
|
+
const data = { ...report, repositories: repos, work: executionRows }
|
|
20
37
|
|
|
21
38
|
if (options.json) {
|
|
22
39
|
log(JSON.stringify(data, null, 2))
|
|
@@ -31,6 +48,33 @@ export const status = (options: StatusOptions = {}) =>
|
|
|
31
48
|
log(
|
|
32
49
|
`Validation: ${report.valid ? "valid" : `${report.issues.length} issues`}`,
|
|
33
50
|
)
|
|
51
|
+
log("")
|
|
52
|
+
log(
|
|
53
|
+
formatTable(
|
|
54
|
+
[
|
|
55
|
+
"KIND",
|
|
56
|
+
"WORK",
|
|
57
|
+
"PARENT",
|
|
58
|
+
"STATUS",
|
|
59
|
+
"READINESS",
|
|
60
|
+
"REPOSITORIES",
|
|
61
|
+
"BRANCH",
|
|
62
|
+
"PR",
|
|
63
|
+
"WORKTREE",
|
|
64
|
+
],
|
|
65
|
+
executionRows.map((row) => [
|
|
66
|
+
row.kind,
|
|
67
|
+
row.kind === "phase" ? row.key : row.id,
|
|
68
|
+
row.parent,
|
|
69
|
+
row.status,
|
|
70
|
+
row.readiness,
|
|
71
|
+
row.repositories,
|
|
72
|
+
row.branch,
|
|
73
|
+
row.pr,
|
|
74
|
+
row.worktree,
|
|
75
|
+
]),
|
|
76
|
+
),
|
|
77
|
+
)
|
|
34
78
|
})
|
|
35
79
|
|
|
36
80
|
export const help = `
|
|
@@ -39,5 +83,10 @@ Usage: agency status [options]
|
|
|
39
83
|
Show workbase repository, entity, and validation status.
|
|
40
84
|
|
|
41
85
|
Options:
|
|
42
|
-
--json
|
|
86
|
+
--json Output status as JSON
|
|
87
|
+
--status <status> Filter by status; repeatable
|
|
88
|
+
--repository <alias> Filter by repository; repeatable
|
|
89
|
+
--ready Include only ready work
|
|
90
|
+
--blocked Include only blocked work
|
|
91
|
+
--pr / --no-pr Filter by recorded PR presence
|
|
43
92
|
`
|
|
@@ -124,6 +124,26 @@ describe("task and phase command JSON output", () => {
|
|
|
124
124
|
})
|
|
125
125
|
})
|
|
126
126
|
|
|
127
|
+
test("renders task and phase operational tables", async () => {
|
|
128
|
+
const taskLogs = await captureLogs(() =>
|
|
129
|
+
runTestEffect(task({ subcommand: "list", args: [], cwd: root })),
|
|
130
|
+
)
|
|
131
|
+
expect(taskLogs[0]).toContain(
|
|
132
|
+
"TASK PARENT STATUS READINESS REPOSITORIES BRANCH",
|
|
133
|
+
)
|
|
134
|
+
expect(taskLogs[0]).toContain("multi - open ready")
|
|
135
|
+
|
|
136
|
+
const phaseLogs = await captureLogs(() =>
|
|
137
|
+
runTestEffect(phase({ subcommand: "list", args: ["multi"], cwd: root })),
|
|
138
|
+
)
|
|
139
|
+
expect(phaseLogs[0]).toContain(
|
|
140
|
+
"PHASE PARENT STATUS READINESS REPOSITORIES BRANCH",
|
|
141
|
+
)
|
|
142
|
+
expect(phaseLogs[0]).toContain(
|
|
143
|
+
"first multi open ready agency task/first absent absent",
|
|
144
|
+
)
|
|
145
|
+
})
|
|
146
|
+
|
|
127
147
|
test("outputs created task and phase records as JSON", async () => {
|
|
128
148
|
const taskLogs = await captureLogs(() =>
|
|
129
149
|
runTestEffect(
|
package/src/commands/task.ts
CHANGED
|
@@ -8,6 +8,8 @@ import { createLoggers } from "../utils/effect"
|
|
|
8
8
|
import { parseRepositoryReferences } from "../workbase/repository-reference"
|
|
9
9
|
import { WorkbaseService } from "../services/WorkbaseService"
|
|
10
10
|
import { choose } from "../utils/chooser"
|
|
11
|
+
import { formatTable } from "../utils/table"
|
|
12
|
+
import { getWorkViews } from "../work-view"
|
|
11
13
|
|
|
12
14
|
interface TaskOptions extends BaseCommandOptions {
|
|
13
15
|
readonly subcommand?: string
|
|
@@ -21,6 +23,11 @@ interface TaskOptions extends BaseCommandOptions {
|
|
|
21
23
|
readonly base?: string
|
|
22
24
|
readonly multiPhase?: boolean
|
|
23
25
|
readonly json?: boolean
|
|
26
|
+
readonly statuses?: readonly string[]
|
|
27
|
+
readonly repositories?: readonly string[]
|
|
28
|
+
readonly ready?: boolean
|
|
29
|
+
readonly blocked?: boolean
|
|
30
|
+
readonly pr?: boolean
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
export interface TaskInteraction {
|
|
@@ -208,16 +215,51 @@ export const task = (options: TaskOptions, interaction?: TaskInteraction) =>
|
|
|
208
215
|
}
|
|
209
216
|
case "list": {
|
|
210
217
|
const records = yield* tasks.list(cwd)
|
|
218
|
+
const { taskRows } = yield* getWorkViews({
|
|
219
|
+
cwd,
|
|
220
|
+
statuses: options.statuses,
|
|
221
|
+
repositories: options.repositories,
|
|
222
|
+
ready: options.ready,
|
|
223
|
+
blocked: options.blocked,
|
|
224
|
+
pr: options.pr,
|
|
225
|
+
})
|
|
226
|
+
const ordered = taskRows.flatMap((row) => {
|
|
227
|
+
const record = records.find((item) => item.id === row.key)
|
|
228
|
+
return record ? [record] : []
|
|
229
|
+
})
|
|
211
230
|
if (options.json) {
|
|
212
231
|
log(
|
|
213
232
|
JSON.stringify(
|
|
214
|
-
|
|
233
|
+
ordered.map(({ content: _, ...record }) => record),
|
|
215
234
|
null,
|
|
216
235
|
2,
|
|
217
236
|
),
|
|
218
237
|
)
|
|
219
238
|
} else {
|
|
220
|
-
|
|
239
|
+
log(
|
|
240
|
+
formatTable(
|
|
241
|
+
[
|
|
242
|
+
"TASK",
|
|
243
|
+
"PARENT",
|
|
244
|
+
"STATUS",
|
|
245
|
+
"READINESS",
|
|
246
|
+
"REPOSITORIES",
|
|
247
|
+
"BRANCH",
|
|
248
|
+
"PR",
|
|
249
|
+
"WORKTREE",
|
|
250
|
+
],
|
|
251
|
+
taskRows.map((row) => [
|
|
252
|
+
row.id,
|
|
253
|
+
row.parent,
|
|
254
|
+
row.status,
|
|
255
|
+
row.readiness,
|
|
256
|
+
row.repositories,
|
|
257
|
+
row.branch,
|
|
258
|
+
row.pr,
|
|
259
|
+
row.worktree,
|
|
260
|
+
]),
|
|
261
|
+
),
|
|
262
|
+
)
|
|
221
263
|
}
|
|
222
264
|
return
|
|
223
265
|
}
|
|
@@ -286,4 +328,9 @@ through task new, which fails when --no-input is set or no TTY is available.
|
|
|
286
328
|
Options:
|
|
287
329
|
--json Output results as JSON
|
|
288
330
|
--no-input Never open interactive task creation
|
|
331
|
+
--status <status> Filter list by status; repeatable
|
|
332
|
+
--repository <alias> Filter list by repository; repeatable
|
|
333
|
+
--ready Include only ready tasks
|
|
334
|
+
--blocked Include only blocked tasks
|
|
335
|
+
--pr / --no-pr Filter by recorded PR presence
|
|
289
336
|
`
|
|
@@ -47,8 +47,16 @@ const multiPhaseWorkspace: ExecutionWorkspace = {
|
|
|
47
47
|
interface HarnessOptions {
|
|
48
48
|
readonly workspace?: ExecutionWorkspace
|
|
49
49
|
readonly materializeError?: Error
|
|
50
|
-
readonly available?:
|
|
50
|
+
readonly available?: Readonly<Record<string, boolean>>
|
|
51
51
|
readonly chooserCommand?: readonly string[]
|
|
52
|
+
readonly runners?: Record<
|
|
53
|
+
string,
|
|
54
|
+
{
|
|
55
|
+
command: readonly [string, ...string[]]
|
|
56
|
+
resumeCommand?: readonly [string, ...string[]]
|
|
57
|
+
environment?: Record<string, string>
|
|
58
|
+
}
|
|
59
|
+
>
|
|
52
60
|
readonly multiPhaseTasks?: readonly string[]
|
|
53
61
|
readonly epicRecords?: readonly any[]
|
|
54
62
|
readonly taskRecords?: readonly any[]
|
|
@@ -72,6 +80,7 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
72
80
|
args: readonly string[]
|
|
73
81
|
cwd: string
|
|
74
82
|
}> = []
|
|
83
|
+
const launchEnvironments: Array<Readonly<Record<string, string>>> = []
|
|
75
84
|
const materializeOptions: Array<
|
|
76
85
|
Parameters<WorktreeService["materialize"]>[3]
|
|
77
86
|
> = []
|
|
@@ -105,6 +114,7 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
105
114
|
config: {
|
|
106
115
|
version: 2 as const,
|
|
107
116
|
chooserCommand: options.chooserCommand,
|
|
117
|
+
runners: options.runners,
|
|
108
118
|
},
|
|
109
119
|
}),
|
|
110
120
|
}
|
|
@@ -204,7 +214,7 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
204
214
|
isDirectory: (path: string) =>
|
|
205
215
|
Effect.succeed(options.existingDirectories?.includes(path) ?? true),
|
|
206
216
|
runCommand: (args: readonly string[]) => {
|
|
207
|
-
const cli = args[1]
|
|
217
|
+
const cli = args[1]!
|
|
208
218
|
events.push(`probe:${cli}`)
|
|
209
219
|
probes.push(cli)
|
|
210
220
|
return Effect.succeed({
|
|
@@ -214,9 +224,15 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
214
224
|
})
|
|
215
225
|
},
|
|
216
226
|
}
|
|
217
|
-
const launch = (
|
|
227
|
+
const launch = (
|
|
228
|
+
cli: string,
|
|
229
|
+
args: readonly string[],
|
|
230
|
+
cwd: string,
|
|
231
|
+
environment: Readonly<Record<string, string>>,
|
|
232
|
+
) => {
|
|
218
233
|
events.push(`launch:${cli}`)
|
|
219
234
|
launches.push({ cli, args, cwd })
|
|
235
|
+
launchEnvironments.push(environment)
|
|
220
236
|
}
|
|
221
237
|
const defaultPick: PickWorkTarget = () => Effect.succeed(null)
|
|
222
238
|
const defaultPickWorkbase: PickWorkbase = () => Effect.succeed(null)
|
|
@@ -257,6 +273,7 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
257
273
|
events,
|
|
258
274
|
probes,
|
|
259
275
|
launches,
|
|
276
|
+
launchEnvironments,
|
|
260
277
|
materializeOptions,
|
|
261
278
|
statusUpdates,
|
|
262
279
|
shownTasks,
|
|
@@ -353,7 +370,6 @@ describe("work command", () => {
|
|
|
353
370
|
cli: "opencode",
|
|
354
371
|
args: [
|
|
355
372
|
"opencode",
|
|
356
|
-
"--continue",
|
|
357
373
|
"--prompt",
|
|
358
374
|
"Work on the epic. Read /workbase/epics/delivery/EPIC.md.",
|
|
359
375
|
],
|
|
@@ -403,7 +419,6 @@ describe("work command", () => {
|
|
|
403
419
|
cli: "opencode",
|
|
404
420
|
args: [
|
|
405
421
|
"opencode",
|
|
406
|
-
"--continue",
|
|
407
422
|
"--prompt",
|
|
408
423
|
"Work on the task. Read /workbase/tasks/delivery/TASK.md.",
|
|
409
424
|
],
|
|
@@ -429,7 +444,6 @@ describe("work command", () => {
|
|
|
429
444
|
cli: "opencode",
|
|
430
445
|
args: [
|
|
431
446
|
"opencode",
|
|
432
|
-
"--continue",
|
|
433
447
|
"--prompt",
|
|
434
448
|
"Start the task. Read /workbase/tasks/example/TASK.md and /workbase/tasks/example/phases/implementation/PHASE.md.",
|
|
435
449
|
],
|
|
@@ -596,7 +610,7 @@ describe("work command", () => {
|
|
|
596
610
|
|
|
597
611
|
await expect(
|
|
598
612
|
harness.run({ taskId: "example", opencode: true, claude: true }),
|
|
599
|
-
).rejects.toThrow("Cannot
|
|
613
|
+
).rejects.toThrow("Cannot combine --runner, --opencode, and --claude")
|
|
600
614
|
expect(harness.events).toEqual([])
|
|
601
615
|
})
|
|
602
616
|
|
|
@@ -624,7 +638,6 @@ describe("work command", () => {
|
|
|
624
638
|
cli: "opencode",
|
|
625
639
|
args: [
|
|
626
640
|
"opencode",
|
|
627
|
-
"--continue",
|
|
628
641
|
"--prompt",
|
|
629
642
|
"Start the task. Read /workbase/tasks/example/TASK.md.",
|
|
630
643
|
],
|
|
@@ -638,14 +651,18 @@ describe("work command", () => {
|
|
|
638
651
|
])
|
|
639
652
|
})
|
|
640
653
|
|
|
641
|
-
test("
|
|
654
|
+
test("resumes OpenCode deterministically when a session identity exists", async () => {
|
|
642
655
|
const harness = createHarness({ workspace: multiPhaseWorkspace })
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
656
|
+
process.env.AGENCY_SESSION_ID = "existing-session"
|
|
657
|
+
try {
|
|
658
|
+
await harness.run({
|
|
659
|
+
taskId: "example",
|
|
660
|
+
phaseId: "implementation",
|
|
661
|
+
opencode: true,
|
|
662
|
+
})
|
|
663
|
+
} finally {
|
|
664
|
+
delete process.env.AGENCY_SESSION_ID
|
|
665
|
+
}
|
|
649
666
|
|
|
650
667
|
expect(harness.launches[0]).toEqual({
|
|
651
668
|
cli: "opencode",
|
|
@@ -659,6 +676,78 @@ describe("work command", () => {
|
|
|
659
676
|
})
|
|
660
677
|
})
|
|
661
678
|
|
|
679
|
+
test("expands a named runner with shared context and claim identity", async () => {
|
|
680
|
+
const harness = createHarness({
|
|
681
|
+
available: { codex: true },
|
|
682
|
+
runners: {
|
|
683
|
+
custom: {
|
|
684
|
+
command: ["codex", "--task", "{task}", "{prompt}"],
|
|
685
|
+
environment: {
|
|
686
|
+
CUSTOM_TARGET: "{target}",
|
|
687
|
+
AGENCY_TARGET: "cannot-override",
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
},
|
|
691
|
+
})
|
|
692
|
+
|
|
693
|
+
await harness.run({ taskId: "example", runner: "custom" })
|
|
694
|
+
|
|
695
|
+
expect(harness.probes).toEqual(["codex"])
|
|
696
|
+
expect(harness.launches[0]).toEqual({
|
|
697
|
+
cli: "codex",
|
|
698
|
+
args: [
|
|
699
|
+
"codex",
|
|
700
|
+
"--task",
|
|
701
|
+
"example",
|
|
702
|
+
"Start the task. Read /workbase/tasks/example/TASK.md.",
|
|
703
|
+
],
|
|
704
|
+
cwd: singlePhaseWorkspace.writablePath,
|
|
705
|
+
})
|
|
706
|
+
expect(harness.launchEnvironments[0]).toMatchObject({
|
|
707
|
+
AGENCY_RUNNER: "custom",
|
|
708
|
+
AGENCY_CLAIMANT: process.env.USER ?? "agency",
|
|
709
|
+
AGENCY_WORKBASE: "/workbase",
|
|
710
|
+
AGENCY_TARGET: "execution-unit:task/example",
|
|
711
|
+
AGENCY_TASK_ID: "example",
|
|
712
|
+
AGENCY_PHASE_ID: "",
|
|
713
|
+
AGENCY_CLAIM_REVISION: "1".repeat(64),
|
|
714
|
+
CUSTOM_TARGET: "execution-unit:task/example",
|
|
715
|
+
})
|
|
716
|
+
})
|
|
717
|
+
|
|
718
|
+
test("prints the exact command contract without launching and omits secrets", async () => {
|
|
719
|
+
const harness = createHarness({
|
|
720
|
+
available: { agent: true },
|
|
721
|
+
runners: {
|
|
722
|
+
custom: {
|
|
723
|
+
command: ["agent", "{prompt}"],
|
|
724
|
+
environment: {
|
|
725
|
+
VISIBLE: "{task}",
|
|
726
|
+
API_TOKEN: "do-not-print",
|
|
727
|
+
},
|
|
728
|
+
},
|
|
729
|
+
},
|
|
730
|
+
})
|
|
731
|
+
|
|
732
|
+
const output = await captureLogs(() =>
|
|
733
|
+
harness.run({
|
|
734
|
+
taskId: "example",
|
|
735
|
+
runner: "custom",
|
|
736
|
+
printCommand: true,
|
|
737
|
+
}),
|
|
738
|
+
)
|
|
739
|
+
const printed = JSON.parse(output.join("\n"))
|
|
740
|
+
|
|
741
|
+
expect(harness.launches).toEqual([])
|
|
742
|
+
expect(printed.cwd).toBe(singlePhaseWorkspace.writablePath)
|
|
743
|
+
expect(printed.argv).toEqual([
|
|
744
|
+
"agent",
|
|
745
|
+
"Start the task. Read /workbase/tasks/example/TASK.md.",
|
|
746
|
+
])
|
|
747
|
+
expect(printed.environment.VISIBLE).toBe("example")
|
|
748
|
+
expect(printed.environment.API_TOKEN).toBeUndefined()
|
|
749
|
+
})
|
|
750
|
+
|
|
662
751
|
test("automatically falls back to Claude", async () => {
|
|
663
752
|
const harness = createHarness({ available: { opencode: false } })
|
|
664
753
|
|
|
@@ -729,7 +818,7 @@ describe("work command", () => {
|
|
|
729
818
|
verboseHarness.run({ taskId: "example", verbose: true }),
|
|
730
819
|
)
|
|
731
820
|
expect(verboseLogs).toEqual([
|
|
732
|
-
"Launching command: opencode --
|
|
821
|
+
"Launching command: opencode --prompt 'Start the task. Read /workbase/tasks/example/TASK.md.' (cwd: /workbase/tasks/example/code/agency)",
|
|
733
822
|
])
|
|
734
823
|
expect(verboseHarness.materializeOptions[0]?.verbose).toBe(true)
|
|
735
824
|
|