prizmkit 1.1.66 → 1.1.68

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 (26) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/adapters/codex/settings-adapter.js +1 -1
  3. package/bundled/dev-pipeline/.env.example +3 -0
  4. package/bundled/dev-pipeline/SCHEMA_ANALYSIS.md +3 -1
  5. package/bundled/dev-pipeline/lib/common.sh +61 -18
  6. package/bundled/dev-pipeline/lib/heartbeat.sh +104 -11
  7. package/bundled/dev-pipeline/run-bugfix.sh +26 -5
  8. package/bundled/dev-pipeline/run-feature.sh +20 -3
  9. package/bundled/dev-pipeline/run-refactor.sh +26 -5
  10. package/bundled/dev-pipeline/scripts/parse-stream-progress.py +144 -12
  11. package/bundled/dev-pipeline/scripts/update-bug-status.py +15 -0
  12. package/bundled/dev-pipeline/scripts/update-feature-status.py +18 -0
  13. package/bundled/dev-pipeline/scripts/update-refactor-status.py +15 -0
  14. package/bundled/dev-pipeline/tests/test_auto_skip.py +39 -0
  15. package/bundled/dev-pipeline-windows/.env.example +3 -2
  16. package/bundled/dev-pipeline-windows/SCHEMA_ANALYSIS.md +4 -3
  17. package/bundled/dev-pipeline-windows/lib/common.ps1 +97 -5
  18. package/bundled/dev-pipeline-windows/lib/pipeline.ps1 +31 -7
  19. package/bundled/dev-pipeline-windows/run-recovery.ps1 +8 -1
  20. package/bundled/dev-pipeline-windows/scripts/parse-stream-progress.py +144 -12
  21. package/bundled/dev-pipeline-windows/scripts/update-bug-status.py +15 -0
  22. package/bundled/dev-pipeline-windows/scripts/update-feature-status.py +18 -0
  23. package/bundled/dev-pipeline-windows/scripts/update-refactor-status.py +15 -0
  24. package/bundled/skills/_metadata.json +1 -1
  25. package/package.json +1 -1
  26. package/src/scaffold.js +1 -1
@@ -23,6 +23,7 @@ import tempfile
23
23
  import time
24
24
  from collections import Counter
25
25
  from datetime import datetime, timezone
26
+ from pathlib import Path
26
27
 
27
28
 
28
29
  # Ordered pipeline phases — index defines forward-only progression.
@@ -76,6 +77,13 @@ class ProgressTracker:
76
77
  self.event_format = ""
77
78
  self.active_subagent_count = 0
78
79
  self.subagent_status_counts = Counter()
80
+ self.codex_child_thread_ids = set()
81
+ self.child_session_files = []
82
+ self.child_total_bytes = 0
83
+ self.child_activity_signature = ""
84
+ self.last_child_activity_at = ""
85
+ self._codex_child_session_paths = {}
86
+ self._last_child_scan_at = 0.0
79
87
  self._text_buffer = ""
80
88
  self._in_tool_use = False
81
89
  self._current_tool_input_parts = []
@@ -113,6 +121,9 @@ class ProgressTracker:
113
121
 
114
122
  elif item_type == "collab_tool_call":
115
123
  tool_name = item.get("tool", "collab")
124
+ self._record_codex_child_thread_ids(
125
+ item.get("receiver_thread_ids")
126
+ )
116
127
  if event_type == "item.started":
117
128
  self.current_tool = tool_name
118
129
  self.tool_call_counts[tool_name] += 1
@@ -345,8 +356,117 @@ class ProgressTracker:
345
356
  self.subagent_status_counts = counts
346
357
  self.active_subagent_count = active
347
358
 
359
+ def _record_codex_child_thread_ids(self, thread_ids):
360
+ """Remember Codex child thread IDs reported by collab tool calls."""
361
+ if not isinstance(thread_ids, list):
362
+ return
363
+ for thread_id in thread_ids:
364
+ if isinstance(thread_id, str) and thread_id.strip():
365
+ self.codex_child_thread_ids.add(thread_id.strip())
366
+
367
+ def _codex_sessions_dir(self):
368
+ """Return the Codex sessions directory for the current environment."""
369
+ codex_home = os.environ.get("CODEX_HOME")
370
+ if codex_home:
371
+ return Path(codex_home).expanduser() / "sessions"
372
+ return Path.home() / ".codex" / "sessions"
373
+
374
+ def _find_codex_child_session_file(self, thread_id):
375
+ """Find a Codex transcript file for a child thread ID."""
376
+ sessions_dir = self._codex_sessions_dir()
377
+ if not sessions_dir.exists():
378
+ return None
379
+
380
+ try:
381
+ matches = list(sessions_dir.rglob(f"*{thread_id}.jsonl"))
382
+ except OSError:
383
+ return None
384
+
385
+ if not matches:
386
+ return None
387
+
388
+ try:
389
+ matches.sort(key=lambda path: path.stat().st_mtime, reverse=True)
390
+ except OSError:
391
+ pass
392
+ return str(matches[0])
393
+
394
+ def refresh_child_session_activity(self, force=False):
395
+ """Refresh Codex child transcript file stats.
396
+
397
+ 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`.
400
+ """
401
+ previous_signature = self.child_activity_signature
402
+
403
+ if not self.codex_child_thread_ids:
404
+ self.child_session_files = []
405
+ self.child_total_bytes = 0
406
+ self.child_activity_signature = ""
407
+ self.last_child_activity_at = ""
408
+ return previous_signature != self.child_activity_signature
409
+
410
+ now = time.monotonic()
411
+ should_scan = (
412
+ force
413
+ or self._last_child_scan_at == 0.0
414
+ or (now - self._last_child_scan_at >= 2.0)
415
+ )
416
+ if should_scan:
417
+ for thread_id in sorted(self.codex_child_thread_ids):
418
+ path = self._codex_child_session_paths.get(thread_id)
419
+ if not path or not os.path.exists(path):
420
+ found = self._find_codex_child_session_file(thread_id)
421
+ if found:
422
+ self._codex_child_session_paths[thread_id] = found
423
+ self._last_child_scan_at = now
424
+
425
+ files = []
426
+ signature_parts = []
427
+ total_bytes = 0
428
+ max_mtime = 0.0
429
+
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
434
+ try:
435
+ stat = os.stat(path)
436
+ except OSError:
437
+ continue
438
+
439
+ total_bytes += stat.st_size
440
+ 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
+ )
444
+ files.append(
445
+ {
446
+ "thread_id": thread_id,
447
+ "path": path,
448
+ "size": stat.st_size,
449
+ "mtime": datetime.fromtimestamp(
450
+ stat.st_mtime, timezone.utc
451
+ ).strftime("%Y-%m-%dT%H:%M:%SZ"),
452
+ }
453
+ )
454
+
455
+ self.child_session_files = files
456
+ self.child_total_bytes = total_bytes
457
+ self.child_activity_signature = "|".join(signature_parts)
458
+ self.last_child_activity_at = (
459
+ datetime.fromtimestamp(max_mtime, timezone.utc).strftime(
460
+ "%Y-%m-%dT%H:%M:%SZ"
461
+ )
462
+ if max_mtime
463
+ else ""
464
+ )
465
+ return previous_signature != self.child_activity_signature
466
+
348
467
  def to_dict(self):
349
468
  """Export current state as a dictionary for JSON serialization."""
469
+ self.refresh_child_session_activity()
350
470
  tool_calls = [
351
471
  {"name": name, "count": count}
352
472
  for name, count in self.tool_call_counts.most_common()
@@ -367,6 +487,11 @@ class ProgressTracker:
367
487
  "total_tool_calls": self.total_tool_calls,
368
488
  "active_subagent_count": self.active_subagent_count,
369
489
  "subagent_states": subagent_states,
490
+ "child_thread_ids": sorted(self.codex_child_thread_ids),
491
+ "child_session_files": self.child_session_files,
492
+ "child_total_bytes": self.child_total_bytes,
493
+ "child_activity_signature": self.child_activity_signature,
494
+ "last_child_activity_at": self.last_child_activity_at,
370
495
  "last_text_snippet": self.last_text_snippet,
371
496
  "is_active": self.is_active,
372
497
  "errors": self.errors[-10:], # Keep last 10 errors
@@ -397,6 +522,15 @@ def tail_and_parse(session_log, progress_file, poll_interval=0.5):
397
522
  tracker = ProgressTracker()
398
523
  last_write_state = None
399
524
 
525
+ def state_key(state):
526
+ return (
527
+ state["message_count"],
528
+ state["current_tool"],
529
+ state["current_phase"],
530
+ state["total_tool_calls"],
531
+ state.get("child_activity_signature", ""),
532
+ )
533
+
400
534
  # Wait for log file to appear
401
535
  wait_count = 0
402
536
  while not os.path.exists(session_log):
@@ -428,22 +562,20 @@ def tail_and_parse(session_log, progress_file, poll_interval=0.5):
428
562
 
429
563
  # Write progress if state changed
430
564
  current_state = tracker.to_dict()
431
- state_key = (
432
- current_state["message_count"],
433
- current_state["current_tool"],
434
- current_state["current_phase"],
435
- current_state["total_tool_calls"],
436
- )
437
- if state_key != last_write_state:
565
+ current_state_key = state_key(current_state)
566
+ if current_state_key != last_write_state:
438
567
  atomic_write_json(current_state, progress_file)
439
- last_write_state = state_key
568
+ last_write_state = current_state_key
440
569
  else:
441
570
  idle_count += 1
442
- # After 2 seconds of no new data, write current state anyway
443
- # (ensures progress.json stays fresh)
444
- if idle_count == 4:
571
+ # Every 2 seconds of no parent log data, refresh child Codex
572
+ # transcript stats and write if child activity advanced.
573
+ if idle_count % 4 == 0:
445
574
  current_state = tracker.to_dict()
446
- atomic_write_json(current_state, progress_file)
575
+ current_state_key = state_key(current_state)
576
+ if current_state_key != last_write_state or idle_count == 4:
577
+ atomic_write_json(current_state, progress_file)
578
+ last_write_state = current_state_key
447
579
 
448
580
  # After 3600 idle cycles (30 min), mark inactive and exit
449
581
  if idle_count > 3600:
@@ -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
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.1.66",
2
+ "version": "1.1.68",
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.66",
3
+ "version": "1.1.68",
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": {
package/src/scaffold.js CHANGED
@@ -576,7 +576,7 @@ project_doc_fallback_filenames = ["CLAUDE.md", "CODEBUDDY.md"]
576
576
 
577
577
  [agents]
578
578
  max_depth = 1
579
- job_max_runtime_seconds = 840
579
+ job_max_runtime_seconds = 3300
580
580
  `;
581
581
  await fs.writeFile(configPath, configToml);
582
582
  await fs.remove(legacySettingsPath);