@tekyzinc/gsd-t 5.17.14 → 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,44 @@
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
+
22
+ ## [5.18.10] - 2026-09-03
23
+
24
+ ### Added — M115 Test-Plan-First Requirements Interrogation (`/gsd-t-test-plan`)
25
+
26
+ Before any code exists, enumerate every test case a milestone's requirements imply into a
27
+ reviewable sequence-table document. Every row nobody can fill in is a requirements gap.
28
+
29
+ - `commands/gsd-t-test-plan.md` + `/gsd` router case: the front door (in-session, judgement-driven).
30
+ - `templates/prompts/test-plan-enumerator-subagent.md`: the eight enumeration rules (E1-E8), generic
31
+ worked examples; `templates/TestPlan-spec.md`: the mold; `templates/prompts/test-plan-evidence-classifier.md`:
32
+ the `--after` classifier (three arms, no default).
33
+ - `bin/gsd-t-testplan-rows.cjs`: the ONE plan reader (both fence styles, exact six-cell rows, one classifier).
34
+ - `bin/gsd-t-testplan-lint.cjs` (0/4/64), `bin/gsd-t-testplan-halt.cjs` (three-round cap + repeated-symptom
35
+ cap over the loop ledger, per plan and per round), `bin/gsd-t-traceability-gate.cjs` (additive `**Plan-Row**`
36
+ binding; GAP / malformed / duplicate / escaped rows never clear).
37
+ - Verify-gate wiring with a NAMED skip when no plan exists; discovery failure halts.
38
+ - Contract `.gsd-t/contracts/test-plan-first-contract.md` 1.1.0 STABLE; `integration-points.md`.
39
+ - Held-out fixture `test/fixtures/m115-blind-replay/` (the TimeTracking rate-ledger answer key) and the
40
+ clean-room replay artifacts under `.gsd-t/scan/`.
41
+ - 120 milestone tests, of which 29 pin verify findings across seven verify runs.
42
+
5
43
  ## [5.17.14] - 2026-09-03
6
44
 
7
45
  ### Fixed — two more gate defects surfaced by TimeTracking (TD-395 round 2), plus a silent library and a crashing finalizer
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # GSD-T: Contract-Driven Development for Claude Code
2
2
 
3
- **v5.17.14** - 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.
@@ -197,6 +197,7 @@ This will replace changed command files, back up your CLAUDE.md if customized, a
197
197
  | `/gsd-t-milestone` | Define new milestone | Manual |
198
198
  | `/gsd-t-partition` | Decompose into domains + contracts | In wave |
199
199
  | `/gsd-t-plan` | Create atomic task lists per domain (tasks auto-split to fit one context window) | In wave |
200
+ | `/gsd-t-test-plan` | Enumerate every test case a requirements doc implies, before any code or tests exist (`--after` re-enumerates against built code, classifying failures by cited evidence) | Manual |
200
201
  | `/gsd-t-impact` | Analyze downstream effects | In wave |
201
202
  | `/gsd-t-architect` | Interviews you first (shows its read of current behavior for confirmation, researches when unsure — max 3 cycles), then runs the Architect's Oversight Six-Stage Pass — simplest solution + reuse + traps, as plain-English pseudocode (plan-only; `--build` to auto-build, `--no-interview`/`--no-research` to skip a stage) | Manual |
202
203
  | `/gsd-t-execute` | Run tasks — task-level fresh dispatch, worktree isolation, adaptive replanning | In wave |
@@ -167,8 +167,11 @@ function evaluate(consumers) {
167
167
  if (c.wiringModes.length === 0) continue; // never declared; nothing claimed, nothing to prove
168
168
 
169
169
  const claimedWired = c.wiringModes.some((m) => m.toLowerCase() === 'wired');
170
- const since = c.firstWiringTs || '';
171
- const attributed = unlabelledQueryTs.filter((ts) => ts && ts >= since).length;
170
+ // A claim with no usable timestamp cannot be placed in time, so nothing
171
+ // can be attributed to it the one `|| ''` here made EVERY query evidence,
172
+ // queries before the claim included (code-review M115 run 7).
173
+ const since = c.firstWiringTs;
174
+ const attributed = since ? unlabelledQueryTs.filter((ts) => ts && ts >= since).length : 0;
172
175
  const evidenceCount = c.queryCount + attributed;
173
176
  checked.push({ consumer: id, wiringModes: c.wiringModes, queryCount: c.queryCount, attributedQueryCount: attributed });
174
177
 
@@ -440,10 +440,10 @@ function _readManifest(projectDir) {
440
440
  if (!fs.existsSync(p)) return { present: false, manifest: null, error: null };
441
441
  try {
442
442
  const m = JSON.parse(fs.readFileSync(p, 'utf8'));
443
- if (!m || typeof m !== 'object' || Array.isArray(m)) return { present: true, manifest: null, error: 'manifest is not a JSON object' };
443
+ if (!m || typeof m !== 'object' || Array.isArray(m)) return { present: true, manifest: null, error: '.gsd-t/logging-manifest.json is not a JSON object' };
444
444
  return { present: true, manifest: m, error: null };
445
445
  } catch (err) {
446
- return { present: true, manifest: null, error: 'manifest is not valid JSON: ' + (err && err.message ? err.message : String(err)) };
446
+ return { ok: false, present: true, manifest: null, error: '.gsd-t/logging-manifest.json is not valid JSON: ' + (err && err.message ? err.message : String(err)) };
447
447
  }
448
448
  }
449
449
 
@@ -455,8 +455,16 @@ function _declaredStream(projectDir, manifest, stream, failures, notes) {
455
455
  failures.push({ rule: 'logging-manifest-invalid', stream, detail: 'manifest.' + stream + ' must be an object' });
456
456
  return out;
457
457
  }
458
+ // A declared path must stay inside the project (same containment the plan-row
459
+ // loader enforces — review M115 run 7).
460
+ const inside = (rel) => {
461
+ if (typeof rel !== 'string' || !rel) return null;
462
+ const root = path.resolve(projectDir);
463
+ const abs = path.resolve(root, rel);
464
+ return abs.startsWith(root + path.sep) ? abs : null;
465
+ };
458
466
  if (d.module !== undefined) {
459
- const abs = typeof d.module === 'string' && d.module ? path.join(projectDir, d.module) : null;
467
+ const abs = inside(d.module);
460
468
  if (!abs || !fs.existsSync(abs)) {
461
469
  failures.push({ rule: 'logging-manifest-invalid', stream, detail: 'declared ' + stream + ' module not found: ' + String(d.module) });
462
470
  } else {
@@ -465,8 +473,8 @@ function _declaredStream(projectDir, manifest, stream, failures, notes) {
465
473
  }
466
474
  if (d.store !== undefined) {
467
475
  if (typeof d.store === 'string' && d.store) {
468
- const abs = path.join(projectDir, d.store);
469
- if (!fs.existsSync(abs)) failures.push({ rule: 'logging-manifest-invalid', stream, detail: 'declared ' + stream + ' store not found: ' + d.store });
476
+ const abs = inside(d.store);
477
+ if (!abs || !fs.existsSync(abs)) failures.push({ rule: 'logging-manifest-invalid', stream, detail: 'declared ' + stream + ' store not found (or outside the project): ' + d.store });
470
478
  else out.storePath = abs;
471
479
  } else if (d.store && typeof d.store === 'object' && typeof d.store.kind === 'string' && d.store.kind) {
472
480
  out.externalStore = d.store;
@@ -110,20 +110,9 @@ function writeDistilledSchemaIfAbsent(projectDir, planPath) {
110
110
  * a file that already exists in the target project is left byte-for-byte
111
111
  * untouched (recorded in `skipped`, never in `created`).
112
112
  */
113
- // .gsd-t/logging-manifest.json same reader the envelope checker uses (kept in
114
- // step with bin/gsd-t-logging-envelope-check.cjs _readManifest; a stream is
115
- // "declared" when its `module` is named).
116
- function readManifest(projectDir) {
117
- const p = path.join(projectDir, '.gsd-t', 'logging-manifest.json');
118
- if (!fs.existsSync(p)) return { present: false, manifest: null, error: null };
119
- try {
120
- const m = JSON.parse(fs.readFileSync(p, 'utf8'));
121
- if (!m || typeof m !== 'object' || Array.isArray(m)) return { present: true, manifest: null, error: '.gsd-t/logging-manifest.json is not a JSON object' };
122
- return { present: true, manifest: m, error: null };
123
- } catch (err) {
124
- return { present: true, manifest: null, error: '.gsd-t/logging-manifest.json is not valid JSON: ' + (err && err.message ? err.message : String(err)) };
125
- }
126
- }
113
+ // The manifest reader lives in ONE place — the envelope checker and is reused
114
+ // here (review M115 run 7: two byte-identical copies had already appeared).
115
+ const { _readManifest: readManifest } = require("./gsd-t-logging-envelope-check.cjs");
127
116
 
128
117
  function migrateLogging(projectDir, opts) {
129
118
  opts = opts || {};
@@ -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
  *