@markjaquith/agency 3.5.2 → 3.6.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 +160 -16
- package/cli-main.ts +13 -2
- package/package.json +11 -5
- package/src/act-schema.ts +62 -0
- package/src/cli-parser.ts +13 -3
- package/src/commands/act-actions.ts +734 -0
- package/src/commands/act-prompts.ts +294 -0
- package/src/commands/act.ts +602 -436
- package/src/utils/chooser.ts +7 -0
- package/src/utils/interactive.js +977 -0
- package/src/workbase/AGENTS.md +49 -0
- package/src/workbase/work-target.ts +6 -4
- package/src/utils/interactive.tsx +0 -496
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
import { Effect } from "effect"
|
|
2
|
+
import type { GraphNode } from "../graph-schema"
|
|
3
|
+
import type { BaseCommandOptions } from "../utils/command"
|
|
4
|
+
import type { ActionPrompts } from "./act-prompts"
|
|
5
|
+
import { archive } from "./archive"
|
|
6
|
+
import { repo } from "./repo"
|
|
7
|
+
import { task } from "./task"
|
|
8
|
+
import { phase } from "./phase"
|
|
9
|
+
import { sync } from "./sync"
|
|
10
|
+
import { status } from "./status"
|
|
11
|
+
import { pr, prCreate } from "./pr"
|
|
12
|
+
import { work as startWork, type StartWork } from "./work"
|
|
13
|
+
|
|
14
|
+
export type ActEntity = Extract<
|
|
15
|
+
GraphNode,
|
|
16
|
+
{ readonly kind: "epic" | "task" | "phase" }
|
|
17
|
+
>
|
|
18
|
+
type NativeOperation =
|
|
19
|
+
| ReturnType<typeof task>
|
|
20
|
+
| ReturnType<typeof phase>
|
|
21
|
+
| ReturnType<typeof repo>
|
|
22
|
+
| ReturnType<typeof sync>
|
|
23
|
+
| ReturnType<typeof status>
|
|
24
|
+
| ReturnType<typeof prCreate>
|
|
25
|
+
| ReturnType<typeof archive>
|
|
26
|
+
| ReturnType<StartWork>
|
|
27
|
+
type Operation = Effect.Effect<
|
|
28
|
+
unknown,
|
|
29
|
+
Effect.Effect.Error<NativeOperation>,
|
|
30
|
+
Effect.Effect.Context<NativeOperation>
|
|
31
|
+
>
|
|
32
|
+
interface Plan {
|
|
33
|
+
readonly command: readonly string[]
|
|
34
|
+
readonly run: Operation
|
|
35
|
+
readonly followUpCommands?: readonly (readonly string[])[]
|
|
36
|
+
readonly next?: { readonly taskId: string; readonly phaseId?: string }
|
|
37
|
+
}
|
|
38
|
+
interface Input {
|
|
39
|
+
readonly id: string
|
|
40
|
+
readonly label: string
|
|
41
|
+
readonly required: boolean
|
|
42
|
+
readonly multiline?: boolean
|
|
43
|
+
/** An optional input is appended as this native CLI option. */
|
|
44
|
+
readonly option?: string
|
|
45
|
+
}
|
|
46
|
+
interface Details {
|
|
47
|
+
readonly id: string
|
|
48
|
+
readonly label: string
|
|
49
|
+
readonly blockedReason: string | null
|
|
50
|
+
readonly inputs?: readonly Input[]
|
|
51
|
+
}
|
|
52
|
+
export interface ActAction extends Details {
|
|
53
|
+
readonly preview: Plan
|
|
54
|
+
/** Replayable input collection. All mutations belong in the returned plan.run. */
|
|
55
|
+
readonly prepare: (prompts: ActionPrompts) => Effect.Effect<Plan, Error>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// A typed argument builder is used for both discovery and execution. Prompts are
|
|
59
|
+
// ordinary TypeScript; discovery does not expose or interpret form instructions.
|
|
60
|
+
const action = <A>(
|
|
61
|
+
details: Details,
|
|
62
|
+
example: A,
|
|
63
|
+
read: (p: ActionPrompts) => Effect.Effect<A, Error>,
|
|
64
|
+
build: (args: A) => Plan,
|
|
65
|
+
): ActAction => ({
|
|
66
|
+
...details,
|
|
67
|
+
preview: build(example),
|
|
68
|
+
prepare: (p) => Effect.map(read(p), build),
|
|
69
|
+
})
|
|
70
|
+
const immediate = (details: Details, plan: Plan): ActAction => ({
|
|
71
|
+
...details,
|
|
72
|
+
preview: plan,
|
|
73
|
+
prepare: () => Effect.succeed(plan),
|
|
74
|
+
})
|
|
75
|
+
const input = (
|
|
76
|
+
id: string,
|
|
77
|
+
label: string,
|
|
78
|
+
options: { option?: string; multiline?: boolean } = {},
|
|
79
|
+
): Input => ({
|
|
80
|
+
id,
|
|
81
|
+
label,
|
|
82
|
+
required: !options.option,
|
|
83
|
+
...options,
|
|
84
|
+
})
|
|
85
|
+
const taskTarget = (node: ActEntity) =>
|
|
86
|
+
node.kind === "phase"
|
|
87
|
+
? { taskId: node.key.split("/")[0]!, phaseId: node.key.split("/")[1]! }
|
|
88
|
+
: { taskId: node.key }
|
|
89
|
+
|
|
90
|
+
export const actionGroups = [
|
|
91
|
+
{
|
|
92
|
+
id: "create",
|
|
93
|
+
label: "Create a task",
|
|
94
|
+
icon: "",
|
|
95
|
+
actions: ["task-create", "investigation-create"],
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: "work",
|
|
99
|
+
label: "Work on a task or phase",
|
|
100
|
+
icon: "",
|
|
101
|
+
actions: ["work"],
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: "review",
|
|
105
|
+
label: "Review someone else's work",
|
|
106
|
+
icon: "",
|
|
107
|
+
actions: ["review", "review-ref"],
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: "split",
|
|
111
|
+
label: "Split a task / add a phase",
|
|
112
|
+
icon: "",
|
|
113
|
+
actions: ["split"],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: "handoff",
|
|
117
|
+
label: "Turn investigation into implementation",
|
|
118
|
+
icon: "",
|
|
119
|
+
actions: ["handoff"],
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: "current-work",
|
|
123
|
+
label: "See current work",
|
|
124
|
+
icon: "",
|
|
125
|
+
actions: ["current-work"],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: "browse",
|
|
129
|
+
label: "Browse items",
|
|
130
|
+
icon: "",
|
|
131
|
+
actions: [],
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: "pull-request",
|
|
135
|
+
label: "Update pull-request status",
|
|
136
|
+
icon: "",
|
|
137
|
+
actions: ["sync", "pr", "pr-ready", "pr-close"],
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: "close",
|
|
141
|
+
label: "Close or reopen work",
|
|
142
|
+
icon: "",
|
|
143
|
+
actions: ["complete", "drop", "sync", "reopen"],
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: "archive",
|
|
147
|
+
label: "Archive finished work",
|
|
148
|
+
icon: "",
|
|
149
|
+
actions: ["archive"],
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
id: "repository",
|
|
153
|
+
label: "Add a repository",
|
|
154
|
+
icon: "",
|
|
155
|
+
actions: ["repo-add", "repo-link"],
|
|
156
|
+
},
|
|
157
|
+
] as const
|
|
158
|
+
|
|
159
|
+
export const actActions = (
|
|
160
|
+
nodes: readonly GraphNode[],
|
|
161
|
+
options: BaseCommandOptions & { auto?: boolean; draft?: boolean },
|
|
162
|
+
work: StartWork = startWork,
|
|
163
|
+
node?: ActEntity,
|
|
164
|
+
): readonly ActAction[] => {
|
|
165
|
+
const repositories = nodes
|
|
166
|
+
.filter((node) => node.kind === "repository")
|
|
167
|
+
.map((node) => node.key)
|
|
168
|
+
const needsRepository = repositories.length
|
|
169
|
+
? null
|
|
170
|
+
: "Add or link a repository first"
|
|
171
|
+
if (!node)
|
|
172
|
+
return [
|
|
173
|
+
...(["add", "link"] as const).map((operation) =>
|
|
174
|
+
action(
|
|
175
|
+
{
|
|
176
|
+
id: `repo-${operation}`,
|
|
177
|
+
label:
|
|
178
|
+
operation === "add"
|
|
179
|
+
? "Add from a remote"
|
|
180
|
+
: "Link a local repository",
|
|
181
|
+
blockedReason: null,
|
|
182
|
+
inputs: [
|
|
183
|
+
input("alias", "Repository alias"),
|
|
184
|
+
input(
|
|
185
|
+
"source",
|
|
186
|
+
operation === "add" ? "Remote URL" : "Local path",
|
|
187
|
+
),
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
{ alias: "<alias>", source: "<source>" },
|
|
191
|
+
(p) =>
|
|
192
|
+
Effect.gen(function* () {
|
|
193
|
+
return {
|
|
194
|
+
alias: yield* p.text("Repository alias"),
|
|
195
|
+
source: yield* p.text(
|
|
196
|
+
operation === "add"
|
|
197
|
+
? "Repository remote URL"
|
|
198
|
+
: "Local repository path",
|
|
199
|
+
),
|
|
200
|
+
}
|
|
201
|
+
}),
|
|
202
|
+
({ alias, source }) => ({
|
|
203
|
+
command: ["agency", "repo", operation, alias, source],
|
|
204
|
+
run: repo({
|
|
205
|
+
...options,
|
|
206
|
+
subcommand: operation,
|
|
207
|
+
args: [alias, source],
|
|
208
|
+
}),
|
|
209
|
+
}),
|
|
210
|
+
),
|
|
211
|
+
),
|
|
212
|
+
...([false, true] as const).map((investigation) =>
|
|
213
|
+
action(
|
|
214
|
+
{
|
|
215
|
+
id: investigation ? "investigation-create" : "task-create",
|
|
216
|
+
label: investigation
|
|
217
|
+
? "Create an investigation"
|
|
218
|
+
: "Create a standard task",
|
|
219
|
+
blockedReason: needsRepository,
|
|
220
|
+
inputs: [
|
|
221
|
+
input("description", "Outcome", { multiline: true }),
|
|
222
|
+
input("id", "Task ID"),
|
|
223
|
+
input("repo", "Repository alias"),
|
|
224
|
+
input("base", "Base branch"),
|
|
225
|
+
],
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
id: "<id>",
|
|
229
|
+
description: "<description>",
|
|
230
|
+
repo: "<repo>",
|
|
231
|
+
base: "<base>",
|
|
232
|
+
},
|
|
233
|
+
(p) =>
|
|
234
|
+
Effect.gen(function* () {
|
|
235
|
+
const description = yield* p.text("Outcome")
|
|
236
|
+
return {
|
|
237
|
+
description,
|
|
238
|
+
id: yield* p.id("New task ID", description),
|
|
239
|
+
repo: yield* p.repository(),
|
|
240
|
+
base: yield* p.text("Base branch", "main"),
|
|
241
|
+
}
|
|
242
|
+
}),
|
|
243
|
+
(args) => ({
|
|
244
|
+
command: [
|
|
245
|
+
"agency",
|
|
246
|
+
"task",
|
|
247
|
+
"create",
|
|
248
|
+
args.id,
|
|
249
|
+
"--repo",
|
|
250
|
+
args.repo,
|
|
251
|
+
"--base",
|
|
252
|
+
args.base,
|
|
253
|
+
"--description",
|
|
254
|
+
args.description,
|
|
255
|
+
...(investigation ? ["--purpose", "investigation"] : []),
|
|
256
|
+
],
|
|
257
|
+
run: task({
|
|
258
|
+
...options,
|
|
259
|
+
subcommand: "create",
|
|
260
|
+
args: [args.id],
|
|
261
|
+
...args,
|
|
262
|
+
purpose: investigation ? "investigation" : undefined,
|
|
263
|
+
}),
|
|
264
|
+
next: { taskId: args.id },
|
|
265
|
+
}),
|
|
266
|
+
),
|
|
267
|
+
),
|
|
268
|
+
...(["pull-request", "ref"] as const).map((sourceType) =>
|
|
269
|
+
action(
|
|
270
|
+
{
|
|
271
|
+
id: sourceType === "ref" ? "review-ref" : "review",
|
|
272
|
+
label:
|
|
273
|
+
sourceType === "ref"
|
|
274
|
+
? "Review a branch or commit"
|
|
275
|
+
: "Review a pull request",
|
|
276
|
+
blockedReason: needsRepository,
|
|
277
|
+
inputs: [
|
|
278
|
+
input("repo", "Repository alias"),
|
|
279
|
+
input(
|
|
280
|
+
"source",
|
|
281
|
+
sourceType === "ref" ? "Remote ref" : "PR URL or number",
|
|
282
|
+
),
|
|
283
|
+
input("id", "Review task ID"),
|
|
284
|
+
],
|
|
285
|
+
},
|
|
286
|
+
{ id: "<id>", repo: "<repo>", source: "<source>" },
|
|
287
|
+
(p) =>
|
|
288
|
+
Effect.gen(function* () {
|
|
289
|
+
const repo = yield* p.repository()
|
|
290
|
+
const source = yield* p.text(
|
|
291
|
+
sourceType === "ref"
|
|
292
|
+
? "Remote branch or commit"
|
|
293
|
+
: "Pull request URL or number",
|
|
294
|
+
sourceType === "ref" ? "main" : "",
|
|
295
|
+
)
|
|
296
|
+
return {
|
|
297
|
+
repo,
|
|
298
|
+
source,
|
|
299
|
+
id: yield* p.id(
|
|
300
|
+
"New review task ID",
|
|
301
|
+
`review-${source.split("/").pop()}`,
|
|
302
|
+
),
|
|
303
|
+
}
|
|
304
|
+
}),
|
|
305
|
+
({ id, repo, source }) => ({
|
|
306
|
+
command: [
|
|
307
|
+
"agency",
|
|
308
|
+
"task",
|
|
309
|
+
"create",
|
|
310
|
+
id,
|
|
311
|
+
"--review",
|
|
312
|
+
repo,
|
|
313
|
+
`--${sourceType}`,
|
|
314
|
+
source,
|
|
315
|
+
],
|
|
316
|
+
run: task({
|
|
317
|
+
...options,
|
|
318
|
+
subcommand: "create",
|
|
319
|
+
args: [id],
|
|
320
|
+
review: repo,
|
|
321
|
+
...(sourceType === "ref"
|
|
322
|
+
? { ref: source }
|
|
323
|
+
: { pullRequest: source }),
|
|
324
|
+
}),
|
|
325
|
+
next: { taskId: id },
|
|
326
|
+
}),
|
|
327
|
+
),
|
|
328
|
+
),
|
|
329
|
+
immediate(
|
|
330
|
+
{
|
|
331
|
+
id: "current-work",
|
|
332
|
+
label: "See current work",
|
|
333
|
+
blockedReason: null,
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
command: ["agency", "status", "--status", "working"],
|
|
337
|
+
run: status({ ...options, statuses: ["working"] }),
|
|
338
|
+
},
|
|
339
|
+
),
|
|
340
|
+
]
|
|
341
|
+
|
|
342
|
+
const target = taskTarget(node)
|
|
343
|
+
const args = [target.taskId, ...(target.phaseId ? [target.phaseId] : [])]
|
|
344
|
+
const execution = nodes.find(
|
|
345
|
+
(candidate): candidate is Extract<GraphNode, { kind: "execution-unit" }> =>
|
|
346
|
+
candidate.kind === "execution-unit" &&
|
|
347
|
+
candidate.id === `execution-unit:${node.kind}/${node.key}`,
|
|
348
|
+
)
|
|
349
|
+
const parent = nodes.find(
|
|
350
|
+
(candidate) => candidate.kind === "task" && candidate.key === target.taskId,
|
|
351
|
+
)
|
|
352
|
+
const purpose =
|
|
353
|
+
parent && "purpose" in parent.data ? parent.data.purpose : undefined
|
|
354
|
+
const terminal = node.status === "done" || node.status === "dropped"
|
|
355
|
+
const blockers = (execution ?? node).readiness.blockers
|
|
356
|
+
const validation = blockers.find(
|
|
357
|
+
(blocker) => blocker.kind === "validation",
|
|
358
|
+
)?.reason
|
|
359
|
+
const dependency = blockers.find(
|
|
360
|
+
(blocker) => blocker.kind === "dependency",
|
|
361
|
+
)?.reason
|
|
362
|
+
const recordedPr = "pr" in node.data ? node.data.pr : null
|
|
363
|
+
const prUrl = typeof recordedPr === "string" ? recordedPr : recordedPr?.url
|
|
364
|
+
const base = "base" in node.data ? node.data.base : "main"
|
|
365
|
+
const preferredRepo = "repo" in node.data ? node.data.repo : undefined
|
|
366
|
+
const details = (
|
|
367
|
+
id: string,
|
|
368
|
+
label: string,
|
|
369
|
+
reason: string | null = null,
|
|
370
|
+
): Details => ({
|
|
371
|
+
id,
|
|
372
|
+
label,
|
|
373
|
+
blockedReason: validation ?? reason,
|
|
374
|
+
})
|
|
375
|
+
const noExecution = execution ? null : "Select an execution unit"
|
|
376
|
+
const statusCommand = (status: string) => [
|
|
377
|
+
"agency",
|
|
378
|
+
target.phaseId ? "phase" : "task",
|
|
379
|
+
"status",
|
|
380
|
+
...args,
|
|
381
|
+
status,
|
|
382
|
+
"--if-revision",
|
|
383
|
+
node.data.sha256,
|
|
384
|
+
]
|
|
385
|
+
const changeStatus = (status: string, summary?: string) => {
|
|
386
|
+
const update = {
|
|
387
|
+
...options,
|
|
388
|
+
subcommand: "status",
|
|
389
|
+
args: [...args, status],
|
|
390
|
+
ifRevision: node.data.sha256,
|
|
391
|
+
...(summary ? { noPullRequest: true, summary } : {}),
|
|
392
|
+
}
|
|
393
|
+
return target.phaseId ? phase(update) : task(update)
|
|
394
|
+
}
|
|
395
|
+
const workTarget = node.kind === "epic" ? { epicId: node.key } : target
|
|
396
|
+
const canWork =
|
|
397
|
+
node.readiness.ready ||
|
|
398
|
+
Boolean(
|
|
399
|
+
execution && node.status === "working" && !dependency && !validation,
|
|
400
|
+
)
|
|
401
|
+
return [
|
|
402
|
+
immediate(
|
|
403
|
+
details(
|
|
404
|
+
"work",
|
|
405
|
+
"Work on this item",
|
|
406
|
+
canWork
|
|
407
|
+
? null
|
|
408
|
+
: blockers.map((blocker) => blocker.reason).join("; ") ||
|
|
409
|
+
"Item is not ready for work",
|
|
410
|
+
),
|
|
411
|
+
{
|
|
412
|
+
command: [
|
|
413
|
+
"agency",
|
|
414
|
+
"work",
|
|
415
|
+
...(node.kind === "epic"
|
|
416
|
+
? ["--epic", node.key]
|
|
417
|
+
: [
|
|
418
|
+
"--task",
|
|
419
|
+
target.taskId,
|
|
420
|
+
...(target.phaseId ? ["--phase", target.phaseId] : []),
|
|
421
|
+
]),
|
|
422
|
+
...(options.auto ? ["--auto"] : []),
|
|
423
|
+
],
|
|
424
|
+
run: Effect.suspend(() => work({ ...options, ...workTarget })),
|
|
425
|
+
},
|
|
426
|
+
),
|
|
427
|
+
immediate(
|
|
428
|
+
details(
|
|
429
|
+
"pr",
|
|
430
|
+
"Create pull request",
|
|
431
|
+
noExecution ??
|
|
432
|
+
(parent && "review" in parent.data
|
|
433
|
+
? "Review tasks do not create delivery PRs"
|
|
434
|
+
: purpose === "investigation"
|
|
435
|
+
? "Create an implementation follow-up first"
|
|
436
|
+
: terminal
|
|
437
|
+
? "Item is terminal"
|
|
438
|
+
: prUrl
|
|
439
|
+
? "Item already has a recorded PR"
|
|
440
|
+
: (dependency ?? null)),
|
|
441
|
+
),
|
|
442
|
+
{
|
|
443
|
+
command: [
|
|
444
|
+
"agency",
|
|
445
|
+
"pr",
|
|
446
|
+
"create",
|
|
447
|
+
...args,
|
|
448
|
+
...(options.draft ? ["--draft"] : []),
|
|
449
|
+
],
|
|
450
|
+
run: prCreate({ ...options, ...target }),
|
|
451
|
+
},
|
|
452
|
+
),
|
|
453
|
+
...(["reopen", "drop"] as const).map((id) => {
|
|
454
|
+
const status = id === "reopen" ? "open" : "dropped"
|
|
455
|
+
return immediate(
|
|
456
|
+
details(
|
|
457
|
+
id,
|
|
458
|
+
id === "reopen" ? "Reopen" : "Drop",
|
|
459
|
+
noExecution ??
|
|
460
|
+
(id === "reopen"
|
|
461
|
+
? terminal
|
|
462
|
+
? null
|
|
463
|
+
: "Item is not terminal"
|
|
464
|
+
: terminal
|
|
465
|
+
? "Item is already terminal"
|
|
466
|
+
: null),
|
|
467
|
+
),
|
|
468
|
+
{ command: statusCommand(status), run: changeStatus(status) },
|
|
469
|
+
)
|
|
470
|
+
}),
|
|
471
|
+
immediate(
|
|
472
|
+
details(
|
|
473
|
+
"archive",
|
|
474
|
+
"Archive",
|
|
475
|
+
node.readiness.terminal
|
|
476
|
+
? null
|
|
477
|
+
: "Item and all children must be terminal",
|
|
478
|
+
),
|
|
479
|
+
{
|
|
480
|
+
command: ["agency", "archive", node.kind, ...args],
|
|
481
|
+
run: archive({ ...options, type: node.kind, args }),
|
|
482
|
+
},
|
|
483
|
+
),
|
|
484
|
+
action(
|
|
485
|
+
{
|
|
486
|
+
...details(
|
|
487
|
+
"split",
|
|
488
|
+
"Add a phase / split this task",
|
|
489
|
+
node.kind !== "task"
|
|
490
|
+
? "Select a task"
|
|
491
|
+
: "review" in node.data
|
|
492
|
+
? "Review tasks cannot be split into execution phases"
|
|
493
|
+
: terminal
|
|
494
|
+
? "Task is terminal"
|
|
495
|
+
: null,
|
|
496
|
+
),
|
|
497
|
+
inputs: [
|
|
498
|
+
input("id", "New phase ID"),
|
|
499
|
+
...(node.kind === "task" && !("phases" in node.data)
|
|
500
|
+
? [input("firstPhase", "Existing work's phase ID")]
|
|
501
|
+
: []),
|
|
502
|
+
input("repo", "Repository alias"),
|
|
503
|
+
input("base", "Base branch"),
|
|
504
|
+
input("branch", "New phase branch"),
|
|
505
|
+
input("dependsOn", "Completion dependency", {
|
|
506
|
+
option: "--depends-on",
|
|
507
|
+
}),
|
|
508
|
+
],
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
id: "<id>",
|
|
512
|
+
firstPhase:
|
|
513
|
+
node.kind === "task" && !("phases" in node.data)
|
|
514
|
+
? "<firstPhase>"
|
|
515
|
+
: undefined,
|
|
516
|
+
repo: "<repo>",
|
|
517
|
+
base: "<base>",
|
|
518
|
+
branch: "<branch>",
|
|
519
|
+
dependsOn: "",
|
|
520
|
+
},
|
|
521
|
+
(p) =>
|
|
522
|
+
Effect.gen(function* () {
|
|
523
|
+
const id = yield* p.id(
|
|
524
|
+
"New phase ID",
|
|
525
|
+
"phases" in node.data
|
|
526
|
+
? `phase-${node.data.phases.length + 1}`
|
|
527
|
+
: "next",
|
|
528
|
+
`${node.key}/`,
|
|
529
|
+
)
|
|
530
|
+
const firstPhase =
|
|
531
|
+
"phases" in node.data
|
|
532
|
+
? undefined
|
|
533
|
+
: yield* p.text(
|
|
534
|
+
"Name for the existing work's first phase",
|
|
535
|
+
"initial",
|
|
536
|
+
)
|
|
537
|
+
return {
|
|
538
|
+
id,
|
|
539
|
+
firstPhase,
|
|
540
|
+
repo: yield* p.repository(preferredRepo),
|
|
541
|
+
base: yield* p.text("Base branch", base),
|
|
542
|
+
branch: yield* p.text("New phase branch", `task/${node.key}-${id}`),
|
|
543
|
+
dependsOn: yield* p.text(
|
|
544
|
+
"Wait for phase (optional; independent of base branch)",
|
|
545
|
+
"",
|
|
546
|
+
false,
|
|
547
|
+
),
|
|
548
|
+
}
|
|
549
|
+
}),
|
|
550
|
+
(values) => ({
|
|
551
|
+
command: [
|
|
552
|
+
"agency",
|
|
553
|
+
"phase",
|
|
554
|
+
"create",
|
|
555
|
+
target.taskId,
|
|
556
|
+
values.id,
|
|
557
|
+
"--repo",
|
|
558
|
+
values.repo,
|
|
559
|
+
"--base",
|
|
560
|
+
values.base,
|
|
561
|
+
"--branch",
|
|
562
|
+
values.branch,
|
|
563
|
+
...(values.firstPhase ? ["--first-phase", values.firstPhase] : []),
|
|
564
|
+
...(values.dependsOn ? ["--depends-on", values.dependsOn] : []),
|
|
565
|
+
],
|
|
566
|
+
run: phase({
|
|
567
|
+
...options,
|
|
568
|
+
subcommand: "create",
|
|
569
|
+
args: [target.taskId, values.id],
|
|
570
|
+
...values,
|
|
571
|
+
dependsOn: values.dependsOn ? [values.dependsOn] : undefined,
|
|
572
|
+
}),
|
|
573
|
+
next: { taskId: target.taskId, phaseId: values.id },
|
|
574
|
+
}),
|
|
575
|
+
),
|
|
576
|
+
action(
|
|
577
|
+
{
|
|
578
|
+
...details(
|
|
579
|
+
"handoff",
|
|
580
|
+
"Create implementation follow-up",
|
|
581
|
+
noExecution ??
|
|
582
|
+
(purpose === "investigation"
|
|
583
|
+
? null
|
|
584
|
+
: "Select an investigation execution unit"),
|
|
585
|
+
),
|
|
586
|
+
inputs: [
|
|
587
|
+
input("id", "New task ID"),
|
|
588
|
+
input("repo", "Repository alias"),
|
|
589
|
+
input("base", "Base branch"),
|
|
590
|
+
],
|
|
591
|
+
},
|
|
592
|
+
{ id: "<id>", repo: "<repo>", base: "<base>" },
|
|
593
|
+
(p) =>
|
|
594
|
+
Effect.gen(function* () {
|
|
595
|
+
return {
|
|
596
|
+
id: yield* p.id(
|
|
597
|
+
"New implementation task ID",
|
|
598
|
+
`${target.taskId}-implementation`,
|
|
599
|
+
),
|
|
600
|
+
repo: yield* p.repository(preferredRepo),
|
|
601
|
+
base: yield* p.text("Base branch", base),
|
|
602
|
+
}
|
|
603
|
+
}),
|
|
604
|
+
(values) => ({
|
|
605
|
+
command: [
|
|
606
|
+
"agency",
|
|
607
|
+
"task",
|
|
608
|
+
"handoff",
|
|
609
|
+
target.taskId,
|
|
610
|
+
values.id,
|
|
611
|
+
"--repo",
|
|
612
|
+
values.repo,
|
|
613
|
+
"--base",
|
|
614
|
+
values.base,
|
|
615
|
+
...(target.phaseId ? ["--source-phase", target.phaseId] : []),
|
|
616
|
+
],
|
|
617
|
+
run: task({
|
|
618
|
+
...options,
|
|
619
|
+
subcommand: "handoff",
|
|
620
|
+
args: [target.taskId, values.id],
|
|
621
|
+
...values,
|
|
622
|
+
sourcePhase: target.phaseId,
|
|
623
|
+
}),
|
|
624
|
+
next: { taskId: values.id },
|
|
625
|
+
}),
|
|
626
|
+
),
|
|
627
|
+
action(
|
|
628
|
+
{
|
|
629
|
+
...details(
|
|
630
|
+
"complete",
|
|
631
|
+
"Complete without a pull request",
|
|
632
|
+
noExecution ??
|
|
633
|
+
(terminal
|
|
634
|
+
? "Item is already terminal"
|
|
635
|
+
: prUrl
|
|
636
|
+
? "Reconcile the recorded pull request instead"
|
|
637
|
+
: null),
|
|
638
|
+
),
|
|
639
|
+
inputs: [
|
|
640
|
+
input("summary", "Completed outcome summary", { multiline: true }),
|
|
641
|
+
],
|
|
642
|
+
},
|
|
643
|
+
"<summary>",
|
|
644
|
+
(p) => p.text("Completed non-PR outcome summary"),
|
|
645
|
+
(summary) => ({
|
|
646
|
+
command: [
|
|
647
|
+
...statusCommand("done"),
|
|
648
|
+
"--no-pull-request",
|
|
649
|
+
"--summary",
|
|
650
|
+
summary,
|
|
651
|
+
],
|
|
652
|
+
run: changeStatus("done", summary),
|
|
653
|
+
}),
|
|
654
|
+
),
|
|
655
|
+
immediate(
|
|
656
|
+
details(
|
|
657
|
+
"sync",
|
|
658
|
+
"Refresh Agency state from GitHub / provider",
|
|
659
|
+
node.kind === "epic" ? "Select a task or phase" : null,
|
|
660
|
+
),
|
|
661
|
+
{
|
|
662
|
+
command: ["agency", "sync", ...args],
|
|
663
|
+
run: sync({ ...options, ...target }),
|
|
664
|
+
},
|
|
665
|
+
),
|
|
666
|
+
...(["ready", "close"] as const).map((operation) =>
|
|
667
|
+
immediate(
|
|
668
|
+
details(
|
|
669
|
+
`pr-${operation}`,
|
|
670
|
+
operation === "ready"
|
|
671
|
+
? "Mark GitHub PR ready for review"
|
|
672
|
+
: "Close GitHub PR",
|
|
673
|
+
prUrl?.startsWith("https://github.com/")
|
|
674
|
+
? null
|
|
675
|
+
: "Requires a recorded GitHub pull request URL",
|
|
676
|
+
),
|
|
677
|
+
{
|
|
678
|
+
command: ["agency", "pr", operation, prUrl ?? "<pull-request-url>"],
|
|
679
|
+
followUpCommands: [["agency", "sync", ...args]],
|
|
680
|
+
run: Effect.gen(function* () {
|
|
681
|
+
const code = yield* pr([operation, prUrl ?? ""], options.cwd)
|
|
682
|
+
if (code !== 0)
|
|
683
|
+
return yield* Effect.fail(
|
|
684
|
+
new Error(`GitHub PR ${operation} failed (${code})`),
|
|
685
|
+
)
|
|
686
|
+
yield* sync({ ...options, ...target })
|
|
687
|
+
}),
|
|
688
|
+
},
|
|
689
|
+
),
|
|
690
|
+
),
|
|
691
|
+
]
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
export const actionOutput = (action: ActAction, auto?: boolean) => {
|
|
695
|
+
const { id, label, blockedReason, preview, inputs = [] } = action
|
|
696
|
+
return {
|
|
697
|
+
id,
|
|
698
|
+
label,
|
|
699
|
+
available: blockedReason === null,
|
|
700
|
+
blockedReason,
|
|
701
|
+
...(blockedReason
|
|
702
|
+
? {}
|
|
703
|
+
: {
|
|
704
|
+
inputs,
|
|
705
|
+
command: inputs.some((input) => input.required)
|
|
706
|
+
? null
|
|
707
|
+
: preview.command,
|
|
708
|
+
...(inputs.length ? { commandTemplate: preview.command } : {}),
|
|
709
|
+
...(preview.followUpCommands
|
|
710
|
+
? { followUpCommands: preview.followUpCommands }
|
|
711
|
+
: {}),
|
|
712
|
+
...(preview.next
|
|
713
|
+
? {
|
|
714
|
+
nextActions: [
|
|
715
|
+
{
|
|
716
|
+
id: "work",
|
|
717
|
+
requiresSelection: true,
|
|
718
|
+
commandTemplate: [
|
|
719
|
+
"agency",
|
|
720
|
+
"work",
|
|
721
|
+
"--task",
|
|
722
|
+
preview.next.taskId,
|
|
723
|
+
...(preview.next.phaseId
|
|
724
|
+
? ["--phase", preview.next.phaseId]
|
|
725
|
+
: []),
|
|
726
|
+
...(auto ? ["--auto"] : []),
|
|
727
|
+
],
|
|
728
|
+
},
|
|
729
|
+
],
|
|
730
|
+
}
|
|
731
|
+
: {}),
|
|
732
|
+
}),
|
|
733
|
+
}
|
|
734
|
+
}
|