ai-push-hooks 0.2.1 → 0.3.1
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/CHANGELOG.md +73 -1
- package/README.md +80 -525
- package/SECURITY.md +102 -14
- package/ai-push-hooks.toml +9 -2
- package/bin/ai-push-hooks.js +6 -6
- package/package.json +3 -2
- package/pyproject.toml +1 -1
- package/src/ai_push_hooks/artifacts.py +67 -13
- package/src/ai_push_hooks/config.py +575 -22
- package/src/ai_push_hooks/engine.py +116 -7
- package/src/ai_push_hooks/executors/apply.py +75 -36
- package/src/ai_push_hooks/executors/ask.py +224 -0
- package/src/ai_push_hooks/executors/exec.py +17 -801
- package/src/ai_push_hooks/executors/runner_workflow.py +478 -0
- package/src/ai_push_hooks/executors/runners/__init__.py +78 -0
- package/src/ai_push_hooks/executors/runners/claude.py +286 -0
- package/src/ai_push_hooks/executors/runners/codex.py +254 -0
- package/src/ai_push_hooks/executors/runners/command.py +178 -0
- package/src/ai_push_hooks/executors/runners/contracts.py +597 -0
- package/src/ai_push_hooks/executors/runners/opencode.py +528 -0
- package/src/ai_push_hooks/executors/runners/opencode_support.py +276 -0
- package/src/ai_push_hooks/executors/runners/process.py +464 -0
- package/src/ai_push_hooks/executors/runners/registry.py +117 -0
- package/src/ai_push_hooks/executors/step_commands.py +478 -0
- package/src/ai_push_hooks/git_utils.py +834 -0
- package/src/ai_push_hooks/hook.py +1 -1
- package/src/ai_push_hooks/modules/beads.py +1 -1
- package/src/ai_push_hooks/modules/docs.py +129 -89
- package/src/ai_push_hooks/modules/pr.py +1 -1
- package/src/ai_push_hooks/plugin_loader.py +422 -0
- package/src/ai_push_hooks/plugins.py +134 -0
- package/src/ai_push_hooks/prompts_builtin.py +9 -2
- package/src/ai_push_hooks/types.py +407 -75
- package/vendor/README.md +15 -0
- package/vendor/requirements.txt +1 -0
- package/vendor/tomli-2.4.0-py3-none-any.whl +0 -0
- package/src/ai_push_hooks/executors/llm.py +0 -624
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
"""Runner-neutral orchestration for one workflow model invocation.
|
|
2
|
+
|
|
3
|
+
The adapters own process invocation and protocol parsing. This module owns
|
|
4
|
+
profile resolution, the common request shape, call accounting, lifecycle
|
|
5
|
+
cleanup, and the bounded user-facing error boundary.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import pathlib
|
|
12
|
+
import stat
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
from ..config import resolve_runner_profile
|
|
16
|
+
from ..paths import is_path_within, path_has_symlink
|
|
17
|
+
from ..types import HookError, RunnerProfile, RuntimeContext, StepConfig
|
|
18
|
+
from .runners import (
|
|
19
|
+
Runner,
|
|
20
|
+
RunnerArtifact,
|
|
21
|
+
RunnerRequest,
|
|
22
|
+
RunnerResult,
|
|
23
|
+
SessionMetadata,
|
|
24
|
+
bounded_diagnostic,
|
|
25
|
+
finalize_runner,
|
|
26
|
+
get_runner,
|
|
27
|
+
redact_diagnostic,
|
|
28
|
+
require_final_text,
|
|
29
|
+
require_zero_exit,
|
|
30
|
+
)
|
|
31
|
+
from .runners.contracts import RunnerProtocolError, request_sensitive_diagnostics
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
RUNNER_INPUT_MAX_BYTES = 16 * 1024 * 1024
|
|
35
|
+
RUNNER_INPUT_READ_CHUNK_BYTES = 1024 * 1024
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class _RunnerInvocation:
|
|
40
|
+
request: RunnerRequest
|
|
41
|
+
runner: Runner
|
|
42
|
+
result: RunnerResult
|
|
43
|
+
call_number: int
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _selected_runner_name(context: RuntimeContext, step: StepConfig) -> str:
|
|
47
|
+
return step.runner or context.config.llm.runner
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _safe_input_artifacts(
|
|
51
|
+
context: RuntimeContext,
|
|
52
|
+
step: StepConfig,
|
|
53
|
+
input_paths: list[pathlib.Path],
|
|
54
|
+
) -> tuple[RunnerArtifact, ...]:
|
|
55
|
+
"""Build the ordered logical artifact list shared by every adapter.
|
|
56
|
+
|
|
57
|
+
``validate_hook_owned_artifacts`` is the hook-owned artifact boundary used
|
|
58
|
+
before adapter dispatch. The logical names are the
|
|
59
|
+
configured input references, rather than filesystem basenames, so command,
|
|
60
|
+
Codex, Claude, and OpenCode receive the same ordered context.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
from .runners.opencode_support import validate_hook_owned_artifacts
|
|
64
|
+
|
|
65
|
+
validated = validate_hook_owned_artifacts(context, input_paths)
|
|
66
|
+
if len(validated) != len(step.inputs):
|
|
67
|
+
raise HookError(
|
|
68
|
+
f"Runner input count does not match configured inputs for step `{step.id}`"
|
|
69
|
+
)
|
|
70
|
+
run_root = context.run_dir.resolve(strict=True)
|
|
71
|
+
artifacts: list[RunnerArtifact] = []
|
|
72
|
+
total_bytes = 0
|
|
73
|
+
for name, path in zip(step.inputs, validated, strict=True):
|
|
74
|
+
lexical_path = pathlib.Path(os.path.abspath(path))
|
|
75
|
+
# The first validation happens before this loop. Recheck the lexical
|
|
76
|
+
# parents immediately before opening each file; this is a same-user
|
|
77
|
+
# race defense, not an OS sandbox. O_NOFOLLOW and descriptor checks
|
|
78
|
+
# below protect the final open even if the leaf changes concurrently.
|
|
79
|
+
if (
|
|
80
|
+
not is_path_within(lexical_path, run_root)
|
|
81
|
+
or path_has_symlink(run_root, lexical_path)
|
|
82
|
+
):
|
|
83
|
+
raise HookError(f"Runner artifact must not traverse a symlink: {name}")
|
|
84
|
+
try:
|
|
85
|
+
resolved_path = lexical_path.resolve(strict=True)
|
|
86
|
+
except (OSError, RuntimeError) as exc:
|
|
87
|
+
raise HookError(f"Unable to safely resolve runner artifact: {name}") from exc
|
|
88
|
+
if (
|
|
89
|
+
resolved_path != lexical_path
|
|
90
|
+
or not is_path_within(resolved_path, run_root)
|
|
91
|
+
or not resolved_path.is_file()
|
|
92
|
+
):
|
|
93
|
+
raise HookError(f"Runner artifact must be a regular hook-owned file: {name}")
|
|
94
|
+
|
|
95
|
+
descriptor: int | None = None
|
|
96
|
+
try:
|
|
97
|
+
flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0)
|
|
98
|
+
descriptor = os.open(lexical_path, flags)
|
|
99
|
+
metadata = os.fstat(descriptor)
|
|
100
|
+
if not os.path.isfile(lexical_path) or not stat.S_ISREG(metadata.st_mode):
|
|
101
|
+
raise HookError(f"Runner artifact must be a regular hook-owned file: {name}")
|
|
102
|
+
remaining = RUNNER_INPUT_MAX_BYTES - total_bytes
|
|
103
|
+
if metadata.st_size > remaining:
|
|
104
|
+
raise HookError(
|
|
105
|
+
f"Runner input artifacts exceed the {RUNNER_INPUT_MAX_BYTES}-byte budget"
|
|
106
|
+
)
|
|
107
|
+
content_bytes = bytearray()
|
|
108
|
+
while True:
|
|
109
|
+
read_limit = min(
|
|
110
|
+
RUNNER_INPUT_READ_CHUNK_BYTES,
|
|
111
|
+
remaining - len(content_bytes) + 1,
|
|
112
|
+
)
|
|
113
|
+
chunk = os.read(descriptor, max(1, read_limit))
|
|
114
|
+
if not chunk:
|
|
115
|
+
break
|
|
116
|
+
content_bytes.extend(chunk)
|
|
117
|
+
if len(content_bytes) > remaining:
|
|
118
|
+
raise HookError(
|
|
119
|
+
f"Runner input artifacts exceed the {RUNNER_INPUT_MAX_BYTES}-byte budget"
|
|
120
|
+
)
|
|
121
|
+
total_bytes += len(content_bytes)
|
|
122
|
+
content = bytes(content_bytes).decode("utf-8")
|
|
123
|
+
except (OSError, UnicodeError) as exc:
|
|
124
|
+
raise HookError(f"Unable to read hook-owned runner artifact: {name}") from exc
|
|
125
|
+
finally:
|
|
126
|
+
if descriptor is not None:
|
|
127
|
+
os.close(descriptor)
|
|
128
|
+
artifacts.append(RunnerArtifact(name=name, content=content, path=path))
|
|
129
|
+
return tuple(artifacts)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _build_request(
|
|
133
|
+
context: RuntimeContext,
|
|
134
|
+
step: StepConfig,
|
|
135
|
+
prompt: str,
|
|
136
|
+
input_paths: list[pathlib.Path],
|
|
137
|
+
stage_name: str,
|
|
138
|
+
working_directory: pathlib.Path,
|
|
139
|
+
*,
|
|
140
|
+
session_id: str | None,
|
|
141
|
+
resume_session: bool,
|
|
142
|
+
) -> tuple[RunnerProfile, RunnerRequest]:
|
|
143
|
+
profile = resolve_runner_profile(context.config, step)
|
|
144
|
+
cwd = pathlib.Path(working_directory).resolve(strict=True)
|
|
145
|
+
if not cwd.is_dir():
|
|
146
|
+
raise HookError(f"Runner working directory is not a directory: {working_directory}")
|
|
147
|
+
request = RunnerRequest(
|
|
148
|
+
profile_id=profile.name,
|
|
149
|
+
runner_type=profile.type,
|
|
150
|
+
stage=stage_name,
|
|
151
|
+
purpose=f"{step.type}:{step.id}",
|
|
152
|
+
mode=step.type, # type: ignore[arg-type]
|
|
153
|
+
instruction=prompt,
|
|
154
|
+
artifacts=_safe_input_artifacts(context, step, input_paths),
|
|
155
|
+
cwd=cwd,
|
|
156
|
+
timeout_seconds=context.config.llm.timeout_seconds,
|
|
157
|
+
model=profile.model,
|
|
158
|
+
variant=profile.variant,
|
|
159
|
+
project_access=profile.project_access, # type: ignore[arg-type]
|
|
160
|
+
allow_paths=step.allow_paths,
|
|
161
|
+
command=profile.command,
|
|
162
|
+
prompt_transport=profile.prompt_transport, # type: ignore[arg-type]
|
|
163
|
+
session_id=session_id,
|
|
164
|
+
resume_session=resume_session,
|
|
165
|
+
integration_context=context,
|
|
166
|
+
)
|
|
167
|
+
return profile, request
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _call_logger(
|
|
171
|
+
context: RuntimeContext,
|
|
172
|
+
request: RunnerRequest,
|
|
173
|
+
model: str | None,
|
|
174
|
+
*,
|
|
175
|
+
attempt: int | None,
|
|
176
|
+
total_attempts: int | None,
|
|
177
|
+
) -> int:
|
|
178
|
+
return context.logger.llm_call(
|
|
179
|
+
request.stage,
|
|
180
|
+
request.purpose,
|
|
181
|
+
model or "",
|
|
182
|
+
attempt,
|
|
183
|
+
total_attempts,
|
|
184
|
+
runner_profile=request.profile_id,
|
|
185
|
+
runner_type=request.runner_type,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _session_metadata(runner: Runner, session_id: str) -> SessionMetadata:
|
|
190
|
+
supports_resume = bool(
|
|
191
|
+
getattr(getattr(runner, "capabilities", None), "supports_resume", False)
|
|
192
|
+
)
|
|
193
|
+
return SessionMetadata(
|
|
194
|
+
session_id=session_id,
|
|
195
|
+
state="persisted" if supports_resume else "ephemeral",
|
|
196
|
+
resumable=supports_resume,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _failure_result(
|
|
201
|
+
runner: Runner,
|
|
202
|
+
error: BaseException,
|
|
203
|
+
fallback_session_id: str | None = None,
|
|
204
|
+
) -> RunnerResult:
|
|
205
|
+
session_id = getattr(error, "session_id", None)
|
|
206
|
+
if not isinstance(session_id, str) or not session_id.strip():
|
|
207
|
+
session_id = fallback_session_id
|
|
208
|
+
session = _session_metadata(runner, session_id.strip()) if session_id else None
|
|
209
|
+
return RunnerResult(final_text="", returncode=1, stdout="", stderr="", session=session)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _preserve_failure_session(
|
|
213
|
+
runner: Runner,
|
|
214
|
+
result: RunnerResult,
|
|
215
|
+
fallback_session_id: str | None,
|
|
216
|
+
) -> RunnerResult:
|
|
217
|
+
if (result.session is not None and result.session.session_id) or not fallback_session_id:
|
|
218
|
+
return result
|
|
219
|
+
return RunnerResult(
|
|
220
|
+
final_text=result.final_text,
|
|
221
|
+
returncode=result.returncode,
|
|
222
|
+
stdout=result.stdout,
|
|
223
|
+
stderr=result.stderr,
|
|
224
|
+
session=_session_metadata(runner, fallback_session_id),
|
|
225
|
+
transcript=result.transcript,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _completion(
|
|
230
|
+
context: RuntimeContext,
|
|
231
|
+
invocation: _RunnerInvocation,
|
|
232
|
+
*,
|
|
233
|
+
failed: bool,
|
|
234
|
+
) -> None:
|
|
235
|
+
session = invocation.result.session
|
|
236
|
+
context.logger.llm_complete(
|
|
237
|
+
invocation.call_number,
|
|
238
|
+
invocation.request.stage,
|
|
239
|
+
invocation.request.profile_id,
|
|
240
|
+
invocation.request.runner_type,
|
|
241
|
+
session_id=session.session_id if session else None,
|
|
242
|
+
session_state=session.state if session else None,
|
|
243
|
+
resumable=session.resumable if session else False,
|
|
244
|
+
transcript=session.transcript if session else None,
|
|
245
|
+
# No adapter currently supplies a safe, provider-correct resume
|
|
246
|
+
# command. In particular, ordinary ``opencode -s`` is invalid
|
|
247
|
+
# for the isolated scratch state used by hook runs.
|
|
248
|
+
resume_command=None,
|
|
249
|
+
failed=failed,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _print_normalized_output(context: RuntimeContext, invocation: _RunnerInvocation) -> None:
|
|
254
|
+
if not context.config.logging.print_llm_output or not invocation.result.final_text:
|
|
255
|
+
return
|
|
256
|
+
# This is an explicit opt-in. Only normalized final text is printed and
|
|
257
|
+
# request/environment values are redacted; raw child streams stay private.
|
|
258
|
+
print(
|
|
259
|
+
redact_diagnostic(
|
|
260
|
+
invocation.result.final_text,
|
|
261
|
+
secrets=_request_sensitive_values(invocation.request),
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _request_sensitive_values(request: RunnerRequest) -> tuple[str, ...]:
|
|
267
|
+
environment_values = tuple(
|
|
268
|
+
value
|
|
269
|
+
for name, value in os.environ.items()
|
|
270
|
+
if value
|
|
271
|
+
and any(
|
|
272
|
+
marker in name.upper()
|
|
273
|
+
for marker in ("API_KEY", "TOKEN", "SECRET", "PASSWORD", "AUTH", "CREDENTIAL")
|
|
274
|
+
)
|
|
275
|
+
)
|
|
276
|
+
return (
|
|
277
|
+
request.instruction,
|
|
278
|
+
request.prompt_packet().render(),
|
|
279
|
+
*(artifact.content for artifact in request.artifacts),
|
|
280
|
+
*environment_values,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _named_error(
|
|
285
|
+
profile_name: str,
|
|
286
|
+
runner_type: str,
|
|
287
|
+
stage_name: str,
|
|
288
|
+
error: BaseException,
|
|
289
|
+
*,
|
|
290
|
+
request: RunnerRequest | None = None,
|
|
291
|
+
) -> HookError:
|
|
292
|
+
if request is None:
|
|
293
|
+
details = bounded_diagnostic(str(error), max_chars=1_200)
|
|
294
|
+
else:
|
|
295
|
+
process_result = getattr(error, "_process_result", None)
|
|
296
|
+
stdout = getattr(process_result, "stdout", "")
|
|
297
|
+
stderr = getattr(process_result, "stderr", "")
|
|
298
|
+
if not isinstance(stdout, str):
|
|
299
|
+
stdout = ""
|
|
300
|
+
if not isinstance(stderr, str):
|
|
301
|
+
stderr = ""
|
|
302
|
+
reason = request_sensitive_diagnostics(
|
|
303
|
+
request,
|
|
304
|
+
str(error),
|
|
305
|
+
max_chars=1_200,
|
|
306
|
+
env=os.environ,
|
|
307
|
+
)
|
|
308
|
+
streams = request_sensitive_diagnostics(
|
|
309
|
+
request,
|
|
310
|
+
stdout,
|
|
311
|
+
stderr,
|
|
312
|
+
max_chars=1_200,
|
|
313
|
+
env=os.environ,
|
|
314
|
+
)
|
|
315
|
+
details = bounded_diagnostic(
|
|
316
|
+
"\n".join(part for part in (reason, streams) if part),
|
|
317
|
+
max_chars=1_200,
|
|
318
|
+
)
|
|
319
|
+
message = (
|
|
320
|
+
f"Runner profile `{profile_name}` ({runner_type}) failed at stage `{stage_name}`"
|
|
321
|
+
)
|
|
322
|
+
if details:
|
|
323
|
+
message += f": {details}"
|
|
324
|
+
return HookError(message)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _invoke_runner(
|
|
328
|
+
context: RuntimeContext,
|
|
329
|
+
step: StepConfig,
|
|
330
|
+
prompt: str,
|
|
331
|
+
input_paths: list[pathlib.Path],
|
|
332
|
+
stage_name: str,
|
|
333
|
+
*,
|
|
334
|
+
working_directory: pathlib.Path,
|
|
335
|
+
session_id: str | None = None,
|
|
336
|
+
resume_session: bool = False,
|
|
337
|
+
attempt: int | None = None,
|
|
338
|
+
total_attempts: int | None = None,
|
|
339
|
+
prior_invocation: _RunnerInvocation | None = None,
|
|
340
|
+
) -> _RunnerInvocation:
|
|
341
|
+
selected_name = _selected_runner_name(context, step)
|
|
342
|
+
profile_name = selected_name
|
|
343
|
+
runner_type = "unknown"
|
|
344
|
+
request: RunnerRequest | None = None
|
|
345
|
+
try:
|
|
346
|
+
profile, request = _build_request(
|
|
347
|
+
context,
|
|
348
|
+
step,
|
|
349
|
+
prompt,
|
|
350
|
+
input_paths,
|
|
351
|
+
stage_name,
|
|
352
|
+
working_directory,
|
|
353
|
+
session_id=session_id,
|
|
354
|
+
resume_session=resume_session,
|
|
355
|
+
)
|
|
356
|
+
profile_name = profile.name
|
|
357
|
+
runner_type = profile.type
|
|
358
|
+
# Adapter construction (including optional CLI capability checks) is
|
|
359
|
+
# deliberately before call accounting. Counts represent invocations,
|
|
360
|
+
# never capability probes.
|
|
361
|
+
runner = get_runner(profile.type)
|
|
362
|
+
call_number = _call_logger(
|
|
363
|
+
context,
|
|
364
|
+
request,
|
|
365
|
+
profile.model,
|
|
366
|
+
attempt=attempt,
|
|
367
|
+
total_attempts=total_attempts,
|
|
368
|
+
)
|
|
369
|
+
except Exception as exc: # noqa: BLE001
|
|
370
|
+
if prior_invocation is not None:
|
|
371
|
+
try:
|
|
372
|
+
_finalize_invocation(context, prior_invocation, failed=True)
|
|
373
|
+
except Exception as finalize_error: # noqa: BLE001
|
|
374
|
+
context.logger.warn(
|
|
375
|
+
"llm.finalize_failed",
|
|
376
|
+
"Runner lifecycle finalization failed while cleaning a retained retry session.",
|
|
377
|
+
stage_name=stage_name,
|
|
378
|
+
runner_profile=prior_invocation.request.profile_id,
|
|
379
|
+
runner_type=prior_invocation.request.runner_type,
|
|
380
|
+
reason=type(finalize_error).__name__,
|
|
381
|
+
)
|
|
382
|
+
raise _named_error(
|
|
383
|
+
profile_name,
|
|
384
|
+
runner_type,
|
|
385
|
+
stage_name,
|
|
386
|
+
exc,
|
|
387
|
+
request=request,
|
|
388
|
+
) from exc
|
|
389
|
+
|
|
390
|
+
result: RunnerResult | None = None
|
|
391
|
+
try:
|
|
392
|
+
result = runner.run(request)
|
|
393
|
+
if not isinstance(result, RunnerResult):
|
|
394
|
+
raise RunnerProtocolError("runner returned an invalid result")
|
|
395
|
+
require_zero_exit(request, result, env=os.environ)
|
|
396
|
+
require_final_text(result.final_text, mode=request.mode)
|
|
397
|
+
except Exception as exc: # noqa: BLE001
|
|
398
|
+
# A runner may have learned a session ID before reporting a process or
|
|
399
|
+
# protocol failure. Give the optional lifecycle a chance to clean it
|
|
400
|
+
# up, even though no successful RunnerResult was returned.
|
|
401
|
+
if isinstance(result, RunnerResult):
|
|
402
|
+
result = _preserve_failure_session(runner, result, request.session_id)
|
|
403
|
+
else:
|
|
404
|
+
result = _failure_result(runner, exc, request.session_id)
|
|
405
|
+
invocation = _RunnerInvocation(request, runner, result, call_number)
|
|
406
|
+
try:
|
|
407
|
+
invocation.result = finalize_runner(runner, request, result)
|
|
408
|
+
except Exception as finalize_error: # noqa: BLE001
|
|
409
|
+
context.logger.warn(
|
|
410
|
+
"llm.finalize_failed",
|
|
411
|
+
"Runner lifecycle finalization failed after a runner error.",
|
|
412
|
+
stage_name=stage_name,
|
|
413
|
+
runner_profile=profile_name,
|
|
414
|
+
runner_type=runner_type,
|
|
415
|
+
reason=type(finalize_error).__name__,
|
|
416
|
+
)
|
|
417
|
+
_completion(context, invocation, failed=True)
|
|
418
|
+
raise _named_error(
|
|
419
|
+
profile_name,
|
|
420
|
+
runner_type,
|
|
421
|
+
stage_name,
|
|
422
|
+
exc,
|
|
423
|
+
request=request,
|
|
424
|
+
) from exc
|
|
425
|
+
|
|
426
|
+
return _RunnerInvocation(request, runner, result, call_number)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _finalize_invocation(
|
|
430
|
+
context: RuntimeContext,
|
|
431
|
+
invocation: _RunnerInvocation,
|
|
432
|
+
*,
|
|
433
|
+
failed: bool,
|
|
434
|
+
) -> RunnerResult:
|
|
435
|
+
try:
|
|
436
|
+
invocation.result = finalize_runner(
|
|
437
|
+
invocation.runner,
|
|
438
|
+
invocation.request,
|
|
439
|
+
invocation.result,
|
|
440
|
+
)
|
|
441
|
+
except Exception as exc: # noqa: BLE001
|
|
442
|
+
_completion(context, invocation, failed=True)
|
|
443
|
+
raise _named_error(
|
|
444
|
+
invocation.request.profile_id,
|
|
445
|
+
invocation.request.runner_type,
|
|
446
|
+
invocation.request.stage,
|
|
447
|
+
exc,
|
|
448
|
+
request=invocation.request,
|
|
449
|
+
) from exc
|
|
450
|
+
_completion(context, invocation, failed=failed)
|
|
451
|
+
if not failed:
|
|
452
|
+
_print_normalized_output(context, invocation)
|
|
453
|
+
return invocation.result
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def run_runner_once(
|
|
457
|
+
context: RuntimeContext,
|
|
458
|
+
step: StepConfig,
|
|
459
|
+
prompt: str,
|
|
460
|
+
input_paths: list[pathlib.Path],
|
|
461
|
+
stage_name: str,
|
|
462
|
+
*,
|
|
463
|
+
working_directory: pathlib.Path,
|
|
464
|
+
) -> RunnerResult:
|
|
465
|
+
"""Resolve, invoke, validate, finalize, and report one runner call."""
|
|
466
|
+
|
|
467
|
+
invocation = _invoke_runner(
|
|
468
|
+
context,
|
|
469
|
+
step,
|
|
470
|
+
prompt,
|
|
471
|
+
input_paths,
|
|
472
|
+
stage_name,
|
|
473
|
+
working_directory=working_directory,
|
|
474
|
+
)
|
|
475
|
+
return _finalize_invocation(context, invocation, failed=False)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
__all__ = ["run_runner_once"]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Internal runner boundary used by future workflow integrations.
|
|
2
|
+
|
|
3
|
+
Adapters are deliberately not imported here. ST-2 supplies the four lazy
|
|
4
|
+
adapter modules while ST-3 supplies workflow dispatch. Importing this package
|
|
5
|
+
is therefore safe in installations that do not have any selected CLI.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .contracts import (
|
|
9
|
+
MAX_DIAGNOSTIC_CHARS,
|
|
10
|
+
Runner,
|
|
11
|
+
RunnerAdapterUnavailableError,
|
|
12
|
+
RunnerArtifact,
|
|
13
|
+
RunnerCapabilities,
|
|
14
|
+
RunnerContractError,
|
|
15
|
+
RunnerError,
|
|
16
|
+
RunnerExecutableNotFoundError,
|
|
17
|
+
RunnerMissingOutputError,
|
|
18
|
+
RunnerNonzeroExitError,
|
|
19
|
+
RunnerProtocolError,
|
|
20
|
+
RunnerRequest,
|
|
21
|
+
RunnerResult,
|
|
22
|
+
RunnerSignalError,
|
|
23
|
+
RunnerTimeoutError,
|
|
24
|
+
SessionMetadata,
|
|
25
|
+
PromptPacket,
|
|
26
|
+
build_prompt_packet,
|
|
27
|
+
bounded_diagnostic,
|
|
28
|
+
bounded_redacted_diagnostics,
|
|
29
|
+
finalize_runner,
|
|
30
|
+
redact_diagnostic,
|
|
31
|
+
require_final_text,
|
|
32
|
+
require_zero_exit,
|
|
33
|
+
strip_terminal_controls,
|
|
34
|
+
)
|
|
35
|
+
from .process import DEFAULT_MAX_OUTPUT_BYTES, ProcessResult, run_process
|
|
36
|
+
from .registry import (
|
|
37
|
+
DEFAULT_RUNNER_REGISTRY,
|
|
38
|
+
KNOWN_RUNNER_TYPES,
|
|
39
|
+
LazyRunnerSpec,
|
|
40
|
+
RunnerRegistry,
|
|
41
|
+
get_runner,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"DEFAULT_MAX_OUTPUT_BYTES",
|
|
46
|
+
"DEFAULT_RUNNER_REGISTRY",
|
|
47
|
+
"KNOWN_RUNNER_TYPES",
|
|
48
|
+
"MAX_DIAGNOSTIC_CHARS",
|
|
49
|
+
"LazyRunnerSpec",
|
|
50
|
+
"ProcessResult",
|
|
51
|
+
"PromptPacket",
|
|
52
|
+
"Runner",
|
|
53
|
+
"RunnerAdapterUnavailableError",
|
|
54
|
+
"RunnerArtifact",
|
|
55
|
+
"RunnerCapabilities",
|
|
56
|
+
"RunnerContractError",
|
|
57
|
+
"RunnerError",
|
|
58
|
+
"RunnerExecutableNotFoundError",
|
|
59
|
+
"RunnerMissingOutputError",
|
|
60
|
+
"RunnerNonzeroExitError",
|
|
61
|
+
"RunnerProtocolError",
|
|
62
|
+
"RunnerRegistry",
|
|
63
|
+
"RunnerRequest",
|
|
64
|
+
"RunnerResult",
|
|
65
|
+
"RunnerSignalError",
|
|
66
|
+
"RunnerTimeoutError",
|
|
67
|
+
"SessionMetadata",
|
|
68
|
+
"bounded_diagnostic",
|
|
69
|
+
"bounded_redacted_diagnostics",
|
|
70
|
+
"build_prompt_packet",
|
|
71
|
+
"finalize_runner",
|
|
72
|
+
"get_runner",
|
|
73
|
+
"redact_diagnostic",
|
|
74
|
+
"require_final_text",
|
|
75
|
+
"require_zero_exit",
|
|
76
|
+
"run_process",
|
|
77
|
+
"strip_terminal_controls",
|
|
78
|
+
]
|