prizmkit 1.1.106 → 1.1.108
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/bin/create-prizmkit.js +4 -0
- package/bundled/VERSION.json +3 -3
- package/bundled/dev-pipeline/prizmkit_runtime/config.py +81 -14
- package/bundled/dev-pipeline/prizmkit_runtime/heartbeat.py +19 -8
- package/bundled/dev-pipeline/prizmkit_runtime/interoperability.py +0 -1
- package/bundled/dev-pipeline/prizmkit_runtime/launch_profiles.py +18 -0
- package/bundled/dev-pipeline/prizmkit_runtime/runner_bookkeeping.py +22 -2
- package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +56 -5
- package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +0 -8
- package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +11 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +298 -63
- package/bundled/dev-pipeline/prizmkit_runtime/sessions.py +243 -37
- package/bundled/dev-pipeline/scripts/continuation.py +0 -5
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +10 -82
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +7 -64
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +40 -63
- package/bundled/dev-pipeline/scripts/utils.py +123 -0
- package/bundled/dev-pipeline/templates/agent-prompts/critic-plan-challenge.md +4 -3
- package/bundled/dev-pipeline/templates/agent-prompts/reviewer-review.md +5 -6
- package/bundled/dev-pipeline/templates/bootstrap-prompt.md +1 -1
- package/bundled/dev-pipeline/templates/bootstrap-tier1.md +36 -35
- package/bundled/dev-pipeline/templates/bootstrap-tier2.md +53 -80
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +43 -78
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +5 -17
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +6 -18
- package/bundled/dev-pipeline/templates/sections/checkpoint-system.md +14 -48
- package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +7 -14
- package/bundled/dev-pipeline/templates/sections/feature-context.md +0 -4
- package/bundled/dev-pipeline/templates/sections/phase-browser-verification-auto.md +0 -7
- package/bundled/dev-pipeline/templates/sections/phase-browser-verification-opencli.md +0 -7
- package/bundled/dev-pipeline/templates/sections/phase-browser-verification.md +0 -7
- package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +0 -14
- package/bundled/dev-pipeline/templates/sections/phase-commit.md +0 -14
- package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-agent-suffix.md +0 -7
- package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-base.md +0 -2
- package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-lite-suffix.md +0 -7
- package/bundled/dev-pipeline/templates/sections/phase-critic-plan-full.md +6 -8
- package/bundled/dev-pipeline/templates/sections/phase-critic-plan.md +3 -7
- package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +4 -16
- package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +4 -17
- package/bundled/dev-pipeline/templates/sections/phase-implement-lite.md +0 -7
- package/bundled/dev-pipeline/templates/sections/phase-plan-agent.md +2 -9
- package/bundled/dev-pipeline/templates/sections/phase-plan-lite.md +1 -8
- package/bundled/dev-pipeline/templates/sections/phase-prizmkit-test.md +15 -16
- package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +3 -10
- package/bundled/dev-pipeline/templates/sections/phase-review-full.md +3 -10
- package/bundled/dev-pipeline/templates/sections/phase-specify-plan-full.md +2 -9
- package/bundled/dev-pipeline/templates/sections/phase0-init.md +0 -7
- package/bundled/dev-pipeline/templates/sections/phase0-test-baseline.md +2 -15
- package/bundled/dev-pipeline/tests/conftest.py +0 -1
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +395 -21
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +50 -14
- package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +154 -2
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +431 -3
- package/bundled/dev-pipeline/tests/test_unified_cli.py +857 -5
- package/bundled/skills/_metadata.json +4 -4
- package/bundled/skills/app-planner/SKILL.md +1 -1
- package/bundled/skills/app-planner/references/architecture-decisions.md +1 -1
- package/bundled/skills/bug-fix-workflow/SKILL.md +174 -128
- package/bundled/skills/bug-planner/SKILL.md +101 -15
- package/bundled/skills/bug-planner/references/severity-rules.md +20 -8
- package/bundled/skills/feature-planner/SKILL.md +87 -8
- package/bundled/skills/feature-planner/assets/planning-guide.md +34 -10
- package/bundled/skills/feature-workflow/SKILL.md +100 -81
- package/bundled/skills/{prizm-kit → prizmkit}/SKILL.md +1 -1
- package/bundled/skills/prizmkit-code-review/SKILL.md +12 -13
- package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +10 -10
- package/bundled/skills/refactor-planner/SKILL.md +88 -7
- package/bundled/skills/refactor-planner/references/fast-path.md +2 -1
- package/bundled/skills/refactor-planner/references/planning-phases.md +29 -11
- package/bundled/skills/refactor-workflow/SKILL.md +116 -57
- package/bundled/templates/project-memory-template.md +1 -1
- package/package.json +1 -1
- package/src/ai-cli-launch.js +194 -0
- package/src/config.js +26 -14
- package/src/index.js +11 -44
- package/src/platforms.js +10 -3
- package/src/prompts.js +60 -2
- package/src/scaffold.js +26 -14
- package/bundled/dev-pipeline/scripts/prizmkit-test-gate.py +0 -446
- package/bundled/dev-pipeline/templates/agent-prompts/dev-fix.md +0 -7
- package/bundled/dev-pipeline/templates/agent-prompts/dev-resume.md +0 -5
- package/bundled/dev-pipeline/templates/sections/log-size-awareness.md +0 -20
- package/bundled/dev-pipeline/tests/test_prizmkit_test_gate.py +0 -107
- package/bundled/skills/prizmkit-code-review/references/dev-agent-prompt.md +0 -30
package/bin/create-prizmkit.js
CHANGED
|
@@ -46,6 +46,8 @@ program
|
|
|
46
46
|
.option('--no-open-cli-auto-download', 'Skip auto-download, show manual steps only')
|
|
47
47
|
.option('--rules <preset>', 'Rules preset: recommended, minimal, or none', 'recommended')
|
|
48
48
|
.option('--ai-cli <command>', 'AI CLI executable command (e.g. cbc, claude, codex)')
|
|
49
|
+
.option('--headless-prompt-arg <arg>', 'Prompt injection argv token for a custom AI CLI (e.g. -p or -i)')
|
|
50
|
+
.option('--headless-extra-args <args>', 'Additional headless argv tokens for a custom AI CLI')
|
|
49
51
|
.option('--external-skills <names>', 'Comma-separated external skill names to install (e.g. find-skills,impeccable)')
|
|
50
52
|
.option('--non-interactive', 'Skip interactive prompts (requires --platform)')
|
|
51
53
|
.option('--dry-run', 'Show what would be created without making changes')
|
|
@@ -95,6 +97,8 @@ program
|
|
|
95
97
|
.option('--skills <suite>', 'Skill suite: core, minimal, or recommended:<type>')
|
|
96
98
|
.option('--rules <preset>', 'Rules preset: recommended, minimal, or none')
|
|
97
99
|
.option('--ai-cli <command>', 'AI CLI executable command (e.g. cbc, claude, codex)')
|
|
100
|
+
.option('--headless-prompt-arg <arg>', 'Prompt injection argv token for a custom AI CLI (e.g. -p or -i)')
|
|
101
|
+
.option('--headless-extra-args <args>', 'Additional headless argv tokens for a custom AI CLI')
|
|
98
102
|
.option('--team', 'Enable multi-agent team mode')
|
|
99
103
|
.option('--no-team', 'Disable multi-agent team mode')
|
|
100
104
|
.option('--pipeline', 'Enable dev-pipeline')
|
package/bundled/VERSION.json
CHANGED
|
@@ -4,11 +4,13 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
import os
|
|
7
|
+
import shlex
|
|
7
8
|
import shutil
|
|
8
9
|
from collections.abc import Mapping
|
|
9
10
|
from dataclasses import dataclass
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
|
|
13
|
+
from .launch_profiles import HeadlessLaunchProfile
|
|
12
14
|
from .paths import RuntimePaths
|
|
13
15
|
|
|
14
16
|
|
|
@@ -33,6 +35,7 @@ class AIClientResolution:
|
|
|
33
35
|
source: str
|
|
34
36
|
precedence: tuple[str, ...]
|
|
35
37
|
notes: tuple[str, ...] = ()
|
|
38
|
+
launch_profile: HeadlessLaunchProfile | None = None
|
|
36
39
|
|
|
37
40
|
|
|
38
41
|
@dataclass(frozen=True)
|
|
@@ -65,6 +68,7 @@ class RuntimeConfig:
|
|
|
65
68
|
AI_CLIENT_PRECEDENCE = (
|
|
66
69
|
"AI_CLI",
|
|
67
70
|
".prizmkit/config.json:ai_cli",
|
|
71
|
+
".prizmkit/config.json:ai_cli_launch",
|
|
68
72
|
".prizmkit/manifest.json:platform",
|
|
69
73
|
".prizmkit/config.json:platform",
|
|
70
74
|
"CODEBUDDY_CLI",
|
|
@@ -137,13 +141,18 @@ def _int_from_env(env: Mapping[str, str], name: str, default: int) -> int:
|
|
|
137
141
|
return default
|
|
138
142
|
|
|
139
143
|
|
|
140
|
-
def
|
|
144
|
+
def _read_json_object(path: Path) -> dict[str, object]:
|
|
141
145
|
if not path.is_file():
|
|
142
|
-
return
|
|
146
|
+
return {}
|
|
143
147
|
try:
|
|
144
148
|
data = json.loads(path.read_text(encoding="utf-8"))
|
|
145
149
|
except (OSError, json.JSONDecodeError):
|
|
146
|
-
return
|
|
150
|
+
return {}
|
|
151
|
+
return data if isinstance(data, dict) else {}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _read_json_value(path: Path, key: str) -> str | None:
|
|
155
|
+
data = _read_json_object(path)
|
|
147
156
|
value = data.get(key)
|
|
148
157
|
return value if isinstance(value, str) and value.strip() else None
|
|
149
158
|
|
|
@@ -170,13 +179,16 @@ def _cli_names_for_platform(platform: str | None) -> tuple[str, ...]:
|
|
|
170
179
|
def _known_platform_from_cli(cli_command: str | None) -> str | None:
|
|
171
180
|
if not cli_command:
|
|
172
181
|
return None
|
|
173
|
-
|
|
182
|
+
try:
|
|
183
|
+
token = shlex.split(cli_command)[0]
|
|
184
|
+
except ValueError:
|
|
185
|
+
token = cli_command.split()[0].strip("'\"")
|
|
174
186
|
name = Path(token).name.lower()
|
|
175
|
-
if "claude"
|
|
187
|
+
if name == "claude":
|
|
176
188
|
return "claude"
|
|
177
|
-
if "codex"
|
|
189
|
+
if name == "codex":
|
|
178
190
|
return "codex"
|
|
179
|
-
if "cbc"
|
|
191
|
+
if name in {"cbc", "codebuddy"}:
|
|
180
192
|
return "codebuddy"
|
|
181
193
|
return None
|
|
182
194
|
|
|
@@ -204,24 +216,73 @@ def _expected_cli_text(platform: str | None) -> str:
|
|
|
204
216
|
return ", ".join(_cli_names_for_platform(platform))
|
|
205
217
|
|
|
206
218
|
|
|
219
|
+
def _normalize_extra_args(value: object) -> tuple[str, ...]:
|
|
220
|
+
if value is None:
|
|
221
|
+
return ()
|
|
222
|
+
if isinstance(value, list):
|
|
223
|
+
if not all(isinstance(item, str) for item in value):
|
|
224
|
+
raise RuntimeConfigError("Custom AI CLI extra arguments must be strings.")
|
|
225
|
+
return tuple(value)
|
|
226
|
+
if isinstance(value, str):
|
|
227
|
+
if not value.strip():
|
|
228
|
+
return ()
|
|
229
|
+
try:
|
|
230
|
+
return tuple(shlex.split(value))
|
|
231
|
+
except ValueError as exc:
|
|
232
|
+
raise RuntimeConfigError(f"Invalid custom AI CLI extra arguments: {exc}") from exc
|
|
233
|
+
raise RuntimeConfigError("Custom AI CLI extra arguments must be an array of argv tokens.")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _one_token(value: object, label: str) -> str:
|
|
237
|
+
text = value.strip() if isinstance(value, str) else ""
|
|
238
|
+
if not text:
|
|
239
|
+
raise RuntimeConfigError("Custom AI CLI requires both executable and prompt injection argument.")
|
|
240
|
+
try:
|
|
241
|
+
tokens = shlex.split(text)
|
|
242
|
+
except ValueError as exc:
|
|
243
|
+
raise RuntimeConfigError(f"Invalid custom AI CLI {label}: {exc}") from exc
|
|
244
|
+
if len(tokens) != 1 or not tokens[0]:
|
|
245
|
+
raise RuntimeConfigError(f"Custom AI CLI {label} must be exactly one argv token.")
|
|
246
|
+
return tokens[0]
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _custom_launch_profile(config_data: Mapping[str, object], config_ai_cli: str | None) -> HeadlessLaunchProfile | None:
|
|
250
|
+
launch = config_data.get("ai_cli_launch")
|
|
251
|
+
custom_cli = bool(config_ai_cli) and _known_platform_from_cli(config_ai_cli) is None
|
|
252
|
+
if not custom_cli:
|
|
253
|
+
return None
|
|
254
|
+
if launch is None:
|
|
255
|
+
raise RuntimeConfigError("Custom AI CLI requires both executable and prompt injection argument in .prizmkit/config.json:ai_cli_launch.prompt_arg.")
|
|
256
|
+
if not isinstance(launch, Mapping):
|
|
257
|
+
raise RuntimeConfigError(".prizmkit/config.json:ai_cli_launch must be an object.")
|
|
258
|
+
if launch.get("profile") != "custom":
|
|
259
|
+
raise RuntimeConfigError("Custom AI CLI requires .prizmkit/config.json:ai_cli_launch.profile to be 'custom'.")
|
|
260
|
+
prompt_arg = _one_token(launch.get("prompt_arg"), "prompt injection argument")
|
|
261
|
+
return HeadlessLaunchProfile(
|
|
262
|
+
profile="custom",
|
|
263
|
+
prompt_arg=prompt_arg,
|
|
264
|
+
extra_args=_normalize_extra_args(launch.get("extra_args")),
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
207
268
|
def _installed_platform_cli(
|
|
208
269
|
paths: RuntimePaths,
|
|
209
270
|
env: Mapping[str, str],
|
|
210
271
|
) -> tuple[str | None, str | None]:
|
|
211
272
|
if (
|
|
212
273
|
(paths.project_root / ".codex" / "agents" / "prizm-dev-team-dev.toml").is_file()
|
|
213
|
-
or (paths.project_root / ".agents" / "skills" / "
|
|
274
|
+
or (paths.project_root / ".agents" / "skills" / "prizmkit" / "SKILL.md").is_file()
|
|
214
275
|
):
|
|
215
276
|
resolved = _resolve_cli_name("codex", env)
|
|
216
277
|
if resolved:
|
|
217
278
|
return resolved, "installed-platform:codex"
|
|
218
|
-
if (paths.project_root / ".claude" / "commands" / "
|
|
279
|
+
if (paths.project_root / ".claude" / "commands" / "prizmkit.md").is_file():
|
|
219
280
|
resolved = _resolve_cli_name("claude", env)
|
|
220
281
|
if resolved:
|
|
221
282
|
return resolved, "installed-platform:claude"
|
|
222
283
|
if (
|
|
223
284
|
(paths.project_root / ".codebuddy" / "agents" / "prizm-dev-team-dev.md").is_file()
|
|
224
|
-
or (paths.project_root / ".codebuddy" / "skills" / "
|
|
285
|
+
or (paths.project_root / ".codebuddy" / "skills" / "prizmkit" / "SKILL.md").is_file()
|
|
225
286
|
):
|
|
226
287
|
resolved = _resolve_cli_name("cbc", env)
|
|
227
288
|
if resolved:
|
|
@@ -255,8 +316,12 @@ def resolve_ai_client(
|
|
|
255
316
|
|
|
256
317
|
|
|
257
318
|
def _resolve_ai_client(paths: RuntimePaths, env: Mapping[str, str]) -> AIClientResolution:
|
|
258
|
-
|
|
259
|
-
|
|
319
|
+
config_path = paths.prizmkit_dir / "config.json"
|
|
320
|
+
config_data = _read_json_object(config_path)
|
|
321
|
+
config_ai_cli_value = config_data.get("ai_cli")
|
|
322
|
+
config_ai_cli = config_ai_cli_value if isinstance(config_ai_cli_value, str) and config_ai_cli_value.strip() else None
|
|
323
|
+
config_platform_value = config_data.get("platform")
|
|
324
|
+
config_platform = config_platform_value if isinstance(config_platform_value, str) and config_platform_value.strip() else None
|
|
260
325
|
manifest_platform = _read_json_value(paths.prizmkit_dir / "manifest.json", "platform")
|
|
261
326
|
|
|
262
327
|
explicit_command = env.get("AI_CLI")
|
|
@@ -269,11 +334,13 @@ def _resolve_ai_client(paths: RuntimePaths, env: Mapping[str, str]) -> AIClientR
|
|
|
269
334
|
)
|
|
270
335
|
|
|
271
336
|
if config_ai_cli:
|
|
337
|
+
custom_launch = _custom_launch_profile(config_data, config_ai_cli)
|
|
272
338
|
return AIClientResolution(
|
|
273
339
|
command=config_ai_cli,
|
|
274
|
-
platform=_project_platform_for_cli(config_ai_cli, env, manifest_platform, config_platform),
|
|
275
|
-
source="prizmkit-config:ai_cli",
|
|
340
|
+
platform="custom" if custom_launch is not None else _project_platform_for_cli(config_ai_cli, env, manifest_platform, config_platform),
|
|
341
|
+
source="prizmkit-config:ai_cli_launch" if custom_launch is not None else "prizmkit-config:ai_cli",
|
|
276
342
|
precedence=AI_CLIENT_PRECEDENCE,
|
|
343
|
+
launch_profile=custom_launch,
|
|
277
344
|
)
|
|
278
345
|
|
|
279
346
|
for platform, source, configured_label in (
|
|
@@ -100,11 +100,16 @@ def observe_heartbeat_state(
|
|
|
100
100
|
or (progress_signature and progress_signature != previous.progress_signature)
|
|
101
101
|
or (child_signature and child_signature != previous.child_activity_signature)
|
|
102
102
|
)
|
|
103
|
-
|
|
103
|
+
if progressed:
|
|
104
|
+
last_seen_epoch = now
|
|
105
|
+
stale_seconds = 0.0
|
|
106
|
+
else:
|
|
107
|
+
last_seen_epoch = previous.last_seen_epoch if previous and previous.last_seen_epoch is not None else now
|
|
108
|
+
stale_seconds = max(0.0, now - last_seen_epoch)
|
|
104
109
|
stale = bool(config.stale_after_seconds > 0 and stale_seconds >= config.stale_after_seconds)
|
|
105
110
|
details = "progress" if progressed else "no progress"
|
|
106
111
|
return HeartbeatState(
|
|
107
|
-
last_seen_epoch=
|
|
112
|
+
last_seen_epoch=last_seen_epoch,
|
|
108
113
|
stale=stale,
|
|
109
114
|
details=details,
|
|
110
115
|
stale_seconds=stale_seconds,
|
|
@@ -118,6 +123,11 @@ def _utc_marker_time() -> str:
|
|
|
118
123
|
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
119
124
|
|
|
120
125
|
|
|
126
|
+
def _marker_number(value: float) -> int | float:
|
|
127
|
+
normalized = max(0.0, float(value))
|
|
128
|
+
return int(normalized) if normalized.is_integer() else normalized
|
|
129
|
+
|
|
130
|
+
|
|
121
131
|
def _progress_fatal_error_code(path: Path | None) -> str:
|
|
122
132
|
if path is None or not path.is_file():
|
|
123
133
|
return ""
|
|
@@ -142,8 +152,8 @@ def write_stale_marker(
|
|
|
142
152
|
payload = {
|
|
143
153
|
"killed_at": _utc_marker_time(),
|
|
144
154
|
"reason": reason,
|
|
145
|
-
"stale_seconds":
|
|
146
|
-
"threshold":
|
|
155
|
+
"stale_seconds": _marker_number(stale_seconds),
|
|
156
|
+
"threshold": _marker_number(threshold),
|
|
147
157
|
}
|
|
148
158
|
if fatal_error_code:
|
|
149
159
|
payload["fatal_error_code"] = fatal_error_code
|
|
@@ -164,8 +174,8 @@ def write_fatal_marker(
|
|
|
164
174
|
"killed_at": _utc_marker_time(),
|
|
165
175
|
"reason": fatal_error_code,
|
|
166
176
|
"fatal_error_code": fatal_error_code,
|
|
167
|
-
"stale_seconds":
|
|
168
|
-
"threshold":
|
|
177
|
+
"stale_seconds": _marker_number(stale_seconds),
|
|
178
|
+
"threshold": _marker_number(threshold),
|
|
169
179
|
}
|
|
170
180
|
fatal_path = marker_dir / "fatal-error.json"
|
|
171
181
|
stale_path = marker_dir / "stale-kill.json"
|
|
@@ -231,13 +241,14 @@ class HeartbeatMonitor:
|
|
|
231
241
|
self._killed = True
|
|
232
242
|
self._reason = fatal_error_code
|
|
233
243
|
break
|
|
234
|
-
|
|
244
|
+
should_kill = bool(self.config.kill_after_seconds > 0 and self._state.stale_seconds >= self.config.kill_after_seconds)
|
|
245
|
+
if should_kill:
|
|
235
246
|
marker_dir = self.config.marker_dir or (self.config.session_log.parent if self.config.session_log else self.config.heartbeat_file.parent)
|
|
236
247
|
self._marker_path = write_stale_marker(
|
|
237
248
|
marker_dir,
|
|
238
249
|
reason="stale_session",
|
|
239
250
|
stale_seconds=self._state.stale_seconds,
|
|
240
|
-
threshold=self.config.
|
|
251
|
+
threshold=self.config.kill_after_seconds,
|
|
241
252
|
)
|
|
242
253
|
terminate_process_tree(self.handle, "stale_session", grace_seconds=self.config.grace_seconds)
|
|
243
254
|
self._killed = True
|
|
@@ -24,7 +24,6 @@ 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-test-gate": "scripts/prizmkit-test-gate.py",
|
|
28
27
|
"update-bug-status": "scripts/update-bug-status.py",
|
|
29
28
|
"update-checkpoint": "scripts/update-checkpoint.py",
|
|
30
29
|
"update-feature-status": "scripts/update-feature-status.py",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Provider-neutral headless launch profile models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class HeadlessLaunchProfile:
|
|
10
|
+
"""Custom AI CLI headless prompt injection contract."""
|
|
11
|
+
|
|
12
|
+
profile: str
|
|
13
|
+
prompt_arg: str
|
|
14
|
+
extra_args: tuple[str, ...] = ()
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def is_custom(self) -> bool:
|
|
18
|
+
return self.profile == "custom"
|
|
@@ -67,7 +67,21 @@ def commit_bookkeeping_changes(
|
|
|
67
67
|
if not paths:
|
|
68
68
|
return BookkeepingResult(attempted=False, committed=False)
|
|
69
69
|
run_git_command(project_root, ("add", "-A", "--", *paths))
|
|
70
|
-
result = run_git_command(project_root, ("commit", "--no-verify", "-m", message))
|
|
70
|
+
result = run_git_command(project_root, ("commit", "--no-verify", "-m", message, "--", *paths))
|
|
71
|
+
return BookkeepingResult(attempted=True, committed=result.return_code == 0, result=result)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def commit_preflight_ready_changes(project_root: Path, item_id: str) -> BookkeepingResult:
|
|
75
|
+
"""Preserve visible pre-existing changes before task branch setup."""
|
|
76
|
+
if not git_status_safe(project_root).strip():
|
|
77
|
+
return BookkeepingResult(attempted=False, committed=False)
|
|
78
|
+
add_result = run_git_command(project_root, ("add", "-A", "--", ".", *HIDDEN_TOOL_WORKTREE_EXCLUDES))
|
|
79
|
+
if add_result.return_code != 0:
|
|
80
|
+
return BookkeepingResult(attempted=True, committed=False, result=add_result)
|
|
81
|
+
result = run_git_command(
|
|
82
|
+
project_root,
|
|
83
|
+
("commit", "--no-verify", "-m", f"chore: ready for {item_id}", "--", ".", *HIDDEN_TOOL_WORKTREE_EXCLUDES),
|
|
84
|
+
)
|
|
71
85
|
return BookkeepingResult(attempted=True, committed=result.return_code == 0, result=result)
|
|
72
86
|
|
|
73
87
|
|
|
@@ -84,7 +98,10 @@ def commit_task_branch_changes(
|
|
|
84
98
|
if not git_status_safe(project_root).strip():
|
|
85
99
|
return BookkeepingResult(attempted=False, committed=False)
|
|
86
100
|
run_git_command(project_root, ("add", "-A", "--", ".", *HIDDEN_TOOL_WORKTREE_EXCLUDES))
|
|
87
|
-
result = run_git_command(
|
|
101
|
+
result = run_git_command(
|
|
102
|
+
project_root,
|
|
103
|
+
("commit", "--no-verify", "-m", message, "--", ".", *HIDDEN_TOOL_WORKTREE_EXCLUDES),
|
|
104
|
+
)
|
|
88
105
|
return BookkeepingResult(attempted=True, committed=result.return_code == 0, result=result)
|
|
89
106
|
|
|
90
107
|
|
|
@@ -107,6 +124,9 @@ def commit_wip_changes(
|
|
|
107
124
|
f"wip({item_id}): preserve {family.kind} {status} state",
|
|
108
125
|
"-m",
|
|
109
126
|
"Pipeline preserved task branch work without merging it to the base branch.",
|
|
127
|
+
"--",
|
|
128
|
+
".",
|
|
129
|
+
*HIDDEN_TOOL_WORKTREE_EXCLUDES,
|
|
110
130
|
),
|
|
111
131
|
)
|
|
112
132
|
return BookkeepingResult(attempted=True, committed=result.return_code == 0, result=result)
|
|
@@ -29,6 +29,10 @@ INFRA_MARKERS = (
|
|
|
29
29
|
"authentication",
|
|
30
30
|
"unauthorized",
|
|
31
31
|
"api error",
|
|
32
|
+
"quota",
|
|
33
|
+
"usage limit",
|
|
34
|
+
"credit balance",
|
|
35
|
+
"billing",
|
|
32
36
|
)
|
|
33
37
|
|
|
34
38
|
|
|
@@ -52,6 +56,13 @@ def classify_session(
|
|
|
52
56
|
progress_fingerprint=fingerprint,
|
|
53
57
|
)
|
|
54
58
|
|
|
59
|
+
if _has_late_runtime_error_after_completion(result, prompt, has_commit_since_base):
|
|
60
|
+
return SessionClassification(
|
|
61
|
+
"success",
|
|
62
|
+
reason="completed_checkpoint_with_late_runtime_error",
|
|
63
|
+
progress_fingerprint=fingerprint,
|
|
64
|
+
)
|
|
65
|
+
|
|
55
66
|
if _is_context_overflow(result):
|
|
56
67
|
if has_commit_since_base:
|
|
57
68
|
return SessionClassification("success", reason="commit_since_base", progress_fingerprint=fingerprint)
|
|
@@ -121,6 +132,16 @@ def _is_terminal_success(result: AISessionResult) -> bool:
|
|
|
121
132
|
)
|
|
122
133
|
|
|
123
134
|
|
|
135
|
+
def _has_late_runtime_error_after_completion(
|
|
136
|
+
result: AISessionResult,
|
|
137
|
+
prompt: PromptGenerationResult | None,
|
|
138
|
+
has_commit_since_base: bool,
|
|
139
|
+
) -> bool:
|
|
140
|
+
if not has_commit_since_base or not _has_completed_workflow_artifact(prompt):
|
|
141
|
+
return False
|
|
142
|
+
return _is_context_overflow(result) or _is_infra_error(result)
|
|
143
|
+
|
|
144
|
+
|
|
124
145
|
def _has_completed_workflow_artifact(prompt: PromptGenerationResult | None) -> bool:
|
|
125
146
|
if prompt is None:
|
|
126
147
|
return False
|
|
@@ -131,11 +152,32 @@ def _has_completed_workflow_artifact(prompt: PromptGenerationResult | None) -> b
|
|
|
131
152
|
data = {}
|
|
132
153
|
if isinstance(data, dict) and _checkpoint_cursor(data) == "complete":
|
|
133
154
|
return True
|
|
134
|
-
if prompt.artifact_path and (prompt.artifact_path
|
|
155
|
+
if prompt.artifact_path and has_completed_artifact_path(prompt.artifact_path):
|
|
135
156
|
return True
|
|
136
157
|
return False
|
|
137
158
|
|
|
138
159
|
|
|
160
|
+
def has_branch_completion_evidence(project_root: Path, base_branch: str, working_branch: str, artifact_path: Path | None) -> bool:
|
|
161
|
+
"""Return true when a recorded task branch already has completed work to merge."""
|
|
162
|
+
if not base_branch or not working_branch or not has_completed_artifact_path(artifact_path):
|
|
163
|
+
return False
|
|
164
|
+
return _has_commit_range(project_root, base_branch, working_branch)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def has_completed_artifact_path(artifact_path: Path | None) -> bool:
|
|
168
|
+
if artifact_path is None:
|
|
169
|
+
return False
|
|
170
|
+
checkpoint = artifact_path / "workflow-checkpoint.json"
|
|
171
|
+
if checkpoint.is_file():
|
|
172
|
+
try:
|
|
173
|
+
data = json.loads(checkpoint.read_text(encoding="utf-8"))
|
|
174
|
+
except (OSError, json.JSONDecodeError):
|
|
175
|
+
data = {}
|
|
176
|
+
if isinstance(data, dict) and _checkpoint_cursor(data) == "complete":
|
|
177
|
+
return True
|
|
178
|
+
return (artifact_path / "completion-summary.json").is_file()
|
|
179
|
+
|
|
180
|
+
|
|
139
181
|
def _next_no_progress_count(previous: object | None, current: object, count: int) -> int:
|
|
140
182
|
if previous and previous == current:
|
|
141
183
|
return max(0, count) + 1
|
|
@@ -161,11 +203,20 @@ def _is_infra_error(result: AISessionResult) -> bool:
|
|
|
161
203
|
return bool(result.fatal_error_code and result.fatal_error_code != CONTEXT_OVERFLOW) or any(marker in text for marker in INFRA_MARKERS)
|
|
162
204
|
|
|
163
205
|
|
|
164
|
-
def
|
|
165
|
-
if not
|
|
206
|
+
def _has_commit_range(project_root: Path, start_ref: str, end_ref: str = "HEAD") -> bool:
|
|
207
|
+
if not start_ref or not end_ref:
|
|
166
208
|
return False
|
|
167
|
-
|
|
168
|
-
|
|
209
|
+
result = run_git_command(project_root, ("rev-list", "--count", f"{start_ref}..{end_ref}"))
|
|
210
|
+
if result.return_code != 0:
|
|
211
|
+
return False
|
|
212
|
+
try:
|
|
213
|
+
return int(result.stdout.strip() or "0") > 0
|
|
214
|
+
except ValueError:
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _has_commit_since(project_root: Path, base_head: str) -> bool:
|
|
219
|
+
return _has_commit_range(project_root, base_head, "HEAD")
|
|
169
220
|
|
|
170
221
|
|
|
171
222
|
def _has_workspace_changes(project_root: Path) -> bool:
|
|
@@ -45,7 +45,6 @@ class RunnerEnvironment:
|
|
|
45
45
|
heartbeat_interval_seconds: int = 30
|
|
46
46
|
heartbeat_stale_threshold_seconds: int = 600
|
|
47
47
|
stale_kill_threshold_seconds: int = 600
|
|
48
|
-
max_log_size: int | None = None
|
|
49
48
|
max_retries: int = 3
|
|
50
49
|
strict_behavior_check: bool = True
|
|
51
50
|
|
|
@@ -67,7 +66,6 @@ class RunnerEnvironment:
|
|
|
67
66
|
_int_value(values, "HEARTBEAT_TIMEOUT_SECONDS", 600),
|
|
68
67
|
),
|
|
69
68
|
stale_kill_threshold_seconds=_int_value(values, "STALE_KILL_THRESHOLD", 600),
|
|
70
|
-
max_log_size=_optional_int(values.get("MAX_LOG_SIZE")),
|
|
71
69
|
max_retries=max(0, _int_value(values, "MAX_RETRIES", 3)),
|
|
72
70
|
strict_behavior_check=not _falsey(values.get("STRICT_BEHAVIOR_CHECK")),
|
|
73
71
|
)
|
|
@@ -349,9 +347,3 @@ def _safe_int(value: str | None, default: int) -> int:
|
|
|
349
347
|
|
|
350
348
|
def _int_value(values: Mapping[str, str], name: str, default: int) -> int:
|
|
351
349
|
return _safe_int(values.get(name), default)
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
def _optional_int(value: str | None) -> int | None:
|
|
355
|
-
if value is None or str(value).strip() == "":
|
|
356
|
-
return None
|
|
357
|
-
return _safe_int(value, 0)
|
|
@@ -12,7 +12,7 @@ from pathlib import Path
|
|
|
12
12
|
from .config import load_runtime_config
|
|
13
13
|
from .paths import RuntimePaths
|
|
14
14
|
from .runner_models import RunnerEnvironment, SessionPaths
|
|
15
|
-
from .sessions import AISessionConfig, AISessionLauncher
|
|
15
|
+
from .sessions import AISessionConfig, AISessionLauncher, detect_stream_json_support
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
@dataclass(frozen=True)
|
|
@@ -122,6 +122,9 @@ def run_recovery(paths: RuntimePaths, legacy_args: tuple[str, ...] = ()) -> tupl
|
|
|
122
122
|
if not config.ai_client.command:
|
|
123
123
|
return 1, "", "No supported AI CLI command was found for recovery\n"
|
|
124
124
|
env = RunnerEnvironment.from_env()
|
|
125
|
+
stream_json = False
|
|
126
|
+
if env.live_output and getattr(config.ai_client, "launch_profile", None) is None:
|
|
127
|
+
stream_json = detect_stream_json_support(config.ai_client.command, config.ai_client.platform).enabled
|
|
125
128
|
launcher = AISessionLauncher(
|
|
126
129
|
AISessionConfig(
|
|
127
130
|
cli_command=config.ai_client.command,
|
|
@@ -134,7 +137,14 @@ def run_recovery(paths: RuntimePaths, legacy_args: tuple[str, ...] = ()) -> tupl
|
|
|
134
137
|
heartbeat_path=session_paths.heartbeat_json,
|
|
135
138
|
effort=config.effort,
|
|
136
139
|
verbose=env.verbose,
|
|
140
|
+
live_output=env.live_output,
|
|
141
|
+
use_stream_json=stream_json,
|
|
142
|
+
launch_profile=getattr(config.ai_client, "launch_profile", None),
|
|
137
143
|
stale_kill_threshold_seconds=env.stale_kill_threshold_seconds,
|
|
144
|
+
heartbeat_stale_threshold_seconds=env.heartbeat_stale_threshold_seconds,
|
|
145
|
+
live_progress_interval_seconds=env.heartbeat_interval_seconds,
|
|
146
|
+
live_banner=False,
|
|
147
|
+
suppress_stream_output=True,
|
|
138
148
|
)
|
|
139
149
|
)
|
|
140
150
|
result = launcher.run()
|