@markjaquith/agency 3.9.5 → 3.10.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 +3 -1
- package/package.json +1 -1
- package/src/commands/act-actions.ts +62 -1
- package/src/commands/act-prompts.ts +12 -0
- package/src/commands/act.ts +61 -6
package/README.md
CHANGED
|
@@ -568,7 +568,9 @@ of non-archived tasks and phases. When run inside an epic, task, phase, or one o
|
|
|
568
568
|
its checkout descendants, it opens that item's actions first; Escape returns to
|
|
569
569
|
Workstream, where **Tab** reaches global Workbase actions. `agency .` is shorthand
|
|
570
570
|
for `agency act .` and explicitly focuses the item containing the current path.
|
|
571
|
-
Otherwise, select an item to see its available actions.
|
|
571
|
+
Otherwise, select an item to see its available actions. After an action,
|
|
572
|
+
Agency reloads the graph and returns to Workstream so changed items and available
|
|
573
|
+
actions are current, and archived items no longer appear.
|
|
572
574
|
Press **Tab** to cycle to **Workbase** for workbase actions. Both tabs have a blank
|
|
573
575
|
row above a chevron-prefixed filter with the placeholder “type to filter”.
|
|
574
576
|
Each tab remembers its filter and selection. The active
|
package/package.json
CHANGED
|
@@ -17,6 +17,7 @@ import { status } from "./status"
|
|
|
17
17
|
import { pr, prCreate } from "./pr"
|
|
18
18
|
import { validate } from "./validate"
|
|
19
19
|
import { work as startWork, type StartWork } from "./work"
|
|
20
|
+
import { worktree } from "./worktree"
|
|
20
21
|
import { macchiato } from "../utils/theme"
|
|
21
22
|
|
|
22
23
|
export type ActEntity = Extract<
|
|
@@ -37,6 +38,7 @@ type NativeOperation =
|
|
|
37
38
|
| ReturnType<typeof validate>
|
|
38
39
|
| ReturnType<typeof prCreate>
|
|
39
40
|
| ReturnType<typeof archive>
|
|
41
|
+
| ReturnType<typeof worktree>
|
|
40
42
|
| ReturnType<StartWork>
|
|
41
43
|
type Operation = Effect.Effect<
|
|
42
44
|
unknown,
|
|
@@ -72,6 +74,12 @@ export interface ActAction extends Details {
|
|
|
72
74
|
readonly prepare: (prompts: ActionPrompts) => Effect.Effect<Plan, Error>
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
export interface ActCheckoutState {
|
|
78
|
+
readonly paths: readonly string[]
|
|
79
|
+
readonly conflicts: readonly string[]
|
|
80
|
+
readonly dirty: boolean
|
|
81
|
+
}
|
|
82
|
+
|
|
75
83
|
const actionPresentation: Record<
|
|
76
84
|
string,
|
|
77
85
|
{
|
|
@@ -205,6 +213,12 @@ const actionPresentation: Record<
|
|
|
205
213
|
icon: "",
|
|
206
214
|
color: macchiato.green,
|
|
207
215
|
},
|
|
216
|
+
"worktree-remove": {
|
|
217
|
+
description:
|
|
218
|
+
"Remove clean local worktrees while preserving the item, commits, and branches. Agency recreates the checkout when you work on this item again.",
|
|
219
|
+
icon: "",
|
|
220
|
+
color: macchiato.red,
|
|
221
|
+
},
|
|
208
222
|
push: {
|
|
209
223
|
description: "Validate and publish this execution branch without a PR.",
|
|
210
224
|
icon: "",
|
|
@@ -440,7 +454,11 @@ export const actionGroups = [
|
|
|
440
454
|
|
|
441
455
|
export const actActions = (
|
|
442
456
|
nodes: readonly GraphNode[],
|
|
443
|
-
options: BaseCommandOptions & {
|
|
457
|
+
options: BaseCommandOptions & {
|
|
458
|
+
auto?: boolean
|
|
459
|
+
draft?: boolean
|
|
460
|
+
checkoutStates?: ReadonlyMap<string, ActCheckoutState>
|
|
461
|
+
},
|
|
444
462
|
work: StartWork = startWork,
|
|
445
463
|
node?: ActEntity,
|
|
446
464
|
): readonly ActAction[] => {
|
|
@@ -878,6 +896,26 @@ export const actActions = (
|
|
|
878
896
|
blockedReason: validation ?? reason,
|
|
879
897
|
})
|
|
880
898
|
const noExecution = execution ? null : "Select an execution unit"
|
|
899
|
+
const checkoutState = options.checkoutStates?.get(node.id)
|
|
900
|
+
const activeClaim =
|
|
901
|
+
"claim" in node.data &&
|
|
902
|
+
typeof node.data.claim === "object" &&
|
|
903
|
+
node.data.claim !== null &&
|
|
904
|
+
"state" in node.data.claim &&
|
|
905
|
+
node.data.claim.state === "active"
|
|
906
|
+
const checkoutBlockedReason = noExecution
|
|
907
|
+
? noExecution
|
|
908
|
+
: !checkoutState
|
|
909
|
+
? "Local checkout is not materialized"
|
|
910
|
+
: activeClaim
|
|
911
|
+
? "Item has an active claim; release or finish it before clearing its checkout"
|
|
912
|
+
: node.status === "working" || node.status === "delegated"
|
|
913
|
+
? `Item has active '${node.status}' ownership; reopen it before clearing its checkout`
|
|
914
|
+
: checkoutState.conflicts.length
|
|
915
|
+
? checkoutState.conflicts.join("; ")
|
|
916
|
+
: checkoutState.dirty
|
|
917
|
+
? "Local checkout has uncommitted changes"
|
|
918
|
+
: null
|
|
881
919
|
const statusCommand = (status: string) => [
|
|
882
920
|
"agency",
|
|
883
921
|
target.phaseId ? "phase" : "task",
|
|
@@ -937,6 +975,29 @@ export const actActions = (
|
|
|
937
975
|
run: Effect.suspend(() => work({ ...options, ...workTarget })),
|
|
938
976
|
},
|
|
939
977
|
),
|
|
978
|
+
action(
|
|
979
|
+
{
|
|
980
|
+
id: "worktree-remove",
|
|
981
|
+
label: "Clear local checkout",
|
|
982
|
+
blockedReason: checkoutBlockedReason,
|
|
983
|
+
},
|
|
984
|
+
undefined,
|
|
985
|
+
(p) =>
|
|
986
|
+
options.dryRun
|
|
987
|
+
? Effect.void
|
|
988
|
+
: p.confirm(
|
|
989
|
+
`Clear these local worktrees?\n${checkoutState?.paths.map((path) => ` ${path}`).join("\n") ?? ""}\n\nThe item, commits, and branches will be preserved.`,
|
|
990
|
+
"Clear local checkout",
|
|
991
|
+
),
|
|
992
|
+
() => ({
|
|
993
|
+
command: ["agency", "worktree", "remove", ...args],
|
|
994
|
+
run: worktree({
|
|
995
|
+
...options,
|
|
996
|
+
subcommand: "remove",
|
|
997
|
+
args,
|
|
998
|
+
}),
|
|
999
|
+
}),
|
|
1000
|
+
),
|
|
940
1001
|
immediate(
|
|
941
1002
|
details(
|
|
942
1003
|
"push",
|
|
@@ -248,6 +248,18 @@ export const actionPrompts = (
|
|
|
248
248
|
})
|
|
249
249
|
return {
|
|
250
250
|
text,
|
|
251
|
+
confirm: (prompt: string, label = "Confirm") =>
|
|
252
|
+
Effect.gen(function* () {
|
|
253
|
+
const confirmed = yield* interaction.select(
|
|
254
|
+
prompt,
|
|
255
|
+
[
|
|
256
|
+
{ key: "confirm", label, value: true },
|
|
257
|
+
{ key: "cancel", label: "Cancel", value: false },
|
|
258
|
+
],
|
|
259
|
+
chooser,
|
|
260
|
+
)
|
|
261
|
+
if (!confirmed) return yield* Effect.fail(new ActCancelled())
|
|
262
|
+
}),
|
|
251
263
|
id: (label: string, suggestion: string, prefix = "") => {
|
|
252
264
|
const base = slug(suggestion)
|
|
253
265
|
let candidate = base
|
package/src/commands/act.ts
CHANGED
|
@@ -5,6 +5,7 @@ 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
|
+
import { WorktreeService } from "../services/WorktreeService"
|
|
8
9
|
import type { BaseCommandOptions } from "../utils/command"
|
|
9
10
|
import type { Choice } from "../utils/chooser"
|
|
10
11
|
import { createLoggers } from "../utils/effect"
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
isActActionId,
|
|
18
19
|
type ActAction,
|
|
19
20
|
type ActEntity,
|
|
21
|
+
type ActCheckoutState,
|
|
20
22
|
} from "./act-actions"
|
|
21
23
|
import {
|
|
22
24
|
ActCancelled,
|
|
@@ -170,17 +172,27 @@ const entitySummary = (node: ActEntity) => ({
|
|
|
170
172
|
readiness: node.readiness,
|
|
171
173
|
revision: node.data.sha256,
|
|
172
174
|
})
|
|
175
|
+
|
|
173
176
|
const actionChoices = (actions: readonly ActAction[]): Choice<string>[] =>
|
|
174
|
-
actions.map(({ id, label, description, icon, color }) => {
|
|
177
|
+
actions.map(({ id, label, description, icon, color, blockedReason }) => {
|
|
178
|
+
const availability = blockedReason ? ` — unavailable: ${blockedReason}` : ""
|
|
175
179
|
return {
|
|
176
180
|
key: id,
|
|
177
181
|
value: id,
|
|
178
|
-
label: `${icon} ${label} — ${description}`,
|
|
179
|
-
plainLabel: `${label} — ${description}`,
|
|
182
|
+
label: `${icon} ${label} — ${description}${availability}`,
|
|
183
|
+
plainLabel: `${label} — ${description}${availability}`,
|
|
180
184
|
segments: [
|
|
181
185
|
{ text: `${icon} `, color },
|
|
182
186
|
{ text: label },
|
|
183
187
|
{ text: ` — ${description}`, color: macchiato.overlay0 },
|
|
188
|
+
...(blockedReason
|
|
189
|
+
? [
|
|
190
|
+
{
|
|
191
|
+
text: availability,
|
|
192
|
+
color: macchiato.yellow,
|
|
193
|
+
},
|
|
194
|
+
]
|
|
195
|
+
: []),
|
|
184
196
|
],
|
|
185
197
|
}
|
|
186
198
|
})
|
|
@@ -222,6 +234,7 @@ export const act = (
|
|
|
222
234
|
Effect.tap(() =>
|
|
223
235
|
Effect.sync(() => {
|
|
224
236
|
state.goal = undefined
|
|
237
|
+
state.item = undefined
|
|
225
238
|
}),
|
|
226
239
|
),
|
|
227
240
|
Effect.as(true),
|
|
@@ -240,7 +253,12 @@ export const act = (
|
|
|
240
253
|
return Effect.succeed(true)
|
|
241
254
|
}),
|
|
242
255
|
)
|
|
243
|
-
if (
|
|
256
|
+
if (
|
|
257
|
+
(!state.native && options.exitOnEscape !== false) ||
|
|
258
|
+
!keepGoing ||
|
|
259
|
+
state.session?.quitRequested
|
|
260
|
+
)
|
|
261
|
+
break
|
|
244
262
|
state.session?.resetCancellation()
|
|
245
263
|
if (state.session?.takeNavigation()) {
|
|
246
264
|
state.item = undefined
|
|
@@ -294,6 +312,7 @@ const actStep = (
|
|
|
294
312
|
const fs = yield* FileSystemService
|
|
295
313
|
const workbase = yield* WorkbaseService
|
|
296
314
|
const graphs = yield* GraphService
|
|
315
|
+
const worktrees = yield* WorktreeService
|
|
297
316
|
const { log } = createLoggers(options)
|
|
298
317
|
const cwd = options.cwd ?? process.cwd()
|
|
299
318
|
const path = resolve(cwd, options.directory ?? ".")
|
|
@@ -307,7 +326,35 @@ const actStep = (
|
|
|
307
326
|
const graph = yield* graphs.get({
|
|
308
327
|
cwd: root,
|
|
309
328
|
loadedConfig: { root, config },
|
|
329
|
+
include: ["workspace"],
|
|
310
330
|
})
|
|
331
|
+
const checkoutStates = new Map<string, ActCheckoutState>()
|
|
332
|
+
const materialized = graph.nodes.some(
|
|
333
|
+
(node) => node.kind === "execution-unit" && node.workspace?.materialized,
|
|
334
|
+
)
|
|
335
|
+
for (const inspection of materialized
|
|
336
|
+
? yield* worktrees.list(root, { materializedOnly: true })
|
|
337
|
+
: []) {
|
|
338
|
+
const id =
|
|
339
|
+
inspection.owner.kind === "phase"
|
|
340
|
+
? `phase:${inspection.owner.taskId}/${inspection.owner.phaseId}`
|
|
341
|
+
: `task:${inspection.owner.taskId}`
|
|
342
|
+
checkoutStates.set(id, {
|
|
343
|
+
paths: inspection.checkouts.flatMap((checkout) =>
|
|
344
|
+
checkout.exists
|
|
345
|
+
? [checkout.path]
|
|
346
|
+
: checkout.registeredPath
|
|
347
|
+
? [checkout.registeredPath]
|
|
348
|
+
: [],
|
|
349
|
+
),
|
|
350
|
+
conflicts: inspection.conflicts
|
|
351
|
+
.filter((conflict) => conflict.kind !== "stale-registration")
|
|
352
|
+
.map((conflict) => conflict.message),
|
|
353
|
+
dirty: inspection.checkouts.some(
|
|
354
|
+
(checkout) => checkout.exists && checkout.dirty !== false,
|
|
355
|
+
),
|
|
356
|
+
})
|
|
357
|
+
}
|
|
311
358
|
const nodes = graph.nodes.filter(
|
|
312
359
|
(node): node is ActEntity =>
|
|
313
360
|
node.kind === "epic" || node.kind === "task" || node.kind === "phase",
|
|
@@ -335,6 +382,7 @@ const actStep = (
|
|
|
335
382
|
inputAllowed: options.inputAllowed,
|
|
336
383
|
silent: session ? true : options.silent,
|
|
337
384
|
verbose: options.verbose,
|
|
385
|
+
checkoutStates,
|
|
338
386
|
}
|
|
339
387
|
const runWork: StartWork = session
|
|
340
388
|
? (args) => work({ ...args, silent: options.silent })
|
|
@@ -535,9 +583,15 @@ const actStep = (
|
|
|
535
583
|
const actions = selected ? catalog.get(selected.id)! : globals
|
|
536
584
|
if (!actionId) {
|
|
537
585
|
state.view = "item-menu"
|
|
586
|
+
const menuActions = actions.filter(
|
|
587
|
+
(action) =>
|
|
588
|
+
available(action) ||
|
|
589
|
+
(action.id === "worktree-remove" &&
|
|
590
|
+
action.blockedReason !== "Local checkout is not materialized"),
|
|
591
|
+
)
|
|
538
592
|
actionId = yield* select(
|
|
539
593
|
`Act on ${selected!.kind} ${selected!.key}`,
|
|
540
|
-
actionChoices(
|
|
594
|
+
actionChoices(menuActions),
|
|
541
595
|
)
|
|
542
596
|
state.view = "flow"
|
|
543
597
|
}
|
|
@@ -668,7 +722,8 @@ Usage: agency act [<directory-or-task-id> | --epic <id> | --task <id> [--phase <
|
|
|
668
722
|
|
|
669
723
|
Choose a task/phase in Workstream, or press Tab for Workbase actions. Guided
|
|
670
724
|
creation offers an explicit Work choice afterward; creation alone never starts work.
|
|
671
|
-
|
|
725
|
+
Completed item actions return to a freshly loaded Workstream. Escape clears input,
|
|
726
|
+
then steps back through
|
|
672
727
|
wizard prompts, the action menu, and Workstream,
|
|
673
728
|
or exits from the front screen; Ctrl-C quits. A recap is printed when you exit.
|
|
674
729
|
An existing directory or file selects its containing epic, task, or phase.
|