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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "frameworkVersion": "1.1.61",
3
- "bundledAt": "2026-06-08T16:20:29.783Z",
4
- "bundledFrom": "819ffc3"
2
+ "frameworkVersion": "1.1.63",
3
+ "bundledAt": "2026-06-08T18:06:47.062Z",
4
+ "bundledFrom": "72acb14"
5
5
  }
@@ -20,6 +20,7 @@ project_doc_fallback_filenames = ["CLAUDE.md", "CODEBUDDY.md"]
20
20
 
21
21
  [agents]
22
22
  max_depth = 1
23
+ job_max_runtime_seconds = 840
23
24
  `;
24
25
 
25
26
  await writeFile(configPath, configToml);
@@ -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 exec --cd "$PROJECT_ROOT" --skip-git-repo-check)
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 exec --cd "$PROJECT_ROOT" --skip-git-repo-check)
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
- sleep 10
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 --output-format stream-json.
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
- local has_commits=""
168
- if git -C "$project_root" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
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
- # Treat stale-killed as potentially successful check for commits
178
- local has_commits=""
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
- local has_commits=""
170
- if git -C "$project_root" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
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
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.1.61",
2
+ "version": "1.1.63",
3
3
  "skills": {
4
4
  "prizm-kit": {
5
5
  "description": "Full-lifecycle dev toolkit. Covers spec-driven development, Prizm context docs, code quality, debugging, deployment, and knowledge management.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prizmkit",
3
- "version": "1.1.61",
3
+ "version": "1.1.63",
4
4
  "description": "Create a new PrizmKit-powered project with clean initialization — no framework dev files, just what you need.",
5
5
  "type": "module",
6
6
  "bin": {
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)