shikiru-cli 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- shikiru/__init__.py +1 -0
- shikiru/__main__.py +8 -0
- shikiru/agents/__init__.py +3 -0
- shikiru/agents/contracts.py +590 -0
- shikiru/agents/registry.py +87 -0
- shikiru/agents/sdk/__init__.py +17 -0
- shikiru/agents/sdk/reset_time.py +137 -0
- shikiru/agents/sdk/turn.py +335 -0
- shikiru/agents/settings.py +151 -0
- shikiru/agents/testing/__init__.py +26 -0
- shikiru/agents/testing/contract_suite.py +653 -0
- shikiru/agents/testing/resume.py +15 -0
- shikiru/bootstrap.py +1277 -0
- shikiru/cli/__init__.py +1 -0
- shikiru/cli/app.py +770 -0
- shikiru/cli/context.py +63 -0
- shikiru/cli/render.py +1475 -0
- shikiru/cli/report.py +156 -0
- shikiru/cli/session_prompt.py +63 -0
- shikiru/cli/view.py +131 -0
- shikiru/clock.py +41 -0
- shikiru/config/__init__.py +8 -0
- shikiru/config/defaults.toml +131 -0
- shikiru/config/loader.py +178 -0
- shikiru/config/settings.py +392 -0
- shikiru/dashboard/__init__.py +3 -0
- shikiru/dashboard/actions_list.py +799 -0
- shikiru/dashboard/activity.py +150 -0
- shikiru/dashboard/app.py +1765 -0
- shikiru/dashboard/blocker_evidence.py +122 -0
- shikiru/dashboard/blocker_rows.py +84 -0
- shikiru/dashboard/briefing_view.py +871 -0
- shikiru/dashboard/contract.py +1086 -0
- shikiru/dashboard/covered.py +32 -0
- shikiru/dashboard/detail.py +1209 -0
- shikiru/dashboard/detail_fold.py +244 -0
- shikiru/dashboard/detail_hint.py +68 -0
- shikiru/dashboard/details_list_view.py +150 -0
- shikiru/dashboard/diagnostics.py +80 -0
- shikiru/dashboard/diagnostics_sections.py +186 -0
- shikiru/dashboard/evidence_table.py +123 -0
- shikiru/dashboard/extra_details.py +214 -0
- shikiru/dashboard/fault_guidance.py +216 -0
- shikiru/dashboard/fold_bodies.py +276 -0
- shikiru/dashboard/help_keys.py +166 -0
- shikiru/dashboard/help_screen.py +204 -0
- shikiru/dashboard/interaction.py +17 -0
- shikiru/dashboard/ladder_editor.py +544 -0
- shikiru/dashboard/live_settings.py +62 -0
- shikiru/dashboard/motion.py +368 -0
- shikiru/dashboard/orchestration_fields.py +201 -0
- shikiru/dashboard/palette.py +1606 -0
- shikiru/dashboard/palette_app.py +89 -0
- shikiru/dashboard/palette_drafts.py +33 -0
- shikiru/dashboard/resolution.py +573 -0
- shikiru/dashboard/resolution_view.py +302 -0
- shikiru/dashboard/row_list.py +256 -0
- shikiru/dashboard/section_rows.py +173 -0
- shikiru/dashboard/services.py +665 -0
- shikiru/dashboard/session_history.py +402 -0
- shikiru/dashboard/shikiru_init_ascii_art.txt +40 -0
- shikiru/dashboard/task_picker.py +358 -0
- shikiru/dashboard/tick.py +59 -0
- shikiru/dashboard/timeline_filter.py +129 -0
- shikiru/dashboard/timeline_section.py +385 -0
- shikiru/dashboard/usage_body.py +269 -0
- shikiru/dashboard/views.py +2314 -0
- shikiru/dashboard/wizard.py +1389 -0
- shikiru/domain/__init__.py +1 -0
- shikiru/domain/acceptance.py +130 -0
- shikiru/domain/activity.py +28 -0
- shikiru/domain/attempts.py +333 -0
- shikiru/domain/briefing.py +257 -0
- shikiru/domain/commands.py +219 -0
- shikiru/domain/decisions.py +231 -0
- shikiru/domain/docker.py +262 -0
- shikiru/domain/engine_uptime.py +87 -0
- shikiru/domain/escalation.py +194 -0
- shikiru/domain/escalation_evidence.py +186 -0
- shikiru/domain/escalation_panel.py +144 -0
- shikiru/domain/events.py +71 -0
- shikiru/domain/evidence_export.py +89 -0
- shikiru/domain/faults.py +86 -0
- shikiru/domain/feedback.py +180 -0
- shikiru/domain/feedback_rounds.py +172 -0
- shikiru/domain/fencing.py +31 -0
- shikiru/domain/ladder.py +102 -0
- shikiru/domain/ladder_text.py +44 -0
- shikiru/domain/levels.py +44 -0
- shikiru/domain/motion.py +199 -0
- shikiru/domain/packet_evidence.py +377 -0
- shikiru/domain/pushes.py +47 -0
- shikiru/domain/quota_blocks.py +44 -0
- shikiru/domain/quota_waits.py +219 -0
- shikiru/domain/recovery.py +194 -0
- shikiru/domain/reports.py +498 -0
- shikiru/domain/results.py +217 -0
- shikiru/domain/runs.py +85 -0
- shikiru/domain/scheduling.py +231 -0
- shikiru/domain/status_tasks.py +40 -0
- shikiru/domain/steps.py +325 -0
- shikiru/domain/tasks.py +174 -0
- shikiru/domain/timeline.py +208 -0
- shikiru/domain/timeline_phrases.py +271 -0
- shikiru/domain/versions.py +18 -0
- shikiru/domain/workflow.py +372 -0
- shikiru/engine/__init__.py +0 -0
- shikiru/engine/acceptance.py +90 -0
- shikiru/engine/agents.py +61 -0
- shikiru/engine/cosmetic_decorator.py +99 -0
- shikiru/engine/daemon.py +409 -0
- shikiru/engine/dependency.py +37 -0
- shikiru/engine/escalation_filing.py +110 -0
- shikiru/engine/escalations.py +401 -0
- shikiru/engine/events.py +285 -0
- shikiru/engine/evidence.py +81 -0
- shikiru/engine/fault_reporter.py +144 -0
- shikiru/engine/feedback.py +577 -0
- shikiru/engine/feedback_audit.py +80 -0
- shikiru/engine/feedback_intake.py +95 -0
- shikiru/engine/feedback_snapshot.py +148 -0
- shikiru/engine/flow.py +87 -0
- shikiru/engine/handlers/__init__.py +33 -0
- shikiru/engine/handlers/base.py +148 -0
- shikiru/engine/handlers/engine.py +24 -0
- shikiru/engine/handlers/escalations.py +174 -0
- shikiru/engine/handlers/operator.py +81 -0
- shikiru/engine/handlers/tasks.py +237 -0
- shikiru/engine/history.py +128 -0
- shikiru/engine/integration.py +1234 -0
- shikiru/engine/integration_turns.py +337 -0
- shikiru/engine/layout_guard.py +362 -0
- shikiru/engine/lifecycle.py +60 -0
- shikiru/engine/monitor.py +317 -0
- shikiru/engine/notification_delivery.py +124 -0
- shikiru/engine/notification_feed.py +57 -0
- shikiru/engine/notifier.py +91 -0
- shikiru/engine/pull_request_closing.py +117 -0
- shikiru/engine/push_service.py +177 -0
- shikiru/engine/quota_probe_threads.py +79 -0
- shikiru/engine/quota_reconciler.py +210 -0
- shikiru/engine/quota_schedule.py +24 -0
- shikiru/engine/quota_waits.py +102 -0
- shikiru/engine/reconcile.py +91 -0
- shikiru/engine/recovery.py +284 -0
- shikiru/engine/retention.py +102 -0
- shikiru/engine/root_preparation.py +86 -0
- shikiru/engine/scheduler.py +320 -0
- shikiru/engine/session_guard.py +46 -0
- shikiru/engine/sessions.py +95 -0
- shikiru/engine/signalled.py +75 -0
- shikiru/engine/step_runner.py +140 -0
- shikiru/engine/supervisor.py +101 -0
- shikiru/engine/takeover.py +223 -0
- shikiru/engine/task_facts.py +46 -0
- shikiru/engine/tick_guard.py +95 -0
- shikiru/engine/titles.py +297 -0
- shikiru/engine/transitions.py +79 -0
- shikiru/engine/turn_context.py +206 -0
- shikiru/engine/turn_execution.py +30 -0
- shikiru/engine/turn_loop.py +230 -0
- shikiru/engine/turn_processes.py +79 -0
- shikiru/engine/turns.py +457 -0
- shikiru/errors.py +144 -0
- shikiru/helper/__init__.py +3 -0
- shikiru/helper/helper.py +182 -0
- shikiru/helper/models.py +11 -0
- shikiru/mcp/__init__.py +1 -0
- shikiru/mcp/server.py +196 -0
- shikiru/operator/__init__.py +33 -0
- shikiru/operator/attachment.py +24 -0
- shikiru/operator/commands.py +319 -0
- shikiru/operator/detectors.py +374 -0
- shikiru/operator/doctor.py +351 -0
- shikiru/operator/events.py +57 -0
- shikiru/operator/evidence_export.py +498 -0
- shikiru/operator/evidence_writer.py +78 -0
- shikiru/operator/lifecycle.py +294 -0
- shikiru/operator/plugin_checks.py +194 -0
- shikiru/operator/probe.py +287 -0
- shikiru/operator/queries.py +1601 -0
- shikiru/operator/services.py +36 -0
- shikiru/operator/sessions.py +378 -0
- shikiru/operator/strip_views.py +162 -0
- shikiru/operator/timeline.py +416 -0
- shikiru/operator/toml_text.py +62 -0
- shikiru/palette.py +18 -0
- shikiru/paths.py +91 -0
- shikiru/plugins/__init__.py +0 -0
- shikiru/plugins/agents/__init__.py +1 -0
- shikiru/plugins/agents/claude/__init__.py +5 -0
- shikiru/plugins/agents/claude/agent.py +251 -0
- shikiru/plugins/agents/claude/context.py +116 -0
- shikiru/plugins/agents/claude/headless.py +92 -0
- shikiru/plugins/agents/claude/hook.py +57 -0
- shikiru/plugins/agents/claude/model_catalog.py +105 -0
- shikiru/plugins/agents/claude/options.py +62 -0
- shikiru/plugins/agents/claude/turn.py +1018 -0
- shikiru/plugins/agents/codex/__init__.py +5 -0
- shikiru/plugins/agents/codex/agent.py +486 -0
- shikiru/plugins/agents/codex/headless.py +166 -0
- shikiru/plugins/agents/codex/home.py +14 -0
- shikiru/plugins/agents/codex/model_catalog.py +128 -0
- shikiru/plugins/agents/codex/options.py +94 -0
- shikiru/plugins/agents/codex/quota.py +195 -0
- shikiru/plugins/agents/codex/sandbox.py +31 -0
- shikiru/plugins/agents/codex/sandbox_probe.py +97 -0
- shikiru/plugins/agents/codex/turn.py +767 -0
- shikiru/plugins/agents/codex/usage.py +106 -0
- shikiru/plugins/agents/shared_settings.py +62 -0
- shikiru/plugins/widgets/__init__.py +3 -0
- shikiru/plugins/widgets/dialogue.py +144 -0
- shikiru/plugins/widgets/disk.py +123 -0
- shikiru/plugins/widgets/docker.py +225 -0
- shikiru/plugins/widgets/escalations.py +402 -0
- shikiru/plugins/widgets/faults.py +204 -0
- shikiru/plugins/widgets/header.py +321 -0
- shikiru/plugins/widgets/queue.py +1140 -0
- shikiru/plugins/widgets/queue_hint.py +93 -0
- shikiru/plugins/widgets/resources.py +105 -0
- shikiru/plugins/widgets/strips.py +82 -0
- shikiru/plugins/widgets/usage.py +176 -0
- shikiru/private.py +94 -0
- shikiru/process.py +535 -0
- shikiru/prompts/__init__.py +3 -0
- shikiru/prompts/renderer.py +85 -0
- shikiru/prompts/templates/_base.md.j2 +17 -0
- shikiru/prompts/templates/_macros.md.j2 +61 -0
- shikiru/prompts/templates/conflicts.md.j2 +22 -0
- shikiru/prompts/templates/consult.md.j2 +5 -0
- shikiru/prompts/templates/continue.md.j2 +4 -0
- shikiru/prompts/templates/feedback_analysis.md.j2 +29 -0
- shikiru/prompts/templates/feedback_rework.md.j2 +33 -0
- shikiru/prompts/templates/feedback_verify.md.j2 +33 -0
- shikiru/prompts/templates/handoff.md.j2 +23 -0
- shikiru/prompts/templates/implement.md.j2 +13 -0
- shikiru/prompts/templates/plan.md.j2 +30 -0
- shikiru/prompts/templates/rework.md.j2 +13 -0
- shikiru/prompts/templates/triage.md.j2 +20 -0
- shikiru/prompts/templates/verify.md.j2 +29 -0
- shikiru/store/__init__.py +1 -0
- shikiru/store/commands.py +242 -0
- shikiru/store/database.py +297 -0
- shikiru/store/escalations.py +240 -0
- shikiru/store/events.py +213 -0
- shikiru/store/feedback.py +266 -0
- shikiru/store/migrations/001_initial.sql +63 -0
- shikiru/store/migrations/002_headless_turns.sql +31 -0
- shikiru/store/migrations/003_drop_command_handled_at.sql +1 -0
- shikiru/store/migrations/004_repo_base_ref.sql +3 -0
- shikiru/store/migrations/005_complexity_explicit.sql +3 -0
- shikiru/store/migrations/006_events_kind_index.sql +1 -0
- shikiru/store/migrations/007_notification_cursor.sql +1 -0
- shikiru/store/migrations/008_reported_usage.sql +1 -0
- shikiru/store/migrations/009_billing_declarations.sql +2 -0
- shikiru/store/migrations/010_canonical_quota_windows.sql +3 -0
- shikiru/store/migrations/012_drop_task_quota_evidence.sql +2 -0
- shikiru/store/migrations/013_push_targets.sql +7 -0
- shikiru/store/migrations/014_agent_block_time.sql +6 -0
- shikiru/store/migrations/015_repair_orphan_agent_blocks.sql +3 -0
- shikiru/store/migrations/016_feedback_rounds.sql +29 -0
- shikiru/store/migrations/017_feedback_round_comments.sql +5 -0
- shikiru/store/migrations/__init__.py +1 -0
- shikiru/store/notification_cursor.py +29 -0
- shikiru/store/pushes.py +117 -0
- shikiru/store/quotas.py +153 -0
- shikiru/store/reader.py +310 -0
- shikiru/store/retention.py +92 -0
- shikiru/store/runs.py +227 -0
- shikiru/store/tasks.py +949 -0
- shikiru/telemetry/__init__.py +1 -0
- shikiru/telemetry/disk.py +167 -0
- shikiru/telemetry/docker.py +164 -0
- shikiru/telemetry/resources.py +296 -0
- shikiru/tmux/__init__.py +3 -0
- shikiru/tmux/client.py +637 -0
- shikiru/tmux/decorator.py +225 -0
- shikiru/tmux/layout.py +200 -0
- shikiru/tmux/session.py +381 -0
- shikiru/ui/__init__.py +9 -0
- shikiru/ui/age.py +45 -0
- shikiru/ui/bubbles.py +41 -0
- shikiru/ui/compact.py +1209 -0
- shikiru/ui/console.py +49 -0
- shikiru/ui/fold.py +50 -0
- shikiru/ui/gated_static.py +117 -0
- shikiru/ui/gauge.py +40 -0
- shikiru/ui/glyphs.py +109 -0
- shikiru/ui/keys.py +57 -0
- shikiru/ui/live_interval.py +42 -0
- shikiru/ui/popup.py +311 -0
- shikiru/ui/quota_status.py +26 -0
- shikiru/ui/recent_window.py +78 -0
- shikiru/ui/screen.py +57 -0
- shikiru/ui/section.py +193 -0
- shikiru/ui/session.py +65 -0
- shikiru/ui/size.py +17 -0
- shikiru/ui/splash.py +42 -0
- shikiru/ui/status.py +47 -0
- shikiru/ui/status_strip.py +149 -0
- shikiru/ui/stream.py +152 -0
- shikiru/ui/strip.py +332 -0
- shikiru/ui/theme.py +83 -0
- shikiru/ui/tokens.py +51 -0
- shikiru/view_log.py +126 -0
- shikiru/workspace/__init__.py +3 -0
- shikiru/workspace/base_refs.py +160 -0
- shikiru/workspace/caches.py +136 -0
- shikiru/workspace/facts.py +118 -0
- shikiru/workspace/git.py +601 -0
- shikiru/workspace/github.py +744 -0
- shikiru/workspace/messages.py +107 -0
- shikiru/workspace/reports.py +145 -0
- shikiru/workspace/roots.py +180 -0
- shikiru_cli-0.2.0.dist-info/METADATA +428 -0
- shikiru_cli-0.2.0.dist-info/RECORD +319 -0
- shikiru_cli-0.2.0.dist-info/WHEEL +4 -0
- shikiru_cli-0.2.0.dist-info/entry_points.txt +18 -0
- shikiru_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"""Bounded reads of recorded normalized signal files for task evidence pages."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import TYPE_CHECKING, cast
|
|
9
|
+
|
|
10
|
+
from shikiru.domain.tasks import Role
|
|
11
|
+
from shikiru.domain.timeline import (
|
|
12
|
+
MISSING_SIGNAL_SUMMARY,
|
|
13
|
+
TIMELINE_ACTIVITY_CATEGORY,
|
|
14
|
+
TIMELINE_AGENT_CATEGORY,
|
|
15
|
+
TIMELINE_ALL_CATEGORY,
|
|
16
|
+
TIMELINE_DECISION_CATEGORY,
|
|
17
|
+
TIMELINE_ENGINE_CATEGORY,
|
|
18
|
+
TIMELINE_EVIDENCE_CATEGORY,
|
|
19
|
+
TIMELINE_MESSAGE_CATEGORY,
|
|
20
|
+
TIMELINE_SIGNAL_CATEGORY,
|
|
21
|
+
TIMELINE_SOURCE_AGENT,
|
|
22
|
+
TIMELINE_TOOL_CATEGORY,
|
|
23
|
+
TIMELINE_TRANSITION_CATEGORY,
|
|
24
|
+
TIMELINE_USAGE_CATEGORY,
|
|
25
|
+
ParsedSignal,
|
|
26
|
+
parse_signal_row,
|
|
27
|
+
)
|
|
28
|
+
from shikiru.domain.timeline_phrases import GLYPH_ATTENTION, Phrase, Tone
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from collections.abc import Mapping, Sequence
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
CURSOR_VERSION = 1
|
|
35
|
+
SIGNAL_LINE_BLOCK_BYTES = 65536
|
|
36
|
+
CURSOR_POSITION_FIELDS = 2
|
|
37
|
+
TIMELINE_CURSOR_ERROR = "task evidence cursor is invalid"
|
|
38
|
+
TIMELINE_CURSOR_FILTER_ERROR = "task evidence cursor belongs to different filters"
|
|
39
|
+
TIMELINE_MISSING_DETAIL = "No recorded normalized signal file is available for this role."
|
|
40
|
+
TIMELINE_READ_ERROR_SUMMARY = "Could not read recorded signal evidence"
|
|
41
|
+
TIMELINE_READ_ERROR_PREFIX = "unavailable:"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class TimelineFilters:
|
|
46
|
+
"""Filters applied consistently to engine events and normalized signal evidence."""
|
|
47
|
+
|
|
48
|
+
role: str | None
|
|
49
|
+
turn: str | None
|
|
50
|
+
category: str
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class TimelineCursor:
|
|
55
|
+
"""Positions immediately after the newest rows already shown from each source."""
|
|
56
|
+
|
|
57
|
+
event_before: tuple[str, int] | None
|
|
58
|
+
signal_before: Mapping[str, int | None]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class TimelineItem:
|
|
63
|
+
"""One evidence row with stable identity and its source ordering key."""
|
|
64
|
+
|
|
65
|
+
identity: str
|
|
66
|
+
timestamp: datetime | None
|
|
67
|
+
source: str
|
|
68
|
+
category: str
|
|
69
|
+
summary: str
|
|
70
|
+
phrase: Phrase
|
|
71
|
+
detail: str
|
|
72
|
+
role: str | None
|
|
73
|
+
turn: str | None
|
|
74
|
+
event_id: int | None = None
|
|
75
|
+
signal_offset: int | None = None
|
|
76
|
+
signal_end_offset: int | None = None
|
|
77
|
+
incomplete: bool = False
|
|
78
|
+
timestamp_error: str | None = None
|
|
79
|
+
listed: bool = True
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True)
|
|
83
|
+
class SignalBatch:
|
|
84
|
+
"""A bounded signal page and a position that safely replays its first row."""
|
|
85
|
+
|
|
86
|
+
items: tuple[TimelineItem, ...]
|
|
87
|
+
replay_offset: int | None
|
|
88
|
+
scan_offset: int
|
|
89
|
+
has_more: bool = False
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def encode_cursor(cursor: TimelineCursor, filters: TimelineFilters) -> str:
|
|
93
|
+
"""Serialize a source-position cursor with the filters that define its ordering."""
|
|
94
|
+
encoded = {
|
|
95
|
+
"version": CURSOR_VERSION,
|
|
96
|
+
"event_before": list(cursor.event_before) if cursor.event_before is not None else None,
|
|
97
|
+
"signal_before": dict(cursor.signal_before),
|
|
98
|
+
"filters": {"role": filters.role, "turn": filters.turn, "category": filters.category},
|
|
99
|
+
}
|
|
100
|
+
return json.dumps(encoded, separators=(",", ":"), sort_keys=True)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def decode_cursor(
|
|
104
|
+
token: str,
|
|
105
|
+
*,
|
|
106
|
+
filters: TimelineFilters,
|
|
107
|
+
roles: Sequence[str],
|
|
108
|
+
) -> TimelineCursor:
|
|
109
|
+
"""Validate an opaque cursor before continuing a filtered evidence page."""
|
|
110
|
+
data = _cursor_mapping(token)
|
|
111
|
+
expected_filters = {"role": filters.role, "turn": filters.turn, "category": filters.category}
|
|
112
|
+
if data.get("filters") != expected_filters:
|
|
113
|
+
raise ValueError(TIMELINE_CURSOR_FILTER_ERROR)
|
|
114
|
+
signal_before = _signal_positions(data, filters, roles)
|
|
115
|
+
event_before = _event_position(data)
|
|
116
|
+
return TimelineCursor(event_before=event_before, signal_before=signal_before)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _cursor_mapping(token: str) -> dict[str, object]:
|
|
120
|
+
try:
|
|
121
|
+
raw_data: object = json.loads(token)
|
|
122
|
+
except json.JSONDecodeError as error:
|
|
123
|
+
raise ValueError(TIMELINE_CURSOR_ERROR) from error
|
|
124
|
+
if not isinstance(raw_data, dict):
|
|
125
|
+
raise TypeError(TIMELINE_CURSOR_ERROR)
|
|
126
|
+
untyped_data = cast("dict[object, object]", raw_data)
|
|
127
|
+
if not all(isinstance(key, str) for key in untyped_data):
|
|
128
|
+
raise ValueError(TIMELINE_CURSOR_ERROR)
|
|
129
|
+
data = cast("dict[str, object]", untyped_data)
|
|
130
|
+
if type(data.get("version")) is not int or data.get("version") != CURSOR_VERSION:
|
|
131
|
+
raise ValueError(TIMELINE_CURSOR_ERROR)
|
|
132
|
+
return data
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _signal_positions(
|
|
136
|
+
data: Mapping[str, object], filters: TimelineFilters, roles: Sequence[str]
|
|
137
|
+
) -> dict[str, int | None]:
|
|
138
|
+
raw_signal_before = data.get("signal_before")
|
|
139
|
+
valid_roles = {item.value for item in Role}
|
|
140
|
+
if not isinstance(raw_signal_before, dict):
|
|
141
|
+
raise TypeError(TIMELINE_CURSOR_ERROR)
|
|
142
|
+
untyped_positions = cast("dict[object, object]", raw_signal_before)
|
|
143
|
+
if not all(isinstance(role, str) for role in untyped_positions):
|
|
144
|
+
raise TypeError(TIMELINE_CURSOR_ERROR)
|
|
145
|
+
signal_before = cast("dict[str, object]", untyped_positions)
|
|
146
|
+
if not set(signal_before) <= valid_roles:
|
|
147
|
+
raise ValueError(TIMELINE_CURSOR_ERROR)
|
|
148
|
+
if not set(signal_before) <= set(roles):
|
|
149
|
+
raise ValueError(TIMELINE_CURSOR_ERROR)
|
|
150
|
+
if filters.role is not None and set(signal_before) != set(roles):
|
|
151
|
+
raise ValueError(TIMELINE_CURSOR_ERROR)
|
|
152
|
+
positions: dict[str, int | None] = {}
|
|
153
|
+
for role, offset in signal_before.items():
|
|
154
|
+
if offset is not None and (
|
|
155
|
+
not isinstance(offset, int) or isinstance(offset, bool) or offset < -1
|
|
156
|
+
):
|
|
157
|
+
raise ValueError(TIMELINE_CURSOR_ERROR)
|
|
158
|
+
positions[role] = offset
|
|
159
|
+
return positions
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _event_position(data: Mapping[str, object]) -> tuple[str, int] | None:
|
|
163
|
+
value = data.get("event_before")
|
|
164
|
+
if value is None:
|
|
165
|
+
return None
|
|
166
|
+
if not isinstance(value, list):
|
|
167
|
+
raise TypeError(TIMELINE_CURSOR_ERROR)
|
|
168
|
+
position = cast("list[object]", value)
|
|
169
|
+
if len(position) != CURSOR_POSITION_FIELDS:
|
|
170
|
+
raise ValueError(TIMELINE_CURSOR_ERROR)
|
|
171
|
+
timestamp_value, event_id = position
|
|
172
|
+
if not isinstance(timestamp_value, str):
|
|
173
|
+
raise TypeError(TIMELINE_CURSOR_ERROR)
|
|
174
|
+
if not isinstance(event_id, int) or isinstance(event_id, bool) or event_id < 1:
|
|
175
|
+
raise ValueError(TIMELINE_CURSOR_ERROR)
|
|
176
|
+
try:
|
|
177
|
+
timestamp = datetime.fromisoformat(timestamp_value)
|
|
178
|
+
except ValueError as error:
|
|
179
|
+
raise ValueError(TIMELINE_CURSOR_ERROR) from error
|
|
180
|
+
if timestamp.tzinfo is None:
|
|
181
|
+
raise ValueError(TIMELINE_CURSOR_ERROR)
|
|
182
|
+
return timestamp_value, event_id
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def initial_cursor(paths: Mapping[str, Path]) -> TimelineCursor:
|
|
186
|
+
"""Capture each normalized source's current tail for a stable latest-page read."""
|
|
187
|
+
positions: dict[str, int | None] = {}
|
|
188
|
+
for role, path in paths.items():
|
|
189
|
+
try:
|
|
190
|
+
positions[role] = path.stat().st_size
|
|
191
|
+
except OSError:
|
|
192
|
+
positions[role] = None
|
|
193
|
+
return TimelineCursor(event_before=None, signal_before=positions)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def read_signal_batch(
|
|
197
|
+
path: Path,
|
|
198
|
+
*,
|
|
199
|
+
role: str,
|
|
200
|
+
before_offset: int | None,
|
|
201
|
+
filters: TimelineFilters,
|
|
202
|
+
limit: int,
|
|
203
|
+
scan_limit: int,
|
|
204
|
+
) -> SignalBatch:
|
|
205
|
+
"""Read at most `limit` matching signal rows backwards without retaining the file."""
|
|
206
|
+
if before_offset == -1:
|
|
207
|
+
return SignalBatch(items=(), replay_offset=-1, scan_offset=-1)
|
|
208
|
+
try:
|
|
209
|
+
snapshot = _signal_snapshot(path, before_offset)
|
|
210
|
+
if snapshot is None:
|
|
211
|
+
return _missing_batch(role, filters)
|
|
212
|
+
return _scan_signal_file(
|
|
213
|
+
path,
|
|
214
|
+
role=role,
|
|
215
|
+
end_position=snapshot,
|
|
216
|
+
filters=filters,
|
|
217
|
+
limit=limit,
|
|
218
|
+
scan_limit=scan_limit,
|
|
219
|
+
)
|
|
220
|
+
except (OSError, ValueError) as error:
|
|
221
|
+
return _unavailable_batch(role, filters, error)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _signal_snapshot(path: Path, before_offset: int | None) -> int | None:
|
|
225
|
+
try:
|
|
226
|
+
file_size = path.stat().st_size
|
|
227
|
+
except FileNotFoundError:
|
|
228
|
+
return None
|
|
229
|
+
end_position = file_size if before_offset is None else before_offset
|
|
230
|
+
if end_position > file_size:
|
|
231
|
+
message = f"recorded signal file changed before its saved cursor: {path}"
|
|
232
|
+
raise ValueError(message)
|
|
233
|
+
return end_position
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _missing_batch(role: str, filters: TimelineFilters) -> SignalBatch:
|
|
237
|
+
missing = _missing_signal(role)
|
|
238
|
+
if _matches_signal(missing, filters):
|
|
239
|
+
return SignalBatch(items=(missing,), replay_offset=None, scan_offset=0)
|
|
240
|
+
return SignalBatch(items=(), replay_offset=0, scan_offset=0)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _unavailable_batch(
|
|
244
|
+
role: str, filters: TimelineFilters, error: OSError | ValueError
|
|
245
|
+
) -> SignalBatch:
|
|
246
|
+
unavailable = _unavailable_signal(role, error)
|
|
247
|
+
if _matches_signal(unavailable, filters):
|
|
248
|
+
return SignalBatch(items=(unavailable,), replay_offset=-1, scan_offset=-1)
|
|
249
|
+
return SignalBatch(items=(), replay_offset=-1, scan_offset=-1)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _scan_signal_file(
|
|
253
|
+
path: Path,
|
|
254
|
+
*,
|
|
255
|
+
role: str,
|
|
256
|
+
end_position: int,
|
|
257
|
+
filters: TimelineFilters,
|
|
258
|
+
limit: int,
|
|
259
|
+
scan_limit: int,
|
|
260
|
+
) -> SignalBatch:
|
|
261
|
+
items: list[TimelineItem] = []
|
|
262
|
+
position = end_position
|
|
263
|
+
scanned = 0
|
|
264
|
+
while position > 0 and len(items) < limit and scanned < scan_limit:
|
|
265
|
+
parsed = _previous_signal(path, role, position)
|
|
266
|
+
if parsed is None:
|
|
267
|
+
position = 0
|
|
268
|
+
break
|
|
269
|
+
position = parsed.start_offset
|
|
270
|
+
scanned += 1
|
|
271
|
+
item = _timeline_item(parsed)
|
|
272
|
+
if _matches_signal(item, filters):
|
|
273
|
+
items.append(item)
|
|
274
|
+
replay_offset = items[0].signal_end_offset if items else position
|
|
275
|
+
scan_offset = position
|
|
276
|
+
return SignalBatch(
|
|
277
|
+
items=tuple(items),
|
|
278
|
+
replay_offset=replay_offset,
|
|
279
|
+
scan_offset=scan_offset,
|
|
280
|
+
has_more=position > 0,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def signal_category_is_requested(category: str) -> bool:
|
|
285
|
+
"""Whether an evidence category can contain normalized agent signal rows."""
|
|
286
|
+
return category in {
|
|
287
|
+
TIMELINE_ALL_CATEGORY,
|
|
288
|
+
TIMELINE_AGENT_CATEGORY,
|
|
289
|
+
TIMELINE_SIGNAL_CATEGORY,
|
|
290
|
+
TIMELINE_TOOL_CATEGORY,
|
|
291
|
+
TIMELINE_MESSAGE_CATEGORY,
|
|
292
|
+
TIMELINE_USAGE_CATEGORY,
|
|
293
|
+
TIMELINE_EVIDENCE_CATEGORY,
|
|
294
|
+
TIMELINE_TRANSITION_CATEGORY,
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def event_category_is_requested(category: str) -> bool:
|
|
299
|
+
"""Whether an evidence category can contain engine event rows."""
|
|
300
|
+
return category in {
|
|
301
|
+
TIMELINE_ALL_CATEGORY,
|
|
302
|
+
TIMELINE_ACTIVITY_CATEGORY,
|
|
303
|
+
TIMELINE_ENGINE_CATEGORY,
|
|
304
|
+
TIMELINE_DECISION_CATEGORY,
|
|
305
|
+
TIMELINE_TRANSITION_CATEGORY,
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _previous_signal(
|
|
310
|
+
path: Path,
|
|
311
|
+
role: str,
|
|
312
|
+
end_position: int,
|
|
313
|
+
) -> ParsedSignal | None:
|
|
314
|
+
with path.open("rb") as handle:
|
|
315
|
+
handle.seek(end_position - 1)
|
|
316
|
+
last_byte = handle.read(1)
|
|
317
|
+
incomplete = last_byte != b"\n"
|
|
318
|
+
line_end = end_position - 1 if last_byte == b"\n" else end_position
|
|
319
|
+
if line_end <= 0:
|
|
320
|
+
return None
|
|
321
|
+
search_end = line_end
|
|
322
|
+
line_start = 0
|
|
323
|
+
while search_end > 0:
|
|
324
|
+
block_start = max(search_end - SIGNAL_LINE_BLOCK_BYTES, 0)
|
|
325
|
+
handle.seek(block_start)
|
|
326
|
+
block = handle.read(search_end - block_start)
|
|
327
|
+
delimiter = block.rfind(b"\n")
|
|
328
|
+
if delimiter >= 0:
|
|
329
|
+
line_start = block_start + delimiter + 1
|
|
330
|
+
break
|
|
331
|
+
search_end = block_start
|
|
332
|
+
handle.seek(line_start)
|
|
333
|
+
raw = handle.read(line_end - line_start)
|
|
334
|
+
return parse_signal_row(
|
|
335
|
+
raw,
|
|
336
|
+
role=role,
|
|
337
|
+
start_offset=line_start,
|
|
338
|
+
end_offset=end_position,
|
|
339
|
+
incomplete=incomplete,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _timeline_item(signal: ParsedSignal) -> TimelineItem:
|
|
344
|
+
timestamp_error = signal.timestamp_error
|
|
345
|
+
if signal.incomplete:
|
|
346
|
+
timestamp_error = "JSONL row has not ended with a newline"
|
|
347
|
+
return TimelineItem(
|
|
348
|
+
identity=f"signal:{signal.role}:{signal.start_offset}",
|
|
349
|
+
timestamp=signal.timestamp,
|
|
350
|
+
source=TIMELINE_SOURCE_AGENT,
|
|
351
|
+
category=signal.category,
|
|
352
|
+
summary=signal.summary,
|
|
353
|
+
phrase=signal.phrase,
|
|
354
|
+
detail=signal.detail,
|
|
355
|
+
role=signal.role,
|
|
356
|
+
turn=signal.turn,
|
|
357
|
+
signal_offset=signal.start_offset,
|
|
358
|
+
signal_end_offset=signal.end_offset,
|
|
359
|
+
incomplete=signal.incomplete,
|
|
360
|
+
timestamp_error=timestamp_error,
|
|
361
|
+
listed=signal.listed,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _missing_signal(role: str) -> TimelineItem:
|
|
366
|
+
return TimelineItem(
|
|
367
|
+
identity=f"missing:{role}",
|
|
368
|
+
timestamp=None,
|
|
369
|
+
source=TIMELINE_SOURCE_AGENT,
|
|
370
|
+
category=TIMELINE_EVIDENCE_CATEGORY,
|
|
371
|
+
summary=MISSING_SIGNAL_SUMMARY,
|
|
372
|
+
phrase=Phrase(MISSING_SIGNAL_SUMMARY),
|
|
373
|
+
detail=TIMELINE_MISSING_DETAIL,
|
|
374
|
+
role=role,
|
|
375
|
+
turn=None,
|
|
376
|
+
signal_offset=0,
|
|
377
|
+
signal_end_offset=None,
|
|
378
|
+
timestamp_error="recorded timestamp is missing",
|
|
379
|
+
listed=False,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _unavailable_signal(role: str, error: OSError | ValueError) -> TimelineItem:
|
|
384
|
+
return TimelineItem(
|
|
385
|
+
identity=f"{TIMELINE_READ_ERROR_PREFIX}{role}",
|
|
386
|
+
timestamp=None,
|
|
387
|
+
source=TIMELINE_SOURCE_AGENT,
|
|
388
|
+
category=TIMELINE_EVIDENCE_CATEGORY,
|
|
389
|
+
summary=TIMELINE_READ_ERROR_SUMMARY,
|
|
390
|
+
phrase=Phrase(TIMELINE_READ_ERROR_SUMMARY, GLYPH_ATTENTION, Tone.WARNING),
|
|
391
|
+
detail=str(error),
|
|
392
|
+
role=role,
|
|
393
|
+
turn=None,
|
|
394
|
+
signal_offset=None,
|
|
395
|
+
signal_end_offset=None,
|
|
396
|
+
timestamp_error="recorded timestamp is unavailable because the source could not be read",
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _matches_signal(item: TimelineItem, filters: TimelineFilters) -> bool:
|
|
401
|
+
if filters.role is not None and filters.role != item.role:
|
|
402
|
+
return False
|
|
403
|
+
if item.identity.startswith(TIMELINE_READ_ERROR_PREFIX):
|
|
404
|
+
return filters.category in {
|
|
405
|
+
TIMELINE_ALL_CATEGORY,
|
|
406
|
+
TIMELINE_AGENT_CATEGORY,
|
|
407
|
+
TIMELINE_SIGNAL_CATEGORY,
|
|
408
|
+
TIMELINE_EVIDENCE_CATEGORY,
|
|
409
|
+
}
|
|
410
|
+
if filters.turn is not None and filters.turn != item.turn:
|
|
411
|
+
return False
|
|
412
|
+
if filters.category == TIMELINE_AGENT_CATEGORY:
|
|
413
|
+
return item.listed
|
|
414
|
+
if filters.category in {TIMELINE_ALL_CATEGORY, TIMELINE_SIGNAL_CATEGORY}:
|
|
415
|
+
return True
|
|
416
|
+
return filters.category == item.category
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Render keys and values as TOML text that reads back unchanged."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
import re
|
|
7
|
+
from typing import TYPE_CHECKING, cast
|
|
8
|
+
|
|
9
|
+
from shikiru.errors import ConfigError
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from collections.abc import Mapping, Sequence
|
|
13
|
+
|
|
14
|
+
BARE_KEY = re.compile(r"[A-Za-z0-9_-]+")
|
|
15
|
+
SHORT_ESCAPES: Mapping[str, str] = {
|
|
16
|
+
"\b": "\\b",
|
|
17
|
+
"\t": "\\t",
|
|
18
|
+
"\n": "\\n",
|
|
19
|
+
"\f": "\\f",
|
|
20
|
+
"\r": "\\r",
|
|
21
|
+
'"': '\\"',
|
|
22
|
+
"\\": "\\\\",
|
|
23
|
+
}
|
|
24
|
+
LAST_CONTROL_CODE = 0x1F
|
|
25
|
+
DELETE_CODE = 0x7F
|
|
26
|
+
VALUE_SEPARATOR = ", "
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def toml_string(text: str) -> str:
|
|
30
|
+
"""Return `text` as a TOML basic string, keeping non-ASCII characters literal."""
|
|
31
|
+
parts: list[str] = []
|
|
32
|
+
for character in text:
|
|
33
|
+
code = ord(character)
|
|
34
|
+
if character in SHORT_ESCAPES:
|
|
35
|
+
parts.append(SHORT_ESCAPES[character])
|
|
36
|
+
elif code <= LAST_CONTROL_CODE or code == DELETE_CODE:
|
|
37
|
+
parts.append(f"\\u{code:04X}")
|
|
38
|
+
else:
|
|
39
|
+
parts.append(character)
|
|
40
|
+
return f'"{"".join(parts)}"'
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def toml_key(key: str) -> str:
|
|
44
|
+
"""Return `key` bare when TOML allows it, otherwise as one quoted key."""
|
|
45
|
+
return key if BARE_KEY.fullmatch(key) else toml_string(key)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def toml_value(value: object) -> str:
|
|
49
|
+
"""Return a bool, int, finite float, string or list of those as TOML text."""
|
|
50
|
+
if isinstance(value, bool):
|
|
51
|
+
return str(value).lower()
|
|
52
|
+
if isinstance(value, int):
|
|
53
|
+
return str(value)
|
|
54
|
+
if isinstance(value, float) and math.isfinite(value):
|
|
55
|
+
return repr(value)
|
|
56
|
+
if isinstance(value, str):
|
|
57
|
+
return toml_string(value)
|
|
58
|
+
if isinstance(value, (list, tuple)):
|
|
59
|
+
items = cast("Sequence[object]", value)
|
|
60
|
+
return f"[{VALUE_SEPARATOR.join(toml_value(item) for item in items)}]"
|
|
61
|
+
message = f"cannot write {value!r} to the project config"
|
|
62
|
+
raise ConfigError(message)
|
shikiru/palette.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Shared semantic colors for terminal and dashboard surfaces."""
|
|
2
|
+
|
|
3
|
+
BACKGROUND_COLOR = "#11151b"
|
|
4
|
+
SURFACE_COLOR = "#171d25"
|
|
5
|
+
RAISED_SURFACE_COLOR = "#171d25"
|
|
6
|
+
SELECTED_SURFACE_COLOR = "#233340"
|
|
7
|
+
DETAIL_SURFACE_COLOR = "#1c2530"
|
|
8
|
+
NEUTRAL_BORDER_COLOR = "#3d4654"
|
|
9
|
+
PRIMARY_TEXT_COLOR = "#dce2e9"
|
|
10
|
+
SECONDARY_TEXT_COLOR = "#9aa5b5"
|
|
11
|
+
STEEL_BLUE = "#83bddd"
|
|
12
|
+
SAGE = "#90cba6"
|
|
13
|
+
AMBER = "#dfb16c"
|
|
14
|
+
DUSTY_ROSE = "#e3a0a0"
|
|
15
|
+
SCROLLBAR_THUMB_COLOR = "#344b5d"
|
|
16
|
+
SCROLLBAR_THUMB_HOVER_COLOR = "#415e72"
|
|
17
|
+
SCROLLBAR_THUMB_ACTIVE_COLOR = STEEL_BLUE
|
|
18
|
+
SCROLLBAR_TRACK_COLOR = "#000008"
|
shikiru/paths.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Project-local paths defined by the runtime layout."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
SHIKIRU_DIR_NAME = ".shikiru"
|
|
11
|
+
PROJECT_CONFIG_RELATIVE_PATH = f"{SHIKIRU_DIR_NAME}/config.toml"
|
|
12
|
+
PROMPT_OVERRIDES_RELATIVE_PATH = f"{SHIKIRU_DIR_NAME}/prompts"
|
|
13
|
+
STATE_DB_FILENAME = "state.db"
|
|
14
|
+
SCHEMA_BACKUPS_DIR_NAME = "backups"
|
|
15
|
+
ENGINE_LOCK_FILENAME = "engine.lock"
|
|
16
|
+
ENGINE_LOG_DIR = "logs"
|
|
17
|
+
ENGINE_LOG_FILENAME = "engine.log"
|
|
18
|
+
ENGINE_OUTPUT_FILENAME = "engine.out"
|
|
19
|
+
WORKTREES_DIR_NAME = "worktrees"
|
|
20
|
+
RUNS_DIR_NAME = "runs"
|
|
21
|
+
TURNS_DIR_NAME = "turns"
|
|
22
|
+
MESSAGES_DIR_NAME = "messages"
|
|
23
|
+
PROBE_DIR_NAME = "probe"
|
|
24
|
+
PROBE_REPO_DIR_NAME = "repo"
|
|
25
|
+
PROBE_RUN_DIR_NAME = "run"
|
|
26
|
+
EXCLUDE_LINE = f"/{SHIKIRU_DIR_NAME}/"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def project_state_dir(project_root: Path) -> Path:
|
|
30
|
+
"""Return the project's `.shikiru/` state directory."""
|
|
31
|
+
return project_root / SHIKIRU_DIR_NAME
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def project_config_path(project_root: Path) -> Path:
|
|
35
|
+
"""Return the project's settings file path."""
|
|
36
|
+
return project_root / PROJECT_CONFIG_RELATIVE_PATH
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def state_db_path(project_root: Path) -> Path:
|
|
40
|
+
"""Return the project's SQLite state database path."""
|
|
41
|
+
return project_state_dir(project_root) / STATE_DB_FILENAME
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def engine_lock_path(project_root: Path) -> Path:
|
|
45
|
+
"""Return the project's engine liveness lock path."""
|
|
46
|
+
return project_state_dir(project_root) / ENGINE_LOCK_FILENAME
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def engine_log_path(project_root: Path) -> Path:
|
|
50
|
+
"""Return the project's engine log path."""
|
|
51
|
+
return project_state_dir(project_root) / ENGINE_LOG_DIR / ENGINE_LOG_FILENAME
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def engine_output_path(project_root: Path) -> Path:
|
|
55
|
+
"""Return the file holding the stdout and stderr of the engine process `start` spawned."""
|
|
56
|
+
return project_state_dir(project_root) / ENGINE_LOG_DIR / ENGINE_OUTPUT_FILENAME
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def runs_root(project_root: Path) -> Path:
|
|
60
|
+
"""Return the `.shikiru/runs/` directory holding every task's run directory."""
|
|
61
|
+
return project_state_dir(project_root) / RUNS_DIR_NAME
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def run_dir(project_root: Path, task_id: str) -> Path:
|
|
65
|
+
"""Return `task_id`'s `.shikiru/runs/` directory."""
|
|
66
|
+
return runs_root(project_root) / task_id
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def turns_dir(project_root: Path, task_id: str) -> Path:
|
|
70
|
+
"""Return `task_id`'s turn artifacts directory."""
|
|
71
|
+
return run_dir(project_root, task_id) / TURNS_DIR_NAME
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def task_root(project_root: Path, task_id: str) -> Path:
|
|
75
|
+
"""Return `task_id`'s `.shikiru/worktrees/` directory."""
|
|
76
|
+
return project_state_dir(project_root) / WORKTREES_DIR_NAME / task_id
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def queued_messages_dir(project_root: Path, task_id: str, role: str) -> Path:
|
|
80
|
+
"""Return the directory holding `role`'s queued operator messages for `task_id`."""
|
|
81
|
+
return run_dir(project_root, task_id) / MESSAGES_DIR_NAME / role
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def probe_repo_path(project_root: Path) -> Path:
|
|
85
|
+
"""Return the scratch repository `doctor --probe` runs its turns in."""
|
|
86
|
+
return project_state_dir(project_root) / PROBE_DIR_NAME / PROBE_REPO_DIR_NAME
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def probe_run_dir(project_root: Path) -> Path:
|
|
90
|
+
"""Return the run directory `doctor --probe` keeps its turn artifacts in."""
|
|
91
|
+
return project_state_dir(project_root) / PROBE_DIR_NAME / PROBE_RUN_DIR_NAME
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Built-in agent plugins."""
|