@markjaquith/agency 2.68.0 → 2.69.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 +37 -24
- package/cli-main.ts +2 -2
- package/fixtures/protocol/orchestration-recipes.json +2 -2
- package/package.json +1 -1
- package/schemas/agency-graph-v1.schema.json +2 -2
- package/src/cli-parser.test.ts +7 -7
- package/src/cli-parser.ts +11 -11
- package/src/cli.test.ts +9 -9
- package/src/commands/claim.ts +6 -6
- package/src/commands/doctor.test.ts +3 -3
- package/src/commands/work.test.ts +74 -22
- package/src/commands/work.ts +32 -38
- package/src/protocol.test.ts +2 -2
- package/src/services/ArchiveBulkService.test.ts +2 -2
- package/src/services/ClaimService.test.ts +3 -3
- package/src/services/ClaimService.ts +4 -4
- package/src/services/DoctorService.ts +24 -24
- package/src/services/IntegrationService.test.ts +2 -2
- package/src/services/ReadinessService.test.ts +1 -1
- package/src/services/ReviewService.test.ts +2 -2
- package/src/services/SyncService.test.ts +156 -2
- package/src/services/SyncService.ts +54 -2
- package/src/services/VcsMigrationService.test.ts +1 -1
- package/src/services/WorkbaseService.test.ts +33 -2
- package/src/services/WorkbaseService.ts +39 -3
- package/src/workbase/AGENTS.md +7 -7
- package/src/workbase/{runner-command.test.ts → agent-command.test.ts} +33 -23
- package/src/workbase/{runner-command.ts → agent-command.ts} +33 -29
- package/src/workbase/kickoff-contract.ts +4 -4
- package/src/workbase/opencode-file.ts +1 -1
- package/src/workbase/schemas.test.ts +5 -5
- package/src/workbase/schemas.ts +8 -2
|
@@ -238,7 +238,7 @@ pr: null
|
|
|
238
238
|
{
|
|
239
239
|
taskId: "example",
|
|
240
240
|
claimant: "orchestrator",
|
|
241
|
-
|
|
241
|
+
agent: "agent",
|
|
242
242
|
sessionId: "session-1",
|
|
243
243
|
revision: inspected.revision,
|
|
244
244
|
expiresAt: "2099-01-01T00:00:00.000Z",
|
|
@@ -523,7 +523,7 @@ process.stdout.write(${JSON.stringify(JSON.stringify(record))})
|
|
|
523
523
|
{
|
|
524
524
|
taskId: "finished-claim",
|
|
525
525
|
claimant: "orchestrator",
|
|
526
|
-
|
|
526
|
+
agent: "agent",
|
|
527
527
|
sessionId: "session-1",
|
|
528
528
|
revision: initial.revision,
|
|
529
529
|
},
|
|
@@ -575,6 +575,8 @@ process.stdout.write(${JSON.stringify(JSON.stringify(record))})
|
|
|
575
575
|
})
|
|
576
576
|
|
|
577
577
|
test("materializes missing workspaces but leaves branch conflicts unresolved", async () => {
|
|
578
|
+
await Bun.write(join(root, "bin", "gh"), "#!/bin/sh\nprintf '[]\\n'\n")
|
|
579
|
+
await chmod(join(root, "bin", "gh"), 0o755)
|
|
578
580
|
for (const [id, branch] of [
|
|
579
581
|
["missing", "feat/missing"],
|
|
580
582
|
["conflict", "feat/conflict"],
|
|
@@ -649,6 +651,158 @@ process.stdout.write(${JSON.stringify(JSON.stringify(record))})
|
|
|
649
651
|
).toBe(false)
|
|
650
652
|
})
|
|
651
653
|
|
|
654
|
+
test("reconciles a recorded merged PR without materializing an absent checkout", async () => {
|
|
655
|
+
await runTestEffect(
|
|
656
|
+
TaskService.pipe(
|
|
657
|
+
Effect.flatMap((service) =>
|
|
658
|
+
service.create(
|
|
659
|
+
{
|
|
660
|
+
id: "recorded-merged",
|
|
661
|
+
ticketUrl: null,
|
|
662
|
+
repo: "agency",
|
|
663
|
+
branch: "feat/example",
|
|
664
|
+
base: "main",
|
|
665
|
+
},
|
|
666
|
+
root,
|
|
667
|
+
),
|
|
668
|
+
),
|
|
669
|
+
),
|
|
670
|
+
)
|
|
671
|
+
await git(
|
|
672
|
+
["remote", "set-url", "origin", "git@github.com:example/agency.git"],
|
|
673
|
+
join(root, "repos/agency"),
|
|
674
|
+
)
|
|
675
|
+
await runTestEffect(
|
|
676
|
+
PullRequestService.pipe(
|
|
677
|
+
Effect.flatMap((service) =>
|
|
678
|
+
service.setUrl(
|
|
679
|
+
"recorded-merged",
|
|
680
|
+
undefined,
|
|
681
|
+
"https://github.com/example/agency/pull/42",
|
|
682
|
+
root,
|
|
683
|
+
),
|
|
684
|
+
),
|
|
685
|
+
),
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
const applied = await runTestEffect(
|
|
689
|
+
SyncService.pipe(
|
|
690
|
+
Effect.flatMap((service) =>
|
|
691
|
+
service.reconcile({
|
|
692
|
+
cwd: root,
|
|
693
|
+
apply: true,
|
|
694
|
+
taskId: "recorded-merged",
|
|
695
|
+
}),
|
|
696
|
+
),
|
|
697
|
+
),
|
|
698
|
+
)
|
|
699
|
+
expect(applied.changes.map((change) => change.kind)).toEqual([
|
|
700
|
+
"record-pr",
|
|
701
|
+
"mark-done",
|
|
702
|
+
])
|
|
703
|
+
expect(applied.executions[0]?.checkouts).toEqual([])
|
|
704
|
+
expect(
|
|
705
|
+
await Bun.file(join(root, "tasks/recorded-merged/code/agency")).exists(),
|
|
706
|
+
).toBe(false)
|
|
707
|
+
})
|
|
708
|
+
|
|
709
|
+
test("reconciles a uniquely discovered merged PR without materializing", async () => {
|
|
710
|
+
await runTestEffect(
|
|
711
|
+
TaskService.pipe(
|
|
712
|
+
Effect.flatMap((service) =>
|
|
713
|
+
service.create(
|
|
714
|
+
{
|
|
715
|
+
id: "discovered-merged",
|
|
716
|
+
ticketUrl: null,
|
|
717
|
+
repo: "agency",
|
|
718
|
+
branch: "feat/example",
|
|
719
|
+
base: "main",
|
|
720
|
+
},
|
|
721
|
+
root,
|
|
722
|
+
),
|
|
723
|
+
),
|
|
724
|
+
),
|
|
725
|
+
)
|
|
726
|
+
await git(
|
|
727
|
+
["remote", "set-url", "origin", "git@github.com:example/agency.git"],
|
|
728
|
+
join(root, "repos/agency"),
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
const applied = await runTestEffect(
|
|
732
|
+
SyncService.pipe(
|
|
733
|
+
Effect.flatMap((service) =>
|
|
734
|
+
service.reconcile({
|
|
735
|
+
cwd: root,
|
|
736
|
+
apply: true,
|
|
737
|
+
taskId: "discovered-merged",
|
|
738
|
+
}),
|
|
739
|
+
),
|
|
740
|
+
),
|
|
741
|
+
)
|
|
742
|
+
expect(applied.changes.map((change) => change.kind)).toEqual([
|
|
743
|
+
"record-pr",
|
|
744
|
+
"mark-done",
|
|
745
|
+
])
|
|
746
|
+
expect(applied.executions[0]?.checkouts).toEqual([])
|
|
747
|
+
expect(
|
|
748
|
+
await Bun.file(
|
|
749
|
+
join(root, "tasks/discovered-merged/code/agency"),
|
|
750
|
+
).exists(),
|
|
751
|
+
).toBe(false)
|
|
752
|
+
})
|
|
753
|
+
|
|
754
|
+
test("materializes when discovered PR evidence is ambiguous", async () => {
|
|
755
|
+
await runTestEffect(
|
|
756
|
+
TaskService.pipe(
|
|
757
|
+
Effect.flatMap((service) =>
|
|
758
|
+
service.create(
|
|
759
|
+
{
|
|
760
|
+
id: "ambiguous",
|
|
761
|
+
ticketUrl: null,
|
|
762
|
+
repo: "agency",
|
|
763
|
+
branch: "feat/example",
|
|
764
|
+
base: "main",
|
|
765
|
+
},
|
|
766
|
+
root,
|
|
767
|
+
),
|
|
768
|
+
),
|
|
769
|
+
),
|
|
770
|
+
)
|
|
771
|
+
const gh = await Bun.file(join(root, "bin", "gh")).text()
|
|
772
|
+
await Bun.write(
|
|
773
|
+
join(root, "bin", "gh"),
|
|
774
|
+
gh
|
|
775
|
+
.replace('[{"number":42', '[{"number":42')
|
|
776
|
+
.replace(
|
|
777
|
+
"]\nJSON\n",
|
|
778
|
+
`,{\"number\":43,\"state\":\"MERGED\",\"title\":\"Ship again\",\"isDraft\":false,\"headRefName\":\"feat/example\",\"baseRefName\":\"main\",\"headRepository\":{\"nameWithOwner\":\"example/agency\"},\"url\":\"https://github.com/example/agency/pull/43\",\"mergedAt\":\"2100-01-01T00:00:00Z\",\"mergeCommit\":{\"oid\":\"def\"},\"mergeable\":\"MERGEABLE\"}]\nJSON\n`,
|
|
779
|
+
),
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
const applied = await runTestEffect(
|
|
783
|
+
SyncService.pipe(
|
|
784
|
+
Effect.flatMap((service) =>
|
|
785
|
+
service.reconcile({
|
|
786
|
+
cwd: root,
|
|
787
|
+
apply: true,
|
|
788
|
+
taskId: "ambiguous",
|
|
789
|
+
}),
|
|
790
|
+
),
|
|
791
|
+
),
|
|
792
|
+
)
|
|
793
|
+
expect(applied.unresolved).toContainEqual(
|
|
794
|
+
expect.objectContaining({ kind: "multiple-prs" }),
|
|
795
|
+
)
|
|
796
|
+
expect(applied.changes).toContainEqual(
|
|
797
|
+
expect.objectContaining({ kind: "materialize-workspace" }),
|
|
798
|
+
)
|
|
799
|
+
expect(
|
|
800
|
+
await Bun.file(
|
|
801
|
+
join(root, "tasks/ambiguous/code/agency/README.md"),
|
|
802
|
+
).text(),
|
|
803
|
+
).toBe("example\n")
|
|
804
|
+
})
|
|
805
|
+
|
|
652
806
|
test("leaves a missing checkout registration unresolved", async () => {
|
|
653
807
|
await runTestEffect(
|
|
654
808
|
TaskService.pipe(
|
|
@@ -162,6 +162,46 @@ const commandErrorSummary = (stderr: string, fallback: string) =>
|
|
|
162
162
|
.map((line) => line.trim())
|
|
163
163
|
.find(Boolean) ?? fallback
|
|
164
164
|
|
|
165
|
+
interface PullRequestQuery {
|
|
166
|
+
readonly remoteUrl: string | null
|
|
167
|
+
readonly remoteRepository: string
|
|
168
|
+
readonly result:
|
|
169
|
+
| {
|
|
170
|
+
readonly exitCode: number
|
|
171
|
+
readonly stdout: string
|
|
172
|
+
readonly stderr: string
|
|
173
|
+
}
|
|
174
|
+
| undefined
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const mergedPullRequestFromGitHub = (
|
|
178
|
+
data: ExecutionData,
|
|
179
|
+
query: PullRequestQuery | undefined,
|
|
180
|
+
) => {
|
|
181
|
+
if (!query?.result || query.result.exitCode !== 0) return null
|
|
182
|
+
const existing = data.pr ? normalizePullRequestRecord(data.pr) : null
|
|
183
|
+
const details = existing
|
|
184
|
+
? [parseJson<Record<string, unknown>>(query.result.stdout, {})]
|
|
185
|
+
: parseJson<Record<string, unknown>[]>(query.result.stdout, []).filter(
|
|
186
|
+
(item) =>
|
|
187
|
+
item.headRefName === data.branch && item.baseRefName === data.base,
|
|
188
|
+
)
|
|
189
|
+
if (details.length !== 1) return null
|
|
190
|
+
const current = recordFromGitHubJson(details[0]!)
|
|
191
|
+
if (
|
|
192
|
+
current.merged !== true ||
|
|
193
|
+
current.headRepository?.toLowerCase() !==
|
|
194
|
+
query.remoteRepository.toLowerCase() ||
|
|
195
|
+
current.headBranch !== data.branch ||
|
|
196
|
+
current.baseRepository?.toLowerCase() !==
|
|
197
|
+
current.repository.toLowerCase() ||
|
|
198
|
+
current.baseBranch !== data.base
|
|
199
|
+
) {
|
|
200
|
+
return null
|
|
201
|
+
}
|
|
202
|
+
return current
|
|
203
|
+
}
|
|
204
|
+
|
|
165
205
|
export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
166
206
|
sync: () => ({
|
|
167
207
|
reconcile: (
|
|
@@ -463,6 +503,14 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
463
503
|
let revision = record.revision
|
|
464
504
|
const codePath = join(dirname(record.path), "code")
|
|
465
505
|
const checkoutStates: CheckoutState[] = []
|
|
506
|
+
const query = prQueries.get(record.key)
|
|
507
|
+
const remoteMergedPr = config.delivery
|
|
508
|
+
? null
|
|
509
|
+
: mergedPullRequestFromGitHub(data, query)
|
|
510
|
+
const skipCheckoutReconciliation =
|
|
511
|
+
remoteMergedPr !== null &&
|
|
512
|
+
data.claim?.state !== "active" &&
|
|
513
|
+
!data.completion
|
|
466
514
|
let materialize = false
|
|
467
515
|
let workspaceConflict = false
|
|
468
516
|
const declared: readonly (
|
|
@@ -473,7 +521,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
473
521
|
...(data.repos ?? []),
|
|
474
522
|
]
|
|
475
523
|
|
|
476
|
-
for (const checkout of declared) {
|
|
524
|
+
for (const checkout of skipCheckoutReconciliation ? [] : declared) {
|
|
477
525
|
const repositoryPath = join(root, "repos", checkout.repo)
|
|
478
526
|
const checkoutPath = join(codePath, checkout.repo)
|
|
479
527
|
const kind = "branch" in checkout ? "writable" : "reference"
|
|
@@ -811,7 +859,11 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
811
859
|
state: "none",
|
|
812
860
|
}
|
|
813
861
|
let prConflict = false
|
|
814
|
-
|
|
862
|
+
if (!query) {
|
|
863
|
+
return yield* new SyncError({
|
|
864
|
+
message: `Missing pull request query for '${record.key}'`,
|
|
865
|
+
})
|
|
866
|
+
}
|
|
815
867
|
const { remoteUrl, remoteRepository } = query
|
|
816
868
|
|
|
817
869
|
if (config.delivery && !remoteUrl) {
|
|
@@ -41,6 +41,37 @@ describe("WorkbaseService", () => {
|
|
|
41
41
|
).toBe(false)
|
|
42
42
|
})
|
|
43
43
|
|
|
44
|
+
test("loads and validates the global agent", async () => {
|
|
45
|
+
const configDirectory = join(root, "config")
|
|
46
|
+
await write(
|
|
47
|
+
configDirectory,
|
|
48
|
+
"agency/agency.json",
|
|
49
|
+
JSON.stringify({ agent: "pi" }),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const config = await runTestEffect(
|
|
53
|
+
WorkbaseService.pipe(
|
|
54
|
+
Effect.flatMap((service) => service.loadGlobalConfig(configDirectory)),
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
expect(config).toEqual({ agent: "pi" })
|
|
58
|
+
|
|
59
|
+
await write(
|
|
60
|
+
configDirectory,
|
|
61
|
+
"agency/agency.json",
|
|
62
|
+
JSON.stringify({ agent: "codex" }),
|
|
63
|
+
)
|
|
64
|
+
await expect(
|
|
65
|
+
runTestEffect(
|
|
66
|
+
WorkbaseService.pipe(
|
|
67
|
+
Effect.flatMap((service) =>
|
|
68
|
+
service.loadGlobalConfig(configDirectory),
|
|
69
|
+
),
|
|
70
|
+
),
|
|
71
|
+
),
|
|
72
|
+
).rejects.toThrow("Invalid global Agency configuration")
|
|
73
|
+
})
|
|
74
|
+
|
|
44
75
|
test("treats declared but missing repositories as valid aliases", async () => {
|
|
45
76
|
await write(
|
|
46
77
|
root,
|
|
@@ -415,13 +446,13 @@ status: done
|
|
|
415
446
|
).rejects.toThrow("Repository 'agency'")
|
|
416
447
|
})
|
|
417
448
|
|
|
418
|
-
test("rejects an unknown
|
|
449
|
+
test("rejects an unknown agent command placeholder", async () => {
|
|
419
450
|
await write(
|
|
420
451
|
root,
|
|
421
452
|
"agency.json",
|
|
422
453
|
JSON.stringify({
|
|
423
454
|
version: 2,
|
|
424
|
-
|
|
455
|
+
agents: { custom: { command: ["agent", "{unknown}"] } },
|
|
425
456
|
}),
|
|
426
457
|
)
|
|
427
458
|
|
|
@@ -8,6 +8,7 @@ import { parseFrontmatter } from "../workbase/frontmatter"
|
|
|
8
8
|
import {
|
|
9
9
|
EntityId,
|
|
10
10
|
EpicFrontmatter,
|
|
11
|
+
GlobalConfig,
|
|
11
12
|
LegacyWorkbaseRegistry,
|
|
12
13
|
PhaseFrontmatter,
|
|
13
14
|
TaskFrontmatter,
|
|
@@ -22,7 +23,7 @@ import {
|
|
|
22
23
|
import { validateWorktreeCreateCommand } from "../workbase/worktree-command"
|
|
23
24
|
import { validateWorkspaceCreateCommand } from "../workbase/workspace-command"
|
|
24
25
|
import { validatePostCheckoutCommand } from "../workbase/checkout-command"
|
|
25
|
-
import {
|
|
26
|
+
import { validateAgents } from "../workbase/agent-command"
|
|
26
27
|
import { findDependencyCycles } from "../workbase/dependency-graph"
|
|
27
28
|
import { validateDelivery } from "../workbase/delivery-command"
|
|
28
29
|
import { preferredVersionControl } from "../workbase/version-control"
|
|
@@ -90,6 +91,15 @@ const registryPath = (configDirectory?: string) =>
|
|
|
90
91
|
"workbases.json",
|
|
91
92
|
)
|
|
92
93
|
|
|
94
|
+
const globalConfigPath = (configDirectory?: string) =>
|
|
95
|
+
join(
|
|
96
|
+
configDirectory ||
|
|
97
|
+
process.env.XDG_CONFIG_HOME ||
|
|
98
|
+
join(homedir(), ".config"),
|
|
99
|
+
"agency",
|
|
100
|
+
"agency.json",
|
|
101
|
+
)
|
|
102
|
+
|
|
93
103
|
const registrationId = (path: string) =>
|
|
94
104
|
`wb-${new Bun.CryptoHasher("sha256").update(path).digest("hex").slice(0, 12)}`
|
|
95
105
|
|
|
@@ -178,6 +188,32 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
178
188
|
"WorkbaseService",
|
|
179
189
|
{
|
|
180
190
|
sync: () => ({
|
|
191
|
+
loadGlobalConfig: (configDirectory?: string) =>
|
|
192
|
+
Effect.gen(function* () {
|
|
193
|
+
const fs = yield* FileSystemService
|
|
194
|
+
const path = globalConfigPath(configDirectory)
|
|
195
|
+
if (!(yield* fs.exists(path))) return {}
|
|
196
|
+
const content = yield* fs.readFile(path)
|
|
197
|
+
let input: unknown
|
|
198
|
+
try {
|
|
199
|
+
input = JSON.parse(content)
|
|
200
|
+
} catch (cause) {
|
|
201
|
+
return yield* new WorkbaseConfigError({
|
|
202
|
+
path,
|
|
203
|
+
message: `Invalid JSON in global Agency configuration ${path}`,
|
|
204
|
+
cause,
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
const decoded = decode(GlobalConfig, input)
|
|
208
|
+
if (!decoded.success) {
|
|
209
|
+
return yield* new WorkbaseConfigError({
|
|
210
|
+
path,
|
|
211
|
+
message: `Invalid global Agency configuration in ${path}:\n${decoded.error}`,
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
return decoded.value
|
|
215
|
+
}),
|
|
216
|
+
|
|
181
217
|
initialize: (path: string = process.cwd()) =>
|
|
182
218
|
Effect.gen(function* () {
|
|
183
219
|
const fs = yield* FileSystemService
|
|
@@ -309,7 +345,7 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
309
345
|
}
|
|
310
346
|
}
|
|
311
347
|
try {
|
|
312
|
-
|
|
348
|
+
validateAgents(decoded.value.agents)
|
|
313
349
|
validateDelivery(decoded.value.delivery)
|
|
314
350
|
} catch (cause) {
|
|
315
351
|
return yield* new WorkbaseConfigError({
|
|
@@ -317,7 +353,7 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
317
353
|
message:
|
|
318
354
|
cause instanceof Error
|
|
319
355
|
? cause.message
|
|
320
|
-
: "Invalid
|
|
356
|
+
: "Invalid agent configuration",
|
|
321
357
|
})
|
|
322
358
|
}
|
|
323
359
|
return current
|
package/src/workbase/AGENTS.md
CHANGED
|
@@ -121,7 +121,7 @@ Do not rediscover commands that this recipe or a known-current
|
|
|
121
121
|
prepare the task with
|
|
122
122
|
`agency work prepare <slug> --evidence <creation-json> --dry-run --json`, then
|
|
123
123
|
execute the returned plan through `task-document-split` to prepare the
|
|
124
|
-
checkout and open or reuse the background tab. Stop before `
|
|
124
|
+
checkout and open or reuse the background tab. Stop before `agent-start`.
|
|
125
125
|
3. For work/launch/start/kickoff intent, run that same preflight and execute its
|
|
126
126
|
ordered kickoff steps. The plan owns worktree dry-run/preparation, a
|
|
127
127
|
retry-safe background Herdr tab, the side-by-side task document,
|
|
@@ -131,8 +131,8 @@ Do not rediscover commands that this recipe or a known-current
|
|
|
131
131
|
`agency-kickoff-v1` capability, execute the supplied actions directly. Do not
|
|
132
132
|
call Herdr help, skill, or CLI discovery. If capability/version evidence is
|
|
133
133
|
absent or stale, discovery is the compatibility path; then resume the same
|
|
134
|
-
idempotency key rather than creating another tab, checkout, or
|
|
135
|
-
5. After the one final context verification succeeds, leave the
|
|
134
|
+
idempotency key rather than creating another tab, checkout, or agent.
|
|
135
|
+
5. After the one final context verification succeeds, leave the agent in the
|
|
136
136
|
background and stop. Do not inspect, poll, or babysit it unless the user asks.
|
|
137
137
|
|
|
138
138
|
Validation evidence is a local, auditable optimization, not authority. Preflight
|
|
@@ -153,12 +153,12 @@ IDs and remediation commands before retrying.
|
|
|
153
153
|
|
|
154
154
|
`agency work` is the human launch flow: it reconciles managed integration,
|
|
155
155
|
selects work, checks readiness, prepares checkouts, marks execution work
|
|
156
|
-
`working` without creating a claim, and starts the
|
|
156
|
+
`working` without creating a claim, and starts the agent. Epic and multi-phase
|
|
157
157
|
task launches remain orchestration-only. External orchestrators instead claim
|
|
158
|
-
an execution unit, launch and monitor their
|
|
158
|
+
an execution unit, launch and monitor their agent separately, and finish or
|
|
159
159
|
release the claim with the current document revision.
|
|
160
160
|
|
|
161
|
-
An Agency-launched
|
|
161
|
+
An Agency-launched agent receives process-local worker identity through both
|
|
162
162
|
the `AGENCY_SESSION_ID` and `AGENCY_TARGET` environment variables and a generated
|
|
163
163
|
prompt beginning `Agency worker launch target: <target>.` Treat either form as
|
|
164
164
|
launch evidence only after `agency context . --json` confirms the same target,
|
|
@@ -166,7 +166,7 @@ document paths, valid context, and expected write authority. Once confirmed,
|
|
|
166
166
|
perform the assigned work directly and never invoke `agency work` to start the
|
|
167
167
|
same target again.
|
|
168
168
|
|
|
169
|
-
Some
|
|
169
|
+
Some agent clients attach to a long-lived process and may not preserve launch
|
|
170
170
|
environment variables. If the variables and prompt marker are absent, fail safe
|
|
171
171
|
when the initial instruction is a generated `Start`, `Continue`, or `Work on`
|
|
172
172
|
prompt whose absolute document paths match the current directory and the active,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test"
|
|
2
2
|
import {
|
|
3
3
|
printableEnvironment,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from "./
|
|
4
|
+
resolveAgentCommand,
|
|
5
|
+
agentEnvironment,
|
|
6
|
+
validateAgents,
|
|
7
|
+
} from "./agent-command"
|
|
8
8
|
|
|
9
9
|
const variables = {
|
|
10
10
|
prompt: "Read the task.",
|
|
@@ -17,45 +17,55 @@ const variables = {
|
|
|
17
17
|
claimRevision: "revision-1",
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
describe("
|
|
20
|
+
describe("agent commands", () => {
|
|
21
21
|
test("uses promptless interactive commands for built-in presets", () => {
|
|
22
22
|
expect(
|
|
23
|
-
|
|
23
|
+
resolveAgentCommand("opencode2", undefined, variables, false).argv,
|
|
24
24
|
).toEqual(["opencode2"])
|
|
25
25
|
expect(
|
|
26
|
-
|
|
26
|
+
resolveAgentCommand("opencode2", undefined, variables, true).argv,
|
|
27
27
|
).toEqual(["opencode2", "--continue"])
|
|
28
28
|
expect(
|
|
29
|
-
|
|
29
|
+
resolveAgentCommand("opencode", undefined, variables, false).argv,
|
|
30
30
|
).toEqual(["opencode"])
|
|
31
31
|
expect(
|
|
32
|
-
|
|
32
|
+
resolveAgentCommand("opencode", undefined, variables, true).argv,
|
|
33
33
|
).toEqual(["opencode", "--continue"])
|
|
34
|
+
expect(resolveAgentCommand("pi", undefined, variables, false).argv).toEqual(
|
|
35
|
+
["pi"],
|
|
36
|
+
)
|
|
37
|
+
expect(resolveAgentCommand("pi", undefined, variables, true).argv).toEqual([
|
|
38
|
+
"pi",
|
|
39
|
+
"--continue",
|
|
40
|
+
])
|
|
34
41
|
expect(
|
|
35
|
-
|
|
42
|
+
resolveAgentCommand("claude", undefined, variables, true).argv,
|
|
36
43
|
).toEqual(["claude", "--continue"])
|
|
37
44
|
})
|
|
38
45
|
|
|
39
46
|
test("uses autonomous commands when a prompt is requested", () => {
|
|
40
47
|
expect(
|
|
41
|
-
|
|
48
|
+
resolveAgentCommand("opencode2", undefined, variables, false, true).argv,
|
|
42
49
|
).toEqual(["opencode2", "--prompt", "Read the task."])
|
|
43
50
|
expect(
|
|
44
|
-
|
|
51
|
+
resolveAgentCommand("opencode2", undefined, variables, true, true).argv,
|
|
45
52
|
).toEqual(["opencode2", "--continue", "--prompt", "Read the task."])
|
|
46
53
|
expect(
|
|
47
|
-
|
|
54
|
+
resolveAgentCommand("opencode", undefined, variables, false, true).argv,
|
|
48
55
|
).toEqual(["opencode", "--prompt", "Read the task."])
|
|
49
56
|
expect(
|
|
50
|
-
|
|
57
|
+
resolveAgentCommand("opencode", undefined, variables, true, true).argv,
|
|
51
58
|
).toEqual(["opencode", "--continue", "--prompt", "Read the task."])
|
|
52
59
|
expect(
|
|
53
|
-
|
|
60
|
+
resolveAgentCommand("pi", undefined, variables, false, true).argv,
|
|
61
|
+
).toEqual(["pi", "Read the task."])
|
|
62
|
+
expect(
|
|
63
|
+
resolveAgentCommand("claude", undefined, variables, true, true).argv,
|
|
54
64
|
).toEqual(["claude", "--continue", "Read the task."])
|
|
55
65
|
})
|
|
56
66
|
|
|
57
67
|
test("expands configured argv and environment without a shell", () => {
|
|
58
|
-
const resolved =
|
|
68
|
+
const resolved = resolveAgentCommand(
|
|
59
69
|
"custom",
|
|
60
70
|
{
|
|
61
71
|
custom: {
|
|
@@ -79,33 +89,33 @@ describe("runner commands", () => {
|
|
|
79
89
|
})
|
|
80
90
|
})
|
|
81
91
|
|
|
82
|
-
test("rejects --auto for configured
|
|
92
|
+
test("rejects --auto for configured agents without an auto command", () => {
|
|
83
93
|
expect(() =>
|
|
84
|
-
|
|
94
|
+
resolveAgentCommand(
|
|
85
95
|
"custom",
|
|
86
96
|
{ custom: { command: ["agent"] } },
|
|
87
97
|
variables,
|
|
88
98
|
false,
|
|
89
99
|
true,
|
|
90
100
|
),
|
|
91
|
-
).toThrow("
|
|
101
|
+
).toThrow("Agent 'custom' does not support --auto")
|
|
92
102
|
})
|
|
93
103
|
|
|
94
104
|
test("rejects unknown placeholders", () => {
|
|
95
105
|
expect(() =>
|
|
96
|
-
|
|
97
|
-
).toThrow("Unknown
|
|
106
|
+
validateAgents({ custom: { command: ["agent", "{unknown}"] } }),
|
|
107
|
+
).toThrow("Unknown agent 'custom' placeholder: {unknown}")
|
|
98
108
|
})
|
|
99
109
|
|
|
100
110
|
test("provides normalized Agency environment and filters secret values", () => {
|
|
101
111
|
const environment = {
|
|
102
|
-
...
|
|
112
|
+
...agentEnvironment("custom", variables),
|
|
103
113
|
VISIBLE: "yes",
|
|
104
114
|
ACCESS_TOKEN: "secret",
|
|
105
115
|
}
|
|
106
116
|
|
|
107
117
|
expect(environment).toMatchObject({
|
|
108
|
-
|
|
118
|
+
AGENCY_AGENT: "custom",
|
|
109
119
|
AGENCY_CLAIMANT: "orchestrator",
|
|
110
120
|
AGENCY_SESSION_ID: "session-1",
|
|
111
121
|
AGENCY_WORKBASE: "/workbase",
|