@pmelab/gtd 29.2.3 → 29.2.4
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/.claude-plugin/plugin.json +1 -1
- package/README.md +10 -9
- package/claude/hooks/ship.ts +56 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -341,15 +341,16 @@ wait for the next rest. When the process finishes, Claude offers to squash it
|
|
|
341
341
|
and open its pull request(s) (or run `/gtd ship`; `GTD_SHIP_MODE` picks one
|
|
342
342
|
stacked pull request per slice or one commit per slice, see the
|
|
343
343
|
[driver doc](docs/driver.md)); a process started for an issue ships with
|
|
344
|
-
`Closes #N` on its top pull request;
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
`/gtd
|
|
344
|
+
`Closes #N` on its top pull request; a closed pull request whose branch was
|
|
345
|
+
deleted from origin is left behind and a new one opens; re-running `/gtd ship`
|
|
346
|
+
after a lower stacked pull request merged restacks the rest (it refuses, with
|
|
347
|
+
nothing changed, when that pull request merged anywhere but the default branch
|
|
348
|
+
(unless into a lower slice branch before that one merged into it), a slice
|
|
349
|
+
branch is checked out in another worktree, an unrelated local branch sits inside
|
|
350
|
+
the rebased commits, or a slice's change is already upstream; a failed rebase is
|
|
351
|
+
rolled back). A process can change hands at any gate: ask Claude to hand it off
|
|
352
|
+
(or run `/gtd throw @dev`) and it opens a draft pull request assigned to them,
|
|
353
|
+
and `/gtd catch <pr>` picks it up exactly where it waits, so whoever wrote the
|
|
353
354
|
requirements can hand the architecture to someone else. Ship, hand-off and throw
|
|
354
355
|
answer at once and run in the background, with progress on the status line; they
|
|
355
356
|
report by waking Claude, a toast, a transcript line and a push notification, and
|
package/claude/hooks/ship.ts
CHANGED
|
@@ -633,7 +633,13 @@ export const NO_PR_FOUND = "no pull requests found"
|
|
|
633
633
|
|
|
634
634
|
// `gh pr view <branch>`: only NO_PR_FOUND reads as none; any
|
|
635
635
|
// other failure (auth, network) must refuse, or ship would open a duplicate PR.
|
|
636
|
-
|
|
636
|
+
// A closed PR whose branch origin lost is an abandoned earlier ship and reads as
|
|
637
|
+
// none; `abandoned` is recorded before the push so a retry, with origin holding
|
|
638
|
+
// the branch again, still reads it as none. `gone`: a tracking ref is stale.
|
|
639
|
+
async function readPr(
|
|
640
|
+
io: ShipIo,
|
|
641
|
+
branch: string,
|
|
642
|
+
): Promise<{ view?: View; gone?: boolean; abandoned?: number } | string> {
|
|
637
643
|
const v = await io.run([
|
|
638
644
|
"gh",
|
|
639
645
|
"pr",
|
|
@@ -643,14 +649,41 @@ async function viewPr(io: ShipIo, branch: string): Promise<View | undefined | st
|
|
|
643
649
|
"number,title,body,url,state,baseRefName,mergedAt",
|
|
644
650
|
])
|
|
645
651
|
if (v.code !== 0) {
|
|
646
|
-
if (v.err.includes(NO_PR_FOUND)) return
|
|
652
|
+
if (v.err.includes(NO_PR_FOUND)) return {}
|
|
647
653
|
return `gh pr view ${branch} failed: ${v.err.trim()}`
|
|
648
654
|
}
|
|
655
|
+
let view: View
|
|
649
656
|
try {
|
|
650
|
-
|
|
657
|
+
view = JSON.parse(v.out) as View
|
|
651
658
|
} catch {
|
|
652
659
|
return `gh pr view ${branch} returned something unreadable.`
|
|
653
660
|
}
|
|
661
|
+
if (view.state !== "CLOSED") return { view }
|
|
662
|
+
const key = await io.run(["git", "config", "--local", "--get", ABANDONED_KEY(branch)])
|
|
663
|
+
const recorded = key.out.trim() === String(view.number)
|
|
664
|
+
const ls = await io.run(["git", "ls-remote", "origin", `refs/heads/${branch}`])
|
|
665
|
+
const gone = ls.code === 0 && !ls.out.trim()
|
|
666
|
+
if (recorded) return { gone }
|
|
667
|
+
return gone ? { gone, abandoned: view.number } : { view }
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const ABANDONED_KEY = (branch: string) => `branch.${branch}.gtdAbandonedPr`
|
|
671
|
+
|
|
672
|
+
// Once the new PR opens, gh pr view finds it: the record has served its purpose.
|
|
673
|
+
async function settled<T extends Shipped>(io: ShipIo, branch: string, done: T) {
|
|
674
|
+
if (done.ok) await io.run(["git", "config", "--local", "--unset", ABANDONED_KEY(branch)])
|
|
675
|
+
return done
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
async function abandon(io: ShipIo, branch: string, pr: number | undefined) {
|
|
679
|
+
if (pr === undefined) return undefined
|
|
680
|
+
const set = await io.run(["git", "config", "--local", ABANDONED_KEY(branch), String(pr)])
|
|
681
|
+
return set.code === 0 ? undefined : `git config failed.\n${set.err}`.trimEnd()
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
async function viewPr(io: ShipIo, branch: string): Promise<View | undefined | string> {
|
|
685
|
+
const read = await readPr(io, branch)
|
|
686
|
+
return typeof read === "string" ? read : read.view
|
|
654
687
|
}
|
|
655
688
|
|
|
656
689
|
// `dead` names belong to a retired record gtd built itself: not a collision.
|
|
@@ -1512,10 +1545,13 @@ async function shipLink(
|
|
|
1512
1545
|
const l = links[i]!
|
|
1513
1546
|
const lower = i ? links[i - 1]! : undefined
|
|
1514
1547
|
const base = baseOf(links, i, baseBranch)
|
|
1515
|
-
const
|
|
1516
|
-
if (typeof
|
|
1517
|
-
return { step: "pull request", why: `${
|
|
1518
|
-
const
|
|
1548
|
+
const read = await readPr(io, l.branch)
|
|
1549
|
+
if (typeof read === "string")
|
|
1550
|
+
return { step: "pull request", why: `${read} ${l.branch} was not pushed; /gtd ship retries.` }
|
|
1551
|
+
const { view, gone } = read
|
|
1552
|
+
const marked = await abandon(io, l.branch, read.abandoned)
|
|
1553
|
+
if (marked) return { step: "push", why: marked }
|
|
1554
|
+
const pushed = await pushLink(io, git, { ...l, fresh: l.fresh || gone }, i < links.length - 1)
|
|
1519
1555
|
if (pushed) return { step: "push", why: pushed }
|
|
1520
1556
|
const lowerRef = lower?.branch ?? baseRef
|
|
1521
1557
|
const ctx: Ctx = {
|
|
@@ -1528,6 +1564,7 @@ async function shipLink(
|
|
|
1528
1564
|
}
|
|
1529
1565
|
const done = await sliceStep(io, git, ok, l, view, base, lowerRef, ctx)
|
|
1530
1566
|
if ("step" in done) return done
|
|
1567
|
+
if (!view) await settled(io, l.branch, done)
|
|
1531
1568
|
if (done.number === undefined && i < links.length - 1)
|
|
1532
1569
|
return { step: "pull request", why: "its number could not be read from gh's reply." }
|
|
1533
1570
|
return done
|
|
@@ -1764,17 +1801,24 @@ async function pullRequest(
|
|
|
1764
1801
|
if (mergeBase === (await git("rev-parse", "HEAD")))
|
|
1765
1802
|
return fail(`HEAD is an ancestor of ${baseRef}: nothing to describe.`)
|
|
1766
1803
|
|
|
1767
|
-
const
|
|
1768
|
-
if (typeof
|
|
1804
|
+
const read = await readPr(io, branch)
|
|
1805
|
+
if (typeof read === "string")
|
|
1769
1806
|
return fail(
|
|
1770
|
-
`${
|
|
1807
|
+
`${read} ${branch} was not pushed; the squash and history ref stay, /gtd ship retries.`,
|
|
1771
1808
|
)
|
|
1809
|
+
const { view: existing, gone } = read
|
|
1772
1810
|
if (existing && existing.state !== "OPEN")
|
|
1773
1811
|
return fail(`The pull request for ${branch} is ${existing.state}, not open.`)
|
|
1774
|
-
const
|
|
1812
|
+
const marked = await abandon(io, branch, read.abandoned)
|
|
1813
|
+
if (marked) return fail(`${marked} ${branch} was not pushed; /gtd ship retries.`)
|
|
1814
|
+
const pushed = await pushBranch(io, branch, "HEAD", undefined, gone)
|
|
1775
1815
|
if (pushed) return fail(pushed)
|
|
1776
1816
|
if (!existing)
|
|
1777
|
-
return
|
|
1817
|
+
return settled(
|
|
1818
|
+
io,
|
|
1819
|
+
branch,
|
|
1820
|
+
await createPr(io, git, branch, baseBranch, baseRef, { from: mergeBase, to: "HEAD" }),
|
|
1821
|
+
)
|
|
1778
1822
|
// A branch once shipped as a stack keeps the PR aimed at a slice branch; any
|
|
1779
1823
|
// other base is the author's own and stays.
|
|
1780
1824
|
const onSlice = recorded.some((n) => existing.baseRefName === `${branch}-${n}`)
|