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
shikiru/bootstrap.py ADDED
@@ -0,0 +1,1277 @@
1
+ """The composition root. Assembles concrete components for each entry point."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ import os
8
+ from importlib import import_module
9
+ from typing import TYPE_CHECKING, Protocol, cast
10
+
11
+ from shikiru.agents.registry import AgentRegistry
12
+ from shikiru.cli.session_prompt import prompt_for_session
13
+ from shikiru.clock import Clock, SystemClock
14
+ from shikiru.config.loader import load_default_orchestration, load_session_settings, load_settings
15
+ from shikiru.config.settings import (
16
+ DOCKER_SCOPE_ALL,
17
+ MERGE_INTEGRATION,
18
+ PULL_REQUEST_INTEGRATION,
19
+ )
20
+ from shikiru.engine.acceptance import AcceptanceGuard
21
+ from shikiru.engine.agents import AgentPool
22
+ from shikiru.engine.cosmetic_decorator import CosmeticDecorator
23
+ from shikiru.engine.daemon import (
24
+ ENGINE_LOGGER_NAME,
25
+ Engine,
26
+ EngineLock,
27
+ engine_is_running,
28
+ )
29
+ from shikiru.engine.dependency import DependencyEscalator
30
+ from shikiru.engine.escalation_filing import EscalationFiler
31
+ from shikiru.engine.escalations import DispatchFailureEscalator, EscalationRaiser
32
+ from shikiru.engine.evidence import EvidenceCollector
33
+ from shikiru.engine.fault_reporter import FaultReporter
34
+ from shikiru.engine.feedback import FeedbackRounds
35
+ from shikiru.engine.feedback_audit import FeedbackWorkAudit
36
+ from shikiru.engine.feedback_intake import FeedbackIntake
37
+ from shikiru.engine.flow import TaskFlow
38
+ from shikiru.engine.handlers import (
39
+ CancelHandler,
40
+ HandBackHandler,
41
+ HandlerContext,
42
+ HandlerRegistry,
43
+ PauseHandler,
44
+ PrioritizeHandler,
45
+ ResolveHandler,
46
+ ResumeHandler,
47
+ RetryHandler,
48
+ SayHandler,
49
+ SetRungHandler,
50
+ StopHandler,
51
+ SubmitHandler,
52
+ TakeOverHandler,
53
+ )
54
+ from shikiru.engine.history import ReportHistory
55
+ from shikiru.engine.integration import (
56
+ IntegrationStrategy,
57
+ Integrator,
58
+ LocalMerge,
59
+ PullRequest,
60
+ PullRequestCloser,
61
+ SquashMerge,
62
+ )
63
+ from shikiru.engine.integration_turns import IntegrationTurns
64
+ from shikiru.engine.layout_guard import LayoutGuard
65
+ from shikiru.engine.lifecycle import TaskFinisher
66
+ from shikiru.engine.monitor import TurnMonitor
67
+ from shikiru.engine.notification_delivery import NotificationDelivery
68
+ from shikiru.engine.notification_feed import NotificationFeed
69
+ from shikiru.engine.notifier import Notifier
70
+ from shikiru.engine.pull_request_closing import PullRequestClosing
71
+ from shikiru.engine.push_service import PUSH_FAULT_SCOPE, REMOTE_CHECK_FAULT_SCOPE, PushService
72
+ from shikiru.engine.quota_reconciler import QuotaReconciler
73
+ from shikiru.engine.quota_schedule import QuotaSchedule
74
+ from shikiru.engine.quota_waits import QuotaWaits
75
+ from shikiru.engine.reconcile import Reconciler
76
+ from shikiru.engine.recovery import TurnRecovery
77
+ from shikiru.engine.retention import Retention
78
+ from shikiru.engine.root_preparation import RootPreparation
79
+ from shikiru.engine.scheduler import Scheduler
80
+ from shikiru.engine.session_guard import SessionGuard
81
+ from shikiru.engine.sessions import SessionManager
82
+ from shikiru.engine.signalled import SignalledEngine
83
+ from shikiru.engine.step_runner import StepRunner
84
+ from shikiru.engine.supervisor import TaskSupervisor
85
+ from shikiru.engine.takeover import Takeover
86
+ from shikiru.engine.task_facts import TaskFacts
87
+ from shikiru.engine.tick_guard import TickGuard
88
+ from shikiru.engine.titles import TaskTitles
89
+ from shikiru.engine.transitions import TransitionWriter
90
+ from shikiru.engine.turn_context import TurnContextPreparer
91
+ from shikiru.engine.turn_loop import TurnLoop
92
+ from shikiru.engine.turn_processes import TurnProcesses
93
+ from shikiru.engine.turns import TurnRunner
94
+ from shikiru.errors import ConfigError
95
+ from shikiru.helper.helper import Helper, HelperEntry
96
+ from shikiru.operator.commands import OperatorCommands
97
+ from shikiru.operator.detectors import InitWizardResult, ProjectDetectors
98
+ from shikiru.operator.doctor import Doctor
99
+ from shikiru.operator.events import EventFeed
100
+ from shikiru.operator.evidence_export import TaskEvidenceExporter
101
+ from shikiru.operator.evidence_writer import LocalEvidenceWriter
102
+ from shikiru.operator.lifecycle import ProjectLifecycle, StartResult, home_command
103
+ from shikiru.operator.plugin_checks import PluginChecks
104
+ from shikiru.operator.probe import AgentProbe
105
+ from shikiru.operator.queries import OperatorQueries
106
+ from shikiru.operator.services import OperatorServices
107
+ from shikiru.operator.sessions import SessionAttacher, SessionLister
108
+ from shikiru.paths import (
109
+ engine_lock_path,
110
+ engine_log_path,
111
+ project_config_path,
112
+ project_state_dir,
113
+ state_db_path,
114
+ )
115
+ from shikiru.private import PrivateRotatingFileHandler, make_private_dir, tighten_state_dir
116
+ from shikiru.process import SubprocessRunner
117
+ from shikiru.prompts.renderer import PromptRenderer
118
+ from shikiru.store.commands import CommandQueue
119
+ from shikiru.store.database import Database, transaction
120
+ from shikiru.store.escalations import EscalationStore
121
+ from shikiru.store.events import EventLog
122
+ from shikiru.store.feedback import FeedbackStore
123
+ from shikiru.store.notification_cursor import NotificationCursor
124
+ from shikiru.store.pushes import PushTargets
125
+ from shikiru.store.quotas import QuotaStore
126
+ from shikiru.store.reader import StateReader
127
+ from shikiru.store.retention import StateRetention
128
+ from shikiru.store.runs import RunStore
129
+ from shikiru.store.tasks import TaskStore
130
+ from shikiru.telemetry.disk import DiskSampler
131
+ from shikiru.telemetry.docker import DockerSampler
132
+ from shikiru.telemetry.resources import ResourceSampler
133
+ from shikiru.tmux.client import FORMAT_SESSION_NAME, Tmux, exact_target, tmux_key_label
134
+ from shikiru.tmux.layout import LayoutBuilder
135
+ from shikiru.tmux.session import PROJECT_DIR_ENV_VAR, SessionView, session_name
136
+ from shikiru.workspace.base_refs import BaseRefs, FetchedBaseRefs, LocalBaseRefs, LocalBaseSync
137
+ from shikiru.workspace.caches import CacheCloner, default_cow_args
138
+ from shikiru.workspace.facts import GitFacts, TaskWorkObserver
139
+ from shikiru.workspace.git import Git
140
+ from shikiru.workspace.github import GitHub
141
+ from shikiru.workspace.messages import OperatorMessages
142
+ from shikiru.workspace.roots import TaskRootBuilder
143
+
144
+ _TMUX_PANE_ENVIRONMENT_VARIABLE = "TMUX_PANE"
145
+
146
+ if TYPE_CHECKING:
147
+ import sqlite3 # noqa: TID251
148
+ from collections.abc import Awaitable, Callable, Sequence
149
+ from contextlib import AbstractContextManager
150
+ from pathlib import Path
151
+
152
+ from mcp.server.fastmcp import FastMCP
153
+ from rich.console import RenderableType
154
+ from textual.screen import ModalScreen
155
+
156
+ from shikiru.config.settings import LoggingConfig, Settings
157
+ from shikiru.engine.integration import IntegrationCoordinator
158
+
159
+ StartEngine = Callable[[], Awaitable[StartResult]]
160
+
161
+
162
+ DASHBOARD_RESOURCES_WIDGET = "resources"
163
+
164
+
165
+ class _PaletteContext(Protocol):
166
+ action: str | None
167
+ task_id: str | None
168
+ role: str | None
169
+ drafts: tuple[object, ...]
170
+
171
+
172
+ async def dashboard_tmux_binding(
173
+ tmux: Tmux,
174
+ pane_target: str | None,
175
+ project_root: Path,
176
+ expected_session: str,
177
+ ) -> tuple[str | None, Callable[[], Awaitable[bool]] | None]:
178
+ """Return the session prefix and a revalidated action for this project pane."""
179
+ if pane_target is None or not await tmux.pane_exists(pane_target):
180
+ return None, None
181
+ current_session = await tmux.display(pane_target, FORMAT_SESSION_NAME)
182
+ if current_session != expected_session:
183
+ return None, None
184
+ session_target = exact_target(current_session)
185
+ project_path = str(project_root)
186
+ if await tmux.show_session_environment(session_target, PROJECT_DIR_ENV_VAR) != project_path:
187
+ return None, None
188
+ hint = tmux_key_label(await tmux.current_prefix(session_target))
189
+
190
+ async def detach_project_client() -> bool:
191
+ if not await tmux.pane_exists(pane_target):
192
+ return False
193
+ active_session = await tmux.display(pane_target, FORMAT_SESSION_NAME)
194
+ if active_session != expected_session:
195
+ return False
196
+ active_session_target = exact_target(active_session)
197
+ if (
198
+ await tmux.show_session_environment(active_session_target, PROJECT_DIR_ENV_VAR)
199
+ != project_path
200
+ ):
201
+ return False
202
+ return await tmux.detach_client_for_pane(active_session_target, pane_target)
203
+
204
+ return hint, detach_project_client
205
+
206
+
207
+ def build_clock() -> Clock:
208
+ """Return the `Clock` implementation used by every real entry point."""
209
+ return SystemClock()
210
+
211
+
212
+ def build_project_detectors(project_root: Path) -> ProjectDetectors:
213
+ """Build the detector with one injected process, clock and git boundary."""
214
+ runner = SubprocessRunner()
215
+ plugins = PluginChecks(AgentRegistry(), runner, build_clock())
216
+ timeouts = load_default_orchestration().timeouts
217
+ return ProjectDetectors(
218
+ project_root,
219
+ git=Git(runner, command_timeout=timeouts.git_command),
220
+ plugin_checks=plugins,
221
+ cache_cloner=CacheCloner(
222
+ runner, cow_args=default_cow_args(), copy_timeout=timeouts.cache_copy
223
+ ),
224
+ )
225
+
226
+
227
+ async def run_init_wizard(project_root: Path) -> InitWizardResult | None:
228
+ """Run setup after the detector validates the repository prerequisites."""
229
+ doctor_renderer = cast(
230
+ "Callable[..., RenderableType]",
231
+ import_module("shikiru.cli.render").__dict__["doctor_renderable"],
232
+ )
233
+ detectors = build_project_detectors(project_root)
234
+ project = await detectors.detect()
235
+ config_path = project_config_path(project_root)
236
+ if await asyncio.to_thread(config_path.is_file):
237
+ message = f"{config_path} already exists; refusing to overwrite it"
238
+ raise ConfigError(message)
239
+ wizard_type = import_module("shikiru.dashboard.wizard").__dict__["InitWizardApp"]
240
+ wizard = wizard_type(
241
+ project_root=project_root,
242
+ project=project,
243
+ detectors=detectors,
244
+ doctor=build_doctor(project_root),
245
+ doctor_renderer=doctor_renderer,
246
+ orchestration_defaults=load_default_orchestration(),
247
+ )
248
+ return cast("InitWizardResult | None", await wizard.run_async(inline=False))
249
+
250
+
251
+ def build_doctor(project_root: Path) -> Doctor:
252
+ """Build environment diagnostics with one injected process, clock and registry."""
253
+ runner = SubprocessRunner()
254
+ clock = build_clock()
255
+ registry = AgentRegistry()
256
+ timeouts = load_default_orchestration().timeouts
257
+ return Doctor(
258
+ project_root,
259
+ cache_cloner=CacheCloner(
260
+ runner, cow_args=default_cow_args(), copy_timeout=timeouts.cache_copy
261
+ ),
262
+ git=Git(runner, command_timeout=timeouts.git_command),
263
+ runner=runner,
264
+ clock=clock,
265
+ plugin_checks=PluginChecks(registry, runner, clock),
266
+ probe=AgentProbe(registry, runner, clock),
267
+ )
268
+
269
+
270
+ def build_lifecycle() -> ProjectLifecycle:
271
+ """Build the detached engine lifecycle service."""
272
+ clock = build_clock()
273
+ runner = SubprocessRunner()
274
+ tmux = Tmux(runner)
275
+ session = SessionView(tmux)
276
+ registry = AgentRegistry()
277
+ plugin_checks = PluginChecks(registry, runner, clock)
278
+
279
+ def operator_factory(project_root: Path, settings: Settings) -> OperatorServices:
280
+ return build_operator(project_root, settings, clock=clock)
281
+
282
+ async def plugin_validator(settings: Settings) -> None:
283
+ results = [
284
+ await plugin_checks.check(name, config.options, settings.ladder_models(name))
285
+ for name, config in sorted(settings.agents.items())
286
+ ]
287
+ failures = [result for result in results if not result.ok]
288
+ if failures:
289
+ detail = "; ".join(f"{result.name}: {result.detail}" for result in failures)
290
+ message = f"agent plugin checks failed: {detail}"
291
+ raise ConfigError(message)
292
+
293
+ def engine_up(project_root: Path) -> bool:
294
+ return engine_is_running(engine_lock_path(project_root))
295
+
296
+ return ProjectLifecycle(
297
+ runner=runner,
298
+ tmux=tmux,
299
+ session=session,
300
+ clock=clock,
301
+ operator_factory=operator_factory,
302
+ plugin_validator=plugin_validator,
303
+ engine_up=engine_up,
304
+ )
305
+
306
+
307
+ def build_session_tools(
308
+ overrides: Sequence[str] = (),
309
+ ) -> tuple[SessionLister, SessionAttacher]:
310
+ """Build the read-only session lister and process-replacing attach adapter."""
311
+ runner = SubprocessRunner()
312
+ clock = build_clock()
313
+ tmux = Tmux(runner)
314
+ session_settings = load_session_settings(overrides)
315
+
316
+ def engine_up(project_root: Path) -> bool:
317
+ return engine_is_running(engine_lock_path(project_root))
318
+
319
+ def reader_for(project_root: Path) -> StateReader:
320
+ return StateReader(Database(state_db_path(project_root)), clock)
321
+
322
+ return (
323
+ SessionLister(
324
+ tmux=tmux,
325
+ runner=runner,
326
+ engine_up=engine_up,
327
+ reader_factory=reader_for,
328
+ remote_timeout_seconds=session_settings.ssh_timeout_seconds,
329
+ ),
330
+ SessionAttacher(runner, tmux, prompt_for_session),
331
+ )
332
+
333
+
334
+ def build_project_starter(project_root: Path, overrides: Sequence[str]) -> StartEngine:
335
+ """Build a lifecycle callback that starts this project without attaching a client."""
336
+ lifecycle = build_lifecycle()
337
+
338
+ async def start_engine() -> StartResult:
339
+ return await lifecycle.start(project_root, overrides, attach=False)
340
+
341
+ return start_engine
342
+
343
+
344
+ def _open_migrated_state_reader(project_root: Path, clock: Clock) -> StateReader:
345
+ database = Database(state_db_path(project_root))
346
+ database.connect().close()
347
+ return StateReader(database, clock)
348
+
349
+
350
+ async def run_dashboard(
351
+ project_root: Path,
352
+ overrides: Sequence[str],
353
+ focus_task: str | None,
354
+ ) -> None:
355
+ """Run the configured dashboard for one project entry point."""
356
+ dashboard_app_module = import_module("shikiru.dashboard.app")
357
+ dashboard_palette_module = import_module("shikiru.dashboard.palette")
358
+ dashboard_actions_module = import_module("shikiru.dashboard.actions_list")
359
+ dashboard_contract_module = import_module("shikiru.dashboard.contract")
360
+ dashboard_resolution_module = import_module("shikiru.dashboard.resolution")
361
+ dashboard_services_module = import_module("shikiru.dashboard.services")
362
+ dashboard_views_module = import_module("shikiru.dashboard.views")
363
+ dashboard_motion_module = import_module("shikiru.dashboard.motion")
364
+ dashboard_activity_module = import_module("shikiru.dashboard.activity")
365
+ clock = build_clock()
366
+ runner = SubprocessRunner()
367
+ state = _open_migrated_state_reader(project_root, clock)
368
+ live_settings = import_module("shikiru.dashboard.live_settings").LiveSettings(
369
+ state,
370
+ lambda: load_settings(project_root, overrides),
371
+ )
372
+ settings = live_settings.current()
373
+ git = Git(runner, command_timeout=settings.orchestration.timeouts.git_command)
374
+ services = build_operator(project_root, settings, clock=clock)
375
+ try:
376
+ evidence_exporter = TaskEvidenceExporter(
377
+ services.queries,
378
+ LocalEvidenceWriter(project_root),
379
+ clock,
380
+ )
381
+ lock_path = engine_lock_path(project_root)
382
+ session_target = exact_target(session_name(settings.project.name))
383
+ tmux = Tmux(runner)
384
+ tmux_detach_hint, detach_tmux_client = await dashboard_tmux_binding(
385
+ tmux,
386
+ os.environ.get(_TMUX_PANE_ENVIRONMENT_VARIABLE),
387
+ project_root,
388
+ session_name(settings.project.name),
389
+ )
390
+ dashboard_services = dashboard_services_module.DashboardServices(
391
+ dashboard_services_module.GitRepoStatusService(
392
+ git,
393
+ settings.repo,
394
+ project_root,
395
+ clock,
396
+ cache_ttl_seconds=settings.dashboard.repo_status_cache_seconds,
397
+ ),
398
+ clock=clock,
399
+ engine_up=lambda: engine_is_running(lock_path),
400
+ engine_identity=lambda: EngineLock.identity_if_running(lock_path),
401
+ project_root=project_root,
402
+ state=services.reader,
403
+ tmux=tmux,
404
+ session_target=session_target,
405
+ resources=ResourceSampler(
406
+ clock,
407
+ lambda: live_settings.current().orchestration.resources,
408
+ ),
409
+ docker=DockerSampler(
410
+ runner,
411
+ clock,
412
+ refresh_seconds=lambda: live_settings.current().dashboard.docker_refresh_seconds,
413
+ timeout_seconds=settings.dashboard.docker_timeout_seconds,
414
+ project_roots=(
415
+ None if settings.dashboard.docker_scope == DOCKER_SCOPE_ALL else (project_root,)
416
+ ),
417
+ ),
418
+ disk=DiskSampler(
419
+ clock,
420
+ project_root,
421
+ refresh_seconds=lambda: live_settings.current().dashboard.disk_refresh_seconds,
422
+ size_refresh_seconds=lambda: (
423
+ live_settings.current().dashboard.disk_size_refresh_seconds
424
+ ),
425
+ ),
426
+ )
427
+ views = dashboard_views_module.StateDashboardViews(
428
+ services.reader,
429
+ focus_task=focus_task,
430
+ settings=live_settings,
431
+ sources=dashboard_services,
432
+ queries=services.queries,
433
+ clock=clock,
434
+ motion=dashboard_motion_module.DashboardMotion(
435
+ services.reader,
436
+ project_root,
437
+ live_settings,
438
+ dashboard_activity_module.RoleViewTail(settings.dashboard.last_words_chars),
439
+ ),
440
+ work=dashboard_services_module.TaskWorkService(
441
+ TaskWorkObserver(git),
442
+ clock,
443
+ cache_ttl_seconds=settings.dashboard.task_work_cache_seconds,
444
+ ),
445
+ )
446
+ navigation = dashboard_services_module.TaskNavigation(
447
+ tmux,
448
+ services.queries,
449
+ session_target,
450
+ )
451
+
452
+ app_instance = None
453
+ resolution_drafts = dashboard_resolution_module.ResolutionDrafts()
454
+
455
+ def resolution_screen(escalation_id: str) -> ModalScreen[bool]:
456
+ return dashboard_resolution_module.ResolutionScreen(
457
+ services.commands,
458
+ services.queries,
459
+ clock=clock,
460
+ on_change=services.reader.watch,
461
+ drafts=resolution_drafts,
462
+ escalation_id=escalation_id,
463
+ )
464
+
465
+ async def push_palette(request: _PaletteContext) -> None:
466
+ if app_instance is None:
467
+ message = "dashboard palette is not mounted"
468
+ raise ConfigError(message)
469
+ dashboard = app_instance
470
+
471
+ def popup(popup_request: _PaletteContext) -> ModalScreen[str | None]:
472
+ return dashboard_palette_module.PaletteScreen(
473
+ services.commands,
474
+ services.queries,
475
+ tmux.select_pane,
476
+ on_change=services.reader.watch,
477
+ action_confirmation_seconds=(
478
+ live_settings.current().dashboard.action_confirmation_seconds
479
+ ),
480
+ initial_context=dashboard.attach_palette_drafts(popup_request),
481
+ start_engine=(
482
+ build_project_starter(project_root, overrides)
483
+ if popup_request.action
484
+ == dashboard_contract_module.PaletteAction.START.value
485
+ else None
486
+ ),
487
+ evidence_exporter=evidence_exporter,
488
+ remember_contextual_draft=dashboard.remember_contextual_palette_draft,
489
+ )
490
+
491
+ def popup_closed(queued: str | None) -> None:
492
+ del queued
493
+ dashboard.refresh_task_detail_after_resume()
494
+
495
+ if request.action:
496
+ await dashboard.push_screen(popup(request), callback=popup_closed)
497
+ return
498
+ actions = dashboard_actions_module.ActionsListScreen(
499
+ services.queries,
500
+ popup,
501
+ request,
502
+ on_change=services.reader.watch,
503
+ action_confirmation_seconds=(
504
+ live_settings.current().dashboard.action_confirmation_seconds
505
+ ),
506
+ engine_state_refresh_hz=live_settings.current().dashboard.motion.refresh_hz,
507
+ task_routes_available=True,
508
+ project_diagnostics_available=(
509
+ DASHBOARD_RESOURCES_WIDGET in settings.dashboard.widgets
510
+ ),
511
+ extensions=dashboard.extension_entries(),
512
+ resolution_screen=resolution_screen,
513
+ )
514
+ await dashboard.push_screen(actions, callback=dashboard.palette_return_callback())
515
+
516
+ async def open_palette(action: str) -> None:
517
+ await push_palette(dashboard_contract_module.PaletteRequest(action=action or None))
518
+
519
+ app_instance = dashboard_app_module.DashboardApp(
520
+ focus_task=focus_task,
521
+ views=views,
522
+ on_change=services.reader.watch,
523
+ clock=clock,
524
+ motion_interval_seconds=1 / settings.dashboard.motion.refresh_hz,
525
+ settings=live_settings,
526
+ open_palette=open_palette,
527
+ widget_names=settings.dashboard.widgets,
528
+ open_palette_context=push_palette,
529
+ resolution_screen=resolution_screen,
530
+ navigate_task_window=navigation.task_window,
531
+ navigate_home=navigation.home,
532
+ tmux_detach_hint=tmux_detach_hint,
533
+ detach_tmux_client=detach_tmux_client,
534
+ )
535
+ await app_instance.run_async()
536
+ finally:
537
+ services.close()
538
+ state.close()
539
+
540
+
541
+ async def run_palette(
542
+ project_root: Path, overrides: Sequence[str], origin_pane: str | None = None
543
+ ) -> None:
544
+ """Run the operator palette as a popup using project-local operator services."""
545
+ palette_module = import_module("shikiru.dashboard.palette_app")
546
+ resolution_module = import_module("shikiru.dashboard.resolution")
547
+ palette_request = import_module("shikiru.dashboard.contract").PaletteRequest
548
+ clock = build_clock()
549
+ settings = load_settings(project_root, overrides)
550
+ start_engine = build_project_starter(project_root, overrides)
551
+ services = build_operator(project_root, settings, clock=clock)
552
+ try:
553
+ evidence_exporter = TaskEvidenceExporter(
554
+ services.queries,
555
+ LocalEvidenceWriter(project_root),
556
+ clock,
557
+ )
558
+ task_role = (
559
+ services.queries.task_role_for_pane(origin_pane) if origin_pane is not None else None
560
+ )
561
+ initial_context = None
562
+ if task_role is not None:
563
+ task_id, role = task_role
564
+ initial_context = palette_request(task_id=task_id, role=role.value)
565
+ resolution_drafts = resolution_module.ResolutionDrafts()
566
+
567
+ def resolution_screen(escalation_id: str) -> ModalScreen[bool]:
568
+ return resolution_module.ResolutionScreen(
569
+ services.commands,
570
+ services.queries,
571
+ clock=clock,
572
+ on_change=services.reader.watch,
573
+ drafts=resolution_drafts,
574
+ escalation_id=escalation_id,
575
+ )
576
+
577
+ app_instance = palette_module.PaletteApp(
578
+ services.commands,
579
+ services.queries,
580
+ Tmux(SubprocessRunner()).select_pane,
581
+ on_change=services.reader.watch,
582
+ action_confirmation_seconds=settings.dashboard.action_confirmation_seconds,
583
+ engine_state_refresh_hz=settings.dashboard.motion.refresh_hz,
584
+ start_engine=start_engine,
585
+ initial_context=initial_context,
586
+ evidence_exporter=evidence_exporter,
587
+ resolution_screen=resolution_screen,
588
+ )
589
+ await app_instance.run_async()
590
+ finally:
591
+ services.close()
592
+
593
+
594
+ def build_operator(
595
+ project_root: Path,
596
+ settings: Settings | None = None,
597
+ *,
598
+ clock: Clock | None = None,
599
+ ) -> OperatorServices:
600
+ """Build read, command and event services for one initialized project."""
601
+ resolved_clock = clock if clock is not None else build_clock()
602
+ resolved_settings = settings if settings is not None else load_settings(project_root)
603
+ database = Database(state_db_path(project_root))
604
+ connection = database.connect()
605
+ reader = StateReader(database, resolved_clock)
606
+ command_queue = CommandQueue(connection, resolved_clock)
607
+ engine_lock = engine_lock_path(project_root)
608
+
609
+ def engine_state() -> bool:
610
+ return engine_is_running(engine_lock)
611
+
612
+ queries = OperatorQueries(
613
+ reader,
614
+ project=resolved_settings.project.name,
615
+ project_root=project_root,
616
+ engine_up=engine_state,
617
+ engine_identity=lambda: EngineLock.identity_if_running(engine_lock),
618
+ max_parallel=resolved_settings.orchestration.max_parallel,
619
+ clock=resolved_clock,
620
+ )
621
+ commands = OperatorCommands(
622
+ command_queue,
623
+ resolved_clock,
624
+ engine_up=engine_state,
625
+ )
626
+ events = EventFeed(reader, resolved_clock)
627
+
628
+ def close() -> None:
629
+ reader.close()
630
+ connection.close()
631
+
632
+ return OperatorServices(reader, queries, commands, events, close)
633
+
634
+
635
+ def build_mcp_server(
636
+ project_root: Path,
637
+ overrides: Sequence[str] = (),
638
+ ) -> tuple[FastMCP, OperatorServices]:
639
+ """Build the stdio MCP server and the operator services it owns."""
640
+ settings = load_settings(project_root, overrides)
641
+ services = build_operator(project_root, settings)
642
+ module = import_module("shikiru.mcp.server")
643
+ factory = cast("Callable[[OperatorServices], FastMCP]", module.build_server)
644
+ return factory(services), services
645
+
646
+
647
+ def _build_flow(
648
+ *,
649
+ project_root: Path,
650
+ tasks: TaskStore,
651
+ events: EventLog,
652
+ runs: RunStore,
653
+ sessions: SessionManager,
654
+ turns: TurnRunner,
655
+ contexts: TurnContextPreparer,
656
+ recovery: TurnRecovery,
657
+ writer: TransitionWriter,
658
+ raiser: EscalationRaiser,
659
+ evidence: EvidenceCollector,
660
+ facts: TaskFacts,
661
+ history: ReportHistory,
662
+ integrator: IntegrationCoordinator,
663
+ settings: Settings,
664
+ feedback_store: FeedbackStore | None,
665
+ github: GitHub,
666
+ git: Git,
667
+ ) -> TaskFlow:
668
+ filer = EscalationFiler(raiser=raiser, evidence=evidence, writer=writer, tasks=tasks)
669
+ turn_loop = TurnLoop(
670
+ tasks=tasks,
671
+ events=events,
672
+ sessions=sessions,
673
+ turns=turns,
674
+ contexts=contexts,
675
+ recovery=recovery,
676
+ escalations=filer,
677
+ runs=runs,
678
+ )
679
+ steps = StepRunner(
680
+ loop=turn_loop,
681
+ acceptance=AcceptanceGuard(events=events, facts=facts),
682
+ escalations=filer,
683
+ writer=writer,
684
+ events=events,
685
+ settings=settings,
686
+ )
687
+ feedback_rounds = (
688
+ FeedbackRounds(
689
+ store=feedback_store,
690
+ steps=steps,
691
+ github=github,
692
+ git=git,
693
+ tasks=tasks,
694
+ events=events,
695
+ project_root=project_root,
696
+ remote=settings.repo.remote,
697
+ settings=settings.orchestration,
698
+ )
699
+ if feedback_store is not None
700
+ else None
701
+ )
702
+ return TaskFlow(
703
+ tasks=tasks,
704
+ writer=writer,
705
+ steps=steps,
706
+ turn_loop=turn_loop,
707
+ escalations=filer,
708
+ integration_turns=IntegrationTurns(
709
+ coordinator=integrator,
710
+ events=events,
711
+ history=history,
712
+ project_root=project_root,
713
+ tasks=tasks,
714
+ steps=steps,
715
+ escalations=filer,
716
+ feedback=feedback_rounds,
717
+ ),
718
+ )
719
+
720
+
721
+ def _build_fault_reporting(
722
+ *,
723
+ events: EventLog,
724
+ quotas: QuotaStore,
725
+ agents: AgentPool,
726
+ clock: Clock,
727
+ settings: Settings,
728
+ logger: logging.Logger,
729
+ ) -> tuple[FaultReporter, QuotaReconciler, QuotaSchedule]:
730
+ faults = FaultReporter(
731
+ events=events, clock=clock, settings=settings.orchestration.faults, logger=logger
732
+ )
733
+ schedule = QuotaSchedule()
734
+ reconciler = QuotaReconciler(
735
+ quotas=quotas,
736
+ agents=agents,
737
+ events=events,
738
+ clock=clock,
739
+ settings=settings.orchestration.quota_refresh,
740
+ faults=faults,
741
+ schedule=schedule,
742
+ )
743
+ return faults, reconciler, schedule
744
+
745
+
746
+ def build_engine(
747
+ project_root: Path,
748
+ settings: Settings | None = None,
749
+ *,
750
+ clock: Clock | None = None,
751
+ runner: SubprocessRunner | None = None,
752
+ tmux: Tmux | None = None,
753
+ registry: AgentRegistry | None = None,
754
+ ) -> Engine:
755
+ """Build the in-process engine from project boundaries and validated settings."""
756
+ resolved_clock = clock if clock is not None else build_clock()
757
+ resolved_settings = settings if settings is not None else load_settings(project_root)
758
+ resolved_runner = runner if runner is not None else SubprocessRunner()
759
+ resolved_tmux = tmux if tmux is not None else Tmux(resolved_runner)
760
+ resolved_registry = registry if registry is not None else AgentRegistry()
761
+ agent_pool = AgentPool(
762
+ resolved_registry.create, resolved_runner, resolved_settings, resolved_clock
763
+ )
764
+ helper_entries = tuple(
765
+ HelperEntry(rung, agent_pool.get(rung.agent))
766
+ for rung in resolved_settings.roles.helper.ladder
767
+ )
768
+ helper = Helper(helper_entries, resolved_clock)
769
+ connection = Database(state_db_path(project_root)).connect()
770
+ tasks = TaskStore(connection, resolved_clock)
771
+ runs = RunStore(connection, resolved_clock)
772
+ events = EventLog(connection, resolved_clock)
773
+ quotas = QuotaStore(connection)
774
+ escalations_store = EscalationStore(connection, resolved_clock)
775
+ command_queue = CommandQueue(connection, resolved_clock)
776
+
777
+ decorator = CosmeticDecorator(resolved_tmux)
778
+
779
+ sessions = SessionManager(
780
+ runs=runs,
781
+ agents=agent_pool,
782
+ project_root=project_root,
783
+ settings=resolved_settings,
784
+ )
785
+ turn_processes = TurnProcesses(
786
+ runner=resolved_runner, clock=resolved_clock, project_root=project_root
787
+ )
788
+ target = exact_target(session_name(resolved_settings.project.name))
789
+ logger, log_handler = _configure_engine_logging(project_root, resolved_settings.logging)
790
+ fault_reporter, reconciler, quota_schedule = _build_fault_reporting(
791
+ events=events,
792
+ quotas=quotas,
793
+ agents=agent_pool,
794
+ clock=resolved_clock,
795
+ settings=resolved_settings,
796
+ logger=logger,
797
+ )
798
+ titles = TaskTitles(
799
+ decorator,
800
+ runs,
801
+ tasks,
802
+ resolved_settings,
803
+ waits=QuotaWaits(
804
+ quotas=quotas,
805
+ events=events,
806
+ schedule=quota_schedule,
807
+ clock=resolved_clock,
808
+ settings=resolved_settings,
809
+ ),
810
+ project_root=project_root,
811
+ )
812
+ monitor = TurnMonitor(
813
+ sessions=sessions,
814
+ runs=runs,
815
+ quotas=reconciler,
816
+ events=events,
817
+ clock=resolved_clock,
818
+ settings=resolved_settings,
819
+ decorator=decorator,
820
+ refresh_titles=titles.refresh,
821
+ processes=turn_processes,
822
+ )
823
+
824
+ layout = LayoutGuard(
825
+ tmux=resolved_tmux,
826
+ view=SessionView(resolved_tmux),
827
+ builder=LayoutBuilder(resolved_tmux),
828
+ decorator=decorator,
829
+ session_target=target,
830
+ tasks=tasks,
831
+ settings=resolved_settings,
832
+ project_root=project_root,
833
+ events=events,
834
+ runs=runs,
835
+ commands=command_queue,
836
+ faults=fault_reporter,
837
+ refresh_titles=titles.refresh,
838
+ )
839
+ session_guard = SessionGuard(
840
+ tmux=resolved_tmux,
841
+ view=SessionView(resolved_tmux),
842
+ session_target=target,
843
+ project_name=resolved_settings.project.name,
844
+ project_root=project_root,
845
+ home_command=home_command(project_root),
846
+ logger=logger,
847
+ )
848
+ git = Git(
849
+ resolved_runner,
850
+ command_timeout=resolved_settings.orchestration.timeouts.git_command,
851
+ )
852
+ fetched_bases = FetchedBaseRefs(
853
+ git,
854
+ project_root,
855
+ resolved_settings.repo.remote,
856
+ fetch_timeout=resolved_settings.orchestration.network.fetch_timeout_seconds,
857
+ )
858
+ roots = TaskRootBuilder(
859
+ git,
860
+ project_root,
861
+ resolved_settings.repo,
862
+ CacheCloner(
863
+ resolved_runner,
864
+ cow_args=default_cow_args(),
865
+ copy_timeout=resolved_settings.orchestration.timeouts.cache_copy,
866
+ ),
867
+ fetched_bases
868
+ if resolved_settings.orchestration.integration == PULL_REQUEST_INTEGRATION
869
+ else LocalBaseRefs(),
870
+ trackers=resolved_settings.project.trackers,
871
+ )
872
+ pull_request_closing = PullRequestClosing(
873
+ closer=PullRequestCloser(
874
+ github=GitHub(resolved_runner), events=events, project_root=project_root
875
+ ),
876
+ tasks=tasks,
877
+ faults=fault_reporter,
878
+ clock=resolved_clock,
879
+ settings=resolved_settings.orchestration,
880
+ )
881
+ task_finisher = TaskFinisher(
882
+ sessions,
883
+ layout,
884
+ roots,
885
+ pull_request_closing,
886
+ FeedbackWorkAudit(
887
+ git=git,
888
+ github=GitHub(resolved_runner),
889
+ events=events,
890
+ store=FeedbackStore(connection),
891
+ project_root=project_root,
892
+ ),
893
+ )
894
+ history = ReportHistory(project_root)
895
+ task_facts = TaskFacts(
896
+ git_facts=GitFacts(git), project_root=project_root, settings=resolved_settings
897
+ )
898
+ evidence = EvidenceCollector(history=history, facts=task_facts, project_root=project_root)
899
+ escalation_raiser = EscalationRaiser(
900
+ escalations_store,
901
+ events,
902
+ runs,
903
+ project_root,
904
+ atomically=lambda: transaction(connection),
905
+ titles=titles,
906
+ evidence=evidence,
907
+ settings=resolved_settings,
908
+ )
909
+ push_service = _build_push_service(
910
+ settings=resolved_settings,
911
+ targets=PushTargets(connection),
912
+ git=git,
913
+ faults=fault_reporter,
914
+ clock=resolved_clock,
915
+ project_root=project_root,
916
+ logger=logger,
917
+ )
918
+ integrator = _build_integrator(
919
+ project_root=project_root,
920
+ github=GitHub(resolved_runner),
921
+ feedback_store=_feedback_store(connection, resolved_settings),
922
+ settings=resolved_settings,
923
+ clock=resolved_clock,
924
+ git=git,
925
+ fetched_bases=fetched_bases,
926
+ tasks=tasks,
927
+ events=events,
928
+ faults=fault_reporter,
929
+ task_finisher=task_finisher,
930
+ publication=push_service,
931
+ atomically=lambda: transaction(connection),
932
+ )
933
+ contexts = TurnContextPreparer(
934
+ history=history,
935
+ escalation_store=escalations_store,
936
+ facts=task_facts,
937
+ settings=resolved_settings,
938
+ project_root=project_root,
939
+ )
940
+ messages = OperatorMessages(project_root)
941
+ writer = TransitionWriter(tasks=tasks, events=events, titles=titles, settings=resolved_settings)
942
+ recovery = TurnRecovery(
943
+ tasks=tasks,
944
+ events=events,
945
+ quota=quotas,
946
+ reconciler=reconciler,
947
+ clock=resolved_clock,
948
+ writer=writer,
949
+ settings=resolved_settings,
950
+ )
951
+ flow = _build_flow(
952
+ project_root=project_root,
953
+ tasks=tasks,
954
+ events=events,
955
+ runs=runs,
956
+ sessions=sessions,
957
+ turns=TurnRunner(
958
+ agents=agent_pool,
959
+ sessions=sessions,
960
+ monitor=monitor,
961
+ helper=helper,
962
+ renderer=PromptRenderer(project_root),
963
+ messages=messages,
964
+ events=events,
965
+ runs=runs,
966
+ tasks=tasks,
967
+ project_root=project_root,
968
+ settings=resolved_settings,
969
+ ),
970
+ contexts=contexts,
971
+ recovery=recovery,
972
+ writer=writer,
973
+ raiser=escalation_raiser,
974
+ evidence=evidence,
975
+ facts=task_facts,
976
+ history=history,
977
+ integrator=integrator,
978
+ settings=resolved_settings,
979
+ feedback_store=_feedback_store(connection, resolved_settings),
980
+ github=GitHub(resolved_runner),
981
+ git=git,
982
+ )
983
+ task_supervisor = TaskSupervisor(tasks, flow, task_finisher)
984
+ handler_context = HandlerContext(
985
+ tasks=tasks,
986
+ escalations=escalations_store,
987
+ events=events,
988
+ runs=runs,
989
+ project_root=project_root,
990
+ cancel_task=task_supervisor.cancel_task,
991
+ start_task=task_supervisor.start_task,
992
+ finish_task=task_finisher.finish_task,
993
+ stop_engine=task_supervisor.request_stop,
994
+ refresh_titles=titles.refresh,
995
+ atomically=lambda: transaction(connection),
996
+ )
997
+ takeover = Takeover(
998
+ tmux=resolved_tmux,
999
+ layout=layout,
1000
+ agents=agent_pool,
1001
+ tasks=tasks,
1002
+ runs=runs,
1003
+ messages=messages,
1004
+ events=events,
1005
+ project_root=project_root,
1006
+ project_name=resolved_settings.project.name,
1007
+ settings=resolved_settings,
1008
+ cancel_task=task_supervisor.cancel_task,
1009
+ start_task=task_supervisor.start_task,
1010
+ refresh_titles=titles.refresh,
1011
+ atomically=lambda: transaction(connection),
1012
+ )
1013
+ handlers = HandlerRegistry(
1014
+ [
1015
+ SubmitHandler(handler_context),
1016
+ PrioritizeHandler(handler_context),
1017
+ PauseHandler(handler_context),
1018
+ ResumeHandler(handler_context),
1019
+ CancelHandler(handler_context),
1020
+ RetryHandler(handler_context),
1021
+ SetRungHandler(handler_context),
1022
+ SayHandler(handler_context, messages),
1023
+ TakeOverHandler(handler_context, takeover),
1024
+ HandBackHandler(handler_context, takeover),
1025
+ ResolveHandler(handler_context),
1026
+ StopHandler(handler_context),
1027
+ ]
1028
+ )
1029
+ sampler = ResourceSampler(resolved_clock, lambda: resolved_settings.orchestration.resources)
1030
+ notifier = Notifier(
1031
+ resolved_tmux,
1032
+ resolved_runner,
1033
+ fault_reporter,
1034
+ session_target=target,
1035
+ command=resolved_settings.notify.command,
1036
+ timeout_seconds=resolved_settings.notify.timeout_seconds,
1037
+ )
1038
+ scheduler = Scheduler(
1039
+ tasks=tasks,
1040
+ settings=resolved_settings,
1041
+ quotas=quotas,
1042
+ reconciler=reconciler,
1043
+ resources=sampler,
1044
+ clock=resolved_clock,
1045
+ layout=layout,
1046
+ roots=RootPreparation(
1047
+ roots,
1048
+ resolved_clock,
1049
+ budget_seconds=resolved_settings.orchestration.dispatch_budget_seconds,
1050
+ ),
1051
+ events=events,
1052
+ on_dependency_cancelled=DependencyEscalator(writer, escalation_raiser),
1053
+ on_dispatch_failure=DispatchFailureEscalator(writer, escalation_raiser),
1054
+ titles=titles,
1055
+ faults=fault_reporter,
1056
+ )
1057
+ notifications = NotificationDelivery(notifier)
1058
+ return Engine(
1059
+ lock_path=engine_lock_path(project_root),
1060
+ command_queue=command_queue,
1061
+ tasks=tasks,
1062
+ events=events,
1063
+ scheduler=scheduler,
1064
+ task_supervisor=task_supervisor,
1065
+ handlers=handlers,
1066
+ layout=layout,
1067
+ session_guard=session_guard,
1068
+ tick_guard=TickGuard(
1069
+ reporter=fault_reporter,
1070
+ clock=resolved_clock,
1071
+ settings=resolved_settings.orchestration.faults,
1072
+ logger=logger,
1073
+ scope="engine tick",
1074
+ ),
1075
+ command_guard=TickGuard(
1076
+ reporter=fault_reporter,
1077
+ clock=resolved_clock,
1078
+ settings=resolved_settings.orchestration.faults,
1079
+ logger=logger,
1080
+ scope="command loop",
1081
+ ),
1082
+ loop_guard=TickGuard(
1083
+ reporter=fault_reporter,
1084
+ clock=resolved_clock,
1085
+ settings=resolved_settings.orchestration.faults,
1086
+ logger=logger,
1087
+ scope="engine loop",
1088
+ ),
1089
+ reconciler=Reconciler(
1090
+ tasks=tasks,
1091
+ layout=layout,
1092
+ processes=turn_processes,
1093
+ resume_task=task_supervisor.resume_task,
1094
+ finish_task=task_finisher.finish_task,
1095
+ restore_escalation=escalation_raiser.ensure_actionable,
1096
+ restore_pull_request_closes=pull_request_closing.restore,
1097
+ faults=fault_reporter,
1098
+ project_root=project_root,
1099
+ ),
1100
+ notifications=notifications,
1101
+ notification_feed=NotificationFeed(events, NotificationCursor(connection), notifications),
1102
+ retention=Retention(
1103
+ store=StateRetention(connection),
1104
+ tasks=tasks,
1105
+ cursor=NotificationCursor(connection),
1106
+ project_root=project_root,
1107
+ has_open_pull_request=pull_request_closing.has_open,
1108
+ clock=resolved_clock,
1109
+ settings=resolved_settings.retention,
1110
+ ),
1111
+ retention_guard=TickGuard(
1112
+ reporter=fault_reporter,
1113
+ clock=resolved_clock,
1114
+ settings=resolved_settings.orchestration.faults,
1115
+ logger=logger,
1116
+ scope="retention",
1117
+ ),
1118
+ pull_request_closing=pull_request_closing,
1119
+ push_service=push_service,
1120
+ settings=resolved_settings,
1121
+ clock=resolved_clock,
1122
+ close_state=connection.close,
1123
+ logger=logger,
1124
+ log_handler=log_handler,
1125
+ )
1126
+
1127
+
1128
+ def build_engine_process(project_root: Path, settings: Settings) -> SignalledEngine:
1129
+ """Build the engine as the `shikiru engine` process runs it: stopped by SIGTERM and SIGINT."""
1130
+ clock = build_clock()
1131
+ runner = SubprocessRunner()
1132
+ return SignalledEngine(
1133
+ engine=build_engine(project_root, settings, clock=clock, runner=runner),
1134
+ turn_processes=TurnProcesses(runner=runner, clock=clock, project_root=project_root),
1135
+ clock=clock,
1136
+ shutdown_seconds=settings.orchestration.shutdown_seconds,
1137
+ )
1138
+
1139
+
1140
+ def _build_push_service(
1141
+ *,
1142
+ settings: Settings,
1143
+ targets: PushTargets,
1144
+ git: Git,
1145
+ faults: FaultReporter,
1146
+ clock: Clock,
1147
+ project_root: Path,
1148
+ logger: logging.Logger,
1149
+ ) -> PushService | None:
1150
+ if settings.orchestration.integration == PULL_REQUEST_INTEGRATION:
1151
+ return None
1152
+ return PushService(
1153
+ targets=targets,
1154
+ git=git,
1155
+ guard_for=lambda base: TickGuard(
1156
+ reporter=faults,
1157
+ clock=clock,
1158
+ settings=settings.orchestration.faults,
1159
+ logger=logger,
1160
+ scope=f"{PUSH_FAULT_SCOPE} {base}",
1161
+ ),
1162
+ remote_guard=TickGuard(
1163
+ reporter=faults,
1164
+ clock=clock,
1165
+ settings=settings.orchestration.faults,
1166
+ logger=logger,
1167
+ scope=f"{REMOTE_CHECK_FAULT_SCOPE} {settings.repo.remote}",
1168
+ ),
1169
+ clock=clock,
1170
+ project_root=project_root,
1171
+ remote=settings.repo.remote,
1172
+ interval_seconds=settings.orchestration.push_interval_seconds,
1173
+ remote_recheck_seconds=settings.orchestration.remote_recheck_seconds,
1174
+ fetch_timeout_seconds=settings.orchestration.network.fetch_timeout_seconds,
1175
+ )
1176
+
1177
+
1178
+ def _feedback_store(connection: sqlite3.Connection, settings: Settings) -> FeedbackStore | None:
1179
+ if settings.orchestration.integration != PULL_REQUEST_INTEGRATION:
1180
+ return None
1181
+ return FeedbackStore(connection)
1182
+
1183
+
1184
+ def _build_integrator(
1185
+ *,
1186
+ project_root: Path,
1187
+ settings: Settings,
1188
+ clock: Clock,
1189
+ git: Git,
1190
+ fetched_bases: FetchedBaseRefs,
1191
+ tasks: TaskStore,
1192
+ events: EventLog,
1193
+ faults: FaultReporter,
1194
+ task_finisher: TaskFinisher,
1195
+ publication: PushService | None,
1196
+ atomically: Callable[[], AbstractContextManager[object]],
1197
+ github: GitHub,
1198
+ feedback_store: FeedbackStore | None,
1199
+ ) -> Integrator:
1200
+ strategy: IntegrationStrategy
1201
+ base_refs: BaseRefs
1202
+ feedback = (
1203
+ FeedbackIntake(
1204
+ github=github,
1205
+ store=feedback_store,
1206
+ events=events,
1207
+ faults=faults,
1208
+ project_root=project_root,
1209
+ scope=settings.orchestration.pr_feedback,
1210
+ )
1211
+ if feedback_store is not None
1212
+ else None
1213
+ )
1214
+ match settings.orchestration.integration:
1215
+ case "merge" | "squash" as mode:
1216
+ base_refs = LocalBaseRefs()
1217
+ local = LocalMerge if mode == MERGE_INTEGRATION else SquashMerge
1218
+ strategy = local(
1219
+ git,
1220
+ clock,
1221
+ events=events,
1222
+ project_root=project_root,
1223
+ integration_wait=settings.orchestration.timeouts.integration_wait,
1224
+ )
1225
+ case "pr":
1226
+ base_refs = fetched_bases
1227
+ strategy = PullRequest(
1228
+ git=git,
1229
+ github=github,
1230
+ clock=clock,
1231
+ tasks=tasks,
1232
+ events=events,
1233
+ project_root=project_root,
1234
+ remote=settings.repo.remote,
1235
+ base_refs=fetched_bases,
1236
+ local_base=LocalBaseSync(git, project_root),
1237
+ faults=faults,
1238
+ settings=settings.orchestration,
1239
+ feedback=feedback,
1240
+ )
1241
+ return Integrator(
1242
+ git=git,
1243
+ strategy=strategy,
1244
+ base_refs=base_refs,
1245
+ lock=asyncio.Lock(),
1246
+ project_root=project_root,
1247
+ tasks=tasks,
1248
+ events=events,
1249
+ finish_task=task_finisher.finish_task,
1250
+ publication=publication,
1251
+ atomically=atomically,
1252
+ )
1253
+
1254
+
1255
+ def _tighten_state_dir(project_root: Path, logger: logging.Logger) -> None:
1256
+ state_dir = project_state_dir(project_root)
1257
+ tightened = tighten_state_dir(state_dir)
1258
+ if tightened:
1259
+ logger.info(
1260
+ "removed group and other access from %d paths under %s", len(tightened), state_dir
1261
+ )
1262
+
1263
+
1264
+ def _configure_engine_logging(
1265
+ project_root: Path, config: LoggingConfig
1266
+ ) -> tuple[logging.Logger, logging.FileHandler]:
1267
+ log_path = engine_log_path(project_root)
1268
+ make_private_dir(log_path.parent)
1269
+ logger = logging.getLogger(ENGINE_LOGGER_NAME)
1270
+ handler = PrivateRotatingFileHandler(
1271
+ log_path, maxBytes=config.engine_log_max_bytes, backupCount=config.engine_log_backups
1272
+ )
1273
+ logger.addHandler(handler)
1274
+ logger.setLevel(logging.INFO)
1275
+ logger.propagate = False
1276
+ _tighten_state_dir(project_root, logger)
1277
+ return logger, handler