detached f27a376

refactor(agent): remove Ponytail mode and retain verification guidance

commit
f27a376fa314d52c61e93e48bc48254f2429bf32
author
brice · · 2026-10-06 08:05 UTC
parent
5c62c33
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
15 files · +22 -576
M README.md +4 -5
@@ -158,13 +158,13 @@ $ZUT_HOME/
158158 └── logs/ # app log files
159159 ```
160160
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.
162162
163163 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.
164164
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.
166166
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.
168168
169169 ### HTTP proxy
170170
@@ -354,7 +354,7 @@ Slash command names are case-insensitive in the TUI and messaging backends; argu
354354 | `/unjail` | Allow tools to touch paths outside again. |
355355 | `/reload-ext` | Hot-reload all extensions (re-read manifests, respawn subprocesses, rebuild tool registry). |
356356 | `/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. |
358358 | `/clear` | Clear the chat transcript. |
359359 | `/exit` | Exit zut. |
360360
@@ -470,7 +470,6 @@ Opens a dialog with every persistent setting. `up`/`down` to navigate, `enter` o
470470 - **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.
471471 - **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`.
472472 - **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.
474473 - **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.
475474 - **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`.
476475 - **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 files158 └── 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 proxy169 ### 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
2525
2626 `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.
2727
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.
2929
3030 ## Auth
3131
@@ -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 ## Auth30 ## 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
115115 }
116116 ```
117117
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 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.
119119
120120 ## Bundled skills
121121
@@ -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 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 skills120 ## Bundled skills
121 121
M packages/agent/build.go +0 -5
@@ -962,11 +962,6 @@ func Resolve(args Args, requireCred bool) (Resolved, error) {
962962 contextFiles = loadAgentsContext(args.CWD, ZutHome())
963963 }
964964 append_ := []string(nil)
965- if cfg.PonytailModeEnabled() {
966- if ponytailAddendum := PonytailSystemAddendum(); ponytailAddendum != "" {
967- append_ = append(append_, ponytailAddendum)
968- }
969- }
970965 append_ = append(append_, args.AppendSystemPrompt...)
971966 if agentsAddendum := formatAgentsContext(contextFiles); agentsAddendum != "" {
972967 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 +0 -1
@@ -2715,7 +2715,6 @@ func runInteractive(ctx context.Context, args Args, version string) (runErr erro
27152715 TerminalAlertsEnabled: initialCfg.TerminalAlertsEnabled,
27162716 TerminalTitleEnabled: initialCfg.TerminalTitleEnabled,
27172717 AutoSubagentsEnabled: initialCfg.AutoSubagentsEnabled,
2718- PonytailEnabled: initialCfg.PonytailEnabled,
27192718 WebSearchEnabled: initialCfg.WebSearchEnabled,
27202719 WebSearchToolAllowed: &webSearchToolAllowedForInvocation,
27212720 WebSearchInvocationOverride: webSearchInvocationOverride,
@@ -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,
M packages/agent/config.go +0 -10
@@ -102,10 +102,6 @@ type Config struct {
102102 // nil/missing means disabled. Toggle from /settings.
103103 AutoSubagentsEnabled *bool `json:"auto_subagents_enabled,omitempty"`
104104
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-
109105 // WebSearchEnabled controls the built-in web_search capability for
110106 // normal CLI sessions. nil/missing means enabled so existing config
111107 // files retain the default-on behavior.
@@ -274,12 +270,6 @@ func (c Config) LSPEnabledFor(subagent bool) bool {
274270 return value == nil || *value
275271 }
276272
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-
283273 // WebSearchEnabledForCLI reports the default-on web-search preference
284274 // without requiring a generated config entry.
285275 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 for105 // WebSearchEnabled controls the built-in web_search capability for
110 // normal CLI sessions. nil/missing means enabled so existing config106 // 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 || *value270 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 preference273 // 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 -10
@@ -69,10 +69,6 @@ type InteractiveConfig struct {
6969 // re-reading config.json on every open.
7070 AutoSubagentsEnabled *bool
7171
72- // PonytailEnabled mirrors the persisted default-on coding-guidance
73- // setting at startup. nil/missing means enabled.
74- PonytailEnabled *bool
75-
7672 // WebSearchEnabled mirrors the persisted default-on web-search setting.
7773 // nil/missing means enabled for normal interactive sessions.
7874 WebSearchEnabled *bool
@@ -586,10 +582,6 @@ type fastModeSettingsStore interface {
586582 SetFastMode(enabled bool) error
587583 }
588584
589-type ponytailSettingsStore interface {
590- SetPonytailEnabled(enabled bool) error
591-}
592-
593585 type webSearchSettingsStore interface {
594586 SetWebSearchEnabled(enabled bool) error
595587 }
@@ -1994,8 +1986,7 @@ const modalBackdropDimPercent = 50
19941986 // hold i.mu.
19951987
19961988 // 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.
1989+// instruction defining recovery completion criteria.
19991990 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.`
20001991
20011992 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 *bool70 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 *bool74 WebSearchEnabled *bool
@@ -586,10 +582,6 @@ type fastModeSettingsStore interface {
586 SetFastMode(enabled bool) error582 SetFastMode(enabled bool) error
587 }583 }
588 584
589-type ponytailSettingsStore interface {
590- SetPonytailEnabled(enabled bool) error
591-}
592-
593 type webSearchSettingsStore interface {585 type webSearchSettingsStore interface {
594 SetWebSearchEnabled(enabled bool) error586 SetWebSearchEnabled(enabled bool) error
595 }587 }
@@ -1994,8 +1986,7 @@ const modalBackdropDimPercent = 50
1994 // hold i.mu.1986 // hold i.mu.
1995 1987
1996 // autoCompactContinuationPrompt is an interactive-only compact-handoff1988 // autoCompactContinuationPrompt is an interactive-only compact-handoff
1997-// instruction. Its completion criteria must stay separate from Ponytail so1989+// 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.`1990 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 1991
2001 type compactHandoffResume uint81992 type compactHandoffResume uint8
M packages/agent/modes/interactive_settings.go +0 -72
@@ -158,13 +158,6 @@ func (i *Interactive) openSettingsDialog() {
158158 autoSubagents = false
159159 }
160160
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-
168161 webSearchEnabled := i.webSearchEffectiveEnabled()
169162 webSearchDisabled := !i.webSearchSettingsAvailable()
170163 webSearchHint := i.webSearchUnavailableHint()
@@ -296,14 +289,6 @@ func (i *Interactive) openSettingsDialog() {
296289 disabled: autoSubagentsDisabled,
297290 hint: autoSubagentsHint,
298291 },
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- },
307292 {
308293 key: "web_search_enabled",
309294 label: "web search",
@@ -645,13 +630,6 @@ func (i *Interactive) resetSettingsToggle(key string, value bool) {
645630 }
646631 }
647632 }
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-}
655633 func (i *Interactive) webSearchEnabled() bool {
656634 return i.cfg.WebSearchEnabled == nil || *i.cfg.WebSearchEnabled
657635 }
@@ -847,56 +825,6 @@ func (i *Interactive) applySettingToggle(key string, value bool) {
847825 } else {
848826 apply()
849827 }
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()
900828 case "auto_subagents_enabled":
901829 if value && !i.autoSubagentsAvailable() {
902830 i.mu.Lock()
@@ -158,13 +158,6 @@ func (i *Interactive) openSettingsDialog() {
158 autoSubagents = false158 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.WebSearchEnabled634 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 {
106106 return SaveConfig(cfg)
107107 }
108108
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-
121109 func (configSettingsStore) SetWebSearchEnabled(enabled bool) error {
122110 cfg, err := LoadConfig()
123111 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
109109
110110 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.`
111111
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.`
113113
114114 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.`
115115
@@ -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 +14 -0
@@ -6,6 +6,20 @@ import (
66 "time"
77 )
88
9+func TestBuildSystemPromptAlwaysIncludesVerificationGuidance(t *testing.T) {
10+ for _, custom := range []string{"", "Custom identity"} {
11+ prompt := BuildSystemPrompt(SystemPromptOpts{Custom: custom, Append: []string{"Project context"}})
12+ for _, want := range []string{
13+ "Verify changes with checks appropriate to their scope.",
14+ "Report which checks ran and what remains unverified.",
15+ } {
16+ if count := strings.Count(prompt, want); count != 1 {
17+ t.Fatalf("verification guidance %q count = %d, want 1", want, count)
18+ }
19+ }
20+ }
21+}
22+
923 func TestBuildSystemPromptAlwaysPrefersDedicatedTools(t *testing.T) {
1024 for _, custom := range []string{"", "Custom identity"} {
1125 prompt := BuildSystemPrompt(SystemPromptOpts{Custom: custom})
@@ -6,6 +6,20 @@ import (
6 "time"6 "time"
7 )7 )
8 8
9+func TestBuildSystemPromptAlwaysIncludesVerificationGuidance(t *testing.T) {
10+ for _, custom := range []string{"", "Custom identity"} {
11+ prompt := BuildSystemPrompt(SystemPromptOpts{Custom: custom, Append: []string{"Project context"}})
12+ for _, want := range []string{
13+ "Verify changes with checks appropriate to their scope.",
14+ "Report which checks ran and what remains unverified.",
15+ } {
16+ if count := strings.Count(prompt, want); count != 1 {
17+ t.Fatalf("verification guidance %q count = %d, want 1", want, count)
18+ }
19+ }
20+ }
21+}
22+
9 func TestBuildSystemPromptAlwaysPrefersDedicatedTools(t *testing.T) {23 func TestBuildSystemPromptAlwaysPrefersDedicatedTools(t *testing.T) {
10 for _, custom := range []string{"", "Custom identity"} {24 for _, custom := range []string{"", "Custom identity"} {
11 prompt := BuildSystemPrompt(SystemPromptOpts{Custom: custom})25 prompt := BuildSystemPrompt(SystemPromptOpts{Custom: custom})

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close