@tekyzinc/gsd-t 5.11.17 → 5.11.19

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,55 @@
2
2
 
3
3
  All notable changes to GSD-T are documented here. Updated with each release.
4
4
 
5
+ ## [5.11.19] - 2026-08-10
6
+
7
+ ### Changed — a scan that lost areas now STOPS instead of writing the report
8
+
9
+ It already warned and stamped the register PARTIAL. It kept going anyway, so the
10
+ register got written, read, and acted on. On HiloAviation the two lost areas
11
+ were repositories and data-access — the areas with the most to find. A report
12
+ that under-counts the debt while looking finished is worse than no report.
13
+
14
+ The stop happens **before synthesis and before any document is written**, names
15
+ every area that was missed, and returns a failure rather than a quiet success.
16
+ It also says plainly that a failed area is not a clean one.
17
+
18
+ `allowPartial: true` continues deliberately, says so out loud, and the register
19
+ still carries its PARTIAL banner.
20
+
21
+ - `templates/workflows/gsd-t-scan.workflow.js`: the halt + the `allowPartial` option
22
+ - `test/m112-scan-schema-tolerance.test.js`: 4 more tests — the halt precedes writing, names the areas, fails rather than passes, and cannot be bypassed by a truthy-ish value
23
+
24
+ ## [5.11.18] - 2026-08-10
25
+
26
+ ### Fixed — a scan finding was thrown away over its FORM, not its truth
27
+
28
+ HiloAviation, a 228-slice deep scan: 2 slices failed after 179.6k tokens and 61
29
+ tool calls of real work. The agent's own log shows one call carrying findings
30
+ and the next carrying an empty array — an agent giving up and submitting nothing
31
+ to satisfy the schema. Both refused. The report then read 226 of 228 with no
32
+ sign that two of the densest areas had vanished.
33
+
34
+ Two rules did it, neither of which checks whether a finding is *true*:
35
+
36
+ - **No extra fields.** A finder adding `evidence` or a line number lost the
37
+ entire call, not the extra field.
38
+ - **Case-exact word lists.** "High" refused where "HIGH" was demanded, and
39
+ "High" refused where "high" was demanded — in the same finding.
40
+
41
+ Large projects hit this hardest: the failing slices were data-access and
42
+ repositories, the areas producing the most findings and the most extra context.
43
+
44
+ **Nothing about what a finding must contain is loosened.** Title, severity,
45
+ area, files, detail and recommendation are still required.
46
+
47
+ Widening a word list breaks exact-match comparisons, so three were fixed in the
48
+ same pass — most importantly the false-positive check, which would otherwise
49
+ have KEPT a finding the verifier had just rejected.
50
+
51
+ - `templates/workflows/gsd-t-scan.workflow.js`: agent-authored schemas accept extra keys and any casing; severity normalised once at the merge point; verdict and document-status comparisons made case-insensitive
52
+ - `test/m112-scan-schema-tolerance.test.js`: 8 tests, including that the required-field list was not quietly shortened
53
+
5
54
  ## [5.11.17] - 2026-08-10
6
55
 
7
56
  ### Added — `/concise`, rewrite the last reply short on request
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # GSD-T: Contract-Driven Development for Claude Code
2
2
 
3
- **v5.11.17** - A methodology for reliable, parallelizable development using Claude Code with optional Agent Teams support.
3
+ **v5.11.19** - 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.17",
3
+ "version": "5.11.19",
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",
@@ -56,6 +56,9 @@ const maxSlicesOverride = _args.maxSlicesHint || null; // optional power-user ce
56
56
  // M94-D6: graph wiring — "wired" (default) or "disabled" (no-graph baseline for AC-4).
57
57
  // [RULE] scan-injects-structural-slice / [RULE] no-graph-baseline-proven-graph-free
58
58
  const graphMode = (_args.graphMode === "disabled") ? "disabled" : "wired";
59
+ // A scan that lost areas STOPS rather than writing a register that under-counts
60
+ // the debt while looking finished. Set true to accept an incomplete scan.
61
+ const allowPartial = _args.allowPartial === true;
59
62
 
60
63
  // VOLUME-DERIVED CAP — a RUNAWAY BACKSTOP, not the target count. The probe decides
61
64
  // the actual slice count by cohesive sub-domain WITHIN this cap; the cap only fires
@@ -112,7 +115,7 @@ const PROBE_SCHEMA = {
112
115
  properties: {
113
116
  key: { type: "string" },
114
117
  paths: { type: "array", items: { type: "string" } },
115
- dimension: { type: "string", enum: ["architecture", "business-rules", "security", "quality", "contracts", "feature-domain", "data-layer", "api-surface", "testing"] },
118
+ dimension: { type: "string", enum: ["architecture", "ARCHITECTURE", "Architecture", "business-rules", "BUSINESS-RULES", "Business-rules", "security", "SECURITY", "Security", "quality", "QUALITY", "Quality", "contracts", "CONTRACTS", "Contracts", "feature-domain", "FEATURE-DOMAIN", "Feature-domain", "data-layer", "DATA-LAYER", "Data-layer", "api-surface", "API-SURFACE", "Api-surface", "testing", "TESTING", "Testing"] },
116
119
  why: { type: "string" },
117
120
  },
118
121
  },
@@ -124,7 +127,8 @@ const PROBE_SCHEMA = {
124
127
  const FINDER_SCHEMA = {
125
128
  type: "object",
126
129
  required: ["slice", "findings"],
127
- additionalProperties: false,
130
+ // Agent-authored, so extra keys are allowed here too — see the finding object.
131
+ additionalProperties: true,
128
132
  properties: {
129
133
  slice: { type: "string" },
130
134
  findings: {
@@ -132,16 +136,21 @@ const FINDER_SCHEMA = {
132
136
  items: {
133
137
  type: "object",
134
138
  required: ["title", "severity", "area", "files", "detail", "recommendation"],
135
- additionalProperties: false,
139
+ // Extra keys are ALLOWED. A finder that adds `evidence` or a line number
140
+ // is giving more, not less — and rejecting the whole call for it threw
141
+ // away 179.6k tokens of real findings on two of 228 slices in a large
142
+ // scan (HiloAviation, 2026-08-10). The required keys are still
143
+ // required, so nothing about what a finding must contain is loosened.
144
+ additionalProperties: true,
136
145
  properties: {
137
146
  title: { type: "string" },
138
- severity: { type: "string", enum: ["CRITICAL", "HIGH", "MEDIUM", "LOW"] },
147
+ severity: { type: "string", enum: ["CRITICAL", "critical", "Critical", "HIGH", "high", "High", "MEDIUM", "medium", "Medium", "LOW", "low", "Low"] },
139
148
  area: { type: "string" },
140
149
  files: { type: "array", items: { type: "string" } },
141
150
  detail: { type: "string" },
142
151
  impact: { type: "string" },
143
152
  recommendation: { type: "string" },
144
- confidence: { type: "string", enum: ["high", "medium", "low"] },
153
+ confidence: { type: "string", enum: ["high", "HIGH", "High", "medium", "MEDIUM", "Medium", "low", "LOW", "Low"] },
145
154
  },
146
155
  },
147
156
  },
@@ -152,12 +161,13 @@ const FINDER_SCHEMA = {
152
161
  const VERIFY_SCHEMA = {
153
162
  type: "object",
154
163
  required: ["confirmed", "verdict"],
155
- additionalProperties: false,
164
+ // Agent-authored — extra context must not cost the whole verdict.
165
+ additionalProperties: true,
156
166
  properties: {
157
167
  confirmed: { type: "boolean" },
158
- verdict: { type: "string", enum: ["confirmed", "false-positive", "needs-detail"] },
168
+ verdict: { type: "string", enum: ["confirmed", "CONFIRMED", "Confirmed", "false-positive", "FALSE-POSITIVE", "False-positive", "needs-detail", "NEEDS-DETAIL", "Needs-detail"] },
159
169
  note: { type: "string" },
160
- correctedSeverity: { type: "string", enum: ["CRITICAL", "HIGH", "MEDIUM", "LOW"] },
170
+ correctedSeverity: { type: "string", enum: ["CRITICAL", "critical", "Critical", "HIGH", "high", "High", "MEDIUM", "medium", "Medium", "LOW", "low", "Low"] },
161
171
  },
162
172
  };
163
173
 
@@ -173,7 +183,7 @@ const DOC_RESULT_SCHEMA = {
173
183
  additionalProperties: false,
174
184
  properties: {
175
185
  doc: { type: "string" },
176
- status: { type: "string", enum: ["written", "merged", "skipped", "failed"] },
186
+ status: { type: "string", enum: ["written", "WRITTEN", "Written", "merged", "MERGED", "Merged", "skipped", "SKIPPED", "Skipped", "failed", "FAILED", "Failed"] },
177
187
  path: { type: "string" },
178
188
  notes: { type: "string" },
179
189
  },
@@ -184,7 +194,7 @@ const RENDER_SCHEMA = {
184
194
  required: ["status"],
185
195
  additionalProperties: false,
186
196
  properties: {
187
- status: { type: "string", enum: ["rendered", "skipped", "failed"] },
197
+ status: { type: "string", enum: ["rendered", "RENDERED", "Rendered", "skipped", "SKIPPED", "Skipped", "failed", "FAILED", "Failed"] },
188
198
  outputPath: { type: "string" },
189
199
  notes: { type: "string" },
190
200
  },
@@ -699,8 +709,15 @@ async function scanSlice(slice) {
699
709
  ].join("\n"),
700
710
  { label: `verify:${sliceKey}`, phase: "Deep Scan", schema: VERIFY_SCHEMA, model: "sonnet" }
701
711
  );
702
- if (!v || v.verdict === "false-positive" || v.confirmed === false) return null;
703
- return { ...f, severity: v.correctedSeverity || f.severity, _verify: v.verdict };
712
+ // Compared case-INSENSITIVELY: the schema now accepts "false-positive"
713
+ // in any casing, so an exact match would silently KEEP a finding the
714
+ // verifier had rejected.
715
+ const verdict = String(v && v.verdict || "").toLowerCase();
716
+ if (!v || verdict === "false-positive" || v.confirmed === false) return null;
717
+ // Severity is normalised to the shouted form here, once, so the report
718
+ // reads consistently no matter how a finder typed it.
719
+ const sev = String(v.correctedSeverity || f.severity || "").toUpperCase();
720
+ return { ...f, severity: sev, _verify: verdict };
704
721
  } catch (e) {
705
722
  return { ...f, _verify: "verify-errored" };
706
723
  }
@@ -726,7 +743,50 @@ const succeededCount = slices.length - failedSlices.length;
726
743
  const coverageComplete = failedSlices.length === 0;
727
744
  const allFindings = sliceResults.filter(Boolean).filter((r) => !r.failed).flatMap((r) => (r.findings || []).map((f) => ({ ...f, slice: r.slice })));
728
745
  if (!coverageComplete) {
729
- log(`⚠ PARTIAL COVERAGE — ${failedSlices.length}/${slices.length} slices failed after retry and produced NO findings: ${failedSlices.join(", ")}. The register will be flagged INCOMPLETE. Resume the run to re-scan only the failed slices.`);
746
+ log(`⚠ PARTIAL COVERAGE — ${failedSlices.length}/${slices.length} slices failed after retry and produced NO findings: ${failedSlices.join(", ")}.`);
747
+
748
+ // HALT. A scan whose densest areas dropped out is not a scan with a caveat —
749
+ // it is a report that under-counts the debt while looking finished. On
750
+ // HiloAviation two of 228 slices failed, and the two were repositories and
751
+ // data-access: the areas with the most to find. Warning and continuing meant
752
+ // the register got written, read, and acted on as if complete.
753
+ //
754
+ // Writing the documents is the point of no return, so the stop goes HERE,
755
+ // before them — not after, where the flawed register already exists.
756
+ //
757
+ // Not a fallback and not a gate on findings: the run stops and says exactly
758
+ // which areas are missing. `allowPartial: true` continues deliberately, and
759
+ // the register still carries its PARTIAL banner in that case.
760
+ if (!allowPartial) {
761
+ const lines = [
762
+ "",
763
+ "════════════════════════════════════════════════════════════════",
764
+ ` SCAN HALTED — ${failedSlices.length} of ${slices.length} areas were never scanned`,
765
+ "════════════════════════════════════════════════════════════════",
766
+ "",
767
+ " Not scanned:",
768
+ ...failedSlices.map((k) => ` · ${k}`),
769
+ "",
770
+ " These areas found nothing because they FAILED, not because they",
771
+ " are clean. Their problems are missing from the register.",
772
+ "",
773
+ " Nothing has been written. Re-run to scan only the failed areas,",
774
+ " or re-run with allowPartial: true to accept an incomplete scan",
775
+ " (the register will say PARTIAL on its first line).",
776
+ "════════════════════════════════════════════════════════════════",
777
+ "",
778
+ ].join("\n");
779
+ log(lines);
780
+ return {
781
+ ok: false,
782
+ halted: "partial-coverage",
783
+ slicesTotal: slices.length,
784
+ slicesFailed: failedSlices,
785
+ findingsDiscarded: allFindings.length,
786
+ message: `${failedSlices.length} of ${slices.length} areas were never scanned. Nothing written.`,
787
+ };
788
+ }
789
+ log("→ allowPartial: true — continuing with an INCOMPLETE scan, as asked.");
730
790
  }
731
791
  log(`deep scan complete: ${allFindings.length} verified findings across ${succeededCount}/${slices.length} slices${coverageComplete ? " (full coverage)" : " (PARTIAL)"}`);
732
792
 
@@ -1131,8 +1191,11 @@ const docResults = await parallel(
1131
1191
  }
1132
1192
  })
1133
1193
  );
1134
- const docsOk = docResults.filter(Boolean).filter((r) => r.status === "written" || r.status === "merged");
1135
- const docsFailed = docResults.filter(Boolean).filter((r) => r.status === "failed");
1194
+ // Case-insensitive: the status word-list accepts any casing, so an exact match
1195
+ // would count a written document as neither written nor failed.
1196
+ const _status = (r) => String(r && r.status || "").toLowerCase();
1197
+ const docsOk = docResults.filter(Boolean).filter((r) => _status(r) === "written" || _status(r) === "merged");
1198
+ const docsFailed = docResults.filter(Boolean).filter((r) => _status(r) === "failed");
1136
1199
  log(`document phase: ${docsOk.length}/${docTargets.length} written/merged${docsFailed.length ? `; ${docsFailed.length} failed (non-fatal): ${docsFailed.map((d) => d.doc).join(", ")}` : ""}`);
1137
1200
 
1138
1201
  // ─── Plain-English phase (M78) ───────────────────────────────────────────────