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