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/cli/app.py ADDED
@@ -0,0 +1,770 @@
1
+ """The `shikiru` Typer application and its command adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import os
8
+ from importlib.metadata import version
9
+ from pathlib import Path
10
+ from typing import TYPE_CHECKING, Annotated, NoReturn
11
+
12
+ import typer
13
+ import typer.rich_utils as typer_rich_utils
14
+ from pydantic import BaseModel
15
+ from typer._click.exceptions import MissingParameter
16
+ from typer.core import TyperCommand
17
+
18
+ from shikiru.agents.contracts import parse_rung
19
+ from shikiru.bootstrap import (
20
+ build_doctor,
21
+ build_engine_process,
22
+ build_lifecycle,
23
+ build_mcp_server,
24
+ build_operator,
25
+ build_project_detectors,
26
+ build_session_tools,
27
+ run_dashboard,
28
+ run_init_wizard,
29
+ run_palette,
30
+ )
31
+ from shikiru.cli.context import CliState, project_root, state_from
32
+ from shikiru.cli.render import (
33
+ ack_renderable,
34
+ doctor_renderable,
35
+ error_renderable,
36
+ init_renderable,
37
+ init_wizard_result_renderable,
38
+ missing_submit_request_renderable,
39
+ restart_renderable,
40
+ session_diagnostics_renderable,
41
+ session_rows_renderable,
42
+ start_renderable,
43
+ status_renderable,
44
+ )
45
+ from shikiru.cli.report import report_command
46
+ from shikiru.cli.view import view_command
47
+ from shikiru.config.loader import load_settings
48
+ from shikiru.config.settings import ComplexityName
49
+ from shikiru.domain.tasks import Mode
50
+ from shikiru.errors import (
51
+ EXIT_COMMAND_REJECTED,
52
+ EXIT_GENERIC_ERROR,
53
+ EXIT_USAGE_ERROR,
54
+ ShikiruError,
55
+ UsageError,
56
+ )
57
+ from shikiru.operator.commands import Ack, AckStatus
58
+ from shikiru.operator.lifecycle import (
59
+ CLI_DASH_COMMAND,
60
+ CLI_ENGINE_COMMAND,
61
+ CLI_PALETTE_ORIGIN_PANE_OPTION,
62
+ DISTRIBUTION_NAME,
63
+ PACKAGE_NAME,
64
+ )
65
+ from shikiru.operator.queries import TaskNotFoundError
66
+ from shikiru.operator.sessions import REMOTE_ORIGIN_HOST_ENV_VAR
67
+ from shikiru.paths import project_config_path
68
+ from shikiru.tmux.session import PROJECT_DIR_ENV_VAR
69
+ from shikiru.ui.console import human_console
70
+ from shikiru.ui.tokens import (
71
+ ALERT_COLOR,
72
+ NEUTRAL_BORDER_COLOR,
73
+ PRIMARY_TEXT_COLOR,
74
+ SECONDARY_TEXT_COLOR,
75
+ STEEL_BLUE,
76
+ STYLE_BOLD,
77
+ STYLE_DIM,
78
+ )
79
+ from shikiru.view_log import CLI_VIEW_COMMAND
80
+
81
+ if TYPE_CHECKING:
82
+ from collections.abc import Awaitable
83
+
84
+ from rich.console import RenderableType
85
+ from typer._click.core import Context as ClickContext
86
+
87
+ from shikiru.operator.services import OperatorServices
88
+ from shikiru.operator.sessions import (
89
+ SessionAttacher,
90
+ SessionLister,
91
+ SessionListing,
92
+ )
93
+
94
+ PROJECT_OPTION = typer.Option(
95
+ "--project",
96
+ help=("Select a project directory. [bold]Default:[/] the nearest initialized project."),
97
+ )
98
+ JSON_OPTION = typer.Option(
99
+ "--json", help="Print machine-readable JSON. [bold]Default:[/] human-readable output."
100
+ )
101
+ OVERRIDES_OPTION = typer.Option(
102
+ "--set",
103
+ help=("Override config with key.path=value; repeat as needed. [bold]Default:[/] no overrides."),
104
+ )
105
+ ATTACH_OPTION = typer.Option(
106
+ "--attach",
107
+ help=(
108
+ "Attach to the project session after starting or reusing the engine. "
109
+ "[bold]Default:[/] stay detached."
110
+ ),
111
+ )
112
+ FORCE_OPTION = typer.Option(
113
+ "--force",
114
+ help=(
115
+ "Restart even while tasks run, which may interrupt their turns. "
116
+ "[bold]Default:[/] refuse while a task is running."
117
+ ),
118
+ )
119
+ KILL_SESSION_OPTION = typer.Option(
120
+ "--kill-session",
121
+ help=(
122
+ "Close the project session even if the engine is stopped. "
123
+ "[bold]Default:[/] keep the session."
124
+ ),
125
+ )
126
+ WATCH_OPTION = typer.Option(
127
+ "--watch", help="Refresh the report when project state changes. [bold]Default:[/] one report."
128
+ )
129
+ PROBE_OPTION = typer.Option(
130
+ "--probe",
131
+ help=(
132
+ "Run live contract and smoke checks for configured role rungs. "
133
+ "[bold]Default:[/] skip live agent probes."
134
+ ),
135
+ )
136
+ REQUIRED_LEGEND = f"[bold {ALERT_COLOR}]*[/] marks a required argument."
137
+ SUBMIT_REQUEST_ARGUMENT_NAME = "request"
138
+
139
+
140
+ def _configure_typer_help_palette() -> None:
141
+ typer_rich_utils.STYLE_OPTION = f"{STYLE_BOLD} {STEEL_BLUE}"
142
+ typer_rich_utils.STYLE_SWITCH = f"{STYLE_BOLD} {STEEL_BLUE}"
143
+ typer_rich_utils.STYLE_NEGATIVE_OPTION = f"{STYLE_BOLD} {ALERT_COLOR}"
144
+ typer_rich_utils.STYLE_NEGATIVE_SWITCH = f"{STYLE_BOLD} {ALERT_COLOR}"
145
+ typer_rich_utils.STYLE_TYPES = SECONDARY_TEXT_COLOR
146
+ typer_rich_utils.STYLE_TYPES_SEPARATOR = f"{STYLE_DIM} {SECONDARY_TEXT_COLOR}"
147
+ typer_rich_utils.STYLE_USAGE = STEEL_BLUE
148
+ typer_rich_utils.STYLE_USAGE_COMMAND = f"{STYLE_BOLD} {PRIMARY_TEXT_COLOR}"
149
+ typer_rich_utils.STYLE_DEPRECATED = ALERT_COLOR
150
+ typer_rich_utils.STYLE_DEPRECATED_COMMAND = f"{STYLE_DIM} {SECONDARY_TEXT_COLOR}"
151
+ typer_rich_utils.STYLE_HELPTEXT_FIRST_LINE = PRIMARY_TEXT_COLOR
152
+ typer_rich_utils.STYLE_HELPTEXT = f"{STYLE_DIM} {SECONDARY_TEXT_COLOR}"
153
+ typer_rich_utils.STYLE_OPTION_HELP = PRIMARY_TEXT_COLOR
154
+ typer_rich_utils.STYLE_OPTION_DEFAULT = f"{STYLE_DIM} {SECONDARY_TEXT_COLOR}"
155
+ typer_rich_utils.STYLE_OPTION_ENVVAR = f"{STYLE_DIM} {SECONDARY_TEXT_COLOR}"
156
+ typer_rich_utils.STYLE_REQUIRED_SHORT = ALERT_COLOR
157
+ typer_rich_utils.STYLE_REQUIRED_LONG = f"{STYLE_DIM} {ALERT_COLOR}"
158
+ typer_rich_utils.STYLE_OPTIONS_PANEL_BORDER = f"{STYLE_DIM} {NEUTRAL_BORDER_COLOR}"
159
+ typer_rich_utils.STYLE_COMMANDS_PANEL_BORDER = f"{STYLE_DIM} {NEUTRAL_BORDER_COLOR}"
160
+ typer_rich_utils.ARGUMENTS_PANEL_TITLE = f"[bold not dim {PRIMARY_TEXT_COLOR}]Arguments[/]"
161
+ typer_rich_utils.OPTIONS_PANEL_TITLE = f"[bold not dim {PRIMARY_TEXT_COLOR}]Options[/]"
162
+ typer_rich_utils.COMMANDS_PANEL_TITLE = f"[bold not dim {PRIMARY_TEXT_COLOR}]Commands[/]"
163
+ typer_rich_utils.REQUIRED_LONG_STRING = ""
164
+ typer_rich_utils.STYLE_COMMANDS_TABLE_FIRST_COLUMN = f"{STYLE_BOLD} {STEEL_BLUE}"
165
+ typer_rich_utils.STYLE_ERRORS_PANEL_BORDER = ALERT_COLOR
166
+ typer_rich_utils.STYLE_ERRORS_SUGGESTION = f"{STYLE_DIM} {SECONDARY_TEXT_COLOR}"
167
+ typer_rich_utils.STYLE_ABORTED = ALERT_COLOR
168
+
169
+
170
+ _configure_typer_help_palette()
171
+
172
+
173
+ class SubmitCommand(TyperCommand):
174
+ def parse_args(self, ctx: ClickContext, args: list[str]) -> list[str]:
175
+ try:
176
+ return super().parse_args(ctx, args)
177
+ except MissingParameter as error:
178
+ if error.param is None or error.param.name != SUBMIT_REQUEST_ARGUMENT_NAME:
179
+ raise
180
+ state = ctx.find_root().obj
181
+ if not isinstance(state, CliState) or state.json_output:
182
+ raise
183
+ human_console(stderr=True).print(missing_submit_request_renderable())
184
+ raise typer.Exit(code=EXIT_USAGE_ERROR) from None
185
+
186
+
187
+ app = typer.Typer(name=PACKAGE_NAME, no_args_is_help=True)
188
+ app.command(name="report", hidden=True)(report_command)
189
+ app.command(name=CLI_VIEW_COMMAND, hidden=True)(view_command)
190
+
191
+
192
+ def _print_version(*, show: bool) -> None:
193
+ if show:
194
+ typer.echo(version(DISTRIBUTION_NAME))
195
+ raise typer.Exit
196
+
197
+
198
+ @app.callback()
199
+ def main(
200
+ context: typer.Context,
201
+ *,
202
+ _version_flag: Annotated[
203
+ bool,
204
+ typer.Option(
205
+ "--version",
206
+ callback=_print_version,
207
+ is_eager=True,
208
+ help="Show the installed version and exit.",
209
+ ),
210
+ ] = False,
211
+ project: Annotated[Path | None, PROJECT_OPTION] = None,
212
+ json_output: Annotated[bool, JSON_OPTION] = False,
213
+ overrides: Annotated[list[str] | None, OVERRIDES_OPTION] = None,
214
+ ) -> None:
215
+ """Shikiru: run coding-agent tasks in tmux panes."""
216
+ del _version_flag
217
+ context.obj = CliState(project, json_output, tuple(overrides or ()))
218
+
219
+
220
+ @app.command()
221
+ def init(
222
+ context: typer.Context,
223
+ *,
224
+ yes: Annotated[
225
+ bool,
226
+ typer.Option(
227
+ "--yes",
228
+ help=(
229
+ "Write detected defaults without opening the setup wizard. "
230
+ "[bold]Default:[/] use the wizard."
231
+ ),
232
+ ),
233
+ ] = False,
234
+ ) -> None:
235
+ """Initialize a project from detected repositories, plugins and trackers."""
236
+ state = state_from(context)
237
+ root = state.project.resolve() if state.project is not None else Path.cwd().resolve()
238
+ wizard_result = None
239
+ try:
240
+ if yes:
241
+ config_path = _run(build_project_detectors(root).initialize())
242
+ else:
243
+ wizard_result = _run(run_init_wizard(root))
244
+ if wizard_result is None:
245
+ return
246
+ config_path = wizard_result.config_path
247
+ except ShikiruError as error:
248
+ _fail(error)
249
+ value = {"config": str(config_path)}
250
+ wizard_failed = wizard_result is not None and (
251
+ wizard_result.doctor_error is not None
252
+ or wizard_result.doctor_report is None
253
+ or not wizard_result.doctor_report.ok
254
+ )
255
+ if not state.json_output:
256
+ console = human_console(stderr=wizard_failed)
257
+ renderable = (
258
+ init_renderable(
259
+ config_path,
260
+ console=console,
261
+ include_placeholder_warning=yes,
262
+ )
263
+ if wizard_result is None
264
+ else init_wizard_result_renderable(wizard_result, console=console)
265
+ )
266
+ console.print(renderable)
267
+ if wizard_failed:
268
+ raise typer.Exit(code=EXIT_GENERIC_ERROR)
269
+ if state.json_output:
270
+ _emit_value(
271
+ state,
272
+ value,
273
+ f"initialized {config_path}",
274
+ )
275
+
276
+
277
+ @app.command()
278
+ def start(
279
+ context: typer.Context,
280
+ *,
281
+ attach: Annotated[bool, ATTACH_OPTION] = False,
282
+ ) -> None:
283
+ """Validate config, start the detached engine and ensure its tmux session."""
284
+ try:
285
+ state = state_from(context)
286
+ root = project_root(state)
287
+ result = _run(
288
+ build_lifecycle().start(
289
+ root,
290
+ state.overrides,
291
+ attach=attach,
292
+ inside_tmux=bool(os.environ.get("TMUX")),
293
+ current_pane=os.environ.get("TMUX_PANE"),
294
+ )
295
+ )
296
+ except ShikiruError as error:
297
+ _fail(error)
298
+ _emit_value(state, result, start_renderable(result))
299
+
300
+
301
+ @app.command()
302
+ def stop(
303
+ context: typer.Context,
304
+ *,
305
+ kill_session: Annotated[bool, KILL_SESSION_OPTION] = False,
306
+ ) -> None:
307
+ """Queue an orderly engine stop, optionally closing its tmux session."""
308
+ try:
309
+ state = state_from(context)
310
+ root = project_root(state)
311
+ result = _run(build_lifecycle().stop(root, state.overrides, kill_session=kill_session))
312
+ except ShikiruError as error:
313
+ _fail(error)
314
+ emit_ack(state, result, stopping=True)
315
+
316
+
317
+ @app.command()
318
+ def restart(
319
+ context: typer.Context,
320
+ *,
321
+ force: Annotated[bool, FORCE_OPTION] = False,
322
+ ) -> None:
323
+ """Stop the project's engine if one runs, then start it again."""
324
+ try:
325
+ state = state_from(context)
326
+ root = project_root(state)
327
+ result = _run(build_lifecycle().restart(root, state.overrides, force=force))
328
+ except ShikiruError as error:
329
+ _fail(error)
330
+ _emit_value(state, result, restart_renderable(result))
331
+
332
+
333
+ @app.command(name="ls")
334
+ def list_sessions(
335
+ context: typer.Context,
336
+ *,
337
+ host: Annotated[
338
+ str | None,
339
+ typer.Option(
340
+ "--host", help="List sessions on this SSH host instead. [bold]Default:[/] this host."
341
+ ),
342
+ ] = None,
343
+ json_output: Annotated[bool, JSON_OPTION] = False,
344
+ ) -> None:
345
+ """List Shikiru project sessions visible on this host or an SSH host."""
346
+ try:
347
+ state = state_from(context)
348
+ lister, _ = build_session_tools(state.overrides)
349
+ listing = _run(lister.list_sessions(host))
350
+ except ShikiruError as error:
351
+ _fail(error)
352
+ if state.json_output or json_output:
353
+ typer.echo(json.dumps([row.model_dump(mode="json") for row in listing.rows]))
354
+ else:
355
+ console = human_console()
356
+ console.print(
357
+ session_rows_renderable(
358
+ list(listing.rows),
359
+ host=host,
360
+ console=console,
361
+ unavailable=bool(listing.diagnostics or listing.remote_exit_code),
362
+ )
363
+ )
364
+ _print_session_diagnostics(listing, host)
365
+ if listing.diagnostics or listing.remote_exit_code:
366
+ raise typer.Exit(code=EXIT_GENERIC_ERROR)
367
+
368
+
369
+ @app.command()
370
+ def attach(
371
+ context: typer.Context,
372
+ name: Annotated[
373
+ str | None,
374
+ typer.Argument(
375
+ help=(
376
+ "Project or full session name to attach to; omit to use the current project "
377
+ "or choose from a numbered list."
378
+ )
379
+ ),
380
+ ] = None,
381
+ *,
382
+ host: Annotated[
383
+ str | None,
384
+ typer.Option(
385
+ "--host", help="Attach over SSH to this host instead. [bold]Default:[/] this host."
386
+ ),
387
+ ] = None,
388
+ ) -> None:
389
+ """Attach to a project session or choose one from a numbered terminal list."""
390
+ try:
391
+ state = state_from(context)
392
+ lister, attacher = build_session_tools(state.overrides)
393
+ _run(_attach_from_cli(lister, attacher, state, name=name, host=host))
394
+ except ShikiruError as error:
395
+ _fail(error)
396
+
397
+
398
+ def _attach_project_root(state: CliState) -> Path | None:
399
+ if state.project is not None:
400
+ return project_root(state)
401
+ root = Path.cwd().resolve()
402
+ while True:
403
+ if project_config_path(root).is_file():
404
+ return root
405
+ if root.parent == root:
406
+ return None
407
+ root = root.parent
408
+
409
+
410
+ async def _attach_from_cli(
411
+ lister: SessionLister,
412
+ attacher: SessionAttacher,
413
+ state: CliState,
414
+ *,
415
+ name: str | None,
416
+ host: str | None,
417
+ ) -> None:
418
+ if host is not None and name is not None:
419
+ await attacher.attach(
420
+ (),
421
+ name=name,
422
+ host=host,
423
+ inside_tmux=bool(os.environ.get("TMUX")),
424
+ current_pane=os.environ.get("TMUX_PANE"),
425
+ )
426
+ return
427
+ listing, project_root = await _attach_listing(lister, state, name=name, host=host)
428
+ _print_session_diagnostics(listing, host)
429
+ if not listing.rows:
430
+ if listing.diagnostics:
431
+ raise typer.Exit(code=EXIT_USAGE_ERROR)
432
+ if listing.remote_exit_code:
433
+ raise typer.Exit(code=listing.remote_exit_code)
434
+ if project_root is not None:
435
+ message = (
436
+ f"no Shikiru session exists for project {project_root}; "
437
+ "run `shikiru --project PATH start --attach` to create and attach it"
438
+ )
439
+ raise UsageError(message)
440
+ if project_root is not None and len(listing.rows) > 1:
441
+ message = (
442
+ f"multiple Shikiru sessions belong to project {project_root}; "
443
+ "choose one with `shikiru attach NAME`"
444
+ )
445
+ raise UsageError(message)
446
+ await attacher.attach(
447
+ listing.rows,
448
+ name=name,
449
+ host=host,
450
+ inside_tmux=bool(os.environ.get("TMUX")),
451
+ current_pane=os.environ.get("TMUX_PANE"),
452
+ )
453
+
454
+
455
+ async def _attach_listing(
456
+ lister: SessionLister,
457
+ state: CliState,
458
+ *,
459
+ name: str | None,
460
+ host: str | None,
461
+ ) -> tuple[SessionListing, Path | None]:
462
+ if host is not None or name is not None:
463
+ return await lister.list_sessions(host, session_name=name if host is None else None), None
464
+ root = _attach_project_root(state)
465
+ if root is None:
466
+ return await lister.list_sessions(), None
467
+ listing = await lister.list_sessions(project_dir=root)
468
+ return listing, root
469
+
470
+
471
+ def _print_session_diagnostics(listing: SessionListing, host: str | None) -> None:
472
+ if listing.diagnostics:
473
+ human_console(stderr=True).print(
474
+ session_diagnostics_renderable(
475
+ listing.diagnostics,
476
+ remote_host=os.environ.get(REMOTE_ORIGIN_HOST_ENV_VAR),
477
+ )
478
+ )
479
+ if listing.remote_diagnostics:
480
+ typer.echo(
481
+ f"Session diagnostics from remote host {host!r}; "
482
+ "run any listed repair commands on that host:",
483
+ err=True,
484
+ )
485
+ typer.echo(listing.remote_diagnostics, err=True, nl=False)
486
+ if not listing.remote_diagnostics.endswith("\n"):
487
+ typer.echo(err=True)
488
+
489
+
490
+ @app.command(cls=SubmitCommand, epilog=REQUIRED_LEGEND)
491
+ def submit(
492
+ context: typer.Context,
493
+ *,
494
+ request: Annotated[str, typer.Argument(help="Task for triage and implementation.")],
495
+ mode: Annotated[
496
+ Mode | None,
497
+ typer.Option(
498
+ "--mode",
499
+ help="Choose a task mode. [bold]Default:[/] the Leader decides during triage.",
500
+ ),
501
+ ] = None,
502
+ complexity: Annotated[
503
+ ComplexityName | None,
504
+ typer.Option(
505
+ "--complexity",
506
+ help="Set task complexity. [bold]Default:[/] triage decides.",
507
+ ),
508
+ ] = None,
509
+ priority: Annotated[
510
+ int,
511
+ typer.Option(
512
+ "--priority",
513
+ help="Higher values dispatch sooner. [bold]Default:[/] no priority (0).",
514
+ show_default=False,
515
+ ),
516
+ ] = 0,
517
+ depends_on: Annotated[
518
+ list[str] | None,
519
+ typer.Option(
520
+ "--after",
521
+ metavar="TASK_ID",
522
+ help="Require earlier tasks first; repeat. [bold]Default:[/] no dependencies.",
523
+ ),
524
+ ] = None,
525
+ leader: Annotated[
526
+ str | None,
527
+ typer.Option(
528
+ "--leader",
529
+ metavar="RUNG",
530
+ help="Pin a rung using agent:model\\[:effort]. [bold]Default:[/] the project ladder.",
531
+ ),
532
+ ] = None,
533
+ implementer: Annotated[
534
+ str | None,
535
+ typer.Option(
536
+ "--implementer",
537
+ metavar="RUNG",
538
+ help="Pin a rung using agent:model\\[:effort]. [bold]Default:[/] the project ladder.",
539
+ ),
540
+ ] = None,
541
+ ) -> None:
542
+ """Queue one task and print its allocated `T-n` id."""
543
+ try:
544
+ state = state_from(context)
545
+ root = project_root(state)
546
+ settings = load_settings(root, state.overrides)
547
+ services = build_operator(root, settings)
548
+ try:
549
+ ack = _run(
550
+ services.commands.submit_task(
551
+ request,
552
+ mode=mode,
553
+ complexity=complexity,
554
+ priority=priority,
555
+ depends_on=depends_on or (),
556
+ leader=parse_rung(leader) if leader is not None else None,
557
+ implementer=parse_rung(implementer) if implementer is not None else None,
558
+ )
559
+ )
560
+ finally:
561
+ services.close()
562
+ except ShikiruError as error:
563
+ _fail(error)
564
+ task_id = ack.result.get("task_id") if ack.result else None
565
+ if not state.json_output:
566
+ emit_ack(state, ack)
567
+ elif task_id is not None:
568
+ _emit_value(state, {"task_id": task_id, "command_id": ack.id, "status": ack.status}, "")
569
+ else:
570
+ emit_ack(state, ack)
571
+
572
+
573
+ @app.command()
574
+ def status(
575
+ context: typer.Context,
576
+ *,
577
+ task_id: Annotated[
578
+ str | None, typer.Argument(help="Task ID to show; omit for project status.")
579
+ ] = None,
580
+ watch: Annotated[bool, WATCH_OPTION] = False,
581
+ ) -> None:
582
+ """Show engine, queue, task, escalation, usage and quota status."""
583
+ try:
584
+ state = state_from(context)
585
+ root = project_root(state)
586
+ settings = load_settings(root, state.overrides)
587
+ services = build_operator(root, settings)
588
+ try:
589
+ _run(
590
+ _status_loop(
591
+ services,
592
+ state,
593
+ task_id,
594
+ watch,
595
+ project_root_path=root,
596
+ quota_warn_pct=settings.dashboard.gauges.quota_warn_pct,
597
+ quota_alert_pct=settings.dashboard.gauges.quota_alert_pct,
598
+ )
599
+ )
600
+ finally:
601
+ services.close()
602
+ except ShikiruError as error:
603
+ _fail(error)
604
+
605
+
606
+ @app.command()
607
+ def doctor(
608
+ context: typer.Context,
609
+ *,
610
+ probe: Annotated[bool, PROBE_OPTION] = False,
611
+ ) -> None:
612
+ """Check the environment, project configuration and configured agent plugins."""
613
+ try:
614
+ state = state_from(context)
615
+ root = project_root(state)
616
+ report = _run(build_doctor(root).run(state.overrides, probe=probe))
617
+ except ShikiruError as error:
618
+ _fail(error)
619
+ if state.json_output:
620
+ typer.echo(report.model_dump_json())
621
+ else:
622
+ console = human_console()
623
+ console.print(doctor_renderable(report, width=console.size.width))
624
+ if not report.ok:
625
+ raise typer.Exit(code=EXIT_GENERIC_ERROR)
626
+
627
+
628
+ @app.command(name=CLI_ENGINE_COMMAND, hidden=True)
629
+ def engine(context: typer.Context) -> None:
630
+ """Run the engine daemon in the current process."""
631
+ try:
632
+ state = state_from(context)
633
+ root = project_root(state)
634
+ process = build_engine_process(root, load_settings(root, state.overrides))
635
+ _run(process.run())
636
+ except ShikiruError as error:
637
+ _fail(error)
638
+
639
+
640
+ @app.command(name=CLI_DASH_COMMAND, hidden=True)
641
+ def dash(
642
+ context: typer.Context,
643
+ *,
644
+ home: Annotated[bool, typer.Option("--home")] = False,
645
+ task: Annotated[str | None, typer.Option("--task")] = None,
646
+ ) -> None:
647
+ """Run the Textual dashboard process in a tmux pane."""
648
+ try:
649
+ state = state_from(context)
650
+ root = project_root(state)
651
+ if home == (task is not None):
652
+ message = "dash requires exactly one of --home or --task"
653
+ raise UsageError(message)
654
+ _run(run_dashboard(root, state.overrides, None if home else task))
655
+ except ShikiruError as error:
656
+ _fail(error)
657
+
658
+
659
+ @app.command(hidden=True)
660
+ def palette(
661
+ context: typer.Context,
662
+ *,
663
+ origin_pane: Annotated[
664
+ str | None,
665
+ typer.Option(CLI_PALETTE_ORIGIN_PANE_OPTION, hidden=True),
666
+ ] = None,
667
+ ) -> None:
668
+ """Run the operator palette for the invoking Shikiru tmux session."""
669
+ try:
670
+ state = state_from(context)
671
+ project = state.project
672
+ if project is None:
673
+ session_project = os.environ.get(PROJECT_DIR_ENV_VAR)
674
+ project = Path(session_project) if session_project is not None else None
675
+ root = project_root(CliState(project, state.json_output, state.overrides))
676
+ _run(run_palette(root, state.overrides, origin_pane))
677
+ except ShikiruError as error:
678
+ _fail(error)
679
+
680
+
681
+ @app.command(name="mcp")
682
+ def mcp(context: typer.Context) -> None:
683
+ """Run the FastMCP stdio server for this project."""
684
+ try:
685
+ state = state_from(context)
686
+ root = project_root(state)
687
+ server, services = build_mcp_server(root, state.overrides)
688
+ try:
689
+ server.run(transport="stdio")
690
+ finally:
691
+ services.close()
692
+ except ShikiruError as error:
693
+ _fail(error)
694
+
695
+
696
+ async def _status_loop(
697
+ services: OperatorServices,
698
+ state: CliState,
699
+ task_id: str | None,
700
+ watch: bool,
701
+ *,
702
+ project_root_path: Path,
703
+ quota_warn_pct: int,
704
+ quota_alert_pct: int,
705
+ ) -> None:
706
+ def render() -> None:
707
+ if state.json_output:
708
+ view = services.queries.status(task_id)
709
+ if task_id is not None and view.task is None:
710
+ raise TaskNotFoundError(task_id, project_root_path)
711
+ typer.echo(view.model_dump_json())
712
+ else:
713
+ view = services.queries.human_status(task_id)
714
+ console = human_console()
715
+ uptime_seconds = (
716
+ services.queries.current_engine_uptime_seconds()
717
+ if view.status.task is None
718
+ else None
719
+ )
720
+ console.print(
721
+ status_renderable(
722
+ view,
723
+ width=console.width,
724
+ uptime_seconds=uptime_seconds,
725
+ quota_warn_pct=quota_warn_pct,
726
+ quota_alert_pct=quota_alert_pct,
727
+ )
728
+ )
729
+
730
+ render()
731
+ if watch:
732
+
733
+ async def on_change() -> None:
734
+ render()
735
+
736
+ await services.reader.watch(on_change)
737
+
738
+
739
+ def emit_ack(
740
+ state: CliState,
741
+ ack: Ack,
742
+ *,
743
+ stopping: bool = False,
744
+ ) -> None:
745
+ """Render an acknowledgement and map rejected commands to their exit code."""
746
+ if state.json_output:
747
+ typer.echo(ack.model_dump_json())
748
+ else:
749
+ human_console().print(ack_renderable(ack, stopping=stopping))
750
+ if ack.status is AckStatus.REJECTED:
751
+ raise typer.Exit(code=EXIT_COMMAND_REJECTED)
752
+
753
+
754
+ def _emit_value(state: CliState, value: object, human: str | RenderableType) -> None:
755
+ if state.json_output:
756
+ if isinstance(value, BaseModel):
757
+ typer.echo(value.model_dump_json())
758
+ else:
759
+ typer.echo(json.dumps(value, default=str))
760
+ elif human:
761
+ human_console().print(human)
762
+
763
+
764
+ def _run[T](operation: Awaitable[T]) -> T:
765
+ return asyncio.run(operation)
766
+
767
+
768
+ def _fail(error: ShikiruError) -> NoReturn:
769
+ human_console(stderr=True).print(error_renderable(error))
770
+ raise typer.Exit(code=error.exit_code)