@markjaquith/agency 3.5.2 → 3.6.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 +155 -16
- package/cli-main.ts +13 -2
- package/package.json +8 -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.tsx +528 -87
- package/src/workbase/AGENTS.md +49 -0
- package/src/workbase/work-target.ts +6 -4
package/src/commands/act.ts
CHANGED
|
@@ -1,536 +1,702 @@
|
|
|
1
|
+
import { Schema } from "@effect/schema"
|
|
1
2
|
import { Effect } from "effect"
|
|
2
|
-
import { isAbsolute, relative, resolve, sep } from "node:path"
|
|
3
|
-
import
|
|
4
|
-
import { isTerminalStatus } from "../readiness"
|
|
3
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "node:path"
|
|
4
|
+
import { ActDiscovery } from "../act-schema"
|
|
5
5
|
import { FileSystemService } from "../services/FileSystemService"
|
|
6
6
|
import { GraphService } from "../services/GraphService"
|
|
7
7
|
import { WorkbaseService } from "../services/WorkbaseService"
|
|
8
8
|
import type { BaseCommandOptions } from "../utils/command"
|
|
9
|
-
import {
|
|
9
|
+
import type { Choice } from "../utils/chooser"
|
|
10
10
|
import { createLoggers } from "../utils/effect"
|
|
11
11
|
import { macchiato } from "../utils/theme"
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
import { workKindStyle, workStatusStyle } from "../workbase/work-target"
|
|
13
|
+
import {
|
|
14
|
+
actionGroups,
|
|
15
|
+
actActions,
|
|
16
|
+
actionOutput,
|
|
17
|
+
type ActAction,
|
|
18
|
+
type ActEntity,
|
|
19
|
+
} from "./act-actions"
|
|
20
|
+
import {
|
|
21
|
+
ActCancelled,
|
|
22
|
+
actionPrompts,
|
|
23
|
+
wizardInputs,
|
|
24
|
+
actTabs,
|
|
25
|
+
defaultInteraction,
|
|
26
|
+
openActSession,
|
|
27
|
+
type ActInteraction,
|
|
28
|
+
} from "./act-prompts"
|
|
16
29
|
import { work as startWork, type StartWork } from "./work"
|
|
17
30
|
|
|
18
|
-
type
|
|
19
|
-
GraphNode,
|
|
20
|
-
{ readonly kind: "epic" | "task" | "phase" }
|
|
21
|
-
>
|
|
22
|
-
|
|
23
|
-
type ActAction = "work" | "pr" | "reopen" | "drop" | "archive"
|
|
24
|
-
|
|
25
|
-
export interface ActInteraction {
|
|
26
|
-
readonly select: <T>(
|
|
27
|
-
prompt: string,
|
|
28
|
-
choices: readonly Choice<T>[],
|
|
29
|
-
command?: readonly string[],
|
|
30
|
-
) => Effect.Effect<T | null, Error>
|
|
31
|
-
}
|
|
31
|
+
export type { ActInteraction } from "./act-prompts"
|
|
32
32
|
|
|
33
33
|
interface ActOptions extends BaseCommandOptions {
|
|
34
|
+
readonly action?: string
|
|
34
35
|
readonly directory?: string
|
|
35
36
|
readonly auto?: boolean
|
|
36
37
|
readonly draft?: boolean
|
|
37
38
|
readonly dryRun?: boolean
|
|
38
|
-
readonly json?: boolean
|
|
39
39
|
readonly epicId?: string
|
|
40
40
|
readonly taskId?: string
|
|
41
41
|
readonly phaseId?: string
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const defaultInteraction: ActInteraction = {
|
|
45
|
-
select: (prompt, choices, command) => choose(prompt, choices, command),
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const entityKey = (node: EntityNode) => `${node.kind}:${node.key}`
|
|
49
|
-
|
|
50
|
-
const entityDescription = (node: EntityNode) =>
|
|
51
|
-
"description" in node.data && node.data.description
|
|
52
|
-
? ` - ${node.data.description}`
|
|
53
|
-
: ""
|
|
54
|
-
|
|
55
|
-
const orderedEntities = (
|
|
56
|
-
nodes: readonly EntityNode[],
|
|
57
|
-
edges: readonly {
|
|
58
|
-
readonly kind: string
|
|
59
|
-
readonly from: string
|
|
60
|
-
readonly to: string
|
|
61
|
-
}[],
|
|
62
|
-
) => {
|
|
63
|
-
const byId = new Map(nodes.map((node) => [node.id, node]))
|
|
64
|
-
const children = new Map<string, EntityNode[]>()
|
|
65
|
-
const owned = new Set<string>()
|
|
66
|
-
for (const edge of edges) {
|
|
67
|
-
if (edge.kind !== "owns") continue
|
|
68
|
-
const parent = byId.get(edge.from)
|
|
69
|
-
const child = byId.get(edge.to)
|
|
70
|
-
if (!parent || !child) continue
|
|
71
|
-
children.set(edge.from, [...(children.get(edge.from) ?? []), child])
|
|
72
|
-
owned.add(child.id)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const ordered: { readonly node: EntityNode; readonly depth: number }[] = []
|
|
76
|
-
const append = (node: EntityNode, depth: number) => {
|
|
77
|
-
ordered.push({ node, depth })
|
|
78
|
-
for (const child of children.get(node.id) ?? []) append(child, depth + 1)
|
|
79
|
-
}
|
|
80
|
-
for (const node of nodes) {
|
|
81
|
-
if (!owned.has(node.id)) append(node, 0)
|
|
82
|
-
}
|
|
83
|
-
return ordered
|
|
42
|
+
readonly exitOnEscape?: boolean
|
|
84
43
|
}
|
|
85
44
|
|
|
86
45
|
const entityChoices = (
|
|
87
|
-
nodes: readonly
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
depth,
|
|
98
|
-
segments: [
|
|
99
|
-
{ text: `[${node.status}] `, color: macchiato.overlay1 },
|
|
100
|
-
{ text: `${node.kind} `, color: macchiato.sapphire },
|
|
101
|
-
{ text: node.key },
|
|
102
|
-
...(entityDescription(node)
|
|
103
|
-
? [{ text: entityDescription(node), color: macchiato.overlay0 }]
|
|
104
|
-
: []),
|
|
105
|
-
],
|
|
106
|
-
value: entityKey(node),
|
|
107
|
-
}))
|
|
108
|
-
|
|
109
|
-
const executionNode = (
|
|
110
|
-
node: EntityNode,
|
|
111
|
-
executions: ReadonlyMap<
|
|
112
|
-
string,
|
|
113
|
-
Extract<GraphNode, { readonly kind: "execution-unit" }>
|
|
114
|
-
>,
|
|
115
|
-
): Extract<GraphNode, { readonly kind: "execution-unit" }> | undefined => {
|
|
116
|
-
if (node.kind === "epic") return undefined
|
|
117
|
-
if (node.kind === "task" && "phases" in node.data) return undefined
|
|
118
|
-
return executions.get(node.id)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const canWork = (
|
|
122
|
-
node: EntityNode,
|
|
123
|
-
executions: Parameters<typeof executionNode>[1],
|
|
124
|
-
) => {
|
|
125
|
-
if (node.kind === "epic" || (node.kind === "task" && "phases" in node.data)) {
|
|
126
|
-
return node.readiness.ready
|
|
127
|
-
}
|
|
128
|
-
const execution = executionNode(node, executions)
|
|
129
|
-
return Boolean(
|
|
130
|
-
execution &&
|
|
131
|
-
(execution.readiness.ready ||
|
|
132
|
-
(execution.status === "working" &&
|
|
133
|
-
execution.readiness.blockers.every(
|
|
134
|
-
(blocker) => blocker.kind !== "validation",
|
|
135
|
-
))),
|
|
136
|
-
)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const canCreatePr = (
|
|
140
|
-
node: EntityNode,
|
|
141
|
-
executions: Parameters<typeof executionNode>[1],
|
|
142
|
-
) => {
|
|
143
|
-
const execution = executionNode(node, executions)
|
|
144
|
-
return Boolean(
|
|
145
|
-
execution &&
|
|
146
|
-
!execution.readiness.terminal &&
|
|
147
|
-
!("pr" in execution.data && execution.data.pr) &&
|
|
148
|
-
!execution.readiness.blockers.some(
|
|
149
|
-
(blocker) =>
|
|
150
|
-
blocker.kind === "dependency" || blocker.kind === "validation",
|
|
151
|
-
),
|
|
46
|
+
nodes: readonly ActEntity[],
|
|
47
|
+
twoRows = false,
|
|
48
|
+
): Choice<string>[] => {
|
|
49
|
+
const name = (node: ActEntity) =>
|
|
50
|
+
node.kind === "phase"
|
|
51
|
+
? node.key.slice(node.key.lastIndexOf("/") + 1)
|
|
52
|
+
: node.key
|
|
53
|
+
const nameWidth = Math.min(
|
|
54
|
+
32,
|
|
55
|
+
Math.max(0, ...nodes.map((node) => name(node).length)),
|
|
152
56
|
)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
: ["--task", taskId, ...(phaseId ? ["--phase", phaseId] : [])]),
|
|
202
|
-
...(options.auto ? ["--auto"] : []),
|
|
203
|
-
]
|
|
204
|
-
case "pr":
|
|
205
|
-
return [
|
|
206
|
-
"agency",
|
|
207
|
-
"pr",
|
|
208
|
-
"create",
|
|
209
|
-
taskId,
|
|
210
|
-
...(phaseId ? [phaseId] : []),
|
|
211
|
-
...(options.draft ? ["--draft"] : []),
|
|
57
|
+
const shorten = (text: string, width: number) =>
|
|
58
|
+
text.length > width ? `${text.slice(0, width - 1)}…` : text
|
|
59
|
+
return nodes
|
|
60
|
+
.toSorted((a, b) => a.key.localeCompare(b.key))
|
|
61
|
+
.map((node) => {
|
|
62
|
+
const description = (
|
|
63
|
+
"description" in node.data ? node.data.description : ""
|
|
64
|
+
)
|
|
65
|
+
?.replace(/\s+/g, " ")
|
|
66
|
+
.trim()
|
|
67
|
+
const parent =
|
|
68
|
+
node.kind === "phase"
|
|
69
|
+
? node.key.slice(0, node.key.lastIndexOf("/"))
|
|
70
|
+
: undefined
|
|
71
|
+
const blocked = node.readiness.blockers.some(
|
|
72
|
+
(blocker) =>
|
|
73
|
+
blocker.kind === "dependency" || blocker.kind === "validation",
|
|
74
|
+
)
|
|
75
|
+
const state = blocked
|
|
76
|
+
? { icon: "", color: macchiato.yellow }
|
|
77
|
+
: workStatusStyle[node.status]
|
|
78
|
+
const segments = [
|
|
79
|
+
{
|
|
80
|
+
text: `${workKindStyle[node.kind].icon} `,
|
|
81
|
+
color: workKindStyle[node.kind].color,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
text: shorten(name(node), nameWidth).padEnd(nameWidth),
|
|
85
|
+
color: macchiato.text,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
text: ` ${state.icon} ${(blocked ? "blocked" : node.status).padEnd(9)}`,
|
|
89
|
+
color: state.color,
|
|
90
|
+
},
|
|
91
|
+
...(parent
|
|
92
|
+
? [{ text: ` in ${shorten(parent, 24)}`, color: macchiato.subtext0 }]
|
|
93
|
+
: []),
|
|
94
|
+
...(node.repositories.length
|
|
95
|
+
? [
|
|
96
|
+
{
|
|
97
|
+
text: ` ${node.repositories.join(", ")}`,
|
|
98
|
+
color: macchiato.overlay1,
|
|
99
|
+
},
|
|
100
|
+
]
|
|
101
|
+
: []),
|
|
102
|
+
...(description
|
|
103
|
+
? [{ text: ` — ${description}`, color: macchiato.overlay0 }]
|
|
104
|
+
: []),
|
|
212
105
|
]
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
106
|
+
return {
|
|
107
|
+
key: node.id,
|
|
108
|
+
value: node.id,
|
|
109
|
+
segments,
|
|
110
|
+
...(twoRows
|
|
111
|
+
? {
|
|
112
|
+
details: {
|
|
113
|
+
title: [
|
|
114
|
+
{
|
|
115
|
+
text: `${workKindStyle[node.kind].icon} `,
|
|
116
|
+
color: workKindStyle[node.kind].color,
|
|
117
|
+
},
|
|
118
|
+
{ text: node.key, color: macchiato.text },
|
|
119
|
+
],
|
|
120
|
+
metadata: [
|
|
121
|
+
{
|
|
122
|
+
text: ` ${"repo" in node.data ? node.data.repo : (node.repositories[0] ?? "—")} `,
|
|
123
|
+
color: macchiato.overlay1,
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
text: `${state.icon} ${blocked ? "blocked" : node.status}`,
|
|
127
|
+
color: state.color,
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
description: description ?? "",
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
: {}),
|
|
134
|
+
label: segments.map((segment) => segment.text).join(""),
|
|
135
|
+
plainLabel: `[${node.status}] ${node.kind} ${node.key}${blocked ? " blocked" : ""} ${node.repositories.join(" ")}${description ? ` — ${description}` : ""}`,
|
|
136
|
+
}
|
|
137
|
+
})
|
|
225
138
|
}
|
|
226
139
|
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
140
|
+
const pathEntityKey = (root: string, path: string) => {
|
|
141
|
+
const local = relative(root, path)
|
|
142
|
+
const parts =
|
|
143
|
+
!local || isAbsolute(local) || local.startsWith(`..${sep}`)
|
|
144
|
+
? []
|
|
145
|
+
: local.split(sep)
|
|
146
|
+
if (parts[0] === "epics" && parts[1]) return `epic:${parts[1]}`
|
|
147
|
+
if (parts[0] !== "tasks" || !parts[1]) return undefined
|
|
148
|
+
return parts[2] === "phases" && parts[3]
|
|
149
|
+
? `phase:${parts[1]}/${parts[3]}`
|
|
150
|
+
: `task:${parts[1]}`
|
|
151
|
+
}
|
|
152
|
+
const shellCommand = (argv: readonly string[]) =>
|
|
153
|
+
argv
|
|
154
|
+
.map((arg) =>
|
|
155
|
+
/^[A-Za-z0-9_./:=+@%-]+$/.test(arg)
|
|
156
|
+
? arg
|
|
157
|
+
: `'${arg.replaceAll("'", `'\\''`)}'`,
|
|
233
158
|
)
|
|
234
159
|
.join(" ")
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
node: EntityNode,
|
|
238
|
-
executions: Parameters<typeof executionNode>[1],
|
|
239
|
-
options: Pick<ActOptions, "auto" | "draft">,
|
|
240
|
-
) => ({
|
|
160
|
+
const available = (action: ActAction) => !action.blockedReason
|
|
161
|
+
const entitySummary = (node: ActEntity) => ({
|
|
241
162
|
kind: node.kind,
|
|
242
163
|
id: node.id,
|
|
243
164
|
key: node.key,
|
|
244
165
|
status: node.status,
|
|
166
|
+
description: "description" in node.data ? node.data.description : undefined,
|
|
167
|
+
repo: "repo" in node.data ? node.data.repo : undefined,
|
|
168
|
+
repositories: node.repositories,
|
|
245
169
|
readiness: node.readiness,
|
|
246
170
|
revision: node.data.sha256,
|
|
247
|
-
actions: actionChoices(node, executions).map((choice) => ({
|
|
248
|
-
id: choice.value,
|
|
249
|
-
label: choice.label,
|
|
250
|
-
command: actionCommand(node, choice.value, options),
|
|
251
|
-
})),
|
|
252
171
|
})
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if (node.kind !== "execution-unit") continue
|
|
261
|
-
const key =
|
|
262
|
-
"phaseId" in node.data
|
|
263
|
-
? `phase:${node.data.taskId}/${node.data.phaseId}`
|
|
264
|
-
: `task:${node.data.taskId}`
|
|
265
|
-
executions.set(key, node)
|
|
266
|
-
}
|
|
267
|
-
return executions
|
|
172
|
+
const actionIcons: Record<string, string> = {
|
|
173
|
+
reopen: "",
|
|
174
|
+
drop: "",
|
|
175
|
+
complete: "",
|
|
176
|
+
sync: "",
|
|
177
|
+
"pr-ready": "",
|
|
178
|
+
"pr-close": "",
|
|
268
179
|
}
|
|
180
|
+
const actionChoices = (actions: readonly ActAction[]): Choice<string>[] =>
|
|
181
|
+
actions.map(({ id, label }) => {
|
|
182
|
+
const icon =
|
|
183
|
+
actionIcons[id] ??
|
|
184
|
+
actionGroups.find((group) =>
|
|
185
|
+
group.actions.some((action) => action === id),
|
|
186
|
+
)!.icon
|
|
187
|
+
const color =
|
|
188
|
+
id === "drop" || id === "pr-close" ? macchiato.red : macchiato.overlay1
|
|
189
|
+
return {
|
|
190
|
+
key: id,
|
|
191
|
+
value: id,
|
|
192
|
+
label: `${icon} ${label}`,
|
|
193
|
+
plainLabel: label,
|
|
194
|
+
segments: [{ text: `${icon} `, color }, { text: label }],
|
|
195
|
+
}
|
|
196
|
+
})
|
|
269
197
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const pathEntityKey = (
|
|
280
|
-
directory: string | undefined,
|
|
281
|
-
isDirectory: boolean,
|
|
282
|
-
root: string,
|
|
283
|
-
startPath: string,
|
|
284
|
-
) => {
|
|
285
|
-
if (!directory) return undefined
|
|
286
|
-
if (!isDirectory) return `task:${directory}`
|
|
287
|
-
const path = relative(root, startPath)
|
|
288
|
-
const parts =
|
|
289
|
-
!path || isAbsolute(path) || path.startsWith(`..${sep}`)
|
|
290
|
-
? []
|
|
291
|
-
: path.split(sep)
|
|
292
|
-
if (parts[0] === "epics" && parts[1]) return `epic:${parts[1]}`
|
|
293
|
-
if (parts[0] !== "tasks" || !parts[1]) return undefined
|
|
294
|
-
return parts[2] === "phases" && parts[3]
|
|
295
|
-
? `phase:${parts[1]}/${parts[3]}`
|
|
296
|
-
: `task:${parts[1]}`
|
|
198
|
+
interface ActState {
|
|
199
|
+
session?: Effect.Effect.Success<ReturnType<typeof openActSession>>
|
|
200
|
+
recap: string[]
|
|
201
|
+
root?: string
|
|
202
|
+
view: "home" | "item-menu" | "goal-menu" | "flow"
|
|
203
|
+
native: boolean
|
|
204
|
+
notice: string
|
|
205
|
+
item?: string
|
|
206
|
+
goal?: string
|
|
297
207
|
}
|
|
298
208
|
|
|
299
|
-
const sameActions = (
|
|
300
|
-
left: readonly Choice<ActAction>[],
|
|
301
|
-
right: readonly Choice<ActAction>[],
|
|
302
|
-
) =>
|
|
303
|
-
left.map((choice) => choice.value).join("\0") ===
|
|
304
|
-
right.map((choice) => choice.value).join("\0")
|
|
305
|
-
|
|
306
209
|
export const act = (
|
|
307
210
|
options: ActOptions = {},
|
|
308
211
|
interaction: ActInteraction = defaultInteraction,
|
|
309
212
|
work: StartWork = startWork,
|
|
310
213
|
) =>
|
|
311
214
|
Effect.gen(function* () {
|
|
312
|
-
|
|
215
|
+
const state: ActState = {
|
|
216
|
+
recap: [],
|
|
217
|
+
view: "home",
|
|
218
|
+
native: false,
|
|
219
|
+
notice: "",
|
|
220
|
+
}
|
|
221
|
+
let nextOptions = options
|
|
222
|
+
const { log } = createLoggers(options)
|
|
223
|
+
yield* Effect.gen(function* () {
|
|
224
|
+
while (true) {
|
|
225
|
+
state.view = "home"
|
|
226
|
+
const keepGoing = yield* actStep(
|
|
227
|
+
nextOptions,
|
|
228
|
+
interaction,
|
|
229
|
+
work,
|
|
230
|
+
state,
|
|
231
|
+
).pipe(
|
|
232
|
+
Effect.tap(() =>
|
|
233
|
+
Effect.sync(() => {
|
|
234
|
+
state.goal = undefined
|
|
235
|
+
}),
|
|
236
|
+
),
|
|
237
|
+
Effect.as(true),
|
|
238
|
+
Effect.catchAll((error) => {
|
|
239
|
+
if (error instanceof ActCancelled) {
|
|
240
|
+
if (state.view === "item-menu") state.item = undefined
|
|
241
|
+
if (state.view === "goal-menu") state.goal = undefined
|
|
242
|
+
return Effect.succeed(
|
|
243
|
+
state.view !== "home" || options.exitOnEscape === false,
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
if (!state.native || state.view === "home")
|
|
247
|
+
return Effect.fail(error)
|
|
248
|
+
state.goal = undefined
|
|
249
|
+
state.notice = ` ${error instanceof Error ? error.message : String(error)}`
|
|
250
|
+
return Effect.succeed(true)
|
|
251
|
+
}),
|
|
252
|
+
)
|
|
253
|
+
if (!state.native || !keepGoing || state.session?.quitRequested) break
|
|
254
|
+
state.session?.resetCancellation()
|
|
255
|
+
if (state.session?.takeNavigation()) {
|
|
256
|
+
state.item = undefined
|
|
257
|
+
state.goal = undefined
|
|
258
|
+
}
|
|
259
|
+
state.session?.notice(state.notice)
|
|
260
|
+
nextOptions = {
|
|
261
|
+
...options,
|
|
262
|
+
cwd: state.root,
|
|
263
|
+
directory: undefined,
|
|
264
|
+
epicId: undefined,
|
|
265
|
+
taskId: undefined,
|
|
266
|
+
phaseId: undefined,
|
|
267
|
+
action: undefined,
|
|
268
|
+
...(state.item?.startsWith("phase:")
|
|
269
|
+
? {
|
|
270
|
+
taskId: state.item.slice(6).split("/")[0],
|
|
271
|
+
phaseId: state.item.slice(6).split("/")[1],
|
|
272
|
+
}
|
|
273
|
+
: state.item?.startsWith("task:")
|
|
274
|
+
? { taskId: state.item.slice(5) }
|
|
275
|
+
: state.item?.startsWith("epic:")
|
|
276
|
+
? { epicId: state.item.slice(5) }
|
|
277
|
+
: {}),
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}).pipe(
|
|
281
|
+
Effect.scoped,
|
|
282
|
+
Effect.ensuring(
|
|
283
|
+
Effect.sync(() => {
|
|
284
|
+
if (state.recap.length)
|
|
285
|
+
log(
|
|
286
|
+
`\n Agency\n\n${state.recap.map((line) => ` ${line}`).join("\n")}\n`,
|
|
287
|
+
)
|
|
288
|
+
}),
|
|
289
|
+
),
|
|
290
|
+
)
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
const actStep = (
|
|
294
|
+
options: ActOptions,
|
|
295
|
+
interaction: ActInteraction,
|
|
296
|
+
work: StartWork,
|
|
297
|
+
state: ActState,
|
|
298
|
+
) =>
|
|
299
|
+
Effect.gen(function* () {
|
|
300
|
+
if (!options.json && options.inputAllowed === false)
|
|
313
301
|
return yield* Effect.fail(
|
|
314
302
|
new Error(
|
|
315
303
|
"agency act requires interactive input; use --json to list actions for automation",
|
|
316
304
|
),
|
|
317
305
|
)
|
|
318
|
-
}
|
|
319
|
-
const cwd = options.cwd ?? process.cwd()
|
|
320
306
|
const fs = yield* FileSystemService
|
|
321
307
|
const workbase = yield* WorkbaseService
|
|
322
308
|
const graphs = yield* GraphService
|
|
323
309
|
const { log } = createLoggers(options)
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
310
|
+
const cwd = options.cwd ?? process.cwd()
|
|
311
|
+
const path = resolve(cwd, options.directory ?? ".")
|
|
312
|
+
const isDirectory = yield* fs.isDirectory(path)
|
|
313
|
+
const isPath =
|
|
314
|
+
isDirectory || Boolean(options.directory && (yield* fs.exists(path)))
|
|
315
|
+
const { root, config } = yield* workbase.loadConfig(
|
|
316
|
+
isDirectory ? path : isPath ? dirname(path) : cwd,
|
|
317
|
+
)
|
|
318
|
+
state.root = root
|
|
332
319
|
const graph = yield* graphs.get({
|
|
333
320
|
cwd: root,
|
|
334
321
|
loadedConfig: { root, config },
|
|
335
322
|
})
|
|
336
|
-
const executions = executionNodes(graph.nodes)
|
|
337
323
|
const nodes = graph.nodes.filter(
|
|
338
|
-
(node): node is
|
|
324
|
+
(node): node is ActEntity =>
|
|
339
325
|
node.kind === "epic" || node.kind === "task" || node.kind === "phase",
|
|
340
326
|
)
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
327
|
+
const recap = state.recap
|
|
328
|
+
const session =
|
|
329
|
+
state.session ??
|
|
330
|
+
(!options.json &&
|
|
331
|
+
interaction === defaultInteraction &&
|
|
332
|
+
!config.chooserCommand &&
|
|
333
|
+
process.stdin.isTTY &&
|
|
334
|
+
process.stdout.isTTY
|
|
335
|
+
? yield* Effect.acquireRelease(openActSession(), (session) =>
|
|
336
|
+
session.close(),
|
|
337
|
+
)
|
|
338
|
+
: undefined)
|
|
339
|
+
state.session = session
|
|
340
|
+
state.native = Boolean(session)
|
|
341
|
+
session?.notice(state.notice)
|
|
342
|
+
const ui = session?.interaction ?? interaction
|
|
343
|
+
const nativeOptions = {
|
|
344
|
+
cwd: root,
|
|
345
|
+
auto: options.auto,
|
|
346
|
+
draft: options.draft,
|
|
347
|
+
inputAllowed: options.inputAllowed,
|
|
348
|
+
silent: session ? true : options.silent,
|
|
349
|
+
verbose: options.verbose,
|
|
350
|
+
}
|
|
351
|
+
const runWork: StartWork = session
|
|
352
|
+
? (args) => work({ ...args, silent: options.silent })
|
|
353
|
+
: work
|
|
354
|
+
const globals = actActions(graph.nodes, nativeOptions, runWork)
|
|
355
|
+
const catalog = new Map(
|
|
356
|
+
nodes.map((node) => [
|
|
357
|
+
node.id,
|
|
358
|
+
actActions(graph.nodes, nativeOptions, runWork, node),
|
|
359
|
+
]),
|
|
360
|
+
)
|
|
361
|
+
let selectedKey = options.epicId
|
|
362
|
+
? `epic:${options.epicId}`
|
|
363
|
+
: options.phaseId
|
|
364
|
+
? `phase:${options.taskId}/${options.phaseId}`
|
|
365
|
+
: options.taskId
|
|
366
|
+
? `task:${options.taskId}`
|
|
367
|
+
: options.directory
|
|
368
|
+
? isPath
|
|
369
|
+
? pathEntityKey(root, path)
|
|
370
|
+
: `task:${options.directory}`
|
|
371
|
+
: undefined
|
|
372
|
+
let actionId = options.action
|
|
373
|
+
if (session && options.directory && isPath && !selectedKey && !actionId)
|
|
374
|
+
session.activateTab("workbase")
|
|
375
|
+
state.view = !selectedKey && !actionId ? "home" : "flow"
|
|
376
|
+
if (
|
|
377
|
+
actionId &&
|
|
378
|
+
!actionGroups.some((group) => group.actions.some((id) => id === actionId))
|
|
379
|
+
)
|
|
346
380
|
return yield* Effect.fail(
|
|
347
|
-
new Error(
|
|
381
|
+
new Error(
|
|
382
|
+
`Unknown action '${actionId}'; use agency act --json to discover actions`,
|
|
383
|
+
),
|
|
348
384
|
)
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const requestedKey =
|
|
352
|
-
selectedEntityKey(options) ??
|
|
353
|
-
pathEntityKey(options.directory, isDirectory, root, startPath)
|
|
354
|
-
const matchingNodes = requestedKey
|
|
355
|
-
? nodes.filter((node) => entityKey(node) === requestedKey)
|
|
356
|
-
: nodes
|
|
357
|
-
if (requestedKey && matchingNodes.length === 0) {
|
|
385
|
+
const globalAction = globals.find((action) => action.id === actionId)
|
|
386
|
+
if (selectedKey && globalAction)
|
|
358
387
|
return yield* Effect.fail(
|
|
359
|
-
new Error(
|
|
388
|
+
new Error("Workbase actions do not accept an item selector"),
|
|
360
389
|
)
|
|
390
|
+
if (
|
|
391
|
+
selectedKey &&
|
|
392
|
+
!catalog.has(selectedKey) &&
|
|
393
|
+
state.item === selectedKey
|
|
394
|
+
) {
|
|
395
|
+
state.item = undefined
|
|
396
|
+
selectedKey = undefined
|
|
397
|
+
state.view = "home"
|
|
361
398
|
}
|
|
399
|
+
if (selectedKey && !catalog.has(selectedKey))
|
|
400
|
+
return yield* Effect.fail(
|
|
401
|
+
new Error(`Selected work item '${selectedKey}' was not found`),
|
|
402
|
+
)
|
|
362
403
|
if (options.json) {
|
|
404
|
+
const matches = (action: ActAction) => !actionId || action.id === actionId
|
|
363
405
|
log(
|
|
364
406
|
JSON.stringify(
|
|
365
|
-
{
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
407
|
+
yield* Schema.decodeUnknown(ActDiscovery)({
|
|
408
|
+
workbase: {
|
|
409
|
+
root,
|
|
410
|
+
repositories: graph.nodes
|
|
411
|
+
.filter((node) => node.kind === "repository")
|
|
412
|
+
.map((node) => node.key),
|
|
413
|
+
actions: globals
|
|
414
|
+
.filter(matches)
|
|
415
|
+
.map((action) => actionOutput(action, options.auto)),
|
|
416
|
+
},
|
|
417
|
+
currentWork: nodes
|
|
418
|
+
.filter((node) => node.status === "working")
|
|
419
|
+
.map(entitySummary),
|
|
420
|
+
targets: globalAction
|
|
421
|
+
? []
|
|
422
|
+
: nodes
|
|
423
|
+
.filter((node) => !selectedKey || node.id === selectedKey)
|
|
424
|
+
.map((node) => {
|
|
425
|
+
const actions = catalog.get(node.id)!.filter(matches)
|
|
426
|
+
return {
|
|
427
|
+
...entitySummary(node),
|
|
428
|
+
actions: actions
|
|
429
|
+
.filter(available)
|
|
430
|
+
.map((action) => actionOutput(action, options.auto)),
|
|
431
|
+
blockedActions: actions
|
|
432
|
+
.filter((action) => !available(action))
|
|
433
|
+
.map((action) => actionOutput(action, options.auto)),
|
|
434
|
+
}
|
|
435
|
+
}),
|
|
436
|
+
}),
|
|
370
437
|
null,
|
|
371
438
|
2,
|
|
372
439
|
),
|
|
373
440
|
)
|
|
374
441
|
return
|
|
375
442
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
443
|
+
const select = <T>(prompt: string, choices: readonly Choice<T>[]) =>
|
|
444
|
+
Effect.gen(function* () {
|
|
445
|
+
const answer = yield* ui.select(prompt, choices, config.chooserCommand)
|
|
446
|
+
if (answer === null) return yield* Effect.fail(new ActCancelled())
|
|
447
|
+
return answer
|
|
448
|
+
})
|
|
449
|
+
if (!selectedKey && !actionId) {
|
|
450
|
+
const goals = actionGroups.map(({ id, label, icon }) => ({
|
|
451
|
+
key: id,
|
|
452
|
+
label: `${icon} ${label}`,
|
|
453
|
+
plainLabel: label,
|
|
454
|
+
value: id,
|
|
455
|
+
segments: [
|
|
456
|
+
{ text: `${icon} `, color: macchiato.overlay1 },
|
|
457
|
+
{ text: label },
|
|
458
|
+
],
|
|
459
|
+
}))
|
|
460
|
+
let goal: string | undefined = state.goal
|
|
461
|
+
if (goal) {
|
|
462
|
+
state.view = "flow"
|
|
463
|
+
} else if (ui.tabs) {
|
|
464
|
+
type HomeChoice = { kind: "goal" | "item"; id: string }
|
|
465
|
+
const chosen = yield* ui.tabs<HomeChoice>([
|
|
466
|
+
{
|
|
467
|
+
...actTabs[0],
|
|
468
|
+
emptyLabel: "No tasks or phases yet",
|
|
469
|
+
choices: entityChoices(
|
|
470
|
+
nodes.filter(
|
|
471
|
+
(node) => node.kind === "task" || node.kind === "phase",
|
|
472
|
+
),
|
|
473
|
+
true,
|
|
474
|
+
).map((choice) => ({
|
|
475
|
+
...choice,
|
|
476
|
+
value: { kind: "item", id: choice.value },
|
|
477
|
+
})),
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
...actTabs[1],
|
|
481
|
+
choices: goals
|
|
482
|
+
.filter(
|
|
483
|
+
(choice) =>
|
|
484
|
+
!["browse", "split", "handoff"].includes(choice.value),
|
|
485
|
+
)
|
|
486
|
+
.map((choice) => ({
|
|
487
|
+
...choice,
|
|
488
|
+
value: { kind: "goal", id: choice.value },
|
|
489
|
+
})),
|
|
490
|
+
},
|
|
491
|
+
])
|
|
492
|
+
if (!chosen) return yield* Effect.fail(new ActCancelled())
|
|
493
|
+
state.view = "flow"
|
|
494
|
+
if (chosen.kind === "item") selectedKey = chosen.id
|
|
495
|
+
else goal = chosen.id
|
|
496
|
+
} else {
|
|
497
|
+
goal = yield* select("Choose an action", goals)
|
|
498
|
+
state.view = "flow"
|
|
499
|
+
}
|
|
500
|
+
if (goal === "browse") {
|
|
501
|
+
if (!nodes.length)
|
|
502
|
+
return yield* Effect.fail(
|
|
503
|
+
new Error("No work items yet; choose Create a task first"),
|
|
504
|
+
)
|
|
505
|
+
selectedKey = yield* select("Choose an item", entityChoices(nodes))
|
|
506
|
+
} else if (goal) {
|
|
507
|
+
const group = actionGroups.find((group) => group.id === goal)!
|
|
508
|
+
const all = [...globals, ...Array.from(catalog.values()).flat()]
|
|
509
|
+
const choices = group.actions.flatMap((id) => {
|
|
510
|
+
const action = all.find((action) => action.id === id)
|
|
511
|
+
return action ? [action] : []
|
|
512
|
+
})
|
|
513
|
+
if (!choices.length)
|
|
514
|
+
return yield* Effect.fail(
|
|
515
|
+
new Error("No work items yet; choose Create a task first"),
|
|
516
|
+
)
|
|
517
|
+
if (session && choices.length > 1) state.goal = goal
|
|
518
|
+
state.view = "goal-menu"
|
|
519
|
+
actionId =
|
|
520
|
+
choices.length === 1
|
|
521
|
+
? choices[0]!.id
|
|
522
|
+
: yield* select(group.label, actionChoices(choices))
|
|
523
|
+
state.view = "flow"
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (
|
|
527
|
+
!selectedKey &&
|
|
528
|
+
actionId &&
|
|
529
|
+
!globals.some((action) => action.id === actionId)
|
|
530
|
+
) {
|
|
531
|
+
const eligible = nodes.filter((node) =>
|
|
532
|
+
catalog
|
|
533
|
+
.get(node.id)!
|
|
534
|
+
.some((action) => action.id === actionId && available(action)),
|
|
385
535
|
)
|
|
536
|
+
if (!eligible.length)
|
|
537
|
+
return yield* Effect.fail(
|
|
538
|
+
new Error(
|
|
539
|
+
`No eligible items for '${actionId}'; use agency act --action ${actionId} --json for blocked reasons`,
|
|
540
|
+
),
|
|
541
|
+
)
|
|
542
|
+
selectedKey = yield* select("Choose an item", entityChoices(eligible))
|
|
386
543
|
}
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
if (
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
544
|
+
const selected = nodes.find((node) => node.id === selectedKey)
|
|
545
|
+
if (selected && session) {
|
|
546
|
+
state.goal = undefined
|
|
547
|
+
state.item = selected.id
|
|
548
|
+
session.activateTab("workstream")
|
|
549
|
+
}
|
|
550
|
+
const actions = selected ? catalog.get(selected.id)! : globals
|
|
551
|
+
if (!actionId) {
|
|
552
|
+
state.view = "item-menu"
|
|
553
|
+
actionId = yield* select(
|
|
554
|
+
`Act on ${selected!.kind} ${selected!.key}`,
|
|
555
|
+
actionChoices(actions.filter(available)),
|
|
399
556
|
)
|
|
557
|
+
state.view = "flow"
|
|
400
558
|
}
|
|
401
|
-
const
|
|
402
|
-
if (
|
|
559
|
+
const action = actions.find((action) => action.id === actionId)
|
|
560
|
+
if (!action || action.blockedReason)
|
|
403
561
|
return yield* Effect.fail(
|
|
404
562
|
new Error(
|
|
405
|
-
`
|
|
563
|
+
`Action '${actionId}' is unavailable: ${action?.blockedReason ?? "not found"}`,
|
|
406
564
|
),
|
|
407
565
|
)
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const refreshedExecutions = executionNodes(refreshed.nodes)
|
|
418
|
-
const current = refreshed.nodes.find(
|
|
419
|
-
(node): node is EntityNode =>
|
|
420
|
-
(node.kind === "epic" ||
|
|
421
|
-
node.kind === "task" ||
|
|
422
|
-
node.kind === "phase") &&
|
|
423
|
-
entityKey(node) === selectedKey,
|
|
424
|
-
)
|
|
425
|
-
if (!current) {
|
|
426
|
-
return yield* Effect.fail(
|
|
427
|
-
new Error(
|
|
428
|
-
"Selected work item changed or was removed; run agency act again",
|
|
566
|
+
const prepare = (interaction: ActInteraction) =>
|
|
567
|
+
action.prepare(
|
|
568
|
+
actionPrompts(
|
|
569
|
+
interaction,
|
|
570
|
+
graph.nodes
|
|
571
|
+
.filter((node) => node.kind === "repository")
|
|
572
|
+
.map((node) => node.key),
|
|
573
|
+
nodes.filter((node) => node.kind !== "epic").map((node) => node.key),
|
|
574
|
+
config.chooserCommand,
|
|
429
575
|
),
|
|
430
576
|
)
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
577
|
+
const plan = yield* session
|
|
578
|
+
? wizardInputs(
|
|
579
|
+
ui,
|
|
580
|
+
prepare,
|
|
581
|
+
() => !session.quitRequested && !session.navigationRequested,
|
|
582
|
+
(message) => session.notice(message ? ` ${message}` : state.notice),
|
|
583
|
+
)
|
|
584
|
+
: prepare(ui)
|
|
585
|
+
if (selected) {
|
|
586
|
+
const fresh = yield* graphs.get({ cwd: root })
|
|
587
|
+
const current = fresh.nodes.find(
|
|
588
|
+
(node): node is ActEntity =>
|
|
589
|
+
node.id === selected.id &&
|
|
590
|
+
(node.kind === "task" ||
|
|
591
|
+
node.kind === "phase" ||
|
|
592
|
+
node.kind === "epic"),
|
|
593
|
+
)
|
|
594
|
+
if (
|
|
595
|
+
!current ||
|
|
596
|
+
current.data.sha256 !== selected.data.sha256 ||
|
|
597
|
+
!actActions(fresh.nodes, nativeOptions, work, current).some(
|
|
598
|
+
(candidate) => candidate.id === actionId && available(candidate),
|
|
599
|
+
)
|
|
438
600
|
)
|
|
601
|
+
return yield* Effect.fail(
|
|
602
|
+
new Error("Selected work item changed; run agency act again"),
|
|
603
|
+
)
|
|
439
604
|
}
|
|
440
|
-
const { taskId, phaseId } = entityParts(current)
|
|
441
605
|
if (options.dryRun) {
|
|
442
|
-
|
|
606
|
+
for (const command of [plan.command, ...(plan.followUpCommands ?? [])])
|
|
607
|
+
if (session) recap.push(` Preview: ${shellCommand(command)}`)
|
|
608
|
+
else log(shellCommand(command))
|
|
609
|
+
state.notice = ` Preview: ${shellCommand(plan.command)}`
|
|
443
610
|
return
|
|
444
611
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
612
|
+
session?.show(` ${action.label}…`)
|
|
613
|
+
// An interactive worker must receive a restored terminal.
|
|
614
|
+
if (action.id === "work" && session) {
|
|
615
|
+
yield* session.close()
|
|
616
|
+
state.session = undefined
|
|
617
|
+
}
|
|
618
|
+
yield* (
|
|
619
|
+
session && action.id !== "work"
|
|
620
|
+
? Effect.raceFirst(plan.run, session.cancelled)
|
|
621
|
+
: plan.run
|
|
622
|
+
).pipe(
|
|
623
|
+
Effect.tapError((error) =>
|
|
624
|
+
Effect.sync(() => {
|
|
625
|
+
if (session) {
|
|
626
|
+
recap.push(
|
|
627
|
+
` ${action.label} ${error instanceof ActCancelled ? "interrupted; check item state before retrying" : "failed"}${selected ? ` — ${selected.key}` : ""}`,
|
|
628
|
+
)
|
|
629
|
+
state.notice = recap[recap.length - 1]!
|
|
630
|
+
}
|
|
631
|
+
}),
|
|
632
|
+
),
|
|
633
|
+
)
|
|
634
|
+
if (session) {
|
|
635
|
+
recap.push(` ${action.label}${selected ? ` — ${selected.key}` : ""}`)
|
|
636
|
+
state.notice = recap[recap.length - 1]!
|
|
637
|
+
if (plan.next) {
|
|
638
|
+
recap.push(
|
|
639
|
+
`Item: ${plan.next.taskId}${plan.next.phaseId ? `/${plan.next.phaseId}` : ""}`,
|
|
640
|
+
)
|
|
641
|
+
state.notice += ` — ${plan.next.taskId}${plan.next.phaseId ? `/${plan.next.phaseId}` : ""}`
|
|
642
|
+
}
|
|
643
|
+
recap.push(`Command: ${shellCommand(plan.command)}`)
|
|
644
|
+
if (action.id === "current-work") {
|
|
645
|
+
const current = nodes.filter((node) => node.status === "working")
|
|
646
|
+
state.notice = current.length
|
|
647
|
+
? ` Working: ${current.map((node) => node.key).join(", ")}`
|
|
648
|
+
: " No work is in progress."
|
|
649
|
+
recap.push(
|
|
650
|
+
...(current.length
|
|
651
|
+
? current.map(
|
|
652
|
+
(node) =>
|
|
653
|
+
`${node.kind} ${node.key} · ${node.repositories.join(", ")}${"description" in node.data && node.data.description ? ` — ${node.data.description}` : ""}`,
|
|
654
|
+
)
|
|
655
|
+
: ["No work is in progress."]),
|
|
656
|
+
)
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
if (plan.next) {
|
|
660
|
+
const next = yield* select("Created. What next?", [
|
|
661
|
+
{
|
|
662
|
+
key: "finish",
|
|
663
|
+
label: session
|
|
664
|
+
? "Keep for later — return to tabs"
|
|
665
|
+
: "Finish — keep this item for later",
|
|
666
|
+
value: "finish",
|
|
667
|
+
},
|
|
668
|
+
{ key: "work", label: "Work on the new item now", value: "work" },
|
|
669
|
+
])
|
|
670
|
+
if (next === "work") {
|
|
671
|
+
if (session) {
|
|
672
|
+
yield* session.close()
|
|
673
|
+
state.session = undefined
|
|
505
674
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
type: current.kind,
|
|
510
|
-
args: current.kind === "phase" ? [taskId, phaseId!] : [current.key],
|
|
511
|
-
cwd,
|
|
512
|
-
silent: options.silent,
|
|
513
|
-
verbose: options.verbose,
|
|
514
|
-
})
|
|
675
|
+
yield* work({ ...nativeOptions, silent: options.silent, ...plan.next })
|
|
676
|
+
if (session) recap.push(" Work handoff completed")
|
|
677
|
+
} else if (session) recap.push("Kept for later — work was not started.")
|
|
515
678
|
}
|
|
516
679
|
})
|
|
517
680
|
|
|
518
681
|
export const help = `
|
|
519
|
-
Usage: agency act [<directory-or-task-id> | --epic <id> | --task <id> [--phase <id>]] [--dry-run | --json] [--auto] [--draft]
|
|
682
|
+
Usage: agency act [<directory-or-task-id> | --epic <id> | --task <id> [--phase <id>]] [--action <id>] [--dry-run | --json] [--auto] [--draft]
|
|
520
683
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
684
|
+
Choose a task/phase in Workstream, or press Tab for Workbase actions. Guided
|
|
685
|
+
creation offers an explicit Work choice afterward; creation alone never starts work.
|
|
686
|
+
Item actions stay on that item. Escape clears input, then steps back through
|
|
687
|
+
wizard prompts, the action menu, and Workstream,
|
|
688
|
+
or exits from the front screen; Ctrl-C quits. A recap is printed when you exit.
|
|
689
|
+
An existing directory or file selects its containing epic, task, or phase.
|
|
690
|
+
A workbase path opens Workbase actions. Otherwise the positional value is a
|
|
691
|
+
task ID. Selectors skip item selection.
|
|
527
692
|
|
|
528
693
|
Options:
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
694
|
+
--action <id> Start an action or filter discovery (IDs from --json)
|
|
695
|
+
--epic <id> Select an epic
|
|
696
|
+
--task <id> Select a task
|
|
697
|
+
--phase <id> Select a phase; requires --task
|
|
698
|
+
--dry-run Collect inputs and print commands without executing
|
|
699
|
+
--json Discover actions, required inputs, argv, and blocked reasons
|
|
534
700
|
--auto Pass --auto when starting or continuing work
|
|
535
701
|
--draft Create a draft pull request
|
|
536
702
|
`
|