@pmelab/gtd 64.3.0 → 65.0.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.
Files changed (58) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +34 -25
  3. package/claude/hooks/assist.ts +9 -35
  4. package/claude/hooks/doors.ts +4 -4
  5. package/claude/hooks/git.ts +56 -0
  6. package/claude/hooks/handoff.ts +11 -11
  7. package/claude/hooks/jobs.ts +8 -8
  8. package/claude/hooks/pr.ts +8 -8
  9. package/claude/hooks/register.tsx +16 -78
  10. package/claude/hooks/tools.ts +7 -20
  11. package/claude/hooks/wording.ts +3 -3
  12. package/dist/chunks/{flows-dnIE_qbe.mjs → flows-DGs0W3HI.mjs} +19 -2
  13. package/dist/chunks/{testkit-BUX-47TD.mjs → testkit-CgrLmpdJ.mjs} +740 -79
  14. package/dist/chunks/{wire-CaPJ9Tme.mjs → wire-BNS8ciNu.mjs} +5 -3
  15. package/dist/flows.mjs +1 -1
  16. package/dist/gtd.bundle.mjs +553 -81
  17. package/dist/gtd.core.mjs +132 -129
  18. package/dist/testkit/gherkin.mjs +1 -1
  19. package/dist/testkit.mjs +2 -2
  20. package/dist/wire.mjs +1 -1
  21. package/dist/workflow-kit.mjs +1 -1
  22. package/package.json +1 -1
  23. package/src/cli/Cli.ts +4 -4
  24. package/src/cli/History.test.ts +10 -8
  25. package/src/cli/Metrics.test.ts +2 -1
  26. package/src/cli/Program.test.ts +4 -2
  27. package/src/cli/Program.ts +87 -5
  28. package/src/edge/Edge.test.ts +37 -16
  29. package/src/edge/Edge.ts +296 -31
  30. package/src/emit/GitScript.ts +116 -15
  31. package/src/emit/index.ts +7 -0
  32. package/src/flows/config.ts +7 -1
  33. package/src/flows/index.test.ts +1 -0
  34. package/src/flows/index.ts +2 -0
  35. package/src/flows/runtime.ts +32 -1
  36. package/src/platform/RetainedHistory.ts +3 -0
  37. package/src/platform/index.ts +1 -0
  38. package/src/replay/Replay.test.ts +9 -0
  39. package/src/replay/Replay.ts +59 -15
  40. package/src/replay/Trailers.ts +14 -0
  41. package/src/replay/index.ts +1 -1
  42. package/src/step/LandStep.ts +12 -0
  43. package/src/step/RepoSnapshot.ts +11 -0
  44. package/src/step/index.ts +1 -0
  45. package/src/step/planStart.test.ts +4 -3
  46. package/src/step/planStart.ts +7 -0
  47. package/src/step/planStep.ts +17 -12
  48. package/src/step/startBranch.ts +54 -0
  49. package/src/twins/EmittedScriptRecognizer.ts +117 -0
  50. package/src/twins/EmittedScripts.test.ts +147 -1
  51. package/src/twins/InMemRepo.ts +116 -0
  52. package/src/wire/Demand.ts +22 -10
  53. package/src/wire/constants.ts +3 -0
  54. package/src/wire/types.ts +4 -0
  55. package/src/workflow/Workflow.ts +1 -0
  56. package/src/workflow/compile.ts +2 -0
  57. package/src/workflow/load.ts +2 -0
  58. package/claude/hooks/ship.ts +0 -730
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtd",
3
- "version": "64.3.0",
3
+ "version": "65.0.0",
4
4
  "description": "Drive the gtd loop inside Claude Code: agent turns run as subagents, gtd decides every beat",
5
5
  "author": {
6
6
  "name": "Philipp Melab"
package/README.md CHANGED
@@ -362,24 +362,19 @@ questions one at a time, answers your own questions about the plan or the diff,
362
362
  records what you decide in the steering file the way the step asks, and moves
363
363
  the process on only when you say so; a review you leave untouched signs off.
364
364
  While a step runs you can keep asking; requests for changes wait for the next
365
- rest. When the process finishes, Claude offers to squash it and open its pull
366
- request (or run `/gtd ship`: one commit under one pull request, see the
367
- [driver doc](docs/driver.md)); a process started for an issue ships with
368
- `Closes #N` on its pull request; a closed pull request whose branch was deleted
369
- from origin is left behind and a new one opens. A process can change hands at
370
- any gate: ask Claude to hand it off (or run `/gtd throw @dev`) and it opens a
371
- draft pull request assigned to them, and `/gtd catch <pr>` picks it up exactly
372
- where it waits, so whoever wrote the requirements can hand the build to someone
373
- else. `/gtd pr` brings a pull request's unresolved review threads into the
374
- review: each becomes a note in `.gtd/REVIEW.md` ticked "fix as suggested"
375
- (untick it for "won't fix" plus a reason, or your own answer), the review door
376
- fixes them in place, and each thread then gets a reply naming the fixing commit
377
- (or your reason) and is resolved. Ship, hand-off and throw answer at once and
378
- run in the background, with progress on the status line; they report by waking
379
- Claude, a toast, a transcript line and a push notification, and a failed push or
380
- `gh` call reports as a failure. While a ship or hand-off runs or is queued,
381
- `/gtd catch`, the doors, `/gtd <requirements>` and bare `/gtd` wait for its
382
- report. See
365
+ rest. A process can change hands at any gate: ask Claude to hand it off (or run
366
+ `/gtd throw @dev`) and it opens a draft pull request assigned to them, and
367
+ `/gtd catch <pr>` picks it up exactly where it waits, so whoever wrote the
368
+ requirements can hand the build to someone else. `/gtd pr` brings a pull
369
+ request's unresolved review threads into the review: each becomes a note in
370
+ `.gtd/REVIEW.md` ticked "fix as suggested" (untick it for "won't fix" plus a
371
+ reason, or your own answer), the review door fixes them in place, and each
372
+ thread then gets a reply naming the fixing commit (or your reason) and is
373
+ resolved. Hand-off and throw answer at once and run in the background, with
374
+ progress on the status line; they report by waking Claude, a toast, a transcript
375
+ line and a push notification, and a failed push or `gh` call reports as a
376
+ failure. While a hand-off runs or is queued, `/gtd catch`, the doors,
377
+ `/gtd <requirements>` and bare `/gtd` wait for its report. See
383
378
  [Inside Claude Code](https://github.com/pmelab/gtd/blob/main/docs/driver.md#inside-claude-code-the-gtd-mod).
384
379
 
385
380
  ### Then let an agent build your own
@@ -499,15 +494,29 @@ or `.gitmodules` `ignore` say. Works in any git repository, no process needed.
499
494
 
500
495
  ## Process history
501
496
 
497
+ Every process runs on the branch `gtd/<b>`, never on your branch `<b>`: starting
498
+ creates it (or reuses one whose commits are on `<b>` or in a kept history ref;
499
+ any other `gtd/<b>` is refused). A finished process switches HEAD back to `<b>`,
500
+ and gtd moves `<b>` only when the workflow calls `ship()`, which squashes the
501
+ process onto `<b>` as one commit (see
502
+ [Shipping](docs/configuration.md#shipping)); what it committed after its last
503
+ ship stays on `gtd/<b>`. `gtd abandon` switches back to `<b>` with the unshipped
504
+ work uncommitted and deletes `gtd/<b>`; `gtd restore` recreates it. The closing
505
+ commit records `Gtd-Branch: <b>`.
506
+
502
507
  When a process finishes, the landing that closes it also keeps the whole run
503
508
  behind a ref, `refs/gtd/history/<entry>`, so a squash or a deleted branch
504
- doesn't lose it. `/gtd ship` pushes this process's own ref to origin alongside
505
- the branch (never the others) and writes a `Gtd-History: <ref>` trailer on the
506
- squashed commit. The push is forced, so it replaces origin's copy of that one
507
- ref. If that push fails, the ship fails and nothing is squashed. Recover the
508
- full trail with `s=$(gtd history fetch --json=script) && sh -c "$s"`, then
509
+ doesn't lose it. A `ship()` step writes the ref too, and names it in a
510
+ `Gtd-History: <ref>` trailer on the commit it puts on `<b>`. Recover the full
511
+ trail with `s=$(gtd history fetch --json=script) && sh -c "$s"`, then
509
512
  `gtd history show <ref>`.
510
513
 
514
+ `gtd abandon` keeps `refs/gtd/history/<entry>` at the abandoned tip too; history
515
+ lists and counts it like a finished one. Set `historyRemote` in the `config`
516
+ export to a remote name and every finish, ship and abandon force-pushes its ref
517
+ there; a failed push exits non-zero after the commit landed and prints the
518
+ retry.
519
+
511
520
  ```sh
512
521
  gtd history list # one line per kept process
513
522
  gtd history show <ref> # replay its steps, judge verdicts and approvals
@@ -586,7 +595,7 @@ gtd history judges --jsonl > judges.jsonl
586
595
  ```
587
596
 
588
597
  `gtd history fetch` only prints a fetch script; pipe it into `sh` to run it.
589
- Beyond that one ship push, refs are not pushed or fetched by default. The ref
598
+ Without `historyRemote`, refs are not pushed or fetched by default. The ref
590
599
  holds every step commit, including steering files and code a later step removed,
591
600
  so anyone who can fetch the remote can read it. To share the rest:
592
601
 
@@ -597,7 +606,7 @@ git config --add remote.origin.fetch 'refs/gtd/history/*:refs/gtd/history/*'
597
606
 
598
607
  The second line makes every plain `git fetch` include them. It has no leading
599
608
  `+`, so a conflicting local ref fails instead of being overwritten: after a
600
- forced `/gtd ship` push, a clone holding the old tip deletes its local ref
609
+ forced `historyRemote` push, a clone holding the old tip deletes its local ref
601
610
  (`git update-ref -d <ref>`) and fetches again.
602
611
 
603
612
  A step declared with `keep: true` files its turn under `refs/gtd/kept/` instead
@@ -5,7 +5,7 @@
5
5
 
6
6
  import type { Stop } from "../types"
7
7
  import { RELOAD_CUT_SCRIPT } from "./wording"
8
- import type { Shipped } from "./ship"
8
+ import type { Done } from "./git"
9
9
  import { JOB_NAME, TOOL } from "./tools"
10
10
  import type { JobName } from "./tools"
11
11
 
@@ -23,16 +23,12 @@ export type Wake = {
23
23
  isRepeat?: boolean
24
24
  // the judge questions an undecided judge gate puts to the person
25
25
  questions?: Asked[]
26
- // the branch a finished process can open its pull request from
27
- branch?: string
28
26
  }
29
27
 
30
- // No wake for a stop the person asked for; a finished process wakes only
31
- // when it has a pull request to offer.
32
- export function wakes(stop: Stop, branch: string | undefined): boolean {
28
+ // No wake for a finished process or a stop the person asked for.
29
+ export function wakes(stop: Stop): boolean {
33
30
  if (stop.kind === "stopped") return stop.text === RELOAD_CUT_SCRIPT
34
- if (stop.kind === "done") return branch !== undefined
35
- return true
31
+ return stop.kind !== "done"
36
32
  }
37
33
 
38
34
  const step = (s: Stop) => s.label ?? s.state ?? "the current step"
@@ -116,23 +112,9 @@ function trouble(w: Wake) {
116
112
  ]
117
113
  }
118
114
 
119
- function finished(w: Wake) {
120
- return [
121
- `gtd finished the process on branch ${w.branch}.`,
122
- `Tell the person in two or three lines what was built (read the branch's commits), then ask whether to squash it and open its pull request(s). On yes, call ${TOOL.ship}.`,
123
- ]
124
- }
125
-
126
115
  export function wakePrompt(w: Wake): string {
127
116
  const { stop } = w
128
- const body =
129
- stop.kind === "done"
130
- ? finished(w)
131
- : stop.kind === "gate"
132
- ? stop.isJudge
133
- ? judged(w)
134
- : gate(w)
135
- : trouble(w)
117
+ const body = stop.kind === "gate" ? (stop.isJudge ? judged(w) : gate(w)) : trouble(w)
136
118
  const notes = w.notes?.length
137
119
  ? [
138
120
  `While gtd ran, the person asked for the following. Fold each in now in the way this step allows, or tell them why it does not fit here:\n${w.notes.map((n) => `- ${n}`).join("\n")}`,
@@ -142,9 +124,9 @@ export function wakePrompt(w: Wake): string {
142
124
  return [...body, ...notes, RULES, extras].join("\n\n")
143
125
  }
144
126
 
145
- // The wake after a background ship or hand-off ended. It must not forbid further work:
127
+ // The wake after a background hand-off ended. It must not forbid further work:
146
128
  // an agent with standing instructions (a review loop, a CI watch) would go idle.
147
- export function jobPrompt(job: JobName, outcome: Shipped): string {
129
+ export function jobPrompt(job: JobName, outcome: Done): string {
148
130
  const name = JOB_NAME[job]
149
131
  if (outcome.ok) {
150
132
  return [
@@ -153,17 +135,9 @@ export function jobPrompt(job: JobName, outcome: Shipped): string {
153
135
  "Relay the result to the person in a line or two, with any link in it; then continue with any work the person already asked for.",
154
136
  ].join("\n\n")
155
137
  }
156
- const lines = [
138
+ return [
157
139
  `The ${name} the person asked for failed. gtd reports the failed step:`,
158
140
  fence(outcome.text),
159
- ]
160
- if (job === "ship") {
161
- lines.push(
162
- `If the commits were already squashed, they stay squashed: running /gtd ship again pushes the already-squashed branch.`,
163
- )
164
- }
165
- lines.push(
166
141
  "Tell the person what failed in plain words and what to try. Do not retry it yourself.",
167
- )
168
- return lines.join("\n\n")
142
+ ].join("\n\n")
169
143
  }
@@ -2,7 +2,7 @@
2
2
  // prints the script that starts one; running it is the driver's job, as with
3
3
  // every git write gtd plans. The mod hardcodes no door: it asks gtd.
4
4
 
5
- import type { ShipIo, Shipped } from "./ship"
5
+ import type { GitIo, Done } from "./git"
6
6
 
7
7
  export type Door = {
8
8
  name: string
@@ -10,12 +10,12 @@ export type Door = {
10
10
  args: { name: string; optional: boolean }[]
11
11
  }
12
12
 
13
- const fail = (text: string): Shipped => ({ ok: false, text })
13
+ const fail = (text: string): Done => ({ ok: false, text })
14
14
 
15
15
  type Startable = { kind: "workflow"; name: string } | ({ kind: "door" } & Door)
16
16
 
17
17
  // Every door the repository offers; none when gtd cannot say.
18
- export async function doors(io: ShipIo): Promise<Door[]> {
18
+ export async function doors(io: GitIo): Promise<Door[]> {
19
19
  const listed = await io.run(["gtd", "start", "--json"])
20
20
  if (listed.code !== 0) return []
21
21
  try {
@@ -27,7 +27,7 @@ export async function doors(io: ShipIo): Promise<Door[]> {
27
27
  }
28
28
  }
29
29
 
30
- export async function enter(io: ShipIo, name: string, args: string[]): Promise<Shipped> {
30
+ export async function enter(io: GitIo, name: string, args: string[]): Promise<Done> {
31
31
  const argv = ["gtd", "start", name, ...args]
32
32
  const planned = await io.run(argv)
33
33
  if (planned.code !== 0)
@@ -0,0 +1,56 @@
1
+ export type Ran = { code: number; out: string; err: string }
2
+
3
+ export type GitIo = {
4
+ // argv runs in the repository root; stdin is the child's whole input
5
+ run(argv: string[], stdin?: string): Promise<Ran>
6
+ log(line: string): void
7
+ }
8
+
9
+ export type Done = { ok: boolean; text: string }
10
+
11
+ // A rejection becomes `killed`, so the caller can tell a timeout from a refused
12
+ // push. Relies on the host's process runner rejecting at its timeout; a spawn
13
+ // failure lands here too. Only pushes: any other crashed read must abort, not
14
+ // pass as an answer.
15
+ const attempt = async (io: GitIo, argv: string[]): Promise<Ran & { killed: boolean }> => {
16
+ try {
17
+ return { ...(await io.run(argv)), killed: false }
18
+ } catch (err) {
19
+ return { code: 1, out: "", err: err instanceof Error ? err.message : String(err), killed: true }
20
+ }
21
+ }
22
+
23
+ export async function defaultBranch(git: (...args: string[]) => Promise<string>) {
24
+ const head = await git("symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD")
25
+ return head.replace(/^origin\//, "") || "main"
26
+ }
27
+
28
+ // A pull request describes what the remote branch holds. A rewritten branch
29
+ // needs --force-with-lease on an existing origin/<branch>. `fresh`: origin
30
+ // lacks the branch, so any tracking ref is stale. Returns why the push failed,
31
+ // or nothing.
32
+ export async function pushBranch(io: GitIo, branch: string, fresh = false) {
33
+ const ok = async (...args: string[]) => (await io.run(["git", ...args])).code === 0
34
+ const push = async (args: string[], failure: string) => {
35
+ const r = await attempt(io, ["git", "push", ...args])
36
+ if (r.killed)
37
+ return `git push of ${branch} did not finish; nothing was lost. Retry without fetching.\n${r.err}`.trimEnd()
38
+ if (r.code !== 0) return `${failure}\n${r.err}`.trimEnd()
39
+ return undefined
40
+ }
41
+ if (fresh || !(await ok("rev-parse", "--verify", "--quiet", `origin/${branch}`))) {
42
+ io.log(`Pushing ${branch} to origin…`)
43
+ return push(["--set-upstream", "origin", branch], "git push failed.")
44
+ }
45
+ if (!(await ok("merge-base", "--is-ancestor", "HEAD", `origin/${branch}`))) {
46
+ io.log(`Pushing ${branch} to origin…`)
47
+ return push(
48
+ ["--force-with-lease", "origin", branch],
49
+ `git push failed: origin/${branch} moved. Fetch and reconcile first.`,
50
+ )
51
+ }
52
+ return undefined
53
+ }
54
+
55
+ // Marks a draft pull request `/gtd throw` opened for a hand-off.
56
+ export const THROWN = "<!-- gtd:thrown -->"
@@ -2,12 +2,12 @@
2
2
  // pushing the branch and telling them. `throw` opens (or refreshes) a draft
3
3
  // pull request for the hand-off; `catch` checks it out wherever it was thrown.
4
4
 
5
- import { defaultBranch, pushBranch, THROWN } from "./ship"
6
- import type { ShipIo, Shipped } from "./ship"
5
+ import { defaultBranch, pushBranch, THROWN } from "./git"
6
+ import type { GitIo, Done } from "./git"
7
7
 
8
8
  export type Rest = { state?: string; label?: string; content?: string; isIdle: boolean }
9
9
 
10
- const fail = (text: string): Shipped => ({ ok: false, text })
10
+ const fail = (text: string): Done => ({ ok: false, text })
11
11
 
12
12
  type Pr = {
13
13
  number: number
@@ -18,11 +18,11 @@ type Pr = {
18
18
  }
19
19
 
20
20
  export async function throwTo(
21
- io: ShipIo,
21
+ io: GitIo,
22
22
  rest: Rest,
23
23
  target: string | undefined,
24
24
  now: string,
25
- ): Promise<Shipped> {
25
+ ): Promise<Done> {
26
26
  if (rest.isIdle) return fail("Nothing is in progress, so there is nothing to throw.")
27
27
  // Only landed state travels: an edit still in the tree is half an answer.
28
28
  if ((await io.run(["git", "status", "--porcelain"])).out.trim()) {
@@ -59,7 +59,7 @@ export async function throwTo(
59
59
  // The catcher checks out what origin holds, so a throw describing local HEAD
60
60
  // must not leave origin elsewhere: fetch first, a stale tracking ref can hide
61
61
  // a teammate's commits or a rewind. Returns why the throw refuses, or nothing.
62
- async function remoteAhead(io: ShipIo, branch: string) {
62
+ async function remoteAhead(io: GitIo, branch: string) {
63
63
  const ok = async (...args: string[]) => (await io.run(["git", ...args])).code === 0
64
64
  if (!(await ok("fetch", "--prune", "origin"))) return "git fetch failed."
65
65
  if (!(await ok("rev-parse", "--verify", "--quiet", `origin/${branch}`))) return undefined
@@ -69,7 +69,7 @@ async function remoteAhead(io: ShipIo, branch: string) {
69
69
 
70
70
  // The branch to throw, moving the process off the default branch onto a new
71
71
  // one when it sits there; a string is why that failed.
72
- async function throwBranch(io: ShipIo, now: string) {
72
+ async function throwBranch(io: GitIo, now: string) {
73
73
  const git = async (...args: string[]) => (await io.run(["git", ...args])).out.trim()
74
74
  const branch = await git("rev-parse", "--abbrev-ref", "HEAD")
75
75
  if (!branch || branch === "HEAD") return "Detached HEAD: there is no branch to throw."
@@ -103,7 +103,7 @@ ${content.slice(0, 6000)}
103
103
  // Opens the hand-off's draft, or refreshes a thrown one; a string is why that
104
104
  // failed.
105
105
  async function draft(
106
- io: ShipIo,
106
+ io: GitIo,
107
107
  branch: string,
108
108
  base: string,
109
109
  rest: Rest,
@@ -144,7 +144,7 @@ async function draft(
144
144
  const pr = JSON.parse(view.out) as Pr
145
145
  if (pr.state !== "OPEN") return `The pull request for ${branch} is ${pr.state}, not open.`
146
146
  // A pull request someone opened for review keeps its own description and
147
- // stays ready: only ship marks a draft ready again, and only a thrown one.
147
+ // stays ready: only a thrown one is rewritten and made a draft again.
148
148
  if (pr.body?.includes(THROWN)) {
149
149
  const edited = await io.run(["gh", "pr", "edit", String(pr.number), "--body-file", "-"], body)
150
150
  if (edited.code !== 0) return `gh pr edit failed; the pull request is unchanged.\n${edited.err}`
@@ -155,7 +155,7 @@ async function draft(
155
155
  return pr
156
156
  }
157
157
 
158
- async function assign(io: ShipIo, pr: Pr, handle: string) {
158
+ async function assign(io: GitIo, pr: Pr, handle: string) {
159
159
  const stale = (pr.assignees ?? []).map((a) => a.login).filter((login) => login !== handle)
160
160
  const args = ["gh", "pr", "edit", String(pr.number), "--add-assignee", handle]
161
161
  for (const login of stale) args.push("--remove-assignee", login)
@@ -165,7 +165,7 @@ async function assign(io: ShipIo, pr: Pr, handle: string) {
165
165
  : `Thrown to ${pr.url}, but assigning @${handle} failed.\n${assigned.err}`
166
166
  }
167
167
 
168
- export async function catchFrom(io: ShipIo, ref: string): Promise<Shipped> {
168
+ export async function catchFrom(io: GitIo, ref: string): Promise<Done> {
169
169
  if ((await io.run(["git", "status", "--porcelain"])).out.trim()) {
170
170
  return fail("The working tree has edits. Commit or stash them before catching.")
171
171
  }
@@ -1,8 +1,8 @@
1
- // The one background slot ship and hand-off share: both push the branch, so
2
- // two at once would race. No global deadline on purpose: a deadline that freed
1
+ // The one background slot for a hand-off: it pushes the branch, so two at
2
+ // once would race. No global deadline on purpose: a deadline that freed
3
3
  // the slot while the work still ran would let a second push overwrite the first.
4
4
 
5
- import type { Shipped } from "./ship"
5
+ import type { Done } from "./git"
6
6
  import type { JobName } from "./tools"
7
7
  import { JOB_NAME } from "./tools"
8
8
 
@@ -13,9 +13,9 @@ export type Jobs = {
13
13
  // running or queued.
14
14
  start(
15
15
  job: JobName,
16
- work: () => Promise<Shipped>,
16
+ work: () => Promise<Done>,
17
17
  isInTurn: boolean,
18
- done: (outcome: Shipped) => Promise<void>,
18
+ done: (outcome: Done) => Promise<void>,
19
19
  ): string
20
20
  flush(): void
21
21
  busy(): JobName | undefined
@@ -25,8 +25,8 @@ export function jobs(): Jobs {
25
25
  let current:
26
26
  | {
27
27
  job: JobName
28
- work: () => Promise<Shipped>
29
- done: (outcome: Shipped) => Promise<void>
28
+ work: () => Promise<Done>
29
+ done: (outcome: Done) => Promise<void>
30
30
  isRunning: boolean
31
31
  }
32
32
  | undefined
@@ -36,7 +36,7 @@ export function jobs(): Jobs {
36
36
  if (!slot || slot.isRunning) return
37
37
  slot.isRunning = true
38
38
  void (async () => {
39
- let outcome: Shipped
39
+ let outcome: Done
40
40
  try {
41
41
  outcome = await slot.work()
42
42
  } catch (err) {
@@ -4,8 +4,8 @@
4
4
  // reads and writes the threads, and gtd only sees the notes.
5
5
 
6
6
  import type { PrFeedback, PrThread } from "../types"
7
- import { pushBranch } from "./ship"
8
- import type { ShipIo, Shipped } from "./ship"
7
+ import { pushBranch } from "./git"
8
+ import type { GitIo, Done } from "./git"
9
9
 
10
10
  export const REVIEW_FILE = ".gtd/REVIEW.md"
11
11
 
@@ -27,9 +27,9 @@ const TAG = /\(thread ([A-Za-z0-9_=-]+)\)\s*$/
27
27
  const LEAD = "Tick exactly one:"
28
28
  const PLACEHOLDER = "_your answer_"
29
29
 
30
- const fail = (text: string): Shipped => ({ ok: false, text })
30
+ const fail = (text: string): Done => ({ ok: false, text })
31
31
 
32
- export async function currentPr(io: ShipIo): Promise<Pr | string> {
32
+ export async function currentPr(io: GitIo): Promise<Pr | string> {
33
33
  const branch = (await io.run(["git", "rev-parse", "--abbrev-ref", "HEAD"])).out.trim()
34
34
  const v = await io.run(["gh", "pr", "view", "--json", "number,url"])
35
35
  if (v.code !== 0) return `gh pr view failed: ${v.err.trim()}`
@@ -69,7 +69,7 @@ type ThreadNode = {
69
69
  comments: { nodes: { author: { login: string } | null; body: string; url: string }[] }
70
70
  }
71
71
 
72
- export async function unresolvedThreads(io: ShipIo, pr: Pr): Promise<Thread[] | string> {
72
+ export async function unresolvedThreads(io: GitIo, pr: Pr): Promise<Thread[] | string> {
73
73
  const r = await io.run([
74
74
  "gh",
75
75
  "api",
@@ -226,7 +226,7 @@ export function missingReasons(review: string) {
226
226
 
227
227
  // The first commit since `since` whose review answers any of `ids`: the gate
228
228
  // that decided them. Undefined while none has landed.
229
- export async function decided(io: ShipIo, since: string, ids: readonly string[]) {
229
+ export async function decided(io: GitIo, since: string, ids: readonly string[]) {
230
230
  // A reset or rebase since the import leaves nothing to find the gate in.
231
231
  if ((await io.run(["git", "merge-base", "--is-ancestor", since, "HEAD"])).code !== 0)
232
232
  return "gone"
@@ -279,11 +279,11 @@ const RESOLVE = `mutation($thread: ID!) {
279
279
  // to each answered thread and resolves it. The code commits after `gate` are
280
280
  // the fixes; a thread that fails is named and left open.
281
281
  export async function reply(
282
- io: ShipIo,
282
+ io: GitIo,
283
283
  host: string,
284
284
  gate: string,
285
285
  decidedAnswers: readonly Answer[],
286
- ): Promise<Shipped & { isPushed: boolean }> {
286
+ ): Promise<Done & { isPushed: boolean }> {
287
287
  const branch = (await io.run(["git", "rev-parse", "--abbrev-ref", "HEAD"])).out.trim()
288
288
  const pushed = await pushBranch(io, branch)
289
289
  if (pushed) return { ...fail(pushed), isPushed: false }