@tekyzinc/gsd-t 5.18.10 → 5.18.11
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/CHANGELOG.md +17 -0
- package/README.md +1 -1
- package/bin/gsd-t-pick-worktree.cjs +58 -23
- package/commands/cpua.md +9 -1
- package/package.json +1 -1
- package/templates/CLAUDE-global.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GSD-T are documented here. Updated with each release.
|
|
4
4
|
|
|
5
|
+
## [5.18.11] - 2026-09-07
|
|
6
|
+
|
|
7
|
+
### Fixed — `gsd-t pick-worktree --name main` refused when the main checkout sat on a feature branch
|
|
8
|
+
|
|
9
|
+
The launcher prompt offers `"main" = work in main`, but the picker only treated a name as
|
|
10
|
+
"stay here" when it equalled the branch currently checked out. A main checkout left on
|
|
11
|
+
`feat/m26-file-mgmt` (NiceNote) turned `main` into `git worktree add -b main`, which git refused:
|
|
12
|
+
`a branch named 'main' already exists`. The person typing `main` means the FOLDER, not the branch.
|
|
13
|
+
|
|
14
|
+
- `bin/gsd-t-pick-worktree.cjs`: a typed name means the main checkout when it equals EITHER the branch
|
|
15
|
+
checked out there OR the repo's default branch as the remote declares it (`origin/HEAD` → `main`);
|
|
16
|
+
nothing is inferred from the name, so a `trunk` repo with no remote still treats `main` as a new
|
|
17
|
+
branch. The stderr notice names the branch actually checked out.
|
|
18
|
+
- Same class, other half: naming an EXISTING branch that is checked out nowhere now checks it out
|
|
19
|
+
into a worktree (`git worktree add <dest> <branch>`) instead of asking git to create it again.
|
|
20
|
+
- 2 regression tests in `test/m111-pick-worktree.test.js`.
|
|
21
|
+
|
|
5
22
|
## [5.18.10] - 2026-09-03
|
|
6
23
|
|
|
7
24
|
### Added — M115 Test-Plan-First Requirements Interrogation (`/gsd-t-test-plan`)
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# GSD-T: Contract-Driven Development for Claude Code
|
|
2
2
|
|
|
3
|
-
**v5.18.
|
|
3
|
+
**v5.18.11** - A methodology for reliable, parallelizable development using Claude Code with optional Agent Teams support.
|
|
4
4
|
|
|
5
5
|
**Eliminates context rot** — task-level fresh dispatch (one subagent per task, ~10-20% context each) means compaction never triggers.
|
|
6
6
|
**Compaction-proof debug loops** — `gsd-t headless --debug-loop` runs test-fix-retest cycles as separate `claude -p` sessions. A JSONL debug ledger persists all hypothesis/fix/learning history across fresh sessions. Anti-repetition preamble injection prevents retrying failed hypotheses. Escalation tiers (sonnet → opus → human) and a hard iteration ceiling enforced externally.
|
|
@@ -102,17 +102,20 @@ function main() {
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
if (wanted) {
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
|
|
105
|
+
// Naming the main checkout means "work here" — not "make a worktree called
|
|
106
|
+
// main", which git refuses anyway. The main checkout answers to two names:
|
|
107
|
+
// the branch it is sitting on right now, and the repo's default branch
|
|
108
|
+
// (`main` in a repo whose origin/HEAD is main), which is what the launcher
|
|
109
|
+
// prompt offers even when the checkout has wandered onto a feature branch.
|
|
110
|
+
// Rare, but it is a real choice, and the only way to express it is to name it.
|
|
111
|
+
if (meansMainCheckout(cwd, branchNameFrom(wanted))) {
|
|
110
112
|
// Said out loud on stderr: every other path here is silent, but this one
|
|
111
113
|
// is a deliberate choice to work somewhere the house rules steer away
|
|
112
114
|
// from, and silence would read as "the name was ignored". stdout stays
|
|
113
|
-
// empty because the shell reads it as the directory to move to.
|
|
115
|
+
// empty because the shell reads it as the directory to move to. The
|
|
116
|
+
// branch named is the one actually checked out, not the one typed.
|
|
114
117
|
process.stderr.write(
|
|
115
|
-
`[GSD-T WORKTREE] staying in the main checkout on ${
|
|
118
|
+
`[GSD-T WORKTREE] staying in the main checkout on ${currentBranch(cwd) || "(detached)"} — ` +
|
|
116
119
|
`no worktree created.\n`
|
|
117
120
|
);
|
|
118
121
|
stay();
|
|
@@ -138,25 +141,45 @@ function main() {
|
|
|
138
141
|
}
|
|
139
142
|
|
|
140
143
|
/**
|
|
141
|
-
*
|
|
144
|
+
* Does this name refer to the main checkout — the folder the session started in?
|
|
142
145
|
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
146
|
+
* Two names do: the branch checked out there right now, and the repo's default
|
|
147
|
+
* branch. The second matters because the main checkout is often left on a
|
|
148
|
+
* feature branch, and the person typing "main" at the launcher prompt means the
|
|
149
|
+
* FOLDER, not the branch — a worktree on `main` is exactly what git refuses.
|
|
147
150
|
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
+
* The default is asked of git (origin/HEAD), never assumed: a repo on `trunk`
|
|
152
|
+
* with no origin treats `main` as an ordinary new branch name. A repo git
|
|
153
|
+
* cannot answer for falls through to the ordinary worktree path, which fails
|
|
154
|
+
* loudly on its own if git is genuinely broken.
|
|
151
155
|
*/
|
|
152
|
-
function
|
|
156
|
+
function meansMainCheckout(repo, name) {
|
|
157
|
+
const typed = String(name).toLowerCase();
|
|
158
|
+
// Both sides lowercased: a branch name typed at a prompt is a value the user
|
|
159
|
+
// types, and "Main" must mean main.
|
|
160
|
+
const current = currentBranch(repo);
|
|
161
|
+
if (current && current.toLowerCase() === typed) return true;
|
|
162
|
+
const def = defaultBranch(repo);
|
|
163
|
+
return Boolean(def) && def.toLowerCase() === typed;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// The branch the main checkout sits on; "" when detached or git cannot say.
|
|
167
|
+
function currentBranch(repo) {
|
|
153
168
|
const r = spawnSync("git", ["branch", "--show-current"], {
|
|
154
169
|
cwd: repo, encoding: "utf8", timeout: 10000,
|
|
155
170
|
});
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
171
|
+
return r.status === 0 ? String(r.stdout || "").trim() : "";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// The repo's default branch as the remote declares it (origin/HEAD → "main"),
|
|
175
|
+
// or null when there is no remote to ask. Nothing is inferred from the name.
|
|
176
|
+
function defaultBranch(repo) {
|
|
177
|
+
const r = spawnSync("git", ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], {
|
|
178
|
+
cwd: repo, encoding: "utf8", timeout: 10000,
|
|
179
|
+
});
|
|
180
|
+
if (r.status !== 0) return null;
|
|
181
|
+
const ref = String(r.stdout || "").trim();
|
|
182
|
+
return ref.startsWith("origin/") ? ref.slice("origin/".length) : null;
|
|
160
183
|
}
|
|
161
184
|
|
|
162
185
|
// Turn what the user typed into a name git will accept, without silently
|
|
@@ -253,9 +276,13 @@ function enterOrCreate(repo, home, branch) {
|
|
|
253
276
|
|
|
254
277
|
fs.mkdirSync(home, { recursive: true });
|
|
255
278
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
279
|
+
// A branch that already exists (a feature branch from last week, nobody in
|
|
280
|
+
// it) is checked out as it is; `-b` would ask git to create it again, and git
|
|
281
|
+
// refuses. Only a name git has never seen becomes a new branch.
|
|
282
|
+
const args = localBranchExists(repo, branch)
|
|
283
|
+
? ["worktree", "add", dest, branch]
|
|
284
|
+
: ["worktree", "add", dest, "-b", branch];
|
|
285
|
+
const r = spawnSync("git", args, { cwd: repo, encoding: "utf8" });
|
|
259
286
|
|
|
260
287
|
// git writes progress to stderr even when it succeeds, so the exit code and
|
|
261
288
|
// the directory existing are what actually prove it worked.
|
|
@@ -272,6 +299,14 @@ function enterOrCreate(repo, home, branch) {
|
|
|
272
299
|
return { path: dest };
|
|
273
300
|
}
|
|
274
301
|
|
|
302
|
+
// Is there a local branch by this exact name? Asked of git's refs, not the
|
|
303
|
+
// worktree list, so a branch checked out nowhere still counts.
|
|
304
|
+
function localBranchExists(repo, branch) {
|
|
305
|
+
return spawnSync("git", ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`], {
|
|
306
|
+
cwd: repo, stdio: "pipe", timeout: 10000,
|
|
307
|
+
}).status === 0;
|
|
308
|
+
}
|
|
309
|
+
|
|
275
310
|
/**
|
|
276
311
|
* Does THIS repo know `dest` as its worktree for `branch`?
|
|
277
312
|
*
|
package/commands/cpua.md
CHANGED
|
@@ -133,8 +133,16 @@ if [ "$ON_DISK" != "{NEW_VERSION}" ]; then
|
|
|
133
133
|
ON_DISK=$(node -p "require('$G/package.json').version")
|
|
134
134
|
[ "$ON_DISK" = "{NEW_VERSION}" ] || { echo "HALT: global install still $ON_DISK"; exit 1; }
|
|
135
135
|
fi
|
|
136
|
-
# 3.
|
|
136
|
+
# 3. Refresh the HOME install (commands + ~/.claude/.gsd-t-version) BEFORE propagating.
|
|
137
|
+
# v5.18.10 (2026-09-03): with the version file still on the old number, `update-all`
|
|
138
|
+
# reported all 33 projects "already current", copied nothing, and the global package
|
|
139
|
+
# was found back on the old version afterwards. `gsd-t install` writes the version
|
|
140
|
+
# file and the command files; only then does update-all see the new release.
|
|
141
|
+
gsd-t install 2>&1 | tail -5
|
|
142
|
+
[ "$(cat ~/.claude/.gsd-t-version)" = "{NEW_VERSION}" ] || { echo "HALT: ~/.claude/.gsd-t-version did not advance"; exit 1; }
|
|
143
|
+
# 4. Propagate, then prove the global is STILL the new version and a NEW file reached a project.
|
|
137
144
|
gsd-t update-all 2>&1 | tail -30
|
|
145
|
+
[ "$(node -p "require('$G/package.json').version")" = "{NEW_VERSION}" ] || { echo "HALT: global package reverted during update-all"; exit 1; }
|
|
138
146
|
```
|
|
139
147
|
|
|
140
148
|
Never `npm update -g` (npm rejects it for legacy version strings like `3.19.00`). After `update-all`, prove propagation by `ls`/`grep` of one NEW or CHANGED file in a real registered project — the "copied N tool(s)" line is a report, not proof. Note: `update-all` also overwrites `~/.claude/commands/*.md` from the package, so THIS file's source of truth is `commands/cpua.md` in the GSD-T repo; an edit made only in `~/.claude/commands/` is lost on the next propagation.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tekyzinc/gsd-t",
|
|
3
|
-
"version": "5.18.
|
|
3
|
+
"version": "5.18.11",
|
|
4
4
|
"description": "GSD-T: Contract-Driven Development for Claude Code — 54 slash commands with headless-by-default workflow spawning, unattended supervisor relay with event stream, graph-powered code analysis, real-time agent dashboard, task telemetry, doc-ripple enforcement, backlog management, impact analysis, test sync, milestone archival, and PRD generation",
|
|
5
5
|
"author": "Tekyz, Inc.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -154,7 +154,7 @@ WHEN creating a worktree directly (git worktree add, isolation: "worktree", etc.
|
|
|
154
154
|
|
|
155
155
|
**A worktree is NOT usable until it is provisioned (M112).** `git worktree add` brings only what git TRACKS, so every ignored file stays behind — `.env` and its secrets, local settings, and the installed dependencies. The new folder looks complete and is not: tests fail on a missing module and the app cannot reach its database, both reading as broken code rather than a setup gap. `bin/gsd-t-worktree-provision.cjs` runs automatically inside `gsd-t-pick-worktree` and treats the three kinds of ignored file differently — **CARRY** local config and secrets (`.env*`, `.npmrc`, credentials; permissions preserved, so a 600 secret does not become world-readable), **SKIP** per-session `.gsd-t` state, build output and OS junk (copying another session's briefs/heartbeats is how two sessions come to believe they own the same work), and **INSTALL** dependencies from the worktree's own lockfile rather than copying or symlinking (a symlink makes a lockfile change in one tree silently alter the other). Anything it cannot read or copy is REPORTED, and a failed install HALTS — a worktree that came up short says so instead of looking ready. Creating a worktree by hand skips all of this, so prefer `gsd-t-pick-worktree --name <branch>`.
|
|
156
156
|
|
|
157
|
-
**Naming an existing worktree WALKS YOU INTO IT (M113).** `gsd-t pick-worktree --name <branch>` used to refuse whenever the folder was already there, which refused the ordinary case — your own worktree, from yesterday, nobody in it — and left no way back except quitting the session and starting one by hand. It now ENTERS a worktree git confirms as this repo's, on that branch, with no interactive session in it. Two cases still STOP, because each is a way of landing on somebody's uncommitted work: a directory git does not know as that branch's worktree (a stray folder, or another branch's), and a worktree an interactive session already occupies (the M105 collision). `gsd-t pick-worktree --list` prints one line per worktree as `free|busy<TAB><path>`, so the launcher can show what exists before asking for a name.
|
|
157
|
+
**Naming an existing worktree WALKS YOU INTO IT (M113).** `gsd-t pick-worktree --name <branch>` used to refuse whenever the folder was already there, which refused the ordinary case — your own worktree, from yesterday, nobody in it — and left no way back except quitting the session and starting one by hand. It now ENTERS a worktree git confirms as this repo's, on that branch, with no interactive session in it. Two cases still STOP, because each is a way of landing on somebody's uncommitted work: a directory git does not know as that branch's worktree (a stray folder, or another branch's), and a worktree an interactive session already occupies (the M105 collision). `gsd-t pick-worktree --list` prints one line per worktree as `free|busy<TAB><path>`, so the launcher can show what exists before asking for a name. **Typing the DEFAULT branch (`main`) means the main checkout FOLDER, even when that checkout sits on a feature branch** — the picker asks git for the remote-declared default (`origin/HEAD`) as well as the current branch, and stays put on either; an existing branch checked out nowhere is checked out into a worktree, never re-created.
|
|
158
158
|
|
|
159
159
|
|
|
160
160
|
**The expected-branch rule governs the MAIN checkout only (M112).** A worktree exists precisely to be on its own branch, so `branch-guard` passes there and names the skip; a **detached HEAD in a worktree FAILS**, because commits made with no branch attached are easily lost. The rule is read from the project CLAUDE.md in either shape — a sentence (`Expected branch: main`) or a table row (`| Expected branch | main |`) — and when no rule is declared the check says `NOT CHECKED` rather than returning a bare pass.
|