@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 +4 -3
- package/cli-main.ts +0 -1
- package/fixtures/protocol/orchestration-recipes.json +1 -1
- package/package.json +1 -1
- package/src/cli-parser.test.ts +6 -8
- package/src/cli-parser.ts +3 -5
- package/src/commands/claim.ts +1 -1
- package/src/commands/sync.test.ts +100 -0
- package/src/commands/sync.ts +44 -7
- package/src/services/IntegrationService.test.ts +1 -1
- package/src/services/PhaseService.ts +1 -1
- package/src/services/TaskService.ts +1 -1
- package/src/workbase/AGENTS.md +1 -1
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
|
|
492
|
+
default mode applies safe reconciliation transitions; `--dry-run` is explicitly
|
|
493
|
+
observational.
|
|
493
494
|
|
|
494
|
-
`agency sync
|
|
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
|
|
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
|
@@ -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", "--
|
|
33
|
+
["sync", "--json"],
|
|
34
34
|
["pr", "create", "checkout", "ui", "--json"],
|
|
35
35
|
[
|
|
36
36
|
"finish",
|
package/package.json
CHANGED
package/src/cli-parser.test.ts
CHANGED
|
@@ -371,18 +371,16 @@ describe("strict CLI parsing", () => {
|
|
|
371
371
|
})
|
|
372
372
|
})
|
|
373
373
|
|
|
374
|
-
test("parses
|
|
375
|
-
expect(parseCli(["sync"
|
|
374
|
+
test("parses explicit dry-run reconciliation", () => {
|
|
375
|
+
expect(parseCli(["sync"])).toMatchObject({
|
|
376
376
|
commandName: "sync",
|
|
377
|
-
values: {
|
|
377
|
+
values: {},
|
|
378
378
|
})
|
|
379
|
-
expect(parseCli(["sync", "--
|
|
379
|
+
expect(parseCli(["sync", "--dry-run", "--json"])).toMatchObject({
|
|
380
380
|
commandName: "sync",
|
|
381
|
-
values: {
|
|
381
|
+
values: { "dry-run": true, json: true },
|
|
382
382
|
})
|
|
383
|
-
expect(() => parseCli(["sync", "--
|
|
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
|
|
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
|
|
687
|
+
usage: "agency sync [--dry-run] [--json]",
|
|
689
688
|
minArgs: 0,
|
|
690
689
|
maxArgs: 0,
|
|
691
|
-
options: ["dry-run", "
|
|
692
|
-
conflicts: [["dry-run", "apply"]],
|
|
690
|
+
options: ["dry-run", "json"],
|
|
693
691
|
},
|
|
694
692
|
},
|
|
695
693
|
archive: {
|
package/src/commands/claim.ts
CHANGED
|
@@ -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
|
|
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
|
+
})
|
package/src/commands/sync.ts
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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({
|
package/src/workbase/AGENTS.md
CHANGED
|
@@ -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
|
|
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.
|