@tekyzinc/gsd-t 5.2.10 → 5.3.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 CHANGED
@@ -2,6 +2,40 @@
2
2
 
3
3
  All notable changes to GSD-T are documented here. Updated with each release.
4
4
 
5
+ ## [5.3.10] - 2026-07-24
6
+
7
+ ### Changed — Fable 5 removed; Opus tier upgraded to Opus 5 (claude-opus-5)
8
+
9
+ The 4-tier model map (haiku/sonnet/opus/fable) collapses to **3 tiers (haiku/sonnet/opus)** where `opus` = **`claude-opus-5`**. Opus 5 shipped 2026-07-24 at the SAME price as Opus 4.8 ($5/$25 per M tokens) but >2× its coding score (Frontier-Bench) and within 0.5% of Fable 5 at max effort — so Fable's cost premium ($10/$50, double Opus 5) is no longer justified. **Every stage formerly on Fable OR Opus 4.8 now runs Opus 5.** Opus 5's context window is 1M tokens (same as 4.8 — no large-context regression), knowledge cutoff May 2026.
10
+
11
+ The M82 competition judge-blindness invariant is **RELAXED** from "judge model ≠ producers model" to "judge runs in a fresh independent context" — producers AND judge both run Opus 5 (fresh contexts remove memory-bias; the modest residual taste/blind-spot bias is accepted for a stronger judge). The judge≠producers clamp is removed from the tier policy, the profile resolver, and the drift lint.
12
+
13
+ - `bin/gsd-t-model-tier-policy.cjs`: MODEL_IDS `opus`→`claude-opus-5`, `fable` DELETED; all 7 STAGE_TIERS → opus; `requiresThinkingOmitted` now a single-home no-op (no model requires omission post-Fable); judge blindness clamp removed.
14
+ - `bin/gsd-t-model-profile.cjs`: 3 profiles remapped to dial OPUS-vs-SONNET (standard=probes-opus/rest-sonnet, pro=mid, premium=all-opus); blindness clamp removed.
15
+ - `bin/gsd-t-parallel.cjs`: fable alias removed.
16
+ - `templates/workflows/*.workflow.js` (5): every `model: "fable"` / `?? "fable"` → `"opus"`.
17
+ - `templates/prompts/{blind-adversary,research}-subagent.md`: model annotation fable → opus.
18
+ - Drift-enforcer lints (`test/m85-*`, `test/m86-*`, `test/m90-tier-policy-lint`) updated atomically with the policy — fable fixtures/expectations → opus/sonnet; the "cycles must differ" + "judge≠producers" assertions removed.
19
+ - Contracts: `model-tier-policy-contract.md` v2.0.0 (BREAKING), `model-profile-config-contract.md` v1.1.0 — each with a Fable-removed banner.
20
+ - `templates/CLAUDE-global.md` + live `~/.claude/CLAUDE.md`: Model Display doctrine updated.
21
+ - Command files (`gsd-t-help/status/debug/partition`), README, GSD-T-README updated.
22
+
23
+ Full suite: 3019/3019 pass, 13 skipped. Zero executable `fable` literal remains in bin/workflows/lints; producers AND judge both resolve to `claude-opus-5`; all 3 profiles resolve with no fable.
24
+
25
+ ## [5.2.11] - 2026-07-22
26
+
27
+ ### Changed — architect-overview cleanup (dead tool, shared reader, archived contracts)
28
+
29
+ Applied the three safe findings from a `/gsd-t-architect` framework-overview pass, and recorded a correction to a wrong finding.
30
+
31
+ - Deleted `bin/model-selector.js` + `test/model-selector.test.js` — the M35 model picker with zero live callers, superseded by M85's `bin/gsd-t-model-tier-policy.cjs`. Scrubbed 3 stale `selectModel()` doc references (`gsd-t-quick.md`, `gsd-t-test-sync.md`, `templates/CLAUDE-project.md`, `README.md`).
32
+ - Extracted a shared marker-block *reader* (`extractMarkedDocBlock`) into `bin/gsd-t-doc-marker.cjs` and rewired `bin/gsd-t-env-registry.cjs::extractBlock` to it — finishing the M102 writer/reader extraction. The verify-gate's independent copy is left intact on purpose (defensive-independence invariant — a writer bug must not disable the gate).
33
+ - Archived 13 stale `mXX-integration-points.md` contracts to `.gsd-t/milestones/_integration-points-archive/`. `m94-integration-points.md` was RESTORED — 3 live M94 tests read it by path (a live fixture, not archaeology).
34
+
35
+ Recorded in `progress.md`: the overview's "graph subsystem is broken sprawl" finding was a **false negative** — it checked the pre-M99 path `.gsd-t/graph.db`; the real store moved to `.gsd-t/graphDB/graph.db` (M99 consolidation, already shipped, with a canonical resolver). The graph builds and works in every project. No code change — the correction is the deliverable.
36
+
37
+ Suite: 3020/3020 pass, 13 skipped.
38
+
5
39
  ## [5.2.10] - 2026-07-17
6
40
 
7
41
  ### Added — Environment Registry (M102): the committed, secret-free connection map
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # GSD-T: Contract-Driven Development for Claude Code
2
2
 
3
- **v5.2.10** - A methodology for reliable, parallelizable development using Claude Code with optional Agent Teams support.
3
+ **v5.3.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.
@@ -18,7 +18,7 @@
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
20
  **Visualizer (`/gsd-t-visualize`)** — launches a real-time browser dashboard with dual-pane view: top pane streams the main session, bottom pane streams whichever spawn the user clicks. Left rail shows Live Spawns and Completed (last 100 spawns, status-badged, collapsible). Right rail shows Spawn Plan / Parallelism / Tool Cost. Powered by `gsd-t-stream-feed-server.js` + `gsd-t-dashboard.html`.
21
- **Surgical model selection** — `bin/model-selector.js` assigns haiku/sonnet/opus/fable per phase via a declarative rules table; `/advisor` escalation path with convention-based fallback. **M85 single-source tier policy:** `bin/gsd-t-model-tier-policy.cjs` is the SINGLE source of truth for model-tier assignments; the 5 highest-leverage stages (solution-space probe, partition probe, competition judge, pre-mortem, Red Team) run on `fable` (Claude Fable 5, tier above Opus); competition producers stay `opus` (M82 blindness); debug escalates cycle-1→opus, cycle-2→fable. 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 control which stages run on Fable vs. Opus/Sonnet (see [Model Profiles](#model-profiles) below).
21
+ **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.
22
22
  **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.
23
23
  **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).
24
24
  **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.
@@ -466,7 +466,6 @@ get-stuff-done-teams/
466
466
  │ ├── graph-*.js # Code graph engine (CGC/Neo4j integration)
467
467
  │ ├── journey-coverage.cjs # Listener detector + coverage gap reporting
468
468
  │ ├── playwright-bootstrap.cjs # Idempotent Playwright installer
469
- │ ├── model-selector.js # Phase-to-model assignment (haiku/sonnet/opus/fable)
470
469
  │ ├── gsd-t-model-tier-policy.cjs # M85: single-source tier policy (haiku/sonnet/opus/fable), resolver CLI
471
470
  │ ├── rule-engine.js # Declarative failure-pattern rules
472
471
  │ ├── patch-lifecycle.js # 5-stage patch candidate→graduated lifecycle
@@ -62,4 +62,22 @@ function upsertMarkedDocBlock(targetPath, startMarker, endMarker, block) {
62
62
  return targetPath;
63
63
  }
64
64
 
65
- module.exports = { upsertMarkedDocBlock };
65
+ // Read the marked block back OUT of `content`. Returns the slice from the START
66
+ // marker up to (but NOT including) the END marker — i.e. the block body a parser
67
+ // walks line-by-line — or null if either marker is absent. The complement of
68
+ // upsertMarkedDocBlock's write. (Shared so a reader isn't re-implemented per
69
+ // caller — the env-registry writer/reader used to copy this inline.)
70
+ //
71
+ // NOTE: the verify GATE (gsd-t-env-registry-check.cjs) deliberately does NOT use
72
+ // this — it re-implements the extract independently so a bug here cannot disable
73
+ // the gate (M102 defensive-independence invariant). That duplication is
74
+ // load-bearing; do not "consolidate" it.
75
+ function extractMarkedDocBlock(content, startMarker, endMarker) {
76
+ if (typeof content !== "string" || !startMarker || !endMarker) return null;
77
+ if (!content.includes(startMarker) || !content.includes(endMarker)) return null;
78
+ const start = content.indexOf(startMarker);
79
+ const end = content.indexOf(endMarker);
80
+ return content.slice(start, end);
81
+ }
82
+
83
+ module.exports = { upsertMarkedDocBlock, extractMarkedDocBlock };
@@ -24,7 +24,7 @@
24
24
 
25
25
  const fs = require("fs");
26
26
  const path = require("path");
27
- const { upsertMarkedDocBlock } = require("./gsd-t-doc-marker.cjs");
27
+ const { upsertMarkedDocBlock, extractMarkedDocBlock } = require("./gsd-t-doc-marker.cjs");
28
28
 
29
29
  // ─── Markers + schema ────────────────────────────────────────────────────────
30
30
 
@@ -851,13 +851,11 @@ function infraDocPath(projectDir) {
851
851
  return path.join(projectDir, "docs", "infrastructure.md");
852
852
  }
853
853
 
854
+ // Uses the ONE shared reader from gsd-t-doc-marker.cjs (complement of the shared
855
+ // writer this file already calls) — no inline re-implementation. The verify gate
856
+ // keeps its OWN independent copy on purpose (defensive independence).
854
857
  function extractBlock(content) {
855
- if (!content.includes(ENV_MARKER_START) || !content.includes(ENV_MARKER_END)) {
856
- return null;
857
- }
858
- const start = content.indexOf(ENV_MARKER_START);
859
- const end = content.indexOf(ENV_MARKER_END);
860
- return content.slice(start, end);
858
+ return extractMarkedDocBlock(content, ENV_MARKER_START, ENV_MARKER_END);
861
859
  }
862
860
 
863
861
  function splitRow(line) {
@@ -51,7 +51,7 @@ const GLOBAL_DEFAULT_PROFILE = 'premium';
51
51
  const VALID_PROFILES = Object.keys(PROFILE_STAGE_TIERS); // ['standard','pro','premium']
52
52
 
53
53
  /** Valid tier names. */
54
- const VALID_TIERS = Object.keys(MODEL_IDS); // ['opus','fable','sonnet','haiku']
54
+ const VALID_TIERS = Object.keys(MODEL_IDS); // ['opus','sonnet','haiku'] (Fable removed 2026-07-24)
55
55
 
56
56
  /**
57
57
  * Stages that CANNOT be overridden (M82 blindness invariant).
@@ -216,12 +216,10 @@ function validateSetStage(stageKey, tier) {
216
216
  };
217
217
  }
218
218
 
219
- if (stageKey === 'competition-judge' && MODEL_IDS[tier] === MODEL_IDS.opus) {
220
- return {
221
- ok: false,
222
- error: `competition-judge cannot be set to "${tier}" (resolves to "${MODEL_IDS[tier]}") — judge model must differ from producers' model (M82 blindness invariant)`,
223
- };
224
- }
219
+ // Blindness clamp REMOVED 2026-07-24: the M82 invariant relaxed from "judge
220
+ // model ≠ producers model" to "judge runs in a fresh independent context", so
221
+ // competition-judge = opus (= producers' model) is now ALLOWED. Any valid tier
222
+ // is accepted for the judge.
225
223
 
226
224
  return { ok: true };
227
225
  }
@@ -281,14 +279,9 @@ function buildResolveEnvelope(profile, stageOverrides, specificStage, configErro
281
279
  if (r.configError && !envelopeConfigError) envelopeConfigError = r.configError;
282
280
  }
283
281
 
284
- // Verify competition-judge !== producers' model (final assertion)
285
- if (overrides['competition-judge'] === MODEL_IDS.opus) {
286
- // This should never reach here (resolveProfile clamps it), but defense in depth
287
- envelopeConfigError = envelopeConfigError || 'blindness clamp: competition-judge resolved to producers model — using profile fallback';
288
- const fallbackR = resolveProfile('competition-judge', { profile, stageOverrides: {} });
289
- overrides['competition-judge'] = fallbackR.model;
290
- thinkingMap['competition-judge'] = fallbackR.requiresThinkingOmitted;
291
- }
282
+ // Blindness clamp REMOVED 2026-07-24: competition-judge = opus (= producers'
283
+ // model) is now permitted (invariant relaxed to "fresh independent context").
284
+ // No rewrite of the judge model.
292
285
 
293
286
  const result = {
294
287
  ok: true,
@@ -18,11 +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
- * @type {Readonly<{opus: string, fable: string, sonnet: string, haiku: string}>}
21
+ * THREE tiers (Fable removed 2026-07-24): `opus` is now `claude-opus-5` — the
22
+ * default top tier. Opus 5 shipped at the SAME price as Opus 4.8 ($5/$25 per M
23
+ * tokens) but >2× its coding score and within 0.5% of Fable 5 at max effort, so
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.
26
+ *
27
+ * @type {Readonly<{opus: string, sonnet: string, haiku: string}>}
22
28
  */
23
29
  const MODEL_IDS = Object.freeze({
24
- opus: 'claude-opus-4-8',
25
- fable: 'claude-fable-5',
30
+ opus: 'claude-opus-5',
26
31
  sonnet: 'claude-sonnet-4-6',
27
32
  haiku: 'claude-haiku-4-5-20251001',
28
33
  });
@@ -33,18 +38,23 @@ const MODEL_IDS = Object.freeze({
33
38
 
34
39
  /**
35
40
  * Frozen map: stage key → tier alias.
36
- * 6 stages fable; competition-producers held at opus (M82 blindness invariant).
41
+ * Fable removed 2026-07-24: all 7 stages resolve to `opus` (= claude-opus-5).
42
+ * The M82 competition judge-blindness invariant is RELAXED from "different model"
43
+ * to "fresh independent context" — producers AND judge both run Opus 5 (fresh
44
+ * contexts remove memory-bias; the modest residual taste/blind-spot bias is
45
+ * accepted for a stronger judge — user-locked 2026-07-24). See
46
+ * competition-mode-contract.md §Different-context judge.
37
47
  *
38
48
  * @type {Readonly<Record<string, string>>}
39
49
  */
40
50
  const STAGE_TIERS = Object.freeze({
41
- 'solution-space-probe': 'fable',
42
- 'partition-probe': 'fable',
43
- 'competition-judge': 'fable',
44
- 'competition-producers': 'opus', // HELD — M82 judge-blindness invariant; do NOT move to fable
45
- 'pre-mortem': 'fable',
46
- 'red-team': 'fable',
47
- 'debug-cycle-2': 'fable',
51
+ 'solution-space-probe': 'opus',
52
+ 'partition-probe': 'opus',
53
+ 'competition-judge': 'opus', // was fable; blindness now via fresh context, not different model
54
+ 'competition-producers': 'opus',
55
+ 'pre-mortem': 'opus',
56
+ 'red-team': 'opus',
57
+ 'debug-cycle-2': 'opus',
48
58
  });
49
59
 
50
60
  // ---------------------------------------------------------------------------
@@ -55,18 +65,22 @@ const STAGE_TIERS = Object.freeze({
55
65
  * Returns true IFF the model requires the explicit thinking-disabled parameter
56
66
  * to be OMITTED from the API call.
57
67
  *
58
- * Rationale (canonical, single home): `claude-fable-5` returns HTTP 400 when
59
- * the explicit thinking-disabled parameter is sent. The parameter must therefore
60
- * be OMITTED for Fable. No other file may re-implement or re-state this predicate.
68
+ * This predicate existed for `claude-fable-5`, which returned HTTP 400 when the
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
71
+ * omission — Opus 5 and Sonnet 5 default `effort:high` on the API and accept the
72
+ * thinking params normally. Kept as a single-home predicate (callers still import
73
+ * it) so a future model that needs omission is added HERE, never re-hardcoded.
61
74
  *
62
75
  * @param {string} model — concrete model id or tier alias or any string
63
76
  * @returns {boolean}
64
77
  */
78
+ const MODELS_REQUIRING_THINKING_OMITTED = Object.freeze([]); // none post-Fable
65
79
  function requiresThinkingOmitted(model) {
66
80
  if (typeof model !== 'string') return false;
67
- // Source the id from MODEL_IDS (single-source — no second literal), and accept
68
- // the runtime's bracket-suffixed display form (e.g. "claude-fable-5[1m]").
69
- return model === MODEL_IDS.fable || model.startsWith(MODEL_IDS.fable + '[');
81
+ return MODELS_REQUIRING_THINKING_OMITTED.some(
82
+ (id) => model === id || model.startsWith(id + '[')
83
+ );
70
84
  }
71
85
 
72
86
  // ---------------------------------------------------------------------------
@@ -98,16 +112,15 @@ function resolve(stageKey) {
98
112
  /**
99
113
  * Frozen profile → stage-key → tier map.
100
114
  *
101
- * Three named profiles:
102
- * standard — ZERO fable (pre-M85 tiers: probes→opus, judge→sonnet,
103
- * pre-mortem→opus, red-team→opus, debug both cycles→opus).
104
- * pro — red-team + pre-mortem + debug-cycle-2 → fable; everything
105
- * else reverts to standard.
106
- * premium — all 6 M85 fable stages (the full M85 posture).
115
+ * Fable removed 2026-07-24 — profiles now dial OPUS-vs-SONNET spend (not Fable):
116
+ * standard — cost-leanest: the high-stakes reasoning stages run sonnet,
117
+ * only the probes stay opus.
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).
107
120
  *
108
- * competition-producers is HELD at opus in ALL profiles (M82 blindness
109
- * invariant never fable). It is NOT included here because it is never
110
- * overridable; the resolver enforces this separately.
121
+ * competition-producers is held at opus in ALL profiles (always opus-5). The
122
+ * former judge≠producers blindness clamp is REMOVED the invariant is now
123
+ * "fresh independent context," so the judge may equal producers' model.
111
124
  *
112
125
  * @type {Readonly<Record<string, Readonly<Record<string, string>>>>}
113
126
  */
@@ -116,25 +129,25 @@ const PROFILE_STAGE_TIERS = Object.freeze({
116
129
  'solution-space-probe': 'opus',
117
130
  'partition-probe': 'opus',
118
131
  'competition-judge': 'sonnet',
119
- 'pre-mortem': 'opus',
120
- 'red-team': 'opus',
121
- 'debug-cycle-2': 'opus',
132
+ 'pre-mortem': 'sonnet',
133
+ 'red-team': 'sonnet',
134
+ 'debug-cycle-2': 'sonnet',
122
135
  }),
123
136
  pro: Object.freeze({
124
137
  'solution-space-probe': 'opus',
125
138
  'partition-probe': 'opus',
126
139
  'competition-judge': 'sonnet',
127
- 'pre-mortem': 'fable',
128
- 'red-team': 'fable',
129
- 'debug-cycle-2': 'fable',
140
+ 'pre-mortem': 'opus',
141
+ 'red-team': 'opus',
142
+ 'debug-cycle-2': 'opus',
130
143
  }),
131
144
  premium: Object.freeze({
132
- 'solution-space-probe': 'fable',
133
- 'partition-probe': 'fable',
134
- 'competition-judge': 'fable',
135
- 'pre-mortem': 'fable',
136
- 'red-team': 'fable',
137
- 'debug-cycle-2': 'fable',
145
+ 'solution-space-probe': 'opus',
146
+ 'partition-probe': 'opus',
147
+ 'competition-judge': 'opus',
148
+ 'pre-mortem': 'opus',
149
+ 'red-team': 'opus',
150
+ 'debug-cycle-2': 'opus',
138
151
  }),
139
152
  });
140
153
 
@@ -148,18 +161,19 @@ const INJECTABLE_STAGES = Object.freeze([
148
161
  'debug-cycle-2',
149
162
  ]);
150
163
 
151
- /** The HELD producers model id used by blindness clamps. */
152
- const PRODUCERS_MODEL_ID = MODEL_IDS.opus; // claude-opus-4-8
164
+ /** The HELD producers model id (always opus = claude-opus-5). */
165
+ const PRODUCERS_MODEL_ID = MODEL_IDS.opus; // claude-opus-5
153
166
 
154
167
  /**
155
168
  * Resolves the concrete model id for a given stage key under a profile,
156
169
  * honoring precedence: stageOverrides[stage] ?? profile-tier ?? global-default.
157
170
  *
158
- * Blindness clamps (M82 / pre-mortem c2 #4 enforced at RESOLVE, not only at
159
- * write time because the config file is hand-editable):
160
- * - competition-producers key in stageOverrides: silently dropped (never in overrides map).
161
- * - competition-judge resolved to the producers' model id: BLOCKEDdrops the override
162
- * and uses the profile tier for competition-judge instead.
171
+ * Blindness (M82, RELAXED 2026-07-24 to "fresh independent context"):
172
+ * - competition-producers key in stageOverrides: still silently dropped (producers
173
+ * are always opus not profile-overridable).
174
+ * - competition-judge may now equal the producers' model (both opus) — the old
175
+ * judge≠producers clamp is REMOVED; isolation is enforced by fresh context, not
176
+ * by a different model.
163
177
  *
164
178
  * @param {string} stageKey
165
179
  * @param {{ profile?: string, stageOverrides?: Record<string,string> }} opts
@@ -222,11 +236,10 @@ function resolveProfile(stageKey, opts) {
222
236
  // (Red Team M86 r2 LOW: unknown stage + invalid override resolved fable on standard).
223
237
  errors.push(`stageOverrides["${stageKey}"] has invalid tier "${rawOverrideTier}"; falling back to profile tier`);
224
238
  resolvedTier = stageKnown ? profileTierMap[stageKey] : 'sonnet';
225
- } else if (stageKey === 'competition-judge' && MODEL_IDS[rawOverrideTier] === PRODUCERS_MODEL_ID) {
226
- // Blindness clamp: competition-judge must not equal producers' model
227
- errors.push(`stageOverrides["competition-judge"] resolves to "${MODEL_IDS[rawOverrideTier]}" (=producers' model); blindness clamp rejected — falling back to profile tier`);
228
- resolvedTier = stageKnown ? profileTierMap[stageKey] : 'sonnet';
229
239
  } else {
240
+ // Blindness clamp REMOVED (2026-07-24): competition-judge may equal the
241
+ // producers' model — isolation is now enforced by fresh context, not a
242
+ // different model. Any valid tier override is honored.
230
243
  resolvedTier = rawOverrideTier;
231
244
  }
232
245
  } else {
@@ -610,10 +610,9 @@ function runDispatch(opts) {
610
610
  // is sufficient (single-source thesis, AC b).
611
611
  const modelAlias = {
612
612
  opus: MODEL_IDS.opus,
613
- fable: MODEL_IDS.fable,
614
613
  sonnet: MODEL_IDS.sonnet,
615
614
  haiku: MODEL_IDS.haiku,
616
- };
615
+ }; // fable removed 2026-07-24
617
616
  const callerModel = opts && opts.workerModel;
618
617
  const workerModel = callerModel === false
619
618
  ? null // explicit opt-out: inherit parent's ANTHROPIC_MODEL
@@ -62,7 +62,7 @@ Call the `Workflow` tool with:
62
62
  symptom: "describe the failing test or runtime error in one sentence",
63
63
  projectDir: ".",
64
64
  // M86: inject the resolved overrides map so the workflow's ?? form for debug-cycle-2
65
- // picks up the profile-tier assignment instead of the premium fable literal.
65
+ // picks up the profile-tier assignment instead of the premium opus literal.
66
66
  // Pass {} when the resolver failed AND you chose the loud-warning path (not halt).
67
67
  overrides: { /* ...from resolver result.overrides, or {} on failure */ }
68
68
  }
@@ -519,16 +519,16 @@ Use these when user asks for help on a specific command:
519
519
  - **Contract**: `.gsd-t/contracts/plan-hardening-contract.md` v1.0.0 STABLE.
520
520
 
521
521
  ### model-tier-policy (M85)
522
- - **Summary**: SINGLE source of truth for GSD-T model-tier assignments. Publishes the authoritative tier set (haiku/sonnet/opus/fable), the 7 designated stage→tier mappings, and the `requiresThinkingOmitted(model)` predicate (encoding Fable's HTTP-400 breaking change ONCE). M85 slots the Fable tier into the 5 highest-leverage stages (solution-space probe, partition probe, competition judge, pre-mortem, Red Team) where one call's judgment gates the most downstream spend; competition producers STAY opus (M82 blindness invariant); debug cycle-1→opus, cycle-2→fable. 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).
522
+ - **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).
523
523
  - **Files**: `bin/gsd-t-model-tier-policy.cjs` (zero external deps — installer invariant).
524
524
  - **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.
525
525
  - **CLI**: `gsd-t model-tier-policy resolve <stageKey> [--json]`. Emits `{ok, stageKey, tier, model, requiresThinkingOmitted}`. Exit 0 resolved · 1 unknown stage key.
526
526
  - **Contract**: `.gsd-t/contracts/model-tier-policy-contract.md` v1.1.0 STABLE.
527
527
 
528
528
  ### model-profile (M86)
529
- - **Summary**: Per-project model-tier profile switcher — a SECOND dimension over the M85 stage-tier policy. Manages `.gsd-t/model-profile.json` and resolves which concrete model id each workflow stage runs on under the active profile. Three named profiles: `standard` (zero Fablepre-M85 posture), `pro` (Fable on red-team + pre-mortem + debug-cycle-2), `premium` (all 6 M85 designated Fable stages — global default). Switching profiles injects the overrides into workflow args at invoke time (M69 pattern — NO tracked-file rewriting). Competition producers are HELD at opus in ALL profiles (M82 blindness invariant). The active profile is surfaced in the session banner (`[GSD-T PROFILE]`), the statusline, and `gsd-t status` — always named, never an implicit fallback (SC(f)).
529
+ - **Summary**: Per-project model-tier profile switcher — a SECOND dimension over the stage-tier policy. Manages `.gsd-t/model-profile.json` and resolves which concrete model id each workflow stage runs on under the active profile. Three named profiles (Fable removed 2026-07-24 — they now dial OPUS-vs-SONNET spend): `standard` (probes opus, high-stakes stages sonnet cost-leanest), `pro` (probes + pre-mortem + red-team + debug-cycle-2 opus, judge sonnet), `premium` (all 6 designated stages opus — global default). Switching profiles injects the overrides into workflow args at invoke time (M69 pattern — NO tracked-file rewriting). Competition producers are held at opus in ALL profiles. The active profile is surfaced in the session banner (`[GSD-T PROFILE]`), the statusline, and `gsd-t status` — always named, never an implicit fallback (SC(f)).
530
530
  - **Files**: `bin/gsd-t-model-profile.cjs` (zero external deps — installer invariant). Config: `.gsd-t/model-profile.json` (`{ "profile": "pro", "stageOverrides": { ... } }`).
531
- - **Use when**: A project needs a different spend posture than the global default (e.g., run `standard` on a CI cost budget, or `pro` for a production release with targeted Fable quality gates). Per-stage overrides allow fine-grained control beyond the named profiles.
531
+ - **Use when**: A project needs a different spend posture than the global default (e.g., run `standard` on a CI cost budget, or `pro` for a production release with targeted opus quality gates). Per-stage overrides allow fine-grained control beyond the named profiles.
532
532
  - **CLI**:
533
533
  - `gsd-t model-profile show [--json]` — display active profile + per-stage resolution
534
534
  - `gsd-t model-profile set <standard|pro|premium>` — switch the project profile
@@ -540,7 +540,7 @@ Use these when user asks for help on a specific command:
540
540
  - **Contract**: `.gsd-t/contracts/model-profile-config-contract.md` v1.0.0 STABLE.
541
541
 
542
542
  ### research-gate (M89)
543
- - **Summary**: Deterministic CLASSIFY step of the M89 auto-research pipeline. Takes a load-bearing claim the agent tagged `[GUESSED:*]` in its Stated Claims section. The classifier is a MECHANICAL STRING-FACT FILTER (NOT a semantic oracle) returning one of THREE classes: `internal` (grep/Read, route `grep`), `external` (web-research, route `web`), or `ambiguous` (route `judge`). No LLM call in the classifier itself — semantic judgment is deferred to the wiring's LLM judge. An `ambiguous` claim is routed to a small LLM `classify-judge` stage (model: "fable") that decides internal/external/uncertain; **uncertain → research** (never guess-internal). The research `agent()` stage (model: "fable") runs for external guesses. An ENFORCE marker (`<!-- auto-research-claim: ... status=uncited -->`) is written into the artifact (a deterministic `.gsd-t/research/...` fallback if the stage reports no artifact path — fail-closed); the verify gate FAILs if it stays uncited.
543
+ - **Summary**: Deterministic CLASSIFY step of the M89 auto-research pipeline. Takes a load-bearing claim the agent tagged `[GUESSED:*]` in its Stated Claims section. The classifier is a MECHANICAL STRING-FACT FILTER (NOT a semantic oracle) returning one of THREE classes: `internal` (grep/Read, route `grep`), `external` (web-research, route `web`), or `ambiguous` (route `judge`). No LLM call in the classifier itself — semantic judgment is deferred to the wiring's LLM judge. An `ambiguous` claim is routed to a small LLM `classify-judge` stage (model: "opus") that decides internal/external/uncertain; **uncertain → research** (never guess-internal). The research `agent()` stage (model: "opus") runs for external guesses. An ENFORCE marker (`<!-- auto-research-claim: ... status=uncited -->`) is written into the artifact (a deterministic `.gsd-t/research/...` fallback if the stage reports no artifact path — fail-closed); the verify gate FAILs if it stays uncited.
544
544
  - **Files**: `bin/gsd-t-research-gate.cjs` (D1). Contract: `.gsd-t/contracts/auto-research-contract.md` v1.3.2 STABLE. Stage prompt: `templates/prompts/research-subagent.md`. DETECT snippet: `templates/prompts/stated-claims-snippet.md`. Cite-format test: `test/m89-research-stage-cite-format.test.js`.
545
545
  - **Use when**: A workflow stage needs to classify a guessed claim before deciding whether to grep, run a web-research sub-agent, or route to the LLM judge. Called by D3 (upper phases: plan, pre-mortem, partition, discuss, milestone) and D4 (worker phases: execute, debug, quick). NOT called manually — the wiring invokes it per `[GUESSED:*]` entry in the `## Stated Claims` section.
546
546
  - **CLI**: `gsd-t research-gate classify "<guessed claim text>"`. Emits `{ok, gap, class, route, reason}` where `class` ∈ {internal, external, ambiguous} and `route` ∈ {grep, web, judge}. Exit 0 on classify · 1 on bad input.
@@ -549,7 +549,7 @@ Use these when user asks for help on a specific command:
549
549
 
550
550
  ### architectural-trigger (M90)
551
551
  - **Summary**: Deterministic architectural-assumption trigger (M90 §2). Two fire paths: **R-ARCH-1** (divergence-sampling — N fresh-context producer answers → divergence score; competition-arm only, EXPERIMENTAL+MEASURED) and **R-ARCH-2** (protocol-class — any task whose `**Touches**` lists an EXISTING file triggers unconditionally; everywhere feed). Fires produce an instrumentation record in `.gsd-t/metrics/arch-trigger-events.jsonl`; a `provenByAdversaryOnly=true` flag from either path surfaces at the §4 fail-closed verify gate (R-FAIL-2). The module NEVER asserts it works — it emits measurement data only.
552
- - **Files**: `bin/gsd-t-architectural-trigger.cjs`. Contract: `.gsd-t/contracts/unproven-assumption-doctrine-contract.md` §2. Blind-adversary prompt: `templates/prompts/blind-adversary-subagent.md`. Model: `fable` (M85 RULE-ARCH-TIER).
552
+ - **Files**: `bin/gsd-t-architectural-trigger.cjs`. Contract: `.gsd-t/contracts/unproven-assumption-doctrine-contract.md` §2. Blind-adversary prompt: `templates/prompts/blind-adversary-subagent.md`. Model: `opus` (RULE-ARCH-TIER; Fable removed 2026-07-24).
553
553
  - **Use when**: Workflows detect that a task extends existing code (R-ARCH-2 everywhere feed) or that competition producers diverged (R-ARCH-1 competition-arm only). Called automatically by `gsd-t-execute`, `gsd-t-quick`, and `gsd-t-phase` competition arm via inline runCli helper (M81 runtime-native).
554
554
  - **CLI**: `gsd-t architectural-trigger trigger '<JSON>'`. JSON shapes: `{"type":"extend-existing-code","context":"...","basis":"..."}` or `{"type":"divergence-sampling","answers":[...],"basis":"..."}`. Emits `{ok, firePath, fired, basis, reason, ...}`. Exit 0 on success · 1 on bad input.
555
555
  - **Contract**: `.gsd-t/contracts/unproven-assumption-doctrine-contract.md` §2 v1.0.0 STABLE.
@@ -58,7 +58,7 @@ binary), do NOT silently proceed on the premium fallback. Either:
58
58
  (configured profile unknown; stale global binary may lack model-profile subcommand)
59
59
  ```
60
60
 
61
- A configured-standard project silently billing premium fable post-promo is the exact inverse of
61
+ A configured-standard project silently billing premium opus post-promo is the exact inverse of
62
62
  the spend-switch goal. Never silently fall through.
63
63
 
64
64
  Also surface a SUCCESSFUL resolve that carries a `configError` field (the resolver returns a
@@ -25,8 +25,8 @@ Spawn path is `autoSpawnHeadless({command, spawnType, projectDir, sessionContext
25
25
 
26
26
  Per `.gsd-t/contracts/model-selection-contract.md` v1.0.0.
27
27
 
28
- - **Default**: `sonnet` (`selectModel({phase: "quick"})`) — routine one-off task.
29
- - **Mechanical subroutines** (demote to `haiku`): test runners (`selectModel({phase: "quick", task_type: "test_runner"})`).
28
+ - **Default**: `sonnet` (per `bin/gsd-t-model-tier-policy.cjs`, phase `quick`) — routine one-off task.
29
+ - **Mechanical subroutines** (demote to `haiku`): test runners.
30
30
  - **Red Team (Step 5.5)**: `opus` — adversarial reasoning always runs at top tier.
31
31
  - **Escalation**: `/advisor` convention-based fallback from `bin/advisor-integration.js` at declared high-stakes sub-decisions (see `.gsd-t/M35-advisor-findings.md`). Never silently downgrade the model or skip Red Team / doc-ripple under context pressure — M35 removed that behavior.
32
32
 
@@ -81,10 +81,10 @@ The **Model Profile** line MUST always name the active profile — never blank,
81
81
  - If the file is absent, display the global default by name with the `(default)` marker: `Model Profile: premium (default)`.
82
82
  - 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.
83
83
 
84
- Profiles control which workflow stages run on Fable vs. Opus/Sonnet:
85
- - `standard` — zero Fable stages (pre-M85 posture)
86
- - `pro` — Fable on red-team + pre-mortem + debug-cycle-2
87
- - `premium` — all 6 M85 designated Fable stages (full posture, global default)
84
+ Profiles control which workflow stages run on Opus vs. Sonnet (Fable removed 2026-07-24 — `opus` = claude-opus-5):
85
+ - `standard` — probes opus; high-stakes stages (judge/pre-mortem/red-team/debug-cycle-2) sonnet (cost-leanest)
86
+ - `pro` — probes + pre-mortem + red-team + debug-cycle-2 opus; judge sonnet
87
+ - `premium` — all 6 designated stages opus (full posture, global default)
88
88
 
89
89
  ### Backlog Section
90
90
 
@@ -6,7 +6,7 @@ You are maintaining test coverage as code changes. Your job is to identify stale
6
6
 
7
7
  Per `.gsd-t/contracts/model-selection-contract.md` v1.0.0.
8
8
 
9
- - **Default**: `sonnet` (`selectModel({phase: "test-sync"})`) — test alignment is routine refactoring work.
9
+ - **Default**: `sonnet` (per `bin/gsd-t-model-tier-policy.cjs`, phase `test-sync`) — test alignment is routine refactoring work.
10
10
  - **Escalation**: `/advisor` convention-based fallback from `bin/advisor-integration.js` when a test touches a contract boundary or requires judgment about what "missing coverage" means. Never silently skip test-sync under context pressure — M35 removed that behavior.
11
11
 
12
12
  This command is:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekyzinc/gsd-t",
3
- "version": "5.2.10",
3
+ "version": "5.3.10",
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",
@@ -251,7 +251,7 @@ Every GSD-T project gets TWO logging streams scaffolded by default at `gsd-t-ini
251
251
  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.
252
252
 
253
253
  - **`/code-review ultra`** — cooperative correctness + cleanup. Severity: `important` / `nit` / `pre-existing`. Skippable via `args.skipUltra=true` + `args.skipUltraReason`. `skipUltra=true` is INELIGIBLE for `VERIFIED`.
254
- - **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: "fable"` (M85).
254
+ - **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).
255
255
  - **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"`.
256
256
 
257
257
  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.
@@ -260,13 +260,13 @@ Synthesis stage merges results without category collapse. Verdict: `VERIFIED` /
260
260
 
261
261
  ## Model Display (MANDATORY)
262
262
 
263
- **Each Workflow `agent()` call declares its model explicitly** via the `model:` option (`"haiku"` / `"sonnet"` / `"opus"` / `"fable"`). The Workflow runtime emits a `⚙ [{model}] {label}` line per stage in `/workflows`, giving the user real-time visibility into which model handles each operation.
263
+ **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.
264
264
 
265
265
  **Model assignments:**
266
266
  - `model: "haiku"` — strictly mechanical tasks: run test suites and report counts, check file existence, validate JSON structure, branch guard checks
267
267
  - `model: "sonnet"` — mid-tier reasoning: routine code changes, standard refactors, test writing, QA evaluation, straightforward synthesis
268
268
  - `model: "opus"` — high-stakes reasoning: architecture decisions, security analysis, complex debugging, cross-module refactors, quality judgment on critical paths
269
- - `model: "fable"`highest-stakes calls where one judgment gates the most downstream spend (M85): solution-space probe, partition probe, competition judge, pre-mortem, Red Team. Competition producers STAY `opus` (M82 blindness invariant judge must differ from producers). Debug cycle-1 `opus`, cycle-2 `fable` (escalation). **Single source of truth for tier assignments:** `bin/gsd-t-model-tier-policy.cjs` + `.gsd-t/contracts/model-tier-policy-contract.md` v1.0.0 STABLE. 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).
269
+ - **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).
270
270
 
271
271
  **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.
272
272
 
@@ -421,9 +421,9 @@ For every load-bearing claim, tag it:
421
421
  - **`[GUESSED:stale]`** — external/time-varying fact that may have changed.
422
422
 
423
423
  A `[GUESSED:*]` claim is then CLASSIFIED — mechanical string-fact filter, three classes:
424
- - **`class: external`** → research agent (`model: "fable"`) writes a `## Verified Facts (auto-research)` block (URL + fetch date); ENFORCE marker `<!-- auto-research-claim: class=external key=<key> status=uncited -->` is written; verify FAILs if it stays `status=uncited` (R-FAIL-1).
424
+ - **`class: external`** → research agent (`model: "opus"`) writes a `## Verified Facts (auto-research)` block (URL + fetch date); ENFORCE marker `<!-- auto-research-claim: class=external key=<key> status=uncited -->` is written; verify FAILs if it stays `status=uncited` (R-FAIL-1).
425
425
  - **`class: internal`** → grep/Read only; escalate to external if grep empty.
426
- - **`class: ambiguous`** → LLM judge (`model: "fable"`) decides; uncertain → research (never guess-internal).
426
+ - **`class: ambiguous`** → LLM judge (`model: "opus"`) decides; uncertain → research (never guess-internal).
427
427
 
428
428
  **§2 — Architectural Trigger (`gsd-t architectural-trigger trigger '<JSON>'`):**
429
429
  Two fire paths — both INSTRUMENTED (fire-rate emitted to `.gsd-t/metrics/arch-trigger-events.jsonl`):
@@ -23,7 +23,7 @@ Contract-driven development methodology for Claude Code. npm package providing s
23
23
 
24
24
  ```
25
25
  bin/ — CLI entry (gsd-t.js) + orchestrators (orchestrator.js, design-orchestrator.js)
26
- + support modules (gsd-t-context-brief.cjs, cli-preflight.cjs, gsd-t-verify-gate.cjs, model-selector.js, …)
26
+ + support modules (gsd-t-context-brief.cjs, cli-preflight.cjs, gsd-t-verify-gate.cjs, gsd-t-model-tier-policy.cjs, …)
27
27
  commands/ — slash commands for Claude Code (GSD-T workflow + utility)
28
28
  templates/ — document + prompt + stack templates
29
29
  CLAUDE-{global,project}.md, requirements.md, architecture.md, workflows.md,
@@ -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:** `fable` (M85 tier policy — highest-leverage judgment; separate context from the proposing agent)
7
+ **Model:** `opus` (= claude-opus-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
 
@@ -32,7 +32,7 @@ You are NOT checking whether the code is correct. You are attacking the **archit
32
32
  3. **Has someone else solved this class of problem? How?** Grep the repo, check the brief, research prior art. Do NOT confirm the premise by default — search for evidence that it is WRONG or that a different approach is clearly superior.
33
33
  4. **What is the minimal falsifying test?** What single experiment would prove the premise is wrong? Propose it.
34
34
 
35
- ## Extends M83 Pre-Mortem + Red-Team-on-fable
35
+ ## Extends M83 Pre-Mortem + Red-Team-on-opus
36
36
 
37
37
  This prompt is the **architectural dual** of the M83 pre-mortem: the pre-mortem attacks a PLAN before execution; this prompt attacks an ARCHITECTURAL PREMISE before the plan is written. It also extends the Red Team (which attacks finished code at verify) by moving the adversarial review LEFT to the assumption stage.
38
38
 
@@ -6,7 +6,7 @@ ZERO feature code writes. You do NOT answer questions about what the claim PROBA
6
6
  it up and cite it.
7
7
 
8
8
  <!-- M89 — Workflow-stage invocation -->
9
- **Invocation context.** This protocol runs as a native Workflow `agent()` stage (bare `model: "fable"`,
9
+ **Invocation context.** This protocol runs as a native Workflow `agent()` stage (bare `model: "opus"`,
10
10
  Fable tier — the single highest-leverage web call per phase). Your **final emission MUST be a single
11
11
  StructuredOutput object** matching the schema declared by the calling Workflow. The Verified-Facts block
12
12
  is embedded in the artifact; the StructuredOutput envelope carries the block text + the gap-key.
@@ -7,11 +7,11 @@
7
7
  // M89: Stated-Claims→classify→research wiring (auto-research-contract v1.2.0 §6.5/§1/§2/§3/§4/§7).
8
8
  // Each debug cycle embeds the Stated-Claims snippet (§6.5) so the debug agent tags failure-root
9
9
  // claims KNOWN|GUESSED. After each cycle, GUESSED claims are classified: external failure-root
10
- // → write §7 marker (status=uncited) → research agent (model:"fable") → cite → flip marker
10
+ // → write §7 marker (status=uncited) → research agent (model:"opus") → cite → flip marker
11
11
  // (status=cited) — instead of a patch-guess (pairs with #33 circuit-breaker). Internal →
12
12
  // grep/Read; grep-empty → escalate to external (§5.1). The existing debug-cycle ternary
13
- // (model: cycle===1?"opus":(overrides["debug-cycle-2"]??"fable")) is PRESERVED; the research
14
- // stage is a SEPARATE agent() with its own bare "fable" literal. Idempotent per §4.1.
13
+ // (model: cycle===1?"opus":(overrides["debug-cycle-2"]??"opus")) is PRESERVED; the research
14
+ // stage is a SEPARATE agent() with its own bare "opus" literal. Idempotent per §4.1.
15
15
  //
16
16
  // M94-D11 WRITER pattern (graph-consumer-wiring-contract.md §WRITER Pattern):
17
17
  // READER half: before the fix agent, query blast-radius + who-calls to localize the
@@ -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 concrete-id fable path by probe wf_c9faf817-373 (no HTTP 400).
60
+ // the tier alias resolves to claude-opus-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,
@@ -184,7 +184,7 @@ async function grepForClaim(projectDir, claimText, phaseName) {
184
184
  return r || { found: false, matches: [] };
185
185
  }
186
186
 
187
- // M89 §1.1 — AMBIGUOUS → LLM JUDGE (bare model:"fable" — NOT the ?? form; "judge" is not
187
+ // M89 §1.1 — AMBIGUOUS → LLM JUDGE (bare model:"opus" — NOT the ?? form; "judge" is not
188
188
  // a designated stage, so the bare literal passes the M85 tier-set check and stays DISTINCT
189
189
  // from the debug-cycle ternary). internal/external/uncertain; uncertain → research (never
190
190
  // guess-internal). On error → "uncertain" (fail toward research).
@@ -201,7 +201,7 @@ async function judgeAmbiguous(claimText, phaseName) {
201
201
  `- "uncertain" = you cannot CONFIDENTLY place it internal — per M89 doctrine it is RESEARCHED, never guessed.`,
202
202
  `Return JSON: { "verdict": "internal"|"external"|"uncertain", "reason": "<one line>" }. No file/web work in THIS step.`,
203
203
  ].join("\n");
204
- const r = await agent(prompt, { label: "classify-judge", model: "fable", schema: CLASSIFY_JUDGE_SCHEMA, phase: phaseName })
204
+ const r = await agent(prompt, { label: "classify-judge", model: "opus", schema: CLASSIFY_JUDGE_SCHEMA, phase: phaseName })
205
205
  .catch((e) => ({ verdict: "uncertain", reason: `judge error: ${e && e.message}` }));
206
206
  return (r && r.verdict) || "uncertain";
207
207
  }
@@ -263,7 +263,7 @@ async function runResearchForClaim(projectDir, claimText, artifactPath, phaseNam
263
263
 
264
264
  // External-claim handler closure (reused by the ambiguous→judge path).
265
265
  const doExternal = async () => {
266
- log(`Research: external failure-root → research(fable) instead of patch-guess for "${claimKey.slice(0, 50)}"`);
266
+ log(`Research: external failure-root → research(opus) instead of patch-guess for "${claimKey.slice(0, 50)}"`);
267
267
  await appendUncitedMarker(externalArtifact, claimKey);
268
268
  const rr = await agent(
269
269
  [
@@ -272,7 +272,7 @@ async function runResearchForClaim(projectDir, claimText, artifactPath, phaseNam
272
272
  `Gap-key: "${claimKey}"`,
273
273
  `Emit ## Verified Facts (auto-research) block with source URL + fetch date. Append the trailer \`key: ${claimKey}\` on every fact line so the §7 gate matches by claim-key (Red Team MEDIUM #2). Return StructuredOutput JSON.`,
274
274
  ].join("\n"),
275
- { label: "research", model: "fable", schema: RESEARCH_RESULT_SCHEMA, phase: phaseName }
275
+ { label: "research", model: "opus", schema: RESEARCH_RESULT_SCHEMA, phase: phaseName }
276
276
  ).catch((e) => ({ ok: false, gapKey: claimKey, reason: String(e && e.message) }));
277
277
 
278
278
  if (rr && rr.ok && rr.citedBlock) {
@@ -300,7 +300,7 @@ async function runResearchForClaim(projectDir, claimText, artifactPath, phaseNam
300
300
  `Gap-key: "${claimKey}"`,
301
301
  `Emit ## Verified Facts (auto-research) block. Append the trailer \`key: ${claimKey}\` on every fact line so the §7 gate matches by claim-key (Red Team MEDIUM #2). Return StructuredOutput JSON.`,
302
302
  ].join("\n"),
303
- { label: "research", model: "fable", schema: RESEARCH_RESULT_SCHEMA, phase: phaseName }
303
+ { label: "research", model: "opus", schema: RESEARCH_RESULT_SCHEMA, phase: phaseName }
304
304
  ).catch((e) => ({ ok: false, gapKey: claimKey, reason: String(e && e.message) }));
305
305
 
306
306
  if (er && er.ok && er.citedBlock) {
@@ -449,7 +449,7 @@ for (let cycle = 1; cycle <= 2; cycle++) {
449
449
  label: `debug-cycle-${cycle}`,
450
450
  phase: `Cycle ${cycle}`,
451
451
  schema: DEBUG_CYCLE_SCHEMA,
452
- model: cycle === 1 ? "opus" : (overrides["debug-cycle-2"] ?? "fable"),
452
+ model: cycle === 1 ? "opus" : (overrides["debug-cycle-2"] ?? "opus"),
453
453
  }).catch((e) => ({
454
454
  resolved: false,
455
455
  rootCause: `agent error: ${e && e.message}`,
@@ -17,7 +17,7 @@
17
17
  // Domain workers embed the Stated-Claims snippet (§6.5) so agents tag load-bearing claims
18
18
  // KNOWN|GUESSED. After domain workers, the Research phase iterates GUESSED entries through
19
19
  // bin/gsd-t-research-gate.cjs: external → write §7 marker (status=uncited) → research agent
20
- // (model:"fable") → cite → flip marker (status=cited). Internal → grep/Read; grep-empty →
20
+ // (model:"opus") → cite → flip marker (status=cited). Internal → grep/Read; grep-empty →
21
21
  // escalate to external research + cite + marker (§5.1 ambiguous escalation). Idempotent per §4.1
22
22
  // (exact normalized claim-key match only). Wave is a pure composer and does NOT embed this wiring
23
23
  // (M85 zero-model: invariant; research reaches wave via its execute sub-workflow).
@@ -142,7 +142,7 @@ async function grepForClaim(projectDir, claimText, phaseName) {
142
142
 
143
143
  // M89 §1.1 — the AMBIGUOUS → LLM JUDGE. When the mechanical classifier finds no string
144
144
  // fact (class:ambiguous), the LLM decides internal/external/uncertain in natural language.
145
- // Bare model:"fable" (the research tier — NOT the ?? override form; "judge" is not a
145
+ // Bare model:"opus" (the research tier — NOT the ?? override form; "judge" is not a
146
146
  // designated stage). Returns the verdict string; on any error → "uncertain" (fail toward
147
147
  // research — never guess-internal). The caller treats external/uncertain as research.
148
148
  const CLASSIFY_JUDGE_SCHEMA = {
@@ -165,7 +165,7 @@ async function judgeAmbiguous(claimText, phaseName) {
165
165
  `Return JSON: { "verdict": "internal"|"external"|"uncertain", "reason": "<one line>" }.`,
166
166
  `Do NOT modify files. Do NOT run web searches in THIS step — only decide the verdict.`,
167
167
  ].join("\n");
168
- const r = await agent(prompt, { label: "classify-judge", model: "fable", schema: CLASSIFY_JUDGE_SCHEMA, phase: phaseName })
168
+ const r = await agent(prompt, { label: "classify-judge", model: "opus", schema: CLASSIFY_JUDGE_SCHEMA, phase: phaseName })
169
169
  .catch((e) => ({ verdict: "uncertain", reason: `judge error: ${e && e.message}` }));
170
170
  return (r && r.verdict) || "uncertain";
171
171
  }
@@ -422,14 +422,14 @@ if (allGuessedClaims.length === 0) {
422
422
  continue;
423
423
  }
424
424
 
425
- // External-claim handler (§7 marker → research(fable) → cite → flip). Closure so the
425
+ // External-claim handler (§7 marker → research(opus) → cite → flip). Closure so the
426
426
  // ambiguous→judge path reuses it for an "external"/"uncertain" verdict.
427
427
  const doExternal = async () => {
428
- log(`Research: external claim → write §7 marker + fable research for "${claimKey.slice(0, 50)}"${artifactPath ? "" : " (FALLBACK artifact — worker reported no path)"}`);
428
+ log(`Research: external claim → write §7 marker + opus research for "${claimKey.slice(0, 50)}"${artifactPath ? "" : " (FALLBACK artifact — worker reported no path)"}`);
429
429
  // §7: write uncited marker at classify time (to the real OR fallback artifact — fail-CLOSED)
430
430
  await writeUncitedMarker(externalArtifact, claimKey);
431
431
 
432
- // §2: research agent — bare "fable" tier literal (NOT the ??-override form; contract §2)
432
+ // §2: research agent — bare "opus" tier literal (NOT the ??-override form; contract §2)
433
433
  const researchPrompt = [
434
434
  `Read \`${projectDir}/templates/prompts/research-subagent.md\` for the full research protocol.`,
435
435
  `Your task: verify this external guessed claim via live web sources.`,
@@ -440,7 +440,7 @@ if (allGuessedClaims.length === 0) {
440
440
  ].join("\n");
441
441
  const researchResult = await agent(researchPrompt, {
442
442
  label: "research",
443
- model: "fable",
443
+ model: "opus",
444
444
  schema: RESEARCH_RESULT_SCHEMA,
445
445
  phase: "Research",
446
446
  }).catch((e) => ({ ok: false, gapKey: claimKey, reason: `research agent error: ${e && e.message}` }));
@@ -49,10 +49,10 @@
49
49
  // iterates each [GUESSED:*] entry through the D1 classifier, which is a MECHANICAL
50
50
  // STRING-FACT FILTER returning internal | external | AMBIGUOUS (v1.3.0). On
51
51
  // class:external write a §7 status=uncited marker, run the research agent (bare
52
- // model:"fable"), write a ## Verified Facts (auto-research) block, flip the marker to
52
+ // model:"opus"), write a ## Verified Facts (auto-research) block, flip the marker to
53
53
  // status=cited. On class:internal: grep first; if grep empty, escalate to external
54
54
  // (§5.1). On class:AMBIGUOUS (the regex has no string fact — semantic placement is the
55
- // LLM's call, NOT regex's): run a small LLM JUDGE (model:"fable") that decides
55
+ // LLM's call, NOT regex's): run a small LLM JUDGE (model:"opus") that decides
56
56
  // internal/external/uncertain in natural language. internal→grep; external→research;
57
57
  // UNCERTAIN→treat as external→research (uncertain = verify, NEVER guess-internal — a
58
58
  // silent miss is the one unacceptable outcome). Idempotent: an already-cited marker
@@ -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 concrete-id fable path by probe wf_c9faf817-373 (no HTTP 400).
86
+ // the tier alias resolves to claude-opus-5 (Fable removed 2026-07-24).
87
87
  const overrides = (_args.overrides && typeof _args.overrides === "object") ? _args.overrides : {};
88
88
  const _CLI_ENVELOPE_SCHEMA = {
89
89
  type: "object", required: ["ok", "exitCode"], additionalProperties: true,
@@ -486,7 +486,7 @@ async function runStatedClaimsPipeline(projectDir, phaseName, phaseResult, state
486
486
 
487
487
  log(`m89: claim "${claimKey}" → class:${claimClass} route:${claimRoute} — ${envelope.reason || ""}`);
488
488
 
489
- // External-claim handler (§7 marker → research(fable) → cite → flip). Closure so the
489
+ // External-claim handler (§7 marker → research(opus) → cite → flip). Closure so the
490
490
  // ambiguous→judge path can reuse it for an "external"/"uncertain" verdict.
491
491
  const doExternal = async () => {
492
492
  // §7: Write status=uncited marker into the (real OR fallback) artifact — ALWAYS written
@@ -504,7 +504,7 @@ async function runStatedClaimsPipeline(projectDir, phaseName, phaseResult, state
504
504
  ).catch((e) => ({ ok: false, error: String(e && e.message) }));
505
505
  log(`m89: wrote status=uncited marker for claim "${claimKey}" into ${externalArtifact}${primaryArtifact ? "" : " (FALLBACK artifact — worker reported no path)"}`);
506
506
 
507
- // §2: Run the research agent (bare model: "fable" — not the ?? override form).
507
+ // §2: Run the research agent (bare model: "opus" — not the ?? override form).
508
508
  // The research agent reads its own protocol from research-subagent.md.
509
509
  log(`m89: running research agent for external claim "${claimKey}"`);
510
510
  const researchResult = await agent(
@@ -519,7 +519,7 @@ async function runStatedClaimsPipeline(projectDir, phaseName, phaseResult, state
519
519
  `Use WebSearch + WebFetch to find authoritative sources. Emit a ## Verified Facts (auto-research) block per §3 format. On every fact line append the trailer \`key: ${claimKey}\` so the §7 gate matches by claim-key (Red Team MEDIUM #2).`,
520
520
  `Return JSON per the schema with ok:true and citedBlock (the full markdown block) on success, or ok:false and reason on STAGE-FAILURE.`,
521
521
  ].join("\n"),
522
- { label: "research-stage", phase: "Phase", schema: RESEARCH_RESULT_SCHEMA, model: "fable" }
522
+ { label: "research-stage", phase: "Phase", schema: RESEARCH_RESULT_SCHEMA, model: "opus" }
523
523
  ).catch((e) => ({ ok: false, gapKey: claimKey, reason: `research agent error: ${e && e.message}` }));
524
524
 
525
525
  if (researchResult && researchResult.ok && researchResult.citedBlock) {
@@ -598,7 +598,7 @@ async function runStatedClaimsPipeline(projectDir, phaseName, phaseResult, state
598
598
  await doInternal();
599
599
  } else {
600
600
  // class:AMBIGUOUS — the mechanical filter found NO string fact. Semantic placement
601
- // is the LLM's call, NOT regex's. Run the LLM JUDGE (fable). internal→grep;
601
+ // is the LLM's call, NOT regex's. Run the LLM JUDGE (opus). internal→grep;
602
602
  // external→research; UNCERTAIN→research (uncertain = verify, NEVER guess-internal —
603
603
  // a silent miss is the one unacceptable outcome). The classifier never guessed a
604
604
  // default — it deferred, and now the LLM decides (and on doubt we research).
@@ -623,7 +623,7 @@ async function runStatedClaimsPipeline(projectDir, phaseName, phaseResult, state
623
623
  `Return JSON: { "verdict": "internal" | "external" | "uncertain", "reason": "<one line>" }.`,
624
624
  `Do NOT modify files. Do NOT run web searches in THIS step — only decide the verdict.`,
625
625
  ].join("\n"),
626
- { label: "classify-judge", phase: "Phase", schema: CLASSIFY_JUDGE_SCHEMA, model: "fable" }
626
+ { label: "classify-judge", phase: "Phase", schema: CLASSIFY_JUDGE_SCHEMA, model: "opus" }
627
627
  ).catch((e) => ({ verdict: "uncertain", reason: `judge error: ${e && e.message} — failing toward research` }));
628
628
 
629
629
  const verdict = (judge && judge.verdict) || "uncertain";
@@ -674,7 +674,7 @@ async function runSolutionSpaceProbe(projectDir, phaseName, { milestone, briefPa
674
674
  // probe shifts UP — it competes over high-level APPROACHES (what/why/when, actors,
675
675
  // one-breath thesis), NOT lower-altitude implementation detail. This changes WHAT
676
676
  // the producers compete on, never WHO competes (producers stay opus, judge differs —
677
- // M82 blindness invariant preserved) nor the probe's model (stays fable).
677
+ // M82 blindness now via fresh context) probe + judge run opus (Fable removed 2026-07-24).
678
678
  const atApproachAltitude = altitude === "high-level-approach";
679
679
  const prompt = [
680
680
  `You are the Solution-Space Probe for the ${phaseName} phase${milestone ? ` of ${milestone}` : ""}. Decide ONE thing: should this phase generate MULTIPLE competing candidates (then a judge picks the best), or is a single draft sufficient?`,
@@ -688,7 +688,7 @@ async function runSolutionSpaceProbe(projectDir, phaseName, { milestone, briefPa
688
688
  `BIAS TOWARD COMPETING: if you are uncertain, or can name even two plausibly-different approaches, choose compete=true. A wasted competition costs ~3× this one phase; a missed-better-approach costs far more downstream (more pre-mortem blocks, more bugs, more verify cycles). Err on the side of generating options.`,
689
689
  `Return JSON per the schema: { "compete": true|false, "reason": "<one sentence>", "approaches": ["<a>","<b>",...] }.`,
690
690
  ].filter(Boolean).join("\n");
691
- const opts = { label: "solution-space-probe", schema: _PROBE_SCHEMA, model: overrides["solution-space-probe"] ?? "fable" };
691
+ const opts = { label: "solution-space-probe", schema: _PROBE_SCHEMA, model: overrides["solution-space-probe"] ?? "opus" };
692
692
  if (phaseNameOpt) opts.phase = phaseNameOpt;
693
693
  const r = await agent(prompt, opts).catch(() => null);
694
694
  // Probe failure → bias toward competing (fail-toward-options, per the cost logic).
@@ -714,7 +714,7 @@ async function runPartitionProbe(projectDir, { milestone, briefPath, userInput,
714
714
  `BIAS TOWARD COMPETING: if ≥3 files/areas are in play or you're unsure, choose compete=true — the file-disjointness oracle will objectively pick the most-parallelizable valid carving among the candidates, so competing is low-risk and high-reward.`,
715
715
  `Return JSON per the schema.`,
716
716
  ].filter(Boolean).join("\n");
717
- const opts = { label: "partition-probe", schema: _PROBE_SCHEMA, model: overrides["partition-probe"] ?? "fable" };
717
+ const opts = { label: "partition-probe", schema: _PROBE_SCHEMA, model: overrides["partition-probe"] ?? "opus" };
718
718
  if (phaseNameOpt) opts.phase = phaseNameOpt;
719
719
  const r = await agent(prompt, opts).catch(() => null);
720
720
  if (!r || typeof r.compete !== "boolean") {
@@ -1099,7 +1099,7 @@ if (!competitionOn) {
1099
1099
  `IMPORTANT: use the CANDIDATE LABEL (A, B, C…) shown above as the "id" in your scores.`,
1100
1100
  ].join("\n"),
1101
1101
  {
1102
- label: "judge:rubric", phase: "Judge", model: overrides["competition-judge"] ?? "fable",
1102
+ label: "judge:rubric", phase: "Judge", model: overrides["competition-judge"] ?? "opus",
1103
1103
  schema: {
1104
1104
  type: "object", required: ["scores"], additionalProperties: true,
1105
1105
  properties: { scores: { type: "array", items: { type: "object", additionalProperties: true } } },
@@ -1353,7 +1353,7 @@ if (phaseName === "plan" && result && result.status !== "failed") {
1353
1353
  ``,
1354
1354
  `M89 ${STATED_CLAIMS_INSTRUCTION}`,
1355
1355
  ].join("\n"),
1356
- { label: "pre-mortem", phase: "Plan Hardening", schema: PRE_MORTEM_SCHEMA, model: overrides["pre-mortem"] ?? "fable" }
1356
+ { label: "pre-mortem", phase: "Plan Hardening", schema: PRE_MORTEM_SCHEMA, model: overrides["pre-mortem"] ?? "opus" }
1357
1357
  ).catch((e) => ({ verdict: "BLOCK", findings: [{ severity: "HIGH", condition: `pre-mortem agent error: ${e && e.message}`, requiredTest: "re-run pre-mortem" }], notes: "agent-error" }));
1358
1358
 
1359
1359
  result.preMortem = preMortem;
@@ -8,7 +8,7 @@
8
8
  // The task agent embeds the Stated-Claims snippet (§6.5) and tags load-bearing claims KNOWN|GUESSED.
9
9
  // After the Execute phase, the Research phase iterates GUESSED entries through
10
10
  // bin/gsd-t-research-gate.cjs: external → write §7 marker (status=uncited) → research agent
11
- // (model:"fable") → cite → flip marker (status=cited). Internal → grep/Read; grep-empty →
11
+ // (model:"opus") → cite → flip marker (status=cited). Internal → grep/Read; grep-empty →
12
12
  // escalate to external (§5.1). Idempotent per §4.1 (exact normalized claim-key match only).
13
13
  //
14
14
  // M90 §2 D4-T4: Architectural-trigger wiring (protocol-class path, R-ARCH-2 "everywhere" feed).
@@ -179,7 +179,7 @@ async function grepForClaim(projectDir, claimText, phaseName) {
179
179
  return r || { found: false, matches: [] };
180
180
  }
181
181
 
182
- // M89 §1.1 — AMBIGUOUS → LLM JUDGE (bare model:"fable"). internal/external/uncertain;
182
+ // M89 §1.1 — AMBIGUOUS → LLM JUDGE (bare model:"opus"). internal/external/uncertain;
183
183
  // uncertain → research (never guess-internal). On error → "uncertain" (fail toward research).
184
184
  const CLASSIFY_JUDGE_SCHEMA = {
185
185
  type: "object", required: ["verdict"], additionalProperties: true,
@@ -194,7 +194,7 @@ async function judgeAmbiguous(claimText, phaseName) {
194
194
  `- "uncertain" = you cannot CONFIDENTLY place it internal — per M89 doctrine it is RESEARCHED, never guessed.`,
195
195
  `Return JSON: { "verdict": "internal"|"external"|"uncertain", "reason": "<one line>" }. No file/web work in THIS step.`,
196
196
  ].join("\n");
197
- const r = await agent(prompt, { label: "classify-judge", model: "fable", schema: CLASSIFY_JUDGE_SCHEMA, phase: phaseName })
197
+ const r = await agent(prompt, { label: "classify-judge", model: "opus", schema: CLASSIFY_JUDGE_SCHEMA, phase: phaseName })
198
198
  .catch((e) => ({ verdict: "uncertain", reason: `judge error: ${e && e.message}` }));
199
199
  return (r && r.verdict) || "uncertain";
200
200
  }
@@ -383,7 +383,7 @@ if (guessedClaims.length === 0) {
383
383
 
384
384
  // External-claim handler closure (reused by the ambiguous→judge path).
385
385
  const doExternal = async () => {
386
- log(`Research: external → marker + research(fable) for "${claimKey.slice(0, 50)}"${artifactPath ? "" : " (FALLBACK artifact — no reported path)"}`);
386
+ log(`Research: external → marker + research(opus) for "${claimKey.slice(0, 50)}"${artifactPath ? "" : " (FALLBACK artifact — no reported path)"}`);
387
387
  // §7 write uncited marker to the real OR fallback artifact — ALWAYS (fail-CLOSED)
388
388
  {
389
389
  const m = uncitedMarker(claimKey);
@@ -394,7 +394,7 @@ if (guessedClaims.length === 0) {
394
394
  ).catch(() => {});
395
395
  delete artifactCache[externalArtifact];
396
396
  }
397
- // §2 research agent — bare literal model: "fable"
397
+ // §2 research agent — bare literal model: "opus"
398
398
  const rr = await agent(
399
399
  [
400
400
  `Read \`${projectDir}/templates/prompts/research-subagent.md\` for the research protocol.`,
@@ -402,7 +402,7 @@ if (guessedClaims.length === 0) {
402
402
  `Gap-key: "${claimKey}"`,
403
403
  `Emit ## Verified Facts (auto-research) block with source URL + fetch date. Append the trailer \`key: ${claimKey}\` on every fact line so the §7 gate matches by claim-key (Red Team MEDIUM #2). Return StructuredOutput JSON.`,
404
404
  ].join("\n"),
405
- { label: "research", model: "fable", schema: RESEARCH_RESULT_SCHEMA, phase: "Research" }
405
+ { label: "research", model: "opus", schema: RESEARCH_RESULT_SCHEMA, phase: "Research" }
406
406
  ).catch((e) => ({ ok: false, gapKey: claimKey, reason: String(e && e.message) }));
407
407
 
408
408
  if (rr && rr.ok && rr.citedBlock) {
@@ -56,7 +56,7 @@ const _args = (typeof args === "string") ? (() => { try { return JSON.parse(args
56
56
  // Default to {} so the premium fallback literals apply when no invoker injects overrides.
57
57
  // overrides values are CONCRETE model ids (resolver envelope); the bare literals below
58
58
  // are tier ALIASES. The sandbox runtime accepts BOTH forms in model: — proven live for
59
- // the concrete-id fable path by probe wf_c9faf817-373 (no HTTP 400).
59
+ // the tier alias resolves to claude-opus-5 (Fable removed 2026-07-24).
60
60
  const overrides = (_args.overrides && typeof _args.overrides === "object") ? _args.overrides : {};
61
61
  const _CLI_ENVELOPE_SCHEMA = {
62
62
  type: "object", required: ["ok", "exitCode"], additionalProperties: true,
@@ -771,7 +771,7 @@ const stages = [
771
771
  `Verdict is FAIL if you found any CRITICAL or HIGH severity bug; GRUDGING-PASS`,
772
772
  `if you searched exhaustively and found nothing. Return JSON per the schema.`,
773
773
  ].join("\n"),
774
- { label: "red-team", phase: "Orthogonal Triad", schema: RED_TEAM_SCHEMA, model: overrides["red-team"] ?? "fable" }
774
+ { label: "red-team", phase: "Orthogonal Triad", schema: RED_TEAM_SCHEMA, model: overrides["red-team"] ?? "opus" }
775
775
  ),
776
776
 
777
777
  // Stage C — QA (test execution + shallow-test detection + contract compliance)
@@ -1,234 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * GSD-T Model Selector — surgical per-phase model tier assignment
5
- *
6
- * Replaces the v2.x "silent downgrade under context pressure" behavior with
7
- * declarative per-phase tier assignments. Callers ask `selectModel({phase, ...})`
8
- * and get back `{model, reason, escalation_hook}` — the tier decision is
9
- * deterministic, driven by the rules table below, and does NOT depend on
10
- * session context percentage.
11
- *
12
- * Contract: .gsd-t/contracts/model-selection-contract.md v1.0.0 (M35 T4)
13
- * Findings: .gsd-t/M35-advisor-findings.md (convention-based /advisor fallback)
14
- *
15
- * Zero external dependencies.
16
- */
17
-
18
- // ── Tiers ───────────────────────────────────────────────────────────────────
19
- // M85: FABLE tier added alongside HAIKU/SONNET/OPUS.
20
- // Contract: .gsd-t/contracts/model-tier-policy-contract.md v1.0.0 § "Stage Policy"
21
-
22
- const TIERS = Object.freeze({
23
- HAIKU: "haiku",
24
- SONNET: "sonnet",
25
- OPUS: "opus",
26
- FABLE: "fable",
27
- });
28
-
29
- const DEFAULT_TIER = TIERS.SONNET;
30
-
31
- // ── Escalation hook block (convention-based /advisor fallback) ──────────────
32
- //
33
- // Per `.gsd-t/M35-advisor-findings.md`, Claude Code's native /advisor has no
34
- // programmable API at subagent scope. This block is injected into the subagent
35
- // prompt at declared escalation points on sonnet-tier phases where the
36
- // orchestrator has flagged a high-stakes sub-decision.
37
- //
38
- // Kept as a constant so all consumers (command files, advisor-integration.js,
39
- // M35-advisor-findings.md) reference the same canonical text.
40
-
41
- const ESCALATION_HOOK = [
42
- "## Escalation Hook — /advisor convention-based fallback",
43
- "",
44
- "Before finalizing your answer for this phase, stop and consider:",
45
- "1. Is this decision high-stakes? (architecture, contract design, security boundary,",
46
- " data-loss risk, cross-module refactor, adversarial QA verdict)",
47
- "2. Would a more capable model produce a materially better answer?",
48
- "3. Are you confident in the assumptions you're making?",
49
- "",
50
- "If YES to any of the above, do ONE of the following:",
51
- "- Escalate internally: spend an extra reasoning pass re-examining the decision",
52
- " from first principles. Document the re-examination in your output.",
53
- "- Spawn a nested opus subagent: use the Task tool with",
54
- " `subagent_type: \"general-purpose\"` and include `model: opus` in the spawn.",
55
- "",
56
- "Record in your output whether you escalated: set `ESCALATED_VIA_ADVISOR=true` or",
57
- "`ESCALATED_VIA_ADVISOR=false` on a line by itself near the end of your report.",
58
- ].join("\n");
59
-
60
- // ── Declarative phase rules table ───────────────────────────────────────────
61
- //
62
- // Each rule maps (phase, task_type) → tier. The first matching rule wins.
63
- // `task_type` is optional — when absent, the rule matches any task within
64
- // that phase. Order the rules from most-specific to least-specific.
65
- //
66
- // Tier assignments mirror `.gsd-t/M35-definition.md` Part B and the Model
67
- // Assignments section of the GSD-T global CLAUDE template:
68
- // - haiku: strictly mechanical — test runners, branch guards, file checks,
69
- // JSON validation, no judgment
70
- // - sonnet: routine code work — execute step 2, test-sync, doc-ripple wiring,
71
- // quick fixes, integration wiring, debug fix-apply
72
- // - opus: high-stakes reasoning — partition, discuss, Red Team, verify
73
- // judgment, debug root-cause, contract/architecture design
74
-
75
- const PHASE_RULES = Object.freeze([
76
- // Phase: execute
77
- { phase: "execute", task_type: "test_runner", model: TIERS.HAIKU, reason: "Mechanical test-suite runner — zero judgment" },
78
- { phase: "execute", task_type: "branch_guard", model: TIERS.HAIKU, reason: "Mechanical branch-name check — zero judgment" },
79
- { phase: "execute", task_type: "file_check", model: TIERS.HAIKU, reason: "Mechanical file-existence check — zero judgment" },
80
- { phase: "execute", task_type: "qa", model: TIERS.SONNET, reason: "QA evaluation needs judgment per M31 tier refinement" },
81
- { phase: "execute", task_type: "red_team", model: TIERS.OPUS, reason: "Adversarial QA benefits most from top tier" },
82
- { phase: "execute", model: TIERS.SONNET, reason: "Routine task execution — sonnet is the M35 default for routine work", hasEscalation: true },
83
-
84
- // Phase: wave (the wave orchestrator itself)
85
- { phase: "wave", model: TIERS.SONNET, reason: "Wave orchestration dispatches per-phase subagents; the orchestrator itself is routine coordination", hasEscalation: true },
86
-
87
- // Phase: quick
88
- { phase: "quick", task_type: "test_runner", model: TIERS.HAIKU, reason: "Mechanical test-suite runner — zero judgment" },
89
- { phase: "quick", model: TIERS.SONNET, reason: "Routine one-off task — sonnet default" },
90
-
91
- // Phase: integrate
92
- { phase: "integrate", task_type: "test_runner", model: TIERS.HAIKU, reason: "Mechanical integration test runner — zero judgment" },
93
- { phase: "integrate", model: TIERS.SONNET, reason: "Integration wiring is routine coordination work" },
94
-
95
- // Phase: debug
96
- { phase: "debug", task_type: "fix_apply", model: TIERS.SONNET, reason: "Applying a known fix is routine code work" },
97
- { phase: "debug", task_type: "root_cause", model: TIERS.OPUS, reason: "Root-cause analysis is high-stakes reasoning" },
98
- // M85: cycle-2 escalation — when debug cycle-1 (opus) has not resolved the issue,
99
- // cycle-2 escalates to Fable. The debug DEFAULT (cycle-1/general) remains opus —
100
- // no existing rule is altered (AC f, no silent degradation). This is a DOCUMENTED
101
- // MIRROR for Task-based/bin/ callers; the live enforcement is in the debug workflow
102
- // ternary (D3-T3); the D4 lint guards that ternary.
103
- // API shape: selectModel({ phase: "debug", task_type: "cycle_2_escalation" }) → fable
104
- { phase: "debug", task_type: "cycle_2_escalation", model: TIERS.FABLE, reason: "Cycle-2 debug escalation — Fable after opus cycle-1 has not resolved; no existing rule altered (AC f)" },
105
- { phase: "debug", model: TIERS.OPUS, reason: "Debug default is high-stakes — prefer opus unless the task_type says otherwise" },
106
-
107
- // Phase: partition — high-stakes architectural decomposition
108
- { phase: "partition", model: TIERS.OPUS, reason: "Domain partitioning is architectural reasoning — high stakes" },
109
-
110
- // Phase: discuss — multi-perspective design exploration
111
- { phase: "discuss", model: TIERS.OPUS, reason: "Design exploration benefits from top-tier reasoning" },
112
-
113
- // Phase: plan — task-list authoring
114
- { phase: "plan", model: TIERS.SONNET, reason: "Task decomposition is structured work — sonnet with escalation hook", hasEscalation: true },
115
-
116
- // Phase: verify — final quality judgment before milestone complete
117
- { phase: "verify", model: TIERS.OPUS, reason: "Milestone verification is the final quality gate — high stakes" },
118
-
119
- // Phase: test-sync — keeping tests aligned with code
120
- { phase: "test-sync", model: TIERS.SONNET, reason: "Test alignment is routine refactoring work" },
121
-
122
- // Phase: doc-ripple — downstream document updates
123
- { phase: "doc-ripple", model: TIERS.SONNET, reason: "Documentation updates are routine prose editing" },
124
-
125
- // Phase: red_team — explicit adversarial QA phase (separate from execute task_type)
126
- { phase: "red_team", model: TIERS.OPUS, reason: "Adversarial QA — always opus, the incentive is to find bugs" },
127
-
128
- // Phase: qa — explicit standalone QA phase
129
- { phase: "qa", model: TIERS.SONNET, reason: "QA per M31 refinement — sonnet produces fewer false negatives than haiku" },
130
- ]);
131
-
132
- // Complexity-signal overrides. If the caller provides `complexity_signals`,
133
- // these can bump a sonnet decision to opus regardless of phase rule.
134
- const COMPLEXITY_OVERRIDES = Object.freeze({
135
- cross_module_refactor: TIERS.OPUS,
136
- security_boundary: TIERS.OPUS,
137
- data_loss_risk: TIERS.OPUS,
138
- contract_design: TIERS.OPUS,
139
- });
140
-
141
- // ── Public API ──────────────────────────────────────────────────────────────
142
-
143
- /**
144
- * Select the model tier for a subagent spawn.
145
- *
146
- * @param {object} args
147
- * @param {string} args.phase — required; one of the phase names above
148
- * @param {string} [args.task_type] — optional task_type for finer-grained rules
149
- * @param {string} [args.domain_type] — optional, currently unused (reserved for future per-domain rules)
150
- * @param {object} [args.complexity_signals] — optional object, keys matching COMPLEXITY_OVERRIDES escalate sonnet→opus
151
- * @returns {{model: string, reason: string, escalation_hook: string|null}}
152
- */
153
- function selectModel(args) {
154
- if (!args || typeof args !== "object") {
155
- return {
156
- model: DEFAULT_TIER,
157
- reason: "No args provided — default to routine tier (sonnet)",
158
- escalation_hook: null,
159
- };
160
- }
161
-
162
- const { phase, task_type, complexity_signals } = args;
163
-
164
- if (!phase || typeof phase !== "string") {
165
- return {
166
- model: DEFAULT_TIER,
167
- reason: "No phase provided — default to routine tier (sonnet)",
168
- escalation_hook: null,
169
- };
170
- }
171
-
172
- // First pass: find the most-specific matching rule.
173
- let matched = null;
174
- for (const rule of PHASE_RULES) {
175
- if (rule.phase !== phase) continue;
176
- if (rule.task_type && rule.task_type !== task_type) continue;
177
- matched = rule;
178
- break;
179
- }
180
-
181
- if (!matched) {
182
- return {
183
- model: DEFAULT_TIER,
184
- reason: `Unknown phase "${phase}" — fallback to routine tier (sonnet)`,
185
- escalation_hook: null,
186
- };
187
- }
188
-
189
- let model = matched.model;
190
- let reason = matched.reason;
191
-
192
- // Complexity-signal overrides: bump sonnet → opus if any flagged signal is truthy.
193
- if (model === TIERS.SONNET && complexity_signals && typeof complexity_signals === "object") {
194
- for (const key of Object.keys(complexity_signals)) {
195
- if (!complexity_signals[key]) continue;
196
- const override = COMPLEXITY_OVERRIDES[key];
197
- if (override && override !== model) {
198
- model = override;
199
- reason = `${reason} (escalated to ${override} by complexity signal: ${key})`;
200
- break;
201
- }
202
- }
203
- }
204
-
205
- // Escalation hook is only injected on sonnet phases flagged as high-stakes-adjacent.
206
- // Haiku phases have no hook (mechanical, no judgment). Opus phases have no hook
207
- // (already at top tier — nowhere to escalate).
208
- let escalation_hook = null;
209
- if (model === TIERS.SONNET && matched.hasEscalation) {
210
- escalation_hook = ESCALATION_HOOK;
211
- }
212
-
213
- return { model, reason, escalation_hook };
214
- }
215
-
216
- /**
217
- * Return the canonical list of phase names the selector knows about.
218
- * Used by tests and documentation tooling to assert coverage.
219
- */
220
- function listPhases() {
221
- const seen = new Set();
222
- for (const rule of PHASE_RULES) seen.add(rule.phase);
223
- return [...seen].sort();
224
- }
225
-
226
- module.exports = {
227
- selectModel,
228
- listPhases,
229
- TIERS,
230
- DEFAULT_TIER,
231
- ESCALATION_HOOK,
232
- PHASE_RULES,
233
- COMPLEXITY_OVERRIDES,
234
- };