@markjaquith/agency 2.48.0 → 2.48.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 +4 -11
- package/package.json +1 -1
- package/src/cli.test.ts +11 -3
- package/src/commands/init.test.ts +3 -5
- package/src/commands/integration.test.ts +1 -16
- package/src/commands/integration.ts +0 -2
- package/src/commands/work.test.ts +38 -4
- package/src/commands/work.ts +1 -1
- package/src/services/IntegrationService.test.ts +28 -74
- package/src/services/IntegrationService.ts +27 -52
- package/src/workbase/AGENTS.md +6 -6
- package/src/workbase/AGENCY_COMMAND.md +0 -48
- package/src/workbase/opencode-command-file.ts +0 -30
package/README.md
CHANGED
|
@@ -53,7 +53,6 @@ workbase/
|
|
|
53
53
|
.opencode/
|
|
54
54
|
opencode.jsonc # managed @agency subagent, instructions, and reference
|
|
55
55
|
tui.jsonc # managed TUI plugin registration
|
|
56
|
-
command/agency.md # managed /agency workflow command
|
|
57
56
|
plugin/agency-repository-skills.ts # managed workbase access and checkout skills
|
|
58
57
|
tui/agency-debug.ts # managed /agency-debug TUI diagnostic
|
|
59
58
|
agency.json # tracked config and portable repository declarations
|
|
@@ -84,10 +83,11 @@ workbase/
|
|
|
84
83
|
|
|
85
84
|
Agency keeps discovery and other observational commands read-only. Run
|
|
86
85
|
`agency integration status` to inspect `.agency/AGENTS.md` and
|
|
87
|
-
`.opencode/opencode.jsonc`, `.opencode/tui.jsonc`, and their managed
|
|
88
|
-
|
|
86
|
+
`.opencode/opencode.jsonc`, `.opencode/tui.jsonc`, and their managed plugin
|
|
87
|
+
files, then `agency integration sync` to create missing files or refresh
|
|
89
88
|
checksum-safe managed files. Customized files are reported but never
|
|
90
|
-
overwritten.
|
|
89
|
+
overwritten. Sync also removes checksum-valid retired managed artifacts while
|
|
90
|
+
preserving customized files at their former paths. The root
|
|
91
91
|
`AGENTS.md` is user-owned and is not inspected or modified by Agency.
|
|
92
92
|
|
|
93
93
|
The managed `.agency/AGENTS.md` is the complete in-workbase operating contract
|
|
@@ -122,13 +122,6 @@ The plugin grants whole-workbase access dynamically, while the portable
|
|
|
122
122
|
reference advertises that context to agents. Bash and Agency operations must
|
|
123
123
|
still follow the write authority reported by `agency context`.
|
|
124
124
|
|
|
125
|
-
OpenCode also discovers a managed `/agency` command. Use `/agency status` for a
|
|
126
|
-
read-only current-work summary, `/agency start [target]` to begin or resume work
|
|
127
|
-
in the active session, `/agency next` to inspect ready work, `/agency validate`
|
|
128
|
-
to check the workbase, and `/agency finish [target]` for verified closeout. The
|
|
129
|
-
command uses OpenCode positional arguments internally and defaults to the safe
|
|
130
|
-
`status` workflow when no subcommand is supplied.
|
|
131
|
-
|
|
132
125
|
Repository aliases and canonical fetch remotes are declared in tracked
|
|
133
126
|
`agency.json`; local bare clones and symlinks remain ignored under
|
|
134
127
|
`repos/{alias}`. A declaration contains no local path, symlink target, checkout,
|
package/package.json
CHANGED
package/src/cli.test.ts
CHANGED
|
@@ -526,7 +526,6 @@ describe("CLI", () => {
|
|
|
526
526
|
expect(before.files).toMatchObject([
|
|
527
527
|
{ name: "agents", state: "managed" },
|
|
528
528
|
{ name: "opencode", state: "managed" },
|
|
529
|
-
{ name: "opencode-command", state: "managed" },
|
|
530
529
|
{ name: "opencode-plugin", state: "managed" },
|
|
531
530
|
{ name: "opencode-tui", state: "managed" },
|
|
532
531
|
{ name: "opencode-tui-plugin", state: "managed" },
|
|
@@ -538,7 +537,6 @@ describe("CLI", () => {
|
|
|
538
537
|
expect(synced.files).toMatchObject([
|
|
539
538
|
{ name: "agents", state: "managed", changed: false },
|
|
540
539
|
{ name: "opencode", state: "managed", changed: false },
|
|
541
|
-
{ name: "opencode-command", state: "managed", changed: false },
|
|
542
540
|
{ name: "opencode-plugin", state: "managed", changed: false },
|
|
543
541
|
{ name: "opencode-tui", state: "managed", changed: false },
|
|
544
542
|
{ name: "opencode-tui-plugin", state: "managed", changed: false },
|
|
@@ -1022,7 +1020,7 @@ status: open
|
|
|
1022
1020
|
},
|
|
1023
1021
|
{
|
|
1024
1022
|
args: ["--task", "pipeline", "--phase", "build"],
|
|
1025
|
-
cwd: join(workbaseRoot, "tasks/pipeline"),
|
|
1023
|
+
cwd: join(workbaseRoot, "tasks/pipeline/phases/build"),
|
|
1026
1024
|
checkoutPath: join(
|
|
1027
1025
|
workbaseRoot,
|
|
1028
1026
|
"tasks/pipeline/phases/build/code/agency",
|
|
@@ -1060,6 +1058,16 @@ status: open
|
|
|
1060
1058
|
launch.checkoutPath,
|
|
1061
1059
|
)
|
|
1062
1060
|
expect(contract.environment.OPENCODE_CONFIG_CONTENT).toBeUndefined()
|
|
1061
|
+
if (launch === launches[1]) {
|
|
1062
|
+
const context = parseJson(
|
|
1063
|
+
await runCli(["context", ".", "--json"], contract.cwd),
|
|
1064
|
+
)
|
|
1065
|
+
expect(context.target).toMatchObject({
|
|
1066
|
+
kind: "phase",
|
|
1067
|
+
taskId: "pipeline",
|
|
1068
|
+
phaseId: "build",
|
|
1069
|
+
})
|
|
1070
|
+
}
|
|
1063
1071
|
const environment = {
|
|
1064
1072
|
...process.env,
|
|
1065
1073
|
...contract.environment,
|
|
@@ -68,11 +68,9 @@ describe("init command", () => {
|
|
|
68
68
|
workbase: expect.objectContaining({ path: ".." }),
|
|
69
69
|
})
|
|
70
70
|
expect(config.permission).toBeUndefined()
|
|
71
|
-
|
|
72
|
-
join(root, ".opencode/command/agency.md"),
|
|
73
|
-
).
|
|
74
|
-
expect(command).toContain("Workflow: `$1`")
|
|
75
|
-
expect(command).toContain("Optional target: `$2`")
|
|
71
|
+
expect(
|
|
72
|
+
await Bun.file(join(root, ".opencode/command/agency.md")).exists(),
|
|
73
|
+
).toBe(false)
|
|
76
74
|
const plugin = await Bun.file(
|
|
77
75
|
join(root, ".opencode/plugin/agency-repository-skills.ts"),
|
|
78
76
|
).text()
|
|
@@ -36,11 +36,6 @@ describe("integration command", () => {
|
|
|
36
36
|
diagnostic: expect.stringContaining("cannot load"),
|
|
37
37
|
remediation: expect.stringContaining("integration sync"),
|
|
38
38
|
},
|
|
39
|
-
{
|
|
40
|
-
name: "opencode-command",
|
|
41
|
-
state: "missing",
|
|
42
|
-
remediation: expect.stringContaining("integration sync"),
|
|
43
|
-
},
|
|
44
39
|
{
|
|
45
40
|
name: "opencode-plugin",
|
|
46
41
|
state: "missing",
|
|
@@ -91,11 +86,6 @@ OpenCode config: missing
|
|
|
91
86
|
Agency OpenCode launches cannot load current Agency instructions or whole-workbase access.
|
|
92
87
|
Action: Run 'agency integration sync' to install Agency instructions and whole-workbase OpenCode access.
|
|
93
88
|
|
|
94
|
-
OpenCode /agency command: missing
|
|
95
|
-
Path: .opencode/command/agency.md
|
|
96
|
-
The managed OpenCode /agency command needs synchronization.
|
|
97
|
-
Action: Run 'agency integration sync' to install the managed /agency command.
|
|
98
|
-
|
|
99
89
|
OpenCode workbase plugin: missing
|
|
100
90
|
Path: .opencode/plugin/agency-repository-skills.ts
|
|
101
91
|
The managed OpenCode workbase plugin needs synchronization.
|
|
@@ -120,7 +110,6 @@ OpenCode /agency-debug: missing
|
|
|
120
110
|
expect(JSON.parse(logs[0]!).files).toMatchObject([
|
|
121
111
|
{ name: "agents", state: "managed", changed: true },
|
|
122
112
|
{ name: "opencode", state: "managed", changed: true },
|
|
123
|
-
{ name: "opencode-command", state: "managed", changed: true },
|
|
124
113
|
{ name: "opencode-plugin", state: "managed", changed: true },
|
|
125
114
|
{ name: "opencode-tui", state: "managed", changed: true },
|
|
126
115
|
{ name: "opencode-tui-plugin", state: "managed", changed: true },
|
|
@@ -132,7 +121,7 @@ OpenCode /agency-debug: missing
|
|
|
132
121
|
).toBe(true)
|
|
133
122
|
expect(
|
|
134
123
|
await Bun.file(join(root, ".opencode/command/agency.md")).exists(),
|
|
135
|
-
).toBe(
|
|
124
|
+
).toBe(false)
|
|
136
125
|
expect(
|
|
137
126
|
await Bun.file(
|
|
138
127
|
join(root, ".opencode/plugin/agency-repository-skills.ts"),
|
|
@@ -161,10 +150,6 @@ OpenCode config: synced
|
|
|
161
150
|
Path: .opencode/opencode.jsonc
|
|
162
151
|
Agency's managed OpenCode launch config is ready to load Agency instructions and provide whole-workbase read access.
|
|
163
152
|
|
|
164
|
-
OpenCode /agency command: synced
|
|
165
|
-
Path: .opencode/command/agency.md
|
|
166
|
-
Agency's managed OpenCode /agency command is current.
|
|
167
|
-
|
|
168
153
|
OpenCode workbase plugin: synced
|
|
169
154
|
Path: .opencode/plugin/agency-repository-skills.ts
|
|
170
155
|
Agency's managed OpenCode plugin provides whole-workbase access and exposes writable-checkout skills.
|
|
@@ -15,7 +15,6 @@ interface IntegrationResult {
|
|
|
15
15
|
readonly name:
|
|
16
16
|
| "agents"
|
|
17
17
|
| "opencode"
|
|
18
|
-
| "opencode-command"
|
|
19
18
|
| "opencode-plugin"
|
|
20
19
|
| "opencode-tui"
|
|
21
20
|
| "opencode-tui-plugin"
|
|
@@ -30,7 +29,6 @@ interface IntegrationResult {
|
|
|
30
29
|
const integrationNames = {
|
|
31
30
|
agents: "Agent instructions",
|
|
32
31
|
opencode: "OpenCode config",
|
|
33
|
-
"opencode-command": "OpenCode /agency command",
|
|
34
32
|
"opencode-plugin": "OpenCode workbase plugin",
|
|
35
33
|
"opencode-tui": "OpenCode TUI config",
|
|
36
34
|
"opencode-tui-plugin": "OpenCode /agency-debug",
|
|
@@ -45,6 +45,8 @@ const multiPhaseWorkspace: ExecutionWorkspace = {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
const taskDirectory = "/workbase/tasks/example"
|
|
48
|
+
const phaseDirectory = `${taskDirectory}/phases/implementation`
|
|
49
|
+
|
|
48
50
|
interface HarnessOptions {
|
|
49
51
|
readonly workspace?: ExecutionWorkspace
|
|
50
52
|
readonly materializeError?: Error
|
|
@@ -563,7 +565,7 @@ describe("work command", () => {
|
|
|
563
565
|
"--prompt",
|
|
564
566
|
"Start the task. Read /workbase/tasks/example/TASK.md and /workbase/tasks/example/phases/implementation/PHASE.md.",
|
|
565
567
|
],
|
|
566
|
-
cwd:
|
|
568
|
+
cwd: phaseDirectory,
|
|
567
569
|
})
|
|
568
570
|
expect(harness.statusUpdates).toEqual([
|
|
569
571
|
"phase:example:implementation:working",
|
|
@@ -593,7 +595,11 @@ describe("work command", () => {
|
|
|
593
595
|
data: {},
|
|
594
596
|
}
|
|
595
597
|
const harness = createHarness({
|
|
596
|
-
workspace:
|
|
598
|
+
workspace: {
|
|
599
|
+
...multiPhaseWorkspace,
|
|
600
|
+
taskPath: "/workbase/tasks/delivery/TASK.md",
|
|
601
|
+
phasePath: "/workbase/tasks/delivery/phases/build/PHASE.md",
|
|
602
|
+
},
|
|
597
603
|
taskRecords: [
|
|
598
604
|
{
|
|
599
605
|
id: "delivery",
|
|
@@ -615,7 +621,9 @@ describe("work command", () => {
|
|
|
615
621
|
"probe:opencode",
|
|
616
622
|
"launch:opencode",
|
|
617
623
|
])
|
|
618
|
-
expect(harness.launches[0]?.cwd).toBe(
|
|
624
|
+
expect(harness.launches[0]?.cwd).toBe(
|
|
625
|
+
"/workbase/tasks/delivery/phases/build",
|
|
626
|
+
)
|
|
619
627
|
})
|
|
620
628
|
|
|
621
629
|
test("requires an explicit target when input is disabled", async () => {
|
|
@@ -767,6 +775,18 @@ describe("work command", () => {
|
|
|
767
775
|
])
|
|
768
776
|
})
|
|
769
777
|
|
|
778
|
+
test("launches OpenCode in the phase directory with explicit context", async () => {
|
|
779
|
+
const harness = createHarness({ workspace: multiPhaseWorkspace })
|
|
780
|
+
|
|
781
|
+
await harness.run({
|
|
782
|
+
taskId: "example",
|
|
783
|
+
phaseId: "implementation",
|
|
784
|
+
opencode: true,
|
|
785
|
+
})
|
|
786
|
+
|
|
787
|
+
expect(harness.launches[0]?.cwd).toBe(phaseDirectory)
|
|
788
|
+
})
|
|
789
|
+
|
|
770
790
|
test("sends the generated prompt only with --auto", async () => {
|
|
771
791
|
const harness = createHarness()
|
|
772
792
|
|
|
@@ -820,7 +840,7 @@ describe("work command", () => {
|
|
|
820
840
|
"--prompt",
|
|
821
841
|
"Continue the task. Read /workbase/tasks/example/TASK.md and /workbase/tasks/example/phases/implementation/PHASE.md.",
|
|
822
842
|
],
|
|
823
|
-
cwd:
|
|
843
|
+
cwd: phaseDirectory,
|
|
824
844
|
})
|
|
825
845
|
})
|
|
826
846
|
|
|
@@ -897,6 +917,20 @@ describe("work command", () => {
|
|
|
897
917
|
expect(printed.environment.API_TOKEN).toBeUndefined()
|
|
898
918
|
})
|
|
899
919
|
|
|
920
|
+
test("prints the phase directory in the command contract", async () => {
|
|
921
|
+
const harness = createHarness({ workspace: multiPhaseWorkspace })
|
|
922
|
+
const output = await captureLogs(() =>
|
|
923
|
+
harness.run({
|
|
924
|
+
taskId: "example",
|
|
925
|
+
phaseId: "implementation",
|
|
926
|
+
opencode: true,
|
|
927
|
+
printCommand: true,
|
|
928
|
+
}),
|
|
929
|
+
)
|
|
930
|
+
|
|
931
|
+
expect(JSON.parse(output.join("\n")).cwd).toBe(phaseDirectory)
|
|
932
|
+
})
|
|
933
|
+
|
|
900
934
|
test("does not reopen a forced terminal target in print-only mode", async () => {
|
|
901
935
|
const harness = createHarness({ taskStatuses: { example: "done" } })
|
|
902
936
|
|
package/src/commands/work.ts
CHANGED
|
@@ -261,7 +261,7 @@ export const work = (
|
|
|
261
261
|
prompt = workspace.phasePath
|
|
262
262
|
? `${action} the task. Read ${workspace.taskPath} and ${workspace.phasePath}.`
|
|
263
263
|
: `${action} the task. Read ${workspace.taskPath}.`
|
|
264
|
-
launchPath = dirname(workspace.taskPath)
|
|
264
|
+
launchPath = dirname(workspace.phasePath ?? workspace.taskPath)
|
|
265
265
|
writablePath = workspace.writablePath
|
|
266
266
|
}
|
|
267
267
|
|
|
@@ -6,10 +6,6 @@ import { dirname, join } from "node:path"
|
|
|
6
6
|
import { pathToFileURL } from "node:url"
|
|
7
7
|
import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
|
|
8
8
|
import { managedWorkbaseAgents } from "../workbase/agents-file"
|
|
9
|
-
import {
|
|
10
|
-
canUpdateManagedWorkbaseOpencodeCommand,
|
|
11
|
-
managedWorkbaseOpencodeCommand,
|
|
12
|
-
} from "../workbase/opencode-command-file"
|
|
13
9
|
import { managedWorkbaseOpencode } from "../workbase/opencode-file"
|
|
14
10
|
import {
|
|
15
11
|
canUpdateManagedWorkbaseOpencodePlugin,
|
|
@@ -36,6 +32,12 @@ const managed = (prefix: string, body: string, suffix = "") => {
|
|
|
36
32
|
const managedBody = (content: string) =>
|
|
37
33
|
content.slice(content.indexOf("\n\n") + 2)
|
|
38
34
|
|
|
35
|
+
const legacyCommand = (content: string) =>
|
|
36
|
+
content.replace(
|
|
37
|
+
/^---\n/,
|
|
38
|
+
`---\n# agency-managed: sha256=${createHash("sha256").update(content).digest("hex")}\n`,
|
|
39
|
+
)
|
|
40
|
+
|
|
39
41
|
const status = (root: string) =>
|
|
40
42
|
runTestEffect(
|
|
41
43
|
IntegrationService.pipe(Effect.flatMap((service) => service.status(root))),
|
|
@@ -63,17 +65,11 @@ describe("IntegrationService", () => {
|
|
|
63
65
|
"missing",
|
|
64
66
|
"missing",
|
|
65
67
|
"missing",
|
|
66
|
-
"missing",
|
|
67
68
|
])
|
|
68
69
|
expect(await Bun.file(join(root, ".agency/AGENTS.md")).exists()).toBe(false)
|
|
69
70
|
|
|
70
71
|
await write(root, ".agency/AGENTS.md", managedWorkbaseAgents)
|
|
71
72
|
await write(root, ".opencode/opencode.jsonc", managedWorkbaseOpencode)
|
|
72
|
-
await write(
|
|
73
|
-
root,
|
|
74
|
-
".opencode/command/agency.md",
|
|
75
|
-
managedWorkbaseOpencodeCommand,
|
|
76
|
-
)
|
|
77
73
|
await write(
|
|
78
74
|
root,
|
|
79
75
|
".opencode/plugin/agency-repository-skills.ts",
|
|
@@ -91,7 +87,6 @@ describe("IntegrationService", () => {
|
|
|
91
87
|
"managed",
|
|
92
88
|
"managed",
|
|
93
89
|
"managed",
|
|
94
|
-
"managed",
|
|
95
90
|
])
|
|
96
91
|
})
|
|
97
92
|
|
|
@@ -109,7 +104,6 @@ describe("IntegrationService", () => {
|
|
|
109
104
|
"missing",
|
|
110
105
|
"missing",
|
|
111
106
|
"missing",
|
|
112
|
-
"missing",
|
|
113
107
|
])
|
|
114
108
|
})
|
|
115
109
|
|
|
@@ -257,36 +251,6 @@ describe("IntegrationService", () => {
|
|
|
257
251
|
).toBe(true)
|
|
258
252
|
})
|
|
259
253
|
|
|
260
|
-
test("generates a positional OpenCode command for Agency workflows", () => {
|
|
261
|
-
expect(managedWorkbaseOpencodeCommand).toContain(
|
|
262
|
-
"description: Operate Agency work",
|
|
263
|
-
)
|
|
264
|
-
expect(managedWorkbaseOpencodeCommand).toContain("Workflow: `$1`")
|
|
265
|
-
expect(managedWorkbaseOpencodeCommand).toContain("Optional target: `$2`")
|
|
266
|
-
expect(managedWorkbaseOpencodeCommand).toContain(
|
|
267
|
-
"Complete request: `$ARGUMENTS`",
|
|
268
|
-
)
|
|
269
|
-
for (const workflow of [
|
|
270
|
-
"start",
|
|
271
|
-
"status",
|
|
272
|
-
"next",
|
|
273
|
-
"validate",
|
|
274
|
-
"finish",
|
|
275
|
-
"help",
|
|
276
|
-
]) {
|
|
277
|
-
expect(managedWorkbaseOpencodeCommand).toContain(`- \`${workflow}\`:`)
|
|
278
|
-
}
|
|
279
|
-
expect(managedWorkbaseOpencodeCommand).toContain("Never run `agency work`")
|
|
280
|
-
expect(
|
|
281
|
-
canUpdateManagedWorkbaseOpencodeCommand(managedWorkbaseOpencodeCommand),
|
|
282
|
-
).toBe(true)
|
|
283
|
-
expect(
|
|
284
|
-
canUpdateManagedWorkbaseOpencodeCommand(
|
|
285
|
-
managedWorkbaseOpencodeCommand.replace("Workflow: `$1`", "Workflow"),
|
|
286
|
-
),
|
|
287
|
-
).toBe(false)
|
|
288
|
-
})
|
|
289
|
-
|
|
290
254
|
test("generates context-first safety and execution closeout guidance", () => {
|
|
291
255
|
const body = managedBody(managedWorkbaseAgents)
|
|
292
256
|
|
|
@@ -323,7 +287,7 @@ describe("IntegrationService", () => {
|
|
|
323
287
|
expect(body).toContain("agency integration status")
|
|
324
288
|
expect(body).not.toContain("SKILL.md")
|
|
325
289
|
expect(body).not.toContain("~/.agents")
|
|
326
|
-
expect(body).toContain(".opencode/command/agency.md")
|
|
290
|
+
expect(body).not.toContain(".opencode/command/agency.md")
|
|
327
291
|
})
|
|
328
292
|
|
|
329
293
|
test("configures Agency agents with complete workbase access", () => {
|
|
@@ -454,7 +418,6 @@ describe("IntegrationService", () => {
|
|
|
454
418
|
expect(first.files).toMatchObject([
|
|
455
419
|
{ name: "agents", state: "customized", changed: false },
|
|
456
420
|
{ name: "opencode", state: "managed", changed: true },
|
|
457
|
-
{ name: "opencode-command", state: "managed", changed: true },
|
|
458
421
|
{ name: "opencode-plugin", state: "managed", changed: true },
|
|
459
422
|
{ name: "opencode-tui", state: "managed", changed: true },
|
|
460
423
|
{ name: "opencode-tui-plugin", state: "managed", changed: true },
|
|
@@ -469,8 +432,8 @@ describe("IntegrationService", () => {
|
|
|
469
432
|
managedWorkbaseOpencode,
|
|
470
433
|
)
|
|
471
434
|
expect(
|
|
472
|
-
await Bun.file(join(root, ".opencode/command/agency.md")).
|
|
473
|
-
).toBe(
|
|
435
|
+
await Bun.file(join(root, ".opencode/command/agency.md")).exists(),
|
|
436
|
+
).toBe(false)
|
|
474
437
|
expect(
|
|
475
438
|
await Bun.file(
|
|
476
439
|
join(root, ".opencode/plugin/agency-repository-skills.ts"),
|
|
@@ -524,7 +487,7 @@ describe("IntegrationService", () => {
|
|
|
524
487
|
await write(root, ".opencode/plugins/agency-repository-skills.ts", custom)
|
|
525
488
|
|
|
526
489
|
let result = await sync(root)
|
|
527
|
-
expect(result.files[
|
|
490
|
+
expect(result.files[2]).toMatchObject({
|
|
528
491
|
name: "opencode-plugin",
|
|
529
492
|
path: join(root, ".opencode/plugins/agency-repository-skills.ts"),
|
|
530
493
|
state: "customized",
|
|
@@ -539,7 +502,7 @@ describe("IntegrationService", () => {
|
|
|
539
502
|
await unlink(join(root, ".opencode/plugins/agency-repository-skills.ts"))
|
|
540
503
|
await write(root, ".opencode/plugin/agency-repository-skills.ts", custom)
|
|
541
504
|
result = await sync(root)
|
|
542
|
-
expect(result.files[
|
|
505
|
+
expect(result.files[2]).toMatchObject({
|
|
543
506
|
path: join(root, ".opencode/plugin/agency-repository-skills.ts"),
|
|
544
507
|
state: "customized",
|
|
545
508
|
changed: false,
|
|
@@ -554,14 +517,14 @@ describe("IntegrationService", () => {
|
|
|
554
517
|
await write(root, ".opencode/tui/agency-debug.ts", customPlugin)
|
|
555
518
|
|
|
556
519
|
const result = await sync(root)
|
|
557
|
-
expect(result.files[
|
|
520
|
+
expect(result.files[3]).toMatchObject({
|
|
558
521
|
name: "opencode-tui",
|
|
559
522
|
path: join(root, ".opencode/tui.json"),
|
|
560
523
|
state: "customized",
|
|
561
524
|
changed: false,
|
|
562
525
|
remediation: expect.stringContaining("plugin list"),
|
|
563
526
|
})
|
|
564
|
-
expect(result.files[
|
|
527
|
+
expect(result.files[4]).toMatchObject({
|
|
565
528
|
name: "opencode-tui-plugin",
|
|
566
529
|
state: "customized",
|
|
567
530
|
changed: false,
|
|
@@ -574,32 +537,23 @@ describe("IntegrationService", () => {
|
|
|
574
537
|
).toBe(customPlugin)
|
|
575
538
|
})
|
|
576
539
|
|
|
577
|
-
test("
|
|
578
|
-
const
|
|
579
|
-
|
|
540
|
+
test("removes a checksum-valid legacy command and preserves a customized file", async () => {
|
|
541
|
+
const path = ".opencode/command/agency.md"
|
|
542
|
+
const legacy = legacyCommand(
|
|
543
|
+
"---\ndescription: Operate Agency work\n---\n\nLegacy command.\n",
|
|
544
|
+
)
|
|
545
|
+
await write(root, path, legacy)
|
|
580
546
|
|
|
581
|
-
|
|
582
|
-
expect(
|
|
583
|
-
name: "opencode-command",
|
|
584
|
-
path: join(root, ".opencode/commands/agency.md"),
|
|
585
|
-
state: "customized",
|
|
586
|
-
changed: false,
|
|
587
|
-
})
|
|
588
|
-
expect(
|
|
589
|
-
await Bun.file(join(root, ".opencode/command/agency.md")).exists(),
|
|
590
|
-
).toBe(false)
|
|
547
|
+
await status(root)
|
|
548
|
+
expect(await Bun.file(join(root, path)).text()).toBe(legacy)
|
|
591
549
|
|
|
592
|
-
await
|
|
593
|
-
await
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
})
|
|
600
|
-
expect(
|
|
601
|
-
await Bun.file(join(root, ".opencode/command/agency.md")).text(),
|
|
602
|
-
).toBe(custom)
|
|
550
|
+
await sync(root)
|
|
551
|
+
expect(await Bun.file(join(root, path)).exists()).toBe(false)
|
|
552
|
+
|
|
553
|
+
const custom = `${legacy}User edit.\n`
|
|
554
|
+
await write(root, path, custom)
|
|
555
|
+
await sync(root)
|
|
556
|
+
expect(await Bun.file(join(root, path)).text()).toBe(custom)
|
|
603
557
|
})
|
|
604
558
|
|
|
605
559
|
test("migrates checksum-valid root instructions", async () => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Effect } from "effect"
|
|
2
|
+
import { createHash } from "node:crypto"
|
|
2
3
|
import { join } from "node:path"
|
|
3
4
|
import { FileSystemService } from "./FileSystemService"
|
|
4
5
|
import { WorkbaseService } from "./WorkbaseService"
|
|
@@ -10,10 +11,6 @@ import {
|
|
|
10
11
|
canUpdateManagedWorkbaseOpencode,
|
|
11
12
|
managedWorkbaseOpencode,
|
|
12
13
|
} from "../workbase/opencode-file"
|
|
13
|
-
import {
|
|
14
|
-
canUpdateManagedWorkbaseOpencodeCommand,
|
|
15
|
-
managedWorkbaseOpencodeCommand,
|
|
16
|
-
} from "../workbase/opencode-command-file"
|
|
17
14
|
import {
|
|
18
15
|
canUpdateManagedWorkbaseOpencodePlugin,
|
|
19
16
|
managedWorkbaseOpencodePlugin,
|
|
@@ -33,7 +30,6 @@ interface IntegrationFileStatus {
|
|
|
33
30
|
readonly name:
|
|
34
31
|
| "agents"
|
|
35
32
|
| "opencode"
|
|
36
|
-
| "opencode-command"
|
|
37
33
|
| "opencode-plugin"
|
|
38
34
|
| "opencode-tui"
|
|
39
35
|
| "opencode-tui-plugin"
|
|
@@ -74,25 +70,6 @@ const describe = (
|
|
|
74
70
|
"Run 'agency integration sync' to install Agency instructions and whole-workbase OpenCode access.",
|
|
75
71
|
}
|
|
76
72
|
}
|
|
77
|
-
if (name === "opencode-command") {
|
|
78
|
-
return state === "managed"
|
|
79
|
-
? {
|
|
80
|
-
diagnostic: "Agency's managed OpenCode /agency command is current.",
|
|
81
|
-
remediation: null,
|
|
82
|
-
}
|
|
83
|
-
: state === "customized"
|
|
84
|
-
? {
|
|
85
|
-
diagnostic:
|
|
86
|
-
"A user-owned OpenCode /agency command is present and was preserved.",
|
|
87
|
-
remediation: null,
|
|
88
|
-
}
|
|
89
|
-
: {
|
|
90
|
-
diagnostic:
|
|
91
|
-
"The managed OpenCode /agency command needs synchronization.",
|
|
92
|
-
remediation:
|
|
93
|
-
"Run 'agency integration sync' to install the managed /agency command.",
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
73
|
if (name === "opencode-plugin") {
|
|
97
74
|
return state === "managed"
|
|
98
75
|
? {
|
|
@@ -202,8 +179,6 @@ const inspect = (root: string) =>
|
|
|
202
179
|
const opencodeJsonPath = join(opencodeDirectory, "opencode.json")
|
|
203
180
|
const tuiPath = join(opencodeDirectory, "tui.jsonc")
|
|
204
181
|
const tuiJsonPath = join(opencodeDirectory, "tui.json")
|
|
205
|
-
const commandPath = join(opencodeDirectory, "command", "agency.md")
|
|
206
|
-
const pluralCommandPath = join(opencodeDirectory, "commands", "agency.md")
|
|
207
182
|
const pluginPath = join(
|
|
208
183
|
opencodeDirectory,
|
|
209
184
|
"plugin",
|
|
@@ -252,29 +227,6 @@ const inspect = (root: string) =>
|
|
|
252
227
|
files.push(fileStatus("opencode", opencodePath, "missing"))
|
|
253
228
|
}
|
|
254
229
|
|
|
255
|
-
if ((yield* fs.readSymlinkTarget(commandPath)) !== null) {
|
|
256
|
-
files.push(fileStatus("opencode-command", commandPath, "customized"))
|
|
257
|
-
} else if (
|
|
258
|
-
(yield* fs.readSymlinkTarget(pluralCommandPath)) !== null ||
|
|
259
|
-
(yield* fs.exists(pluralCommandPath))
|
|
260
|
-
) {
|
|
261
|
-
files.push(
|
|
262
|
-
fileStatus("opencode-command", pluralCommandPath, "customized"),
|
|
263
|
-
)
|
|
264
|
-
} else if (yield* fs.exists(commandPath)) {
|
|
265
|
-
files.push(
|
|
266
|
-
classify(
|
|
267
|
-
"opencode-command",
|
|
268
|
-
commandPath,
|
|
269
|
-
yield* fs.readFile(commandPath),
|
|
270
|
-
managedWorkbaseOpencodeCommand,
|
|
271
|
-
canUpdateManagedWorkbaseOpencodeCommand,
|
|
272
|
-
),
|
|
273
|
-
)
|
|
274
|
-
} else {
|
|
275
|
-
files.push(fileStatus("opencode-command", commandPath, "missing"))
|
|
276
|
-
}
|
|
277
|
-
|
|
278
230
|
if ((yield* fs.readSymlinkTarget(pluginPath)) !== null) {
|
|
279
231
|
files.push(fileStatus("opencode-plugin", pluginPath, "customized"))
|
|
280
232
|
} else if (
|
|
@@ -348,6 +300,25 @@ const canRemoveLegacyAgents = (root: string) =>
|
|
|
348
300
|
return canUpdateManagedWorkbaseAgents(yield* fs.readFile(path))
|
|
349
301
|
})
|
|
350
302
|
|
|
303
|
+
const canRemoveLegacyOpencodeCommand = (root: string) =>
|
|
304
|
+
Effect.gen(function* () {
|
|
305
|
+
const fs = yield* FileSystemService
|
|
306
|
+
const path = join(root, ".opencode", "command", "agency.md")
|
|
307
|
+
if (
|
|
308
|
+
(yield* fs.readSymlinkTarget(path)) !== null ||
|
|
309
|
+
!(yield* fs.exists(path))
|
|
310
|
+
)
|
|
311
|
+
return false
|
|
312
|
+
|
|
313
|
+
const content = yield* fs.readFile(path)
|
|
314
|
+
const header = /^---\r?\n# agency-managed: sha256=([a-f0-9]{64})\r?\n/
|
|
315
|
+
const match = content.match(header)
|
|
316
|
+
if (!match?.[1]) return false
|
|
317
|
+
|
|
318
|
+
const canonical = content.replace(header, "---\n")
|
|
319
|
+
return createHash("sha256").update(canonical).digest("hex") === match[1]
|
|
320
|
+
})
|
|
321
|
+
|
|
351
322
|
export class IntegrationService extends Effect.Service<IntegrationService>()(
|
|
352
323
|
"IntegrationService",
|
|
353
324
|
{
|
|
@@ -370,6 +341,8 @@ export class IntegrationService extends Effect.Service<IntegrationService>()(
|
|
|
370
341
|
(status) =>
|
|
371
342
|
status.name === "opencode" && status.state !== "customized",
|
|
372
343
|
) && (yield* canRemoveLegacyAgents(root))
|
|
344
|
+
const removeLegacyOpencodeCommand =
|
|
345
|
+
yield* canRemoveLegacyOpencodeCommand(root)
|
|
373
346
|
const files: IntegrationSyncFile[] = []
|
|
374
347
|
|
|
375
348
|
for (const status of statuses) {
|
|
@@ -382,9 +355,6 @@ export class IntegrationService extends Effect.Service<IntegrationService>()(
|
|
|
382
355
|
} else if (status.name === "opencode") {
|
|
383
356
|
yield* fs.createDirectory(join(root, ".opencode"))
|
|
384
357
|
yield* fs.writeFile(status.path, managedWorkbaseOpencode)
|
|
385
|
-
} else if (status.name === "opencode-command") {
|
|
386
|
-
yield* fs.createDirectory(join(root, ".opencode", "command"))
|
|
387
|
-
yield* fs.writeFile(status.path, managedWorkbaseOpencodeCommand)
|
|
388
358
|
} else if (status.name === "opencode-plugin") {
|
|
389
359
|
yield* fs.createDirectory(join(root, ".opencode", "plugin"))
|
|
390
360
|
yield* fs.writeFile(status.path, managedWorkbaseOpencodePlugin)
|
|
@@ -411,6 +381,11 @@ export class IntegrationService extends Effect.Service<IntegrationService>()(
|
|
|
411
381
|
}
|
|
412
382
|
|
|
413
383
|
if (removeLegacyAgents) yield* fs.deleteFile(join(root, "AGENTS.md"))
|
|
384
|
+
if (removeLegacyOpencodeCommand) {
|
|
385
|
+
yield* fs.deleteFile(
|
|
386
|
+
join(root, ".opencode", "command", "agency.md"),
|
|
387
|
+
)
|
|
388
|
+
}
|
|
414
389
|
|
|
415
390
|
return { root, files }
|
|
416
391
|
}),
|
package/src/workbase/AGENTS.md
CHANGED
|
@@ -100,15 +100,15 @@ a refinement loop, or pausing or handing off completed implementation work):
|
|
|
100
100
|
`agency integration status` reports `managed`, `drifted`, `customized`, or
|
|
101
101
|
`missing` generated files. Agency keeps these instructions in
|
|
102
102
|
`.agency/AGENTS.md`, and its managed OpenCode config loads them automatically.
|
|
103
|
-
It also installs
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
TUI companion providing `/agency-debug` without submitting an LLM prompt.
|
|
103
|
+
It also installs a managed server plugin that exposes skills from the
|
|
104
|
+
authoritative writable checkout and an explicitly registered TUI companion
|
|
105
|
+
providing `/agency-debug` without submitting an LLM prompt.
|
|
107
106
|
The workbase-root `AGENTS.md`, when present, belongs entirely to the workbase
|
|
108
107
|
owner and composes with these instructions through OpenCode's normal discovery.
|
|
109
108
|
`agency integration sync` updates only missing or checksum-safe drifted managed
|
|
110
|
-
files and preserves user-customized
|
|
111
|
-
files, and `agency work` reconciles them
|
|
109
|
+
files, removes checksum-valid retired artifacts, and preserves user-customized
|
|
110
|
+
files. `agency init` creates the managed files, and `agency work` reconciles them
|
|
111
|
+
before launching an agent.
|
|
112
112
|
|
|
113
113
|
OpenCode can access the complete workbase tree, but this filesystem permission
|
|
114
114
|
does not expand Agency write authority beyond the checkout reported by
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Operate Agency work with safe start, status, next, validate, and finish workflows
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
Operate the current Agency workbase using the managed Agency instructions.
|
|
6
|
-
|
|
7
|
-
Invocation inputs:
|
|
8
|
-
|
|
9
|
-
- Workflow: `$1`
|
|
10
|
-
- Optional target: `$2`
|
|
11
|
-
- Complete request: `$ARGUMENTS`
|
|
12
|
-
|
|
13
|
-
Use `status` when the workflow is empty. Treat words after the optional target as
|
|
14
|
-
additional user instructions. If the workflow is unknown, make no changes and
|
|
15
|
-
list the supported workflows.
|
|
16
|
-
|
|
17
|
-
Always follow these rules:
|
|
18
|
-
|
|
19
|
-
- Run `agency context . --json` first when no target is provided. With a target,
|
|
20
|
-
pass that target to `agency context` instead.
|
|
21
|
-
- Use the returned document paths, readiness, authority, checkout state, claim,
|
|
22
|
-
and validation result. Do not infer them from directory names.
|
|
23
|
-
- Stop on validation errors, dependency blockers, an unexpected writable
|
|
24
|
-
repository, or a conflicting active claim.
|
|
25
|
-
- Write code only in `authority.writable.checkoutPath`; references are read-only.
|
|
26
|
-
- Never run `agency work` to start the current agent again.
|
|
27
|
-
- Create a pull request only when the complete request explicitly asks for one.
|
|
28
|
-
|
|
29
|
-
Dispatch the workflow as follows:
|
|
30
|
-
|
|
31
|
-
- `start`: Read the returned task and phase documents, inspect the writable
|
|
32
|
-
checkout, then begin or resume the requested work in this session. Keep durable
|
|
33
|
-
decisions current and carry the work through focused verification.
|
|
34
|
-
- `status`: Make no changes. Summarize the target, readiness, authority, durable
|
|
35
|
-
status, claim and PR state, checkout state, current Git changes, blockers, and
|
|
36
|
-
the most useful next action.
|
|
37
|
-
- `next`: Run `agency next --json`, summarize ready and blocked execution units,
|
|
38
|
-
and recommend the highest-priority ready unit. Do not launch another agent.
|
|
39
|
-
- `validate`: Run Agency validation for the discovered workbase. Explain every
|
|
40
|
-
issue and, when safe and within authority, repair requested non-structural
|
|
41
|
-
problems before validating again.
|
|
42
|
-
- `finish`: Complete any remaining requested implementation first. Run focused
|
|
43
|
-
repository checks, refresh the task or phase delivery context, and run
|
|
44
|
-
`agency validate`. Finish an active claim with its current revision; otherwise
|
|
45
|
-
update unclaimed status only when the requested outcome and delivery work are
|
|
46
|
-
actually complete. Do not create a PR unless explicitly requested.
|
|
47
|
-
- `help`: Make no changes. Briefly explain these workflows and the positional
|
|
48
|
-
form `/agency <workflow> [target] [additional instructions]`.
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto"
|
|
2
|
-
import commandTemplate from "./AGENCY_COMMAND.md" with { type: "text" }
|
|
3
|
-
|
|
4
|
-
const managedHeaderPattern =
|
|
5
|
-
/^---\r?\n# agency-managed: sha256=([a-f0-9]{64})\r?\n/
|
|
6
|
-
|
|
7
|
-
const checksum = (content: string) =>
|
|
8
|
-
createHash("sha256").update(content).digest("hex")
|
|
9
|
-
|
|
10
|
-
const canonicalBody = commandTemplate.endsWith("\n")
|
|
11
|
-
? commandTemplate
|
|
12
|
-
: `${commandTemplate}\n`
|
|
13
|
-
|
|
14
|
-
const renderManagedWorkbaseOpencodeCommand = (
|
|
15
|
-
content: string = canonicalBody,
|
|
16
|
-
) =>
|
|
17
|
-
content.replace(
|
|
18
|
-
/^---\n/,
|
|
19
|
-
`---\n# agency-managed: sha256=${checksum(content)}\n`,
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
export const managedWorkbaseOpencodeCommand =
|
|
23
|
-
renderManagedWorkbaseOpencodeCommand()
|
|
24
|
-
|
|
25
|
-
export const canUpdateManagedWorkbaseOpencodeCommand = (content: string) => {
|
|
26
|
-
const match = content.match(managedHeaderPattern)
|
|
27
|
-
if (!match?.[1]) return false
|
|
28
|
-
|
|
29
|
-
return checksum(content.replace(managedHeaderPattern, "---\n")) === match[1]
|
|
30
|
-
}
|