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,230 @@
1
+ """Runs a step's turns and their recoveries until one completes or the task stops."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING
7
+
8
+ from shikiru.domain.recovery import Completed
9
+ from shikiru.domain.steps import ACTING_ROLE, Transition
10
+ from shikiru.engine.events import emit_nudge, emit_turn_started, emit_turn_stopped
11
+ from shikiru.engine.turn_context import ORDINARY_TURN, NextTurn
12
+ from shikiru.engine.turn_execution import TurnExecution
13
+ from shikiru.engine.turns import TurnHeld
14
+ from shikiru.errors import InvariantError
15
+
16
+ if TYPE_CHECKING:
17
+ from pathlib import Path
18
+
19
+ from shikiru.domain.feedback import FeedbackInput
20
+ from shikiru.domain.recovery import TurnCounters
21
+ from shikiru.domain.reports import Report, ReportKind
22
+ from shikiru.domain.tasks import Role, Step, Task
23
+ from shikiru.engine.escalation_filing import EscalationFiler
24
+ from shikiru.engine.recovery import TurnRecovery
25
+ from shikiru.engine.sessions import SessionManager
26
+ from shikiru.engine.turn_context import TurnContextPreparer
27
+ from shikiru.engine.turns import TurnRunner
28
+ from shikiru.prompts.renderer import PromptName
29
+ from shikiru.store.events import EventLog
30
+ from shikiru.store.runs import RunStore
31
+ from shikiru.store.tasks import TaskStore
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class TurnOverrides:
36
+ """What an integration turn changes about a step's ordinary turn."""
37
+
38
+ role: Role | None = None
39
+ template: PromptName | None = None
40
+ kind: ReportKind | None = None
41
+ conflict_context: str | None = None
42
+ feedback: FeedbackInput | None = None
43
+
44
+
45
+ NO_OVERRIDES = TurnOverrides()
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class TurnsCompleted:
50
+ """The loop ended with a turn that produced `report`."""
51
+
52
+ task: Task
53
+ counters: TurnCounters
54
+ execution: TurnExecution
55
+ report: Report
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class TurnsStopped:
60
+ """The loop ended because the task was held or escalated."""
61
+
62
+ task: Task
63
+ counters: TurnCounters
64
+
65
+
66
+ TurnLoopResult = TurnsCompleted | TurnsStopped
67
+
68
+
69
+ def _acting_role(step: Step, role_override: Role | None) -> Role:
70
+ return role_override if role_override is not None else ACTING_ROLE[step]
71
+
72
+
73
+ class TurnLoop:
74
+ """Runs a step's turn, recovering failed turns until one completes or the task stops."""
75
+
76
+ def __init__(
77
+ self,
78
+ *,
79
+ tasks: TaskStore,
80
+ events: EventLog,
81
+ sessions: SessionManager,
82
+ turns: TurnRunner,
83
+ contexts: TurnContextPreparer,
84
+ recovery: TurnRecovery,
85
+ escalations: EscalationFiler,
86
+ runs: RunStore,
87
+ ) -> None:
88
+ """Run turns through `turns`, recover through `recovery`, escalate through `escalations`."""
89
+ self._tasks = tasks
90
+ self._events = events
91
+ self._sessions = sessions
92
+ self._turns = turns
93
+ self._contexts = contexts
94
+ self._recovery = recovery
95
+ self._escalations = escalations
96
+ self._runs = runs
97
+ self._resumed: set[str] = set()
98
+
99
+ def expect_resume(self, task_id: str) -> None:
100
+ """Let `task_id`'s next turn consume a report its interrupted turn already submitted."""
101
+ self._resumed.add(task_id)
102
+
103
+ def forget_resume(self, task_id: str) -> None:
104
+ """Drop the pending resume of `task_id`."""
105
+ self._resumed.discard(task_id)
106
+
107
+ async def run(
108
+ self,
109
+ task: Task,
110
+ step: Step,
111
+ counters: TurnCounters,
112
+ overrides: TurnOverrides = NO_OVERRIDES,
113
+ ) -> TurnLoopResult:
114
+ """Run `step`'s turn and its recoveries until a report, a hold or an escalation."""
115
+ current = self._tasks.require(task.id)
116
+ if current.held:
117
+ return TurnsStopped(current, counters)
118
+ current_step = step
119
+ execution = await self._execute(current, current_step, overrides, ORDINARY_TURN)
120
+ while execution is not None:
121
+ result = execution.result
122
+ if isinstance(result, Completed):
123
+ return TurnsCompleted(current, counters, execution, result.report)
124
+ role = _acting_role(current_step, overrides.role)
125
+ recovery = await self._recovery.recover(
126
+ current, current_step, role, execution, counters
127
+ )
128
+ current, counters = recovery.task, recovery.counters
129
+ if recovery.escalation is not None:
130
+ transition = Transition.escalate(current, recovery.escalation, step=current_step)
131
+ escalated = await self._escalations.escalate(
132
+ current, transition, None, result=result, role=role
133
+ )
134
+ return TurnsStopped(escalated, counters)
135
+ if recovery.execution is not None:
136
+ execution = recovery.execution
137
+ continue
138
+ if current.step is None:
139
+ message = f"{current.id} recovery left the task with no step"
140
+ raise InvariantError(message)
141
+ current_step = current.step
142
+ current = self._tasks.require(current.id)
143
+ if current.held:
144
+ return TurnsStopped(current, counters)
145
+ execution = await self._execute(current, current_step, overrides, recovery.next_turn)
146
+ return TurnsStopped(self._tasks.require(current.id), counters)
147
+
148
+ async def _execute(
149
+ self, task: Task, step: Step, overrides: TurnOverrides, next_turn: NextTurn
150
+ ) -> TurnExecution | None:
151
+ role = _acting_role(step, overrides.role)
152
+ selection = self._sessions.session_for(task, role)
153
+ turn_request = await self._contexts.prepare(
154
+ task,
155
+ step,
156
+ session_handoff=selection.handoff,
157
+ next_turn=next_turn,
158
+ template_override=overrides.template,
159
+ kind_override=overrides.kind,
160
+ conflict_context=overrides.conflict_context,
161
+ feedback=overrides.feedback,
162
+ )
163
+ usage_before = selection.run.usage
164
+ resumed = task.id in self._resumed
165
+ self._resumed.discard(task.id)
166
+ completed = self._turns.completed_turn(task.id, role, turn_request) if resumed else None
167
+ if completed is not None:
168
+ result, turn_dir = completed
169
+ return TurnExecution(
170
+ result=result,
171
+ task_id=task.id,
172
+ role=role,
173
+ turn_dir=turn_dir,
174
+ rung=selection.rung,
175
+ run_id=selection.run.id,
176
+ usage_before=usage_before,
177
+ usage_after=usage_before,
178
+ kind=turn_request.kind,
179
+ )
180
+ turn_name: str | None = None
181
+
182
+ def on_start(turn_dir: Path) -> None:
183
+ nonlocal turn_name
184
+ turn_name = turn_dir.name
185
+ emit_turn_started(
186
+ self._events,
187
+ task,
188
+ step,
189
+ role,
190
+ selection.rung,
191
+ kind=turn_request.kind,
192
+ run_id=selection.run.id,
193
+ turn=turn_name,
194
+ )
195
+
196
+ def on_nudge() -> None:
197
+ emit_nudge(self._events, task, step, role, turn=turn_name)
198
+
199
+ try:
200
+ result, turn_dir, selection = await self._turns.run(
201
+ task, role, selection, turn_request, on_start=on_start, on_nudge=on_nudge
202
+ )
203
+ except BaseException as stop:
204
+ emit_turn_stopped(
205
+ self._events,
206
+ task,
207
+ step,
208
+ role,
209
+ rung=selection.rung,
210
+ run_id=selection.run.id,
211
+ turn=turn_name,
212
+ )
213
+ if isinstance(stop, TurnHeld):
214
+ return None
215
+ raise
216
+ live_run = self._runs.get_live(task.id, role)
217
+ if live_run is None:
218
+ message = f"{task.id} {role.value} run disappeared after its turn"
219
+ raise InvariantError(message)
220
+ return TurnExecution(
221
+ result=result,
222
+ task_id=task.id,
223
+ role=role,
224
+ turn_dir=turn_dir,
225
+ rung=selection.rung,
226
+ run_id=selection.run.id,
227
+ usage_before=usage_before,
228
+ usage_after=live_run.usage,
229
+ kind=turn_request.kind,
230
+ )
@@ -0,0 +1,79 @@
1
+ """The `turn.pid` record of each running turn, and ending the ones a dead engine left."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import signal
7
+ from dataclasses import asdict
8
+ from datetime import timedelta
9
+ from typing import TYPE_CHECKING
10
+
11
+ from shikiru.agents.contracts import role_dir
12
+ from shikiru.paths import RUNS_DIR_NAME, project_state_dir
13
+ from shikiru.private import make_private_dir, write_private_text
14
+ from shikiru.process import TURN_STOP_GRACE, ProcessIdentity
15
+
16
+ if TYPE_CHECKING:
17
+ from pathlib import Path
18
+
19
+ from shikiru.clock import Clock
20
+ from shikiru.domain.tasks import Role
21
+ from shikiru.process import SubprocessRunner
22
+
23
+ TURN_PID_NAME = "turn.pid"
24
+ EXIT_POLL_SECONDS = 0.1
25
+ _STAGED_SUFFIX = ".staged"
26
+
27
+
28
+ def turn_pid_path(run_dir: Path, role: Role) -> Path:
29
+ """Return the file recording the process that runs `role`'s current turn."""
30
+ return role_dir(run_dir, role.value) / TURN_PID_NAME
31
+
32
+
33
+ def read_turn_pid(path: Path) -> ProcessIdentity:
34
+ """Read the process identity recorded in a `turn.pid` file."""
35
+ return ProcessIdentity(**json.loads(path.read_text()))
36
+
37
+
38
+ class TurnProcesses:
39
+ """Record the process running each role's turn and stop orphans after an engine restart."""
40
+
41
+ def __init__(self, *, runner: SubprocessRunner, clock: Clock, project_root: Path) -> None:
42
+ """Identify and signal processes through `runner`, waiting on `clock`."""
43
+ self._runner = runner
44
+ self._clock = clock
45
+ self._project_root = project_root
46
+
47
+ def record(self, run_dir: Path, role: Role, pid: int) -> None:
48
+ """Write `role`'s `turn.pid` with the identity of `pid`, or of its group once it left."""
49
+ path = turn_pid_path(run_dir, role)
50
+ identity = self._runner.identify(pid)
51
+ if identity is None and self._runner.group_is_running(pid):
52
+ identity = ProcessIdentity(pid, None)
53
+ if identity is None:
54
+ path.unlink(missing_ok=True)
55
+ return
56
+ make_private_dir(path.parent)
57
+ staged = path.with_suffix(_STAGED_SUFFIX)
58
+ write_private_text(staged, json.dumps(asdict(identity)))
59
+ staged.replace(path)
60
+
61
+ @staticmethod
62
+ def clear(run_dir: Path, role: Role) -> None:
63
+ """Remove `role`'s `turn.pid` once its turn process has ended."""
64
+ turn_pid_path(run_dir, role).unlink(missing_ok=True)
65
+
66
+ async def end_orphans(self) -> None:
67
+ """Stop every recorded turn process that still runs, then drop its `turn.pid`."""
68
+ runs_root = project_state_dir(self._project_root) / RUNS_DIR_NAME
69
+ for path in sorted(runs_root.glob(f"*/*/{TURN_PID_NAME}")):
70
+ await self._stop(read_turn_pid(path))
71
+ path.unlink()
72
+
73
+ async def _stop(self, identity: ProcessIdentity) -> None:
74
+ if not self._runner.signal_group(identity, signal.SIGTERM):
75
+ return
76
+ deadline = self._clock.now() + timedelta(seconds=TURN_STOP_GRACE)
77
+ while self._runner.group_is_running(identity.pid) and self._clock.now() < deadline:
78
+ await self._clock.sleep(EXIT_POLL_SECONDS)
79
+ self._runner.signal_group(identity, signal.SIGKILL)