@markjaquith/agency 3.2.13 → 3.2.15

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.
Files changed (69) hide show
  1. package/package.json +7 -2
  2. package/src/workbase/schemas.ts +12 -0
  3. package/fixtures/protocol/orchestration-recipes.json +0 -53
  4. package/pi-extensions/agency.test.ts +0 -117
  5. package/src/check-commits.test.ts +0 -58
  6. package/src/cli-parser.test.ts +0 -945
  7. package/src/cli.test.ts +0 -1889
  8. package/src/commands/act.test.ts +0 -418
  9. package/src/commands/archive.test.ts +0 -236
  10. package/src/commands/context.test.ts +0 -665
  11. package/src/commands/description.test.ts +0 -103
  12. package/src/commands/doctor.test.ts +0 -185
  13. package/src/commands/epic.test.ts +0 -196
  14. package/src/commands/init.test.ts +0 -117
  15. package/src/commands/integration.test.ts +0 -165
  16. package/src/commands/pr.test.ts +0 -248
  17. package/src/commands/push.test.ts +0 -56
  18. package/src/commands/read-only.test.ts +0 -161
  19. package/src/commands/repo.test.ts +0 -199
  20. package/src/commands/restore.test.ts +0 -106
  21. package/src/commands/status.test.ts +0 -113
  22. package/src/commands/sync.test.ts +0 -200
  23. package/src/commands/task-phase.test.ts +0 -410
  24. package/src/commands/task.test.ts +0 -281
  25. package/src/commands/validate.test.ts +0 -186
  26. package/src/commands/work.test.ts +0 -1375
  27. package/src/commands/workbase.test.ts +0 -170
  28. package/src/graph-schema.test.ts +0 -124
  29. package/src/protocol.test.ts +0 -163
  30. package/src/readiness.test.ts +0 -105
  31. package/src/services/ArchiveBulkService.test.ts +0 -384
  32. package/src/services/ArchiveService.test.ts +0 -1092
  33. package/src/services/EpicService.test.ts +0 -74
  34. package/src/services/FileSystemService.test.ts +0 -81
  35. package/src/services/GraphMutationService.test.ts +0 -486
  36. package/src/services/GraphService.test.ts +0 -494
  37. package/src/services/IntegrationService.test.ts +0 -1091
  38. package/src/services/LifecycleTransaction.test.ts +0 -145
  39. package/src/services/PullRequestService.test.ts +0 -716
  40. package/src/services/PushService.test.ts +0 -408
  41. package/src/services/ReadinessService.test.ts +0 -290
  42. package/src/services/RepositoryService.test.ts +0 -789
  43. package/src/services/ReviewService.test.ts +0 -408
  44. package/src/services/SyncService.test.ts +0 -1377
  45. package/src/services/TaskPhaseService.test.ts +0 -852
  46. package/src/services/VersionControlService.test.ts +0 -62
  47. package/src/services/WorkbaseService.test.ts +0 -910
  48. package/src/services/WorktreeLock.test.ts +0 -205
  49. package/src/services/WorktreePerformance.test.ts +0 -71
  50. package/src/services/WorktreeService.test.ts +0 -2292
  51. package/src/test-utils.ts +0 -110
  52. package/src/usage-log.test.ts +0 -188
  53. package/src/utils/chooser.test.ts +0 -192
  54. package/src/utils/effect.test.ts +0 -30
  55. package/src/utils/interactive.pty.test.ts +0 -128
  56. package/src/utils/interactive.test.tsx +0 -860
  57. package/src/utils/process.test.ts +0 -132
  58. package/src/utils/progress.test.ts +0 -37
  59. package/src/work-view.test.ts +0 -151
  60. package/src/workbase/agent-command.test.ts +0 -128
  61. package/src/workbase/checkout-command.test.ts +0 -62
  62. package/src/workbase/delivery-command.test.ts +0 -180
  63. package/src/workbase/dependency-graph.test.ts +0 -50
  64. package/src/workbase/execution-contract.test.ts +0 -161
  65. package/src/workbase/frontmatter.test.ts +0 -67
  66. package/src/workbase/repository-reference.test.ts +0 -25
  67. package/src/workbase/schemas.test.ts +0 -543
  68. package/src/workbase/work-target.test.ts +0 -108
  69. package/src/workbase/worktree-command.test.ts +0 -61
@@ -1,1091 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
- import { Effect } from "effect"
3
- import { createHash } from "node:crypto"
4
- import { mkdir, stat, symlink, unlink, utimes } from "node:fs/promises"
5
- import { dirname, join } from "node:path"
6
- import { pathToFileURL } from "node:url"
7
- import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
8
- import { managedWorkbaseAgents } from "../workbase/agents-file"
9
- import { managedWorkbaseOpencode } from "../workbase/opencode-file"
10
- import {
11
- canUpdateManagedWorkbaseOpencodePlugin,
12
- managedWorkbaseOpencodePlugin,
13
- } from "../workbase/opencode-plugin-file"
14
- import { managedWorkbaseOpencodeTui } from "../workbase/opencode-tui-file"
15
- import {
16
- canUpdateManagedWorkbaseOpencodeTuiPlugin,
17
- managedWorkbaseOpencodeTuiPlugin,
18
- } from "../workbase/opencode-tui-plugin-file"
19
- import { IntegrationService } from "./IntegrationService"
20
- import { FileSystemService } from "./FileSystemService"
21
-
22
- const write = async (root: string, path: string, content: string) => {
23
- const fullPath = join(root, path)
24
- await mkdir(dirname(fullPath), { recursive: true })
25
- await Bun.write(fullPath, content)
26
- }
27
-
28
- const managed = (prefix: string, body: string, suffix = "") => {
29
- const checksum = createHash("sha256").update(body).digest("hex")
30
- return `${prefix}${checksum}${suffix}\n\n${body}`
31
- }
32
-
33
- const managedBody = (content: string) =>
34
- content.slice(content.indexOf("\n\n") + 2)
35
-
36
- const legacyCommand = (content: string) =>
37
- content.replace(
38
- /^---\n/,
39
- `---\n# agency-managed: sha256=${createHash("sha256").update(content).digest("hex")}\n`,
40
- )
41
-
42
- const status = (root: string) =>
43
- runTestEffect(
44
- IntegrationService.pipe(Effect.flatMap((service) => service.status(root))),
45
- )
46
-
47
- const sync = (root: string) =>
48
- runTestEffect(
49
- IntegrationService.pipe(Effect.flatMap((service) => service.sync(root))),
50
- )
51
-
52
- describe("IntegrationService", () => {
53
- let root: string
54
-
55
- beforeEach(async () => {
56
- root = await createTempDir()
57
- await write(root, "agency.json", '{"version":2}\n')
58
- })
59
-
60
- afterEach(async () => cleanupTempDir(root))
61
-
62
- test("reports missing and current managed files without writing", async () => {
63
- expect((await status(root)).files.map(({ state }) => state)).toEqual([
64
- "missing",
65
- "missing",
66
- "missing",
67
- "missing",
68
- "missing",
69
- ])
70
- expect(await Bun.file(join(root, ".agency/AGENTS.md")).exists()).toBe(false)
71
-
72
- await write(root, ".agency/AGENTS.md", managedWorkbaseAgents)
73
- await write(root, ".opencode/opencode.jsonc", managedWorkbaseOpencode)
74
- await write(
75
- root,
76
- ".opencode/plugins/agency-repository-skills.ts",
77
- managedWorkbaseOpencodePlugin,
78
- )
79
- await write(root, ".opencode/tui.jsonc", managedWorkbaseOpencodeTui)
80
- await write(
81
- root,
82
- ".opencode/tui/agency-debug.ts",
83
- managedWorkbaseOpencodeTuiPlugin,
84
- )
85
- expect((await status(root)).files.map(({ state }) => state)).toEqual([
86
- "managed",
87
- "managed",
88
- "managed",
89
- "managed",
90
- "managed",
91
- ])
92
- })
93
-
94
- test("inspects each integration path once per status call", async () => {
95
- const service = await Effect.runPromise(
96
- Effect.provide(FileSystemService, FileSystemService.Default),
97
- )
98
- const inspected = new Map<string, number>()
99
- const instrumented = {
100
- ...service,
101
- inspectFile: (path: string) => {
102
- inspected.set(path, (inspected.get(path) ?? 0) + 1)
103
- return service.inspectFile(path)
104
- },
105
- }
106
-
107
- await runTestEffect(
108
- IntegrationService.pipe(
109
- Effect.flatMap((integration) => integration.statusRoot(root)),
110
- Effect.provideService(FileSystemService, instrumented),
111
- ),
112
- )
113
-
114
- expect(inspected.size).toBe(8)
115
- expect([...inspected.values()]).toEqual(Array(8).fill(1))
116
- })
117
-
118
- test("inspects integration and legacy paths once per synchronized call", async () => {
119
- const service = await Effect.runPromise(
120
- Effect.provide(FileSystemService, FileSystemService.Default),
121
- )
122
- const inspected = new Map<string, number>()
123
- const instrumented = {
124
- ...service,
125
- inspectFile: (path: string) => {
126
- inspected.set(path, (inspected.get(path) ?? 0) + 1)
127
- return service.inspectFile(path)
128
- },
129
- }
130
-
131
- await runTestEffect(
132
- IntegrationService.pipe(
133
- Effect.flatMap((integration) => integration.syncRoot(root)),
134
- Effect.provideService(FileSystemService, instrumented),
135
- ),
136
- )
137
-
138
- expect(inspected.size).toBe(11)
139
- expect([...inspected.values()]).toEqual(Array(11).fill(1))
140
- })
141
-
142
- test("reports customized and checksum-safe drifted files", async () => {
143
- await write(root, ".agency/AGENTS.md", "# Custom instructions\n")
144
- await write(
145
- root,
146
- ".opencode/opencode.jsonc",
147
- managed("// agency-managed: sha256=", '{"references":{}}\n'),
148
- )
149
-
150
- expect((await status(root)).files.map(({ state }) => state)).toEqual([
151
- "customized",
152
- "drifted",
153
- "missing",
154
- "missing",
155
- "missing",
156
- ])
157
- })
158
-
159
- test("generates the complete Agency command fast paths with precedence", () => {
160
- const body = managedBody(managedWorkbaseAgents)
161
-
162
- expect(body.indexOf("## Command Fast Paths")).toBeLessThan(
163
- body.indexOf("## Bootstrap"),
164
- )
165
- expect(managedWorkbaseAgents).toContain(
166
- "take precedence over separately installed Agency skill guidance",
167
- )
168
- for (const recipe of [
169
- "Create a single-phase task only",
170
- "agency work prepare <slug> --evidence",
171
- "Materialize an existing execution unit without starting it",
172
- "agency sync <task> [phase] --json",
173
- "agency phase create <task> <new-phase> --first-phase <existing-phase>",
174
- "agency archive task <task> --dry-run --json",
175
- "agency task create <slug> --review <alias>",
176
- "agency status --json",
177
- "agency task status <task> dropped --if-revision <revision> --json",
178
- "Continue already materialized work",
179
- "agency pr create <task> [phase]",
180
- "agency task status <task> done --if-revision <revision> --no-pull-request",
181
- "agency task create <slug> --multi-phase",
182
- "agency task handoff <investigation-task> <new-task>",
183
- "agency review refresh <task> --if-revision <revision> --json",
184
- ]) {
185
- expect(managedWorkbaseAgents).toContain(recipe)
186
- }
187
- expect(managedWorkbaseAgents).toContain("agency push --json")
188
- expect(managedWorkbaseAgents).toContain("agency-execution-v1")
189
- expect(managedWorkbaseAgents).toContain("agency context . --json")
190
- expect(managedWorkbaseAgents).toContain(
191
- "Pass `--full` only when document prose or low-level VCS details are needed",
192
- )
193
- expect(managedWorkbaseAgents).not.toContain(
194
- "agency context . --full --json",
195
- )
196
- expect(managedWorkbaseAgents).toContain(
197
- "Never pass `--work` or `--auto` to `agency task create`",
198
- )
199
- expect(managedWorkbaseAgents).toContain(
200
- "Use `agency <command> --help` only as a recovery",
201
- )
202
- expect(managedWorkbaseAgents).not.toContain("`agency --help`")
203
- expect(managedWorkbaseAgents).not.toContain(
204
- "`agency worktree prepare <task>",
205
- )
206
- })
207
-
208
- test("generates a dynamic workbase plugin", () => {
209
- expect(managedWorkbaseOpencodePlugin).toContain(
210
- "process.env.AGENCY_WRITABLE_CHECKOUT",
211
- )
212
- expect(managedWorkbaseOpencodePlugin).toContain(
213
- 'import type { Plugin, PluginModule } from "@opencode-ai/plugin/v1"',
214
- )
215
- expect(managedWorkbaseOpencodePlugin).toContain(
216
- "export const AgencyPlugin = plugin",
217
- )
218
- expect(managedWorkbaseOpencodePlugin).toContain('id: "agency"')
219
- expect(managedWorkbaseOpencodePlugin).toContain("setup,")
220
- expect(managedWorkbaseOpencodePlugin).toContain("server: plugin")
221
- expect(managedWorkbaseOpencodePlugin).toContain(
222
- '["agency", "context", ".", "--compact", "--json"]',
223
- )
224
- expect(managedWorkbaseOpencodePlugin).toContain(
225
- 'join(checkout, ".claude", "skills")',
226
- )
227
- expect(managedWorkbaseOpencodePlugin).toContain(
228
- 'join(checkout, ".agents", "skills")',
229
- )
230
- expect(managedWorkbaseOpencodePlugin).toContain(
231
- 'join(checkout, ".opencode", "skills")',
232
- )
233
- expect(managedWorkbaseOpencodePlugin).toContain(
234
- 'config.permission.external_directory = { [join(root, "*")]: "allow" }',
235
- )
236
- expect(managedWorkbaseOpencodePlugin).toContain(
237
- "config.skills.paths = [...new Set",
238
- )
239
- expect(managedWorkbaseOpencodePlugin).toContain('"chat.message"')
240
- expect(managedWorkbaseOpencodePlugin).toContain(
241
- "if (!Array.isArray(parts)) return",
242
- )
243
- expect(managedWorkbaseOpencodePlugin).toContain(
244
- '"experimental.chat.system.transform"',
245
- )
246
- expect(managedWorkbaseOpencodePlugin).toContain('"shell.env"')
247
- expect(managedWorkbaseOpencodePlugin).toContain(
248
- "if (context?.target !== launchTarget) return",
249
- )
250
- expect(managedWorkbaseOpencodePlugin).toContain(
251
- "agencyContext(directory, false)",
252
- )
253
- expect(managedWorkbaseOpencodePlugin).toContain(
254
- "result.validation?.valid !== true",
255
- )
256
- expect(managedWorkbaseOpencodePlugin).toContain(
257
- "dirname(document) !== resolve(directory)",
258
- )
259
- expect(managedWorkbaseOpencodePlugin).toContain(
260
- "!result.authority?.writable?.checkoutPath",
261
- )
262
- expect(managedWorkbaseOpencodePlugin).toContain('status !== "working"')
263
- expect(managedWorkbaseOpencodePlugin).toContain(
264
- 'output.env.AGENCY_INVOCATION_SOURCE = "agent"',
265
- )
266
- expect(managedWorkbaseOpencodePlugin).toContain(
267
- "output.env.AGENCY_SESSION_ID = sessionID",
268
- )
269
- expect(managedWorkbaseOpencodePlugin).toContain(
270
- "Do not invoke agency work for this target",
271
- )
272
- expect(managedWorkbaseOpencodePlugin).toContain(
273
- "as the default implementation directory",
274
- )
275
- expect(managedWorkbaseOpencodePlugin).toContain(
276
- "Set each tool's working directory to that checkout when supported",
277
- )
278
- expect(managedWorkbaseOpencodePlugin).toContain(
279
- "Run Agency lifecycle and context commands from the task or phase directory",
280
- )
281
- expect(managedWorkbaseOpencodePlugin).toContain(
282
- "reference checkouts reported by Agency context are read-only",
283
- )
284
- expect(managedWorkbaseOpencodePlugin).toContain(
285
- ".map((path) => `${path}${sep}.`)",
286
- )
287
- expect(
288
- canUpdateManagedWorkbaseOpencodePlugin(managedWorkbaseOpencodePlugin),
289
- ).toBe(true)
290
- expect(
291
- canUpdateManagedWorkbaseOpencodePlugin(
292
- managedWorkbaseOpencodePlugin.replace("config.skills ??= {}", ""),
293
- ),
294
- ).toBe(false)
295
- })
296
-
297
- const testWorkerIdentity = async ({
298
- target,
299
- launchTarget,
300
- phase,
301
- }: {
302
- readonly target:
303
- | { readonly kind: "task"; readonly taskId: string }
304
- | {
305
- readonly kind: "phase"
306
- readonly taskId: string
307
- readonly phaseId: string
308
- }
309
- readonly launchTarget: string
310
- readonly phase?: string
311
- }) => {
312
- const path = join(root, "agency-repository-skills.ts")
313
- const checkoutPath = join(root, "code/agency")
314
- const contextResponse = JSON.stringify({
315
- version: 1,
316
- ok: true,
317
- result: {
318
- workbase: { root },
319
- target: {
320
- ...target,
321
- path: join(root, phase ? "PHASE.md" : "TASK.md"),
322
- },
323
- authority: {
324
- mode: "execution",
325
- writable: { checkoutPath },
326
- },
327
- documents: phase
328
- ? { phase: { data: { status: "working" } } }
329
- : { task: { data: { status: "working" } } },
330
- validation: { valid: true, warnings: [] },
331
- },
332
- })
333
- await Bun.write(
334
- path,
335
- managedWorkbaseOpencodePlugin
336
- .replace("const contextTarget =", "export const contextTarget =")
337
- .replace("const agencyContext =", "export const agencyContext =")
338
- .replace(
339
- "const workerLaunchTarget =",
340
- "export const workerLaunchTarget =",
341
- ),
342
- )
343
- const originalSpawn = Bun.spawn
344
- Bun.spawn = (() =>
345
- ({
346
- stdout: new Response(contextResponse).body!,
347
- exited: Promise.resolve(0),
348
- }) as ReturnType<typeof Bun.spawn>) as typeof Bun.spawn
349
- try {
350
- const generated = await import(
351
- `${pathToFileURL(path).href}?worker-identity=${encodeURIComponent(launchTarget)}`
352
- )
353
- expect(
354
- generated.workerLaunchTarget([
355
- {
356
- type: "text",
357
- text: `Agency worker launch target: ${launchTarget}. Start the task.`,
358
- },
359
- ]),
360
- ).toBe(launchTarget)
361
- expect(generated.workerLaunchTarget(undefined)).toBeUndefined()
362
- expect(
363
- generated.contextTarget({
364
- target,
365
- authority: { mode: "execution" },
366
- }),
367
- ).toBe(launchTarget)
368
- expect(await generated.agencyContext(root)).toMatchObject({
369
- root,
370
- target: launchTarget,
371
- task: "example",
372
- phase,
373
- })
374
- expect(generated.default).toMatchObject({
375
- id: "agency",
376
- setup: expect.any(Function),
377
- server: generated.AgencyPlugin,
378
- })
379
- const hooks = await generated.AgencyPlugin({ directory: root } as never)
380
- await hooks["chat.message"]!(
381
- { sessionID: "worker-session" } as never,
382
- {
383
- parts: [
384
- {
385
- type: "text",
386
- text: `Agency worker launch target: ${launchTarget}. Start the task.`,
387
- },
388
- ],
389
- } as never,
390
- )
391
- await hooks["chat.message"]!(
392
- { sessionID: "mismatched-session" } as never,
393
- {
394
- parts: [
395
- {
396
- type: "text",
397
- text: "Agency worker launch target: execution-unit:task/other. Start the task.",
398
- },
399
- ],
400
- } as never,
401
- )
402
-
403
- const system = { system: [] as string[] }
404
- await hooks["experimental.chat.system.transform"]!(
405
- { sessionID: "worker-session" } as never,
406
- system,
407
- )
408
- expect(system.system).toHaveLength(1)
409
- expect(system.system[0]).toContain(`active worker for ${launchTarget}`)
410
- expect(system.system[0]).toContain(
411
- `${checkoutPath} as the default implementation directory`,
412
- )
413
- expect(system.system[0]).toContain(
414
- "Run Agency lifecycle and context commands from the task or phase directory",
415
- )
416
- expect(system.system[0]).toContain(
417
- "reference checkouts reported by Agency context are read-only",
418
- )
419
- const mismatchedSystem = { system: [] as string[] }
420
- await hooks["experimental.chat.system.transform"]!(
421
- { sessionID: "mismatched-session" } as never,
422
- mismatchedSystem,
423
- )
424
- expect(mismatchedSystem.system).toEqual([])
425
-
426
- const shell = { env: {} as Record<string, string> }
427
- await hooks["shell.env"]!({ sessionID: "worker-session" } as never, shell)
428
- expect(shell.env).toMatchObject({
429
- AGENCY_SESSION_ID: "worker-session",
430
- AGENCY_TARGET: launchTarget,
431
- AGENCY_WORKBASE: root,
432
- AGENCY_TASK_ID: "example",
433
- AGENCY_WRITABLE_CHECKOUT: checkoutPath,
434
- })
435
- if (phase) expect(shell.env.AGENCY_PHASE_ID).toBe(phase)
436
- } finally {
437
- Bun.spawn = originalSpawn
438
- }
439
- }
440
-
441
- test("binds validated task worker identity to an OpenCode session", () =>
442
- testWorkerIdentity({
443
- target: { kind: "task", taskId: "example" },
444
- launchTarget: "execution-unit:task/example",
445
- }))
446
-
447
- test("binds validated phase worker identity to an OpenCode session", () =>
448
- testWorkerIdentity({
449
- target: { kind: "phase", taskId: "example", phaseId: "build" },
450
- launchTarget: "execution-unit:phase/example/build",
451
- phase: "build",
452
- }))
453
-
454
- test("registers the workbase reference through the OpenCode V2 API", async () => {
455
- const path = join(root, ".opencode/plugins/agency-repository-skills.ts")
456
- await write(
457
- root,
458
- ".opencode/plugins/agency-repository-skills.ts",
459
- managedWorkbaseOpencodePlugin,
460
- )
461
- const generated = await import(`${pathToFileURL(path).href}?v2-setup`)
462
- let reference:
463
- | {
464
- name: string
465
- source: { type: string; path: string; description: string }
466
- }
467
- | undefined
468
-
469
- await generated.default.setup({
470
- reference: {
471
- transform: async (callback: (references: unknown) => void) =>
472
- callback({
473
- list: () => [],
474
- add: (
475
- name: string,
476
- source: {
477
- type: string
478
- path: string
479
- description: string
480
- },
481
- ) => {
482
- reference = { name, source }
483
- },
484
- }),
485
- },
486
- skill: { transform: async () => {} },
487
- })
488
-
489
- expect(reference).toEqual({
490
- name: "workbase",
491
- source: {
492
- type: "local",
493
- path: root,
494
- description:
495
- "Complete Agency workbase context; write authority still comes only from agency context",
496
- },
497
- })
498
- })
499
-
500
- test("registers a TUI-only /agency-debug diagnostic", async () => {
501
- const config = JSON.parse(managedBody(managedWorkbaseOpencodeTui))
502
- expect(config).toEqual({
503
- $schema: "https://opencode.ai/tui.json",
504
- plugin: ["./tui/agency-debug.ts"],
505
- })
506
-
507
- const path = join(root, ".opencode/tui/agency-debug.ts")
508
- await write(
509
- root,
510
- ".opencode/tui/agency-debug.ts",
511
- managedWorkbaseOpencodeTuiPlugin,
512
- )
513
- const module = await import(pathToFileURL(path).href)
514
- let clientReads = 0
515
-
516
- const runDiagnostic = async (
517
- paths: string[],
518
- options: {
519
- ready?: boolean
520
- managedReference?: boolean
521
- externalDirectory?: Record<string, string>
522
- } = {},
523
- ) => {
524
- let command:
525
- | {
526
- slashName?: string
527
- namespace?: string
528
- run: () => void
529
- }
530
- | undefined
531
- let toast: { variant: string; message: string } | undefined
532
- await module.default.tui({
533
- get client() {
534
- clientReads += 1
535
- throw new Error("diagnostic must not access the server client")
536
- },
537
- state: {
538
- ready: options.ready ?? true,
539
- config: {
540
- skills: { paths },
541
- references: options.managedReference
542
- ? {
543
- workbase: {
544
- path: "..",
545
- description:
546
- "Complete Agency workbase context; write authority still comes only from agency context",
547
- },
548
- }
549
- : undefined,
550
- permission: options.externalDirectory
551
- ? { external_directory: options.externalDirectory }
552
- : undefined,
553
- },
554
- path: { directory: join(root, "tasks", "example") },
555
- },
556
- keymap: {
557
- registerLayer: (layer: { commands: (typeof command)[] }) => {
558
- command = layer.commands[0]
559
- },
560
- },
561
- ui: {
562
- toast: (input: { variant: string; message: string }) => {
563
- toast = input
564
- },
565
- },
566
- } as never)
567
- expect(command).toMatchObject({
568
- namespace: "palette",
569
- slashName: "agency-debug",
570
- })
571
- command?.run()
572
- return toast
573
- }
574
-
575
- expect(await runDiagnostic(["/checkout/.agents/skills/."])).toMatchObject({
576
- variant: "success",
577
- message: expect.stringContaining("Server plugin: initialized"),
578
- })
579
- expect(
580
- await runDiagnostic([], {
581
- managedReference: true,
582
- externalDirectory: { [join(root, "*")]: "allow" },
583
- }),
584
- ).toMatchObject({
585
- variant: "success",
586
- message: expect.stringContaining("workbase access registered"),
587
- })
588
- expect(
589
- await runDiagnostic([], {
590
- externalDirectory: { [join(root, "*")]: "allow" },
591
- }),
592
- ).toMatchObject({
593
- variant: "warning",
594
- message: expect.stringContaining("Server plugin: indeterminate"),
595
- })
596
- expect(await runDiagnostic([])).toMatchObject({
597
- variant: "warning",
598
- message: expect.stringContaining("Server plugin: indeterminate"),
599
- })
600
- expect(clientReads).toBe(0)
601
- expect(managedWorkbaseOpencodeTuiPlugin).not.toContain("chat.message")
602
- expect(
603
- canUpdateManagedWorkbaseOpencodeTuiPlugin(
604
- managedWorkbaseOpencodeTuiPlugin,
605
- ),
606
- ).toBe(true)
607
- })
608
-
609
- test("generates context-first safety and execution closeout guidance", () => {
610
- const body = managedBody(managedWorkbaseAgents)
611
-
612
- expect(body).toContain("agency context . --json")
613
- expect(body).toContain("agency next --json")
614
- expect(body).toContain("agency <command> --help")
615
- expect(body).toContain("authority.writable.checkoutPath")
616
- expect(body).toContain("authority.documents.writable")
617
- expect(body).toContain("Only `done` satisfies a dependency")
618
- expect(body).toContain("Require explicit user intent")
619
- expect(body).toContain(
620
- "explicit request for a new, separate, or follow-up item",
621
- )
622
- expect(body).toContain("agency task handoff")
623
- expect(body).toContain(
624
- "Creation and handoff do not imply worktree preparation",
625
- )
626
- expect(body).toContain("changing repository")
627
- expect(body).toMatch(
628
- /archiving, restoring,\s+dropping, reopening, or completing work without a pull request/,
629
- )
630
- expect(body).toContain("Never invent entity IDs")
631
- expect(body).toContain("Preserve parent backlinks")
632
- expect(body).toContain("dirty-worktree, revision")
633
- expect(body).toContain("`agency work` is the human launch flow")
634
- expect(body).toContain("Agency worker launch target: <target>.")
635
- expect(body).toContain("environment variables and a generated")
636
- expect(body).toContain("the initial instruction is a generated")
637
- expect(body).toContain(
638
- "External session state is never part of worker identity",
639
- )
640
- expect(body).toMatch(/If\s+the prompt\s+and context disagree/)
641
- expect(body).toContain("marks execution work")
642
- expect(body).toContain("marks execution work")
643
- expect(body).toContain("formatting, type checks, build, dead-code checks")
644
- expect(body).toContain("Review and commit the diff")
645
- expect(body).toContain("Use `agency push`")
646
- expect(body).toContain("never authors semantic commit descriptions")
647
- expect(body).toContain("Run `agency validate`")
648
- expect(body).toContain("only with explicit user intent")
649
- expect(body).toContain("An execution unit remains `working`")
650
- expect(body).toContain("It becomes `done`")
651
- expect(body).toContain("Do not mark committed")
652
- expect(body).toContain("creating or updating a PR")
653
- expect(body).toContain("marking it ready")
654
- expect(body).toMatch(/completing\s+a refinement loop/)
655
- expect(body).toContain("pausing or handing off")
656
- expect(body).toContain("update status")
657
- expect(body).toContain("`agency sync`")
658
- expect(body).toContain("`--no-pull-request --summary <text>`")
659
- expect(body).toContain("`TASK.md` or `PHASE.md`")
660
- expect(body).toContain("PR state, current head, diff summary")
661
- expect(body).toContain("Run `agency validate` before reporting completion")
662
- expect(body).toContain("agency integration status")
663
- expect(body).not.toContain("SKILL.md")
664
- expect(body).not.toContain("~/.agents")
665
- expect(body).not.toContain(".opencode/command/agency.md")
666
- })
667
-
668
- test("generates repository add and setup guidance", () => {
669
- const body = managedBody(managedWorkbaseAgents)
670
-
671
- expect(body).toContain("## Adding a Repository")
672
- expect(body).toContain("agency repo add <alias> <remote> --json")
673
- expect(body).toContain(
674
- "`agency repo add` mutates immediately and does not accept `--apply`",
675
- )
676
- expect(body).toContain("agency repo setup --dry-run")
677
- expect(body).toContain("agency repo setup --apply")
678
- expect(body).toMatch(
679
- /repositories that are already declared\s+but locally missing/,
680
- )
681
- expect(body).toContain("Do not edit\n`agency.json` or `repos/` manually")
682
- expect(body).toMatch(
683
- /agency repo verify <alias> --json\s+agency validate --json/,
684
- )
685
- expect(body).toMatch(
686
- /run only these checks, in order, unless\s+`agency context` reports a relevant problem/,
687
- )
688
- })
689
-
690
- test("configures Agency planning with complete workbase access", () => {
691
- const config = JSON.parse(managedBody(managedWorkbaseOpencode))
692
-
693
- expect(config.instructions).toEqual([".agency/AGENTS.md"])
694
- expect(config.agent).toEqual({
695
- plan: {
696
- disable: true,
697
- },
698
- "agency-plan": {
699
- description:
700
- "Agency planning mode. May update Agency plans and planning structure.",
701
- mode: "primary",
702
- prompt: expect.stringContaining("You are in Agency Plan mode"),
703
- permission: {
704
- question: "allow",
705
- bash: {
706
- "agency *": "allow",
707
- },
708
- edit: {
709
- "*": "deny",
710
- "tasks/*/TASK.md": "allow",
711
- "tasks/*/phases/*/PHASE.md": "allow",
712
- "epics/*/EPIC.md": "allow",
713
- },
714
- },
715
- },
716
- })
717
- expect(config.agent["agency-plan"].prompt).toContain(
718
- "Explicit-new intent overrides reuse",
719
- )
720
- expect(config.agent.agency).toBeUndefined()
721
- expect(config.agent["agency-plan"].prompt).toContain(
722
- "Start with `agency context . --json`",
723
- )
724
- expect(config.agent["agency-plan"].prompt).toContain(
725
- "Pass `--full` only when document prose or low-level VCS details are needed",
726
- )
727
- expect(config.agent["agency-plan"].prompt).toContain(
728
- "decompose it into independently deliverable tasks",
729
- )
730
- expect(config.agent["agency-plan"].prompt).toContain("Use `--if-revision`")
731
- expect(config.agent["agency-plan"].prompt).toContain(
732
- "available ticket tools",
733
- )
734
- expect(config.agent["agency-plan"].prompt).toContain(
735
- "changing lifecycle state",
736
- )
737
- expect(config.agent["agency-plan"].prompt).toContain(
738
- "Follow the managed Agency instructions and reported authority",
739
- )
740
- expect(config.agent["agency-plan"].permission.bash["*"]).toBeUndefined()
741
- expect(config.references).toEqual({
742
- workbase: {
743
- path: "..",
744
- description:
745
- "Complete Agency workbase context; write authority still comes only from agency context",
746
- },
747
- })
748
- expect(config.permission).toBeUndefined()
749
- expect(managedBody(managedWorkbaseOpencode)).not.toContain(process.cwd())
750
- })
751
-
752
- test("treats an existing JSON OpenCode config as customized", async () => {
753
- await write(root, ".opencode/opencode.json", '{"model":"test/model"}\n')
754
-
755
- const result = await status(root)
756
- expect(result.files[1]).toMatchObject({
757
- name: "opencode",
758
- path: join(root, ".opencode/opencode.json"),
759
- state: "customized",
760
- diagnostic: expect.stringContaining("cannot guarantee"),
761
- remediation: expect.stringContaining("global config"),
762
- })
763
- })
764
-
765
- test("treats a JSON config beside managed JSONC as customized", async () => {
766
- await write(root, ".opencode/opencode.jsonc", managedWorkbaseOpencode)
767
- await write(root, ".opencode/opencode.json", '{"model":"test/model"}\n')
768
-
769
- const result = await status(root)
770
- expect(result.files[1]).toMatchObject({
771
- name: "opencode",
772
- path: join(root, ".opencode/opencode.json"),
773
- state: "customized",
774
- })
775
- })
776
-
777
- test("reports actionable whole-workbase access diagnostics", async () => {
778
- let result = await status(root)
779
- expect(result.files[1]).toMatchObject({
780
- state: "missing",
781
- diagnostic: expect.stringContaining("cannot load"),
782
- remediation: expect.stringContaining("integration sync"),
783
- })
784
-
785
- await write(root, ".opencode/opencode.jsonc", '{"model":"test/model"}\n')
786
- result = await status(root)
787
- expect(result.files[1]).toMatchObject({
788
- state: "customized",
789
- diagnostic: expect.stringContaining("cannot guarantee"),
790
- remediation: expect.stringContaining("global config"),
791
- })
792
- })
793
-
794
- test("syncs missing and drifted files while preserving customized files", async () => {
795
- const customRootAgents = "# User-owned workbase instructions\n"
796
- const customManagedAgents = "# Customized Agency instructions\n"
797
- await write(root, "AGENTS.md", customRootAgents)
798
- await write(root, ".agency/AGENTS.md", customManagedAgents)
799
- await write(
800
- root,
801
- ".opencode/opencode.jsonc",
802
- managed("// agency-managed: sha256=", '{"references":{}}\n'),
803
- )
804
-
805
- const first = await sync(root)
806
- expect(first.files).toMatchObject([
807
- { name: "agents", state: "customized", changed: false },
808
- { name: "opencode", state: "managed", changed: true },
809
- { name: "opencode-plugin", state: "managed", changed: true },
810
- { name: "opencode-tui", state: "managed", changed: true },
811
- { name: "opencode-tui-plugin", state: "managed", changed: true },
812
- ])
813
- expect(await Bun.file(join(root, "AGENTS.md")).text()).toBe(
814
- customRootAgents,
815
- )
816
- expect(await Bun.file(join(root, ".agency/AGENTS.md")).text()).toBe(
817
- customManagedAgents,
818
- )
819
- expect(await Bun.file(join(root, ".opencode/opencode.jsonc")).text()).toBe(
820
- managedWorkbaseOpencode,
821
- )
822
- expect(
823
- await Bun.file(join(root, ".opencode/command/agency.md")).exists(),
824
- ).toBe(false)
825
- expect(
826
- await Bun.file(
827
- join(root, ".opencode/plugins/agency-repository-skills.ts"),
828
- ).text(),
829
- ).toBe(managedWorkbaseOpencodePlugin)
830
- expect(await Bun.file(join(root, ".opencode/tui.jsonc")).text()).toBe(
831
- managedWorkbaseOpencodeTui,
832
- )
833
- expect(
834
- await Bun.file(join(root, ".opencode/tui/agency-debug.ts")).text(),
835
- ).toBe(managedWorkbaseOpencodeTuiPlugin)
836
-
837
- await unlink(join(root, ".agency/AGENTS.md"))
838
- const second = await sync(root)
839
- expect(second.files[0]).toMatchObject({ state: "managed", changed: true })
840
- expect(await Bun.file(join(root, ".agency/AGENTS.md")).text()).toBe(
841
- managedWorkbaseAgents,
842
- )
843
- expect(await Bun.file(join(root, "AGENTS.md")).text()).toBe(
844
- customRootAgents,
845
- )
846
- })
847
-
848
- test("upgrades prior managed instructions without changing owner instructions", async () => {
849
- const ownerInstructions = "# User-owned workbase instructions\n"
850
- const priorInstructions = managed(
851
- "<!-- agency-managed: sha256=",
852
- "# Previous Agency instructions\n",
853
- " -->",
854
- )
855
- await write(root, "AGENTS.md", ownerInstructions)
856
- await write(root, ".agency/AGENTS.md", priorInstructions)
857
-
858
- const result = await sync(root)
859
-
860
- expect(result.files[0]).toMatchObject({
861
- name: "agents",
862
- state: "managed",
863
- changed: true,
864
- })
865
- expect(await Bun.file(join(root, ".agency/AGENTS.md")).text()).toBe(
866
- managedWorkbaseAgents,
867
- )
868
- expect(await Bun.file(join(root, "AGENTS.md")).text()).toBe(
869
- ownerInstructions,
870
- )
871
- })
872
-
873
- test("preserves user-owned checkout-skill plugins at either supported path", async () => {
874
- const custom = "export default async () => ({})\n"
875
- await write(root, ".opencode/plugins/agency-repository-skills.ts", custom)
876
-
877
- let result = await sync(root)
878
- expect(result.files[2]).toMatchObject({
879
- name: "opencode-plugin",
880
- path: join(root, ".opencode/plugins/agency-repository-skills.ts"),
881
- state: "customized",
882
- changed: false,
883
- })
884
- expect(
885
- await Bun.file(
886
- join(root, ".opencode/plugin/agency-repository-skills.ts"),
887
- ).exists(),
888
- ).toBe(false)
889
-
890
- await unlink(join(root, ".opencode/plugins/agency-repository-skills.ts"))
891
- await write(root, ".opencode/plugin/agency-repository-skills.ts", custom)
892
- result = await sync(root)
893
- expect(result.files[2]).toMatchObject({
894
- path: join(root, ".opencode/plugin/agency-repository-skills.ts"),
895
- state: "customized",
896
- changed: false,
897
- })
898
- })
899
-
900
- test("removes a checksum-valid workbase-local Pi extension", async () => {
901
- const body = "export default function legacyAgencyExtension() {}\n"
902
- const checksum = createHash("sha256").update(body).digest("hex")
903
- const path = join(root, ".pi/extensions/agency-workbase.ts")
904
- await write(
905
- root,
906
- ".pi/extensions/agency-workbase.ts",
907
- `// agency-managed: sha256=${checksum}\n\n${body}`,
908
- )
909
-
910
- await sync(root)
911
- expect(await Bun.file(path).exists()).toBe(false)
912
- expect(await Bun.file(join(root, ".pi")).exists()).toBe(false)
913
- })
914
-
915
- test("preserves a customized workbase-local Pi extension", async () => {
916
- const custom = "export default function customPiExtension() {}\n"
917
- const path = join(root, ".pi/extensions/agency-workbase.ts")
918
- await write(root, ".pi/extensions/agency-workbase.ts", custom)
919
-
920
- await sync(root)
921
- expect(await Bun.file(path).text()).toBe(custom)
922
- })
923
-
924
- test("migrates a checksum-valid plugin from the legacy singular path", async () => {
925
- const legacyPath = join(
926
- root,
927
- ".opencode/plugin/agency-repository-skills.ts",
928
- )
929
- const pluginPath = join(
930
- root,
931
- ".opencode/plugins/agency-repository-skills.ts",
932
- )
933
- await write(
934
- root,
935
- ".opencode/plugin/agency-repository-skills.ts",
936
- managedWorkbaseOpencodePlugin,
937
- )
938
-
939
- const result = await sync(root)
940
-
941
- expect(result.files[2]).toMatchObject({
942
- name: "opencode-plugin",
943
- path: pluginPath,
944
- state: "managed",
945
- changed: true,
946
- })
947
- expect(await Bun.file(pluginPath).text()).toBe(
948
- managedWorkbaseOpencodePlugin,
949
- )
950
- expect(await Bun.file(legacyPath).exists()).toBe(false)
951
- })
952
-
953
- test("preserves user-owned TUI config and diagnostic plugin", async () => {
954
- const customConfig = '{"theme":"custom"}\n'
955
- const customPlugin =
956
- "export default { id: 'agency.debug', tui: async () => {} }\n"
957
- await write(root, ".opencode/tui.json", customConfig)
958
- await write(root, ".opencode/tui/agency-debug.ts", customPlugin)
959
-
960
- const result = await sync(root)
961
- expect(result.files[3]).toMatchObject({
962
- name: "opencode-tui",
963
- path: join(root, ".opencode/tui.json"),
964
- state: "customized",
965
- changed: false,
966
- remediation: expect.stringContaining("plugin list"),
967
- })
968
- expect(result.files[4]).toMatchObject({
969
- name: "opencode-tui-plugin",
970
- state: "customized",
971
- changed: false,
972
- })
973
- expect(await Bun.file(join(root, ".opencode/tui.json")).text()).toBe(
974
- customConfig,
975
- )
976
- expect(
977
- await Bun.file(join(root, ".opencode/tui/agency-debug.ts")).text(),
978
- ).toBe(customPlugin)
979
- })
980
-
981
- test("removes a checksum-valid legacy command and preserves a customized file", async () => {
982
- const path = ".opencode/command/agency.md"
983
- const legacy = legacyCommand(
984
- "---\ndescription: Operate Agency work\n---\n\nLegacy command.\n",
985
- )
986
- await write(root, path, legacy)
987
-
988
- await status(root)
989
- expect(await Bun.file(join(root, path)).text()).toBe(legacy)
990
-
991
- await sync(root)
992
- expect(await Bun.file(join(root, path)).exists()).toBe(false)
993
-
994
- const custom = `${legacy}User edit.\n`
995
- await write(root, path, custom)
996
- await sync(root)
997
- expect(await Bun.file(join(root, path)).text()).toBe(custom)
998
- })
999
-
1000
- test("migrates checksum-valid root instructions", async () => {
1001
- await write(root, "AGENTS.md", managedWorkbaseAgents)
1002
-
1003
- const result = await sync(root)
1004
-
1005
- expect(result.files[0]).toMatchObject({ state: "managed", changed: true })
1006
- expect(await Bun.file(join(root, ".agency/AGENTS.md")).text()).toBe(
1007
- managedWorkbaseAgents,
1008
- )
1009
- expect(await Bun.file(join(root, "AGENTS.md")).exists()).toBe(false)
1010
- })
1011
-
1012
- test("preserves legacy instructions when the OpenCode config is customized", async () => {
1013
- await write(root, "AGENTS.md", managedWorkbaseAgents)
1014
- await write(root, ".opencode/opencode.json", '{"model":"test/model"}\n')
1015
-
1016
- const result = await sync(root)
1017
-
1018
- expect(result.files[0]).toMatchObject({ state: "managed", changed: true })
1019
- expect(result.files[1]).toMatchObject({
1020
- state: "customized",
1021
- changed: false,
1022
- })
1023
- expect(await Bun.file(join(root, "AGENTS.md")).text()).toBe(
1024
- managedWorkbaseAgents,
1025
- )
1026
- })
1027
-
1028
- test("does not rewrite an already-current OpenCode configuration", async () => {
1029
- const path = join(root, ".opencode/opencode.jsonc")
1030
- await write(root, ".opencode/opencode.jsonc", managedWorkbaseOpencode)
1031
- const timestamp = new Date("2000-01-01T00:00:00.000Z")
1032
- await utimes(path, timestamp, timestamp)
1033
-
1034
- const result = await sync(root)
1035
-
1036
- expect(result.files[1]).toMatchObject({
1037
- state: "managed",
1038
- changed: false,
1039
- })
1040
- expect((await stat(path)).mtimeMs).toBe(timestamp.getTime())
1041
- })
1042
-
1043
- test("does not overwrite managed files whose checksums no longer match", async () => {
1044
- const tampered = `${managed(
1045
- "<!-- agency-managed: sha256=",
1046
- "# Previous Agency instructions\n",
1047
- " -->",
1048
- )}User edit\n`
1049
- await write(root, ".agency/AGENTS.md", tampered)
1050
-
1051
- const result = await sync(root)
1052
- expect(result.files[0]).toMatchObject({
1053
- state: "customized",
1054
- changed: false,
1055
- })
1056
- expect(await Bun.file(join(root, ".agency/AGENTS.md")).text()).toBe(
1057
- tampered,
1058
- )
1059
- })
1060
-
1061
- test("does not overwrite a user-modified OpenCode configuration", async () => {
1062
- const tampered = `${managed(
1063
- "// agency-managed: sha256=",
1064
- '{"references":{}}\n',
1065
- )}// User edit\n`
1066
- await write(root, ".opencode/opencode.jsonc", tampered)
1067
-
1068
- const result = await sync(root)
1069
- expect(result.files[1]).toMatchObject({
1070
- state: "customized",
1071
- changed: false,
1072
- })
1073
- expect(await Bun.file(join(root, ".opencode/opencode.jsonc")).text()).toBe(
1074
- tampered,
1075
- )
1076
- })
1077
-
1078
- test("does not follow symlinked integration files", async () => {
1079
- const target = join(root, "custom-agents.md")
1080
- await Bun.write(target, "# External instructions\n")
1081
- await mkdir(join(root, ".agency"), { recursive: true })
1082
- await symlink(target, join(root, ".agency/AGENTS.md"))
1083
-
1084
- const result = await sync(root)
1085
- expect(result.files[0]).toMatchObject({
1086
- state: "customized",
1087
- changed: false,
1088
- })
1089
- expect(await Bun.file(target).text()).toBe("# External instructions\n")
1090
- })
1091
- })