@markjaquith/agency 2.52.1 → 2.53.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/cli-main.ts ADDED
@@ -0,0 +1,845 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { Effect, Either, Layer } from "effect"
4
+ import { join, resolve } from "node:path"
5
+ import { parseCli } from "./src/cli-parser"
6
+ import { init, help as initHelp } from "./src/commands/init"
7
+ import { task, help as taskHelp } from "./src/commands/task"
8
+ import { pr, help as prHelp } from "./src/commands/pr"
9
+ import { work, workPrepare, help as workHelp } from "./src/commands/work"
10
+ import { worktree, help as worktreeHelp } from "./src/commands/worktree"
11
+ import { status, help as statusHelp } from "./src/commands/status"
12
+ import { doctor, help as doctorHelp } from "./src/commands/doctor"
13
+ import { validate, help as validateHelp } from "./src/commands/validate"
14
+ import { context, help as contextHelp } from "./src/commands/context"
15
+ import { graph, help as graphHelp } from "./src/commands/graph"
16
+ import { next, help as nextHelp } from "./src/commands/next"
17
+ import { sync, help as syncHelp } from "./src/commands/sync"
18
+ import { repo, help as repoHelp } from "./src/commands/repo"
19
+ import { epic, help as epicHelp } from "./src/commands/epic"
20
+ import { phase, help as phaseHelp } from "./src/commands/phase"
21
+ import { archive, help as archiveHelp } from "./src/commands/archive"
22
+ import { restore, help as restoreHelp } from "./src/commands/restore"
23
+ import { workbase, help as workbaseHelp } from "./src/commands/workbase"
24
+ import {
25
+ integration,
26
+ help as integrationHelp,
27
+ } from "./src/commands/integration"
28
+ import type { Command } from "./src/types"
29
+ import { FileSystemService } from "./src/services/FileSystemService"
30
+ import { WorkbaseService } from "./src/services/WorkbaseService"
31
+ import { RepositoryService } from "./src/services/RepositoryService"
32
+ import { EpicService } from "./src/services/EpicService"
33
+ import { TaskService } from "./src/services/TaskService"
34
+ import { PhaseService } from "./src/services/PhaseService"
35
+ import { WorktreeService } from "./src/services/WorktreeService"
36
+ import { PullRequestService } from "./src/services/PullRequestService"
37
+ import { ArchiveService } from "./src/services/ArchiveService"
38
+ import { IntegrationService } from "./src/services/IntegrationService"
39
+ import { ContextService } from "./src/services/ContextService"
40
+ import { GraphService } from "./src/services/GraphService"
41
+ import { ClaimService } from "./src/services/ClaimService"
42
+ import { SyncService } from "./src/services/SyncService"
43
+ import { ReadinessService } from "./src/services/ReadinessService"
44
+ import { GraphMutationService } from "./src/services/GraphMutationService"
45
+ import { DoctorService } from "./src/services/DoctorService"
46
+ import { ReviewService } from "./src/services/ReviewService"
47
+ import {
48
+ GitVersionControlService,
49
+ JjVersionControlService,
50
+ VersionControlService,
51
+ } from "./src/services/VersionControlService"
52
+ import { review, help as reviewHelp } from "./src/commands/review"
53
+ import { vcs, help as vcsHelp } from "./src/commands/vcs"
54
+ import { VcsMigrationService } from "./src/services/VcsMigrationService"
55
+ import {
56
+ claimCommand,
57
+ claimHelp,
58
+ releaseHelp,
59
+ finishHelp,
60
+ } from "./src/commands/claim"
61
+ import {
62
+ collectCommandResult,
63
+ errorEnvelope,
64
+ successEnvelope,
65
+ writeEnvelope,
66
+ } from "./src/protocol"
67
+
68
+ // Create CLI layer with all services
69
+ const CliLayer = Layer.mergeAll(
70
+ FileSystemService.Default,
71
+ WorkbaseService.Default,
72
+ GitVersionControlService.Default,
73
+ JjVersionControlService.Default,
74
+ VersionControlService.Default,
75
+ VcsMigrationService.Default,
76
+ RepositoryService.Default,
77
+ EpicService.Default,
78
+ TaskService.Default,
79
+ PhaseService.Default,
80
+ WorktreeService.Default,
81
+ PullRequestService.Default,
82
+ ArchiveService.Default,
83
+ IntegrationService.Default,
84
+ ContextService.Default,
85
+ GraphService.Default,
86
+ ClaimService.Default,
87
+ SyncService.Default,
88
+ ReadinessService.Default,
89
+ GraphMutationService.Default,
90
+ DoctorService.Default,
91
+ ReviewService.Default,
92
+ )
93
+
94
+ /**
95
+ * Run a command Effect with all services provided
96
+ */
97
+ async function runEffect<A, E>(effect: Effect.Effect<A, E, any>): Promise<A> {
98
+ const providedEffect = Effect.provide(effect, CliLayer) as Effect.Effect<
99
+ A,
100
+ E,
101
+ never
102
+ >
103
+
104
+ const toError = (error: unknown) => {
105
+ if (error instanceof Error) {
106
+ return error
107
+ }
108
+ if (
109
+ typeof error === "object" &&
110
+ error !== null &&
111
+ "message" in error &&
112
+ typeof error.message === "string"
113
+ ) {
114
+ return new Error(error.message)
115
+ }
116
+ return new Error(String(error))
117
+ }
118
+
119
+ const result = await Effect.runPromise(
120
+ providedEffect.pipe(
121
+ Effect.catchAllDefect((defect) => Effect.fail(toError(defect))),
122
+ Effect.either,
123
+ ),
124
+ )
125
+ if (Either.isLeft(result)) throw result.left
126
+ return result.right
127
+ }
128
+
129
+ const runCommand = <E>(effect: Effect.Effect<void, E, any>) => runEffect(effect)
130
+
131
+ const resolveInvocationCwd = (
132
+ commandName: string,
133
+ options: Record<string, any>,
134
+ ) =>
135
+ runEffect(
136
+ Effect.gen(function* () {
137
+ if (commandName === "pr") {
138
+ return resolve(options.cwd ?? process.cwd())
139
+ }
140
+ if (
141
+ options.help ||
142
+ commandName === "init" ||
143
+ commandName === "workbase"
144
+ ) {
145
+ return resolve(options.cwd ?? process.cwd())
146
+ }
147
+ const workbases = yield* WorkbaseService
148
+ if (options.workbase) {
149
+ return yield* workbases.resolveRegistered(options.workbase)
150
+ }
151
+ if (options.cwd) {
152
+ const selectedCwd = resolve(options.cwd)
153
+ yield* workbases.discover(selectedCwd)
154
+ return selectedCwd
155
+ }
156
+ return yield* workbases.discover(process.cwd()).pipe(
157
+ Effect.as(process.cwd()),
158
+ Effect.catchTag("WorkbaseNotFoundError", () =>
159
+ workbases
160
+ .getDefault()
161
+ .pipe(Effect.map((entry) => entry?.path ?? process.cwd())),
162
+ ),
163
+ )
164
+ }),
165
+ )
166
+
167
+ // Read version from package.json
168
+ const packageJson = await Bun.file(
169
+ new URL("./package.json", import.meta.url),
170
+ ).json()
171
+ const VERSION = packageJson.version
172
+
173
+ // Define commands
174
+ const commands: Record<string, Command> = {
175
+ claim: {
176
+ run: async (args: string[], options: Record<string, any>) => {
177
+ if (options.help) return console.log(claimHelp)
178
+ await runCommand(
179
+ claimCommand({
180
+ operation: "claim",
181
+ taskId: args[0],
182
+ phaseId: args[1],
183
+ claimant: options.claimant,
184
+ runner: options.runner,
185
+ sessionId: options["session-id"],
186
+ revision: options.revision,
187
+ expiresAt: options["expires-at"],
188
+ json: options.json,
189
+ silent: options.silent,
190
+ verbose: options.verbose,
191
+ cwd: options.cwd,
192
+ }),
193
+ )
194
+ },
195
+ },
196
+ release: {
197
+ run: async (args: string[], options: Record<string, any>) => {
198
+ if (options.help) return console.log(releaseHelp)
199
+ await runCommand(
200
+ claimCommand({
201
+ operation: "release",
202
+ taskId: args[0],
203
+ phaseId: args[1],
204
+ sessionId: options["session-id"],
205
+ revision: options.revision,
206
+ json: options.json,
207
+ silent: options.silent,
208
+ verbose: options.verbose,
209
+ cwd: options.cwd,
210
+ }),
211
+ )
212
+ },
213
+ },
214
+ finish: {
215
+ run: async (args: string[], options: Record<string, any>) => {
216
+ if (options.help) return console.log(finishHelp)
217
+ await runCommand(
218
+ claimCommand({
219
+ operation: "finish",
220
+ taskId: args[0],
221
+ phaseId: args[1],
222
+ sessionId: options["session-id"],
223
+ revision: options.revision,
224
+ outcome: options.outcome,
225
+ noPullRequest: options["no-pull-request"],
226
+ summary: options.summary,
227
+ evidenceUrl: options["evidence-url"],
228
+ json: options.json,
229
+ silent: options.silent,
230
+ verbose: options.verbose,
231
+ cwd: options.cwd,
232
+ }),
233
+ )
234
+ },
235
+ },
236
+ init: {
237
+ run: async (args: string[], options: Record<string, any>) => {
238
+ if (options.help) {
239
+ console.log(initHelp)
240
+ return
241
+ }
242
+ await runCommand(
243
+ init({
244
+ path: args[0],
245
+ json: options.json,
246
+ silent: options.silent,
247
+ verbose: options.verbose,
248
+ cwd: options.cwd,
249
+ }),
250
+ )
251
+ },
252
+ },
253
+ epic: {
254
+ run: async (args: string[], options: Record<string, any>) => {
255
+ if (options.help) {
256
+ console.log(epicHelp)
257
+ return
258
+ }
259
+ await runCommand(
260
+ epic({
261
+ subcommand: args[0],
262
+ args: args.slice(1),
263
+ ticketUrl: options["ticket-url"],
264
+ description: options.description,
265
+ clearDescription: options["clear-description"],
266
+ ifRevision: options["if-revision"],
267
+ repos: options.repo,
268
+ json: options.json,
269
+ statuses: options.status,
270
+ repositories: options.repository,
271
+ ready: options.ready,
272
+ blocked: options.blocked,
273
+ pr: options.pr ? true : options["no-pr"] ? false : undefined,
274
+ work: options.work,
275
+ auto: options.auto,
276
+ silent: options.silent,
277
+ verbose: options.verbose,
278
+ inputAllowed: options.inputAllowed,
279
+ cwd: options.cwd,
280
+ }),
281
+ )
282
+ },
283
+ },
284
+ pr: {
285
+ run: async (args: string[], options: Record<string, any>) => {
286
+ if (options.help) {
287
+ console.log(prHelp)
288
+ return
289
+ }
290
+ process.exitCode = await runEffect(pr(args, options.cwd))
291
+ },
292
+ },
293
+ phase: {
294
+ run: async (args: string[], options: Record<string, any>) => {
295
+ if (options.help) return console.log(phaseHelp)
296
+ await runCommand(
297
+ phase({
298
+ subcommand: args[0],
299
+ args: args.slice(1),
300
+ description: options.description,
301
+ clearDescription: options["clear-description"],
302
+ repo: options.repo?.[0],
303
+ references: options.reference,
304
+ branch: options.branch,
305
+ base: options.base,
306
+ clearReferences: options["clear-references"],
307
+ prUrl: options["pr-url"],
308
+ clearPr: options["clear-pr"],
309
+ noPullRequest: options["no-pull-request"],
310
+ summary: options.summary,
311
+ evidenceUrl: options["evidence-url"],
312
+ ifRevision: options["if-revision"],
313
+ dependsOn: options["depends-on"],
314
+ firstPhase: options["first-phase"],
315
+ json: options.json,
316
+ statuses: options.status,
317
+ repositories: options.repository,
318
+ ready: options.ready,
319
+ blocked: options.blocked,
320
+ pr: options.pr ? true : options["no-pr"] ? false : undefined,
321
+ work: options.work,
322
+ auto: options.auto,
323
+ silent: options.silent,
324
+ verbose: options.verbose,
325
+ inputAllowed: options.inputAllowed,
326
+ cwd: options.cwd,
327
+ }),
328
+ )
329
+ },
330
+ },
331
+ archive: {
332
+ run: async (args: string[], options: Record<string, any>) => {
333
+ if (options.help) {
334
+ console.log(archiveHelp)
335
+ return
336
+ }
337
+ await runCommand(
338
+ archive({
339
+ type: args[0],
340
+ args: args.slice(1),
341
+ json: options.json,
342
+ dryRun: options["dry-run"],
343
+ kinds: options.kind,
344
+ statuses: options.status,
345
+ repositories: options.repository,
346
+ silent: options.silent,
347
+ verbose: options.verbose,
348
+ cwd: options.cwd,
349
+ }),
350
+ )
351
+ },
352
+ },
353
+ restore: {
354
+ run: async (args: string[], options: Record<string, any>) => {
355
+ if (options.help) {
356
+ console.log(restoreHelp)
357
+ return
358
+ }
359
+ await runCommand(
360
+ restore({
361
+ type: args[0],
362
+ args: args.slice(1),
363
+ json: options.json,
364
+ dryRun: options["dry-run"],
365
+ silent: options.silent,
366
+ verbose: options.verbose,
367
+ cwd: options.cwd,
368
+ }),
369
+ )
370
+ },
371
+ },
372
+ workbase: {
373
+ run: async (args: string[], options: Record<string, any>) => {
374
+ if (options.help) {
375
+ console.log(workbaseHelp)
376
+ return
377
+ }
378
+ if (args[0] === "init") {
379
+ await runCommand(
380
+ init({
381
+ path: args[1],
382
+ json: options.json,
383
+ silent: options.silent,
384
+ verbose: options.verbose,
385
+ cwd: options.cwd,
386
+ }),
387
+ )
388
+ return
389
+ }
390
+ await runCommand(
391
+ workbase({
392
+ subcommand: args[0],
393
+ args: args.slice(1),
394
+ json: options.json,
395
+ name: options.name,
396
+ clear: options.clear,
397
+ silent: options.silent,
398
+ verbose: options.verbose,
399
+ cwd: options.cwd,
400
+ }),
401
+ )
402
+ },
403
+ },
404
+ integration: {
405
+ run: async (args: string[], options: Record<string, any>) => {
406
+ if (options.help) {
407
+ console.log(integrationHelp)
408
+ return
409
+ }
410
+ await runCommand(
411
+ integration({
412
+ subcommand: args[0],
413
+ json: options.json,
414
+ silent: options.silent,
415
+ verbose: options.verbose,
416
+ cwd: options.cwd,
417
+ }),
418
+ )
419
+ },
420
+ },
421
+ repo: {
422
+ run: async (args: string[], options: Record<string, any>) => {
423
+ if (options.help) {
424
+ console.log(repoHelp)
425
+ return
426
+ }
427
+ await runCommand(
428
+ repo({
429
+ subcommand: args[0],
430
+ args: args.slice(1),
431
+ silent: options.silent,
432
+ verbose: options.verbose,
433
+ json: options.json,
434
+ dryRun: options["dry-run"],
435
+ apply: options.apply,
436
+ cwd: options.cwd,
437
+ }),
438
+ )
439
+ },
440
+ },
441
+ task: {
442
+ run: async (args: string[], options: Record<string, any>) => {
443
+ if (options.help) {
444
+ console.log(taskHelp)
445
+ return
446
+ }
447
+ await runCommand(
448
+ task({
449
+ subcommand: args[0],
450
+ args: args.slice(1),
451
+ ticketUrl: options["ticket-url"],
452
+ clearTicket: options["clear-ticket"],
453
+ description: options.description,
454
+ clearDescription: options["clear-description"],
455
+ epic: options.epic,
456
+ repo: options.repo?.[0],
457
+ review: options.review,
458
+ pullRequest: options["pull-request"],
459
+ ref: options.ref,
460
+ references: options.reference,
461
+ branch: options.branch,
462
+ base: options.base,
463
+ clearReferences: options["clear-references"],
464
+ prUrl: options["pr-url"],
465
+ clearPr: options["clear-pr"],
466
+ noPullRequest: options["no-pull-request"],
467
+ summary: options.summary,
468
+ evidenceUrl: options["evidence-url"],
469
+ ifRevision: options["if-revision"],
470
+ noEpic: options["no-epic"],
471
+ multiPhase: options["multi-phase"],
472
+ json: options.json,
473
+ statuses: options.status,
474
+ repositories: options.repository,
475
+ ready: options.ready,
476
+ blocked: options.blocked,
477
+ pr: options.pr ? true : options["no-pr"] ? false : undefined,
478
+ work: options.work,
479
+ auto: options.auto,
480
+ silent: options.silent,
481
+ verbose: options.verbose,
482
+ inputAllowed: options.inputAllowed,
483
+ cwd: options.cwd,
484
+ }),
485
+ )
486
+ },
487
+ },
488
+ review: {
489
+ run: async (args: string[], options: Record<string, any>) => {
490
+ if (options.help) return console.log(reviewHelp)
491
+ await runCommand(
492
+ review({
493
+ subcommand: args[0],
494
+ taskId: args[1],
495
+ ifRevision: options["if-revision"],
496
+ json: options.json,
497
+ silent: options.silent,
498
+ verbose: options.verbose,
499
+ cwd: options.cwd,
500
+ }),
501
+ )
502
+ },
503
+ },
504
+ work: {
505
+ run: async (args: string[], options: Record<string, any>) => {
506
+ if (options.help) {
507
+ console.log(workHelp)
508
+ return
509
+ }
510
+ const preparing = args[0] === "prepare"
511
+ await runCommand(
512
+ (preparing ? workPrepare : work)({
513
+ directory: args[preparing ? 1 : 0],
514
+ epicId: options.epic,
515
+ json: options.json,
516
+ dryRun: options["dry-run"],
517
+ silent: options.silent,
518
+ verbose: options.verbose,
519
+ opencode: options.opencode,
520
+ claude: options.claude,
521
+ runner: options.runner,
522
+ auto: options.auto,
523
+ printCommand: options["print-command"],
524
+ force: options.force,
525
+ inputAllowed: options.inputAllowed,
526
+ cwd: options.cwd,
527
+ taskId: options.task,
528
+ phaseId: options.phase,
529
+ }),
530
+ )
531
+ },
532
+ },
533
+ worktree: {
534
+ run: async (args: string[], options: Record<string, any>) => {
535
+ if (options.help) {
536
+ console.log(worktreeHelp)
537
+ return
538
+ }
539
+ await runCommand(
540
+ worktree({
541
+ subcommand: args[0],
542
+ args: args.slice(1),
543
+ dryRun: options["dry-run"],
544
+ json: options.json,
545
+ silent: options.silent,
546
+ verbose: options.verbose,
547
+ cwd: options.cwd,
548
+ }),
549
+ )
550
+ },
551
+ },
552
+ vcs: {
553
+ run: async (args: string[], options: Record<string, any>) => {
554
+ if (options.help) {
555
+ console.log(vcsHelp)
556
+ return
557
+ }
558
+ await runCommand(
559
+ vcs({
560
+ subcommand: args[0],
561
+ target: args[1],
562
+ apply: options.apply,
563
+ dryRun: options["dry-run"],
564
+ json: options.json,
565
+ silent: options.silent,
566
+ verbose: options.verbose,
567
+ cwd: options.cwd,
568
+ }),
569
+ )
570
+ },
571
+ },
572
+ next: {
573
+ run: async (_args: string[], options: Record<string, any>) => {
574
+ if (options.help) return console.log(nextHelp)
575
+ await runCommand(
576
+ next({
577
+ select: options.select,
578
+ json: options.json,
579
+ silent: options.silent,
580
+ verbose: options.verbose,
581
+ cwd: options.cwd,
582
+ }),
583
+ )
584
+ },
585
+ },
586
+ status: {
587
+ run: async (_args: string[], options: Record<string, any>) => {
588
+ if (options.help) {
589
+ console.log(statusHelp)
590
+ return
591
+ }
592
+ await runCommand(
593
+ status({
594
+ silent: options.silent,
595
+ verbose: options.verbose,
596
+ json: options.json,
597
+ statuses: options.status,
598
+ repositories: options.repository,
599
+ ready: options.ready,
600
+ blocked: options.blocked,
601
+ pr: options.pr ? true : options["no-pr"] ? false : undefined,
602
+ cwd: options.cwd,
603
+ }),
604
+ )
605
+ },
606
+ },
607
+ doctor: {
608
+ run: async (_args: string[], options: Record<string, any>) => {
609
+ if (options.help) {
610
+ console.log(doctorHelp)
611
+ return
612
+ }
613
+ await runCommand(
614
+ doctor({
615
+ silent: options.silent,
616
+ verbose: options.verbose,
617
+ json: options.json,
618
+ cwd: options.cwd,
619
+ }),
620
+ )
621
+ },
622
+ },
623
+ validate: {
624
+ run: async (args: string[], options: Record<string, any>) => {
625
+ if (options.help) {
626
+ console.log(validateHelp)
627
+ return
628
+ }
629
+ await runCommand(
630
+ validate({
631
+ path: args[0],
632
+ silent: options.silent,
633
+ verbose: options.verbose,
634
+ json: options.json,
635
+ inputAllowed: options.inputAllowed,
636
+ cwd: options.cwd,
637
+ }),
638
+ )
639
+ },
640
+ },
641
+ context: {
642
+ run: async (args: string[], options: Record<string, any>) => {
643
+ if (options.help) {
644
+ console.log(contextHelp)
645
+ return
646
+ }
647
+ await runCommand(
648
+ context({
649
+ target: options.epic
650
+ ? join("epics", options.epic)
651
+ : options.phase
652
+ ? join("tasks", options.task, "phases", options.phase)
653
+ : options.task
654
+ ? join("tasks", options.task)
655
+ : args[0],
656
+ compact: options.compact,
657
+ full: options.full,
658
+ json: options.json,
659
+ silent: options.silent,
660
+ verbose: options.verbose,
661
+ cwd: options.cwd,
662
+ }),
663
+ )
664
+ },
665
+ },
666
+ graph: {
667
+ run: async (_args: string[], options: Record<string, any>) => {
668
+ if (options.help) {
669
+ console.log(graphHelp)
670
+ return
671
+ }
672
+ await runCommand(
673
+ graph({
674
+ json: options.json,
675
+ jsonl: options.jsonl,
676
+ ready: options.ready,
677
+ blocked: options.blocked,
678
+ statuses: options.status,
679
+ repositories: options.repository,
680
+ kinds: options.kind,
681
+ include: options.include,
682
+ silent: options.silent,
683
+ verbose: options.verbose,
684
+ cwd: options.cwd,
685
+ }),
686
+ )
687
+ },
688
+ },
689
+ sync: {
690
+ run: async (_args: string[], options: Record<string, any>) => {
691
+ if (options.help) {
692
+ console.log(syncHelp)
693
+ return
694
+ }
695
+ await runCommand(
696
+ sync({
697
+ apply: options.apply,
698
+ dryRun: options["dry-run"],
699
+ json: options.json,
700
+ silent: options.silent,
701
+ verbose: options.verbose,
702
+ cwd: options.cwd,
703
+ }),
704
+ )
705
+ },
706
+ },
707
+ }
708
+
709
+ function showMainHelp() {
710
+ console.log(`
711
+ agency v${VERSION}
712
+
713
+ Usage: agency <command> [options]
714
+
715
+ Commands:
716
+ init [path] Initialize an Agency workbase
717
+ workbase <subcommand> Manage registered workbases
718
+ integration <command> Inspect or sync managed integration files
719
+ epic <subcommand> Manage epics
720
+ phase <subcommand> Manage task phases
721
+ claim <task> [phase] Claim an execution unit
722
+ release <task> [phase] Release an execution unit
723
+ finish <task> [phase] Finish an execution unit
724
+ archive <type> Archive a work item
725
+ task <subcommand> Manage tasks
726
+ work [directory|task] Work on an epic, task, or phase
727
+ worktree <subcommand> Inspect and maintain managed workspaces
728
+ vcs <subcommand> Inspect or migrate the version-control backend
729
+ next List or select ready execution units
730
+ pr [args...] Run gh pr with Agency repository focus
731
+ review refresh Explicitly refresh a pinned review task
732
+ repo <subcommand> Manage workbase repositories
733
+ status Show status for the current workbase
734
+ doctor Diagnose workbase health and integrations
735
+ validate [path] Validate a workbase
736
+ context [target] Return complete target context
737
+ graph Export the complete workbase graph
738
+ sync Reconcile declarations with external state
739
+
740
+ Global Options:
741
+ -h, --help Show help for a command
742
+ -V, --version Show version number
743
+ -s, --silent Suppress output messages
744
+ -v, --verbose Show verbose output including detailed debugging info
745
+ --no-input Never open an interactive prompt or selector
746
+ --workbase <selector> Use a registered workbase ID, name, or path
747
+ --cwd <path> Resolve context from this directory
748
+
749
+ Examples:
750
+ agency init # Initialize the current directory
751
+ agency task list # List tasks
752
+ agency work tasks/refresh-cli-copy # Start working on a task
753
+
754
+ For more information about a command, run:
755
+ agency <command> --help
756
+ `)
757
+ }
758
+
759
+ const machineMode = process.argv
760
+ .slice(2)
761
+ .some((argument) => argument === "--json" || argument === "--jsonl")
762
+
763
+ try {
764
+ const args = process.argv.slice(2)
765
+ const { commandName, args: commandArgs, values } = parseCli(args)
766
+
767
+ // Handle global flags
768
+ if (values.version) {
769
+ if (machineMode) {
770
+ writeEnvelope(successEnvelope({ version: VERSION }))
771
+ } else {
772
+ console.log(`v${VERSION}`)
773
+ }
774
+ process.exit(0)
775
+ }
776
+
777
+ // Get command
778
+ // Show help if no command
779
+ if (!commandName) {
780
+ showMainHelp()
781
+ process.exit(values.help ? 0 : 1)
782
+ }
783
+
784
+ const command = commands[commandName]!
785
+ const inputAllowed =
786
+ !values.json &&
787
+ !values["no-input"] &&
788
+ Boolean(process.stdin.isTTY && process.stdout.isTTY)
789
+ const cwd = await resolveInvocationCwd(commandName, values)
790
+ if (values.json || (values.jsonl && values.help)) {
791
+ const result = await collectCommandResult(() =>
792
+ command.run(commandArgs, { ...values, cwd, inputAllowed }),
793
+ )
794
+ writeEnvelope(successEnvelope(result))
795
+ } else if (values.jsonl) {
796
+ await command.run(commandArgs, { ...values, cwd, inputAllowed: false })
797
+ } else {
798
+ await command.run(commandArgs, { ...values, cwd, inputAllowed })
799
+ }
800
+ } catch (error) {
801
+ if (machineMode) {
802
+ writeEnvelope(errorEnvelope(error))
803
+ process.exit(1)
804
+ }
805
+ if (error instanceof Error) {
806
+ let message = error.message
807
+ let details: any = error
808
+
809
+ // Handle Effect FiberFailure errors that wrap tagged errors
810
+ // When the message is generic "An error has occurred", try to extract the actual error
811
+ if (message === "An error has occurred") {
812
+ // Try to extract the actual error from Effect's Cause structure
813
+ const causeSymbol = Object.getOwnPropertySymbols(error).find((s) =>
814
+ s.toString().includes("Cause"),
815
+ )
816
+ if (causeSymbol) {
817
+ const cause = (error as any)[causeSymbol]
818
+ if (cause && cause._tag === "Fail" && cause.failure) {
819
+ const failure = cause.failure
820
+ details = failure
821
+ // Try common error message patterns
822
+ message =
823
+ failure.message ||
824
+ failure.stderr ||
825
+ (failure._tag
826
+ ? `${failure._tag}: ${JSON.stringify(failure)}`
827
+ : JSON.stringify(failure))
828
+ }
829
+ }
830
+ }
831
+ for (const [field, label] of [
832
+ ["completed", "Completed"],
833
+ ["rolledBack", "Rolled back"],
834
+ ["manualRecovery", "Manual recovery"],
835
+ ] as const) {
836
+ if (Array.isArray(details[field]) && details[field].length > 0)
837
+ message += `\n${label}: ${details[field].join("; ")}`
838
+ }
839
+
840
+ console.error(`ⓘ ${message}`)
841
+ } else {
842
+ console.error("An unexpected error occurred:", error)
843
+ }
844
+ process.exit(1)
845
+ }