@markjaquith/agency 2.48.1 → 2.50.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 +40 -9
- package/cli.ts +32 -0
- package/package.json +1 -1
- package/schemas/agency-graph-v1.schema.json +141 -38
- package/src/cli-parser.test.ts +120 -0
- package/src/cli-parser.ts +122 -7
- package/src/cli.test.ts +71 -0
- package/src/commands/claim.ts +26 -2
- package/src/commands/phase.ts +23 -2
- package/src/commands/review.ts +38 -0
- package/src/commands/task.ts +49 -7
- package/src/commands/work.test.ts +2 -0
- package/src/commands/work.ts +2 -2
- package/src/graph-schema.ts +35 -13
- package/src/protocol.ts +1 -0
- package/src/services/ArchiveService.test.ts +2 -2
- package/src/services/ArchiveService.ts +1 -0
- package/src/services/ClaimService.test.ts +59 -0
- package/src/services/ClaimService.ts +36 -2
- package/src/services/ContextService.ts +56 -4
- package/src/services/DoctorService.ts +45 -1
- package/src/services/GraphMutationService.ts +21 -0
- package/src/services/GraphService.ts +118 -54
- package/src/services/IntegrationService.test.ts +4 -1
- package/src/services/PhaseService.ts +55 -3
- package/src/services/PullRequestService.test.ts +22 -2
- package/src/services/PullRequestService.ts +44 -3
- package/src/services/ReadinessService.ts +7 -3
- package/src/services/ReviewService.test.ts +472 -0
- package/src/services/ReviewService.ts +404 -0
- package/src/services/SyncService.test.ts +68 -2
- package/src/services/SyncService.ts +123 -6
- package/src/services/TaskPhaseService.test.ts +182 -0
- package/src/services/TaskService.ts +128 -11
- package/src/services/WorkbaseService.test.ts +58 -0
- package/src/services/WorkbaseService.ts +22 -1
- package/src/services/WorktreeService.test.ts +16 -16
- package/src/services/WorktreeService.ts +76 -40
- package/src/test-utils.ts +2 -0
- package/src/work-view.ts +14 -6
- package/src/workbase/AGENTS.md +7 -2
- package/src/workbase/completion.ts +28 -0
- package/src/workbase/schemas.test.ts +57 -0
- package/src/workbase/schemas.ts +65 -0
|
@@ -43,7 +43,8 @@ interface ExecutionWorkspace {
|
|
|
43
43
|
readonly taskPath: string
|
|
44
44
|
readonly phasePath: string | null
|
|
45
45
|
readonly codePath: string
|
|
46
|
-
readonly writablePath: string
|
|
46
|
+
readonly writablePath: string | null
|
|
47
|
+
readonly reviewPath: string | null
|
|
47
48
|
readonly repo: string
|
|
48
49
|
readonly repos: readonly RepositoryReference[]
|
|
49
50
|
readonly dryRun: boolean
|
|
@@ -181,12 +182,14 @@ const inspectExecution = (
|
|
|
181
182
|
const root = yield* workbase.discover(startPath)
|
|
182
183
|
const task = yield* tasks.show(taskId, root)
|
|
183
184
|
|
|
184
|
-
let execution:
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
185
|
+
let execution:
|
|
186
|
+
| {
|
|
187
|
+
repo: string
|
|
188
|
+
repos?: readonly RepositoryReference[]
|
|
189
|
+
branch: string
|
|
190
|
+
base: string
|
|
191
|
+
}
|
|
192
|
+
| { review: { repo: string; commit: string } }
|
|
190
193
|
let owner: WorktreeOwner
|
|
191
194
|
let codePath: string
|
|
192
195
|
if ("phases" in task.data) {
|
|
@@ -230,6 +233,7 @@ const inspectExecution = (
|
|
|
230
233
|
ownership.set(key, owners)
|
|
231
234
|
}
|
|
232
235
|
} else {
|
|
236
|
+
if (!("repo" in taskRecord.data)) continue
|
|
233
237
|
const key = `${taskRecord.data.repo}:${taskRecord.data.branch}`
|
|
234
238
|
const owners = ownership.get(key) ?? []
|
|
235
239
|
owners.push({
|
|
@@ -244,10 +248,13 @@ const inspectExecution = (
|
|
|
244
248
|
const declared: readonly (
|
|
245
249
|
| { readonly repo: string; readonly branch: string }
|
|
246
250
|
| RepositoryReference
|
|
247
|
-
)[] =
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
+
)[] =
|
|
252
|
+
"review" in execution
|
|
253
|
+
? [{ repo: execution.review.repo, ref: execution.review.commit }]
|
|
254
|
+
: [
|
|
255
|
+
{ repo: execution.repo, branch: execution.branch },
|
|
256
|
+
...(execution.repos ?? []),
|
|
257
|
+
]
|
|
251
258
|
const checkouts: WorktreeCheckoutInspection[] = []
|
|
252
259
|
for (const checkout of declared) {
|
|
253
260
|
const repositoryPath = join(root, "repos", checkout.repo)
|
|
@@ -608,12 +615,14 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
608
615
|
}
|
|
609
616
|
const task = yield* tasks.show(taskId, root)
|
|
610
617
|
|
|
611
|
-
let execution:
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
618
|
+
let execution:
|
|
619
|
+
| {
|
|
620
|
+
repo: string
|
|
621
|
+
repos?: readonly RepositoryReference[]
|
|
622
|
+
branch: string
|
|
623
|
+
base: string
|
|
624
|
+
}
|
|
625
|
+
| { review: { repo: string; commit: string } }
|
|
617
626
|
let phasePath: string | null = null
|
|
618
627
|
let codePath: string
|
|
619
628
|
if ("phases" in task.data) {
|
|
@@ -639,10 +648,19 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
639
648
|
const requestedCheckouts: readonly (
|
|
640
649
|
| { readonly repo: string; readonly branch: string }
|
|
641
650
|
| RepositoryReference
|
|
642
|
-
)[] =
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
651
|
+
)[] =
|
|
652
|
+
"review" in execution
|
|
653
|
+
? [
|
|
654
|
+
{
|
|
655
|
+
repo: execution.review.repo,
|
|
656
|
+
ref: execution.review.commit,
|
|
657
|
+
},
|
|
658
|
+
]
|
|
659
|
+
: [
|
|
660
|
+
{ repo: execution.repo, branch: execution.branch },
|
|
661
|
+
...(execution.repos ?? []),
|
|
662
|
+
]
|
|
663
|
+
const executionBase = "base" in execution ? execution.base : ""
|
|
646
664
|
const canonicalCodePath = (yield* fs.exists(codePath))
|
|
647
665
|
? yield* fs.realPath(codePath)
|
|
648
666
|
: resolve(codePath)
|
|
@@ -732,7 +750,7 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
732
750
|
repositoryPath,
|
|
733
751
|
"rev-parse",
|
|
734
752
|
"--verify",
|
|
735
|
-
`${
|
|
753
|
+
`${executionBase}^{commit}`,
|
|
736
754
|
],
|
|
737
755
|
{ captureOutput: true },
|
|
738
756
|
)
|
|
@@ -744,7 +762,7 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
744
762
|
repositoryPath,
|
|
745
763
|
"ls-remote",
|
|
746
764
|
"origin",
|
|
747
|
-
originRef(
|
|
765
|
+
originRef(executionBase),
|
|
748
766
|
],
|
|
749
767
|
{ captureOutput: true },
|
|
750
768
|
)
|
|
@@ -753,7 +771,7 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
753
771
|
!remoteBase.stdout.trim()
|
|
754
772
|
) {
|
|
755
773
|
return yield* new WorktreeError({
|
|
756
|
-
message: `Base '${
|
|
774
|
+
message: `Base '${executionBase}' for repository '${alias}' does not resolve to a commit`,
|
|
757
775
|
})
|
|
758
776
|
}
|
|
759
777
|
}
|
|
@@ -764,7 +782,7 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
764
782
|
repo: repositoryPath,
|
|
765
783
|
worktree: checkoutPath,
|
|
766
784
|
branch: checkout.branch,
|
|
767
|
-
base:
|
|
785
|
+
base: executionBase,
|
|
768
786
|
})
|
|
769
787
|
} catch (cause) {
|
|
770
788
|
return yield* new WorktreeError({
|
|
@@ -991,7 +1009,7 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
991
1009
|
repo: repositoryPath,
|
|
992
1010
|
worktree: checkoutPath,
|
|
993
1011
|
branch: checkout.branch,
|
|
994
|
-
base:
|
|
1012
|
+
base: executionBase,
|
|
995
1013
|
}
|
|
996
1014
|
try {
|
|
997
1015
|
args = expandWorktreeCreateCommand(
|
|
@@ -1026,7 +1044,7 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
1026
1044
|
repositoryPath,
|
|
1027
1045
|
"branch",
|
|
1028
1046
|
checkout.branch,
|
|
1029
|
-
|
|
1047
|
+
executionBase,
|
|
1030
1048
|
]
|
|
1031
1049
|
operations.push({
|
|
1032
1050
|
action: "create-branch",
|
|
@@ -1085,7 +1103,7 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
1085
1103
|
repositoryPath,
|
|
1086
1104
|
"rev-parse",
|
|
1087
1105
|
"--verify",
|
|
1088
|
-
`${
|
|
1106
|
+
`${executionBase}^{commit}`,
|
|
1089
1107
|
],
|
|
1090
1108
|
{ captureOutput: true },
|
|
1091
1109
|
)
|
|
@@ -1266,9 +1284,17 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
1266
1284
|
taskPath: task.path,
|
|
1267
1285
|
phasePath,
|
|
1268
1286
|
codePath,
|
|
1269
|
-
writablePath:
|
|
1270
|
-
|
|
1271
|
-
|
|
1287
|
+
writablePath:
|
|
1288
|
+
"review" in execution ? null : join(codePath, execution.repo),
|
|
1289
|
+
reviewPath:
|
|
1290
|
+
"review" in execution
|
|
1291
|
+
? join(codePath, execution.review.repo)
|
|
1292
|
+
: null,
|
|
1293
|
+
repo:
|
|
1294
|
+
"review" in execution
|
|
1295
|
+
? execution.review.repo
|
|
1296
|
+
: execution.repo,
|
|
1297
|
+
repos: "review" in execution ? [] : (execution.repos ?? []),
|
|
1272
1298
|
dryRun: options.dryRun === true,
|
|
1273
1299
|
checkouts: checkoutReports,
|
|
1274
1300
|
operations,
|
|
@@ -1412,11 +1438,13 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
1412
1438
|
}
|
|
1413
1439
|
const task = yield* tasks.show(taskId, root)
|
|
1414
1440
|
|
|
1415
|
-
let execution:
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1441
|
+
let execution:
|
|
1442
|
+
| {
|
|
1443
|
+
repo: string
|
|
1444
|
+
repos?: readonly RepositoryReference[]
|
|
1445
|
+
branch: string
|
|
1446
|
+
}
|
|
1447
|
+
| { review: { repo: string; commit: string } }
|
|
1420
1448
|
let codePath: string
|
|
1421
1449
|
if ("phases" in task.data) {
|
|
1422
1450
|
if (!phaseId) {
|
|
@@ -1450,10 +1478,18 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
1450
1478
|
const expectedCheckouts: readonly (
|
|
1451
1479
|
| { readonly repo: string; readonly branch: string }
|
|
1452
1480
|
| RepositoryReference
|
|
1453
|
-
)[] =
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1481
|
+
)[] =
|
|
1482
|
+
"review" in execution
|
|
1483
|
+
? [
|
|
1484
|
+
{
|
|
1485
|
+
repo: execution.review.repo,
|
|
1486
|
+
ref: execution.review.commit,
|
|
1487
|
+
},
|
|
1488
|
+
]
|
|
1489
|
+
: [
|
|
1490
|
+
{ repo: execution.repo, branch: execution.branch },
|
|
1491
|
+
...(execution.repos ?? []),
|
|
1492
|
+
]
|
|
1457
1493
|
const expectedAliases = expectedCheckouts.map(({ repo }) => repo)
|
|
1458
1494
|
if (codeDirectoryExists) {
|
|
1459
1495
|
const unmanaged = (yield* fs.readDirectory(codePath)).filter(
|
package/src/test-utils.ts
CHANGED
|
@@ -20,6 +20,7 @@ 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"
|
|
23
24
|
|
|
24
25
|
export const createTempDir = () => mkdtemp(join(tmpdir(), "agency-test-"))
|
|
25
26
|
|
|
@@ -44,6 +45,7 @@ const TestLayer = Layer.mergeAll(
|
|
|
44
45
|
ReadinessService.Default,
|
|
45
46
|
GraphMutationService.Default,
|
|
46
47
|
DoctorService.Default,
|
|
48
|
+
ReviewService.Default,
|
|
47
49
|
)
|
|
48
50
|
|
|
49
51
|
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
|
@@ -43,7 +43,8 @@ reason to edit `agency.json` or `repos/` by hand.
|
|
|
43
43
|
Require explicit user intent before initializing a workbase; changing repository
|
|
44
44
|
aliases or applying repository setup or workbase sync changes; launching another
|
|
45
45
|
agent from an active agent session; creating a pull request; archiving, restoring,
|
|
46
|
-
dropping, or
|
|
46
|
+
dropping, reopening, or completing work without a pull request; or using `--force`
|
|
47
|
+
to override readiness.
|
|
47
48
|
|
|
48
49
|
## Safety
|
|
49
50
|
|
|
@@ -80,7 +81,9 @@ release the claim with the current document revision.
|
|
|
80
81
|
An execution unit remains `working` after implementation is committed and while
|
|
81
82
|
its pull request is open. It becomes `done` only after its authoritative pull
|
|
82
83
|
request is merged and Agency reconciles that state. Do not mark committed or
|
|
83
|
-
review-ready work `done` manually.
|
|
84
|
+
review-ready work `done` manually. A genuine investigation, operational action,
|
|
85
|
+
or no-change result may complete without a pull request only with explicit user
|
|
86
|
+
intent, `--no-pull-request`, and a durable outcome summary.
|
|
84
87
|
|
|
85
88
|
At each closeout trigger (creating or updating a PR, marking it ready, completing
|
|
86
89
|
a refinement loop, or pausing or handing off completed implementation work):
|
|
@@ -90,6 +93,8 @@ a refinement loop, or pausing or handing off completed implementation work):
|
|
|
90
93
|
keep the execution unit `working` through review and merge.
|
|
91
94
|
- After merge, run `agency sync --apply` to reconcile the execution unit to
|
|
92
95
|
`done`.
|
|
96
|
+
- For an approved non-PR outcome, finish an active claim or update unclaimed
|
|
97
|
+
status with `--no-pull-request --summary <text>` and optional supporting URL.
|
|
93
98
|
- Refresh durable delivery context in `TASK.md` or `PHASE.md`, including recorded
|
|
94
99
|
PR state, current head, diff summary, and verification results after later
|
|
95
100
|
pushes when those details are maintained there.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { CompletionRecord } from "./schemas"
|
|
2
|
+
|
|
3
|
+
export interface NonPrCompletionInput {
|
|
4
|
+
readonly summary: string
|
|
5
|
+
readonly evidenceUrl?: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const buildNonPrCompletion = (
|
|
9
|
+
input: NonPrCompletionInput,
|
|
10
|
+
now: Date,
|
|
11
|
+
): { readonly value: CompletionRecord } | { readonly error: string } => {
|
|
12
|
+
const summary = input.summary.trim()
|
|
13
|
+
if (!summary) return { error: "Non-PR completion summary must not be empty" }
|
|
14
|
+
|
|
15
|
+
const evidenceUrl = input.evidenceUrl?.trim()
|
|
16
|
+
if (evidenceUrl && !/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(evidenceUrl)) {
|
|
17
|
+
return { error: `Invalid completion evidence URL: ${evidenceUrl}` }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
value: {
|
|
22
|
+
mode: "non-pr",
|
|
23
|
+
completedAt: now.toISOString(),
|
|
24
|
+
summary,
|
|
25
|
+
...(evidenceUrl ? { evidenceUrl } : {}),
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -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({
|
|
@@ -75,6 +77,13 @@ export const PullRequestRecord = Schema.Struct({
|
|
|
75
77
|
mergeable: Schema.optional(Schema.NullOr(Schema.Boolean)),
|
|
76
78
|
})
|
|
77
79
|
|
|
80
|
+
export const CompletionRecord = Schema.Struct({
|
|
81
|
+
mode: Schema.Literal("non-pr"),
|
|
82
|
+
completedAt: IsoTimestamp,
|
|
83
|
+
summary: NonEmptyString,
|
|
84
|
+
evidenceUrl: Schema.optional(Url),
|
|
85
|
+
})
|
|
86
|
+
|
|
78
87
|
const DeliveryProvider = Schema.Struct({
|
|
79
88
|
provider: EntityId,
|
|
80
89
|
remote: Schema.optional(NonEmptyString),
|
|
@@ -141,6 +150,7 @@ const ExecutionUnit = {
|
|
|
141
150
|
pr: Schema.NullOr(Schema.Union(GitHubPullRequestUrl, PullRequestRecord)),
|
|
142
151
|
status: Schema.optionalWith(WorkStatus, { default: () => "open" as const }),
|
|
143
152
|
claim: Schema.optional(ClaimRecord),
|
|
153
|
+
completion: Schema.optional(CompletionRecord),
|
|
144
154
|
}
|
|
145
155
|
|
|
146
156
|
export const EpicFrontmatter = Schema.Struct({
|
|
@@ -164,9 +174,61 @@ const MultiPhaseTaskFrontmatter = Schema.Struct({
|
|
|
164
174
|
phases: Schema.Array(Dependency),
|
|
165
175
|
})
|
|
166
176
|
|
|
177
|
+
export const ReviewPullRequestSource = Schema.Struct({
|
|
178
|
+
kind: Schema.Literal("pull-request"),
|
|
179
|
+
provider: Schema.Literal("github"),
|
|
180
|
+
repository: NonEmptyString,
|
|
181
|
+
identifier: NonEmptyString.pipe(Schema.pattern(/^\d+$/)),
|
|
182
|
+
url: GitHubPullRequestUrl,
|
|
183
|
+
fetchRef: NonEmptyString,
|
|
184
|
+
}).pipe(
|
|
185
|
+
Schema.filter(
|
|
186
|
+
(source) =>
|
|
187
|
+
source.url ===
|
|
188
|
+
`https://github.com/${source.repository}/pull/${source.identifier}` &&
|
|
189
|
+
source.fetchRef === `refs/pull/${source.identifier}/head`,
|
|
190
|
+
{
|
|
191
|
+
message: () =>
|
|
192
|
+
"Pull request review source URL, repository, identifier, and fetch ref must agree",
|
|
193
|
+
},
|
|
194
|
+
),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
export const ReviewBranchSource = Schema.Struct({
|
|
198
|
+
kind: Schema.Literal("branch"),
|
|
199
|
+
ref: NonEmptyString.pipe(
|
|
200
|
+
Schema.pattern(
|
|
201
|
+
/^refs\/heads\/(?!HEAD$)(?!.*(?:\.\.|@\{|[ ~^:?*\[\\\]]))(?!.*\/\/)(?!.*(?:^|\/)\.)(?!.*\/$)(?!.*\.lock(?:\/|$))[A-Za-z0-9._\/-]+$/,
|
|
202
|
+
),
|
|
203
|
+
),
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
export const ReviewSource = Schema.Union(
|
|
207
|
+
ReviewPullRequestSource,
|
|
208
|
+
ReviewBranchSource,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
export const ReviewRecord = Schema.Struct({
|
|
212
|
+
repo: RepositoryAlias,
|
|
213
|
+
source: ReviewSource,
|
|
214
|
+
commit: GitCommit,
|
|
215
|
+
refreshedAt: IsoTimestamp,
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
const ReviewTaskFrontmatter = Schema.Struct({
|
|
219
|
+
ticketUrl: Schema.NullOr(Url),
|
|
220
|
+
description: Description,
|
|
221
|
+
epic: Schema.optional(EntityId),
|
|
222
|
+
review: ReviewRecord,
|
|
223
|
+
status: Schema.optionalWith(WorkStatus, { default: () => "open" as const }),
|
|
224
|
+
claim: Schema.optional(ClaimRecord),
|
|
225
|
+
completion: Schema.optional(CompletionRecord),
|
|
226
|
+
})
|
|
227
|
+
|
|
167
228
|
export const TaskFrontmatter = Schema.Union(
|
|
168
229
|
SinglePhaseTaskFrontmatter,
|
|
169
230
|
MultiPhaseTaskFrontmatter,
|
|
231
|
+
ReviewTaskFrontmatter,
|
|
170
232
|
)
|
|
171
233
|
|
|
172
234
|
export const PhaseFrontmatter = Schema.Struct({
|
|
@@ -187,6 +249,9 @@ export type RepositoryDeclaration = Schema.Schema.Type<
|
|
|
187
249
|
export type WorkStatus = Schema.Schema.Type<typeof WorkStatus>
|
|
188
250
|
export type ClaimRecord = Schema.Schema.Type<typeof ClaimRecord>
|
|
189
251
|
export type PullRequestRecord = Schema.Schema.Type<typeof PullRequestRecord>
|
|
252
|
+
export type ReviewSource = Schema.Schema.Type<typeof ReviewSource>
|
|
253
|
+
export type ReviewRecord = Schema.Schema.Type<typeof ReviewRecord>
|
|
254
|
+
export type CompletionRecord = Schema.Schema.Type<typeof CompletionRecord>
|
|
190
255
|
export type EpicFrontmatter = Schema.Schema.Type<typeof EpicFrontmatter>
|
|
191
256
|
export type TaskFrontmatter = Schema.Schema.Type<typeof TaskFrontmatter>
|
|
192
257
|
export type PhaseFrontmatter = Schema.Schema.Type<typeof PhaseFrontmatter>
|