@markjaquith/agency 3.2.1 → 3.2.3

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.
Files changed (54) hide show
  1. package/README.md +30 -60
  2. package/cli-main.ts +38 -72
  3. package/fixtures/protocol/orchestration-recipes.json +9 -34
  4. package/package.json +1 -4
  5. package/schemas/agency-graph-v1.schema.json +2 -31
  6. package/src/cli-parser.test.ts +13 -132
  7. package/src/cli-parser.ts +10 -101
  8. package/src/cli.test.ts +12 -111
  9. package/src/commands/act.ts +2 -6
  10. package/src/commands/push.test.ts +4 -2
  11. package/src/commands/push.ts +2 -0
  12. package/src/commands/sync.ts +3 -3
  13. package/src/commands/validate.ts +3 -1
  14. package/src/commands/work.test.ts +70 -8
  15. package/src/commands/work.ts +15 -8
  16. package/src/graph-schema.ts +0 -2
  17. package/src/protocol.test.ts +24 -25
  18. package/src/protocol.ts +56 -15
  19. package/src/readiness.test.ts +2 -2
  20. package/src/services/ArchiveBulkService.test.ts +0 -88
  21. package/src/services/ArchiveService.ts +0 -32
  22. package/src/services/FileSystemService.ts +2 -0
  23. package/src/services/GraphMutationService.ts +0 -26
  24. package/src/services/GraphService.test.ts +1 -1
  25. package/src/services/IntegrationService.test.ts +4 -4
  26. package/src/services/LifecycleTransaction.ts +5 -1
  27. package/src/services/PhaseService.ts +1 -8
  28. package/src/services/PushService.test.ts +110 -4
  29. package/src/services/PushService.ts +435 -85
  30. package/src/services/ReadinessService.test.ts +0 -34
  31. package/src/services/ReadinessService.ts +1 -20
  32. package/src/services/ReviewService.test.ts +0 -64
  33. package/src/services/ReviewService.ts +0 -5
  34. package/src/services/SyncService.test.ts +75 -88
  35. package/src/services/SyncService.ts +52 -123
  36. package/src/services/TaskPhaseService.test.ts +13 -1
  37. package/src/services/TaskService.ts +1 -7
  38. package/src/services/WorkbaseService.ts +0 -3
  39. package/src/services/WorktreeService.test.ts +192 -1
  40. package/src/services/WorktreeService.ts +169 -34
  41. package/src/test-utils.ts +0 -2
  42. package/src/usage-log.test.ts +11 -1
  43. package/src/usage-log.ts +22 -4
  44. package/src/utils/process.test.ts +10 -0
  45. package/src/utils/process.ts +59 -6
  46. package/src/workbase/AGENTS.md +9 -15
  47. package/src/workbase/agent-command.test.ts +0 -3
  48. package/src/workbase/agent-command.ts +0 -6
  49. package/src/workbase/document-revision.ts +0 -4
  50. package/src/workbase/schemas.test.ts +12 -25
  51. package/src/workbase/schemas.ts +0 -16
  52. package/src/commands/claim.ts +0 -122
  53. package/src/services/ClaimService.test.ts +0 -415
  54. package/src/services/ClaimService.ts +0 -608
@@ -2,7 +2,6 @@ import { describe, expect, test } from "bun:test"
2
2
  import { Schema } from "@effect/schema"
3
3
  import {
4
4
  EntityId,
5
- ClaimRecord,
6
5
  EpicFrontmatter,
7
6
  PhaseFrontmatter,
8
7
  TaskFrontmatter,
@@ -401,30 +400,18 @@ describe("work status", () => {
401
400
  })
402
401
  })
403
402
 
404
- describe("claim records", () => {
405
- const record = {
406
- claimant: "orchestrator",
407
- agent: "agent",
408
- sessionId: "job-1",
409
- startedAt: "2026-07-17T12:00:00.000Z",
410
- targetRevision: "0".repeat(64),
411
- expiresAt: "2026-07-17T13:00:00.000Z",
412
- state: "active" as const,
413
- }
414
-
415
- test("accepts explicit ownership and revision metadata", () => {
416
- expect(Schema.decodeUnknownSync(ClaimRecord)(record)).toEqual(record)
417
- })
418
-
419
- test("rejects malformed timestamps, revisions, and empty identities", () => {
420
- for (const invalid of [
421
- { ...record, claimant: "" },
422
- { ...record, startedAt: "today" },
423
- { ...record, targetRevision: "abc" },
424
- ]) {
425
- expect(() => Schema.decodeUnknownSync(ClaimRecord)(invalid)).toThrow()
426
- }
427
- })
403
+ test("rejects removed claim frontmatter", () => {
404
+ expect(() =>
405
+ Schema.decodeUnknownSync(TaskFrontmatter, { onExcessProperty: "error" })({
406
+ ticketUrl: null,
407
+ repo: "agency",
408
+ branch: "task/example",
409
+ base: "main",
410
+ pr: null,
411
+ status: "working",
412
+ claim: { state: "active" },
413
+ }),
414
+ ).toThrow()
428
415
  })
429
416
 
430
417
  describe("workbase registry", () => {
@@ -50,19 +50,6 @@ export const DocumentRevision = Schema.String.pipe(
50
50
  Schema.pattern(/^[a-f0-9]{64}$/),
51
51
  )
52
52
 
53
- export const ClaimRecord = Schema.Struct({
54
- claimant: NonEmptyString,
55
- agent: NonEmptyString,
56
- sessionId: NonEmptyString,
57
- startedAt: IsoTimestamp,
58
- targetRevision: DocumentRevision,
59
- expiresAt: Schema.optional(IsoTimestamp),
60
- state: Schema.Literal("active", "released", "finished"),
61
- releasedAt: Schema.optional(IsoTimestamp),
62
- finishedAt: Schema.optional(IsoTimestamp),
63
- outcome: Schema.optional(Schema.Literal("done", "dropped")),
64
- })
65
-
66
53
  const Url = NonEmptyString.pipe(Schema.pattern(/^[a-zA-Z][a-zA-Z0-9+.-]*:/))
67
54
 
68
55
  const GitHubPullRequestUrl = NonEmptyString.pipe(
@@ -163,7 +150,6 @@ const ExecutionUnit = {
163
150
  base: NonEmptyString,
164
151
  pr: Schema.NullOr(Schema.Union(GitHubPullRequestUrl, PullRequestRecord)),
165
152
  status: Schema.optionalWith(WorkStatus, { default: () => "open" as const }),
166
- claim: Schema.optional(ClaimRecord),
167
153
  completion: Schema.optional(CompletionRecord),
168
154
  }
169
155
 
@@ -262,7 +248,6 @@ const ReviewTaskFrontmatter = Schema.Struct({
262
248
  ...TaskMetadata,
263
249
  review: ReviewRecord,
264
250
  status: Schema.optionalWith(WorkStatus, { default: () => "open" as const }),
265
- claim: Schema.optional(ClaimRecord),
266
251
  completion: Schema.optional(CompletionRecord),
267
252
  })
268
253
 
@@ -288,7 +273,6 @@ export type RepositoryDeclaration = Schema.Schema.Type<
288
273
  typeof RepositoryDeclaration
289
274
  >
290
275
  export type WorkStatus = Schema.Schema.Type<typeof WorkStatus>
291
- export type ClaimRecord = Schema.Schema.Type<typeof ClaimRecord>
292
276
  export type PullRequestRecord = Schema.Schema.Type<typeof PullRequestRecord>
293
277
  export type ReviewSource = Schema.Schema.Type<typeof ReviewSource>
294
278
  export type ReviewRecord = Schema.Schema.Type<typeof ReviewRecord>
@@ -1,122 +0,0 @@
1
- import { Effect } from "effect"
2
- import { ClaimService } from "../services/ClaimService"
3
- import type { BaseCommandOptions } from "../utils/command"
4
- import { createLoggers } from "../utils/effect"
5
-
6
- interface ClaimCommandOptions extends BaseCommandOptions {
7
- readonly operation: "claim" | "release" | "finish"
8
- readonly taskId?: string
9
- readonly phaseId?: string
10
- readonly claimant?: string
11
- readonly agent?: string
12
- readonly sessionId?: string
13
- readonly revision?: string
14
- readonly expiresAt?: string
15
- readonly outcome?: string
16
- readonly noPullRequest?: boolean
17
- readonly summary?: string
18
- readonly evidenceUrl?: string
19
- readonly json?: boolean
20
- }
21
-
22
- export const claimCommand = (options: ClaimCommandOptions) =>
23
- Effect.gen(function* () {
24
- const claims = yield* ClaimService
25
- const { log } = createLoggers(options)
26
- const cwd = options.cwd ?? process.cwd()
27
- if (!options.taskId || !options.sessionId || !options.revision) {
28
- return yield* Effect.fail(new Error("Missing required claim arguments"))
29
- }
30
- if (
31
- options.operation === "claim" &&
32
- (!options.claimant || !options.agent)
33
- ) {
34
- return yield* Effect.fail(
35
- new Error("Claimant and agent identities are required"),
36
- )
37
- }
38
- if (options.noPullRequest && !options.summary?.trim()) {
39
- return yield* Effect.fail(
40
- new Error("Non-PR completion requires a non-empty summary"),
41
- )
42
- }
43
- if (options.noPullRequest && options.outcome !== "done") {
44
- return yield* Effect.fail(
45
- new Error("Non-PR completion is valid only with a done outcome"),
46
- )
47
- }
48
- if (
49
- options.operation === "finish" &&
50
- options.outcome !== "done" &&
51
- options.outcome !== "dropped"
52
- ) {
53
- return yield* Effect.fail(
54
- new Error("Finish outcome must be done or dropped"),
55
- )
56
- }
57
-
58
- const common = {
59
- taskId: options.taskId,
60
- ...(options.phaseId ? { phaseId: options.phaseId } : {}),
61
- sessionId: options.sessionId,
62
- revision: options.revision,
63
- }
64
- const result =
65
- options.operation === "claim"
66
- ? yield* claims.claim(
67
- {
68
- ...common,
69
- claimant: options.claimant!,
70
- agent: options.agent!,
71
- ...(options.expiresAt ? { expiresAt: options.expiresAt } : {}),
72
- },
73
- cwd,
74
- )
75
- : options.operation === "release"
76
- ? yield* claims.release(common, cwd)
77
- : yield* claims.finish(
78
- {
79
- ...common,
80
- outcome: options.outcome as "done" | "dropped",
81
- ...(options.noPullRequest
82
- ? {
83
- nonPrCompletion: {
84
- summary: options.summary!,
85
- ...(options.evidenceUrl
86
- ? { evidenceUrl: options.evidenceUrl }
87
- : {}),
88
- },
89
- }
90
- : {}),
91
- },
92
- cwd,
93
- )
94
-
95
- const { data: _, ...output } = result
96
- log(
97
- options.json
98
- ? JSON.stringify(output, null, 2)
99
- : `${options.operation === "claim" ? "Claimed" : options.operation === "release" ? "Released" : "Finished"} ${result.target} at revision ${result.revision}`,
100
- )
101
- })
102
-
103
- export const claimHelp = `
104
- Usage: agency claim <task-id> [phase-id] --claimant <id> --agent <id> --session-id <id> --revision <sha256>
105
-
106
- Claim an execution unit. Use distinct claimant and agent identities for delegated
107
- work. --expires-at accepts an optional future ISO-8601 timestamp.
108
- `
109
-
110
- export const releaseHelp = `
111
- Usage: agency release <task-id> [phase-id] --session-id <id> --revision <sha256>
112
-
113
- Release an execution unit owned by the session.
114
- `
115
-
116
- export const finishHelp = `
117
- Usage: agency finish <task-id> [phase-id] --session-id <id> --revision <sha256> --outcome <done|dropped> [--no-pull-request --summary <text> [--evidence-url <url>]]
118
-
119
- Finish a claim owned by the session. A done claim outcome leaves unmerged work
120
- working; agency sync marks the execution unit done after merge. Use
121
- --no-pull-request with a summary for an explicit non-PR completion.
122
- `
@@ -1,415 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
- import { Effect } from "effect"
3
- import { mkdir, readFile, writeFile } from "node:fs/promises"
4
- import { join } from "node:path"
5
- import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
6
- import { ClaimService } from "./ClaimService"
7
- import { PhaseService } from "./PhaseService"
8
- import { TaskService } from "./TaskService"
9
-
10
- const at = (value: string) => new Date(value)
11
-
12
- describe("claim service", () => {
13
- let root: string
14
-
15
- beforeEach(async () => {
16
- root = await createTempDir()
17
- await Bun.write(join(root, "agency.json"), '{"version":2}\n')
18
- await mkdir(join(root, "repos/agency"), { recursive: true })
19
- await runTestEffect(
20
- TaskService.pipe(
21
- Effect.flatMap((service) =>
22
- service.create(
23
- {
24
- id: "single",
25
- ticketUrl: null,
26
- repo: "agency",
27
- branch: "task/single",
28
- base: "main",
29
- },
30
- root,
31
- ),
32
- ),
33
- ),
34
- )
35
- })
36
-
37
- afterEach(async () => cleanupTempDir(root))
38
-
39
- const inspect = (taskId = "single", phaseId?: string) =>
40
- runTestEffect(
41
- ClaimService.pipe(
42
- Effect.flatMap((service) => service.inspect(taskId, phaseId, root)),
43
- ),
44
- )
45
-
46
- const claim = async (
47
- revision: string,
48
- sessionId = "session-1",
49
- now = at("2026-07-17T12:00:00.000Z"),
50
- expiresAt?: string,
51
- ) =>
52
- runTestEffect(
53
- ClaimService.pipe(
54
- Effect.flatMap((service) =>
55
- service.claim(
56
- {
57
- taskId: "single",
58
- claimant: "orchestrator-1",
59
- agent: "agent-1",
60
- sessionId,
61
- revision,
62
- now,
63
- ...(expiresAt ? { expiresAt } : {}),
64
- },
65
- root,
66
- ),
67
- ),
68
- ),
69
- )
70
-
71
- test("records ownership and guarded release and finish transitions", async () => {
72
- const initial = await inspect()
73
- const acquired = await claim(
74
- initial.revision,
75
- "session-1",
76
- at("2026-07-17T12:00:00.000Z"),
77
- "2026-07-17T13:00:00.000Z",
78
- )
79
-
80
- expect(acquired.claim).toEqual({
81
- claimant: "orchestrator-1",
82
- agent: "agent-1",
83
- sessionId: "session-1",
84
- startedAt: "2026-07-17T12:00:00.000Z",
85
- targetRevision: initial.revision,
86
- expiresAt: "2026-07-17T13:00:00.000Z",
87
- state: "active",
88
- })
89
- expect((await inspect()).data.status).toBe("working")
90
- await expect(
91
- runTestEffect(
92
- TaskService.pipe(
93
- Effect.flatMap((service) =>
94
- service.setStatus("single", "done", root),
95
- ),
96
- ),
97
- ),
98
- ).rejects.toThrow("has an active claim")
99
-
100
- const released = await runTestEffect(
101
- ClaimService.pipe(
102
- Effect.flatMap((service) =>
103
- service.release(
104
- {
105
- taskId: "single",
106
- sessionId: "session-1",
107
- revision: acquired.revision,
108
- now: at("2026-07-17T12:15:00.000Z"),
109
- },
110
- root,
111
- ),
112
- ),
113
- ),
114
- )
115
- expect(released.data.status).toBe("open")
116
- expect(released.claim).toMatchObject({
117
- state: "released",
118
- releasedAt: "2026-07-17T12:15:00.000Z",
119
- })
120
-
121
- const reacquired = await claim(
122
- released.revision,
123
- "session-2",
124
- at("2026-07-17T12:20:00.000Z"),
125
- )
126
- const finished = await runTestEffect(
127
- ClaimService.pipe(
128
- Effect.flatMap((service) =>
129
- service.finish(
130
- {
131
- taskId: "single",
132
- sessionId: "session-2",
133
- revision: reacquired.revision,
134
- outcome: "done",
135
- now: at("2026-07-17T12:45:00.000Z"),
136
- },
137
- root,
138
- ),
139
- ),
140
- ),
141
- )
142
- expect(finished.data.status).toBe("working")
143
- expect(finished.claim).toMatchObject({
144
- state: "finished",
145
- finishedAt: "2026-07-17T12:45:00.000Z",
146
- outcome: "done",
147
- })
148
- })
149
-
150
- test("finishes claimed non-PR work with durable completion evidence", async () => {
151
- const initial = await inspect()
152
- const acquired = await claim(initial.revision)
153
- const finished = await runTestEffect(
154
- ClaimService.pipe(
155
- Effect.flatMap((service) =>
156
- service.finish(
157
- {
158
- taskId: "single",
159
- sessionId: "session-1",
160
- revision: acquired.revision,
161
- outcome: "done",
162
- nonPrCompletion: {
163
- summary: "Review completed; no code change was needed.",
164
- evidenceUrl: "https://example.com/review",
165
- },
166
- now: at("2026-07-17T12:45:00.000Z"),
167
- },
168
- root,
169
- ),
170
- ),
171
- ),
172
- )
173
-
174
- expect(finished.data).toMatchObject({
175
- status: "done",
176
- claim: { state: "finished", outcome: "done" },
177
- completion: {
178
- mode: "non-pr",
179
- completedAt: "2026-07-17T12:45:00.000Z",
180
- summary: "Review completed; no code change was needed.",
181
- evidenceUrl: "https://example.com/review",
182
- },
183
- })
184
- })
185
-
186
- test("rejects invalid claimed non-PR completion", async () => {
187
- const initial = await inspect()
188
- const acquired = await claim(initial.revision)
189
- await expect(
190
- runTestEffect(
191
- ClaimService.pipe(
192
- Effect.flatMap((service) =>
193
- service.finish(
194
- {
195
- taskId: "single",
196
- sessionId: "session-1",
197
- revision: acquired.revision,
198
- outcome: "done",
199
- nonPrCompletion: { summary: " " },
200
- },
201
- root,
202
- ),
203
- ),
204
- ),
205
- ),
206
- ).rejects.toThrow("summary must not be empty")
207
- })
208
-
209
- test("returns structured ownership and revision conflicts", async () => {
210
- const initial = await inspect()
211
- const acquired = await claim(initial.revision)
212
-
213
- await expect(claim(acquired.revision, "session-2")).rejects.toThrow(
214
- "is claimed by 'agent-1'",
215
- )
216
- await expect(
217
- runTestEffect(
218
- ClaimService.pipe(
219
- Effect.flatMap((service) =>
220
- service.release(
221
- {
222
- taskId: "single",
223
- sessionId: "session-2",
224
- revision: acquired.revision,
225
- },
226
- root,
227
- ),
228
- ),
229
- ),
230
- ),
231
- ).rejects.toThrow("does not own")
232
- await expect(claim(initial.revision, "session-3")).rejects.toThrow(
233
- "Revision conflict",
234
- )
235
- })
236
-
237
- test("inspects a task without parsing unrelated task documents", async () => {
238
- await mkdir(join(root, "tasks/broken"), { recursive: true })
239
- await Bun.write(join(root, "tasks/broken/TASK.md"), "not frontmatter\n")
240
-
241
- const inspected = await inspect()
242
- expect(inspected.target.path).toBe(join(root, "tasks/single/TASK.md"))
243
- expect(inspected.data).toMatchObject({ branch: "task/single" })
244
- })
245
-
246
- test("inspects a phase without parsing unrelated phase documents", async () => {
247
- await mkdir(join(root, "tasks/phased/phases/target"), { recursive: true })
248
- await mkdir(join(root, "tasks/phased/phases/broken"), { recursive: true })
249
- await Bun.write(
250
- join(root, "tasks/phased/TASK.md"),
251
- "---\nticketUrl: null\nphases:\n - id: target\n - id: broken\nstatus: working\n---\n",
252
- )
253
- await Bun.write(
254
- join(root, "tasks/phased/phases/target/PHASE.md"),
255
- "---\nrepo: agency\nbranch: phase/target\nbase: main\npr: null\nstatus: open\n---\n",
256
- )
257
- await Bun.write(
258
- join(root, "tasks/phased/phases/broken/PHASE.md"),
259
- "not frontmatter\n",
260
- )
261
-
262
- const inspected = await inspect("phased", "target")
263
- expect(inspected.target.path).toBe(
264
- join(root, "tasks/phased/phases/target/PHASE.md"),
265
- )
266
- expect(inspected.data).toMatchObject({ branch: "phase/target" })
267
- })
268
- test("serializes concurrent claims and allows expired ownership replacement", async () => {
269
- const initial = await inspect()
270
- const attempts = await Promise.allSettled([
271
- claim(initial.revision, "session-a"),
272
- claim(initial.revision, "session-b"),
273
- ])
274
- expect(
275
- attempts.filter((result) => result.status === "fulfilled"),
276
- ).toHaveLength(1)
277
- expect(
278
- attempts.filter((result) => result.status === "rejected"),
279
- ).toHaveLength(1)
280
- const current = await inspect()
281
- expect(current.data.claim?.state).toBe("active")
282
-
283
- await runTestEffect(
284
- ClaimService.pipe(
285
- Effect.flatMap((service) =>
286
- service.release(
287
- {
288
- taskId: "single",
289
- sessionId: current.data.claim!.sessionId,
290
- revision: current.revision,
291
- },
292
- root,
293
- ),
294
- ),
295
- ),
296
- )
297
- const released = await inspect()
298
- const expiring = await claim(
299
- released.revision,
300
- "expiring",
301
- at("2026-07-17T12:00:00.000Z"),
302
- "2026-07-17T12:01:00.000Z",
303
- )
304
- const replacement = await claim(
305
- expiring.revision,
306
- "replacement",
307
- at("2026-07-17T12:02:00.000Z"),
308
- )
309
- expect(replacement.claim.sessionId).toBe("replacement")
310
- })
311
-
312
- test("claims phases and rejects multi-phase task containers", async () => {
313
- await runTestEffect(
314
- TaskService.pipe(
315
- Effect.flatMap((service) =>
316
- service.create(
317
- { id: "multi", ticketUrl: null, multiPhase: true },
318
- root,
319
- ),
320
- ),
321
- ),
322
- )
323
- await runTestEffect(
324
- PhaseService.pipe(
325
- Effect.flatMap((service) =>
326
- service.create(
327
- {
328
- taskId: "multi",
329
- id: "implementation",
330
- repo: "agency",
331
- branch: "task/multi",
332
- base: "main",
333
- },
334
- root,
335
- ),
336
- ),
337
- ),
338
- )
339
- await expect(inspect("multi")).rejects.toThrow("claim a phase instead")
340
- const phase = await inspect("multi", "implementation")
341
- const acquired = await runTestEffect(
342
- ClaimService.pipe(
343
- Effect.flatMap((service) =>
344
- service.claim(
345
- {
346
- taskId: "multi",
347
- phaseId: "implementation",
348
- claimant: "orchestrator",
349
- agent: "agent",
350
- sessionId: "phase-session",
351
- revision: phase.revision,
352
- },
353
- root,
354
- ),
355
- ),
356
- ),
357
- )
358
- expect(acquired.target).toBe("phase 'multi/implementation'")
359
- })
360
-
361
- test("inspects only the requested task on large workbases", async () => {
362
- for (let index = 0; index < 100; index += 1) {
363
- const taskRoot = join(root, "tasks", `unrelated-${index}`)
364
- await mkdir(taskRoot, { recursive: true })
365
- await writeFile(join(taskRoot, "TASK.md"), "not valid frontmatter\n")
366
- }
367
-
368
- const inspected = await inspect()
369
- expect(inspected.target.path).toBe(join(root, "tasks/single/TASK.md"))
370
- expect(inspected.data.status).toBe("open")
371
- })
372
-
373
- test("inspects only the requested phase", async () => {
374
- await runTestEffect(
375
- TaskService.pipe(
376
- Effect.flatMap((service) =>
377
- service.create(
378
- { id: "multi", ticketUrl: null, multiPhase: true },
379
- root,
380
- ),
381
- ),
382
- ),
383
- )
384
- await runTestEffect(
385
- PhaseService.pipe(
386
- Effect.flatMap((service) =>
387
- service.create(
388
- {
389
- taskId: "multi",
390
- id: "requested",
391
- repo: "agency",
392
- branch: "task/multi",
393
- base: "main",
394
- },
395
- root,
396
- ),
397
- ),
398
- ),
399
- )
400
- const taskPath = join(root, "tasks/multi/TASK.md")
401
- await writeFile(
402
- taskPath,
403
- (await readFile(taskPath, "utf8")).replace("multi", "["),
404
- )
405
- const unrelatedPhase = join(root, "tasks/multi/phases/unrelated")
406
- await mkdir(unrelatedPhase, { recursive: true })
407
- await writeFile(join(unrelatedPhase, "PHASE.md"), "not valid frontmatter\n")
408
-
409
- const inspected = await inspect("multi", "requested")
410
- expect(inspected.target.path).toBe(
411
- join(root, "tasks/multi/phases/requested/PHASE.md"),
412
- )
413
- expect(inspected.data.status).toBe("open")
414
- })
415
- })