feat(stack): Add forward-merge carry and an OpenSpec rot sweep#66
Merged
Conversation
Support incremental-forward (root->tip) landing alongside the existing tip->root collapse, and add a scheduled backstop for the OpenSpec debris that incremental-forward can leave on main. carryBackward is the mirror of carryForward: on an incremental forward land, the merged root's body is absorbed into the redirected root (the child GitHub retargets onto the default branch) under a "Built on top of #N" heading, so the PR that finally reaches main still holds the full stack legacy — read from the opposite end. renderCarriedRegion takes a heading argument; re-homed regions keep whatever heading they were first carried under, so provenance is never rewritten. A new carry-backward job drives it on merge-into-default-branch, finding the redirected root by live base or recorded-parent marker. The rot sweep is the direction-agnostic backstop the merge-time archive gate structurally can't provide. Incremental-forward lands a change dir on main unarchived and only archives it on the final slice (invariant I1: main ends clean, transiently in-flight while draining); a stalled or abandoned stack would strand it. The daily job fails on a STALE change (live, no git activity in STALE_DAYS) and immediately on a DONE change (all tasks checked, never archived). Age comes from git history, never mtime, since a fresh checkout stamps mtime to checkout time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SYWV5padNohKwVmWDGgNoe
Contributor
There was a problem hiding this comment.
Pull request overview
Adds infrastructure to support incremental stack landing (root→tip) by carrying merged parent content into the redirected root PR, and introduces a scheduled “OpenSpec rot” sweep on main to catch unarchived/stale OpenSpec change directories.
Changes:
- Add a
carry-backwardworkflow job that, on merge into the default branch, finds redirected-root children and absorbs the merged parent PR body into them. - Extend stack-breadcrumb script with
carryBackwardand associated unit tests to preserve provenance headings and maintain canonical body order. - Add a scheduled
openspec: rotworkflow to fail on stale live changes or complete-but-unarchived OpenSpec change directories onmain.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| .github/workflows/stack-breadcrumb.yml | Adds carry-backward merge handler for incremental forward landing. |
| .github/workflows/openspec-rot.yml | Adds daily scheduled rot check for stale/unarchived OpenSpec changes on main. |
| .github/scripts/stack-breadcrumb.test.cjs | Adds unit tests for carryBackward behavior and ordering/idempotence. |
| .github/scripts/stack-breadcrumb.cjs | Implements carryBackward and extends carried-region rendering to support headings. |
New OpenSpec changes start with an empty tasks.md, which has no unchecked `- [ ]` items — so the previous "live" test (has unchecked tasks, or no tasks.md) classified an abandoned change with an empty or non-checkbox tasks.md as not-live and never flagged it STALE, defeating the backstop for exactly the abandoned-stack case it exists for. Drop the special-cased live variable: anything unarchived that is not DONE is live by definition, so staleness applies to every non-done change. Measure age from the last commit touching ANY file in the change directory rather than tasks.md alone, so real activity on proposal.md or specs counts as "not abandoned". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SYWV5padNohKwVmWDGgNoe
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds incremental-forward (
root → tip) landing alongside the existing tip→root collapse, and a scheduled backstop for the OpenSpec debris that incremental-forward can leave onmain. Requested by the taskless/taskless team, who want to land stacks slice-by-slice rather than only as an atomic collapse.Forward-merge carry
carryBackwardis the mirror ofcarryForward. When a stack lands incrementally, the bottom PR merges to the default branch first and the next PR up is retargeted onto it, becoming the redirected root. That surviving child absorbs the parent it was built on as# Built on top of #N, where the down-merge absorbs a child as# Contains #N. Either way the PR that finally reachesmainholds the full stack legacy — read from the opposite end.renderCarriedRegiontakes aheadingargument (defaultContains). A region's heading records how a PR joined the body and is fixed at authoring time, so a region re-homed by a later merge in the other direction keeps its original heading — provenance is never rewritten.carry-backwardjob drives it on merge-into-default-branch. It finds the redirected root by live base (base.ref === merged.head.ref) or, once GitHub has auto-retargeted the child onto the default branch after branch deletion, by the breadcrumb marker recording the merged PR as its parent. A branching stack fans out — the parent is absorbed into every direct child. A merge with no such child (standalone PR, or the genuine last slice) is a clean no-op.OpenSpec rot sweep
The direction-agnostic backstop the merge-time archive gate structurally can't provide. Incremental-forward lands a change directory on
mainunarchived and only archives it on the final slice — invariant I1:mainends clean, but transiently carries an in-flight change while the stack drains. A stalled or abandoned stack would strand that change indefinitely. The daily sweep ofmainfails on:- [ ], or notasks.md) with no git activity inSTALE_DAYS(7, tunable). A stalled/abandoned stack rotting on main.- [x]but never archived. Debris regardless of age → fails immediately. Defence in depth for an admin-merge or botched down-merge that slipped past the tip gate.Age is read from
git log(withfetch-depth: 0), never file mtime — a freshactions/checkoutstamps every file's mtime to checkout time. The job is namedopenspec: rot, deliberately notstack:-prefixed, so the breadcrumb reconcile's "trueing" logic won't re-run it. Cron is15:17 UTC(early Pacific business day; fixed-UTC can't follow DST).Notes
carryBackwardhas unit coverage (heading, re-home-keeps-heading, idempotence, canonical order); full suite is 66/66. Rot detection was dry-run against fixtures (stale-live + no-tasks → STALE, done → DONE, fresh-live → OK,archive/skipped)..github/infra and doesn't touch the published@taskless/cli. Applyingskip-changesetso the require-changeset gate passes.