@markjaquith/agency 2.60.0 → 2.61.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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "2.60.0",
3
+ "version": "2.61.1",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -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,
@@ -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
package/src/cli.test.ts CHANGED
@@ -1,4 +1,11 @@
1
- import { afterAll, afterEach, describe, expect, test } from "bun:test"
1
+ import {
2
+ afterAll,
3
+ afterEach,
4
+ describe,
5
+ expect,
6
+ setDefaultTimeout,
7
+ test,
8
+ } from "bun:test"
2
9
  import { access, chmod, mkdir, realpath, stat, symlink } from "node:fs/promises"
3
10
  import { join, sep } from "node:path"
4
11
  import errorFixture from "../fixtures/protocol/error.json"
@@ -9,6 +16,8 @@ const projectRoot = join(import.meta.dir, "..")
9
16
  const cliPath = join(projectRoot, "cli.ts")
10
17
  const isolatedConfigHome = await createTempDir()
11
18
 
19
+ setDefaultTimeout(15_000)
20
+
12
21
  afterAll(() => cleanupTempDir(isolatedConfigHome))
13
22
 
14
23
  interface CliResult {
@@ -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(
@@ -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
@@ -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", () => {
@@ -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; restore it")
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(