@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.
- package/package.json +7 -2
- package/src/workbase/schemas.ts +12 -0
- package/fixtures/protocol/orchestration-recipes.json +0 -53
- package/pi-extensions/agency.test.ts +0 -117
- package/src/check-commits.test.ts +0 -58
- package/src/cli-parser.test.ts +0 -945
- package/src/cli.test.ts +0 -1889
- package/src/commands/act.test.ts +0 -418
- package/src/commands/archive.test.ts +0 -236
- package/src/commands/context.test.ts +0 -665
- package/src/commands/description.test.ts +0 -103
- package/src/commands/doctor.test.ts +0 -185
- package/src/commands/epic.test.ts +0 -196
- package/src/commands/init.test.ts +0 -117
- package/src/commands/integration.test.ts +0 -165
- package/src/commands/pr.test.ts +0 -248
- package/src/commands/push.test.ts +0 -56
- package/src/commands/read-only.test.ts +0 -161
- package/src/commands/repo.test.ts +0 -199
- package/src/commands/restore.test.ts +0 -106
- package/src/commands/status.test.ts +0 -113
- package/src/commands/sync.test.ts +0 -200
- package/src/commands/task-phase.test.ts +0 -410
- package/src/commands/task.test.ts +0 -281
- package/src/commands/validate.test.ts +0 -186
- package/src/commands/work.test.ts +0 -1375
- package/src/commands/workbase.test.ts +0 -170
- package/src/graph-schema.test.ts +0 -124
- package/src/protocol.test.ts +0 -163
- package/src/readiness.test.ts +0 -105
- package/src/services/ArchiveBulkService.test.ts +0 -384
- package/src/services/ArchiveService.test.ts +0 -1092
- package/src/services/EpicService.test.ts +0 -74
- package/src/services/FileSystemService.test.ts +0 -81
- package/src/services/GraphMutationService.test.ts +0 -486
- package/src/services/GraphService.test.ts +0 -494
- package/src/services/IntegrationService.test.ts +0 -1091
- package/src/services/LifecycleTransaction.test.ts +0 -145
- package/src/services/PullRequestService.test.ts +0 -716
- package/src/services/PushService.test.ts +0 -408
- package/src/services/ReadinessService.test.ts +0 -290
- package/src/services/RepositoryService.test.ts +0 -789
- package/src/services/ReviewService.test.ts +0 -408
- package/src/services/SyncService.test.ts +0 -1377
- package/src/services/TaskPhaseService.test.ts +0 -852
- package/src/services/VersionControlService.test.ts +0 -62
- package/src/services/WorkbaseService.test.ts +0 -910
- package/src/services/WorktreeLock.test.ts +0 -205
- package/src/services/WorktreePerformance.test.ts +0 -71
- package/src/services/WorktreeService.test.ts +0 -2292
- package/src/test-utils.ts +0 -110
- package/src/usage-log.test.ts +0 -188
- package/src/utils/chooser.test.ts +0 -192
- package/src/utils/effect.test.ts +0 -30
- package/src/utils/interactive.pty.test.ts +0 -128
- package/src/utils/interactive.test.tsx +0 -860
- package/src/utils/process.test.ts +0 -132
- package/src/utils/progress.test.ts +0 -37
- package/src/work-view.test.ts +0 -151
- package/src/workbase/agent-command.test.ts +0 -128
- package/src/workbase/checkout-command.test.ts +0 -62
- package/src/workbase/delivery-command.test.ts +0 -180
- package/src/workbase/dependency-graph.test.ts +0 -50
- package/src/workbase/execution-contract.test.ts +0 -161
- package/src/workbase/frontmatter.test.ts +0 -67
- package/src/workbase/repository-reference.test.ts +0 -25
- package/src/workbase/schemas.test.ts +0 -543
- package/src/workbase/work-target.test.ts +0 -108
- 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
|
-
})
|