@markjaquith/agency 3.9.6 → 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/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 +52 -4
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
|
})
|
|
@@ -300,6 +312,7 @@ const actStep = (
|
|
|
300
312
|
const fs = yield* FileSystemService
|
|
301
313
|
const workbase = yield* WorkbaseService
|
|
302
314
|
const graphs = yield* GraphService
|
|
315
|
+
const worktrees = yield* WorktreeService
|
|
303
316
|
const { log } = createLoggers(options)
|
|
304
317
|
const cwd = options.cwd ?? process.cwd()
|
|
305
318
|
const path = resolve(cwd, options.directory ?? ".")
|
|
@@ -313,7 +326,35 @@ const actStep = (
|
|
|
313
326
|
const graph = yield* graphs.get({
|
|
314
327
|
cwd: root,
|
|
315
328
|
loadedConfig: { root, config },
|
|
329
|
+
include: ["workspace"],
|
|
316
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
|
+
}
|
|
317
358
|
const nodes = graph.nodes.filter(
|
|
318
359
|
(node): node is ActEntity =>
|
|
319
360
|
node.kind === "epic" || node.kind === "task" || node.kind === "phase",
|
|
@@ -341,6 +382,7 @@ const actStep = (
|
|
|
341
382
|
inputAllowed: options.inputAllowed,
|
|
342
383
|
silent: session ? true : options.silent,
|
|
343
384
|
verbose: options.verbose,
|
|
385
|
+
checkoutStates,
|
|
344
386
|
}
|
|
345
387
|
const runWork: StartWork = session
|
|
346
388
|
? (args) => work({ ...args, silent: options.silent })
|
|
@@ -541,9 +583,15 @@ const actStep = (
|
|
|
541
583
|
const actions = selected ? catalog.get(selected.id)! : globals
|
|
542
584
|
if (!actionId) {
|
|
543
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
|
+
)
|
|
544
592
|
actionId = yield* select(
|
|
545
593
|
`Act on ${selected!.kind} ${selected!.key}`,
|
|
546
|
-
actionChoices(
|
|
594
|
+
actionChoices(menuActions),
|
|
547
595
|
)
|
|
548
596
|
state.view = "flow"
|
|
549
597
|
}
|