@markjaquith/agency 2.36.0 → 2.37.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 +22 -12
- package/package.json +1 -1
- package/src/commands/doctor.test.ts +3 -2
- package/src/commands/init.test.ts +3 -1
- package/src/commands/integration.test.ts +3 -2
- package/src/services/FileSystemService.ts +11 -0
- package/src/services/IntegrationService.test.ts +52 -10
- package/src/services/IntegrationService.ts +30 -9
- package/src/workbase/AGENTS.md +7 -3
- package/src/workbase/opencode-file.ts +1 -0
package/README.md
CHANGED
|
@@ -47,9 +47,11 @@ and customized documents remain valid.
|
|
|
47
47
|
|
|
48
48
|
```text
|
|
49
49
|
workbase/
|
|
50
|
-
AGENTS.md #
|
|
50
|
+
AGENTS.md # optional user-owned workbase instructions
|
|
51
|
+
.agency/
|
|
52
|
+
AGENTS.md # managed Agency instructions
|
|
51
53
|
.opencode/
|
|
52
|
-
opencode.jsonc # managed whole-workbase reference
|
|
54
|
+
opencode.jsonc # managed instructions and whole-workbase reference
|
|
53
55
|
agency.json # tracked config and portable repository declarations
|
|
54
56
|
repos/ # ignored local materializations
|
|
55
57
|
frontend/ # bare Git repository or symlink
|
|
@@ -77,18 +79,26 @@ workbase/
|
|
|
77
79
|
```
|
|
78
80
|
|
|
79
81
|
Agency keeps discovery and other observational commands read-only. Run
|
|
80
|
-
`agency integration status` to inspect
|
|
82
|
+
`agency integration status` to inspect `.agency/AGENTS.md` and
|
|
81
83
|
`.opencode/opencode.jsonc`, then `agency integration sync` to create missing
|
|
82
84
|
files or refresh checksum-safe managed files. Customized files are reported but
|
|
83
|
-
never overwritten.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
85
|
+
never overwritten. The root `AGENTS.md` is user-owned and is not inspected or
|
|
86
|
+
modified by Agency.
|
|
87
|
+
|
|
88
|
+
When upgrading an existing workbase, synchronization moves a checksum-valid
|
|
89
|
+
Agency-managed root `AGENTS.md` to `.agency/AGENTS.md` once the OpenCode config
|
|
90
|
+
can load the hidden file. A customized root file, including a symlink, is
|
|
91
|
+
preserved as user-owned content.
|
|
92
|
+
|
|
93
|
+
The OpenCode config loads Agency's hidden instructions in addition to any
|
|
94
|
+
user-owned root `AGENTS.md` and advertises the complete workbase as one portable
|
|
95
|
+
relative reference. `agency work` sets `OPENCODE_CONFIG` to the managed file and
|
|
96
|
+
injects runtime-only absolute rules that allow external access across the
|
|
97
|
+
workbase while denying edits outside the execution unit's writable checkout.
|
|
98
|
+
This makes the config effective from nested task and phase Git checkouts without
|
|
99
|
+
persisting a machine-specific path or granting access outside the workbase. Bash
|
|
100
|
+
and Agency operations must still follow the write authority reported by
|
|
101
|
+
`agency context`.
|
|
92
102
|
|
|
93
103
|
Repository aliases and canonical fetch remotes are declared in tracked
|
|
94
104
|
`agency.json`; local bare clones and symlinks remain ignored under
|
package/package.json
CHANGED
|
@@ -114,7 +114,7 @@ status: open
|
|
|
114
114
|
test("reports repository, ref, remote, and managed-file failures", async () => {
|
|
115
115
|
await Bun.$`git -C ${repository} remote remove origin`
|
|
116
116
|
await Bun.$`git -C ${repository} branch -m other`
|
|
117
|
-
await write(root, "AGENTS.md", managedAgents("old\n"))
|
|
117
|
+
await write(root, ".agency/AGENTS.md", managedAgents("old\n"))
|
|
118
118
|
|
|
119
119
|
const logs = await captureLogs(() =>
|
|
120
120
|
runTestEffect(doctor({ cwd: root, json: true })),
|
|
@@ -153,7 +153,7 @@ status: open
|
|
|
153
153
|
expect(check).toMatchObject({
|
|
154
154
|
level: "warning",
|
|
155
155
|
status: "fail",
|
|
156
|
-
message: expect.stringContaining("cannot guarantee
|
|
156
|
+
message: expect.stringContaining("cannot guarantee its instructions"),
|
|
157
157
|
remediation: expect.stringContaining("global config"),
|
|
158
158
|
})
|
|
159
159
|
expect(await Bun.file(join(root, ".opencode/opencode.jsonc")).text()).toBe(
|
|
@@ -181,6 +181,7 @@ status: open
|
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
expect(await Bun.file(join(root, "AGENTS.md")).exists()).toBe(false)
|
|
184
|
+
expect(await Bun.file(join(root, ".agency/AGENTS.md")).exists()).toBe(false)
|
|
184
185
|
expect(
|
|
185
186
|
await Bun.file(join(root, ".opencode/opencode.jsonc")).exists(),
|
|
186
187
|
).toBe(false)
|
|
@@ -33,11 +33,13 @@ describe("init command", () => {
|
|
|
33
33
|
expect(await Bun.file(join(root, ".gitignore")).text()).toBe(
|
|
34
34
|
"/repos/\n/tasks/*/code/\n/tasks/*/phases/*/code/\n",
|
|
35
35
|
)
|
|
36
|
-
expect(await Bun.file(join(root, "AGENTS.md")).exists()).toBe(
|
|
36
|
+
expect(await Bun.file(join(root, "AGENTS.md")).exists()).toBe(false)
|
|
37
|
+
expect(await Bun.file(join(root, ".agency/AGENTS.md")).exists()).toBe(true)
|
|
37
38
|
const opencode = await Bun.file(
|
|
38
39
|
join(root, ".opencode/opencode.jsonc"),
|
|
39
40
|
).text()
|
|
40
41
|
const config = JSON.parse(opencode.slice(opencode.indexOf("\n\n") + 2))
|
|
42
|
+
expect(config.instructions).toEqual([".agency/AGENTS.md"])
|
|
41
43
|
expect(config.references).toEqual({
|
|
42
44
|
workbase: expect.objectContaining({ path: ".." }),
|
|
43
45
|
})
|
|
@@ -50,7 +50,7 @@ describe("integration command", () => {
|
|
|
50
50
|
runTestEffect(integration({ subcommand: "status", cwd: root })),
|
|
51
51
|
)
|
|
52
52
|
|
|
53
|
-
expect(logs.join("\n")).toContain("cannot guarantee
|
|
53
|
+
expect(logs.join("\n")).toContain("cannot guarantee its instructions")
|
|
54
54
|
expect(logs.join("\n")).toContain("global config")
|
|
55
55
|
})
|
|
56
56
|
|
|
@@ -63,7 +63,8 @@ describe("integration command", () => {
|
|
|
63
63
|
{ name: "agents", state: "managed", changed: true },
|
|
64
64
|
{ name: "opencode", state: "managed", changed: true },
|
|
65
65
|
])
|
|
66
|
-
expect(await Bun.file(join(root, "AGENTS.md")).exists()).toBe(
|
|
66
|
+
expect(await Bun.file(join(root, "AGENTS.md")).exists()).toBe(false)
|
|
67
|
+
expect(await Bun.file(join(root, ".agency/AGENTS.md")).exists()).toBe(true)
|
|
67
68
|
expect(
|
|
68
69
|
await Bun.file(join(root, ".opencode/opencode.jsonc")).exists(),
|
|
69
70
|
).toBe(true)
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
rmdir,
|
|
10
10
|
stat,
|
|
11
11
|
symlink,
|
|
12
|
+
unlink,
|
|
12
13
|
} from "node:fs/promises"
|
|
13
14
|
import { spawnProcess } from "../utils/process"
|
|
14
15
|
|
|
@@ -117,6 +118,16 @@ export class FileSystemService extends Effect.Service<FileSystemService>()(
|
|
|
117
118
|
}),
|
|
118
119
|
}),
|
|
119
120
|
|
|
121
|
+
deleteFile: (path: string) =>
|
|
122
|
+
Effect.tryPromise({
|
|
123
|
+
try: () => unlink(path),
|
|
124
|
+
catch: (error) =>
|
|
125
|
+
new FileSystemError({
|
|
126
|
+
message: `Failed to delete file: ${path}`,
|
|
127
|
+
cause: error,
|
|
128
|
+
}),
|
|
129
|
+
}),
|
|
130
|
+
|
|
120
131
|
createDirectory: (path: string) =>
|
|
121
132
|
Effect.tryPromise({
|
|
122
133
|
try: () => mkdir(path, { recursive: true }),
|
|
@@ -47,9 +47,9 @@ describe("IntegrationService", () => {
|
|
|
47
47
|
"missing",
|
|
48
48
|
"missing",
|
|
49
49
|
])
|
|
50
|
-
expect(await Bun.file(join(root, "AGENTS.md")).exists()).toBe(false)
|
|
50
|
+
expect(await Bun.file(join(root, ".agency/AGENTS.md")).exists()).toBe(false)
|
|
51
51
|
|
|
52
|
-
await write(root, "AGENTS.md", managedWorkbaseAgents)
|
|
52
|
+
await write(root, ".agency/AGENTS.md", managedWorkbaseAgents)
|
|
53
53
|
await write(root, ".opencode/opencode.jsonc", managedWorkbaseOpencode)
|
|
54
54
|
expect((await status(root)).files.map(({ state }) => state)).toEqual([
|
|
55
55
|
"managed",
|
|
@@ -58,7 +58,7 @@ describe("IntegrationService", () => {
|
|
|
58
58
|
})
|
|
59
59
|
|
|
60
60
|
test("reports customized and checksum-safe drifted files", async () => {
|
|
61
|
-
await write(root, "AGENTS.md", "# Custom instructions\n")
|
|
61
|
+
await write(root, ".agency/AGENTS.md", "# Custom instructions\n")
|
|
62
62
|
await write(
|
|
63
63
|
root,
|
|
64
64
|
".opencode/opencode.jsonc",
|
|
@@ -97,6 +97,7 @@ describe("IntegrationService", () => {
|
|
|
97
97
|
test("grants OpenCode access to the complete workbase", () => {
|
|
98
98
|
const config = JSON.parse(managedBody(managedWorkbaseOpencode))
|
|
99
99
|
|
|
100
|
+
expect(config.instructions).toEqual([".agency/AGENTS.md"])
|
|
100
101
|
expect(config.references).toEqual({
|
|
101
102
|
workbase: {
|
|
102
103
|
path: "..",
|
|
@@ -151,8 +152,10 @@ describe("IntegrationService", () => {
|
|
|
151
152
|
})
|
|
152
153
|
|
|
153
154
|
test("syncs missing and drifted files while preserving customized files", async () => {
|
|
154
|
-
const
|
|
155
|
-
|
|
155
|
+
const customRootAgents = "# User-owned workbase instructions\n"
|
|
156
|
+
const customManagedAgents = "# Customized Agency instructions\n"
|
|
157
|
+
await write(root, "AGENTS.md", customRootAgents)
|
|
158
|
+
await write(root, ".agency/AGENTS.md", customManagedAgents)
|
|
156
159
|
await write(
|
|
157
160
|
root,
|
|
158
161
|
".opencode/opencode.jsonc",
|
|
@@ -164,14 +167,50 @@ describe("IntegrationService", () => {
|
|
|
164
167
|
{ name: "agents", state: "customized", changed: false },
|
|
165
168
|
{ name: "opencode", state: "managed", changed: true },
|
|
166
169
|
])
|
|
167
|
-
expect(await Bun.file(join(root, "AGENTS.md")).text()).toBe(
|
|
170
|
+
expect(await Bun.file(join(root, "AGENTS.md")).text()).toBe(
|
|
171
|
+
customRootAgents,
|
|
172
|
+
)
|
|
173
|
+
expect(await Bun.file(join(root, ".agency/AGENTS.md")).text()).toBe(
|
|
174
|
+
customManagedAgents,
|
|
175
|
+
)
|
|
168
176
|
expect(await Bun.file(join(root, ".opencode/opencode.jsonc")).text()).toBe(
|
|
169
177
|
managedWorkbaseOpencode,
|
|
170
178
|
)
|
|
171
179
|
|
|
172
|
-
await unlink(join(root, "AGENTS.md"))
|
|
180
|
+
await unlink(join(root, ".agency/AGENTS.md"))
|
|
173
181
|
const second = await sync(root)
|
|
174
182
|
expect(second.files[0]).toMatchObject({ state: "managed", changed: true })
|
|
183
|
+
expect(await Bun.file(join(root, ".agency/AGENTS.md")).text()).toBe(
|
|
184
|
+
managedWorkbaseAgents,
|
|
185
|
+
)
|
|
186
|
+
expect(await Bun.file(join(root, "AGENTS.md")).text()).toBe(
|
|
187
|
+
customRootAgents,
|
|
188
|
+
)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
test("migrates checksum-valid root instructions", async () => {
|
|
192
|
+
await write(root, "AGENTS.md", managedWorkbaseAgents)
|
|
193
|
+
|
|
194
|
+
const result = await sync(root)
|
|
195
|
+
|
|
196
|
+
expect(result.files[0]).toMatchObject({ state: "managed", changed: true })
|
|
197
|
+
expect(await Bun.file(join(root, ".agency/AGENTS.md")).text()).toBe(
|
|
198
|
+
managedWorkbaseAgents,
|
|
199
|
+
)
|
|
200
|
+
expect(await Bun.file(join(root, "AGENTS.md")).exists()).toBe(false)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
test("preserves legacy instructions when the OpenCode config is customized", async () => {
|
|
204
|
+
await write(root, "AGENTS.md", managedWorkbaseAgents)
|
|
205
|
+
await write(root, ".opencode/opencode.json", '{"model":"test/model"}\n')
|
|
206
|
+
|
|
207
|
+
const result = await sync(root)
|
|
208
|
+
|
|
209
|
+
expect(result.files[0]).toMatchObject({ state: "managed", changed: true })
|
|
210
|
+
expect(result.files[1]).toMatchObject({
|
|
211
|
+
state: "customized",
|
|
212
|
+
changed: false,
|
|
213
|
+
})
|
|
175
214
|
expect(await Bun.file(join(root, "AGENTS.md")).text()).toBe(
|
|
176
215
|
managedWorkbaseAgents,
|
|
177
216
|
)
|
|
@@ -198,14 +237,16 @@ describe("IntegrationService", () => {
|
|
|
198
237
|
"# Previous Agency instructions\n",
|
|
199
238
|
" -->",
|
|
200
239
|
)}User edit\n`
|
|
201
|
-
await write(root, "AGENTS.md", tampered)
|
|
240
|
+
await write(root, ".agency/AGENTS.md", tampered)
|
|
202
241
|
|
|
203
242
|
const result = await sync(root)
|
|
204
243
|
expect(result.files[0]).toMatchObject({
|
|
205
244
|
state: "customized",
|
|
206
245
|
changed: false,
|
|
207
246
|
})
|
|
208
|
-
expect(await Bun.file(join(root, "AGENTS.md")).text()).toBe(
|
|
247
|
+
expect(await Bun.file(join(root, ".agency/AGENTS.md")).text()).toBe(
|
|
248
|
+
tampered,
|
|
249
|
+
)
|
|
209
250
|
})
|
|
210
251
|
|
|
211
252
|
test("does not overwrite a user-modified OpenCode configuration", async () => {
|
|
@@ -228,7 +269,8 @@ describe("IntegrationService", () => {
|
|
|
228
269
|
test("does not follow symlinked integration files", async () => {
|
|
229
270
|
const target = join(root, "custom-agents.md")
|
|
230
271
|
await Bun.write(target, "# External instructions\n")
|
|
231
|
-
await
|
|
272
|
+
await mkdir(join(root, ".agency"), { recursive: true })
|
|
273
|
+
await symlink(target, join(root, ".agency/AGENTS.md"))
|
|
232
274
|
|
|
233
275
|
const result = await sync(root)
|
|
234
276
|
expect(result.files[0]).toMatchObject({
|
|
@@ -33,23 +33,23 @@ const describe = (
|
|
|
33
33
|
if (state === "managed") {
|
|
34
34
|
return {
|
|
35
35
|
diagnostic:
|
|
36
|
-
"Agency's managed OpenCode launch config is ready to provide whole-workbase read access.",
|
|
36
|
+
"Agency's managed OpenCode launch config is ready to load Agency instructions and provide whole-workbase read access.",
|
|
37
37
|
remediation: null,
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
if (state === "customized") {
|
|
41
41
|
return {
|
|
42
42
|
diagnostic:
|
|
43
|
-
"Agency cannot guarantee whole-workbase read access from this customized OpenCode config.",
|
|
43
|
+
"Agency cannot guarantee its instructions or whole-workbase read access from this customized OpenCode config.",
|
|
44
44
|
remediation:
|
|
45
45
|
"Back up and remove the customized file, run 'agency integration sync', then move any retained custom settings to OpenCode's global config.",
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
return {
|
|
49
49
|
diagnostic:
|
|
50
|
-
"Agency OpenCode launches cannot load current whole-workbase access.",
|
|
50
|
+
"Agency OpenCode launches cannot load current Agency instructions or whole-workbase access.",
|
|
51
51
|
remediation:
|
|
52
|
-
"Run 'agency integration sync' to install whole-workbase OpenCode access.",
|
|
52
|
+
"Run 'agency integration sync' to install Agency instructions and whole-workbase OpenCode access.",
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -94,7 +94,7 @@ const classify = (
|
|
|
94
94
|
const inspect = (root: string) =>
|
|
95
95
|
Effect.gen(function* () {
|
|
96
96
|
const fs = yield* FileSystemService
|
|
97
|
-
const agentsPath = join(root, "AGENTS.md")
|
|
97
|
+
const agentsPath = join(root, ".agency", "AGENTS.md")
|
|
98
98
|
const opencodeDirectory = join(root, ".opencode")
|
|
99
99
|
const opencodePath = join(opencodeDirectory, "opencode.jsonc")
|
|
100
100
|
const opencodeJsonPath = join(opencodeDirectory, "opencode.json")
|
|
@@ -138,6 +138,18 @@ const inspect = (root: string) =>
|
|
|
138
138
|
return files
|
|
139
139
|
})
|
|
140
140
|
|
|
141
|
+
const canRemoveLegacyAgents = (root: string) =>
|
|
142
|
+
Effect.gen(function* () {
|
|
143
|
+
const fs = yield* FileSystemService
|
|
144
|
+
const path = join(root, "AGENTS.md")
|
|
145
|
+
if (
|
|
146
|
+
(yield* fs.readSymlinkTarget(path)) !== null ||
|
|
147
|
+
!(yield* fs.exists(path))
|
|
148
|
+
)
|
|
149
|
+
return false
|
|
150
|
+
return canUpdateManagedWorkbaseAgents(yield* fs.readFile(path))
|
|
151
|
+
})
|
|
152
|
+
|
|
141
153
|
export class IntegrationService extends Effect.Service<IntegrationService>()(
|
|
142
154
|
"IntegrationService",
|
|
143
155
|
{
|
|
@@ -155,13 +167,19 @@ export class IntegrationService extends Effect.Service<IntegrationService>()(
|
|
|
155
167
|
const workbase = yield* WorkbaseService
|
|
156
168
|
const root = yield* workbase.discover(startPath)
|
|
157
169
|
const statuses = yield* inspect(root)
|
|
170
|
+
const removeLegacyAgents =
|
|
171
|
+
statuses.some(
|
|
172
|
+
(status) =>
|
|
173
|
+
status.name === "opencode" && status.state !== "customized",
|
|
174
|
+
) && (yield* canRemoveLegacyAgents(root))
|
|
158
175
|
const files: IntegrationSyncFile[] = []
|
|
159
176
|
|
|
160
177
|
for (const status of statuses) {
|
|
161
|
-
const
|
|
178
|
+
const needsWrite =
|
|
162
179
|
status.state === "missing" || status.state === "drifted"
|
|
163
|
-
if (
|
|
180
|
+
if (needsWrite) {
|
|
164
181
|
if (status.name === "agents") {
|
|
182
|
+
yield* fs.createDirectory(join(root, ".agency"))
|
|
165
183
|
yield* fs.writeFile(status.path, managedWorkbaseAgents)
|
|
166
184
|
} else {
|
|
167
185
|
yield* fs.createDirectory(join(root, ".opencode"))
|
|
@@ -172,12 +190,15 @@ export class IntegrationService extends Effect.Service<IntegrationService>()(
|
|
|
172
190
|
...fileStatus(
|
|
173
191
|
status.name,
|
|
174
192
|
status.path,
|
|
175
|
-
|
|
193
|
+
needsWrite ? "managed" : status.state,
|
|
176
194
|
),
|
|
177
|
-
changed
|
|
195
|
+
changed:
|
|
196
|
+
needsWrite || (status.name === "agents" && removeLegacyAgents),
|
|
178
197
|
})
|
|
179
198
|
}
|
|
180
199
|
|
|
200
|
+
if (removeLegacyAgents) yield* fs.deleteFile(join(root, "AGENTS.md"))
|
|
201
|
+
|
|
181
202
|
return { root, files }
|
|
182
203
|
}),
|
|
183
204
|
}),
|
package/src/workbase/AGENTS.md
CHANGED
|
@@ -67,9 +67,13 @@ a refinement loop, or pausing or handing off completed implementation work):
|
|
|
67
67
|
## Managed Integration
|
|
68
68
|
|
|
69
69
|
`agency integration status` reports `managed`, `drifted`, `customized`, or
|
|
70
|
-
`missing` generated files.
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
`missing` generated files. Agency keeps these instructions in
|
|
71
|
+
`.agency/AGENTS.md`, and its managed OpenCode config loads them automatically.
|
|
72
|
+
The workbase-root `AGENTS.md`, when present, belongs entirely to the workbase
|
|
73
|
+
owner and composes with these instructions through OpenCode's normal discovery.
|
|
74
|
+
`agency integration sync` updates only missing or checksum-safe drifted managed
|
|
75
|
+
files and preserves user-customized files. `agency init` creates the managed
|
|
76
|
+
files, and `agency work` reconciles them before launching an agent.
|
|
73
77
|
|
|
74
78
|
OpenCode can access the complete workbase tree, but this filesystem permission
|
|
75
79
|
does not expand Agency write authority beyond the checkout reported by
|