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.
- package/bundled/VERSION.json +3 -3
- package/bundled/dev-pipeline/prizmkit_runtime/cli.py +20 -0
- package/bundled/dev-pipeline/prizmkit_runtime/commands.py +1 -0
- package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +21 -0
- package/bundled/dev-pipeline/prizmkit_runtime/interoperability.py +1 -0
- package/bundled/dev-pipeline/prizmkit_runtime/platform_detection.py +208 -0
- package/bundled/dev-pipeline/prizmkit_runtime/runtime_helper.py +508 -0
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +3 -1
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +3 -1
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +3 -1
- package/bundled/dev-pipeline/scripts/prizmkit-runtime-helper.py +27 -0
- package/bundled/dev-pipeline/scripts/utils.py +48 -77
- package/bundled/dev-pipeline/templates/bootstrap-tier1.md +33 -60
- package/bundled/dev-pipeline/templates/bootstrap-tier2.md +35 -62
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +40 -66
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +2 -2
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +2 -2
- package/bundled/dev-pipeline/templates/sections/checkpoint-system.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-browser-verification-auto.md +31 -54
- package/bundled/dev-pipeline/templates/sections/phase-browser-verification-opencli.md +21 -30
- package/bundled/dev-pipeline/templates/sections/phase-browser-verification.md +22 -62
- package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-base.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-critic-plan-full.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-critic-plan.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-plan-agent.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-plan-lite.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-prizmkit-test.md +2 -2
- package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-review-full.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-specify-plan-full.md +7 -6
- package/bundled/dev-pipeline/templates/sections/phase0-init.md +1 -1
- package/bundled/dev-pipeline/templates/sections/subagent-timeout-recovery.md +1 -1
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +45 -3
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +2 -1
- package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +2 -1
- package/bundled/dev-pipeline/tests/test_runtime_helper.py +211 -0
- package/bundled/dev-pipeline/tests/test_unified_cli.py +5 -0
- package/bundled/skills/_metadata.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
"""Cross-platform helper CLI for prompt runtime operations.
|
|
2
|
+
|
|
3
|
+
The helpers in this module use Python standard-library APIs for file checks,
|
|
4
|
+
source discovery, text search, platform detection, executable checks, port
|
|
5
|
+
readiness, process cleanup, and lightweight browser-readiness polling. Stdout
|
|
6
|
+
starts with deterministic markers so generated AI prompts can branch on text
|
|
7
|
+
instead of shell exit-code chains.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import shutil
|
|
16
|
+
import socket
|
|
17
|
+
import subprocess
|
|
18
|
+
import sys
|
|
19
|
+
import time
|
|
20
|
+
import urllib.error
|
|
21
|
+
import urllib.request
|
|
22
|
+
from dataclasses import asdict, dataclass
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from .paths import resolve_runtime_paths
|
|
27
|
+
from .platform_detection import resolve_platform, resolve_skill
|
|
28
|
+
from .processes import is_pid_running, terminate_pid_tree
|
|
29
|
+
|
|
30
|
+
SOURCE_EXTENSIONS = (
|
|
31
|
+
".js",
|
|
32
|
+
".jsx",
|
|
33
|
+
".ts",
|
|
34
|
+
".tsx",
|
|
35
|
+
".mjs",
|
|
36
|
+
".cjs",
|
|
37
|
+
".py",
|
|
38
|
+
".go",
|
|
39
|
+
".rs",
|
|
40
|
+
".java",
|
|
41
|
+
".kt",
|
|
42
|
+
".rb",
|
|
43
|
+
".php",
|
|
44
|
+
".cs",
|
|
45
|
+
".swift",
|
|
46
|
+
".sh",
|
|
47
|
+
".ps1",
|
|
48
|
+
".md",
|
|
49
|
+
".json",
|
|
50
|
+
".yaml",
|
|
51
|
+
".yml",
|
|
52
|
+
".toml",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
DEFAULT_EXCLUDES = (
|
|
56
|
+
".git",
|
|
57
|
+
"node_modules",
|
|
58
|
+
"__pycache__",
|
|
59
|
+
".pytest_cache",
|
|
60
|
+
".mypy_cache",
|
|
61
|
+
"dist",
|
|
62
|
+
"build",
|
|
63
|
+
"vendor",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class HelperResult:
|
|
69
|
+
"""Structured helper result rendered as marker text or JSON."""
|
|
70
|
+
|
|
71
|
+
marker: str
|
|
72
|
+
ok: bool
|
|
73
|
+
fields: dict[str, Any]
|
|
74
|
+
exit_code: int = 0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _path(value: str | Path) -> Path:
|
|
78
|
+
return Path(value).expanduser().resolve()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _relative(path: Path, root: Path) -> str:
|
|
82
|
+
try:
|
|
83
|
+
return path.relative_to(root).as_posix()
|
|
84
|
+
except ValueError:
|
|
85
|
+
return path.as_posix()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def render_result(result: HelperResult, *, json_output: bool = False) -> str:
|
|
89
|
+
"""Render a helper result without writing to stdout."""
|
|
90
|
+
if json_output:
|
|
91
|
+
payload = {"marker": result.marker, "ok": result.ok, **result.fields}
|
|
92
|
+
return json.dumps(payload, ensure_ascii=False, sort_keys=True)
|
|
93
|
+
|
|
94
|
+
lines = [result.marker]
|
|
95
|
+
for key, value in result.fields.items():
|
|
96
|
+
if value is None:
|
|
97
|
+
continue
|
|
98
|
+
if isinstance(value, (list, tuple)):
|
|
99
|
+
for item in value:
|
|
100
|
+
lines.append(f"{key}: {item}")
|
|
101
|
+
elif isinstance(value, dict):
|
|
102
|
+
lines.append(f"{key}: {json.dumps(value, ensure_ascii=False, sort_keys=True)}")
|
|
103
|
+
else:
|
|
104
|
+
lines.append(f"{key}: {value}")
|
|
105
|
+
return "\n".join(lines)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _emit(result: HelperResult, *, json_output: bool = False) -> int:
|
|
109
|
+
print(render_result(result, json_output=json_output))
|
|
110
|
+
return result.exit_code
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _add_json(parser: argparse.ArgumentParser) -> None:
|
|
114
|
+
parser.add_argument("--json", action="store_true", help="Emit a single JSON object instead of marker text.")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _iter_files(root: Path, *, excludes: tuple[str, ...] = DEFAULT_EXCLUDES):
|
|
118
|
+
if root.is_file():
|
|
119
|
+
yield root
|
|
120
|
+
return
|
|
121
|
+
if not root.is_dir():
|
|
122
|
+
return
|
|
123
|
+
for current_root, dir_names, file_names in os.walk(root):
|
|
124
|
+
dir_names[:] = sorted(name for name in dir_names if name not in excludes)
|
|
125
|
+
for file_name in sorted(file_names):
|
|
126
|
+
yield Path(current_root) / file_name
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _artifact_exists(args: argparse.Namespace) -> HelperResult:
|
|
130
|
+
target = _path(args.path)
|
|
131
|
+
exists = target.exists()
|
|
132
|
+
exists_marker = getattr(args, "exists_marker", "EXISTS")
|
|
133
|
+
missing_marker = getattr(args, "missing_marker", "MISSING")
|
|
134
|
+
return HelperResult(exists_marker if exists else missing_marker, exists, {"path": str(target), "type": "dir" if target.is_dir() else "file" if target.is_file() else "missing"})
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _artifact_ensure_dir(args: argparse.Namespace) -> HelperResult:
|
|
138
|
+
target = _path(args.path)
|
|
139
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
140
|
+
return HelperResult("DIR_READY", True, {"path": str(target)})
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _artifact_touch(args: argparse.Namespace) -> HelperResult:
|
|
144
|
+
target = _path(args.path)
|
|
145
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
146
|
+
target.touch(exist_ok=True)
|
|
147
|
+
return HelperResult("TOUCHED", True, {"path": str(target)})
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _artifact_list(args: argparse.Namespace) -> HelperResult:
|
|
151
|
+
target = _path(args.path)
|
|
152
|
+
if not target.is_dir():
|
|
153
|
+
return HelperResult("MISSING", False, {"path": str(target), "entries": []}, 1)
|
|
154
|
+
entries = []
|
|
155
|
+
for child in sorted(target.iterdir(), key=lambda item: item.name):
|
|
156
|
+
entries.append({"name": child.name, "type": "dir" if child.is_dir() else "file" if child.is_file() else "other"})
|
|
157
|
+
if args.json:
|
|
158
|
+
return HelperResult("LIST", True, {"path": str(target), "entries": entries})
|
|
159
|
+
lines = [f"{entry['type']}:{entry['name']}" for entry in entries]
|
|
160
|
+
return HelperResult("LIST", True, {"path": str(target), "entry": lines})
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _artifact_tree(args: argparse.Namespace) -> HelperResult:
|
|
164
|
+
root = _path(args.path)
|
|
165
|
+
if not root.exists():
|
|
166
|
+
return HelperResult("MISSING", False, {"path": str(root), "entries": []}, 1)
|
|
167
|
+
max_depth = max(0, int(args.max_depth))
|
|
168
|
+
entries: list[str] = []
|
|
169
|
+
if root.is_file():
|
|
170
|
+
entries.append(root.name)
|
|
171
|
+
else:
|
|
172
|
+
for current_root, dir_names, file_names in os.walk(root):
|
|
173
|
+
current = Path(current_root)
|
|
174
|
+
depth = len(current.relative_to(root).parts) if current != root else 0
|
|
175
|
+
if depth >= max_depth:
|
|
176
|
+
dir_names[:] = []
|
|
177
|
+
dir_names[:] = sorted(name for name in dir_names if name not in DEFAULT_EXCLUDES)
|
|
178
|
+
for directory in dir_names:
|
|
179
|
+
entries.append(f"{_relative(current / directory, root)}/")
|
|
180
|
+
for file_name in sorted(file_names):
|
|
181
|
+
entries.append(_relative(current / file_name, root))
|
|
182
|
+
return HelperResult("TREE", True, {"path": str(root), "entries": entries} if args.json else {"path": str(root), "entry": entries})
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _artifact_source_files(args: argparse.Namespace) -> HelperResult:
|
|
186
|
+
root = _path(args.path)
|
|
187
|
+
extensions = tuple(args.ext or SOURCE_EXTENSIONS)
|
|
188
|
+
files = [
|
|
189
|
+
_relative(file_path, root if root.is_dir() else root.parent)
|
|
190
|
+
for file_path in _iter_files(root)
|
|
191
|
+
if file_path.suffix in extensions
|
|
192
|
+
]
|
|
193
|
+
return HelperResult("SOURCE_FILES", True, {"path": str(root), "files": files} if args.json else {"path": str(root), "file": files})
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _text_search(args: argparse.Namespace) -> HelperResult:
|
|
197
|
+
target = _path(args.path)
|
|
198
|
+
pattern = args.pattern
|
|
199
|
+
count = 0
|
|
200
|
+
matches: list[str] = []
|
|
201
|
+
for file_path in _iter_files(target):
|
|
202
|
+
try:
|
|
203
|
+
text = file_path.read_text(encoding="utf-8", errors="ignore")
|
|
204
|
+
except OSError:
|
|
205
|
+
continue
|
|
206
|
+
file_count = text.count(pattern)
|
|
207
|
+
if file_count:
|
|
208
|
+
count += file_count
|
|
209
|
+
matches.append(_relative(file_path, target if target.is_dir() else target.parent))
|
|
210
|
+
found = count > 0
|
|
211
|
+
return HelperResult("FOUND" if found else "NOT_FOUND", found, {"count": count, "match": matches})
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _text_count(args: argparse.Namespace) -> HelperResult:
|
|
215
|
+
target = _path(args.path)
|
|
216
|
+
count = 0
|
|
217
|
+
for file_path in _iter_files(target):
|
|
218
|
+
try:
|
|
219
|
+
count += file_path.read_text(encoding="utf-8", errors="ignore").count(args.pattern)
|
|
220
|
+
except OSError:
|
|
221
|
+
continue
|
|
222
|
+
return HelperResult("COUNT", True, {"count": count})
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _text_contains(args: argparse.Namespace) -> HelperResult:
|
|
226
|
+
target = _path(args.path)
|
|
227
|
+
if not target.is_file():
|
|
228
|
+
return HelperResult("GATE:MISSING", False, {"path": str(target), "reason": "file_missing"}, 1)
|
|
229
|
+
try:
|
|
230
|
+
found = args.pattern in target.read_text(encoding="utf-8", errors="ignore")
|
|
231
|
+
except OSError as exc:
|
|
232
|
+
return HelperResult("GATE:MISSING", False, {"path": str(target), "reason": type(exc).__name__}, 1)
|
|
233
|
+
return HelperResult(args.found_marker if found else args.missing_marker, found, {"path": str(target), "pattern": args.pattern})
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _platform_detect(args: argparse.Namespace) -> HelperResult:
|
|
237
|
+
paths = resolve_runtime_paths(project_root=args.project_root, pipeline_root=args.pipeline_root)
|
|
238
|
+
resolution = resolve_platform(paths.project_root)
|
|
239
|
+
marker = resolution.platform.upper() if resolution.platform else "NOT_INSTALLED"
|
|
240
|
+
return HelperResult(marker, bool(resolution.platform), {"platform": resolution.platform, "source": resolution.source, "notes": list(resolution.notes)})
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _platform_skill(args: argparse.Namespace) -> HelperResult:
|
|
244
|
+
paths = resolve_runtime_paths(project_root=args.project_root, pipeline_root=args.pipeline_root)
|
|
245
|
+
resolution = resolve_skill(paths.project_root, args.skill, platform=args.platform, prefix=args.prefix)
|
|
246
|
+
fields = {
|
|
247
|
+
"platform": resolution.platform,
|
|
248
|
+
"skill": resolution.skill,
|
|
249
|
+
"path": str(resolution.path) if resolution.path else None,
|
|
250
|
+
"candidates": [asdict(candidate) | {"path": str(candidate.path)} for candidate in resolution.candidates],
|
|
251
|
+
}
|
|
252
|
+
return HelperResult(resolution.marker, resolution.marker == "SKILL_EXISTS", fields)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _executable_check(args: argparse.Namespace) -> HelperResult:
|
|
256
|
+
executable = shutil.which(args.name)
|
|
257
|
+
return HelperResult("INSTALLED" if executable else "NOT_INSTALLED", bool(executable), {"name": args.name, "path": executable})
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _executable_version(args: argparse.Namespace) -> HelperResult:
|
|
261
|
+
executable = shutil.which(args.name)
|
|
262
|
+
if not executable:
|
|
263
|
+
return HelperResult("NOT_INSTALLED", False, {"name": args.name}, 1)
|
|
264
|
+
command = [executable, *(args.arg or ["--version"])]
|
|
265
|
+
try:
|
|
266
|
+
completed = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=args.timeout, check=False)
|
|
267
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
268
|
+
return HelperResult("VERSION_ERROR", False, {"name": args.name, "error": type(exc).__name__}, 1)
|
|
269
|
+
output = (completed.stdout or completed.stderr or "").strip().splitlines()
|
|
270
|
+
return HelperResult("VERSION", completed.returncode == 0, {"name": args.name, "path": executable, "version": output[0] if output else "", "return_code": completed.returncode}, completed.returncode)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _port_check(args: argparse.Namespace) -> HelperResult:
|
|
274
|
+
port = int(args.port)
|
|
275
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
276
|
+
try:
|
|
277
|
+
sock.settimeout(float(args.timeout))
|
|
278
|
+
in_use = sock.connect_ex((args.host, port)) == 0
|
|
279
|
+
finally:
|
|
280
|
+
sock.close()
|
|
281
|
+
return HelperResult("PORT_IN_USE" if in_use else "PORT_FREE", not in_use, {"host": args.host, "port": port})
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _process_status_pid(args: argparse.Namespace) -> HelperResult:
|
|
285
|
+
pid = int(args.pid)
|
|
286
|
+
running = is_pid_running(pid)
|
|
287
|
+
return HelperResult("PROCESS_RUNNING" if running else "PROCESS_NOT_RUNNING", running, {"pid": pid})
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _process_cleanup_pid(args: argparse.Namespace) -> HelperResult:
|
|
291
|
+
pid = int(args.pid)
|
|
292
|
+
result = terminate_pid_tree(pid, "runtime_helper_cleanup", grace_seconds=float(args.grace_seconds))
|
|
293
|
+
marker = "PROCESS_TERMINATED" if result.terminated else "PROCESS_NOT_RUNNING" if not is_pid_running(pid) else "PROCESS_CLEANUP_FAILED"
|
|
294
|
+
return HelperResult(marker, marker != "PROCESS_CLEANUP_FAILED", {"pid": pid, "forced": result.forced, "details": list(result.details)})
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _process_cleanup_port(args: argparse.Namespace) -> HelperResult:
|
|
298
|
+
# The Python standard library cannot portably map listening ports to owning
|
|
299
|
+
# PIDs on all supported platforms without shelling out to lsof/netstat/taskkill.
|
|
300
|
+
# Keep this deterministic and safe instead of pretending cleanup happened.
|
|
301
|
+
port_result = _port_check(argparse.Namespace(port=args.port, host=args.host, timeout=args.timeout))
|
|
302
|
+
marker = "PORT_FREE" if port_result.marker == "PORT_FREE" else "UNSUPPORTED"
|
|
303
|
+
ok = marker == "PORT_FREE"
|
|
304
|
+
return HelperResult(marker, ok, {"host": args.host, "port": int(args.port), "reason": "pid_lookup_by_port_not_available_in_python_stdlib"}, 0 if ok else 2)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _process_start(args: argparse.Namespace) -> HelperResult:
|
|
308
|
+
command = list(args.command)
|
|
309
|
+
if command and command[0] == "--":
|
|
310
|
+
command = command[1:]
|
|
311
|
+
if not command:
|
|
312
|
+
return HelperResult("PROCESS_START_FAILED", False, {"reason": "missing_command"}, 2)
|
|
313
|
+
cwd = _path(args.cwd or ".")
|
|
314
|
+
stdout_target = subprocess.DEVNULL if not args.stdout else open(_path(args.stdout), "a", encoding="utf-8")
|
|
315
|
+
stderr_target = subprocess.DEVNULL if not args.stderr else open(_path(args.stderr), "a", encoding="utf-8")
|
|
316
|
+
try:
|
|
317
|
+
process = subprocess.Popen(
|
|
318
|
+
[str(part) for part in command],
|
|
319
|
+
cwd=cwd,
|
|
320
|
+
stdout=stdout_target,
|
|
321
|
+
stderr=stderr_target,
|
|
322
|
+
stdin=subprocess.DEVNULL,
|
|
323
|
+
start_new_session=(os.name != "nt"),
|
|
324
|
+
creationflags=getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0) if os.name == "nt" else 0,
|
|
325
|
+
)
|
|
326
|
+
except OSError as exc:
|
|
327
|
+
return HelperResult("PROCESS_START_FAILED", False, {"reason": type(exc).__name__, "error": str(exc)}, 1)
|
|
328
|
+
finally:
|
|
329
|
+
for target in (stdout_target, stderr_target):
|
|
330
|
+
if hasattr(target, "close"):
|
|
331
|
+
target.close()
|
|
332
|
+
if args.pid_file:
|
|
333
|
+
pid_file = _path(args.pid_file)
|
|
334
|
+
pid_file.parent.mkdir(parents=True, exist_ok=True)
|
|
335
|
+
pid_file.write_text(f"{process.pid}\n", encoding="utf-8")
|
|
336
|
+
return HelperResult("PROCESS_STARTED", True, {"pid": process.pid, "pid_file": str(_path(args.pid_file)) if args.pid_file else None})
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _web_wait_ready(args: argparse.Namespace) -> HelperResult:
|
|
340
|
+
deadline = time.monotonic() + float(args.timeout)
|
|
341
|
+
statuses: list[str] = []
|
|
342
|
+
while time.monotonic() <= deadline:
|
|
343
|
+
try:
|
|
344
|
+
request = urllib.request.Request(args.url, method="GET")
|
|
345
|
+
with urllib.request.urlopen(request, timeout=float(args.request_timeout)) as response:
|
|
346
|
+
status = int(response.status)
|
|
347
|
+
statuses.append(str(status))
|
|
348
|
+
if status in args.status:
|
|
349
|
+
return HelperResult("READY", True, {"url": args.url, "status": status})
|
|
350
|
+
except urllib.error.HTTPError as exc:
|
|
351
|
+
statuses.append(str(exc.code))
|
|
352
|
+
if int(exc.code) in args.status:
|
|
353
|
+
return HelperResult("READY", True, {"url": args.url, "status": int(exc.code)})
|
|
354
|
+
except (OSError, urllib.error.URLError, TimeoutError) as exc:
|
|
355
|
+
statuses.append(type(exc).__name__)
|
|
356
|
+
time.sleep(float(args.interval))
|
|
357
|
+
return HelperResult("NOT_READY", False, {"url": args.url, "observed": statuses[-5:]}, 1)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
361
|
+
parser = argparse.ArgumentParser(prog="prizmkit-runtime-helper", description="Cross-platform PrizmKit prompt runtime helper CLI.")
|
|
362
|
+
subparsers = parser.add_subparsers(dest="group", required=True)
|
|
363
|
+
|
|
364
|
+
artifact = subparsers.add_parser("artifact", help="Artifact and source discovery helpers")
|
|
365
|
+
artifact_sub = artifact.add_subparsers(dest="action", required=True)
|
|
366
|
+
exists = artifact_sub.add_parser("exists", help="Check whether a path exists")
|
|
367
|
+
exists.add_argument("path")
|
|
368
|
+
exists.add_argument("--exists-marker", default="EXISTS")
|
|
369
|
+
exists.add_argument("--missing-marker", default="MISSING")
|
|
370
|
+
_add_json(exists)
|
|
371
|
+
exists.set_defaults(func=_artifact_exists)
|
|
372
|
+
ensure_dir = artifact_sub.add_parser("ensure-dir", help="Create a directory if missing")
|
|
373
|
+
ensure_dir.add_argument("path")
|
|
374
|
+
_add_json(ensure_dir)
|
|
375
|
+
ensure_dir.set_defaults(func=_artifact_ensure_dir)
|
|
376
|
+
touch = artifact_sub.add_parser("touch", help="Create a file and parent directories if missing")
|
|
377
|
+
touch.add_argument("path")
|
|
378
|
+
_add_json(touch)
|
|
379
|
+
touch.set_defaults(func=_artifact_touch)
|
|
380
|
+
list_parser = artifact_sub.add_parser("list", help="List a directory deterministically")
|
|
381
|
+
list_parser.add_argument("path")
|
|
382
|
+
_add_json(list_parser)
|
|
383
|
+
list_parser.set_defaults(func=_artifact_list)
|
|
384
|
+
tree = artifact_sub.add_parser("tree", help="Render a deterministic tree")
|
|
385
|
+
tree.add_argument("path")
|
|
386
|
+
tree.add_argument("--max-depth", type=int, default=3)
|
|
387
|
+
_add_json(tree)
|
|
388
|
+
tree.set_defaults(func=_artifact_tree)
|
|
389
|
+
source = artifact_sub.add_parser("source-files", help="Discover source files")
|
|
390
|
+
source.add_argument("path")
|
|
391
|
+
source.add_argument("--ext", action="append", help="Extension to include, e.g. .py")
|
|
392
|
+
_add_json(source)
|
|
393
|
+
source.set_defaults(func=_artifact_source_files)
|
|
394
|
+
|
|
395
|
+
text = subparsers.add_parser("text", help="Text search helpers")
|
|
396
|
+
text_sub = text.add_subparsers(dest="action", required=True)
|
|
397
|
+
search = text_sub.add_parser("search", help="Search text under a path")
|
|
398
|
+
search.add_argument("path")
|
|
399
|
+
search.add_argument("--pattern", required=True)
|
|
400
|
+
_add_json(search)
|
|
401
|
+
search.set_defaults(func=_text_search)
|
|
402
|
+
count = text_sub.add_parser("count", help="Count text occurrences under a path")
|
|
403
|
+
count.add_argument("path")
|
|
404
|
+
count.add_argument("--pattern", required=True)
|
|
405
|
+
_add_json(count)
|
|
406
|
+
count.set_defaults(func=_text_count)
|
|
407
|
+
contains = text_sub.add_parser("contains", help="Check one file contains text and emit gate markers")
|
|
408
|
+
contains.add_argument("path")
|
|
409
|
+
contains.add_argument("--pattern", required=True)
|
|
410
|
+
contains.add_argument("--found-marker", default="GATE:PASS")
|
|
411
|
+
contains.add_argument("--missing-marker", default="GATE:MISSING")
|
|
412
|
+
_add_json(contains)
|
|
413
|
+
contains.set_defaults(func=_text_contains)
|
|
414
|
+
|
|
415
|
+
platform = subparsers.add_parser("platform", help="AI platform and skill detection")
|
|
416
|
+
platform_sub = platform.add_subparsers(dest="action", required=True)
|
|
417
|
+
detect = platform_sub.add_parser("detect", help="Detect current AI platform")
|
|
418
|
+
detect.add_argument("--project-root")
|
|
419
|
+
detect.add_argument("--pipeline-root")
|
|
420
|
+
_add_json(detect)
|
|
421
|
+
detect.set_defaults(func=_platform_detect)
|
|
422
|
+
skill = platform_sub.add_parser("skill", help="Detect a platform skill")
|
|
423
|
+
skill.add_argument("skill")
|
|
424
|
+
skill.add_argument("--platform", default="auto")
|
|
425
|
+
skill.add_argument("--project-root")
|
|
426
|
+
skill.add_argument("--pipeline-root")
|
|
427
|
+
skill.add_argument("--prefix", action="store_true")
|
|
428
|
+
_add_json(skill)
|
|
429
|
+
skill.set_defaults(func=_platform_skill)
|
|
430
|
+
|
|
431
|
+
executable = subparsers.add_parser("executable", help="Executable checks")
|
|
432
|
+
executable_sub = executable.add_subparsers(dest="action", required=True)
|
|
433
|
+
check = executable_sub.add_parser("check", help="Check executable availability")
|
|
434
|
+
check.add_argument("name")
|
|
435
|
+
_add_json(check)
|
|
436
|
+
check.set_defaults(func=_executable_check)
|
|
437
|
+
version = executable_sub.add_parser("version", help="Run executable version command")
|
|
438
|
+
version.add_argument("name")
|
|
439
|
+
version.add_argument("--arg", action="append", default=None)
|
|
440
|
+
version.add_argument("--timeout", type=float, default=10.0)
|
|
441
|
+
_add_json(version)
|
|
442
|
+
version.set_defaults(func=_executable_version)
|
|
443
|
+
|
|
444
|
+
port = subparsers.add_parser("port", help="Port readiness helpers")
|
|
445
|
+
port_sub = port.add_subparsers(dest="action", required=True)
|
|
446
|
+
port_check = port_sub.add_parser("check", help="Check whether a TCP port is in use")
|
|
447
|
+
port_check.add_argument("port", type=int)
|
|
448
|
+
port_check.add_argument("--host", default="127.0.0.1")
|
|
449
|
+
port_check.add_argument("--timeout", type=float, default=0.5)
|
|
450
|
+
_add_json(port_check)
|
|
451
|
+
port_check.set_defaults(func=_port_check)
|
|
452
|
+
|
|
453
|
+
process = subparsers.add_parser("process", help="Process lifecycle helpers")
|
|
454
|
+
process_sub = process.add_subparsers(dest="action", required=True)
|
|
455
|
+
start = process_sub.add_parser("start", help="Start a process and optionally write a PID file")
|
|
456
|
+
start.add_argument("--pid-file")
|
|
457
|
+
start.add_argument("--cwd")
|
|
458
|
+
start.add_argument("--stdout")
|
|
459
|
+
start.add_argument("--stderr")
|
|
460
|
+
start.add_argument("command", nargs=argparse.REMAINDER)
|
|
461
|
+
_add_json(start)
|
|
462
|
+
start.set_defaults(func=_process_start)
|
|
463
|
+
status_pid = process_sub.add_parser("status-pid", help="Check whether a PID is running")
|
|
464
|
+
status_pid.add_argument("pid", type=int)
|
|
465
|
+
_add_json(status_pid)
|
|
466
|
+
status_pid.set_defaults(func=_process_status_pid)
|
|
467
|
+
cleanup_pid = process_sub.add_parser("cleanup-pid", help="Best-effort cleanup by PID")
|
|
468
|
+
cleanup_pid.add_argument("pid", type=int)
|
|
469
|
+
cleanup_pid.add_argument("--grace-seconds", type=float, default=2.0)
|
|
470
|
+
_add_json(cleanup_pid)
|
|
471
|
+
cleanup_pid.set_defaults(func=_process_cleanup_pid)
|
|
472
|
+
cleanup_port = process_sub.add_parser("cleanup-port", help="Best-effort cleanup by port when supported")
|
|
473
|
+
cleanup_port.add_argument("port", type=int)
|
|
474
|
+
cleanup_port.add_argument("--host", default="127.0.0.1")
|
|
475
|
+
cleanup_port.add_argument("--timeout", type=float, default=0.5)
|
|
476
|
+
_add_json(cleanup_port)
|
|
477
|
+
cleanup_port.set_defaults(func=_process_cleanup_port)
|
|
478
|
+
|
|
479
|
+
web = subparsers.add_parser("web", help="Browser/server readiness helpers")
|
|
480
|
+
web_sub = web.add_subparsers(dest="action", required=True)
|
|
481
|
+
wait_ready = web_sub.add_parser("wait-ready", help="Wait for an HTTP endpoint to be ready")
|
|
482
|
+
wait_ready.add_argument("--url", required=True)
|
|
483
|
+
wait_ready.add_argument("--timeout", type=float, default=30.0)
|
|
484
|
+
wait_ready.add_argument("--interval", type=float, default=2.0)
|
|
485
|
+
wait_ready.add_argument("--request-timeout", type=float, default=2.0)
|
|
486
|
+
wait_ready.add_argument("--status", action="append", type=int, default=[200, 302])
|
|
487
|
+
_add_json(wait_ready)
|
|
488
|
+
wait_ready.set_defaults(func=_web_wait_ready)
|
|
489
|
+
|
|
490
|
+
return parser
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def run_helper_command(argv: list[str] | tuple[str, ...]) -> HelperResult:
|
|
494
|
+
"""Run a helper command argv and return the structured result."""
|
|
495
|
+
parser = build_parser()
|
|
496
|
+
args = parser.parse_args(list(argv))
|
|
497
|
+
return args.func(args)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def main(argv: list[str] | None = None) -> int:
|
|
501
|
+
parser = build_parser()
|
|
502
|
+
args = parser.parse_args(argv)
|
|
503
|
+
result = args.func(args)
|
|
504
|
+
return _emit(result, json_output=bool(getattr(args, "json", False)))
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
if __name__ == "__main__":
|
|
508
|
+
raise SystemExit(main())
|
|
@@ -27,7 +27,7 @@ import os
|
|
|
27
27
|
import re
|
|
28
28
|
import sys
|
|
29
29
|
|
|
30
|
-
from utils import enrich_global_context, load_json_file, resolve_prompt_platform, setup_logging
|
|
30
|
+
from utils import enrich_global_context, helper_replacements, load_json_file, resolve_prompt_platform, setup_logging
|
|
31
31
|
from continuation import add_continuation_args, append_continuation_handoff, is_continuation
|
|
32
32
|
|
|
33
33
|
|
|
@@ -1941,6 +1941,8 @@ def build_replacements(args, feature, features, global_context, script_dir):
|
|
|
1941
1941
|
),
|
|
1942
1942
|
}
|
|
1943
1943
|
|
|
1944
|
+
replacements.update(helper_replacements(script_dir))
|
|
1945
|
+
|
|
1944
1946
|
return replacements, effective_resume, browser_enabled, browser_tool
|
|
1945
1947
|
|
|
1946
1948
|
|
|
@@ -19,7 +19,7 @@ import os
|
|
|
19
19
|
import re
|
|
20
20
|
import sys
|
|
21
21
|
|
|
22
|
-
from utils import enrich_global_context, load_json_file, read_checkpoint_system_section, resolve_prompt_platform, setup_logging
|
|
22
|
+
from utils import enrich_global_context, helper_replacements, load_json_file, read_checkpoint_system_section, resolve_prompt_platform, setup_logging
|
|
23
23
|
from continuation import add_continuation_args, append_continuation_handoff, is_continuation
|
|
24
24
|
|
|
25
25
|
|
|
@@ -362,6 +362,8 @@ def build_replacements(args, bug, global_context, script_dir):
|
|
|
362
362
|
"{{BROWSER_VERIFY_STEPS}}": browser_verify_steps,
|
|
363
363
|
}
|
|
364
364
|
|
|
365
|
+
replacements.update(helper_replacements(script_dir))
|
|
366
|
+
|
|
365
367
|
return replacements
|
|
366
368
|
|
|
367
369
|
|
|
@@ -19,7 +19,7 @@ import os
|
|
|
19
19
|
import re
|
|
20
20
|
import sys
|
|
21
21
|
|
|
22
|
-
from utils import enrich_global_context, load_json_file, read_checkpoint_system_section, resolve_prompt_platform, setup_logging
|
|
22
|
+
from utils import enrich_global_context, helper_replacements, load_json_file, read_checkpoint_system_section, resolve_prompt_platform, setup_logging
|
|
23
23
|
from continuation import add_continuation_args, append_continuation_handoff, is_continuation
|
|
24
24
|
|
|
25
25
|
|
|
@@ -544,6 +544,8 @@ def build_replacements(args, refactor, refactors, global_context, script_dir):
|
|
|
544
544
|
"{{BROWSER_VERIFY_STEPS}}": browser_verify_steps,
|
|
545
545
|
}
|
|
546
546
|
|
|
547
|
+
replacements.update(helper_replacements(script_dir))
|
|
548
|
+
|
|
547
549
|
return replacements
|
|
548
550
|
|
|
549
551
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Thin entrypoint for the PrizmKit prompt runtime helper CLI."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _bootstrap_import_path() -> None:
|
|
11
|
+
"""Ensure direct script execution can import the runtime package."""
|
|
12
|
+
pipeline_root = Path(__file__).resolve().parents[1]
|
|
13
|
+
pipeline_root_text = str(pipeline_root)
|
|
14
|
+
if pipeline_root_text not in sys.path:
|
|
15
|
+
sys.path.insert(0, pipeline_root_text)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main(argv: list[str] | None = None) -> int:
|
|
19
|
+
"""Run the runtime helper CLI and return its process exit code."""
|
|
20
|
+
_bootstrap_import_path()
|
|
21
|
+
from prizmkit_runtime.runtime_helper import main as helper_main
|
|
22
|
+
|
|
23
|
+
return helper_main(argv)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
if __name__ == "__main__":
|
|
27
|
+
raise SystemExit(main())
|