Follow-up from #218. That issue's approved Stage A v5 plan specified a full PR-session lifecycle subsystem; the implementation landed only the plan's identity layer — the typed session key, the canonical codec, and the derived window-name codec — because that is what actually fixes #218's collision. Everything below is the deferred remainder, filed so the design work stays findable rather than evaporating with the lane that scoped down.
Plan of record: plans/forgectl-campaign-019ff8b3-wave5/218-stage-a-v5.md in the approving session's cadence plan directory (session 019ff8b3-b8e7-7cd2-8570-224e1fbabc8b), with the review verdict alongside it in 218-plan-review-v5.md.
Nothing here is a defect. forgectl pr works today; this is the durability and recoverability program the plan designed around the identity layer, and it should be scoped, sequenced, and re-reviewed on its own terms rather than inherited wholesale.
Deferred items
- Transaction lifecycle. The durable phase machine —
intent → preparing:<step> → prepared → creating → window-created → binding → active, plus rolling-back, rolled-back, orphan, and needs-repair — with each phase fsynced before the mutation it authorizes.
- Kernel advisory lock. A single
.pr-session-lifecycle.lock held from first durable intent through preparation, creation, active bind, or completed rollback, so a crash or a concurrent invocation cannot interleave.
- Strict monotonic record writer. One JSON codec for every record kind: duplicate-key and trailing-content rejection, revision compare-and-write under lock, private same-directory temp with exclusive no-follow open, checked full writes, file sync, destination revalidation, atomic rename, directory sync, and verified temp cleanup — with crash tests at each of those boundaries.
- Quarantine move-graph persistence. A read-only strict discovery API returning the complete deterministic From/To graph with identity preconditions, persisted as a monotonic revision before the first rename, then applied one entry at a time through a single-move API with an intent and a receipt around each. Recovery consumes the persisted graph and never rediscovers.
- Dry-run bootstrap contract. A first-run dry-run may create only the private state directory, the permanent lock, and the version header, under exclusive lock, converging across concurrent first runs — and is read-only after that, with the exception disclosed in the CLI and docs.
forgectl pr repair CLI. Inspect-only without --apply; accepts a transaction UUID or a confined breadcrumb plus an explicit --adopt-window @N, --rollback, or --forget-if-absent, and never accepts a name as authority.
- v2 breadcrumb schema with legacy migration. A discriminator-first reader, no fallback from malformed v2 to legacy, old binaries failing closed, and a migration that validates the parent, generation, native id, and name twice before atomic replacement.
PRSessionInstance persistence. Binding the logical key to a persisted tmux.SessionIdentity parent, a validated tmux.WindowIdentity child, and a transaction UUID — replacing today's resolve-by-name-on-use with a persisted, revalidated native identity.
- Eight-attempt shell-nonce machinery for
pr open. A bounded shell-<nonce> name preflighted against live names, retried at most maxShellNameAttempts = 8, refusing before new-window after eight collisions, with an injectable nonce source for the first-collision-then-success and eight-collision-refusal cases.
Two decisions already made, worth carrying forward
The local key holds a short oid, not the full HEAD oid. The plan specified the full oid. The breadcrumb persists only the ref string and a locality flag, and Ref — which carries a 7-char abbreviation in Repo — is also a map key in WindowsLive, so a partially-populated new field would break lookups silently. The codec was built with a declared algorithm tag (short / sha1 / sha256) so widening later needs no format break; item 7 above is the natural place to do it. Marked in internal/pr/sessionkey.go with the upgrade trigger named.
The same-name collision refusal was removed, contingently. #237 made every tmux action target a native id, so a same-name window can no longer receive an action meant for another — the hazard was eliminated upstream. internal/pr/launch.go records that the removal is contingent on #237's targeting and must return if that is ever reverted. Item 1 above should not silently reintroduce it without re-deriving whether it is still needed.
Sizing
This is a multi-PR program, not one change. Items 1–3 are the substrate everything else sits on and should land together or in a tight sequence; 4 depends on 3; 6, 7, and 8 depend on 1–3; 5 and 9 are independent and small.
Follow-up from #218. That issue's approved Stage A v5 plan specified a full PR-session lifecycle subsystem; the implementation landed only the plan's identity layer — the typed session key, the canonical codec, and the derived window-name codec — because that is what actually fixes #218's collision. Everything below is the deferred remainder, filed so the design work stays findable rather than evaporating with the lane that scoped down.
Plan of record:
plans/forgectl-campaign-019ff8b3-wave5/218-stage-a-v5.mdin the approving session's cadence plan directory (session019ff8b3-b8e7-7cd2-8570-224e1fbabc8b), with the review verdict alongside it in218-plan-review-v5.md.Nothing here is a defect.
forgectl prworks today; this is the durability and recoverability program the plan designed around the identity layer, and it should be scoped, sequenced, and re-reviewed on its own terms rather than inherited wholesale.Deferred items
intent→preparing:<step>→prepared→creating→window-created→binding→active, plusrolling-back,rolled-back,orphan, andneeds-repair— with each phase fsynced before the mutation it authorizes..pr-session-lifecycle.lockheld from first durable intent through preparation, creation, active bind, or completed rollback, so a crash or a concurrent invocation cannot interleave.forgectl pr repairCLI. Inspect-only without--apply; accepts a transaction UUID or a confined breadcrumb plus an explicit--adopt-window @N,--rollback, or--forget-if-absent, and never accepts a name as authority.PRSessionInstancepersistence. Binding the logical key to a persistedtmux.SessionIdentityparent, a validatedtmux.WindowIdentitychild, and a transaction UUID — replacing today's resolve-by-name-on-use with a persisted, revalidated native identity.pr open. A boundedshell-<nonce>name preflighted against live names, retried at mostmaxShellNameAttempts = 8, refusing beforenew-windowafter eight collisions, with an injectable nonce source for the first-collision-then-success and eight-collision-refusal cases.Two decisions already made, worth carrying forward
The local key holds a short oid, not the full HEAD oid. The plan specified the full oid. The breadcrumb persists only the ref string and a locality flag, and
Ref— which carries a 7-char abbreviation inRepo— is also a map key inWindowsLive, so a partially-populated new field would break lookups silently. The codec was built with a declared algorithm tag (short/sha1/sha256) so widening later needs no format break; item 7 above is the natural place to do it. Marked ininternal/pr/sessionkey.gowith the upgrade trigger named.The same-name collision refusal was removed, contingently. #237 made every tmux action target a native id, so a same-name window can no longer receive an action meant for another — the hazard was eliminated upstream.
internal/pr/launch.gorecords that the removal is contingent on #237's targeting and must return if that is ever reverted. Item 1 above should not silently reintroduce it without re-deriving whether it is still needed.Sizing
This is a multi-PR program, not one change. Items 1–3 are the substrate everything else sits on and should land together or in a tight sequence; 4 depends on 3; 6, 7, and 8 depend on 1–3; 5 and 9 are independent and small.