@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 +40 -0
- package/README.md +1 -1
- package/bin/gsd-t-pick-worktree.cjs +98 -7
- package/bin/gsd-t.js +1 -1
- package/package.json +1 -1
- package/templates/CLAUDE-global.md +3 -0
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.
|
|
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
|
-
*
|
|
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>
|
|
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(
|
|
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
|
|
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
|
-
//
|
|
212
|
-
//
|
|
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
|
-
|
|
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 --
|
|
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.
|
|
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)
|