Merge pull request 'refactor(agent): remove Ponytail mode' (#3) from refactor/remove-ponytail into main
Reviewed-on: https://git.bnema.dev/bnema/zut/pulls/3
15 files · +11 -590
M README.md +4 -5
| @@ -158,13 +158,13 @@ $ZUT_HOME/ | |||
| 158 | 158 | └── logs/ # app log files | |
| 159 | 159 | ``` | |
| 160 | 160 | ||
| 161 | - | Drop a `SYSTEM.md` in `$ZUT_HOME` to replace the built-in identity and docs guidance for every run. Existing append addenda, including `AGENTS.md`, skills, and enabled Ponytail coding mode, remain layered on top. `--system-prompt` wins per invocation unless a selected replace-mode subagent profile supplies the child's identity. Delete the file to revert to the default identity. | |
| 161 | + | Drop a `SYSTEM.md` in `$ZUT_HOME` to replace the built-in identity and docs guidance for every run. Shared harness guidance and append addenda, including `AGENTS.md` and skills, remain layered on top. `--system-prompt` wins per invocation unless a selected replace-mode subagent profile supplies the child's identity. Delete the file to revert to the default identity. | |
| 162 | 162 | ||
| 163 | 163 | Shared prompt guidance remains active with custom identities and across providers and models. It asks the agent to complete authorized, non-destructive work rather than stop at a plan, while respecting requests for explanation or review and preserving required approvals. Explicit user instructions take precedence over skill guidelines; skills cannot grant permissions or override system or developer constraints. Writing guidance favors concise paragraphs, useful lists, and direct language without canned transitions. These are prompt instructions, not changes to tool permissions or guarantees of model behavior. | |
| 164 | 164 | ||
| 165 | - | Ponytail coding mode is enabled by default, including when an existing `config.json` does not contain a `ponytail_enabled` field. When disabled, its compact guidance is omitted from resolved system prompts; when enabled, the guidance tells the model to apply itself only to engineering work. The setting persists its explicit on/off choice in `$ZUT_HOME/config.json` and can be changed from `/settings`. It is advisory guidance only: it does not change tool permissions, confirmations, jail behavior, or sandboxing. | |
| 165 | + | Shared harness guidance asks agents to verify changes with checks appropriate to their scope and report which checks ran and what remains unverified. This guidance is included with custom identities and does not change tool permissions, confirmations, jail behavior, or sandboxing. | |
| 166 | 166 | ||
| 167 | - | A selected subagent profile with `systemPromptMode: replace` supplies the child's base identity even when `--system-prompt` was provided for the parent run. Global append addenda, including enabled Ponytail guidance, still follow their normal inclusion rules. | |
| 167 | + | A selected subagent profile with `systemPromptMode: replace` supplies the child's base identity even when `--system-prompt` was provided for the parent run. Append-mode profiles retain shared harness guidance and global append addenda. | |
| 168 | 168 | ||
| 169 | 169 | ### HTTP proxy | |
| 170 | 170 | ||
| @@ -354,7 +354,7 @@ Slash command names are case-insensitive in the TUI and messaging backends; argu | |||
| 354 | 354 | | `/unjail` | Allow tools to touch paths outside again. | | |
| 355 | 355 | | `/reload-ext` | Hot-reload all extensions (re-read manifests, respawn subprocesses, rebuild tool registry). | | |
| 356 | 356 | | `/telegram` | Connect, disconnect, or show status of the Telegram bridge (takes `connect` / `disconnect` / `status` as an optional argument; opens a picker without one). When connected, DMs from the paired user become prompts in the running session and the assistant's replies are mirrored back to Telegram. Alias: `/tg`. | | |
| 357 | - | | `/settings` | Change persistent settings, including web search, inline images, terminal alerts, AI terminal titles, proactive delegation, Ponytail coding mode, fast mode, main/sub-agent LSP access, and the auto-compact threshold. `/fast` and `/orchestrator` are shortcuts for their corresponding toggles. Saved to `$ZUT_HOME/config.json`; setting changes apply without a restart, while AI title generation waits for the first real prompt. | | |
| 357 | + | | `/settings` | Change persistent settings, including web search, inline images, terminal alerts, AI terminal titles, proactive delegation, fast mode, main/sub-agent LSP access, and the auto-compact threshold. `/fast` and `/orchestrator` are shortcuts for their corresponding toggles. Saved to `$ZUT_HOME/config.json`; setting changes apply without a restart, while AI title generation waits for the first real prompt. | | |
| 358 | 358 | | `/clear` | Clear the chat transcript. | | |
| 359 | 359 | | `/exit` | Exit zut. | | |
| 360 | 360 | ||
| @@ -470,7 +470,6 @@ Opens a dialog with every persistent setting. `up`/`down` to navigate, `enter` o | |||
| 470 | 470 | - **terminal alerts** — emit a terminal bell when the main session stops with work that may need attention, or when an extension raises a structured alert. Enabled by default; changes apply immediately and persist as `terminal_alerts_enabled`. Terminal emulators may render the bell audibly, visually, or not at all. | |
| 471 | 471 | - **AI terminal titles** — after the first real prompt of a fresh interactive session, make one small hidden request to the active model and set the terminal title to `zut: <title>`. The title is limited to 40 Unicode characters, persisted with the session, restored on resume, and never added to the conversation. Enabled by default; disable it to avoid the extra model request. The toggle applies immediately, but title generation still waits for the first real prompt and never starts from startup context, resumed history, or slash commands. Persists as `terminal_title_enabled`. | |
| 472 | 472 | - **proactive delegation** — let the interactive primary delegate concrete, bounded sidecars while it keeps ownership of the critical path. Because the task and the workers it needs are unknown until the user states them, the primary routes at that point: the reply that starts the work names the parts it keeps on the critical path and the profile that owns each delegable part, and it states when nothing is worth delegating instead of forcing workers. Before spawning, it identifies useful local work and gives the child a non-overlapping question, responsibility, package, or file set. Immediate blockers and tightly coupled work stay local; after spawning, the worker owns its scope until completion and the primary continues only its preselected independent task. If a user request or active workflow explicitly delegates blocking work, the primary yields rather than reproducing it. Off by default. When off, the permitted canonical subagent tools and profile manifest remain available for user-requested delegation or when an active skill workflow requires it. The tools accept named profiles, reasoning, per-spawn fast-mode overrides, and shared/worktree isolation where applicable; lifecycle and result references are persisted. Completion and required-work state are host-driven through `[auto-subagents update]` and `[required-subagents update]` messages; required work stays asynchronous, persists its outcome, and prevents the terminal parent response until a successful completion. If launch-time restrictions prevent spawning, the interactive primary continues locally. Explicit headless `--orchestrate` remains a separate strict manager-only mode and reports unavailable delegation rather than implementing directly. Do not poll or inspect dashboards, metadata, event logs, or files solely to wait. See `/subagents` and [docs/subagents.md](docs/subagents.md) for details. | |
| 473 | - | - **Ponytail coding mode** — include compact engineering guidance in each resolved system prompt; the guidance tells the model to apply it to coding, debugging, and review work rather than ordinary conversation. It favors understanding the real flow, small validated changes, reuse, and preserving safety checks. Enabled by default; changes apply to the next model call and persist as `ponytail_enabled` in `$ZUT_HOME/config.json`. It is included by interactive, print, stream, JSON, RPC, subagent, bot, SDK, and Zutfile agents. | |
| 474 | 473 | - **fast mode** — request the provider's fast tier where supported. It currently uses OpenAI's Fast service tier for OpenAI, OpenAI Responses, and OpenAI Codex models. Off by default; toggle it with `/fast` or `/settings`; changes apply on the next model call and persist as `fast_mode`. When a model profile is active, the value is saved in that profile and restored when switching profiles. Other providers currently return an unsupported-provider error. Fast mode may cost more and depends on the selected model/account. | |
| 475 | 474 | - **lsp in main session** — enable the built-in `lsp` tool and code diagnostics for the main agent. Enabled by default; changes persist as `lsp_enabled`. | |
| 476 | 475 | - **lsp in sub-agents** — allow newly spawned background sub-agents to use the built-in `lsp` tool. Enabled by default; changes persist as `subagent_lsp_enabled` and apply when a child starts. | |
| @@ -158,13 +158,13 @@ $ZUT_HOME/ | |||||
| 158 | └── logs/ # app log files | 158 | └── logs/ # app log files | ||
| 159 | ``` | 159 | ``` | ||
| 160 | 160 | ||||
| 161 | - | Drop a `SYSTEM.md` in `$ZUT_HOME` to replace the built-in identity and docs guidance for every run. Existing append addenda, including `AGENTS.md`, skills, and enabled Ponytail coding mode, remain layered on top. `--system-prompt` wins per invocation unless a selected replace-mode subagent profile supplies the child's identity. Delete the file to revert to the default identity. | 161 | + | Drop a `SYSTEM.md` in `$ZUT_HOME` to replace the built-in identity and docs guidance for every run. Shared harness guidance and append addenda, including `AGENTS.md` and skills, remain layered on top. `--system-prompt` wins per invocation unless a selected replace-mode subagent profile supplies the child's identity. Delete the file to revert to the default identity. |
| 162 | 162 | ||||
| 163 | Shared prompt guidance remains active with custom identities and across providers and models. It asks the agent to complete authorized, non-destructive work rather than stop at a plan, while respecting requests for explanation or review and preserving required approvals. Explicit user instructions take precedence over skill guidelines; skills cannot grant permissions or override system or developer constraints. Writing guidance favors concise paragraphs, useful lists, and direct language without canned transitions. These are prompt instructions, not changes to tool permissions or guarantees of model behavior. | 163 | Shared prompt guidance remains active with custom identities and across providers and models. It asks the agent to complete authorized, non-destructive work rather than stop at a plan, while respecting requests for explanation or review and preserving required approvals. Explicit user instructions take precedence over skill guidelines; skills cannot grant permissions or override system or developer constraints. Writing guidance favors concise paragraphs, useful lists, and direct language without canned transitions. These are prompt instructions, not changes to tool permissions or guarantees of model behavior. | ||
| 164 | 164 | ||||
| 165 | - | Ponytail coding mode is enabled by default, including when an existing `config.json` does not contain a `ponytail_enabled` field. When disabled, its compact guidance is omitted from resolved system prompts; when enabled, the guidance tells the model to apply itself only to engineering work. The setting persists its explicit on/off choice in `$ZUT_HOME/config.json` and can be changed from `/settings`. It is advisory guidance only: it does not change tool permissions, confirmations, jail behavior, or sandboxing. | 165 | + | Shared harness guidance asks agents to verify changes with checks appropriate to their scope and report which checks ran and what remains unverified. This guidance is included with custom identities and does not change tool permissions, confirmations, jail behavior, or sandboxing. |
| 166 | 166 | ||||
| 167 | - | A selected subagent profile with `systemPromptMode: replace` supplies the child's base identity even when `--system-prompt` was provided for the parent run. Global append addenda, including enabled Ponytail guidance, still follow their normal inclusion rules. | 167 | + | A selected subagent profile with `systemPromptMode: replace` supplies the child's base identity even when `--system-prompt` was provided for the parent run. Append-mode profiles retain shared harness guidance and global append addenda. |
| 168 | 168 | ||||
| 169 | ### HTTP proxy | 169 | ### HTTP proxy | ||
| 170 | 170 | ||||
| @@ -354,7 +354,7 @@ Slash command names are case-insensitive in the TUI and messaging backends; argu | |||||
| 354 | | `/unjail` | Allow tools to touch paths outside again. | | 354 | | `/unjail` | Allow tools to touch paths outside again. | | ||
| 355 | | `/reload-ext` | Hot-reload all extensions (re-read manifests, respawn subprocesses, rebuild tool registry). | | 355 | | `/reload-ext` | Hot-reload all extensions (re-read manifests, respawn subprocesses, rebuild tool registry). | | ||
| 356 | | `/telegram` | Connect, disconnect, or show status of the Telegram bridge (takes `connect` / `disconnect` / `status` as an optional argument; opens a picker without one). When connected, DMs from the paired user become prompts in the running session and the assistant's replies are mirrored back to Telegram. Alias: `/tg`. | | 356 | | `/telegram` | Connect, disconnect, or show status of the Telegram bridge (takes `connect` / `disconnect` / `status` as an optional argument; opens a picker without one). When connected, DMs from the paired user become prompts in the running session and the assistant's replies are mirrored back to Telegram. Alias: `/tg`. | | ||
| 357 | - | | `/settings` | Change persistent settings, including web search, inline images, terminal alerts, AI terminal titles, proactive delegation, Ponytail coding mode, fast mode, main/sub-agent LSP access, and the auto-compact threshold. `/fast` and `/orchestrator` are shortcuts for their corresponding toggles. Saved to `$ZUT_HOME/config.json`; setting changes apply without a restart, while AI title generation waits for the first real prompt. | | 357 | + | | `/settings` | Change persistent settings, including web search, inline images, terminal alerts, AI terminal titles, proactive delegation, fast mode, main/sub-agent LSP access, and the auto-compact threshold. `/fast` and `/orchestrator` are shortcuts for their corresponding toggles. Saved to `$ZUT_HOME/config.json`; setting changes apply without a restart, while AI title generation waits for the first real prompt. | |
| 358 | | `/clear` | Clear the chat transcript. | | 358 | | `/clear` | Clear the chat transcript. | | ||
| 359 | | `/exit` | Exit zut. | | 359 | | `/exit` | Exit zut. | | ||
| 360 | 360 | ||||
| @@ -470,7 +470,6 @@ Opens a dialog with every persistent setting. `up`/`down` to navigate, `enter` o | |||||
| 470 | - **terminal alerts** — emit a terminal bell when the main session stops with work that may need attention, or when an extension raises a structured alert. Enabled by default; changes apply immediately and persist as `terminal_alerts_enabled`. Terminal emulators may render the bell audibly, visually, or not at all. | 470 | - **terminal alerts** — emit a terminal bell when the main session stops with work that may need attention, or when an extension raises a structured alert. Enabled by default; changes apply immediately and persist as `terminal_alerts_enabled`. Terminal emulators may render the bell audibly, visually, or not at all. | ||
| 471 | - **AI terminal titles** — after the first real prompt of a fresh interactive session, make one small hidden request to the active model and set the terminal title to `zut: <title>`. The title is limited to 40 Unicode characters, persisted with the session, restored on resume, and never added to the conversation. Enabled by default; disable it to avoid the extra model request. The toggle applies immediately, but title generation still waits for the first real prompt and never starts from startup context, resumed history, or slash commands. Persists as `terminal_title_enabled`. | 471 | - **AI terminal titles** — after the first real prompt of a fresh interactive session, make one small hidden request to the active model and set the terminal title to `zut: <title>`. The title is limited to 40 Unicode characters, persisted with the session, restored on resume, and never added to the conversation. Enabled by default; disable it to avoid the extra model request. The toggle applies immediately, but title generation still waits for the first real prompt and never starts from startup context, resumed history, or slash commands. Persists as `terminal_title_enabled`. | ||
| 472 | - **proactive delegation** — let the interactive primary delegate concrete, bounded sidecars while it keeps ownership of the critical path. Because the task and the workers it needs are unknown until the user states them, the primary routes at that point: the reply that starts the work names the parts it keeps on the critical path and the profile that owns each delegable part, and it states when nothing is worth delegating instead of forcing workers. Before spawning, it identifies useful local work and gives the child a non-overlapping question, responsibility, package, or file set. Immediate blockers and tightly coupled work stay local; after spawning, the worker owns its scope until completion and the primary continues only its preselected independent task. If a user request or active workflow explicitly delegates blocking work, the primary yields rather than reproducing it. Off by default. When off, the permitted canonical subagent tools and profile manifest remain available for user-requested delegation or when an active skill workflow requires it. The tools accept named profiles, reasoning, per-spawn fast-mode overrides, and shared/worktree isolation where applicable; lifecycle and result references are persisted. Completion and required-work state are host-driven through `[auto-subagents update]` and `[required-subagents update]` messages; required work stays asynchronous, persists its outcome, and prevents the terminal parent response until a successful completion. If launch-time restrictions prevent spawning, the interactive primary continues locally. Explicit headless `--orchestrate` remains a separate strict manager-only mode and reports unavailable delegation rather than implementing directly. Do not poll or inspect dashboards, metadata, event logs, or files solely to wait. See `/subagents` and [docs/subagents.md](docs/subagents.md) for details. | 472 | - **proactive delegation** — let the interactive primary delegate concrete, bounded sidecars while it keeps ownership of the critical path. Because the task and the workers it needs are unknown until the user states them, the primary routes at that point: the reply that starts the work names the parts it keeps on the critical path and the profile that owns each delegable part, and it states when nothing is worth delegating instead of forcing workers. Before spawning, it identifies useful local work and gives the child a non-overlapping question, responsibility, package, or file set. Immediate blockers and tightly coupled work stay local; after spawning, the worker owns its scope until completion and the primary continues only its preselected independent task. If a user request or active workflow explicitly delegates blocking work, the primary yields rather than reproducing it. Off by default. When off, the permitted canonical subagent tools and profile manifest remain available for user-requested delegation or when an active skill workflow requires it. The tools accept named profiles, reasoning, per-spawn fast-mode overrides, and shared/worktree isolation where applicable; lifecycle and result references are persisted. Completion and required-work state are host-driven through `[auto-subagents update]` and `[required-subagents update]` messages; required work stays asynchronous, persists its outcome, and prevents the terminal parent response until a successful completion. If launch-time restrictions prevent spawning, the interactive primary continues locally. Explicit headless `--orchestrate` remains a separate strict manager-only mode and reports unavailable delegation rather than implementing directly. Do not poll or inspect dashboards, metadata, event logs, or files solely to wait. See `/subagents` and [docs/subagents.md](docs/subagents.md) for details. | ||
| 473 | - | - **Ponytail coding mode** — include compact engineering guidance in each resolved system prompt; the guidance tells the model to apply it to coding, debugging, and review work rather than ordinary conversation. It favors understanding the real flow, small validated changes, reuse, and preserving safety checks. Enabled by default; changes apply to the next model call and persist as `ponytail_enabled` in `$ZUT_HOME/config.json`. It is included by interactive, print, stream, JSON, RPC, subagent, bot, SDK, and Zutfile agents. | |||
| 474 | - **fast mode** — request the provider's fast tier where supported. It currently uses OpenAI's Fast service tier for OpenAI, OpenAI Responses, and OpenAI Codex models. Off by default; toggle it with `/fast` or `/settings`; changes apply on the next model call and persist as `fast_mode`. When a model profile is active, the value is saved in that profile and restored when switching profiles. Other providers currently return an unsupported-provider error. Fast mode may cost more and depends on the selected model/account. | 473 | - **fast mode** — request the provider's fast tier where supported. It currently uses OpenAI's Fast service tier for OpenAI, OpenAI Responses, and OpenAI Codex models. Off by default; toggle it with `/fast` or `/settings`; changes apply on the next model call and persist as `fast_mode`. When a model profile is active, the value is saved in that profile and restored when switching profiles. Other providers currently return an unsupported-provider error. Fast mode may cost more and depends on the selected model/account. | ||
| 475 | - **lsp in main session** — enable the built-in `lsp` tool and code diagnostics for the main agent. Enabled by default; changes persist as `lsp_enabled`. | 474 | - **lsp in main session** — enable the built-in `lsp` tool and code diagnostics for the main agent. Enabled by default; changes persist as `lsp_enabled`. | ||
| 476 | - **lsp in sub-agents** — allow newly spawned background sub-agents to use the built-in `lsp` tool. Enabled by default; changes persist as `subagent_lsp_enabled` and apply when a child starts. | 475 | - **lsp in sub-agents** — allow newly spawned background sub-agents to use the built-in `lsp` tool. Enabled by default; changes persist as `subagent_lsp_enabled` and apply when a child starts. | ||
M docs/rpc.md +1 -1
| @@ -25,7 +25,7 @@ You'll see one JSON object per line on stdout: a response acknowledging the prom | |||
| 25 | 25 | ||
| 26 | 26 | `zut rpc` accepts the same flags as the other modes: `--provider`, `--model`, `--cwd`, `--api-key`, `--base-url`, `--system-prompt`, `--append-system-prompt`, `--reasoning`, `--max-steps`, `--no-tools`, `--tools`, `--no-context-files`. Sessions are disabled by default in RPC mode — the embedding application owns persistence. | |
| 27 | 27 | ||
| 28 | - | RPC agents use the same resolved system-prompt policy as other modes. Ponytail coding mode is enabled when `ponytail_enabled` is missing or `true` in `$ZUT_HOME/config.json`, and disabled when it is `false`. A custom `--system-prompt` replaces the built-in identity and docs guidance, while enabled append addenda remain present. | |
| 28 | + | RPC agents use the same resolved system-prompt policy as other modes. A custom `--system-prompt` replaces the built-in identity and docs guidance, while shared harness guidance and append addenda remain present. Shared guidance asks agents to verify changes with checks appropriate to their scope and report which checks ran and what remains unverified. | |
| 29 | 29 | ||
| 30 | 30 | ## Auth | |
| 31 | 31 | ||
| @@ -25,7 +25,7 @@ You'll see one JSON object per line on stdout: a response acknowledging the prom | |||||
| 25 | 25 | ||||
| 26 | `zut rpc` accepts the same flags as the other modes: `--provider`, `--model`, `--cwd`, `--api-key`, `--base-url`, `--system-prompt`, `--append-system-prompt`, `--reasoning`, `--max-steps`, `--no-tools`, `--tools`, `--no-context-files`. Sessions are disabled by default in RPC mode — the embedding application owns persistence. | 26 | `zut rpc` accepts the same flags as the other modes: `--provider`, `--model`, `--cwd`, `--api-key`, `--base-url`, `--system-prompt`, `--append-system-prompt`, `--reasoning`, `--max-steps`, `--no-tools`, `--tools`, `--no-context-files`. Sessions are disabled by default in RPC mode — the embedding application owns persistence. | ||
| 27 | 27 | ||||
| 28 | - | RPC agents use the same resolved system-prompt policy as other modes. Ponytail coding mode is enabled when `ponytail_enabled` is missing or `true` in `$ZUT_HOME/config.json`, and disabled when it is `false`. A custom `--system-prompt` replaces the built-in identity and docs guidance, while enabled append addenda remain present. | 28 | + | RPC agents use the same resolved system-prompt policy as other modes. A custom `--system-prompt` replaces the built-in identity and docs guidance, while shared harness guidance and append addenda remain present. Shared guidance asks agents to verify changes with checks appropriate to their scope and report which checks ran and what remains unverified. |
| 29 | 29 | ||||
| 30 | ## Auth | 30 | ## Auth | ||
| 31 | 31 | ||||
M docs/zutfiles.md +1 -1
| @@ -115,7 +115,7 @@ Set `replace_system_prompt` to `true` in the manifest to use `AGENT.md` as the r | |||
| 115 | 115 | } | |
| 116 | 116 | ``` | |
| 117 | 117 | ||
| 118 | - | Replacement is intended for fully specialized agents. It replaces zut's built-in identity, while global append addenda such as enabled Ponytail coding guidance, project context, and skills retain their normal inclusion rules. The default layering behavior is usually preferable because it retains zut's normal identity and tool-use guidance. | |
| 118 | + | Replacement is intended for fully specialized agents. It replaces zut's built-in identity, while shared harness guidance and global append addenda such as project context and skills retain their normal inclusion rules. The default layering behavior is usually preferable because it retains zut's normal identity and tool-use guidance. | |
| 119 | 119 | ||
| 120 | 120 | ## Bundled skills | |
| 121 | 121 | ||
| @@ -115,7 +115,7 @@ Set `replace_system_prompt` to `true` in the manifest to use `AGENT.md` as the r | |||||
| 115 | } | 115 | } | ||
| 116 | ``` | 116 | ``` | ||
| 117 | 117 | ||||
| 118 | - | Replacement is intended for fully specialized agents. It replaces zut's built-in identity, while global append addenda such as enabled Ponytail coding guidance, project context, and skills retain their normal inclusion rules. The default layering behavior is usually preferable because it retains zut's normal identity and tool-use guidance. | 118 | + | Replacement is intended for fully specialized agents. It replaces zut's built-in identity, while shared harness guidance and global append addenda such as project context and skills retain their normal inclusion rules. The default layering behavior is usually preferable because it retains zut's normal identity and tool-use guidance. |
| 119 | 119 | ||||
| 120 | ## Bundled skills | 120 | ## Bundled skills | ||
| 121 | 121 | ||||
M packages/agent/build.go +0 -5
| @@ -962,11 +962,6 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { | |||
| 962 | 962 | contextFiles = loadAgentsContext(args.CWD, ZutHome()) | |
| 963 | 963 | } | |
| 964 | 964 | append_ := []string(nil) | |
| 965 | - | if cfg.PonytailModeEnabled() { | |
| 966 | - | if ponytailAddendum := PonytailSystemAddendum(); ponytailAddendum != "" { | |
| 967 | - | append_ = append(append_, ponytailAddendum) | |
| 968 | - | } | |
| 969 | - | } | |
| 970 | 965 | append_ = append(append_, args.AppendSystemPrompt...) | |
| 971 | 966 | if agentsAddendum := formatAgentsContext(contextFiles); agentsAddendum != "" { | |
| 972 | 967 | append_ = append(append_, agentsAddendum) | |
| @@ -962,11 +962,6 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { | |||||
| 962 | contextFiles = loadAgentsContext(args.CWD, ZutHome()) | 962 | contextFiles = loadAgentsContext(args.CWD, ZutHome()) | ||
| 963 | } | 963 | } | ||
| 964 | append_ := []string(nil) | 964 | append_ := []string(nil) | ||
| 965 | - | if cfg.PonytailModeEnabled() { | |||
| 966 | - | if ponytailAddendum := PonytailSystemAddendum(); ponytailAddendum != "" { | |||
| 967 | - | append_ = append(append_, ponytailAddendum) | |||
| 968 | - | } | |||
| 969 | - | } | |||
| 970 | append_ = append(append_, args.AppendSystemPrompt...) | 965 | append_ = append(append_, args.AppendSystemPrompt...) | ||
| 971 | if agentsAddendum := formatAgentsContext(contextFiles); agentsAddendum != "" { | 966 | if agentsAddendum := formatAgentsContext(contextFiles); agentsAddendum != "" { | ||
| 972 | append_ = append(append_, agentsAddendum) | 967 | append_ = append(append_, agentsAddendum) | ||
M packages/agent/cli.go +1 -11
| @@ -2715,7 +2715,6 @@ func runInteractive(ctx context.Context, args Args, version string) (runErr erro | |||
| 2715 | 2715 | TerminalAlertsEnabled: initialCfg.TerminalAlertsEnabled, | |
| 2716 | 2716 | TerminalTitleEnabled: initialCfg.TerminalTitleEnabled, | |
| 2717 | 2717 | AutoSubagentsEnabled: initialCfg.AutoSubagentsEnabled, | |
| 2718 | - | PonytailEnabled: initialCfg.PonytailEnabled, | |
| 2719 | 2718 | WebSearchEnabled: initialCfg.WebSearchEnabled, | |
| 2720 | 2719 | WebSearchToolAllowed: &webSearchToolAllowedForInvocation, | |
| 2721 | 2720 | WebSearchInvocationOverride: webSearchInvocationOverride, | |
| @@ -2809,16 +2808,7 @@ func runInteractive(ctx context.Context, args Args, version string) (runErr erro | |||
| 2809 | 2808 | setWebSearchAvailable(webSearchPolicy == subagents.WebSearchAllow) | |
| 2810 | 2809 | return nil | |
| 2811 | 2810 | }, | |
| 2812 | - | SetWebSearchAvailable: setWebSearchAvailable, | |
| 2813 | - | RefreshPrompt: func() error { | |
| 2814 | - | current := liveInteractiveAgent(iv, ag) | |
| 2815 | - | webSearchPolicy, err := refreshAgentToolsAndPrompt(args, sharedSandbox, extToolAdapter, current, prepareInteractiveRegistryWithScheduler, iv) | |
| 2816 | - | if err != nil { | |
| 2817 | - | return err | |
| 2818 | - | } | |
| 2819 | - | setWebSearchAvailable(webSearchPolicy == subagents.WebSearchAllow) | |
| 2820 | - | return nil | |
| 2821 | - | }, | |
| 2811 | + | SetWebSearchAvailable: setWebSearchAvailable, | |
| 2822 | 2812 | AuthManager: mgr, | |
| 2823 | 2813 | LlamaCPPConfig: ResolveLlamaCPPConfig, | |
| 2824 | 2814 | RefreshLlamaCPPModels: RefreshLlamaCPPModels, | |
| @@ -2715,7 +2715,6 @@ func runInteractive(ctx context.Context, args Args, version string) (runErr erro | |||||
| 2715 | TerminalAlertsEnabled: initialCfg.TerminalAlertsEnabled, | 2715 | TerminalAlertsEnabled: initialCfg.TerminalAlertsEnabled, | ||
| 2716 | TerminalTitleEnabled: initialCfg.TerminalTitleEnabled, | 2716 | TerminalTitleEnabled: initialCfg.TerminalTitleEnabled, | ||
| 2717 | AutoSubagentsEnabled: initialCfg.AutoSubagentsEnabled, | 2717 | AutoSubagentsEnabled: initialCfg.AutoSubagentsEnabled, | ||
| 2718 | - | PonytailEnabled: initialCfg.PonytailEnabled, | |||
| 2719 | WebSearchEnabled: initialCfg.WebSearchEnabled, | 2718 | WebSearchEnabled: initialCfg.WebSearchEnabled, | ||
| 2720 | WebSearchToolAllowed: &webSearchToolAllowedForInvocation, | 2719 | WebSearchToolAllowed: &webSearchToolAllowedForInvocation, | ||
| 2721 | WebSearchInvocationOverride: webSearchInvocationOverride, | 2720 | WebSearchInvocationOverride: webSearchInvocationOverride, | ||
| @@ -2809,16 +2808,7 @@ func runInteractive(ctx context.Context, args Args, version string) (runErr erro | |||||
| 2809 | setWebSearchAvailable(webSearchPolicy == subagents.WebSearchAllow) | 2808 | setWebSearchAvailable(webSearchPolicy == subagents.WebSearchAllow) | ||
| 2810 | return nil | 2809 | return nil | ||
| 2811 | }, | 2810 | }, | ||
| 2812 | - | SetWebSearchAvailable: setWebSearchAvailable, | 2811 | + | SetWebSearchAvailable: setWebSearchAvailable, |
| 2813 | - | RefreshPrompt: func() error { | |||
| 2814 | - | current := liveInteractiveAgent(iv, ag) | |||
| 2815 | - | webSearchPolicy, err := refreshAgentToolsAndPrompt(args, sharedSandbox, extToolAdapter, current, prepareInteractiveRegistryWithScheduler, iv) | |||
| 2816 | - | if err != nil { | |||
| 2817 | - | return err | |||
| 2818 | - | } | |||
| 2819 | - | setWebSearchAvailable(webSearchPolicy == subagents.WebSearchAllow) | |||
| 2820 | - | return nil | |||
| 2821 | - | }, | |||
| 2822 | AuthManager: mgr, | 2812 | AuthManager: mgr, | ||
| 2823 | LlamaCPPConfig: ResolveLlamaCPPConfig, | 2813 | LlamaCPPConfig: ResolveLlamaCPPConfig, | ||
| 2824 | RefreshLlamaCPPModels: RefreshLlamaCPPModels, | 2814 | RefreshLlamaCPPModels: RefreshLlamaCPPModels, | ||
M packages/agent/config.go +0 -10
| @@ -102,10 +102,6 @@ type Config struct { | |||
| 102 | 102 | // nil/missing means disabled. Toggle from /settings. | |
| 103 | 103 | AutoSubagentsEnabled *bool `json:"auto_subagents_enabled,omitempty"` | |
| 104 | 104 | ||
| 105 | - | // PonytailEnabled controls the compact coding-guidance addendum. On by | |
| 106 | - | // default; nil/missing means enabled. Toggle from /settings. | |
| 107 | - | PonytailEnabled *bool `json:"ponytail_enabled,omitempty"` | |
| 108 | - | ||
| 109 | 105 | // WebSearchEnabled controls the built-in web_search capability for | |
| 110 | 106 | // normal CLI sessions. nil/missing means enabled so existing config | |
| 111 | 107 | // files retain the default-on behavior. | |
| @@ -274,12 +270,6 @@ func (c Config) LSPEnabledFor(subagent bool) bool { | |||
| 274 | 270 | return value == nil || *value | |
| 275 | 271 | } | |
| 276 | 272 | ||
| 277 | - | // PonytailModeEnabled reports the default-on coding-guidance preference | |
| 278 | - | // without requiring a generated config entry. | |
| 279 | - | func (c Config) PonytailModeEnabled() bool { | |
| 280 | - | return c.PonytailEnabled == nil || *c.PonytailEnabled | |
| 281 | - | } | |
| 282 | - | ||
| 283 | 273 | // WebSearchEnabledForCLI reports the default-on web-search preference | |
| 284 | 274 | // without requiring a generated config entry. | |
| 285 | 275 | func (c Config) WebSearchEnabledForCLI() bool { | |
| @@ -102,10 +102,6 @@ type Config struct { | |||||
| 102 | // nil/missing means disabled. Toggle from /settings. | 102 | // nil/missing means disabled. Toggle from /settings. | ||
| 103 | AutoSubagentsEnabled *bool `json:"auto_subagents_enabled,omitempty"` | 103 | AutoSubagentsEnabled *bool `json:"auto_subagents_enabled,omitempty"` | ||
| 104 | 104 | ||||
| 105 | - | // PonytailEnabled controls the compact coding-guidance addendum. On by | |||
| 106 | - | // default; nil/missing means enabled. Toggle from /settings. | |||
| 107 | - | PonytailEnabled *bool `json:"ponytail_enabled,omitempty"` | |||
| 108 | - | ||||
| 109 | // WebSearchEnabled controls the built-in web_search capability for | 105 | // WebSearchEnabled controls the built-in web_search capability for | ||
| 110 | // normal CLI sessions. nil/missing means enabled so existing config | 106 | // normal CLI sessions. nil/missing means enabled so existing config | ||
| 111 | // files retain the default-on behavior. | 107 | // files retain the default-on behavior. | ||
| @@ -274,12 +270,6 @@ func (c Config) LSPEnabledFor(subagent bool) bool { | |||||
| 274 | return value == nil || *value | 270 | return value == nil || *value | ||
| 275 | } | 271 | } | ||
| 276 | 272 | ||||
| 277 | - | // PonytailModeEnabled reports the default-on coding-guidance preference | |||
| 278 | - | // without requiring a generated config entry. | |||
| 279 | - | func (c Config) PonytailModeEnabled() bool { | |||
| 280 | - | return c.PonytailEnabled == nil || *c.PonytailEnabled | |||
| 281 | - | } | |||
| 282 | - | ||||
| 283 | // WebSearchEnabledForCLI reports the default-on web-search preference | 273 | // WebSearchEnabledForCLI reports the default-on web-search preference | ||
| 284 | // without requiring a generated config entry. | 274 | // without requiring a generated config entry. | ||
| 285 | func (c Config) WebSearchEnabledForCLI() bool { | 275 | func (c Config) WebSearchEnabledForCLI() bool { | ||
M packages/agent/modes/interactive.go +1 -14
| @@ -69,10 +69,6 @@ type InteractiveConfig struct { | |||
| 69 | 69 | // re-reading config.json on every open. | |
| 70 | 70 | AutoSubagentsEnabled *bool | |
| 71 | 71 | ||
| 72 | - | // PonytailEnabled mirrors the persisted default-on coding-guidance | |
| 73 | - | // setting at startup. nil/missing means enabled. | |
| 74 | - | PonytailEnabled *bool | |
| 75 | - | ||
| 76 | 72 | // WebSearchEnabled mirrors the persisted default-on web-search setting. | |
| 77 | 73 | // nil/missing means enabled for normal interactive sessions. | |
| 78 | 74 | WebSearchEnabled *bool | |
| @@ -232,10 +228,6 @@ type InteractiveConfig struct { | |||
| 232 | 228 | // web-search guard and resident child policy. | |
| 233 | 229 | SetWebSearchAvailable func(bool) | |
| 234 | 230 | ||
| 235 | - | // RefreshPrompt re-resolves the complete live prompt and tool registry | |
| 236 | - | // after a prompt-affecting setting changes. Optional for embedders. | |
| 237 | - | RefreshPrompt func() error | |
| 238 | - | ||
| 239 | 231 | // AutoSubmitInitial, when true, auto-submits InitialInput after | |
| 240 | 232 | // StartupPre completes (or immediately when StartupPre is empty). | |
| 241 | 233 | // When false, InitialInput only pre-fills the editor. | |
| @@ -586,10 +578,6 @@ type fastModeSettingsStore interface { | |||
| 586 | 578 | SetFastMode(enabled bool) error | |
| 587 | 579 | } | |
| 588 | 580 | ||
| 589 | - | type ponytailSettingsStore interface { | |
| 590 | - | SetPonytailEnabled(enabled bool) error | |
| 591 | - | } | |
| 592 | - | ||
| 593 | 581 | type webSearchSettingsStore interface { | |
| 594 | 582 | SetWebSearchEnabled(enabled bool) error | |
| 595 | 583 | } | |
| @@ -1994,8 +1982,7 @@ const modalBackdropDimPercent = 50 | |||
| 1994 | 1982 | // hold i.mu. | |
| 1995 | 1983 | ||
| 1996 | 1984 | // autoCompactContinuationPrompt is an interactive-only compact-handoff | |
| 1997 | - | // instruction. Its completion criteria must stay separate from Ponytail so | |
| 1998 | - | // toggling the optional coding-style addendum never changes recovery behavior. | |
| 1985 | + | // instruction defining recovery completion criteria. | |
| 1999 | 1986 | const autoCompactContinuationPrompt = `Context was compacted while work was in progress. Continue the user's most recent active request now; do not wait for them to type "continue". Inspect the context summary and kept recent messages, treating active constraints and preferences there as still in force, including delegation/subagent instructions. If a newer request supersedes earlier plans in the summary, follow the newer request. A progress report, plan update, or statement of future work is not completion: if required work remains, take the next concrete action now. Only finish when the request is actually complete or when a specific user decision is required. If nothing remains, give a brief truthful completion; do not invent work or force a tool call.` | |
| 2000 | 1987 | ||
| 2001 | 1988 | type compactHandoffResume uint8 | |
| @@ -69,10 +69,6 @@ type InteractiveConfig struct { | |||||
| 69 | // re-reading config.json on every open. | 69 | // re-reading config.json on every open. | ||
| 70 | AutoSubagentsEnabled *bool | 70 | AutoSubagentsEnabled *bool | ||
| 71 | 71 | ||||
| 72 | - | // PonytailEnabled mirrors the persisted default-on coding-guidance | |||
| 73 | - | // setting at startup. nil/missing means enabled. | |||
| 74 | - | PonytailEnabled *bool | |||
| 75 | - | ||||
| 76 | // WebSearchEnabled mirrors the persisted default-on web-search setting. | 72 | // WebSearchEnabled mirrors the persisted default-on web-search setting. | ||
| 77 | // nil/missing means enabled for normal interactive sessions. | 73 | // nil/missing means enabled for normal interactive sessions. | ||
| 78 | WebSearchEnabled *bool | 74 | WebSearchEnabled *bool | ||
| @@ -232,10 +228,6 @@ type InteractiveConfig struct { | |||||
| 232 | // web-search guard and resident child policy. | 228 | // web-search guard and resident child policy. | ||
| 233 | SetWebSearchAvailable func(bool) | 229 | SetWebSearchAvailable func(bool) | ||
| 234 | 230 | ||||
| 235 | - | // RefreshPrompt re-resolves the complete live prompt and tool registry | |||
| 236 | - | // after a prompt-affecting setting changes. Optional for embedders. | |||
| 237 | - | RefreshPrompt func() error | |||
| 238 | - | ||||
| 239 | // AutoSubmitInitial, when true, auto-submits InitialInput after | 231 | // AutoSubmitInitial, when true, auto-submits InitialInput after | ||
| 240 | // StartupPre completes (or immediately when StartupPre is empty). | 232 | // StartupPre completes (or immediately when StartupPre is empty). | ||
| 241 | // When false, InitialInput only pre-fills the editor. | 233 | // When false, InitialInput only pre-fills the editor. | ||
| @@ -586,10 +578,6 @@ type fastModeSettingsStore interface { | |||||
| 586 | SetFastMode(enabled bool) error | 578 | SetFastMode(enabled bool) error | ||
| 587 | } | 579 | } | ||
| 588 | 580 | ||||
| 589 | - | type ponytailSettingsStore interface { | |||
| 590 | - | SetPonytailEnabled(enabled bool) error | |||
| 591 | - | } | |||
| 592 | - | ||||
| 593 | type webSearchSettingsStore interface { | 581 | type webSearchSettingsStore interface { | ||
| 594 | SetWebSearchEnabled(enabled bool) error | 582 | SetWebSearchEnabled(enabled bool) error | ||
| 595 | } | 583 | } | ||
| @@ -1994,8 +1982,7 @@ const modalBackdropDimPercent = 50 | |||||
| 1994 | // hold i.mu. | 1982 | // hold i.mu. | ||
| 1995 | 1983 | ||||
| 1996 | // autoCompactContinuationPrompt is an interactive-only compact-handoff | 1984 | // autoCompactContinuationPrompt is an interactive-only compact-handoff | ||
| 1997 | - | // instruction. Its completion criteria must stay separate from Ponytail so | 1985 | + | // instruction defining recovery completion criteria. |
| 1998 | - | // toggling the optional coding-style addendum never changes recovery behavior. | |||
| 1999 | const autoCompactContinuationPrompt = `Context was compacted while work was in progress. Continue the user's most recent active request now; do not wait for them to type "continue". Inspect the context summary and kept recent messages, treating active constraints and preferences there as still in force, including delegation/subagent instructions. If a newer request supersedes earlier plans in the summary, follow the newer request. A progress report, plan update, or statement of future work is not completion: if required work remains, take the next concrete action now. Only finish when the request is actually complete or when a specific user decision is required. If nothing remains, give a brief truthful completion; do not invent work or force a tool call.` | 1986 | const autoCompactContinuationPrompt = `Context was compacted while work was in progress. Continue the user's most recent active request now; do not wait for them to type "continue". Inspect the context summary and kept recent messages, treating active constraints and preferences there as still in force, including delegation/subagent instructions. If a newer request supersedes earlier plans in the summary, follow the newer request. A progress report, plan update, or statement of future work is not completion: if required work remains, take the next concrete action now. Only finish when the request is actually complete or when a specific user decision is required. If nothing remains, give a brief truthful completion; do not invent work or force a tool call.` | ||
| 2000 | 1987 | ||||
| 2001 | type compactHandoffResume uint8 | 1988 | type compactHandoffResume uint8 | ||
M packages/agent/modes/interactive_settings.go +0 -72
| @@ -158,13 +158,6 @@ func (i *Interactive) openSettingsDialog() { | |||
| 158 | 158 | autoSubagents = false | |
| 159 | 159 | } | |
| 160 | 160 | ||
| 161 | - | ponytailEnabled := i.ponytailEnabled() | |
| 162 | - | ponytailDisabled := !i.ponytailSettingsAvailable() | |
| 163 | - | ponytailHint := "" | |
| 164 | - | if ponytailDisabled { | |
| 165 | - | ponytailHint = "requires persistent settings and live prompt refresh support" | |
| 166 | - | } | |
| 167 | - | ||
| 168 | 161 | webSearchEnabled := i.webSearchEffectiveEnabled() | |
| 169 | 162 | webSearchDisabled := !i.webSearchSettingsAvailable() | |
| 170 | 163 | webSearchHint := i.webSearchUnavailableHint() | |
| @@ -296,14 +289,6 @@ func (i *Interactive) openSettingsDialog() { | |||
| 296 | 289 | disabled: autoSubagentsDisabled, | |
| 297 | 290 | hint: autoSubagentsHint, | |
| 298 | 291 | }, | |
| 299 | - | { | |
| 300 | - | key: "ponytail_enabled", | |
| 301 | - | label: "ponytail coding mode", | |
| 302 | - | desc: "apply compact coding guidance that favors small, validated, maintainable changes", | |
| 303 | - | value: ponytailEnabled, | |
| 304 | - | disabled: ponytailDisabled, | |
| 305 | - | hint: ponytailHint, | |
| 306 | - | }, | |
| 307 | 292 | { | |
| 308 | 293 | key: "web_search_enabled", | |
| 309 | 294 | label: "web search", | |
| @@ -645,13 +630,6 @@ func (i *Interactive) resetSettingsToggle(key string, value bool) { | |||
| 645 | 630 | } | |
| 646 | 631 | } | |
| 647 | 632 | } | |
| 648 | - | func (i *Interactive) ponytailEnabled() bool { | |
| 649 | - | return i.cfg.PonytailEnabled == nil || *i.cfg.PonytailEnabled | |
| 650 | - | } | |
| 651 | - | func (i *Interactive) ponytailSettingsAvailable() bool { | |
| 652 | - | _, persistent := i.cfg.SettingsStore.(ponytailSettingsStore) | |
| 653 | - | return persistent && i.cfg.RefreshPrompt != nil | |
| 654 | - | } | |
| 655 | 633 | func (i *Interactive) webSearchEnabled() bool { | |
| 656 | 634 | return i.cfg.WebSearchEnabled == nil || *i.cfg.WebSearchEnabled | |
| 657 | 635 | } | |
| @@ -847,56 +825,6 @@ func (i *Interactive) applySettingToggle(key string, value bool) { | |||
| 847 | 825 | } else { | |
| 848 | 826 | apply() | |
| 849 | 827 | } | |
| 850 | - | case "ponytail_enabled": | |
| 851 | - | previous := i.ponytailEnabled() | |
| 852 | - | store, available := i.cfg.SettingsStore.(ponytailSettingsStore) | |
| 853 | - | if !available || i.cfg.RefreshPrompt == nil { | |
| 854 | - | i.resetSettingsToggle(key, previous) | |
| 855 | - | i.mu.Lock() | |
| 856 | - | i.statusOK = "" | |
| 857 | - | i.statusErr = "ponytail coding mode unavailable: persistent settings and live prompt refresh are required" | |
| 858 | - | i.mu.Unlock() | |
| 859 | - | return | |
| 860 | - | } | |
| 861 | - | if err := store.SetPonytailEnabled(value); err != nil { | |
| 862 | - | i.resetSettingsToggle(key, previous) | |
| 863 | - | i.mu.Lock() | |
| 864 | - | i.statusOK = "" | |
| 865 | - | i.statusErr = "settings: " + err.Error() | |
| 866 | - | i.mu.Unlock() | |
| 867 | - | return | |
| 868 | - | } | |
| 869 | - | val := value | |
| 870 | - | i.cfg.PonytailEnabled = &val | |
| 871 | - | if err := i.cfg.RefreshPrompt(); err != nil { | |
| 872 | - | errMsg := "settings: ponytail prompt refresh: " + err.Error() | |
| 873 | - | if rollbackErr := store.SetPonytailEnabled(previous); rollbackErr != nil { | |
| 874 | - | // The first write succeeded, so keep the in-memory setting at | |
| 875 | - | // the value that remains durable instead of claiming that the | |
| 876 | - | // rollback took effect. | |
| 877 | - | i.resetSettingsToggle(key, value) | |
| 878 | - | errMsg += "; rollback persistence: " + rollbackErr.Error() | |
| 879 | - | if reconcileErr := i.cfg.RefreshPrompt(); reconcileErr != nil { | |
| 880 | - | errMsg += "; durable-state refresh: " + reconcileErr.Error() | |
| 881 | - | } | |
| 882 | - | } else { | |
| 883 | - | previousVal := previous | |
| 884 | - | i.cfg.PonytailEnabled = &previousVal | |
| 885 | - | i.resetSettingsToggle(key, previous) | |
| 886 | - | if refreshErr := i.cfg.RefreshPrompt(); refreshErr != nil { | |
| 887 | - | errMsg += "; rollback refresh: " + refreshErr.Error() | |
| 888 | - | } | |
| 889 | - | } | |
| 890 | - | i.mu.Lock() | |
| 891 | - | i.statusOK = "" | |
| 892 | - | i.statusErr = errMsg | |
| 893 | - | i.mu.Unlock() | |
| 894 | - | return | |
| 895 | - | } | |
| 896 | - | i.mu.Lock() | |
| 897 | - | i.statusOK = "ponytail coding mode " + onOff(value) | |
| 898 | - | i.statusErr = "" | |
| 899 | - | i.mu.Unlock() | |
| 900 | 828 | case "auto_subagents_enabled": | |
| 901 | 829 | if value && !i.autoSubagentsAvailable() { | |
| 902 | 830 | i.mu.Lock() | |
| @@ -158,13 +158,6 @@ func (i *Interactive) openSettingsDialog() { | |||||
| 158 | autoSubagents = false | 158 | autoSubagents = false | ||
| 159 | } | 159 | } | ||
| 160 | 160 | ||||
| 161 | - | ponytailEnabled := i.ponytailEnabled() | |||
| 162 | - | ponytailDisabled := !i.ponytailSettingsAvailable() | |||
| 163 | - | ponytailHint := "" | |||
| 164 | - | if ponytailDisabled { | |||
| 165 | - | ponytailHint = "requires persistent settings and live prompt refresh support" | |||
| 166 | - | } | |||
| 167 | - | ||||
| 168 | webSearchEnabled := i.webSearchEffectiveEnabled() | 161 | webSearchEnabled := i.webSearchEffectiveEnabled() | ||
| 169 | webSearchDisabled := !i.webSearchSettingsAvailable() | 162 | webSearchDisabled := !i.webSearchSettingsAvailable() | ||
| 170 | webSearchHint := i.webSearchUnavailableHint() | 163 | webSearchHint := i.webSearchUnavailableHint() | ||
| @@ -296,14 +289,6 @@ func (i *Interactive) openSettingsDialog() { | |||||
| 296 | disabled: autoSubagentsDisabled, | 289 | disabled: autoSubagentsDisabled, | ||
| 297 | hint: autoSubagentsHint, | 290 | hint: autoSubagentsHint, | ||
| 298 | }, | 291 | }, | ||
| 299 | - | { | |||
| 300 | - | key: "ponytail_enabled", | |||
| 301 | - | label: "ponytail coding mode", | |||
| 302 | - | desc: "apply compact coding guidance that favors small, validated, maintainable changes", | |||
| 303 | - | value: ponytailEnabled, | |||
| 304 | - | disabled: ponytailDisabled, | |||
| 305 | - | hint: ponytailHint, | |||
| 306 | - | }, | |||
| 307 | { | 292 | { | ||
| 308 | key: "web_search_enabled", | 293 | key: "web_search_enabled", | ||
| 309 | label: "web search", | 294 | label: "web search", | ||
| @@ -645,13 +630,6 @@ func (i *Interactive) resetSettingsToggle(key string, value bool) { | |||||
| 645 | } | 630 | } | ||
| 646 | } | 631 | } | ||
| 647 | } | 632 | } | ||
| 648 | - | func (i *Interactive) ponytailEnabled() bool { | |||
| 649 | - | return i.cfg.PonytailEnabled == nil || *i.cfg.PonytailEnabled | |||
| 650 | - | } | |||
| 651 | - | func (i *Interactive) ponytailSettingsAvailable() bool { | |||
| 652 | - | _, persistent := i.cfg.SettingsStore.(ponytailSettingsStore) | |||
| 653 | - | return persistent && i.cfg.RefreshPrompt != nil | |||
| 654 | - | } | |||
| 655 | func (i *Interactive) webSearchEnabled() bool { | 633 | func (i *Interactive) webSearchEnabled() bool { | ||
| 656 | return i.cfg.WebSearchEnabled == nil || *i.cfg.WebSearchEnabled | 634 | return i.cfg.WebSearchEnabled == nil || *i.cfg.WebSearchEnabled | ||
| 657 | } | 635 | } | ||
| @@ -847,56 +825,6 @@ func (i *Interactive) applySettingToggle(key string, value bool) { | |||||
| 847 | } else { | 825 | } else { | ||
| 848 | apply() | 826 | apply() | ||
| 849 | } | 827 | } | ||
| 850 | - | case "ponytail_enabled": | |||
| 851 | - | previous := i.ponytailEnabled() | |||
| 852 | - | store, available := i.cfg.SettingsStore.(ponytailSettingsStore) | |||
| 853 | - | if !available || i.cfg.RefreshPrompt == nil { | |||
| 854 | - | i.resetSettingsToggle(key, previous) | |||
| 855 | - | i.mu.Lock() | |||
| 856 | - | i.statusOK = "" | |||
| 857 | - | i.statusErr = "ponytail coding mode unavailable: persistent settings and live prompt refresh are required" | |||
| 858 | - | i.mu.Unlock() | |||
| 859 | - | return | |||
| 860 | - | } | |||
| 861 | - | if err := store.SetPonytailEnabled(value); err != nil { | |||
| 862 | - | i.resetSettingsToggle(key, previous) | |||
| 863 | - | i.mu.Lock() | |||
| 864 | - | i.statusOK = "" | |||
| 865 | - | i.statusErr = "settings: " + err.Error() | |||
| 866 | - | i.mu.Unlock() | |||
| 867 | - | return | |||
| 868 | - | } | |||
| 869 | - | val := value | |||
| 870 | - | i.cfg.PonytailEnabled = &val | |||
| 871 | - | if err := i.cfg.RefreshPrompt(); err != nil { | |||
| 872 | - | errMsg := "settings: ponytail prompt refresh: " + err.Error() | |||
| 873 | - | if rollbackErr := store.SetPonytailEnabled(previous); rollbackErr != nil { | |||
| 874 | - | // The first write succeeded, so keep the in-memory setting at | |||
| 875 | - | // the value that remains durable instead of claiming that the | |||
| 876 | - | // rollback took effect. | |||
| 877 | - | i.resetSettingsToggle(key, value) | |||
| 878 | - | errMsg += "; rollback persistence: " + rollbackErr.Error() | |||
| 879 | - | if reconcileErr := i.cfg.RefreshPrompt(); reconcileErr != nil { | |||
| 880 | - | errMsg += "; durable-state refresh: " + reconcileErr.Error() | |||
| 881 | - | } | |||
| 882 | - | } else { | |||
| 883 | - | previousVal := previous | |||
| 884 | - | i.cfg.PonytailEnabled = &previousVal | |||
| 885 | - | i.resetSettingsToggle(key, previous) | |||
| 886 | - | if refreshErr := i.cfg.RefreshPrompt(); refreshErr != nil { | |||
| 887 | - | errMsg += "; rollback refresh: " + refreshErr.Error() | |||
| 888 | - | } | |||
| 889 | - | } | |||
| 890 | - | i.mu.Lock() | |||
| 891 | - | i.statusOK = "" | |||
| 892 | - | i.statusErr = errMsg | |||
| 893 | - | i.mu.Unlock() | |||
| 894 | - | return | |||
| 895 | - | } | |||
| 896 | - | i.mu.Lock() | |||
| 897 | - | i.statusOK = "ponytail coding mode " + onOff(value) | |||
| 898 | - | i.statusErr = "" | |||
| 899 | - | i.mu.Unlock() | |||
| 900 | case "auto_subagents_enabled": | 828 | case "auto_subagents_enabled": | ||
| 901 | if value && !i.autoSubagentsAvailable() { | 829 | if value && !i.autoSubagentsAvailable() { | ||
| 902 | i.mu.Lock() | 830 | i.mu.Lock() | ||
D packages/agent/modes/ponytail_setting_test.go +0 -230
| @@ -1,230 +0,0 @@ | |||
| 1 | - | package modes | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "errors" | |
| 5 | - | "strings" | |
| 6 | - | "testing" | |
| 7 | - | ||
| 8 | - | "github.com/bnema/zut/packages/core" | |
| 9 | - | ) | |
| 10 | - | ||
| 11 | - | type recordingPonytailSettingsStore struct { | |
| 12 | - | SettingsStore | |
| 13 | - | values []bool | |
| 14 | - | calls int | |
| 15 | - | err error | |
| 16 | - | failures map[int]error | |
| 17 | - | } | |
| 18 | - | ||
| 19 | - | func (s *recordingPonytailSettingsStore) SetPonytailEnabled(enabled bool) error { | |
| 20 | - | s.calls++ | |
| 21 | - | if s.err != nil { | |
| 22 | - | return s.err | |
| 23 | - | } | |
| 24 | - | if err := s.failures[s.calls]; err != nil { | |
| 25 | - | return err | |
| 26 | - | } | |
| 27 | - | s.values = append(s.values, enabled) | |
| 28 | - | return nil | |
| 29 | - | } | |
| 30 | - | ||
| 31 | - | func TestSettingsDialogOffersPonytailEnabledByDefault(t *testing.T) { | |
| 32 | - | store := &recordingPonytailSettingsStore{} | |
| 33 | - | interactive := NewInteractive(InteractiveConfig{ | |
| 34 | - | SettingsStore: store, | |
| 35 | - | RefreshPrompt: func() error { return nil }, | |
| 36 | - | }) | |
| 37 | - | interactive.openSettingsDialog() | |
| 38 | - | ||
| 39 | - | item := findSettingsItem(interactive.settingsDialog.items, "ponytail_enabled") | |
| 40 | - | if item == nil { | |
| 41 | - | t.Fatal("settings dialog is missing Ponytail mode") | |
| 42 | - | } | |
| 43 | - | if !item.value { | |
| 44 | - | t.Fatal("Ponytail mode should be enabled when the config value is absent") | |
| 45 | - | } | |
| 46 | - | if item.disabled { | |
| 47 | - | t.Fatal("Ponytail mode should be available with persistence and refresh capabilities") | |
| 48 | - | } | |
| 49 | - | } | |
| 50 | - | ||
| 51 | - | func TestSettingsDialogDisablesPonytailWithoutCapabilities(t *testing.T) { | |
| 52 | - | cases := []struct { | |
| 53 | - | name string | |
| 54 | - | cfg InteractiveConfig | |
| 55 | - | }{ | |
| 56 | - | {name: "neither capability", cfg: InteractiveConfig{}}, | |
| 57 | - | {name: "persistence only", cfg: InteractiveConfig{SettingsStore: &recordingPonytailSettingsStore{}}}, | |
| 58 | - | {name: "refresh only", cfg: InteractiveConfig{RefreshPrompt: func() error { return nil }}}, | |
| 59 | - | } | |
| 60 | - | for _, tc := range cases { | |
| 61 | - | t.Run(tc.name, func(t *testing.T) { | |
| 62 | - | interactive := NewInteractive(tc.cfg) | |
| 63 | - | interactive.openSettingsDialog() | |
| 64 | - | ||
| 65 | - | item := findSettingsItem(interactive.settingsDialog.items, "ponytail_enabled") | |
| 66 | - | if item == nil { | |
| 67 | - | t.Fatal("settings dialog is missing Ponytail mode") | |
| 68 | - | } | |
| 69 | - | if !item.disabled { | |
| 70 | - | t.Fatal("Ponytail mode should be disabled without both capabilities") | |
| 71 | - | } | |
| 72 | - | if item.hint == "" { | |
| 73 | - | t.Fatal("disabled Ponytail mode should explain why it is unavailable") | |
| 74 | - | } | |
| 75 | - | }) | |
| 76 | - | } | |
| 77 | - | } | |
| 78 | - | ||
| 79 | - | func TestApplyPonytailToggleRefreshesAndPersists(t *testing.T) { | |
| 80 | - | store := &recordingPonytailSettingsStore{} | |
| 81 | - | refreshCalls := 0 | |
| 82 | - | interactive := NewInteractive(InteractiveConfig{ | |
| 83 | - | SettingsStore: store, | |
| 84 | - | RefreshPrompt: func() error { | |
| 85 | - | refreshCalls++ | |
| 86 | - | return nil | |
| 87 | - | }, | |
| 88 | - | }) | |
| 89 | - | interactive.rend = nil | |
| 90 | - | ||
| 91 | - | interactive.applySettingToggle("ponytail_enabled", false) | |
| 92 | - | if interactive.ponytailEnabled() { | |
| 93 | - | t.Fatal("Ponytail mode remained enabled after disabling it") | |
| 94 | - | } | |
| 95 | - | interactive.applySettingToggle("ponytail_enabled", true) | |
| 96 | - | if !interactive.ponytailEnabled() { | |
| 97 | - | t.Fatal("Ponytail mode remained disabled after enabling it") | |
| 98 | - | } | |
| 99 | - | if refreshCalls != 2 { | |
| 100 | - | t.Fatalf("refresh calls = %d, want 2", refreshCalls) | |
| 101 | - | } | |
| 102 | - | if len(store.values) != 2 || store.values[0] || !store.values[1] { | |
| 103 | - | t.Fatalf("persisted values = %v, want [false true]", store.values) | |
| 104 | - | } | |
| 105 | - | if interactive.statusErr != "" { | |
| 106 | - | t.Fatalf("unexpected status error: %q", interactive.statusErr) | |
| 107 | - | } | |
| 108 | - | } | |
| 109 | - | ||
| 110 | - | func TestApplyPonytailToggleRollsBackWhenRefreshFails(t *testing.T) { | |
| 111 | - | store := &recordingPonytailSettingsStore{} | |
| 112 | - | refreshCalls := 0 | |
| 113 | - | interactive := NewInteractive(InteractiveConfig{ | |
| 114 | - | SettingsStore: store, | |
| 115 | - | RefreshPrompt: func() error { | |
| 116 | - | refreshCalls++ | |
| 117 | - | if refreshCalls == 1 { | |
| 118 | - | return errors.New("refresh failed") | |
| 119 | - | } | |
| 120 | - | return nil | |
| 121 | - | }, | |
| 122 | - | }) | |
| 123 | - | interactive.rend = nil | |
| 124 | - | ||
| 125 | - | interactive.applySettingToggle("ponytail_enabled", false) | |
| 126 | - | if !interactive.ponytailEnabled() { | |
| 127 | - | t.Fatal("Ponytail mode stayed disabled after refresh rollback") | |
| 128 | - | } | |
| 129 | - | if refreshCalls != 2 { | |
| 130 | - | t.Fatalf("refresh calls = %d, want initial refresh and rollback refresh", refreshCalls) | |
| 131 | - | } | |
| 132 | - | if len(store.values) != 2 || store.values[0] || !store.values[1] { | |
| 133 | - | t.Fatalf("persisted rollback values = %v, want [false true]", store.values) | |
| 134 | - | } | |
| 135 | - | if !strings.Contains(interactive.statusErr, "refresh failed") { | |
| 136 | - | t.Fatalf("status error = %q, want refresh failure", interactive.statusErr) | |
| 137 | - | } | |
| 138 | - | } | |
| 139 | - | ||
| 140 | - | func TestApplyPonytailToggleKeepsDurableStateWhenRollbackPersistenceFails(t *testing.T) { | |
| 141 | - | store := &recordingPonytailSettingsStore{ | |
| 142 | - | failures: map[int]error{2: errors.New("rollback disk full")}, | |
| 143 | - | } | |
| 144 | - | refreshCalls := 0 | |
| 145 | - | interactive := NewInteractive(InteractiveConfig{ | |
| 146 | - | SettingsStore: store, | |
| 147 | - | RefreshPrompt: func() error { | |
| 148 | - | refreshCalls++ | |
| 149 | - | if refreshCalls == 1 { | |
| 150 | - | return errors.New("refresh failed") | |
| 151 | - | } | |
| 152 | - | return nil | |
| 153 | - | }, | |
| 154 | - | }) | |
| 155 | - | interactive.rend = nil | |
| 156 | - | ||
| 157 | - | interactive.applySettingToggle("ponytail_enabled", false) | |
| 158 | - | if interactive.ponytailEnabled() { | |
| 159 | - | t.Fatal("in-memory setting reverted despite rollback persistence failure") | |
| 160 | - | } | |
| 161 | - | if len(store.values) != 1 || store.values[0] { | |
| 162 | - | t.Fatalf("durable writes = %v, want only the successful disable", store.values) | |
| 163 | - | } | |
| 164 | - | if refreshCalls != 2 { | |
| 165 | - | t.Fatalf("refresh calls = %d, want failed refresh plus durable-state reconciliation", refreshCalls) | |
| 166 | - | } | |
| 167 | - | if !strings.Contains(interactive.statusErr, "rollback disk full") { | |
| 168 | - | t.Fatalf("status error = %q, want rollback failure", interactive.statusErr) | |
| 169 | - | } | |
| 170 | - | } | |
| 171 | - | ||
| 172 | - | func TestApplyPonytailToggleKeepsStateWhenPersistenceFails(t *testing.T) { | |
| 173 | - | store := &recordingPonytailSettingsStore{err: errors.New("disk full")} | |
| 174 | - | refreshCalls := 0 | |
| 175 | - | interactive := NewInteractive(InteractiveConfig{ | |
| 176 | - | SettingsStore: store, | |
| 177 | - | RefreshPrompt: func() error { | |
| 178 | - | refreshCalls++ | |
| 179 | - | return nil | |
| 180 | - | }, | |
| 181 | - | }) | |
| 182 | - | interactive.rend = nil | |
| 183 | - | ||
| 184 | - | interactive.applySettingToggle("ponytail_enabled", false) | |
| 185 | - | if !interactive.ponytailEnabled() { | |
| 186 | - | t.Fatal("Ponytail mode changed despite persistence failure") | |
| 187 | - | } | |
| 188 | - | if refreshCalls != 0 { | |
| 189 | - | t.Fatalf("refresh calls = %d, want none", refreshCalls) | |
| 190 | - | } | |
| 191 | - | if !strings.Contains(interactive.statusErr, "disk full") { | |
| 192 | - | t.Fatalf("status error = %q, want persistence failure", interactive.statusErr) | |
| 193 | - | } | |
| 194 | - | } | |
| 195 | - | ||
| 196 | - | func TestApplyPonytailToggleUpdatesLiveAgentPrompt(t *testing.T) { | |
| 197 | - | const ponytailBlock = "[ponytail test block]" | |
| 198 | - | store := &recordingPonytailSettingsStore{} | |
| 199 | - | ag := core.NewAgent(nil, "model", "base\n\n"+ponytailBlock, nil) | |
| 200 | - | var interactive *Interactive | |
| 201 | - | refreshCalls := 0 | |
| 202 | - | interactive = NewInteractive(InteractiveConfig{ | |
| 203 | - | Agent: ag, | |
| 204 | - | SettingsStore: store, | |
| 205 | - | RefreshPrompt: func() error { | |
| 206 | - | refreshCalls++ | |
| 207 | - | system := "base" | |
| 208 | - | if interactive.ponytailEnabled() { | |
| 209 | - | system += "\n\n" + ponytailBlock | |
| 210 | - | } | |
| 211 | - | ag.SetSystemPrompt(system) | |
| 212 | - | return nil | |
| 213 | - | }, | |
| 214 | - | }) | |
| 215 | - | interactive.rend = nil | |
| 216 | - | ||
| 217 | - | interactive.applySettingToggle("ponytail_enabled", false) | |
| 218 | - | system, _ := ag.PromptConfig() | |
| 219 | - | if strings.Contains(system, ponytailBlock) { | |
| 220 | - | t.Fatalf("live prompt still contains disabled Ponytail block: %q", system) | |
| 221 | - | } | |
| 222 | - | interactive.applySettingToggle("ponytail_enabled", true) | |
| 223 | - | system, _ = ag.PromptConfig() | |
| 224 | - | if strings.Count(system, ponytailBlock) != 1 { | |
| 225 | - | t.Fatalf("live prompt block count = %d, want 1: %q", strings.Count(system, ponytailBlock), system) | |
| 226 | - | } | |
| 227 | - | if refreshCalls != 2 { | |
| 228 | - | t.Fatalf("refresh calls = %d, want 2", refreshCalls) | |
| 229 | - | } | |
| 230 | - | } | |
| @@ -1,230 +0,0 @@ | |||||
| 1 | - | package modes | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "errors" | |||
| 5 | - | "strings" | |||
| 6 | - | "testing" | |||
| 7 | - | ||||
| 8 | - | "github.com/bnema/zut/packages/core" | |||
| 9 | - | ) | |||
| 10 | - | ||||
| 11 | - | type recordingPonytailSettingsStore struct { | |||
| 12 | - | SettingsStore | |||
| 13 | - | values []bool | |||
| 14 | - | calls int | |||
| 15 | - | err error | |||
| 16 | - | failures map[int]error | |||
| 17 | - | } | |||
| 18 | - | ||||
| 19 | - | func (s *recordingPonytailSettingsStore) SetPonytailEnabled(enabled bool) error { | |||
| 20 | - | s.calls++ | |||
| 21 | - | if s.err != nil { | |||
| 22 | - | return s.err | |||
| 23 | - | } | |||
| 24 | - | if err := s.failures[s.calls]; err != nil { | |||
| 25 | - | return err | |||
| 26 | - | } | |||
| 27 | - | s.values = append(s.values, enabled) | |||
| 28 | - | return nil | |||
| 29 | - | } | |||
| 30 | - | ||||
| 31 | - | func TestSettingsDialogOffersPonytailEnabledByDefault(t *testing.T) { | |||
| 32 | - | store := &recordingPonytailSettingsStore{} | |||
| 33 | - | interactive := NewInteractive(InteractiveConfig{ | |||
| 34 | - | SettingsStore: store, | |||
| 35 | - | RefreshPrompt: func() error { return nil }, | |||
| 36 | - | }) | |||
| 37 | - | interactive.openSettingsDialog() | |||
| 38 | - | ||||
| 39 | - | item := findSettingsItem(interactive.settingsDialog.items, "ponytail_enabled") | |||
| 40 | - | if item == nil { | |||
| 41 | - | t.Fatal("settings dialog is missing Ponytail mode") | |||
| 42 | - | } | |||
| 43 | - | if !item.value { | |||
| 44 | - | t.Fatal("Ponytail mode should be enabled when the config value is absent") | |||
| 45 | - | } | |||
| 46 | - | if item.disabled { | |||
| 47 | - | t.Fatal("Ponytail mode should be available with persistence and refresh capabilities") | |||
| 48 | - | } | |||
| 49 | - | } | |||
| 50 | - | ||||
| 51 | - | func TestSettingsDialogDisablesPonytailWithoutCapabilities(t *testing.T) { | |||
| 52 | - | cases := []struct { | |||
| 53 | - | name string | |||
| 54 | - | cfg InteractiveConfig | |||
| 55 | - | }{ | |||
| 56 | - | {name: "neither capability", cfg: InteractiveConfig{}}, | |||
| 57 | - | {name: "persistence only", cfg: InteractiveConfig{SettingsStore: &recordingPonytailSettingsStore{}}}, | |||
| 58 | - | {name: "refresh only", cfg: InteractiveConfig{RefreshPrompt: func() error { return nil }}}, | |||
| 59 | - | } | |||
| 60 | - | for _, tc := range cases { | |||
| 61 | - | t.Run(tc.name, func(t *testing.T) { | |||
| 62 | - | interactive := NewInteractive(tc.cfg) | |||
| 63 | - | interactive.openSettingsDialog() | |||
| 64 | - | ||||
| 65 | - | item := findSettingsItem(interactive.settingsDialog.items, "ponytail_enabled") | |||
| 66 | - | if item == nil { | |||
| 67 | - | t.Fatal("settings dialog is missing Ponytail mode") | |||
| 68 | - | } | |||
| 69 | - | if !item.disabled { | |||
| 70 | - | t.Fatal("Ponytail mode should be disabled without both capabilities") | |||
| 71 | - | } | |||
| 72 | - | if item.hint == "" { | |||
| 73 | - | t.Fatal("disabled Ponytail mode should explain why it is unavailable") | |||
| 74 | - | } | |||
| 75 | - | }) | |||
| 76 | - | } | |||
| 77 | - | } | |||
| 78 | - | ||||
| 79 | - | func TestApplyPonytailToggleRefreshesAndPersists(t *testing.T) { | |||
| 80 | - | store := &recordingPonytailSettingsStore{} | |||
| 81 | - | refreshCalls := 0 | |||
| 82 | - | interactive := NewInteractive(InteractiveConfig{ | |||
| 83 | - | SettingsStore: store, | |||
| 84 | - | RefreshPrompt: func() error { | |||
| 85 | - | refreshCalls++ | |||
| 86 | - | return nil | |||
| 87 | - | }, | |||
| 88 | - | }) | |||
| 89 | - | interactive.rend = nil | |||
| 90 | - | ||||
| 91 | - | interactive.applySettingToggle("ponytail_enabled", false) | |||
| 92 | - | if interactive.ponytailEnabled() { | |||
| 93 | - | t.Fatal("Ponytail mode remained enabled after disabling it") | |||
| 94 | - | } | |||
| 95 | - | interactive.applySettingToggle("ponytail_enabled", true) | |||
| 96 | - | if !interactive.ponytailEnabled() { | |||
| 97 | - | t.Fatal("Ponytail mode remained disabled after enabling it") | |||
| 98 | - | } | |||
| 99 | - | if refreshCalls != 2 { | |||
| 100 | - | t.Fatalf("refresh calls = %d, want 2", refreshCalls) | |||
| 101 | - | } | |||
| 102 | - | if len(store.values) != 2 || store.values[0] || !store.values[1] { | |||
| 103 | - | t.Fatalf("persisted values = %v, want [false true]", store.values) | |||
| 104 | - | } | |||
| 105 | - | if interactive.statusErr != "" { | |||
| 106 | - | t.Fatalf("unexpected status error: %q", interactive.statusErr) | |||
| 107 | - | } | |||
| 108 | - | } | |||
| 109 | - | ||||
| 110 | - | func TestApplyPonytailToggleRollsBackWhenRefreshFails(t *testing.T) { | |||
| 111 | - | store := &recordingPonytailSettingsStore{} | |||
| 112 | - | refreshCalls := 0 | |||
| 113 | - | interactive := NewInteractive(InteractiveConfig{ | |||
| 114 | - | SettingsStore: store, | |||
| 115 | - | RefreshPrompt: func() error { | |||
| 116 | - | refreshCalls++ | |||
| 117 | - | if refreshCalls == 1 { | |||
| 118 | - | return errors.New("refresh failed") | |||
| 119 | - | } | |||
| 120 | - | return nil | |||
| 121 | - | }, | |||
| 122 | - | }) | |||
| 123 | - | interactive.rend = nil | |||
| 124 | - | ||||
| 125 | - | interactive.applySettingToggle("ponytail_enabled", false) | |||
| 126 | - | if !interactive.ponytailEnabled() { | |||
| 127 | - | t.Fatal("Ponytail mode stayed disabled after refresh rollback") | |||
| 128 | - | } | |||
| 129 | - | if refreshCalls != 2 { | |||
| 130 | - | t.Fatalf("refresh calls = %d, want initial refresh and rollback refresh", refreshCalls) | |||
| 131 | - | } | |||
| 132 | - | if len(store.values) != 2 || store.values[0] || !store.values[1] { | |||
| 133 | - | t.Fatalf("persisted rollback values = %v, want [false true]", store.values) | |||
| 134 | - | } | |||
| 135 | - | if !strings.Contains(interactive.statusErr, "refresh failed") { | |||
| 136 | - | t.Fatalf("status error = %q, want refresh failure", interactive.statusErr) | |||
| 137 | - | } | |||
| 138 | - | } | |||
| 139 | - | ||||
| 140 | - | func TestApplyPonytailToggleKeepsDurableStateWhenRollbackPersistenceFails(t *testing.T) { | |||
| 141 | - | store := &recordingPonytailSettingsStore{ | |||
| 142 | - | failures: map[int]error{2: errors.New("rollback disk full")}, | |||
| 143 | - | } | |||
| 144 | - | refreshCalls := 0 | |||
| 145 | - | interactive := NewInteractive(InteractiveConfig{ | |||
| 146 | - | SettingsStore: store, | |||
| 147 | - | RefreshPrompt: func() error { | |||
| 148 | - | refreshCalls++ | |||
| 149 | - | if refreshCalls == 1 { | |||
| 150 | - | return errors.New("refresh failed") | |||
| 151 | - | } | |||
| 152 | - | return nil | |||
| 153 | - | }, | |||
| 154 | - | }) | |||
| 155 | - | interactive.rend = nil | |||
| 156 | - | ||||
| 157 | - | interactive.applySettingToggle("ponytail_enabled", false) | |||
| 158 | - | if interactive.ponytailEnabled() { | |||
| 159 | - | t.Fatal("in-memory setting reverted despite rollback persistence failure") | |||
| 160 | - | } | |||
| 161 | - | if len(store.values) != 1 || store.values[0] { | |||
| 162 | - | t.Fatalf("durable writes = %v, want only the successful disable", store.values) | |||
| 163 | - | } | |||
| 164 | - | if refreshCalls != 2 { | |||
| 165 | - | t.Fatalf("refresh calls = %d, want failed refresh plus durable-state reconciliation", refreshCalls) | |||
| 166 | - | } | |||
| 167 | - | if !strings.Contains(interactive.statusErr, "rollback disk full") { | |||
| 168 | - | t.Fatalf("status error = %q, want rollback failure", interactive.statusErr) | |||
| 169 | - | } | |||
| 170 | - | } | |||
| 171 | - | ||||
| 172 | - | func TestApplyPonytailToggleKeepsStateWhenPersistenceFails(t *testing.T) { | |||
| 173 | - | store := &recordingPonytailSettingsStore{err: errors.New("disk full")} | |||
| 174 | - | refreshCalls := 0 | |||
| 175 | - | interactive := NewInteractive(InteractiveConfig{ | |||
| 176 | - | SettingsStore: store, | |||
| 177 | - | RefreshPrompt: func() error { | |||
| 178 | - | refreshCalls++ | |||
| 179 | - | return nil | |||
| 180 | - | }, | |||
| 181 | - | }) | |||
| 182 | - | interactive.rend = nil | |||
| 183 | - | ||||
| 184 | - | interactive.applySettingToggle("ponytail_enabled", false) | |||
| 185 | - | if !interactive.ponytailEnabled() { | |||
| 186 | - | t.Fatal("Ponytail mode changed despite persistence failure") | |||
| 187 | - | } | |||
| 188 | - | if refreshCalls != 0 { | |||
| 189 | - | t.Fatalf("refresh calls = %d, want none", refreshCalls) | |||
| 190 | - | } | |||
| 191 | - | if !strings.Contains(interactive.statusErr, "disk full") { | |||
| 192 | - | t.Fatalf("status error = %q, want persistence failure", interactive.statusErr) | |||
| 193 | - | } | |||
| 194 | - | } | |||
| 195 | - | ||||
| 196 | - | func TestApplyPonytailToggleUpdatesLiveAgentPrompt(t *testing.T) { | |||
| 197 | - | const ponytailBlock = "[ponytail test block]" | |||
| 198 | - | store := &recordingPonytailSettingsStore{} | |||
| 199 | - | ag := core.NewAgent(nil, "model", "base\n\n"+ponytailBlock, nil) | |||
| 200 | - | var interactive *Interactive | |||
| 201 | - | refreshCalls := 0 | |||
| 202 | - | interactive = NewInteractive(InteractiveConfig{ | |||
| 203 | - | Agent: ag, | |||
| 204 | - | SettingsStore: store, | |||
| 205 | - | RefreshPrompt: func() error { | |||
| 206 | - | refreshCalls++ | |||
| 207 | - | system := "base" | |||
| 208 | - | if interactive.ponytailEnabled() { | |||
| 209 | - | system += "\n\n" + ponytailBlock | |||
| 210 | - | } | |||
| 211 | - | ag.SetSystemPrompt(system) | |||
| 212 | - | return nil | |||
| 213 | - | }, | |||
| 214 | - | }) | |||
| 215 | - | interactive.rend = nil | |||
| 216 | - | ||||
| 217 | - | interactive.applySettingToggle("ponytail_enabled", false) | |||
| 218 | - | system, _ := ag.PromptConfig() | |||
| 219 | - | if strings.Contains(system, ponytailBlock) { | |||
| 220 | - | t.Fatalf("live prompt still contains disabled Ponytail block: %q", system) | |||
| 221 | - | } | |||
| 222 | - | interactive.applySettingToggle("ponytail_enabled", true) | |||
| 223 | - | system, _ = ag.PromptConfig() | |||
| 224 | - | if strings.Count(system, ponytailBlock) != 1 { | |||
| 225 | - | t.Fatalf("live prompt block count = %d, want 1: %q", strings.Count(system, ponytailBlock), system) | |||
| 226 | - | } | |||
| 227 | - | if refreshCalls != 2 { | |||
| 228 | - | t.Fatalf("refresh calls = %d, want 2", refreshCalls) | |||
| 229 | - | } | |||
| 230 | - | } | |||
D packages/agent/ponytail.go +0 -15
| @@ -1,15 +0,0 @@ | |||
| 1 | - | package agent | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | _ "embed" | |
| 5 | - | "strings" | |
| 6 | - | ) | |
| 7 | - | ||
| 8 | - | //go:embed ponytail.md | |
| 9 | - | var ponytailSystemAddendum string | |
| 10 | - | ||
| 11 | - | // PonytailSystemAddendum returns the compact coding-guidance block included | |
| 12 | - | // in resolved system prompts when Ponytail mode is enabled. | |
| 13 | - | func PonytailSystemAddendum() string { | |
| 14 | - | return strings.TrimSpace(ponytailSystemAddendum) | |
| 15 | - | } | |
| @@ -1,15 +0,0 @@ | |||||
| 1 | - | package agent | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | _ "embed" | |||
| 5 | - | "strings" | |||
| 6 | - | ) | |||
| 7 | - | ||||
| 8 | - | //go:embed ponytail.md | |||
| 9 | - | var ponytailSystemAddendum string | |||
| 10 | - | ||||
| 11 | - | // PonytailSystemAddendum returns the compact coding-guidance block included | |||
| 12 | - | // in resolved system prompts when Ponytail mode is enabled. | |||
| 13 | - | func PonytailSystemAddendum() string { | |||
| 14 | - | return strings.TrimSpace(ponytailSystemAddendum) | |||
| 15 | - | } | |||
D packages/agent/ponytail.md +0 -14
| @@ -1,14 +0,0 @@ | |||
| 1 | - | # Ponytail coding mode | |
| 2 | - | ||
| 3 | - | When coding, debugging, reviewing, or changing software: | |
| 4 | - | ||
| 5 | - | - Understand the real flow and the owning boundary before editing. | |
| 6 | - | - Prefer the smallest correct implementation; follow YAGNI. | |
| 7 | - | - Reuse existing code and patterns before introducing abstractions. | |
| 8 | - | - Prefer the standard library and native platform features before dependencies. | |
| 9 | - | - Fix root causes rather than masking symptoms. | |
| 10 | - | - Keep validation, error handling, security, accessibility, data-loss protection, and runnable checks intact. | |
| 11 | - | - Avoid speculative features, boilerplate, and unrelated refactors. | |
| 12 | - | - Before finishing non-trivial work, run the narrowest useful check and report what remains uncertain. | |
| 13 | - | ||
| 14 | - | This is coding guidance, not a permission or security policy. Apply it to engineering work, not ordinary conversation or creative requests. | |
| @@ -1,14 +0,0 @@ | |||||
| 1 | - | # Ponytail coding mode | |||
| 2 | - | ||||
| 3 | - | When coding, debugging, reviewing, or changing software: | |||
| 4 | - | ||||
| 5 | - | - Understand the real flow and the owning boundary before editing. | |||
| 6 | - | - Prefer the smallest correct implementation; follow YAGNI. | |||
| 7 | - | - Reuse existing code and patterns before introducing abstractions. | |||
| 8 | - | - Prefer the standard library and native platform features before dependencies. | |||
| 9 | - | - Fix root causes rather than masking symptoms. | |||
| 10 | - | - Keep validation, error handling, security, accessibility, data-loss protection, and runnable checks intact. | |||
| 11 | - | - Avoid speculative features, boilerplate, and unrelated refactors. | |||
| 12 | - | - Before finishing non-trivial work, run the narrowest useful check and report what remains uncertain. | |||
| 13 | - | ||||
| 14 | - | This is coding guidance, not a permission or security policy. Apply it to engineering work, not ordinary conversation or creative requests. | |||
D packages/agent/ponytail_test.go +0 -199
| @@ -1,199 +0,0 @@ | |||
| 1 | - | package agent | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "os" | |
| 5 | - | "path/filepath" | |
| 6 | - | "strings" | |
| 7 | - | "testing" | |
| 8 | - | ) | |
| 9 | - | ||
| 10 | - | func TestConfigPonytailModeDefaultsToEnabled(t *testing.T) { | |
| 11 | - | cfg := Config{} | |
| 12 | - | if !cfg.PonytailModeEnabled() { | |
| 13 | - | t.Fatal("Ponytail mode should default to enabled") | |
| 14 | - | } | |
| 15 | - | ||
| 16 | - | disabled := false | |
| 17 | - | cfg.PonytailEnabled = &disabled | |
| 18 | - | if cfg.PonytailModeEnabled() { | |
| 19 | - | t.Fatal("Ponytail mode should honor an explicit false value") | |
| 20 | - | } | |
| 21 | - | ||
| 22 | - | enabled := true | |
| 23 | - | cfg.PonytailEnabled = &enabled | |
| 24 | - | if !cfg.PonytailModeEnabled() { | |
| 25 | - | t.Fatal("Ponytail mode should honor an explicit true value") | |
| 26 | - | } | |
| 27 | - | } | |
| 28 | - | ||
| 29 | - | func TestPonytailSystemAddendumIsCompact(t *testing.T) { | |
| 30 | - | addendum := PonytailSystemAddendum() | |
| 31 | - | if addendum == "" { | |
| 32 | - | t.Fatal("Ponytail system addendum is empty") | |
| 33 | - | } | |
| 34 | - | if len(addendum) > 2000 { | |
| 35 | - | t.Fatalf("Ponytail system addendum is %d bytes, want at most 2000", len(addendum)) | |
| 36 | - | } | |
| 37 | - | for _, want := range []string{"YAGNI", "standard library", "validation", "ordinary conversation"} { | |
| 38 | - | if !strings.Contains(addendum, want) { | |
| 39 | - | t.Fatalf("Ponytail system addendum missing %q:\n%s", want, addendum) | |
| 40 | - | } | |
| 41 | - | } | |
| 42 | - | } | |
| 43 | - | ||
| 44 | - | func TestResolvePonytailModeDefaultAndExplicitDisable(t *testing.T) { | |
| 45 | - | cases := []struct { | |
| 46 | - | name string | |
| 47 | - | disabled bool | |
| 48 | - | want bool | |
| 49 | - | }{ | |
| 50 | - | {name: "default enabled", want: true}, | |
| 51 | - | {name: "explicitly disabled", disabled: true, want: false}, | |
| 52 | - | } | |
| 53 | - | ||
| 54 | - | for _, tc := range cases { | |
| 55 | - | t.Run(tc.name, func(t *testing.T) { | |
| 56 | - | root := t.TempDir() | |
| 57 | - | t.Setenv("ZUT_HOME", filepath.Join(root, "zut-home")) | |
| 58 | - | t.Setenv("HOME", filepath.Join(root, "home")) | |
| 59 | - | if tc.disabled { | |
| 60 | - | disabled := false | |
| 61 | - | if err := SaveConfig(Config{PonytailEnabled: &disabled}); err != nil { | |
| 62 | - | t.Fatal(err) | |
| 63 | - | } | |
| 64 | - | } | |
| 65 | - | ||
| 66 | - | resolved, err := Resolve(Args{ | |
| 67 | - | Provider: "ollama", | |
| 68 | - | Model: "any-local-model", | |
| 69 | - | CWD: filepath.Join(root, "project"), | |
| 70 | - | NoLSP: true, | |
| 71 | - | NoSkill: true, | |
| 72 | - | NoTools: true, | |
| 73 | - | }, false) | |
| 74 | - | if err != nil { | |
| 75 | - | t.Fatalf("Resolve failed: %v", err) | |
| 76 | - | } | |
| 77 | - | got := strings.Contains(resolved.SystemPrompt, PonytailSystemAddendum()) | |
| 78 | - | if got != tc.want { | |
| 79 | - | t.Fatalf("Ponytail prompt inclusion = %v, want %v:\n%s", got, tc.want, resolved.SystemPrompt) | |
| 80 | - | } | |
| 81 | - | if count := strings.Count(resolved.SystemPrompt, PonytailSystemAddendum()); count != btoi(tc.want) { | |
| 82 | - | t.Fatalf("Ponytail prompt occurrence count = %d, want %d", count, btoi(tc.want)) | |
| 83 | - | } | |
| 84 | - | }) | |
| 85 | - | } | |
| 86 | - | } | |
| 87 | - | ||
| 88 | - | func btoi(value bool) int { | |
| 89 | - | if value { | |
| 90 | - | return 1 | |
| 91 | - | } | |
| 92 | - | return 0 | |
| 93 | - | } | |
| 94 | - | ||
| 95 | - | func TestResolvePonytailModeRetainsAddendumWithSystemFile(t *testing.T) { | |
| 96 | - | root := t.TempDir() | |
| 97 | - | zutHome := filepath.Join(root, "zut-home") | |
| 98 | - | t.Setenv("ZUT_HOME", zutHome) | |
| 99 | - | t.Setenv("HOME", filepath.Join(root, "home")) | |
| 100 | - | if err := os.MkdirAll(zutHome, 0o755); err != nil { | |
| 101 | - | t.Fatal(err) | |
| 102 | - | } | |
| 103 | - | if err := os.WriteFile(filepath.Join(zutHome, "SYSTEM.md"), []byte("custom system identity"), 0o600); err != nil { | |
| 104 | - | t.Fatal(err) | |
| 105 | - | } | |
| 106 | - | ||
| 107 | - | resolved, err := Resolve(Args{ | |
| 108 | - | Provider: "ollama", | |
| 109 | - | Model: "any-local-model", | |
| 110 | - | CWD: filepath.Join(root, "project"), | |
| 111 | - | NoLSP: true, | |
| 112 | - | NoSkill: true, | |
| 113 | - | NoTools: true, | |
| 114 | - | }, false) | |
| 115 | - | if err != nil { | |
| 116 | - | t.Fatalf("Resolve failed: %v", err) | |
| 117 | - | } | |
| 118 | - | if !strings.Contains(resolved.SystemPrompt, "custom system identity") { | |
| 119 | - | t.Fatalf("SYSTEM.md content missing:\n%s", resolved.SystemPrompt) | |
| 120 | - | } | |
| 121 | - | if !strings.Contains(resolved.SystemPrompt, PonytailSystemAddendum()) { | |
| 122 | - | t.Fatalf("SYSTEM.md replacement lost Ponytail addendum:\n%s", resolved.SystemPrompt) | |
| 123 | - | } | |
| 124 | - | } | |
| 125 | - | ||
| 126 | - | func TestResolvePonytailModePrecedesExplicitAppendPrompt(t *testing.T) { | |
| 127 | - | root := t.TempDir() | |
| 128 | - | t.Setenv("ZUT_HOME", filepath.Join(root, "zut-home")) | |
| 129 | - | t.Setenv("HOME", filepath.Join(root, "home")) | |
| 130 | - | ||
| 131 | - | const explicit = "explicit invocation guidance" | |
| 132 | - | resolved, err := Resolve(Args{ | |
| 133 | - | Provider: "ollama", | |
| 134 | - | Model: "any-local-model", | |
| 135 | - | CWD: filepath.Join(root, "project"), | |
| 136 | - | NoLSP: true, | |
| 137 | - | NoSkill: true, | |
| 138 | - | NoTools: true, | |
| 139 | - | AppendSystemPrompt: []string{explicit}, | |
| 140 | - | }, false) | |
| 141 | - | if err != nil { | |
| 142 | - | t.Fatalf("Resolve failed: %v", err) | |
| 143 | - | } | |
| 144 | - | ponytailIndex := strings.Index(resolved.SystemPrompt, PonytailSystemAddendum()) | |
| 145 | - | explicitIndex := strings.Index(resolved.SystemPrompt, explicit) | |
| 146 | - | if ponytailIndex < 0 || explicitIndex < 0 || ponytailIndex > explicitIndex { | |
| 147 | - | t.Fatalf("prompt ordering = Ponytail %d, explicit %d:\n%s", ponytailIndex, explicitIndex, resolved.SystemPrompt) | |
| 148 | - | } | |
| 149 | - | } | |
| 150 | - | ||
| 151 | - | func TestResolvePonytailModeAppliesAcrossHeadlessModesAndCustomIdentity(t *testing.T) { | |
| 152 | - | root := t.TempDir() | |
| 153 | - | t.Setenv("ZUT_HOME", filepath.Join(root, "zut-home")) | |
| 154 | - | t.Setenv("HOME", filepath.Join(root, "home")) | |
| 155 | - | ||
| 156 | - | modes := []Mode{"", ModePrint, ModeStream, ModeJSON, ModeRPC} | |
| 157 | - | for _, mode := range modes { | |
| 158 | - | resolved, err := Resolve(Args{ | |
| 159 | - | Mode: mode, | |
| 160 | - | Provider: "ollama", | |
| 161 | - | Model: "any-local-model", | |
| 162 | - | CWD: filepath.Join(root, "project"), | |
| 163 | - | NoLSP: true, | |
| 164 | - | NoSkill: true, | |
| 165 | - | NoTools: true, | |
| 166 | - | SystemPrompt: "custom identity", | |
| 167 | - | }, false) | |
| 168 | - | if err != nil { | |
| 169 | - | t.Fatalf("Resolve(%q) failed: %v", mode, err) | |
| 170 | - | } | |
| 171 | - | if !strings.Contains(resolved.SystemPrompt, "custom identity") { | |
| 172 | - | t.Fatalf("Resolve(%q) lost custom identity:\n%s", mode, resolved.SystemPrompt) | |
| 173 | - | } | |
| 174 | - | if !strings.Contains(resolved.SystemPrompt, PonytailSystemAddendum()) { | |
| 175 | - | t.Fatalf("Resolve(%q) omitted Ponytail guidance:\n%s", mode, resolved.SystemPrompt) | |
| 176 | - | } | |
| 177 | - | } | |
| 178 | - | } | |
| 179 | - | ||
| 180 | - | func TestConfigSettingsStorePersistsPonytailWithoutChangingKnownFields(t *testing.T) { | |
| 181 | - | t.Setenv("ZUT_HOME", t.TempDir()) | |
| 182 | - | if err := SaveConfig(Config{Theme: "dark", Provider: "ollama", Model: "local"}); err != nil { | |
| 183 | - | t.Fatal(err) | |
| 184 | - | } | |
| 185 | - | ||
| 186 | - | if err := (configSettingsStore{}).SetPonytailEnabled(false); err != nil { | |
| 187 | - | t.Fatal(err) | |
| 188 | - | } | |
| 189 | - | cfg, err := LoadConfig() | |
| 190 | - | if err != nil { | |
| 191 | - | t.Fatal(err) | |
| 192 | - | } | |
| 193 | - | if cfg.PonytailEnabled == nil || *cfg.PonytailEnabled { | |
| 194 | - | t.Fatal("Ponytail mode was not persisted as disabled") | |
| 195 | - | } | |
| 196 | - | if cfg.Theme != "dark" || cfg.Provider != "ollama" || cfg.Model != "local" { | |
| 197 | - | t.Fatalf("known config fields changed: %#v", cfg) | |
| 198 | - | } | |
| 199 | - | } | |
| @@ -1,199 +0,0 @@ | |||||
| 1 | - | package agent | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "os" | |||
| 5 | - | "path/filepath" | |||
| 6 | - | "strings" | |||
| 7 | - | "testing" | |||
| 8 | - | ) | |||
| 9 | - | ||||
| 10 | - | func TestConfigPonytailModeDefaultsToEnabled(t *testing.T) { | |||
| 11 | - | cfg := Config{} | |||
| 12 | - | if !cfg.PonytailModeEnabled() { | |||
| 13 | - | t.Fatal("Ponytail mode should default to enabled") | |||
| 14 | - | } | |||
| 15 | - | ||||
| 16 | - | disabled := false | |||
| 17 | - | cfg.PonytailEnabled = &disabled | |||
| 18 | - | if cfg.PonytailModeEnabled() { | |||
| 19 | - | t.Fatal("Ponytail mode should honor an explicit false value") | |||
| 20 | - | } | |||
| 21 | - | ||||
| 22 | - | enabled := true | |||
| 23 | - | cfg.PonytailEnabled = &enabled | |||
| 24 | - | if !cfg.PonytailModeEnabled() { | |||
| 25 | - | t.Fatal("Ponytail mode should honor an explicit true value") | |||
| 26 | - | } | |||
| 27 | - | } | |||
| 28 | - | ||||
| 29 | - | func TestPonytailSystemAddendumIsCompact(t *testing.T) { | |||
| 30 | - | addendum := PonytailSystemAddendum() | |||
| 31 | - | if addendum == "" { | |||
| 32 | - | t.Fatal("Ponytail system addendum is empty") | |||
| 33 | - | } | |||
| 34 | - | if len(addendum) > 2000 { | |||
| 35 | - | t.Fatalf("Ponytail system addendum is %d bytes, want at most 2000", len(addendum)) | |||
| 36 | - | } | |||
| 37 | - | for _, want := range []string{"YAGNI", "standard library", "validation", "ordinary conversation"} { | |||
| 38 | - | if !strings.Contains(addendum, want) { | |||
| 39 | - | t.Fatalf("Ponytail system addendum missing %q:\n%s", want, addendum) | |||
| 40 | - | } | |||
| 41 | - | } | |||
| 42 | - | } | |||
| 43 | - | ||||
| 44 | - | func TestResolvePonytailModeDefaultAndExplicitDisable(t *testing.T) { | |||
| 45 | - | cases := []struct { | |||
| 46 | - | name string | |||
| 47 | - | disabled bool | |||
| 48 | - | want bool | |||
| 49 | - | }{ | |||
| 50 | - | {name: "default enabled", want: true}, | |||
| 51 | - | {name: "explicitly disabled", disabled: true, want: false}, | |||
| 52 | - | } | |||
| 53 | - | ||||
| 54 | - | for _, tc := range cases { | |||
| 55 | - | t.Run(tc.name, func(t *testing.T) { | |||
| 56 | - | root := t.TempDir() | |||
| 57 | - | t.Setenv("ZUT_HOME", filepath.Join(root, "zut-home")) | |||
| 58 | - | t.Setenv("HOME", filepath.Join(root, "home")) | |||
| 59 | - | if tc.disabled { | |||
| 60 | - | disabled := false | |||
| 61 | - | if err := SaveConfig(Config{PonytailEnabled: &disabled}); err != nil { | |||
| 62 | - | t.Fatal(err) | |||
| 63 | - | } | |||
| 64 | - | } | |||
| 65 | - | ||||
| 66 | - | resolved, err := Resolve(Args{ | |||
| 67 | - | Provider: "ollama", | |||
| 68 | - | Model: "any-local-model", | |||
| 69 | - | CWD: filepath.Join(root, "project"), | |||
| 70 | - | NoLSP: true, | |||
| 71 | - | NoSkill: true, | |||
| 72 | - | NoTools: true, | |||
| 73 | - | }, false) | |||
| 74 | - | if err != nil { | |||
| 75 | - | t.Fatalf("Resolve failed: %v", err) | |||
| 76 | - | } | |||
| 77 | - | got := strings.Contains(resolved.SystemPrompt, PonytailSystemAddendum()) | |||
| 78 | - | if got != tc.want { | |||
| 79 | - | t.Fatalf("Ponytail prompt inclusion = %v, want %v:\n%s", got, tc.want, resolved.SystemPrompt) | |||
| 80 | - | } | |||
| 81 | - | if count := strings.Count(resolved.SystemPrompt, PonytailSystemAddendum()); count != btoi(tc.want) { | |||
| 82 | - | t.Fatalf("Ponytail prompt occurrence count = %d, want %d", count, btoi(tc.want)) | |||
| 83 | - | } | |||
| 84 | - | }) | |||
| 85 | - | } | |||
| 86 | - | } | |||
| 87 | - | ||||
| 88 | - | func btoi(value bool) int { | |||
| 89 | - | if value { | |||
| 90 | - | return 1 | |||
| 91 | - | } | |||
| 92 | - | return 0 | |||
| 93 | - | } | |||
| 94 | - | ||||
| 95 | - | func TestResolvePonytailModeRetainsAddendumWithSystemFile(t *testing.T) { | |||
| 96 | - | root := t.TempDir() | |||
| 97 | - | zutHome := filepath.Join(root, "zut-home") | |||
| 98 | - | t.Setenv("ZUT_HOME", zutHome) | |||
| 99 | - | t.Setenv("HOME", filepath.Join(root, "home")) | |||
| 100 | - | if err := os.MkdirAll(zutHome, 0o755); err != nil { | |||
| 101 | - | t.Fatal(err) | |||
| 102 | - | } | |||
| 103 | - | if err := os.WriteFile(filepath.Join(zutHome, "SYSTEM.md"), []byte("custom system identity"), 0o600); err != nil { | |||
| 104 | - | t.Fatal(err) | |||
| 105 | - | } | |||
| 106 | - | ||||
| 107 | - | resolved, err := Resolve(Args{ | |||
| 108 | - | Provider: "ollama", | |||
| 109 | - | Model: "any-local-model", | |||
| 110 | - | CWD: filepath.Join(root, "project"), | |||
| 111 | - | NoLSP: true, | |||
| 112 | - | NoSkill: true, | |||
| 113 | - | NoTools: true, | |||
| 114 | - | }, false) | |||
| 115 | - | if err != nil { | |||
| 116 | - | t.Fatalf("Resolve failed: %v", err) | |||
| 117 | - | } | |||
| 118 | - | if !strings.Contains(resolved.SystemPrompt, "custom system identity") { | |||
| 119 | - | t.Fatalf("SYSTEM.md content missing:\n%s", resolved.SystemPrompt) | |||
| 120 | - | } | |||
| 121 | - | if !strings.Contains(resolved.SystemPrompt, PonytailSystemAddendum()) { | |||
| 122 | - | t.Fatalf("SYSTEM.md replacement lost Ponytail addendum:\n%s", resolved.SystemPrompt) | |||
| 123 | - | } | |||
| 124 | - | } | |||
| 125 | - | ||||
| 126 | - | func TestResolvePonytailModePrecedesExplicitAppendPrompt(t *testing.T) { | |||
| 127 | - | root := t.TempDir() | |||
| 128 | - | t.Setenv("ZUT_HOME", filepath.Join(root, "zut-home")) | |||
| 129 | - | t.Setenv("HOME", filepath.Join(root, "home")) | |||
| 130 | - | ||||
| 131 | - | const explicit = "explicit invocation guidance" | |||
| 132 | - | resolved, err := Resolve(Args{ | |||
| 133 | - | Provider: "ollama", | |||
| 134 | - | Model: "any-local-model", | |||
| 135 | - | CWD: filepath.Join(root, "project"), | |||
| 136 | - | NoLSP: true, | |||
| 137 | - | NoSkill: true, | |||
| 138 | - | NoTools: true, | |||
| 139 | - | AppendSystemPrompt: []string{explicit}, | |||
| 140 | - | }, false) | |||
| 141 | - | if err != nil { | |||
| 142 | - | t.Fatalf("Resolve failed: %v", err) | |||
| 143 | - | } | |||
| 144 | - | ponytailIndex := strings.Index(resolved.SystemPrompt, PonytailSystemAddendum()) | |||
| 145 | - | explicitIndex := strings.Index(resolved.SystemPrompt, explicit) | |||
| 146 | - | if ponytailIndex < 0 || explicitIndex < 0 || ponytailIndex > explicitIndex { | |||
| 147 | - | t.Fatalf("prompt ordering = Ponytail %d, explicit %d:\n%s", ponytailIndex, explicitIndex, resolved.SystemPrompt) | |||
| 148 | - | } | |||
| 149 | - | } | |||
| 150 | - | ||||
| 151 | - | func TestResolvePonytailModeAppliesAcrossHeadlessModesAndCustomIdentity(t *testing.T) { | |||
| 152 | - | root := t.TempDir() | |||
| 153 | - | t.Setenv("ZUT_HOME", filepath.Join(root, "zut-home")) | |||
| 154 | - | t.Setenv("HOME", filepath.Join(root, "home")) | |||
| 155 | - | ||||
| 156 | - | modes := []Mode{"", ModePrint, ModeStream, ModeJSON, ModeRPC} | |||
| 157 | - | for _, mode := range modes { | |||
| 158 | - | resolved, err := Resolve(Args{ | |||
| 159 | - | Mode: mode, | |||
| 160 | - | Provider: "ollama", | |||
| 161 | - | Model: "any-local-model", | |||
| 162 | - | CWD: filepath.Join(root, "project"), | |||
| 163 | - | NoLSP: true, | |||
| 164 | - | NoSkill: true, | |||
| 165 | - | NoTools: true, | |||
| 166 | - | SystemPrompt: "custom identity", | |||
| 167 | - | }, false) | |||
| 168 | - | if err != nil { | |||
| 169 | - | t.Fatalf("Resolve(%q) failed: %v", mode, err) | |||
| 170 | - | } | |||
| 171 | - | if !strings.Contains(resolved.SystemPrompt, "custom identity") { | |||
| 172 | - | t.Fatalf("Resolve(%q) lost custom identity:\n%s", mode, resolved.SystemPrompt) | |||
| 173 | - | } | |||
| 174 | - | if !strings.Contains(resolved.SystemPrompt, PonytailSystemAddendum()) { | |||
| 175 | - | t.Fatalf("Resolve(%q) omitted Ponytail guidance:\n%s", mode, resolved.SystemPrompt) | |||
| 176 | - | } | |||
| 177 | - | } | |||
| 178 | - | } | |||
| 179 | - | ||||
| 180 | - | func TestConfigSettingsStorePersistsPonytailWithoutChangingKnownFields(t *testing.T) { | |||
| 181 | - | t.Setenv("ZUT_HOME", t.TempDir()) | |||
| 182 | - | if err := SaveConfig(Config{Theme: "dark", Provider: "ollama", Model: "local"}); err != nil { | |||
| 183 | - | t.Fatal(err) | |||
| 184 | - | } | |||
| 185 | - | ||||
| 186 | - | if err := (configSettingsStore{}).SetPonytailEnabled(false); err != nil { | |||
| 187 | - | t.Fatal(err) | |||
| 188 | - | } | |||
| 189 | - | cfg, err := LoadConfig() | |||
| 190 | - | if err != nil { | |||
| 191 | - | t.Fatal(err) | |||
| 192 | - | } | |||
| 193 | - | if cfg.PonytailEnabled == nil || *cfg.PonytailEnabled { | |||
| 194 | - | t.Fatal("Ponytail mode was not persisted as disabled") | |||
| 195 | - | } | |||
| 196 | - | if cfg.Theme != "dark" || cfg.Provider != "ollama" || cfg.Model != "local" { | |||
| 197 | - | t.Fatalf("known config fields changed: %#v", cfg) | |||
| 198 | - | } | |||
| 199 | - | } | |||
M packages/agent/settings_store.go +0 -12
| @@ -106,18 +106,6 @@ func (configSettingsStore) SetAutoSubagents(enabled bool) error { | |||
| 106 | 106 | return SaveConfig(cfg) | |
| 107 | 107 | } | |
| 108 | 108 | ||
| 109 | - | func (configSettingsStore) SetPonytailEnabled(enabled bool) error { | |
| 110 | - | cfg, err := LoadConfig() | |
| 111 | - | if err != nil { | |
| 112 | - | return fmt.Errorf("load config for Ponytail setting: %w", err) | |
| 113 | - | } | |
| 114 | - | cfg.PonytailEnabled = &enabled | |
| 115 | - | if err := SaveConfig(cfg); err != nil { | |
| 116 | - | return fmt.Errorf("save Ponytail setting: %w", err) | |
| 117 | - | } | |
| 118 | - | return nil | |
| 119 | - | } | |
| 120 | - | ||
| 121 | 109 | func (configSettingsStore) SetWebSearchEnabled(enabled bool) error { | |
| 122 | 110 | cfg, err := LoadConfig() | |
| 123 | 111 | if err != nil { | |
| @@ -106,18 +106,6 @@ func (configSettingsStore) SetAutoSubagents(enabled bool) error { | |||||
| 106 | return SaveConfig(cfg) | 106 | return SaveConfig(cfg) | ||
| 107 | } | 107 | } | ||
| 108 | 108 | ||||
| 109 | - | func (configSettingsStore) SetPonytailEnabled(enabled bool) error { | |||
| 110 | - | cfg, err := LoadConfig() | |||
| 111 | - | if err != nil { | |||
| 112 | - | return fmt.Errorf("load config for Ponytail setting: %w", err) | |||
| 113 | - | } | |||
| 114 | - | cfg.PonytailEnabled = &enabled | |||
| 115 | - | if err := SaveConfig(cfg); err != nil { | |||
| 116 | - | return fmt.Errorf("save Ponytail setting: %w", err) | |||
| 117 | - | } | |||
| 118 | - | return nil | |||
| 119 | - | } | |||
| 120 | - | ||||
| 121 | func (configSettingsStore) SetWebSearchEnabled(enabled bool) error { | 109 | func (configSettingsStore) SetWebSearchEnabled(enabled bool) error { | ||
| 122 | cfg, err := LoadConfig() | 110 | cfg, err := LoadConfig() | ||
| 123 | if err != nil { | 111 | if err != nil { | ||
M packages/agent/systemprompt.go +1 -1
| @@ -109,7 +109,7 @@ const toolSelectionGuidance = `Prefer dedicated tools over bash: glob for file d | |||
| 109 | 109 | ||
| 110 | 110 | const compactedSummaryHandoffInstruction = `When you see a "## Context Summary (compacted)" message, treat it as a handoff from earlier work. Keep its active constraints and preferences in force, continue the most recent unresolved user request without waiting for the user to type "continue", and follow a newer user request when it supersedes the summary.` | |
| 111 | 111 | ||
| 112 | - | const taskExecutionGuidance = `Treat requests such as "can you" as instructions to do the requested work within the user's intended scope; do not stop at a plan or an offer to continue. Proceed with authorized, non-destructive work and reasonable assumptions until the task is complete or genuinely blocked. Follow each progress statement with the action it describes: pair every intermediate update with tool calls or a concrete next step in the same turn. Final answers contain the result of the work or a concrete blocker, never a bare promise to continue. Respect requests for explanation, planning, or review without making unsolicited changes. Ask only when a necessary decision blocks safe, correct progress; complete independent authorized work first. Preserve tool permissions, required confirmations, and explicit approval requirements; do not infer permission for destructive or irreversible actions.` | |
| 112 | + | const taskExecutionGuidance = `Treat requests such as "can you" as instructions to do the requested work within the user's intended scope; do not stop at a plan or an offer to continue. Proceed with authorized, non-destructive work and reasonable assumptions until the task is complete or genuinely blocked. Follow each progress statement with the action it describes: pair every intermediate update with tool calls or a concrete next step in the same turn. Final answers contain the result of the work or a concrete blocker, never a bare promise to continue. Respect requests for explanation, planning, or review without making unsolicited changes. Ask only when a necessary decision blocks safe, correct progress; complete independent authorized work first. Verify changes with checks appropriate to their scope. Report which checks ran and what remains unverified. Preserve tool permissions, required confirmations, and explicit approval requirements; do not infer permission for destructive or irreversible actions.` | |
| 113 | 113 | ||
| 114 | 114 | const planGuidance = `For multi-phase or dependency-ordered work, and whenever the user asks for a plan, maintain the plan checklist instead of tracking steps in prose. Skip it for single-step or informational requests. Keep at most one step in_progress and update the plan at phase boundaries, not after every command. Group related steps instead of appending endlessly: a plan is phases you can verify, not a task log. Never mark a step completed without doing the work, and do not batch-complete steps after the fact. Prefer add, update, and remove over re-sending the whole list with set; use set only to replace the checklist and clear to remove it. After a plan call, do not restate the plan in prose because the interface already shows it. After compaction, or whenever step identity is unclear, call plan with action:"show" instead of guessing an index. A plan carried over from earlier work stays visible until it is cleared or replaced, so clear it once the work it describes is no longer in flight.` | |
| 115 | 115 | ||
| @@ -109,7 +109,7 @@ const toolSelectionGuidance = `Prefer dedicated tools over bash: glob for file d | |||||
| 109 | 109 | ||||
| 110 | const compactedSummaryHandoffInstruction = `When you see a "## Context Summary (compacted)" message, treat it as a handoff from earlier work. Keep its active constraints and preferences in force, continue the most recent unresolved user request without waiting for the user to type "continue", and follow a newer user request when it supersedes the summary.` | 110 | const compactedSummaryHandoffInstruction = `When you see a "## Context Summary (compacted)" message, treat it as a handoff from earlier work. Keep its active constraints and preferences in force, continue the most recent unresolved user request without waiting for the user to type "continue", and follow a newer user request when it supersedes the summary.` | ||
| 111 | 111 | ||||
| 112 | - | const taskExecutionGuidance = `Treat requests such as "can you" as instructions to do the requested work within the user's intended scope; do not stop at a plan or an offer to continue. Proceed with authorized, non-destructive work and reasonable assumptions until the task is complete or genuinely blocked. Follow each progress statement with the action it describes: pair every intermediate update with tool calls or a concrete next step in the same turn. Final answers contain the result of the work or a concrete blocker, never a bare promise to continue. Respect requests for explanation, planning, or review without making unsolicited changes. Ask only when a necessary decision blocks safe, correct progress; complete independent authorized work first. Preserve tool permissions, required confirmations, and explicit approval requirements; do not infer permission for destructive or irreversible actions.` | 112 | + | const taskExecutionGuidance = `Treat requests such as "can you" as instructions to do the requested work within the user's intended scope; do not stop at a plan or an offer to continue. Proceed with authorized, non-destructive work and reasonable assumptions until the task is complete or genuinely blocked. Follow each progress statement with the action it describes: pair every intermediate update with tool calls or a concrete next step in the same turn. Final answers contain the result of the work or a concrete blocker, never a bare promise to continue. Respect requests for explanation, planning, or review without making unsolicited changes. Ask only when a necessary decision blocks safe, correct progress; complete independent authorized work first. Verify changes with checks appropriate to their scope. Report which checks ran and what remains unverified. Preserve tool permissions, required confirmations, and explicit approval requirements; do not infer permission for destructive or irreversible actions.` |
| 113 | 113 | ||||
| 114 | const planGuidance = `For multi-phase or dependency-ordered work, and whenever the user asks for a plan, maintain the plan checklist instead of tracking steps in prose. Skip it for single-step or informational requests. Keep at most one step in_progress and update the plan at phase boundaries, not after every command. Group related steps instead of appending endlessly: a plan is phases you can verify, not a task log. Never mark a step completed without doing the work, and do not batch-complete steps after the fact. Prefer add, update, and remove over re-sending the whole list with set; use set only to replace the checklist and clear to remove it. After a plan call, do not restate the plan in prose because the interface already shows it. After compaction, or whenever step identity is unclear, call plan with action:"show" instead of guessing an index. A plan carried over from earlier work stays visible until it is cleared or replaced, so clear it once the work it describes is no longer in flight.` | 114 | const planGuidance = `For multi-phase or dependency-ordered work, and whenever the user asks for a plan, maintain the plan checklist instead of tracking steps in prose. Skip it for single-step or informational requests. Keep at most one step in_progress and update the plan at phase boundaries, not after every command. Group related steps instead of appending endlessly: a plan is phases you can verify, not a task log. Never mark a step completed without doing the work, and do not batch-complete steps after the fact. Prefer add, update, and remove over re-sending the whole list with set; use set only to replace the checklist and clear to remove it. After a plan call, do not restate the plan in prose because the interface already shows it. After compaction, or whenever step identity is unclear, call plan with action:"show" instead of guessing an index. A plan carried over from earlier work stays visible until it is cleared or replaced, so clear it once the work it describes is no longer in flight.` | ||
| 115 | 115 | ||||
M packages/agent/systemprompt_test.go +2 -0
| @@ -113,6 +113,8 @@ func TestBuildSystemPromptAlwaysIncludesTaskAndSkillGuidance(t *testing.T) { | |||
| 113 | 113 | "Final answers contain the result of the work or a concrete blocker", | |
| 114 | 114 | "Respect requests for explanation, planning, or review without making unsolicited changes", | |
| 115 | 115 | "Ask only when a necessary decision blocks safe, correct progress", | |
| 116 | + | "Verify changes with checks appropriate to their scope.", | |
| 117 | + | "Report which checks ran and what remains unverified.", | |
| 116 | 118 | "Preserve tool permissions, required confirmations, and explicit approval requirements", | |
| 117 | 119 | "Explicit user instructions take precedence over skill guidelines", | |
| 118 | 120 | "Skills do not grant permissions or override system or developer constraints", | |
| @@ -113,6 +113,8 @@ func TestBuildSystemPromptAlwaysIncludesTaskAndSkillGuidance(t *testing.T) { | |||||
| 113 | "Final answers contain the result of the work or a concrete blocker", | 113 | "Final answers contain the result of the work or a concrete blocker", | ||
| 114 | "Respect requests for explanation, planning, or review without making unsolicited changes", | 114 | "Respect requests for explanation, planning, or review without making unsolicited changes", | ||
| 115 | "Ask only when a necessary decision blocks safe, correct progress", | 115 | "Ask only when a necessary decision blocks safe, correct progress", | ||
| 116 | + | "Verify changes with checks appropriate to their scope.", | |||
| 117 | + | "Report which checks ran and what remains unverified.", | |||
| 116 | "Preserve tool permissions, required confirmations, and explicit approval requirements", | 118 | "Preserve tool permissions, required confirmations, and explicit approval requirements", | ||
| 117 | "Explicit user instructions take precedence over skill guidelines", | 119 | "Explicit user instructions take precedence over skill guidelines", | ||
| 118 | "Skills do not grant permissions or override system or developer constraints", | 120 | "Skills do not grant permissions or override system or developer constraints", | ||