@markjaquith/agency 2.16.0 → 2.18.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/src/cli-parser.ts CHANGED
@@ -25,6 +25,14 @@ const commonOptions = {
25
25
  silent: { type: "boolean", short: "s" },
26
26
  verbose: { type: "boolean", short: "v" },
27
27
  "no-input": { type: "boolean" },
28
+ workbase: { type: "string" },
29
+ cwd: { type: "string" },
30
+ } satisfies OptionConfig
31
+
32
+ const entitySelectorOptions = {
33
+ epic: { type: "string" },
34
+ task: { type: "string" },
35
+ phase: { type: "string" },
28
36
  } satisfies OptionConfig
29
37
 
30
38
  const outputOptions = {
@@ -86,14 +94,18 @@ const commands = {
86
94
  },
87
95
  },
88
96
  workbase: {
89
- usage: "agency workbase <add|list>",
90
- options: outputOptions,
97
+ usage: "agency workbase <add|list|remove|prune|default>",
98
+ options: {
99
+ ...outputOptions,
100
+ name: { type: "string" },
101
+ clear: { type: "boolean" },
102
+ },
91
103
  subcommands: {
92
104
  add: {
93
105
  usage: "agency workbase add <path> [--json]",
94
106
  minArgs: 1,
95
107
  maxArgs: 1,
96
- options: ["json"],
108
+ options: ["json", "name"],
97
109
  },
98
110
  list: {
99
111
  usage: "agency workbase list [--json]",
@@ -101,6 +113,25 @@ const commands = {
101
113
  maxArgs: 0,
102
114
  options: ["json"],
103
115
  },
116
+ remove: {
117
+ usage: "agency workbase remove <selector> [--json]",
118
+ minArgs: 1,
119
+ maxArgs: 1,
120
+ options: ["json"],
121
+ },
122
+ prune: {
123
+ usage: "agency workbase prune [--json]",
124
+ minArgs: 0,
125
+ maxArgs: 0,
126
+ options: ["json"],
127
+ },
128
+ default: {
129
+ usage: "agency workbase default [selector | --clear] [--json]",
130
+ minArgs: 0,
131
+ maxArgs: 1,
132
+ options: ["json", "clear"],
133
+ conflicts: [["clear", "$positional"]],
134
+ },
104
135
  },
105
136
  },
106
137
  integration: {
@@ -147,7 +178,7 @@ const commands = {
147
178
  },
148
179
  epic: {
149
180
  usage: "agency epic <create|list|show>",
150
- options: createOptions,
181
+ options: { ...createOptions, epic: { type: "string" } },
151
182
  subcommands: {
152
183
  create: {
153
184
  usage:
@@ -168,13 +199,13 @@ const commands = {
168
199
  usage: "agency epic show <id> [--json]",
169
200
  minArgs: 1,
170
201
  maxArgs: 1,
171
- options: ["json"],
202
+ options: ["json", "epic"],
172
203
  },
173
204
  },
174
205
  },
175
206
  task: {
176
207
  usage: "agency task <new|create|list|show|status>",
177
- options: taskCreateOptions,
208
+ options: { ...taskCreateOptions, task: { type: "string" } },
178
209
  subcommands: {
179
210
  new: {
180
211
  usage: "agency task new [id] [options]",
@@ -221,19 +252,23 @@ const commands = {
221
252
  usage: "agency task show <id> [--json]",
222
253
  minArgs: 1,
223
254
  maxArgs: 1,
224
- options: ["json"],
255
+ options: ["json", "task"],
225
256
  },
226
257
  status: {
227
258
  usage: "agency task status <id> <status> [--json]",
228
259
  minArgs: 2,
229
260
  maxArgs: 2,
230
- options: ["json"],
261
+ options: ["json", "task"],
231
262
  },
232
263
  },
233
264
  },
234
265
  phase: {
235
266
  usage: "agency phase <create|list|show|status>",
236
- options: phaseCreateOptions,
267
+ options: {
268
+ ...phaseCreateOptions,
269
+ task: { type: "string" },
270
+ phase: { type: "string" },
271
+ },
237
272
  subcommands: {
238
273
  create: {
239
274
  usage:
@@ -249,6 +284,8 @@ const commands = {
249
284
  "depends-on",
250
285
  "first-phase",
251
286
  "json",
287
+ "task",
288
+ "phase",
252
289
  ],
253
290
  required: ["repo", "branch", "base"],
254
291
  repeatable: ["reference", "depends-on"],
@@ -257,25 +294,29 @@ const commands = {
257
294
  usage: "agency phase list <task-id> [--json]",
258
295
  minArgs: 1,
259
296
  maxArgs: 1,
260
- options: ["json"],
297
+ options: ["json", "task"],
261
298
  },
262
299
  show: {
263
300
  usage: "agency phase show <task-id> <phase-id> [--json]",
264
301
  minArgs: 2,
265
302
  maxArgs: 2,
266
- options: ["json"],
303
+ options: ["json", "task", "phase"],
267
304
  },
268
305
  status: {
269
306
  usage: "agency phase status <task-id> <phase-id> <status> [--json]",
270
307
  minArgs: 3,
271
308
  maxArgs: 3,
272
- options: ["json"],
309
+ options: ["json", "task", "phase"],
273
310
  },
274
311
  },
275
312
  },
276
313
  claim: {
277
314
  usage: "agency claim <task-id> [phase-id] [options]",
278
- options: claimOptions,
315
+ options: {
316
+ ...claimOptions,
317
+ task: { type: "string" },
318
+ phase: { type: "string" },
319
+ },
279
320
  command: {
280
321
  usage:
281
322
  "agency claim <task-id> [phase-id] --claimant <id> --runner <id> --session-id <id> --revision <sha256> [--expires-at <timestamp>] [--json]",
@@ -288,19 +329,25 @@ const commands = {
288
329
  "revision",
289
330
  "expires-at",
290
331
  "json",
332
+ "task",
333
+ "phase",
291
334
  ],
292
335
  required: ["claimant", "runner", "session-id", "revision"],
293
336
  },
294
337
  },
295
338
  release: {
296
339
  usage: "agency release <task-id> [phase-id] [options]",
297
- options: ownedClaimOptions,
340
+ options: {
341
+ ...ownedClaimOptions,
342
+ task: { type: "string" },
343
+ phase: { type: "string" },
344
+ },
298
345
  command: {
299
346
  usage:
300
347
  "agency release <task-id> [phase-id] --session-id <id> --revision <sha256> [--json]",
301
348
  minArgs: 1,
302
349
  maxArgs: 2,
303
- options: ["session-id", "revision", "json"],
350
+ options: ["session-id", "revision", "json", "task", "phase"],
304
351
  required: ["session-id", "revision"],
305
352
  },
306
353
  },
@@ -309,13 +356,15 @@ const commands = {
309
356
  options: {
310
357
  ...ownedClaimOptions,
311
358
  outcome: { type: "string" },
359
+ task: { type: "string" },
360
+ phase: { type: "string" },
312
361
  },
313
362
  command: {
314
363
  usage:
315
364
  "agency finish <task-id> [phase-id] --session-id <id> --revision <sha256> --outcome <done|dropped> [--json]",
316
365
  minArgs: 1,
317
366
  maxArgs: 2,
318
- options: ["session-id", "revision", "outcome", "json"],
367
+ options: ["session-id", "revision", "outcome", "json", "task", "phase"],
319
368
  required: ["session-id", "revision", "outcome"],
320
369
  },
321
370
  },
@@ -336,25 +385,25 @@ const commands = {
336
385
  },
337
386
  archive: {
338
387
  usage: "agency archive <epic|task|phase>",
339
- options: outputOptions,
388
+ options: { ...outputOptions, ...entitySelectorOptions },
340
389
  subcommands: {
341
390
  epic: {
342
391
  usage: "agency archive epic <epic-id> [--json]",
343
392
  minArgs: 1,
344
393
  maxArgs: 1,
345
- options: ["json"],
394
+ options: ["json", "epic"],
346
395
  },
347
396
  task: {
348
397
  usage: "agency archive task <task-id> [--json]",
349
398
  minArgs: 1,
350
399
  maxArgs: 1,
351
- options: ["json"],
400
+ options: ["json", "task"],
352
401
  },
353
402
  phase: {
354
403
  usage: "agency archive phase <task-id> <phase-id> [--json]",
355
404
  minArgs: 2,
356
405
  maxArgs: 2,
357
- options: ["json"],
406
+ options: ["json", "task", "phase"],
358
407
  },
359
408
  },
360
409
  },
@@ -363,18 +412,28 @@ const commands = {
363
412
  "agency work [<directory-or-task-id> | --epic <epic-id>] | agency work prepare [target] [--dry-run] [--json]",
364
413
  options: {
365
414
  ...commonOptions,
415
+ ...entitySelectorOptions,
366
416
  json: { type: "boolean" },
367
417
  "dry-run": { type: "boolean" },
368
- epic: { type: "string" },
369
418
  opencode: { type: "boolean" },
370
419
  claude: { type: "boolean" },
420
+ force: { type: "boolean" },
371
421
  },
372
422
  command: {
373
423
  usage:
374
424
  "agency work [<directory-or-task-id> | --epic <epic-id>] | agency work prepare [target] [--dry-run] [--json]",
375
425
  minArgs: 0,
376
426
  maxArgs: 2,
377
- options: ["json", "dry-run", "epic", "opencode", "claude"],
427
+ options: [
428
+ "json",
429
+ "dry-run",
430
+ "epic",
431
+ "task",
432
+ "phase",
433
+ "opencode",
434
+ "claude",
435
+ "force",
436
+ ],
378
437
  conflicts: [
379
438
  ["opencode", "claude"],
380
439
  ["epic", "$positional"],
@@ -386,16 +445,33 @@ const commands = {
386
445
  options: {
387
446
  ...outputOptions,
388
447
  draft: { type: "boolean" },
448
+ force: { type: "boolean" },
449
+ task: { type: "string" },
450
+ phase: { type: "string" },
389
451
  },
390
452
  subcommands: {
391
453
  create: {
392
- usage: "agency pr create <task-id> [phase-id] [--draft] [--json]",
454
+ usage:
455
+ "agency pr create <task-id> [phase-id] [--draft] [--force] [--json]",
393
456
  minArgs: 1,
394
457
  maxArgs: 2,
395
- options: ["draft", "json"],
458
+ options: ["draft", "force", "json", "task", "phase"],
396
459
  },
397
460
  },
398
461
  },
462
+ next: {
463
+ usage: "agency next [--select] [--json]",
464
+ options: {
465
+ ...outputOptions,
466
+ select: { type: "boolean" },
467
+ },
468
+ command: {
469
+ usage: "agency next [--select] [--json]",
470
+ minArgs: 0,
471
+ maxArgs: 0,
472
+ options: ["select", "json"],
473
+ },
474
+ },
399
475
  status: {
400
476
  usage: "agency status [--json]",
401
477
  options: outputOptions,
@@ -422,13 +498,14 @@ const commands = {
422
498
  usage: "agency context [target] [--json] [--compact]",
423
499
  options: {
424
500
  ...outputOptions,
501
+ ...entitySelectorOptions,
425
502
  compact: { type: "boolean" },
426
503
  },
427
504
  command: {
428
505
  usage: "agency context [target] [--json] [--compact]",
429
506
  minArgs: 0,
430
507
  maxArgs: 1,
431
- options: ["json", "compact"],
508
+ options: ["json", "compact", "epic", "task", "phase"],
432
509
  },
433
510
  },
434
511
  graph: {
@@ -479,6 +556,7 @@ const preCommandOptions = new Set([
479
556
  "-v",
480
557
  "--no-input",
481
558
  ])
559
+ const preCommandValueOptions = new Set(["--workbase", "--cwd"])
482
560
 
483
561
  export interface ParsedCli {
484
562
  readonly commandName?: keyof typeof commands
@@ -507,8 +585,22 @@ const usageError = (message: string, usage: string) =>
507
585
  const optionLabel = (name: string) => `--${name}`
508
586
 
509
587
  function findCommandIndex(args: readonly string[]) {
510
- for (const [index, argument] of args.entries()) {
588
+ for (let index = 0; index < args.length; index++) {
589
+ const argument = args[index]!
511
590
  if (!argument.startsWith("-")) return index
591
+ if (argument.startsWith("--workbase=") || argument.startsWith("--cwd=")) {
592
+ continue
593
+ }
594
+ if (preCommandValueOptions.has(argument)) {
595
+ if (args[index + 1] === undefined) {
596
+ throw usageError(
597
+ `Option '${argument}' expects a value.`,
598
+ "agency <command> [options]",
599
+ )
600
+ }
601
+ index++
602
+ continue
603
+ }
512
604
  if (!preCommandOptions.has(argument) && !/^-[hVsv]+$/.test(argument)) {
513
605
  throw usageError(
514
606
  `Unknown option '${argument}'.`,
@@ -519,6 +611,87 @@ function findCommandIndex(args: readonly string[]) {
519
611
  return -1
520
612
  }
521
613
 
614
+ const targetSlots = (
615
+ commandName: string,
616
+ subcommand: string | undefined,
617
+ ): readonly ("epic" | "task" | "phase")[] => {
618
+ if (commandName === "epic" && subcommand === "show") return ["epic"]
619
+ if (commandName === "task" && ["show", "status"].includes(subcommand ?? ""))
620
+ return ["task"]
621
+ if (commandName === "phase")
622
+ return subcommand === "list" ? ["task"] : ["task", "phase"]
623
+ if (["claim", "release", "finish"].includes(commandName))
624
+ return ["task", "phase"]
625
+ if (commandName === "archive")
626
+ return subcommand === "epic"
627
+ ? ["epic"]
628
+ : subcommand === "task"
629
+ ? ["task"]
630
+ : ["task", "phase"]
631
+ if (commandName === "pr" && subcommand === "create") return ["task", "phase"]
632
+ return []
633
+ }
634
+
635
+ function applyEntitySelectors(
636
+ commandName: string,
637
+ subcommand: string | undefined,
638
+ positionals: readonly string[],
639
+ values: ParsedCli["values"],
640
+ spec: LeafCommand,
641
+ ) {
642
+ const supplied = ["epic", "task", "phase"].filter(
643
+ (name) => values[name] !== undefined,
644
+ )
645
+ if (supplied.length === 0) return [...positionals]
646
+ if (values.phase !== undefined && values.task === undefined) {
647
+ throw usageError("Option '--phase' requires '--task'.", spec.usage)
648
+ }
649
+ if (values.epic !== undefined && (values.task || values.phase)) {
650
+ throw usageError(
651
+ "Option '--epic' cannot be combined with '--task' or '--phase'.",
652
+ spec.usage,
653
+ )
654
+ }
655
+ if (commandName === "work" || commandName === "context") {
656
+ if (
657
+ positionals.length >
658
+ (commandName === "work" && positionals[0] === "prepare" ? 1 : 0)
659
+ ) {
660
+ throw usageError(
661
+ "Entity selector options cannot be combined with a positional target.",
662
+ spec.usage,
663
+ )
664
+ }
665
+ return [...positionals]
666
+ }
667
+
668
+ const slots = targetSlots(commandName, subcommand)
669
+ const selectedSlots = slots.filter((slot) => values[slot] !== undefined)
670
+ if (selectedSlots.length === 0) return [...positionals]
671
+ const trailingCount = spec.maxArgs - slots.length
672
+ if (positionals.length > trailingCount) {
673
+ throw usageError(
674
+ "Entity selector options cannot be combined with positional target IDs.",
675
+ spec.usage,
676
+ )
677
+ }
678
+ const requiredSlots = slots.slice(0, spec.minArgs - trailingCount)
679
+ for (const slot of requiredSlots) {
680
+ if (values[slot] === undefined) {
681
+ throw usageError(
682
+ `Option '--${slot}' is required with explicit selectors.`,
683
+ spec.usage,
684
+ )
685
+ }
686
+ }
687
+ return [
688
+ ...slots.flatMap((slot) =>
689
+ typeof values[slot] === "string" ? [values[slot] as string] : [],
690
+ ),
691
+ ...positionals,
692
+ ]
693
+ }
694
+
522
695
  function assertNoDuplicateOptions(
523
696
  tokens: readonly { readonly kind: string; readonly name?: string }[],
524
697
  repeatable: ReadonlySet<string>,
@@ -658,7 +831,7 @@ export function parseCli(args: readonly string[]): ParsedCli {
658
831
  throw usageError(message, definition.usage)
659
832
  }
660
833
 
661
- const commandPositionals = definition.subcommands
834
+ let commandPositionals = definition.subcommands
662
835
  ? parsed.positionals.slice(1)
663
836
  : parsed.positionals
664
837
  const allowed = new Set([...commonOptionNames, ...(spec.options ?? [])])
@@ -678,6 +851,20 @@ export function parseCli(args: readonly string[]): ParsedCli {
678
851
  spec.usage,
679
852
  )
680
853
  }
854
+ for (const selector of ["workbase", "cwd", "epic", "task", "phase"]) {
855
+ if (parsed.values[selector] === "") {
856
+ throw usageError(
857
+ `Option '--${selector}' requires a non-empty value.`,
858
+ spec.usage,
859
+ )
860
+ }
861
+ }
862
+ if (parsed.values.workbase && parsed.values.cwd) {
863
+ throw usageError(
864
+ "Options '--workbase' and '--cwd' cannot be combined.",
865
+ spec.usage,
866
+ )
867
+ }
681
868
  if (parsed.values.version) {
682
869
  return {
683
870
  commandName: commandName as keyof typeof commands,
@@ -693,6 +880,14 @@ export function parseCli(args: readonly string[]): ParsedCli {
693
880
  }
694
881
  }
695
882
 
883
+ commandPositionals = applyEntitySelectors(
884
+ commandName,
885
+ subcommand,
886
+ commandPositionals,
887
+ parsed.values,
888
+ spec,
889
+ )
890
+
696
891
  if (
697
892
  commandPositionals.length < spec.minArgs ||
698
893
  commandPositionals.length > spec.maxArgs
@@ -744,7 +939,10 @@ export function parseCli(args: readonly string[]): ParsedCli {
744
939
  if (
745
940
  (!preparing && commandPositionals.length > 1) ||
746
941
  (preparing &&
747
- (parsed.values.epic || parsed.values.opencode || parsed.values.claude))
942
+ (parsed.values.epic ||
943
+ parsed.values.opencode ||
944
+ parsed.values.claude ||
945
+ parsed.values.force))
748
946
  ) {
749
947
  throw usageError(
750
948
  preparing
@@ -763,7 +961,9 @@ export function parseCli(args: readonly string[]): ParsedCli {
763
961
 
764
962
  return {
765
963
  commandName: commandName as keyof typeof commands,
766
- args: parsed.positionals,
964
+ args: definition.subcommands
965
+ ? [subcommand!, ...commandPositionals]
966
+ : commandPositionals,
767
967
  values: parsed.values,
768
968
  }
769
969
  }
package/src/cli.test.ts CHANGED
@@ -217,6 +217,15 @@ describe("CLI", () => {
217
217
  await expect(access(root)).rejects.toThrow()
218
218
  })
219
219
 
220
+ test("resolves relative init paths from explicit cwd", async () => {
221
+ const parent = await createTempDir()
222
+ tempDirs.push(parent)
223
+ const result = parseJson(
224
+ await runCli(["init", "child", "--cwd", parent, "--json"], projectRoot),
225
+ )
226
+ expect(result.root).toBe(join(parent, "child"))
227
+ })
228
+
220
229
  test("refuses guided input without a TTY or with --no-input", async () => {
221
230
  for (const args of [
222
231
  ["task", "new"],
@@ -245,6 +254,7 @@ describe("CLI", () => {
245
254
  ["validate", "Usage: agency validate"],
246
255
  ["context", "Usage: agency context"],
247
256
  ["graph", "Usage: agency graph"],
257
+ ["next", "Usage: agency next"],
248
258
  ] as const) {
249
259
  const result = await runCli([command, "--help"])
250
260
  expect(result.exitCode).toBe(0)
@@ -264,6 +274,55 @@ describe("CLI", () => {
264
274
 
265
275
  const after = await runCli(["status", "--silent"], root)
266
276
  expect(after).toEqual({ exitCode: 0, stdout: "", stderr: "" })
277
+ }, 10_000)
278
+
279
+ test("lists ready work and exposes excluded blockers through one result", async () => {
280
+ const root = await createTempDir()
281
+ tempDirs.push(root)
282
+ await Bun.write(join(root, "agency.json"), '{"version":2}\n')
283
+ await mkdir(join(root, "repos/agency"), { recursive: true })
284
+ for (const id of ["ready", "finished"]) {
285
+ parseJson(
286
+ await runCli(
287
+ ["task", "create", id, "--repo", "agency", "--json"],
288
+ root,
289
+ ),
290
+ )
291
+ }
292
+ parseJson(
293
+ await runCli(["task", "status", "finished", "done", "--json"], root),
294
+ )
295
+
296
+ const human = await runCli(["next"], root)
297
+ expect(human).toMatchObject({ exitCode: 0, stderr: "" })
298
+ expect(human.stdout).toContain("1. task/ready")
299
+ expect(human.stdout).not.toContain("task/finished")
300
+
301
+ const result = parseJson(await runCli(["next", "--select", "--json"], root))
302
+ expect(result.selected).toMatchObject({ key: "task/ready", rank: 1 })
303
+ expect(result.ready.map((item: any) => item.key)).toEqual(["task/ready"])
304
+ expect(result.excluded).toMatchObject([
305
+ {
306
+ key: "task/finished",
307
+ status: "done",
308
+ terminal: true,
309
+ blockers: [{ kind: "status", reason: "Task status is done" }],
310
+ },
311
+ ])
312
+
313
+ const blockedPr = await runCli(["pr", "create", "finished", "--json"], root)
314
+ expect(blockedPr.exitCode).toBe(1)
315
+ expect(blockedPr.stderr).toBe("")
316
+ expect(JSON.parse(blockedPr.stdout)).toMatchObject({
317
+ ok: false,
318
+ error: {
319
+ code: "EXECUTION_BLOCKED",
320
+ fields: {
321
+ status: "done",
322
+ blockers: [{ kind: "status", reason: "Task status is done" }],
323
+ },
324
+ },
325
+ })
267
326
  })
268
327
 
269
328
  test("reports and synchronizes managed integration files", async () => {
@@ -299,12 +358,93 @@ describe("CLI", () => {
299
358
  ).toEqual({
300
359
  root,
301
360
  })
302
- expect(
303
- parseJson(await runCli(["workbase", "add", root, "--json"], parent, env)),
304
- ).toEqual({ path: await realpath(root) })
361
+ const registration = parseJson(
362
+ await runCli(
363
+ [
364
+ "workbase",
365
+ "add",
366
+ "workbase",
367
+ "--cwd",
368
+ parent,
369
+ "--name",
370
+ "primary",
371
+ "--json",
372
+ ],
373
+ projectRoot,
374
+ env,
375
+ ),
376
+ )
377
+ expect(registration).toMatchObject({
378
+ name: "primary",
379
+ path: await realpath(root),
380
+ })
305
381
  expect(
306
382
  parseJson(await runCli(["workbase", "list", "--json"], parent, env)),
307
- ).toEqual([await realpath(root)])
383
+ ).toEqual({ workbases: [registration] })
384
+
385
+ await mkdir(join(root, "repos", "agency"), { recursive: true })
386
+ parseJson(
387
+ await runCli(
388
+ [
389
+ "task",
390
+ "create",
391
+ "explicit",
392
+ "--repo",
393
+ "agency",
394
+ "--workbase",
395
+ "primary",
396
+ "--no-input",
397
+ "--json",
398
+ ],
399
+ parent,
400
+ env,
401
+ ),
402
+ )
403
+ const shown = parseJson(
404
+ await runCli(
405
+ [
406
+ "task",
407
+ "show",
408
+ "--task",
409
+ "explicit",
410
+ "--workbase",
411
+ registration.id,
412
+ "--no-input",
413
+ "--json",
414
+ ],
415
+ parent,
416
+ env,
417
+ ),
418
+ )
419
+ expect(shown.id).toBe("explicit")
420
+ const inferred = parseJson(
421
+ await runCli(
422
+ ["context", "--cwd", join(root, "tasks", "explicit"), "--json"],
423
+ projectRoot,
424
+ env,
425
+ ),
426
+ )
427
+ expect(inferred.target).toMatchObject({
428
+ kind: "task",
429
+ taskId: "explicit",
430
+ })
431
+
432
+ parseJson(
433
+ await runCli(["workbase", "default", "primary", "--json"], parent, env),
434
+ )
435
+ expect(
436
+ parseJson(await runCli(["task", "list", "--json"], parent, env)),
437
+ ).toHaveLength(1)
438
+ const explicitOutside = await runCli(
439
+ ["task", "list", "--cwd", parent, "--json"],
440
+ projectRoot,
441
+ env,
442
+ )
443
+ expect(explicitOutside.exitCode).toBe(1)
444
+ expect(explicitOutside.stderr).toBe("")
445
+ expect(JSON.parse(explicitOutside.stdout).error.message).toContain(
446
+ "No Agency workbase found from",
447
+ )
308
448
  })
309
449
 
310
450
  test("exports equivalent JSON and JSONL graph contracts", async () => {
@@ -26,4 +26,7 @@ defaults to the current directory and may be an entity path or task ID.
26
26
  Options:
27
27
  --json Output a versioned machine result
28
28
  --compact Omit prose bodies and low-level Git details
29
+ --epic <id> Select an epic
30
+ --task <id> Select a task
31
+ --phase <id> Select a phase together with --task
29
32
  `
@@ -1,4 +1,5 @@
1
1
  import { Effect } from "effect"
2
+ import { resolve } from "node:path"
2
3
  import type { BaseCommandOptions } from "../utils/command"
3
4
  import { WorkbaseService } from "../services/WorkbaseService"
4
5
  import { createLoggers } from "../utils/effect"
@@ -11,8 +12,9 @@ export const init = (options: InitOptions = {}) =>
11
12
  Effect.gen(function* () {
12
13
  const workbase = yield* WorkbaseService
13
14
  const { log } = createLoggers(options)
15
+ const cwd = options.cwd ?? process.cwd()
14
16
  const root = yield* workbase.initialize(
15
- options.path ?? options.cwd ?? process.cwd(),
17
+ options.path ? resolve(cwd, options.path) : cwd,
16
18
  )
17
19
  log(
18
20
  options.json