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.
- shikiru/__init__.py +1 -0
- shikiru/__main__.py +8 -0
- shikiru/agents/__init__.py +3 -0
- shikiru/agents/contracts.py +590 -0
- shikiru/agents/registry.py +87 -0
- shikiru/agents/sdk/__init__.py +17 -0
- shikiru/agents/sdk/reset_time.py +137 -0
- shikiru/agents/sdk/turn.py +335 -0
- shikiru/agents/settings.py +151 -0
- shikiru/agents/testing/__init__.py +26 -0
- shikiru/agents/testing/contract_suite.py +653 -0
- shikiru/agents/testing/resume.py +15 -0
- shikiru/bootstrap.py +1277 -0
- shikiru/cli/__init__.py +1 -0
- shikiru/cli/app.py +770 -0
- shikiru/cli/context.py +63 -0
- shikiru/cli/render.py +1475 -0
- shikiru/cli/report.py +156 -0
- shikiru/cli/session_prompt.py +63 -0
- shikiru/cli/view.py +131 -0
- shikiru/clock.py +41 -0
- shikiru/config/__init__.py +8 -0
- shikiru/config/defaults.toml +131 -0
- shikiru/config/loader.py +178 -0
- shikiru/config/settings.py +392 -0
- shikiru/dashboard/__init__.py +3 -0
- shikiru/dashboard/actions_list.py +799 -0
- shikiru/dashboard/activity.py +150 -0
- shikiru/dashboard/app.py +1765 -0
- shikiru/dashboard/blocker_evidence.py +122 -0
- shikiru/dashboard/blocker_rows.py +84 -0
- shikiru/dashboard/briefing_view.py +871 -0
- shikiru/dashboard/contract.py +1086 -0
- shikiru/dashboard/covered.py +32 -0
- shikiru/dashboard/detail.py +1209 -0
- shikiru/dashboard/detail_fold.py +244 -0
- shikiru/dashboard/detail_hint.py +68 -0
- shikiru/dashboard/details_list_view.py +150 -0
- shikiru/dashboard/diagnostics.py +80 -0
- shikiru/dashboard/diagnostics_sections.py +186 -0
- shikiru/dashboard/evidence_table.py +123 -0
- shikiru/dashboard/extra_details.py +214 -0
- shikiru/dashboard/fault_guidance.py +216 -0
- shikiru/dashboard/fold_bodies.py +276 -0
- shikiru/dashboard/help_keys.py +166 -0
- shikiru/dashboard/help_screen.py +204 -0
- shikiru/dashboard/interaction.py +17 -0
- shikiru/dashboard/ladder_editor.py +544 -0
- shikiru/dashboard/live_settings.py +62 -0
- shikiru/dashboard/motion.py +368 -0
- shikiru/dashboard/orchestration_fields.py +201 -0
- shikiru/dashboard/palette.py +1606 -0
- shikiru/dashboard/palette_app.py +89 -0
- shikiru/dashboard/palette_drafts.py +33 -0
- shikiru/dashboard/resolution.py +573 -0
- shikiru/dashboard/resolution_view.py +302 -0
- shikiru/dashboard/row_list.py +256 -0
- shikiru/dashboard/section_rows.py +173 -0
- shikiru/dashboard/services.py +665 -0
- shikiru/dashboard/session_history.py +402 -0
- shikiru/dashboard/shikiru_init_ascii_art.txt +40 -0
- shikiru/dashboard/task_picker.py +358 -0
- shikiru/dashboard/tick.py +59 -0
- shikiru/dashboard/timeline_filter.py +129 -0
- shikiru/dashboard/timeline_section.py +385 -0
- shikiru/dashboard/usage_body.py +269 -0
- shikiru/dashboard/views.py +2314 -0
- shikiru/dashboard/wizard.py +1389 -0
- shikiru/domain/__init__.py +1 -0
- shikiru/domain/acceptance.py +130 -0
- shikiru/domain/activity.py +28 -0
- shikiru/domain/attempts.py +333 -0
- shikiru/domain/briefing.py +257 -0
- shikiru/domain/commands.py +219 -0
- shikiru/domain/decisions.py +231 -0
- shikiru/domain/docker.py +262 -0
- shikiru/domain/engine_uptime.py +87 -0
- shikiru/domain/escalation.py +194 -0
- shikiru/domain/escalation_evidence.py +186 -0
- shikiru/domain/escalation_panel.py +144 -0
- shikiru/domain/events.py +71 -0
- shikiru/domain/evidence_export.py +89 -0
- shikiru/domain/faults.py +86 -0
- shikiru/domain/feedback.py +180 -0
- shikiru/domain/feedback_rounds.py +172 -0
- shikiru/domain/fencing.py +31 -0
- shikiru/domain/ladder.py +102 -0
- shikiru/domain/ladder_text.py +44 -0
- shikiru/domain/levels.py +44 -0
- shikiru/domain/motion.py +199 -0
- shikiru/domain/packet_evidence.py +377 -0
- shikiru/domain/pushes.py +47 -0
- shikiru/domain/quota_blocks.py +44 -0
- shikiru/domain/quota_waits.py +219 -0
- shikiru/domain/recovery.py +194 -0
- shikiru/domain/reports.py +498 -0
- shikiru/domain/results.py +217 -0
- shikiru/domain/runs.py +85 -0
- shikiru/domain/scheduling.py +231 -0
- shikiru/domain/status_tasks.py +40 -0
- shikiru/domain/steps.py +325 -0
- shikiru/domain/tasks.py +174 -0
- shikiru/domain/timeline.py +208 -0
- shikiru/domain/timeline_phrases.py +271 -0
- shikiru/domain/versions.py +18 -0
- shikiru/domain/workflow.py +372 -0
- shikiru/engine/__init__.py +0 -0
- shikiru/engine/acceptance.py +90 -0
- shikiru/engine/agents.py +61 -0
- shikiru/engine/cosmetic_decorator.py +99 -0
- shikiru/engine/daemon.py +409 -0
- shikiru/engine/dependency.py +37 -0
- shikiru/engine/escalation_filing.py +110 -0
- shikiru/engine/escalations.py +401 -0
- shikiru/engine/events.py +285 -0
- shikiru/engine/evidence.py +81 -0
- shikiru/engine/fault_reporter.py +144 -0
- shikiru/engine/feedback.py +577 -0
- shikiru/engine/feedback_audit.py +80 -0
- shikiru/engine/feedback_intake.py +95 -0
- shikiru/engine/feedback_snapshot.py +148 -0
- shikiru/engine/flow.py +87 -0
- shikiru/engine/handlers/__init__.py +33 -0
- shikiru/engine/handlers/base.py +148 -0
- shikiru/engine/handlers/engine.py +24 -0
- shikiru/engine/handlers/escalations.py +174 -0
- shikiru/engine/handlers/operator.py +81 -0
- shikiru/engine/handlers/tasks.py +237 -0
- shikiru/engine/history.py +128 -0
- shikiru/engine/integration.py +1234 -0
- shikiru/engine/integration_turns.py +337 -0
- shikiru/engine/layout_guard.py +362 -0
- shikiru/engine/lifecycle.py +60 -0
- shikiru/engine/monitor.py +317 -0
- shikiru/engine/notification_delivery.py +124 -0
- shikiru/engine/notification_feed.py +57 -0
- shikiru/engine/notifier.py +91 -0
- shikiru/engine/pull_request_closing.py +117 -0
- shikiru/engine/push_service.py +177 -0
- shikiru/engine/quota_probe_threads.py +79 -0
- shikiru/engine/quota_reconciler.py +210 -0
- shikiru/engine/quota_schedule.py +24 -0
- shikiru/engine/quota_waits.py +102 -0
- shikiru/engine/reconcile.py +91 -0
- shikiru/engine/recovery.py +284 -0
- shikiru/engine/retention.py +102 -0
- shikiru/engine/root_preparation.py +86 -0
- shikiru/engine/scheduler.py +320 -0
- shikiru/engine/session_guard.py +46 -0
- shikiru/engine/sessions.py +95 -0
- shikiru/engine/signalled.py +75 -0
- shikiru/engine/step_runner.py +140 -0
- shikiru/engine/supervisor.py +101 -0
- shikiru/engine/takeover.py +223 -0
- shikiru/engine/task_facts.py +46 -0
- shikiru/engine/tick_guard.py +95 -0
- shikiru/engine/titles.py +297 -0
- shikiru/engine/transitions.py +79 -0
- shikiru/engine/turn_context.py +206 -0
- shikiru/engine/turn_execution.py +30 -0
- shikiru/engine/turn_loop.py +230 -0
- shikiru/engine/turn_processes.py +79 -0
- shikiru/engine/turns.py +457 -0
- shikiru/errors.py +144 -0
- shikiru/helper/__init__.py +3 -0
- shikiru/helper/helper.py +182 -0
- shikiru/helper/models.py +11 -0
- shikiru/mcp/__init__.py +1 -0
- shikiru/mcp/server.py +196 -0
- shikiru/operator/__init__.py +33 -0
- shikiru/operator/attachment.py +24 -0
- shikiru/operator/commands.py +319 -0
- shikiru/operator/detectors.py +374 -0
- shikiru/operator/doctor.py +351 -0
- shikiru/operator/events.py +57 -0
- shikiru/operator/evidence_export.py +498 -0
- shikiru/operator/evidence_writer.py +78 -0
- shikiru/operator/lifecycle.py +294 -0
- shikiru/operator/plugin_checks.py +194 -0
- shikiru/operator/probe.py +287 -0
- shikiru/operator/queries.py +1601 -0
- shikiru/operator/services.py +36 -0
- shikiru/operator/sessions.py +378 -0
- shikiru/operator/strip_views.py +162 -0
- shikiru/operator/timeline.py +416 -0
- shikiru/operator/toml_text.py +62 -0
- shikiru/palette.py +18 -0
- shikiru/paths.py +91 -0
- shikiru/plugins/__init__.py +0 -0
- shikiru/plugins/agents/__init__.py +1 -0
- shikiru/plugins/agents/claude/__init__.py +5 -0
- shikiru/plugins/agents/claude/agent.py +251 -0
- shikiru/plugins/agents/claude/context.py +116 -0
- shikiru/plugins/agents/claude/headless.py +92 -0
- shikiru/plugins/agents/claude/hook.py +57 -0
- shikiru/plugins/agents/claude/model_catalog.py +105 -0
- shikiru/plugins/agents/claude/options.py +62 -0
- shikiru/plugins/agents/claude/turn.py +1018 -0
- shikiru/plugins/agents/codex/__init__.py +5 -0
- shikiru/plugins/agents/codex/agent.py +486 -0
- shikiru/plugins/agents/codex/headless.py +166 -0
- shikiru/plugins/agents/codex/home.py +14 -0
- shikiru/plugins/agents/codex/model_catalog.py +128 -0
- shikiru/plugins/agents/codex/options.py +94 -0
- shikiru/plugins/agents/codex/quota.py +195 -0
- shikiru/plugins/agents/codex/sandbox.py +31 -0
- shikiru/plugins/agents/codex/sandbox_probe.py +97 -0
- shikiru/plugins/agents/codex/turn.py +767 -0
- shikiru/plugins/agents/codex/usage.py +106 -0
- shikiru/plugins/agents/shared_settings.py +62 -0
- shikiru/plugins/widgets/__init__.py +3 -0
- shikiru/plugins/widgets/dialogue.py +144 -0
- shikiru/plugins/widgets/disk.py +123 -0
- shikiru/plugins/widgets/docker.py +225 -0
- shikiru/plugins/widgets/escalations.py +402 -0
- shikiru/plugins/widgets/faults.py +204 -0
- shikiru/plugins/widgets/header.py +321 -0
- shikiru/plugins/widgets/queue.py +1140 -0
- shikiru/plugins/widgets/queue_hint.py +93 -0
- shikiru/plugins/widgets/resources.py +105 -0
- shikiru/plugins/widgets/strips.py +82 -0
- shikiru/plugins/widgets/usage.py +176 -0
- shikiru/private.py +94 -0
- shikiru/process.py +535 -0
- shikiru/prompts/__init__.py +3 -0
- shikiru/prompts/renderer.py +85 -0
- shikiru/prompts/templates/_base.md.j2 +17 -0
- shikiru/prompts/templates/_macros.md.j2 +61 -0
- shikiru/prompts/templates/conflicts.md.j2 +22 -0
- shikiru/prompts/templates/consult.md.j2 +5 -0
- shikiru/prompts/templates/continue.md.j2 +4 -0
- shikiru/prompts/templates/feedback_analysis.md.j2 +29 -0
- shikiru/prompts/templates/feedback_rework.md.j2 +33 -0
- shikiru/prompts/templates/feedback_verify.md.j2 +33 -0
- shikiru/prompts/templates/handoff.md.j2 +23 -0
- shikiru/prompts/templates/implement.md.j2 +13 -0
- shikiru/prompts/templates/plan.md.j2 +30 -0
- shikiru/prompts/templates/rework.md.j2 +13 -0
- shikiru/prompts/templates/triage.md.j2 +20 -0
- shikiru/prompts/templates/verify.md.j2 +29 -0
- shikiru/store/__init__.py +1 -0
- shikiru/store/commands.py +242 -0
- shikiru/store/database.py +297 -0
- shikiru/store/escalations.py +240 -0
- shikiru/store/events.py +213 -0
- shikiru/store/feedback.py +266 -0
- shikiru/store/migrations/001_initial.sql +63 -0
- shikiru/store/migrations/002_headless_turns.sql +31 -0
- shikiru/store/migrations/003_drop_command_handled_at.sql +1 -0
- shikiru/store/migrations/004_repo_base_ref.sql +3 -0
- shikiru/store/migrations/005_complexity_explicit.sql +3 -0
- shikiru/store/migrations/006_events_kind_index.sql +1 -0
- shikiru/store/migrations/007_notification_cursor.sql +1 -0
- shikiru/store/migrations/008_reported_usage.sql +1 -0
- shikiru/store/migrations/009_billing_declarations.sql +2 -0
- shikiru/store/migrations/010_canonical_quota_windows.sql +3 -0
- shikiru/store/migrations/012_drop_task_quota_evidence.sql +2 -0
- shikiru/store/migrations/013_push_targets.sql +7 -0
- shikiru/store/migrations/014_agent_block_time.sql +6 -0
- shikiru/store/migrations/015_repair_orphan_agent_blocks.sql +3 -0
- shikiru/store/migrations/016_feedback_rounds.sql +29 -0
- shikiru/store/migrations/017_feedback_round_comments.sql +5 -0
- shikiru/store/migrations/__init__.py +1 -0
- shikiru/store/notification_cursor.py +29 -0
- shikiru/store/pushes.py +117 -0
- shikiru/store/quotas.py +153 -0
- shikiru/store/reader.py +310 -0
- shikiru/store/retention.py +92 -0
- shikiru/store/runs.py +227 -0
- shikiru/store/tasks.py +949 -0
- shikiru/telemetry/__init__.py +1 -0
- shikiru/telemetry/disk.py +167 -0
- shikiru/telemetry/docker.py +164 -0
- shikiru/telemetry/resources.py +296 -0
- shikiru/tmux/__init__.py +3 -0
- shikiru/tmux/client.py +637 -0
- shikiru/tmux/decorator.py +225 -0
- shikiru/tmux/layout.py +200 -0
- shikiru/tmux/session.py +381 -0
- shikiru/ui/__init__.py +9 -0
- shikiru/ui/age.py +45 -0
- shikiru/ui/bubbles.py +41 -0
- shikiru/ui/compact.py +1209 -0
- shikiru/ui/console.py +49 -0
- shikiru/ui/fold.py +50 -0
- shikiru/ui/gated_static.py +117 -0
- shikiru/ui/gauge.py +40 -0
- shikiru/ui/glyphs.py +109 -0
- shikiru/ui/keys.py +57 -0
- shikiru/ui/live_interval.py +42 -0
- shikiru/ui/popup.py +311 -0
- shikiru/ui/quota_status.py +26 -0
- shikiru/ui/recent_window.py +78 -0
- shikiru/ui/screen.py +57 -0
- shikiru/ui/section.py +193 -0
- shikiru/ui/session.py +65 -0
- shikiru/ui/size.py +17 -0
- shikiru/ui/splash.py +42 -0
- shikiru/ui/status.py +47 -0
- shikiru/ui/status_strip.py +149 -0
- shikiru/ui/stream.py +152 -0
- shikiru/ui/strip.py +332 -0
- shikiru/ui/theme.py +83 -0
- shikiru/ui/tokens.py +51 -0
- shikiru/view_log.py +126 -0
- shikiru/workspace/__init__.py +3 -0
- shikiru/workspace/base_refs.py +160 -0
- shikiru/workspace/caches.py +136 -0
- shikiru/workspace/facts.py +118 -0
- shikiru/workspace/git.py +601 -0
- shikiru/workspace/github.py +744 -0
- shikiru/workspace/messages.py +107 -0
- shikiru/workspace/reports.py +145 -0
- shikiru/workspace/roots.py +180 -0
- shikiru_cli-0.2.0.dist-info/METADATA +428 -0
- shikiru_cli-0.2.0.dist-info/RECORD +319 -0
- shikiru_cli-0.2.0.dist-info/WHEEL +4 -0
- shikiru_cli-0.2.0.dist-info/entry_points.txt +18 -0
- shikiru_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"""Resolve bounded state and current-turn signal evidence for dashboard frames."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from shikiru.dashboard.tick import TickMemo
|
|
8
|
+
from shikiru.domain.events import EventKind
|
|
9
|
+
from shikiru.domain.motion import (
|
|
10
|
+
MotionStamp,
|
|
11
|
+
MotionStampKind,
|
|
12
|
+
MotionState,
|
|
13
|
+
QuotaMotionState,
|
|
14
|
+
TaskMotionState,
|
|
15
|
+
)
|
|
16
|
+
from shikiru.domain.motion import (
|
|
17
|
+
frame as motion_frame,
|
|
18
|
+
)
|
|
19
|
+
from shikiru.domain.reports import CURRENT_TURN_LINK_NAME
|
|
20
|
+
from shikiru.domain.runs import RunStatus
|
|
21
|
+
from shikiru.domain.steps import ACTING_ROLE, Step
|
|
22
|
+
from shikiru.domain.tasks import Role, Status, Task
|
|
23
|
+
from shikiru.paths import run_dir
|
|
24
|
+
from shikiru.view_log import TURN_DIR_ITERATION_MARKER, TURN_DIR_SEPARATOR, view_log_path
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from datetime import datetime
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
from shikiru.dashboard.activity import RoleViewTail
|
|
31
|
+
from shikiru.dashboard.live_settings import LiveSettings
|
|
32
|
+
from shikiru.store.reader import StateReader
|
|
33
|
+
|
|
34
|
+
_MILLISECONDS_PER_SECOND = 1000
|
|
35
|
+
_ENGINE_DRIVEN_STEPS = (None, Step.INTEGRATE)
|
|
36
|
+
_TASK_EVENT_KINDS = (
|
|
37
|
+
EventKind.STEP_STARTED,
|
|
38
|
+
EventKind.TURN_STARTED,
|
|
39
|
+
EventKind.TURN_COMPLETED,
|
|
40
|
+
EventKind.TURN_FAILED,
|
|
41
|
+
EventKind.TURN_STOPPED,
|
|
42
|
+
EventKind.VERDICT,
|
|
43
|
+
EventKind.INTEGRATION_WAITING,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class DashboardMotion:
|
|
48
|
+
"""Build time-derived frames from cached events and incremental current-turn logs."""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
state: StateReader,
|
|
53
|
+
project_root: Path,
|
|
54
|
+
settings: LiveSettings,
|
|
55
|
+
role_view_tail: RoleViewTail,
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Use the read-only state, configuration and injected role-log tail."""
|
|
58
|
+
self._state = state
|
|
59
|
+
self._project_root = project_root
|
|
60
|
+
self._settings = settings
|
|
61
|
+
self._role_view_tail = role_view_tail
|
|
62
|
+
self._event_version = -1
|
|
63
|
+
self._task_events: dict[str, tuple[Event, ...]] = {}
|
|
64
|
+
self._task_runs: dict[str, tuple[Run, ...]] = {}
|
|
65
|
+
self._merge_events: tuple[Event, ...] = ()
|
|
66
|
+
self._tick_memo = TickMemo(state.data_version)
|
|
67
|
+
|
|
68
|
+
def recent_merges(self, now: datetime) -> list[Task]:
|
|
69
|
+
"""Return completed tasks whose merge stamp is still within its flash window."""
|
|
70
|
+
self._sync_event_cache()
|
|
71
|
+
if not self._settings.current().dashboard.animations:
|
|
72
|
+
return []
|
|
73
|
+
flash_seconds = (
|
|
74
|
+
self._settings.current().dashboard.motion.stamp_flash_milliseconds
|
|
75
|
+
/ _MILLISECONDS_PER_SECOND
|
|
76
|
+
)
|
|
77
|
+
tasks: list[Task] = []
|
|
78
|
+
for event in reversed(self._merge_events):
|
|
79
|
+
age = (now - event.ts).total_seconds()
|
|
80
|
+
if event.task_id is None or age < 0 or age >= flash_seconds:
|
|
81
|
+
continue
|
|
82
|
+
task = self._state.get_task(event.task_id)
|
|
83
|
+
if task is not None and task.status is Status.DONE:
|
|
84
|
+
tasks.append(task)
|
|
85
|
+
return tasks
|
|
86
|
+
|
|
87
|
+
def frames(
|
|
88
|
+
self,
|
|
89
|
+
tasks: tuple[Task, ...],
|
|
90
|
+
now: datetime,
|
|
91
|
+
*,
|
|
92
|
+
quotas: tuple[QuotaMotionState, ...] = (),
|
|
93
|
+
escalations_open: bool = False,
|
|
94
|
+
engine_running: bool = True,
|
|
95
|
+
) -> MotionFrame:
|
|
96
|
+
"""Resolve task, quota and escalation frames at one injected time value."""
|
|
97
|
+
self._sync_event_cache()
|
|
98
|
+
return motion_frame(
|
|
99
|
+
self._motion_state(
|
|
100
|
+
tasks=tuple(self._shared_task_motion(task) for task in tasks),
|
|
101
|
+
quotas=quotas,
|
|
102
|
+
escalations_open=escalations_open,
|
|
103
|
+
engine_running=engine_running,
|
|
104
|
+
),
|
|
105
|
+
now,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def current_cost(self, task: Task) -> float | None:
|
|
109
|
+
"""Return the acting run's latest cost when that run is known."""
|
|
110
|
+
if task.step is None:
|
|
111
|
+
return None
|
|
112
|
+
run = self._run_for_role(task, ACTING_ROLE[task.step])
|
|
113
|
+
return run.usage.cost_usd if run is not None else None
|
|
114
|
+
|
|
115
|
+
def _sync_event_cache(self) -> None:
|
|
116
|
+
version = self._state.data_version()
|
|
117
|
+
if version == self._event_version:
|
|
118
|
+
return
|
|
119
|
+
self._event_version = version
|
|
120
|
+
self._task_events.clear()
|
|
121
|
+
self._task_runs.clear()
|
|
122
|
+
self._merge_events = tuple(
|
|
123
|
+
self._state.latest_events(
|
|
124
|
+
self._settings.current().dashboard.motion.merge_event_limit,
|
|
125
|
+
kinds=(EventKind.TASK_DONE,),
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def _shared_task_motion(self, task: Task) -> TaskMotionState:
|
|
130
|
+
return self._tick_memo.get(task.id, lambda: self._task_motion(task))
|
|
131
|
+
|
|
132
|
+
def _task_motion(self, task: Task) -> TaskMotionState:
|
|
133
|
+
events = self._events_for(task)
|
|
134
|
+
step_started_at, actual_step_started_at, turn_started_at, turn_active, stamp = (
|
|
135
|
+
self._event_timing(task, events)
|
|
136
|
+
)
|
|
137
|
+
(
|
|
138
|
+
last_signal_at,
|
|
139
|
+
last_signal,
|
|
140
|
+
tool_running,
|
|
141
|
+
turn_active,
|
|
142
|
+
last_words,
|
|
143
|
+
) = self._current_activity(task, turn_active, turn_started_at)
|
|
144
|
+
held = self._is_held(task)
|
|
145
|
+
engine_driven = self._engine_driven(task, events)
|
|
146
|
+
stamp = self._merge_stamp(task.id, stamp)
|
|
147
|
+
return TaskMotionState(
|
|
148
|
+
task_id=task.id,
|
|
149
|
+
step_started_at=step_started_at,
|
|
150
|
+
actual_step_started_at=actual_step_started_at,
|
|
151
|
+
turn_started_at=turn_started_at,
|
|
152
|
+
last_signal_at=last_signal_at,
|
|
153
|
+
last_signal=last_signal,
|
|
154
|
+
last_words=last_words,
|
|
155
|
+
tool_running=tool_running,
|
|
156
|
+
turn_active=turn_active,
|
|
157
|
+
engine_driven=engine_driven,
|
|
158
|
+
held=held,
|
|
159
|
+
ended_at=task.finished_at,
|
|
160
|
+
stamp=stamp,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
@staticmethod
|
|
164
|
+
def _engine_driven(task: Task, events: tuple[Event, ...]) -> bool:
|
|
165
|
+
if task.step not in _ENGINE_DRIVEN_STEPS:
|
|
166
|
+
return False
|
|
167
|
+
for event in reversed(events):
|
|
168
|
+
if event.kind is EventKind.INTEGRATION_WAITING:
|
|
169
|
+
return False
|
|
170
|
+
if event.kind is EventKind.STEP_STARTED:
|
|
171
|
+
return True
|
|
172
|
+
return True
|
|
173
|
+
|
|
174
|
+
def _events_for(self, task: Task) -> tuple[Event, ...]:
|
|
175
|
+
events = self._task_events.get(task.id)
|
|
176
|
+
if events is None:
|
|
177
|
+
events = tuple(
|
|
178
|
+
self._state.latest_events(
|
|
179
|
+
self._settings.current().dashboard.motion.task_event_limit,
|
|
180
|
+
kinds=_TASK_EVENT_KINDS,
|
|
181
|
+
task_id=task.id,
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
self._task_events[task.id] = events
|
|
185
|
+
return events
|
|
186
|
+
|
|
187
|
+
@staticmethod
|
|
188
|
+
def _event_timing(
|
|
189
|
+
task: Task, events: tuple[Event, ...]
|
|
190
|
+
) -> tuple[datetime, datetime | None, datetime | None, bool, MotionStamp | None]:
|
|
191
|
+
step_started_at = task.started_at or task.created_at
|
|
192
|
+
actual_step_started_at = None
|
|
193
|
+
turn_started_at = None
|
|
194
|
+
turn_active = False
|
|
195
|
+
stamp: MotionStamp | None = None
|
|
196
|
+
role = ACTING_ROLE.get(task.step) if task.step is not None else None
|
|
197
|
+
latest_stage_entry = next(
|
|
198
|
+
(event for event in reversed(events) if event.kind is EventKind.STEP_STARTED),
|
|
199
|
+
None,
|
|
200
|
+
)
|
|
201
|
+
if latest_stage_entry is not None:
|
|
202
|
+
step_value = latest_stage_entry.data.get("step")
|
|
203
|
+
iteration = latest_stage_entry.data.get("iteration")
|
|
204
|
+
try:
|
|
205
|
+
entry_step = Step(step_value) if isinstance(step_value, str) else None
|
|
206
|
+
except ValueError:
|
|
207
|
+
entry_step = None
|
|
208
|
+
if (
|
|
209
|
+
task.step is not None
|
|
210
|
+
and entry_step is task.step
|
|
211
|
+
and type(iteration) is int
|
|
212
|
+
and iteration == task.iteration
|
|
213
|
+
):
|
|
214
|
+
actual_step_started_at = latest_stage_entry.ts
|
|
215
|
+
step_started_at = latest_stage_entry.ts
|
|
216
|
+
for event in events:
|
|
217
|
+
if event.kind is EventKind.VERDICT:
|
|
218
|
+
stamp = MotionStamp(
|
|
219
|
+
event.ts,
|
|
220
|
+
MotionStampKind.VERDICT,
|
|
221
|
+
str(event.data["decision"]),
|
|
222
|
+
)
|
|
223
|
+
if role is None or task.step is None:
|
|
224
|
+
continue
|
|
225
|
+
matching_turn = (
|
|
226
|
+
event.data.get("step") == task.step.value
|
|
227
|
+
and event.data.get("role") == role.value
|
|
228
|
+
and event.data.get("iteration") == task.iteration
|
|
229
|
+
)
|
|
230
|
+
if event.kind is EventKind.TURN_STARTED and matching_turn:
|
|
231
|
+
turn_started_at = event.ts
|
|
232
|
+
turn_active = True
|
|
233
|
+
elif (
|
|
234
|
+
event.kind in (EventKind.TURN_COMPLETED, EventKind.TURN_STOPPED) and matching_turn
|
|
235
|
+
) or (
|
|
236
|
+
event.kind is EventKind.TURN_FAILED and event.data.get("step") == task.step.value
|
|
237
|
+
):
|
|
238
|
+
turn_started_at = None
|
|
239
|
+
turn_active = False
|
|
240
|
+
return step_started_at, actual_step_started_at, turn_started_at, turn_active, stamp
|
|
241
|
+
|
|
242
|
+
def _current_activity(
|
|
243
|
+
self, task: Task, turn_active: bool, turn_started_at: datetime | None
|
|
244
|
+
) -> tuple[datetime | None, str | None, bool, bool, str | None]:
|
|
245
|
+
role = ACTING_ROLE.get(task.step) if task.step is not None else None
|
|
246
|
+
last_signal_at = None
|
|
247
|
+
last_signal = None
|
|
248
|
+
tool_running = False
|
|
249
|
+
last_words = None
|
|
250
|
+
if role is None:
|
|
251
|
+
return (
|
|
252
|
+
last_signal_at,
|
|
253
|
+
last_signal,
|
|
254
|
+
tool_running,
|
|
255
|
+
False,
|
|
256
|
+
last_words,
|
|
257
|
+
)
|
|
258
|
+
turn_name = self._current_turn_name(task)
|
|
259
|
+
if turn_name is None:
|
|
260
|
+
return (
|
|
261
|
+
last_signal_at,
|
|
262
|
+
last_signal,
|
|
263
|
+
tool_running,
|
|
264
|
+
turn_active,
|
|
265
|
+
last_words,
|
|
266
|
+
)
|
|
267
|
+
activity = self._role_view_tail.read(
|
|
268
|
+
view_log_path(run_dir(self._project_root, task.id), role.value), turn_name
|
|
269
|
+
)
|
|
270
|
+
if activity is None or (
|
|
271
|
+
turn_started_at is not None and activity.last_signal_at < turn_started_at
|
|
272
|
+
):
|
|
273
|
+
return (
|
|
274
|
+
last_signal_at,
|
|
275
|
+
last_signal,
|
|
276
|
+
tool_running,
|
|
277
|
+
turn_active,
|
|
278
|
+
last_words,
|
|
279
|
+
)
|
|
280
|
+
last_signal_at = activity.last_signal_at
|
|
281
|
+
last_signal = activity.last_signal
|
|
282
|
+
tool_running = activity.tool_running
|
|
283
|
+
last_words = activity.last_words
|
|
284
|
+
turn_active = turn_active and not activity.ended
|
|
285
|
+
return (
|
|
286
|
+
last_signal_at,
|
|
287
|
+
last_signal,
|
|
288
|
+
tool_running,
|
|
289
|
+
turn_active,
|
|
290
|
+
last_words,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
def _is_held(self, task: Task) -> bool:
|
|
294
|
+
role = ACTING_ROLE.get(task.step) if task.step is not None else None
|
|
295
|
+
run = self._run_for_role(task, role) if role is not None else None
|
|
296
|
+
held = (
|
|
297
|
+
bool(task.holds)
|
|
298
|
+
or task.wait_reason is not None
|
|
299
|
+
or task.status not in (Status.RUNNING, Status.INTEGRATING)
|
|
300
|
+
)
|
|
301
|
+
if run is not None and run.status is RunStatus.TAKEN_OVER:
|
|
302
|
+
held = True
|
|
303
|
+
return held
|
|
304
|
+
|
|
305
|
+
def _merge_stamp(self, task_id: str, stamp: MotionStamp | None) -> MotionStamp | None:
|
|
306
|
+
merge = next(
|
|
307
|
+
(event for event in reversed(self._merge_events) if event.task_id == task_id), None
|
|
308
|
+
)
|
|
309
|
+
if merge is not None:
|
|
310
|
+
return MotionStamp(merge.ts, MotionStampKind.MERGED)
|
|
311
|
+
return stamp
|
|
312
|
+
|
|
313
|
+
def _current_turn_name(self, task: Task) -> str | None:
|
|
314
|
+
if task.step is None:
|
|
315
|
+
return None
|
|
316
|
+
role = ACTING_ROLE[task.step]
|
|
317
|
+
link = run_dir(self._project_root, task.id) / role.value / CURRENT_TURN_LINK_NAME
|
|
318
|
+
try:
|
|
319
|
+
if not link.is_symlink():
|
|
320
|
+
return None
|
|
321
|
+
turn_name = link.readlink().name
|
|
322
|
+
except FileNotFoundError:
|
|
323
|
+
return None
|
|
324
|
+
expected_suffix = (
|
|
325
|
+
f"{TURN_DIR_SEPARATOR}{task.step.value}{TURN_DIR_ITERATION_MARKER}{task.iteration}"
|
|
326
|
+
)
|
|
327
|
+
return turn_name if turn_name.endswith(expected_suffix) else None
|
|
328
|
+
|
|
329
|
+
def _run_for_role(self, task: Task, role: Role) -> Run | None:
|
|
330
|
+
runs = self._task_runs.get(task.id)
|
|
331
|
+
if runs is None:
|
|
332
|
+
runs = tuple(self._state.list_runs(task.id))
|
|
333
|
+
self._task_runs[task.id] = runs
|
|
334
|
+
return next(
|
|
335
|
+
(
|
|
336
|
+
run
|
|
337
|
+
for run in runs
|
|
338
|
+
if run.role is role and run.status in (RunStatus.LIVE, RunStatus.TAKEN_OVER)
|
|
339
|
+
),
|
|
340
|
+
None,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
def _motion_state(
|
|
344
|
+
self,
|
|
345
|
+
*,
|
|
346
|
+
tasks: tuple[TaskMotionState, ...] = (),
|
|
347
|
+
quotas: tuple[QuotaMotionState, ...] = (),
|
|
348
|
+
escalations_open: bool = False,
|
|
349
|
+
engine_running: bool = True,
|
|
350
|
+
) -> MotionState:
|
|
351
|
+
config = self._settings.current().dashboard.motion
|
|
352
|
+
return MotionState(
|
|
353
|
+
animations=self._settings.current().dashboard.animations,
|
|
354
|
+
timeouts=self._settings.current().orchestration.timeouts,
|
|
355
|
+
refresh_hz=config.refresh_hz,
|
|
356
|
+
stamp_flash_seconds=config.stamp_flash_milliseconds / _MILLISECONDS_PER_SECOND,
|
|
357
|
+
escalation_pulse_hz=config.escalation_pulse_hz,
|
|
358
|
+
tasks=tasks,
|
|
359
|
+
quotas=quotas,
|
|
360
|
+
escalations_open=escalations_open,
|
|
361
|
+
engine_running=engine_running,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
if TYPE_CHECKING:
|
|
366
|
+
from shikiru.domain.events import Event
|
|
367
|
+
from shikiru.domain.motion import MotionFrame
|
|
368
|
+
from shikiru.domain.runs import Run
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""The orchestration inputs of the installation wizard: units, guidance and accepted values."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from shikiru.config.settings import MAX_USAGE_PCT
|
|
10
|
+
from shikiru.errors import ConfigError
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
|
|
15
|
+
from shikiru.config.settings import OrchestrationConfig
|
|
16
|
+
|
|
17
|
+
MINIMUM_PARALLEL = 1
|
|
18
|
+
MINIMUM_ITERATIONS = 1
|
|
19
|
+
MINIMUM_PUSH_INTERVAL = 0
|
|
20
|
+
MINIMUM_QUOTA_WAIT = 0
|
|
21
|
+
MINIMUM_STALL_TIMEOUT = 1
|
|
22
|
+
MINIMUM_TOOL_TIMEOUT = 1
|
|
23
|
+
MINIMUM_CPU_PCT = 0
|
|
24
|
+
MINIMUM_FREE_RAM_GB = 0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class OrchestrationFieldError(ConfigError):
|
|
28
|
+
"""A rejected orchestration value; `input_id` names the wizard input to correct."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, input_id: str, message: str) -> None:
|
|
31
|
+
"""Explain why the value typed into `input_id` cannot be written."""
|
|
32
|
+
super().__init__(message)
|
|
33
|
+
self.input_id = input_id
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True, slots=True)
|
|
37
|
+
class OrchestrationField:
|
|
38
|
+
"""One orchestration input: the setting it edits, its unit, guidance and accepted values."""
|
|
39
|
+
|
|
40
|
+
input_id: str
|
|
41
|
+
label: str
|
|
42
|
+
summary: str
|
|
43
|
+
unit: str
|
|
44
|
+
explanation: str
|
|
45
|
+
default: Callable[[OrchestrationConfig], int | float]
|
|
46
|
+
minimum: float
|
|
47
|
+
maximum: float | None
|
|
48
|
+
whole_number: bool
|
|
49
|
+
|
|
50
|
+
def parse(self, raw: str) -> int | float:
|
|
51
|
+
"""Return the number typed into the input, or raise `OrchestrationFieldError` for it."""
|
|
52
|
+
value = _whole(raw) if self.whole_number else _decimal(raw)
|
|
53
|
+
if value is None or value < self.minimum or self._above_maximum(value):
|
|
54
|
+
raise OrchestrationFieldError(self.input_id, self._rule())
|
|
55
|
+
return value
|
|
56
|
+
|
|
57
|
+
def _above_maximum(self, value: float) -> bool:
|
|
58
|
+
return self.maximum is not None and value > self.maximum
|
|
59
|
+
|
|
60
|
+
def _rule(self) -> str:
|
|
61
|
+
kind = "a whole number" if self.whole_number else "a number"
|
|
62
|
+
if self.maximum is None:
|
|
63
|
+
bounds = f"at least {self.minimum:g}"
|
|
64
|
+
else:
|
|
65
|
+
bounds = f"from {self.minimum:g} to {self.maximum:g}"
|
|
66
|
+
return f"{self.label} must be {kind} {bounds}."
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _whole(raw: str) -> int | None:
|
|
70
|
+
text = raw.strip()
|
|
71
|
+
if not text.isdecimal():
|
|
72
|
+
return None
|
|
73
|
+
return int(text)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _decimal(raw: str) -> float | None:
|
|
77
|
+
try:
|
|
78
|
+
value = float(raw)
|
|
79
|
+
except ValueError:
|
|
80
|
+
return None
|
|
81
|
+
return value if math.isfinite(value) else None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
MAX_PARALLEL = OrchestrationField(
|
|
85
|
+
input_id="max-parallel",
|
|
86
|
+
label="Maximum parallel tasks",
|
|
87
|
+
summary="Max parallel",
|
|
88
|
+
unit="tasks",
|
|
89
|
+
explanation="Task slots the engine fills at once; each running task uses one.",
|
|
90
|
+
default=lambda config: config.max_parallel,
|
|
91
|
+
minimum=MINIMUM_PARALLEL,
|
|
92
|
+
maximum=None,
|
|
93
|
+
whole_number=True,
|
|
94
|
+
)
|
|
95
|
+
MAX_ITERATIONS = OrchestrationField(
|
|
96
|
+
input_id="max-iterations",
|
|
97
|
+
label="Maximum iterations",
|
|
98
|
+
summary="Max iterations",
|
|
99
|
+
unit="passes",
|
|
100
|
+
explanation=(
|
|
101
|
+
"Implement and verify passes per task, the first included; "
|
|
102
|
+
"the pass that reaches this count is the last."
|
|
103
|
+
),
|
|
104
|
+
default=lambda config: config.max_iterations,
|
|
105
|
+
minimum=MINIMUM_ITERATIONS,
|
|
106
|
+
maximum=None,
|
|
107
|
+
whole_number=True,
|
|
108
|
+
)
|
|
109
|
+
PUSH_INTERVAL = OrchestrationField(
|
|
110
|
+
input_id="push-interval",
|
|
111
|
+
label="Push interval",
|
|
112
|
+
summary="Push interval",
|
|
113
|
+
unit="seconds",
|
|
114
|
+
explanation=(
|
|
115
|
+
"How often locally integrated work is pushed to the remote; the default 14400 is "
|
|
116
|
+
"four hours, and 0 pushes after every integration."
|
|
117
|
+
),
|
|
118
|
+
default=lambda config: config.push_interval_seconds,
|
|
119
|
+
minimum=MINIMUM_PUSH_INTERVAL,
|
|
120
|
+
maximum=None,
|
|
121
|
+
whole_number=True,
|
|
122
|
+
)
|
|
123
|
+
MAX_QUOTA_WAIT = OrchestrationField(
|
|
124
|
+
input_id="max-quota-wait",
|
|
125
|
+
label="Maximum quota wait",
|
|
126
|
+
summary="Max quota wait",
|
|
127
|
+
unit="seconds",
|
|
128
|
+
explanation=(
|
|
129
|
+
"The longest wait for an agent's usage quota to reset before the task moves to "
|
|
130
|
+
"another rung or escalates; 0 moves on at once."
|
|
131
|
+
),
|
|
132
|
+
default=lambda config: config.timeouts.max_quota_wait,
|
|
133
|
+
minimum=MINIMUM_QUOTA_WAIT,
|
|
134
|
+
maximum=None,
|
|
135
|
+
whole_number=True,
|
|
136
|
+
)
|
|
137
|
+
STALL_TIMEOUT = OrchestrationField(
|
|
138
|
+
input_id="stall-timeout",
|
|
139
|
+
label="Stall timeout",
|
|
140
|
+
summary="Stall timeout",
|
|
141
|
+
unit="seconds",
|
|
142
|
+
explanation=(
|
|
143
|
+
"How long an agent may stay silent while no tool is running before its turn "
|
|
144
|
+
"is stopped as stalled."
|
|
145
|
+
),
|
|
146
|
+
default=lambda config: config.timeouts.stall,
|
|
147
|
+
minimum=MINIMUM_STALL_TIMEOUT,
|
|
148
|
+
maximum=None,
|
|
149
|
+
whole_number=True,
|
|
150
|
+
)
|
|
151
|
+
TOOL_TIMEOUT = OrchestrationField(
|
|
152
|
+
input_id="tool-timeout",
|
|
153
|
+
label="Tool timeout",
|
|
154
|
+
summary="Tool timeout",
|
|
155
|
+
unit="seconds",
|
|
156
|
+
explanation=(
|
|
157
|
+
"How long an agent may stay silent while a tool is running before its turn "
|
|
158
|
+
"is stopped as stalled."
|
|
159
|
+
),
|
|
160
|
+
default=lambda config: config.timeouts.tool,
|
|
161
|
+
minimum=MINIMUM_TOOL_TIMEOUT,
|
|
162
|
+
maximum=None,
|
|
163
|
+
whole_number=True,
|
|
164
|
+
)
|
|
165
|
+
MAX_CPU = OrchestrationField(
|
|
166
|
+
input_id="max-cpu",
|
|
167
|
+
label="Maximum CPU use",
|
|
168
|
+
summary="Max CPU",
|
|
169
|
+
unit="percent",
|
|
170
|
+
explanation=(
|
|
171
|
+
"New tasks start only while host CPU use is below this; 0 holds every new start, "
|
|
172
|
+
"100 holds only a fully busy host."
|
|
173
|
+
),
|
|
174
|
+
default=lambda config: config.resources.max_cpu_pct,
|
|
175
|
+
minimum=MINIMUM_CPU_PCT,
|
|
176
|
+
maximum=MAX_USAGE_PCT,
|
|
177
|
+
whole_number=True,
|
|
178
|
+
)
|
|
179
|
+
MIN_FREE_RAM = OrchestrationField(
|
|
180
|
+
input_id="min-free-ram",
|
|
181
|
+
label="Minimum free RAM",
|
|
182
|
+
summary="Min free RAM",
|
|
183
|
+
unit="GB",
|
|
184
|
+
explanation=(
|
|
185
|
+
"New tasks start only while at least this much RAM is free; 0 turns the RAM check off."
|
|
186
|
+
),
|
|
187
|
+
default=lambda config: config.resources.min_free_ram_gb,
|
|
188
|
+
minimum=MINIMUM_FREE_RAM_GB,
|
|
189
|
+
maximum=None,
|
|
190
|
+
whole_number=False,
|
|
191
|
+
)
|
|
192
|
+
ORCHESTRATION_FIELDS = (
|
|
193
|
+
MAX_PARALLEL,
|
|
194
|
+
MAX_ITERATIONS,
|
|
195
|
+
PUSH_INTERVAL,
|
|
196
|
+
MAX_QUOTA_WAIT,
|
|
197
|
+
STALL_TIMEOUT,
|
|
198
|
+
TOOL_TIMEOUT,
|
|
199
|
+
MAX_CPU,
|
|
200
|
+
MIN_FREE_RAM,
|
|
201
|
+
)
|