prizmkit 1.1.108 → 1.1.110

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 (46) 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 +44 -71
  14. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +48 -75
  15. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +52 -78
  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/context-budget-rules.md +8 -8
  20. package/bundled/dev-pipeline/templates/sections/phase-browser-verification-auto.md +33 -55
  21. package/bundled/dev-pipeline/templates/sections/phase-browser-verification-opencli.md +28 -33
  22. package/bundled/dev-pipeline/templates/sections/phase-browser-verification.md +22 -62
  23. package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +2 -2
  24. package/bundled/dev-pipeline/templates/sections/phase-commit.md +1 -1
  25. package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-base.md +1 -1
  26. package/bundled/dev-pipeline/templates/sections/phase-critic-plan-full.md +1 -1
  27. package/bundled/dev-pipeline/templates/sections/phase-critic-plan.md +1 -1
  28. package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +1 -1
  29. package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +1 -1
  30. package/bundled/dev-pipeline/templates/sections/phase-plan-agent.md +1 -1
  31. package/bundled/dev-pipeline/templates/sections/phase-plan-lite.md +1 -1
  32. package/bundled/dev-pipeline/templates/sections/phase-prizmkit-test.md +2 -2
  33. package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +1 -1
  34. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +1 -1
  35. package/bundled/dev-pipeline/templates/sections/phase-specify-plan-full.md +7 -6
  36. package/bundled/dev-pipeline/templates/sections/phase0-init.md +1 -1
  37. package/bundled/dev-pipeline/templates/sections/subagent-timeout-recovery.md +1 -1
  38. package/bundled/dev-pipeline/templates/sections/test-failure-recovery-agent.md +1 -1
  39. package/bundled/dev-pipeline/templates/sections/test-failure-recovery-lite.md +1 -1
  40. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +152 -3
  41. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +2 -1
  42. package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +2 -1
  43. package/bundled/dev-pipeline/tests/test_runtime_helper.py +211 -0
  44. package/bundled/dev-pipeline/tests/test_unified_cli.py +5 -0
  45. package/bundled/skills/_metadata.json +1 -1
  46. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  {
2
- "frameworkVersion": "1.1.108",
3
- "bundledAt": "2026-07-08T01:30:44.419Z",
4
- "bundledFrom": "d04466a"
2
+ "frameworkVersion": "1.1.110",
3
+ "bundledAt": "2026-07-08T05:42:13.015Z",
4
+ "bundledFrom": "84797b8"
5
5
  }
@@ -9,6 +9,7 @@ from collections.abc import Sequence
9
9
  from .commands import CommandResult, handle_diagnostics, handle_runtime_command
10
10
  from .compat import UnsupportedPythonVersion, ensure_supported_python
11
11
  from .paths import resolve_runtime_paths
12
+ from .runtime_helper import render_result, run_helper_command
12
13
 
13
14
  DESCRIPTION = (
14
15
  "PrizmKit canonical Python runtime CLI.\n"
@@ -81,6 +82,16 @@ def _add_diagnostics_subparsers(subparsers: argparse._SubParsersAction[argparse.
81
82
  action_parser.set_defaults(handler="diagnostics", action=action)
82
83
 
83
84
 
85
+ def _add_helper_subparser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
86
+ helper_parser = subparsers.add_parser("helper", help="Cross-platform prompt runtime helper commands")
87
+ helper_parser.add_argument(
88
+ "args",
89
+ nargs=argparse.REMAINDER,
90
+ help="Arguments forwarded to prizmkit-runtime-helper.",
91
+ )
92
+ helper_parser.set_defaults(handler="helper", group="helper", action="run", target=None)
93
+
94
+
84
95
  _PIPELINE_GROUPS = {"feature", "bugfix", "refactor", "recovery"}
85
96
  _GLOBAL_OPTIONS_WITH_VALUES = {"--project-root", "--pipeline-root"}
86
97
 
@@ -150,6 +161,7 @@ def build_parser() -> argparse.ArgumentParser:
150
161
  _add_daemon_subparsers(subparsers)
151
162
  _add_status_subparsers(subparsers)
152
163
  _add_diagnostics_subparsers(subparsers)
164
+ _add_helper_subparser(subparsers)
153
165
  return parser
154
166
 
155
167
 
@@ -161,6 +173,14 @@ def _dispatch(args: argparse.Namespace) -> CommandResult:
161
173
  paths = _runtime_paths_from_args(args)
162
174
  if args.handler == "diagnostics":
163
175
  return handle_diagnostics(args.action, paths)
176
+ if args.handler == "helper":
177
+ helper_result = run_helper_command(tuple(getattr(args, "args", ()) or ()))
178
+ return CommandResult(
179
+ helper_result.exit_code,
180
+ "Python runtime helper",
181
+ "Cross-platform prompt runtime helper result.",
182
+ stdout=render_result(helper_result, json_output="--json" in tuple(getattr(args, "args", ()) or ())),
183
+ )
164
184
  return handle_runtime_command(
165
185
  args.group,
166
186
  args.action,
@@ -280,6 +280,7 @@ def diagnostics_entrypoints(paths: RuntimePaths | None = None) -> CommandResult:
280
280
  details: list[str] = [
281
281
  f"runtime:cli: {runtime_paths.pipeline_root / 'cli.py'}",
282
282
  f"runtime:module: {runtime_paths.pipeline_root / 'prizmkit_runtime'}",
283
+ f"runtime:helper-module: {runtime_paths.pipeline_root / 'prizmkit_runtime' / 'runtime_helper.py'}",
283
284
  ]
284
285
  for name, relative_path in sorted(UTILITY_ENTRYPOINTS.items()):
285
286
  invocation = describe_existing_entrypoint("utility", name, paths=runtime_paths)
@@ -33,6 +33,8 @@ HIDDEN_TOOL_WORKTREE_EXCLUDES = (
33
33
  ":(top,exclude).prizmkit/manifest.json",
34
34
  ":(top,exclude).prizmkit/dev-pipeline/scripts",
35
35
  ":(top,exclude,glob).prizmkit/dev-pipeline/scripts/**",
36
+ ":(top,exclude).prizmkit/dev-pipeline/prizmkit_runtime",
37
+ ":(top,exclude,glob).prizmkit/dev-pipeline/prizmkit_runtime/**",
36
38
  )
37
39
 
38
40
  WORKTREE_PLATFORM_SUPPORT_DIRS = (".claude", ".codebuddy", ".codex", ".agents")
@@ -242,6 +244,14 @@ def materialize_worktree_support_assets(runtime: WorktreeRuntimeContext) -> Work
242
244
  target = target_root / ".prizmkit" / "dev-pipeline" / "scripts"
243
245
  changed = _copy_support_entry(runtime_scripts, target)
244
246
  (copied if changed else existing).append(target)
247
+
248
+ runtime_package = _runtime_support_package_source(source_root)
249
+ if runtime_package is None:
250
+ missing.append(".prizmkit/dev-pipeline/prizmkit_runtime")
251
+ else:
252
+ target = target_root / ".prizmkit" / "dev-pipeline" / "prizmkit_runtime"
253
+ changed = _copy_support_entry(runtime_package, target)
254
+ (copied if changed else existing).append(target)
245
255
  except OSError as exc:
246
256
  return WorktreeSupportAssetsResult(False, tuple(copied), tuple(existing), tuple(missing), str(exc))
247
257
 
@@ -276,6 +286,17 @@ def _runtime_support_scripts_source(project_root: Path) -> Path | None:
276
286
  return None
277
287
 
278
288
 
289
+ def _runtime_support_package_source(project_root: Path) -> Path | None:
290
+ candidates = (
291
+ project_root / ".prizmkit" / "dev-pipeline" / "prizmkit_runtime",
292
+ project_root / "dev-pipeline" / "prizmkit_runtime",
293
+ )
294
+ for candidate in candidates:
295
+ if candidate.is_dir():
296
+ return candidate
297
+ return None
298
+
299
+
279
300
  def _copy_support_entry(source: Path, target: Path) -> bool:
280
301
  if source.is_symlink():
281
302
  if target.exists() or target.is_symlink():
@@ -24,6 +24,7 @@ UTILITY_ENTRYPOINTS = {
24
24
  "init-refactor-pipeline": "scripts/init-refactor-pipeline.py",
25
25
  "parse-stream-progress": "scripts/parse-stream-progress.py",
26
26
  "patch-completion-notes": "scripts/patch-completion-notes.py",
27
+ "prizmkit-runtime-helper": "scripts/prizmkit-runtime-helper.py",
27
28
  "update-bug-status": "scripts/update-bug-status.py",
28
29
  "update-checkpoint": "scripts/update-checkpoint.py",
29
30
  "update-feature-status": "scripts/update-feature-status.py",
@@ -0,0 +1,208 @@
1
+ """Shared AI platform and skill path detection for prompt/runtime helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from collections.abc import Mapping
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+
11
+ SUPPORTED_PLATFORMS = ("claude", "codex", "codebuddy")
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class PlatformResolution:
16
+ """Resolved AI platform plus the source that selected it."""
17
+
18
+ platform: str
19
+ source: str
20
+ notes: tuple[str, ...] = ()
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class SkillCandidate:
25
+ """One possible filesystem location for a skill."""
26
+
27
+ platform: str
28
+ scope: str
29
+ path: Path
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class SkillResolution:
34
+ """Resolved skill lookup result."""
35
+
36
+ marker: str
37
+ platform: str
38
+ skill: str
39
+ path: Path | None
40
+ candidates: tuple[SkillCandidate, ...]
41
+
42
+
43
+ _PLATFORM_MARKERS = {
44
+ "claude": (".claude/agents", ".claude/commands", ".claude/skills"),
45
+ "codex": (".codex/agents", ".agents/skills"),
46
+ "codebuddy": (".codebuddy/agents", ".codebuddy/skills"),
47
+ }
48
+
49
+
50
+ _PLATFORM_SKILL_PATTERNS = {
51
+ "claude": (
52
+ ("project-command-file", ".claude/commands/{skill}.md"),
53
+ ("project-command-dir", ".claude/commands/{skill}"),
54
+ ("project-skill", ".claude/skills/{skill}"),
55
+ ("home-skill", "~/.claude/skills/{skill}"),
56
+ ),
57
+ "codex": (
58
+ ("project-skill", ".agents/skills/{skill}"),
59
+ ("project-codex-skill", ".codex/skills/{skill}"),
60
+ ),
61
+ "codebuddy": (
62
+ ("project-skill", ".codebuddy/skills/{skill}"),
63
+ ("home-skill", "~/.codebuddy/skills/{skill}"),
64
+ ("legacy-home-skill", "~/.cbc/skills/{skill}"),
65
+ ),
66
+ }
67
+
68
+
69
+ def read_json_object(path: str | Path) -> dict:
70
+ """Read a JSON object, returning an empty dict when unavailable."""
71
+ try:
72
+ with Path(path).expanduser().open("r", encoding="utf-8") as handle:
73
+ data = json.load(handle)
74
+ except (OSError, json.JSONDecodeError):
75
+ return {}
76
+ return data if isinstance(data, dict) else {}
77
+
78
+
79
+ def platform_from_command(command: object) -> str:
80
+ """Map an AI CLI command/config value to a PrizmKit platform name."""
81
+ cmd = str(command or "").strip().lower()
82
+ if not cmd:
83
+ return ""
84
+ base = os.path.basename(cmd.split()[0])
85
+ if base in ("claude", "claude-code") or "claude" in base:
86
+ return "claude"
87
+ if base in ("codex", "openai-codex") or "codex" in base:
88
+ return "codex"
89
+ if base in ("cbc", "codebuddy") or "codebuddy" in base:
90
+ return "codebuddy"
91
+ return ""
92
+
93
+
94
+ def valid_prompt_platform(value: object) -> str:
95
+ """Normalize a platform value for prompt path rendering."""
96
+ platform = str(value or "").strip().lower()
97
+ return platform if platform in SUPPORTED_PLATFORMS else ""
98
+
99
+
100
+ def resolve_platform(project_root: str | Path, env: Mapping[str, str] | None = None) -> PlatformResolution:
101
+ """Resolve the platform whose paths should appear in prompts/helper output.
102
+
103
+ Precedence mirrors prompt generation: explicit environment, active/configured
104
+ AI CLI, PrizmKit config/manifest platform, then filesystem markers.
105
+ """
106
+ environment = os.environ if env is None else env
107
+ root = Path(project_root).expanduser().resolve()
108
+
109
+ explicit = valid_prompt_platform(environment.get("PRIZMKIT_PLATFORM", ""))
110
+ if explicit:
111
+ return PlatformResolution(explicit, "environment:PRIZMKIT_PLATFORM")
112
+
113
+ for env_name in ("AI_CLI", "CLAUDECODE", "CLAUDE_CODE", "CODEBUDDY_CLI"):
114
+ detected = platform_from_command(environment.get(env_name, ""))
115
+ if detected:
116
+ return PlatformResolution(detected, f"environment:{env_name}")
117
+
118
+ config = read_json_object(root / ".prizmkit" / "config.json")
119
+ for key in ("ai_cli", "aiCli"):
120
+ detected = platform_from_command(config.get(key, ""))
121
+ if detected:
122
+ return PlatformResolution(detected, f"config:{key}")
123
+ detected = valid_prompt_platform(config.get("platform", ""))
124
+ if detected:
125
+ return PlatformResolution(detected, "config:platform")
126
+
127
+ manifest = read_json_object(root / ".prizmkit" / "manifest.json")
128
+ options = manifest.get("options", {}) if isinstance(manifest.get("options"), dict) else {}
129
+ for key, value in (("aiCli", manifest.get("aiCli", "")), ("options.aiCli", options.get("aiCli", ""))):
130
+ detected = platform_from_command(value)
131
+ if detected:
132
+ return PlatformResolution(detected, f"manifest:{key}")
133
+ detected = valid_prompt_platform(manifest.get("platform", ""))
134
+ if detected:
135
+ return PlatformResolution(detected, "manifest:platform")
136
+
137
+ for platform in SUPPORTED_PLATFORMS:
138
+ for marker in _PLATFORM_MARKERS[platform]:
139
+ if (root / marker).is_dir():
140
+ return PlatformResolution(platform, f"filesystem:{marker}")
141
+
142
+ return PlatformResolution(
143
+ "",
144
+ "unresolved",
145
+ (
146
+ "No supported platform marker found. Set PRIZMKIT_PLATFORM, configure .prizmkit/config.json, "
147
+ "or install Claude/Codex/CodeBuddy platform files.",
148
+ ),
149
+ )
150
+
151
+
152
+ def _expand_skill_pattern(project_root: Path, skill: str, platform: str, scope: str, pattern: str) -> SkillCandidate:
153
+ rendered = pattern.format(skill=skill)
154
+ if rendered.startswith("~"):
155
+ path = Path(rendered).expanduser()
156
+ else:
157
+ path = project_root / rendered
158
+ return SkillCandidate(platform=platform, scope=scope, path=path.resolve())
159
+
160
+
161
+ def skill_candidates(project_root: str | Path, skill: str, platform: str) -> tuple[SkillCandidate, ...]:
162
+ """Return deterministic candidate locations for a skill on one platform."""
163
+ normalized = valid_prompt_platform(platform)
164
+ if not normalized:
165
+ return ()
166
+ root = Path(project_root).expanduser().resolve()
167
+ return tuple(
168
+ _expand_skill_pattern(root, skill, normalized, scope, pattern)
169
+ for scope, pattern in _PLATFORM_SKILL_PATTERNS[normalized]
170
+ )
171
+
172
+
173
+ def _candidate_matches(candidate: SkillCandidate, skill: str, *, prefix: bool) -> bool:
174
+ path = candidate.path
175
+ if path.exists():
176
+ return True
177
+ if not prefix:
178
+ return False
179
+ parent = path.parent
180
+ if not parent.is_dir():
181
+ return False
182
+ return any(child.name.startswith(skill) for child in parent.iterdir())
183
+
184
+
185
+ def resolve_skill(
186
+ project_root: str | Path,
187
+ skill: str,
188
+ *,
189
+ platform: str = "auto",
190
+ env: Mapping[str, str] | None = None,
191
+ prefix: bool = False,
192
+ ) -> SkillResolution:
193
+ """Resolve a skill path for the selected platform."""
194
+ normalized_skill = str(skill or "").strip()
195
+ selected_platform = valid_prompt_platform(platform)
196
+ if platform == "auto" or not platform:
197
+ selected_platform = resolve_platform(project_root, env=env).platform
198
+ if not selected_platform:
199
+ return SkillResolution("NOT_INSTALLED", "", normalized_skill, None, ())
200
+
201
+ candidates = skill_candidates(project_root, normalized_skill, selected_platform)
202
+ for candidate in candidates:
203
+ if _candidate_matches(candidate, normalized_skill, prefix=prefix):
204
+ if candidate.path.exists():
205
+ return SkillResolution("SKILL_EXISTS", selected_platform, normalized_skill, candidate.path, candidates)
206
+ matches = sorted(child for child in candidate.path.parent.iterdir() if child.name.startswith(normalized_skill))
207
+ return SkillResolution("SKILL_EXISTS", selected_platform, normalized_skill, matches[0].resolve(), candidates)
208
+ return SkillResolution("SKILL_MISSING", selected_platform, normalized_skill, None, candidates)