@markjaquith/agency 3.10.2 → 3.12.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 +54 -6
- package/package.json +1 -1
- package/pi-extensions/agency.ts +26 -5
- package/src/act-schema.ts +13 -0
- package/src/cli-parser.ts +2 -2
- package/src/commands/act.ts +18 -0
- package/src/commands/phase.ts +20 -10
- package/src/commands/task.ts +48 -11
- package/src/commands/work.ts +13 -1
- package/src/protocol.ts +6 -0
- package/src/services/WorkbaseService.ts +14 -0
- package/src/workbase/AGENTS.md +14 -8
- package/src/workbase/branch-name-command.ts +109 -0
- package/src/workbase/branch-name-template.ts +48 -0
- package/src/workbase/opencode-launch.ts +15 -0
- package/src/workbase/opencode-plugin-file.ts +53 -27
- package/src/workbase/schemas.ts +1 -0
package/README.md
CHANGED
|
@@ -140,7 +140,8 @@ whether the server plugin registered writable-checkout skills. It uses a native
|
|
|
140
140
|
toast and does not submit a prompt to an LLM. When no writable checkout skill
|
|
141
141
|
directory is available, server initialization is reported as indeterminate
|
|
142
142
|
rather than inferred from plugin discovery.
|
|
143
|
-
OpenCode discovers the config and plugin from
|
|
143
|
+
OpenCode discovers the config and plugin from Agency launch directories and
|
|
144
|
+
their ancestors.
|
|
144
145
|
The plugin uses OpenCode's plural discovery directory and exports both the V1
|
|
145
146
|
server function and the `opencode2` module wrapper. Integration sync migrates a
|
|
146
147
|
checksum-valid legacy singular-path plugin and preserves customized files.
|
|
@@ -186,6 +187,48 @@ Existing version 2 workbases without `repositories` remain valid. Run
|
|
|
186
187
|
unambiguous. Workbase configuration may also provide a custom writable-worktree
|
|
187
188
|
creation command.
|
|
188
189
|
|
|
190
|
+
### Custom Branch Names
|
|
191
|
+
|
|
192
|
+
Set `branchNameCommand` to an argv template to choose the branch recorded for a
|
|
193
|
+
new execution unit when `--branch` is omitted:
|
|
194
|
+
|
|
195
|
+
```json
|
|
196
|
+
{
|
|
197
|
+
"version": 2,
|
|
198
|
+
"branchNameCommand": ["wt-resolve-new-branch-name", "{ticket}"]
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Agency invokes the command directly, without a shell, from the workbase root and
|
|
203
|
+
uses its trimmed stdout as the branch name. The command has 120 seconds to
|
|
204
|
+
finish. A non-zero exit, empty output, timeout, or output rejected by
|
|
205
|
+
`git check-ref-format --branch` fails creation without falling back. Explicit
|
|
206
|
+
`--branch` always wins. Multi-phase task containers and review tasks do not have
|
|
207
|
+
writable branches and therefore do not invoke this command.
|
|
208
|
+
|
|
209
|
+
Available placeholders are:
|
|
210
|
+
|
|
211
|
+
| Placeholder | Value |
|
|
212
|
+
| ---------------- | -------------------------------------------------- |
|
|
213
|
+
| `{id}` | ID of the task or phase being created |
|
|
214
|
+
| `{ticket}` | Task ticket URL, or `{id}` when there is no ticket |
|
|
215
|
+
| `{ticketUrl}` | Task ticket URL, or an empty string when absent |
|
|
216
|
+
| `{repo}` | Writable repository alias |
|
|
217
|
+
| `{base}` | Base branch |
|
|
218
|
+
| `{workbaseRoot}` | Absolute workbase root |
|
|
219
|
+
| `{taskId}` | Task ID |
|
|
220
|
+
| `{phaseId}` | Phase ID, or an empty string when creating a task |
|
|
221
|
+
|
|
222
|
+
Resolver failures are reported with the `BRANCH_NAME_COMMAND_FAILED` error code
|
|
223
|
+
in JSON output.
|
|
224
|
+
|
|
225
|
+
Matching `AGENCY_ID`, `AGENCY_TICKET`, `AGENCY_TICKET_URL`, `AGENCY_REPO`,
|
|
226
|
+
`AGENCY_BASE`, `AGENCY_WORKBASE_ROOT`, `AGENCY_TASK_ID`, and `AGENCY_PHASE_ID`
|
|
227
|
+
environment variables are also set. Without `branchNameCommand`, task branches
|
|
228
|
+
remain `task/<id>` and phase branches default to `task/<task-id>-<phase-id>`.
|
|
229
|
+
Callers should omit `--branch` when they want the workbase policy; do not pass a
|
|
230
|
+
hard-coded value merely to reproduce Agency's built-in default.
|
|
231
|
+
|
|
189
232
|
### Custom Worktree Command
|
|
190
233
|
|
|
191
234
|
Git workbases create worktrees with Git. Set `worktreeCreateCommand` to an
|
|
@@ -394,9 +437,13 @@ identify the invoking session, so Agency deliberately avoids leaking one
|
|
|
394
437
|
session's worker identity into another; the validated marker and injected system
|
|
395
438
|
instruction remain the V2 fallback when the long-lived server did not inherit
|
|
396
439
|
the client's launch environment.
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
440
|
+
For execution units, the built-in `pi` agent and the `opencode2` and `opencode`
|
|
441
|
+
agents running OpenCode V2 launch from the authoritative writable checkout so
|
|
442
|
+
their project and Git interfaces reflect the implementation repository.
|
|
443
|
+
OpenCode V1 stops configuration discovery at the checkout's Git root, so V1
|
|
444
|
+
launches remain rooted in the task or phase directory. Epic and multi-phase task orchestration remains
|
|
445
|
+
rooted in its Agency document directory. Ancestor discovery still supplies the
|
|
446
|
+
workbase `AGENTS.md` and managed OpenCode config.
|
|
400
447
|
Agency's managed OpenCode plugin grants the active workbase external-directory
|
|
401
448
|
access and exposes existing checkout-local `.claude/skills`, `.agents/skills`,
|
|
402
449
|
and `.opencode/{skill,skills}` definitions. V1 adds those source directories to
|
|
@@ -406,8 +453,9 @@ hook. The global Pi
|
|
|
406
453
|
extension provides equivalent whole-workbase context and additionally discovers
|
|
407
454
|
checkout-local `.pi/skills` through Pi's `resources_discover` lifecycle.
|
|
408
455
|
`agency work` supplies the checkout directly; plain OpenCode and Pi launches
|
|
409
|
-
resolve a materialized execution-unit
|
|
410
|
-
|
|
456
|
+
from a task or phase directory resolve a materialized execution-unit checkout
|
|
457
|
+
through `agency context` and instruct the agent to change to it before
|
|
458
|
+
implementation or Git operations. A multi-phase
|
|
411
459
|
task root has no single checkout, so launch from its phase directory when using
|
|
412
460
|
plain OpenCode or Pi. Other checkout-local configuration is not composed.
|
|
413
461
|
`--print-command` prints the cwd, command template, and non-secret environment
|
package/package.json
CHANGED
package/pi-extensions/agency.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs"
|
|
2
|
-
import { dirname, join, resolve } from "node:path"
|
|
2
|
+
import { dirname, join, resolve, sep } from "node:path"
|
|
3
3
|
|
|
4
4
|
type ExtensionAPI = {
|
|
5
5
|
exec: (
|
|
@@ -26,7 +26,9 @@ type ExtensionAPI = {
|
|
|
26
26
|
type AgencyContext = {
|
|
27
27
|
root?: string
|
|
28
28
|
checkout?: string
|
|
29
|
+
documentDirectory?: string
|
|
29
30
|
target?: string
|
|
31
|
+
working?: boolean
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
const contextTarget = (result: Record<string, any>): string | undefined => {
|
|
@@ -78,14 +80,18 @@ const agencyContext = async (
|
|
|
78
80
|
result.workbase?.root !== root ||
|
|
79
81
|
!target ||
|
|
80
82
|
(target.startsWith("execution-unit:") &&
|
|
81
|
-
|
|
83
|
+
!result.authority?.writable?.checkoutPath)
|
|
82
84
|
)
|
|
83
85
|
return
|
|
84
86
|
|
|
85
87
|
return {
|
|
86
88
|
root,
|
|
87
89
|
checkout: result.authority?.writable?.checkoutPath,
|
|
90
|
+
documentDirectory: result.target?.path
|
|
91
|
+
? dirname(result.target.path)
|
|
92
|
+
: undefined,
|
|
88
93
|
target,
|
|
94
|
+
working: status === "working",
|
|
89
95
|
}
|
|
90
96
|
}
|
|
91
97
|
|
|
@@ -150,12 +156,27 @@ export default function agencyExtension(pi: ExtensionAPI) {
|
|
|
150
156
|
: undefined
|
|
151
157
|
const activeTarget = process.env.AGENCY_TARGET
|
|
152
158
|
const worker =
|
|
153
|
-
activeTarget && context.target === activeTarget
|
|
159
|
+
activeTarget && context.target === activeTarget && context.working
|
|
154
160
|
? `Agency verified this Pi session as the active worker for ${activeTarget}. Perform the assigned work directly. Do not invoke agency work for this target or launch a replacement worker.`
|
|
155
161
|
: undefined
|
|
156
162
|
const access = `The complete Agency workbase is available at ${context.root}. Use absolute paths under that root when workbase context is needed. Agency context remains the authority for writes.`
|
|
157
|
-
const
|
|
158
|
-
|
|
163
|
+
const checkout = context.checkout ? resolve(context.checkout) : undefined
|
|
164
|
+
const current = resolve(ctx.cwd)
|
|
165
|
+
const repository =
|
|
166
|
+
checkout &&
|
|
167
|
+
(current === checkout || current.startsWith(`${checkout}${sep}`))
|
|
168
|
+
const implementation = checkout
|
|
169
|
+
? [
|
|
170
|
+
repository
|
|
171
|
+
? `Pi is rooted in Agency's authoritative writable checkout at ${checkout}. Use this directory for source reads, edits, Git status and other Git operations, builds, tests, and formatting.`
|
|
172
|
+
: `Agency's authoritative writable checkout is ${checkout}. Before doing implementation work or Git operations, change the working directory to that checkout. Set each tool's working directory to it when supported; otherwise use absolute paths.`,
|
|
173
|
+
context.documentDirectory
|
|
174
|
+
? `Run Agency lifecycle commands from ${context.documentDirectory}; agency context may also be queried from the checkout.`
|
|
175
|
+
: undefined,
|
|
176
|
+
"Any reference checkouts reported by Agency context are read-only.",
|
|
177
|
+
]
|
|
178
|
+
.filter(Boolean)
|
|
179
|
+
.join(" ")
|
|
159
180
|
: undefined
|
|
160
181
|
|
|
161
182
|
return {
|
package/src/act-schema.ts
CHANGED
|
@@ -49,6 +49,19 @@ const EntityFields = {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
export const ActDiscovery = Schema.Struct({
|
|
52
|
+
creationDefaults: Schema.Struct({
|
|
53
|
+
branch: Schema.Union(
|
|
54
|
+
Schema.Struct({
|
|
55
|
+
configured: Schema.Literal(true),
|
|
56
|
+
guidance: Schema.String,
|
|
57
|
+
}),
|
|
58
|
+
Schema.Struct({
|
|
59
|
+
configured: Schema.Literal(false),
|
|
60
|
+
task: Schema.String,
|
|
61
|
+
phase: Schema.String,
|
|
62
|
+
}),
|
|
63
|
+
),
|
|
64
|
+
}),
|
|
52
65
|
workbase: Schema.Struct({
|
|
53
66
|
root: Schema.String,
|
|
54
67
|
repositories: Argv,
|
package/src/cli-parser.ts
CHANGED
|
@@ -573,7 +573,7 @@ const commands = {
|
|
|
573
573
|
subcommands: {
|
|
574
574
|
new: {
|
|
575
575
|
usage:
|
|
576
|
-
"agency phase new <task-id> <phase-id> --repo <alias> --
|
|
576
|
+
"agency phase new <task-id> <phase-id> --repo <alias> --base <name> [--branch <name>] [options] [--work [--auto]]",
|
|
577
577
|
minArgs: 2,
|
|
578
578
|
maxArgs: 2,
|
|
579
579
|
options: [
|
|
@@ -596,7 +596,7 @@ const commands = {
|
|
|
596
596
|
},
|
|
597
597
|
create: {
|
|
598
598
|
usage:
|
|
599
|
-
"agency phase create <task-id> <phase-id> --repo <alias> --
|
|
599
|
+
"agency phase create <task-id> <phase-id> --repo <alias> --base <name> [--branch <name>] [options]",
|
|
600
600
|
minArgs: 2,
|
|
601
601
|
maxArgs: 2,
|
|
602
602
|
options: [
|
package/src/commands/act.ts
CHANGED
|
@@ -160,6 +160,21 @@ const shellCommand = (argv: readonly string[]) =>
|
|
|
160
160
|
: `'${arg.replaceAll("'", `'\\''`)}'`,
|
|
161
161
|
)
|
|
162
162
|
.join(" ")
|
|
163
|
+
const creationDefaults = (config: {
|
|
164
|
+
readonly branchNameCommand?: readonly string[]
|
|
165
|
+
}) => ({
|
|
166
|
+
branch: config.branchNameCommand
|
|
167
|
+
? {
|
|
168
|
+
configured: true as const,
|
|
169
|
+
guidance:
|
|
170
|
+
"Omit --branch to use the workbase branchNameCommand; pass it only for an explicit override.",
|
|
171
|
+
}
|
|
172
|
+
: {
|
|
173
|
+
configured: false as const,
|
|
174
|
+
task: "task/<id>",
|
|
175
|
+
phase: "task/<task-id>-<phase-id>",
|
|
176
|
+
},
|
|
177
|
+
})
|
|
163
178
|
const available = (action: ActAction) => !action.blockedReason
|
|
164
179
|
const checkoutState = (inspection: {
|
|
165
180
|
readonly checkouts: readonly {
|
|
@@ -463,6 +478,7 @@ const actStep = (
|
|
|
463
478
|
log(
|
|
464
479
|
JSON.stringify(
|
|
465
480
|
yield* Schema.decodeUnknown(ActDiscovery)({
|
|
481
|
+
creationDefaults: creationDefaults(config),
|
|
466
482
|
workbase: {
|
|
467
483
|
root,
|
|
468
484
|
repositories: graph.nodes
|
|
@@ -772,6 +788,8 @@ or exits from the front screen; Ctrl-C quits. A recap is printed when you exit.
|
|
|
772
788
|
An existing directory or file selects its containing epic, task, or phase.
|
|
773
789
|
A workbase path opens Workbase actions. Otherwise the positional value is a
|
|
774
790
|
task ID. Selectors skip item selection.
|
|
791
|
+
JSON discovery includes creation defaults identifying when callers should omit
|
|
792
|
+
--branch so the workbase branchNameCommand can choose it.
|
|
775
793
|
|
|
776
794
|
Options:
|
|
777
795
|
--action <id> Start an action or filter discovery (IDs from --json)
|
package/src/commands/phase.ts
CHANGED
|
@@ -7,6 +7,8 @@ import { getWorkViews } from "../work-view"
|
|
|
7
7
|
import { parseRepositoryReferences } from "../workbase/repository-reference"
|
|
8
8
|
import { GraphMutationService } from "../services/GraphMutationService"
|
|
9
9
|
import { work as startWork, type StartWork } from "./work"
|
|
10
|
+
import { TaskService } from "../services/TaskService"
|
|
11
|
+
import { resolveBranchName } from "../workbase/branch-name-command"
|
|
10
12
|
|
|
11
13
|
interface PhaseOptions extends BaseCommandOptions {
|
|
12
14
|
readonly subcommand?: string
|
|
@@ -39,6 +41,7 @@ interface PhaseOptions extends BaseCommandOptions {
|
|
|
39
41
|
export const phase = (options: PhaseOptions, work: StartWork = startWork) =>
|
|
40
42
|
Effect.gen(function* () {
|
|
41
43
|
const phases = yield* PhaseService
|
|
44
|
+
const tasks = yield* TaskService
|
|
42
45
|
const mutations = yield* GraphMutationService
|
|
43
46
|
const { log } = createLoggers(options)
|
|
44
47
|
const cwd = options.cwd ?? process.cwd()
|
|
@@ -47,19 +50,26 @@ export const phase = (options: PhaseOptions, work: StartWork = startWork) =>
|
|
|
47
50
|
switch (options.subcommand) {
|
|
48
51
|
case "new":
|
|
49
52
|
case "create": {
|
|
50
|
-
if (
|
|
51
|
-
!taskId ||
|
|
52
|
-
!phaseId ||
|
|
53
|
-
!options.repo ||
|
|
54
|
-
!options.branch ||
|
|
55
|
-
!options.base
|
|
56
|
-
) {
|
|
53
|
+
if (!taskId || !phaseId || !options.repo || !options.base) {
|
|
57
54
|
return yield* Effect.fail(
|
|
58
55
|
new Error(
|
|
59
|
-
"Usage: agency phase create <task-id> <phase-id> --repo <alias> --
|
|
56
|
+
"Usage: agency phase create <task-id> <phase-id> --repo <alias> --base <name> [--branch <name>]",
|
|
60
57
|
),
|
|
61
58
|
)
|
|
62
59
|
}
|
|
60
|
+
const parent = yield* tasks.show(taskId, cwd)
|
|
61
|
+
const branch =
|
|
62
|
+
options.branch ??
|
|
63
|
+
(yield* resolveBranchName({
|
|
64
|
+
id: phaseId,
|
|
65
|
+
taskId,
|
|
66
|
+
phaseId,
|
|
67
|
+
ticketUrl: parent.data.ticketUrl,
|
|
68
|
+
repo: options.repo,
|
|
69
|
+
base: options.base,
|
|
70
|
+
defaultBranch: `task/${taskId}-${phaseId}`,
|
|
71
|
+
startPath: cwd,
|
|
72
|
+
}))
|
|
63
73
|
const record = yield* phases.create(
|
|
64
74
|
{
|
|
65
75
|
taskId,
|
|
@@ -67,7 +77,7 @@ export const phase = (options: PhaseOptions, work: StartWork = startWork) =>
|
|
|
67
77
|
description: options.description,
|
|
68
78
|
repo: options.repo,
|
|
69
79
|
repos: parseRepositoryReferences(options.references),
|
|
70
|
-
branch
|
|
80
|
+
branch,
|
|
71
81
|
base: options.base,
|
|
72
82
|
dependsOn: options.dependsOn,
|
|
73
83
|
firstPhase: options.firstPhase,
|
|
@@ -311,7 +321,7 @@ Create options:
|
|
|
311
321
|
--repo <alias> Writable repository
|
|
312
322
|
--reference <alias>:<ref>
|
|
313
323
|
Read-only repository reference; repeatable
|
|
314
|
-
--branch <name> Working branch
|
|
324
|
+
--branch <name> Working branch (default: configured resolver or task/<task>-<phase>)
|
|
315
325
|
--base <name> Base branch
|
|
316
326
|
--depends-on <id> Phase dependency; repeatable
|
|
317
327
|
--first-phase <id> Existing execution phase ID when converting a task
|
package/src/commands/task.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
buildValidationEvidence,
|
|
17
17
|
normalizeRecalledContext,
|
|
18
18
|
} from "../workbase/execution-contract"
|
|
19
|
+
import { resolveBranchName } from "../workbase/branch-name-command"
|
|
19
20
|
|
|
20
21
|
interface TaskOptions extends BaseCommandOptions {
|
|
21
22
|
readonly subcommand?: string
|
|
@@ -188,6 +189,19 @@ export const task = (
|
|
|
188
189
|
}
|
|
189
190
|
}
|
|
190
191
|
|
|
192
|
+
const base = options.base ?? "main"
|
|
193
|
+
const branch =
|
|
194
|
+
multiPhase || options.branch
|
|
195
|
+
? options.branch
|
|
196
|
+
: yield* resolveBranchName({
|
|
197
|
+
id,
|
|
198
|
+
taskId: id,
|
|
199
|
+
ticketUrl,
|
|
200
|
+
repo: repo!,
|
|
201
|
+
base,
|
|
202
|
+
defaultBranch: `task/${id}`,
|
|
203
|
+
startPath: cwd,
|
|
204
|
+
})
|
|
191
205
|
const record = yield* tasks.create(
|
|
192
206
|
{
|
|
193
207
|
id,
|
|
@@ -197,8 +211,8 @@ export const task = (
|
|
|
197
211
|
multiPhase,
|
|
198
212
|
repo,
|
|
199
213
|
repos: parseRepositoryReferences(options.references),
|
|
200
|
-
branch: multiPhase ? undefined :
|
|
201
|
-
base: multiPhase ? undefined :
|
|
214
|
+
branch: multiPhase ? undefined : branch,
|
|
215
|
+
base: multiPhase ? undefined : base,
|
|
202
216
|
purpose: options.purpose as "investigation" | undefined,
|
|
203
217
|
},
|
|
204
218
|
cwd,
|
|
@@ -275,10 +289,23 @@ export const task = (
|
|
|
275
289
|
cwd,
|
|
276
290
|
)
|
|
277
291
|
: undefined
|
|
292
|
+
const ticketUrl = options.ticketUrl?.trim() || null
|
|
293
|
+
const branch =
|
|
294
|
+
multiPhase || review || options.branch
|
|
295
|
+
? options.branch
|
|
296
|
+
: yield* resolveBranchName({
|
|
297
|
+
id,
|
|
298
|
+
taskId: id,
|
|
299
|
+
ticketUrl,
|
|
300
|
+
repo: repo!,
|
|
301
|
+
base,
|
|
302
|
+
defaultBranch: `task/${id}`,
|
|
303
|
+
startPath: cwd,
|
|
304
|
+
})
|
|
278
305
|
const record = yield* tasks.create(
|
|
279
306
|
{
|
|
280
307
|
id,
|
|
281
|
-
ticketUrl
|
|
308
|
+
ticketUrl,
|
|
282
309
|
description: options.description?.trim() || undefined,
|
|
283
310
|
epic: options.epic,
|
|
284
311
|
multiPhase,
|
|
@@ -287,10 +314,7 @@ export const task = (
|
|
|
287
314
|
repos: review
|
|
288
315
|
? undefined
|
|
289
316
|
: parseRepositoryReferences(options.references),
|
|
290
|
-
branch:
|
|
291
|
-
multiPhase || review
|
|
292
|
-
? undefined
|
|
293
|
-
: (options.branch ?? `task/${id}`),
|
|
317
|
+
branch: multiPhase || review ? undefined : branch,
|
|
294
318
|
base: multiPhase || review ? undefined : base,
|
|
295
319
|
purpose: options.purpose as "investigation" | undefined,
|
|
296
320
|
},
|
|
@@ -337,18 +361,31 @@ export const task = (
|
|
|
337
361
|
new Error("Writable repository is required for task handoff"),
|
|
338
362
|
)
|
|
339
363
|
}
|
|
364
|
+
const ticketUrl = options.ticketUrl?.trim() || null
|
|
365
|
+
const base = options.base ?? "main"
|
|
366
|
+
const branch =
|
|
367
|
+
options.branch ??
|
|
368
|
+
(yield* resolveBranchName({
|
|
369
|
+
id,
|
|
370
|
+
taskId: id,
|
|
371
|
+
ticketUrl,
|
|
372
|
+
repo: options.repo,
|
|
373
|
+
base,
|
|
374
|
+
defaultBranch: `task/${id}`,
|
|
375
|
+
startPath: cwd,
|
|
376
|
+
}))
|
|
340
377
|
const output = yield* tasks.handoff(
|
|
341
378
|
{
|
|
342
379
|
sourceTaskId,
|
|
343
380
|
sourcePhaseId: options.sourcePhase,
|
|
344
381
|
id,
|
|
345
|
-
ticketUrl
|
|
382
|
+
ticketUrl,
|
|
346
383
|
description: options.description?.trim() || undefined,
|
|
347
384
|
epic: options.epic,
|
|
348
385
|
repo: options.repo,
|
|
349
386
|
repos: parseRepositoryReferences(options.references),
|
|
350
|
-
branch
|
|
351
|
-
base
|
|
387
|
+
branch,
|
|
388
|
+
base,
|
|
352
389
|
},
|
|
353
390
|
cwd,
|
|
354
391
|
)
|
|
@@ -591,7 +628,7 @@ Create options:
|
|
|
591
628
|
--repo <alias> Writable repository
|
|
592
629
|
--reference <alias>:<ref>
|
|
593
630
|
Read-only repository reference; repeatable
|
|
594
|
-
--branch <name> Working branch (default: task/<id>)
|
|
631
|
+
--branch <name> Working branch (default: configured resolver or task/<id>)
|
|
595
632
|
--base <name> Base branch (default: main)
|
|
596
633
|
--context-repo <alias> Recalled repository; must agree with --repo
|
|
597
634
|
--context-base <name> Recalled base; must agree with --base
|
package/src/commands/work.ts
CHANGED
|
@@ -37,7 +37,10 @@ import {
|
|
|
37
37
|
readValidationEvidence,
|
|
38
38
|
} from "../workbase/execution-contract"
|
|
39
39
|
import { ValidationFailedError } from "./validate"
|
|
40
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
openCodeDiscoversAncestorConfig,
|
|
42
|
+
prepareOpenCodeLaunch,
|
|
43
|
+
} from "../workbase/opencode-launch"
|
|
41
44
|
|
|
42
45
|
export interface WorkOptions extends BaseCommandOptions {
|
|
43
46
|
readonly directory?: string
|
|
@@ -378,6 +381,15 @@ export const work = (
|
|
|
378
381
|
options.auto,
|
|
379
382
|
)
|
|
380
383
|
cli = resolved.argv[0]!
|
|
384
|
+
if (writablePath && !config.agents?.[agent]) {
|
|
385
|
+
if (
|
|
386
|
+
agent === "pi" ||
|
|
387
|
+
((agent === "opencode2" || agent === "opencode") &&
|
|
388
|
+
(yield* openCodeDiscoversAncestorConfig(cli)))
|
|
389
|
+
) {
|
|
390
|
+
launchPath = writablePath
|
|
391
|
+
}
|
|
392
|
+
}
|
|
381
393
|
const environment = {
|
|
382
394
|
...resolved.environment,
|
|
383
395
|
...agentEnvironment(agent, variables),
|
package/src/protocol.ts
CHANGED
|
@@ -113,6 +113,12 @@ const errorMetadata: Readonly<Record<string, ErrorMetadata>> = {
|
|
|
113
113
|
retryable: false,
|
|
114
114
|
remediation: "Resolve workbase validation errors before reconciling.",
|
|
115
115
|
},
|
|
116
|
+
BranchNameCommandError: {
|
|
117
|
+
code: "BRANCH_NAME_COMMAND_FAILED",
|
|
118
|
+
retryable: false,
|
|
119
|
+
remediation:
|
|
120
|
+
"Fix the workbase branchNameCommand or pass an explicit --branch.",
|
|
121
|
+
},
|
|
116
122
|
ProcessError: { code: "PROCESS_ERROR", retryable: true },
|
|
117
123
|
ProtocolOutputError: {
|
|
118
124
|
code: "PROTOCOL_OUTPUT_ERROR",
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from "../workbase/schemas"
|
|
23
23
|
import { validateWorktreeCreateCommand } from "../workbase/worktree-command"
|
|
24
24
|
import { validatePostCheckoutCommand } from "../workbase/checkout-command"
|
|
25
|
+
import { validateBranchNameCommand } from "../workbase/branch-name-template"
|
|
25
26
|
import { validateAgents } from "../workbase/agent-command"
|
|
26
27
|
import { findDependencyCycles } from "../workbase/dependency-graph"
|
|
27
28
|
import { validateDelivery } from "../workbase/delivery-command"
|
|
@@ -329,6 +330,19 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
329
330
|
})
|
|
330
331
|
}
|
|
331
332
|
}
|
|
333
|
+
if (decoded.value.branchNameCommand) {
|
|
334
|
+
try {
|
|
335
|
+
validateBranchNameCommand(decoded.value.branchNameCommand)
|
|
336
|
+
} catch (cause) {
|
|
337
|
+
return yield* new WorkbaseConfigError({
|
|
338
|
+
path: configPath,
|
|
339
|
+
message:
|
|
340
|
+
cause instanceof Error
|
|
341
|
+
? cause.message
|
|
342
|
+
: "Invalid branchNameCommand",
|
|
343
|
+
})
|
|
344
|
+
}
|
|
345
|
+
}
|
|
332
346
|
for (const [alias, repository] of Object.entries(
|
|
333
347
|
decoded.value.repositories ?? {},
|
|
334
348
|
)) {
|
package/src/workbase/AGENTS.md
CHANGED
|
@@ -73,7 +73,7 @@ retain `--if-revision` guards when shown, and do not add flags that are not show
|
|
|
73
73
|
4. Reconcile remote pull-request state and completion:
|
|
74
74
|
`agency sync <task> [phase] --json`.
|
|
75
75
|
5. Convert an existing single-phase task and add a phase:
|
|
76
|
-
`agency phase create <task> <new-phase> --first-phase <existing-phase> --repo <alias> --
|
|
76
|
+
`agency phase create <task> <new-phase> --first-phase <existing-phase> --repo <alias> --base <base> [--depends-on <existing-phase>] --json`.
|
|
77
77
|
6. Archive terminal work: first run `agency archive task <task> --dry-run --json`,
|
|
78
78
|
`agency archive phase <task> <phase> --dry-run --json`, or
|
|
79
79
|
`agency archive epic <epic> --dry-run --json`; if the preflight is safe,
|
|
@@ -102,7 +102,7 @@ retain `--if-revision` guards when shown, and do not add flags that are not show
|
|
|
102
102
|
13. Create a multi-phase task initially with
|
|
103
103
|
`agency task create <slug> --multi-phase --description <text> --json`, then
|
|
104
104
|
create each execution phase with
|
|
105
|
-
`agency phase create <slug> <phase> --repo <alias> --
|
|
105
|
+
`agency phase create <slug> <phase> --repo <alias> --base <base> [--depends-on <phase>] --json`.
|
|
106
106
|
14. Hand off an investigation to distinct implementation work with
|
|
107
107
|
`agency task handoff <investigation-task> <new-task> [--source-phase <phase>] --repo <alias> --base <base> --json`, then verify the returned destination with
|
|
108
108
|
`agency context <new-task> --json`. Do not prepare or start it unless requested.
|
|
@@ -114,6 +114,9 @@ Never pass `--work` or `--auto` to `agency task create`. Do not run separate
|
|
|
114
114
|
or `agency repo list` commands before these recipes when the required parameters
|
|
115
115
|
are already known. `agency work prepare` owns validation, readiness checks,
|
|
116
116
|
workspace materialization, and the versioned `agency-execution-v1` contract.
|
|
117
|
+
Do not pass `--branch` merely to reproduce a default: when the workbase has a
|
|
118
|
+
`branchNameCommand`, omitting the option lets that policy choose and record the
|
|
119
|
+
branch. Pass `--branch` only when the user explicitly requires an override.
|
|
117
120
|
|
|
118
121
|
These fast paths take precedence over separately installed Agency skill guidance.
|
|
119
122
|
Use `agency <command> --help` only as a recovery step when no recipe matches or a
|
|
@@ -303,9 +306,12 @@ before launching an agent.
|
|
|
303
306
|
|
|
304
307
|
OpenCode can access the complete workbase tree, but this filesystem permission
|
|
305
308
|
does not expand Agency write authority beyond the checkout reported by
|
|
306
|
-
`agency context`.
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
309
|
+
`agency context`. Pi and OpenCode V2 execution units launched by `agency work`
|
|
310
|
+
are rooted in the authoritative writable checkout so their project and Git
|
|
311
|
+
status reflect the implementation repository; OpenCode V1 remains rooted in the
|
|
312
|
+
task or phase directory. Orchestration remains rooted in its
|
|
313
|
+
Agency document directory. The managed integrations resolve the writable
|
|
314
|
+
checkout from launch context or `agency context`, guide task-directory sessions
|
|
315
|
+
to use it for implementation, and expose its supported skill directories through
|
|
316
|
+
the applicable plugin API; this does not make other checkout-local configuration
|
|
317
|
+
authoritative. Agents must follow the authority reported by `agency context`.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Data, Effect } from "effect"
|
|
2
|
+
import { FileSystemService } from "../services/FileSystemService"
|
|
3
|
+
import { WorkbaseService } from "../services/WorkbaseService"
|
|
4
|
+
import {
|
|
5
|
+
expandBranchNameCommand,
|
|
6
|
+
type BranchNameVariables,
|
|
7
|
+
} from "./branch-name-template"
|
|
8
|
+
|
|
9
|
+
class BranchNameCommandError extends Data.TaggedError(
|
|
10
|
+
"BranchNameCommandError",
|
|
11
|
+
)<{
|
|
12
|
+
readonly message: string
|
|
13
|
+
readonly command: readonly string[]
|
|
14
|
+
readonly exitCode?: number
|
|
15
|
+
readonly branch?: string
|
|
16
|
+
}> {}
|
|
17
|
+
|
|
18
|
+
const TIMEOUT_MS = 120_000
|
|
19
|
+
|
|
20
|
+
const commandEnvironment = (
|
|
21
|
+
variables: BranchNameVariables,
|
|
22
|
+
): Record<string, string> =>
|
|
23
|
+
Object.fromEntries(
|
|
24
|
+
Object.entries(variables).map(([name, value]) => [
|
|
25
|
+
`AGENCY_${name.replaceAll(/([a-z])([A-Z])/g, "$1_$2").toUpperCase()}`,
|
|
26
|
+
value,
|
|
27
|
+
]),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
interface ResolveBranchNameInput {
|
|
31
|
+
readonly id: string
|
|
32
|
+
readonly taskId: string
|
|
33
|
+
readonly phaseId?: string
|
|
34
|
+
readonly ticketUrl?: string | null
|
|
35
|
+
readonly repo: string
|
|
36
|
+
readonly base: string
|
|
37
|
+
readonly defaultBranch: string
|
|
38
|
+
readonly startPath?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const resolveBranchName = (input: ResolveBranchNameInput) =>
|
|
42
|
+
Effect.gen(function* () {
|
|
43
|
+
const fs = yield* FileSystemService
|
|
44
|
+
const workbase = yield* WorkbaseService
|
|
45
|
+
const { root, config } = yield* workbase.loadConfig(
|
|
46
|
+
input.startPath ?? process.cwd(),
|
|
47
|
+
)
|
|
48
|
+
if (!config.branchNameCommand) return input.defaultBranch
|
|
49
|
+
|
|
50
|
+
const variables: BranchNameVariables = {
|
|
51
|
+
id: input.id,
|
|
52
|
+
ticket: input.ticketUrl || input.id,
|
|
53
|
+
ticketUrl: input.ticketUrl || "",
|
|
54
|
+
repo: input.repo,
|
|
55
|
+
base: input.base,
|
|
56
|
+
workbaseRoot: root,
|
|
57
|
+
taskId: input.taskId,
|
|
58
|
+
phaseId: input.phaseId ?? "",
|
|
59
|
+
}
|
|
60
|
+
const command = expandBranchNameCommand(config.branchNameCommand, variables)
|
|
61
|
+
const result = yield* fs
|
|
62
|
+
.runCommand(command, {
|
|
63
|
+
cwd: root,
|
|
64
|
+
captureOutput: true,
|
|
65
|
+
env: commandEnvironment(variables),
|
|
66
|
+
timeoutMs: TIMEOUT_MS,
|
|
67
|
+
})
|
|
68
|
+
.pipe(
|
|
69
|
+
Effect.mapError(
|
|
70
|
+
(error) =>
|
|
71
|
+
new BranchNameCommandError({
|
|
72
|
+
message: `branchNameCommand could not run: ${error.cause instanceof Error ? error.cause.message : error.message}`,
|
|
73
|
+
command,
|
|
74
|
+
}),
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
if (result.exitCode !== 0) {
|
|
78
|
+
return yield* Effect.fail(
|
|
79
|
+
new BranchNameCommandError({
|
|
80
|
+
message: `branchNameCommand failed with exit code ${result.exitCode}${result.stderr ? `: ${result.stderr}` : ""}`,
|
|
81
|
+
command,
|
|
82
|
+
exitCode: result.exitCode,
|
|
83
|
+
}),
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
const branch = result.stdout.trim()
|
|
87
|
+
if (!branch) {
|
|
88
|
+
return yield* Effect.fail(
|
|
89
|
+
new BranchNameCommandError({
|
|
90
|
+
message: "branchNameCommand produced an empty branch name",
|
|
91
|
+
command,
|
|
92
|
+
}),
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
const checked = yield* fs.runCommand(
|
|
96
|
+
["git", "check-ref-format", "--branch", branch],
|
|
97
|
+
{ cwd: root, captureOutput: true },
|
|
98
|
+
)
|
|
99
|
+
if (checked.exitCode !== 0) {
|
|
100
|
+
return yield* Effect.fail(
|
|
101
|
+
new BranchNameCommandError({
|
|
102
|
+
message: `branchNameCommand produced invalid Git branch name '${branch}'`,
|
|
103
|
+
command,
|
|
104
|
+
branch,
|
|
105
|
+
}),
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
return branch
|
|
109
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface BranchNameVariables {
|
|
2
|
+
readonly id: string
|
|
3
|
+
readonly ticket: string
|
|
4
|
+
readonly ticketUrl: string
|
|
5
|
+
readonly repo: string
|
|
6
|
+
readonly base: string
|
|
7
|
+
readonly workbaseRoot: string
|
|
8
|
+
readonly taskId: string
|
|
9
|
+
readonly phaseId: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const PLACEHOLDERS = new Set<keyof BranchNameVariables>([
|
|
13
|
+
"id",
|
|
14
|
+
"ticket",
|
|
15
|
+
"ticketUrl",
|
|
16
|
+
"repo",
|
|
17
|
+
"base",
|
|
18
|
+
"workbaseRoot",
|
|
19
|
+
"taskId",
|
|
20
|
+
"phaseId",
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
export const validateBranchNameCommand = (command: readonly string[]) => {
|
|
24
|
+
for (const argument of command) {
|
|
25
|
+
for (const match of argument.matchAll(/\{([^{}]+)\}/g)) {
|
|
26
|
+
const placeholder = match[1]!
|
|
27
|
+
if (!PLACEHOLDERS.has(placeholder as keyof BranchNameVariables)) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Unknown branchNameCommand placeholder: {${placeholder}}`,
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const expandBranchNameCommand = (
|
|
37
|
+
command: readonly string[],
|
|
38
|
+
variables: BranchNameVariables,
|
|
39
|
+
): string[] => {
|
|
40
|
+
validateBranchNameCommand(command)
|
|
41
|
+
return command.map((argument) =>
|
|
42
|
+
argument.replaceAll(
|
|
43
|
+
/\{([^{}]+)\}/g,
|
|
44
|
+
(match, placeholder: string) =>
|
|
45
|
+
variables[placeholder as keyof BranchNameVariables] ?? match,
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
}
|
|
@@ -15,6 +15,21 @@ const Admitted = Schema.Struct({
|
|
|
15
15
|
}),
|
|
16
16
|
})
|
|
17
17
|
|
|
18
|
+
// V1 stops config discovery at the Git root, so a checkout-rooted V1 launch
|
|
19
|
+
// would miss the workbase's managed OpenCode config and plugin.
|
|
20
|
+
export const openCodeDiscoversAncestorConfig = (cli: string) =>
|
|
21
|
+
Effect.gen(function* () {
|
|
22
|
+
const fs = yield* FileSystemService
|
|
23
|
+
const result = yield* fs.runCommand([cli, "--version"], {
|
|
24
|
+
captureOutput: true,
|
|
25
|
+
timeoutMs: 60_000,
|
|
26
|
+
})
|
|
27
|
+
return (
|
|
28
|
+
result.exitCode === 0 &&
|
|
29
|
+
/^(?:opencode\s+v?)?2\./.test(result.stdout.trim())
|
|
30
|
+
)
|
|
31
|
+
}).pipe(Effect.orElseSucceed(() => false))
|
|
32
|
+
|
|
18
33
|
// V2's TUI --prompt only seeds the composer. Submit through the same CLI's
|
|
19
34
|
// authenticated service connection, then attach without a prompt. Reloading or
|
|
20
35
|
// reconnecting the TUI cannot replay the launch input. Only built-in Agency
|
|
@@ -27,6 +27,7 @@ const workerLaunchPattern = /^Agency worker launch target: ([^.\\s]+)\\./
|
|
|
27
27
|
type AgencyContext = {
|
|
28
28
|
root?: string
|
|
29
29
|
checkout?: string
|
|
30
|
+
documentDirectory?: string
|
|
30
31
|
target?: string
|
|
31
32
|
task?: string
|
|
32
33
|
phase?: string
|
|
@@ -139,9 +140,27 @@ const skillInfo = (path: string) => {
|
|
|
139
140
|
}
|
|
140
141
|
}
|
|
141
142
|
|
|
143
|
+
const implementationInstructions = (
|
|
144
|
+
client: "OpenCode" | "Pi",
|
|
145
|
+
context: AgencyContext | undefined,
|
|
146
|
+
directory: string,
|
|
147
|
+
) => {
|
|
148
|
+
if (!context?.checkout) return
|
|
149
|
+
const checkout = resolve(context.checkout)
|
|
150
|
+
const current = resolve(directory)
|
|
151
|
+
const repository = current === checkout || current.startsWith(\`\${checkout}\${sep}\`)
|
|
152
|
+
? \`\${client} is rooted in Agency's authoritative writable checkout at \${checkout}. Use this directory for source reads, edits, Git status and other Git operations, builds, tests, and formatting.\`
|
|
153
|
+
: \`Agency's authoritative writable checkout is \${checkout}. Before doing implementation work or Git operations, change the working directory to that checkout. Set each tool's working directory to it when supported; otherwise use absolute paths.\`
|
|
154
|
+
const lifecycle = context.documentDirectory
|
|
155
|
+
? \`Run Agency lifecycle commands from \${context.documentDirectory}; agency context may also be queried from the checkout. Any reference checkouts reported by Agency context are read-only.\`
|
|
156
|
+
: "Any reference checkouts reported by Agency context are read-only."
|
|
157
|
+
return \`\${repository} \${lifecycle}\`
|
|
158
|
+
}
|
|
159
|
+
|
|
142
160
|
const agencyContext = async (
|
|
143
161
|
directory: string,
|
|
144
162
|
useEnvironmentTarget = true,
|
|
163
|
+
requireWorking = false,
|
|
145
164
|
): Promise<AgencyContext | undefined> => {
|
|
146
165
|
const task = useEnvironmentTarget ? process.env.AGENCY_TASK_ID : undefined
|
|
147
166
|
const phase = useEnvironmentTarget ? process.env.AGENCY_PHASE_ID : undefined
|
|
@@ -161,18 +180,26 @@ const agencyContext = async (
|
|
|
161
180
|
const result = envelope.result ?? {}
|
|
162
181
|
const target = contextTarget(result)
|
|
163
182
|
const document = result.target?.path
|
|
183
|
+
const checkout = result.authority?.writable?.checkoutPath
|
|
164
184
|
const status = result.documents?.phase?.data?.status ?? result.documents?.task?.data?.status
|
|
185
|
+
const current = resolve(directory)
|
|
186
|
+
const documentDirectory = document ? dirname(document) : undefined
|
|
187
|
+
const matchesLocation =
|
|
188
|
+
documentDirectory === current ||
|
|
189
|
+
(checkout &&
|
|
190
|
+
(current === resolve(checkout) || current.startsWith(\`\${resolve(checkout)}\${sep}\`)))
|
|
165
191
|
if (
|
|
166
192
|
result.validation?.valid !== true ||
|
|
167
193
|
!target ||
|
|
168
194
|
!document ||
|
|
169
|
-
|
|
170
|
-
(target.startsWith("execution-unit:") &&
|
|
195
|
+
!matchesLocation ||
|
|
196
|
+
(requireWorking && target.startsWith("execution-unit:") &&
|
|
171
197
|
(!result.authority?.writable?.checkoutPath || status !== "working"))
|
|
172
198
|
) return
|
|
173
199
|
return {
|
|
174
200
|
root: result.workbase?.root,
|
|
175
|
-
checkout
|
|
201
|
+
checkout,
|
|
202
|
+
documentDirectory,
|
|
176
203
|
target,
|
|
177
204
|
task: result.target?.taskId,
|
|
178
205
|
phase: result.target?.phaseId,
|
|
@@ -181,10 +208,11 @@ const agencyContext = async (
|
|
|
181
208
|
|
|
182
209
|
const plugin: V1Plugin = async ({ directory }) => {
|
|
183
210
|
const workerSessions = new Map<string, AgencyContext>()
|
|
211
|
+
const locationContext = agencyContext(directory, false).catch(() => undefined)
|
|
184
212
|
|
|
185
213
|
return {
|
|
186
214
|
config: async (config: any) => {
|
|
187
|
-
const context = await
|
|
215
|
+
const context = await locationContext
|
|
188
216
|
const root = process.env.AGENCY_WORKBASE ?? context?.root ?? discoverWorkbase(directory)
|
|
189
217
|
const checkout = process.env.AGENCY_WRITABLE_CHECKOUT ?? context?.checkout ?? discoverCheckout(directory, root)
|
|
190
218
|
|
|
@@ -220,7 +248,7 @@ const plugin: V1Plugin = async ({ directory }) => {
|
|
|
220
248
|
"chat.message": async ({ sessionID }: { sessionID: string }, output: any) => {
|
|
221
249
|
const launchTarget = workerLaunchTarget(output.parts)
|
|
222
250
|
if (!launchTarget) return
|
|
223
|
-
const context = await agencyContext(directory, false).catch(() => undefined)
|
|
251
|
+
const context = await agencyContext(directory, false, true).catch(() => undefined)
|
|
224
252
|
if (context?.target !== launchTarget) return
|
|
225
253
|
workerSessions.set(sessionID, context)
|
|
226
254
|
},
|
|
@@ -229,16 +257,16 @@ const plugin: V1Plugin = async ({ directory }) => {
|
|
|
229
257
|
output: any,
|
|
230
258
|
) => {
|
|
231
259
|
if (!sessionID) return
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
)
|
|
260
|
+
const worker = workerSessions.get(sessionID)
|
|
261
|
+
const context = worker ?? await locationContext
|
|
262
|
+
const implementation = implementationInstructions("OpenCode", context, directory)
|
|
263
|
+
if (!worker?.target && !implementation) return
|
|
264
|
+
output.system.push([
|
|
265
|
+
worker?.target
|
|
266
|
+
? \`Agency verified this OpenCode session as the active worker for \${worker.target}. Perform the assigned work directly. Do not invoke agency work for this target or launch a replacement worker.\`
|
|
267
|
+
: undefined,
|
|
268
|
+
implementation,
|
|
269
|
+
].filter(Boolean).join(" "))
|
|
242
270
|
},
|
|
243
271
|
"shell.env": async ({ sessionID }: { sessionID?: string }, output: any) => {
|
|
244
272
|
if (!sessionID) return
|
|
@@ -259,7 +287,7 @@ export const AgencyPlugin = plugin
|
|
|
259
287
|
|
|
260
288
|
const setup = async (context: V2PluginContext) => {
|
|
261
289
|
const directory = context.location.directory
|
|
262
|
-
const agency = await agencyContext(directory).catch(() => undefined)
|
|
290
|
+
const agency = await agencyContext(directory, false).catch(() => undefined)
|
|
263
291
|
const root = agency?.root ?? resolve(dirname(fileURLToPath(import.meta.url)), "../..")
|
|
264
292
|
const instructionsPath = join(root, ".agency", "AGENTS.md")
|
|
265
293
|
const instructions = existsSync(instructionsPath)
|
|
@@ -302,23 +330,21 @@ const setup = async (context: V2PluginContext) => {
|
|
|
302
330
|
await context.session.hook("prompt", async (event) => {
|
|
303
331
|
const launchTarget = event.prompt.text.match(workerLaunchPattern)?.[1]
|
|
304
332
|
if (!launchTarget) return
|
|
305
|
-
const current = await agencyContext(directory, false).catch(() => undefined)
|
|
333
|
+
const current = await agencyContext(directory, false, true).catch(() => undefined)
|
|
306
334
|
if (!current || current.target !== launchTarget) return
|
|
307
335
|
workerSessions.set(event.sessionID, current)
|
|
308
336
|
})
|
|
337
|
+
const implementation = implementationInstructions("OpenCode", agency, directory)
|
|
309
338
|
await context.session.hook("context", (event) => {
|
|
310
339
|
if (instructions) event.system.push({ type: "text", text: instructions })
|
|
311
340
|
const current = workerSessions.get(event.sessionID)
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
: undefined,
|
|
320
|
-
].filter(Boolean).join(" "),
|
|
321
|
-
})
|
|
341
|
+
const text = [
|
|
342
|
+
current?.target
|
|
343
|
+
? \`Agency verified this OpenCode session as the active worker for \${current.target}. Perform the assigned work directly. Do not invoke agency work for this target or launch a replacement worker.\`
|
|
344
|
+
: undefined,
|
|
345
|
+
implementationInstructions("OpenCode", current, directory) ?? implementation,
|
|
346
|
+
].filter(Boolean).join(" ")
|
|
347
|
+
if (text) event.system.push({ type: "text", text })
|
|
322
348
|
})
|
|
323
349
|
}
|
|
324
350
|
|
package/src/workbase/schemas.ts
CHANGED
|
@@ -107,6 +107,7 @@ export const WorkbaseConfig = Schema.Struct({
|
|
|
107
107
|
Schema.Record({ key: RepositoryAlias, value: RepositoryDeclaration }),
|
|
108
108
|
),
|
|
109
109
|
chooserCommand: Schema.optional(Schema.NonEmptyArray(NonEmptyString)),
|
|
110
|
+
branchNameCommand: Schema.optional(Schema.NonEmptyArray(NonEmptyString)),
|
|
110
111
|
worktreeCreateCommand: Schema.optional(Schema.NonEmptyArray(NonEmptyString)),
|
|
111
112
|
agents: Schema.optional(
|
|
112
113
|
Schema.Record({
|