@markjaquith/agency 2.67.0 → 2.69.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 +61 -33
- package/cli-main.ts +2 -2
- package/fixtures/protocol/orchestration-recipes.json +2 -2
- package/package.json +1 -1
- package/schemas/agency-graph-v1.schema.json +2 -2
- package/src/cli-parser.test.ts +7 -7
- package/src/cli-parser.ts +11 -11
- package/src/cli.test.ts +11 -9
- package/src/commands/claim.ts +6 -6
- package/src/commands/doctor.test.ts +3 -3
- package/src/commands/init.test.ts +6 -0
- package/src/commands/integration.test.ts +20 -2
- package/src/commands/integration.ts +6 -4
- package/src/commands/work.test.ts +74 -22
- package/src/commands/work.ts +32 -38
- package/src/protocol.test.ts +2 -2
- package/src/services/ArchiveBulkService.test.ts +2 -2
- package/src/services/ClaimService.test.ts +3 -3
- package/src/services/ClaimService.ts +4 -4
- package/src/services/DoctorService.ts +24 -24
- package/src/services/IntegrationService.test.ts +158 -2
- package/src/services/IntegrationService.ts +51 -1
- package/src/services/ReadinessService.test.ts +1 -1
- package/src/services/ReviewService.test.ts +2 -2
- package/src/services/SyncService.test.ts +2 -2
- package/src/services/VcsMigrationService.test.ts +1 -1
- package/src/services/WorkbaseService.test.ts +33 -2
- package/src/services/WorkbaseService.ts +39 -3
- package/src/workbase/AGENTS.md +7 -7
- package/src/workbase/{runner-command.test.ts → agent-command.test.ts} +33 -23
- package/src/workbase/{runner-command.ts → agent-command.ts} +33 -29
- package/src/workbase/kickoff-contract.ts +4 -4
- package/src/workbase/opencode-file.ts +1 -1
- package/src/workbase/pi-extension-file.ts +158 -0
- package/src/workbase/schemas.test.ts +5 -5
- package/src/workbase/schemas.ts +8 -2
|
@@ -16,6 +16,10 @@ import {
|
|
|
16
16
|
canUpdateManagedWorkbaseOpencodeTuiPlugin,
|
|
17
17
|
managedWorkbaseOpencodeTuiPlugin,
|
|
18
18
|
} from "../workbase/opencode-tui-plugin-file"
|
|
19
|
+
import {
|
|
20
|
+
canUpdateManagedWorkbasePiExtension,
|
|
21
|
+
managedWorkbasePiExtension,
|
|
22
|
+
} from "../workbase/pi-extension-file"
|
|
19
23
|
import { IntegrationService } from "./IntegrationService"
|
|
20
24
|
|
|
21
25
|
const write = async (root: string, path: string, content: string) => {
|
|
@@ -65,6 +69,7 @@ describe("IntegrationService", () => {
|
|
|
65
69
|
"missing",
|
|
66
70
|
"missing",
|
|
67
71
|
"missing",
|
|
72
|
+
"missing",
|
|
68
73
|
])
|
|
69
74
|
expect(await Bun.file(join(root, ".agency/AGENTS.md")).exists()).toBe(false)
|
|
70
75
|
|
|
@@ -81,12 +86,18 @@ describe("IntegrationService", () => {
|
|
|
81
86
|
".opencode/tui/agency-debug.ts",
|
|
82
87
|
managedWorkbaseOpencodeTuiPlugin,
|
|
83
88
|
)
|
|
89
|
+
await write(
|
|
90
|
+
root,
|
|
91
|
+
".pi/extensions/agency-workbase.ts",
|
|
92
|
+
managedWorkbasePiExtension,
|
|
93
|
+
)
|
|
84
94
|
expect((await status(root)).files.map(({ state }) => state)).toEqual([
|
|
85
95
|
"managed",
|
|
86
96
|
"managed",
|
|
87
97
|
"managed",
|
|
88
98
|
"managed",
|
|
89
99
|
"managed",
|
|
100
|
+
"managed",
|
|
90
101
|
])
|
|
91
102
|
})
|
|
92
103
|
|
|
@@ -104,6 +115,7 @@ describe("IntegrationService", () => {
|
|
|
104
115
|
"missing",
|
|
105
116
|
"missing",
|
|
106
117
|
"missing",
|
|
118
|
+
"missing",
|
|
107
119
|
])
|
|
108
120
|
})
|
|
109
121
|
|
|
@@ -119,7 +131,7 @@ describe("IntegrationService", () => {
|
|
|
119
131
|
"call Herdr help, skill, or CLI discovery",
|
|
120
132
|
)
|
|
121
133
|
expect(managedWorkbaseAgents).toContain("exactly one final")
|
|
122
|
-
expect(managedWorkbaseAgents).toContain("leave the
|
|
134
|
+
expect(managedWorkbaseAgents).toContain("leave the agent in the")
|
|
123
135
|
})
|
|
124
136
|
|
|
125
137
|
test("generates a dynamic workbase plugin", () => {
|
|
@@ -208,6 +220,131 @@ describe("IntegrationService", () => {
|
|
|
208
220
|
).toBe(false)
|
|
209
221
|
})
|
|
210
222
|
|
|
223
|
+
test("generates a Pi extension with workbase context and checkout skills", async () => {
|
|
224
|
+
expect(managedWorkbasePiExtension).toContain(
|
|
225
|
+
'import { CONFIG_DIR_NAME, type ExtensionAPI } from "@earendil-works/pi-coding-agent"',
|
|
226
|
+
)
|
|
227
|
+
expect(managedWorkbasePiExtension).toContain('pi.on("resources_discover"')
|
|
228
|
+
expect(managedWorkbasePiExtension).toContain('pi.on("before_agent_start"')
|
|
229
|
+
expect(managedWorkbasePiExtension).toContain(
|
|
230
|
+
'join(checkout, ".agents", "skills")',
|
|
231
|
+
)
|
|
232
|
+
expect(managedWorkbasePiExtension).toContain(
|
|
233
|
+
'join(checkout, CONFIG_DIR_NAME, "skills")',
|
|
234
|
+
)
|
|
235
|
+
expect(managedWorkbasePiExtension).toContain(
|
|
236
|
+
"The complete Agency workbase is available",
|
|
237
|
+
)
|
|
238
|
+
expect(managedWorkbasePiExtension).toContain(
|
|
239
|
+
"Agency context remains the authority for writes",
|
|
240
|
+
)
|
|
241
|
+
expect(
|
|
242
|
+
canUpdateManagedWorkbasePiExtension(managedWorkbasePiExtension),
|
|
243
|
+
).toBe(true)
|
|
244
|
+
expect(
|
|
245
|
+
canUpdateManagedWorkbasePiExtension(
|
|
246
|
+
managedWorkbasePiExtension.replace("resources_discover", "discover"),
|
|
247
|
+
),
|
|
248
|
+
).toBe(false)
|
|
249
|
+
|
|
250
|
+
const taskDirectory = join(root, "tasks/example")
|
|
251
|
+
const checkoutPath = join(taskDirectory, "code/agency")
|
|
252
|
+
await write(root, ".agency/AGENTS.md", "# Managed Agency instructions\n")
|
|
253
|
+
await write(
|
|
254
|
+
root,
|
|
255
|
+
"tasks/example/TASK.md",
|
|
256
|
+
"---\nrepo: agency\nstatus: working\n---\n",
|
|
257
|
+
)
|
|
258
|
+
await write(
|
|
259
|
+
root,
|
|
260
|
+
"tasks/example/code/agency/.agents/skills/example/SKILL.md",
|
|
261
|
+
"---\nname: example\ndescription: Example.\n---\n",
|
|
262
|
+
)
|
|
263
|
+
await write(
|
|
264
|
+
root,
|
|
265
|
+
"tasks/example/code/agency/.pi/skills/pi-example/SKILL.md",
|
|
266
|
+
"---\nname: pi-example\ndescription: Pi example.\n---\n",
|
|
267
|
+
)
|
|
268
|
+
const path = join(root, ".pi/extensions/agency-workbase.ts")
|
|
269
|
+
await write(
|
|
270
|
+
root,
|
|
271
|
+
".pi/extensions/agency-workbase.ts",
|
|
272
|
+
managedWorkbasePiExtension.replace(
|
|
273
|
+
'import { CONFIG_DIR_NAME, type ExtensionAPI } from "@earendil-works/pi-coding-agent"',
|
|
274
|
+
'const CONFIG_DIR_NAME = ".pi"\ntype ExtensionAPI = any',
|
|
275
|
+
),
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
const contextResponse = JSON.stringify({
|
|
279
|
+
version: 1,
|
|
280
|
+
ok: true,
|
|
281
|
+
result: {
|
|
282
|
+
workbase: { root },
|
|
283
|
+
target: {
|
|
284
|
+
kind: "task",
|
|
285
|
+
taskId: "example",
|
|
286
|
+
path: join(taskDirectory, "TASK.md"),
|
|
287
|
+
},
|
|
288
|
+
authority: {
|
|
289
|
+
mode: "execution",
|
|
290
|
+
writable: { checkoutPath },
|
|
291
|
+
},
|
|
292
|
+
documents: { task: { data: { status: "working" } } },
|
|
293
|
+
validation: { valid: true, warnings: [] },
|
|
294
|
+
},
|
|
295
|
+
})
|
|
296
|
+
const handlers = new Map<string, (...args: any[]) => any>()
|
|
297
|
+
const pi = {
|
|
298
|
+
exec: async () => ({ code: 0, stdout: contextResponse, stderr: "" }),
|
|
299
|
+
on: (name: string, handler: (...args: any[]) => any) => {
|
|
300
|
+
handlers.set(name, handler)
|
|
301
|
+
},
|
|
302
|
+
}
|
|
303
|
+
const previousTarget = process.env.AGENCY_TARGET
|
|
304
|
+
const previousWorkbase = process.env.AGENCY_WORKBASE
|
|
305
|
+
const previousCheckout = process.env.AGENCY_WRITABLE_CHECKOUT
|
|
306
|
+
delete process.env.AGENCY_WORKBASE
|
|
307
|
+
delete process.env.AGENCY_WRITABLE_CHECKOUT
|
|
308
|
+
process.env.AGENCY_TARGET = "execution-unit:task/example"
|
|
309
|
+
try {
|
|
310
|
+
const generated = await import(
|
|
311
|
+
`${pathToFileURL(path).href}?pi-extension=${Date.now()}`
|
|
312
|
+
)
|
|
313
|
+
generated.default(pi)
|
|
314
|
+
const resources = await handlers.get("resources_discover")?.({
|
|
315
|
+
cwd: taskDirectory,
|
|
316
|
+
reason: "startup",
|
|
317
|
+
})
|
|
318
|
+
expect(resources.skillPaths).toEqual([
|
|
319
|
+
join(checkoutPath, ".agents/skills"),
|
|
320
|
+
join(checkoutPath, ".pi/skills"),
|
|
321
|
+
])
|
|
322
|
+
|
|
323
|
+
const prompt = await handlers.get("before_agent_start")?.(
|
|
324
|
+
{ systemPrompt: "Base prompt" },
|
|
325
|
+
{ cwd: taskDirectory },
|
|
326
|
+
)
|
|
327
|
+
expect(prompt.systemPrompt).toContain("# Managed Agency instructions")
|
|
328
|
+
expect(prompt.systemPrompt).toContain(
|
|
329
|
+
`The complete Agency workbase is available at ${root}`,
|
|
330
|
+
)
|
|
331
|
+
expect(prompt.systemPrompt).toContain(
|
|
332
|
+
"active worker for execution-unit:task/example",
|
|
333
|
+
)
|
|
334
|
+
expect(prompt.systemPrompt).toContain(
|
|
335
|
+
`${checkoutPath} as the default implementation directory`,
|
|
336
|
+
)
|
|
337
|
+
} finally {
|
|
338
|
+
if (previousTarget === undefined) delete process.env.AGENCY_TARGET
|
|
339
|
+
else process.env.AGENCY_TARGET = previousTarget
|
|
340
|
+
if (previousWorkbase === undefined) delete process.env.AGENCY_WORKBASE
|
|
341
|
+
else process.env.AGENCY_WORKBASE = previousWorkbase
|
|
342
|
+
if (previousCheckout === undefined)
|
|
343
|
+
delete process.env.AGENCY_WRITABLE_CHECKOUT
|
|
344
|
+
else process.env.AGENCY_WRITABLE_CHECKOUT = previousCheckout
|
|
345
|
+
}
|
|
346
|
+
})
|
|
347
|
+
|
|
211
348
|
const testWorkerIdentity = async ({
|
|
212
349
|
target,
|
|
213
350
|
launchTarget,
|
|
@@ -640,7 +777,7 @@ describe("IntegrationService", () => {
|
|
|
640
777
|
expect(config.agent.agency.hidden).toBeUndefined()
|
|
641
778
|
expect(config.agent.agency.steps).toBeUndefined()
|
|
642
779
|
expect(config.agent.agency.prompt).toContain(
|
|
643
|
-
"verify that the
|
|
780
|
+
"verify that the agent started successfully",
|
|
644
781
|
)
|
|
645
782
|
expect(config.agent.agency.prompt).toContain(
|
|
646
783
|
"return without waiting for the task to finish",
|
|
@@ -733,6 +870,7 @@ describe("IntegrationService", () => {
|
|
|
733
870
|
{ name: "opencode-plugin", state: "managed", changed: true },
|
|
734
871
|
{ name: "opencode-tui", state: "managed", changed: true },
|
|
735
872
|
{ name: "opencode-tui-plugin", state: "managed", changed: true },
|
|
873
|
+
{ name: "pi-extension", state: "managed", changed: true },
|
|
736
874
|
])
|
|
737
875
|
expect(await Bun.file(join(root, "AGENTS.md")).text()).toBe(
|
|
738
876
|
customRootAgents,
|
|
@@ -757,6 +895,9 @@ describe("IntegrationService", () => {
|
|
|
757
895
|
expect(
|
|
758
896
|
await Bun.file(join(root, ".opencode/tui/agency-debug.ts")).text(),
|
|
759
897
|
).toBe(managedWorkbaseOpencodeTuiPlugin)
|
|
898
|
+
expect(
|
|
899
|
+
await Bun.file(join(root, ".pi/extensions/agency-workbase.ts")).text(),
|
|
900
|
+
).toBe(managedWorkbasePiExtension)
|
|
760
901
|
|
|
761
902
|
await unlink(join(root, ".agency/AGENTS.md"))
|
|
762
903
|
const second = await sync(root)
|
|
@@ -850,6 +991,21 @@ describe("IntegrationService", () => {
|
|
|
850
991
|
expect(await Bun.file(legacyPath).exists()).toBe(false)
|
|
851
992
|
})
|
|
852
993
|
|
|
994
|
+
test("preserves a user-owned Pi extension", async () => {
|
|
995
|
+
const custom = "export default function customPiExtension() {}\n"
|
|
996
|
+
await write(root, ".pi/extensions/agency-workbase.ts", custom)
|
|
997
|
+
|
|
998
|
+
const result = await sync(root)
|
|
999
|
+
expect(result.files[5]).toMatchObject({
|
|
1000
|
+
name: "pi-extension",
|
|
1001
|
+
state: "customized",
|
|
1002
|
+
changed: false,
|
|
1003
|
+
})
|
|
1004
|
+
expect(
|
|
1005
|
+
await Bun.file(join(root, ".pi/extensions/agency-workbase.ts")).text(),
|
|
1006
|
+
).toBe(custom)
|
|
1007
|
+
})
|
|
1008
|
+
|
|
853
1009
|
test("preserves user-owned TUI config and diagnostic plugin", async () => {
|
|
854
1010
|
const customConfig = '{"theme":"custom"}\n'
|
|
855
1011
|
const customPlugin =
|
|
@@ -23,6 +23,10 @@ import {
|
|
|
23
23
|
canUpdateManagedWorkbaseOpencodeTuiPlugin,
|
|
24
24
|
managedWorkbaseOpencodeTuiPlugin,
|
|
25
25
|
} from "../workbase/opencode-tui-plugin-file"
|
|
26
|
+
import {
|
|
27
|
+
canUpdateManagedWorkbasePiExtension,
|
|
28
|
+
managedWorkbasePiExtension,
|
|
29
|
+
} from "../workbase/pi-extension-file"
|
|
26
30
|
|
|
27
31
|
type IntegrationFileState = "managed" | "customized" | "missing" | "drifted"
|
|
28
32
|
|
|
@@ -33,6 +37,7 @@ interface IntegrationFileStatus {
|
|
|
33
37
|
| "opencode-plugin"
|
|
34
38
|
| "opencode-tui"
|
|
35
39
|
| "opencode-tui-plugin"
|
|
40
|
+
| "pi-extension"
|
|
36
41
|
readonly path: string
|
|
37
42
|
readonly state: IntegrationFileState
|
|
38
43
|
readonly diagnostic: string
|
|
@@ -131,6 +136,26 @@ const describe = (
|
|
|
131
136
|
"Run 'agency integration sync' to install /agency-debug.",
|
|
132
137
|
}
|
|
133
138
|
}
|
|
139
|
+
if (name === "pi-extension") {
|
|
140
|
+
return state === "managed"
|
|
141
|
+
? {
|
|
142
|
+
diagnostic:
|
|
143
|
+
"Agency's managed Pi extension provides whole-workbase context and exposes writable-checkout skills.",
|
|
144
|
+
remediation: null,
|
|
145
|
+
}
|
|
146
|
+
: state === "customized"
|
|
147
|
+
? {
|
|
148
|
+
diagnostic:
|
|
149
|
+
"A user-owned Pi workbase extension is present and was preserved.",
|
|
150
|
+
remediation: null,
|
|
151
|
+
}
|
|
152
|
+
: {
|
|
153
|
+
diagnostic:
|
|
154
|
+
"The managed Pi workbase extension needs synchronization.",
|
|
155
|
+
remediation:
|
|
156
|
+
"Run 'agency integration sync' to provide workbase context and expose writable-checkout skills in Pi.",
|
|
157
|
+
}
|
|
158
|
+
}
|
|
134
159
|
|
|
135
160
|
return state === "missing" || state === "drifted"
|
|
136
161
|
? {
|
|
@@ -190,6 +215,12 @@ const inspect = (root: string) =>
|
|
|
190
215
|
"agency-repository-skills.ts",
|
|
191
216
|
)
|
|
192
217
|
const tuiPluginPath = join(opencodeDirectory, "tui", "agency-debug.ts")
|
|
218
|
+
const piExtensionPath = join(
|
|
219
|
+
root,
|
|
220
|
+
".pi",
|
|
221
|
+
"extensions",
|
|
222
|
+
"agency-workbase.ts",
|
|
223
|
+
)
|
|
193
224
|
const files: IntegrationFileStatus[] = []
|
|
194
225
|
|
|
195
226
|
files.push(
|
|
@@ -294,6 +325,22 @@ const inspect = (root: string) =>
|
|
|
294
325
|
files.push(fileStatus("opencode-tui-plugin", tuiPluginPath, "missing"))
|
|
295
326
|
}
|
|
296
327
|
|
|
328
|
+
if ((yield* fs.readSymlinkTarget(piExtensionPath)) !== null) {
|
|
329
|
+
files.push(fileStatus("pi-extension", piExtensionPath, "customized"))
|
|
330
|
+
} else if (yield* fs.exists(piExtensionPath)) {
|
|
331
|
+
files.push(
|
|
332
|
+
classify(
|
|
333
|
+
"pi-extension",
|
|
334
|
+
piExtensionPath,
|
|
335
|
+
yield* fs.readFile(piExtensionPath),
|
|
336
|
+
managedWorkbasePiExtension,
|
|
337
|
+
canUpdateManagedWorkbasePiExtension,
|
|
338
|
+
),
|
|
339
|
+
)
|
|
340
|
+
} else {
|
|
341
|
+
files.push(fileStatus("pi-extension", piExtensionPath, "missing"))
|
|
342
|
+
}
|
|
343
|
+
|
|
297
344
|
return files
|
|
298
345
|
})
|
|
299
346
|
|
|
@@ -389,12 +436,15 @@ export class IntegrationService extends Effect.Service<IntegrationService>()(
|
|
|
389
436
|
} else if (status.name === "opencode-tui") {
|
|
390
437
|
yield* fs.createDirectory(join(root, ".opencode"))
|
|
391
438
|
yield* fs.writeFile(status.path, managedWorkbaseOpencodeTui)
|
|
392
|
-
} else {
|
|
439
|
+
} else if (status.name === "opencode-tui-plugin") {
|
|
393
440
|
yield* fs.createDirectory(join(root, ".opencode", "tui"))
|
|
394
441
|
yield* fs.writeFile(
|
|
395
442
|
status.path,
|
|
396
443
|
managedWorkbaseOpencodeTuiPlugin,
|
|
397
444
|
)
|
|
445
|
+
} else {
|
|
446
|
+
yield* fs.createDirectory(join(root, ".pi", "extensions"))
|
|
447
|
+
yield* fs.writeFile(status.path, managedWorkbasePiExtension)
|
|
398
448
|
}
|
|
399
449
|
}
|
|
400
450
|
files.push({
|
|
@@ -275,7 +275,7 @@ describe("ReviewService", () => {
|
|
|
275
275
|
{
|
|
276
276
|
taskId: "review",
|
|
277
277
|
claimant: "reviewer",
|
|
278
|
-
|
|
278
|
+
agent: "opencode",
|
|
279
279
|
sessionId: "session",
|
|
280
280
|
revision: task.revision,
|
|
281
281
|
},
|
|
@@ -311,7 +311,7 @@ describe("ReviewService", () => {
|
|
|
311
311
|
{
|
|
312
312
|
taskId: "review",
|
|
313
313
|
claimant: "reviewer",
|
|
314
|
-
|
|
314
|
+
agent: "opencode",
|
|
315
315
|
sessionId: "active",
|
|
316
316
|
revision: task.revision,
|
|
317
317
|
},
|
|
@@ -238,7 +238,7 @@ pr: null
|
|
|
238
238
|
{
|
|
239
239
|
taskId: "example",
|
|
240
240
|
claimant: "orchestrator",
|
|
241
|
-
|
|
241
|
+
agent: "agent",
|
|
242
242
|
sessionId: "session-1",
|
|
243
243
|
revision: inspected.revision,
|
|
244
244
|
expiresAt: "2099-01-01T00:00:00.000Z",
|
|
@@ -523,7 +523,7 @@ process.stdout.write(${JSON.stringify(JSON.stringify(record))})
|
|
|
523
523
|
{
|
|
524
524
|
taskId: "finished-claim",
|
|
525
525
|
claimant: "orchestrator",
|
|
526
|
-
|
|
526
|
+
agent: "agent",
|
|
527
527
|
sessionId: "session-1",
|
|
528
528
|
revision: initial.revision,
|
|
529
529
|
},
|
|
@@ -41,6 +41,37 @@ describe("WorkbaseService", () => {
|
|
|
41
41
|
).toBe(false)
|
|
42
42
|
})
|
|
43
43
|
|
|
44
|
+
test("loads and validates the global agent", async () => {
|
|
45
|
+
const configDirectory = join(root, "config")
|
|
46
|
+
await write(
|
|
47
|
+
configDirectory,
|
|
48
|
+
"agency/agency.json",
|
|
49
|
+
JSON.stringify({ agent: "pi" }),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const config = await runTestEffect(
|
|
53
|
+
WorkbaseService.pipe(
|
|
54
|
+
Effect.flatMap((service) => service.loadGlobalConfig(configDirectory)),
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
expect(config).toEqual({ agent: "pi" })
|
|
58
|
+
|
|
59
|
+
await write(
|
|
60
|
+
configDirectory,
|
|
61
|
+
"agency/agency.json",
|
|
62
|
+
JSON.stringify({ agent: "codex" }),
|
|
63
|
+
)
|
|
64
|
+
await expect(
|
|
65
|
+
runTestEffect(
|
|
66
|
+
WorkbaseService.pipe(
|
|
67
|
+
Effect.flatMap((service) =>
|
|
68
|
+
service.loadGlobalConfig(configDirectory),
|
|
69
|
+
),
|
|
70
|
+
),
|
|
71
|
+
),
|
|
72
|
+
).rejects.toThrow("Invalid global Agency configuration")
|
|
73
|
+
})
|
|
74
|
+
|
|
44
75
|
test("treats declared but missing repositories as valid aliases", async () => {
|
|
45
76
|
await write(
|
|
46
77
|
root,
|
|
@@ -415,13 +446,13 @@ status: done
|
|
|
415
446
|
).rejects.toThrow("Repository 'agency'")
|
|
416
447
|
})
|
|
417
448
|
|
|
418
|
-
test("rejects an unknown
|
|
449
|
+
test("rejects an unknown agent command placeholder", async () => {
|
|
419
450
|
await write(
|
|
420
451
|
root,
|
|
421
452
|
"agency.json",
|
|
422
453
|
JSON.stringify({
|
|
423
454
|
version: 2,
|
|
424
|
-
|
|
455
|
+
agents: { custom: { command: ["agent", "{unknown}"] } },
|
|
425
456
|
}),
|
|
426
457
|
)
|
|
427
458
|
|
|
@@ -8,6 +8,7 @@ import { parseFrontmatter } from "../workbase/frontmatter"
|
|
|
8
8
|
import {
|
|
9
9
|
EntityId,
|
|
10
10
|
EpicFrontmatter,
|
|
11
|
+
GlobalConfig,
|
|
11
12
|
LegacyWorkbaseRegistry,
|
|
12
13
|
PhaseFrontmatter,
|
|
13
14
|
TaskFrontmatter,
|
|
@@ -22,7 +23,7 @@ import {
|
|
|
22
23
|
import { validateWorktreeCreateCommand } from "../workbase/worktree-command"
|
|
23
24
|
import { validateWorkspaceCreateCommand } from "../workbase/workspace-command"
|
|
24
25
|
import { validatePostCheckoutCommand } from "../workbase/checkout-command"
|
|
25
|
-
import {
|
|
26
|
+
import { validateAgents } from "../workbase/agent-command"
|
|
26
27
|
import { findDependencyCycles } from "../workbase/dependency-graph"
|
|
27
28
|
import { validateDelivery } from "../workbase/delivery-command"
|
|
28
29
|
import { preferredVersionControl } from "../workbase/version-control"
|
|
@@ -90,6 +91,15 @@ const registryPath = (configDirectory?: string) =>
|
|
|
90
91
|
"workbases.json",
|
|
91
92
|
)
|
|
92
93
|
|
|
94
|
+
const globalConfigPath = (configDirectory?: string) =>
|
|
95
|
+
join(
|
|
96
|
+
configDirectory ||
|
|
97
|
+
process.env.XDG_CONFIG_HOME ||
|
|
98
|
+
join(homedir(), ".config"),
|
|
99
|
+
"agency",
|
|
100
|
+
"agency.json",
|
|
101
|
+
)
|
|
102
|
+
|
|
93
103
|
const registrationId = (path: string) =>
|
|
94
104
|
`wb-${new Bun.CryptoHasher("sha256").update(path).digest("hex").slice(0, 12)}`
|
|
95
105
|
|
|
@@ -178,6 +188,32 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
178
188
|
"WorkbaseService",
|
|
179
189
|
{
|
|
180
190
|
sync: () => ({
|
|
191
|
+
loadGlobalConfig: (configDirectory?: string) =>
|
|
192
|
+
Effect.gen(function* () {
|
|
193
|
+
const fs = yield* FileSystemService
|
|
194
|
+
const path = globalConfigPath(configDirectory)
|
|
195
|
+
if (!(yield* fs.exists(path))) return {}
|
|
196
|
+
const content = yield* fs.readFile(path)
|
|
197
|
+
let input: unknown
|
|
198
|
+
try {
|
|
199
|
+
input = JSON.parse(content)
|
|
200
|
+
} catch (cause) {
|
|
201
|
+
return yield* new WorkbaseConfigError({
|
|
202
|
+
path,
|
|
203
|
+
message: `Invalid JSON in global Agency configuration ${path}`,
|
|
204
|
+
cause,
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
const decoded = decode(GlobalConfig, input)
|
|
208
|
+
if (!decoded.success) {
|
|
209
|
+
return yield* new WorkbaseConfigError({
|
|
210
|
+
path,
|
|
211
|
+
message: `Invalid global Agency configuration in ${path}:\n${decoded.error}`,
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
return decoded.value
|
|
215
|
+
}),
|
|
216
|
+
|
|
181
217
|
initialize: (path: string = process.cwd()) =>
|
|
182
218
|
Effect.gen(function* () {
|
|
183
219
|
const fs = yield* FileSystemService
|
|
@@ -309,7 +345,7 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
309
345
|
}
|
|
310
346
|
}
|
|
311
347
|
try {
|
|
312
|
-
|
|
348
|
+
validateAgents(decoded.value.agents)
|
|
313
349
|
validateDelivery(decoded.value.delivery)
|
|
314
350
|
} catch (cause) {
|
|
315
351
|
return yield* new WorkbaseConfigError({
|
|
@@ -317,7 +353,7 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
317
353
|
message:
|
|
318
354
|
cause instanceof Error
|
|
319
355
|
? cause.message
|
|
320
|
-
: "Invalid
|
|
356
|
+
: "Invalid agent configuration",
|
|
321
357
|
})
|
|
322
358
|
}
|
|
323
359
|
return current
|
package/src/workbase/AGENTS.md
CHANGED
|
@@ -121,7 +121,7 @@ Do not rediscover commands that this recipe or a known-current
|
|
|
121
121
|
prepare the task with
|
|
122
122
|
`agency work prepare <slug> --evidence <creation-json> --dry-run --json`, then
|
|
123
123
|
execute the returned plan through `task-document-split` to prepare the
|
|
124
|
-
checkout and open or reuse the background tab. Stop before `
|
|
124
|
+
checkout and open or reuse the background tab. Stop before `agent-start`.
|
|
125
125
|
3. For work/launch/start/kickoff intent, run that same preflight and execute its
|
|
126
126
|
ordered kickoff steps. The plan owns worktree dry-run/preparation, a
|
|
127
127
|
retry-safe background Herdr tab, the side-by-side task document,
|
|
@@ -131,8 +131,8 @@ Do not rediscover commands that this recipe or a known-current
|
|
|
131
131
|
`agency-kickoff-v1` capability, execute the supplied actions directly. Do not
|
|
132
132
|
call Herdr help, skill, or CLI discovery. If capability/version evidence is
|
|
133
133
|
absent or stale, discovery is the compatibility path; then resume the same
|
|
134
|
-
idempotency key rather than creating another tab, checkout, or
|
|
135
|
-
5. After the one final context verification succeeds, leave the
|
|
134
|
+
idempotency key rather than creating another tab, checkout, or agent.
|
|
135
|
+
5. After the one final context verification succeeds, leave the agent in the
|
|
136
136
|
background and stop. Do not inspect, poll, or babysit it unless the user asks.
|
|
137
137
|
|
|
138
138
|
Validation evidence is a local, auditable optimization, not authority. Preflight
|
|
@@ -153,12 +153,12 @@ IDs and remediation commands before retrying.
|
|
|
153
153
|
|
|
154
154
|
`agency work` is the human launch flow: it reconciles managed integration,
|
|
155
155
|
selects work, checks readiness, prepares checkouts, marks execution work
|
|
156
|
-
`working` without creating a claim, and starts the
|
|
156
|
+
`working` without creating a claim, and starts the agent. Epic and multi-phase
|
|
157
157
|
task launches remain orchestration-only. External orchestrators instead claim
|
|
158
|
-
an execution unit, launch and monitor their
|
|
158
|
+
an execution unit, launch and monitor their agent separately, and finish or
|
|
159
159
|
release the claim with the current document revision.
|
|
160
160
|
|
|
161
|
-
An Agency-launched
|
|
161
|
+
An Agency-launched agent receives process-local worker identity through both
|
|
162
162
|
the `AGENCY_SESSION_ID` and `AGENCY_TARGET` environment variables and a generated
|
|
163
163
|
prompt beginning `Agency worker launch target: <target>.` Treat either form as
|
|
164
164
|
launch evidence only after `agency context . --json` confirms the same target,
|
|
@@ -166,7 +166,7 @@ document paths, valid context, and expected write authority. Once confirmed,
|
|
|
166
166
|
perform the assigned work directly and never invoke `agency work` to start the
|
|
167
167
|
same target again.
|
|
168
168
|
|
|
169
|
-
Some
|
|
169
|
+
Some agent clients attach to a long-lived process and may not preserve launch
|
|
170
170
|
environment variables. If the variables and prompt marker are absent, fail safe
|
|
171
171
|
when the initial instruction is a generated `Start`, `Continue`, or `Work on`
|
|
172
172
|
prompt whose absolute document paths match the current directory and the active,
|