@markjaquith/agency 3.13.0 → 3.15.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 +9 -3
- package/cli-main.ts +1 -1
- package/package.json +1 -1
- package/src/cli-parser.ts +8 -1
- package/src/commands/act-actions.ts +42 -9
- package/src/commands/review.ts +28 -7
- package/src/commands/sync.ts +2 -1
- package/src/services/ReviewService.ts +45 -0
- package/src/services/SyncService.ts +189 -26
- package/src/services/TaskService.ts +9 -0
- package/src/workbase/AGENTS.md +7 -3
package/README.md
CHANGED
|
@@ -710,7 +710,7 @@ external chooser continues to offer goals and **Browse items** through that choo
|
|
|
710
710
|
| Split work | Add a phase / split this task; name the existing work's first phase | `split` |
|
|
711
711
|
| Work on a task or phase | Work on this item | `work` |
|
|
712
712
|
| Move from investigation to execution | Create implementation follow-up | `handoff` |
|
|
713
|
-
| Review someone else's work | Review a PR, or a remote branch/commit
|
|
713
|
+
| Review someone else's work | Review a PR, or a remote branch/commit; finish a review | `review`, `review-ref`, `finish-review` |
|
|
714
714
|
| Publish or update PR status | Push without a PR, refresh provider state, create a PR, mark a GitHub PR ready, or close it | `push`, `sync`, `pr`, `pr-ready`, `pr-close` |
|
|
715
715
|
| Maintain review work | Fetch and repin a review task to its current source | `review-refresh` |
|
|
716
716
|
| Organize work | Rename an item, move a task into/out of an epic, or add/remove a sibling dependency | `rename`, `move-to-epic`, `remove-from-epic`, `dependency-add`, `dependency-remove` |
|
|
@@ -723,7 +723,9 @@ on the new item now** or **Finish**. Only choosing Work prepares checkouts and
|
|
|
723
723
|
launches the configured runner; creation alone leaves the item for later.
|
|
724
724
|
Investigation handoff creates a distinct implementation item with source and
|
|
725
725
|
revision provenance. Completing a non-PR outcome requires a durable summary.
|
|
726
|
-
|
|
726
|
+
**Finish review** marks a review task done without a summary; review tasks offer
|
|
727
|
+
it instead of non-PR completion. PR-backed completion comes from provider
|
|
728
|
+
reconciliation after merge.
|
|
727
729
|
|
|
728
730
|
**Refresh Agency state** reads the provider and reconciles local records. The
|
|
729
731
|
explicit **GitHub PR ready/close** actions mutate the recorded GitHub URL, then
|
|
@@ -1060,6 +1062,7 @@ Create a pinned, read-only review task from the selected alias's origin:
|
|
|
1060
1062
|
agency task create <id> --review <alias> --pull-request <url-or-number>
|
|
1061
1063
|
agency task create <id> --review <alias> --ref <remote-ref>
|
|
1062
1064
|
agency review refresh <id> [--if-revision <hash>] [--json]
|
|
1065
|
+
agency review finish <id> [--if-revision <hash>] [--json]
|
|
1063
1066
|
```
|
|
1064
1067
|
|
|
1065
1068
|
Review creation fetches the source and records its exact 40-character commit.
|
|
@@ -1069,7 +1072,10 @@ detached checkout and no writable branch. Source movement is observed separately
|
|
|
1069
1072
|
from the pin and applied only by `review refresh`; sync, doctor, work, cleanup,
|
|
1070
1073
|
and archive never move the pin implicitly. Dirty or structurally unexpected
|
|
1071
1074
|
review checkouts block refresh and cleanup. Review tasks support the normal status
|
|
1072
|
-
lifecycle, but reject phase conversion and delivery PR operations.
|
|
1075
|
+
lifecycle, but reject phase conversion and delivery PR operations. Because a
|
|
1076
|
+
review has no delivery pull request, `review finish` marks an open, working, or
|
|
1077
|
+
delegated review done without the `--no-pull-request --summary` requirement
|
|
1078
|
+
that applies to other work.
|
|
1073
1079
|
Each active or archived review task owns one internal task-scoped pin ref. A
|
|
1074
1080
|
refresh advances that ref transactionally; failed creation removes it. Archiving
|
|
1075
1081
|
retains the pin so a deleted source can still be restored and inspected.
|
package/cli-main.ts
CHANGED
|
@@ -726,7 +726,7 @@ Commands:
|
|
|
726
726
|
next List or select ready execution units
|
|
727
727
|
pr create / pr [...] Create an Agency PR or run gh pr with repository focus
|
|
728
728
|
push Validate and publish the current execution unit
|
|
729
|
-
review refresh
|
|
729
|
+
review refresh|finish Refresh or finish a pinned review task
|
|
730
730
|
repo <subcommand> Manage workbase repositories
|
|
731
731
|
status Show status for the current workbase
|
|
732
732
|
doctor Diagnose workbase health and integrations
|
package/package.json
CHANGED
package/src/cli-parser.ts
CHANGED
|
@@ -783,7 +783,8 @@ const commands = {
|
|
|
783
783
|
},
|
|
784
784
|
},
|
|
785
785
|
review: {
|
|
786
|
-
usage:
|
|
786
|
+
usage:
|
|
787
|
+
"agency review <refresh|finish> <task-id> [--if-revision <hash>] [--json]",
|
|
787
788
|
options: {
|
|
788
789
|
...outputOptions,
|
|
789
790
|
"if-revision": { type: "string" },
|
|
@@ -796,6 +797,12 @@ const commands = {
|
|
|
796
797
|
maxArgs: 1,
|
|
797
798
|
options: ["if-revision", "json"],
|
|
798
799
|
},
|
|
800
|
+
finish: {
|
|
801
|
+
usage: "agency review finish <task-id> [--if-revision <hash>] [--json]",
|
|
802
|
+
minArgs: 1,
|
|
803
|
+
maxArgs: 1,
|
|
804
|
+
options: ["if-revision", "json"],
|
|
805
|
+
},
|
|
799
806
|
},
|
|
800
807
|
},
|
|
801
808
|
worktree: {
|
|
@@ -234,6 +234,11 @@ const actionPresentation: Record<
|
|
|
234
234
|
icon: "",
|
|
235
235
|
color: macchiato.sapphire,
|
|
236
236
|
},
|
|
237
|
+
"finish-review": {
|
|
238
|
+
description: "Mark this review done; no completion summary is required.",
|
|
239
|
+
icon: "",
|
|
240
|
+
color: macchiato.green,
|
|
241
|
+
},
|
|
237
242
|
reopen: {
|
|
238
243
|
description: "Return terminal work to open status.",
|
|
239
244
|
icon: "",
|
|
@@ -371,7 +376,7 @@ export const actionGroups = [
|
|
|
371
376
|
id: "review",
|
|
372
377
|
label: "Review someone else's work",
|
|
373
378
|
icon: "",
|
|
374
|
-
actions: ["review", "review-ref"],
|
|
379
|
+
actions: ["review", "review-ref", "finish-review"],
|
|
375
380
|
},
|
|
376
381
|
{
|
|
377
382
|
id: "split",
|
|
@@ -875,6 +880,7 @@ export const actActions = (
|
|
|
875
880
|
const purpose =
|
|
876
881
|
parent && "purpose" in parent.data ? parent.data.purpose : undefined
|
|
877
882
|
const terminal = node.status === "done" || node.status === "dropped"
|
|
883
|
+
const isReview = node.kind === "task" && "review" in node.data
|
|
878
884
|
const blockers = (execution ?? node).readiness.blockers
|
|
879
885
|
const validation = blockers.find(
|
|
880
886
|
(blocker) => blocker.kind === "validation",
|
|
@@ -1044,9 +1050,7 @@ export const actActions = (
|
|
|
1044
1050
|
details(
|
|
1045
1051
|
"review-refresh",
|
|
1046
1052
|
"Refresh review source",
|
|
1047
|
-
|
|
1048
|
-
? null
|
|
1049
|
-
: "Select a review task",
|
|
1053
|
+
isReview ? null : "Select a review task",
|
|
1050
1054
|
),
|
|
1051
1055
|
{
|
|
1052
1056
|
command: [
|
|
@@ -1065,6 +1069,33 @@ export const actActions = (
|
|
|
1065
1069
|
}),
|
|
1066
1070
|
},
|
|
1067
1071
|
),
|
|
1072
|
+
immediate(
|
|
1073
|
+
details(
|
|
1074
|
+
"finish-review",
|
|
1075
|
+
"Finish review",
|
|
1076
|
+
!isReview
|
|
1077
|
+
? "Select a review task"
|
|
1078
|
+
: terminal
|
|
1079
|
+
? "Review is already terminal"
|
|
1080
|
+
: null,
|
|
1081
|
+
),
|
|
1082
|
+
{
|
|
1083
|
+
command: [
|
|
1084
|
+
"agency",
|
|
1085
|
+
"review",
|
|
1086
|
+
"finish",
|
|
1087
|
+
target.taskId,
|
|
1088
|
+
"--if-revision",
|
|
1089
|
+
node.data.sha256,
|
|
1090
|
+
],
|
|
1091
|
+
run: review({
|
|
1092
|
+
...options,
|
|
1093
|
+
subcommand: "finish",
|
|
1094
|
+
taskId: target.taskId,
|
|
1095
|
+
ifRevision: node.data.sha256,
|
|
1096
|
+
}),
|
|
1097
|
+
},
|
|
1098
|
+
),
|
|
1068
1099
|
...(["reopen", "drop"] as const).map((id) => {
|
|
1069
1100
|
const status = id === "reopen" ? "open" : "dropped"
|
|
1070
1101
|
return immediate(
|
|
@@ -1300,11 +1331,13 @@ export const actActions = (
|
|
|
1300
1331
|
"complete",
|
|
1301
1332
|
"Complete without a pull request",
|
|
1302
1333
|
noExecution ??
|
|
1303
|
-
(
|
|
1304
|
-
? "
|
|
1305
|
-
:
|
|
1306
|
-
? "
|
|
1307
|
-
:
|
|
1334
|
+
(isReview
|
|
1335
|
+
? "Use Finish review for review tasks"
|
|
1336
|
+
: terminal
|
|
1337
|
+
? "Item is already terminal"
|
|
1338
|
+
: prUrl
|
|
1339
|
+
? "Reconcile the recorded pull request instead"
|
|
1340
|
+
: null),
|
|
1308
1341
|
),
|
|
1309
1342
|
inputs: [
|
|
1310
1343
|
input("summary", "Completed outcome summary", { multiline: true }),
|
package/src/commands/review.ts
CHANGED
|
@@ -12,17 +12,34 @@ interface ReviewOptions extends BaseCommandOptions {
|
|
|
12
12
|
|
|
13
13
|
export const review = (options: ReviewOptions) =>
|
|
14
14
|
Effect.gen(function* () {
|
|
15
|
-
if (
|
|
15
|
+
if (
|
|
16
|
+
(options.subcommand !== "refresh" && options.subcommand !== "finish") ||
|
|
17
|
+
!options.taskId
|
|
18
|
+
) {
|
|
16
19
|
return yield* Effect.fail(
|
|
17
|
-
new Error("Usage: agency review refresh <task>"),
|
|
20
|
+
new Error("Usage: agency review <refresh|finish> <task>"),
|
|
18
21
|
)
|
|
19
22
|
}
|
|
20
|
-
const
|
|
23
|
+
const service = yield* ReviewService
|
|
24
|
+
const { log } = createLoggers(options)
|
|
25
|
+
if (options.subcommand === "finish") {
|
|
26
|
+
const result = yield* service.finish(
|
|
27
|
+
options.taskId,
|
|
28
|
+
options.cwd,
|
|
29
|
+
options.ifRevision,
|
|
30
|
+
)
|
|
31
|
+
log(
|
|
32
|
+
options.json
|
|
33
|
+
? JSON.stringify(result, null, 2)
|
|
34
|
+
: `Finished review '${options.taskId}'`,
|
|
35
|
+
)
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
const result = yield* service.refresh(
|
|
21
39
|
options.taskId,
|
|
22
40
|
options.cwd,
|
|
23
41
|
options.ifRevision,
|
|
24
42
|
)
|
|
25
|
-
const { log } = createLoggers(options)
|
|
26
43
|
log(
|
|
27
44
|
options.json
|
|
28
45
|
? JSON.stringify(result, null, 2)
|
|
@@ -31,8 +48,12 @@ export const review = (options: ReviewOptions) =>
|
|
|
31
48
|
})
|
|
32
49
|
|
|
33
50
|
export const help = `
|
|
34
|
-
Usage: agency review refresh <task-id> [--if-revision <hash>] [--json]
|
|
51
|
+
Usage: agency review <refresh|finish> <task-id> [--if-revision <hash>] [--json]
|
|
35
52
|
|
|
36
|
-
|
|
37
|
-
|
|
53
|
+
Subcommands:
|
|
54
|
+
refresh Fetch the review source explicitly and replace the pinned commit
|
|
55
|
+
and any clean, detached review checkout. Review sources never
|
|
56
|
+
move implicitly.
|
|
57
|
+
finish Mark an open, working, or delegated review task done. Reviews
|
|
58
|
+
have no delivery pull request and need no completion summary.
|
|
38
59
|
`
|
package/src/commands/sync.ts
CHANGED
|
@@ -138,6 +138,7 @@ Options:
|
|
|
138
138
|
|
|
139
139
|
Sync may materialize declared repositories and unambiguous missing checkouts,
|
|
140
140
|
adopt legacy repositories with portable origins, record a uniquely matched PR,
|
|
141
|
-
|
|
141
|
+
adopt a recorded PR's retargeted base or renamed head branch, and mark merged
|
|
142
|
+
work done. Dirty, stale, or
|
|
142
143
|
conflicting checkouts are always left unresolved.
|
|
143
144
|
`
|
|
@@ -424,6 +424,51 @@ export class ReviewService extends Effect.Service<ReviewService>()(
|
|
|
424
424
|
}),
|
|
425
425
|
)
|
|
426
426
|
}),
|
|
427
|
+
|
|
428
|
+
finish: (
|
|
429
|
+
taskId: string,
|
|
430
|
+
startPath: string = process.cwd(),
|
|
431
|
+
ifRevision?: string,
|
|
432
|
+
) =>
|
|
433
|
+
Effect.gen(function* () {
|
|
434
|
+
const workbase = yield* WorkbaseService
|
|
435
|
+
const tasks = yield* TaskService
|
|
436
|
+
const root = yield* workbase.discover(startPath)
|
|
437
|
+
const task = yield* tasks.show(taskId, root)
|
|
438
|
+
if (!("review" in task.data)) {
|
|
439
|
+
return yield* new ReviewError({
|
|
440
|
+
message: `Task '${taskId}' is not a review task`,
|
|
441
|
+
})
|
|
442
|
+
}
|
|
443
|
+
if (ifRevision && task.revision !== ifRevision) {
|
|
444
|
+
return yield* new RevisionConflictError({
|
|
445
|
+
path: task.path,
|
|
446
|
+
target: `task '${taskId}'`,
|
|
447
|
+
expectedRevision: ifRevision,
|
|
448
|
+
currentRevision: task.revision,
|
|
449
|
+
message: `Revision conflict for task '${taskId}'`,
|
|
450
|
+
})
|
|
451
|
+
}
|
|
452
|
+
if (task.data.status === "done" || task.data.status === "dropped") {
|
|
453
|
+
return yield* new ReviewError({
|
|
454
|
+
message: `Cannot finish review task '${taskId}' from ${task.data.status}; reopen it first`,
|
|
455
|
+
})
|
|
456
|
+
}
|
|
457
|
+
const parsed = yield* parseFrontmatter(task.content, task.path)
|
|
458
|
+
const data = { ...task.data, status: "done" as const }
|
|
459
|
+
const content = formatMarkdownDocument(data, parsed.body)
|
|
460
|
+
yield* runLifecycleTransaction({
|
|
461
|
+
root,
|
|
462
|
+
preconditions: [{ path: task.path, revision: task.revision }],
|
|
463
|
+
steps: [documentWriteStep(root, [{ path: task.path, content }])],
|
|
464
|
+
})
|
|
465
|
+
return {
|
|
466
|
+
id: taskId,
|
|
467
|
+
path: task.path,
|
|
468
|
+
revision: documentRevision(content),
|
|
469
|
+
data,
|
|
470
|
+
}
|
|
471
|
+
}),
|
|
427
472
|
}),
|
|
428
473
|
},
|
|
429
474
|
) {}
|
|
@@ -58,7 +58,12 @@ interface RegisteredWorktree {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
interface SyncChange {
|
|
61
|
-
readonly kind:
|
|
61
|
+
readonly kind:
|
|
62
|
+
| "materialize-workspace"
|
|
63
|
+
| "adopt-pr-branch"
|
|
64
|
+
| "adopt-pr-base"
|
|
65
|
+
| "record-pr"
|
|
66
|
+
| "mark-done"
|
|
62
67
|
readonly target: string
|
|
63
68
|
readonly message: string
|
|
64
69
|
readonly status: "planned" | "applied"
|
|
@@ -162,14 +167,34 @@ interface PullRequestQuery {
|
|
|
162
167
|
| undefined
|
|
163
168
|
}
|
|
164
169
|
|
|
170
|
+
const canonicalRecord = (record: PullRequestRecord | null) =>
|
|
171
|
+
record
|
|
172
|
+
? JSON.stringify(
|
|
173
|
+
Object.entries(record)
|
|
174
|
+
.filter(([, value]) => value !== undefined)
|
|
175
|
+
.sort(([a], [b]) => a.localeCompare(b)),
|
|
176
|
+
)
|
|
177
|
+
: null
|
|
178
|
+
|
|
179
|
+
const samePullRequestRecord = (
|
|
180
|
+
current: PullRequestRecord,
|
|
181
|
+
existing: PullRequestRecord | null,
|
|
182
|
+
) => canonicalRecord(current) === canonicalRecord(existing)
|
|
183
|
+
|
|
184
|
+
const belongsToWritableRepository = (
|
|
185
|
+
current: PullRequestRecord,
|
|
186
|
+
remoteRepository: string,
|
|
187
|
+
) =>
|
|
188
|
+
current.headRepository?.toLowerCase() === remoteRepository.toLowerCase() &&
|
|
189
|
+
current.baseRepository?.toLowerCase() === current.repository.toLowerCase()
|
|
190
|
+
|
|
165
191
|
const mergedPullRequestFromGitHub = (
|
|
166
192
|
data: ExecutionData,
|
|
167
193
|
query: PullRequestQuery | undefined,
|
|
168
194
|
details: readonly PullRequestRecord[] | undefined,
|
|
169
195
|
) => {
|
|
170
196
|
if (!query?.result || query.result.exitCode !== 0 || !details) return null
|
|
171
|
-
const
|
|
172
|
-
const matches = existing
|
|
197
|
+
const matches = data.pr
|
|
173
198
|
? details
|
|
174
199
|
: details.filter(
|
|
175
200
|
(item) =>
|
|
@@ -177,18 +202,10 @@ const mergedPullRequestFromGitHub = (
|
|
|
177
202
|
)
|
|
178
203
|
if (matches.length !== 1) return null
|
|
179
204
|
const current = matches[0]!
|
|
180
|
-
|
|
181
|
-
current.
|
|
182
|
-
current
|
|
183
|
-
|
|
184
|
-
current.headBranch !== data.branch ||
|
|
185
|
-
current.baseRepository?.toLowerCase() !==
|
|
186
|
-
current.repository.toLowerCase() ||
|
|
187
|
-
current.baseBranch !== data.base
|
|
188
|
-
) {
|
|
189
|
-
return null
|
|
190
|
-
}
|
|
191
|
-
return current
|
|
205
|
+
return current.merged === true &&
|
|
206
|
+
belongsToWritableRepository(current, query.remoteRepository)
|
|
207
|
+
? current
|
|
208
|
+
: null
|
|
192
209
|
}
|
|
193
210
|
|
|
194
211
|
const mutateExecution = (
|
|
@@ -650,6 +667,22 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
650
667
|
{ concurrency: 8 },
|
|
651
668
|
)
|
|
652
669
|
|
|
670
|
+
const branchOwners = new Map<string, string>()
|
|
671
|
+
const branchKey = (repo: string, branch: string) =>
|
|
672
|
+
JSON.stringify([repo, branch])
|
|
673
|
+
for (const task of allTaskRecords) {
|
|
674
|
+
if ("repo" in task.data)
|
|
675
|
+
branchOwners.set(
|
|
676
|
+
branchKey(task.data.repo, task.data.branch),
|
|
677
|
+
`task:${task.id}`,
|
|
678
|
+
)
|
|
679
|
+
for (const phase of documents.phasesByTask.get(task.id) ?? [])
|
|
680
|
+
branchOwners.set(
|
|
681
|
+
branchKey(phase.data.repo, phase.data.branch),
|
|
682
|
+
`phase:${task.id}/${phase.id}`,
|
|
683
|
+
)
|
|
684
|
+
}
|
|
685
|
+
|
|
653
686
|
for (const record of records.sort((a, b) =>
|
|
654
687
|
a.key.localeCompare(b.key),
|
|
655
688
|
)) {
|
|
@@ -662,6 +695,126 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
662
695
|
const githubDetails = githubResponse?.ok
|
|
663
696
|
? githubResponse.details
|
|
664
697
|
: undefined
|
|
698
|
+
let renamedFrom: string | null = null
|
|
699
|
+
let adoptionConflict = false
|
|
700
|
+
const recordedPr =
|
|
701
|
+
!config.delivery && data.pr ? githubDetails?.[0] : undefined
|
|
702
|
+
if (
|
|
703
|
+
query &&
|
|
704
|
+
recordedPr?.headBranch &&
|
|
705
|
+
recordedPr.baseBranch &&
|
|
706
|
+
belongsToWritableRepository(recordedPr, query.remoteRepository) &&
|
|
707
|
+
(recordedPr.headBranch !== data.branch ||
|
|
708
|
+
recordedPr.baseBranch !== data.base)
|
|
709
|
+
) {
|
|
710
|
+
const previous = { branch: data.branch, base: data.base }
|
|
711
|
+
const adopted = {
|
|
712
|
+
branch: recordedPr.headBranch,
|
|
713
|
+
base: recordedPr.baseBranch,
|
|
714
|
+
}
|
|
715
|
+
const repositoryPath = join(root, "repos", data.repo)
|
|
716
|
+
let renameLocal = false
|
|
717
|
+
if (adopted.branch !== previous.branch) {
|
|
718
|
+
const owner = branchOwners.get(
|
|
719
|
+
branchKey(data.repo, adopted.branch),
|
|
720
|
+
)
|
|
721
|
+
const [previousLocal, adoptedLocal] = yield* Effect.all([
|
|
722
|
+
backend.resolveRevision(
|
|
723
|
+
repositoryPath,
|
|
724
|
+
`refs/heads/${previous.branch}`,
|
|
725
|
+
),
|
|
726
|
+
backend.resolveRevision(
|
|
727
|
+
repositoryPath,
|
|
728
|
+
`refs/heads/${adopted.branch}`,
|
|
729
|
+
),
|
|
730
|
+
])
|
|
731
|
+
if (owner && owner !== record.key) {
|
|
732
|
+
adoptionConflict = true
|
|
733
|
+
unresolved.push({
|
|
734
|
+
kind: "pr-branch-conflict",
|
|
735
|
+
target: record.key,
|
|
736
|
+
message: `Recorded PR head '${adopted.branch}' is declared by '${owner}'`,
|
|
737
|
+
action:
|
|
738
|
+
"Correct the recorded PR URL or the other declaration",
|
|
739
|
+
})
|
|
740
|
+
} else if (previousLocal && adoptedLocal) {
|
|
741
|
+
adoptionConflict = true
|
|
742
|
+
unresolved.push({
|
|
743
|
+
kind: "pr-branch-conflict",
|
|
744
|
+
target: record.key,
|
|
745
|
+
message: `Cannot rename local branch '${previous.branch}' to recorded PR head '${adopted.branch}' because it already exists`,
|
|
746
|
+
action: `Reconcile or delete local branch '${adopted.branch}' manually`,
|
|
747
|
+
})
|
|
748
|
+
} else {
|
|
749
|
+
renameLocal = previousLocal !== null
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
if (!adoptionConflict && apply) {
|
|
753
|
+
const renameBranch = (from: string, to: string) =>
|
|
754
|
+
runExternal(["git", "branch", "-m", from, to], {
|
|
755
|
+
cwd: repositoryPath,
|
|
756
|
+
})
|
|
757
|
+
const renamed = renameLocal
|
|
758
|
+
? yield* renameBranch(previous.branch, adopted.branch)
|
|
759
|
+
: null
|
|
760
|
+
if (renamed && renamed.exitCode !== 0) {
|
|
761
|
+
adoptionConflict = true
|
|
762
|
+
unresolved.push({
|
|
763
|
+
kind: "pr-branch-rename-failed",
|
|
764
|
+
target: record.key,
|
|
765
|
+
message: `Could not rename local branch '${previous.branch}' to '${adopted.branch}': ${commandErrorSummary(renamed.stderr, "git branch failed")}`,
|
|
766
|
+
action: "Rename the local branch manually",
|
|
767
|
+
})
|
|
768
|
+
} else {
|
|
769
|
+
const updated = yield* mutateExecution(root, record, revision, {
|
|
770
|
+
...data,
|
|
771
|
+
...adopted,
|
|
772
|
+
}).pipe(
|
|
773
|
+
Effect.tapError(() =>
|
|
774
|
+
renamed
|
|
775
|
+
? renameBranch(adopted.branch, previous.branch)
|
|
776
|
+
: Effect.void,
|
|
777
|
+
),
|
|
778
|
+
)
|
|
779
|
+
data = updated.data
|
|
780
|
+
revision = updated.revision
|
|
781
|
+
const cached = registeredByRepository.get(repositoryPath)
|
|
782
|
+
for (const [index, item] of (cached ?? []).entries()) {
|
|
783
|
+
if (item.branch === `refs/heads/${previous.branch}`)
|
|
784
|
+
cached![index] = {
|
|
785
|
+
...item,
|
|
786
|
+
branch: `refs/heads/${adopted.branch}`,
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
} else if (!adoptionConflict) {
|
|
791
|
+
data = { ...data, ...adopted }
|
|
792
|
+
if (renameLocal) renamedFrom = previous.branch
|
|
793
|
+
}
|
|
794
|
+
if (!adoptionConflict) {
|
|
795
|
+
if (adopted.branch !== previous.branch) {
|
|
796
|
+
branchOwners.delete(branchKey(data.repo, previous.branch))
|
|
797
|
+
branchOwners.set(
|
|
798
|
+
branchKey(data.repo, adopted.branch),
|
|
799
|
+
record.key,
|
|
800
|
+
)
|
|
801
|
+
changes.push({
|
|
802
|
+
kind: "adopt-pr-branch",
|
|
803
|
+
target: record.key,
|
|
804
|
+
message: `Adopt pull request head '${adopted.branch}' (was '${previous.branch}')`,
|
|
805
|
+
status: apply ? "applied" : "planned",
|
|
806
|
+
})
|
|
807
|
+
}
|
|
808
|
+
if (adopted.base !== previous.base) {
|
|
809
|
+
changes.push({
|
|
810
|
+
kind: "adopt-pr-base",
|
|
811
|
+
target: record.key,
|
|
812
|
+
message: `Adopt pull request base '${adopted.base}' (was '${previous.base}')`,
|
|
813
|
+
status: apply ? "applied" : "planned",
|
|
814
|
+
})
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
665
818
|
const remoteMergedPr = config.delivery
|
|
666
819
|
? null
|
|
667
820
|
: mergedPullRequestFromGitHub(data, query, githubDetails)
|
|
@@ -838,7 +991,12 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
838
991
|
action: "Review and preserve or discard local changes manually",
|
|
839
992
|
})
|
|
840
993
|
}
|
|
841
|
-
if (
|
|
994
|
+
if (
|
|
995
|
+
atPath &&
|
|
996
|
+
branchRef &&
|
|
997
|
+
atPath.branch !== branchRef &&
|
|
998
|
+
!(renamedFrom && atPath.branch === `refs/heads/${renamedFrom}`)
|
|
999
|
+
) {
|
|
842
1000
|
unresolved.push({
|
|
843
1001
|
kind: "wrong-branch",
|
|
844
1002
|
target: record.key,
|
|
@@ -1022,21 +1180,26 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
1022
1180
|
if (githubDetails) {
|
|
1023
1181
|
current = githubDetails[0]!
|
|
1024
1182
|
pr = current
|
|
1025
|
-
if (
|
|
1026
|
-
current.headRepository?.toLowerCase() !==
|
|
1027
|
-
remoteRepository.toLowerCase() ||
|
|
1028
|
-
current.headBranch !== data.branch ||
|
|
1029
|
-
current.baseRepository?.toLowerCase() !==
|
|
1030
|
-
current.repository.toLowerCase() ||
|
|
1031
|
-
current.baseBranch !== data.base
|
|
1032
|
-
) {
|
|
1183
|
+
if (!belongsToWritableRepository(current, remoteRepository)) {
|
|
1033
1184
|
prConflict = true
|
|
1034
1185
|
unresolved.push({
|
|
1035
1186
|
kind: "pr-repository-conflict",
|
|
1036
1187
|
target: record.key,
|
|
1037
|
-
message: `Recorded PR
|
|
1188
|
+
message: `Recorded PR repositories do not match writable repository '${remoteRepository}' and base '${current.repository}'`,
|
|
1038
1189
|
action: "Correct the declaration or recorded PR URL",
|
|
1039
1190
|
})
|
|
1191
|
+
} else if (
|
|
1192
|
+
current.headBranch !== data.branch ||
|
|
1193
|
+
current.baseBranch !== data.base
|
|
1194
|
+
) {
|
|
1195
|
+
prConflict = true
|
|
1196
|
+
if (!adoptionConflict)
|
|
1197
|
+
unresolved.push({
|
|
1198
|
+
kind: "pr-branch-conflict",
|
|
1199
|
+
target: record.key,
|
|
1200
|
+
message: `Recorded PR head '${current.headBranch ?? "unknown"}' or base '${current.baseBranch ?? "unknown"}' could not be adopted`,
|
|
1201
|
+
action: "Correct the declaration or recorded PR URL",
|
|
1202
|
+
})
|
|
1040
1203
|
}
|
|
1041
1204
|
}
|
|
1042
1205
|
} else {
|
|
@@ -1092,7 +1255,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
1092
1255
|
}
|
|
1093
1256
|
}
|
|
1094
1257
|
|
|
1095
|
-
if (current &&
|
|
1258
|
+
if (current && !samePullRequestRecord(current, existing)) {
|
|
1096
1259
|
if (apply) {
|
|
1097
1260
|
const recorded = yield* mutateExecution(root, record, revision, {
|
|
1098
1261
|
...data,
|
|
@@ -652,6 +652,15 @@ export class TaskService extends Effect.Service<TaskService>()("TaskService", {
|
|
|
652
652
|
!canTransitionStatus(record.data.status, validStatus) &&
|
|
653
653
|
!completionResult
|
|
654
654
|
) {
|
|
655
|
+
if (
|
|
656
|
+
validStatus === "done" &&
|
|
657
|
+
"review" in record.data &&
|
|
658
|
+
record.data.status !== "dropped"
|
|
659
|
+
) {
|
|
660
|
+
return yield* new TaskError({
|
|
661
|
+
message: `Finish review task '${id}' with 'agency review finish ${id}'; reviews do not require a completion summary`,
|
|
662
|
+
})
|
|
663
|
+
}
|
|
655
664
|
if (validStatus === "done") {
|
|
656
665
|
return yield* new TaskError({
|
|
657
666
|
message:
|
package/src/workbase/AGENTS.md
CHANGED
|
@@ -108,6 +108,8 @@ retain `--if-revision` guards when shown, and do not add flags that are not show
|
|
|
108
108
|
`agency context <new-task> --json`. Do not prepare or start it unless requested.
|
|
109
109
|
15. Refresh a pinned review task with the current revision:
|
|
110
110
|
`agency review refresh <task> --if-revision <revision> --json`.
|
|
111
|
+
16. Finish a review task with the current revision; no summary is required:
|
|
112
|
+
`agency review finish <task> --if-revision <revision> --json`.
|
|
111
113
|
|
|
112
114
|
Never pass `--work` or `--auto` to `agency task create`. Do not run separate
|
|
113
115
|
`agency validate`, `agency worktree prepare`, `agency graph`, `agency task list`,
|
|
@@ -182,8 +184,8 @@ agency validate --json
|
|
|
182
184
|
Require explicit user intent before initializing a workbase; changing repository
|
|
183
185
|
aliases or applying repository setup or workbase sync changes; launching another
|
|
184
186
|
agent from an active agent session; creating a pull request; archiving, restoring,
|
|
185
|
-
dropping, reopening, or completing work without a pull
|
|
186
|
-
to override readiness.
|
|
187
|
+
dropping, reopening, finishing a review, or completing work without a pull
|
|
188
|
+
request; or using `--force` to override readiness.
|
|
187
189
|
|
|
188
190
|
## Investigation Handoffs
|
|
189
191
|
|
|
@@ -272,7 +274,9 @@ its pull request is open. It becomes `done` only after its authoritative pull
|
|
|
272
274
|
request is merged and Agency reconciles that state. Do not mark committed or
|
|
273
275
|
review-ready work `done` manually. A genuine investigation, operational action,
|
|
274
276
|
or no-change result may complete without a pull request only with explicit user
|
|
275
|
-
intent, `--no-pull-request`, and a durable outcome summary.
|
|
277
|
+
intent, `--no-pull-request`, and a durable outcome summary. A review task has no
|
|
278
|
+
delivery pull request; with explicit user intent, finish it with
|
|
279
|
+
`agency review finish` instead, which requires no summary.
|
|
276
280
|
|
|
277
281
|
At each closeout trigger (creating or updating a PR, marking it ready, completing
|
|
278
282
|
a refinement loop, or pausing or handing off completed implementation work):
|