prizmkit 1.1.63 → 1.1.64
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundled/VERSION.json +3 -3
- package/bundled/dev-pipeline-windows/.env.example +11 -0
- package/bundled/dev-pipeline-windows/SCHEMA_ANALYSIS.md +11 -0
- package/bundled/dev-pipeline-windows/lib/branch.ps1 +222 -0
- package/bundled/dev-pipeline-windows/lib/common.ps1 +160 -10
- package/bundled/dev-pipeline-windows/lib/pipeline.ps1 +332 -30
- package/bundled/dev-pipeline-windows/run-recovery.ps1 +101 -5
- package/bundled/dev-pipeline-windows/scripts/parse-stream-progress.py +96 -0
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/templates/project-memory-template.md +5 -0
- package/package.json +1 -1
package/bundled/VERSION.json
CHANGED
|
@@ -21,6 +21,17 @@
|
|
|
21
21
|
# MAX_RETRIES=3 # Max retry attempts per task before marking failed
|
|
22
22
|
# SESSION_TIMEOUT=0 # Session timeout in seconds (0 = no limit)
|
|
23
23
|
# VERBOSE=1 # Verbose logging (1=on, 0=off)
|
|
24
|
+
# HEARTBEAT_INTERVAL=30 # Poll interval for session progress/stale checks
|
|
25
|
+
# STALE_KILL_THRESHOLD=900 # Auto-kill session after N seconds without log progress (0 = disabled)
|
|
26
|
+
# STALE_KILL_GRACE_SECONDS=10 # Grace period after stale-kill before force-stopping the job
|
|
27
|
+
# CODEX_SUBAGENT_TIMEOUT_SECONDS=840 # Codex subagent max runtime; defaults to stale threshold - 60
|
|
28
|
+
# LOG_CLEANUP_ENABLED=1 # Run periodic session log cleanup
|
|
29
|
+
# LOG_RETENTION_DAYS=14 # Delete session logs older than N days
|
|
30
|
+
# LOG_MAX_TOTAL_MB=1024 # Keep total logs under N MB via oldest-first cleanup
|
|
31
|
+
# STOP_ON_FAILURE=0 # Stop after the first failed task (1=stop, 0=continue)
|
|
32
|
+
# ENABLE_DEPLOY=0 # Start a deploy session after all tasks complete successfully
|
|
33
|
+
# DEV_BRANCH= # Optional custom dev branch name for each task
|
|
34
|
+
# AUTO_PUSH=0 # Push original branch after successful merge (1=enabled)
|
|
24
35
|
|
|
25
36
|
# ─── Feature Pipeline Only ────────────────────────────────────────────
|
|
26
37
|
# ENABLE_CRITIC=false # Enable adversarial critic review (true/false)
|
|
@@ -345,6 +345,17 @@ pending, in_progress, completed, failed, skipped
|
|
|
345
345
|
| `MAX_RETRIES` | integer | (not specified) | Retry attempts per task |
|
|
346
346
|
| `SESSION_TIMEOUT` | integer | 0 | 0 = no limit |
|
|
347
347
|
| `VERBOSE` | integer | (not specified) | 1=on, 0=off |
|
|
348
|
+
| `HEARTBEAT_INTERVAL` | integer | 30 | Poll interval for session progress/stale checks |
|
|
349
|
+
| `STALE_KILL_THRESHOLD` | integer | 900 | Auto-kill after N seconds without log progress; 0 disables |
|
|
350
|
+
| `STALE_KILL_GRACE_SECONDS` | integer | 10 | Grace period after stale-kill before force-stopping |
|
|
351
|
+
| `CODEX_SUBAGENT_TIMEOUT_SECONDS` | integer | 840 | Codex subagent max runtime |
|
|
352
|
+
| `LOG_CLEANUP_ENABLED` | boolean | 1 | Periodic session log cleanup |
|
|
353
|
+
| `LOG_RETENTION_DAYS` | integer | 14 | Delete session logs older than N days |
|
|
354
|
+
| `LOG_MAX_TOTAL_MB` | integer | 1024 | Keep total logs under N MB |
|
|
355
|
+
| `STOP_ON_FAILURE` | boolean | 0 | Stop after the first failed task |
|
|
356
|
+
| `ENABLE_DEPLOY` | boolean | 0 | Start deploy session after all tasks complete |
|
|
357
|
+
| `DEV_BRANCH` | string | auto-generated | Optional custom dev branch name |
|
|
358
|
+
| `AUTO_PUSH` | boolean | 0 | Push original branch after successful merge |
|
|
348
359
|
| `ENABLE_CRITIC` | boolean | false | Adversarial review enable |
|
|
349
360
|
| `PIPELINE_MODE` | string | auto-detect | lite/standard/full override |
|
|
350
361
|
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
Set-StrictMode -Version 2.0
|
|
2
|
+
$ErrorActionPreference = 'Stop'
|
|
3
|
+
|
|
4
|
+
function Get-PrizmCurrentBranch {
|
|
5
|
+
param([string]$ProjectRoot)
|
|
6
|
+
$branch = & git -C $ProjectRoot rev-parse --abbrev-ref HEAD 2>$null
|
|
7
|
+
if ($LASTEXITCODE -eq 0 -and $branch) { return [string]($branch | Select-Object -First 1) }
|
|
8
|
+
return ''
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function Test-PrizmTrackedDirty {
|
|
12
|
+
param([string]$ProjectRoot)
|
|
13
|
+
$trackedDirty = & git -C $ProjectRoot diff --name-only 2>$null
|
|
14
|
+
$stagedDirty = & git -C $ProjectRoot diff --cached --name-only 2>$null
|
|
15
|
+
return -not [string]::IsNullOrWhiteSpace((@($trackedDirty) + @($stagedDirty) -join "`n"))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function New-PrizmDevBranch {
|
|
19
|
+
param(
|
|
20
|
+
[string]$ProjectRoot,
|
|
21
|
+
[string]$DevBranch,
|
|
22
|
+
[string]$SourceBranch
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
if (-not $DevBranch -or -not $SourceBranch) { return $false }
|
|
26
|
+
|
|
27
|
+
& git -C $ProjectRoot rev-parse --verify $DevBranch *> $null
|
|
28
|
+
if ($LASTEXITCODE -eq 0) {
|
|
29
|
+
Write-PrizmInfo "Branch already exists: $DevBranch - checking out"
|
|
30
|
+
& git -C $ProjectRoot checkout $DevBranch *> $null
|
|
31
|
+
if ($LASTEXITCODE -ne 0) {
|
|
32
|
+
Write-PrizmError "Failed to checkout existing branch: $DevBranch"
|
|
33
|
+
return $false
|
|
34
|
+
}
|
|
35
|
+
return $true
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
& git -C $ProjectRoot checkout -b $DevBranch $SourceBranch *> $null
|
|
39
|
+
if ($LASTEXITCODE -ne 0) {
|
|
40
|
+
Write-PrizmWarn "Failed to create branch: $DevBranch from $SourceBranch"
|
|
41
|
+
return $false
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
Write-PrizmInfo "Created and checked out branch: $DevBranch (from $SourceBranch)"
|
|
45
|
+
return $true
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function Switch-PrizmOriginalBranch {
|
|
49
|
+
param(
|
|
50
|
+
[string]$ProjectRoot,
|
|
51
|
+
[string]$OriginalBranch
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if (-not $OriginalBranch) { return $false }
|
|
55
|
+
$currentBranch = Get-PrizmCurrentBranch $ProjectRoot
|
|
56
|
+
if ($currentBranch -eq $OriginalBranch) { return $true }
|
|
57
|
+
|
|
58
|
+
& git -C $ProjectRoot checkout $OriginalBranch *> $null
|
|
59
|
+
if ($LASTEXITCODE -ne 0) {
|
|
60
|
+
Write-PrizmError "Failed to checkout original branch: $OriginalBranch"
|
|
61
|
+
return $false
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
Write-PrizmInfo "Returned to branch: $OriginalBranch"
|
|
65
|
+
return $true
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function Save-PrizmBranchWip {
|
|
69
|
+
param(
|
|
70
|
+
[string]$ProjectRoot,
|
|
71
|
+
[string]$DevBranch
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if (-not $DevBranch) { return $true }
|
|
75
|
+
|
|
76
|
+
$currentBranch = Get-PrizmCurrentBranch $ProjectRoot
|
|
77
|
+
if ($currentBranch -ne $DevBranch) { return $true }
|
|
78
|
+
|
|
79
|
+
$changes = & git -C $ProjectRoot status --porcelain 2>$null
|
|
80
|
+
if ([string]::IsNullOrWhiteSpace(($changes -join "`n"))) { return $true }
|
|
81
|
+
|
|
82
|
+
Write-PrizmWarn "Saving uncommitted work-in-progress on branch: $DevBranch"
|
|
83
|
+
& git -C $ProjectRoot add -A *> $null
|
|
84
|
+
if ($LASTEXITCODE -ne 0) {
|
|
85
|
+
Write-PrizmWarn "git add -A failed - uncommitted work may remain on $DevBranch"
|
|
86
|
+
return $true
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
& git -C $ProjectRoot commit --no-verify `
|
|
90
|
+
-m "wip($DevBranch): interrupted - uncommitted work saved" `
|
|
91
|
+
-m "Pipeline was interrupted or failed. This commit preserves work-in-progress." `
|
|
92
|
+
-m "To resume: git checkout $DevBranch" *> $null
|
|
93
|
+
|
|
94
|
+
if ($LASTEXITCODE -eq 0) {
|
|
95
|
+
Write-PrizmInfo "Saved uncommitted work on branch $DevBranch"
|
|
96
|
+
} else {
|
|
97
|
+
Write-PrizmWarn "git commit failed - uncommitted work may remain on $DevBranch"
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return $true
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function Merge-PrizmDevBranch {
|
|
104
|
+
param(
|
|
105
|
+
[string]$ProjectRoot,
|
|
106
|
+
[string]$DevBranch,
|
|
107
|
+
[string]$OriginalBranch,
|
|
108
|
+
[bool]$AutoPush = $false
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if (-not $DevBranch -or -not $OriginalBranch) { return $false }
|
|
112
|
+
|
|
113
|
+
$hadStash = $false
|
|
114
|
+
if (Test-PrizmTrackedDirty $ProjectRoot) {
|
|
115
|
+
& git -C $ProjectRoot stash push -m 'pipeline-merge-stash' *> $null
|
|
116
|
+
if ($LASTEXITCODE -eq 0) {
|
|
117
|
+
$hadStash = $true
|
|
118
|
+
} else {
|
|
119
|
+
Write-PrizmWarn "git stash failed - tracked changes may block branch merge"
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
Write-PrizmInfo "Merging $DevBranch into $OriginalBranch..."
|
|
124
|
+
& git -C $ProjectRoot rebase $OriginalBranch $DevBranch
|
|
125
|
+
if ($LASTEXITCODE -ne 0) {
|
|
126
|
+
Write-PrizmError "Rebase of $DevBranch onto $OriginalBranch failed - resolve manually"
|
|
127
|
+
& git -C $ProjectRoot rebase --abort *> $null
|
|
128
|
+
if ($hadStash) {
|
|
129
|
+
& git -C $ProjectRoot stash pop *> $null
|
|
130
|
+
if ($LASTEXITCODE -ne 0) { Write-PrizmWarn "git stash pop failed after rebase abort" }
|
|
131
|
+
}
|
|
132
|
+
return $false
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
& git -C $ProjectRoot checkout $OriginalBranch *> $null
|
|
136
|
+
if ($LASTEXITCODE -ne 0) {
|
|
137
|
+
Write-PrizmError "Failed to checkout $OriginalBranch for merge"
|
|
138
|
+
if ($hadStash) {
|
|
139
|
+
& git -C $ProjectRoot stash pop *> $null
|
|
140
|
+
if ($LASTEXITCODE -ne 0) { Write-PrizmWarn "git stash pop failed after checkout failure" }
|
|
141
|
+
}
|
|
142
|
+
return $false
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
& git -C $ProjectRoot merge --ff-only $DevBranch
|
|
146
|
+
if ($LASTEXITCODE -ne 0) {
|
|
147
|
+
Write-PrizmError "Merge failed after rebase - resolve manually"
|
|
148
|
+
if ($hadStash) {
|
|
149
|
+
& git -C $ProjectRoot stash pop *> $null
|
|
150
|
+
if ($LASTEXITCODE -ne 0) { Write-PrizmWarn "git stash pop failed after merge failure" }
|
|
151
|
+
}
|
|
152
|
+
return $false
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
Write-PrizmSuccess "Merged $DevBranch into $OriginalBranch"
|
|
156
|
+
|
|
157
|
+
if ($AutoPush) {
|
|
158
|
+
Write-PrizmInfo "Pushing $OriginalBranch to remote..."
|
|
159
|
+
& git -C $ProjectRoot push *> $null
|
|
160
|
+
if ($LASTEXITCODE -eq 0) {
|
|
161
|
+
Write-PrizmSuccess "Pushed $OriginalBranch to remote"
|
|
162
|
+
} else {
|
|
163
|
+
Write-PrizmWarn "Push failed - run git push manually"
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
& git -C $ProjectRoot branch -d $DevBranch *> $null
|
|
168
|
+
if ($LASTEXITCODE -eq 0) { Write-PrizmInfo "Deleted merged branch: $DevBranch" }
|
|
169
|
+
|
|
170
|
+
if ($hadStash) {
|
|
171
|
+
& git -C $ProjectRoot stash pop *> $null
|
|
172
|
+
if ($LASTEXITCODE -ne 0) { Write-PrizmWarn "git stash pop failed after merge" }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return $true
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function Restore-PrizmOriginalBranch {
|
|
179
|
+
param(
|
|
180
|
+
[string]$ProjectRoot,
|
|
181
|
+
[string]$OriginalBranch,
|
|
182
|
+
[string]$DevBranch = ''
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
if (-not $OriginalBranch) { return $true }
|
|
186
|
+
|
|
187
|
+
& git -C $ProjectRoot rebase --show-current-patch *> $null
|
|
188
|
+
if ($LASTEXITCODE -eq 0) {
|
|
189
|
+
Write-PrizmWarn "Aborting in-progress rebase..."
|
|
190
|
+
& git -C $ProjectRoot rebase --abort *> $null
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
$currentBranch = Get-PrizmCurrentBranch $ProjectRoot
|
|
194
|
+
if ($currentBranch -eq $OriginalBranch) { return $true }
|
|
195
|
+
|
|
196
|
+
$wipBranch = if ($DevBranch) { $DevBranch } else { $currentBranch }
|
|
197
|
+
if ($wipBranch -and $wipBranch -ne $OriginalBranch) {
|
|
198
|
+
Save-PrizmBranchWip $ProjectRoot $wipBranch | Out-Null
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
Write-PrizmInfo "Ensuring return to original branch: $OriginalBranch (currently on: $currentBranch)"
|
|
202
|
+
|
|
203
|
+
$hadStash = $false
|
|
204
|
+
if (Test-PrizmTrackedDirty $ProjectRoot) {
|
|
205
|
+
& git -C $ProjectRoot stash push -m 'pipeline-ensure-return-stash' *> $null
|
|
206
|
+
if ($LASTEXITCODE -eq 0) { $hadStash = $true }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
& git -C $ProjectRoot checkout $OriginalBranch *> $null
|
|
210
|
+
if ($LASTEXITCODE -eq 0) {
|
|
211
|
+
Write-PrizmInfo "Returned to branch: $OriginalBranch"
|
|
212
|
+
} else {
|
|
213
|
+
Write-PrizmError "Failed to checkout $OriginalBranch - manual recovery needed"
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if ($hadStash) {
|
|
217
|
+
& git -C $ProjectRoot stash pop *> $null
|
|
218
|
+
if ($LASTEXITCODE -ne 0) { Write-PrizmWarn "git stash pop failed during branch return" }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return $true
|
|
222
|
+
}
|
|
@@ -298,6 +298,90 @@ function Get-PrizmSessionPlatform {
|
|
|
298
298
|
return Get-PrizmPlatformFromCli $CliCommand
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
+
function Get-PrizmCodexSubagentTimeoutSeconds {
|
|
302
|
+
$configuredTimeout = 0
|
|
303
|
+
if ([int]::TryParse($env:CODEX_SUBAGENT_TIMEOUT_SECONDS, [ref]$configuredTimeout) -and $configuredTimeout -gt 0) {
|
|
304
|
+
return $configuredTimeout
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
$outerThreshold = 0
|
|
308
|
+
$outerThresholdText = if ($env:STALE_KILL_THRESHOLD) { $env:STALE_KILL_THRESHOLD } else { $env:SESSION_TIMEOUT }
|
|
309
|
+
if ([int]::TryParse($outerThresholdText, [ref]$outerThreshold) -and $outerThreshold -gt 120) {
|
|
310
|
+
return ($outerThreshold - 60)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return 840
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function Test-PrizmCodexJsonSupport {
|
|
317
|
+
param([string]$CliExecutable)
|
|
318
|
+
try {
|
|
319
|
+
$helpOutput = & $CliExecutable exec --help 2>&1
|
|
320
|
+
return (($helpOutput -join "`n") -match '--json')
|
|
321
|
+
} catch {
|
|
322
|
+
return $false
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function Test-PrizmStreamJsonSupport {
|
|
327
|
+
param([string]$CliCommand)
|
|
328
|
+
$sessionPlatform = Get-PrizmSessionPlatform $CliCommand
|
|
329
|
+
$parsedCli = Split-PrizmCommandLine $CliCommand
|
|
330
|
+
|
|
331
|
+
if ($sessionPlatform -eq 'codebuddy') { return $true }
|
|
332
|
+
if ($sessionPlatform -eq 'codex') { return (Test-PrizmCodexJsonSupport $parsedCli.Command) }
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
$helpOutput = & $parsedCli.Command --help 2>&1
|
|
336
|
+
return (($helpOutput -join "`n") -match 'stream-json')
|
|
337
|
+
} catch {
|
|
338
|
+
return $false
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function Start-PrizmProgressParser {
|
|
343
|
+
param(
|
|
344
|
+
[string[]]$PythonCommand,
|
|
345
|
+
[string]$ScriptsDir,
|
|
346
|
+
[string]$SessionLog,
|
|
347
|
+
[string]$ProgressFile,
|
|
348
|
+
[string]$CliCommand
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
if (-not (Test-PrizmStreamJsonSupport $CliCommand)) { return $null }
|
|
352
|
+
|
|
353
|
+
$parserScript = Join-Path $ScriptsDir 'parse-stream-progress.py'
|
|
354
|
+
if (-not (Test-Path $parserScript)) { return $null }
|
|
355
|
+
|
|
356
|
+
$cmd = $PythonCommand[0]
|
|
357
|
+
$prefix = @()
|
|
358
|
+
if ($PythonCommand.Count -gt 1) { $prefix = $PythonCommand[1..($PythonCommand.Count - 1)] }
|
|
359
|
+
|
|
360
|
+
$progressDir = Split-Path $ProgressFile -Parent
|
|
361
|
+
if ($progressDir) { New-Item -ItemType Directory -Force -Path $progressDir | Out-Null }
|
|
362
|
+
|
|
363
|
+
$psi = [System.Diagnostics.ProcessStartInfo]::new()
|
|
364
|
+
$psi.UseShellExecute = $false
|
|
365
|
+
$psi.CreateNoWindow = $true
|
|
366
|
+
$psi.FileName = $cmd
|
|
367
|
+
$psi.Arguments = Join-PrizmProcessArguments ($prefix + @($parserScript, '--session-log', $SessionLog, '--progress-file', $ProgressFile))
|
|
368
|
+
return [System.Diagnostics.Process]::Start($psi)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function Stop-PrizmProgressParser {
|
|
372
|
+
param($Process)
|
|
373
|
+
if ($Process -eq $null) { return }
|
|
374
|
+
try {
|
|
375
|
+
if (-not $Process.HasExited) {
|
|
376
|
+
try {
|
|
377
|
+
$Process.Kill($true)
|
|
378
|
+
} catch {
|
|
379
|
+
Stop-PrizmProcessTreeById -ProcessId ([int]$Process.Id)
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
} catch {}
|
|
383
|
+
}
|
|
384
|
+
|
|
301
385
|
function Stop-PrizmProcessTreeById {
|
|
302
386
|
param([int]$ProcessId)
|
|
303
387
|
if ($ProcessId -le 0) { return }
|
|
@@ -328,6 +412,28 @@ function Stop-PrizmProcessTreeById {
|
|
|
328
412
|
}
|
|
329
413
|
}
|
|
330
414
|
|
|
415
|
+
function Stop-PrizmSessionProcess {
|
|
416
|
+
param([string]$PidPath)
|
|
417
|
+
if (-not (Test-Path $PidPath)) { return }
|
|
418
|
+
$rawPid = Get-Content $PidPath -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
419
|
+
$aiPid = 0
|
|
420
|
+
if ([int]::TryParse($rawPid, [ref]$aiPid)) {
|
|
421
|
+
Stop-PrizmProcessTreeById -ProcessId $aiPid
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function Write-PrizmStaleKillMarker {
|
|
426
|
+
param([string]$MarkerPath, [int]$StaleSeconds, [int]$Threshold)
|
|
427
|
+
$markerDir = Split-Path $MarkerPath -Parent
|
|
428
|
+
if ($markerDir) { New-Item -ItemType Directory -Force -Path $markerDir | Out-Null }
|
|
429
|
+
[ordered]@{
|
|
430
|
+
killed_at = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
|
|
431
|
+
reason = 'stale_session'
|
|
432
|
+
stale_seconds = $StaleSeconds
|
|
433
|
+
threshold = $Threshold
|
|
434
|
+
} | ConvertTo-Json -Compress | Set-Content -Path $MarkerPath -Encoding UTF8
|
|
435
|
+
}
|
|
436
|
+
|
|
331
437
|
function Invoke-PrizmAiSession {
|
|
332
438
|
param(
|
|
333
439
|
[string]$CliCommand,
|
|
@@ -352,16 +458,27 @@ function Invoke-PrizmAiSession {
|
|
|
352
458
|
$parsedCli = Split-PrizmCommandLine $CliCommand
|
|
353
459
|
$cliExecutable = $parsedCli.Command
|
|
354
460
|
$sessionPlatform = Get-PrizmSessionPlatform $CliCommand
|
|
461
|
+
$useStreamJson = Test-PrizmStreamJsonSupport $CliCommand
|
|
355
462
|
|
|
356
463
|
if ($sessionPlatform -eq 'claude') {
|
|
357
464
|
$cliArgs += @('-p', '--dangerously-skip-permissions')
|
|
465
|
+
if ($env:VERBOSE -in @('1','true','yes','on') -or $useStreamJson) { $cliArgs += '--verbose' }
|
|
466
|
+
if ($useStreamJson) { $cliArgs += @('--output-format', 'stream-json') }
|
|
358
467
|
if ($Model) { $cliArgs += @('--model', $Model) }
|
|
359
468
|
} elseif ($sessionPlatform -eq 'codex') {
|
|
360
|
-
$cliArgs += @('--ask-for-approval', 'never', '--sandbox', 'danger-full-access'
|
|
469
|
+
$cliArgs += @('--ask-for-approval', 'never', '--sandbox', 'danger-full-access')
|
|
470
|
+
$codexSubagentTimeout = Get-PrizmCodexSubagentTimeoutSeconds
|
|
471
|
+
if ($codexSubagentTimeout -gt 0) {
|
|
472
|
+
$cliArgs += @('--config', "agents.job_max_runtime_seconds=$codexSubagentTimeout")
|
|
473
|
+
}
|
|
474
|
+
$cliArgs += @('exec', '--cd', $ProjectRoot, '--skip-git-repo-check')
|
|
475
|
+
if ($useStreamJson) { $cliArgs += '--json' }
|
|
361
476
|
if ($Model) { $cliArgs += @('--model', $Model) }
|
|
362
477
|
$cliArgs += '-'
|
|
363
478
|
} else {
|
|
364
479
|
$cliArgs += @('--print', '-y')
|
|
480
|
+
if ($env:VERBOSE -in @('1','true','yes','on')) { $cliArgs += '--verbose' }
|
|
481
|
+
if ($useStreamJson) { $cliArgs += @('--output-format', 'stream-json') }
|
|
365
482
|
if ($Model) { $cliArgs += @('--model', $Model) }
|
|
366
483
|
}
|
|
367
484
|
$generatedArgs = (($cliArgs | ForEach-Object { ConvertTo-PrizmProcessArgument $_ }) -join ' ')
|
|
@@ -390,20 +507,53 @@ function Invoke-PrizmAiSession {
|
|
|
390
507
|
|
|
391
508
|
$process = [System.Diagnostics.Process]::Start($psi)
|
|
392
509
|
$script:PrizmAiProcess = $process
|
|
510
|
+
$logWriter = [System.IO.StreamWriter]::new($LogPath, $false, [System.Text.UTF8Encoding]::new($false))
|
|
511
|
+
$logLock = [object]::new()
|
|
512
|
+
$outputHandler = [System.Diagnostics.DataReceivedEventHandler]{
|
|
513
|
+
param($sender, $eventArgs)
|
|
514
|
+
if ($eventArgs.Data -ne $null) {
|
|
515
|
+
[System.Threading.Monitor]::Enter($logLock)
|
|
516
|
+
try {
|
|
517
|
+
$logWriter.WriteLine($eventArgs.Data)
|
|
518
|
+
$logWriter.Flush()
|
|
519
|
+
} finally {
|
|
520
|
+
[System.Threading.Monitor]::Exit($logLock)
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
$errorHandler = [System.Diagnostics.DataReceivedEventHandler]{
|
|
525
|
+
param($sender, $eventArgs)
|
|
526
|
+
if ($eventArgs.Data -ne $null) {
|
|
527
|
+
[System.Threading.Monitor]::Enter($logLock)
|
|
528
|
+
try {
|
|
529
|
+
$logWriter.WriteLine($eventArgs.Data)
|
|
530
|
+
$logWriter.Flush()
|
|
531
|
+
} finally {
|
|
532
|
+
[System.Threading.Monitor]::Exit($logLock)
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
$process.add_OutputDataReceived($outputHandler)
|
|
537
|
+
$process.add_ErrorDataReceived($errorHandler)
|
|
393
538
|
if ($PidPath) {
|
|
394
539
|
$pidDir = Split-Path $PidPath -Parent
|
|
395
540
|
if ($pidDir) { New-Item -ItemType Directory -Force -Path $pidDir | Out-Null }
|
|
396
541
|
Set-Content -Path $PidPath -Value ([string]$process.Id) -Encoding UTF8
|
|
397
542
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
543
|
+
try {
|
|
544
|
+
$process.BeginOutputReadLine()
|
|
545
|
+
$process.BeginErrorReadLine()
|
|
546
|
+
$process.StandardInput.Write($prompt)
|
|
547
|
+
$process.StandardInput.Close()
|
|
548
|
+
$process.WaitForExit()
|
|
549
|
+
$process.WaitForExit()
|
|
550
|
+
return $process.ExitCode
|
|
551
|
+
} finally {
|
|
552
|
+
$process.remove_OutputDataReceived($outputHandler)
|
|
553
|
+
$process.remove_ErrorDataReceived($errorHandler)
|
|
554
|
+
$logWriter.Close()
|
|
555
|
+
$logWriter.Dispose()
|
|
556
|
+
}
|
|
407
557
|
}
|
|
408
558
|
|
|
409
559
|
function New-PrizmSessionId {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
. "$PSScriptRoot\common.ps1"
|
|
2
|
+
. "$PSScriptRoot\branch.ps1"
|
|
2
3
|
$global:PRIZM_EXIT_CODE = 0
|
|
3
4
|
|
|
4
5
|
function Invoke-PrizmPipeline {
|
|
@@ -83,6 +84,54 @@ function Invoke-PrizmPipeline {
|
|
|
83
84
|
}
|
|
84
85
|
$timeoutSeconds = $parsedEnvTimeout
|
|
85
86
|
}
|
|
87
|
+
$heartbeatInterval = 30
|
|
88
|
+
if ($env:HEARTBEAT_INTERVAL) {
|
|
89
|
+
$parsedHeartbeatInterval = 0
|
|
90
|
+
if (-not [int]::TryParse($env:HEARTBEAT_INTERVAL, [ref]$parsedHeartbeatInterval) -or $parsedHeartbeatInterval -lt 1) {
|
|
91
|
+
throw "HEARTBEAT_INTERVAL must be a positive integer: $($env:HEARTBEAT_INTERVAL)"
|
|
92
|
+
}
|
|
93
|
+
$heartbeatInterval = $parsedHeartbeatInterval
|
|
94
|
+
}
|
|
95
|
+
$staleKillThreshold = 900
|
|
96
|
+
if ($env:STALE_KILL_THRESHOLD) {
|
|
97
|
+
$parsedStaleKillThreshold = 0
|
|
98
|
+
if (-not [int]::TryParse($env:STALE_KILL_THRESHOLD, [ref]$parsedStaleKillThreshold) -or $parsedStaleKillThreshold -lt 0) {
|
|
99
|
+
throw "STALE_KILL_THRESHOLD must be a non-negative integer: $($env:STALE_KILL_THRESHOLD)"
|
|
100
|
+
}
|
|
101
|
+
$staleKillThreshold = $parsedStaleKillThreshold
|
|
102
|
+
}
|
|
103
|
+
$staleKillGraceSeconds = 10
|
|
104
|
+
if ($env:STALE_KILL_GRACE_SECONDS) {
|
|
105
|
+
$parsedStaleKillGraceSeconds = 0
|
|
106
|
+
if (-not [int]::TryParse($env:STALE_KILL_GRACE_SECONDS, [ref]$parsedStaleKillGraceSeconds) -or $parsedStaleKillGraceSeconds -lt 0) {
|
|
107
|
+
throw "STALE_KILL_GRACE_SECONDS must be a non-negative integer: $($env:STALE_KILL_GRACE_SECONDS)"
|
|
108
|
+
}
|
|
109
|
+
$staleKillGraceSeconds = $parsedStaleKillGraceSeconds
|
|
110
|
+
}
|
|
111
|
+
$autoPush = $env:AUTO_PUSH -in @('1','true','yes','on')
|
|
112
|
+
$enableDeploy = $env:ENABLE_DEPLOY -in @('1','true','yes','on')
|
|
113
|
+
$stopOnFailure = $env:STOP_ON_FAILURE -in @('1','true','yes','on')
|
|
114
|
+
$devBranchOverride = if ($env:DEV_BRANCH) { $env:DEV_BRANCH.Trim() } else { '' }
|
|
115
|
+
$logCleanupEnabled = $true
|
|
116
|
+
if ($env:LOG_CLEANUP_ENABLED) {
|
|
117
|
+
$logCleanupEnabled = $env:LOG_CLEANUP_ENABLED -in @('1','true','yes','on')
|
|
118
|
+
}
|
|
119
|
+
$logRetentionDays = 14
|
|
120
|
+
if ($env:LOG_RETENTION_DAYS) {
|
|
121
|
+
$parsedLogRetentionDays = 0
|
|
122
|
+
if (-not [int]::TryParse($env:LOG_RETENTION_DAYS, [ref]$parsedLogRetentionDays) -or $parsedLogRetentionDays -lt 0) {
|
|
123
|
+
throw "LOG_RETENTION_DAYS must be a non-negative integer: $($env:LOG_RETENTION_DAYS)"
|
|
124
|
+
}
|
|
125
|
+
$logRetentionDays = $parsedLogRetentionDays
|
|
126
|
+
}
|
|
127
|
+
$logMaxTotalMb = 1024
|
|
128
|
+
if ($env:LOG_MAX_TOTAL_MB) {
|
|
129
|
+
$parsedLogMaxTotalMb = 0
|
|
130
|
+
if (-not [int]::TryParse($env:LOG_MAX_TOTAL_MB, [ref]$parsedLogMaxTotalMb) -or $parsedLogMaxTotalMb -lt 0) {
|
|
131
|
+
throw "LOG_MAX_TOTAL_MB must be a non-negative integer: $($env:LOG_MAX_TOTAL_MB)"
|
|
132
|
+
}
|
|
133
|
+
$logMaxTotalMb = $parsedLogMaxTotalMb
|
|
134
|
+
}
|
|
86
135
|
$featuresFilter = $null
|
|
87
136
|
|
|
88
137
|
for ($i = 0; $i -lt $remaining.Count; $i++) {
|
|
@@ -130,7 +179,7 @@ function Invoke-PrizmPipeline {
|
|
|
130
179
|
$criticLabel = if ($critic) { $critic } else { 'plan-default' }
|
|
131
180
|
$retryLabel = if ($maxRetries -ne $null) { [string]$maxRetries } else { 'default' }
|
|
132
181
|
Write-PrizmInfo "Verbose mode enabled."
|
|
133
|
-
Write-PrizmInfo "Effective options: mode=$modeLabel critic=$criticLabel maxRetries=$retryLabel timeoutSeconds=$timeoutSeconds dryRun=$dryRun"
|
|
182
|
+
Write-PrizmInfo "Effective options: mode=$modeLabel critic=$criticLabel maxRetries=$retryLabel timeoutSeconds=$timeoutSeconds staleKillThreshold=$staleKillThreshold dryRun=$dryRun"
|
|
134
183
|
}
|
|
135
184
|
|
|
136
185
|
if (-not (Test-Path $listPath)) { throw "List file not found: $listPath" }
|
|
@@ -227,6 +276,28 @@ function Invoke-PrizmPipeline {
|
|
|
227
276
|
return -not [string]::IsNullOrWhiteSpace([string]$dirty)
|
|
228
277
|
}
|
|
229
278
|
|
|
279
|
+
function Stop-PrizmSessionProcess {
|
|
280
|
+
param([string]$PidPath)
|
|
281
|
+
if (-not (Test-Path $PidPath)) { return }
|
|
282
|
+
$rawPid = Get-Content $PidPath -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
283
|
+
$aiPid = 0
|
|
284
|
+
if ([int]::TryParse($rawPid, [ref]$aiPid)) {
|
|
285
|
+
Stop-PrizmProcessTreeById -ProcessId $aiPid
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function Write-PrizmStaleKillMarker {
|
|
290
|
+
param([string]$MarkerPath, [int]$StaleSeconds, [int]$Threshold)
|
|
291
|
+
$markerDir = Split-Path $MarkerPath -Parent
|
|
292
|
+
if ($markerDir) { New-Item -ItemType Directory -Force -Path $markerDir | Out-Null }
|
|
293
|
+
[ordered]@{
|
|
294
|
+
killed_at = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
|
|
295
|
+
reason = 'stale_session'
|
|
296
|
+
stale_seconds = $StaleSeconds
|
|
297
|
+
threshold = $Threshold
|
|
298
|
+
} | ConvertTo-Json -Compress | Set-Content -Path $MarkerPath -Encoding UTF8
|
|
299
|
+
}
|
|
300
|
+
|
|
230
301
|
function Invoke-PrizmGitAutoCommit {
|
|
231
302
|
param([string]$ProjectRoot, [string]$Message)
|
|
232
303
|
& git -C $ProjectRoot add -A *> $null
|
|
@@ -251,6 +322,140 @@ function Invoke-PrizmPipeline {
|
|
|
251
322
|
& git -C $ProjectRoot commit --no-verify --amend --no-edit --only -- $listRel *> $null
|
|
252
323
|
}
|
|
253
324
|
|
|
325
|
+
function Invoke-PrizmGitCommitPath {
|
|
326
|
+
param([string]$ProjectRoot, [string]$Path, [string]$Message)
|
|
327
|
+
if (-not $Path) { return $false }
|
|
328
|
+
$relPath = ConvertTo-PrizmGitRelativePath $ProjectRoot $Path
|
|
329
|
+
if (-not $relPath) { return $false }
|
|
330
|
+
& git -C $ProjectRoot diff --quiet -- $relPath 2>$null
|
|
331
|
+
if ($LASTEXITCODE -eq 0) { return $false }
|
|
332
|
+
& git -C $ProjectRoot add -- $relPath *> $null
|
|
333
|
+
if ($LASTEXITCODE -ne 0) { return $false }
|
|
334
|
+
& git -C $ProjectRoot commit --no-verify -m $Message *> $null
|
|
335
|
+
return $LASTEXITCODE -eq 0
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function New-PrizmDefaultDevBranchName {
|
|
339
|
+
param([string]$Kind, [string]$CurrentItemId)
|
|
340
|
+
$timestamp = Get-Date -Format 'yyyyMMddHHmm'
|
|
341
|
+
switch ($Kind) {
|
|
342
|
+
'feature' { return "dev/$CurrentItemId-$timestamp" }
|
|
343
|
+
'bugfix' { return "bugfix/$CurrentItemId-$timestamp" }
|
|
344
|
+
'refactor' { return "refactor/$CurrentItemId-$timestamp" }
|
|
345
|
+
default { return "dev/$CurrentItemId-$timestamp" }
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function Invoke-PrizmLogCleanup {
|
|
350
|
+
if (-not $logCleanupEnabled) { return }
|
|
351
|
+
if (-not (Test-Path $stateDir)) { return }
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
$cleanup = Invoke-PrizmPythonJson $python @(
|
|
355
|
+
(Join-Path $paths.ScriptsDir 'cleanup-logs.py'),
|
|
356
|
+
'--state-dir', $stateDir,
|
|
357
|
+
'--retention-days', [string]$logRetentionDays,
|
|
358
|
+
'--max-total-mb', [string]$logMaxTotalMb
|
|
359
|
+
)
|
|
360
|
+
if ($cleanup -and $cleanup.deleted_files -gt 0) {
|
|
361
|
+
$reclaimedKb = [int]([int64]$cleanup.reclaimed_bytes / 1024)
|
|
362
|
+
Write-PrizmInfo "Log cleanup: deleted $($cleanup.deleted_files) files, reclaimed ${reclaimedKb}KB"
|
|
363
|
+
}
|
|
364
|
+
} catch {
|
|
365
|
+
Write-PrizmWarn "Log cleanup failed (continuing)"
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function Get-PrizmDeployIncompleteItems {
|
|
370
|
+
param([string]$Kind, [string]$ListPath)
|
|
371
|
+
|
|
372
|
+
if (-not (Test-Path $ListPath)) { return @("List file not found: $ListPath") }
|
|
373
|
+
try {
|
|
374
|
+
$data = Get-Content -Raw $ListPath | ConvertFrom-Json
|
|
375
|
+
} catch {
|
|
376
|
+
return @("List file is not valid JSON: $ListPath")
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
$collectionName = @{ feature = 'features'; bugfix = 'bugs'; refactor = 'refactors' }[$Kind]
|
|
380
|
+
$allowedStatuses = @{
|
|
381
|
+
feature = @('completed', 'skipped')
|
|
382
|
+
bugfix = @('completed', 'skipped', 'needs_info')
|
|
383
|
+
refactor = @('completed', 'skipped')
|
|
384
|
+
}[$Kind]
|
|
385
|
+
|
|
386
|
+
$items = @()
|
|
387
|
+
$property = $data.PSObject.Properties[$collectionName]
|
|
388
|
+
if ($property -and $property.Value) { $items = @($property.Value) }
|
|
389
|
+
|
|
390
|
+
$bad = @()
|
|
391
|
+
foreach ($item in $items) {
|
|
392
|
+
$status = if ($item.PSObject.Properties['status']) { [string]$item.status } else { 'unknown' }
|
|
393
|
+
if ($status -notin $allowedStatuses) {
|
|
394
|
+
$itemIdText = if ($item.PSObject.Properties['id']) { [string]$item.id } else { 'unknown' }
|
|
395
|
+
$titleText = if ($item.PSObject.Properties['title']) { [string]$item.title } else { '' }
|
|
396
|
+
$bad += " ${itemIdText}: $status - $titleText"
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return @($bad)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function Invoke-PrizmDeploySession {
|
|
403
|
+
param([string]$Kind, [string]$ListPath)
|
|
404
|
+
|
|
405
|
+
if (-not $enableDeploy) { return 0 }
|
|
406
|
+
|
|
407
|
+
$incomplete = @(Get-PrizmDeployIncompleteItems $Kind $ListPath)
|
|
408
|
+
if ($incomplete.Count -gt 0) {
|
|
409
|
+
Write-PrizmWarn "DEPLOY BLOCKED: $($incomplete.Count) task(s) not completed successfully."
|
|
410
|
+
foreach ($line in $incomplete) { Write-PrizmWarn $line }
|
|
411
|
+
Write-PrizmWarn "Fix failed tasks and re-run, or manually run /prizmkit-deploy."
|
|
412
|
+
return 1
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
Write-PrizmInfo "All tasks completed - starting deploy session..."
|
|
416
|
+
Write-PrizmInfo "ENABLE_DEPLOY=1"
|
|
417
|
+
|
|
418
|
+
$deploySessionId = "deploy-$(Get-Date -Format 'yyyyMMddHHmmss')"
|
|
419
|
+
$deploySessionDir = Join-Path $stateDir "deploy\$deploySessionId"
|
|
420
|
+
$deployLogsDir = Join-Path $deploySessionDir 'logs'
|
|
421
|
+
New-Item -ItemType Directory -Force -Path $deployLogsDir | Out-Null
|
|
422
|
+
|
|
423
|
+
$deployPrompt = Join-Path $deploySessionDir 'bootstrap-prompt.md'
|
|
424
|
+
$deployLog = Join-Path $deployLogsDir 'session.log'
|
|
425
|
+
$deployPidPath = Join-Path $deployLogsDir 'ai.pid'
|
|
426
|
+
$deployBranch = Get-PrizmCurrentBranch $paths.ProjectRoot
|
|
427
|
+
if (-not $deployBranch) { $deployBranch = 'unknown' }
|
|
428
|
+
$deployCommit = & git -C $paths.ProjectRoot rev-parse --short HEAD 2>$null
|
|
429
|
+
if ($LASTEXITCODE -ne 0 -or -not $deployCommit) { $deployCommit = 'unknown' }
|
|
430
|
+
$deployCommit = [string]($deployCommit | Select-Object -First 1)
|
|
431
|
+
|
|
432
|
+
@(
|
|
433
|
+
'## Deploy',
|
|
434
|
+
'',
|
|
435
|
+
"All $Kind tasks in the pipeline completed successfully.",
|
|
436
|
+
'',
|
|
437
|
+
"- Branch: $deployBranch",
|
|
438
|
+
"- Commit: $deployCommit",
|
|
439
|
+
'',
|
|
440
|
+
'Run /prizmkit-deploy to deploy the project. Read .prizmkit/deploy/deploy.config.json',
|
|
441
|
+
'for deployment configuration. If no deploy config exists, guide the user through',
|
|
442
|
+
'setting one up before deploying.'
|
|
443
|
+
) | Set-Content -Path $deployPrompt -Encoding UTF8
|
|
444
|
+
|
|
445
|
+
Write-PrizmInfo "Deploy prompt: $deployPrompt"
|
|
446
|
+
Write-PrizmInfo "Deploy log: $deployLog"
|
|
447
|
+
|
|
448
|
+
$cli = Resolve-PrizmAiCli $paths.ProjectRoot $paths.PrizmkitDir
|
|
449
|
+
$env:PRIZMKIT_PLATFORM = Get-PrizmPlatformFromProject $paths.ProjectRoot $paths.PrizmkitDir $cli
|
|
450
|
+
$exitCode = Invoke-PrizmAiSession -CliCommand $cli -PromptPath $deployPrompt -LogPath $deployLog -ProjectRoot $paths.ProjectRoot -Model $env:MODEL -PidPath $deployPidPath
|
|
451
|
+
if ($exitCode -eq 0) {
|
|
452
|
+
Write-PrizmSuccess "Deploy session completed"
|
|
453
|
+
} else {
|
|
454
|
+
Write-PrizmWarn "Deploy session failed with exit code $exitCode"
|
|
455
|
+
}
|
|
456
|
+
return $exitCode
|
|
457
|
+
}
|
|
458
|
+
|
|
254
459
|
function Invoke-PrizmPipelineItem {
|
|
255
460
|
param([string]$CurrentItemId)
|
|
256
461
|
$script:PRIZM_ITEM_EXIT_CODE = 0
|
|
@@ -260,7 +465,8 @@ function Invoke-PrizmPipeline {
|
|
|
260
465
|
$retryCount = '0'
|
|
261
466
|
$resumePhase = 'null'
|
|
262
467
|
$isGitRepository = if (-not $dryRun) { Test-PrizmGitRepository $paths.ProjectRoot } else { $false }
|
|
263
|
-
$
|
|
468
|
+
$originalBranch = if ($isGitRepository) { Get-PrizmCurrentBranch $paths.ProjectRoot } else { '' }
|
|
469
|
+
$devBranchName = ''
|
|
264
470
|
$hadDirtyBaseline = if ($isGitRepository) { Test-PrizmGitWorkDirty $paths.ProjectRoot $stateDir $listPath } else { $false }
|
|
265
471
|
if ($hadDirtyBaseline) {
|
|
266
472
|
Write-PrizmWarn "Dirty working tree detected before pipeline bookkeeping; session success requires a new commit."
|
|
@@ -281,8 +487,21 @@ function Invoke-PrizmPipeline {
|
|
|
281
487
|
$start = Invoke-PrizmPythonJson $python (@((Join-Path $paths.ScriptsDir $updateScript), $listOption, $listPath, '--state-dir', $stateDir, '--action', 'start', $idOption, $CurrentItemId, '--session-id', $sessionId) + $maxRetryArgs)
|
|
282
488
|
if ($start.retry_count -ne $null) { $retryCount = [string]$start.retry_count }
|
|
283
489
|
if ($start.resume_from_phase -ne $null) { $resumePhase = [string]$start.resume_from_phase }
|
|
490
|
+
|
|
491
|
+
if ($isGitRepository -and $originalBranch) {
|
|
492
|
+
Invoke-PrizmGitCommitPath $paths.ProjectRoot $listPath "chore($CurrentItemId): mark in_progress" | Out-Null
|
|
493
|
+
$candidateBranch = if ($devBranchOverride) { $devBranchOverride } else { New-PrizmDefaultDevBranchName $Kind $CurrentItemId }
|
|
494
|
+
if (New-PrizmDevBranch $paths.ProjectRoot $candidateBranch $originalBranch) {
|
|
495
|
+
$devBranchName = $candidateBranch
|
|
496
|
+
Write-PrizmInfo "Dev branch: $devBranchName"
|
|
497
|
+
} else {
|
|
498
|
+
Write-PrizmWarn "Failed to create dev branch; running on current branch: $originalBranch"
|
|
499
|
+
}
|
|
500
|
+
}
|
|
284
501
|
}
|
|
285
502
|
|
|
503
|
+
$baseCommit = if ($isGitRepository) { Get-PrizmGitHead $paths.ProjectRoot } else { '' }
|
|
504
|
+
|
|
286
505
|
$sessionDir = if ($dryRun) {
|
|
287
506
|
Join-Path ([System.IO.Path]::GetTempPath()) "prizmkit-dry-run\$sessionId"
|
|
288
507
|
} else {
|
|
@@ -321,35 +540,84 @@ function Invoke-PrizmPipeline {
|
|
|
321
540
|
|
|
322
541
|
Write-PrizmInfo "Starting $cli session for $CurrentItemId ($sessionId)"
|
|
323
542
|
|
|
543
|
+
$progressJson = Join-Path $logsDir 'progress.json'
|
|
544
|
+
$parserProcess = Start-PrizmProgressParser -PythonCommand $python -ScriptsDir $paths.ScriptsDir -SessionLog $sessionLog -ProgressFile $progressJson -CliCommand $cli
|
|
545
|
+
|
|
324
546
|
$job = Start-Job -ScriptBlock {
|
|
325
547
|
param($commonPath, $cli, $promptPath, $sessionLog, $projectRoot, $model, $pidPath)
|
|
326
548
|
. $commonPath
|
|
327
549
|
Invoke-PrizmAiSession -CliCommand $cli -PromptPath $promptPath -LogPath $sessionLog -ProjectRoot $projectRoot -Model $model -PidPath $pidPath
|
|
328
550
|
} -ArgumentList (Join-Path $paths.PipelineDir 'lib\common.ps1'), $cli, $promptPath, $sessionLog, $paths.ProjectRoot, $effectiveModel, $pidPath
|
|
329
551
|
|
|
330
|
-
$
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
552
|
+
$elapsedSeconds = 0
|
|
553
|
+
$staleSeconds = 0
|
|
554
|
+
$previousLogSize = 0
|
|
555
|
+
$wasTimedOut = $false
|
|
556
|
+
$staleKillMarker = Join-Path $logsDir 'stale-kill.json'
|
|
557
|
+
$wasStaleKilled = $false
|
|
558
|
+
while ($true) {
|
|
559
|
+
$remainingTimeout = if ($timeoutSeconds -gt 0) { $timeoutSeconds - $elapsedSeconds } else { $heartbeatInterval }
|
|
560
|
+
$waitSeconds = if ($timeoutSeconds -gt 0) { [Math]::Min($heartbeatInterval, [Math]::Max(1, $remainingTimeout)) } else { $heartbeatInterval }
|
|
561
|
+
$completed = Wait-Job $job -Timeout $waitSeconds
|
|
562
|
+
if ($completed) { break }
|
|
563
|
+
|
|
564
|
+
$elapsedSeconds += $waitSeconds
|
|
565
|
+
$currentLogSize = 0
|
|
566
|
+
if (Test-Path $sessionLog) {
|
|
567
|
+
$currentLogSize = [int64](Get-Item $sessionLog).Length
|
|
568
|
+
}
|
|
569
|
+
$growth = $currentLogSize - $previousLogSize
|
|
570
|
+
$previousLogSize = $currentLogSize
|
|
571
|
+
if ($growth -gt 0) {
|
|
572
|
+
$staleSeconds = 0
|
|
573
|
+
} else {
|
|
574
|
+
$staleSeconds += $waitSeconds
|
|
342
575
|
}
|
|
576
|
+
|
|
577
|
+
if ($timeoutSeconds -gt 0 -and $elapsedSeconds -ge $timeoutSeconds) {
|
|
578
|
+
$wasTimedOut = $true
|
|
579
|
+
Stop-PrizmSessionProcess $pidPath
|
|
580
|
+
break
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if ($staleKillThreshold -gt 0 -and $staleSeconds -ge $staleKillThreshold) {
|
|
584
|
+
$wasStaleKilled = $true
|
|
585
|
+
Write-PrizmWarn "Session stale-killed (no progress for ${staleKillThreshold}s)"
|
|
586
|
+
Write-PrizmStaleKillMarker $staleKillMarker $staleSeconds $staleKillThreshold
|
|
587
|
+
Stop-PrizmSessionProcess $pidPath
|
|
588
|
+
if ($staleKillGraceSeconds -gt 0) { Start-Sleep -Seconds $staleKillGraceSeconds }
|
|
589
|
+
break
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
$exitCode = 0
|
|
594
|
+
if ($wasTimedOut) {
|
|
595
|
+
Stop-Job $job
|
|
596
|
+
Remove-Job $job
|
|
597
|
+
$exitCode = 124
|
|
598
|
+
} elseif ($wasStaleKilled) {
|
|
343
599
|
Stop-Job $job
|
|
344
600
|
Remove-Job $job
|
|
345
|
-
|
|
346
|
-
|
|
601
|
+
$exitCode = 143
|
|
602
|
+
} else {
|
|
603
|
+
try {
|
|
604
|
+
$exitCode = [int](Receive-Job $job)
|
|
605
|
+
} catch {
|
|
606
|
+
Write-PrizmWarn "AI session job failed: $($_.Exception.Message)"
|
|
607
|
+
$exitCode = 1
|
|
608
|
+
}
|
|
609
|
+
Remove-Job $job
|
|
347
610
|
}
|
|
611
|
+
Stop-PrizmProgressParser $parserProcess
|
|
348
612
|
|
|
349
|
-
$exitCode = [int](Receive-Job $job)
|
|
350
|
-
Remove-Job $job
|
|
351
613
|
$status = 'crashed'
|
|
352
|
-
if ($
|
|
614
|
+
if ($wasTimedOut) {
|
|
615
|
+
$status = 'timed_out'
|
|
616
|
+
Write-PrizmWarn "AI session timed out after $timeoutSeconds seconds"
|
|
617
|
+
} elseif ($wasStaleKilled -or (Test-Path $staleKillMarker)) {
|
|
618
|
+
Write-PrizmWarn "Session was stale-killed by heartbeat monitor (no progress for too long)"
|
|
619
|
+
Write-PrizmWarn "Stale-killed sessions are treated as failed; dev branch is preserved for inspection"
|
|
620
|
+
} elseif ($exitCode -ne 0) {
|
|
353
621
|
Write-PrizmWarn "AI session exited with code $exitCode"
|
|
354
622
|
} elseif (-not $isGitRepository) {
|
|
355
623
|
Write-PrizmWarn "AI session exited cleanly, but project is not a git repository; cannot verify work was committed."
|
|
@@ -368,27 +636,57 @@ function Invoke-PrizmPipeline {
|
|
|
368
636
|
Write-PrizmWarn "AI session exited cleanly but produced no commits and no changes."
|
|
369
637
|
}
|
|
370
638
|
|
|
371
|
-
|
|
639
|
+
$mergeSucceeded = $true
|
|
640
|
+
if ($status -eq 'success') {
|
|
641
|
+
Invoke-PrizmPythonText $python (@((Join-Path $paths.ScriptsDir $updateScript), $listOption, $listPath, '--state-dir', $stateDir, '--action', 'update', $idOption, $CurrentItemId, '--session-id', $sessionId, '--session-status', $status) + $maxRetryArgs)
|
|
642
|
+
|
|
643
|
+
if (Test-PrizmGitDirty $paths.ProjectRoot) {
|
|
644
|
+
if ($hadDirtyBaseline) {
|
|
645
|
+
Write-PrizmInfo "Auto-committing pipeline bookkeeping artifacts only."
|
|
646
|
+
Invoke-PrizmGitIncludeBookkeepingArtifacts $paths.ProjectRoot $stateDir $listPath
|
|
647
|
+
} else {
|
|
648
|
+
Write-PrizmInfo "Auto-committing remaining session artifacts."
|
|
649
|
+
Invoke-PrizmGitIncludeRemainingArtifacts $paths.ProjectRoot $CurrentItemId
|
|
650
|
+
}
|
|
651
|
+
}
|
|
372
652
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
653
|
+
if ($isGitRepository -and $devBranchName) {
|
|
654
|
+
if (Merge-PrizmDevBranch $paths.ProjectRoot $devBranchName $originalBranch $autoPush) {
|
|
655
|
+
$devBranchName = ''
|
|
656
|
+
} else {
|
|
657
|
+
$mergeSucceeded = $false
|
|
658
|
+
$status = 'merge_conflict'
|
|
659
|
+
Write-PrizmWarn "Auto-merge failed - dev branch preserved for inspection"
|
|
660
|
+
}
|
|
380
661
|
}
|
|
662
|
+
} elseif ($isGitRepository -and $devBranchName) {
|
|
663
|
+
Write-PrizmWarn "Session failed - dev branch preserved for inspection: $devBranchName"
|
|
381
664
|
}
|
|
382
665
|
|
|
383
|
-
if ($
|
|
666
|
+
if ($isGitRepository -and $originalBranch) {
|
|
667
|
+
Restore-PrizmOriginalBranch $paths.ProjectRoot $originalBranch $devBranchName | Out-Null
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if ($status -ne 'success') {
|
|
671
|
+
Invoke-PrizmPythonText $python (@((Join-Path $paths.ScriptsDir $updateScript), $listOption, $listPath, '--state-dir', $stateDir, '--action', 'update', $idOption, $CurrentItemId, '--session-id', $sessionId, '--session-status', $status) + $maxRetryArgs)
|
|
672
|
+
if ($isGitRepository) {
|
|
673
|
+
Invoke-PrizmGitCommitPath $paths.ProjectRoot $listPath "chore($CurrentItemId): update $idName status" | Out-Null
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if ($status -eq 'success' -and $mergeSucceeded) {
|
|
384
678
|
Write-PrizmSuccess "$Kind item completed: $CurrentItemId"
|
|
385
679
|
} else {
|
|
386
680
|
Write-PrizmError "$Kind item failed: $CurrentItemId. Log: $sessionLog"
|
|
387
681
|
}
|
|
388
|
-
$script:PRIZM_ITEM_EXIT_CODE = if ($status -eq 'success') { 0 } else { 1 }
|
|
682
|
+
$script:PRIZM_ITEM_EXIT_CODE = if ($status -eq 'success' -and $mergeSucceeded) { 0 } else { 1 }
|
|
389
683
|
return
|
|
390
684
|
}
|
|
391
685
|
|
|
686
|
+
if (-not $dryRun) {
|
|
687
|
+
Invoke-PrizmLogCleanup
|
|
688
|
+
}
|
|
689
|
+
|
|
392
690
|
if ($itemId) {
|
|
393
691
|
Invoke-PrizmPipelineItem $itemId
|
|
394
692
|
$global:PRIZM_EXIT_CODE = $script:PRIZM_ITEM_EXIT_CODE
|
|
@@ -407,6 +705,10 @@ function Invoke-PrizmPipeline {
|
|
|
407
705
|
} else {
|
|
408
706
|
Write-PrizmSuccess "Processed $processedCount $Kind item(s)."
|
|
409
707
|
}
|
|
708
|
+
if (-not $dryRun) {
|
|
709
|
+
$deployExit = Invoke-PrizmDeploySession $Kind $listPath
|
|
710
|
+
if ($deployExit -ne 0 -and $lastExitCode -eq 0) { $lastExitCode = $deployExit }
|
|
711
|
+
}
|
|
410
712
|
$global:PRIZM_EXIT_CODE = $lastExitCode
|
|
411
713
|
return
|
|
412
714
|
}
|
|
@@ -438,7 +740,7 @@ function Invoke-PrizmPipeline {
|
|
|
438
740
|
$global:PRIZM_EXIT_CODE = $lastExitCode
|
|
439
741
|
return
|
|
440
742
|
}
|
|
441
|
-
if ($lastExitCode -ne 0 -and $
|
|
743
|
+
if ($lastExitCode -ne 0 -and $stopOnFailure) {
|
|
442
744
|
$global:PRIZM_EXIT_CODE = $lastExitCode
|
|
443
745
|
return
|
|
444
746
|
}
|
|
@@ -41,6 +41,22 @@ for ($i = 0; $i -lt $remaining.Count; $i++) {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
function Get-PrizmRecoveryIntEnv {
|
|
45
|
+
param([string]$Name, [int]$DefaultValue, [int]$MinimumValue = 0)
|
|
46
|
+
$value = [Environment]::GetEnvironmentVariable($Name, 'Process')
|
|
47
|
+
if (-not $value) { return $DefaultValue }
|
|
48
|
+
$parsed = 0
|
|
49
|
+
if (-not [int]::TryParse($value, [ref]$parsed) -or $parsed -lt $MinimumValue) {
|
|
50
|
+
throw "$Name must be an integer >= $MinimumValue`: $value"
|
|
51
|
+
}
|
|
52
|
+
return $parsed
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
$timeoutSeconds = Get-PrizmRecoveryIntEnv 'SESSION_TIMEOUT' 0 0
|
|
56
|
+
$heartbeatInterval = Get-PrizmRecoveryIntEnv 'HEARTBEAT_INTERVAL' 30 1
|
|
57
|
+
$staleKillThreshold = Get-PrizmRecoveryIntEnv 'STALE_KILL_THRESHOLD' 900 0
|
|
58
|
+
$staleKillGraceSeconds = Get-PrizmRecoveryIntEnv 'STALE_KILL_GRACE_SECONDS' 10 0
|
|
59
|
+
|
|
44
60
|
$recoveryDetectScript = $null
|
|
45
61
|
foreach ($candidate in @(
|
|
46
62
|
(Join-Path $paths.ProjectRoot '.agents\skills\recovery-workflow\scripts\detect-recovery-state.py'),
|
|
@@ -60,17 +76,97 @@ if (-not $recoveryDetectScript) {
|
|
|
60
76
|
$stateDir = Join-Path $paths.PrizmkitDir 'state\recovery'
|
|
61
77
|
New-Item -ItemType Directory -Force -Path $stateDir | Out-Null
|
|
62
78
|
$detectionPath = Join-Path $stateDir 'detection.json'
|
|
63
|
-
$promptPath = Join-Path $paths.PrizmkitDir 'state\recovery-bootstrap-prompt.md'
|
|
64
|
-
$logPath = Join-Path $paths.PrizmkitDir 'state\recovery-session.log'
|
|
65
79
|
|
|
66
80
|
Invoke-PrizmPythonText $python @($recoveryDetectScript, '--project-root', $paths.ProjectRoot) | Set-Content -Path $detectionPath -Encoding UTF8
|
|
67
81
|
if ($command -eq 'detect') { Get-Content $detectionPath; exit 0 }
|
|
68
82
|
|
|
69
|
-
|
|
83
|
+
$sessionId = New-PrizmSessionId 'recovery'
|
|
84
|
+
$sessionDir = Join-Path $stateDir "sessions\$sessionId"
|
|
85
|
+
$logsDir = Join-Path $sessionDir 'logs'
|
|
86
|
+
New-Item -ItemType Directory -Force -Path $logsDir | Out-Null
|
|
87
|
+
$sessionDetectionPath = Join-Path $sessionDir 'detection.json'
|
|
88
|
+
Copy-Item -Path $detectionPath -Destination $sessionDetectionPath -Force
|
|
89
|
+
$promptPath = Join-Path $sessionDir 'bootstrap-prompt.md'
|
|
90
|
+
$logPath = Join-Path $logsDir 'session.log'
|
|
91
|
+
$progressPath = Join-Path $logsDir 'progress.json'
|
|
92
|
+
$pidPath = Join-Path $logsDir 'ai.pid'
|
|
93
|
+
$staleKillMarker = Join-Path $logsDir 'stale-kill.json'
|
|
94
|
+
|
|
95
|
+
Invoke-PrizmPythonText $python @((Join-Path $paths.ScriptsDir 'generate-recovery-prompt.py'), '--detection-json', $sessionDetectionPath, '--project-root', $paths.ProjectRoot, '--session-id', $sessionId, '--output', $promptPath)
|
|
70
96
|
if ($dryRun) { Get-Content $promptPath; exit 0 }
|
|
71
97
|
$cli = Resolve-PrizmAiCli $paths.ProjectRoot $paths.PrizmkitDir
|
|
72
98
|
$env:PRIZMKIT_PLATFORM = Get-PrizmPlatformFromProject $paths.ProjectRoot $paths.PrizmkitDir $cli
|
|
73
|
-
|
|
74
|
-
|
|
99
|
+
Write-PrizmInfo "Starting recovery session: $sessionId"
|
|
100
|
+
Write-PrizmInfo "Prompt: $promptPath"
|
|
101
|
+
Write-PrizmInfo "Log: $logPath"
|
|
102
|
+
|
|
103
|
+
$parserProcess = Start-PrizmProgressParser -PythonCommand $python -ScriptsDir $paths.ScriptsDir -SessionLog $logPath -ProgressFile $progressPath -CliCommand $cli
|
|
104
|
+
$job = Start-Job -ScriptBlock {
|
|
105
|
+
param($commonPath, $cli, $promptPath, $logPath, $projectRoot, $model, $pidPath)
|
|
106
|
+
. $commonPath
|
|
107
|
+
Invoke-PrizmAiSession -CliCommand $cli -PromptPath $promptPath -LogPath $logPath -ProjectRoot $projectRoot -Model $model -PidPath $pidPath
|
|
108
|
+
} -ArgumentList (Join-Path $paths.PipelineDir 'lib\common.ps1'), $cli, $promptPath, $logPath, $paths.ProjectRoot, $model, $pidPath
|
|
109
|
+
|
|
110
|
+
$elapsedSeconds = 0
|
|
111
|
+
$staleSeconds = 0
|
|
112
|
+
$previousLogSize = 0
|
|
113
|
+
$wasTimedOut = $false
|
|
114
|
+
$wasStaleKilled = $false
|
|
115
|
+
while ($true) {
|
|
116
|
+
$remainingTimeout = if ($timeoutSeconds -gt 0) { $timeoutSeconds - $elapsedSeconds } else { $heartbeatInterval }
|
|
117
|
+
$waitSeconds = if ($timeoutSeconds -gt 0) { [Math]::Min($heartbeatInterval, [Math]::Max(1, $remainingTimeout)) } else { $heartbeatInterval }
|
|
118
|
+
$completed = Wait-Job $job -Timeout $waitSeconds
|
|
119
|
+
if ($completed) { break }
|
|
120
|
+
|
|
121
|
+
$elapsedSeconds += $waitSeconds
|
|
122
|
+
$currentLogSize = 0
|
|
123
|
+
if (Test-Path $logPath) { $currentLogSize = [int64](Get-Item $logPath).Length }
|
|
124
|
+
$growth = $currentLogSize - $previousLogSize
|
|
125
|
+
$previousLogSize = $currentLogSize
|
|
126
|
+
if ($growth -gt 0) { $staleSeconds = 0 } else { $staleSeconds += $waitSeconds }
|
|
127
|
+
|
|
128
|
+
if ($timeoutSeconds -gt 0 -and $elapsedSeconds -ge $timeoutSeconds) {
|
|
129
|
+
$wasTimedOut = $true
|
|
130
|
+
Stop-PrizmSessionProcess $pidPath
|
|
131
|
+
break
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if ($staleKillThreshold -gt 0 -and $staleSeconds -ge $staleKillThreshold) {
|
|
135
|
+
$wasStaleKilled = $true
|
|
136
|
+
Write-PrizmWarn "Recovery session stale-killed (no progress for ${staleKillThreshold}s)"
|
|
137
|
+
Write-PrizmStaleKillMarker $staleKillMarker $staleSeconds $staleKillThreshold
|
|
138
|
+
Stop-PrizmSessionProcess $pidPath
|
|
139
|
+
if ($staleKillGraceSeconds -gt 0) { Start-Sleep -Seconds $staleKillGraceSeconds }
|
|
140
|
+
break
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
$exitCode = 0
|
|
145
|
+
if ($wasTimedOut) {
|
|
146
|
+
Stop-Job $job
|
|
147
|
+
Remove-Job $job
|
|
148
|
+
$exitCode = 124
|
|
149
|
+
} elseif ($wasStaleKilled) {
|
|
150
|
+
Stop-Job $job
|
|
151
|
+
Remove-Job $job
|
|
152
|
+
$exitCode = 143
|
|
153
|
+
} else {
|
|
154
|
+
try {
|
|
155
|
+
$exitCode = [int](Receive-Job $job)
|
|
156
|
+
} catch {
|
|
157
|
+
Write-PrizmWarn "Recovery session job failed: $($_.Exception.Message)"
|
|
158
|
+
$exitCode = 1
|
|
159
|
+
}
|
|
160
|
+
Remove-Job $job
|
|
161
|
+
}
|
|
162
|
+
Stop-PrizmProgressParser $parserProcess
|
|
163
|
+
|
|
164
|
+
if (Test-Path $logPath) {
|
|
165
|
+
$lineCount = (Get-Content $logPath | Measure-Object -Line).Lines
|
|
166
|
+
$sizeKb = [int](([int64](Get-Item $logPath).Length) / 1024)
|
|
167
|
+
Write-PrizmInfo "Session log: $lineCount lines, ${sizeKb}KB"
|
|
168
|
+
}
|
|
169
|
+
if ($wasTimedOut) { Write-PrizmWarn "Recovery session timed out after $timeoutSeconds seconds." }
|
|
170
|
+
if ($wasStaleKilled -or (Test-Path $staleKillMarker)) { Write-PrizmWarn "Recovery session was stale-killed." }
|
|
75
171
|
if ($exitCode -eq 0) { Write-PrizmSuccess "Recovery session completed." } else { Write-PrizmError "Recovery session failed. Log: $logPath" }
|
|
76
172
|
exit $exitCode
|
|
@@ -73,6 +73,9 @@ class ProgressTracker:
|
|
|
73
73
|
self.last_text_snippet = ""
|
|
74
74
|
self.is_active = True
|
|
75
75
|
self.errors = []
|
|
76
|
+
self.event_format = ""
|
|
77
|
+
self.active_subagent_count = 0
|
|
78
|
+
self.subagent_status_counts = Counter()
|
|
76
79
|
self._text_buffer = ""
|
|
77
80
|
self._in_tool_use = False
|
|
78
81
|
self._current_tool_input_parts = []
|
|
@@ -87,8 +90,72 @@ class ProgressTracker:
|
|
|
87
90
|
"""
|
|
88
91
|
event_type = event.get("type", "")
|
|
89
92
|
|
|
93
|
+
# ── Codex exec --json JSONL format ──────────────────────────
|
|
94
|
+
if event_type in (
|
|
95
|
+
"thread.started", "turn.started", "turn.completed",
|
|
96
|
+
"turn.failed", "item.started", "item.completed", "error",
|
|
97
|
+
):
|
|
98
|
+
self.event_format = "codex-json"
|
|
99
|
+
self.is_active = True
|
|
100
|
+
|
|
101
|
+
if event_type == "turn.started":
|
|
102
|
+
self.message_count += 1
|
|
103
|
+
|
|
104
|
+
elif event_type in ("item.started", "item.completed"):
|
|
105
|
+
item = event.get("item", {})
|
|
106
|
+
item_type = item.get("type", "")
|
|
107
|
+
|
|
108
|
+
if item_type == "agent_message":
|
|
109
|
+
text = item.get("text", "")
|
|
110
|
+
if text.strip():
|
|
111
|
+
self.last_text_snippet = text.strip()[:120]
|
|
112
|
+
self._detect_phase(text)
|
|
113
|
+
|
|
114
|
+
elif item_type == "collab_tool_call":
|
|
115
|
+
tool_name = item.get("tool", "collab")
|
|
116
|
+
if event_type == "item.started":
|
|
117
|
+
self.current_tool = tool_name
|
|
118
|
+
self.tool_call_counts[tool_name] += 1
|
|
119
|
+
self.total_tool_calls += 1
|
|
120
|
+
elif item.get("status") == "completed":
|
|
121
|
+
self.current_tool = None
|
|
122
|
+
self._extract_tool_summary_from_dict(item)
|
|
123
|
+
self._update_subagent_status_counts(
|
|
124
|
+
item.get("agents_states", {})
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
prompt = item.get("prompt")
|
|
128
|
+
if prompt:
|
|
129
|
+
self._detect_phase(prompt)
|
|
130
|
+
|
|
131
|
+
else:
|
|
132
|
+
tool_name = item.get("tool") or item.get("name")
|
|
133
|
+
if tool_name:
|
|
134
|
+
if event_type == "item.started":
|
|
135
|
+
self.current_tool = tool_name
|
|
136
|
+
self.tool_call_counts[tool_name] += 1
|
|
137
|
+
self.total_tool_calls += 1
|
|
138
|
+
elif item.get("status") == "completed":
|
|
139
|
+
self.current_tool = None
|
|
140
|
+
self._extract_tool_summary_from_dict(item)
|
|
141
|
+
|
|
142
|
+
elif event_type == "turn.completed":
|
|
143
|
+
self.current_tool = None
|
|
144
|
+
|
|
145
|
+
elif event_type == "turn.failed":
|
|
146
|
+
error = event.get("error") or event.get("message") or "Codex turn failed"
|
|
147
|
+
self.errors.append(str(error))
|
|
148
|
+
self.current_tool = None
|
|
149
|
+
|
|
150
|
+
elif event_type == "error":
|
|
151
|
+
error = event.get("error") or event.get("message") or "Unknown error"
|
|
152
|
+
self.errors.append(str(error))
|
|
153
|
+
|
|
154
|
+
return
|
|
155
|
+
|
|
90
156
|
# ── Claude Code verbose format ──────────────────────────────
|
|
91
157
|
if event_type == "assistant":
|
|
158
|
+
self.event_format = self.event_format or "stream-json"
|
|
92
159
|
self.message_count += 1
|
|
93
160
|
self.is_active = True
|
|
94
161
|
message = event.get("message", {})
|
|
@@ -113,16 +180,19 @@ class ProgressTracker:
|
|
|
113
180
|
|
|
114
181
|
elif event_type == "tool_result" or event_type == "user":
|
|
115
182
|
# tool_result contains output from tool execution
|
|
183
|
+
self.event_format = self.event_format or "stream-json"
|
|
116
184
|
self.is_active = True
|
|
117
185
|
|
|
118
186
|
elif event_type == "system":
|
|
119
187
|
# System events (hooks, init, etc.) — track but don't count as messages
|
|
188
|
+
self.event_format = self.event_format or "stream-json"
|
|
120
189
|
subtype = event.get("subtype", "")
|
|
121
190
|
if subtype == "init":
|
|
122
191
|
self.is_active = True
|
|
123
192
|
|
|
124
193
|
# ── Claude API raw stream format ────────────────────────────
|
|
125
194
|
elif event_type == "message_start":
|
|
195
|
+
self.event_format = self.event_format or "stream-json"
|
|
126
196
|
self.message_count += 1
|
|
127
197
|
self.is_active = True
|
|
128
198
|
|
|
@@ -256,14 +326,38 @@ class ProgressTracker:
|
|
|
256
326
|
elif "prompt" in data:
|
|
257
327
|
self.current_tool_input_summary = str(data["prompt"])[:100]
|
|
258
328
|
|
|
329
|
+
def _update_subagent_status_counts(self, agents_states):
|
|
330
|
+
"""Track Codex subagent state counts from collab_tool_call items."""
|
|
331
|
+
counts = Counter()
|
|
332
|
+
active = 0
|
|
333
|
+
if isinstance(agents_states, dict):
|
|
334
|
+
for state in agents_states.values():
|
|
335
|
+
if not isinstance(state, dict):
|
|
336
|
+
continue
|
|
337
|
+
status = str(state.get("status", "unknown"))
|
|
338
|
+
counts[status] += 1
|
|
339
|
+
if status not in ("completed", "failed", "cancelled", "canceled"):
|
|
340
|
+
active += 1
|
|
341
|
+
message = state.get("message")
|
|
342
|
+
if message:
|
|
343
|
+
self.last_text_snippet = str(message).strip()[:120]
|
|
344
|
+
self._detect_phase(str(message))
|
|
345
|
+
self.subagent_status_counts = counts
|
|
346
|
+
self.active_subagent_count = active
|
|
347
|
+
|
|
259
348
|
def to_dict(self):
|
|
260
349
|
"""Export current state as a dictionary for JSON serialization."""
|
|
261
350
|
tool_calls = [
|
|
262
351
|
{"name": name, "count": count}
|
|
263
352
|
for name, count in self.tool_call_counts.most_common()
|
|
264
353
|
]
|
|
354
|
+
subagent_states = [
|
|
355
|
+
{"status": status, "count": count}
|
|
356
|
+
for status, count in self.subagent_status_counts.most_common()
|
|
357
|
+
]
|
|
265
358
|
return {
|
|
266
359
|
"updated_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
360
|
+
"event_format": self.event_format,
|
|
267
361
|
"message_count": self.message_count,
|
|
268
362
|
"current_tool": self.current_tool,
|
|
269
363
|
"current_tool_input_summary": self.current_tool_input_summary,
|
|
@@ -271,6 +365,8 @@ class ProgressTracker:
|
|
|
271
365
|
"detected_phases": self.detected_phases,
|
|
272
366
|
"tool_calls": tool_calls,
|
|
273
367
|
"total_tool_calls": self.total_tool_calls,
|
|
368
|
+
"active_subagent_count": self.active_subagent_count,
|
|
369
|
+
"subagent_states": subagent_states,
|
|
274
370
|
"last_text_snippet": self.last_text_snippet,
|
|
275
371
|
"is_active": self.is_active,
|
|
276
372
|
"errors": self.errors[-10:], # Keep last 10 errors
|
|
@@ -29,6 +29,11 @@ This project uses PrizmKit with the Prizm documentation system for AI-optimized
|
|
|
29
29
|
2. Generate a new L2 `.prizm` file following Prizm specification
|
|
30
30
|
3. Add a pointer in the parent L1 doc's SUBDIRS section
|
|
31
31
|
|
|
32
|
+
### Framework Source Parity
|
|
33
|
+
- When working in the PrizmKit framework source, `dev-pipeline/` and `dev-pipeline-windows/` are paired runtimes. Any runtime, prompt, schema, reset/recovery, timeout, heartbeat, logging, or AI CLI behavior change in one must be mirrored in the other, or explicitly documented as platform-specific.
|
|
34
|
+
- When working in the PrizmKit framework source, `core/skills/orchestration-skill/` and `core/skills/orchestration-skill-windows/` are paired skill sources. Any planner, workflow, launcher, prompt, or documentation behavior change in one must be mirrored in the other, or explicitly documented as platform-specific.
|
|
35
|
+
- Before finishing framework work, run `git diff --name-only` and verify these paired Unix/Windows paths were updated together.
|
|
36
|
+
|
|
32
37
|
### Available Commands
|
|
33
38
|
Run `/prizm-kit` to see all available PrizmKit commands.
|
|
34
39
|
|