@markjaquith/agency 2.18.0 → 2.20.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 +55 -9
- package/cli.ts +22 -0
- package/package.json +1 -1
- package/skills/agency/SKILL.md +4 -1
- package/src/cli-parser.test.ts +53 -2
- package/src/cli-parser.ts +78 -14
- package/src/cli.test.ts +11 -1
- package/src/commands/epic.test.ts +22 -0
- package/src/commands/epic.ts +38 -2
- package/src/commands/phase.ts +52 -2
- package/src/commands/status.test.ts +44 -0
- package/src/commands/status.ts +51 -2
- package/src/commands/task-phase.test.ts +20 -0
- package/src/commands/task.ts +49 -2
- package/src/commands/work.test.ts +105 -16
- package/src/commands/work.ts +99 -28
- package/src/services/WorkbaseService.test.ts +19 -0
- package/src/services/WorkbaseService.ts +12 -0
- package/src/utils/table.ts +19 -0
- package/src/work-view.test.ts +151 -0
- package/src/work-view.ts +253 -0
- package/src/workbase/runner-command.test.ts +79 -0
- package/src/workbase/runner-command.ts +118 -0
- package/src/workbase/schemas.test.ts +26 -0
- package/src/workbase/schemas.ts +15 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import {
|
|
3
|
+
printableEnvironment,
|
|
4
|
+
resolveRunnerCommand,
|
|
5
|
+
runnerEnvironment,
|
|
6
|
+
validateRunners,
|
|
7
|
+
} from "./runner-command"
|
|
8
|
+
|
|
9
|
+
const variables = {
|
|
10
|
+
prompt: "Read the task.",
|
|
11
|
+
workbase: "/workbase",
|
|
12
|
+
target: "execution-unit:phase/task/build",
|
|
13
|
+
task: "task",
|
|
14
|
+
phase: "build",
|
|
15
|
+
claimant: "orchestrator",
|
|
16
|
+
sessionId: "session-1",
|
|
17
|
+
claimRevision: "revision-1",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("runner commands", () => {
|
|
21
|
+
test("uses deterministic fresh and resume commands for built-in presets", () => {
|
|
22
|
+
expect(
|
|
23
|
+
resolveRunnerCommand("opencode", undefined, variables, false).argv,
|
|
24
|
+
).toEqual(["opencode", "--prompt", "Read the task."])
|
|
25
|
+
expect(
|
|
26
|
+
resolveRunnerCommand("opencode", undefined, variables, true).argv,
|
|
27
|
+
).toEqual(["opencode", "--continue", "--prompt", "Read the task."])
|
|
28
|
+
expect(
|
|
29
|
+
resolveRunnerCommand("claude", undefined, variables, true).argv,
|
|
30
|
+
).toEqual(["claude", "--continue", "Read the task."])
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test("expands configured argv and environment without a shell", () => {
|
|
34
|
+
const resolved = resolveRunnerCommand(
|
|
35
|
+
"custom",
|
|
36
|
+
{
|
|
37
|
+
custom: {
|
|
38
|
+
command: ["agent", "--target={target}", "{prompt}"],
|
|
39
|
+
environment: { CUSTOM_SESSION: "{sessionId}" },
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
variables,
|
|
43
|
+
false,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
expect(resolved).toEqual({
|
|
47
|
+
argv: [
|
|
48
|
+
"agent",
|
|
49
|
+
"--target=execution-unit:phase/task/build",
|
|
50
|
+
"Read the task.",
|
|
51
|
+
],
|
|
52
|
+
environment: { CUSTOM_SESSION: "session-1" },
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test("rejects unknown placeholders", () => {
|
|
57
|
+
expect(() =>
|
|
58
|
+
validateRunners({ custom: { command: ["agent", "{unknown}"] } }),
|
|
59
|
+
).toThrow("Unknown runner 'custom' placeholder: {unknown}")
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test("provides normalized Agency environment and filters secret values", () => {
|
|
63
|
+
const environment = {
|
|
64
|
+
...runnerEnvironment("custom", variables),
|
|
65
|
+
VISIBLE: "yes",
|
|
66
|
+
ACCESS_TOKEN: "secret",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
expect(environment).toMatchObject({
|
|
70
|
+
AGENCY_RUNNER: "custom",
|
|
71
|
+
AGENCY_CLAIMANT: "orchestrator",
|
|
72
|
+
AGENCY_TARGET: "execution-unit:phase/task/build",
|
|
73
|
+
AGENCY_TASK_ID: "task",
|
|
74
|
+
AGENCY_PHASE_ID: "build",
|
|
75
|
+
})
|
|
76
|
+
expect(printableEnvironment(environment).VISIBLE).toBe("yes")
|
|
77
|
+
expect(printableEnvironment(environment).ACCESS_TOKEN).toBeUndefined()
|
|
78
|
+
})
|
|
79
|
+
})
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { WorkbaseConfig } from "./schemas"
|
|
2
|
+
|
|
3
|
+
export interface RunnerCommandVariables {
|
|
4
|
+
readonly prompt: string
|
|
5
|
+
readonly workbase: string
|
|
6
|
+
readonly target: string
|
|
7
|
+
readonly task: string
|
|
8
|
+
readonly phase: string
|
|
9
|
+
readonly claimant: string
|
|
10
|
+
readonly sessionId: string
|
|
11
|
+
readonly claimRevision: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface RunnerDefinition {
|
|
15
|
+
readonly command: readonly string[]
|
|
16
|
+
readonly resumeCommand?: readonly string[]
|
|
17
|
+
readonly environment?: Readonly<Record<string, string>>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const PLACEHOLDERS = new Set<keyof RunnerCommandVariables>([
|
|
21
|
+
"prompt",
|
|
22
|
+
"workbase",
|
|
23
|
+
"target",
|
|
24
|
+
"task",
|
|
25
|
+
"phase",
|
|
26
|
+
"claimant",
|
|
27
|
+
"sessionId",
|
|
28
|
+
"claimRevision",
|
|
29
|
+
])
|
|
30
|
+
|
|
31
|
+
const BUILTIN_RUNNERS: Readonly<Record<string, RunnerDefinition>> = {
|
|
32
|
+
opencode: {
|
|
33
|
+
command: ["opencode", "--prompt", "{prompt}"],
|
|
34
|
+
resumeCommand: ["opencode", "--continue", "--prompt", "{prompt}"],
|
|
35
|
+
},
|
|
36
|
+
claude: {
|
|
37
|
+
command: ["claude", "{prompt}"],
|
|
38
|
+
resumeCommand: ["claude", "--continue", "{prompt}"],
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const validateTemplate = (runner: string, value: string) => {
|
|
43
|
+
for (const match of value.matchAll(/\{([^{}]+)\}/g)) {
|
|
44
|
+
const placeholder = match[1]!
|
|
45
|
+
if (!PLACEHOLDERS.has(placeholder as keyof RunnerCommandVariables)) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Unknown runner '${runner}' placeholder: {${placeholder}}`,
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const validateRunners = (runners: WorkbaseConfig["runners"]): void => {
|
|
54
|
+
for (const [name, runner] of Object.entries(runners ?? {})) {
|
|
55
|
+
for (const value of [
|
|
56
|
+
...runner.command,
|
|
57
|
+
...(runner.resumeCommand ?? []),
|
|
58
|
+
...Object.values(runner.environment ?? {}),
|
|
59
|
+
]) {
|
|
60
|
+
validateTemplate(name, value)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const expand = (value: string, variables: RunnerCommandVariables) =>
|
|
66
|
+
value.replaceAll(
|
|
67
|
+
/\{([^{}]+)\}/g,
|
|
68
|
+
(match, placeholder: string) =>
|
|
69
|
+
variables[placeholder as keyof RunnerCommandVariables] ?? match,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
export const resolveRunnerCommand = (
|
|
73
|
+
name: string,
|
|
74
|
+
configured: WorkbaseConfig["runners"],
|
|
75
|
+
variables: RunnerCommandVariables,
|
|
76
|
+
resume: boolean,
|
|
77
|
+
) => {
|
|
78
|
+
validateRunners(configured)
|
|
79
|
+
const definition = configured?.[name] ?? BUILTIN_RUNNERS[name]
|
|
80
|
+
if (!definition) throw new Error(`Unknown runner: ${name}`)
|
|
81
|
+
const template =
|
|
82
|
+
resume && definition.resumeCommand
|
|
83
|
+
? definition.resumeCommand
|
|
84
|
+
: definition.command
|
|
85
|
+
const argv = template.map((argument) => expand(argument, variables))
|
|
86
|
+
const environment = Object.fromEntries(
|
|
87
|
+
Object.entries(definition.environment ?? {}).map(([key, value]) => [
|
|
88
|
+
key,
|
|
89
|
+
expand(value, variables),
|
|
90
|
+
]),
|
|
91
|
+
)
|
|
92
|
+
return { argv, environment }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const runnerEnvironment = (
|
|
96
|
+
runner: string,
|
|
97
|
+
variables: RunnerCommandVariables,
|
|
98
|
+
): Record<string, string> => ({
|
|
99
|
+
AGENCY_RUNNER: runner,
|
|
100
|
+
AGENCY_CLAIMANT: variables.claimant,
|
|
101
|
+
AGENCY_SESSION_ID: variables.sessionId,
|
|
102
|
+
AGENCY_CLAIM_REVISION: variables.claimRevision,
|
|
103
|
+
AGENCY_WORKBASE: variables.workbase,
|
|
104
|
+
AGENCY_TARGET: variables.target,
|
|
105
|
+
AGENCY_TASK_ID: variables.task,
|
|
106
|
+
AGENCY_PHASE_ID: variables.phase,
|
|
107
|
+
AGENCY_PROMPT: variables.prompt,
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const SECRET_NAME =
|
|
111
|
+
/(secret|token|password|credential|api[_-]?key|private[_-]?key)/i
|
|
112
|
+
|
|
113
|
+
export const printableEnvironment = (environment: Record<string, string>) =>
|
|
114
|
+
Object.fromEntries(
|
|
115
|
+
Object.entries(environment)
|
|
116
|
+
.filter(([key]) => !SECRET_NAME.test(key))
|
|
117
|
+
.sort(([left], [right]) => left.localeCompare(right)),
|
|
118
|
+
)
|
|
@@ -78,6 +78,32 @@ describe("body-of-work descriptions", () => {
|
|
|
78
78
|
})
|
|
79
79
|
})
|
|
80
80
|
|
|
81
|
+
describe("runner configuration", () => {
|
|
82
|
+
test("accepts named argv commands with resume commands and environment", () => {
|
|
83
|
+
const config = Schema.decodeUnknownSync(WorkbaseConfig)({
|
|
84
|
+
version: 2,
|
|
85
|
+
runners: {
|
|
86
|
+
custom: {
|
|
87
|
+
command: ["agent", "{prompt}"],
|
|
88
|
+
resumeCommand: ["agent", "resume", "{sessionId}"],
|
|
89
|
+
environment: { CUSTOM_TARGET: "{target}" },
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
expect(config.runners?.custom?.command).toEqual(["agent", "{prompt}"])
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test("rejects shell strings in place of argv arrays", () => {
|
|
98
|
+
expect(() =>
|
|
99
|
+
Schema.decodeUnknownSync(WorkbaseConfig)({
|
|
100
|
+
version: 2,
|
|
101
|
+
runners: { custom: { command: "agent {prompt}" } },
|
|
102
|
+
}),
|
|
103
|
+
).toThrow()
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
81
107
|
describe("work status", () => {
|
|
82
108
|
const supportedStatuses: Record<WorkStatus, true> = {
|
|
83
109
|
open: true,
|
package/src/workbase/schemas.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { Schema } from "@effect/schema"
|
|
2
2
|
|
|
3
3
|
const NonEmptyString = Schema.String.pipe(Schema.minLength(1))
|
|
4
|
+
const EnvironmentName = NonEmptyString.pipe(
|
|
5
|
+
Schema.pattern(/^[A-Za-z_][A-Za-z0-9_]*$/),
|
|
6
|
+
)
|
|
4
7
|
|
|
5
8
|
const Description = Schema.optional(NonEmptyString)
|
|
6
9
|
|
|
@@ -52,6 +55,18 @@ export const WorkbaseConfig = Schema.Struct({
|
|
|
52
55
|
version: Schema.Literal(2),
|
|
53
56
|
chooserCommand: Schema.optional(Schema.NonEmptyArray(NonEmptyString)),
|
|
54
57
|
worktreeCreateCommand: Schema.optional(Schema.NonEmptyArray(NonEmptyString)),
|
|
58
|
+
runners: Schema.optional(
|
|
59
|
+
Schema.Record({
|
|
60
|
+
key: EntityId,
|
|
61
|
+
value: Schema.Struct({
|
|
62
|
+
command: Schema.NonEmptyArray(NonEmptyString),
|
|
63
|
+
resumeCommand: Schema.optional(Schema.NonEmptyArray(NonEmptyString)),
|
|
64
|
+
environment: Schema.optional(
|
|
65
|
+
Schema.Record({ key: EnvironmentName, value: Schema.String }),
|
|
66
|
+
),
|
|
67
|
+
}),
|
|
68
|
+
}),
|
|
69
|
+
),
|
|
55
70
|
})
|
|
56
71
|
|
|
57
72
|
export const LegacyWorkbaseRegistry = Schema.Struct({
|