prizmkit 1.1.107 → 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_recovery.py +3 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +20 -1
- package/bundled/dev-pipeline/prizmkit_runtime/sessions.py +151 -30
- package/bundled/dev-pipeline/scripts/continuation.py +0 -1
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +6 -46
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +7 -34
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +40 -33
- 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/bootstrap-prompt.md +1 -1
- package/bundled/dev-pipeline/templates/bootstrap-tier1.md +8 -7
- package/bundled/dev-pipeline/templates/bootstrap-tier2.md +25 -36
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +16 -31
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +4 -16
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +3 -15
- package/bundled/dev-pipeline/templates/sections/checkpoint-system.md +14 -48
- package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +7 -13
- 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 +0 -7
- package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +0 -7
- 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 +0 -7
- package/bundled/dev-pipeline/templates/sections/phase-review-full.md +0 -7
- 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 +350 -23
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +36 -17
- package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +117 -14
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +79 -2
- package/bundled/dev-pipeline/tests/test_unified_cli.py +658 -13
- 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 +1 -1
- 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/sections/log-size-awareness.md +0 -20
- package/bundled/dev-pipeline/tests/test_prizmkit_test_gate.py +0 -107
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)
|
|
@@ -123,7 +123,7 @@ def run_recovery(paths: RuntimePaths, legacy_args: tuple[str, ...] = ()) -> tupl
|
|
|
123
123
|
return 1, "", "No supported AI CLI command was found for recovery\n"
|
|
124
124
|
env = RunnerEnvironment.from_env()
|
|
125
125
|
stream_json = False
|
|
126
|
-
if env.live_output:
|
|
126
|
+
if env.live_output and getattr(config.ai_client, "launch_profile", None) is None:
|
|
127
127
|
stream_json = detect_stream_json_support(config.ai_client.command, config.ai_client.platform).enabled
|
|
128
128
|
launcher = AISessionLauncher(
|
|
129
129
|
AISessionConfig(
|
|
@@ -139,7 +139,9 @@ def run_recovery(paths: RuntimePaths, legacy_args: tuple[str, ...] = ()) -> tupl
|
|
|
139
139
|
verbose=env.verbose,
|
|
140
140
|
live_output=env.live_output,
|
|
141
141
|
use_stream_json=stream_json,
|
|
142
|
+
launch_profile=getattr(config.ai_client, "launch_profile", None),
|
|
142
143
|
stale_kill_threshold_seconds=env.stale_kill_threshold_seconds,
|
|
144
|
+
heartbeat_stale_threshold_seconds=env.heartbeat_stale_threshold_seconds,
|
|
143
145
|
live_progress_interval_seconds=env.heartbeat_interval_seconds,
|
|
144
146
|
live_banner=False,
|
|
145
147
|
suppress_stream_output=True,
|
|
@@ -29,6 +29,7 @@ from .gitops import (
|
|
|
29
29
|
from .runner_bookkeeping import (
|
|
30
30
|
commit_final_bookkeeping,
|
|
31
31
|
commit_pre_branch_bookkeeping,
|
|
32
|
+
commit_preflight_ready_changes,
|
|
32
33
|
commit_task_branch_changes,
|
|
33
34
|
commit_wip_changes,
|
|
34
35
|
)
|
|
@@ -389,6 +390,20 @@ def _process_item(
|
|
|
389
390
|
execution_root = paths.project_root
|
|
390
391
|
|
|
391
392
|
try:
|
|
393
|
+
preflight = commit_preflight_ready_changes(paths.project_root, item_id)
|
|
394
|
+
if preflight.committed:
|
|
395
|
+
_emit_info(f"Preflight: preserved existing user-visible changes before {family.kind} task {item_id}")
|
|
396
|
+
elif preflight.attempted:
|
|
397
|
+
detail = ""
|
|
398
|
+
if preflight.result is not None:
|
|
399
|
+
detail = (preflight.result.stderr or preflight.result.stdout).strip()
|
|
400
|
+
_emit_warning(f"Preflight: failed to preserve existing changes before {family.kind} task {item_id}")
|
|
401
|
+
if detail:
|
|
402
|
+
_emit_warning(detail.splitlines()[0][:500])
|
|
403
|
+
raise RuntimeError(f"Preflight dirty workspace preservation failed before {family.kind} task {item_id}")
|
|
404
|
+
else:
|
|
405
|
+
_emit_info(f"Preflight: no user-visible changes to preserve before {family.kind} task {item_id}")
|
|
406
|
+
|
|
392
407
|
if use_worktree:
|
|
393
408
|
worktree_runtime = worktree_runtime_context(
|
|
394
409
|
branch_context,
|
|
@@ -499,7 +514,9 @@ def _process_item(
|
|
|
499
514
|
if invocation.dry_run:
|
|
500
515
|
return "dry_run"
|
|
501
516
|
_emit_prompt_summary(prompt, session_id)
|
|
502
|
-
stream_json =
|
|
517
|
+
stream_json = False
|
|
518
|
+
if getattr(config.ai_client, "launch_profile", None) is None:
|
|
519
|
+
stream_json = detect_stream_json_support(config.ai_client.command, config.ai_client.platform).enabled
|
|
503
520
|
base_head = run_git_command(execution_root, ("rev-parse", "HEAD")).stdout.strip()
|
|
504
521
|
session_result = AISessionLauncher(
|
|
505
522
|
AISessionConfig(
|
|
@@ -515,7 +532,9 @@ def _process_item(
|
|
|
515
532
|
verbose=env.verbose,
|
|
516
533
|
live_output=env.live_output,
|
|
517
534
|
use_stream_json=stream_json,
|
|
535
|
+
launch_profile=getattr(config.ai_client, "launch_profile", None),
|
|
518
536
|
stale_kill_threshold_seconds=env.stale_kill_threshold_seconds,
|
|
537
|
+
heartbeat_stale_threshold_seconds=env.heartbeat_stale_threshold_seconds,
|
|
519
538
|
live_progress_interval_seconds=env.heartbeat_interval_seconds,
|
|
520
539
|
live_banner=False,
|
|
521
540
|
suppress_stream_output=True,
|