@markjaquith/agency 2.53.0 → 2.54.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
@@ -489,9 +489,10 @@ materializations, then compares every execution declaration with local branch
489
489
  and worktree registration, checkout dirtiness, resolved reference commits, claim
490
490
  expiry, and pull request state, merge state, and mergeability. It reports
491
491
  structured `changes`, `warnings`, `unresolved`, and per-execution evidence. The
492
- default and `--dry-run` modes are observational.
492
+ default mode applies safe reconciliation transitions; `--dry-run` is explicitly
493
+ observational.
493
494
 
494
- `agency sync --apply` performs only these safe transitions:
495
+ `agency sync` performs only these safe transitions:
495
496
 
496
497
  - materialize declared but missing repositories from their canonical remotes;
497
498
  - adopt legacy materializations only when they have an unambiguous portable origin;
@@ -722,7 +723,7 @@ Single-phase tasks and phases store status in YAML. New execution units start
722
723
  `open`, and `agency work` marks the selected execution unit `working` immediately
723
724
  before launch. Running `agency work` again can relaunch unclaimed `working` work.
724
725
  By default, `done` requires an authoritative merged pull request and is applied
725
- by `agency sync --apply`. Work whose intended outcome genuinely requires no pull
726
+ by `agency sync`. Work whose intended outcome genuinely requires no pull
726
727
  request may instead use an explicit `--no-pull-request --summary <text>` status
727
728
  transition. Agency records the summary, completion time, and optional evidence
728
729
  URL durably; reopening removes that evidence. This exceptional path refuses work
@@ -804,6 +805,7 @@ for restoration. Archived IDs are reserved until restored.
804
805
  agency work [<directory> | --epic <epic-id>] [--runner <name>] [--auto] [--print-command]
805
806
  agency work prepare [target] [--dry-run] [--json]
806
807
  agency worktree <list|inspect|prepare|remove|rebuild|repair>
808
+ agency pr create <task-id> [phase-id] [--draft] [--force] [--json]
807
809
  agency pr [args...]
808
810
  ```
809
811
 
@@ -850,9 +852,12 @@ Agency resolves the ref to a commit and creates a detached worktree. Existing
850
852
  reference worktrees are reused only while their commit still matches the declared
851
853
  ref; use a commit SHA as `ref` when reproducibility matters.
852
854
 
853
- `agency pr` forwards every argument to `gh pr`. From an execution task or phase
854
- directory, including descendants, it runs in that execution unit's authoritative
855
- writable checkout. Otherwise it runs in the caller's current directory.
855
+ Task-aware `agency pr create <task-id> [phase-id]` uses Agency's delivery flow,
856
+ including readiness checks and durable PR recording. Other `agency pr`
857
+ invocations forward every argument to `gh pr`. From an execution task or phase
858
+ directory, including descendants, passthrough runs in that execution unit's
859
+ authoritative writable checkout. Otherwise it runs in the caller's current
860
+ directory.
856
861
 
857
862
  ### Status and Validation
858
863
 
package/cli-main.ts CHANGED
@@ -5,7 +5,7 @@ import { join, resolve } from "node:path"
5
5
  import { parseCli } from "./src/cli-parser"
6
6
  import { init, help as initHelp } from "./src/commands/init"
7
7
  import { task, help as taskHelp } from "./src/commands/task"
8
- import { pr, help as prHelp } from "./src/commands/pr"
8
+ import { pr, prCreate, help as prHelp } from "./src/commands/pr"
9
9
  import { work, workPrepare, help as workHelp } from "./src/commands/work"
10
10
  import { worktree, help as worktreeHelp } from "./src/commands/worktree"
11
11
  import { status, help as statusHelp } from "./src/commands/status"
@@ -134,7 +134,7 @@ const resolveInvocationCwd = (
134
134
  ) =>
135
135
  runEffect(
136
136
  Effect.gen(function* () {
137
- if (commandName === "pr") {
137
+ if (commandName === "pr" && options.passthrough) {
138
138
  return resolve(options.cwd ?? process.cwd())
139
139
  }
140
140
  if (
@@ -287,7 +287,22 @@ const commands: Record<string, Command> = {
287
287
  console.log(prHelp)
288
288
  return
289
289
  }
290
- process.exitCode = await runEffect(pr(args, options.cwd))
290
+ if (options.passthrough) {
291
+ process.exitCode = await runEffect(pr(args, options.cwd))
292
+ return
293
+ }
294
+ await runCommand(
295
+ prCreate({
296
+ taskId: args[1]!,
297
+ phaseId: args[2],
298
+ draft: options.draft,
299
+ force: options.force,
300
+ json: options.json,
301
+ silent: options.silent,
302
+ verbose: options.verbose,
303
+ cwd: options.cwd,
304
+ }),
305
+ )
291
306
  },
292
307
  },
293
308
  phase: {
@@ -694,7 +709,6 @@ const commands: Record<string, Command> = {
694
709
  }
695
710
  await runCommand(
696
711
  sync({
697
- apply: options.apply,
698
712
  dryRun: options["dry-run"],
699
713
  json: options.json,
700
714
  silent: options.silent,
@@ -727,7 +741,7 @@ Commands:
727
741
  worktree <subcommand> Inspect and maintain managed workspaces
728
742
  vcs <subcommand> Inspect or migrate the version-control backend
729
743
  next List or select ready execution units
730
- pr [args...] Run gh pr with Agency repository focus
744
+ pr create / pr [...] Create an Agency PR or run gh pr with repository focus
731
745
  review refresh Explicitly refresh a pinned review task
732
746
  repo <subcommand> Manage workbase repositories
733
747
  status Show status for the current workbase
@@ -762,7 +776,7 @@ const machineMode = process.argv
762
776
 
763
777
  try {
764
778
  const args = process.argv.slice(2)
765
- const { commandName, args: commandArgs, values } = parseCli(args)
779
+ const { commandName, args: commandArgs, passthrough, values } = parseCli(args)
766
780
 
767
781
  // Handle global flags
768
782
  if (values.version) {
@@ -786,16 +800,29 @@ try {
786
800
  !values.json &&
787
801
  !values["no-input"] &&
788
802
  Boolean(process.stdin.isTTY && process.stdout.isTTY)
789
- const cwd = await resolveInvocationCwd(commandName, values)
803
+ const cwd = await resolveInvocationCwd(commandName, {
804
+ ...values,
805
+ passthrough,
806
+ })
790
807
  if (values.json || (values.jsonl && values.help)) {
791
808
  const result = await collectCommandResult(() =>
792
- command.run(commandArgs, { ...values, cwd, inputAllowed }),
809
+ command.run(commandArgs, { ...values, cwd, inputAllowed, passthrough }),
793
810
  )
794
811
  writeEnvelope(successEnvelope(result))
795
812
  } else if (values.jsonl) {
796
- await command.run(commandArgs, { ...values, cwd, inputAllowed: false })
813
+ await command.run(commandArgs, {
814
+ ...values,
815
+ cwd,
816
+ inputAllowed: false,
817
+ passthrough,
818
+ })
797
819
  } else {
798
- await command.run(commandArgs, { ...values, cwd, inputAllowed })
820
+ await command.run(commandArgs, {
821
+ ...values,
822
+ cwd,
823
+ inputAllowed,
824
+ passthrough,
825
+ })
799
826
  }
800
827
  } catch (error) {
801
828
  if (machineMode) {
@@ -30,7 +30,7 @@
30
30
  ["work", "prepare", "--task", "checkout", "--phase", "ui", "--json"],
31
31
  ["work", "tasks/checkout/phases/ui", "--runner", "opencode"],
32
32
  ["sync", "--dry-run", "--json"],
33
- ["sync", "--apply", "--json"],
33
+ ["sync", "--json"],
34
34
  ["pr", "create", "checkout", "ui", "--json"],
35
35
  [
36
36
  "finish",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "2.53.0",
3
+ "version": "2.54.1",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -367,22 +367,44 @@ describe("strict CLI parsing", () => {
367
367
  expect(parseCli(["--cwd", "/workbase", "pr", ...args])).toEqual({
368
368
  commandName: "pr",
369
369
  args,
370
+ passthrough: true,
370
371
  values: { cwd: "/workbase" },
371
372
  })
372
373
  })
373
374
 
374
- test("parses reconciliation modes and rejects conflicting modes", () => {
375
- expect(parseCli(["sync", "--dry-run", "--json"])).toMatchObject({
375
+ test("parses task-aware pr create without routing it to passthrough", () => {
376
+ expect(
377
+ parseCli([
378
+ "pr",
379
+ "create",
380
+ "ship",
381
+ "release",
382
+ "--draft",
383
+ "--force",
384
+ "--json",
385
+ ]),
386
+ ).toEqual({
387
+ commandName: "pr",
388
+ args: ["create", "ship", "release"],
389
+ values: { draft: true, force: true, json: true },
390
+ })
391
+ const selected = parseCli(["pr", "create", "--task", "ship"])
392
+ expect(selected).toMatchObject({
393
+ args: ["create", "ship"],
394
+ })
395
+ expect(selected.passthrough).toBeUndefined()
396
+ })
397
+
398
+ test("parses explicit dry-run reconciliation", () => {
399
+ expect(parseCli(["sync"])).toMatchObject({
376
400
  commandName: "sync",
377
- values: { "dry-run": true, json: true },
401
+ values: {},
378
402
  })
379
- expect(parseCli(["sync", "--apply"])).toMatchObject({
403
+ expect(parseCli(["sync", "--dry-run", "--json"])).toMatchObject({
380
404
  commandName: "sync",
381
- values: { apply: true },
405
+ values: { "dry-run": true, json: true },
382
406
  })
383
- expect(() => parseCli(["sync", "--dry-run", "--apply"])).toThrow(
384
- "cannot be combined",
385
- )
407
+ expect(() => parseCli(["sync", "--apply"])).toThrow("Unknown option")
386
408
  })
387
409
 
388
410
  test("accepts archive dry-run", () => {
package/src/cli-parser.ts CHANGED
@@ -678,18 +678,16 @@ const commands = {
678
678
  },
679
679
  },
680
680
  sync: {
681
- usage: "agency sync [--dry-run | --apply] [--json]",
681
+ usage: "agency sync [--dry-run] [--json]",
682
682
  options: {
683
683
  ...outputOptions,
684
684
  "dry-run": { type: "boolean" },
685
- apply: { type: "boolean" },
686
685
  },
687
686
  command: {
688
- usage: "agency sync [--dry-run | --apply] [--json]",
687
+ usage: "agency sync [--dry-run] [--json]",
689
688
  minArgs: 0,
690
689
  maxArgs: 0,
691
- options: ["dry-run", "apply", "json"],
692
- conflicts: [["dry-run", "apply"]],
690
+ options: ["dry-run", "json"],
693
691
  },
694
692
  },
695
693
  archive: {
@@ -897,12 +895,22 @@ const commands = {
897
895
  },
898
896
  },
899
897
  pr: {
900
- usage: "agency pr [args...]",
901
- options: commonOptions,
902
- command: {
903
- usage: "agency pr [args...]",
904
- minArgs: 0,
905
- maxArgs: Number.POSITIVE_INFINITY,
898
+ usage: "agency pr create <task-id> [phase-id] | agency pr [args...]",
899
+ options: {
900
+ ...outputOptions,
901
+ draft: { type: "boolean" },
902
+ force: { type: "boolean" },
903
+ task: { type: "string" },
904
+ phase: { type: "string" },
905
+ },
906
+ subcommands: {
907
+ create: {
908
+ usage:
909
+ "agency pr create <task-id> [phase-id] [--draft] [--force] [--json]",
910
+ minArgs: 1,
911
+ maxArgs: 2,
912
+ options: ["draft", "force", "json", "task", "phase"],
913
+ },
906
914
  },
907
915
  },
908
916
  next: {
@@ -1021,6 +1029,7 @@ const preCommandValueOptions = new Set(["--workbase", "--cwd"])
1021
1029
  export interface ParsedCli {
1022
1030
  readonly commandName?: keyof typeof commands
1023
1031
  readonly args: string[]
1032
+ readonly passthrough?: boolean
1024
1033
  readonly values: Record<
1025
1034
  string,
1026
1035
  boolean | string | (boolean | string)[] | undefined
@@ -1093,6 +1102,7 @@ const targetSlots = (
1093
1102
  if (commandName === "worktree" && subcommand !== "list") {
1094
1103
  return ["task", "phase"]
1095
1104
  }
1105
+ if (commandName === "pr" && subcommand === "create") return ["task", "phase"]
1096
1106
  return []
1097
1107
  }
1098
1108
 
@@ -1313,7 +1323,15 @@ export function parseCli(args: readonly string[]): ParsedCli {
1313
1323
  "agency <command> [options]",
1314
1324
  )
1315
1325
  }
1316
- if (commandName === "pr") {
1326
+ const prArgs = args.slice(commandIndex + 1)
1327
+ const taskAwarePrCreate =
1328
+ commandName === "pr" &&
1329
+ prArgs[0] === "create" &&
1330
+ ((prArgs[1] !== undefined && !prArgs[1].startsWith("-")) ||
1331
+ prArgs.some(
1332
+ (argument) => argument === "--task" || argument.startsWith("--task="),
1333
+ ))
1334
+ if (commandName === "pr" && !taskAwarePrCreate) {
1317
1335
  const parsed = parse(
1318
1336
  args.slice(0, commandIndex),
1319
1337
  rootOptions,
@@ -1338,7 +1356,8 @@ export function parseCli(args: readonly string[]): ParsedCli {
1338
1356
  }
1339
1357
  return {
1340
1358
  commandName,
1341
- args: [...args.slice(commandIndex + 1)],
1359
+ args: prArgs,
1360
+ passthrough: true,
1342
1361
  values: parsed.values,
1343
1362
  }
1344
1363
  }
package/src/cli.test.ts CHANGED
@@ -598,6 +598,20 @@ exit 23
598
598
  blockers: [{ kind: "status", reason: "Task status is dropped" }],
599
599
  },
600
600
  ])
601
+
602
+ const blockedPr = await runCli(["pr", "create", "finished", "--json"], root)
603
+ expect(blockedPr.exitCode).toBe(1)
604
+ expect(blockedPr.stderr).toBe("")
605
+ expect(JSON.parse(blockedPr.stdout)).toMatchObject({
606
+ ok: false,
607
+ error: {
608
+ code: "EXECUTION_BLOCKED",
609
+ fields: {
610
+ status: "dropped",
611
+ blockers: [{ kind: "status", reason: "Task status is dropped" }],
612
+ },
613
+ },
614
+ })
601
615
  })
602
616
 
603
617
  test("reports and synchronizes managed integration files", async () => {
@@ -117,6 +117,6 @@ export const finishHelp = `
117
117
  Usage: agency finish <task-id> [phase-id] --session-id <id> --revision <sha256> --outcome <done|dropped> [--no-pull-request --summary <text> [--evidence-url <url>]]
118
118
 
119
119
  Finish a claim owned by the session. A done claim outcome leaves unmerged work
120
- working; agency sync --apply marks the execution unit done after merge. Use
120
+ working; agency sync marks the execution unit done after merge. Use
121
121
  --no-pull-request with a summary for an explicit non-PR completion.
122
122
  `
@@ -1,8 +1,15 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
+ import { Effect } from "effect"
2
3
  import { chmod, mkdir, realpath } from "node:fs/promises"
3
4
  import { dirname, join } from "node:path"
4
- import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
5
- import { pr } from "./pr"
5
+ import { PullRequestService } from "../services/PullRequestService"
6
+ import {
7
+ captureLogs,
8
+ cleanupTempDir,
9
+ createTempDir,
10
+ runTestEffect,
11
+ } from "../test-utils"
12
+ import { pr, prCreate } from "./pr"
6
13
 
7
14
  const write = async (root: string, path: string, content: string) => {
8
15
  const fullPath = join(root, path)
@@ -95,6 +102,39 @@ status: open
95
102
  return root
96
103
  }
97
104
 
105
+ test("creates and records an Agency pull request", async () => {
106
+ const url = "https://github.com/markjaquith/agency/pull/123"
107
+ let received: unknown[] = []
108
+ const logs = await captureLogs(() =>
109
+ Effect.runPromise(
110
+ prCreate({
111
+ taskId: "example",
112
+ phaseId: "implementation",
113
+ draft: true,
114
+ force: true,
115
+ cwd: "/workbase",
116
+ json: true,
117
+ }).pipe(
118
+ Effect.provideService(PullRequestService, {
119
+ create: (...args: unknown[]) => {
120
+ received = args
121
+ return Effect.succeed(url)
122
+ },
123
+ } as never),
124
+ ) as Effect.Effect<void, unknown, never>,
125
+ ),
126
+ )
127
+
128
+ expect(JSON.parse(logs[0]!)).toEqual({ url })
129
+ expect(received).toEqual([
130
+ "example",
131
+ "implementation",
132
+ true,
133
+ "/workbase",
134
+ expect.objectContaining({ force: true, draft: true, json: true }),
135
+ ])
136
+ })
137
+
98
138
  test("focuses task and descendant invocations on the writable checkout", async () => {
99
139
  const root = await createExecutionWorkbase()
100
140
  const task = join(root, "tasks/single")
@@ -2,6 +2,30 @@ import { Effect } from "effect"
2
2
  import { resolve } from "node:path"
3
3
  import { ContextService } from "../services/ContextService"
4
4
  import { FileSystemService } from "../services/FileSystemService"
5
+ import { PullRequestService } from "../services/PullRequestService"
6
+ import type { BaseCommandOptions } from "../utils/command"
7
+ import { createLoggers } from "../utils/effect"
8
+
9
+ interface PrCreateOptions extends BaseCommandOptions {
10
+ readonly taskId: string
11
+ readonly phaseId?: string
12
+ readonly draft?: boolean
13
+ readonly force?: boolean
14
+ }
15
+
16
+ export const prCreate = (options: PrCreateOptions) =>
17
+ Effect.gen(function* () {
18
+ const pullRequests = yield* PullRequestService
19
+ const { log } = createLoggers(options)
20
+ const url = yield* pullRequests.create(
21
+ options.taskId,
22
+ options.phaseId,
23
+ options.draft,
24
+ options.cwd ?? process.cwd(),
25
+ options,
26
+ )
27
+ log(options.json ? JSON.stringify({ url }, null, 2) : url)
28
+ })
5
29
 
6
30
  export const pr = (args: readonly string[], cwd: string = process.cwd()) =>
7
31
  Effect.gen(function* () {
@@ -24,8 +48,10 @@ export const pr = (args: readonly string[], cwd: string = process.cwd()) =>
24
48
  })
25
49
 
26
50
  export const help = `
27
- Usage: agency pr [args...]
51
+ Usage: agency pr create <task-id> [phase-id] [--draft] [--force] [--json]
52
+ agency pr [args...]
28
53
 
29
- Run gh pr unchanged, focusing the writable repository checkout when invoked
30
- from an Agency execution task or phase.
54
+ Create records a pull request for an Agency execution unit. Other invocations
55
+ run gh pr unchanged, focusing the writable repository checkout when invoked from
56
+ an Agency execution task or phase.
31
57
  `
@@ -0,0 +1,100 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
+ import { Effect } from "effect"
3
+ import { mkdir } from "node:fs/promises"
4
+ import { join } from "node:path"
5
+ import { TaskService } from "../services/TaskService"
6
+ import {
7
+ captureLogs,
8
+ cleanupTempDir,
9
+ createTempDir,
10
+ runTestEffect,
11
+ } from "../test-utils"
12
+ import { sync } from "./sync"
13
+
14
+ const git = async (args: string[], cwd: string) => {
15
+ const process = Bun.spawn(["git", ...args], {
16
+ cwd,
17
+ stdout: "pipe",
18
+ stderr: "pipe",
19
+ })
20
+ await process.exited
21
+ if (process.exitCode !== 0) {
22
+ throw new Error(await new Response(process.stderr).text())
23
+ }
24
+ }
25
+
26
+ describe("sync command", () => {
27
+ let root: string
28
+
29
+ beforeEach(async () => {
30
+ root = await createTempDir()
31
+ const source = join(root, "source")
32
+ await mkdir(source)
33
+ await git(["init", "--initial-branch=main"], source)
34
+ await git(["config", "user.email", "test@example.com"], source)
35
+ await git(["config", "user.name", "Test"], source)
36
+ await Bun.write(join(source, "README.md"), "example\n")
37
+ await git(["add", "README.md"], source)
38
+ await git(["-c", "commit.gpgsign=false", "commit", "-m", "initial"], source)
39
+ await mkdir(join(root, "repos"))
40
+ await git(["clone", "--bare", source, join(root, "repos/agency")], root)
41
+ await Bun.write(join(root, "agency.json"), '{"version":2}\n')
42
+ await runTestEffect(
43
+ TaskService.pipe(
44
+ Effect.flatMap((service) =>
45
+ service.create(
46
+ {
47
+ id: "example",
48
+ ticketUrl: null,
49
+ repo: "agency",
50
+ branch: "task/example",
51
+ base: "main",
52
+ },
53
+ root,
54
+ ),
55
+ ),
56
+ ),
57
+ )
58
+ })
59
+
60
+ afterEach(async () => cleanupTempDir(root))
61
+
62
+ test("applies safe reconciliation by default", async () => {
63
+ const logs = await captureLogs(() => runTestEffect(sync({ cwd: root })))
64
+
65
+ expect(
66
+ await Bun.file(join(root, "tasks/example/code/agency/README.md")).text(),
67
+ ).toBe("example\n")
68
+ expect(logs).toContainEqual(
69
+ expect.stringContaining("Applied materialize-workspace 'task:example'"),
70
+ )
71
+ expect(() => JSON.parse(logs.join("\n"))).toThrow()
72
+ })
73
+
74
+ test("keeps explicit dry-run observational", async () => {
75
+ await runTestEffect(sync({ cwd: root, dryRun: true, silent: true }))
76
+
77
+ expect(
78
+ await Bun.file(
79
+ join(root, "tasks/example/code/agency/README.md"),
80
+ ).exists(),
81
+ ).toBe(false)
82
+ })
83
+
84
+ test("preserves structured output behind --json", async () => {
85
+ const logs = await captureLogs(() =>
86
+ runTestEffect(sync({ cwd: root, dryRun: true, json: true })),
87
+ )
88
+
89
+ expect(logs).toHaveLength(1)
90
+ expect(JSON.parse(logs[0]!)).toMatchObject({
91
+ mode: "dry-run",
92
+ changes: [
93
+ expect.objectContaining({
94
+ kind: "materialize-workspace",
95
+ status: "planned",
96
+ }),
97
+ ],
98
+ })
99
+ })
100
+ })
@@ -4,7 +4,6 @@ import type { BaseCommandOptions } from "../utils/command"
4
4
  import { createLoggers } from "../utils/effect"
5
5
 
6
6
  interface SyncCommandOptions extends BaseCommandOptions {
7
- readonly apply?: boolean
8
7
  readonly dryRun?: boolean
9
8
  }
10
9
 
@@ -14,24 +13,62 @@ export const sync = (options: SyncCommandOptions = {}) =>
14
13
  const { log } = createLoggers(options)
15
14
  const result = yield* service.reconcile({
16
15
  cwd: options.cwd,
17
- apply: options.apply === true,
16
+ apply: options.dryRun !== true,
18
17
  })
19
- log(JSON.stringify(result, null, 2))
18
+ if (options.json) return log(JSON.stringify(result, null, 2))
19
+
20
+ for (const action of result.repositories.actions) {
21
+ log(
22
+ `${action.status === "applied" ? "Applied" : "Planned"} repository ${action.kind} '${action.alias}' from ${action.remote}`,
23
+ )
24
+ }
25
+ for (const change of result.changes) {
26
+ log(
27
+ `${change.status === "applied" ? "Applied" : "Planned"} ${change.kind} '${change.target}': ${change.message}`,
28
+ )
29
+ }
30
+ for (const warning of result.warnings) {
31
+ log(
32
+ `Warning '${warning.target}': ${warning.message}${warning.action ? `. ${warning.action}` : ""}`,
33
+ )
34
+ }
35
+ for (const issue of result.repositories.unresolved) {
36
+ log(
37
+ `Unresolved repository '${issue.alias}': ${issue.message}. ${issue.action}`,
38
+ )
39
+ }
40
+ for (const issue of result.unresolved) {
41
+ log(
42
+ `Unresolved '${issue.target}': ${issue.message}${issue.action ? `. ${issue.action}` : ""}`,
43
+ )
44
+ }
45
+ if (
46
+ result.repositories.actions.length === 0 &&
47
+ result.changes.length === 0 &&
48
+ result.warnings.length === 0 &&
49
+ result.repositories.unresolved.length === 0 &&
50
+ result.unresolved.length === 0
51
+ ) {
52
+ log(
53
+ result.mode === "apply"
54
+ ? "Workbase sync is current"
55
+ : "Workbase sync plan is current",
56
+ )
57
+ }
20
58
  })
21
59
 
22
60
  export const help = `
23
- Usage: agency sync [--dry-run | --apply] [--json]
61
+ Usage: agency sync [--dry-run] [--json]
24
62
 
25
63
  Compare portable repository declarations and execution state with local Git
26
64
  repositories, worktrees, branches, references, claims, and pull requests.
27
- Dry-run is the default.
65
+ Safe reconciliation transitions are applied by default.
28
66
 
29
67
  Options:
30
68
  --dry-run Report planned safe transitions without changing state
31
- --apply Apply safe reconciliation transitions
32
69
  --json Output one versioned machine result
33
70
 
34
- Apply may materialize declared repositories and unambiguous missing checkouts,
71
+ Sync may materialize declared repositories and unambiguous missing checkouts,
35
72
  adopt legacy repositories with portable origins, release expired claims,
36
73
  record a uniquely matched PR, and mark merged work done. Dirty, stale, or
37
74
  conflicting checkouts are always left unresolved.
@@ -282,7 +282,7 @@ describe("IntegrationService", () => {
282
282
  expect(body).toMatch(/completing\s+a refinement loop/)
283
283
  expect(body).toContain("pausing or handing off")
284
284
  expect(body).toContain("`agency finish`")
285
- expect(body).toContain("`agency sync --apply`")
285
+ expect(body).toContain("`agency sync`")
286
286
  expect(body).toContain("`--no-pull-request --summary <text>`")
287
287
  expect(body).toContain("`TASK.md` or `PHASE.md`")
288
288
  expect(body).toContain("PR state, current head, diff summary")
@@ -654,7 +654,7 @@ export class PhaseService extends Effect.Service<PhaseService>()(
654
654
  if (validStatus === "done") {
655
655
  return yield* new PhaseError({
656
656
  message:
657
- "Work becomes done after its authoritative pull request is merged; run 'agency sync --apply', or explicitly complete a non-PR outcome with '--no-pull-request --summary <text>'",
657
+ "Work becomes done after its authoritative pull request is merged; run 'agency sync', or explicitly complete a non-PR outcome with '--no-pull-request --summary <text>'",
658
658
  })
659
659
  }
660
660
  return yield* new PhaseError({
@@ -381,7 +381,7 @@ export class TaskService extends Effect.Service<TaskService>()("TaskService", {
381
381
  if (validStatus === "done") {
382
382
  return yield* new TaskError({
383
383
  message:
384
- "Work becomes done after its authoritative pull request is merged; run 'agency sync --apply', or explicitly complete a non-PR outcome with '--no-pull-request --summary <text>'",
384
+ "Work becomes done after its authoritative pull request is merged; run 'agency sync', or explicitly complete a non-PR outcome with '--no-pull-request --summary <text>'",
385
385
  })
386
386
  }
387
387
  return yield* new TaskError({
@@ -59,8 +59,8 @@ to override readiness.
59
59
  - Do not run `agency work` from an active agent session unless the user
60
60
  explicitly asks to launch another agent.
61
61
  - Run `agency validate` before worktree or pull-request operations.
62
- - Create a pull request only with explicit user intent. Run `agency pr create`
63
- from its execution unit, then use `agency sync --apply` to reconcile PR state.
62
+ - Create a pull request only with explicit user intent. Run
63
+ `agency pr create <task> [phase]` so the URL is recorded durably.
64
64
 
65
65
  ## Execution
66
66
 
@@ -91,7 +91,7 @@ a refinement loop, or pausing or handing off completed implementation work):
91
91
  - Finish an active claim with the current revision via `agency finish`; a
92
92
  successful claim outcome leaves unmerged work `working`. For unclaimed work,
93
93
  keep the execution unit `working` through review and merge.
94
- - After merge, run `agency sync --apply` to reconcile the execution unit to
94
+ - After merge, run `agency sync` to reconcile the execution unit to
95
95
  `done`.
96
96
  - For an approved non-PR outcome, finish an active claim or update unclaimed
97
97
  status with `--no-pull-request --summary <text>` and optional supporting URL.