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.
Files changed (319) hide show
  1. shikiru/__init__.py +1 -0
  2. shikiru/__main__.py +8 -0
  3. shikiru/agents/__init__.py +3 -0
  4. shikiru/agents/contracts.py +590 -0
  5. shikiru/agents/registry.py +87 -0
  6. shikiru/agents/sdk/__init__.py +17 -0
  7. shikiru/agents/sdk/reset_time.py +137 -0
  8. shikiru/agents/sdk/turn.py +335 -0
  9. shikiru/agents/settings.py +151 -0
  10. shikiru/agents/testing/__init__.py +26 -0
  11. shikiru/agents/testing/contract_suite.py +653 -0
  12. shikiru/agents/testing/resume.py +15 -0
  13. shikiru/bootstrap.py +1277 -0
  14. shikiru/cli/__init__.py +1 -0
  15. shikiru/cli/app.py +770 -0
  16. shikiru/cli/context.py +63 -0
  17. shikiru/cli/render.py +1475 -0
  18. shikiru/cli/report.py +156 -0
  19. shikiru/cli/session_prompt.py +63 -0
  20. shikiru/cli/view.py +131 -0
  21. shikiru/clock.py +41 -0
  22. shikiru/config/__init__.py +8 -0
  23. shikiru/config/defaults.toml +131 -0
  24. shikiru/config/loader.py +178 -0
  25. shikiru/config/settings.py +392 -0
  26. shikiru/dashboard/__init__.py +3 -0
  27. shikiru/dashboard/actions_list.py +799 -0
  28. shikiru/dashboard/activity.py +150 -0
  29. shikiru/dashboard/app.py +1765 -0
  30. shikiru/dashboard/blocker_evidence.py +122 -0
  31. shikiru/dashboard/blocker_rows.py +84 -0
  32. shikiru/dashboard/briefing_view.py +871 -0
  33. shikiru/dashboard/contract.py +1086 -0
  34. shikiru/dashboard/covered.py +32 -0
  35. shikiru/dashboard/detail.py +1209 -0
  36. shikiru/dashboard/detail_fold.py +244 -0
  37. shikiru/dashboard/detail_hint.py +68 -0
  38. shikiru/dashboard/details_list_view.py +150 -0
  39. shikiru/dashboard/diagnostics.py +80 -0
  40. shikiru/dashboard/diagnostics_sections.py +186 -0
  41. shikiru/dashboard/evidence_table.py +123 -0
  42. shikiru/dashboard/extra_details.py +214 -0
  43. shikiru/dashboard/fault_guidance.py +216 -0
  44. shikiru/dashboard/fold_bodies.py +276 -0
  45. shikiru/dashboard/help_keys.py +166 -0
  46. shikiru/dashboard/help_screen.py +204 -0
  47. shikiru/dashboard/interaction.py +17 -0
  48. shikiru/dashboard/ladder_editor.py +544 -0
  49. shikiru/dashboard/live_settings.py +62 -0
  50. shikiru/dashboard/motion.py +368 -0
  51. shikiru/dashboard/orchestration_fields.py +201 -0
  52. shikiru/dashboard/palette.py +1606 -0
  53. shikiru/dashboard/palette_app.py +89 -0
  54. shikiru/dashboard/palette_drafts.py +33 -0
  55. shikiru/dashboard/resolution.py +573 -0
  56. shikiru/dashboard/resolution_view.py +302 -0
  57. shikiru/dashboard/row_list.py +256 -0
  58. shikiru/dashboard/section_rows.py +173 -0
  59. shikiru/dashboard/services.py +665 -0
  60. shikiru/dashboard/session_history.py +402 -0
  61. shikiru/dashboard/shikiru_init_ascii_art.txt +40 -0
  62. shikiru/dashboard/task_picker.py +358 -0
  63. shikiru/dashboard/tick.py +59 -0
  64. shikiru/dashboard/timeline_filter.py +129 -0
  65. shikiru/dashboard/timeline_section.py +385 -0
  66. shikiru/dashboard/usage_body.py +269 -0
  67. shikiru/dashboard/views.py +2314 -0
  68. shikiru/dashboard/wizard.py +1389 -0
  69. shikiru/domain/__init__.py +1 -0
  70. shikiru/domain/acceptance.py +130 -0
  71. shikiru/domain/activity.py +28 -0
  72. shikiru/domain/attempts.py +333 -0
  73. shikiru/domain/briefing.py +257 -0
  74. shikiru/domain/commands.py +219 -0
  75. shikiru/domain/decisions.py +231 -0
  76. shikiru/domain/docker.py +262 -0
  77. shikiru/domain/engine_uptime.py +87 -0
  78. shikiru/domain/escalation.py +194 -0
  79. shikiru/domain/escalation_evidence.py +186 -0
  80. shikiru/domain/escalation_panel.py +144 -0
  81. shikiru/domain/events.py +71 -0
  82. shikiru/domain/evidence_export.py +89 -0
  83. shikiru/domain/faults.py +86 -0
  84. shikiru/domain/feedback.py +180 -0
  85. shikiru/domain/feedback_rounds.py +172 -0
  86. shikiru/domain/fencing.py +31 -0
  87. shikiru/domain/ladder.py +102 -0
  88. shikiru/domain/ladder_text.py +44 -0
  89. shikiru/domain/levels.py +44 -0
  90. shikiru/domain/motion.py +199 -0
  91. shikiru/domain/packet_evidence.py +377 -0
  92. shikiru/domain/pushes.py +47 -0
  93. shikiru/domain/quota_blocks.py +44 -0
  94. shikiru/domain/quota_waits.py +219 -0
  95. shikiru/domain/recovery.py +194 -0
  96. shikiru/domain/reports.py +498 -0
  97. shikiru/domain/results.py +217 -0
  98. shikiru/domain/runs.py +85 -0
  99. shikiru/domain/scheduling.py +231 -0
  100. shikiru/domain/status_tasks.py +40 -0
  101. shikiru/domain/steps.py +325 -0
  102. shikiru/domain/tasks.py +174 -0
  103. shikiru/domain/timeline.py +208 -0
  104. shikiru/domain/timeline_phrases.py +271 -0
  105. shikiru/domain/versions.py +18 -0
  106. shikiru/domain/workflow.py +372 -0
  107. shikiru/engine/__init__.py +0 -0
  108. shikiru/engine/acceptance.py +90 -0
  109. shikiru/engine/agents.py +61 -0
  110. shikiru/engine/cosmetic_decorator.py +99 -0
  111. shikiru/engine/daemon.py +409 -0
  112. shikiru/engine/dependency.py +37 -0
  113. shikiru/engine/escalation_filing.py +110 -0
  114. shikiru/engine/escalations.py +401 -0
  115. shikiru/engine/events.py +285 -0
  116. shikiru/engine/evidence.py +81 -0
  117. shikiru/engine/fault_reporter.py +144 -0
  118. shikiru/engine/feedback.py +577 -0
  119. shikiru/engine/feedback_audit.py +80 -0
  120. shikiru/engine/feedback_intake.py +95 -0
  121. shikiru/engine/feedback_snapshot.py +148 -0
  122. shikiru/engine/flow.py +87 -0
  123. shikiru/engine/handlers/__init__.py +33 -0
  124. shikiru/engine/handlers/base.py +148 -0
  125. shikiru/engine/handlers/engine.py +24 -0
  126. shikiru/engine/handlers/escalations.py +174 -0
  127. shikiru/engine/handlers/operator.py +81 -0
  128. shikiru/engine/handlers/tasks.py +237 -0
  129. shikiru/engine/history.py +128 -0
  130. shikiru/engine/integration.py +1234 -0
  131. shikiru/engine/integration_turns.py +337 -0
  132. shikiru/engine/layout_guard.py +362 -0
  133. shikiru/engine/lifecycle.py +60 -0
  134. shikiru/engine/monitor.py +317 -0
  135. shikiru/engine/notification_delivery.py +124 -0
  136. shikiru/engine/notification_feed.py +57 -0
  137. shikiru/engine/notifier.py +91 -0
  138. shikiru/engine/pull_request_closing.py +117 -0
  139. shikiru/engine/push_service.py +177 -0
  140. shikiru/engine/quota_probe_threads.py +79 -0
  141. shikiru/engine/quota_reconciler.py +210 -0
  142. shikiru/engine/quota_schedule.py +24 -0
  143. shikiru/engine/quota_waits.py +102 -0
  144. shikiru/engine/reconcile.py +91 -0
  145. shikiru/engine/recovery.py +284 -0
  146. shikiru/engine/retention.py +102 -0
  147. shikiru/engine/root_preparation.py +86 -0
  148. shikiru/engine/scheduler.py +320 -0
  149. shikiru/engine/session_guard.py +46 -0
  150. shikiru/engine/sessions.py +95 -0
  151. shikiru/engine/signalled.py +75 -0
  152. shikiru/engine/step_runner.py +140 -0
  153. shikiru/engine/supervisor.py +101 -0
  154. shikiru/engine/takeover.py +223 -0
  155. shikiru/engine/task_facts.py +46 -0
  156. shikiru/engine/tick_guard.py +95 -0
  157. shikiru/engine/titles.py +297 -0
  158. shikiru/engine/transitions.py +79 -0
  159. shikiru/engine/turn_context.py +206 -0
  160. shikiru/engine/turn_execution.py +30 -0
  161. shikiru/engine/turn_loop.py +230 -0
  162. shikiru/engine/turn_processes.py +79 -0
  163. shikiru/engine/turns.py +457 -0
  164. shikiru/errors.py +144 -0
  165. shikiru/helper/__init__.py +3 -0
  166. shikiru/helper/helper.py +182 -0
  167. shikiru/helper/models.py +11 -0
  168. shikiru/mcp/__init__.py +1 -0
  169. shikiru/mcp/server.py +196 -0
  170. shikiru/operator/__init__.py +33 -0
  171. shikiru/operator/attachment.py +24 -0
  172. shikiru/operator/commands.py +319 -0
  173. shikiru/operator/detectors.py +374 -0
  174. shikiru/operator/doctor.py +351 -0
  175. shikiru/operator/events.py +57 -0
  176. shikiru/operator/evidence_export.py +498 -0
  177. shikiru/operator/evidence_writer.py +78 -0
  178. shikiru/operator/lifecycle.py +294 -0
  179. shikiru/operator/plugin_checks.py +194 -0
  180. shikiru/operator/probe.py +287 -0
  181. shikiru/operator/queries.py +1601 -0
  182. shikiru/operator/services.py +36 -0
  183. shikiru/operator/sessions.py +378 -0
  184. shikiru/operator/strip_views.py +162 -0
  185. shikiru/operator/timeline.py +416 -0
  186. shikiru/operator/toml_text.py +62 -0
  187. shikiru/palette.py +18 -0
  188. shikiru/paths.py +91 -0
  189. shikiru/plugins/__init__.py +0 -0
  190. shikiru/plugins/agents/__init__.py +1 -0
  191. shikiru/plugins/agents/claude/__init__.py +5 -0
  192. shikiru/plugins/agents/claude/agent.py +251 -0
  193. shikiru/plugins/agents/claude/context.py +116 -0
  194. shikiru/plugins/agents/claude/headless.py +92 -0
  195. shikiru/plugins/agents/claude/hook.py +57 -0
  196. shikiru/plugins/agents/claude/model_catalog.py +105 -0
  197. shikiru/plugins/agents/claude/options.py +62 -0
  198. shikiru/plugins/agents/claude/turn.py +1018 -0
  199. shikiru/plugins/agents/codex/__init__.py +5 -0
  200. shikiru/plugins/agents/codex/agent.py +486 -0
  201. shikiru/plugins/agents/codex/headless.py +166 -0
  202. shikiru/plugins/agents/codex/home.py +14 -0
  203. shikiru/plugins/agents/codex/model_catalog.py +128 -0
  204. shikiru/plugins/agents/codex/options.py +94 -0
  205. shikiru/plugins/agents/codex/quota.py +195 -0
  206. shikiru/plugins/agents/codex/sandbox.py +31 -0
  207. shikiru/plugins/agents/codex/sandbox_probe.py +97 -0
  208. shikiru/plugins/agents/codex/turn.py +767 -0
  209. shikiru/plugins/agents/codex/usage.py +106 -0
  210. shikiru/plugins/agents/shared_settings.py +62 -0
  211. shikiru/plugins/widgets/__init__.py +3 -0
  212. shikiru/plugins/widgets/dialogue.py +144 -0
  213. shikiru/plugins/widgets/disk.py +123 -0
  214. shikiru/plugins/widgets/docker.py +225 -0
  215. shikiru/plugins/widgets/escalations.py +402 -0
  216. shikiru/plugins/widgets/faults.py +204 -0
  217. shikiru/plugins/widgets/header.py +321 -0
  218. shikiru/plugins/widgets/queue.py +1140 -0
  219. shikiru/plugins/widgets/queue_hint.py +93 -0
  220. shikiru/plugins/widgets/resources.py +105 -0
  221. shikiru/plugins/widgets/strips.py +82 -0
  222. shikiru/plugins/widgets/usage.py +176 -0
  223. shikiru/private.py +94 -0
  224. shikiru/process.py +535 -0
  225. shikiru/prompts/__init__.py +3 -0
  226. shikiru/prompts/renderer.py +85 -0
  227. shikiru/prompts/templates/_base.md.j2 +17 -0
  228. shikiru/prompts/templates/_macros.md.j2 +61 -0
  229. shikiru/prompts/templates/conflicts.md.j2 +22 -0
  230. shikiru/prompts/templates/consult.md.j2 +5 -0
  231. shikiru/prompts/templates/continue.md.j2 +4 -0
  232. shikiru/prompts/templates/feedback_analysis.md.j2 +29 -0
  233. shikiru/prompts/templates/feedback_rework.md.j2 +33 -0
  234. shikiru/prompts/templates/feedback_verify.md.j2 +33 -0
  235. shikiru/prompts/templates/handoff.md.j2 +23 -0
  236. shikiru/prompts/templates/implement.md.j2 +13 -0
  237. shikiru/prompts/templates/plan.md.j2 +30 -0
  238. shikiru/prompts/templates/rework.md.j2 +13 -0
  239. shikiru/prompts/templates/triage.md.j2 +20 -0
  240. shikiru/prompts/templates/verify.md.j2 +29 -0
  241. shikiru/store/__init__.py +1 -0
  242. shikiru/store/commands.py +242 -0
  243. shikiru/store/database.py +297 -0
  244. shikiru/store/escalations.py +240 -0
  245. shikiru/store/events.py +213 -0
  246. shikiru/store/feedback.py +266 -0
  247. shikiru/store/migrations/001_initial.sql +63 -0
  248. shikiru/store/migrations/002_headless_turns.sql +31 -0
  249. shikiru/store/migrations/003_drop_command_handled_at.sql +1 -0
  250. shikiru/store/migrations/004_repo_base_ref.sql +3 -0
  251. shikiru/store/migrations/005_complexity_explicit.sql +3 -0
  252. shikiru/store/migrations/006_events_kind_index.sql +1 -0
  253. shikiru/store/migrations/007_notification_cursor.sql +1 -0
  254. shikiru/store/migrations/008_reported_usage.sql +1 -0
  255. shikiru/store/migrations/009_billing_declarations.sql +2 -0
  256. shikiru/store/migrations/010_canonical_quota_windows.sql +3 -0
  257. shikiru/store/migrations/012_drop_task_quota_evidence.sql +2 -0
  258. shikiru/store/migrations/013_push_targets.sql +7 -0
  259. shikiru/store/migrations/014_agent_block_time.sql +6 -0
  260. shikiru/store/migrations/015_repair_orphan_agent_blocks.sql +3 -0
  261. shikiru/store/migrations/016_feedback_rounds.sql +29 -0
  262. shikiru/store/migrations/017_feedback_round_comments.sql +5 -0
  263. shikiru/store/migrations/__init__.py +1 -0
  264. shikiru/store/notification_cursor.py +29 -0
  265. shikiru/store/pushes.py +117 -0
  266. shikiru/store/quotas.py +153 -0
  267. shikiru/store/reader.py +310 -0
  268. shikiru/store/retention.py +92 -0
  269. shikiru/store/runs.py +227 -0
  270. shikiru/store/tasks.py +949 -0
  271. shikiru/telemetry/__init__.py +1 -0
  272. shikiru/telemetry/disk.py +167 -0
  273. shikiru/telemetry/docker.py +164 -0
  274. shikiru/telemetry/resources.py +296 -0
  275. shikiru/tmux/__init__.py +3 -0
  276. shikiru/tmux/client.py +637 -0
  277. shikiru/tmux/decorator.py +225 -0
  278. shikiru/tmux/layout.py +200 -0
  279. shikiru/tmux/session.py +381 -0
  280. shikiru/ui/__init__.py +9 -0
  281. shikiru/ui/age.py +45 -0
  282. shikiru/ui/bubbles.py +41 -0
  283. shikiru/ui/compact.py +1209 -0
  284. shikiru/ui/console.py +49 -0
  285. shikiru/ui/fold.py +50 -0
  286. shikiru/ui/gated_static.py +117 -0
  287. shikiru/ui/gauge.py +40 -0
  288. shikiru/ui/glyphs.py +109 -0
  289. shikiru/ui/keys.py +57 -0
  290. shikiru/ui/live_interval.py +42 -0
  291. shikiru/ui/popup.py +311 -0
  292. shikiru/ui/quota_status.py +26 -0
  293. shikiru/ui/recent_window.py +78 -0
  294. shikiru/ui/screen.py +57 -0
  295. shikiru/ui/section.py +193 -0
  296. shikiru/ui/session.py +65 -0
  297. shikiru/ui/size.py +17 -0
  298. shikiru/ui/splash.py +42 -0
  299. shikiru/ui/status.py +47 -0
  300. shikiru/ui/status_strip.py +149 -0
  301. shikiru/ui/stream.py +152 -0
  302. shikiru/ui/strip.py +332 -0
  303. shikiru/ui/theme.py +83 -0
  304. shikiru/ui/tokens.py +51 -0
  305. shikiru/view_log.py +126 -0
  306. shikiru/workspace/__init__.py +3 -0
  307. shikiru/workspace/base_refs.py +160 -0
  308. shikiru/workspace/caches.py +136 -0
  309. shikiru/workspace/facts.py +118 -0
  310. shikiru/workspace/git.py +601 -0
  311. shikiru/workspace/github.py +744 -0
  312. shikiru/workspace/messages.py +107 -0
  313. shikiru/workspace/reports.py +145 -0
  314. shikiru/workspace/roots.py +180 -0
  315. shikiru_cli-0.2.0.dist-info/METADATA +428 -0
  316. shikiru_cli-0.2.0.dist-info/RECORD +319 -0
  317. shikiru_cli-0.2.0.dist-info/WHEEL +4 -0
  318. shikiru_cli-0.2.0.dist-info/entry_points.txt +18 -0
  319. 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)