prizmkit 1.1.57 → 1.1.60
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 +8 -6
- package/bundled/VERSION.json +3 -3
- package/bundled/adapters/codex/agent-adapter.js +38 -0
- package/bundled/adapters/codex/paths.js +27 -0
- package/bundled/adapters/codex/rules-adapter.js +30 -0
- package/bundled/adapters/codex/settings-adapter.js +27 -0
- package/bundled/adapters/codex/skill-adapter.js +65 -0
- package/bundled/adapters/codex/team-adapter.js +37 -0
- package/bundled/dev-pipeline/.env.example +2 -1
- package/bundled/dev-pipeline/README.md +10 -7
- package/bundled/dev-pipeline/lib/common.sh +278 -37
- package/bundled/dev-pipeline/run-bugfix.sh +10 -61
- package/bundled/dev-pipeline/run-feature.sh +10 -78
- package/bundled/dev-pipeline/run-recovery.sh +10 -46
- package/bundled/dev-pipeline/run-refactor.sh +10 -61
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +17 -7
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +9 -3
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +9 -3
- package/bundled/dev-pipeline/scripts/utils.py +6 -4
- package/bundled/dev-pipeline-windows/.env.example +28 -0
- package/bundled/dev-pipeline-windows/README.md +30 -0
- package/bundled/dev-pipeline-windows/SCHEMA_ANALYSIS.md +525 -0
- package/bundled/dev-pipeline-windows/assets/feature-list-example.json +146 -0
- package/bundled/dev-pipeline-windows/assets/prizm-dev-team-integration.md +138 -0
- package/bundled/dev-pipeline-windows/launch-bugfix-daemon.ps1 +9 -0
- package/bundled/dev-pipeline-windows/launch-feature-daemon.ps1 +9 -0
- package/bundled/dev-pipeline-windows/launch-refactor-daemon.ps1 +9 -0
- package/bundled/dev-pipeline-windows/lib/common.ps1 +432 -0
- package/bundled/dev-pipeline-windows/lib/daemon.ps1 +140 -0
- package/bundled/dev-pipeline-windows/lib/pipeline.ps1 +446 -0
- package/bundled/dev-pipeline-windows/lib/reset.ps1 +87 -0
- package/bundled/dev-pipeline-windows/reset-bug.ps1 +9 -0
- package/bundled/dev-pipeline-windows/reset-feature.ps1 +9 -0
- package/bundled/dev-pipeline-windows/reset-refactor.ps1 +9 -0
- package/bundled/dev-pipeline-windows/run-bugfix.ps1 +9 -0
- package/bundled/dev-pipeline-windows/run-feature.ps1 +9 -0
- package/bundled/dev-pipeline-windows/run-recovery.ps1 +76 -0
- package/bundled/dev-pipeline-windows/run-refactor.ps1 +9 -0
- package/bundled/dev-pipeline-windows/scripts/check-session-status.py +228 -0
- package/bundled/dev-pipeline-windows/scripts/cleanup-logs.py +192 -0
- package/bundled/dev-pipeline-windows/scripts/detect-stuck.py +530 -0
- package/bundled/dev-pipeline-windows/scripts/generate-bootstrap-prompt.py +1737 -0
- package/bundled/dev-pipeline-windows/scripts/generate-bugfix-prompt.py +685 -0
- package/bundled/dev-pipeline-windows/scripts/generate-recovery-prompt.py +805 -0
- package/bundled/dev-pipeline-windows/scripts/generate-refactor-prompt.py +763 -0
- package/bundled/dev-pipeline-windows/scripts/init-bugfix-pipeline.py +316 -0
- package/bundled/dev-pipeline-windows/scripts/init-dev-team.py +134 -0
- package/bundled/dev-pipeline-windows/scripts/init-pipeline.py +380 -0
- package/bundled/dev-pipeline-windows/scripts/init-refactor-pipeline.py +399 -0
- package/bundled/dev-pipeline-windows/scripts/parse-stream-progress.py +388 -0
- package/bundled/dev-pipeline-windows/scripts/patch-completion-notes.py +191 -0
- package/bundled/dev-pipeline-windows/scripts/update-bug-status.py +864 -0
- package/bundled/dev-pipeline-windows/scripts/update-checkpoint.py +173 -0
- package/bundled/dev-pipeline-windows/scripts/update-feature-status.py +1501 -0
- package/bundled/dev-pipeline-windows/scripts/update-refactor-status.py +1073 -0
- package/bundled/dev-pipeline-windows/scripts/utils.py +542 -0
- package/bundled/dev-pipeline-windows/templates/agent-prompts/critic-plan-challenge.md +7 -0
- package/bundled/dev-pipeline-windows/templates/agent-prompts/dev-fix.md +7 -0
- package/bundled/dev-pipeline-windows/templates/agent-prompts/dev-implement.md +30 -0
- package/bundled/dev-pipeline-windows/templates/agent-prompts/dev-resume.md +5 -0
- package/bundled/dev-pipeline-windows/templates/agent-prompts/reviewer-review.md +7 -0
- package/bundled/dev-pipeline-windows/templates/bootstrap-prompt.md +46 -0
- package/bundled/dev-pipeline-windows/templates/bootstrap-tier1.md +43 -0
- package/bundled/dev-pipeline-windows/templates/bootstrap-tier2.md +43 -0
- package/bundled/dev-pipeline-windows/templates/bootstrap-tier3.md +43 -0
- package/bundled/dev-pipeline-windows/templates/bug-fix-list-schema.json +263 -0
- package/bundled/dev-pipeline-windows/templates/bugfix-bootstrap-prompt.md +320 -0
- package/bundled/dev-pipeline-windows/templates/feature-list-schema.json +237 -0
- package/bundled/dev-pipeline-windows/templates/refactor-bootstrap-prompt.md +331 -0
- package/bundled/dev-pipeline-windows/templates/refactor-list-schema.json +270 -0
- package/bundled/dev-pipeline-windows/templates/sections/ac-verification-checklist.md +13 -0
- package/bundled/dev-pipeline-windows/templates/sections/checkpoint-system.md +91 -0
- package/bundled/dev-pipeline-windows/templates/sections/context-budget-rules.md +33 -0
- package/bundled/dev-pipeline-windows/templates/sections/critical-paths-agent.md +10 -0
- package/bundled/dev-pipeline-windows/templates/sections/critical-paths-full.md +12 -0
- package/bundled/dev-pipeline-windows/templates/sections/critical-paths-lite.md +7 -0
- package/bundled/dev-pipeline-windows/templates/sections/directory-convention-agent.md +8 -0
- package/bundled/dev-pipeline-windows/templates/sections/directory-convention-full.md +9 -0
- package/bundled/dev-pipeline-windows/templates/sections/directory-convention-lite.md +6 -0
- package/bundled/dev-pipeline-windows/templates/sections/failure-capture.md +21 -0
- package/bundled/dev-pipeline-windows/templates/sections/feature-context.md +31 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-browser-verification-auto.md +72 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-browser-verification-opencli.md +63 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-browser-verification.md +62 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-commit-full.md +71 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-commit.md +64 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-context-snapshot-agent-suffix.md +23 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-context-snapshot-base.md +24 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-context-snapshot-lite-suffix.md +12 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-critic-plan-full.md +53 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-critic-plan.md +32 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-implement-agent.md +37 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-implement-full.md +50 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-implement-lite.md +52 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-plan-agent.md +27 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-plan-lite.md +27 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-review-agent.md +27 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-review-full.md +29 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-specify-plan-full.md +77 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase0-init.md +13 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase0-test-baseline.md +23 -0
- package/bundled/dev-pipeline-windows/templates/sections/session-context.md +5 -0
- package/bundled/dev-pipeline-windows/templates/sections/subagent-timeout-recovery.md +6 -0
- package/bundled/dev-pipeline-windows/templates/sections/test-failure-recovery-agent.md +67 -0
- package/bundled/dev-pipeline-windows/templates/sections/test-failure-recovery-lite.md +58 -0
- package/bundled/dev-pipeline-windows/templates/session-status-schema.json +83 -0
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/app-planner/SKILL.md +26 -18
- package/bundled/skills/app-planner/references/architecture-decisions.md +9 -5
- package/bundled/skills/app-planner/references/frontend-design-guide.md +1 -1
- package/bundled/skills/feature-planner/SKILL.md +9 -2
- package/bundled/skills/prizmkit-init/SKILL.md +7 -6
- package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +2 -0
- package/bundled/skills-windows/app-planner/SKILL.md +639 -0
- package/bundled/skills-windows/app-planner/assets/app-design-guide.md +101 -0
- package/bundled/skills-windows/app-planner/references/architecture-decisions.md +52 -0
- package/bundled/skills-windows/app-planner/references/brainstorm-guide.md +101 -0
- package/bundled/skills-windows/app-planner/references/frontend-design-guide.md +71 -0
- package/bundled/skills-windows/app-planner/references/project-brief-guide.md +82 -0
- package/bundled/skills-windows/app-planner/references/red-team-checklist.md +40 -0
- package/bundled/skills-windows/app-planner/references/rules/backend/derivation-rules.md +609 -0
- package/bundled/skills-windows/app-planner/references/rules/backend/fixed-rules.md +285 -0
- package/bundled/skills-windows/app-planner/references/rules/backend/question-bank.md +249 -0
- package/bundled/skills-windows/app-planner/references/rules/backend/template.md +173 -0
- package/bundled/skills-windows/app-planner/references/rules/database/derivation-rules.md +373 -0
- package/bundled/skills-windows/app-planner/references/rules/database/fixed-rules.md +211 -0
- package/bundled/skills-windows/app-planner/references/rules/database/question-bank.md +184 -0
- package/bundled/skills-windows/app-planner/references/rules/database/template.md +158 -0
- package/bundled/skills-windows/app-planner/references/rules/frontend/derivation-rules.md +810 -0
- package/bundled/skills-windows/app-planner/references/rules/frontend/fixed-rules.md +188 -0
- package/bundled/skills-windows/app-planner/references/rules/frontend/question-bank.md +302 -0
- package/bundled/skills-windows/app-planner/references/rules/frontend/template.md +320 -0
- package/bundled/skills-windows/app-planner/references/rules/mobile/derivation-rules.md +639 -0
- package/bundled/skills-windows/app-planner/references/rules/mobile/fixed-rules.md +290 -0
- package/bundled/skills-windows/app-planner/references/rules/mobile/question-bank.md +232 -0
- package/bundled/skills-windows/app-planner/references/rules/mobile/template.md +175 -0
- package/bundled/skills-windows/bug-fix-workflow/SKILL.md +415 -0
- package/bundled/skills-windows/bug-planner/SKILL.md +395 -0
- package/bundled/skills-windows/bug-planner/assets/bug-confirmation-template.md +43 -0
- package/bundled/skills-windows/bug-planner/references/critic-and-verification.md +44 -0
- package/bundled/skills-windows/bug-planner/references/error-recovery.md +73 -0
- package/bundled/skills-windows/bug-planner/references/input-formats.md +53 -0
- package/bundled/skills-windows/bug-planner/references/schema-validation.md +25 -0
- package/bundled/skills-windows/bug-planner/references/severity-rules.md +16 -0
- package/bundled/skills-windows/bug-planner/scripts/validate-bug-list.py +322 -0
- package/bundled/skills-windows/bugfix-pipeline-launcher/SKILL.md +380 -0
- package/bundled/skills-windows/feature-pipeline-launcher/SKILL.md +441 -0
- package/bundled/skills-windows/feature-pipeline-launcher/scripts/preflight-check.py +462 -0
- package/bundled/skills-windows/feature-planner/SKILL.md +401 -0
- package/bundled/skills-windows/feature-planner/assets/evaluation-guide.md +64 -0
- package/bundled/skills-windows/feature-planner/assets/planning-guide.md +214 -0
- package/bundled/skills-windows/feature-planner/references/browser-interaction.md +59 -0
- package/bundled/skills-windows/feature-planner/references/completeness-review.md +57 -0
- package/bundled/skills-windows/feature-planner/references/decomposition-patterns.md +75 -0
- package/bundled/skills-windows/feature-planner/references/error-recovery.md +90 -0
- package/bundled/skills-windows/feature-planner/references/incremental-feature-planning.md +112 -0
- package/bundled/skills-windows/feature-planner/references/new-project-planning.md +85 -0
- package/bundled/skills-windows/feature-planner/scripts/validate-and-generate.py +1029 -0
- package/bundled/skills-windows/feature-workflow/SKILL.md +531 -0
- package/bundled/skills-windows/prizmkit-init/SKILL.md +356 -0
- package/bundled/skills-windows/prizmkit-init/assets/project-brief-template.md +82 -0
- package/bundled/skills-windows/prizmkit-init/references/config-schema.md +68 -0
- package/bundled/skills-windows/prizmkit-init/references/rules/layer-detection.md +41 -0
- package/bundled/skills-windows/prizmkit-init/references/tech-stack-catalog.md +13 -0
- package/bundled/skills-windows/prizmkit-init/references/update-supplement.md +9 -0
- package/bundled/skills-windows/recovery-workflow/SKILL.md +456 -0
- package/bundled/skills-windows/recovery-workflow/evals/evals.json +46 -0
- package/bundled/skills-windows/recovery-workflow/scripts/detect-recovery-state.py +544 -0
- package/bundled/skills-windows/refactor-pipeline-launcher/SKILL.md +406 -0
- package/bundled/skills-windows/refactor-planner/SKILL.md +540 -0
- package/bundled/skills-windows/refactor-planner/assets/planning-guide.md +292 -0
- package/bundled/skills-windows/refactor-planner/references/behavior-preservation.md +301 -0
- package/bundled/skills-windows/refactor-planner/references/refactor-scoping-guide.md +221 -0
- package/bundled/skills-windows/refactor-planner/scripts/validate-and-generate-refactor.py +858 -0
- package/bundled/skills-windows/refactor-workflow/SKILL.md +503 -0
- package/package.json +3 -2
- package/src/clean.js +73 -2
- package/src/config.js +159 -50
- package/src/detect-platform.js +16 -8
- package/src/external-skills.js +26 -19
- package/src/index.js +31 -9
- package/src/manifest.js +6 -2
- package/src/metadata.js +43 -5
- package/src/platforms.js +36 -0
- package/src/prompts.js +31 -6
- package/src/runtimes.js +20 -0
- package/src/scaffold.js +314 -110
- package/src/upgrade.js +81 -41
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
parse-stream-progress.py - Real-time stream-json progress parser
|
|
4
|
+
|
|
5
|
+
Continuously reads an AI CLI session log (JSONL from --output-format stream-json),
|
|
6
|
+
extracts tool calls, phase changes, and activity metrics, and writes structured
|
|
7
|
+
progress to a progress.json file for heartbeat monitoring.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python3 parse-stream-progress.py --session-log <path> --progress-file <path>
|
|
11
|
+
|
|
12
|
+
The script runs until:
|
|
13
|
+
- The session log stops growing and the CLI process exits
|
|
14
|
+
- It receives SIGTERM/SIGINT
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import signal
|
|
21
|
+
import sys
|
|
22
|
+
import tempfile
|
|
23
|
+
import time
|
|
24
|
+
from collections import Counter
|
|
25
|
+
from datetime import datetime, timezone
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Ordered pipeline phases — index defines forward-only progression.
|
|
29
|
+
# Phase detection is monotonic: once a phase is reached, earlier phases
|
|
30
|
+
# cannot be re-entered (prevents false positives from file content mentions).
|
|
31
|
+
PHASE_ORDER = ["plan", "implement", "code-review", "retrospective", "commit"]
|
|
32
|
+
|
|
33
|
+
# Keywords for phase detection.
|
|
34
|
+
# "strong" keywords are skill invocations — high confidence, but still
|
|
35
|
+
# forward-only (a mention of prizmkit-plan while in commit phase is noise).
|
|
36
|
+
# "weak" keywords are contextual hints — also forward-only, used when no
|
|
37
|
+
# strong keyword matches.
|
|
38
|
+
PHASE_KEYWORDS = {
|
|
39
|
+
"plan": {
|
|
40
|
+
"strong": ["prizmkit-plan"],
|
|
41
|
+
"weak": ["spec.md", "plan.md", "specification", "architecture", "task checklist", "task breakdown", "gathering requirements"],
|
|
42
|
+
},
|
|
43
|
+
"implement": {
|
|
44
|
+
"strong": ["prizmkit-implement"],
|
|
45
|
+
"weak": ["implement", "writing code", "TDD", "coding"],
|
|
46
|
+
},
|
|
47
|
+
"code-review": {
|
|
48
|
+
"strong": ["prizmkit-code-review"],
|
|
49
|
+
"weak": ["code review", "review verdict", "reviewing"],
|
|
50
|
+
},
|
|
51
|
+
"retrospective": {
|
|
52
|
+
"strong": ["prizmkit-retrospective"],
|
|
53
|
+
"weak": ["retrospective", "structural sync", "architecture knowledge", ".prizmkit/prizm-docs/ sync"],
|
|
54
|
+
},
|
|
55
|
+
"commit": {
|
|
56
|
+
"strong": ["prizmkit-committer"],
|
|
57
|
+
"weak": ["git commit", "committing", "feat(", "fix("],
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ProgressTracker:
|
|
63
|
+
"""Tracks progress state from stream-json events."""
|
|
64
|
+
|
|
65
|
+
def __init__(self):
|
|
66
|
+
self.message_count = 0
|
|
67
|
+
self.current_tool = None
|
|
68
|
+
self.current_tool_input_summary = ""
|
|
69
|
+
self.current_phase = None
|
|
70
|
+
self.detected_phases = []
|
|
71
|
+
self.tool_call_counts = Counter()
|
|
72
|
+
self.total_tool_calls = 0
|
|
73
|
+
self.last_text_snippet = ""
|
|
74
|
+
self.is_active = True
|
|
75
|
+
self.errors = []
|
|
76
|
+
self._text_buffer = ""
|
|
77
|
+
self._in_tool_use = False
|
|
78
|
+
self._current_tool_input_parts = []
|
|
79
|
+
|
|
80
|
+
def process_event(self, event):
|
|
81
|
+
"""Process a single stream-json event and update state.
|
|
82
|
+
|
|
83
|
+
Supports two formats:
|
|
84
|
+
1. Claude API raw stream: message_start, content_block_start, content_block_delta, etc.
|
|
85
|
+
2. Claude Code verbose stream-json: assistant, user, tool_result, system, etc.
|
|
86
|
+
(produced by claude/claude-internal --verbose --output-format stream-json)
|
|
87
|
+
"""
|
|
88
|
+
event_type = event.get("type", "")
|
|
89
|
+
|
|
90
|
+
# ── Claude Code verbose format ──────────────────────────────
|
|
91
|
+
if event_type == "assistant":
|
|
92
|
+
self.message_count += 1
|
|
93
|
+
self.is_active = True
|
|
94
|
+
message = event.get("message", {})
|
|
95
|
+
content_blocks = message.get("content", [])
|
|
96
|
+
for block in content_blocks:
|
|
97
|
+
block_type = block.get("type", "")
|
|
98
|
+
if block_type == "tool_use":
|
|
99
|
+
tool_name = block.get("name", "unknown")
|
|
100
|
+
self.current_tool = tool_name
|
|
101
|
+
self.tool_call_counts[tool_name] += 1
|
|
102
|
+
self.total_tool_calls += 1
|
|
103
|
+
# Extract summary from input
|
|
104
|
+
tool_input = block.get("input", {})
|
|
105
|
+
if isinstance(tool_input, dict):
|
|
106
|
+
self._extract_tool_summary_from_dict(tool_input)
|
|
107
|
+
self._detect_phase(json.dumps(tool_input, ensure_ascii=False)[:500])
|
|
108
|
+
elif block_type == "text":
|
|
109
|
+
text = block.get("text", "")
|
|
110
|
+
if text.strip():
|
|
111
|
+
self.last_text_snippet = text.strip()[:120]
|
|
112
|
+
self._detect_phase(text)
|
|
113
|
+
|
|
114
|
+
elif event_type == "tool_result" or event_type == "user":
|
|
115
|
+
# tool_result contains output from tool execution
|
|
116
|
+
self.is_active = True
|
|
117
|
+
|
|
118
|
+
elif event_type == "system":
|
|
119
|
+
# System events (hooks, init, etc.) — track but don't count as messages
|
|
120
|
+
subtype = event.get("subtype", "")
|
|
121
|
+
if subtype == "init":
|
|
122
|
+
self.is_active = True
|
|
123
|
+
|
|
124
|
+
# ── Claude API raw stream format ────────────────────────────
|
|
125
|
+
elif event_type == "message_start":
|
|
126
|
+
self.message_count += 1
|
|
127
|
+
self.is_active = True
|
|
128
|
+
|
|
129
|
+
elif event_type == "message_stop":
|
|
130
|
+
self.current_tool = None
|
|
131
|
+
self.current_tool_input_summary = ""
|
|
132
|
+
self._in_tool_use = False
|
|
133
|
+
self._current_tool_input_parts = []
|
|
134
|
+
|
|
135
|
+
elif event_type == "content_block_start":
|
|
136
|
+
content_block = event.get("content_block", {})
|
|
137
|
+
block_type = content_block.get("type", "")
|
|
138
|
+
|
|
139
|
+
if block_type == "tool_use":
|
|
140
|
+
tool_name = content_block.get("name", "unknown")
|
|
141
|
+
self.current_tool = tool_name
|
|
142
|
+
self.current_tool_input_summary = ""
|
|
143
|
+
self.tool_call_counts[tool_name] += 1
|
|
144
|
+
self.total_tool_calls += 1
|
|
145
|
+
self._in_tool_use = True
|
|
146
|
+
self._current_tool_input_parts = []
|
|
147
|
+
|
|
148
|
+
elif block_type == "text":
|
|
149
|
+
self._text_buffer = ""
|
|
150
|
+
self._in_tool_use = False
|
|
151
|
+
|
|
152
|
+
elif event_type == "content_block_delta":
|
|
153
|
+
delta = event.get("delta", {})
|
|
154
|
+
delta_type = delta.get("type", "")
|
|
155
|
+
|
|
156
|
+
if delta_type == "text_delta":
|
|
157
|
+
text = delta.get("text", "")
|
|
158
|
+
self._text_buffer += text
|
|
159
|
+
# Keep last meaningful snippet
|
|
160
|
+
stripped = text.strip()
|
|
161
|
+
if stripped:
|
|
162
|
+
self.last_text_snippet = stripped[:120]
|
|
163
|
+
# Try to detect phase from text
|
|
164
|
+
self._detect_phase(text)
|
|
165
|
+
|
|
166
|
+
elif delta_type == "input_json_delta":
|
|
167
|
+
partial = delta.get("partial_json", "")
|
|
168
|
+
self._current_tool_input_parts.append(partial)
|
|
169
|
+
# Build a summary from accumulated input
|
|
170
|
+
accumulated = "".join(self._current_tool_input_parts)
|
|
171
|
+
self.current_tool_input_summary = accumulated[:150]
|
|
172
|
+
|
|
173
|
+
elif event_type == "content_block_stop":
|
|
174
|
+
if self._in_tool_use:
|
|
175
|
+
# Try to extract a better summary from complete tool input
|
|
176
|
+
full_input = "".join(self._current_tool_input_parts)
|
|
177
|
+
self._extract_tool_summary(full_input)
|
|
178
|
+
self._detect_phase(full_input)
|
|
179
|
+
else:
|
|
180
|
+
# Text block finished - detect phase from accumulated text
|
|
181
|
+
if self._text_buffer:
|
|
182
|
+
self._detect_phase(self._text_buffer)
|
|
183
|
+
self._in_tool_use = False
|
|
184
|
+
self._current_tool_input_parts = []
|
|
185
|
+
|
|
186
|
+
elif event_type == "error":
|
|
187
|
+
error_msg = event.get("error", {}).get("message", "Unknown error")
|
|
188
|
+
self.errors.append(error_msg)
|
|
189
|
+
|
|
190
|
+
# Check for subagent indicator
|
|
191
|
+
if event.get("parent_tool_use_id"):
|
|
192
|
+
# This is a sub-agent event; tool name is still tracked normally
|
|
193
|
+
pass
|
|
194
|
+
|
|
195
|
+
def _detect_phase(self, text):
|
|
196
|
+
"""Detect pipeline phase from text content.
|
|
197
|
+
|
|
198
|
+
Uses monotonic (forward-only) progression to avoid false positives.
|
|
199
|
+
Strong keywords (skill invocations) always trigger; weak keywords
|
|
200
|
+
only trigger for phases at or ahead of the current position.
|
|
201
|
+
"""
|
|
202
|
+
text_lower = text.lower()
|
|
203
|
+
current_idx = (
|
|
204
|
+
PHASE_ORDER.index(self.current_phase)
|
|
205
|
+
if self.current_phase in PHASE_ORDER
|
|
206
|
+
else -1
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
for phase, kw_groups in PHASE_KEYWORDS.items():
|
|
210
|
+
phase_idx = PHASE_ORDER.index(phase)
|
|
211
|
+
|
|
212
|
+
# Check strong keywords — high confidence but still forward-only
|
|
213
|
+
for keyword in kw_groups.get("strong", []):
|
|
214
|
+
if keyword.lower() in text_lower:
|
|
215
|
+
if phase_idx >= current_idx:
|
|
216
|
+
self.current_phase = phase
|
|
217
|
+
if phase not in self.detected_phases:
|
|
218
|
+
self.detected_phases.append(phase)
|
|
219
|
+
return
|
|
220
|
+
# Backward strong match — skip this phase but do NOT
|
|
221
|
+
# early-return, so forward weak matches can still fire.
|
|
222
|
+
break
|
|
223
|
+
|
|
224
|
+
# Check weak keywords — only trigger for forward phases
|
|
225
|
+
if phase_idx >= current_idx:
|
|
226
|
+
for keyword in kw_groups.get("weak", []):
|
|
227
|
+
if keyword.lower() in text_lower:
|
|
228
|
+
self.current_phase = phase
|
|
229
|
+
if phase not in self.detected_phases:
|
|
230
|
+
self.detected_phases.append(phase)
|
|
231
|
+
return
|
|
232
|
+
|
|
233
|
+
def _extract_tool_summary(self, raw_input):
|
|
234
|
+
"""Extract a human-readable summary from tool input JSON string."""
|
|
235
|
+
try:
|
|
236
|
+
data = json.loads(raw_input)
|
|
237
|
+
self._extract_tool_summary_from_dict(data)
|
|
238
|
+
except (json.JSONDecodeError, TypeError):
|
|
239
|
+
# Keep whatever partial summary we had
|
|
240
|
+
pass
|
|
241
|
+
|
|
242
|
+
def _extract_tool_summary_from_dict(self, data):
|
|
243
|
+
"""Extract a human-readable summary from tool input dict."""
|
|
244
|
+
if isinstance(data, dict):
|
|
245
|
+
# Common patterns in tool inputs
|
|
246
|
+
if "description" in data:
|
|
247
|
+
self.current_tool_input_summary = str(data["description"])[:100]
|
|
248
|
+
elif "command" in data:
|
|
249
|
+
self.current_tool_input_summary = str(data["command"])[:100]
|
|
250
|
+
elif "file_path" in data:
|
|
251
|
+
self.current_tool_input_summary = str(data["file_path"])[:100]
|
|
252
|
+
elif "pattern" in data:
|
|
253
|
+
self.current_tool_input_summary = str(data["pattern"])[:100]
|
|
254
|
+
elif "query" in data:
|
|
255
|
+
self.current_tool_input_summary = str(data["query"])[:100]
|
|
256
|
+
elif "prompt" in data:
|
|
257
|
+
self.current_tool_input_summary = str(data["prompt"])[:100]
|
|
258
|
+
|
|
259
|
+
def to_dict(self):
|
|
260
|
+
"""Export current state as a dictionary for JSON serialization."""
|
|
261
|
+
tool_calls = [
|
|
262
|
+
{"name": name, "count": count}
|
|
263
|
+
for name, count in self.tool_call_counts.most_common()
|
|
264
|
+
]
|
|
265
|
+
return {
|
|
266
|
+
"updated_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
267
|
+
"message_count": self.message_count,
|
|
268
|
+
"current_tool": self.current_tool,
|
|
269
|
+
"current_tool_input_summary": self.current_tool_input_summary,
|
|
270
|
+
"current_phase": self.current_phase,
|
|
271
|
+
"detected_phases": self.detected_phases,
|
|
272
|
+
"tool_calls": tool_calls,
|
|
273
|
+
"total_tool_calls": self.total_tool_calls,
|
|
274
|
+
"last_text_snippet": self.last_text_snippet,
|
|
275
|
+
"is_active": self.is_active,
|
|
276
|
+
"errors": self.errors[-10:], # Keep last 10 errors
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def atomic_write_json(data, filepath):
|
|
281
|
+
"""Write JSON data atomically using tmp file + rename."""
|
|
282
|
+
dir_path = os.path.dirname(filepath)
|
|
283
|
+
tmp_path = None
|
|
284
|
+
try:
|
|
285
|
+
fd, tmp_path = tempfile.mkstemp(dir=dir_path, suffix=".tmp")
|
|
286
|
+
with os.fdopen(fd, "w") as f:
|
|
287
|
+
json.dump(data, f, indent=2)
|
|
288
|
+
f.write("\n")
|
|
289
|
+
os.rename(tmp_path, filepath)
|
|
290
|
+
except OSError:
|
|
291
|
+
# Best effort - remove tmp file if rename failed
|
|
292
|
+
if tmp_path:
|
|
293
|
+
try:
|
|
294
|
+
os.unlink(tmp_path)
|
|
295
|
+
except OSError:
|
|
296
|
+
pass
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def tail_and_parse(session_log, progress_file, poll_interval=0.5):
|
|
300
|
+
"""Tail session log and parse stream-json events."""
|
|
301
|
+
tracker = ProgressTracker()
|
|
302
|
+
last_write_state = None
|
|
303
|
+
|
|
304
|
+
# Wait for log file to appear
|
|
305
|
+
wait_count = 0
|
|
306
|
+
while not os.path.exists(session_log):
|
|
307
|
+
time.sleep(poll_interval)
|
|
308
|
+
wait_count += 1
|
|
309
|
+
if wait_count > 120: # 60 seconds max wait
|
|
310
|
+
sys.stderr.write(f"Timeout waiting for {session_log}\n")
|
|
311
|
+
sys.exit(1)
|
|
312
|
+
|
|
313
|
+
with open(session_log, "r") as f:
|
|
314
|
+
idle_count = 0
|
|
315
|
+
while True:
|
|
316
|
+
line = f.readline()
|
|
317
|
+
if line:
|
|
318
|
+
idle_count = 0
|
|
319
|
+
line = line.strip()
|
|
320
|
+
if not line:
|
|
321
|
+
continue
|
|
322
|
+
try:
|
|
323
|
+
event = json.loads(line)
|
|
324
|
+
tracker.process_event(event)
|
|
325
|
+
except json.JSONDecodeError:
|
|
326
|
+
# Not a JSON line (could be stderr mixed in)
|
|
327
|
+
# Use it as a text snippet if meaningful
|
|
328
|
+
stripped = line.strip()
|
|
329
|
+
if stripped and len(stripped) > 5:
|
|
330
|
+
tracker.last_text_snippet = stripped[:120]
|
|
331
|
+
continue
|
|
332
|
+
|
|
333
|
+
# Write progress if state changed
|
|
334
|
+
current_state = tracker.to_dict()
|
|
335
|
+
state_key = (
|
|
336
|
+
current_state["message_count"],
|
|
337
|
+
current_state["current_tool"],
|
|
338
|
+
current_state["current_phase"],
|
|
339
|
+
current_state["total_tool_calls"],
|
|
340
|
+
)
|
|
341
|
+
if state_key != last_write_state:
|
|
342
|
+
atomic_write_json(current_state, progress_file)
|
|
343
|
+
last_write_state = state_key
|
|
344
|
+
else:
|
|
345
|
+
idle_count += 1
|
|
346
|
+
# After 2 seconds of no new data, write current state anyway
|
|
347
|
+
# (ensures progress.json stays fresh)
|
|
348
|
+
if idle_count == 4:
|
|
349
|
+
current_state = tracker.to_dict()
|
|
350
|
+
atomic_write_json(current_state, progress_file)
|
|
351
|
+
|
|
352
|
+
# After 3600 idle cycles (30 min), mark inactive and exit
|
|
353
|
+
if idle_count > 3600:
|
|
354
|
+
tracker.is_active = False
|
|
355
|
+
atomic_write_json(tracker.to_dict(), progress_file)
|
|
356
|
+
break
|
|
357
|
+
|
|
358
|
+
time.sleep(poll_interval)
|
|
359
|
+
|
|
360
|
+
# Final write
|
|
361
|
+
tracker.is_active = False
|
|
362
|
+
atomic_write_json(tracker.to_dict(), progress_file)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def main():
|
|
366
|
+
parser = argparse.ArgumentParser(description="Parse stream-json progress")
|
|
367
|
+
parser.add_argument("--session-log", required=True, help="Path to session.log (JSONL)")
|
|
368
|
+
parser.add_argument("--progress-file", required=True, help="Path to write progress.json")
|
|
369
|
+
args = parser.parse_args()
|
|
370
|
+
|
|
371
|
+
# Handle graceful shutdown
|
|
372
|
+
def handle_signal(signum, frame):
|
|
373
|
+
sys.exit(0)
|
|
374
|
+
|
|
375
|
+
signal.signal(signal.SIGTERM, handle_signal)
|
|
376
|
+
signal.signal(signal.SIGINT, handle_signal)
|
|
377
|
+
|
|
378
|
+
try:
|
|
379
|
+
tail_and_parse(args.session_log, args.progress_file)
|
|
380
|
+
except SystemExit:
|
|
381
|
+
pass
|
|
382
|
+
except Exception as e:
|
|
383
|
+
sys.stderr.write(f"parse-stream-progress.py error: {e}\n")
|
|
384
|
+
sys.exit(1)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
if __name__ == "__main__":
|
|
388
|
+
main()
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Patch completion_notes into feature-list.json, refactor-list.json, or bug-fix-list.json.
|
|
3
|
+
|
|
4
|
+
Reads a completion-summary.json file written by the AI session and patches
|
|
5
|
+
the corresponding item in the task list with the completion_notes field.
|
|
6
|
+
|
|
7
|
+
This enables rich dependency context propagation: when a downstream task's
|
|
8
|
+
bootstrap prompt is generated, it can read completion_notes from its
|
|
9
|
+
completed dependencies to understand what was built/changed.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
python3 patch-completion-notes.py \
|
|
13
|
+
--feature-list .prizmkit/plans/feature-list.json \
|
|
14
|
+
--feature-id F-001 \
|
|
15
|
+
--summary .prizmkit/specs/001-my-feature/completion-summary.json
|
|
16
|
+
|
|
17
|
+
python3 patch-completion-notes.py \
|
|
18
|
+
--refactor-list .prizmkit/plans/refactor-list.json \
|
|
19
|
+
--refactor-id R-001 \
|
|
20
|
+
--summary <path-to-summary>
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
import sys
|
|
27
|
+
|
|
28
|
+
from utils import load_json_file, write_json_file, setup_logging
|
|
29
|
+
|
|
30
|
+
LOGGER = setup_logging("patch-completion-notes")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def parse_args():
|
|
34
|
+
parser = argparse.ArgumentParser(
|
|
35
|
+
description="Patch completion_notes into a task list from completion-summary.json."
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"--feature-list",
|
|
39
|
+
default=None,
|
|
40
|
+
help="Path to .prizmkit/plans/feature-list.json",
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"--refactor-list",
|
|
44
|
+
default=None,
|
|
45
|
+
help="Path to .prizmkit/plans/refactor-list.json",
|
|
46
|
+
)
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"--bug-list",
|
|
49
|
+
default=None,
|
|
50
|
+
help="Path to .prizmkit/plans/bug-fix-list.json",
|
|
51
|
+
)
|
|
52
|
+
parser.add_argument(
|
|
53
|
+
"--feature-id",
|
|
54
|
+
default=None,
|
|
55
|
+
help="Feature ID to patch (e.g. F-001)",
|
|
56
|
+
)
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--refactor-id",
|
|
59
|
+
default=None,
|
|
60
|
+
help="Refactor ID to patch (e.g. R-001)",
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"--bug-id",
|
|
64
|
+
default=None,
|
|
65
|
+
help="Bug ID to patch (e.g. B-001)",
|
|
66
|
+
)
|
|
67
|
+
parser.add_argument(
|
|
68
|
+
"--summary",
|
|
69
|
+
required=True,
|
|
70
|
+
help="Path to completion-summary.json file",
|
|
71
|
+
)
|
|
72
|
+
return parser.parse_args()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def read_completion_notes(summary_path):
|
|
76
|
+
"""Read completion_notes from a completion-summary.json file.
|
|
77
|
+
|
|
78
|
+
Returns a list of strings, or an empty list if the file is missing
|
|
79
|
+
or malformed.
|
|
80
|
+
"""
|
|
81
|
+
if not os.path.isfile(summary_path):
|
|
82
|
+
LOGGER.warning("Summary file not found: %s", summary_path)
|
|
83
|
+
return []
|
|
84
|
+
|
|
85
|
+
data, err = load_json_file(summary_path)
|
|
86
|
+
if err:
|
|
87
|
+
LOGGER.warning("Failed to read summary: %s", err)
|
|
88
|
+
return []
|
|
89
|
+
|
|
90
|
+
notes = data.get("completion_notes", [])
|
|
91
|
+
if not isinstance(notes, list):
|
|
92
|
+
LOGGER.warning("completion_notes is not a list in %s", summary_path)
|
|
93
|
+
return []
|
|
94
|
+
|
|
95
|
+
# Filter: only keep non-empty strings
|
|
96
|
+
return [n for n in notes if isinstance(n, str) and n.strip()]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def patch_list(list_path, item_id, item_key, notes):
|
|
100
|
+
"""Patch completion_notes into a task list JSON file.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
list_path: Path to the JSON list file
|
|
104
|
+
item_id: ID of the item to patch (e.g. "F-001" or "R-001")
|
|
105
|
+
item_key: Key for the items array (e.g. "features" or "refactors")
|
|
106
|
+
notes: List of completion note strings
|
|
107
|
+
"""
|
|
108
|
+
data, err = load_json_file(list_path)
|
|
109
|
+
if err:
|
|
110
|
+
LOGGER.error("Failed to read list: %s", err)
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
items = data.get(item_key, [])
|
|
114
|
+
found = False
|
|
115
|
+
for item in items:
|
|
116
|
+
if isinstance(item, dict) and item.get("id") == item_id:
|
|
117
|
+
item["completion_notes"] = notes
|
|
118
|
+
found = True
|
|
119
|
+
break
|
|
120
|
+
|
|
121
|
+
if not found:
|
|
122
|
+
LOGGER.error("Item %s not found in %s", item_id, list_path)
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
err = write_json_file(list_path, data)
|
|
126
|
+
if err:
|
|
127
|
+
LOGGER.error("Failed to write list: %s", err)
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
LOGGER.info(
|
|
131
|
+
"Patched %d completion notes for %s in %s",
|
|
132
|
+
len(notes), item_id, list_path,
|
|
133
|
+
)
|
|
134
|
+
return True
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def main():
|
|
138
|
+
args = parse_args()
|
|
139
|
+
|
|
140
|
+
# Determine mode: feature, refactor, or bug
|
|
141
|
+
if args.feature_list and args.feature_id:
|
|
142
|
+
list_path = args.feature_list
|
|
143
|
+
item_id = args.feature_id
|
|
144
|
+
item_key = "features"
|
|
145
|
+
elif args.refactor_list and args.refactor_id:
|
|
146
|
+
list_path = args.refactor_list
|
|
147
|
+
item_id = args.refactor_id
|
|
148
|
+
item_key = "refactors"
|
|
149
|
+
elif args.bug_list and args.bug_id:
|
|
150
|
+
list_path = args.bug_list
|
|
151
|
+
item_id = args.bug_id
|
|
152
|
+
item_key = "bugs"
|
|
153
|
+
else:
|
|
154
|
+
print(
|
|
155
|
+
"Error: must provide either (--feature-list + --feature-id) "
|
|
156
|
+
"or (--refactor-list + --refactor-id) "
|
|
157
|
+
"or (--bug-list + --bug-id)",
|
|
158
|
+
file=sys.stderr,
|
|
159
|
+
)
|
|
160
|
+
sys.exit(1)
|
|
161
|
+
|
|
162
|
+
# Read completion notes
|
|
163
|
+
notes = read_completion_notes(args.summary)
|
|
164
|
+
if not notes:
|
|
165
|
+
LOGGER.info("No completion notes to patch for %s", item_id)
|
|
166
|
+
sys.exit(0)
|
|
167
|
+
|
|
168
|
+
# Patch the list
|
|
169
|
+
if not patch_list(list_path, item_id, item_key, notes):
|
|
170
|
+
sys.exit(1)
|
|
171
|
+
|
|
172
|
+
# Output result
|
|
173
|
+
result = {
|
|
174
|
+
"item_id": item_id,
|
|
175
|
+
"notes_count": len(notes),
|
|
176
|
+
"list_path": os.path.abspath(list_path),
|
|
177
|
+
}
|
|
178
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
if __name__ == "__main__":
|
|
182
|
+
try:
|
|
183
|
+
main()
|
|
184
|
+
except KeyboardInterrupt:
|
|
185
|
+
sys.exit(130)
|
|
186
|
+
except SystemExit:
|
|
187
|
+
raise
|
|
188
|
+
except Exception as exc:
|
|
189
|
+
LOGGER.exception("Unhandled exception")
|
|
190
|
+
print("Error: {}".format(exc), file=sys.stderr)
|
|
191
|
+
sys.exit(1)
|