shikiru-cli 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (319) hide show
  1. shikiru/__init__.py +1 -0
  2. shikiru/__main__.py +8 -0
  3. shikiru/agents/__init__.py +3 -0
  4. shikiru/agents/contracts.py +590 -0
  5. shikiru/agents/registry.py +87 -0
  6. shikiru/agents/sdk/__init__.py +17 -0
  7. shikiru/agents/sdk/reset_time.py +137 -0
  8. shikiru/agents/sdk/turn.py +335 -0
  9. shikiru/agents/settings.py +151 -0
  10. shikiru/agents/testing/__init__.py +26 -0
  11. shikiru/agents/testing/contract_suite.py +653 -0
  12. shikiru/agents/testing/resume.py +15 -0
  13. shikiru/bootstrap.py +1277 -0
  14. shikiru/cli/__init__.py +1 -0
  15. shikiru/cli/app.py +770 -0
  16. shikiru/cli/context.py +63 -0
  17. shikiru/cli/render.py +1475 -0
  18. shikiru/cli/report.py +156 -0
  19. shikiru/cli/session_prompt.py +63 -0
  20. shikiru/cli/view.py +131 -0
  21. shikiru/clock.py +41 -0
  22. shikiru/config/__init__.py +8 -0
  23. shikiru/config/defaults.toml +131 -0
  24. shikiru/config/loader.py +178 -0
  25. shikiru/config/settings.py +392 -0
  26. shikiru/dashboard/__init__.py +3 -0
  27. shikiru/dashboard/actions_list.py +799 -0
  28. shikiru/dashboard/activity.py +150 -0
  29. shikiru/dashboard/app.py +1765 -0
  30. shikiru/dashboard/blocker_evidence.py +122 -0
  31. shikiru/dashboard/blocker_rows.py +84 -0
  32. shikiru/dashboard/briefing_view.py +871 -0
  33. shikiru/dashboard/contract.py +1086 -0
  34. shikiru/dashboard/covered.py +32 -0
  35. shikiru/dashboard/detail.py +1209 -0
  36. shikiru/dashboard/detail_fold.py +244 -0
  37. shikiru/dashboard/detail_hint.py +68 -0
  38. shikiru/dashboard/details_list_view.py +150 -0
  39. shikiru/dashboard/diagnostics.py +80 -0
  40. shikiru/dashboard/diagnostics_sections.py +186 -0
  41. shikiru/dashboard/evidence_table.py +123 -0
  42. shikiru/dashboard/extra_details.py +214 -0
  43. shikiru/dashboard/fault_guidance.py +216 -0
  44. shikiru/dashboard/fold_bodies.py +276 -0
  45. shikiru/dashboard/help_keys.py +166 -0
  46. shikiru/dashboard/help_screen.py +204 -0
  47. shikiru/dashboard/interaction.py +17 -0
  48. shikiru/dashboard/ladder_editor.py +544 -0
  49. shikiru/dashboard/live_settings.py +62 -0
  50. shikiru/dashboard/motion.py +368 -0
  51. shikiru/dashboard/orchestration_fields.py +201 -0
  52. shikiru/dashboard/palette.py +1606 -0
  53. shikiru/dashboard/palette_app.py +89 -0
  54. shikiru/dashboard/palette_drafts.py +33 -0
  55. shikiru/dashboard/resolution.py +573 -0
  56. shikiru/dashboard/resolution_view.py +302 -0
  57. shikiru/dashboard/row_list.py +256 -0
  58. shikiru/dashboard/section_rows.py +173 -0
  59. shikiru/dashboard/services.py +665 -0
  60. shikiru/dashboard/session_history.py +402 -0
  61. shikiru/dashboard/shikiru_init_ascii_art.txt +40 -0
  62. shikiru/dashboard/task_picker.py +358 -0
  63. shikiru/dashboard/tick.py +59 -0
  64. shikiru/dashboard/timeline_filter.py +129 -0
  65. shikiru/dashboard/timeline_section.py +385 -0
  66. shikiru/dashboard/usage_body.py +269 -0
  67. shikiru/dashboard/views.py +2314 -0
  68. shikiru/dashboard/wizard.py +1389 -0
  69. shikiru/domain/__init__.py +1 -0
  70. shikiru/domain/acceptance.py +130 -0
  71. shikiru/domain/activity.py +28 -0
  72. shikiru/domain/attempts.py +333 -0
  73. shikiru/domain/briefing.py +257 -0
  74. shikiru/domain/commands.py +219 -0
  75. shikiru/domain/decisions.py +231 -0
  76. shikiru/domain/docker.py +262 -0
  77. shikiru/domain/engine_uptime.py +87 -0
  78. shikiru/domain/escalation.py +194 -0
  79. shikiru/domain/escalation_evidence.py +186 -0
  80. shikiru/domain/escalation_panel.py +144 -0
  81. shikiru/domain/events.py +71 -0
  82. shikiru/domain/evidence_export.py +89 -0
  83. shikiru/domain/faults.py +86 -0
  84. shikiru/domain/feedback.py +180 -0
  85. shikiru/domain/feedback_rounds.py +172 -0
  86. shikiru/domain/fencing.py +31 -0
  87. shikiru/domain/ladder.py +102 -0
  88. shikiru/domain/ladder_text.py +44 -0
  89. shikiru/domain/levels.py +44 -0
  90. shikiru/domain/motion.py +199 -0
  91. shikiru/domain/packet_evidence.py +377 -0
  92. shikiru/domain/pushes.py +47 -0
  93. shikiru/domain/quota_blocks.py +44 -0
  94. shikiru/domain/quota_waits.py +219 -0
  95. shikiru/domain/recovery.py +194 -0
  96. shikiru/domain/reports.py +498 -0
  97. shikiru/domain/results.py +217 -0
  98. shikiru/domain/runs.py +85 -0
  99. shikiru/domain/scheduling.py +231 -0
  100. shikiru/domain/status_tasks.py +40 -0
  101. shikiru/domain/steps.py +325 -0
  102. shikiru/domain/tasks.py +174 -0
  103. shikiru/domain/timeline.py +208 -0
  104. shikiru/domain/timeline_phrases.py +271 -0
  105. shikiru/domain/versions.py +18 -0
  106. shikiru/domain/workflow.py +372 -0
  107. shikiru/engine/__init__.py +0 -0
  108. shikiru/engine/acceptance.py +90 -0
  109. shikiru/engine/agents.py +61 -0
  110. shikiru/engine/cosmetic_decorator.py +99 -0
  111. shikiru/engine/daemon.py +409 -0
  112. shikiru/engine/dependency.py +37 -0
  113. shikiru/engine/escalation_filing.py +110 -0
  114. shikiru/engine/escalations.py +401 -0
  115. shikiru/engine/events.py +285 -0
  116. shikiru/engine/evidence.py +81 -0
  117. shikiru/engine/fault_reporter.py +144 -0
  118. shikiru/engine/feedback.py +577 -0
  119. shikiru/engine/feedback_audit.py +80 -0
  120. shikiru/engine/feedback_intake.py +95 -0
  121. shikiru/engine/feedback_snapshot.py +148 -0
  122. shikiru/engine/flow.py +87 -0
  123. shikiru/engine/handlers/__init__.py +33 -0
  124. shikiru/engine/handlers/base.py +148 -0
  125. shikiru/engine/handlers/engine.py +24 -0
  126. shikiru/engine/handlers/escalations.py +174 -0
  127. shikiru/engine/handlers/operator.py +81 -0
  128. shikiru/engine/handlers/tasks.py +237 -0
  129. shikiru/engine/history.py +128 -0
  130. shikiru/engine/integration.py +1234 -0
  131. shikiru/engine/integration_turns.py +337 -0
  132. shikiru/engine/layout_guard.py +362 -0
  133. shikiru/engine/lifecycle.py +60 -0
  134. shikiru/engine/monitor.py +317 -0
  135. shikiru/engine/notification_delivery.py +124 -0
  136. shikiru/engine/notification_feed.py +57 -0
  137. shikiru/engine/notifier.py +91 -0
  138. shikiru/engine/pull_request_closing.py +117 -0
  139. shikiru/engine/push_service.py +177 -0
  140. shikiru/engine/quota_probe_threads.py +79 -0
  141. shikiru/engine/quota_reconciler.py +210 -0
  142. shikiru/engine/quota_schedule.py +24 -0
  143. shikiru/engine/quota_waits.py +102 -0
  144. shikiru/engine/reconcile.py +91 -0
  145. shikiru/engine/recovery.py +284 -0
  146. shikiru/engine/retention.py +102 -0
  147. shikiru/engine/root_preparation.py +86 -0
  148. shikiru/engine/scheduler.py +320 -0
  149. shikiru/engine/session_guard.py +46 -0
  150. shikiru/engine/sessions.py +95 -0
  151. shikiru/engine/signalled.py +75 -0
  152. shikiru/engine/step_runner.py +140 -0
  153. shikiru/engine/supervisor.py +101 -0
  154. shikiru/engine/takeover.py +223 -0
  155. shikiru/engine/task_facts.py +46 -0
  156. shikiru/engine/tick_guard.py +95 -0
  157. shikiru/engine/titles.py +297 -0
  158. shikiru/engine/transitions.py +79 -0
  159. shikiru/engine/turn_context.py +206 -0
  160. shikiru/engine/turn_execution.py +30 -0
  161. shikiru/engine/turn_loop.py +230 -0
  162. shikiru/engine/turn_processes.py +79 -0
  163. shikiru/engine/turns.py +457 -0
  164. shikiru/errors.py +144 -0
  165. shikiru/helper/__init__.py +3 -0
  166. shikiru/helper/helper.py +182 -0
  167. shikiru/helper/models.py +11 -0
  168. shikiru/mcp/__init__.py +1 -0
  169. shikiru/mcp/server.py +196 -0
  170. shikiru/operator/__init__.py +33 -0
  171. shikiru/operator/attachment.py +24 -0
  172. shikiru/operator/commands.py +319 -0
  173. shikiru/operator/detectors.py +374 -0
  174. shikiru/operator/doctor.py +351 -0
  175. shikiru/operator/events.py +57 -0
  176. shikiru/operator/evidence_export.py +498 -0
  177. shikiru/operator/evidence_writer.py +78 -0
  178. shikiru/operator/lifecycle.py +294 -0
  179. shikiru/operator/plugin_checks.py +194 -0
  180. shikiru/operator/probe.py +287 -0
  181. shikiru/operator/queries.py +1601 -0
  182. shikiru/operator/services.py +36 -0
  183. shikiru/operator/sessions.py +378 -0
  184. shikiru/operator/strip_views.py +162 -0
  185. shikiru/operator/timeline.py +416 -0
  186. shikiru/operator/toml_text.py +62 -0
  187. shikiru/palette.py +18 -0
  188. shikiru/paths.py +91 -0
  189. shikiru/plugins/__init__.py +0 -0
  190. shikiru/plugins/agents/__init__.py +1 -0
  191. shikiru/plugins/agents/claude/__init__.py +5 -0
  192. shikiru/plugins/agents/claude/agent.py +251 -0
  193. shikiru/plugins/agents/claude/context.py +116 -0
  194. shikiru/plugins/agents/claude/headless.py +92 -0
  195. shikiru/plugins/agents/claude/hook.py +57 -0
  196. shikiru/plugins/agents/claude/model_catalog.py +105 -0
  197. shikiru/plugins/agents/claude/options.py +62 -0
  198. shikiru/plugins/agents/claude/turn.py +1018 -0
  199. shikiru/plugins/agents/codex/__init__.py +5 -0
  200. shikiru/plugins/agents/codex/agent.py +486 -0
  201. shikiru/plugins/agents/codex/headless.py +166 -0
  202. shikiru/plugins/agents/codex/home.py +14 -0
  203. shikiru/plugins/agents/codex/model_catalog.py +128 -0
  204. shikiru/plugins/agents/codex/options.py +94 -0
  205. shikiru/plugins/agents/codex/quota.py +195 -0
  206. shikiru/plugins/agents/codex/sandbox.py +31 -0
  207. shikiru/plugins/agents/codex/sandbox_probe.py +97 -0
  208. shikiru/plugins/agents/codex/turn.py +767 -0
  209. shikiru/plugins/agents/codex/usage.py +106 -0
  210. shikiru/plugins/agents/shared_settings.py +62 -0
  211. shikiru/plugins/widgets/__init__.py +3 -0
  212. shikiru/plugins/widgets/dialogue.py +144 -0
  213. shikiru/plugins/widgets/disk.py +123 -0
  214. shikiru/plugins/widgets/docker.py +225 -0
  215. shikiru/plugins/widgets/escalations.py +402 -0
  216. shikiru/plugins/widgets/faults.py +204 -0
  217. shikiru/plugins/widgets/header.py +321 -0
  218. shikiru/plugins/widgets/queue.py +1140 -0
  219. shikiru/plugins/widgets/queue_hint.py +93 -0
  220. shikiru/plugins/widgets/resources.py +105 -0
  221. shikiru/plugins/widgets/strips.py +82 -0
  222. shikiru/plugins/widgets/usage.py +176 -0
  223. shikiru/private.py +94 -0
  224. shikiru/process.py +535 -0
  225. shikiru/prompts/__init__.py +3 -0
  226. shikiru/prompts/renderer.py +85 -0
  227. shikiru/prompts/templates/_base.md.j2 +17 -0
  228. shikiru/prompts/templates/_macros.md.j2 +61 -0
  229. shikiru/prompts/templates/conflicts.md.j2 +22 -0
  230. shikiru/prompts/templates/consult.md.j2 +5 -0
  231. shikiru/prompts/templates/continue.md.j2 +4 -0
  232. shikiru/prompts/templates/feedback_analysis.md.j2 +29 -0
  233. shikiru/prompts/templates/feedback_rework.md.j2 +33 -0
  234. shikiru/prompts/templates/feedback_verify.md.j2 +33 -0
  235. shikiru/prompts/templates/handoff.md.j2 +23 -0
  236. shikiru/prompts/templates/implement.md.j2 +13 -0
  237. shikiru/prompts/templates/plan.md.j2 +30 -0
  238. shikiru/prompts/templates/rework.md.j2 +13 -0
  239. shikiru/prompts/templates/triage.md.j2 +20 -0
  240. shikiru/prompts/templates/verify.md.j2 +29 -0
  241. shikiru/store/__init__.py +1 -0
  242. shikiru/store/commands.py +242 -0
  243. shikiru/store/database.py +297 -0
  244. shikiru/store/escalations.py +240 -0
  245. shikiru/store/events.py +213 -0
  246. shikiru/store/feedback.py +266 -0
  247. shikiru/store/migrations/001_initial.sql +63 -0
  248. shikiru/store/migrations/002_headless_turns.sql +31 -0
  249. shikiru/store/migrations/003_drop_command_handled_at.sql +1 -0
  250. shikiru/store/migrations/004_repo_base_ref.sql +3 -0
  251. shikiru/store/migrations/005_complexity_explicit.sql +3 -0
  252. shikiru/store/migrations/006_events_kind_index.sql +1 -0
  253. shikiru/store/migrations/007_notification_cursor.sql +1 -0
  254. shikiru/store/migrations/008_reported_usage.sql +1 -0
  255. shikiru/store/migrations/009_billing_declarations.sql +2 -0
  256. shikiru/store/migrations/010_canonical_quota_windows.sql +3 -0
  257. shikiru/store/migrations/012_drop_task_quota_evidence.sql +2 -0
  258. shikiru/store/migrations/013_push_targets.sql +7 -0
  259. shikiru/store/migrations/014_agent_block_time.sql +6 -0
  260. shikiru/store/migrations/015_repair_orphan_agent_blocks.sql +3 -0
  261. shikiru/store/migrations/016_feedback_rounds.sql +29 -0
  262. shikiru/store/migrations/017_feedback_round_comments.sql +5 -0
  263. shikiru/store/migrations/__init__.py +1 -0
  264. shikiru/store/notification_cursor.py +29 -0
  265. shikiru/store/pushes.py +117 -0
  266. shikiru/store/quotas.py +153 -0
  267. shikiru/store/reader.py +310 -0
  268. shikiru/store/retention.py +92 -0
  269. shikiru/store/runs.py +227 -0
  270. shikiru/store/tasks.py +949 -0
  271. shikiru/telemetry/__init__.py +1 -0
  272. shikiru/telemetry/disk.py +167 -0
  273. shikiru/telemetry/docker.py +164 -0
  274. shikiru/telemetry/resources.py +296 -0
  275. shikiru/tmux/__init__.py +3 -0
  276. shikiru/tmux/client.py +637 -0
  277. shikiru/tmux/decorator.py +225 -0
  278. shikiru/tmux/layout.py +200 -0
  279. shikiru/tmux/session.py +381 -0
  280. shikiru/ui/__init__.py +9 -0
  281. shikiru/ui/age.py +45 -0
  282. shikiru/ui/bubbles.py +41 -0
  283. shikiru/ui/compact.py +1209 -0
  284. shikiru/ui/console.py +49 -0
  285. shikiru/ui/fold.py +50 -0
  286. shikiru/ui/gated_static.py +117 -0
  287. shikiru/ui/gauge.py +40 -0
  288. shikiru/ui/glyphs.py +109 -0
  289. shikiru/ui/keys.py +57 -0
  290. shikiru/ui/live_interval.py +42 -0
  291. shikiru/ui/popup.py +311 -0
  292. shikiru/ui/quota_status.py +26 -0
  293. shikiru/ui/recent_window.py +78 -0
  294. shikiru/ui/screen.py +57 -0
  295. shikiru/ui/section.py +193 -0
  296. shikiru/ui/session.py +65 -0
  297. shikiru/ui/size.py +17 -0
  298. shikiru/ui/splash.py +42 -0
  299. shikiru/ui/status.py +47 -0
  300. shikiru/ui/status_strip.py +149 -0
  301. shikiru/ui/stream.py +152 -0
  302. shikiru/ui/strip.py +332 -0
  303. shikiru/ui/theme.py +83 -0
  304. shikiru/ui/tokens.py +51 -0
  305. shikiru/view_log.py +126 -0
  306. shikiru/workspace/__init__.py +3 -0
  307. shikiru/workspace/base_refs.py +160 -0
  308. shikiru/workspace/caches.py +136 -0
  309. shikiru/workspace/facts.py +118 -0
  310. shikiru/workspace/git.py +601 -0
  311. shikiru/workspace/github.py +744 -0
  312. shikiru/workspace/messages.py +107 -0
  313. shikiru/workspace/reports.py +145 -0
  314. shikiru/workspace/roots.py +180 -0
  315. shikiru_cli-0.2.0.dist-info/METADATA +428 -0
  316. shikiru_cli-0.2.0.dist-info/RECORD +319 -0
  317. shikiru_cli-0.2.0.dist-info/WHEEL +4 -0
  318. shikiru_cli-0.2.0.dist-info/entry_points.txt +18 -0
  319. shikiru_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,122 @@
1
+ """The evidence disclosure of a task's Blockers: quota readings, notes and raw events."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from rich.console import Group
8
+ from rich.table import Table
9
+
10
+ from shikiru.clock import to_local
11
+ from shikiru.domain.levels import displayed_pct
12
+ from shikiru.domain.quota_waits import (
13
+ QUOTA_NOT_REPORTED,
14
+ provider_reset_text,
15
+ quota_window_label,
16
+ )
17
+ from shikiru.ui.age import AGE_NOW, format_short_age
18
+ from shikiru.ui.section import grey
19
+ from shikiru.ui.tokens import SECONDARY_TEXT_COLOR
20
+
21
+ if TYPE_CHECKING:
22
+ from collections.abc import Sequence
23
+ from datetime import datetime, timedelta
24
+
25
+ from rich.console import RenderableType
26
+ from rich.text import Text
27
+
28
+ from shikiru.dashboard.contract import BlockerEvidenceView, QuotaEvidenceView, QuotaReadingView
29
+
30
+ _OBSERVED_PREFIX = "observed"
31
+ _AGO_SUFFIX = "ago"
32
+ _JUST_NOW = "just now"
33
+ _DIFFERENT_TIMES = "at different times"
34
+ _RANGE_SEPARATOR = " to "
35
+ _WINDOW_HEADER = "Window"
36
+ _USED_HEADER = "Used"
37
+ _RESET_HEADER = "Provider reset"
38
+ _NO_READING = "No provider quota reading is recorded."
39
+ _NO_CURRENT_READING = "No current provider quota reading is available."
40
+ _COLUMN_GAP = 2
41
+
42
+
43
+ def evidence_headline(evidence: BlockerEvidenceView, now: datetime) -> str | None:
44
+ """Return how old the observations are: one age, or the span of differing ages."""
45
+ ages = sorted(
46
+ [now - reading.observed_at for quota in evidence.quotas for reading in quota.readings]
47
+ + [now - observed for observed in evidence.observed_at]
48
+ )
49
+ if not ages:
50
+ return None
51
+ newest, oldest = ages[0], ages[-1]
52
+ newest_age = _observed_age(newest)
53
+ oldest_age = _observed_age(oldest)
54
+ if newest == oldest:
55
+ return f"{_OBSERVED_PREFIX} {newest_age}"
56
+ if newest_age == oldest_age:
57
+ return f"{_OBSERVED_PREFIX} {_DIFFERENT_TIMES} ({oldest_age})"
58
+ return f"{_OBSERVED_PREFIX} {newest_age}{_RANGE_SEPARATOR}{oldest_age}"
59
+
60
+
61
+ def evidence_body(evidence: BlockerEvidenceView, now: datetime) -> RenderableType:
62
+ """Return the normal evidence: each provider's readings and reset, then the notes.
63
+
64
+ A provider label heads each group only when more than one provider is blocked.
65
+ """
66
+ labelled = len(evidence.quotas) > 1
67
+ parts: list[RenderableType] = []
68
+ for quota in evidence.quotas:
69
+ if labelled:
70
+ parts.append(grey(quota.agent))
71
+ parts.append(_quota_block(quota, to_local(now)))
72
+ parts.extend(grey(note) for note in evidence.notes)
73
+ return Group(*parts)
74
+
75
+
76
+ def raw_evidence_text(evidence: BlockerEvidenceView) -> Text:
77
+ """Return the raw events in grey: exact timestamps, event references and diagnostics."""
78
+ return grey("\n".join(evidence.raw))
79
+
80
+
81
+ def _quota_block(quota: QuotaEvidenceView, now: datetime) -> RenderableType:
82
+ if quota.readings:
83
+ parts: list[RenderableType] = [_readings_table(quota.readings, now)]
84
+ elif quota.has_recorded_readings:
85
+ parts = [grey(_NO_CURRENT_READING)]
86
+ else:
87
+ parts = [grey(_NO_READING)]
88
+ if quota.retry_note is not None:
89
+ parts.append(grey(quota.retry_note))
90
+ return Group(*parts)
91
+
92
+
93
+ def _readings_table(readings: Sequence[QuotaReadingView], now: datetime) -> Table:
94
+ table = Table(
95
+ box=None,
96
+ show_edge=False,
97
+ pad_edge=False,
98
+ padding=(0, _COLUMN_GAP, 0, 0),
99
+ header_style=SECONDARY_TEXT_COLOR,
100
+ style=SECONDARY_TEXT_COLOR,
101
+ )
102
+ table.add_column(_WINDOW_HEADER)
103
+ table.add_column(_USED_HEADER, justify="right")
104
+ table.add_column(_RESET_HEADER)
105
+ for reading in readings:
106
+ table.add_row(
107
+ quota_window_label(reading.window).capitalize(),
108
+ f"{displayed_pct(reading.used_pct)}%",
109
+ _reset_text(reading.resets_at, now),
110
+ )
111
+ return table
112
+
113
+
114
+ def _reset_text(resets_at: datetime | None, now: datetime) -> str:
115
+ if resets_at is None:
116
+ return QUOTA_NOT_REPORTED
117
+ return provider_reset_text(to_local(resets_at), now)
118
+
119
+
120
+ def _observed_age(age: timedelta) -> str:
121
+ short = format_short_age(age)
122
+ return _JUST_NOW if short == AGE_NOW else f"{short} {_AGO_SUFFIX}"
@@ -0,0 +1,84 @@
1
+ """The Blockers rows of the briefing's Problems group: each cause, next step and occupants."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from rich.cells import cell_len
8
+ from rich.text import Text
9
+
10
+ from shikiru.clock import to_local
11
+ from shikiru.domain.quota_waits import quota_clock_text
12
+ from shikiru.domain.scheduling import WaitReason
13
+ from shikiru.domain.tasks import Status
14
+ from shikiru.ui.glyphs import GLYPH_WARNING
15
+ from shikiru.ui.section import Row, grey
16
+ from shikiru.ui.tokens import ALERT_COLOR, AMBER, SECONDARY_TEXT_COLOR, STYLE_BOLD
17
+
18
+ if TYPE_CHECKING:
19
+ from datetime import datetime
20
+
21
+ from shikiru.dashboard.contract import TaskBlockerView, TaskDetailView, TaskReferenceView
22
+
23
+ BLOCKERS_LABEL = "Blockers"
24
+ _BOLD_AMBER = f"{STYLE_BOLD} {AMBER}"
25
+ _BOLD_ALERT = f"{STYLE_BOLD} {ALERT_COLOR}"
26
+ _COLUMN_GAP = " "
27
+
28
+
29
+ def blocker_rows(view: TaskDetailView, now: datetime) -> list[Row]:
30
+ """Return the Blockers rows: each blocker's warning headline, then its grey detail lines.
31
+
32
+ The label sits on the first row only, and every line lines up under the value column. A quota
33
+ wait's clock is worded for `now`, so the countdown moves with the clock. A blocker waiting on a
34
+ cancelled task turns the alert colour.
35
+ """
36
+ rows: list[Row] = []
37
+ for index, blocker in enumerate(view.blockers):
38
+ alert = _waits_on_cancelled(view, blocker)
39
+ label = grey(BLOCKERS_LABEL) if index == 0 else Text()
40
+ rows.append((label, (_headline(blocker, now, alert),)))
41
+ rows.extend((Text(), (grey(line),)) for line in blocker.details)
42
+ rows.extend((Text(), (line,)) for line in _occupant_lines(blocker.related_tasks))
43
+ return rows
44
+
45
+
46
+ def _headline(blocker: TaskBlockerView, now: datetime, alert: bool) -> Text:
47
+ cause = blocker.headline
48
+ if blocker.quota_wait is not None:
49
+ quota = blocker.quota_wait
50
+ description, remaining = quota_clock_text(quota.agent, to_local(quota.until), to_local(now))
51
+ cause = f"{description} (in {remaining})"
52
+ return Text.assemble(
53
+ (GLYPH_WARNING, _BOLD_ALERT if alert else _BOLD_AMBER),
54
+ (f" {cause}", ALERT_COLOR if alert else ""),
55
+ )
56
+
57
+
58
+ def _occupant_lines(tasks: tuple[TaskReferenceView, ...]) -> list[Text]:
59
+ id_width = max((cell_len(task.task_id) for task in tasks), default=0)
60
+ title_width = max((cell_len(task.title) for task in tasks), default=0)
61
+ return [
62
+ Text(
63
+ f"{_pad_cells(task.task_id, id_width)}{_COLUMN_GAP}"
64
+ f"{_pad_cells(task.title, title_width)}{_COLUMN_GAP}{task.status}",
65
+ style=ALERT_COLOR if task.status == Status.CANCELLED.value else SECONDARY_TEXT_COLOR,
66
+ )
67
+ for task in tasks
68
+ ]
69
+
70
+
71
+ def _pad_cells(text: str, width: int) -> str:
72
+ return f"{text}{' ' * (width - cell_len(text))}"
73
+
74
+
75
+ def _waits_on_cancelled(view: TaskDetailView, blocker: TaskBlockerView) -> bool:
76
+ if blocker.kind == WaitReason.DEPENDENCY.value:
77
+ if view.relationships is None:
78
+ return False
79
+ return _any_cancelled(view.relationships.blocking_dependencies)
80
+ return _any_cancelled(blocker.related_tasks)
81
+
82
+
83
+ def _any_cancelled(tasks: tuple[TaskReferenceView, ...]) -> bool:
84
+ return any(task.status == Status.CANCELLED.value for task in tasks)