prizmkit 1.1.61 → 1.1.63
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/bundled/VERSION.json +3 -3
- package/bundled/adapters/codex/settings-adapter.js +1 -0
- package/bundled/adapters/codex/skill-adapter.js +17 -2
- package/bundled/dev-pipeline/lib/common.sh +38 -4
- package/bundled/dev-pipeline/lib/heartbeat.sh +39 -7
- package/bundled/dev-pipeline/run-bugfix.sh +2 -25
- package/bundled/dev-pipeline/run-feature.sh +2 -26
- package/bundled/dev-pipeline/run-refactor.sh +2 -25
- package/bundled/dev-pipeline/scripts/parse-stream-progress.py +96 -0
- package/bundled/skills/_metadata.json +1 -1
- package/package.json +1 -1
- package/src/scaffold.js +117 -1
- package/src/upgrade.js +14 -2
package/bundled/VERSION.json
CHANGED
|
@@ -16,12 +16,27 @@ function toCodexSkillReference(commandName) {
|
|
|
16
16
|
return `PrizmKit skill \`${commandName}\` (\`.agents/skills/${commandName}/SKILL.md\`)`;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function applyCodexInteractionCompatibility(content) {
|
|
20
|
+
const fallbackInstruction = 'Use `request_user_input` when available. If unavailable in the current Codex surface, including Default mode, ask the same options directly in chat and wait for an explicit answer. Tool unavailability is not permission to choose defaults.';
|
|
21
|
+
|
|
22
|
+
return content
|
|
23
|
+
.replace(/\bAskUserQuestion\b/g, 'request_user_input')
|
|
24
|
+
.replace(
|
|
25
|
+
/Do NOT render options as plain text — the user must be able to click\/select\./g,
|
|
26
|
+
fallbackInstruction
|
|
27
|
+
)
|
|
28
|
+
.replace(
|
|
29
|
+
/Do NOT render options as plain text \(e\.g\., `\[A\] option \[B\] option`\) — the user must be able to click\/select, not type a letter\./g,
|
|
30
|
+
fallbackInstruction
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
19
34
|
export function convertSkill(skillContent, skillName) {
|
|
20
35
|
const { frontmatter, body } = parseFrontmatter(skillContent);
|
|
21
36
|
|
|
22
37
|
if (!frontmatter.name) frontmatter.name = skillName;
|
|
23
38
|
|
|
24
|
-
let convertedBody = body.replace(
|
|
39
|
+
let convertedBody = applyCodexInteractionCompatibility(body).replace(
|
|
25
40
|
/\$\{SKILL_DIR\}/g,
|
|
26
41
|
`.agents/skills/${skillName}`
|
|
27
42
|
);
|
|
@@ -34,7 +49,7 @@ export function convertSkill(skillContent, skillName) {
|
|
|
34
49
|
}
|
|
35
50
|
);
|
|
36
51
|
|
|
37
|
-
const codexNote = `> Codex project install: when instructions mention a PrizmKit slash command such as \`/prizmkit-plan\`, read and execute the matching project skill at \`.agents/skills/prizmkit-plan/SKILL.md\`.\n\n`;
|
|
52
|
+
const codexNote = `> Codex project install: when instructions mention a PrizmKit slash command such as \`/prizmkit-plan\`, read and execute the matching project skill at \`.agents/skills/prizmkit-plan/SKILL.md\`.\n>\n> Codex interaction compatibility: when this skill or any referenced PrizmKit file says \`AskUserQuestion\`, use Codex \`request_user_input\` if it is available. If the current Codex surface does not expose \`request_user_input\`, including Default mode, ask the same question directly in chat and wait for explicit user input. Tool unavailability is not permission to choose defaults. If a source instruction asks more questions than Codex allows in one call, split them into multiple calls while preserving order. Only true headless/non-interactive runs, such as \`codex exec\` automation with no conversational user available, may skip interaction and choose documented defaults; never silently choose defaults in an interactive session.\n\n`;
|
|
38
53
|
|
|
39
54
|
return buildMarkdown(frontmatter, codexNote + convertedBody);
|
|
40
55
|
}
|
|
@@ -369,7 +369,23 @@ prizm_start_ai_session() {
|
|
|
369
369
|
"$CLI_CMD" "${claude_args[@]}" > "$log_path" 2>&1 &
|
|
370
370
|
;;
|
|
371
371
|
codex)
|
|
372
|
-
local codex_args=(--ask-for-approval never --sandbox danger-full-access
|
|
372
|
+
local codex_args=(--ask-for-approval never --sandbox danger-full-access)
|
|
373
|
+
local codex_subagent_timeout="${CODEX_SUBAGENT_TIMEOUT_SECONDS:-}"
|
|
374
|
+
if [[ -z "$codex_subagent_timeout" ]]; then
|
|
375
|
+
local outer_stale_threshold="${STALE_KILL_THRESHOLD:-900}"
|
|
376
|
+
if [[ "$outer_stale_threshold" =~ ^[0-9]+$ && "$outer_stale_threshold" -gt 120 ]]; then
|
|
377
|
+
codex_subagent_timeout=$((outer_stale_threshold - 60))
|
|
378
|
+
else
|
|
379
|
+
codex_subagent_timeout=840
|
|
380
|
+
fi
|
|
381
|
+
fi
|
|
382
|
+
if [[ "$codex_subagent_timeout" =~ ^[0-9]+$ && "$codex_subagent_timeout" -gt 0 ]]; then
|
|
383
|
+
codex_args+=(--config "agents.job_max_runtime_seconds=$codex_subagent_timeout")
|
|
384
|
+
fi
|
|
385
|
+
codex_args+=(exec --cd "$PROJECT_ROOT" --skip-git-repo-check)
|
|
386
|
+
if [[ "$USE_STREAM_JSON" == "true" ]]; then
|
|
387
|
+
codex_args+=(--json)
|
|
388
|
+
fi
|
|
373
389
|
if [[ -n "$model" ]]; then
|
|
374
390
|
codex_args+=(--model "$model")
|
|
375
391
|
fi
|
|
@@ -413,7 +429,23 @@ prizm_run_ai_session() {
|
|
|
413
429
|
"$CLI_CMD" "${claude_args[@]}" > "$log_path" 2>&1
|
|
414
430
|
;;
|
|
415
431
|
codex)
|
|
416
|
-
local codex_args=(--ask-for-approval never --sandbox danger-full-access
|
|
432
|
+
local codex_args=(--ask-for-approval never --sandbox danger-full-access)
|
|
433
|
+
local codex_subagent_timeout="${CODEX_SUBAGENT_TIMEOUT_SECONDS:-}"
|
|
434
|
+
if [[ -z "$codex_subagent_timeout" ]]; then
|
|
435
|
+
local outer_stale_threshold="${STALE_KILL_THRESHOLD:-900}"
|
|
436
|
+
if [[ "$outer_stale_threshold" =~ ^[0-9]+$ && "$outer_stale_threshold" -gt 120 ]]; then
|
|
437
|
+
codex_subagent_timeout=$((outer_stale_threshold - 60))
|
|
438
|
+
else
|
|
439
|
+
codex_subagent_timeout=840
|
|
440
|
+
fi
|
|
441
|
+
fi
|
|
442
|
+
if [[ "$codex_subagent_timeout" =~ ^[0-9]+$ && "$codex_subagent_timeout" -gt 0 ]]; then
|
|
443
|
+
codex_args+=(--config "agents.job_max_runtime_seconds=$codex_subagent_timeout")
|
|
444
|
+
fi
|
|
445
|
+
codex_args+=(exec --cd "$PROJECT_ROOT" --skip-git-repo-check)
|
|
446
|
+
if [[ "$USE_STREAM_JSON" == "true" ]]; then
|
|
447
|
+
codex_args+=(--json)
|
|
448
|
+
fi
|
|
417
449
|
if [[ -n "$model" ]]; then
|
|
418
450
|
codex_args+=(--model "$model")
|
|
419
451
|
fi
|
|
@@ -441,10 +473,12 @@ prizm_detect_subagents() {
|
|
|
441
473
|
[[ -f "$session_log" ]] || return 0
|
|
442
474
|
|
|
443
475
|
local count=0
|
|
444
|
-
if [[ "$USE_STREAM_JSON" == "true" ]]; then
|
|
476
|
+
if [[ "$USE_STREAM_JSON" == "true" && "${STREAM_JSON_FORMAT:-}" == "codex-json" ]]; then
|
|
477
|
+
count=$(grep -c '"tool"[[:space:]]*:[[:space:]]*"spawn_agent"' "$session_log" 2>/dev/null) || true
|
|
478
|
+
elif [[ "$USE_STREAM_JSON" == "true" ]]; then
|
|
445
479
|
count=$(grep -c '"name"[[:space:]]*:[[:space:]]*"Agent"' "$session_log" 2>/dev/null) || true
|
|
446
480
|
else
|
|
447
|
-
count=$(grep -cE '(Tool: Agent|"tool":\s*"Agent"|tool_use.*Agent|subagent_type)' "$session_log" 2>/dev/null) || true
|
|
481
|
+
count=$(grep -cE '(Tool: Agent|"tool":\s*"Agent"|"tool":\s*"spawn_agent"|tool_use.*Agent|subagent_type|collab: SpawnAgent)' "$session_log" 2>/dev/null) || true
|
|
448
482
|
fi
|
|
449
483
|
|
|
450
484
|
count=${count:-0}
|
|
@@ -88,17 +88,22 @@ start_heartbeat() {
|
|
|
88
88
|
local stale_mins=$((stale_seconds / 60))
|
|
89
89
|
echo -e " ${RED}[HEARTBEAT]${NC} ${mins}m${secs}s | log: ${size_display} | ${RED}STALE-KILL: no progress for ${stale_mins}m (threshold: ${stale_kill_threshold}s)${NC}"
|
|
90
90
|
echo -e " ${RED}[HEARTBEAT]${NC} Killing AI CLI process $cli_pid (stale session)..."
|
|
91
|
+
# Write the marker before killing. Some CLIs exit quickly, and the
|
|
92
|
+
# parent runner may stop this heartbeat process immediately after
|
|
93
|
+
# wait(1) returns.
|
|
94
|
+
local _marker_dir
|
|
95
|
+
_marker_dir="$(dirname "$session_log")"
|
|
96
|
+
echo "{\"killed_at\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\", \"reason\": \"stale_session\", \"stale_seconds\": $stale_seconds, \"threshold\": $stale_kill_threshold}" > "$_marker_dir/stale-kill.json" 2>/dev/null || true
|
|
91
97
|
kill -TERM "$cli_pid" 2>/dev/null || true
|
|
92
98
|
# Give process 10s to exit gracefully, then force kill
|
|
93
|
-
|
|
99
|
+
local stale_kill_grace_seconds="${STALE_KILL_GRACE_SECONDS:-10}"
|
|
100
|
+
if [[ $stale_kill_grace_seconds -gt 0 ]]; then
|
|
101
|
+
sleep "$stale_kill_grace_seconds"
|
|
102
|
+
fi
|
|
94
103
|
if kill -0 "$cli_pid" 2>/dev/null; then
|
|
95
104
|
echo -e " ${RED}[HEARTBEAT]${NC} Process still alive after SIGTERM, sending SIGKILL..."
|
|
96
105
|
kill -9 "$cli_pid" 2>/dev/null || true
|
|
97
106
|
fi
|
|
98
|
-
# Write stale-kill marker so spawn_and_wait_session knows this wasn't a crash
|
|
99
|
-
local _marker_dir
|
|
100
|
-
_marker_dir="$(dirname "$session_log")"
|
|
101
|
-
echo "{\"killed_at\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\", \"reason\": \"stale_session\", \"stale_seconds\": $stale_seconds, \"threshold\": $stale_kill_threshold}" > "$_marker_dir/stale-kill.json" 2>/dev/null || true
|
|
102
107
|
break
|
|
103
108
|
fi
|
|
104
109
|
|
|
@@ -200,7 +205,7 @@ stop_progress_parser() {
|
|
|
200
205
|
fi
|
|
201
206
|
}
|
|
202
207
|
|
|
203
|
-
# Detect whether the AI CLI supports
|
|
208
|
+
# Detect whether the AI CLI supports structured JSON progress.
|
|
204
209
|
# Sets USE_STREAM_JSON to "true" or "false".
|
|
205
210
|
#
|
|
206
211
|
# Arguments:
|
|
@@ -208,10 +213,35 @@ stop_progress_parser() {
|
|
|
208
213
|
detect_stream_json_support() {
|
|
209
214
|
local cli_cmd="$1"
|
|
210
215
|
USE_STREAM_JSON="false"
|
|
216
|
+
STREAM_JSON_FORMAT=""
|
|
217
|
+
|
|
218
|
+
local session_platform=""
|
|
219
|
+
session_platform="$(_prizm_known_platform_from_cli "$cli_cmd" 2>/dev/null || true)"
|
|
220
|
+
if [[ -z "$session_platform" ]]; then
|
|
221
|
+
case "$(_prizm_normalize_platform "${PRIZMKIT_PLATFORM:-${PLATFORM:-}}")" in
|
|
222
|
+
codex|claude|codebuddy)
|
|
223
|
+
session_platform="$(_prizm_normalize_platform "${PRIZMKIT_PLATFORM:-${PLATFORM:-}}")"
|
|
224
|
+
;;
|
|
225
|
+
esac
|
|
226
|
+
fi
|
|
211
227
|
|
|
212
228
|
# CodeBuddy (cbc) always supports stream-json
|
|
213
|
-
if [[ "$cli_cmd" == "cbc" ]]; then
|
|
229
|
+
if [[ "$session_platform" == "codebuddy" || "$cli_cmd" == "cbc" ]]; then
|
|
214
230
|
USE_STREAM_JSON="true"
|
|
231
|
+
STREAM_JSON_FORMAT="stream-json"
|
|
232
|
+
export USE_STREAM_JSON STREAM_JSON_FORMAT
|
|
233
|
+
return 0
|
|
234
|
+
fi
|
|
235
|
+
|
|
236
|
+
# Codex uses `codex exec --json` JSONL, not `--output-format stream-json`.
|
|
237
|
+
if [[ "$session_platform" == "codex" ]]; then
|
|
238
|
+
local codex_help
|
|
239
|
+
codex_help=$("$cli_cmd" exec --help 2>&1) || true
|
|
240
|
+
if echo "$codex_help" | grep -q -- "--json"; then
|
|
241
|
+
USE_STREAM_JSON="true"
|
|
242
|
+
STREAM_JSON_FORMAT="codex-json"
|
|
243
|
+
fi
|
|
244
|
+
export USE_STREAM_JSON STREAM_JSON_FORMAT
|
|
215
245
|
return 0
|
|
216
246
|
fi
|
|
217
247
|
|
|
@@ -222,5 +252,7 @@ detect_stream_json_support() {
|
|
|
222
252
|
|
|
223
253
|
if echo "$help_output" | grep -q "stream-json"; then
|
|
224
254
|
USE_STREAM_JSON="true"
|
|
255
|
+
STREAM_JSON_FORMAT="stream-json"
|
|
225
256
|
fi
|
|
257
|
+
export USE_STREAM_JSON STREAM_JSON_FORMAT
|
|
226
258
|
}
|
|
@@ -164,31 +164,8 @@ spawn_and_wait_session() {
|
|
|
164
164
|
session_status="timed_out"
|
|
165
165
|
elif [[ "$was_stale_killed" == true ]]; then
|
|
166
166
|
log_warn "Session stale-killed (no progress for ${STALE_KILL_THRESHOLD}s)"
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
has_commits=$(git -C "$project_root" log "${default_branch}..HEAD" --oneline 2>/dev/null | head -1)
|
|
170
|
-
fi
|
|
171
|
-
if [[ -n "$has_commits" ]]; then
|
|
172
|
-
log_info "Stale-killed session has commits — treating as success"
|
|
173
|
-
session_status="success"
|
|
174
|
-
else
|
|
175
|
-
local uncommitted=""
|
|
176
|
-
uncommitted=$(git -C "$project_root" status --porcelain 2>/dev/null | head -1 || true)
|
|
177
|
-
if [[ -n "$uncommitted" ]]; then
|
|
178
|
-
log_warn "Stale-killed session has uncommitted changes — auto-committing..."
|
|
179
|
-
git -C "$project_root" add -A 2>/dev/null || true
|
|
180
|
-
if git -C "$project_root" commit --no-verify -m "chore($bug_id): auto-commit session work (stale-killed)" 2>/dev/null; then
|
|
181
|
-
log_info "Auto-commit succeeded"
|
|
182
|
-
session_status="success"
|
|
183
|
-
else
|
|
184
|
-
log_warn "Auto-commit failed — no changes to commit"
|
|
185
|
-
session_status="crashed"
|
|
186
|
-
fi
|
|
187
|
-
else
|
|
188
|
-
log_warn "Stale-killed session produced no commits and no changes"
|
|
189
|
-
session_status="crashed"
|
|
190
|
-
fi
|
|
191
|
-
fi
|
|
167
|
+
log_warn "Stale-killed sessions are treated as failed; dev branch is preserved for inspection"
|
|
168
|
+
session_status="crashed"
|
|
192
169
|
elif [[ $exit_code -ne 0 ]]; then
|
|
193
170
|
log_warn "Session exited with code $exit_code"
|
|
194
171
|
session_status="crashed"
|
|
@@ -174,32 +174,8 @@ spawn_and_wait_session() {
|
|
|
174
174
|
session_status="timed_out"
|
|
175
175
|
elif [[ "$was_stale_killed" == true ]]; then
|
|
176
176
|
log_warn "Session stale-killed (no progress for ${STALE_KILL_THRESHOLD}s)"
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if git -C "$project_root" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
180
|
-
has_commits=$(git -C "$project_root" log "${default_branch}..HEAD" --oneline 2>/dev/null | head -1)
|
|
181
|
-
fi
|
|
182
|
-
if [[ -n "$has_commits" ]]; then
|
|
183
|
-
log_info "Stale-killed session has commits — treating as success"
|
|
184
|
-
session_status="success"
|
|
185
|
-
else
|
|
186
|
-
local uncommitted=""
|
|
187
|
-
uncommitted=$(git -C "$project_root" status --porcelain 2>/dev/null | head -1 || true)
|
|
188
|
-
if [[ -n "$uncommitted" ]]; then
|
|
189
|
-
log_warn "Stale-killed session has uncommitted changes — auto-committing..."
|
|
190
|
-
git -C "$project_root" add -A 2>/dev/null || true
|
|
191
|
-
if git -C "$project_root" commit --no-verify -m "chore($feature_id): auto-commit session work (stale-killed)" 2>/dev/null; then
|
|
192
|
-
log_info "Auto-commit succeeded"
|
|
193
|
-
session_status="success"
|
|
194
|
-
else
|
|
195
|
-
log_warn "Auto-commit failed — no changes to commit"
|
|
196
|
-
session_status="crashed"
|
|
197
|
-
fi
|
|
198
|
-
else
|
|
199
|
-
log_warn "Stale-killed session produced no commits and no changes"
|
|
200
|
-
session_status="crashed"
|
|
201
|
-
fi
|
|
202
|
-
fi
|
|
177
|
+
log_warn "Stale-killed sessions are treated as failed; dev branch is preserved for inspection"
|
|
178
|
+
session_status="crashed"
|
|
203
179
|
elif [[ $exit_code -ne 0 ]]; then
|
|
204
180
|
log_warn "Session exited with code $exit_code"
|
|
205
181
|
session_status="crashed"
|
|
@@ -166,31 +166,8 @@ spawn_and_wait_session() {
|
|
|
166
166
|
session_status="timed_out"
|
|
167
167
|
elif [[ "$was_stale_killed" == true ]]; then
|
|
168
168
|
log_warn "Session stale-killed (no progress for ${STALE_KILL_THRESHOLD}s)"
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
has_commits=$(git -C "$project_root" log "${default_branch}..HEAD" --oneline 2>/dev/null | head -1)
|
|
172
|
-
fi
|
|
173
|
-
if [[ -n "$has_commits" ]]; then
|
|
174
|
-
log_info "Stale-killed session has commits — treating as success"
|
|
175
|
-
session_status="success"
|
|
176
|
-
else
|
|
177
|
-
local uncommitted=""
|
|
178
|
-
uncommitted=$(git -C "$project_root" status --porcelain 2>/dev/null | head -1 || true)
|
|
179
|
-
if [[ -n "$uncommitted" ]]; then
|
|
180
|
-
log_warn "Stale-killed session has uncommitted changes — auto-committing..."
|
|
181
|
-
git -C "$project_root" add -A 2>/dev/null || true
|
|
182
|
-
if git -C "$project_root" commit --no-verify -m "chore($refactor_id): auto-commit session work (stale-killed)" 2>/dev/null; then
|
|
183
|
-
log_info "Auto-commit succeeded"
|
|
184
|
-
session_status="success"
|
|
185
|
-
else
|
|
186
|
-
log_warn "Auto-commit failed — no changes to commit"
|
|
187
|
-
session_status="crashed"
|
|
188
|
-
fi
|
|
189
|
-
else
|
|
190
|
-
log_warn "Stale-killed session produced no commits and no changes"
|
|
191
|
-
session_status="crashed"
|
|
192
|
-
fi
|
|
193
|
-
fi
|
|
169
|
+
log_warn "Stale-killed sessions are treated as failed; dev branch is preserved for inspection"
|
|
170
|
+
session_status="crashed"
|
|
194
171
|
elif [[ $exit_code -ne 0 ]]; then
|
|
195
172
|
log_warn "Session exited with code $exit_code"
|
|
196
173
|
session_status="crashed"
|
|
@@ -73,6 +73,9 @@ class ProgressTracker:
|
|
|
73
73
|
self.last_text_snippet = ""
|
|
74
74
|
self.is_active = True
|
|
75
75
|
self.errors = []
|
|
76
|
+
self.event_format = ""
|
|
77
|
+
self.active_subagent_count = 0
|
|
78
|
+
self.subagent_status_counts = Counter()
|
|
76
79
|
self._text_buffer = ""
|
|
77
80
|
self._in_tool_use = False
|
|
78
81
|
self._current_tool_input_parts = []
|
|
@@ -87,8 +90,72 @@ class ProgressTracker:
|
|
|
87
90
|
"""
|
|
88
91
|
event_type = event.get("type", "")
|
|
89
92
|
|
|
93
|
+
# ── Codex exec --json JSONL format ──────────────────────────
|
|
94
|
+
if event_type in (
|
|
95
|
+
"thread.started", "turn.started", "turn.completed",
|
|
96
|
+
"turn.failed", "item.started", "item.completed", "error",
|
|
97
|
+
):
|
|
98
|
+
self.event_format = "codex-json"
|
|
99
|
+
self.is_active = True
|
|
100
|
+
|
|
101
|
+
if event_type == "turn.started":
|
|
102
|
+
self.message_count += 1
|
|
103
|
+
|
|
104
|
+
elif event_type in ("item.started", "item.completed"):
|
|
105
|
+
item = event.get("item", {})
|
|
106
|
+
item_type = item.get("type", "")
|
|
107
|
+
|
|
108
|
+
if item_type == "agent_message":
|
|
109
|
+
text = item.get("text", "")
|
|
110
|
+
if text.strip():
|
|
111
|
+
self.last_text_snippet = text.strip()[:120]
|
|
112
|
+
self._detect_phase(text)
|
|
113
|
+
|
|
114
|
+
elif item_type == "collab_tool_call":
|
|
115
|
+
tool_name = item.get("tool", "collab")
|
|
116
|
+
if event_type == "item.started":
|
|
117
|
+
self.current_tool = tool_name
|
|
118
|
+
self.tool_call_counts[tool_name] += 1
|
|
119
|
+
self.total_tool_calls += 1
|
|
120
|
+
elif item.get("status") == "completed":
|
|
121
|
+
self.current_tool = None
|
|
122
|
+
self._extract_tool_summary_from_dict(item)
|
|
123
|
+
self._update_subagent_status_counts(
|
|
124
|
+
item.get("agents_states", {})
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
prompt = item.get("prompt")
|
|
128
|
+
if prompt:
|
|
129
|
+
self._detect_phase(prompt)
|
|
130
|
+
|
|
131
|
+
else:
|
|
132
|
+
tool_name = item.get("tool") or item.get("name")
|
|
133
|
+
if tool_name:
|
|
134
|
+
if event_type == "item.started":
|
|
135
|
+
self.current_tool = tool_name
|
|
136
|
+
self.tool_call_counts[tool_name] += 1
|
|
137
|
+
self.total_tool_calls += 1
|
|
138
|
+
elif item.get("status") == "completed":
|
|
139
|
+
self.current_tool = None
|
|
140
|
+
self._extract_tool_summary_from_dict(item)
|
|
141
|
+
|
|
142
|
+
elif event_type == "turn.completed":
|
|
143
|
+
self.current_tool = None
|
|
144
|
+
|
|
145
|
+
elif event_type == "turn.failed":
|
|
146
|
+
error = event.get("error") or event.get("message") or "Codex turn failed"
|
|
147
|
+
self.errors.append(str(error))
|
|
148
|
+
self.current_tool = None
|
|
149
|
+
|
|
150
|
+
elif event_type == "error":
|
|
151
|
+
error = event.get("error") or event.get("message") or "Unknown error"
|
|
152
|
+
self.errors.append(str(error))
|
|
153
|
+
|
|
154
|
+
return
|
|
155
|
+
|
|
90
156
|
# ── Claude Code verbose format ──────────────────────────────
|
|
91
157
|
if event_type == "assistant":
|
|
158
|
+
self.event_format = self.event_format or "stream-json"
|
|
92
159
|
self.message_count += 1
|
|
93
160
|
self.is_active = True
|
|
94
161
|
message = event.get("message", {})
|
|
@@ -113,16 +180,19 @@ class ProgressTracker:
|
|
|
113
180
|
|
|
114
181
|
elif event_type == "tool_result" or event_type == "user":
|
|
115
182
|
# tool_result contains output from tool execution
|
|
183
|
+
self.event_format = self.event_format or "stream-json"
|
|
116
184
|
self.is_active = True
|
|
117
185
|
|
|
118
186
|
elif event_type == "system":
|
|
119
187
|
# System events (hooks, init, etc.) — track but don't count as messages
|
|
188
|
+
self.event_format = self.event_format or "stream-json"
|
|
120
189
|
subtype = event.get("subtype", "")
|
|
121
190
|
if subtype == "init":
|
|
122
191
|
self.is_active = True
|
|
123
192
|
|
|
124
193
|
# ── Claude API raw stream format ────────────────────────────
|
|
125
194
|
elif event_type == "message_start":
|
|
195
|
+
self.event_format = self.event_format or "stream-json"
|
|
126
196
|
self.message_count += 1
|
|
127
197
|
self.is_active = True
|
|
128
198
|
|
|
@@ -256,14 +326,38 @@ class ProgressTracker:
|
|
|
256
326
|
elif "prompt" in data:
|
|
257
327
|
self.current_tool_input_summary = str(data["prompt"])[:100]
|
|
258
328
|
|
|
329
|
+
def _update_subagent_status_counts(self, agents_states):
|
|
330
|
+
"""Track Codex subagent state counts from collab_tool_call items."""
|
|
331
|
+
counts = Counter()
|
|
332
|
+
active = 0
|
|
333
|
+
if isinstance(agents_states, dict):
|
|
334
|
+
for state in agents_states.values():
|
|
335
|
+
if not isinstance(state, dict):
|
|
336
|
+
continue
|
|
337
|
+
status = str(state.get("status", "unknown"))
|
|
338
|
+
counts[status] += 1
|
|
339
|
+
if status not in ("completed", "failed", "cancelled", "canceled"):
|
|
340
|
+
active += 1
|
|
341
|
+
message = state.get("message")
|
|
342
|
+
if message:
|
|
343
|
+
self.last_text_snippet = str(message).strip()[:120]
|
|
344
|
+
self._detect_phase(str(message))
|
|
345
|
+
self.subagent_status_counts = counts
|
|
346
|
+
self.active_subagent_count = active
|
|
347
|
+
|
|
259
348
|
def to_dict(self):
|
|
260
349
|
"""Export current state as a dictionary for JSON serialization."""
|
|
261
350
|
tool_calls = [
|
|
262
351
|
{"name": name, "count": count}
|
|
263
352
|
for name, count in self.tool_call_counts.most_common()
|
|
264
353
|
]
|
|
354
|
+
subagent_states = [
|
|
355
|
+
{"status": status, "count": count}
|
|
356
|
+
for status, count in self.subagent_status_counts.most_common()
|
|
357
|
+
]
|
|
265
358
|
return {
|
|
266
359
|
"updated_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
360
|
+
"event_format": self.event_format,
|
|
267
361
|
"message_count": self.message_count,
|
|
268
362
|
"current_tool": self.current_tool,
|
|
269
363
|
"current_tool_input_summary": self.current_tool_input_summary,
|
|
@@ -271,6 +365,8 @@ class ProgressTracker:
|
|
|
271
365
|
"detected_phases": self.detected_phases,
|
|
272
366
|
"tool_calls": tool_calls,
|
|
273
367
|
"total_tool_calls": self.total_tool_calls,
|
|
368
|
+
"active_subagent_count": self.active_subagent_count,
|
|
369
|
+
"subagent_states": subagent_states,
|
|
274
370
|
"last_text_snippet": self.last_text_snippet,
|
|
275
371
|
"is_active": self.is_active,
|
|
276
372
|
"errors": self.errors[-10:], # Keep last 10 errors
|
package/package.json
CHANGED
package/src/scaffold.js
CHANGED
|
@@ -33,6 +33,23 @@ import { normalizeRuntime, runtimeLabel, RUNTIME_UNIX, RUNTIME_WINDOWS } from '.
|
|
|
33
33
|
const __scaffoldDirname = dirname(fileURLToPath(import.meta.url));
|
|
34
34
|
const scaffoldPkg = JSON.parse(readFileSync(join(__scaffoldDirname, '..', 'package.json'), 'utf-8'));
|
|
35
35
|
const PIPELINE_INSTALL_EXCLUDE = new Set(['tests', 'docs', '__pycache__', 'node_modules', '.DS_Store']);
|
|
36
|
+
const PIPELINE_MANAGED_DIRS = new Set(['assets', 'lib', 'scripts', 'templates', 'tests', 'docs']);
|
|
37
|
+
const PIPELINE_RUNTIME_PRESERVE_DIRS = new Set([
|
|
38
|
+
'state',
|
|
39
|
+
'logs',
|
|
40
|
+
'runs',
|
|
41
|
+
'sessions',
|
|
42
|
+
'tmp',
|
|
43
|
+
'cache',
|
|
44
|
+
'node_modules',
|
|
45
|
+
'venv',
|
|
46
|
+
'__pycache__',
|
|
47
|
+
]);
|
|
48
|
+
const PIPELINE_MANAGED_TOP_LEVEL_PATTERNS = [
|
|
49
|
+
/^(run|launch|reset|retry)-.+\.(sh|ps1)$/i,
|
|
50
|
+
/^README\.md$/i,
|
|
51
|
+
/^SCHEMA_ANALYSIS\.md$/i,
|
|
52
|
+
];
|
|
36
53
|
|
|
37
54
|
// ============================================================
|
|
38
55
|
// Adapter 动态加载
|
|
@@ -159,6 +176,7 @@ export async function installSkills(platform, skills, projectRoot, dryRun, runti
|
|
|
159
176
|
continue;
|
|
160
177
|
}
|
|
161
178
|
|
|
179
|
+
await fs.remove(targetDir);
|
|
162
180
|
await fs.ensureDir(targetDir);
|
|
163
181
|
|
|
164
182
|
// 读取并写入 SKILL.md(CodeBuddy 格式基本透传)
|
|
@@ -189,17 +207,18 @@ export async function installSkills(platform, skills, projectRoot, dryRun, runti
|
|
|
189
207
|
// so Claude Code shows it as /skillName (not /skillName:skillName).
|
|
190
208
|
// Assets/scripts are copied into a subdirectory for reference.
|
|
191
209
|
const commandsDir = path.join(projectRoot, '.claude', 'commands');
|
|
210
|
+
const assetTargetDir = path.join(projectRoot, '.claude', 'command-assets', skillName);
|
|
192
211
|
if (dryRun) {
|
|
193
212
|
console.log(chalk.gray(` [dry-run] .claude/commands/${skillName}.md`));
|
|
194
213
|
continue;
|
|
195
214
|
}
|
|
196
215
|
await fs.ensureDir(commandsDir);
|
|
197
216
|
await fs.writeFile(path.join(commandsDir, `${skillName}.md`), converted);
|
|
217
|
+
await fs.remove(assetTargetDir);
|
|
198
218
|
|
|
199
219
|
if (skillSubdirs.length > 0) {
|
|
200
220
|
// Place subdirectories outside .claude/commands/ to prevent Claude Code
|
|
201
221
|
// from registering them as slash commands (e.g. /skillName:assets:file).
|
|
202
|
-
const assetTargetDir = path.join(projectRoot, '.claude', 'command-assets', skillName);
|
|
203
222
|
await fs.ensureDir(assetTargetDir);
|
|
204
223
|
for (const subdir of skillSubdirs) {
|
|
205
224
|
await fs.copy(path.join(corePath, subdir), path.join(assetTargetDir, subdir));
|
|
@@ -216,6 +235,7 @@ export async function installSkills(platform, skills, projectRoot, dryRun, runti
|
|
|
216
235
|
continue;
|
|
217
236
|
}
|
|
218
237
|
|
|
238
|
+
await fs.remove(targetDir);
|
|
219
239
|
await fs.ensureDir(targetDir);
|
|
220
240
|
await fs.writeFile(path.join(targetDir, 'SKILL.md'), converted);
|
|
221
241
|
|
|
@@ -556,6 +576,7 @@ project_doc_fallback_filenames = ["CLAUDE.md", "CODEBUDDY.md"]
|
|
|
556
576
|
|
|
557
577
|
[agents]
|
|
558
578
|
max_depth = 1
|
|
579
|
+
job_max_runtime_seconds = 840
|
|
559
580
|
`;
|
|
560
581
|
await fs.writeFile(configPath, configToml);
|
|
561
582
|
await fs.remove(legacySettingsPath);
|
|
@@ -922,6 +943,101 @@ export function resolvePipelineFileList(runtime = 'unix') {
|
|
|
922
943
|
return collectInstallablePipelineFiles(pipelineSource);
|
|
923
944
|
}
|
|
924
945
|
|
|
946
|
+
function normalizePipelineRelPath(relativeFile) {
|
|
947
|
+
return relativeFile.split(path.sep).join('/');
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function isPreservedPipelineRuntimeFile(relativeFile) {
|
|
951
|
+
const normalized = normalizePipelineRelPath(relativeFile);
|
|
952
|
+
const segments = normalized.split('/');
|
|
953
|
+
if (segments.some(segment => !segment || segment === '..')) return true;
|
|
954
|
+
if (segments.some(segment => segment.startsWith('.'))) return true;
|
|
955
|
+
return segments.some(segment => PIPELINE_RUNTIME_PRESERVE_DIRS.has(segment));
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function isLikelyManagedPipelineFile(relativeFile) {
|
|
959
|
+
const normalized = normalizePipelineRelPath(relativeFile);
|
|
960
|
+
const segments = normalized.split('/');
|
|
961
|
+
if (segments.length === 0 || isPreservedPipelineRuntimeFile(normalized)) return false;
|
|
962
|
+
if (segments.length > 1 && PIPELINE_MANAGED_DIRS.has(segments[0])) return true;
|
|
963
|
+
return PIPELINE_MANAGED_TOP_LEVEL_PATTERNS.some(pattern => pattern.test(normalized));
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
async function collectTargetPipelineFiles(dir, base = '') {
|
|
967
|
+
if (!await fs.pathExists(dir)) return [];
|
|
968
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
969
|
+
const files = [];
|
|
970
|
+
for (const entry of entries) {
|
|
971
|
+
const rel = base ? path.join(base, entry.name) : entry.name;
|
|
972
|
+
const fullPath = path.join(dir, entry.name);
|
|
973
|
+
if (entry.isDirectory()) {
|
|
974
|
+
files.push(...await collectTargetPipelineFiles(fullPath, rel));
|
|
975
|
+
} else if (entry.isFile()) {
|
|
976
|
+
files.push(rel);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
return files;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/**
|
|
983
|
+
* Find PrizmKit-owned pipeline files present in the target install that no
|
|
984
|
+
* longer exist in the framework source for the selected runtime.
|
|
985
|
+
*
|
|
986
|
+
* This intentionally avoids arbitrary top-level files and runtime/state
|
|
987
|
+
* directories so user scratch files are preserved.
|
|
988
|
+
*/
|
|
989
|
+
export async function findStaleManagedPipelineFiles(projectRoot, runtime = 'unix') {
|
|
990
|
+
const pipelineTarget = path.join(projectRoot, '.prizmkit', 'dev-pipeline');
|
|
991
|
+
if (!await fs.pathExists(pipelineTarget)) return [];
|
|
992
|
+
|
|
993
|
+
const currentFiles = new Set(
|
|
994
|
+
resolvePipelineFileList(runtime).map(file => normalizePipelineRelPath(file)),
|
|
995
|
+
);
|
|
996
|
+
const targetFiles = await collectTargetPipelineFiles(pipelineTarget);
|
|
997
|
+
|
|
998
|
+
return targetFiles
|
|
999
|
+
.filter(file => !currentFiles.has(normalizePipelineRelPath(file)))
|
|
1000
|
+
.filter(file => isLikelyManagedPipelineFile(file));
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
export async function removeStaleManagedPipelineFiles(projectRoot, staleFiles, dryRun) {
|
|
1004
|
+
if (!staleFiles.length) return 0;
|
|
1005
|
+
|
|
1006
|
+
const pipelineTarget = path.join(projectRoot, '.prizmkit', 'dev-pipeline');
|
|
1007
|
+
const removedDirs = new Set();
|
|
1008
|
+
let removedCount = 0;
|
|
1009
|
+
|
|
1010
|
+
for (const relativeFile of staleFiles) {
|
|
1011
|
+
const targetFile = path.join(pipelineTarget, relativeFile);
|
|
1012
|
+
if (!await fs.pathExists(targetFile)) continue;
|
|
1013
|
+
|
|
1014
|
+
if (dryRun) {
|
|
1015
|
+
console.log(chalk.gray(` [dry-run] remove .prizmkit/dev-pipeline/${normalizePipelineRelPath(relativeFile)}`));
|
|
1016
|
+
} else {
|
|
1017
|
+
await fs.remove(targetFile);
|
|
1018
|
+
console.log(chalk.red(` ✗ removed .prizmkit/dev-pipeline/${normalizePipelineRelPath(relativeFile)}`));
|
|
1019
|
+
removedDirs.add(path.dirname(relativeFile));
|
|
1020
|
+
}
|
|
1021
|
+
removedCount++;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
if (!dryRun) {
|
|
1025
|
+
const dirsByDepth = [...removedDirs]
|
|
1026
|
+
.filter(dir => dir && dir !== '.')
|
|
1027
|
+
.sort((a, b) => b.split(path.sep).length - a.split(path.sep).length);
|
|
1028
|
+
for (const relativeDir of dirsByDepth) {
|
|
1029
|
+
const targetDir = path.join(pipelineTarget, relativeDir);
|
|
1030
|
+
if (!await fs.pathExists(targetDir)) continue;
|
|
1031
|
+
const entries = await fs.readdir(targetDir);
|
|
1032
|
+
if (entries.length === 0) {
|
|
1033
|
+
await fs.remove(targetDir);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
return removedCount;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
925
1041
|
async function pruneStalePipelineRuntimeFiles(pipelineTarget, runtime) {
|
|
926
1042
|
if (!await fs.pathExists(pipelineTarget)) return false;
|
|
927
1043
|
|
package/src/upgrade.js
CHANGED
|
@@ -28,6 +28,8 @@ import {
|
|
|
28
28
|
installGitignore,
|
|
29
29
|
installProjectMemory,
|
|
30
30
|
resolvePipelineFileList,
|
|
31
|
+
findStaleManagedPipelineFiles,
|
|
32
|
+
removeStaleManagedPipelineFiles,
|
|
31
33
|
resolveRuleNamesForRuntime,
|
|
32
34
|
resolveSkillList,
|
|
33
35
|
EXTRAS_REGISTRY,
|
|
@@ -296,6 +298,11 @@ export async function runUpgrade(directory, options = {}) {
|
|
|
296
298
|
}
|
|
297
299
|
|
|
298
300
|
const diff = oldManifest ? diffManifest(oldManifest, newManifest) : { skills: { added: [], removed: [] }, agents: { added: [], removed: [] }, rules: { added: [], removed: [] }, pipeline: { added: [], removed: [] }, extras: { added: [], removed: [] } };
|
|
301
|
+
const removedPipelineSet = new Set(diff.pipeline.removed);
|
|
302
|
+
const staleManagedPipelineFiles = pipeline
|
|
303
|
+
? (await findStaleManagedPipelineFiles(projectRoot, runtime))
|
|
304
|
+
.filter(file => !removedPipelineSet.has(file))
|
|
305
|
+
: [];
|
|
299
306
|
|
|
300
307
|
// 5. Display upgrade summary
|
|
301
308
|
const oldVersion = oldManifest?.version || 'unknown';
|
|
@@ -307,7 +314,7 @@ export async function runUpgrade(directory, options = {}) {
|
|
|
307
314
|
console.log('');
|
|
308
315
|
|
|
309
316
|
const totalAdded = diff.skills.added.length + diff.agents.added.length + diff.rules.added.length + diff.pipeline.added.length + diff.extras.added.length;
|
|
310
|
-
const totalRemoved = diff.skills.removed.length + diff.agents.removed.length + diff.rules.removed.length + diff.pipeline.removed.length + diff.extras.removed.length;
|
|
317
|
+
const totalRemoved = diff.skills.removed.length + diff.agents.removed.length + diff.rules.removed.length + diff.pipeline.removed.length + diff.extras.removed.length + staleManagedPipelineFiles.length;
|
|
311
318
|
const totalUpdated = newSkillList.length + newAgentFiles.length + newRuleFiles.length;
|
|
312
319
|
|
|
313
320
|
if (diff.skills.added.length) console.log(chalk.green(` + Skills added: ${diff.skills.added.join(', ')}`));
|
|
@@ -318,6 +325,7 @@ export async function runUpgrade(directory, options = {}) {
|
|
|
318
325
|
if (diff.rules.removed.length) console.log(chalk.red(` - Rules removed: ${diff.rules.removed.join(', ')}`));
|
|
319
326
|
if (diff.pipeline.added.length) console.log(chalk.green(` + Pipeline files added: ${diff.pipeline.added.length} file(s)`));
|
|
320
327
|
if (diff.pipeline.removed.length) console.log(chalk.red(` - Pipeline files removed: ${diff.pipeline.removed.length} file(s)`));
|
|
328
|
+
if (staleManagedPipelineFiles.length) console.log(chalk.red(` - Stale managed pipeline files: ${staleManagedPipelineFiles.length} file(s)`));
|
|
321
329
|
if (diff.extras.added.length) console.log(chalk.green(` + Extras added: ${diff.extras.added.join(', ')}`));
|
|
322
330
|
if (diff.extras.removed.length) console.log(chalk.red(` - Extras removed: ${diff.extras.removed.join(', ')}`));
|
|
323
331
|
|
|
@@ -344,7 +352,7 @@ export async function runUpgrade(directory, options = {}) {
|
|
|
344
352
|
const platforms = expandPlatforms(platform);
|
|
345
353
|
|
|
346
354
|
// 7a. Remove orphaned files
|
|
347
|
-
if (diff.skills.removed.length || diff.agents.removed.length || diff.rules.removed.length || diff.pipeline.removed.length) {
|
|
355
|
+
if (diff.skills.removed.length || diff.agents.removed.length || diff.rules.removed.length || diff.pipeline.removed.length || staleManagedPipelineFiles.length) {
|
|
348
356
|
console.log(chalk.bold('\n Removing orphaned files...'));
|
|
349
357
|
for (const p of platforms) {
|
|
350
358
|
if (diff.skills.removed.length) {
|
|
@@ -364,6 +372,10 @@ export async function runUpgrade(directory, options = {}) {
|
|
|
364
372
|
console.log(chalk.blue('\n Removed pipeline files:'));
|
|
365
373
|
await removePipelineFiles(projectRoot, diff.pipeline.removed, dryRun);
|
|
366
374
|
}
|
|
375
|
+
if (staleManagedPipelineFiles.length) {
|
|
376
|
+
console.log(chalk.blue('\n Removed stale managed pipeline files:'));
|
|
377
|
+
await removeStaleManagedPipelineFiles(projectRoot, staleManagedPipelineFiles, dryRun);
|
|
378
|
+
}
|
|
367
379
|
}
|
|
368
380
|
|
|
369
381
|
// 7b. Re-install all current files (overwrite mode)
|