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