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
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"""Environment and configured agent diagnostics for the operator CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import uuid
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, ConfigDict
|
|
11
|
+
|
|
12
|
+
from shikiru.agents.contracts import (
|
|
13
|
+
DOCTOR_TIMEOUT_SECONDS,
|
|
14
|
+
CheckResult,
|
|
15
|
+
ModelCatalog,
|
|
16
|
+
assess_model_choice,
|
|
17
|
+
)
|
|
18
|
+
from shikiru.config.loader import load_settings
|
|
19
|
+
from shikiru.domain.versions import Version, parse_version
|
|
20
|
+
from shikiru.errors import ConfigError, ShikiruError
|
|
21
|
+
from shikiru.operator.detectors import not_repository_root
|
|
22
|
+
from shikiru.paths import engine_output_path, project_config_path, state_db_path
|
|
23
|
+
from shikiru.store.database import Database
|
|
24
|
+
from shikiru.tmux.client import Tmux, private_socket_name
|
|
25
|
+
from shikiru.tmux.session import DETACHED_SIZE
|
|
26
|
+
from shikiru.workspace.caches import (
|
|
27
|
+
CACHE_COW_UNSUPPORTED_WARNING,
|
|
28
|
+
CacheCloner,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from collections.abc import Sequence
|
|
33
|
+
|
|
34
|
+
from shikiru.agents.contracts import Rung
|
|
35
|
+
from shikiru.clock import Clock
|
|
36
|
+
from shikiru.config.settings import Settings
|
|
37
|
+
from shikiru.operator.plugin_checks import PluginChecks
|
|
38
|
+
from shikiru.operator.probe import AgentProbe
|
|
39
|
+
from shikiru.process import ProcessResult, SubprocessRunner
|
|
40
|
+
from shikiru.workspace.git import Git
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
SESSION_SUFFIX_LENGTH = 8
|
|
44
|
+
TMUX_VERSION_ARGS = ("tmux", "-V")
|
|
45
|
+
TMUX_MINIMUM_VERSION = (3, 5)
|
|
46
|
+
TMUX_SESSION_PREFIX = "shikiru-doctor-"
|
|
47
|
+
TMUX_SESSION_NAME = "probe"
|
|
48
|
+
_NO_TMUX_CONFIG = Path("/dev/null")
|
|
49
|
+
GIT_COMMAND = "git"
|
|
50
|
+
GIT_VERSION_ARGS = (GIT_COMMAND, "--version")
|
|
51
|
+
GIT_MINIMUM_VERSION = (2, 45)
|
|
52
|
+
GIT_CHECK_NAME = "git"
|
|
53
|
+
TMUX_CHECK_NAME = "tmux"
|
|
54
|
+
REPOSITORY_CHECK_NAME = "repository"
|
|
55
|
+
CONFIG_CHECK_NAME = "config"
|
|
56
|
+
PLUGINS_CHECK_NAME = "plugins"
|
|
57
|
+
PLUGIN_CONFIG_CHECK_NAME = "config.plugins"
|
|
58
|
+
PLUGIN_CHECK_PREFIX = "plugin"
|
|
59
|
+
PLUGIN_CHECK_SEPARATOR = ":"
|
|
60
|
+
MODEL_CHOICES_CHECK_NAME = "model_choices"
|
|
61
|
+
COW_CHECK_NAME = "cow"
|
|
62
|
+
DATABASE_CHECK_NAME = "db"
|
|
63
|
+
ENGINE_OUTPUT_CHECK_NAME = "engine-output"
|
|
64
|
+
PROBE_CHECK_PREFIX = "probe"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class DoctorCheck(BaseModel):
|
|
68
|
+
"""One named environment or project diagnostic."""
|
|
69
|
+
|
|
70
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
71
|
+
|
|
72
|
+
name: str
|
|
73
|
+
ok: bool
|
|
74
|
+
detail: str
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class DoctorReport(BaseModel):
|
|
78
|
+
"""The complete result of one `doctor` invocation."""
|
|
79
|
+
|
|
80
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
81
|
+
|
|
82
|
+
ok: bool
|
|
83
|
+
checks: list[DoctorCheck]
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def from_checks(cls, checks: Sequence[DoctorCheck]) -> DoctorReport:
|
|
87
|
+
"""Build a report whose status reflects every diagnostic."""
|
|
88
|
+
values = list(checks)
|
|
89
|
+
return cls(ok=all(check.ok for check in values), checks=values)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class Doctor:
|
|
93
|
+
"""Run diagnostics through the same boundaries used by the application."""
|
|
94
|
+
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
97
|
+
project_root: Path,
|
|
98
|
+
*,
|
|
99
|
+
cache_cloner: CacheCloner,
|
|
100
|
+
git: Git,
|
|
101
|
+
runner: SubprocessRunner,
|
|
102
|
+
clock: Clock,
|
|
103
|
+
plugin_checks: PluginChecks,
|
|
104
|
+
probe: AgentProbe,
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Store the project and injected git, process, clock, plugin and probe boundaries."""
|
|
107
|
+
self._project_root = project_root.resolve()
|
|
108
|
+
self._cache_cloner = cache_cloner
|
|
109
|
+
self._git = git
|
|
110
|
+
self._runner = runner
|
|
111
|
+
self._clock = clock
|
|
112
|
+
self._plugins = plugin_checks
|
|
113
|
+
self._probe = probe
|
|
114
|
+
|
|
115
|
+
async def run(self, overrides: Sequence[str] = (), *, probe: bool = False) -> DoctorReport:
|
|
116
|
+
"""Run core and plugin checks, plus one live turn per distinct ladder rung with `probe`."""
|
|
117
|
+
checks: list[DoctorCheck] = []
|
|
118
|
+
checks.append(await self._tmux_check())
|
|
119
|
+
checks.append(await self._git_check())
|
|
120
|
+
checks.append(await self._repository_check())
|
|
121
|
+
settings, config_check = self._settings_check(overrides)
|
|
122
|
+
checks.append(config_check)
|
|
123
|
+
checks.append(await self._database_check())
|
|
124
|
+
checks.extend(self._engine_output_check())
|
|
125
|
+
checks.append(await self._cow_check())
|
|
126
|
+
if settings is None:
|
|
127
|
+
checks.append(
|
|
128
|
+
DoctorCheck(name=PLUGIN_CONFIG_CHECK_NAME, ok=False, detail="config unavailable")
|
|
129
|
+
)
|
|
130
|
+
return DoctorReport.from_checks(checks)
|
|
131
|
+
plugin_checks = await self._plugin_checks(settings)
|
|
132
|
+
checks.extend(plugin_checks)
|
|
133
|
+
plugin_ok = bool(settings.agents) and all(check.ok for check in plugin_checks)
|
|
134
|
+
if plugin_ok:
|
|
135
|
+
plugins_detail = "all configured plugins passed"
|
|
136
|
+
elif not settings.agents:
|
|
137
|
+
plugins_detail = "no configured plugins"
|
|
138
|
+
else:
|
|
139
|
+
plugins_detail = "one or more plugins failed"
|
|
140
|
+
checks.append(DoctorCheck(name=PLUGINS_CHECK_NAME, ok=plugin_ok, detail=plugins_detail))
|
|
141
|
+
if probe:
|
|
142
|
+
checks.extend(await self._probe_checks(settings))
|
|
143
|
+
return DoctorReport.from_checks(checks)
|
|
144
|
+
|
|
145
|
+
async def _probe_checks(self, settings: Settings) -> list[DoctorCheck]:
|
|
146
|
+
checks: list[DoctorCheck] = []
|
|
147
|
+
probed_agents: set[str] = set()
|
|
148
|
+
for rung in dict.fromkeys(settings.ladder_rungs()):
|
|
149
|
+
probe = self._probe.smoke if rung.agent in probed_agents else self._probe.run
|
|
150
|
+
probed_agents.add(rung.agent)
|
|
151
|
+
outcome = await probe(
|
|
152
|
+
self._project_root, rung, settings.agents[rung.agent].options, settings.doctor
|
|
153
|
+
)
|
|
154
|
+
checks.append(
|
|
155
|
+
DoctorCheck(name=_probe_check_name(rung), ok=outcome.ok, detail=outcome.detail)
|
|
156
|
+
)
|
|
157
|
+
return checks
|
|
158
|
+
|
|
159
|
+
async def _tmux_check(self) -> DoctorCheck:
|
|
160
|
+
socket_name = private_socket_name(TMUX_SESSION_PREFIX)
|
|
161
|
+
tmux = Tmux(self._runner, socket=socket_name, config_file=_NO_TMUX_CONFIG)
|
|
162
|
+
try:
|
|
163
|
+
version = await self._runner.run(
|
|
164
|
+
TMUX_VERSION_ARGS,
|
|
165
|
+
timeout=DOCTOR_TIMEOUT_SECONDS,
|
|
166
|
+
check=False,
|
|
167
|
+
)
|
|
168
|
+
problem = _version_problem(version, TMUX_MINIMUM_VERSION)
|
|
169
|
+
if problem is None:
|
|
170
|
+
await tmux.new_session(
|
|
171
|
+
f"{TMUX_SESSION_NAME}-{uuid.uuid4().hex[:SESSION_SUFFIX_LENGTH]}",
|
|
172
|
+
cwd=self._project_root,
|
|
173
|
+
env={},
|
|
174
|
+
window_name=TMUX_SESSION_NAME,
|
|
175
|
+
size=DETACHED_SIZE,
|
|
176
|
+
)
|
|
177
|
+
check = DoctorCheck(
|
|
178
|
+
name=TMUX_CHECK_NAME, ok=problem is None, detail=problem or _process_detail(version)
|
|
179
|
+
)
|
|
180
|
+
except (OSError, ShikiruError) as error:
|
|
181
|
+
check = DoctorCheck(name=TMUX_CHECK_NAME, ok=False, detail=str(error))
|
|
182
|
+
try:
|
|
183
|
+
await tmux.kill_server()
|
|
184
|
+
except (OSError, ShikiruError) as error:
|
|
185
|
+
check = DoctorCheck(
|
|
186
|
+
name=TMUX_CHECK_NAME,
|
|
187
|
+
ok=False,
|
|
188
|
+
detail=f"{check.detail}; cleanup failed: {_error_detail(error)}",
|
|
189
|
+
)
|
|
190
|
+
return check
|
|
191
|
+
|
|
192
|
+
async def _git_check(self) -> DoctorCheck:
|
|
193
|
+
try:
|
|
194
|
+
result = await self._runner.run(
|
|
195
|
+
GIT_VERSION_ARGS,
|
|
196
|
+
timeout=DOCTOR_TIMEOUT_SECONDS,
|
|
197
|
+
check=False,
|
|
198
|
+
)
|
|
199
|
+
except (OSError, ShikiruError) as error:
|
|
200
|
+
return DoctorCheck(name=GIT_CHECK_NAME, ok=False, detail=str(error))
|
|
201
|
+
problem = _version_problem(result, GIT_MINIMUM_VERSION)
|
|
202
|
+
return DoctorCheck(
|
|
203
|
+
name=GIT_CHECK_NAME,
|
|
204
|
+
ok=problem is None,
|
|
205
|
+
detail=problem or _process_detail(result),
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
async def _repository_check(self) -> DoctorCheck:
|
|
209
|
+
try:
|
|
210
|
+
is_root = await self._git.is_repository_root(self._project_root)
|
|
211
|
+
except (OSError, ShikiruError) as error:
|
|
212
|
+
return DoctorCheck(name=REPOSITORY_CHECK_NAME, ok=False, detail=str(error))
|
|
213
|
+
detail = str(self._project_root) if is_root else not_repository_root(self._project_root)
|
|
214
|
+
return DoctorCheck(name=REPOSITORY_CHECK_NAME, ok=is_root, detail=detail)
|
|
215
|
+
|
|
216
|
+
def _settings_check(self, overrides: Sequence[str]) -> tuple[Settings | None, DoctorCheck]:
|
|
217
|
+
config_path = project_config_path(self._project_root)
|
|
218
|
+
if not config_path.is_file():
|
|
219
|
+
return None, DoctorCheck(name=CONFIG_CHECK_NAME, ok=False, detail="config missing")
|
|
220
|
+
try:
|
|
221
|
+
settings = load_settings(self._project_root, overrides)
|
|
222
|
+
except (ConfigError, OSError) as error:
|
|
223
|
+
return None, DoctorCheck(name=CONFIG_CHECK_NAME, ok=False, detail=str(error))
|
|
224
|
+
return settings, DoctorCheck(name=CONFIG_CHECK_NAME, ok=True, detail=str(config_path))
|
|
225
|
+
|
|
226
|
+
async def _database_check(self) -> DoctorCheck:
|
|
227
|
+
database = Database(state_db_path(self._project_root))
|
|
228
|
+
try:
|
|
229
|
+
version = await asyncio.to_thread(database.health)
|
|
230
|
+
except (OSError, ShikiruError) as error:
|
|
231
|
+
return DoctorCheck(name=DATABASE_CHECK_NAME, ok=False, detail=str(error))
|
|
232
|
+
return DoctorCheck(name=DATABASE_CHECK_NAME, ok=True, detail=f"schema {version}")
|
|
233
|
+
|
|
234
|
+
def _engine_output_check(self) -> list[DoctorCheck]:
|
|
235
|
+
path = engine_output_path(self._project_root)
|
|
236
|
+
if not path.is_file() or path.stat().st_size == 0:
|
|
237
|
+
return []
|
|
238
|
+
return [
|
|
239
|
+
DoctorCheck(
|
|
240
|
+
name=ENGINE_OUTPUT_CHECK_NAME,
|
|
241
|
+
ok=True,
|
|
242
|
+
detail=f"the last engine start printed output: {path}",
|
|
243
|
+
)
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
async def _cow_check(self) -> DoctorCheck:
|
|
247
|
+
try:
|
|
248
|
+
supported = await self._cache_cloner.check_support(self._project_root)
|
|
249
|
+
except (OSError, ShikiruError) as error:
|
|
250
|
+
return DoctorCheck(name=COW_CHECK_NAME, ok=False, detail=str(error))
|
|
251
|
+
detail = "copy-on-write supported" if supported else CACHE_COW_UNSUPPORTED_WARNING
|
|
252
|
+
return DoctorCheck(name=COW_CHECK_NAME, ok=True, detail=detail)
|
|
253
|
+
|
|
254
|
+
async def _plugin_checks(self, settings: Settings) -> list[DoctorCheck]:
|
|
255
|
+
checks: list[DoctorCheck] = []
|
|
256
|
+
rungs = tuple(dict.fromkeys(settings.ladder_rungs()))
|
|
257
|
+
for name, agent_config in sorted(settings.agents.items()):
|
|
258
|
+
configured_rungs = tuple(rung for rung in rungs if rung.agent == name)
|
|
259
|
+
result = await self._plugins.check(
|
|
260
|
+
name,
|
|
261
|
+
agent_config.options,
|
|
262
|
+
settings.ladder_models(name),
|
|
263
|
+
include_model_catalog=bool(configured_rungs),
|
|
264
|
+
)
|
|
265
|
+
if result.error is not None:
|
|
266
|
+
checks.append(
|
|
267
|
+
DoctorCheck(name=_plugin_check_name(name), ok=False, detail=result.detail)
|
|
268
|
+
)
|
|
269
|
+
continue
|
|
270
|
+
plugin_results = list(result.checks)
|
|
271
|
+
choice_check = None
|
|
272
|
+
if configured_rungs and result.catalog is not None:
|
|
273
|
+
choice_check = _model_choices_check(configured_rungs, result.catalog)
|
|
274
|
+
plugin_results.append(choice_check)
|
|
275
|
+
checks.extend(
|
|
276
|
+
DoctorCheck(
|
|
277
|
+
name=_plugin_result_name(name, plugin_result.name),
|
|
278
|
+
ok=plugin_result.ok,
|
|
279
|
+
detail=plugin_result.detail,
|
|
280
|
+
)
|
|
281
|
+
for plugin_result in plugin_results
|
|
282
|
+
)
|
|
283
|
+
plugin_ok = result.ok and (choice_check is None or choice_check.ok)
|
|
284
|
+
plugin_detail = result.detail
|
|
285
|
+
if choice_check is not None and not choice_check.ok:
|
|
286
|
+
plugin_detail = f"{plugin_detail}; {choice_check.name}: {choice_check.detail}"
|
|
287
|
+
checks.append(
|
|
288
|
+
DoctorCheck(name=_plugin_check_name(name), ok=plugin_ok, detail=plugin_detail)
|
|
289
|
+
)
|
|
290
|
+
return checks
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _process_detail(result: ProcessResult) -> str:
|
|
294
|
+
return result.stdout.strip() or result.stderr.strip() or f"exit {result.returncode}"
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _version_problem(result: ProcessResult, minimum: Version) -> str | None:
|
|
298
|
+
detail = _process_detail(result)
|
|
299
|
+
if result.returncode != 0:
|
|
300
|
+
return detail
|
|
301
|
+
found = parse_version(detail)
|
|
302
|
+
if found is None:
|
|
303
|
+
return f"{detail}: no version found"
|
|
304
|
+
if found < minimum:
|
|
305
|
+
return f"{detail}: {minimum[0]}.{minimum[1]} or newer is required"
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _error_detail(error: BaseException) -> str:
|
|
310
|
+
return str(error) or type(error).__name__
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _probe_check_name(rung: Rung) -> str:
|
|
314
|
+
efforts = () if rung.effort is None else (rung.effort,)
|
|
315
|
+
return ":".join((PROBE_CHECK_PREFIX, rung.agent, rung.model, *efforts))
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _plugin_check_name(name: str) -> str:
|
|
319
|
+
return PLUGIN_CHECK_SEPARATOR.join((PLUGIN_CHECK_PREFIX, name))
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _plugin_result_name(name: str, result: str) -> str:
|
|
323
|
+
return PLUGIN_CHECK_SEPARATOR.join((PLUGIN_CHECK_PREFIX, name, result))
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _model_choices_check(rungs: Sequence[Rung], catalog: ModelCatalog) -> CheckResult:
|
|
327
|
+
choices = tuple(dict.fromkeys((rung.model, rung.effort) for rung in rungs))
|
|
328
|
+
assessments = tuple(assess_model_choice(model, effort, catalog) for model, effort in choices)
|
|
329
|
+
invalid = tuple(
|
|
330
|
+
assessment.detail for assessment in assessments if assessment.supported is False
|
|
331
|
+
)
|
|
332
|
+
unverified = tuple(
|
|
333
|
+
assessment.detail for assessment in assessments if assessment.supported is None
|
|
334
|
+
)
|
|
335
|
+
if invalid:
|
|
336
|
+
return CheckResult(
|
|
337
|
+
name=MODEL_CHOICES_CHECK_NAME,
|
|
338
|
+
ok=False,
|
|
339
|
+
detail="; ".join((*invalid, *unverified)),
|
|
340
|
+
)
|
|
341
|
+
if unverified:
|
|
342
|
+
return CheckResult(
|
|
343
|
+
name=MODEL_CHOICES_CHECK_NAME,
|
|
344
|
+
ok=True,
|
|
345
|
+
detail=f"model and effort choices could not be fully verified: {'; '.join(unverified)}",
|
|
346
|
+
)
|
|
347
|
+
return CheckResult(
|
|
348
|
+
name=MODEL_CHOICES_CHECK_NAME,
|
|
349
|
+
ok=True,
|
|
350
|
+
detail="configured model and effort choices are listed in the agent catalog",
|
|
351
|
+
)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Event reads and bounded long-polling for operators."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import timedelta
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
|
|
11
|
+
from shikiru.clock import Clock
|
|
12
|
+
from shikiru.domain.events import Event, EventKind
|
|
13
|
+
from shikiru.store.reader import StateReader
|
|
14
|
+
|
|
15
|
+
EVENT_POLL_SECONDS = 0.5
|
|
16
|
+
MAX_WAIT_SECONDS = 300.0
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class EventFeed:
|
|
20
|
+
"""Read and wait for state events through a read-only `StateReader`."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, reader: StateReader, clock: Clock) -> None:
|
|
23
|
+
"""Use `reader` for event rows and `clock` for deterministic long-poll delays."""
|
|
24
|
+
self._reader = reader
|
|
25
|
+
self._clock = clock
|
|
26
|
+
|
|
27
|
+
def read(
|
|
28
|
+
self,
|
|
29
|
+
since: int,
|
|
30
|
+
*,
|
|
31
|
+
kinds: Sequence[EventKind] | None = None,
|
|
32
|
+
task: str | None = None,
|
|
33
|
+
) -> list[Event]:
|
|
34
|
+
"""Return events after `since`, with optional kind and task filters."""
|
|
35
|
+
return self._reader.events_since(since, kinds=kinds, task_id=task)
|
|
36
|
+
|
|
37
|
+
async def wait(
|
|
38
|
+
self,
|
|
39
|
+
since: int,
|
|
40
|
+
*,
|
|
41
|
+
kinds: Sequence[EventKind] | None = None,
|
|
42
|
+
task: str | None = None,
|
|
43
|
+
timeout: float = MAX_WAIT_SECONDS,
|
|
44
|
+
) -> list[Event]:
|
|
45
|
+
"""Wait until matching events arrive or `timeout` seconds elapse."""
|
|
46
|
+
if timeout < 0 or timeout > MAX_WAIT_SECONDS:
|
|
47
|
+
message = f"timeout must be between 0 and {MAX_WAIT_SECONDS:g} seconds"
|
|
48
|
+
raise ValueError(message)
|
|
49
|
+
deadline = self._clock.now() + timedelta(seconds=timeout)
|
|
50
|
+
while True:
|
|
51
|
+
events = self.read(since, kinds=kinds, task=task)
|
|
52
|
+
if events:
|
|
53
|
+
return events
|
|
54
|
+
if self._clock.now() >= deadline:
|
|
55
|
+
return []
|
|
56
|
+
remaining = (deadline - self._clock.now()).total_seconds()
|
|
57
|
+
await self._clock.sleep(min(EVENT_POLL_SECONDS, remaining))
|