@markjaquith/agency 2.12.0 → 2.14.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.
@@ -193,6 +193,8 @@ export class PhaseService extends Effect.Service<PhaseService>()(
193
193
  branch: task.data.branch,
194
194
  base: task.data.base,
195
195
  pr: task.data.pr,
196
+ status: task.data.status,
197
+ ...(task.data.claim ? { claim: task.data.claim } : {}),
196
198
  })
197
199
  const firstTitle = firstPhaseId!
198
200
  .split("-")
@@ -340,7 +342,18 @@ export class PhaseService extends Effect.Service<PhaseService>()(
340
342
  const fs = yield* FileSystemService
341
343
  const service = yield* PhaseService
342
344
  const validStatus = yield* decodeStatus(status)
345
+ if (validStatus === "working" || validStatus === "delegated") {
346
+ return yield* new PhaseError({
347
+ message:
348
+ "Active work and delegation require explicit ownership; use 'agency claim'",
349
+ })
350
+ }
343
351
  const record = yield* service.show(taskId, id, startPath)
352
+ if (record.data.claim?.state === "active") {
353
+ return yield* new PhaseError({
354
+ message: `Phase '${id}' has an active claim; use agency release or agency finish`,
355
+ })
356
+ }
344
357
  if (!canTransitionStatus(record.data.status, validStatus)) {
345
358
  return yield* new PhaseError({
346
359
  message: `Cannot transition phase '${id}' from ${record.data.status} to ${validStatus}; reopen it first`,
@@ -234,16 +234,17 @@ describe("task and phase services", () => {
234
234
  ),
235
235
  )
236
236
  expect(createdTask.content).toContain("status: open")
237
- const task = await runTestEffect(
238
- TaskService.pipe(
239
- Effect.flatMap((service) =>
240
- service.setStatus("single-status", "delegated", root),
237
+ for (const status of ["working", "delegated"]) {
238
+ await expect(
239
+ runTestEffect(
240
+ TaskService.pipe(
241
+ Effect.flatMap((service) =>
242
+ service.setStatus("single-status", status, root),
243
+ ),
244
+ ),
241
245
  ),
242
- ),
243
- )
244
- expect(task.data.status).toBe("delegated")
245
- expect(task.content).toContain("status: delegated")
246
- expect(task.content).toContain("Describe the task outcome.")
246
+ ).rejects.toThrow("require explicit ownership")
247
+ }
247
248
  await runTestEffect(
248
249
  TaskService.pipe(
249
250
  Effect.flatMap((service) =>
@@ -207,12 +207,23 @@ export class TaskService extends Effect.Service<TaskService>()("TaskService", {
207
207
  const fs = yield* FileSystemService
208
208
  const service = yield* TaskService
209
209
  const validStatus = yield* decodeStatus(status)
210
+ if (validStatus === "working" || validStatus === "delegated") {
211
+ return yield* new TaskError({
212
+ message:
213
+ "Active work and delegation require explicit ownership; use 'agency claim'",
214
+ })
215
+ }
210
216
  const record = yield* service.show(id, startPath)
211
217
  if ("phases" in record.data) {
212
218
  return yield* new TaskError({
213
219
  message: `Task '${id}' has multiple phases; set status on a phase instead`,
214
220
  })
215
221
  }
222
+ if (record.data.claim?.state === "active") {
223
+ return yield* new TaskError({
224
+ message: `Task '${id}' has an active claim; use agency release or agency finish`,
225
+ })
226
+ }
216
227
  if (!canTransitionStatus(record.data.status, validStatus)) {
217
228
  return yield* new TaskError({
218
229
  message: `Cannot transition task '${id}' from ${record.data.status} to ${validStatus}; reopen it first`,
@@ -113,15 +113,17 @@ describe("WorktreeService", () => {
113
113
  ["remote", "set-url", "origin", join(root, "missing")],
114
114
  join(root, "repos/agency"),
115
115
  )
116
- await expect(
117
- runTestEffect(
118
- WorktreeService.pipe(
119
- Effect.flatMap((service) =>
120
- service.materialize("existing", undefined, root),
121
- ),
116
+ const reused = await runTestEffect(
117
+ WorktreeService.pipe(
118
+ Effect.flatMap((service) =>
119
+ service.materialize("existing", undefined, root),
122
120
  ),
123
121
  ),
124
- ).resolves.toMatchObject({ repo: "agency" })
122
+ )
123
+ expect(reused.checkouts).toEqual([
124
+ expect.objectContaining({ repo: "agency", action: "reused" }),
125
+ ])
126
+ expect(reused.operations).toEqual([])
125
127
  })
126
128
 
127
129
  test("reuses an immutable reference checkout without fetching", async () => {
@@ -771,6 +773,79 @@ pr: null
771
773
  "refs/heads/task/removable",
772
774
  ])
773
775
  expect(branch.exitCode).toBe(0)
776
+ expect(workspace.checkouts).toEqual([
777
+ expect.objectContaining({
778
+ repo: "agency",
779
+ kind: "writable",
780
+ action: "created",
781
+ resolvedCommit: expect.stringMatching(/^[0-9a-f]{40}$/),
782
+ }),
783
+ expect.objectContaining({
784
+ repo: "effect",
785
+ kind: "reference",
786
+ action: "created",
787
+ resolvedCommit: expect.stringMatching(/^[0-9a-f]{40}$/),
788
+ }),
789
+ ])
790
+ })
791
+
792
+ test("reports dry-run fetch and worktree changes without mutating", async () => {
793
+ await runTestEffect(
794
+ TaskService.pipe(
795
+ Effect.flatMap((service) =>
796
+ service.create(
797
+ {
798
+ id: "planned",
799
+ ticketUrl: "https://example.com/task",
800
+ repo: "agency",
801
+ repos: [{ repo: "effect", ref: "main" }],
802
+ branch: "task/planned",
803
+ base: "main",
804
+ },
805
+ root,
806
+ ),
807
+ ),
808
+ ),
809
+ )
810
+
811
+ const workspace = await runTestEffect(
812
+ WorktreeService.pipe(
813
+ Effect.flatMap((service) =>
814
+ service.materialize("planned", undefined, root, { dryRun: true }),
815
+ ),
816
+ ),
817
+ )
818
+
819
+ expect(workspace.dryRun).toBe(true)
820
+ expect(
821
+ workspace.checkouts.every(({ resolvedCommit }) => resolvedCommit),
822
+ ).toBe(true)
823
+ expect(
824
+ workspace.checkouts.map(({ repo, action }) => ({ repo, action })),
825
+ ).toEqual([
826
+ { repo: "agency", action: "created" },
827
+ { repo: "effect", action: "created" },
828
+ ])
829
+ expect(workspace.operations).toEqual(
830
+ expect.arrayContaining([
831
+ expect.objectContaining({ action: "fetch", status: "planned" }),
832
+ expect.objectContaining({
833
+ action: "create-worktree",
834
+ status: "planned",
835
+ }),
836
+ ]),
837
+ )
838
+ expect(await Bun.file(workspace.codePath).exists()).toBe(false)
839
+ expect(
840
+ Bun.spawnSync([
841
+ "git",
842
+ "-C",
843
+ join(root, "repos/agency"),
844
+ "show-ref",
845
+ "--verify",
846
+ "refs/heads/task/planned",
847
+ ]).exitCode,
848
+ ).not.toBe(0)
774
849
  })
775
850
 
776
851
  test("refuses to remove a worktree with uncommitted changes", async () => {
@@ -16,6 +16,22 @@ class WorktreeError extends Data.TaggedError("WorktreeError")<{
16
16
  readonly message: string
17
17
  }> {}
18
18
 
19
+ interface WorkspaceOperation {
20
+ readonly action: "fetch" | "create-branch" | "create-worktree"
21
+ readonly repo: string
22
+ readonly command: readonly string[]
23
+ readonly status: "planned" | "completed"
24
+ }
25
+
26
+ interface WorkspaceCheckout {
27
+ readonly repo: string
28
+ readonly kind: "writable" | "reference"
29
+ readonly path: string
30
+ readonly requestedRef: string
31
+ readonly resolvedCommit: string | null
32
+ readonly action: "created" | "reused"
33
+ }
34
+
19
35
  interface ExecutionWorkspace {
20
36
  readonly root: string
21
37
  readonly taskPath: string
@@ -24,6 +40,9 @@ interface ExecutionWorkspace {
24
40
  readonly writablePath: string
25
41
  readonly repo: string
26
42
  readonly repos: readonly RepositoryReference[]
43
+ readonly dryRun: boolean
44
+ readonly checkouts: readonly WorkspaceCheckout[]
45
+ readonly operations: readonly WorkspaceOperation[]
27
46
  }
28
47
 
29
48
  interface GitWorktree {
@@ -121,7 +140,9 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
121
140
  codePath = join(dirname(task.path), "code")
122
141
  }
123
142
 
124
- yield* fs.createDirectory(codePath)
143
+ if (!options.dryRun) yield* fs.createDirectory(codePath)
144
+ const operations: WorkspaceOperation[] = []
145
+ const checkoutReports: WorkspaceCheckout[] = []
125
146
  const checkouts: readonly (
126
147
  | { readonly repo: string; readonly branch: string }
127
148
  | RepositoryReference
@@ -146,23 +167,38 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
146
167
  { captureOutput: true },
147
168
  )
148
169
  if (remote.exitCode !== 0) return false
170
+ const command = [
171
+ "git",
172
+ "-C",
173
+ repositoryPath,
174
+ "fetch",
175
+ "origin",
176
+ ...(ref ? [ref] : []),
177
+ ]
178
+ if (options.dryRun) {
179
+ operations.push({
180
+ action: "fetch",
181
+ repo: alias,
182
+ command,
183
+ status: "planned",
184
+ })
185
+ return false
186
+ }
149
187
 
150
- const fetch = yield* fs.runCommand(
151
- [
152
- "git",
153
- "-C",
154
- repositoryPath,
155
- "fetch",
156
- "origin",
157
- ...(ref ? [ref] : []),
158
- ],
159
- { captureOutput: true },
160
- )
188
+ const fetch = yield* fs.runCommand(command, {
189
+ captureOutput: true,
190
+ })
161
191
  if (fetch.exitCode !== 0) {
162
192
  return yield* new WorktreeError({
163
193
  message: `Failed to fetch '${alias}': ${fetch.stderr}`,
164
194
  })
165
195
  }
196
+ operations.push({
197
+ action: "fetch",
198
+ repo: alias,
199
+ command,
200
+ status: "completed",
201
+ })
166
202
  return true
167
203
  })
168
204
 
@@ -183,7 +219,9 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
183
219
  message: `Failed to inspect worktrees for '${alias}': ${listed.stderr}`,
184
220
  })
185
221
  }
186
- const canonicalCodePath = yield* fs.realPath(codePath)
222
+ const canonicalCodePath = (yield* fs.exists(codePath))
223
+ ? yield* fs.realPath(codePath)
224
+ : resolve(codePath)
187
225
  const canonicalCheckoutPath = join(canonicalCodePath, alias)
188
226
  const worktrees: GitWorktree[] = []
189
227
  for (const worktree of parseWorktreeList(listed.stdout)) {
@@ -212,7 +250,17 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
212
250
  })
213
251
  }
214
252
  if (yield* fs.isDirectory(checkoutPath)) {
215
- if (registeredAtPath?.branch === branchRef) continue
253
+ if (registeredAtPath?.branch === branchRef) {
254
+ checkoutReports.push({
255
+ repo: alias,
256
+ kind: "writable",
257
+ path: checkoutPath,
258
+ requestedRef: checkout.branch,
259
+ resolvedCommit: registeredAtPath.head ?? null,
260
+ action: "reused",
261
+ })
262
+ continue
263
+ }
216
264
  return yield* new WorktreeError({
217
265
  message: `Existing checkout ${checkoutPath} is not registered to branch '${checkout.branch}'`,
218
266
  })
@@ -260,21 +308,29 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
260
308
  { captureOutput: true },
261
309
  )
262
310
  if (branchExists.exitCode !== 0) {
263
- const createBranch = yield* fs.runCommand(
264
- [
265
- "git",
266
- "-C",
267
- repositoryPath,
268
- "branch",
269
- checkout.branch,
270
- execution.base,
271
- ],
272
- { captureOutput: true },
273
- )
274
- if (createBranch.exitCode !== 0) {
275
- return yield* new WorktreeError({
276
- message: `Failed to create branch '${checkout.branch}': ${createBranch.stderr}`,
311
+ const command = [
312
+ "git",
313
+ "-C",
314
+ repositoryPath,
315
+ "branch",
316
+ checkout.branch,
317
+ execution.base,
318
+ ]
319
+ operations.push({
320
+ action: "create-branch",
321
+ repo: alias,
322
+ command,
323
+ status: options.dryRun ? "planned" : "completed",
324
+ })
325
+ if (!options.dryRun) {
326
+ const createBranch = yield* fs.runCommand(command, {
327
+ captureOutput: true,
277
328
  })
329
+ if (createBranch.exitCode !== 0) {
330
+ return yield* new WorktreeError({
331
+ message: `Failed to create branch '${checkout.branch}': ${createBranch.stderr}`,
332
+ })
333
+ }
278
334
  }
279
335
  }
280
336
  args = [
@@ -287,6 +343,48 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
287
343
  checkout.branch,
288
344
  ]
289
345
  }
346
+ if (options.dryRun) {
347
+ operations.push({
348
+ action: "create-worktree",
349
+ repo: alias,
350
+ command: args,
351
+ status: "planned",
352
+ })
353
+ let resolved = yield* fs.runCommand(
354
+ [
355
+ "git",
356
+ "-C",
357
+ repositoryPath,
358
+ "rev-parse",
359
+ "--verify",
360
+ `${checkout.branch}^{commit}`,
361
+ ],
362
+ { captureOutput: true },
363
+ )
364
+ if (resolved.exitCode !== 0) {
365
+ resolved = yield* fs.runCommand(
366
+ [
367
+ "git",
368
+ "-C",
369
+ repositoryPath,
370
+ "rev-parse",
371
+ "--verify",
372
+ `${execution.base}^{commit}`,
373
+ ],
374
+ { captureOutput: true },
375
+ )
376
+ }
377
+ checkoutReports.push({
378
+ repo: alias,
379
+ kind: "writable",
380
+ path: checkoutPath,
381
+ requestedRef: checkout.branch,
382
+ resolvedCommit:
383
+ resolved.exitCode === 0 ? resolved.stdout.trim() : null,
384
+ action: "created",
385
+ })
386
+ continue
387
+ }
290
388
 
291
389
  if (config.worktreeCreateCommand) {
292
390
  verboseLog(`Running worktree command: ${formatCommand(args)}`)
@@ -308,6 +406,24 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
308
406
  message: `Worktree command did not create ${checkoutPath}`,
309
407
  })
310
408
  }
409
+ operations.push({
410
+ action: "create-worktree",
411
+ repo: alias,
412
+ command: args,
413
+ status: "completed",
414
+ })
415
+ const head = yield* fs.runCommand(
416
+ ["git", "-C", checkoutPath, "rev-parse", "HEAD"],
417
+ { captureOutput: true },
418
+ )
419
+ checkoutReports.push({
420
+ repo: alias,
421
+ kind: "writable",
422
+ path: checkoutPath,
423
+ requestedRef: checkout.branch,
424
+ resolvedCommit: head.exitCode === 0 ? head.stdout.trim() : null,
425
+ action: "created",
426
+ })
311
427
  } else {
312
428
  const fetched = isCommitId(checkout.ref)
313
429
  ? false
@@ -349,6 +465,14 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
349
465
  currentHead.exitCode === 0 &&
350
466
  currentHead.stdout.trim() === commit
351
467
  ) {
468
+ checkoutReports.push({
469
+ repo: alias,
470
+ kind: "reference",
471
+ path: checkoutPath,
472
+ requestedRef: checkout.ref,
473
+ resolvedCommit: commit,
474
+ action: "reused",
475
+ })
352
476
  continue
353
477
  }
354
478
  return yield* new WorktreeError({
@@ -360,24 +484,55 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
360
484
  message: `Worktree registry contains a missing checkout at ${checkoutPath}`,
361
485
  })
362
486
  }
363
- const result = yield* fs.runCommand(
364
- [
365
- "git",
366
- "-C",
367
- repositoryPath,
368
- "worktree",
369
- "add",
370
- "--detach",
371
- checkoutPath,
372
- commit,
373
- ],
374
- { captureOutput: true },
375
- )
487
+ const command = [
488
+ "git",
489
+ "-C",
490
+ repositoryPath,
491
+ "worktree",
492
+ "add",
493
+ "--detach",
494
+ checkoutPath,
495
+ commit,
496
+ ]
497
+ if (options.dryRun) {
498
+ operations.push({
499
+ action: "create-worktree",
500
+ repo: alias,
501
+ command,
502
+ status: "planned",
503
+ })
504
+ checkoutReports.push({
505
+ repo: alias,
506
+ kind: "reference",
507
+ path: checkoutPath,
508
+ requestedRef: checkout.ref,
509
+ resolvedCommit: commit,
510
+ action: "created",
511
+ })
512
+ continue
513
+ }
514
+ const result = yield* fs.runCommand(command, {
515
+ captureOutput: true,
516
+ })
376
517
  if (result.exitCode !== 0) {
377
518
  return yield* new WorktreeError({
378
519
  message: `Failed to create worktree for '${alias}': ${result.stderr}`,
379
520
  })
380
521
  }
522
+ operations.push({
523
+ action: "create-worktree",
524
+ repo: alias,
525
+ command,
526
+ status: "completed",
527
+ })
528
+ checkoutReports.push({
529
+ repo: alias,
530
+ kind: "reference",
531
+ path: checkoutPath,
532
+ requestedRef: checkout.ref,
533
+ resolvedCommit: commit,
534
+ action: "created",
535
+ })
381
536
  }
382
537
  }
383
538
 
@@ -389,6 +544,9 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
389
544
  writablePath: join(codePath, execution.repo),
390
545
  repo: execution.repo,
391
546
  repos: execution.repos ?? [],
547
+ dryRun: options.dryRun === true,
548
+ checkouts: checkoutReports,
549
+ operations,
392
550
  } satisfies ExecutionWorkspace
393
551
  }),
394
552
 
package/src/test-utils.ts CHANGED
@@ -15,6 +15,7 @@ import { ArchiveService } from "./services/ArchiveService"
15
15
  import { IntegrationService } from "./services/IntegrationService"
16
16
  import { ContextService } from "./services/ContextService"
17
17
  import { GraphService } from "./services/GraphService"
18
+ import { ClaimService } from "./services/ClaimService"
18
19
 
19
20
  export const createTempDir = () => mkdtemp(join(tmpdir(), "agency-test-"))
20
21
 
@@ -34,6 +35,7 @@ const TestLayer = Layer.mergeAll(
34
35
  IntegrationService.Default,
35
36
  ContextService.Default,
36
37
  GraphService.Default,
38
+ ClaimService.Default,
37
39
  )
38
40
 
39
41
  export async function runTestEffect<A, E>(
@@ -5,6 +5,7 @@ export interface BaseCommandOptions {
5
5
  readonly silent?: boolean
6
6
  readonly verbose?: boolean
7
7
  readonly json?: boolean
8
+ readonly dryRun?: boolean
8
9
  /** Whether this invocation may open prompts or selectors. */
9
10
  readonly inputAllowed?: boolean
10
11
  /**
@@ -26,8 +26,8 @@ field. Repositories listed in plural `repos` are read-only references.
26
26
  `agency integration sync` to update managed agent files explicitly.
27
27
  - Keep task-level decisions in `TASK.md` and phase-specific delivery context in
28
28
  `PHASE.md`.
29
- - Keep execution-unit `status` current with `agency task status` or
30
- `agency phase status`; `agency work` marks launched work as `working`.
29
+ - Coordinate execution ownership with `agency claim`, `agency release`, and
30
+ `agency finish`; `agency work` claims execution units before launch.
31
31
  - Do not manually create, move, or remove worktrees under `code/`.
32
32
  - Use `agency archive`, rather than moving work item folders manually.
33
33
  - Do not edit bare repositories or repository symlinks under `repos/`.
@@ -0,0 +1,2 @@
1
+ export const documentRevision = (content: string) =>
2
+ new Bun.CryptoHasher("sha256").update(content).digest("hex")
@@ -12,65 +12,71 @@ interface ParsedFrontmatter {
12
12
  readonly body: string
13
13
  }
14
14
 
15
- export const parseFrontmatter = (content: string, path: string) =>
16
- Effect.try({
17
- try: (): ParsedFrontmatter => {
18
- const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)
19
- if (!match) {
20
- throw new Error("Markdown file must begin with YAML frontmatter")
21
- }
15
+ export const parseFrontmatterSync = (
16
+ content: string,
17
+ path: string,
18
+ ): ParsedFrontmatter => {
19
+ try {
20
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)
21
+ if (!match) {
22
+ throw new Error("Markdown file must begin with YAML frontmatter")
23
+ }
24
+
25
+ const document = parseDocument(match[1]!, {
26
+ customTags: [],
27
+ merge: false,
28
+ schema: "core",
29
+ strict: true,
30
+ uniqueKeys: true,
31
+ version: "1.2",
32
+ })
22
33
 
23
- const document = parseDocument(match[1]!, {
24
- customTags: [],
25
- merge: false,
26
- schema: "core",
27
- strict: true,
28
- uniqueKeys: true,
29
- version: "1.2",
30
- })
34
+ const parseMessages = [...document.errors, ...document.warnings]
35
+ if (parseMessages.length > 0) {
36
+ throw new Error(parseMessages.map((error) => error.message).join("; "))
37
+ }
31
38
 
32
- const parseMessages = [...document.errors, ...document.warnings]
33
- if (parseMessages.length > 0) {
34
- throw new Error(parseMessages.map((error) => error.message).join("; "))
35
- }
39
+ let unsupportedFeature: string | null = null
40
+ visit(document, {
41
+ Alias: () => {
42
+ unsupportedFeature = "YAML aliases are not supported"
43
+ },
44
+ Node: (_key, node) => {
45
+ if (node.anchor) {
46
+ unsupportedFeature = "YAML anchors are not supported"
47
+ } else if (node.tag && !node.tag.startsWith("tag:yaml.org,2002:")) {
48
+ unsupportedFeature = "Custom YAML tags are not supported"
49
+ }
50
+ },
51
+ })
36
52
 
37
- let unsupportedFeature: string | null = null
38
- visit(document, {
39
- Alias: () => {
40
- unsupportedFeature = "YAML aliases are not supported"
41
- },
42
- Node: (_key, node) => {
43
- if (node.anchor) {
44
- unsupportedFeature = "YAML anchors are not supported"
45
- } else if (node.tag && !node.tag.startsWith("tag:yaml.org,2002:")) {
46
- unsupportedFeature = "Custom YAML tags are not supported"
47
- }
48
- },
49
- })
53
+ if (unsupportedFeature) {
54
+ throw new Error(unsupportedFeature)
55
+ }
50
56
 
51
- if (unsupportedFeature) {
52
- throw new Error(unsupportedFeature)
53
- }
57
+ const data = document.toJS({ maxAliasCount: 0 })
58
+ if (data === null || typeof data !== "object" || Array.isArray(data)) {
59
+ throw new Error("YAML frontmatter must be a mapping")
60
+ }
54
61
 
55
- const data = document.toJS({ maxAliasCount: 0 })
56
- if (data === null || typeof data !== "object" || Array.isArray(data)) {
57
- throw new Error("YAML frontmatter must be a mapping")
58
- }
62
+ return {
63
+ data,
64
+ body: content.slice(match[0].length),
65
+ }
66
+ } catch (cause) {
67
+ throw new FrontmatterParseError({
68
+ path,
69
+ message:
70
+ cause instanceof Error ? cause.message : "Failed to parse frontmatter",
71
+ cause,
72
+ })
73
+ }
74
+ }
59
75
 
60
- return {
61
- data,
62
- body: content.slice(match[0].length),
63
- }
64
- },
65
- catch: (cause) =>
66
- new FrontmatterParseError({
67
- path,
68
- message:
69
- cause instanceof Error
70
- ? cause.message
71
- : "Failed to parse frontmatter",
72
- cause,
73
- }),
76
+ export const parseFrontmatter = (content: string, path: string) =>
77
+ Effect.try({
78
+ try: () => parseFrontmatterSync(content, path),
79
+ catch: (error) => error as FrontmatterParseError,
74
80
  })
75
81
 
76
82
  export const formatMarkdownDocument = (data: object, body: string) =>