@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 +34 -0
- package/README.md +2 -3
- package/bin/gsd-t-doc-marker.cjs +19 -1
- package/bin/gsd-t-env-registry.cjs +5 -7
- package/bin/gsd-t-model-profile.cjs +8 -15
- package/bin/gsd-t-model-tier-policy.cjs +62 -49
- package/bin/gsd-t-parallel.cjs +1 -2
- package/commands/gsd-t-debug.md +1 -1
- package/commands/gsd-t-help.md +5 -5
- package/commands/gsd-t-partition.md +1 -1
- package/commands/gsd-t-quick.md +2 -2
- package/commands/gsd-t-status.md +4 -4
- package/commands/gsd-t-test-sync.md +1 -1
- package/package.json +1 -1
- package/templates/CLAUDE-global.md +5 -5
- package/templates/CLAUDE-project.md +1 -1
- package/templates/prompts/blind-adversary-subagent.md +2 -2
- package/templates/prompts/research-subagent.md +1 -1
- package/templates/workflows/gsd-t-debug.workflow.js +10 -10
- package/templates/workflows/gsd-t-execute.workflow.js +7 -7
- package/templates/workflows/gsd-t-phase.workflow.js +13 -13
- package/templates/workflows/gsd-t-quick.workflow.js +6 -6
- package/templates/workflows/gsd-t-verify.workflow.js +2 -2
- package/bin/model-selector.js +0 -234
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.
|
|
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** —
|
|
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
|
package/bin/gsd-t-doc-marker.cjs
CHANGED
|
@@ -62,4 +62,22 @@ function upsertMarkedDocBlock(targetPath, startMarker, endMarker, block) {
|
|
|
62
62
|
return targetPath;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
|
|
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
|
-
|
|
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','
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
*
|
|
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-
|
|
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
|
-
*
|
|
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': '
|
|
42
|
-
'partition-probe': '
|
|
43
|
-
'competition-judge': '
|
|
44
|
-
'competition-producers': 'opus',
|
|
45
|
-
'pre-mortem': '
|
|
46
|
-
'red-team': '
|
|
47
|
-
'debug-cycle-2': '
|
|
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
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
*
|
|
102
|
-
* standard —
|
|
103
|
-
*
|
|
104
|
-
* pro — red-team + pre-mortem + debug-cycle-2 →
|
|
105
|
-
*
|
|
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
|
|
109
|
-
*
|
|
110
|
-
*
|
|
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': '
|
|
120
|
-
'red-team': '
|
|
121
|
-
'debug-cycle-2': '
|
|
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': '
|
|
128
|
-
'red-team': '
|
|
129
|
-
'debug-cycle-2': '
|
|
140
|
+
'pre-mortem': 'opus',
|
|
141
|
+
'red-team': 'opus',
|
|
142
|
+
'debug-cycle-2': 'opus',
|
|
130
143
|
}),
|
|
131
144
|
premium: Object.freeze({
|
|
132
|
-
'solution-space-probe': '
|
|
133
|
-
'partition-probe': '
|
|
134
|
-
'competition-judge': '
|
|
135
|
-
'pre-mortem': '
|
|
136
|
-
'red-team': '
|
|
137
|
-
'debug-cycle-2': '
|
|
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
|
|
152
|
-
const PRODUCERS_MODEL_ID = MODEL_IDS.opus; // claude-opus-
|
|
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
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
* - competition-judge
|
|
162
|
-
*
|
|
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 {
|
package/bin/gsd-t-parallel.cjs
CHANGED
|
@@ -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
|
package/commands/gsd-t-debug.md
CHANGED
|
@@ -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
|
|
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
|
}
|
package/commands/gsd-t-help.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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: "
|
|
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: `
|
|
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
|
|
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
|
package/commands/gsd-t-quick.md
CHANGED
|
@@ -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` (`
|
|
29
|
-
- **Mechanical subroutines** (demote to `haiku`): test runners
|
|
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
|
|
package/commands/gsd-t-status.md
CHANGED
|
@@ -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
|
|
85
|
-
- `standard` —
|
|
86
|
-
- `pro` —
|
|
87
|
-
- `premium` — all 6
|
|
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` (`
|
|
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.
|
|
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: "
|
|
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"`
|
|
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
|
-
-
|
|
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: "
|
|
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: "
|
|
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-
|
|
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:** `
|
|
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-
|
|
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: "
|
|
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:"
|
|
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"]??"
|
|
14
|
-
// stage is a SEPARATE agent() with its own bare "
|
|
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
|
|
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:"
|
|
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: "
|
|
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(
|
|
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: "
|
|
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: "
|
|
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"] ?? "
|
|
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:"
|
|
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:"
|
|
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: "
|
|
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(
|
|
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 +
|
|
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 "
|
|
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: "
|
|
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:"
|
|
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:"
|
|
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
|
|
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(
|
|
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: "
|
|
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: "
|
|
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 (
|
|
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: "
|
|
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
|
|
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"] ?? "
|
|
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"] ?? "
|
|
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"] ?? "
|
|
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"] ?? "
|
|
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:"
|
|
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:"
|
|
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: "
|
|
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(
|
|
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: "
|
|
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: "
|
|
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
|
|
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"] ?? "
|
|
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)
|
package/bin/model-selector.js
DELETED
|
@@ -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
|
-
};
|