@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 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.10** - A methodology for reliable, parallelizable development using Claude Code with optional Agent Teams support.
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
- // Asking for the repo's own default branch means "work here, in the main
106
- // checkout" not "make a worktree called main", which git refuses anyway
107
- // because that branch is already checked out. Rare, but it is a real
108
- // choice, and the only way to express it is to name it.
109
- if (isDefaultBranch(cwd, branchNameFrom(wanted))) {
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 ${branchNameFrom(wanted)} — ` +
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
- * Is this the repo's own default branch — the one the main checkout sits on?
144
+ * Does this name refer to the main checkout — the folder the session started in?
142
145
  *
143
- * Asked of git rather than matched against a list: a repo may use `master`,
144
- * `trunk` or anything else, and a hardcoded list would send those repos into a
145
- * worktree named after their own main branch. The branch currently checked out
146
- * in the main tree IS the answer, since that is the thing being opted into.
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
- * A repo git cannot answer for is not the default-branch case it falls
149
- * through to the ordinary worktree path, which fails loudly on its own if git
150
- * is genuinely broken.
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 isDefaultBranch(repo, name) {
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
- if (r.status !== 0) return false;
157
- // Both sides lowercased: a branch name typed at a prompt is a value the user
158
- // types, and "Main" must mean main.
159
- return String(r.stdout || "").trim().toLowerCase() === String(name).toLowerCase();
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
- const r = spawnSync("git", ["worktree", "add", dest, "-b", branch], {
257
- cwd: repo, encoding: "utf8",
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. Only now propagate.
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.10",
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.