@markjaquith/agency 2.71.10 → 2.71.11
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markjaquith/agency",
|
|
3
|
-
"version": "2.71.
|
|
3
|
+
"version": "2.71.11",
|
|
4
4
|
"description": "Manage agentic work across repositories with durable workbases",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
"benchmark:push": "bun scripts/benchmark-push.ts",
|
|
74
74
|
"benchmark:sync": "bun scripts/benchmark-sync.ts",
|
|
75
75
|
"benchmark:task": "bun scripts/benchmark-task.ts",
|
|
76
|
+
"benchmark:validate": "bun scripts/benchmark-validate.ts",
|
|
76
77
|
"benchmark:worktree": "bun scripts/benchmark-worktree.ts",
|
|
77
78
|
"test": "find src \\( -name '*.test.ts' -o -name '*.test.tsx' \\) -print0 | xargs -0 -n 1 -P 4 bun test",
|
|
78
79
|
"test:opencode": "AGENCY_TEST_OPENCODE=1 bun test src/cli.test.ts --test-name-pattern 'provides effective whole-workbase OpenCode access'",
|
|
@@ -3,6 +3,7 @@ import { Effect } from "effect"
|
|
|
3
3
|
import { mkdir, realpath, rm, symlink } from "node:fs/promises"
|
|
4
4
|
import { dirname, join } from "node:path"
|
|
5
5
|
import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
|
|
6
|
+
import { FileSystemService } from "./FileSystemService"
|
|
6
7
|
import { WorkbaseService } from "./WorkbaseService"
|
|
7
8
|
|
|
8
9
|
const write = async (root: string, path: string, content: string) => {
|
|
@@ -104,6 +105,65 @@ pr: null
|
|
|
104
105
|
expect(report.issues).toEqual([])
|
|
105
106
|
})
|
|
106
107
|
|
|
108
|
+
test("reads configuration and documents once during validation", async () => {
|
|
109
|
+
await write(
|
|
110
|
+
root,
|
|
111
|
+
"agency.json",
|
|
112
|
+
JSON.stringify({
|
|
113
|
+
version: 2,
|
|
114
|
+
repositories: {
|
|
115
|
+
agency: { remote: "https://example.com/agency.git" },
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
118
|
+
)
|
|
119
|
+
await write(
|
|
120
|
+
root,
|
|
121
|
+
"tasks/example/TASK.md",
|
|
122
|
+
`---
|
|
123
|
+
ticketUrl: null
|
|
124
|
+
repo: agency
|
|
125
|
+
branch: task/example
|
|
126
|
+
base: main
|
|
127
|
+
pr: null
|
|
128
|
+
---
|
|
129
|
+
`,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
const fs = await Effect.runPromise(
|
|
133
|
+
FileSystemService.pipe(Effect.provide(FileSystemService.Default)),
|
|
134
|
+
)
|
|
135
|
+
const reads: string[] = []
|
|
136
|
+
const exists: string[] = []
|
|
137
|
+
const trackedFs = {
|
|
138
|
+
...fs,
|
|
139
|
+
readFile: (path: string) => {
|
|
140
|
+
reads.push(path)
|
|
141
|
+
return fs.readFile(path)
|
|
142
|
+
},
|
|
143
|
+
exists: (path: string) => {
|
|
144
|
+
exists.push(path)
|
|
145
|
+
return fs.exists(path)
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const report = await Effect.runPromise(
|
|
150
|
+
WorkbaseService.pipe(
|
|
151
|
+
Effect.flatMap((service) => service.validate(root)),
|
|
152
|
+
Effect.provide(WorkbaseService.Default),
|
|
153
|
+
Effect.provideService(FileSystemService, trackedFs),
|
|
154
|
+
) as Effect.Effect<unknown, unknown, never>,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
expect(report).toMatchObject({ valid: true, taskCount: 1 })
|
|
158
|
+
expect(
|
|
159
|
+
reads.filter((path) => path === join(root, "agency.json")),
|
|
160
|
+
).toHaveLength(2)
|
|
161
|
+
expect(
|
|
162
|
+
reads.filter((path) => path === join(root, "tasks/example/TASK.md")),
|
|
163
|
+
).toHaveLength(1)
|
|
164
|
+
expect(exists).not.toContain(join(root, "tasks/example/TASK.md"))
|
|
165
|
+
})
|
|
166
|
+
|
|
107
167
|
test("validates non-PR completion invariants without rejecting legacy done work", async () => {
|
|
108
168
|
await write(
|
|
109
169
|
root,
|
|
@@ -65,6 +65,8 @@ interface DocumentRecord<T> {
|
|
|
65
65
|
readonly data: T
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
const validationConcurrency = 32
|
|
69
|
+
|
|
68
70
|
interface ValidationDocuments {
|
|
69
71
|
readonly epics: readonly DocumentRecord<EpicData>[]
|
|
70
72
|
readonly tasks: readonly DocumentRecord<TaskData>[]
|
|
@@ -671,10 +673,23 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
671
673
|
const service = yield* WorkbaseService
|
|
672
674
|
const fs = yield* FileSystemService
|
|
673
675
|
const root = yield* service.discover(startPath)
|
|
676
|
+
const configPath = join(root, "agency.json")
|
|
677
|
+
const configInput = JSON.parse(
|
|
678
|
+
yield* fs.readFile(configPath),
|
|
679
|
+
) as unknown
|
|
680
|
+
const configResult = decode(WorkbaseConfig, configInput)
|
|
681
|
+
if (!configResult.success) {
|
|
682
|
+
return yield* new WorkbaseConfigError({
|
|
683
|
+
path: configPath,
|
|
684
|
+
message: `Invalid workbase configuration in ${configPath}:\n${configResult.error}`,
|
|
685
|
+
})
|
|
686
|
+
}
|
|
687
|
+
const config = configResult.value
|
|
674
688
|
const issues: ValidationIssue[] = []
|
|
675
689
|
const epics = new Map<string, DocumentRecord<EpicData>>()
|
|
676
690
|
const tasks = new Map<string, DocumentRecord<TaskData>>()
|
|
677
691
|
const phases = new Map<string, DocumentRecord<PhaseData>>()
|
|
692
|
+
const phaseIdsByTask = new Map<string, Set<string>>()
|
|
678
693
|
|
|
679
694
|
const issue = (path: string, message: string) => {
|
|
680
695
|
issues.push({ path: relative(root, path) || ".", message })
|
|
@@ -691,7 +706,13 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
691
706
|
Effect.catchAll(() => Effect.succeed([])),
|
|
692
707
|
)
|
|
693
708
|
|
|
694
|
-
const aliases = new Set(
|
|
709
|
+
const aliases = new Set(Object.keys(config.repositories ?? {}))
|
|
710
|
+
const reposPath = join(root, "repos")
|
|
711
|
+
if (yield* fs.isDirectory(reposPath)) {
|
|
712
|
+
for (const entry of yield* fs.readDirectory(reposPath)) {
|
|
713
|
+
if (!entry.name.startsWith(".agency-")) aliases.add(entry.name)
|
|
714
|
+
}
|
|
715
|
+
}
|
|
695
716
|
|
|
696
717
|
const readDocument = <S extends Schema.Schema.AnyNoContext>(
|
|
697
718
|
path: string,
|
|
@@ -728,7 +749,7 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
728
749
|
return data ? { id, path, data } : null
|
|
729
750
|
}),
|
|
730
751
|
),
|
|
731
|
-
{ concurrency:
|
|
752
|
+
{ concurrency: validationConcurrency },
|
|
732
753
|
)
|
|
733
754
|
for (const document of epicDocuments) {
|
|
734
755
|
if (document) epics.set(document.id, document)
|
|
@@ -744,6 +765,9 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
744
765
|
const phaseIds = yield* readDirectories(
|
|
745
766
|
join(taskPath, "phases"),
|
|
746
767
|
)
|
|
768
|
+
if (phaseIds.length > 0) {
|
|
769
|
+
phaseIdsByTask.set(id, new Set(phaseIds))
|
|
770
|
+
}
|
|
747
771
|
const taskPhases = yield* Effect.all(
|
|
748
772
|
phaseIds.map((phaseId) =>
|
|
749
773
|
Effect.gen(function* () {
|
|
@@ -762,7 +786,7 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
762
786
|
: null
|
|
763
787
|
}),
|
|
764
788
|
),
|
|
765
|
-
{ concurrency:
|
|
789
|
+
{ concurrency: validationConcurrency },
|
|
766
790
|
)
|
|
767
791
|
return {
|
|
768
792
|
id,
|
|
@@ -771,7 +795,7 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
771
795
|
}
|
|
772
796
|
}),
|
|
773
797
|
),
|
|
774
|
-
{ concurrency:
|
|
798
|
+
{ concurrency: validationConcurrency },
|
|
775
799
|
)
|
|
776
800
|
for (const documents of taskDocuments) {
|
|
777
801
|
if (documents.task) tasks.set(documents.id, documents.task)
|
|
@@ -922,10 +946,8 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
922
946
|
}
|
|
923
947
|
}
|
|
924
948
|
|
|
925
|
-
const
|
|
926
|
-
|
|
927
|
-
.filter((key) => key.startsWith(phasePrefix))
|
|
928
|
-
.map((key) => key.slice(phasePrefix.length))
|
|
949
|
+
const actualPhaseIds =
|
|
950
|
+
phaseIdsByTask.get(task.id) ?? new Set<string>()
|
|
929
951
|
|
|
930
952
|
if ("phases" in task.data) {
|
|
931
953
|
const declaredIds = new Set(
|
|
@@ -952,7 +974,7 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
952
974
|
for (const cycle of findDependencyCycles(task.data.phases)) {
|
|
953
975
|
issue(task.path, `Phase dependency cycle includes '${cycle}'`)
|
|
954
976
|
}
|
|
955
|
-
} else if (actualPhaseIds.
|
|
977
|
+
} else if (actualPhaseIds.size > 0) {
|
|
956
978
|
issue(
|
|
957
979
|
task.path,
|
|
958
980
|
"Single-phase task cannot contain phase directories",
|