Files
msd-core/gsd-core/references/model-profiles.md
Tom Boucher 8442d984b9 fix(#3809): route runtime-loaded markdown through the gsd_run launcher (#3815)
* test(#3809): generalize dead-ref guard into a rule table (failing first)

The #2020 guard hardcoded `sdk/(src|dist|handlers)/` — the three dead paths
that had caused that storm. That proved those three paths were gone and said
nothing about the class, so #3809 reproduced the identical Windows find.exe
storm under a different token and the guard could not see it.

Replaces the single regex with a rule table over the same runtime-loaded
markdown surface, adds `commands/` to the scan set (previously uncovered),
and adds rule B: the runtime shim filename must never appear in command
position, because it is not a PATH command and an agent that meets it falls
back to locating the file.

Rule B's matcher is deliberately lenient — the launcher's own resolver
assignment, `node <path>/<shim>` calls, bare paths, and prose that names the
file all stay unflagged, each pinned by a negative-space row.

This commit is expected to FAIL: 50 offenders across 23 files remain in the
tree. The remediation lands next.

Refs #3809

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(#3809): route every workflow call through the gsd_run launcher

50 places across 23 runtime-loaded workflow, agent, reference, and command
files instructed the agent to run the runtime shim by filename. That filename
is not on PATH under any name -- package.json ships gsd-core, gsd-tools,
gsd_run and gsd-mcp-server -- so the call exited 127, the file-shaped token
sent the agent looking for the file, and on Git Bash for Windows the resulting
`find /` walked the entire drive (7268 CPU-seconds in the report) until
somebody killed it by hand.

CONTEXT.md -> Runtime Launcher Module already makes gsd_run the single entry
point: "Canonical space-safe shell preamble (`gsd_run`) used by every workflow
bash block to invoke the GSD runtime CLI." These sites predate that rule --
they trace to 0e6907050 (docs(#195): migrate workflow markdown off gsd-sdk
query), which swapped one non-PATH token for another.

Two further instances of the same class surfaced during remediation and are
fixed here rather than left for later:

  - references/model-profiles.md prescribed `node <shim> effort sync` with no
    path at all; node resolves a bare filename against cwd, so it fails the
    same way.
  - references/universal-anti-patterns.md rule 25 instructed every agent to
    "use <shim>" when shelling out. That rule did not contain the defect, it
    prescribed it repo-wide.

Five "(or legacy <shim>)" parentheticals left dangling by the substitution are
removed; after the rewrite they offered the non-resolving form as an
alternative.

The guard from the previous commit now passes. Its node-prefix exemption was
tightened to require a path separator, which is what exposed model-profiles.

Fixes #3809

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(#3809): key the guard on the CLI's whole verb roster, not observed usage

Review found the first cut of rule B repeating the very mistake it exists to
prevent. Its verb set held query, commit and effort -- the verbs that happened
to appear in the tree -- so it could not see `<shim> phase add`,
`<shim> state load`, `<shim> verify ...` or twenty-odd other real single-word
subcommands. A guard that only recognises yesterday's offenders is not a guard.

The set is now the CLI's full advertised roster, unioned from the usage banner
and HOST_COMMAND_ROUTERS (which carries verification, planning, uat, stats,
todo and windows, all absent from the banner).

Widening it immediately caught a live offender the first pass had missed:
references/planning-config.md prescribed `node <shim> worktree set-baseref`
with no path. Fixed here.

Also drops the "a hyphen or a dot means subcommand" heuristic, which was
unsound for prose -- it flagged `built-in` and `v1.2`. Detection now keys
entirely on the roster, testing the first dot-segment so that phase.add and
state.patch still match while prose does not. Both false positives are pinned
as negative-space rows.

Guard verified against the pre-fix tree at origin/next: 52 offenders across 25
files, and 0 after this branch's remediation.

Refs #3809

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(#3809): derive the verb roster from the router; repair launcher parity

Standards review caught the guard repeating the defect it exists to prevent.
Its verb list was a hand-copied literal -- and worse, transcribed from an
INSTALLED older binary, so it was missing 22 verbs this tree actually ships
(websearch, windows, state-snapshot, context-predicates and the dispatch-*
family among them). gsd-tools.cjs already carries three hand-maintained
rosters whose drift is a named defect pinned by the parity test in
tests/commands.test.cjs; a hand-copied fourth was that same defect wearing a
guard's clothes.

The roster is now derived from HOST_COMMAND_ROUTERS + TOP_LEVEL_USAGE, lazily
and memoised, with `query` supplemented explicitly -- it dispatches through
the routing hub ahead of the host-router table, so it appears in neither
export, yet 45 of the 50 offenders used it. A parity test pins the derivation.

Two regressions this branch introduced, both caught by the remote runner:

  - runtime-launcher-parity: rewriting a comment in gsd-research-synthesizer.md
    put a `gsd_run` token at line 65 while the canonical preamble sits at 158,
    breaking "exactly ONE preamble, before the first gsd_run call". The comment
    is descriptive and needs no command token at all; it now names none.
  - The #2751 guard's PROSE_ALLOWLIST entry for that same line went stale once
    the line stopped carrying a bare mention. Pruned, exactly as that guard's
    own stale-entry test instructs.

Also corrects git-planning-commit.md, where the first pass rewrote only the
trailing "legacy" clause and left the sentence reading backwards.

Note the #2751 guard and this one are complementary, not duplicates: its regex
requires whitespace immediately after `gsd-tools`, so it cannot match the
`.cjs` form, and this one only matches the `.cjs` form.

Refs #3809

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(#2751): extend the bare-command guard to references/ and commands/

The #2751 guard has only ever scanned agents/ and gsd-core/workflows/. Two
runtime-loaded directories were never in its scan set, and 47 bare
`gsd-tools <verb>` calls had accumulated there unseen -- the same defect that
guard exists to catch, in the rooms it never entered.

  - gsd-core/references/: 37 calls, all rewritten to gsd_run. references are
    fragments inlined into a parent that defines the launcher, which is why 21
    of the 22 files already using gsd_run carry no local preamble.
  - commands/gsd/: 10 operative calls rewritten. The remaining 10 are
    descriptive prose ("resolved inside the workflow via ...") and are
    allowlisted with reasons, bringing PROSE_ALLOWLIST to 15.

commands/ also came under launcher propagation. sync-runtime-launcher.cjs
walked only WORKFLOWS_DIR and AGENTS_DIR, so every preamble under commands/
was a hand-pasted copy nothing propagated and no test checked -- graphify.md
had accumulated five. It now walks COMMANDS_DIR too, which collapses those
five to the canonical one-per-file, and runtime-launcher-parity gains a
(B-commands) arm mirroring (B-agents) exactly so the placement stays honest.

The parity arm keys on shell blocks, so commands/gsd/workstreams.md and
config.md -- which name gsd_run only in inline backtick prose -- are exempt,
as they should be. gsd_run is itself a shipped npm bin, so those inline
instructions resolve from PATH exactly as the gsd-tools form they replace did.

skills/ is deliberately NOT added to either guard's scan set: it is generated
from commands/ and pinned by lint:generated-sync, so guarding the source
guards both, and scanning the mirror would double-report every future
offender. Regenerated here.

Refs #2751, #3809

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(#3809): acknowledge the one emitted file this change grows

The emitted-attribution gate failed on the previous sha: gsd-research-synthesizer.md
grew 3 bytes (13847 -> 13850) with no acknowledgment. The substitution SHRANK the
other 19 emitted files, which is why the growth arm was not expected to fire at all.

The 3 bytes are unavoidable. Line 65 is a descriptive comment inside a fenced block;
naming any command there puts a gsd_run token ahead of the file's canonical preamble
at line 158, which runtime-launcher-parity's (B-agents) arm correctly rejects. So the
comment names no command and says where the config is actually loaded instead, which
reads longer than the token it replaced.

Acks only the path the gate reported, per the fragment rules.

Refs #3809

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* revert(#2751): drop the commands/ half — three contracts pin it in place

The remote runner refuted the commands/ extension outright. Reverting it and
keeping the references/ conversion, which passed.

What broke, all of it caused by bringing commands/ under launcher propagation:

  - graphify.md's five per-block preambles are LOAD-BEARING, not accumulated
    drift. tests/graphify-visualization.test.cjs extracts individual Step-3
    shell chains and executes them standalone, so each fenced block needs its
    own definition of gsd_run. Collapsing them to the canonical one-per-file
    produced `bash: gsd_run: command not found`, exit 127, across four tests.
    The "define once per file" contract holds for workflows and agents because
    nothing extracts their blocks in isolation; commands/ is not like that.
  - explore.md broke "the preamble that DEFINES gsd_run must appear before the
    first USE of gsd_run anywhere in the file".
  - tests/gsd-tools-path-refs.test.cjs (#1766) ASSERTS that
    commands/gsd/workstreams.md contains the literal string
    `gsd-tools query workstream.list`. Rewriting it to gsd_run contradicts a
    test that pins the opposite, so the two guards disagree about that file by
    construction.

So commands/ is not a scan-set widening. It needs those contracts reconciled
first, and that is its own change. SCAN_DIRS keeps gsd-core/references/ and
drops commands/, the ten commands/ allowlist entries go with it (back to 5),
and the reasoning is recorded in the guard itself so the next person does not
rediscover it by burning a matrix run.

commands/gsd/import.md keeps its #3809 fix — that one is the .cjs form this
PR exists to remove, and it is untouched by any of the above.

Refs #2751, #3809

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* revert(#3809): restore explore.md's Step 1 preamble placement

Running the launcher sync script processed workflows/ and agents/ too, not
just the commands/ directory the run was aimed at, and it MOVED
gsd-core/workflows/explore.md's preamble from Step 1 down to Step 3.

The script inserts into the first bash block that USES gsd_run. explore.md's
Step 1 block only DEFINES it, and that placement is deliberate -- the file
says so on the line above: "Placed in Step 1 rather than Step 3 so declining
the research offer cannot leave Step 5's commit call unbootstrapped."
tests/explore-command.test.cjs pins it.

explore.md carried no #3809 offender, so reverting it costs this fix nothing.
This was collateral from invoking the sync script at all, not from the
COMMANDS_DIR change, which is why the earlier commands/ revert did not catch it.

Refs #3809

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(#3809): backfill PR number into changeset fragments

pr:0 -> pr:3815 for both fragments now that the PR exists.

Refs #3809

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(#3809): drop the hand-rolled regex escaper CodeQL flagged

CodeQL raised js/incomplete-sanitization (HIGH) on the guard's pattern build:
`SHIM.replace(/\./g, '\\.')` escapes the dot and nothing else, so it does not
escape backslashes. It blocked PR #3815.

The repo already bans this shape -- local/no-adhoc-regex-escape exists exactly
to stop hand-rolled escapers, with the canonical one in src/pattern.cts. Rather
than reach for that helper, the pattern now carries no escaping logic at all:
SHIM is a compile-time constant whose only metacharacter is the dot, so the
regex source is spelled out literally. The generated source string is
byte-identical to what the replace() produced, verified before and after --
0 offenders on this tree, 52 against origin/next, unchanged.

A drift pin asserts SHIM_PATTERN still matches SHIM exactly, and that the dot
is escaped rather than acting as a wildcard, so the two cannot separate.

Refs #3809

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 11:47:37 -04:00

14 KiB

Model Profiles

Model profiles control which Claude model each GSD agent uses. This allows balancing quality vs token spend, or inheriting the currently selected session model.

Profile Definitions

Agent quality balanced budget adaptive inherit
gsd-planner opus opus sonnet opus inherit
gsd-roadmapper opus sonnet sonnet sonnet inherit
gsd-executor opus sonnet sonnet sonnet inherit
gsd-phase-researcher opus sonnet haiku sonnet inherit
gsd-project-researcher opus sonnet haiku sonnet inherit
gsd-research-synthesizer sonnet sonnet haiku haiku inherit
gsd-debugger opus sonnet sonnet opus inherit
gsd-codebase-mapper sonnet haiku haiku haiku inherit
gsd-verifier sonnet sonnet haiku sonnet inherit
gsd-plan-checker sonnet sonnet haiku haiku inherit
gsd-integration-checker sonnet sonnet haiku haiku inherit
gsd-nyquist-auditor sonnet sonnet haiku haiku inherit

Per-Phase-Type Model Map (#3023)

.planning/config.json accepts a coarse per-phase-type map under the models key. Use this when you want tuning at the phase level ("Opus for planning and execution, Sonnet for the rest") without learning the agent taxonomy.

{
  "model_profile": "balanced",
  "models": {
    "planning": "opus",
    "discuss": "opus",
    "research": "sonnet",
    "execution": "opus",
    "verification": "sonnet",
    "completion": "sonnet"
  },
  "model_overrides": {
    "gsd-codebase-mapper": "haiku"
  }
}

Phase-type → agent mapping

Phase type Agents
planning gsd-planner, gsd-roadmapper, gsd-pattern-mapper
discuss gsd-assumptions-analyzer
research gsd-phase-researcher, gsd-project-researcher, gsd-research-synthesizer, gsd-codebase-mapper, gsd-ui-researcher
execution gsd-executor, gsd-debugger, gsd-doc-writer
verification gsd-verifier, gsd-plan-checker, gsd-integration-checker, gsd-nyquist-auditor, gsd-ui-checker, gsd-ui-auditor, gsd-doc-verifier, gsd-code-reviewer
completion (reserved — no subagent today)

Resolution precedence (highest to lowest)

  1. Per-agent model_overrides[agent] — full IDs accepted; targeted exceptions
  2. Phase-type models[phase_type] — tier alias only (opus / sonnet / haiku / inherit)
  3. Profile table — the per-agent column from the active model_profile
  4. Runtime default — when nothing else applies

Why two layers above the profile?

  • Profile is a global tier strategy (everyone runs balanced).
  • models is coarse phase-level tuning without learning agent names.
  • model_overrides is per-agent precision (e.g. force haiku on gsd-codebase-mapper for a fan-out).

The three layers compose: models defaults a phase, model_overrides carves an exception out of it.

Profile Philosophy

quality - Maximum reasoning power

  • Opus for all decision-making agents
  • Sonnet for read-only verification
  • Use when: quota available, critical architecture work

balanced (default) - Smart allocation

  • Opus only for planning (where architecture decisions happen)
  • Sonnet for execution and research (follows explicit instructions)
  • Sonnet for verification (needs reasoning, not just pattern matching)
  • Use when: normal development, good balance of quality and cost

budget - Minimal Opus usage

  • Sonnet for anything that writes code
  • Haiku for research and verification
  • Use when: conserving quota, high-volume work, less critical phases

adaptive — Role-based cost optimization

  • Opus for planning and debugging (where reasoning quality has highest impact)
  • Sonnet for execution, research, and verification (follows explicit instructions)
  • Haiku for mapping, checking, and auditing (high volume, structured output)
  • Use when: optimizing cost without sacrificing plan quality, solo development on paid API tiers

inherit - Follow the current session model

  • All agents resolve to inherit
  • Best when you switch models interactively (for example OpenCode or Kilo /model)
  • Required when using non-Anthropic providers (OpenRouter, local models, etc.) — otherwise GSD may call Anthropic models directly, incurring unexpected costs
  • Use when: you want GSD to follow your currently selected runtime model

Using Non-Claude Runtimes (Codex, OpenCode, Gemini CLI, Kilo)

When installed for a non-Claude runtime, the GSD installer sets resolve_model_ids: "omit" in ~/.gsd/defaults.json. This returns an empty model parameter for all agents, so each agent uses the runtime's default model. No manual setup is needed.

To assign different models to different agents, add model_overrides with model IDs your runtime recognizes:

{
  "resolve_model_ids": "omit",
  "model_overrides": {
    "gsd-planner": "o3",
    "gsd-executor": "o4-mini",
    "gsd-debugger": "o3",
    "gsd-codebase-mapper": "o4-mini"
  }
}

The same tiering logic applies: stronger models for planning and debugging, cheaper models for execution and mapping.

Using Claude Code with Non-Anthropic Providers (OpenRouter, Local)

If you're using Claude Code with OpenRouter, a local model, or any non-Anthropic provider, set the inherit profile to prevent GSD from calling Anthropic models for subagents:

# Via settings command
/gsd:settings
# → Select "Inherit" for model profile

# Or manually in .planning/config.json
{
  "model_profile": "inherit"
}

Without inherit, GSD's default balanced profile spawns specific Anthropic models (opus, sonnet, haiku) for each agent type, which can result in additional API costs through your non-Anthropic provider.

Advisor Tool (Claude Code)

Claude Code (v2.1.98+) can pair the session's executor model with a stronger advisor model that it consults mid-generation for strategy and course-correction (Anthropic's advisor tool). This is a host-runtime feature, not a GSD setting — GSD selects each agent's executor model through the profile/tier system above; Claude Code supplies the advisor.

Set it once at the session level with /advisor <model> (or the advisorModel setting / --advisor flag). Subagents inherit the session advisor automatically, so every GSD subagent an orchestrator spawns gets the same advisor with no per-agent configuration. It composes cleanly with GSD's tiering: the profile keeps executors cheap where the work is mechanical, and the advisor adds a stronger reviewer inline on the turns that benefit.

Candidate pairings

Per Anthropic's advisor-tool docs the advisor must be at least as capable as the executor. Candidate pairings by profile — evaluate on your own workload; the quality/cost characterizations below are Anthropic-reported, not GSD guarantees:

Profile Typical executors Candidate advisor Rationale (per Anthropic docs)
budget Haiku / Sonnet Fable 5 or Opus A step up in intelligence over Haiku alone, at lower cost than switching the executor to a larger model
balanced Sonnet Fable 5 or Opus A quality lift at similar or lower total cost than Sonnet-solo on complex tasks
quality / adaptive Opus (planning), Sonnet Fable 5 or Opus Marginal on turns already at top capability; most valuable on the Sonnet-executor agents

Fable 5 is a valid advisor for Haiku 4.5, Sonnet 4.6/5, and Opus 4.8 executors, so it pairs with any tier a profile assigns.

When it's worth enabling

  • Worth it: long, multi-step agent loops where the plan matters but most turns are mechanical — e.g. execute-phase and debug. Anthropic's docs note advisor prompt-caching pays off at roughly three or more advisor calls, which these long loops make.
  • Skip it: short, one-shot agents (mappers, quick audits, single-file checks) — there is little to plan, and the advisor adds cost without a commensurate quality gain.

Constraint: session-level only (today)

The advisor is a single session-wide setting inherited by all subagents; there is no per-agent advisor selection, so GSD cannot vary the advisor by role the way it varies the executor model (e.g. "no advisor on the Haiku mapper, a Fable 5 advisor on the Sonnet executor"). Per-agent advisor control is tracked upstream at anthropics/claude-code#73072; until it lands, pick one session advisor that fits the most valuable agents in your run.

Dynamic Routing with Failure-Tier Escalation (#3024)

When dynamic_routing.enabled = true in .planning/config.json, the resolver picks a model from a tier-mapped table based on the agent's default tier (light / standard / heavy) and escalates to the next tier up on orchestrator-detected soft failure.

{
  "dynamic_routing": {
    "enabled": true,
    "tier_models": {
      "light":    "haiku",
      "standard": "sonnet",
      "heavy":    "opus"
    },
    "escalate_on_failure": true,
    "max_escalations": 1
  }
}

Agent default tiers (each agent in MODEL_PROFILES declares one):

Tier Agents Use case
light gsd-codebase-mapper, gsd-pattern-mapper, gsd-research-synthesizer, gsd-plan-checker, gsd-integration-checker, gsd-nyquist-auditor, gsd-ui-checker, gsd-ui-auditor, gsd-doc-verifier Cheap/fast — pure mappers, scanners, low-stakes audits
standard gsd-executor, gsd-phase-researcher, gsd-project-researcher, gsd-verifier, gsd-doc-writer, gsd-ui-researcher Default workhorse — research, writing, primary verification
heavy gsd-planner, gsd-roadmapper, gsd-debugger Deep reasoning — already at top, can't escalate further

Escalation flow (orchestrator-driven):

  1. Orchestrator spawns agent with attempt: 0 → resolver returns tier_models[default_tier]
  2. If orchestrator marks the result a soft failure, it re-spawns with attempt: 1 → resolver returns tier_models[next_tier_up]
  3. max_escalations caps total retries (default 1). Beyond the cap the resolver returns the cap-tier model so the orchestrator can log without burning further budget.
  4. Hard failures (exceptions) bypass escalation and surface immediately.

Precedence with other tier sources (highest → lowest):

  1. model_overrides[<agent>] — full ID, always wins
  2. dynamic_routing.tier_models[escalated_tier] — when enabled: true
  3. models[<phase_type>] — coarse phase-level (#3023)
  4. model_profile — global tier strategy

When dynamic_routing.enabled = false (default), behavior is identical to today.

Resolution Logic

Orchestrators resolve model before spawning. The full precedence ladder is (highest → lowest):

1. Read .planning/config.json
2. Check model_overrides[<agent>] (full IDs accepted; targeted exceptions)
3. If dynamic_routing.enabled, return tier_models[escalated_tier]
   (see §Dynamic Routing — escalation steps tier up per attempt counter)
4. If no dynamic_routing match, check models[phase_type] for a phase-type tier
   (see §Per-Phase-Type Model Map for the agent → phase-type mapping)
5. If no phase-type slot, look up agent in profile table
6. Pass model parameter to Task call

model and effort resolve through different mechanisms at different times — they do not share the ladder above. model resolves at runtime, per spawn, from .planning/config.json; a config change takes effect on the next spawn. effort (claude runtime) has its own cascade (agent_overrides → routing_tier_defaults → default; see docs/CONFIGURATION.md § "Where effort actually reaches") and is baked at install time into the effort: frontmatter key of ~/.claude/agents/gsd-*.md — Claude Code's Agent tool has no per-spawn effort parameter, so per-agent frontmatter is the only channel. An effort config change has no effect until gsd_run effort sync --apply re-syncs the agent files. Codex agents instead pin model_reasoning_effort in ~/.codex/agents/*.toml at install time.

Per-Agent Overrides

Override specific agents without changing the entire profile:

{
  "model_profile": "balanced",
  "model_overrides": {
    "gsd-executor": "opus",
    "gsd-planner": "haiku"
  }
}

Overrides take precedence over the profile. Valid values: opus, sonnet, haiku, inherit, or any fully-qualified model ID (e.g., "o3", "openai/o3", "google/gemini-2.5-pro").

Switching Profiles

Runtime: /gsd-set-profile <profile>

Per-project default: Set in .planning/config.json:

{
  "model_profile": "balanced"
}

Design Rationale

Why Opus for gsd-planner? Planning involves architecture decisions, goal decomposition, and task design. This is where model quality has the highest impact.

Why Sonnet for gsd-executor? Executors follow explicit PLAN.md instructions. The plan already contains the reasoning; execution is implementation.

Why Sonnet (not Haiku) for verifiers in balanced? Verification requires goal-backward reasoning - checking if code delivers what the phase promised, not just pattern matching. Sonnet handles this well; Haiku may miss subtle gaps.

Why Haiku for gsd-codebase-mapper? Read-only exploration and pattern extraction. No reasoning required, just structured output from file contents.

Why inherit instead of passing opus directly? Claude Code's "opus" alias maps to a specific model version. Organizations may block older opus versions while allowing newer ones. GSD returns "inherit" for opus-tier agents, causing them to use whatever opus version the user has configured in their session. This avoids version conflicts and silent fallbacks to Sonnet.

Why inherit profile? Some runtimes (including OpenCode) let users switch models at runtime (/model). The inherit profile keeps all GSD subagents aligned to that live selection.