detached 6f4c43c

fix(subagents): isolate resident worker prompts

commit
6f4c43ceb666bd4ed3e8a42d16a0316a4be08104
author
brice · · 2026-08-24 10:40 UTC
parent
f9c8b6d
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +29 -9
M packages/agent/args.go +6 -3
@@ -28,9 +28,12 @@ const (
2828 type Args struct {
2929 Mode Mode
3030 Orchestrate bool
31- Provider string
32- Model string
33- APIKey string
31+ // ResidentChild marks internal child resolution so primary-only delegation
32+ // policies and profile metadata are not added to worker prompts.
33+ ResidentChild bool
34+ Provider string
35+ Model string
36+ APIKey string
3437
3538 BaseURL string // override provider base URL (for tests/self-hosted)
3639 SystemPrompt string
@@ -28,9 +28,12 @@ const (
28 type Args struct {28 type Args struct {
29 Mode Mode29 Mode Mode
30 Orchestrate bool30 Orchestrate bool
31- Provider string31+ // ResidentChild marks internal child resolution so primary-only delegation
32- Model string32+ // policies and profile metadata are not added to worker prompts.
33- APIKey string33+ ResidentChild bool
34+ Provider string
35+ Model string
36+ APIKey string
34 37
35 BaseURL string // override provider base URL (for tests/self-hosted)38 BaseURL string // override provider base URL (for tests/self-hosted)
36 SystemPrompt string39 SystemPrompt string
M packages/agent/build.go +2 -2
@@ -809,8 +809,8 @@ func Resolve(args Args, requireCred bool) (Resolved, error) {
809809 append_ = append(append_, skillAddendum)
810810 }
811811 interactiveMode := args.Mode == "" || args.Mode == ModeInteractive
812- primaryInteractive := interactiveMode
813- primaryOrchestrator := args.Orchestrate
812+ primaryInteractive := interactiveMode && !args.ResidentChild
813+ primaryOrchestrator := args.Orchestrate && !args.ResidentChild
814814 if (primaryInteractive || primaryOrchestrator) && autoSubagentsToolAllowed(args) {
815815 homeDir, _ := os.UserHomeDir()
816816 profiles, _ := subagents.Discover(args.CWD, homeDir)
@@ -809,8 +809,8 @@ func Resolve(args Args, requireCred bool) (Resolved, error) {
809 append_ = append(append_, skillAddendum)809 append_ = append(append_, skillAddendum)
810 }810 }
811 interactiveMode := args.Mode == "" || args.Mode == ModeInteractive811 interactiveMode := args.Mode == "" || args.Mode == ModeInteractive
812- primaryInteractive := interactiveMode812+ primaryInteractive := interactiveMode && !args.ResidentChild
813- primaryOrchestrator := args.Orchestrate813+ primaryOrchestrator := args.Orchestrate && !args.ResidentChild
814 if (primaryInteractive || primaryOrchestrator) && autoSubagentsToolAllowed(args) {814 if (primaryInteractive || primaryOrchestrator) && autoSubagentsToolAllowed(args) {
815 homeDir, _ := os.UserHomeDir()815 homeDir, _ := os.UserHomeDir()
816 profiles, _ := subagents.Discover(args.CWD, homeDir)816 profiles, _ := subagents.Discover(args.CWD, homeDir)
M packages/agent/build_orchestrator_test.go +18 -2
@@ -19,6 +19,13 @@ func TestResolveSeparatesInteractiveProactiveAndHeadlessStrictPolicies(t *testin
1919 if err := os.MkdirAll(project, 0o755); err != nil {
2020 t.Fatal(err)
2121 }
22+ profilesDir := filepath.Join(root, "home", ".agents", "agents")
23+ if err := os.MkdirAll(profilesDir, 0o755); err != nil {
24+ t.Fatal(err)
25+ }
26+ if err := os.WriteFile(filepath.Join(profilesDir, "reviewer.md"), []byte("---\nname: reviewer\ndescription: Review delegated work\n---\nReview the requested scope."), 0o600); err != nil {
27+ t.Fatal(err)
28+ }
2229 enabled := true
2330 if err := SaveConfig(Config{AutoSubagentsEnabled: &enabled}); err != nil {
2431 t.Fatal(err)
@@ -35,8 +42,17 @@ func TestResolveSeparatesInteractiveProactiveAndHeadlessStrictPolicies(t *testin
3542 if err != nil {
3643 t.Fatalf("resolve interactive: %v", err)
3744 }
38- if !strings.Contains(interactive.SystemPrompt, ProactiveSubagentsSystemAddendum) || strings.Contains(interactive.SystemPrompt, StrictOrchestratorSystemAddendum) {
39- t.Fatalf("interactive prompt did not select proactive policy:\n%s", interactive.SystemPrompt)
45+ if !strings.Contains(interactive.SystemPrompt, ProactiveSubagentsSystemAddendum) || strings.Contains(interactive.SystemPrompt, StrictOrchestratorSystemAddendum) || !strings.Contains(interactive.SystemPrompt, "[subagents_list]") {
46+ t.Fatalf("interactive prompt did not select proactive primary policy and profiles:\n%s", interactive.SystemPrompt)
47+ }
48+
49+ childArgs := residentChildArgs(base, "ollama", subagents.ResidentChildSpec{Provider: "ollama", Model: "any-local-model"})
50+ child, err := Resolve(childArgs, false)
51+ if err != nil {
52+ t.Fatalf("resolve resident child: %v", err)
53+ }
54+ if strings.Contains(child.SystemPrompt, ProactiveSubagentsSystemAddendum) || strings.Contains(child.SystemPrompt, StrictOrchestratorSystemAddendum) || strings.Contains(child.SystemPrompt, "[subagents_list]") {
55+ t.Fatalf("resident child inherited primary delegation policy or profiles:\n%s", child.SystemPrompt)
4056 }
4157
4258 headlessArgs := base
@@ -19,6 +19,13 @@ func TestResolveSeparatesInteractiveProactiveAndHeadlessStrictPolicies(t *testin
19 if err := os.MkdirAll(project, 0o755); err != nil {19 if err := os.MkdirAll(project, 0o755); err != nil {
20 t.Fatal(err)20 t.Fatal(err)
21 }21 }
22+ profilesDir := filepath.Join(root, "home", ".agents", "agents")
23+ if err := os.MkdirAll(profilesDir, 0o755); err != nil {
24+ t.Fatal(err)
25+ }
26+ if err := os.WriteFile(filepath.Join(profilesDir, "reviewer.md"), []byte("---\nname: reviewer\ndescription: Review delegated work\n---\nReview the requested scope."), 0o600); err != nil {
27+ t.Fatal(err)
28+ }
22 enabled := true29 enabled := true
23 if err := SaveConfig(Config{AutoSubagentsEnabled: &enabled}); err != nil {30 if err := SaveConfig(Config{AutoSubagentsEnabled: &enabled}); err != nil {
24 t.Fatal(err)31 t.Fatal(err)
@@ -35,8 +42,17 @@ func TestResolveSeparatesInteractiveProactiveAndHeadlessStrictPolicies(t *testin
35 if err != nil {42 if err != nil {
36 t.Fatalf("resolve interactive: %v", err)43 t.Fatalf("resolve interactive: %v", err)
37 }44 }
38- if !strings.Contains(interactive.SystemPrompt, ProactiveSubagentsSystemAddendum) || strings.Contains(interactive.SystemPrompt, StrictOrchestratorSystemAddendum) {45+ if !strings.Contains(interactive.SystemPrompt, ProactiveSubagentsSystemAddendum) || strings.Contains(interactive.SystemPrompt, StrictOrchestratorSystemAddendum) || !strings.Contains(interactive.SystemPrompt, "[subagents_list]") {
39- t.Fatalf("interactive prompt did not select proactive policy:\n%s", interactive.SystemPrompt)46+ t.Fatalf("interactive prompt did not select proactive primary policy and profiles:\n%s", interactive.SystemPrompt)
47+ }
48+
49+ childArgs := residentChildArgs(base, "ollama", subagents.ResidentChildSpec{Provider: "ollama", Model: "any-local-model"})
50+ child, err := Resolve(childArgs, false)
51+ if err != nil {
52+ t.Fatalf("resolve resident child: %v", err)
53+ }
54+ if strings.Contains(child.SystemPrompt, ProactiveSubagentsSystemAddendum) || strings.Contains(child.SystemPrompt, StrictOrchestratorSystemAddendum) || strings.Contains(child.SystemPrompt, "[subagents_list]") {
55+ t.Fatalf("resident child inherited primary delegation policy or profiles:\n%s", child.SystemPrompt)
40 }56 }
41 57
42 headlessArgs := base58 headlessArgs := base
M packages/agent/subagent_factory.go +2 -1
@@ -100,9 +100,10 @@ func newResidentChildRunner(args Args, spec subagents.ResidentChildSpec, journal
100100 // decision accepted at spawn time across explicit resume and restart.
101101 func residentChildArgs(args Args, parentProvider string, spec subagents.ResidentChildSpec) Args {
102102 next := args
103- // --orchestrate is a role for the headless parent, not its workers. A child
103+ // Primary delegation policy belongs to the parent, not its workers. A child
104104 // receives its accepted profile instructions and exact child tool list.
105105 next.Orchestrate = false
106+ next.ResidentChild = true
106107 // An explicit CLI key belongs to the provider that resolved the parent.
107108 // A child can select another provider, but it must resolve that provider's
108109 // own credential rather than forwarding or persisting the parent's key.
@@ -100,9 +100,10 @@ func newResidentChildRunner(args Args, spec subagents.ResidentChildSpec, journal
100 // decision accepted at spawn time across explicit resume and restart.100 // decision accepted at spawn time across explicit resume and restart.
101 func residentChildArgs(args Args, parentProvider string, spec subagents.ResidentChildSpec) Args {101 func residentChildArgs(args Args, parentProvider string, spec subagents.ResidentChildSpec) Args {
102 next := args102 next := args
103- // --orchestrate is a role for the headless parent, not its workers. A child103+ // Primary delegation policy belongs to the parent, not its workers. A child
104 // receives its accepted profile instructions and exact child tool list.104 // receives its accepted profile instructions and exact child tool list.
105 next.Orchestrate = false105 next.Orchestrate = false
106+ next.ResidentChild = true
106 // An explicit CLI key belongs to the provider that resolved the parent.107 // An explicit CLI key belongs to the provider that resolved the parent.
107 // A child can select another provider, but it must resolve that provider's108 // A child can select another provider, but it must resolve that provider's
108 // own credential rather than forwarding or persisting the parent's key.109 // own credential rather than forwarding or persisting the parent's key.
M packages/agent/subagent_factory_test.go +1 -1
@@ -36,7 +36,7 @@ func TestResidentChildArgsPreserveDurableProfileInheritance(t *testing.T) {
3636 Provider: "openai", BaseURL: "https://current.example/v1", InsecureTLS: true, Model: "gpt-5", Workspace: "/repo/child",
3737 InheritSkills: &no, InheritProjectContext: &no,
3838 })
39- if next.Provider != "openai" || next.BaseURL != "https://current.example/v1" || !next.InsecureTLS || next.Model != "gpt-5" || next.CWD != "/repo/child" || !next.NoSkill || !next.NoContextFiles || next.Orchestrate {
39+ if next.Provider != "openai" || next.BaseURL != "https://current.example/v1" || !next.InsecureTLS || next.Model != "gpt-5" || next.CWD != "/repo/child" || !next.NoSkill || !next.NoContextFiles || next.Orchestrate || !next.ResidentChild {
4040 t.Fatalf("resident child args = %#v", next)
4141 }
4242 }
@@ -36,7 +36,7 @@ func TestResidentChildArgsPreserveDurableProfileInheritance(t *testing.T) {
36 Provider: "openai", BaseURL: "https://current.example/v1", InsecureTLS: true, Model: "gpt-5", Workspace: "/repo/child",36 Provider: "openai", BaseURL: "https://current.example/v1", InsecureTLS: true, Model: "gpt-5", Workspace: "/repo/child",
37 InheritSkills: &no, InheritProjectContext: &no,37 InheritSkills: &no, InheritProjectContext: &no,
38 })38 })
39- if next.Provider != "openai" || next.BaseURL != "https://current.example/v1" || !next.InsecureTLS || next.Model != "gpt-5" || next.CWD != "/repo/child" || !next.NoSkill || !next.NoContextFiles || next.Orchestrate {39+ if next.Provider != "openai" || next.BaseURL != "https://current.example/v1" || !next.InsecureTLS || next.Model != "gpt-5" || next.CWD != "/repo/child" || !next.NoSkill || !next.NoContextFiles || next.Orchestrate || !next.ResidentChild {
40 t.Fatalf("resident child args = %#v", next)40 t.Fatalf("resident child args = %#v", next)
41 }41 }
42 }42 }

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