@tekyzinc/gsd-t 5.20.12 → 5.20.13

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,19 @@
2
2
 
3
3
  All notable changes to GSD-T are documented here. Updated with each release.
4
4
 
5
+ ## [5.20.13] - 2026-09-21
6
+
7
+ ### Fixed — Team Mix staffs the midpoint of Low and High, not Low
8
+
9
+ David's correction on the first live sheet. The Team Mix was sized to the Low figure;
10
+ Tekyz staffs the midpoint of the Low and High estimates.
11
+
12
+ - `bin/gsd-t-estimate-sheet.cjs`: `midDays = low × (1 + high factor) / 2`; each phase grid is
13
+ built and audited against `(Low Hrs + High Hrs) ÷ 2 ÷ 8` from that phase's rollups; the
14
+ grids together against the midpoint of `Total Days` and `Total Days × high factor`.
15
+ - `templates/estimate-sheet-spec.md` §2.2 / §2.6 / §5; tests updated; live write on the E2E
16
+ copy: 3 phase grids, all audit checks pass.
17
+
5
18
  ## [5.20.12] - 2026-09-21
6
19
 
7
20
  ### Fixed — estimate writer proven end-to-end; two-month remainder formula
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # GSD-T: Contract-Driven Development for Claude Code
2
2
 
3
- **v5.20.12** - A methodology for reliable, parallelizable development using Claude Code with optional Agent Teams support.
3
+ **v5.20.13** - 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.
@@ -740,13 +740,19 @@ async function writeTshirtSizes(api, grid, sheetId, layout, plan, totals, phaseS
740
740
 
741
741
  // ───────────────────────── write: Team Mix (spec §2) ─────────────────────────
742
742
 
743
- /** Per-phase T-Shirt totals in PHASES order — only phases with hours > 0 (spec §2.6). */
743
+ /** Team Mix staffs the MIDPOINT of the Low and High figures (David, 2026-09-21): mid = low × (1 + highFactor) / 2. */
744
+ function midDays(lowDays, highFactor) { return round2(lowDays * (1 + highFactor) / 2); }
745
+
746
+ /** Per-phase T-Shirt totals in PHASES order — only phases with hours > 0 (spec §2.6). `totalDays` is the Low figure; `staffDays` the midpoint the Team Mix staffs. */
744
747
  function phaseTotals(plan, layout) {
745
748
  const raw = {};
746
749
  for (const s of plan.tshirt.sections) for (const it of s.items) {
747
750
  raw[it.phase] = (raw[it.phase] ? raw[it.phase] : 0) + sizeDays(it.fe, layout.legend) + sizeDays(it.be, layout.legend);
748
751
  }
749
- return PHASES.filter((ph) => raw[ph] > 0).map((ph) => ({ phase: ph, rawDays: round2(raw[ph]), totalDays: round2(raw[ph] * (1 + layout.mfTotal)) }));
752
+ return PHASES.filter((ph) => raw[ph] > 0).map((ph) => {
753
+ const low = round2(raw[ph] * (1 + layout.mfTotal));
754
+ return { phase: ph, rawDays: round2(raw[ph]), totalDays: low, highDays: round2(low * layout.highFactor), staffDays: midDays(low, layout.highFactor) };
755
+ });
750
756
  }
751
757
 
752
758
  /** One roster per phase with hours. FTE comes from teamMix.phases[<phase>].fte when given, else teamMix.fte. */
@@ -756,7 +762,7 @@ function buildRosters(plan, layout) {
756
762
  return phases.map((ph) => {
757
763
  const override = plan.teamMix.phases && plan.teamMix.phases[ph.phase] && plan.teamMix.phases[ph.phase].fte;
758
764
  const sub = override ? { ...plan, teamMix: { ...plan.teamMix, fte: override } } : plan;
759
- return { ...ph, roster: buildRoster(sub, ph.totalDays, layout.mf) };
765
+ return { ...ph, roster: buildRoster(sub, ph.staffDays, layout.mf) };
760
766
  });
761
767
  }
762
768
 
@@ -928,10 +934,11 @@ function auditTshirt(grid) {
928
934
  check(out, "T-Shirt: section rows are styled (bg #1C4F8B)", !badSectionFmt.length, badSectionFmt.join(", "));
929
935
  check(out, "T-Shirt: 'Total (Days)' row exists below the items", totalRow0 > lastItem0 && lastItem0 >= 0, "not found");
930
936
  let totalDaysCell = NaN;
937
+ // Team Mix reconciles to the MIDPOINT of each phase's Low Hrs (L) and High Hrs (N), in days
931
938
  const phaseDays = {};
932
939
  for (const rr of layout.rollupRows) {
933
- const hrs = numAt(grid, rr, 11); // L = Low Hrs for that phase
934
- phaseDays[textAt(grid, rr, 9).trim()] = Number.isNaN(hrs) ? NaN : round2(hrs / 8);
940
+ const low = numAt(grid, rr, 11), high = numAt(grid, rr, 13);
941
+ phaseDays[textAt(grid, rr, 9).trim()] = Number.isNaN(low) || Number.isNaN(high) ? NaN : round2((low + high) / 2 / 8);
935
942
  }
936
943
  if (totalRow0 > lastItem0 && lastItem0 >= 0) {
937
944
  check(out, "T-Shirt: totals row sits directly under the last item (no blank row)", totalRow0 === lastItem0 + 1, `last item row ${lastItem0 + 1}, totals row ${totalRow0 + 1}`);
@@ -962,7 +969,7 @@ function auditTshirt(grid) {
962
969
  const recomputed = round2(raw * (1 + layout.mfTotal));
963
970
  check(out, "T-Shirt: Σ raw sizes × (1+MF) == Total Days cell", Math.abs(recomputed - totalDaysCell) < 0.02, `recomputed ${recomputed}, cell ${totalDaysCell}`);
964
971
  }
965
- return { checks: out, layout, totalDays: totalDaysCell, lastItem1: lastItem0 + 1, phaseDays };
972
+ return { checks: out, layout, totalDays: totalDaysCell, staffDays: Number.isNaN(totalDaysCell) ? NaN : midDays(totalDaysCell, layout.highFactor), lastItem1: lastItem0 + 1, phaseDays };
966
973
  }
967
974
 
968
975
  function disciplineOfLabel(label) {
@@ -1089,7 +1096,7 @@ function auditTeamMix(grid, mfList, tshirtTotalDays, phaseDays) {
1089
1096
  const phase = expectedPhases.find((ph) => title.endsWith(ph));
1090
1097
  if (expectedPhases.length) {
1091
1098
  check(out, `${label}: title names a phase with hours`, !!phase, `title '${title}' ends with none of [${expectedPhases.join(", ")}]`);
1092
- if (phase) check(out, `${label} (${phase}): Σ Days == that phase's T-Shirt days`, Math.abs(sumDays - phaseDays[phase]) < tol, `Team Mix ${sumDays}, T-Shirt ${phaseDays[phase]} (tolerance ${round2(tol)})`);
1099
+ if (phase) check(out, `${label} (${phase}): Σ Days == midpoint of that phase's Low/High days`, Math.abs(sumDays - phaseDays[phase]) < tol, `Team Mix ${sumDays}, T-Shirt ${phaseDays[phase]} (tolerance ${round2(tol)})`);
1093
1100
  }
1094
1101
  prevBottom = totalR + 1;
1095
1102
  }
@@ -1100,7 +1107,7 @@ function auditTeamMix(grid, mfList, tshirtTotalDays, phaseDays) {
1100
1107
  check(out, "Team Mix: nothing formatted below the last grid", !strayBelow.length, strayBelow.slice(0, 8).join(", "));
1101
1108
  }
1102
1109
  if (!Number.isNaN(tshirtTotalDays) && headers.length) {
1103
- check(out, "Team Mix: Σ Days over all grids == T-Shirt Total Days", Math.abs(sumDaysAll - tshirtTotalDays) < Math.max(0.15, tolAll), `Team Mix ${round2(sumDaysAll)}, T-Shirt ${tshirtTotalDays} (tolerance ${round2(Math.max(0.15, tolAll))})`);
1110
+ check(out, "Team Mix: Σ Days over all grids == midpoint of T-Shirt Low/High days", Math.abs(sumDaysAll - tshirtTotalDays) < Math.max(0.15, tolAll), `Team Mix ${round2(sumDaysAll)}, T-Shirt midpoint ${tshirtTotalDays} (tolerance ${round2(Math.max(0.15, tolAll))})`);
1104
1111
  }
1105
1112
  return { checks: out, grids: headers.length };
1106
1113
  }
@@ -1134,7 +1141,7 @@ async function runAudit(api) {
1134
1141
  const checks = [];
1135
1142
  const tshirt = auditTshirt(await api.grid(TAB_TSHIRT));
1136
1143
  checks.push(...tshirt.checks);
1137
- checks.push(...auditTeamMix(await api.grid(TAB_TEAM), tshirt.layout.mf, tshirt.totalDays, tshirt.phaseDays).checks);
1144
+ checks.push(...auditTeamMix(await api.grid(TAB_TEAM), tshirt.layout.mf, tshirt.staffDays, tshirt.phaseDays).checks);
1138
1145
  checks.push(...auditTechStack(await api.grid(TAB_TECH)).checks);
1139
1146
  if (tabs.has(TAB_OVERVIEW)) checks.push(...auditOverview(await api.grid(TAB_OVERVIEW)).checks);
1140
1147
  else check(checks, "Overview tab exists (the estimates index imports its cells)", false, "missing");
@@ -1191,7 +1198,7 @@ async function verbRead(api, tab) {
1191
1198
  }
1192
1199
 
1193
1200
  function rostersTable(rosters) {
1194
- return rosters.map((r) => `── ${r.phase}: ${r.totalDays} d ──\n${rosterTable(r.roster)}`).join("\n");
1201
+ return rosters.map((r) => `── ${r.phase}: low ${r.totalDays} d · high ${r.highDays} d → staffing the midpoint ${r.staffDays} d ──\n${rosterTable(r.roster)}`).join("\n");
1195
1202
  }
1196
1203
 
1197
1204
  async function verbPlanCheck(api, plan) {
@@ -1208,7 +1215,7 @@ async function verbWrite(api, plan, opts) {
1208
1215
  const tech = await writeTechStack(api, plan);
1209
1216
  const result = {
1210
1217
  tshirt: { mode: plan.tshirt.mode, itemsWritten: ts.itemsWritten, firstItem1: ts.firstItem1, lastItem1: ts.lastItem1, totalRow1: ts.totalRow1, totals: ts.totals },
1211
- teamMix: { grids: tm.grids, phases: rosters.map((r) => ({ phase: r.phase, totalDays: r.totalDays, people: r.roster.people.length, months: r.roster.months, columns: r.roster.n, sumDays: r.roster.sumDays })), table: rostersTable(rosters), lastRow: tm.lastRow },
1218
+ teamMix: { grids: tm.grids, phases: rosters.map((r) => ({ phase: r.phase, lowDays: r.totalDays, highDays: r.highDays, staffDays: r.staffDays, people: r.roster.people.length, months: r.roster.months, columns: r.roster.n, sumDays: r.roster.sumDays })), table: rostersTable(rosters), lastRow: tm.lastRow },
1212
1219
  techStack: tech,
1213
1220
  };
1214
1221
  if (!opts.noAudit) result.audit = await runAudit(api);
@@ -1271,7 +1278,7 @@ async function main(args) {
1271
1278
  if (json) console.log(JSON.stringify({ ok, exitCode: ok ? 0 : 4, ...r }, null, 2));
1272
1279
  else {
1273
1280
  console.log(`T-Shirt (${r.tshirt.mode}): ${r.tshirt.itemsWritten} items, rows ${r.tshirt.firstItem1}–${r.tshirt.lastItem1}, totals row ${r.tshirt.totalRow1}; total ${r.tshirt.totals.totalDays} d, $${r.tshirt.totals.lowDollars} – $${r.tshirt.totals.highDollars}`);
1274
- for (const ph of r.teamMix.phases) console.log(`Team Mix — ${ph.phase}: ${ph.people} people, ${ph.months} months, ${ph.columns} columns, Σ Days ${ph.sumDays}`);
1281
+ for (const ph of r.teamMix.phases) console.log(`Team Mix — ${ph.phase}: ${ph.people} people, ${ph.months} months, ${ph.columns} columns, Σ Days ${ph.sumDays} (midpoint of ${ph.lowDays}–${ph.highDays})`);
1275
1282
  console.log(r.teamMix.table);
1276
1283
  console.log(`Technology Stack: ${r.techStack.rows} rows`);
1277
1284
  if (r.audit) printChecks(r.audit);
@@ -1291,7 +1298,7 @@ function haltAndExit(e, json) {
1291
1298
 
1292
1299
  module.exports = {
1293
1300
  validatePlan, splitRoster, rosterViolations, mfCoverageViolations, monthPlan, resampleWeights, rampHours, buildRoster,
1294
- tshirtTotals, phaseTotals, buildRosters, itemFormulas, rollupFormulas, tshirtRows, teamMixValues, teamMixFormatReqs, remainderFormula, locateTshirt, findPhaseSource,
1301
+ tshirtTotals, phaseTotals, buildRosters, midDays, itemFormulas, rollupFormulas, tshirtRows, teamMixValues, teamMixFormatReqs, remainderFormula, locateTshirt, findPhaseSource,
1295
1302
  auditTshirt, auditTeamMix, auditTechStack, auditOverview, colLetter, hexToColor, colorToHex, sheetIdFromArg,
1296
1303
  constants: { SIZE_CODES, PHASES, COLOR, RAMP, ROLE_LABEL, SOFT_CEILING, FOLD_THRESHOLD, TAB_TSHIRT, TAB_TEAM, TAB_TECH, PLAN_SCHEMA },
1297
1304
  Halt, SheetsApi, getToken, runAudit, main,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekyzinc/gsd-t",
3
- "version": "5.20.12",
3
+ "version": "5.20.13",
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",
@@ -130,14 +130,15 @@ The old `Month / Days / Tot Days / Hrs` layout is retired: `Days` IS the per-per
130
130
 
131
131
  ### 2.2 The math
132
132
 
133
- - `months = totalDays / (Σ Count × 20)` — solve so `Σ Days` equals the T-Shirt `Total Days`.
133
+ - **The Team Mix staffs the MIDPOINT of the Low and High figures** (David, 2026-09-21): `staffDays = Low Total Days × (1 + high factor) / 2` — per phase, from that phase's `Low Hrs` and `High Hrs` rollups. Never the Low figure alone.
134
+ - `months = staffDays / (Σ Count × 20)` — solve so `Σ Days` equals that midpoint.
134
135
  - Month columns `N = ceil(months)`, EXCEPT when the fractional tail is under ~0.1 month: fold it into the last full month (soft ceiling ~172 hrs) instead of opening a near-empty column.
135
136
  - **Recompute `months` and `N` after ANY roster change.** Adding a role raises Σ Count, shortens the duration, and turns the remainder column negative (−12.80 hrs was the symptom).
136
137
  - The remainder formula makes each row sum exactly. Never write a fractional last month as a value.
137
138
 
138
139
  ### 2.6 One grid per phase
139
140
 
140
- Every phase (`MVP` / `Phase 1` / `Phase 2` / `Phase 3`) whose T-Shirt items have hours > 0 gets its own Team Mix grid, in phase order, on the same tab, with 2 blank unformatted rows between grids. Each grid's `Σ Days` reconciles to **that phase's** T-Shirt days (the phase rollup `Low Hrs ÷ 8`), and the grids together reconcile to `Total Days`. Months and column count are computed per grid. The team mix is the same for every phase unless the plan gives `teamMix.phases.<phase>.fte`.
141
+ Every phase (`MVP` / `Phase 1` / `Phase 2` / `Phase 3`) whose T-Shirt items have hours > 0 gets its own Team Mix grid, in phase order, on the same tab, with 2 blank unformatted rows between grids. Each grid's `Σ Days` reconciles to **the midpoint of that phase's Low and High days** (`(Low Hrs + High Hrs) ÷ 2 ÷ 8` from the phase rollups), and the grids together reconcile to the midpoint of `Total Days` and `Total Days × high factor`. Months and column count are computed per grid. The team mix is the same for every phase unless the plan gives `teamMix.phases.<phase>.fte`.
141
142
 
142
143
  ### 2.3 Roster shape — one row per PERSON
143
144
 
@@ -226,7 +227,7 @@ TEAM MIX (each grid)
226
227
  [ ] font Arial 10 everywhere; sage on exactly E, F, Total Hrs of role rows; monthly cells white
227
228
  [ ] Total row (#E5E5E5) and Total Days row (#9EC1EF) span every month column + Total Hrs
228
229
  [ ] nothing formatted outside the data area
229
- [ ] each grid's Σ Days == that phase's T-Shirt days; all grids together == Total Days
230
+ [ ] each grid's Σ Days == midpoint of that phase's Low/High days; all grids together == midpoint of Total Days and Total Days × high factor
230
231
  TECH STACK
231
232
  [ ] at least the categories that apply are filled; none says TBD
232
233
  OVERVIEW