@markjaquith/agency 3.1.1 → 3.2.1
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 +6 -4
- package/package.json +1 -1
- package/src/cli-parser.test.ts +5 -3
- package/src/cli-parser.ts +3 -4
- package/src/cli.test.ts +9 -1
- package/src/commands/context.test.ts +56 -0
- package/src/commands/work.test.ts +43 -0
- package/src/commands/work.ts +5 -4
- package/src/services/ContextService.ts +157 -200
package/README.md
CHANGED
|
@@ -1028,7 +1028,7 @@ until restored.
|
|
|
1028
1028
|
|
|
1029
1029
|
```text
|
|
1030
1030
|
agency work [<directory> | --epic <epic-id>] [--agent <name>] [--auto] [--print-command]
|
|
1031
|
-
agency work prepare [target] [--evidence <json-or-path>] [--dry-run] [--json]
|
|
1031
|
+
agency work prepare [target] [--evidence <json-or-path>] [--force] [--dry-run] [--json]
|
|
1032
1032
|
agency worktree <list|inspect|prepare|remove|rebuild|repair>
|
|
1033
1033
|
agency push [--json]
|
|
1034
1034
|
agency pr create <task-id> [phase-id] [--draft] [--title <title>] [--head <branch>] [--base <branch>] [--label <label>] [--force] [--json]
|
|
@@ -1062,9 +1062,11 @@ work and reading context. It does not prescribe how callers present or execute
|
|
|
1062
1062
|
prepared work.
|
|
1063
1063
|
The evidence argument may be an evidence object, task-creation JSON, or a path to
|
|
1064
1064
|
either. Use `--dry-run` to report planned fetch, branch, and worktree changes
|
|
1065
|
-
without applying them.
|
|
1066
|
-
|
|
1067
|
-
|
|
1065
|
+
without applying them. Use `--force` to prepare work blocked by readiness, such
|
|
1066
|
+
as a follow-on phase whose dependency is still active; preparation still does
|
|
1067
|
+
not launch an agent or change lifecycle status. Validation reuse never skips
|
|
1068
|
+
readiness, active-claim, repository, ownership, reference-drift, dirty-workspace,
|
|
1069
|
+
or worktree safety checks.
|
|
1068
1070
|
|
|
1069
1071
|
The authoritative implementation locations for this contract are
|
|
1070
1072
|
`src/commands/task.ts` (creation output),
|
package/package.json
CHANGED
package/src/cli-parser.test.ts
CHANGED
|
@@ -397,9 +397,11 @@ describe("strict CLI parsing", () => {
|
|
|
397
397
|
commandName: "work",
|
|
398
398
|
values: { force: true },
|
|
399
399
|
})
|
|
400
|
-
expect(
|
|
401
|
-
|
|
402
|
-
|
|
400
|
+
expect(parseCli(["work", "prepare", "example", "--force"])).toMatchObject({
|
|
401
|
+
commandName: "work",
|
|
402
|
+
args: ["prepare", "example"],
|
|
403
|
+
values: { force: true },
|
|
404
|
+
})
|
|
403
405
|
})
|
|
404
406
|
|
|
405
407
|
test("preserves every argument after pr without parsing it", () => {
|
package/src/cli-parser.ts
CHANGED
|
@@ -917,7 +917,7 @@ const commands = {
|
|
|
917
917
|
},
|
|
918
918
|
work: {
|
|
919
919
|
usage:
|
|
920
|
-
"agency work [<directory-or-task-id> | --epic <epic-id>] [--agent <name>] [--auto] | agency work prepare [target] [--dry-run] [--json]",
|
|
920
|
+
"agency work [<directory-or-task-id> | --epic <epic-id>] [--agent <name>] [--auto] | agency work prepare [target] [--force] [--dry-run] [--json]",
|
|
921
921
|
options: {
|
|
922
922
|
...commonOptions,
|
|
923
923
|
...entitySelectorOptions,
|
|
@@ -933,7 +933,7 @@ const commands = {
|
|
|
933
933
|
},
|
|
934
934
|
command: {
|
|
935
935
|
usage:
|
|
936
|
-
"agency work [<directory-or-task-id> | --epic <epic-id>] [--agent <name>] [--auto] | agency work prepare [target] [--dry-run] [--json]",
|
|
936
|
+
"agency work [<directory-or-task-id> | --epic <epic-id>] [--agent <name>] [--auto] | agency work prepare [target] [--force] [--dry-run] [--json]",
|
|
937
937
|
minArgs: 0,
|
|
938
938
|
maxArgs: 2,
|
|
939
939
|
options: [
|
|
@@ -1691,8 +1691,7 @@ export function parseCli(args: readonly string[]): ParsedCli {
|
|
|
1691
1691
|
parsed.values.claude ||
|
|
1692
1692
|
parsed.values.agent ||
|
|
1693
1693
|
parsed.values.auto ||
|
|
1694
|
-
parsed.values["print-command"]
|
|
1695
|
-
parsed.values.force))
|
|
1694
|
+
parsed.values["print-command"]))
|
|
1696
1695
|
) {
|
|
1697
1696
|
throw usageError(
|
|
1698
1697
|
preparing
|
package/src/cli.test.ts
CHANGED
|
@@ -1310,7 +1310,12 @@ status: open
|
|
|
1310
1310
|
...process.env,
|
|
1311
1311
|
...contract.environment,
|
|
1312
1312
|
XDG_CONFIG_HOME: isolatedConfigHome,
|
|
1313
|
+
OPENCODE_DISABLE_AUTOUPDATE: "1",
|
|
1313
1314
|
OPENCODE_DISABLE_EXTERNAL_SKILLS: "1",
|
|
1315
|
+
OPENCODE_DISABLE_MODELS_FETCH: "1",
|
|
1316
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
1317
|
+
model: "opencode/big-pickle",
|
|
1318
|
+
}),
|
|
1314
1319
|
}
|
|
1315
1320
|
const probe = Bun.spawnSync(["opencode", "debug", "agent", "build"], {
|
|
1316
1321
|
cwd: contract.cwd,
|
|
@@ -1409,7 +1414,9 @@ status: open
|
|
|
1409
1414
|
...process.env,
|
|
1410
1415
|
PATH: `${agencyBin}:${process.env.PATH ?? ""}`,
|
|
1411
1416
|
XDG_CONFIG_HOME: isolatedConfigHome,
|
|
1417
|
+
OPENCODE_DISABLE_AUTOUPDATE: "1",
|
|
1412
1418
|
OPENCODE_DISABLE_EXTERNAL_SKILLS: "1",
|
|
1419
|
+
OPENCODE_DISABLE_MODELS_FETCH: "1",
|
|
1413
1420
|
}
|
|
1414
1421
|
delete directEnvironment.OPENCODE_CONFIG
|
|
1415
1422
|
delete directEnvironment.OPENCODE_CONFIG_CONTENT
|
|
@@ -1480,6 +1487,7 @@ status: open
|
|
|
1480
1487
|
env: {
|
|
1481
1488
|
...directEnvironment,
|
|
1482
1489
|
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
1490
|
+
model: "opencode/big-pickle",
|
|
1483
1491
|
permission: { external_directory: { "*": "deny" } },
|
|
1484
1492
|
}),
|
|
1485
1493
|
},
|
|
@@ -1578,7 +1586,7 @@ status: open
|
|
|
1578
1586
|
graph.nodes.find((node: any) => node.id === "task:pipeline").status,
|
|
1579
1587
|
).toBe("working")
|
|
1580
1588
|
},
|
|
1581
|
-
|
|
1589
|
+
360_000,
|
|
1582
1590
|
)
|
|
1583
1591
|
|
|
1584
1592
|
test("envelopes help and version output in machine mode", async () => {
|
|
@@ -282,6 +282,20 @@ status: dropped
|
|
|
282
282
|
})
|
|
283
283
|
|
|
284
284
|
test("reads each workbase document at most once per invocation", async () => {
|
|
285
|
+
const unrelatedArchive = join(root, "archive/tasks/unrelated/TASK.md")
|
|
286
|
+
await write(
|
|
287
|
+
root,
|
|
288
|
+
"archive/tasks/unrelated/TASK.md",
|
|
289
|
+
`---
|
|
290
|
+
ticketUrl: null
|
|
291
|
+
repo: agency
|
|
292
|
+
branch: unrelated
|
|
293
|
+
base: main
|
|
294
|
+
pr: null
|
|
295
|
+
status: done
|
|
296
|
+
---
|
|
297
|
+
`,
|
|
298
|
+
)
|
|
285
299
|
const reads = new Map<string, number>()
|
|
286
300
|
const originalFile = Bun.file
|
|
287
301
|
Bun.file = mock((path: string) => {
|
|
@@ -304,6 +318,48 @@ status: dropped
|
|
|
304
318
|
)
|
|
305
319
|
expect(documents.length).toBeGreaterThan(0)
|
|
306
320
|
expect(documents.every(([, count]) => count === 1)).toBe(true)
|
|
321
|
+
expect(reads.has(unrelatedArchive)).toBe(false)
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
test("limits repository inspection and overlaps independent Git observations", async () => {
|
|
325
|
+
const commands: string[][] = []
|
|
326
|
+
let activeGitCommands = 0
|
|
327
|
+
let maxActiveGitCommands = 0
|
|
328
|
+
const originalSpawn = Bun.spawn
|
|
329
|
+
;(Bun as { spawn: unknown }).spawn = mock(((
|
|
330
|
+
args: string[],
|
|
331
|
+
options: Parameters<typeof Bun.spawn>[1],
|
|
332
|
+
) => {
|
|
333
|
+
const command = [...args]
|
|
334
|
+
const child = originalSpawn(command, options)
|
|
335
|
+
if (command[0] !== "git") return child
|
|
336
|
+
commands.push(command)
|
|
337
|
+
activeGitCommands += 1
|
|
338
|
+
maxActiveGitCommands = Math.max(maxActiveGitCommands, activeGitCommands)
|
|
339
|
+
const exited = child.exited.finally(() => {
|
|
340
|
+
activeGitCommands -= 1
|
|
341
|
+
})
|
|
342
|
+
return new Proxy(child, {
|
|
343
|
+
get(target, property) {
|
|
344
|
+
return property === "exited"
|
|
345
|
+
? exited
|
|
346
|
+
: Reflect.get(target, property, target)
|
|
347
|
+
},
|
|
348
|
+
})
|
|
349
|
+
}) as unknown as typeof Bun.spawn)
|
|
350
|
+
try {
|
|
351
|
+
await readContext(root, "tasks/agent-contract/phases/context-command")
|
|
352
|
+
} finally {
|
|
353
|
+
;(Bun as { spawn: unknown }).spawn = originalSpawn
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
expect(
|
|
357
|
+
commands.some(
|
|
358
|
+
(command) =>
|
|
359
|
+
command.includes("config") && command.includes("--show-origin"),
|
|
360
|
+
),
|
|
361
|
+
).toBe(false)
|
|
362
|
+
expect(maxActiveGitCommands).toBeGreaterThanOrEqual(3)
|
|
307
363
|
})
|
|
308
364
|
|
|
309
365
|
test("resolves a bare task ID and returns root discovery context", async () => {
|
|
@@ -569,6 +569,49 @@ describe("work command", () => {
|
|
|
569
569
|
).rejects.toThrow("Recalled repository conflicts")
|
|
570
570
|
})
|
|
571
571
|
|
|
572
|
+
test("force prepares a phase blocked by an active dependency without launching or changing status", async () => {
|
|
573
|
+
const blocked = createHarness({
|
|
574
|
+
workspace: multiPhaseWorkspace,
|
|
575
|
+
multiPhaseTasks: ["example"],
|
|
576
|
+
guardError: new Error("Phase dependency is working"),
|
|
577
|
+
})
|
|
578
|
+
await expect(
|
|
579
|
+
blocked.runPrepare({
|
|
580
|
+
cwd: "/workbase",
|
|
581
|
+
taskId: "example",
|
|
582
|
+
phaseId: "implementation",
|
|
583
|
+
}),
|
|
584
|
+
).rejects.toThrow("Phase dependency is working")
|
|
585
|
+
expect(blocked.events).toEqual(["guard"])
|
|
586
|
+
|
|
587
|
+
const forced = createHarness({
|
|
588
|
+
workspace: multiPhaseWorkspace,
|
|
589
|
+
multiPhaseTasks: ["example"],
|
|
590
|
+
guardError: new Error("Phase dependency is working"),
|
|
591
|
+
})
|
|
592
|
+
await forced.runPrepare({
|
|
593
|
+
cwd: "/workbase",
|
|
594
|
+
taskId: "example",
|
|
595
|
+
phaseId: "implementation",
|
|
596
|
+
force: true,
|
|
597
|
+
silent: true,
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
expect(forced.guards).toEqual([
|
|
601
|
+
{
|
|
602
|
+
target: "execution-unit:phase/example/implementation",
|
|
603
|
+
override: true,
|
|
604
|
+
},
|
|
605
|
+
])
|
|
606
|
+
expect(forced.events).toEqual(["guard", "materialize"])
|
|
607
|
+
expect(forced.materializeOptions[0]).toMatchObject({
|
|
608
|
+
force: true,
|
|
609
|
+
validationAlreadyPerformed: true,
|
|
610
|
+
})
|
|
611
|
+
expect(forced.launches).toEqual([])
|
|
612
|
+
expect(forced.statusUpdates).toEqual([])
|
|
613
|
+
})
|
|
614
|
+
|
|
572
615
|
test("launches an epic agent from an epic directory", async () => {
|
|
573
616
|
const harness = createHarness()
|
|
574
617
|
|
package/src/commands/work.ts
CHANGED
|
@@ -577,7 +577,7 @@ export const workPrepare = (options: WorkOptions = {}) =>
|
|
|
577
577
|
|
|
578
578
|
export const help = `
|
|
579
579
|
Usage: agency work [<directory-or-task-id> | --epic <epic-id>] [--agent <name>] [--auto]
|
|
580
|
-
agency work prepare [target] [--evidence <json-or-path>] [--dry-run] [--json]
|
|
580
|
+
agency work prepare [target] [--evidence <json-or-path>] [--force] [--dry-run] [--json]
|
|
581
581
|
|
|
582
582
|
Launch an agent for an epic, task, or phase. With no directory, select one
|
|
583
583
|
interactively. A positional argument resolves as a directory first, then as a task
|
|
@@ -592,7 +592,8 @@ changes without fetching, creating branches, or creating worktrees.
|
|
|
592
592
|
It emits revision-bound validation evidence and an idempotent external-orchestrator
|
|
593
593
|
contract. Evidence is reused only while the target, workbase, configuration, and
|
|
594
594
|
repository mapping remain unchanged. Dynamic readiness and workspace safety checks
|
|
595
|
-
always run.
|
|
595
|
+
always run. With prepare, --force overrides readiness without launching or changing
|
|
596
|
+
lifecycle status.
|
|
596
597
|
|
|
597
598
|
Options:
|
|
598
599
|
--epic <id> Work on an epic
|
|
@@ -605,8 +606,8 @@ Options:
|
|
|
605
606
|
--print-command Print cwd, argv, and non-secret environment without launch
|
|
606
607
|
--opencode Require the OpenCode preset
|
|
607
608
|
--claude Require the Claude Code preset
|
|
608
|
-
--force Override readiness;
|
|
609
|
-
|
|
609
|
+
--force Override readiness; launched terminal work is reopened
|
|
610
|
+
--evidence <value> Validation evidence JSON or a path to JSON (prepare only)
|
|
610
611
|
--no-input Never open an interactive selector
|
|
611
612
|
|
|
612
613
|
Without interactive input, provide an explicit workbase or cwd and an entity
|
|
@@ -271,78 +271,57 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
271
271
|
|
|
272
272
|
if (relative(root, candidate) === "") {
|
|
273
273
|
const compact = !options.full
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
274
|
+
const validation = yield* provideContextServices(
|
|
275
|
+
workbase.validate(root, { includeDocuments: true }),
|
|
276
|
+
)
|
|
277
|
+
const projectDiscoveryDocument = <T>(
|
|
278
|
+
document: {
|
|
279
|
+
readonly id: string
|
|
280
|
+
readonly path: string
|
|
281
|
+
readonly content: string
|
|
282
|
+
readonly revision: string
|
|
283
|
+
readonly data: T
|
|
284
|
+
},
|
|
278
285
|
extra: Record<string, string> = {},
|
|
279
286
|
) =>
|
|
280
287
|
Effect.gen(function* () {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
288
|
+
const body = compact
|
|
289
|
+
? {}
|
|
290
|
+
: {
|
|
291
|
+
body: (yield* parseFrontmatter(
|
|
292
|
+
document.content,
|
|
293
|
+
document.path,
|
|
294
|
+
)).body,
|
|
295
|
+
}
|
|
289
296
|
return {
|
|
290
297
|
...extra,
|
|
291
|
-
id,
|
|
292
|
-
path,
|
|
293
|
-
sha256:
|
|
294
|
-
data:
|
|
295
|
-
...
|
|
298
|
+
id: document.id,
|
|
299
|
+
path: document.path,
|
|
300
|
+
sha256: document.revision,
|
|
301
|
+
data: document.data,
|
|
302
|
+
...body,
|
|
296
303
|
}
|
|
297
304
|
})
|
|
298
|
-
|
|
299
|
-
const epics
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
for (const entry of (yield* fs.readDirectory(taskRoot))
|
|
319
|
-
.filter((item) => item.isDirectory)
|
|
320
|
-
.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
321
|
-
const document = yield* discover(
|
|
322
|
-
entry.name,
|
|
323
|
-
join(taskRoot, entry.name, "TASK.md"),
|
|
324
|
-
TaskFrontmatter,
|
|
325
|
-
)
|
|
326
|
-
if (document) tasks.push(document)
|
|
327
|
-
|
|
328
|
-
const phaseRoot = join(taskRoot, entry.name, "phases")
|
|
329
|
-
if (!(yield* fs.isDirectory(phaseRoot))) continue
|
|
330
|
-
for (const phaseEntry of (yield* fs.readDirectory(phaseRoot))
|
|
331
|
-
.filter((item) => item.isDirectory)
|
|
332
|
-
.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
333
|
-
const phase = yield* discover(
|
|
334
|
-
phaseEntry.name,
|
|
335
|
-
join(phaseRoot, phaseEntry.name, "PHASE.md"),
|
|
336
|
-
PhaseFrontmatter,
|
|
337
|
-
{ taskId: entry.name },
|
|
338
|
-
)
|
|
339
|
-
if (phase) phases.push(phase)
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const validation = yield* provideContextServices(
|
|
345
|
-
workbase.validate(root),
|
|
305
|
+
const documents = validation.documents!
|
|
306
|
+
const epics = yield* Effect.all(
|
|
307
|
+
documents.epics.map((document) =>
|
|
308
|
+
projectDiscoveryDocument(document),
|
|
309
|
+
),
|
|
310
|
+
{ concurrency: "unbounded" },
|
|
311
|
+
)
|
|
312
|
+
const tasks = yield* Effect.all(
|
|
313
|
+
documents.tasks.map((document) =>
|
|
314
|
+
projectDiscoveryDocument(document),
|
|
315
|
+
),
|
|
316
|
+
{ concurrency: "unbounded" },
|
|
317
|
+
)
|
|
318
|
+
const phases = yield* Effect.all(
|
|
319
|
+
[...documents.phasesByTask].flatMap(([taskId, records]) =>
|
|
320
|
+
records.map((document) =>
|
|
321
|
+
projectDiscoveryDocument(document, { taskId }),
|
|
322
|
+
),
|
|
323
|
+
),
|
|
324
|
+
{ concurrency: "unbounded" },
|
|
346
325
|
)
|
|
347
326
|
return {
|
|
348
327
|
projection: compact ? "compact" : "complete",
|
|
@@ -556,104 +535,74 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
556
535
|
})
|
|
557
536
|
}
|
|
558
537
|
|
|
559
|
-
const
|
|
538
|
+
const validation = yield* provideContextServices(
|
|
539
|
+
workbase.validate(root, { includeDocuments: true }),
|
|
540
|
+
)
|
|
541
|
+
const validationDocuments = validation.documents!
|
|
542
|
+
const taskDocuments = new Map<string, Document<TaskData>>(
|
|
543
|
+
validationDocuments.tasks.map((document) => [
|
|
544
|
+
document.id,
|
|
545
|
+
{
|
|
546
|
+
id: document.id,
|
|
547
|
+
path: document.path,
|
|
548
|
+
sha256: document.revision,
|
|
549
|
+
data: document.data,
|
|
550
|
+
body: "",
|
|
551
|
+
},
|
|
552
|
+
]),
|
|
553
|
+
)
|
|
560
554
|
const phaseDocuments = new Map<string, Document<PhaseData>>()
|
|
561
|
-
for (const
|
|
562
|
-
join(root, "tasks"),
|
|
563
|
-
join(root, "archive", "tasks"),
|
|
564
|
-
]) {
|
|
565
|
-
if (!(yield* fs.isDirectory(taskRoot))) continue
|
|
566
|
-
const entries = (yield* fs.readDirectory(taskRoot))
|
|
567
|
-
.filter((entry) => entry.isDirectory)
|
|
568
|
-
.sort((a, b) => a.name.localeCompare(b.name))
|
|
569
|
-
const documents = yield* Effect.all(
|
|
570
|
-
entries.map((entry) =>
|
|
571
|
-
Effect.gen(function* () {
|
|
572
|
-
const path = join(taskRoot, entry.name, "TASK.md")
|
|
573
|
-
let taskDocument: Document<TaskData> | null = null
|
|
574
|
-
if (yield* fs.exists(path)) {
|
|
575
|
-
const content = yield* fs.readFile(path)
|
|
576
|
-
const parsed = yield* Effect.either(
|
|
577
|
-
parseFrontmatter(content, path),
|
|
578
|
-
)
|
|
579
|
-
if (Either.isRight(parsed)) {
|
|
580
|
-
const decoded = decode(TaskFrontmatter, parsed.right.data)
|
|
581
|
-
if (decoded.ok) {
|
|
582
|
-
taskDocument = {
|
|
583
|
-
id: entry.name,
|
|
584
|
-
path,
|
|
585
|
-
sha256: documentRevision(content),
|
|
586
|
-
data: decoded.value,
|
|
587
|
-
body: parsed.right.body,
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
const phasesPath = join(taskRoot, entry.name, "phases")
|
|
594
|
-
if (!(yield* fs.isDirectory(phasesPath))) {
|
|
595
|
-
return { taskDocument, phaseDocuments: [] }
|
|
596
|
-
}
|
|
597
|
-
const phaseEntries = (yield* fs.readDirectory(phasesPath))
|
|
598
|
-
.filter((item) => item.isDirectory)
|
|
599
|
-
.sort((a, b) => a.name.localeCompare(b.name))
|
|
600
|
-
const childDocuments = yield* Effect.all(
|
|
601
|
-
phaseEntries.map((phaseEntry) =>
|
|
602
|
-
Effect.gen(function* () {
|
|
603
|
-
const phasePath = join(
|
|
604
|
-
phasesPath,
|
|
605
|
-
phaseEntry.name,
|
|
606
|
-
"PHASE.md",
|
|
607
|
-
)
|
|
608
|
-
if (!(yield* fs.exists(phasePath))) return null
|
|
609
|
-
const content = yield* fs.readFile(phasePath)
|
|
610
|
-
const parsed = yield* Effect.either(
|
|
611
|
-
parseFrontmatter(content, phasePath),
|
|
612
|
-
)
|
|
613
|
-
if (Either.isLeft(parsed)) return null
|
|
614
|
-
const decoded = decode(
|
|
615
|
-
PhaseFrontmatter,
|
|
616
|
-
parsed.right.data,
|
|
617
|
-
)
|
|
618
|
-
if (!decoded.ok) return null
|
|
619
|
-
return [
|
|
620
|
-
`${entry.name}/${phaseEntry.name}`,
|
|
621
|
-
{
|
|
622
|
-
id: phaseEntry.name,
|
|
623
|
-
path: phasePath,
|
|
624
|
-
sha256: documentRevision(content),
|
|
625
|
-
data: decoded.value,
|
|
626
|
-
body: parsed.right.body,
|
|
627
|
-
},
|
|
628
|
-
] as const
|
|
629
|
-
}),
|
|
630
|
-
),
|
|
631
|
-
{ concurrency: "unbounded" },
|
|
632
|
-
)
|
|
633
|
-
return {
|
|
634
|
-
taskDocument,
|
|
635
|
-
phaseDocuments: childDocuments.filter(
|
|
636
|
-
(document): document is NonNullable<typeof document> =>
|
|
637
|
-
document !== null,
|
|
638
|
-
),
|
|
639
|
-
}
|
|
640
|
-
}),
|
|
641
|
-
),
|
|
642
|
-
{ concurrency: "unbounded" },
|
|
643
|
-
)
|
|
555
|
+
for (const [taskId, documents] of validationDocuments.phasesByTask) {
|
|
644
556
|
for (const document of documents) {
|
|
645
|
-
|
|
646
|
-
document.
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
557
|
+
phaseDocuments.set(`${taskId}/${document.id}`, {
|
|
558
|
+
id: document.id,
|
|
559
|
+
path: document.path,
|
|
560
|
+
sha256: document.revision,
|
|
561
|
+
data: document.data,
|
|
562
|
+
body: "",
|
|
563
|
+
})
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (task && !taskDocuments.has(task.id))
|
|
567
|
+
taskDocuments.set(task.id, task)
|
|
568
|
+
if (phase && target.taskId) {
|
|
569
|
+
phaseDocuments.set(`${target.taskId}/${phase.id}`, phase)
|
|
570
|
+
}
|
|
571
|
+
const relevantTaskIds = new Set([
|
|
572
|
+
...(target.taskId ? [target.taskId] : []),
|
|
573
|
+
...(epic?.data.tasks.map((child: Dependency) => child.id) ?? []),
|
|
574
|
+
])
|
|
575
|
+
for (const taskId of relevantTaskIds) {
|
|
576
|
+
let document = taskDocuments.get(taskId)
|
|
577
|
+
if (!document) {
|
|
578
|
+
const archived = yield* Effect.either(
|
|
579
|
+
readOptionalDocument(
|
|
580
|
+
taskId,
|
|
581
|
+
join(archivedTaskDirectory(root, taskId), "TASK.md"),
|
|
582
|
+
TaskFrontmatter,
|
|
583
|
+
),
|
|
584
|
+
)
|
|
585
|
+
document = Either.isRight(archived)
|
|
586
|
+
? (archived.right ?? undefined)
|
|
587
|
+
: undefined
|
|
588
|
+
if (document) taskDocuments.set(taskId, document)
|
|
589
|
+
}
|
|
590
|
+
if (!document || !("phases" in document.data)) continue
|
|
591
|
+
for (const child of document.data.phases) {
|
|
592
|
+
const key = `${taskId}/${child.id}`
|
|
593
|
+
if (phaseDocuments.has(key)) continue
|
|
594
|
+
const archived = yield* Effect.either(
|
|
595
|
+
readOptionalDocument(
|
|
596
|
+
child.id,
|
|
597
|
+
join(
|
|
598
|
+
archivedPhaseDirectory(root, taskId, child.id),
|
|
599
|
+
"PHASE.md",
|
|
600
|
+
),
|
|
601
|
+
PhaseFrontmatter,
|
|
602
|
+
),
|
|
603
|
+
)
|
|
604
|
+
if (Either.isRight(archived) && archived.right) {
|
|
605
|
+
phaseDocuments.set(key, archived.right)
|
|
657
606
|
}
|
|
658
607
|
}
|
|
659
608
|
}
|
|
@@ -720,9 +669,6 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
720
669
|
})
|
|
721
670
|
}
|
|
722
671
|
|
|
723
|
-
const validation = yield* provideContextServices(
|
|
724
|
-
workbase.validate(root),
|
|
725
|
-
)
|
|
726
672
|
const relevantPaths = new Set<string>(
|
|
727
673
|
[epic?.path, task?.path, phase?.path]
|
|
728
674
|
.filter((path): path is string => Boolean(path))
|
|
@@ -1017,9 +963,11 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
1017
963
|
const codePath = join(entityDirectory, "code")
|
|
1018
964
|
const inspectionWarnings: string[] = []
|
|
1019
965
|
const repositories = new Map(
|
|
1020
|
-
|
|
1021
|
-
(
|
|
1022
|
-
|
|
966
|
+
reviewData
|
|
967
|
+
? (yield* provideContextServices(
|
|
968
|
+
repositoryService.list(root),
|
|
969
|
+
)).map((repository) => [repository.alias, repository])
|
|
970
|
+
: [],
|
|
1023
971
|
)
|
|
1024
972
|
|
|
1025
973
|
const inspectCheckout = (
|
|
@@ -1027,12 +975,18 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
1027
975
|
checkoutPath: string,
|
|
1028
976
|
) =>
|
|
1029
977
|
Effect.gen(function* (): Generator<any, CheckoutInspection, any> {
|
|
1030
|
-
const materialized = yield*
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
978
|
+
const [materialized, listed, canonicalRoot] = yield* Effect.all(
|
|
979
|
+
[
|
|
980
|
+
fs.isDirectory(checkoutPath),
|
|
981
|
+
runGit(fs, repositoryPath, [
|
|
982
|
+
"worktree",
|
|
983
|
+
"list",
|
|
984
|
+
"--porcelain",
|
|
985
|
+
]),
|
|
986
|
+
fs.realPath(root),
|
|
987
|
+
],
|
|
988
|
+
{ concurrency: "unbounded" },
|
|
989
|
+
)
|
|
1036
990
|
if (listed === null) {
|
|
1037
991
|
inspectionWarnings.push(
|
|
1038
992
|
`Unable to inspect worktree registrations for ${repositoryPath}`,
|
|
@@ -1040,7 +994,7 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
1040
994
|
}
|
|
1041
995
|
const listedPaths = worktreePaths(listed)
|
|
1042
996
|
const canonicalCheckoutPath = join(
|
|
1043
|
-
|
|
997
|
+
canonicalRoot,
|
|
1044
998
|
relative(root, checkoutPath),
|
|
1045
999
|
)
|
|
1046
1000
|
let registered =
|
|
@@ -1056,21 +1010,25 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
1056
1010
|
dirty: null,
|
|
1057
1011
|
}
|
|
1058
1012
|
}
|
|
1059
|
-
const
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1013
|
+
const [
|
|
1014
|
+
checkoutCommit,
|
|
1015
|
+
checkoutBranch,
|
|
1016
|
+
checkoutStatus,
|
|
1017
|
+
resolvedCheckoutPath,
|
|
1018
|
+
] = yield* Effect.all(
|
|
1019
|
+
[
|
|
1020
|
+
runGit(fs, checkoutPath, ["rev-parse", "HEAD"]),
|
|
1021
|
+
runGit(fs, checkoutPath, [
|
|
1022
|
+
"symbolic-ref",
|
|
1023
|
+
"--quiet",
|
|
1024
|
+
"--short",
|
|
1025
|
+
"HEAD",
|
|
1026
|
+
]),
|
|
1027
|
+
runGitText(fs, checkoutPath, ["status", "--porcelain"]),
|
|
1028
|
+
fs.realPath(checkoutPath),
|
|
1029
|
+
],
|
|
1030
|
+
{ concurrency: "unbounded" },
|
|
1031
|
+
)
|
|
1074
1032
|
registered = registered || listedPaths.has(resolvedCheckoutPath)
|
|
1075
1033
|
if (checkoutCommit === null) {
|
|
1076
1034
|
inspectionWarnings.push(
|
|
@@ -1094,17 +1052,16 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
1094
1052
|
const repositoryPath =
|
|
1095
1053
|
repository?.path ?? join(root, "repos", executionData.repo)
|
|
1096
1054
|
const checkoutPath = join(codePath, executionData.repo)
|
|
1097
|
-
const branchCommit = yield*
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
checkoutPath,
|
|
1055
|
+
const [branchCommit, baseCommit, checkout] = yield* Effect.all(
|
|
1056
|
+
[
|
|
1057
|
+
backend.resolveRevision(
|
|
1058
|
+
repositoryPath,
|
|
1059
|
+
executionData.branch,
|
|
1060
|
+
),
|
|
1061
|
+
backend.resolveRevision(repositoryPath, executionData.base),
|
|
1062
|
+
inspectCheckout(repositoryPath, checkoutPath),
|
|
1063
|
+
],
|
|
1064
|
+
{ concurrency: "unbounded" },
|
|
1108
1065
|
)
|
|
1109
1066
|
if (branchCommit === null) {
|
|
1110
1067
|
inspectionWarnings.push(
|