@tekyzinc/gsd-t 5.11.26 → 5.11.27

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,40 @@
2
2
 
3
3
  All notable changes to GSD-T are documented here. Updated with each release.
4
4
 
5
+ ## [5.11.27] - 2026-08-11
6
+
7
+ ### Fixed — the scan's volume probe returned a stand-in, and the whole run was built on it
8
+
9
+ HiloAviation, this morning. The probe answered `totals: {"trackedFiles": 5036}`
10
+ and `slices: [{"key": "test", "paths": ["src/"]}]` — one slice for a 4,900-file
11
+ application, keyed "test", and none of the six measurements the prompt asked for.
12
+ The schema accepted it (one slice is legal, `totals` takes any object), so the
13
+ scan read that single slice, produced 3 findings, and headed the register
14
+ "Coverage: FULL".
15
+
16
+ The probe had no retry. Every finder gets re-run when it answers `{"findings":[]}`;
17
+ the probe — which decides what every finder will ever look at — got one call, and
18
+ whatever came back became the plan.
19
+
20
+ The tell is not the slice count; a small repo really is one slice. It is that the
21
+ fields explicitly requested are absent: the shape of an answer with none of the
22
+ work behind it. So the result is now inspected before it is trusted, retried on
23
+ opus with the fault named, and a second stand-in HALTS the scan rather than
24
+ scanning it — a register that under-counts while claiming full coverage is worse
25
+ than no register.
26
+
27
+ - `templates/workflows/gsd-t-scan.workflow.js`: `probePlaceholderFaults()` +
28
+ retry-on-opus + halt; the prompt is now a reusable constant so the retry sends
29
+ the same task; the slice count and totals are logged at probe time, so a thin
30
+ plan is visible before the run rather than inferred afterwards from a thin
31
+ register.
32
+ - `test/m112-probe-placeholder.test.js`: 11 regressions, including the verbatim
33
+ payload that produced the 3-finding scan.
34
+
35
+ The whole-tree tell only fires when the measurements are missing too. A small
36
+ project genuinely is one slice covering `src/`, and halting that scan would be a
37
+ false alarm on every small repo — worse than the bug being fixed.
38
+
5
39
  ## [5.11.26] - 2026-08-11
6
40
 
7
41
  ### Fixed — a quarter of every import edge pointed at a string no file matched
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # GSD-T: Contract-Driven Development for Claude Code
2
2
 
3
- **v5.11.26** - A methodology for reliable, parallelizable development using Claude Code with optional Agent Teams support.
3
+ **v5.11.27** - 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekyzinc/gsd-t",
3
- "version": "5.11.26",
3
+ "version": "5.11.27",
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",
@@ -399,7 +399,7 @@ log(`preflight ok — branch=${pre.branch}, repo=${repoName}, priorRegister=${pr
399
399
 
400
400
  // Volume probe — an agent measures the codebase (its own Bash) and carves slices.
401
401
  phase("Probe");
402
- const probe = await agent(
402
+ const PROBE_PROMPT =
403
403
  [
404
404
  `⛔ TARGET DIRECTORY IS FIXED: you MUST scan ONLY the project at the absolute path \`${projectDir}\`. Before any measurement, \`cd ${projectDir}\` (or pass that exact path to every Read/Grep/Bash). Do NOT scan your current working directory, the GSD-T package, or any other tree — every file you measure and every slice path you emit MUST be under \`${projectDir}\`. If \`${projectDir}\` does not exist or is empty, return a single slice noting that.`,
405
405
  ``,
@@ -437,14 +437,116 @@ const probe = await agent(
437
437
  ``,
438
438
  `Measure with real tooling and report in \`totals\`: files, loc, routes, tables, components, featureDomains (distinct business/feature areas). Read \`${projectDir}/package.json\` for the stack. Return JSON per the schema: totals + slices.`,
439
439
  SHAPE_RULE,
440
- ].join("\n"),
441
- { label: "volume-probe", phase: "Probe", schema: PROBE_SCHEMA, model: "sonnet" }
442
- );
440
+ ].join("\n");
441
+
442
+ // Did the probe MEASURE the codebase, or return something shaped like an answer?
443
+ //
444
+ // [RULE] probe-placeholder-is-rejected-not-scanned
445
+ //
446
+ // HiloAviation, 2026-08-11. The probe returned:
447
+ //
448
+ // totals : {"trackedFiles": 5036}
449
+ // slices : [{"key": "test", "paths": ["src/"]}]
450
+ //
451
+ // One slice for a 4,900-file application, keyed "test", and a totals object
452
+ // carrying none of the six numbers the prompt asked for. The schema accepted it
453
+ // — one slice is legal, and totals takes any object — so the scan ran that one
454
+ // slice, found 3 findings, and the register's header read "Coverage: FULL".
455
+ //
456
+ // The tell is not the slice count. It is that the fields explicitly named in the
457
+ // prompt are simply absent: the agent produced the SHAPE of an answer without
458
+ // doing the work, the same instinct that sends `{"findings":[]}` through a
459
+ // finder. So the probe result is inspected before it is trusted, and a probe
460
+ // that did not measure is retried on a stronger model rather than scanned.
461
+ function probePlaceholderFaults(result) {
462
+ const faults = [];
463
+ const totals = (result && result.totals) || {};
464
+ const slices = (result && Array.isArray(result.slices) && result.slices) || [];
465
+
466
+ // Every number the prompt asks for by name. None present means the agent
467
+ // reported whatever it had to hand instead of measuring.
468
+ const asked = ["files", "loc", "routes", "tables", "components", "featureDomains"];
469
+ const present = asked.filter((k) => totals[k] !== undefined && totals[k] !== null);
470
+ if (present.length === 0) {
471
+ faults.push(
472
+ `totals contains none of the six requested measurements (${asked.join(", ")}) — it holds ` +
473
+ `${JSON.stringify(totals).slice(0, 160)}`
474
+ );
475
+ }
476
+
477
+ // A single slice whose path is the whole source tree has decomposed nothing —
478
+ // but ONLY when the totals are missing too. A small repo genuinely is one
479
+ // slice covering src/, and halting that scan would be a false alarm on every
480
+ // small project. What separates the two is whether the agent measured: a real
481
+ // probe reports the numbers AND explains the single slice. Both tells together
482
+ // are one judgment, not two independent ones.
483
+ const WHOLE_TREE = new Set([".", "./", "*", "src", "src/", "./src", "./src/", projectDir]);
484
+ if (slices.length === 1 && present.length === 0) {
485
+ const paths = Array.isArray(slices[0].paths) ? slices[0].paths : [];
486
+ if (paths.length && paths.every((p) => WHOLE_TREE.has(String(p).trim().replace(/\/+$/, "/")) || WHOLE_TREE.has(String(p).trim()))) {
487
+ faults.push(
488
+ `the one slice "${slices[0].key}" owns the entire source tree (${JSON.stringify(paths)}) — ` +
489
+ `that is not a decomposition`
490
+ );
491
+ }
492
+ }
493
+
494
+ return faults;
495
+ }
496
+
497
+ let probe = await agent(PROBE_PROMPT, {
498
+ label: "volume-probe", phase: "Probe", schema: PROBE_SCHEMA, model: "sonnet",
499
+ });
500
+
501
+ let probeFaults = probePlaceholderFaults(probe);
502
+ if (probeFaults.length) {
503
+ log(`⚠ PROBE DID NOT MEASURE — the answer has the right shape but not the work behind it:`);
504
+ for (const f of probeFaults) log(` · ${f}`);
505
+ log(` retrying on opus with the fault named.`);
506
+
507
+ const retry = await agent(
508
+ [
509
+ PROBE_PROMPT,
510
+ ``,
511
+ `!! A PREVIOUS ATTEMPT AT THIS EXACT TASK WAS REJECTED. What was wrong with it:`,
512
+ ...probeFaults.map((f) => ` · ${f}`),
513
+ ``,
514
+ `RUN THE MEASUREMENTS. Do not answer from a guess, and do not return a minimal stand-in to see whether it is accepted — it will not be.`,
515
+ `Count the files. Count the lines. Find the routes, the database tables, the components, and the distinct business areas. Those NUMBERS go in \`totals\`.`,
516
+ `Then carve the codebase into slices by business capability, one slice per real area of the product. A single slice owning the whole tree is not an answer.`,
517
+ ].join("\n"),
518
+ { label: "volume-probe (retry on opus)", phase: "Probe", schema: PROBE_SCHEMA, model: "opus" }
519
+ );
520
+
521
+ const retryFaults = probePlaceholderFaults(retry);
522
+ if (retry && Array.isArray(retry.slices) && retry.slices.length && retryFaults.length === 0) {
523
+ log(`✓ probe recovered on opus — ${retry.slices.length} slice(s), totals=${JSON.stringify(retry.totals)}`);
524
+ probe = retry;
525
+ } else {
526
+ // Both attempts produced a stand-in. Scanning it would read a fraction of the
527
+ // codebase and print "Coverage: FULL" over the result — a register that lies
528
+ // is worse than no register.
529
+ log(`✗ PROBE FAILED — two attempts, neither measured the codebase.`);
530
+ for (const f of (retryFaults.length ? retryFaults : probeFaults)) log(` · ${f}`);
531
+ return {
532
+ status: "failed",
533
+ reason: "probe-placeholder",
534
+ message:
535
+ "The probe never measured the codebase, on either attempt. Nothing was scanned: a run on this " +
536
+ "answer would have covered a fraction of the project while reporting full coverage.",
537
+ probe: retry || probe,
538
+ };
539
+ }
540
+ }
541
+
443
542
  const rawSlices = (probe && Array.isArray(probe.slices) && probe.slices) || [];
444
543
  if (!rawSlices.length) {
445
544
  log("probe returned no slices — halting");
446
545
  return { status: "failed", reason: "no-slices", probe };
447
546
  }
547
+ // What the plan actually is, before anything runs on it. A thin plan was only
548
+ // ever visible afterwards, in a thin register.
549
+ log(`probe: ${rawSlices.length} slice(s) — totals=${JSON.stringify(probe.totals || {})}`);
448
550
  // Did the probe slice the way it was told? A layer prefix is the tell.
449
551
  //
450
552
  // The instruction alone is not enough — a prompt is advice, and this axis flipped