detached 042538b

Merge pull request #123 from bnema/feat/global-writing-guidance

feat(agent): add universal writing guidance
commit
042538b70004196cdae60812a49487f9455cc036
author
bnema · · 2026-08-13 04:29 UTC
committer
GitHub · · 2026-08-13 04:29 UTC
merge of
14b0be1 + 3442086 · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
refs
tag: v0.16.0
5 files · +106 -15
M packages/agent/cli.go +1 -0
@@ -2414,6 +2414,7 @@ func runInteractive(ctx context.Context, args Args, version string) (runErr erro
24142414 Reasoning: r.Reasoning,
24152415 OnReasoningChanged: runtime.SetReasoning,
24162416 SystemPrompt: r.SystemPrompt,
2417+ WritingGuidance: WritingGuidance(),
24172418 Tools: r.ToolRegistry,
24182419 MaxSteps: r.MaxSteps,
24192420 CWD: r.CWD,
@@ -2414,6 +2414,7 @@ func runInteractive(ctx context.Context, args Args, version string) (runErr erro
2414 Reasoning: r.Reasoning,2414 Reasoning: r.Reasoning,
2415 OnReasoningChanged: runtime.SetReasoning,2415 OnReasoningChanged: runtime.SetReasoning,
2416 SystemPrompt: r.SystemPrompt,2416 SystemPrompt: r.SystemPrompt,
2417+ WritingGuidance: WritingGuidance(),
2417 Tools: r.ToolRegistry,2418 Tools: r.ToolRegistry,
2418 MaxSteps: r.MaxSteps,2419 MaxSteps: r.MaxSteps,
2419 CWD: r.CWD,2420 CWD: r.CWD,
M packages/agent/modes/auto_subagents_test.go +28 -0
@@ -15,6 +15,34 @@ import (
1515
1616 const autoSubagentsTestTimeout = 10 * time.Second
1717
18+func TestAutoSubagentsSystemPromptKeepsWritingGuidanceLast(t *testing.T) {
19+ const guidance = "universal writing guidance"
20+ iv := &Interactive{
21+ agent: &core.Agent{System: "base system\n\n" + guidance},
22+ cfg: InteractiveConfig{
23+ AutoSubagentsSystemAddendum: "auto-subagents guidance",
24+ WritingGuidance: guidance,
25+ Supervisor: &subagents.Supervisor{},
26+ },
27+ }
28+
29+ iv.applyAutoSubagentsSystemPrompt(true)
30+ if count := strings.Count(iv.agent.System, guidance); count != 1 {
31+ t.Fatalf("writing guidance count = %d, want 1: %q", count, iv.agent.System)
32+ }
33+ if !strings.HasSuffix(iv.agent.System, guidance) {
34+ t.Fatalf("writing guidance is not final after enabling: %q", iv.agent.System)
35+ }
36+
37+ iv.applyAutoSubagentsSystemPrompt(false)
38+ if count := strings.Count(iv.agent.System, guidance); count != 1 {
39+ t.Fatalf("writing guidance count = %d, want 1: %q", count, iv.agent.System)
40+ }
41+ if !strings.HasSuffix(iv.agent.System, guidance) {
42+ t.Fatalf("writing guidance is not final after disabling: %q", iv.agent.System)
43+ }
44+}
45+
1846 func TestAutoSubagentsSystemPromptKeepsProfileManifestWhileTogglingDelegationGuidance(t *testing.T) {
1947 const onDemand = "delegate only on an explicit user request"
2048 iv := &Interactive{
@@ -15,6 +15,34 @@ import (
15 15
16 const autoSubagentsTestTimeout = 10 * time.Second16 const autoSubagentsTestTimeout = 10 * time.Second
17 17
18+func TestAutoSubagentsSystemPromptKeepsWritingGuidanceLast(t *testing.T) {
19+ const guidance = "universal writing guidance"
20+ iv := &Interactive{
21+ agent: &core.Agent{System: "base system\n\n" + guidance},
22+ cfg: InteractiveConfig{
23+ AutoSubagentsSystemAddendum: "auto-subagents guidance",
24+ WritingGuidance: guidance,
25+ Supervisor: &subagents.Supervisor{},
26+ },
27+ }
28+
29+ iv.applyAutoSubagentsSystemPrompt(true)
30+ if count := strings.Count(iv.agent.System, guidance); count != 1 {
31+ t.Fatalf("writing guidance count = %d, want 1: %q", count, iv.agent.System)
32+ }
33+ if !strings.HasSuffix(iv.agent.System, guidance) {
34+ t.Fatalf("writing guidance is not final after enabling: %q", iv.agent.System)
35+ }
36+
37+ iv.applyAutoSubagentsSystemPrompt(false)
38+ if count := strings.Count(iv.agent.System, guidance); count != 1 {
39+ t.Fatalf("writing guidance count = %d, want 1: %q", count, iv.agent.System)
40+ }
41+ if !strings.HasSuffix(iv.agent.System, guidance) {
42+ t.Fatalf("writing guidance is not final after disabling: %q", iv.agent.System)
43+ }
44+}
45+
18 func TestAutoSubagentsSystemPromptKeepsProfileManifestWhileTogglingDelegationGuidance(t *testing.T) {46 func TestAutoSubagentsSystemPromptKeepsProfileManifestWhileTogglingDelegationGuidance(t *testing.T) {
19 const onDemand = "delegate only on an explicit user request"47 const onDemand = "delegate only on an explicit user request"
20 iv := &Interactive{48 iv := &Interactive{
M packages/agent/modes/interactive.go +25 -15
@@ -32,17 +32,18 @@ import (
3232
3333 // InteractiveConfig configures the interactive loop.
3434 type InteractiveConfig struct {
35- Terminal tui.Terminal
36- Theme tui.Theme
37- Model string
38- Provider string
39- AuthMethod string // "apikey" | "oauth" — used to tag cost as (sub) in status bar
40- BaseURL string
41- Reasoning string
42- SystemPrompt string
43- Tools core.Registry
44- MaxSteps int
45- CWD string
35+ Terminal tui.Terminal
36+ Theme tui.Theme
37+ Model string
38+ Provider string
39+ AuthMethod string // "apikey" | "oauth" — used to tag cost as (sub) in status bar
40+ BaseURL string
41+ Reasoning string
42+ SystemPrompt string
43+ WritingGuidance string
44+ Tools core.Registry
45+ MaxSteps int
46+ CWD string
4647
4748 // Startup resource fields are loaded inputs available to list before the
4849 // transcript. They are never sent to the provider or persisted by the view.
@@ -9176,6 +9177,11 @@ func (i *Interactive) applyAutoSubagentsSystemPrompt(orchestrating bool) {
91769177
91779178 sys, _ := i.agent.PromptConfig()
91789179 changed := false
9180+ if guidance := strings.TrimSpace(i.cfg.WritingGuidance); guidance != "" {
9181+ var removed bool
9182+ sys, removed = removeLastAutoSubagentsAddendum(sys, guidance)
9183+ changed = changed || removed
9184+ }
91799185 for idx := len(i.managedAutoSubagentsAddenda) - 1; idx >= 0; idx-- {
91809186 var removed bool
91819187 sys, removed = removeLastAutoSubagentsAddendum(sys, i.managedAutoSubagentsAddenda[idx])
@@ -9184,7 +9190,7 @@ func (i *Interactive) applyAutoSubagentsSystemPrompt(orchestrating bool) {
91849190 i.managedAutoSubagentsAddenda = nil
91859191
91869192 addenda := autoSubagentsAddenda(i.cfg, orchestrating)
9187- if changed && len(addenda) > 0 {
9193+ if changed {
91889194 sys = strings.TrimRight(sys, "\n")
91899195 }
91909196 for _, addendum := range addenda {
@@ -9195,12 +9201,16 @@ func (i *Interactive) applyAutoSubagentsSystemPrompt(orchestrating bool) {
91959201 i.managedAutoSubagentsAddenda = append(i.managedAutoSubagentsAddenda, addendum)
91969202 changed = true
91979203 }
9204+ if guidance := strings.TrimSpace(i.cfg.WritingGuidance); guidance != "" {
9205+ if sys != "" {
9206+ sys += "\n\n"
9207+ }
9208+ sys += guidance
9209+ changed = true
9210+ }
91989211 if !changed {
91999212 return
92009213 }
9201- if len(addenda) == 0 {
9202- sys = strings.TrimRight(sys, "\n") + "\n"
9203- }
92049214 i.agent.SetSystemPrompt(sys)
92059215 }
92069216
@@ -32,17 +32,18 @@ import (
32 32
33 // InteractiveConfig configures the interactive loop.33 // InteractiveConfig configures the interactive loop.
34 type InteractiveConfig struct {34 type InteractiveConfig struct {
35- Terminal tui.Terminal35+ Terminal tui.Terminal
36- Theme tui.Theme36+ Theme tui.Theme
37- Model string37+ Model string
38- Provider string38+ Provider string
39- AuthMethod string // "apikey" | "oauth" — used to tag cost as (sub) in status bar39+ AuthMethod string // "apikey" | "oauth" — used to tag cost as (sub) in status bar
40- BaseURL string40+ BaseURL string
41- Reasoning string41+ Reasoning string
42- SystemPrompt string42+ SystemPrompt string
43- Tools core.Registry43+ WritingGuidance string
44- MaxSteps int44+ Tools core.Registry
45- CWD string45+ MaxSteps int
46+ CWD string
46 47
47 // Startup resource fields are loaded inputs available to list before the48 // Startup resource fields are loaded inputs available to list before the
48 // transcript. They are never sent to the provider or persisted by the view.49 // transcript. They are never sent to the provider or persisted by the view.
@@ -9176,6 +9177,11 @@ func (i *Interactive) applyAutoSubagentsSystemPrompt(orchestrating bool) {
9176 9177
9177 sys, _ := i.agent.PromptConfig()9178 sys, _ := i.agent.PromptConfig()
9178 changed := false9179 changed := false
9180+ if guidance := strings.TrimSpace(i.cfg.WritingGuidance); guidance != "" {
9181+ var removed bool
9182+ sys, removed = removeLastAutoSubagentsAddendum(sys, guidance)
9183+ changed = changed || removed
9184+ }
9179 for idx := len(i.managedAutoSubagentsAddenda) - 1; idx >= 0; idx-- {9185 for idx := len(i.managedAutoSubagentsAddenda) - 1; idx >= 0; idx-- {
9180 var removed bool9186 var removed bool
9181 sys, removed = removeLastAutoSubagentsAddendum(sys, i.managedAutoSubagentsAddenda[idx])9187 sys, removed = removeLastAutoSubagentsAddendum(sys, i.managedAutoSubagentsAddenda[idx])
@@ -9184,7 +9190,7 @@ func (i *Interactive) applyAutoSubagentsSystemPrompt(orchestrating bool) {
9184 i.managedAutoSubagentsAddenda = nil9190 i.managedAutoSubagentsAddenda = nil
9185 9191
9186 addenda := autoSubagentsAddenda(i.cfg, orchestrating)9192 addenda := autoSubagentsAddenda(i.cfg, orchestrating)
9187- if changed && len(addenda) > 0 {9193+ if changed {
9188 sys = strings.TrimRight(sys, "\n")9194 sys = strings.TrimRight(sys, "\n")
9189 }9195 }
9190 for _, addendum := range addenda {9196 for _, addendum := range addenda {
@@ -9195,12 +9201,16 @@ func (i *Interactive) applyAutoSubagentsSystemPrompt(orchestrating bool) {
9195 i.managedAutoSubagentsAddenda = append(i.managedAutoSubagentsAddenda, addendum)9201 i.managedAutoSubagentsAddenda = append(i.managedAutoSubagentsAddenda, addendum)
9196 changed = true9202 changed = true
9197 }9203 }
9204+ if guidance := strings.TrimSpace(i.cfg.WritingGuidance); guidance != "" {
9205+ if sys != "" {
9206+ sys += "\n\n"
9207+ }
9208+ sys += guidance
9209+ changed = true
9210+ }
9198 if !changed {9211 if !changed {
9199 return9212 return
9200 }9213 }
9201- if len(addenda) == 0 {
9202- sys = strings.TrimRight(sys, "\n") + "\n"
9203- }
9204 i.agent.SetSystemPrompt(sys)9214 i.agent.SetSystemPrompt(sys)
9205 }9215 }
9206 9216
M packages/agent/systemprompt.go +15 -0
@@ -35,6 +35,8 @@ type SystemPromptOpts struct {
3535 //
3636 // - A one-paragraph identity (who zut is, what the name means,
3737 // what the TUI expects for output format).
38+// - Compact handoff and writing-quality guidance that must survive
39+// custom identities and appended context.
3840 // - The date + cwd footer so the model has current-context.
3941 //
4042 // Everything else (tool listing, operating guidelines, "don't run
@@ -81,6 +83,11 @@ func BuildSystemPrompt(o SystemPromptOpts) string {
8183 sb.WriteString(a)
8284 }
8385
86+ // Keep the universal writing policy after optional context so narrower
87+ // addenda cannot accidentally disable it.
88+ sb.WriteString("\n\n")
89+ sb.WriteString(writingGuidance)
90+
8491 fmt.Fprintf(&sb, "\n\nCurrent date: %s\nCurrent working directory: %s\n", date, cwd)
8592 return sb.String()
8693 }
@@ -92,3 +99,11 @@ Your output renders in a TUI that understands markdown for prose and plain text
9299 For focused changes to an existing file, inspect its current contents and use edit with verbatim oldText taken from that same file. Include only enough context to make each match unambiguous. Use write when creating a file or replacing it wholesale. Do not mutate files through bash redirections or commands such as cat, echo, sed, or tee, because those changes appear as opaque shell output instead of a readable edit diff.`
93100
94101 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.`
102+
103+const writingGuidance = `Adapt every response and document to its medium, audience, and reader's immediate need. Lead with the answer or next useful action. Use plain, precise language, concrete details, and strong verbs. Prefer verified facts to vague emphasis; qualify uncertain claims. Preserve useful structure, descriptive links, and accessibility. Cut throat-clearing, ceremony, repetition, catalogue-like prose, and mechanical sentence patterns. Do not manufacture slang, errors, hesitation, or artificial variation to sound human. Revise silently unless asked to explain your writing choices.`
104+
105+// WritingGuidance returns the universal policy kept at the end of resolved
106+// prompt context, including after interactive prompt updates.
107+func WritingGuidance() string {
108+ return writingGuidance
109+}
@@ -35,6 +35,8 @@ type SystemPromptOpts struct {
35 //35 //
36 // - A one-paragraph identity (who zut is, what the name means,36 // - A one-paragraph identity (who zut is, what the name means,
37 // what the TUI expects for output format).37 // what the TUI expects for output format).
38+// - Compact handoff and writing-quality guidance that must survive
39+// custom identities and appended context.
38 // - The date + cwd footer so the model has current-context.40 // - The date + cwd footer so the model has current-context.
39 //41 //
40 // Everything else (tool listing, operating guidelines, "don't run42 // Everything else (tool listing, operating guidelines, "don't run
@@ -81,6 +83,11 @@ func BuildSystemPrompt(o SystemPromptOpts) string {
81 sb.WriteString(a)83 sb.WriteString(a)
82 }84 }
83 85
86+ // Keep the universal writing policy after optional context so narrower
87+ // addenda cannot accidentally disable it.
88+ sb.WriteString("\n\n")
89+ sb.WriteString(writingGuidance)
90+
84 fmt.Fprintf(&sb, "\n\nCurrent date: %s\nCurrent working directory: %s\n", date, cwd)91 fmt.Fprintf(&sb, "\n\nCurrent date: %s\nCurrent working directory: %s\n", date, cwd)
85 return sb.String()92 return sb.String()
86 }93 }
@@ -92,3 +99,11 @@ Your output renders in a TUI that understands markdown for prose and plain text
92 For focused changes to an existing file, inspect its current contents and use edit with verbatim oldText taken from that same file. Include only enough context to make each match unambiguous. Use write when creating a file or replacing it wholesale. Do not mutate files through bash redirections or commands such as cat, echo, sed, or tee, because those changes appear as opaque shell output instead of a readable edit diff.`99 For focused changes to an existing file, inspect its current contents and use edit with verbatim oldText taken from that same file. Include only enough context to make each match unambiguous. Use write when creating a file or replacing it wholesale. Do not mutate files through bash redirections or commands such as cat, echo, sed, or tee, because those changes appear as opaque shell output instead of a readable edit diff.`
93 100
94 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.`101 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.`
102+
103+const writingGuidance = `Adapt every response and document to its medium, audience, and reader's immediate need. Lead with the answer or next useful action. Use plain, precise language, concrete details, and strong verbs. Prefer verified facts to vague emphasis; qualify uncertain claims. Preserve useful structure, descriptive links, and accessibility. Cut throat-clearing, ceremony, repetition, catalogue-like prose, and mechanical sentence patterns. Do not manufacture slang, errors, hesitation, or artificial variation to sound human. Revise silently unless asked to explain your writing choices.`
104+
105+// WritingGuidance returns the universal policy kept at the end of resolved
106+// prompt context, including after interactive prompt updates.
107+func WritingGuidance() string {
108+ return writingGuidance
109+}
M packages/agent/systemprompt_test.go +37 -0
@@ -6,6 +6,43 @@ import (
66 "time"
77 )
88
9+func TestBuildSystemPromptAlwaysIncludesFinalWritingGuidance(t *testing.T) {
10+ for _, tt := range []struct {
11+ name string
12+ custom string
13+ }{
14+ {name: "default"},
15+ {name: "custom", custom: "custom identity"},
16+ } {
17+ t.Run(tt.name, func(t *testing.T) {
18+ const conflictingAddendum = "Always use ceremonial language."
19+ prompt := BuildSystemPrompt(SystemPromptOpts{
20+ Custom: tt.custom,
21+ Append: []string{conflictingAddendum},
22+ })
23+
24+ if count := strings.Count(prompt, writingGuidance); count != 1 {
25+ t.Fatalf("writing guidance count = %d, want 1:\n%s", count, prompt)
26+ }
27+ if strings.Index(prompt, writingGuidance) < strings.Index(prompt, conflictingAddendum) {
28+ t.Fatalf("writing guidance must follow appended context:\n%s", prompt)
29+ }
30+ for _, want := range []string{
31+ "medium, audience, and reader's immediate need",
32+ "plain, precise language",
33+ "verified facts",
34+ "descriptive links, and accessibility",
35+ "Do not manufacture slang, errors, hesitation",
36+ "Revise silently",
37+ } {
38+ if !strings.Contains(prompt, want) {
39+ t.Errorf("writing guidance missing %q:\n%s", want, prompt)
40+ }
41+ }
42+ })
43+ }
44+}
45+
946 func TestBuildSystemPromptAddsCompactionHandoffToCustomPrompt(t *testing.T) {
1047 prompt := BuildSystemPrompt(SystemPromptOpts{Custom: "custom identity"})
1148 if !strings.Contains(prompt, "custom identity") {
@@ -6,6 +6,43 @@ import (
6 "time"6 "time"
7 )7 )
8 8
9+func TestBuildSystemPromptAlwaysIncludesFinalWritingGuidance(t *testing.T) {
10+ for _, tt := range []struct {
11+ name string
12+ custom string
13+ }{
14+ {name: "default"},
15+ {name: "custom", custom: "custom identity"},
16+ } {
17+ t.Run(tt.name, func(t *testing.T) {
18+ const conflictingAddendum = "Always use ceremonial language."
19+ prompt := BuildSystemPrompt(SystemPromptOpts{
20+ Custom: tt.custom,
21+ Append: []string{conflictingAddendum},
22+ })
23+
24+ if count := strings.Count(prompt, writingGuidance); count != 1 {
25+ t.Fatalf("writing guidance count = %d, want 1:\n%s", count, prompt)
26+ }
27+ if strings.Index(prompt, writingGuidance) < strings.Index(prompt, conflictingAddendum) {
28+ t.Fatalf("writing guidance must follow appended context:\n%s", prompt)
29+ }
30+ for _, want := range []string{
31+ "medium, audience, and reader's immediate need",
32+ "plain, precise language",
33+ "verified facts",
34+ "descriptive links, and accessibility",
35+ "Do not manufacture slang, errors, hesitation",
36+ "Revise silently",
37+ } {
38+ if !strings.Contains(prompt, want) {
39+ t.Errorf("writing guidance missing %q:\n%s", want, prompt)
40+ }
41+ }
42+ })
43+ }
44+}
45+
9 func TestBuildSystemPromptAddsCompactionHandoffToCustomPrompt(t *testing.T) {46 func TestBuildSystemPromptAddsCompactionHandoffToCustomPrompt(t *testing.T) {
10 prompt := BuildSystemPrompt(SystemPromptOpts{Custom: "custom identity"})47 prompt := BuildSystemPrompt(SystemPromptOpts{Custom: "custom identity"})
11 if !strings.Contains(prompt, "custom identity") {48 if !strings.Contains(prompt, "custom identity") {

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