shikiru-cli 0.2.0__py3-none-any.whl
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.
- shikiru/__init__.py +1 -0
- shikiru/__main__.py +8 -0
- shikiru/agents/__init__.py +3 -0
- shikiru/agents/contracts.py +590 -0
- shikiru/agents/registry.py +87 -0
- shikiru/agents/sdk/__init__.py +17 -0
- shikiru/agents/sdk/reset_time.py +137 -0
- shikiru/agents/sdk/turn.py +335 -0
- shikiru/agents/settings.py +151 -0
- shikiru/agents/testing/__init__.py +26 -0
- shikiru/agents/testing/contract_suite.py +653 -0
- shikiru/agents/testing/resume.py +15 -0
- shikiru/bootstrap.py +1277 -0
- shikiru/cli/__init__.py +1 -0
- shikiru/cli/app.py +770 -0
- shikiru/cli/context.py +63 -0
- shikiru/cli/render.py +1475 -0
- shikiru/cli/report.py +156 -0
- shikiru/cli/session_prompt.py +63 -0
- shikiru/cli/view.py +131 -0
- shikiru/clock.py +41 -0
- shikiru/config/__init__.py +8 -0
- shikiru/config/defaults.toml +131 -0
- shikiru/config/loader.py +178 -0
- shikiru/config/settings.py +392 -0
- shikiru/dashboard/__init__.py +3 -0
- shikiru/dashboard/actions_list.py +799 -0
- shikiru/dashboard/activity.py +150 -0
- shikiru/dashboard/app.py +1765 -0
- shikiru/dashboard/blocker_evidence.py +122 -0
- shikiru/dashboard/blocker_rows.py +84 -0
- shikiru/dashboard/briefing_view.py +871 -0
- shikiru/dashboard/contract.py +1086 -0
- shikiru/dashboard/covered.py +32 -0
- shikiru/dashboard/detail.py +1209 -0
- shikiru/dashboard/detail_fold.py +244 -0
- shikiru/dashboard/detail_hint.py +68 -0
- shikiru/dashboard/details_list_view.py +150 -0
- shikiru/dashboard/diagnostics.py +80 -0
- shikiru/dashboard/diagnostics_sections.py +186 -0
- shikiru/dashboard/evidence_table.py +123 -0
- shikiru/dashboard/extra_details.py +214 -0
- shikiru/dashboard/fault_guidance.py +216 -0
- shikiru/dashboard/fold_bodies.py +276 -0
- shikiru/dashboard/help_keys.py +166 -0
- shikiru/dashboard/help_screen.py +204 -0
- shikiru/dashboard/interaction.py +17 -0
- shikiru/dashboard/ladder_editor.py +544 -0
- shikiru/dashboard/live_settings.py +62 -0
- shikiru/dashboard/motion.py +368 -0
- shikiru/dashboard/orchestration_fields.py +201 -0
- shikiru/dashboard/palette.py +1606 -0
- shikiru/dashboard/palette_app.py +89 -0
- shikiru/dashboard/palette_drafts.py +33 -0
- shikiru/dashboard/resolution.py +573 -0
- shikiru/dashboard/resolution_view.py +302 -0
- shikiru/dashboard/row_list.py +256 -0
- shikiru/dashboard/section_rows.py +173 -0
- shikiru/dashboard/services.py +665 -0
- shikiru/dashboard/session_history.py +402 -0
- shikiru/dashboard/shikiru_init_ascii_art.txt +40 -0
- shikiru/dashboard/task_picker.py +358 -0
- shikiru/dashboard/tick.py +59 -0
- shikiru/dashboard/timeline_filter.py +129 -0
- shikiru/dashboard/timeline_section.py +385 -0
- shikiru/dashboard/usage_body.py +269 -0
- shikiru/dashboard/views.py +2314 -0
- shikiru/dashboard/wizard.py +1389 -0
- shikiru/domain/__init__.py +1 -0
- shikiru/domain/acceptance.py +130 -0
- shikiru/domain/activity.py +28 -0
- shikiru/domain/attempts.py +333 -0
- shikiru/domain/briefing.py +257 -0
- shikiru/domain/commands.py +219 -0
- shikiru/domain/decisions.py +231 -0
- shikiru/domain/docker.py +262 -0
- shikiru/domain/engine_uptime.py +87 -0
- shikiru/domain/escalation.py +194 -0
- shikiru/domain/escalation_evidence.py +186 -0
- shikiru/domain/escalation_panel.py +144 -0
- shikiru/domain/events.py +71 -0
- shikiru/domain/evidence_export.py +89 -0
- shikiru/domain/faults.py +86 -0
- shikiru/domain/feedback.py +180 -0
- shikiru/domain/feedback_rounds.py +172 -0
- shikiru/domain/fencing.py +31 -0
- shikiru/domain/ladder.py +102 -0
- shikiru/domain/ladder_text.py +44 -0
- shikiru/domain/levels.py +44 -0
- shikiru/domain/motion.py +199 -0
- shikiru/domain/packet_evidence.py +377 -0
- shikiru/domain/pushes.py +47 -0
- shikiru/domain/quota_blocks.py +44 -0
- shikiru/domain/quota_waits.py +219 -0
- shikiru/domain/recovery.py +194 -0
- shikiru/domain/reports.py +498 -0
- shikiru/domain/results.py +217 -0
- shikiru/domain/runs.py +85 -0
- shikiru/domain/scheduling.py +231 -0
- shikiru/domain/status_tasks.py +40 -0
- shikiru/domain/steps.py +325 -0
- shikiru/domain/tasks.py +174 -0
- shikiru/domain/timeline.py +208 -0
- shikiru/domain/timeline_phrases.py +271 -0
- shikiru/domain/versions.py +18 -0
- shikiru/domain/workflow.py +372 -0
- shikiru/engine/__init__.py +0 -0
- shikiru/engine/acceptance.py +90 -0
- shikiru/engine/agents.py +61 -0
- shikiru/engine/cosmetic_decorator.py +99 -0
- shikiru/engine/daemon.py +409 -0
- shikiru/engine/dependency.py +37 -0
- shikiru/engine/escalation_filing.py +110 -0
- shikiru/engine/escalations.py +401 -0
- shikiru/engine/events.py +285 -0
- shikiru/engine/evidence.py +81 -0
- shikiru/engine/fault_reporter.py +144 -0
- shikiru/engine/feedback.py +577 -0
- shikiru/engine/feedback_audit.py +80 -0
- shikiru/engine/feedback_intake.py +95 -0
- shikiru/engine/feedback_snapshot.py +148 -0
- shikiru/engine/flow.py +87 -0
- shikiru/engine/handlers/__init__.py +33 -0
- shikiru/engine/handlers/base.py +148 -0
- shikiru/engine/handlers/engine.py +24 -0
- shikiru/engine/handlers/escalations.py +174 -0
- shikiru/engine/handlers/operator.py +81 -0
- shikiru/engine/handlers/tasks.py +237 -0
- shikiru/engine/history.py +128 -0
- shikiru/engine/integration.py +1234 -0
- shikiru/engine/integration_turns.py +337 -0
- shikiru/engine/layout_guard.py +362 -0
- shikiru/engine/lifecycle.py +60 -0
- shikiru/engine/monitor.py +317 -0
- shikiru/engine/notification_delivery.py +124 -0
- shikiru/engine/notification_feed.py +57 -0
- shikiru/engine/notifier.py +91 -0
- shikiru/engine/pull_request_closing.py +117 -0
- shikiru/engine/push_service.py +177 -0
- shikiru/engine/quota_probe_threads.py +79 -0
- shikiru/engine/quota_reconciler.py +210 -0
- shikiru/engine/quota_schedule.py +24 -0
- shikiru/engine/quota_waits.py +102 -0
- shikiru/engine/reconcile.py +91 -0
- shikiru/engine/recovery.py +284 -0
- shikiru/engine/retention.py +102 -0
- shikiru/engine/root_preparation.py +86 -0
- shikiru/engine/scheduler.py +320 -0
- shikiru/engine/session_guard.py +46 -0
- shikiru/engine/sessions.py +95 -0
- shikiru/engine/signalled.py +75 -0
- shikiru/engine/step_runner.py +140 -0
- shikiru/engine/supervisor.py +101 -0
- shikiru/engine/takeover.py +223 -0
- shikiru/engine/task_facts.py +46 -0
- shikiru/engine/tick_guard.py +95 -0
- shikiru/engine/titles.py +297 -0
- shikiru/engine/transitions.py +79 -0
- shikiru/engine/turn_context.py +206 -0
- shikiru/engine/turn_execution.py +30 -0
- shikiru/engine/turn_loop.py +230 -0
- shikiru/engine/turn_processes.py +79 -0
- shikiru/engine/turns.py +457 -0
- shikiru/errors.py +144 -0
- shikiru/helper/__init__.py +3 -0
- shikiru/helper/helper.py +182 -0
- shikiru/helper/models.py +11 -0
- shikiru/mcp/__init__.py +1 -0
- shikiru/mcp/server.py +196 -0
- shikiru/operator/__init__.py +33 -0
- shikiru/operator/attachment.py +24 -0
- shikiru/operator/commands.py +319 -0
- shikiru/operator/detectors.py +374 -0
- shikiru/operator/doctor.py +351 -0
- shikiru/operator/events.py +57 -0
- shikiru/operator/evidence_export.py +498 -0
- shikiru/operator/evidence_writer.py +78 -0
- shikiru/operator/lifecycle.py +294 -0
- shikiru/operator/plugin_checks.py +194 -0
- shikiru/operator/probe.py +287 -0
- shikiru/operator/queries.py +1601 -0
- shikiru/operator/services.py +36 -0
- shikiru/operator/sessions.py +378 -0
- shikiru/operator/strip_views.py +162 -0
- shikiru/operator/timeline.py +416 -0
- shikiru/operator/toml_text.py +62 -0
- shikiru/palette.py +18 -0
- shikiru/paths.py +91 -0
- shikiru/plugins/__init__.py +0 -0
- shikiru/plugins/agents/__init__.py +1 -0
- shikiru/plugins/agents/claude/__init__.py +5 -0
- shikiru/plugins/agents/claude/agent.py +251 -0
- shikiru/plugins/agents/claude/context.py +116 -0
- shikiru/plugins/agents/claude/headless.py +92 -0
- shikiru/plugins/agents/claude/hook.py +57 -0
- shikiru/plugins/agents/claude/model_catalog.py +105 -0
- shikiru/plugins/agents/claude/options.py +62 -0
- shikiru/plugins/agents/claude/turn.py +1018 -0
- shikiru/plugins/agents/codex/__init__.py +5 -0
- shikiru/plugins/agents/codex/agent.py +486 -0
- shikiru/plugins/agents/codex/headless.py +166 -0
- shikiru/plugins/agents/codex/home.py +14 -0
- shikiru/plugins/agents/codex/model_catalog.py +128 -0
- shikiru/plugins/agents/codex/options.py +94 -0
- shikiru/plugins/agents/codex/quota.py +195 -0
- shikiru/plugins/agents/codex/sandbox.py +31 -0
- shikiru/plugins/agents/codex/sandbox_probe.py +97 -0
- shikiru/plugins/agents/codex/turn.py +767 -0
- shikiru/plugins/agents/codex/usage.py +106 -0
- shikiru/plugins/agents/shared_settings.py +62 -0
- shikiru/plugins/widgets/__init__.py +3 -0
- shikiru/plugins/widgets/dialogue.py +144 -0
- shikiru/plugins/widgets/disk.py +123 -0
- shikiru/plugins/widgets/docker.py +225 -0
- shikiru/plugins/widgets/escalations.py +402 -0
- shikiru/plugins/widgets/faults.py +204 -0
- shikiru/plugins/widgets/header.py +321 -0
- shikiru/plugins/widgets/queue.py +1140 -0
- shikiru/plugins/widgets/queue_hint.py +93 -0
- shikiru/plugins/widgets/resources.py +105 -0
- shikiru/plugins/widgets/strips.py +82 -0
- shikiru/plugins/widgets/usage.py +176 -0
- shikiru/private.py +94 -0
- shikiru/process.py +535 -0
- shikiru/prompts/__init__.py +3 -0
- shikiru/prompts/renderer.py +85 -0
- shikiru/prompts/templates/_base.md.j2 +17 -0
- shikiru/prompts/templates/_macros.md.j2 +61 -0
- shikiru/prompts/templates/conflicts.md.j2 +22 -0
- shikiru/prompts/templates/consult.md.j2 +5 -0
- shikiru/prompts/templates/continue.md.j2 +4 -0
- shikiru/prompts/templates/feedback_analysis.md.j2 +29 -0
- shikiru/prompts/templates/feedback_rework.md.j2 +33 -0
- shikiru/prompts/templates/feedback_verify.md.j2 +33 -0
- shikiru/prompts/templates/handoff.md.j2 +23 -0
- shikiru/prompts/templates/implement.md.j2 +13 -0
- shikiru/prompts/templates/plan.md.j2 +30 -0
- shikiru/prompts/templates/rework.md.j2 +13 -0
- shikiru/prompts/templates/triage.md.j2 +20 -0
- shikiru/prompts/templates/verify.md.j2 +29 -0
- shikiru/store/__init__.py +1 -0
- shikiru/store/commands.py +242 -0
- shikiru/store/database.py +297 -0
- shikiru/store/escalations.py +240 -0
- shikiru/store/events.py +213 -0
- shikiru/store/feedback.py +266 -0
- shikiru/store/migrations/001_initial.sql +63 -0
- shikiru/store/migrations/002_headless_turns.sql +31 -0
- shikiru/store/migrations/003_drop_command_handled_at.sql +1 -0
- shikiru/store/migrations/004_repo_base_ref.sql +3 -0
- shikiru/store/migrations/005_complexity_explicit.sql +3 -0
- shikiru/store/migrations/006_events_kind_index.sql +1 -0
- shikiru/store/migrations/007_notification_cursor.sql +1 -0
- shikiru/store/migrations/008_reported_usage.sql +1 -0
- shikiru/store/migrations/009_billing_declarations.sql +2 -0
- shikiru/store/migrations/010_canonical_quota_windows.sql +3 -0
- shikiru/store/migrations/012_drop_task_quota_evidence.sql +2 -0
- shikiru/store/migrations/013_push_targets.sql +7 -0
- shikiru/store/migrations/014_agent_block_time.sql +6 -0
- shikiru/store/migrations/015_repair_orphan_agent_blocks.sql +3 -0
- shikiru/store/migrations/016_feedback_rounds.sql +29 -0
- shikiru/store/migrations/017_feedback_round_comments.sql +5 -0
- shikiru/store/migrations/__init__.py +1 -0
- shikiru/store/notification_cursor.py +29 -0
- shikiru/store/pushes.py +117 -0
- shikiru/store/quotas.py +153 -0
- shikiru/store/reader.py +310 -0
- shikiru/store/retention.py +92 -0
- shikiru/store/runs.py +227 -0
- shikiru/store/tasks.py +949 -0
- shikiru/telemetry/__init__.py +1 -0
- shikiru/telemetry/disk.py +167 -0
- shikiru/telemetry/docker.py +164 -0
- shikiru/telemetry/resources.py +296 -0
- shikiru/tmux/__init__.py +3 -0
- shikiru/tmux/client.py +637 -0
- shikiru/tmux/decorator.py +225 -0
- shikiru/tmux/layout.py +200 -0
- shikiru/tmux/session.py +381 -0
- shikiru/ui/__init__.py +9 -0
- shikiru/ui/age.py +45 -0
- shikiru/ui/bubbles.py +41 -0
- shikiru/ui/compact.py +1209 -0
- shikiru/ui/console.py +49 -0
- shikiru/ui/fold.py +50 -0
- shikiru/ui/gated_static.py +117 -0
- shikiru/ui/gauge.py +40 -0
- shikiru/ui/glyphs.py +109 -0
- shikiru/ui/keys.py +57 -0
- shikiru/ui/live_interval.py +42 -0
- shikiru/ui/popup.py +311 -0
- shikiru/ui/quota_status.py +26 -0
- shikiru/ui/recent_window.py +78 -0
- shikiru/ui/screen.py +57 -0
- shikiru/ui/section.py +193 -0
- shikiru/ui/session.py +65 -0
- shikiru/ui/size.py +17 -0
- shikiru/ui/splash.py +42 -0
- shikiru/ui/status.py +47 -0
- shikiru/ui/status_strip.py +149 -0
- shikiru/ui/stream.py +152 -0
- shikiru/ui/strip.py +332 -0
- shikiru/ui/theme.py +83 -0
- shikiru/ui/tokens.py +51 -0
- shikiru/view_log.py +126 -0
- shikiru/workspace/__init__.py +3 -0
- shikiru/workspace/base_refs.py +160 -0
- shikiru/workspace/caches.py +136 -0
- shikiru/workspace/facts.py +118 -0
- shikiru/workspace/git.py +601 -0
- shikiru/workspace/github.py +744 -0
- shikiru/workspace/messages.py +107 -0
- shikiru/workspace/reports.py +145 -0
- shikiru/workspace/roots.py +180 -0
- shikiru_cli-0.2.0.dist-info/METADATA +428 -0
- shikiru_cli-0.2.0.dist-info/RECORD +319 -0
- shikiru_cli-0.2.0.dist-info/WHEEL +4 -0
- shikiru_cli-0.2.0.dist-info/entry_points.txt +18 -0
- shikiru_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
shikiru/engine/turns.py
ADDED
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
"""Builds one prompt directory and runs its resumable headless turns."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from datetime import timedelta
|
|
9
|
+
from typing import TYPE_CHECKING, TypedDict
|
|
10
|
+
|
|
11
|
+
from shikiru.agents.contracts import (
|
|
12
|
+
PROJECT_ENV_VAR,
|
|
13
|
+
ROLE_ENV_VAR,
|
|
14
|
+
RUN_DIR_ENV_VAR,
|
|
15
|
+
SLOT_ENV_VAR,
|
|
16
|
+
TASK_ENV_VAR,
|
|
17
|
+
FailureKind,
|
|
18
|
+
TurnFailed,
|
|
19
|
+
role_dir,
|
|
20
|
+
)
|
|
21
|
+
from shikiru.agents.contracts import (
|
|
22
|
+
TurnSpec as AgentTurnSpec,
|
|
23
|
+
)
|
|
24
|
+
from shikiru.domain.events import EventKind
|
|
25
|
+
from shikiru.domain.feedback import FeedbackContractError, FeedbackInput, check_feedback_report
|
|
26
|
+
from shikiru.domain.recovery import Completed, Failed, ProtocolError, Stalled, TurnResult
|
|
27
|
+
from shikiru.domain.reports import (
|
|
28
|
+
CURRENT_TURN_LINK_NAME,
|
|
29
|
+
FACTS_FILENAME,
|
|
30
|
+
FEEDBACK_FILENAME,
|
|
31
|
+
HELPER_REPORT_FILENAME,
|
|
32
|
+
PROMPT_FILENAME,
|
|
33
|
+
REPORT_FILENAME,
|
|
34
|
+
REPORT_JSON_INDENT,
|
|
35
|
+
REPORT_KIND_FILENAME,
|
|
36
|
+
REPORT_SCHEMA_FILENAME,
|
|
37
|
+
schema_for,
|
|
38
|
+
)
|
|
39
|
+
from shikiru.errors import InvariantError, ShikiruError
|
|
40
|
+
from shikiru.helper.helper import FailureClassification
|
|
41
|
+
from shikiru.helper.models import Unrecoverable
|
|
42
|
+
from shikiru.paths import run_dir, task_root, turns_dir
|
|
43
|
+
from shikiru.private import make_private_dir, write_private_text
|
|
44
|
+
from shikiru.prompts.renderer import OPERATOR_MESSAGES_KEY
|
|
45
|
+
from shikiru.view_log import TURN_DIR_ITERATION_MARKER, TURN_DIR_SEPARATOR
|
|
46
|
+
from shikiru.workspace.reports import InvalidTurnReportError, load_turn_report
|
|
47
|
+
|
|
48
|
+
if TYPE_CHECKING:
|
|
49
|
+
from collections.abc import Callable, Mapping
|
|
50
|
+
from pathlib import Path
|
|
51
|
+
|
|
52
|
+
from shikiru.config.settings import Settings
|
|
53
|
+
from shikiru.domain.reports import Report, ReportKind
|
|
54
|
+
from shikiru.domain.tasks import Role, Step, Task
|
|
55
|
+
from shikiru.engine.agents import AgentPool
|
|
56
|
+
from shikiru.engine.monitor import TurnMonitor
|
|
57
|
+
from shikiru.engine.sessions import SessionManager, SessionSelection
|
|
58
|
+
from shikiru.helper.helper import Helper
|
|
59
|
+
from shikiru.prompts.renderer import PromptName, PromptRenderer
|
|
60
|
+
from shikiru.store.events import EventLog
|
|
61
|
+
from shikiru.store.runs import RunStore
|
|
62
|
+
from shikiru.store.tasks import TaskStore
|
|
63
|
+
from shikiru.workspace.messages import OperatorMessages
|
|
64
|
+
|
|
65
|
+
NUDGE_LIMIT = 2
|
|
66
|
+
CLASSIFY_CONFIDENCE = 0.8
|
|
67
|
+
_TURN_NUMBER_WIDTH = 2
|
|
68
|
+
TURN_PROMPT = "Read {prompt_path} and follow it exactly."
|
|
69
|
+
_NUDGE_MESSAGE = (
|
|
70
|
+
"Your turn ended without a valid report. Submit it with `shikiru report` as described "
|
|
71
|
+
"at the end of {prompt_path}."
|
|
72
|
+
)
|
|
73
|
+
_LOGGER = logging.getLogger(__name__)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class TurnHeld(Exception):
|
|
77
|
+
"""A task hold was set before the turn's agent process could start."""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class _HelperCall(TypedDict):
|
|
81
|
+
cwd: Path
|
|
82
|
+
timeout: timedelta
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True)
|
|
86
|
+
class TurnRequest:
|
|
87
|
+
"""Prompt details for one workflow step."""
|
|
88
|
+
|
|
89
|
+
step: Step
|
|
90
|
+
iteration: int
|
|
91
|
+
kind: ReportKind
|
|
92
|
+
template: PromptName
|
|
93
|
+
context: Mapping[str, object]
|
|
94
|
+
facts: str
|
|
95
|
+
feedback: FeedbackInput | None = None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def read_turn_report(turn_dir: Path) -> Report | None:
|
|
99
|
+
"""Read `turn_dir`'s submitted report or Helper rebuild; invalid files raise explicitly."""
|
|
100
|
+
loaded = load_turn_report(turn_dir)
|
|
101
|
+
return loaded.report if loaded is not None else None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def turn_environment(
|
|
105
|
+
task: Task, role: Role, run_dir_path: Path, project_name: str
|
|
106
|
+
) -> dict[str, str]:
|
|
107
|
+
"""Return the environment used by every agent process for `task`'s `role`."""
|
|
108
|
+
if task.slot is None:
|
|
109
|
+
message = f"{task.id} has no assigned slot; a turn needs a dispatched task"
|
|
110
|
+
raise InvariantError(message)
|
|
111
|
+
return session_environment(task, role, run_dir_path, project_name)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def session_environment(
|
|
115
|
+
task: Task, role: Role, run_dir_path: Path, project_name: str
|
|
116
|
+
) -> dict[str, str]:
|
|
117
|
+
"""Return the environment of `task`'s `role` process, with the slot only when it has one."""
|
|
118
|
+
environment = {
|
|
119
|
+
RUN_DIR_ENV_VAR: str(run_dir_path),
|
|
120
|
+
ROLE_ENV_VAR: role.value,
|
|
121
|
+
PROJECT_ENV_VAR: project_name,
|
|
122
|
+
TASK_ENV_VAR: task.id,
|
|
123
|
+
}
|
|
124
|
+
if task.slot is not None:
|
|
125
|
+
environment[SLOT_ENV_VAR] = str(task.slot)
|
|
126
|
+
return environment
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class TurnRunner:
|
|
130
|
+
"""Create turn files, run a process per attempt, and resolve reports and failures."""
|
|
131
|
+
|
|
132
|
+
def __init__(
|
|
133
|
+
self,
|
|
134
|
+
*,
|
|
135
|
+
agents: AgentPool,
|
|
136
|
+
sessions: SessionManager,
|
|
137
|
+
monitor: TurnMonitor,
|
|
138
|
+
helper: Helper,
|
|
139
|
+
renderer: PromptRenderer,
|
|
140
|
+
messages: OperatorMessages,
|
|
141
|
+
events: EventLog,
|
|
142
|
+
runs: RunStore,
|
|
143
|
+
tasks: TaskStore,
|
|
144
|
+
project_root: Path,
|
|
145
|
+
settings: Settings,
|
|
146
|
+
) -> None:
|
|
147
|
+
self._agents = agents
|
|
148
|
+
self._sessions = sessions
|
|
149
|
+
self._monitor = monitor
|
|
150
|
+
self._helper = helper
|
|
151
|
+
self._renderer = renderer
|
|
152
|
+
self._messages = messages
|
|
153
|
+
self._events = events
|
|
154
|
+
self._runs = runs
|
|
155
|
+
self._tasks = tasks
|
|
156
|
+
self._project_root = project_root
|
|
157
|
+
self._settings = settings
|
|
158
|
+
|
|
159
|
+
async def run(
|
|
160
|
+
self,
|
|
161
|
+
task: Task,
|
|
162
|
+
role: Role,
|
|
163
|
+
selection: SessionSelection,
|
|
164
|
+
request: TurnRequest,
|
|
165
|
+
*,
|
|
166
|
+
on_start: Callable[[Path], None] | None = None,
|
|
167
|
+
on_nudge: Callable[[], None],
|
|
168
|
+
) -> tuple[TurnResult, Path, SessionSelection]:
|
|
169
|
+
"""Run one workflow turn and any resumed nudge turns to a normalized result.
|
|
170
|
+
|
|
171
|
+
Operator messages delivered into the turn return to the queue unless it completes.
|
|
172
|
+
Raises `TurnHeld`, before any agent process starts, when the task is held.
|
|
173
|
+
"""
|
|
174
|
+
turn_dir = self._prepare_turn_dir(
|
|
175
|
+
run_dir(self._project_root, task.id), task.id, role, request
|
|
176
|
+
)
|
|
177
|
+
if on_start is not None:
|
|
178
|
+
on_start(turn_dir)
|
|
179
|
+
completed = False
|
|
180
|
+
try:
|
|
181
|
+
result, selection = await self._attempts(
|
|
182
|
+
task, role, selection, request, turn_dir, on_nudge=on_nudge
|
|
183
|
+
)
|
|
184
|
+
completed = isinstance(result, Completed)
|
|
185
|
+
finally:
|
|
186
|
+
if not completed:
|
|
187
|
+
self._messages.requeue(task.id, role, turn_dir)
|
|
188
|
+
return result, turn_dir, selection
|
|
189
|
+
|
|
190
|
+
async def _attempts(
|
|
191
|
+
self,
|
|
192
|
+
task: Task,
|
|
193
|
+
role: Role,
|
|
194
|
+
selection: SessionSelection,
|
|
195
|
+
request: TurnRequest,
|
|
196
|
+
turn_dir: Path,
|
|
197
|
+
*,
|
|
198
|
+
on_nudge: Callable[[], None],
|
|
199
|
+
) -> tuple[TurnResult, SessionSelection]:
|
|
200
|
+
task_run_dir = run_dir(self._project_root, task.id)
|
|
201
|
+
task_root_dir = task_root(self._project_root, task.id)
|
|
202
|
+
prompt_path = turn_dir / PROMPT_FILENAME
|
|
203
|
+
prompt = TURN_PROMPT.format(prompt_path=prompt_path)
|
|
204
|
+
nudges = 0
|
|
205
|
+
last_message = ""
|
|
206
|
+
while True:
|
|
207
|
+
if self._tasks.require(task.id).held:
|
|
208
|
+
raise TurnHeld
|
|
209
|
+
if nudges:
|
|
210
|
+
on_nudge()
|
|
211
|
+
agent = self._agents.get(selection.rung.agent)
|
|
212
|
+
turn_spec = AgentTurnSpec(
|
|
213
|
+
rung=selection.rung,
|
|
214
|
+
role=role.value,
|
|
215
|
+
prompt=prompt,
|
|
216
|
+
cwd=task_root_dir,
|
|
217
|
+
run_dir=task_run_dir,
|
|
218
|
+
turn_dir=turn_dir,
|
|
219
|
+
add_dirs=(),
|
|
220
|
+
env=turn_environment(task, role, task_run_dir, self._settings.project.name),
|
|
221
|
+
session_id=selection.session_id,
|
|
222
|
+
usage=selection.run.usage,
|
|
223
|
+
on_process=self._monitor.process_recorder(task_run_dir, role),
|
|
224
|
+
)
|
|
225
|
+
turn = await agent.start_turn(turn_spec)
|
|
226
|
+
terminal = await self._monitor.run(
|
|
227
|
+
turn,
|
|
228
|
+
agent=agent,
|
|
229
|
+
task=task,
|
|
230
|
+
role=role,
|
|
231
|
+
rung=selection.rung,
|
|
232
|
+
run_id=selection.run.id,
|
|
233
|
+
turn_dir=turn_dir,
|
|
234
|
+
run_dir=task_run_dir,
|
|
235
|
+
)
|
|
236
|
+
if isinstance(terminal, TurnFailed):
|
|
237
|
+
return await self._classify_unknown(terminal, task, selection), selection
|
|
238
|
+
if isinstance(terminal, Stalled | ProtocolError):
|
|
239
|
+
return terminal, selection
|
|
240
|
+
last_message = terminal.last_message
|
|
241
|
+
try:
|
|
242
|
+
report = read_turn_report(turn_dir)
|
|
243
|
+
_require_feedback_contract(request, report)
|
|
244
|
+
except InvalidTurnReportError, FeedbackContractError:
|
|
245
|
+
_LOGGER.exception("invalid report in completed turn %s", turn_dir)
|
|
246
|
+
report = None
|
|
247
|
+
if report is not None:
|
|
248
|
+
return Completed(report=report), selection
|
|
249
|
+
if nudges >= NUDGE_LIMIT:
|
|
250
|
+
rebuilt = await self._rebuild(task, selection, turn_dir, request, last_message)
|
|
251
|
+
return rebuilt, selection
|
|
252
|
+
nudges += 1
|
|
253
|
+
prompt = _NUDGE_MESSAGE.format(prompt_path=prompt_path)
|
|
254
|
+
selection = self._sessions.session_for(task, role)
|
|
255
|
+
|
|
256
|
+
def completed_turn(
|
|
257
|
+
self, task_id: str, role: Role, request: TurnRequest
|
|
258
|
+
) -> tuple[Completed, Path] | None:
|
|
259
|
+
"""Return the report of `request`'s latest turn when it holds a valid one.
|
|
260
|
+
|
|
261
|
+
A restarted engine re-runs the step whose turn it lost. When that turn is the task's
|
|
262
|
+
latest, still `role`'s current turn, and already submitted a valid report, the report is
|
|
263
|
+
consumed as is instead of starting another turn.
|
|
264
|
+
"""
|
|
265
|
+
turns_root = turns_dir(self._project_root, task_id)
|
|
266
|
+
if not turns_root.is_dir():
|
|
267
|
+
return None
|
|
268
|
+
turn_dir = turns_root / _turn_dir_name(_turn_count(turns_root), request)
|
|
269
|
+
link = role_dir(run_dir(self._project_root, task_id), role.value) / CURRENT_TURN_LINK_NAME
|
|
270
|
+
if not link.is_symlink() or link.readlink() != turn_dir:
|
|
271
|
+
return None
|
|
272
|
+
if not _turn_matches_request(turn_dir, request):
|
|
273
|
+
return None
|
|
274
|
+
try:
|
|
275
|
+
report = read_turn_report(turn_dir)
|
|
276
|
+
if report is not None:
|
|
277
|
+
_require_feedback_contract(request, report)
|
|
278
|
+
except InvalidTurnReportError, FeedbackContractError:
|
|
279
|
+
_LOGGER.exception("invalid report in interrupted turn %s", turn_dir)
|
|
280
|
+
return None
|
|
281
|
+
if report is None:
|
|
282
|
+
return None
|
|
283
|
+
return Completed(report=report), turn_dir
|
|
284
|
+
|
|
285
|
+
def _prepare_turn_dir(
|
|
286
|
+
self, task_run_dir: Path, task_id: str, role: Role, request: TurnRequest
|
|
287
|
+
) -> Path:
|
|
288
|
+
turns_root = turns_dir(self._project_root, task_id)
|
|
289
|
+
make_private_dir(turns_root)
|
|
290
|
+
turn_dir = turns_root / _turn_dir_name(_turn_count(turns_root) + 1, request)
|
|
291
|
+
make_private_dir(turn_dir)
|
|
292
|
+
self._requeue_unreported(task_run_dir, task_id, role)
|
|
293
|
+
(turn_dir / REPORT_FILENAME).unlink(missing_ok=True)
|
|
294
|
+
(turn_dir / HELPER_REPORT_FILENAME).unlink(missing_ok=True)
|
|
295
|
+
self._point_current_turn(task_run_dir, role, turn_dir)
|
|
296
|
+
delivered = self._messages.deliver(task_id, role, turn_dir)
|
|
297
|
+
context = {**request.context, OPERATOR_MESSAGES_KEY: [str(path) for path in delivered]}
|
|
298
|
+
write_private_text(
|
|
299
|
+
turn_dir / PROMPT_FILENAME, self._renderer.render(request.template, context)
|
|
300
|
+
)
|
|
301
|
+
write_private_text(
|
|
302
|
+
turn_dir / REPORT_SCHEMA_FILENAME,
|
|
303
|
+
json.dumps(schema_for(request.kind), indent=REPORT_JSON_INDENT),
|
|
304
|
+
)
|
|
305
|
+
write_private_text(turn_dir / REPORT_KIND_FILENAME, request.kind.value)
|
|
306
|
+
if request.facts:
|
|
307
|
+
write_private_text(turn_dir / FACTS_FILENAME, request.facts)
|
|
308
|
+
if request.feedback is not None:
|
|
309
|
+
write_private_text(
|
|
310
|
+
turn_dir / FEEDBACK_FILENAME,
|
|
311
|
+
request.feedback.model_dump_json(indent=REPORT_JSON_INDENT),
|
|
312
|
+
)
|
|
313
|
+
return turn_dir
|
|
314
|
+
|
|
315
|
+
def _requeue_unreported(self, task_run_dir: Path, task_id: str, role: Role) -> None:
|
|
316
|
+
link = role_dir(task_run_dir, role.value) / CURRENT_TURN_LINK_NAME
|
|
317
|
+
if not link.is_symlink():
|
|
318
|
+
return
|
|
319
|
+
previous = link.readlink()
|
|
320
|
+
reported = any(
|
|
321
|
+
(previous / name).exists() for name in (REPORT_FILENAME, HELPER_REPORT_FILENAME)
|
|
322
|
+
)
|
|
323
|
+
if not reported:
|
|
324
|
+
self._messages.requeue(task_id, role, previous)
|
|
325
|
+
|
|
326
|
+
@staticmethod
|
|
327
|
+
def _point_current_turn(task_run_dir: Path, role: Role, turn_dir: Path) -> None:
|
|
328
|
+
link = role_dir(task_run_dir, role.value) / CURRENT_TURN_LINK_NAME
|
|
329
|
+
make_private_dir(link.parent)
|
|
330
|
+
if link.is_symlink() or link.exists():
|
|
331
|
+
link.unlink()
|
|
332
|
+
link.symlink_to(turn_dir)
|
|
333
|
+
|
|
334
|
+
def _helper_call(self, task: Task) -> _HelperCall:
|
|
335
|
+
return _HelperCall(
|
|
336
|
+
cwd=task_root(self._project_root, task.id),
|
|
337
|
+
timeout=timedelta(seconds=self._settings.orchestration.timeouts.helper),
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
async def _classify_unknown(
|
|
341
|
+
self, failure: TurnFailed, task: Task, selection: SessionSelection
|
|
342
|
+
) -> Failed:
|
|
343
|
+
if failure.kind is not FailureKind.UNKNOWN:
|
|
344
|
+
return Failed(failure.kind, failure.resets_at, failure.message)
|
|
345
|
+
try:
|
|
346
|
+
classification = await self._helper.classify_failure(
|
|
347
|
+
failure.message, **self._helper_call(task)
|
|
348
|
+
)
|
|
349
|
+
except (ShikiruError, TimeoutError, OSError) as error:
|
|
350
|
+
_LOGGER.exception("failure classification failed for %s", task.id)
|
|
351
|
+
self._events.append(
|
|
352
|
+
EventKind.HELPER_ACTION,
|
|
353
|
+
task_id=task.id,
|
|
354
|
+
message=f"{task.id} failure classification failed",
|
|
355
|
+
data={"role": selection.run.role.value, "error": str(error)},
|
|
356
|
+
)
|
|
357
|
+
return Failed(FailureKind.OTHER, None, failure.message, unclassified=True)
|
|
358
|
+
if isinstance(classification, FailureClassification):
|
|
359
|
+
accepted = (
|
|
360
|
+
classification.confidence >= CLASSIFY_CONFIDENCE
|
|
361
|
+
and classification.kind is not FailureKind.UNKNOWN
|
|
362
|
+
)
|
|
363
|
+
self._events.append(
|
|
364
|
+
EventKind.HELPER_ACTION,
|
|
365
|
+
task_id=task.id,
|
|
366
|
+
message=f"{task.id} failure classified as {classification.kind.value}",
|
|
367
|
+
data={
|
|
368
|
+
"role": selection.run.role.value,
|
|
369
|
+
"kind": classification.kind.value,
|
|
370
|
+
"confidence": classification.confidence,
|
|
371
|
+
"accepted": accepted,
|
|
372
|
+
},
|
|
373
|
+
)
|
|
374
|
+
if accepted:
|
|
375
|
+
return Failed(classification.kind, classification.resets_at, failure.message)
|
|
376
|
+
else:
|
|
377
|
+
self._events.append(
|
|
378
|
+
EventKind.HELPER_ACTION,
|
|
379
|
+
task_id=task.id,
|
|
380
|
+
message=f"{task.id} failure could not be classified",
|
|
381
|
+
data={"role": selection.run.role.value, "reason": classification.unrecoverable},
|
|
382
|
+
)
|
|
383
|
+
return Failed(FailureKind.OTHER, None, failure.message, unclassified=True)
|
|
384
|
+
|
|
385
|
+
async def _rebuild(
|
|
386
|
+
self,
|
|
387
|
+
task: Task,
|
|
388
|
+
selection: SessionSelection,
|
|
389
|
+
turn_dir: Path,
|
|
390
|
+
request: TurnRequest,
|
|
391
|
+
last_message: str,
|
|
392
|
+
) -> TurnResult:
|
|
393
|
+
try:
|
|
394
|
+
report = await self._helper.rebuild_report(
|
|
395
|
+
request.kind, last_message, request.facts, **self._helper_call(task)
|
|
396
|
+
)
|
|
397
|
+
except (ShikiruError, TimeoutError, OSError) as error:
|
|
398
|
+
_LOGGER.exception("report rebuild failed for %s", task.id)
|
|
399
|
+
self._events.append(
|
|
400
|
+
EventKind.HELPER_ACTION,
|
|
401
|
+
task_id=task.id,
|
|
402
|
+
message=f"{task.id} report rebuild failed",
|
|
403
|
+
data={"role": selection.run.role.value, "error": str(error)},
|
|
404
|
+
)
|
|
405
|
+
return ProtocolError()
|
|
406
|
+
if isinstance(report, Unrecoverable):
|
|
407
|
+
self._events.append(
|
|
408
|
+
EventKind.HELPER_ACTION,
|
|
409
|
+
task_id=task.id,
|
|
410
|
+
message=f"{task.id} report could not be rebuilt",
|
|
411
|
+
data={"role": selection.run.role.value, "reason": report.unrecoverable},
|
|
412
|
+
)
|
|
413
|
+
return ProtocolError()
|
|
414
|
+
if request.feedback is not None:
|
|
415
|
+
try:
|
|
416
|
+
check_feedback_report(request.feedback, report)
|
|
417
|
+
except FeedbackContractError as error:
|
|
418
|
+
_LOGGER.warning(
|
|
419
|
+
"rebuilt report for %s breaks the feedback contract: %s", task.id, error
|
|
420
|
+
)
|
|
421
|
+
return ProtocolError()
|
|
422
|
+
write_private_text(
|
|
423
|
+
turn_dir / HELPER_REPORT_FILENAME, report.model_dump_json(indent=REPORT_JSON_INDENT)
|
|
424
|
+
)
|
|
425
|
+
return Completed(report=report, rebuilt=True)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _turn_matches_request(turn_dir: Path, request: TurnRequest) -> bool:
|
|
429
|
+
kind_path = turn_dir / REPORT_KIND_FILENAME
|
|
430
|
+
if not kind_path.is_file() or kind_path.read_text().strip() != request.kind.value:
|
|
431
|
+
return False
|
|
432
|
+
feedback_path = turn_dir / FEEDBACK_FILENAME
|
|
433
|
+
if request.feedback is None:
|
|
434
|
+
return not feedback_path.exists()
|
|
435
|
+
if not feedback_path.is_file():
|
|
436
|
+
return False
|
|
437
|
+
try:
|
|
438
|
+
recorded = FeedbackInput.model_validate_json(feedback_path.read_text())
|
|
439
|
+
except ValueError:
|
|
440
|
+
return False
|
|
441
|
+
return recorded == request.feedback
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _require_feedback_contract(request: TurnRequest, report: Report | None) -> None:
|
|
445
|
+
if request.feedback is not None and report is not None:
|
|
446
|
+
check_feedback_report(request.feedback, report)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _turn_count(turns_root: Path) -> int:
|
|
450
|
+
return sum(1 for entry in turns_root.iterdir() if entry.is_dir())
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _turn_dir_name(number: int, request: TurnRequest) -> str:
|
|
454
|
+
return (
|
|
455
|
+
f"{number:0{_TURN_NUMBER_WIDTH}d}{TURN_DIR_SEPARATOR}{request.step.value}"
|
|
456
|
+
f"{TURN_DIR_ITERATION_MARKER}{request.iteration}"
|
|
457
|
+
)
|
shikiru/errors.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""The Shikiru error hierarchy and its exit codes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
EXIT_GENERIC_ERROR = 1
|
|
12
|
+
EXIT_USAGE_ERROR = 2
|
|
13
|
+
EXIT_NOT_INITIALISED = 3
|
|
14
|
+
EXIT_COMMAND_REJECTED = 4
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ShikiruError(Exception):
|
|
18
|
+
"""Base class for all errors the CLI maps to a process exit code."""
|
|
19
|
+
|
|
20
|
+
exit_code: int = EXIT_GENERIC_ERROR
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class InvariantError(ShikiruError):
|
|
24
|
+
"""An invariant the engine relies on does not hold: a logic bug, not an environmental fault."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class UsageError(ShikiruError):
|
|
28
|
+
"""The operator invoked a command incorrectly."""
|
|
29
|
+
|
|
30
|
+
exit_code = EXIT_USAGE_ERROR
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class NotInitialisedError(ShikiruError):
|
|
34
|
+
"""No `.shikiru/` project was found where one was required."""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
message: str,
|
|
39
|
+
*,
|
|
40
|
+
project_root: Path | None = None,
|
|
41
|
+
missing_path: Path | None = None,
|
|
42
|
+
) -> None:
|
|
43
|
+
"""Record the missing project location when the CLI can identify it."""
|
|
44
|
+
super().__init__(message)
|
|
45
|
+
self.project_root = project_root
|
|
46
|
+
self.missing_path = missing_path
|
|
47
|
+
|
|
48
|
+
exit_code = EXIT_NOT_INITIALISED
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class CommandRejectedError(ShikiruError):
|
|
52
|
+
"""The engine rejected an operator command."""
|
|
53
|
+
|
|
54
|
+
exit_code = EXIT_COMMAND_REJECTED
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class StaleTaskBranchError(ShikiruError):
|
|
58
|
+
"""A task branch left by an earlier state already exists and is not at the task's base."""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ConfigError(ShikiruError):
|
|
62
|
+
"""Project or default configuration is missing or invalid."""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ProjectNotRepositoryRootError(ConfigError):
|
|
66
|
+
"""The selected project directory is outside or below a Git repository root."""
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
message: str,
|
|
71
|
+
*,
|
|
72
|
+
project_root: Path,
|
|
73
|
+
repository_root: Path | None,
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Record both the selected directory and an enclosing repository, when available."""
|
|
76
|
+
super().__init__(message)
|
|
77
|
+
self.project_root = project_root
|
|
78
|
+
self.repository_root = repository_root
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class RepositoryHasNoCommitsError(ConfigError):
|
|
82
|
+
"""The selected Git repository does not have a commit at HEAD."""
|
|
83
|
+
|
|
84
|
+
def __init__(self, message: str, *, project_root: Path) -> None:
|
|
85
|
+
"""Record the repository whose first commit is required."""
|
|
86
|
+
super().__init__(message)
|
|
87
|
+
self.project_root = project_root
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class HeadlessOutputError(ShikiruError):
|
|
91
|
+
"""An agent's headless response did not contain valid structured output."""
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class DatabaseError(ShikiruError):
|
|
95
|
+
"""The state database could not be opened, migrated or queried."""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class CommandFailed(ShikiruError):
|
|
99
|
+
"""A subprocess run through `process.SubprocessRunner` exited non-zero."""
|
|
100
|
+
|
|
101
|
+
def __init__(self, argv: Sequence[str], returncode: int, stderr: str) -> None:
|
|
102
|
+
"""Record the failed command's `argv`, exit code and captured stderr."""
|
|
103
|
+
argv_tuple = tuple(argv)
|
|
104
|
+
super().__init__(f"command {argv_tuple} exited {returncode}: {stderr}")
|
|
105
|
+
self.argv = argv_tuple
|
|
106
|
+
self.returncode = returncode
|
|
107
|
+
self.stderr = stderr
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class CommandTimeoutError(ShikiruError):
|
|
111
|
+
"""A subprocess run through `process.SubprocessRunner` was killed after its timeout elapsed."""
|
|
112
|
+
|
|
113
|
+
def __init__(self, argv: Sequence[str], timeout: float) -> None:
|
|
114
|
+
"""Record the timed-out command's `argv` and its `timeout` in seconds."""
|
|
115
|
+
argv_tuple = tuple(argv)
|
|
116
|
+
super().__init__(f"command {argv_tuple} timed out after {timeout}s")
|
|
117
|
+
self.argv = argv_tuple
|
|
118
|
+
self.timeout = timeout
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
PROCESS_FAILURES = (CommandFailed, CommandTimeoutError)
|
|
122
|
+
"""A subprocess call that exited non-zero or timed out."""
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class DetachedProcessError(ShikiruError):
|
|
126
|
+
"""A detached process could not be launched."""
|
|
127
|
+
|
|
128
|
+
def __init__(self, argv: Sequence[str], error: OSError) -> None:
|
|
129
|
+
"""Record the detached command and operating-system error."""
|
|
130
|
+
argv_tuple = tuple(argv)
|
|
131
|
+
super().__init__(f"could not launch detached command {argv_tuple}: {error}")
|
|
132
|
+
self.argv = argv_tuple
|
|
133
|
+
self.error = error
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class ProcessReplacementError(ShikiruError):
|
|
137
|
+
"""A process could not replace the current process."""
|
|
138
|
+
|
|
139
|
+
def __init__(self, argv: Sequence[str], error: OSError) -> None:
|
|
140
|
+
"""Record the replacement command and operating-system error."""
|
|
141
|
+
argv_tuple = tuple(argv)
|
|
142
|
+
super().__init__(f"could not replace process with {argv_tuple}: {error}")
|
|
143
|
+
self.argv = argv_tuple
|
|
144
|
+
self.error = error
|