Skip to content

Commit ce8c069

Browse files
feat(coding): add workspace coding sessions
1 parent 4c43e33 commit ce8c069

13 files changed

Lines changed: 1095 additions & 51 deletions

File tree

‎README.md‎

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ Maestro is an AI code review and repository analysis assistant built on top of `
99
│ USER INTERFACE │
1010
│ │
1111
│ CLI / TUI │
12-
│ • PR review │
13-
│ • /ask repository questions │
12+
│ • workspace coding sessions (ls/read/write/edit; bash opt-in) │
13+
│ • PR review and /ask repository questions │
1414
│ • optional review artifact + skill-store loading │
1515
└──────────────────────────────────────┬───────────────────────────────────────┘
1616
│
@@ -19,8 +19,8 @@ Maestro is an AI code review and repository analysis assistant built on top of `
1919
│ MAESTRO SERVICE │
2020
│ │
2121
│ Root CLI + TUI routing │
22-
│ • review path │
23-
│ • ask / overview path │
22+
│ • coding-agent path with typed lifecycle events │
23+
│ • review and ask / overview paths │
2424
│ • model and provider wiring │
2525
│ • ACE + persisted skill integration │
2626
└───────────────┬───────────────────────────────────────┬──────────────────────┘
@@ -69,6 +69,7 @@ Maestro is an AI code review and repository analysis assistant built on top of `
6969
- **Guideline Integration**: review runs can use cached guidelines and repository-aware context.
7070
- **Artifact-Aware Runtime**: live review runs can load tuned review artifacts and persisted skill stores.
7171
- **Repository Ask Support**: Maestro can answer codebase questions in addition to reviewing PRs.
72+
- **Coding Sessions**: Natural-language TUI input runs a workspace-scoped native agent with `ls`, `read`, `write`, and `edit`; unrestricted `bash` requires explicit `--allow-coding-bash`. `/ask` remains read-only QA.
7273

7374
### **Intelligent Review Pipeline**
7475
- **Specialized PR Review Agent**: Maestro uses a dedicated Go review path rather than a generic chat wrapper.
@@ -83,7 +84,9 @@ Maestro is an AI code review and repository analysis assistant built on top of `
8384

8485
### **Terminal UI (TUI v2)**
8586
- **Interactive Mode**: the root command still launches the modern interactive interface when no PR is provided.
86-
- **Slash-Command Workflow**: Maestro supports review and ask-style interaction patterns.
87+
- **Coding-Agent Workflow**: Natural-language prompts can inspect and edit the cloned workspace while typed run/turn/tool events update progress; shell verification is explicit opt-in.
88+
- **Slash-Command Workflow**: `/ask`, `/review`, session, and subagent commands remain explicit specialist paths.
89+
- **Cancellation**: Escape cancels the active coding run.
8790
- **Shared Runtime Wiring**: the same service layer backs both direct CLI review and interactive usage.
8891

8992
### **Semantic Code Search (Sgrep)**
@@ -219,6 +222,14 @@ The repo still contains ACE and RLM-related runtime wiring, but the most importa
219222
### **Model Selection**
220223

221224
```bash
225+
# Interactive coding session (natural prompts edit the cloned workspace; /ask is read-only QA)
226+
go run . --interactive --model google:gemini-2.5-flash --owner XiaoConstantine --repo dspy-go
227+
228+
# Non-interactive coding-session probe
229+
go run ./cmd/maestro-probe --strategy coding --allow-coding-bash --repo-path /path/to/repo \
230+
--question "Inspect the failing test, fix it, and run the focused test" \
231+
--model google:gemini-2.5-flash
232+
222233
# Gemini
223234
go run . --model google:gemini-2.5-flash --owner XiaoConstantine --repo dspy-go --pr 291
224235

‎cmd/maestro-probe/main.go‎

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,13 @@ func run() error {
9090
rlmTargetedArtifactsPath string
9191
timeout time.Duration
9292
verbose bool
93+
allowCodingBash bool
9394
)
9495

9596
flag.Usage = func() {
9697
fmt.Fprintf(flag.CommandLine.Output(), "Usage: maestro-probe --question <question> [flags]\n\n")
97-
fmt.Fprintf(flag.CommandLine.Output(), "Runs MaestroService.ProcessRequest(RequestAsk) against a local repository and prints the response metadata as JSON.\n")
98-
fmt.Fprintf(flag.CommandLine.Output(), "Scope: ask-side verification only; this does not exercise the PR review/RLM review path.\n\n")
98+
fmt.Fprintf(flag.CommandLine.Output(), "Runs a Maestro ask or coding request against a local repository and prints response metadata as JSON.\n")
99+
fmt.Fprintf(flag.CommandLine.Output(), "Use --strategy coding for workspace tools; PR review is not exercised.\n\n")
99100
flag.PrintDefaults()
100101
}
101102

@@ -109,12 +110,13 @@ func run() error {
109110
flag.StringVar(&modelConfig, "model-config", "", "Additional model configuration")
110111
flag.StringVar(&apiKey, "api-key", "", "API key for external model providers")
111112
flag.StringVar(&baseURL, "base-url", "", "Optional base URL override")
112-
flag.StringVar(&strategy, "strategy", "", "Optional ask strategy override: native, rlm, or rlm-targeted")
113+
flag.StringVar(&strategy, "strategy", "", "Execution strategy: native, rlm, rlm-targeted, or coding")
113114
flag.StringVar(&memoryPath, "memory-path", "", "Optional Maestro memory path; defaults under /tmp")
114115
flag.StringVar(&rlmOverviewArtifactsPath, "rlm-overview-artifact", "", "Optional RLM overview optimized program path")
115116
flag.StringVar(&rlmTargetedArtifactsPath, "rlm-targeted-ask-artifact", "", "Optional RLM targeted ask optimized program path")
116117
flag.DurationVar(&timeout, "timeout", 5*time.Minute, "Request timeout")
117118
flag.BoolVar(&verbose, "verbose", false, "Enable debug logging to stderr")
119+
flag.BoolVar(&allowCodingBash, "allow-coding-bash", false, "Allow unrestricted shell commands for --strategy coding")
118120
flag.Parse()
119121

120122
if strings.TrimSpace(question) == "" {
@@ -179,7 +181,8 @@ func run() error {
179181
core.GlobalConfig.DefaultLLM = defaultLLM
180182

181183
previousStrategy, hadStrategy := os.LookupEnv("MAESTRO_FORCE_ASK_STRATEGY")
182-
if strategy = strings.TrimSpace(strategy); strategy != "" {
184+
strategy = strings.TrimSpace(strategy)
185+
if strategy != "" && strategy != "coding" {
183186
if err := os.Setenv("MAESTRO_FORCE_ASK_STRATEGY", strategy); err != nil {
184187
return fmt.Errorf("set ask strategy: %w", err)
185188
}
@@ -195,6 +198,7 @@ func run() error {
195198
Owner: owner,
196199
Repo: repo,
197200
BudgetManager: budgetManager,
201+
AllowCodingBash: allowCodingBash,
198202
}, nil)
199203
if err != nil {
200204
return fmt.Errorf("create service: %w", err)
@@ -206,12 +210,13 @@ func run() error {
206210
}()
207211
service.SetReviewAgent(&localReviewAgent{repoPath: resolvedRepoPath, status: types.NewIndexingStatus()})
208212

209-
response, err := service.ProcessRequest(ctx, orchestration.Request{
210-
Type: orchestration.RequestAsk,
211-
Question: question,
212-
})
213+
request := orchestration.Request{Type: orchestration.RequestAsk, Question: question}
214+
if strategy == "coding" {
215+
request = orchestration.Request{Type: orchestration.RequestCoding, Prompt: question}
216+
}
217+
response, err := service.ProcessRequest(ctx, request)
213218
if err != nil {
214-
return fmt.Errorf("process ask request: %w", err)
219+
return fmt.Errorf("process %s request: %w", request.Type, err)
215220
}
216221

217222
output := probeOutput{

‎docs/coding_session.md‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Maestro Coding Session
2+
3+
Maestro's interactive default is a workspace coding agent. Natural-language TUI input runs a provider-neutral `dspy-go` native agent against the cloned repository; `/ask` remains the read-only repository-QA route.
4+
5+
## Architecture
6+
7+
```text
8+
terminal.MaestroModel
9+
-> terminal.MaestroBackend
10+
-> orchestration.MaestroService
11+
-> internal/coding.Session
12+
-> native.Agent.ExecuteWithTrace
13+
```
14+
15+
The boundaries mirror Tau/Pi:
16+
17+
- `dspy-go/pkg/agents` owns the reusable execution loop, typed events, messages, and canonical traces.
18+
- `internal/coding.Session` owns Maestro's workspace tools, active-run cancellation, and session persistence configuration.
19+
- `internal/orchestration` selects the cloned workspace and records trace usage.
20+
- `terminal` maps typed lifecycle events into Bubble Tea display progress.
21+
22+
The frontend does not inspect provider responses or parse result maps.
23+
24+
## Behavior
25+
26+
A coding session registers these workspace-contained file tools by default:
27+
28+
- `ls`
29+
- `read`
30+
- `write`
31+
- `edit`
32+
33+
File tools reject paths outside the cloned repository workspace. Shell execution is disabled by default because setting a working directory is not a sandbox: shell commands can otherwise access absolute paths, `$HOME`, and the network. Pass `--allow-coding-bash` to opt into unrestricted `bash` with the workspace as its initial working directory.
34+
35+
One run may mutate a session at a time. Overlapping prompts are rejected. Press **Esc** to cancel the active coding run. Run, turn, and tool lifecycle events update the TUI progress display. Output and accounting use the operation-scoped `ExecutionTrace` returned by `ExecuteWithTrace`, avoiding races against a mutable last-trace accessor.
36+
37+
The active Maestro session name selects the native session id, so switching or creating a session selects separate persisted coding history.
38+
39+
## Running
40+
41+
```bash
42+
go run . --interactive \
43+
--owner XiaoConstantine \
44+
--repo dspy-go \
45+
--model google:gemini-2.5-flash
46+
```
47+
48+
Add `--allow-coding-bash` only when you trust the active model and accept unrestricted shell access.
49+
50+
Enter a task such as:
51+
52+
```text
53+
Inspect the failing tests, implement the smallest fix, and run the focused tests.
54+
```
55+
56+
For a non-interactive smoke test:
57+
58+
```bash
59+
go run ./cmd/maestro-probe \
60+
--strategy coding \
61+
--repo-path /path/to/repository \
62+
--question "Create hello.txt and verify its contents" \
63+
--model google:gemini-2.5-flash
64+
```
65+
66+
## Current boundary
67+
68+
This slice establishes the coding-session execution spine. Tau-level steering/follow-up queues, compaction, session-tree transcript rendering, project instruction discovery, skill/prompt pickers, model switching, and token-delta streaming remain follow-up frontend/session features. They should be added above `dspy-go`'s canonical contracts rather than by creating another execution loop.

‎internal/coding/session.go‎

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package coding
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"strings"
8+
"sync"
9+
10+
"github.com/XiaoConstantine/dspy-go/pkg/agents"
11+
"github.com/XiaoConstantine/dspy-go/pkg/agents/native"
12+
"github.com/XiaoConstantine/dspy-go/pkg/agents/sessionevent"
13+
"github.com/XiaoConstantine/dspy-go/pkg/core"
14+
"github.com/XiaoConstantine/dspy-go/pkg/tools/defaults"
15+
)
16+
17+
var (
18+
ErrRunActive = errors.New("coding session already has an active run")
19+
ErrSessionClosed = errors.New("coding session is closed")
20+
)
21+
22+
type Config struct {
23+
LLM core.LLM
24+
Workspace string
25+
SessionID string
26+
SessionStore sessionevent.SessionEventStore
27+
SystemPrompt string
28+
MaxTurns int
29+
AllowBash bool
30+
}
31+
32+
type Session struct {
33+
workspace string
34+
agent *native.Agent
35+
forwarder *eventForwarder
36+
37+
runMu sync.Mutex
38+
mu sync.Mutex
39+
stop context.CancelFunc
40+
runDone chan struct{}
41+
closed bool
42+
}
43+
44+
func NewSession(cfg Config) (*Session, error) {
45+
if cfg.LLM == nil {
46+
return nil, fmt.Errorf("llm is required")
47+
}
48+
if strings.TrimSpace(cfg.Workspace) == "" {
49+
return nil, fmt.Errorf("workspace is required")
50+
}
51+
52+
toolset, err := defaults.NewToolset(defaults.Config{Root: cfg.Workspace})
53+
if err != nil {
54+
return nil, fmt.Errorf("create coding tools: %w", err)
55+
}
56+
forwarder := &eventForwarder{}
57+
agent, err := native.NewAgent(cfg.LLM, native.Config{
58+
MaxTurns: cfg.MaxTurns,
59+
SystemPrompt: cfg.SystemPrompt,
60+
SessionID: strings.TrimSpace(cfg.SessionID),
61+
SessionEventStore: cfg.SessionStore,
62+
EventSink: forwarder,
63+
})
64+
if err != nil {
65+
return nil, fmt.Errorf("create coding agent: %w", err)
66+
}
67+
for _, tool := range toolset.Tools() {
68+
if tool.Name() == "bash" && !cfg.AllowBash {
69+
continue
70+
}
71+
if err := agent.RegisterTool(tool); err != nil {
72+
return nil, fmt.Errorf("register coding tool %s: %w", tool.Name(), err)
73+
}
74+
}
75+
76+
return &Session{workspace: toolset.Root(), agent: agent, forwarder: forwarder}, nil
77+
}
78+
79+
func (s *Session) Workspace() string {
80+
if s == nil {
81+
return ""
82+
}
83+
return s.workspace
84+
}
85+
86+
func (s *Session) Prompt(ctx context.Context, prompt string, sink agents.EventSink) (agents.AgentExecutionResult, error) {
87+
if s == nil || s.agent == nil {
88+
return agents.AgentExecutionResult{}, fmt.Errorf("coding session is not initialized")
89+
}
90+
if strings.TrimSpace(prompt) == "" {
91+
return agents.AgentExecutionResult{}, fmt.Errorf("prompt is required")
92+
}
93+
if !s.runMu.TryLock() {
94+
return agents.AgentExecutionResult{}, ErrRunActive
95+
}
96+
defer s.runMu.Unlock()
97+
98+
runCtx, cancel := context.WithCancel(ctx)
99+
s.mu.Lock()
100+
if s.closed {
101+
s.mu.Unlock()
102+
cancel()
103+
return agents.AgentExecutionResult{}, ErrSessionClosed
104+
}
105+
done := make(chan struct{})
106+
s.stop = cancel
107+
s.runDone = done
108+
s.mu.Unlock()
109+
s.forwarder.Set(sink)
110+
defer func() {
111+
s.forwarder.Set(nil)
112+
cancel()
113+
s.mu.Lock()
114+
s.stop = nil
115+
s.runDone = nil
116+
close(done)
117+
s.mu.Unlock()
118+
}()
119+
120+
return s.agent.ExecuteWithTrace(runCtx, map[string]any{"task": prompt})
121+
}
122+
123+
func (s *Session) Close(ctx context.Context) error {
124+
if s == nil {
125+
return nil
126+
}
127+
s.mu.Lock()
128+
s.closed = true
129+
cancel := s.stop
130+
done := s.runDone
131+
if cancel != nil {
132+
cancel()
133+
}
134+
s.mu.Unlock()
135+
if done == nil {
136+
return nil
137+
}
138+
select {
139+
case <-done:
140+
return nil
141+
case <-ctx.Done():
142+
return ctx.Err()
143+
}
144+
}
145+
146+
func (s *Session) Cancel() bool {
147+
if s == nil {
148+
return false
149+
}
150+
s.mu.Lock()
151+
defer s.mu.Unlock()
152+
if s.stop == nil {
153+
return false
154+
}
155+
s.stop()
156+
return true
157+
}
158+
159+
type eventForwarder struct {
160+
mu sync.RWMutex
161+
sink agents.EventSink
162+
}
163+
164+
func (f *eventForwarder) Set(sink agents.EventSink) {
165+
f.mu.Lock()
166+
defer f.mu.Unlock()
167+
f.sink = sink
168+
}
169+
170+
func (f *eventForwarder) EmitEvent(ctx context.Context, event agents.ExecutionEvent) {
171+
f.mu.RLock()
172+
sink := f.sink
173+
f.mu.RUnlock()
174+
if sink != nil {
175+
sink.EmitEvent(ctx, event)
176+
}
177+
}

0 commit comments

Comments
 (0)