diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index a644e1ec54..9952fb0fa1 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -315,6 +315,8 @@ ### Added +- Skill gate for 7 presets -- `gemini`, `muse-spark`, `glm-5-2`, `glm-5-3` carry `load-matching-skills` typed rule, `grok-4.5`, `grok-4.6`, `gpt-5.5` expose a visible-skill terminal gate (`SKILL.md` + `load_skills`) mirroring the DeepSeek/K3 canary verbatim -- loose-match only, 0~3 loads, no always-5. + ### Fixed - Goals no longer stall after a settings hot-reload: a reload `session_start` now re-engages an active goal (re-arming the monitor backstop while wake sources are live, or queueing a continuation through the existing sessionStart admission) instead of parking it until the next user message; stopped goals still never auto-start on reload ([#936](https://github.com/code-yeongyu/senpi/pull/936)). @@ -421,6 +423,7 @@ - `--system-prompt` and `--append-system-prompt` work again on the CLI path, and now compose with per-model prompt presets instead of being clobbered by them. A custom system prompt replaces the generated base and makes the preset step aside (the startup "Optimized system prompt applied" header also stands down); append texts are reattached after a preset replaces the base, so they survive on preset-matching models and across model switches. The CLI flags had been parsed but disconnected since 2026-07-19 because presets overwrote user overrides. Extensions can now read the user overrides via `ctx.getSystemPromptOptions()`, which moved from the command context to the base `ExtensionContext` ([#903](https://github.com/code-yeongyu/senpi/pull/903)). - Settings files now support dependency-free JSONC comments and trailing commas. When both `settings.jsonc` and `settings.json` exist in one config directory, JSONC wins; writes remain on the loaded file, config reload watches both formats, RPC emits `settings_source_selected` with the selected path/format/reason, and the interactive TUI shows the choice at startup or reload ([#902](https://github.com/code-yeongyu/senpi/pull/902)). - GLM 5.3 prompt preset: a new `glm-5.3` system-prompt preset cloned from `glm-5.2` (thin `tuningSection` wrapper over the shared dynamic core, `workstationDialect: "claude"`). The `hasGlm53Signal`/`isGlm53Model` matcher is checked before the 5.2 matcher, `"glm-5.3"` joins `PromptPresetName`/`VALID_PRESETS`, and the settings.md value list is updated. Models selecting GLM 5.3 now get the tuned system prompt instead of the untuned fallback ([#895](https://github.com/code-yeongyu/senpi/pull/895)). +- Gemini and Muse Spark prompt presets: new `gemini` and `muse-spark` system-prompt presets as thin `tuningSection` wrappers over the shared dynamic core (`workstationDialect: "default"`), each carrying its vendor-guidance behaviors as typed rule data (`GEMINI_RULES` / `MUSE_SPARK_RULES`) and ending with a machine-consumed `model-family: ` token. The `hasGeminiSignal` matcher covers the catalog Gemini 3.x Flash ids (`google/gemini-3.6-flash`, `google/gemini-3.1-flash-lite`, `google/gemini-3.5-flash`, `google/gemini-3.5-flash-lite`, `google/gemini-3.7-flash`, plus the unprefixed, `:batch`-tagged, and `-preview` shapes; `-image` variants stay out) and `hasMuseSparkSignal` covers `meta/muse-spark-1.1`, `meta/muse-spark-1.2`, and `meta/muse-spark-1.2-contributor`; truncated ids match neither. Both names join `PromptPresetName`/`VALID_PRESETS` and the settings.md value list. Models selecting these families now get tuned system prompts instead of the untuned fallback ([#899](https://github.com/code-yeongyu/senpi/pull/899)). ### Fixed @@ -492,6 +495,10 @@ ### Fixed +- DeepSeek V4 (flash/flash-0731/pro) and Kimi K3 presets now carry delegation + skill gates: partitioned `task` batch before the local critical path and a terminal visible-skill scan (`SKILL.md` + `load_skills`), mirroring the canary's M1/M2 cure for under-delegation / under-load (see PR #912, draft — ready on ≥20 qualified turns). + +### Fixed + - Extension selectors (including the `/fallback` model picker) now window long option lists around the highlighted row instead of rendering every entry. On large model registries the full list overflowed the viewport and the moved highlight was never painted, so arrow keys and j/k appeared to do nothing even diff --git a/packages/coding-agent/docs/settings.md b/packages/coding-agent/docs/settings.md index 422d11388c..5324da2cce 100644 --- a/packages/coding-agent/docs/settings.md +++ b/packages/coding-agent/docs/settings.md @@ -87,7 +87,7 @@ Permission rules are a confirmation policy, not a sandbox. Senpi, extensions, pa | `modelThinkingLevels` | object | - | Per-model reasoning effort memory (`"provider/id": "level"`) | | `modelLastOnThinkingLevels` | object | - | Per-model last non-off reasoning level, used by `/reasoning on` to restore the previous effort | | `modelServiceTiers` | object | - | Per-model service tier memory (`"provider/id": "auto" \| "priority"`) | -| `promptPreset` | string | `"auto"` | Force a system prompt preset: `"auto"`, `"kimi-k2-6"`, `"kimi-k2-7"`, `"kimi-k3"`, `"glm-5.2"`, `"glm-5.3"`, `"grok-4.5"`, `"grok-4.6"`, `"claude-fable-5"`, `"claude-opus-5"`, `"claude-opus-4-5"`, `"claude-opus-4-6"`, `"claude-opus-4-7"`, `"claude-opus-4-8"`, `"deepseek-v4-flash"`, `"deepseek-v4-flash-0731"`, `"deepseek-v4-pro"`, `"gpt-5"`, `"gpt-5.2"`, `"gpt-5.3-codex"`, `"gpt-5.4"`, `"gpt-5.5"`, or `"gpt-5.6"` | +| `promptPreset` | string | `"auto"` | Force a system prompt preset: `"auto"`, `"kimi-k2-6"`, `"kimi-k2-7"`, `"kimi-k3"`, `"glm-5.2"`, `"glm-5.3"`, `"grok-4.5"`, `"grok-4.6"`, `"claude-fable-5"`, `"claude-opus-5"`, `"claude-opus-4-5"`, `"claude-opus-4-6"`, `"claude-opus-4-7"`, `"claude-opus-4-8"`, `"deepseek-v4-flash"`, `"deepseek-v4-flash-0731"`, `"deepseek-v4-pro"`, `"gemini"`, `"muse-spark"`, `"gpt-5"`, `"gpt-5.2"`, `"gpt-5.3-codex"`, `"gpt-5.4"`, `"gpt-5.5"`, or `"gpt-5.6"` | | `hideThinkingBlock` | boolean | `false` | Hide thinking blocks in output | | `showCacheMissNotices` | boolean | `false` | Show transcript notices for significant prompt-cache misses and compaction or branch-summary usage | | `thinkingBudgets` | object | - | Custom token budgets per thinking level. Anthropic, Google, and Bedrock use these natively. OpenAI-compatible models use them when `compat.thinkingTokenBudgetField` (or `supportsThinkingTokenBudget`) is set. | diff --git a/packages/coding-agent/src/core/dynamic-prompt/style.ts b/packages/coding-agent/src/core/dynamic-prompt/style.ts index c8eb48ebd3..7998700390 100644 --- a/packages/coding-agent/src/core/dynamic-prompt/style.ts +++ b/packages/coding-agent/src/core/dynamic-prompt/style.ts @@ -1,7 +1,7 @@ export function buildStyleSection(): string { return `## Style -Be concise and concrete. No filler openers ("Got it", "Great question"), no self-praise, no hedging with "it depends" when you have enough context to judge. Use bullets only for genuinely list-shaped content. Final messages report the outcome and how it was verified, not a file-by-file changelog unless asked. Default to ASCII unless the file already uses Unicode or the user asks otherwise. +Be concise and concrete. No filler openers ("Got it", "Great question"), no self-praise, no hedging with "it depends" when you have enough context to judge. Use bullets only for genuinely list-shaped content. Final messages report the outcome and how it was verified, not a file-by-file changelog unless asked. Match the user's language: when the user's messages, project context, or ~/.omo/AGENTS.md indicates a language (e.g. Korean), respond in that language; default to the user's language rather than ASCII. Smallest correct change wins. Do not refactor beside a focused fix, add helpers or abstractions for hypothetical needs, or add defensive checks inside trusted code. Trust framework guarantees; validate only at system boundaries. diff --git a/packages/coding-agent/src/core/extensions/builtin/AGENTS.md b/packages/coding-agent/src/core/extensions/builtin/AGENTS.md index c449342326..966015cf52 100644 --- a/packages/coding-agent/src/core/extensions/builtin/AGENTS.md +++ b/packages/coding-agent/src/core/extensions/builtin/AGENTS.md @@ -12,7 +12,7 @@ | 4 | `gpt-apply-patch` | `gpt-apply-patch/` | Codex-style `apply_patch` tool with rich render + freeform grammar | | 5 | `imagegen` | `imagegen/` | Client-side image generation tool plus the bundled `skill/` guidance and shared auth/state used by the native lane | | 6 | `openai-image-gen` | `openai-image-gen/` | OpenAI-native image generation; follows `imagegen` so the native injector's bypass wiring observes the registered client tool | -| 7 | `prompt-preset` | `prompt-preset/` | Per-model system prompts (gpt-5.x, claude-fable-5, claude-opus-5, claude-opus-4-{5,6,7,8}, glm-5.2, glm-5.3, deepseek-v4-{flash,flash-0731,pro}, grok-4.{5,6}, kimi-k2-{6,7}, kimi-k3) | +| 7 | `prompt-preset` | `prompt-preset/` | Per-model system prompts (gpt-5.x, claude-fable-5, claude-opus-5, claude-opus-4-{5,6,7,8}, glm-5.2, glm-5.3, deepseek-v4-{flash,flash-0731,pro}, gemini, muse-spark, grok-4.{5,6}, kimi-k2-{6,7}, kimi-k3) | | 8 | `todowrite` | `todotools/` | Op-based oh-my-pi todo port + `/todo` command; fully diverged from `../pi-extensions/pi-todotools` | | 9 | `redraws` | `redraws.ts` | `/tui` full-redraw count diagnostic | | 10 | `anthropic-web-search` | `anthropic-web-search/` | Anthropic-native web search tool | diff --git a/packages/coding-agent/src/core/extensions/builtin/prompt-preset/AGENTS.md b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/AGENTS.md index 5bac2efece..53d3095c49 100644 --- a/packages/coding-agent/src/core/extensions/builtin/prompt-preset/AGENTS.md +++ b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/AGENTS.md @@ -1,6 +1,6 @@ # builtin/prompt-preset -Builtin extension #3. On `before_agent_start` and `model_select`, picks a system prompt preset by **model family** (gpt-5.x through gpt-5.6, claude-fable-5, claude-opus-5, claude-opus-4-{5,6,7,8}, glm-5.2, glm-5.3, deepseek-v4-{flash,flash-0731,pro}, kimi-k2-{6,7}, kimi-k3) and falls back to the senpi dynamic prompt when nothing matches. Renders the active preset name in the startup header. After 2026-04-30, presets are thin wrappers around `buildDynamicSystemPrompt()` carrying only model-specific tuning. +Builtin extension #3. On `before_agent_start` and `model_select`, picks a system prompt preset by **model family** (gpt-5.x through gpt-5.6, claude-fable-5, claude-opus-5, claude-opus-4-{5,6,7,8}, glm-5.2, glm-5.3, deepseek-v4-{flash,flash-0731,pro}, gemini, muse-spark, kimi-k2-{6,7}, kimi-k3) and falls back to the senpi dynamic prompt when nothing matches. Renders the active preset name in the startup header. After 2026-04-30, presets are thin wrappers around `buildDynamicSystemPrompt()` carrying only model-specific tuning. ## FILES @@ -25,6 +25,8 @@ prompt-preset/ ├── claude-opus-4-8.ts # Claude Opus 4.8 preset ├── glm-5-2.ts # GLM 5.2 preset ├── glm-5-3.ts # GLM 5.3 preset +├── gemini.ts # Gemini 3.x Flash preset — typed rule data (`GEMINI_RULES`) over a thin tuningSection wrapper +├── muse-spark.ts # Muse Spark preset — typed rule data (`MUSE_SPARK_RULES`) over a thin tuningSection wrapper ├── deepseek-v4.ts # Shared DeepSeek V4 rule data (`DEEPSEEK_V4_RULES`) + tuning builders (directive authority, todo discipline, missing-info, settled-reading, reasoning-aim) ├── deepseek-v4-flash.ts # DeepSeek V4 Flash preset (thin tuningSection over the shared core) ├── deepseek-v4-flash-0731.ts # DeepSeek V4 Flash 0731 snapshot preset — dated snapshot resolves before the generic flash alias diff --git a/packages/coding-agent/src/core/extensions/builtin/prompt-preset/changes.md b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/changes.md index a39d796cd7..3bda66581f 100644 --- a/packages/coding-agent/src/core/extensions/builtin/prompt-preset/changes.md +++ b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/changes.md @@ -42,6 +42,27 @@ ### Expected merge conflict zones on next upstream sync - LOW: `index.ts` handler bodies; keep the customPrompt yield and append reattachment when upstream reshapes handlers. +## Gemini + Muse Spark presets (2026-08-16) + +### What changed + +- `gemini.ts` / `muse-spark.ts`: new presets for the Gemini 3.x Flash and Muse Spark families — thin `tuningSection` wrappers over the shared dynamic core with `workstationDialect: "default"`, each carrying its vendor-guidance behaviors as typed rule data (`GEMINI_RULES` / `MUSE_SPARK_RULES`, deepseek-v4.ts precedent) and ending the tuning block with a machine-consumed `model-family: ` token line. Gemini rules: direct-instructions, lean-output, long-context-anchoring, behavior-requirements-binding, action-budget. Muse Spark rules: exposed-tools-only, no-hidden-control, one-goal-per-turn, evidence-before-success, observe-first, observation-summary, chain-checkpoints. +- `presets.ts`: `hasGeminiSignal` / `hasMuseSparkSignal` matchers on normalized id OR display name with `[/@:._-]` boundaries, verified against the installed pi-ai provider catalogs (2026-08-16). Gemini matches the exact flash ids `google/gemini-3.6-flash`, `google/gemini-3.1-flash-lite`, `google/gemini-3.5-flash`, `google/gemini-3.5-flash-lite`, `google/gemini-3.7-flash` plus their unprefixed (google, google-vertex, github-copilot, opencode), `:batch`-tagged, and `-preview` shapes; `-image` variants (Nano Banana image models) are excluded as a different modality, and truncated ids (`google/gemini-3.6`) do not match. Muse Spark matches `meta/muse-spark-1.1`, `meta/muse-spark-1.2`, `meta/muse-spark-1.2-contributor`; truncated `meta/muse-spark` and bare `spark` substrings do not. +- `settings.ts`: `"gemini"` and `"muse-spark"` join `PromptPresetName` and `VALID_PRESETS`. +- `docs/settings.md`, `AGENTS.md`, `builtin/AGENTS.md`: preset lists updated. +- `test/suite/prompt-presets-gemini-muse.test.ts` (new): routing decisions and `model-family` tokens only — exact-id tables for both families, truncated-id and loose-substring negatives, image-variant exclusion, a deepseek-v4-pro negative control, settings force + `parsePromptPreset` wiring, a catalog sweep asserting every built-in Gemini/Muse catalog model resolves, and per-prompt token stamping with cross-token non-leakage. + +### Why + +- Gemini 3.x Flash and Muse Spark models ship in the provider catalogs without a preset, so they fell back to the untuned dynamic prompt. Google's Gemini 3 guidance reads as calibration for a native reasoner (direct instructions, lean output, instruction-at-the-end anchoring, binding behavior requirements, binding action budgets); Meta's Muse Spark guidance centers on capability honesty and evidence-anchored tool loops. Both fit the thin-wrapper architecture with no shared-core changes. + +### Why extension system couldn't handle this differently + +- Content-only addition inside this builtin; follows the thin-wrapper preset architecture (`tuningSection` only). + +### Expected merge conflict zones on next upstream sync + +- LOW: `gemini.ts`, `muse-spark.ts`, and the test file are new files; `presets.ts`/`settings.ts` touch shared lists — trivial adjacent-line conflicts if upstream adds presets. ## GLM 5.3 preset (2026-08-16) diff --git a/packages/coding-agent/src/core/extensions/builtin/prompt-preset/deepseek-v4.ts b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/deepseek-v4.ts index de12ab10ca..ae21e854d8 100644 --- a/packages/coding-agent/src/core/extensions/builtin/prompt-preset/deepseek-v4.ts +++ b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/deepseek-v4.ts @@ -22,9 +22,17 @@ export type DeepseekV4RuleId = | "todo-discipline" | "missing-info" | "settled-reading" - | "reasoning-aim"; + | "reasoning-aim" + | "delegate-separable-work" + | "load-matching-skills"; -export type DeepseekV4Concern = "harness-contract" | "todo" | "grounding" | "deliberation"; +export type DeepseekV4Concern = + | "harness-contract" + | "todo" + | "grounding" + | "deliberation" + | "subagent-delegation" + | "skill-utilization"; export type DeepseekV4PresetName = "deepseek-v4-flash" | "deepseek-v4-flash-0731" | "deepseek-v4-pro"; @@ -74,6 +82,20 @@ export const DEEPSEEK_V4_RULES: readonly DeepseekV4Rule[] = [ directive: "Aim extended reasoning at the problem - the code, the design, the failure - and end it in an action. When reasoning stalls on a missing fact, stop deliberating and fetch the fact; a cheap read beats a long internal debate. Deliver a conclusion and a recommendation, not a survey of options.", }, + { + id: "delegate-separable-work", + concern: "subagent-delegation", + presets: ALL_PRESETS, + directive: + "At task intake, partition the work into the local critical path and separable workstreams. A bounded workstream that does not depend on the parent's in-progress edits is a `task` call: spawn it before local implementation, and submit independent workstreams in one batch. Keep only trivial lookups and tightly coupled edits local. Delegation is complete only after every child result has been read and its evidence integrated into the parent answer.", + }, + { + id: "load-matching-skills", + concern: "skill-utilization", + presets: ALL_PRESETS, + directive: + "Before the first non-discovery action, compare the task with every visible skill description. For each loose match, read that skill's listed `SKILL.md` and follow it; when matching work is delegated, include the skill name in `load_skills`. Proceed without a skill only after this scan finds no match - recognizing a match without loading it does not complete the gate.", + }, ]; export function buildDeepseekV4FlashIntro(modelLabel: string): string { diff --git a/packages/coding-agent/src/core/extensions/builtin/prompt-preset/gemini.ts b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/gemini.ts new file mode 100644 index 0000000000..9b8c383224 --- /dev/null +++ b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/gemini.ts @@ -0,0 +1,80 @@ +// Gemini 3.x Flash family tuning. +// +// Gemini 3 reasons natively, so the vendor prompting guidance (Google's +// Gemini 3 API docs, 2026-08) reads as calibration rather than scaffolding: +// direct instructions over chain-of-thought prompt engineering, lean output +// by default, instruction-at-the-end for long-context tasks, behavior +// requirements treated as binding system-instruction-style rules, and stated +// action budgets honored when tool calls would otherwise over-trigger. The +// preset stays a thin tuningSection wrapper over the shared dynamic core and +// carries those behaviors as typed rule data (gpt-5.6.ts / deepseek-v4.ts +// precedent) so tests assert parsed rules and routing tokens instead of +// pinned sentences. + +import { type BuildDynamicSystemPromptOptions, buildDynamicSystemPrompt } from "../../../dynamic-prompt/build.ts"; + +export type GeminiRuleId = + | "direct-instructions" + | "lean-output" + | "long-context-anchoring" + | "behavior-requirements-binding" + | "action-budget" + | "load-matching-skills"; + +export type GeminiConcern = "style" | "grounding" | "harness-contract" | "tool-orchestration" | "skill-utilization"; + +export interface GeminiRule { + readonly id: GeminiRuleId; + readonly concern: GeminiConcern; + readonly directive: string; +} + +export const GEMINI_RULES: readonly GeminiRule[] = [ + { + id: "direct-instructions", + concern: "style", + directive: + "Be concise and direct: the instructions in this prompt are plain directives to execute as written, not material to elaborate on. Verbose chain-of-thought prompt engineering - restating the task, narrating a plan before acting, explaining instructions back - adds no signal here; do the reasoning internally and let the answer or the tool call carry the result.", + }, + { + id: "lean-output", + concern: "style", + directive: + "Default output is lean: short answers, minimal preamble, no summaries of what you are about to do. Steer toward verbosity - longer explanations, walkthroughs, extra detail - only when the user explicitly asks for it.", + }, + { + id: "long-context-anchoring", + concern: "grounding", + directive: + 'When long data precedes the task - pasted files, logs, or documents - treat the specific instruction at the end as the operative request and anchor the answer on the provided data, grounding claims in it explicitly ("Based on the information above...") instead of answering from prior knowledge.', + }, + { + id: "behavior-requirements-binding", + concern: "harness-contract", + directive: + 'Behavior requirements in this prompt and in injected directives are binding system-instruction-style rules, applied literally and at full weight every turn - not style suggestions to weigh against the task. When a requirement states a scope ("every", "all", "never"), that scope is exact.', + }, + { + id: "action-budget", + concern: "tool-orchestration", + directive: + "When a stated action budget or step limit applies, treat it as binding: plan tool calls to fit inside it instead of over-triggering, and when one more call would break the budget, consolidate the remaining work into fewer, denser calls or stop and report.", + }, + { + id: "load-matching-skills", + concern: "skill-utilization", + directive: + "Before the first non-discovery action, compare the task with every visible skill description. For each loose match, read that skill's listed `SKILL.md` and follow it; when matching work is delegated, include the skill name in `load_skills`. Proceed without a skill only after this scan finds no match - recognizing a match without loading it does not complete the gate.", + }, +]; + +const GEMINI_INTRO = + "You are running on Gemini 3.x, a reasoning model. Reasoning is already on, so instructions are read as direct, literal input - the rules below calibrate style, grounding, and tool use rather than scaffolding thought."; + +function buildGeminiTuning(): string { + return [GEMINI_INTRO, ...GEMINI_RULES.map((rule) => rule.directive), "model-family: gemini"].join("\n\n"); +} + +export function buildGeminiPrompt(options: BuildDynamicSystemPromptOptions): string { + return buildDynamicSystemPrompt({ ...options, tuningSection: buildGeminiTuning(), workstationDialect: "default" }); +} diff --git a/packages/coding-agent/src/core/extensions/builtin/prompt-preset/glm-5-2.ts b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/glm-5-2.ts index 295c9121f7..d48ec2563a 100644 --- a/packages/coding-agent/src/core/extensions/builtin/prompt-preset/glm-5-2.ts +++ b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/glm-5-2.ts @@ -1,8 +1,26 @@ import { type BuildDynamicSystemPromptOptions, buildDynamicSystemPrompt } from "../../../dynamic-prompt/build.ts"; +export type Glm52RuleId = "load-matching-skills"; +export type Glm52Concern = "skill-utilization"; +export interface Glm52Rule { + readonly id: Glm52RuleId; + readonly concern: Glm52Concern; + readonly directive: string; +} +export const GLM_52_RULES: readonly Glm52Rule[] = [ + { + id: "load-matching-skills", + concern: "skill-utilization", + directive: + "Before the first non-discovery action, compare the task with every visible skill description. For each loose match, read that skill's listed `SKILL.md` and follow it; when matching work is delegated, include the skill name in `load_skills`. Proceed without a skill only after this scan finds no match - recognizing a match without loading it does not complete the gate.", + }, +]; + function buildGlm52Tuning(): string { return `You are running on GLM 5.2: Opus 4.6-class agent behavior tuned toward Fable 5 decisiveness and GPT 5.5 outcome-first coding. Apply literal scopes literally - "every", "all", and "for each" mean the full set. Prefer sufficient context over exhaustive context, pick minor decisions and note them, and use matching tools or skills immediately instead of under-reaching. +${GLM_52_RULES[0].directive} + Calibrate deliberation. Use extended reasoning only for genuine multi-step uncertainty; routine classification, file edits, and lookups should be decided directly. A cheap tool call beats long internal debate: act, inspect evidence, and verify. Code toward the destination: define the outcome, constraints, and stopping condition, then work without mechanical step-by-step recitation. In ultrawork mode, maintain absolute certainty discipline: preserve the goal, prove completion with evidence, and do not deliver partial work. diff --git a/packages/coding-agent/src/core/extensions/builtin/prompt-preset/glm-5-3.ts b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/glm-5-3.ts index b0338de165..ce7fcb08b7 100644 --- a/packages/coding-agent/src/core/extensions/builtin/prompt-preset/glm-5-3.ts +++ b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/glm-5-3.ts @@ -1,8 +1,26 @@ import { type BuildDynamicSystemPromptOptions, buildDynamicSystemPrompt } from "../../../dynamic-prompt/build.ts"; +export type Glm53RuleId = "load-matching-skills"; +export type Glm53Concern = "skill-utilization"; +export interface Glm53Rule { + readonly id: Glm53RuleId; + readonly concern: Glm53Concern; + readonly directive: string; +} +export const GLM_53_RULES: readonly Glm53Rule[] = [ + { + id: "load-matching-skills", + concern: "skill-utilization", + directive: + "Before the first non-discovery action, compare the task with every visible skill description. For each loose match, read that skill's listed `SKILL.md` and follow it; when matching work is delegated, include the skill name in `load_skills`. Proceed without a skill only after this scan finds no match - recognizing a match without loading it does not complete the gate.", + }, +]; + function buildGlm53Tuning(): string { return `You are running on GLM 5.3: Opus 4.6-class agent behavior tuned toward Fable 5 decisiveness and GPT 5.5 outcome-first coding. Apply literal scopes literally - "every", "all", and "for each" mean the full set. Prefer sufficient context over exhaustive context, pick minor decisions and note them, and use matching tools or skills immediately instead of under-reaching. +${GLM_53_RULES[0].directive} + Calibrate deliberation. Use extended reasoning only for genuine multi-step uncertainty; routine classification, file edits, and lookups should be decided directly. A cheap tool call beats long internal debate: act, inspect evidence, and verify. Code toward the destination: define the outcome, constraints, and stopping condition, then work without mechanical step-by-step recitation. In ultrawork mode, maintain absolute certainty discipline: preserve the goal, prove completion with evidence, and do not deliver partial work. diff --git a/packages/coding-agent/src/core/extensions/builtin/prompt-preset/gpt-5.5.ts b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/gpt-5.5.ts index 6620d1db2b..120dbc8c28 100644 --- a/packages/coding-agent/src/core/extensions/builtin/prompt-preset/gpt-5.5.ts +++ b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/gpt-5.5.ts @@ -68,6 +68,8 @@ ${context.toolSection} - Never present unread code or unrun commands as verified fact. - Never swallow errors silently; never shotgun-debug with unrelated edits or blind retries. +Make the visible-skill scan a terminal gate before substantive work: compare the task with every listed description, read each loosely matching \`SKILL.md\`, and apply it. If matching work is delegated, pass its skill name in \`load_skills\`. The gate is complete only when all matches are loaded or the scan finds none. + ## Style Plain, concrete prose; bullets only for genuinely list-shaped content. Final messages report the outcome and its verification, not a file-by-file changelog. Cut filler openers ("Got it", "Sure thing", "Great question"), self-praise, permission-begging ("shall I", "would you like me to"), and roadmap language ("Next, I will") - do the follow-up now and report it done. diff --git a/packages/coding-agent/src/core/extensions/builtin/prompt-preset/grok-4.5.ts b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/grok-4.5.ts index f699233b2f..e869941a2a 100644 --- a/packages/coding-agent/src/core/extensions/builtin/prompt-preset/grok-4.5.ts +++ b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/grok-4.5.ts @@ -46,6 +46,8 @@ ${buildTestDisciplineSection()} ${context.toolSection} +Make the visible-skill scan a terminal gate before substantive work: compare the task with every listed description, read each loosely matching \`SKILL.md\`, and apply it. If matching work is delegated, pass its skill name in \`load_skills\` via the spawned worker's delegation prompt. The gate is complete only when all matches are loaded or the scan finds none. + ## Hard Limits - Never commit unless the user asked; never use destructive git (\`reset --hard\`, \`checkout --\`, force-push) or amend without approval. - Never suppress type errors, lint warnings, or test failures; never delete, skip, or weaken a failing test to go green. diff --git a/packages/coding-agent/src/core/extensions/builtin/prompt-preset/grok-4.6.ts b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/grok-4.6.ts index 1e992ef8e5..66a0f2ec6b 100644 --- a/packages/coding-agent/src/core/extensions/builtin/prompt-preset/grok-4.6.ts +++ b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/grok-4.6.ts @@ -57,6 +57,8 @@ Decide one path and act; reopen a settled choice only when new evidence contradi When the same logic or markup starts appearing in a second place, break it into a shared piece instead of repeating it - repeated near-identical blocks across components are a defect. +Make the visible-skill scan a terminal gate before substantive work: compare the task with every listed description, read each loosely matching \`SKILL.md\`, and apply it. If matching work is delegated, pass its skill name in \`load_skills\`. The gate is complete only when all matches are loaded or the scan finds none. + ## Verification Tier the scope, never the rigor. diff --git a/packages/coding-agent/src/core/extensions/builtin/prompt-preset/kimi-k3.ts b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/kimi-k3.ts index 6a20a634e0..b4acd24535 100644 --- a/packages/coding-agent/src/core/extensions/builtin/prompt-preset/kimi-k3.ts +++ b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/kimi-k3.ts @@ -84,6 +84,10 @@ Explicitly scoped requests get exactly that scope; open-ended ones take the smal Decide one path and act; reopen a settled choice only when new evidence contradicts it. Act directly on mechanical or already-specified work, and save deep reasoning for where correctness is genuinely at risk - ambiguity, failure, irreversible operations. +Settle the delegation decision once at task intake: if a bounded research, reconnaissance, review, or independent-verification workstream can run without the parent's in-progress edits, spawn it with \`task\` before the local critical path, batching independent workstreams. Keep trivial or tightly coupled work local. Stop delegation once child results are consumed and integrated; do not reopen the decision or duplicate the child's work. + +Make the visible-skill scan a terminal gate before substantive work: compare the task with every listed description, read each loosely matching \`SKILL.md\`, and apply it. If matching work is delegated, pass its skill name in \`load_skills\`. The gate is complete only when all matches are loaded or the scan finds none. + Fire independent tool calls - reads, searches, listings, diagnostics - in one parallel wave; sequence only when a call needs a value another produced, and never fill missing parameters with placeholders. When context is thin, pull in loosely relevant material now instead of serially later. Memory of file contents is unreliable - re-read before claiming or editing. Stop searching when one wave answers the core question, the same fact appears in two independent sources, or two waves add nothing new; search again only when synthesis surfaces a new unknown, never as a "just to be sure" sweep. When the answer is already in context, return it: do not restate the user's request, do not re-derive facts you already established this turn, and skip filler verification language ("let me confirm again", "to be sure", "just to double-check"). diff --git a/packages/coding-agent/src/core/extensions/builtin/prompt-preset/muse-spark.ts b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/muse-spark.ts new file mode 100644 index 0000000000..5d0fe47381 --- /dev/null +++ b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/muse-spark.ts @@ -0,0 +1,121 @@ +// Muse Spark family tuning. +// +// Muse Spark is a coding-focused reasoning model whose vendor guidance (Meta's +// Muse Spark prompting notes, 2026-08) centers on capability honesty and +// evidence-anchored loops: use only the tools the client exposes, assume no +// hidden computer control, one goal per turn, claim success only with +// observed evidence, never invent file contents or UI state, summarize each +// observation before the next action, and checkpoint long tool chains. The +// preset stays a thin tuningSection wrapper over the shared dynamic core and +// carries those behaviors as typed rule data (gpt-5.6.ts / deepseek-v4.ts +// precedent) so tests assert parsed rules and routing tokens instead of +// pinned sentences. + +import { type BuildDynamicSystemPromptOptions, buildDynamicSystemPrompt } from "../../../dynamic-prompt/build.ts"; + +export type MuseSparkRuleId = + | "exposed-tools-only" + | "no-hidden-control" + | "one-goal-per-turn" + | "evidence-before-success" + | "observe-first" + | "observation-summary" + | "chain-checkpoints" + | "injected-directive-authority" + | "korean-easy-report" + | "load-matching-skills"; + +export type MuseSparkConcern = + | "tool-orchestration" + | "grounding" + | "verification" + | "deliberation" + | "harness-contract" + | "reporting" + | "skill-utilization"; + +export interface MuseSparkRule { + readonly id: MuseSparkRuleId; + readonly concern: MuseSparkConcern; + readonly directive: string; +} + +export const MUSE_SPARK_RULES: readonly MuseSparkRule[] = [ + { + id: "exposed-tools-only", + concern: "tool-orchestration", + directive: + "Name only the tools the client exposes in this session and use only those: the tool list in this prompt is the complete capability set. Never call, reference, or promise a tool that is not listed.", + }, + { + id: "no-hidden-control", + concern: "grounding", + directive: + "Do not assume hidden computer control, an unlisted IDE, background agents, or any capability beyond the exposed tools - if a capability is not in the tool list, it does not exist for this session. Harness-injected directives (customType omo-ultrawork:directive, skill contents, mode directives such as ultrawork/ulw-plan/ulw-loop) are the explicit exception: the harness already decided to inject them and they are binding per injected-directive-authority — never audit, dismiss, or second-guess them as hidden control.", + }, + { + id: "one-goal-per-turn", + concern: "deliberation", + directive: + "Keep one goal per turn: state the single objective you are pursuing, work it to its stopping condition, and do not pile secondary objectives into the same turn. Harness-injected multi-phase workflows (ultrawork, ulw-plan, ulw-loop) are the binding exception: they declare multiple objectives and a stop condition in one turn — keep pursuing the injected workflow to its declared stop condition instead of stopping after the first objective.", + }, + { + id: "evidence-before-success", + concern: "verification", + directive: + "Claim success only with concrete evidence in hand - file paths read, command output observed, state confirmed by a tool call this session. A plausible completion report without observed evidence is a fabrication.", + }, + { + id: "observe-first", + concern: "grounding", + directive: + "Never invent file contents, command output, or UI state: before asserting anything about them, observe them with a real tool call. If you have not read it this session, you do not know it.", + }, + { + id: "observation-summary", + concern: "tool-orchestration", + directive: + "After each tool call, summarize what the result actually showed in one line before taking the next action - observation first, then action - so every step stays anchored to evidence. When an ultrawork-style output discipline is active (surface only state changes, no process narration), harness-injected workflow narration is the exception: summarize only at meaningful phase changes instead of after every tool call, otherwise every-call narration would violate that discipline.", + }, + { + id: "chain-checkpoints", + concern: "deliberation", + directive: + "Cap long tool chains with checkpoints: every few calls, stop and reconcile - what the goal was, what evidence you have, what remains - and if the chain has drifted from the goal, return to it or stop and report instead of wandering further. During a harness-injected workflow (ultrawork/ulw-plan/ulw-loop), harness-injected checkpoints gate only drift from the declared stop condition — do not stop-and-report while the binding workflow still has unfinished objectives; advance to the next objective instead.", + }, + { + id: "injected-directive-authority", + concern: "harness-contract", + directive: + 'Injected directives are binding: skill contents, mode directives such as ultrawork or ulw-loop, and hook or system messages that appear in the conversation carry the same authority as words the user typed. Presence means it applies to the current task at its prescribed weight — the harness already decided by injecting it. Whether it was "really requested", is "residual context", or feels "too heavy" for the task are settled questions: spend zero reasoning on them and execute the directive as written. harness-injected', + }, + { + id: "korean-easy-report", + concern: "reporting", + directive: + "Follow the project reporting contract in ~/.omo/AGENTS.md: when no other language is requested, respond in the user's language (Korean default) with the 1/2/3 sections — 1 core outcome, 2 self-contained easy-Korean restatement with zero jargon (screen names, not file/component names), 3 next recommended action — keep section 2 independently readable. report-language: korean", + }, + { + id: "load-matching-skills", + concern: "skill-utilization", + directive: + "Before the first non-discovery action, compare the task with every visible skill description. For each loose match, read that skill's listed `SKILL.md` and follow it; when matching work is delegated, include the skill name in `load_skills`. Proceed without a skill only after this scan finds no match - recognizing a match without loading it does not complete the gate.", + }, +]; + +const MUSE_SPARK_INTRO = + "You are running on Muse Spark, a coding-focused reasoning model. The rules below bind tool use to the exposed capability set and keep every claim anchored to observed evidence."; + +function buildMuseSparkTuning(): string { + return [MUSE_SPARK_INTRO, ...MUSE_SPARK_RULES.map((rule) => rule.directive), "model-family: muse-spark"].join( + "\n\n", + ); +} + +export function buildMuseSparkPrompt(options: BuildDynamicSystemPromptOptions): string { + return buildDynamicSystemPrompt({ + ...options, + tuningSection: buildMuseSparkTuning(), + workstationDialect: "default", + }); +} diff --git a/packages/coding-agent/src/core/extensions/builtin/prompt-preset/presets.ts b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/presets.ts index 9bd56c3e33..722719fc11 100644 --- a/packages/coding-agent/src/core/extensions/builtin/prompt-preset/presets.ts +++ b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/presets.ts @@ -9,6 +9,7 @@ import { buildClaudeOpus5Prompt } from "./claude-opus-5.ts"; import { buildDeepseekV4FlashPrompt } from "./deepseek-v4-flash.ts"; import { buildDeepseekV4Flash0731Prompt } from "./deepseek-v4-flash-0731.ts"; import { buildDeepseekV4ProPrompt } from "./deepseek-v4-pro.ts"; +import { buildGeminiPrompt } from "./gemini.ts"; import { buildGlm52Prompt } from "./glm-5-2.ts"; import { buildGlm53Prompt } from "./glm-5-3.ts"; import { buildGpt52Prompt } from "./gpt-5.2.ts"; @@ -22,6 +23,7 @@ import { buildGrok46Prompt } from "./grok-4.6.ts"; import { buildKimiK26Prompt } from "./kimi-k2-6.ts"; import { buildKimiK27Prompt } from "./kimi-k2-7.ts"; import { buildKimiK3Prompt } from "./kimi-k3.ts"; +import { buildMuseSparkPrompt } from "./muse-spark.ts"; import { type PromptPresetName, type PromptPresetSettings, parsePromptPreset } from "./settings.ts"; export type { PromptPresetSettings } from "./settings.ts"; @@ -154,6 +156,35 @@ function isGrok46Model(model: ModelWithPromptPresetMetadata): boolean { return hasGrok46Signal(model.id) || (model.name !== undefined && hasGrok46Signal(model.name)); } +// Gemini 3.x Flash id shapes verified against senpi's generated provider +// catalogs (2026-08-16): google/gemini-3.6-flash, google/gemini-3.1-flash-lite, +// google/gemini-3.5-flash, google/gemini-3.5-flash-lite, google/gemini-3.7-flash +// (openrouter/vercel-ai-gateway), plus the unprefixed google / google-vertex / +// github-copilot / opencode shapes, :batch trailing tags, and -preview suffixes. +// Truncated ids (google/gemini-3.6, bare "gemini") stay out, and -image variants +// (Nano Banana image models) are a different modality and never route here. +function hasGeminiSignal(value: string): boolean { + return /(?:^|[/@:._-])gemini[._-]3[._-](?:1[._-]flash[._-]lite|5[._-]flash(?:[._-]lite)?|6[._-]flash|7[._-]flash)(?:$|[/@:._-])(?!image(?:$|[/@:._-]))/.test( + normalizeModelId(value), + ); +} + +function isGeminiModel(model: ModelWithPromptPresetMetadata): boolean { + return hasGeminiSignal(model.id) || (model.name !== undefined && hasGeminiSignal(model.name)); +} + +// Muse Spark id shapes verified against senpi's generated provider catalogs +// (2026-08-16): meta/muse-spark-1.1, meta/muse-spark-1.2, and +// meta/muse-spark-1.2-contributor (openrouter, vercel-ai-gateway). Truncated +// ids (meta/muse-spark) and bare "spark" substrings stay out. +function hasMuseSparkSignal(value: string): boolean { + return /(?:^|[/@:._-])muse[._-]spark[._-]1[._-][12](?:$|[/@:._-])/.test(normalizeModelId(value)); +} + +function isMuseSparkModel(model: ModelWithPromptPresetMetadata): boolean { + return hasMuseSparkSignal(model.id) || (model.name !== undefined && hasMuseSparkSignal(model.name)); +} + function isClaudeFable5Model(modelId: string): boolean { return normalizeModelId(modelId).includes("fable-5"); } @@ -239,6 +270,12 @@ export function resolvePresetName( if (isGrok45Model(model)) { return "grok-4.5"; } + if (isGeminiModel(model)) { + return "gemini"; + } + if (isMuseSparkModel(model)) { + return "muse-spark"; + } return undefined; } @@ -270,6 +307,10 @@ function buildPreset(name: ResolvedPresetName, options: BuildDynamicSystemPrompt return { name, prompt: buildGrok46Prompt(options) }; case "grok-4.5": return { name, prompt: buildGrok45Prompt(options) }; + case "gemini": + return { name, prompt: buildGeminiPrompt(options) }; + case "muse-spark": + return { name, prompt: buildMuseSparkPrompt(options) }; case "kimi-k3": return { name, prompt: buildKimiK3Prompt(options) }; case "kimi-k2-7": diff --git a/packages/coding-agent/src/core/extensions/builtin/prompt-preset/settings.ts b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/settings.ts index 51085c80c1..1498f7f087 100644 --- a/packages/coding-agent/src/core/extensions/builtin/prompt-preset/settings.ts +++ b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/settings.ts @@ -11,6 +11,7 @@ export type PromptPresetName = | "deepseek-v4-flash" | "deepseek-v4-flash-0731" | "deepseek-v4-pro" + | "gemini" | "glm-5.2" | "glm-5.3" | "grok-4.5" @@ -18,6 +19,7 @@ export type PromptPresetName = | "kimi-k3" | "kimi-k2-7" | "kimi-k2-6" + | "muse-spark" | "gpt-5" | "gpt-5.2" | "gpt-5.3-codex" @@ -42,6 +44,7 @@ const VALID_PRESETS: ReadonlySet = new Set([ "deepseek-v4-flash", "deepseek-v4-flash-0731", "deepseek-v4-pro", + "gemini", "glm-5.2", "glm-5.3", "grok-4.5", @@ -49,6 +52,7 @@ const VALID_PRESETS: ReadonlySet = new Set([ "kimi-k3", "kimi-k2-7", "kimi-k2-6", + "muse-spark", "gpt-5", "gpt-5.2", "gpt-5.3-codex", diff --git a/packages/coding-agent/test/suite/prompt-presets-deepseek-v4.test.ts b/packages/coding-agent/test/suite/prompt-presets-deepseek-v4.test.ts index 2926d071ce..691ba52255 100644 --- a/packages/coding-agent/test/suite/prompt-presets-deepseek-v4.test.ts +++ b/packages/coding-agent/test/suite/prompt-presets-deepseek-v4.test.ts @@ -288,10 +288,11 @@ describe("DeepSeek V4 rule data", () => { resolvePreset(createModel("glm-5.2", "zai"), settings)?.prompt, ]; - // then + // then — load-matching-skills is intentionally shared verbatim with glm-5.2/gemini/muse-spark (all-models-skill-gate), so exclude it from the leak check + const nonSharedRules = DEEPSEEK_V4_RULES.filter((rule) => rule.id !== "load-matching-skills"); for (const prompt of otherPrompts) { expect(prompt).toBeDefined(); - for (const rule of DEEPSEEK_V4_RULES) { + for (const rule of nonSharedRules) { expect(prompt).not.toContain(rule.directive); } } diff --git a/packages/coding-agent/test/suite/prompt-presets-gemini-muse.test.ts b/packages/coding-agent/test/suite/prompt-presets-gemini-muse.test.ts new file mode 100644 index 0000000000..ef1129d296 --- /dev/null +++ b/packages/coding-agent/test/suite/prompt-presets-gemini-muse.test.ts @@ -0,0 +1,337 @@ +import type { Api, Model } from "@earendil-works/pi-ai"; +import { getModels, getProviders } from "@earendil-works/pi-ai/compat"; +import { describe, expect, it } from "vitest"; +import type { BuildDynamicSystemPromptOptions } from "../../src/core/dynamic-prompt/build.ts"; +import { buildStyleSection } from "../../src/core/dynamic-prompt/style.ts"; +import { buildGeminiPrompt } from "../../src/core/extensions/builtin/prompt-preset/gemini.ts"; +import { buildMuseSparkPrompt, MUSE_SPARK_RULES } from "../../src/core/extensions/builtin/prompt-preset/muse-spark.ts"; +import { + type PromptPresetSettings, + resolvePreset, + resolvePresetName, +} from "../../src/core/extensions/builtin/prompt-preset/presets.ts"; +import { parsePromptPreset } from "../../src/core/extensions/builtin/prompt-preset/settings.ts"; + +function createModel(id: string, provider: string, api: Api = "openai-completions"): Model { + return { + id, + name: id, + api, + provider, + baseUrl: "https://example.com/v1", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 128_000, + maxTokens: 16_384, + }; +} + +const OPTIONS: BuildDynamicSystemPromptOptions = { + cwd: "/test/project", + selectedTools: ["read", "bash", "edit", "write"], + toolSnippets: { + read: "Read file contents", + bash: "Execute shell commands", + edit: "Apply surgical edits", + write: "Create or overwrite files", + }, + promptGuidelines: [], + contextFiles: [], + skills: [], +}; + +// Real-world id shapes verified against senpi's generated provider catalogs +// (2026-08-16): the exact registry ids, plus the unprefixed, :batch-tagged, +// -preview-suffixed, and display-name shapes those catalogs carry. +const GEMINI_MODEL_IDS = [ + "google/gemini-3.6-flash", // openrouter, vercel-ai-gateway + "google/gemini-3.1-flash-lite", // openrouter, vercel-ai-gateway + "google/gemini-3.5-flash", // openrouter, vercel-ai-gateway + "google/gemini-3.5-flash-lite", // openrouter, vercel-ai-gateway + "google/gemini-3.7-flash", // contract-listed registry id + "gemini-3.6-flash", // google, google-vertex, github-copilot, opencode + "gemini-3.5-flash", // google, google-vertex, github-copilot, opencode + "gemini-3.5-flash-lite", // google, google-vertex, opencode + "gemini-3.1-flash-lite", // google, google-vertex + "opengateway/google/gemini-3.5-flash", // extra path segment + "google/gemini-3.6-flash:batch", // openrouter batch tag + "google/gemini-3.5-flash-lite:batch", // openrouter batch tag + "google/gemini-3.1-flash-lite-preview", // preview suffix + "Gemini 3.6 Flash", // display-name matching +]; + +const MUSE_SPARK_MODEL_IDS = [ + "meta/muse-spark-1.1", // openrouter, vercel-ai-gateway + "meta/muse-spark-1.2", // openrouter, vercel-ai-gateway + "meta/muse-spark-1.2-contributor", // vercel-ai-gateway + "Muse Spark 1.2", // display-name matching +]; + +const NON_MATCHING_MODEL_IDS = [ + "google/gemini-3-flash", // no minor version - not a listed id + "google/gemini-3-flash-preview", + "google/gemini-2.5-flash", // previous generation + "google/gemini-3.1-pro-preview", // pro line, not flash + "google/gemini-3.1-flash-image", // image modality + "google/gemini-3.1-flash-lite-image", // image modality (Nano Banana 2 Lite) + "google/gemini-3-pro-image", + "meta/muse-spark-1.3", // not a listed id + "meta/muse-spark-2.0", + "my-gemini-router", // bare substring, no versioned id + "spark-1.2", // no muse signal + "mimo-v2-pro", +]; + +function hasGeminiCatalogSignal(searchable: string): boolean { + return /(?:^|[/@:._-])gemini[._-]3[._-](?:1[._-]flash[._-]lite|5[._-]flash(?:[._-]lite)?|6[._-]flash|7[._-]flash)(?:$|[/@:._-])(?!image(?:$|[/@:._-]))/.test( + searchable, + ); +} + +function hasMuseSparkCatalogSignal(searchable: string): boolean { + return /(?:^|[/@:._-])muse[._-]spark[._-]1[._-][12](?:$|[/@:._-])/.test(searchable); +} + +function expectedCatalogPreset(model: Model): "gemini" | "muse-spark" | undefined { + const searchable = `${model.id} ${model.name}`.toLowerCase().replace(/\s+/g, "-"); + if (hasGeminiCatalogSignal(searchable)) { + return "gemini"; + } + if (hasMuseSparkCatalogSignal(searchable)) { + return "muse-spark"; + } + return undefined; +} + +function getGeminiMuseCatalogModels(): Array<{ model: Model; expected: "gemini" | "muse-spark" }> { + return getProviders().flatMap((provider) => + (getModels(provider) as Model[]) + .map((model) => ({ model, expected: expectedCatalogPreset(model) })) + .filter( + (entry): entry is { model: Model; expected: "gemini" | "muse-spark" } => entry.expected !== undefined, + ), + ); +} + +describe("Gemini prompt preset routing", () => { + it.each(GEMINI_MODEL_IDS)("resolves %s to the gemini preset", (modelId) => { + // given + const settings: PromptPresetSettings = { promptPreset: "auto" }; + const model = createModel(modelId, "openrouter"); + + // when + const name = resolvePresetName(model, settings); + + // then + expect(name).toBe("gemini"); + }); + + it("matches by display name when the raw id carries no signal", () => { + // given + const settings: PromptPresetSettings = { promptPreset: "auto" }; + const model = { ...createModel("gateway-alias-12", "custom"), name: "Google: Gemini 3.5 Flash Lite" }; + + // when + const name = resolvePresetName(model, settings); + + // then + expect(name).toBe("gemini"); + }); +}); + +describe("Muse Spark prompt preset routing", () => { + it.each(MUSE_SPARK_MODEL_IDS)("resolves %s to the muse-spark preset", (modelId) => { + // given + const settings: PromptPresetSettings = { promptPreset: "auto" }; + const model = createModel(modelId, "openrouter"); + + // when + const name = resolvePresetName(model, settings); + + // then + expect(name).toBe("muse-spark"); + }); +}); + +describe("Gemini/Muse Spark routing boundaries", () => { + it.each(["google/gemini-3.6", "meta/muse-spark"])("leaves truncated id %s unresolved", (modelId) => { + // given + const settings: PromptPresetSettings = { promptPreset: "auto" }; + const model = createModel(modelId, "openrouter"); + + // when + const name = resolvePresetName(model, settings); + + // then + expect(name).toBeUndefined(); + }); + + it.each(NON_MATCHING_MODEL_IDS)("does not route %s to the gemini or muse-spark preset", (modelId) => { + // given + const settings: PromptPresetSettings = { promptPreset: "auto" }; + const model = createModel(modelId, "openrouter"); + + // when + const name = resolvePresetName(model, settings); + + // then + expect(name === undefined || (name !== "gemini" && name !== "muse-spark")).toBe(true); + }); + + it("keeps deepseek/deepseek-v4-pro on its existing preset", () => { + // given + const settings: PromptPresetSettings = { promptPreset: "auto" }; + const model = createModel("deepseek/deepseek-v4-pro", "openrouter"); + + // when + const name = resolvePresetName(model, settings); + + // then + expect(name).toBe("deepseek-v4-pro"); + }); + + it("returns the correct preset for every Gemini/Muse built-in catalog model", () => { + // given + const settings: PromptPresetSettings = { promptPreset: "auto" }; + const catalogEntries = getGeminiMuseCatalogModels(); + const catalogModelIds = catalogEntries.map(({ model }) => `${model.provider}/${model.id}`); + + // when + const misses = catalogEntries + .filter(({ model, expected }) => resolvePresetName(model, settings) !== expected) + .map(({ model, expected }) => `${model.provider}/${model.id} != ${expected}`); + + // then + expect(catalogModelIds).toEqual( + expect.arrayContaining([ + "google/gemini-3.6-flash", + "google/gemini-3.5-flash", + "openrouter/google/gemini-3.6-flash", + "openrouter/google/gemini-3.6-flash:batch", + "vercel-ai-gateway/google/gemini-3.5-flash-lite", + "github-copilot/gemini-3.6-flash", + "google-vertex/gemini-3.6-flash", + "opencode/gemini-3.5-flash", + "openrouter/meta/muse-spark-1.1", + "openrouter/meta/muse-spark-1.2", + "vercel-ai-gateway/meta/muse-spark-1.2-contributor", + ]), + ); + expect(misses).toEqual([]); + }); +}); + +describe("Gemini/Muse Spark preset settings and tokens", () => { + it.each(["gemini", "muse-spark"] as const)("accepts %s as a valid promptPreset setting", (presetName) => { + expect(parsePromptPreset(presetName)).toBe(presetName); + }); + + it.each(["gemini", "muse-spark"] as const)( + "allows settings.json to force %s regardless of model id", + (presetName) => { + // given + const settings = { promptPreset: presetName } as PromptPresetSettings; + const model = createModel("some-random-model", "custom"); + + // when + const preset = resolvePreset(model, settings); + + // then + expect(preset?.name).toBe(presetName); + }, + ); + + it("stamps the gemini prompt with its model-family token", () => { + // given / when + const prompt = buildGeminiPrompt(OPTIONS); + + // then + expect(prompt).toContain("model-family: gemini"); + expect(prompt).not.toContain("model-family: muse-spark"); + }); + + it("stamps the muse-spark prompt with its model-family token", () => { + // given / when + const prompt = buildMuseSparkPrompt(OPTIONS); + + // then + expect(prompt).toContain("model-family: muse-spark"); + expect(prompt).not.toContain("model-family: gemini"); + }); +}); + +describe("Muse Spark hardened rules (ultrawork + Korean report)", () => { + it("exposes 10 typed rules including the three new harness/reporting/skill rules", () => { + const ids = MUSE_SPARK_RULES.map((rule) => rule.id); + expect(MUSE_SPARK_RULES).toHaveLength(10); + expect(ids).toEqual( + expect.arrayContaining([ + "exposed-tools-only", + "no-hidden-control", + "one-goal-per-turn", + "evidence-before-success", + "observe-first", + "observation-summary", + "chain-checkpoints", + "injected-directive-authority", + "korean-easy-report", + ]), + ); + }); + + it("stamps the muse-spark rendered prompt with ultrawork authority + Korean sentinel tokens", () => { + const prompt = buildMuseSparkPrompt(OPTIONS); + // sentinel tokens — never pin prose sentences per verification.ts prompt-behavior-coverage + expect(prompt).toContain("model-family: muse-spark"); + expect(prompt).toContain("report-language: korean"); + expect(prompt).toContain("Injected directives are binding"); + expect(prompt).toContain("harness-injected"); + }); + + it("relaxes the four stall rules with harness-injected exceptions", () => { + const prompt = buildMuseSparkPrompt(OPTIONS); + // each relaxed directive must carry the harness exception token in its rendered form + expect(prompt).toContain("harness-injected"); + // the four pre-existing directives remain present as rule ids + const ids = new Set(MUSE_SPARK_RULES.map((r) => r.id)); + expect(ids.has("no-hidden-control")).toBe(true); + expect(ids.has("one-goal-per-turn")).toBe(true); + expect(ids.has("observation-summary")).toBe(true); + expect(ids.has("chain-checkpoints")).toBe(true); + }); +}); + +describe("Global style language default (2.b)", () => { + it("matches the user's language instead of defaulting to ASCII", () => { + const style = buildStyleSection(); + expect(style).toContain("Match the user's language"); + expect(style).not.toContain("Default to ASCII"); + }); +}); + +describe("Thin skill gate (gemini + muse-spark)", () => { + it("renders load-matching-skills exactly once for gemini", () => { + const prompt = buildGeminiPrompt(OPTIONS); + expect(prompt).toContain("Before the first non-discovery action"); + let c = 0, + i = prompt.indexOf("Before the first non-discovery action"); + while (i !== -1) { + c++; + i = prompt.indexOf("Before the first non-discovery action", i + 1); + } + expect(c).toBe(1); + }); + it("renders load-matching-skills exactly once for muse-spark (10 rules)", () => { + expect(MUSE_SPARK_RULES).toHaveLength(10); + const prompt = buildMuseSparkPrompt(OPTIONS); + expect(prompt).toContain("Before the first non-discovery action"); + let c = 0, + i = prompt.indexOf("Before the first non-discovery action"); + while (i !== -1) { + c++; + i = prompt.indexOf("Before the first non-discovery action", i + 1); + } + expect(c).toBe(1); + }); +}); diff --git a/packages/coding-agent/test/suite/prompt-presets-glm-5-2.test.ts b/packages/coding-agent/test/suite/prompt-presets-glm-5-2.test.ts index 3a8cb8b9d0..9cb6754224 100644 --- a/packages/coding-agent/test/suite/prompt-presets-glm-5-2.test.ts +++ b/packages/coding-agent/test/suite/prompt-presets-glm-5-2.test.ts @@ -1,6 +1,7 @@ import type { Api, Model } from "@earendil-works/pi-ai"; import { getModels, getProviders } from "@earendil-works/pi-ai/compat"; import { describe, expect, it } from "vitest"; +import { buildGlm52Prompt, GLM_52_RULES } from "../../src/core/extensions/builtin/prompt-preset/glm-5-2.ts"; import { type PromptPresetSettings, resolvePreset, @@ -102,3 +103,26 @@ describe("GLM 5.2 prompt preset", () => { expect(misses).toEqual([]); }); }); + +describe("GLM 5.2 skill gate", () => { + it("exposes load-matching-skills and renders it once", () => { + expect(GLM_52_RULES).toHaveLength(1); + expect(GLM_52_RULES[0].id).toBe("load-matching-skills"); + const prompt = buildGlm52Prompt({ + cwd: "/tmp", + selectedTools: ["read"], + toolSnippets: { read: "r" }, + promptGuidelines: [], + contextFiles: [], + skills: [], + }); + expect(prompt).toContain("Before the first non-discovery action"); + let c = 0, + i = prompt.indexOf("Before the first non-discovery action"); + while (i !== -1) { + c++; + i = prompt.indexOf("Before the first non-discovery action", i + 1); + } + expect(c).toBe(1); + }); +}); diff --git a/packages/coding-agent/test/suite/prompt-presets-glm-5-3.test.ts b/packages/coding-agent/test/suite/prompt-presets-glm-5-3.test.ts index 95aa15810f..bb16ce9df5 100644 --- a/packages/coding-agent/test/suite/prompt-presets-glm-5-3.test.ts +++ b/packages/coding-agent/test/suite/prompt-presets-glm-5-3.test.ts @@ -1,6 +1,7 @@ import type { Api, Model } from "@earendil-works/pi-ai"; import { getModels, getProviders } from "@earendil-works/pi-ai/compat"; import { describe, expect, it } from "vitest"; +import { buildGlm53Prompt, GLM_53_RULES } from "../../src/core/extensions/builtin/prompt-preset/glm-5-3.ts"; import { type PromptPresetSettings, resolvePreset, @@ -103,3 +104,26 @@ describe("GLM 5.3 prompt preset", () => { expect(misses).toEqual([]); }); }); + +describe("GLM 5.3 skill gate", () => { + it("exposes load-matching-skills and renders it once", () => { + expect(GLM_53_RULES).toHaveLength(1); + expect(GLM_53_RULES[0].id).toBe("load-matching-skills"); + const prompt = buildGlm53Prompt({ + cwd: "/tmp", + selectedTools: ["read"], + toolSnippets: { read: "r" }, + promptGuidelines: [], + contextFiles: [], + skills: [], + }); + expect(prompt).toContain("Before the first non-discovery action"); + let c = 0, + i = prompt.indexOf("Before the first non-discovery action"); + while (i !== -1) { + c++; + i = prompt.indexOf("Before the first non-discovery action", i + 1); + } + expect(c).toBe(1); + }); +}); diff --git a/packages/coding-agent/test/suite/prompt-presets-gpt-5-5.test.ts b/packages/coding-agent/test/suite/prompt-presets-gpt-5-5.test.ts new file mode 100644 index 0000000000..041db0f15f --- /dev/null +++ b/packages/coding-agent/test/suite/prompt-presets-gpt-5-5.test.ts @@ -0,0 +1,50 @@ +import type { Api, Model } from "@earendil-works/pi-ai"; +import { describe, expect, it } from "vitest"; +import { buildGpt55Prompt } from "../../src/core/extensions/builtin/prompt-preset/gpt-5.5.ts"; +import { type PromptPresetSettings, resolvePreset } from "../../src/core/extensions/builtin/prompt-preset/presets.ts"; + +function createModel(id: string, provider: string, api: Api = "openai-responses"): Model { + return { + id, + name: id, + api, + provider, + baseUrl: "https://example.com/v1", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 128_000, + maxTokens: 16_384, + }; +} + +describe("GPT 5.5 skill gate", () => { + it("renders visible-skill scan gate exactly once", () => { + const prompt = buildGpt55Prompt({ + cwd: "/tmp", + selectedTools: ["read"], + toolSnippets: { read: "r" }, + promptGuidelines: [], + contextFiles: [], + skills: [], + }); + expect(prompt).toContain("terminal gate before substantive"); + let c = 0, + i = prompt.indexOf("terminal gate before substantive"); + while (i !== -1) { + c++; + i = prompt.indexOf("terminal gate before substantive", i + 1); + } + expect(c).toBe(1); + }); +}); + +describe("GPT 5.5 prompt preset", () => { + it("routes gpt-5.5 to its preset", () => { + const settings: PromptPresetSettings = { promptPreset: "auto" }; + const model = createModel("gpt-5.5", "openai"); + const preset = resolvePreset(model, settings); + expect(preset?.name).toBe("gpt-5.5"); + expect(preset?.prompt).toContain("terminal gate before substantive"); + }); +}); diff --git a/packages/coding-agent/test/suite/prompt-presets-grok-4-5.test.ts b/packages/coding-agent/test/suite/prompt-presets-grok-4-5.test.ts index ece7b4976c..58a9e53377 100644 --- a/packages/coding-agent/test/suite/prompt-presets-grok-4-5.test.ts +++ b/packages/coding-agent/test/suite/prompt-presets-grok-4-5.test.ts @@ -1,6 +1,7 @@ import type { Api, Model } from "@earendil-works/pi-ai"; import { getModels, getProviders } from "@earendil-works/pi-ai/compat"; import { describe, expect, it } from "vitest"; +import { buildGrok45Prompt } from "../../src/core/extensions/builtin/prompt-preset/grok-4.5.ts"; import { type PromptPresetSettings, resolvePreset, @@ -117,3 +118,24 @@ describe("Grok 4.5 prompt preset", () => { expect(misses).toEqual([]); }); }); + +describe("Grok 4.5 skill gate", () => { + it("renders visible-skill scan gate exactly once", () => { + const prompt = buildGrok45Prompt({ + cwd: "/tmp", + selectedTools: ["read"], + toolSnippets: { read: "r" }, + promptGuidelines: [], + contextFiles: [], + skills: [], + }); + expect(prompt).toContain("terminal gate before substantive"); + let c = 0, + i = prompt.indexOf("terminal gate before substantive"); + while (i !== -1) { + c++; + i = prompt.indexOf("terminal gate before substantive", i + 1); + } + expect(c).toBe(1); + }); +}); diff --git a/packages/coding-agent/test/suite/prompt-presets-grok-4-6.test.ts b/packages/coding-agent/test/suite/prompt-presets-grok-4-6.test.ts index 37ea5af5ca..7194b4d675 100644 --- a/packages/coding-agent/test/suite/prompt-presets-grok-4-6.test.ts +++ b/packages/coding-agent/test/suite/prompt-presets-grok-4-6.test.ts @@ -1,6 +1,7 @@ import type { Api, Model } from "@earendil-works/pi-ai"; import { getModels, getProviders } from "@earendil-works/pi-ai/compat"; import { describe, expect, it } from "vitest"; +import { buildGrok46Prompt } from "../../src/core/extensions/builtin/prompt-preset/grok-4.6.ts"; import { type PromptPresetSettings, resolvePreset, @@ -134,3 +135,24 @@ describe("Grok 4.6 prompt preset", () => { expect(misses).toEqual([]); }); }); + +describe("Grok 4.6 skill gate", () => { + it("renders visible-skill scan gate exactly once", () => { + const prompt = buildGrok46Prompt({ + cwd: "/tmp", + selectedTools: ["read"], + toolSnippets: { read: "r" }, + promptGuidelines: [], + contextFiles: [], + skills: [], + }); + expect(prompt).toContain("terminal gate before substantive"); + let c = 0, + i = prompt.indexOf("terminal gate before substantive"); + while (i !== -1) { + c++; + i = prompt.indexOf("terminal gate before substantive", i + 1); + } + expect(c).toBe(1); + }); +});