detached 2ddba2d

Fix empty system prompt override (#143)

* Fix empty system prompt override

* Complete empty system prompt overrides

* Remove duplicate system prompt test

---------

Co-authored-by: patriceckhart <mail@patriceckhart.com>
commit
2ddba2d152dbb7053a78a3bcbfe43ec23acf64ed
author
Neal Lin · · 2026-08-13 14:47 UTC
committer
GitHub · · 2026-08-13 14:47 UTC
parent
9fa3d04
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
refs
tag: v0.3.42
11 files · +129 -31
M README.md +2 -2
@@ -148,7 +148,7 @@ $ZOT_HOME/
148148 └── logs/ # app log files
149149 ```
150150
151-Drop a `SYSTEM.md` in `$ZOT_HOME` to replace the built-in identity and zot-docs guidance for every run. `--system-prompt` still wins per-invocation. Custom prompts still receive appended instructions and generated context, including `AGENTS.md`, skills, auto-swarm guidance when enabled, and the date/cwd footer. Delete the file to revert to the default.
151+Drop a `SYSTEM.md` in `$ZOT_HOME` to replace the built-in identity and zot-docs guidance for every run. `--system-prompt` still wins per-invocation. Pass an empty value (`--system-prompt ""`) to intentionally omit the built-in identity. Custom prompts still receive appended instructions and generated context, including `AGENTS.md`, skills, auto-swarm guidance when enabled, and the date/cwd footer. Delete the file to revert to the default.
152152
153153 ### HTTP proxy
154154
@@ -222,7 +222,7 @@ Print-mode stats contain `provider`, `model`, `prompt_tokens`, `reasoning_tokens
222222 | `--api-key <key>` | Override the API key. |
223223 | `--base-url <url>` | Override the provider base URL (tests, self-hosted). |
224224 | `--insecure` | Skip TLS certificate verification for the explicit `--base-url` endpoint or a `baseUrl` defined for a user model in `models.json` (self-signed local/internal inference servers). Built-in providers, auth, and model discovery keep normal TLS verification. |
225-| `--system-prompt <text>` | Replace the default system prompt for this run (also overrides `$ZOT_HOME/SYSTEM.md`). |
225+| `--system-prompt <text>` | Replace the default system prompt for this run (also overrides `$ZOT_HOME/SYSTEM.md`; pass `""` for no built-in identity). |
226226 | `--append-system-prompt <text>` | Append text to the system prompt (repeatable). |
227227 | `--reasoning off\|minimum\|low\|medium\|high\|xhigh\|max` | Set the reasoning level on supported models (default: off). `max` is a separate opt-in tier above `xhigh`. |
228228 | `--stats <path>` | With `-p`/`--print`, write generation statistics as JSON. |
@@ -148,7 +148,7 @@ $ZOT_HOME/
148 └── logs/ # app log files148 └── logs/ # app log files
149 ```149 ```
150 150
151-Drop a `SYSTEM.md` in `$ZOT_HOME` to replace the built-in identity and zot-docs guidance for every run. `--system-prompt` still wins per-invocation. Custom prompts still receive appended instructions and generated context, including `AGENTS.md`, skills, auto-swarm guidance when enabled, and the date/cwd footer. Delete the file to revert to the default.151+Drop a `SYSTEM.md` in `$ZOT_HOME` to replace the built-in identity and zot-docs guidance for every run. `--system-prompt` still wins per-invocation. Pass an empty value (`--system-prompt ""`) to intentionally omit the built-in identity. Custom prompts still receive appended instructions and generated context, including `AGENTS.md`, skills, auto-swarm guidance when enabled, and the date/cwd footer. Delete the file to revert to the default.
152 152
153 ### HTTP proxy153 ### HTTP proxy
154 154
@@ -222,7 +222,7 @@ Print-mode stats contain `provider`, `model`, `prompt_tokens`, `reasoning_tokens
222 | `--api-key <key>` | Override the API key. |222 | `--api-key <key>` | Override the API key. |
223 | `--base-url <url>` | Override the provider base URL (tests, self-hosted). |223 | `--base-url <url>` | Override the provider base URL (tests, self-hosted). |
224 | `--insecure` | Skip TLS certificate verification for the explicit `--base-url` endpoint or a `baseUrl` defined for a user model in `models.json` (self-signed local/internal inference servers). Built-in providers, auth, and model discovery keep normal TLS verification. |224 | `--insecure` | Skip TLS certificate verification for the explicit `--base-url` endpoint or a `baseUrl` defined for a user model in `models.json` (self-signed local/internal inference servers). Built-in providers, auth, and model discovery keep normal TLS verification. |
225-| `--system-prompt <text>` | Replace the default system prompt for this run (also overrides `$ZOT_HOME/SYSTEM.md`). |225+| `--system-prompt <text>` | Replace the default system prompt for this run (also overrides `$ZOT_HOME/SYSTEM.md`; pass `""` for no built-in identity). |
226 | `--append-system-prompt <text>` | Append text to the system prompt (repeatable). |226 | `--append-system-prompt <text>` | Append text to the system prompt (repeatable). |
227 | `--reasoning off\|minimum\|low\|medium\|high\|xhigh\|max` | Set the reasoning level on supported models (default: off). `max` is a separate opt-in tier above `xhigh`. |227 | `--reasoning off\|minimum\|low\|medium\|high\|xhigh\|max` | Set the reasoning level on supported models (default: off). `max` is a separate opt-in tier above `xhigh`. |
228 | `--stats <path>` | With `-p`/`--print`, write generation statistics as JSON. |228 | `--stats <path>` | With `-p`/`--print`, write generation statistics as JSON. |
M docs/zotfiles.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. The default layering behavior is usually preferable because it retains zot's normal identity and tool-use guidance.
118+Replacement is intended for fully specialized agents. The default layering behavior is usually preferable because it retains zot's normal identity and tool-use guidance. With replacement enabled, an empty `AGENT.md` intentionally omits the built-in identity while retaining generated context.
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. The default layering behavior is usually preferable because it retains zot's normal identity and tool-use guidance.118+Replacement is intended for fully specialized agents. The default layering behavior is usually preferable because it retains zot's normal identity and tool-use guidance. With replacement enabled, an empty `AGENT.md` intentionally omits the built-in identity while retaining generated context.
119 119
120 ## Bundled skills120 ## Bundled skills
121 121
M packages/agent/args.go +6 -2
@@ -42,8 +42,11 @@ type Args struct {
4242 inheritedAuthMethod string
4343 inheritedAccountID string
4444
45- BaseURL string // override provider base URL (for tests/self-hosted)
46- SystemPrompt string
45+ BaseURL string // override provider base URL (for tests/self-hosted)
46+ SystemPrompt string
47+ // SystemPromptSet records whether --system-prompt was provided. An empty
48+ // value is an intentional override, distinct from leaving the flag absent.
49+ SystemPromptSet bool
4750 AppendSystemPrompt []string
4851 Reasoning string
4952 Temperature *float32
@@ -202,6 +205,7 @@ func ParseArgs(in []string) (Args, error) {
202205 return a, err
203206 }
204207 a.SystemPrompt = v
208+ a.SystemPromptSet = true
205209 case "--append-system-prompt":
206210 v, err := want(&i, arg)
207211 if err != nil {
@@ -42,8 +42,11 @@ type Args struct {
42 inheritedAuthMethod string42 inheritedAuthMethod string
43 inheritedAccountID string43 inheritedAccountID string
44 44
45- BaseURL string // override provider base URL (for tests/self-hosted)45+ BaseURL string // override provider base URL (for tests/self-hosted)
46- SystemPrompt string46+ SystemPrompt string
47+ // SystemPromptSet records whether --system-prompt was provided. An empty
48+ // value is an intentional override, distinct from leaving the flag absent.
49+ SystemPromptSet bool
47 AppendSystemPrompt []string50 AppendSystemPrompt []string
48 Reasoning string51 Reasoning string
49 Temperature *float3252 Temperature *float32
@@ -202,6 +205,7 @@ func ParseArgs(in []string) (Args, error) {
202 return a, err205 return a, err
203 }206 }
204 a.SystemPrompt = v207 a.SystemPrompt = v
208+ a.SystemPromptSet = true
205 case "--append-system-prompt":209 case "--append-system-prompt":
206 v, err := want(&i, arg)210 v, err := want(&i, arg)
207 if err != nil {211 if err != nil {
M packages/agent/args_test.go +13 -0
@@ -2,6 +2,19 @@ package agent
22
33 import "testing"
44
5+func TestParseArgsExplicitEmptySystemPromptIsSet(t *testing.T) {
6+ args, err := ParseArgs([]string{"--system-prompt", ""})
7+ if err != nil {
8+ t.Fatalf("ParseArgs returned %v", err)
9+ }
10+ if !args.SystemPromptSet {
11+ t.Fatal("SystemPromptSet = false; want true for an explicitly empty flag value")
12+ }
13+ if args.SystemPrompt != "" {
14+ t.Fatalf("SystemPrompt = %q; want empty", args.SystemPrompt)
15+ }
16+}
17+
518 func TestParseArgsTemperatureAllowsZero(t *testing.T) {
619 args, err := ParseArgs([]string{"--temperature", "0"})
720 if err != nil {
@@ -2,6 +2,19 @@ package agent
2 2
3 import "testing"3 import "testing"
4 4
5+func TestParseArgsExplicitEmptySystemPromptIsSet(t *testing.T) {
6+ args, err := ParseArgs([]string{"--system-prompt", ""})
7+ if err != nil {
8+ t.Fatalf("ParseArgs returned %v", err)
9+ }
10+ if !args.SystemPromptSet {
11+ t.Fatal("SystemPromptSet = false; want true for an explicitly empty flag value")
12+ }
13+ if args.SystemPrompt != "" {
14+ t.Fatalf("SystemPrompt = %q; want empty", args.SystemPrompt)
15+ }
16+}
17+
5 func TestParseArgsTemperatureAllowsZero(t *testing.T) {18 func TestParseArgsTemperatureAllowsZero(t *testing.T) {
6 args, err := ParseArgs([]string{"--temperature", "0"})19 args, err := ParseArgs([]string{"--temperature", "0"})
7 if err != nil {20 if err != nil {
M packages/agent/build.go +7 -1
@@ -63,6 +63,7 @@ type Resolved struct {
6363 // resolve.
6464 systemAppend []string
6565 systemCustom string
66+ systemCustomSet bool
6667 toolDescriptions map[string]string
6768 }
6869
@@ -102,6 +103,7 @@ func (r *Resolved) MergeExtensionTools(mgr ExtensionToolSource) {
102103 CWD: r.CWD,
103104 Tools: toolSummariesFromRegistry(r.ToolRegistry, r.toolDescriptions),
104105 Custom: r.systemCustom,
106+ CustomSet: r.systemCustomSet,
105107 Append: append_,
106108 ZotDocsDir: filepath.Join(ZotHome(), "docs"),
107109 })
@@ -619,14 +621,17 @@ func Resolve(args Args, requireCred bool) (Resolved, error) {
619621 // 2. $ZOT_HOME/SYSTEM.md (persistent user override)
620622 // 3. built-in default (defaultIdentity + defaultGuidelines)
621623 custom := args.SystemPrompt
622- if custom == "" {
624+ customSet := args.SystemPromptSet || custom != ""
625+ if !customSet {
623626 custom = readUserSystemPrompt(ZotHome())
627+ customSet = custom != ""
624628 }
625629
626630 sys := BuildSystemPrompt(SystemPromptOpts{
627631 CWD: args.CWD,
628632 Tools: summaries,
629633 Custom: custom,
634+ CustomSet: customSet,
630635 Append: append_,
631636 ZotDocsDir: docsDir,
632637 })
@@ -660,6 +665,7 @@ func Resolve(args Args, requireCred bool) (Resolved, error) {
660665 ContextFiles: contextFiles,
661666 systemAppend: append_,
662667 systemCustom: custom,
668+ systemCustomSet: customSet,
663669 toolDescriptions: descMapFromSummaries(summaries),
664670 }, nil
665671 }
@@ -63,6 +63,7 @@ type Resolved struct {
63 // resolve.63 // resolve.
64 systemAppend []string64 systemAppend []string
65 systemCustom string65 systemCustom string
66+ systemCustomSet bool
66 toolDescriptions map[string]string67 toolDescriptions map[string]string
67 }68 }
68 69
@@ -102,6 +103,7 @@ func (r *Resolved) MergeExtensionTools(mgr ExtensionToolSource) {
102 CWD: r.CWD,103 CWD: r.CWD,
103 Tools: toolSummariesFromRegistry(r.ToolRegistry, r.toolDescriptions),104 Tools: toolSummariesFromRegistry(r.ToolRegistry, r.toolDescriptions),
104 Custom: r.systemCustom,105 Custom: r.systemCustom,
106+ CustomSet: r.systemCustomSet,
105 Append: append_,107 Append: append_,
106 ZotDocsDir: filepath.Join(ZotHome(), "docs"),108 ZotDocsDir: filepath.Join(ZotHome(), "docs"),
107 })109 })
@@ -619,14 +621,17 @@ func Resolve(args Args, requireCred bool) (Resolved, error) {
619 // 2. $ZOT_HOME/SYSTEM.md (persistent user override)621 // 2. $ZOT_HOME/SYSTEM.md (persistent user override)
620 // 3. built-in default (defaultIdentity + defaultGuidelines)622 // 3. built-in default (defaultIdentity + defaultGuidelines)
621 custom := args.SystemPrompt623 custom := args.SystemPrompt
622- if custom == "" {624+ customSet := args.SystemPromptSet || custom != ""
625+ if !customSet {
623 custom = readUserSystemPrompt(ZotHome())626 custom = readUserSystemPrompt(ZotHome())
627+ customSet = custom != ""
624 }628 }
625 629
626 sys := BuildSystemPrompt(SystemPromptOpts{630 sys := BuildSystemPrompt(SystemPromptOpts{
627 CWD: args.CWD,631 CWD: args.CWD,
628 Tools: summaries,632 Tools: summaries,
629 Custom: custom,633 Custom: custom,
634+ CustomSet: customSet,
630 Append: append_,635 Append: append_,
631 ZotDocsDir: docsDir,636 ZotDocsDir: docsDir,
632 })637 })
@@ -660,6 +665,7 @@ func Resolve(args Args, requireCred bool) (Resolved, error) {
660 ContextFiles: contextFiles,665 ContextFiles: contextFiles,
661 systemAppend: append_,666 systemAppend: append_,
662 systemCustom: custom,667 systemCustom: custom,
668+ systemCustomSet: customSet,
663 toolDescriptions: descMapFromSummaries(summaries),669 toolDescriptions: descMapFromSummaries(summaries),
664 }, nil670 }, nil
665 }671 }
M packages/agent/build_test.go +28 -0
@@ -98,6 +98,34 @@ func TestResolveNoContextFilesSkipsAgentsInstructions(t *testing.T) {
9898 }
9999 }
100100
101+func TestResolveExplicitEmptySystemPromptOverridesPersistentPrompt(t *testing.T) {
102+ zotHome := t.TempDir()
103+ cwd := t.TempDir()
104+ t.Setenv("ZOT_HOME", zotHome)
105+ t.Setenv("OPENAI_API_KEY", "test-key")
106+ if err := os.WriteFile(filepath.Join(zotHome, "SYSTEM.md"), []byte("persistent instructions"), 0o644); err != nil {
107+ t.Fatal(err)
108+ }
109+
110+ r, err := Resolve(Args{
111+ Provider: "openai",
112+ Model: "gpt-5",
113+ CWD: cwd,
114+ SystemPromptSet: true,
115+ SystemPrompt: "",
116+ NoContextFiles: true,
117+ NoSkill: true,
118+ }, false)
119+ if err != nil {
120+ t.Fatal(err)
121+ }
122+ for _, unwanted := range []string{"persistent instructions", defaultIdentity, "Zot's own docs are installed under"} {
123+ if strings.Contains(r.SystemPrompt, unwanted) {
124+ t.Fatalf("explicit empty system prompt unexpectedly contains %q:\n%s", unwanted, r.SystemPrompt)
125+ }
126+ }
127+}
128+
101129 // TestResolveFallsBackWhenConfiguredModelIsGone reproduces the
102130 // startup failure caught by the user's screenshot: the persisted
103131 // config.json points at a model id that's no longer in the active
@@ -98,6 +98,34 @@ func TestResolveNoContextFilesSkipsAgentsInstructions(t *testing.T) {
98 }98 }
99 }99 }
100 100
101+func TestResolveExplicitEmptySystemPromptOverridesPersistentPrompt(t *testing.T) {
102+ zotHome := t.TempDir()
103+ cwd := t.TempDir()
104+ t.Setenv("ZOT_HOME", zotHome)
105+ t.Setenv("OPENAI_API_KEY", "test-key")
106+ if err := os.WriteFile(filepath.Join(zotHome, "SYSTEM.md"), []byte("persistent instructions"), 0o644); err != nil {
107+ t.Fatal(err)
108+ }
109+
110+ r, err := Resolve(Args{
111+ Provider: "openai",
112+ Model: "gpt-5",
113+ CWD: cwd,
114+ SystemPromptSet: true,
115+ SystemPrompt: "",
116+ NoContextFiles: true,
117+ NoSkill: true,
118+ }, false)
119+ if err != nil {
120+ t.Fatal(err)
121+ }
122+ for _, unwanted := range []string{"persistent instructions", defaultIdentity, "Zot's own docs are installed under"} {
123+ if strings.Contains(r.SystemPrompt, unwanted) {
124+ t.Fatalf("explicit empty system prompt unexpectedly contains %q:\n%s", unwanted, r.SystemPrompt)
125+ }
126+ }
127+}
128+
101 // TestResolveFallsBackWhenConfiguredModelIsGone reproduces the129 // TestResolveFallsBackWhenConfiguredModelIsGone reproduces the
102 // startup failure caught by the user's screenshot: the persisted130 // startup failure caught by the user's screenshot: the persisted
103 // config.json points at a model id that's no longer in the active131 // config.json points at a model id that's no longer in the active
M packages/agent/sdk/sdk.go +27 -17
@@ -50,9 +50,14 @@ type Config struct {
5050 // process cwd.
5151 CWD string
5252
53- // SystemPrompt overrides the built-in system prompt.
53+ // SystemPrompt overrides the built-in system prompt when non-empty or when
54+ // SystemPromptSet is true.
5455 SystemPrompt string
5556
57+ // SystemPromptSet allows an empty SystemPrompt to intentionally omit the
58+ // built-in identity. It is unnecessary for non-empty prompts.
59+ SystemPromptSet bool
60+
5661 // AppendSystemPrompt is appended to the built-in (or overridden)
5762 // system prompt. Useful for project-specific instructions.
5863 AppendSystemPrompt []string
@@ -105,22 +110,7 @@ type Runtime struct {
105110 // New constructs a Runtime from cfg. Returns an error if no
106111 // credential is available for the requested provider.
107112 func New(cfg Config) (*Runtime, error) {
108- args := agent.Args{
109- Mode: agent.ModeJSON, // headless
110- Provider: cfg.Provider,
111- Model: cfg.Model,
112- CWD: cfg.CWD,
113- APIKey: cfg.APIKey,
114- BaseURL: cfg.BaseURL,
115- SystemPrompt: cfg.SystemPrompt,
116- AppendSystemPrompt: cfg.AppendSystemPrompt,
117- Reasoning: cfg.Reasoning,
118- Temperature: cfg.Temperature,
119- MaxSteps: cfg.MaxSteps,
120- Tools: cfg.Tools,
121- NoTools: cfg.NoTools,
122- NoSess: true, // SDK callers manage persistence themselves
123- }
113+ args := argsFromConfig(cfg)
124114 if args.MaxSteps == 0 {
125115 args.MaxSteps = 50
126116 }
@@ -140,6 +130,26 @@ func New(cfg Config) (*Runtime, error) {
140130 }, nil
141131 }
142132
133+func argsFromConfig(cfg Config) agent.Args {
134+ return agent.Args{
135+ Mode: agent.ModeJSON, // headless
136+ Provider: cfg.Provider,
137+ Model: cfg.Model,
138+ CWD: cfg.CWD,
139+ APIKey: cfg.APIKey,
140+ BaseURL: cfg.BaseURL,
141+ SystemPrompt: cfg.SystemPrompt,
142+ SystemPromptSet: cfg.SystemPromptSet || cfg.SystemPrompt != "",
143+ AppendSystemPrompt: cfg.AppendSystemPrompt,
144+ Reasoning: cfg.Reasoning,
145+ Temperature: cfg.Temperature,
146+ MaxSteps: cfg.MaxSteps,
147+ Tools: cfg.Tools,
148+ NoTools: cfg.NoTools,
149+ NoSess: true, // SDK callers manage persistence themselves
150+ }
151+}
152+
143153 // Provider returns the active provider id.
144154 func (r *Runtime) Provider() string { r.mu.Lock(); defer r.mu.Unlock(); return r.provider }
145155
@@ -50,9 +50,14 @@ type Config struct {
50 // process cwd.50 // process cwd.
51 CWD string51 CWD string
52 52
53- // SystemPrompt overrides the built-in system prompt.53+ // SystemPrompt overrides the built-in system prompt when non-empty or when
54+ // SystemPromptSet is true.
54 SystemPrompt string55 SystemPrompt string
55 56
57+ // SystemPromptSet allows an empty SystemPrompt to intentionally omit the
58+ // built-in identity. It is unnecessary for non-empty prompts.
59+ SystemPromptSet bool
60+
56 // AppendSystemPrompt is appended to the built-in (or overridden)61 // AppendSystemPrompt is appended to the built-in (or overridden)
57 // system prompt. Useful for project-specific instructions.62 // system prompt. Useful for project-specific instructions.
58 AppendSystemPrompt []string63 AppendSystemPrompt []string
@@ -105,22 +110,7 @@ type Runtime struct {
105 // New constructs a Runtime from cfg. Returns an error if no110 // New constructs a Runtime from cfg. Returns an error if no
106 // credential is available for the requested provider.111 // credential is available for the requested provider.
107 func New(cfg Config) (*Runtime, error) {112 func New(cfg Config) (*Runtime, error) {
108- args := agent.Args{113+ args := argsFromConfig(cfg)
109- Mode: agent.ModeJSON, // headless
110- Provider: cfg.Provider,
111- Model: cfg.Model,
112- CWD: cfg.CWD,
113- APIKey: cfg.APIKey,
114- BaseURL: cfg.BaseURL,
115- SystemPrompt: cfg.SystemPrompt,
116- AppendSystemPrompt: cfg.AppendSystemPrompt,
117- Reasoning: cfg.Reasoning,
118- Temperature: cfg.Temperature,
119- MaxSteps: cfg.MaxSteps,
120- Tools: cfg.Tools,
121- NoTools: cfg.NoTools,
122- NoSess: true, // SDK callers manage persistence themselves
123- }
124 if args.MaxSteps == 0 {114 if args.MaxSteps == 0 {
125 args.MaxSteps = 50115 args.MaxSteps = 50
126 }116 }
@@ -140,6 +130,26 @@ func New(cfg Config) (*Runtime, error) {
140 }, nil130 }, nil
141 }131 }
142 132
133+func argsFromConfig(cfg Config) agent.Args {
134+ return agent.Args{
135+ Mode: agent.ModeJSON, // headless
136+ Provider: cfg.Provider,
137+ Model: cfg.Model,
138+ CWD: cfg.CWD,
139+ APIKey: cfg.APIKey,
140+ BaseURL: cfg.BaseURL,
141+ SystemPrompt: cfg.SystemPrompt,
142+ SystemPromptSet: cfg.SystemPromptSet || cfg.SystemPrompt != "",
143+ AppendSystemPrompt: cfg.AppendSystemPrompt,
144+ Reasoning: cfg.Reasoning,
145+ Temperature: cfg.Temperature,
146+ MaxSteps: cfg.MaxSteps,
147+ Tools: cfg.Tools,
148+ NoTools: cfg.NoTools,
149+ NoSess: true, // SDK callers manage persistence themselves
150+ }
151+}
152+
143 // Provider returns the active provider id.153 // Provider returns the active provider id.
144 func (r *Runtime) Provider() string { r.mu.Lock(); defer r.mu.Unlock(); return r.provider }154 func (r *Runtime) Provider() string { r.mu.Lock(); defer r.mu.Unlock(); return r.provider }
145 155
A packages/agent/sdk/systemprompt_test.go +20 -0
@@ -0,0 +1,20 @@
1+package sdk
2+
3+import "testing"
4+
5+func TestArgsFromConfigPreservesExplicitEmptySystemPrompt(t *testing.T) {
6+ args := argsFromConfig(Config{SystemPromptSet: true})
7+ if !args.SystemPromptSet {
8+ t.Fatal("SystemPromptSet = false; want true for an explicitly empty SDK prompt")
9+ }
10+ if args.SystemPrompt != "" {
11+ t.Fatalf("SystemPrompt = %q; want empty", args.SystemPrompt)
12+ }
13+}
14+
15+func TestArgsFromConfigTreatsNonEmptySystemPromptAsSet(t *testing.T) {
16+ args := argsFromConfig(Config{SystemPrompt: "custom"})
17+ if !args.SystemPromptSet {
18+ t.Fatal("SystemPromptSet = false; want true for a non-empty SDK prompt")
19+ }
20+}
@@ -0,0 +1,20 @@
1+package sdk
2+
3+import "testing"
4+
5+func TestArgsFromConfigPreservesExplicitEmptySystemPrompt(t *testing.T) {
6+ args := argsFromConfig(Config{SystemPromptSet: true})
7+ if !args.SystemPromptSet {
8+ t.Fatal("SystemPromptSet = false; want true for an explicitly empty SDK prompt")
9+ }
10+ if args.SystemPrompt != "" {
11+ t.Fatalf("SystemPrompt = %q; want empty", args.SystemPrompt)
12+ }
13+}
14+
15+func TestArgsFromConfigTreatsNonEmptySystemPromptAsSet(t *testing.T) {
16+ args := argsFromConfig(Config{SystemPrompt: "custom"})
17+ if !args.SystemPromptSet {
18+ t.Fatal("SystemPromptSet = false; want true for a non-empty SDK prompt")
19+ }
20+}
M packages/agent/systemprompt.go +3 -2
@@ -21,7 +21,8 @@ type ToolSummary struct {
2121 type SystemPromptOpts struct {
2222 CWD string
2323 Tools []ToolSummary
24- Custom string // if set, replaces the built-in identity and docs guidance
24+ Custom string // when CustomSet, replaces the built-in identity and docs guidance
25+ CustomSet bool // preserves an intentionally empty custom prompt
2526 Append []string // extra text appended at the end
2627 Now time.Time
2728 ZotDocsDir string
@@ -58,7 +59,7 @@ func BuildSystemPrompt(o SystemPromptOpts) string {
5859
5960 var sb strings.Builder
6061
61- if o.Custom != "" {
62+ if o.CustomSet || o.Custom != "" {
6263 sb.WriteString(o.Custom)
6364 } else {
6465 sb.WriteString(defaultIdentity)
@@ -21,7 +21,8 @@ type ToolSummary struct {
21 type SystemPromptOpts struct {21 type SystemPromptOpts struct {
22 CWD string22 CWD string
23 Tools []ToolSummary23 Tools []ToolSummary
24- Custom string // if set, replaces the built-in identity and docs guidance24+ Custom string // when CustomSet, replaces the built-in identity and docs guidance
25+ CustomSet bool // preserves an intentionally empty custom prompt
25 Append []string // extra text appended at the end26 Append []string // extra text appended at the end
26 Now time.Time27 Now time.Time
27 ZotDocsDir string28 ZotDocsDir string
@@ -58,7 +59,7 @@ func BuildSystemPrompt(o SystemPromptOpts) string {
58 59
59 var sb strings.Builder60 var sb strings.Builder
60 61
61- if o.Custom != "" {62+ if o.CustomSet || o.Custom != "" {
62 sb.WriteString(o.Custom)63 sb.WriteString(o.Custom)
63 } else {64 } else {
64 sb.WriteString(defaultIdentity)65 sb.WriteString(defaultIdentity)
M packages/agent/zotfile.go +11 -6
@@ -151,12 +151,7 @@ func runLocalZotfile(ref string, args Args, version string) error {
151151 agentPrompt, err := os.ReadFile(agentPath)
152152 switch {
153153 case err == nil:
154- text := strings.TrimSpace(string(agentPrompt))
155- if zf.Manifest.ReplaceSystemPrompt {
156- args.SystemPrompt = text
157- } else {
158- args.AppendSystemPrompt = append(args.AppendSystemPrompt, text)
159- }
154+ applyZotfileAgentPrompt(&args, zf.Manifest.ReplaceSystemPrompt, string(agentPrompt))
160155 case os.IsNotExist(err):
161156 // AGENT.md is optional; missing file leaves the system prompt unchanged.
162157 default:
@@ -181,6 +176,16 @@ func runLocalZotfile(ref string, args Args, version string) error {
181176 return runWithArgs(args, version)
182177 }
183178
179+func applyZotfileAgentPrompt(args *Args, replace bool, prompt string) {
180+ text := strings.TrimSpace(prompt)
181+ if replace {
182+ args.SystemPrompt = text
183+ args.SystemPromptSet = true
184+ return
185+ }
186+ args.AppendSystemPrompt = append(args.AppendSystemPrompt, text)
187+}
188+
184189 func zotInspect(ref string) error {
185190 zf, cleanup, err := loadZotfile(ref)
186191 if cleanup != nil {
@@ -151,12 +151,7 @@ func runLocalZotfile(ref string, args Args, version string) error {
151 agentPrompt, err := os.ReadFile(agentPath)151 agentPrompt, err := os.ReadFile(agentPath)
152 switch {152 switch {
153 case err == nil:153 case err == nil:
154- text := strings.TrimSpace(string(agentPrompt))154+ applyZotfileAgentPrompt(&args, zf.Manifest.ReplaceSystemPrompt, string(agentPrompt))
155- if zf.Manifest.ReplaceSystemPrompt {
156- args.SystemPrompt = text
157- } else {
158- args.AppendSystemPrompt = append(args.AppendSystemPrompt, text)
159- }
160 case os.IsNotExist(err):155 case os.IsNotExist(err):
161 // AGENT.md is optional; missing file leaves the system prompt unchanged.156 // AGENT.md is optional; missing file leaves the system prompt unchanged.
162 default:157 default:
@@ -181,6 +176,16 @@ func runLocalZotfile(ref string, args Args, version string) error {
181 return runWithArgs(args, version)176 return runWithArgs(args, version)
182 }177 }
183 178
179+func applyZotfileAgentPrompt(args *Args, replace bool, prompt string) {
180+ text := strings.TrimSpace(prompt)
181+ if replace {
182+ args.SystemPrompt = text
183+ args.SystemPromptSet = true
184+ return
185+ }
186+ args.AppendSystemPrompt = append(args.AppendSystemPrompt, text)
187+}
188+
184 func zotInspect(ref string) error {189 func zotInspect(ref string) error {
185 zf, cleanup, err := loadZotfile(ref)190 zf, cleanup, err := loadZotfile(ref)
186 if cleanup != nil {191 if cleanup != nil {
M packages/agent/zotfile_test.go +11 -0
@@ -93,6 +93,17 @@ func writeTestZotfile(t *testing.T, manifest string) string {
9393 return dir
9494 }
9595
96+func TestApplyZotfileAgentPromptPreservesEmptyReplacement(t *testing.T) {
97+ args := Args{SystemPrompt: "cli prompt"}
98+ applyZotfileAgentPrompt(&args, true, " \n")
99+ if !args.SystemPromptSet {
100+ t.Fatal("SystemPromptSet = false; want true for an empty replacement AGENT.md")
101+ }
102+ if args.SystemPrompt != "" {
103+ t.Fatalf("SystemPrompt = %q; want empty", args.SystemPrompt)
104+ }
105+}
106+
96107 func TestLoadZotfileAllowsMissingAgentMD(t *testing.T) {
97108 dir := t.TempDir()
98109 if err := os.WriteFile(filepath.Join(dir, "manifest.json"), []byte(`{"zotfile":1,"name":"test"}`), 0o600); err != nil {
@@ -93,6 +93,17 @@ func writeTestZotfile(t *testing.T, manifest string) string {
93 return dir93 return dir
94 }94 }
95 95
96+func TestApplyZotfileAgentPromptPreservesEmptyReplacement(t *testing.T) {
97+ args := Args{SystemPrompt: "cli prompt"}
98+ applyZotfileAgentPrompt(&args, true, " \n")
99+ if !args.SystemPromptSet {
100+ t.Fatal("SystemPromptSet = false; want true for an empty replacement AGENT.md")
101+ }
102+ if args.SystemPrompt != "" {
103+ t.Fatalf("SystemPrompt = %q; want empty", args.SystemPrompt)
104+ }
105+}
106+
96 func TestLoadZotfileAllowsMissingAgentMD(t *testing.T) {107 func TestLoadZotfileAllowsMissingAgentMD(t *testing.T) {
97 dir := t.TempDir()108 dir := t.TempDir()
98 if err := os.WriteFile(filepath.Join(dir, "manifest.json"), []byte(`{"zotfile":1,"name":"test"}`), 0o600); err != nil {109 if err := os.WriteFile(filepath.Join(dir, "manifest.json"), []byte(`{"zotfile":1,"name":"test"}`), 0o600); err != nil {

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