@markjaquith/agency 2.28.0 → 2.29.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 +45 -26
- package/cli.ts +12 -0
- package/fixtures/protocol/orchestration-recipes.json +59 -0
- package/package.json +1 -1
- package/skills/agency/SKILL.md +112 -331
- package/skills/agency/references/commands.md +162 -0
- package/skills/agency/references/contracts.md +288 -0
- package/skills/agency/references/recipes.md +219 -0
- package/src/cli-parser.test.ts +9 -0
- package/src/cli-parser.ts +7 -1
- package/src/cli.test.ts +28 -4
- package/src/commands/init.test.ts +8 -4
- package/src/commands/init.ts +3 -0
- package/src/commands/work.test.ts +20 -0
- package/src/commands/work.ts +3 -0
- package/src/commands/workbase.ts +2 -1
- package/src/services/IntegrationService.test.ts +80 -3
- package/src/services/IntegrationService.ts +2 -2
- package/src/workbase/AGENTS.md +53 -19
- package/src/workbase/opencode-file.ts +7 -8
package/src/cli-parser.test.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import orchestrationRecipes from "../fixtures/protocol/orchestration-recipes.json"
|
|
2
3
|
import { parseCli } from "./cli-parser"
|
|
3
4
|
|
|
4
5
|
const expectUsageError = (args: string[], usage: string) => {
|
|
@@ -6,6 +7,12 @@ const expectUsageError = (args: string[], usage: string) => {
|
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
describe("strict CLI parsing", () => {
|
|
10
|
+
test("accepts every documented orchestration recipe command", () => {
|
|
11
|
+
for (const args of orchestrationRecipes) {
|
|
12
|
+
expect(() => parseCli(args)).not.toThrow()
|
|
13
|
+
}
|
|
14
|
+
})
|
|
15
|
+
|
|
9
16
|
test("rejects misspelled and command-inapplicable options", () => {
|
|
10
17
|
expectUsageError(["task", "list", "--josn"], "agency task")
|
|
11
18
|
expectUsageError(
|
|
@@ -132,6 +139,7 @@ describe("strict CLI parsing", () => {
|
|
|
132
139
|
["repo", "remote", "agency", "https://example.com/repo.git"],
|
|
133
140
|
["repo", "verify", "agency"],
|
|
134
141
|
["workbase", "show", "primary", "--json"],
|
|
142
|
+
["workbase", "init", "new-workbase", "--json"],
|
|
135
143
|
["workbase", "name", "primary", "renamed"],
|
|
136
144
|
["workbase", "name", "primary", "--clear"],
|
|
137
145
|
]) {
|
|
@@ -157,6 +165,7 @@ describe("strict CLI parsing", () => {
|
|
|
157
165
|
test("enforces exact maximum positional arity for every leaf command", () => {
|
|
158
166
|
for (const [args, usage] of [
|
|
159
167
|
[["init", "one", "two"], "agency init"],
|
|
168
|
+
[["workbase", "init", "one", "two"], "agency workbase init"],
|
|
160
169
|
[["workbase", "add", "one", "two"], "agency workbase add"],
|
|
161
170
|
[["workbase", "list", "extra"], "agency workbase list"],
|
|
162
171
|
[["integration", "status", "extra"], "agency integration status"],
|
package/src/cli-parser.ts
CHANGED
|
@@ -127,13 +127,19 @@ const commands = {
|
|
|
127
127
|
},
|
|
128
128
|
},
|
|
129
129
|
workbase: {
|
|
130
|
-
usage: "agency workbase <add|list|show|name|remove|prune|default>",
|
|
130
|
+
usage: "agency workbase <init|add|list|show|name|remove|prune|default>",
|
|
131
131
|
options: {
|
|
132
132
|
...outputOptions,
|
|
133
133
|
name: { type: "string" },
|
|
134
134
|
clear: { type: "boolean" },
|
|
135
135
|
},
|
|
136
136
|
subcommands: {
|
|
137
|
+
init: {
|
|
138
|
+
usage: "agency workbase init [path] [--json]",
|
|
139
|
+
minArgs: 0,
|
|
140
|
+
maxArgs: 1,
|
|
141
|
+
options: ["json"],
|
|
142
|
+
},
|
|
137
143
|
add: {
|
|
138
144
|
usage: "agency workbase add <path> [--json]",
|
|
139
145
|
minArgs: 1,
|
package/src/cli.test.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { 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
|
+
import errorFixture from "../fixtures/protocol/error.json"
|
|
5
|
+
import successFixture from "../fixtures/protocol/success.json"
|
|
4
6
|
import { cleanupTempDir, createTempDir } from "./test-utils"
|
|
5
7
|
|
|
6
8
|
const projectRoot = join(import.meta.dir, "..")
|
|
@@ -65,6 +67,28 @@ describe("CLI", () => {
|
|
|
65
67
|
expect(help.stderr).toBe("")
|
|
66
68
|
})
|
|
67
69
|
|
|
70
|
+
test("keeps the published protocol fixtures synchronized with CLI output", async () => {
|
|
71
|
+
const parent = await createTempDir()
|
|
72
|
+
tempDirs.push(parent)
|
|
73
|
+
const root = join(parent, "workbase")
|
|
74
|
+
const success = await runCli(["init", root, "--json"], parent)
|
|
75
|
+
expect(success.exitCode).toBe(0)
|
|
76
|
+
expect(success.stderr).toBe("")
|
|
77
|
+
expect(success.stdout.endsWith("\n")).toBe(true)
|
|
78
|
+
const successEnvelope = JSON.parse(success.stdout)
|
|
79
|
+
expect(successEnvelope.result.root).toBe(root)
|
|
80
|
+
expect({
|
|
81
|
+
...successEnvelope,
|
|
82
|
+
result: { ...successEnvelope.result, root: "/work/agency" },
|
|
83
|
+
}).toEqual(successFixture)
|
|
84
|
+
|
|
85
|
+
const failure = await runCli(["unknown", "--json"])
|
|
86
|
+
expect(failure.exitCode).toBe(1)
|
|
87
|
+
expect(failure.stderr).toBe("")
|
|
88
|
+
expect(failure.stdout.endsWith("\n")).toBe(true)
|
|
89
|
+
expect(JSON.parse(failure.stdout)).toEqual(errorFixture)
|
|
90
|
+
})
|
|
91
|
+
|
|
68
92
|
test("reports unknown commands and preserves tagged error messages", async () => {
|
|
69
93
|
const unknown = await runCli(["unknown"])
|
|
70
94
|
expect(unknown.exitCode).toBe(1)
|
|
@@ -432,16 +456,16 @@ describe("CLI", () => {
|
|
|
432
456
|
await runCli(["integration", "status", "--json"], root),
|
|
433
457
|
)
|
|
434
458
|
expect(before.files).toMatchObject([
|
|
435
|
-
{ name: "agents", state: "
|
|
436
|
-
{ name: "opencode", state: "
|
|
459
|
+
{ name: "agents", state: "managed" },
|
|
460
|
+
{ name: "opencode", state: "managed" },
|
|
437
461
|
])
|
|
438
462
|
|
|
439
463
|
const synced = parseJson(
|
|
440
464
|
await runCli(["integration", "sync", "--json"], root),
|
|
441
465
|
)
|
|
442
466
|
expect(synced.files).toMatchObject([
|
|
443
|
-
{ name: "agents", state: "managed", changed:
|
|
444
|
-
{ name: "opencode", state: "managed", changed:
|
|
467
|
+
{ name: "agents", state: "managed", changed: false },
|
|
468
|
+
{ name: "opencode", state: "managed", changed: false },
|
|
445
469
|
])
|
|
446
470
|
})
|
|
447
471
|
|
|
@@ -33,10 +33,14 @@ 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(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
).
|
|
36
|
+
expect(await Bun.file(join(root, "AGENTS.md")).exists()).toBe(true)
|
|
37
|
+
const opencode = await Bun.file(
|
|
38
|
+
join(root, ".opencode/opencode.jsonc"),
|
|
39
|
+
).text()
|
|
40
|
+
const config = JSON.parse(opencode.slice(opencode.indexOf("\n\n") + 2))
|
|
41
|
+
expect(config.permission.external_directory).toEqual({
|
|
42
|
+
"../**": "allow",
|
|
43
|
+
})
|
|
40
44
|
})
|
|
41
45
|
|
|
42
46
|
test("preserves existing gitignore entries", async () => {
|
package/src/commands/init.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Effect } from "effect"
|
|
2
2
|
import { resolve } from "node:path"
|
|
3
3
|
import type { BaseCommandOptions } from "../utils/command"
|
|
4
|
+
import { IntegrationService } from "../services/IntegrationService"
|
|
4
5
|
import { WorkbaseService } from "../services/WorkbaseService"
|
|
5
6
|
import { createLoggers } from "../utils/effect"
|
|
6
7
|
|
|
@@ -10,12 +11,14 @@ interface InitOptions extends BaseCommandOptions {
|
|
|
10
11
|
|
|
11
12
|
export const init = (options: InitOptions = {}) =>
|
|
12
13
|
Effect.gen(function* () {
|
|
14
|
+
const integrations = yield* IntegrationService
|
|
13
15
|
const workbase = yield* WorkbaseService
|
|
14
16
|
const { log } = createLoggers(options)
|
|
15
17
|
const cwd = options.cwd ?? process.cwd()
|
|
16
18
|
const root = yield* workbase.initialize(
|
|
17
19
|
options.path ? resolve(cwd, options.path) : cwd,
|
|
18
20
|
)
|
|
21
|
+
yield* integrations.sync(root)
|
|
19
22
|
log(
|
|
20
23
|
options.json
|
|
21
24
|
? JSON.stringify({ root }, null, 2)
|
|
@@ -8,6 +8,7 @@ import { PhaseService } from "../services/PhaseService"
|
|
|
8
8
|
import { WorktreeService } from "../services/WorktreeService"
|
|
9
9
|
import { ClaimService } from "../services/ClaimService"
|
|
10
10
|
import { ReadinessService } from "../services/ReadinessService"
|
|
11
|
+
import { IntegrationService } from "../services/IntegrationService"
|
|
11
12
|
import { captureErrors, captureLogs } from "../test-utils"
|
|
12
13
|
import { work, workPrepare } from "./work"
|
|
13
14
|
import type { PickWorkTarget } from "../workbase/work-target"
|
|
@@ -74,6 +75,7 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
74
75
|
const statusUpdates: string[] = []
|
|
75
76
|
const shownTasks: string[] = []
|
|
76
77
|
const progressUpdates: string[] = []
|
|
78
|
+
let integrationSyncs = 0
|
|
77
79
|
const guards: Array<{ target: string; override?: boolean }> = []
|
|
78
80
|
const launches: Array<{
|
|
79
81
|
cli: string
|
|
@@ -210,6 +212,12 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
210
212
|
: Effect.void
|
|
211
213
|
},
|
|
212
214
|
}
|
|
215
|
+
const integrations = {
|
|
216
|
+
sync: () => {
|
|
217
|
+
integrationSyncs += 1
|
|
218
|
+
return Effect.succeed({ root: "/workbase", files: [] })
|
|
219
|
+
},
|
|
220
|
+
}
|
|
213
221
|
const fs = {
|
|
214
222
|
isDirectory: (path: string) =>
|
|
215
223
|
Effect.succeed(options.existingDirectories?.includes(path) ?? true),
|
|
@@ -256,6 +264,7 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
256
264
|
Effect.provideService(PhaseService, phases as never),
|
|
257
265
|
Effect.provideService(ClaimService, claims as never),
|
|
258
266
|
Effect.provideService(ReadinessService, readiness as never),
|
|
267
|
+
Effect.provideService(IntegrationService, integrations as never),
|
|
259
268
|
) as Effect.Effect<void, unknown, never>,
|
|
260
269
|
)
|
|
261
270
|
const runPrepare = (commandOptions: Parameters<typeof workPrepare>[0]) =>
|
|
@@ -279,12 +288,23 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
279
288
|
shownTasks,
|
|
280
289
|
progressUpdates,
|
|
281
290
|
guards,
|
|
291
|
+
get integrationSyncs() {
|
|
292
|
+
return integrationSyncs
|
|
293
|
+
},
|
|
282
294
|
run,
|
|
283
295
|
runPrepare,
|
|
284
296
|
}
|
|
285
297
|
}
|
|
286
298
|
|
|
287
299
|
describe("work command", () => {
|
|
300
|
+
test("reconciles managed integration files before preparing work", async () => {
|
|
301
|
+
const harness = createHarness()
|
|
302
|
+
|
|
303
|
+
await harness.run({ taskId: "example", opencode: true })
|
|
304
|
+
|
|
305
|
+
expect(harness.integrationSyncs).toBe(1)
|
|
306
|
+
})
|
|
307
|
+
|
|
288
308
|
test("guards execution targets before materialization and honors --force", async () => {
|
|
289
309
|
const blocked = createHarness({ guardError: new Error("blocked") })
|
|
290
310
|
await expect(
|
package/src/commands/work.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { TaskService } from "../services/TaskService"
|
|
|
9
9
|
import { PhaseService } from "../services/PhaseService"
|
|
10
10
|
import { ClaimService } from "../services/ClaimService"
|
|
11
11
|
import { ReadinessService } from "../services/ReadinessService"
|
|
12
|
+
import { IntegrationService } from "../services/IntegrationService"
|
|
12
13
|
import { createLoggers } from "../utils/effect"
|
|
13
14
|
import { execvp } from "../utils/exec"
|
|
14
15
|
import { createProgress, type Progress } from "../utils/progress"
|
|
@@ -106,6 +107,7 @@ export const work = (
|
|
|
106
107
|
const phases = yield* PhaseService
|
|
107
108
|
const claims = yield* ClaimService
|
|
108
109
|
const readiness = yield* ReadinessService
|
|
110
|
+
const integrations = yield* IntegrationService
|
|
109
111
|
const { log, verboseLog } = createLoggers(options)
|
|
110
112
|
const cwd = options.cwd ?? process.cwd()
|
|
111
113
|
const directoryPath = options.directory
|
|
@@ -118,6 +120,7 @@ export const work = (
|
|
|
118
120
|
const inputAllowed = options.inputAllowed ?? true
|
|
119
121
|
const root = yield* resolveWorkbase(startPath, pickBase, inputAllowed)
|
|
120
122
|
if (!root) return
|
|
123
|
+
yield* integrations.sync(root)
|
|
121
124
|
const { config } = yield* workbase.loadConfig(root)
|
|
122
125
|
|
|
123
126
|
let target: WorkTarget | null = null
|
package/src/commands/workbase.ts
CHANGED
|
@@ -151,7 +151,7 @@ export const workbase = (options: WorkbaseOptions) =>
|
|
|
151
151
|
default:
|
|
152
152
|
return yield* Effect.fail(
|
|
153
153
|
new Error(
|
|
154
|
-
"Subcommand is required. Available: add, list, show, name, remove, prune, default",
|
|
154
|
+
"Subcommand is required. Available: init, add, list, show, name, remove, prune, default",
|
|
155
155
|
),
|
|
156
156
|
)
|
|
157
157
|
}
|
|
@@ -161,6 +161,7 @@ export const help = `
|
|
|
161
161
|
Usage: agency workbase <subcommand>
|
|
162
162
|
|
|
163
163
|
Subcommands:
|
|
164
|
+
init [path] Initialize an Agency workbase
|
|
164
165
|
add <path> Register an Agency workbase
|
|
165
166
|
list List registered workbases
|
|
166
167
|
show <selector> Show a registered workbase
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
2
|
import { Effect } from "effect"
|
|
3
3
|
import { createHash } from "node:crypto"
|
|
4
|
-
import { mkdir, symlink, unlink } from "node:fs/promises"
|
|
4
|
+
import { mkdir, stat, symlink, unlink, utimes } from "node:fs/promises"
|
|
5
5
|
import { dirname, join } from "node:path"
|
|
6
6
|
import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
|
|
7
7
|
import { managedWorkbaseAgents } from "../workbase/agents-file"
|
|
@@ -19,6 +19,9 @@ const managed = (prefix: string, body: string, suffix = "") => {
|
|
|
19
19
|
return `${prefix}${checksum}${suffix}\n\n${body}`
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
const managedBody = (content: string) =>
|
|
23
|
+
content.slice(content.indexOf("\n\n") + 2)
|
|
24
|
+
|
|
22
25
|
const status = (root: string) =>
|
|
23
26
|
runTestEffect(
|
|
24
27
|
IntegrationService.pipe(Effect.flatMap((service) => service.status(root))),
|
|
@@ -47,7 +50,7 @@ describe("IntegrationService", () => {
|
|
|
47
50
|
expect(await Bun.file(join(root, "AGENTS.md")).exists()).toBe(false)
|
|
48
51
|
|
|
49
52
|
await write(root, "AGENTS.md", managedWorkbaseAgents)
|
|
50
|
-
await write(root, ".opencode/opencode.jsonc", managedWorkbaseOpencode
|
|
53
|
+
await write(root, ".opencode/opencode.jsonc", managedWorkbaseOpencode)
|
|
51
54
|
expect((await status(root)).files.map(({ state }) => state)).toEqual([
|
|
52
55
|
"managed",
|
|
53
56
|
"managed",
|
|
@@ -68,6 +71,48 @@ describe("IntegrationService", () => {
|
|
|
68
71
|
])
|
|
69
72
|
})
|
|
70
73
|
|
|
74
|
+
test("generates context-first safety and execution closeout guidance", () => {
|
|
75
|
+
const body = managedBody(managedWorkbaseAgents)
|
|
76
|
+
|
|
77
|
+
expect(body).toContain("agency context . --json")
|
|
78
|
+
expect(body).toContain("authority.writable.checkoutPath")
|
|
79
|
+
expect(body).toContain("Do not begin execution without a claim")
|
|
80
|
+
expect(body).toContain("Run `agency validate`")
|
|
81
|
+
expect(body).toContain("only with explicit user intent")
|
|
82
|
+
expect(body).toContain("An execution unit is `working`")
|
|
83
|
+
expect(body).toContain("It becomes `done`")
|
|
84
|
+
expect(body).toContain("solely because its PR")
|
|
85
|
+
expect(body).toContain("creating or updating a PR")
|
|
86
|
+
expect(body).toContain("marking it ready")
|
|
87
|
+
expect(body).toMatch(/completing\s+a refinement loop/)
|
|
88
|
+
expect(body).toContain("pausing or handing off")
|
|
89
|
+
expect(body).toContain("`agency task status` or `agency phase status`")
|
|
90
|
+
expect(body).toContain("`TASK.md` or `PHASE.md`")
|
|
91
|
+
expect(body).toContain("PR state, current head, diff summary")
|
|
92
|
+
expect(body).toContain("Run `agency validate` before reporting completion")
|
|
93
|
+
expect(body).toContain("agency integration status")
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test("grants OpenCode access to the complete workbase", () => {
|
|
97
|
+
const config = JSON.parse(managedBody(managedWorkbaseOpencode))
|
|
98
|
+
|
|
99
|
+
expect(config.references).toEqual({
|
|
100
|
+
tasks: {
|
|
101
|
+
path: "../tasks",
|
|
102
|
+
description:
|
|
103
|
+
"Agency task definitions and execution context; authority still comes from agency context",
|
|
104
|
+
},
|
|
105
|
+
epics: {
|
|
106
|
+
path: "../epics",
|
|
107
|
+
description:
|
|
108
|
+
"Agency epic definitions and orchestration context; no implementation write authority",
|
|
109
|
+
},
|
|
110
|
+
})
|
|
111
|
+
expect(config.permission).toEqual({
|
|
112
|
+
external_directory: { "../**": "allow" },
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
71
116
|
test("treats an existing JSON OpenCode config as customized", async () => {
|
|
72
117
|
await write(root, ".opencode/opencode.json", '{"model":"test/model"}\n')
|
|
73
118
|
|
|
@@ -95,7 +140,7 @@ describe("IntegrationService", () => {
|
|
|
95
140
|
])
|
|
96
141
|
expect(await Bun.file(join(root, "AGENTS.md")).text()).toBe(customAgents)
|
|
97
142
|
expect(await Bun.file(join(root, ".opencode/opencode.jsonc")).text()).toBe(
|
|
98
|
-
managedWorkbaseOpencode
|
|
143
|
+
managedWorkbaseOpencode,
|
|
99
144
|
)
|
|
100
145
|
|
|
101
146
|
await unlink(join(root, "AGENTS.md"))
|
|
@@ -106,6 +151,21 @@ describe("IntegrationService", () => {
|
|
|
106
151
|
)
|
|
107
152
|
})
|
|
108
153
|
|
|
154
|
+
test("does not rewrite an already-current OpenCode configuration", async () => {
|
|
155
|
+
const path = join(root, ".opencode/opencode.jsonc")
|
|
156
|
+
await write(root, ".opencode/opencode.jsonc", managedWorkbaseOpencode)
|
|
157
|
+
const timestamp = new Date("2000-01-01T00:00:00.000Z")
|
|
158
|
+
await utimes(path, timestamp, timestamp)
|
|
159
|
+
|
|
160
|
+
const result = await sync(root)
|
|
161
|
+
|
|
162
|
+
expect(result.files[1]).toMatchObject({
|
|
163
|
+
state: "managed",
|
|
164
|
+
changed: false,
|
|
165
|
+
})
|
|
166
|
+
expect((await stat(path)).mtimeMs).toBe(timestamp.getTime())
|
|
167
|
+
})
|
|
168
|
+
|
|
109
169
|
test("does not overwrite managed files whose checksums no longer match", async () => {
|
|
110
170
|
const tampered = `${managed(
|
|
111
171
|
"<!-- agency-managed: sha256=",
|
|
@@ -122,6 +182,23 @@ describe("IntegrationService", () => {
|
|
|
122
182
|
expect(await Bun.file(join(root, "AGENTS.md")).text()).toBe(tampered)
|
|
123
183
|
})
|
|
124
184
|
|
|
185
|
+
test("does not overwrite a user-modified OpenCode configuration", async () => {
|
|
186
|
+
const tampered = `${managed(
|
|
187
|
+
"// agency-managed: sha256=",
|
|
188
|
+
'{"references":{}}\n',
|
|
189
|
+
)}// User edit\n`
|
|
190
|
+
await write(root, ".opencode/opencode.jsonc", tampered)
|
|
191
|
+
|
|
192
|
+
const result = await sync(root)
|
|
193
|
+
expect(result.files[1]).toMatchObject({
|
|
194
|
+
state: "customized",
|
|
195
|
+
changed: false,
|
|
196
|
+
})
|
|
197
|
+
expect(await Bun.file(join(root, ".opencode/opencode.jsonc")).text()).toBe(
|
|
198
|
+
tampered,
|
|
199
|
+
)
|
|
200
|
+
})
|
|
201
|
+
|
|
125
202
|
test("does not follow symlinked integration files", async () => {
|
|
126
203
|
const target = join(root, "custom-agents.md")
|
|
127
204
|
await Bun.write(target, "# External instructions\n")
|
|
@@ -75,7 +75,7 @@ const inspect = (root: string) =>
|
|
|
75
75
|
"opencode",
|
|
76
76
|
opencodePath,
|
|
77
77
|
yield* fs.readFile(opencodePath),
|
|
78
|
-
managedWorkbaseOpencode
|
|
78
|
+
managedWorkbaseOpencode,
|
|
79
79
|
canUpdateManagedWorkbaseOpencode,
|
|
80
80
|
),
|
|
81
81
|
)
|
|
@@ -119,7 +119,7 @@ export class IntegrationService extends Effect.Service<IntegrationService>()(
|
|
|
119
119
|
yield* fs.writeFile(status.path, managedWorkbaseAgents)
|
|
120
120
|
} else {
|
|
121
121
|
yield* fs.createDirectory(join(root, ".opencode"))
|
|
122
|
-
yield* fs.writeFile(status.path, managedWorkbaseOpencode
|
|
122
|
+
yield* fs.writeFile(status.path, managedWorkbaseOpencode)
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
files.push({
|
package/src/workbase/AGENTS.md
CHANGED
|
@@ -4,34 +4,68 @@ This directory is an Agency workbase. Epics, tasks, and phases are durable
|
|
|
4
4
|
Markdown documents; repository aliases and generated Git worktrees provide code
|
|
5
5
|
access.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Bootstrap
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
read its context:
|
|
9
|
+
Start every session with one read-only command:
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
session; a task without `phases` is a single execution unit.
|
|
16
|
-
- In `tasks/<task>/phases/<phase>/`, read both `../../TASK.md` and `PHASE.md`.
|
|
17
|
-
The phase is the execution unit.
|
|
11
|
+
```bash
|
|
12
|
+
agency context . --json
|
|
13
|
+
```
|
|
18
14
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
Use the returned target, document paths and revisions, dependency readiness,
|
|
16
|
+
authority, checkout state, PR state, and validation result. Do not infer these
|
|
17
|
+
from directory names or stale prose.
|
|
18
|
+
|
|
19
|
+
## Authority
|
|
20
|
+
|
|
21
|
+
- An epic or multi-phase task is orchestration context and has no implementation
|
|
22
|
+
write authority.
|
|
23
|
+
- For an execution unit, write code only at
|
|
24
|
+
`authority.writable.checkoutPath`. Every `authority.references` checkout is
|
|
25
|
+
read-only, even if filesystem permissions allow writes.
|
|
26
|
+
- Keep task-wide decisions in `TASK.md` and phase-specific delivery context in
|
|
27
|
+
`PHASE.md`. Use Agency commands for structural frontmatter mutations.
|
|
22
28
|
|
|
23
29
|
## Safety
|
|
24
30
|
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
-
|
|
28
|
-
`
|
|
29
|
-
- Coordinate execution ownership with `agency claim`, `agency release`, and
|
|
30
|
-
`agency finish`; `agency work` claims execution units before launch.
|
|
31
|
+
- Stop on validation errors, dependency blockers, an unexpected writable
|
|
32
|
+
repository, or a conflicting active claim.
|
|
33
|
+
- Do not begin execution without a claim. `agency work` claims before launch;
|
|
34
|
+
external orchestrators use `agency claim` with the revision from context.
|
|
31
35
|
- Do not manually create, move, or remove worktrees under `code/`.
|
|
32
36
|
- Use `agency archive`, rather than moving work item folders manually.
|
|
33
37
|
- Do not edit bare repositories or repository symlinks under `repos/`.
|
|
34
38
|
- Do not run `agency work` from an active agent session unless the user
|
|
35
39
|
explicitly asks to launch another agent.
|
|
36
40
|
- Run `agency validate` before worktree or pull-request operations.
|
|
37
|
-
- Create a pull request only
|
|
41
|
+
- Create a pull request only with explicit user intent, using
|
|
42
|
+
`agency pr create <task> [phase]` so the URL is recorded durably.
|
|
43
|
+
|
|
44
|
+
## Closeout
|
|
45
|
+
|
|
46
|
+
An execution unit is `working` while implementation or requested delivery work
|
|
47
|
+
remains. It becomes `done` when both are complete, even if its PR remains open
|
|
48
|
+
for review or merge. Do not leave a task or phase `working` solely because its PR
|
|
49
|
+
is open; if merge was requested, merge remains delivery work.
|
|
50
|
+
|
|
51
|
+
At each closeout trigger (creating or updating a PR, marking it ready, completing
|
|
52
|
+
a refinement loop, or pausing or handing off completed implementation work):
|
|
53
|
+
|
|
54
|
+
- Use `agency task status` or `agency phase status` to set the execution unit's
|
|
55
|
+
current status. Finish an active claim with the current revision via
|
|
56
|
+
`agency finish`.
|
|
57
|
+
- Refresh durable delivery context in `TASK.md` or `PHASE.md`, including recorded
|
|
58
|
+
PR state, current head, diff summary, and verification results after later
|
|
59
|
+
pushes when those details are maintained there.
|
|
60
|
+
- Run `agency validate` before reporting completion.
|
|
61
|
+
|
|
62
|
+
## Managed Integration
|
|
63
|
+
|
|
64
|
+
`agency integration status` reports `managed`, `drifted`, `customized`, or
|
|
65
|
+
`missing` generated files. `agency integration sync` updates only missing or
|
|
66
|
+
checksum-safe drifted files and preserves user-customized files. `agency init`
|
|
67
|
+
creates these files, and `agency work` reconciles them before launching an agent.
|
|
68
|
+
|
|
69
|
+
OpenCode can access the complete workbase tree, but this filesystem permission
|
|
70
|
+
does not expand Agency write authority beyond the checkout reported by
|
|
71
|
+
`agency context`.
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { createHash } from "node:crypto"
|
|
2
|
-
import { join } from "node:path"
|
|
3
2
|
|
|
4
3
|
const managedHeaderPattern =
|
|
5
4
|
/^\/\/ agency-managed: sha256=([a-f0-9]{64})\r?\n\r?\n/
|
|
@@ -7,24 +6,25 @@ const managedHeaderPattern =
|
|
|
7
6
|
const checksum = (content: string) =>
|
|
8
7
|
createHash("sha256").update(content).digest("hex")
|
|
9
8
|
|
|
10
|
-
const body = (
|
|
9
|
+
const body = () =>
|
|
11
10
|
`${JSON.stringify(
|
|
12
11
|
{
|
|
13
12
|
$schema: "https://opencode.ai/config.json",
|
|
14
13
|
references: {
|
|
15
14
|
tasks: {
|
|
16
15
|
path: "../tasks",
|
|
17
|
-
description:
|
|
16
|
+
description:
|
|
17
|
+
"Agency task definitions and execution context; authority still comes from agency context",
|
|
18
18
|
},
|
|
19
19
|
epics: {
|
|
20
20
|
path: "../epics",
|
|
21
|
-
description:
|
|
21
|
+
description:
|
|
22
|
+
"Agency epic definitions and orchestration context; no implementation write authority",
|
|
22
23
|
},
|
|
23
24
|
},
|
|
24
25
|
permission: {
|
|
25
26
|
external_directory: {
|
|
26
|
-
|
|
27
|
-
[`${join(root, "epics")}/*`]: "allow",
|
|
27
|
+
"../**": "allow",
|
|
28
28
|
},
|
|
29
29
|
},
|
|
30
30
|
},
|
|
@@ -35,8 +35,7 @@ const body = (root: string) =>
|
|
|
35
35
|
const renderManagedWorkbaseOpencode = (content: string) =>
|
|
36
36
|
`// agency-managed: sha256=${checksum(content)}\n\n${content}`
|
|
37
37
|
|
|
38
|
-
export const managedWorkbaseOpencode = (
|
|
39
|
-
renderManagedWorkbaseOpencode(body(root))
|
|
38
|
+
export const managedWorkbaseOpencode = renderManagedWorkbaseOpencode(body())
|
|
40
39
|
|
|
41
40
|
export const canUpdateManagedWorkbaseOpencode = (content: string) => {
|
|
42
41
|
const match = content.match(managedHeaderPattern)
|