@tekyzinc/gsd-t 4.4.10 → 4.6.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.
@@ -5,7 +5,7 @@
5
5
  * Zero external runtime deps — installer-package invariant.
6
6
  * No top-level side effects.
7
7
  *
8
- * Contract: .gsd-t/contracts/model-tier-policy-contract.md v1.0.0 STABLE
8
+ * Contract: .gsd-t/contracts/model-tier-policy-contract.md v1.1.0 STABLE
9
9
  */
10
10
 
11
11
  'use strict';
@@ -91,6 +91,158 @@ function resolve(stageKey) {
91
91
  }
92
92
  }
93
93
 
94
+ // ---------------------------------------------------------------------------
95
+ // Profile Dimension (M86 — additive over the frozen M85 STAGE_TIERS)
96
+ // ---------------------------------------------------------------------------
97
+
98
+ /**
99
+ * Frozen profile → stage-key → tier map.
100
+ *
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).
107
+ *
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.
111
+ *
112
+ * @type {Readonly<Record<string, Readonly<Record<string, string>>>>}
113
+ */
114
+ const PROFILE_STAGE_TIERS = Object.freeze({
115
+ standard: Object.freeze({
116
+ 'solution-space-probe': 'opus',
117
+ 'partition-probe': 'opus',
118
+ 'competition-judge': 'sonnet',
119
+ 'pre-mortem': 'opus',
120
+ 'red-team': 'opus',
121
+ 'debug-cycle-2': 'opus',
122
+ }),
123
+ pro: Object.freeze({
124
+ 'solution-space-probe': 'opus',
125
+ 'partition-probe': 'opus',
126
+ 'competition-judge': 'sonnet',
127
+ 'pre-mortem': 'fable',
128
+ 'red-team': 'fable',
129
+ 'debug-cycle-2': 'fable',
130
+ }),
131
+ 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',
138
+ }),
139
+ });
140
+
141
+ /** The 6 injectable designated stages (competition-producers excluded). */
142
+ const INJECTABLE_STAGES = Object.freeze([
143
+ 'solution-space-probe',
144
+ 'partition-probe',
145
+ 'competition-judge',
146
+ 'pre-mortem',
147
+ 'red-team',
148
+ 'debug-cycle-2',
149
+ ]);
150
+
151
+ /** The HELD producers model id — used by blindness clamps. */
152
+ const PRODUCERS_MODEL_ID = MODEL_IDS.opus; // claude-opus-4-8
153
+
154
+ /**
155
+ * Resolves the concrete model id for a given stage key under a profile,
156
+ * honoring precedence: stageOverrides[stage] ?? profile-tier ?? global-default.
157
+ *
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: BLOCKED — drops the override
162
+ * and uses the profile tier for competition-judge instead.
163
+ *
164
+ * @param {string} stageKey
165
+ * @param {{ profile?: string, stageOverrides?: Record<string,string> }} opts
166
+ * @returns {{ model: string, tier: string, requiresThinkingOmitted: boolean,
167
+ * configError?: string }}
168
+ */
169
+ // Own-property lookup guard. Validation-by-truthiness (`!MODEL_IDS[x]`) is a
170
+ // validation BYPASS for Object.prototype keys ("constructor", "toString", …):
171
+ // the inherited value is truthy, the resolved "model" is a function, and
172
+ // JSON.stringify silently DROPS the key from the envelope — the workflow's
173
+ // `?? "fable"` fallback then bills premium on a cost-control profile
174
+ // (Red Team M86 HIGH). Every tier/profile/stage map lookup goes through this.
175
+ function hasOwn(obj, key) {
176
+ return Object.prototype.hasOwnProperty.call(obj, key);
177
+ }
178
+
179
+ function resolveProfile(stageKey, opts) {
180
+ opts = opts || {};
181
+ const profileValid = typeof opts.profile === 'string' && hasOwn(PROFILE_STAGE_TIERS, opts.profile);
182
+ const profile = profileValid ? opts.profile : 'premium'; // named global default
183
+
184
+ const stageOverrides = (opts.stageOverrides && typeof opts.stageOverrides === 'object' && !Array.isArray(opts.stageOverrides))
185
+ ? opts.stageOverrides
186
+ : {};
187
+
188
+ // competition-producers is held at opus — not resolvable via profile dimension.
189
+ if (stageKey === 'competition-producers') {
190
+ return {
191
+ model: PRODUCERS_MODEL_ID,
192
+ tier: 'opus',
193
+ requiresThinkingOmitted: requiresThinkingOmitted(PRODUCERS_MODEL_ID),
194
+ };
195
+ }
196
+
197
+ // Resolve tier from precedence chain:
198
+ // 1. stageOverrides[stage] if it's a valid tier and not a blindness violation
199
+ // 2. profile-tier
200
+ // global-default (premium) is the fallback when profile is unknown — MARKED,
201
+ // never silent (Red Team M86 r2 LOW: library callers bypassing readConfig got
202
+ // silent premium for an invalid profile).
203
+
204
+ const profileTierMap = PROFILE_STAGE_TIERS[profile];
205
+ const stageKnown = hasOwn(profileTierMap, stageKey);
206
+ const errors = [];
207
+ if (!profileValid && opts.profile !== undefined) {
208
+ errors.push(`unknown profile "${opts.profile}" — using the named global default "premium"`);
209
+ }
210
+ if (!stageKnown) {
211
+ // Unknown stage key — defensive sonnet, but NEVER silently (Red Team M86 MEDIUM:
212
+ // a typo'd stage returning ok:true sonnet regressed the M85 explicit unknown-stage error)
213
+ errors.push(`unknown stage "${stageKey}" — not a designated stage; defensive sonnet fallback`);
214
+ }
215
+ let resolvedTier;
216
+
217
+ const rawOverrideTier = hasOwn(stageOverrides, stageKey) ? stageOverrides[stageKey] : undefined;
218
+ if (rawOverrideTier !== undefined) {
219
+ if (typeof rawOverrideTier !== 'string' || !hasOwn(MODEL_IDS, rawOverrideTier)) {
220
+ // Invalid tier in override — fall back to profile tier, record configError.
221
+ // The fallback for an UNKNOWN stage is the cheap defensive tier, never fable
222
+ // (Red Team M86 r2 LOW: unknown stage + invalid override resolved fable on standard).
223
+ errors.push(`stageOverrides["${stageKey}"] has invalid tier "${rawOverrideTier}"; falling back to profile tier`);
224
+ 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
+ } else {
230
+ resolvedTier = rawOverrideTier;
231
+ }
232
+ } else {
233
+ resolvedTier = stageKnown ? profileTierMap[stageKey] : 'sonnet';
234
+ }
235
+
236
+ const modelId = hasOwn(MODEL_IDS, resolvedTier) ? MODEL_IDS[resolvedTier] : MODEL_IDS.sonnet;
237
+ const result = {
238
+ model: modelId,
239
+ tier: resolvedTier,
240
+ requiresThinkingOmitted: requiresThinkingOmitted(modelId),
241
+ };
242
+ if (errors.length) result.configError = errors.join('; ');
243
+ return result;
244
+ }
245
+
94
246
  // ---------------------------------------------------------------------------
95
247
  // Exports
96
248
  // ---------------------------------------------------------------------------
@@ -98,8 +250,11 @@ function resolve(stageKey) {
98
250
  module.exports = {
99
251
  MODEL_IDS,
100
252
  STAGE_TIERS,
253
+ PROFILE_STAGE_TIERS,
254
+ INJECTABLE_STAGES,
101
255
  requiresThinkingOmitted,
102
256
  resolve,
257
+ resolveProfile,
103
258
  };
104
259
 
105
260
  // ---------------------------------------------------------------------------
package/bin/gsd-t.js CHANGED
@@ -1011,12 +1011,17 @@ function configureAutoRouteHook(scriptPath) {
1011
1011
  const HOOKS_DIR = path.join(SCRIPTS_DIR, "hooks");
1012
1012
  const PKG_HOOKS = path.join(PKG_SCRIPTS, "hooks");
1013
1013
 
1014
- // Each entry: { script, events, async } — `events` is the array of hook event
1015
- // names this script must be wired into. The `gsd-t-conversation-capture.js`
1016
- // hook runs on SessionStart, UserPromptSubmit, and Stop (per the global
1017
- // CLAUDE.md M45 D2 install block PostToolUse stays opt-in via the
1018
- // GSD_T_CAPTURE_TOOL_USES env flag, so we don't auto-register it).
1019
- // `gsd-t-in-session-usage-hook.js` runs on Stop (per M43 D1 contract).
1014
+ // Each entry: { script, events, async, runner } — `events` is the array of hook
1015
+ // event names this script must be wired into. `runner` is the interpreter
1016
+ // ("node" default, "bash" for *.sh hooks). `async: true` lets the hook run
1017
+ // detached; OMIT it for hooks whose stdout must reach the terminal (the
1018
+ // ctx-cue banner is synchronous by design see its header).
1019
+ // The `gsd-t-conversation-capture.js` hook runs on SessionStart,
1020
+ // UserPromptSubmit, and Stop (per the global CLAUDE.md M45 D2 install block —
1021
+ // PostToolUse stays opt-in via the GSD_T_CAPTURE_TOOL_USES env flag, so we
1022
+ // don't auto-register it). `gsd-t-in-session-usage-hook.js` runs on Stop (per
1023
+ // M43 D1 contract). `gsd-t-ctx-cue.sh` runs synchronously on Stop (M85 —
1024
+ // low-context red banner; sync so its stdout reaches the terminal).
1020
1025
  const IN_SESSION_HOOKS = [
1021
1026
  {
1022
1027
  script: "gsd-t-conversation-capture.js",
@@ -1028,6 +1033,11 @@ const IN_SESSION_HOOKS = [
1028
1033
  events: ["Stop"],
1029
1034
  async: true,
1030
1035
  },
1036
+ {
1037
+ script: "gsd-t-ctx-cue.sh",
1038
+ events: ["Stop"],
1039
+ runner: "bash",
1040
+ },
1031
1041
  ];
1032
1042
 
1033
1043
  function installInSessionHooks() {
@@ -1072,7 +1082,8 @@ function configureInSessionHooks() {
1072
1082
  let added = 0;
1073
1083
  for (const hook of IN_SESSION_HOOKS) {
1074
1084
  const scriptPath = path.join(HOOKS_DIR, hook.script);
1075
- const cmd = `node "${scriptPath.replace(/\\/g, "\\\\")}"`;
1085
+ const runner = hook.runner || "node";
1086
+ const cmd = `${runner} "${scriptPath.replace(/\\/g, "\\\\")}"`;
1076
1087
 
1077
1088
  for (const event of hook.events) {
1078
1089
  if (!settings.hooks[event]) settings.hooks[event] = [];
@@ -1106,6 +1117,67 @@ function configureInSessionHooks() {
1106
1117
  }
1107
1118
  }
1108
1119
 
1120
+ // ─── Status Line ─────────────────────────────────────────────────────────────
1121
+
1122
+ // The GSD-T status bar. Canonical source: scripts/statusline-command.sh. Copy
1123
+ // it to ~/.claude/statusline-command.sh and point settings.statusLine at it so
1124
+ // edits in the package survive install/update/update-all. We only set
1125
+ // statusLine when it is absent or already points at our script — never clobber
1126
+ // a user's custom status line.
1127
+ const STATUSLINE_SCRIPT = "statusline-command.sh";
1128
+
1129
+ function installStatusLine() {
1130
+ ensureDir(CLAUDE_DIR);
1131
+ const src = path.join(PKG_SCRIPTS, STATUSLINE_SCRIPT);
1132
+ if (!fs.existsSync(src)) {
1133
+ info("No statusline-command.sh in package — skipping status line");
1134
+ return;
1135
+ }
1136
+ const dest = path.join(CLAUDE_DIR, STATUSLINE_SCRIPT);
1137
+ const srcContent = fs.readFileSync(src, "utf8");
1138
+ const destContent = fs.existsSync(dest) ? fs.readFileSync(dest, "utf8") : "";
1139
+ if (normalizeEol(srcContent) !== normalizeEol(destContent)) {
1140
+ copyFile(src, dest, STATUSLINE_SCRIPT);
1141
+ try { fs.chmodSync(dest, 0o755); } catch {}
1142
+ } else {
1143
+ info("Status line script unchanged");
1144
+ }
1145
+
1146
+ const parsed = readSettingsJson();
1147
+ if (parsed === null && fs.existsSync(SETTINGS_JSON)) {
1148
+ warn("settings.json has invalid JSON — cannot configure status line");
1149
+ return;
1150
+ }
1151
+ const settings = parsed || {};
1152
+ const desiredCmd = `bash ${dest}`;
1153
+ const existing = settings.statusLine;
1154
+ // Set only when absent or already ours (command references our script).
1155
+ const isOurs =
1156
+ existing &&
1157
+ existing.type === "command" &&
1158
+ typeof existing.command === "string" &&
1159
+ existing.command.includes(STATUSLINE_SCRIPT);
1160
+ if (existing && !isOurs) {
1161
+ info("Custom status line present — leaving it untouched");
1162
+ return;
1163
+ }
1164
+ if (isOurs && existing.command === desiredCmd) {
1165
+ info("Status line already configured");
1166
+ return;
1167
+ }
1168
+ if (isSymlink(SETTINGS_JSON)) {
1169
+ warn("Skipping settings.json write — target is a symlink");
1170
+ return;
1171
+ }
1172
+ settings.statusLine = { type: "command", command: desiredCmd };
1173
+ try {
1174
+ fs.writeFileSync(SETTINGS_JSON, JSON.stringify(settings, null, 2));
1175
+ success("Status line configured in settings.json");
1176
+ } catch (e) {
1177
+ warn(`Failed to write settings.json: ${e.message}`);
1178
+ }
1179
+ }
1180
+
1109
1181
  // ─── Figma MCP ──────────────────────────────────────────────────────────────
1110
1182
 
1111
1183
  const FIGMA_MCP_URL = "https://mcp.figma.com/mcp";
@@ -1188,6 +1260,8 @@ const GLOBAL_BIN_TOOLS = [
1188
1260
  "gsd-t-traceability-gate.cjs",
1189
1261
  // M85 — Model-tier policy single source of truth (resolver + predicate).
1190
1262
  "gsd-t-model-tier-policy.cjs",
1263
+ // M86 — Model-profile config + resolver CLI (standard/pro/premium tier-spend switch).
1264
+ "gsd-t-model-profile.cjs",
1191
1265
  ];
1192
1266
 
1193
1267
  function installGlobalBinTools() {
@@ -1537,9 +1611,12 @@ async function doInstall(opts = {}) {
1537
1611
  heading("Auto-Route (UserPromptSubmit)");
1538
1612
  installAutoRoute();
1539
1613
 
1540
- heading("In-Session Hooks (Conversation Capture + Token Usage)");
1614
+ heading("In-Session Hooks (Conversation Capture + Token Usage + Ctx Cue)");
1541
1615
  installInSessionHooks();
1542
1616
 
1617
+ heading("Status Line");
1618
+ installStatusLine();
1619
+
1543
1620
  heading("Figma MCP (Design-to-Code)");
1544
1621
  configureFigmaMcp();
1545
1622
 
@@ -2484,6 +2561,8 @@ const PROJECT_BIN_TOOLS = [
2484
2561
  // M85 — Model-tier policy resolver, so command invokers in consumer projects
2485
2562
  // can resolve stage tiers at invoke time (M69 injection pattern).
2486
2563
  "gsd-t-model-tier-policy.cjs",
2564
+ // M86 — Model-profile config + resolver CLI (standard/pro/premium tier-spend switch).
2565
+ "gsd-t-model-profile.cjs",
2487
2566
  ];
2488
2567
 
2489
2568
  // Files that older versions of this installer copied into project bin/ but
@@ -2543,7 +2622,37 @@ function _matchedStraySignature(name, content) {
2543
2622
  return null;
2544
2623
  }
2545
2624
 
2625
+ // Self-protection identity check: is this project dir GSD-T's own source repo?
2626
+ // Guards BOTH the bin-tool copy loop (copying the installed package's tools over
2627
+ // the source repo reverts in-flight work — Red Team M86 r2 HIGH, fired live) and
2628
+ // the deprecated-stray sweep (signature-matching bin/gsd-t.js — the installer
2629
+ // itself — would delete the source file). Identity is by package.json name, NOT
2630
+ // by path — when update-all runs from the globally-installed package, PKG_ROOT
2631
+ // points to the global install and realpath comparison against the local source
2632
+ // always fails.
2633
+ function _isGsdTSourcePackage(projectDir) {
2634
+ try {
2635
+ const pkgPath = path.join(projectDir, "package.json");
2636
+ if (!fs.existsSync(pkgPath)) return false;
2637
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
2638
+ return !!pkg && pkg.name === "@tekyzinc/gsd-t";
2639
+ } catch {
2640
+ return false;
2641
+ }
2642
+ }
2643
+
2546
2644
  function copyBinToolsToProject(projectDir, projectName) {
2645
+ // Self-protection for the COPY loop, not just the stray sweep below: the GSD-T
2646
+ // source repo is legitimately registered as a project during development, and
2647
+ // copying the installed package's bin tools over it REVERTS in-flight work —
2648
+ // fired live during M86 verify (Red Team r2 HIGH: a 4.4.11 update-all clobbered
2649
+ // the committed M86 policy module mid-run; every model-profile call then
2650
+ // hard-crashed until restored from HEAD). The source repo IS the canonical
2651
+ // origin of these tools; propagating into it is wrong in every case.
2652
+ if (_isGsdTSourcePackage(projectDir)) {
2653
+ info(`${projectName} — GSD-T source repo: bin propagation skipped (canonical origin)`);
2654
+ return false;
2655
+ }
2547
2656
  const projectBinDir = path.join(projectDir, "bin");
2548
2657
  if (!fs.existsSync(projectBinDir)) {
2549
2658
  try {
@@ -2577,25 +2686,8 @@ function copyBinToolsToProject(projectDir, projectName) {
2577
2686
  }
2578
2687
  }
2579
2688
  }
2580
- // Self-protection: NEVER sweep GSD-T's own source repo. Without this guard,
2581
- // running `gsd-t update-all` with the GSD-T source repo itself registered
2582
- // as a project (legitimate during development) would signature-match
2583
- // bin/gsd-t.js — which IS the installer — and delete the source file.
2584
- // Identity is by package.json name, NOT by path — when update-all runs from
2585
- // the globally-installed package, PKG_ROOT points to the global install and
2586
- // realpath comparison against the local source always fails.
2587
- const isSourcePackage = (() => {
2588
- try {
2589
- const pkgPath = path.join(projectDir, "package.json");
2590
- if (!fs.existsSync(pkgPath)) return false;
2591
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
2592
- return pkg && pkg.name === "@tekyzinc/gsd-t";
2593
- } catch {
2594
- return false;
2595
- }
2596
- })();
2597
2689
  let cleaned = 0;
2598
- if (!isSourcePackage) {
2690
+ if (!_isGsdTSourcePackage(projectDir)) {
2599
2691
  for (const stray of DEPRECATED_BIN_STRAYS) {
2600
2692
  const strayPath = path.join(projectBinDir, stray);
2601
2693
  if (!fs.existsSync(strayPath)) continue;
@@ -4590,6 +4682,16 @@ if (require.main === module) {
4590
4682
  });
4591
4683
  process.exit(res.status == null ? 1 : res.status);
4592
4684
  }
4685
+ case "model-profile": {
4686
+ // M86 — `gsd-t model-profile` thin dispatcher to the profile config +
4687
+ // resolver (standard/pro/premium tier-spend switch, per-project config).
4688
+ const { spawnSync } = require("child_process");
4689
+ const js = path.join(__dirname, "gsd-t-model-profile.cjs");
4690
+ const res = spawnSync(process.execPath, [js, ...args.slice(1)], {
4691
+ stdio: "inherit",
4692
+ });
4693
+ process.exit(res.status == null ? 1 : res.status);
4694
+ }
4593
4695
  case "metrics":
4594
4696
  doMetrics(args.slice(1));
4595
4697
  break;
@@ -19,7 +19,36 @@ preflight → brief → Cycle 1 (diagnose → hypothesis → fix → test → re
19
19
 
20
20
  Read `.gsd-t/progress.md`. Note any failing tests or runtime errors in the Decision Log.
21
21
 
22
- ## Step 2: Invoke the debug Workflow
22
+ ## Step 2: Resolve the active model profile (M86 — invoke-time injection)
23
+
24
+ Before calling the Workflow, resolve the active model profile to build the `overrides` map:
25
+
26
+ ```bash
27
+ # Run via Bash at invoke time:
28
+ gsd-t model-profile resolve --json
29
+ # Bare form (NO --profile flag): reads .gsd-t/model-profile.json — profile AND stageOverrides
30
+ # (set-stage overrides MUST win — contract precedence; --profile is a config-blind diagnostic
31
+ # form that ZEROES stageOverrides and must never be used for invocation — Red Team M86 r3)
32
+ # Output: { "ok": true, "profile": "...", "overrides": { "stage-key": "concrete-model-id", ... } }
33
+ ```
34
+
35
+ **Resolver-failure handling (M86 — SC(f), pre-mortem c2 #2):** if the resolve call fails
36
+ (`{ok:false}`, spawn error, or the `model-profile` subcommand is not present in the installed
37
+ binary), do NOT silently proceed on the premium fallback. Either:
38
+ - HALT with `blocked-needs-human` and explain the resolver is unavailable; OR
39
+ - Proceed ONLY with a **loud, surfaced warning** that names the effective posture:
40
+
41
+ ```
42
+ ⚠ model-profile resolver unavailable — running on PREMIUM fallback literals
43
+ (configured profile unknown; stale global binary may lack model-profile subcommand)
44
+ ```
45
+
46
+ Also surface a SUCCESSFUL resolve that carries a `configError` field (the resolver returns a
47
+ named default + `configError` for malformed/hand-edited configs — Red Team M86): print the
48
+ `configError` as a visible warning naming the effective profile before proceeding. A clean-looking
49
+ run on a posture the user did not configure is the same silent-spend failure class.
50
+
51
+ ## Step 3: Invoke the debug Workflow
23
52
 
24
53
  Call the `Workflow` tool with:
25
54
 
@@ -31,14 +60,18 @@ Call the `Workflow` tool with:
31
60
  scriptPath: "<absolute path printed by `gsd-t workflow-path debug`>",
32
61
  args: {
33
62
  symptom: "describe the failing test or runtime error in one sentence",
34
- projectDir: "."
63
+ projectDir: ".",
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.
66
+ // Pass {} when the resolver failed AND you chose the loud-warning path (not halt).
67
+ overrides: { /* ...from resolver result.overrides, or {} on failure */ }
35
68
  }
36
69
  }
37
70
  ```
38
71
 
39
72
  The Workflow runs up to 2 cycles. Cycle 2 receives Cycle 1's failed hypothesis to prevent re-trying the same approach.
40
73
 
41
- ## Step 3: Interpret the result
74
+ ## Step 4: Interpret the result
42
75
 
43
76
  ```js
44
77
  {
@@ -14,7 +14,32 @@ preflight → brief (kind=design-decompose) → design agent (opus, with phase p
14
14
 
15
15
  Capture the design reference from `$ARGUMENTS` (Figma URL / image path). If Figma MCP is configured, the agent uses it to extract tokens. Read any existing `.gsd-t/contracts/design/` to avoid duplication.
16
16
 
17
- ## Step 2: Invoke the phase Workflow
17
+ ## Step 2: Resolve the active model profile (M86 — invoke-time injection)
18
+
19
+ Before calling the Workflow, resolve the active model profile to build the `overrides` map:
20
+
21
+ ```bash
22
+ # Run via Bash at invoke time:
23
+ gsd-t model-profile resolve --json
24
+ # Bare form (NO --profile flag): reads .gsd-t/model-profile.json — profile AND stageOverrides
25
+ # (set-stage overrides MUST win — contract precedence; --profile is a config-blind diagnostic
26
+ # form that ZEROES stageOverrides and must never be used for invocation — Red Team M86 r3)
27
+ ```
28
+
29
+ **Resolver-failure handling (M86 — pre-mortem c2 #2):** if the resolve call fails, do NOT
30
+ silently proceed on the premium fallback. Either HALT with `blocked-needs-human`, or proceed
31
+ ONLY with a loud, surfaced warning:
32
+ ```
33
+ ⚠ model-profile resolver unavailable — running on PREMIUM fallback literals
34
+ (configured profile unknown; stale global binary may lack model-profile subcommand)
35
+ ```
36
+
37
+ Also surface a SUCCESSFUL resolve that carries a `configError` field (the resolver returns a
38
+ named default + `configError` for malformed/hand-edited configs — Red Team M86): print the
39
+ `configError` as a visible warning naming the effective profile before proceeding. A clean-looking
40
+ run on a posture the user did not configure is the same silent-spend failure class.
41
+
42
+ ## Step 3: Invoke the phase Workflow
18
43
 
19
44
  ```js
20
45
  {
@@ -25,7 +50,10 @@ Capture the design reference from `$ARGUMENTS` (Figma URL / image path). If Figm
25
50
  args: {
26
51
  phase: "design-decompose",
27
52
  projectDir: ".",
28
- userInput: "$ARGUMENTS"
53
+ userInput: "$ARGUMENTS",
54
+ // M86: inject the resolved overrides map (probe + judge use this).
55
+ // Pass {} when the resolver failed AND you chose the loud-warning path (not halt).
56
+ overrides: { /* ...from resolver result.overrides, or {} on failure */ }
29
57
  // M84 Competition Mode is AUTOMATIC — do NOT pass `competition` by default.
30
58
  // The workflow probes (opus) and self-decides; it competes when a design is
31
59
  // ambiguous or the element/widget/page boundaries aren't obvious (a blind,
@@ -37,7 +65,7 @@ Capture the design reference from `$ARGUMENTS` (Figma URL / image path). If Figm
37
65
 
38
66
  **Competition Mode (`--competition N`).** When a design is ambiguous or the element/widget/page boundaries aren't obvious, `/gsd-t-design-decompose --competition 3` fans out N candidate decompositions and a blind, different-model rubric judge picks the best. Parse N (clamped 2..5). See `.gsd-t/contracts/competition-mode-contract.md`. Default off.
39
67
 
40
- ## Step 3: Interpret the result
68
+ ## Step 4: Interpret the result
41
69
 
42
70
  The Workflow returns `{ status, artifacts, summary, decisions }` (plus `competition: { n, winner, ranked }` when Competition Mode ran).
43
71
 
@@ -14,7 +14,32 @@ The agent identifies the full blast radius of recent code changes and updates ev
14
14
 
15
15
  Read the recent commit range (or `$ARGUMENTS` if a specific scope is given) and `.gsd-t/progress.md` Decision Log to learn what changed.
16
16
 
17
- ## Step 2: Invoke the phase Workflow
17
+ ## Step 2: Resolve the active model profile (M86 — invoke-time injection)
18
+
19
+ Before calling the Workflow, resolve the active model profile to build the `overrides` map:
20
+
21
+ ```bash
22
+ # Run via Bash at invoke time:
23
+ gsd-t model-profile resolve --json
24
+ # Bare form (NO --profile flag): reads .gsd-t/model-profile.json — profile AND stageOverrides
25
+ # (set-stage overrides MUST win — contract precedence; --profile is a config-blind diagnostic
26
+ # form that ZEROES stageOverrides and must never be used for invocation — Red Team M86 r3)
27
+ ```
28
+
29
+ **Resolver-failure handling (M86 — pre-mortem c2 #2):** if the resolve call fails, do NOT
30
+ silently proceed on the premium fallback. Either HALT with `blocked-needs-human`, or proceed
31
+ ONLY with a loud, surfaced warning:
32
+ ```
33
+ ⚠ model-profile resolver unavailable — running on PREMIUM fallback literals
34
+ (configured profile unknown; stale global binary may lack model-profile subcommand)
35
+ ```
36
+
37
+ Also surface a SUCCESSFUL resolve that carries a `configError` field (the resolver returns a
38
+ named default + `configError` for malformed/hand-edited configs — Red Team M86): print the
39
+ `configError` as a visible warning naming the effective profile before proceeding. A clean-looking
40
+ run on a posture the user did not configure is the same silent-spend failure class.
41
+
42
+ ## Step 3: Invoke the phase Workflow
18
43
 
19
44
  ```js
20
45
  {
@@ -25,12 +50,15 @@ Read the recent commit range (or `$ARGUMENTS` if a specific scope is given) and
25
50
  args: {
26
51
  phase: "doc-ripple",
27
52
  projectDir: ".",
28
- userInput: "$ARGUMENTS"
53
+ userInput: "$ARGUMENTS",
54
+ // M86: inject the resolved overrides map.
55
+ // Pass {} when the resolver failed AND you chose the loud-warning path (not halt).
56
+ overrides: { /* ...from resolver result.overrides, or {} on failure */ }
29
57
  }
30
58
  }
31
59
  ```
32
60
 
33
- ## Step 3: Interpret the result
61
+ ## Step 4: Interpret the result
34
62
 
35
63
  The Workflow returns `{ status, artifacts, summary, decisions }`.
36
64
 
@@ -500,7 +500,21 @@ Use these when user asks for help on a specific command:
500
500
  - **Files**: `bin/gsd-t-model-tier-policy.cjs` (zero external deps — installer invariant).
501
501
  - **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.
502
502
  - **CLI**: `gsd-t model-tier-policy resolve <stageKey> [--json]`. Emits `{ok, stageKey, tier, model, requiresThinkingOmitted}`. Exit 0 resolved · 1 unknown stage key.
503
- - **Contract**: `.gsd-t/contracts/model-tier-policy-contract.md` v1.0.0 STABLE.
503
+ - **Contract**: `.gsd-t/contracts/model-tier-policy-contract.md` v1.1.0 STABLE.
504
+
505
+ ### model-profile (M86)
506
+ - **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 Fable — pre-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)).
507
+ - **Files**: `bin/gsd-t-model-profile.cjs` (zero external deps — installer invariant). Config: `.gsd-t/model-profile.json` (`{ "profile": "pro", "stageOverrides": { ... } }`).
508
+ - **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.
509
+ - **CLI**:
510
+ - `gsd-t model-profile show [--json]` — display active profile + per-stage resolution
511
+ - `gsd-t model-profile set <standard|pro|premium>` — switch the project profile
512
+ - `gsd-t model-profile set-stage <stage> <tier>` — per-stage override (rejects `competition-producers` and `competition-judge→opus` — M82 blindness clamps)
513
+ - `gsd-t model-profile resolve --json` — resolve the ACTIVE config (profile + stageOverrides) into the overrides envelope consumed by workflow invokers (the invoker form)
514
+ - `gsd-t model-profile resolve --profile <p> [stage] [--json]` — diagnostic form: pure profile envelope, stageOverrides ZEROED by design (census/divergence checks only — never for invocation)
515
+ - Emits `{ok, profile, overrides: { "<stage>": "<concreteModelId>" }, requiresThinkingOmitted?}`. Exit 0 resolved · 1 unknown profile/tier.
516
+ - **Out of scope**: Session default model (`/model`) — profiles govern WORKFLOW STAGES only.
517
+ - **Contract**: `.gsd-t/contracts/model-profile-config-contract.md` v1.0.0 STABLE.
504
518
 
505
519
  ## Unknown Command
506
520
 
@@ -14,7 +14,32 @@ The agent analyzes the downstream effects of proposed changes: what might break,
14
14
 
15
15
  Read `.gsd-t/progress.md`, the relevant domain `tasks.md`, and `docs/architecture.md`/`.gsd-t/contracts/` for the surfaces in scope.
16
16
 
17
- ## Step 2: Invoke the phase Workflow
17
+ ## Step 2: Resolve the active model profile (M86 — invoke-time injection)
18
+
19
+ Before calling the Workflow, resolve the active model profile to build the `overrides` map:
20
+
21
+ ```bash
22
+ # Run via Bash at invoke time:
23
+ gsd-t model-profile resolve --json
24
+ # Bare form (NO --profile flag): reads .gsd-t/model-profile.json — profile AND stageOverrides
25
+ # (set-stage overrides MUST win — contract precedence; --profile is a config-blind diagnostic
26
+ # form that ZEROES stageOverrides and must never be used for invocation — Red Team M86 r3)
27
+ ```
28
+
29
+ **Resolver-failure handling (M86 — pre-mortem c2 #2):** if the resolve call fails, do NOT
30
+ silently proceed on the premium fallback. Either HALT with `blocked-needs-human`, or proceed
31
+ ONLY with a loud, surfaced warning:
32
+ ```
33
+ ⚠ model-profile resolver unavailable — running on PREMIUM fallback literals
34
+ (configured profile unknown; stale global binary may lack model-profile subcommand)
35
+ ```
36
+
37
+ Also surface a SUCCESSFUL resolve that carries a `configError` field (the resolver returns a
38
+ named default + `configError` for malformed/hand-edited configs — Red Team M86): print the
39
+ `configError` as a visible warning naming the effective profile before proceeding. A clean-looking
40
+ run on a posture the user did not configure is the same silent-spend failure class.
41
+
42
+ ## Step 3: Invoke the phase Workflow
18
43
 
19
44
  ```js
20
45
  {
@@ -26,12 +51,15 @@ Read `.gsd-t/progress.md`, the relevant domain `tasks.md`, and `docs/architectur
26
51
  phase: "impact",
27
52
  milestone: "M{NN}",
28
53
  projectDir: ".",
29
- userInput: "$ARGUMENTS"
54
+ userInput: "$ARGUMENTS",
55
+ // M86: inject the resolved overrides map.
56
+ // Pass {} when the resolver failed AND you chose the loud-warning path (not halt).
57
+ overrides: { /* ...from resolver result.overrides, or {} on failure */ }
30
58
  }
31
59
  }
32
60
  ```
33
61
 
34
- ## Step 3: Interpret the result
62
+ ## Step 4: Interpret the result
35
63
 
36
64
  The Workflow returns `{ status, artifacts, summary, decisions }`.
37
65