@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.
- package/README.md +72 -16
- package/cli.ts +3 -0
- package/fixtures/protocol/skill-setup-commands.json +18 -0
- package/package.json +1 -1
- package/skills/agency/SKILL.md +30 -15
- package/skills/agency/references/commands.md +38 -17
- package/skills/agency/references/contracts.md +46 -19
- package/skills/agency/references/recipes.md +52 -12
- package/src/cli-parser.test.ts +7 -0
- package/src/cli-parser.ts +13 -2
- package/src/cli.test.ts +422 -3
- package/src/commands/doctor.test.ts +22 -0
- package/src/commands/init.test.ts +3 -2
- package/src/commands/integration.test.ts +21 -1
- package/src/commands/integration.ts +8 -4
- package/src/commands/pr.test.ts +20 -1
- package/src/commands/repo.test.ts +66 -1
- package/src/commands/repo.ts +35 -8
- package/src/commands/status.test.ts +22 -0
- package/src/commands/status.ts +1 -0
- package/src/commands/sync.ts +5 -3
- package/src/commands/work.test.ts +74 -1
- package/src/commands/work.ts +26 -3
- package/src/graph-schema.test.ts +52 -4
- package/src/protocol.test.ts +41 -8
- package/src/readiness.test.ts +75 -17
- package/src/services/DoctorService.ts +32 -18
- package/src/services/EpicService.ts +1 -1
- package/src/services/GraphMutationService.ts +3 -3
- package/src/services/GraphService.ts +13 -4
- package/src/services/IntegrationService.test.ts +37 -12
- package/src/services/IntegrationService.ts +70 -21
- package/src/services/PhaseService.ts +1 -1
- package/src/services/ReadinessService.test.ts +47 -0
- package/src/services/RepositoryService.test.ts +299 -5
- package/src/services/RepositoryService.ts +725 -98
- package/src/services/SyncService.test.ts +36 -0
- package/src/services/SyncService.ts +20 -1
- package/src/services/TaskService.ts +1 -1
- package/src/services/WorkbaseService.test.ts +32 -0
- package/src/services/WorkbaseService.ts +22 -14
- package/src/services/WorktreeLock.test.ts +122 -0
- package/src/services/WorktreeService.test.ts +17 -2
- package/src/services/WorktreeService.ts +3 -3
- package/src/utils/process.test.ts +4 -3
- package/src/workbase/AGENTS.md +9 -1
- package/src/workbase/dependency-graph.test.ts +50 -0
- package/src/workbase/opencode-file.ts +3 -13
- package/src/workbase/schemas.test.ts +52 -0
- 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:
|
|
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:
|
|
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.
|
|
42
|
-
|
|
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
|
-
{
|
|
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
|
|
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:
|
package/src/commands/pr.test.ts
CHANGED
|
@@ -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: () =>
|
|
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
|
})
|