Skip to content

Commit 0d73bdf

Browse files
Jim Wordelmanclaude
authored andcommitted
fix: include --settings and default args in gc session attach resume command
buildResumeCommand (used by gc session attach and gc session submit) re-resolved the provider via CommandString() but did not append --settings or ResolveDefaultArgs(). The reconciler's template_resolve path did both. When gc session attach started Claude before the reconciler, SessionStart hooks defined in .gc/settings.json were not loaded — causing intermittent missing startup text. The "every-other" pattern was a timing race: after gc-cycle, sometimes the reconciler started the session (with --settings → hooks worked), sometimes the user's gc session attach won (without → hooks missing). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1bfd8b3 commit 0d73bdf

3 files changed

Lines changed: 70 additions & 7 deletions

File tree

cmd/gc/cmd_session.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/gastownhall/gascity/internal/events"
2020
"github.com/gastownhall/gascity/internal/runtime"
2121
"github.com/gastownhall/gascity/internal/session"
22+
"github.com/gastownhall/gascity/internal/shellquote"
2223
"github.com/spf13/cobra"
2324
)
2425

@@ -695,7 +696,7 @@ func cmdSessionAttach(args []string, stdout, stderr io.Writer) int {
695696
}
696697

697698
// Build the resume command from the template's provider.
698-
resumeCmd, hints := buildResumeCommand(cfg, info, beadSessionKind(store, sessionID))
699+
resumeCmd, hints := buildResumeCommand(cityPath, cfg, info, beadSessionKind(store, sessionID))
699700

700701
fmt.Fprintf(stdout, "Attaching to session %s (%s)...\n", sessionID, info.Template) //nolint:errcheck // best-effort stdout
701702
if err := mgr.Attach(context.Background(), sessionID, resumeCmd, hints); err != nil {
@@ -709,11 +710,15 @@ func cmdSessionAttach(args []string, stdout, stderr io.Writer) int {
709710
// a session. Uses provider resume if the session has a session key and the
710711
// provider supports resume; otherwise falls back to the stored command.
711712
//
713+
// cityPath is needed to resolve the --settings flag for Claude sessions.
714+
// Without it, SessionStart hooks defined in .gc/settings.json are not loaded
715+
// when gc session attach starts the process (as opposed to the reconciler).
716+
//
712717
// sessionKind mirrors the mc_session_kind bead metadata: "provider" means
713718
// the session was created from a bare provider name (not an agent template),
714719
// so the agent-template lookup should be skipped. This matches the guard in
715720
// the API handler (handler_session_chat.go).
716-
func buildResumeCommand(cfg *config.City, info session.Info, sessionKind string) (string, runtime.Config) {
721+
func buildResumeCommand(cityPath string, cfg *config.City, info session.Info, sessionKind string) (string, runtime.Config) {
717722
cmd := session.BuildResumeCommand(info)
718723
if cfg == nil {
719724
return cmd, runtime.Config{WorkDir: info.WorkDir}
@@ -724,7 +729,16 @@ func buildResumeCommand(cfg *config.City, info session.Info, sessionKind string)
724729
return cmd, runtime.Config{WorkDir: info.WorkDir}
725730
}
726731
resolvedInfo := info
727-
resolvedInfo.Command = resolved.CommandString()
732+
// Build command with default args and settings, matching the
733+
// reconciler's template_resolve.go command construction.
734+
command := resolved.CommandString()
735+
if defaultArgs := resolved.ResolveDefaultArgs(); len(defaultArgs) > 0 {
736+
command = command + " " + shellquote.Join(defaultArgs)
737+
}
738+
if sa := settingsArgs(cityPath, resolved.Name); sa != "" {
739+
command = command + " " + sa
740+
}
741+
resolvedInfo.Command = command
728742
resolvedInfo.Provider = resolved.Name
729743
resolvedInfo.ResumeFlag = resolved.ResumeFlag
730744
resolvedInfo.ResumeStyle = resolved.ResumeStyle
@@ -1247,7 +1261,7 @@ func cmdSessionSubmit(args []string, intent session.SubmitIntent, stdout, stderr
12471261
fmt.Fprintf(stderr, "gc session submit: %v\n", err) //nolint:errcheck // best-effort stderr
12481262
return 1
12491263
}
1250-
resumeCmd, hints := buildResumeCommand(cfg, info, beadSessionKind(store, sessionID))
1264+
resumeCmd, hints := buildResumeCommand(cityPath, cfg, info, beadSessionKind(store, sessionID))
12511265
outcome, err := mgr.Submit(context.Background(), sessionID, message, resumeCmd, hints, intent)
12521266
if err != nil {
12531267
fmt.Fprintf(stderr, "gc session submit: %v\n", err) //nolint:errcheck // best-effort stderr

cmd/gc/cmd_session_test.go

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"context"
66
"errors"
7+
"fmt"
78
"net"
89
"os"
910
"path/filepath"
@@ -283,7 +284,7 @@ func TestBuildResumeCommandUsesResolvedProviderCommand(t *testing.T) {
283284
WorkDir: "/tmp/workdir",
284285
}
285286

286-
cmd, hints := buildResumeCommand(cfg, info, "")
287+
cmd, hints := buildResumeCommand(t.TempDir(), cfg, info, "")
287288
if got, want := cmd, "aimux run gemini -- --approval-mode yolo"; got != want {
288289
t.Fatalf("resume command = %q, want %q", got, want)
289290
}
@@ -298,6 +299,54 @@ func TestBuildResumeCommandUsesResolvedProviderCommand(t *testing.T) {
298299
}
299300
}
300301

302+
func TestBuildResumeCommandIncludesSettingsAndDefaultArgs(t *testing.T) {
303+
cityDir := t.TempDir()
304+
// Write a .gc/settings.json so settingsArgs finds it.
305+
gcDir := filepath.Join(cityDir, ".gc")
306+
if err := os.MkdirAll(gcDir, 0o755); err != nil {
307+
t.Fatal(err)
308+
}
309+
if err := os.WriteFile(filepath.Join(gcDir, "settings.json"), []byte(`{"hooks":{}}`), 0o644); err != nil {
310+
t.Fatal(err)
311+
}
312+
313+
cfg := &config.City{
314+
Workspace: config.Workspace{Name: "test-city"},
315+
Agents: []config.Agent{
316+
{Name: "mayor"},
317+
},
318+
}
319+
info := session.Info{
320+
Template: "mayor",
321+
Command: "claude",
322+
Provider: "claude",
323+
WorkDir: "/tmp/workdir",
324+
SessionKey: "abc-123",
325+
ResumeFlag: "--resume",
326+
}
327+
328+
cmd, _ := buildResumeCommand(cityDir, cfg, info, "")
329+
330+
// Must include --settings pointing to .gc/settings.json.
331+
wantSettings := fmt.Sprintf("--settings %q", filepath.Join(gcDir, "settings.json"))
332+
if !strings.Contains(cmd, "--settings") {
333+
t.Fatalf("resume command missing --settings:\n got: %s", cmd)
334+
}
335+
if !strings.Contains(cmd, wantSettings) {
336+
t.Fatalf("resume command has wrong --settings path:\n got: %s\n want: ...%s...", cmd, wantSettings)
337+
}
338+
339+
// Must include --resume flag.
340+
if !strings.Contains(cmd, "--resume abc-123") {
341+
t.Fatalf("resume command missing --resume flag:\n got: %s", cmd)
342+
}
343+
344+
// Must include default args (--dangerously-skip-permissions for claude).
345+
if !strings.Contains(cmd, "--dangerously-skip-permissions") {
346+
t.Fatalf("resume command missing default args:\n got: %s", cmd)
347+
}
348+
}
349+
301350
func TestSessionReason_FallsThroughToProviderForSleepingAttachment(t *testing.T) {
302351
sp := runtime.NewFake()
303352
_ = sp.Start(context.Background(), "sleeping-worker", runtime.Config{})

cmd/gc/live_submit_probe_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ func TestLiveClaudeInterruptNow(t *testing.T) {
9797
if err != nil {
9898
t.Fatalf("mgr.Get(%q): %v", id, err)
9999
}
100-
resumeCmd, hints := buildResumeCommand(cfg, info, "")
100+
resumeCmd, hints := buildResumeCommand(t.TempDir(), cfg, info, "")
101101
socket := cfg.Session.Socket
102102
if socket == "" {
103103
socket = cfg.Workspace.Name
@@ -174,7 +174,7 @@ func TestLiveGeminiSubmitIntents(t *testing.T) {
174174
if err != nil {
175175
t.Fatalf("mgr.Get(%q): %v", id, err)
176176
}
177-
resumeCmd, hints := buildResumeCommand(cfg, info, "")
177+
resumeCmd, hints := buildResumeCommand(t.TempDir(), cfg, info, "")
178178
socket := cfg.Session.Socket
179179
if socket == "" {
180180
socket = cfg.Workspace.Name

0 commit comments

Comments
 (0)