@markjaquith/agency 2.67.0 → 2.69.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 +61 -33
- 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 +11 -9
- package/src/commands/claim.ts +6 -6
- package/src/commands/doctor.test.ts +3 -3
- package/src/commands/init.test.ts +6 -0
- package/src/commands/integration.test.ts +20 -2
- package/src/commands/integration.ts +6 -4
- 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 +158 -2
- package/src/services/IntegrationService.ts +51 -1
- package/src/services/ReadinessService.test.ts +1 -1
- package/src/services/ReviewService.test.ts +2 -2
- package/src/services/SyncService.test.ts +2 -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/pi-extension-file.ts +158 -0
- package/src/workbase/schemas.test.ts +5 -5
- package/src/workbase/schemas.ts +8 -2
|
@@ -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",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { WorkbaseConfig } from "./schemas"
|
|
2
2
|
|
|
3
|
-
export interface
|
|
3
|
+
export interface AgentCommandVariables {
|
|
4
4
|
readonly prompt: string
|
|
5
5
|
readonly workbase: string
|
|
6
6
|
readonly target: string
|
|
@@ -11,7 +11,7 @@ export interface RunnerCommandVariables {
|
|
|
11
11
|
readonly claimRevision: string
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
interface
|
|
14
|
+
interface AgentDefinition {
|
|
15
15
|
readonly command: readonly string[]
|
|
16
16
|
readonly autoCommand?: readonly string[]
|
|
17
17
|
readonly resumeCommand?: readonly string[]
|
|
@@ -19,7 +19,7 @@ interface RunnerDefinition {
|
|
|
19
19
|
readonly environment?: Readonly<Record<string, string>>
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
const PLACEHOLDERS = new Set<keyof
|
|
22
|
+
const PLACEHOLDERS = new Set<keyof AgentCommandVariables>([
|
|
23
23
|
"prompt",
|
|
24
24
|
"workbase",
|
|
25
25
|
"target",
|
|
@@ -30,7 +30,7 @@ const PLACEHOLDERS = new Set<keyof RunnerCommandVariables>([
|
|
|
30
30
|
"claimRevision",
|
|
31
31
|
])
|
|
32
32
|
|
|
33
|
-
const
|
|
33
|
+
const BUILTIN_AGENTS: Readonly<Record<string, AgentDefinition>> = {
|
|
34
34
|
opencode2: {
|
|
35
35
|
command: ["opencode2"],
|
|
36
36
|
autoCommand: ["opencode2", "--prompt", "{prompt}"],
|
|
@@ -43,6 +43,12 @@ const BUILTIN_RUNNERS: Readonly<Record<string, RunnerDefinition>> = {
|
|
|
43
43
|
resumeCommand: ["opencode", "--continue"],
|
|
44
44
|
autoResumeCommand: ["opencode", "--continue", "--prompt", "{prompt}"],
|
|
45
45
|
},
|
|
46
|
+
pi: {
|
|
47
|
+
command: ["pi"],
|
|
48
|
+
autoCommand: ["pi", "{prompt}"],
|
|
49
|
+
resumeCommand: ["pi", "--continue"],
|
|
50
|
+
autoResumeCommand: ["pi", "--continue", "{prompt}"],
|
|
51
|
+
},
|
|
46
52
|
claude: {
|
|
47
53
|
command: ["claude"],
|
|
48
54
|
autoCommand: ["claude", "{prompt}"],
|
|
@@ -51,48 +57,46 @@ const BUILTIN_RUNNERS: Readonly<Record<string, RunnerDefinition>> = {
|
|
|
51
57
|
},
|
|
52
58
|
}
|
|
53
59
|
|
|
54
|
-
const validateTemplate = (
|
|
60
|
+
const validateTemplate = (agent: string, value: string) => {
|
|
55
61
|
for (const match of value.matchAll(/\{([^{}]+)\}/g)) {
|
|
56
62
|
const placeholder = match[1]!
|
|
57
|
-
if (!PLACEHOLDERS.has(placeholder as keyof
|
|
58
|
-
throw new Error(
|
|
59
|
-
`Unknown runner '${runner}' placeholder: {${placeholder}}`,
|
|
60
|
-
)
|
|
63
|
+
if (!PLACEHOLDERS.has(placeholder as keyof AgentCommandVariables)) {
|
|
64
|
+
throw new Error(`Unknown agent '${agent}' placeholder: {${placeholder}}`)
|
|
61
65
|
}
|
|
62
66
|
}
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
export const
|
|
66
|
-
for (const [name,
|
|
69
|
+
export const validateAgents = (agents: WorkbaseConfig["agents"]): void => {
|
|
70
|
+
for (const [name, agent] of Object.entries(agents ?? {})) {
|
|
67
71
|
for (const value of [
|
|
68
|
-
...
|
|
69
|
-
...(
|
|
70
|
-
...(
|
|
71
|
-
...(
|
|
72
|
-
...Object.values(
|
|
72
|
+
...agent.command,
|
|
73
|
+
...(agent.autoCommand ?? []),
|
|
74
|
+
...(agent.resumeCommand ?? []),
|
|
75
|
+
...(agent.autoResumeCommand ?? []),
|
|
76
|
+
...Object.values(agent.environment ?? {}),
|
|
73
77
|
]) {
|
|
74
78
|
validateTemplate(name, value)
|
|
75
79
|
}
|
|
76
80
|
}
|
|
77
81
|
}
|
|
78
82
|
|
|
79
|
-
const expand = (value: string, variables:
|
|
83
|
+
const expand = (value: string, variables: AgentCommandVariables) =>
|
|
80
84
|
value.replaceAll(
|
|
81
85
|
/\{([^{}]+)\}/g,
|
|
82
86
|
(match, placeholder: string) =>
|
|
83
|
-
variables[placeholder as keyof
|
|
87
|
+
variables[placeholder as keyof AgentCommandVariables] ?? match,
|
|
84
88
|
)
|
|
85
89
|
|
|
86
|
-
export const
|
|
90
|
+
export const resolveAgentCommand = (
|
|
87
91
|
name: string,
|
|
88
|
-
configured: WorkbaseConfig["
|
|
89
|
-
variables:
|
|
92
|
+
configured: WorkbaseConfig["agents"],
|
|
93
|
+
variables: AgentCommandVariables,
|
|
90
94
|
resume: boolean,
|
|
91
95
|
auto = false,
|
|
92
96
|
) => {
|
|
93
|
-
|
|
94
|
-
const definition = configured?.[name] ??
|
|
95
|
-
if (!definition) throw new Error(`Unknown
|
|
97
|
+
validateAgents(configured)
|
|
98
|
+
const definition = configured?.[name] ?? BUILTIN_AGENTS[name]
|
|
99
|
+
if (!definition) throw new Error(`Unknown agent: ${name}`)
|
|
96
100
|
const template = auto
|
|
97
101
|
? resume
|
|
98
102
|
? (definition.autoResumeCommand ?? definition.autoCommand)
|
|
@@ -101,7 +105,7 @@ export const resolveRunnerCommand = (
|
|
|
101
105
|
? definition.resumeCommand
|
|
102
106
|
: definition.command
|
|
103
107
|
if (!template) {
|
|
104
|
-
throw new Error(`
|
|
108
|
+
throw new Error(`Agent '${name}' does not support --auto`)
|
|
105
109
|
}
|
|
106
110
|
const argv = template.map((argument) => expand(argument, variables))
|
|
107
111
|
const environment = Object.fromEntries(
|
|
@@ -113,11 +117,11 @@ export const resolveRunnerCommand = (
|
|
|
113
117
|
return { argv, environment }
|
|
114
118
|
}
|
|
115
119
|
|
|
116
|
-
export const
|
|
117
|
-
|
|
118
|
-
variables:
|
|
120
|
+
export const agentEnvironment = (
|
|
121
|
+
agent: string,
|
|
122
|
+
variables: AgentCommandVariables,
|
|
119
123
|
): Record<string, string> => ({
|
|
120
|
-
|
|
124
|
+
AGENCY_AGENT: agent,
|
|
121
125
|
AGENCY_CLAIMANT: variables.claimant,
|
|
122
126
|
AGENCY_SESSION_ID: variables.sessionId,
|
|
123
127
|
AGENCY_CLAIM_REVISION: variables.claimRevision,
|
|
@@ -325,11 +325,11 @@ export const buildKickoffPlan = (input: {
|
|
|
325
325
|
recovery: "Reuse the existing split when present.",
|
|
326
326
|
},
|
|
327
327
|
{
|
|
328
|
-
id: "
|
|
328
|
+
id: "agent-start",
|
|
329
329
|
cwd: taskDirectory,
|
|
330
330
|
argv: ["agency", "work", ".", "--auto"],
|
|
331
331
|
recovery:
|
|
332
|
-
"Inspect the recorded tab before retrying; a working
|
|
332
|
+
"Inspect the recorded tab before retrying; a working agent must not be duplicated.",
|
|
333
333
|
},
|
|
334
334
|
{
|
|
335
335
|
id: "final-context-verification",
|
|
@@ -341,7 +341,7 @@ export const buildKickoffPlan = (input: {
|
|
|
341
341
|
],
|
|
342
342
|
exactlyOnce: true,
|
|
343
343
|
recovery:
|
|
344
|
-
"If verification fails, inspect the existing tab; do not launch another
|
|
344
|
+
"If verification fails, inspect the existing tab; do not launch another agent.",
|
|
345
345
|
},
|
|
346
346
|
],
|
|
347
347
|
successFields: [
|
|
@@ -351,7 +351,7 @@ export const buildKickoffPlan = (input: {
|
|
|
351
351
|
"preparedCheckout",
|
|
352
352
|
"herdrWorkspace",
|
|
353
353
|
"herdrTab",
|
|
354
|
-
"
|
|
354
|
+
"agentStart",
|
|
355
355
|
"contextVerification",
|
|
356
356
|
],
|
|
357
357
|
}
|
|
@@ -31,7 +31,7 @@ const body = () =>
|
|
|
31
31
|
"Handles Agency workbase orchestration and workflow operations with the Agency CLI",
|
|
32
32
|
mode: "subagent",
|
|
33
33
|
prompt:
|
|
34
|
-
"You are the Agency workflow specialist. Use the Agency CLI to handle delegated workbase orchestration and workflow operations. Always start with `agency context . --json`, follow the managed Agency instructions and reported authority, use Agency commands for durable mutations, and report the resulting state concisely. When delegated to start or kick off work in another agent, launch it, verify that the
|
|
34
|
+
"You are the Agency workflow specialist. Use the Agency CLI to handle delegated workbase orchestration and workflow operations. Always start with `agency context . --json`, follow the managed Agency instructions and reported authority, use Agency commands for durable mutations, and report the resulting state concisely. When delegated to start or kick off work in another agent, launch it, verify that the agent started successfully, and return without waiting for the task to finish.",
|
|
35
35
|
},
|
|
36
36
|
plan: {
|
|
37
37
|
disable: true,
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { createHash } from "node:crypto"
|
|
2
|
+
|
|
3
|
+
const managedHeaderPattern =
|
|
4
|
+
/^\/\/ agency-managed: sha256=([a-f0-9]{64})\r?\n\r?\n/
|
|
5
|
+
|
|
6
|
+
const checksum = (content: string) =>
|
|
7
|
+
createHash("sha256").update(content).digest("hex")
|
|
8
|
+
|
|
9
|
+
const body = `import { existsSync, readFileSync } from "node:fs"
|
|
10
|
+
import { dirname, join, resolve } from "node:path"
|
|
11
|
+
import { CONFIG_DIR_NAME, type ExtensionAPI } from "@earendil-works/pi-coding-agent"
|
|
12
|
+
|
|
13
|
+
type AgencyContext = {
|
|
14
|
+
root?: string
|
|
15
|
+
checkout?: string
|
|
16
|
+
target?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const contextTarget = (result: Record<string, any>): string | undefined => {
|
|
20
|
+
const target = result.target
|
|
21
|
+
if (target?.kind === "epic") return \`epic:\${target.epicId}\`
|
|
22
|
+
if (target?.kind === "phase") {
|
|
23
|
+
return \`execution-unit:phase/\${target.taskId}/\${target.phaseId}\`
|
|
24
|
+
}
|
|
25
|
+
if (target?.kind === "task") {
|
|
26
|
+
return result.authority?.mode === "execution"
|
|
27
|
+
? \`execution-unit:task/\${target.taskId}\`
|
|
28
|
+
: \`task:\${target.taskId}\`
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const discoverWorkbase = (directory: string) => {
|
|
33
|
+
let current = directory
|
|
34
|
+
while (true) {
|
|
35
|
+
if (existsSync(join(current, "agency.json"))) return current
|
|
36
|
+
const parent = dirname(current)
|
|
37
|
+
if (parent === current) return
|
|
38
|
+
current = parent
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const discoverCheckout = (directory: string, root: string | undefined) => {
|
|
43
|
+
if (!root) return
|
|
44
|
+
let current = directory
|
|
45
|
+
while (current.startsWith(root)) {
|
|
46
|
+
for (const name of ["PHASE.md", "TASK.md"]) {
|
|
47
|
+
const document = join(current, name)
|
|
48
|
+
if (!existsSync(document)) continue
|
|
49
|
+
const repo = readFileSync(document, "utf8").match(/^repo:\\s*([^\\s]+)\\s*$/m)?.[1]
|
|
50
|
+
if (!repo) return
|
|
51
|
+
const checkout = join(current, "code", repo.replace(/^['\"]|['\"]$/g, ""))
|
|
52
|
+
return existsSync(checkout) ? checkout : undefined
|
|
53
|
+
}
|
|
54
|
+
if (current === root) return
|
|
55
|
+
current = dirname(current)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const agencyContext = async (
|
|
60
|
+
pi: ExtensionAPI,
|
|
61
|
+
directory: string,
|
|
62
|
+
): Promise<AgencyContext | undefined> => {
|
|
63
|
+
const task = process.env.AGENCY_TASK_ID
|
|
64
|
+
const phase = process.env.AGENCY_PHASE_ID
|
|
65
|
+
const args = task
|
|
66
|
+
? ["context", "--task", task, ...(phase ? ["--phase", phase] : []), "--compact", "--json"]
|
|
67
|
+
: ["context", ".", "--compact", "--json"]
|
|
68
|
+
const command = await pi.exec("agency", args, { timeout: 5000 })
|
|
69
|
+
if (command.code !== 0) return
|
|
70
|
+
const envelope = JSON.parse(command.stdout)
|
|
71
|
+
if (envelope.ok !== true) return
|
|
72
|
+
const result = envelope.result ?? {}
|
|
73
|
+
const target = contextTarget(result)
|
|
74
|
+
const document = result.target?.path
|
|
75
|
+
const status = result.documents?.phase?.data?.status ?? result.documents?.task?.data?.status
|
|
76
|
+
if (
|
|
77
|
+
result.validation?.valid !== true ||
|
|
78
|
+
!target ||
|
|
79
|
+
!document ||
|
|
80
|
+
dirname(document) !== resolve(directory) ||
|
|
81
|
+
(target.startsWith("execution-unit:") &&
|
|
82
|
+
(!result.authority?.writable?.checkoutPath || status !== "working"))
|
|
83
|
+
) return
|
|
84
|
+
return {
|
|
85
|
+
root: result.workbase?.root,
|
|
86
|
+
checkout: result.authority?.writable?.checkoutPath,
|
|
87
|
+
target,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const extension = (pi: ExtensionAPI) => {
|
|
92
|
+
const contexts = new Map<string, Promise<AgencyContext | undefined>>()
|
|
93
|
+
const runtimeContext = (directory: string) => {
|
|
94
|
+
let context = contexts.get(directory)
|
|
95
|
+
if (!context) {
|
|
96
|
+
context = agencyContext(pi, directory).catch(() => undefined)
|
|
97
|
+
contexts.set(directory, context)
|
|
98
|
+
}
|
|
99
|
+
return context
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
pi.on("resources_discover", async (event) => {
|
|
103
|
+
const context = await runtimeContext(event.cwd)
|
|
104
|
+
const root = process.env.AGENCY_WORKBASE ?? context?.root ?? discoverWorkbase(event.cwd)
|
|
105
|
+
const checkout = process.env.AGENCY_WRITABLE_CHECKOUT ?? context?.checkout ?? discoverCheckout(event.cwd, root)
|
|
106
|
+
if (!checkout) return
|
|
107
|
+
|
|
108
|
+
const skillPaths = [
|
|
109
|
+
join(checkout, ".claude", "skills"),
|
|
110
|
+
join(checkout, ".agents", "skills"),
|
|
111
|
+
join(checkout, ".opencode", "skill"),
|
|
112
|
+
join(checkout, ".opencode", "skills"),
|
|
113
|
+
join(checkout, CONFIG_DIR_NAME, "skills"),
|
|
114
|
+
].filter(existsSync)
|
|
115
|
+
if (skillPaths.length === 0) return
|
|
116
|
+
return { skillPaths: [...new Set(skillPaths)] }
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
120
|
+
const context = await runtimeContext(ctx.cwd)
|
|
121
|
+
const root = process.env.AGENCY_WORKBASE ?? context?.root ?? discoverWorkbase(ctx.cwd)
|
|
122
|
+
if (!root) return
|
|
123
|
+
const checkout = process.env.AGENCY_WRITABLE_CHECKOUT ?? context?.checkout ?? discoverCheckout(ctx.cwd, root)
|
|
124
|
+
const instructionsPath = join(root, ".agency", "AGENTS.md")
|
|
125
|
+
const instructions = existsSync(instructionsPath)
|
|
126
|
+
? readFileSync(instructionsPath, "utf8").trim()
|
|
127
|
+
: undefined
|
|
128
|
+
const activeTarget = process.env.AGENCY_TARGET
|
|
129
|
+
const worker = activeTarget && context?.target === activeTarget
|
|
130
|
+
? \`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.\`
|
|
131
|
+
: undefined
|
|
132
|
+
const access = \`The complete Agency workbase is available at \${root}. Use absolute paths under that root when workbase context is needed. Agency context remains the authority for writes.\`
|
|
133
|
+
const implementation = checkout
|
|
134
|
+
? \`Pi remains rooted in the task or phase directory for Agency instructions and context. Treat \${checkout} as the default implementation directory for source reads, edits, repository status, builds, tests, formatting, and other repository-local commands. Run Agency lifecycle and context commands from the task or phase directory. Any reference checkouts reported by Agency context are read-only.\`
|
|
135
|
+
: undefined
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
systemPrompt: [event.systemPrompt, instructions, access, worker, implementation]
|
|
139
|
+
.filter(Boolean)
|
|
140
|
+
.join("\\n\\n"),
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export default extension
|
|
146
|
+
`
|
|
147
|
+
|
|
148
|
+
const renderManagedWorkbasePiExtension = (content: string) =>
|
|
149
|
+
`// agency-managed: sha256=${checksum(content)}\n\n${content}`
|
|
150
|
+
|
|
151
|
+
export const managedWorkbasePiExtension = renderManagedWorkbasePiExtension(body)
|
|
152
|
+
|
|
153
|
+
export const canUpdateManagedWorkbasePiExtension = (content: string) => {
|
|
154
|
+
const match = content.match(managedHeaderPattern)
|
|
155
|
+
if (!match?.[1]) return false
|
|
156
|
+
|
|
157
|
+
return checksum(content.slice(match[0].length)) === match[1]
|
|
158
|
+
}
|
|
@@ -276,11 +276,11 @@ describe("workspace creation configuration", () => {
|
|
|
276
276
|
})
|
|
277
277
|
})
|
|
278
278
|
|
|
279
|
-
describe("
|
|
279
|
+
describe("agent configuration", () => {
|
|
280
280
|
test("accepts named argv commands with resume commands and environment", () => {
|
|
281
281
|
const config = Schema.decodeUnknownSync(WorkbaseConfig)({
|
|
282
282
|
version: 2,
|
|
283
|
-
|
|
283
|
+
agents: {
|
|
284
284
|
custom: {
|
|
285
285
|
command: ["agent"],
|
|
286
286
|
autoCommand: ["agent", "{prompt}"],
|
|
@@ -291,14 +291,14 @@ describe("runner configuration", () => {
|
|
|
291
291
|
},
|
|
292
292
|
})
|
|
293
293
|
|
|
294
|
-
expect(config.
|
|
294
|
+
expect(config.agents?.custom?.autoCommand).toEqual(["agent", "{prompt}"])
|
|
295
295
|
})
|
|
296
296
|
|
|
297
297
|
test("rejects shell strings in place of argv arrays", () => {
|
|
298
298
|
expect(() =>
|
|
299
299
|
Schema.decodeUnknownSync(WorkbaseConfig)({
|
|
300
300
|
version: 2,
|
|
301
|
-
|
|
301
|
+
agents: { custom: { command: "agent {prompt}" } },
|
|
302
302
|
}),
|
|
303
303
|
).toThrow()
|
|
304
304
|
})
|
|
@@ -423,7 +423,7 @@ describe("work status", () => {
|
|
|
423
423
|
describe("claim records", () => {
|
|
424
424
|
const record = {
|
|
425
425
|
claimant: "orchestrator",
|
|
426
|
-
|
|
426
|
+
agent: "agent",
|
|
427
427
|
sessionId: "job-1",
|
|
428
428
|
startedAt: "2026-07-17T12:00:00.000Z",
|
|
429
429
|
targetRevision: "0".repeat(64),
|
package/src/workbase/schemas.ts
CHANGED
|
@@ -52,7 +52,7 @@ export const DocumentRevision = Schema.String.pipe(
|
|
|
52
52
|
|
|
53
53
|
export const ClaimRecord = Schema.Struct({
|
|
54
54
|
claimant: NonEmptyString,
|
|
55
|
-
|
|
55
|
+
agent: NonEmptyString,
|
|
56
56
|
sessionId: NonEmptyString,
|
|
57
57
|
startedAt: IsoTimestamp,
|
|
58
58
|
targetRevision: DocumentRevision,
|
|
@@ -110,7 +110,7 @@ export const WorkbaseConfig = Schema.Struct({
|
|
|
110
110
|
chooserCommand: Schema.optional(Schema.NonEmptyArray(NonEmptyString)),
|
|
111
111
|
worktreeCreateCommand: Schema.optional(Schema.NonEmptyArray(NonEmptyString)),
|
|
112
112
|
workspaceCreateCommand: Schema.optional(Schema.NonEmptyArray(NonEmptyString)),
|
|
113
|
-
|
|
113
|
+
agents: Schema.optional(
|
|
114
114
|
Schema.Record({
|
|
115
115
|
key: EntityId,
|
|
116
116
|
value: Schema.Struct({
|
|
@@ -146,6 +146,12 @@ export const WorkbaseRegistry = Schema.Struct({
|
|
|
146
146
|
defaultId: Schema.optional(EntityId),
|
|
147
147
|
})
|
|
148
148
|
|
|
149
|
+
export const GlobalConfig = Schema.Struct({
|
|
150
|
+
agent: Schema.optional(
|
|
151
|
+
Schema.Literal("opencode2", "opencode", "pi", "claude"),
|
|
152
|
+
),
|
|
153
|
+
})
|
|
154
|
+
|
|
149
155
|
export const Dependency = Schema.Struct({
|
|
150
156
|
id: EntityId,
|
|
151
157
|
dependsOn: Schema.optional(Schema.Array(EntityId)),
|