@markjaquith/agency 2.7.2 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -7
- package/cli.ts +33 -98
- package/package.json +1 -1
- package/skills/agency/SKILL.md +8 -1
- package/src/cli-parser.test.ts +205 -0
- package/src/cli-parser.ts +573 -0
- package/src/cli.test.ts +49 -2
- package/src/commands/init.test.ts +3 -8
- package/src/commands/integration.test.ts +51 -0
- package/src/commands/integration.ts +62 -0
- package/src/commands/read-only.test.ts +144 -0
- package/src/commands/task.test.ts +43 -0
- package/src/commands/task.ts +75 -38
- package/src/commands/validate.test.ts +28 -0
- package/src/commands/validate.ts +10 -1
- package/src/commands/work.test.ts +48 -0
- package/src/commands/work.ts +13 -1
- package/src/services/IntegrationService.test.ts +137 -0
- package/src/services/IntegrationService.ts +136 -0
- package/src/services/WorkbaseService.test.ts +4 -118
- package/src/services/WorkbaseService.ts +0 -53
- package/src/test-utils.ts +2 -0
- package/src/utils/command.ts +2 -0
- package/src/workbase/AGENTS.md +2 -0
- package/src/workbase/workbase-choice.ts +8 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
|
+
import { Effect } from "effect"
|
|
3
|
+
import { createHash } from "node:crypto"
|
|
4
|
+
import { mkdir, symlink, unlink } from "node:fs/promises"
|
|
5
|
+
import { dirname, join } from "node:path"
|
|
6
|
+
import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
|
|
7
|
+
import { managedWorkbaseAgents } from "../workbase/agents-file"
|
|
8
|
+
import { managedWorkbaseOpencode } from "../workbase/opencode-file"
|
|
9
|
+
import { IntegrationService } from "./IntegrationService"
|
|
10
|
+
|
|
11
|
+
const write = async (root: string, path: string, content: string) => {
|
|
12
|
+
const fullPath = join(root, path)
|
|
13
|
+
await mkdir(dirname(fullPath), { recursive: true })
|
|
14
|
+
await Bun.write(fullPath, content)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const managed = (prefix: string, body: string, suffix = "") => {
|
|
18
|
+
const checksum = createHash("sha256").update(body).digest("hex")
|
|
19
|
+
return `${prefix}${checksum}${suffix}\n\n${body}`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const status = (root: string) =>
|
|
23
|
+
runTestEffect(
|
|
24
|
+
IntegrationService.pipe(Effect.flatMap((service) => service.status(root))),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
const sync = (root: string) =>
|
|
28
|
+
runTestEffect(
|
|
29
|
+
IntegrationService.pipe(Effect.flatMap((service) => service.sync(root))),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
describe("IntegrationService", () => {
|
|
33
|
+
let root: string
|
|
34
|
+
|
|
35
|
+
beforeEach(async () => {
|
|
36
|
+
root = await createTempDir()
|
|
37
|
+
await write(root, "agency.json", '{"version":2}\n')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
afterEach(async () => cleanupTempDir(root))
|
|
41
|
+
|
|
42
|
+
test("reports missing and current managed files without writing", async () => {
|
|
43
|
+
expect((await status(root)).files.map(({ state }) => state)).toEqual([
|
|
44
|
+
"missing",
|
|
45
|
+
"missing",
|
|
46
|
+
])
|
|
47
|
+
expect(await Bun.file(join(root, "AGENTS.md")).exists()).toBe(false)
|
|
48
|
+
|
|
49
|
+
await write(root, "AGENTS.md", managedWorkbaseAgents)
|
|
50
|
+
await write(root, ".opencode/opencode.jsonc", managedWorkbaseOpencode(root))
|
|
51
|
+
expect((await status(root)).files.map(({ state }) => state)).toEqual([
|
|
52
|
+
"managed",
|
|
53
|
+
"managed",
|
|
54
|
+
])
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test("reports customized and checksum-safe drifted files", async () => {
|
|
58
|
+
await write(root, "AGENTS.md", "# Custom instructions\n")
|
|
59
|
+
await write(
|
|
60
|
+
root,
|
|
61
|
+
".opencode/opencode.jsonc",
|
|
62
|
+
managed("// agency-managed: sha256=", '{"references":{}}\n'),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
expect((await status(root)).files.map(({ state }) => state)).toEqual([
|
|
66
|
+
"customized",
|
|
67
|
+
"drifted",
|
|
68
|
+
])
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test("treats an existing JSON OpenCode config as customized", async () => {
|
|
72
|
+
await write(root, ".opencode/opencode.json", '{"model":"test/model"}\n')
|
|
73
|
+
|
|
74
|
+
const result = await status(root)
|
|
75
|
+
expect(result.files[1]).toEqual({
|
|
76
|
+
name: "opencode",
|
|
77
|
+
path: join(root, ".opencode/opencode.json"),
|
|
78
|
+
state: "customized",
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test("syncs missing and drifted files while preserving customized files", async () => {
|
|
83
|
+
const customAgents = "# Custom instructions\n"
|
|
84
|
+
await write(root, "AGENTS.md", customAgents)
|
|
85
|
+
await write(
|
|
86
|
+
root,
|
|
87
|
+
".opencode/opencode.jsonc",
|
|
88
|
+
managed("// agency-managed: sha256=", '{"references":{}}\n'),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
const first = await sync(root)
|
|
92
|
+
expect(first.files).toMatchObject([
|
|
93
|
+
{ name: "agents", state: "customized", changed: false },
|
|
94
|
+
{ name: "opencode", state: "managed", changed: true },
|
|
95
|
+
])
|
|
96
|
+
expect(await Bun.file(join(root, "AGENTS.md")).text()).toBe(customAgents)
|
|
97
|
+
expect(await Bun.file(join(root, ".opencode/opencode.jsonc")).text()).toBe(
|
|
98
|
+
managedWorkbaseOpencode(root),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
await unlink(join(root, "AGENTS.md"))
|
|
102
|
+
const second = await sync(root)
|
|
103
|
+
expect(second.files[0]).toMatchObject({ state: "managed", changed: true })
|
|
104
|
+
expect(await Bun.file(join(root, "AGENTS.md")).text()).toBe(
|
|
105
|
+
managedWorkbaseAgents,
|
|
106
|
+
)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test("does not overwrite managed files whose checksums no longer match", async () => {
|
|
110
|
+
const tampered = `${managed(
|
|
111
|
+
"<!-- agency-managed: sha256=",
|
|
112
|
+
"# Previous Agency instructions\n",
|
|
113
|
+
" -->",
|
|
114
|
+
)}User edit\n`
|
|
115
|
+
await write(root, "AGENTS.md", tampered)
|
|
116
|
+
|
|
117
|
+
const result = await sync(root)
|
|
118
|
+
expect(result.files[0]).toMatchObject({
|
|
119
|
+
state: "customized",
|
|
120
|
+
changed: false,
|
|
121
|
+
})
|
|
122
|
+
expect(await Bun.file(join(root, "AGENTS.md")).text()).toBe(tampered)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test("does not follow symlinked integration files", async () => {
|
|
126
|
+
const target = join(root, "custom-agents.md")
|
|
127
|
+
await Bun.write(target, "# External instructions\n")
|
|
128
|
+
await symlink(target, join(root, "AGENTS.md"))
|
|
129
|
+
|
|
130
|
+
const result = await sync(root)
|
|
131
|
+
expect(result.files[0]).toMatchObject({
|
|
132
|
+
state: "customized",
|
|
133
|
+
changed: false,
|
|
134
|
+
})
|
|
135
|
+
expect(await Bun.file(target).text()).toBe("# External instructions\n")
|
|
136
|
+
})
|
|
137
|
+
})
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { Effect } from "effect"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import { FileSystemService } from "./FileSystemService"
|
|
4
|
+
import { WorkbaseService } from "./WorkbaseService"
|
|
5
|
+
import {
|
|
6
|
+
canUpdateManagedWorkbaseAgents,
|
|
7
|
+
managedWorkbaseAgents,
|
|
8
|
+
} from "../workbase/agents-file"
|
|
9
|
+
import {
|
|
10
|
+
canUpdateManagedWorkbaseOpencode,
|
|
11
|
+
managedWorkbaseOpencode,
|
|
12
|
+
} from "../workbase/opencode-file"
|
|
13
|
+
|
|
14
|
+
type IntegrationFileState = "managed" | "customized" | "missing" | "drifted"
|
|
15
|
+
|
|
16
|
+
interface IntegrationFileStatus {
|
|
17
|
+
readonly name: "agents" | "opencode"
|
|
18
|
+
readonly path: string
|
|
19
|
+
readonly state: IntegrationFileState
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface IntegrationSyncFile extends IntegrationFileStatus {
|
|
23
|
+
readonly changed: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const classify = (
|
|
27
|
+
name: IntegrationFileStatus["name"],
|
|
28
|
+
path: string,
|
|
29
|
+
content: string,
|
|
30
|
+
managed: string,
|
|
31
|
+
canUpdate: (content: string) => boolean,
|
|
32
|
+
): IntegrationFileStatus => ({
|
|
33
|
+
name,
|
|
34
|
+
path,
|
|
35
|
+
state:
|
|
36
|
+
content === managed
|
|
37
|
+
? "managed"
|
|
38
|
+
: canUpdate(content)
|
|
39
|
+
? "drifted"
|
|
40
|
+
: "customized",
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const inspect = (root: string) =>
|
|
44
|
+
Effect.gen(function* () {
|
|
45
|
+
const fs = yield* FileSystemService
|
|
46
|
+
const agentsPath = join(root, "AGENTS.md")
|
|
47
|
+
const opencodeDirectory = join(root, ".opencode")
|
|
48
|
+
const opencodePath = join(opencodeDirectory, "opencode.jsonc")
|
|
49
|
+
const opencodeJsonPath = join(opencodeDirectory, "opencode.json")
|
|
50
|
+
const files: IntegrationFileStatus[] = []
|
|
51
|
+
|
|
52
|
+
files.push(
|
|
53
|
+
(yield* fs.readSymlinkTarget(agentsPath)) !== null
|
|
54
|
+
? { name: "agents", path: agentsPath, state: "customized" }
|
|
55
|
+
: (yield* fs.exists(agentsPath))
|
|
56
|
+
? classify(
|
|
57
|
+
"agents",
|
|
58
|
+
agentsPath,
|
|
59
|
+
yield* fs.readFile(agentsPath),
|
|
60
|
+
managedWorkbaseAgents,
|
|
61
|
+
canUpdateManagedWorkbaseAgents,
|
|
62
|
+
)
|
|
63
|
+
: { name: "agents", path: agentsPath, state: "missing" },
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if ((yield* fs.readSymlinkTarget(opencodePath)) !== null) {
|
|
67
|
+
files.push({
|
|
68
|
+
name: "opencode",
|
|
69
|
+
path: opencodePath,
|
|
70
|
+
state: "customized",
|
|
71
|
+
})
|
|
72
|
+
} else if (yield* fs.exists(opencodePath)) {
|
|
73
|
+
files.push(
|
|
74
|
+
classify(
|
|
75
|
+
"opencode",
|
|
76
|
+
opencodePath,
|
|
77
|
+
yield* fs.readFile(opencodePath),
|
|
78
|
+
managedWorkbaseOpencode(root),
|
|
79
|
+
canUpdateManagedWorkbaseOpencode,
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
} else if (yield* fs.exists(opencodeJsonPath)) {
|
|
83
|
+
files.push({
|
|
84
|
+
name: "opencode",
|
|
85
|
+
path: opencodeJsonPath,
|
|
86
|
+
state: "customized",
|
|
87
|
+
})
|
|
88
|
+
} else {
|
|
89
|
+
files.push({ name: "opencode", path: opencodePath, state: "missing" })
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return files
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
export class IntegrationService extends Effect.Service<IntegrationService>()(
|
|
96
|
+
"IntegrationService",
|
|
97
|
+
{
|
|
98
|
+
sync: () => ({
|
|
99
|
+
status: (startPath: string = process.cwd()) =>
|
|
100
|
+
Effect.gen(function* () {
|
|
101
|
+
const workbase = yield* WorkbaseService
|
|
102
|
+
const root = yield* workbase.discover(startPath)
|
|
103
|
+
return { root, files: yield* inspect(root) }
|
|
104
|
+
}),
|
|
105
|
+
|
|
106
|
+
sync: (startPath: string = process.cwd()) =>
|
|
107
|
+
Effect.gen(function* () {
|
|
108
|
+
const fs = yield* FileSystemService
|
|
109
|
+
const workbase = yield* WorkbaseService
|
|
110
|
+
const root = yield* workbase.discover(startPath)
|
|
111
|
+
const statuses = yield* inspect(root)
|
|
112
|
+
const files: IntegrationSyncFile[] = []
|
|
113
|
+
|
|
114
|
+
for (const status of statuses) {
|
|
115
|
+
const changed =
|
|
116
|
+
status.state === "missing" || status.state === "drifted"
|
|
117
|
+
if (changed) {
|
|
118
|
+
if (status.name === "agents") {
|
|
119
|
+
yield* fs.writeFile(status.path, managedWorkbaseAgents)
|
|
120
|
+
} else {
|
|
121
|
+
yield* fs.createDirectory(join(root, ".opencode"))
|
|
122
|
+
yield* fs.writeFile(status.path, managedWorkbaseOpencode(root))
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
files.push({
|
|
126
|
+
...status,
|
|
127
|
+
state: changed ? "managed" : status.state,
|
|
128
|
+
changed,
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { root, files }
|
|
133
|
+
}),
|
|
134
|
+
}),
|
|
135
|
+
},
|
|
136
|
+
) {}
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
2
|
import { Effect } from "effect"
|
|
3
|
-
import { createHash } from "node:crypto"
|
|
4
3
|
import { mkdir, realpath } from "node:fs/promises"
|
|
5
4
|
import { dirname, join } from "node:path"
|
|
6
5
|
import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
|
|
7
|
-
import { managedWorkbaseAgents } from "../workbase/agents-file"
|
|
8
|
-
import { managedWorkbaseOpencode } from "../workbase/opencode-file"
|
|
9
6
|
import { WorkbaseService } from "./WorkbaseService"
|
|
10
7
|
|
|
11
8
|
const write = async (root: string, path: string, content: string) => {
|
|
@@ -14,16 +11,6 @@ const write = async (root: string, path: string, content: string) => {
|
|
|
14
11
|
await Bun.write(fullPath, content)
|
|
15
12
|
}
|
|
16
13
|
|
|
17
|
-
const managedAgents = (body: string) => {
|
|
18
|
-
const checksum = createHash("sha256").update(body).digest("hex")
|
|
19
|
-
return `<!-- agency-managed: sha256=${checksum} -->\n\n${body}`
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const managedOpencode = (body: string) => {
|
|
23
|
-
const checksum = createHash("sha256").update(body).digest("hex")
|
|
24
|
-
return `// agency-managed: sha256=${checksum}\n\n${body}`
|
|
25
|
-
}
|
|
26
|
-
|
|
27
14
|
describe("WorkbaseService", () => {
|
|
28
15
|
let root: string
|
|
29
16
|
|
|
@@ -48,12 +35,10 @@ describe("WorkbaseService", () => {
|
|
|
48
35
|
)
|
|
49
36
|
|
|
50
37
|
expect(discovered).toBe(root)
|
|
51
|
-
expect(await Bun.file(join(root, "AGENTS.md")).
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
managedWorkbaseOpencode(root),
|
|
56
|
-
)
|
|
38
|
+
expect(await Bun.file(join(root, "AGENTS.md")).exists()).toBe(false)
|
|
39
|
+
expect(
|
|
40
|
+
await Bun.file(join(root, ".opencode/opencode.jsonc")).exists(),
|
|
41
|
+
).toBe(false)
|
|
57
42
|
})
|
|
58
43
|
|
|
59
44
|
test("registers canonical workbase paths without duplicates", async () => {
|
|
@@ -88,105 +73,6 @@ describe("WorkbaseService", () => {
|
|
|
88
73
|
).toEqual({ version: 1, workbases: [first] })
|
|
89
74
|
})
|
|
90
75
|
|
|
91
|
-
test("preserves an unmanaged workbase OpenCode config", async () => {
|
|
92
|
-
await write(root, "agency.json", '{"version":2}\n')
|
|
93
|
-
await write(root, ".opencode/opencode.jsonc", '{"model":"test/model"}\n')
|
|
94
|
-
|
|
95
|
-
await runTestEffect(
|
|
96
|
-
WorkbaseService.pipe(Effect.flatMap((service) => service.discover(root))),
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
expect(await Bun.file(join(root, ".opencode/opencode.jsonc")).text()).toBe(
|
|
100
|
-
'{"model":"test/model"}\n',
|
|
101
|
-
)
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
test("does not override an existing JSON OpenCode config", async () => {
|
|
105
|
-
await write(root, "agency.json", '{"version":2}\n')
|
|
106
|
-
await write(root, ".opencode/opencode.json", '{"model":"test/model"}\n')
|
|
107
|
-
|
|
108
|
-
await runTestEffect(
|
|
109
|
-
WorkbaseService.pipe(Effect.flatMap((service) => service.discover(root))),
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
expect(
|
|
113
|
-
await Bun.file(join(root, ".opencode/opencode.jsonc")).exists(),
|
|
114
|
-
).toBe(false)
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
test("updates an unmodified managed workbase OpenCode config", async () => {
|
|
118
|
-
await write(root, "agency.json", '{"version":2}\n')
|
|
119
|
-
await write(
|
|
120
|
-
root,
|
|
121
|
-
".opencode/opencode.jsonc",
|
|
122
|
-
managedOpencode('{"references":{}}\n'),
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
await runTestEffect(
|
|
126
|
-
WorkbaseService.pipe(Effect.flatMap((service) => service.discover(root))),
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
expect(await Bun.file(join(root, ".opencode/opencode.jsonc")).text()).toBe(
|
|
130
|
-
managedWorkbaseOpencode(root),
|
|
131
|
-
)
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
test("preserves a modified managed workbase OpenCode config", async () => {
|
|
135
|
-
await write(root, "agency.json", '{"version":2}\n')
|
|
136
|
-
const content = `${managedOpencode('{"references":{}}\n')}\n// User edit\n`
|
|
137
|
-
await write(root, ".opencode/opencode.jsonc", content)
|
|
138
|
-
|
|
139
|
-
await runTestEffect(
|
|
140
|
-
WorkbaseService.pipe(Effect.flatMap((service) => service.discover(root))),
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
expect(await Bun.file(join(root, ".opencode/opencode.jsonc")).text()).toBe(
|
|
144
|
-
content,
|
|
145
|
-
)
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
test("preserves an unmanaged workbase AGENTS.md", async () => {
|
|
149
|
-
await write(root, "agency.json", '{"version":2}\n')
|
|
150
|
-
await write(root, "AGENTS.md", "# Custom instructions\n")
|
|
151
|
-
|
|
152
|
-
await runTestEffect(
|
|
153
|
-
WorkbaseService.pipe(Effect.flatMap((service) => service.discover(root))),
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
expect(await Bun.file(join(root, "AGENTS.md")).text()).toBe(
|
|
157
|
-
"# Custom instructions\n",
|
|
158
|
-
)
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
test("updates an unmodified managed workbase AGENTS.md", async () => {
|
|
162
|
-
await write(root, "agency.json", '{"version":2}\n')
|
|
163
|
-
await write(
|
|
164
|
-
root,
|
|
165
|
-
"AGENTS.md",
|
|
166
|
-
managedAgents("# Previous Agency instructions\n"),
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
await runTestEffect(
|
|
170
|
-
WorkbaseService.pipe(Effect.flatMap((service) => service.discover(root))),
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
expect(await Bun.file(join(root, "AGENTS.md")).text()).toBe(
|
|
174
|
-
managedWorkbaseAgents,
|
|
175
|
-
)
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
test("preserves a modified managed workbase AGENTS.md", async () => {
|
|
179
|
-
await write(root, "agency.json", '{"version":2}\n')
|
|
180
|
-
const content = `${managedAgents("# Previous Agency instructions\n")}\nUser edit\n`
|
|
181
|
-
await write(root, "AGENTS.md", content)
|
|
182
|
-
|
|
183
|
-
await runTestEffect(
|
|
184
|
-
WorkbaseService.pipe(Effect.flatMap((service) => service.discover(root))),
|
|
185
|
-
)
|
|
186
|
-
|
|
187
|
-
expect(await Bun.file(join(root, "AGENTS.md")).text()).toBe(content)
|
|
188
|
-
})
|
|
189
|
-
|
|
190
76
|
test("rejects an invalid worktree command template", async () => {
|
|
191
77
|
await write(
|
|
192
78
|
root,
|
|
@@ -17,14 +17,6 @@ import {
|
|
|
17
17
|
type TaskFrontmatter as TaskData,
|
|
18
18
|
} from "../workbase/schemas"
|
|
19
19
|
import { validateWorktreeCreateCommand } from "../workbase/worktree-command"
|
|
20
|
-
import {
|
|
21
|
-
canUpdateManagedWorkbaseAgents,
|
|
22
|
-
managedWorkbaseAgents,
|
|
23
|
-
} from "../workbase/agents-file"
|
|
24
|
-
import {
|
|
25
|
-
canUpdateManagedWorkbaseOpencode,
|
|
26
|
-
managedWorkbaseOpencode,
|
|
27
|
-
} from "../workbase/opencode-file"
|
|
28
20
|
|
|
29
21
|
class WorkbaseNotFoundError extends Data.TaggedError("WorkbaseNotFoundError")<{
|
|
30
22
|
readonly message: string
|
|
@@ -119,48 +111,6 @@ const readRegistry = (configDirectory?: string) =>
|
|
|
119
111
|
return { path, registry: decoded.value }
|
|
120
112
|
})
|
|
121
113
|
|
|
122
|
-
const ensureWorkbaseAgents = (root: string) =>
|
|
123
|
-
Effect.gen(function* () {
|
|
124
|
-
const fs = yield* FileSystemService
|
|
125
|
-
const path = join(root, "AGENTS.md")
|
|
126
|
-
if (!(yield* fs.exists(path))) {
|
|
127
|
-
yield* fs.writeFile(path, managedWorkbaseAgents)
|
|
128
|
-
return
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const content = yield* fs.readFile(path)
|
|
132
|
-
if (
|
|
133
|
-
content !== managedWorkbaseAgents &&
|
|
134
|
-
canUpdateManagedWorkbaseAgents(content)
|
|
135
|
-
) {
|
|
136
|
-
yield* fs.writeFile(path, managedWorkbaseAgents)
|
|
137
|
-
}
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
const ensureWorkbaseOpencode = (root: string) =>
|
|
141
|
-
Effect.gen(function* () {
|
|
142
|
-
const fs = yield* FileSystemService
|
|
143
|
-
const directory = join(root, ".opencode")
|
|
144
|
-
const path = join(directory, "opencode.jsonc")
|
|
145
|
-
const managed = managedWorkbaseOpencode(root)
|
|
146
|
-
if (!(yield* fs.exists(path))) {
|
|
147
|
-
if (yield* fs.exists(join(directory, "opencode.json"))) return
|
|
148
|
-
yield* fs.createDirectory(directory)
|
|
149
|
-
yield* fs.writeFile(path, managed)
|
|
150
|
-
return
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const content = yield* fs.readFile(path)
|
|
154
|
-
if (content !== managed && canUpdateManagedWorkbaseOpencode(content)) {
|
|
155
|
-
yield* fs.writeFile(path, managed)
|
|
156
|
-
}
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
const ensureWorkbaseAgentFiles = (root: string) =>
|
|
160
|
-
Effect.all([ensureWorkbaseAgents(root), ensureWorkbaseOpencode(root)], {
|
|
161
|
-
concurrency: "unbounded",
|
|
162
|
-
})
|
|
163
|
-
|
|
164
114
|
const findCycles = (nodes: readonly Dependency[]): readonly string[] => {
|
|
165
115
|
const dependencies = new Map(
|
|
166
116
|
nodes.map((node) => [node.id, [...(node.dependsOn ?? [])]]),
|
|
@@ -239,8 +189,6 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
239
189
|
`${existing}${prefix}${missing.join("\n")}\n`,
|
|
240
190
|
)
|
|
241
191
|
}
|
|
242
|
-
yield* ensureWorkbaseAgentFiles(root)
|
|
243
|
-
|
|
244
192
|
return root
|
|
245
193
|
}),
|
|
246
194
|
|
|
@@ -296,7 +244,6 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
296
244
|
})
|
|
297
245
|
}
|
|
298
246
|
}
|
|
299
|
-
yield* ensureWorkbaseAgentFiles(current)
|
|
300
247
|
return current
|
|
301
248
|
}
|
|
302
249
|
}
|
package/src/test-utils.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { PhaseService } from "./services/PhaseService"
|
|
|
12
12
|
import { WorktreeService } from "./services/WorktreeService"
|
|
13
13
|
import { PullRequestService } from "./services/PullRequestService"
|
|
14
14
|
import { ArchiveService } from "./services/ArchiveService"
|
|
15
|
+
import { IntegrationService } from "./services/IntegrationService"
|
|
15
16
|
|
|
16
17
|
export const createTempDir = () => mkdtemp(join(tmpdir(), "agency-test-"))
|
|
17
18
|
|
|
@@ -28,6 +29,7 @@ const TestLayer = Layer.mergeAll(
|
|
|
28
29
|
WorktreeService.Default,
|
|
29
30
|
PullRequestService.Default,
|
|
30
31
|
ArchiveService.Default,
|
|
32
|
+
IntegrationService.Default,
|
|
31
33
|
)
|
|
32
34
|
|
|
33
35
|
export async function runTestEffect<A, E>(
|
package/src/utils/command.ts
CHANGED
|
@@ -5,6 +5,8 @@ export interface BaseCommandOptions {
|
|
|
5
5
|
readonly silent?: boolean
|
|
6
6
|
readonly verbose?: boolean
|
|
7
7
|
readonly json?: boolean
|
|
8
|
+
/** Whether this invocation may open prompts or selectors. */
|
|
9
|
+
readonly inputAllowed?: boolean
|
|
8
10
|
/**
|
|
9
11
|
* Working directory to use instead of process.cwd().
|
|
10
12
|
* Primarily used for testing to enable concurrent test execution.
|
package/src/workbase/AGENTS.md
CHANGED
|
@@ -22,6 +22,8 @@ field. Repositories listed in plural `repos` are read-only references.
|
|
|
22
22
|
|
|
23
23
|
## Safety
|
|
24
24
|
|
|
25
|
+
- Discovery and observational commands are read-only; use
|
|
26
|
+
`agency integration sync` to update managed agent files explicitly.
|
|
25
27
|
- Keep task-level decisions in `TASK.md` and phase-specific delivery context in
|
|
26
28
|
`PHASE.md`.
|
|
27
29
|
- Keep execution-unit `status` current with `agency task status` or
|
|
@@ -34,6 +34,7 @@ export const resolveWorkbase = (
|
|
|
34
34
|
startPath: string,
|
|
35
35
|
log: (message: string) => void,
|
|
36
36
|
pick: PickWorkbase = pickWorkbase,
|
|
37
|
+
inputAllowed = true,
|
|
37
38
|
) =>
|
|
38
39
|
Effect.gen(function* () {
|
|
39
40
|
const fs = yield* FileSystemService
|
|
@@ -50,6 +51,13 @@ export const resolveWorkbase = (
|
|
|
50
51
|
),
|
|
51
52
|
)
|
|
52
53
|
}
|
|
54
|
+
if (!inputAllowed) {
|
|
55
|
+
return yield* Effect.fail(
|
|
56
|
+
new Error(
|
|
57
|
+
"Workbase selection requires interactive input; provide an explicit path or run Agency from a workbase",
|
|
58
|
+
),
|
|
59
|
+
)
|
|
60
|
+
}
|
|
53
61
|
|
|
54
62
|
const fzf = yield* fs.runCommand(["which", "fzf"], {
|
|
55
63
|
captureOutput: true,
|