@markjaquith/agency 2.58.2 → 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`
@@ -925,6 +967,15 @@ conflicting worktrees. Repair is deliberately conservative: it repairs safe Git
925
967
  registration issues and materializes missing checkouts, but never switches a
926
968
  branch, resets a commit, or discards uncommitted work.
927
969
 
970
+ For jj, explicit removal is a suspend operation. Agency records each workspace's
971
+ exact `@` commit and stable change ID in `.agency-jj-resume.json`, roots that
972
+ commit with an internal local bookmark, and only then forgets the workspace.
973
+ The next `work prepare` validates all three identities, restores the workspace by
974
+ editing the recorded commit, and consumes the resume metadata and bookmarks.
975
+ Missing, ambiguous, or conflicting resume state stops instead of falling back to
976
+ the declared delivery bookmark, `@-`, or the base. Unknown checkout cleanliness
977
+ also stops removal. Git worktree removal and preparation are unchanged.
978
+
928
979
  Agency launches every agent beside its epic or task document. Single-phase tasks
929
980
  and phases first fetch repositories and create or reuse worktrees under `code/`,
930
981
  then launch the execution agent from the task directory with absolute context
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.58.2",
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: