@markjaquith/agency 2.52.2 → 2.53.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 +11 -10
- package/cli-main.ts +5 -14
- package/package.json +1 -1
- package/src/cli-parser.test.ts +9 -5
- package/src/cli-parser.ts +35 -17
- package/src/cli.test.ts +30 -16
- package/src/commands/pr.test.ts +139 -37
- package/src/commands/pr.ts +23 -35
- package/src/services/FileSystemService.ts +17 -8
- package/src/workbase/AGENTS.md +2 -2
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ read or write.
|
|
|
10
10
|
- [Bun](https://bun.sh) 1.0 or newer
|
|
11
11
|
- Git
|
|
12
12
|
- [Jujutsu](https://jj-vcs.github.io/jj/) is preferred when available
|
|
13
|
-
- [GitHub CLI](https://cli.github.com/) for `agency pr
|
|
13
|
+
- [GitHub CLI](https://cli.github.com/) for `agency pr`
|
|
14
14
|
- OpenCode, Claude Code, or a configured runner for `agency work`
|
|
15
15
|
|
|
16
16
|
## Installation
|
|
@@ -401,7 +401,8 @@ agency task new
|
|
|
401
401
|
agency validate
|
|
402
402
|
agency context tasks/refresh-copy --json
|
|
403
403
|
agency work tasks/refresh-copy
|
|
404
|
-
|
|
404
|
+
cd tasks/refresh-copy
|
|
405
|
+
agency pr create --fill
|
|
405
406
|
```
|
|
406
407
|
|
|
407
408
|
After cloning an existing workbase on another machine, restore its declared
|
|
@@ -476,10 +477,10 @@ priority ready unit in human output.
|
|
|
476
477
|
execution unit. Excluded entries retain status, terminal state, `blockedBy`, and
|
|
477
478
|
detailed dependency, validation, or status blockers for orchestrators.
|
|
478
479
|
|
|
479
|
-
`agency work`
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
480
|
+
`agency work` consults this shared readiness model before materializing. Blocked,
|
|
481
|
+
done, and dropped targets are rejected unless `--force` is supplied explicitly.
|
|
482
|
+
`agency pr` leaves command semantics to `gh`; run `agency context . --json` first
|
|
483
|
+
when readiness or validation state needs inspection.
|
|
483
484
|
|
|
484
485
|
### Reconciliation
|
|
485
486
|
|
|
@@ -803,7 +804,7 @@ for restoration. Archived IDs are reserved until restored.
|
|
|
803
804
|
agency work [<directory> | --epic <epic-id>] [--runner <name>] [--auto] [--print-command]
|
|
804
805
|
agency work prepare [target] [--dry-run] [--json]
|
|
805
806
|
agency worktree <list|inspect|prepare|remove|rebuild|repair>
|
|
806
|
-
agency pr
|
|
807
|
+
agency pr [args...]
|
|
807
808
|
```
|
|
808
809
|
|
|
809
810
|
`agency work` presents the full hierarchy in the native OpenTUI selector or the
|
|
@@ -849,9 +850,9 @@ Agency resolves the ref to a commit and creates a detached worktree. Existing
|
|
|
849
850
|
reference worktrees are reused only while their commit still matches the declared
|
|
850
851
|
ref; use a commit SHA as `ref` when reproducibility matters.
|
|
851
852
|
|
|
852
|
-
`agency pr
|
|
853
|
-
|
|
854
|
-
the
|
|
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
856
|
|
|
856
857
|
### Status and Validation
|
|
857
858
|
|
package/cli-main.ts
CHANGED
|
@@ -134,6 +134,9 @@ const resolveInvocationCwd = (
|
|
|
134
134
|
) =>
|
|
135
135
|
runEffect(
|
|
136
136
|
Effect.gen(function* () {
|
|
137
|
+
if (commandName === "pr") {
|
|
138
|
+
return resolve(options.cwd ?? process.cwd())
|
|
139
|
+
}
|
|
137
140
|
if (
|
|
138
141
|
options.help ||
|
|
139
142
|
commandName === "init" ||
|
|
@@ -284,19 +287,7 @@ const commands: Record<string, Command> = {
|
|
|
284
287
|
console.log(prHelp)
|
|
285
288
|
return
|
|
286
289
|
}
|
|
287
|
-
await
|
|
288
|
-
pr({
|
|
289
|
-
subcommand: args[0],
|
|
290
|
-
taskId: args[1],
|
|
291
|
-
phaseId: args[2],
|
|
292
|
-
draft: options.draft,
|
|
293
|
-
force: options.force,
|
|
294
|
-
json: options.json,
|
|
295
|
-
silent: options.silent,
|
|
296
|
-
verbose: options.verbose,
|
|
297
|
-
cwd: options.cwd,
|
|
298
|
-
}),
|
|
299
|
-
)
|
|
290
|
+
process.exitCode = await runEffect(pr(args, options.cwd))
|
|
300
291
|
},
|
|
301
292
|
},
|
|
302
293
|
phase: {
|
|
@@ -736,7 +727,7 @@ Commands:
|
|
|
736
727
|
worktree <subcommand> Inspect and maintain managed workspaces
|
|
737
728
|
vcs <subcommand> Inspect or migrate the version-control backend
|
|
738
729
|
next List or select ready execution units
|
|
739
|
-
pr
|
|
730
|
+
pr [args...] Run gh pr with Agency repository focus
|
|
740
731
|
review refresh Explicitly refresh a pinned review task
|
|
741
732
|
repo <subcommand> Manage workbase repositories
|
|
742
733
|
status Show status for the current workbase
|
package/package.json
CHANGED
package/src/cli-parser.test.ts
CHANGED
|
@@ -292,7 +292,6 @@ describe("strict CLI parsing", () => {
|
|
|
292
292
|
[["restore", "task", "one", "two"], "agency restore task"],
|
|
293
293
|
[["restore", "phase", "one", "two", "three"], "agency restore phase"],
|
|
294
294
|
[["work", "one", "two"], "agency work"],
|
|
295
|
-
[["pr", "create", "one", "two", "three"], "agency pr create"],
|
|
296
295
|
[["status", "extra"], "agency status"],
|
|
297
296
|
[["validate", "one", "two"], "agency validate"],
|
|
298
297
|
[["context", "one", "two"], "agency context"],
|
|
@@ -358,15 +357,20 @@ describe("strict CLI parsing", () => {
|
|
|
358
357
|
commandName: "work",
|
|
359
358
|
values: { force: true },
|
|
360
359
|
})
|
|
361
|
-
expect(parseCli(["pr", "create", "example", "--force"])).toMatchObject({
|
|
362
|
-
commandName: "pr",
|
|
363
|
-
values: { force: true },
|
|
364
|
-
})
|
|
365
360
|
expect(() => parseCli(["work", "prepare", "example", "--force"])).toThrow(
|
|
366
361
|
"cannot be combined",
|
|
367
362
|
)
|
|
368
363
|
})
|
|
369
364
|
|
|
365
|
+
test("preserves every argument after pr without parsing it", () => {
|
|
366
|
+
const args = ["create", "--title", "two words", "--", "--literal"]
|
|
367
|
+
expect(parseCli(["--cwd", "/workbase", "pr", ...args])).toEqual({
|
|
368
|
+
commandName: "pr",
|
|
369
|
+
args,
|
|
370
|
+
values: { cwd: "/workbase" },
|
|
371
|
+
})
|
|
372
|
+
})
|
|
373
|
+
|
|
370
374
|
test("parses reconciliation modes and rejects conflicting modes", () => {
|
|
371
375
|
expect(parseCli(["sync", "--dry-run", "--json"])).toMatchObject({
|
|
372
376
|
commandName: "sync",
|
package/src/cli-parser.ts
CHANGED
|
@@ -897,22 +897,12 @@ const commands = {
|
|
|
897
897
|
},
|
|
898
898
|
},
|
|
899
899
|
pr: {
|
|
900
|
-
usage: "agency pr
|
|
901
|
-
options:
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
phase: { type: "string" },
|
|
907
|
-
},
|
|
908
|
-
subcommands: {
|
|
909
|
-
create: {
|
|
910
|
-
usage:
|
|
911
|
-
"agency pr create <task-id> [phase-id] [--draft] [--force] [--json]",
|
|
912
|
-
minArgs: 1,
|
|
913
|
-
maxArgs: 2,
|
|
914
|
-
options: ["draft", "force", "json", "task", "phase"],
|
|
915
|
-
},
|
|
900
|
+
usage: "agency pr [args...]",
|
|
901
|
+
options: commonOptions,
|
|
902
|
+
command: {
|
|
903
|
+
usage: "agency pr [args...]",
|
|
904
|
+
minArgs: 0,
|
|
905
|
+
maxArgs: Number.POSITIVE_INFINITY,
|
|
916
906
|
},
|
|
917
907
|
},
|
|
918
908
|
next: {
|
|
@@ -1103,7 +1093,6 @@ const targetSlots = (
|
|
|
1103
1093
|
if (commandName === "worktree" && subcommand !== "list") {
|
|
1104
1094
|
return ["task", "phase"]
|
|
1105
1095
|
}
|
|
1106
|
-
if (commandName === "pr" && subcommand === "create") return ["task", "phase"]
|
|
1107
1096
|
return []
|
|
1108
1097
|
}
|
|
1109
1098
|
|
|
@@ -1324,6 +1313,35 @@ export function parseCli(args: readonly string[]): ParsedCli {
|
|
|
1324
1313
|
"agency <command> [options]",
|
|
1325
1314
|
)
|
|
1326
1315
|
}
|
|
1316
|
+
if (commandName === "pr") {
|
|
1317
|
+
const parsed = parse(
|
|
1318
|
+
args.slice(0, commandIndex),
|
|
1319
|
+
rootOptions,
|
|
1320
|
+
"agency <command> [options]",
|
|
1321
|
+
)
|
|
1322
|
+
assertNoDuplicateOptions(
|
|
1323
|
+
parsed.tokens,
|
|
1324
|
+
new Set(),
|
|
1325
|
+
"agency <command> [options]",
|
|
1326
|
+
)
|
|
1327
|
+
if (parsed.values.silent && parsed.values.verbose) {
|
|
1328
|
+
throw usageError(
|
|
1329
|
+
"Options '--silent' and '--verbose' cannot be combined.",
|
|
1330
|
+
"agency <command> [options]",
|
|
1331
|
+
)
|
|
1332
|
+
}
|
|
1333
|
+
if (parsed.values.workbase && parsed.values.cwd) {
|
|
1334
|
+
throw usageError(
|
|
1335
|
+
"Options '--workbase' and '--cwd' cannot be combined.",
|
|
1336
|
+
"agency <command> [options]",
|
|
1337
|
+
)
|
|
1338
|
+
}
|
|
1339
|
+
return {
|
|
1340
|
+
commandName,
|
|
1341
|
+
args: [...args.slice(commandIndex + 1)],
|
|
1342
|
+
values: parsed.values,
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1327
1345
|
const commandArgs = [
|
|
1328
1346
|
...args.slice(0, commandIndex),
|
|
1329
1347
|
...args.slice(commandIndex + 1),
|
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, stat, symlink } from "node:fs/promises"
|
|
2
|
+
import { access, chmod, 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"
|
|
@@ -502,7 +502,7 @@ describe("CLI", () => {
|
|
|
502
502
|
["archive", "Usage: agency archive"],
|
|
503
503
|
["restore", "Usage: agency restore"],
|
|
504
504
|
["work", "Usage: agency work"],
|
|
505
|
-
["pr", "
|
|
505
|
+
["pr", "Work with GitHub pull requests."],
|
|
506
506
|
["status", "Usage: agency status"],
|
|
507
507
|
["validate", "Usage: agency validate"],
|
|
508
508
|
["context", "Usage: agency context"],
|
|
@@ -537,6 +537,34 @@ describe("CLI", () => {
|
|
|
537
537
|
expect(after).toEqual({ exitCode: 0, stdout: "", stderr: "" })
|
|
538
538
|
}, 30_000)
|
|
539
539
|
|
|
540
|
+
test("passes pr arguments and exit status through to gh", async () => {
|
|
541
|
+
const root = await createTempDir()
|
|
542
|
+
tempDirs.push(root)
|
|
543
|
+
const bin = join(root, "bin")
|
|
544
|
+
const capture = join(root, "capture")
|
|
545
|
+
await mkdir(bin)
|
|
546
|
+
await Bun.write(
|
|
547
|
+
join(bin, "gh"),
|
|
548
|
+
`#!/bin/sh
|
|
549
|
+
printf '%s\n' "$PWD" "$@" > "$GH_CAPTURE"
|
|
550
|
+
exit 23
|
|
551
|
+
`,
|
|
552
|
+
)
|
|
553
|
+
await chmod(join(bin, "gh"), 0o755)
|
|
554
|
+
const args = ["pr", "create", "--title", "two words", "--", "literal"]
|
|
555
|
+
|
|
556
|
+
const result = await runCli(args, root, {
|
|
557
|
+
PATH: `${bin}:${process.env.PATH ?? ""}`,
|
|
558
|
+
GH_CAPTURE: capture,
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
expect(result).toEqual({ exitCode: 23, stdout: "", stderr: "" })
|
|
562
|
+
expect((await Bun.file(capture).text()).trim().split("\n")).toEqual([
|
|
563
|
+
await realpath(root),
|
|
564
|
+
...args,
|
|
565
|
+
])
|
|
566
|
+
})
|
|
567
|
+
|
|
540
568
|
test("lists ready work and exposes excluded blockers through one result", async () => {
|
|
541
569
|
const root = await createTempDir()
|
|
542
570
|
tempDirs.push(root)
|
|
@@ -570,20 +598,6 @@ describe("CLI", () => {
|
|
|
570
598
|
blockers: [{ kind: "status", reason: "Task status is dropped" }],
|
|
571
599
|
},
|
|
572
600
|
])
|
|
573
|
-
|
|
574
|
-
const blockedPr = await runCli(["pr", "create", "finished", "--json"], root)
|
|
575
|
-
expect(blockedPr.exitCode).toBe(1)
|
|
576
|
-
expect(blockedPr.stderr).toBe("")
|
|
577
|
-
expect(JSON.parse(blockedPr.stdout)).toMatchObject({
|
|
578
|
-
ok: false,
|
|
579
|
-
error: {
|
|
580
|
-
code: "EXECUTION_BLOCKED",
|
|
581
|
-
fields: {
|
|
582
|
-
status: "dropped",
|
|
583
|
-
blockers: [{ kind: "status", reason: "Task status is dropped" }],
|
|
584
|
-
},
|
|
585
|
-
},
|
|
586
|
-
})
|
|
587
601
|
})
|
|
588
602
|
|
|
589
603
|
test("reports and synchronizes managed integration files", async () => {
|
package/src/commands/pr.test.ts
CHANGED
|
@@ -1,45 +1,147 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test"
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
|
+
import { chmod, mkdir, realpath } from "node:fs/promises"
|
|
3
|
+
import { dirname, join } from "node:path"
|
|
4
|
+
import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
|
|
5
5
|
import { pr } from "./pr"
|
|
6
6
|
|
|
7
|
+
const write = async (root: string, path: string, content: string) => {
|
|
8
|
+
const fullPath = join(root, path)
|
|
9
|
+
await mkdir(dirname(fullPath), { recursive: true })
|
|
10
|
+
await Bun.write(fullPath, content)
|
|
11
|
+
}
|
|
12
|
+
|
|
7
13
|
describe("pr command", () => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
14
|
+
let tempRoot: string
|
|
15
|
+
let capturePath: string
|
|
16
|
+
let originalPath: string | undefined
|
|
17
|
+
|
|
18
|
+
beforeEach(async () => {
|
|
19
|
+
tempRoot = await createTempDir()
|
|
20
|
+
capturePath = join(tempRoot, "capture")
|
|
21
|
+
const bin = join(tempRoot, "bin")
|
|
22
|
+
await mkdir(bin)
|
|
23
|
+
await Bun.write(
|
|
24
|
+
join(bin, "gh"),
|
|
25
|
+
`#!/bin/sh
|
|
26
|
+
printf '%s\n' "$PWD" "$@" > "$GH_CAPTURE"
|
|
27
|
+
exit "\${GH_EXIT:-0}"
|
|
28
|
+
`,
|
|
29
|
+
)
|
|
30
|
+
await chmod(join(bin, "gh"), 0o755)
|
|
31
|
+
originalPath = process.env.PATH
|
|
32
|
+
process.env.PATH = `${bin}:${originalPath ?? ""}`
|
|
33
|
+
process.env.GH_CAPTURE = capturePath
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
afterEach(async () => {
|
|
37
|
+
process.env.PATH = originalPath
|
|
38
|
+
delete process.env.GH_CAPTURE
|
|
39
|
+
delete process.env.GH_EXIT
|
|
40
|
+
await cleanupTempDir(tempRoot)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const captured = async () =>
|
|
44
|
+
(await Bun.file(capturePath).text()).trim().split("\n")
|
|
45
|
+
|
|
46
|
+
const createExecutionWorkbase = async () => {
|
|
47
|
+
const root = join(tempRoot, "workbase")
|
|
48
|
+
await write(root, "agency.json", '{"version":2}\n')
|
|
49
|
+
await mkdir(join(root, "repos/agency"), { recursive: true })
|
|
50
|
+
await mkdir(join(root, "tasks/single/code/agency"), { recursive: true })
|
|
51
|
+
await write(
|
|
52
|
+
root,
|
|
53
|
+
"tasks/single/TASK.md",
|
|
54
|
+
`---
|
|
55
|
+
ticketUrl: null
|
|
56
|
+
repo: agency
|
|
57
|
+
branch: feat/single
|
|
58
|
+
base: main
|
|
59
|
+
pr: null
|
|
60
|
+
status: open
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
# Single
|
|
64
|
+
`,
|
|
30
65
|
)
|
|
66
|
+
await mkdir(join(root, "tasks/multi/phases/build/code/agency"), {
|
|
67
|
+
recursive: true,
|
|
68
|
+
})
|
|
69
|
+
await write(
|
|
70
|
+
root,
|
|
71
|
+
"tasks/multi/TASK.md",
|
|
72
|
+
`---
|
|
73
|
+
ticketUrl: null
|
|
74
|
+
phases:
|
|
75
|
+
- id: build
|
|
76
|
+
---
|
|
31
77
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"/
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
78
|
+
# Multi
|
|
79
|
+
`,
|
|
80
|
+
)
|
|
81
|
+
await write(
|
|
82
|
+
root,
|
|
83
|
+
"tasks/multi/phases/build/PHASE.md",
|
|
84
|
+
`---
|
|
85
|
+
repo: agency
|
|
86
|
+
branch: feat/build
|
|
87
|
+
base: main
|
|
88
|
+
pr: null
|
|
89
|
+
status: open
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
# Build
|
|
93
|
+
`,
|
|
94
|
+
)
|
|
95
|
+
return root
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
test("focuses task and descendant invocations on the writable checkout", async () => {
|
|
99
|
+
const root = await createExecutionWorkbase()
|
|
100
|
+
const task = join(root, "tasks/single")
|
|
101
|
+
const descendant = join(task, "notes/deep")
|
|
102
|
+
await mkdir(descendant, { recursive: true })
|
|
103
|
+
|
|
104
|
+
for (const cwd of [task, descendant]) {
|
|
105
|
+
expect(await runTestEffect(pr(["view"], cwd))).toBe(0)
|
|
106
|
+
expect(await captured()).toEqual([
|
|
107
|
+
await realpath(join(task, "code/agency")),
|
|
108
|
+
"pr",
|
|
109
|
+
"view",
|
|
110
|
+
])
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test("focuses a phase invocation on its writable checkout", async () => {
|
|
115
|
+
const root = await createExecutionWorkbase()
|
|
116
|
+
const phase = join(root, "tasks/multi/phases/build")
|
|
117
|
+
|
|
118
|
+
expect(await runTestEffect(pr(["status"], phase))).toBe(0)
|
|
119
|
+
expect(await captured()).toEqual([
|
|
120
|
+
await realpath(join(phase, "code/agency")),
|
|
121
|
+
"pr",
|
|
122
|
+
"status",
|
|
43
123
|
])
|
|
44
124
|
})
|
|
125
|
+
|
|
126
|
+
test("falls back to the invocation directory without execution authority", async () => {
|
|
127
|
+
const root = await createExecutionWorkbase()
|
|
128
|
+
const orchestration = join(root, "tasks/multi")
|
|
129
|
+
const outside = join(tempRoot, "outside")
|
|
130
|
+
await mkdir(outside)
|
|
131
|
+
|
|
132
|
+
for (const cwd of [orchestration, outside]) {
|
|
133
|
+
expect(await runTestEffect(pr(["list"], cwd))).toBe(0)
|
|
134
|
+
expect(await captured()).toEqual([await realpath(cwd), "pr", "list"])
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test("forwards arguments unchanged and returns the gh exit code", async () => {
|
|
139
|
+
const outside = join(tempRoot, "outside")
|
|
140
|
+
await mkdir(outside)
|
|
141
|
+
process.env.GH_EXIT = "23"
|
|
142
|
+
const args = ["create", "--title", "two words", "--", "literal"]
|
|
143
|
+
|
|
144
|
+
expect(await runTestEffect(pr(args, outside))).toBe(23)
|
|
145
|
+
expect(await captured()).toEqual([await realpath(outside), "pr", ...args])
|
|
146
|
+
})
|
|
45
147
|
})
|
package/src/commands/pr.ts
CHANGED
|
@@ -1,43 +1,31 @@
|
|
|
1
1
|
import { Effect } from "effect"
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { resolve } from "node:path"
|
|
3
|
+
import { ContextService } from "../services/ContextService"
|
|
4
|
+
import { FileSystemService } from "../services/FileSystemService"
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
readonly subcommand?: string
|
|
8
|
-
readonly taskId?: string
|
|
9
|
-
readonly phaseId?: string
|
|
10
|
-
readonly draft?: boolean
|
|
11
|
-
readonly force?: boolean
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export const pr = (options: PrOptions) =>
|
|
6
|
+
export const pr = (args: readonly string[], cwd: string = process.cwd()) =>
|
|
15
7
|
Effect.gen(function* () {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
8
|
+
const contexts = yield* ContextService
|
|
9
|
+
const fs = yield* FileSystemService
|
|
10
|
+
const invocationCwd = resolve(cwd)
|
|
11
|
+
const context = yield* contexts
|
|
12
|
+
.get({ cwd: invocationCwd, target: ".", compact: true })
|
|
13
|
+
.pipe(Effect.catchAll(() => Effect.succeed(null)))
|
|
14
|
+
const writableCheckout =
|
|
15
|
+
context?.validation.valid && context.workspace?.writable?.materialized
|
|
16
|
+
? context.authority.writable?.checkoutPath
|
|
17
|
+
: null
|
|
18
|
+
const focusedCwd = writableCheckout ?? invocationCwd
|
|
19
|
+
const result = yield* fs.runCommand(["gh", "pr", ...args], {
|
|
20
|
+
cwd: focusedCwd,
|
|
21
|
+
passthrough: true,
|
|
22
|
+
})
|
|
23
|
+
return result.exitCode
|
|
31
24
|
})
|
|
32
25
|
|
|
33
26
|
export const help = `
|
|
34
|
-
Usage: agency pr
|
|
35
|
-
|
|
36
|
-
Push the execution branch, create a pull request with the delivery provider,
|
|
37
|
-
and update its task or phase document.
|
|
27
|
+
Usage: agency pr [args...]
|
|
38
28
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
--force Override readiness and terminal-state guards
|
|
42
|
-
--json Output the pull request URL as JSON
|
|
29
|
+
Run gh pr unchanged, focusing the writable repository checkout when invoked
|
|
30
|
+
from an Agency execution task or phase.
|
|
43
31
|
`
|
|
@@ -218,25 +218,34 @@ export class FileSystemService extends Effect.Service<FileSystemService>()(
|
|
|
218
218
|
readonly cwd?: string
|
|
219
219
|
readonly captureOutput?: boolean
|
|
220
220
|
readonly forwardOutput?: boolean
|
|
221
|
+
readonly passthrough?: boolean
|
|
221
222
|
readonly env?: Record<string, string>
|
|
222
223
|
},
|
|
223
224
|
) =>
|
|
224
225
|
pipe(
|
|
225
226
|
spawnProcess(args, {
|
|
226
227
|
cwd: options?.cwd,
|
|
227
|
-
stdin: "pipe",
|
|
228
|
-
stdout: options?.
|
|
229
|
-
? "
|
|
230
|
-
: options?.
|
|
231
|
-
? "
|
|
232
|
-
:
|
|
233
|
-
|
|
228
|
+
stdin: options?.passthrough ? "inherit" : "pipe",
|
|
229
|
+
stdout: options?.passthrough
|
|
230
|
+
? "inherit"
|
|
231
|
+
: options?.forwardOutput
|
|
232
|
+
? "tee"
|
|
233
|
+
: options?.captureOutput
|
|
234
|
+
? "pipe"
|
|
235
|
+
: "inherit",
|
|
236
|
+
stderr: options?.passthrough
|
|
237
|
+
? "inherit"
|
|
238
|
+
: options?.forwardOutput
|
|
239
|
+
? "tee"
|
|
240
|
+
: "pipe",
|
|
234
241
|
env: options?.env,
|
|
235
242
|
}),
|
|
236
243
|
Effect.mapError(
|
|
237
244
|
(processError) =>
|
|
238
245
|
new FileSystemError({
|
|
239
|
-
message:
|
|
246
|
+
message: options?.passthrough
|
|
247
|
+
? processError.message
|
|
248
|
+
: `Failed to run command: ${args.join(" ")}`,
|
|
240
249
|
cause: processError,
|
|
241
250
|
}),
|
|
242
251
|
),
|
package/src/workbase/AGENTS.md
CHANGED
|
@@ -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
|
|
63
|
-
|
|
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.
|
|
64
64
|
|
|
65
65
|
## Execution
|
|
66
66
|
|