prizmkit 1.1.67 → 1.1.69

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 (36) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/lib/common.sh +40 -0
  3. package/bundled/dev-pipeline/lib/heartbeat.sh +5 -5
  4. package/bundled/dev-pipeline/run-bugfix.sh +26 -5
  5. package/bundled/dev-pipeline/run-feature.sh +20 -3
  6. package/bundled/dev-pipeline/run-refactor.sh +26 -5
  7. package/bundled/dev-pipeline/scripts/parse-stream-progress.py +217 -18
  8. package/bundled/dev-pipeline/scripts/update-bug-status.py +15 -0
  9. package/bundled/dev-pipeline/scripts/update-feature-status.py +18 -0
  10. package/bundled/dev-pipeline/scripts/update-refactor-status.py +15 -0
  11. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +19 -1
  12. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +19 -1
  13. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +22 -1
  14. package/bundled/dev-pipeline/templates/sections/phase-critic-plan-full.md +10 -0
  15. package/bundled/dev-pipeline/templates/sections/phase-critic-plan.md +10 -0
  16. package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +12 -0
  17. package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +12 -0
  18. package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +5 -1
  19. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +5 -1
  20. package/bundled/dev-pipeline/tests/test_auto_skip.py +39 -0
  21. package/bundled/dev-pipeline-windows/SCHEMA_ANALYSIS.md +1 -1
  22. package/bundled/dev-pipeline-windows/lib/common.ps1 +19 -0
  23. package/bundled/dev-pipeline-windows/lib/pipeline.ps1 +19 -3
  24. package/bundled/dev-pipeline-windows/scripts/parse-stream-progress.py +217 -18
  25. package/bundled/dev-pipeline-windows/scripts/update-bug-status.py +15 -0
  26. package/bundled/dev-pipeline-windows/scripts/update-feature-status.py +18 -0
  27. package/bundled/dev-pipeline-windows/scripts/update-refactor-status.py +15 -0
  28. package/bundled/dev-pipeline-windows/templates/refactor-bootstrap-prompt.md +22 -1
  29. package/bundled/dev-pipeline-windows/templates/sections/phase-critic-plan-full.md +10 -0
  30. package/bundled/dev-pipeline-windows/templates/sections/phase-critic-plan.md +10 -0
  31. package/bundled/dev-pipeline-windows/templates/sections/phase-implement-agent.md +12 -0
  32. package/bundled/dev-pipeline-windows/templates/sections/phase-implement-full.md +12 -0
  33. package/bundled/dev-pipeline-windows/templates/sections/phase-review-agent.md +5 -1
  34. package/bundled/dev-pipeline-windows/templates/sections/phase-review-full.md +5 -1
  35. package/bundled/skills/_metadata.json +1 -1
  36. package/package.json +1 -1
@@ -63,7 +63,8 @@ PHASE_KEYWORDS = {
63
63
  class ProgressTracker:
64
64
  """Tracks progress state from stream-json events."""
65
65
 
66
- def __init__(self):
66
+ def __init__(self, session_log=None):
67
+ self.session_log_path = Path(session_log).expanduser() if session_log else None
67
68
  self.message_count = 0
68
69
  self.current_tool = None
69
70
  self.current_tool_input_summary = ""
@@ -78,12 +79,19 @@ class ProgressTracker:
78
79
  self.active_subagent_count = 0
79
80
  self.subagent_status_counts = Counter()
80
81
  self.codex_child_thread_ids = set()
82
+ self.claude_session_id = ""
83
+ self.claude_cwd = ""
84
+ self.claude_task_states = {}
81
85
  self.child_session_files = []
82
86
  self.child_total_bytes = 0
83
87
  self.child_activity_signature = ""
84
88
  self.last_child_activity_at = ""
85
89
  self._codex_child_session_paths = {}
90
+ self._claude_child_session_files = []
86
91
  self._last_child_scan_at = 0.0
92
+ self._last_claude_fallback_scan_at = 0.0
93
+ self._last_claude_fallback_scan_key = ""
94
+ self._claude_fallback_scan_interval_seconds = 10.0
87
95
  self._text_buffer = ""
88
96
  self._in_tool_use = False
89
97
  self._current_tool_input_parts = []
@@ -195,11 +203,76 @@ class ProgressTracker:
195
203
  self.is_active = True
196
204
 
197
205
  elif event_type == "system":
198
- # System events (hooks, init, etc.) — track but don't count as messages
206
+ # System events (hooks, init, task notifications, etc.) — track but don't count as messages.
199
207
  self.event_format = self.event_format or "stream-json"
200
208
  subtype = event.get("subtype", "")
201
209
  if subtype == "init":
202
210
  self.is_active = True
211
+ session_id = event.get("session_id")
212
+ if isinstance(session_id, str) and session_id.strip():
213
+ self.claude_session_id = session_id.strip()
214
+ cwd = event.get("cwd")
215
+ if isinstance(cwd, str) and cwd.strip():
216
+ self.claude_cwd = cwd.strip()
217
+ elif subtype == "task_started":
218
+ task_id = event.get("task_id")
219
+ if isinstance(task_id, str) and task_id.strip():
220
+ self.claude_task_states[task_id.strip()] = {
221
+ "status": "running",
222
+ "summary": str(event.get("description") or "")[:120],
223
+ "tool_use_id": str(event.get("tool_use_id") or ""),
224
+ "task_type": str(event.get("task_type") or ""),
225
+ "subagent_type": str(event.get("subagent_type") or ""),
226
+ }
227
+ self._update_claude_subagent_status_counts()
228
+ elif subtype in ("task_updated", "task_progress"):
229
+ task_id = event.get("task_id")
230
+ if isinstance(task_id, str) and task_id.strip():
231
+ state = self.claude_task_states.setdefault(task_id.strip(), {})
232
+ patch = event.get("patch") if isinstance(event.get("patch"), dict) else {}
233
+ status = patch.get("status") or event.get("status")
234
+ if status:
235
+ state["status"] = str(status)
236
+ summary = patch.get("summary") or patch.get("description") or event.get("summary") or event.get("description")
237
+ if summary:
238
+ state["summary"] = str(summary)[:120]
239
+ else:
240
+ state.setdefault("summary", "")
241
+ tool_use_id = patch.get("tool_use_id") or event.get("tool_use_id")
242
+ if tool_use_id:
243
+ state["tool_use_id"] = str(tool_use_id)
244
+ else:
245
+ state.setdefault("tool_use_id", "")
246
+ task_type = patch.get("task_type") or event.get("task_type")
247
+ if task_type:
248
+ state["task_type"] = str(task_type)
249
+ else:
250
+ state.setdefault("task_type", "")
251
+ subagent_type = patch.get("subagent_type") or event.get("subagent_type")
252
+ if subagent_type:
253
+ state["subagent_type"] = str(subagent_type)
254
+ else:
255
+ state.setdefault("subagent_type", "")
256
+ self._update_claude_subagent_status_counts()
257
+ elif subtype == "task_notification":
258
+ task_id = event.get("task_id")
259
+ if isinstance(task_id, str) and task_id.strip():
260
+ state = self.claude_task_states.setdefault(task_id.strip(), {})
261
+ status = event.get("status") or "completed"
262
+ state["status"] = str(status)
263
+ state["summary"] = str(event.get("summary") or state.get("summary") or "")[:120]
264
+ state.setdefault("tool_use_id", str(event.get("tool_use_id") or ""))
265
+ task_type = event.get("task_type")
266
+ if task_type:
267
+ state["task_type"] = str(task_type)
268
+ else:
269
+ state.setdefault("task_type", "")
270
+ subagent_type = event.get("subagent_type")
271
+ if subagent_type:
272
+ state["subagent_type"] = str(subagent_type)
273
+ else:
274
+ state.setdefault("subagent_type", "")
275
+ self._update_claude_subagent_status_counts()
203
276
 
204
277
  # ── Claude API raw stream format ────────────────────────────
205
278
  elif event_type == "message_start":
@@ -391,16 +464,135 @@ class ProgressTracker:
391
464
  pass
392
465
  return str(matches[0])
393
466
 
467
+ def _is_tracked_claude_subagent_state(self, state):
468
+ """Return true for Claude Code task events representing in-process agents."""
469
+ if not isinstance(state, dict):
470
+ return False
471
+ task_type = str(state.get("task_type") or "")
472
+ task_type_lower = task_type.lower()
473
+ subagent_type = str(state.get("subagent_type") or "")
474
+ if task_type_lower == "local_bash":
475
+ return False
476
+ tracked_types = {"in_process_teammate", "subagent", "agent", "teammate"}
477
+ if task_type_lower in tracked_types:
478
+ return True
479
+ if task_type_lower == "local_agent" and subagent_type:
480
+ return True
481
+ summary = str(state.get("summary") or "")
482
+ return bool(
483
+ not task_type
484
+ and summary.lower().startswith(("dev:", "critic:", "reviewer:", "agent:"))
485
+ )
486
+
487
+ def _has_tracked_claude_subagent_task(self):
488
+ """Return true once a Claude Code local-agent/subagent task has been observed."""
489
+ return any(
490
+ self._is_tracked_claude_subagent_state(state)
491
+ for state in self.claude_task_states.values()
492
+ )
493
+
494
+ def _update_claude_subagent_status_counts(self):
495
+ """Track Claude Code in-process teammate task state counts."""
496
+ counts = Counter()
497
+ active = 0
498
+ inactive_statuses = {
499
+ "completed",
500
+ "failed",
501
+ "cancelled",
502
+ "canceled",
503
+ "killed",
504
+ "stopped",
505
+ "success",
506
+ "error",
507
+ }
508
+ for state in self.claude_task_states.values():
509
+ if not self._is_tracked_claude_subagent_state(state):
510
+ continue
511
+ status = str(state.get("status") or "unknown")
512
+ counts[status] += 1
513
+ if status.lower() not in inactive_statuses:
514
+ active += 1
515
+ summary = state.get("summary") or state.get("subagent_type")
516
+ if summary:
517
+ self.last_text_snippet = str(summary).strip()[:120]
518
+ self._detect_phase(str(summary))
519
+ self.subagent_status_counts = counts
520
+ self.active_subagent_count = active
521
+
522
+ def _claude_projects_dir(self):
523
+ """Return the Claude Code projects directory for transcript lookup."""
524
+ projects_dir = os.environ.get("CLAUDE_PROJECTS_DIR")
525
+ if projects_dir:
526
+ return Path(projects_dir).expanduser()
527
+ claude_config_dir = os.environ.get("CLAUDE_CONFIG_DIR")
528
+ if claude_config_dir:
529
+ return Path(claude_config_dir).expanduser() / "projects"
530
+ claude_home = os.environ.get("CLAUDE_HOME")
531
+ if claude_home:
532
+ return Path(claude_home).expanduser() / "projects"
533
+ return Path.home() / ".claude" / "projects"
534
+
535
+ def _claude_project_key(self):
536
+ """Encode cwd the same way Claude Code stores project transcript dirs."""
537
+ cwd = self.claude_cwd
538
+ if not cwd:
539
+ return ""
540
+ return cwd.replace("\\", "-").replace("/", "-").replace(":", "")
541
+
542
+ def _find_claude_child_session_files(self):
543
+ """Find Claude Code subagent transcripts for this parent session."""
544
+ if not self.claude_session_id:
545
+ return []
546
+
547
+ projects_dir = self._claude_projects_dir()
548
+ if not projects_dir.exists():
549
+ return []
550
+
551
+ candidates = []
552
+ project_key = self._claude_project_key()
553
+ if project_key:
554
+ candidates.append(
555
+ projects_dir / project_key / self.claude_session_id / "subagents"
556
+ )
557
+
558
+ for candidate in candidates:
559
+ if candidate.exists():
560
+ try:
561
+ return sorted(candidate.glob("*.jsonl"))
562
+ except OSError:
563
+ return []
564
+
565
+ # Fallback for non-standard cwd encoding or custom Claude homes. Avoid
566
+ # repeatedly walking every stored transcript before any Agent task exists.
567
+ if not self._has_tracked_claude_subagent_task():
568
+ return []
569
+
570
+ fallback_scan_key = f"{projects_dir}:{self.claude_session_id}"
571
+ now = time.monotonic()
572
+ if (
573
+ self._last_claude_fallback_scan_key == fallback_scan_key
574
+ and now - self._last_claude_fallback_scan_at < self._claude_fallback_scan_interval_seconds
575
+ ):
576
+ return self._claude_child_session_files
577
+ self._last_claude_fallback_scan_key = fallback_scan_key
578
+ self._last_claude_fallback_scan_at = now
579
+ try:
580
+ matches = sorted(projects_dir.rglob(f"{self.claude_session_id}/subagents/*.jsonl"))
581
+ except OSError:
582
+ return []
583
+ return matches
584
+
394
585
  def refresh_child_session_activity(self, force=False):
395
- """Refresh Codex child transcript file stats.
586
+ """Refresh child transcript file stats.
396
587
 
397
588
  The heartbeat monitor uses this activity signature to treat subagent
398
- transcript growth as real progress while the parent Codex session is
399
- blocked in `wait`.
589
+ transcript growth as real progress while the parent session is blocked
590
+ waiting for a child agent/tool result. Supports Codex child threads and
591
+ Claude Code in-process teammate transcripts.
400
592
  """
401
593
  previous_signature = self.child_activity_signature
402
594
 
403
- if not self.codex_child_thread_ids:
595
+ if not self.codex_child_thread_ids and not self.claude_session_id:
404
596
  self.child_session_files = []
405
597
  self.child_total_bytes = 0
406
598
  self.child_activity_signature = ""
@@ -420,6 +612,7 @@ class ProgressTracker:
420
612
  found = self._find_codex_child_session_file(thread_id)
421
613
  if found:
422
614
  self._codex_child_session_paths[thread_id] = found
615
+ self._claude_child_session_files = self._find_claude_child_session_files()
423
616
  self._last_child_scan_at = now
424
617
 
425
618
  files = []
@@ -427,24 +620,22 @@ class ProgressTracker:
427
620
  total_bytes = 0
428
621
  max_mtime = 0.0
429
622
 
430
- for thread_id in sorted(self.codex_child_thread_ids):
431
- path = self._codex_child_session_paths.get(thread_id)
432
- if not path:
433
- continue
623
+ def add_file(kind, identifier, path):
624
+ nonlocal total_bytes, max_mtime
434
625
  try:
435
626
  stat = os.stat(path)
436
627
  except OSError:
437
- continue
438
-
628
+ return
629
+ path_str = str(path)
439
630
  total_bytes += stat.st_size
440
631
  max_mtime = max(max_mtime, stat.st_mtime)
441
- signature_parts.append(
442
- f"{thread_id}:{stat.st_size}:{getattr(stat, 'st_mtime_ns', int(stat.st_mtime * 1_000_000_000))}"
443
- )
632
+ mtime_ns = getattr(stat, "st_mtime_ns", int(stat.st_mtime * 1_000_000_000))
633
+ signature_parts.append(f"{kind}:{identifier}:{stat.st_size}:{mtime_ns}")
444
634
  files.append(
445
635
  {
446
- "thread_id": thread_id,
447
- "path": path,
636
+ "kind": kind,
637
+ "thread_id": identifier,
638
+ "path": path_str,
448
639
  "size": stat.st_size,
449
640
  "mtime": datetime.fromtimestamp(
450
641
  stat.st_mtime, timezone.utc
@@ -452,6 +643,14 @@ class ProgressTracker:
452
643
  }
453
644
  )
454
645
 
646
+ for thread_id in sorted(self.codex_child_thread_ids):
647
+ path = self._codex_child_session_paths.get(thread_id)
648
+ if path:
649
+ add_file("codex", thread_id, path)
650
+
651
+ for path in self._claude_child_session_files:
652
+ add_file("claude", path.stem, path)
653
+
455
654
  self.child_session_files = files
456
655
  self.child_total_bytes = total_bytes
457
656
  self.child_activity_signature = "|".join(signature_parts)
@@ -519,7 +718,7 @@ def atomic_write_json(data, filepath):
519
718
 
520
719
  def tail_and_parse(session_log, progress_file, poll_interval=0.5):
521
720
  """Tail session log and parse stream-json events."""
522
- tracker = ProgressTracker()
721
+ tracker = ProgressTracker(session_log)
523
722
  last_write_state = None
524
723
 
525
724
  def state_key(state):
@@ -41,6 +41,7 @@ SESSION_STATUS_VALUES = [
41
41
  "failed",
42
42
  "crashed",
43
43
  "timed_out",
44
+ "infra_error",
44
45
  "commit_missing",
45
46
  "docs_missing",
46
47
  "merge_conflict",
@@ -280,6 +281,16 @@ def action_update(args, bug_list_path, state_dir):
280
281
  bs["sessions"] = []
281
282
  bs["last_session_id"] = None
282
283
 
284
+ err = update_bug_in_list(bug_list_path, bug_id, new_status)
285
+ if err:
286
+ error_out("Failed to update .prizmkit/plans/bug-fix-list.json: {}".format(err))
287
+ return
288
+ elif session_status == "infra_error":
289
+ new_status = "pending"
290
+ bs["infra_error_count"] = bs.get("infra_error_count", 0) + 1
291
+ bs["last_infra_error_session_id"] = session_id
292
+ bs["resume_from_phase"] = None
293
+
283
294
  err = update_bug_in_list(bug_list_path, bug_id, new_status)
284
295
  if err:
285
296
  error_out("Failed to update .prizmkit/plans/bug-fix-list.json: {}".format(err))
@@ -333,6 +344,10 @@ def action_update(args, bug_list_path, state_dir):
333
344
  if session_status in ("commit_missing", "docs_missing", "merge_conflict"):
334
345
  summary["degraded_reason"] = session_status
335
346
  summary["restart_policy"] = "finalization_retry"
347
+ elif session_status == "infra_error":
348
+ summary["restart_policy"] = "infra_retry"
349
+ summary["infra_error_count"] = bs.get("infra_error_count", 0)
350
+ summary["artifacts_preserved"] = True
336
351
  elif session_status != "success":
337
352
  summary["restart_policy"] = "full_restart"
338
353
  summary["cleanup_performed"] = cleaned
@@ -45,6 +45,7 @@ SESSION_STATUS_VALUES = [
45
45
  "failed",
46
46
  "crashed",
47
47
  "timed_out",
48
+ "infra_error",
48
49
  "commit_missing",
49
50
  "docs_missing",
50
51
  "merge_conflict",
@@ -645,6 +646,19 @@ def action_update(args, feature_list_path, state_dir):
645
646
  fs["sessions"] = []
646
647
  fs["last_session_id"] = None
647
648
 
649
+ err = update_feature_in_list(feature_list_path, feature_id, new_status)
650
+ if err:
651
+ error_out("Failed to update .prizmkit/plans/feature-list.json: {}".format(err))
652
+ return
653
+ elif session_status == "infra_error":
654
+ # AI CLI/provider outage, auth failure, gateway error, etc.
655
+ # This is outside the code's control, so keep the item pending without
656
+ # consuming the task's retry budget.
657
+ new_status = "pending"
658
+ fs["infra_error_count"] = fs.get("infra_error_count", 0) + 1
659
+ fs["last_infra_error_session_id"] = session_id
660
+ fs["resume_from_phase"] = None
661
+
648
662
  err = update_feature_in_list(feature_list_path, feature_id, new_status)
649
663
  if err:
650
664
  error_out("Failed to update .prizmkit/plans/feature-list.json: {}".format(err))
@@ -701,6 +715,10 @@ def action_update(args, feature_list_path, state_dir):
701
715
  if session_status in ("commit_missing", "docs_missing", "merge_conflict"):
702
716
  summary["degraded_reason"] = session_status
703
717
  summary["restart_policy"] = "finalization_retry"
718
+ elif session_status == "infra_error":
719
+ summary["restart_policy"] = "infra_retry"
720
+ summary["infra_error_count"] = fs.get("infra_error_count", 0)
721
+ summary["artifacts_preserved"] = True
704
722
  elif session_status != "success":
705
723
  summary["restart_policy"] = "preserve_and_retry"
706
724
  summary["artifacts_preserved"] = True
@@ -42,6 +42,7 @@ SESSION_STATUS_VALUES = [
42
42
  "failed",
43
43
  "crashed",
44
44
  "timed_out",
45
+ "infra_error",
45
46
  "commit_missing",
46
47
  "docs_missing",
47
48
  "merge_conflict",
@@ -314,6 +315,16 @@ def action_update(args, refactor_list_path, state_dir):
314
315
  rs["sessions"] = []
315
316
  rs["last_session_id"] = None
316
317
 
318
+ err = update_refactor_in_list(refactor_list_path, refactor_id, new_status)
319
+ if err:
320
+ error_out("Failed to update .prizmkit/plans/refactor-list.json: {}".format(err))
321
+ return
322
+ elif session_status == "infra_error":
323
+ new_status = "pending"
324
+ rs["infra_error_count"] = rs.get("infra_error_count", 0) + 1
325
+ rs["last_infra_error_session_id"] = session_id
326
+ rs["resume_from_phase"] = None
327
+
317
328
  err = update_refactor_in_list(refactor_list_path, refactor_id, new_status)
318
329
  if err:
319
330
  error_out("Failed to update .prizmkit/plans/refactor-list.json: {}".format(err))
@@ -376,6 +387,10 @@ def action_update(args, refactor_list_path, state_dir):
376
387
  if session_status in ("commit_missing", "docs_missing", "merge_conflict"):
377
388
  summary["degraded_reason"] = session_status
378
389
  summary["restart_policy"] = "finalization_retry"
390
+ elif session_status == "infra_error":
391
+ summary["restart_policy"] = "infra_retry"
392
+ summary["infra_error_count"] = rs.get("infra_error_count", 0)
393
+ summary["artifacts_preserved"] = True
379
394
  elif session_status != "success":
380
395
  summary["restart_policy"] = "full_restart"
381
396
  summary["cleanup_performed"] = cleaned
@@ -80,6 +80,12 @@ You are the **refactor session orchestrator**. Execute Refactor {{REFACTOR_ID}}:
80
80
 
81
81
  **YOU are the orchestrator. Execute each phase by spawning the appropriate team agent with run_in_background=false.**
82
82
 
83
+ **Agent spawn failure policy (all Agent tool calls)**:
84
+ - If spawning Dev or Reviewer fails with team/config/lock errors, retry at most once.
85
+ - If the second attempt fails, do not keep spawning variants and do not enter artifact polling for Implementation Log, review-report, or refactor-report markers.
86
+ - Use the documented inline/recovery fallback for that phase: complete remaining refactor work directly in the orchestrator when safe, write the required report yourself where possible, or write `failure-log.md` with the spawn error and last observable state before stopping for recovery.
87
+ - Apply the same cap to any re-spawn for report repair or resume prompts; do not burn multiple minutes on identical team/config/lock failures.
88
+
83
89
  ## Workflow Checkpoint System
84
90
 
85
91
  A checkpoint file tracks your progress through this workflow:
@@ -164,6 +170,7 @@ Include browser verification approach in plan.md:
164
170
  **Goal**: Execute all tasks from plan.md while preserving existing behavior.
165
171
 
166
172
  - Spawn Dev agent (Agent tool, subagent_type="prizm-dev-team-dev", run_in_background=false)
173
+ Spawn failure cap: for team/config/lock errors, retry at most once for this Dev spawn. If the second attempt fails, do not poll for `## Implementation Log`; write `failure-log.md` and either complete remaining refactor work directly in the orchestrator or stop for recovery.
167
174
  Prompt: "Read {{DEV_SUBAGENT_PATH}}. For refactor {{REFACTOR_ID}} ('{{REFACTOR_TITLE}}'):
168
175
  1. Read `.prizmkit/refactor/{{REFACTOR_ID}}/spec.md` and `.prizmkit/refactor/{{REFACTOR_ID}}/plan.md`
169
176
  2. Read `.prizmkit/prizm-docs/` for affected modules (TRAPS, RULES, PATTERNS)
@@ -201,6 +208,7 @@ Include browser verification approach in plan.md:
201
208
  **Goal**: Verify refactoring quality and behavior preservation.
202
209
 
203
210
  - Spawn Reviewer agent (Agent tool, subagent_type="prizm-dev-team-reviewer", run_in_background=false)
211
+ Spawn failure cap: for team/config/lock errors, retry at most once for this Reviewer spawn. If the second attempt fails, do not poll for `review-report.md`; write `failure-log.md` with the spawn error and last observable state before stopping or performing an inline fallback.
204
212
  Prompt: "Read {{REVIEWER_SUBAGENT_PATH}}. For refactor {{REFACTOR_ID}}:
205
213
  1. Read `.prizmkit/refactor/{{REFACTOR_ID}}/spec.md` for goals and behavior preservation contracts
206
214
  2. Read `.prizmkit/refactor/{{REFACTOR_ID}}/plan.md` for architecture decisions and completed tasks
@@ -221,7 +229,20 @@ Include browser verification approach in plan.md:
221
229
  7. Report: verdict (PASS/NEEDS_FIXES), number of rounds, findings fixed/rejected
222
230
  "
223
231
  - **Wait for Reviewer to return**
224
- - Read `review-report.md`if PASS proceed, if NEEDS_FIXES log remaining findings and proceed.
232
+ - **Gate CheckReview Report**:
233
+ After Reviewer returns, verify the review report contains a verdict:
234
+ ```powershell
235
+ if (Select-String -Path .prizmkit/refactor/{{REFACTOR_ID}}/review-report.md -Pattern "## Verdict" -Quiet) { "GATE:PASS" } else { "GATE:MISSING" }
236
+ ```
237
+ If GATE:MISSING:
238
+ - Do not enter an unbounded report-repair loop and do not repeatedly re-spawn Reviewer.
239
+ - Perform one bounded status check; retry at most once: inspect the Reviewer output, `review-report.md` path, and any internal Reviewer/Dev spawn messages from `/prizmkit-code-review`.
240
+ - If the missing report is caused by team/config/lock errors from the Reviewer or internal Reviewer/Dev agent spawn, retry the Reviewer agent at most once only if it appears transient.
241
+ - If the report is still missing after that single check/retry, write `.prizmkit/refactor/{{REFACTOR_ID}}/failure-log.md` with the spawn/skill error and last observable state, then either perform a safe inline fallback review (spec/plan/diff/tests → write `review-report.md` with `## Verdict`) or stop with a clear recovery failure.
242
+
243
+ Read `review-report.md` and check the Verdict:
244
+ - `PASS` → proceed to next phase
245
+ - `NEEDS_FIXES` → log remaining findings and proceed (do not retry externally)
225
246
  - **CP-RF-3**: Code review complete, tests pass, behavior preserved
226
247
  - **Checkpoint update**: set step `prizmkit-code-review` to `"completed"` in `{{CHECKPOINT_PATH}}`
227
248
 
@@ -8,6 +8,16 @@ If CRITIC:MISSING — skip this phase entirely and proceed. Log: "Critic agent n
8
8
 
9
9
  **Choose ONE path based on `{{CRITIC_COUNT}}`:**
10
10
 
11
+ **Agent spawn failure policy**:
12
+ - If spawning Critic fails with team/config/lock errors, retry at most once.
13
+ - If the second attempt fails, do not keep spawning variants. Either create the required team once (when team tooling is available) or perform the plan challenge inline and write the required challenge report yourself.
14
+ - Record the fallback in the report; do not burn multiple minutes on repeated identical spawn failures.
15
+
16
+ **No silent report polling**:
17
+ - Do NOT run a long no-output loop waiting for `challenge-report*.md`.
18
+ - If you need to wait for a report file, use a short bounded check (≤120s) that prints elapsed time and reports present on every iteration.
19
+ - If reports are still missing after the bounded check, request one status update; if still missing, perform the missing challenge lens inline and continue.
20
+
11
21
  **If {{CRITIC_COUNT}} = 1 → Single Critic** (skip to CP-2.5 after this):
12
22
 
13
23
  **Spawn Agent**:
@@ -16,6 +16,16 @@ If CRITIC:MISSING — skip this phase entirely and proceed. Log: "Critic agent n
16
16
  **Prompt**:
17
17
  > {{AGENT_PROMPT_CRITIC_PLAN_CHALLENGE}}
18
18
 
19
+ **Agent spawn failure policy**:
20
+ - If spawning Critic fails with team/config/lock errors, retry at most once.
21
+ - If the second attempt fails, do not keep spawning variants. Either create the required team once (when team tooling is available) or perform the plan challenge inline and write `challenge-report.md` yourself.
22
+ - Record the fallback in the report; do not burn multiple minutes on repeated identical spawn failures.
23
+
24
+ **No silent report polling**:
25
+ - Do NOT run a long no-output loop waiting for `challenge-report.md`.
26
+ - If you need to wait for the report file, use a short bounded check (≤120s) that prints elapsed time and whether the report exists on every iteration.
27
+ - If the report is still missing after the bounded check, request one status update; if still missing, perform the challenge inline and continue.
28
+
19
29
  Wait for Critic to return.
20
30
  - Read challenge-report.md. For items marked CRITICAL/HIGH: decide whether to adjust plan.md or document why the plan stands.
21
31
  - Max 1 plan revision round.
@@ -15,11 +15,23 @@
15
15
  | subagent_type | prizm-dev-team-dev |
16
16
  | run_in_background | false |
17
17
 
18
+ **Agent spawn failure policy**:
19
+ - If spawning Dev fails with team/config/lock errors, retry at most once.
20
+ - If the second attempt fails, do not enter Implementation Log polling or repeated recovery spawn loops.
21
+ - Use the documented inline/recovery fallback: write `failure-log.md` with the spawn error and last observable state, then either complete remaining tasks directly in the orchestrator or stop with a clear failure for recovery.
22
+ - Apply the same cap to Dev re-spawns for Implementation Log repair or resume prompts; do not burn multiple minutes on identical team/config/lock failures.
23
+
18
24
  **Prompt**:
19
25
  > {{AGENT_PROMPT_DEV_IMPLEMENT}}
20
26
 
21
27
  Wait for Dev to return. All tasks must be `[x]`, tests pass.
22
28
 
29
+ **No silent artifact polling**:
30
+ - Do NOT run a long no-output loop that only waits for `## Implementation Log` or any other file marker.
31
+ - If you must wait for Dev after spawning or sending a status request, use short bounded checks (≤120s) that print a heartbeat line each iteration with: elapsed time, remaining unchecked task count, whether `## Implementation Log` exists, and whether `git diff --stat` changed.
32
+ - If Dev has no transcript/file/diff progress for one bounded check, send one status request. If there is still no progress on the next bounded check, stop waiting, write `failure-log.md` with the last observable state, and follow Subagent Timeout Recovery.
33
+ - Prefer the Agent tool's completion notification or Dev's `COMPLETION_SIGNAL`; file presence alone is not a liveness signal.
34
+
23
35
  **Gate Check — Implementation Log**:
24
36
  After Dev agent returns, verify the Implementation Log was written:
25
37
  ```powershell
@@ -22,9 +22,21 @@ Select-String -Pattern '^\- \[ \]' .prizmkit/specs/{{FEATURE_SLUG}}/plan.md 2>$n
22
22
  | subagent_type | prizm-dev-team-dev |
23
23
  | run_in_background | false |
24
24
 
25
+ **Agent spawn failure policy**:
26
+ - If spawning Dev fails with team/config/lock errors, retry at most once.
27
+ - If the second attempt fails, do not enter Implementation Log polling or repeated recovery spawn loops.
28
+ - Use the documented inline/recovery fallback: write `failure-log.md` with the spawn error and last observable state, then either complete remaining tasks directly in the orchestrator or stop with a clear failure for recovery.
29
+ - Apply the same cap to Dev re-spawns for Implementation Log repair or resume prompts; do not burn multiple minutes on identical team/config/lock failures.
30
+
25
31
  **Prompt**:
26
32
  > {{AGENT_PROMPT_DEV_IMPLEMENT}}
27
33
 
34
+ **No silent artifact polling**:
35
+ - Do NOT run a long no-output loop that only waits for `## Implementation Log` or any other file marker.
36
+ - If you must wait for Dev after spawning or sending a status request, use short bounded checks (≤120s) that print a heartbeat line each iteration with: elapsed time, remaining unchecked task count, whether `## Implementation Log` exists, and whether `git diff --stat` changed.
37
+ - If Dev has no transcript/file/diff progress for one bounded check, send one status request. If there is still no progress on the next bounded check, stop waiting, write `failure-log.md` with the last observable state, and follow Subagent Timeout Recovery.
38
+ - Prefer the Agent tool's completion notification or Dev's `COMPLETION_SIGNAL`; file presence alone is not a liveness signal.
39
+
28
40
  **Gate Check — Implementation Log**:
29
41
  After Dev agent returns, verify the Implementation Log was written:
30
42
  ```powershell
@@ -9,7 +9,11 @@ After `/prizmkit-code-review` returns, verify the review report:
9
9
  ```powershell
10
10
  if (Select-String -Path .prizmkit/specs/{{FEATURE_SLUG}}/review-report.md -Pattern "## Verdict" -Quiet) { "GATE:PASS" } else { "GATE:MISSING" }
11
11
  ```
12
- If GATE:MISSING — re-run `/prizmkit-code-review`.
12
+ If GATE:MISSING:
13
+ - Do not re-run `/prizmkit-code-review` in an unbounded report-repair loop.
14
+ - Perform one bounded status check; retry at most once: inspect the skill output, `review-report.md` path, and any Reviewer/Dev spawn messages.
15
+ - If the missing report is caused by team/config/lock errors from the internal Reviewer/Dev agent spawn, retry `/prizmkit-code-review` at most once only if it appears transient.
16
+ - If the report is still missing after that single check/retry, write `failure-log.md` with the spawn/skill error and last observable state, then either perform a safe inline fallback review (spec/plan/diff/tests → write `review-report.md` with `## Verdict`) or stop with a clear recovery failure.
13
17
 
14
18
  Read `review-report.md` and check the Verdict:
15
19
  - `PASS` → proceed to next phase
@@ -9,7 +9,11 @@ After `/prizmkit-code-review` returns, verify the review report:
9
9
  ```powershell
10
10
  if (Select-String -Path .prizmkit/specs/{{FEATURE_SLUG}}/review-report.md -Pattern "## Verdict" -Quiet) { "GATE:PASS" } else { "GATE:MISSING" }
11
11
  ```
12
- If GATE:MISSING — re-run `/prizmkit-code-review`.
12
+ If GATE:MISSING:
13
+ - Do not re-run `/prizmkit-code-review` in an unbounded report-repair loop.
14
+ - Perform one bounded status check; retry at most once: inspect the skill output, `review-report.md` path, and any Reviewer/Dev spawn messages.
15
+ - If the missing report is caused by team/config/lock errors from the internal Reviewer/Dev agent spawn, retry `/prizmkit-code-review` at most once only if it appears transient.
16
+ - If the report is still missing after that single check/retry, write `failure-log.md` with the spawn/skill error and last observable state, then either perform a safe inline fallback review (spec/plan/diff/tests → write `review-report.md` with `## Verdict`) or stop with a clear recovery failure.
13
17
 
14
18
  Read `review-report.md` and check the Verdict:
15
19
  - `PASS` → proceed to next phase
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.1.67",
2
+ "version": "1.1.69",
3
3
  "skills": {
4
4
  "prizm-kit": {
5
5
  "description": "Full-lifecycle dev toolkit. Covers spec-driven development, Prizm context docs, code quality, debugging, deployment, and knowledge management.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prizmkit",
3
- "version": "1.1.67",
3
+ "version": "1.1.69",
4
4
  "description": "Create a new PrizmKit-powered project with clean initialization — no framework dev files, just what you need.",
5
5
  "type": "module",
6
6
  "bin": {