@tekyzinc/gsd-t 5.20.14 → 5.21.10
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 +47 -0
- package/README.md +2 -2
- package/bin/gsd-t-estimate-sheet.cjs +124 -13
- package/bin/gsd-t-model-tier-policy.cjs +8 -8
- package/commands/gsd-t-estimate.md +1 -0
- package/commands/gsd-t-help.md +1 -1
- package/commands/gsd-t-status.md +1 -1
- package/package.json +2 -2
- package/templates/CLAUDE-global.md +3 -3
- package/templates/estimate-sheet-spec.md +15 -5
- package/templates/prompts/blind-adversary-subagent.md +1 -1
- package/templates/workflows/gsd-t-debug.workflow.js +1 -1
- package/templates/workflows/gsd-t-phase.workflow.js +1 -1
- package/templates/workflows/gsd-t-verify.workflow.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,53 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GSD-T are documented here. Updated with each release.
|
|
4
4
|
|
|
5
|
+
## [5.21.10] - 2026-09-22
|
|
6
|
+
|
|
7
|
+
### Changed — the top tier now points at Opus 5.5
|
|
8
|
+
|
|
9
|
+
The `opus` tier alias resolved to `claude-opus-5`. It now resolves to
|
|
10
|
+
`claude-opus-5-5`, so every high-stakes stage (solution-space probe, partition
|
|
11
|
+
probe, competition producers and judge, pre-mortem, Red Team, both debug cycles)
|
|
12
|
+
runs Opus 5.5. Only the concrete model id changed: the three-tier shape, the
|
|
13
|
+
stage-to-tier map, and the relaxed fresh-context judge-blindness invariant are
|
|
14
|
+
untouched.
|
|
15
|
+
|
|
16
|
+
- `bin/gsd-t-model-tier-policy.cjs`: `MODEL_IDS.opus` → `claude-opus-5-5`; the
|
|
17
|
+
thinking-omission predicate still matches no current tier model
|
|
18
|
+
- `.gsd-t/contracts/model-tier-policy-contract.md`: → v2.1.0, alias table and
|
|
19
|
+
Updated line; the v2.0.0 note kept under `## Previously`
|
|
20
|
+
- `templates/workflows/gsd-t-{phase,verify,debug}.workflow.js`,
|
|
21
|
+
`templates/prompts/blind-adversary-subagent.md`: model id in the stage comments
|
|
22
|
+
- `templates/CLAUDE-global.md`, `README.md`, `commands/gsd-t-{help,status}.md`:
|
|
23
|
+
the documented `opus` = model id
|
|
24
|
+
- `test/m85-model-tier-policy.test.js`, `test/m86-policy-profiles.test.js`,
|
|
25
|
+
`test/m90-tier-policy-lint.test.js`: expected id; the drifted-literal negative
|
|
26
|
+
test still fails on a mismatch
|
|
27
|
+
|
|
28
|
+
No migration. A project picks the new id on its next `gsd-t update-all`.
|
|
29
|
+
|
|
30
|
+
## [5.20.15] - 2026-09-21
|
|
31
|
+
|
|
32
|
+
### Changed — phases must be contiguous; the Team Mix title row is the phase name only
|
|
33
|
+
|
|
34
|
+
David's review of the 19 Hilo estimates: one had items in MVP and Phase 2 with nothing in
|
|
35
|
+
Phase 1, and every Team Mix title row carried the estimate name.
|
|
36
|
+
|
|
37
|
+
- Rule (spec §2.6): phases are contiguous — `MVP`, `Phase 1`, `Phase 2`, … with no empty phase
|
|
38
|
+
between used ones. The audit fails a gap. New `phases` verb renumbers the item Phase cells to
|
|
39
|
+
close the gap and rebuilds the Team Mix grids.
|
|
40
|
+
- Rule (spec §2.1): each grid's title row is EXACTLY the phase name. The writer, `teammix` and
|
|
41
|
+
the audit follow it; the plan's `title` is no longer used. New `titles` verb sets existing
|
|
42
|
+
grids' title rows (the nearest non-empty row above the header — older grids keep a blank row
|
|
43
|
+
between title and header).
|
|
44
|
+
- Applied: Acron Academy renumbered (Phase 2→1, Phase 3→2, 20 cells) and its grids rebuilt;
|
|
45
|
+
all 19 estimates' Team Mix titles renamed.
|
|
46
|
+
- Rule (spec §0 / §1.2): Functionality and Low Level Requirements wrap on every item row, and
|
|
47
|
+
every cell on every tab is top-aligned. Writer, `format` verb (which now top-aligns all tabs)
|
|
48
|
+
and audit follow it; applied to all 19 estimates.
|
|
49
|
+
- The 14 single-phase estimates' Team Mixes rebuilt on the Low/High midpoint (`teammix`), so all
|
|
50
|
+
19 now staff the midpoint; the command's Step 4 states the rule.
|
|
51
|
+
|
|
5
52
|
## [5.20.14] - 2026-09-21
|
|
6
53
|
|
|
7
54
|
### Added — `gsd-t estimate-sheet teammix` rebuilds any estimate's Team Mix; the reader handles both template layouts
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# GSD-T: Contract-Driven Development for Claude Code
|
|
2
2
|
|
|
3
|
-
**v5.
|
|
3
|
+
**v5.21.10** - 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.
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
**Real-Time Agent Dashboard** — `gsd-t-stream-feed-server.js` serves a streaming UI at `127.0.0.1:7842` that renders all workers' stream-json output as a continuous feed with task/wave banners, duration + usage chips, token corner bar, localStorage filters, and replay via `WS /feed?from=N`. Dashboard auto-starts idempotently on each spawn (`scripts/gsd-t-dashboard-autostart.cjs`). Port is project-scoped via `projectScopedDefaultPort(projectDir)` so multi-project workflows do not clobber each other.
|
|
18
18
|
**Rigorous User-Journey Coverage + Anti-Drift Test Quality** — `bin/journey-coverage.cjs` regex listener detector + `gsd-t check-coverage` CLI + `scripts/hooks/pre-commit-journey-coverage` commit gate blocks viewer-source commits when uncovered listeners exist. Journey specs in `e2e/journeys/` use functional assertions (zero `toBeVisible`-only tests) per the E2E Test Quality Standard in CLAUDE.md.
|
|
19
19
|
**Universal Playwright Bootstrap + Deterministic UI Enforcement (M50)** — three executable enforcement layers: (1) `bin/playwright-bootstrap.cjs` + `bin/ui-detection.cjs` - idempotent installer detects package manager, installs `@playwright/test` + chromium, scaffolds `e2e/`; (2) Workflow runtime runs `playwright-bootstrap.cjs::installPlaywright()` before any E2E stage when `hasUI && !hasPlaywright`; install failure halts with `blocked-needs-human`; (3) `scripts/hooks/pre-commit-playwright-gate` (opt-in via `gsd-t doctor --install-hooks`) blocks viewer-source commits when staged files are newer than `.gsd-t/.last-playwright-pass`. The `gsd-t setup-playwright [path]` subcommand handles manual install.
|
|
20
|
-
**Surgical model selection** — models are assigned haiku/sonnet/opus per phase (**Fable removed 2026-07-24**; `opus` = **claude-opus-5**). **Single-source tier policy:** `bin/gsd-t-model-tier-policy.cjs` is the SINGLE source of truth; every high-stakes stage (solution-space probe, partition probe, competition judge, pre-mortem, Red Team, competition producers, debug both cycles) runs Opus 5. Opus 5 shipped at the same price as Opus 4.8 but >2× its coding score, so Fable's cost premium is no longer justified. The M82 judge-blindness invariant is relaxed to "fresh independent context" — producers and judge both run opus. Drift is mechanically enforced by the M71-family lint (`test/m85-workflow-tier-policy-lint.test.js`). **M86 model profiles:** `bin/gsd-t-model-profile.cjs` adds a per-project SECOND dimension — three named profiles (`standard` / `pro` / `premium`) that dial which stages run on Opus vs. Sonnet.
|
|
20
|
+
**Surgical model selection** — models are assigned haiku/sonnet/opus per phase (**Fable removed 2026-07-24**; `opus` = **claude-opus-5-5**). **Single-source tier policy:** `bin/gsd-t-model-tier-policy.cjs` is the SINGLE source of truth; every high-stakes stage (solution-space probe, partition probe, competition judge, pre-mortem, Red Team, competition producers, debug both cycles) runs Opus 5. Opus 5 shipped at the same price as Opus 4.8 but >2× its coding score, so Fable's cost premium is no longer justified. The M82 judge-blindness invariant is relaxed to "fresh independent context" — producers and judge both run opus. Drift is mechanically enforced by the M71-family lint (`test/m85-workflow-tier-policy-lint.test.js`). **M86 model profiles:** `bin/gsd-t-model-profile.cjs` adds a per-project SECOND dimension — three named profiles (`standard` / `pro` / `premium`) that dial which stages run on Opus vs. Sonnet.
|
|
21
21
|
**Token Telemetry** — `gsd-t-calibration-hook.js` records token usage per spawn to `.gsd-t/token-metrics.jsonl` (18-field rows). `gsd-t-token-aggregator.js` aggregates across tasks for the `/gsd-t-metrics` view. Use the native Claude Code `/context` command for live in-session context percentage.
|
|
22
22
|
**Quality North Star** — projects define a `## Quality North Star` section in CLAUDE.md (1–3 sentences, e.g., "This is a published npm library. Every public API must be intuitive and backward-compatible."). `gsd-t-init` auto-detects preset (library/web-app/cli) from package.json signals; `gsd-t-setup` configures it for existing projects. Subagents read it as a quality lens; absent = silent skip (backward compatible).
|
|
23
23
|
**Design Brief Artifact** — during partition, UI/frontend projects (React, Vue, Svelte, Flutter, Tailwind) automatically get `.gsd-t/contracts/design-brief.md` with color palette, typography, spacing system, component patterns, and tone/voice. Non-UI projects skip silently. User-customized briefs are preserved. Referenced in plan phase for visual consistency.
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
* write --sheet <id|url> --plan <plan.json> [--replace] write T-Shirt + Team Mix + Tech Stack, then audit
|
|
26
26
|
* teammix --sheet <id|url> [--fte <json>] [--dry-run] rebuild the Team Mix (one grid per phase) from the sheet's own roster + rollups
|
|
27
27
|
* format --sheet <id|url> [--dry-run] normalise T-Shirt formatting only (section rows, totals band, summary block); values untouched
|
|
28
|
+
* phases --sheet <id|url> [--dry-run] close phase gaps (MVP, Phase 2 → MVP, Phase 1) on the T-Shirt tab, then rebuild the Team Mix
|
|
29
|
+
* titles --sheet <id|url> [--dry-run] set each Team Mix grid's title row to exactly its phase name
|
|
28
30
|
* audit --sheet <id|url> the spec §5 checklist, by read-back
|
|
29
31
|
* plan-schema print the plan shape
|
|
30
32
|
* Flags: --json (envelope only) --key <path> --no-audit (write only; for debugging)
|
|
@@ -232,7 +234,7 @@ class SheetsApi {
|
|
|
232
234
|
|
|
233
235
|
async grid(tab) {
|
|
234
236
|
const rng = encodeURIComponent(`'${tab}'`);
|
|
235
|
-
const fields = "sheets(merges,properties,data(rowData(values(formattedValue,userEnteredValue,effectiveValue,userEnteredFormat(backgroundColor,textFormat,horizontalAlignment,numberFormat),dataValidation)),columnMetadata(pixelSize)))";
|
|
237
|
+
const fields = "sheets(merges,properties,data(rowData(values(formattedValue,userEnteredValue,effectiveValue,userEnteredFormat(backgroundColor,textFormat,horizontalAlignment,verticalAlignment,wrapStrategy,numberFormat),dataValidation)),columnMetadata(pixelSize)))";
|
|
236
238
|
const r = await this.call("GET", `${this.base}?ranges=${rng}&includeGridData=true&fields=${fields}`);
|
|
237
239
|
if (!r.sheets || !r.sheets[0]) throw new Halt(`tab '${tab}' not found`, 64);
|
|
238
240
|
return r.sheets[0];
|
|
@@ -412,7 +414,7 @@ function findPhaseSource(grid, fromRow) { return findPhaseSourceIn(grid, fromRow
|
|
|
412
414
|
// ───────────────────────── plan validation (pure) ─────────────────────────
|
|
413
415
|
|
|
414
416
|
const PLAN_SCHEMA = {
|
|
415
|
-
title: "string — Team Mix title
|
|
417
|
+
title: "optional string — no longer written anywhere (the Team Mix title row is the phase name only)",
|
|
416
418
|
tshirt: {
|
|
417
419
|
mode: "'items' (write whole rows from row 14) | 'sizes' (rows already exist; fill E:L, matched by the id in column C)",
|
|
418
420
|
sections: [{
|
|
@@ -437,7 +439,7 @@ function sizeOf(v) { return v == null ? "" : String(v).trim(); }
|
|
|
437
439
|
function validatePlan(plan) {
|
|
438
440
|
const errors = [];
|
|
439
441
|
if (!plan || typeof plan !== "object") return ["plan is not an object"];
|
|
440
|
-
if (
|
|
442
|
+
if (plan.title != null && typeof plan.title !== "string") errors.push("title: must be a string when given (unused since v5.20.15 — the Team Mix title row is the phase name)");
|
|
441
443
|
const t = plan.tshirt && typeof plan.tshirt === "object" ? plan.tshirt : {};
|
|
442
444
|
if (!["items", "sizes"].includes(t.mode)) errors.push("tshirt.mode: must be 'items' or 'sizes'");
|
|
443
445
|
const sections = Array.isArray(t.sections) ? t.sections : [];
|
|
@@ -690,6 +692,13 @@ function tshirtRows(plan, firstRow0) {
|
|
|
690
692
|
function fmtReq(sheetId, r0, r1, c0, c1, format, fields) {
|
|
691
693
|
return { repeatCell: { range: gridRange(sheetId, r0, r1, c0, c1), cell: { userEnteredFormat: format }, fields } };
|
|
692
694
|
}
|
|
695
|
+
/** Rule (spec §0): every cell on a tab the tool writes is TOP-aligned; Functionality + Low Level Requirements wrap. */
|
|
696
|
+
function topAlignReq(sheetId, rows) {
|
|
697
|
+
return fmtReq(sheetId, 0, rows, 0, 26, { verticalAlignment: "TOP" }, "userEnteredFormat.verticalAlignment");
|
|
698
|
+
}
|
|
699
|
+
function wrapReq(sheetId, r0, r1, c0, c1) {
|
|
700
|
+
return fmtReq(sheetId, r0, r1, c0, c1, { wrapStrategy: "WRAP" }, "userEnteredFormat.wrapStrategy");
|
|
701
|
+
}
|
|
693
702
|
function widthReqs(sheetId, widths) {
|
|
694
703
|
return widths.map((w, i) => ({ updateDimensionProperties: { range: { sheetId, dimension: "COLUMNS", startIndex: i, endIndex: i + 1 }, properties: { pixelSize: w }, fields: "pixelSize" } }));
|
|
695
704
|
}
|
|
@@ -767,6 +776,8 @@ async function writeTshirt(api, plan, opts) {
|
|
|
767
776
|
reqs.push(copyPhaseReq(sheetId, phaseSrc, r0)); // carries validation + chip format; value re-put below
|
|
768
777
|
}
|
|
769
778
|
}
|
|
779
|
+
reqs.push(wrapReq(sheetId, first0, lastItem0, 2, 4)); // Functionality + Low Level Requirements wrap
|
|
780
|
+
reqs.push(topAlignReq(sheetId, lastWritten1 + 5)); // every cell top-aligned
|
|
770
781
|
reqs.push(...widthReqs(sheetId, TSHIRT_WIDTHS));
|
|
771
782
|
await api.batch(reqs);
|
|
772
783
|
// 5. the Phase VALUES again — copyPaste overwrote them with the source cell's value
|
|
@@ -809,7 +820,9 @@ async function writeTshirtSizes(api, grid, sheetId, layout, plan, totals, phaseS
|
|
|
809
820
|
reqs.push(fmtReq(sheetId, w.r0, w.r0 + 1, 4, 7, FMT_ITEM_SIZE, "userEnteredFormat(horizontalAlignment,textFormat)"));
|
|
810
821
|
reqs.push(fmtReq(sheetId, w.r0, w.r0 + 1, 7, 10, FMT_ITEM_NUM, FMT_FIELDS_ALL));
|
|
811
822
|
reqs.push(fmtReq(sheetId, w.r0, w.r0 + 1, 10, 12, FMT_ITEM_CUR, FMT_FIELDS_ALL));
|
|
823
|
+
reqs.push(wrapReq(sheetId, w.r0, w.r0 + 1, 2, 4));
|
|
812
824
|
}
|
|
825
|
+
reqs.push(topAlignReq(sheetId, rowCount(grid) + 2));
|
|
813
826
|
await api.batch(reqs);
|
|
814
827
|
for (const w of writes) await api.putValues(TAB_TSHIRT, `E${w.r0 + 1}`, [[w.phase]]);
|
|
815
828
|
const first1 = layout.firstItemRow + 1;
|
|
@@ -872,7 +885,7 @@ function teamMixValues(plan, roster, phase, top) {
|
|
|
872
885
|
for (let m = 1; m <= n; m++) header.push(`Mon ${m}`);
|
|
873
886
|
header.push("Total Hrs");
|
|
874
887
|
const rows = [];
|
|
875
|
-
rows.push([phase ?
|
|
888
|
+
rows.push([phase ? phase : "MVP", ...Array(width - 1).fill("")]); // the title row holds ONLY the phase name (David, 2026-09-21)
|
|
876
889
|
rows.push(header);
|
|
877
890
|
const firstP = top + 3; // 1-based row of the first person
|
|
878
891
|
roster.people.forEach((p, i) => {
|
|
@@ -929,7 +942,7 @@ async function writeTeamMix(api, plan, rosters) {
|
|
|
929
942
|
const grids = [];
|
|
930
943
|
let top = 0;
|
|
931
944
|
for (const r of rosters) {
|
|
932
|
-
const v = teamMixValues(plan, r.roster,
|
|
945
|
+
const v = teamMixValues(plan, r.roster, r.phase, top);
|
|
933
946
|
grids.push({ phase: r.phase, v });
|
|
934
947
|
top = v.bottom + GRID_GAP;
|
|
935
948
|
}
|
|
@@ -948,6 +961,7 @@ async function writeTeamMix(api, plan, rosters) {
|
|
|
948
961
|
maxN = Math.max(maxN, g.v.totalC - g.v.firstMonthC);
|
|
949
962
|
}
|
|
950
963
|
reqs.push(...widthReqs(sheetId, [...TEAM_WIDTHS_FIXED, ...Array(maxN).fill(TEAM_MONTH_WIDTH), TEAM_TOTAL_WIDTH]));
|
|
964
|
+
reqs.push(topAlignReq(sheetId, lastRow + 5));
|
|
951
965
|
await api.batch(reqs);
|
|
952
966
|
return { grids: grids.map((g) => ({ phase: g.phase, titleR: g.v.titleR, bottom: g.v.bottom, rows: g.v.rows.length })), lastRow };
|
|
953
967
|
}
|
|
@@ -969,6 +983,7 @@ async function writeTechStack(api, plan) {
|
|
|
969
983
|
{ mergeCells: { range: gridRange(sheetId, 0, 1, 0, 2), mergeType: "MERGE_ALL" } },
|
|
970
984
|
fmtReq(sheetId, 0, 1, 0, 2, { backgroundColor: hexToColor(COLOR.teamHeaderBg), textFormat: { fontSize: 12, bold: true, foregroundColor: hexToColor(COLOR.white) } }, "userEnteredFormat(backgroundColor,textFormat)"),
|
|
971
985
|
fmtReq(sheetId, 1, rows.length, 0, 2, { textFormat: { fontFamily: "Arial", fontSize: 10 }, wrapStrategy: "WRAP" }, "userEnteredFormat(textFormat,wrapStrategy)"),
|
|
986
|
+
topAlignReq(sheetId, rows.length + 5),
|
|
972
987
|
...widthReqs(sheetId, TECH_WIDTHS),
|
|
973
988
|
]);
|
|
974
989
|
return { rows: rows.length - 1 };
|
|
@@ -985,7 +1000,7 @@ function auditTshirt(grid) {
|
|
|
985
1000
|
const { cols } = layout;
|
|
986
1001
|
const legendRows = { first1: layout.legendFirst1, last1: layout.legendLast1 };
|
|
987
1002
|
const first0 = layout.firstItemRow;
|
|
988
|
-
const badSizes = [], noDv = [], badFormula = [], sectionsWithSizes = [], badSectionFmt = [];
|
|
1003
|
+
const badSizes = [], noDv = [], badFormula = [], sectionsWithSizes = [], badSectionFmt = [], noWrap = [], notTop = [];
|
|
989
1004
|
let lastItem0 = -1, totalRow0 = -1;
|
|
990
1005
|
for (let r = first0; r < rowCount(grid); r++) {
|
|
991
1006
|
const a = textAt(grid, r, 0);
|
|
@@ -1006,14 +1021,22 @@ function auditTshirt(grid) {
|
|
|
1006
1021
|
if (!(cell && cell.dataValidation && cell.dataValidation.condition && cell.dataValidation.condition.type === "ONE_OF_LIST")) noDv.push(`${colLetter(cols.phase)}${r + 1}`);
|
|
1007
1022
|
const f = itemFormulasFor(r + 1, cols, legendRows);
|
|
1008
1023
|
for (const [c, want] of f.cells) if (formulaAt(grid, r, c) !== want) badFormula.push(`${colLetter(c)}${r + 1}`);
|
|
1024
|
+
for (const c of [2, 3]) { const fm = cellAt(grid, r, c) && cellAt(grid, r, c).userEnteredFormat; if (!fm || fm.wrapStrategy !== "WRAP") noWrap.push(`${colLetter(c)}${r + 1}`); }
|
|
1025
|
+
for (const c of [0, 2, cols.days]) { const fm = cellAt(grid, r, c) && cellAt(grid, r, c).userEnteredFormat; if (!fm || fm.verticalAlignment !== "TOP") notTop.push(`${colLetter(c)}${r + 1}`); }
|
|
1009
1026
|
}
|
|
1010
1027
|
check(out, "T-Shirt: at least one item row", lastItem0 >= 0, "no item rows below the header");
|
|
1028
|
+
check(out, "T-Shirt: Functionality + Low Level Requirements wrap on every item row", !noWrap.length, noWrap.slice(0, 8).join(", "));
|
|
1029
|
+
check(out, "T-Shirt: item cells are top-aligned", !notTop.length, notTop.slice(0, 8).join(", "));
|
|
1011
1030
|
check(out, "T-Shirt: size cells are bare codes (no legend text, no '-')", !badSizes.length, badSizes.slice(0, 10).join(", "));
|
|
1012
1031
|
check(out, "T-Shirt: every item row has the Phase dropdown", !noDv.length, noDv.slice(0, 10).join(", "));
|
|
1013
1032
|
check(out, "T-Shirt: Days..HIGH $ are the spec formulas on every item row", !badFormula.length, badFormula.slice(0, 12).join(", "));
|
|
1014
1033
|
check(out, "T-Shirt: section rows carry no sizes", !sectionsWithSizes.length, `rows ${sectionsWithSizes.join(", ")}`);
|
|
1015
1034
|
check(out, "T-Shirt: section rows are styled (bg #1C4F8B)", !badSectionFmt.length, badSectionFmt.slice(0, 6).join(", "));
|
|
1016
1035
|
check(out, "T-Shirt: 'Total (Days)' row exists below the items", totalRow0 > lastItem0 && lastItem0 >= 0, "not found");
|
|
1036
|
+
const usedPhases = new Set();
|
|
1037
|
+
for (let r = first0; r <= lastItem0; r++) { const v = textAt(grid, r, cols.phase).trim(); if (PHASES.includes(v)) usedPhases.add(v); }
|
|
1038
|
+
const gapMap = phaseGapMap([...usedPhases]);
|
|
1039
|
+
check(out, "T-Shirt: phases are contiguous (no empty phase between used ones)", !Object.keys(gapMap).length, `used [${PHASES.filter((p) => usedPhases.has(p)).join(", ")}] — rename ${Object.entries(gapMap).map(([a, b]) => `${a}→${b}`).join(", ")}`);
|
|
1017
1040
|
let totalDaysCell = NaN;
|
|
1018
1041
|
const phaseDays = {};
|
|
1019
1042
|
for (const rr of layout.rollupRows) {
|
|
@@ -1163,6 +1186,8 @@ function auditTeamMix(grid, mfList, tshirtTotalDays, phaseDays) {
|
|
|
1163
1186
|
}
|
|
1164
1187
|
}
|
|
1165
1188
|
check(out, `${label}: body font is Arial (never Calibri)`, !badFont.length, badFont.slice(0, 8).join(", "));
|
|
1189
|
+
const notTopTm = people.filter((p) => { const fm = cellAt(grid, p.r, 0) && cellAt(grid, p.r, 0).userEnteredFormat; return !fm || fm.verticalAlignment !== "TOP"; }).map((p) => `A${p.r + 1}`);
|
|
1190
|
+
check(out, `${label}: cells are top-aligned`, !notTopTm.length, notTopTm.slice(0, 6).join(", "));
|
|
1166
1191
|
check(out, `${label}: sage on exactly Days, Hrs, Total Hrs of role rows`, !badSage.length && !whiteMissing.length, [...badSage, ...whiteMissing].slice(0, 8).join(", "));
|
|
1167
1192
|
if (totalR > 0 && totalC > 0) {
|
|
1168
1193
|
const bandBad = [];
|
|
@@ -1179,9 +1204,9 @@ function auditTeamMix(grid, mfList, tshirtTotalDays, phaseDays) {
|
|
|
1179
1204
|
// ΣCount × 20 × 0.005 from the exact figure — the tolerance follows the roster size.
|
|
1180
1205
|
const tol = 0.05 + 0.1 * (Number.isNaN(sumCount) ? 1 : sumCount);
|
|
1181
1206
|
tolAll += tol;
|
|
1182
|
-
const phase = expectedPhases.find((ph) => title.
|
|
1207
|
+
const phase = expectedPhases.find((ph) => title.trim() === ph);
|
|
1183
1208
|
if (expectedPhases.length) {
|
|
1184
|
-
check(out, `${label}: title
|
|
1209
|
+
check(out, `${label}: title row is exactly the phase name`, !!phase, `title '${title}' is not one of [${expectedPhases.join(", ")}]`);
|
|
1185
1210
|
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)})`);
|
|
1186
1211
|
}
|
|
1187
1212
|
prevBottom = totalR + 1;
|
|
@@ -1361,9 +1386,8 @@ async function verbTeamMix(api, opts) {
|
|
|
1361
1386
|
const layout = locateTshirt(tshirt);
|
|
1362
1387
|
const team = await api.grid(TAB_TEAM);
|
|
1363
1388
|
const fte = opts.fte ? opts.fte : deriveFteFromTeamMix(team);
|
|
1364
|
-
const title =
|
|
1365
|
-
|
|
1366
|
-
const plan = { title, teamMix: { fte } };
|
|
1389
|
+
const title = "";
|
|
1390
|
+
const plan = { teamMix: { fte } };
|
|
1367
1391
|
const rosters = phaseTotalsFromSheet(tshirt, layout).map((ph) => ({ ...ph, roster: buildRoster(plan, ph.staffDays, layout.mf) }));
|
|
1368
1392
|
if (opts.dryRun) return { title, fte, rosters, table: rostersTable(rosters) };
|
|
1369
1393
|
const tm = await writeTeamMix(api, plan, rosters);
|
|
@@ -1476,11 +1500,83 @@ async function verbFormat(api, opts) {
|
|
|
1476
1500
|
fmtReq(sheetId, t1, t1 + 2, cols.total + 1, cols.total + 3, { horizontalAlignment: "RIGHT", numberFormat: { type: "NUMBER", pattern: "0.00" } }, "userEnteredFormat(horizontalAlignment,numberFormat)"),
|
|
1477
1501
|
fmtReq(sheetId, t1 + 2, t1 + 3, cols.total + 1, cols.total + 3, { horizontalAlignment: "RIGHT", numberFormat: { type: "CURRENCY", pattern: "$#,##0.00" } }, "userEnteredFormat(horizontalAlignment,numberFormat)"),
|
|
1478
1502
|
]);
|
|
1503
|
+
// 5. wrap Functionality + Low Level Requirements on item rows; top-align every cell on EVERY tab
|
|
1504
|
+
await api.batch([wrapReq(sheetId, first0, tot0, 2, 4), topAlignReq(sheetId, Math.max(rowCount(grid), t1 + 6))]);
|
|
1505
|
+
const meta = await api.meta();
|
|
1506
|
+
for (const s of meta.sheets) {
|
|
1507
|
+
if (s.properties.title === TAB_TSHIRT) continue;
|
|
1508
|
+
const rows = s.properties.gridProperties && s.properties.gridProperties.rowCount ? Math.min(s.properties.gridProperties.rowCount, 200) : 100;
|
|
1509
|
+
await api.batch([topAlignReq(s.properties.sheetId, rows)]);
|
|
1510
|
+
}
|
|
1479
1511
|
const result = { plan, totalRow1: t1 };
|
|
1480
1512
|
if (!opts.noAudit) result.audit = await runAudit(api);
|
|
1481
1513
|
return result;
|
|
1482
1514
|
}
|
|
1483
1515
|
|
|
1516
|
+
/**
|
|
1517
|
+
* Phases must be contiguous: MVP, then Phase 1, Phase 2, … with no empty phase between used
|
|
1518
|
+
* ones (David, 2026-09-21: "MVP then nothing in Phase 1 but items in Phase 2"). Given the set
|
|
1519
|
+
* of phases that carry items, return the rename map that closes the gaps ({} when none).
|
|
1520
|
+
*/
|
|
1521
|
+
function phaseGapMap(usedPhases) {
|
|
1522
|
+
const numbered = PHASES.filter((p) => p !== "MVP" && usedPhases.includes(p));
|
|
1523
|
+
const map = {};
|
|
1524
|
+
numbered.forEach((p, i) => { const want = `Phase ${i + 1}`; if (p !== want) map[p] = want; });
|
|
1525
|
+
return map;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
/** Renumber phases on the T-Shirt tab to close gaps, then rebuild the Team Mix grids to match. */
|
|
1529
|
+
async function verbPhases(api, opts) {
|
|
1530
|
+
const grid = await api.grid(TAB_TSHIRT);
|
|
1531
|
+
const layout = locateTshirt(grid);
|
|
1532
|
+
const { cols } = layout;
|
|
1533
|
+
const used = new Set();
|
|
1534
|
+
const cells = [];
|
|
1535
|
+
for (let r = layout.firstItemRow; r < rowCount(grid); r++) {
|
|
1536
|
+
if (/^Total \(Days\)/i.test(textAt(grid, r, 0))) break;
|
|
1537
|
+
const v = textAt(grid, r, cols.phase).trim();
|
|
1538
|
+
if (v === "") continue;
|
|
1539
|
+
if (!PHASES.includes(v)) throw new Halt(`${TAB_TSHIRT}: ${colLetter(cols.phase)}${r + 1} holds '${v}', not one of ${PHASES.join(" | ")}`);
|
|
1540
|
+
used.add(v);
|
|
1541
|
+
cells.push({ r, v });
|
|
1542
|
+
}
|
|
1543
|
+
const map = phaseGapMap([...used]);
|
|
1544
|
+
const plan = { phasesUsed: PHASES.filter((p) => used.has(p)), renames: map, cellsToChange: cells.filter((c) => map[c.v]).length };
|
|
1545
|
+
if (opts.dryRun || !Object.keys(map).length) return { plan, changed: false };
|
|
1546
|
+
for (const c of cells) if (map[c.v]) await api.putValues(TAB_TSHIRT, `${colLetter(cols.phase)}${c.r + 1}`, [[map[c.v]]]);
|
|
1547
|
+
const tm = await verbTeamMix(api, { fte: opts.fte, noAudit: true });
|
|
1548
|
+
const result = { plan, changed: true, teamMix: tm.phases };
|
|
1549
|
+
if (!opts.noAudit) result.audit = await runAudit(api);
|
|
1550
|
+
return result;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
/** Set every Team Mix grid's title row to exactly its phase name (rule §2.1). */
|
|
1554
|
+
async function verbTitles(api, opts) {
|
|
1555
|
+
const tshirt = await api.grid(TAB_TSHIRT);
|
|
1556
|
+
const layout = locateTshirt(tshirt);
|
|
1557
|
+
const withHours = phaseTotalsFromSheet(tshirt, layout).map((p) => p.phase);
|
|
1558
|
+
const team = await api.grid(TAB_TEAM);
|
|
1559
|
+
const headers = [];
|
|
1560
|
+
for (let r = 0; r < rowCount(team); r++) if (textAt(team, r, 0) === "Skill set") headers.push(r);
|
|
1561
|
+
if (!headers.length) throw new Halt(`${TAB_TEAM}: no grid found`);
|
|
1562
|
+
if (headers.length !== withHours.length) throw new Halt(`${TAB_TEAM}: ${headers.length} grid(s) but ${withHours.length} phase(s) with hours [${withHours.join(", ")}] — run 'teammix' first`);
|
|
1563
|
+
const changes = [];
|
|
1564
|
+
headers.forEach((h, i) => {
|
|
1565
|
+
if (h < 1) throw new Halt(`${TAB_TEAM}: header at row 1 has no title row above it`);
|
|
1566
|
+
// the title is the nearest non-empty row above the header (older grids keep a blank row 2
|
|
1567
|
+
// between title and header — writing into the blank row left the old title in place)
|
|
1568
|
+
const titleR = textAt(team, h - 1, 0).trim() === "" && h >= 2 && textAt(team, h - 2, 0).trim() !== "" ? h - 2 : h - 1;
|
|
1569
|
+
const cur = textAt(team, titleR, 0).trim();
|
|
1570
|
+
const fromTitle = PHASES.find((p) => cur === p || cur.endsWith(` — ${p}`));
|
|
1571
|
+
const want = fromTitle ? fromTitle : withHours[i];
|
|
1572
|
+
if (fromTitle && fromTitle !== withHours[i]) throw new Halt(`${TAB_TEAM}: grid ${i + 1} is titled for ${fromTitle} but the ${i + 1}${["st", "nd", "rd"][i] || "th"} phase with hours is ${withHours[i]} — run 'teammix' first`);
|
|
1573
|
+
if (cur !== want) changes.push({ row1: titleR + 1, from: cur, to: want });
|
|
1574
|
+
});
|
|
1575
|
+
if (opts.dryRun) return { changes, changed: false };
|
|
1576
|
+
for (const c of changes) await api.putValues(TAB_TEAM, `A${c.row1}`, [[c.to]]);
|
|
1577
|
+
return { changes, changed: changes.length > 0 };
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1484
1580
|
async function verbWrite(api, plan, opts) {
|
|
1485
1581
|
const ts = await writeTshirt(api, plan, opts);
|
|
1486
1582
|
const rosters = buildRosters(plan, ts.layout);
|
|
@@ -1513,7 +1609,7 @@ function printChecks(audit) {
|
|
|
1513
1609
|
console.log(`${audit.ok ? "AUDIT PASS" : `AUDIT FAIL (${audit.failed})`} — ${audit.title}`);
|
|
1514
1610
|
}
|
|
1515
1611
|
|
|
1516
|
-
const USAGE = "usage: gsd-t estimate-sheet <read|plan-check|write|teammix|format|audit|plan-schema> --sheet <id|url> [--tab <name>] [--plan <plan.json>] [--replace] [--fte '{\"backend\":1.5}'] [--title <t>] [--dry-run] [--no-audit] [--key <path>] [--json]";
|
|
1612
|
+
const USAGE = "usage: gsd-t estimate-sheet <read|plan-check|write|teammix|format|phases|titles|audit|plan-schema> --sheet <id|url> [--tab <name>] [--plan <plan.json>] [--replace] [--fte '{\"backend\":1.5}'] [--title <t>] [--dry-run] [--no-audit] [--key <path>] [--json]";
|
|
1517
1613
|
|
|
1518
1614
|
/** Runs a verb; returns the exit code. Throws Halt (or any error) — the runner below turns that into exit 4/64. */
|
|
1519
1615
|
async function main(args) {
|
|
@@ -1542,6 +1638,21 @@ async function main(args) {
|
|
|
1542
1638
|
}
|
|
1543
1639
|
return ok ? 0 : 4;
|
|
1544
1640
|
}
|
|
1641
|
+
if (verb === "phases") {
|
|
1642
|
+
let fte;
|
|
1643
|
+
if (args.fte) { try { fte = JSON.parse(args.fte); } catch (e) { throw new Halt(`--fte must be JSON: ${e.message}`, 64); } }
|
|
1644
|
+
const r = await verbPhases(api, { dryRun: !!args["dry-run"], noAudit: !!args["no-audit"], fte });
|
|
1645
|
+
const ok = !r.audit || r.audit.ok;
|
|
1646
|
+
if (json) console.log(JSON.stringify({ ok, exitCode: ok ? 0 : 4, ...r }, null, 2));
|
|
1647
|
+
else { console.log(JSON.stringify(r.plan)); console.log(r.changed ? "phases renumbered + Team Mix rebuilt" : (Object.keys(r.plan.renames).length ? "(dry run — nothing written)" : "no gap — nothing to do")); if (r.audit) printChecks(r.audit); }
|
|
1648
|
+
return ok ? 0 : 4;
|
|
1649
|
+
}
|
|
1650
|
+
if (verb === "titles") {
|
|
1651
|
+
const r = await verbTitles(api, { dryRun: !!args["dry-run"] });
|
|
1652
|
+
if (json) console.log(JSON.stringify({ ok: true, exitCode: 0, ...r }, null, 2));
|
|
1653
|
+
else { console.log(r.changes.length ? r.changes.map((c) => `A${c.row1}: '${c.from}' → '${c.to}'`).join("\n") : "titles already correct"); if (args["dry-run"] && r.changes.length) console.log("(dry run — nothing written)"); }
|
|
1654
|
+
return 0;
|
|
1655
|
+
}
|
|
1545
1656
|
if (verb === "format") {
|
|
1546
1657
|
const r = await verbFormat(api, { dryRun: !!args["dry-run"], noAudit: !!args["no-audit"] });
|
|
1547
1658
|
const ok = !r.audit || r.audit.ok;
|
|
@@ -1591,7 +1702,7 @@ function haltAndExit(e, json) {
|
|
|
1591
1702
|
|
|
1592
1703
|
module.exports = {
|
|
1593
1704
|
validatePlan, splitRoster, rosterViolations, mfCoverageViolations, monthPlan, resampleWeights, rampHours, buildRoster,
|
|
1594
|
-
tshirtTotals, phaseTotals, buildRosters, midDays, deriveFteFromTeamMix, phaseTotalsFromSheet, verbTeamMix, itemFormulasFor, rollupFormulasFor, findCell, itemFormulas, rollupFormulas, tshirtRows, teamMixValues, teamMixFormatReqs, remainderFormula, locateTshirt, findPhaseSource,
|
|
1705
|
+
tshirtTotals, phaseTotals, buildRosters, midDays, deriveFteFromTeamMix, phaseTotalsFromSheet, verbTeamMix, phaseGapMap, verbPhases, verbTitles, itemFormulasFor, rollupFormulasFor, findCell, itemFormulas, rollupFormulas, tshirtRows, teamMixValues, teamMixFormatReqs, remainderFormula, locateTshirt, findPhaseSource,
|
|
1595
1706
|
auditTshirt, auditTeamMix, auditTechStack, auditOverview, colLetter, hexToColor, colorToHex, sheetIdFromArg,
|
|
1596
1707
|
constants: { SIZE_CODES, PHASES, COLOR, RAMP, ROLE_LABEL, SOFT_CEILING, FOLD_THRESHOLD, TAB_TSHIRT, TAB_TEAM, TAB_TECH, PLAN_SCHEMA },
|
|
1597
1708
|
Halt, SheetsApi, getToken, runAudit, main,
|
|
@@ -18,16 +18,16 @@
|
|
|
18
18
|
* Frozen map: tier alias → concrete model id.
|
|
19
19
|
* Consumers MUST import from here — never re-hardcode these strings.
|
|
20
20
|
*
|
|
21
|
-
* THREE tiers (Fable removed 2026-07-24): `opus` is now `claude-opus-5` — the
|
|
21
|
+
* THREE tiers (Fable removed 2026-07-24): `opus` is now `claude-opus-5-5` — the
|
|
22
22
|
* default top tier. Opus 5 shipped at the SAME price as Opus 4.8 ($5/$25 per M
|
|
23
23
|
* tokens) but >2× its coding score and within 0.5% of Fable 5 at max effort, so
|
|
24
24
|
* the Fable cost premium ($10/$50 — double Opus 5) is no longer justified. Every
|
|
25
|
-
* stage formerly on `fable` OR `opus` (4.8) now runs `opus` = claude-opus-5.
|
|
25
|
+
* stage formerly on `fable` OR `opus` (4.8) now runs `opus` = claude-opus-5-5.
|
|
26
26
|
*
|
|
27
27
|
* @type {Readonly<{opus: string, sonnet: string, haiku: string}>}
|
|
28
28
|
*/
|
|
29
29
|
const MODEL_IDS = Object.freeze({
|
|
30
|
-
opus: 'claude-opus-5',
|
|
30
|
+
opus: 'claude-opus-5-5',
|
|
31
31
|
sonnet: 'claude-sonnet-4-6',
|
|
32
32
|
haiku: 'claude-haiku-4-5-20251001',
|
|
33
33
|
});
|
|
@@ -38,7 +38,7 @@ const MODEL_IDS = Object.freeze({
|
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
40
|
* Frozen map: stage key → tier alias.
|
|
41
|
-
* Fable removed 2026-07-24: all 7 stages resolve to `opus` (= claude-opus-5).
|
|
41
|
+
* Fable removed 2026-07-24: all 7 stages resolve to `opus` (= claude-opus-5-5).
|
|
42
42
|
* The M82 competition judge-blindness invariant is RELAXED from "different model"
|
|
43
43
|
* to "fresh independent context" — producers AND judge both run Opus 5 (fresh
|
|
44
44
|
* contexts remove memory-bias; the modest residual taste/blind-spot bias is
|
|
@@ -67,7 +67,7 @@ const STAGE_TIERS = Object.freeze({
|
|
|
67
67
|
*
|
|
68
68
|
* This predicate existed for `claude-fable-5`, which returned HTTP 400 when the
|
|
69
69
|
* explicit thinking-disabled parameter was sent. Fable was removed 2026-07-24;
|
|
70
|
-
* NO current tier model (opus=claude-opus-5, sonnet, haiku) is known to require
|
|
70
|
+
* NO current tier model (opus=claude-opus-5-5, sonnet, haiku) is known to require
|
|
71
71
|
* omission — Opus 5 and Sonnet 5 default `effort:high` on the API and accept the
|
|
72
72
|
* thinking params normally. Kept as a single-home predicate (callers still import
|
|
73
73
|
* it) so a future model that needs omission is added HERE, never re-hardcoded.
|
|
@@ -116,7 +116,7 @@ function resolve(stageKey) {
|
|
|
116
116
|
* standard — cost-leanest: the high-stakes reasoning stages run sonnet,
|
|
117
117
|
* only the probes stay opus.
|
|
118
118
|
* pro — mid: red-team + pre-mortem + debug-cycle-2 → opus; the rest sonnet.
|
|
119
|
-
* premium — full opus posture: all 6 designated stages → opus (= claude-opus-5).
|
|
119
|
+
* premium — full opus posture: all 6 designated stages → opus (= claude-opus-5-5).
|
|
120
120
|
*
|
|
121
121
|
* competition-producers is held at opus in ALL profiles (always opus-5). The
|
|
122
122
|
* former judge≠producers blindness clamp is REMOVED — the invariant is now
|
|
@@ -161,8 +161,8 @@ const INJECTABLE_STAGES = Object.freeze([
|
|
|
161
161
|
'debug-cycle-2',
|
|
162
162
|
]);
|
|
163
163
|
|
|
164
|
-
/** The HELD producers model id (always opus = claude-opus-5). */
|
|
165
|
-
const PRODUCERS_MODEL_ID = MODEL_IDS.opus; // claude-opus-5
|
|
164
|
+
/** The HELD producers model id (always opus = claude-opus-5-5). */
|
|
165
|
+
const PRODUCERS_MODEL_ID = MODEL_IDS.opus; // claude-opus-5-5
|
|
166
166
|
|
|
167
167
|
/**
|
|
168
168
|
* Resolves the concrete model id for a given stage key under a profile,
|
|
@@ -87,6 +87,7 @@ Reorder items into domains. Insert a **section-heading row** before each group p
|
|
|
87
87
|
|
|
88
88
|
Your judgment is ONE thing: the FTE per discipline (`teamMix.fte` in the plan — `backend` `frontend` `qa` `pm` `ba`, optionally `techlead` `devops`; per-phase override via `teamMix.phases.<phase>.fte`). Everything else on the tab is computed by the tool per spec §2: the split into people (saturate at 1.00, then spill), months, the column count, the ramp by discipline, the remainder formula — and **one grid per phase with hours** (MVP, Phase 1, …), stacked on the one tab with 2 blank rows between.
|
|
89
89
|
|
|
90
|
+
0. **The Team Mix staffs the MIDPOINT of the Low and High estimates** — per phase, `(Low Hrs + High Hrs) ÷ 2 ÷ 8` days from that phase's rollups — never the Low figure. The tool computes it; you do not choose it.
|
|
90
91
|
1. **Staff every weighted MF factor** — QA → `qa`, PM → `pm`, Analysis → `ba`. Deployment / standups / buffer are absorbed by the engineers and lead. Typical fractions: PM 0.20–0.25 · BA 0.10 · Tech Lead 0.25 · QA 0.40–0.50. The tool HALTS on a roster that leaves a factor unstaffed — do not argue with it; add the person.
|
|
91
92
|
2. **Run `gsd-t estimate-sheet plan-check --sheet <url> --plan <plan.json>`.** It prints the MF list it read, the T-Shirt total, and the roster table it WOULD write (person · Count · Mths · Days · Hrs · Mon 1..N with the remainder) — or halts with the violation.
|
|
92
93
|
3. **PAUSE:** present that table verbatim. Wait for `continue` or corrections (a resize or a different FTE → edit the plan, re-run plan-check, present again).
|
package/commands/gsd-t-help.md
CHANGED
|
@@ -536,7 +536,7 @@ Use these when user asks for help on a specific command:
|
|
|
536
536
|
- **Contract**: `.gsd-t/contracts/plan-hardening-contract.md` v1.0.0 STABLE.
|
|
537
537
|
|
|
538
538
|
### model-tier-policy (M85)
|
|
539
|
-
- **Summary**: SINGLE source of truth for GSD-T model-tier assignments. Publishes the authoritative tier set (haiku/sonnet/opus — **Fable removed 2026-07-24**; `opus` = claude-opus-5) and the 7 designated stage→tier mappings (all → opus). Opus 5 shipped at the same price as Opus 4.8 but >2× its coding score, so every stage formerly on Fable OR Opus 4.8 now runs Opus 5. The M82 competition judge-blindness invariant is RELAXED to "fresh independent context" — producers AND judge both run opus. A M71-family lint (`test/m85-workflow-tier-policy-lint.test.js`) proves every workflow `model:` literal matches the policy — a drifted literal FAILS the lint (mandatory negative test).
|
|
539
|
+
- **Summary**: SINGLE source of truth for GSD-T model-tier assignments. Publishes the authoritative tier set (haiku/sonnet/opus — **Fable removed 2026-07-24**; `opus` = claude-opus-5-5) and the 7 designated stage→tier mappings (all → opus). Opus 5 shipped at the same price as Opus 4.8 but >2× its coding score, so every stage formerly on Fable OR Opus 4.8 now runs Opus 5. The M82 competition judge-blindness invariant is RELAXED to "fresh independent context" — producers AND judge both run opus. A M71-family lint (`test/m85-workflow-tier-policy-lint.test.js`) proves every workflow `model:` literal matches the policy — a drifted literal FAILS the lint (mandatory negative test).
|
|
540
540
|
- **Files**: `bin/gsd-t-model-tier-policy.cjs` (zero external deps — installer invariant).
|
|
541
541
|
- **Use when**: Any phase that needs to resolve a concrete model id from a stage key at invoke time (M69 pattern). Workflows NEVER `require` this module (sandbox ban) — they use hard-coded tier alias literals the lint proves match the policy.
|
|
542
542
|
- **CLI**: `gsd-t model-tier-policy resolve <stageKey> [--json]`. Emits `{ok, stageKey, tier, model, requiresThinkingOmitted}`. Exit 0 resolved · 1 unknown stage key.
|
package/commands/gsd-t-status.md
CHANGED
|
@@ -88,7 +88,7 @@ The **Model Profile** line MUST always name the active profile — never blank,
|
|
|
88
88
|
- If the file is absent, display the global default by name with the `(default)` marker: `Model Profile: premium (default)`.
|
|
89
89
|
- If the file is present but malformed or contains an unknown profile, display: `Model Profile: premium (default, config-error)` — never silently promote to the most expensive posture.
|
|
90
90
|
|
|
91
|
-
Profiles control which workflow stages run on Opus vs. Sonnet (Fable removed 2026-07-24 — `opus` = claude-opus-5):
|
|
91
|
+
Profiles control which workflow stages run on Opus vs. Sonnet (Fable removed 2026-07-24 — `opus` = claude-opus-5-5):
|
|
92
92
|
- `standard` — probes opus; high-stakes stages (judge/pre-mortem/red-team/debug-cycle-2) sonnet (cost-leanest)
|
|
93
93
|
- `pro` — probes + pre-mortem + red-team + debug-cycle-2 opus; judge sonnet
|
|
94
94
|
- `premium` — all 6 designated stages opus (full posture, global default)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tekyzinc/gsd-t",
|
|
3
|
-
"version": "5.
|
|
4
|
-
"description": "GSD-T: Contract-Driven Development for Claude Code
|
|
3
|
+
"version": "5.21.10",
|
|
4
|
+
"description": "GSD-T: Contract-Driven Development for Claude Code \u2014 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",
|
|
7
7
|
"repository": {
|
|
@@ -263,7 +263,7 @@ Every GSD-T project gets TWO logging streams scaffolded by default at `gsd-t-ini
|
|
|
263
263
|
Every code-producing phase ends with `gsd-t-verify.workflow.js`, which runs three orthogonal validators as `parallel()` `agent()` stages with schema-validated output. Per `.gsd-t/contracts/orthogonal-validation-contract.md` v1.0.0 STABLE, they are declared orthogonal objective functions — no collapse, no substitution, no transitive trust.
|
|
264
264
|
|
|
265
265
|
- **`/code-review ultra`** — cooperative correctness + cleanup. Severity: `important` / `nit` / `pre-existing`. Skippable via `args.skipUltra=true` + `args.skipUltraReason`. `skipUltra=true` is INELIGIBLE for `VERIFIED`.
|
|
266
|
-
- **Red Team** — adversarial / security / boundaries. Non-skippable. Protocol: `templates/prompts/red-team-subagent.md`. Verdict: `FAIL` (any CRITICAL or HIGH bug — blocks completion) or `GRUDGING-PASS` (exhaustive search, nothing found). CRITICAL/HIGH bugs get up to 2 fix cycles before deferral. Runs on `model: "opus"` (= claude-opus-5; Fable removed 2026-07-24).
|
|
266
|
+
- **Red Team** — adversarial / security / boundaries. Non-skippable. Protocol: `templates/prompts/red-team-subagent.md`. Verdict: `FAIL` (any CRITICAL or HIGH bug — blocks completion) or `GRUDGING-PASS` (exhaustive search, nothing found). CRITICAL/HIGH bugs get up to 2 fix cycles before deferral. Runs on `model: "opus"` (= claude-opus-5-5; Fable removed 2026-07-24).
|
|
267
267
|
- **QA** — test execution + shallow-test detection + contract compliance. Non-skippable. Protocol: `templates/prompts/qa-subagent.md`. Writes ZERO feature code. Any shallow E2E test blocks phase completion. Runs on `model: "sonnet"`.
|
|
268
268
|
|
|
269
269
|
When `.gsd-t/contracts/design-contract.md` or `.gsd-t/contracts/design/` exists, a fourth stage runs Design Verification (protocol: `templates/prompts/design-verify-subagent.md`) — opens a browser, compares the build against the design, returns a structured element-by-element MATCH/DEVIATION schema. Deviations block completion.
|
|
@@ -272,13 +272,13 @@ Synthesis stage merges results without category collapse. Verdict: `VERIFIED` /
|
|
|
272
272
|
|
|
273
273
|
## Model Display (MANDATORY)
|
|
274
274
|
|
|
275
|
-
**Each Workflow `agent()` call declares its model explicitly** via the `model:` option (`"haiku"` / `"sonnet"` / `"opus"` — **Fable removed 2026-07-24**; `opus` = claude-opus-5). The Workflow runtime emits a `⚙ [{model}] {label}` line per stage in `/workflows`, giving the user real-time visibility into which model handles each operation.
|
|
275
|
+
**Each Workflow `agent()` call declares its model explicitly** via the `model:` option (`"haiku"` / `"sonnet"` / `"opus"` — **Fable removed 2026-07-24**; `opus` = claude-opus-5-5). The Workflow runtime emits a `⚙ [{model}] {label}` line per stage in `/workflows`, giving the user real-time visibility into which model handles each operation.
|
|
276
276
|
|
|
277
277
|
**Model assignments:**
|
|
278
278
|
- `model: "haiku"` — strictly mechanical tasks: run test suites and report counts, check file existence, validate JSON structure, branch guard checks
|
|
279
279
|
- `model: "sonnet"` — mid-tier reasoning: routine code changes, standard refactors, test writing, QA evaluation, straightforward synthesis
|
|
280
280
|
- `model: "opus"` — high-stakes reasoning: architecture decisions, security analysis, complex debugging, cross-module refactors, quality judgment on critical paths
|
|
281
|
-
- **Fable removed 2026-07-24** — `opus` (= claude-opus-5) is the top tier and the default for every high-stakes stage: solution-space probe, partition probe, competition judge, pre-mortem, Red Team, competition producers, debug both cycles. Opus 5 shipped at the same price as Opus 4.8 but >2× its coding score, so every stage formerly on Fable OR Opus 4.8 now runs Opus 5. The M82 competition judge-blindness invariant is RELAXED to "fresh independent context" — producers AND judge both run `opus` (fresh contexts remove memory-bias; the modest residual taste/blind-spot bias is accepted for a stronger judge). **Single source of truth for tier assignments:** `bin/gsd-t-model-tier-policy.cjs` + `.gsd-t/contracts/model-tier-policy-contract.md`. The M71-family lint (`test/m85-workflow-tier-policy-lint.test.js`) proves every workflow `model:` literal matches the policy and a drifted literal FAILS the lint (mandatory negative test).
|
|
281
|
+
- **Fable removed 2026-07-24** — `opus` (= claude-opus-5-5) is the top tier and the default for every high-stakes stage: solution-space probe, partition probe, competition judge, pre-mortem, Red Team, competition producers, debug both cycles. Opus 5 shipped at the same price as Opus 4.8 but >2× its coding score, so every stage formerly on Fable OR Opus 4.8 now runs Opus 5. The M82 competition judge-blindness invariant is RELAXED to "fresh independent context" — producers AND judge both run `opus` (fresh contexts remove memory-bias; the modest residual taste/blind-spot bias is accepted for a stronger judge). **Single source of truth for tier assignments:** `bin/gsd-t-model-tier-policy.cjs` + `.gsd-t/contracts/model-tier-policy-contract.md`. The M71-family lint (`test/m85-workflow-tier-policy-lint.test.js`) proves every workflow `model:` literal matches the policy and a drifted literal FAILS the lint (mandatory negative test).
|
|
282
282
|
|
|
283
283
|
**Context budget:** Workflow scripts receive a `budget` global (`budget.total`, `budget.spent()`, `budget.remaining()`) tied to the user's per-turn token target. Use it for dynamic loops (`while (budget.total && budget.remaining() > 50_000) { ... }`) or to scale fleet size. Opus 4.7/4.8 ship 1M context windows; the legacy meter at `bin/token-budget.cjs` was retired in M61 — use native `/context` for live in-session usage.
|
|
284
284
|
|
|
@@ -27,6 +27,8 @@ Reference implementation (read it, don't guess): "ATP SOW Gap Analysis and Estim
|
|
|
27
27
|
| **Map sheet rows to items BY NAME, not by counting.** | Section header rows shift positional alignment and put a whole column on the wrong tasks. |
|
|
28
28
|
| **Recompute the total from raw sizes independently** (`Σ(FE,BE days) × (1 + MF)`) before reporting a number. | A total was reported from a cell just repaired; the true figure differed by 13 days. |
|
|
29
29
|
| **`IMPORTRANGE` `#REF!` on the index sheet needs a human "Allow access" click.** Report it; do not "fix" it. | The service account cannot grant it. |
|
|
30
|
+
| **Every cell on every tab the tool writes is TOP-aligned.** | David, 2026-09-21. |
|
|
31
|
+
| **Functionality and Low Level Requirements wrap** (T-Shirt columns C and D, every item row); Technology Stack descriptions wrap. | Long requirement text was running off the cell. |
|
|
30
32
|
| Access token expires in 1 h — mint fresh per run; regenerate on a sudden 401. | |
|
|
31
33
|
|
|
32
34
|
---
|
|
@@ -65,8 +67,8 @@ Column widths: `[150, 120, 300, 430, 122, 90, 90, 61, 53, 76, 81, 81]`.
|
|
|
65
67
|
|---|---|---|
|
|
66
68
|
| `A` | Module | |
|
|
67
69
|
| `B` | User type | |
|
|
68
|
-
| `C` | Functionality — **include the item id** `(GA-n)` / `(TD-n)` / `(FR-n)` | |
|
|
69
|
-
| `D` | Low-level requirement (one or two sentences) | |
|
|
70
|
+
| `C` | Functionality — **include the item id** `(GA-n)` / `(TD-n)` / `(FR-n)` | **wrap** |
|
|
71
|
+
| `D` | Low-level requirement (one or two sentences) | **wrap** |
|
|
70
72
|
| `E` | Phase — `MVP` / `Phase 1` / `Phase 2` / `Phase 3` | **Carries a `ONE_OF_LIST` validation + chip format. Populate by `copyPaste` (`PASTE_NORMAL`) from an existing MVP cell — a values-write drops the dropdown.** |
|
|
71
73
|
| `F` | Web Portal (frontend) size | **Bare code only: `XS` `S` `M` `L` `XL` `XXL`. Never the legend text `"XS - Extra Small"`.** Blank = 0. |
|
|
72
74
|
| `G` | Backend/API size | same |
|
|
@@ -97,7 +99,7 @@ Total (Days) | … | H =SUM(H14:H<last>) | I =SUM(I…) | J =SUM(J…) | K =SUM(
|
|
|
97
99
|
### 2.1 Layout of ONE grid
|
|
98
100
|
|
|
99
101
|
```
|
|
100
|
-
Row t Title
|
|
102
|
+
Row t Title — EXACTLY the phase name: "MVP" / "Phase 1" / … (merged A:<Total Hrs col>; nothing else in it)
|
|
101
103
|
Row t+1 Header (the ONLY header row in the grid)
|
|
102
104
|
Row t+2.. one row PER PERSON
|
|
103
105
|
Row T Total (T = t + 2 + nroles)
|
|
@@ -138,6 +140,10 @@ The old `Month / Days / Tot Days / Hrs` layout is retired: `Days` IS the per-per
|
|
|
138
140
|
|
|
139
141
|
### 2.6 One grid per phase
|
|
140
142
|
|
|
143
|
+
**Phases are contiguous.** Items use `MVP`, then `Phase 1`, `Phase 2`, `Phase 3` with no empty phase between used ones — `MVP` + `Phase 2` with nothing in `Phase 1` is a defect; renumber (`Phase 2` → `Phase 1`, `Phase 3` → `Phase 2`) so the numbering has no gap (`gsd-t estimate-sheet phases` does this and rebuilds the grids). The audit fails a gap.
|
|
144
|
+
|
|
145
|
+
**The title row of each grid is the phase name and nothing else** — `MVP`, `Phase 1`, … Never the estimate title, never "<title> — Phase 1" (David, 2026-09-21).
|
|
146
|
+
|
|
141
147
|
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`.
|
|
142
148
|
|
|
143
149
|
### 2.3 Roster shape — one row per PERSON
|
|
@@ -212,13 +218,15 @@ Every item is a read-back check against the live sheet. Any ✗ blocks delivery.
|
|
|
212
218
|
T-SHIRT
|
|
213
219
|
[ ] every size cell in F:G is one of XS S M L XL XXL or blank (no legend text)
|
|
214
220
|
[ ] every item row's E cell has ONE_OF_LIST validation
|
|
221
|
+
[ ] C and D wrap on every item row; every cell top-aligned
|
|
215
222
|
[ ] every item row's H:L are the §1.2 formulas (no values)
|
|
216
223
|
[ ] section heading rows are merged A:L, bg #1C4F8B, and hold no sizes
|
|
217
224
|
[ ] totals row + phase rollups (K4:N7) + summary block reference <last item row>
|
|
218
225
|
[ ] totals row directly under the last item, grey band #D8DDE8 bold across A:L; summary block directly under it, labels bold, Total Cost in dollars
|
|
219
226
|
[ ] Σ raw sizes × (1+MF) == Total Days cell (recomputed independently)
|
|
227
|
+
[ ] phases are contiguous — no empty phase between used ones
|
|
220
228
|
TEAM MIX (each grid)
|
|
221
|
-
[ ] one grid per phase with hours; the title
|
|
229
|
+
[ ] one grid per phase with hours; the title row is EXACTLY the phase name; exactly 2 empty, unformatted rows between grids
|
|
222
230
|
[ ] the header is the row under the title; the row under the header is a person
|
|
223
231
|
[ ] header months read Mon 1..Mon N with N == month column count; last header is Total Hrs
|
|
224
232
|
[ ] no Count > 1.00; per discipline, all rows but the last are 1.00
|
|
@@ -251,7 +259,9 @@ gsd-t estimate-sheet audit --sheet <id|url> # §5 checklis
|
|
|
251
259
|
gsd-t estimate-sheet format --sheet <id|url> [--dry-run] # normalise T-Shirt FORMATTING only: section rows (§1.2), grey totals band directly under
|
|
252
260
|
# the last item, standard summary block directly under it (§1.3); no size, phase, text or item formula is touched;
|
|
253
261
|
# rows under the totals row that are not the old summary block are never overwritten (rows are inserted above them)
|
|
254
|
-
gsd-t estimate-sheet
|
|
262
|
+
gsd-t estimate-sheet phases --sheet <id|url> [--dry-run] # close phase gaps on the T-Shirt tab (Phase 2→1, 3→2 …), then rebuild the Team Mix grids
|
|
263
|
+
gsd-t estimate-sheet titles --sheet <id|url> [--dry-run] # set each Team Mix grid's title row to exactly its phase name
|
|
264
|
+
gsd-t estimate-sheet teammix --sheet <id|url> [--fte '{"backend":1.5,…}'] [--dry-run]
|
|
255
265
|
# rebuild the Team Mix (one grid per phase) from the sheet's OWN roster and phase rollups — no plan needed;
|
|
256
266
|
# the roster is derived from the existing grid (entered Counts summed per discipline; older peak-utilisation
|
|
257
267
|
# rosters split the sheet's total FTE by each role's hours); --fte overrides it; halts on a role it cannot map
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
**Report concisely:** verdict/answer first, no preamble. Gloss every code/jargon term (e.g. `M93-D2` = milestone 93, domain 2) in plain words on first use. Bullets over paragraphs. Expand only if asked.
|
|
5
5
|
<!-- /reader-contract -->
|
|
6
6
|
|
|
7
|
-
**Model:** `opus` (= claude-opus-5; Fable removed 2026-07-24 — highest-leverage judgment; separate context from the proposing agent)
|
|
7
|
+
**Model:** `opus` (= claude-opus-5-5; Fable removed 2026-07-24 — highest-leverage judgment; separate context from the proposing agent)
|
|
8
8
|
|
|
9
9
|
**Framing:** You are reviewing someone ELSE's architectural design — you did NOT propose it and have no attachment to it. Your goal is to find the **fatal flaw** in the premise being challenged, before a single line of code is committed to that premise. This framing (independent reviewer, not the author) is essential for escaping self-preference bias: the proposing agent's prior context makes it systematically less able to see its own premise's failures (source: https://arxiv.org/abs/2310.08118 — LLM self-evaluation is biased toward confirming prior outputs; https://arxiv.org/abs/2404.13076 — blind adversarial framing surfaces failures that self-critique misses).
|
|
10
10
|
|
|
@@ -57,7 +57,7 @@ const _args = (typeof args === "string") ? (() => { try { return JSON.parse(args
|
|
|
57
57
|
// Default to {} so the premium fallback literals apply when no invoker injects overrides.
|
|
58
58
|
// overrides values are CONCRETE model ids (resolver envelope); the bare literals below
|
|
59
59
|
// are tier ALIASES. The sandbox runtime accepts BOTH forms in model: — proven live for
|
|
60
|
-
// the tier alias resolves to claude-opus-5 (Fable removed 2026-07-24).
|
|
60
|
+
// the tier alias resolves to claude-opus-5-5 (Fable removed 2026-07-24).
|
|
61
61
|
const overrides = (_args.overrides && typeof _args.overrides === "object") ? _args.overrides : {};
|
|
62
62
|
const _CLI_ENVELOPE_SCHEMA = {
|
|
63
63
|
type: "object", required: ["ok", "exitCode"], additionalProperties: true,
|
|
@@ -83,7 +83,7 @@ const _args = (typeof args === "string") ? (() => { try { return JSON.parse(args
|
|
|
83
83
|
// (preserves byte-identical M85 behavior for callers that have not been updated yet).
|
|
84
84
|
// overrides values are CONCRETE model ids (resolver envelope); the bare literals below
|
|
85
85
|
// are tier ALIASES. The sandbox runtime accepts BOTH forms in model: — proven live for
|
|
86
|
-
// the tier alias resolves to claude-opus-5 (Fable removed 2026-07-24).
|
|
86
|
+
// the tier alias resolves to claude-opus-5-5 (Fable removed 2026-07-24).
|
|
87
87
|
const overrides = (_args.overrides && typeof _args.overrides === "object") ? _args.overrides : {};
|
|
88
88
|
// `envelope` is typed as an OBJECT (or null), not "any".
|
|
89
89
|
//
|
|
@@ -57,7 +57,7 @@ const _args = (typeof args === "string") ? (() => { try { return JSON.parse(args
|
|
|
57
57
|
// Default to {} so the premium fallback literals apply when no invoker injects overrides.
|
|
58
58
|
// overrides values are CONCRETE model ids (resolver envelope); the bare literals below
|
|
59
59
|
// are tier ALIASES. The sandbox runtime accepts BOTH forms in model: — proven live for
|
|
60
|
-
// the tier alias resolves to claude-opus-5 (Fable removed 2026-07-24).
|
|
60
|
+
// the tier alias resolves to claude-opus-5-5 (Fable removed 2026-07-24).
|
|
61
61
|
const overrides = (_args.overrides && typeof _args.overrides === "object") ? _args.overrides : {};
|
|
62
62
|
const _CLI_ENVELOPE_SCHEMA = {
|
|
63
63
|
type: "object", required: ["ok", "exitCode"], additionalProperties: true,
|