prizmkit 1.1.108 → 1.1.109

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 (39) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/prizmkit_runtime/cli.py +20 -0
  3. package/bundled/dev-pipeline/prizmkit_runtime/commands.py +1 -0
  4. package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +21 -0
  5. package/bundled/dev-pipeline/prizmkit_runtime/interoperability.py +1 -0
  6. package/bundled/dev-pipeline/prizmkit_runtime/platform_detection.py +208 -0
  7. package/bundled/dev-pipeline/prizmkit_runtime/runtime_helper.py +508 -0
  8. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +3 -1
  9. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +3 -1
  10. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +3 -1
  11. package/bundled/dev-pipeline/scripts/prizmkit-runtime-helper.py +27 -0
  12. package/bundled/dev-pipeline/scripts/utils.py +48 -77
  13. package/bundled/dev-pipeline/templates/bootstrap-tier1.md +33 -60
  14. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +35 -62
  15. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +40 -66
  16. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +2 -2
  17. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +2 -2
  18. package/bundled/dev-pipeline/templates/sections/checkpoint-system.md +1 -1
  19. package/bundled/dev-pipeline/templates/sections/phase-browser-verification-auto.md +31 -54
  20. package/bundled/dev-pipeline/templates/sections/phase-browser-verification-opencli.md +21 -30
  21. package/bundled/dev-pipeline/templates/sections/phase-browser-verification.md +22 -62
  22. package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-base.md +1 -1
  23. package/bundled/dev-pipeline/templates/sections/phase-critic-plan-full.md +1 -1
  24. package/bundled/dev-pipeline/templates/sections/phase-critic-plan.md +1 -1
  25. package/bundled/dev-pipeline/templates/sections/phase-plan-agent.md +1 -1
  26. package/bundled/dev-pipeline/templates/sections/phase-plan-lite.md +1 -1
  27. package/bundled/dev-pipeline/templates/sections/phase-prizmkit-test.md +2 -2
  28. package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +1 -1
  29. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +1 -1
  30. package/bundled/dev-pipeline/templates/sections/phase-specify-plan-full.md +7 -6
  31. package/bundled/dev-pipeline/templates/sections/phase0-init.md +1 -1
  32. package/bundled/dev-pipeline/templates/sections/subagent-timeout-recovery.md +1 -1
  33. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +45 -3
  34. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +2 -1
  35. package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +2 -1
  36. package/bundled/dev-pipeline/tests/test_runtime_helper.py +211 -0
  37. package/bundled/dev-pipeline/tests/test_unified_cli.py +5 -0
  38. package/bundled/skills/_metadata.json +1 -1
  39. package/package.json +1 -1
@@ -10,9 +10,9 @@ You MUST execute this phase. Do NOT skip it. Do NOT mark it as completed without
10
10
 
11
11
  **Step 0 — Playwright CLI Readiness Check (BLOCKING — must pass before any browser action)**:
12
12
 
13
- 0a. Check if `playwright-cli` is installed:
13
+ 0a. Check if `playwright-cli` is installed using the runtime helper:
14
14
  ```bash
15
- which playwright-cli 2>/dev/null && playwright-cli --version 2>/dev/null || echo "NOT_INSTALLED"
15
+ {{RUNTIME_HELPER_CMD}} executable version playwright-cli --arg=--version
16
16
  ```
17
17
  If output is `NOT_INSTALLED`, install it:
18
18
  ```bash
@@ -20,7 +20,7 @@ npm install -g @playwright/cli@latest
20
20
  ```
21
21
  Then verify installation succeeded:
22
22
  ```bash
23
- playwright-cli --version
23
+ {{RUNTIME_HELPER_CMD}} executable version playwright-cli --arg=--version
24
24
  ```
25
25
  If installation fails, log the error in context-snapshot.md under `## Browser Verification: SKIPPED — playwright-cli installation failed` and proceed to the next phase. Do NOT attempt browser verification without playwright-cli.
26
26
 
@@ -32,43 +32,9 @@ Use this output to determine the correct commands for your verification steps. D
32
32
 
33
33
  0c. Check if playwright-cli skill is installed for the current AI platform:
34
34
  ```bash
35
- # Detect AI CLI platform
36
- CURRENT_PLATFORM=""
37
- if which claude >/dev/null 2>&1; then
38
- CURRENT_PLATFORM="claude"
39
- SKILL_DIR="$HOME/.claude/skills"
40
- elif which cbc >/dev/null 2>&1; then
41
- CURRENT_PLATFORM="codebuddy"
42
- SKILL_DIR="$HOME/.cbc/skills"
43
- else
44
- # Try to detect from environment or config
45
- CURRENT_PLATFORM="unknown"
46
- fi
47
-
48
- # Check if playwright-cli skill exists
49
- if [ -d "$SKILL_DIR/playwright-cli" ] || ls "$SKILL_DIR"/playwright* 2>/dev/null | grep -q .; then
50
- echo "SKILL_EXISTS"
51
- else
52
- echo "SKILL_MISSING"
53
- fi
54
- ```
55
- If `SKILL_MISSING`:
56
- ```bash
57
- # Install playwright-cli skills (defaults to claude platform)
58
- playwright-cli install --skills
59
- ```
60
- If the current platform is NOT claude, move the installed skill files to the correct location:
61
- ```bash
62
- # Skills are installed to claude's default location — move to current platform's skill dir
63
- if [ "$CURRENT_PLATFORM" != "claude" ] && [ "$CURRENT_PLATFORM" != "unknown" ]; then
64
- CLAUDE_SKILL_DIR="$HOME/.claude/skills"
65
- if [ -d "$CLAUDE_SKILL_DIR/playwright-cli" ]; then
66
- mkdir -p "$SKILL_DIR"
67
- cp -r "$CLAUDE_SKILL_DIR/playwright-cli" "$SKILL_DIR/"
68
- echo "Moved playwright-cli skill from claude to $CURRENT_PLATFORM"
69
- fi
70
- fi
35
+ {{RUNTIME_HELPER_CMD}} platform skill playwright-cli --prefix
71
36
  ```
37
+ If `SKILL_MISSING`: run `playwright-cli install --skills`. If current platform is NOT claude, copy installed skill from `$HOME/.claude/skills/playwright-cli` to the path shown by the helper output for the current platform.
72
38
 
73
39
  0d. Read the installed playwright-cli skill for workflow guidance:
74
40
  After skill installation, read the skill's SKILL.md to understand recommended workflows and patterns. Use these patterns to construct your verification flow — do NOT invent your own patterns if the skill provides them.
@@ -78,31 +44,20 @@ After skill installation, read the skill's SKILL.md to understand recommended wo
78
44
  You know this project's tech stack. Detect and start the dev server yourself:
79
45
 
80
46
  1. Identify the dev server start command from project config (`package.json` scripts, `Makefile`, `docker-compose.yml`, etc.)
81
- 2. **Detect the dev server port** — use the pre-detected port from pipeline if available, otherwise extract from project config. Do NOT hardcode or guess the port:
47
+ 2. Use the pre-detected port if available, otherwise determine the dev server port from project config. Do NOT hardcode or guess the port. Record it as `DEV_PORT` in your notes.
48
+ 3. Verify the port is available:
82
49
  ```bash
83
- # Use pipeline-injected port if available, otherwise extract from package.json
84
- DEV_PORT={{DEV_PORT}}
85
- # If DEV_PORT is still a placeholder, detect at runtime:
86
- if [ "$DEV_PORT" = "{{DEV_PORT}}" ]; then
87
- DEV_PORT=$(node -e "const s=require('./package.json').scripts.dev; const m=s.match(/-p\s+(\d+)/); console.log(m?m[1]:'')")
88
- if [ -z "$DEV_PORT" ]; then
89
- DEV_PORT=$(echo "$NEXT_PUBLIC_SITE_URL" | sed -nE 's|.*:([0-9]+).*|\1|p')
90
- fi
91
- DEV_PORT=${DEV_PORT:-3000}
92
- fi
93
- echo "Detected DEV_PORT=$DEV_PORT"
50
+ {{RUNTIME_HELPER_CMD}} port check {{DEV_PORT}}
94
51
  ```
95
- 3. Verify the port is available:
52
+ 4. Start the dev server with the runtime helper and capture a PID file:
96
53
  ```bash
97
- lsof -ti:$DEV_PORT 2>/dev/null && echo "PORT_IN_USE" || echo "PORT_FREE"
54
+ {{RUNTIME_HELPER_CMD}} process start --pid-file .prizmkit/state/browser-dev-server.pid -- <start-command tokens>
98
55
  ```
99
- 4. Start the dev server in background, capture PID:
56
+ 5. Wait for server to be ready:
100
57
  ```bash
101
- <start-command> &
102
- DEV_SERVER_PID=$!
58
+ {{RUNTIME_HELPER_CMD}} web wait-ready --url http://localhost:{{DEV_PORT}} --timeout 30 --interval 2
103
59
  ```
104
- 5. Wait for server to be ready: poll `http://localhost:$DEV_PORT` with `curl -s -o /dev/null -w "%{http_code}"` until it returns 200 or 302 (max 30 seconds, 2s interval)
105
- 6. Open the app in playwright-cli: `playwright-cli open http://localhost:$DEV_PORT`
60
+ 6. Open the app in playwright-cli: `playwright-cli open http://localhost:{{DEV_PORT}}`
106
61
  7. If the page requires authentication, use playwright-cli to register a test user and log in first
107
62
 
108
63
  **Step 2 — Verification**:
@@ -122,15 +77,21 @@ Take a final screenshot for evidence: `playwright-cli screenshot`
122
77
  **Step 3 — Cleanup (REQUIRED — you started it, you stop it)**:
123
78
 
124
79
  1. Close the playwright-cli browser: `playwright-cli close`
125
- 2. Kill the dev server process: `kill $DEV_SERVER_PID 2>/dev/null || true`
126
- 3. Verify port is released: `lsof -ti:$DEV_PORT | xargs kill -9 2>/dev/null || true`
80
+ 2. Stop the dev server process using the PID file written in Step 1:
81
+ ```bash
82
+ {{RUNTIME_HELPER_CMD}} process cleanup-pid <pid-from-.prizmkit/state/browser-dev-server.pid>
83
+ ```
84
+ 3. Verify port is released:
85
+ ```bash
86
+ {{RUNTIME_HELPER_CMD}} port check {{DEV_PORT}}
87
+ ```
127
88
 
128
89
  **Step 4 — Reporting**:
129
90
 
130
91
  Append results to `context-snapshot.md`:
131
92
  ```
132
93
  ## Browser Verification
133
- URL: http://localhost:$DEV_PORT
94
+ URL: http://localhost:<DEV_PORT>
134
95
  Dev Server Command: <actual command used>
135
96
  playwright-cli version: <version>
136
97
  Steps executed: [list of playwright-cli commands used]
@@ -142,4 +103,3 @@ Browser cleanup: confirmed
142
103
 
143
104
  If verification fails, log the failure details but continue to commit. Failures do NOT block the commit, but you MUST attempt verification and MUST clean up the dev server.
144
105
 
145
-
@@ -1,7 +1,7 @@
1
1
  ### Build Context Snapshot
2
2
 
3
3
  ```bash
4
- ls .prizmkit/specs/{{FEATURE_SLUG}}/context-snapshot.md 2>/dev/null && echo "EXISTS" || echo "MISSING"
4
+ {{RUNTIME_HELPER_CMD}} artifact exists .prizmkit/specs/{{FEATURE_SLUG}}/context-snapshot.md
5
5
  ```
6
6
 
7
7
  If MISSING — build it now:
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Guard**: Verify critic agent file exists before spawning:
4
4
  ```bash
5
- ls {{CRITIC_SUBAGENT_PATH}} 2>/dev/null && echo "CRITIC:READY" || echo "CRITIC:MISSING"
5
+ {{RUNTIME_HELPER_CMD}} artifact exists {{CRITIC_SUBAGENT_PATH}} --exists-marker CRITIC:READY --missing-marker CRITIC:MISSING
6
6
  ```
7
7
  If CRITIC:MISSING — skip this phase entirely and proceed. Log: "Critic agent not installed — skipping Plan Challenge."
8
8
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Guard**: Verify critic agent file exists before spawning:
4
4
  ```bash
5
- ls {{CRITIC_SUBAGENT_PATH}} 2>/dev/null && echo "CRITIC:READY" || echo "CRITIC:MISSING"
5
+ {{RUNTIME_HELPER_CMD}} artifact exists {{CRITIC_SUBAGENT_PATH}} --exists-marker CRITIC:READY --missing-marker CRITIC:MISSING
6
6
  ```
7
7
  If CRITIC:MISSING — skip this phase entirely and proceed. Log: "Critic agent not installed — skipping Plan Challenge."
8
8
 
@@ -1,7 +1,7 @@
1
1
  ### Plan & Tasks
2
2
 
3
3
  ```bash
4
- ls .prizmkit/specs/{{FEATURE_SLUG}}/plan.md 2>/dev/null
4
+ {{RUNTIME_HELPER_CMD}} artifact exists .prizmkit/specs/{{FEATURE_SLUG}}/plan.md
5
5
  ```
6
6
 
7
7
  If missing, run `/prizmkit-plan` with `artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` to generate `plan.md`:
@@ -1,7 +1,7 @@
1
1
  ### Plan & Tasks
2
2
 
3
3
  ```bash
4
- ls .prizmkit/specs/{{FEATURE_SLUG}}/ 2>/dev/null
4
+ {{RUNTIME_HELPER_CMD}} artifact exists .prizmkit/specs/{{FEATURE_SLUG}}
5
5
  ```
6
6
 
7
7
  If plan.md missing, run `/prizmkit-plan` with `artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/`:
@@ -5,8 +5,8 @@
5
5
  Create a current-run marker immediately before invoking the skill:
6
6
 
7
7
  ```bash
8
- mkdir -p .prizmkit/specs/{{FEATURE_SLUG}}
9
- touch .prizmkit/specs/{{FEATURE_SLUG}}/.prizmkit-test-started
8
+ {{RUNTIME_HELPER_CMD}} artifact ensure-dir .prizmkit/specs/{{FEATURE_SLUG}}
9
+ {{RUNTIME_HELPER_CMD}} artifact touch .prizmkit/specs/{{FEATURE_SLUG}}/.prizmkit-test-started
10
10
  ```
11
11
 
12
12
  Run `/prizmkit-test` with the feature artifact directory:
@@ -9,7 +9,7 @@ The skill runs an internal review-fix loop (Reviewer Agent → filter → orches
9
9
  **Gate Check — Review Report**:
10
10
  After `/prizmkit-code-review` returns, verify the review report:
11
11
  ```bash
12
- grep -q "## Verdict" .prizmkit/specs/{{FEATURE_SLUG}}/review-report.md && echo "GATE:PASS" || echo "GATE:MISSING"
12
+ {{RUNTIME_HELPER_CMD}} text contains .prizmkit/specs/{{FEATURE_SLUG}}/review-report.md --pattern "## Verdict"
13
13
  ```
14
14
  If GATE:MISSING:
15
15
  - Do not re-run `/prizmkit-code-review` in an unbounded report-repair loop.
@@ -9,7 +9,7 @@ The skill runs an internal review-fix loop (Reviewer Agent → filter → orches
9
9
  **Gate Check — Review Report**:
10
10
  After `/prizmkit-code-review` returns, verify the review report:
11
11
  ```bash
12
- grep -q "## Verdict" .prizmkit/specs/{{FEATURE_SLUG}}/review-report.md && echo "GATE:PASS" || echo "GATE:MISSING"
12
+ {{RUNTIME_HELPER_CMD}} text contains .prizmkit/specs/{{FEATURE_SLUG}}/review-report.md --pattern "## Verdict"
13
13
  ```
14
14
  If GATE:MISSING:
15
15
  - Do not re-run `/prizmkit-code-review` in an unbounded report-repair loop.
@@ -2,7 +2,7 @@
2
2
 
3
3
  Check existing artifacts first:
4
4
  ```bash
5
- ls .prizmkit/specs/{{FEATURE_SLUG}}/ 2>/dev/null
5
+ {{RUNTIME_HELPER_CMD}} artifact exists .prizmkit/specs/{{FEATURE_SLUG}}
6
6
  ```
7
7
 
8
8
  - Both (spec.md, plan.md) exist → **SKIP to CP-1**
@@ -12,11 +12,11 @@ ls .prizmkit/specs/{{FEATURE_SLUG}}/ 2>/dev/null
12
12
  **Step A — Build Context Snapshot** (skip if `context-snapshot.md` already exists):
13
13
 
14
14
  1. Read `.prizmkit/prizm-docs/root.prizm` and relevant L1/L2 prizm docs
15
- 2. Detect source code directories: read KEY_FILES and STRUCTURE sections from `root.prizm` to identify where source code lives (e.g. `src/`, `app/`, `lib/`, `cmd/`, `packages/`, or project root). If `root.prizm` is missing, scan the project tree:
15
+ 2. Detect source code directories from KEY_FILES and STRUCTURE sections in `root.prizm`; if root docs are unavailable, use the helper to list likely source files:
16
16
  ```bash
17
- find . -maxdepth 2 -type f \( -name "*.js" -o -name "*.ts" -o -name "*.py" -o -name "*.go" -o -name "*.java" -o -name "*.rb" -o -name "*.rs" \) -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/dist/*' -not -path '*/build/*' -not -path '*/vendor/*' | head -30
17
+ {{RUNTIME_HELPER_CMD}} artifact source-files . --json
18
18
  ```
19
- Identify the top-level source directories from the results.
19
+ Identify the top-level source directories from the helper output.
20
20
  3. Scan the detected source directories for files related to this feature; read each one
21
21
  4. Write `.prizmkit/specs/{{FEATURE_SLUG}}/context-snapshot.md`:
22
22
  - **Section 1 — Feature Brief**: feature description (copy from Task Contract above, 2-3 lines only)
@@ -35,14 +35,15 @@ ls .prizmkit/specs/{{FEATURE_SLUG}}/ 2>/dev/null
35
35
 
36
36
  ### Known TRAPS (from .prizmkit/prizm-docs/)
37
37
  - <trap entries extracted from L1/L2 docs>
38
- 5. Confirm: `ls .prizmkit/specs/{{FEATURE_SLUG}}/context-snapshot.md`
38
+ 5. Confirm: `{{RUNTIME_HELPER_CMD}} artifact exists .prizmkit/specs/{{FEATURE_SLUG}}/context-snapshot.md`
39
39
 
40
40
  **After Step A**: Use context-snapshot.md Section 4 File Manifest to guide targeted file reads. Do NOT scan directories or read unrelated files.
41
41
 
42
42
  **Step B — Planning Artifacts** (generate only missing files):
43
43
 
44
44
  ```bash
45
- ls .prizmkit/specs/{{FEATURE_SLUG}}/spec.md .prizmkit/specs/{{FEATURE_SLUG}}/plan.md 2>/dev/null
45
+ {{RUNTIME_HELPER_CMD}} artifact exists .prizmkit/specs/{{FEATURE_SLUG}}/spec.md
46
+ {{RUNTIME_HELPER_CMD}} artifact exists .prizmkit/specs/{{FEATURE_SLUG}}/plan.md
46
47
  ```
47
48
 
48
49
  - spec.md missing: Run `/prizmkit-plan` → generate spec.md. Resolve uncertain requirements from the task description, existing code, and conservative existing-project patterns.
@@ -1,6 +1,6 @@
1
1
  ### Phase 0: Project Bootstrap
2
2
  - Run `/prizmkit-init` (invoke the prizmkit-init skill)
3
- - Run `python3 {{INIT_SCRIPT_PATH}} --project-root {{PROJECT_ROOT}} --feature-id {{FEATURE_ID}} --feature-slug {{FEATURE_SLUG}}`
3
+ - Run `{{PYTHON_CMD}} {{INIT_SCRIPT_PATH}} --project-root {{PROJECT_ROOT}} --feature-id {{FEATURE_ID}} --feature-slug {{FEATURE_SLUG}}`
4
4
  - **CP-0**: Verify `.prizmkit/prizm-docs/root.prizm`, `.prizmkit/config.json` exist
5
5
 
6
6
 
@@ -1,6 +1,6 @@
1
1
  ## Subagent Timeout Recovery
2
2
 
3
3
  If any agent times out:
4
- 1. `ls .prizmkit/specs/{{FEATURE_SLUG}}/` — check what exists
4
+ 1. `{{RUNTIME_HELPER_CMD}} artifact list .prizmkit/specs/{{FEATURE_SLUG}}` — check what exists
5
5
  2. If `context-snapshot.md` exists: open recovery prompt with `"Read .prizmkit/specs/{{FEATURE_SLUG}}/context-snapshot.md for project context and any Implementation Log/Review Notes from previous agents. Run git diff HEAD to see actual code changes already made. Do NOT re-read individual source files unless the File Manifest directs you to."` + only remaining steps + `model: "lite"`
6
6
  3. Max 2 retries per phase. After 2 failures, orchestrator completes the work directly and appends a Recovery Note to context-snapshot.md.
@@ -444,7 +444,8 @@ class TestScopedFeatureTestGate:
444
444
 
445
445
  rendered = "\n".join(content for _, content in sections)
446
446
  assert "prizmkit-test-gate.py" not in rendered
447
- assert ".prizmkit-test-started" in rendered
447
+ assert "{{RUNTIME_HELPER_CMD}} artifact touch .prizmkit/specs/{{FEATURE_SLUG}}/.prizmkit-test-started" in rendered
448
+ assert "mkdir -p .prizmkit/specs/{{FEATURE_SLUG}}" not in rendered
448
449
  assert "when present" not in rendered
449
450
  assert "Boundary Matrix" in rendered
450
451
  assert "Boundary Completion Gate" in rendered
@@ -474,7 +475,8 @@ class TestScopedFeatureTestGate:
474
475
  rendered = "\n".join(content for _, content in sections)
475
476
 
476
477
  assert "### Checkpoint Update Helper" in rendered
477
- assert "python3 .prizmkit/dev-pipeline/scripts/update-checkpoint.py" in rendered
478
+ assert "{{CHECKPOINT_PYTHON_CMD}}" in rendered
479
+ assert "python3 .prizmkit/dev-pipeline/scripts/update-checkpoint.py" not in rendered
478
480
  assert "--checkpoint-path {{CHECKPOINT_PATH}}" in rendered
479
481
  assert "--step <step-id-or-skill-name>" in rendered
480
482
  assert "Do not hand-edit `workflow-checkpoint.json` directly." in rendered
@@ -1016,7 +1018,7 @@ def _write_feature_list(path, feature):
1016
1018
  path.write_text(json.dumps({"features": [feature], "global_context": {"language": "Python"}}), encoding="utf-8")
1017
1019
 
1018
1020
 
1019
- def _render_feature_prompt(tmp_path, mode="standard", critic=False, template=None, platform="claude"):
1021
+ def _render_feature_prompt(tmp_path, mode="standard", critic=False, template=None, platform="claude", browser_interaction=None):
1020
1022
  project = tmp_path / "project"
1021
1023
  project.mkdir(parents=True)
1022
1024
  (project / ".prizmkit" / "plans").mkdir(parents=True)
@@ -1036,6 +1038,8 @@ def _render_feature_prompt(tmp_path, mode="standard", critic=False, template=Non
1036
1038
  "acceptance_criteria": ["Prompts are clean"],
1037
1039
  "critic": critic,
1038
1040
  }
1041
+ if browser_interaction is not None:
1042
+ feature["browser_interaction"] = browser_interaction
1039
1043
  feature_list = project / ".prizmkit" / "plans" / "feature-list.json"
1040
1044
  _write_feature_list(feature_list, feature)
1041
1045
  output = project / "prompt.md"
@@ -1208,6 +1212,9 @@ class TestHeadlessPromptCleanupF033:
1208
1212
  if "Feature ID" in prompt:
1209
1213
  assert "/prizmkit-test scope=this-change artifact_dir=.prizmkit/specs/123-prompt-cleanup/" in prompt
1210
1214
  assert ".prizmkit-test-started" in prompt
1215
+ assert "{{RUNTIME_HELPER_CMD}}" not in prompt
1216
+ assert "prizmkit-runtime-helper.py" in prompt
1217
+ assert "python3 {{INIT_SCRIPT_PATH}}" not in prompt
1211
1218
 
1212
1219
  def test_legacy_feature_templates_are_clean_when_rendered(self, tmp_path):
1213
1220
  for template, mode in (("bootstrap-tier1.md", "lite"), ("bootstrap-tier2.md", "standard"), ("bootstrap-tier3.md", "full")):
@@ -1215,7 +1222,42 @@ class TestHeadlessPromptCleanupF033:
1215
1222
  for forbidden in HEADLESS_FORBIDDEN_PROMPT_STRINGS:
1216
1223
  assert forbidden not in prompt
1217
1224
  assert "prizmkit-test-gate.py" not in prompt
1225
+ assert "python3 {{INIT_SCRIPT_PATH}}" not in prompt
1226
+ assert "which playwright-cli" not in prompt
1227
+ assert "lsof -ti" not in prompt
1228
+ assert "sed -nE" not in prompt
1218
1229
  assert ".prizmkit-test-started" in prompt
1230
+ assert "{{RUNTIME_HELPER_CMD}}" not in prompt
1231
+ assert "prizmkit-runtime-helper.py" in prompt
1232
+
1233
+ def test_legacy_feature_templates_with_browser_blocks_are_shell_free(self, tmp_path):
1234
+ forbidden = [
1235
+ "python3 {{INIT_SCRIPT_PATH}}",
1236
+ "which playwright-cli",
1237
+ "which opencli",
1238
+ "lsof -ti",
1239
+ "sed -nE",
1240
+ "SKILL_DIR",
1241
+ "DEV_SERVER_PID",
1242
+ "opencli doctor 2>/dev/null ||",
1243
+ "echo \"=== playwright-cli ===\" &&",
1244
+ ]
1245
+ cases = [
1246
+ ("bootstrap-tier1.md", "lite"),
1247
+ ("bootstrap-tier2.md", "standard"),
1248
+ ("bootstrap-tier3.md", "full"),
1249
+ ]
1250
+ for template, mode in cases:
1251
+ prompt = _render_feature_prompt(
1252
+ tmp_path / f"browser-{template}",
1253
+ mode=mode,
1254
+ critic=(mode != "lite"),
1255
+ template=template,
1256
+ browser_interaction={"tool": "auto", "verify_steps": ["Open the page"]},
1257
+ )
1258
+ assert "prizmkit-runtime-helper.py" in prompt
1259
+ for snippet in forbidden:
1260
+ assert snippet not in prompt
1219
1261
 
1220
1262
  def test_claude_config_wins_over_codex_directory_for_critic_path(self, tmp_path, monkeypatch):
1221
1263
  project = tmp_path / "mixed"
@@ -201,8 +201,9 @@ class TestExplicitProjectRoot:
201
201
  assert "{{" + REMOVED_MAX_LOG_ENV + "}}" not in replacements
202
202
  assert "{{" + REMOVED_MAX_LOG_HUMAN + "}}" not in replacements
203
203
  assert "### Checkpoint Update Helper" in replacements["{{CHECKPOINT_SYSTEM}}"]
204
- assert "python3 .prizmkit/dev-pipeline/scripts/update-checkpoint.py" in replacements["{{CHECKPOINT_SYSTEM}}"]
204
+ assert "{{CHECKPOINT_PYTHON_CMD}}" in replacements["{{CHECKPOINT_SYSTEM}}"]
205
205
  assert "pipeline progress mechanism" not in replacements["{{CHECKPOINT_SYSTEM}}"]
206
+ assert "prizmkit-runtime-helper.py" in replacements["{{RUNTIME_HELPER_CMD}}"]
206
207
 
207
208
 
208
209
  # ---------------------------------------------------------------------------
@@ -129,8 +129,9 @@ class TestExplicitProjectRoot:
129
129
  assert not replacements["{{SESSION_STATUS_PATH}}"].startswith(str(execution_root.resolve()))
130
130
  assert replacements["{{DEV_SUBAGENT_PATH}}"].startswith(str(execution_root.resolve()))
131
131
  assert "### Checkpoint Update Helper" in replacements["{{CHECKPOINT_SYSTEM}}"]
132
- assert "python3 .prizmkit/dev-pipeline/scripts/update-checkpoint.py" in replacements["{{CHECKPOINT_SYSTEM}}"]
132
+ assert "{{CHECKPOINT_PYTHON_CMD}}" in replacements["{{CHECKPOINT_SYSTEM}}"]
133
133
  assert "pipeline progress mechanism" not in replacements["{{CHECKPOINT_SYSTEM}}"]
134
+ assert "prizmkit-runtime-helper.py" in replacements["{{RUNTIME_HELPER_CMD}}"]
134
135
 
135
136
 
136
137
  class TestRefactorPromptMaxLogRemoval:
@@ -0,0 +1,211 @@
1
+ """Tests for the cross-platform prompt runtime helper CLI."""
2
+
3
+ import json
4
+ import os
5
+ import socket
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ REPO_ROOT = Path(__file__).resolve().parents[2]
11
+ PIPELINE_ROOT = REPO_ROOT / "dev-pipeline"
12
+ HELPER_SCRIPT = PIPELINE_ROOT / "scripts" / "prizmkit-runtime-helper.py"
13
+ CLI_PATH = PIPELINE_ROOT / "cli.py"
14
+
15
+ if str(PIPELINE_ROOT) not in sys.path:
16
+ sys.path.insert(0, str(PIPELINE_ROOT))
17
+
18
+
19
+ def run_helper(*args, cwd=None):
20
+ return subprocess.run(
21
+ [sys.executable, str(HELPER_SCRIPT), *args],
22
+ cwd=cwd or REPO_ROOT,
23
+ text=True,
24
+ stdout=subprocess.PIPE,
25
+ stderr=subprocess.PIPE,
26
+ check=False,
27
+ )
28
+
29
+
30
+ def first_line(result):
31
+ return result.stdout.splitlines()[0]
32
+
33
+
34
+ class TestArtifactAndTextHelpers:
35
+ def test_exists_ensure_dir_touch_list_tree_and_source_files(self, tmp_path):
36
+ missing = run_helper("artifact", "exists", str(tmp_path / "missing"))
37
+ assert first_line(missing) == "MISSING"
38
+
39
+ ready = run_helper("artifact", "ensure-dir", str(tmp_path / "src"))
40
+ assert first_line(ready) == "DIR_READY"
41
+ touched = run_helper("artifact", "touch", str(tmp_path / "src" / "app.py"))
42
+ assert first_line(touched) == "TOUCHED"
43
+ (tmp_path / "src" / "README.md").write_text("hello helper\n", encoding="utf-8")
44
+ (tmp_path / "src" / "ignored.bin").write_bytes(b"\x00")
45
+
46
+ listing = run_helper("artifact", "list", str(tmp_path / "src"))
47
+ assert first_line(listing) == "LIST"
48
+ assert "file:README.md" in listing.stdout
49
+ assert "file:app.py" in listing.stdout
50
+
51
+ tree = run_helper("artifact", "tree", str(tmp_path), "--max-depth", "2")
52
+ assert first_line(tree) == "TREE"
53
+ assert "entry: src/" in tree.stdout
54
+ assert "entry: src/app.py" in tree.stdout
55
+
56
+ sources = run_helper("artifact", "source-files", str(tmp_path), "--ext", ".py", "--json")
57
+ payload = json.loads(sources.stdout)
58
+ assert payload["marker"] == "SOURCE_FILES"
59
+ assert payload["files"] == ["src/app.py"]
60
+
61
+ def test_text_search_count_and_contains_markers(self, tmp_path):
62
+ target = tmp_path / "notes.md"
63
+ target.write_text("alpha\nbeta alpha\n", encoding="utf-8")
64
+
65
+ search = run_helper("text", "search", str(tmp_path), "--pattern", "alpha")
66
+ assert first_line(search) == "FOUND"
67
+ assert "count: 2" in search.stdout
68
+
69
+ count = run_helper("text", "count", str(tmp_path), "--pattern", "alpha")
70
+ assert first_line(count) == "COUNT"
71
+ assert "count: 2" in count.stdout
72
+
73
+ contains = run_helper("text", "contains", str(target), "--pattern", "beta")
74
+ assert first_line(contains) == "GATE:PASS"
75
+ missing = run_helper("text", "contains", str(target), "--pattern", "gamma")
76
+ assert first_line(missing) == "GATE:MISSING"
77
+
78
+
79
+ class TestPlatformExecutableAndPortHelpers:
80
+ def test_platform_detection_and_skill_lookup_follow_config(self, tmp_path, monkeypatch):
81
+ project = tmp_path / "project"
82
+ skill_dir = project / ".agents" / "skills" / "playwright-cli"
83
+ skill_dir.mkdir(parents=True)
84
+ (project / ".prizmkit").mkdir()
85
+ (project / ".prizmkit" / "config.json").write_text(
86
+ json.dumps({"platform": "codex", "ai_cli": "codex"}), encoding="utf-8"
87
+ )
88
+ monkeypatch.delenv("PRIZMKIT_PLATFORM", raising=False)
89
+
90
+ detect = run_helper("platform", "detect", "--project-root", str(project), "--json")
91
+ payload = json.loads(detect.stdout)
92
+ assert payload["marker"] == "CODEX"
93
+ assert payload["platform"] == "codex"
94
+ assert payload["source"] == "config:ai_cli"
95
+
96
+ skill = run_helper("platform", "skill", "playwright-cli", "--project-root", str(project), "--json")
97
+ payload = json.loads(skill.stdout)
98
+ assert payload["marker"] == "SKILL_EXISTS"
99
+ assert payload["platform"] == "codex"
100
+ assert payload["path"].endswith(".agents/skills/playwright-cli")
101
+
102
+ def test_codebuddy_cli_env_wins_over_filesystem_markers(self, tmp_path, monkeypatch):
103
+ from prizmkit_runtime.platform_detection import resolve_platform
104
+
105
+ project = tmp_path / "project"
106
+ (project / ".claude" / "agents").mkdir(parents=True)
107
+ monkeypatch.delenv("PRIZMKIT_PLATFORM", raising=False)
108
+ resolution = resolve_platform(project, env={"CODEBUDDY_CLI": "cbc"})
109
+
110
+ assert resolution.platform == "codebuddy"
111
+ assert resolution.source == "environment:CODEBUDDY_CLI"
112
+
113
+ def test_executable_check_and_version_use_shutil_and_direct_subprocess(self, tmp_path):
114
+ executable = tmp_path / "tool.py"
115
+ executable.write_text("#!/usr/bin/env python3\nprint('tool 1.2.3')\n", encoding="utf-8")
116
+ executable.chmod(0o755)
117
+ env_path = f"{tmp_path}{os.pathsep}{os.environ.get('PATH', '')}"
118
+
119
+ result = subprocess.run(
120
+ [sys.executable, str(HELPER_SCRIPT), "executable", "check", "tool.py"],
121
+ env={**os.environ, "PATH": env_path},
122
+ text=True,
123
+ stdout=subprocess.PIPE,
124
+ stderr=subprocess.PIPE,
125
+ check=False,
126
+ )
127
+ assert first_line(result) == "INSTALLED"
128
+
129
+ version = subprocess.run(
130
+ [sys.executable, str(HELPER_SCRIPT), "executable", "version", "tool.py", "--arg=--version"],
131
+ env={**os.environ, "PATH": env_path},
132
+ text=True,
133
+ stdout=subprocess.PIPE,
134
+ stderr=subprocess.PIPE,
135
+ check=False,
136
+ )
137
+ assert first_line(version) == "VERSION"
138
+ assert "version: tool 1.2.3" in version.stdout
139
+
140
+ def test_port_check_reports_free_and_in_use(self):
141
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
142
+ sock.bind(("127.0.0.1", 0))
143
+ sock.listen(1)
144
+ port = sock.getsockname()[1]
145
+ try:
146
+ in_use = run_helper("port", "check", str(port))
147
+ assert first_line(in_use) == "PORT_IN_USE"
148
+ finally:
149
+ sock.close()
150
+ free = run_helper("port", "check", str(port))
151
+ assert first_line(free) == "PORT_FREE"
152
+
153
+
154
+ class TestProcessAndEntrypointHelpers:
155
+ def test_process_start_status_and_cleanup(self, tmp_path):
156
+ sleeper = tmp_path / "sleep.py"
157
+ sleeper.write_text("import time\ntime.sleep(30)\n", encoding="utf-8")
158
+ pid_file = tmp_path / "server.pid"
159
+
160
+ start = run_helper(
161
+ "process",
162
+ "start",
163
+ "--pid-file",
164
+ str(pid_file),
165
+ "--",
166
+ sys.executable,
167
+ str(sleeper),
168
+ )
169
+ assert first_line(start) == "PROCESS_STARTED"
170
+ pid = int(pid_file.read_text(encoding="utf-8").strip())
171
+
172
+ status = run_helper("process", "status-pid", str(pid))
173
+ assert first_line(status) == "PROCESS_RUNNING"
174
+
175
+ cleanup = run_helper("process", "cleanup-pid", str(pid), "--grace-seconds", "0.2")
176
+ assert first_line(cleanup) in {"PROCESS_TERMINATED", "PROCESS_NOT_RUNNING"}
177
+
178
+ def test_cleanup_port_is_deterministic_without_lsof(self):
179
+ result = run_helper("process", "cleanup-port", "9")
180
+ assert first_line(result) in {"PORT_FREE", "UNSUPPORTED"}
181
+ assert "lsof" not in result.stdout.lower()
182
+
183
+ def test_canonical_cli_exposes_helper_group(self, tmp_path):
184
+ existing = tmp_path / "exists.txt"
185
+ existing.write_text("ok", encoding="utf-8")
186
+ result = subprocess.run(
187
+ [sys.executable, str(CLI_PATH), "helper", "artifact", "exists", str(existing)],
188
+ cwd=REPO_ROOT,
189
+ text=True,
190
+ stdout=subprocess.PIPE,
191
+ stderr=subprocess.PIPE,
192
+ check=False,
193
+ )
194
+ assert result.returncode == 0
195
+ assert first_line(result) == "EXISTS"
196
+
197
+ def test_web_wait_ready_accepts_local_http_server(self, tmp_path):
198
+ server = tmp_path / "server.py"
199
+ server.write_text(
200
+ "from http.server import HTTPServer, BaseHTTPRequestHandler\n"
201
+ "class H(BaseHTTPRequestHandler):\n"
202
+ " def do_GET(self):\n"
203
+ " self.send_response(200); self.end_headers(); self.wfile.write(b'ok')\n"
204
+ " def log_message(self, *args): pass\n"
205
+ "HTTPServer(('127.0.0.1', 0), H).serve_forever()\n",
206
+ encoding="utf-8",
207
+ )
208
+ # Use helper process start for coverage, then skip fixed URL because the
209
+ # child selects an ephemeral port that is not externally reported.
210
+ result = run_helper("web", "wait-ready", "--url", "http://127.0.0.1:9", "--timeout", "0.1", "--interval", "0.05")
211
+ assert first_line(result) == "NOT_READY"
@@ -1904,6 +1904,9 @@ def test_materialize_worktree_support_assets_copies_ignored_assets_without_retir
1904
1904
  (repo / ".prizmkit" / "manifest.json").write_text("{}", encoding="utf-8")
1905
1905
  (repo / ".prizmkit" / "dev-pipeline" / "scripts").mkdir(parents=True)
1906
1906
  (repo / ".prizmkit" / "dev-pipeline" / "scripts" / "update-checkpoint.py").write_text("# update", encoding="utf-8")
1907
+ (repo / ".prizmkit" / "dev-pipeline" / "prizmkit_runtime").mkdir(parents=True)
1908
+ (repo / ".prizmkit" / "dev-pipeline" / "prizmkit_runtime" / "__init__.py").write_text("", encoding="utf-8")
1909
+ (repo / ".prizmkit" / "dev-pipeline" / "prizmkit_runtime" / "runtime_helper.py").write_text("# helper", encoding="utf-8")
1907
1910
  (repo / ".prizmkit" / "dev-pipeline" / "run-feature.sh").write_text("retired", encoding="utf-8")
1908
1911
 
1909
1912
  runtime = worktree_runtime_context(
@@ -1922,6 +1925,8 @@ def test_materialize_worktree_support_assets_copies_ignored_assets_without_retir
1922
1925
  assert (runtime.worktree_path / ".prizmkit" / "config.json").is_file()
1923
1926
  assert (runtime.worktree_path / ".prizmkit" / "manifest.json").is_file()
1924
1927
  assert (runtime.worktree_path / ".prizmkit" / "dev-pipeline" / "scripts" / "update-checkpoint.py").is_file()
1928
+ assert (runtime.worktree_path / ".prizmkit" / "dev-pipeline" / "prizmkit_runtime" / "__init__.py").is_file()
1929
+ assert (runtime.worktree_path / ".prizmkit" / "dev-pipeline" / "prizmkit_runtime" / "runtime_helper.py").is_file()
1925
1930
  assert not (runtime.worktree_path / ".prizmkit" / "dev-pipeline" / "run-feature.sh").exists()
1926
1931
 
1927
1932
  assert guarded_worktree_remove(runtime, delete_branch=False).ok
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.1.108",
2
+ "version": "1.1.109",
3
3
  "skills": {
4
4
  "prizmkit": {
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.108",
3
+ "version": "1.1.109",
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": {