prizmkit 1.1.119 → 1.1.121
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.
- package/bin/create-prizmkit.js +1 -5
- package/bundled/VERSION.json +3 -3
- package/bundled/dev-pipeline/README.md +41 -38
- package/bundled/dev-pipeline/assets/skill-subagent-integration.md +76 -0
- package/bundled/dev-pipeline/prizmkit_runtime/config.py +2 -8
- package/bundled/dev-pipeline/prizmkit_runtime/interoperability.py +1 -1
- package/bundled/dev-pipeline/prizmkit_runtime/platform_detection.py +3 -3
- package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +23 -25
- package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +8 -2
- package/bundled/dev-pipeline/prizmkit_runtime/runner_prompts.py +10 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +88 -32
- package/bundled/dev-pipeline/prizmkit_runtime/sessions.py +28 -6
- package/bundled/dev-pipeline/scripts/continuation.py +51 -6
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +60 -102
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +1 -19
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +1 -19
- package/bundled/dev-pipeline/scripts/{init-dev-team.py → init-change-artifact.py} +6 -16
- package/bundled/dev-pipeline/scripts/parse-stream-progress.py +364 -76
- package/bundled/dev-pipeline/scripts/update-bug-status.py +57 -14
- package/bundled/dev-pipeline/scripts/update-feature-status.py +54 -5
- package/bundled/dev-pipeline/scripts/update-refactor-status.py +57 -14
- package/bundled/dev-pipeline/scripts/utils.py +1 -1
- package/bundled/dev-pipeline/templates/bootstrap-prompt.md +2 -2
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +9 -11
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +2 -1
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +2 -2
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +3 -3
- package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +1 -1
- package/bundled/dev-pipeline/templates/sections/critical-paths-agent.md +0 -1
- package/bundled/dev-pipeline/templates/sections/critical-paths-full.md +0 -2
- package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +8 -1
- package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +6 -7
- package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +6 -7
- package/bundled/dev-pipeline/templates/sections/phase-implement-lite.md +4 -2
- package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +3 -3
- package/bundled/dev-pipeline/templates/sections/phase-review-full.md +5 -3
- package/bundled/dev-pipeline/templates/sections/phase-specify-plan-full.md +20 -0
- package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +3 -3
- package/bundled/dev-pipeline/tests/test_auto_skip.py +5 -5
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +214 -144
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +25 -12
- package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +11 -14
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +235 -32
- package/bundled/dev-pipeline/tests/test_unified_cli.py +219 -16
- package/bundled/skills/_metadata.json +4 -4
- package/bundled/skills/app-planner/SKILL.md +2 -2
- package/bundled/skills/app-planner/references/architecture-decisions.md +1 -3
- package/bundled/skills/prizmkit-code-review/SKILL.md +50 -47
- package/bundled/skills/prizmkit-code-review/references/review-report-template.md +13 -8
- package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +47 -33
- package/bundled/skills/prizmkit-code-review/references/reviewer-workspace-protocol.md +90 -0
- package/bundled/skills/prizmkit-implement/SKILL.md +21 -1
- package/bundled/skills/prizmkit-implement/references/implementation-subagent-procedure.md +67 -0
- package/bundled/skills/prizmkit-test/SKILL.md +159 -155
- package/bundled/skills/prizmkit-test/assets/authoritative-records.schema.json +420 -0
- package/bundled/skills/prizmkit-test/assets/behavior-risk-matrix.schema.json +116 -0
- package/bundled/skills/prizmkit-test/assets/evidence-manifest.schema.json +112 -0
- package/bundled/skills/prizmkit-test/assets/evidence-package-template.json +97 -0
- package/bundled/skills/prizmkit-test/references/boundary-coverage-protocol.md +76 -192
- package/bundled/skills/prizmkit-test/references/contract-mock-protocol.md +65 -0
- package/bundled/skills/prizmkit-test/references/evidence-protocol.md +194 -0
- package/bundled/skills/prizmkit-test/references/evidence-request-protocol.md +78 -0
- package/bundled/skills/prizmkit-test/references/examples.md +65 -108
- package/bundled/skills/prizmkit-test/references/service-boundary-test-catalog.md +10 -7
- package/bundled/skills/prizmkit-test/references/test-generation-steps.md +90 -82
- package/bundled/skills/prizmkit-test/references/test-report-template.md +77 -98
- package/bundled/skills/prizmkit-test/references/trusted-evidence-execution.md +97 -0
- package/bundled/skills/prizmkit-test/scripts/build_test_evidence.py +723 -0
- package/bundled/skills/prizmkit-test/scripts/validate_test_evidence.py +1426 -0
- package/package.json +1 -1
- package/src/clean.js +12 -8
- package/src/config.js +24 -42
- package/src/index.js +1 -10
- package/src/manifest.js +3 -9
- package/src/metadata.js +0 -26
- package/src/prompts.js +0 -13
- package/src/scaffold.js +76 -201
- package/src/upgrade.js +16 -33
- package/bundled/adapters/claude/agent-adapter.js +0 -96
- package/bundled/adapters/claude/team-adapter.js +0 -183
- package/bundled/adapters/codebuddy/agent-adapter.js +0 -42
- package/bundled/adapters/codebuddy/team-adapter.js +0 -46
- package/bundled/adapters/codex/agent-adapter.js +0 -38
- package/bundled/adapters/codex/team-adapter.js +0 -37
- package/bundled/agents/prizm-dev-team-dev.md +0 -123
- package/bundled/agents/prizm-dev-team-reviewer.md +0 -113
- package/bundled/dev-pipeline/assets/prizm-dev-team-integration.md +0 -65
- package/bundled/dev-pipeline/templates/bootstrap-tier1.md +0 -436
- package/bundled/dev-pipeline/templates/bootstrap-tier2.md +0 -510
- package/bundled/skills/prizmkit-test/scripts/validate_boundary_report.py +0 -347
- 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
|
-
#
|
|
350
|
-
self._record_cb_agent_tool_call(
|
|
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(
|
|
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
|
-
#
|
|
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
|
|
744
|
-
"""Record
|
|
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
|
-
|
|
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
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
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
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
998
|
-
|
|
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,
|