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,271 @@
|
|
|
1
|
+
"""Human one-line phrases for recorded engine events and normalized agent signals."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import Callable, Mapping
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from enum import StrEnum
|
|
9
|
+
from typing import TYPE_CHECKING, cast
|
|
10
|
+
|
|
11
|
+
from shikiru.agents.contracts import (
|
|
12
|
+
Compacted,
|
|
13
|
+
Message,
|
|
14
|
+
QuotaReported,
|
|
15
|
+
SessionStarted,
|
|
16
|
+
ToolFinished,
|
|
17
|
+
ToolOutcome,
|
|
18
|
+
ToolStarted,
|
|
19
|
+
TurnEnded,
|
|
20
|
+
TurnFailed,
|
|
21
|
+
UsageReported,
|
|
22
|
+
)
|
|
23
|
+
from shikiru.domain.events import EventKind
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from shikiru.domain.events import Event
|
|
27
|
+
|
|
28
|
+
GLYPH_OK = "\N{CHECK MARK}"
|
|
29
|
+
GLYPH_FAIL = "\N{BALLOT X}"
|
|
30
|
+
GLYPH_ATTENTION = "!"
|
|
31
|
+
GLYPH_ALARM = "\N{DOUBLE EXCLAMATION MARK}\N{VARIATION SELECTOR-15}"
|
|
32
|
+
NO_GLYPH = ""
|
|
33
|
+
PHRASE_SEPARATOR = " · "
|
|
34
|
+
SIGNAL_MESSAGE = Message.__name__
|
|
35
|
+
SIGNAL_TOOL_STARTED = ToolStarted.__name__
|
|
36
|
+
SIGNAL_TOOL_FINISHED = ToolFinished.__name__
|
|
37
|
+
SIGNAL_TURN_ENDED = TurnEnded.__name__
|
|
38
|
+
SIGNAL_TURN_FAILED = TurnFailed.__name__
|
|
39
|
+
_BOOKKEEPING = frozenset(
|
|
40
|
+
{
|
|
41
|
+
SessionStarted.__name__,
|
|
42
|
+
UsageReported.__name__,
|
|
43
|
+
QuotaReported.__name__,
|
|
44
|
+
Compacted.__name__,
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
_WORD_SEPARATOR = " "
|
|
48
|
+
_QUOTE = '"'
|
|
49
|
+
_ROUND_WORD = "round"
|
|
50
|
+
_LIST_SEPARATOR = ", "
|
|
51
|
+
_EXIT_LABEL = "exit"
|
|
52
|
+
_SUCCESS_EXIT_CODE = 0
|
|
53
|
+
_TURN_ENDED_LABEL = "turn ended"
|
|
54
|
+
_TASK_ID_SEPARATOR = " "
|
|
55
|
+
_DETAIL_FIELDS = ("command", "file_path", "path", "pattern", "url", "query")
|
|
56
|
+
_TOOL_VERBS = {
|
|
57
|
+
"Bash": "ran",
|
|
58
|
+
"command": "ran",
|
|
59
|
+
"Edit": "edited",
|
|
60
|
+
"MultiEdit": "edited",
|
|
61
|
+
"Write": "edited",
|
|
62
|
+
"NotebookEdit": "edited",
|
|
63
|
+
"file_change": "edited",
|
|
64
|
+
"Read": "read",
|
|
65
|
+
"Grep": "searched",
|
|
66
|
+
"Glob": "searched",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class Tone(StrEnum):
|
|
71
|
+
"""How strongly a phrase should stand out."""
|
|
72
|
+
|
|
73
|
+
PLAIN = "plain"
|
|
74
|
+
SUCCESS = "success"
|
|
75
|
+
FAILURE = "failure"
|
|
76
|
+
WARNING = "warning"
|
|
77
|
+
CRITICAL = "critical"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class Phrase:
|
|
82
|
+
"""A one-line human description with its status glyph and tone."""
|
|
83
|
+
|
|
84
|
+
text: str
|
|
85
|
+
glyph: str = NO_GLYPH
|
|
86
|
+
tone: Tone = Tone.PLAIN
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
type Detail = Callable[[Event], str]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(frozen=True)
|
|
93
|
+
class _EventSpec:
|
|
94
|
+
label: str
|
|
95
|
+
glyph: str = NO_GLYPH
|
|
96
|
+
tone: Tone = Tone.PLAIN
|
|
97
|
+
detail: Detail | None = None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _field(key: str) -> Detail:
|
|
101
|
+
def read(event: Event) -> str:
|
|
102
|
+
value = event.data.get(key)
|
|
103
|
+
return "" if value is None else str(value)
|
|
104
|
+
|
|
105
|
+
return read
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _message(event: Event) -> str:
|
|
109
|
+
prefix = f"{event.task_id}{_TASK_ID_SEPARATOR}" if event.task_id else ""
|
|
110
|
+
return event.message.removeprefix(prefix)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _step_round(event: Event) -> str:
|
|
114
|
+
parts = [str(event.data.get("step", ""))]
|
|
115
|
+
iteration = event.data.get("iteration")
|
|
116
|
+
if isinstance(iteration, int) and iteration >= 1:
|
|
117
|
+
parts.append(f"{_ROUND_WORD} {iteration}")
|
|
118
|
+
return _LIST_SEPARATOR.join(part for part in parts if part)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _rung_change(event: Event) -> str:
|
|
122
|
+
moved = f"{event.data.get('from')} to {event.data.get('to')}"
|
|
123
|
+
return PHRASE_SEPARATOR.join(part for part in (_field("role")(event), moved) if part)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _new_tasks(event: Event) -> str:
|
|
127
|
+
count = len(event.data.get("new_task_ids") or ())
|
|
128
|
+
return f"{count} new" if count else ""
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
_VERDICTS = {
|
|
132
|
+
"accept": Phrase("accepted", GLYPH_OK, Tone.SUCCESS),
|
|
133
|
+
"changes_requested": Phrase("changes requested", GLYPH_FAIL, Tone.FAILURE),
|
|
134
|
+
"escalate": Phrase("escalate", GLYPH_ATTENTION, Tone.WARNING),
|
|
135
|
+
"supersede": Phrase("superseded"),
|
|
136
|
+
}
|
|
137
|
+
_EVENTS: dict[str, _EventSpec] = {
|
|
138
|
+
EventKind.TASK_QUEUED: _EventSpec("task queued"),
|
|
139
|
+
EventKind.TASK_STARTED: _EventSpec("task started", detail=_field("slot")),
|
|
140
|
+
EventKind.DISPATCH_FAILED: _EventSpec(
|
|
141
|
+
"dispatch failed", GLYPH_FAIL, Tone.FAILURE, _field("error")
|
|
142
|
+
),
|
|
143
|
+
EventKind.STEP_STARTED: _EventSpec("step started", detail=_step_round),
|
|
144
|
+
EventKind.TURN_STARTED: _EventSpec("turn started", detail=_field("role")),
|
|
145
|
+
EventKind.TURN_COMPLETED: _EventSpec("turn completed", GLYPH_OK, Tone.SUCCESS, _field("role")),
|
|
146
|
+
EventKind.TURN_FAILED: _EventSpec("turn failed", GLYPH_FAIL, Tone.FAILURE, _message),
|
|
147
|
+
EventKind.TURN_STOPPED: _EventSpec("turn stopped", detail=_field("role")),
|
|
148
|
+
EventKind.NUDGE: _EventSpec("nudge sent", GLYPH_ATTENTION, Tone.WARNING, _field("role")),
|
|
149
|
+
EventKind.HELPER_ACTION: _EventSpec("helper", GLYPH_ATTENTION, Tone.WARNING, _message),
|
|
150
|
+
EventKind.RUNG_CHANGED: _EventSpec("rung changed", GLYPH_ATTENTION, Tone.WARNING, _rung_change),
|
|
151
|
+
EventKind.COMPACTED: _EventSpec("context compacted", detail=_field("role")),
|
|
152
|
+
EventKind.QUOTA_BLOCKED: _EventSpec(
|
|
153
|
+
"quota blocked", GLYPH_ATTENTION, Tone.WARNING, _field("agent")
|
|
154
|
+
),
|
|
155
|
+
EventKind.QUOTA_CLEARED: _EventSpec("quota cleared", detail=_field("agent")),
|
|
156
|
+
EventKind.SCOPE_CHANGE: _EventSpec("scope changed", detail=_new_tasks),
|
|
157
|
+
EventKind.HOLD_SET: _EventSpec("hold set", detail=_field("hold")),
|
|
158
|
+
EventKind.HOLD_CLEARED: _EventSpec("hold cleared", detail=_field("hold")),
|
|
159
|
+
EventKind.TAKEN_OVER: _EventSpec("taken over", detail=_field("role")),
|
|
160
|
+
EventKind.HANDED_BACK: _EventSpec("handed back", detail=_field("role")),
|
|
161
|
+
EventKind.TAKEOVER_LOST: _EventSpec(
|
|
162
|
+
"takeover lost", GLYPH_ATTENTION, Tone.WARNING, _field("role")
|
|
163
|
+
),
|
|
164
|
+
EventKind.OPERATOR_MESSAGE: _EventSpec("operator message", detail=_field("role")),
|
|
165
|
+
EventKind.ESCALATION: _EventSpec("escalated", GLYPH_ALARM, Tone.CRITICAL, _message),
|
|
166
|
+
EventKind.ESCALATION_RESOLVED: _EventSpec(
|
|
167
|
+
"escalation resolved", GLYPH_OK, Tone.SUCCESS, _field("resolution")
|
|
168
|
+
),
|
|
169
|
+
EventKind.INTEGRATION_STARTED: _EventSpec("integration started"),
|
|
170
|
+
EventKind.INTEGRATION_APPLYING: _EventSpec("integration applying"),
|
|
171
|
+
EventKind.INTEGRATION_WAITING: _EventSpec(
|
|
172
|
+
"integration waiting", GLYPH_ATTENTION, Tone.WARNING, _message
|
|
173
|
+
),
|
|
174
|
+
EventKind.PR_OPENED: _EventSpec("PR opened", GLYPH_OK, Tone.SUCCESS, _field("url")),
|
|
175
|
+
EventKind.PR_WITHDRAWN: _EventSpec("PR withdrawn", detail=_field("url")),
|
|
176
|
+
EventKind.TASK_DONE: _EventSpec("task done", GLYPH_OK, Tone.SUCCESS, _message),
|
|
177
|
+
EventKind.TASK_RETRIED: _EventSpec("task retried"),
|
|
178
|
+
EventKind.TASK_CANCELLED: _EventSpec("task cancelled", GLYPH_FAIL, Tone.FAILURE),
|
|
179
|
+
EventKind.PANE_RESTORED: _EventSpec("pane restored", detail=_field("pane")),
|
|
180
|
+
EventKind.ENGINE_FAULT: _EventSpec("engine fault", GLYPH_FAIL, Tone.FAILURE, _message),
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def kind_phrase(kind: str) -> Phrase | None:
|
|
185
|
+
"""Return the label, glyph and tone of an engine event kind, or None when it is unlisted."""
|
|
186
|
+
spec = _EVENTS.get(kind)
|
|
187
|
+
return None if spec is None else Phrase(spec.label, spec.glyph, spec.tone)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def verdict_phrase(decision: str) -> Phrase | None:
|
|
191
|
+
"""Return the phrase of a verdict decision, or None when the decision is unknown."""
|
|
192
|
+
return _VERDICTS.get(decision)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _joined(label: str, detail: str) -> str:
|
|
196
|
+
return f"{label}{PHRASE_SEPARATOR}{detail}" if detail else label
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def event_phrase(event: Event) -> Phrase:
|
|
200
|
+
"""Describe one engine event in a human phrase; an unlisted kind shows its recorded message."""
|
|
201
|
+
if event.kind is EventKind.VERDICT:
|
|
202
|
+
verdict = _VERDICTS.get(str(event.data.get("decision")))
|
|
203
|
+
if verdict is None:
|
|
204
|
+
return Phrase(_joined("verdict", event.message))
|
|
205
|
+
return Phrase(_joined("verdict", verdict.text), verdict.glyph, verdict.tone)
|
|
206
|
+
spec = _EVENTS.get(event.kind)
|
|
207
|
+
if spec is None:
|
|
208
|
+
return Phrase(event.message)
|
|
209
|
+
detail = spec.detail(event) if spec.detail is not None else ""
|
|
210
|
+
return Phrase(_joined(spec.label, detail), spec.glyph, spec.tone)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def tool_target(detail: str) -> str:
|
|
214
|
+
"""Return the command, path or query inside a tool's JSON detail, else the detail itself."""
|
|
215
|
+
try:
|
|
216
|
+
decoded: object = json.loads(detail)
|
|
217
|
+
except json.JSONDecodeError:
|
|
218
|
+
return detail
|
|
219
|
+
if not isinstance(decoded, dict):
|
|
220
|
+
return detail
|
|
221
|
+
fields = cast("dict[str, object]", decoded)
|
|
222
|
+
for key in _DETAIL_FIELDS:
|
|
223
|
+
value = fields.get(key)
|
|
224
|
+
if isinstance(value, str) and value:
|
|
225
|
+
return value
|
|
226
|
+
return detail
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _tool_started(data: Mapping[str, object]) -> Phrase:
|
|
230
|
+
name = str(data.get("name", ""))
|
|
231
|
+
target = tool_target(str(data.get("detail") or ""))
|
|
232
|
+
verb = _TOOL_VERBS.get(name, name)
|
|
233
|
+
return Phrase(" ".join(part for part in (verb, target) if part))
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _tool_finished(data: Mapping[str, object]) -> Phrase | None:
|
|
237
|
+
outcome = data.get("outcome")
|
|
238
|
+
exit_code = data.get("exit_code")
|
|
239
|
+
has_exit = type(exit_code) is int
|
|
240
|
+
if outcome not in (ToolOutcome.SUCCESS.value, ToolOutcome.FAILURE.value) and not has_exit:
|
|
241
|
+
return None
|
|
242
|
+
failed = outcome == ToolOutcome.FAILURE.value or (
|
|
243
|
+
has_exit and exit_code != _SUCCESS_EXIT_CODE and outcome != ToolOutcome.SUCCESS.value
|
|
244
|
+
)
|
|
245
|
+
label = f"{_EXIT_LABEL} {exit_code}" if has_exit else str(outcome)
|
|
246
|
+
glyph, tone = (GLYPH_FAIL, Tone.FAILURE) if failed else (GLYPH_OK, Tone.SUCCESS)
|
|
247
|
+
return Phrase(_joined(str(data.get("name", "")), label), glyph, tone)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _message_text(data: Mapping[str, object], key: str) -> str:
|
|
251
|
+
return _WORD_SEPARATOR.join(str(data.get(key) or "").split())
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def signal_phrase(signal: str, data: Mapping[str, object]) -> Phrase | None:
|
|
255
|
+
"""Describe one normalized agent signal; bookkeeping and empty signals have no phrase (None)."""
|
|
256
|
+
if signal == SIGNAL_MESSAGE:
|
|
257
|
+
text = _message_text(data, "text")
|
|
258
|
+
return Phrase(f"{_QUOTE}{text}{_QUOTE}") if text else None
|
|
259
|
+
if signal == SIGNAL_TOOL_STARTED:
|
|
260
|
+
return _tool_started(data)
|
|
261
|
+
if signal == SIGNAL_TOOL_FINISHED:
|
|
262
|
+
return _tool_finished(data)
|
|
263
|
+
if signal == SIGNAL_TURN_ENDED:
|
|
264
|
+
return Phrase(
|
|
265
|
+
_joined(_TURN_ENDED_LABEL, _message_text(data, "last_message")), GLYPH_OK, Tone.SUCCESS
|
|
266
|
+
)
|
|
267
|
+
if signal == SIGNAL_TURN_FAILED:
|
|
268
|
+
return Phrase(
|
|
269
|
+
f"{data.get('kind')}: {_message_text(data, 'message')}", GLYPH_FAIL, Tone.FAILURE
|
|
270
|
+
)
|
|
271
|
+
return None if signal in _BOOKKEEPING else Phrase(signal)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Reading and comparing the version a tool prints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
VERSION_PATTERN = re.compile(r"(\d+)\.(\d+)")
|
|
8
|
+
|
|
9
|
+
Version = tuple[int, int]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def parse_version(text: str) -> Version | None:
|
|
13
|
+
"""Return the first `major.minor` in `text`, ignoring suffixes like the `a` of `3.3a`."""
|
|
14
|
+
found = VERSION_PATTERN.search(text)
|
|
15
|
+
if found is None:
|
|
16
|
+
return None
|
|
17
|
+
major, minor = found.groups()
|
|
18
|
+
return int(major), int(minor)
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
"""Pure recorded workflow history projection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import StrEnum, auto
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from shikiru.domain.events import Event, EventKind
|
|
10
|
+
from shikiru.domain.reports import (
|
|
11
|
+
DECISION_ACCEPT,
|
|
12
|
+
DECISION_ANSWER,
|
|
13
|
+
DECISION_CHANGES_REQUESTED,
|
|
14
|
+
STATUS_COMPLETED,
|
|
15
|
+
ConsultAnswer,
|
|
16
|
+
ImplementationReport,
|
|
17
|
+
PlanReport,
|
|
18
|
+
Report,
|
|
19
|
+
ReportKind,
|
|
20
|
+
TriageReport,
|
|
21
|
+
Verdict,
|
|
22
|
+
)
|
|
23
|
+
from shikiru.domain.tasks import Mode, Status, Step, Task
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from collections.abc import Sequence
|
|
27
|
+
|
|
28
|
+
WORKFLOW_STEP_KEY = "step"
|
|
29
|
+
WORKFLOW_ITERATION_KEY = "iteration"
|
|
30
|
+
WORKFLOW_TURN_KEY = "turn"
|
|
31
|
+
WORKFLOW_DECISION_KEY = "decision"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class WorkflowMilestoneState(StrEnum):
|
|
35
|
+
"""Recorded semantic outcome for one workflow stage."""
|
|
36
|
+
|
|
37
|
+
DONE = auto()
|
|
38
|
+
CURRENT = auto()
|
|
39
|
+
LATER = auto()
|
|
40
|
+
SKIPPED = auto()
|
|
41
|
+
REWORK = auto()
|
|
42
|
+
UNKNOWN = auto()
|
|
43
|
+
STOPPED = auto()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class WorkflowReport:
|
|
48
|
+
"""One already-validated agent report with its retained turn identity."""
|
|
49
|
+
|
|
50
|
+
turn: str
|
|
51
|
+
kind: ReportKind
|
|
52
|
+
data: Report
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class WorkflowMilestone:
|
|
57
|
+
"""One stage and its recorded workflow outcome."""
|
|
58
|
+
|
|
59
|
+
stage: Step
|
|
60
|
+
state: WorkflowMilestoneState
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class WorkflowProgress:
|
|
65
|
+
"""Milestones projected from bounded task history and its completeness signal."""
|
|
66
|
+
|
|
67
|
+
milestones: tuple[WorkflowMilestone, ...]
|
|
68
|
+
history_complete: bool
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True)
|
|
72
|
+
class _StepEntry:
|
|
73
|
+
event_index: int
|
|
74
|
+
event_id: int
|
|
75
|
+
step: Step | None
|
|
76
|
+
iteration: int | None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def project_workflow(
|
|
80
|
+
task: Task,
|
|
81
|
+
events: Sequence[Event],
|
|
82
|
+
reports: Sequence[WorkflowReport],
|
|
83
|
+
*,
|
|
84
|
+
event_window_complete: bool,
|
|
85
|
+
) -> WorkflowProgress:
|
|
86
|
+
"""Project factual milestones from retained transitions and validated agent reports."""
|
|
87
|
+
history_complete = (
|
|
88
|
+
event_window_complete and bool(events) and events[0].kind is EventKind.TASK_QUEUED
|
|
89
|
+
)
|
|
90
|
+
entries = _step_entries(events)
|
|
91
|
+
entered_steps = {entry.step for entry in entries if entry.step is not None}
|
|
92
|
+
first_entered_step = entries[0].step if entries else None
|
|
93
|
+
reported_steps = {
|
|
94
|
+
step for report in reports if (step := _report_stage(report.kind)) is not None
|
|
95
|
+
}
|
|
96
|
+
completed_reports = _completed_reports(reports, events, entries)
|
|
97
|
+
stages = _workflow_stages(task, entered_steps, reported_steps, entries)
|
|
98
|
+
if not stages:
|
|
99
|
+
return WorkflowProgress((), history_complete)
|
|
100
|
+
recorded = entered_steps | reported_steps
|
|
101
|
+
settled = _settled_milestones(task, stages, recorded)
|
|
102
|
+
if settled is not None:
|
|
103
|
+
return WorkflowProgress(settled, history_complete)
|
|
104
|
+
stage_positions = {step: index for index, step in enumerate(stages)}
|
|
105
|
+
current_position = stage_positions.get(task.step) if task.step is not None else None
|
|
106
|
+
last_entries = _last_step_entries(entries)
|
|
107
|
+
verdicts = _verdicts(events)
|
|
108
|
+
|
|
109
|
+
def milestone_state(step: Step) -> WorkflowMilestoneState:
|
|
110
|
+
if step is task.step:
|
|
111
|
+
state = WorkflowMilestoneState.CURRENT
|
|
112
|
+
elif (
|
|
113
|
+
step is Step.TRIAGE
|
|
114
|
+
and history_complete
|
|
115
|
+
and first_entered_step in {Step.PLAN, Step.IMPLEMENT}
|
|
116
|
+
):
|
|
117
|
+
state = WorkflowMilestoneState.SKIPPED
|
|
118
|
+
elif step in entered_steps | reported_steps:
|
|
119
|
+
latest_entry = last_entries.get(step)
|
|
120
|
+
later_steps = {
|
|
121
|
+
entry.step
|
|
122
|
+
for entry in entries
|
|
123
|
+
if latest_entry is not None
|
|
124
|
+
and entry.step is not None
|
|
125
|
+
and entry.event_index > latest_entry.event_index
|
|
126
|
+
}
|
|
127
|
+
rework_requested = step is Step.VERIFY and any(
|
|
128
|
+
index > (latest_entry.event_index if latest_entry is not None else -1)
|
|
129
|
+
and decision == DECISION_CHANGES_REQUESTED
|
|
130
|
+
for index, decision in verdicts
|
|
131
|
+
)
|
|
132
|
+
if rework_requested:
|
|
133
|
+
state = WorkflowMilestoneState.REWORK
|
|
134
|
+
elif _completed_by_later_step(step, later_steps) or _report_completes_entered_stage(
|
|
135
|
+
step,
|
|
136
|
+
latest_entry.event_id if latest_entry is not None else None,
|
|
137
|
+
completed_reports,
|
|
138
|
+
):
|
|
139
|
+
state = WorkflowMilestoneState.DONE
|
|
140
|
+
else:
|
|
141
|
+
state = WorkflowMilestoneState.UNKNOWN
|
|
142
|
+
elif current_position is not None and stage_positions[step] > current_position:
|
|
143
|
+
state = WorkflowMilestoneState.LATER
|
|
144
|
+
else:
|
|
145
|
+
state = WorkflowMilestoneState.UNKNOWN
|
|
146
|
+
return state
|
|
147
|
+
|
|
148
|
+
milestones = tuple(
|
|
149
|
+
WorkflowMilestone(
|
|
150
|
+
stage=step,
|
|
151
|
+
state=milestone_state(step),
|
|
152
|
+
)
|
|
153
|
+
for step in stages
|
|
154
|
+
)
|
|
155
|
+
return WorkflowProgress(milestones, history_complete)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _settled_milestones(
|
|
159
|
+
task: Task, stages: tuple[Step, ...], recorded: set[Step]
|
|
160
|
+
) -> tuple[WorkflowMilestone, ...] | None:
|
|
161
|
+
if task.status in (Status.DONE, Status.CANCELLED):
|
|
162
|
+
return _terminal_milestones(task, stages, recorded)
|
|
163
|
+
if task.step is None and not recorded:
|
|
164
|
+
return tuple(WorkflowMilestone(step, WorkflowMilestoneState.LATER) for step in stages)
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _terminal_milestones(
|
|
169
|
+
task: Task, stages: tuple[Step, ...], recorded: set[Step]
|
|
170
|
+
) -> tuple[WorkflowMilestone, ...]:
|
|
171
|
+
def state(step: Step) -> WorkflowMilestoneState:
|
|
172
|
+
if task.status is Status.CANCELLED and step is task.step:
|
|
173
|
+
return WorkflowMilestoneState.STOPPED
|
|
174
|
+
if step in recorded or (step is Step.INTEGRATE and task.status is Status.DONE):
|
|
175
|
+
return WorkflowMilestoneState.DONE
|
|
176
|
+
return WorkflowMilestoneState.SKIPPED
|
|
177
|
+
|
|
178
|
+
return tuple(WorkflowMilestone(step, state(step)) for step in stages)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _step_entries(events: Sequence[Event]) -> list[_StepEntry]:
|
|
182
|
+
entries: list[_StepEntry] = []
|
|
183
|
+
for index, event in enumerate(events):
|
|
184
|
+
value = event.data.get(WORKFLOW_STEP_KEY)
|
|
185
|
+
if event.kind is not EventKind.STEP_STARTED:
|
|
186
|
+
continue
|
|
187
|
+
try:
|
|
188
|
+
step = Step(value) if isinstance(value, str) else None
|
|
189
|
+
except ValueError:
|
|
190
|
+
step = None
|
|
191
|
+
iteration = event.data.get(WORKFLOW_ITERATION_KEY)
|
|
192
|
+
entries.append(
|
|
193
|
+
_StepEntry(
|
|
194
|
+
index,
|
|
195
|
+
event.id,
|
|
196
|
+
step,
|
|
197
|
+
iteration if type(iteration) is int else None,
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
return entries
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _completed_reports(
|
|
204
|
+
reports: Sequence[WorkflowReport],
|
|
205
|
+
events: Sequence[Event],
|
|
206
|
+
entries: list[_StepEntry],
|
|
207
|
+
) -> set[tuple[Step, int]]:
|
|
208
|
+
report_entries: dict[str, tuple[Step, int]] = {}
|
|
209
|
+
for index, event in enumerate(events):
|
|
210
|
+
turn = event.data.get(WORKFLOW_TURN_KEY)
|
|
211
|
+
step_value = event.data.get(WORKFLOW_STEP_KEY)
|
|
212
|
+
iteration = event.data.get(WORKFLOW_ITERATION_KEY)
|
|
213
|
+
if (
|
|
214
|
+
event.kind is EventKind.TURN_STARTED
|
|
215
|
+
and isinstance(turn, str)
|
|
216
|
+
and isinstance(step_value, str)
|
|
217
|
+
and type(iteration) is int
|
|
218
|
+
):
|
|
219
|
+
try:
|
|
220
|
+
step = Step(step_value)
|
|
221
|
+
except ValueError:
|
|
222
|
+
continue
|
|
223
|
+
if not entries:
|
|
224
|
+
continue
|
|
225
|
+
latest_entry = next(
|
|
226
|
+
(entry for entry in reversed(entries) if entry.event_index < index),
|
|
227
|
+
None,
|
|
228
|
+
)
|
|
229
|
+
if latest_entry is None or (latest_entry.step, latest_entry.iteration) != (
|
|
230
|
+
step,
|
|
231
|
+
iteration,
|
|
232
|
+
):
|
|
233
|
+
continue
|
|
234
|
+
report_entries[turn] = (step, latest_entry.event_id)
|
|
235
|
+
completed: set[tuple[Step, int]] = set()
|
|
236
|
+
for report in reports:
|
|
237
|
+
stage = _report_stage(report.kind)
|
|
238
|
+
if stage is None or not _report_completes_stage(report.data):
|
|
239
|
+
continue
|
|
240
|
+
report_entry = report_entries.get(report.turn)
|
|
241
|
+
if report_entry is not None and report_entry[0] is stage:
|
|
242
|
+
completed.add((stage, report_entry[1]))
|
|
243
|
+
return completed
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _last_step_entries(
|
|
247
|
+
entries: list[_StepEntry],
|
|
248
|
+
) -> dict[Step, _StepEntry]:
|
|
249
|
+
last_entries: dict[Step, _StepEntry] = {}
|
|
250
|
+
for entry in entries:
|
|
251
|
+
if entry.step is not None:
|
|
252
|
+
last_entries[entry.step] = entry
|
|
253
|
+
return last_entries
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _verdicts(events: Sequence[Event]) -> list[tuple[int, object]]:
|
|
257
|
+
return [
|
|
258
|
+
(index, event.data.get(WORKFLOW_DECISION_KEY))
|
|
259
|
+
for index, event in enumerate(events)
|
|
260
|
+
if event.kind is EventKind.VERDICT
|
|
261
|
+
]
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _workflow_stages(
|
|
265
|
+
task: Task,
|
|
266
|
+
entered_steps: set[Step],
|
|
267
|
+
reported_steps: set[Step],
|
|
268
|
+
entries: list[_StepEntry],
|
|
269
|
+
) -> tuple[Step, ...]:
|
|
270
|
+
observed_steps = entered_steps | reported_steps
|
|
271
|
+
if task.mode is Mode.PLAN or task.step is Step.PLAN or Step.PLAN in observed_steps:
|
|
272
|
+
stages = [Step.TRIAGE, Step.PLAN, Step.INTEGRATE]
|
|
273
|
+
if Step.CONSULT in observed_steps or task.step is Step.CONSULT:
|
|
274
|
+
stages.append(Step.CONSULT)
|
|
275
|
+
if Step.VERIFY in entered_steps or task.step is Step.VERIFY:
|
|
276
|
+
stages.append(Step.VERIFY)
|
|
277
|
+
return tuple(stages)
|
|
278
|
+
if (
|
|
279
|
+
task.mode is Mode.IMPLEMENT
|
|
280
|
+
or task.step
|
|
281
|
+
in {
|
|
282
|
+
Step.IMPLEMENT,
|
|
283
|
+
Step.CONSULT,
|
|
284
|
+
Step.VERIFY,
|
|
285
|
+
Step.INTEGRATE,
|
|
286
|
+
}
|
|
287
|
+
or observed_steps & {Step.IMPLEMENT, Step.CONSULT, Step.VERIFY, Step.INTEGRATE}
|
|
288
|
+
):
|
|
289
|
+
stages = [Step.TRIAGE, Step.IMPLEMENT, Step.VERIFY, Step.INTEGRATE]
|
|
290
|
+
elif task.step is Step.TRIAGE:
|
|
291
|
+
stages = [Step.TRIAGE]
|
|
292
|
+
else:
|
|
293
|
+
return ()
|
|
294
|
+
if Step.CONSULT in observed_steps or task.step is Step.CONSULT:
|
|
295
|
+
if _consult_follows_integration(task, entries):
|
|
296
|
+
stages.append(Step.CONSULT)
|
|
297
|
+
else:
|
|
298
|
+
stages.insert(stages.index(Step.IMPLEMENT) + 1, Step.CONSULT)
|
|
299
|
+
return tuple(stages)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _consult_follows_integration(task: Task, entries: list[_StepEntry]) -> bool:
|
|
303
|
+
if task.step is Step.CONSULT and task.status is Status.INTEGRATING:
|
|
304
|
+
return True
|
|
305
|
+
for index in range(len(entries) - 1, -1, -1):
|
|
306
|
+
entry = entries[index]
|
|
307
|
+
if entry.step is not Step.CONSULT or entry.iteration != task.iteration:
|
|
308
|
+
continue
|
|
309
|
+
if index == 0:
|
|
310
|
+
return False
|
|
311
|
+
previous = entries[index - 1]
|
|
312
|
+
return previous.step is Step.INTEGRATE and previous.iteration == task.iteration
|
|
313
|
+
if task.step is Step.CONSULT and entries:
|
|
314
|
+
latest = entries[-1]
|
|
315
|
+
return latest.step is Step.INTEGRATE and latest.iteration == task.iteration
|
|
316
|
+
return False
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _completed_by_later_step(stage: Step, later_steps: set[Step]) -> bool:
|
|
320
|
+
match stage:
|
|
321
|
+
case Step.TRIAGE:
|
|
322
|
+
return bool(later_steps & {Step.PLAN, Step.IMPLEMENT})
|
|
323
|
+
case Step.PLAN:
|
|
324
|
+
return Step.INTEGRATE in later_steps
|
|
325
|
+
case Step.IMPLEMENT:
|
|
326
|
+
return bool(later_steps & {Step.VERIFY, Step.CONSULT})
|
|
327
|
+
case Step.CONSULT:
|
|
328
|
+
return bool(later_steps & {Step.IMPLEMENT, Step.INTEGRATE})
|
|
329
|
+
case Step.VERIFY:
|
|
330
|
+
return Step.INTEGRATE in later_steps
|
|
331
|
+
case Step.INTEGRATE:
|
|
332
|
+
return False
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _report_stage(kind: ReportKind) -> Step | None:
|
|
336
|
+
match kind:
|
|
337
|
+
case ReportKind.TRIAGE:
|
|
338
|
+
return Step.TRIAGE
|
|
339
|
+
case ReportKind.PLAN:
|
|
340
|
+
return Step.PLAN
|
|
341
|
+
case ReportKind.IMPLEMENTATION:
|
|
342
|
+
return Step.IMPLEMENT
|
|
343
|
+
case ReportKind.CONSULT:
|
|
344
|
+
return Step.CONSULT
|
|
345
|
+
case ReportKind.VERDICT:
|
|
346
|
+
return Step.VERIFY
|
|
347
|
+
case (
|
|
348
|
+
ReportKind.FEEDBACK_ANALYSIS | ReportKind.FEEDBACK_REWORK | ReportKind.FEEDBACK_VERDICT
|
|
349
|
+
):
|
|
350
|
+
return None
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _report_completes_stage(report: Report) -> bool:
|
|
354
|
+
match report:
|
|
355
|
+
case TriageReport() | PlanReport():
|
|
356
|
+
return True
|
|
357
|
+
case ImplementationReport(status=status) if status == STATUS_COMPLETED:
|
|
358
|
+
return True
|
|
359
|
+
case ConsultAnswer(decision=decision) if decision == DECISION_ANSWER:
|
|
360
|
+
return True
|
|
361
|
+
case Verdict(decision=decision) if decision == DECISION_ACCEPT:
|
|
362
|
+
return True
|
|
363
|
+
case _:
|
|
364
|
+
return False
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _report_completes_entered_stage(
|
|
368
|
+
stage: Step,
|
|
369
|
+
entered_identity: int | None,
|
|
370
|
+
completed_reports: set[tuple[Step, int]],
|
|
371
|
+
) -> bool:
|
|
372
|
+
return entered_identity is not None and (stage, entered_identity) in completed_reports
|
|
File without changes
|