@@ -3,7 +3,6 @@ package agentclaude
3
3
import (
4
4
"context"
5
5
"encoding/json"
6
- "fmt"
7
6
"io"
8
7
"os"
9
8
"os/exec"
@@ -13,34 +12,42 @@ import (
13
12
"time"
14
13
15
14
"github.com/spf13/afero"
15
+ "golang.org/x/xerrors"
16
16
)
17
17
18
- func New (ctx context.Context , apiKey , systemPrompt , taskPrompt string , onPause func ()) error {
18
+ func New (ctx context.Context , apiKey , systemPrompt , taskPrompt string , onWaiting func (waiting bool )) error {
19
19
claudePath , err := exec .LookPath ("claude" )
20
20
if err != nil {
21
- return fmt .Errorf ("claude not found: %w" , err )
21
+ return xerrors .Errorf ("claude not found: %w" , err )
22
22
}
23
23
fs := afero .NewOsFs ()
24
- err = injectClaudeMD (fs , `You are an AI agent in a Coder Workspace .
24
+ err = injectClaudeMD (fs , `YOU MUST REPORT YOUR STATUS IMMEDIATELY AFTER EACH USER MESSAGE .
25
25
26
- The user is running this task entirely autonomously .
26
+ INTERRUPT READING FILES OR ANY OTHER TOOL CALL IF YOU HAVE NOT REPORTED A STATUS YET .
27
27
28
- Use the coder-agent MCP server to report your progress. You must report when you:
29
- - Start a new task.
30
- - Complete a task (e.g. push a commit, open a PR, etc.)
31
- - Make progress on a task.
28
+ You MUST use the mcp__coder-agent__report_status function with all required parameters:
29
+ - summary: Short description of what you're doing
30
+ - link: A relevant link for the status
31
+ - done: Boolean indicating if the task is complete (true/false)
32
+ - emoji: Relevant emoji for the status
32
33
33
- You should report your progress frequently. Try to report as much as possible.
34
+ WHEN TO REPORT (MANDATORY):
35
+ 1. IMMEDIATELY after receiving ANY user message, before any other actions
36
+ 2. After completing any task
37
+ 3. When making significant progress
38
+ 4. When encountering roadblocks
39
+ 5. When asking questions
40
+ 6. Before and after using search tools or making code changes
34
41
35
- If you do not, the user will not be able to see your progress .
42
+ FAILING TO REPORT STATUS PROPERLY WILL RESULT IN INCORRECT BEHAVIOR .
36
43
` , systemPrompt , "" )
37
44
if err != nil {
38
- return fmt .Errorf ("failed to inject claude md: %w" , err )
45
+ return xerrors .Errorf ("failed to inject claude md: %w" , err )
39
46
}
40
47
41
48
wd , err := os .Getwd ()
42
49
if err != nil {
43
- return fmt .Errorf ("failed to get working directory: %w" , err )
50
+ return xerrors .Errorf ("failed to get working directory: %w" , err )
44
51
}
45
52
46
53
err = configureClaude (fs , ClaudeConfig {
@@ -59,20 +66,22 @@ If you do not, the user will not be able to see your progress.
59
66
},
60
67
})
61
68
if err != nil {
62
- return fmt .Errorf ("failed to configure claude: %w" , err )
69
+ return xerrors .Errorf ("failed to configure claude: %w" , err )
63
70
}
64
71
65
72
cmd := exec .CommandContext (ctx , claudePath , "--dangerously-skip-permissions" , taskPrompt )
66
73
// Create a simple wrapper that starts monitoring only after first write
67
74
stdoutWriter := & delayedPauseWriter {
68
75
writer : os .Stdout ,
69
76
pauseWindow : 2 * time .Second ,
70
- onPause : onPause ,
77
+ onWaiting : onWaiting ,
78
+ cooldown : 15 * time .Second ,
71
79
}
72
80
stderrWriter := & delayedPauseWriter {
73
81
writer : os .Stderr ,
74
82
pauseWindow : 2 * time .Second ,
75
- onPause : onPause ,
83
+ onWaiting : onWaiting ,
84
+ cooldown : 15 * time .Second ,
76
85
}
77
86
78
87
cmd .Stdout = stdoutWriter
@@ -86,11 +95,13 @@ If you do not, the user will not be able to see your progress.
86
95
type delayedPauseWriter struct {
87
96
writer io.Writer
88
97
pauseWindow time.Duration
89
- onPause func ()
98
+ cooldown time.Duration
99
+ onWaiting func (waiting bool )
90
100
lastWrite time.Time
91
101
mu sync.Mutex
92
102
started bool
93
- pauseNotified bool
103
+ waitingState bool
104
+ cooldownUntil time.Time
94
105
}
95
106
96
107
// Write implements io.Writer and starts monitoring on first write
@@ -100,8 +111,12 @@ func (w *delayedPauseWriter) Write(p []byte) (n int, err error) {
100
111
w .started = true
101
112
w .lastWrite = time .Now ()
102
113
103
- // Reset pause notification state when new output appears
104
- w .pauseNotified = false
114
+ // If we were in waiting state, we're now resumed
115
+ if w .waitingState {
116
+ w .waitingState = false
117
+ w .cooldownUntil = time .Now ().Add (w .cooldown )
118
+ w .onWaiting (false ) // Signal resume
119
+ }
105
120
106
121
w .mu .Unlock ()
107
122
@@ -113,26 +128,32 @@ func (w *delayedPauseWriter) Write(p []byte) (n int, err error) {
113
128
return w .writer .Write (p )
114
129
}
115
130
116
- // monitorPauses checks for pauses in writing and calls onPause when detected
131
+ // monitorPauses checks for pauses in writing and calls onWaiting when detected
117
132
func (w * delayedPauseWriter ) monitorPauses () {
118
133
ticker := time .NewTicker (500 * time .Millisecond )
119
134
defer ticker .Stop ()
120
135
121
136
for range ticker .C {
122
137
w .mu .Lock ()
123
- elapsed := time .Since (w .lastWrite )
124
- alreadyNotified := w .pauseNotified
125
138
126
- // If we detect a pause and haven't notified yet, mark as notified
127
- if elapsed >= w .pauseWindow && ! alreadyNotified {
128
- w .pauseNotified = true
139
+ // Check if we're in a cooldown period
140
+ inCooldown := time .Now ().Before (w .cooldownUntil )
141
+ elapsed := time .Since (w .lastWrite )
142
+ shouldWait := elapsed >= w .pauseWindow && ! inCooldown
143
+ currentState := w .waitingState
144
+ shouldNotify := false
145
+
146
+ // Only update state if it changed
147
+ if shouldWait != currentState {
148
+ w .waitingState = shouldWait
149
+ shouldNotify = true
129
150
}
130
151
131
152
w .mu .Unlock ()
132
153
133
- // Only notify once per pause period
134
- if elapsed >= w . pauseWindow && ! alreadyNotified {
135
- w .onPause ( )
154
+ // Notify outside of the lock to avoid deadlocks
155
+ if shouldNotify {
156
+ w .onWaiting ( shouldWait )
136
157
}
137
158
}
138
159
}
@@ -144,14 +165,14 @@ func injectClaudeMD(fs afero.Fs, coderPrompt, systemPrompt string, configPath st
144
165
_ , err := fs .Stat (configPath )
145
166
if err != nil {
146
167
if ! os .IsNotExist (err ) {
147
- return fmt .Errorf ("failed to stat claude config: %w" , err )
168
+ return xerrors .Errorf ("failed to stat claude config: %w" , err )
148
169
}
149
170
}
150
171
content := ""
151
172
if err == nil {
152
173
contentBytes , err := afero .ReadFile (fs , configPath )
153
174
if err != nil {
154
- return fmt .Errorf ("failed to read claude config: %w" , err )
175
+ return xerrors .Errorf ("failed to read claude config: %w" , err )
155
176
}
156
177
content = string (contentBytes )
157
178
}
@@ -202,13 +223,13 @@ func injectClaudeMD(fs afero.Fs, coderPrompt, systemPrompt string, configPath st
202
223
203
224
err = fs .MkdirAll (filepath .Dir (configPath ), 0755 )
204
225
if err != nil {
205
- return fmt .Errorf ("failed to create claude config directory: %w" , err )
226
+ return xerrors .Errorf ("failed to create claude config directory: %w" , err )
206
227
}
207
228
208
229
// Write the updated content back to the file
209
230
err = afero .WriteFile (fs , configPath , []byte (newContent ), 0644 )
210
231
if err != nil {
211
- return fmt .Errorf ("failed to write claude config: %w" , err )
232
+ return xerrors .Errorf ("failed to write claude config: %w" , err )
212
233
}
213
234
214
235
return nil
@@ -249,17 +270,17 @@ func configureClaude(fs afero.Fs, cfg ClaudeConfig) error {
249
270
if os .IsNotExist (err ) {
250
271
config = make (map [string ]any )
251
272
} else {
252
- return fmt .Errorf ("failed to stat claude config: %w" , err )
273
+ return xerrors .Errorf ("failed to stat claude config: %w" , err )
253
274
}
254
275
}
255
276
if err == nil {
256
277
jsonBytes , err := afero .ReadFile (fs , cfg .ConfigPath )
257
278
if err != nil {
258
- return fmt .Errorf ("failed to read claude config: %w" , err )
279
+ return xerrors .Errorf ("failed to read claude config: %w" , err )
259
280
}
260
281
err = json .Unmarshal (jsonBytes , & config )
261
282
if err != nil {
262
- return fmt .Errorf ("failed to unmarshal claude config: %w" , err )
283
+ return xerrors .Errorf ("failed to unmarshal claude config: %w" , err )
263
284
}
264
285
}
265
286
@@ -312,16 +333,32 @@ func configureClaude(fs afero.Fs, cfg ClaudeConfig) error {
312
333
project ["mcpServers" ] = mcpServers
313
334
// Prevents Claude from asking the user to complete the project onboarding.
314
335
project ["hasCompletedProjectOnboarding" ] = true
336
+
337
+ history , ok := project ["history" ].([]string )
338
+ injectedHistoryLine := "make sure to read claude.md and report tasks properly"
339
+
340
+ if ! ok || len (history ) == 0 {
341
+ // History doesn't exist or is empty, create it with our injected line
342
+ history = []string {injectedHistoryLine }
343
+ } else {
344
+ // Check if our line is already the first item
345
+ if history [0 ] != injectedHistoryLine {
346
+ // Prepend our line to the existing history
347
+ history = append ([]string {injectedHistoryLine }, history ... )
348
+ }
349
+ }
350
+ project ["history" ] = history
351
+
315
352
projects [cfg .ProjectDirectory ] = project
316
353
config ["projects" ] = projects
317
354
318
355
jsonBytes , err := json .MarshalIndent (config , "" , " " )
319
356
if err != nil {
320
- return fmt .Errorf ("failed to marshal claude config: %w" , err )
357
+ return xerrors .Errorf ("failed to marshal claude config: %w" , err )
321
358
}
322
359
err = afero .WriteFile (fs , cfg .ConfigPath , jsonBytes , 0644 )
323
360
if err != nil {
324
- return fmt .Errorf ("failed to write claude config: %w" , err )
361
+ return xerrors .Errorf ("failed to write claude config: %w" , err )
325
362
}
326
363
return nil
327
364
}
0 commit comments