@markjaquith/agency 2.54.2 → 2.54.3

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
@@ -857,7 +857,8 @@ including readiness checks and durable PR recording. Other `agency pr`
857
857
  invocations forward every argument to `gh pr`. From an execution task or phase
858
858
  directory, including descendants, passthrough runs in that execution unit's
859
859
  authoritative writable checkout. Otherwise it runs in the caller's current
860
- directory.
860
+ directory. In jj workbases, Agency also supplies the repository and the work
861
+ item's declared branch to subcommands that would otherwise infer Git context.
861
862
 
862
863
  ### Status and Validation
863
864
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "2.54.2",
3
+ "version": "2.54.3",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -50,9 +50,19 @@ exit "\${GH_EXIT:-0}"
50
50
  const captured = async () =>
51
51
  (await Bun.file(capturePath).text()).trim().split("\n")
52
52
 
53
- const createExecutionWorkbase = async () => {
53
+ const createExecutionWorkbase = async (vcs: "git" | "jj" = "git") => {
54
54
  const root = join(tempRoot, "workbase")
55
- await write(root, "agency.json", '{"version":2}\n')
55
+ await write(
56
+ root,
57
+ "agency.json",
58
+ `${JSON.stringify({
59
+ version: 2,
60
+ vcs,
61
+ repositories: {
62
+ agency: { remote: "https://github.com/example/agency.git" },
63
+ },
64
+ })}\n`,
65
+ )
56
66
  await mkdir(join(root, "repos/agency"), { recursive: true })
57
67
  await mkdir(join(root, "tasks/single/code/agency"), { recursive: true })
58
68
  await write(
@@ -163,6 +173,51 @@ status: open
163
173
  ])
164
174
  })
165
175
 
176
+ test("injects the execution branch and repository for jj targets", async () => {
177
+ const root = await createExecutionWorkbase("jj")
178
+ const task = join(root, "tasks/single")
179
+ const phase = join(root, "tasks/multi/phases/build")
180
+
181
+ expect(await runTestEffect(pr(["view", "--web"], task))).toBe(0)
182
+ expect(await captured()).toEqual([
183
+ await realpath(join(task, "code/agency")),
184
+ "pr",
185
+ "view",
186
+ "feat/single",
187
+ "--repo",
188
+ "example/agency",
189
+ "--web",
190
+ ])
191
+
192
+ expect(
193
+ await runTestEffect(pr(["create", "--title", "Example"], phase)),
194
+ ).toBe(0)
195
+ expect(await captured()).toEqual([
196
+ await realpath(join(phase, "code/agency")),
197
+ "pr",
198
+ "create",
199
+ "--head",
200
+ "feat/build",
201
+ "--repo",
202
+ "example/agency",
203
+ "--title",
204
+ "Example",
205
+ ])
206
+ })
207
+
208
+ test("preserves explicit jj PR and repository targets", async () => {
209
+ const root = await createExecutionWorkbase("jj")
210
+ const task = join(root, "tasks/single")
211
+ const args = ["view", "123", "--repo", "other/repository"]
212
+
213
+ expect(await runTestEffect(pr(args, task))).toBe(0)
214
+ expect(await captured()).toEqual([
215
+ await realpath(join(task, "code/agency")),
216
+ "pr",
217
+ ...args,
218
+ ])
219
+ })
220
+
166
221
  test("falls back to the invocation directory without execution authority", async () => {
167
222
  const root = await createExecutionWorkbase()
168
223
  const orchestration = join(root, "tasks/multi")
@@ -3,8 +3,10 @@ import { resolve } from "node:path"
3
3
  import { ContextService } from "../services/ContextService"
4
4
  import { FileSystemService } from "../services/FileSystemService"
5
5
  import { PullRequestService } from "../services/PullRequestService"
6
+ import { WorkbaseService } from "../services/WorkbaseService"
6
7
  import type { BaseCommandOptions } from "../utils/command"
7
8
  import { createLoggers } from "../utils/effect"
9
+ import { repositoryFromRemote } from "../workbase/delivery-command"
8
10
 
9
11
  interface PrCreateOptions extends BaseCommandOptions {
10
12
  readonly taskId: string
@@ -13,6 +15,56 @@ interface PrCreateOptions extends BaseCommandOptions {
13
15
  readonly force?: boolean
14
16
  }
15
17
 
18
+ const branchTargetCommands = new Set([
19
+ "checkout",
20
+ "checks",
21
+ "close",
22
+ "comment",
23
+ "diff",
24
+ "edit",
25
+ "lock",
26
+ "merge",
27
+ "ready",
28
+ "reopen",
29
+ "revert",
30
+ "review",
31
+ "unlock",
32
+ "update-branch",
33
+ "view",
34
+ ])
35
+
36
+ const hasOption = (args: readonly string[], short: string, long: string) =>
37
+ args.some(
38
+ (argument) =>
39
+ argument === short ||
40
+ argument.startsWith(`${short}=`) ||
41
+ argument === long ||
42
+ argument.startsWith(`${long}=`),
43
+ )
44
+
45
+ const withJjContext = (
46
+ args: readonly string[],
47
+ branch: string,
48
+ repository: string,
49
+ ) => {
50
+ const [command, ...rest] = args
51
+ if (!command) return args
52
+
53
+ const branchArgs =
54
+ (command === "create" || command === "new") &&
55
+ !hasOption(rest, "-H", "--head")
56
+ ? ["--head", branch]
57
+ : branchTargetCommands.has(command) &&
58
+ (rest.length === 0 || rest[0]?.startsWith("-"))
59
+ ? [branch]
60
+ : []
61
+ const repositoryArgs = hasOption(rest, "-R", "--repo")
62
+ ? []
63
+ : ["--repo", repository]
64
+
65
+ return [command, ...branchArgs, ...repositoryArgs, ...rest]
66
+ }
67
+
16
68
  export const prCreate = (options: PrCreateOptions) =>
17
69
  Effect.gen(function* () {
18
70
  const pullRequests = yield* PullRequestService
@@ -31,6 +83,7 @@ export const pr = (args: readonly string[], cwd: string = process.cwd()) =>
31
83
  Effect.gen(function* () {
32
84
  const contexts = yield* ContextService
33
85
  const fs = yield* FileSystemService
86
+ const workbase = yield* WorkbaseService
34
87
  const invocationCwd = resolve(cwd)
35
88
  const context = yield* contexts
36
89
  .get({ cwd: invocationCwd, target: ".", compact: true })
@@ -40,7 +93,26 @@ export const pr = (args: readonly string[], cwd: string = process.cwd()) =>
40
93
  ? context.authority.writable?.checkoutPath
41
94
  : null
42
95
  const focusedCwd = writableCheckout ?? invocationCwd
43
- const result = yield* fs.runCommand(["gh", "pr", ...args], {
96
+ let forwardedArgs = args
97
+ if (
98
+ writableCheckout &&
99
+ context?.workbase.vcs === "jj" &&
100
+ context.authority.writable
101
+ ) {
102
+ const execution =
103
+ context.documents?.phase?.data ?? context.documents?.task?.data
104
+ const { config } = yield* workbase.loadConfig(context.workbase.root)
105
+ const remote =
106
+ config.repositories?.[context.authority.writable.repo]?.remote
107
+ if (execution && "branch" in execution && remote) {
108
+ forwardedArgs = withJjContext(
109
+ args,
110
+ execution.branch,
111
+ repositoryFromRemote(remote),
112
+ )
113
+ }
114
+ }
115
+ const result = yield* fs.runCommand(["gh", "pr", ...forwardedArgs], {
44
116
  cwd: focusedCwd,
45
117
  passthrough: true,
46
118
  })
@@ -53,5 +125,6 @@ Usage: agency pr create <task-id> [phase-id] [--draft] [--force] [--json]
53
125
 
54
126
  Create records a pull request for an Agency execution unit. Other invocations
55
127
  run gh pr unchanged, focusing the writable repository checkout when invoked from
56
- an Agency execution task or phase.
128
+ an Agency execution task or phase. In jj workbases, Agency supplies the declared
129
+ branch and repository to gh subcommands that would otherwise infer Git context.
57
130
  `
@@ -16,20 +16,15 @@ import {
16
16
  normalizePullRequestRecord,
17
17
  parsePullRequestRecord,
18
18
  recordFromGitHubUrl,
19
+ repositoryFromRemote,
19
20
  resolveDeliveryCommand,
21
+ resolveGitHubCreateCommand,
20
22
  } from "../workbase/delivery-command"
21
23
 
22
24
  class PullRequestError extends Data.TaggedError("PullRequestError")<{
23
25
  readonly message: string
24
26
  }> {}
25
27
 
26
- const repositoryFromRemote = (remote: string) =>
27
- remote
28
- .replace(/^[a-z][a-z0-9+.-]*:\/\/(?:[^@/]+@)?[^/]+\//i, "")
29
- .replace(/^[^:]+:/, "")
30
- .replace(/\.git\/?$/, "")
31
- .replace(/\/$/, "")
32
-
33
28
  interface PullRequestOptions extends BaseCommandOptions {
34
29
  readonly force?: boolean
35
30
  }
@@ -197,18 +192,13 @@ export class PullRequestService extends Effect.Service<PullRequestService>()(
197
192
  url: "",
198
193
  identifier: "",
199
194
  })
200
- : {
201
- argv: [
202
- "gh",
203
- "pr",
204
- "create",
205
- "--fill",
206
- "--base",
207
- execution.base,
208
- ...(draft ? ["--draft"] : []),
209
- ],
210
- environment: {},
211
- }
195
+ : resolveGitHubCreateCommand({
196
+ base: execution.base,
197
+ branch: execution.branch,
198
+ repository,
199
+ draft,
200
+ vcs: config.vcs ?? "git",
201
+ })
212
202
  const created = yield* fs.runCommand(resolved.argv, {
213
203
  cwd: workspace.writablePath,
214
204
  captureOutput: true,
@@ -3,6 +3,7 @@ import {
3
3
  parsePullRequestRecord,
4
4
  recordFromGitHubJson,
5
5
  resolveDeliveryCommand,
6
+ resolveGitHubCreateCommand,
6
7
  validateDelivery,
7
8
  } from "./delivery-command"
8
9
 
@@ -31,6 +32,34 @@ describe("delivery commands", () => {
31
32
  })
32
33
  })
33
34
 
35
+ test("adds explicit jj context to the default GitHub create command", () => {
36
+ const input = {
37
+ base: "main",
38
+ branch: "feat/example",
39
+ repository: "example/agency",
40
+ draft: false,
41
+ } as const
42
+ expect(resolveGitHubCreateCommand({ ...input, vcs: "jj" })).toEqual({
43
+ argv: [
44
+ "gh",
45
+ "pr",
46
+ "create",
47
+ "--fill",
48
+ "--base",
49
+ "main",
50
+ "--head",
51
+ "feat/example",
52
+ "--repo",
53
+ "example/agency",
54
+ ],
55
+ environment: {},
56
+ })
57
+ expect(resolveGitHubCreateCommand({ ...input, vcs: "git" })).toEqual({
58
+ argv: ["gh", "pr", "create", "--fill", "--base", "main"],
59
+ environment: {},
60
+ })
61
+ })
62
+
34
63
  test("rejects unknown placeholders", () => {
35
64
  expect(() =>
36
65
  validateDelivery({ ...delivery, queryCommand: ["forge", "{unknown}"] }),
@@ -11,6 +11,39 @@ export interface DeliveryCommandVariables {
11
11
  readonly identifier: string
12
12
  }
13
13
 
14
+ export const repositoryFromRemote = (remote: string) =>
15
+ remote
16
+ .replace(/^[a-z][a-z0-9+.-]*:\/\/(?:[^@/]+@)?[^/]+\//i, "")
17
+ .replace(/^[^:]+:/, "")
18
+ .replace(/\.git\/?$/, "")
19
+ .replace(/\/$/, "")
20
+
21
+ export const resolveGitHubCreateCommand = ({
22
+ base,
23
+ branch,
24
+ repository,
25
+ draft,
26
+ vcs,
27
+ }: {
28
+ readonly base: string
29
+ readonly branch: string
30
+ readonly repository: string
31
+ readonly draft: boolean
32
+ readonly vcs: "git" | "jj"
33
+ }) => ({
34
+ argv: [
35
+ "gh",
36
+ "pr",
37
+ "create",
38
+ "--fill",
39
+ "--base",
40
+ base,
41
+ ...(vcs === "jj" ? ["--head", branch, "--repo", repository] : []),
42
+ ...(draft ? ["--draft"] : []),
43
+ ],
44
+ environment: {},
45
+ })
46
+
14
47
  const PLACEHOLDERS = new Set<keyof DeliveryCommandVariables>([
15
48
  "repository",
16
49
  "branch",