detached fcf1dc2

feat(subagents): support profile fast mode

commit
fcf1dc2004b1827753a659a9dcab80c4f82c3ae5
author
brice · · 2026-08-05 03:41 UTC
parent
ebe86d6
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
11 files · +236 -9
M docs/subagents.md +3 -1
@@ -29,6 +29,7 @@ thinking: max
2929 systemPromptMode: replace
3030 inheritProjectContext: false
3131 inheritSkills: false
32+fastMode: false
3233 ---
3334
3435 You are a review worker. Inspect the requested scope, report evidence-backed findings, and do not edit files.
@@ -47,6 +48,7 @@ Supported metadata:
4748 | `systemPromptMode` | `append` (default) or `replace`. |
4849 | `inheritProjectContext` | Set to `false` to omit `AGENTS.md` context from the child. |
4950 | `inheritSkills` | Set to `false` to omit skill discovery and the conditional `skill` loader from the child; `--no-skill` has the same effect for a run. |
51+| `fastMode` | Optional fast-mode restriction. Omit it to inherit the host setting; `false` disables fast mode for this profile; `true` only permits fast mode when the host setting is enabled. |
5052
5153 Other frontmatter from another agent host is ignored when zot does not have an equivalent setting. For example, `maxSubagentDepth` is currently not enforced by zot.
5254
@@ -82,4 +84,4 @@ The interactive command also supports the same selection explicitly:
8284 /swarm new --agent implementer --reasoning high Implement the parser change
8385 ```
8486
85-All swarm children still share the host working directory. Named profiles change the child's instructions and configuration; they do not create a worktree, branch, or security sandbox. When the host has **fast mode** enabled, every child inherits it; non-OpenAI child providers return an unsupported-provider error instead of silently ignoring the setting.
87+All swarm children still share the host working directory. Named profiles change the child's instructions and configuration; they do not create a worktree, branch, or security sandbox. Fast mode is inherited by default. A profile with `fastMode: false` opts out, while `fastMode: true` cannot enable fast mode when the host setting is off. Non-OpenAI child providers return an unsupported-provider error instead of silently ignoring an enabled setting.
@@ -29,6 +29,7 @@ thinking: max
29 systemPromptMode: replace29 systemPromptMode: replace
30 inheritProjectContext: false30 inheritProjectContext: false
31 inheritSkills: false31 inheritSkills: false
32+fastMode: false
32 ---33 ---
33 34
34 You are a review worker. Inspect the requested scope, report evidence-backed findings, and do not edit files.35 You are a review worker. Inspect the requested scope, report evidence-backed findings, and do not edit files.
@@ -47,6 +48,7 @@ Supported metadata:
47 | `systemPromptMode` | `append` (default) or `replace`. |48 | `systemPromptMode` | `append` (default) or `replace`. |
48 | `inheritProjectContext` | Set to `false` to omit `AGENTS.md` context from the child. |49 | `inheritProjectContext` | Set to `false` to omit `AGENTS.md` context from the child. |
49 | `inheritSkills` | Set to `false` to omit skill discovery and the conditional `skill` loader from the child; `--no-skill` has the same effect for a run. |50 | `inheritSkills` | Set to `false` to omit skill discovery and the conditional `skill` loader from the child; `--no-skill` has the same effect for a run. |
51+| `fastMode` | Optional fast-mode restriction. Omit it to inherit the host setting; `false` disables fast mode for this profile; `true` only permits fast mode when the host setting is enabled. |
50 52
51 Other frontmatter from another agent host is ignored when zot does not have an equivalent setting. For example, `maxSubagentDepth` is currently not enforced by zot.53 Other frontmatter from another agent host is ignored when zot does not have an equivalent setting. For example, `maxSubagentDepth` is currently not enforced by zot.
52 54
@@ -82,4 +84,4 @@ The interactive command also supports the same selection explicitly:
82 /swarm new --agent implementer --reasoning high Implement the parser change84 /swarm new --agent implementer --reasoning high Implement the parser change
83 ```85 ```
84 86
85-All swarm children still share the host working directory. Named profiles change the child's instructions and configuration; they do not create a worktree, branch, or security sandbox. When the host has **fast mode** enabled, every child inherits it; non-OpenAI child providers return an unsupported-provider error instead of silently ignoring the setting.87+All swarm children still share the host working directory. Named profiles change the child's instructions and configuration; they do not create a worktree, branch, or security sandbox. Fast mode is inherited by default. A profile with `fastMode: false` opts out, while `fastMode: true` cannot enable fast mode when the host setting is off. Non-OpenAI child providers return an unsupported-provider error instead of silently ignoring an enabled setting.
M packages/agent/build.go +7 -0
@@ -777,6 +777,13 @@ func Resolve(args Args, requireCred bool) (Resolved, error) {
777777 if args.FastModeSet {
778778 fastMode = args.FastMode
779779 }
780+ // A profile can opt out of the effective host/child fast-mode setting,
781+ // but it does not enable fast mode by itself. Keep this as a
782+ // restriction on the effective value rather than an override so an
783+ // explicit child --fast-mode flag cannot bypass fastMode: false.
784+ if selectedProfile != nil && selectedProfile.FastMode != nil {
785+ fastMode = fastMode && *selectedProfile.FastMode
786+ }
780787
781788 max := args.MaxSteps // 0 = unlimited
782789
@@ -777,6 +777,13 @@ func Resolve(args Args, requireCred bool) (Resolved, error) {
777 if args.FastModeSet {777 if args.FastModeSet {
778 fastMode = args.FastMode778 fastMode = args.FastMode
779 }779 }
780+ // A profile can opt out of the effective host/child fast-mode setting,
781+ // but it does not enable fast mode by itself. Keep this as a
782+ // restriction on the effective value rather than an override so an
783+ // explicit child --fast-mode flag cannot bypass fastMode: false.
784+ if selectedProfile != nil && selectedProfile.FastMode != nil {
785+ fastMode = fastMode && *selectedProfile.FastMode
786+ }
780 787
781 max := args.MaxSteps // 0 = unlimited788 max := args.MaxSteps // 0 = unlimited
782 789
M packages/agent/build_test.go +64 -0
@@ -2,6 +2,7 @@ package agent
22
33 import (
44 "context"
5+ "fmt"
56 "net/http"
67 "net/http/httptest"
78 "os"
@@ -248,6 +249,69 @@ You are a read-only reviewer.
248249 }
249250 }
250251
252+func TestResolveSubagentFastModeUsesHostSettingAsAnUpperBound(t *testing.T) {
253+ cases := []struct {
254+ name string
255+ hostFastMode bool
256+ profileFast *bool
257+ argFastMode bool
258+ argFastSet bool
259+ wantFastMode bool
260+ }{
261+ {name: "unset inherits enabled host", hostFastMode: true, wantFastMode: true},
262+ {name: "unset inherits disabled host", hostFastMode: false, wantFastMode: false},
263+ {name: "false disables enabled host", hostFastMode: true, profileFast: boolPtr(false), wantFastMode: false},
264+ {name: "true cannot enable disabled host", hostFastMode: false, profileFast: boolPtr(true), wantFastMode: false},
265+ {name: "false stays disabled with disabled host", hostFastMode: false, profileFast: boolPtr(false), wantFastMode: false},
266+ {name: "profile false cannot be bypassed", hostFastMode: true, profileFast: boolPtr(false), argFastMode: true, argFastSet: true, wantFastMode: false},
267+ }
268+ for _, tc := range cases {
269+ t.Run(tc.name, func(t *testing.T) {
270+ root := t.TempDir()
271+ home := filepath.Join(root, "home")
272+ zotHome := filepath.Join(root, "zot-home")
273+ project := filepath.Join(root, "repo")
274+ profilesDir := filepath.Join(home, ".agents", "agents")
275+ if err := os.MkdirAll(profilesDir, 0o755); err != nil {
276+ t.Fatal(err)
277+ }
278+ if err := os.MkdirAll(project, 0o755); err != nil {
279+ t.Fatal(err)
280+ }
281+ t.Setenv("HOME", home)
282+ t.Setenv("ZOT_HOME", zotHome)
283+ t.Setenv("ZOT_AGENT_PROFILES", profilesDir)
284+
285+ cfg := Config{Provider: "openai", Model: "gpt-5"}
286+ cfg.FastMode = &tc.hostFastMode
287+ if err := SaveConfig(cfg); err != nil {
288+ t.Fatal(err)
289+ }
290+ profile := "---\nname: reviewer\n"
291+ if tc.profileFast != nil {
292+ profile += fmt.Sprintf("fastMode: %t\n", *tc.profileFast)
293+ }
294+ profile += "---\nReview the requested scope.\n"
295+ if err := os.WriteFile(filepath.Join(profilesDir, "reviewer.md"), []byte(profile), 0o600); err != nil {
296+ t.Fatal(err)
297+ }
298+
299+ r, err := Resolve(Args{
300+ CWD: project,
301+ Subagent: "reviewer",
302+ FastMode: tc.argFastMode,
303+ FastModeSet: tc.argFastSet,
304+ }, false)
305+ if err != nil {
306+ t.Fatal(err)
307+ }
308+ if r.FastMode != tc.wantFastMode {
309+ t.Fatalf("FastMode = %v, want %v", r.FastMode, tc.wantFastMode)
310+ }
311+ })
312+ }
313+}
314+
251315 func TestReadAgentsContextMissingFilesIsEmpty(t *testing.T) {
252316 files := loadAgentsContext(t.TempDir(), t.TempDir())
253317 if len(files) != 0 {
@@ -2,6 +2,7 @@ package agent
2 2
3 import (3 import (
4 "context"4 "context"
5+ "fmt"
5 "net/http"6 "net/http"
6 "net/http/httptest"7 "net/http/httptest"
7 "os"8 "os"
@@ -248,6 +249,69 @@ You are a read-only reviewer.
248 }249 }
249 }250 }
250 251
252+func TestResolveSubagentFastModeUsesHostSettingAsAnUpperBound(t *testing.T) {
253+ cases := []struct {
254+ name string
255+ hostFastMode bool
256+ profileFast *bool
257+ argFastMode bool
258+ argFastSet bool
259+ wantFastMode bool
260+ }{
261+ {name: "unset inherits enabled host", hostFastMode: true, wantFastMode: true},
262+ {name: "unset inherits disabled host", hostFastMode: false, wantFastMode: false},
263+ {name: "false disables enabled host", hostFastMode: true, profileFast: boolPtr(false), wantFastMode: false},
264+ {name: "true cannot enable disabled host", hostFastMode: false, profileFast: boolPtr(true), wantFastMode: false},
265+ {name: "false stays disabled with disabled host", hostFastMode: false, profileFast: boolPtr(false), wantFastMode: false},
266+ {name: "profile false cannot be bypassed", hostFastMode: true, profileFast: boolPtr(false), argFastMode: true, argFastSet: true, wantFastMode: false},
267+ }
268+ for _, tc := range cases {
269+ t.Run(tc.name, func(t *testing.T) {
270+ root := t.TempDir()
271+ home := filepath.Join(root, "home")
272+ zotHome := filepath.Join(root, "zot-home")
273+ project := filepath.Join(root, "repo")
274+ profilesDir := filepath.Join(home, ".agents", "agents")
275+ if err := os.MkdirAll(profilesDir, 0o755); err != nil {
276+ t.Fatal(err)
277+ }
278+ if err := os.MkdirAll(project, 0o755); err != nil {
279+ t.Fatal(err)
280+ }
281+ t.Setenv("HOME", home)
282+ t.Setenv("ZOT_HOME", zotHome)
283+ t.Setenv("ZOT_AGENT_PROFILES", profilesDir)
284+
285+ cfg := Config{Provider: "openai", Model: "gpt-5"}
286+ cfg.FastMode = &tc.hostFastMode
287+ if err := SaveConfig(cfg); err != nil {
288+ t.Fatal(err)
289+ }
290+ profile := "---\nname: reviewer\n"
291+ if tc.profileFast != nil {
292+ profile += fmt.Sprintf("fastMode: %t\n", *tc.profileFast)
293+ }
294+ profile += "---\nReview the requested scope.\n"
295+ if err := os.WriteFile(filepath.Join(profilesDir, "reviewer.md"), []byte(profile), 0o600); err != nil {
296+ t.Fatal(err)
297+ }
298+
299+ r, err := Resolve(Args{
300+ CWD: project,
301+ Subagent: "reviewer",
302+ FastMode: tc.argFastMode,
303+ FastModeSet: tc.argFastSet,
304+ }, false)
305+ if err != nil {
306+ t.Fatal(err)
307+ }
308+ if r.FastMode != tc.wantFastMode {
309+ t.Fatalf("FastMode = %v, want %v", r.FastMode, tc.wantFastMode)
310+ }
311+ })
312+ }
313+}
314+
251 func TestReadAgentsContextMissingFilesIsEmpty(t *testing.T) {315 func TestReadAgentsContextMissingFilesIsEmpty(t *testing.T) {
252 files := loadAgentsContext(t.TempDir(), t.TempDir())316 files := loadAgentsContext(t.TempDir(), t.TempDir())
253 if len(files) != 0 {317 if len(files) != 0 {
M packages/agent/modes/swarm_slash.go +4 -1
@@ -99,6 +99,7 @@ func (i *Interactive) runSwarm(ctx context.Context, args []string) {
9999 // Anything that isn't a recognised flag terminates parsing and
100100 // the rest becomes the task; only leading flags are consumed.
101101 model, provider, reasoning, subagent, task := parseSpawnFlags(rest)
102+ var fastModeOverride *bool
102103 if task == "" {
103104 i.swarmStatus("", "/swarm new: missing task (after any spawn flags)")
104105 return
@@ -118,6 +119,7 @@ func (i *Interactive) runSwarm(ctx context.Context, args []string) {
118119 return
119120 }
120121 subagent = profile.Name
122+ fastModeOverride = profile.FastMode
121123 if model == "" && provider == "" {
122124 profileProvider, profileModel := profile.ModelSelection()
123125 if profileProvider != "" && profileModel != "" {
@@ -140,7 +142,8 @@ func (i *Interactive) runSwarm(ctx context.Context, args []string) {
140142 reasoning = normalizedReasoning
141143 }
142144 a, err := i.cfg.Swarm.SpawnReq(ctx, swarm.SpawnRequest{
143- Task: task, Model: model, Provider: provider, Reasoning: reasoning, Subagent: subagent,
145+ Task: task, Model: model, Provider: provider, Reasoning: reasoning,
146+ FastMode: fastModeOverride, Subagent: subagent,
144147 })
145148 if err != nil {
146149 i.swarmStatus("", "spawn: "+err.Error())
@@ -99,6 +99,7 @@ func (i *Interactive) runSwarm(ctx context.Context, args []string) {
99 // Anything that isn't a recognised flag terminates parsing and99 // Anything that isn't a recognised flag terminates parsing and
100 // the rest becomes the task; only leading flags are consumed.100 // the rest becomes the task; only leading flags are consumed.
101 model, provider, reasoning, subagent, task := parseSpawnFlags(rest)101 model, provider, reasoning, subagent, task := parseSpawnFlags(rest)
102+ var fastModeOverride *bool
102 if task == "" {103 if task == "" {
103 i.swarmStatus("", "/swarm new: missing task (after any spawn flags)")104 i.swarmStatus("", "/swarm new: missing task (after any spawn flags)")
104 return105 return
@@ -118,6 +119,7 @@ func (i *Interactive) runSwarm(ctx context.Context, args []string) {
118 return119 return
119 }120 }
120 subagent = profile.Name121 subagent = profile.Name
122+ fastModeOverride = profile.FastMode
121 if model == "" && provider == "" {123 if model == "" && provider == "" {
122 profileProvider, profileModel := profile.ModelSelection()124 profileProvider, profileModel := profile.ModelSelection()
123 if profileProvider != "" && profileModel != "" {125 if profileProvider != "" && profileModel != "" {
@@ -140,7 +142,8 @@ func (i *Interactive) runSwarm(ctx context.Context, args []string) {
140 reasoning = normalizedReasoning142 reasoning = normalizedReasoning
141 }143 }
142 a, err := i.cfg.Swarm.SpawnReq(ctx, swarm.SpawnRequest{144 a, err := i.cfg.Swarm.SpawnReq(ctx, swarm.SpawnRequest{
143- Task: task, Model: model, Provider: provider, Reasoning: reasoning, Subagent: subagent,145+ Task: task, Model: model, Provider: provider, Reasoning: reasoning,
146+ FastMode: fastModeOverride, Subagent: subagent,
144 })147 })
145 if err != nil {148 if err != nil {
146 i.swarmStatus("", "spawn: "+err.Error())149 i.swarmStatus("", "spawn: "+err.Error())
M packages/agent/modes/swarm_slash_test.go +32 -0
@@ -7,6 +7,7 @@ import (
77 "testing"
88 "time"
99
10+ "github.com/patriceckhart/zot/packages/agent/subagents"
1011 "github.com/patriceckhart/zot/packages/agent/swarm"
1112 )
1213
@@ -110,6 +111,37 @@ func TestRunSwarmNewSpawnsAgent(t *testing.T) {
110111 }
111112 }
112113
114+func TestRunSwarmNamedProfileAppliesFastModeRestriction(t *testing.T) {
115+ root := t.TempDir()
116+ profileFastMode := false
117+ f := swarm.New(swarm.Config{
118+ Root: root,
119+ RepoRoot: root,
120+ FastMode: true,
121+ NewRunner: func(*swarm.Agent) swarm.Runner {
122+ return swarm.RunnerFunc(func(ctx context.Context, _ swarm.Sink) error {
123+ <-ctx.Done()
124+ return ctx.Err()
125+ })
126+ },
127+ })
128+ defer f.StopAll()
129+ iv := &Interactive{swarmDialog: newSwarmDialog(), dirty: make(chan struct{}, 1)}
130+ iv.cfg.Swarm = f
131+ iv.cfg.ResolveSubagent = func(name string) (*subagents.Profile, error) {
132+ if name != "reviewer" {
133+ return nil, nil
134+ }
135+ return &subagents.Profile{Name: name, FastMode: &profileFastMode}, nil
136+ }
137+
138+ iv.runSwarm(context.Background(), []string{"new", "--agent", "reviewer", "review", "auth"})
139+ agents := f.List()
140+ if len(agents) != 1 || agents[0].FastMode {
141+ t.Fatalf("agent fast mode = %#v, want disabled by profile", agents)
142+ }
143+}
144+
113145 // TestRunSwarmSendDeliversToAgentInbox spins up a real agent with a
114146 // fake Runner whose only job is to forward inbox lines to a channel,
115147 // then asserts the /swarm send <id> <text...> path routes through
@@ -7,6 +7,7 @@ import (
7 "testing"7 "testing"
8 "time"8 "time"
9 9
10+ "github.com/patriceckhart/zot/packages/agent/subagents"
10 "github.com/patriceckhart/zot/packages/agent/swarm"11 "github.com/patriceckhart/zot/packages/agent/swarm"
11 )12 )
12 13
@@ -110,6 +111,37 @@ func TestRunSwarmNewSpawnsAgent(t *testing.T) {
110 }111 }
111 }112 }
112 113
114+func TestRunSwarmNamedProfileAppliesFastModeRestriction(t *testing.T) {
115+ root := t.TempDir()
116+ profileFastMode := false
117+ f := swarm.New(swarm.Config{
118+ Root: root,
119+ RepoRoot: root,
120+ FastMode: true,
121+ NewRunner: func(*swarm.Agent) swarm.Runner {
122+ return swarm.RunnerFunc(func(ctx context.Context, _ swarm.Sink) error {
123+ <-ctx.Done()
124+ return ctx.Err()
125+ })
126+ },
127+ })
128+ defer f.StopAll()
129+ iv := &Interactive{swarmDialog: newSwarmDialog(), dirty: make(chan struct{}, 1)}
130+ iv.cfg.Swarm = f
131+ iv.cfg.ResolveSubagent = func(name string) (*subagents.Profile, error) {
132+ if name != "reviewer" {
133+ return nil, nil
134+ }
135+ return &subagents.Profile{Name: name, FastMode: &profileFastMode}, nil
136+ }
137+
138+ iv.runSwarm(context.Background(), []string{"new", "--agent", "reviewer", "review", "auth"})
139+ agents := f.List()
140+ if len(agents) != 1 || agents[0].FastMode {
141+ t.Fatalf("agent fast mode = %#v, want disabled by profile", agents)
142+ }
143+}
144+
113 // TestRunSwarmSendDeliversToAgentInbox spins up a real agent with a145 // TestRunSwarmSendDeliversToAgentInbox spins up a real agent with a
114 // fake Runner whose only job is to forward inbox lines to a channel,146 // fake Runner whose only job is to forward inbox lines to a channel,
115 // then asserts the /swarm send <id> <text...> path routes through147 // then asserts the /swarm send <id> <text...> path routes through
M packages/agent/subagents/profiles.go +15 -2
@@ -30,6 +30,9 @@ type Profile struct {
3030 Provider string
3131 Thinking string
3232
33+ // Nil means the profile did not specify whether fast mode is enabled.
34+ FastMode *bool
35+
3336 // SystemPromptMode is "append" (the default) or "replace".
3437 SystemPromptMode string
3538
@@ -125,7 +128,7 @@ func SystemPromptAddendum(profiles []*Profile) string {
125128 }
126129 var sb strings.Builder
127130 sb.WriteString("[subagents_list]\n")
128- sb.WriteString("Named subagents available to swarm_spawn. Choose the profile whose description best matches the independent task and pass its name as the tool's agent field. The selected profile's instructions, model, thinking level, and tool limits are applied to the child.\n")
131+ sb.WriteString("Named subagents available to swarm_spawn. Choose the profile whose description best matches the independent task and pass its name as the tool's agent field. The selected profile's instructions, model, thinking level, tool limits, and fast-mode preference are applied to the child.\n")
129132 for _, profile := range profiles {
130133 if profile == nil {
131134 continue
@@ -135,7 +138,7 @@ func SystemPromptAddendum(profiles []*Profile) string {
135138 if description == "" {
136139 description = "(no description)"
137140 }
138- metadata := make([]string, 0, 4)
141+ metadata := make([]string, 0, 5)
139142 if model := manifestValue(profile.Model); model != "" {
140143 metadata = append(metadata, "model="+model)
141144 }
@@ -148,6 +151,9 @@ func SystemPromptAddendum(profiles []*Profile) string {
148151 if len(profile.Tools) > 0 {
149152 metadata = append(metadata, "tools="+manifestValue(strings.Join(profile.Tools, ",")))
150153 }
154+ if profile.FastMode != nil {
155+ metadata = append(metadata, "fastMode="+strconv.FormatBool(*profile.FastMode))
156+ }
151157 if len(metadata) > 0 {
152158 fmt.Fprintf(&sb, "- %s [%s]: %s\n", name, strings.Join(metadata, " "), description)
153159 } else {
@@ -263,6 +269,13 @@ func load(path, source string) (*Profile, error) {
263269 }
264270 profile.InheritSkills = &value
265271 }
272+ if raw, ok := values["fastmode"]; ok {
273+ value, parsed := parseOptionalBool(raw)
274+ if !parsed {
275+ return nil, fmt.Errorf("fastMode must be true or false")
276+ }
277+ profile.FastMode = &value
278+ }
266279 return profile, nil
267280 }
268281
@@ -30,6 +30,9 @@ type Profile struct {
30 Provider string30 Provider string
31 Thinking string31 Thinking string
32 32
33+ // Nil means the profile did not specify whether fast mode is enabled.
34+ FastMode *bool
35+
33 // SystemPromptMode is "append" (the default) or "replace".36 // SystemPromptMode is "append" (the default) or "replace".
34 SystemPromptMode string37 SystemPromptMode string
35 38
@@ -125,7 +128,7 @@ func SystemPromptAddendum(profiles []*Profile) string {
125 }128 }
126 var sb strings.Builder129 var sb strings.Builder
127 sb.WriteString("[subagents_list]\n")130 sb.WriteString("[subagents_list]\n")
128- sb.WriteString("Named subagents available to swarm_spawn. Choose the profile whose description best matches the independent task and pass its name as the tool's agent field. The selected profile's instructions, model, thinking level, and tool limits are applied to the child.\n")131+ sb.WriteString("Named subagents available to swarm_spawn. Choose the profile whose description best matches the independent task and pass its name as the tool's agent field. The selected profile's instructions, model, thinking level, tool limits, and fast-mode preference are applied to the child.\n")
129 for _, profile := range profiles {132 for _, profile := range profiles {
130 if profile == nil {133 if profile == nil {
131 continue134 continue
@@ -135,7 +138,7 @@ func SystemPromptAddendum(profiles []*Profile) string {
135 if description == "" {138 if description == "" {
136 description = "(no description)"139 description = "(no description)"
137 }140 }
138- metadata := make([]string, 0, 4)141+ metadata := make([]string, 0, 5)
139 if model := manifestValue(profile.Model); model != "" {142 if model := manifestValue(profile.Model); model != "" {
140 metadata = append(metadata, "model="+model)143 metadata = append(metadata, "model="+model)
141 }144 }
@@ -148,6 +151,9 @@ func SystemPromptAddendum(profiles []*Profile) string {
148 if len(profile.Tools) > 0 {151 if len(profile.Tools) > 0 {
149 metadata = append(metadata, "tools="+manifestValue(strings.Join(profile.Tools, ",")))152 metadata = append(metadata, "tools="+manifestValue(strings.Join(profile.Tools, ",")))
150 }153 }
154+ if profile.FastMode != nil {
155+ metadata = append(metadata, "fastMode="+strconv.FormatBool(*profile.FastMode))
156+ }
151 if len(metadata) > 0 {157 if len(metadata) > 0 {
152 fmt.Fprintf(&sb, "- %s [%s]: %s\n", name, strings.Join(metadata, " "), description)158 fmt.Fprintf(&sb, "- %s [%s]: %s\n", name, strings.Join(metadata, " "), description)
153 } else {159 } else {
@@ -263,6 +269,13 @@ func load(path, source string) (*Profile, error) {
263 }269 }
264 profile.InheritSkills = &value270 profile.InheritSkills = &value
265 }271 }
272+ if raw, ok := values["fastmode"]; ok {
273+ value, parsed := parseOptionalBool(raw)
274+ if !parsed {
275+ return nil, fmt.Errorf("fastMode must be true or false")
276+ }
277+ profile.FastMode = &value
278+ }
266 return profile, nil279 return profile, nil
267 }280 }
268 281
M packages/agent/subagents/profiles_test.go +13 -1
@@ -27,6 +27,7 @@ thinking: max
2727 systemPromptMode: replace
2828 inheritProjectContext: false
2929 inheritSkills: false
30+fastMode: false
3031 ---
3132 Review the requested scope without editing it.
3233 `)
@@ -66,6 +67,13 @@ Implement the requested change.
6667 if reviewer.InheritSkills == nil || *reviewer.InheritSkills {
6768 t.Fatalf("inherit skills = %#v, want false", reviewer.InheritSkills)
6869 }
70+ if reviewer.FastMode == nil || *reviewer.FastMode {
71+ t.Fatalf("fast mode = %#v, want false", reviewer.FastMode)
72+ }
73+ implementer := Find(profiles, "implementer")
74+ if implementer == nil || implementer.FastMode != nil {
75+ t.Fatalf("unset fast mode = %#v, want nil", implementer)
76+ }
6977 }
7078
7179 func TestDiscoverResolvesConfiguredRelativePathAgainstCWD(t *testing.T) {
@@ -155,6 +163,7 @@ func TestLoadRejectsInvalidClosedSchemaMetadata(t *testing.T) {
155163 {name: "invalid-prompt-mode", field: "systemPromptMode: merge"},
156164 {name: "invalid-project-context", field: "inheritProjectContext: sometimes"},
157165 {name: "invalid-skills", field: "inheritSkills: sometimes"},
166+ {name: "invalid-fast-mode", field: "fastMode: sometimes"},
158167 }
159168 for _, tc := range cases {
160169 t.Run(tc.name, func(t *testing.T) {
@@ -188,11 +197,12 @@ func TestSystemPromptAddendumIsCompactAndDoesNotExposeBodyOrPath(t *testing.T) {
188197 SystemPrompt: "secret instructions that belong only to the child",
189198 Model: "openai-codex/gpt-5",
190199 Thinking: "max",
200+ FastMode: boolPtr(false),
191201 Tools: []string{"read", "bash"},
192202 Path: "/private/profile.md",
193203 }}
194204 got := SystemPromptAddendum(profiles)
195- for _, want := range []string{"[subagents_list]", "[/subagents_list]", "reviewer", "Read-only reviewer", "model=openai-codex/gpt-5", "thinking=max"} {
205+ for _, want := range []string{"[subagents_list]", "[/subagents_list]", "reviewer", "Read-only reviewer", "model=openai-codex/gpt-5", "thinking=max", "fastMode=false"} {
196206 if !strings.Contains(got, want) {
197207 t.Fatalf("addendum missing %q:\n%s", want, got)
198208 }
@@ -204,6 +214,8 @@ func TestSystemPromptAddendumIsCompactAndDoesNotExposeBodyOrPath(t *testing.T) {
204214 }
205215 }
206216
217+func boolPtr(value bool) *bool { return &value }
218+
207219 func writeProfile(t *testing.T, path, content string) {
208220 t.Helper()
209221 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
@@ -27,6 +27,7 @@ thinking: max
27 systemPromptMode: replace27 systemPromptMode: replace
28 inheritProjectContext: false28 inheritProjectContext: false
29 inheritSkills: false29 inheritSkills: false
30+fastMode: false
30 ---31 ---
31 Review the requested scope without editing it.32 Review the requested scope without editing it.
32 `)33 `)
@@ -66,6 +67,13 @@ Implement the requested change.
66 if reviewer.InheritSkills == nil || *reviewer.InheritSkills {67 if reviewer.InheritSkills == nil || *reviewer.InheritSkills {
67 t.Fatalf("inherit skills = %#v, want false", reviewer.InheritSkills)68 t.Fatalf("inherit skills = %#v, want false", reviewer.InheritSkills)
68 }69 }
70+ if reviewer.FastMode == nil || *reviewer.FastMode {
71+ t.Fatalf("fast mode = %#v, want false", reviewer.FastMode)
72+ }
73+ implementer := Find(profiles, "implementer")
74+ if implementer == nil || implementer.FastMode != nil {
75+ t.Fatalf("unset fast mode = %#v, want nil", implementer)
76+ }
69 }77 }
70 78
71 func TestDiscoverResolvesConfiguredRelativePathAgainstCWD(t *testing.T) {79 func TestDiscoverResolvesConfiguredRelativePathAgainstCWD(t *testing.T) {
@@ -155,6 +163,7 @@ func TestLoadRejectsInvalidClosedSchemaMetadata(t *testing.T) {
155 {name: "invalid-prompt-mode", field: "systemPromptMode: merge"},163 {name: "invalid-prompt-mode", field: "systemPromptMode: merge"},
156 {name: "invalid-project-context", field: "inheritProjectContext: sometimes"},164 {name: "invalid-project-context", field: "inheritProjectContext: sometimes"},
157 {name: "invalid-skills", field: "inheritSkills: sometimes"},165 {name: "invalid-skills", field: "inheritSkills: sometimes"},
166+ {name: "invalid-fast-mode", field: "fastMode: sometimes"},
158 }167 }
159 for _, tc := range cases {168 for _, tc := range cases {
160 t.Run(tc.name, func(t *testing.T) {169 t.Run(tc.name, func(t *testing.T) {
@@ -188,11 +197,12 @@ func TestSystemPromptAddendumIsCompactAndDoesNotExposeBodyOrPath(t *testing.T) {
188 SystemPrompt: "secret instructions that belong only to the child",197 SystemPrompt: "secret instructions that belong only to the child",
189 Model: "openai-codex/gpt-5",198 Model: "openai-codex/gpt-5",
190 Thinking: "max",199 Thinking: "max",
200+ FastMode: boolPtr(false),
191 Tools: []string{"read", "bash"},201 Tools: []string{"read", "bash"},
192 Path: "/private/profile.md",202 Path: "/private/profile.md",
193 }}203 }}
194 got := SystemPromptAddendum(profiles)204 got := SystemPromptAddendum(profiles)
195- for _, want := range []string{"[subagents_list]", "[/subagents_list]", "reviewer", "Read-only reviewer", "model=openai-codex/gpt-5", "thinking=max"} {205+ for _, want := range []string{"[subagents_list]", "[/subagents_list]", "reviewer", "Read-only reviewer", "model=openai-codex/gpt-5", "thinking=max", "fastMode=false"} {
196 if !strings.Contains(got, want) {206 if !strings.Contains(got, want) {
197 t.Fatalf("addendum missing %q:\n%s", want, got)207 t.Fatalf("addendum missing %q:\n%s", want, got)
198 }208 }
@@ -204,6 +214,8 @@ func TestSystemPromptAddendumIsCompactAndDoesNotExposeBodyOrPath(t *testing.T) {
204 }214 }
205 }215 }
206 216
217+func boolPtr(value bool) *bool { return &value }
218+
207 func writeProfile(t *testing.T, path, content string) {219 func writeProfile(t *testing.T, path, content string) {
208 t.Helper()220 t.Helper()
209 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {221 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
M packages/agent/swarm/fast_mode_test.go +45 -0
@@ -58,6 +58,51 @@ func TestSwarmFastModePropagatesToChild(t *testing.T) {
5858 }
5959 }
6060
61+func TestSpawnRequestFastModeIsBoundByHostSetting(t *testing.T) {
62+ falseValue, trueValue := false, true
63+ cases := []struct {
64+ name string
65+ hostFast bool
66+ profileFast *bool
67+ want bool
68+ }{
69+ {name: "unset inherits enabled host", hostFast: true, want: true},
70+ {name: "unset inherits disabled host", hostFast: false, want: false},
71+ {name: "false disables enabled host", hostFast: true, profileFast: &falseValue, want: false},
72+ {name: "true cannot enable disabled host", hostFast: false, profileFast: &trueValue, want: false},
73+ {name: "true preserves enabled host", hostFast: true, profileFast: &trueValue, want: true},
74+ }
75+ for _, tc := range cases {
76+ t.Run(tc.name, func(t *testing.T) {
77+ root := t.TempDir()
78+ f := New(Config{
79+ Root: root,
80+ RepoRoot: root,
81+ FastMode: tc.hostFast,
82+ NewRunner: func(*Agent) Runner {
83+ return RunnerFunc(func(context.Context, Sink) error { return nil })
84+ },
85+ })
86+ a, err := f.SpawnReq(context.Background(), SpawnRequest{Task: "x", FastMode: tc.profileFast})
87+ if err != nil {
88+ t.Fatalf("spawn: %v", err)
89+ }
90+ a.Wait()
91+ if a.FastMode != tc.want {
92+ t.Fatalf("FastMode = %v, want %v", a.FastMode, tc.want)
93+ }
94+ args := defaultChildArgs("/zot", a, "/session", "/inbox")
95+ wantFlag := "--no-fast-mode"
96+ if tc.want {
97+ wantFlag = "--fast-mode"
98+ }
99+ if !containsArg(args, wantFlag) {
100+ t.Fatalf("child args = %v, want %s", args, wantFlag)
101+ }
102+ })
103+ }
104+}
105+
61106 func TestSwarmReloadPreservesExplicitFastModeOff(t *testing.T) {
62107 root := t.TempDir()
63108 newSwarm := func(fastMode bool) *Swarm {
@@ -58,6 +58,51 @@ func TestSwarmFastModePropagatesToChild(t *testing.T) {
58 }58 }
59 }59 }
60 60
61+func TestSpawnRequestFastModeIsBoundByHostSetting(t *testing.T) {
62+ falseValue, trueValue := false, true
63+ cases := []struct {
64+ name string
65+ hostFast bool
66+ profileFast *bool
67+ want bool
68+ }{
69+ {name: "unset inherits enabled host", hostFast: true, want: true},
70+ {name: "unset inherits disabled host", hostFast: false, want: false},
71+ {name: "false disables enabled host", hostFast: true, profileFast: &falseValue, want: false},
72+ {name: "true cannot enable disabled host", hostFast: false, profileFast: &trueValue, want: false},
73+ {name: "true preserves enabled host", hostFast: true, profileFast: &trueValue, want: true},
74+ }
75+ for _, tc := range cases {
76+ t.Run(tc.name, func(t *testing.T) {
77+ root := t.TempDir()
78+ f := New(Config{
79+ Root: root,
80+ RepoRoot: root,
81+ FastMode: tc.hostFast,
82+ NewRunner: func(*Agent) Runner {
83+ return RunnerFunc(func(context.Context, Sink) error { return nil })
84+ },
85+ })
86+ a, err := f.SpawnReq(context.Background(), SpawnRequest{Task: "x", FastMode: tc.profileFast})
87+ if err != nil {
88+ t.Fatalf("spawn: %v", err)
89+ }
90+ a.Wait()
91+ if a.FastMode != tc.want {
92+ t.Fatalf("FastMode = %v, want %v", a.FastMode, tc.want)
93+ }
94+ args := defaultChildArgs("/zot", a, "/session", "/inbox")
95+ wantFlag := "--no-fast-mode"
96+ if tc.want {
97+ wantFlag = "--fast-mode"
98+ }
99+ if !containsArg(args, wantFlag) {
100+ t.Fatalf("child args = %v, want %s", args, wantFlag)
101+ }
102+ })
103+ }
104+}
105+
61 func TestSwarmReloadPreservesExplicitFastModeOff(t *testing.T) {106 func TestSwarmReloadPreservesExplicitFastModeOff(t *testing.T) {
62 root := t.TempDir()107 root := t.TempDir()
63 newSwarm := func(fastMode bool) *Swarm {108 newSwarm := func(fastMode bool) *Swarm {
M packages/agent/swarm/swarm.go +10 -2
@@ -217,13 +217,18 @@ func (f *Swarm) agentStateDir(id string) string {
217217 // chosen model regardless of the parent's current selection. A named
218218 // Subagent is passed to the child as --subagent, where it resolves the
219219 // markdown profile and applies its instructions. Reasoning is passed
220-// as --reasoning when set.
220+// as --reasoning when set. FastMode restricts the host setting when a
221+// selected profile declares a fastMode preference.
221222 type SpawnRequest struct {
222223 Task string
223224 Model string // optional override; child resolves default if empty
224225 Provider string // optional override; usually paired with Model
225226 Reasoning string // optional reasoning/thinking level override
226- Subagent string // optional named markdown profile
227+ // FastMode is an optional profile restriction. A non-nil false value
228+ // disables fast mode for this child; true still cannot enable fast mode
229+ // when Config.FastMode is false. Nil inherits Config.FastMode.
230+ FastMode *bool
231+ Subagent string // optional named markdown profile
227232 }
228233
229234 // Spawn creates a new Agent for the given task, allocates its
@@ -258,6 +263,9 @@ func (f *Swarm) SpawnReq(ctx context.Context, req SpawnRequest) (*Agent, error)
258263 fastMode := f.cfg.FastMode
259264 sessionID := f.activeSession
260265 f.mu.Unlock()
266+ if req.FastMode != nil {
267+ fastMode = fastMode && *req.FastMode
268+ }
261269
262270 stateDir := f.agentStateDir(id)
263271 if err := os.MkdirAll(stateDir, 0o755); err != nil {
@@ -217,13 +217,18 @@ func (f *Swarm) agentStateDir(id string) string {
217 // chosen model regardless of the parent's current selection. A named217 // chosen model regardless of the parent's current selection. A named
218 // Subagent is passed to the child as --subagent, where it resolves the218 // Subagent is passed to the child as --subagent, where it resolves the
219 // markdown profile and applies its instructions. Reasoning is passed219 // markdown profile and applies its instructions. Reasoning is passed
220-// as --reasoning when set.220+// as --reasoning when set. FastMode restricts the host setting when a
221+// selected profile declares a fastMode preference.
221 type SpawnRequest struct {222 type SpawnRequest struct {
222 Task string223 Task string
223 Model string // optional override; child resolves default if empty224 Model string // optional override; child resolves default if empty
224 Provider string // optional override; usually paired with Model225 Provider string // optional override; usually paired with Model
225 Reasoning string // optional reasoning/thinking level override226 Reasoning string // optional reasoning/thinking level override
226- Subagent string // optional named markdown profile227+ // FastMode is an optional profile restriction. A non-nil false value
228+ // disables fast mode for this child; true still cannot enable fast mode
229+ // when Config.FastMode is false. Nil inherits Config.FastMode.
230+ FastMode *bool
231+ Subagent string // optional named markdown profile
227 }232 }
228 233
229 // Spawn creates a new Agent for the given task, allocates its234 // Spawn creates a new Agent for the given task, allocates its
@@ -258,6 +263,9 @@ func (f *Swarm) SpawnReq(ctx context.Context, req SpawnRequest) (*Agent, error)
258 fastMode := f.cfg.FastMode263 fastMode := f.cfg.FastMode
259 sessionID := f.activeSession264 sessionID := f.activeSession
260 f.mu.Unlock()265 f.mu.Unlock()
266+ if req.FastMode != nil {
267+ fastMode = fastMode && *req.FastMode
268+ }
261 269
262 stateDir := f.agentStateDir(id)270 stateDir := f.agentStateDir(id)
263 if err := os.MkdirAll(stateDir, 0o755); err != nil {271 if err := os.MkdirAll(stateDir, 0o755); err != nil {
M packages/agent/tools/swarm_spawn.go +5 -2
@@ -69,7 +69,7 @@ const swarmSpawnSchema = `{
6969 },
7070 "agent": {
7171 "type": "string",
72- "description": "Optional named markdown profile from [subagents_list]. The child applies that profile's system prompt, model, thinking level, and tool limits. Omit for a generic child."
72+ "description": "Optional named markdown profile from [subagents_list]. The child applies that profile's system prompt, model, thinking level, tool limits, and fast-mode preference. Omit for a generic child."
7373 },
7474 "model": {
7575 "type": "string",
@@ -95,7 +95,7 @@ const swarmSpawnSchema = `{
9595
9696 func (t *SwarmSpawnTool) Name() string { return "swarm_spawn" }
9797 func (t *SwarmSpawnTool) Description() string {
98- return "Spawn a background sub-agent to work on a parallel sub-task. Optionally select a named markdown profile with agent and set its model/provider/reasoning. Returns the sub-agent id immediately; the sub-agent keeps running while this conversation continues. The sub-agent shares this working directory."
98+ return "Spawn a background sub-agent to work on a parallel sub-task. Optionally select a named markdown profile with agent and set its model/provider/reasoning. Named profiles may restrict fast mode but cannot enable it when the host setting is off. Returns the sub-agent id immediately; the sub-agent keeps running while this conversation continues. The sub-agent shares this working directory."
9999 }
100100 func (t *SwarmSpawnTool) Schema() json.RawMessage { return json.RawMessage(swarmSpawnSchema) }
101101
@@ -117,6 +117,7 @@ func (t *SwarmSpawnTool) Execute(ctx context.Context, raw json.RawMessage, progr
117117
118118 agentName := strings.TrimSpace(a.Agent)
119119 var profile *subagents.Profile
120+ var fastModeOverride *bool
120121 if agentName != "" {
121122 if t.ResolveSubagent == nil {
122123 return protocolToolError("swarm_spawn: named subagent profiles are unavailable")
@@ -130,6 +131,7 @@ func (t *SwarmSpawnTool) Execute(ctx context.Context, raw json.RawMessage, progr
130131 return protocolToolError("swarm_spawn: unknown subagent profile " + agentName)
131132 }
132133 agentName = profile.Name
134+ fastModeOverride = profile.FastMode
133135 }
134136
135137 model := strings.TrimSpace(a.Model)
@@ -178,6 +180,7 @@ func (t *SwarmSpawnTool) Execute(ctx context.Context, raw json.RawMessage, progr
178180 Model: model,
179181 Provider: providerID,
180182 Reasoning: reasoning,
183+ FastMode: fastModeOverride,
181184 Subagent: agentName,
182185 })
183186 if err != nil {
@@ -69,7 +69,7 @@ const swarmSpawnSchema = `{
69 },69 },
70 "agent": {70 "agent": {
71 "type": "string",71 "type": "string",
72- "description": "Optional named markdown profile from [subagents_list]. The child applies that profile's system prompt, model, thinking level, and tool limits. Omit for a generic child."72+ "description": "Optional named markdown profile from [subagents_list]. The child applies that profile's system prompt, model, thinking level, tool limits, and fast-mode preference. Omit for a generic child."
73 },73 },
74 "model": {74 "model": {
75 "type": "string",75 "type": "string",
@@ -95,7 +95,7 @@ const swarmSpawnSchema = `{
95 95
96 func (t *SwarmSpawnTool) Name() string { return "swarm_spawn" }96 func (t *SwarmSpawnTool) Name() string { return "swarm_spawn" }
97 func (t *SwarmSpawnTool) Description() string {97 func (t *SwarmSpawnTool) Description() string {
98- return "Spawn a background sub-agent to work on a parallel sub-task. Optionally select a named markdown profile with agent and set its model/provider/reasoning. Returns the sub-agent id immediately; the sub-agent keeps running while this conversation continues. The sub-agent shares this working directory."98+ return "Spawn a background sub-agent to work on a parallel sub-task. Optionally select a named markdown profile with agent and set its model/provider/reasoning. Named profiles may restrict fast mode but cannot enable it when the host setting is off. Returns the sub-agent id immediately; the sub-agent keeps running while this conversation continues. The sub-agent shares this working directory."
99 }99 }
100 func (t *SwarmSpawnTool) Schema() json.RawMessage { return json.RawMessage(swarmSpawnSchema) }100 func (t *SwarmSpawnTool) Schema() json.RawMessage { return json.RawMessage(swarmSpawnSchema) }
101 101
@@ -117,6 +117,7 @@ func (t *SwarmSpawnTool) Execute(ctx context.Context, raw json.RawMessage, progr
117 117
118 agentName := strings.TrimSpace(a.Agent)118 agentName := strings.TrimSpace(a.Agent)
119 var profile *subagents.Profile119 var profile *subagents.Profile
120+ var fastModeOverride *bool
120 if agentName != "" {121 if agentName != "" {
121 if t.ResolveSubagent == nil {122 if t.ResolveSubagent == nil {
122 return protocolToolError("swarm_spawn: named subagent profiles are unavailable")123 return protocolToolError("swarm_spawn: named subagent profiles are unavailable")
@@ -130,6 +131,7 @@ func (t *SwarmSpawnTool) Execute(ctx context.Context, raw json.RawMessage, progr
130 return protocolToolError("swarm_spawn: unknown subagent profile " + agentName)131 return protocolToolError("swarm_spawn: unknown subagent profile " + agentName)
131 }132 }
132 agentName = profile.Name133 agentName = profile.Name
134+ fastModeOverride = profile.FastMode
133 }135 }
134 136
135 model := strings.TrimSpace(a.Model)137 model := strings.TrimSpace(a.Model)
@@ -178,6 +180,7 @@ func (t *SwarmSpawnTool) Execute(ctx context.Context, raw json.RawMessage, progr
178 Model: model,180 Model: model,
179 Provider: providerID,181 Provider: providerID,
180 Reasoning: reasoning,182 Reasoning: reasoning,
183+ FastMode: fastModeOverride,
181 Subagent: agentName,184 Subagent: agentName,
182 })185 })
183 if err != nil {186 if err != nil {
M packages/agent/tools/swarm_spawn_test.go +38 -0
@@ -109,6 +109,44 @@ func TestSwarmSpawnSelectsProfileAndReasoning(t *testing.T) {
109109 }
110110 }
111111
112+func TestSwarmSpawnAppliesProfileFastModeRestriction(t *testing.T) {
113+ root := t.TempDir()
114+ profileFastMode := false
115+ f := swarm.New(swarm.Config{
116+ Root: filepath.Join(root, "swarm"),
117+ RepoRoot: root,
118+ FastMode: true,
119+ NewRunner: func(*swarm.Agent) swarm.Runner {
120+ return noopSwarmRunner{}
121+ },
122+ })
123+ tool := &SwarmSpawnTool{
124+ Swarm: f,
125+ Enabled: func() bool { return true },
126+ ResolveSubagent: func(name string) (*subagents.Profile, error) {
127+ if name != "reviewer" {
128+ return nil, nil
129+ }
130+ return &subagents.Profile{Name: name, FastMode: &profileFastMode}, nil
131+ },
132+ }
133+
134+ res, err := tool.Execute(context.Background(), json.RawMessage(`{"task":"review auth","agent":"reviewer"}`), nil)
135+ if err != nil {
136+ t.Fatal(err)
137+ }
138+ if res.IsError {
139+ t.Fatalf("unexpected tool error: %s", textResult(res.Content))
140+ }
141+ agents := f.List()
142+ if len(agents) != 1 || agents[0].FastMode {
143+ t.Fatalf("agent fast mode = %#v, want disabled by profile", agents)
144+ }
145+ if got := res.Details.(map[string]any)["fast_mode"]; got != false {
146+ t.Fatalf("fast_mode detail = %v, want false", got)
147+ }
148+}
149+
112150 func TestSwarmSpawnUsesProfileReasoningWhenOmitted(t *testing.T) {
113151 tool := &SwarmSpawnTool{
114152 Swarm: newTestSwarm(t),
@@ -109,6 +109,44 @@ func TestSwarmSpawnSelectsProfileAndReasoning(t *testing.T) {
109 }109 }
110 }110 }
111 111
112+func TestSwarmSpawnAppliesProfileFastModeRestriction(t *testing.T) {
113+ root := t.TempDir()
114+ profileFastMode := false
115+ f := swarm.New(swarm.Config{
116+ Root: filepath.Join(root, "swarm"),
117+ RepoRoot: root,
118+ FastMode: true,
119+ NewRunner: func(*swarm.Agent) swarm.Runner {
120+ return noopSwarmRunner{}
121+ },
122+ })
123+ tool := &SwarmSpawnTool{
124+ Swarm: f,
125+ Enabled: func() bool { return true },
126+ ResolveSubagent: func(name string) (*subagents.Profile, error) {
127+ if name != "reviewer" {
128+ return nil, nil
129+ }
130+ return &subagents.Profile{Name: name, FastMode: &profileFastMode}, nil
131+ },
132+ }
133+
134+ res, err := tool.Execute(context.Background(), json.RawMessage(`{"task":"review auth","agent":"reviewer"}`), nil)
135+ if err != nil {
136+ t.Fatal(err)
137+ }
138+ if res.IsError {
139+ t.Fatalf("unexpected tool error: %s", textResult(res.Content))
140+ }
141+ agents := f.List()
142+ if len(agents) != 1 || agents[0].FastMode {
143+ t.Fatalf("agent fast mode = %#v, want disabled by profile", agents)
144+ }
145+ if got := res.Details.(map[string]any)["fast_mode"]; got != false {
146+ t.Fatalf("fast_mode detail = %v, want false", got)
147+ }
148+}
149+
112 func TestSwarmSpawnUsesProfileReasoningWhenOmitted(t *testing.T) {150 func TestSwarmSpawnUsesProfileReasoningWhenOmitted(t *testing.T) {
113 tool := &SwarmSpawnTool{151 tool := &SwarmSpawnTool{
114 Swarm: newTestSwarm(t),152 Swarm: newTestSwarm(t),

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 477

↑↓ moveenter openesc close