@markjaquith/agency 2.14.0 → 2.16.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
@@ -11,7 +11,6 @@ or write.
11
11
  - Git
12
12
  - [GitHub CLI](https://cli.github.com/) for `agency pr create`
13
13
  - OpenCode or Claude Code for `agency work`
14
- - [fzf](https://github.com/junegunn/fzf) for interactive work target selection
15
14
 
16
15
  ## Installation
17
16
 
@@ -146,6 +145,29 @@ The configured command applies only to the writable checkout. Supplemental
146
145
  read-only repositories remain detached Git worktrees at their declared refs so
147
146
  they do not acquire writable branches.
148
147
 
148
+ ### Custom Chooser Command
149
+
150
+ Interactive selection uses a native numbered chooser by default. To use an
151
+ external chooser, configure an argv command in `agency.json`:
152
+
153
+ ```json
154
+ {
155
+ "version": 2,
156
+ "chooserCommand": ["fzf", "--ansi", "--delimiter=\\t", "--with-nth=2.."]
157
+ }
158
+ ```
159
+
160
+ Agency writes one `key<TAB>label` record per choice to the command's stdin. The
161
+ command must write the selected opaque key or selected record to stdout; commands
162
+ such as `["gum", "filter"]` therefore work without wrappers. Exit codes 1 and
163
+ 130, empty stdout, native `q`, and an empty native response cancel selection.
164
+ Other nonzero exits, unknown keys, and invalid native numbers are errors.
165
+
166
+ Selectors are opened only when stdin and stderr are terminals and neither
167
+ `--no-input` nor JSON output is active. Labels use color only when stderr is a
168
+ terminal, `TERM` is not `dumb`, and `NO_COLOR` is unset; otherwise selectors use
169
+ plain labels without ANSI styling or icon-font dependencies.
170
+
149
171
  ## Frontmatter
150
172
 
151
173
  ### Epic
@@ -282,6 +304,25 @@ inspection are opt-in include layers.
282
304
  `end` record with counts. Combining the metadata with the streamed node and edge
283
305
  records reconstructs the same result as `--json`.
284
306
 
307
+ ### Reconciliation
308
+
309
+ `agency sync` compares every execution declaration with local branch and worktree
310
+ registration, writable and reference checkout dirtiness, resolved reference
311
+ commits, claim expiry, and GitHub pull request and merge state. It reports
312
+ structured `changes`, `warnings`, `unresolved`, and per-execution evidence. The
313
+ default and `--dry-run` modes are observational.
314
+
315
+ `agency sync --apply` performs only these safe transitions:
316
+
317
+ - materialize missing checkouts when no registration, branch, or path conflicts;
318
+ - release an active claim only after its declared expiry has passed;
319
+ - record a single PR whose head and base match the declaration; and
320
+ - mark work done after its authoritative PR is merged and no active claim remains.
321
+
322
+ Apply never modifies dirty checkouts, moves worktrees, switches branches, resets
323
+ reference commits, chooses among multiple PRs, or bypasses active claims. Those
324
+ conditions remain visible in `warnings` or `unresolved` with a suggested action.
325
+
285
326
  ### Workbase and Repositories
286
327
 
287
328
  ```text
@@ -302,7 +343,7 @@ repository. Alias names are then used by all documents and commands.
302
343
 
303
344
  Commands that print Agency-owned results accept `--json`, including initialization,
304
345
  integration inspection/sync, repository mutations, entity creation/list/show,
305
- status, validation, graph export, and PR creation.
346
+ status, validation, graph export, reconciliation, and PR creation.
306
347
 
307
348
  ### Epics
308
349
 
package/cli.ts CHANGED
@@ -10,6 +10,7 @@ import { status, help as statusHelp } from "./src/commands/status"
10
10
  import { validate, help as validateHelp } from "./src/commands/validate"
11
11
  import { context, help as contextHelp } from "./src/commands/context"
12
12
  import { graph, help as graphHelp } from "./src/commands/graph"
13
+ import { sync, help as syncHelp } from "./src/commands/sync"
13
14
  import { repo, help as repoHelp } from "./src/commands/repo"
14
15
  import { epic, help as epicHelp } from "./src/commands/epic"
15
16
  import { phase, help as phaseHelp } from "./src/commands/phase"
@@ -33,6 +34,7 @@ import { IntegrationService } from "./src/services/IntegrationService"
33
34
  import { ContextService } from "./src/services/ContextService"
34
35
  import { GraphService } from "./src/services/GraphService"
35
36
  import { ClaimService } from "./src/services/ClaimService"
37
+ import { SyncService } from "./src/services/SyncService"
36
38
  import {
37
39
  claimCommand,
38
40
  claimHelp,
@@ -61,6 +63,7 @@ const CliLayer = Layer.mergeAll(
61
63
  ContextService.Default,
62
64
  GraphService.Default,
63
65
  ClaimService.Default,
66
+ SyncService.Default,
64
67
  )
65
68
 
66
69
  /**
@@ -424,6 +427,23 @@ const commands: Record<string, Command> = {
424
427
  )
425
428
  },
426
429
  },
430
+ sync: {
431
+ run: async (_args: string[], options: Record<string, any>) => {
432
+ if (options.help) {
433
+ console.log(syncHelp)
434
+ return
435
+ }
436
+ await runCommand(
437
+ sync({
438
+ apply: options.apply,
439
+ dryRun: options["dry-run"],
440
+ json: options.json,
441
+ silent: options.silent,
442
+ verbose: options.verbose,
443
+ }),
444
+ )
445
+ },
446
+ },
427
447
  }
428
448
 
429
449
  function showMainHelp() {
@@ -450,6 +470,7 @@ Commands:
450
470
  validate [path] Validate a workbase
451
471
  context [target] Return complete target context
452
472
  graph Export the complete workbase graph
473
+ sync Reconcile declarations with external state
453
474
 
454
475
  Global Options:
455
476
  -h, --help Show help for a command
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "2.14.0",
3
+ "version": "2.16.0",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -141,6 +141,7 @@ describe("strict CLI parsing", () => {
141
141
  [["validate", "one", "two"], "agency validate"],
142
142
  [["context", "one", "two"], "agency context"],
143
143
  [["graph", "extra"], "agency graph"],
144
+ [["sync", "extra"], "agency sync"],
144
145
  [
145
146
  [
146
147
  "claim",
@@ -191,6 +192,20 @@ describe("strict CLI parsing", () => {
191
192
  }
192
193
  })
193
194
 
195
+ test("parses reconciliation modes and rejects conflicting modes", () => {
196
+ expect(parseCli(["sync", "--dry-run", "--json"])).toMatchObject({
197
+ commandName: "sync",
198
+ values: { "dry-run": true, json: true },
199
+ })
200
+ expect(parseCli(["sync", "--apply"])).toMatchObject({
201
+ commandName: "sync",
202
+ values: { apply: true },
203
+ })
204
+ expect(() => parseCli(["sync", "--dry-run", "--apply"])).toThrow(
205
+ "cannot be combined",
206
+ )
207
+ })
208
+
194
209
  test("validates revision-guarded claim operations", () => {
195
210
  const revision = "0".repeat(64)
196
211
  expect(
package/src/cli-parser.ts CHANGED
@@ -319,6 +319,21 @@ const commands = {
319
319
  required: ["session-id", "revision", "outcome"],
320
320
  },
321
321
  },
322
+ sync: {
323
+ usage: "agency sync [--dry-run | --apply] [--json]",
324
+ options: {
325
+ ...outputOptions,
326
+ "dry-run": { type: "boolean" },
327
+ apply: { type: "boolean" },
328
+ },
329
+ command: {
330
+ usage: "agency sync [--dry-run | --apply] [--json]",
331
+ minArgs: 0,
332
+ maxArgs: 0,
333
+ options: ["dry-run", "apply", "json"],
334
+ conflicts: [["dry-run", "apply"]],
335
+ },
336
+ },
322
337
  archive: {
323
338
  usage: "agency archive <epic|task|phase>",
324
339
  options: outputOptions,
@@ -0,0 +1,36 @@
1
+ import { Effect } from "effect"
2
+ import { SyncService } from "../services/SyncService"
3
+ import type { BaseCommandOptions } from "../utils/command"
4
+ import { createLoggers } from "../utils/effect"
5
+
6
+ interface SyncCommandOptions extends BaseCommandOptions {
7
+ readonly apply?: boolean
8
+ readonly dryRun?: boolean
9
+ }
10
+
11
+ export const sync = (options: SyncCommandOptions = {}) =>
12
+ Effect.gen(function* () {
13
+ const service = yield* SyncService
14
+ const { log } = createLoggers(options)
15
+ const result = yield* service.reconcile({
16
+ cwd: options.cwd,
17
+ apply: options.apply === true,
18
+ })
19
+ log(JSON.stringify(result, null, 2))
20
+ })
21
+
22
+ export const help = `
23
+ Usage: agency sync [--dry-run | --apply] [--json]
24
+
25
+ Compare declared execution state with Git worktrees, branches, references, claims,
26
+ and GitHub pull requests. Dry-run is the default.
27
+
28
+ Options:
29
+ --dry-run Report planned safe transitions without changing state
30
+ --apply Apply safe reconciliation transitions
31
+ --json Output one versioned machine result
32
+
33
+ Apply may materialize unambiguous missing checkouts, release expired claims,
34
+ record a uniquely matched PR, and mark merged work done. Dirty, stale, or
35
+ conflicting checkouts are always left unresolved.
36
+ `
@@ -6,6 +6,8 @@ import { EpicService } from "../services/EpicService"
6
6
  import { RepositoryService } from "../services/RepositoryService"
7
7
  import { createLoggers } from "../utils/effect"
8
8
  import { parseRepositoryReferences } from "../workbase/repository-reference"
9
+ import { WorkbaseService } from "../services/WorkbaseService"
10
+ import { choose } from "../utils/chooser"
9
11
 
10
12
  interface TaskOptions extends BaseCommandOptions {
11
13
  readonly subcommand?: string
@@ -29,7 +31,9 @@ export interface TaskInteraction {
29
31
  ) => Effect.Effect<string | null, Error>
30
32
  }
31
33
 
32
- const defaultInteraction: TaskInteraction = {
34
+ const defaultInteraction = (
35
+ chooserCommand?: readonly string[],
36
+ ): TaskInteraction => ({
33
37
  text: (prompt) =>
34
38
  Effect.tryPromise({
35
39
  try: async () => {
@@ -46,37 +50,23 @@ const defaultInteraction: TaskInteraction = {
46
50
  catch: (cause) => new Error("Failed to read task input", { cause }),
47
51
  }),
48
52
  select: (prompt, choices) =>
49
- Effect.tryPromise({
50
- try: async () => {
51
- const process = Bun.spawn(
52
- ["fzf", `--prompt=${prompt}> `, "--height=~40%", "--reverse"],
53
- {
54
- stdin: new Blob([choices.join("\n")]),
55
- stdout: "pipe",
56
- stderr: "inherit",
57
- },
58
- )
59
- const [exitCode, output] = await Promise.all([
60
- process.exited,
61
- new Response(process.stdout).text(),
62
- ])
63
- if (exitCode === 1 || exitCode === 130) return null
64
- if (exitCode !== 0) throw new Error(`fzf exited with code ${exitCode}`)
65
- return output.trim() || null
66
- },
67
- catch: (cause) =>
68
- new Error("Failed to select task input with fzf", { cause }),
69
- }),
70
- }
53
+ choose(
54
+ prompt,
55
+ choices.map((choice, index) => ({
56
+ key: String(index),
57
+ label: choice,
58
+ value: choice,
59
+ })),
60
+ chooserCommand,
61
+ ),
62
+ })
71
63
 
72
- export const task = (
73
- options: TaskOptions,
74
- interaction: TaskInteraction = defaultInteraction,
75
- ) =>
64
+ export const task = (options: TaskOptions, interaction?: TaskInteraction) =>
76
65
  Effect.gen(function* () {
77
66
  const tasks = yield* TaskService
78
67
  const epics = yield* EpicService
79
68
  const repositories = yield* RepositoryService
69
+ const workbase = yield* WorkbaseService
80
70
  const { log } = createLoggers(options)
81
71
  const cwd = options.cwd ?? process.cwd()
82
72
 
@@ -89,8 +79,13 @@ export const task = (
89
79
  ),
90
80
  )
91
81
  }
82
+ const activeInteraction =
83
+ interaction ??
84
+ defaultInteraction(
85
+ (yield* workbase.loadConfig(cwd)).config.chooserCommand,
86
+ )
92
87
  const id =
93
- options.args[0] ?? (yield* interaction.text("Task ID: ")).trim()
88
+ options.args[0] ?? (yield* activeInteraction.text("Task ID: ")).trim()
94
89
  if (!id) {
95
90
  return yield* Effect.fail(new Error("Task ID is required"))
96
91
  }
@@ -103,18 +98,20 @@ export const task = (
103
98
 
104
99
  if (options.ticketUrl === undefined) {
105
100
  ticketUrl =
106
- (yield* interaction.text("Ticket URL (optional): ")).trim() || null
101
+ (yield* activeInteraction.text("Ticket URL (optional): ")).trim() ||
102
+ null
107
103
  }
108
104
  if (options.description === undefined) {
109
105
  description =
110
- (yield* interaction.text("Description (optional): ")).trim() ||
111
- undefined
106
+ (yield* activeInteraction.text(
107
+ "Description (optional): ",
108
+ )).trim() || undefined
112
109
  }
113
110
  if (options.epic === undefined) {
114
111
  const epicRecords = yield* epics.list(cwd)
115
112
  if (epicRecords.length > 0) {
116
113
  const none = "(none)"
117
- const selected = yield* interaction.select("Parent epic", [
114
+ const selected = yield* activeInteraction.select("Parent epic", [
118
115
  none,
119
116
  ...epicRecords.map((record) => record.id),
120
117
  ])
@@ -125,7 +122,7 @@ export const task = (
125
122
  }
126
123
  }
127
124
  if (options.multiPhase === undefined) {
128
- const selected = yield* interaction.select("Task type", [
125
+ const selected = yield* activeInteraction.select("Task type", [
129
126
  "single-phase",
130
127
  "multi-phase",
131
128
  ])
@@ -144,7 +141,7 @@ export const task = (
144
141
  ),
145
142
  )
146
143
  }
147
- const selected = yield* interaction.select(
144
+ const selected = yield* activeInteraction.select(
148
145
  "Writable repository",
149
146
  records.map((record) => record.alias),
150
147
  )
@@ -32,12 +32,7 @@ export const validate = (
32
32
  const startPath = options.path ?? options.cwd ?? process.cwd()
33
33
  const root = options.path
34
34
  ? yield* workbase.discover(startPath)
35
- : yield* resolveWorkbase(
36
- startPath,
37
- log,
38
- pick,
39
- options.inputAllowed ?? true,
40
- )
35
+ : yield* resolveWorkbase(startPath, pick, options.inputAllowed ?? true)
41
36
  if (!root) return
42
37
  const report = yield* workbase.validate(root)
43
38
 
@@ -46,7 +46,8 @@ const multiPhaseWorkspace: ExecutionWorkspace = {
46
46
  interface HarnessOptions {
47
47
  readonly workspace?: ExecutionWorkspace
48
48
  readonly materializeError?: Error
49
- readonly available?: Partial<Record<"opencode" | "claude" | "fzf", boolean>>
49
+ readonly available?: Partial<Record<"opencode" | "claude", boolean>>
50
+ readonly chooserCommand?: readonly string[]
50
51
  readonly multiPhaseTasks?: readonly string[]
51
52
  readonly epicRecords?: readonly any[]
52
53
  readonly taskRecords?: readonly any[]
@@ -93,6 +94,14 @@ const createHarness = (options: HarnessOptions = {}) => {
93
94
  })
94
95
  : Effect.succeed("/workbase"),
95
96
  listRegistered: () => Effect.succeed(options.registeredWorkbases ?? []),
97
+ loadConfig: () =>
98
+ Effect.succeed({
99
+ root: "/workbase",
100
+ config: {
101
+ version: 2 as const,
102
+ chooserCommand: options.chooserCommand,
103
+ },
104
+ }),
96
105
  }
97
106
  const epics = {
98
107
  show: (id: string) =>
@@ -359,7 +368,7 @@ describe("work command", () => {
359
368
  expect(harness.launches[0]?.cwd).toBe(singlePhaseWorkspace.writablePath)
360
369
  })
361
370
 
362
- test("selects a target with fzf when no directory is provided", async () => {
371
+ test("selects a target when no directory is provided", async () => {
363
372
  const phase = {
364
373
  taskId: "delivery",
365
374
  id: "build",
@@ -385,7 +394,6 @@ describe("work command", () => {
385
394
  await harness.run({ cwd: "/workbase/tasks/example", opencode: true }, pick)
386
395
 
387
396
  expect(harness.events).toEqual([
388
- "probe:fzf",
389
397
  "materialize",
390
398
  "probe:opencode",
391
399
  "launch:opencode",
@@ -442,7 +450,6 @@ describe("work command", () => {
442
450
 
443
451
  expect(selections).toEqual([["/first", "/workbase"]])
444
452
  expect(harness.events).toEqual([
445
- "probe:fzf",
446
453
  "materialize",
447
454
  "probe:opencode",
448
455
  "launch:opencode",
@@ -476,9 +483,9 @@ describe("work command", () => {
476
483
  expect(harness.events).toEqual([])
477
484
  })
478
485
 
479
- test("prints the target tree when fzf is unavailable", async () => {
486
+ test("passes the configured chooser command to the shared picker", async () => {
480
487
  const harness = createHarness({
481
- available: { fzf: false },
488
+ chooserCommand: ["gum", "filter"],
482
489
  epicRecords: [
483
490
  {
484
491
  id: "delivery",
@@ -487,14 +494,15 @@ describe("work command", () => {
487
494
  },
488
495
  ],
489
496
  })
497
+ let command: readonly string[] | undefined
498
+ const pick: PickWorkTarget = (_choices, chooserCommand) => {
499
+ command = chooserCommand
500
+ return Effect.succeed(null)
501
+ }
490
502
 
491
- const logs = await captureLogs(async () => {
492
- await expect(harness.run({ cwd: "/workbase" })).rejects.toThrow(
493
- "fzf is required",
494
- )
495
- })
503
+ await harness.run({ cwd: "/workbase" }, pick)
496
504
 
497
- expect(logs).toEqual(["\x1b[35m\x1b[0m delivery"])
505
+ expect(command).toEqual(["gum", "filter"])
498
506
  expect(harness.launches).toEqual([])
499
507
  })
500
508
 
@@ -89,7 +89,7 @@ export const work = (
89
89
  : false
90
90
  const startPath = isDirectory && directoryPath ? directoryPath : cwd
91
91
  const inputAllowed = options.inputAllowed ?? true
92
- const root = yield* resolveWorkbase(startPath, log, pickBase, inputAllowed)
92
+ const root = yield* resolveWorkbase(startPath, pickBase, inputAllowed)
93
93
  if (!root) return
94
94
 
95
95
  let target: WorkTarget | null = null
@@ -178,18 +178,8 @@ export const work = (
178
178
  new Error("No epics, tasks, or phases found in this workbase"),
179
179
  )
180
180
  }
181
- const fzf = yield* fs.runCommand(["which", "fzf"], {
182
- captureOutput: true,
183
- })
184
- if (fzf.exitCode !== 0) {
185
- for (const choice of choices) log(choice.label)
186
- return yield* Effect.fail(
187
- new Error(
188
- "fzf is required to select a work target; install fzf or provide a directory explicitly",
189
- ),
190
- )
191
- }
192
- target = yield* pick(choices)
181
+ const { config } = yield* workbase.loadConfig(root)
182
+ target = yield* pick(choices, config.chooserCommand)
193
183
  if (!target) return
194
184
  }
195
185
 
@@ -332,7 +322,7 @@ Usage: agency work [<directory-or-task-id> | --epic <epic-id>]
332
322
  agency work prepare [target] [--dry-run] [--json]
333
323
 
334
324
  Launch an agent for an epic, task, or phase. With no directory, select one
335
- with fzf. A positional argument resolves as a directory first, then as a task
325
+ interactively. A positional argument resolves as a directory first, then as a task
336
326
  ID. Use '.' for the current directory. Outside a workbase, select a registered
337
327
  workbase first.
338
328
 
package/src/protocol.ts CHANGED
@@ -111,6 +111,11 @@ const errorMetadata: Readonly<Record<string, ErrorMetadata>> = {
111
111
  retryable: false,
112
112
  remediation: "Correct the workbase graph data or filters and retry.",
113
113
  },
114
+ SyncError: {
115
+ code: "SYNC_ERROR",
116
+ retryable: false,
117
+ remediation: "Resolve workbase validation errors before reconciling.",
118
+ },
114
119
  ProcessError: { code: "PROCESS_ERROR", retryable: true },
115
120
  ProtocolOutputError: {
116
121
  code: "PROTOCOL_OUTPUT_ERROR",
@@ -87,6 +87,23 @@ interface FinishInput extends OwnedClaimInput {
87
87
  readonly outcome: "done" | "dropped"
88
88
  }
89
89
 
90
+ interface ExpireClaimInput {
91
+ readonly taskId: string
92
+ readonly phaseId?: string
93
+ readonly revision: string
94
+ readonly now?: Date
95
+ }
96
+
97
+ interface ReconcileInput {
98
+ readonly taskId: string
99
+ readonly phaseId?: string
100
+ readonly revision: string
101
+ readonly pr?: string
102
+ readonly status?: "done"
103
+ }
104
+
105
+ const PR_URL = /^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+\/?$/
106
+
90
107
  type SingleTaskData = Extract<TaskData, { readonly repo: string }>
91
108
  type ExecutionData = SingleTaskData | PhaseData
92
109
 
@@ -286,6 +303,83 @@ export class ClaimService extends Effect.Service<ClaimService>()(
286
303
  }
287
304
  }),
288
305
 
306
+ expire: (input: ExpireClaimInput, startPath: string = process.cwd()) =>
307
+ Effect.gen(function* () {
308
+ const service = yield* ClaimService
309
+ const inspected = yield* service.inspect(
310
+ input.taskId,
311
+ input.phaseId,
312
+ startPath,
313
+ )
314
+ return yield* operation(() =>
315
+ updateAtomically(
316
+ inspected.target,
317
+ input.revision,
318
+ (data, now) => {
319
+ if (
320
+ data.claim?.state !== "active" ||
321
+ data.claim.expiresAt === undefined ||
322
+ Date.parse(data.claim.expiresAt) > now.getTime() ||
323
+ (data.status !== "working" && data.status !== "delegated")
324
+ ) {
325
+ throw new ClaimError({
326
+ target: inspected.target.label,
327
+ message: `${inspected.target.label} does not have an expired active claim`,
328
+ })
329
+ }
330
+ const claim: ClaimRecord = {
331
+ ...data.claim,
332
+ state: "released",
333
+ releasedAt: now.toISOString(),
334
+ }
335
+ return { data: { ...data, status: "open", claim }, claim }
336
+ },
337
+ input.now ?? new Date(),
338
+ ),
339
+ )
340
+ }),
341
+
342
+ reconcile: (input: ReconcileInput, startPath: string = process.cwd()) =>
343
+ Effect.gen(function* () {
344
+ if (input.pr !== undefined && !PR_URL.test(input.pr)) {
345
+ return yield* new ClaimError({
346
+ message: `Invalid GitHub pull request URL: ${input.pr}`,
347
+ })
348
+ }
349
+ const service = yield* ClaimService
350
+ const inspected = yield* service.inspect(
351
+ input.taskId,
352
+ input.phaseId,
353
+ startPath,
354
+ )
355
+ return yield* operation(() =>
356
+ updateAtomically(
357
+ inspected.target,
358
+ input.revision,
359
+ (data) => {
360
+ if (input.status === "done" && data.claim?.state === "active") {
361
+ throw new ClaimConflictError({
362
+ target: inspected.target.label,
363
+ currentRevision: input.revision,
364
+ claim: data.claim,
365
+ message: `${inspected.target.label} has an active claim`,
366
+ })
367
+ }
368
+ return {
369
+ data: {
370
+ ...data,
371
+ ...(input.pr !== undefined ? { pr: input.pr } : {}),
372
+ ...(input.status !== undefined
373
+ ? { status: input.status }
374
+ : {}),
375
+ },
376
+ }
377
+ },
378
+ new Date(),
379
+ ),
380
+ )
381
+ }),
382
+
289
383
  claim: (input: ClaimInput, startPath: string = process.cwd()) =>
290
384
  Effect.gen(function* () {
291
385
  for (const [label, value] of [