-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrun-agent.ps1
More file actions
355 lines (317 loc) · 13.7 KB
/
Copy pathrun-agent.ps1
File metadata and controls
355 lines (317 loc) · 13.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
# run-agent.ps1 - Centralized Startup script for Windows (PowerShell)
param (
[Parameter(Position = 0, Mandatory = $false)]
[string]$ProjectPath,
[Alias("c", "engine")]
[string]$Container = "gemini",
[Alias("r", "mode")]
[string]$Role = "coder",
[Alias("p")]
[string]$Prompt = "",
[Alias("v")]
[switch]$VerboseMode,
[Alias("t")]
[switch]$Tui,
[Parameter(ValueFromRemainingArguments = $true)]
[string[]]$RemainingArgs
)
# Locate Script Directory
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
# Load Pluggable Engine Driver Config (lives alongside the engine's Dockerfile)
$EngineDir = Join-Path $ScriptDir $Container
$ConfFile = Join-Path $EngineDir "agent.psd1"
if (-not (Test-Path -Path $ConfFile -PathType Leaf)) {
Write-Error "Engine '${Container}' is not a valid driver config (file not found: $ConfFile)."
$AvailableEngines = Get-ChildItem -Path $ScriptDir -Directory |
Where-Object { Test-Path -Path (Join-Path $_.FullName "agent.psd1") -PathType Leaf } |
ForEach-Object { $_.Name }
Write-Host "💡 Available engines: $($AvailableEngines -join ', ')"
exit 1
}
$Config = Import-PowerShellDataFile -Path $ConfFile
# --- Docker Check ---
docker info > $null 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Error "Docker daemon is not running. Please start Docker Desktop or your Docker environment."
exit 1
}
# Resolve Workspace Folder
if ($ProjectPath) {
if (Test-Path -Path $ProjectPath -PathType Container) {
$ResolvedPath = (Get-Item $ProjectPath).FullName
} else {
Write-Error "Provided path is not a valid directory: $ProjectPath"
exit 1
}
} else {
$ResolvedPath = (Get-Location).Path
Write-Host "ℹ️ No project path specified. Defaulting to current directory: $ResolvedPath"
}
# --- Branch Safety Check (single-repo, writable runs only) ---
# Prevent an autonomous agent from editing the working tree while checked out on
# a shared/default branch. Only runs when the mount root is itself a git repo
# root and the workspace is writable (coder role); parent-dir/context mounts and
# read-only design/spec runs skip this check.
if (($env:AGENT_TESTING -ne "true") -and ($Role -ne "design") -and ($Role -ne "spec") -and (Get-Command git -ErrorAction SilentlyContinue)) {
$IsRepoRoot = $false
git -C $ResolvedPath rev-parse --is-inside-work-tree > $null 2>&1
if ($LASTEXITCODE -eq 0) {
$TopLevel = (git -C $ResolvedPath rev-parse --show-toplevel 2>$null)
if ($TopLevel) { $TopLevel = $TopLevel.Trim() }
# git returns forward slashes; normalize the resolved path before comparing (case-insensitive by default)
if ($TopLevel -eq ($ResolvedPath -replace '\\', '/')) {
$IsRepoRoot = $true
}
}
if ($IsRepoRoot) {
$CurrentBranch = (git -C $ResolvedPath rev-parse --abbrev-ref HEAD 2>$null)
if ($CurrentBranch) { $CurrentBranch = $CurrentBranch.Trim() }
$DefaultBranches = @("main", "master", "develop", "development", "trunk", "release")
if ($DefaultBranches -contains $CurrentBranch) {
Write-Error "🛑 Refusing to launch: workspace is on default/shared branch '$CurrentBranch'."
Write-Host "💡 Switch to a working branch first, e.g.:"
Write-Host " git -C `"$ResolvedPath`" switch -c my-work-branch"
exit 1
}
Write-Host "🌿 Branch check OK: workspace on '$CurrentBranch'."
} else {
Write-Host "ℹ️ Workspace is not a single git repo root — skipping branch safety check."
}
}
# Parse Prompt
$RawPrompt = $Prompt
$HasPrompt = $false
if ($RawPrompt -ne "") {
$HasPrompt = $true
} elseif ($RemainingArgs) {
if ($RemainingArgs[0] -eq "-p" -or $RemainingArgs[0] -eq "--prompt") {
if ($RemainingArgs.Count -gt 1) {
$RawPrompt = $RemainingArgs[1]
$HasPrompt = $true
}
} else {
$RawPrompt = $RemainingArgs -join " "
$HasPrompt = $true
}
}
# Check for prompt.txt or prompt.md if no prompt was provided
if (-not $HasPrompt) {
if (Test-Path -Path "${ResolvedPath}\prompt.txt" -PathType Leaf) {
$RawPrompt = Get-Content -Raw -Path "${ResolvedPath}\prompt.txt"
$HasPrompt = $true
Write-Host "📄 Found prompt.txt in project directory: ${ResolvedPath}\prompt.txt"
} elseif (Test-Path -Path "${ResolvedPath}\prompt.md" -PathType Leaf) {
$RawPrompt = Get-Content -Raw -Path "${ResolvedPath}\prompt.md"
$HasPrompt = $true
Write-Host "📄 Found prompt.md in project directory: ${ResolvedPath}\prompt.md"
} elseif (Test-Path -Path "prompt.txt" -PathType Leaf) {
$RawPrompt = Get-Content -Raw -Path "prompt.txt"
$HasPrompt = $true
Write-Host "📄 Found prompt.txt in current working directory: prompt.txt"
} elseif (Test-Path -Path "prompt.md" -PathType Leaf) {
$RawPrompt = Get-Content -Raw -Path "prompt.md"
$HasPrompt = $true
Write-Host "📄 Found prompt.md in current working directory: prompt.md"
}
}
# --- Resolve Guidelines and Roles ---
$FinalPrompt = $RawPrompt
$WorkspaceMountFlag = "rw"
if ($HasPrompt) {
# Load guidelines.txt
$Guidelines = ""
$GuidelinesPath = Join-Path $ScriptDir "guidelines.txt"
if (Test-Path -Path $GuidelinesPath -PathType Leaf) {
$Guidelines = Get-Content -Raw -Path $GuidelinesPath
}
# Load role instructions
$RoleInstructions = ""
if ($Role -eq "design" -or $Role -eq "spec") {
$WorkspaceMountFlag = "ro"
$RoleInstructions = "### Specification Writing Mode`nYou are running in DESIGN & SPECIFICATION mode. The workspace is mounted as READ-ONLY. You cannot edit files or compile code. Your task is to analyze the codebase and write specifications, prompt designs, or plan drafts. Deliver all your findings as markdown outputs in the chat."
Write-Host "🛡️ Role: Design & Specification (Workspace mounted as READ-ONLY)"
} else {
Write-Host "🛠️ Role: Coder (Workspace mounted as Read-Write)"
}
# Combine guidelines
$CombinedGuidelines = ""
if ($Guidelines) {
$CombinedGuidelines = $Guidelines
}
if ($RoleInstructions) {
if ($CombinedGuidelines) {
$CombinedGuidelines = "${CombinedGuidelines}`n`n${RoleInstructions}"
} else {
$CombinedGuidelines = $RoleInstructions
}
}
if ($CombinedGuidelines) {
$FinalPrompt = "${RawPrompt}`n`n---`n`n### Global Guidelines & Execution Rules`n`n${CombinedGuidelines}"
Write-Host "📜 Appended safety guidelines and role rules."
}
}
if ($HasPrompt) {
Write-Host "📝 Prompt:"
Write-Host $RawPrompt
Write-Host ""
# Set terminal title if the first line is <= 50 characters
$FirstLine = ($RawPrompt -split "`n")[0].Trim()
if ($FirstLine.Length -gt 0 -and $FirstLine.Length -le 50) {
$Host.UI.RawUI.WindowTitle = $FirstLine
}
}
# --- Load Local Env File if the engine driver declares one (e.g. Mistral's ~/.vibe/.env) ---
if ($Config.EnvFile) {
$EnvFilePath = $Config.EnvFile -replace '^~', $HOME
if (Test-Path -Path $EnvFilePath -PathType Leaf) {
Get-Content $EnvFilePath | ForEach-Object {
$line = $_.Trim()
if ($line -and -not $line.StartsWith("#")) {
if ($line -match "^([^=]+)=(.*)$") {
$key = $Matches[1].Trim()
$val = $Matches[2].Trim().Trim("'").Trim('"')
if (-not (Get-Item -Path "env:$key" -ErrorAction SilentlyContinue)) {
[System.Environment]::SetEnvironmentVariable($key, $val, [System.EnvironmentVariableTarget]::Process)
}
}
}
}
}
}
# --- Authentication Mode Check ---
$IsEnvAuth = $false
$EnvArgs = @()
foreach ($var in $Config.EnvVars) {
$val = Get-Item -Path "env:$var" -ErrorAction SilentlyContinue
if ($val) {
$IsEnvAuth = $true
$EnvArgs += @("-e", "$var=$($val.Value)")
Write-Host "🔑 Mode: API Key Authentication ($var detected)"
break
}
}
if (-not $IsEnvAuth) {
Write-Host "👤 Mode: OAuth / Local Credentials Authentication (no API key detected in host environment)"
Write-Host "ℹ️ Authentication tokens will be securely saved in persistent Docker volumes."
}
# Resolve Volume Arguments
$VolumeArgs = @()
foreach ($vol in $Config.Volumes) {
$VolumeArgs += @("-v", $vol)
}
# Check if Docker Image exists locally, build if missing
$ImageFullName = "$($Config.ImageName):$($Config.Tag)"
$ImageId = docker images -q $ImageFullName 2>$null
if (-not $ImageId) {
Write-Host "⚠️ Docker image '$ImageFullName' not found locally." -ForegroundColor Yellow
$DockerfilePath = $EngineDir
if (Test-Path -Path $DockerfilePath -PathType Container) {
Write-Host "🔨 Building Docker image '$ImageFullName' from $DockerfilePath..." -ForegroundColor Cyan
& docker build -t $ImageFullName $DockerfilePath
if ($LASTEXITCODE -ne 0) {
Write-Error "❌ Failed to build Docker image '$ImageFullName'."
exit 1
}
Write-Host "✅ Docker image '$ImageFullName' built successfully!" -ForegroundColor Green
} else {
Write-Error "❌ Dockerfile directory not found at $DockerfilePath. Cannot build image."
exit 1
}
}
Write-Host "🚀 Starting Coder Container [Engine: $($Config.ImageName)]..."
Write-Host "📂 Mounting Host Path: $ResolvedPath -> /workspace ($WorkspaceMountFlag)"
if ($HasPrompt -and -not $Tui) {
Write-Host "📺 Real-time terminal output active."
} else {
Write-Host "📺 Real-time terminal output active. Type 'exit' to quit."
}
Write-Host "--------------------------------------------------------"
# --- Assemble CLI arguments from the engine driver's per-mode contract ---
# The driver declares one array per invocation mode; the runner picks the mode
# and knows nothing about any vendor's flag grammar.
$Streaming = $false
$ModeArgs = @()
$StdinArgs = @()
$ExecLabel = ""
if ($HasPrompt) {
if ($Tui) {
$ModeArgs = $Config.ArgsTui
$StdinArgs = $Config.StdinTui
$ExecLabel = "$($Config.CliCommand) [prompt + guidelines]"
} elseif ($Config.StreamFormatter) {
$ModeArgs = $Config.ArgsStream
$StdinArgs = $Config.StdinStream
$Streaming = $true
$ExecLabel = "$($Config.CliCommand) -p [prompt + guidelines] (streaming real-time output)"
} else {
$ModeArgs = $Config.ArgsHeadless
$StdinArgs = $Config.StdinHeadless
$ExecLabel = "$($Config.CliCommand) -p [prompt + guidelines]"
}
} else {
$ModeArgs = $Config.ArgsInteractive
$StdinArgs = $Config.StdinInteractive
}
# A driver that declares no stdin flags for the selected mode still needs stdin
# attached, so fall back to -i alone rather than to nothing.
if (-not $StdinArgs -or $StdinArgs.Count -eq 0) {
$StdinArgs = @("-i")
}
# Substitute the driver's {{PROMPT}} token (a standalone argument, never a substring)
$DeclaredArgs = @()
if ($Config.ArgsCommon) { $DeclaredArgs += $Config.ArgsCommon }
if ($ModeArgs) { $DeclaredArgs += $ModeArgs }
$CmdArgs = @()
foreach ($arg in $DeclaredArgs) {
if ($arg -eq "{{PROMPT}}") {
$CmdArgs += $FinalPrompt
} else {
$CmdArgs += $arg
}
}
# Honour -v only when the driver names a verbose flag the mode has not already declared
if ($VerboseMode -and $Config.VerboseFlag -and ($CmdArgs -notcontains $Config.VerboseFlag)) {
$CmdArgs += $Config.VerboseFlag
$ExecLabel = "$ExecLabel (with $($Config.VerboseFlag))"
}
# Assemble docker execution arguments
$DockerArgs = @("run") + $StdinArgs + @("--rm", "-v", "${ResolvedPath}:/workspace:${WorkspaceMountFlag}")
if ($EnvArgs) { $DockerArgs += $EnvArgs }
if ($VolumeArgs) { $DockerArgs += $VolumeArgs }
$DockerArgs += @("$($Config.ImageName):$($Config.Tag)")
$DockerArgs += @($Config.CliCommand)
$DockerArgs += $CmdArgs
if ($HasPrompt) {
Write-Host "🤖 Executing: $ExecLabel"
} else {
Write-Host "🤖 Launching interactive CLI TUI..."
}
# The assembled invocation is otherwise invisible to the test suite, which is
# why the stdin/TTY and volume-path regressions went unnoticed.
if ($env:AGENT_TESTING -eq "true") {
Write-Host "🧪 Container flags: $($StdinArgs -join ' ')"
Write-Host "🧪 Volumes: $($Config.Volumes -join ' ')"
}
# Run docker
if ($Streaming) {
$FormatterPath = Join-Path $EngineDir $Config.StreamFormatter
$null | & docker $DockerArgs | python3 -u $FormatterPath
} else {
& docker $DockerArgs
}
if ($LASTEXITCODE -ne 0) {
Write-Host "--------------------------------------------------------"
Write-Host "❌ Container exited with error code $LASTEXITCODE." -ForegroundColor Red
if ($Config.LogPath -and $Config.Volumes -and $Config.Volumes.Count -gt 0) {
$FirstVolumeMapping = $Config.Volumes[0]
$VolumeName = ($FirstVolumeMapping -split ":")[0]
$LogPath = $Config.LogPath
Write-Host "🔍 Extracting latest logs from Docker volume '$VolumeName'..." -ForegroundColor Cyan
$ShCommand = 'latest_log=$(ls -t /volume/' + $LogPath + ' 2>/dev/null | head -n 1); if [ -f $latest_log ]; then echo === Latest Logs: $latest_log ===; tail -n 100 $latest_log; fi'
& docker run --rm -v "${VolumeName}:/volume" alpine sh -c $ShCommand
}
if (-not $IsEnvAuth) {
Write-Host "💡 Troubleshooting: $($Config.TroubleshootingTip)" -ForegroundColor Yellow
}
exit $LASTEXITCODE
}