prizmkit 1.1.90 → 1.1.92

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.
@@ -17,7 +17,7 @@ Usage:
17
17
  --feature-list <path> --state-dir <path> \
18
18
  --action <get_next|start|update|status|pause|reset|clean|complete|unskip> \
19
19
  [--feature-id <id>] [--session-status <status>] \
20
- [--session-id <id>] [--max-retries <n>] \
20
+ [--session-id <id>] [--max-retries <n>] [--max-infra-retries <n>] \
21
21
  [--features <filter>]
22
22
  """
23
23
 
@@ -94,7 +94,13 @@ def parse_args():
94
94
  "--max-retries",
95
95
  type=int,
96
96
  default=3,
97
- help="Maximum retry count before marking as failed (default: 3)",
97
+ help="Maximum code retry count before marking as failed (default: 3)",
98
+ )
99
+ parser.add_argument(
100
+ "--max-infra-retries",
101
+ type=int,
102
+ default=3,
103
+ help="Maximum infrastructure retry count before marking as failed (default: 3)",
98
104
  )
99
105
  parser.add_argument(
100
106
  "--feature-slug",
@@ -170,6 +176,9 @@ def load_feature_status(state_dir, feature_id):
170
176
  "feature_id": feature_id,
171
177
  "retry_count": 0,
172
178
  "max_retries": 3,
179
+ "max_infra_retries": 3,
180
+ "infra_error_count": 0,
181
+ "last_infra_error_session_id": None,
173
182
  "sessions": [],
174
183
  "last_session_id": None,
175
184
  "resume_from_phase": None,
@@ -183,6 +192,9 @@ def load_feature_status(state_dir, feature_id):
183
192
  "feature_id": feature_id,
184
193
  "retry_count": 0,
185
194
  "max_retries": 3,
195
+ "max_infra_retries": 3,
196
+ "infra_error_count": 0,
197
+ "last_infra_error_session_id": None,
186
198
  "sessions": [],
187
199
  "last_session_id": None,
188
200
  "resume_from_phase": None,
@@ -565,6 +577,7 @@ def action_get_next(feature_list_data, state_dir, feature_filter=None):
565
577
  "feature_id": chosen_id,
566
578
  "title": chosen.get("title", ""),
567
579
  "retry_count": chosen_status_data.get("retry_count", 0),
580
+ "infra_error_count": chosen_status_data.get("infra_error_count", 0),
568
581
  "resume_from_phase": chosen_status_data.get("resume_from_phase", None),
569
582
  }
570
583
  print(json.dumps(result, indent=2, ensure_ascii=False))
@@ -585,6 +598,7 @@ def action_update(args, feature_list_path, state_dir):
585
598
  session_status = args.session_status
586
599
  session_id = args.session_id
587
600
  max_retries = args.max_retries
601
+ max_infra_retries = args.max_infra_retries
588
602
 
589
603
  if not feature_id:
590
604
  error_out("--feature-id is required for 'update' action")
@@ -600,6 +614,8 @@ def action_update(args, feature_list_path, state_dir):
600
614
  new_status = current_list_status
601
615
 
602
616
  if session_status == "success":
617
+ fs["infra_error_count"] = 0
618
+ fs["last_infra_error_session_id"] = None
603
619
  # No-op guard: if this exact successful session was already recorded,
604
620
  # avoid rewriting state files again (prevents post-commit dirty changes).
605
621
  existing_sessions = fs.get("sessions", [])
@@ -616,6 +632,8 @@ def action_update(args, feature_list_path, state_dir):
616
632
  "session_status": session_status,
617
633
  "new_status": "completed",
618
634
  "retry_count": fs.get("retry_count", 0),
635
+ "infra_error_count": fs.get("infra_error_count", 0),
636
+ "max_infra_retries": max_infra_retries,
619
637
  "resume_from_phase": fs.get("resume_from_phase"),
620
638
  "updated_at": fs.get("updated_at"),
621
639
  "no_op": True,
@@ -654,15 +672,24 @@ def action_update(args, feature_list_path, state_dir):
654
672
  return
655
673
  elif session_status == "infra_error":
656
674
  # AI CLI/provider outage, auth failure, gateway error, etc.
657
- # This is outside the code's control, so keep the item pending without
658
- # consuming the task's retry budget.
659
- new_status = "pending"
660
- fs["infra_error_count"] = fs.get("infra_error_count", 0) + 1
675
+ # Infra failures do not consume the code retry budget, but they still
676
+ # need their own bounded budget so a flaky provider cannot loop forever.
677
+ infra_error_count = fs.get("infra_error_count", 0) + 1
678
+ fs["infra_error_count"] = infra_error_count
661
679
  fs["last_infra_error_session_id"] = session_id
680
+ fs["max_infra_retries"] = max_infra_retries
681
+ fs["degraded_reason"] = "infra_error"
662
682
  if session_id:
663
683
  fs["last_session_id"] = session_id
664
684
  fs["resume_from_phase"] = None
665
685
 
686
+ if infra_error_count >= max_infra_retries:
687
+ new_status = "failed"
688
+ if session_id:
689
+ fs["last_failed_session_id"] = session_id
690
+ else:
691
+ new_status = "pending"
692
+
666
693
  err = update_feature_in_list(feature_list_path, feature_id, new_status)
667
694
  if err:
668
695
  error_out("Failed to update .prizmkit/plans/feature-list.json: {}".format(err))
@@ -714,6 +741,8 @@ def action_update(args, feature_list_path, state_dir):
714
741
  "session_status": session_status,
715
742
  "new_status": new_status,
716
743
  "retry_count": fs["retry_count"],
744
+ "infra_error_count": fs.get("infra_error_count", 0),
745
+ "max_infra_retries": max_infra_retries,
717
746
  "resume_from_phase": fs.get("resume_from_phase"),
718
747
  "updated_at": fs["updated_at"],
719
748
  }
@@ -1143,6 +1172,8 @@ def action_start(args, feature_list_path, state_dir):
1143
1172
  "feature_id": feature_id,
1144
1173
  "old_status": old_status,
1145
1174
  "new_status": "in_progress",
1175
+ "retry_count": fs.get("retry_count", 0),
1176
+ "infra_error_count": fs.get("infra_error_count", 0),
1146
1177
  "updated_at": fs["updated_at"],
1147
1178
  }
1148
1179
  print(json.dumps(result, indent=2, ensure_ascii=False))
@@ -1171,6 +1202,8 @@ def action_reset(args, feature_list_path, state_dir):
1171
1202
 
1172
1203
  # Reset runtime fields
1173
1204
  fs["retry_count"] = 0
1205
+ fs["infra_error_count"] = 0
1206
+ fs["last_infra_error_session_id"] = None
1174
1207
  fs["sessions"] = []
1175
1208
  fs["last_session_id"] = None
1176
1209
  fs["resume_from_phase"] = None
@@ -1264,6 +1297,8 @@ def action_clean(args, feature_list_path, state_dir):
1264
1297
  old_retry = fs.get("retry_count", 0)
1265
1298
 
1266
1299
  fs["retry_count"] = 0
1300
+ fs["infra_error_count"] = 0
1301
+ fs["last_infra_error_session_id"] = None
1267
1302
  fs["sessions"] = []
1268
1303
  fs["last_session_id"] = None
1269
1304
  fs["resume_from_phase"] = None
@@ -1423,6 +1458,8 @@ def action_unskip(args, feature_list_path, state_dir):
1423
1458
  for fid in to_reset:
1424
1459
  fs = load_feature_status(state_dir, fid)
1425
1460
  fs["retry_count"] = 0
1461
+ fs["infra_error_count"] = 0
1462
+ fs["last_infra_error_session_id"] = None
1426
1463
  fs["sessions"] = []
1427
1464
  fs["last_session_id"] = None
1428
1465
  fs["resume_from_phase"] = None
@@ -16,7 +16,7 @@ Usage:
16
16
  --refactor-list <path> --state-dir <path> \
17
17
  --action <get_next|start|update|status|pause|reset|clean|unskip> \
18
18
  [--refactor-id <id>] [--session-status <status>] \
19
- [--session-id <id>] [--max-retries <n>]
19
+ [--session-id <id>] [--max-retries <n>] [--max-infra-retries <n>]
20
20
  """
21
21
 
22
22
  import argparse
@@ -87,7 +87,8 @@ def parse_args():
87
87
  help="Session outcome status (required for 'update' action)",
88
88
  )
89
89
  parser.add_argument("--session-id", default=None, help="Session ID (optional, for 'update' action)")
90
- parser.add_argument("--max-retries", type=int, default=3, help="Maximum retry count (default: 3)")
90
+ parser.add_argument("--max-retries", type=int, default=3, help="Maximum code retry count (default: 3)")
91
+ parser.add_argument("--max-infra-retries", type=int, default=3, help="Maximum infrastructure retry count (default: 3)")
91
92
  parser.add_argument("--project-root", default=None, help="Project root directory. Required for 'clean' action.")
92
93
  return parser.parse_args()
93
94
 
@@ -103,6 +104,9 @@ def _default_status(refactor_id):
103
104
  "refactor_id": refactor_id,
104
105
  "retry_count": 0,
105
106
  "max_retries": 3,
107
+ "max_infra_retries": 3,
108
+ "infra_error_count": 0,
109
+ "last_infra_error_session_id": None,
106
110
  "sessions": [],
107
111
  "last_session_id": None,
108
112
  "resume_from_phase": None,
@@ -268,6 +272,7 @@ def action_get_next(refactor_list_data, state_dir):
268
272
  "priority": chosen.get("priority", "medium"),
269
273
  "complexity": chosen.get("complexity", "medium"),
270
274
  "retry_count": chosen_status_data.get("retry_count", 0),
275
+ "infra_error_count": chosen_status_data.get("infra_error_count", 0),
271
276
  "resume_from_phase": chosen_status_data.get("resume_from_phase", None),
272
277
  }
273
278
  print(json.dumps(result, indent=2, ensure_ascii=False))
@@ -282,6 +287,7 @@ def action_update(args, refactor_list_path, state_dir):
282
287
  session_status = args.session_status
283
288
  session_id = args.session_id
284
289
  max_retries = args.max_retries
290
+ max_infra_retries = args.max_infra_retries
285
291
 
286
292
  if not refactor_id:
287
293
  error_out("--refactor-id is required for 'update' action")
@@ -296,6 +302,8 @@ def action_update(args, refactor_list_path, state_dir):
296
302
  new_status = get_refactor_status_from_list(refactor_list_path, refactor_id)
297
303
 
298
304
  if session_status == "success":
305
+ rs["infra_error_count"] = 0
306
+ rs["last_infra_error_session_id"] = None
299
307
  new_status = "completed"
300
308
  rs["resume_from_phase"] = None
301
309
  err = update_refactor_in_list(refactor_list_path, refactor_id, "completed")
@@ -320,11 +328,25 @@ def action_update(args, refactor_list_path, state_dir):
320
328
  error_out("Failed to update .prizmkit/plans/refactor-list.json: {}".format(err))
321
329
  return
322
330
  elif session_status == "infra_error":
323
- new_status = "pending"
324
- rs["infra_error_count"] = rs.get("infra_error_count", 0) + 1
331
+ # AI CLI/provider outage, auth failure, gateway error, etc.
332
+ # Infra failures do not consume the code retry budget, but they still
333
+ # need their own bounded budget so a flaky provider cannot loop forever.
334
+ infra_error_count = rs.get("infra_error_count", 0) + 1
335
+ rs["infra_error_count"] = infra_error_count
325
336
  rs["last_infra_error_session_id"] = session_id
337
+ rs["max_infra_retries"] = max_infra_retries
338
+ rs["degraded_reason"] = "infra_error"
339
+ if session_id:
340
+ rs["last_session_id"] = session_id
326
341
  rs["resume_from_phase"] = None
327
342
 
343
+ if infra_error_count >= max_infra_retries:
344
+ new_status = "failed"
345
+ if session_id:
346
+ rs["last_failed_session_id"] = session_id
347
+ else:
348
+ new_status = "pending"
349
+
328
350
  err = update_refactor_in_list(refactor_list_path, refactor_id, new_status)
329
351
  if err:
330
352
  error_out("Failed to update .prizmkit/plans/refactor-list.json: {}".format(err))
@@ -379,6 +401,8 @@ def action_update(args, refactor_list_path, state_dir):
379
401
  "session_status": session_status,
380
402
  "new_status": new_status,
381
403
  "retry_count": rs["retry_count"],
404
+ "infra_error_count": rs.get("infra_error_count", 0),
405
+ "max_infra_retries": max_infra_retries,
382
406
  "resume_from_phase": rs.get("resume_from_phase"),
383
407
  "updated_at": rs["updated_at"],
384
408
  }
@@ -728,6 +752,8 @@ def action_reset(args, refactor_list_path, state_dir):
728
752
  old_retry = rs.get("retry_count", 0)
729
753
 
730
754
  rs["retry_count"] = 0
755
+ rs["infra_error_count"] = 0
756
+ rs["last_infra_error_session_id"] = None
731
757
  rs["sessions"] = []
732
758
  rs["last_session_id"] = None
733
759
  rs["resume_from_phase"] = None
@@ -799,6 +825,8 @@ def action_clean(args, refactor_list_path, state_dir):
799
825
  old_retry = rs.get("retry_count", 0)
800
826
 
801
827
  rs["retry_count"] = 0
828
+ rs["infra_error_count"] = 0
829
+ rs["last_infra_error_session_id"] = None
802
830
  rs["sessions"] = []
803
831
  rs["last_session_id"] = None
804
832
  rs["resume_from_phase"] = None
@@ -887,6 +915,8 @@ def action_start(args, refactor_list_path, state_dir):
887
915
  "refactor_id": refactor_id,
888
916
  "old_status": old_status,
889
917
  "new_status": "in_progress",
918
+ "retry_count": rs.get("retry_count", 0),
919
+ "infra_error_count": rs.get("infra_error_count", 0),
890
920
  "updated_at": rs["updated_at"],
891
921
  }
892
922
  print(json.dumps(result, indent=2, ensure_ascii=False))
@@ -1025,6 +1055,8 @@ def action_unskip(args, refactor_list_path, state_dir):
1025
1055
  for rid in to_reset:
1026
1056
  rs = load_refactor_status(state_dir, rid)
1027
1057
  rs["retry_count"] = 0
1058
+ rs["infra_error_count"] = 0
1059
+ rs["last_infra_error_session_id"] = None
1028
1060
  rs["sessions"] = []
1029
1061
  rs["last_session_id"] = None
1030
1062
  rs["resume_from_phase"] = None
@@ -303,7 +303,7 @@ def _run_get_next(fl_path, state_dir):
303
303
  return result.stdout.strip()
304
304
 
305
305
 
306
- def _run_update(fl_path, state_dir, feature_id, session_status, session_id="session-1", max_retries=3):
306
+ def _run_update(fl_path, state_dir, feature_id, session_status, session_id="session-1", max_retries=3, max_infra_retries=3):
307
307
  cmd = [
308
308
  "python3", _SCRIPT,
309
309
  "--feature-list", fl_path,
@@ -312,6 +312,7 @@ def _run_update(fl_path, state_dir, feature_id, session_status, session_id="sess
312
312
  "--session-status", session_status,
313
313
  "--session-id", session_id,
314
314
  "--max-retries", str(max_retries),
315
+ "--max-infra-retries", str(max_infra_retries),
315
316
  "--action", "update",
316
317
  ]
317
318
  result = subprocess.run(cmd, capture_output=True, text=True)
@@ -320,7 +321,7 @@ def _run_update(fl_path, state_dir, feature_id, session_status, session_id="sess
320
321
 
321
322
 
322
323
  class TestInfraErrorUpdate:
323
- def test_infra_error_keeps_pending_without_consuming_retry(self, tmp_path):
324
+ def test_infra_error_below_budget_keeps_pending_without_consuming_retry(self, tmp_path):
324
325
  features = [_make_feature("F-001", "Root", status="in_progress")]
325
326
  fl_path = _write_fl(tmp_path, features)
326
327
  state_dir = _init_state(tmp_path, ["F-001"])
@@ -329,10 +330,20 @@ class TestInfraErrorUpdate:
329
330
  fs["retry_count"] = 2
330
331
  write_json_file(status_path, fs)
331
332
 
332
- result = _run_update(fl_path, state_dir, "F-001", "infra_error", "session-infra", max_retries=3)
333
+ result = _run_update(
334
+ fl_path,
335
+ state_dir,
336
+ "F-001",
337
+ "infra_error",
338
+ "session-infra",
339
+ max_retries=3,
340
+ max_infra_retries=3,
341
+ )
333
342
 
334
343
  assert result["new_status"] == "pending"
335
344
  assert result["retry_count"] == 2
345
+ assert result["infra_error_count"] == 1
346
+ assert result["max_infra_retries"] == 3
336
347
  assert result["restart_policy"] == "infra_retry"
337
348
  assert _read_statuses(fl_path)["F-001"] == "pending"
338
349
 
@@ -340,7 +351,85 @@ class TestInfraErrorUpdate:
340
351
  assert fs["retry_count"] == 2
341
352
  assert fs["infra_error_count"] == 1
342
353
  assert fs["last_infra_error_session_id"] == "session-infra"
354
+ assert fs["max_infra_retries"] == 3
343
355
 
356
+ def test_infra_error_at_budget_marks_failed_without_consuming_code_retry(self, tmp_path):
357
+ features = [_make_feature("F-001", "Root", status="in_progress")]
358
+ fl_path = _write_fl(tmp_path, features)
359
+ state_dir = _init_state(tmp_path, ["F-001"])
360
+ status_path = os.path.join(state_dir, "features", "F-001", "status.json")
361
+ fs = load_feature_status(state_dir, "F-001")
362
+ fs["retry_count"] = 1
363
+ fs["infra_error_count"] = 2
364
+ write_json_file(status_path, fs)
365
+
366
+ result = _run_update(
367
+ fl_path,
368
+ state_dir,
369
+ "F-001",
370
+ "infra_error",
371
+ "session-infra-final",
372
+ max_retries=3,
373
+ max_infra_retries=3,
374
+ )
375
+
376
+ assert result["new_status"] == "failed"
377
+ assert result["retry_count"] == 1
378
+ assert result["infra_error_count"] == 3
379
+ assert result["restart_policy"] == "infra_retry"
380
+ assert _read_statuses(fl_path)["F-001"] == "failed"
381
+
382
+ fs = load_feature_status(state_dir, "F-001")
383
+ assert fs["retry_count"] == 1
384
+ assert fs["infra_error_count"] == 3
385
+ assert fs["last_infra_error_session_id"] == "session-infra-final"
386
+ assert fs["last_failed_session_id"] == "session-infra-final"
387
+
388
+ def test_reset_clears_infra_retry_counters(self, tmp_path):
389
+ features = [_make_feature("F-001", "Root", status="failed")]
390
+ fl_path = _write_fl(tmp_path, features)
391
+ state_dir = _init_state(tmp_path, ["F-001"])
392
+ status_path = os.path.join(state_dir, "features", "F-001", "status.json")
393
+ fs = load_feature_status(state_dir, "F-001")
394
+ fs["retry_count"] = 3
395
+ fs["infra_error_count"] = 3
396
+ fs["last_infra_error_session_id"] = "session-infra-final"
397
+ write_json_file(status_path, fs)
398
+
399
+ cmd = [
400
+ "python3", _SCRIPT,
401
+ "--feature-list", fl_path,
402
+ "--state-dir", state_dir,
403
+ "--feature-id", "F-001",
404
+ "--action", "reset",
405
+ ]
406
+ result = subprocess.run(cmd, capture_output=True, text=True)
407
+ assert result.returncode == 0, result.stderr
408
+
409
+ fs = load_feature_status(state_dir, "F-001")
410
+ assert fs["retry_count"] == 0
411
+ assert fs["infra_error_count"] == 0
412
+ assert fs["last_infra_error_session_id"] is None
413
+ assert _read_statuses(fl_path)["F-001"] == "pending"
414
+
415
+ def test_unskip_clears_infra_retry_counters(self, tmp_path):
416
+ features = [_make_feature("F-001", "Root", status="failed")]
417
+ fl_path = _write_fl(tmp_path, features)
418
+ state_dir = _init_state(tmp_path, ["F-001"])
419
+ status_path = os.path.join(state_dir, "features", "F-001", "status.json")
420
+ fs = load_feature_status(state_dir, "F-001")
421
+ fs["retry_count"] = 3
422
+ fs["infra_error_count"] = 3
423
+ fs["last_infra_error_session_id"] = "session-infra-final"
424
+ write_json_file(status_path, fs)
425
+
426
+ _run_unskip(fl_path, state_dir, "F-001")
427
+
428
+ fs = load_feature_status(state_dir, "F-001")
429
+ assert fs["retry_count"] == 0
430
+ assert fs["infra_error_count"] == 0
431
+ assert fs["last_infra_error_session_id"] is None
432
+ assert _read_statuses(fl_path)["F-001"] == "pending"
344
433
 
345
434
  class TestUnskipByFeatureId:
346
435
  """Unskip with --feature-id targets a specific failed feature + downstream."""
@@ -1,6 +1,20 @@
1
1
  Set-StrictMode -Version 2.0
2
2
  $ErrorActionPreference = 'Stop'
3
3
 
4
+ function Get-PrizmGitStatusSafe {
5
+ param([string]$ProjectRoot)
6
+ $hiddenToolWorktreeExcludes = Get-PrizmHiddenToolWorktreeExcludes
7
+ $lines = & git -C $ProjectRoot status --porcelain -- . @hiddenToolWorktreeExcludes 2>$null
8
+ return @($lines)
9
+ }
10
+
11
+ function Add-PrizmGitAllSafe {
12
+ param([string]$ProjectRoot)
13
+ $hiddenToolWorktreeExcludes = Get-PrizmHiddenToolWorktreeExcludes
14
+ & git -C $ProjectRoot add -A -- . @hiddenToolWorktreeExcludes *> $null
15
+ return $LASTEXITCODE -eq 0
16
+ }
17
+
4
18
  function Get-PrizmCurrentBranch {
5
19
  param([string]$ProjectRoot)
6
20
  $branch = & git -C $ProjectRoot rev-parse --abbrev-ref HEAD 2>$null
@@ -76,13 +90,12 @@ function Save-PrizmBranchWip {
76
90
  $currentBranch = Get-PrizmCurrentBranch $ProjectRoot
77
91
  if ($currentBranch -ne $DevBranch) { return $true }
78
92
 
79
- $changes = & git -C $ProjectRoot status --porcelain 2>$null
93
+ $changes = Get-PrizmGitStatusSafe $ProjectRoot
80
94
  if ([string]::IsNullOrWhiteSpace(($changes -join "`n"))) { return $true }
81
95
 
82
96
  Write-PrizmWarn "Saving uncommitted work-in-progress on branch: $DevBranch"
83
- & git -C $ProjectRoot add -A *> $null
84
- if ($LASTEXITCODE -ne 0) {
85
- Write-PrizmWarn "git add -A failed - uncommitted work may remain on $DevBranch"
97
+ if (-not (Add-PrizmGitAllSafe $ProjectRoot)) {
98
+ Write-PrizmWarn "git add failed - uncommitted work may remain on $DevBranch"
86
99
  return $true
87
100
  }
88
101
 
@@ -51,6 +51,15 @@ function Write-PrizmWarn { param([string]$Message) Write-Host "[WARN] $(Get-D
51
51
  function Write-PrizmError { param([string]$Message) Write-Host "[ERROR] $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') $Message" -ForegroundColor Red }
52
52
  function Write-PrizmSuccess { param([string]$Message) Write-Host "[SUCCESS] $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') $Message" -ForegroundColor Green }
53
53
 
54
+ function Get-PrizmHiddenToolWorktreeExcludes {
55
+ return @(
56
+ ':(top,exclude,glob).*/worktree',
57
+ ':(top,exclude,glob).*/worktree/**',
58
+ ':(top,exclude,glob).*/worktrees',
59
+ ':(top,exclude,glob).*/worktrees/**'
60
+ )
61
+ }
62
+
54
63
  function Initialize-PrizmPaths {
55
64
  param([string]$ScriptRoot)
56
65
  $pipelineDir = Resolve-Path $ScriptRoot
@@ -467,13 +476,14 @@ function Get-PrizmEffectiveStaleKillThreshold {
467
476
  return [Math]::Max($BaseThreshold * 4, 3600)
468
477
  }
469
478
 
470
- function Get-PrizmProgressChildActivity {
479
+ function Get-PrizmProgressActivity {
471
480
  param([string]$ProgressFile)
472
481
 
473
482
  $empty = [pscustomobject]@{
474
- Signature = ''
475
- TotalBytes = 0
476
- SessionCount = 0
483
+ ChildSignature = ''
484
+ ChildTotalBytes = 0
485
+ ChildSessionCount = 0
486
+ ProgressSignature = ''
477
487
  }
478
488
  if (-not (Test-Path $ProgressFile)) { return $empty }
479
489
 
@@ -483,25 +493,72 @@ function Get-PrizmProgressChildActivity {
483
493
  return $empty
484
494
  }
485
495
 
486
- $signature = ''
496
+ $childSignature = ''
487
497
  if ($progress.PSObject.Properties['child_activity_signature'] -and $progress.child_activity_signature) {
488
- $signature = [string]$progress.child_activity_signature
498
+ $childSignature = [string]$progress.child_activity_signature
489
499
  }
490
500
 
491
- $totalBytes = [int64]0
501
+ $childTotalBytes = [int64]0
492
502
  if ($progress.PSObject.Properties['child_total_bytes']) {
493
- [int64]::TryParse([string]$progress.child_total_bytes, [ref]$totalBytes) | Out-Null
503
+ [int64]::TryParse([string]$progress.child_total_bytes, [ref]$childTotalBytes) | Out-Null
494
504
  }
495
505
 
496
- $sessionCount = 0
506
+ $childSessionCount = 0
497
507
  if ($progress.PSObject.Properties['child_session_files'] -and $progress.child_session_files) {
498
- $sessionCount = @($progress.child_session_files).Count
508
+ $childSessionCount = @($progress.child_session_files).Count
509
+ }
510
+
511
+ $messageCount = 0
512
+ if ($progress.PSObject.Properties['message_count']) {
513
+ [int]::TryParse([string]$progress.message_count, [ref]$messageCount) | Out-Null
514
+ }
515
+ $totalToolCalls = 0
516
+ if ($progress.PSObject.Properties['total_tool_calls']) {
517
+ [int]::TryParse([string]$progress.total_tool_calls, [ref]$totalToolCalls) | Out-Null
518
+ }
519
+ $activeSubagentCount = 0
520
+ if ($progress.PSObject.Properties['active_subagent_count']) {
521
+ [int]::TryParse([string]$progress.active_subagent_count, [ref]$activeSubagentCount) | Out-Null
522
+ }
523
+ $subagentSpawnCount = 0
524
+ if ($progress.PSObject.Properties['subagent_spawn_count']) {
525
+ [int]::TryParse([string]$progress.subagent_spawn_count, [ref]$subagentSpawnCount) | Out-Null
526
+ }
527
+ $currentPhase = if ($progress.PSObject.Properties['current_phase'] -and $progress.current_phase) { [string]$progress.current_phase } else { '' }
528
+ $currentTool = if ($progress.PSObject.Properties['current_tool'] -and $progress.current_tool) { [string]$progress.current_tool } else { '' }
529
+ $fatalErrorCode = if ($progress.PSObject.Properties['fatal_error_code'] -and $progress.fatal_error_code) { [string]$progress.fatal_error_code } else { '' }
530
+ $terminalResultText = if ($progress.PSObject.Properties['terminal_result_text'] -and $progress.terminal_result_text) { [string]$progress.terminal_result_text } else { '' }
531
+
532
+ $progressSignature = @(
533
+ $currentPhase,
534
+ $currentTool,
535
+ $messageCount,
536
+ $totalToolCalls,
537
+ $activeSubagentCount,
538
+ $subagentSpawnCount,
539
+ $childSignature,
540
+ $childTotalBytes,
541
+ $childSessionCount,
542
+ $fatalErrorCode,
543
+ $terminalResultText
544
+ ) | ConvertTo-Json -Compress
545
+
546
+ return [pscustomobject]@{
547
+ ChildSignature = $childSignature
548
+ ChildTotalBytes = $childTotalBytes
549
+ ChildSessionCount = $childSessionCount
550
+ ProgressSignature = $progressSignature
499
551
  }
552
+ }
553
+
554
+ function Get-PrizmProgressChildActivity {
555
+ param([string]$ProgressFile)
500
556
 
557
+ $activity = Get-PrizmProgressActivity -ProgressFile $ProgressFile
501
558
  return [pscustomobject]@{
502
- Signature = $signature
503
- TotalBytes = $totalBytes
504
- SessionCount = $sessionCount
559
+ Signature = $activity.ChildSignature
560
+ TotalBytes = $activity.ChildTotalBytes
561
+ SessionCount = $activity.ChildSessionCount
505
562
  }
506
563
  }
507
564