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,287 @@
|
|
|
1
|
+
"""`doctor --probe` turns: live contract check 1 and the smoke turn for other rungs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import shutil
|
|
8
|
+
from dataclasses import dataclass, replace
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from pydantic import ValidationError
|
|
12
|
+
|
|
13
|
+
from shikiru.agents.contracts import (
|
|
14
|
+
ROLE_ENV_VAR,
|
|
15
|
+
RUN_DIR_ENV_VAR,
|
|
16
|
+
AgentContext,
|
|
17
|
+
SessionStarted,
|
|
18
|
+
ToolStarted,
|
|
19
|
+
TurnEnded,
|
|
20
|
+
TurnFailed,
|
|
21
|
+
TurnSpec,
|
|
22
|
+
role_dir,
|
|
23
|
+
shikiru_command,
|
|
24
|
+
)
|
|
25
|
+
from shikiru.agents.sdk.turn import EMPTY_OUTPUT, stderr_log_path, stream_log_path
|
|
26
|
+
from shikiru.domain.reports import (
|
|
27
|
+
CURRENT_TURN_LINK_NAME,
|
|
28
|
+
REPORT_FILENAME,
|
|
29
|
+
REPORT_KIND_FILENAME,
|
|
30
|
+
REPORT_SCHEMA_FILENAME,
|
|
31
|
+
ReportKind,
|
|
32
|
+
model_for,
|
|
33
|
+
schema_for,
|
|
34
|
+
)
|
|
35
|
+
from shikiru.engine.turns import TURN_PROMPT
|
|
36
|
+
from shikiru.errors import ShikiruError
|
|
37
|
+
from shikiru.paths import TURNS_DIR_NAME, probe_repo_path, probe_run_dir
|
|
38
|
+
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
from typing import Any
|
|
43
|
+
|
|
44
|
+
from shikiru.agents.contracts import AgentSignal, Rung, Turn
|
|
45
|
+
from shikiru.agents.registry import AgentRegistry
|
|
46
|
+
from shikiru.clock import Clock
|
|
47
|
+
from shikiru.config.settings import DoctorConfig
|
|
48
|
+
from shikiru.process import SubprocessRunner
|
|
49
|
+
|
|
50
|
+
GIT_INIT_TIMEOUT_SECONDS = 10.0
|
|
51
|
+
GIT_INIT_ARGV = ("git", "init")
|
|
52
|
+
PROBE_ROLE = "implementer"
|
|
53
|
+
REPORT_INPUT_FILENAME = "contract-report-input.json"
|
|
54
|
+
REPORT_PROMPT_FILENAME = "contract-prompt.md"
|
|
55
|
+
REPORT_TOOL_NAME = "shikiru report"
|
|
56
|
+
REPORT_COMMAND = shikiru_command("report", "--file")
|
|
57
|
+
LIVE_REPORT_KIND = ReportKind.IMPLEMENTATION
|
|
58
|
+
LIVE_REPORT_DATA: dict[str, object] = {
|
|
59
|
+
"status": "completed",
|
|
60
|
+
"summary": "Isolated live report contract.",
|
|
61
|
+
"commits": [],
|
|
62
|
+
"tests": [],
|
|
63
|
+
"trackers_updated": False,
|
|
64
|
+
"risks": [],
|
|
65
|
+
}
|
|
66
|
+
LIVE_REPORT_PROMPT = (
|
|
67
|
+
"Use Bash to run exactly this report command once:\n"
|
|
68
|
+
f'{REPORT_COMMAND} "$SHIKIRU_RUN_DIR/{REPORT_INPUT_FILENAME}"\n'
|
|
69
|
+
"Do not inspect or edit other files or run any other commands. "
|
|
70
|
+
"After the command succeeds, reply exactly REPORT_OK."
|
|
71
|
+
)
|
|
72
|
+
SMOKE_PROMPT = "Hi"
|
|
73
|
+
LIVE_REPORT_PASSED = "valid report submitted"
|
|
74
|
+
SMOKE_PASSED = "turn ended"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True)
|
|
78
|
+
class ProbeOutcome:
|
|
79
|
+
"""One probe turn's verdict, with the raw stream tail and stderr kept for a failure."""
|
|
80
|
+
|
|
81
|
+
passed: str
|
|
82
|
+
problem: str | None
|
|
83
|
+
stream_tail: str = ""
|
|
84
|
+
stderr: str = ""
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def ok(self) -> bool:
|
|
88
|
+
return self.problem is None
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def detail(self) -> str:
|
|
92
|
+
if self.problem is None:
|
|
93
|
+
return self.passed
|
|
94
|
+
return (
|
|
95
|
+
f"{self.problem}\nstream tail:\n{self.stream_tail or EMPTY_OUTPUT}"
|
|
96
|
+
f"\nstderr:\n{self.stderr or EMPTY_OUTPUT}"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True)
|
|
101
|
+
class _ProbeCheck:
|
|
102
|
+
passed: str
|
|
103
|
+
prepare: Callable[[SubprocessRunner, TurnSpec], Awaitable[str]]
|
|
104
|
+
problem: Callable[[TurnSpec, Sequence[AgentSignal]], str | None]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class AgentProbe:
|
|
108
|
+
"""Run one probe turn for a rung in the project's reusable `.shikiru/probe/` scratch."""
|
|
109
|
+
|
|
110
|
+
def __init__(self, registry: AgentRegistry, runner: SubprocessRunner, clock: Clock) -> None:
|
|
111
|
+
"""Use the shared agent registry and runtime boundaries the engine uses."""
|
|
112
|
+
self._registry = registry
|
|
113
|
+
self._runner = runner
|
|
114
|
+
self._clock = clock
|
|
115
|
+
|
|
116
|
+
async def run(
|
|
117
|
+
self, project_root: Path, rung: Rung, options: Mapping[str, Any], config: DoctorConfig
|
|
118
|
+
) -> ProbeOutcome:
|
|
119
|
+
"""Start one real turn on `rung` that must submit a valid report through the CLI."""
|
|
120
|
+
return await self._probe(project_root, rung, options, config, _LIVE_REPORT_CHECK)
|
|
121
|
+
|
|
122
|
+
async def smoke(
|
|
123
|
+
self, project_root: Path, rung: Rung, options: Mapping[str, Any], config: DoctorConfig
|
|
124
|
+
) -> ProbeOutcome:
|
|
125
|
+
"""Start one real turn on `rung` with `SMOKE_PROMPT` that must end in `TurnEnded`."""
|
|
126
|
+
return await self._probe(project_root, rung, options, config, _SMOKE_CHECK)
|
|
127
|
+
|
|
128
|
+
async def _probe(
|
|
129
|
+
self,
|
|
130
|
+
project_root: Path,
|
|
131
|
+
rung: Rung,
|
|
132
|
+
options: Mapping[str, Any],
|
|
133
|
+
config: DoctorConfig,
|
|
134
|
+
check: _ProbeCheck,
|
|
135
|
+
) -> ProbeOutcome:
|
|
136
|
+
run_dir = probe_run_dir(project_root)
|
|
137
|
+
turn_dir = run_dir / TURNS_DIR_NAME / rung.agent
|
|
138
|
+
await asyncio.to_thread(shutil.rmtree, turn_dir, ignore_errors=True)
|
|
139
|
+
spec = TurnSpec(
|
|
140
|
+
rung=rung,
|
|
141
|
+
role=PROBE_ROLE,
|
|
142
|
+
prompt="",
|
|
143
|
+
cwd=probe_repo_path(project_root),
|
|
144
|
+
run_dir=run_dir,
|
|
145
|
+
turn_dir=turn_dir,
|
|
146
|
+
add_dirs=(),
|
|
147
|
+
env={RUN_DIR_ENV_VAR: str(run_dir), ROLE_ENV_VAR: PROBE_ROLE},
|
|
148
|
+
session_id=None,
|
|
149
|
+
usage=None,
|
|
150
|
+
)
|
|
151
|
+
try:
|
|
152
|
+
problem = await self._run_turn(spec, options, config.probe_timeout, check)
|
|
153
|
+
except (ShikiruError, OSError) as error:
|
|
154
|
+
problem = str(error) or type(error).__name__
|
|
155
|
+
if problem is None:
|
|
156
|
+
return ProbeOutcome(passed=check.passed, problem=None)
|
|
157
|
+
return await asyncio.to_thread(
|
|
158
|
+
_failed_outcome, spec, check.passed, problem, config.probe_stream_tail_lines
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
async def _run_turn(
|
|
162
|
+
self, spec: TurnSpec, options: Mapping[str, Any], timeout: float, check: _ProbeCheck
|
|
163
|
+
) -> str | None:
|
|
164
|
+
agent = self._registry.create(
|
|
165
|
+
spec.rung.agent, AgentContext(options=options, runner=self._runner, clock=self._clock)
|
|
166
|
+
)
|
|
167
|
+
prompt = await check.prepare(self._runner, spec)
|
|
168
|
+
turn = await agent.start_turn(replace(spec, prompt=prompt))
|
|
169
|
+
try:
|
|
170
|
+
signals = await _collect(turn, timeout)
|
|
171
|
+
except TimeoutError:
|
|
172
|
+
await turn.stop()
|
|
173
|
+
return f"live turn exceeded {timeout:g}s"
|
|
174
|
+
return await asyncio.to_thread(check.problem, spec, signals)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
async def _collect(turn: Turn, timeout: float) -> list[AgentSignal]:
|
|
178
|
+
async with asyncio.timeout(timeout):
|
|
179
|
+
return [signal async for signal in turn.signals()]
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _failed_outcome(spec: TurnSpec, passed: str, problem: str, tail_lines: int) -> ProbeOutcome:
|
|
183
|
+
stream_path = stream_log_path(spec.turn_dir, spec.role)
|
|
184
|
+
stderr_path = stderr_log_path(spec.turn_dir, spec.role)
|
|
185
|
+
stream_lines = stream_path.read_text().splitlines() if stream_path.is_file() else []
|
|
186
|
+
return ProbeOutcome(
|
|
187
|
+
passed=passed,
|
|
188
|
+
problem=problem,
|
|
189
|
+
stream_tail="\n".join(stream_lines[-tail_lines:]),
|
|
190
|
+
stderr=stderr_path.read_text().strip() if stderr_path.is_file() else "",
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
async def init_scratch_repo(runner: SubprocessRunner, path: Path) -> None:
|
|
195
|
+
"""Create `path` as a git repository, reusing one that already exists."""
|
|
196
|
+
await asyncio.to_thread(path.mkdir, parents=True, exist_ok=True)
|
|
197
|
+
await runner.run(GIT_INIT_ARGV, timeout=GIT_INIT_TIMEOUT_SECONDS, cwd=path, check=True)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
async def prepare_live_report(runner: SubprocessRunner, spec: TurnSpec) -> Path:
|
|
201
|
+
"""Lay out `spec`'s repository, run and turn dirs for the report check; return the prompt."""
|
|
202
|
+
await init_scratch_repo(runner, spec.cwd)
|
|
203
|
+
await asyncio.to_thread(_prepare_report_turn, spec)
|
|
204
|
+
await asyncio.to_thread(
|
|
205
|
+
(spec.run_dir / REPORT_INPUT_FILENAME).write_text, json.dumps(LIVE_REPORT_DATA)
|
|
206
|
+
)
|
|
207
|
+
prompt_path = spec.cwd / REPORT_PROMPT_FILENAME
|
|
208
|
+
await asyncio.to_thread(prompt_path.write_text, LIVE_REPORT_PROMPT)
|
|
209
|
+
return prompt_path
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
async def _prepare_live_report_prompt(runner: SubprocessRunner, spec: TurnSpec) -> str:
|
|
213
|
+
return TURN_PROMPT.format(prompt_path=await prepare_live_report(runner, spec))
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
async def _prepare_smoke(runner: SubprocessRunner, spec: TurnSpec) -> str:
|
|
217
|
+
await init_scratch_repo(runner, spec.cwd)
|
|
218
|
+
await asyncio.to_thread(_clear_current_turn, spec)
|
|
219
|
+
return SMOKE_PROMPT
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def live_report_problem(spec: TurnSpec, signals: Sequence[AgentSignal]) -> str | None:
|
|
223
|
+
"""Return why a finished report-check turn failed, or `None` when it passed."""
|
|
224
|
+
return (
|
|
225
|
+
_turn_problem(signals) or _report_command_problem(signals) or _report_problem(spec.turn_dir)
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _smoke_problem(spec: TurnSpec, signals: Sequence[AgentSignal]) -> str | None:
|
|
230
|
+
del spec
|
|
231
|
+
return _turn_problem(signals)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _turn_problem(signals: Sequence[AgentSignal]) -> str | None:
|
|
235
|
+
if not signals or not isinstance(signals[0], SessionStarted):
|
|
236
|
+
return "live turn omitted SessionStarted"
|
|
237
|
+
terminal = signals[-1]
|
|
238
|
+
if isinstance(terminal, TurnFailed):
|
|
239
|
+
return f"live turn failed ({terminal.kind.value}): {terminal.message}"
|
|
240
|
+
if not isinstance(terminal, TurnEnded):
|
|
241
|
+
return "live turn omitted TurnEnded"
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _report_command_problem(signals: Sequence[AgentSignal]) -> str | None:
|
|
246
|
+
if not any(_is_report_command(signal) for signal in signals):
|
|
247
|
+
return "live turn did not invoke shikiru report"
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _report_problem(turn_dir: Path) -> str | None:
|
|
252
|
+
report_path = turn_dir / REPORT_FILENAME
|
|
253
|
+
if not report_path.is_file():
|
|
254
|
+
return "live turn wrote no report"
|
|
255
|
+
kind = ReportKind((turn_dir / REPORT_KIND_FILENAME).read_text().strip())
|
|
256
|
+
try:
|
|
257
|
+
model_for(kind).model_validate_json(report_path.read_text())
|
|
258
|
+
except ValidationError as error:
|
|
259
|
+
return f"live turn wrote an invalid report: {error}"
|
|
260
|
+
return None
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _is_report_command(signal: AgentSignal) -> bool:
|
|
264
|
+
return isinstance(signal, ToolStarted) and (
|
|
265
|
+
signal.name == REPORT_TOOL_NAME
|
|
266
|
+
or (REPORT_COMMAND in signal.detail and REPORT_INPUT_FILENAME in signal.detail)
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _prepare_report_turn(spec: TurnSpec) -> None:
|
|
271
|
+
spec.turn_dir.mkdir(parents=True, exist_ok=True)
|
|
272
|
+
(spec.turn_dir / REPORT_KIND_FILENAME).write_text(LIVE_REPORT_KIND.value)
|
|
273
|
+
(spec.turn_dir / REPORT_SCHEMA_FILENAME).write_text(json.dumps(schema_for(LIVE_REPORT_KIND)))
|
|
274
|
+
_clear_current_turn(spec).symlink_to(spec.turn_dir)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _clear_current_turn(spec: TurnSpec) -> Path:
|
|
278
|
+
link = role_dir(spec.run_dir, spec.role) / CURRENT_TURN_LINK_NAME
|
|
279
|
+
link.parent.mkdir(parents=True, exist_ok=True)
|
|
280
|
+
link.unlink(missing_ok=True)
|
|
281
|
+
return link
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
_LIVE_REPORT_CHECK = _ProbeCheck(
|
|
285
|
+
passed=LIVE_REPORT_PASSED, prepare=_prepare_live_report_prompt, problem=live_report_problem
|
|
286
|
+
)
|
|
287
|
+
_SMOKE_CHECK = _ProbeCheck(passed=SMOKE_PASSED, prepare=_prepare_smoke, problem=_smoke_problem)
|