prizmkit 1.1.98 → 1.1.100

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.
Files changed (79) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/README.md +5 -5
  3. package/bundled/dev-pipeline/launch-bugfix-daemon.sh +1 -1
  4. package/bundled/dev-pipeline/launch-feature-daemon.sh +1 -1
  5. package/bundled/dev-pipeline/launch-refactor-daemon.sh +1 -1
  6. package/bundled/dev-pipeline/lib/common.sh +244 -25
  7. package/bundled/dev-pipeline/reset-bug.sh +32 -2
  8. package/bundled/dev-pipeline/reset-feature.sh +32 -2
  9. package/bundled/dev-pipeline/reset-refactor.sh +32 -2
  10. package/bundled/dev-pipeline/run-bugfix.sh +133 -18
  11. package/bundled/dev-pipeline/run-feature.sh +134 -18
  12. package/bundled/dev-pipeline/run-refactor.sh +133 -18
  13. package/bundled/dev-pipeline/scripts/check-session-status.py +74 -1
  14. package/bundled/dev-pipeline/scripts/continuation.py +374 -0
  15. package/bundled/dev-pipeline/scripts/detect-stuck.py +5 -5
  16. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +29 -35
  17. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +19 -25
  18. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +19 -25
  19. package/bundled/dev-pipeline/scripts/init-bugfix-pipeline.py +1 -1
  20. package/bundled/dev-pipeline/scripts/init-pipeline.py +1 -1
  21. package/bundled/dev-pipeline/scripts/init-refactor-pipeline.py +1 -1
  22. package/bundled/dev-pipeline/scripts/parse-stream-progress.py +146 -16
  23. package/bundled/dev-pipeline/scripts/update-bug-status.py +220 -13
  24. package/bundled/dev-pipeline/scripts/update-feature-status.py +244 -18
  25. package/bundled/dev-pipeline/scripts/update-refactor-status.py +220 -13
  26. package/bundled/dev-pipeline/templates/agent-prompts/dev-implement.md +1 -1
  27. package/bundled/dev-pipeline/templates/bootstrap-tier1.md +2 -2
  28. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +3 -3
  29. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +3 -3
  30. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +1 -1
  31. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +3 -3
  32. package/bundled/dev-pipeline/templates/sections/log-size-awareness.md +31 -66
  33. package/bundled/dev-pipeline/templates/session-status-schema.json +1 -1
  34. package/bundled/dev-pipeline/tests/conftest.py +1 -0
  35. package/bundled/dev-pipeline/tests/test_auto_skip.py +518 -8
  36. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +103 -0
  37. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +60 -0
  38. package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +43 -0
  39. package/bundled/dev-pipeline-windows/lib/common.ps1 +172 -10
  40. package/bundled/dev-pipeline-windows/lib/pipeline.ps1 +95 -11
  41. package/bundled/dev-pipeline-windows/lib/reset.ps1 +34 -0
  42. package/bundled/dev-pipeline-windows/reset-bug.ps1 +1 -0
  43. package/bundled/dev-pipeline-windows/reset-feature.ps1 +1 -0
  44. package/bundled/dev-pipeline-windows/reset-refactor.ps1 +1 -0
  45. package/bundled/dev-pipeline-windows/scripts/check-session-status.py +74 -1
  46. package/bundled/dev-pipeline-windows/scripts/continuation.py +374 -0
  47. package/bundled/dev-pipeline-windows/scripts/detect-stuck.py +5 -5
  48. package/bundled/dev-pipeline-windows/scripts/generate-bootstrap-prompt.py +29 -35
  49. package/bundled/dev-pipeline-windows/scripts/generate-bugfix-prompt.py +20 -25
  50. package/bundled/dev-pipeline-windows/scripts/generate-refactor-prompt.py +20 -25
  51. package/bundled/dev-pipeline-windows/scripts/init-bugfix-pipeline.py +1 -1
  52. package/bundled/dev-pipeline-windows/scripts/init-pipeline.py +1 -1
  53. package/bundled/dev-pipeline-windows/scripts/init-refactor-pipeline.py +1 -1
  54. package/bundled/dev-pipeline-windows/scripts/parse-stream-progress.py +146 -16
  55. package/bundled/dev-pipeline-windows/scripts/update-bug-status.py +220 -13
  56. package/bundled/dev-pipeline-windows/scripts/update-feature-status.py +244 -18
  57. package/bundled/dev-pipeline-windows/scripts/update-refactor-status.py +220 -13
  58. package/bundled/dev-pipeline-windows/templates/agent-prompts/dev-implement.md +1 -1
  59. package/bundled/dev-pipeline-windows/templates/bootstrap-tier1.md +2 -2
  60. package/bundled/dev-pipeline-windows/templates/bootstrap-tier2.md +3 -3
  61. package/bundled/dev-pipeline-windows/templates/bootstrap-tier3.md +3 -3
  62. package/bundled/dev-pipeline-windows/templates/bugfix-bootstrap-prompt.md +1 -1
  63. package/bundled/dev-pipeline-windows/templates/refactor-bootstrap-prompt.md +3 -3
  64. package/bundled/dev-pipeline-windows/templates/sections/log-size-awareness.md +31 -68
  65. package/bundled/dev-pipeline-windows/templates/session-status-schema.json +1 -1
  66. package/bundled/skills/_metadata.json +1 -1
  67. package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +2 -2
  68. package/bundled/skills/feature-pipeline-launcher/SKILL.md +2 -2
  69. package/bundled/skills/recovery-workflow/SKILL.md +2 -2
  70. package/bundled/skills/recovery-workflow/evals/evals.json +2 -2
  71. package/bundled/skills/refactor-pipeline-launcher/SKILL.md +2 -2
  72. package/bundled/skills-windows/bugfix-pipeline-launcher/SKILL.md +2 -2
  73. package/bundled/skills-windows/feature-pipeline-launcher/SKILL.md +2 -2
  74. package/bundled/skills-windows/recovery-workflow/SKILL.md +2 -2
  75. package/bundled/skills-windows/recovery-workflow/evals/evals.json +2 -2
  76. package/bundled/skills-windows/refactor-pipeline-launcher/SKILL.md +2 -2
  77. package/package.json +1 -1
  78. package/bundled/dev-pipeline/scripts/monitor-log.sh +0 -104
  79. package/bundled/dev-pipeline-windows/scripts/monitor-log.ps1 +0 -102
@@ -3,6 +3,7 @@
3
3
  import os
4
4
  import re
5
5
  import time
6
+ from argparse import Namespace
6
7
  from pathlib import Path
7
8
 
8
9
  from generate_bootstrap_prompt import (
@@ -18,7 +19,9 @@ from generate_bootstrap_prompt import (
18
19
  assemble_sections,
19
20
  generate_checkpoint_definition,
20
21
  merge_checkpoint_state,
22
+ load_log_size_section,
21
23
  )
24
+ from continuation import append_continuation_handoff, checkpoint_cursor
22
25
 
23
26
 
24
27
  def scoped_report_text(verdict="PASS", boundary_missing=0,
@@ -656,3 +659,103 @@ class TestScopedFeatureTestGate:
656
659
  merged = merge_checkpoint_state(existing, fresh, str(tmp_path))
657
660
 
658
661
  assert merged["steps"][0]["status"] == "pending"
662
+
663
+
664
+ # ---------------------------------------------------------------------------
665
+ # Active compaction harness rollback
666
+ # ---------------------------------------------------------------------------
667
+
668
+ class TestHeadlessRecoveryReframing:
669
+ def test_log_size_awareness_is_passive_checkpoint_guidance(self):
670
+ content = load_log_size_section(str(Path("dev-pipeline/scripts").resolve()))
671
+
672
+ assert "Session Log Budget Awareness" in content
673
+ assert "passive checkpoint guidance" in content
674
+ assert "durable checkpoints and artifacts" in content
675
+ assert "fresh continuation session" in content
676
+ assert "optional interactive convenience only" in content
677
+ assert "COMPACT_NEEDED" not in content
678
+ assert "monitor-log" not in content
679
+ assert "background monitoring subagent" not in content
680
+ assert "Execute `/compact`" not in content
681
+ assert "run `/compact`" not in content.lower()
682
+
683
+ def test_assembled_prompts_do_not_spawn_log_monitor_or_require_compact(self):
684
+ sections_dir = Path("dev-pipeline/templates/sections").resolve()
685
+
686
+ for mode in ("lite", "standard", "full"):
687
+ rendered = "\n".join(
688
+ section for _name, section in assemble_sections(
689
+ mode, str(sections_dir), init_done=True, is_resume=False,
690
+ critic_enabled=False, browser_enabled=False,
691
+ )
692
+ )
693
+ assert "Headless recovery must rely on durable checkpoints" in rendered
694
+ assert "fresh continuation session" in rendered
695
+ assert "monitor-log" not in rendered
696
+ assert "COMPACT_NEEDED" not in rendered
697
+ assert "log-size monitor" not in rendered
698
+ assert "The log-size monitor is the only allowed" not in rendered
699
+ assert "Run this command and keep it running" not in rendered
700
+ assert "Execute `/compact`" not in rendered
701
+ assert "run `/compact` after" not in rendered
702
+
703
+
704
+ # ---------------------------------------------------------------------------
705
+ # continuation handoff
706
+ # ---------------------------------------------------------------------------
707
+
708
+ class TestContinuationHandoff:
709
+ def test_addendum_and_summary_include_required_context(self, tmp_path):
710
+ args = Namespace(
711
+ output=str(tmp_path / ".prizmkit/state/features/F-123/sessions/F-123-new/bootstrap-prompt.md"),
712
+ state_dir=str(tmp_path / ".prizmkit/state/features"),
713
+ continuation_mode="context_overflow",
714
+ previous_session_id="F-123-old",
715
+ continuation_count="2",
716
+ context_overflow_count="2",
717
+ active_dev_branch="dev/F-123",
718
+ base_branch="main",
719
+ continuation_summary_path="",
720
+ )
721
+ checkpoint = {"steps": [
722
+ {"id": "S01", "skill": "test-baseline", "status": "completed"},
723
+ {"id": "S02", "skill": "context-snapshot", "status": "skipped"},
724
+ {"id": "S03", "skill": "prizmkit-implement", "status": "pending"},
725
+ ]}
726
+
727
+ rendered = append_continuation_handoff(
728
+ "NORMAL BOOTSTRAP", args, str(tmp_path), "feature", "F-123",
729
+ "123-continuation", checkpoint,
730
+ )
731
+
732
+ assert rendered.startswith("NORMAL BOOTSTRAP")
733
+ assert "## Continuation Addendum" in rendered
734
+ assert "This is an automatic continuation after context overflow." in rendered
735
+ assert "This is a fresh headless AI CLI session for the same task." in rendered
736
+ assert "Continue on the current branch/worktree." in rendered
737
+ assert "Do not reset, clean, switch branches, or discard work." in rendered
738
+ assert "Start with `git status` and `git diff`." in rendered
739
+ assert "Treat completed/skipped checkpoint steps as already done." in rendered
740
+ assert "Do not read the previous full `session.log` unless explicitly necessary." in rendered
741
+ assert "prefer synthesized artifacts and summaries over logs" in rendered
742
+ assert "Do not attempt `/compact` as the recovery mechanism." in rendered
743
+ assert ".prizmkit/specs/123-continuation/workflow-checkpoint.json" in rendered
744
+ assert ".prizmkit/state/features/F-123/sessions/F-123-new/logs/session.log" in rendered
745
+ assert ".prizmkit/state/features/F-123/sessions/F-123-new/logs/progress.json" in rendered
746
+ assert ".prizmkit/state/features/F-123/sessions/F-123-new/logs/ai.pid" in rendered
747
+
748
+ summary = tmp_path / ".prizmkit/specs/123-continuation/continuation-summary.md"
749
+ assert summary.exists()
750
+ summary_text = summary.read_text(encoding="utf-8")
751
+ assert "Previous session id: F-123-old" in summary_text
752
+ assert "First pending/in-progress checkpoint step: S03 (prizmkit-implement)" in summary_text
753
+ assert "Workflow checkpoint: .prizmkit/specs/123-continuation/workflow-checkpoint.json" in summary_text
754
+
755
+ def test_checkpoint_cursor_prefers_in_progress_before_pending(self):
756
+ checkpoint = {"steps": [
757
+ {"id": "S01", "skill": "done", "status": "completed"},
758
+ {"id": "S02", "skill": "active", "status": "in_progress"},
759
+ {"id": "S03", "skill": "later", "status": "pending"},
760
+ ]}
761
+ assert checkpoint_cursor(checkpoint)["first_cursor"] == "S02 (active)"
@@ -1,5 +1,8 @@
1
1
  """Tests for generate-bugfix-prompt.py core functions."""
2
2
 
3
+ from argparse import Namespace
4
+ from pathlib import Path
5
+
3
6
  from generate_bugfix_prompt import (
4
7
  find_bug,
5
8
  format_acceptance_criteria,
@@ -8,7 +11,9 @@ from generate_bugfix_prompt import (
8
11
  format_environment,
9
12
  process_conditional_blocks,
10
13
  render_template,
14
+ load_log_size_section,
11
15
  )
16
+ from continuation import append_continuation_handoff
12
17
 
13
18
 
14
19
  # ---------------------------------------------------------------------------
@@ -166,3 +171,58 @@ class TestSharedHelpers:
166
171
  def test_format_global_context_dict(self):
167
172
  result = format_global_context({"lang": "Python"})
168
173
  assert "**lang**: Python" in result
174
+
175
+
176
+ # ---------------------------------------------------------------------------
177
+ # Active compaction harness rollback
178
+ # ---------------------------------------------------------------------------
179
+
180
+ class TestHeadlessRecoveryReframing:
181
+ def test_bugfix_log_size_section_is_passive_checkpoint_guidance(self):
182
+ content = load_log_size_section(str(Path("dev-pipeline/scripts").resolve()))
183
+
184
+ assert "passive checkpoint guidance" in content
185
+ assert "Headless recovery must rely on durable checkpoints" in content
186
+ assert "fresh continuation session" in content
187
+ assert "optional interactive convenience only" in content
188
+ assert "COMPACT_NEEDED" not in content
189
+ assert "monitor-log" not in content
190
+ assert "background monitoring subagent" not in content
191
+ assert "Execute `/compact`" not in content
192
+ assert "run `/compact`" not in content.lower()
193
+
194
+ def test_bugfix_template_does_not_spawn_monitor_or_require_compact(self):
195
+ content = Path("dev-pipeline/templates/bugfix-bootstrap-prompt.md").read_text(encoding="utf-8")
196
+
197
+ assert "Do not spawn persistent background subagents for headless recovery" in content
198
+ assert "log-size monitor" not in content
199
+ assert "monitor-log" not in content
200
+ assert "COMPACT_NEEDED" not in content
201
+ assert "Run this command and keep it running" not in content
202
+ assert "Execute `/compact`" not in content
203
+ assert "run `/compact` after" not in content
204
+
205
+
206
+ class TestBugfixContinuationHandoff:
207
+ def test_writes_specs_and_task_local_summaries(self, tmp_path):
208
+ args = Namespace(
209
+ output=str(tmp_path / ".prizmkit/state/bugfix/B-007/sessions/B-007-new/bootstrap-prompt.md"),
210
+ state_dir=str(tmp_path / ".prizmkit/state/bugfix"),
211
+ continuation_mode="context_overflow",
212
+ previous_session_id="B-007-old",
213
+ continuation_count="1",
214
+ context_overflow_count="1",
215
+ active_dev_branch="bugfix/B-007",
216
+ base_branch="main",
217
+ continuation_summary_path=".prizmkit/specs/B-007/continuation-summary.md",
218
+ )
219
+ prompt = append_continuation_handoff(
220
+ "BUGFIX PROMPT", args, str(tmp_path), "bugfix", "B-007", "B-007",
221
+ {"steps": [{"id": "S01", "skill": "diagnose", "status": "pending"}]},
222
+ )
223
+
224
+ assert prompt.startswith("BUGFIX PROMPT")
225
+ assert "This is an automatic continuation after context overflow." in prompt
226
+ assert "Do not attempt `/compact` as the recovery mechanism." in prompt
227
+ assert (tmp_path / ".prizmkit/specs/B-007/continuation-summary.md").exists()
228
+ assert (tmp_path / ".prizmkit/bugfix/B-007/continuation-summary.md").exists()
@@ -0,0 +1,43 @@
1
+ """Tests for generate-refactor-prompt.py continuation behavior."""
2
+
3
+ from argparse import Namespace
4
+
5
+ from continuation import append_continuation_handoff
6
+ from generate_refactor_prompt import generate_refactor_checkpoint, merge_refactor_checkpoint_state
7
+
8
+
9
+ class TestRefactorContinuationHandoff:
10
+ def test_writes_specs_and_task_local_summaries(self, tmp_path):
11
+ args = Namespace(
12
+ output=str(tmp_path / ".prizmkit/state/refactor/R-007/sessions/R-007-new/bootstrap-prompt.md"),
13
+ state_dir=str(tmp_path / ".prizmkit/state/refactor"),
14
+ continuation_mode="context_overflow",
15
+ previous_session_id="R-007-old",
16
+ continuation_count="1",
17
+ context_overflow_count="1",
18
+ active_dev_branch="refactor/R-007",
19
+ base_branch="main",
20
+ continuation_summary_path=".prizmkit/specs/R-007/continuation-summary.md",
21
+ )
22
+ prompt = append_continuation_handoff(
23
+ "REFACTOR PROMPT", args, str(tmp_path), "refactor", "R-007", "R-007",
24
+ {"steps": [{"id": "S01", "skill": "plan", "status": "pending"}]},
25
+ )
26
+
27
+ assert prompt.startswith("REFACTOR PROMPT")
28
+ assert "This is a fresh headless AI CLI session for the same task." in prompt
29
+ assert "Do not attempt `/compact` as the recovery mechanism." in prompt
30
+ assert (tmp_path / ".prizmkit/specs/R-007/continuation-summary.md").exists()
31
+ assert (tmp_path / ".prizmkit/refactor/R-007/continuation-summary.md").exists()
32
+
33
+
34
+ class TestRefactorCheckpointMerge:
35
+ def test_preserves_skipped_without_artifacts(self, tmp_path):
36
+ fresh = generate_refactor_checkpoint("R-008", "R-008-new")
37
+ existing = generate_refactor_checkpoint("R-008", "R-008-old")
38
+ existing["steps"][0]["status"] = "skipped"
39
+
40
+ merged = merge_refactor_checkpoint_state(existing, fresh, str(tmp_path))
41
+
42
+ assert merged["steps"][0]["status"] == "skipped"
43
+ assert merged["steps"][1]["status"] == "pending"
@@ -175,19 +175,30 @@ function Test-PrizmInfraError {
175
175
  return ($haystack -match '(?i)auth_unavailable|no auth available|502 Bad Gateway|503 Service Unavailable|504 Gateway Timeout|gateway timeout|upstream (connect )?error|connection reset|ECONNRESET|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|rate limit|rate_limit|temporarily unavailable|overloaded')
176
176
  }
177
177
 
178
- function Test-PrizmAiRuntimeError {
178
+ function Test-PrizmContextOverflowError {
179
179
  param([string]$SessionLog, [string]$ProgressJson)
180
180
 
181
+ $parts = @()
182
+ $codeParts = @()
183
+ $progressErrorLike = $false
184
+
181
185
  if ($ProgressJson -and (Test-Path $ProgressJson)) {
182
186
  try {
183
187
  $progress = Get-Content $ProgressJson -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
184
- if ($progress.PSObject.Properties['fatal_error_code'] -and $progress.fatal_error_code) {
185
- return $true
188
+ foreach ($name in @('fatal_error_code', 'api_error_code', 'error_code', 'stop_reason')) {
189
+ if ($progress.PSObject.Properties[$name] -and $progress.$name) { $codeParts += [string]$progress.$name }
190
+ }
191
+ if (($progress.PSObject.Properties['last_result_is_error'] -and $progress.last_result_is_error) -or ($progress.PSObject.Properties['api_error_status'] -and $progress.api_error_status)) {
192
+ $progressErrorLike = $true
186
193
  }
194
+ $parts += ($progress | ConvertTo-Json -Depth 12 -Compress)
187
195
  } catch {}
188
196
  }
189
197
 
190
- $parts = @()
198
+ $codeText = $codeParts -join ' '
199
+ $codePattern = '(?i)\b(context_overflow|context_too_large|context_length_exceeded|model_context_window_exceeded)\b'
200
+ if ($codeText -match $codePattern) { return $true }
201
+
191
202
  if ($SessionLog -and (Test-Path $SessionLog)) {
192
203
  try {
193
204
  $text = Get-Content $SessionLog -Raw -ErrorAction Stop
@@ -195,15 +206,166 @@ function Test-PrizmAiRuntimeError {
195
206
  $parts += $text
196
207
  } catch {}
197
208
  }
198
- if ($ProgressJson -and (Test-Path $ProgressJson)) {
199
- try { $parts += (Get-Content $ProgressJson -Raw -ErrorAction Stop) } catch {}
200
- }
201
209
  if ($parts.Count -eq 0) { return $false }
202
210
 
203
211
  $haystack = $parts -join "`n"
204
- $contextPattern = '(?i)context_too_large|model_context_window_exceeded|input exceeds the context window|context window of this model|context window (was )?exceeded|exceeded (the )?context window|invalid_request_error.*context window|context window.*invalid_request_error'
205
- $errorPattern = '(?i)api error|invalid_request_error|api_error_status|api_error_code|status\s*[:=]?\s*(400|413)|last_result_is_error\s*["'':=]?\s*true|is_error\s*["'':=]?\s*true'
206
- if (($haystack -match $contextPattern) -and ($haystack -match $errorPattern)) { return $true }
212
+ $wrapperPattern = '(?i)\bPRIZMKIT_FATAL_ERROR\s*=\s*context_overflow\b'
213
+ if ($haystack -match $wrapperPattern) { return $true }
214
+ if ($haystack -match $codePattern) { return $true }
215
+
216
+ $errorPattern = '(?i)\bapi error\b|invalid_request_error|\bbad request\b|\bstatus\s*[:=]?\s*(400|413)\b|\bapi_error_status\b|\bapi_error_code\b|\blast_result_is_error\b\s*["'':=]*\s*true\b|\bis_error\b\s*["'':=]*\s*true\b|\berror\s*[:=]|\bfatal\s*[:=]'
217
+ $hasErrorContext = $progressErrorLike -or ($haystack -match $errorPattern)
218
+ if (-not $hasErrorContext) { return $false }
219
+
220
+ $weakPattern = '(?i)Your input exceeds the context window|input exceeds (?:the )?(?:model )?context window|context window of this model|context window (?:was )?exceeded|exceeded (?:the )?context window|maximum context length exceeded|prompt is too long|too many tokens|input token count exceeds|input tokens? exceeds?|token count exceeds'
221
+ if ($haystack -match $weakPattern) { return $true }
222
+
223
+ $requestTooLargePattern = '(?i)\brequest (?:is )?too large\b'
224
+ $requestSemanticsPattern = '(?i)\b(context|token|tokens|prompt|input|model)\b'
225
+ if (($haystack -match $requestTooLargePattern) -and ($haystack -match $requestSemanticsPattern)) { return $true }
226
+
227
+ return $false
228
+ }
229
+
230
+ function Test-PrizmAiRuntimeError {
231
+ param([string]$SessionLog, [string]$ProgressJson)
232
+
233
+ if (Test-PrizmContextOverflowError -SessionLog $SessionLog -ProgressJson $ProgressJson) { return $false }
234
+
235
+ if ($ProgressJson -and (Test-Path $ProgressJson)) {
236
+ try {
237
+ $progress = Get-Content $ProgressJson -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
238
+ if ($progress.PSObject.Properties['fatal_error_code'] -and $progress.fatal_error_code) {
239
+ return $true
240
+ }
241
+ } catch {}
242
+ }
243
+
244
+ return $false
245
+ }
246
+
247
+ function Get-PrizmContextProgressFingerprint {
248
+ param(
249
+ [string]$ProjectRoot,
250
+ [string]$TaskType,
251
+ [string]$TaskId,
252
+ [string]$ArtifactDir = '',
253
+ [string]$CheckpointFile = ''
254
+ )
255
+
256
+ $projectRootResolved = if ($ProjectRoot) { $ProjectRoot } else { (Get-Location).Path }
257
+ $artifactArg = if ($ArtifactDir) { $ArtifactDir } else { '' }
258
+ $checkpointArg = if ($CheckpointFile) { $CheckpointFile } else { '' }
259
+ $python = @'
260
+ import hashlib
261
+ import json
262
+ import os
263
+ import subprocess
264
+ import sys
265
+
266
+ project_root, task_type, task_id, artifact_dir, checkpoint_file = sys.argv[1:6]
267
+ project_root = os.path.abspath(project_root or ".")
268
+ artifact_dir = os.path.abspath(os.path.join(project_root, artifact_dir)) if artifact_dir and not os.path.isabs(artifact_dir) else artifact_dir
269
+ checkpoint_file = os.path.abspath(os.path.join(project_root, checkpoint_file)) if checkpoint_file and not os.path.isabs(checkpoint_file) else checkpoint_file
270
+
271
+ def run_git(args):
272
+ try:
273
+ return subprocess.check_output(["git", "-C", project_root, *args], stderr=subprocess.DEVNULL)
274
+ except Exception:
275
+ return b""
276
+
277
+ def sha_bytes(*parts):
278
+ h = hashlib.sha256()
279
+ for part in parts:
280
+ if isinstance(part, str):
281
+ part = part.encode("utf-8", errors="surrogateescape")
282
+ h.update(part or b"")
283
+ h.update(b"\0")
284
+ return h.hexdigest()
285
+
286
+ def checkpoint_cursor(path):
287
+ if not path or not os.path.exists(path):
288
+ return "missing"
289
+ try:
290
+ with open(path, encoding="utf-8") as fh:
291
+ data = json.load(fh)
292
+ except Exception as exc:
293
+ return "unreadable:{}".format(type(exc).__name__)
294
+ steps = data.get("steps") if isinstance(data, dict) else None
295
+ if not isinstance(steps, list):
296
+ return "no-steps"
297
+ counts = {"completed": 0, "skipped": 0, "pending": 0, "in_progress": 0, "failed": 0}
298
+ first_open = None
299
+ for step in steps:
300
+ if not isinstance(step, dict):
301
+ continue
302
+ status = str(step.get("status", "pending"))
303
+ counts[status] = counts.get(status, 0) + 1
304
+ if first_open is None and status in ("pending", "in_progress"):
305
+ first_open = "{}:{}:{}".format(step.get("id", ""), step.get("skill", ""), status)
306
+ return "first_open={};counts={}".format(first_open or "none", ",".join("{}:{}".format(k, counts[k]) for k in sorted(counts)))
307
+
308
+ def artifact_fingerprint(path):
309
+ if not path or not os.path.isdir(path):
310
+ return "missing"
311
+ h = hashlib.sha256()
312
+ for root, dirs, files in os.walk(path):
313
+ dirs[:] = sorted(d for d in dirs if d not in {".git", "node_modules", "__pycache__"})
314
+ for name in sorted(files):
315
+ file_path = os.path.join(root, name)
316
+ rel = os.path.relpath(file_path, path).replace(os.sep, "/")
317
+ h.update(rel.encode("utf-8", errors="surrogateescape"))
318
+ h.update(b"\0")
319
+ try:
320
+ with open(file_path, "rb") as fh:
321
+ for chunk in iter(lambda: fh.read(65536), b""):
322
+ h.update(chunk)
323
+ except Exception as exc:
324
+ h.update("unreadable:{}".format(type(exc).__name__).encode("utf-8"))
325
+ h.update(b"\0")
326
+ return h.hexdigest()
327
+
328
+ fingerprint = {
329
+ "task_type": task_type,
330
+ "task_id": task_id,
331
+ "git_diff_fingerprint": sha_bytes(run_git(["status", "--porcelain", "--untracked-files=all", "--", "."]), run_git(["diff", "--binary", "HEAD", "--", "."])),
332
+ "checkpoint_cursor": checkpoint_cursor(checkpoint_file),
333
+ "artifact_fingerprint": artifact_fingerprint(artifact_dir),
334
+ "head_commit": run_git(["rev-parse", "HEAD"]).decode("utf-8", errors="ignore").strip(),
335
+ }
336
+ print(json.dumps(fingerprint, sort_keys=True, ensure_ascii=False))
337
+ '@
338
+ $scriptPath = [System.IO.Path]::GetTempFileName()
339
+ try {
340
+ Set-Content -Path $scriptPath -Value $python -Encoding UTF8
341
+ $pythonCmd = Get-Command python -ErrorAction SilentlyContinue
342
+ if ($pythonCmd) {
343
+ $output = & $pythonCmd.Source $scriptPath $projectRootResolved $TaskType $TaskId $artifactArg $checkpointArg
344
+ } else {
345
+ $pyCmd = Get-Command py -ErrorAction SilentlyContinue
346
+ if (-not $pyCmd) { throw 'Python 3 is required for progress fingerprinting.' }
347
+ $output = & $pyCmd.Source -3 $scriptPath $projectRootResolved $TaskType $TaskId $artifactArg $checkpointArg
348
+ }
349
+ if ($LASTEXITCODE -ne 0) { throw 'progress fingerprint python helper failed' }
350
+ return ($output -join "`n")
351
+ } finally {
352
+ Remove-Item -Force $scriptPath -ErrorAction SilentlyContinue
353
+ }
354
+ }
355
+
356
+ function Test-PrizmContextProgressChanged {
357
+ param([string]$BeforeJson, [string]$AfterJson)
358
+ try {
359
+ $before = if ($BeforeJson) { $BeforeJson | ConvertFrom-Json } else { $null }
360
+ $after = if ($AfterJson) { $AfterJson | ConvertFrom-Json } else { $null }
361
+ } catch {
362
+ return $true
363
+ }
364
+ foreach ($key in @('git_diff_fingerprint', 'checkpoint_cursor', 'artifact_fingerprint', 'head_commit')) {
365
+ $beforeValue = if ($before -and $before.PSObject.Properties[$key]) { [string]$before.$key } else { '' }
366
+ $afterValue = if ($after -and $after.PSObject.Properties[$key]) { [string]$after.$key } else { '' }
367
+ if ($beforeValue -ne $afterValue) { return $true }
368
+ }
207
369
  return $false
208
370
  }
209
371