@markjaquith/agency 2.59.0 → 2.60.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
@@ -684,6 +684,32 @@ The branch defaults to `task/<id>` and the base defaults to `main`.
684
684
  `task create` is always noninteractive and requires `--repo` for a single-phase
685
685
  task. Use it instead of `task new` in scripts and agent workflows.
686
686
 
687
+ For deterministic callers, creation also accepts recalled context:
688
+
689
+ ```text
690
+ agency task create <id>
691
+ --context-repo <alias> --context-base <base> --context-slug <id>
692
+ [--authoritative-source <absolute-path-or-http-url>...] --json
693
+ ```
694
+
695
+ Recalled values are used instead of rediscovery but must agree with equivalent
696
+ explicit flags. The preferred slug must equal the created ID, repository aliases
697
+ are validated by normal task creation, and authoritative sources must be absolute
698
+ paths or HTTP(S) URLs.
699
+
700
+ Machine output adds fields without changing the version 1 protocol envelope. It
701
+ includes `selector`, absolute `documentPath`, the document `revision`, full
702
+ `validation`, normalized `recalledContext`, and `evidence`. Evidence version 1 is
703
+ an auditable local payload containing the canonical workbase root, target and
704
+ document identities, aggregate workbase revision, configuration revision,
705
+ repository-mapping revision, kickoff-contract version, validity result, recalled
706
+ context, and a digest over those fields. It is not a signature or an authority
707
+ grant. A different workbase, target, document revision, document set,
708
+ configuration, repository mapping, contract version, or payload digest
709
+ invalidates reuse. Older creation output without evidence remains compatible;
710
+ preflight simply validates again. The published machine schema is
711
+ `schemas/agency-kickoff-v1.schema.json`.
712
+
687
713
  Create a multi-phase task container:
688
714
 
689
715
  ```text
@@ -892,7 +918,7 @@ until restored.
892
918
 
893
919
  ```text
894
920
  agency work [<directory> | --epic <epic-id>] [--runner <name>] [--auto] [--print-command]
895
- agency work prepare [target] [--dry-run] [--json]
921
+ agency work prepare [target] [--evidence <json-or-path>] [--dry-run] [--json]
896
922
  agency worktree <list|inspect|prepare|remove|rebuild|repair>
897
923
  agency push [--json]
898
924
  agency pr create <task-id> [phase-id] [--draft] [--force] [--json]
@@ -912,9 +938,25 @@ their built-in presets. Launches are interactive and promptless by default; use
912
938
  `agency work prepare` resolves an execution unit and creates or reuses its
913
939
  writable and reference worktrees, or its single pinned review checkout, without
914
940
  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.
941
+ Its JSON result includes the workspace, validation result, whether supplied
942
+ evidence was `reused` or `refreshed` with stable reason strings, refreshed
943
+ evidence, and a versioned `agency-kickoff-v1` orchestration plan. The plan has a
944
+ deterministic idempotency key and ordered, retry-safe actions for worktree
945
+ preflight/preparation, a background tab, side-by-side task document,
946
+ `agency work . --auto`, and exactly one final `agency context <document> --json`.
947
+ The evidence argument may be an evidence object, task-creation JSON, or a path to
948
+ either. Use `--dry-run` to report planned fetch, branch, and worktree changes
949
+ without applying them. Validation reuse never skips readiness, active-claim,
950
+ repository, ownership, reference-drift, dirty-workspace, or worktree safety
951
+ checks.
952
+
953
+ The authoritative implementation locations for this contract are
954
+ `src/commands/task.ts` (creation output),
955
+ `src/workbase/kickoff-contract.ts` (evidence and orchestration schemas),
956
+ `src/commands/work.ts` (launch preflight),
957
+ `src/services/WorktreeService.ts` (workspace safety), and
958
+ `src/workbase/AGENTS.md` (generated OpenCode guidance). These paths are the
959
+ deterministic source-location fixture for compatible orchestrators.
918
960
 
919
961
  `agency worktree list` and `inspect` report each declared checkout's expected and
920
962
  registered path, branch, commit, Agency owner, dirtiness, and conflicts. `prepare`
package/cli-main.ts CHANGED
@@ -494,6 +494,10 @@ 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"],
497
501
  clearReferences: options["clear-references"],
498
502
  prUrl: options["pr-url"],
499
503
  clearPr: options["clear-pr"],
@@ -560,6 +564,7 @@ const commands: Record<string, Command> = {
560
564
  cwd: options.cwd,
561
565
  taskId: options.task,
562
566
  phaseId: options.phase,
567
+ evidence: options.evidence,
563
568
  }),
564
569
  )
565
570
  },
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.60.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
  },
@@ -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
+ }
@@ -809,7 +809,7 @@ describe("strict CLI parsing", () => {
809
809
 
810
810
  test("rejects required-option omissions and explicit conflicts", () => {
811
811
  expect(() => parseCli(["task", "create", "example"])).toThrow(
812
- "--repo' is required",
812
+ "--repo' or '--context-repo' is required",
813
813
  )
814
814
  for (const option of ["repo", "reference", "branch", "base"] as const) {
815
815
  const value = option === "reference" ? "other:main" : "value"
package/src/cli-parser.ts CHANGED
@@ -85,6 +85,10 @@ 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 },
88
92
  } satisfies OptionConfig
89
93
 
90
94
  const phaseCreateOptions = {
@@ -396,7 +400,7 @@ const commands = {
396
400
  },
397
401
  create: {
398
402
  usage:
399
- "agency task create <id> (--repo <alias> | --multi-phase | --review <alias> (--pull-request <value> | --ref <remote-ref>)) [options]",
403
+ "agency task create <id> (--repo <alias> | --context-repo <alias> | --multi-phase | --review <alias> (--pull-request <value> | --ref <remote-ref>)) [options]",
400
404
  minArgs: 1,
401
405
  maxArgs: 1,
402
406
  options: [
@@ -411,9 +415,13 @@ const commands = {
411
415
  "review",
412
416
  "pull-request",
413
417
  "ref",
418
+ "context-repo",
419
+ "context-base",
420
+ "context-slug",
421
+ "authoritative-source",
414
422
  "json",
415
423
  ],
416
- repeatable: ["reference"],
424
+ repeatable: ["reference", "authoritative-source"],
417
425
  },
418
426
  list: {
419
427
  usage: "agency task list [filters] [--json]",
@@ -873,6 +881,7 @@ const commands = {
873
881
  opencode: { type: "boolean" },
874
882
  claude: { type: "boolean" },
875
883
  force: { type: "boolean" },
884
+ evidence: { type: "string" },
876
885
  },
877
886
  command: {
878
887
  usage:
@@ -891,6 +900,7 @@ const commands = {
891
900
  "opencode",
892
901
  "claude",
893
902
  "force",
903
+ "evidence",
894
904
  ],
895
905
  conflicts: [
896
906
  ["opencode", "claude"],
@@ -1262,9 +1272,13 @@ function validateTaskCreate(
1262
1272
  )
1263
1273
  }
1264
1274
  }
1265
- } else if (requireRepo && values.repo === undefined) {
1275
+ } else if (
1276
+ requireRepo &&
1277
+ values.repo === undefined &&
1278
+ values["context-repo"] === undefined
1279
+ ) {
1266
1280
  throw usageError(
1267
- "Option '--repo' is required unless '--multi-phase' is used.",
1281
+ "Option '--repo' or '--context-repo' is required unless '--multi-phase' is used.",
1268
1282
  spec.usage,
1269
1283
  )
1270
1284
  }
@@ -1620,9 +1634,12 @@ export function parseCli(args: readonly string[]): ParsedCli {
1620
1634
  spec.usage,
1621
1635
  )
1622
1636
  }
1623
- if (!preparing && (parsed.values.json || parsed.values["dry-run"])) {
1637
+ if (
1638
+ !preparing &&
1639
+ (parsed.values.json || parsed.values["dry-run"] || parsed.values.evidence)
1640
+ ) {
1624
1641
  throw usageError(
1625
- "Options '--json' and '--dry-run' are only valid with 'agency work prepare'.",
1642
+ "Options '--json', '--dry-run', and '--evidence' are only valid with 'agency work prepare'.",
1626
1643
  spec.usage,
1627
1644
  )
1628
1645
  }
@@ -2,7 +2,12 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
2
  import { Effect } from "effect"
3
3
  import { mkdir } from "node:fs/promises"
4
4
  import { join } from "node:path"
5
- import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
5
+ import {
6
+ captureLogs,
7
+ cleanupTempDir,
8
+ createTempDir,
9
+ runTestEffect,
10
+ } from "../test-utils"
6
11
  import { task, type TaskInteraction } from "./task"
7
12
 
8
13
  describe("task creation input", () => {
@@ -125,6 +130,77 @@ describe("task creation input", () => {
125
130
  expect(content).toContain("base: main")
126
131
  })
127
132
 
133
+ test("returns revision-bound validation evidence and normalized recalled context", async () => {
134
+ const [output] = await captureLogs(() =>
135
+ runTestEffect(
136
+ task({
137
+ subcommand: "create",
138
+ args: ["scripted-context"],
139
+ contextRepo: "agency",
140
+ contextBase: "main",
141
+ contextSlug: "scripted-context",
142
+ authoritativeSources: [
143
+ "https://example.com/spec",
144
+ join(root, "SOURCE.md"),
145
+ ],
146
+ cwd: root,
147
+ json: true,
148
+ }),
149
+ ),
150
+ )
151
+ const result = JSON.parse(output!)
152
+ expect(result.selector).toBe("execution-unit:task/scripted-context")
153
+ expect(result.documentPath).toBe(
154
+ join(root, "tasks/scripted-context/TASK.md"),
155
+ )
156
+ expect(result.validation.valid).toBe(true)
157
+ expect(result.evidence).toEqual(
158
+ expect.objectContaining({
159
+ version: 1,
160
+ target: "execution-unit:task/scripted-context",
161
+ documentRevision: result.revision,
162
+ valid: true,
163
+ digest: expect.any(String),
164
+ }),
165
+ )
166
+ expect(result.recalledContext).toEqual({
167
+ repo: "agency",
168
+ base: "main",
169
+ preferredSlug: "scripted-context",
170
+ authoritativeSources: [
171
+ join(root, "SOURCE.md"),
172
+ "https://example.com/spec",
173
+ ],
174
+ })
175
+ })
176
+
177
+ test("rejects stale or conflicting recalled context", async () => {
178
+ await expect(
179
+ runTestEffect(
180
+ task({
181
+ subcommand: "create",
182
+ args: ["conflict"],
183
+ repo: "agency",
184
+ contextRepo: "other",
185
+ cwd: root,
186
+ silent: true,
187
+ }),
188
+ ),
189
+ ).rejects.toThrow("conflicts with --repo")
190
+ await expect(
191
+ runTestEffect(
192
+ task({
193
+ subcommand: "create",
194
+ args: ["conflict"],
195
+ contextRepo: "agency",
196
+ contextSlug: "stale-slug",
197
+ cwd: root,
198
+ silent: true,
199
+ }),
200
+ ),
201
+ ).rejects.toThrow("conflicts with task ID")
202
+ })
203
+
128
204
  test("never prompts when scripted creation is incomplete", async () => {
129
205
  const interaction: TaskInteraction = {
130
206
  text: () => Effect.fail(new Error("unexpected text prompt")),
@@ -12,6 +12,10 @@ import { getWorkViews } from "../work-view"
12
12
  import { GraphMutationService } from "../services/GraphMutationService"
13
13
  import { work as startWork, type StartWork } from "./work"
14
14
  import { ReviewService } from "../services/ReviewService"
15
+ import {
16
+ buildValidationEvidence,
17
+ normalizeRecalledContext,
18
+ } from "../workbase/kickoff-contract"
15
19
 
16
20
  interface TaskOptions extends BaseCommandOptions {
17
21
  readonly subcommand?: string
@@ -45,6 +49,10 @@ interface TaskOptions extends BaseCommandOptions {
45
49
  readonly noPullRequest?: boolean
46
50
  readonly summary?: string
47
51
  readonly evidenceUrl?: string
52
+ readonly contextRepo?: string
53
+ readonly contextBase?: string
54
+ readonly contextSlug?: string
55
+ readonly authoritativeSources?: readonly string[]
48
56
  }
49
57
 
50
58
  export interface TaskInteraction {
@@ -216,7 +224,43 @@ export const task = (
216
224
  return yield* Effect.fail(new Error("Task ID is required"))
217
225
  }
218
226
  const multiPhase = options.multiPhase ?? false
219
- if (!multiPhase && !options.repo && !options.review) {
227
+ if (
228
+ options.repo &&
229
+ options.contextRepo &&
230
+ options.repo !== options.contextRepo
231
+ ) {
232
+ return yield* Effect.fail(
233
+ new Error(
234
+ `Recalled repository '${options.contextRepo}' conflicts with --repo '${options.repo}'`,
235
+ ),
236
+ )
237
+ }
238
+ if (
239
+ options.base &&
240
+ options.contextBase &&
241
+ options.base !== options.contextBase
242
+ ) {
243
+ return yield* Effect.fail(
244
+ new Error(
245
+ `Recalled base '${options.contextBase}' conflicts with --base '${options.base}'`,
246
+ ),
247
+ )
248
+ }
249
+ const repo = options.repo ?? options.contextRepo
250
+ const base = options.base ?? options.contextBase ?? "main"
251
+ const recalledContext = yield* Effect.try({
252
+ try: () =>
253
+ normalizeRecalledContext({
254
+ id,
255
+ repo,
256
+ base: multiPhase || options.review ? undefined : base,
257
+ preferredSlug: options.contextSlug,
258
+ authoritativeSources: options.authoritativeSources,
259
+ }),
260
+ catch: (cause) =>
261
+ cause instanceof Error ? cause : new Error(String(cause)),
262
+ })
263
+ if (!multiPhase && !repo && !options.review) {
220
264
  return yield* Effect.fail(
221
265
  new Error("Writable repository is required for task create"),
222
266
  )
@@ -236,7 +280,7 @@ export const task = (
236
280
  epic: options.epic,
237
281
  multiPhase,
238
282
  review,
239
- repo: options.repo,
283
+ repo: review ? undefined : repo,
240
284
  repos: review
241
285
  ? undefined
242
286
  : parseRepositoryReferences(options.references),
@@ -244,11 +288,32 @@ export const task = (
244
288
  multiPhase || review
245
289
  ? undefined
246
290
  : (options.branch ?? `task/${id}`),
247
- base: multiPhase || review ? undefined : (options.base ?? "main"),
291
+ base: multiPhase || review ? undefined : base,
248
292
  },
249
293
  cwd,
250
294
  )
251
- const { content: _, ...output } = record
295
+ const validation = yield* workbase.validate(cwd)
296
+ const selector = multiPhase
297
+ ? `task:${record.id}`
298
+ : `execution-unit:task/${record.id}`
299
+ const evidence = validation.valid
300
+ ? yield* buildValidationEvidence({
301
+ startPath: cwd,
302
+ target: selector,
303
+ documentPath: record.path,
304
+ documentRevision: record.revision,
305
+ recalledContext,
306
+ })
307
+ : null
308
+ const { content: _, ...created } = record
309
+ const output = {
310
+ ...created,
311
+ selector,
312
+ documentPath: record.path,
313
+ validation,
314
+ evidence,
315
+ recalledContext,
316
+ }
252
317
  log(
253
318
  options.json
254
319
  ? JSON.stringify(output, null, 2)
@@ -480,6 +545,11 @@ Create options:
480
545
  Read-only repository reference; repeatable
481
546
  --branch <name> Working branch (default: task/<id>)
482
547
  --base <name> Base branch (default: main)
548
+ --context-repo <alias> Recalled repository; must agree with --repo
549
+ --context-base <name> Recalled base; must agree with --base
550
+ --context-slug <id> Recalled preferred slug; must equal the task ID
551
+ --authoritative-source <path-or-url>
552
+ Authoritative input location; repeatable
483
553
  --multi-phase Create a task container for phases
484
554
  --review <alias> Create a pinned read-only review task
485
555
  --pull-request <url-or-number>
@@ -497,8 +567,8 @@ Update options:
497
567
  --base <name> Replace the base branch
498
568
  --pr-url <url> / --clear-pr
499
569
 
500
- Task creation is noninteractive. Single-phase tasks require --repo; use
501
- --multi-phase instead for a task container. Guided input is available only
570
+ Task creation is noninteractive. Single-phase tasks require --repo or
571
+ --context-repo; use --multi-phase instead for a task container. Guided input is available only
502
572
  through task new, which fails when --no-input is set or no TTY is available.
503
573
 
504
574
  Options:
@@ -82,6 +82,7 @@ interface HarnessOptions {
82
82
  readonly phaseStatuses?: Readonly<
83
83
  Record<string, "open" | "working" | "delegated" | "done" | "dropped">
84
84
  >
85
+ readonly taskRepo?: string
85
86
  }
86
87
 
87
88
  const createHarness = (options: HarnessOptions = {}) => {
@@ -138,6 +139,16 @@ const createHarness = (options: HarnessOptions = {}) => {
138
139
  runners: options.runners,
139
140
  },
140
141
  }),
142
+ repositoryAliases: () => Effect.succeed(["agency"]),
143
+ validate: () =>
144
+ Effect.succeed({
145
+ root: "/workbase",
146
+ issues: [],
147
+ epicCount: 0,
148
+ taskCount: 1,
149
+ phaseCount: 0,
150
+ valid: true,
151
+ }),
141
152
  }
142
153
  const epics = {
143
154
  show: (id: string) =>
@@ -154,10 +165,11 @@ const createHarness = (options: HarnessOptions = {}) => {
154
165
  return Effect.succeed({
155
166
  id,
156
167
  path: `/workbase/tasks/${id}/TASK.md`,
168
+ revision: "a".repeat(64),
157
169
  data: options.multiPhaseTasks?.includes(id)
158
170
  ? { phases: [] }
159
171
  : {
160
- repo: "agency",
172
+ repo: options.taskRepo ?? "agency",
161
173
  branch: `task/${id}`,
162
174
  base: "main",
163
175
  status: taskStatuses[id] ?? options.taskStatus ?? "open",
@@ -182,6 +194,7 @@ const createHarness = (options: HarnessOptions = {}) => {
182
194
  taskId,
183
195
  id,
184
196
  path: `/workbase/tasks/${taskId}/phases/${id}/PHASE.md`,
197
+ revision: "b".repeat(64),
185
198
  data: {
186
199
  repo: "agency",
187
200
  branch: `task/${id}`,
@@ -246,7 +259,16 @@ const createHarness = (options: HarnessOptions = {}) => {
246
259
  }
247
260
  const fs = {
248
261
  isDirectory: (path: string) =>
249
- Effect.succeed(options.existingDirectories?.includes(path) ?? true),
262
+ Effect.succeed(
263
+ path === "/workbase/epics" || path === "/workbase/tasks"
264
+ ? false
265
+ : (options.existingDirectories?.includes(path) ?? true),
266
+ ),
267
+ readFile: (path: string) =>
268
+ Effect.succeed(path.endsWith("agency.json") ? '{"version":2}\n' : ""),
269
+ readDirectory: () => Effect.succeed([]),
270
+ exists: () => Effect.succeed(false),
271
+ realPath: (path: string) => Effect.succeed(path),
250
272
  runCommand: (args: readonly string[]) => {
251
273
  const cli = args[1]!
252
274
  events.push(`probe:${cli}`)
@@ -306,6 +328,7 @@ const createHarness = (options: HarnessOptions = {}) => {
306
328
  Effect.provideService(WorkbaseService, workbase as never),
307
329
  Effect.provideService(TaskService, tasks as never),
308
330
  Effect.provideService(PhaseService, phases as never),
331
+ Effect.provideService(ReadinessService, readiness as never),
309
332
  ) as Effect.Effect<void, unknown, never>,
310
333
  )
311
334
 
@@ -458,7 +481,7 @@ describe("work command", () => {
458
481
  test("prepares without launching or changing lifecycle status", async () => {
459
482
  const harness = createHarness({ existingDirectories: [] })
460
483
 
461
- await captureLogs(() =>
484
+ const [output] = await captureLogs(() =>
462
485
  harness.runPrepare({
463
486
  cwd: "/workbase",
464
487
  directory: "example",
@@ -473,7 +496,43 @@ describe("work command", () => {
473
496
  expect(harness.materializeOptions[0]).toMatchObject({
474
497
  json: true,
475
498
  dryRun: true,
499
+ validationAlreadyPerformed: true,
500
+ })
501
+ const result = JSON.parse(output!)
502
+ expect(result.validationEvidence.status).toBe("refreshed")
503
+ expect(result.validationEvidence.reasons).toEqual(["not-supplied"])
504
+ expect(result.kickoff.target).toBe("execution-unit:task/example")
505
+ expect(
506
+ result.kickoff.steps.filter(
507
+ ({ id }: { id: string }) => id === "final-context-verification",
508
+ ),
509
+ ).toHaveLength(1)
510
+
511
+ const [reusedOutput] = await captureLogs(() =>
512
+ harness.runPrepare({
513
+ cwd: "/workbase",
514
+ directory: "example",
515
+ json: true,
516
+ dryRun: true,
517
+ evidence: JSON.stringify(result.validationEvidence.evidence),
518
+ }),
519
+ )
520
+ expect(JSON.parse(reusedOutput!).validationEvidence).toEqual(
521
+ expect.objectContaining({ status: "reused", reasons: [] }),
522
+ )
523
+
524
+ const conflicting = createHarness({
525
+ existingDirectories: [],
526
+ taskRepo: "other",
476
527
  })
528
+ await expect(
529
+ conflicting.runPrepare({
530
+ cwd: "/workbase",
531
+ directory: "example",
532
+ dryRun: true,
533
+ evidence: JSON.stringify(result.validationEvidence.evidence),
534
+ }),
535
+ ).rejects.toThrow("Recalled repository conflicts")
477
536
  })
478
537
 
479
538
  test("launches an epic agent from an epic directory", async () => {