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/report.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""`shikiru report`: validates and atomically stores an agent turn's report."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
from pydantic import ValidationError
|
|
13
|
+
|
|
14
|
+
from shikiru.agents.contracts import ROLE_ENV_VAR, RUN_DIR_ENV_VAR, role_dir
|
|
15
|
+
from shikiru.domain.feedback import FeedbackContractError, FeedbackInput, check_feedback_report
|
|
16
|
+
from shikiru.domain.reports import (
|
|
17
|
+
CURRENT_TURN_LINK_NAME,
|
|
18
|
+
FEEDBACK_FILENAME,
|
|
19
|
+
REPORT_FILENAME,
|
|
20
|
+
REPORT_JSON_INDENT,
|
|
21
|
+
REPORT_KIND_FILENAME,
|
|
22
|
+
ReportKind,
|
|
23
|
+
model_for,
|
|
24
|
+
)
|
|
25
|
+
from shikiru.errors import EXIT_NOT_INITIALISED, EXIT_USAGE_ERROR, ShikiruError
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from collections.abc import Mapping
|
|
29
|
+
|
|
30
|
+
from pydantic import BaseModel
|
|
31
|
+
|
|
32
|
+
from shikiru.domain.reports import Report
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ReportError(ShikiruError):
|
|
36
|
+
"""A failure while handling `shikiru report`, with the process exit code to return.
|
|
37
|
+
|
|
38
|
+
`exit_code` is set per instance rather than per subclass. Invalid input and a missing current
|
|
39
|
+
turn use different exit codes through this same error type.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, exit_code: int, message: str) -> None:
|
|
43
|
+
"""Record the process `exit_code` and the human-readable `message` for stderr."""
|
|
44
|
+
super().__init__(message)
|
|
45
|
+
self.exit_code = exit_code
|
|
46
|
+
self.message = message
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def resolve_current_turn_dir(environ: Mapping[str, str]) -> Path:
|
|
50
|
+
"""Resolve `$SHIKIRU_RUN_DIR/$SHIKIRU_ROLE/current_turn` to its target directory."""
|
|
51
|
+
run_dir = environ.get(RUN_DIR_ENV_VAR)
|
|
52
|
+
role = environ.get(ROLE_ENV_VAR)
|
|
53
|
+
if not run_dir or not role:
|
|
54
|
+
message = f"{RUN_DIR_ENV_VAR} and {ROLE_ENV_VAR} must both be set"
|
|
55
|
+
raise ReportError(EXIT_NOT_INITIALISED, message)
|
|
56
|
+
link = role_dir(Path(run_dir), role) / CURRENT_TURN_LINK_NAME
|
|
57
|
+
if not link.is_symlink() or not link.exists():
|
|
58
|
+
message = f"no current turn: {link} is missing or does not point at a live turn dir"
|
|
59
|
+
raise ReportError(EXIT_NOT_INITIALISED, message)
|
|
60
|
+
return link.resolve()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _report_kind(turn_dir: Path) -> ReportKind:
|
|
64
|
+
kind_path = turn_dir / REPORT_KIND_FILENAME
|
|
65
|
+
if not kind_path.is_file():
|
|
66
|
+
message = f"{kind_path} is missing"
|
|
67
|
+
raise ReportError(EXIT_USAGE_ERROR, message)
|
|
68
|
+
raw_kind = kind_path.read_text().strip()
|
|
69
|
+
try:
|
|
70
|
+
return ReportKind(raw_kind)
|
|
71
|
+
except ValueError:
|
|
72
|
+
message = f"{kind_path}: unknown report kind {raw_kind!r}"
|
|
73
|
+
raise ReportError(EXIT_USAGE_ERROR, message) from None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _load_report_data(file_path: Path) -> object:
|
|
77
|
+
try:
|
|
78
|
+
raw_text = file_path.read_text()
|
|
79
|
+
except OSError as error:
|
|
80
|
+
message = f"cannot read {file_path}: {error}"
|
|
81
|
+
raise ReportError(EXIT_USAGE_ERROR, message) from error
|
|
82
|
+
try:
|
|
83
|
+
return json.loads(raw_text)
|
|
84
|
+
except json.JSONDecodeError as error:
|
|
85
|
+
message = f"{file_path}: invalid JSON: {error}"
|
|
86
|
+
raise ReportError(EXIT_USAGE_ERROR, message) from error
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _validation_error_messages(error: ValidationError) -> list[str]:
|
|
90
|
+
messages: list[str] = []
|
|
91
|
+
for violation in error.errors():
|
|
92
|
+
field_path = ".".join(str(part) for part in violation["loc"]) or "<root>"
|
|
93
|
+
messages.append(f"{field_path}: {violation['msg']}")
|
|
94
|
+
return messages
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def validate_report(turn_dir: Path, file_path: Path) -> BaseModel:
|
|
98
|
+
"""Validate `file_path`'s JSON against the model named in `turn_dir`'s `report.kind`."""
|
|
99
|
+
kind = _report_kind(turn_dir)
|
|
100
|
+
data = _load_report_data(file_path)
|
|
101
|
+
model = model_for(kind)
|
|
102
|
+
try:
|
|
103
|
+
report = model.model_validate(data)
|
|
104
|
+
except ValidationError as error:
|
|
105
|
+
message = "\n".join(_validation_error_messages(error))
|
|
106
|
+
raise ReportError(EXIT_USAGE_ERROR, message) from error
|
|
107
|
+
_check_feedback_contract(turn_dir, report)
|
|
108
|
+
return report
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _check_feedback_contract(turn_dir: Path, report: Report) -> None:
|
|
112
|
+
feedback_path = turn_dir / FEEDBACK_FILENAME
|
|
113
|
+
if not feedback_path.is_file():
|
|
114
|
+
return
|
|
115
|
+
try:
|
|
116
|
+
feedback = FeedbackInput.model_validate_json(feedback_path.read_text())
|
|
117
|
+
check_feedback_report(feedback, report)
|
|
118
|
+
except (ValidationError, OSError) as error:
|
|
119
|
+
message = f"cannot read {feedback_path}: {error}"
|
|
120
|
+
raise ReportError(EXIT_USAGE_ERROR, message) from error
|
|
121
|
+
except FeedbackContractError as error:
|
|
122
|
+
raise ReportError(EXIT_USAGE_ERROR, str(error)) from error
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def write_report(turn_dir: Path, report: BaseModel) -> Path:
|
|
126
|
+
"""Write `report` to `turn_dir/report.json`, replacing it atomically."""
|
|
127
|
+
report_path = turn_dir / REPORT_FILENAME
|
|
128
|
+
descriptor, temp_name = tempfile.mkstemp(dir=turn_dir, prefix=f".{REPORT_FILENAME}.")
|
|
129
|
+
try:
|
|
130
|
+
with os.fdopen(descriptor, "w") as temp_file:
|
|
131
|
+
temp_file.write(report.model_dump_json(indent=REPORT_JSON_INDENT))
|
|
132
|
+
Path(temp_name).replace(report_path)
|
|
133
|
+
except BaseException:
|
|
134
|
+
Path(temp_name).unlink(missing_ok=True)
|
|
135
|
+
raise
|
|
136
|
+
return report_path
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def submit_report(file_path: Path, environ: Mapping[str, str]) -> Path:
|
|
140
|
+
"""Resolve the current turn, validate `file_path` against its report kind, and store it."""
|
|
141
|
+
turn_dir = resolve_current_turn_dir(environ)
|
|
142
|
+
report = validate_report(turn_dir, file_path)
|
|
143
|
+
return write_report(turn_dir, report)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
_FILE_OPTION = typer.Option(..., "--file", exists=True, dir_okay=False, help="Report JSON.")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def report_command(file: Path = _FILE_OPTION) -> None:
|
|
150
|
+
"""Validate and store the current turn's report."""
|
|
151
|
+
try:
|
|
152
|
+
report_path = submit_report(file, os.environ)
|
|
153
|
+
except ReportError as error:
|
|
154
|
+
typer.echo(error.message, err=True)
|
|
155
|
+
raise typer.Exit(code=error.exit_code) from None
|
|
156
|
+
typer.echo(f"report accepted: {report_path}")
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Plain numbered session selection for the attach command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import signal
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from rich.text import Text
|
|
9
|
+
|
|
10
|
+
from shikiru.cli.render import session_options_renderable
|
|
11
|
+
from shikiru.errors import UsageError
|
|
12
|
+
from shikiru.ui.console import human_console
|
|
13
|
+
from shikiru.ui.section import BOLD_STEEL
|
|
14
|
+
from shikiru.ui.tokens import SECONDARY_TEXT_COLOR
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from collections.abc import Sequence
|
|
18
|
+
|
|
19
|
+
from shikiru.operator.sessions import SessionRow
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
SESSION_CANCELLED = "session selection was cancelled"
|
|
23
|
+
SESSION_PROMPT_PREFIX = "Select a session number (or "
|
|
24
|
+
SESSION_PROMPT_SUFFIX = " to cancel): "
|
|
25
|
+
SESSION_CANCEL_CHOICES = frozenset({"q", "quit"})
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def prompt_for_session(rows: Sequence[SessionRow], host: str | None) -> SessionRow:
|
|
29
|
+
"""Show full session identities and return the validated numbered selection."""
|
|
30
|
+
console = human_console()
|
|
31
|
+
console.print(session_options_renderable(tuple(rows), host=host, console=console))
|
|
32
|
+
while True:
|
|
33
|
+
prompt = Text.assemble(
|
|
34
|
+
(SESSION_PROMPT_PREFIX, SECONDARY_TEXT_COLOR),
|
|
35
|
+
("q", BOLD_STEEL),
|
|
36
|
+
(SESSION_PROMPT_SUFFIX, SECONDARY_TEXT_COLOR),
|
|
37
|
+
)
|
|
38
|
+
console.print(prompt, end="")
|
|
39
|
+
previous_handler = signal.signal(signal.SIGINT, _interrupt_input)
|
|
40
|
+
try:
|
|
41
|
+
answer = input().strip()
|
|
42
|
+
except (EOFError, KeyboardInterrupt) as error:
|
|
43
|
+
raise UsageError(SESSION_CANCELLED) from error
|
|
44
|
+
finally:
|
|
45
|
+
signal.signal(signal.SIGINT, previous_handler)
|
|
46
|
+
if answer.lower() in SESSION_CANCEL_CHOICES:
|
|
47
|
+
raise UsageError(SESSION_CANCELLED)
|
|
48
|
+
if answer.isdecimal():
|
|
49
|
+
try:
|
|
50
|
+
selected = int(answer)
|
|
51
|
+
except ValueError:
|
|
52
|
+
pass
|
|
53
|
+
else:
|
|
54
|
+
if 1 <= selected <= len(rows):
|
|
55
|
+
return rows[selected - 1]
|
|
56
|
+
retry = Text(f"Enter a number from 1 to {len(rows)}, or ", style=SECONDARY_TEXT_COLOR)
|
|
57
|
+
retry.append("q", style=BOLD_STEEL)
|
|
58
|
+
retry.append(" to cancel.", style=SECONDARY_TEXT_COLOR)
|
|
59
|
+
console.print(retry)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _interrupt_input(_signum: int, _frame: object) -> None:
|
|
63
|
+
raise KeyboardInterrupt
|
shikiru/cli/view.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""`shikiru view`: a role pane's read-only live tail of its `view.jsonl`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from typing import TYPE_CHECKING, Annotated
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
|
|
13
|
+
from shikiru.cli.context import project_root, state_from
|
|
14
|
+
from shikiru.clock import SystemClock, to_local
|
|
15
|
+
from shikiru.domain.tasks import Role
|
|
16
|
+
from shikiru.paths import run_dir
|
|
17
|
+
from shikiru.ui.quota_status import quota_status_text
|
|
18
|
+
from shikiru.ui.status_strip import StatusStrip
|
|
19
|
+
from shikiru.ui.stream import StreamLine
|
|
20
|
+
from shikiru.ui.tokens import AMBER
|
|
21
|
+
from shikiru.view_log import quota_status_path, read_quota_status, view_log_path
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from shikiru.clock import Clock
|
|
27
|
+
|
|
28
|
+
_LINE_END = b"\n"
|
|
29
|
+
_VIEW_POLL_SECONDS = 0.5
|
|
30
|
+
_VIEW_READ_BYTES = 65_536
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ViewTail:
|
|
34
|
+
"""Follows one `view.jsonl`, printing each complete row through a `StreamLine`.
|
|
35
|
+
|
|
36
|
+
While `status_path` exists, its quota wait shows as a status strip above the output.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
path: Path,
|
|
42
|
+
stream: StreamLine,
|
|
43
|
+
console: Console,
|
|
44
|
+
clock: Clock,
|
|
45
|
+
*,
|
|
46
|
+
status_path: Path,
|
|
47
|
+
strip: StatusStrip,
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Tail `path` from its start, rendering with `stream` onto `console`."""
|
|
50
|
+
self._path = path
|
|
51
|
+
self._stream = stream
|
|
52
|
+
self._console = console
|
|
53
|
+
self._clock = clock
|
|
54
|
+
self._status_path = status_path
|
|
55
|
+
self._strip = strip
|
|
56
|
+
self._offset = 0
|
|
57
|
+
self._pending_bytes = b""
|
|
58
|
+
self._size = (console.size.width, console.size.height)
|
|
59
|
+
|
|
60
|
+
async def follow(self) -> None:
|
|
61
|
+
"""Print rows as they are appended, polling through the clock until cancelled."""
|
|
62
|
+
while True:
|
|
63
|
+
size = self._console.size
|
|
64
|
+
resized = (size.width, size.height) != self._size and self._strip.active
|
|
65
|
+
if resized:
|
|
66
|
+
self._reset_for_replay()
|
|
67
|
+
self.sync_status()
|
|
68
|
+
while self.drain():
|
|
69
|
+
await self._clock.sleep(0)
|
|
70
|
+
if not resized and self.sync_status():
|
|
71
|
+
self._reset_for_replay()
|
|
72
|
+
self.sync_status()
|
|
73
|
+
while self.drain():
|
|
74
|
+
await self._clock.sleep(0)
|
|
75
|
+
size = self._console.size
|
|
76
|
+
self._size = (size.width, size.height)
|
|
77
|
+
await self._clock.sleep(_VIEW_POLL_SECONDS)
|
|
78
|
+
|
|
79
|
+
def sync_status(self) -> bool:
|
|
80
|
+
"""Show the published quota wait and report whether output needs replaying."""
|
|
81
|
+
status = read_quota_status(self._status_path)
|
|
82
|
+
if status is None:
|
|
83
|
+
return self._strip.sync(None)
|
|
84
|
+
action, at = status
|
|
85
|
+
now = to_local(self._clock.now())
|
|
86
|
+
return self._strip.sync(
|
|
87
|
+
quota_status_text(action, to_local(at) if at is not None else None, now)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def _reset_for_replay(self) -> None:
|
|
91
|
+
self._strip.reset_for_replay()
|
|
92
|
+
self._stream.reset()
|
|
93
|
+
self._offset = 0
|
|
94
|
+
self._pending_bytes = b""
|
|
95
|
+
|
|
96
|
+
def drain(self) -> bool:
|
|
97
|
+
"""Print complete rows from one bounded chunk; return whether more may remain."""
|
|
98
|
+
if not self._path.is_file():
|
|
99
|
+
return False
|
|
100
|
+
with self._path.open("rb") as log:
|
|
101
|
+
log.seek(self._offset)
|
|
102
|
+
chunk = log.read(_VIEW_READ_BYTES)
|
|
103
|
+
if not chunk:
|
|
104
|
+
return False
|
|
105
|
+
self._offset += len(chunk)
|
|
106
|
+
combined = self._pending_bytes + chunk
|
|
107
|
+
complete = combined[: combined.rfind(_LINE_END) + 1]
|
|
108
|
+
self._pending_bytes = combined[len(complete) :]
|
|
109
|
+
for raw in complete.splitlines():
|
|
110
|
+
for line in self._stream.render(json.loads(raw)):
|
|
111
|
+
self._console.print(line)
|
|
112
|
+
return len(chunk) == _VIEW_READ_BYTES
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def view_command(
|
|
116
|
+
context: typer.Context,
|
|
117
|
+
task_id: Annotated[str, typer.Argument()],
|
|
118
|
+
role: Annotated[Role, typer.Argument()],
|
|
119
|
+
) -> None:
|
|
120
|
+
"""Render `role`'s live view of `task_id`."""
|
|
121
|
+
root = project_root(state_from(context), require_config=False)
|
|
122
|
+
console = Console()
|
|
123
|
+
tail = ViewTail(
|
|
124
|
+
view_log_path(run_dir(root, task_id), role.value),
|
|
125
|
+
StreamLine(role),
|
|
126
|
+
console,
|
|
127
|
+
SystemClock(),
|
|
128
|
+
status_path=quota_status_path(run_dir(root, task_id), role.value),
|
|
129
|
+
strip=StatusStrip(console, sys.stdin, AMBER),
|
|
130
|
+
)
|
|
131
|
+
asyncio.run(tail.follow())
|
shikiru/clock.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Injectable time: every timer in Shikiru goes through a `Clock`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
from typing import Protocol
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Clock(Protocol):
|
|
11
|
+
"""A source of the current time and of delay, injected wherever either is needed."""
|
|
12
|
+
|
|
13
|
+
def now(self) -> datetime:
|
|
14
|
+
"""Return the current UTC-aware time."""
|
|
15
|
+
...
|
|
16
|
+
|
|
17
|
+
async def sleep(self, seconds: float) -> None:
|
|
18
|
+
"""Suspend the caller for `seconds`."""
|
|
19
|
+
...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SystemClock:
|
|
23
|
+
"""A `Clock` backed by the real wall clock and event loop."""
|
|
24
|
+
|
|
25
|
+
def now(self) -> datetime:
|
|
26
|
+
"""Return the current UTC-aware time."""
|
|
27
|
+
return datetime.now(UTC)
|
|
28
|
+
|
|
29
|
+
async def sleep(self, seconds: float) -> None:
|
|
30
|
+
"""Suspend the caller for `seconds`."""
|
|
31
|
+
await asyncio.sleep(seconds)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def to_local(value: datetime) -> datetime:
|
|
35
|
+
"""Return `value` as the viewer's local time, using the offset in force at that instant."""
|
|
36
|
+
return value.astimezone()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def from_local(value: datetime) -> datetime:
|
|
40
|
+
"""Return the aware instant the naive wall-clock `value` names in the viewer's zone."""
|
|
41
|
+
return value.astimezone()
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
trackers = []
|
|
3
|
+
|
|
4
|
+
[sessions]
|
|
5
|
+
ssh_timeout_seconds = 30
|
|
6
|
+
|
|
7
|
+
[orchestration]
|
|
8
|
+
max_parallel = 2
|
|
9
|
+
max_iterations = 5
|
|
10
|
+
integration = "merge"
|
|
11
|
+
pr_feedback = "changes_requested"
|
|
12
|
+
push_interval_seconds = 14400
|
|
13
|
+
remote_recheck_seconds = 60
|
|
14
|
+
poll_seconds = 2
|
|
15
|
+
shutdown_seconds = 15
|
|
16
|
+
dispatch_budget_seconds = 10
|
|
17
|
+
engine_start_seconds = 5
|
|
18
|
+
|
|
19
|
+
[orchestration.resources]
|
|
20
|
+
max_cpu_pct = 85
|
|
21
|
+
min_free_ram_gb = 4
|
|
22
|
+
sample_seconds = 2
|
|
23
|
+
cpu_window_seconds = 30
|
|
24
|
+
|
|
25
|
+
[orchestration.timeouts]
|
|
26
|
+
stall = 1200
|
|
27
|
+
tool = 7200
|
|
28
|
+
max_quota_wait = 28800
|
|
29
|
+
integration_wait = 1800
|
|
30
|
+
pr_wait = 604800
|
|
31
|
+
helper = 300
|
|
32
|
+
cache_copy = 600
|
|
33
|
+
git_command = 120
|
|
34
|
+
|
|
35
|
+
[orchestration.faults]
|
|
36
|
+
backoff_initial_seconds = 2
|
|
37
|
+
backoff_max_seconds = 60
|
|
38
|
+
unexpected_fault_limit = 10
|
|
39
|
+
stop_notify_seconds = 30
|
|
40
|
+
repeat_notify_seconds = 300
|
|
41
|
+
|
|
42
|
+
[orchestration.retries]
|
|
43
|
+
unavailable_initial_seconds = 30
|
|
44
|
+
unavailable_multiplier = 4
|
|
45
|
+
unavailable_max_seconds = 300
|
|
46
|
+
unavailable_max_retries = 3
|
|
47
|
+
|
|
48
|
+
[orchestration.quota_refresh]
|
|
49
|
+
interval_seconds = 300
|
|
50
|
+
timeout_seconds = 30
|
|
51
|
+
|
|
52
|
+
[orchestration.network]
|
|
53
|
+
fetch_timeout_seconds = 30
|
|
54
|
+
poll_attempts = 8
|
|
55
|
+
pr_poll_seconds = 300
|
|
56
|
+
pr_close_attempts = 5
|
|
57
|
+
|
|
58
|
+
[escalation]
|
|
59
|
+
tried_lines = 8
|
|
60
|
+
|
|
61
|
+
[roles.implementer]
|
|
62
|
+
escalate_after = 2
|
|
63
|
+
|
|
64
|
+
[dashboard]
|
|
65
|
+
widgets = ["header", "escalations", "strips", "queue", "dialogue", "usage", "resources", "docker", "disk", "faults"]
|
|
66
|
+
animations = true
|
|
67
|
+
docker_refresh_seconds = 5
|
|
68
|
+
docker_timeout_seconds = 5
|
|
69
|
+
docker_scope = "project"
|
|
70
|
+
disk_refresh_seconds = 60
|
|
71
|
+
disk_size_refresh_seconds = 300
|
|
72
|
+
disk_warn_free_pct = 10
|
|
73
|
+
disk_alert_free_pct = 5
|
|
74
|
+
repo_status_cache_seconds = 10
|
|
75
|
+
task_work_cache_seconds = 2
|
|
76
|
+
timeline_page_size = 20
|
|
77
|
+
timeline_scan_rows = 256
|
|
78
|
+
timeline_refresh_seconds = 1
|
|
79
|
+
recent_task_limit = 30
|
|
80
|
+
recent_task_window = 3
|
|
81
|
+
last_words_chars = 4000
|
|
82
|
+
now_max_lines = 6
|
|
83
|
+
now_compact_lines = 2
|
|
84
|
+
results_commits = 3
|
|
85
|
+
action_confirmation_seconds = 5
|
|
86
|
+
|
|
87
|
+
[dashboard.motion]
|
|
88
|
+
refresh_hz = 4
|
|
89
|
+
stamp_flash_milliseconds = 400
|
|
90
|
+
escalation_pulse_hz = 1
|
|
91
|
+
task_event_limit = 50
|
|
92
|
+
merge_event_limit = 20
|
|
93
|
+
|
|
94
|
+
[dashboard.layout]
|
|
95
|
+
split = "v"
|
|
96
|
+
panes = [
|
|
97
|
+
{ pane = "leader", size = 40 },
|
|
98
|
+
{ pane = "implementer", size = 40 },
|
|
99
|
+
{ pane = "shell", size = 20 },
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
[dashboard.titles]
|
|
103
|
+
window = "{task_id} {title:.24}"
|
|
104
|
+
leader = "{role_label} · {agent}:{model}/{effort}{pending_rung} · iter {iteration} · ctx {context} · {cost} {flags_text}"
|
|
105
|
+
implementer = "{role_label} · {agent}:{model}/{effort}{pending_rung} · iter {iteration} · ctx {context} · {cost} {flags_text}"
|
|
106
|
+
system = "Shikiru · {project} · {active}/{max_parallel} active · {queued} queued"
|
|
107
|
+
shell = "Project folder · {cwd}"
|
|
108
|
+
context_warn_pct = 40
|
|
109
|
+
context_alert_pct = 60
|
|
110
|
+
|
|
111
|
+
[dashboard.gauges]
|
|
112
|
+
quota_warn_pct = 50
|
|
113
|
+
quota_alert_pct = 90
|
|
114
|
+
|
|
115
|
+
[notify]
|
|
116
|
+
command = ""
|
|
117
|
+
timeout_seconds = 30
|
|
118
|
+
|
|
119
|
+
[logging]
|
|
120
|
+
engine_log_max_bytes = 10485760
|
|
121
|
+
engine_log_backups = 3
|
|
122
|
+
|
|
123
|
+
[doctor]
|
|
124
|
+
probe_timeout = 180
|
|
125
|
+
probe_stream_tail_lines = 20
|
|
126
|
+
|
|
127
|
+
[retention]
|
|
128
|
+
interval_seconds = 3600
|
|
129
|
+
finished_task_days = 30
|
|
130
|
+
event_days = 90
|
|
131
|
+
command_days = 30
|