@markjaquith/agency 2.40.0 → 2.40.1
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 +11 -8
- package/package.json +1 -1
- package/skills/agency/SKILL.md +6 -5
- package/skills/agency/references/commands.md +2 -2
- package/skills/agency/references/contracts.md +2 -0
- package/skills/agency/references/recipes.md +5 -3
- package/src/cli.test.ts +13 -9
- package/src/commands/claim.ts +2 -1
- package/src/commands/phase.ts +1 -1
- package/src/commands/status.test.ts +1 -1
- package/src/commands/task-phase.test.ts +4 -4
- package/src/commands/task.ts +1 -1
- package/src/readiness.ts +3 -3
- package/src/services/ClaimService.test.ts +1 -1
- package/src/services/ClaimService.ts +5 -1
- package/src/services/IntegrationService.test.ts +4 -3
- package/src/services/PhaseService.ts +6 -0
- package/src/services/PullRequestService.test.ts +4 -2
- package/src/services/SyncService.test.ts +93 -0
- package/src/services/TaskPhaseService.test.ts +19 -20
- package/src/services/TaskService.ts +6 -0
- package/src/workbase/AGENTS.md +9 -7
package/README.md
CHANGED
|
@@ -569,7 +569,7 @@ Targeted commands accept `--epic`, `--task`, and `--phase` where those entity
|
|
|
569
569
|
kinds apply. A phase selector requires a task selector. Entity selectors cannot
|
|
570
570
|
be mixed with positional target IDs, and an epic selector cannot be mixed with
|
|
571
571
|
task or phase selectors. This makes commands such as
|
|
572
|
-
`agency phase status
|
|
572
|
+
`agency phase status working --task ship --phase release --workbase primary --no-input`
|
|
573
573
|
fully independent of process cwd and prompts.
|
|
574
574
|
|
|
575
575
|
Inspect tasks:
|
|
@@ -577,7 +577,7 @@ Inspect tasks:
|
|
|
577
577
|
```text
|
|
578
578
|
agency task list [filters] [--json]
|
|
579
579
|
agency task show <id> [--json]
|
|
580
|
-
agency task status <id> <open|
|
|
580
|
+
agency task status <id> <open|working|dropped> [--json]
|
|
581
581
|
agency task update <id> [metadata options] [--json]
|
|
582
582
|
agency task rename <id> <new-id> [--json]
|
|
583
583
|
agency task move <id> (--epic <epic-id> | --no-epic) [--json]
|
|
@@ -616,7 +616,7 @@ agency phase create <task-id> <phase-id>
|
|
|
616
616
|
|
|
617
617
|
agency phase list <task-id> [filters] [--json]
|
|
618
618
|
agency phase show <task-id> <phase-id> [--json]
|
|
619
|
-
agency phase status <task-id> <phase-id> <open|
|
|
619
|
+
agency phase status <task-id> <phase-id> <open|working|dropped> [--json]
|
|
620
620
|
agency phase update <task-id> <phase-id> [metadata options] [--json]
|
|
621
621
|
agency phase rename <task-id> <phase-id> <new-id> [--json]
|
|
622
622
|
agency phase dependency <add|remove> <task-id> <phase-id> <dependency-id>
|
|
@@ -639,6 +639,8 @@ writing anything.
|
|
|
639
639
|
Single-phase tasks and phases store status in YAML. New execution units start
|
|
640
640
|
`open`, and `agency work` marks the selected execution unit `working` immediately
|
|
641
641
|
before launch. Running `agency work` again can relaunch unclaimed `working` work.
|
|
642
|
+
The `done` status is reserved for an authoritative merged pull request and is
|
|
643
|
+
applied by `agency sync --apply`, not by task or phase status commands.
|
|
642
644
|
Use explicit claims only when an external orchestrator needs coordinated
|
|
643
645
|
ownership. The interactive work selector displays status markers before
|
|
644
646
|
execution units. Existing working and delegated work may be released to `open`
|
|
@@ -672,11 +674,12 @@ agency finish <task-id> [phase-id] --session-id <id>
|
|
|
672
674
|
--revision <sha256> --outcome <done|dropped> [--json]
|
|
673
675
|
```
|
|
674
676
|
|
|
675
|
-
An active claim sets status to `working`. Release returns it to `open
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
677
|
+
An active claim sets status to `working`. Release returns it to `open`. Finish
|
|
678
|
+
records the claim outcome and ownership history; a `done` claim outcome leaves
|
|
679
|
+
the execution unit `working` until its pull request is merged, while `dropped`
|
|
680
|
+
remains terminal. Conflicts return the current revision and complete ownership
|
|
681
|
+
record in the machine error envelope rather than overwriting it. Expired claims
|
|
682
|
+
may be replaced with a revision-guarded claim.
|
|
680
683
|
|
|
681
684
|
`agency work` does not claim execution units. It refuses active explicit claims,
|
|
682
685
|
marks open execution work `working`, and launches the runner. External
|
package/package.json
CHANGED
package/skills/agency/SKILL.md
CHANGED
|
@@ -118,11 +118,12 @@ independently meaningful tasks need coordination.
|
|
|
118
118
|
|
|
119
119
|
1. Re-run `agency validate` and repository checks.
|
|
120
120
|
2. Create a PR only when requested: `agency pr create <task> [phase]`.
|
|
121
|
-
3.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
121
|
+
3. Keep committed work `working` while its pull request is open. After the
|
|
122
|
+
authoritative pull request merges, use `agency sync --apply` to reconcile it
|
|
123
|
+
to `done`.
|
|
124
|
+
4. If the session has a claim, use revision-guarded `agency finish`; a successful
|
|
125
|
+
claim outcome closes ownership without marking unmerged work `done`. Use
|
|
126
|
+
`dropped` only for intentionally abandoned work.
|
|
126
127
|
5. Report the durable status and PR URL. Do not manually remove the worktree.
|
|
127
128
|
|
|
128
129
|
## Human Launch vs Active Agent
|
|
@@ -89,7 +89,7 @@ agency task create <id> --multi-phase [--ticket-url <url>]
|
|
|
89
89
|
[--description <text>] [--epic <id>] [--json]
|
|
90
90
|
agency task list [filters] [--json]
|
|
91
91
|
agency task show <id> [--json]
|
|
92
|
-
agency task status <id> <open|
|
|
92
|
+
agency task status <id> <open|working|dropped> [--json]
|
|
93
93
|
agency task update <id> [metadata options] [--if-revision <hash>] [--json]
|
|
94
94
|
agency task rename <id> <new-id> [--if-revision <hash>] [--json]
|
|
95
95
|
agency task move <id> (--epic <epic-id> | --no-epic)
|
|
@@ -102,7 +102,7 @@ agency phase create <task-id> <phase-id> --repo <alias> --branch <name>
|
|
|
102
102
|
[--depends-on <phase-id>...] [--first-phase <phase-id>] [--json]
|
|
103
103
|
agency phase list <task-id> [filters] [--json]
|
|
104
104
|
agency phase show <task-id> <phase-id> [--json]
|
|
105
|
-
agency phase status <task-id> <phase-id> <open|
|
|
105
|
+
agency phase status <task-id> <phase-id> <open|working|dropped> [--json]
|
|
106
106
|
agency phase update <task-id> <phase-id> [metadata options]
|
|
107
107
|
[--if-revision <hash>] [--json]
|
|
108
108
|
agency phase rename <task-id> <phase-id> <new-id>
|
|
@@ -137,6 +137,8 @@ readable legacy state but cannot be newly assigned.
|
|
|
137
137
|
- An execution unit has one writable `repo`; plural `repos` are read-only
|
|
138
138
|
`{ repo, ref }` entries and cannot repeat the writable alias.
|
|
139
139
|
- A writable `(repo, branch)` pair belongs to exactly one active execution unit.
|
|
140
|
+
- `done` represents an authoritative merged pull request and is reconciled by
|
|
141
|
+
`agency sync --apply`; committed or review-ready work remains `working`.
|
|
140
142
|
- Only `done` satisfies dependencies. `dropped` is terminal but blocks dependents.
|
|
141
143
|
- Epic task dependencies live in `EPIC.md`; phase dependencies live in `TASK.md`.
|
|
142
144
|
- IDs remain stable; use `dependsOn`, not numeric directory prefixes, for order.
|
|
@@ -184,15 +184,17 @@ agency release checkout ui --session-id session-123 \
|
|
|
184
184
|
|
|
185
185
|
## Finish Verified Work
|
|
186
186
|
|
|
187
|
-
|
|
187
|
+
After the claimed run completes successfully:
|
|
188
188
|
|
|
189
189
|
```bash
|
|
190
190
|
agency finish checkout ui --session-id session-123 \
|
|
191
191
|
--revision <current-sha256> --outcome done --json
|
|
192
192
|
```
|
|
193
193
|
|
|
194
|
-
Do not substitute
|
|
195
|
-
|
|
194
|
+
Do not substitute a phase status mutation for `finish`; `finish` preserves
|
|
195
|
+
ownership history and revision safety. A `done` claim outcome leaves unmerged
|
|
196
|
+
execution work `working`. After the authoritative pull request merges, use
|
|
197
|
+
`agency sync --apply` to reconcile the execution unit to `done`.
|
|
196
198
|
|
|
197
199
|
## Create A Pull Request
|
|
198
200
|
|
package/src/cli.test.ts
CHANGED
|
@@ -244,6 +244,10 @@ describe("CLI", () => {
|
|
|
244
244
|
),
|
|
245
245
|
)
|
|
246
246
|
expect(finished.claim).toMatchObject({ state: "finished", outcome: "done" })
|
|
247
|
+
expect(
|
|
248
|
+
parseJson(await runCli(["task", "show", "claimed", "--json"], root)).data
|
|
249
|
+
.status,
|
|
250
|
+
).toBe("working")
|
|
247
251
|
})
|
|
248
252
|
|
|
249
253
|
test("routes graph mutations through structured output", async () => {
|
|
@@ -476,7 +480,7 @@ describe("CLI", () => {
|
|
|
476
480
|
)
|
|
477
481
|
}
|
|
478
482
|
parseJson(
|
|
479
|
-
await runCli(["task", "status", "finished", "
|
|
483
|
+
await runCli(["task", "status", "finished", "dropped", "--json"], root),
|
|
480
484
|
)
|
|
481
485
|
|
|
482
486
|
const human = await runCli(["next"], root)
|
|
@@ -490,9 +494,9 @@ describe("CLI", () => {
|
|
|
490
494
|
expect(result.excluded).toMatchObject([
|
|
491
495
|
{
|
|
492
496
|
key: "task/finished",
|
|
493
|
-
status: "
|
|
497
|
+
status: "dropped",
|
|
494
498
|
terminal: true,
|
|
495
|
-
blockers: [{ kind: "status", reason: "Task status is
|
|
499
|
+
blockers: [{ kind: "status", reason: "Task status is dropped" }],
|
|
496
500
|
},
|
|
497
501
|
])
|
|
498
502
|
|
|
@@ -504,8 +508,8 @@ describe("CLI", () => {
|
|
|
504
508
|
error: {
|
|
505
509
|
code: "EXECUTION_BLOCKED",
|
|
506
510
|
fields: {
|
|
507
|
-
status: "
|
|
508
|
-
blockers: [{ kind: "status", reason: "Task status is
|
|
511
|
+
status: "dropped",
|
|
512
|
+
blockers: [{ kind: "status", reason: "Task status is dropped" }],
|
|
509
513
|
},
|
|
510
514
|
},
|
|
511
515
|
})
|
|
@@ -1131,18 +1135,18 @@ status: open
|
|
|
1131
1135
|
}
|
|
1132
1136
|
|
|
1133
1137
|
parseJson(
|
|
1134
|
-
await runCli(["task", "status", "example", "
|
|
1138
|
+
await runCli(["task", "status", "example", "dropped", "--json"], root),
|
|
1135
1139
|
)
|
|
1136
1140
|
const blocked = await runCli(
|
|
1137
1141
|
["work", "--task", "example", "--runner", "noop"],
|
|
1138
1142
|
root,
|
|
1139
1143
|
)
|
|
1140
1144
|
expect(blocked.exitCode).toBe(1)
|
|
1141
|
-
expect(blocked.stderr).toContain("Task status is
|
|
1145
|
+
expect(blocked.stderr).toContain("Task status is dropped")
|
|
1142
1146
|
expect(
|
|
1143
1147
|
parseJson(await runCli(["task", "show", "example", "--json"], root))
|
|
1144
1148
|
.data.status,
|
|
1145
|
-
).toBe("
|
|
1149
|
+
).toBe("dropped")
|
|
1146
1150
|
|
|
1147
1151
|
const resumedTask = await runCli(
|
|
1148
1152
|
["work", "--task", "example", "--runner", "noop", "--force"],
|
|
@@ -1150,7 +1154,7 @@ status: open
|
|
|
1150
1154
|
)
|
|
1151
1155
|
expect(resumedTask).toMatchObject({ exitCode: 0, stderr: "" })
|
|
1152
1156
|
expect(resumedTask.stdout).toContain(
|
|
1153
|
-
"Reopened task/example from
|
|
1157
|
+
"Reopened task/example from dropped as working",
|
|
1154
1158
|
)
|
|
1155
1159
|
expect(
|
|
1156
1160
|
parseJson(await runCli(["task", "show", "example", "--json"], root))
|
package/src/commands/claim.ts
CHANGED
|
@@ -93,5 +93,6 @@ Release an execution unit owned by the session.
|
|
|
93
93
|
export const finishHelp = `
|
|
94
94
|
Usage: agency finish <task-id> [phase-id] --session-id <id> --revision <sha256> --outcome <done|dropped>
|
|
95
95
|
|
|
96
|
-
Finish
|
|
96
|
+
Finish a claim owned by the session. A done claim outcome leaves unmerged work
|
|
97
|
+
working; agency sync --apply marks the execution unit done after merge.
|
|
97
98
|
`
|
package/src/commands/phase.ts
CHANGED
|
@@ -275,7 +275,7 @@ Subcommands:
|
|
|
275
275
|
list <task> List task phases
|
|
276
276
|
show <task> <phase> Show a phase
|
|
277
277
|
status <task> <phase> <status>
|
|
278
|
-
Set open, working,
|
|
278
|
+
Set open, working, or dropped
|
|
279
279
|
update <task> <phase> Update phase metadata
|
|
280
280
|
rename <task> <phase> <new-id>
|
|
281
281
|
Rename a phase and update dependencies
|
|
@@ -239,13 +239,13 @@ describe("task and phase command JSON output", () => {
|
|
|
239
239
|
runTestEffect(
|
|
240
240
|
phase({
|
|
241
241
|
subcommand: "status",
|
|
242
|
-
args: ["multi", "first", "
|
|
242
|
+
args: ["multi", "first", "working"],
|
|
243
243
|
cwd: root,
|
|
244
244
|
json: true,
|
|
245
245
|
}),
|
|
246
246
|
),
|
|
247
247
|
)
|
|
248
|
-
expect(JSON.parse(phaseLogs[0]!).data.status).toBe("
|
|
248
|
+
expect(JSON.parse(phaseLogs[0]!).data.status).toBe("working")
|
|
249
249
|
|
|
250
250
|
await runTestEffect(
|
|
251
251
|
task({
|
|
@@ -263,13 +263,13 @@ describe("task and phase command JSON output", () => {
|
|
|
263
263
|
runTestEffect(
|
|
264
264
|
task({
|
|
265
265
|
subcommand: "status",
|
|
266
|
-
args: ["single-status", "
|
|
266
|
+
args: ["single-status", "working"],
|
|
267
267
|
cwd: root,
|
|
268
268
|
json: true,
|
|
269
269
|
}),
|
|
270
270
|
),
|
|
271
271
|
)
|
|
272
|
-
expect(JSON.parse(taskLogs[0]!).data.status).toBe("
|
|
272
|
+
expect(JSON.parse(taskLogs[0]!).data.status).toBe("working")
|
|
273
273
|
})
|
|
274
274
|
|
|
275
275
|
test("converts a single-phase task with an explicit first phase ID", async () => {
|
package/src/commands/task.ts
CHANGED
|
@@ -423,7 +423,7 @@ Subcommands:
|
|
|
423
423
|
create <id> Create a task without prompting
|
|
424
424
|
list List tasks
|
|
425
425
|
show <id> Show a task
|
|
426
|
-
status <id> <status> Set open, working,
|
|
426
|
+
status <id> <status> Set open, working, or dropped
|
|
427
427
|
update <id> Update task metadata
|
|
428
428
|
rename <id> <new-id> Rename a task and update graph references
|
|
429
429
|
move <id> Move a task with --epic or --no-epic
|
package/src/readiness.ts
CHANGED
|
@@ -5,9 +5,9 @@ export interface ReadinessBlocker {
|
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
export const WORK_STATUS_TRANSITIONS = {
|
|
8
|
-
open: ["open", "working", "delegated", "
|
|
9
|
-
working: ["open", "working", "delegated", "
|
|
10
|
-
delegated: ["open", "working", "delegated", "
|
|
8
|
+
open: ["open", "working", "delegated", "dropped"],
|
|
9
|
+
working: ["open", "working", "delegated", "dropped"],
|
|
10
|
+
delegated: ["open", "working", "delegated", "dropped"],
|
|
11
11
|
done: ["open", "done"],
|
|
12
12
|
dropped: ["open", "dropped"],
|
|
13
13
|
} as const satisfies Record<WorkStatus, readonly WorkStatus[]>
|
|
@@ -139,7 +139,7 @@ describe("claim service", () => {
|
|
|
139
139
|
),
|
|
140
140
|
),
|
|
141
141
|
)
|
|
142
|
-
expect(finished.data.status).toBe("
|
|
142
|
+
expect(finished.data.status).toBe("working")
|
|
143
143
|
expect(finished.claim).toMatchObject({
|
|
144
144
|
state: "finished",
|
|
145
145
|
finishedAt: "2026-07-17T12:45:00.000Z",
|
|
@@ -538,7 +538,11 @@ export class ClaimService extends Effect.Service<ClaimService>()(
|
|
|
538
538
|
outcome: input.outcome,
|
|
539
539
|
}
|
|
540
540
|
return {
|
|
541
|
-
data: {
|
|
541
|
+
data: {
|
|
542
|
+
...data,
|
|
543
|
+
status: input.outcome === "done" ? "working" : "dropped",
|
|
544
|
+
claim,
|
|
545
|
+
},
|
|
542
546
|
claim,
|
|
543
547
|
}
|
|
544
548
|
},
|
|
@@ -80,14 +80,15 @@ describe("IntegrationService", () => {
|
|
|
80
80
|
expect(body).toContain("External orchestrators claim before launching")
|
|
81
81
|
expect(body).toContain("Run `agency validate`")
|
|
82
82
|
expect(body).toContain("only with explicit user intent")
|
|
83
|
-
expect(body).toContain("An execution unit
|
|
83
|
+
expect(body).toContain("An execution unit remains `working`")
|
|
84
84
|
expect(body).toContain("It becomes `done`")
|
|
85
|
-
expect(body).toContain("
|
|
85
|
+
expect(body).toContain("Do not mark committed")
|
|
86
86
|
expect(body).toContain("creating or updating a PR")
|
|
87
87
|
expect(body).toContain("marking it ready")
|
|
88
88
|
expect(body).toMatch(/completing\s+a refinement loop/)
|
|
89
89
|
expect(body).toContain("pausing or handing off")
|
|
90
|
-
expect(body).toContain("`agency
|
|
90
|
+
expect(body).toContain("`agency finish`")
|
|
91
|
+
expect(body).toContain("`agency sync --apply`")
|
|
91
92
|
expect(body).toContain("`TASK.md` or `PHASE.md`")
|
|
92
93
|
expect(body).toContain("PR state, current head, diff summary")
|
|
93
94
|
expect(body).toContain("Run `agency validate` before reporting completion")
|
|
@@ -487,6 +487,12 @@ export class PhaseService extends Effect.Service<PhaseService>()(
|
|
|
487
487
|
})
|
|
488
488
|
}
|
|
489
489
|
if (!canTransitionStatus(record.data.status, validStatus)) {
|
|
490
|
+
if (validStatus === "done") {
|
|
491
|
+
return yield* new PhaseError({
|
|
492
|
+
message:
|
|
493
|
+
"Work becomes done after its authoritative pull request is merged; run 'agency sync --apply'",
|
|
494
|
+
})
|
|
495
|
+
}
|
|
490
496
|
return yield* new PhaseError({
|
|
491
497
|
message: `Cannot transition phase '${id}' from ${record.data.status} to ${validStatus}; reopen it first`,
|
|
492
498
|
})
|
|
@@ -263,11 +263,13 @@ process.exit(${exitCode})
|
|
|
263
263
|
await createTask()
|
|
264
264
|
await runTestEffect(
|
|
265
265
|
TaskService.pipe(
|
|
266
|
-
Effect.flatMap((service) =>
|
|
266
|
+
Effect.flatMap((service) =>
|
|
267
|
+
service.setStatus("example", "dropped", root),
|
|
268
|
+
),
|
|
267
269
|
),
|
|
268
270
|
)
|
|
269
271
|
|
|
270
|
-
await expect(createPullRequest()).rejects.toThrow("Task status is
|
|
272
|
+
await expect(createPullRequest()).rejects.toThrow("Task status is dropped")
|
|
271
273
|
expect(
|
|
272
274
|
await Bun.file(join(root, "tasks/example/code/agency")).exists(),
|
|
273
275
|
).toBe(false)
|
|
@@ -390,6 +390,99 @@ process.stdout.write(${JSON.stringify(JSON.stringify(record))})
|
|
|
390
390
|
expect("pr" in task.data && task.data.pr).toEqual(record)
|
|
391
391
|
})
|
|
392
392
|
|
|
393
|
+
test("marks a successfully finished claim done only after merge", async () => {
|
|
394
|
+
await runTestEffect(
|
|
395
|
+
TaskService.pipe(
|
|
396
|
+
Effect.flatMap((service) =>
|
|
397
|
+
service.create(
|
|
398
|
+
{
|
|
399
|
+
id: "finished-claim",
|
|
400
|
+
ticketUrl: null,
|
|
401
|
+
repo: "agency",
|
|
402
|
+
branch: "feat/example",
|
|
403
|
+
base: "main",
|
|
404
|
+
},
|
|
405
|
+
root,
|
|
406
|
+
),
|
|
407
|
+
),
|
|
408
|
+
),
|
|
409
|
+
)
|
|
410
|
+
await runTestEffect(
|
|
411
|
+
WorktreeService.pipe(
|
|
412
|
+
Effect.flatMap((service) =>
|
|
413
|
+
service.materialize("finished-claim", undefined, root),
|
|
414
|
+
),
|
|
415
|
+
),
|
|
416
|
+
)
|
|
417
|
+
await git(
|
|
418
|
+
["remote", "set-url", "origin", "git@github.com:example/agency.git"],
|
|
419
|
+
join(root, "repos/agency"),
|
|
420
|
+
)
|
|
421
|
+
const initial = await runTestEffect(
|
|
422
|
+
ClaimService.pipe(
|
|
423
|
+
Effect.flatMap((service) =>
|
|
424
|
+
service.inspect("finished-claim", undefined, root),
|
|
425
|
+
),
|
|
426
|
+
),
|
|
427
|
+
)
|
|
428
|
+
const claimed = await runTestEffect(
|
|
429
|
+
ClaimService.pipe(
|
|
430
|
+
Effect.flatMap((service) =>
|
|
431
|
+
service.claim(
|
|
432
|
+
{
|
|
433
|
+
taskId: "finished-claim",
|
|
434
|
+
claimant: "orchestrator",
|
|
435
|
+
runner: "agent",
|
|
436
|
+
sessionId: "session-1",
|
|
437
|
+
revision: initial.revision,
|
|
438
|
+
},
|
|
439
|
+
root,
|
|
440
|
+
),
|
|
441
|
+
),
|
|
442
|
+
),
|
|
443
|
+
)
|
|
444
|
+
const finished = await runTestEffect(
|
|
445
|
+
ClaimService.pipe(
|
|
446
|
+
Effect.flatMap((service) =>
|
|
447
|
+
service.finish(
|
|
448
|
+
{
|
|
449
|
+
taskId: "finished-claim",
|
|
450
|
+
sessionId: "session-1",
|
|
451
|
+
revision: claimed.revision,
|
|
452
|
+
outcome: "done",
|
|
453
|
+
},
|
|
454
|
+
root,
|
|
455
|
+
),
|
|
456
|
+
),
|
|
457
|
+
),
|
|
458
|
+
)
|
|
459
|
+
expect(finished.data).toMatchObject({
|
|
460
|
+
status: "working",
|
|
461
|
+
claim: { state: "finished", outcome: "done" },
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
const synced = await runTestEffect(
|
|
465
|
+
SyncService.pipe(
|
|
466
|
+
Effect.flatMap((service) =>
|
|
467
|
+
service.reconcile({ cwd: root, apply: true }),
|
|
468
|
+
),
|
|
469
|
+
),
|
|
470
|
+
)
|
|
471
|
+
expect(synced.changes.map((change) => change.kind)).toContain("mark-done")
|
|
472
|
+
expect(
|
|
473
|
+
await runTestEffect(
|
|
474
|
+
TaskService.pipe(
|
|
475
|
+
Effect.flatMap((service) => service.show("finished-claim", root)),
|
|
476
|
+
),
|
|
477
|
+
),
|
|
478
|
+
).toMatchObject({
|
|
479
|
+
data: {
|
|
480
|
+
status: "done",
|
|
481
|
+
claim: { state: "finished", outcome: "done" },
|
|
482
|
+
},
|
|
483
|
+
})
|
|
484
|
+
})
|
|
485
|
+
|
|
393
486
|
test("materializes missing workspaces but leaves branch conflicts unresolved", async () => {
|
|
394
487
|
for (const [id, branch] of [
|
|
395
488
|
["missing", "feat/missing"],
|
|
@@ -318,22 +318,23 @@ describe("task and phase services", () => {
|
|
|
318
318
|
),
|
|
319
319
|
),
|
|
320
320
|
).rejects.toThrow("Delegation requires explicit ownership")
|
|
321
|
-
await runTestEffect(
|
|
322
|
-
TaskService.pipe(
|
|
323
|
-
Effect.flatMap((service) =>
|
|
324
|
-
service.setStatus("single-status", "done", root),
|
|
325
|
-
),
|
|
326
|
-
),
|
|
327
|
-
)
|
|
328
321
|
await expect(
|
|
329
322
|
runTestEffect(
|
|
330
323
|
TaskService.pipe(
|
|
331
324
|
Effect.flatMap((service) =>
|
|
332
|
-
service.setStatus("single-status", "
|
|
325
|
+
service.setStatus("single-status", "done", root),
|
|
333
326
|
),
|
|
334
327
|
),
|
|
335
328
|
),
|
|
336
|
-
).rejects.toThrow("
|
|
329
|
+
).rejects.toThrow("authoritative pull request is merged")
|
|
330
|
+
const droppedTask = await runTestEffect(
|
|
331
|
+
TaskService.pipe(
|
|
332
|
+
Effect.flatMap((service) =>
|
|
333
|
+
service.setStatus("single-status", "dropped", root),
|
|
334
|
+
),
|
|
335
|
+
),
|
|
336
|
+
)
|
|
337
|
+
expect(droppedTask.data.status).toBe("dropped")
|
|
337
338
|
|
|
338
339
|
await runTestEffect(
|
|
339
340
|
TaskService.pipe(
|
|
@@ -389,7 +390,7 @@ describe("task and phase services", () => {
|
|
|
389
390
|
),
|
|
390
391
|
),
|
|
391
392
|
),
|
|
392
|
-
).rejects.toThrow("
|
|
393
|
+
).rejects.toThrow("authoritative pull request is merged")
|
|
393
394
|
await runTestEffect(
|
|
394
395
|
PhaseService.pipe(
|
|
395
396
|
Effect.flatMap((service) =>
|
|
@@ -397,17 +398,15 @@ describe("task and phase services", () => {
|
|
|
397
398
|
),
|
|
398
399
|
),
|
|
399
400
|
)
|
|
400
|
-
expect(
|
|
401
|
-
(
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
service.setStatus("multi-status", "implementation", "done", root),
|
|
406
|
-
),
|
|
401
|
+
await expect(
|
|
402
|
+
runTestEffect(
|
|
403
|
+
PhaseService.pipe(
|
|
404
|
+
Effect.flatMap((service) =>
|
|
405
|
+
service.setStatus("multi-status", "implementation", "done", root),
|
|
407
406
|
),
|
|
408
|
-
)
|
|
409
|
-
)
|
|
410
|
-
).
|
|
407
|
+
),
|
|
408
|
+
),
|
|
409
|
+
).rejects.toThrow("authoritative pull request is merged")
|
|
411
410
|
await expect(
|
|
412
411
|
runTestEffect(
|
|
413
412
|
TaskService.pipe(
|
|
@@ -270,6 +270,12 @@ export class TaskService extends Effect.Service<TaskService>()("TaskService", {
|
|
|
270
270
|
})
|
|
271
271
|
}
|
|
272
272
|
if (!canTransitionStatus(record.data.status, validStatus)) {
|
|
273
|
+
if (validStatus === "done") {
|
|
274
|
+
return yield* new TaskError({
|
|
275
|
+
message:
|
|
276
|
+
"Work becomes done after its authoritative pull request is merged; run 'agency sync --apply'",
|
|
277
|
+
})
|
|
278
|
+
}
|
|
273
279
|
return yield* new TaskError({
|
|
274
280
|
message: `Cannot transition task '${id}' from ${record.data.status} to ${validStatus}; reopen it first`,
|
|
275
281
|
})
|
package/src/workbase/AGENTS.md
CHANGED
|
@@ -48,17 +48,19 @@ reason to edit `agency.json` or `repos/` by hand.
|
|
|
48
48
|
|
|
49
49
|
## Closeout
|
|
50
50
|
|
|
51
|
-
An execution unit
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
An execution unit remains `working` after implementation is committed and while
|
|
52
|
+
its pull request is open. It becomes `done` only after its authoritative pull
|
|
53
|
+
request is merged and Agency reconciles that state. Do not mark committed or
|
|
54
|
+
review-ready work `done` manually.
|
|
55
55
|
|
|
56
56
|
At each closeout trigger (creating or updating a PR, marking it ready, completing
|
|
57
57
|
a refinement loop, or pausing or handing off completed implementation work):
|
|
58
58
|
|
|
59
|
-
- Finish an active claim with the current revision via `agency finish
|
|
60
|
-
|
|
61
|
-
execution unit
|
|
59
|
+
- Finish an active claim with the current revision via `agency finish`; a
|
|
60
|
+
successful claim outcome leaves unmerged work `working`. For unclaimed work,
|
|
61
|
+
keep the execution unit `working` through review and merge.
|
|
62
|
+
- After merge, run `agency sync --apply` to reconcile the execution unit to
|
|
63
|
+
`done`.
|
|
62
64
|
- Refresh durable delivery context in `TASK.md` or `PHASE.md`, including recorded
|
|
63
65
|
PR state, current head, diff summary, and verification results after later
|
|
64
66
|
pushes when those details are maintained there.
|