prizmkit 1.1.119 → 1.1.120

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 (62) hide show
  1. package/bin/create-prizmkit.js +1 -5
  2. package/bundled/VERSION.json +3 -3
  3. package/bundled/dev-pipeline/README.md +41 -38
  4. package/bundled/dev-pipeline/assets/skill-subagent-integration.md +76 -0
  5. package/bundled/dev-pipeline/prizmkit_runtime/config.py +2 -8
  6. package/bundled/dev-pipeline/prizmkit_runtime/interoperability.py +1 -1
  7. package/bundled/dev-pipeline/prizmkit_runtime/platform_detection.py +3 -3
  8. package/bundled/dev-pipeline/prizmkit_runtime/runners.py +45 -3
  9. package/bundled/dev-pipeline/prizmkit_runtime/sessions.py +28 -6
  10. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +60 -102
  11. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +1 -19
  12. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +1 -19
  13. package/bundled/dev-pipeline/scripts/{init-dev-team.py → init-change-artifact.py} +6 -16
  14. package/bundled/dev-pipeline/scripts/parse-stream-progress.py +364 -76
  15. package/bundled/dev-pipeline/scripts/utils.py +1 -1
  16. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +2 -2
  17. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +7 -9
  18. package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +2 -2
  19. package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +1 -1
  20. package/bundled/dev-pipeline/templates/sections/critical-paths-agent.md +0 -1
  21. package/bundled/dev-pipeline/templates/sections/critical-paths-full.md +0 -2
  22. package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +8 -1
  23. package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +6 -7
  24. package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +6 -7
  25. package/bundled/dev-pipeline/templates/sections/phase-implement-lite.md +4 -2
  26. package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +2 -2
  27. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +4 -2
  28. package/bundled/dev-pipeline/templates/sections/phase-specify-plan-full.md +20 -0
  29. package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +2 -2
  30. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +204 -141
  31. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +16 -10
  32. package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +2 -13
  33. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +13 -14
  34. package/bundled/dev-pipeline/tests/test_unified_cli.py +205 -6
  35. package/bundled/skills/_metadata.json +1 -1
  36. package/bundled/skills/app-planner/SKILL.md +2 -2
  37. package/bundled/skills/app-planner/references/architecture-decisions.md +1 -3
  38. package/bundled/skills/prizmkit-code-review/SKILL.md +16 -14
  39. package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +9 -9
  40. package/bundled/skills/prizmkit-implement/SKILL.md +21 -1
  41. package/bundled/skills/prizmkit-implement/references/implementation-subagent-procedure.md +67 -0
  42. package/package.json +1 -1
  43. package/src/clean.js +12 -8
  44. package/src/config.js +24 -42
  45. package/src/index.js +1 -10
  46. package/src/manifest.js +3 -9
  47. package/src/metadata.js +0 -26
  48. package/src/prompts.js +0 -13
  49. package/src/scaffold.js +76 -201
  50. package/src/upgrade.js +16 -33
  51. package/bundled/adapters/claude/agent-adapter.js +0 -96
  52. package/bundled/adapters/claude/team-adapter.js +0 -183
  53. package/bundled/adapters/codebuddy/agent-adapter.js +0 -42
  54. package/bundled/adapters/codebuddy/team-adapter.js +0 -46
  55. package/bundled/adapters/codex/agent-adapter.js +0 -38
  56. package/bundled/adapters/codex/team-adapter.js +0 -37
  57. package/bundled/agents/prizm-dev-team-dev.md +0 -123
  58. package/bundled/agents/prizm-dev-team-reviewer.md +0 -113
  59. package/bundled/dev-pipeline/assets/prizm-dev-team-integration.md +0 -65
  60. package/bundled/dev-pipeline/templates/bootstrap-tier1.md +0 -436
  61. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +0 -510
  62. package/bundled/team/prizm-dev-team.json +0 -27
@@ -15,6 +15,7 @@ The script runs until:
15
15
  """
16
16
 
17
17
  import argparse
18
+ import hashlib
18
19
  import json
19
20
  import os
20
21
  import re
@@ -169,6 +170,34 @@ def detect_api_error_code(text, require_error_context=False, structured_code=Fal
169
170
  return ""
170
171
 
171
172
 
173
+ TERMINAL_SUBAGENT_STATUSES = {
174
+ "completed",
175
+ "failed",
176
+ "cancelled",
177
+ "canceled",
178
+ "killed",
179
+ "stopped",
180
+ "success",
181
+ "error",
182
+ }
183
+
184
+
185
+ def _stable_subagent_digest(*parts):
186
+ """Return a short stable digest for provider events without durable ids."""
187
+ canonical = json.dumps(parts, ensure_ascii=False, sort_keys=True, default=str)
188
+ return hashlib.sha1(canonical.encode("utf-8")).hexdigest()[:16]
189
+
190
+
191
+ def _normalize_status(status, default="unknown"):
192
+ """Normalize provider status values while preserving provider-specific names."""
193
+ text = str(status or "").strip().lower()
194
+ if not text:
195
+ return default
196
+ if text == "cancelled":
197
+ return "canceled"
198
+ return text
199
+
200
+
172
201
  class ProgressTracker:
173
202
  """Tracks progress state from stream-json events."""
174
203
 
@@ -195,6 +224,11 @@ class ProgressTracker:
195
224
  self.active_subagent_count = 0
196
225
  self.subagent_status_counts = Counter()
197
226
  self._subagent_spawn_count = 0
227
+ self.subagent_records = {}
228
+ self.subagent_aliases = {}
229
+ self.subagent_source_providers = set()
230
+ self.subagent_source_events = Counter()
231
+ self.parent_subagent_activity_ids = set()
198
232
  self.codex_child_thread_ids = set()
199
233
  self.cb_session_id = ""
200
234
  self.result_subtype = ""
@@ -219,6 +253,7 @@ class ProgressTracker:
219
253
  self._text_buffer = ""
220
254
  self._in_tool_use = False
221
255
  self._current_tool_input_parts = []
256
+ self._current_tool_id = ""
222
257
 
223
258
  def process_event(self, event):
224
259
  """Process a single stream-json event and update state.
@@ -236,6 +271,22 @@ class ProgressTracker:
236
271
  self.event_type_counts[str(event_type)] += 1
237
272
  self._detect_structured_terminal_fields(event)
238
273
  self._record_usage_and_permission_metadata(event)
274
+ parent_tool_use_id = event.get("parent_tool_use_id")
275
+ if isinstance(parent_tool_use_id, str) and parent_tool_use_id.strip():
276
+ parent_tool_use_id = parent_tool_use_id.strip()
277
+ self.parent_subagent_activity_ids.add(parent_tool_use_id)
278
+ self._record_subagent_event(
279
+ "claude",
280
+ "parent_tool_use_id",
281
+ identifier=parent_tool_use_id,
282
+ aliases=[parent_tool_use_id],
283
+ status="running",
284
+ active=True,
285
+ spawn=False,
286
+ metadata={"parent_tool_use_id": parent_tool_use_id},
287
+ )
288
+
289
+ self._record_structured_subagent_metadata(event)
239
290
 
240
291
  # ── Codex exec --json JSONL format ──────────────────────────
241
292
  if event_type in (
@@ -260,9 +311,6 @@ class ProgressTracker:
260
311
 
261
312
  elif item_type == "collab_tool_call":
262
313
  tool_name = item.get("tool", "collab")
263
- self._record_codex_child_thread_ids(
264
- item.get("receiver_thread_ids")
265
- )
266
314
  if event_type == "item.started":
267
315
  self.current_tool = tool_name
268
316
  self.tool_call_counts[tool_name] += 1
@@ -270,9 +318,6 @@ class ProgressTracker:
270
318
  elif item.get("status") == "completed":
271
319
  self.current_tool = None
272
320
  self._extract_tool_summary_from_dict(item)
273
- self._update_subagent_status_counts(
274
- item.get("agents_states", {})
275
- )
276
321
 
277
322
  prompt = item.get("prompt")
278
323
  if prompt:
@@ -346,8 +391,13 @@ class ProgressTracker:
346
391
  json.dumps(tool_input, ensure_ascii=False)[:500]
347
392
  )
348
393
  # Track CodeBuddy Agent/Task tool invocations as
349
- # potential sub-agent spawns (Gap 3 fix).
350
- self._record_cb_agent_tool_call(tool_name, tool_input)
394
+ # session-log authoritative sub-agent spawns.
395
+ self._record_cb_agent_tool_call(
396
+ tool_name,
397
+ tool_input,
398
+ event_id=str(block.get("id") or block.get("tool_use_id") or block.get("call_id") or ""),
399
+ source_event="message.tool_use",
400
+ )
351
401
  elif block_type == "text":
352
402
  text = block.get("text", "")
353
403
  if text.strip():
@@ -378,8 +428,13 @@ class ProgressTracker:
378
428
  if isinstance(raw_args, str) and raw_args.strip():
379
429
  self._extract_tool_summary(raw_args)
380
430
  self._detect_phase(raw_args[:500])
381
- # Track Agent/Task tool invocations as sub-agent spawns
382
- self._record_cb_agent_tool_call(tool_name, raw_args)
431
+ # Track Agent/Task tool invocations as session-log sub-agent spawns.
432
+ self._record_cb_agent_tool_call(
433
+ tool_name,
434
+ raw_args,
435
+ event_id=str(event.get("id") or event.get("call_id") or event.get("callId") or ""),
436
+ source_event="function_call",
437
+ )
383
438
  elif event_type == "function_call_result":
384
439
  self.current_tool = None
385
440
  # Check result text for phase / error hints
@@ -411,6 +466,13 @@ class ProgressTracker:
411
466
  if isinstance(tool_input, dict):
412
467
  self._extract_tool_summary_from_dict(tool_input)
413
468
  self._detect_phase(json.dumps(tool_input, ensure_ascii=False)[:500])
469
+ self._record_agent_tool_call(
470
+ "claude",
471
+ tool_name,
472
+ tool_input,
473
+ source_event="assistant.tool_use",
474
+ event_id=str(block.get("id") or block.get("tool_use_id") or ""),
475
+ )
414
476
  elif block_type == "text":
415
477
  text = block.get("text", "")
416
478
  if text.strip():
@@ -572,6 +634,7 @@ class ProgressTracker:
572
634
  self.current_tool_input_summary = ""
573
635
  self._in_tool_use = False
574
636
  self._current_tool_input_parts = []
637
+ self._current_tool_id = ""
575
638
 
576
639
  elif event_type == "content_block_start":
577
640
  content_block = event.get("content_block", {})
@@ -585,10 +648,12 @@ class ProgressTracker:
585
648
  self.total_tool_calls += 1
586
649
  self._in_tool_use = True
587
650
  self._current_tool_input_parts = []
651
+ self._current_tool_id = str(content_block.get("id") or content_block.get("tool_use_id") or "")
588
652
 
589
653
  elif block_type == "text":
590
654
  self._text_buffer = ""
591
655
  self._in_tool_use = False
656
+ self._current_tool_id = ""
592
657
 
593
658
  elif event_type == "content_block_delta":
594
659
  delta = event.get("delta", {})
@@ -618,6 +683,13 @@ class ProgressTracker:
618
683
  full_input = "".join(self._current_tool_input_parts)
619
684
  self._extract_tool_summary(full_input)
620
685
  self._detect_phase(full_input)
686
+ self._record_agent_tool_call(
687
+ "claude",
688
+ self.current_tool,
689
+ full_input,
690
+ source_event="content_block.tool_use",
691
+ event_id=self._current_tool_id,
692
+ )
621
693
  else:
622
694
  # Text block finished - detect phase and terminal errors from accumulated text
623
695
  if self._text_buffer:
@@ -628,6 +700,7 @@ class ProgressTracker:
628
700
  )
629
701
  self._in_tool_use = False
630
702
  self._current_tool_input_parts = []
703
+ self._current_tool_id = ""
631
704
 
632
705
  elif event_type == "error":
633
706
  self.event_format = self.event_format or "stream-json"
@@ -644,10 +717,173 @@ class ProgressTracker:
644
717
 
645
718
  # Check for subagent indicator
646
719
  if event.get("parent_tool_use_id"):
647
- # This is a sub-agent event; tool name is still tracked normally
720
+ # Already recorded as child activity metadata near the top of process_event.
648
721
  pass
649
722
 
650
723
 
724
+ def _canonical_subagent_key(self, provider, identifier, aliases=()):
725
+ """Return the existing canonical key for any alias, otherwise a new key."""
726
+ provider = str(provider or "unknown").strip() or "unknown"
727
+ keys = []
728
+ if identifier:
729
+ keys.append(f"{provider}:{identifier}")
730
+ for alias in aliases or ():
731
+ if alias:
732
+ alias_text = str(alias).strip()
733
+ if alias_text:
734
+ keys.append(f"{provider}:{alias_text}")
735
+ for key in keys:
736
+ canonical = self.subagent_aliases.get(key)
737
+ if canonical:
738
+ return canonical
739
+ return keys[0] if keys else f"{provider}:unknown:{len(self.subagent_records) + 1}"
740
+
741
+ def _record_subagent_event(
742
+ self,
743
+ provider,
744
+ source_event,
745
+ *,
746
+ identifier="",
747
+ aliases=(),
748
+ status="unknown",
749
+ active=None,
750
+ spawn=False,
751
+ metadata=None,
752
+ ):
753
+ """Record authoritative session-log-derived subagent statistics."""
754
+ provider = str(provider or "unknown").strip() or "unknown"
755
+ source_event = str(source_event or "unknown").strip() or "unknown"
756
+ identifier = str(identifier or "").strip()
757
+ alias_values = [str(alias).strip() for alias in aliases or () if str(alias or "").strip()]
758
+ canonical = self._canonical_subagent_key(provider, identifier, alias_values)
759
+ record = self.subagent_records.setdefault(
760
+ canonical,
761
+ {
762
+ "id": identifier or canonical.split(":", 1)[-1],
763
+ "provider": provider,
764
+ "source_events": set(),
765
+ "aliases": set(),
766
+ "status": "unknown",
767
+ "active": False,
768
+ "spawned": False,
769
+ "metadata": {},
770
+ },
771
+ )
772
+ for alias in [identifier, *alias_values]:
773
+ if alias:
774
+ alias_key = f"{provider}:{alias}"
775
+ self.subagent_aliases[alias_key] = canonical
776
+ record["aliases"].add(alias)
777
+ if identifier:
778
+ if not record.get("id") or str(record.get("id", "")).startswith("unknown:"):
779
+ record["id"] = identifier
780
+ record.setdefault("observed_ids", set()).add(identifier)
781
+ for alias in alias_values:
782
+ record.setdefault("observed_ids", set()).add(alias)
783
+ record["source_events"].add(source_event)
784
+ self.subagent_source_providers.add(provider)
785
+ self.subagent_source_events[f"{provider}:{source_event}"] += 1
786
+ existing_status = _normalize_status(record.get("status"), default="unknown")
787
+ normalized_status = _normalize_status(status, default=existing_status)
788
+ if (
789
+ source_event == "parent_tool_use_id"
790
+ and existing_status in TERMINAL_SUBAGENT_STATUSES
791
+ ):
792
+ normalized_status = existing_status
793
+ active = False
794
+ if normalized_status:
795
+ record["status"] = normalized_status
796
+ if active is None:
797
+ record["active"] = record["status"] not in TERMINAL_SUBAGENT_STATUSES
798
+ else:
799
+ record["active"] = bool(active)
800
+ if spawn:
801
+ record["spawned"] = True
802
+ if isinstance(metadata, dict):
803
+ for key, value in metadata.items():
804
+ if value not in (None, "", [], {}):
805
+ record["metadata"][str(key)] = str(value)[:200]
806
+ self._refresh_normalized_subagent_totals()
807
+ return canonical
808
+
809
+ def _refresh_normalized_subagent_totals(self):
810
+ """Mirror normalized records into legacy progress fields."""
811
+ counts = Counter()
812
+ active = 0
813
+ spawned = 0
814
+ for record in self.subagent_records.values():
815
+ status = _normalize_status(record.get("status"), default="unknown")
816
+ counts[status] += 1
817
+ if bool(record.get("active")):
818
+ active += 1
819
+ if bool(record.get("spawned")):
820
+ spawned += 1
821
+ self.subagent_status_counts = counts
822
+ self.active_subagent_count = active
823
+ self._subagent_spawn_count = spawned
824
+
825
+ def _subagent_statistics(self):
826
+ """Return additive normalized subagent statistics for progress.json."""
827
+ records = []
828
+ for key, record in sorted(self.subagent_records.items()):
829
+ records.append(
830
+ {
831
+ "key": key,
832
+ "id": str(record.get("id") or ""),
833
+ "provider": str(record.get("provider") or "unknown"),
834
+ "status": _normalize_status(record.get("status"), default="unknown"),
835
+ "active": bool(record.get("active")),
836
+ "spawned": bool(record.get("spawned")),
837
+ "source_events": sorted(record.get("source_events") or []),
838
+ "aliases": sorted(record.get("aliases") or []),
839
+ "observed_ids": sorted(record.get("observed_ids") or []),
840
+ "metadata": dict(record.get("metadata") or {}),
841
+ }
842
+ )
843
+ observed_ids = []
844
+ for record in records:
845
+ identifier = record.get("id") or record.get("key")
846
+ if identifier:
847
+ observed_ids.append(str(identifier))
848
+ for observed in record.get("observed_ids") or []:
849
+ observed_ids.append(str(observed))
850
+ states = [
851
+ {"status": status, "count": count}
852
+ for status, count in self.subagent_status_counts.most_common()
853
+ ]
854
+ return {
855
+ "source": "session_log",
856
+ "providers": sorted(self.subagent_source_providers),
857
+ "spawned": self._subagent_spawn_count,
858
+ "active": self.active_subagent_count,
859
+ "status_counts": dict(self.subagent_status_counts),
860
+ "states": states,
861
+ "observed_ids": sorted(set(observed_ids)),
862
+ "source_events": dict(sorted(self.subagent_source_events.items())),
863
+ "parent_activity_ids": sorted(self.parent_subagent_activity_ids),
864
+ "records": records,
865
+ "child_activity_source": "filesystem_liveness_only",
866
+ }
867
+
868
+ def _record_structured_subagent_metadata(self, event):
869
+ """Extract provider-specific subagent metadata when fields appear anywhere."""
870
+ if not isinstance(event, dict):
871
+ return
872
+ receiver_ids = event.get("receiver_thread_ids")
873
+ if isinstance(receiver_ids, list):
874
+ self._record_codex_child_thread_ids(receiver_ids)
875
+ agents_states = event.get("agents_states")
876
+ if isinstance(agents_states, dict):
877
+ self._update_subagent_status_counts(agents_states)
878
+ item = event.get("item")
879
+ if isinstance(item, dict):
880
+ nested_receivers = item.get("receiver_thread_ids")
881
+ if isinstance(nested_receivers, list):
882
+ self._record_codex_child_thread_ids(nested_receivers)
883
+ nested_states = item.get("agents_states")
884
+ if isinstance(nested_states, dict):
885
+ self._update_subagent_status_counts(nested_states)
886
+
651
887
  def _record_usage_and_permission_metadata(self, event):
652
888
  """Record additive metadata required by Python runtime parity tests."""
653
889
  if not isinstance(event, dict):
@@ -740,42 +976,57 @@ class ProgressTracker:
740
976
  if not self.errors or self.errors[-1] != normalized_code:
741
977
  self.errors.append(normalized_code)
742
978
 
743
- def _record_cb_agent_tool_call(self, tool_name, raw_args):
744
- """Record a CodeBuddy Agent/Task* tool invocation for sub-agent tracking.
745
-
746
- CodeBuddy spawns sub-agents via:
747
- - Agent(prompt=..., run_in_background=True/False) — synchronous or bg fork
748
- - TaskCreate(subject=..., description=...) — scheduled task
749
-
750
- (TaskUpdate/TaskOutput exist but are lifecycle-only — they track
751
- existing tasks rather than creating new sub-agents, so we don't
752
- count them as spawns.)
753
-
754
- We don't get child session ids from these tool calls in the stream,
755
- but recording the count lets the heartbeat code in heartbeat.sh apply
756
- an extended stale-kill window just as it does for Codex spawn_agent.
757
- """
979
+ def _record_agent_tool_call(self, provider, tool_name, raw_args, *, source_event="tool_use", event_id=""):
980
+ """Record Agent/TaskCreate tool calls as session-log subagent spawns."""
758
981
  if tool_name not in ("Agent", "TaskCreate"):
759
982
  return
760
- # Both dict (from message events) and str/JSON (from function_call events)
761
- # are supported.
762
983
  if isinstance(raw_args, dict):
763
984
  args = raw_args
764
985
  elif isinstance(raw_args, str):
765
986
  try:
766
987
  args = json.loads(raw_args)
767
988
  except (json.JSONDecodeError, TypeError):
768
- return
989
+ args = {"raw_args": raw_args}
769
990
  else:
991
+ args = {}
992
+ if tool_name == "Agent" and not any(args.get(key) for key in ("subagent_type", "prompt", "description")):
770
993
  return
771
- # For Agent tool, subagent_type or run_in_background hints at delegation
772
- if tool_name == "Agent":
773
- subagent_type = args.get("subagent_type", "")
774
- prompt = args.get("prompt", "")
775
- if subagent_type or prompt:
776
- self._subagent_spawn_count += 1
777
- elif tool_name == "TaskCreate":
778
- self._subagent_spawn_count += 1
994
+ explicit_ids = []
995
+ for key in ("id", "tool_use_id", "call_id", "callId", "task_id", "taskId"):
996
+ value = args.get(key) if isinstance(args, dict) else None
997
+ if value:
998
+ explicit_ids.append(str(value))
999
+ if event_id:
1000
+ explicit_ids.append(str(event_id))
1001
+ fallback = _stable_subagent_digest(provider, tool_name, args)
1002
+ identifier = explicit_ids[0] if explicit_ids else f"{tool_name}:{fallback}"
1003
+ aliases = [*explicit_ids, f"{tool_name}:{fallback}"]
1004
+ metadata = {
1005
+ "tool": tool_name,
1006
+ "subagent_type": args.get("subagent_type", "") if isinstance(args, dict) else "",
1007
+ "description": args.get("description", "") if isinstance(args, dict) else "",
1008
+ "run_in_background": args.get("run_in_background", "") if isinstance(args, dict) else "",
1009
+ }
1010
+ self._record_subagent_event(
1011
+ provider,
1012
+ source_event,
1013
+ identifier=identifier,
1014
+ aliases=aliases,
1015
+ status="running",
1016
+ active=True,
1017
+ spawn=True,
1018
+ metadata=metadata,
1019
+ )
1020
+
1021
+ def _record_cb_agent_tool_call(self, tool_name, raw_args, *, event_id="", source_event="tool_use"):
1022
+ """Record a CodeBuddy Agent/TaskCreate tool invocation for sub-agent tracking."""
1023
+ self._record_agent_tool_call(
1024
+ "codebuddy",
1025
+ tool_name,
1026
+ raw_args,
1027
+ source_event=source_event,
1028
+ event_id=event_id,
1029
+ )
779
1030
 
780
1031
  def _record_terminal_result(self, text="", is_error=False, api_error_status=None, api_error_code="", stop_reason=""):
781
1032
  """Record a Claude Code terminal result event."""
@@ -889,31 +1140,52 @@ class ProgressTracker:
889
1140
  self.current_tool_input_summary = str(data["prompt"])[:100]
890
1141
 
891
1142
  def _update_subagent_status_counts(self, agents_states):
892
- """Track Codex subagent state counts from collab_tool_call items."""
893
- counts = Counter()
894
- active = 0
895
- if isinstance(agents_states, dict):
896
- for state in agents_states.values():
897
- if not isinstance(state, dict):
898
- continue
899
- status = str(state.get("status", "unknown"))
900
- counts[status] += 1
901
- if status not in ("completed", "failed", "cancelled", "canceled"):
902
- active += 1
903
- message = state.get("message")
904
- if message:
905
- self.last_text_snippet = str(message).strip()[:120]
906
- self._detect_phase(str(message))
907
- self.subagent_status_counts = counts
908
- self.active_subagent_count = active
1143
+ """Track Codex subagent state counts from session-log collab items."""
1144
+ if not isinstance(agents_states, dict):
1145
+ return
1146
+ for agent_id, state in agents_states.items():
1147
+ if not isinstance(state, dict):
1148
+ continue
1149
+ status = _normalize_status(state.get("status"), default="unknown")
1150
+ message = state.get("message")
1151
+ if message:
1152
+ self.last_text_snippet = str(message).strip()[:120]
1153
+ self._detect_phase(str(message))
1154
+ identifier = str(state.get("thread_id") or state.get("id") or agent_id or "")
1155
+ aliases = [agent_id, state.get("thread_id", ""), state.get("id", "")]
1156
+ self._record_subagent_event(
1157
+ "codex",
1158
+ "agents_states",
1159
+ identifier=identifier,
1160
+ aliases=aliases,
1161
+ status=status,
1162
+ active=status not in TERMINAL_SUBAGENT_STATUSES,
1163
+ spawn=True,
1164
+ metadata={
1165
+ "message": message or "",
1166
+ "name": state.get("name", ""),
1167
+ "role": state.get("role", ""),
1168
+ },
1169
+ )
909
1170
 
910
1171
  def _record_codex_child_thread_ids(self, thread_ids):
911
- """Remember Codex child thread IDs reported by collab tool calls."""
1172
+ """Remember Codex child thread IDs reported by session-log collab tool calls."""
912
1173
  if not isinstance(thread_ids, list):
913
1174
  return
914
1175
  for thread_id in thread_ids:
915
1176
  if isinstance(thread_id, str) and thread_id.strip():
916
- self.codex_child_thread_ids.add(thread_id.strip())
1177
+ thread_id = thread_id.strip()
1178
+ self.codex_child_thread_ids.add(thread_id)
1179
+ self._record_subagent_event(
1180
+ "codex",
1181
+ "receiver_thread_ids",
1182
+ identifier=thread_id,
1183
+ aliases=[thread_id],
1184
+ status="running",
1185
+ active=True,
1186
+ spawn=True,
1187
+ metadata={"thread_id": thread_id},
1188
+ )
917
1189
 
918
1190
  def _codex_sessions_dir(self):
919
1191
  """Return the Codex sessions directory for the current environment."""
@@ -970,32 +1242,30 @@ class ProgressTracker:
970
1242
  )
971
1243
 
972
1244
  def _update_claude_subagent_status_counts(self):
973
- """Track Claude Code in-process teammate task state counts."""
974
- counts = Counter()
975
- active = 0
976
- inactive_statuses = {
977
- "completed",
978
- "failed",
979
- "cancelled",
980
- "canceled",
981
- "killed",
982
- "stopped",
983
- "success",
984
- "error",
985
- }
986
- for state in self.claude_task_states.values():
1245
+ """Track Claude Code in-process teammate task state counts from session.log."""
1246
+ for task_id, state in self.claude_task_states.items():
987
1247
  if not self._is_tracked_claude_subagent_state(state):
988
1248
  continue
989
- status = str(state.get("status") or "unknown")
990
- counts[status] += 1
991
- if status.lower() not in inactive_statuses:
992
- active += 1
1249
+ status = _normalize_status(state.get("status"), default="unknown")
993
1250
  summary = state.get("summary") or state.get("subagent_type")
994
1251
  if summary:
995
1252
  self.last_text_snippet = str(summary).strip()[:120]
996
1253
  self._detect_phase(str(summary))
997
- self.subagent_status_counts = counts
998
- self.active_subagent_count = active
1254
+ self._record_subagent_event(
1255
+ "claude",
1256
+ "system.task",
1257
+ identifier=str(task_id),
1258
+ aliases=[state.get("tool_use_id", "")],
1259
+ status=status,
1260
+ active=status not in TERMINAL_SUBAGENT_STATUSES,
1261
+ spawn=True,
1262
+ metadata={
1263
+ "task_type": state.get("task_type", ""),
1264
+ "subagent_type": state.get("subagent_type", ""),
1265
+ "summary": state.get("summary", ""),
1266
+ "tool_use_id": state.get("tool_use_id", ""),
1267
+ },
1268
+ )
999
1269
 
1000
1270
  def _claude_projects_dir(self):
1001
1271
  """Return the Claude Code projects directory for transcript lookup."""
@@ -1162,6 +1432,9 @@ class ProgressTracker:
1162
1432
  self._cb_child_session_files = self._find_cb_child_session_files()
1163
1433
  self._last_child_scan_at = now
1164
1434
 
1435
+ # Filesystem scanning below is intentionally liveness-only. The
1436
+ # session-log-derived subagent_* fields are updated exclusively by
1437
+ # process_event()/record helpers and are not inferred from file counts.
1165
1438
  files = []
1166
1439
  signature_parts = []
1167
1440
  total_bytes = 0
@@ -1229,6 +1502,7 @@ class ProgressTracker:
1229
1502
  {"status": status, "count": count}
1230
1503
  for status, count in self.subagent_status_counts.most_common()
1231
1504
  ]
1505
+ subagent_stats = self._subagent_statistics()
1232
1506
  return {
1233
1507
  "updated_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
1234
1508
  "event_format": self.event_format,
@@ -1249,7 +1523,21 @@ class ProgressTracker:
1249
1523
  "active_subagent_count": self.active_subagent_count,
1250
1524
  "subagent_states": subagent_states,
1251
1525
  "subagent_spawn_count": self._subagent_spawn_count,
1526
+ "subagent_stats_source": "session_log",
1527
+ "subagent_provider": ",".join(subagent_stats["providers"]),
1528
+ "subagent_observed_ids": subagent_stats["observed_ids"],
1529
+ "subagent_source_metadata": {
1530
+ "source": subagent_stats["source"],
1531
+ "providers": subagent_stats["providers"],
1532
+ "source_events": subagent_stats["source_events"],
1533
+ "parent_activity_ids": subagent_stats["parent_activity_ids"],
1534
+ "child_activity_source": subagent_stats["child_activity_source"],
1535
+ },
1536
+ "subagent_statistics": subagent_stats,
1537
+ "child_activity_source": "filesystem_liveness_only",
1252
1538
  "child_thread_ids": sorted(self.codex_child_thread_ids),
1539
+ # Filesystem child activity is a heartbeat liveness fallback only.
1540
+ # Authoritative subagent counts/statuses above come from session.log.
1253
1541
  "child_session_files": self.child_session_files,
1254
1542
  "child_total_bytes": self.child_total_bytes,
1255
1543
  "child_activity_signature": self.child_activity_signature,
@@ -745,7 +745,7 @@ def resolve_prompt_platform(project_root):
745
745
  if resolution.platform:
746
746
  return resolution.platform
747
747
  raise RuntimeError(
748
- "PrizmKit agents not found. None of .claude/agents/, .codex/agents/, or .codebuddy/agents/ exists. "
748
+ "PrizmKit platform skills not found. None of .claude/commands/, .agents/skills/, or .codebuddy/skills/ exists. "
749
749
  "Run `npx prizmkit install` first, set .prizmkit/config.json ai_cli/platform, or set PRIZMKIT_PLATFORM=claude|codex|codebuddy explicitly."
750
750
  )
751
751
 
@@ -1,8 +1,8 @@
1
1
  # Dev-Pipeline Fallback Bootstrap Prompt
2
2
 
3
3
  > **Note**: This is an emergency fallback template. Normally, `generate-bootstrap-prompt.py`
4
- > selects a tier-specific template (`bootstrap-tier1.md`, `bootstrap-tier2.md`, or `bootstrap-tier3.md`).
5
- > If you are seeing this prompt, the tier-specific templates were not found.
4
+ > renders modular full guidance from `templates/sections/`. If sections are unavailable,
5
+ > it tries `bootstrap-tier3.md` before this last-resort fallback.
6
6
 
7
7
  ## Feature Context
8
8