prizmkit 1.1.101 → 1.1.102

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 (31) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +10 -0
  3. package/bundled/dev-pipeline/prizmkit_runtime/runner_status.py +9 -3
  4. package/bundled/dev-pipeline/prizmkit_runtime/runners.py +4 -1
  5. package/bundled/dev-pipeline/prizmkit_runtime/sessions.py +10 -2
  6. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +13 -13
  7. package/bundled/dev-pipeline/templates/agent-prompts/dev-implement.md +1 -1
  8. package/bundled/dev-pipeline/templates/agent-prompts/reviewer-review.md +1 -1
  9. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +2 -2
  10. package/bundled/dev-pipeline/templates/bootstrap-tier1.md +6 -7
  11. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +1 -1
  12. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +1 -1
  13. package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +7 -7
  14. package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +7 -7
  15. package/bundled/dev-pipeline/templates/sections/phase-implement-lite.md +8 -17
  16. package/bundled/dev-pipeline/templates/sections/phase-prizmkit-test.md +2 -2
  17. package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +1 -3
  18. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +1 -3
  19. package/bundled/dev-pipeline/templates/sections/phase0-test-baseline.md +3 -8
  20. package/bundled/dev-pipeline/templates/sections/test-failure-recovery-agent.md +2 -2
  21. package/bundled/dev-pipeline/templates/sections/test-failure-recovery-lite.md +1 -1
  22. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +11 -7
  23. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +89 -1
  24. package/bundled/skills/_metadata.json +1 -1
  25. package/package.json +1 -1
  26. package/src/config.js +9 -7
  27. package/src/external-skills.js +14 -14
  28. package/src/manifest.js +8 -3
  29. package/src/platforms.js +25 -0
  30. package/src/scaffold.js +11 -6
  31. package/src/upgrade.js +11 -2
@@ -1,5 +1,5 @@
1
1
  {
2
- "frameworkVersion": "1.1.101",
3
- "bundledAt": "2026-07-06T10:58:41.729Z",
4
- "bundledFrom": "fa258ae"
2
+ "frameworkVersion": "1.1.102",
3
+ "bundledAt": "2026-07-06T15:09:00.037Z",
4
+ "bundledFrom": "e918269"
5
5
  }
@@ -45,6 +45,7 @@ class RunnerEnvironment:
45
45
  heartbeat_stale_threshold_seconds: int = 600
46
46
  stale_kill_threshold_seconds: int = 600
47
47
  max_log_size: int | None = None
48
+ max_retries: int = 3
48
49
  strict_behavior_check: bool = True
49
50
 
50
51
  @classmethod
@@ -65,6 +66,7 @@ class RunnerEnvironment:
65
66
  ),
66
67
  stale_kill_threshold_seconds=_int_value(values, "STALE_KILL_THRESHOLD", 600),
67
68
  max_log_size=_optional_int(values.get("MAX_LOG_SIZE")),
69
+ max_retries=max(0, _int_value(values, "MAX_RETRIES", 3)),
68
70
  strict_behavior_check=not _falsey(values.get("STRICT_BEHAVIOR_CHECK")),
69
71
  )
70
72
 
@@ -77,6 +79,7 @@ class RunnerInvocation:
77
79
  list_path: Path
78
80
  item_id: str = ""
79
81
  item_filter: str = ""
82
+ max_retries: int | None = None
80
83
  max_infra_retries: int = 3
81
84
  mode: str | None = None
82
85
  critic: bool | None = None
@@ -228,6 +231,7 @@ def parse_invocation(
228
231
  list_path = family.plan_path
229
232
  item_id = ""
230
233
  item_filter = ""
234
+ max_retries: int | None = None
231
235
  max_infra_retries = 3
232
236
  mode: str | None = None
233
237
  critic: bool | None = None
@@ -259,6 +263,11 @@ def parse_invocation(
259
263
  item_filter = args[index]
260
264
  elif arg.startswith("--features="):
261
265
  item_filter = arg.split("=", 1)[1]
266
+ elif arg == "--max-retries" and index + 1 < len(args):
267
+ index += 1
268
+ max_retries = max(0, _safe_int(args[index], 3))
269
+ elif arg.startswith("--max-retries="):
270
+ max_retries = max(0, _safe_int(arg.split("=", 1)[1], 3))
262
271
  elif arg == "--max-infra-retries" and index + 1 < len(args):
263
272
  index += 1
264
273
  max_infra_retries = _safe_int(args[index], 3)
@@ -293,6 +302,7 @@ def parse_invocation(
293
302
  list_path=list_path,
294
303
  item_id=item_id,
295
304
  item_filter=item_filter,
305
+ max_retries=max_retries,
296
306
  max_infra_retries=max_infra_retries,
297
307
  mode=mode,
298
308
  critic=critic,
@@ -8,7 +8,7 @@ import sys
8
8
  from dataclasses import dataclass
9
9
  from pathlib import Path
10
10
 
11
- from .runner_models import RunnerFamily, RunnerInvocation
11
+ from .runner_models import RunnerEnvironment, RunnerFamily, RunnerInvocation
12
12
 
13
13
 
14
14
  @dataclass(frozen=True)
@@ -30,6 +30,12 @@ class StatusCommandResult:
30
30
  return self.stdout.strip() or self.stderr.strip()
31
31
 
32
32
 
33
+ def _resolved_max_retries(invocation: RunnerInvocation) -> int:
34
+ if invocation.max_retries is not None:
35
+ return max(0, invocation.max_retries)
36
+ return RunnerEnvironment.from_env().max_retries
37
+
38
+
33
39
  def run_status_action(
34
40
  family: RunnerFamily,
35
41
  action: str,
@@ -38,7 +44,7 @@ def run_status_action(
38
44
  item_id: str = "",
39
45
  session_status: str = "",
40
46
  session_id: str = "",
41
- max_retries: int = 3,
47
+ max_retries: int | None = None,
42
48
  active_dev_branch: str = "",
43
49
  base_branch: str = "",
44
50
  last_fatal_error_code: str = "",
@@ -47,7 +53,7 @@ def run_status_action(
47
53
  progress_fingerprint: object | None = None,
48
54
  project_root: Path | None = None,
49
55
  ) -> StatusCommandResult:
50
- """Invoke an existing updater script and parse JSON output when present."""
56
+ max_retries = _resolved_max_retries(invocation) if max_retries is None else max(0, max_retries)
51
57
  command = [
52
58
  sys.executable,
53
59
  str(family.updater_script),
@@ -114,6 +114,8 @@ def _run_pipeline(family: RunnerFamily, invocation: RunnerInvocation, paths) ->
114
114
  details.append(f"{item_id}:{final_status}")
115
115
  if final_status == "completed":
116
116
  continue
117
+ if final_status == "pending":
118
+ continue
117
119
  if RunnerEnvironment.from_env().stop_on_failure:
118
120
  return PipelineRunResult(False, processed, "stop_on_failure", final_status, tuple(details))
119
121
  if final_status in {"failed", "needs_info"}:
@@ -401,6 +403,7 @@ def _resolve_invocation_paths(invocation: RunnerInvocation, project_root: Path)
401
403
  list_path=(project_root / invocation.list_path).resolve(),
402
404
  item_id=invocation.item_id,
403
405
  item_filter=invocation.item_filter,
406
+ max_retries=invocation.max_retries,
404
407
  max_infra_retries=invocation.max_infra_retries,
405
408
  mode=invocation.mode,
406
409
  critic=invocation.critic,
@@ -418,5 +421,5 @@ def _help_text(family: RunnerFamily) -> str:
418
421
  return (
419
422
  f"Python foreground runner for {family.kind}.\n"
420
423
  "Supported legacy options include plan list path, item id, --features for feature, "
421
- "--max-infra-retries, --mode, --critic/--no-critic, --resume-phase, and --dry-run."
424
+ "--max-retries, --max-infra-retries, --mode, --critic/--no-critic, --resume-phase, and --dry-run."
422
425
  )
@@ -152,7 +152,14 @@ def build_ai_session_command(config: AISessionConfig) -> AISessionCommand:
152
152
  platform = _normalize_platform(config.platform)
153
153
  prompt = _prompt_text(config.prompt_path)
154
154
  if platform == "claude":
155
- argv = [config.cli_command, "-p", prompt, "--dangerously-skip-permissions"]
155
+ argv = [
156
+ config.cli_command,
157
+ "-p",
158
+ prompt,
159
+ "--dangerously-skip-permissions",
160
+ "--disallowedTools",
161
+ "EnterWorktree",
162
+ ]
156
163
  if config.verbose or config.use_stream_json:
157
164
  argv.append("--verbose")
158
165
  if config.use_stream_json:
@@ -175,6 +182,7 @@ def build_ai_session_command(config: AISessionConfig) -> AISessionCommand:
175
182
  timeout = _codex_subagent_timeout(config)
176
183
  if timeout > 0:
177
184
  argv.extend(["--config", f"agents.job_max_runtime_seconds={timeout}"])
185
+ argv.extend(["--profile", "no-worktree"])
178
186
  argv.extend(["exec", "--cd", str(config.cwd), "--skip-git-repo-check"])
179
187
  if config.use_stream_json:
180
188
  argv.append("--json")
@@ -186,7 +194,7 @@ def build_ai_session_command(config: AISessionConfig) -> AISessionCommand:
186
194
  argv.append("-")
187
195
  return AISessionCommand(tuple(argv), prompt, "codex", "stdin")
188
196
 
189
- argv = [config.cli_command, "--print", "-y"]
197
+ argv = [config.cli_command, "--print", "-y", "--disallowedTools", "EnterWorktree"]
190
198
  if config.verbose:
191
199
  argv.append("--verbose")
192
200
  if config.use_stream_json:
@@ -676,7 +676,7 @@ def determine_pipeline_mode(complexity):
676
676
  SECTION_TO_SKILL = {
677
677
  "phase0-init": ("prizmkit-init", "Project Bootstrap",
678
678
  [".prizmkit/prizm-docs/root.prizm", ".prizmkit/config.json"]),
679
- "phase0-test-baseline": ("test-baseline", "Test Baseline", []),
679
+ "phase0-test-baseline": ("test-baseline", "Detect Test Commands", []),
680
680
  "phase-context-snapshot": ("context-snapshot", "Build Context Snapshot",
681
681
  [".prizmkit/specs/{slug}/context-snapshot.md"]),
682
682
  "phase-specify-plan": ("context-snapshot-and-plan", "Specify & Plan",
@@ -1559,6 +1559,18 @@ def assemble_sections(pipeline_mode, sections_dir, init_done, is_resume,
1559
1559
  load_section(sections_dir,
1560
1560
  "phase-implement-agent.md")))
1561
1561
 
1562
+ # Verification Gates are included in Task Contract. Keep AC in one place so
1563
+ # background context and implementation prompts cannot redefine scope.
1564
+ # --- Review ---
1565
+ if pipeline_mode == "full":
1566
+ sections.append(("phase-review",
1567
+ load_section(sections_dir,
1568
+ "phase-review-full.md")))
1569
+ else:
1570
+ sections.append(("phase-review",
1571
+ load_section(sections_dir,
1572
+ "phase-review-agent.md")))
1573
+
1562
1574
  # --- Test Failure Recovery Protocol (tier-specific) ---
1563
1575
  if pipeline_mode == "lite":
1564
1576
  sections.append(("test-failure-recovery",
@@ -1574,18 +1586,6 @@ def assemble_sections(pipeline_mode, sections_dir, init_done, is_resume,
1574
1586
  load_section(sections_dir,
1575
1587
  "phase-prizmkit-test.md")))
1576
1588
 
1577
- # Verification Gates are included in Task Contract. Keep AC in one place so
1578
- # background context and implementation prompts cannot redefine scope.
1579
- # --- Review (only for agent tiers) ---
1580
- if pipeline_mode == "full":
1581
- sections.append(("phase-review",
1582
- load_section(sections_dir,
1583
- "phase-review-full.md")))
1584
- elif pipeline_mode == "standard":
1585
- sections.append(("phase-review",
1586
- load_section(sections_dir,
1587
- "phase-review-agent.md")))
1588
-
1589
1589
  # --- Browser Verification (conditional, tool-aware) ---
1590
1590
  if browser_enabled:
1591
1591
  if browser_tool == "opencli":
@@ -61,7 +61,7 @@ Before returning, append `## Implementation Log` to `context-snapshot.md` with:
61
61
  - Do not run git commands; staging and commit are handled by the orchestrator.
62
62
  - **Edit safety**: If an Edit fails with 'String to replace not found', grep for the target text before retrying. Never guess file offsets — verify them with a Read or grep first.
63
63
  - **Read safety**: If 3 consecutive Reads to the same file return 'shorter than offset' or 'Wasted call', STOP and report BLOCKED.
64
- - **Test early**: Run `{{TEST_CMD}}` after every 3 successful Edit operations. Capture output to /tmp/test-out.txt and grep for failures.
64
+ - **Test gate placement**: Do not run mandatory periodic or full-suite tests here. The scoped/full test gate runs after code review; only use tiny targeted checks when needed for local debugging.
65
65
 
66
66
  Do not return success unless:
67
67
  1. implementation tasks are complete;
@@ -2,6 +2,6 @@
2
2
  1. Read `.prizmkit/specs/{{FEATURE_SLUG}}/spec.md` (if it exists) for goals and Verification Gates; if spec.md does not exist, read `.prizmkit/specs/{{FEATURE_SLUG}}/context-snapshot.md` Section 1 Task Contract instead
3
3
  2. Read `.prizmkit/specs/{{FEATURE_SLUG}}/plan.md` for architecture decisions and completed tasks
4
4
  3. Run /prizmkit-code-review with artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/. The skill will run its internal review-fix loop (Reviewer → filter → Dev fix, max 3 rounds) and write review-report.md.
5
- 4. Run the full test suite using `{{TEST_CMD}}`. When running tests: `({{TEST_CMD}}) 2>&1 | tee /tmp/review-test-out.txt | tail -20`, then grep `/tmp/review-test-out.txt` for details — do NOT re-run the suite multiple times.
5
+ 4. Do not run test suites here. The orchestrator runs `/prizmkit-test` after review completes and then fixes code/tests until the scoped gate passes.
6
6
  5. review-report.md will be written to .prizmkit/specs/{{FEATURE_SLUG}}/ by prizmkit-code-review.
7
7
  Report: verdict (PASS/NEEDS_FIXES), number of rounds, findings fixed/rejected."
@@ -30,9 +30,9 @@ Infer what needs to be done from the feature context above and follow the standa
30
30
 
31
31
  3. **Implement**: Run `/prizmkit-implement` with `artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` to execute the plan using TDD (write tests first, then implement).
32
32
 
33
- 4. **Test**: Run `/prizmkit-test scope=this-change artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` to generate and verify tests only for this feature's changed scope. Do not use bugfix/refactor artifact directories for this gate.
33
+ 4. **Review**: Run `/prizmkit-code-review` with `artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` to review and auto-fix changes against the spec (internal review-fix loop, max 3 rounds).
34
34
 
35
- 5. **Review**: Run `/prizmkit-code-review` with `artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` to review and auto-fix changes against the spec (internal review-fix loop, max 3 rounds).
35
+ 5. **Test**: Run `/prizmkit-test scope=this-change artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` to generate and verify tests only for this feature's changed scope after review. Do not use bugfix/refactor artifact directories for this gate.
36
36
 
37
37
  6. **Retrospective**: Run `/prizmkit-retrospective` to sync `.prizmkit/prizm-docs/` with code changes.
38
38
 
@@ -145,9 +145,9 @@ You know this project's tech stack. Identify ALL test commands that apply (e.g.,
145
145
  **3b.** Run `/prizmkit-implement` — this handles the full implementation cycle:
146
146
  - Reads plan.md Tasks section from `.prizmkit/specs/{{FEATURE_SLUG}}/`
147
147
  - Reads context from `context-snapshot.md` (Prizm docs, TRAPS, file manifest)
148
- - Implements task-by-task with TDD, marking each `[x]` immediately
148
+ - Implements task-by-task, marking each `[x]` immediately
149
149
  - Creates/updates L2 `.prizm` docs when creating new modules or significantly modifying existing ones — AI selectively decides which modules warrant L2 based on complexity and importance
150
- - Runs tests using `TEST_CMD` after each task
150
+ - Defers scoped/full test execution until after review
151
151
  - Writes '## Implementation Log' to `context-snapshot.md`
152
152
 
153
153
  **3b-checkpoint.** Context management — if plan.md has more than 5 tasks, update durable checkpoints/artifacts after every 3 tasks, minimize output, and continue. If context overflow occurs in a headless run, the runner starts a fresh continuation session from those checkpoints and artifacts. In an interactive Claude Code session operated manually outside the headless pipeline, `/compact` may be used as an optional convenience only.
@@ -155,10 +155,9 @@ You know this project's tech stack. Identify ALL test commands that apply (e.g.,
155
155
  **3c.** After implement completes, verify:
156
156
  1. All tasks in plan.md are `[x]`
157
157
  2. **Verification Gate Self-Check**: for each acceptance criterion, write one line of evidence (test name / file:line / fixture path). Any gate without evidence is BLOCKED, not success.
158
- 3. **Layered test strategy**: debug with single-file tests (`npx vitest run <file> -t '<name>'`), only run full suite as gate when single-file is green. Never run full suite while a single-file test is still red.
159
- 4. Run the full test suite to ensure nothing is broken
160
- 5. Verify each acceptance criterion from Section 1 of context-snapshot.md is met check mentally, do NOT re-read files you already wrote
161
- 6. If any criterion is not met, fix it now using the convergence-based recovery loop below
158
+ 3. **Layered test strategy**: do not run mandatory periodic/full-suite tests during implementation. Use only tiny targeted checks when needed for local debugging; the scoped feature test gate runs after review.
159
+ 4. Verify each acceptance criterion from Section 1 of context-snapshot.md is met — check mentally, do NOT re-read files you already wrote
160
+ 5. If any criterion is not met, fix it now using the convergence-based recovery loop below
162
161
 
163
162
  **CP-2**: All acceptance criteria met, all tests pass.
164
163
 
@@ -177,7 +176,7 @@ When tests fail, use convergence recovery — keep fixing while progress is bein
177
176
 
178
177
  ### Scoped Feature Test Gate — PrizmKit Test
179
178
 
180
- Run `/prizmkit-test scope=this-change artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` after implementation and before review/commit.
179
+ Run `/prizmkit-test scope=this-change artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` after review and before commit.
181
180
 
182
181
  Before invoking the skill, create `.prizmkit/specs/{{FEATURE_SLUG}}/.prizmkit-test-started` so stale reports are ignored.
183
182
 
@@ -302,7 +302,7 @@ If GATE:MISSING — send message to Dev (re-spawn if needed): "Write the '## Imp
302
302
 
303
303
  ### Scoped Feature Test Gate — PrizmKit Test
304
304
 
305
- Run `/prizmkit-test scope=this-change artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` after implementation and before review/commit.
305
+ Run `/prizmkit-test scope=this-change artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` after review and before commit.
306
306
 
307
307
  Before invoking the skill, create `.prizmkit/specs/{{FEATURE_SLUG}}/.prizmkit-test-started` so stale reports are ignored.
308
308
 
@@ -320,7 +320,7 @@ All tasks `[x]`, tests pass.
320
320
 
321
321
  ### Scoped Feature Test Gate — PrizmKit Test
322
322
 
323
- Run `/prizmkit-test scope=this-change artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` after implementation and before review/commit.
323
+ Run `/prizmkit-test scope=this-change artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` after review and before commit.
324
324
 
325
325
  Before invoking the skill, create `.prizmkit/specs/{{FEATURE_SLUG}}/.prizmkit-test-started` so stale reports are ignored.
326
326
 
@@ -19,15 +19,15 @@ grep -c '^\- \[ \]' .prizmkit/specs/{{FEATURE_SLUG}}/plan.md 2>/dev/null || true
19
19
  **3b.** Run `/prizmkit-implement` directly — you (the orchestrator) execute the full cycle:
20
20
  - Read plan.md Tasks from `.prizmkit/specs/{{FEATURE_SLUG}}/`
21
21
  - Use context-snapshot.md Section 4 File Manifest for targeted reads — do NOT re-read files already summarized there
22
- - Implement task-by-task with TDD, marking each `[x]` immediately
22
+ - Implements task-by-task, marking each `[x]` immediately
23
23
  - Creates/updates L2 `.prizm` docs when creating new modules
24
- - Runs tests using `TEST_CMD`
24
+ - Defers scoped/full test execution until after the code-review loop completes
25
25
  - If plan.md has >5 tasks: update checkpoints after every 3 tasks
26
26
 
27
- **3c. Layered test strategy** (replaces "run full suite every 3 Edits"):
28
- - Debugging a single test: `npx vitest run <test-file> -t '<test name>'` (or per-stack equivalent). Capture to `/tmp/test-out.txt`, grep for failures.
29
- - Only when single-file is green, run ONE full `{{TEST_CMD}}` as the gate.
30
- - Never run full suite while a single-file test is still red.
27
+ **3c. Focused checks only (no test gate here)**:
28
+ - During implementation, do not run mandatory periodic or full-suite tests.
29
+ - If a tiny targeted check is needed to understand a local failure, run only the smallest relevant command and capture output to `/tmp/test-out.txt`.
30
+ - The scoped feature test gate runs after code review; do not declare implementation success based on an early full-suite run.
31
31
 
32
32
  **3d. Verification Gate Self-Check (BLOCKING — before declaring implement done)**:
33
33
  For each Verification Gate from the Task Contract, write one evidence line to Implementation Log:
@@ -45,7 +45,7 @@ For each Verification Gate from the Task Contract, write one evidence line to Im
45
45
  - Gate Evidence section
46
46
  - unresolved blockers
47
47
 
48
- **CP-2**: All tasks `[x]`, Implementation Log written with Gate Evidence, Test Failure Recovery Protocol's Success Rule satisfied. Blocked gates → `failure-log.md`, not success.
48
+ **CP-2**: All tasks `[x]`, Implementation Log written with Gate Evidence, and blocked gates documented in `failure-log.md`. Test execution is deferred until after code review.
49
49
 
50
50
  **Checkpoint update**:
51
51
  ```bash
@@ -20,15 +20,15 @@ Save pre-existing failing tests as `BASELINE_FAILURES`.
20
20
  **3b.** Run `/prizmkit-implement` directly — you (the orchestrator) execute the full cycle:
21
21
  - Read plan.md Tasks from `.prizmkit/specs/{{FEATURE_SLUG}}/`
22
22
  - Use context-snapshot.md Section 4 File Manifest for targeted reads — do NOT re-read files already summarized there
23
- - Implement task-by-task with TDD, marking each `[x]` immediately
23
+ - Implements task-by-task, marking each `[x]` immediately
24
24
  - Creates/updates L2 `.prizm` docs when creating new modules
25
- - Runs tests using `TEST_CMD`
25
+ - Defers scoped/full test execution until after the code-review loop completes
26
26
  - If plan.md has >5 tasks: update checkpoints after every 3 tasks
27
27
 
28
- **3c. Layered test strategy** (replaces "run full suite every 3 Edits"):
29
- - Debugging a single test: `npx vitest run <test-file> -t '<test name>'` (or per-stack equivalent). Capture to `/tmp/test-out.txt`, grep for failures.
30
- - Only when single-file is green, run ONE full `{{TEST_CMD}}` as the gate.
31
- - Never run full suite while a single-file test is still red.
28
+ **3c. Focused checks only (no test gate here)**:
29
+ - During implementation, do not run mandatory periodic or full-suite tests.
30
+ - If a tiny targeted check is needed to understand a local failure, run only the smallest relevant command and capture output to `/tmp/test-out.txt`.
31
+ - The scoped feature test gate runs after code review; do not declare implementation success based on an early full-suite run.
32
32
 
33
33
  **3d. Verification Gate Self-Check (BLOCKING — before declaring implement done)**:
34
34
  For each Verification Gate from the Task Contract, write one evidence line to Implementation Log:
@@ -46,7 +46,7 @@ For each Verification Gate from the Task Contract, write one evidence line to Im
46
46
  - Gate Evidence section
47
47
  - unresolved blockers
48
48
 
49
- **CP-2**: All tasks `[x]`, Implementation Log written with Gate Evidence, Test Failure Recovery Protocol's Success Rule satisfied. Blocked gates → `failure-log.md`, not success.
49
+ **CP-2**: All tasks `[x]`, Implementation Log written with Gate Evidence, and blocked gates documented in `failure-log.md`. Test execution is deferred until after code review.
50
50
 
51
51
  **Checkpoint update**:
52
52
  ```bash
@@ -1,33 +1,24 @@
1
- ### Implement + Test
1
+ ### Implement
2
2
 
3
3
  **Protocol references**:
4
4
  - Follow Context Budget Rules §8 for scaffold/generated files.
5
5
  - Follow Context Budget Rules §9 before package install/build commands that resolve dependencies.
6
6
  - Follow Context Budget Rules §10 after build/compile commands.
7
7
 
8
- **3a.** Detect test commands and record baseline:
9
-
10
- You know this project's tech stack. Identify ALL test commands that apply (e.g., `go test ./...`, `npm test`, `cargo test`, `pytest`, `make test`, etc.). Record them as `TEST_CMDS`. Then record baseline:
11
- ```bash
12
- # Run each test command, capture output
13
- ($TEST_CMD) 2>&1 | tee /tmp/test-baseline.txt | tail -20
14
- ```
15
-
16
- **3b.** Run `/prizmkit-implement` — this handles the full implementation cycle:
8
+ **3a.** Run `/prizmkit-implement` this handles the implementation cycle:
17
9
  - Reads plan.md Tasks section from `.prizmkit/specs/{{FEATURE_SLUG}}/`
18
10
  - Reads context from `context-snapshot.md` (Prizm docs, TRAPS, file manifest)
19
- - Implements task-by-task with TDD, marking each `[x]` immediately
11
+ - Implements task-by-task, marking each `[x]` immediately
20
12
  - Creates/updates L2 `.prizm` docs when creating new modules or significantly modifying existing ones — AI selectively decides which modules warrant L2 based on complexity and importance
21
- - Runs tests using `TEST_CMD` after each task
13
+ - Does not run the scoped/full test gate; testing happens after code review in the PrizmKit Test phase
22
14
  - Writes '## Implementation Log' to `context-snapshot.md`
23
15
 
24
- **3c.** After implement completes, verify:
16
+ **3b.** After implement completes, verify:
25
17
  1. All tasks in plan.md are `[x]`
26
- 2. Run the full test suite to ensure no new regressions remain
27
- 3. Verify each Verification Gate from the Task Contract — check mentally, do NOT re-read files you already wrote
28
- 4. If any gate is unmet or blocked, follow the Test Failure Recovery Protocol
18
+ 2. Verify each Verification Gate from the Task Contract using code/file evidence already in context — do NOT re-read files you already wrote
19
+ 3. If any gate is unmet or blocked, write `failure-log.md` and stop for recovery
29
20
 
30
- **CP-2**: Implementation may proceed only when all tasks are `[x]` and the Test Failure Recovery Protocol's Success Rule is satisfied. Blocked gates must be documented in `failure-log.md` and are not success.
21
+ **CP-2**: Implementation may proceed only when all tasks are `[x]` and blocked gates are documented in `failure-log.md`. Test execution is deferred until after code review.
31
22
 
32
23
 
33
24
  **Checkpoint update**: Run the update script to set step `prizmkit-implement` to `"completed"`:
@@ -1,6 +1,6 @@
1
1
  ### Scoped Feature Test Gate — PrizmKit Test
2
2
 
3
- **Goal**: Generate and verify tests for this feature's changed scope before code review without embedding the full gate implementation in this prompt.
3
+ **Goal**: After code review completes, generate and verify tests for this feature's changed scope without embedding the full gate implementation in this prompt.
4
4
 
5
5
  Create a start marker immediately before invoking the skill so the gate can reject stale reports from older sessions:
6
6
 
@@ -34,6 +34,6 @@ python3 "$PIPELINE_DIR/scripts/prizmkit-test-gate.py" check \
34
34
 
35
35
  Gate outcomes:
36
36
  - `GATE:PASS` → append the report path and a 3-5 bullet summary to `context-snapshot.md` under `## PrizmKit Test Gate`, then proceed to the next checkpoint step. The gate script already wrote `test-report-path.txt` and marked `prizmkit-test` completed.
37
- - `GATE:FAIL` with `NEEDS_FIXES` → read the report's `In-Scope Failures`, fix the feature implementation or generated tests within this feature scope, rerun `/prizmkit-test`, and repeat the gate check. Do not proceed to code review while the verdict is not `PASS`.
37
+ - `GATE:FAIL` with `NEEDS_FIXES` → read the report's `In-Scope Failures`, fix the feature implementation or generated tests within this feature scope, rerun `/prizmkit-test`, and repeat the gate check. Do not proceed to retrospective/commit while the verdict is not `PASS`.
38
38
  - `GATE:FAIL` with `BLOCKED`, wrong mode, wrong artifact dir, unreadable report, stale scope, boundary failure, validator failure, or checkpoint update failure → write `.prizmkit/specs/{{FEATURE_SLUG}}/failure-log.md` with the scoped test failure and stop for recovery.
39
39
  - `GATE:MISSING` → perform one bounded status check: inspect `/prizmkit-test` output and `.prizmkit/test/` for a report directory. If no current-run report exists, write `failure-log.md` and stop for recovery.
@@ -1,8 +1,6 @@
1
1
  ### Review — Code Review
2
2
 
3
- Precondition: the `prizmkit-test` checkpoint is completed and `.prizmkit/specs/{{FEATURE_SLUG}}/test-report-path.txt` points to the scoped test report.
4
-
5
- Before invoking review, read `.prizmkit/specs/{{FEATURE_SLUG}}/test-report-path.txt`, read the referenced `/prizmkit-test` report, and confirm the summary in `context-snapshot.md` under `## PrizmKit Test Gate` matches it. The code-review skill must receive this context through `artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` and the Reviewer must assess generated/updated test quality, `In-Scope Failures`, `Baseline Failures`, `Out of Scope Gaps`, boundary coverage if present, and the scoped report verdict.
3
+ Review the completed implementation before the scoped feature test gate runs. Use `artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` so the code-review skill can read spec, plan, context snapshot, implementation log, and current diff artifacts. Do not require or read a prior `/prizmkit-test` report here; test generation and test-fix looping happen in the next phase.
6
4
 
7
5
  Run `/prizmkit-code-review` with `artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/`.
8
6
 
@@ -1,8 +1,6 @@
1
1
  ### Review — Code Review
2
2
 
3
- Precondition: the `prizmkit-test` checkpoint is completed and `.prizmkit/specs/{{FEATURE_SLUG}}/test-report-path.txt` points to the scoped test report.
4
-
5
- Before invoking review, read `.prizmkit/specs/{{FEATURE_SLUG}}/test-report-path.txt`, read the referenced `/prizmkit-test` report, and confirm the summary in `context-snapshot.md` under `## PrizmKit Test Gate` matches it. The code-review skill must receive this context through `artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` and the Reviewer must assess generated/updated test quality, `In-Scope Failures`, `Baseline Failures`, `Out of Scope Gaps`, boundary coverage if present, and the scoped report verdict.
3
+ Review the completed implementation before the scoped feature test gate runs. Use `artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` so the code-review skill can read spec, plan, context snapshot, implementation log, and current diff artifacts. Do not require or read a prior `/prizmkit-test` report here; test generation and test-fix looping happen in the next phase.
6
4
 
7
5
  Run `/prizmkit-code-review` with `artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/`.
8
6
 
@@ -1,15 +1,10 @@
1
- ### Phase 0: Record Test Baseline & Detect Test Commands
1
+ ### Phase 0: Detect Test Commands
2
2
 
3
3
  **Step 1 — Detect test commands**: You know this project's tech stack. Identify ALL test commands that apply (e.g., `go test ./...`, `npm test`, `cargo test`, `pytest`, `make test`, etc.). Record them as `TEST_CMDS`.
4
4
 
5
- **Step 2 — Record pre-existing failure baseline**:
6
- ```bash
7
- # Run each test command, capture output
8
- ($TEST_CMD) 2>&1 | tee /tmp/test-baseline.txt | tail -20
9
- ```
10
- Save the list of **pre-existing failing tests** (if any) as `BASELINE_FAILURES`. These are known failures that existed before this session — Dev must NOT be blamed for them, but must list them in COMPLETION_SIGNAL.
5
+ **Step 2 — Do not run tests yet**: This phase only records the commands that will be used by the post-review PrizmKit Test gate. Do not run baseline, periodic, or full-suite tests here; test execution happens after code review.
11
6
 
12
- > **Test Output Rule**: Always capture test output to a temp file (`tee /tmp/test-out.txt`). Then grep the file instead of re-running the suite.
7
+ > **Test Output Rule**: When the post-review gate runs, capture test output to a temp file (`tee /tmp/test-out.txt`). Then grep the file instead of re-running the suite.
13
8
 
14
9
 
15
10
  **Checkpoint update**: Run the update script to set step `test-baseline` to `"completed"`:
@@ -1,6 +1,6 @@
1
1
  ## Test Failure Recovery Protocol
2
2
 
3
- Use this protocol whenever implementation or review tests fail. Its purpose is to distinguish tolerated pre-existing failures from blockers introduced by this session.
3
+ Use this protocol whenever the post-review PrizmKit Test gate fails. Its purpose is to distinguish tolerated pre-existing failures from blockers introduced by this session.
4
4
 
5
5
  ### Failure Classes
6
6
 
@@ -25,7 +25,7 @@ After **6 total test-fix rounds**, STOP trial-and-error. Write failure-log listi
25
25
 
26
26
  ### Success Rule
27
27
 
28
- Proceed to review only when:
28
+ Proceed to retrospective/commit only when:
29
29
 
30
30
  1. all new regressions are fixed;
31
31
  2. baseline failures are documented;
@@ -1,6 +1,6 @@
1
1
  ## Test Failure Recovery Protocol
2
2
 
3
- Use this protocol whenever implementation tests fail. Its purpose is to distinguish tolerated pre-existing failures from blockers introduced by this session.
3
+ Use this protocol whenever the post-review PrizmKit Test gate fails. Its purpose is to distinguish tolerated pre-existing failures from blockers introduced by this session.
4
4
 
5
5
  ### Failure Classes
6
6
 
@@ -406,7 +406,7 @@ class TestProcessModeBlocks:
406
406
  # ---------------------------------------------------------------------------
407
407
 
408
408
  class TestScopedFeatureTestGate:
409
- def test_standard_sections_insert_prizmkit_test_between_implement_and_review(self):
409
+ def test_standard_sections_insert_prizmkit_test_after_review(self):
410
410
  sections_dir = Path(__file__).resolve().parents[1] / "templates" / "sections"
411
411
  sections = assemble_sections(
412
412
  "standard",
@@ -419,7 +419,7 @@ class TestScopedFeatureTestGate:
419
419
  names = [name for name, _ in sections]
420
420
 
421
421
  assert "phase-prizmkit-test" in names
422
- assert names.index("phase-implement") < names.index("phase-prizmkit-test") < names.index("phase-review")
422
+ assert names.index("phase-implement") < names.index("phase-review") < names.index("phase-prizmkit-test")
423
423
 
424
424
  rendered = "\n".join(content for _, content in sections)
425
425
  assert "prizmkit-test-gate.py" in rendered
@@ -430,8 +430,12 @@ class TestScopedFeatureTestGate:
430
430
  assert ".prizmkit/refactor/" in rendered # explicitly forbidden in the gate wording
431
431
  assert "latest_test_report=$(python3" not in rendered
432
432
  assert "boundary_columns_ok=$(awk" not in rendered
433
+ assert "Runs tests using `TEST_CMD` after each task" not in rendered
434
+ assert "Run the full test suite" not in rendered
435
+ assert "before code review" not in rendered
436
+ assert "Precondition: the `prizmkit-test` checkpoint is completed" not in rendered
433
437
 
434
- def test_lite_sections_include_prizmkit_test_before_commit(self):
438
+ def test_lite_sections_include_review_before_prizmkit_test_and_commit(self):
435
439
  sections_dir = Path(__file__).resolve().parents[1] / "templates" / "sections"
436
440
  sections = assemble_sections(
437
441
  "lite",
@@ -444,10 +448,10 @@ class TestScopedFeatureTestGate:
444
448
  names = [name for name, _ in sections]
445
449
 
446
450
  assert "phase-prizmkit-test" in names
447
- assert "phase-review" not in names
448
- assert names.index("phase-implement") < names.index("phase-prizmkit-test") < names.index("phase-commit")
451
+ assert "phase-review" in names
452
+ assert names.index("phase-implement") < names.index("phase-review") < names.index("phase-prizmkit-test") < names.index("phase-commit")
449
453
 
450
- def test_checkpoint_orders_prizmkit_test_between_implement_and_review(self):
454
+ def test_checkpoint_orders_prizmkit_test_after_review(self):
451
455
  sections_dir = Path(__file__).resolve().parents[1] / "templates" / "sections"
452
456
  sections = assemble_sections(
453
457
  "standard",
@@ -468,7 +472,7 @@ class TestScopedFeatureTestGate:
468
472
  )
469
473
  skills = [step["skill"] for step in checkpoint["steps"]]
470
474
 
471
- assert skills.index("prizmkit-implement") < skills.index("prizmkit-test") < skills.index("prizmkit-code-review")
475
+ assert skills.index("prizmkit-implement") < skills.index("prizmkit-code-review") < skills.index("prizmkit-test")
472
476
  test_step = next(step for step in checkpoint["steps"] if step["skill"] == "prizmkit-test")
473
477
  assert test_step["required_artifacts"] == [
474
478
  ".prizmkit/specs/123-scope-test/test-report-path.txt",
@@ -291,7 +291,7 @@ def test_runner_models_parse_legacy_args_and_environment(tmp_path):
291
291
  invocation = parse_invocation(
292
292
  family,
293
293
  "run",
294
- ("custom-list.json", "F-2", "--features", "F-003:F-001", "--max-infra-retries=5", "--mode", "full", "--critic"),
294
+ ("custom-list.json", "F-2", "--features", "F-003:F-001", "--max-retries", "7", "--max-infra-retries=5", "--mode", "full", "--critic"),
295
295
  )
296
296
  env = RunnerEnvironment.from_env(
297
297
  {
@@ -300,6 +300,7 @@ def test_runner_models_parse_legacy_args_and_environment(tmp_path):
300
300
  "DEV_BRANCH": "dev/custom",
301
301
  "VERBOSE": "1",
302
302
  "STALE_KILL_THRESHOLD": "9",
303
+ "MAX_RETRIES": "9",
303
304
  "STRICT_BEHAVIOR_CHECK": "false",
304
305
  }
305
306
  )
@@ -307,6 +308,7 @@ def test_runner_models_parse_legacy_args_and_environment(tmp_path):
307
308
  assert invocation.list_path == Path("custom-list.json")
308
309
  assert invocation.item_id == "F-002"
309
310
  assert invocation.item_filter == "F-003:F-001"
311
+ assert invocation.max_retries == 7
310
312
  assert invocation.max_infra_retries == 5
311
313
  assert invocation.mode == "full"
312
314
  assert invocation.critic is True
@@ -314,6 +316,7 @@ def test_runner_models_parse_legacy_args_and_environment(tmp_path):
314
316
  assert env.auto_push is True
315
317
  assert env.dev_branch == "dev/custom"
316
318
  assert env.stale_kill_threshold_seconds == 9
319
+ assert env.max_retries == 9
317
320
  assert env.strict_behavior_check is False
318
321
 
319
322
 
@@ -355,6 +358,91 @@ def test_status_bridge_preserves_feature_filter_selection(tmp_path):
355
358
  assert update.data["context_overflow_count"] == 1
356
359
 
357
360
 
361
+ def test_status_bridge_passes_max_retries_from_invocation_or_environment(monkeypatch, tmp_path):
362
+ import subprocess
363
+ from prizmkit_runtime.runner_models import family_for, parse_invocation
364
+ from prizmkit_runtime.runner_status import status_text
365
+
366
+ paths = _make_paths(tmp_path)
367
+ family = family_for("feature", paths)
368
+ explicit = parse_invocation(family, "run", ("--max-retries", "8"))
369
+ explicit = explicit.__class__(**{**explicit.__dict__, "list_path": paths.feature_plan})
370
+ fallback = parse_invocation(family, "run", ())
371
+ fallback = fallback.__class__(**{**fallback.__dict__, "list_path": paths.feature_plan})
372
+ calls = []
373
+
374
+ def fake_run(command, **kwargs):
375
+ calls.append(tuple(command))
376
+ return subprocess.CompletedProcess(command, 0, stdout="PIPELINE_COMPLETE\n", stderr="")
377
+
378
+ monkeypatch.setattr(subprocess, "run", fake_run)
379
+ monkeypatch.setenv("MAX_RETRIES", "6")
380
+
381
+ status_text(family, explicit, paths.project_root)
382
+ status_text(family, fallback, paths.project_root)
383
+
384
+ first = calls[0]
385
+ second = calls[1]
386
+ assert first[first.index("--max-retries") + 1] == "8"
387
+ assert second[second.index("--max-retries") + 1] == "6"
388
+
389
+
390
+ def test_stop_on_failure_does_not_stop_on_retryable_pending(monkeypatch, tmp_path):
391
+ from prizmkit_runtime import runners
392
+ from prizmkit_runtime.runner_models import family_for, parse_invocation
393
+
394
+ paths = _make_paths(tmp_path)
395
+ family = family_for("feature", paths)
396
+ invocation = parse_invocation(family, "run", ())
397
+ invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
398
+ queue = [
399
+ SimpleNamespace(ok=True, stdout=json.dumps({"feature_id": "F-001"}), data={"feature_id": "F-001"}, text=""),
400
+ SimpleNamespace(ok=True, stdout=json.dumps({"feature_id": "F-002"}), data={"feature_id": "F-002"}, text=""),
401
+ SimpleNamespace(ok=True, stdout="PIPELINE_COMPLETE\n", data=None, text="PIPELINE_COMPLETE"),
402
+ ]
403
+ processed = []
404
+
405
+ monkeypatch.setenv("STOP_ON_FAILURE", "1")
406
+ monkeypatch.setattr(runners, "get_next", lambda *args, **kwargs: queue.pop(0))
407
+
408
+ def fake_process(_family, _invocation, item_id, _paths, *, initial_metadata):
409
+ processed.append(item_id)
410
+ return "pending" if item_id == "F-001" else "completed"
411
+
412
+ monkeypatch.setattr(runners, "_process_item", fake_process)
413
+
414
+ result = runners._run_pipeline(family, invocation, paths)
415
+
416
+ assert processed == ["F-001", "F-002"]
417
+ assert result.completed is True
418
+ assert result.stopped_reason == "pipeline_complete"
419
+ assert result.details == ("F-001:pending", "F-002:completed")
420
+
421
+
422
+ def test_stop_on_failure_still_stops_on_terminal_status(monkeypatch, tmp_path):
423
+ from prizmkit_runtime import runners
424
+ from prizmkit_runtime.runner_models import family_for, parse_invocation
425
+
426
+ paths = _make_paths(tmp_path)
427
+ family = family_for("feature", paths)
428
+ invocation = parse_invocation(family, "run", ())
429
+ invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
430
+
431
+ monkeypatch.setenv("STOP_ON_FAILURE", "1")
432
+ monkeypatch.setattr(
433
+ runners,
434
+ "get_next",
435
+ lambda *args, **kwargs: SimpleNamespace(ok=True, stdout=json.dumps({"feature_id": "F-001"}), data={"feature_id": "F-001"}, text=""),
436
+ )
437
+ monkeypatch.setattr(runners, "_process_item", lambda *args, **kwargs: "failed")
438
+
439
+ result = runners._run_pipeline(family, invocation, paths)
440
+
441
+ assert result.completed is False
442
+ assert result.stopped_reason == "stop_on_failure"
443
+ assert result.last_status == "failed"
444
+
445
+
358
446
  def test_prompt_generation_result_preserves_per_item_model(monkeypatch, tmp_path):
359
447
  from prizmkit_runtime.runner_models import SessionPaths, family_for, parse_invocation
360
448
  from prizmkit_runtime.runner_prompts import generate_prompt
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.1.101",
2
+ "version": "1.1.102",
3
3
  "skills": {
4
4
  "prizm-kit": {
5
5
  "description": "Full-lifecycle dev toolkit. Covers spec-driven development, Prizm context docs, code quality, debugging, deployment, and knowledge management.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prizmkit",
3
- "version": "1.1.101",
3
+ "version": "1.1.102",
4
4
  "description": "Create a new PrizmKit-powered project with clean initialization — no framework dev files, just what you need.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/config.js CHANGED
@@ -54,7 +54,7 @@ import {
54
54
  confirmTeamMode,
55
55
  confirmPipeline,
56
56
  } from './prompts.js';
57
- import { expandPlatforms, isKnownPlatform, platformLabel, projectMemoryFile } from './platforms.js';
57
+ import { isKnownPlatform, platformLabel, projectMemoryFile, resolvePayloadPlatforms } from './platforms.js';
58
58
  import { normalizeRuntime, runtimeLabel } from './runtimes.js';
59
59
 
60
60
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -490,9 +490,9 @@ export async function runConfig(directory, options = {}) {
490
490
  }
491
491
 
492
492
  // Warn about destructive changes
493
- if (newConfig.platform !== currentConfig.platform) {
494
- const oldPlatforms = expandPlatforms(currentConfig.platform);
495
- const newPlatforms = expandPlatforms(newConfig.platform);
493
+ if (newConfig.platform !== currentConfig.platform || newConfig.aiCli !== currentConfig.aiCli) {
494
+ const oldPlatforms = resolvePayloadPlatforms(currentConfig.platform, currentConfig.aiCli);
495
+ const newPlatforms = resolvePayloadPlatforms(newConfig.platform, newConfig.aiCli);
496
496
  const dropping = oldPlatforms.filter(p => !newPlatforms.includes(p));
497
497
  if (dropping.length) {
498
498
  console.log(chalk.yellow(`\n ⚠ 将移除 ${dropping.map(platformLabel).join(', ')} 的 PrizmKit 文件`));
@@ -521,8 +521,8 @@ export async function runConfig(directory, options = {}) {
521
521
 
522
522
  console.log(chalk.bold('\n 应用配置变更...\n'));
523
523
 
524
- const oldPlatforms = expandPlatforms(currentConfig.platform);
525
- const newPlatforms = expandPlatforms(newConfig.platform);
524
+ const oldPlatforms = resolvePayloadPlatforms(currentConfig.platform, currentConfig.aiCli);
525
+ const newPlatforms = resolvePayloadPlatforms(newConfig.platform, newConfig.aiCli);
526
526
  const platformsToRemove = oldPlatforms.filter(p => !newPlatforms.includes(p));
527
527
  const platformsToAdd = newPlatforms.filter(p => !oldPlatforms.includes(p));
528
528
  const platformsToUpdate = newPlatforms.filter(p => oldPlatforms.includes(p));
@@ -554,7 +554,7 @@ export async function runConfig(directory, options = {}) {
554
554
  version: pkg.version, platform: newConfig.platform, suite: newConfig.suite,
555
555
  skills: newSkillList, agents: newAgentFiles, rules: newRuleFiles,
556
556
  pipeline: newPipelineFiles, team: newConfig.team, aiCli: newConfig.aiCli,
557
- runtime: newConfig.runtime, rulesPreset: newConfig.rules, extras: oldManifest?.files?.extras || [],
557
+ runtime: newConfig.runtime, rulesPreset: newConfig.rules, extras: oldManifest?.files?.extras || [], payloadPlatforms: newPlatforms,
558
558
  });
559
559
  const diff = diffManifest(oldManifest, newTempManifest);
560
560
 
@@ -580,6 +580,7 @@ export async function runConfig(directory, options = {}) {
580
580
  || newConfig.runtime !== currentConfig.runtime
581
581
  || newConfig.suite !== currentConfig.suite
582
582
  || newConfig.rules !== currentConfig.rules
583
+ || newConfig.aiCli !== currentConfig.aiCli
583
584
  || newConfig.team !== currentConfig.team;
584
585
 
585
586
  for (const p of allTargetPlatforms) {
@@ -675,6 +676,7 @@ export async function runConfig(directory, options = {}) {
675
676
  team: newConfig.team,
676
677
  aiCli: newConfig.aiCli,
677
678
  rulesPreset: newConfig.rules,
679
+ payloadPlatforms: newPlatforms,
678
680
  extras: oldManifest?.files?.extras || [],
679
681
  });
680
682
 
@@ -14,7 +14,7 @@
14
14
  import { execSync } from 'node:child_process';
15
15
  import path from 'path';
16
16
  import fs from 'fs-extra';
17
- import { expandPlatforms } from './platforms.js';
17
+ import { resolvePayloadPlatforms } from './platforms.js';
18
18
 
19
19
  /**
20
20
  * @param {Object} skill - Skill definition from external_skills.known
@@ -23,9 +23,9 @@ import { expandPlatforms } from './platforms.js';
23
23
  * skill.installAll {boolean} - If true, install all skills from the repo
24
24
  * @param {string} platform - 'claude' | 'codebuddy' | 'codex' | 'all' ('both' supported only for legacy manifests)
25
25
  * @param {string} projectRoot - Target project root
26
- * @param {boolean} dryRun
26
+ * @param {string} [aiCli] - optional AI CLI command that may require an additional payload platform
27
27
  */
28
- export async function installExternalSkill(skill, platform, projectRoot, dryRun) {
28
+ export async function installExternalSkill(skill, platform, projectRoot, dryRun, aiCli = '') {
29
29
  if (!skill.repo) {
30
30
  throw new Error(`Skill "${skill.name}" has no repo URL defined`);
31
31
  }
@@ -40,7 +40,7 @@ export async function installExternalSkill(skill, platform, projectRoot, dryRun)
40
40
  if (skill.installAll) {
41
41
  console.log(` [dry-run] → all skills from ${skill.repo}`);
42
42
  } else {
43
- const targets = getTargetDirs(platform, projectRoot)
43
+ const targets = getTargetDirs(platform, projectRoot, aiCli)
44
44
  .map(d => path.relative(projectRoot, path.join(d, skill.name, 'SKILL.md')));
45
45
  for (const t of targets) console.log(` [dry-run] → ${t}`);
46
46
  }
@@ -61,7 +61,7 @@ export async function installExternalSkill(skill, platform, projectRoot, dryRun)
61
61
  .filter(d => d.isDirectory())
62
62
  .map(d => d.name);
63
63
 
64
- for (const targetDir of getTargetDirs(platform, projectRoot)) {
64
+ for (const targetDir of getTargetDirs(platform, projectRoot, aiCli)) {
65
65
  for (const skillName of skillDirs) {
66
66
  const srcDir = path.join(agentsSkillsDir, skillName);
67
67
  const destDir = path.join(targetDir, skillName);
@@ -81,7 +81,7 @@ export async function installExternalSkill(skill, platform, projectRoot, dryRun)
81
81
 
82
82
  const content = await fs.readFile(canonicalSkillMd, 'utf8');
83
83
 
84
- for (const targetDir of getTargetDirs(platform, projectRoot)) {
84
+ for (const targetDir of getTargetDirs(platform, projectRoot, aiCli)) {
85
85
  const skillDir = path.join(targetDir, skill.name);
86
86
  if (path.resolve(skillDir) === path.resolve(path.dirname(canonicalSkillMd))) {
87
87
  continue;
@@ -94,7 +94,7 @@ export async function installExternalSkill(skill, platform, projectRoot, dryRun)
94
94
 
95
95
  // Clean up npx skills artifacts after copying. Preserve .agents/skills when
96
96
  // Codex is selected because it is the official repository skill location.
97
- if (!expandPlatforms(platform).includes('codex')) {
97
+ if (!resolvePayloadPlatforms(platform, aiCli).includes('codex')) {
98
98
  await fs.remove(path.join(projectRoot, '.agents')).catch(() => {});
99
99
  }
100
100
  await fs.remove(path.join(projectRoot, 'skills-lock.json')).catch(() => {});
@@ -104,15 +104,15 @@ export async function installExternalSkill(skill, platform, projectRoot, dryRun)
104
104
  * Remove all artifacts created by `npx skills` that are not needed by PrizmKit.
105
105
  * Call this once after all external skills have been installed.
106
106
  *
107
- * @param {string} projectRoot
108
107
  * @param {string} platform - 'claude' | 'codebuddy' | 'codex' | 'all' ('both' supported only for legacy manifests)
108
+ * @param {string} [aiCli] - optional AI CLI command that may require an additional payload platform
109
109
  */
110
- export async function cleanExternalSkillArtifacts(projectRoot, platform) {
110
+ export async function cleanExternalSkillArtifacts(projectRoot, platform, aiCli = '') {
111
111
  const keepDirs = new Set(
112
- getTargetDirs(platform, projectRoot).map(d => path.resolve(d))
112
+ getTargetDirs(platform, projectRoot, aiCli).map(d => path.resolve(d))
113
113
  );
114
114
 
115
- const selectedPlatforms = expandPlatforms(platform);
115
+ const selectedPlatforms = resolvePayloadPlatforms(platform, aiCli);
116
116
  const dirsToCheck = [
117
117
  selectedPlatforms.includes('codex') ? null : '.agents',
118
118
  '.agent',
@@ -152,7 +152,7 @@ export async function cleanExternalSkillArtifacts(projectRoot, platform) {
152
152
  const abs = path.join(projectRoot, dir);
153
153
  if (!await fs.pathExists(abs)) continue;
154
154
  // Only remove if this platform dir is entirely unneeded
155
- const isKept = getTargetDirs(platform, projectRoot)
155
+ const isKept = getTargetDirs(platform, projectRoot, aiCli)
156
156
  .some(d => path.resolve(d).startsWith(path.resolve(abs) + path.sep));
157
157
  if (isKept) continue;
158
158
  try {
@@ -166,8 +166,8 @@ export async function cleanExternalSkillArtifacts(projectRoot, platform) {
166
166
  await fs.remove(path.join(projectRoot, 'skills-lock.json')).catch(() => {});
167
167
  }
168
168
 
169
- function getTargetDirs(platform, projectRoot) {
170
- return expandPlatforms(platform).map(p => {
169
+ function getTargetDirs(platform, projectRoot, aiCli = '') {
170
+ return resolvePayloadPlatforms(platform, aiCli).map(p => {
171
171
  if (p === 'codebuddy') return path.join(projectRoot, '.codebuddy', 'skills');
172
172
  if (p === 'codex') return path.join(projectRoot, '.agents', 'skills');
173
173
  return path.join(projectRoot, '.claude', 'skills');
package/src/manifest.js CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  import fs from 'fs-extra';
11
11
  import path from 'path';
12
- import { expandPlatforms, projectMemoryFile } from './platforms.js';
12
+ import { projectMemoryFile, resolvePayloadPlatforms } from './platforms.js';
13
13
  import { normalizeRuntime } from './runtimes.js';
14
14
 
15
15
  const MANIFEST_FILE = 'manifest.json';
@@ -58,7 +58,7 @@ export async function writeManifest(projectRoot, data) {
58
58
  * @param {boolean} params.team - whether team config was installed
59
59
  * @param {string} [params.aiCli] - AI CLI command
60
60
  * @param {string} [params.rulesPreset] - rules preset name
61
- * @param {string[]} [params.extras] - installed extras (e.g. ['playwright-cli'])
61
+ * @param {string[]} [params.payloadPlatforms] - concrete platform payloads installed for platform + aiCli
62
62
  * @returns {Object} manifest
63
63
  */
64
64
  export function buildManifest({
@@ -74,8 +74,12 @@ export function buildManifest({
74
74
  aiCli,
75
75
  rulesPreset,
76
76
  extras,
77
+ payloadPlatforms,
77
78
  }) {
78
79
  const now = new Date().toISOString();
80
+ const installedPayloadPlatforms = payloadPlatforms?.length
81
+ ? [...new Set(payloadPlatforms)]
82
+ : resolvePayloadPlatforms(platform, aiCli);
79
83
  return {
80
84
  version,
81
85
  installedAt: now,
@@ -95,7 +99,8 @@ export function buildManifest({
95
99
  rules: [...rules],
96
100
  pipeline: pipeline ? [...pipeline] : [],
97
101
  extras: extras ? [...extras] : [],
98
- other: [...new Set(expandPlatforms(platform).map(projectMemoryFile).filter(Boolean))],
102
+ payloadPlatforms: installedPayloadPlatforms,
103
+ other: [...new Set(installedPayloadPlatforms.map(projectMemoryFile).filter(Boolean))],
99
104
  },
100
105
  };
101
106
  }
package/src/platforms.js CHANGED
@@ -10,6 +10,31 @@ export function expandPlatforms(platform) {
10
10
  return PLATFORM_GROUPS[platform] || LEGACY_PLATFORM_GROUPS[platform] || [platform];
11
11
  }
12
12
 
13
+ export function platformFromAiCli(aiCli) {
14
+ if (!aiCli || typeof aiCli !== 'string') return null;
15
+ const token = aiCli.trim().split(/\s+/)[0]?.replace(/^['"]|['"]$/g, '');
16
+ const name = token.split(/[\\/]/).pop()?.toLowerCase() || '';
17
+ if (name.includes('claude')) return 'claude';
18
+ if (name.includes('codex')) return 'codex';
19
+ if (name.includes('cbc') || name.includes('codebuddy')) return 'codebuddy';
20
+ return null;
21
+ }
22
+
23
+ export function uniquePlatforms(platforms) {
24
+ return [...new Set(platforms.filter(p => PLATFORM_IDS.includes(p)))];
25
+ }
26
+
27
+ export function resolvePayloadPlatforms(platform, aiCli = '') {
28
+ const selected = uniquePlatforms(expandPlatforms(platform));
29
+ const cliPlatform = platformFromAiCli(aiCli);
30
+ return uniquePlatforms([...selected, cliPlatform].filter(Boolean));
31
+ }
32
+
33
+ export function payloadPlatformLabel(platform, aiCli = '') {
34
+ const payload = resolvePayloadPlatforms(platform, aiCli);
35
+ return payload.map(platformLabel).join(' + ');
36
+ }
37
+
13
38
  export function isKnownPlatform(platform) {
14
39
  return PLATFORM_IDS.includes(platform) || Object.hasOwn(PLATFORM_GROUPS, platform);
15
40
  }
package/src/scaffold.js CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  } from './metadata.js';
28
28
  import { generateGitignore } from './gitignore-template.js';
29
29
  import { buildManifest, writeManifest } from './manifest.js';
30
- import { defaultCliForPlatform, expandPlatforms, isKnownPlatform, platformLabel, projectMemoryFile } from './platforms.js';
30
+ import { defaultCliForPlatform, expandPlatforms, isKnownPlatform, platformLabel, projectMemoryFile, resolvePayloadPlatforms } from './platforms.js';
31
31
  import { normalizeRuntime, runtimeLabel } from './runtimes.js';
32
32
 
33
33
  const __scaffoldDirname = dirname(fileURLToPath(import.meta.url));
@@ -1436,6 +1436,7 @@ export async function scaffold(config) {
1436
1436
  throw new Error(`Unknown platform "${platform}". Expected codebuddy, claude, codex, or all.`);
1437
1437
  }
1438
1438
  const platforms = expandPlatforms(platform);
1439
+ const payloadPlatforms = resolvePayloadPlatforms(platform, aiCli);
1439
1440
 
1440
1441
  if (dryRun) {
1441
1442
  console.log(chalk.yellow('\n [DRY RUN] 预览将要安装的内容:\n'));
@@ -1446,8 +1447,8 @@ export async function scaffold(config) {
1446
1447
 
1447
1448
  console.log('');
1448
1449
 
1449
- for (const p of platforms) {
1450
- console.log(chalk.bold(` 正在安装 ${platformLabel(p)} 环境...\n`));
1450
+ for (const p of payloadPlatforms) {
1451
+ console.log(chalk.bold(` 正在安装 ${platformLabel(p)} 环境...${platforms.includes(p) ? '' : chalk.gray(' (AI CLI payload)')}\n`));
1451
1452
 
1452
1453
  // 1. Skills
1453
1454
  console.log(chalk.blue(' 技能文件:'));
@@ -1515,7 +1516,7 @@ export async function scaffold(config) {
1515
1516
  const skillDef = knownSkills.find(s => s.name === name);
1516
1517
  if (skillDef) {
1517
1518
  try {
1518
- await installExternalSkill(skillDef, platform, projectRoot, dryRun);
1519
+ await installExternalSkill(skillDef, platform, projectRoot, dryRun, aiCli);
1519
1520
  console.log(chalk.green(` ✓ ${name} (external)`));
1520
1521
  } catch (e) {
1521
1522
  console.log(chalk.yellow(` ⚠ ${name} 安装失败,跳过: ${e.message}`));
@@ -1523,7 +1524,7 @@ export async function scaffold(config) {
1523
1524
  }
1524
1525
  }
1525
1526
  if (!dryRun) {
1526
- await cleanExternalSkillArtifacts(projectRoot, platform);
1527
+ await cleanExternalSkillArtifacts(projectRoot, platform, aiCli);
1527
1528
  }
1528
1529
  }
1529
1530
 
@@ -1545,7 +1546,7 @@ export async function scaffold(config) {
1545
1546
  // 11. Clean up stray .codebuddy/ directory left by third-party tools (e.g. npx skills)
1546
1547
  // when installing for Claude Code only. CodeBuddy files should never appear in a
1547
1548
  // claude-only install.
1548
- if (!dryRun && !platforms.includes('codebuddy')) {
1549
+ if (!dryRun && !payloadPlatforms.includes('codebuddy')) {
1549
1550
  const strayCbDir = path.join(projectRoot, '.codebuddy');
1550
1551
  if (await fs.pathExists(strayCbDir)) {
1551
1552
  const entries = await fs.readdir(strayCbDir);
@@ -1584,6 +1585,7 @@ export async function scaffold(config) {
1584
1585
  aiCli,
1585
1586
  rulesPreset: rulesPresetName,
1586
1587
  extras: activeExtras,
1588
+ payloadPlatforms,
1587
1589
  });
1588
1590
  await writeManifest(projectRoot, manifest);
1589
1591
  console.log(chalk.green(' ✓ .prizmkit/manifest.json'));
@@ -1632,6 +1634,9 @@ export async function scaffold(config) {
1632
1634
  console.log(chalk.gray(` 安装统计:`));
1633
1635
  console.log(chalk.gray(` 技能: ${suiteLabel}`));
1634
1636
  console.log(chalk.gray(` 平台: ${platforms.map(platformLabel).join(' + ')}`));
1637
+ if (payloadPlatforms.length !== platforms.length || payloadPlatforms.some(p => !platforms.includes(p))) {
1638
+ console.log(chalk.gray(` AI CLI 载荷平台: ${payloadPlatforms.map(platformLabel).join(' + ')}`));
1639
+ }
1635
1640
  console.log(chalk.gray(` 运行时: ${runtimeLabel(runtime)}`));
1636
1641
  console.log(chalk.gray(` 团队: ${team ? '已启用' : '未启用'}`));
1637
1642
  console.log(chalk.gray(` 流水线: ${pipeline ? '已安装' : '未安装'}`));
package/src/upgrade.js CHANGED
@@ -27,6 +27,7 @@ import {
27
27
  installGitHook,
28
28
  installGitignore,
29
29
  installProjectMemory,
30
+ installTeamConfig,
30
31
  resolvePipelineFileList,
31
32
  findStaleManagedPipelineFiles,
32
33
  removeStaleManagedPipelineFiles,
@@ -34,7 +35,7 @@ import {
34
35
  resolveSkillList,
35
36
  EXTRAS_REGISTRY,
36
37
  } from './scaffold.js';
37
- import { expandPlatforms, platformLabel } from './platforms.js';
38
+ import { platformLabel, resolvePayloadPlatforms } from './platforms.js';
38
39
  import { normalizeRuntime, runtimeLabel } from './runtimes.js';
39
40
 
40
41
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -276,6 +277,8 @@ export async function runUpgrade(directory, options = {}) {
276
277
  ...(!oldManifest?.files?.extras ? Object.keys(EXTRAS_REGISTRY) : []),
277
278
  ])].filter(name => EXTRAS_REGISTRY[name]); // only keep extras that still exist in registry
278
279
 
280
+ const payloadPlatforms = resolvePayloadPlatforms(platform, aiCli);
281
+
279
282
  // 4. Build new manifest and compute diff
280
283
  const newManifest = buildManifest({
281
284
  version: pkg.version,
@@ -290,6 +293,7 @@ export async function runUpgrade(directory, options = {}) {
290
293
  aiCli,
291
294
  rulesPreset,
292
295
  extras: extrasToInstall,
296
+ payloadPlatforms,
293
297
  });
294
298
 
295
299
  // Preserve original installedAt
@@ -349,7 +353,7 @@ export async function runUpgrade(directory, options = {}) {
349
353
  }
350
354
 
351
355
  // 7. Execute
352
- const platforms = expandPlatforms(platform);
356
+ const platforms = payloadPlatforms;
353
357
 
354
358
  // 7a. Remove orphaned files
355
359
  if (diff.skills.removed.length || diff.agents.removed.length || diff.rules.removed.length || diff.pipeline.removed.length || staleManagedPipelineFiles.length) {
@@ -396,6 +400,11 @@ export async function runUpgrade(directory, options = {}) {
396
400
  console.log(chalk.blue('\n Settings & Rules:'));
397
401
  await installSettings(p, projectRoot, { pipeline, rules: rulesPreset }, dryRun, runtime);
398
402
 
403
+ if (team) {
404
+ console.log(chalk.blue('\n Team Config:'));
405
+ await installTeamConfig(p, projectRoot, dryRun);
406
+ }
407
+
399
408
  console.log('');
400
409
  }
401
410