@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
|
@@ -315,7 +315,7 @@ describe("ArchiveService", () => {
|
|
|
315
315
|
),
|
|
316
316
|
),
|
|
317
317
|
)
|
|
318
|
-
await Bun.write(join(workspace.writablePath
|
|
318
|
+
await Bun.write(join(workspace.writablePath!, "dirty.txt"), "keep me\n")
|
|
319
319
|
|
|
320
320
|
await expect(
|
|
321
321
|
runTestEffect(
|
|
@@ -747,7 +747,7 @@ describe("ArchiveService", () => {
|
|
|
747
747
|
),
|
|
748
748
|
).rejects.toThrow("Another archive or restore operation")
|
|
749
749
|
expect(
|
|
750
|
-
await Bun.file(join(workspace.writablePath
|
|
750
|
+
await Bun.file(join(workspace.writablePath!, "README.md")).exists(),
|
|
751
751
|
).toBe(true)
|
|
752
752
|
expect(await Bun.file(join(root, "tasks/locked/TASK.md")).exists()).toBe(
|
|
753
753
|
true,
|
|
@@ -147,6 +147,65 @@ describe("claim service", () => {
|
|
|
147
147
|
})
|
|
148
148
|
})
|
|
149
149
|
|
|
150
|
+
test("finishes claimed non-PR work with durable completion evidence", async () => {
|
|
151
|
+
const initial = await inspect()
|
|
152
|
+
const acquired = await claim(initial.revision)
|
|
153
|
+
const finished = await runTestEffect(
|
|
154
|
+
ClaimService.pipe(
|
|
155
|
+
Effect.flatMap((service) =>
|
|
156
|
+
service.finish(
|
|
157
|
+
{
|
|
158
|
+
taskId: "single",
|
|
159
|
+
sessionId: "session-1",
|
|
160
|
+
revision: acquired.revision,
|
|
161
|
+
outcome: "done",
|
|
162
|
+
nonPrCompletion: {
|
|
163
|
+
summary: "Review completed; no code change was needed.",
|
|
164
|
+
evidenceUrl: "https://example.com/review",
|
|
165
|
+
},
|
|
166
|
+
now: at("2026-07-17T12:45:00.000Z"),
|
|
167
|
+
},
|
|
168
|
+
root,
|
|
169
|
+
),
|
|
170
|
+
),
|
|
171
|
+
),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
expect(finished.data).toMatchObject({
|
|
175
|
+
status: "done",
|
|
176
|
+
claim: { state: "finished", outcome: "done" },
|
|
177
|
+
completion: {
|
|
178
|
+
mode: "non-pr",
|
|
179
|
+
completedAt: "2026-07-17T12:45:00.000Z",
|
|
180
|
+
summary: "Review completed; no code change was needed.",
|
|
181
|
+
evidenceUrl: "https://example.com/review",
|
|
182
|
+
},
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
test("rejects invalid claimed non-PR completion", async () => {
|
|
187
|
+
const initial = await inspect()
|
|
188
|
+
const acquired = await claim(initial.revision)
|
|
189
|
+
await expect(
|
|
190
|
+
runTestEffect(
|
|
191
|
+
ClaimService.pipe(
|
|
192
|
+
Effect.flatMap((service) =>
|
|
193
|
+
service.finish(
|
|
194
|
+
{
|
|
195
|
+
taskId: "single",
|
|
196
|
+
sessionId: "session-1",
|
|
197
|
+
revision: acquired.revision,
|
|
198
|
+
outcome: "done",
|
|
199
|
+
nonPrCompletion: { summary: " " },
|
|
200
|
+
},
|
|
201
|
+
root,
|
|
202
|
+
),
|
|
203
|
+
),
|
|
204
|
+
),
|
|
205
|
+
),
|
|
206
|
+
).rejects.toThrow("summary must not be empty")
|
|
207
|
+
})
|
|
208
|
+
|
|
150
209
|
test("returns structured ownership and revision conflicts", async () => {
|
|
151
210
|
const initial = await inspect()
|
|
152
211
|
const acquired = await claim(initial.revision)
|
|
@@ -31,6 +31,10 @@ import {
|
|
|
31
31
|
type PhaseFrontmatter as PhaseData,
|
|
32
32
|
type TaskFrontmatter as TaskData,
|
|
33
33
|
} from "../workbase/schemas"
|
|
34
|
+
import {
|
|
35
|
+
buildNonPrCompletion,
|
|
36
|
+
type NonPrCompletionInput,
|
|
37
|
+
} from "../workbase/completion"
|
|
34
38
|
|
|
35
39
|
class ClaimError extends Data.TaggedError("ClaimError")<{
|
|
36
40
|
readonly message: string
|
|
@@ -83,6 +87,7 @@ interface OwnedClaimInput {
|
|
|
83
87
|
|
|
84
88
|
interface FinishInput extends OwnedClaimInput {
|
|
85
89
|
readonly outcome: "done" | "dropped"
|
|
90
|
+
readonly nonPrCompletion?: NonPrCompletionInput
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
interface ExpireClaimInput {
|
|
@@ -103,7 +108,8 @@ interface ReconcileInput {
|
|
|
103
108
|
const PR_URL = /^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+\/?$/
|
|
104
109
|
|
|
105
110
|
type SingleTaskData = Extract<TaskData, { readonly repo: string }>
|
|
106
|
-
type
|
|
111
|
+
type ReviewTaskData = Extract<TaskData, { readonly review: unknown }>
|
|
112
|
+
type ExecutionData = SingleTaskData | ReviewTaskData | PhaseData
|
|
107
113
|
|
|
108
114
|
const isTaggedClaimError = (
|
|
109
115
|
error: unknown,
|
|
@@ -507,6 +513,11 @@ export class ClaimService extends Effect.Service<ClaimService>()(
|
|
|
507
513
|
finish: (input: FinishInput, startPath: string = process.cwd()) =>
|
|
508
514
|
Effect.gen(function* () {
|
|
509
515
|
assertIdentity("Session ID", input.sessionId)
|
|
516
|
+
if (input.nonPrCompletion && input.outcome !== "done") {
|
|
517
|
+
return yield* new ClaimError({
|
|
518
|
+
message: "Non-PR completion is valid only with a done outcome",
|
|
519
|
+
})
|
|
520
|
+
}
|
|
510
521
|
const service = yield* ClaimService
|
|
511
522
|
const inspected = yield* service.inspect(
|
|
512
523
|
input.taskId,
|
|
@@ -531,6 +542,22 @@ export class ClaimService extends Effect.Service<ClaimService>()(
|
|
|
531
542
|
message: `Session '${input.sessionId}' does not own ${inspected.target.label}`,
|
|
532
543
|
})
|
|
533
544
|
}
|
|
545
|
+
if (input.nonPrCompletion && "pr" in data && data.pr !== null) {
|
|
546
|
+
throw new ClaimError({
|
|
547
|
+
target: inspected.target.label,
|
|
548
|
+
message:
|
|
549
|
+
"Cannot complete without a pull request while an authoritative pull request is recorded",
|
|
550
|
+
})
|
|
551
|
+
}
|
|
552
|
+
const completionResult = input.nonPrCompletion
|
|
553
|
+
? buildNonPrCompletion(input.nonPrCompletion, now)
|
|
554
|
+
: undefined
|
|
555
|
+
if (completionResult && "error" in completionResult) {
|
|
556
|
+
throw new ClaimError({
|
|
557
|
+
target: inspected.target.label,
|
|
558
|
+
message: completionResult.error,
|
|
559
|
+
})
|
|
560
|
+
}
|
|
534
561
|
const claim: ClaimRecord = {
|
|
535
562
|
...data.claim,
|
|
536
563
|
state: "finished",
|
|
@@ -540,8 +567,15 @@ export class ClaimService extends Effect.Service<ClaimService>()(
|
|
|
540
567
|
return {
|
|
541
568
|
data: {
|
|
542
569
|
...data,
|
|
543
|
-
status:
|
|
570
|
+
status: completionResult
|
|
571
|
+
? "done"
|
|
572
|
+
: input.outcome === "done"
|
|
573
|
+
? "working"
|
|
574
|
+
: "dropped",
|
|
544
575
|
claim,
|
|
576
|
+
...(completionResult
|
|
577
|
+
? { completion: completionResult.value }
|
|
578
|
+
: {}),
|
|
545
579
|
},
|
|
546
580
|
claim,
|
|
547
581
|
}
|
|
@@ -54,6 +54,7 @@ interface CheckoutInspection {
|
|
|
54
54
|
readonly checkoutCommit: string | null
|
|
55
55
|
readonly checkoutBranch: string | null
|
|
56
56
|
readonly detached: boolean | null
|
|
57
|
+
readonly dirty: boolean | null
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
type ExecutionData = PhaseData & Partial<Pick<TaskData, "ticketUrl" | "epic">>
|
|
@@ -92,6 +93,18 @@ const runGit = (fs: FileSystemService, cwd: string, args: readonly string[]) =>
|
|
|
92
93
|
Effect.catchAll(() => Effect.succeed(null)),
|
|
93
94
|
)
|
|
94
95
|
|
|
96
|
+
const runGitText = (
|
|
97
|
+
fs: FileSystemService,
|
|
98
|
+
cwd: string,
|
|
99
|
+
args: readonly string[],
|
|
100
|
+
) =>
|
|
101
|
+
fs.runCommand(["git", "-C", cwd, ...args], { captureOutput: true }).pipe(
|
|
102
|
+
Effect.map((result) =>
|
|
103
|
+
result.exitCode === 0 ? result.stdout.trim() : null,
|
|
104
|
+
),
|
|
105
|
+
Effect.catchAll(() => Effect.succeed(null)),
|
|
106
|
+
)
|
|
107
|
+
|
|
95
108
|
const worktreePaths = (output: string | null) =>
|
|
96
109
|
new Set(
|
|
97
110
|
(output ?? "")
|
|
@@ -765,9 +778,13 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
765
778
|
: task?.data && "repo" in task.data
|
|
766
779
|
? (task.data as ExecutionData)
|
|
767
780
|
: null
|
|
768
|
-
const
|
|
769
|
-
?
|
|
770
|
-
|
|
781
|
+
const reviewData =
|
|
782
|
+
task?.data && "review" in task.data ? task.data.review : null
|
|
783
|
+
const references: readonly RepositoryReference[] = reviewData
|
|
784
|
+
? [{ repo: reviewData.repo, ref: reviewData.commit }]
|
|
785
|
+
: executionData
|
|
786
|
+
? (executionData.repos ?? [])
|
|
787
|
+
: (epic?.data.repos ?? [])
|
|
771
788
|
const entityDirectory = target.path.replace(
|
|
772
789
|
/\/(?:EPIC|TASK|PHASE)\.md$/,
|
|
773
790
|
"",
|
|
@@ -812,6 +829,7 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
812
829
|
checkoutCommit: null,
|
|
813
830
|
checkoutBranch: null,
|
|
814
831
|
detached: null,
|
|
832
|
+
dirty: null,
|
|
815
833
|
}
|
|
816
834
|
}
|
|
817
835
|
const checkoutCommit = yield* runGit(fs, checkoutPath, [
|
|
@@ -824,6 +842,10 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
824
842
|
"--short",
|
|
825
843
|
"HEAD",
|
|
826
844
|
])
|
|
845
|
+
const checkoutStatus = yield* runGitText(fs, checkoutPath, [
|
|
846
|
+
"status",
|
|
847
|
+
"--porcelain",
|
|
848
|
+
])
|
|
827
849
|
const resolvedCheckoutPath = yield* fs.realPath(checkoutPath)
|
|
828
850
|
registered = registered || listedPaths.has(resolvedCheckoutPath)
|
|
829
851
|
if (checkoutCommit === null) {
|
|
@@ -837,6 +859,8 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
837
859
|
checkoutCommit,
|
|
838
860
|
checkoutBranch,
|
|
839
861
|
detached: checkoutBranch === null,
|
|
862
|
+
dirty:
|
|
863
|
+
checkoutStatus === null ? null : checkoutStatus.length > 0,
|
|
840
864
|
}
|
|
841
865
|
})
|
|
842
866
|
|
|
@@ -901,6 +925,17 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
901
925
|
...(yield* inspectCheckout(repositoryPath, checkoutPath)),
|
|
902
926
|
})
|
|
903
927
|
}
|
|
928
|
+
const reviewSourceCommit = reviewData
|
|
929
|
+
? yield* runGit(fs, join(root, "repos", reviewData.repo), [
|
|
930
|
+
"ls-remote",
|
|
931
|
+
"origin",
|
|
932
|
+
reviewData.source.kind === "pull-request"
|
|
933
|
+
? reviewData.source.fetchRef
|
|
934
|
+
: reviewData.source.ref
|
|
935
|
+
.replace(/^refs\/remotes\/origin\//, "")
|
|
936
|
+
.replace(/^origin\//, ""),
|
|
937
|
+
])
|
|
938
|
+
: null
|
|
904
939
|
|
|
905
940
|
const checkoutStates = [writable, ...referenceCheckouts].filter(
|
|
906
941
|
(value): value is NonNullable<typeof value> => value !== null,
|
|
@@ -956,7 +991,11 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
956
991
|
aggregate: aggregateProgress(aggregateStatuses),
|
|
957
992
|
},
|
|
958
993
|
authority: {
|
|
959
|
-
mode:
|
|
994
|
+
mode: reviewData
|
|
995
|
+
? "review"
|
|
996
|
+
: executionData
|
|
997
|
+
? "execution"
|
|
998
|
+
: "orchestration",
|
|
960
999
|
writable: writable
|
|
961
1000
|
? {
|
|
962
1001
|
repo: writable.repo,
|
|
@@ -972,6 +1011,9 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
972
1011
|
repositoryPath: reference.repositoryPath,
|
|
973
1012
|
checkoutPath: reference.checkoutPath,
|
|
974
1013
|
})),
|
|
1014
|
+
documents: {
|
|
1015
|
+
writable: reviewData && task ? [task.path] : [],
|
|
1016
|
+
},
|
|
975
1017
|
},
|
|
976
1018
|
workspace: options.compact
|
|
977
1019
|
? {
|
|
@@ -997,6 +1039,16 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
997
1039
|
references: referenceCheckouts,
|
|
998
1040
|
warnings: inspectionWarnings,
|
|
999
1041
|
},
|
|
1042
|
+
review: reviewData
|
|
1043
|
+
? {
|
|
1044
|
+
...reviewData,
|
|
1045
|
+
sourceObservation: {
|
|
1046
|
+
available: reviewSourceCommit !== null,
|
|
1047
|
+
commit: reviewSourceCommit?.split(/\s+/)[0] ?? null,
|
|
1048
|
+
},
|
|
1049
|
+
checkout: referenceCheckouts[0] ?? null,
|
|
1050
|
+
}
|
|
1051
|
+
: null,
|
|
1000
1052
|
pr: executionData?.pr
|
|
1001
1053
|
? normalizePullRequestRecord(executionData.pr)
|
|
1002
1054
|
: { url: null, state: "none" },
|
|
@@ -282,6 +282,8 @@ export class DoctorService extends Effect.Service<DoctorService>()(
|
|
|
282
282
|
}
|
|
283
283
|
|
|
284
284
|
const refs = new Map<string, Set<string>>()
|
|
285
|
+
const reviewSources: { repo: string; task: string; ref: string }[] =
|
|
286
|
+
[]
|
|
285
287
|
const declareRef = (repo: string, ref: string) => {
|
|
286
288
|
const values = refs.get(repo) ?? new Set<string>()
|
|
287
289
|
values.add(ref)
|
|
@@ -293,7 +295,19 @@ export class DoctorService extends Effect.Service<DoctorService>()(
|
|
|
293
295
|
declareRef(reference.repo, reference.ref)
|
|
294
296
|
}
|
|
295
297
|
for (const task of yield* tasks.list(root)) {
|
|
296
|
-
if ("
|
|
298
|
+
if ("review" in task.data) {
|
|
299
|
+
declareRef(task.data.review.repo, task.data.review.commit)
|
|
300
|
+
reviewSources.push({
|
|
301
|
+
repo: task.data.review.repo,
|
|
302
|
+
task: task.id,
|
|
303
|
+
ref:
|
|
304
|
+
task.data.review.source.kind === "pull-request"
|
|
305
|
+
? task.data.review.source.fetchRef
|
|
306
|
+
: task.data.review.source.ref
|
|
307
|
+
.replace(/^refs\/remotes\/origin\//, "")
|
|
308
|
+
.replace(/^origin\//, ""),
|
|
309
|
+
})
|
|
310
|
+
} else if ("repo" in task.data) {
|
|
297
311
|
declareRef(task.data.repo, task.data.base)
|
|
298
312
|
for (const reference of task.data.repos ?? [])
|
|
299
313
|
declareRef(reference.repo, reference.ref)
|
|
@@ -357,6 +371,36 @@ export class DoctorService extends Effect.Service<DoctorService>()(
|
|
|
357
371
|
})
|
|
358
372
|
if (!repositoryValid) continue
|
|
359
373
|
|
|
374
|
+
for (const source of reviewSources.filter(
|
|
375
|
+
(item) => item.repo === repository.alias,
|
|
376
|
+
)) {
|
|
377
|
+
const observed = yield* fs.runCommand(
|
|
378
|
+
[
|
|
379
|
+
"git",
|
|
380
|
+
"-C",
|
|
381
|
+
repository.path,
|
|
382
|
+
"ls-remote",
|
|
383
|
+
"origin",
|
|
384
|
+
source.ref,
|
|
385
|
+
],
|
|
386
|
+
{ captureOutput: true },
|
|
387
|
+
)
|
|
388
|
+
const available =
|
|
389
|
+
observed.exitCode === 0 && Boolean(observed.stdout.trim())
|
|
390
|
+
add({
|
|
391
|
+
id: `review.${source.task}.source`,
|
|
392
|
+
category: "repository",
|
|
393
|
+
level: "warning",
|
|
394
|
+
status: available ? "pass" : "fail",
|
|
395
|
+
message: available
|
|
396
|
+
? `Review source '${source.ref}' is available for task '${source.task}'`
|
|
397
|
+
: `Review source '${source.ref}' is unavailable for task '${source.task}'; its pin remains usable`,
|
|
398
|
+
remediation: available
|
|
399
|
+
? undefined
|
|
400
|
+
: "Use the pinned checkout or refresh after restoring the source.",
|
|
401
|
+
})
|
|
402
|
+
}
|
|
403
|
+
|
|
360
404
|
for (const ref of [...(refs.get(repository.alias) ?? [])].sort()) {
|
|
361
405
|
const local = yield* fs.runCommand(
|
|
362
406
|
[
|
|
@@ -360,6 +360,11 @@ export class GraphMutationService extends Effect.Service<GraphMutationService>()
|
|
|
360
360
|
"base",
|
|
361
361
|
"pr",
|
|
362
362
|
].some((key) => updates[key as keyof TaskUpdates] !== undefined)
|
|
363
|
+
if ("review" in record.data && executionChange) {
|
|
364
|
+
return yield* new GraphMutationError({
|
|
365
|
+
message: `Review task '${id}' does not have writable execution metadata`,
|
|
366
|
+
})
|
|
367
|
+
}
|
|
363
368
|
if ("phases" in record.data && executionChange) {
|
|
364
369
|
return yield* new GraphMutationError({
|
|
365
370
|
message: `Task '${id}' has multiple phases; update execution metadata on a phase instead`,
|
|
@@ -374,6 +379,16 @@ export class GraphMutationService extends Effect.Service<GraphMutationService>()
|
|
|
374
379
|
message: `Task '${id}' has an active claim; release or finish it before changing execution metadata`,
|
|
375
380
|
})
|
|
376
381
|
}
|
|
382
|
+
if (
|
|
383
|
+
updates.pr &&
|
|
384
|
+
"completion" in record.data &&
|
|
385
|
+
record.data.completion
|
|
386
|
+
) {
|
|
387
|
+
return yield* new GraphMutationError({
|
|
388
|
+
message:
|
|
389
|
+
"Reopen non-PR completed work before recording a pull request",
|
|
390
|
+
})
|
|
391
|
+
}
|
|
377
392
|
if (
|
|
378
393
|
executionChange &&
|
|
379
394
|
(yield* fs.isDirectory(join(dirname(record.path), "code")))
|
|
@@ -504,6 +519,12 @@ export class GraphMutationService extends Effect.Service<GraphMutationService>()
|
|
|
504
519
|
message: `Phase '${id}' has an active claim; release or finish it before changing execution metadata`,
|
|
505
520
|
})
|
|
506
521
|
}
|
|
522
|
+
if (updates.pr && record.data.completion) {
|
|
523
|
+
return yield* new GraphMutationError({
|
|
524
|
+
message:
|
|
525
|
+
"Reopen non-PR completed work before recording a pull request",
|
|
526
|
+
})
|
|
527
|
+
}
|
|
507
528
|
const data: PhaseData = yield* decode(
|
|
508
529
|
PhaseFrontmatter,
|
|
509
530
|
{
|
|
@@ -54,7 +54,10 @@ interface RepositoryRecord {
|
|
|
54
54
|
readonly target: string | null
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
type ExecutionData =
|
|
57
|
+
type ExecutionData =
|
|
58
|
+
| PhaseData
|
|
59
|
+
| Extract<TaskData, { readonly repo: string }>
|
|
60
|
+
| Extract<TaskData, { readonly review: unknown }>
|
|
58
61
|
|
|
59
62
|
export interface GraphOptions {
|
|
60
63
|
readonly cwd?: string
|
|
@@ -548,10 +551,18 @@ export class GraphService extends Effect.Service<GraphService>()(
|
|
|
548
551
|
const executionRepositories = (
|
|
549
552
|
data: TaskData | PhaseData,
|
|
550
553
|
): readonly string[] =>
|
|
551
|
-
"
|
|
552
|
-
? [data.
|
|
553
|
-
:
|
|
554
|
+
"review" in data
|
|
555
|
+
? [data.review.repo]
|
|
556
|
+
: "repo" in data
|
|
557
|
+
? [data.repo, ...(data.repos ?? []).map((item) => item.repo)]
|
|
558
|
+
: []
|
|
554
559
|
const executionEdges = (id: string, data: TaskData | PhaseData) => {
|
|
560
|
+
if ("review" in data) {
|
|
561
|
+
edges.push(
|
|
562
|
+
edge("references", id, repositoryNodeId(data.review.repo)),
|
|
563
|
+
)
|
|
564
|
+
return
|
|
565
|
+
}
|
|
555
566
|
if (!("repo" in data)) return
|
|
556
567
|
edges.push(edge("writes", id, repositoryNodeId(data.repo)))
|
|
557
568
|
for (const reference of data.repos ?? []) {
|
|
@@ -650,8 +661,9 @@ export class GraphService extends Effect.Service<GraphService>()(
|
|
|
650
661
|
const executionDetails = (entityPath: string, data: ExecutionData) =>
|
|
651
662
|
Effect.gen(function* () {
|
|
652
663
|
const directory = entityPath.replace(/\/(?:TASK|PHASE)\.md$/, "")
|
|
653
|
-
const
|
|
654
|
-
const
|
|
664
|
+
const repo = "review" in data ? data.review.repo : data.repo
|
|
665
|
+
const checkoutPath = join(directory, "code", repo)
|
|
666
|
+
const repositoryPath = join(root, "repos", repo)
|
|
655
667
|
const materialized = yield* fs.isDirectory(checkoutPath)
|
|
656
668
|
const result: {
|
|
657
669
|
workspace?: GraphExecutionWorkspace
|
|
@@ -666,58 +678,110 @@ export class GraphService extends Effect.Service<GraphService>()(
|
|
|
666
678
|
}
|
|
667
679
|
}
|
|
668
680
|
if (include.has("git")) {
|
|
669
|
-
result.git =
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
681
|
+
result.git =
|
|
682
|
+
"review" in data
|
|
683
|
+
? {
|
|
684
|
+
pinnedCommit: data.review.commit,
|
|
685
|
+
sourceCommit: ((value) =>
|
|
686
|
+
value?.split(/\s+/)[0] ?? null)(
|
|
687
|
+
yield* run(fs, [
|
|
688
|
+
"git",
|
|
689
|
+
"-C",
|
|
690
|
+
repositoryPath,
|
|
691
|
+
"ls-remote",
|
|
692
|
+
"origin",
|
|
693
|
+
data.review.source.kind === "pull-request"
|
|
694
|
+
? data.review.source.fetchRef
|
|
695
|
+
: data.review.source.ref
|
|
696
|
+
.replace(/^refs\/remotes\/origin\//, "")
|
|
697
|
+
.replace(/^origin\//, ""),
|
|
698
|
+
]),
|
|
699
|
+
),
|
|
700
|
+
checkoutCommit: materialized
|
|
701
|
+
? yield* run(fs, [
|
|
702
|
+
"git",
|
|
703
|
+
"-C",
|
|
704
|
+
checkoutPath,
|
|
705
|
+
"rev-parse",
|
|
706
|
+
"HEAD",
|
|
707
|
+
])
|
|
708
|
+
: null,
|
|
709
|
+
checkoutBranch: materialized
|
|
710
|
+
? yield* run(fs, [
|
|
711
|
+
"git",
|
|
712
|
+
"-C",
|
|
713
|
+
checkoutPath,
|
|
714
|
+
"symbolic-ref",
|
|
715
|
+
"--quiet",
|
|
716
|
+
"--short",
|
|
717
|
+
"HEAD",
|
|
718
|
+
])
|
|
719
|
+
: null,
|
|
720
|
+
dirty: materialized
|
|
721
|
+
? ((status) =>
|
|
722
|
+
status === null ? null : status.length > 0)(
|
|
723
|
+
yield* runText(fs, [
|
|
724
|
+
"git",
|
|
725
|
+
"-C",
|
|
726
|
+
checkoutPath,
|
|
727
|
+
"status",
|
|
728
|
+
"--porcelain",
|
|
729
|
+
]),
|
|
730
|
+
)
|
|
731
|
+
: null,
|
|
732
|
+
}
|
|
733
|
+
: {
|
|
734
|
+
branch: data.branch,
|
|
735
|
+
base: data.base,
|
|
736
|
+
branchCommit: yield* run(fs, [
|
|
710
737
|
"git",
|
|
711
738
|
"-C",
|
|
712
|
-
|
|
713
|
-
"
|
|
714
|
-
|
|
739
|
+
repositoryPath,
|
|
740
|
+
"rev-parse",
|
|
741
|
+
`${data.branch}^{commit}`,
|
|
715
742
|
]),
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
743
|
+
baseCommit: yield* run(fs, [
|
|
744
|
+
"git",
|
|
745
|
+
"-C",
|
|
746
|
+
repositoryPath,
|
|
747
|
+
"rev-parse",
|
|
748
|
+
`${data.base}^{commit}`,
|
|
749
|
+
]),
|
|
750
|
+
checkoutCommit: materialized
|
|
751
|
+
? yield* run(fs, [
|
|
752
|
+
"git",
|
|
753
|
+
"-C",
|
|
754
|
+
checkoutPath,
|
|
755
|
+
"rev-parse",
|
|
756
|
+
"HEAD",
|
|
757
|
+
])
|
|
758
|
+
: null,
|
|
759
|
+
checkoutBranch: materialized
|
|
760
|
+
? yield* run(fs, [
|
|
761
|
+
"git",
|
|
762
|
+
"-C",
|
|
763
|
+
checkoutPath,
|
|
764
|
+
"symbolic-ref",
|
|
765
|
+
"--quiet",
|
|
766
|
+
"--short",
|
|
767
|
+
"HEAD",
|
|
768
|
+
])
|
|
769
|
+
: null,
|
|
770
|
+
dirty: materialized
|
|
771
|
+
? ((status) =>
|
|
772
|
+
status === null ? null : status.length > 0)(
|
|
773
|
+
yield* runText(fs, [
|
|
774
|
+
"git",
|
|
775
|
+
"-C",
|
|
776
|
+
checkoutPath,
|
|
777
|
+
"status",
|
|
778
|
+
"--porcelain",
|
|
779
|
+
]),
|
|
780
|
+
)
|
|
781
|
+
: null,
|
|
782
|
+
}
|
|
719
783
|
}
|
|
720
|
-
if (include.has("pr")) {
|
|
784
|
+
if (include.has("pr") && !("review" in data)) {
|
|
721
785
|
if (!data.pr) {
|
|
722
786
|
result.pr = { url: null, state: "none" }
|
|
723
787
|
} else {
|
|
@@ -261,7 +261,9 @@ describe("IntegrationService", () => {
|
|
|
261
261
|
expect(body).toContain("Only `done` satisfies a dependency")
|
|
262
262
|
expect(body).toContain("Require explicit user intent")
|
|
263
263
|
expect(body).toContain("changing repository")
|
|
264
|
-
expect(body).toMatch(
|
|
264
|
+
expect(body).toMatch(
|
|
265
|
+
/archiving, restoring,\s+dropping, reopening, or completing work without a pull request/,
|
|
266
|
+
)
|
|
265
267
|
expect(body).toContain("Never invent entity IDs")
|
|
266
268
|
expect(body).toContain("Preserve parent backlinks")
|
|
267
269
|
expect(body).toContain("dirty-worktree, active-claim, revision")
|
|
@@ -281,6 +283,7 @@ describe("IntegrationService", () => {
|
|
|
281
283
|
expect(body).toContain("pausing or handing off")
|
|
282
284
|
expect(body).toContain("`agency finish`")
|
|
283
285
|
expect(body).toContain("`agency sync --apply`")
|
|
286
|
+
expect(body).toContain("`--no-pull-request --summary <text>`")
|
|
284
287
|
expect(body).toContain("`TASK.md` or `PHASE.md`")
|
|
285
288
|
expect(body).toContain("PR state, current head, diff summary")
|
|
286
289
|
expect(body).toContain("Run `agency validate` before reporting completion")
|