@ericrisco/rsc 2.0.2 → 2.0.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/manifest.json +1 -1
- package/package.json +1 -1
- package/skills/worktrees/SKILL.md +6 -2
- package/targets/worktree-reaper.mjs +30 -8
package/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -75,7 +75,11 @@ Run this in order. Each check prevents a class of "lost work" you can't easily u
|
|
|
75
75
|
local HEAD. Stale base = predictable merge pain later. Default: fresh from `origin/<default>`.
|
|
76
76
|
6. **Choose a name** tied to the feature slug — the same `<slug>` the spec and plan use
|
|
77
77
|
(`feat/<slug>`), so the branch, the spec at `02-DOCS/wiki/sdd/specs/<slug>.md`, and the plan at
|
|
78
|
-
`02-DOCS/wiki/sdd/plans/<slug>.md` all line up and are trivially traceable.
|
|
78
|
+
`02-DOCS/wiki/sdd/plans/<slug>.md` all line up and are trivially traceable. A change that is not
|
|
79
|
+
a feature takes the type that fits — `fix/<slug>`, `docs/<slug>`, `chore/<slug>` — from the same
|
|
80
|
+
Conventional Commits vocabulary the commit guard already enforces here. **The prefix is load
|
|
81
|
+
bearing**: the automatic cleanup uses it, with the location, as the two signals that say a
|
|
82
|
+
worktree is ours to remove. A name outside that vocabulary is never swept for you.
|
|
79
83
|
|
|
80
84
|
Only once the tree state is understood and the user's WIP is accounted for do you create anything.
|
|
81
85
|
|
|
@@ -189,7 +193,7 @@ the dial controls verbosity, never whether you check before risking someone's WI
|
|
|
189
193
|
| "There's uncommitted WIP, I'll stash it and switch" | A worktree avoids the stash entirely and can't drop it. Use a worktree; if you must stash, confirm with the user and name the stash. |
|
|
190
194
|
| "I'll branch off local HEAD, fetching is slow" | Stale base = merge conflicts you pay for later. Branch off an up-to-date `origin/<default>` unless the user wants HEAD. |
|
|
191
195
|
| "The worktree's dirty but I'll `remove --force` to clean up" | Force-removing a dirty/unmerged worktree throws away work irreversibly. Resolve or confirm explicitly first; never silent-force. |
|
|
192
|
-
| "I'll name it `wip` / `temp` / `branch2`" | An untraceable name divorces the branch from its spec/plan
|
|
196
|
+
| "I'll name it `wip` / `temp` / `branch2`" | An untraceable name divorces the branch from its spec/plan, and the automatic cleanup will not claim it. Use `<type>/<slug>` — `feat/` for a feature, `fix/`, `docs/`, `chore/` for the rest. |
|
|
193
197
|
| "I'll create the worktree AND start writing code right here" | This skill only isolates. Hand a clean isolated workspace to `implement`; don't blur the two steps. |
|
|
194
198
|
| "Already on a feature branch, I'll make a worktree anyway" | Redundant isolation is just clutter. If the current branch is already isolated and clean, say so and proceed. |
|
|
195
199
|
| "I'll record the worktree path into 02-DOCS so it's tracked" | Isolation is plumbing, not knowledge. No artifact; the branch name traces it. Don't pollute the wiki. |
|
|
@@ -53,8 +53,20 @@ export const REGENERABLE_FILES = ['.DS_Store', 'Thumbs.db', '.coverage'];
|
|
|
53
53
|
// Trunk candidates, most authoritative first. The remote tip beats a local branch that may be stale.
|
|
54
54
|
const TRUNKS = ['origin/main', 'main', 'origin/master', 'master'];
|
|
55
55
|
|
|
56
|
-
// Branch shapes
|
|
57
|
-
|
|
56
|
+
// Branch shapes the catalog's isolation produces.
|
|
57
|
+
//
|
|
58
|
+
// `worktrees` documents `feat/<slug>`, and while SDD was the only lane that was the whole set:
|
|
59
|
+
// every isolated branch was a feature. 2.0.0 made FTD the default and FTD names a branch for what
|
|
60
|
+
// it is — a fix is not a feature — so `fix/`, `docs/` and `chore/` fell outside, scored as half a
|
|
61
|
+
// signal, and were never swept, while `ftd` promises in writing that the cleanup is automatic once
|
|
62
|
+
// the branch lands. Found by using it: the two fixes released on 2026-09-18 both had to be removed
|
|
63
|
+
// by hand. These are the Conventional Commits types, which is what the commit guard already
|
|
64
|
+
// enforces on every commit in a project rsc governs — so the branch shape and the commit shape now
|
|
65
|
+
// come from the same vocabulary instead of two that drifted apart.
|
|
66
|
+
//
|
|
67
|
+
// Widening one signal, never the conjunction: a branch outside this vocabulary is still `ambiguous`
|
|
68
|
+
// wherever it sits, and one of these in a directory rsc does not own is still `ambiguous` too.
|
|
69
|
+
const RSC_BRANCH = /^(?:feat|feature|fix|docs|chore|refactor|test|perf|ci|build|style)\//;
|
|
58
70
|
|
|
59
71
|
function git(cwd, args) {
|
|
60
72
|
// 64 MiB, because node's 1 MiB default kills git mid-write on any worktree with a few thousand
|
|
@@ -182,13 +194,23 @@ export function provenanceOf(root, wt) {
|
|
|
182
194
|
* The reflog is git's own record of the ref moving; when it cannot answer, the answer is no, because
|
|
183
195
|
* accumulating a stale worktree costs a directory and the other mistake costs someone's afternoon.
|
|
184
196
|
*/
|
|
185
|
-
export function hasLandedWork(root, wt
|
|
186
|
-
const tip = git(root, ['rev-parse', `${trunk}^{commit}`]);
|
|
187
|
-
if (tip.ok && wt.head && tip.out === wt.head) return false;
|
|
197
|
+
export function hasLandedWork(root, wt) {
|
|
188
198
|
if (!wt.branch) return false;
|
|
199
|
+
// Ask the reflog FIRST. It is the only record that distinguishes the two states that look
|
|
200
|
+
// identical from the outside: a worktree sitting at the trunk because it was just created there,
|
|
201
|
+
// and one sitting at the trunk because its work landed by fast-forward and took the trunk with
|
|
202
|
+
// it. Until 2.0.3 the tip comparison below ran first and answered "nothing landed" for both, so
|
|
203
|
+
// every branch landed the ordinary FTD way — commit, push, fast-forward — was kept for ever.
|
|
204
|
+
// Found on 2026-09-18 watching this repo refuse to sweep its own branch for the wrong reason.
|
|
189
205
|
const log = git(root, ['reflog', 'show', '--format=%gs', wt.branch]);
|
|
190
|
-
if (
|
|
191
|
-
|
|
206
|
+
if (log.ok && log.out) return log.out.split('\n').some((line) => line.startsWith('commit'));
|
|
207
|
+
// No reflog to ask — a fresh clone, or a branch whose reflog has expired. There is then no
|
|
208
|
+
// evidence this branch ever carried anything, and this whole module fails towards keeping the
|
|
209
|
+
// directory: unproven is not proven. (The tip comparison that used to sit here is deliberately
|
|
210
|
+
// gone rather than kept as a fallback: both of its outcomes are this same answer, and a line
|
|
211
|
+
// that cannot change the result while reading like a guard is the kind of decoration that made
|
|
212
|
+
// the bug above hard to see.)
|
|
213
|
+
return false;
|
|
192
214
|
}
|
|
193
215
|
|
|
194
216
|
export function integrationOf(root, wt, trunk) {
|
|
@@ -282,7 +304,7 @@ export function classifyWorktrees(root) {
|
|
|
282
304
|
|
|
283
305
|
const integration = integrationOf(root, wt, trunk);
|
|
284
306
|
if (integration !== 'integrated') return { ...base, reasons: [integration] };
|
|
285
|
-
if (!hasLandedWork(root, wt
|
|
307
|
+
if (!hasLandedWork(root, wt)) return { ...base, reasons: ['nothing-landed'] };
|
|
286
308
|
|
|
287
309
|
const { dirty, outside, readable } = contentOutsideHistory(wt.path);
|
|
288
310
|
if (!readable) return { ...base, reasons: ['unreadable'] };
|