@markjaquith/agency 2.53.0 → 2.54.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
@@ -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
package/cli-main.ts CHANGED
@@ -694,7 +694,6 @@ const commands: Record<string, Command> = {
694
694
  }
695
695
  await runCommand(
696
696
  sync({
697
- apply: options.apply,
698
697
  dryRun: options["dry-run"],
699
698
  json: options.json,
700
699
  silent: options.silent,
@@ -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.0",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -371,18 +371,16 @@ describe("strict CLI parsing", () => {
371
371
  })
372
372
  })
373
373
 
374
- test("parses reconciliation modes and rejects conflicting modes", () => {
375
- expect(parseCli(["sync", "--dry-run", "--json"])).toMatchObject({
374
+ test("parses explicit dry-run reconciliation", () => {
375
+ expect(parseCli(["sync"])).toMatchObject({
376
376
  commandName: "sync",
377
- values: { "dry-run": true, json: true },
377
+ values: {},
378
378
  })
379
- expect(parseCli(["sync", "--apply"])).toMatchObject({
379
+ expect(parseCli(["sync", "--dry-run", "--json"])).toMatchObject({
380
380
  commandName: "sync",
381
- values: { apply: true },
381
+ values: { "dry-run": true, json: true },
382
382
  })
383
- expect(() => parseCli(["sync", "--dry-run", "--apply"])).toThrow(
384
- "cannot be combined",
385
- )
383
+ expect(() => parseCli(["sync", "--apply"])).toThrow("Unknown option")
386
384
  })
387
385
 
388
386
  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: {
@@ -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
  `
@@ -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({
@@ -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.