@markjaquith/agency 2.33.0 → 2.34.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
@@ -498,6 +498,8 @@ complete Markdown document.
498
498
  ### Epics
499
499
 
500
500
  ```text
501
+ agency epic new <id> --ticket-url <url> [--description <text>]
502
+ --repo <alias>:<ref> [--repo <alias>:<ref>...] [--work [--auto]]
501
503
  agency epic create <id> --ticket-url <url> [--description <text>] [--json]
502
504
  --repo <alias>:<ref> [--repo <alias>:<ref>...]
503
505
  agency epic list [filters] [--json]
@@ -517,9 +519,13 @@ repository is available, Agency selects it without presenting a redundant
517
519
  choice. This command requires a TTY and fails with `--no-input`:
518
520
 
519
521
  ```text
520
- agency task new [id]
522
+ agency task new [id] [--work [--auto]]
521
523
  ```
522
524
 
525
+ `--work` starts work on the newly created entity. Add `--auto` to pass the
526
+ generated context prompt to the selected runner. These launch options are also
527
+ available on `epic new` and `phase new`; they cannot be combined with `--json`.
528
+
523
529
  Create a single-phase task:
524
530
 
525
531
  ```text
@@ -590,6 +596,8 @@ phase. Dependencies remain explicit through `--depends-on`.
590
596
  ### Phases
591
597
 
592
598
  ```text
599
+ agency phase new <task-id> <phase-id>
600
+ --repo <alias> --branch <name> --base <name> [--work [--auto]]
593
601
  agency phase create <task-id> <phase-id>
594
602
  --repo <alias> --branch <name> --base <name>
595
603
  [--description <text>] [--reference <alias>:<ref>...]
package/cli.ts CHANGED
@@ -251,8 +251,11 @@ const commands: Record<string, Command> = {
251
251
  ready: options.ready,
252
252
  blocked: options.blocked,
253
253
  pr: options.pr ? true : options["no-pr"] ? false : undefined,
254
+ work: options.work,
255
+ auto: options.auto,
254
256
  silent: options.silent,
255
257
  verbose: options.verbose,
258
+ inputAllowed: options.inputAllowed,
256
259
  cwd: options.cwd,
257
260
  }),
258
261
  )
@@ -304,8 +307,11 @@ const commands: Record<string, Command> = {
304
307
  ready: options.ready,
305
308
  blocked: options.blocked,
306
309
  pr: options.pr ? true : options["no-pr"] ? false : undefined,
310
+ work: options.work,
311
+ auto: options.auto,
307
312
  silent: options.silent,
308
313
  verbose: options.verbose,
314
+ inputAllowed: options.inputAllowed,
309
315
  cwd: options.cwd,
310
316
  }),
311
317
  )
@@ -452,6 +458,8 @@ const commands: Record<string, Command> = {
452
458
  ready: options.ready,
453
459
  blocked: options.blocked,
454
460
  pr: options.pr ? true : options["no-pr"] ? false : undefined,
461
+ work: options.work,
462
+ auto: options.auto,
455
463
  silent: options.silent,
456
464
  verbose: options.verbose,
457
465
  inputAllowed: options.inputAllowed,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "2.33.0",
3
+ "version": "2.34.1",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -80,9 +80,6 @@
80
80
  "oxfmt": "^0.27.0",
81
81
  "typescript": "^7.0.2"
82
82
  },
83
- "peerDependencies": {
84
- "typescript": "^7.0.2"
85
- },
86
83
  "engines": {
87
84
  "bun": ">=1.0.0"
88
85
  }
@@ -477,6 +477,56 @@ describe("strict CLI parsing", () => {
477
477
  )
478
478
  })
479
479
 
480
+ test("accepts work launch options on new entities", () => {
481
+ expect(
482
+ parseCli(["task", "new", "example", "--work", "--auto"]),
483
+ ).toMatchObject({
484
+ commandName: "task",
485
+ args: ["new", "example"],
486
+ values: { work: true, auto: true },
487
+ })
488
+ expect(
489
+ parseCli([
490
+ "epic",
491
+ "new",
492
+ "delivery",
493
+ "--ticket-url",
494
+ "https://example.com/delivery",
495
+ "--repo",
496
+ "agency:main",
497
+ "--work",
498
+ ]),
499
+ ).toMatchObject({ commandName: "epic", args: ["new", "delivery"] })
500
+ expect(
501
+ parseCli([
502
+ "phase",
503
+ "new",
504
+ "delivery",
505
+ "implementation",
506
+ "--repo",
507
+ "agency",
508
+ "--branch",
509
+ "task/implementation",
510
+ "--base",
511
+ "main",
512
+ "--work",
513
+ ]),
514
+ ).toMatchObject({
515
+ commandName: "phase",
516
+ args: ["new", "delivery", "implementation"],
517
+ })
518
+ expect(() => parseCli(["task", "new", "example", "--auto"])).toThrow(
519
+ "Option '--auto' requires '--work'",
520
+ )
521
+ expect(() =>
522
+ parseCli(["task", "new", "example", "--work", "--json"]),
523
+ ).toThrow("cannot be combined")
524
+ expectUsageError(
525
+ ["task", "create", "example", "--repo", "agency", "--work"],
526
+ "agency task create",
527
+ )
528
+ })
529
+
480
530
  test("accepts repeatable graph filters and rejects output conflicts", () => {
481
531
  expect(
482
532
  parseCli([
package/src/cli-parser.ts CHANGED
@@ -70,6 +70,11 @@ const createOptions = {
70
70
  repo: { type: "string", multiple: true },
71
71
  } satisfies OptionConfig
72
72
 
73
+ const newWorkOptions = {
74
+ work: { type: "boolean" },
75
+ auto: { type: "boolean" },
76
+ } satisfies OptionConfig
77
+
73
78
  const taskCreateOptions = {
74
79
  ...createOptions,
75
80
  reference: { type: "string", multiple: true },
@@ -282,14 +287,25 @@ const commands = {
282
287
  },
283
288
  },
284
289
  epic: {
285
- usage: "agency epic <create|list|show|update|rename>",
290
+ usage: "agency epic <new|create|list|show|update|rename>",
286
291
  options: {
287
292
  ...createOptions,
293
+ ...newWorkOptions,
288
294
  ...viewOptions,
289
295
  ...mutationOptions,
290
296
  epic: { type: "string" },
291
297
  },
292
298
  subcommands: {
299
+ new: {
300
+ usage:
301
+ "agency epic new <id> --ticket-url <url> --repo <alias>:<ref> [--repo <alias>:<ref>...] [--work [--auto]]",
302
+ minArgs: 1,
303
+ maxArgs: 1,
304
+ options: ["ticket-url", "description", "repo", "work", "auto", "json"],
305
+ required: ["ticket-url", "repo"],
306
+ repeatable: ["repo"],
307
+ conflicts: [["work", "json"]],
308
+ },
293
309
  create: {
294
310
  usage:
295
311
  "agency epic create <id> --ticket-url <url> --repo <alias>:<ref> [--repo <alias>:<ref>...]",
@@ -341,6 +357,7 @@ const commands = {
341
357
  "agency task <new|create|list|show|status|update|rename|move|dependency>",
342
358
  options: {
343
359
  ...taskCreateOptions,
360
+ ...newWorkOptions,
344
361
  ...viewOptions,
345
362
  ...mutationOptions,
346
363
  "pr-url": { type: "string" },
@@ -348,7 +365,7 @@ const commands = {
348
365
  },
349
366
  subcommands: {
350
367
  new: {
351
- usage: "agency task new [id] [options]",
368
+ usage: "agency task new [id] [options] [--work [--auto]]",
352
369
  minArgs: 0,
353
370
  maxArgs: 1,
354
371
  options: [
@@ -360,9 +377,12 @@ const commands = {
360
377
  "branch",
361
378
  "base",
362
379
  "multi-phase",
380
+ "work",
381
+ "auto",
363
382
  "json",
364
383
  ],
365
384
  repeatable: ["reference"],
385
+ conflicts: [["work", "json"]],
366
386
  },
367
387
  create: {
368
388
  usage:
@@ -452,9 +472,11 @@ const commands = {
452
472
  },
453
473
  },
454
474
  phase: {
455
- usage: "agency phase <create|list|show|status|update|rename|dependency>",
475
+ usage:
476
+ "agency phase <new|create|list|show|status|update|rename|dependency>",
456
477
  options: {
457
478
  ...phaseCreateOptions,
479
+ ...newWorkOptions,
458
480
  ...viewOptions,
459
481
  ...mutationOptions,
460
482
  "pr-url": { type: "string" },
@@ -462,6 +484,29 @@ const commands = {
462
484
  phase: { type: "string" },
463
485
  },
464
486
  subcommands: {
487
+ new: {
488
+ usage:
489
+ "agency phase new <task-id> <phase-id> --repo <alias> --branch <name> --base <name> [options] [--work [--auto]]",
490
+ minArgs: 2,
491
+ maxArgs: 2,
492
+ options: [
493
+ "description",
494
+ "repo",
495
+ "reference",
496
+ "branch",
497
+ "base",
498
+ "depends-on",
499
+ "first-phase",
500
+ "work",
501
+ "auto",
502
+ "json",
503
+ "task",
504
+ "phase",
505
+ ],
506
+ required: ["repo", "branch", "base"],
507
+ repeatable: ["reference", "depends-on"],
508
+ conflicts: [["work", "json"]],
509
+ },
465
510
  create: {
466
511
  usage:
467
512
  "agency phase create <task-id> <phase-id> --repo <alias> --branch <name> --base <name> [options]",
@@ -1295,6 +1340,14 @@ export function parseCli(args: readonly string[]): ParsedCli {
1295
1340
  ) {
1296
1341
  validateTaskCreate(parsed.values, spec, subcommand === "create")
1297
1342
  }
1343
+ if (
1344
+ ["epic", "task", "phase"].includes(commandName) &&
1345
+ subcommand === "new" &&
1346
+ parsed.values.auto &&
1347
+ !parsed.values.work
1348
+ ) {
1349
+ throw usageError("Option '--auto' requires '--work'.", spec.usage)
1350
+ }
1298
1351
  if (["task", "phase"].includes(commandName) && subcommand === "dependency") {
1299
1352
  if (!["add", "remove"].includes(commandPositionals[0] ?? "")) {
1300
1353
  throw usageError(
@@ -1,6 +1,7 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
2
  import { mkdir } from "node:fs/promises"
3
3
  import { join } from "node:path"
4
+ import { Effect } from "effect"
4
5
  import {
5
6
  captureLogs,
6
7
  cleanupTempDir,
@@ -131,4 +132,36 @@ describe("epic command", () => {
131
132
  expect(logs[0]).toContain("example open waiting agency")
132
133
  expect(logs[0]).not.toMatch(/[\u{e000}-\u{f8ff}]/u)
133
134
  })
135
+
136
+ test("starts work on a newly created epic", async () => {
137
+ const launches: unknown[] = []
138
+
139
+ await runTestEffect(
140
+ epic(
141
+ {
142
+ subcommand: "new",
143
+ args: ["immediate"],
144
+ ticketUrl: "https://example.com/immediate",
145
+ repos: ["agency:main"],
146
+ work: true,
147
+ auto: true,
148
+ cwd: root,
149
+ silent: true,
150
+ },
151
+ (options) =>
152
+ Effect.sync(() => {
153
+ launches.push(options)
154
+ return undefined
155
+ }),
156
+ ),
157
+ )
158
+
159
+ expect(launches).toEqual([
160
+ expect.objectContaining({
161
+ epicId: "immediate",
162
+ auto: true,
163
+ cwd: root,
164
+ }),
165
+ ])
166
+ })
134
167
  })
@@ -6,6 +6,7 @@ import { formatTable } from "../utils/table"
6
6
  import { getWorkViews } from "../work-view"
7
7
  import { parseRepositoryReferences } from "../workbase/repository-reference"
8
8
  import { GraphMutationService } from "../services/GraphMutationService"
9
+ import { work as startWork, type StartWork } from "./work"
9
10
 
10
11
  interface EpicOptions extends BaseCommandOptions {
11
12
  readonly subcommand?: string
@@ -21,9 +22,11 @@ interface EpicOptions extends BaseCommandOptions {
21
22
  readonly ready?: boolean
22
23
  readonly blocked?: boolean
23
24
  readonly pr?: boolean
25
+ readonly work?: boolean
26
+ readonly auto?: boolean
24
27
  }
25
28
 
26
- export const epic = (options: EpicOptions) =>
29
+ export const epic = (options: EpicOptions, work: StartWork = startWork) =>
27
30
  Effect.gen(function* () {
28
31
  const epics = yield* EpicService
29
32
  const mutations = yield* GraphMutationService
@@ -31,6 +34,7 @@ export const epic = (options: EpicOptions) =>
31
34
  const cwd = options.cwd ?? process.cwd()
32
35
 
33
36
  switch (options.subcommand) {
37
+ case "new":
34
38
  case "create": {
35
39
  const id = options.args[0]
36
40
  if (!id || !options.ticketUrl || !options.repos?.length) {
@@ -53,6 +57,16 @@ export const epic = (options: EpicOptions) =>
53
57
  ? JSON.stringify(output, null, 2)
54
58
  : `Created epic '${record.id}'`,
55
59
  )
60
+ if (options.subcommand === "new" && options.work) {
61
+ yield* work({
62
+ epicId: record.id,
63
+ auto: options.auto,
64
+ cwd,
65
+ inputAllowed: options.inputAllowed,
66
+ silent: options.silent,
67
+ verbose: options.verbose,
68
+ })
69
+ }
56
70
  return
57
71
  }
58
72
 
@@ -157,7 +171,7 @@ export const epic = (options: EpicOptions) =>
157
171
  default:
158
172
  return yield* Effect.fail(
159
173
  new Error(
160
- "Subcommand is required. Available subcommands: create, list, show, update, rename",
174
+ "Subcommand is required. Available subcommands: new, create, list, show, update, rename",
161
175
  ),
162
176
  )
163
177
  }
@@ -167,6 +181,7 @@ export const help = `
167
181
  Usage: agency epic <subcommand>
168
182
 
169
183
  Subcommands:
184
+ new <id> Create an epic, optionally starting work
170
185
  create <id> Create an epic
171
186
  list List epics
172
187
  show <id> Show an epic
@@ -177,6 +192,8 @@ Create options:
177
192
  --ticket-url <url> External ticket URL
178
193
  --description <text> Short description of the epic
179
194
  --repo <alias>:<ref> Read-only repository reference; repeatable
195
+ --work Start work on the new epic after creating it
196
+ --auto Pass --auto to work; requires --work
180
197
 
181
198
  Update options:
182
199
  --ticket-url <url> Replace the external ticket URL
@@ -6,6 +6,7 @@ import { formatTable } from "../utils/table"
6
6
  import { getWorkViews } from "../work-view"
7
7
  import { parseRepositoryReferences } from "../workbase/repository-reference"
8
8
  import { GraphMutationService } from "../services/GraphMutationService"
9
+ import { work as startWork, type StartWork } from "./work"
9
10
 
10
11
  interface PhaseOptions extends BaseCommandOptions {
11
12
  readonly subcommand?: string
@@ -28,9 +29,11 @@ interface PhaseOptions extends BaseCommandOptions {
28
29
  readonly ready?: boolean
29
30
  readonly blocked?: boolean
30
31
  readonly pr?: boolean
32
+ readonly work?: boolean
33
+ readonly auto?: boolean
31
34
  }
32
35
 
33
- export const phase = (options: PhaseOptions) =>
36
+ export const phase = (options: PhaseOptions, work: StartWork = startWork) =>
34
37
  Effect.gen(function* () {
35
38
  const phases = yield* PhaseService
36
39
  const mutations = yield* GraphMutationService
@@ -39,6 +42,7 @@ export const phase = (options: PhaseOptions) =>
39
42
  const [taskId, phaseId] = options.args
40
43
 
41
44
  switch (options.subcommand) {
45
+ case "new":
42
46
  case "create": {
43
47
  if (
44
48
  !taskId ||
@@ -73,6 +77,17 @@ export const phase = (options: PhaseOptions) =>
73
77
  ? JSON.stringify(output, null, 2)
74
78
  : `Created phase '${record.id}' on task '${record.taskId}'`,
75
79
  )
80
+ if (options.subcommand === "new" && options.work) {
81
+ yield* work({
82
+ taskId: record.taskId,
83
+ phaseId: record.id,
84
+ auto: options.auto,
85
+ cwd,
86
+ inputAllowed: options.inputAllowed,
87
+ silent: options.silent,
88
+ verbose: options.verbose,
89
+ })
90
+ }
76
91
  return
77
92
  }
78
93
  case "list": {
@@ -245,7 +260,7 @@ export const phase = (options: PhaseOptions) =>
245
260
  default:
246
261
  return yield* Effect.fail(
247
262
  new Error(
248
- "Subcommand is required. Available: create, list, show, status, update, rename, dependency",
263
+ "Subcommand is required. Available: new, create, list, show, status, update, rename, dependency",
249
264
  ),
250
265
  )
251
266
  }
@@ -255,6 +270,7 @@ export const help = `
255
270
  Usage: agency phase <subcommand> <task-id> [phase-id]
256
271
 
257
272
  Subcommands:
273
+ new <task> <phase> Create a phase, optionally starting work
258
274
  create <task> <phase> Create a phase
259
275
  list <task> List task phases
260
276
  show <task> <phase> Show a phase
@@ -278,6 +294,8 @@ Create options:
278
294
  --base <name> Base branch
279
295
  --depends-on <id> Phase dependency; repeatable
280
296
  --first-phase <id> Existing execution phase ID when converting a task
297
+ --work Start work on the new phase after creating it
298
+ --auto Pass --auto to work; requires --work
281
299
 
282
300
  Update options:
283
301
  --description <text> / --clear-description
@@ -1,6 +1,7 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
2
  import { mkdir } from "node:fs/promises"
3
3
  import { join } from "node:path"
4
+ import { Effect } from "effect"
4
5
  import {
5
6
  captureLogs,
6
7
  cleanupTempDir,
@@ -199,6 +200,40 @@ describe("task and phase command JSON output", () => {
199
200
  })
200
201
  })
201
202
 
203
+ test("starts work on a newly created phase", async () => {
204
+ const launches: unknown[] = []
205
+
206
+ await runTestEffect(
207
+ phase(
208
+ {
209
+ subcommand: "new",
210
+ args: ["multi", "immediate"],
211
+ repo: "agency",
212
+ branch: "task/immediate",
213
+ base: "main",
214
+ work: true,
215
+ auto: true,
216
+ cwd: root,
217
+ silent: true,
218
+ },
219
+ (options) =>
220
+ Effect.sync(() => {
221
+ launches.push(options)
222
+ return undefined
223
+ }),
224
+ ),
225
+ )
226
+
227
+ expect(launches).toEqual([
228
+ expect.objectContaining({
229
+ taskId: "multi",
230
+ phaseId: "immediate",
231
+ auto: true,
232
+ cwd: root,
233
+ }),
234
+ ])
235
+ })
236
+
202
237
  test("sets task and phase status", async () => {
203
238
  const phaseLogs = await captureLogs(() =>
204
239
  runTestEffect(
@@ -167,4 +167,39 @@ describe("task creation input", () => {
167
167
  ),
168
168
  ).rejects.toThrow("task new requires interactive input")
169
169
  })
170
+
171
+ test("starts work on a newly created task", async () => {
172
+ const launches: unknown[] = []
173
+
174
+ await runTestEffect(
175
+ task(
176
+ {
177
+ subcommand: "new",
178
+ args: ["immediate"],
179
+ ticketUrl: "",
180
+ description: "",
181
+ multiPhase: false,
182
+ repo: "agency",
183
+ work: true,
184
+ auto: true,
185
+ cwd: root,
186
+ silent: true,
187
+ },
188
+ undefined,
189
+ (options) =>
190
+ Effect.sync(() => {
191
+ launches.push(options)
192
+ return undefined
193
+ }),
194
+ ),
195
+ )
196
+
197
+ expect(launches).toEqual([
198
+ expect.objectContaining({
199
+ taskId: "immediate",
200
+ auto: true,
201
+ cwd: root,
202
+ }),
203
+ ])
204
+ })
170
205
  })
@@ -10,6 +10,7 @@ import { choose } from "../utils/chooser"
10
10
  import { formatTable } from "../utils/table"
11
11
  import { getWorkViews } from "../work-view"
12
12
  import { GraphMutationService } from "../services/GraphMutationService"
13
+ import { work as startWork, type StartWork } from "./work"
13
14
 
14
15
  interface TaskOptions extends BaseCommandOptions {
15
16
  readonly subcommand?: string
@@ -35,6 +36,8 @@ interface TaskOptions extends BaseCommandOptions {
35
36
  readonly ready?: boolean
36
37
  readonly blocked?: boolean
37
38
  readonly pr?: boolean
39
+ readonly work?: boolean
40
+ readonly auto?: boolean
38
41
  }
39
42
 
40
43
  export interface TaskInteraction {
@@ -68,7 +71,11 @@ const defaultInteraction = (
68
71
  ),
69
72
  })
70
73
 
71
- export const task = (options: TaskOptions, interaction?: TaskInteraction) =>
74
+ export const task = (
75
+ options: TaskOptions,
76
+ interaction?: TaskInteraction,
77
+ work: StartWork = startWork,
78
+ ) =>
72
79
  Effect.gen(function* () {
73
80
  const tasks = yield* TaskService
74
81
  const epics = yield* EpicService
@@ -183,6 +190,16 @@ export const task = (options: TaskOptions, interaction?: TaskInteraction) =>
183
190
  ? JSON.stringify(output, null, 2)
184
191
  : `Created task '${record.id}'`,
185
192
  )
193
+ if (options.subcommand === "new" && options.work) {
194
+ yield* work({
195
+ taskId: record.id,
196
+ auto: options.auto,
197
+ cwd,
198
+ inputAllowed: options.inputAllowed,
199
+ silent: options.silent,
200
+ verbose: options.verbose,
201
+ })
202
+ }
186
203
  return
187
204
  }
188
205
  case "create": {
@@ -426,6 +443,8 @@ Create options:
426
443
  --branch <name> Working branch (default: task/<id>)
427
444
  --base <name> Base branch (default: main)
428
445
  --multi-phase Create a task container for phases
446
+ --work Start work on the new task after creating it
447
+ --auto Pass --auto to work; requires --work
429
448
 
430
449
  Update options:
431
450
  --ticket-url <url> / --clear-ticket
@@ -29,7 +29,7 @@ import {
29
29
  runnerEnvironment,
30
30
  } from "../workbase/runner-command"
31
31
 
32
- interface WorkOptions extends BaseCommandOptions {
32
+ export interface WorkOptions extends BaseCommandOptions {
33
33
  readonly directory?: string
34
34
  readonly taskId?: string
35
35
  readonly phaseId?: string
@@ -42,6 +42,8 @@ interface WorkOptions extends BaseCommandOptions {
42
42
  readonly force?: boolean
43
43
  }
44
44
 
45
+ export type StartWork = (options: WorkOptions) => ReturnType<typeof work>
46
+
45
47
  type LaunchAgent = (
46
48
  cli: string,
47
49
  args: readonly string[],