@markjaquith/agency 2.71.10 → 2.71.12
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.12",
|
|
4
4
|
"description": "Manage agentic work across repositories with durable workbases",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
"preuninstall": "bun scripts/install-pi-extension.ts uninstall",
|
|
67
67
|
"benchmark:pr": "bun scripts/benchmark-pr.ts",
|
|
68
68
|
"benchmark:status": "bun scripts/benchmark-status.ts",
|
|
69
|
+
"benchmark:init": "bun scripts/benchmark-init.ts",
|
|
69
70
|
"benchmark:workbase": "bun scripts/benchmark-workbase.ts",
|
|
70
71
|
"benchmark:doctor": "bun scripts/benchmark-doctor.ts",
|
|
71
72
|
"benchmark:context": "bun scripts/benchmark-context.ts",
|
|
@@ -73,6 +74,7 @@
|
|
|
73
74
|
"benchmark:push": "bun scripts/benchmark-push.ts",
|
|
74
75
|
"benchmark:sync": "bun scripts/benchmark-sync.ts",
|
|
75
76
|
"benchmark:task": "bun scripts/benchmark-task.ts",
|
|
77
|
+
"benchmark:validate": "bun scripts/benchmark-validate.ts",
|
|
76
78
|
"benchmark:worktree": "bun scripts/benchmark-worktree.ts",
|
|
77
79
|
"test": "find src \\( -name '*.test.ts' -o -name '*.test.tsx' \\) -print0 | xargs -0 -n 1 -P 4 bun test",
|
|
78
80
|
"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>[]
|
|
@@ -238,13 +240,18 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
238
240
|
}
|
|
239
241
|
|
|
240
242
|
yield* fs.createDirectory(root)
|
|
241
|
-
yield*
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
243
|
+
yield* Effect.all(
|
|
244
|
+
[
|
|
245
|
+
fs.writeJSON(configPath, {
|
|
246
|
+
version: 2,
|
|
247
|
+
vcs: preferredVersionControl(),
|
|
248
|
+
}),
|
|
249
|
+
...["repos", "epics", "tasks"].map((directory) =>
|
|
250
|
+
fs.createDirectory(join(root, directory)),
|
|
251
|
+
),
|
|
252
|
+
],
|
|
253
|
+
{ concurrency: "unbounded" },
|
|
254
|
+
)
|
|
248
255
|
|
|
249
256
|
const ignorePath = join(root, ".gitignore")
|
|
250
257
|
const requiredPatterns = [
|
|
@@ -671,10 +678,23 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
671
678
|
const service = yield* WorkbaseService
|
|
672
679
|
const fs = yield* FileSystemService
|
|
673
680
|
const root = yield* service.discover(startPath)
|
|
681
|
+
const configPath = join(root, "agency.json")
|
|
682
|
+
const configInput = JSON.parse(
|
|
683
|
+
yield* fs.readFile(configPath),
|
|
684
|
+
) as unknown
|
|
685
|
+
const configResult = decode(WorkbaseConfig, configInput)
|
|
686
|
+
if (!configResult.success) {
|
|
687
|
+
return yield* new WorkbaseConfigError({
|
|
688
|
+
path: configPath,
|
|
689
|
+
message: `Invalid workbase configuration in ${configPath}:\n${configResult.error}`,
|
|
690
|
+
})
|
|
691
|
+
}
|
|
692
|
+
const config = configResult.value
|
|
674
693
|
const issues: ValidationIssue[] = []
|
|
675
694
|
const epics = new Map<string, DocumentRecord<EpicData>>()
|
|
676
695
|
const tasks = new Map<string, DocumentRecord<TaskData>>()
|
|
677
696
|
const phases = new Map<string, DocumentRecord<PhaseData>>()
|
|
697
|
+
const phaseIdsByTask = new Map<string, Set<string>>()
|
|
678
698
|
|
|
679
699
|
const issue = (path: string, message: string) => {
|
|
680
700
|
issues.push({ path: relative(root, path) || ".", message })
|
|
@@ -691,7 +711,13 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
691
711
|
Effect.catchAll(() => Effect.succeed([])),
|
|
692
712
|
)
|
|
693
713
|
|
|
694
|
-
const aliases = new Set(
|
|
714
|
+
const aliases = new Set(Object.keys(config.repositories ?? {}))
|
|
715
|
+
const reposPath = join(root, "repos")
|
|
716
|
+
if (yield* fs.isDirectory(reposPath)) {
|
|
717
|
+
for (const entry of yield* fs.readDirectory(reposPath)) {
|
|
718
|
+
if (!entry.name.startsWith(".agency-")) aliases.add(entry.name)
|
|
719
|
+
}
|
|
720
|
+
}
|
|
695
721
|
|
|
696
722
|
const readDocument = <S extends Schema.Schema.AnyNoContext>(
|
|
697
723
|
path: string,
|
|
@@ -728,7 +754,7 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
728
754
|
return data ? { id, path, data } : null
|
|
729
755
|
}),
|
|
730
756
|
),
|
|
731
|
-
{ concurrency:
|
|
757
|
+
{ concurrency: validationConcurrency },
|
|
732
758
|
)
|
|
733
759
|
for (const document of epicDocuments) {
|
|
734
760
|
if (document) epics.set(document.id, document)
|
|
@@ -744,6 +770,9 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
744
770
|
const phaseIds = yield* readDirectories(
|
|
745
771
|
join(taskPath, "phases"),
|
|
746
772
|
)
|
|
773
|
+
if (phaseIds.length > 0) {
|
|
774
|
+
phaseIdsByTask.set(id, new Set(phaseIds))
|
|
775
|
+
}
|
|
747
776
|
const taskPhases = yield* Effect.all(
|
|
748
777
|
phaseIds.map((phaseId) =>
|
|
749
778
|
Effect.gen(function* () {
|
|
@@ -762,7 +791,7 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
762
791
|
: null
|
|
763
792
|
}),
|
|
764
793
|
),
|
|
765
|
-
{ concurrency:
|
|
794
|
+
{ concurrency: validationConcurrency },
|
|
766
795
|
)
|
|
767
796
|
return {
|
|
768
797
|
id,
|
|
@@ -771,7 +800,7 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
771
800
|
}
|
|
772
801
|
}),
|
|
773
802
|
),
|
|
774
|
-
{ concurrency:
|
|
803
|
+
{ concurrency: validationConcurrency },
|
|
775
804
|
)
|
|
776
805
|
for (const documents of taskDocuments) {
|
|
777
806
|
if (documents.task) tasks.set(documents.id, documents.task)
|
|
@@ -922,10 +951,8 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
922
951
|
}
|
|
923
952
|
}
|
|
924
953
|
|
|
925
|
-
const
|
|
926
|
-
|
|
927
|
-
.filter((key) => key.startsWith(phasePrefix))
|
|
928
|
-
.map((key) => key.slice(phasePrefix.length))
|
|
954
|
+
const actualPhaseIds =
|
|
955
|
+
phaseIdsByTask.get(task.id) ?? new Set<string>()
|
|
929
956
|
|
|
930
957
|
if ("phases" in task.data) {
|
|
931
958
|
const declaredIds = new Set(
|
|
@@ -952,7 +979,7 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
|
|
|
952
979
|
for (const cycle of findDependencyCycles(task.data.phases)) {
|
|
953
980
|
issue(task.path, `Phase dependency cycle includes '${cycle}'`)
|
|
954
981
|
}
|
|
955
|
-
} else if (actualPhaseIds.
|
|
982
|
+
} else if (actualPhaseIds.size > 0) {
|
|
956
983
|
issue(
|
|
957
984
|
task.path,
|
|
958
985
|
"Single-phase task cannot contain phase directories",
|