diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 222a491401..0e4712e69c 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -75,6 +75,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 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/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 9eaef3676a..19e8995c8f 100644 --- a/packages/coding-agent/src/core/extensions/builtin/AGENTS.md +++ b/packages/coding-agent/src/core/extensions/builtin/AGENTS.md @@ -1,4 +1,4 @@ -# packages/coding-agent/src/core/extensions/builtin +# packages/coding-agent/src/core/extensions/builtin 32 in-tree extensions plus 4 global defaults. Each is the canonical answer to "can senpi do X without core changes?". Registration order matters. @@ -6,11 +6,11 @@ | # | ID | Path | Role | |---|-----|------|------| -| 1 | `loop-guard` | `loop-guard/` | Identical-loop reminders plus first-veto blocking and system-abort recovery; similar/cyclic loops remain advisory — see `loop-guard/changes.md` | +| 1 | `loop-guard` | `loop-guard/` | Identical-loop reminders plus first-veto blocking and system-abort recovery; similar/cyclic loops remain advisory ??see `loop-guard/changes.md` | | 2 | `hooks` | `hooks/` | Settings-configured lifecycle command hooks (PreToolUse/PostToolUse-style) with trust hashing + live status | | 3 | `permission-system` | `permission-system/` | Full opencode-style permission port: rules, JSONL storage, prompts | | 4 | `gpt-apply-patch` | `gpt-apply-patch/` | Codex-style `apply_patch` tool with rich render + freeform grammar | -| 5 | `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) | +| 5 | `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) | | 6 | `todowrite` | `todotools/` | Op-based oh-my-pi todo port + `/todo` command; fully diverged from `../pi-extensions/pi-todotools` | | 7 | `redraws` | `redraws.ts` | `/tui` full-redraw count diagnostic | | 8 | `anthropic-web-search` | `anthropic-web-search/` | Anthropic-native web search tool | @@ -25,7 +25,7 @@ | 17 | `compaction` | `compaction/` | Plugsuit-style speculative + emergency compaction with restoration | | 18 | `history-search` | `history-search/` | Cross-session transcript search overlay (indexes session files) | | 19 | `help` | `help/` | `/help` + `/keybindings` TUI overlay panel (renders `interactive/help-content.ts`) | -| 20 | `import-repro` | `import-repro.ts` | `/ir` command — import an issue-analysis CI session gist and switch to it | +| 20 | `import-repro` | `import-repro.ts` | `/ir` command ??import an issue-analysis CI session gist and switch to it | | 21 | `websearch` | `websearch/` | Provider-backed `web_search` tool + `/websearch` (providers incl. kimi); vendored from `../pi-extensions/pi-websearch` | | 22 | `webfetch` | `webfetch/` | `webfetch` tool (md/text/html, gated by `PI_WEBFETCH`); vendored from `../pi-extensions/pi-webfetch` | | 23 | `video-in` | `video-in/` | Model-gated `read_video` tool (kimi-code ReadMediaFile parity); active only when the model declares the "video" input modality | @@ -33,23 +33,23 @@ | 25 | `nested-agents-md` | `nested-agents-md/` | Auto-injects nearby `AGENTS.md` + `/nested-agents`; vendored from `../pi-extensions/pi-nested-agents-md` | | 26 | `rules` | `rules/` | Rule-file discovery + `/rules`/`/reload-rules`; vendored from `../pi-extensions/pi-rules` | | 27 | `goal` | `goal/` | Budget-free goal tools + `/goal`; vendored from `../pi-extensions/pi-goal` | -| 28 | `ttsr` | `ttsr/` | Stream-rule detection (collapse + control-token-leak) with abort→remediate→retry; ported from oh-my-pi — see `ttsr/changes.md` | +| 28 | `ttsr` | `ttsr/` | Stream-rule detection (collapse + control-token-leak) with abort?뭨emediate?뭨etry; ported from oh-my-pi ??see `ttsr/changes.md` | | 29 | `btw` | `btw/` | `/btw` side-question command that queries in parallel without touching the main session | -| 30 | `claude-sdk-oauth` | `claude-sdk-oauth/` | Claude SDK OAuth provider: multi-account OAuth, resume-first session continuity, stream-safe failover — see `claude-sdk-oauth/AGENTS.md` + `changes.md` | +| 30 | `claude-sdk-oauth` | `claude-sdk-oauth/` | Claude SDK OAuth provider: multi-account OAuth, resume-first session continuity, stream-safe failover ??see `claude-sdk-oauth/AGENTS.md` + `changes.md` | | 31 | `config-reload` | `config-reload/` | Hash-gated watcher for trusted global/project config surfaces that defers a full session reload until idle and exposes the `config-watch:*` event protocol; registered after settings-dependent builtins so a reload rebuilds their resolved settings, and before final MCP observation | -| 32 | `mcp` | `mcp/` | Built-in MCP client: `mcpServers` config, stdio/http transports, `/mcp` commands, tool exposure policy — kept last so its provider-payload tap observes all co-resident builtin mutations; see `mcp/changes.md` | +| 32 | `mcp` | `mcp/` | Built-in MCP client: `mcpServers` config, stdio/http transports, `/mcp` commands, tool exposure policy ??kept last so its provider-payload tap observes all co-resident builtin mutations; see `mcp/changes.md` | Plus bundled extension **codemode** (`@code-yeongyu/senpi-codemode`, resolved by resource-loader.ts) and 4 **global default extensions** (resolved fast-path): `diff`, `files`, `prompt-url-widget`, `tps` (in `globalDefaultExtensionFactories`). Shared non-factory modules in this directory: -- `rule-activation/` — `appendRuleActivation` + renderer/types; consumed by `rules/` and `ttsr/`. -- `monitor-state-event.ts` — `TerminalMonitorStateEvent` + guard; consumed by `goal/` and `terminal/`. +- `rule-activation/` ??`appendRuleActivation` + renderer/types; consumed by `rules/` and `ttsr/`. +- `monitor-state-event.ts` ??`TerminalMonitorStateEvent` + guard; consumed by `goal/` and `terminal/`. ## ADDING A NEW BUILTIN EXTENSION -1. Create `builtin//index.ts` exporting `default function(pi: ExtensionAPI) { … }`. Single-file extensions go in `builtin/.ts`. -2. Add to `builtin/index.ts` import block + `builtinExtensions` array — pick registration order with intent. +1. Create `builtin//index.ts` exporting `default function(pi: ExtensionAPI) { ??}`. Single-file extensions go in `builtin/.ts`. +2. Add to `builtin/index.ts` import block + `builtinExtensions` array ??pick registration order with intent. 3. Add a regression test under `test/suite/-extension.test.ts` using `test/suite/harness.ts`. 4. If you modify upstream files (rare for new extensions), add a section to `/changes.md`. 5. Reach for `ExtensionContext` getters (the `ctx` parameter of event handlers); do NOT cross into `core/` directly. @@ -58,26 +58,26 @@ 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`. ## ANTI-PATTERNS -- Reordering `builtinExtensions` for cosmetic reasons — registration order is load-bearing for tools and permission hooks. -- Expecting context inside the factory body — `ExtensionContext` only arrives as the `ctx` parameter of event handlers. Do side effects inside `pi.on("session_start", …)`. -- Importing from `core/` directly — extensions must use the public `pi.*` API. +- Reordering `builtinExtensions` for cosmetic reasons ??registration order is load-bearing for tools and permission hooks. +- Expecting context inside the factory body ??`ExtensionContext` only arrives as the `ctx` parameter of event handlers. Do side effects inside `pi.on("session_start", ??`. +- Importing from `core/` directly ??extensions must use the public `pi.*` API. - Adding a new builtin without a regression test in `test/suite/-extension.test.ts`. -- Splitting an existing single-file extension into a folder "for symmetry" — only split when there's actual code to split. +- Splitting an existing single-file extension into a folder "for symmetry" ??only split when there's actual code to split. ## NOTES - `permission-system/storage.ts` writes JSONL approval logs; don't change the line shape without a migration. -- `compaction/restoration-tracker.ts` powers the post-compact context restoration feature — see `compaction/changes.md`. +- `compaction/restoration-tracker.ts` powers the post-compact context restoration feature ??see `compaction/changes.md`. - `goal/elapsed-ticker.ts` drives the live 'Pursuing goal...' footer refresh on a one-second cadence. - MCP search exposure tool is `tool_search` (builtin/tool-search/tool.ts). Do not reintroduce `mcp_search` references anywhere. -- Prompt presets routinely append the shared `file-operations.ts` tuning block. Mirror this when adding GPT-5.x presets — see `prompt-preset/changes.md` 2026-05-07. +- Prompt presets routinely append the shared `file-operations.ts` tuning block. Mirror this when adding GPT-5.x presets ??see `prompt-preset/changes.md` 2026-05-07. --- Generated: 2026-08-07 | Commit: `4f26b8282` 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..f40d031163 --- /dev/null +++ b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/muse-spark.ts @@ -0,0 +1,107 @@ +// 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"; + +export type MuseSparkConcern = "tool-orchestration" | "grounding" | "verification" | "deliberation" | "harness-contract" | "reporting"; + +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", + }, +]; + +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..1b0d500a93 --- /dev/null +++ b/packages/coding-agent/test/suite/prompt-presets-gemini-muse.test.ts @@ -0,0 +1,311 @@ +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, 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, + 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 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"); + }); +});