From 4e4ac73982c769ac353dae5941427e65edb8c73d Mon Sep 17 00:00:00 2001 From: Seungwoo Jeong Date: Sun, 16 Aug 2026 19:07:25 +0900 Subject: [PATCH 1/3] feat(coding-agent): add gemini + muse-spark system-prompt presets --- packages/coding-agent/docs/settings.md | 2 +- .../src/core/extensions/builtin/AGENTS.md | 4 +- .../builtin/prompt-preset/AGENTS.md | 4 +- .../builtin/prompt-preset/changes.md | 21 ++ .../builtin/prompt-preset/gemini.ts | 73 +++++ .../builtin/prompt-preset/muse-spark.ts | 93 +++++++ .../builtin/prompt-preset/presets.ts | 41 +++ .../builtin/prompt-preset/settings.ts | 4 + .../suite/prompt-presets-gemini-muse.test.ts | 261 ++++++++++++++++++ 9 files changed, 499 insertions(+), 4 deletions(-) create mode 100644 packages/coding-agent/src/core/extensions/builtin/prompt-preset/gemini.ts create mode 100644 packages/coding-agent/src/core/extensions/builtin/prompt-preset/muse-spark.ts create mode 100644 packages/coding-agent/test/suite/prompt-presets-gemini-muse.test.ts diff --git a/packages/coding-agent/docs/settings.md b/packages/coding-agent/docs/settings.md index 6236e4614d..291647299e 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 | | `thinkingBudgets` | object | - | Custom token budgets per thinking level | diff --git a/packages/coding-agent/src/core/extensions/builtin/AGENTS.md b/packages/coding-agent/src/core/extensions/builtin/AGENTS.md index 0346c6ce17..5ddf5a2cb5 100644 --- a/packages/coding-agent/src/core/extensions/builtin/AGENTS.md +++ b/packages/coding-agent/src/core/extensions/builtin/AGENTS.md @@ -9,7 +9,7 @@ | 1 | `hooks` | `hooks/` | Settings-configured lifecycle command hooks (PreToolUse/PostToolUse-style) with trust hashing + live status | | 2 | `permission-system` | `permission-system/` | Full opencode-style permission port: rules, JSONL storage, prompts | | 3 | `gpt-apply-patch` | `gpt-apply-patch/` | Codex-style `apply_patch` tool with rich render + freeform grammar | -| 4 | `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}, kimi-k2-{6,7}, kimi-k3) | +| 4 | `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, kimi-k2-{6,7}, kimi-k3) | | 5 | `todowrite` | `todotools/` | Op-based oh-my-pi todo port + `/todo` command; fully diverged from `../pi-extensions/pi-todotools` | | 6 | `redraws` | `redraws.ts` | Force-redraw event hooks for stable streaming visuals | | 7 | `anthropic-web-search` | `anthropic-web-search/` | Anthropic-native web search tool | @@ -58,7 +58,7 @@ Shared non-factory modules in this directory: - **Subdirectory extensions** ship multi-file: `index.ts` + supporting `.ts` (`registry.ts`, `types.ts`, `parsers.ts`, etc.). - **Single-file extensions** are kept flat (`diff.ts`, `files.ts`, `redraws.ts`, `service-tier.ts`, `tps.ts`, `prompt-url-widget.ts`). -- **`prompt-preset/`** has per-model files (`gpt-5.5.ts`, `claude-opus-4-7.ts`, …) and a shared `file-operations.ts` tuning block. New model = new preset file + entry in `presets.ts`. Models covered: 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}, kimi-k2-{6,7}, kimi-k3. +- **`prompt-preset/`** has per-model files (`gpt-5.5.ts`, `claude-opus-4-7.ts`, …) and a shared `file-operations.ts` tuning block. New model = new preset file + entry in `presets.ts`. Models covered: 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, kimi-k2-{6,7}, kimi-k3. - **`permission-system/` is a full port** of opencode's permission flow. - **`compaction/`** is policy-rich (`policy.ts`, `speculative.ts`, `restoration-tracker.ts`, `circuit-breaker.ts`, `degradation-monitor.ts`, `per-turn-cap.ts`, `tool-truncation.ts`, `checkpoint-state.ts`, `context-reduction.ts`, `openai-remote.ts`, `repair-tool-pairs.ts`, `state.ts`, `todo-bridge.ts`, `prompts.ts`). Touch only with policy tests in lock-step. - **External versions**: `external-versions.json` pins versions of sibling `../pi-extensions` packages used as vendored builtins; refresh with `packages/coding-agent/scripts/sync-builtin-extensions.mjs`. 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/gemini.ts b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/gemini.ts new file mode 100644 index 0000000000..3d3de8793f --- /dev/null +++ b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/gemini.ts @@ -0,0 +1,73 @@ +// 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"; + +export type GeminiConcern = "style" | "grounding" | "harness-contract" | "tool-orchestration"; + +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.", + }, +]; + +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/muse-spark.ts b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/muse-spark.ts new file mode 100644 index 0000000000..1b32a7f2e7 --- /dev/null +++ b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/muse-spark.ts @@ -0,0 +1,93 @@ +// 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"; + +export type MuseSparkConcern = "tool-orchestration" | "grounding" | "verification" | "deliberation"; + +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.", + }, + { + 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.", + }, + { + 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.", + }, + { + 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.", + }, +]; + +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-gemini-muse.test.ts b/packages/coding-agent/test/suite/prompt-presets-gemini-muse.test.ts new file mode 100644 index 0000000000..2e767a40fd --- /dev/null +++ b/packages/coding-agent/test/suite/prompt-presets-gemini-muse.test.ts @@ -0,0 +1,261 @@ +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 { buildGeminiPrompt } from "../../src/core/extensions/builtin/prompt-preset/gemini.ts"; +import { buildMuseSparkPrompt } 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"); + }); +}); From 6cfefafda51228c0d6fd398f919940159f6f6d23 Mon Sep 17 00:00:00 2001 From: Seungwoo Jeong Date: Sun, 16 Aug 2026 19:11:35 +0900 Subject: [PATCH 2/3] docs(changelog): record gemini + muse-spark prompt presets --- packages/coding-agent/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 9be6de5b8e..770e9f704a 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -41,6 +41,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 From 6dd60e15bdc75aa9bd1eacc03680e8e2f20d5214 Mon Sep 17 00:00:00 2001 From: Seungwoo Jeong Date: Mon, 17 Aug 2026 08:59:20 +0900 Subject: [PATCH 3/3] feat(prompt-preset): harden muse-spark for ultrawork + Korean report MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Muse Spark was the only thin preset whose rules actively told the model to distrust the exact mechanism ultrawork uses (hidden customType omo-ultrawork:directive) and to stop-and-report after a few calls. Observed symptom: ULTRAWORK MODE ENABLED! then stall, plus English-only reports despite ~/.omo/AGENTS.md Korean default. - muse-spark.ts 7→9 rules: add injected-directive-authority (mirror deepseek-v4.ts) and korean-easy-report with sentinel report-language: korean; relax 4 stall rules (no-hidden-control, one-goal-per-turn, observation-summary, chain-checkpoints) with harness-injected exceptions so ultrawork/ulw-plan/ulw-loop chains keep running and only drift triggers checkpoints - style.ts: Default to ASCII → Match the user's language (Korean default per ~/.omo/AGENTS.md) — global thin-preset language fix; full-core presets keep own copy (known residual documented) - tests: prompt-presets-gemini-muse 45/45 + sweep 291/291 — 9-rule parse + 3 sentinel tokens, verification.ts prompt-behavior-coverage (no pinned sentences) Plan: .omo/plans/muse-spark-ultrawork-korean-report-fix.md Evidence: .omo/evidence/muse-spark-ultrawork-korean-report-fix-20260817/ --- .../src/core/dynamic-prompt/style.ts | 2 +- .../builtin/prompt-preset/muse-spark.ts | 26 +++++++--- .../suite/prompt-presets-gemini-muse.test.ts | 52 ++++++++++++++++++- 3 files changed, 72 insertions(+), 8 deletions(-) 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/prompt-preset/muse-spark.ts b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/muse-spark.ts index 1b32a7f2e7..f40d031163 100644 --- 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 @@ -20,9 +20,11 @@ export type MuseSparkRuleId = | "evidence-before-success" | "observe-first" | "observation-summary" - | "chain-checkpoints"; + | "chain-checkpoints" + | "injected-directive-authority" + | "korean-easy-report"; -export type MuseSparkConcern = "tool-orchestration" | "grounding" | "verification" | "deliberation"; +export type MuseSparkConcern = "tool-orchestration" | "grounding" | "verification" | "deliberation" | "harness-contract" | "reporting"; export interface MuseSparkRule { readonly id: MuseSparkRuleId; @@ -41,13 +43,13 @@ export const MUSE_SPARK_RULES: readonly MuseSparkRule[] = [ 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.", + "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.", + "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", @@ -65,13 +67,25 @@ export const MUSE_SPARK_RULES: readonly MuseSparkRule[] = [ 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.", + "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.", + "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", }, ]; 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 index 2e767a40fd..1b0d500a93 100644 --- a/packages/coding-agent/test/suite/prompt-presets-gemini-muse.test.ts +++ b/packages/coding-agent/test/suite/prompt-presets-gemini-muse.test.ts @@ -3,7 +3,8 @@ 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 { buildGeminiPrompt } from "../../src/core/extensions/builtin/prompt-preset/gemini.ts"; -import { buildMuseSparkPrompt } from "../../src/core/extensions/builtin/prompt-preset/muse-spark.ts"; +import { buildMuseSparkPrompt, MUSE_SPARK_RULES } from "../../src/core/extensions/builtin/prompt-preset/muse-spark.ts"; +import { buildStyleSection } from "../../src/core/dynamic-prompt/style.ts"; import { type PromptPresetSettings, resolvePreset, @@ -259,3 +260,52 @@ describe("Gemini/Muse Spark preset settings and tokens", () => { expect(prompt).not.toContain("model-family: gemini"); }); }); + +describe("Muse Spark hardened rules (ultrawork + Korean report)", () => { + it("exposes 9 typed rules including the two new harness/reporting rules", () => { + const ids = MUSE_SPARK_RULES.map((rule) => rule.id); + expect(MUSE_SPARK_RULES).toHaveLength(9); + 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"); + }); +});