@tekyzinc/gsd-t 5.11.32 → 5.12.10

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,46 @@
2
2
 
3
3
  All notable changes to GSD-T are documented here. Updated with each release.
4
4
 
5
+ ## [5.12.10] - 2026-08-19
6
+
7
+ ### Added — naming an existing worktree walks you into it
8
+
9
+ Naming a worktree that already existed used to be refused outright. That
10
+ refused the ordinary case — your own worktree, from yesterday, with nobody in
11
+ it — and left no way back except quitting the session and starting one by
12
+ hand, which is the exact chore the picker exists to spare you.
13
+
14
+ `--name` now ENTERS a worktree when git confirms it as this repo's, on that
15
+ branch, with no interactive session in it. Two cases still STOP, because each
16
+ is a way of landing on somebody's uncommitted work: a directory git does not
17
+ know as that branch's worktree (a stray folder, or another branch's), and one
18
+ an interactive session already occupies (the M105 collision). The occupancy
19
+ check already existed on the reuse path and was simply never asked on the
20
+ naming path — no new mechanism was added.
21
+
22
+ A path bug surfaced while testing and is fixed: git reports symlink-resolved
23
+ paths while ours were used as typed, so on macOS `/var` vs `/private/var` made
24
+ one folder compare as two. Any repo under a symlinked path would have been
25
+ wrongly refused. An unresolvable path now HALTS rather than answering "not a
26
+ match".
27
+
28
+ - `bin/gsd-t-pick-worktree.cjs`: `create()` becomes `enterOrCreate()`; new
29
+ `isWorktreeOf()` asks the repo's own worktree register (not the directory,
30
+ which a foreign checkout would answer for); new `realPath()` compares
31
+ symlink-resolved paths and halts when one cannot be resolved; new `--list`
32
+ prints `free|busy<TAB><path>` per worktree.
33
+ - `bin/gsd-t.js`: flags pass through unchanged; comment notes `--list`.
34
+ - `templates/CLAUDE-global.md`: states the enter-vs-stop rule.
35
+ - `.gsd-t/pseudocode/PseudoCode-EnterExistingWorktree.md`: source-of-truth
36
+ behaviour map, style gate clean.
37
+ - `test/m111-pick-worktree.test.js`: the old "refuses a directory that already
38
+ exists" test narrows to the two cases that still refuse; adds coverage for
39
+ entering a free worktree, a stray directory, a worktree on another branch,
40
+ and both `--list` shapes.
41
+
42
+ The launcher (`cc()` in `~/.zshrc`) shows the worktree list above the name
43
+ prompt, so an existing one is picked by sight rather than recalled.
44
+
5
45
  ## [5.11.32] - 2026-08-14
6
46
 
7
47
  ### Fixed — a missing graph is now BUILT, and a "wired" claim has to prove itself
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # GSD-T: Contract-Driven Development for Claude Code
2
2
 
3
- **v5.11.32** - A methodology for reliable, parallelizable development using Claude Code with optional Agent Teams support.
3
+ **v5.12.10** - 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.
@@ -12,12 +12,23 @@
12
12
  * d=$(gsd-t-pick-worktree) && [ -n "$d" ] && cd "$d"
13
13
  * claude
14
14
  *
15
- * Three modes:
15
+ * Four modes:
16
16
  *
17
17
  * (no flags) pick a worktree, creating one if none is free
18
18
  * --suggest say what WOULD happen, create nothing. Prints
19
19
  * "reuse:<path>", "create", or nothing at all.
20
- * --name <name> create a worktree on a branch called <name>
20
+ * --name <name> go to the worktree on a branch called <name>, creating
21
+ * it if it isn't there yet
22
+ * --list list this project's worktrees, one per line, as
23
+ * "<free|busy>\t<path>". Creates nothing.
24
+ *
25
+ * --name naming a worktree that already exists ENTERS it. Refusing that was the
26
+ * old behaviour and it refused the ordinary case — your own worktree, from
27
+ * yesterday, with nobody in it — leaving no way back in except quitting and
28
+ * starting a session by hand, which is the thing this script exists to spare
29
+ * you. What the refusal genuinely protected is narrower: another live session
30
+ * sitting in that folder, or a directory git does not know as this branch's
31
+ * worktree. Both still stop.
21
32
  *
22
33
  * --suggest exists so the shell can ask for a name BEFORE anything is created.
23
34
  * A branch named at session start, before the work is known, can only be a
@@ -64,6 +75,7 @@ function fail(message) { // could not decide — say why, change nothing
64
75
  function main() {
65
76
  const argv = process.argv.slice(2);
66
77
  const suggest = argv.includes("--suggest");
78
+ const wantsList = argv.includes("--list");
67
79
  const nameAt = argv.indexOf("--name");
68
80
  const wanted = nameAt >= 0 ? argv[nameAt + 1] : null;
69
81
 
@@ -78,6 +90,17 @@ function main() {
78
90
 
79
91
  const home = path.join(process.env.HOME, "Worktrees", path.basename(cwd));
80
92
 
93
+ // Report what is there so the prompt can show it before a name is typed.
94
+ // Nothing here is a path for the shell to cd into, so each line is labelled;
95
+ // the caller reads this one deliberately rather than as a bare directory.
96
+ if (wantsList) {
97
+ const busy = interactiveClaudeDirs();
98
+ for (const w of worktreesNewestFirst(home)) {
99
+ process.stdout.write(`${busy.has(w.path) ? "busy" : "free"}\t${w.path}\n`);
100
+ }
101
+ return;
102
+ }
103
+
81
104
  if (wanted) {
82
105
  // Asking for the repo's own default branch means "work here, in the main
83
106
  // checkout" — not "make a worktree called main", which git refuses anyway
@@ -94,7 +117,7 @@ function main() {
94
117
  );
95
118
  stay();
96
119
  }
97
- process.stdout.write(create(cwd, home, branchNameFrom(wanted)).path + "\n");
120
+ process.stdout.write(enterOrCreate(cwd, home, branchNameFrom(wanted)).path + "\n");
98
121
  return;
99
122
  }
100
123
 
@@ -202,16 +225,30 @@ function worktreesNewestFirst(home) {
202
225
  // timestamp, and a timestamp describes nothing — which is how this repo
203
226
  // collected twenty `session-2026-08-08T23-10-16` branches. Callers with no name
204
227
  // get "create" from --suggest and must come back with --name.
205
- function create(repo, home, branch) {
228
+ function enterOrCreate(repo, home, branch) {
206
229
  if (!branch) {
207
230
  fail("A worktree needs a branch name — run with --name <name>.");
208
231
  }
209
232
  const dest = path.join(home, branch);
210
233
 
211
- // Reusing a directory that already holds a different branch's work would put
212
- // this session on top of it. Say so instead.
234
+ // Already there: go in, provided it is genuinely this branch's worktree and
235
+ // nobody is working in it. Both conditions are checked before entering, since
236
+ // each is a way of landing on top of somebody's uncommitted work.
213
237
  if (fs.existsSync(dest)) {
214
- fail(`${dest} already exists. Pick a different name, or start there directly.`);
238
+ if (!isWorktreeOf(repo, dest, branch)) {
239
+ fail(
240
+ `${dest} exists but git does not know it as this repo's worktree for ` +
241
+ `"${branch}". Working there would sit on top of whatever is in it — ` +
242
+ `move it aside, or pick a different name.`
243
+ );
244
+ }
245
+ if (interactiveClaudeDirs().has(dest)) {
246
+ fail(
247
+ `Another session is working in ${dest} right now. Two sessions in one ` +
248
+ `folder interleave each other's uncommitted work — pick a different name.`
249
+ );
250
+ }
251
+ return { path: dest };
215
252
  }
216
253
 
217
254
  fs.mkdirSync(home, { recursive: true });
@@ -235,6 +272,60 @@ function create(repo, home, branch) {
235
272
  return { path: dest };
236
273
  }
237
274
 
275
+ /**
276
+ * Does THIS repo know `dest` as its worktree for `branch`?
277
+ *
278
+ * Asked of the main repo rather than of the directory: a folder can be a
279
+ * perfectly valid git checkout of something else entirely, and it would answer
280
+ * "yes, I am a worktree on that branch" while belonging to another project.
281
+ * The repo's own register is the only place that settles ownership.
282
+ *
283
+ * A git that cannot answer is a stop, not a "probably fine" — the unanswered
284
+ * question is precisely whether somebody's work is already there.
285
+ */
286
+ function isWorktreeOf(repo, dest, branch) {
287
+ const r = spawnSync("git", ["worktree", "list", "--porcelain"], {
288
+ cwd: repo, encoding: "utf8", timeout: 10000,
289
+ });
290
+ if (r.status !== 0) {
291
+ fail(
292
+ `Cannot read this repo's worktree list ` +
293
+ `(${String(r.stderr).trim() || "git failed"}), so it can't be confirmed ` +
294
+ `that ${dest} is yours to work in.`
295
+ );
296
+ }
297
+
298
+ // Entries are blank-line separated; the lines that matter are "worktree
299
+ // <path>" and "branch refs/heads/<name>".
300
+ const target = realPath(dest);
301
+ for (const block of String(r.stdout).split(/\n\s*\n/)) {
302
+ const at = block.match(/^worktree (.+)$/m);
303
+ if (!at) continue;
304
+ if (realPath(at[1].trim()) !== target) continue;
305
+ const on = block.match(/^branch refs\/heads\/(.+)$/m);
306
+ return Boolean(on) && on[1].trim() === branch;
307
+ }
308
+ return false;
309
+ }
310
+
311
+ /**
312
+ * The path with every symlink followed, so two spellings of one directory
313
+ * compare equal. git reports resolved paths; ours are as typed, and on macOS
314
+ * /var is a symlink to /private/var — so the same folder arrives under two
315
+ * names and a plain string compare calls them different places.
316
+ *
317
+ * A path that cannot be resolved stops the run. The comparison it feeds decides
318
+ * whether a directory is safe to work in, and an unresolvable path leaves that
319
+ * unanswered rather than answered "no".
320
+ */
321
+ function realPath(p) {
322
+ try {
323
+ return fs.realpathSync(p);
324
+ } catch (e) {
325
+ fail(`Cannot resolve ${p} (${e.message}), so it can't be told apart from another directory.`);
326
+ }
327
+ }
328
+
238
329
  /**
239
330
  * A worktree holds only what git tracks, so the secrets and the installed
240
331
  * dependencies stay behind and the new folder is born unable to run. Carry the
package/bin/gsd-t.js CHANGED
@@ -5658,7 +5658,7 @@ if (require.main === module) {
5658
5658
  case "pick-worktree": {
5659
5659
  const picker = path.join(PKG_ROOT, "bin", "gsd-t-pick-worktree.cjs");
5660
5660
  try {
5661
- // Pass the flags through — --suggest and --name live on the picker.
5661
+ // Pass the flags through — --suggest, --name and --list live on the picker.
5662
5662
  execFileSync(process.execPath, [picker, ...args.slice(1)], { stdio: "inherit" });
5663
5663
  } catch (e) {
5664
5664
  // The picker prints its own reason on stderr and exits non-zero when it
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekyzinc/gsd-t",
3
- "version": "5.11.32",
3
+ "version": "5.12.10",
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,6 +154,9 @@ 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.
158
+
159
+
157
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.
158
161
 
159
162
  # Destructive Action Guard (MANDATORY)