@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 +13 -0
- package/README.md +1 -1
- package/bin/gsd-t-estimate-sheet.cjs +20 -13
- package/package.json +1 -1
- package/templates/estimate-sheet-spec.md +4 -3
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.
|
|
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
|
-
/**
|
|
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) =>
|
|
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.
|
|
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
|
|
934
|
-
phaseDays[textAt(grid, rr, 9).trim()] = Number.isNaN(
|
|
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
|
|
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
|
|
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.
|
|
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,
|
|
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.
|
|
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
|
-
- `
|
|
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
|
|
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
|
|
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
|