@markjaquith/agency 3.7.0 → 3.8.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
@@ -616,18 +616,21 @@ handoffs restore the terminal before starting the interactive worker, then reope
616
616
  the TUI when the worker returns. A configured
617
617
  external chooser continues to offer goals and **Browse items** through that chooser.
618
618
 
619
- | Goal | Choose in `act` | Discovery/action ID |
620
- | ------------------------------------ | ------------------------------------------------------------------------------------------------- | ------------------------------------- |
621
- | Add a repository | Add from a remote, or link an existing local repository | `repo-add`, `repo-link` |
622
- | Create a task | Create a task → standard task or investigation; describe the outcome | `task-create`, `investigation-create` |
623
- | Split work | Add a phase / split this task; name the existing work's first phase | `split` |
624
- | Work on a task or phase | Work on this item | `work` |
625
- | Move from investigation to execution | Create implementation follow-up | `handoff` |
626
- | Review someone else's work | Review a PR, or a remote branch/commit | `review`, `review-ref` |
627
- | Close finished work | Close or reopen work → complete without a PR, drop abandoned work, refresh a merged PR, or reopen | `complete`, `drop`, `sync`, `reopen` |
628
- | Update PR status | Refresh Agency state from the provider, create a PR, mark a GitHub PR ready, or close it | `sync`, `pr`, `pr-ready`, `pr-close` |
629
- | Archive terminal work | Archive | `archive` |
630
- | See current work | See current work | `current-work` |
619
+ | Goal | Choose in `act` | Discovery/action ID |
620
+ | ------------------------------------ | ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
621
+ | Manage repositories | Add, link, set up, materialize, fetch, verify, rename, update, unlink, or remove aliases | `repo-add`, `repo-link`, `repo-setup`, `repo-materialize`, `repo-fetch`, `repo-verify`, `repo-rename`, `repo-remote`, `repo-unlink`, `repo-remove` |
622
+ | Create work | Create a standard task, multi-phase task, investigation, epic, or a task inside an epic | `task-create`, `multi-phase-create`, `investigation-create`, `epic-create`, `task-create-in-epic` |
623
+ | Split work | Add a phase / split this task; name the existing work's first phase | `split` |
624
+ | Work on a task or phase | Work on this item | `work` |
625
+ | Move from investigation to execution | Create implementation follow-up | `handoff` |
626
+ | Review someone else's work | Review a PR, or a remote branch/commit | `review`, `review-ref` |
627
+ | Close finished work | Close or reopen work → complete without a PR, drop abandoned work, refresh a merged PR, or reopen | `complete`, `drop`, `sync`, `reopen` |
628
+ | Publish or update PR status | Push without a PR, refresh provider state, create a PR, mark a GitHub PR ready, or close it | `push`, `sync`, `pr`, `pr-ready`, `pr-close` |
629
+ | Maintain review work | Fetch and repin a review task to its current source | `review-refresh` |
630
+ | Organize work | Rename an item, move a task into/out of an epic, or add/remove a sibling dependency | `rename`, `move-to-epic`, `remove-from-epic`, `dependency-add`, `dependency-remove` |
631
+ | Archive terminal work | Archive | `archive` |
632
+ | See work status | See current or ready work | `current-work`, `ready-work` |
633
+ | Check or refresh the workbase | Validate, diagnose, reconcile all state, or inspect/update managed integration files | `validate`, `doctor`, `sync-all`, `integration-status`, `integration-sync` |
631
634
 
632
635
  After creating a task, phase, review, or implementation follow-up, choose **Work
633
636
  on the new item now** or **Finish**. Only choosing Work prepares checkouts and
@@ -663,6 +666,8 @@ For agents, `--json` never prompts or executes. Its compact, runtime-validated
663
666
  result includes workbase actions and repository aliases, current working items,
664
667
  and matching targets with readiness, document revisions, available `actions`,
665
668
  and `blockedActions` with reasons. `--action` filters discovery to one scenario.
669
+ Every action includes a concise `description`, Nerd Font `icon`, and semantic
670
+ hex `color`, in addition to its label and availability.
666
671
  Targets and current working items share the same identity and metadata fields:
667
672
  `id`, `kind`, `key`, `status`, `description`, `repo` (the declared main repository,
668
673
  when present), `repositories`, `readiness`, and `revision`. Descriptions retain
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "3.7.0",
3
+ "version": "3.8.0",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
package/src/act-schema.ts CHANGED
@@ -6,6 +6,9 @@ const Argv = Schema.Array(Schema.String)
6
6
  const Action = Schema.Struct({
7
7
  id: Schema.String,
8
8
  label: Schema.optional(Schema.String),
9
+ description: Schema.String,
10
+ icon: Schema.String,
11
+ color: Schema.String,
9
12
  available: Schema.Boolean,
10
13
  blockedReason: Schema.optional(Schema.NullOr(Schema.String)),
11
14
  inputs: Schema.optional(
package/src/cli-parser.ts CHANGED
@@ -499,7 +499,14 @@ const commands = {
499
499
  usage: "agency task status <id> <status> [options] [--json]",
500
500
  minArgs: 2,
501
501
  maxArgs: 2,
502
- options: ["json", "task", "no-pull-request", "summary", "evidence-url"],
502
+ options: [
503
+ "json",
504
+ "task",
505
+ "if-revision",
506
+ "no-pull-request",
507
+ "summary",
508
+ "evidence-url",
509
+ ],
503
510
  },
504
511
  update: {
505
512
  usage: "agency task update <id> [options] [--json]",
@@ -630,6 +637,7 @@ const commands = {
630
637
  "json",
631
638
  "task",
632
639
  "phase",
640
+ "if-revision",
633
641
  "no-pull-request",
634
642
  "summary",
635
643
  "evidence-url",
@@ -1,15 +1,23 @@
1
1
  import { Effect } from "effect"
2
+ import { join } from "node:path"
2
3
  import type { GraphNode } from "../graph-schema"
3
4
  import type { BaseCommandOptions } from "../utils/command"
4
5
  import type { ActionPrompts } from "./act-prompts"
5
6
  import { archive } from "./archive"
7
+ import { doctor } from "./doctor"
8
+ import { epic } from "./epic"
9
+ import { integration } from "./integration"
6
10
  import { repo } from "./repo"
11
+ import { review } from "./review"
7
12
  import { task } from "./task"
8
13
  import { phase } from "./phase"
14
+ import { push } from "./push"
9
15
  import { sync } from "./sync"
10
16
  import { status } from "./status"
11
17
  import { pr, prCreate } from "./pr"
18
+ import { validate } from "./validate"
12
19
  import { work as startWork, type StartWork } from "./work"
20
+ import { macchiato } from "../utils/theme"
13
21
 
14
22
  export type ActEntity = Extract<
15
23
  GraphNode,
@@ -18,9 +26,15 @@ export type ActEntity = Extract<
18
26
  type NativeOperation =
19
27
  | ReturnType<typeof task>
20
28
  | ReturnType<typeof phase>
29
+ | ReturnType<typeof epic>
21
30
  | ReturnType<typeof repo>
31
+ | ReturnType<typeof review>
32
+ | ReturnType<typeof push>
22
33
  | ReturnType<typeof sync>
23
34
  | ReturnType<typeof status>
35
+ | ReturnType<typeof doctor>
36
+ | ReturnType<typeof integration>
37
+ | ReturnType<typeof validate>
24
38
  | ReturnType<typeof prCreate>
25
39
  | ReturnType<typeof archive>
26
40
  | ReturnType<StartWork>
@@ -50,11 +64,241 @@ interface Details {
50
64
  readonly inputs?: readonly Input[]
51
65
  }
52
66
  export interface ActAction extends Details {
67
+ readonly description: string
68
+ readonly icon: string
69
+ readonly color: string
53
70
  readonly preview: Plan
54
71
  /** Replayable input collection. All mutations belong in the returned plan.run. */
55
72
  readonly prepare: (prompts: ActionPrompts) => Effect.Effect<Plan, Error>
56
73
  }
57
74
 
75
+ const actionPresentation: Record<
76
+ string,
77
+ {
78
+ readonly description: string
79
+ readonly icon: string
80
+ readonly color: string
81
+ }
82
+ > = {
83
+ "repo-add": {
84
+ description: "Clone a remote repository into this workbase.",
85
+ icon: "󰐕",
86
+ color: macchiato.green,
87
+ },
88
+ "repo-link": {
89
+ description: "Use an existing local checkout as a repository alias.",
90
+ icon: "󰌷",
91
+ color: macchiato.sapphire,
92
+ },
93
+ "repo-setup": {
94
+ description: "Materialize all portable repository declarations.",
95
+ icon: "󰋊",
96
+ color: macchiato.blue,
97
+ },
98
+ "repo-materialize": {
99
+ description: "Replace a linked alias with a managed bare clone.",
100
+ icon: "󰆧",
101
+ color: macchiato.blue,
102
+ },
103
+ "repo-fetch": {
104
+ description: "Fetch and prune a repository alias.",
105
+ icon: "󰑐",
106
+ color: macchiato.sapphire,
107
+ },
108
+ "repo-verify": {
109
+ description: "Check that a repository alias is operational.",
110
+ icon: "󰄬",
111
+ color: macchiato.green,
112
+ },
113
+ "repo-rename": {
114
+ description: "Rename an unused repository alias.",
115
+ icon: "󰑕",
116
+ color: macchiato.yellow,
117
+ },
118
+ "repo-remote": {
119
+ description: "Update an alias's portable remote declaration.",
120
+ icon: "󰛳",
121
+ color: macchiato.sapphire,
122
+ },
123
+ "repo-unlink": {
124
+ description: "Remove only this machine's linked checkout.",
125
+ icon: "󰌺",
126
+ color: macchiato.red,
127
+ },
128
+ "repo-remove": {
129
+ description: "Remove an unused alias and its local materialization.",
130
+ icon: "󰆴",
131
+ color: macchiato.red,
132
+ },
133
+ "task-create": {
134
+ description: "Create one deliverable execution task.",
135
+ icon: "󰐕",
136
+ color: macchiato.green,
137
+ },
138
+ "multi-phase-create": {
139
+ description: "Create a task that will be delivered through phases.",
140
+ icon: "",
141
+ color: macchiato.yellow,
142
+ },
143
+ "investigation-create": {
144
+ description: "Create evidence-gathering work before implementation.",
145
+ icon: "󰍉",
146
+ color: macchiato.mauve,
147
+ },
148
+ "epic-create": {
149
+ description: "Create an epic to coordinate related tasks.",
150
+ icon: "",
151
+ color: macchiato.mauve,
152
+ },
153
+ "task-create-in-epic": {
154
+ description: "Create a new execution task inside this epic.",
155
+ icon: "󰐕",
156
+ color: macchiato.green,
157
+ },
158
+ review: {
159
+ description: "Create a task pinned to an existing pull request.",
160
+ icon: "󰍉",
161
+ color: macchiato.mauve,
162
+ },
163
+ "review-ref": {
164
+ description: "Create a task pinned to a remote branch or commit.",
165
+ icon: "󰓹",
166
+ color: macchiato.mauve,
167
+ },
168
+ "current-work": {
169
+ description: "Show tasks and phases currently being worked.",
170
+ icon: "",
171
+ color: macchiato.blue,
172
+ },
173
+ "ready-work": {
174
+ description: "Show execution units that are ready to start.",
175
+ icon: "󰄱",
176
+ color: macchiato.green,
177
+ },
178
+ validate: {
179
+ description: "Validate workbase documents and relationships.",
180
+ icon: "󰄬",
181
+ color: macchiato.green,
182
+ },
183
+ doctor: {
184
+ description: "Diagnose workbase, repository, and integration health.",
185
+ icon: "󰒡",
186
+ color: macchiato.yellow,
187
+ },
188
+ "sync-all": {
189
+ description: "Reconcile all work with repository and provider state.",
190
+ icon: "󰑓",
191
+ color: macchiato.sapphire,
192
+ },
193
+ "integration-status": {
194
+ description: "Inspect managed agent integration files.",
195
+ icon: "󰋼",
196
+ color: macchiato.blue,
197
+ },
198
+ "integration-sync": {
199
+ description: "Update safe managed agent integration files.",
200
+ icon: "󰑐",
201
+ color: macchiato.sapphire,
202
+ },
203
+ work: {
204
+ description: "Prepare this item and start its configured worker.",
205
+ icon: "",
206
+ color: macchiato.green,
207
+ },
208
+ push: {
209
+ description: "Validate and publish this execution branch without a PR.",
210
+ icon: "󰜷",
211
+ color: macchiato.sapphire,
212
+ },
213
+ pr: {
214
+ description: "Publish this execution branch and record its pull request.",
215
+ icon: "",
216
+ color: macchiato.mauve,
217
+ },
218
+ "review-refresh": {
219
+ description: "Fetch and repin this review task to its current source.",
220
+ icon: "󰑐",
221
+ color: macchiato.sapphire,
222
+ },
223
+ reopen: {
224
+ description: "Return terminal work to open status.",
225
+ icon: "󰑓",
226
+ color: macchiato.sapphire,
227
+ },
228
+ drop: {
229
+ description: "Abandon this item without satisfying dependents.",
230
+ icon: "󰅖",
231
+ color: macchiato.red,
232
+ },
233
+ archive: {
234
+ description: "Move terminal work into the archive.",
235
+ icon: "",
236
+ color: macchiato.yellow,
237
+ },
238
+ split: {
239
+ description: "Convert or extend this task with another delivery phase.",
240
+ icon: "",
241
+ color: macchiato.yellow,
242
+ },
243
+ handoff: {
244
+ description: "Create distinct implementation work from this investigation.",
245
+ icon: "",
246
+ color: macchiato.mauve,
247
+ },
248
+ complete: {
249
+ description: "Record a genuine non-PR outcome as complete.",
250
+ icon: "󰄬",
251
+ color: macchiato.green,
252
+ },
253
+ sync: {
254
+ description: "Reconcile this item with repository and provider state.",
255
+ icon: "󰑓",
256
+ color: macchiato.sapphire,
257
+ },
258
+ "pr-ready": {
259
+ description: "Mark this recorded GitHub pull request ready for review.",
260
+ icon: "󰄬",
261
+ color: macchiato.green,
262
+ },
263
+ "pr-close": {
264
+ description: "Close this recorded GitHub pull request.",
265
+ icon: "",
266
+ color: macchiato.red,
267
+ },
268
+ rename: {
269
+ description: "Rename this item and update durable references.",
270
+ icon: "󰑕",
271
+ color: macchiato.yellow,
272
+ },
273
+ "dependency-add": {
274
+ description: "Require another sibling item to finish first.",
275
+ icon: "󰌹",
276
+ color: macchiato.yellow,
277
+ },
278
+ "dependency-remove": {
279
+ description: "Remove a completion dependency from this item.",
280
+ icon: "󰌺",
281
+ color: macchiato.red,
282
+ },
283
+ "move-to-epic": {
284
+ description: "Move this task into an existing epic.",
285
+ icon: "󰉒",
286
+ color: macchiato.mauve,
287
+ },
288
+ "remove-from-epic": {
289
+ description: "Move this task out of its current epic.",
290
+ icon: "󰉍",
291
+ color: macchiato.yellow,
292
+ },
293
+ }
294
+
295
+ const present = (details: Details) => {
296
+ const presentation = actionPresentation[details.id]
297
+ if (!presentation)
298
+ throw new Error(`Missing presentation for action '${details.id}'`)
299
+ return { ...details, ...presentation }
300
+ }
301
+
58
302
  // A typed argument builder is used for both discovery and execution. Prompts are
59
303
  // ordinary TypeScript; discovery does not expose or interpret form instructions.
60
304
  const action = <A>(
@@ -63,12 +307,12 @@ const action = <A>(
63
307
  read: (p: ActionPrompts) => Effect.Effect<A, Error>,
64
308
  build: (args: A) => Plan,
65
309
  ): ActAction => ({
66
- ...details,
310
+ ...present(details),
67
311
  preview: build(example),
68
312
  prepare: (p) => Effect.map(read(p), build),
69
313
  })
70
314
  const immediate = (details: Details, plan: Plan): ActAction => ({
71
- ...details,
315
+ ...present(details),
72
316
  preview: plan,
73
317
  prepare: () => Effect.succeed(plan),
74
318
  })
@@ -90,9 +334,15 @@ const taskTarget = (node: ActEntity) =>
90
334
  export const actionGroups = [
91
335
  {
92
336
  id: "create",
93
- label: "Create a task",
337
+ label: "Create work",
94
338
  icon: "󰐕",
95
- actions: ["task-create", "investigation-create"],
339
+ actions: [
340
+ "task-create",
341
+ "multi-phase-create",
342
+ "investigation-create",
343
+ "epic-create",
344
+ "task-create-in-epic",
345
+ ],
96
346
  },
97
347
  {
98
348
  id: "work",
@@ -120,9 +370,9 @@ export const actionGroups = [
120
370
  },
121
371
  {
122
372
  id: "current-work",
123
- label: "See current work",
373
+ label: "See work status",
124
374
  icon: "",
125
- actions: ["current-work"],
375
+ actions: ["current-work", "ready-work"],
126
376
  },
127
377
  {
128
378
  id: "browse",
@@ -132,9 +382,9 @@ export const actionGroups = [
132
382
  },
133
383
  {
134
384
  id: "pull-request",
135
- label: "Update pull-request status",
385
+ label: "Publish or update a pull request",
136
386
  icon: "",
137
- actions: ["sync", "pr", "pr-ready", "pr-close"],
387
+ actions: ["push", "sync", "pr", "pr-ready", "pr-close"],
138
388
  },
139
389
  {
140
390
  id: "close",
@@ -150,9 +400,44 @@ export const actionGroups = [
150
400
  },
151
401
  {
152
402
  id: "repository",
153
- label: "Add a repository",
403
+ label: "Manage repositories",
154
404
  icon: "",
155
- actions: ["repo-add", "repo-link"],
405
+ actions: [
406
+ "repo-add",
407
+ "repo-link",
408
+ "repo-setup",
409
+ "repo-materialize",
410
+ "repo-fetch",
411
+ "repo-verify",
412
+ "repo-rename",
413
+ "repo-remote",
414
+ "repo-unlink",
415
+ "repo-remove",
416
+ ],
417
+ },
418
+ {
419
+ id: "health",
420
+ label: "Check or refresh the workbase",
421
+ icon: "󰒡",
422
+ actions: [
423
+ "validate",
424
+ "doctor",
425
+ "sync-all",
426
+ "integration-status",
427
+ "integration-sync",
428
+ ],
429
+ },
430
+ {
431
+ id: "organize",
432
+ label: "Rename or change dependencies",
433
+ icon: "󰙅",
434
+ actions: [
435
+ "rename",
436
+ "move-to-epic",
437
+ "remove-from-epic",
438
+ "dependency-add",
439
+ "dependency-remove",
440
+ ],
156
441
  },
157
442
  ] as const
158
443
 
@@ -209,26 +494,38 @@ export const actActions = (
209
494
  }),
210
495
  ),
211
496
  ),
212
- ...([false, true] as const).map((investigation) =>
497
+ ...(["standard", "multi-phase", "investigation"] as const).map((kind) =>
213
498
  action(
214
499
  {
215
- id: investigation ? "investigation-create" : "task-create",
216
- label: investigation
217
- ? "Create an investigation"
218
- : "Create a standard task",
219
- blockedReason: needsRepository,
500
+ id:
501
+ kind === "standard"
502
+ ? "task-create"
503
+ : kind === "multi-phase"
504
+ ? "multi-phase-create"
505
+ : "investigation-create",
506
+ label:
507
+ kind === "standard"
508
+ ? "Create a standard task"
509
+ : kind === "multi-phase"
510
+ ? "Create a multi-phase task"
511
+ : "Create an investigation",
512
+ blockedReason: kind === "multi-phase" ? null : needsRepository,
220
513
  inputs: [
221
514
  input("description", "Outcome", { multiline: true }),
222
515
  input("id", "Task ID"),
223
- input("repo", "Repository alias"),
224
- input("base", "Base branch"),
516
+ ...(kind === "multi-phase"
517
+ ? []
518
+ : [
519
+ input("repo", "Repository alias"),
520
+ input("base", "Base branch"),
521
+ ]),
225
522
  ],
226
523
  },
227
524
  {
228
525
  id: "<id>",
229
526
  description: "<description>",
230
- repo: "<repo>",
231
- base: "<base>",
527
+ repo: kind === "multi-phase" ? undefined : "<repo>",
528
+ base: kind === "multi-phase" ? undefined : "<base>",
232
529
  },
233
530
  (p) =>
234
531
  Effect.gen(function* () {
@@ -236,8 +533,12 @@ export const actActions = (
236
533
  return {
237
534
  description,
238
535
  id: yield* p.id("New task ID", description),
239
- repo: yield* p.repository(),
240
- base: yield* p.text("Base branch", "main"),
536
+ repo:
537
+ kind === "multi-phase" ? undefined : yield* p.repository(),
538
+ base:
539
+ kind === "multi-phase"
540
+ ? undefined
541
+ : yield* p.text("Base branch", "main"),
241
542
  }
242
543
  }),
243
544
  (args) => ({
@@ -246,25 +547,81 @@ export const actActions = (
246
547
  "task",
247
548
  "create",
248
549
  args.id,
249
- "--repo",
250
- args.repo,
251
- "--base",
252
- args.base,
550
+ ...(args.repo ? ["--repo", args.repo] : []),
551
+ ...(args.base ? ["--base", args.base] : []),
253
552
  "--description",
254
553
  args.description,
255
- ...(investigation ? ["--purpose", "investigation"] : []),
554
+ ...(kind === "multi-phase" ? ["--multi-phase"] : []),
555
+ ...(kind === "investigation"
556
+ ? ["--purpose", "investigation"]
557
+ : []),
256
558
  ],
257
559
  run: task({
258
560
  ...options,
259
561
  subcommand: "create",
260
562
  args: [args.id],
261
563
  ...args,
262
- purpose: investigation ? "investigation" : undefined,
564
+ multiPhase: kind === "multi-phase",
565
+ purpose: kind === "investigation" ? "investigation" : undefined,
263
566
  }),
264
567
  next: { taskId: args.id },
265
568
  }),
266
569
  ),
267
570
  ),
571
+ action(
572
+ {
573
+ id: "epic-create",
574
+ label: "Create an epic",
575
+ blockedReason: needsRepository,
576
+ inputs: [
577
+ input("description", "Outcome", { multiline: true }),
578
+ input("id", "Epic ID"),
579
+ input("ticketUrl", "Ticket URL"),
580
+ input("repo", "Repository alias"),
581
+ input("ref", "Repository ref"),
582
+ ],
583
+ },
584
+ {
585
+ id: "<id>",
586
+ description: "<description>",
587
+ ticketUrl: "<ticketUrl>",
588
+ repo: "<repo>",
589
+ ref: "<ref>",
590
+ },
591
+ (p) =>
592
+ Effect.gen(function* () {
593
+ const description = yield* p.text("Outcome")
594
+ return {
595
+ description,
596
+ id: yield* p.id("New epic ID", description),
597
+ ticketUrl: yield* p.text("Ticket URL"),
598
+ repo: yield* p.repository(),
599
+ ref: yield* p.text("Repository ref", "main"),
600
+ }
601
+ }),
602
+ (values) => ({
603
+ command: [
604
+ "agency",
605
+ "epic",
606
+ "create",
607
+ values.id,
608
+ "--ticket-url",
609
+ values.ticketUrl,
610
+ "--repo",
611
+ `${values.repo}:${values.ref}`,
612
+ "--description",
613
+ values.description,
614
+ ],
615
+ run: epic({
616
+ ...options,
617
+ subcommand: "create",
618
+ args: [values.id],
619
+ ticketUrl: values.ticketUrl,
620
+ repos: [`${values.repo}:${values.ref}`],
621
+ description: values.description,
622
+ }),
623
+ }),
624
+ ),
268
625
  ...(["pull-request", "ref"] as const).map((sourceType) =>
269
626
  action(
270
627
  {
@@ -337,6 +694,157 @@ export const actActions = (
337
694
  run: status({ ...options, statuses: ["working"] }),
338
695
  },
339
696
  ),
697
+ immediate(
698
+ {
699
+ id: "ready-work",
700
+ label: "See ready work",
701
+ blockedReason: null,
702
+ },
703
+ {
704
+ command: ["agency", "status", "--ready"],
705
+ run: status({ ...options, ready: true }),
706
+ },
707
+ ),
708
+ immediate(
709
+ {
710
+ id: "repo-setup",
711
+ label: "Set up declared repositories",
712
+ blockedReason: null,
713
+ },
714
+ {
715
+ command: ["agency", "repo", "setup", "--apply"],
716
+ run: repo({ ...options, subcommand: "setup", args: [], apply: true }),
717
+ },
718
+ ),
719
+ ...(["materialize", "fetch", "verify"] as const).map((operation) =>
720
+ action(
721
+ {
722
+ id: `repo-${operation}`,
723
+ label:
724
+ operation === "materialize"
725
+ ? "Materialize a repository"
726
+ : operation === "fetch"
727
+ ? "Fetch a repository"
728
+ : "Verify a repository",
729
+ blockedReason: needsRepository,
730
+ inputs: [input("repo", "Repository alias")],
731
+ },
732
+ "<repo>",
733
+ (p) => p.repository(),
734
+ (alias) => ({
735
+ command: ["agency", "repo", operation, alias],
736
+ run: repo({ ...options, subcommand: operation, args: [alias] }),
737
+ }),
738
+ ),
739
+ ),
740
+ action(
741
+ {
742
+ id: "repo-rename",
743
+ label: "Rename a repository alias",
744
+ blockedReason: needsRepository,
745
+ inputs: [
746
+ input("repo", "Repository alias"),
747
+ input("id", "New repository alias"),
748
+ ],
749
+ },
750
+ { repo: "<repo>", id: "<id>" },
751
+ (p) =>
752
+ Effect.gen(function* () {
753
+ return {
754
+ repo: yield* p.repository(),
755
+ id: yield* p.id("New repository alias", "repository"),
756
+ }
757
+ }),
758
+ ({ repo: alias, id }) => ({
759
+ command: ["agency", "repo", "rename", alias, id],
760
+ run: repo({
761
+ ...options,
762
+ subcommand: "rename",
763
+ args: [alias, id],
764
+ }),
765
+ }),
766
+ ),
767
+ action(
768
+ {
769
+ id: "repo-remote",
770
+ label: "Update a repository remote",
771
+ blockedReason: needsRepository,
772
+ inputs: [
773
+ input("repo", "Repository alias"),
774
+ input("remote", "Remote URL"),
775
+ ],
776
+ },
777
+ { repo: "<repo>", remote: "<remote>" },
778
+ (p) =>
779
+ Effect.gen(function* () {
780
+ return {
781
+ repo: yield* p.repository(),
782
+ remote: yield* p.text("Portable remote URL"),
783
+ }
784
+ }),
785
+ ({ repo: alias, remote }) => ({
786
+ command: ["agency", "repo", "remote", alias, remote],
787
+ run: repo({
788
+ ...options,
789
+ subcommand: "remote",
790
+ args: [alias, remote],
791
+ }),
792
+ }),
793
+ ),
794
+ ...(["unlink", "remove"] as const).map((operation) =>
795
+ action(
796
+ {
797
+ id: `repo-${operation}`,
798
+ label:
799
+ operation === "unlink"
800
+ ? "Unlink a local repository"
801
+ : "Remove a repository alias",
802
+ blockedReason: needsRepository,
803
+ inputs: [input("repo", "Repository alias")],
804
+ },
805
+ "<repo>",
806
+ (p) => p.repository(),
807
+ (alias) => ({
808
+ command: ["agency", "repo", operation, alias],
809
+ run: repo({ ...options, subcommand: operation, args: [alias] }),
810
+ }),
811
+ ),
812
+ ),
813
+ immediate(
814
+ { id: "validate", label: "Validate workbase", blockedReason: null },
815
+ {
816
+ command: ["agency", "validate"],
817
+ run: validate(options),
818
+ },
819
+ ),
820
+ immediate(
821
+ { id: "doctor", label: "Diagnose workbase", blockedReason: null },
822
+ { command: ["agency", "doctor"], run: doctor(options) },
823
+ ),
824
+ immediate(
825
+ {
826
+ id: "sync-all",
827
+ label: "Refresh all Agency state",
828
+ blockedReason: null,
829
+ },
830
+ { command: ["agency", "sync"], run: sync(options) },
831
+ ),
832
+ ...(["status", "sync"] as const).map((operation) =>
833
+ immediate(
834
+ {
835
+ id: `integration-${operation}`,
836
+ label:
837
+ operation === "status"
838
+ ? "Check agent integration"
839
+ : "Update agent integration",
840
+ blockedReason: null,
841
+ },
842
+ {
843
+ command: ["agency", "integration", operation],
844
+ run: integration({ ...options, subcommand: operation }),
845
+ },
846
+ ),
847
+ ),
340
848
  ]
341
849
 
342
850
  const target = taskTarget(node)
@@ -393,6 +901,14 @@ export const actActions = (
393
901
  return target.phaseId ? phase(update) : task(update)
394
902
  }
395
903
  const workTarget = node.kind === "epic" ? { epicId: node.key } : target
904
+ const itemDirectory = join(
905
+ options.cwd ?? process.cwd(),
906
+ node.kind === "epic"
907
+ ? `epics/${node.key}`
908
+ : target.phaseId
909
+ ? `tasks/${target.taskId}/phases/${target.phaseId}`
910
+ : `tasks/${target.taskId}`,
911
+ )
396
912
  const canWork =
397
913
  node.readiness.ready ||
398
914
  Boolean(
@@ -424,6 +940,22 @@ export const actActions = (
424
940
  run: Effect.suspend(() => work({ ...options, ...workTarget })),
425
941
  },
426
942
  ),
943
+ immediate(
944
+ details(
945
+ "push",
946
+ "Publish branch without a pull request",
947
+ noExecution ??
948
+ (parent && "review" in parent.data
949
+ ? "Review tasks do not publish delivery branches"
950
+ : node.status !== "working"
951
+ ? "Item must be working before publication"
952
+ : null),
953
+ ),
954
+ {
955
+ command: ["agency", "--cwd", itemDirectory, "push"],
956
+ run: push({ ...options, cwd: itemDirectory }),
957
+ },
958
+ ),
427
959
  immediate(
428
960
  details(
429
961
  "pr",
@@ -450,6 +982,31 @@ export const actActions = (
450
982
  run: prCreate({ ...options, ...target }),
451
983
  },
452
984
  ),
985
+ immediate(
986
+ details(
987
+ "review-refresh",
988
+ "Refresh review source",
989
+ node.kind === "task" && "review" in node.data
990
+ ? null
991
+ : "Select a review task",
992
+ ),
993
+ {
994
+ command: [
995
+ "agency",
996
+ "review",
997
+ "refresh",
998
+ target.taskId,
999
+ "--if-revision",
1000
+ node.data.sha256,
1001
+ ],
1002
+ run: review({
1003
+ ...options,
1004
+ subcommand: "refresh",
1005
+ taskId: target.taskId,
1006
+ ifRevision: node.data.sha256,
1007
+ }),
1008
+ },
1009
+ ),
453
1010
  ...(["reopen", "drop"] as const).map((id) => {
454
1011
  const status = id === "reopen" ? "open" : "dropped"
455
1012
  return immediate(
@@ -481,6 +1038,61 @@ export const actActions = (
481
1038
  run: archive({ ...options, type: node.kind, args }),
482
1039
  },
483
1040
  ),
1041
+ action(
1042
+ {
1043
+ ...details(
1044
+ "task-create-in-epic",
1045
+ "Create a task in this epic",
1046
+ node.kind === "epic" ? needsRepository : "Select an epic",
1047
+ ),
1048
+ inputs: [
1049
+ input("description", "Outcome", { multiline: true }),
1050
+ input("id", "Task ID"),
1051
+ input("repo", "Repository alias"),
1052
+ input("base", "Base branch"),
1053
+ ],
1054
+ },
1055
+ {
1056
+ id: "<id>",
1057
+ description: "<description>",
1058
+ repo: "<repo>",
1059
+ base: "<base>",
1060
+ },
1061
+ (p) =>
1062
+ Effect.gen(function* () {
1063
+ const description = yield* p.text("Outcome")
1064
+ return {
1065
+ description,
1066
+ id: yield* p.id("New task ID", description),
1067
+ repo: yield* p.repository(node.repositories[0]),
1068
+ base: yield* p.text("Base branch", "main"),
1069
+ }
1070
+ }),
1071
+ (values) => ({
1072
+ command: [
1073
+ "agency",
1074
+ "task",
1075
+ "create",
1076
+ values.id,
1077
+ "--epic",
1078
+ node.key,
1079
+ "--repo",
1080
+ values.repo,
1081
+ "--base",
1082
+ values.base,
1083
+ "--description",
1084
+ values.description,
1085
+ ],
1086
+ run: task({
1087
+ ...options,
1088
+ subcommand: "create",
1089
+ args: [values.id],
1090
+ epic: node.key,
1091
+ ...values,
1092
+ }),
1093
+ next: { taskId: values.id },
1094
+ }),
1095
+ ),
484
1096
  action(
485
1097
  {
486
1098
  ...details(
@@ -688,14 +1300,172 @@ export const actActions = (
688
1300
  },
689
1301
  ),
690
1302
  ),
1303
+ action(
1304
+ {
1305
+ ...details("rename", "Rename item"),
1306
+ inputs: [input("id", "New item ID")],
1307
+ },
1308
+ "<id>",
1309
+ (p) =>
1310
+ p.id(
1311
+ `New ${node.kind} ID`,
1312
+ node.kind === "phase" ? target.phaseId! : node.key,
1313
+ node.kind === "phase" ? `${target.taskId}/` : "",
1314
+ ),
1315
+ (id) => {
1316
+ const command = [
1317
+ "agency",
1318
+ node.kind,
1319
+ "rename",
1320
+ ...(node.kind === "phase"
1321
+ ? [target.taskId, target.phaseId!, id]
1322
+ : [node.key, id]),
1323
+ "--if-revision",
1324
+ node.data.sha256,
1325
+ ]
1326
+ const update = {
1327
+ ...options,
1328
+ subcommand: "rename",
1329
+ args:
1330
+ node.kind === "phase"
1331
+ ? [target.taskId, target.phaseId!, id]
1332
+ : [node.key, id],
1333
+ ifRevision: node.data.sha256,
1334
+ }
1335
+ return {
1336
+ command,
1337
+ run:
1338
+ node.kind === "epic"
1339
+ ? epic(update)
1340
+ : node.kind === "phase"
1341
+ ? phase(update)
1342
+ : task(update),
1343
+ }
1344
+ },
1345
+ ),
1346
+ action(
1347
+ {
1348
+ ...details(
1349
+ "move-to-epic",
1350
+ "Move task to an epic",
1351
+ node.kind === "task" ? null : "Select a task",
1352
+ ),
1353
+ inputs: [input("epic", "Epic ID")],
1354
+ },
1355
+ "<epic>",
1356
+ (p) => p.text("Destination epic ID"),
1357
+ (epicId) => ({
1358
+ command: [
1359
+ "agency",
1360
+ "task",
1361
+ "move",
1362
+ target.taskId,
1363
+ "--epic",
1364
+ epicId,
1365
+ "--if-revision",
1366
+ node.data.sha256,
1367
+ ],
1368
+ run: task({
1369
+ ...options,
1370
+ subcommand: "move",
1371
+ args: [target.taskId],
1372
+ epic: epicId,
1373
+ ifRevision: node.data.sha256,
1374
+ }),
1375
+ }),
1376
+ ),
1377
+ immediate(
1378
+ details(
1379
+ "remove-from-epic",
1380
+ "Remove task from its epic",
1381
+ node.kind === "task" && node.data.epic
1382
+ ? null
1383
+ : "Select a task that belongs to an epic",
1384
+ ),
1385
+ {
1386
+ command: [
1387
+ "agency",
1388
+ "task",
1389
+ "move",
1390
+ target.taskId,
1391
+ "--no-epic",
1392
+ "--if-revision",
1393
+ node.data.sha256,
1394
+ ],
1395
+ run: task({
1396
+ ...options,
1397
+ subcommand: "move",
1398
+ args: [target.taskId],
1399
+ noEpic: true,
1400
+ ifRevision: node.data.sha256,
1401
+ }),
1402
+ },
1403
+ ),
1404
+ ...(["add", "remove"] as const).map((operation) =>
1405
+ action(
1406
+ {
1407
+ ...details(
1408
+ `dependency-${operation}`,
1409
+ operation === "add" ? "Add dependency" : "Remove dependency",
1410
+ node.kind === "phase"
1411
+ ? terminal
1412
+ ? "Phase is terminal"
1413
+ : null
1414
+ : node.kind === "task" && node.data.epic
1415
+ ? terminal
1416
+ ? "Task is terminal"
1417
+ : null
1418
+ : "Select a phase or a task in an epic",
1419
+ ),
1420
+ inputs: [input("dependency", "Dependency ID")],
1421
+ },
1422
+ "<dependency>",
1423
+ (p) =>
1424
+ p.text(`${operation === "add" ? "Required" : "Current"} sibling ID`),
1425
+ (dependency) => {
1426
+ const command = [
1427
+ "agency",
1428
+ node.kind === "phase" ? "phase" : "task",
1429
+ "dependency",
1430
+ operation,
1431
+ ...args,
1432
+ dependency,
1433
+ "--if-revision",
1434
+ node.data.sha256,
1435
+ ]
1436
+ const update = {
1437
+ ...options,
1438
+ subcommand: "dependency",
1439
+ args: [operation, ...args, dependency],
1440
+ ifRevision: node.data.sha256,
1441
+ }
1442
+ return {
1443
+ command,
1444
+ run: node.kind === "phase" ? phase(update) : task(update),
1445
+ }
1446
+ },
1447
+ ),
1448
+ ),
691
1449
  ]
692
1450
  }
693
1451
 
694
1452
  export const actionOutput = (action: ActAction, auto?: boolean) => {
695
- const { id, label, blockedReason, preview, inputs = [] } = action
1453
+ const {
1454
+ id,
1455
+ label,
1456
+ description,
1457
+ icon,
1458
+ color,
1459
+ blockedReason,
1460
+ preview,
1461
+ inputs = [],
1462
+ } = action
696
1463
  return {
697
1464
  id,
698
1465
  label,
1466
+ description,
1467
+ icon,
1468
+ color,
699
1469
  available: blockedReason === null,
700
1470
  blockedReason,
701
1471
  ...(blockedReason
@@ -169,29 +169,18 @@ const entitySummary = (node: ActEntity) => ({
169
169
  readiness: node.readiness,
170
170
  revision: node.data.sha256,
171
171
  })
172
- const actionIcons: Record<string, string> = {
173
- reopen: "󰑓",
174
- drop: "󰅖",
175
- complete: "󰄬",
176
- sync: "󰑓",
177
- "pr-ready": "󰄬",
178
- "pr-close": "",
179
- }
180
172
  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
173
+ actions.map(({ id, label, description, icon, color }) => {
189
174
  return {
190
175
  key: id,
191
176
  value: id,
192
- label: `${icon} ${label}`,
193
- plainLabel: label,
194
- segments: [{ text: `${icon} `, color }, { text: label }],
177
+ label: `${icon} ${label} — ${description}`,
178
+ plainLabel: `${label} — ${description}`,
179
+ segments: [
180
+ { text: `${icon} `, color },
181
+ { text: label },
182
+ { text: ` — ${description}`, color: macchiato.overlay0 },
183
+ ],
195
184
  }
196
185
  })
197
186
 
@@ -568,7 +557,7 @@ const actStep = (
568
557
  graph.nodes
569
558
  .filter((node) => node.kind === "repository")
570
559
  .map((node) => node.key),
571
- nodes.filter((node) => node.kind !== "epic").map((node) => node.key),
560
+ nodes.map((node) => node.key),
572
561
  config.chooserCommand,
573
562
  ),
574
563
  )