@markjaquith/agency 2.40.0 → 2.41.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/skills/agency/SKILL.md +6 -5
- package/skills/agency/references/commands.md +2 -2
- package/skills/agency/references/contracts.md +2 -0
- package/skills/agency/references/recipes.md +5 -3
- package/src/cli.test.ts +15 -9
- package/src/commands/claim.ts +2 -1
- package/src/commands/init.test.ts +5 -0
- package/src/commands/integration.test.ts +9 -0
- package/src/commands/integration.ts +2 -2
- package/src/commands/phase.ts +1 -1
- package/src/commands/read-only.test.ts +3 -0
- package/src/commands/status.test.ts +1 -1
- package/src/commands/task-phase.test.ts +4 -4
- package/src/commands/task.ts +1 -1
- package/src/readiness.ts +3 -3
- package/src/services/ClaimService.test.ts +1 -1
- package/src/services/ClaimService.ts +5 -1
- package/src/services/IntegrationService.test.ts +79 -3
- package/src/services/IntegrationService.ts +53 -2
- package/src/services/PhaseService.ts +6 -0
- package/src/services/PullRequestService.test.ts +4 -2
- package/src/services/SyncService.test.ts +93 -0
- package/src/services/TaskPhaseService.test.ts +19 -20
- package/src/services/TaskService.ts +6 -0
- package/src/workbase/AGENCY_COMMAND.md +48 -0
- package/src/workbase/AGENTS.md +11 -7
- package/src/workbase/opencode-command-file.ts +30 -0
package/README.md
CHANGED
|
@@ -80,10 +80,10 @@ workbase/
|
|
|
80
80
|
|
|
81
81
|
Agency keeps discovery and other observational commands read-only. Run
|
|
82
82
|
`agency integration status` to inspect `.agency/AGENTS.md` and
|
|
83
|
-
`.opencode/opencode.jsonc`,
|
|
84
|
-
files or refresh checksum-safe managed
|
|
85
|
-
|
|
86
|
-
modified by Agency.
|
|
83
|
+
`.opencode/opencode.jsonc`, and `.opencode/command/agency.md`, then `agency
|
|
84
|
+
integration sync` to create missing files or refresh checksum-safe managed
|
|
85
|
+
files. Customized files are reported but never overwritten. The root
|
|
86
|
+
`AGENTS.md` is user-owned and is not inspected or modified by Agency.
|
|
87
87
|
|
|
88
88
|
When upgrading an existing workbase, synchronization moves a checksum-valid
|
|
89
89
|
Agency-managed root `AGENTS.md` to `.agency/AGENTS.md` once the OpenCode config
|
|
@@ -96,6 +96,13 @@ reference. OpenCode discovers that config from task and epic launch directories.
|
|
|
96
96
|
Agents receive whole-workbase visibility from that reference. Bash and Agency
|
|
97
97
|
operations must still follow the write authority reported by `agency context`.
|
|
98
98
|
|
|
99
|
+
OpenCode also discovers a managed `/agency` command. Use `/agency status` for a
|
|
100
|
+
read-only current-work summary, `/agency start [target]` to begin or resume work
|
|
101
|
+
in the active session, `/agency next` to inspect ready work, `/agency validate`
|
|
102
|
+
to check the workbase, and `/agency finish [target]` for verified closeout. The
|
|
103
|
+
command uses OpenCode positional arguments internally and defaults to the safe
|
|
104
|
+
`status` workflow when no subcommand is supplied.
|
|
105
|
+
|
|
99
106
|
Repository aliases and canonical fetch remotes are declared in tracked
|
|
100
107
|
`agency.json`; local bare clones and symlinks remain ignored under
|
|
101
108
|
`repos/{alias}`. A declaration contains no local path, symlink target, checkout,
|
|
@@ -569,7 +576,7 @@ Targeted commands accept `--epic`, `--task`, and `--phase` where those entity
|
|
|
569
576
|
kinds apply. A phase selector requires a task selector. Entity selectors cannot
|
|
570
577
|
be mixed with positional target IDs, and an epic selector cannot be mixed with
|
|
571
578
|
task or phase selectors. This makes commands such as
|
|
572
|
-
`agency phase status
|
|
579
|
+
`agency phase status working --task ship --phase release --workbase primary --no-input`
|
|
573
580
|
fully independent of process cwd and prompts.
|
|
574
581
|
|
|
575
582
|
Inspect tasks:
|
|
@@ -577,7 +584,7 @@ Inspect tasks:
|
|
|
577
584
|
```text
|
|
578
585
|
agency task list [filters] [--json]
|
|
579
586
|
agency task show <id> [--json]
|
|
580
|
-
agency task status <id> <open|
|
|
587
|
+
agency task status <id> <open|working|dropped> [--json]
|
|
581
588
|
agency task update <id> [metadata options] [--json]
|
|
582
589
|
agency task rename <id> <new-id> [--json]
|
|
583
590
|
agency task move <id> (--epic <epic-id> | --no-epic) [--json]
|
|
@@ -616,7 +623,7 @@ agency phase create <task-id> <phase-id>
|
|
|
616
623
|
|
|
617
624
|
agency phase list <task-id> [filters] [--json]
|
|
618
625
|
agency phase show <task-id> <phase-id> [--json]
|
|
619
|
-
agency phase status <task-id> <phase-id> <open|
|
|
626
|
+
agency phase status <task-id> <phase-id> <open|working|dropped> [--json]
|
|
620
627
|
agency phase update <task-id> <phase-id> [metadata options] [--json]
|
|
621
628
|
agency phase rename <task-id> <phase-id> <new-id> [--json]
|
|
622
629
|
agency phase dependency <add|remove> <task-id> <phase-id> <dependency-id>
|
|
@@ -639,6 +646,8 @@ writing anything.
|
|
|
639
646
|
Single-phase tasks and phases store status in YAML. New execution units start
|
|
640
647
|
`open`, and `agency work` marks the selected execution unit `working` immediately
|
|
641
648
|
before launch. Running `agency work` again can relaunch unclaimed `working` work.
|
|
649
|
+
The `done` status is reserved for an authoritative merged pull request and is
|
|
650
|
+
applied by `agency sync --apply`, not by task or phase status commands.
|
|
642
651
|
Use explicit claims only when an external orchestrator needs coordinated
|
|
643
652
|
ownership. The interactive work selector displays status markers before
|
|
644
653
|
execution units. Existing working and delegated work may be released to `open`
|
|
@@ -672,11 +681,12 @@ agency finish <task-id> [phase-id] --session-id <id>
|
|
|
672
681
|
--revision <sha256> --outcome <done|dropped> [--json]
|
|
673
682
|
```
|
|
674
683
|
|
|
675
|
-
An active claim sets status to `working`. Release returns it to `open
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
684
|
+
An active claim sets status to `working`. Release returns it to `open`. Finish
|
|
685
|
+
records the claim outcome and ownership history; a `done` claim outcome leaves
|
|
686
|
+
the execution unit `working` until its pull request is merged, while `dropped`
|
|
687
|
+
remains terminal. Conflicts return the current revision and complete ownership
|
|
688
|
+
record in the machine error envelope rather than overwriting it. Expired claims
|
|
689
|
+
may be replaced with a revision-guarded claim.
|
|
680
690
|
|
|
681
691
|
`agency work` does not claim execution units. It refuses active explicit claims,
|
|
682
692
|
marks open execution work `working`, and launches the runner. External
|
package/package.json
CHANGED
package/skills/agency/SKILL.md
CHANGED
|
@@ -118,11 +118,12 @@ independently meaningful tasks need coordination.
|
|
|
118
118
|
|
|
119
119
|
1. Re-run `agency validate` and repository checks.
|
|
120
120
|
2. Create a PR only when requested: `agency pr create <task> [phase]`.
|
|
121
|
-
3.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
121
|
+
3. Keep committed work `working` while its pull request is open. After the
|
|
122
|
+
authoritative pull request merges, use `agency sync --apply` to reconcile it
|
|
123
|
+
to `done`.
|
|
124
|
+
4. If the session has a claim, use revision-guarded `agency finish`; a successful
|
|
125
|
+
claim outcome closes ownership without marking unmerged work `done`. Use
|
|
126
|
+
`dropped` only for intentionally abandoned work.
|
|
126
127
|
5. Report the durable status and PR URL. Do not manually remove the worktree.
|
|
127
128
|
|
|
128
129
|
## Human Launch vs Active Agent
|
|
@@ -89,7 +89,7 @@ agency task create <id> --multi-phase [--ticket-url <url>]
|
|
|
89
89
|
[--description <text>] [--epic <id>] [--json]
|
|
90
90
|
agency task list [filters] [--json]
|
|
91
91
|
agency task show <id> [--json]
|
|
92
|
-
agency task status <id> <open|
|
|
92
|
+
agency task status <id> <open|working|dropped> [--json]
|
|
93
93
|
agency task update <id> [metadata options] [--if-revision <hash>] [--json]
|
|
94
94
|
agency task rename <id> <new-id> [--if-revision <hash>] [--json]
|
|
95
95
|
agency task move <id> (--epic <epic-id> | --no-epic)
|
|
@@ -102,7 +102,7 @@ agency phase create <task-id> <phase-id> --repo <alias> --branch <name>
|
|
|
102
102
|
[--depends-on <phase-id>...] [--first-phase <phase-id>] [--json]
|
|
103
103
|
agency phase list <task-id> [filters] [--json]
|
|
104
104
|
agency phase show <task-id> <phase-id> [--json]
|
|
105
|
-
agency phase status <task-id> <phase-id> <open|
|
|
105
|
+
agency phase status <task-id> <phase-id> <open|working|dropped> [--json]
|
|
106
106
|
agency phase update <task-id> <phase-id> [metadata options]
|
|
107
107
|
[--if-revision <hash>] [--json]
|
|
108
108
|
agency phase rename <task-id> <phase-id> <new-id>
|
|
@@ -137,6 +137,8 @@ readable legacy state but cannot be newly assigned.
|
|
|
137
137
|
- An execution unit has one writable `repo`; plural `repos` are read-only
|
|
138
138
|
`{ repo, ref }` entries and cannot repeat the writable alias.
|
|
139
139
|
- A writable `(repo, branch)` pair belongs to exactly one active execution unit.
|
|
140
|
+
- `done` represents an authoritative merged pull request and is reconciled by
|
|
141
|
+
`agency sync --apply`; committed or review-ready work remains `working`.
|
|
140
142
|
- Only `done` satisfies dependencies. `dropped` is terminal but blocks dependents.
|
|
141
143
|
- Epic task dependencies live in `EPIC.md`; phase dependencies live in `TASK.md`.
|
|
142
144
|
- IDs remain stable; use `dependsOn`, not numeric directory prefixes, for order.
|
|
@@ -184,15 +184,17 @@ agency release checkout ui --session-id session-123 \
|
|
|
184
184
|
|
|
185
185
|
## Finish Verified Work
|
|
186
186
|
|
|
187
|
-
|
|
187
|
+
After the claimed run completes successfully:
|
|
188
188
|
|
|
189
189
|
```bash
|
|
190
190
|
agency finish checkout ui --session-id session-123 \
|
|
191
191
|
--revision <current-sha256> --outcome done --json
|
|
192
192
|
```
|
|
193
193
|
|
|
194
|
-
Do not substitute
|
|
195
|
-
|
|
194
|
+
Do not substitute a phase status mutation for `finish`; `finish` preserves
|
|
195
|
+
ownership history and revision safety. A `done` claim outcome leaves unmerged
|
|
196
|
+
execution work `working`. After the authoritative pull request merges, use
|
|
197
|
+
`agency sync --apply` to reconcile the execution unit to `done`.
|
|
196
198
|
|
|
197
199
|
## Create A Pull Request
|
|
198
200
|
|
package/src/cli.test.ts
CHANGED
|
@@ -244,6 +244,10 @@ describe("CLI", () => {
|
|
|
244
244
|
),
|
|
245
245
|
)
|
|
246
246
|
expect(finished.claim).toMatchObject({ state: "finished", outcome: "done" })
|
|
247
|
+
expect(
|
|
248
|
+
parseJson(await runCli(["task", "show", "claimed", "--json"], root)).data
|
|
249
|
+
.status,
|
|
250
|
+
).toBe("working")
|
|
247
251
|
})
|
|
248
252
|
|
|
249
253
|
test("routes graph mutations through structured output", async () => {
|
|
@@ -476,7 +480,7 @@ describe("CLI", () => {
|
|
|
476
480
|
)
|
|
477
481
|
}
|
|
478
482
|
parseJson(
|
|
479
|
-
await runCli(["task", "status", "finished", "
|
|
483
|
+
await runCli(["task", "status", "finished", "dropped", "--json"], root),
|
|
480
484
|
)
|
|
481
485
|
|
|
482
486
|
const human = await runCli(["next"], root)
|
|
@@ -490,9 +494,9 @@ describe("CLI", () => {
|
|
|
490
494
|
expect(result.excluded).toMatchObject([
|
|
491
495
|
{
|
|
492
496
|
key: "task/finished",
|
|
493
|
-
status: "
|
|
497
|
+
status: "dropped",
|
|
494
498
|
terminal: true,
|
|
495
|
-
blockers: [{ kind: "status", reason: "Task status is
|
|
499
|
+
blockers: [{ kind: "status", reason: "Task status is dropped" }],
|
|
496
500
|
},
|
|
497
501
|
])
|
|
498
502
|
|
|
@@ -504,8 +508,8 @@ describe("CLI", () => {
|
|
|
504
508
|
error: {
|
|
505
509
|
code: "EXECUTION_BLOCKED",
|
|
506
510
|
fields: {
|
|
507
|
-
status: "
|
|
508
|
-
blockers: [{ kind: "status", reason: "Task status is
|
|
511
|
+
status: "dropped",
|
|
512
|
+
blockers: [{ kind: "status", reason: "Task status is dropped" }],
|
|
509
513
|
},
|
|
510
514
|
},
|
|
511
515
|
})
|
|
@@ -522,6 +526,7 @@ describe("CLI", () => {
|
|
|
522
526
|
expect(before.files).toMatchObject([
|
|
523
527
|
{ name: "agents", state: "managed" },
|
|
524
528
|
{ name: "opencode", state: "managed" },
|
|
529
|
+
{ name: "opencode-command", state: "managed" },
|
|
525
530
|
])
|
|
526
531
|
|
|
527
532
|
const synced = parseJson(
|
|
@@ -530,6 +535,7 @@ describe("CLI", () => {
|
|
|
530
535
|
expect(synced.files).toMatchObject([
|
|
531
536
|
{ name: "agents", state: "managed", changed: false },
|
|
532
537
|
{ name: "opencode", state: "managed", changed: false },
|
|
538
|
+
{ name: "opencode-command", state: "managed", changed: false },
|
|
533
539
|
])
|
|
534
540
|
})
|
|
535
541
|
|
|
@@ -1131,18 +1137,18 @@ status: open
|
|
|
1131
1137
|
}
|
|
1132
1138
|
|
|
1133
1139
|
parseJson(
|
|
1134
|
-
await runCli(["task", "status", "example", "
|
|
1140
|
+
await runCli(["task", "status", "example", "dropped", "--json"], root),
|
|
1135
1141
|
)
|
|
1136
1142
|
const blocked = await runCli(
|
|
1137
1143
|
["work", "--task", "example", "--runner", "noop"],
|
|
1138
1144
|
root,
|
|
1139
1145
|
)
|
|
1140
1146
|
expect(blocked.exitCode).toBe(1)
|
|
1141
|
-
expect(blocked.stderr).toContain("Task status is
|
|
1147
|
+
expect(blocked.stderr).toContain("Task status is dropped")
|
|
1142
1148
|
expect(
|
|
1143
1149
|
parseJson(await runCli(["task", "show", "example", "--json"], root))
|
|
1144
1150
|
.data.status,
|
|
1145
|
-
).toBe("
|
|
1151
|
+
).toBe("dropped")
|
|
1146
1152
|
|
|
1147
1153
|
const resumedTask = await runCli(
|
|
1148
1154
|
["work", "--task", "example", "--runner", "noop", "--force"],
|
|
@@ -1150,7 +1156,7 @@ status: open
|
|
|
1150
1156
|
)
|
|
1151
1157
|
expect(resumedTask).toMatchObject({ exitCode: 0, stderr: "" })
|
|
1152
1158
|
expect(resumedTask.stdout).toContain(
|
|
1153
|
-
"Reopened task/example from
|
|
1159
|
+
"Reopened task/example from dropped as working",
|
|
1154
1160
|
)
|
|
1155
1161
|
expect(
|
|
1156
1162
|
parseJson(await runCli(["task", "show", "example", "--json"], root))
|
package/src/commands/claim.ts
CHANGED
|
@@ -93,5 +93,6 @@ Release an execution unit owned by the session.
|
|
|
93
93
|
export const finishHelp = `
|
|
94
94
|
Usage: agency finish <task-id> [phase-id] --session-id <id> --revision <sha256> --outcome <done|dropped>
|
|
95
95
|
|
|
96
|
-
Finish
|
|
96
|
+
Finish a claim owned by the session. A done claim outcome leaves unmerged work
|
|
97
|
+
working; agency sync --apply marks the execution unit done after merge.
|
|
97
98
|
`
|
|
@@ -44,6 +44,11 @@ describe("init command", () => {
|
|
|
44
44
|
workbase: expect.objectContaining({ path: ".." }),
|
|
45
45
|
})
|
|
46
46
|
expect(config.permission).toBeUndefined()
|
|
47
|
+
const command = await Bun.file(
|
|
48
|
+
join(root, ".opencode/command/agency.md"),
|
|
49
|
+
).text()
|
|
50
|
+
expect(command).toContain("Workflow: `$1`")
|
|
51
|
+
expect(command).toContain("Optional target: `$2`")
|
|
47
52
|
})
|
|
48
53
|
|
|
49
54
|
test("preserves existing gitignore entries", async () => {
|
|
@@ -36,6 +36,11 @@ 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
|
+
},
|
|
39
44
|
],
|
|
40
45
|
})
|
|
41
46
|
})
|
|
@@ -62,11 +67,15 @@ describe("integration command", () => {
|
|
|
62
67
|
expect(JSON.parse(logs[0]!).files).toMatchObject([
|
|
63
68
|
{ name: "agents", state: "managed", changed: true },
|
|
64
69
|
{ name: "opencode", state: "managed", changed: true },
|
|
70
|
+
{ name: "opencode-command", state: "managed", changed: true },
|
|
65
71
|
])
|
|
66
72
|
expect(await Bun.file(join(root, "AGENTS.md")).exists()).toBe(false)
|
|
67
73
|
expect(await Bun.file(join(root, ".agency/AGENTS.md")).exists()).toBe(true)
|
|
68
74
|
expect(
|
|
69
75
|
await Bun.file(join(root, ".opencode/opencode.jsonc")).exists(),
|
|
70
76
|
).toBe(true)
|
|
77
|
+
expect(
|
|
78
|
+
await Bun.file(join(root, ".opencode/command/agency.md")).exists(),
|
|
79
|
+
).toBe(true)
|
|
71
80
|
})
|
|
72
81
|
})
|
|
@@ -54,8 +54,8 @@ export const help = `
|
|
|
54
54
|
Usage: agency integration <subcommand>
|
|
55
55
|
|
|
56
56
|
Inspect or explicitly synchronize managed agent integration files. OpenCode
|
|
57
|
-
launches load the managed
|
|
58
|
-
|
|
57
|
+
launches load the managed instructions and project config at runtime, and expose
|
|
58
|
+
the managed /agency command, without changing Agency write authority.
|
|
59
59
|
|
|
60
60
|
Subcommands:
|
|
61
61
|
status Report file state, access diagnostics, and safe remediation
|
package/src/commands/phase.ts
CHANGED
|
@@ -275,7 +275,7 @@ Subcommands:
|
|
|
275
275
|
list <task> List task phases
|
|
276
276
|
show <task> <phase> Show a phase
|
|
277
277
|
status <task> <phase> <status>
|
|
278
|
-
Set open, working,
|
|
278
|
+
Set open, working, or dropped
|
|
279
279
|
update <task> <phase> Update phase metadata
|
|
280
280
|
rename <task> <phase> <new-id>
|
|
281
281
|
Rename a phase and update dependencies
|
|
@@ -239,13 +239,13 @@ describe("task and phase command JSON output", () => {
|
|
|
239
239
|
runTestEffect(
|
|
240
240
|
phase({
|
|
241
241
|
subcommand: "status",
|
|
242
|
-
args: ["multi", "first", "
|
|
242
|
+
args: ["multi", "first", "working"],
|
|
243
243
|
cwd: root,
|
|
244
244
|
json: true,
|
|
245
245
|
}),
|
|
246
246
|
),
|
|
247
247
|
)
|
|
248
|
-
expect(JSON.parse(phaseLogs[0]!).data.status).toBe("
|
|
248
|
+
expect(JSON.parse(phaseLogs[0]!).data.status).toBe("working")
|
|
249
249
|
|
|
250
250
|
await runTestEffect(
|
|
251
251
|
task({
|
|
@@ -263,13 +263,13 @@ describe("task and phase command JSON output", () => {
|
|
|
263
263
|
runTestEffect(
|
|
264
264
|
task({
|
|
265
265
|
subcommand: "status",
|
|
266
|
-
args: ["single-status", "
|
|
266
|
+
args: ["single-status", "working"],
|
|
267
267
|
cwd: root,
|
|
268
268
|
json: true,
|
|
269
269
|
}),
|
|
270
270
|
),
|
|
271
271
|
)
|
|
272
|
-
expect(JSON.parse(taskLogs[0]!).data.status).toBe("
|
|
272
|
+
expect(JSON.parse(taskLogs[0]!).data.status).toBe("working")
|
|
273
273
|
})
|
|
274
274
|
|
|
275
275
|
test("converts a single-phase task with an explicit first phase ID", async () => {
|
package/src/commands/task.ts
CHANGED
|
@@ -423,7 +423,7 @@ Subcommands:
|
|
|
423
423
|
create <id> Create a task without prompting
|
|
424
424
|
list List tasks
|
|
425
425
|
show <id> Show a task
|
|
426
|
-
status <id> <status> Set open, working,
|
|
426
|
+
status <id> <status> Set open, working, or dropped
|
|
427
427
|
update <id> Update task metadata
|
|
428
428
|
rename <id> <new-id> Rename a task and update graph references
|
|
429
429
|
move <id> Move a task with --epic or --no-epic
|
package/src/readiness.ts
CHANGED
|
@@ -5,9 +5,9 @@ export interface ReadinessBlocker {
|
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
export const WORK_STATUS_TRANSITIONS = {
|
|
8
|
-
open: ["open", "working", "delegated", "
|
|
9
|
-
working: ["open", "working", "delegated", "
|
|
10
|
-
delegated: ["open", "working", "delegated", "
|
|
8
|
+
open: ["open", "working", "delegated", "dropped"],
|
|
9
|
+
working: ["open", "working", "delegated", "dropped"],
|
|
10
|
+
delegated: ["open", "working", "delegated", "dropped"],
|
|
11
11
|
done: ["open", "done"],
|
|
12
12
|
dropped: ["open", "dropped"],
|
|
13
13
|
} as const satisfies Record<WorkStatus, readonly WorkStatus[]>
|
|
@@ -139,7 +139,7 @@ describe("claim service", () => {
|
|
|
139
139
|
),
|
|
140
140
|
),
|
|
141
141
|
)
|
|
142
|
-
expect(finished.data.status).toBe("
|
|
142
|
+
expect(finished.data.status).toBe("working")
|
|
143
143
|
expect(finished.claim).toMatchObject({
|
|
144
144
|
state: "finished",
|
|
145
145
|
finishedAt: "2026-07-17T12:45:00.000Z",
|
|
@@ -538,7 +538,11 @@ export class ClaimService extends Effect.Service<ClaimService>()(
|
|
|
538
538
|
outcome: input.outcome,
|
|
539
539
|
}
|
|
540
540
|
return {
|
|
541
|
-
data: {
|
|
541
|
+
data: {
|
|
542
|
+
...data,
|
|
543
|
+
status: input.outcome === "done" ? "working" : "dropped",
|
|
544
|
+
claim,
|
|
545
|
+
},
|
|
542
546
|
claim,
|
|
543
547
|
}
|
|
544
548
|
},
|
|
@@ -5,6 +5,10 @@ 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"
|
|
8
|
+
import {
|
|
9
|
+
canUpdateManagedWorkbaseOpencodeCommand,
|
|
10
|
+
managedWorkbaseOpencodeCommand,
|
|
11
|
+
} from "../workbase/opencode-command-file"
|
|
8
12
|
import { managedWorkbaseOpencode } from "../workbase/opencode-file"
|
|
9
13
|
import { IntegrationService } from "./IntegrationService"
|
|
10
14
|
|
|
@@ -46,14 +50,21 @@ describe("IntegrationService", () => {
|
|
|
46
50
|
expect((await status(root)).files.map(({ state }) => state)).toEqual([
|
|
47
51
|
"missing",
|
|
48
52
|
"missing",
|
|
53
|
+
"missing",
|
|
49
54
|
])
|
|
50
55
|
expect(await Bun.file(join(root, ".agency/AGENTS.md")).exists()).toBe(false)
|
|
51
56
|
|
|
52
57
|
await write(root, ".agency/AGENTS.md", managedWorkbaseAgents)
|
|
53
58
|
await write(root, ".opencode/opencode.jsonc", managedWorkbaseOpencode)
|
|
59
|
+
await write(
|
|
60
|
+
root,
|
|
61
|
+
".opencode/command/agency.md",
|
|
62
|
+
managedWorkbaseOpencodeCommand,
|
|
63
|
+
)
|
|
54
64
|
expect((await status(root)).files.map(({ state }) => state)).toEqual([
|
|
55
65
|
"managed",
|
|
56
66
|
"managed",
|
|
67
|
+
"managed",
|
|
57
68
|
])
|
|
58
69
|
})
|
|
59
70
|
|
|
@@ -68,9 +79,40 @@ describe("IntegrationService", () => {
|
|
|
68
79
|
expect((await status(root)).files.map(({ state }) => state)).toEqual([
|
|
69
80
|
"customized",
|
|
70
81
|
"drifted",
|
|
82
|
+
"missing",
|
|
71
83
|
])
|
|
72
84
|
})
|
|
73
85
|
|
|
86
|
+
test("generates a positional OpenCode command for Agency workflows", () => {
|
|
87
|
+
expect(managedWorkbaseOpencodeCommand).toContain(
|
|
88
|
+
"description: Operate Agency work",
|
|
89
|
+
)
|
|
90
|
+
expect(managedWorkbaseOpencodeCommand).toContain("Workflow: `$1`")
|
|
91
|
+
expect(managedWorkbaseOpencodeCommand).toContain("Optional target: `$2`")
|
|
92
|
+
expect(managedWorkbaseOpencodeCommand).toContain(
|
|
93
|
+
"Complete request: `$ARGUMENTS`",
|
|
94
|
+
)
|
|
95
|
+
for (const workflow of [
|
|
96
|
+
"start",
|
|
97
|
+
"status",
|
|
98
|
+
"next",
|
|
99
|
+
"validate",
|
|
100
|
+
"finish",
|
|
101
|
+
"help",
|
|
102
|
+
]) {
|
|
103
|
+
expect(managedWorkbaseOpencodeCommand).toContain(`- \`${workflow}\`:`)
|
|
104
|
+
}
|
|
105
|
+
expect(managedWorkbaseOpencodeCommand).toContain("Never run `agency work`")
|
|
106
|
+
expect(
|
|
107
|
+
canUpdateManagedWorkbaseOpencodeCommand(managedWorkbaseOpencodeCommand),
|
|
108
|
+
).toBe(true)
|
|
109
|
+
expect(
|
|
110
|
+
canUpdateManagedWorkbaseOpencodeCommand(
|
|
111
|
+
managedWorkbaseOpencodeCommand.replace("Workflow: `$1`", "Workflow"),
|
|
112
|
+
),
|
|
113
|
+
).toBe(false)
|
|
114
|
+
})
|
|
115
|
+
|
|
74
116
|
test("generates context-first safety and execution closeout guidance", () => {
|
|
75
117
|
const body = managedBody(managedWorkbaseAgents)
|
|
76
118
|
|
|
@@ -80,18 +122,20 @@ describe("IntegrationService", () => {
|
|
|
80
122
|
expect(body).toContain("External orchestrators claim before launching")
|
|
81
123
|
expect(body).toContain("Run `agency validate`")
|
|
82
124
|
expect(body).toContain("only with explicit user intent")
|
|
83
|
-
expect(body).toContain("An execution unit
|
|
125
|
+
expect(body).toContain("An execution unit remains `working`")
|
|
84
126
|
expect(body).toContain("It becomes `done`")
|
|
85
|
-
expect(body).toContain("
|
|
127
|
+
expect(body).toContain("Do not mark committed")
|
|
86
128
|
expect(body).toContain("creating or updating a PR")
|
|
87
129
|
expect(body).toContain("marking it ready")
|
|
88
130
|
expect(body).toMatch(/completing\s+a refinement loop/)
|
|
89
131
|
expect(body).toContain("pausing or handing off")
|
|
90
|
-
expect(body).toContain("`agency
|
|
132
|
+
expect(body).toContain("`agency finish`")
|
|
133
|
+
expect(body).toContain("`agency sync --apply`")
|
|
91
134
|
expect(body).toContain("`TASK.md` or `PHASE.md`")
|
|
92
135
|
expect(body).toContain("PR state, current head, diff summary")
|
|
93
136
|
expect(body).toContain("Run `agency validate` before reporting completion")
|
|
94
137
|
expect(body).toContain("agency integration status")
|
|
138
|
+
expect(body).toContain(".opencode/command/agency.md")
|
|
95
139
|
})
|
|
96
140
|
|
|
97
141
|
test("grants OpenCode access to the complete workbase", () => {
|
|
@@ -166,6 +210,7 @@ describe("IntegrationService", () => {
|
|
|
166
210
|
expect(first.files).toMatchObject([
|
|
167
211
|
{ name: "agents", state: "customized", changed: false },
|
|
168
212
|
{ name: "opencode", state: "managed", changed: true },
|
|
213
|
+
{ name: "opencode-command", state: "managed", changed: true },
|
|
169
214
|
])
|
|
170
215
|
expect(await Bun.file(join(root, "AGENTS.md")).text()).toBe(
|
|
171
216
|
customRootAgents,
|
|
@@ -176,6 +221,9 @@ describe("IntegrationService", () => {
|
|
|
176
221
|
expect(await Bun.file(join(root, ".opencode/opencode.jsonc")).text()).toBe(
|
|
177
222
|
managedWorkbaseOpencode,
|
|
178
223
|
)
|
|
224
|
+
expect(
|
|
225
|
+
await Bun.file(join(root, ".opencode/command/agency.md")).text(),
|
|
226
|
+
).toBe(managedWorkbaseOpencodeCommand)
|
|
179
227
|
|
|
180
228
|
await unlink(join(root, ".agency/AGENTS.md"))
|
|
181
229
|
const second = await sync(root)
|
|
@@ -188,6 +236,34 @@ describe("IntegrationService", () => {
|
|
|
188
236
|
)
|
|
189
237
|
})
|
|
190
238
|
|
|
239
|
+
test("preserves user-owned OpenCode commands at either supported path", async () => {
|
|
240
|
+
const custom = "---\ndescription: Custom Agency command\n---\n\nCustom.\n"
|
|
241
|
+
await write(root, ".opencode/commands/agency.md", custom)
|
|
242
|
+
|
|
243
|
+
let result = await sync(root)
|
|
244
|
+
expect(result.files[2]).toMatchObject({
|
|
245
|
+
name: "opencode-command",
|
|
246
|
+
path: join(root, ".opencode/commands/agency.md"),
|
|
247
|
+
state: "customized",
|
|
248
|
+
changed: false,
|
|
249
|
+
})
|
|
250
|
+
expect(
|
|
251
|
+
await Bun.file(join(root, ".opencode/command/agency.md")).exists(),
|
|
252
|
+
).toBe(false)
|
|
253
|
+
|
|
254
|
+
await unlink(join(root, ".opencode/commands/agency.md"))
|
|
255
|
+
await write(root, ".opencode/command/agency.md", custom)
|
|
256
|
+
result = await sync(root)
|
|
257
|
+
expect(result.files[2]).toMatchObject({
|
|
258
|
+
path: join(root, ".opencode/command/agency.md"),
|
|
259
|
+
state: "customized",
|
|
260
|
+
changed: false,
|
|
261
|
+
})
|
|
262
|
+
expect(
|
|
263
|
+
await Bun.file(join(root, ".opencode/command/agency.md")).text(),
|
|
264
|
+
).toBe(custom)
|
|
265
|
+
})
|
|
266
|
+
|
|
191
267
|
test("migrates checksum-valid root instructions", async () => {
|
|
192
268
|
await write(root, "AGENTS.md", managedWorkbaseAgents)
|
|
193
269
|
|
|
@@ -10,11 +10,15 @@ import {
|
|
|
10
10
|
canUpdateManagedWorkbaseOpencode,
|
|
11
11
|
managedWorkbaseOpencode,
|
|
12
12
|
} from "../workbase/opencode-file"
|
|
13
|
+
import {
|
|
14
|
+
canUpdateManagedWorkbaseOpencodeCommand,
|
|
15
|
+
managedWorkbaseOpencodeCommand,
|
|
16
|
+
} from "../workbase/opencode-command-file"
|
|
13
17
|
|
|
14
18
|
type IntegrationFileState = "managed" | "customized" | "missing" | "drifted"
|
|
15
19
|
|
|
16
20
|
interface IntegrationFileStatus {
|
|
17
|
-
readonly name: "agents" | "opencode"
|
|
21
|
+
readonly name: "agents" | "opencode" | "opencode-command"
|
|
18
22
|
readonly path: string
|
|
19
23
|
readonly state: IntegrationFileState
|
|
20
24
|
readonly diagnostic: string
|
|
@@ -52,6 +56,25 @@ const describe = (
|
|
|
52
56
|
"Run 'agency integration sync' to install Agency instructions and whole-workbase OpenCode access.",
|
|
53
57
|
}
|
|
54
58
|
}
|
|
59
|
+
if (name === "opencode-command") {
|
|
60
|
+
return state === "managed"
|
|
61
|
+
? {
|
|
62
|
+
diagnostic: "Agency's managed OpenCode /agency command is current.",
|
|
63
|
+
remediation: null,
|
|
64
|
+
}
|
|
65
|
+
: state === "customized"
|
|
66
|
+
? {
|
|
67
|
+
diagnostic:
|
|
68
|
+
"A user-owned OpenCode /agency command is present and was preserved.",
|
|
69
|
+
remediation: null,
|
|
70
|
+
}
|
|
71
|
+
: {
|
|
72
|
+
diagnostic:
|
|
73
|
+
"The managed OpenCode /agency command needs synchronization.",
|
|
74
|
+
remediation:
|
|
75
|
+
"Run 'agency integration sync' to install the managed /agency command.",
|
|
76
|
+
}
|
|
77
|
+
}
|
|
55
78
|
|
|
56
79
|
return state === "missing" || state === "drifted"
|
|
57
80
|
? {
|
|
@@ -98,6 +121,8 @@ const inspect = (root: string) =>
|
|
|
98
121
|
const opencodeDirectory = join(root, ".opencode")
|
|
99
122
|
const opencodePath = join(opencodeDirectory, "opencode.jsonc")
|
|
100
123
|
const opencodeJsonPath = join(opencodeDirectory, "opencode.json")
|
|
124
|
+
const commandPath = join(opencodeDirectory, "command", "agency.md")
|
|
125
|
+
const pluralCommandPath = join(opencodeDirectory, "commands", "agency.md")
|
|
101
126
|
const files: IntegrationFileStatus[] = []
|
|
102
127
|
|
|
103
128
|
files.push(
|
|
@@ -135,6 +160,29 @@ const inspect = (root: string) =>
|
|
|
135
160
|
files.push(fileStatus("opencode", opencodePath, "missing"))
|
|
136
161
|
}
|
|
137
162
|
|
|
163
|
+
if ((yield* fs.readSymlinkTarget(commandPath)) !== null) {
|
|
164
|
+
files.push(fileStatus("opencode-command", commandPath, "customized"))
|
|
165
|
+
} else if (
|
|
166
|
+
(yield* fs.readSymlinkTarget(pluralCommandPath)) !== null ||
|
|
167
|
+
(yield* fs.exists(pluralCommandPath))
|
|
168
|
+
) {
|
|
169
|
+
files.push(
|
|
170
|
+
fileStatus("opencode-command", pluralCommandPath, "customized"),
|
|
171
|
+
)
|
|
172
|
+
} else if (yield* fs.exists(commandPath)) {
|
|
173
|
+
files.push(
|
|
174
|
+
classify(
|
|
175
|
+
"opencode-command",
|
|
176
|
+
commandPath,
|
|
177
|
+
yield* fs.readFile(commandPath),
|
|
178
|
+
managedWorkbaseOpencodeCommand,
|
|
179
|
+
canUpdateManagedWorkbaseOpencodeCommand,
|
|
180
|
+
),
|
|
181
|
+
)
|
|
182
|
+
} else {
|
|
183
|
+
files.push(fileStatus("opencode-command", commandPath, "missing"))
|
|
184
|
+
}
|
|
185
|
+
|
|
138
186
|
return files
|
|
139
187
|
})
|
|
140
188
|
|
|
@@ -181,9 +229,12 @@ export class IntegrationService extends Effect.Service<IntegrationService>()(
|
|
|
181
229
|
if (status.name === "agents") {
|
|
182
230
|
yield* fs.createDirectory(join(root, ".agency"))
|
|
183
231
|
yield* fs.writeFile(status.path, managedWorkbaseAgents)
|
|
184
|
-
} else {
|
|
232
|
+
} else if (status.name === "opencode") {
|
|
185
233
|
yield* fs.createDirectory(join(root, ".opencode"))
|
|
186
234
|
yield* fs.writeFile(status.path, managedWorkbaseOpencode)
|
|
235
|
+
} else {
|
|
236
|
+
yield* fs.createDirectory(join(root, ".opencode", "command"))
|
|
237
|
+
yield* fs.writeFile(status.path, managedWorkbaseOpencodeCommand)
|
|
187
238
|
}
|
|
188
239
|
}
|
|
189
240
|
files.push({
|
|
@@ -487,6 +487,12 @@ export class PhaseService extends Effect.Service<PhaseService>()(
|
|
|
487
487
|
})
|
|
488
488
|
}
|
|
489
489
|
if (!canTransitionStatus(record.data.status, validStatus)) {
|
|
490
|
+
if (validStatus === "done") {
|
|
491
|
+
return yield* new PhaseError({
|
|
492
|
+
message:
|
|
493
|
+
"Work becomes done after its authoritative pull request is merged; run 'agency sync --apply'",
|
|
494
|
+
})
|
|
495
|
+
}
|
|
490
496
|
return yield* new PhaseError({
|
|
491
497
|
message: `Cannot transition phase '${id}' from ${record.data.status} to ${validStatus}; reopen it first`,
|
|
492
498
|
})
|
|
@@ -263,11 +263,13 @@ process.exit(${exitCode})
|
|
|
263
263
|
await createTask()
|
|
264
264
|
await runTestEffect(
|
|
265
265
|
TaskService.pipe(
|
|
266
|
-
Effect.flatMap((service) =>
|
|
266
|
+
Effect.flatMap((service) =>
|
|
267
|
+
service.setStatus("example", "dropped", root),
|
|
268
|
+
),
|
|
267
269
|
),
|
|
268
270
|
)
|
|
269
271
|
|
|
270
|
-
await expect(createPullRequest()).rejects.toThrow("Task status is
|
|
272
|
+
await expect(createPullRequest()).rejects.toThrow("Task status is dropped")
|
|
271
273
|
expect(
|
|
272
274
|
await Bun.file(join(root, "tasks/example/code/agency")).exists(),
|
|
273
275
|
).toBe(false)
|
|
@@ -390,6 +390,99 @@ process.stdout.write(${JSON.stringify(JSON.stringify(record))})
|
|
|
390
390
|
expect("pr" in task.data && task.data.pr).toEqual(record)
|
|
391
391
|
})
|
|
392
392
|
|
|
393
|
+
test("marks a successfully finished claim done only after merge", async () => {
|
|
394
|
+
await runTestEffect(
|
|
395
|
+
TaskService.pipe(
|
|
396
|
+
Effect.flatMap((service) =>
|
|
397
|
+
service.create(
|
|
398
|
+
{
|
|
399
|
+
id: "finished-claim",
|
|
400
|
+
ticketUrl: null,
|
|
401
|
+
repo: "agency",
|
|
402
|
+
branch: "feat/example",
|
|
403
|
+
base: "main",
|
|
404
|
+
},
|
|
405
|
+
root,
|
|
406
|
+
),
|
|
407
|
+
),
|
|
408
|
+
),
|
|
409
|
+
)
|
|
410
|
+
await runTestEffect(
|
|
411
|
+
WorktreeService.pipe(
|
|
412
|
+
Effect.flatMap((service) =>
|
|
413
|
+
service.materialize("finished-claim", undefined, root),
|
|
414
|
+
),
|
|
415
|
+
),
|
|
416
|
+
)
|
|
417
|
+
await git(
|
|
418
|
+
["remote", "set-url", "origin", "git@github.com:example/agency.git"],
|
|
419
|
+
join(root, "repos/agency"),
|
|
420
|
+
)
|
|
421
|
+
const initial = await runTestEffect(
|
|
422
|
+
ClaimService.pipe(
|
|
423
|
+
Effect.flatMap((service) =>
|
|
424
|
+
service.inspect("finished-claim", undefined, root),
|
|
425
|
+
),
|
|
426
|
+
),
|
|
427
|
+
)
|
|
428
|
+
const claimed = await runTestEffect(
|
|
429
|
+
ClaimService.pipe(
|
|
430
|
+
Effect.flatMap((service) =>
|
|
431
|
+
service.claim(
|
|
432
|
+
{
|
|
433
|
+
taskId: "finished-claim",
|
|
434
|
+
claimant: "orchestrator",
|
|
435
|
+
runner: "agent",
|
|
436
|
+
sessionId: "session-1",
|
|
437
|
+
revision: initial.revision,
|
|
438
|
+
},
|
|
439
|
+
root,
|
|
440
|
+
),
|
|
441
|
+
),
|
|
442
|
+
),
|
|
443
|
+
)
|
|
444
|
+
const finished = await runTestEffect(
|
|
445
|
+
ClaimService.pipe(
|
|
446
|
+
Effect.flatMap((service) =>
|
|
447
|
+
service.finish(
|
|
448
|
+
{
|
|
449
|
+
taskId: "finished-claim",
|
|
450
|
+
sessionId: "session-1",
|
|
451
|
+
revision: claimed.revision,
|
|
452
|
+
outcome: "done",
|
|
453
|
+
},
|
|
454
|
+
root,
|
|
455
|
+
),
|
|
456
|
+
),
|
|
457
|
+
),
|
|
458
|
+
)
|
|
459
|
+
expect(finished.data).toMatchObject({
|
|
460
|
+
status: "working",
|
|
461
|
+
claim: { state: "finished", outcome: "done" },
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
const synced = await runTestEffect(
|
|
465
|
+
SyncService.pipe(
|
|
466
|
+
Effect.flatMap((service) =>
|
|
467
|
+
service.reconcile({ cwd: root, apply: true }),
|
|
468
|
+
),
|
|
469
|
+
),
|
|
470
|
+
)
|
|
471
|
+
expect(synced.changes.map((change) => change.kind)).toContain("mark-done")
|
|
472
|
+
expect(
|
|
473
|
+
await runTestEffect(
|
|
474
|
+
TaskService.pipe(
|
|
475
|
+
Effect.flatMap((service) => service.show("finished-claim", root)),
|
|
476
|
+
),
|
|
477
|
+
),
|
|
478
|
+
).toMatchObject({
|
|
479
|
+
data: {
|
|
480
|
+
status: "done",
|
|
481
|
+
claim: { state: "finished", outcome: "done" },
|
|
482
|
+
},
|
|
483
|
+
})
|
|
484
|
+
})
|
|
485
|
+
|
|
393
486
|
test("materializes missing workspaces but leaves branch conflicts unresolved", async () => {
|
|
394
487
|
for (const [id, branch] of [
|
|
395
488
|
["missing", "feat/missing"],
|
|
@@ -318,22 +318,23 @@ describe("task and phase services", () => {
|
|
|
318
318
|
),
|
|
319
319
|
),
|
|
320
320
|
).rejects.toThrow("Delegation requires explicit ownership")
|
|
321
|
-
await runTestEffect(
|
|
322
|
-
TaskService.pipe(
|
|
323
|
-
Effect.flatMap((service) =>
|
|
324
|
-
service.setStatus("single-status", "done", root),
|
|
325
|
-
),
|
|
326
|
-
),
|
|
327
|
-
)
|
|
328
321
|
await expect(
|
|
329
322
|
runTestEffect(
|
|
330
323
|
TaskService.pipe(
|
|
331
324
|
Effect.flatMap((service) =>
|
|
332
|
-
service.setStatus("single-status", "
|
|
325
|
+
service.setStatus("single-status", "done", root),
|
|
333
326
|
),
|
|
334
327
|
),
|
|
335
328
|
),
|
|
336
|
-
).rejects.toThrow("
|
|
329
|
+
).rejects.toThrow("authoritative pull request is merged")
|
|
330
|
+
const droppedTask = await runTestEffect(
|
|
331
|
+
TaskService.pipe(
|
|
332
|
+
Effect.flatMap((service) =>
|
|
333
|
+
service.setStatus("single-status", "dropped", root),
|
|
334
|
+
),
|
|
335
|
+
),
|
|
336
|
+
)
|
|
337
|
+
expect(droppedTask.data.status).toBe("dropped")
|
|
337
338
|
|
|
338
339
|
await runTestEffect(
|
|
339
340
|
TaskService.pipe(
|
|
@@ -389,7 +390,7 @@ describe("task and phase services", () => {
|
|
|
389
390
|
),
|
|
390
391
|
),
|
|
391
392
|
),
|
|
392
|
-
).rejects.toThrow("
|
|
393
|
+
).rejects.toThrow("authoritative pull request is merged")
|
|
393
394
|
await runTestEffect(
|
|
394
395
|
PhaseService.pipe(
|
|
395
396
|
Effect.flatMap((service) =>
|
|
@@ -397,17 +398,15 @@ describe("task and phase services", () => {
|
|
|
397
398
|
),
|
|
398
399
|
),
|
|
399
400
|
)
|
|
400
|
-
expect(
|
|
401
|
-
(
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
service.setStatus("multi-status", "implementation", "done", root),
|
|
406
|
-
),
|
|
401
|
+
await expect(
|
|
402
|
+
runTestEffect(
|
|
403
|
+
PhaseService.pipe(
|
|
404
|
+
Effect.flatMap((service) =>
|
|
405
|
+
service.setStatus("multi-status", "implementation", "done", root),
|
|
407
406
|
),
|
|
408
|
-
)
|
|
409
|
-
)
|
|
410
|
-
).
|
|
407
|
+
),
|
|
408
|
+
),
|
|
409
|
+
).rejects.toThrow("authoritative pull request is merged")
|
|
411
410
|
await expect(
|
|
412
411
|
runTestEffect(
|
|
413
412
|
TaskService.pipe(
|
|
@@ -270,6 +270,12 @@ export class TaskService extends Effect.Service<TaskService>()("TaskService", {
|
|
|
270
270
|
})
|
|
271
271
|
}
|
|
272
272
|
if (!canTransitionStatus(record.data.status, validStatus)) {
|
|
273
|
+
if (validStatus === "done") {
|
|
274
|
+
return yield* new TaskError({
|
|
275
|
+
message:
|
|
276
|
+
"Work becomes done after its authoritative pull request is merged; run 'agency sync --apply'",
|
|
277
|
+
})
|
|
278
|
+
}
|
|
273
279
|
return yield* new TaskError({
|
|
274
280
|
message: `Cannot transition task '${id}' from ${record.data.status} to ${validStatus}; reopen it first`,
|
|
275
281
|
})
|
|
@@ -0,0 +1,48 @@
|
|
|
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]`.
|
package/src/workbase/AGENTS.md
CHANGED
|
@@ -48,17 +48,19 @@ reason to edit `agency.json` or `repos/` by hand.
|
|
|
48
48
|
|
|
49
49
|
## Closeout
|
|
50
50
|
|
|
51
|
-
An execution unit
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
An execution unit remains `working` after implementation is committed and while
|
|
52
|
+
its pull request is open. It becomes `done` only after its authoritative pull
|
|
53
|
+
request is merged and Agency reconciles that state. Do not mark committed or
|
|
54
|
+
review-ready work `done` manually.
|
|
55
55
|
|
|
56
56
|
At each closeout trigger (creating or updating a PR, marking it ready, completing
|
|
57
57
|
a refinement loop, or pausing or handing off completed implementation work):
|
|
58
58
|
|
|
59
|
-
- Finish an active claim with the current revision via `agency finish
|
|
60
|
-
|
|
61
|
-
execution unit
|
|
59
|
+
- Finish an active claim with the current revision via `agency finish`; a
|
|
60
|
+
successful claim outcome leaves unmerged work `working`. For unclaimed work,
|
|
61
|
+
keep the execution unit `working` through review and merge.
|
|
62
|
+
- After merge, run `agency sync --apply` to reconcile the execution unit to
|
|
63
|
+
`done`.
|
|
62
64
|
- Refresh durable delivery context in `TASK.md` or `PHASE.md`, including recorded
|
|
63
65
|
PR state, current head, diff summary, and verification results after later
|
|
64
66
|
pushes when those details are maintained there.
|
|
@@ -69,6 +71,8 @@ a refinement loop, or pausing or handing off completed implementation work):
|
|
|
69
71
|
`agency integration status` reports `managed`, `drifted`, `customized`, or
|
|
70
72
|
`missing` generated files. Agency keeps these instructions in
|
|
71
73
|
`.agency/AGENTS.md`, and its managed OpenCode config loads them automatically.
|
|
74
|
+
It also installs `.opencode/command/agency.md`, which provides safe `/agency`
|
|
75
|
+
workflows for active OpenCode sessions.
|
|
72
76
|
The workbase-root `AGENTS.md`, when present, belongs entirely to the workbase
|
|
73
77
|
owner and composes with these instructions through OpenCode's normal discovery.
|
|
74
78
|
`agency integration sync` updates only missing or checksum-safe drifted managed
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
}
|