@markjaquith/agency 2.60.0 → 2.61.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 +50 -0
- package/cli-main.ts +2 -0
- package/fixtures/protocol/orchestration-recipes.json +19 -0
- package/package.json +1 -1
- package/schemas/agency-graph-v1.schema.json +53 -0
- package/src/cli-parser.test.ts +49 -1
- package/src/cli-parser.ts +30 -1
- package/src/commands/task-phase.test.ts +49 -0
- package/src/commands/task.ts +47 -1
- package/src/graph-schema.test.ts +8 -0
- package/src/graph-schema.ts +7 -0
- package/src/services/ArchiveService.test.ts +18 -1
- package/src/services/GraphMutationService.test.ts +48 -0
- package/src/services/IntegrationService.test.ts +10 -0
- package/src/services/LifecycleTransaction.ts +12 -0
- package/src/services/PhaseService.ts +2 -0
- package/src/services/TaskPhaseService.test.ts +238 -0
- package/src/services/TaskService.ts +265 -6
- package/src/services/WorkbaseService.ts +30 -0
- package/src/workbase/AGENTS.md +25 -0
- package/src/workbase/frontmatter.test.ts +18 -1
- package/src/workbase/frontmatter.ts +29 -1
- package/src/workbase/opencode-file.ts +1 -1
- package/src/workbase/schemas.test.ts +36 -0
- package/src/workbase/schemas.ts +33 -1
package/README.md
CHANGED
|
@@ -427,6 +427,24 @@ status: open
|
|
|
427
427
|
---
|
|
428
428
|
```
|
|
429
429
|
|
|
430
|
+
Tasks may also record a purpose and one-way investigation handoff provenance:
|
|
431
|
+
|
|
432
|
+
```yaml
|
|
433
|
+
purpose: implementation
|
|
434
|
+
handoff:
|
|
435
|
+
source:
|
|
436
|
+
kind: phase
|
|
437
|
+
taskId: investigate-checkout
|
|
438
|
+
phaseId: reproduce
|
|
439
|
+
sourceRevision: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
The source revision is historical evidence, not a readiness dependency or a
|
|
443
|
+
claim that the source still has that content. Source rename, archive, restore,
|
|
444
|
+
or later edits do not rewrite it. Destination rename, archive, and restore
|
|
445
|
+
preserve it unchanged. Task show, context, and graph JSON expose the record as
|
|
446
|
+
task metadata; graph output does not add an edge for it.
|
|
447
|
+
|
|
430
448
|
### Multi-Phase Task
|
|
431
449
|
|
|
432
450
|
```yaml
|
|
@@ -710,6 +728,38 @@ invalidates reuse. Older creation output without evidence remains compatible;
|
|
|
710
728
|
preflight simply validates again. The published machine schema is
|
|
711
729
|
`schemas/agency-kickoff-v1.schema.json`.
|
|
712
730
|
|
|
731
|
+
Create investigation-only work with the existing task architecture:
|
|
732
|
+
|
|
733
|
+
```text
|
|
734
|
+
agency task create <id> --purpose investigation --repo <alias> [options]
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
This records `purpose: investigation` and generates explicit Investigation
|
|
738
|
+
Boundary, Evidence, Findings, Recommendation, Implementation Handoff, and
|
|
739
|
+
Important Decisions sections. A no-change recommendation is a valid result;
|
|
740
|
+
implementation does not belong in the investigation task merely because that
|
|
741
|
+
task has execution authority.
|
|
742
|
+
|
|
743
|
+
Create a distinct implementation task from an investigation task or phase:
|
|
744
|
+
|
|
745
|
+
```text
|
|
746
|
+
agency task handoff <investigation-task> <new-task> --repo <alias> [options] --json
|
|
747
|
+
agency task handoff <investigation-task> <new-task> --source-phase <phase> --repo <alias> [options] --json
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
The destination ID must be new across active and archived work; Agency never
|
|
751
|
+
restores, reopens, renames, or reuses a matching item. Explicit requests for a
|
|
752
|
+
new, separate, or follow-up item always override reuse, even when the subject or
|
|
753
|
+
suggested ID matches existing work. The transaction writes only the destination
|
|
754
|
+
task and any requested epic backlink, while revision-checking the source.
|
|
755
|
+
|
|
756
|
+
JSON returns the exact destination selector, directory and document path,
|
|
757
|
+
branch/base and revision; source selector, document path and captured revision;
|
|
758
|
+
the validation report; and the later `worktree prepare` target and command.
|
|
759
|
+
Handoff creation does not prepare a worktree, change status, launch an agent, or
|
|
760
|
+
perform UI actions. Run `agency context <new-task> --json` to verify authority,
|
|
761
|
+
and prepare or launch only when separately requested.
|
|
762
|
+
|
|
713
763
|
Create a multi-phase task container:
|
|
714
764
|
|
|
715
765
|
```text
|
package/cli-main.ts
CHANGED
|
@@ -498,6 +498,8 @@ const commands: Record<string, Command> = {
|
|
|
498
498
|
contextBase: options["context-base"],
|
|
499
499
|
contextSlug: options["context-slug"],
|
|
500
500
|
authoritativeSources: options["authoritative-source"],
|
|
501
|
+
purpose: options.purpose,
|
|
502
|
+
sourcePhase: options["source-phase"],
|
|
501
503
|
clearReferences: options["clear-references"],
|
|
502
504
|
prUrl: options["pr-url"],
|
|
503
505
|
clearPr: options["clear-pr"],
|
|
@@ -3,6 +3,25 @@
|
|
|
3
3
|
["worktree", "inspect", "checkout", "ui", "--json"],
|
|
4
4
|
["next", "--json"],
|
|
5
5
|
["graph", "--ready", "--json"],
|
|
6
|
+
[
|
|
7
|
+
"task",
|
|
8
|
+
"create",
|
|
9
|
+
"investigate-checkout",
|
|
10
|
+
"--purpose",
|
|
11
|
+
"investigation",
|
|
12
|
+
"--repo",
|
|
13
|
+
"agency",
|
|
14
|
+
"--json"
|
|
15
|
+
],
|
|
16
|
+
[
|
|
17
|
+
"task",
|
|
18
|
+
"handoff",
|
|
19
|
+
"investigate-checkout",
|
|
20
|
+
"implement-checkout",
|
|
21
|
+
"--repo",
|
|
22
|
+
"agency",
|
|
23
|
+
"--json"
|
|
24
|
+
],
|
|
6
25
|
[
|
|
7
26
|
"claim",
|
|
8
27
|
"checkout",
|
package/package.json
CHANGED
|
@@ -333,6 +333,8 @@
|
|
|
333
333
|
"ticketUrl": { "type": ["string", "null"] },
|
|
334
334
|
"description": { "type": "string" },
|
|
335
335
|
"epic": { "type": "string" },
|
|
336
|
+
"purpose": { "$ref": "#/$defs/taskPurpose" },
|
|
337
|
+
"handoff": { "$ref": "#/$defs/taskHandoff" },
|
|
336
338
|
"repo": { "type": "string" },
|
|
337
339
|
"repos": {
|
|
338
340
|
"type": "array",
|
|
@@ -354,6 +356,8 @@
|
|
|
354
356
|
"ticketUrl": { "type": ["string", "null"] },
|
|
355
357
|
"description": { "type": "string" },
|
|
356
358
|
"epic": { "type": "string" },
|
|
359
|
+
"purpose": { "$ref": "#/$defs/taskPurpose" },
|
|
360
|
+
"handoff": { "$ref": "#/$defs/taskHandoff" },
|
|
357
361
|
"phases": {
|
|
358
362
|
"type": "array",
|
|
359
363
|
"items": { "$ref": "#/$defs/dependency" }
|
|
@@ -416,6 +420,8 @@
|
|
|
416
420
|
"ticketUrl": { "type": ["string", "null"] },
|
|
417
421
|
"description": { "type": "string" },
|
|
418
422
|
"epic": { "type": "string" },
|
|
423
|
+
"purpose": { "$ref": "#/$defs/taskPurpose" },
|
|
424
|
+
"handoff": { "$ref": "#/$defs/taskHandoff" },
|
|
419
425
|
"review": { "$ref": "#/$defs/reviewRecord" },
|
|
420
426
|
"status": { "$ref": "#/$defs/status" },
|
|
421
427
|
"claim": { "$ref": "#/$defs/claim" },
|
|
@@ -452,6 +458,8 @@
|
|
|
452
458
|
"phaseId": { "type": "string" },
|
|
453
459
|
"ticketUrl": { "type": ["string", "null"] },
|
|
454
460
|
"epic": { "type": "string" },
|
|
461
|
+
"purpose": { "$ref": "#/$defs/taskPurpose" },
|
|
462
|
+
"handoff": { "$ref": "#/$defs/taskHandoff" },
|
|
455
463
|
"description": { "type": "string" },
|
|
456
464
|
"repo": { "type": "string" },
|
|
457
465
|
"repos": {
|
|
@@ -474,6 +482,8 @@
|
|
|
474
482
|
"ticketUrl": { "type": ["string", "null"] },
|
|
475
483
|
"description": { "type": "string" },
|
|
476
484
|
"epic": { "type": "string" },
|
|
485
|
+
"purpose": { "$ref": "#/$defs/taskPurpose" },
|
|
486
|
+
"handoff": { "$ref": "#/$defs/taskHandoff" },
|
|
477
487
|
"review": { "$ref": "#/$defs/reviewRecord" },
|
|
478
488
|
"status": { "$ref": "#/$defs/status" },
|
|
479
489
|
"claim": { "$ref": "#/$defs/claim" }
|
|
@@ -481,6 +491,49 @@
|
|
|
481
491
|
}
|
|
482
492
|
]
|
|
483
493
|
},
|
|
494
|
+
"taskPurpose": {
|
|
495
|
+
"enum": ["investigation", "implementation"]
|
|
496
|
+
},
|
|
497
|
+
"taskHandoff": {
|
|
498
|
+
"type": "object",
|
|
499
|
+
"additionalProperties": false,
|
|
500
|
+
"required": ["source", "sourceRevision"],
|
|
501
|
+
"properties": {
|
|
502
|
+
"source": {
|
|
503
|
+
"oneOf": [
|
|
504
|
+
{
|
|
505
|
+
"type": "object",
|
|
506
|
+
"additionalProperties": false,
|
|
507
|
+
"required": ["kind", "taskId"],
|
|
508
|
+
"properties": {
|
|
509
|
+
"kind": { "const": "task" },
|
|
510
|
+
"taskId": {
|
|
511
|
+
"type": "string",
|
|
512
|
+
"pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]*$"
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
"type": "object",
|
|
518
|
+
"additionalProperties": false,
|
|
519
|
+
"required": ["kind", "taskId", "phaseId"],
|
|
520
|
+
"properties": {
|
|
521
|
+
"kind": { "const": "phase" },
|
|
522
|
+
"taskId": {
|
|
523
|
+
"type": "string",
|
|
524
|
+
"pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]*$"
|
|
525
|
+
},
|
|
526
|
+
"phaseId": {
|
|
527
|
+
"type": "string",
|
|
528
|
+
"pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]*$"
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
]
|
|
533
|
+
},
|
|
534
|
+
"sourceRevision": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
|
|
535
|
+
}
|
|
536
|
+
},
|
|
484
537
|
"claim": {
|
|
485
538
|
"type": "object",
|
|
486
539
|
"additionalProperties": false,
|
package/src/cli-parser.test.ts
CHANGED
|
@@ -759,10 +759,58 @@ describe("strict CLI parsing", () => {
|
|
|
759
759
|
)
|
|
760
760
|
expectUsageError(
|
|
761
761
|
["task", "crate"],
|
|
762
|
-
"agency task <new|create|list|show|status|update|rename|move|dependency>",
|
|
762
|
+
"agency task <new|create|handoff|list|show|status|update|rename|move|dependency>",
|
|
763
763
|
)
|
|
764
764
|
})
|
|
765
765
|
|
|
766
|
+
test("parses investigation creation and explicit handoff commands", () => {
|
|
767
|
+
expect(
|
|
768
|
+
parseCli([
|
|
769
|
+
"task",
|
|
770
|
+
"create",
|
|
771
|
+
"investigate",
|
|
772
|
+
"--purpose",
|
|
773
|
+
"investigation",
|
|
774
|
+
"--repo",
|
|
775
|
+
"agency",
|
|
776
|
+
]),
|
|
777
|
+
).toMatchObject({
|
|
778
|
+
args: ["create", "investigate"],
|
|
779
|
+
values: { purpose: "investigation", repo: ["agency"] },
|
|
780
|
+
})
|
|
781
|
+
expect(
|
|
782
|
+
parseCli([
|
|
783
|
+
"task",
|
|
784
|
+
"handoff",
|
|
785
|
+
"investigate",
|
|
786
|
+
"implement",
|
|
787
|
+
"--source-phase",
|
|
788
|
+
"evidence",
|
|
789
|
+
"--repo",
|
|
790
|
+
"agency",
|
|
791
|
+
"--json",
|
|
792
|
+
]),
|
|
793
|
+
).toMatchObject({
|
|
794
|
+
args: ["handoff", "investigate", "implement"],
|
|
795
|
+
values: {
|
|
796
|
+
"source-phase": "evidence",
|
|
797
|
+
repo: ["agency"],
|
|
798
|
+
json: true,
|
|
799
|
+
},
|
|
800
|
+
})
|
|
801
|
+
expect(() =>
|
|
802
|
+
parseCli([
|
|
803
|
+
"task",
|
|
804
|
+
"create",
|
|
805
|
+
"investigate",
|
|
806
|
+
"--purpose",
|
|
807
|
+
"unknown",
|
|
808
|
+
"--repo",
|
|
809
|
+
"agency",
|
|
810
|
+
]),
|
|
811
|
+
).toThrow("Invalid '--purpose' value")
|
|
812
|
+
})
|
|
813
|
+
|
|
766
814
|
test("parses graph mutation commands and rejects unsafe ambiguity", () => {
|
|
767
815
|
expect(
|
|
768
816
|
parseCli([
|
package/src/cli-parser.ts
CHANGED
|
@@ -89,6 +89,8 @@ const taskCreateOptions = {
|
|
|
89
89
|
"context-base": { type: "string" },
|
|
90
90
|
"context-slug": { type: "string" },
|
|
91
91
|
"authoritative-source": { type: "string", multiple: true },
|
|
92
|
+
purpose: { type: "string" },
|
|
93
|
+
"source-phase": { type: "string" },
|
|
92
94
|
} satisfies OptionConfig
|
|
93
95
|
|
|
94
96
|
const phaseCreateOptions = {
|
|
@@ -367,7 +369,7 @@ const commands = {
|
|
|
367
369
|
},
|
|
368
370
|
task: {
|
|
369
371
|
usage:
|
|
370
|
-
"agency task <new|create|list|show|status|update|rename|move|dependency>",
|
|
372
|
+
"agency task <new|create|handoff|list|show|status|update|rename|move|dependency>",
|
|
371
373
|
options: {
|
|
372
374
|
...taskCreateOptions,
|
|
373
375
|
...newWorkOptions,
|
|
@@ -390,6 +392,7 @@ const commands = {
|
|
|
390
392
|
"reference",
|
|
391
393
|
"branch",
|
|
392
394
|
"base",
|
|
395
|
+
"purpose",
|
|
393
396
|
"multi-phase",
|
|
394
397
|
"work",
|
|
395
398
|
"auto",
|
|
@@ -411,6 +414,7 @@ const commands = {
|
|
|
411
414
|
"reference",
|
|
412
415
|
"branch",
|
|
413
416
|
"base",
|
|
417
|
+
"purpose",
|
|
414
418
|
"multi-phase",
|
|
415
419
|
"review",
|
|
416
420
|
"pull-request",
|
|
@@ -423,6 +427,25 @@ const commands = {
|
|
|
423
427
|
],
|
|
424
428
|
repeatable: ["reference", "authoritative-source"],
|
|
425
429
|
},
|
|
430
|
+
handoff: {
|
|
431
|
+
usage:
|
|
432
|
+
"agency task handoff <source-task-id> <new-task-id> --repo <alias> [--source-phase <phase-id>] [options]",
|
|
433
|
+
minArgs: 2,
|
|
434
|
+
maxArgs: 2,
|
|
435
|
+
options: [
|
|
436
|
+
"ticket-url",
|
|
437
|
+
"description",
|
|
438
|
+
"epic",
|
|
439
|
+
"repo",
|
|
440
|
+
"reference",
|
|
441
|
+
"branch",
|
|
442
|
+
"base",
|
|
443
|
+
"source-phase",
|
|
444
|
+
"json",
|
|
445
|
+
],
|
|
446
|
+
required: ["repo"],
|
|
447
|
+
repeatable: ["reference"],
|
|
448
|
+
},
|
|
426
449
|
list: {
|
|
427
450
|
usage: "agency task list [filters] [--json]",
|
|
428
451
|
minArgs: 0,
|
|
@@ -1231,6 +1254,12 @@ function validateTaskCreate(
|
|
|
1231
1254
|
spec: LeafCommand,
|
|
1232
1255
|
requireRepo: boolean,
|
|
1233
1256
|
) {
|
|
1257
|
+
if (values.purpose !== undefined && values.purpose !== "investigation") {
|
|
1258
|
+
throw usageError(
|
|
1259
|
+
`Invalid '--purpose' value '${String(values.purpose)}'. Expected: investigation.`,
|
|
1260
|
+
spec.usage,
|
|
1261
|
+
)
|
|
1262
|
+
}
|
|
1234
1263
|
const review = values.review !== undefined
|
|
1235
1264
|
const pullRequest = values["pull-request"] !== undefined
|
|
1236
1265
|
const ref = values.ref !== undefined
|
|
@@ -86,6 +86,55 @@ describe("task and phase command JSON output", () => {
|
|
|
86
86
|
})
|
|
87
87
|
})
|
|
88
88
|
|
|
89
|
+
test("returns complete handoff JSON without preparing work", async () => {
|
|
90
|
+
await runTestEffect(
|
|
91
|
+
task({
|
|
92
|
+
subcommand: "create",
|
|
93
|
+
args: ["investigate"],
|
|
94
|
+
repo: "agency",
|
|
95
|
+
purpose: "investigation",
|
|
96
|
+
cwd: root,
|
|
97
|
+
silent: true,
|
|
98
|
+
}),
|
|
99
|
+
)
|
|
100
|
+
const logs = await captureLogs(() =>
|
|
101
|
+
runTestEffect(
|
|
102
|
+
task({
|
|
103
|
+
subcommand: "handoff",
|
|
104
|
+
args: ["investigate", "implement"],
|
|
105
|
+
repo: "agency",
|
|
106
|
+
cwd: root,
|
|
107
|
+
json: true,
|
|
108
|
+
}),
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
const output = JSON.parse(logs[0]!)
|
|
112
|
+
expect(output).toMatchObject({
|
|
113
|
+
task: {
|
|
114
|
+
id: "implement",
|
|
115
|
+
selector: "task/implement",
|
|
116
|
+
directory: join(root, "tasks/implement"),
|
|
117
|
+
documentPath: join(root, "tasks/implement/TASK.md"),
|
|
118
|
+
branch: "task/implement",
|
|
119
|
+
base: "main",
|
|
120
|
+
},
|
|
121
|
+
source: {
|
|
122
|
+
selector: "task/investigate",
|
|
123
|
+
documentPath: join(root, "tasks/investigate/TASK.md"),
|
|
124
|
+
},
|
|
125
|
+
validation: { valid: true },
|
|
126
|
+
worktreePrepare: {
|
|
127
|
+
target: "task/implement",
|
|
128
|
+
command: ["agency", "worktree", "prepare", "implement"],
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
expect(output.task.revision).toMatch(/^[a-f0-9]{64}$/)
|
|
132
|
+
expect(output.source.revision).toMatch(/^[a-f0-9]{64}$/)
|
|
133
|
+
expect(await Bun.file(join(root, "tasks/implement/code")).exists()).toBe(
|
|
134
|
+
false,
|
|
135
|
+
)
|
|
136
|
+
})
|
|
137
|
+
|
|
89
138
|
test("lists and shows phase metadata without Markdown content", async () => {
|
|
90
139
|
const listLogs = await captureLogs(() =>
|
|
91
140
|
runTestEffect(
|
package/src/commands/task.ts
CHANGED
|
@@ -53,6 +53,8 @@ interface TaskOptions extends BaseCommandOptions {
|
|
|
53
53
|
readonly contextBase?: string
|
|
54
54
|
readonly contextSlug?: string
|
|
55
55
|
readonly authoritativeSources?: readonly string[]
|
|
56
|
+
readonly purpose?: string
|
|
57
|
+
readonly sourcePhase?: string
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
export interface TaskInteraction {
|
|
@@ -197,6 +199,7 @@ export const task = (
|
|
|
197
199
|
repos: parseRepositoryReferences(options.references),
|
|
198
200
|
branch: multiPhase ? undefined : (options.branch ?? `task/${id}`),
|
|
199
201
|
base: multiPhase ? undefined : (options.base ?? "main"),
|
|
202
|
+
purpose: options.purpose as "investigation" | undefined,
|
|
200
203
|
},
|
|
201
204
|
cwd,
|
|
202
205
|
)
|
|
@@ -289,6 +292,7 @@ export const task = (
|
|
|
289
292
|
? undefined
|
|
290
293
|
: (options.branch ?? `task/${id}`),
|
|
291
294
|
base: multiPhase || review ? undefined : base,
|
|
295
|
+
purpose: options.purpose as "investigation" | undefined,
|
|
292
296
|
},
|
|
293
297
|
cwd,
|
|
294
298
|
)
|
|
@@ -321,6 +325,40 @@ export const task = (
|
|
|
321
325
|
)
|
|
322
326
|
return
|
|
323
327
|
}
|
|
328
|
+
case "handoff": {
|
|
329
|
+
const [sourceTaskId, id] = options.args
|
|
330
|
+
if (!sourceTaskId || !id) {
|
|
331
|
+
return yield* Effect.fail(
|
|
332
|
+
new Error("Source task ID and new task ID are required"),
|
|
333
|
+
)
|
|
334
|
+
}
|
|
335
|
+
if (!options.repo) {
|
|
336
|
+
return yield* Effect.fail(
|
|
337
|
+
new Error("Writable repository is required for task handoff"),
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
const output = yield* tasks.handoff(
|
|
341
|
+
{
|
|
342
|
+
sourceTaskId,
|
|
343
|
+
sourcePhaseId: options.sourcePhase,
|
|
344
|
+
id,
|
|
345
|
+
ticketUrl: options.ticketUrl?.trim() || null,
|
|
346
|
+
description: options.description?.trim() || undefined,
|
|
347
|
+
epic: options.epic,
|
|
348
|
+
repo: options.repo,
|
|
349
|
+
repos: parseRepositoryReferences(options.references),
|
|
350
|
+
branch: options.branch ?? `task/${id}`,
|
|
351
|
+
base: options.base ?? "main",
|
|
352
|
+
},
|
|
353
|
+
cwd,
|
|
354
|
+
)
|
|
355
|
+
log(
|
|
356
|
+
options.json
|
|
357
|
+
? JSON.stringify(output, null, 2)
|
|
358
|
+
: `Created implementation task '${id}' from '${output.source.selector}'`,
|
|
359
|
+
)
|
|
360
|
+
return
|
|
361
|
+
}
|
|
324
362
|
case "list": {
|
|
325
363
|
const records = yield* tasks.list(cwd)
|
|
326
364
|
const { taskRows } = yield* getWorkViews({
|
|
@@ -507,7 +545,7 @@ export const task = (
|
|
|
507
545
|
default:
|
|
508
546
|
return yield* Effect.fail(
|
|
509
547
|
new Error(
|
|
510
|
-
"Subcommand is required. Available: new, create, list, show, status, update, rename, move, dependency",
|
|
548
|
+
"Subcommand is required. Available: new, create, handoff, list, show, status, update, rename, move, dependency",
|
|
511
549
|
),
|
|
512
550
|
)
|
|
513
551
|
}
|
|
@@ -519,6 +557,7 @@ Usage: agency task <subcommand>
|
|
|
519
557
|
Subcommands:
|
|
520
558
|
new [id] Create a task with guided input
|
|
521
559
|
create <id> Create a task without prompting
|
|
560
|
+
handoff <source> <id> Create implementation work from an investigation
|
|
522
561
|
list List tasks
|
|
523
562
|
show <id> Show a task
|
|
524
563
|
status <id> <status> Set open, working, dropped, or explicit non-PR done
|
|
@@ -539,6 +578,8 @@ Non-PR completion options (status done only):
|
|
|
539
578
|
Create options:
|
|
540
579
|
--ticket-url <url> External ticket URL (optional)
|
|
541
580
|
--description <text> Short description of the task
|
|
581
|
+
--purpose investigation
|
|
582
|
+
Generate an investigation-only task
|
|
542
583
|
--epic <id> Parent epic
|
|
543
584
|
--repo <alias> Writable repository
|
|
544
585
|
--reference <alias>:<ref>
|
|
@@ -558,6 +599,11 @@ Create options:
|
|
|
558
599
|
--work Start work on the new task after creating it
|
|
559
600
|
--auto Pass --auto to work; requires --work
|
|
560
601
|
|
|
602
|
+
Handoff options:
|
|
603
|
+
--source-phase <id> Use a phase document as the source evidence
|
|
604
|
+
--repo <alias> Writable implementation repository
|
|
605
|
+
--reference, --branch, --base, --ticket-url, --description, --epic
|
|
606
|
+
|
|
561
607
|
Update options:
|
|
562
608
|
--ticket-url <url> / --clear-ticket
|
|
563
609
|
--description <text> / --clear-description
|
package/src/graph-schema.test.ts
CHANGED
|
@@ -27,6 +27,14 @@ describe("graph contract", () => {
|
|
|
27
27
|
readiness: { type: "null" },
|
|
28
28
|
aggregate: { type: "null" },
|
|
29
29
|
})
|
|
30
|
+
expect(jsonSchema.$defs.taskHandoff).toMatchObject({
|
|
31
|
+
additionalProperties: false,
|
|
32
|
+
required: ["source", "sourceRevision"],
|
|
33
|
+
})
|
|
34
|
+
expect(jsonSchema.$defs.singleTaskData.properties).toMatchObject({
|
|
35
|
+
purpose: { $ref: "#/$defs/taskPurpose" },
|
|
36
|
+
handoff: { $ref: "#/$defs/taskHandoff" },
|
|
37
|
+
})
|
|
30
38
|
})
|
|
31
39
|
|
|
32
40
|
test("streams records that reconstruct graph semantics", () => {
|
package/src/graph-schema.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
PullRequestRecord,
|
|
6
6
|
ClaimRecord,
|
|
7
7
|
ReviewRecord,
|
|
8
|
+
TaskHandoff,
|
|
8
9
|
TaskFrontmatter,
|
|
9
10
|
WorkStatus,
|
|
10
11
|
} from "./workbase/schemas"
|
|
@@ -80,6 +81,10 @@ export const GraphExecutionData = Schema.Union(
|
|
|
80
81
|
phaseId: Schema.optional(Schema.String),
|
|
81
82
|
ticketUrl: Schema.optional(Schema.NullOr(Schema.String)),
|
|
82
83
|
epic: Schema.optional(Schema.String),
|
|
84
|
+
purpose: Schema.optional(
|
|
85
|
+
Schema.Literal("investigation", "implementation"),
|
|
86
|
+
),
|
|
87
|
+
handoff: Schema.optional(TaskHandoff),
|
|
83
88
|
}),
|
|
84
89
|
),
|
|
85
90
|
Schema.Struct({
|
|
@@ -87,6 +92,8 @@ export const GraphExecutionData = Schema.Union(
|
|
|
87
92
|
ticketUrl: Schema.NullOr(Schema.String),
|
|
88
93
|
description: Schema.optional(Schema.String),
|
|
89
94
|
epic: Schema.optional(Schema.String),
|
|
95
|
+
purpose: Schema.optional(Schema.Literal("investigation", "implementation")),
|
|
96
|
+
handoff: Schema.optional(TaskHandoff),
|
|
90
97
|
review: ReviewRecord,
|
|
91
98
|
status: WorkStatus,
|
|
92
99
|
claim: Schema.optional(ClaimRecord),
|
|
@@ -734,6 +734,11 @@ describe("ArchiveService", () => {
|
|
|
734
734
|
repo: "agency",
|
|
735
735
|
branch: "task/child",
|
|
736
736
|
base: "main",
|
|
737
|
+
purpose: "implementation",
|
|
738
|
+
handoff: {
|
|
739
|
+
source: { kind: "task", taskId: "investigation" },
|
|
740
|
+
sourceRevision: "a".repeat(64),
|
|
741
|
+
},
|
|
737
742
|
},
|
|
738
743
|
root,
|
|
739
744
|
),
|
|
@@ -787,6 +792,18 @@ describe("ArchiveService", () => {
|
|
|
787
792
|
),
|
|
788
793
|
)
|
|
789
794
|
expect(epic.data.tasks).toEqual([{ id: "child" }])
|
|
795
|
+
const restoredTask = await runTestEffect(
|
|
796
|
+
TaskService.pipe(
|
|
797
|
+
Effect.flatMap((service) => service.show("child", root)),
|
|
798
|
+
),
|
|
799
|
+
)
|
|
800
|
+
expect(restoredTask.data).toMatchObject({
|
|
801
|
+
purpose: "implementation",
|
|
802
|
+
handoff: {
|
|
803
|
+
source: { kind: "task", taskId: "investigation" },
|
|
804
|
+
sourceRevision: "a".repeat(64),
|
|
805
|
+
},
|
|
806
|
+
})
|
|
790
807
|
const provenance = await Bun.file(
|
|
791
808
|
join(root, "tasks/child/.agency-lifecycle.json"),
|
|
792
809
|
).json()
|
|
@@ -1040,7 +1057,7 @@ describe("ArchiveService", () => {
|
|
|
1040
1057
|
),
|
|
1041
1058
|
),
|
|
1042
1059
|
),
|
|
1043
|
-
).rejects.toThrow("is archived;
|
|
1060
|
+
).rejects.toThrow("is archived; explicit creation requires a different ID")
|
|
1044
1061
|
})
|
|
1045
1062
|
|
|
1046
1063
|
test("does not remove worktrees when another lifecycle mutation holds the lock", async () => {
|
|
@@ -326,6 +326,54 @@ describe("GraphMutationService", () => {
|
|
|
326
326
|
)
|
|
327
327
|
})
|
|
328
328
|
|
|
329
|
+
test("preserves historical handoff provenance across source and destination renames", async () => {
|
|
330
|
+
await runTestEffect(
|
|
331
|
+
Effect.gen(function* () {
|
|
332
|
+
const tasks = yield* TaskService
|
|
333
|
+
yield* tasks.create(
|
|
334
|
+
{
|
|
335
|
+
id: "investigation",
|
|
336
|
+
ticketUrl: null,
|
|
337
|
+
repo: "agency",
|
|
338
|
+
branch: "task/investigation",
|
|
339
|
+
base: "main",
|
|
340
|
+
purpose: "investigation",
|
|
341
|
+
},
|
|
342
|
+
root,
|
|
343
|
+
)
|
|
344
|
+
yield* tasks.handoff(
|
|
345
|
+
{
|
|
346
|
+
sourceTaskId: "investigation",
|
|
347
|
+
id: "implementation",
|
|
348
|
+
ticketUrl: null,
|
|
349
|
+
repo: "docs",
|
|
350
|
+
branch: "task/implementation",
|
|
351
|
+
base: "main",
|
|
352
|
+
},
|
|
353
|
+
root,
|
|
354
|
+
)
|
|
355
|
+
const before = yield* tasks.show("implementation", root)
|
|
356
|
+
const mutations = yield* GraphMutationService
|
|
357
|
+
yield* mutations.renameTask(
|
|
358
|
+
"investigation",
|
|
359
|
+
"investigation-renamed",
|
|
360
|
+
root,
|
|
361
|
+
)
|
|
362
|
+
yield* mutations.renameTask(
|
|
363
|
+
"implementation",
|
|
364
|
+
"implementation-renamed",
|
|
365
|
+
root,
|
|
366
|
+
)
|
|
367
|
+
const after = yield* tasks.show("implementation-renamed", root)
|
|
368
|
+
expect(after.data.handoff).toEqual(before.data.handoff)
|
|
369
|
+
expect(after.data.handoff?.source).toEqual({
|
|
370
|
+
kind: "task",
|
|
371
|
+
taskId: "investigation",
|
|
372
|
+
})
|
|
373
|
+
}),
|
|
374
|
+
)
|
|
375
|
+
})
|
|
376
|
+
|
|
329
377
|
test("moves task membership in both directions", async () => {
|
|
330
378
|
await runTestEffect(
|
|
331
379
|
Effect.gen(function* () {
|
|
@@ -420,6 +420,13 @@ describe("IntegrationService", () => {
|
|
|
420
420
|
expect(body).toContain("authority.writable.checkoutPath")
|
|
421
421
|
expect(body).toContain("Only `done` satisfies a dependency")
|
|
422
422
|
expect(body).toContain("Require explicit user intent")
|
|
423
|
+
expect(body).toContain(
|
|
424
|
+
"explicit request for a new, separate, or follow-up item",
|
|
425
|
+
)
|
|
426
|
+
expect(body).toContain("agency task handoff")
|
|
427
|
+
expect(body).toContain(
|
|
428
|
+
"Creation and handoff do not imply worktree preparation",
|
|
429
|
+
)
|
|
423
430
|
expect(body).toContain("changing repository")
|
|
424
431
|
expect(body).toMatch(
|
|
425
432
|
/archiving, restoring,\s+dropping, reopening, or completing work without a pull request/,
|
|
@@ -517,6 +524,9 @@ describe("IntegrationService", () => {
|
|
|
517
524
|
})
|
|
518
525
|
expect(config.agent.agency.model).toBeUndefined()
|
|
519
526
|
expect(config.agent.agency.permission).toBeUndefined()
|
|
527
|
+
expect(config.agent["agency-plan"].prompt).toContain(
|
|
528
|
+
"Explicit-new intent overrides reuse",
|
|
529
|
+
)
|
|
520
530
|
expect(config.agent.agency.hidden).toBeUndefined()
|
|
521
531
|
expect(config.agent.agency.steps).toBeUndefined()
|
|
522
532
|
expect(config.agent.agency.prompt).toContain(
|
|
@@ -25,6 +25,18 @@ export interface TransactionStep {
|
|
|
25
25
|
readonly manualRecovery?: string
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
export const pathMustNotExistStep = (
|
|
29
|
+
root: string,
|
|
30
|
+
path: string,
|
|
31
|
+
message: string,
|
|
32
|
+
): TransactionStep => ({
|
|
33
|
+
label: `verify ${relative(root, path)} is available`,
|
|
34
|
+
preflight: async () => {
|
|
35
|
+
if (await exists(path)) throw new Error(message)
|
|
36
|
+
},
|
|
37
|
+
apply: async () => {},
|
|
38
|
+
})
|
|
39
|
+
|
|
28
40
|
interface DocumentWrite {
|
|
29
41
|
readonly path: string
|
|
30
42
|
readonly content: string
|
|
@@ -249,6 +249,8 @@ export class PhaseService extends Effect.Service<PhaseService>()(
|
|
|
249
249
|
? { description: task.data.description }
|
|
250
250
|
: {}),
|
|
251
251
|
...(task.data.epic ? { epic: task.data.epic } : {}),
|
|
252
|
+
...(task.data.purpose ? { purpose: task.data.purpose } : {}),
|
|
253
|
+
...(task.data.handoff ? { handoff: task.data.handoff } : {}),
|
|
252
254
|
phases: [
|
|
253
255
|
{ id: firstPhaseId! },
|
|
254
256
|
{
|