@markjaquith/agency 2.50.0 → 2.52.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 +34 -8
- package/cli.ts +33 -1
- package/package.json +1 -1
- package/schemas/agency-graph-v1.schema.json +1 -0
- package/src/cli-parser.test.ts +19 -0
- package/src/cli-parser.ts +23 -0
- package/src/cli.test.ts +4 -4
- package/src/commands/init.test.ts +2 -0
- package/src/commands/vcs.test.ts +75 -0
- package/src/commands/vcs.ts +72 -0
- package/src/commands/worktree.ts +1 -1
- package/src/graph-schema.test.ts +1 -1
- package/src/graph-schema.ts +1 -0
- package/src/services/ArchiveService.ts +23 -0
- package/src/services/ContextService.ts +62 -13
- package/src/services/DoctorService.ts +13 -6
- package/src/services/GraphService.ts +1 -0
- package/src/services/PhaseService.ts +191 -70
- package/src/services/PullRequestService.ts +14 -30
- package/src/services/RepositoryService.test.ts +31 -1
- package/src/services/RepositoryService.ts +63 -31
- package/src/services/ReviewService.ts +23 -0
- package/src/services/SyncService.test.ts +67 -0
- package/src/services/SyncService.ts +51 -84
- package/src/services/TaskPhaseService.test.ts +80 -0
- package/src/services/VcsMigrationService.test.ts +245 -0
- package/src/services/VcsMigrationService.ts +812 -0
- package/src/services/VersionControlService.test.ts +100 -0
- package/src/services/VersionControlService.ts +479 -0
- package/src/services/WorkbaseService.ts +5 -1
- package/src/services/WorktreeService.test.ts +72 -1
- package/src/services/WorktreeService.ts +808 -308
- package/src/test-utils.ts +10 -0
- package/src/workbase/AGENTS.md +2 -2
- package/src/workbase/schemas.ts +1 -0
- package/src/workbase/version-control.ts +5 -0
package/README.md
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Agency manages durable agentic work across repositories. Epics, tasks, and
|
|
4
4
|
phases live as Markdown documents in a filesystem-backed workbase. Repository
|
|
5
|
-
aliases and
|
|
6
|
-
or write.
|
|
5
|
+
aliases and managed workspaces provide each execution unit with the code it may
|
|
6
|
+
read or write.
|
|
7
7
|
|
|
8
8
|
## Requirements
|
|
9
9
|
|
|
10
10
|
- [Bun](https://bun.sh) 1.0 or newer
|
|
11
11
|
- Git
|
|
12
|
+
- [Jujutsu](https://jj-vcs.github.io/jj/) is preferred when available
|
|
12
13
|
- [GitHub CLI](https://cli.github.com/) for `agency pr create`
|
|
13
14
|
- OpenCode, Claude Code, or a configured runner for `agency work`
|
|
14
15
|
|
|
@@ -122,14 +123,15 @@ The plugin grants whole-workbase access dynamically, while the portable
|
|
|
122
123
|
reference advertises that context to agents. Bash and Agency operations must
|
|
123
124
|
still follow the write authority reported by `agency context`.
|
|
124
125
|
|
|
125
|
-
Repository aliases and canonical fetch remotes are
|
|
126
|
-
`agency.json`; local
|
|
126
|
+
Repository aliases, the version-control backend, and canonical fetch remotes are
|
|
127
|
+
declared in tracked `agency.json`; local clones and symlinks remain ignored under
|
|
127
128
|
`repos/{alias}`. A declaration contains no local path, symlink target, checkout,
|
|
128
129
|
or credential:
|
|
129
130
|
|
|
130
131
|
```json
|
|
131
132
|
{
|
|
132
133
|
"version": 2,
|
|
134
|
+
"vcs": "jj",
|
|
133
135
|
"repositories": {
|
|
134
136
|
"frontend": {
|
|
135
137
|
"remote": "git@example.com:team/frontend.git"
|
|
@@ -138,6 +140,29 @@ or credential:
|
|
|
138
140
|
}
|
|
139
141
|
```
|
|
140
142
|
|
|
143
|
+
New workbases select `"jj"` when the `jj` executable is available and otherwise
|
|
144
|
+
select `"git"`. Existing version 2 workbases without `vcs` remain Git workbases.
|
|
145
|
+
The selected backend is a workbase-level invariant: jj workbases initialize
|
|
146
|
+
managed clones with `jj git init` and create jj workspaces, while Git workbases
|
|
147
|
+
continue to use Git worktrees.
|
|
148
|
+
|
|
149
|
+
Inspect the current backend and migration readiness with:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
agency vcs status
|
|
153
|
+
agency vcs migrate jj # dry-run
|
|
154
|
+
agency vcs migrate jj --apply
|
|
155
|
+
agency vcs migrate git --apply
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Migration is workbase-wide and transactional. Agency locks every execution
|
|
159
|
+
unit, requires clean registered workspaces, blocks active claims and working or
|
|
160
|
+
delegated units, converts repository metadata, recreates checkout paths with the
|
|
161
|
+
target backend, and updates `agency.json` last. Migration from jj to Git also
|
|
162
|
+
blocks jj-only heads that are not preserved by a bookmark or workspace. Failed
|
|
163
|
+
migrations restore the source repositories and workspaces when rollback is
|
|
164
|
+
possible and report explicit manual recovery paths otherwise.
|
|
165
|
+
|
|
141
166
|
Existing version 2 workbases without `repositories` remain valid. Run
|
|
142
167
|
`agency repo setup` to preview deterministic adoption of legacy local aliases;
|
|
143
168
|
`agency repo setup --apply` writes declarations only when a portable origin is
|
|
@@ -146,7 +171,7 @@ creation command.
|
|
|
146
171
|
|
|
147
172
|
### Custom Worktree Command
|
|
148
173
|
|
|
149
|
-
|
|
174
|
+
Git workbases create worktrees with Git. Set `worktreeCreateCommand` to an
|
|
150
175
|
argv template when another tool should create writable worktrees:
|
|
151
176
|
|
|
152
177
|
```json
|
|
@@ -205,9 +230,10 @@ Custom commands own writable branch creation. Agency checks for conflicting
|
|
|
205
230
|
worktrees first, invokes the command only when the branch is not checked out,
|
|
206
231
|
and verifies that `{worktree}` exists afterward.
|
|
207
232
|
|
|
208
|
-
The configured command applies only to the writable checkout.
|
|
209
|
-
read-only repositories remain detached Git worktrees at their
|
|
210
|
-
they do not acquire writable branches.
|
|
233
|
+
The configured command applies only to the writable checkout of a Git workbase.
|
|
234
|
+
Supplemental read-only repositories remain detached Git worktrees at their
|
|
235
|
+
declared refs so they do not acquire writable branches. Jj workbases always use
|
|
236
|
+
jj workspaces and ignore this Git-specific customization.
|
|
211
237
|
|
|
212
238
|
### Agent Runners
|
|
213
239
|
|
package/cli.ts
CHANGED
|
@@ -44,7 +44,14 @@ import { ReadinessService } from "./src/services/ReadinessService"
|
|
|
44
44
|
import { GraphMutationService } from "./src/services/GraphMutationService"
|
|
45
45
|
import { DoctorService } from "./src/services/DoctorService"
|
|
46
46
|
import { ReviewService } from "./src/services/ReviewService"
|
|
47
|
+
import {
|
|
48
|
+
GitVersionControlService,
|
|
49
|
+
JjVersionControlService,
|
|
50
|
+
VersionControlService,
|
|
51
|
+
} from "./src/services/VersionControlService"
|
|
47
52
|
import { review, help as reviewHelp } from "./src/commands/review"
|
|
53
|
+
import { vcs, help as vcsHelp } from "./src/commands/vcs"
|
|
54
|
+
import { VcsMigrationService } from "./src/services/VcsMigrationService"
|
|
48
55
|
import {
|
|
49
56
|
claimCommand,
|
|
50
57
|
claimHelp,
|
|
@@ -62,6 +69,10 @@ import {
|
|
|
62
69
|
const CliLayer = Layer.mergeAll(
|
|
63
70
|
FileSystemService.Default,
|
|
64
71
|
WorkbaseService.Default,
|
|
72
|
+
GitVersionControlService.Default,
|
|
73
|
+
JjVersionControlService.Default,
|
|
74
|
+
VersionControlService.Default,
|
|
75
|
+
VcsMigrationService.Default,
|
|
65
76
|
RepositoryService.Default,
|
|
66
77
|
EpicService.Default,
|
|
67
78
|
TaskService.Default,
|
|
@@ -547,6 +558,26 @@ const commands: Record<string, Command> = {
|
|
|
547
558
|
)
|
|
548
559
|
},
|
|
549
560
|
},
|
|
561
|
+
vcs: {
|
|
562
|
+
run: async (args: string[], options: Record<string, any>) => {
|
|
563
|
+
if (options.help) {
|
|
564
|
+
console.log(vcsHelp)
|
|
565
|
+
return
|
|
566
|
+
}
|
|
567
|
+
await runCommand(
|
|
568
|
+
vcs({
|
|
569
|
+
subcommand: args[0],
|
|
570
|
+
target: args[1],
|
|
571
|
+
apply: options.apply,
|
|
572
|
+
dryRun: options["dry-run"],
|
|
573
|
+
json: options.json,
|
|
574
|
+
silent: options.silent,
|
|
575
|
+
verbose: options.verbose,
|
|
576
|
+
cwd: options.cwd,
|
|
577
|
+
}),
|
|
578
|
+
)
|
|
579
|
+
},
|
|
580
|
+
},
|
|
550
581
|
next: {
|
|
551
582
|
run: async (_args: string[], options: Record<string, any>) => {
|
|
552
583
|
if (options.help) return console.log(nextHelp)
|
|
@@ -702,7 +733,8 @@ Commands:
|
|
|
702
733
|
archive <type> Archive a work item
|
|
703
734
|
task <subcommand> Manage tasks
|
|
704
735
|
work [directory|task] Work on an epic, task, or phase
|
|
705
|
-
worktree <subcommand> Inspect and maintain managed
|
|
736
|
+
worktree <subcommand> Inspect and maintain managed workspaces
|
|
737
|
+
vcs <subcommand> Inspect or migrate the version-control backend
|
|
706
738
|
next List or select ready execution units
|
|
707
739
|
pr create Create a pull request for an execution unit
|
|
708
740
|
review refresh Explicitly refresh a pinned review task
|
package/package.json
CHANGED
package/src/cli-parser.test.ts
CHANGED
|
@@ -571,6 +571,25 @@ describe("strict CLI parsing", () => {
|
|
|
571
571
|
)
|
|
572
572
|
})
|
|
573
573
|
|
|
574
|
+
test("parses VCS status and migration commands", () => {
|
|
575
|
+
expect(parseCli(["vcs", "status", "--json"])).toMatchObject({
|
|
576
|
+
commandName: "vcs",
|
|
577
|
+
args: ["status"],
|
|
578
|
+
values: { json: true },
|
|
579
|
+
})
|
|
580
|
+
expect(
|
|
581
|
+
parseCli(["vcs", "migrate", "jj", "--apply", "--json"]),
|
|
582
|
+
).toMatchObject({
|
|
583
|
+
commandName: "vcs",
|
|
584
|
+
args: ["migrate", "jj"],
|
|
585
|
+
values: { apply: true, json: true },
|
|
586
|
+
})
|
|
587
|
+
expectUsageError(["vcs", "migrate"], "agency vcs migrate")
|
|
588
|
+
expect(() =>
|
|
589
|
+
parseCli(["vcs", "migrate", "jj", "--dry-run", "--apply"]),
|
|
590
|
+
).toThrow("cannot be combined")
|
|
591
|
+
})
|
|
592
|
+
|
|
574
593
|
test("accepts runner selection and command inspection for work", () => {
|
|
575
594
|
expect(
|
|
576
595
|
parseCli([
|
package/src/cli-parser.ts
CHANGED
|
@@ -832,6 +832,29 @@ const commands = {
|
|
|
832
832
|
},
|
|
833
833
|
},
|
|
834
834
|
},
|
|
835
|
+
vcs: {
|
|
836
|
+
usage: "agency vcs <status|migrate>",
|
|
837
|
+
options: {
|
|
838
|
+
...outputOptions,
|
|
839
|
+
apply: { type: "boolean" },
|
|
840
|
+
"dry-run": { type: "boolean" },
|
|
841
|
+
},
|
|
842
|
+
subcommands: {
|
|
843
|
+
status: {
|
|
844
|
+
usage: "agency vcs status [--json]",
|
|
845
|
+
minArgs: 0,
|
|
846
|
+
maxArgs: 0,
|
|
847
|
+
options: ["json"],
|
|
848
|
+
},
|
|
849
|
+
migrate: {
|
|
850
|
+
usage: "agency vcs migrate <git|jj> [--dry-run | --apply] [--json]",
|
|
851
|
+
minArgs: 1,
|
|
852
|
+
maxArgs: 1,
|
|
853
|
+
options: ["apply", "dry-run", "json"],
|
|
854
|
+
conflicts: [["apply", "dry-run"]],
|
|
855
|
+
},
|
|
856
|
+
},
|
|
857
|
+
},
|
|
835
858
|
work: {
|
|
836
859
|
usage:
|
|
837
860
|
"agency work [<directory-or-task-id> | --epic <epic-id>] [--runner <name>] [--auto] | agency work prepare [target] [--dry-run] [--json]",
|
package/src/cli.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterAll, afterEach, describe, expect, test } from "bun:test"
|
|
2
|
-
import { access, mkdir, realpath, symlink } from "node:fs/promises"
|
|
2
|
+
import { access, mkdir, realpath, stat, symlink } from "node:fs/promises"
|
|
3
3
|
import { join, sep } from "node:path"
|
|
4
4
|
import errorFixture from "../fixtures/protocol/error.json"
|
|
5
5
|
import successFixture from "../fixtures/protocol/success.json"
|
|
@@ -1762,9 +1762,9 @@ status: open
|
|
|
1762
1762
|
alias: "agency",
|
|
1763
1763
|
status: "applied",
|
|
1764
1764
|
})
|
|
1765
|
-
expect(
|
|
1766
|
-
|
|
1767
|
-
)
|
|
1765
|
+
expect(
|
|
1766
|
+
(await stat(join(restored, "repos/agency/.jj"))).isDirectory(),
|
|
1767
|
+
).toBe(true)
|
|
1768
1768
|
|
|
1769
1769
|
const prepared = parseJson(
|
|
1770
1770
|
await runCli(["work", "prepare", "portable", "--json"], restored),
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
runTestEffect,
|
|
9
9
|
} from "../test-utils"
|
|
10
10
|
import { init } from "./init"
|
|
11
|
+
import { preferredVersionControl } from "../workbase/version-control"
|
|
11
12
|
|
|
12
13
|
describe("init command", () => {
|
|
13
14
|
let parent: string
|
|
@@ -26,6 +27,7 @@ describe("init command", () => {
|
|
|
26
27
|
|
|
27
28
|
expect(await Bun.file(join(root, "agency.json")).json()).toEqual({
|
|
28
29
|
version: 2,
|
|
30
|
+
vcs: preferredVersionControl(),
|
|
29
31
|
})
|
|
30
32
|
for (const directory of ["repos", "epics", "tasks"]) {
|
|
31
33
|
expect((await stat(join(root, directory))).isDirectory()).toBe(true)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
|
+
import {
|
|
3
|
+
captureLogs,
|
|
4
|
+
cleanupTempDir,
|
|
5
|
+
createTempDir,
|
|
6
|
+
runTestEffect,
|
|
7
|
+
} from "../test-utils"
|
|
8
|
+
import { vcs } from "./vcs"
|
|
9
|
+
|
|
10
|
+
describe("vcs command", () => {
|
|
11
|
+
let root: string
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
root = await createTempDir()
|
|
15
|
+
await Bun.write(
|
|
16
|
+
`${root}/agency.json`,
|
|
17
|
+
JSON.stringify({ version: 2, vcs: "git" }),
|
|
18
|
+
)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
afterEach(async () => cleanupTempDir(root))
|
|
22
|
+
|
|
23
|
+
test("reports structured backend status", async () => {
|
|
24
|
+
const logs = await captureLogs(() =>
|
|
25
|
+
runTestEffect(vcs({ subcommand: "status", cwd: root, json: true })),
|
|
26
|
+
)
|
|
27
|
+
expect(JSON.parse(logs[0]!)).toMatchObject({
|
|
28
|
+
root,
|
|
29
|
+
configured: "git",
|
|
30
|
+
source: "git",
|
|
31
|
+
target: "git",
|
|
32
|
+
workspaceCount: 0,
|
|
33
|
+
blockers: [],
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test("persists the inferred Git backend for a legacy workbase", async () => {
|
|
38
|
+
await Bun.write(`${root}/agency.json`, JSON.stringify({ version: 2 }))
|
|
39
|
+
await captureLogs(() =>
|
|
40
|
+
runTestEffect(
|
|
41
|
+
vcs({
|
|
42
|
+
subcommand: "migrate",
|
|
43
|
+
target: "git",
|
|
44
|
+
cwd: root,
|
|
45
|
+
apply: true,
|
|
46
|
+
json: true,
|
|
47
|
+
}),
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
expect(await Bun.file(`${root}/agency.json`).json()).toEqual({
|
|
51
|
+
version: 2,
|
|
52
|
+
vcs: "git",
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test("treats migration to the configured backend as a no-op", async () => {
|
|
57
|
+
const logs = await captureLogs(() =>
|
|
58
|
+
runTestEffect(
|
|
59
|
+
vcs({
|
|
60
|
+
subcommand: "migrate",
|
|
61
|
+
target: "git",
|
|
62
|
+
cwd: root,
|
|
63
|
+
apply: true,
|
|
64
|
+
json: true,
|
|
65
|
+
}),
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
expect(JSON.parse(logs[0]!)).toMatchObject({
|
|
69
|
+
source: "git",
|
|
70
|
+
target: "git",
|
|
71
|
+
mode: "apply",
|
|
72
|
+
actions: [],
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
})
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Effect } from "effect"
|
|
2
|
+
import { VcsMigrationService } from "../services/VcsMigrationService"
|
|
3
|
+
import type { BaseCommandOptions } from "../utils/command"
|
|
4
|
+
import { createLoggers } from "../utils/effect"
|
|
5
|
+
|
|
6
|
+
interface VcsOptions extends BaseCommandOptions {
|
|
7
|
+
readonly subcommand?: string
|
|
8
|
+
readonly target?: string
|
|
9
|
+
readonly apply?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const vcs = (options: VcsOptions = {}) =>
|
|
13
|
+
Effect.gen(function* () {
|
|
14
|
+
const migrations = yield* VcsMigrationService
|
|
15
|
+
const { log } = createLoggers(options)
|
|
16
|
+
const root = options.cwd ?? process.cwd()
|
|
17
|
+
if (options.subcommand === "status") {
|
|
18
|
+
const status = yield* migrations.status(root)
|
|
19
|
+
if (options.json) return log(JSON.stringify(status, null, 2))
|
|
20
|
+
log(
|
|
21
|
+
`Version control: ${status.source}${status.configured ? "" : " (inferred for legacy workbase)"}`,
|
|
22
|
+
)
|
|
23
|
+
log(
|
|
24
|
+
`Tools: git=${status.available.git ? "available" : "missing"} jj=${status.available.jj ? "available" : "missing"}`,
|
|
25
|
+
)
|
|
26
|
+
log(
|
|
27
|
+
`Repositories: ${status.repositories.length}; managed workspaces: ${status.workspaceCount}; blockers: ${status.blockers.length}`,
|
|
28
|
+
)
|
|
29
|
+
for (const blocker of status.blockers)
|
|
30
|
+
log(`blocker ${blocker.kind} ${blocker.target}: ${blocker.message}`)
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
if (options.subcommand === "migrate") {
|
|
34
|
+
if (options.target !== "git" && options.target !== "jj") {
|
|
35
|
+
return yield* Effect.fail(
|
|
36
|
+
new Error("VCS migration target must be 'git' or 'jj'"),
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
const result = yield* migrations.migrate(options.target, root, {
|
|
40
|
+
apply: options.apply,
|
|
41
|
+
})
|
|
42
|
+
if (options.json) return log(JSON.stringify(result, null, 2))
|
|
43
|
+
log(
|
|
44
|
+
result.mode === "apply"
|
|
45
|
+
? `Migrated workbase to ${options.target}`
|
|
46
|
+
: `Migration plan: ${result.source} -> ${options.target}`,
|
|
47
|
+
)
|
|
48
|
+
for (const action of result.actions) log(`- ${action}`)
|
|
49
|
+
if (result.mode === "dry-run" && result.actions.length > 0)
|
|
50
|
+
log("Run again with --apply to perform the migration.")
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
return yield* Effect.fail(
|
|
54
|
+
new Error(`Unknown vcs subcommand '${options.subcommand ?? ""}'`),
|
|
55
|
+
)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
export const help = `
|
|
59
|
+
Usage: agency vcs <status|migrate>
|
|
60
|
+
|
|
61
|
+
Inspect or migrate the workbase-wide version-control backend.
|
|
62
|
+
|
|
63
|
+
Commands:
|
|
64
|
+
status Show the configured backend, tools, repositories, and blockers
|
|
65
|
+
migrate <git|jj> [--dry-run | --apply]
|
|
66
|
+
Preview or apply a clean, transactional backend migration
|
|
67
|
+
|
|
68
|
+
Options:
|
|
69
|
+
--apply Apply the migration; omission performs a dry run
|
|
70
|
+
--dry-run Explicitly preview the migration without applying it
|
|
71
|
+
--json Print structured output
|
|
72
|
+
`
|
package/src/commands/worktree.ts
CHANGED
|
@@ -120,7 +120,7 @@ export const worktree = (options: WorktreeOptions = {}) =>
|
|
|
120
120
|
export const help = `
|
|
121
121
|
Usage: agency worktree <list|inspect|prepare|remove|rebuild|repair>
|
|
122
122
|
|
|
123
|
-
Inspect and maintain Agency-managed writable and reference
|
|
123
|
+
Inspect and maintain Agency-managed writable and reference workspaces.
|
|
124
124
|
|
|
125
125
|
Commands:
|
|
126
126
|
list List every managed checkout
|
package/src/graph-schema.test.ts
CHANGED
package/src/graph-schema.ts
CHANGED
|
@@ -11,6 +11,11 @@ import {
|
|
|
11
11
|
WorktreeService,
|
|
12
12
|
type WorktreeRemovalSnapshot,
|
|
13
13
|
} from "./WorktreeService"
|
|
14
|
+
import {
|
|
15
|
+
GitVersionControlService,
|
|
16
|
+
JjVersionControlService,
|
|
17
|
+
VersionControlService,
|
|
18
|
+
} from "./VersionControlService"
|
|
14
19
|
import {
|
|
15
20
|
formatMarkdownDocument,
|
|
16
21
|
parseFrontmatter,
|
|
@@ -268,6 +273,9 @@ const applyMutation = (moves: readonly Move[], writes: readonly Write[]) =>
|
|
|
268
273
|
const WorktreeLayer = Layer.mergeAll(
|
|
269
274
|
FileSystemService.Default,
|
|
270
275
|
WorkbaseService.Default,
|
|
276
|
+
GitVersionControlService.Default,
|
|
277
|
+
JjVersionControlService.Default,
|
|
278
|
+
VersionControlService.Default,
|
|
271
279
|
TaskService.Default,
|
|
272
280
|
PhaseService.Default,
|
|
273
281
|
WorktreeService.Default,
|
|
@@ -298,6 +306,21 @@ const restoreWorktreeSnapshots = async (
|
|
|
298
306
|
continue
|
|
299
307
|
} catch {}
|
|
300
308
|
await mkdir(dirname(snapshot.path), { recursive: true })
|
|
309
|
+
if (snapshot.vcs === "jj") {
|
|
310
|
+
await runGit([
|
|
311
|
+
"jj",
|
|
312
|
+
"-R",
|
|
313
|
+
snapshot.repositoryPath,
|
|
314
|
+
"workspace",
|
|
315
|
+
"add",
|
|
316
|
+
"--name",
|
|
317
|
+
snapshot.workspaceName!,
|
|
318
|
+
"-r",
|
|
319
|
+
snapshot.head,
|
|
320
|
+
snapshot.path,
|
|
321
|
+
])
|
|
322
|
+
continue
|
|
323
|
+
}
|
|
301
324
|
await runGit(
|
|
302
325
|
snapshot.branch
|
|
303
326
|
? [
|
|
@@ -8,6 +8,7 @@ import { RepositoryService } from "./RepositoryService"
|
|
|
8
8
|
import { aggregateProgress, readinessState } from "../readiness"
|
|
9
9
|
import { parseFrontmatter } from "../workbase/frontmatter"
|
|
10
10
|
import { documentRevision } from "../workbase/document-revision"
|
|
11
|
+
import { VersionControlService } from "./VersionControlService"
|
|
11
12
|
import {
|
|
12
13
|
EpicFrontmatter,
|
|
13
14
|
PhaseFrontmatter,
|
|
@@ -127,6 +128,7 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
127
128
|
const fs = yield* FileSystemService
|
|
128
129
|
const workbase = yield* WorkbaseService
|
|
129
130
|
const repositoryService = yield* RepositoryService
|
|
131
|
+
const versionControl = yield* VersionControlService
|
|
130
132
|
const cwd = resolve(options.cwd ?? process.cwd())
|
|
131
133
|
const suppliedTarget = options.target ?? "."
|
|
132
134
|
const candidate = resolve(cwd, suppliedTarget)
|
|
@@ -134,6 +136,7 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
134
136
|
const { root, config } = yield* workbase.loadConfig(
|
|
135
137
|
candidateExists ? candidate : cwd,
|
|
136
138
|
)
|
|
139
|
+
const backend = yield* versionControl.forWorkbase(root)
|
|
137
140
|
|
|
138
141
|
if (relative(root, candidate) === "") {
|
|
139
142
|
const compact = !options.full
|
|
@@ -214,6 +217,7 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
214
217
|
root,
|
|
215
218
|
configPath: join(root, "agency.json"),
|
|
216
219
|
version: config.version,
|
|
220
|
+
vcs: config.vcs ?? "git",
|
|
217
221
|
},
|
|
218
222
|
target: { kind: "workbase", path: root },
|
|
219
223
|
hint: compact
|
|
@@ -801,9 +805,45 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
801
805
|
const inspectCheckout = (
|
|
802
806
|
repositoryPath: string,
|
|
803
807
|
checkoutPath: string,
|
|
808
|
+
expectedBranch: string | null = null,
|
|
804
809
|
) =>
|
|
805
810
|
Effect.gen(function* (): Generator<any, CheckoutInspection, any> {
|
|
806
811
|
const materialized = yield* fs.isDirectory(checkoutPath)
|
|
812
|
+
if (backend.kind === "jj") {
|
|
813
|
+
const listed = yield* Effect.either(
|
|
814
|
+
backend.listWorkspaces(repositoryPath),
|
|
815
|
+
)
|
|
816
|
+
const workspaces = Either.isRight(listed) ? listed.right : []
|
|
817
|
+
if (Either.isLeft(listed)) {
|
|
818
|
+
inspectionWarnings.push(
|
|
819
|
+
`Unable to inspect jj workspace registrations for ${repositoryPath}`,
|
|
820
|
+
)
|
|
821
|
+
}
|
|
822
|
+
const canonicalCheckoutPath = materialized
|
|
823
|
+
? yield* fs.realPath(checkoutPath)
|
|
824
|
+
: resolve(checkoutPath)
|
|
825
|
+
const registered = workspaces.some(
|
|
826
|
+
(workspace) => workspace.path === canonicalCheckoutPath,
|
|
827
|
+
)
|
|
828
|
+
if (!materialized) {
|
|
829
|
+
return {
|
|
830
|
+
materialized: false,
|
|
831
|
+
registered,
|
|
832
|
+
checkoutCommit: null,
|
|
833
|
+
checkoutBranch: null,
|
|
834
|
+
detached: null,
|
|
835
|
+
dirty: null,
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return {
|
|
839
|
+
materialized: true,
|
|
840
|
+
registered,
|
|
841
|
+
checkoutCommit: yield* backend.workspaceHead(checkoutPath),
|
|
842
|
+
checkoutBranch: expectedBranch,
|
|
843
|
+
detached: expectedBranch === null,
|
|
844
|
+
dirty: yield* backend.workspaceDirty(checkoutPath),
|
|
845
|
+
}
|
|
846
|
+
}
|
|
807
847
|
const listed = yield* runGit(fs, repositoryPath, [
|
|
808
848
|
"worktree",
|
|
809
849
|
"list",
|
|
@@ -870,14 +910,22 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
870
910
|
const repositoryPath =
|
|
871
911
|
repository?.path ?? join(root, "repos", executionData.repo)
|
|
872
912
|
const checkoutPath = join(codePath, executionData.repo)
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
const baseCommit = yield*
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
913
|
+
let branchCommit = yield* backend.resolveRevision(
|
|
914
|
+
repositoryPath,
|
|
915
|
+
executionData.branch,
|
|
916
|
+
)
|
|
917
|
+
const baseCommit = yield* backend.resolveRevision(
|
|
918
|
+
repositoryPath,
|
|
919
|
+
executionData.base,
|
|
920
|
+
)
|
|
921
|
+
const checkout = yield* inspectCheckout(
|
|
922
|
+
repositoryPath,
|
|
923
|
+
checkoutPath,
|
|
924
|
+
executionData.branch,
|
|
925
|
+
)
|
|
926
|
+
if (backend.kind === "jj" && branchCommit === null) {
|
|
927
|
+
branchCommit = checkout.checkoutCommit
|
|
928
|
+
}
|
|
881
929
|
if (branchCommit === null) {
|
|
882
930
|
inspectionWarnings.push(
|
|
883
931
|
`Unable to resolve branch '${executionData.branch}' in ${repositoryPath}`,
|
|
@@ -896,7 +944,7 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
896
944
|
base: executionData.base,
|
|
897
945
|
branchCommit,
|
|
898
946
|
baseCommit,
|
|
899
|
-
...
|
|
947
|
+
...checkout,
|
|
900
948
|
}
|
|
901
949
|
})
|
|
902
950
|
: null
|
|
@@ -907,10 +955,10 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
907
955
|
const repositoryPath =
|
|
908
956
|
repository?.path ?? join(root, "repos", reference.repo)
|
|
909
957
|
const checkoutPath = join(codePath, reference.repo)
|
|
910
|
-
const resolvedCommit = yield*
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
958
|
+
const resolvedCommit = yield* backend.resolveRevision(
|
|
959
|
+
repositoryPath,
|
|
960
|
+
reference.ref,
|
|
961
|
+
)
|
|
914
962
|
if (resolvedCommit === null) {
|
|
915
963
|
inspectionWarnings.push(
|
|
916
964
|
`Unable to resolve reference '${reference.ref}' in ${repositoryPath}`,
|
|
@@ -968,6 +1016,7 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
968
1016
|
root,
|
|
969
1017
|
configPath: join(root, "agency.json"),
|
|
970
1018
|
version: config.version,
|
|
1019
|
+
vcs: config.vcs ?? "git",
|
|
971
1020
|
},
|
|
972
1021
|
target,
|
|
973
1022
|
documents: {
|
|
@@ -126,6 +126,14 @@ export class DoctorService extends Effect.Service<DoctorService>()(
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
const gitAvailable = yield* tool("tool.git", "git", "error", "Git")
|
|
129
|
+
const jjAvailable = yield* tool(
|
|
130
|
+
"tool.jj",
|
|
131
|
+
"jj",
|
|
132
|
+
config.vcs === "jj" ? "error" : "optional",
|
|
133
|
+
"Jujutsu",
|
|
134
|
+
)
|
|
135
|
+
const versionControlAvailable =
|
|
136
|
+
gitAvailable && (config.vcs !== "jj" || jjAvailable)
|
|
129
137
|
yield* tool(
|
|
130
138
|
"capability.runner.opencode",
|
|
131
139
|
"opencode",
|
|
@@ -321,18 +329,17 @@ export class DoctorService extends Effect.Service<DoctorService>()(
|
|
|
321
329
|
}
|
|
322
330
|
}
|
|
323
331
|
|
|
324
|
-
const repositoryList =
|
|
332
|
+
const repositoryList = versionControlAvailable
|
|
325
333
|
? yield* repositories.list(root)
|
|
326
334
|
: []
|
|
327
|
-
if (!
|
|
335
|
+
if (!versionControlAvailable) {
|
|
328
336
|
add({
|
|
329
337
|
id: "repository.inspection",
|
|
330
338
|
category: "repository",
|
|
331
339
|
level: "warning",
|
|
332
340
|
status: "fail",
|
|
333
|
-
message:
|
|
334
|
-
|
|
335
|
-
remediation: "Install 'git' and rerun 'agency doctor'.",
|
|
341
|
+
message: `Repository, ref, remote, and workspace checks were skipped because the ${config.vcs ?? "git"} backend is unavailable`,
|
|
342
|
+
remediation: `Install '${config.vcs === "jj" ? "jj" : "git"}' and rerun 'agency doctor'.`,
|
|
336
343
|
})
|
|
337
344
|
}
|
|
338
345
|
for (const repository of repositoryList) {
|
|
@@ -439,7 +446,7 @@ export class DoctorService extends Effect.Service<DoctorService>()(
|
|
|
439
446
|
}
|
|
440
447
|
}
|
|
441
448
|
|
|
442
|
-
if (validation.valid &&
|
|
449
|
+
if (validation.valid && versionControlAvailable) {
|
|
443
450
|
const inspected = yield* Effect.either(worktrees.list(root))
|
|
444
451
|
if (Either.isLeft(inspected)) {
|
|
445
452
|
add({
|