@markjaquith/agency 2.49.0 → 2.51.0
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 +56 -9
- package/cli.ts +56 -1
- package/package.json +1 -1
- package/schemas/agency-graph-v1.schema.json +142 -38
- package/src/cli-parser.test.ts +81 -0
- package/src/cli-parser.ts +78 -1
- package/src/cli.test.ts +4 -4
- package/src/commands/init.test.ts +2 -0
- package/src/commands/review.ts +38 -0
- package/src/commands/task.ts +27 -5
- package/src/commands/vcs.test.ts +75 -0
- package/src/commands/vcs.ts +72 -0
- package/src/commands/work.test.ts +2 -0
- package/src/commands/work.ts +2 -2
- package/src/commands/worktree.ts +1 -1
- package/src/graph-schema.test.ts +1 -1
- package/src/graph-schema.ts +36 -13
- package/src/protocol.ts +1 -0
- package/src/services/ArchiveService.test.ts +2 -2
- package/src/services/ArchiveService.ts +24 -0
- package/src/services/ClaimService.ts +3 -2
- package/src/services/ContextService.ts +118 -17
- package/src/services/DoctorService.ts +58 -7
- package/src/services/GraphMutationService.ts +5 -0
- package/src/services/GraphService.ts +119 -54
- package/src/services/PhaseService.ts +196 -70
- package/src/services/PullRequestService.test.ts +2 -2
- package/src/services/PullRequestService.ts +37 -33
- package/src/services/ReadinessService.ts +7 -3
- package/src/services/RepositoryService.test.ts +31 -1
- package/src/services/RepositoryService.ts +63 -31
- package/src/services/ReviewService.test.ts +472 -0
- package/src/services/ReviewService.ts +427 -0
- package/src/services/SyncService.test.ts +69 -2
- package/src/services/SyncService.ts +161 -90
- package/src/services/TaskPhaseService.test.ts +80 -0
- package/src/services/TaskService.ts +82 -9
- package/src/services/VcsMigrationService.test.ts +211 -0
- package/src/services/VcsMigrationService.ts +816 -0
- package/src/services/VersionControlService.test.ts +100 -0
- package/src/services/VersionControlService.ts +479 -0
- package/src/services/WorkbaseService.ts +7 -2
- package/src/services/WorktreeService.test.ts +88 -17
- package/src/services/WorktreeService.ts +865 -329
- package/src/test-utils.ts +12 -0
- package/src/work-view.ts +14 -6
- package/src/workbase/AGENTS.md +2 -2
- package/src/workbase/schemas.test.ts +57 -0
- package/src/workbase/schemas.ts +57 -0
- package/src/workbase/version-control.ts +5 -0
package/src/test-utils.ts
CHANGED
|
@@ -20,6 +20,13 @@ import { SyncService } from "./services/SyncService"
|
|
|
20
20
|
import { ReadinessService } from "./services/ReadinessService"
|
|
21
21
|
import { GraphMutationService } from "./services/GraphMutationService"
|
|
22
22
|
import { DoctorService } from "./services/DoctorService"
|
|
23
|
+
import { ReviewService } from "./services/ReviewService"
|
|
24
|
+
import {
|
|
25
|
+
GitVersionControlService,
|
|
26
|
+
JjVersionControlService,
|
|
27
|
+
VersionControlService,
|
|
28
|
+
} from "./services/VersionControlService"
|
|
29
|
+
import { VcsMigrationService } from "./services/VcsMigrationService"
|
|
23
30
|
|
|
24
31
|
export const createTempDir = () => mkdtemp(join(tmpdir(), "agency-test-"))
|
|
25
32
|
|
|
@@ -29,6 +36,10 @@ export const cleanupTempDir = (path: string) =>
|
|
|
29
36
|
const TestLayer = Layer.mergeAll(
|
|
30
37
|
FileSystemService.Default,
|
|
31
38
|
WorkbaseService.Default,
|
|
39
|
+
GitVersionControlService.Default,
|
|
40
|
+
JjVersionControlService.Default,
|
|
41
|
+
VersionControlService.Default,
|
|
42
|
+
VcsMigrationService.Default,
|
|
32
43
|
RepositoryService.Default,
|
|
33
44
|
EpicService.Default,
|
|
34
45
|
TaskService.Default,
|
|
@@ -44,6 +55,7 @@ const TestLayer = Layer.mergeAll(
|
|
|
44
55
|
ReadinessService.Default,
|
|
45
56
|
GraphMutationService.Default,
|
|
46
57
|
DoctorService.Default,
|
|
58
|
+
ReviewService.Default,
|
|
47
59
|
)
|
|
48
60
|
|
|
49
61
|
export async function runTestEffect<A, E>(
|
package/src/work-view.ts
CHANGED
|
@@ -79,7 +79,11 @@ const rowFor = (
|
|
|
79
79
|
executions: readonly ExecutionNode[],
|
|
80
80
|
): WorkViewRow => {
|
|
81
81
|
const branches = [
|
|
82
|
-
...new Set(
|
|
82
|
+
...new Set(
|
|
83
|
+
executions.flatMap((execution) =>
|
|
84
|
+
"branch" in execution.data ? [execution.data.branch] : [],
|
|
85
|
+
),
|
|
86
|
+
),
|
|
83
87
|
]
|
|
84
88
|
const parent =
|
|
85
89
|
node.kind === "task"
|
|
@@ -105,16 +109,19 @@ const rowFor = (
|
|
|
105
109
|
: branches.length === 1
|
|
106
110
|
? branches[0]!
|
|
107
111
|
: "multiple",
|
|
108
|
-
pr: aggregateLabel(
|
|
109
|
-
|
|
110
|
-
"
|
|
111
|
-
|
|
112
|
+
pr: aggregateLabel(
|
|
113
|
+
executions,
|
|
114
|
+
(execution) => "pr" in execution.data && Boolean(execution.data.pr),
|
|
115
|
+
["present", "absent"],
|
|
116
|
+
),
|
|
112
117
|
worktree: aggregateLabel(
|
|
113
118
|
executions,
|
|
114
119
|
(execution) => execution.workspace?.materialized === true,
|
|
115
120
|
["materialized", "absent"],
|
|
116
121
|
),
|
|
117
|
-
hasPr: executions.some(
|
|
122
|
+
hasPr: executions.some(
|
|
123
|
+
(execution) => "pr" in execution.data && Boolean(execution.data.pr),
|
|
124
|
+
),
|
|
118
125
|
}
|
|
119
126
|
}
|
|
120
127
|
|
|
@@ -213,6 +220,7 @@ export const getWorkViews = (options: WorkViewOptions = {}) =>
|
|
|
213
220
|
return executions.filter(
|
|
214
221
|
(execution) =>
|
|
215
222
|
execution.data.taskId === taskId &&
|
|
223
|
+
"phaseId" in execution.data &&
|
|
216
224
|
execution.data.phaseId === phaseId,
|
|
217
225
|
)
|
|
218
226
|
}
|
package/src/workbase/AGENTS.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Agency Workbase
|
|
2
2
|
|
|
3
3
|
This directory is an Agency workbase. Epics, tasks, and phases are durable
|
|
4
|
-
Markdown documents; repository aliases and generated Git worktrees
|
|
5
|
-
access.
|
|
4
|
+
Markdown documents; repository aliases and generated Git worktrees or jj
|
|
5
|
+
workspaces provide code access according to the workbase's `vcs` setting.
|
|
6
6
|
|
|
7
7
|
## Bootstrap
|
|
8
8
|
|
|
@@ -64,6 +64,63 @@ describe("portable repository declarations", () => {
|
|
|
64
64
|
})
|
|
65
65
|
|
|
66
66
|
describe("body-of-work descriptions", () => {
|
|
67
|
+
test("decodes review tasks strictly and rejects writable execution fields", () => {
|
|
68
|
+
const review = {
|
|
69
|
+
ticketUrl: null,
|
|
70
|
+
review: {
|
|
71
|
+
repo: "agency",
|
|
72
|
+
source: { kind: "branch", ref: "refs/heads/feature/review" },
|
|
73
|
+
commit: "a".repeat(40),
|
|
74
|
+
refreshedAt: "2026-07-23T12:00:00.000Z",
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
expect(
|
|
78
|
+
Schema.decodeUnknownSync(TaskFrontmatter, { onExcessProperty: "error" })(
|
|
79
|
+
review,
|
|
80
|
+
),
|
|
81
|
+
).toMatchObject({ status: "open", review: review.review })
|
|
82
|
+
for (const field of ["repo", "repos", "branch", "base", "pr"]) {
|
|
83
|
+
expect(() =>
|
|
84
|
+
Schema.decodeUnknownSync(TaskFrontmatter, {
|
|
85
|
+
onExcessProperty: "error",
|
|
86
|
+
})({ ...review, [field]: field === "repos" ? [] : "forbidden" }),
|
|
87
|
+
).toThrow()
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test("rejects inconsistent pull request source provenance", () => {
|
|
92
|
+
const source = {
|
|
93
|
+
kind: "pull-request",
|
|
94
|
+
provider: "github",
|
|
95
|
+
repository: "owner/repo",
|
|
96
|
+
identifier: "42",
|
|
97
|
+
url: "https://github.com/owner/repo/pull/42",
|
|
98
|
+
fetchRef: "refs/pull/42/head",
|
|
99
|
+
}
|
|
100
|
+
const review = {
|
|
101
|
+
ticketUrl: null,
|
|
102
|
+
review: {
|
|
103
|
+
repo: "agency",
|
|
104
|
+
source,
|
|
105
|
+
commit: "a".repeat(40),
|
|
106
|
+
refreshedAt: "2026-07-23T12:00:00.000Z",
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
expect(Schema.decodeUnknownSync(TaskFrontmatter)(review)).toBeDefined()
|
|
110
|
+
for (const inconsistent of [
|
|
111
|
+
{ ...source, url: "https://github.com/owner/repo/pull/41" },
|
|
112
|
+
{ ...source, repository: "other/repo" },
|
|
113
|
+
{ ...source, fetchRef: "refs/pull/41/head" },
|
|
114
|
+
]) {
|
|
115
|
+
expect(() =>
|
|
116
|
+
Schema.decodeUnknownSync(TaskFrontmatter)({
|
|
117
|
+
...review,
|
|
118
|
+
review: { ...review.review, source: inconsistent },
|
|
119
|
+
}),
|
|
120
|
+
).toThrow()
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
|
|
67
124
|
test("accepts descriptions on epics, tasks, and phases", () => {
|
|
68
125
|
const epic = Schema.decodeUnknownSync(EpicFrontmatter)({
|
|
69
126
|
ticketUrl: "https://example.com/epic",
|
package/src/workbase/schemas.ts
CHANGED
|
@@ -43,6 +43,8 @@ const IsoTimestamp = NonEmptyString.pipe(
|
|
|
43
43
|
Schema.pattern(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/),
|
|
44
44
|
)
|
|
45
45
|
|
|
46
|
+
const GitCommit = Schema.String.pipe(Schema.pattern(/^[a-f0-9]{40}$/))
|
|
47
|
+
|
|
46
48
|
const DocumentRevision = Schema.String.pipe(Schema.pattern(/^[a-f0-9]{64}$/))
|
|
47
49
|
|
|
48
50
|
export const ClaimRecord = Schema.Struct({
|
|
@@ -94,6 +96,7 @@ const DeliveryProvider = Schema.Struct({
|
|
|
94
96
|
|
|
95
97
|
export const WorkbaseConfig = Schema.Struct({
|
|
96
98
|
version: Schema.Literal(2),
|
|
99
|
+
vcs: Schema.optional(Schema.Literal("git", "jj")),
|
|
97
100
|
repositories: Schema.optional(
|
|
98
101
|
Schema.Record({ key: RepositoryAlias, value: RepositoryDeclaration }),
|
|
99
102
|
),
|
|
@@ -172,9 +175,61 @@ const MultiPhaseTaskFrontmatter = Schema.Struct({
|
|
|
172
175
|
phases: Schema.Array(Dependency),
|
|
173
176
|
})
|
|
174
177
|
|
|
178
|
+
export const ReviewPullRequestSource = Schema.Struct({
|
|
179
|
+
kind: Schema.Literal("pull-request"),
|
|
180
|
+
provider: Schema.Literal("github"),
|
|
181
|
+
repository: NonEmptyString,
|
|
182
|
+
identifier: NonEmptyString.pipe(Schema.pattern(/^\d+$/)),
|
|
183
|
+
url: GitHubPullRequestUrl,
|
|
184
|
+
fetchRef: NonEmptyString,
|
|
185
|
+
}).pipe(
|
|
186
|
+
Schema.filter(
|
|
187
|
+
(source) =>
|
|
188
|
+
source.url ===
|
|
189
|
+
`https://github.com/${source.repository}/pull/${source.identifier}` &&
|
|
190
|
+
source.fetchRef === `refs/pull/${source.identifier}/head`,
|
|
191
|
+
{
|
|
192
|
+
message: () =>
|
|
193
|
+
"Pull request review source URL, repository, identifier, and fetch ref must agree",
|
|
194
|
+
},
|
|
195
|
+
),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
export const ReviewBranchSource = Schema.Struct({
|
|
199
|
+
kind: Schema.Literal("branch"),
|
|
200
|
+
ref: NonEmptyString.pipe(
|
|
201
|
+
Schema.pattern(
|
|
202
|
+
/^refs\/heads\/(?!HEAD$)(?!.*(?:\.\.|@\{|[ ~^:?*\[\\\]]))(?!.*\/\/)(?!.*(?:^|\/)\.)(?!.*\/$)(?!.*\.lock(?:\/|$))[A-Za-z0-9._\/-]+$/,
|
|
203
|
+
),
|
|
204
|
+
),
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
export const ReviewSource = Schema.Union(
|
|
208
|
+
ReviewPullRequestSource,
|
|
209
|
+
ReviewBranchSource,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
export const ReviewRecord = Schema.Struct({
|
|
213
|
+
repo: RepositoryAlias,
|
|
214
|
+
source: ReviewSource,
|
|
215
|
+
commit: GitCommit,
|
|
216
|
+
refreshedAt: IsoTimestamp,
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
const ReviewTaskFrontmatter = Schema.Struct({
|
|
220
|
+
ticketUrl: Schema.NullOr(Url),
|
|
221
|
+
description: Description,
|
|
222
|
+
epic: Schema.optional(EntityId),
|
|
223
|
+
review: ReviewRecord,
|
|
224
|
+
status: Schema.optionalWith(WorkStatus, { default: () => "open" as const }),
|
|
225
|
+
claim: Schema.optional(ClaimRecord),
|
|
226
|
+
completion: Schema.optional(CompletionRecord),
|
|
227
|
+
})
|
|
228
|
+
|
|
175
229
|
export const TaskFrontmatter = Schema.Union(
|
|
176
230
|
SinglePhaseTaskFrontmatter,
|
|
177
231
|
MultiPhaseTaskFrontmatter,
|
|
232
|
+
ReviewTaskFrontmatter,
|
|
178
233
|
)
|
|
179
234
|
|
|
180
235
|
export const PhaseFrontmatter = Schema.Struct({
|
|
@@ -195,6 +250,8 @@ export type RepositoryDeclaration = Schema.Schema.Type<
|
|
|
195
250
|
export type WorkStatus = Schema.Schema.Type<typeof WorkStatus>
|
|
196
251
|
export type ClaimRecord = Schema.Schema.Type<typeof ClaimRecord>
|
|
197
252
|
export type PullRequestRecord = Schema.Schema.Type<typeof PullRequestRecord>
|
|
253
|
+
export type ReviewSource = Schema.Schema.Type<typeof ReviewSource>
|
|
254
|
+
export type ReviewRecord = Schema.Schema.Type<typeof ReviewRecord>
|
|
198
255
|
export type CompletionRecord = Schema.Schema.Type<typeof CompletionRecord>
|
|
199
256
|
export type EpicFrontmatter = Schema.Schema.Type<typeof EpicFrontmatter>
|
|
200
257
|
export type TaskFrontmatter = Schema.Schema.Type<typeof TaskFrontmatter>
|