@pmelab/gtd 29.3.0 → 29.3.1
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 -8
- package/claude/hooks/ship.ts +36 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -341,14 +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; a
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
344
|
+
`Closes #N` on its top pull request; a later process on an already-stacked
|
|
345
|
+
branch, such as review fixes, lands one commit per slice on the top pull request
|
|
346
|
+
and keeps the stack; a closed pull request whose branch was deleted from origin
|
|
347
|
+
is left behind and a new one opens; re-running `/gtd ship` after a lower stacked
|
|
348
|
+
pull request merged restacks the rest (it refuses, with nothing changed, when
|
|
349
|
+
that pull request merged anywhere but the default branch (unless into a lower
|
|
350
|
+
slice branch before that one merged into it), a slice branch is checked out in
|
|
351
|
+
another worktree, an unrelated local branch sits inside the rebased commits, or
|
|
352
|
+
a slice's change is already upstream; a failed rebase is rolled back; to tear a
|
|
353
|
+
stack down from its worktree, delete the top branch with
|
|
352
354
|
`git push origin --delete`, since `gh pr close --delete-branch` cannot). A
|
|
353
355
|
process can change hands at any gate: ask Claude to hand it off (or run
|
|
354
356
|
`/gtd throw @dev`) and it opens a draft pull request assigned to them, and
|
package/claude/hooks/ship.ts
CHANGED
|
@@ -495,8 +495,7 @@ async function squash(
|
|
|
495
495
|
if (!msgs.ok) return msgs
|
|
496
496
|
const { fulls, history } = msgs
|
|
497
497
|
const warn = () => (mode === "commits" && many ? rebaseWarning(io) : undefined)
|
|
498
|
-
if (resume?.links)
|
|
499
|
-
return squashOntoStack(io, git, ok, groups[0]!, fulls[0]!, history, resume, isDry, tip)
|
|
498
|
+
if (resume?.links) return squashOntoStack(io, git, ok, groups, fulls, history, resume, isDry, tip)
|
|
500
499
|
if (isDry)
|
|
501
500
|
return dryRun(
|
|
502
501
|
groups,
|
|
@@ -552,18 +551,21 @@ async function stackReads(
|
|
|
552
551
|
mode: ShipMode,
|
|
553
552
|
): Promise<Resumed | string | undefined> {
|
|
554
553
|
if (mode !== "stack") return undefined
|
|
555
|
-
if (groups.length > 1)
|
|
554
|
+
if (groups.length > 1)
|
|
555
|
+
return await stackPrecheck(io, git, ok, branch, baseBranch, groups, base, isDry)
|
|
556
556
|
return resumable(io, git, ok, branch, baseBranch, isDry, base)
|
|
557
557
|
}
|
|
558
558
|
|
|
559
|
-
//
|
|
560
|
-
// the
|
|
559
|
+
// A process on a recorded stack — a re-ship, or a follow-up such as review fixes —
|
|
560
|
+
// lands its slice commits on the top branch, so they join the top pull request.
|
|
561
|
+
// The record, including the recorded top the missing-branch fallbacks and the
|
|
562
|
+
// restack read, stays.
|
|
561
563
|
async function squashOntoStack(
|
|
562
564
|
io: ShipIo,
|
|
563
565
|
git: Git,
|
|
564
566
|
ok: Ok,
|
|
565
|
-
|
|
566
|
-
|
|
567
|
+
groups: Group[],
|
|
568
|
+
fulls: string[],
|
|
567
569
|
history: string | undefined,
|
|
568
570
|
resume: Resumed,
|
|
569
571
|
isDry: boolean,
|
|
@@ -572,16 +574,16 @@ async function squashOntoStack(
|
|
|
572
574
|
const links = resume.links!
|
|
573
575
|
const top = links.length - 1
|
|
574
576
|
if (isDry) {
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
messages,
|
|
579
|
-
restack: resume.restack,
|
|
580
|
-
}
|
|
577
|
+
// the follow-up's slice names are not stack slices: no [name] headers
|
|
578
|
+
const messages = links.map((_, i) => (i === top ? fulls.join("\n\n") : ""))
|
|
579
|
+
return { ok: true as const, text: "", history, stack: links, messages, restack: resume.restack }
|
|
581
580
|
}
|
|
582
581
|
const pushed = await pushHistory(io, history)
|
|
583
582
|
if (pushed) return fail(pushed)
|
|
584
|
-
const one =
|
|
583
|
+
const one =
|
|
584
|
+
groups.length === 1
|
|
585
|
+
? await commitSquash(io, git, ok, groups[0]!.base, fulls[0]!)
|
|
586
|
+
: await commitChain(io, git, ok, groups, fulls)
|
|
585
587
|
if (!one.ok) return one
|
|
586
588
|
const failed = await resume.revive?.()
|
|
587
589
|
if (failed) return fail(failed)
|
|
@@ -709,24 +711,30 @@ async function takenSlice(git: Git, ok: Ok, branch: string, groups: Group[], dea
|
|
|
709
711
|
return undefined
|
|
710
712
|
}
|
|
711
713
|
|
|
712
|
-
|
|
714
|
+
// A live record takes the process onto its top branch; a dead one (no links)
|
|
715
|
+
// leaves the way open for a fresh stack.
|
|
716
|
+
async function liveRecord(
|
|
713
717
|
io: ShipIo,
|
|
714
718
|
git: Git,
|
|
715
719
|
ok: Ok,
|
|
716
720
|
branch: string,
|
|
717
721
|
baseBranch: string,
|
|
718
722
|
recorded: string[],
|
|
719
|
-
|
|
720
|
-
|
|
723
|
+
base: string,
|
|
724
|
+
isDry: boolean,
|
|
725
|
+
): Promise<Resumed | string> {
|
|
726
|
+
// The staleness test a resume uses, without the restack's origin and worktree
|
|
727
|
+
// reads: a dead record is not a live stack, and its dropped-record line is
|
|
728
|
+
// silenced since a fresh stack replaces it.
|
|
721
729
|
const quiet = { ...io, log: () => {} }
|
|
722
730
|
const found = await resumable(quiet, git, ok, branch, baseBranch, true, "HEAD", false)
|
|
723
731
|
// resumable refuses (a missing branch, a closed slice PR) before it tests the
|
|
724
732
|
// chain; only a record that still chains to HEAD may refuse a fresh stack.
|
|
725
|
-
if (typeof found === "string")
|
|
726
|
-
|
|
727
|
-
if (!found.
|
|
728
|
-
|
|
729
|
-
return
|
|
733
|
+
if (typeof found === "string") return (await chainsToHead(git, ok, branch, recorded)) ? found : {}
|
|
734
|
+
// `forget` with links is a live stack a restack ends, not a dead record.
|
|
735
|
+
if (!found.links) return {}
|
|
736
|
+
// Live: any refusal of the full resume (origin, worktrees) stands.
|
|
737
|
+
return resumable(io, git, ok, branch, baseBranch, isDry, base)
|
|
730
738
|
}
|
|
731
739
|
|
|
732
740
|
async function chainsToHead(git: Git, ok: Ok, branch: string, recorded: string[]) {
|
|
@@ -760,11 +768,14 @@ async function stackPrecheck(
|
|
|
760
768
|
branch: string,
|
|
761
769
|
baseBranch: string,
|
|
762
770
|
groups: Group[],
|
|
763
|
-
|
|
771
|
+
base: string,
|
|
772
|
+
isDry: boolean,
|
|
773
|
+
): Promise<Resumed | string | undefined> {
|
|
764
774
|
const recorded = names(await git("config", "--local", "--get", STACK_KEY(branch)))
|
|
765
775
|
if (recorded.length) {
|
|
766
|
-
const
|
|
767
|
-
|
|
776
|
+
const live = await liveRecord(io, git, ok, branch, baseBranch, recorded, base, isDry)
|
|
777
|
+
// Without links the squash builds a fresh stack, so the collision checks still run.
|
|
778
|
+
if (typeof live === "string" || live?.links) return live
|
|
768
779
|
}
|
|
769
780
|
const retired = names(await git("config", "--local", "--get", RETIRED_KEY(branch)))
|
|
770
781
|
const taken = await takenSlice(git, ok, branch, groups, [...recorded, ...retired])
|