@markjaquith/agency 2.59.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 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
@@ -684,6 +702,64 @@ The branch defaults to `task/<id>` and the base defaults to `main`.
684
702
  `task create` is always noninteractive and requires `--repo` for a single-phase
685
703
  task. Use it instead of `task new` in scripts and agent workflows.
686
704
 
705
+ For deterministic callers, creation also accepts recalled context:
706
+
707
+ ```text
708
+ agency task create <id>
709
+ --context-repo <alias> --context-base <base> --context-slug <id>
710
+ [--authoritative-source <absolute-path-or-http-url>...] --json
711
+ ```
712
+
713
+ Recalled values are used instead of rediscovery but must agree with equivalent
714
+ explicit flags. The preferred slug must equal the created ID, repository aliases
715
+ are validated by normal task creation, and authoritative sources must be absolute
716
+ paths or HTTP(S) URLs.
717
+
718
+ Machine output adds fields without changing the version 1 protocol envelope. It
719
+ includes `selector`, absolute `documentPath`, the document `revision`, full
720
+ `validation`, normalized `recalledContext`, and `evidence`. Evidence version 1 is
721
+ an auditable local payload containing the canonical workbase root, target and
722
+ document identities, aggregate workbase revision, configuration revision,
723
+ repository-mapping revision, kickoff-contract version, validity result, recalled
724
+ context, and a digest over those fields. It is not a signature or an authority
725
+ grant. A different workbase, target, document revision, document set,
726
+ configuration, repository mapping, contract version, or payload digest
727
+ invalidates reuse. Older creation output without evidence remains compatible;
728
+ preflight simply validates again. The published machine schema is
729
+ `schemas/agency-kickoff-v1.schema.json`.
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
+
687
763
  Create a multi-phase task container:
688
764
 
689
765
  ```text
@@ -892,7 +968,7 @@ until restored.
892
968
 
893
969
  ```text
894
970
  agency work [<directory> | --epic <epic-id>] [--runner <name>] [--auto] [--print-command]
895
- agency work prepare [target] [--dry-run] [--json]
971
+ agency work prepare [target] [--evidence <json-or-path>] [--dry-run] [--json]
896
972
  agency worktree <list|inspect|prepare|remove|rebuild|repair>
897
973
  agency push [--json]
898
974
  agency pr create <task-id> [phase-id] [--draft] [--force] [--json]
@@ -912,9 +988,25 @@ their built-in presets. Launches are interactive and promptless by default; use
912
988
  `agency work prepare` resolves an execution unit and creates or reuses its
913
989
  writable and reference worktrees, or its single pinned review checkout, without
914
990
  launching an agent or changing status.
915
- Its JSON result includes document and checkout paths, resolved commits, actions,
916
- and Git operations. Use `--dry-run` to report planned fetch, branch, and worktree
917
- changes without applying them.
991
+ Its JSON result includes the workspace, validation result, whether supplied
992
+ evidence was `reused` or `refreshed` with stable reason strings, refreshed
993
+ evidence, and a versioned `agency-kickoff-v1` orchestration plan. The plan has a
994
+ deterministic idempotency key and ordered, retry-safe actions for worktree
995
+ preflight/preparation, a background tab, side-by-side task document,
996
+ `agency work . --auto`, and exactly one final `agency context <document> --json`.
997
+ The evidence argument may be an evidence object, task-creation JSON, or a path to
998
+ either. Use `--dry-run` to report planned fetch, branch, and worktree changes
999
+ without applying them. Validation reuse never skips readiness, active-claim,
1000
+ repository, ownership, reference-drift, dirty-workspace, or worktree safety
1001
+ checks.
1002
+
1003
+ The authoritative implementation locations for this contract are
1004
+ `src/commands/task.ts` (creation output),
1005
+ `src/workbase/kickoff-contract.ts` (evidence and orchestration schemas),
1006
+ `src/commands/work.ts` (launch preflight),
1007
+ `src/services/WorktreeService.ts` (workspace safety), and
1008
+ `src/workbase/AGENTS.md` (generated OpenCode guidance). These paths are the
1009
+ deterministic source-location fixture for compatible orchestrators.
918
1010
 
919
1011
  `agency worktree list` and `inspect` report each declared checkout's expected and
920
1012
  registered path, branch, commit, Agency owner, dirtiness, and conflicts. `prepare`
package/cli-main.ts CHANGED
@@ -494,6 +494,12 @@ const commands: Record<string, Command> = {
494
494
  references: options.reference,
495
495
  branch: options.branch,
496
496
  base: options.base,
497
+ contextRepo: options["context-repo"],
498
+ contextBase: options["context-base"],
499
+ contextSlug: options["context-slug"],
500
+ authoritativeSources: options["authoritative-source"],
501
+ purpose: options.purpose,
502
+ sourcePhase: options["source-phase"],
497
503
  clearReferences: options["clear-references"],
498
504
  prUrl: options["pr-url"],
499
505
  clearPr: options["clear-pr"],
@@ -560,6 +566,7 @@ const commands: Record<string, Command> = {
560
566
  cwd: options.cwd,
561
567
  taskId: options.task,
562
568
  phaseId: options.phase,
569
+ evidence: options.evidence,
563
570
  }),
564
571
  )
565
572
  },
@@ -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/index.ts CHANGED
@@ -2,3 +2,4 @@ export * from "./src/workbase/schemas"
2
2
  export * from "./src/protocol"
3
3
  export * from "./src/graph-schema"
4
4
  export * from "./src/readiness"
5
+ export * from "./src/workbase/kickoff-contract"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "2.59.0",
3
+ "version": "2.61.0",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -44,6 +44,9 @@
44
44
  "./schemas/agency-graph-v1.json": {
45
45
  "default": "./schemas/agency-graph-v1.schema.json"
46
46
  },
47
+ "./schemas/agency-kickoff-v1.json": {
48
+ "default": "./schemas/agency-kickoff-v1.schema.json"
49
+ },
47
50
  "./fixtures/protocol/success.json": {
48
51
  "default": "./fixtures/protocol/success.json"
49
52
  },
@@ -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,
@@ -0,0 +1,124 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/markjaquith/agency/schemas/agency-kickoff-v1.schema.json",
4
+ "title": "Agency kickoff contract v1",
5
+ "oneOf": [
6
+ { "$ref": "#/$defs/validationEvidence" },
7
+ { "$ref": "#/$defs/kickoffPlan" }
8
+ ],
9
+ "$defs": {
10
+ "recalledContext": {
11
+ "type": "object",
12
+ "required": ["preferredSlug", "authoritativeSources"],
13
+ "properties": {
14
+ "repo": { "type": "string", "minLength": 1 },
15
+ "base": { "type": "string", "minLength": 1 },
16
+ "preferredSlug": { "type": "string", "minLength": 1 },
17
+ "authoritativeSources": {
18
+ "type": "array",
19
+ "items": { "type": "string", "minLength": 1 }
20
+ }
21
+ },
22
+ "additionalProperties": false
23
+ },
24
+ "validationEvidence": {
25
+ "type": "object",
26
+ "required": [
27
+ "version",
28
+ "workbaseRoot",
29
+ "target",
30
+ "documentPath",
31
+ "documentRevision",
32
+ "workbaseRevision",
33
+ "configRevision",
34
+ "repositoryMappingRevision",
35
+ "valid",
36
+ "recalledContext",
37
+ "digest"
38
+ ],
39
+ "properties": {
40
+ "version": { "const": 1 },
41
+ "workbaseRoot": { "type": "string" },
42
+ "target": { "type": "string" },
43
+ "documentPath": { "type": "string" },
44
+ "documentRevision": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
45
+ "workbaseRevision": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
46
+ "configRevision": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
47
+ "repositoryMappingRevision": {
48
+ "type": "string",
49
+ "pattern": "^[a-f0-9]{64}$"
50
+ },
51
+ "valid": { "const": true },
52
+ "recalledContext": { "$ref": "#/$defs/recalledContext" },
53
+ "digest": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
54
+ },
55
+ "additionalProperties": false
56
+ },
57
+ "kickoffPlan": {
58
+ "type": "object",
59
+ "required": [
60
+ "version",
61
+ "idempotencyKey",
62
+ "workbaseRoot",
63
+ "target",
64
+ "documentRevision",
65
+ "sourceLocations",
66
+ "taskDirectory",
67
+ "taskDocument",
68
+ "phaseDocument",
69
+ "preparedCheckout",
70
+ "orchestrator",
71
+ "steps",
72
+ "successFields"
73
+ ],
74
+ "properties": {
75
+ "version": { "const": 1 },
76
+ "idempotencyKey": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
77
+ "workbaseRoot": { "type": "string" },
78
+ "target": { "type": "string" },
79
+ "documentRevision": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
80
+ "sourceLocations": { "type": "array", "items": { "type": "string" } },
81
+ "taskDirectory": { "type": "string" },
82
+ "taskDocument": { "type": "string" },
83
+ "phaseDocument": { "type": ["string", "null"] },
84
+ "preparedCheckout": { "type": ["string", "null"] },
85
+ "orchestrator": {
86
+ "type": "object",
87
+ "required": [
88
+ "capability",
89
+ "knownCurrentCommandsBypassDiscovery",
90
+ "fallback"
91
+ ],
92
+ "properties": {
93
+ "capability": { "const": "agency-kickoff-v1" },
94
+ "knownCurrentCommandsBypassDiscovery": { "const": true },
95
+ "fallback": { "type": "string" }
96
+ },
97
+ "additionalProperties": false
98
+ },
99
+ "steps": {
100
+ "type": "array",
101
+ "minItems": 6,
102
+ "items": {
103
+ "type": "object",
104
+ "required": ["id"],
105
+ "properties": {
106
+ "id": { "type": "string" },
107
+ "argv": { "type": "array", "items": { "type": "string" } },
108
+ "cwd": { "type": "string" },
109
+ "action": { "type": "string" },
110
+ "path": { "type": "string" },
111
+ "retry": { "type": "string" },
112
+ "recovery": { "type": "string" },
113
+ "idempotencyKey": { "type": "string" },
114
+ "exactlyOnce": { "type": "boolean" }
115
+ },
116
+ "additionalProperties": false
117
+ }
118
+ },
119
+ "successFields": { "type": "array", "items": { "type": "string" } }
120
+ },
121
+ "additionalProperties": false
122
+ }
123
+ }
124
+ }
@@ -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([
@@ -809,7 +857,7 @@ describe("strict CLI parsing", () => {
809
857
 
810
858
  test("rejects required-option omissions and explicit conflicts", () => {
811
859
  expect(() => parseCli(["task", "create", "example"])).toThrow(
812
- "--repo' is required",
860
+ "--repo' or '--context-repo' is required",
813
861
  )
814
862
  for (const option of ["repo", "reference", "branch", "base"] as const) {
815
863
  const value = option === "reference" ? "other:main" : "value"
package/src/cli-parser.ts CHANGED
@@ -85,6 +85,12 @@ const taskCreateOptions = {
85
85
  review: { type: "string" },
86
86
  "pull-request": { type: "string" },
87
87
  ref: { type: "string" },
88
+ "context-repo": { type: "string" },
89
+ "context-base": { type: "string" },
90
+ "context-slug": { type: "string" },
91
+ "authoritative-source": { type: "string", multiple: true },
92
+ purpose: { type: "string" },
93
+ "source-phase": { type: "string" },
88
94
  } satisfies OptionConfig
89
95
 
90
96
  const phaseCreateOptions = {
@@ -363,7 +369,7 @@ const commands = {
363
369
  },
364
370
  task: {
365
371
  usage:
366
- "agency task <new|create|list|show|status|update|rename|move|dependency>",
372
+ "agency task <new|create|handoff|list|show|status|update|rename|move|dependency>",
367
373
  options: {
368
374
  ...taskCreateOptions,
369
375
  ...newWorkOptions,
@@ -386,6 +392,7 @@ const commands = {
386
392
  "reference",
387
393
  "branch",
388
394
  "base",
395
+ "purpose",
389
396
  "multi-phase",
390
397
  "work",
391
398
  "auto",
@@ -396,7 +403,7 @@ const commands = {
396
403
  },
397
404
  create: {
398
405
  usage:
399
- "agency task create <id> (--repo <alias> | --multi-phase | --review <alias> (--pull-request <value> | --ref <remote-ref>)) [options]",
406
+ "agency task create <id> (--repo <alias> | --context-repo <alias> | --multi-phase | --review <alias> (--pull-request <value> | --ref <remote-ref>)) [options]",
400
407
  minArgs: 1,
401
408
  maxArgs: 1,
402
409
  options: [
@@ -407,12 +414,36 @@ const commands = {
407
414
  "reference",
408
415
  "branch",
409
416
  "base",
417
+ "purpose",
410
418
  "multi-phase",
411
419
  "review",
412
420
  "pull-request",
413
421
  "ref",
422
+ "context-repo",
423
+ "context-base",
424
+ "context-slug",
425
+ "authoritative-source",
414
426
  "json",
415
427
  ],
428
+ repeatable: ["reference", "authoritative-source"],
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"],
416
447
  repeatable: ["reference"],
417
448
  },
418
449
  list: {
@@ -873,6 +904,7 @@ const commands = {
873
904
  opencode: { type: "boolean" },
874
905
  claude: { type: "boolean" },
875
906
  force: { type: "boolean" },
907
+ evidence: { type: "string" },
876
908
  },
877
909
  command: {
878
910
  usage:
@@ -891,6 +923,7 @@ const commands = {
891
923
  "opencode",
892
924
  "claude",
893
925
  "force",
926
+ "evidence",
894
927
  ],
895
928
  conflicts: [
896
929
  ["opencode", "claude"],
@@ -1221,6 +1254,12 @@ function validateTaskCreate(
1221
1254
  spec: LeafCommand,
1222
1255
  requireRepo: boolean,
1223
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
+ }
1224
1263
  const review = values.review !== undefined
1225
1264
  const pullRequest = values["pull-request"] !== undefined
1226
1265
  const ref = values.ref !== undefined
@@ -1262,9 +1301,13 @@ function validateTaskCreate(
1262
1301
  )
1263
1302
  }
1264
1303
  }
1265
- } else if (requireRepo && values.repo === undefined) {
1304
+ } else if (
1305
+ requireRepo &&
1306
+ values.repo === undefined &&
1307
+ values["context-repo"] === undefined
1308
+ ) {
1266
1309
  throw usageError(
1267
- "Option '--repo' is required unless '--multi-phase' is used.",
1310
+ "Option '--repo' or '--context-repo' is required unless '--multi-phase' is used.",
1268
1311
  spec.usage,
1269
1312
  )
1270
1313
  }
@@ -1620,9 +1663,12 @@ export function parseCli(args: readonly string[]): ParsedCli {
1620
1663
  spec.usage,
1621
1664
  )
1622
1665
  }
1623
- if (!preparing && (parsed.values.json || parsed.values["dry-run"])) {
1666
+ if (
1667
+ !preparing &&
1668
+ (parsed.values.json || parsed.values["dry-run"] || parsed.values.evidence)
1669
+ ) {
1624
1670
  throw usageError(
1625
- "Options '--json' and '--dry-run' are only valid with 'agency work prepare'.",
1671
+ "Options '--json', '--dry-run', and '--evidence' are only valid with 'agency work prepare'.",
1626
1672
  spec.usage,
1627
1673
  )
1628
1674
  }
@@ -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(