@markjaquith/agency 2.29.0 → 2.30.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +72 -16
  2. package/cli.ts +3 -0
  3. package/fixtures/protocol/skill-setup-commands.json +18 -0
  4. package/package.json +1 -1
  5. package/skills/agency/SKILL.md +30 -15
  6. package/skills/agency/references/commands.md +38 -17
  7. package/skills/agency/references/contracts.md +46 -19
  8. package/skills/agency/references/recipes.md +52 -12
  9. package/src/cli-parser.test.ts +7 -0
  10. package/src/cli-parser.ts +13 -2
  11. package/src/cli.test.ts +422 -3
  12. package/src/commands/doctor.test.ts +22 -0
  13. package/src/commands/init.test.ts +3 -2
  14. package/src/commands/integration.test.ts +21 -1
  15. package/src/commands/integration.ts +8 -4
  16. package/src/commands/pr.test.ts +20 -1
  17. package/src/commands/repo.test.ts +66 -1
  18. package/src/commands/repo.ts +35 -8
  19. package/src/commands/status.test.ts +22 -0
  20. package/src/commands/status.ts +1 -0
  21. package/src/commands/sync.ts +5 -3
  22. package/src/commands/work.test.ts +74 -1
  23. package/src/commands/work.ts +26 -3
  24. package/src/graph-schema.test.ts +52 -4
  25. package/src/protocol.test.ts +41 -8
  26. package/src/readiness.test.ts +75 -17
  27. package/src/services/DoctorService.ts +32 -18
  28. package/src/services/EpicService.ts +1 -1
  29. package/src/services/GraphMutationService.ts +3 -3
  30. package/src/services/GraphService.ts +13 -4
  31. package/src/services/IntegrationService.test.ts +37 -12
  32. package/src/services/IntegrationService.ts +70 -21
  33. package/src/services/PhaseService.ts +1 -1
  34. package/src/services/ReadinessService.test.ts +47 -0
  35. package/src/services/RepositoryService.test.ts +299 -5
  36. package/src/services/RepositoryService.ts +725 -98
  37. package/src/services/SyncService.test.ts +36 -0
  38. package/src/services/SyncService.ts +20 -1
  39. package/src/services/TaskService.ts +1 -1
  40. package/src/services/WorkbaseService.test.ts +32 -0
  41. package/src/services/WorkbaseService.ts +22 -14
  42. package/src/services/WorktreeLock.test.ts +122 -0
  43. package/src/services/WorktreeService.test.ts +17 -2
  44. package/src/services/WorktreeService.ts +3 -3
  45. package/src/utils/process.test.ts +4 -3
  46. package/src/workbase/AGENTS.md +9 -1
  47. package/src/workbase/dependency-graph.test.ts +50 -0
  48. package/src/workbase/opencode-file.ts +3 -13
  49. package/src/workbase/schemas.test.ts +52 -0
  50. package/src/workbase/schemas.ts +19 -0
package/src/cli-parser.ts CHANGED
@@ -205,9 +205,20 @@ const commands = {
205
205
  },
206
206
  repo: {
207
207
  usage:
208
- "agency repo <add|link|list|show|fetch|remove|unlink|rename|remote|verify>",
209
- options: outputOptions,
208
+ "agency repo <setup|add|link|list|show|fetch|remove|unlink|rename|remote|verify>",
209
+ options: {
210
+ ...outputOptions,
211
+ "dry-run": { type: "boolean" },
212
+ apply: { type: "boolean" },
213
+ },
210
214
  subcommands: {
215
+ setup: {
216
+ usage: "agency repo setup [--dry-run | --apply] [--json]",
217
+ minArgs: 0,
218
+ maxArgs: 0,
219
+ options: ["dry-run", "apply", "json"],
220
+ conflicts: [["dry-run", "apply"]],
221
+ },
211
222
  add: {
212
223
  usage: "agency repo add <alias> <remote> [--json]",
213
224
  minArgs: 2,
package/src/cli.test.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { afterEach, describe, expect, test } from "bun:test"
1
+ import { afterAll, afterEach, describe, expect, test } from "bun:test"
2
2
  import { access, mkdir, realpath } from "node:fs/promises"
3
3
  import { join } from "node:path"
4
4
  import errorFixture from "../fixtures/protocol/error.json"
@@ -7,6 +7,9 @@ import { cleanupTempDir, createTempDir } from "./test-utils"
7
7
 
8
8
  const projectRoot = join(import.meta.dir, "..")
9
9
  const cliPath = join(projectRoot, "cli.ts")
10
+ const isolatedConfigHome = await createTempDir()
11
+
12
+ afterAll(() => cleanupTempDir(isolatedConfigHome))
10
13
 
11
14
  interface CliResult {
12
15
  exitCode: number
@@ -21,7 +24,11 @@ async function runCli(
21
24
  ): Promise<CliResult> {
22
25
  const subprocess = Bun.spawn([process.execPath, cliPath, ...args], {
23
26
  cwd,
24
- env: env ? { ...process.env, ...env } : undefined,
27
+ env: {
28
+ ...process.env,
29
+ XDG_CONFIG_HOME: isolatedConfigHome,
30
+ ...env,
31
+ },
25
32
  stdout: "pipe",
26
33
  stderr: "pipe",
27
34
  })
@@ -34,13 +41,55 @@ async function runCli(
34
41
  }
35
42
 
36
43
  function parseJson(result: CliResult) {
37
- expect(result.exitCode).toBe(0)
44
+ expect(result.exitCode, JSON.stringify(result)).toBe(0)
38
45
  expect(result.stderr).toBe("")
39
46
  const envelope = JSON.parse(result.stdout)
40
47
  expect(envelope).toMatchObject({ version: 1, ok: true })
41
48
  return envelope.result
42
49
  }
43
50
 
51
+ async function runGit(args: string[]) {
52
+ const subprocess = Bun.spawn(["git", ...args], {
53
+ stdout: "pipe",
54
+ stderr: "pipe",
55
+ })
56
+ const [exitCode, stdout, stderr] = await Promise.all([
57
+ subprocess.exited,
58
+ new Response(subprocess.stdout).text(),
59
+ new Response(subprocess.stderr).text(),
60
+ ])
61
+ if (exitCode !== 0) throw new Error(stderr)
62
+ return stdout
63
+ }
64
+
65
+ async function startGitDaemon(basePath: string) {
66
+ const port = 20000 + Math.floor(Math.random() * 20000)
67
+ const process = Bun.spawn(
68
+ [
69
+ "git",
70
+ "daemon",
71
+ "--reuseaddr",
72
+ "--export-all",
73
+ `--base-path=${basePath}`,
74
+ "--listen=127.0.0.1",
75
+ `--port=${port}`,
76
+ basePath,
77
+ ],
78
+ { stdout: "pipe", stderr: "pipe" },
79
+ )
80
+ const remote = `git://127.0.0.1:${port}/source.git`
81
+ for (let attempt = 0; attempt < 40; attempt++) {
82
+ const probe = Bun.spawn(["git", "ls-remote", remote], {
83
+ stdout: "ignore",
84
+ stderr: "ignore",
85
+ })
86
+ if ((await probe.exited) === 0) return { process, remote }
87
+ await Bun.sleep(25)
88
+ }
89
+ process.kill()
90
+ throw new Error("Git daemon did not start")
91
+ }
92
+
44
93
  describe("CLI", () => {
45
94
  const tempDirs: string[] = []
46
95
 
@@ -550,6 +599,22 @@ describe("CLI", () => {
550
599
  kind: "task",
551
600
  taskId: "explicit",
552
601
  })
602
+ const nextByCwd = parseJson(
603
+ await runCli(
604
+ ["next", "--cwd", root, "--select", "--json"],
605
+ projectRoot,
606
+ env,
607
+ ),
608
+ )
609
+ expect(nextByCwd.selected.key).toBe("task/explicit")
610
+ const nextByRegistration = parseJson(
611
+ await runCli(
612
+ ["next", "--workbase", "primary", "--select", "--json"],
613
+ projectRoot,
614
+ env,
615
+ ),
616
+ )
617
+ expect(nextByRegistration.selected.key).toBe("task/explicit")
553
618
 
554
619
  parseJson(
555
620
  await runCli(["workbase", "default", "primary", "--json"], parent, env),
@@ -662,11 +727,23 @@ status: open
662
727
  ["config", "user.name", "Test"],
663
728
  ["add", "README.md"],
664
729
  ["-c", "commit.gpgsign=false", "commit", "-m", "initial"],
730
+ ["remote", "add", "origin", source],
665
731
  ]) {
666
732
  expect(Bun.spawnSync(["git", "-C", source, ...args]).exitCode).toBe(0)
667
733
  }
668
734
 
669
735
  parseJson(await runCli(["init", root, "--json"], parent))
736
+ await Bun.write(
737
+ join(root, "agency.json"),
738
+ JSON.stringify({
739
+ version: 2,
740
+ repositories: {
741
+ agency: {
742
+ remote: "https://example.com/agency-tests/source.git",
743
+ },
744
+ },
745
+ }),
746
+ )
670
747
  parseJson(await runCli(["repo", "link", "agency", source, "--json"], root))
671
748
  parseJson(
672
749
  await runCli(
@@ -752,6 +829,223 @@ status: open
752
829
  ).toBe("example\n")
753
830
  })
754
831
 
832
+ test.skipIf(Bun.which("opencode") === null)(
833
+ "provides effective whole-workbase OpenCode access from every launch topology",
834
+ async () => {
835
+ const parent = await createTempDir()
836
+ tempDirs.push(parent)
837
+ const root = join(parent, "workbase")
838
+ const source = join(parent, "source")
839
+ expect(
840
+ Bun.spawnSync(["git", "init", "--initial-branch=main", source])
841
+ .exitCode,
842
+ ).toBe(0)
843
+ await Bun.write(join(source, "README.md"), "example\n")
844
+ for (const args of [
845
+ ["config", "user.email", "test@example.com"],
846
+ ["config", "user.name", "Test"],
847
+ ["add", "README.md"],
848
+ ["-c", "commit.gpgsign=false", "commit", "-m", "initial"],
849
+ ]) {
850
+ expect(Bun.spawnSync(["git", "-C", source, ...args]).exitCode).toBe(0)
851
+ }
852
+
853
+ parseJson(await runCli(["init", root, "--json"], parent))
854
+ parseJson(
855
+ await runCli(["repo", "link", "agency", source, "--json"], root),
856
+ )
857
+ parseJson(
858
+ await runCli(
859
+ [
860
+ "epic",
861
+ "create",
862
+ "delivery",
863
+ "--ticket-url",
864
+ "https://example.com/delivery",
865
+ "--repo",
866
+ "agency:main",
867
+ "--json",
868
+ ],
869
+ root,
870
+ ),
871
+ )
872
+ for (const [id, branch] of [
873
+ ["example", "feat/example"],
874
+ ["sibling", "feat/sibling"],
875
+ ] as const) {
876
+ parseJson(
877
+ await runCli(
878
+ [
879
+ "task",
880
+ "create",
881
+ id,
882
+ "--repo",
883
+ "agency",
884
+ "--branch",
885
+ branch,
886
+ "--base",
887
+ "main",
888
+ "--epic",
889
+ "delivery",
890
+ "--json",
891
+ ],
892
+ root,
893
+ ),
894
+ )
895
+ }
896
+ parseJson(
897
+ await runCli(
898
+ ["task", "create", "pipeline", "--multi-phase", "--json"],
899
+ root,
900
+ ),
901
+ )
902
+ parseJson(
903
+ await runCli(
904
+ [
905
+ "phase",
906
+ "create",
907
+ "pipeline",
908
+ "build",
909
+ "--repo",
910
+ "agency",
911
+ "--branch",
912
+ "feat/pipeline-build",
913
+ "--base",
914
+ "main",
915
+ "--json",
916
+ ],
917
+ root,
918
+ ),
919
+ )
920
+
921
+ const taskWorkspace = parseJson(
922
+ await runCli(["work", "prepare", "example", "--json"], root),
923
+ )
924
+ const phaseWorkspace = parseJson(
925
+ await runCli(
926
+ [
927
+ "work",
928
+ "prepare",
929
+ "--task",
930
+ "pipeline",
931
+ "--phase",
932
+ "build",
933
+ "--json",
934
+ ],
935
+ root,
936
+ ),
937
+ )
938
+ const synced = parseJson(
939
+ await runCli(["integration", "sync", "--json"], root),
940
+ )
941
+ expect(
942
+ synced.files.every((file: { changed: boolean }) => !file.changed),
943
+ ).toBe(true)
944
+
945
+ const workbaseRoot = await realpath(root)
946
+ const config = await Bun.file(
947
+ join(workbaseRoot, ".opencode/opencode.jsonc"),
948
+ ).text()
949
+ expect(config).not.toContain(workbaseRoot)
950
+ const documents = [
951
+ join(workbaseRoot, "tasks/example/TASK.md"),
952
+ join(workbaseRoot, "epics/delivery/EPIC.md"),
953
+ join(workbaseRoot, "tasks/sibling/TASK.md"),
954
+ ]
955
+ for (const document of documents) {
956
+ expect(await Bun.file(document).exists()).toBe(true)
957
+ }
958
+
959
+ const launches = [
960
+ {
961
+ args: ["--task", "example"],
962
+ cwd: taskWorkspace.writablePath,
963
+ writable: true,
964
+ },
965
+ {
966
+ args: ["--task", "pipeline", "--phase", "build"],
967
+ cwd: phaseWorkspace.writablePath,
968
+ writable: true,
969
+ },
970
+ {
971
+ args: ["--epic", "delivery"],
972
+ cwd: join(workbaseRoot, "epics/delivery"),
973
+ writable: false,
974
+ },
975
+ {
976
+ args: ["--task", "pipeline"],
977
+ cwd: join(workbaseRoot, "tasks/pipeline"),
978
+ writable: false,
979
+ },
980
+ ]
981
+ for (const launch of launches) {
982
+ const printed = await runCli(
983
+ ["work", ...launch.args, "--opencode", "--print-command", "--force"],
984
+ root,
985
+ )
986
+ expect(printed.exitCode).toBe(0)
987
+ expect(printed.stderr).toBe("")
988
+ const contract = JSON.parse(printed.stdout)
989
+ expect(contract.cwd).toBe(launch.cwd)
990
+ expect(contract.environment.OPENCODE_CONFIG).toBe(
991
+ join(workbaseRoot, ".opencode/opencode.jsonc"),
992
+ )
993
+ const environment = {
994
+ ...process.env,
995
+ ...contract.environment,
996
+ XDG_CONFIG_HOME: isolatedConfigHome,
997
+ OPENCODE_DISABLE_EXTERNAL_SKILLS: "1",
998
+ }
999
+ const probe = Bun.spawnSync(["opencode", "debug", "agent", "build"], {
1000
+ cwd: contract.cwd,
1001
+ env: environment,
1002
+ })
1003
+ expect(probe.exitCode).toBe(0)
1004
+ const agent = JSON.parse(probe.stdout.toString())
1005
+ expect(agent.permission).toEqual(
1006
+ expect.arrayContaining([
1007
+ expect.objectContaining({
1008
+ permission: "external_directory",
1009
+ pattern: join(workbaseRoot, "**"),
1010
+ action: "allow",
1011
+ }),
1012
+ ]),
1013
+ )
1014
+ expect(agent.permission).toEqual(
1015
+ expect.arrayContaining([
1016
+ expect.objectContaining({
1017
+ permission: "edit",
1018
+ pattern: launch.writable ? "../**" : "*",
1019
+ action: "deny",
1020
+ }),
1021
+ ]),
1022
+ )
1023
+
1024
+ if (launch === launches[0]) {
1025
+ for (const document of documents) {
1026
+ const read = Bun.spawnSync(
1027
+ [
1028
+ "opencode",
1029
+ "debug",
1030
+ "agent",
1031
+ "build",
1032
+ "--tool",
1033
+ "read",
1034
+ "--params",
1035
+ JSON.stringify({ filePath: document }),
1036
+ ],
1037
+ { cwd: contract.cwd, env: environment },
1038
+ )
1039
+ expect(read.exitCode).toBe(0)
1040
+ const result = JSON.parse(read.stdout.toString())
1041
+ expect(result.result.output).toContain(`<path>${document}</path>`)
1042
+ }
1043
+ }
1044
+ }
1045
+ },
1046
+ 30_000,
1047
+ )
1048
+
755
1049
  test("envelopes help and version output in machine mode", async () => {
756
1050
  const help = await runCli(["status", "--help", "--json"])
757
1051
  expect(parseJson(help)).toContain("Usage: agency status")
@@ -789,6 +1083,14 @@ status: open
789
1083
  stderr: "pipe",
790
1084
  })
791
1085
  expect(await git.exited).toBe(0)
1086
+ await runGit([
1087
+ "-C",
1088
+ source,
1089
+ "remote",
1090
+ "add",
1091
+ "origin",
1092
+ "https://example.com/agency-tests/source.git",
1093
+ ])
792
1094
 
793
1095
  expect(parseJson(await runCli(["init", root, "--json"]))).toEqual({
794
1096
  root,
@@ -981,4 +1283,121 @@ status: open
981
1283
  valid: true,
982
1284
  })
983
1285
  }, 30_000)
1286
+
1287
+ test("restores portable repositories in a fresh workbase clone", async () => {
1288
+ const parent = await createTempDir()
1289
+ tempDirs.push(parent)
1290
+ const sourceWorktree = join(parent, "source-worktree")
1291
+ const source = join(parent, "source.git")
1292
+ const root = join(parent, "workbase")
1293
+ const restored = join(parent, "restored")
1294
+
1295
+ await runGit(["init", "--initial-branch=main", sourceWorktree])
1296
+ await Bun.write(join(sourceWorktree, "README.md"), "portable\n")
1297
+ await runGit([
1298
+ "-C",
1299
+ sourceWorktree,
1300
+ "config",
1301
+ "user.email",
1302
+ "test@example.com",
1303
+ ])
1304
+ await runGit(["-C", sourceWorktree, "config", "user.name", "Test"])
1305
+ await runGit(["-C", sourceWorktree, "add", "README.md"])
1306
+ await runGit([
1307
+ "-C",
1308
+ sourceWorktree,
1309
+ "-c",
1310
+ "commit.gpgsign=false",
1311
+ "commit",
1312
+ "-m",
1313
+ "initial",
1314
+ ])
1315
+ await runGit(["clone", "--bare", sourceWorktree, source])
1316
+ const daemon = await startGitDaemon(parent)
1317
+
1318
+ try {
1319
+ parseJson(await runCli(["init", root, "--json"], parent))
1320
+ parseJson(
1321
+ await runCli(["repo", "add", "agency", daemon.remote, "--json"], root),
1322
+ )
1323
+ parseJson(
1324
+ await runCli(
1325
+ [
1326
+ "task",
1327
+ "create",
1328
+ "portable",
1329
+ "--repo",
1330
+ "agency",
1331
+ "--branch",
1332
+ "feat/portable",
1333
+ "--base",
1334
+ "main",
1335
+ "--json",
1336
+ ],
1337
+ root,
1338
+ ),
1339
+ )
1340
+
1341
+ await runGit(["init", "--initial-branch=main", root])
1342
+ await runGit(["-C", root, "config", "user.email", "test@example.com"])
1343
+ await runGit(["-C", root, "config", "user.name", "Test"])
1344
+ await runGit(["-C", root, "add", "."])
1345
+ await runGit([
1346
+ "-C",
1347
+ root,
1348
+ "-c",
1349
+ "commit.gpgsign=false",
1350
+ "commit",
1351
+ "-m",
1352
+ "portable workbase",
1353
+ ])
1354
+ const tracked = await runGit(["-C", root, "ls-files"])
1355
+ expect(tracked).toContain("agency.json")
1356
+ expect(tracked).not.toContain("repos/agency")
1357
+
1358
+ await runGit(["clone", root, restored])
1359
+ const planned = parseJson(
1360
+ await runCli(["repo", "setup", "--dry-run", "--json"], restored),
1361
+ )
1362
+ expect(planned.actions).toEqual([
1363
+ expect.objectContaining({
1364
+ alias: "agency",
1365
+ kind: "materialize",
1366
+ status: "planned",
1367
+ }),
1368
+ ])
1369
+ expect(await Bun.file(join(restored, "repos/agency/HEAD")).exists()).toBe(
1370
+ false,
1371
+ )
1372
+
1373
+ const applied = parseJson(
1374
+ await runCli(["repo", "setup", "--apply", "--json"], restored),
1375
+ )
1376
+ expect(applied.actions[0]).toMatchObject({
1377
+ alias: "agency",
1378
+ status: "applied",
1379
+ })
1380
+ expect(await Bun.file(join(restored, "repos/agency/HEAD")).exists()).toBe(
1381
+ true,
1382
+ )
1383
+
1384
+ const prepared = parseJson(
1385
+ await runCli(["work", "prepare", "portable", "--json"], restored),
1386
+ )
1387
+ expect(prepared.checkouts).toEqual([
1388
+ expect.objectContaining({
1389
+ repo: "agency",
1390
+ action: "created",
1391
+ }),
1392
+ ])
1393
+ expect(
1394
+ await Bun.file(
1395
+ join(restored, "tasks/portable/code/agency/README.md"),
1396
+ ).text(),
1397
+ ).toBe("portable\n")
1398
+ } finally {
1399
+ daemon.process.kill()
1400
+ await daemon.process.exited
1401
+ }
1402
+ }, 30_000)
984
1403
  })
@@ -129,6 +129,28 @@ status: open
129
129
  )
130
130
  })
131
131
 
132
+ test("warns when customized OpenCode config cannot guarantee access", async () => {
133
+ const custom = '{"references":{}}\n'
134
+ await write(root, ".opencode/opencode.jsonc", custom)
135
+
136
+ const logs = await captureLogs(() =>
137
+ runTestEffect(doctor({ cwd: root, json: true })),
138
+ )
139
+ const check = JSON.parse(logs[0]!).checks.find(
140
+ (value: { id: string }) => value.id === "integration.file.opencode",
141
+ )
142
+
143
+ expect(check).toMatchObject({
144
+ level: "warning",
145
+ status: "fail",
146
+ message: expect.stringContaining("cannot guarantee whole-workbase"),
147
+ remediation: expect.stringContaining("global config"),
148
+ })
149
+ expect(await Bun.file(join(root, ".opencode/opencode.jsonc")).text()).toBe(
150
+ custom,
151
+ )
152
+ })
153
+
132
154
  test("is safe when the workbase is read-only", async () => {
133
155
  for (const path of [
134
156
  join(root, "agency.json"),
@@ -38,9 +38,10 @@ describe("init command", () => {
38
38
  join(root, ".opencode/opencode.jsonc"),
39
39
  ).text()
40
40
  const config = JSON.parse(opencode.slice(opencode.indexOf("\n\n") + 2))
41
- expect(config.permission.external_directory).toEqual({
42
- "../**": "allow",
41
+ expect(config.references).toEqual({
42
+ workbase: expect.objectContaining({ path: ".." }),
43
43
  })
44
+ expect(config.permission).toBeUndefined()
44
45
  })
45
46
 
46
47
  test("preserves existing gitignore entries", async () => {
@@ -1,4 +1,5 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
+ import { mkdir } from "node:fs/promises"
2
3
  import { join } from "node:path"
3
4
  import {
4
5
  captureLogs,
@@ -29,11 +30,30 @@ describe("integration command", () => {
29
30
  root,
30
31
  files: [
31
32
  { name: "agents", state: "missing" },
32
- { name: "opencode", state: "missing" },
33
+ {
34
+ name: "opencode",
35
+ state: "missing",
36
+ diagnostic: expect.stringContaining("cannot load"),
37
+ remediation: expect.stringContaining("integration sync"),
38
+ },
33
39
  ],
34
40
  })
35
41
  })
36
42
 
43
+ test("explains remediation for customized OpenCode config", async () => {
44
+ await mkdir(join(root, ".opencode"))
45
+ await Bun.write(
46
+ join(root, ".opencode", "opencode.json"),
47
+ '{"model":"test/model"}\n',
48
+ )
49
+ const logs = await captureLogs(() =>
50
+ runTestEffect(integration({ subcommand: "status", cwd: root })),
51
+ )
52
+
53
+ expect(logs.join("\n")).toContain("cannot guarantee whole-workbase")
54
+ expect(logs.join("\n")).toContain("global config")
55
+ })
56
+
37
57
  test("explicitly synchronizes integration files", async () => {
38
58
  const logs = await captureLogs(() =>
39
59
  runTestEffect(integration({ subcommand: "sync", cwd: root, json: true })),
@@ -22,7 +22,8 @@ export const integration = (options: IntegrationOptions) =>
22
22
  return
23
23
  }
24
24
  for (const file of result.files) {
25
- log(`${file.name}\t${file.state}\t${file.path}`)
25
+ log(`${file.name}\t${file.state}\t${file.path}\t${file.diagnostic}`)
26
+ if (file.remediation) log(` Remediation: ${file.remediation}`)
26
27
  }
27
28
  return
28
29
  }
@@ -35,8 +36,9 @@ export const integration = (options: IntegrationOptions) =>
35
36
  }
36
37
  for (const file of result.files) {
37
38
  log(
38
- `${file.name}\t${file.changed ? "synced" : file.state}\t${file.path}`,
39
+ `${file.name}\t${file.changed ? "synced" : file.state}\t${file.path}\t${file.diagnostic}`,
39
40
  )
41
+ if (file.remediation) log(` Remediation: ${file.remediation}`)
40
42
  }
41
43
  return
42
44
  }
@@ -51,10 +53,12 @@ export const integration = (options: IntegrationOptions) =>
51
53
  export const help = `
52
54
  Usage: agency integration <subcommand>
53
55
 
54
- Inspect or explicitly synchronize managed agent integration files.
56
+ Inspect or explicitly synchronize managed agent integration files. OpenCode
57
+ launches load the managed file at runtime to provide whole-workbase read
58
+ access without changing Agency write authority.
55
59
 
56
60
  Subcommands:
57
- status Report managed, customized, missing, and drifted files
61
+ status Report file state, access diagnostics, and safe remediation
58
62
  sync Create or update checksum-safe managed files
59
63
 
60
64
  Options:
@@ -7,20 +7,39 @@ import { pr } from "./pr"
7
7
  describe("pr command", () => {
8
8
  test("outputs the created pull request URL as JSON", async () => {
9
9
  const url = "https://github.com/markjaquith/agency/pull/123"
10
+ let received: unknown[] = []
10
11
  const logs = await captureLogs(() =>
11
12
  Effect.runPromise(
12
13
  pr({
13
14
  subcommand: "create",
14
15
  taskId: "example",
16
+ phaseId: "implementation",
17
+ draft: true,
18
+ force: true,
19
+ cwd: "/workbase",
15
20
  json: true,
16
21
  }).pipe(
17
22
  Effect.provideService(PullRequestService, {
18
- create: () => Effect.succeed(url),
23
+ create: (...args: unknown[]) => {
24
+ received = args
25
+ return Effect.succeed(url)
26
+ },
19
27
  } as never),
20
28
  ) as Effect.Effect<void, unknown, never>,
21
29
  ),
22
30
  )
23
31
 
24
32
  expect(JSON.parse(logs[0]!)).toEqual({ url })
33
+ expect(received).toEqual([
34
+ "example",
35
+ "implementation",
36
+ true,
37
+ "/workbase",
38
+ expect.objectContaining({
39
+ force: true,
40
+ draft: true,
41
+ json: true,
42
+ }),
43
+ ])
25
44
  })
26
45
  })