@markjaquith/agency 2.6.0 → 2.7.1
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 +8 -8
- package/cli.ts +8 -4
- package/package.json +1 -1
- package/skills/agency/SKILL.md +4 -4
- package/src/commands/init.test.ts +3 -0
- package/src/commands/work.test.ts +53 -8
- package/src/commands/work.ts +41 -10
- package/src/services/WorkbaseService.test.ts +2 -2
- package/src/services/WorkbaseService.ts +4 -6
- package/src/services/WorktreeService.test.ts +45 -0
- package/src/services/WorktreeService.ts +10 -6
- package/src/workbase/opencode-file.ts +25 -16
package/README.md
CHANGED
|
@@ -230,7 +230,7 @@ agency repo link backend ~/Dev/backend
|
|
|
230
230
|
agency task new
|
|
231
231
|
|
|
232
232
|
agency validate
|
|
233
|
-
agency work refresh-copy
|
|
233
|
+
agency work tasks/refresh-copy
|
|
234
234
|
agency pr create refresh-copy
|
|
235
235
|
```
|
|
236
236
|
|
|
@@ -350,21 +350,21 @@ before moving files, refuses dirty worktrees, and preserves branches.
|
|
|
350
350
|
### Work and Pull Requests
|
|
351
351
|
|
|
352
352
|
```text
|
|
353
|
-
agency work [<
|
|
353
|
+
agency work [<directory> | --epic <epic-id>] [--opencode | --claude]
|
|
354
354
|
agency pr create <task-id> [phase-id] [--draft] [--json]
|
|
355
355
|
```
|
|
356
356
|
|
|
357
|
-
`agency work`
|
|
358
|
-
|
|
359
|
-
workbase,
|
|
357
|
+
`agency work` presents the full hierarchy in `fzf`. Pass a directory, including
|
|
358
|
+
`.` for the current directory, to infer its epic, task, or phase. Outside a
|
|
359
|
+
workbase, Agency first presents the registered workbases, then the selected
|
|
360
360
|
workbase's hierarchy. If `fzf` is not installed, Agency prints the available
|
|
361
|
-
choices and asks for an explicit
|
|
361
|
+
choices and asks for an explicit directory.
|
|
362
362
|
|
|
363
363
|
Epic and multi-phase task targets launch orchestration agents beside their
|
|
364
364
|
documents. Single-phase tasks and phases fetch repositories, create or reuse
|
|
365
365
|
worktrees under `code/`, and launch an execution agent in the writable checkout
|
|
366
|
-
with absolute context paths.
|
|
367
|
-
|
|
366
|
+
with absolute context paths. An explicit directory or `--epic` target bypasses
|
|
367
|
+
the hierarchy chooser.
|
|
368
368
|
|
|
369
369
|
Each writable `(repo, branch)` pair may belong to only one task or phase. Agency
|
|
370
370
|
validation reports duplicate ownership, and `agency work` checks Git's worktree
|
package/cli.ts
CHANGED
|
@@ -239,11 +239,15 @@ const commands: Record<string, Command> = {
|
|
|
239
239
|
console.log(workHelp)
|
|
240
240
|
return
|
|
241
241
|
}
|
|
242
|
+
if (args.length > 1) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
"Usage: agency work [<directory-or-task-id> | --epic <epic-id>]",
|
|
245
|
+
)
|
|
246
|
+
}
|
|
242
247
|
|
|
243
248
|
await runCommand(
|
|
244
249
|
work({
|
|
245
|
-
|
|
246
|
-
phaseId: args[1],
|
|
250
|
+
directory: args[0],
|
|
247
251
|
epicId: options.epic,
|
|
248
252
|
silent: options.silent,
|
|
249
253
|
verbose: options.verbose,
|
|
@@ -299,7 +303,7 @@ Commands:
|
|
|
299
303
|
phase <subcommand> Manage task phases
|
|
300
304
|
archive <type> Archive a work item
|
|
301
305
|
task <subcommand> Manage tasks
|
|
302
|
-
work [task]
|
|
306
|
+
work [directory|task] Work on an epic, task, or phase
|
|
303
307
|
pr create Create a pull request for an execution unit
|
|
304
308
|
repo <subcommand> Manage workbase repositories
|
|
305
309
|
status Show status for the current workbase
|
|
@@ -314,7 +318,7 @@ Global Options:
|
|
|
314
318
|
Examples:
|
|
315
319
|
agency init # Initialize the current directory
|
|
316
320
|
agency task list # List tasks
|
|
317
|
-
agency work refresh-cli-copy
|
|
321
|
+
agency work tasks/refresh-cli-copy # Start working on a task
|
|
318
322
|
|
|
319
323
|
For more information about a command, run:
|
|
320
324
|
agency <command> --help
|
package/package.json
CHANGED
package/skills/agency/SKILL.md
CHANGED
|
@@ -246,15 +246,15 @@ Outside a workbase, omitting path opens the registered-workbase picker.
|
|
|
246
246
|
|
|
247
247
|
```bash
|
|
248
248
|
agency work
|
|
249
|
-
agency work <
|
|
249
|
+
agency work <directory>
|
|
250
250
|
agency work --epic <epic-id>
|
|
251
251
|
```
|
|
252
252
|
|
|
253
253
|
Use `--opencode` or `--claude` to require a specific agent. This command fetches
|
|
254
254
|
repositories for execution targets, creates or reuses their worktrees, and
|
|
255
|
-
replaces the current process with the selected agent.
|
|
256
|
-
|
|
257
|
-
|
|
255
|
+
replaces the current process with the selected agent. With no directory it opens
|
|
256
|
+
an `fzf` picker containing the workbase hierarchy. Pass `.`, or another
|
|
257
|
+
directory, to infer the nearest epic, task, or phase.
|
|
258
258
|
Outside a workbase, it first opens a picker containing registered workbases.
|
|
259
259
|
|
|
260
260
|
Epic and multi-phase task targets are orchestration sessions launched beside
|
|
@@ -39,6 +39,9 @@ describe("init command", () => {
|
|
|
39
39
|
expect(
|
|
40
40
|
await Bun.file(join(root, ".opencode/opencode.jsonc")).text(),
|
|
41
41
|
).toContain('"path": "../tasks"')
|
|
42
|
+
expect(
|
|
43
|
+
await Bun.file(join(root, ".opencode/opencode.jsonc")).text(),
|
|
44
|
+
).toContain(`"${join(root, "tasks")}/*": "allow"`)
|
|
42
45
|
})
|
|
43
46
|
|
|
44
47
|
test("preserves existing gitignore entries", async () => {
|
|
@@ -46,12 +46,14 @@ interface HarnessOptions {
|
|
|
46
46
|
readonly phaseRecords?: readonly any[]
|
|
47
47
|
readonly outsideWorkbase?: boolean
|
|
48
48
|
readonly registeredWorkbases?: readonly string[]
|
|
49
|
+
readonly existingDirectories?: readonly string[]
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
const createHarness = (options: HarnessOptions = {}) => {
|
|
52
53
|
const events: string[] = []
|
|
53
54
|
const probes: string[] = []
|
|
54
55
|
const statusUpdates: string[] = []
|
|
56
|
+
const shownTasks: string[] = []
|
|
55
57
|
const progressUpdates: string[] = []
|
|
56
58
|
const launches: Array<{
|
|
57
59
|
cli: string
|
|
@@ -95,14 +97,16 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
95
97
|
list: () => Effect.succeed(options.epicRecords ?? []),
|
|
96
98
|
}
|
|
97
99
|
const tasks = {
|
|
98
|
-
show: (id: string) =>
|
|
99
|
-
|
|
100
|
+
show: (id: string) => {
|
|
101
|
+
shownTasks.push(id)
|
|
102
|
+
return Effect.succeed({
|
|
100
103
|
id,
|
|
101
104
|
path: `/workbase/tasks/${id}/TASK.md`,
|
|
102
105
|
data: options.multiPhaseTasks?.includes(id)
|
|
103
106
|
? { phases: [] }
|
|
104
107
|
: { repo: "agency", branch: `task/${id}`, base: "main" },
|
|
105
|
-
})
|
|
108
|
+
})
|
|
109
|
+
},
|
|
106
110
|
list: () => Effect.succeed(options.taskRecords ?? []),
|
|
107
111
|
setStatus: (id: string, status: string) => {
|
|
108
112
|
statusUpdates.push(`task:${id}:${status}`)
|
|
@@ -124,6 +128,8 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
124
128
|
},
|
|
125
129
|
}
|
|
126
130
|
const fs = {
|
|
131
|
+
isDirectory: (path: string) =>
|
|
132
|
+
Effect.succeed(options.existingDirectories?.includes(path) ?? true),
|
|
127
133
|
runCommand: (args: readonly string[]) => {
|
|
128
134
|
const cli = args[1] as "opencode" | "claude"
|
|
129
135
|
events.push(`probe:${cli}`)
|
|
@@ -168,6 +174,7 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
168
174
|
launches,
|
|
169
175
|
materializeOptions,
|
|
170
176
|
statusUpdates,
|
|
177
|
+
shownTasks,
|
|
171
178
|
progressUpdates,
|
|
172
179
|
run,
|
|
173
180
|
}
|
|
@@ -177,7 +184,11 @@ describe("work command", () => {
|
|
|
177
184
|
test("launches an epic agent from an epic directory", async () => {
|
|
178
185
|
const harness = createHarness()
|
|
179
186
|
|
|
180
|
-
await harness.run({
|
|
187
|
+
await harness.run({
|
|
188
|
+
cwd: "/workbase/epics/delivery",
|
|
189
|
+
directory: ".",
|
|
190
|
+
opencode: true,
|
|
191
|
+
})
|
|
181
192
|
|
|
182
193
|
expect(harness.events).toEqual(["probe:opencode", "launch:opencode"])
|
|
183
194
|
expect(harness.launches[0]).toEqual({
|
|
@@ -187,10 +198,42 @@ describe("work command", () => {
|
|
|
187
198
|
})
|
|
188
199
|
})
|
|
189
200
|
|
|
201
|
+
test("resolves an existing positional path before treating it as a task ID", async () => {
|
|
202
|
+
const harness = createHarness({
|
|
203
|
+
existingDirectories: ["/workbase/tasks/delivery"],
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
await harness.run({
|
|
207
|
+
cwd: "/workbase/tasks/delivery",
|
|
208
|
+
directory: ".",
|
|
209
|
+
opencode: true,
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
expect(harness.shownTasks).toEqual(["delivery"])
|
|
213
|
+
expect(harness.launches[0]?.cwd).toBe("/workbase/tasks/delivery")
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
test("treats a positional value as a task ID when it is not a directory", async () => {
|
|
217
|
+
const harness = createHarness({ existingDirectories: [] })
|
|
218
|
+
|
|
219
|
+
await harness.run({
|
|
220
|
+
cwd: "/workbase",
|
|
221
|
+
directory: "delivery",
|
|
222
|
+
opencode: true,
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
expect(harness.shownTasks).toEqual(["delivery"])
|
|
226
|
+
expect(harness.launches[0]?.cwd).toBe("/workbase/tasks/delivery")
|
|
227
|
+
})
|
|
228
|
+
|
|
190
229
|
test("launches a multi-phase task agent without materializing", async () => {
|
|
191
230
|
const harness = createHarness({ multiPhaseTasks: ["delivery"] })
|
|
192
231
|
|
|
193
|
-
await harness.run({
|
|
232
|
+
await harness.run({
|
|
233
|
+
cwd: "/workbase/tasks/delivery",
|
|
234
|
+
directory: ".",
|
|
235
|
+
opencode: true,
|
|
236
|
+
})
|
|
194
237
|
|
|
195
238
|
expect(harness.events).toEqual(["probe:opencode", "launch:opencode"])
|
|
196
239
|
expect(harness.launches[0]).toEqual({
|
|
@@ -205,6 +248,7 @@ describe("work command", () => {
|
|
|
205
248
|
|
|
206
249
|
await harness.run({
|
|
207
250
|
cwd: "/workbase/tasks/example/phases/implementation/code/agency/src",
|
|
251
|
+
directory: ".",
|
|
208
252
|
opencode: true,
|
|
209
253
|
})
|
|
210
254
|
|
|
@@ -224,6 +268,7 @@ describe("work command", () => {
|
|
|
224
268
|
|
|
225
269
|
await harness.run({
|
|
226
270
|
cwd: "/workbase/tasks/example/code/agency/src",
|
|
271
|
+
directory: ".",
|
|
227
272
|
opencode: true,
|
|
228
273
|
})
|
|
229
274
|
|
|
@@ -231,7 +276,7 @@ describe("work command", () => {
|
|
|
231
276
|
expect(harness.launches[0]?.cwd).toBe("/workbase/tasks/example")
|
|
232
277
|
})
|
|
233
278
|
|
|
234
|
-
test("selects a target with fzf
|
|
279
|
+
test("selects a target with fzf when no directory is provided", async () => {
|
|
235
280
|
const phase = {
|
|
236
281
|
taskId: "delivery",
|
|
237
282
|
id: "build",
|
|
@@ -254,7 +299,7 @@ describe("work command", () => {
|
|
|
254
299
|
choices.find((choice) => choice.label.includes("build"))!.target,
|
|
255
300
|
)
|
|
256
301
|
|
|
257
|
-
await harness.run({ cwd: "/workbase", opencode: true }, pick)
|
|
302
|
+
await harness.run({ cwd: "/workbase/tasks/example", opencode: true }, pick)
|
|
258
303
|
|
|
259
304
|
expect(harness.events).toEqual([
|
|
260
305
|
"probe:fzf",
|
|
@@ -448,7 +493,7 @@ describe("work command", () => {
|
|
|
448
493
|
verboseHarness.run({ taskId: "example", verbose: true }),
|
|
449
494
|
)
|
|
450
495
|
expect(verboseLogs).toEqual([
|
|
451
|
-
"Launching opencode
|
|
496
|
+
"Launching command: opencode --continue (cwd: /workbase/tasks/example)",
|
|
452
497
|
])
|
|
453
498
|
expect(verboseHarness.materializeOptions[0]?.verbose).toBe(true)
|
|
454
499
|
|
package/src/commands/work.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
} from "../workbase/workbase-choice"
|
|
24
24
|
|
|
25
25
|
interface WorkOptions extends BaseCommandOptions {
|
|
26
|
+
readonly directory?: string
|
|
26
27
|
readonly taskId?: string
|
|
27
28
|
readonly phaseId?: string
|
|
28
29
|
readonly epicId?: string
|
|
@@ -32,6 +33,15 @@ interface WorkOptions extends BaseCommandOptions {
|
|
|
32
33
|
|
|
33
34
|
type LaunchAgent = (cli: string, args: readonly string[], cwd: string) => void
|
|
34
35
|
|
|
36
|
+
const formatCommand = (args: readonly string[]) =>
|
|
37
|
+
args
|
|
38
|
+
.map((argument) =>
|
|
39
|
+
/^[A-Za-z0-9_./:=+@%-]+$/.test(argument)
|
|
40
|
+
? argument
|
|
41
|
+
: `'${argument.replaceAll("'", `'\\''`)}'`,
|
|
42
|
+
)
|
|
43
|
+
.join(" ")
|
|
44
|
+
|
|
35
45
|
const launchAgent: LaunchAgent = (cli, args, cwd) => {
|
|
36
46
|
process.chdir(cwd)
|
|
37
47
|
execvp(cli, [...args])
|
|
@@ -50,9 +60,12 @@ export const work = (
|
|
|
50
60
|
new Error("Cannot use both --opencode and --claude"),
|
|
51
61
|
)
|
|
52
62
|
}
|
|
53
|
-
if (
|
|
63
|
+
if (
|
|
64
|
+
options.epicId &&
|
|
65
|
+
(options.directory || options.taskId || options.phaseId)
|
|
66
|
+
) {
|
|
54
67
|
return yield* Effect.fail(
|
|
55
|
-
new Error("Cannot combine --epic with a task or phase ID"),
|
|
68
|
+
new Error("Cannot combine --epic with a directory, task, or phase ID"),
|
|
56
69
|
)
|
|
57
70
|
}
|
|
58
71
|
|
|
@@ -64,7 +77,14 @@ export const work = (
|
|
|
64
77
|
const phases = yield* PhaseService
|
|
65
78
|
const { log, verboseLog } = createLoggers(options)
|
|
66
79
|
const cwd = options.cwd ?? process.cwd()
|
|
67
|
-
const
|
|
80
|
+
const directoryPath = options.directory
|
|
81
|
+
? resolve(cwd, options.directory)
|
|
82
|
+
: undefined
|
|
83
|
+
const isDirectory = directoryPath
|
|
84
|
+
? yield* fs.isDirectory(directoryPath)
|
|
85
|
+
: false
|
|
86
|
+
const startPath = isDirectory && directoryPath ? directoryPath : cwd
|
|
87
|
+
const root = yield* resolveWorkbase(startPath, log, pickBase)
|
|
68
88
|
if (!root) return
|
|
69
89
|
|
|
70
90
|
let target: WorkTarget | null = null
|
|
@@ -89,8 +109,16 @@ export const work = (
|
|
|
89
109
|
multiPhase: "phases" in task.data,
|
|
90
110
|
}
|
|
91
111
|
}
|
|
92
|
-
} else {
|
|
93
|
-
const
|
|
112
|
+
} else if (options.directory && !isDirectory) {
|
|
113
|
+
const task = yield* tasks.show(options.directory, root)
|
|
114
|
+
target = {
|
|
115
|
+
kind: "task",
|
|
116
|
+
taskId: task.id,
|
|
117
|
+
path: task.path,
|
|
118
|
+
multiPhase: "phases" in task.data,
|
|
119
|
+
}
|
|
120
|
+
} else if (directoryPath) {
|
|
121
|
+
const path = relative(root, startPath)
|
|
94
122
|
const parts =
|
|
95
123
|
!path || isAbsolute(path) || path.startsWith(`..${sep}`)
|
|
96
124
|
? []
|
|
@@ -145,7 +173,7 @@ export const work = (
|
|
|
145
173
|
for (const choice of choices) log(choice.label)
|
|
146
174
|
return yield* Effect.fail(
|
|
147
175
|
new Error(
|
|
148
|
-
"fzf is required to select a work target; install fzf or provide a
|
|
176
|
+
"fzf is required to select a work target; install fzf or provide a directory explicitly",
|
|
149
177
|
),
|
|
150
178
|
)
|
|
151
179
|
}
|
|
@@ -199,16 +227,19 @@ export const work = (
|
|
|
199
227
|
yield* tasks.setStatus(target.taskId, "working", root)
|
|
200
228
|
}
|
|
201
229
|
|
|
202
|
-
verboseLog(`Launching ${cli} in ${launchPath}`)
|
|
203
230
|
const args = cli === "opencode" ? ["--continue"] : [prompt]
|
|
231
|
+
verboseLog(
|
|
232
|
+
`Launching command: ${formatCommand([cli, ...args])} (cwd: ${launchPath})`,
|
|
233
|
+
)
|
|
204
234
|
launch(cli, [cli, ...args], launchPath)
|
|
205
235
|
})
|
|
206
236
|
|
|
207
237
|
export const help = `
|
|
208
|
-
Usage: agency work [<task-id>
|
|
238
|
+
Usage: agency work [<directory-or-task-id> | --epic <epic-id>]
|
|
209
239
|
|
|
210
|
-
Launch an agent for
|
|
211
|
-
|
|
240
|
+
Launch an agent for an epic, task, or phase. With no directory, select one
|
|
241
|
+
with fzf. A positional argument resolves as a directory first, then as a task
|
|
242
|
+
ID. Use '.' for the current directory. Outside a workbase, select a registered
|
|
212
243
|
workbase first.
|
|
213
244
|
|
|
214
245
|
Options:
|
|
@@ -52,7 +52,7 @@ describe("WorkbaseService", () => {
|
|
|
52
52
|
managedWorkbaseAgents,
|
|
53
53
|
)
|
|
54
54
|
expect(await Bun.file(join(root, ".opencode/opencode.jsonc")).text()).toBe(
|
|
55
|
-
managedWorkbaseOpencode,
|
|
55
|
+
managedWorkbaseOpencode(root),
|
|
56
56
|
)
|
|
57
57
|
})
|
|
58
58
|
|
|
@@ -127,7 +127,7 @@ describe("WorkbaseService", () => {
|
|
|
127
127
|
)
|
|
128
128
|
|
|
129
129
|
expect(await Bun.file(join(root, ".opencode/opencode.jsonc")).text()).toBe(
|
|
130
|
-
managedWorkbaseOpencode,
|
|
130
|
+
managedWorkbaseOpencode(root),
|
|
131
131
|
)
|
|
132
132
|
})
|
|
133
133
|
|
|
@@ -142,19 +142,17 @@ const ensureWorkbaseOpencode = (root: string) =>
|
|
|
142
142
|
const fs = yield* FileSystemService
|
|
143
143
|
const directory = join(root, ".opencode")
|
|
144
144
|
const path = join(directory, "opencode.jsonc")
|
|
145
|
+
const managed = managedWorkbaseOpencode(root)
|
|
145
146
|
if (!(yield* fs.exists(path))) {
|
|
146
147
|
if (yield* fs.exists(join(directory, "opencode.json"))) return
|
|
147
148
|
yield* fs.createDirectory(directory)
|
|
148
|
-
yield* fs.writeFile(path,
|
|
149
|
+
yield* fs.writeFile(path, managed)
|
|
149
150
|
return
|
|
150
151
|
}
|
|
151
152
|
|
|
152
153
|
const content = yield* fs.readFile(path)
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
canUpdateManagedWorkbaseOpencode(content)
|
|
156
|
-
) {
|
|
157
|
-
yield* fs.writeFile(path, managedWorkbaseOpencode)
|
|
154
|
+
if (content !== managed && canUpdateManagedWorkbaseOpencode(content)) {
|
|
155
|
+
yield* fs.writeFile(path, managed)
|
|
158
156
|
}
|
|
159
157
|
})
|
|
160
158
|
|
|
@@ -85,6 +85,51 @@ describe("WorktreeService", () => {
|
|
|
85
85
|
expect(new TextDecoder().decode(branch.stdout).trim()).toBe("task/example")
|
|
86
86
|
})
|
|
87
87
|
|
|
88
|
+
test("does not fetch origins for existing worktrees", async () => {
|
|
89
|
+
await runTestEffect(
|
|
90
|
+
TaskService.pipe(
|
|
91
|
+
Effect.flatMap((service) =>
|
|
92
|
+
service.create(
|
|
93
|
+
{
|
|
94
|
+
id: "existing",
|
|
95
|
+
ticketUrl: "https://example.com/task",
|
|
96
|
+
repo: "agency",
|
|
97
|
+
repos: [{ repo: "effect", ref: "main" }],
|
|
98
|
+
branch: "task/existing",
|
|
99
|
+
base: "main",
|
|
100
|
+
},
|
|
101
|
+
root,
|
|
102
|
+
),
|
|
103
|
+
),
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
await runTestEffect(
|
|
107
|
+
WorktreeService.pipe(
|
|
108
|
+
Effect.flatMap((service) =>
|
|
109
|
+
service.materialize("existing", undefined, root),
|
|
110
|
+
),
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
await git(
|
|
114
|
+
["remote", "set-url", "origin", join(root, "missing")],
|
|
115
|
+
join(root, "repos/agency"),
|
|
116
|
+
)
|
|
117
|
+
await git(
|
|
118
|
+
["remote", "set-url", "origin", join(root, "missing")],
|
|
119
|
+
join(root, "repos/effect"),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
await expect(
|
|
123
|
+
runTestEffect(
|
|
124
|
+
WorktreeService.pipe(
|
|
125
|
+
Effect.flatMap((service) =>
|
|
126
|
+
service.materialize("existing", undefined, root),
|
|
127
|
+
),
|
|
128
|
+
),
|
|
129
|
+
),
|
|
130
|
+
).resolves.toMatchObject({ repo: "agency" })
|
|
131
|
+
})
|
|
132
|
+
|
|
88
133
|
test("uses a configured worktree creation command", async () => {
|
|
89
134
|
await Bun.write(
|
|
90
135
|
join(root, "agency.json"),
|
|
@@ -136,11 +136,13 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
136
136
|
})
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
139
|
+
const fetchOrigin = Effect.gen(function* () {
|
|
140
|
+
const remote = yield* fs.runCommand(
|
|
141
|
+
["git", "-C", repositoryPath, "remote", "get-url", "origin"],
|
|
142
|
+
{ captureOutput: true },
|
|
143
|
+
)
|
|
144
|
+
if (remote.exitCode !== 0) return
|
|
145
|
+
|
|
144
146
|
const fetch = yield* fs.runCommand(
|
|
145
147
|
["git", "-C", repositoryPath, "fetch", "origin"],
|
|
146
148
|
{ captureOutput: true },
|
|
@@ -150,7 +152,7 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
150
152
|
message: `Failed to fetch '${alias}': ${fetch.stderr}`,
|
|
151
153
|
})
|
|
152
154
|
}
|
|
153
|
-
}
|
|
155
|
+
})
|
|
154
156
|
|
|
155
157
|
const listed = yield* fs.runCommand(
|
|
156
158
|
[
|
|
@@ -208,6 +210,7 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
208
210
|
message: `Worktree registry contains a missing checkout at ${checkoutPath}`,
|
|
209
211
|
})
|
|
210
212
|
}
|
|
213
|
+
yield* fetchOrigin
|
|
211
214
|
|
|
212
215
|
let args: string[]
|
|
213
216
|
let env: Record<string, string> | undefined
|
|
@@ -341,6 +344,7 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
341
344
|
message: `Worktree registry contains a missing checkout at ${checkoutPath}`,
|
|
342
345
|
})
|
|
343
346
|
}
|
|
347
|
+
yield* fetchOrigin
|
|
344
348
|
const result = yield* fs.runCommand(
|
|
345
349
|
[
|
|
346
350
|
"git",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto"
|
|
2
|
+
import { join } from "node:path"
|
|
2
3
|
|
|
3
4
|
const managedHeaderPattern =
|
|
4
5
|
/^\/\/ agency-managed: sha256=([a-f0-9]{64})\r?\n\r?\n/
|
|
@@ -6,28 +7,36 @@ const managedHeaderPattern =
|
|
|
6
7
|
const checksum = (content: string) =>
|
|
7
8
|
createHash("sha256").update(content).digest("hex")
|
|
8
9
|
|
|
9
|
-
const body =
|
|
10
|
-
{
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
const body = (root: string) =>
|
|
11
|
+
`${JSON.stringify(
|
|
12
|
+
{
|
|
13
|
+
$schema: "https://opencode.ai/config.json",
|
|
14
|
+
references: {
|
|
15
|
+
tasks: {
|
|
16
|
+
path: "../tasks",
|
|
17
|
+
description: "Agency task definitions and execution context",
|
|
18
|
+
},
|
|
19
|
+
epics: {
|
|
20
|
+
path: "../epics",
|
|
21
|
+
description: "Agency epic definitions and orchestration context",
|
|
22
|
+
},
|
|
16
23
|
},
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
permission: {
|
|
25
|
+
external_directory: {
|
|
26
|
+
[`${join(root, "tasks")}/*`]: "allow",
|
|
27
|
+
[`${join(root, "epics")}/*`]: "allow",
|
|
28
|
+
},
|
|
20
29
|
},
|
|
21
30
|
},
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
)}\n`
|
|
31
|
+
null,
|
|
32
|
+
2,
|
|
33
|
+
)}\n`
|
|
26
34
|
|
|
27
|
-
const renderManagedWorkbaseOpencode = (content: string
|
|
35
|
+
const renderManagedWorkbaseOpencode = (content: string) =>
|
|
28
36
|
`// agency-managed: sha256=${checksum(content)}\n\n${content}`
|
|
29
37
|
|
|
30
|
-
export const managedWorkbaseOpencode =
|
|
38
|
+
export const managedWorkbaseOpencode = (root: string) =>
|
|
39
|
+
renderManagedWorkbaseOpencode(body(root))
|
|
31
40
|
|
|
32
41
|
export const canUpdateManagedWorkbaseOpencode = (content: string) => {
|
|
33
42
|
const match = content.match(managedHeaderPattern)
|