@markjaquith/agency 2.13.0 → 2.15.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 +62 -8
- package/cli.ts +66 -0
- package/package.json +1 -1
- package/schemas/agency-graph-v1.schema.json +28 -1
- package/skills/agency/SKILL.md +14 -3
- package/src/cli-parser.test.ts +81 -0
- package/src/cli-parser.ts +71 -0
- package/src/cli.test.ts +85 -0
- package/src/commands/claim.ts +97 -0
- package/src/commands/phase.ts +1 -1
- package/src/commands/task-phase.test.ts +4 -4
- package/src/commands/task.ts +32 -35
- package/src/commands/validate.ts +1 -6
- package/src/commands/work.test.ts +46 -12
- package/src/commands/work.ts +38 -19
- package/src/protocol.test.ts +25 -0
- package/src/protocol.ts +16 -0
- package/src/services/ClaimService.test.ts +270 -0
- package/src/services/ClaimService.ts +446 -0
- package/src/services/PhaseService.ts +13 -0
- package/src/services/TaskPhaseService.test.ts +10 -9
- package/src/services/TaskService.ts +11 -0
- package/src/test-utils.ts +2 -0
- package/src/utils/chooser.test.ts +108 -0
- package/src/utils/chooser.ts +222 -0
- package/src/workbase/AGENTS.md +2 -2
- package/src/workbase/document-revision.ts +2 -0
- package/src/workbase/frontmatter.ts +59 -53
- package/src/workbase/schemas.test.ts +39 -0
- package/src/workbase/schemas.ts +22 -0
- package/src/workbase/work-target.test.ts +10 -0
- package/src/workbase/work-target.ts +45 -35
- package/src/workbase/workbase-choice.ts +12 -37
|
@@ -172,13 +172,13 @@ describe("task and phase command JSON output", () => {
|
|
|
172
172
|
runTestEffect(
|
|
173
173
|
phase({
|
|
174
174
|
subcommand: "status",
|
|
175
|
-
args: ["multi", "first", "
|
|
175
|
+
args: ["multi", "first", "done"],
|
|
176
176
|
cwd: root,
|
|
177
177
|
json: true,
|
|
178
178
|
}),
|
|
179
179
|
),
|
|
180
180
|
)
|
|
181
|
-
expect(JSON.parse(phaseLogs[0]!).data.status).toBe("
|
|
181
|
+
expect(JSON.parse(phaseLogs[0]!).data.status).toBe("done")
|
|
182
182
|
|
|
183
183
|
await runTestEffect(
|
|
184
184
|
task({
|
|
@@ -196,13 +196,13 @@ describe("task and phase command JSON output", () => {
|
|
|
196
196
|
runTestEffect(
|
|
197
197
|
task({
|
|
198
198
|
subcommand: "status",
|
|
199
|
-
args: ["single-status", "
|
|
199
|
+
args: ["single-status", "done"],
|
|
200
200
|
cwd: root,
|
|
201
201
|
json: true,
|
|
202
202
|
}),
|
|
203
203
|
),
|
|
204
204
|
)
|
|
205
|
-
expect(JSON.parse(taskLogs[0]!).data.status).toBe("
|
|
205
|
+
expect(JSON.parse(taskLogs[0]!).data.status).toBe("done")
|
|
206
206
|
})
|
|
207
207
|
|
|
208
208
|
test("converts a single-phase task with an explicit first phase ID", async () => {
|
package/src/commands/task.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { EpicService } from "../services/EpicService"
|
|
|
6
6
|
import { RepositoryService } from "../services/RepositoryService"
|
|
7
7
|
import { createLoggers } from "../utils/effect"
|
|
8
8
|
import { parseRepositoryReferences } from "../workbase/repository-reference"
|
|
9
|
+
import { WorkbaseService } from "../services/WorkbaseService"
|
|
10
|
+
import { choose } from "../utils/chooser"
|
|
9
11
|
|
|
10
12
|
interface TaskOptions extends BaseCommandOptions {
|
|
11
13
|
readonly subcommand?: string
|
|
@@ -29,7 +31,9 @@ export interface TaskInteraction {
|
|
|
29
31
|
) => Effect.Effect<string | null, Error>
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
const defaultInteraction
|
|
34
|
+
const defaultInteraction = (
|
|
35
|
+
chooserCommand?: readonly string[],
|
|
36
|
+
): TaskInteraction => ({
|
|
33
37
|
text: (prompt) =>
|
|
34
38
|
Effect.tryPromise({
|
|
35
39
|
try: async () => {
|
|
@@ -46,37 +50,23 @@ const defaultInteraction: TaskInteraction = {
|
|
|
46
50
|
catch: (cause) => new Error("Failed to read task input", { cause }),
|
|
47
51
|
}),
|
|
48
52
|
select: (prompt, choices) =>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const [exitCode, output] = await Promise.all([
|
|
60
|
-
process.exited,
|
|
61
|
-
new Response(process.stdout).text(),
|
|
62
|
-
])
|
|
63
|
-
if (exitCode === 1 || exitCode === 130) return null
|
|
64
|
-
if (exitCode !== 0) throw new Error(`fzf exited with code ${exitCode}`)
|
|
65
|
-
return output.trim() || null
|
|
66
|
-
},
|
|
67
|
-
catch: (cause) =>
|
|
68
|
-
new Error("Failed to select task input with fzf", { cause }),
|
|
69
|
-
}),
|
|
70
|
-
}
|
|
53
|
+
choose(
|
|
54
|
+
prompt,
|
|
55
|
+
choices.map((choice, index) => ({
|
|
56
|
+
key: String(index),
|
|
57
|
+
label: choice,
|
|
58
|
+
value: choice,
|
|
59
|
+
})),
|
|
60
|
+
chooserCommand,
|
|
61
|
+
),
|
|
62
|
+
})
|
|
71
63
|
|
|
72
|
-
export const task = (
|
|
73
|
-
options: TaskOptions,
|
|
74
|
-
interaction: TaskInteraction = defaultInteraction,
|
|
75
|
-
) =>
|
|
64
|
+
export const task = (options: TaskOptions, interaction?: TaskInteraction) =>
|
|
76
65
|
Effect.gen(function* () {
|
|
77
66
|
const tasks = yield* TaskService
|
|
78
67
|
const epics = yield* EpicService
|
|
79
68
|
const repositories = yield* RepositoryService
|
|
69
|
+
const workbase = yield* WorkbaseService
|
|
80
70
|
const { log } = createLoggers(options)
|
|
81
71
|
const cwd = options.cwd ?? process.cwd()
|
|
82
72
|
|
|
@@ -89,8 +79,13 @@ export const task = (
|
|
|
89
79
|
),
|
|
90
80
|
)
|
|
91
81
|
}
|
|
82
|
+
const activeInteraction =
|
|
83
|
+
interaction ??
|
|
84
|
+
defaultInteraction(
|
|
85
|
+
(yield* workbase.loadConfig(cwd)).config.chooserCommand,
|
|
86
|
+
)
|
|
92
87
|
const id =
|
|
93
|
-
options.args[0] ?? (yield*
|
|
88
|
+
options.args[0] ?? (yield* activeInteraction.text("Task ID: ")).trim()
|
|
94
89
|
if (!id) {
|
|
95
90
|
return yield* Effect.fail(new Error("Task ID is required"))
|
|
96
91
|
}
|
|
@@ -103,18 +98,20 @@ export const task = (
|
|
|
103
98
|
|
|
104
99
|
if (options.ticketUrl === undefined) {
|
|
105
100
|
ticketUrl =
|
|
106
|
-
(yield*
|
|
101
|
+
(yield* activeInteraction.text("Ticket URL (optional): ")).trim() ||
|
|
102
|
+
null
|
|
107
103
|
}
|
|
108
104
|
if (options.description === undefined) {
|
|
109
105
|
description =
|
|
110
|
-
(yield*
|
|
111
|
-
|
|
106
|
+
(yield* activeInteraction.text(
|
|
107
|
+
"Description (optional): ",
|
|
108
|
+
)).trim() || undefined
|
|
112
109
|
}
|
|
113
110
|
if (options.epic === undefined) {
|
|
114
111
|
const epicRecords = yield* epics.list(cwd)
|
|
115
112
|
if (epicRecords.length > 0) {
|
|
116
113
|
const none = "(none)"
|
|
117
|
-
const selected = yield*
|
|
114
|
+
const selected = yield* activeInteraction.select("Parent epic", [
|
|
118
115
|
none,
|
|
119
116
|
...epicRecords.map((record) => record.id),
|
|
120
117
|
])
|
|
@@ -125,7 +122,7 @@ export const task = (
|
|
|
125
122
|
}
|
|
126
123
|
}
|
|
127
124
|
if (options.multiPhase === undefined) {
|
|
128
|
-
const selected = yield*
|
|
125
|
+
const selected = yield* activeInteraction.select("Task type", [
|
|
129
126
|
"single-phase",
|
|
130
127
|
"multi-phase",
|
|
131
128
|
])
|
|
@@ -144,7 +141,7 @@ export const task = (
|
|
|
144
141
|
),
|
|
145
142
|
)
|
|
146
143
|
}
|
|
147
|
-
const selected = yield*
|
|
144
|
+
const selected = yield* activeInteraction.select(
|
|
148
145
|
"Writable repository",
|
|
149
146
|
records.map((record) => record.alias),
|
|
150
147
|
)
|
|
@@ -269,7 +266,7 @@ Subcommands:
|
|
|
269
266
|
create <id> Create a task without prompting
|
|
270
267
|
list List tasks
|
|
271
268
|
show <id> Show a task
|
|
272
|
-
status <id> <status> Set open,
|
|
269
|
+
status <id> <status> Set open, done, or dropped
|
|
273
270
|
|
|
274
271
|
Create options:
|
|
275
272
|
--ticket-url <url> External ticket URL (optional)
|
package/src/commands/validate.ts
CHANGED
|
@@ -32,12 +32,7 @@ export const validate = (
|
|
|
32
32
|
const startPath = options.path ?? options.cwd ?? process.cwd()
|
|
33
33
|
const root = options.path
|
|
34
34
|
? yield* workbase.discover(startPath)
|
|
35
|
-
: yield* resolveWorkbase(
|
|
36
|
-
startPath,
|
|
37
|
-
log,
|
|
38
|
-
pick,
|
|
39
|
-
options.inputAllowed ?? true,
|
|
40
|
-
)
|
|
35
|
+
: yield* resolveWorkbase(startPath, pick, options.inputAllowed ?? true)
|
|
41
36
|
if (!root) return
|
|
42
37
|
const report = yield* workbase.validate(root)
|
|
43
38
|
|
|
@@ -6,6 +6,7 @@ import { EpicService } from "../services/EpicService"
|
|
|
6
6
|
import { TaskService } from "../services/TaskService"
|
|
7
7
|
import { PhaseService } from "../services/PhaseService"
|
|
8
8
|
import { WorktreeService } from "../services/WorktreeService"
|
|
9
|
+
import { ClaimService } from "../services/ClaimService"
|
|
9
10
|
import { captureErrors, captureLogs } from "../test-utils"
|
|
10
11
|
import { work, workPrepare } from "./work"
|
|
11
12
|
import type { PickWorkTarget } from "../workbase/work-target"
|
|
@@ -45,7 +46,8 @@ const multiPhaseWorkspace: ExecutionWorkspace = {
|
|
|
45
46
|
interface HarnessOptions {
|
|
46
47
|
readonly workspace?: ExecutionWorkspace
|
|
47
48
|
readonly materializeError?: Error
|
|
48
|
-
readonly available?: Partial<Record<"opencode" | "claude"
|
|
49
|
+
readonly available?: Partial<Record<"opencode" | "claude", boolean>>
|
|
50
|
+
readonly chooserCommand?: readonly string[]
|
|
49
51
|
readonly multiPhaseTasks?: readonly string[]
|
|
50
52
|
readonly epicRecords?: readonly any[]
|
|
51
53
|
readonly taskRecords?: readonly any[]
|
|
@@ -92,6 +94,14 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
92
94
|
})
|
|
93
95
|
: Effect.succeed("/workbase"),
|
|
94
96
|
listRegistered: () => Effect.succeed(options.registeredWorkbases ?? []),
|
|
97
|
+
loadConfig: () =>
|
|
98
|
+
Effect.succeed({
|
|
99
|
+
root: "/workbase",
|
|
100
|
+
config: {
|
|
101
|
+
version: 2 as const,
|
|
102
|
+
chooserCommand: options.chooserCommand,
|
|
103
|
+
},
|
|
104
|
+
}),
|
|
95
105
|
}
|
|
96
106
|
const epics = {
|
|
97
107
|
show: (id: string) =>
|
|
@@ -133,6 +143,30 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
133
143
|
return Effect.void
|
|
134
144
|
},
|
|
135
145
|
}
|
|
146
|
+
const claims = {
|
|
147
|
+
inspect: (taskId: string, phaseId?: string) =>
|
|
148
|
+
Effect.succeed({
|
|
149
|
+
target: {
|
|
150
|
+
kind: phaseId ? "phase" : "task",
|
|
151
|
+
taskId,
|
|
152
|
+
phaseId,
|
|
153
|
+
path: phaseId
|
|
154
|
+
? `/workbase/tasks/${taskId}/phases/${phaseId}/PHASE.md`
|
|
155
|
+
: `/workbase/tasks/${taskId}/TASK.md`,
|
|
156
|
+
label: phaseId ? `phase '${taskId}/${phaseId}'` : `task '${taskId}'`,
|
|
157
|
+
},
|
|
158
|
+
revision: "0".repeat(64),
|
|
159
|
+
data: {},
|
|
160
|
+
}),
|
|
161
|
+
claim: (input: { taskId: string; phaseId?: string }) => {
|
|
162
|
+
statusUpdates.push(
|
|
163
|
+
input.phaseId
|
|
164
|
+
? `phase:${input.taskId}:${input.phaseId}:working`
|
|
165
|
+
: `task:${input.taskId}:working`,
|
|
166
|
+
)
|
|
167
|
+
return Effect.succeed({ revision: "1".repeat(64) })
|
|
168
|
+
},
|
|
169
|
+
}
|
|
136
170
|
const fs = {
|
|
137
171
|
isDirectory: (path: string) =>
|
|
138
172
|
Effect.succeed(options.existingDirectories?.includes(path) ?? true),
|
|
@@ -171,6 +205,7 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
171
205
|
Effect.provideService(EpicService, epics as never),
|
|
172
206
|
Effect.provideService(TaskService, tasks as never),
|
|
173
207
|
Effect.provideService(PhaseService, phases as never),
|
|
208
|
+
Effect.provideService(ClaimService, claims as never),
|
|
174
209
|
) as Effect.Effect<void, unknown, never>,
|
|
175
210
|
)
|
|
176
211
|
const runPrepare = (commandOptions: Parameters<typeof workPrepare>[0]) =>
|
|
@@ -333,7 +368,7 @@ describe("work command", () => {
|
|
|
333
368
|
expect(harness.launches[0]?.cwd).toBe(singlePhaseWorkspace.writablePath)
|
|
334
369
|
})
|
|
335
370
|
|
|
336
|
-
test("selects a target
|
|
371
|
+
test("selects a target when no directory is provided", async () => {
|
|
337
372
|
const phase = {
|
|
338
373
|
taskId: "delivery",
|
|
339
374
|
id: "build",
|
|
@@ -359,7 +394,6 @@ describe("work command", () => {
|
|
|
359
394
|
await harness.run({ cwd: "/workbase/tasks/example", opencode: true }, pick)
|
|
360
395
|
|
|
361
396
|
expect(harness.events).toEqual([
|
|
362
|
-
"probe:fzf",
|
|
363
397
|
"materialize",
|
|
364
398
|
"probe:opencode",
|
|
365
399
|
"launch:opencode",
|
|
@@ -416,7 +450,6 @@ describe("work command", () => {
|
|
|
416
450
|
|
|
417
451
|
expect(selections).toEqual([["/first", "/workbase"]])
|
|
418
452
|
expect(harness.events).toEqual([
|
|
419
|
-
"probe:fzf",
|
|
420
453
|
"materialize",
|
|
421
454
|
"probe:opencode",
|
|
422
455
|
"launch:opencode",
|
|
@@ -450,9 +483,9 @@ describe("work command", () => {
|
|
|
450
483
|
expect(harness.events).toEqual([])
|
|
451
484
|
})
|
|
452
485
|
|
|
453
|
-
test("
|
|
486
|
+
test("passes the configured chooser command to the shared picker", async () => {
|
|
454
487
|
const harness = createHarness({
|
|
455
|
-
|
|
488
|
+
chooserCommand: ["gum", "filter"],
|
|
456
489
|
epicRecords: [
|
|
457
490
|
{
|
|
458
491
|
id: "delivery",
|
|
@@ -461,14 +494,15 @@ describe("work command", () => {
|
|
|
461
494
|
},
|
|
462
495
|
],
|
|
463
496
|
})
|
|
497
|
+
let command: readonly string[] | undefined
|
|
498
|
+
const pick: PickWorkTarget = (_choices, chooserCommand) => {
|
|
499
|
+
command = chooserCommand
|
|
500
|
+
return Effect.succeed(null)
|
|
501
|
+
}
|
|
464
502
|
|
|
465
|
-
|
|
466
|
-
await expect(harness.run({ cwd: "/workbase" })).rejects.toThrow(
|
|
467
|
-
"fzf is required",
|
|
468
|
-
)
|
|
469
|
-
})
|
|
503
|
+
await harness.run({ cwd: "/workbase" }, pick)
|
|
470
504
|
|
|
471
|
-
expect(
|
|
505
|
+
expect(command).toEqual(["gum", "filter"])
|
|
472
506
|
expect(harness.launches).toEqual([])
|
|
473
507
|
})
|
|
474
508
|
|
package/src/commands/work.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { WorkbaseService } from "../services/WorkbaseService"
|
|
|
7
7
|
import { EpicService } from "../services/EpicService"
|
|
8
8
|
import { TaskService } from "../services/TaskService"
|
|
9
9
|
import { PhaseService } from "../services/PhaseService"
|
|
10
|
+
import { ClaimService } from "../services/ClaimService"
|
|
10
11
|
import { createLoggers } from "../utils/effect"
|
|
11
12
|
import { execvp } from "../utils/exec"
|
|
12
13
|
import { createProgress, type Progress } from "../utils/progress"
|
|
@@ -60,6 +61,8 @@ export const work = (
|
|
|
60
61
|
new Error("Cannot use both --opencode and --claude"),
|
|
61
62
|
)
|
|
62
63
|
}
|
|
64
|
+
const previousSessionId = process.env.AGENCY_SESSION_ID
|
|
65
|
+
const previousClaimRevision = process.env.AGENCY_CLAIM_REVISION
|
|
63
66
|
if (
|
|
64
67
|
options.epicId &&
|
|
65
68
|
(options.directory || options.taskId || options.phaseId)
|
|
@@ -75,6 +78,7 @@ export const work = (
|
|
|
75
78
|
const epics = yield* EpicService
|
|
76
79
|
const tasks = yield* TaskService
|
|
77
80
|
const phases = yield* PhaseService
|
|
81
|
+
const claims = yield* ClaimService
|
|
78
82
|
const { log, verboseLog } = createLoggers(options)
|
|
79
83
|
const cwd = options.cwd ?? process.cwd()
|
|
80
84
|
const directoryPath = options.directory
|
|
@@ -85,7 +89,7 @@ export const work = (
|
|
|
85
89
|
: false
|
|
86
90
|
const startPath = isDirectory && directoryPath ? directoryPath : cwd
|
|
87
91
|
const inputAllowed = options.inputAllowed ?? true
|
|
88
|
-
const root = yield* resolveWorkbase(startPath,
|
|
92
|
+
const root = yield* resolveWorkbase(startPath, pickBase, inputAllowed)
|
|
89
93
|
if (!root) return
|
|
90
94
|
|
|
91
95
|
let target: WorkTarget | null = null
|
|
@@ -174,18 +178,8 @@ export const work = (
|
|
|
174
178
|
new Error("No epics, tasks, or phases found in this workbase"),
|
|
175
179
|
)
|
|
176
180
|
}
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
})
|
|
180
|
-
if (fzf.exitCode !== 0) {
|
|
181
|
-
for (const choice of choices) log(choice.label)
|
|
182
|
-
return yield* Effect.fail(
|
|
183
|
-
new Error(
|
|
184
|
-
"fzf is required to select a work target; install fzf or provide a directory explicitly",
|
|
185
|
-
),
|
|
186
|
-
)
|
|
187
|
-
}
|
|
188
|
-
target = yield* pick(choices)
|
|
181
|
+
const { config } = yield* workbase.loadConfig(root)
|
|
182
|
+
target = yield* pick(choices, config.chooserCommand)
|
|
189
183
|
if (!target) return
|
|
190
184
|
}
|
|
191
185
|
|
|
@@ -229,10 +223,27 @@ export const work = (
|
|
|
229
223
|
if (available.exitCode !== 0) {
|
|
230
224
|
return yield* Effect.fail(new Error(`${cli} CLI tool not found`))
|
|
231
225
|
}
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
226
|
+
if (
|
|
227
|
+
target.kind === "phase" ||
|
|
228
|
+
(target.kind === "task" && !target.multiPhase)
|
|
229
|
+
) {
|
|
230
|
+
const phaseId = target.kind === "phase" ? target.phaseId : undefined
|
|
231
|
+
const current = yield* claims.inspect(target.taskId, phaseId, root)
|
|
232
|
+
const sessionId =
|
|
233
|
+
process.env.AGENCY_SESSION_ID ?? `${process.pid}-${Date.now()}`
|
|
234
|
+
const acquired = yield* claims.claim(
|
|
235
|
+
{
|
|
236
|
+
taskId: target.taskId,
|
|
237
|
+
...(phaseId ? { phaseId } : {}),
|
|
238
|
+
claimant: process.env.AGENCY_CLAIMANT ?? process.env.USER ?? "agency",
|
|
239
|
+
runner: process.env.AGENCY_RUNNER ?? cli,
|
|
240
|
+
sessionId,
|
|
241
|
+
revision: current.revision,
|
|
242
|
+
},
|
|
243
|
+
root,
|
|
244
|
+
)
|
|
245
|
+
process.env.AGENCY_SESSION_ID = sessionId
|
|
246
|
+
process.env.AGENCY_CLAIM_REVISION = acquired.revision
|
|
236
247
|
}
|
|
237
248
|
|
|
238
249
|
const args =
|
|
@@ -240,7 +251,15 @@ export const work = (
|
|
|
240
251
|
verboseLog(
|
|
241
252
|
`Launching command: ${formatCommand([cli, ...args])} (cwd: ${launchPath})`,
|
|
242
253
|
)
|
|
243
|
-
|
|
254
|
+
try {
|
|
255
|
+
launch(cli, [cli, ...args], launchPath)
|
|
256
|
+
} finally {
|
|
257
|
+
if (previousSessionId === undefined) delete process.env.AGENCY_SESSION_ID
|
|
258
|
+
else process.env.AGENCY_SESSION_ID = previousSessionId
|
|
259
|
+
if (previousClaimRevision === undefined)
|
|
260
|
+
delete process.env.AGENCY_CLAIM_REVISION
|
|
261
|
+
else process.env.AGENCY_CLAIM_REVISION = previousClaimRevision
|
|
262
|
+
}
|
|
244
263
|
})
|
|
245
264
|
|
|
246
265
|
export const workPrepare = (options: WorkOptions = {}) =>
|
|
@@ -303,7 +322,7 @@ Usage: agency work [<directory-or-task-id> | --epic <epic-id>]
|
|
|
303
322
|
agency work prepare [target] [--dry-run] [--json]
|
|
304
323
|
|
|
305
324
|
Launch an agent for an epic, task, or phase. With no directory, select one
|
|
306
|
-
|
|
325
|
+
interactively. A positional argument resolves as a directory first, then as a task
|
|
307
326
|
ID. Use '.' for the current directory. Outside a workbase, select a registered
|
|
308
327
|
workbase first.
|
|
309
328
|
|
package/src/protocol.test.ts
CHANGED
|
@@ -83,5 +83,30 @@ describe("machine protocol", () => {
|
|
|
83
83
|
},
|
|
84
84
|
},
|
|
85
85
|
})
|
|
86
|
+
expect(
|
|
87
|
+
errorEnvelope({
|
|
88
|
+
_tag: "ClaimConflictError",
|
|
89
|
+
message: "already claimed",
|
|
90
|
+
target: "task 'example'",
|
|
91
|
+
currentRevision: "a".repeat(64),
|
|
92
|
+
claim: {
|
|
93
|
+
claimant: "orchestrator",
|
|
94
|
+
runner: "agent",
|
|
95
|
+
sessionId: "job-1",
|
|
96
|
+
startedAt: "2026-07-17T12:00:00.000Z",
|
|
97
|
+
targetRevision: "0".repeat(64),
|
|
98
|
+
state: "active",
|
|
99
|
+
},
|
|
100
|
+
}),
|
|
101
|
+
).toMatchObject({
|
|
102
|
+
error: {
|
|
103
|
+
code: "CLAIM_CONFLICT",
|
|
104
|
+
retryable: true,
|
|
105
|
+
fields: {
|
|
106
|
+
target: "task 'example'",
|
|
107
|
+
claim: { runner: "agent", sessionId: "job-1" },
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
})
|
|
86
111
|
})
|
|
87
112
|
})
|
package/src/protocol.ts
CHANGED
|
@@ -81,6 +81,22 @@ const errorMetadata: Readonly<Record<string, ErrorMetadata>> = {
|
|
|
81
81
|
EpicError: { code: "EPIC_ERROR", retryable: false },
|
|
82
82
|
TaskError: { code: "TASK_ERROR", retryable: false },
|
|
83
83
|
PhaseError: { code: "PHASE_ERROR", retryable: false },
|
|
84
|
+
ClaimError: { code: "CLAIM_ERROR", retryable: false },
|
|
85
|
+
ClaimConflictError: {
|
|
86
|
+
code: "CLAIM_CONFLICT",
|
|
87
|
+
retryable: true,
|
|
88
|
+
remediation: "Inspect the current ownership details before retrying.",
|
|
89
|
+
},
|
|
90
|
+
RevisionConflictError: {
|
|
91
|
+
code: "REVISION_CONFLICT",
|
|
92
|
+
retryable: true,
|
|
93
|
+
remediation: "Read the current document revision and retry intentionally.",
|
|
94
|
+
},
|
|
95
|
+
ClaimOwnershipError: {
|
|
96
|
+
code: "CLAIM_OWNERSHIP",
|
|
97
|
+
retryable: false,
|
|
98
|
+
remediation: "Use the session that owns the claim.",
|
|
99
|
+
},
|
|
84
100
|
ArchiveError: { code: "ARCHIVE_ERROR", retryable: false },
|
|
85
101
|
WorktreeError: { code: "WORKTREE_ERROR", retryable: false },
|
|
86
102
|
PullRequestError: { code: "PULL_REQUEST_ERROR", retryable: false },
|