@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "3.9.5",
3
+ "version": "3.10.0",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -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 & { auto?: boolean; draft?: boolean },
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
@@ -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 (!state.native || !keepGoing || state.session?.quitRequested) break
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(actions.filter(available)),
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
- Item actions stay on that item. Escape clears input, then steps back through
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.