prizmkit 1.1.143 → 1.1.144

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/adapters/pi/paths.js +9 -0
  3. package/bundled/adapters/pi/rules-adapter.js +13 -0
  4. package/bundled/adapters/pi/settings-adapter.js +23 -0
  5. package/bundled/adapters/pi/skill-adapter.js +18 -0
  6. package/bundled/dev-pipeline/prizmkit_runtime/config.py +15 -3
  7. package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +3 -1
  8. package/bundled/dev-pipeline/prizmkit_runtime/platform_detection.py +10 -2
  9. package/bundled/dev-pipeline/prizmkit_runtime/runner_bookkeeping.py +1 -0
  10. package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +1 -0
  11. package/bundled/dev-pipeline/prizmkit_runtime/sessions.py +26 -2
  12. package/bundled/dev-pipeline/scripts/parse-stream-progress.py +61 -7
  13. package/bundled/dev-pipeline/tests/test_unified_cli.py +88 -0
  14. package/bundled/skills/_metadata.json +1 -1
  15. package/bundled/skills/prizmkit-code-review/SKILL.md +3 -3
  16. package/bundled/skills/prizmkit-code-review/references/independent-code-review.md +63 -58
  17. package/bundled/skills/prizmkit-code-review/references/review-report-template.md +8 -1
  18. package/bundled/skills/prizmkit-plan/SKILL.md +3 -3
  19. package/bundled/skills/prizmkit-plan/assets/plan-template.md +3 -0
  20. package/bundled/skills/prizmkit-plan/references/independent-plan-review.md +55 -52
  21. package/bundled/skills/prizmkit-test/SKILL.md +3 -3
  22. package/bundled/skills/prizmkit-test/references/independent-test-review.md +68 -69
  23. package/bundled/skills/prizmkit-test/references/test-report-template.md +5 -1
  24. package/package.json +1 -1
  25. package/src/ai-cli-launch.js +1 -1
  26. package/src/clean.js +13 -4
  27. package/src/config.js +32 -5
  28. package/src/detect-platform.js +11 -7
  29. package/src/index.js +5 -0
  30. package/src/platforms.js +6 -2
  31. package/src/prompts.js +12 -4
  32. package/src/scaffold.js +72 -20
  33. package/src/upgrade.js +12 -5
@@ -1,5 +1,5 @@
1
1
  {
2
- "frameworkVersion": "1.1.143",
3
- "bundledAt": "2026-07-21T14:41:54.248Z",
4
- "bundledFrom": "1e6fbd1"
2
+ "frameworkVersion": "1.1.144",
3
+ "bundledAt": "2026-07-22T02:07:34.475Z",
4
+ "bundledFrom": "a2ab913"
5
5
  }
@@ -0,0 +1,9 @@
1
+ /** Pi platform path constants and conventions. */
2
+ export const PLATFORM_ID = 'pi';
3
+ export const CLI_COMMAND = 'pi';
4
+ export const SKILLS_DIR = '.pi/skills';
5
+ export const RULES_DIR = '.pi/rules';
6
+ export const SETTINGS_FILE = '.pi/settings.json';
7
+ export const PROJECT_MEMORY_FILE = '.pi/APPEND_SYSTEM.md';
8
+ export const SKILL_DEFINITION_FILE = 'SKILL.md';
9
+ export const SKILL_DIR_VAR = '${SKILL_DIR}';
@@ -0,0 +1,13 @@
1
+ /** Install behavioral Markdown guidance for Pi project sessions. */
2
+ import { mkdirSync } from 'node:fs';
3
+ import { writeFile } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+
6
+ export async function installRules(targetRoot, ruleFiles) {
7
+ const rulesDir = path.join(targetRoot, '.pi', 'rules');
8
+ mkdirSync(rulesDir, { recursive: true });
9
+ for (const rule of ruleFiles) {
10
+ const baseName = path.basename(rule.name);
11
+ await writeFile(path.join(rulesDir, `${baseName}.md`), rule.content);
12
+ }
13
+ }
@@ -0,0 +1,23 @@
1
+ /** Merge the minimal PrizmKit settings required by Pi. */
2
+ import { existsSync, mkdirSync } from 'node:fs';
3
+ import { readFile, writeFile } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+
6
+ export async function generateSettings(targetRoot) {
7
+ const settingsPath = path.join(targetRoot, '.pi', 'settings.json');
8
+ mkdirSync(path.dirname(settingsPath), { recursive: true });
9
+ let existing = {};
10
+ if (existsSync(settingsPath)) {
11
+ const raw = await readFile(settingsPath, 'utf8');
12
+ try {
13
+ existing = JSON.parse(raw);
14
+ } catch (error) {
15
+ throw new Error(`Refusing to overwrite malformed Pi settings at .pi/settings.json: ${error.message}`);
16
+ }
17
+ if (!existing || Array.isArray(existing) || typeof existing !== 'object') {
18
+ throw new Error('Refusing to overwrite Pi settings because .pi/settings.json is not a JSON object.');
19
+ }
20
+ }
21
+ existing.enableSkillCommands = true;
22
+ await writeFile(settingsPath, `${JSON.stringify(existing, null, 2)}\n`);
23
+ }
@@ -0,0 +1,18 @@
1
+ /** Convert canonical PrizmKit skills to Pi project skills. */
2
+ import { parseFrontmatter, buildMarkdown } from '../shared/frontmatter.js';
3
+
4
+ function applyPiInteractionCompatibility(content) {
5
+ return content.replace(
6
+ /\bAskUserQuestion\b/g,
7
+ 'ask the same question directly in chat and wait for explicit user input',
8
+ );
9
+ }
10
+
11
+ export function convertSkill(skillContent, skillName) {
12
+ const { frontmatter, body } = parseFrontmatter(skillContent);
13
+ if (!frontmatter.name) frontmatter.name = skillName;
14
+ const convertedBody = applyPiInteractionCompatibility(body)
15
+ .replace(/\$\{SKILL_DIR\}/g, `.pi/skills/${skillName}`);
16
+ const note = `> Pi project install: invoke this skill with \`/skill:${skillName}\`. When instructions mention a PrizmKit slash command such as \`/prizmkit-plan\`, load and execute the matching project skill with \`/skill:prizmkit-plan\` from \`.pi/skills/prizmkit-plan/SKILL.md\`.\n>\n> Pi interaction compatibility: in interactive sessions, ask required questions directly in chat and wait for explicit user input. Only true headless runs may use documented conservative defaults.\n\n`;
17
+ return buildMarkdown(frontmatter, note + convertedBody);
18
+ }
@@ -76,12 +76,14 @@ AI_CLIENT_PRECEDENCE = (
76
76
  "PATH:claude",
77
77
  "PATH:cbc",
78
78
  "PATH:codex",
79
+ "PATH:pi",
79
80
  )
80
81
 
81
82
  _EFFORT_VALUES_BY_PLATFORM = {
82
83
  "claude": ("low", "medium", "high", "xhigh", "max"),
83
84
  "codex": ("low", "medium", "high", "xhigh"),
84
85
  "codebuddy": ("low", "medium", "high", "xhigh"),
86
+ "pi": ("off", "minimal", "low", "medium", "high", "xhigh", "max"),
85
87
  }
86
88
 
87
89
 
@@ -169,10 +171,12 @@ def _cli_names_for_platform(platform: str | None) -> tuple[str, ...]:
169
171
  return ("claude",)
170
172
  if normalized == "codebuddy":
171
173
  return ("cbc",)
174
+ if normalized == "pi":
175
+ return ("pi",)
172
176
  if normalized == "both":
173
177
  return ("claude", "cbc")
174
178
  if normalized == "all":
175
- return ("codex", "claude", "cbc")
179
+ return ("codex", "claude", "cbc", "pi")
176
180
  return ()
177
181
 
178
182
 
@@ -188,6 +192,8 @@ def _known_platform_from_cli(cli_command: str | None) -> str | None:
188
192
  return "claude"
189
193
  if name == "codex":
190
194
  return "codex"
195
+ if name == "pi":
196
+ return "pi"
191
197
  if name in {"cbc", "codebuddy"}:
192
198
  return "codebuddy"
193
199
  return None
@@ -195,7 +201,7 @@ def _known_platform_from_cli(cli_command: str | None) -> str | None:
195
201
 
196
202
  def _concrete_platform(platform: str | None) -> str | None:
197
203
  normalized = _normalize_platform(platform)
198
- return normalized if normalized in {"claude", "codebuddy", "codex"} else None
204
+ return normalized if normalized in {"claude", "codebuddy", "codex", "pi"} else None
199
205
 
200
206
 
201
207
  def _resolve_cli_name(cli_name: str, env: Mapping[str, str]) -> str | None:
@@ -269,6 +275,10 @@ def _installed_platform_cli(
269
275
  paths: RuntimePaths,
270
276
  env: Mapping[str, str],
271
277
  ) -> tuple[str | None, str | None]:
278
+ if (paths.project_root / ".pi" / "skills" / "prizmkit" / "SKILL.md").is_file():
279
+ resolved = _resolve_cli_name("pi", env)
280
+ if resolved:
281
+ return resolved, "installed-platform:pi"
272
282
  if (paths.project_root / ".agents" / "skills" / "prizmkit" / "SKILL.md").is_file():
273
283
  resolved = _resolve_cli_name("codex", env)
274
284
  if resolved:
@@ -379,7 +389,7 @@ def _resolve_ai_client(paths: RuntimePaths, env: Mapping[str, str]) -> AIClientR
379
389
  precedence=AI_CLIENT_PRECEDENCE,
380
390
  )
381
391
 
382
- for candidate in ("claude", "cbc", "codex"):
392
+ for candidate in ("claude", "cbc", "codex", "pi"):
383
393
  resolved = _resolve_cli_name(candidate, env)
384
394
  if resolved:
385
395
  return AIClientResolution(
@@ -424,6 +434,7 @@ def validate_effort(effort: str | None, platform: str | None) -> EffortValidatio
424
434
  "claude": "Claude Code",
425
435
  "codex": "Codex",
426
436
  "codebuddy": "CodeBuddy",
437
+ "pi": "Pi",
427
438
  }.get(_concrete_platform(platform) or "codebuddy", "CodeBuddy")
428
439
  message = (
429
440
  f"PRIZMKIT_EFFORT='{normalized_effort}' is not supported by the detected CLI "
@@ -453,6 +464,7 @@ def load_runtime_config(paths: RuntimePaths, env: Mapping[str, str] | None = Non
453
464
  paths.project_root / ".codebuddy",
454
465
  paths.project_root / ".claude",
455
466
  paths.project_root / ".codex",
467
+ paths.project_root / ".pi",
456
468
  ),
457
469
  path_entries=_path_entries(runtime_env),
458
470
  sources=("environment", "prizmkit-env", "prizmkit-config", "installed-platform-markers", "PATH"),
@@ -33,6 +33,8 @@ HIDDEN_TOOL_WORKTREE_EXCLUDES = (
33
33
  ":(top,exclude,glob).codex/**",
34
34
  ":(top,exclude).agents",
35
35
  ":(top,exclude,glob).agents/**",
36
+ ":(top,exclude).pi",
37
+ ":(top,exclude,glob).pi/**",
36
38
  )
37
39
 
38
40
  UNTRACKED_TRANSIENT_EXCLUDES = (
@@ -45,7 +47,7 @@ UNTRACKED_TRANSIENT_EXCLUDES = (
45
47
  PIPELINE_FALLBACK_GIT_NAME = "PrizmKit Pipeline"
46
48
  PIPELINE_FALLBACK_GIT_EMAIL = "prizmkit-pipeline@example.invalid"
47
49
 
48
- WORKTREE_PLATFORM_SUPPORT_DIRS = (".claude", ".codebuddy", ".codex", ".agents")
50
+ WORKTREE_PLATFORM_SUPPORT_DIRS = (".claude", ".codebuddy", ".codex", ".agents", ".pi")
49
51
  WORKTREE_ROOT_SUPPORT_FILES = ("skills-lock.json", "AGENTS.md")
50
52
  WORKTREE_PRIZMKIT_SUPPORT_ENTRIES = ("prizm-docs", "config.json", "manifest.json")
51
53
  WORKTREE_SUPPORT_IGNORE_NAMES = {"__pycache__", ".DS_Store", "settings.local.json"}
@@ -8,7 +8,7 @@ from collections.abc import Mapping
8
8
  from dataclasses import dataclass
9
9
  from pathlib import Path
10
10
 
11
- SUPPORTED_PLATFORMS = ("claude", "codex", "codebuddy")
11
+ SUPPORTED_PLATFORMS = ("claude", "codex", "codebuddy", "pi")
12
12
 
13
13
 
14
14
  @dataclass(frozen=True)
@@ -44,6 +44,7 @@ _PLATFORM_MARKERS = {
44
44
  "claude": (".claude/commands", ".claude/skills"),
45
45
  "codex": (".agents/skills",),
46
46
  "codebuddy": (".codebuddy/skills",),
47
+ "pi": (".pi/skills",),
47
48
  }
48
49
 
49
50
 
@@ -63,6 +64,11 @@ _PLATFORM_SKILL_PATTERNS = {
63
64
  ("home-skill", "~/.codebuddy/skills/{skill}"),
64
65
  ("legacy-home-skill", "~/.cbc/skills/{skill}"),
65
66
  ),
67
+ "pi": (
68
+ ("project-skill", ".pi/skills/{skill}"),
69
+ ("project-agent-skill", ".agents/skills/{skill}"),
70
+ ("home-skill", "~/.pi/agent/skills/{skill}"),
71
+ ),
66
72
  }
67
73
 
68
74
 
@@ -88,6 +94,8 @@ def platform_from_command(command: object) -> str:
88
94
  return "codex"
89
95
  if base in ("cbc", "codebuddy") or "codebuddy" in base:
90
96
  return "codebuddy"
97
+ if base == "pi":
98
+ return "pi"
91
99
  return ""
92
100
 
93
101
 
@@ -144,7 +152,7 @@ def resolve_platform(project_root: str | Path, env: Mapping[str, str] | None = N
144
152
  "unresolved",
145
153
  (
146
154
  "No supported platform marker found. Set PRIZMKIT_PLATFORM, configure .prizmkit/config.json, "
147
- "or install Claude/Codex/CodeBuddy platform files.",
155
+ "or install Claude/Codex/CodeBuddy/Pi platform files.",
148
156
  ),
149
157
  )
150
158
 
@@ -27,6 +27,7 @@ LOCAL_ONLY_PLATFORM_PATHS = (
27
27
  ".codebuddy/settings.local.json",
28
28
  ".codex/settings.local.json",
29
29
  ".agents/settings.local.json",
30
+ ".pi/settings.local.json",
30
31
  )
31
32
  PIPELINE_MANIFEST_PATH = ".prizmkit/manifest.json"
32
33
  CONFLICT_MARKERS = (b"<<<<<<< ", b"=======", b">>>>>>> ")
@@ -28,6 +28,7 @@ class RecoveryDetectorResolver:
28
28
  project / ".agents" / "skills" / "recovery-workflow" / "scripts" / "detect-recovery-state.py",
29
29
  project / ".claude" / "command-assets" / "recovery-workflow" / "scripts" / "detect-recovery-state.py",
30
30
  project / ".codebuddy" / "skills" / "recovery-workflow" / "scripts" / "detect-recovery-state.py",
31
+ project / ".pi" / "skills" / "recovery-workflow" / "scripts" / "detect-recovery-state.py",
31
32
  project
32
33
  / "core"
33
34
  / "skills"
@@ -113,7 +113,7 @@ HelpRunner = Callable[[Sequence[str]], str]
113
113
 
114
114
  def _normalize_platform(platform: str | None) -> str:
115
115
  normalized = (platform or "").strip().lower()
116
- return normalized if normalized in {"claude", "codex", "codebuddy", "custom"} else "codebuddy"
116
+ return normalized if normalized in {"claude", "codex", "codebuddy", "pi", "custom"} else "codebuddy"
117
117
 
118
118
 
119
119
  def _prompt_text(path: Path) -> str:
@@ -139,9 +139,16 @@ def detect_stream_json_support(
139
139
  """Detect stream-json support using shell-compatible provider rules."""
140
140
  normalized = _normalize_platform(platform)
141
141
  command_name = Path(str(cli_command).split()[0].strip("'\"")).name.lower()
142
+ runner = help_runner or _default_help_runner
142
143
  if normalized == "codebuddy" or command_name == "cbc":
143
144
  return StreamJsonSupport(True, "stream-json", "codebuddy-default")
144
- runner = help_runner or _default_help_runner
145
+ if normalized == "pi" or command_name == "pi":
146
+ help_output = runner((cli_command, "--help"))
147
+ required = ("--mode", "--approve", "--no-session", "--tools", "--model", "--thinking")
148
+ missing = tuple(flag for flag in required if flag not in help_output)
149
+ if not missing:
150
+ return StreamJsonSupport(True, "pi-json", "pi-help")
151
+ return StreamJsonSupport(False, "", "pi-help", tuple(f"{flag} not found" for flag in missing))
145
152
  if normalized == "codex":
146
153
  help_output = runner((cli_command, "exec", "--help"))
147
154
  if "--json" in help_output:
@@ -168,6 +175,23 @@ def build_ai_session_command(config: AISessionConfig) -> AISessionCommand:
168
175
  argv.extend(config.launch_profile.extra_args)
169
176
  argv.extend(config.extra_args)
170
177
  return AISessionCommand(tuple(argv), None, "custom", "argument", prompt)
178
+ if platform == "pi":
179
+ argv = [
180
+ config.cli_command,
181
+ "--mode",
182
+ "json",
183
+ "--approve",
184
+ "--no-session",
185
+ "--tools",
186
+ "read,bash,edit,write,grep,find,ls",
187
+ ]
188
+ if config.model:
189
+ argv.extend(["--model", config.model])
190
+ if config.effort:
191
+ argv.extend(["--thinking", config.effort])
192
+ argv.extend(config.extra_args)
193
+ return AISessionCommand(tuple(argv), prompt, "pi", "stdin", prompt)
194
+
171
195
  if platform == "claude":
172
196
  argv = [
173
197
  config.cli_command,
@@ -258,13 +258,7 @@ class ProgressTracker:
258
258
  def process_event(self, event):
259
259
  """Process a single stream-json event and update state.
260
260
 
261
- Supports three formats:
262
- 1. Claude API raw stream: message_start, content_block_start, content_block_delta, etc.
263
- 2. Claude Code verbose stream-json: assistant, user, tool_result, system, etc.
264
- (produced by claude/claude-internal --verbose --output-format stream-json)
265
- 3. CodeBuddy stream-json: message/function_call/function_call_result events
266
- (produced by cbc --print -y --output-format stream-json — message type
267
- with sessionId/cwd metadata, function_call for tool invocations).
261
+ Supports Claude API/CLI, CodeBuddy, Codex JSONL, and Pi JSONL formats.
268
262
  """
269
263
  event_type = event.get("type", "")
270
264
  if event_type:
@@ -288,6 +282,66 @@ class ProgressTracker:
288
282
 
289
283
  self._record_structured_subagent_metadata(event)
290
284
 
285
+ # ── Pi --mode json JSONL format ─────────────────────────────
286
+ pi_event_types = {
287
+ "session", "agent_start", "agent_end", "turn_start", "turn_end",
288
+ "message_start", "message_update", "message_end",
289
+ "tool_execution_start", "tool_execution_update", "tool_execution_end",
290
+ "queue_update", "compaction_start", "compaction_end",
291
+ "auto_retry_start", "auto_retry_end",
292
+ }
293
+ if event_type == "session" or (self.event_format == "pi-json" and event_type in pi_event_types):
294
+ self.event_format = "pi-json"
295
+ if event_type in {"agent_start", "turn_start", "message_start", "message_update", "tool_execution_start", "tool_execution_update", "compaction_start", "auto_retry_start"}:
296
+ self.is_active = True
297
+ if event_type == "turn_start":
298
+ self.message_count += 1
299
+ elif event_type in {"message_update", "message_end"}:
300
+ assistant_event = event.get("assistantMessageEvent")
301
+ text = ""
302
+ if isinstance(assistant_event, dict):
303
+ text = str(assistant_event.get("delta") or assistant_event.get("text") or "")
304
+ if not text:
305
+ message = event.get("message")
306
+ if isinstance(message, dict):
307
+ content = message.get("content")
308
+ if isinstance(content, str):
309
+ text = content
310
+ elif isinstance(content, list):
311
+ text = "\n".join(
312
+ str(item.get("text") or "")
313
+ for item in content if isinstance(item, dict)
314
+ )
315
+ if text.strip():
316
+ self.last_text_snippet = text.strip()[-120:]
317
+ self._detect_phase(text)
318
+ self._detect_terminal_error(text, require_error_context=True)
319
+ elif event_type == "tool_execution_start":
320
+ tool_name = str(event.get("toolName") or "unknown")
321
+ self.current_tool = tool_name
322
+ self.tool_call_counts[tool_name] += 1
323
+ self.total_tool_calls += 1
324
+ args = event.get("args")
325
+ if isinstance(args, dict):
326
+ self._extract_tool_summary_from_dict(args)
327
+ self._detect_phase(json.dumps(args, ensure_ascii=False)[:500])
328
+ elif event_type == "tool_execution_end":
329
+ self.current_tool = None
330
+ if event.get("isError"):
331
+ result = event.get("result")
332
+ error_text = json.dumps(result, ensure_ascii=False, default=str)
333
+ self.errors.append(error_text[:500])
334
+ self._detect_terminal_error(error_text, require_error_context=True)
335
+ elif event_type in {"compaction_end", "auto_retry_end"}:
336
+ error_text = str(event.get("errorMessage") or event.get("finalError") or "")
337
+ if error_text:
338
+ self.errors.append(error_text[:500])
339
+ self._detect_terminal_error(error_text, require_error_context=True)
340
+ elif event_type == "agent_end":
341
+ self.current_tool = None
342
+ self.is_active = False
343
+ return
344
+
291
345
  # ── Codex exec --json JSONL format ──────────────────────────
292
346
  if event_type in (
293
347
  "thread.started", "turn.started", "turn.completed",
@@ -547,6 +547,30 @@ def test_runtime_config_loads_prizmkit_env_and_validates_effort(tmp_path):
547
547
  assert config.effort_validation.valid is True
548
548
  assert validate_effort("max", "codex").valid is False
549
549
  assert "Codex" in validate_effort("max", "codex").message
550
+ assert validate_effort("max", "pi").valid is True
551
+ assert validate_effort("minimal", "pi").valid is True
552
+
553
+
554
+ def test_runtime_config_resolves_pi_from_config_and_installed_marker(tmp_path):
555
+ from prizmkit_runtime.config import resolve_ai_client
556
+ from prizmkit_runtime.paths import resolve_runtime_paths
557
+
558
+ project = tmp_path / "project"
559
+ pipeline = project / ".prizmkit" / "dev-pipeline"
560
+ pipeline.mkdir(parents=True)
561
+ (project / ".prizmkit" / "config.json").write_text('{"ai_cli":"pi"}', encoding="utf-8")
562
+ paths = resolve_runtime_paths(pipeline_root=pipeline)
563
+ configured = resolve_ai_client(paths, env={"PATH": ""})
564
+ assert configured.command == "pi"
565
+ assert configured.platform == "pi"
566
+ assert configured.launch_profile is None
567
+
568
+ (project / ".prizmkit" / "config.json").unlink()
569
+ marker = project / ".pi" / "skills" / "prizmkit"
570
+ marker.mkdir(parents=True)
571
+ (marker / "SKILL.md").write_text("---\nname: prizmkit\ndescription: test\n---\n", encoding="utf-8")
572
+ unresolved = resolve_ai_client(paths, env={"PATH": ""})
573
+ assert unresolved.command is None
550
574
 
551
575
 
552
576
  def test_stream_json_support_detection_matches_provider_rules():
@@ -557,16 +581,21 @@ def test_stream_json_support_detection_matches_provider_rules():
557
581
  return "Usage: codex exec --json"
558
582
  if tuple(command) == ("claude", "--help"):
559
583
  return "--output-format stream-json"
584
+ if tuple(command) == ("pi", "--help"):
585
+ return "--mode --approve --no-session --tools --model --thinking"
560
586
  return ""
561
587
 
562
588
  codebuddy = detect_stream_json_support("cbc", "codebuddy", help_runner)
563
589
  codex = detect_stream_json_support("codex", "codex", help_runner)
564
590
  claude = detect_stream_json_support("claude", "claude", help_runner)
591
+ pi = detect_stream_json_support("pi", "pi", help_runner)
565
592
  unsupported = detect_stream_json_support("other", "claude", lambda _cmd: "plain help")
566
593
 
567
594
  assert codebuddy.enabled and codebuddy.format == "stream-json"
568
595
  assert codex.enabled and codex.format == "codex-json"
569
596
  assert claude.enabled and claude.format == "stream-json"
597
+ assert pi.enabled and pi.format == "pi-json"
598
+ assert detect_stream_json_support("pi", "pi", lambda _cmd: "--mode").enabled is False
570
599
  assert unsupported.enabled is False
571
600
 
572
601
 
@@ -628,6 +657,27 @@ def test_ai_session_command_builders_match_unix_runtime_snapshot(tmp_path):
628
657
  assert codebuddy.argv[1:] == expected_codebuddy
629
658
  assert codebuddy.stdin_text == "hello"
630
659
 
660
+ pi = build_ai_session_command(
661
+ AISessionConfig(
662
+ cli_command="pi",
663
+ platform="pi",
664
+ model="anthropic/sonnet",
665
+ prompt_path=prompt,
666
+ cwd=project,
667
+ effort="max",
668
+ use_stream_json=True,
669
+ )
670
+ )
671
+ assert pi.argv == (
672
+ "pi", "--mode", "json", "--approve", "--no-session", "--tools",
673
+ "read,bash,edit,write,grep,find,ls", "--model", "anthropic/sonnet",
674
+ "--thinking", "max",
675
+ )
676
+ assert pi.stdin_text == "hello"
677
+ assert pi.prompt_mode == "stdin"
678
+ assert "hello" not in pi.argv
679
+ assert not ({"--effort", "--sandbox", "--output-format", "--disallowedTools", "-y"} & set(pi.argv))
680
+
631
681
 
632
682
  def test_custom_ai_session_command_builder_injects_prompt_and_tokenizes_extra_args(tmp_path):
633
683
  from prizmkit_runtime.launch_profiles import HeadlessLaunchProfile
@@ -717,6 +767,20 @@ def test_start_command_serialization_redacts_prompt_and_preserves_provider_flags
717
767
  assert codex.endswith(" -")
718
768
  assert secret_prompt not in codex
719
769
 
770
+ pi = format_start_command(
771
+ build_ai_session_command(
772
+ AISessionConfig(
773
+ cli_command="pi",
774
+ platform="pi",
775
+ model=None,
776
+ prompt_path=prompt,
777
+ cwd=project,
778
+ )
779
+ )
780
+ )
781
+ assert pi.startswith("pi --mode json --approve --no-session")
782
+ assert secret_prompt not in pi
783
+
720
784
  custom = format_start_command(
721
785
  build_ai_session_command(
722
786
  AISessionConfig(
@@ -877,6 +941,30 @@ def test_progress_parser_adds_required_metadata(tmp_path):
877
941
 
878
942
 
879
943
 
944
+ def test_progress_parser_normalizes_pi_json_events():
945
+ module = _load_progress_parser_module()
946
+ tracker = module.ProgressTracker()
947
+ tracker.process_event({"type": "session", "version": 3, "id": "pi-session", "cwd": "/tmp/project"})
948
+ tracker.process_event({"type": "agent_start"})
949
+ tracker.process_event({"type": "turn_start"})
950
+ tracker.process_event({
951
+ "type": "message_update",
952
+ "assistantMessageEvent": {"type": "text_delta", "delta": "Running prizmkit-implement"},
953
+ })
954
+ tracker.process_event({"type": "tool_execution_start", "toolName": "edit", "args": {"path": "src/a.js"}})
955
+ tracker.process_event({"type": "tool_execution_end", "toolName": "edit", "result": {"ok": True}, "isError": False})
956
+ tracker.process_event({"type": "compaction_start", "reason": "threshold"})
957
+ tracker.process_event({"type": "compaction_end", "reason": "threshold", "aborted": False, "willRetry": False})
958
+ tracker.process_event({"type": "agent_end", "messages": []})
959
+ data = tracker.to_dict()
960
+ assert data["event_format"] == "pi-json"
961
+ assert data["message_count"] == 1
962
+ assert data["total_tool_calls"] == 1
963
+ assert {item["name"]: item["count"] for item in data["tool_calls"]}["edit"] == 1
964
+ assert data["current_phase"] == "implement"
965
+ assert data["is_active"] is False
966
+
967
+
880
968
  def test_progress_parser_normalizes_claude_subagents_without_double_counting(tmp_path):
881
969
  module = _load_progress_parser_module()
882
970
  tracker = module.ProgressTracker()
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.1.143",
2
+ "version": "1.1.144",
3
3
  "skills": {
4
4
  "prizmkit": {
5
5
  "description": "Framework introduction and navigation for the formal single-requirement lifecycle, project initialization, Prizm docs, and independent deployment.",
@@ -119,11 +119,11 @@ Run only after the Main-Agent review and repair loop converges with no unresolve
119
119
 
120
120
  1. Read `${SKILL_DIR}/references/independent-code-review.md` and follow its complete contract.
121
121
  2. Apply its all-or-nothing semantic Host Capability Gate. If any required structural capability is unavailable or unproven, create no Reviewer, append `independent-review-downgrade`, and preserve the valid completed Main-Agent review.
122
- 3. When the gate passes, capture bounded immutable input for the complete current change and create exactly one Code Reviewer with the reference's Initial Reviewer Prompt.
122
+ 3. When the gate passes, provide exact artifact/spec/plan paths and contents, Main-Agent-captured complete current changes, implementation context, and verification results to one active Code Reviewer with the reference's Initial Reviewer Prompt.
123
123
  4. Use maximum five Reviewer responses. Append `independent-review-round` and `independent-adjudication` events. The Main Agent adjudicates every correction as `accepted`, `rejected`, or `unresolved` and retains all mutation authority.
124
124
  5. For accepted corrections, repair directly in the active checkout, run targeted verification, and inspect the complete resulting change.
125
- 6. When responses remain, capture the complete fresh state and natively resume the exact same Reviewer. Never create a replacement Reviewer or simulate continuation with a fresh Reviewer plus summary.
126
- 7. If native resume fails after repair, append downgrade and rerun the Main-Agent review over that repair within the existing ten-round safety fuse.
125
+ 6. When responses remain, prepare complete latest state and prefer native continuation. If unavailable, the reference permits one compliant replacement with complete latest state, prior adjudication, and the same remaining budget.
126
+ 7. If neither compliant continuation nor replacement is available after repair, append downgrade and rerun the Main-Agent review over that repair within the existing ten-round safety fuse.
127
127
  8. If the fifth response causes a repair, run targeted verification, inspect the complete final change, record `Final State Independently Rechecked: no`, and do not send a sixth response.
128
128
  9. `NO_CORRECTION_NEEDED` or all corrections rejected with evidence converges independent review. Any unresolved correction produces `NEEDS_FIXES`.
129
129