@markjaquith/agency 3.2.7 → 3.2.9
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/package.json +1 -1
- package/src/services/FileSystemService.test.ts +81 -0
- package/src/services/FileSystemService.ts +29 -7
- package/src/services/GraphService.test.ts +11 -0
- package/src/services/GraphService.ts +1 -1
- package/src/services/SyncService.test.ts +129 -0
- package/src/services/SyncService.ts +84 -16
- package/src/services/WorkbaseService.test.ts +15 -0
- package/src/services/WorkbaseService.ts +10 -4
package/package.json
CHANGED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test"
|
|
2
|
+
import { Effect } from "effect"
|
|
3
|
+
import { mkdir } from "node:fs/promises"
|
|
4
|
+
import { join } from "node:path"
|
|
5
|
+
import { cleanupTempDir, createTempDir } from "../test-utils"
|
|
6
|
+
import { FileSystemService } from "./FileSystemService"
|
|
7
|
+
|
|
8
|
+
const runFileSystem = <A, E>(effect: Effect.Effect<A, E, FileSystemService>) =>
|
|
9
|
+
Effect.runPromise(effect.pipe(Effect.provide(FileSystemService.Default)))
|
|
10
|
+
|
|
11
|
+
describe("FileSystemService", () => {
|
|
12
|
+
const roots: string[] = []
|
|
13
|
+
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
await Promise.all(roots.splice(0).map(cleanupTempDir))
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test("distinguishes missing files from other read failures", async () => {
|
|
19
|
+
const root = await createTempDir()
|
|
20
|
+
roots.push(root)
|
|
21
|
+
const directory = join(root, "document")
|
|
22
|
+
await mkdir(directory)
|
|
23
|
+
|
|
24
|
+
const missing = await runFileSystem(
|
|
25
|
+
FileSystemService.pipe(
|
|
26
|
+
Effect.flatMap((service) => service.readFile(join(root, "missing"))),
|
|
27
|
+
Effect.either,
|
|
28
|
+
),
|
|
29
|
+
)
|
|
30
|
+
expect(missing).toMatchObject({
|
|
31
|
+
_tag: "Left",
|
|
32
|
+
left: { _tag: "FileNotFoundError" },
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const unreadable = await runFileSystem(
|
|
36
|
+
FileSystemService.pipe(
|
|
37
|
+
Effect.flatMap((service) => service.readFile(directory)),
|
|
38
|
+
Effect.either,
|
|
39
|
+
),
|
|
40
|
+
)
|
|
41
|
+
expect(unreadable).toMatchObject({
|
|
42
|
+
_tag: "Left",
|
|
43
|
+
left: {
|
|
44
|
+
_tag: "FileSystemError",
|
|
45
|
+
message: `Failed to read file: ${directory}`,
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test("only treats a missing symlink path as absent", async () => {
|
|
51
|
+
const root = await createTempDir()
|
|
52
|
+
roots.push(root)
|
|
53
|
+
const regularFile = join(root, "regular")
|
|
54
|
+
await Bun.write(regularFile, "content")
|
|
55
|
+
|
|
56
|
+
await expect(
|
|
57
|
+
runFileSystem(
|
|
58
|
+
FileSystemService.pipe(
|
|
59
|
+
Effect.flatMap((service) =>
|
|
60
|
+
service.readSymlinkTarget(join(root, "missing")),
|
|
61
|
+
),
|
|
62
|
+
),
|
|
63
|
+
),
|
|
64
|
+
).resolves.toBeNull()
|
|
65
|
+
const unreadable = await runFileSystem(
|
|
66
|
+
FileSystemService.pipe(
|
|
67
|
+
Effect.flatMap((service) =>
|
|
68
|
+
service.readSymlinkTarget(join(regularFile, "child")),
|
|
69
|
+
),
|
|
70
|
+
Effect.either,
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
expect(unreadable).toMatchObject({
|
|
74
|
+
_tag: "Left",
|
|
75
|
+
left: {
|
|
76
|
+
_tag: "FileSystemError",
|
|
77
|
+
message: `Failed to read symlink target: ${join(regularFile, "child")}`,
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
})
|
|
@@ -23,6 +23,12 @@ class FileNotFoundError extends Data.TaggedError("FileNotFoundError")<{
|
|
|
23
23
|
path: string
|
|
24
24
|
}> {}
|
|
25
25
|
|
|
26
|
+
const isFileNotFound = (error: unknown) =>
|
|
27
|
+
typeof error === "object" &&
|
|
28
|
+
error !== null &&
|
|
29
|
+
"code" in error &&
|
|
30
|
+
error.code === "ENOENT"
|
|
31
|
+
|
|
26
32
|
// FileSystem Service using Effect.Service pattern
|
|
27
33
|
export class FileSystemService extends Effect.Service<FileSystemService>()(
|
|
28
34
|
"FileSystemService",
|
|
@@ -88,7 +94,13 @@ export class FileSystemService extends Effect.Service<FileSystemService>()(
|
|
|
88
94
|
readFile: (path: string) =>
|
|
89
95
|
Effect.tryPromise({
|
|
90
96
|
try: () => Bun.file(path).text(),
|
|
91
|
-
catch: () =>
|
|
97
|
+
catch: (error) =>
|
|
98
|
+
isFileNotFound(error)
|
|
99
|
+
? new FileNotFoundError({ path })
|
|
100
|
+
: new FileSystemError({
|
|
101
|
+
message: `Failed to read file: ${path}`,
|
|
102
|
+
cause: error,
|
|
103
|
+
}),
|
|
92
104
|
}),
|
|
93
105
|
|
|
94
106
|
inspectFile: (path: string) =>
|
|
@@ -184,10 +196,12 @@ export class FileSystemService extends Effect.Service<FileSystemService>()(
|
|
|
184
196
|
}))
|
|
185
197
|
},
|
|
186
198
|
catch: (error) =>
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
199
|
+
isFileNotFound(error)
|
|
200
|
+
? new FileNotFoundError({ path })
|
|
201
|
+
: new FileSystemError({
|
|
202
|
+
message: `Failed to read directory: ${path}`,
|
|
203
|
+
cause: error,
|
|
204
|
+
}),
|
|
191
205
|
}),
|
|
192
206
|
|
|
193
207
|
deleteDirectory: (path: string) =>
|
|
@@ -301,8 +315,16 @@ export class FileSystemService extends Effect.Service<FileSystemService>()(
|
|
|
301
315
|
}
|
|
302
316
|
return await readlink(path)
|
|
303
317
|
},
|
|
304
|
-
catch: () =>
|
|
305
|
-
|
|
318
|
+
catch: (error) =>
|
|
319
|
+
isFileNotFound(error)
|
|
320
|
+
? new FileNotFoundError({ path })
|
|
321
|
+
: new FileSystemError({
|
|
322
|
+
message: `Failed to read symlink target: ${path}`,
|
|
323
|
+
cause: error,
|
|
324
|
+
}),
|
|
325
|
+
}).pipe(
|
|
326
|
+
Effect.catchTag("FileNotFoundError", () => Effect.succeed(null)),
|
|
327
|
+
),
|
|
306
328
|
}),
|
|
307
329
|
},
|
|
308
330
|
) {}
|
|
@@ -230,6 +230,17 @@ describe("GraphService", () => {
|
|
|
230
230
|
expect(calls).toBe(0)
|
|
231
231
|
})
|
|
232
232
|
|
|
233
|
+
test("propagates directory discovery failures", async () => {
|
|
234
|
+
const root = await createTempDir()
|
|
235
|
+
roots.push(root)
|
|
236
|
+
await write(root, "agency.json", '{"version":2}\n')
|
|
237
|
+
await write(root, "tasks", "not a directory")
|
|
238
|
+
|
|
239
|
+
await expect(getGraph(root)).rejects.toThrow(
|
|
240
|
+
`Failed to read directory: ${join(root, "tasks")}`,
|
|
241
|
+
)
|
|
242
|
+
})
|
|
243
|
+
|
|
233
244
|
test("never reports active or terminal execution units as ready", async () => {
|
|
234
245
|
const root = await createWorkbase()
|
|
235
246
|
roots.push(root)
|
|
@@ -158,7 +158,7 @@ export class GraphService extends Effect.Service<GraphService>()(
|
|
|
158
158
|
.map((entry) => entry.name)
|
|
159
159
|
.sort(),
|
|
160
160
|
),
|
|
161
|
-
Effect.
|
|
161
|
+
Effect.catchTag("FileNotFoundError", () => Effect.succeed([])),
|
|
162
162
|
)
|
|
163
163
|
|
|
164
164
|
const readDocument = <S extends Schema.Schema.AnyNoContext>(
|
|
@@ -4,6 +4,7 @@ import { chmod, mkdir, rm } from "node:fs/promises"
|
|
|
4
4
|
import { join } from "node:path"
|
|
5
5
|
import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
|
|
6
6
|
import { PullRequestService } from "./PullRequestService"
|
|
7
|
+
import { ReviewService } from "./ReviewService"
|
|
7
8
|
import { SyncService } from "./SyncService"
|
|
8
9
|
import { TaskService } from "./TaskService"
|
|
9
10
|
import { WorktreeService } from "./WorktreeService"
|
|
@@ -1123,6 +1124,134 @@ exit 9
|
|
|
1123
1124
|
).toEqual([])
|
|
1124
1125
|
})
|
|
1125
1126
|
|
|
1127
|
+
test("inspects workspace dirtiness concurrently", async () => {
|
|
1128
|
+
for (const id of ["first", "second", "third"]) {
|
|
1129
|
+
await runTestEffect(
|
|
1130
|
+
TaskService.pipe(
|
|
1131
|
+
Effect.flatMap((service) =>
|
|
1132
|
+
service.create(
|
|
1133
|
+
{
|
|
1134
|
+
id,
|
|
1135
|
+
ticketUrl: null,
|
|
1136
|
+
repo: "agency",
|
|
1137
|
+
branch: `feat/${id}`,
|
|
1138
|
+
base: "main",
|
|
1139
|
+
},
|
|
1140
|
+
root,
|
|
1141
|
+
),
|
|
1142
|
+
),
|
|
1143
|
+
),
|
|
1144
|
+
)
|
|
1145
|
+
await runTestEffect(
|
|
1146
|
+
WorktreeService.pipe(
|
|
1147
|
+
Effect.flatMap((service) => service.materialize(id, undefined, root)),
|
|
1148
|
+
),
|
|
1149
|
+
)
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
const barrier = join(root, "status-barrier")
|
|
1153
|
+
await mkdir(barrier)
|
|
1154
|
+
const realGit = Bun.which("git")!
|
|
1155
|
+
await Bun.write(
|
|
1156
|
+
join(root, "bin", "git"),
|
|
1157
|
+
`#!/bin/sh
|
|
1158
|
+
case "$*" in
|
|
1159
|
+
*" status --porcelain"*)
|
|
1160
|
+
workspace=""
|
|
1161
|
+
previous=""
|
|
1162
|
+
for argument in "$@"; do
|
|
1163
|
+
if [ "$previous" = "-C" ]; then workspace="$argument"; fi
|
|
1164
|
+
previous="$argument"
|
|
1165
|
+
done
|
|
1166
|
+
id="$(basename "$(dirname "$(dirname "$workspace")")")"
|
|
1167
|
+
touch ${JSON.stringify(barrier)}/"$id"
|
|
1168
|
+
attempt=0
|
|
1169
|
+
while [ "$attempt" -lt 200 ]; do
|
|
1170
|
+
set -- ${JSON.stringify(barrier)}/*
|
|
1171
|
+
if [ -e "$1" ] && [ "$#" -ge 3 ]; then exec ${JSON.stringify(realGit)} -C "$workspace" status --porcelain; fi
|
|
1172
|
+
attempt=$((attempt + 1))
|
|
1173
|
+
sleep 0.01
|
|
1174
|
+
done
|
|
1175
|
+
echo "workspace inspections were serialized" >&2
|
|
1176
|
+
exit 9
|
|
1177
|
+
;;
|
|
1178
|
+
esac
|
|
1179
|
+
exec ${JSON.stringify(realGit)} "$@"
|
|
1180
|
+
`,
|
|
1181
|
+
)
|
|
1182
|
+
await chmod(join(root, "bin", "git"), 0o755)
|
|
1183
|
+
|
|
1184
|
+
const result = await runTestEffect(
|
|
1185
|
+
SyncService.pipe(
|
|
1186
|
+
Effect.flatMap((service) => service.reconcile({ cwd: root })),
|
|
1187
|
+
),
|
|
1188
|
+
)
|
|
1189
|
+
expect(
|
|
1190
|
+
result.warnings.filter(
|
|
1191
|
+
(warning) => warning.kind === "status-inspection-failed",
|
|
1192
|
+
),
|
|
1193
|
+
).toEqual([])
|
|
1194
|
+
})
|
|
1195
|
+
|
|
1196
|
+
test("queries review sources concurrently", async () => {
|
|
1197
|
+
const source = join(root, "source")
|
|
1198
|
+
for (const id of ["first", "second", "third"]) {
|
|
1199
|
+
const ref = `review-${id}`
|
|
1200
|
+
await git(["branch", ref, "main"], source)
|
|
1201
|
+
const review = await runTestEffect(
|
|
1202
|
+
ReviewService.pipe(
|
|
1203
|
+
Effect.flatMap((service) => service.resolve("agency", { ref }, root)),
|
|
1204
|
+
),
|
|
1205
|
+
)
|
|
1206
|
+
await runTestEffect(
|
|
1207
|
+
TaskService.pipe(
|
|
1208
|
+
Effect.flatMap((service) =>
|
|
1209
|
+
service.create({ id, ticketUrl: null, review }, root),
|
|
1210
|
+
),
|
|
1211
|
+
),
|
|
1212
|
+
)
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
const barrier = join(root, "review-query-barrier")
|
|
1216
|
+
await mkdir(barrier)
|
|
1217
|
+
const realGit = Bun.which("git")!
|
|
1218
|
+
await Bun.write(
|
|
1219
|
+
join(root, "bin", "git"),
|
|
1220
|
+
`#!/bin/sh
|
|
1221
|
+
case "$*" in
|
|
1222
|
+
*"ls-remote"*)
|
|
1223
|
+
ref=""
|
|
1224
|
+
for argument in "$@"; do ref="$argument"; done
|
|
1225
|
+
id="\${ref##*-}"
|
|
1226
|
+
touch ${JSON.stringify(barrier)}/"$id"
|
|
1227
|
+
attempt=0
|
|
1228
|
+
while [ "$attempt" -lt 200 ]; do
|
|
1229
|
+
set -- ${JSON.stringify(barrier)}/*
|
|
1230
|
+
if [ -e "$1" ] && [ "$#" -ge 3 ]; then printf '%s\t%s\n' '0123456789012345678901234567890123456789' "$ref"; exit 0; fi
|
|
1231
|
+
attempt=$((attempt + 1))
|
|
1232
|
+
sleep 0.01
|
|
1233
|
+
done
|
|
1234
|
+
echo "review source queries were serialized" >&2
|
|
1235
|
+
exit 9
|
|
1236
|
+
;;
|
|
1237
|
+
esac
|
|
1238
|
+
exec ${JSON.stringify(realGit)} "$@"
|
|
1239
|
+
`,
|
|
1240
|
+
)
|
|
1241
|
+
await chmod(join(root, "bin", "git"), 0o755)
|
|
1242
|
+
|
|
1243
|
+
const result = await runTestEffect(
|
|
1244
|
+
SyncService.pipe(
|
|
1245
|
+
Effect.flatMap((service) => service.reconcile({ cwd: root })),
|
|
1246
|
+
),
|
|
1247
|
+
)
|
|
1248
|
+
expect(
|
|
1249
|
+
result.warnings.filter(
|
|
1250
|
+
(warning) => warning.kind === "review-source-unavailable",
|
|
1251
|
+
),
|
|
1252
|
+
).toEqual([])
|
|
1253
|
+
})
|
|
1254
|
+
|
|
1126
1255
|
test("keeps pull request query failures concise", async () => {
|
|
1127
1256
|
await runTestEffect(
|
|
1128
1257
|
TaskService.pipe(
|
|
@@ -493,6 +493,34 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
493
493
|
{ concurrency: 8 },
|
|
494
494
|
),
|
|
495
495
|
)
|
|
496
|
+
const reviewSourceQueries = new Map(
|
|
497
|
+
yield* Effect.forEach(
|
|
498
|
+
reviewRecords,
|
|
499
|
+
(task) =>
|
|
500
|
+
Effect.gen(function* () {
|
|
501
|
+
if (!("review" in task.data)) return [task.id, null] as const
|
|
502
|
+
const repositoryPath = join(
|
|
503
|
+
root,
|
|
504
|
+
"repos",
|
|
505
|
+
task.data.review.repo,
|
|
506
|
+
)
|
|
507
|
+
const remote = yield* backend.remoteUrl(
|
|
508
|
+
repositoryPath,
|
|
509
|
+
"origin",
|
|
510
|
+
)
|
|
511
|
+
const source = yield* runExternal([
|
|
512
|
+
"git",
|
|
513
|
+
"ls-remote",
|
|
514
|
+
remote ?? "origin",
|
|
515
|
+
task.data.review.source.kind === "pull-request"
|
|
516
|
+
? task.data.review.source.fetchRef
|
|
517
|
+
: originRef(task.data.review.source.ref),
|
|
518
|
+
])
|
|
519
|
+
return [task.id, source] as const
|
|
520
|
+
}),
|
|
521
|
+
{ concurrency: 8 },
|
|
522
|
+
),
|
|
523
|
+
)
|
|
496
524
|
const executionTotal = records.length + reviewRecords.length
|
|
497
525
|
let reconciledExecutions = 0
|
|
498
526
|
const reportExecution = (target: string) => {
|
|
@@ -504,6 +532,55 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
504
532
|
target,
|
|
505
533
|
})
|
|
506
534
|
}
|
|
535
|
+
const checkoutRecords = records.filter((record) => {
|
|
536
|
+
if (record.data.completion) return false
|
|
537
|
+
const merged = config.delivery
|
|
538
|
+
? null
|
|
539
|
+
: mergedPullRequestFromGitHub(
|
|
540
|
+
record.data,
|
|
541
|
+
prQueries.get(record.key),
|
|
542
|
+
)
|
|
543
|
+
return merged === null
|
|
544
|
+
})
|
|
545
|
+
const checkoutCandidates = checkoutRecords.flatMap((record) => {
|
|
546
|
+
const codePath = join(dirname(record.path), "code")
|
|
547
|
+
return [
|
|
548
|
+
{ repo: record.data.repo, path: join(codePath, record.data.repo) },
|
|
549
|
+
...(record.data.repos ?? []).map((reference) => ({
|
|
550
|
+
repo: reference.repo,
|
|
551
|
+
path: join(codePath, reference.repo),
|
|
552
|
+
})),
|
|
553
|
+
]
|
|
554
|
+
})
|
|
555
|
+
const checkoutRepositoryPaths = [
|
|
556
|
+
...new Set(
|
|
557
|
+
checkoutCandidates.map(({ repo }) => join(root, "repos", repo)),
|
|
558
|
+
),
|
|
559
|
+
]
|
|
560
|
+
yield* Effect.forEach(checkoutRepositoryPaths, listRegistered, {
|
|
561
|
+
concurrency: 8,
|
|
562
|
+
})
|
|
563
|
+
const dirtyByCheckoutPath = new Map<string, boolean | null>()
|
|
564
|
+
yield* Effect.forEach(
|
|
565
|
+
checkoutCandidates,
|
|
566
|
+
({ repo, path }) =>
|
|
567
|
+
Effect.gen(function* () {
|
|
568
|
+
if (!(yield* fs.isDirectory(path))) return
|
|
569
|
+
const repositoryPath = join(root, "repos", repo)
|
|
570
|
+
const registered = registeredByRepository.get(repositoryPath)
|
|
571
|
+
if (!registered) return
|
|
572
|
+
const expectedPath = yield* fs.realPath(path)
|
|
573
|
+
const atPath = registered.find(
|
|
574
|
+
(item) => item.path === expectedPath,
|
|
575
|
+
)
|
|
576
|
+
if (!atPath) return
|
|
577
|
+
dirtyByCheckoutPath.set(
|
|
578
|
+
path,
|
|
579
|
+
atPath.dirty ?? (yield* backend.workspaceDirty(path)),
|
|
580
|
+
)
|
|
581
|
+
}),
|
|
582
|
+
{ concurrency: 8 },
|
|
583
|
+
)
|
|
507
584
|
|
|
508
585
|
for (const record of records.sort((a, b) =>
|
|
509
586
|
a.key.localeCompare(b.key),
|
|
@@ -666,8 +743,11 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
666
743
|
|
|
667
744
|
const dirty =
|
|
668
745
|
exists && atPath
|
|
669
|
-
?
|
|
670
|
-
|
|
746
|
+
? atPath.dirty !== undefined
|
|
747
|
+
? atPath.dirty
|
|
748
|
+
: dirtyByCheckoutPath.has(checkoutPath)
|
|
749
|
+
? dirtyByCheckoutPath.get(checkoutPath)!
|
|
750
|
+
: yield* backend.workspaceDirty(checkoutPath)
|
|
671
751
|
: null
|
|
672
752
|
if (exists && atPath && dirty === null) {
|
|
673
753
|
warnings.push({
|
|
@@ -1021,20 +1101,8 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
1021
1101
|
status: apply ? "applied" : "planned",
|
|
1022
1102
|
})
|
|
1023
1103
|
}
|
|
1024
|
-
const
|
|
1025
|
-
const
|
|
1026
|
-
repositoryPath,
|
|
1027
|
-
"origin",
|
|
1028
|
-
)
|
|
1029
|
-
const source = yield* runExternal([
|
|
1030
|
-
"git",
|
|
1031
|
-
"ls-remote",
|
|
1032
|
-
reviewRemote ?? "origin",
|
|
1033
|
-
data.review.source.kind === "pull-request"
|
|
1034
|
-
? data.review.source.fetchRef
|
|
1035
|
-
: originRef(data.review.source.ref),
|
|
1036
|
-
])
|
|
1037
|
-
const sourceCommit = source.stdout.trim().split(/\s+/)[0] || null
|
|
1104
|
+
const source = reviewSourceQueries.get(task.id)
|
|
1105
|
+
const sourceCommit = source?.stdout.trim().split(/\s+/)[0] || null
|
|
1038
1106
|
if (!sourceCommit) {
|
|
1039
1107
|
warnings.push({
|
|
1040
1108
|
kind: "review-source-unavailable",
|
|
@@ -164,6 +164,21 @@ pr: null
|
|
|
164
164
|
expect(exists).not.toContain(join(root, "tasks/example/TASK.md"))
|
|
165
165
|
})
|
|
166
166
|
|
|
167
|
+
test("propagates document read failures instead of reporting them as missing", async () => {
|
|
168
|
+
await write(root, "agency.json", '{"version":2}\n')
|
|
169
|
+
await mkdir(join(root, "tasks/example/TASK.md"), { recursive: true })
|
|
170
|
+
|
|
171
|
+
await expect(
|
|
172
|
+
runTestEffect(
|
|
173
|
+
WorkbaseService.pipe(
|
|
174
|
+
Effect.flatMap((service) => service.validate(root)),
|
|
175
|
+
),
|
|
176
|
+
),
|
|
177
|
+
).rejects.toThrow(
|
|
178
|
+
`Failed to read file: ${join(root, "tasks/example/TASK.md")}`,
|
|
179
|
+
)
|
|
180
|
+
})
|
|
181
|
+
|
|
167
182
|
test("validates non-PR completion invariants without rejecting legacy done work", async () => {
|
|
168
183
|
await write(
|
|
169
184
|
root,
|
|
@@ -694,7 +694,7 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
694
694
|
.map((entry) => entry.name)
|
|
695
695
|
.sort(),
|
|
696
696
|
),
|
|
697
|
-
Effect.
|
|
697
|
+
Effect.catchTag("FileNotFoundError", () => Effect.succeed([])),
|
|
698
698
|
)
|
|
699
699
|
|
|
700
700
|
const aliases = new Set(Object.keys(config.repositories ?? {}))
|
|
@@ -710,12 +710,18 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
710
710
|
schema: S,
|
|
711
711
|
) =>
|
|
712
712
|
Effect.gen(function* () {
|
|
713
|
-
const content = yield*
|
|
714
|
-
|
|
713
|
+
const content = yield* fs
|
|
714
|
+
.readFile(path)
|
|
715
|
+
.pipe(
|
|
716
|
+
Effect.catchTag("FileNotFoundError", () =>
|
|
717
|
+
Effect.succeed(null),
|
|
718
|
+
),
|
|
719
|
+
)
|
|
720
|
+
if (content === null) {
|
|
715
721
|
issue(path, "Required document is missing")
|
|
716
722
|
return null
|
|
717
723
|
}
|
|
718
|
-
const documentContent = content
|
|
724
|
+
const documentContent = content
|
|
719
725
|
const parsed = yield* Effect.either(
|
|
720
726
|
parseFrontmatter(documentContent, path),
|
|
721
727
|
)
|