prizmkit 1.1.62 → 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/adapters/codex/settings-adapter.js +1 -0
- package/bundled/dev-pipeline/lib/common.sh +38 -4
- package/bundled/dev-pipeline/lib/heartbeat.sh +39 -7
- package/bundled/dev-pipeline/run-bugfix.sh +2 -25
- package/bundled/dev-pipeline/run-feature.sh +2 -26
- package/bundled/dev-pipeline/run-refactor.sh +2 -25
- package/bundled/dev-pipeline/scripts/parse-stream-progress.py +96 -0
- 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/src/scaffold.js +117 -1
- package/src/upgrade.js +14 -2
|
@@ -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
|