@markjaquith/agency 3.0.0 → 3.1.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 +15 -13
- package/cli-main.ts +11 -2
- package/package.json +1 -1
- package/src/cli-parser.test.ts +12 -1
- package/src/cli-parser.ts +21 -15
- package/src/cli.test.ts +4 -21
- package/src/commands/archive.test.ts +73 -1
- package/src/commands/archive.ts +18 -7
- package/src/commands/init.test.ts +1 -5
- package/src/commands/worktree.ts +2 -0
- package/src/services/ArchiveService.ts +59 -1
- package/src/services/IntegrationService.test.ts +10 -18
- package/src/services/WorktreeLock.test.ts +81 -2
- package/src/services/WorktreeLock.ts +42 -5
- package/src/services/WorktreeService.ts +6 -0
- package/src/workbase/AGENTS.md +2 -2
- package/src/workbase/opencode-file.ts +1 -13
package/README.md
CHANGED
|
@@ -66,7 +66,7 @@ workbase/
|
|
|
66
66
|
.agency/
|
|
67
67
|
AGENTS.md # managed Agency instructions
|
|
68
68
|
.opencode/
|
|
69
|
-
opencode.jsonc # managed
|
|
69
|
+
opencode.jsonc # managed planning agent, instructions, and reference
|
|
70
70
|
tui.jsonc # managed TUI plugin registration
|
|
71
71
|
plugins/agency-repository-skills.ts # managed workbase access and checkout skills
|
|
72
72
|
tui/agency-debug.ts # managed /agency-debug TUI diagnostic
|
|
@@ -116,17 +116,14 @@ Agency-managed root `AGENTS.md` to `.agency/AGENTS.md` once the OpenCode config
|
|
|
116
116
|
can load the hidden file. A customized root file, including a symlink, is
|
|
117
117
|
preserved as user-owned content.
|
|
118
118
|
|
|
119
|
-
The OpenCode config
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
operation.
|
|
128
|
-
When the subagent launches work in another agent, it verifies that the agent
|
|
129
|
-
started and returns without waiting for the task to finish.
|
|
119
|
+
The OpenCode config loads Agency's hidden instructions in addition to any
|
|
120
|
+
user-owned root `AGENTS.md`, advertises the complete workbase as one portable
|
|
121
|
+
reference, and replaces the built-in Plan agent with `agency-plan`. That
|
|
122
|
+
planning agent can update `TASK.md`, `PHASE.md`, and `EPIC.md`, inspect the
|
|
123
|
+
workbase through read-only Agency commands, and use explicit Agency CLI
|
|
124
|
+
permissions to create or update planning structure. Its normal research tools
|
|
125
|
+
and the complete Agency CLI remain available; managed Agency instructions and
|
|
126
|
+
reported authority govern each operation.
|
|
130
127
|
The TUI-only `/agency-debug` command reports TUI companion initialization and
|
|
131
128
|
whether the server plugin registered writable-checkout skills. It uses a native
|
|
132
129
|
toast and does not submit a prompt to an LLM. When no writable checkout skill
|
|
@@ -995,12 +992,17 @@ agency archive epic <epic-id> [--dry-run] [--json]
|
|
|
995
992
|
agency archive task <task-id> [--dry-run] [--json]
|
|
996
993
|
agency archive tasks [--dry-run] [--json]
|
|
997
994
|
agency archive phase <task-id> <phase-id> [--dry-run] [--json]
|
|
995
|
+
agency archive <path> [--dry-run] [--json]
|
|
998
996
|
agency restore epic <epic-id> [--dry-run] [--json]
|
|
999
997
|
agency restore task <task-id> [--dry-run] [--json]
|
|
1000
998
|
agency restore phase <task-id> <phase-id> [--dry-run] [--json]
|
|
1001
999
|
```
|
|
1002
1000
|
|
|
1003
|
-
|
|
1001
|
+
An existing path within an active epic or task infers that work item, so
|
|
1002
|
+
`agency archive .` works from its directory. Collection roots are ambiguous,
|
|
1003
|
+
phase paths require the explicit `archive phase` form, and paths outside active
|
|
1004
|
+
epic or task trees are rejected. Archived work keeps its hierarchy under
|
|
1005
|
+
`archive/`. Epic archiving includes its
|
|
1004
1006
|
listed tasks. A task can be archived only when its effective status is terminal
|
|
1005
1007
|
(`done` or `dropped`). Multi-phase task status is derived from its phases, every
|
|
1006
1008
|
phase must be terminal, and a task with no phases is not eligible.
|
package/cli-main.ts
CHANGED
|
@@ -396,10 +396,18 @@ const commands: Record<string, Command> = {
|
|
|
396
396
|
console.log(archiveHelp)
|
|
397
397
|
return
|
|
398
398
|
}
|
|
399
|
+
const explicitType = [
|
|
400
|
+
"list",
|
|
401
|
+
"show",
|
|
402
|
+
"epic",
|
|
403
|
+
"task",
|
|
404
|
+
"tasks",
|
|
405
|
+
"phase",
|
|
406
|
+
].includes(args[0] ?? "")
|
|
399
407
|
await runCommand(
|
|
400
408
|
archive({
|
|
401
|
-
type: args[0],
|
|
402
|
-
args: args.slice(1),
|
|
409
|
+
type: explicitType ? args[0] : undefined,
|
|
410
|
+
args: explicitType ? args.slice(1) : args,
|
|
403
411
|
json: options.json,
|
|
404
412
|
dryRun: options["dry-run"],
|
|
405
413
|
kinds: options.kind,
|
|
@@ -610,6 +618,7 @@ const commands: Record<string, Command> = {
|
|
|
610
618
|
subcommand: args[0],
|
|
611
619
|
args: args.slice(1),
|
|
612
620
|
dryRun: options["dry-run"],
|
|
621
|
+
force: options.force,
|
|
613
622
|
json: options.json,
|
|
614
623
|
silent: options.silent,
|
|
615
624
|
verbose: options.verbose,
|
package/package.json
CHANGED
package/src/cli-parser.test.ts
CHANGED
|
@@ -478,6 +478,11 @@ describe("strict CLI parsing", () => {
|
|
|
478
478
|
})
|
|
479
479
|
|
|
480
480
|
test("accepts archive dry-run", () => {
|
|
481
|
+
expect(parseCli(["archive", ".", "--dry-run"])).toMatchObject({
|
|
482
|
+
commandName: "archive",
|
|
483
|
+
args: ["."],
|
|
484
|
+
values: { "dry-run": true },
|
|
485
|
+
})
|
|
481
486
|
expect(parseCli(["archive", "task", "example", "--dry-run"])).toMatchObject(
|
|
482
487
|
{
|
|
483
488
|
commandName: "archive",
|
|
@@ -659,11 +664,17 @@ describe("strict CLI parsing", () => {
|
|
|
659
664
|
"--phase",
|
|
660
665
|
"verify",
|
|
661
666
|
"--dry-run",
|
|
667
|
+
"--force",
|
|
662
668
|
]),
|
|
663
669
|
).toMatchObject({
|
|
664
670
|
commandName: "worktree",
|
|
665
671
|
args: ["rebuild", "example", "verify"],
|
|
666
|
-
values: {
|
|
672
|
+
values: {
|
|
673
|
+
task: "example",
|
|
674
|
+
phase: "verify",
|
|
675
|
+
"dry-run": true,
|
|
676
|
+
force: true,
|
|
677
|
+
},
|
|
667
678
|
})
|
|
668
679
|
expectUsageError(
|
|
669
680
|
["worktree", "inspect", "example", "--dry-run"],
|
package/src/cli-parser.ts
CHANGED
|
@@ -763,7 +763,7 @@ const commands = {
|
|
|
763
763
|
},
|
|
764
764
|
},
|
|
765
765
|
archive: {
|
|
766
|
-
usage: "agency archive <list|show|epic|task|tasks|phase>",
|
|
766
|
+
usage: "agency archive <path|list|show|epic|task|tasks|phase>",
|
|
767
767
|
options: {
|
|
768
768
|
...outputOptions,
|
|
769
769
|
...entitySelectorOptions,
|
|
@@ -772,6 +772,12 @@ const commands = {
|
|
|
772
772
|
status: { type: "string", multiple: true },
|
|
773
773
|
repository: { type: "string", multiple: true },
|
|
774
774
|
},
|
|
775
|
+
command: {
|
|
776
|
+
usage: "agency archive <path> [--dry-run] [--json]",
|
|
777
|
+
minArgs: 1,
|
|
778
|
+
maxArgs: 1,
|
|
779
|
+
options: ["dry-run", "json"],
|
|
780
|
+
},
|
|
775
781
|
subcommands: {
|
|
776
782
|
list: {
|
|
777
783
|
usage:
|
|
@@ -864,6 +870,7 @@ const commands = {
|
|
|
864
870
|
...outputOptions,
|
|
865
871
|
...entitySelectorOptions,
|
|
866
872
|
"dry-run": { type: "boolean" },
|
|
873
|
+
force: { type: "boolean" },
|
|
867
874
|
},
|
|
868
875
|
subcommands: {
|
|
869
876
|
list: {
|
|
@@ -880,31 +887,31 @@ const commands = {
|
|
|
880
887
|
},
|
|
881
888
|
prepare: {
|
|
882
889
|
usage:
|
|
883
|
-
"agency worktree prepare <task-id> [phase-id] [--dry-run] [--json]",
|
|
890
|
+
"agency worktree prepare <task-id> [phase-id] [--dry-run] [--force] [--json]",
|
|
884
891
|
minArgs: 1,
|
|
885
892
|
maxArgs: 2,
|
|
886
|
-
options: ["dry-run", "json", "task", "phase"],
|
|
893
|
+
options: ["dry-run", "force", "json", "task", "phase"],
|
|
887
894
|
},
|
|
888
895
|
remove: {
|
|
889
896
|
usage:
|
|
890
|
-
"agency worktree remove <task-id> [phase-id] [--dry-run] [--json]",
|
|
897
|
+
"agency worktree remove <task-id> [phase-id] [--dry-run] [--force] [--json]",
|
|
891
898
|
minArgs: 1,
|
|
892
899
|
maxArgs: 2,
|
|
893
|
-
options: ["dry-run", "json", "task", "phase"],
|
|
900
|
+
options: ["dry-run", "force", "json", "task", "phase"],
|
|
894
901
|
},
|
|
895
902
|
rebuild: {
|
|
896
903
|
usage:
|
|
897
|
-
"agency worktree rebuild <task-id> [phase-id] [--dry-run] [--json]",
|
|
904
|
+
"agency worktree rebuild <task-id> [phase-id] [--dry-run] [--force] [--json]",
|
|
898
905
|
minArgs: 1,
|
|
899
906
|
maxArgs: 2,
|
|
900
|
-
options: ["dry-run", "json", "task", "phase"],
|
|
907
|
+
options: ["dry-run", "force", "json", "task", "phase"],
|
|
901
908
|
},
|
|
902
909
|
repair: {
|
|
903
910
|
usage:
|
|
904
|
-
"agency worktree repair <task-id> [phase-id] [--dry-run] [--json]",
|
|
911
|
+
"agency worktree repair <task-id> [phase-id] [--dry-run] [--force] [--json]",
|
|
905
912
|
minArgs: 1,
|
|
906
913
|
maxArgs: 2,
|
|
907
|
-
options: ["dry-run", "json", "task", "phase"],
|
|
914
|
+
options: ["dry-run", "force", "json", "task", "phase"],
|
|
908
915
|
},
|
|
909
916
|
},
|
|
910
917
|
},
|
|
@@ -1474,9 +1481,8 @@ export function parseCli(args: readonly string[]): ParsedCli {
|
|
|
1474
1481
|
values: parsed.values,
|
|
1475
1482
|
}
|
|
1476
1483
|
}
|
|
1477
|
-
const
|
|
1478
|
-
|
|
1479
|
-
: definition.command
|
|
1484
|
+
const selectedSubcommand = definition.subcommands?.[subcommand ?? ""]
|
|
1485
|
+
const spec = selectedSubcommand ?? definition.command
|
|
1480
1486
|
if (!spec) {
|
|
1481
1487
|
const message = subcommand
|
|
1482
1488
|
? `Unknown subcommand '${subcommand}' for 'agency ${commandName}'.`
|
|
@@ -1484,7 +1490,7 @@ export function parseCli(args: readonly string[]): ParsedCli {
|
|
|
1484
1490
|
throw usageError(message, definition.usage)
|
|
1485
1491
|
}
|
|
1486
1492
|
|
|
1487
|
-
let commandPositionals =
|
|
1493
|
+
let commandPositionals = selectedSubcommand
|
|
1488
1494
|
? parsed.positionals.slice(1)
|
|
1489
1495
|
: parsed.positionals
|
|
1490
1496
|
const allowed = new Set([...commonOptionNames, ...(spec.options ?? [])])
|
|
@@ -1535,7 +1541,7 @@ export function parseCli(args: readonly string[]): ParsedCli {
|
|
|
1535
1541
|
|
|
1536
1542
|
commandPositionals = applyEntitySelectors(
|
|
1537
1543
|
commandName,
|
|
1538
|
-
subcommand,
|
|
1544
|
+
selectedSubcommand ? subcommand : undefined,
|
|
1539
1545
|
commandPositionals,
|
|
1540
1546
|
parsed.values,
|
|
1541
1547
|
spec,
|
|
@@ -1708,7 +1714,7 @@ export function parseCli(args: readonly string[]): ParsedCli {
|
|
|
1708
1714
|
|
|
1709
1715
|
return {
|
|
1710
1716
|
commandName: commandName as keyof typeof commands,
|
|
1711
|
-
args:
|
|
1717
|
+
args: selectedSubcommand
|
|
1712
1718
|
? [subcommand!, ...commandPositionals]
|
|
1713
1719
|
: commandPositionals,
|
|
1714
1720
|
values: parsed.values,
|
package/src/cli.test.ts
CHANGED
|
@@ -531,9 +531,9 @@ status: dropped
|
|
|
531
531
|
`,
|
|
532
532
|
)
|
|
533
533
|
|
|
534
|
-
expect(
|
|
535
|
-
|
|
536
|
-
)
|
|
534
|
+
expect(
|
|
535
|
+
(await runCli(["archive", "."], join(root, "tasks/example"))).exitCode,
|
|
536
|
+
).toBe(0)
|
|
537
537
|
const result = await runCli(["archive", "task", "example", "--json"], root)
|
|
538
538
|
|
|
539
539
|
expect(result.exitCode).toBe(1)
|
|
@@ -1373,12 +1373,7 @@ status: open
|
|
|
1373
1373
|
}
|
|
1374
1374
|
if (launch === launches[0]) {
|
|
1375
1375
|
expect(effectiveConfig.instructions).toContain(".agency/AGENTS.md")
|
|
1376
|
-
expect(effectiveConfig.agent.agency).
|
|
1377
|
-
description: expect.stringContaining(
|
|
1378
|
-
"Agency workbase orchestration",
|
|
1379
|
-
),
|
|
1380
|
-
mode: "subagent",
|
|
1381
|
-
})
|
|
1376
|
+
expect(effectiveConfig.agent.agency).toBeUndefined()
|
|
1382
1377
|
expect(effectiveConfig.references).toEqual({
|
|
1383
1378
|
workbase: {
|
|
1384
1379
|
path: "..",
|
|
@@ -1386,18 +1381,6 @@ status: open
|
|
|
1386
1381
|
"Complete Agency workbase context; write authority still comes only from agency context",
|
|
1387
1382
|
},
|
|
1388
1383
|
})
|
|
1389
|
-
const agencyProbe = Bun.spawnSync(
|
|
1390
|
-
["opencode", "debug", "agent", "agency"],
|
|
1391
|
-
{ cwd: contract.cwd, env: environment },
|
|
1392
|
-
)
|
|
1393
|
-
expect(agencyProbe.exitCode).toBe(0)
|
|
1394
|
-
expect(JSON.parse(agencyProbe.stdout.toString())).toMatchObject({
|
|
1395
|
-
name: "agency",
|
|
1396
|
-
description: expect.stringContaining(
|
|
1397
|
-
"Agency workbase orchestration",
|
|
1398
|
-
),
|
|
1399
|
-
mode: "subagent",
|
|
1400
|
-
})
|
|
1401
1384
|
for (const document of documents) {
|
|
1402
1385
|
const read = Bun.spawnSync(
|
|
1403
1386
|
[
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
|
+
import { mkdir } from "node:fs/promises"
|
|
2
3
|
import { join } from "node:path"
|
|
3
4
|
import {
|
|
4
5
|
captureLogs,
|
|
@@ -93,6 +94,75 @@ describe("archive command", () => {
|
|
|
93
94
|
)
|
|
94
95
|
})
|
|
95
96
|
|
|
97
|
+
test("infers a task from a filesystem path", async () => {
|
|
98
|
+
const logs = await captureLogs(() =>
|
|
99
|
+
runTestEffect(
|
|
100
|
+
archive({
|
|
101
|
+
args: ["."],
|
|
102
|
+
cwd: join(root, "tasks/example"),
|
|
103
|
+
dryRun: true,
|
|
104
|
+
json: true,
|
|
105
|
+
}),
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
expect(JSON.parse(logs[0]!)).toMatchObject({
|
|
110
|
+
operation: "archive",
|
|
111
|
+
kind: "task",
|
|
112
|
+
id: "example",
|
|
113
|
+
dryRun: true,
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test("infers an epic from a filesystem path", async () => {
|
|
118
|
+
const directory = join(root, "epics/delivery")
|
|
119
|
+
await mkdir(directory, { recursive: true })
|
|
120
|
+
await Bun.write(
|
|
121
|
+
join(directory, "EPIC.md"),
|
|
122
|
+
`---
|
|
123
|
+
ticketUrl: https://example.com/epic
|
|
124
|
+
repos:
|
|
125
|
+
- repo: agency
|
|
126
|
+
ref: main
|
|
127
|
+
tasks: []
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
# Delivery
|
|
131
|
+
`,
|
|
132
|
+
)
|
|
133
|
+
const logs = await captureLogs(() =>
|
|
134
|
+
runTestEffect(
|
|
135
|
+
archive({ args: ["."], cwd: directory, dryRun: true, json: true }),
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
expect(JSON.parse(logs[0]!)).toMatchObject({
|
|
140
|
+
operation: "archive",
|
|
141
|
+
kind: "epic",
|
|
142
|
+
id: "delivery",
|
|
143
|
+
dryRun: true,
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test("rejects ambiguous and unsupported archive paths", async () => {
|
|
148
|
+
await expect(
|
|
149
|
+
runTestEffect(archive({ args: ["."], cwd: root, silent: true })),
|
|
150
|
+
).rejects.toThrow("Archive path is ambiguous")
|
|
151
|
+
await expect(
|
|
152
|
+
runTestEffect(
|
|
153
|
+
archive({ args: ["repos/agency"], cwd: root, silent: true }),
|
|
154
|
+
),
|
|
155
|
+
).rejects.toThrow("Archive path must be within an active epic or task")
|
|
156
|
+
|
|
157
|
+
const phaseDirectory = join(root, "tasks/example/phases/build")
|
|
158
|
+
await mkdir(phaseDirectory, { recursive: true })
|
|
159
|
+
await expect(
|
|
160
|
+
runTestEffect(
|
|
161
|
+
archive({ args: ["."], cwd: phaseDirectory, silent: true }),
|
|
162
|
+
),
|
|
163
|
+
).rejects.toThrow("Archive path identifies a phase")
|
|
164
|
+
})
|
|
165
|
+
|
|
96
166
|
test("reports an already archived task", async () => {
|
|
97
167
|
await runTestEffect(
|
|
98
168
|
archive({ type: "task", args: ["example"], cwd: root, silent: true }),
|
|
@@ -143,7 +213,9 @@ describe("archive command", () => {
|
|
|
143
213
|
test("requires a supported work item type", async () => {
|
|
144
214
|
await expect(
|
|
145
215
|
runTestEffect(archive({ args: [], cwd: root, silent: true })),
|
|
146
|
-
).rejects.toThrow(
|
|
216
|
+
).rejects.toThrow(
|
|
217
|
+
"Provide a path or use: list, show, epic, task, tasks, phase",
|
|
218
|
+
)
|
|
147
219
|
})
|
|
148
220
|
|
|
149
221
|
test("rejects an extra archive show identifier", async () => {
|
package/src/commands/archive.ts
CHANGED
|
@@ -24,6 +24,14 @@ export const archive = (options: ArchiveOptions) =>
|
|
|
24
24
|
const { log } = createLoggers(options)
|
|
25
25
|
const cwd = options.cwd ?? process.cwd()
|
|
26
26
|
const [id, phaseId] = options.args
|
|
27
|
+
let archiveType = options.type
|
|
28
|
+
let archiveId = id
|
|
29
|
+
|
|
30
|
+
if (!archiveType && id) {
|
|
31
|
+
const target = yield* archives.resolvePathTarget(id, cwd)
|
|
32
|
+
archiveType = target.kind
|
|
33
|
+
archiveId = target.id
|
|
34
|
+
}
|
|
27
35
|
|
|
28
36
|
if (options.type === "list") {
|
|
29
37
|
const records = yield* archives.list(
|
|
@@ -73,22 +81,22 @@ export const archive = (options: ArchiveOptions) =>
|
|
|
73
81
|
}
|
|
74
82
|
|
|
75
83
|
let result
|
|
76
|
-
switch (
|
|
84
|
+
switch (archiveType) {
|
|
77
85
|
case "epic":
|
|
78
|
-
if (!
|
|
86
|
+
if (!archiveId)
|
|
79
87
|
return yield* Effect.fail(
|
|
80
88
|
new Error("Usage: agency archive epic <epic-id>"),
|
|
81
89
|
)
|
|
82
|
-
result = yield* archives.archiveEpic(
|
|
90
|
+
result = yield* archives.archiveEpic(archiveId, cwd, {
|
|
83
91
|
dryRun: options.dryRun,
|
|
84
92
|
})
|
|
85
93
|
break
|
|
86
94
|
case "task":
|
|
87
|
-
if (!
|
|
95
|
+
if (!archiveId)
|
|
88
96
|
return yield* Effect.fail(
|
|
89
97
|
new Error("Usage: agency archive task <task-id>"),
|
|
90
98
|
)
|
|
91
|
-
result = yield* archives.archiveTask(
|
|
99
|
+
result = yield* archives.archiveTask(archiveId, cwd, {
|
|
92
100
|
dryRun: options.dryRun,
|
|
93
101
|
})
|
|
94
102
|
break
|
|
@@ -109,7 +117,7 @@ export const archive = (options: ArchiveOptions) =>
|
|
|
109
117
|
default:
|
|
110
118
|
return yield* Effect.fail(
|
|
111
119
|
new Error(
|
|
112
|
-
"Archive
|
|
120
|
+
"Archive target is required. Provide a path or use: list, show, epic, task, tasks, phase",
|
|
113
121
|
),
|
|
114
122
|
)
|
|
115
123
|
}
|
|
@@ -141,11 +149,14 @@ export const archive = (options: ArchiveOptions) =>
|
|
|
141
149
|
})
|
|
142
150
|
|
|
143
151
|
export const help = `
|
|
144
|
-
Usage: agency archive <list|show|epic|task|tasks|phase>
|
|
152
|
+
Usage: agency archive <path|list|show|epic|task|tasks|phase>
|
|
145
153
|
|
|
146
154
|
Browse or archive work items after preflighting worktrees and graph references.
|
|
147
155
|
|
|
156
|
+
An existing path within an active epic or task infers that work item.
|
|
157
|
+
|
|
148
158
|
Commands:
|
|
159
|
+
<path> Archive the containing epic or task
|
|
149
160
|
list [filters] List archived work
|
|
150
161
|
show <type> <id> Show an archived epic or task
|
|
151
162
|
show phase <task-id> <phase-id> Show an archived phase
|
|
@@ -45,11 +45,7 @@ describe("init command", () => {
|
|
|
45
45
|
).text()
|
|
46
46
|
const config = JSON.parse(opencode.slice(opencode.indexOf("\n\n") + 2))
|
|
47
47
|
expect(config.instructions).toEqual([".agency/AGENTS.md"])
|
|
48
|
-
expect(config.agent.agency).
|
|
49
|
-
description: expect.stringContaining("Agency workbase orchestration"),
|
|
50
|
-
mode: "subagent",
|
|
51
|
-
prompt: expect.stringContaining("agency context . --json"),
|
|
52
|
-
})
|
|
48
|
+
expect(config.agent.agency).toBeUndefined()
|
|
53
49
|
expect(config.agent.plan).toEqual({ disable: true })
|
|
54
50
|
expect(config.agent["agency-plan"]).toMatchObject({
|
|
55
51
|
mode: "primary",
|
package/src/commands/worktree.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { WorktreeService } from "../services/WorktreeService"
|
|
|
6
6
|
interface WorktreeOptions extends BaseCommandOptions {
|
|
7
7
|
readonly subcommand?: string
|
|
8
8
|
readonly args?: readonly string[]
|
|
9
|
+
readonly force?: boolean
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
const targetLabel = (owner: {
|
|
@@ -134,5 +135,6 @@ Options:
|
|
|
134
135
|
--task <id> Select a task without positional IDs
|
|
135
136
|
--phase <id> Select a phase with --task
|
|
136
137
|
--dry-run Preflight and report changes without applying them
|
|
138
|
+
--force Override an existing worktree operation lock
|
|
137
139
|
--json Print structured output
|
|
138
140
|
`
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Schema, TreeFormatter } from "@effect/schema"
|
|
2
2
|
import { Data, Effect, Either, Layer } from "effect"
|
|
3
3
|
import { lstat, mkdir, open, rename, rm } from "node:fs/promises"
|
|
4
|
-
import { dirname, join, relative } from "node:path"
|
|
4
|
+
import { dirname, join, relative, resolve, sep } from "node:path"
|
|
5
5
|
import { EpicService, type EpicRecord } from "./EpicService"
|
|
6
6
|
import { FileSystemService } from "./FileSystemService"
|
|
7
7
|
import { PhaseService, type PhaseRecord } from "./PhaseService"
|
|
@@ -52,6 +52,11 @@ class ArchiveError extends Data.TaggedError("ArchiveError")<{
|
|
|
52
52
|
|
|
53
53
|
export type ArchiveKind = "epic" | "task" | "phase"
|
|
54
54
|
|
|
55
|
+
interface ArchivePathTarget {
|
|
56
|
+
readonly kind: "epic" | "task"
|
|
57
|
+
readonly id: string
|
|
58
|
+
}
|
|
59
|
+
|
|
55
60
|
const LifecycleEventSchema = Schema.Struct({
|
|
56
61
|
operation: Schema.Literal("archive", "restore"),
|
|
57
62
|
at: Schema.String,
|
|
@@ -478,6 +483,59 @@ export class ArchiveService extends Effect.Service<ArchiveService>()(
|
|
|
478
483
|
"ArchiveService",
|
|
479
484
|
{
|
|
480
485
|
sync: () => ({
|
|
486
|
+
resolvePathTarget: (path: string, cwd: string = process.cwd()) =>
|
|
487
|
+
Effect.gen(function* () {
|
|
488
|
+
const fs = yield* FileSystemService
|
|
489
|
+
const workbase = yield* WorkbaseService
|
|
490
|
+
const candidate = resolve(cwd, path)
|
|
491
|
+
if (!(yield* fs.exists(candidate))) {
|
|
492
|
+
return yield* new ArchiveError({
|
|
493
|
+
message: `Archive path does not exist: ${candidate}`,
|
|
494
|
+
})
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const canonicalPath = yield* fs.realPath(candidate)
|
|
498
|
+
const root = yield* workbase.discover(canonicalPath)
|
|
499
|
+
const child = relative(root, canonicalPath)
|
|
500
|
+
const parts = child.split(sep)
|
|
501
|
+
if (child === "" || parts.length === 1) {
|
|
502
|
+
return yield* new ArchiveError({
|
|
503
|
+
message: `Archive path is ambiguous; it does not identify a single epic or task: ${canonicalPath}`,
|
|
504
|
+
})
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const [collection, id] = parts
|
|
508
|
+
const kind =
|
|
509
|
+
collection === "epics"
|
|
510
|
+
? "epic"
|
|
511
|
+
: collection === "tasks"
|
|
512
|
+
? "task"
|
|
513
|
+
: undefined
|
|
514
|
+
if (!kind || !id) {
|
|
515
|
+
return yield* new ArchiveError({
|
|
516
|
+
message: `Archive path must be within an active epic or task: ${canonicalPath}`,
|
|
517
|
+
})
|
|
518
|
+
}
|
|
519
|
+
if (kind === "task" && parts[2] === "phases") {
|
|
520
|
+
return yield* new ArchiveError({
|
|
521
|
+
message: `Archive path identifies a phase; use 'agency archive phase <task-id> <phase-id>': ${canonicalPath}`,
|
|
522
|
+
})
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const document = join(
|
|
526
|
+
root,
|
|
527
|
+
kind === "epic" ? "epics" : "tasks",
|
|
528
|
+
id,
|
|
529
|
+
kind === "epic" ? "EPIC.md" : "TASK.md",
|
|
530
|
+
)
|
|
531
|
+
if (!(yield* fs.exists(document))) {
|
|
532
|
+
return yield* new ArchiveError({
|
|
533
|
+
message: `Archive path does not identify an active ${kind}: ${canonicalPath}`,
|
|
534
|
+
})
|
|
535
|
+
}
|
|
536
|
+
return { kind, id } satisfies ArchivePathTarget
|
|
537
|
+
}),
|
|
538
|
+
|
|
481
539
|
list: (filters: ArchiveFilters = {}, startPath: string = process.cwd()) =>
|
|
482
540
|
Effect.gen(function* () {
|
|
483
541
|
const fs = yield* FileSystemService
|
|
@@ -186,9 +186,12 @@ describe("IntegrationService", () => {
|
|
|
186
186
|
}
|
|
187
187
|
expect(managedWorkbaseAgents).toContain("agency push --json")
|
|
188
188
|
expect(managedWorkbaseAgents).toContain("agency-execution-v1")
|
|
189
|
-
expect(managedWorkbaseAgents).toContain("agency context . --
|
|
189
|
+
expect(managedWorkbaseAgents).toContain("agency context . --json")
|
|
190
190
|
expect(managedWorkbaseAgents).toContain(
|
|
191
|
-
"
|
|
191
|
+
"Pass `--full` only when document prose or low-level VCS details are needed",
|
|
192
|
+
)
|
|
193
|
+
expect(managedWorkbaseAgents).not.toContain(
|
|
194
|
+
"agency context . --full --json",
|
|
192
195
|
)
|
|
193
196
|
expect(managedWorkbaseAgents).toContain(
|
|
194
197
|
"Never pass `--work` or `--auto` to `agency task create`",
|
|
@@ -681,19 +684,11 @@ describe("IntegrationService", () => {
|
|
|
681
684
|
)
|
|
682
685
|
})
|
|
683
686
|
|
|
684
|
-
test("configures Agency
|
|
687
|
+
test("configures Agency planning with complete workbase access", () => {
|
|
685
688
|
const config = JSON.parse(managedBody(managedWorkbaseOpencode))
|
|
686
689
|
|
|
687
690
|
expect(config.instructions).toEqual([".agency/AGENTS.md"])
|
|
688
691
|
expect(config.agent).toEqual({
|
|
689
|
-
agency: {
|
|
690
|
-
description:
|
|
691
|
-
"Handles Agency workbase orchestration and workflow operations with the Agency CLI",
|
|
692
|
-
mode: "subagent",
|
|
693
|
-
prompt: expect.stringMatching(
|
|
694
|
-
/agency context \. --json[\s\S]+agency work prepare[\s\S]+never pass `--work` or `--auto`/,
|
|
695
|
-
),
|
|
696
|
-
},
|
|
697
692
|
plan: {
|
|
698
693
|
disable: true,
|
|
699
694
|
},
|
|
@@ -716,19 +711,16 @@ describe("IntegrationService", () => {
|
|
|
716
711
|
},
|
|
717
712
|
},
|
|
718
713
|
})
|
|
719
|
-
expect(config.agent.agency.model).toBeUndefined()
|
|
720
|
-
expect(config.agent.agency.permission).toBeUndefined()
|
|
721
714
|
expect(config.agent["agency-plan"].prompt).toContain(
|
|
722
715
|
"Explicit-new intent overrides reuse",
|
|
723
716
|
)
|
|
724
|
-
expect(config.agent.agency
|
|
725
|
-
expect(config.agent.agency.steps).toBeUndefined()
|
|
726
|
-
expect(config.agent.agency.prompt).toContain(
|
|
727
|
-
"Return the prepared execution contract to the caller",
|
|
728
|
-
)
|
|
717
|
+
expect(config.agent.agency).toBeUndefined()
|
|
729
718
|
expect(config.agent["agency-plan"].prompt).toContain(
|
|
730
719
|
"Start with `agency context . --json`",
|
|
731
720
|
)
|
|
721
|
+
expect(config.agent["agency-plan"].prompt).toContain(
|
|
722
|
+
"Pass `--full` only when document prose or low-level VCS details are needed",
|
|
723
|
+
)
|
|
732
724
|
expect(config.agent["agency-plan"].prompt).toContain(
|
|
733
725
|
"decompose it into independently deliverable tasks",
|
|
734
726
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test"
|
|
2
2
|
import { Effect } from "effect"
|
|
3
|
-
import { mkdtemp, readdir, rm } from "node:fs/promises"
|
|
3
|
+
import { mkdtemp, readdir, rm, utimes } from "node:fs/promises"
|
|
4
4
|
import { tmpdir } from "node:os"
|
|
5
5
|
import { join } from "node:path"
|
|
6
6
|
import { withWorktreeLocks } from "./WorktreeLock"
|
|
@@ -54,11 +54,90 @@ describe("withWorktreeLocks", () => {
|
|
|
54
54
|
_tag: "Left",
|
|
55
55
|
left: {
|
|
56
56
|
_tag: "WorktreeLockError",
|
|
57
|
-
message: `Another worktree operation is in progress for 'alpha'.
|
|
57
|
+
message: `Another worktree operation is in progress for 'alpha'. Retry with --force or remove the stale sentinel with: rm '${lockPath}'`,
|
|
58
58
|
},
|
|
59
59
|
})
|
|
60
60
|
})
|
|
61
61
|
|
|
62
|
+
test("removes stale locks before acquiring them", async () => {
|
|
63
|
+
const root = await createTempDir()
|
|
64
|
+
tempDirs.push(root)
|
|
65
|
+
const lockPath = join(
|
|
66
|
+
root,
|
|
67
|
+
`.agency-worktree-${Buffer.from("alpha:task").toString("hex")}.lock`,
|
|
68
|
+
)
|
|
69
|
+
await Bun.write(lockPath, "")
|
|
70
|
+
const staleAt = new Date(Date.now() - 11 * 60 * 1000)
|
|
71
|
+
await utimes(lockPath, staleAt, staleAt)
|
|
72
|
+
|
|
73
|
+
await expect(
|
|
74
|
+
Effect.runPromise(
|
|
75
|
+
withWorktreeLocks(root, [{ taskId: "alpha" }], Effect.void),
|
|
76
|
+
),
|
|
77
|
+
).resolves.toBeUndefined()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test("force overrides an active lock", async () => {
|
|
81
|
+
const root = await createTempDir()
|
|
82
|
+
tempDirs.push(root)
|
|
83
|
+
let firstEntered!: () => void
|
|
84
|
+
let firstRelease!: () => void
|
|
85
|
+
const firstEnteredPromise = new Promise<void>((resolve) => {
|
|
86
|
+
firstEntered = resolve
|
|
87
|
+
})
|
|
88
|
+
const firstReleasePromise = new Promise<void>((resolve) => {
|
|
89
|
+
firstRelease = resolve
|
|
90
|
+
})
|
|
91
|
+
const first = Effect.runPromise(
|
|
92
|
+
withWorktreeLocks(
|
|
93
|
+
root,
|
|
94
|
+
[{ taskId: "alpha" }],
|
|
95
|
+
Effect.promise(async () => {
|
|
96
|
+
firstEntered()
|
|
97
|
+
await firstReleasePromise
|
|
98
|
+
}),
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
await firstEnteredPromise
|
|
102
|
+
|
|
103
|
+
let forcedEntered!: () => void
|
|
104
|
+
let forcedRelease!: () => void
|
|
105
|
+
const forcedEnteredPromise = new Promise<void>((resolve) => {
|
|
106
|
+
forcedEntered = resolve
|
|
107
|
+
})
|
|
108
|
+
const forcedReleasePromise = new Promise<void>((resolve) => {
|
|
109
|
+
forcedRelease = resolve
|
|
110
|
+
})
|
|
111
|
+
const forced = Effect.runPromise(
|
|
112
|
+
withWorktreeLocks(
|
|
113
|
+
root,
|
|
114
|
+
[{ taskId: "alpha" }],
|
|
115
|
+
Effect.promise(async () => {
|
|
116
|
+
forcedEntered()
|
|
117
|
+
await forcedReleasePromise
|
|
118
|
+
}),
|
|
119
|
+
{ force: true },
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
await forcedEnteredPromise
|
|
123
|
+
|
|
124
|
+
firstRelease()
|
|
125
|
+
await first
|
|
126
|
+
|
|
127
|
+
const conflict = await Effect.runPromise(
|
|
128
|
+
Effect.either(
|
|
129
|
+
withWorktreeLocks(root, [{ taskId: "alpha" }], Effect.void),
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
expect(conflict).toMatchObject({
|
|
133
|
+
_tag: "Left",
|
|
134
|
+
left: { _tag: "WorktreeLockError" },
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
forcedRelease()
|
|
138
|
+
await forced
|
|
139
|
+
})
|
|
140
|
+
|
|
62
141
|
test("releases locks when the protected operation fails", async () => {
|
|
63
142
|
const root = await createTempDir()
|
|
64
143
|
tempDirs.push(root)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Data, Effect } from "effect"
|
|
2
|
-
import { open, rm } from "node:fs/promises"
|
|
2
|
+
import { open, rm, stat } from "node:fs/promises"
|
|
3
3
|
import { join } from "node:path"
|
|
4
4
|
|
|
5
5
|
class WorktreeLockError extends Data.TaggedError("WorktreeLockError")<{
|
|
@@ -12,10 +12,23 @@ export interface WorktreeLockTarget {
|
|
|
12
12
|
readonly phaseId?: string
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
export interface WorktreeLockOptions {
|
|
16
|
+
readonly force?: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const lockTimeoutMs = 10 * 60 * 1000
|
|
20
|
+
|
|
21
|
+
const isErrorCode = (cause: unknown, code: string) =>
|
|
22
|
+
typeof cause === "object" &&
|
|
23
|
+
cause !== null &&
|
|
24
|
+
"code" in cause &&
|
|
25
|
+
cause.code === code
|
|
26
|
+
|
|
15
27
|
const withWorktreeLock = <A, E, R>(
|
|
16
28
|
root: string,
|
|
17
29
|
target: WorktreeLockTarget,
|
|
18
30
|
effect: Effect.Effect<A, E, R>,
|
|
31
|
+
options: WorktreeLockOptions,
|
|
19
32
|
): Effect.Effect<A, E | WorktreeLockError, R> => {
|
|
20
33
|
const key = Buffer.from(
|
|
21
34
|
`${target.taskId}:${target.phaseId ?? "task"}`,
|
|
@@ -24,18 +37,41 @@ const withWorktreeLock = <A, E, R>(
|
|
|
24
37
|
const removalCommand = `rm '${lockPath.replaceAll("'", `'\\''`)}'`
|
|
25
38
|
return Effect.acquireUseRelease(
|
|
26
39
|
Effect.tryPromise({
|
|
27
|
-
try: () =>
|
|
40
|
+
try: async () => {
|
|
41
|
+
try {
|
|
42
|
+
return await open(lockPath, "wx")
|
|
43
|
+
} catch (cause) {
|
|
44
|
+
if (!isErrorCode(cause, "EEXIST")) throw cause
|
|
45
|
+
let stale = false
|
|
46
|
+
try {
|
|
47
|
+
stale = Date.now() - (await stat(lockPath)).mtimeMs >= lockTimeoutMs
|
|
48
|
+
} catch (statCause) {
|
|
49
|
+
if (!isErrorCode(statCause, "ENOENT")) throw statCause
|
|
50
|
+
}
|
|
51
|
+
if (!options.force && !stale) throw cause
|
|
52
|
+
await rm(lockPath, { force: true })
|
|
53
|
+
return open(lockPath, "wx")
|
|
54
|
+
}
|
|
55
|
+
},
|
|
28
56
|
catch: (cause) =>
|
|
29
57
|
new WorktreeLockError({
|
|
30
|
-
message: `Another worktree operation is in progress for '${target.taskId}${target.phaseId ? `/${target.phaseId}` : ""}'.
|
|
58
|
+
message: `Another worktree operation is in progress for '${target.taskId}${target.phaseId ? `/${target.phaseId}` : ""}'. Retry with --force or remove the stale sentinel with: ${removalCommand}`,
|
|
31
59
|
cause,
|
|
32
60
|
}),
|
|
33
61
|
}),
|
|
34
62
|
() => effect,
|
|
35
63
|
(lock) =>
|
|
36
64
|
Effect.promise(async () => {
|
|
65
|
+
let ownsLock = false
|
|
66
|
+
try {
|
|
67
|
+
const [held, current] = await Promise.all([
|
|
68
|
+
lock.stat(),
|
|
69
|
+
stat(lockPath),
|
|
70
|
+
])
|
|
71
|
+
ownsLock = held.dev === current.dev && held.ino === current.ino
|
|
72
|
+
} catch {}
|
|
37
73
|
await lock.close().catch(() => undefined)
|
|
38
|
-
await rm(lockPath, { force: true }).catch(() => undefined)
|
|
74
|
+
if (ownsLock) await rm(lockPath, { force: true }).catch(() => undefined)
|
|
39
75
|
}),
|
|
40
76
|
)
|
|
41
77
|
}
|
|
@@ -44,6 +80,7 @@ export const withWorktreeLocks = <A, E, R>(
|
|
|
44
80
|
root: string,
|
|
45
81
|
targets: readonly WorktreeLockTarget[],
|
|
46
82
|
effect: Effect.Effect<A, E, R>,
|
|
83
|
+
options: WorktreeLockOptions = {},
|
|
47
84
|
): Effect.Effect<A, E | WorktreeLockError, R> => {
|
|
48
85
|
const unique = new Map(
|
|
49
86
|
targets.map((target) => [
|
|
@@ -55,7 +92,7 @@ export const withWorktreeLocks = <A, E, R>(
|
|
|
55
92
|
for (const [, target] of [...unique.entries()]
|
|
56
93
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
57
94
|
.reverse()) {
|
|
58
|
-
current = withWorktreeLock(root, target, current)
|
|
95
|
+
current = withWorktreeLock(root, target, current, options)
|
|
59
96
|
}
|
|
60
97
|
return current
|
|
61
98
|
}
|
|
@@ -259,6 +259,7 @@ interface MaterializeOptions extends BaseCommandOptions {
|
|
|
259
259
|
}
|
|
260
260
|
|
|
261
261
|
interface RemoveOptions extends BaseCommandOptions {
|
|
262
|
+
readonly force?: boolean
|
|
262
263
|
readonly snapshots?: WorktreeRemovalSnapshot[]
|
|
263
264
|
readonly lockHeld?: boolean
|
|
264
265
|
readonly allowReferenceDrift?: boolean
|
|
@@ -267,6 +268,7 @@ interface RemoveOptions extends BaseCommandOptions {
|
|
|
267
268
|
}
|
|
268
269
|
|
|
269
270
|
interface LifecycleOptions extends BaseCommandOptions {
|
|
271
|
+
readonly force?: boolean
|
|
270
272
|
readonly lockHeld?: boolean
|
|
271
273
|
}
|
|
272
274
|
|
|
@@ -1825,6 +1827,7 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
1825
1827
|
root,
|
|
1826
1828
|
[{ taskId, ...(phaseId ? { phaseId } : {}) }],
|
|
1827
1829
|
materialization,
|
|
1830
|
+
{ force: options.force },
|
|
1828
1831
|
)
|
|
1829
1832
|
}),
|
|
1830
1833
|
|
|
@@ -2148,6 +2151,7 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
2148
2151
|
root,
|
|
2149
2152
|
[{ taskId, ...(phaseId ? { phaseId } : {}) }],
|
|
2150
2153
|
removal,
|
|
2154
|
+
{ force: options.force },
|
|
2151
2155
|
)
|
|
2152
2156
|
}),
|
|
2153
2157
|
|
|
@@ -2170,6 +2174,7 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
2170
2174
|
...options,
|
|
2171
2175
|
lockHeld: true,
|
|
2172
2176
|
}),
|
|
2177
|
+
{ force: options.force },
|
|
2173
2178
|
)
|
|
2174
2179
|
}
|
|
2175
2180
|
const inspection = yield* inspectExecution(taskId, phaseId, root)
|
|
@@ -2316,6 +2321,7 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
2316
2321
|
...options,
|
|
2317
2322
|
lockHeld: true,
|
|
2318
2323
|
}),
|
|
2324
|
+
{ force: options.force },
|
|
2319
2325
|
)
|
|
2320
2326
|
}
|
|
2321
2327
|
const inspection = yield* inspectExecution(taskId, phaseId, root)
|
package/src/workbase/AGENTS.md
CHANGED
|
@@ -79,10 +79,10 @@ commands without prescribing an execution environment.
|
|
|
79
79
|
Start every session with one read-only command:
|
|
80
80
|
|
|
81
81
|
```bash
|
|
82
|
-
agency context . --
|
|
82
|
+
agency context . --json
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
Pass `--full` only when document prose or low-level VCS details are needed. Use
|
|
86
86
|
the returned target, document paths and revisions, dependency readiness,
|
|
87
87
|
authority, checkout state, PR state, and validation result. Do not infer these
|
|
88
88
|
from directory names or stale prose.
|
|
@@ -8,7 +8,7 @@ const checksum = (content: string) =>
|
|
|
8
8
|
|
|
9
9
|
const agencyPlanPrompt = `You are in Agency Plan mode. Think, read, search, and delegate exploration to construct a well-formed plan for the user's goal. Keep the plan comprehensive but concise, and ask clarifying questions when important tradeoffs or intent are unclear.
|
|
10
10
|
|
|
11
|
-
Start with \`agency context . --json\`. Use its document paths and revisions, then inspect the graph, related epics, tasks, phases, linked tickets, and repository declarations needed to understand the work. Use machine-readable Agency output when available instead of inferring structure from directory names.
|
|
11
|
+
Start with \`agency context . --json\`. Pass \`--full\` only when document prose or low-level VCS details are needed. Use its document paths and revisions, then inspect the graph, related epics, tasks, phases, linked tickets, and repository declarations needed to understand the work. Use machine-readable Agency output when available instead of inferring structure from directory names.
|
|
12
12
|
|
|
13
13
|
When planning an epic, decompose it into independently deliverable tasks with explicit dependencies. Add phases only when one task genuinely requires multiple ordered delivery units. Reuse or update existing work instead of creating duplicate tasks or phases, except when the user explicitly requests a new, separate, or follow-up item. Explicit-new intent overrides reuse of active and archived work even when the subject or suggested ID matches.
|
|
14
14
|
|
|
@@ -20,24 +20,12 @@ const agencyPlanBashPermissions = {
|
|
|
20
20
|
"agency *": "allow",
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
const agencyAgentPrompt = `You are the Agency workflow specialist. Use the Agency CLI to handle delegated workbase orchestration and workflow operations. Always start with \`agency context . --json\` and follow the managed Agency instructions and reported authority.
|
|
24
|
-
|
|
25
|
-
When the intent and parameters are known, use the matching managed Command Fast Path exactly. The recipes cover creation, preparation, synchronization, phase conversion, archiving, review work, inspection, dropping, continuation, publication, pull requests, non-PR completion, multi-phase setup, investigation handoff, and review refresh. Preparation recipes call \`agency work prepare <task-or-document> --json\`. Do not probe help or list unrelated state, never pass \`--work\` or \`--auto\` to \`agency task create\`, and use \`--dry-run\` only where the recipe or caller requests it. Use CLI discovery only when no fast-path recipe matches or a prescribed command rejects known-current syntax.
|
|
26
|
-
|
|
27
|
-
Return the prepared execution contract to the caller. Agency owns the target, validation, materialized workspace facts, and native commands; the caller owns presentation and process orchestration.`
|
|
28
|
-
|
|
29
23
|
const body = () =>
|
|
30
24
|
`${JSON.stringify(
|
|
31
25
|
{
|
|
32
26
|
$schema: "https://opencode.ai/config.json",
|
|
33
27
|
instructions: [".agency/AGENTS.md"],
|
|
34
28
|
agent: {
|
|
35
|
-
agency: {
|
|
36
|
-
description:
|
|
37
|
-
"Handles Agency workbase orchestration and workflow operations with the Agency CLI",
|
|
38
|
-
mode: "subagent",
|
|
39
|
-
prompt: agencyAgentPrompt,
|
|
40
|
-
},
|
|
41
29
|
plan: {
|
|
42
30
|
disable: true,
|
|
43
31
|
},
|