prizmkit 1.1.142 → 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.
- package/bundled/VERSION.json +3 -3
- package/bundled/adapters/pi/paths.js +9 -0
- package/bundled/adapters/pi/rules-adapter.js +13 -0
- package/bundled/adapters/pi/settings-adapter.js +23 -0
- package/bundled/adapters/pi/skill-adapter.js +18 -0
- package/bundled/dev-pipeline/prizmkit_runtime/config.py +15 -3
- package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +3 -1
- package/bundled/dev-pipeline/prizmkit_runtime/platform_detection.py +10 -2
- package/bundled/dev-pipeline/prizmkit_runtime/runner_bookkeeping.py +1 -0
- package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +1 -0
- package/bundled/dev-pipeline/prizmkit_runtime/sessions.py +26 -2
- package/bundled/dev-pipeline/scripts/parse-stream-progress.py +61 -7
- package/bundled/dev-pipeline/tests/test_unified_cli.py +88 -0
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/prizmkit-code-review/SKILL.md +3 -3
- package/bundled/skills/prizmkit-code-review/references/independent-code-review.md +63 -58
- package/bundled/skills/prizmkit-code-review/references/review-report-template.md +8 -1
- package/bundled/skills/prizmkit-plan/SKILL.md +3 -3
- package/bundled/skills/prizmkit-plan/assets/plan-template.md +3 -0
- package/bundled/skills/prizmkit-plan/references/independent-plan-review.md +55 -52
- package/bundled/skills/prizmkit-test/SKILL.md +3 -3
- package/bundled/skills/prizmkit-test/references/independent-test-review.md +68 -69
- package/bundled/skills/prizmkit-test/references/test-report-template.md +5 -1
- package/bundled/templates/hooks/commit-intent-status.py +50 -0
- package/bundled/templates/hooks/commit-intent.json +2 -2
- package/bundled/templates/hooks/post-command-prizm-drift.py +44 -0
- package/package.json +1 -1
- package/src/ai-cli-launch.js +1 -1
- package/src/clean.js +13 -4
- package/src/config.js +32 -5
- package/src/detect-platform.js +11 -7
- package/src/index.js +5 -0
- package/src/platforms.js +6 -2
- package/src/prompts.js +12 -4
- package/src/scaffold.js +74 -20
- package/src/upgrade.js +12 -5
package/bundled/VERSION.json
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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()
|
|
@@ -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,
|
|
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,
|
|
126
|
-
7. If
|
|
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
|
|