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,498 @@
|
|
|
1
|
+
"""Report models an agent's turn produces, validated by `shikiru report`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from enum import StrEnum, auto
|
|
7
|
+
from typing import TYPE_CHECKING, Annotated, Any, Final, Literal, Self
|
|
8
|
+
|
|
9
|
+
from pydantic import Field, StringConstraints, model_validator
|
|
10
|
+
|
|
11
|
+
from shikiru.config.settings import ComplexityName
|
|
12
|
+
from shikiru.domain.tasks import TASK_TITLE_MAX_LENGTH, DomainModel, Mode
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from collections.abc import Sequence
|
|
16
|
+
|
|
17
|
+
DECISION_ACCEPT: Final = "accept"
|
|
18
|
+
DECISION_SUPERSEDE: Final = "supersede"
|
|
19
|
+
DECISION_CHANGES_REQUESTED: Final = "changes_requested"
|
|
20
|
+
DECISION_ANSWER: Final = "answer"
|
|
21
|
+
DECISION_ESCALATE: Final = "escalate"
|
|
22
|
+
SEVERITY_BLOCKER: Final = "blocker"
|
|
23
|
+
STATUS_COMPLETED: Final = "completed"
|
|
24
|
+
REPORT_DECISION_FIELD: Final = "decision"
|
|
25
|
+
COMMIT_SUBJECT_FIELD: Final = "commit_subject"
|
|
26
|
+
|
|
27
|
+
REPORT_JSON_INDENT = 2
|
|
28
|
+
CURRENT_TURN_LINK_NAME = "current_turn"
|
|
29
|
+
PROMPT_FILENAME = "prompt.md"
|
|
30
|
+
FACTS_FILENAME = "facts.md"
|
|
31
|
+
REPORT_KIND_FILENAME = "report.kind"
|
|
32
|
+
FEEDBACK_FILENAME = "feedback.json"
|
|
33
|
+
REPORT_SCHEMA_FILENAME = "report.schema.json"
|
|
34
|
+
REPORT_FILENAME = "report.json"
|
|
35
|
+
HELPER_REPORT_FILENAME = "helper.json"
|
|
36
|
+
|
|
37
|
+
SIBLING_REFERENCE_PREFIX = "#"
|
|
38
|
+
DEPENDS_ON_PATTERN = rf"^(T-\d+|{SIBLING_REFERENCE_PREFIX}\d+)$"
|
|
39
|
+
|
|
40
|
+
COMMIT_SUBJECT_MAX_LENGTH: Final = 72
|
|
41
|
+
_COMMIT_SUBJECT_LINE_BREAK = re.compile(r"[\r\n]")
|
|
42
|
+
|
|
43
|
+
_TaskReference = Annotated[str, StringConstraints(pattern=DEPENDS_ON_PATTERN)]
|
|
44
|
+
_Text = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def resolve_dependency(reference: str, sibling_ids: Sequence[str]) -> str:
|
|
48
|
+
"""Replace a report-local sibling reference with the assigned task id."""
|
|
49
|
+
if reference.startswith(SIBLING_REFERENCE_PREFIX):
|
|
50
|
+
return sibling_ids[int(reference.removeprefix(SIBLING_REFERENCE_PREFIX)) - 1]
|
|
51
|
+
return reference
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _require_iff(*, condition: bool, present: bool, field: str, rule: str) -> None:
|
|
55
|
+
if condition and not present:
|
|
56
|
+
message = f"{field}: required when {rule}"
|
|
57
|
+
raise ValueError(message)
|
|
58
|
+
if not condition and present:
|
|
59
|
+
message = f"{field}: must be empty unless {rule}"
|
|
60
|
+
raise ValueError(message)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _require_commit_subject(subject: str) -> None:
|
|
64
|
+
if (
|
|
65
|
+
not subject
|
|
66
|
+
or subject != subject.strip()
|
|
67
|
+
or _COMMIT_SUBJECT_LINE_BREAK.search(subject)
|
|
68
|
+
or len(subject) > COMMIT_SUBJECT_MAX_LENGTH
|
|
69
|
+
):
|
|
70
|
+
message = (
|
|
71
|
+
f"commit_subject: {subject!r} must be a non-empty, single-line subject of at most "
|
|
72
|
+
f"{COMMIT_SUBJECT_MAX_LENGTH} characters"
|
|
73
|
+
)
|
|
74
|
+
raise ValueError(message)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _require_unique(identities: list[str], field: str) -> None:
|
|
78
|
+
duplicates = sorted({identity for identity in identities if identities.count(identity) > 1})
|
|
79
|
+
if duplicates:
|
|
80
|
+
message = f"{field}: duplicate ids {duplicates}"
|
|
81
|
+
raise ValueError(message)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _require_sibling_references_in_range(new_tasks: list[NewTask], field: str) -> None:
|
|
85
|
+
for new_task in new_tasks:
|
|
86
|
+
for reference in new_task.depends_on:
|
|
87
|
+
if reference.startswith(SIBLING_REFERENCE_PREFIX) and not (
|
|
88
|
+
1 <= int(reference[1:]) <= len(new_tasks)
|
|
89
|
+
):
|
|
90
|
+
message = (
|
|
91
|
+
f"{field}: sibling reference {reference!r} must be #1..#{len(new_tasks)} "
|
|
92
|
+
"(1-based)"
|
|
93
|
+
)
|
|
94
|
+
raise ValueError(message)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class NewTask(DomainModel):
|
|
98
|
+
"""A task to enqueue from a plan or verdict report."""
|
|
99
|
+
|
|
100
|
+
title: str
|
|
101
|
+
request: str
|
|
102
|
+
priority: int = 0
|
|
103
|
+
depends_on: list[_TaskReference] = Field(
|
|
104
|
+
default_factory=list,
|
|
105
|
+
description=(
|
|
106
|
+
"Each item is an existing 'T-n' or a sibling '#k': the k-th task of this list, "
|
|
107
|
+
"counting from 1."
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class Commit(DomainModel):
|
|
113
|
+
"""One commit an agent made in the project's repository during its turn."""
|
|
114
|
+
|
|
115
|
+
sha: str
|
|
116
|
+
subject: str
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class TestRun(DomainModel):
|
|
120
|
+
"""One test (or test suite) the agent ran."""
|
|
121
|
+
|
|
122
|
+
name: str
|
|
123
|
+
result: Literal["passed", "failed", "skipped"]
|
|
124
|
+
details: str = ""
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class Issue(DomainModel):
|
|
128
|
+
"""One issue a verdict raises against an implementation."""
|
|
129
|
+
|
|
130
|
+
severity: Literal["blocker", "major", "minor"]
|
|
131
|
+
description: str
|
|
132
|
+
location: str = ""
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class Option(DomainModel):
|
|
136
|
+
"""One option offered to the Operator in an escalation."""
|
|
137
|
+
|
|
138
|
+
label: str
|
|
139
|
+
consequences: str
|
|
140
|
+
recommended: bool = False
|
|
141
|
+
needs_input: bool = Field(
|
|
142
|
+
default=False,
|
|
143
|
+
description="True when choosing this option means the Operator must supply something.",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class EscalationRequest(DomainModel):
|
|
148
|
+
"""The Leader's escalation packet content: a question and its options."""
|
|
149
|
+
|
|
150
|
+
question: str
|
|
151
|
+
options: list[Option] = Field(description="Exactly one option must have recommended=true.")
|
|
152
|
+
|
|
153
|
+
@model_validator(mode="after")
|
|
154
|
+
def _exactly_one_recommended_option(self) -> Self:
|
|
155
|
+
recommended = [option for option in self.options if option.recommended]
|
|
156
|
+
if len(recommended) != 1:
|
|
157
|
+
message = f"options: exactly one option must be recommended, found {len(recommended)}"
|
|
158
|
+
raise ValueError(message)
|
|
159
|
+
return self
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class TriageReport(DomainModel):
|
|
163
|
+
"""The Leader's `triage` report."""
|
|
164
|
+
|
|
165
|
+
title: str = Field(max_length=TASK_TITLE_MAX_LENGTH)
|
|
166
|
+
mode: Mode
|
|
167
|
+
complexity: ComplexityName
|
|
168
|
+
acceptance_criteria: list[str]
|
|
169
|
+
references: list[str]
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class ImplementationReport(DomainModel):
|
|
173
|
+
"""The Implementer's `implement`/`rework`/`conflicts` report."""
|
|
174
|
+
|
|
175
|
+
status: Literal["completed", "needs_decision", "blocked"]
|
|
176
|
+
summary: str
|
|
177
|
+
commits: list[Commit]
|
|
178
|
+
tests: list[TestRun]
|
|
179
|
+
trackers_updated: bool
|
|
180
|
+
risks: list[str] = Field(default_factory=list)
|
|
181
|
+
question: str = Field(
|
|
182
|
+
default="", description="Required unless status is 'completed'; otherwise empty."
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
@model_validator(mode="after")
|
|
186
|
+
def _question_required_unless_completed(self) -> Self:
|
|
187
|
+
if self.status != STATUS_COMPLETED and not self.question:
|
|
188
|
+
message = "question: required unless status is 'completed'"
|
|
189
|
+
raise ValueError(message)
|
|
190
|
+
return self
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class PlanReport(DomainModel):
|
|
194
|
+
"""The Leader's `plan` report."""
|
|
195
|
+
|
|
196
|
+
summary: str
|
|
197
|
+
commit_subject: str = Field(
|
|
198
|
+
description=(
|
|
199
|
+
"A concise subject for the whole change following the target project's documented "
|
|
200
|
+
"commit convention, or Conventional Commits when none is documented; no task reference."
|
|
201
|
+
)
|
|
202
|
+
)
|
|
203
|
+
commits: list[Commit]
|
|
204
|
+
new_tasks: list[NewTask] = Field(default_factory=list)
|
|
205
|
+
scope_changes: list[str] = Field(default_factory=list)
|
|
206
|
+
|
|
207
|
+
@model_validator(mode="after")
|
|
208
|
+
def _commit_subject_is_valid(self) -> Self:
|
|
209
|
+
_require_commit_subject(self.commit_subject)
|
|
210
|
+
return self
|
|
211
|
+
|
|
212
|
+
@model_validator(mode="after")
|
|
213
|
+
def _sibling_references_in_range(self) -> Self:
|
|
214
|
+
_require_sibling_references_in_range(self.new_tasks, "new_tasks")
|
|
215
|
+
return self
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class ConsultAnswer(DomainModel):
|
|
219
|
+
"""The Leader's answer to a `consult` turn."""
|
|
220
|
+
|
|
221
|
+
decision: Literal["answer", "escalate"]
|
|
222
|
+
answer: str = Field(
|
|
223
|
+
default="", description="Required and non-empty iff decision is 'answer'; otherwise empty."
|
|
224
|
+
)
|
|
225
|
+
escalation: EscalationRequest | None = Field(
|
|
226
|
+
default=None, description="Required iff decision is 'escalate'; otherwise unset."
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
@model_validator(mode="after")
|
|
230
|
+
def _answer_required_iff_decision_is_answer(self) -> Self:
|
|
231
|
+
_require_iff(
|
|
232
|
+
condition=self.decision == DECISION_ANSWER,
|
|
233
|
+
present=bool(self.answer),
|
|
234
|
+
field="answer",
|
|
235
|
+
rule="decision is 'answer'",
|
|
236
|
+
)
|
|
237
|
+
return self
|
|
238
|
+
|
|
239
|
+
@model_validator(mode="after")
|
|
240
|
+
def _escalation_required_iff_decision_is_escalate(self) -> Self:
|
|
241
|
+
_require_iff(
|
|
242
|
+
condition=self.decision == DECISION_ESCALATE,
|
|
243
|
+
present=self.escalation is not None,
|
|
244
|
+
field="escalation",
|
|
245
|
+
rule="decision is 'escalate'",
|
|
246
|
+
)
|
|
247
|
+
return self
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class Verdict(DomainModel):
|
|
251
|
+
"""The Leader's `verify` report (post-rebase `conflicts` verify)."""
|
|
252
|
+
|
|
253
|
+
decision: Literal["accept", "changes_requested", "escalate", "supersede"]
|
|
254
|
+
summary: str
|
|
255
|
+
commit_subject: str = Field(
|
|
256
|
+
default="",
|
|
257
|
+
description=(
|
|
258
|
+
"Required and a concise subject following the target project's documented commit "
|
|
259
|
+
"convention, or Conventional Commits when none is documented, iff decision is 'accept' "
|
|
260
|
+
"(of the whole change; no task reference); otherwise empty."
|
|
261
|
+
),
|
|
262
|
+
)
|
|
263
|
+
issues: list[Issue] = Field(
|
|
264
|
+
default_factory=list,
|
|
265
|
+
description="Required and non-empty iff decision is 'changes_requested'; otherwise empty.",
|
|
266
|
+
)
|
|
267
|
+
residual_risks: list[str] = Field(default_factory=list)
|
|
268
|
+
new_tasks: list[NewTask] = Field(
|
|
269
|
+
default_factory=list,
|
|
270
|
+
description="Required and non-empty iff decision is 'supersede'; otherwise empty.",
|
|
271
|
+
)
|
|
272
|
+
scope_changes: list[str] = Field(default_factory=list)
|
|
273
|
+
escalation: EscalationRequest | None = Field(
|
|
274
|
+
default=None, description="Required iff decision is 'escalate'; otherwise unset."
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
@model_validator(mode="after")
|
|
278
|
+
def _commit_subject_required_iff_accept(self) -> Self:
|
|
279
|
+
_require_iff(
|
|
280
|
+
condition=self.decision == DECISION_ACCEPT,
|
|
281
|
+
present=bool(self.commit_subject),
|
|
282
|
+
field="commit_subject",
|
|
283
|
+
rule="decision is 'accept'",
|
|
284
|
+
)
|
|
285
|
+
if self.commit_subject:
|
|
286
|
+
_require_commit_subject(self.commit_subject)
|
|
287
|
+
return self
|
|
288
|
+
|
|
289
|
+
@model_validator(mode="after")
|
|
290
|
+
def _issues_non_empty_iff_changes_requested(self) -> Self:
|
|
291
|
+
_require_iff(
|
|
292
|
+
condition=self.decision == DECISION_CHANGES_REQUESTED,
|
|
293
|
+
present=bool(self.issues),
|
|
294
|
+
field="issues",
|
|
295
|
+
rule="decision is 'changes_requested'",
|
|
296
|
+
)
|
|
297
|
+
return self
|
|
298
|
+
|
|
299
|
+
@model_validator(mode="after")
|
|
300
|
+
def _new_tasks_non_empty_iff_supersede(self) -> Self:
|
|
301
|
+
_require_iff(
|
|
302
|
+
condition=self.decision == DECISION_SUPERSEDE,
|
|
303
|
+
present=bool(self.new_tasks),
|
|
304
|
+
field="new_tasks",
|
|
305
|
+
rule="decision is 'supersede'",
|
|
306
|
+
)
|
|
307
|
+
_require_sibling_references_in_range(self.new_tasks, "new_tasks")
|
|
308
|
+
return self
|
|
309
|
+
|
|
310
|
+
@model_validator(mode="after")
|
|
311
|
+
def _escalation_required_iff_decision_is_escalate(self) -> Self:
|
|
312
|
+
_require_iff(
|
|
313
|
+
condition=self.decision == DECISION_ESCALATE,
|
|
314
|
+
present=self.escalation is not None,
|
|
315
|
+
field="escalation",
|
|
316
|
+
rule="decision is 'escalate'",
|
|
317
|
+
)
|
|
318
|
+
return self
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class FeedbackAction(StrEnum):
|
|
322
|
+
"""What the Leader decided to do about one source item of PR review feedback."""
|
|
323
|
+
|
|
324
|
+
ADDRESS = auto()
|
|
325
|
+
ALREADY_ADDRESSED = auto()
|
|
326
|
+
DISAGREE = auto()
|
|
327
|
+
NEEDS_OPERATOR = auto()
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
class FeedbackWork(DomainModel):
|
|
331
|
+
"""One implementation instruction, possibly shared by several duplicate source items."""
|
|
332
|
+
|
|
333
|
+
id: _Text
|
|
334
|
+
instruction: _Text
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
class FeedbackDecision(DomainModel):
|
|
338
|
+
"""The Leader's decision for one review body or original inline discussion."""
|
|
339
|
+
|
|
340
|
+
source_id: _Text
|
|
341
|
+
action: FeedbackAction
|
|
342
|
+
points: list[_Text] = Field(
|
|
343
|
+
min_length=1,
|
|
344
|
+
description="Every substantive point the source item raises, one entry each.",
|
|
345
|
+
)
|
|
346
|
+
detail: _Text = Field(
|
|
347
|
+
description=(
|
|
348
|
+
"address: the concrete work requested; already_addressed: the commit or file that "
|
|
349
|
+
"shows it; disagree: the specific reason; needs_operator: the question to ask."
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
work_id: str = Field(
|
|
353
|
+
default="", description="Required iff action is 'address': the id of a `work` entry."
|
|
354
|
+
)
|
|
355
|
+
reply: str = Field(
|
|
356
|
+
default="",
|
|
357
|
+
description="Proposed reply to this source item; required unless action is "
|
|
358
|
+
"'needs_operator', otherwise empty.",
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
@model_validator(mode="after")
|
|
362
|
+
def _work_id_required_iff_address(self) -> Self:
|
|
363
|
+
_require_iff(
|
|
364
|
+
condition=self.action is FeedbackAction.ADDRESS,
|
|
365
|
+
present=bool(self.work_id),
|
|
366
|
+
field="work_id",
|
|
367
|
+
rule="action is 'address'",
|
|
368
|
+
)
|
|
369
|
+
return self
|
|
370
|
+
|
|
371
|
+
@model_validator(mode="after")
|
|
372
|
+
def _reply_required_unless_needs_operator(self) -> Self:
|
|
373
|
+
_require_iff(
|
|
374
|
+
condition=self.action is not FeedbackAction.NEEDS_OPERATOR,
|
|
375
|
+
present=bool(self.reply.strip()),
|
|
376
|
+
field="reply",
|
|
377
|
+
rule="action is not 'needs_operator'",
|
|
378
|
+
)
|
|
379
|
+
return self
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
class FeedbackAnalysis(DomainModel):
|
|
383
|
+
"""The Leader's `feedback_analysis` report: one decision per source item of a snapshot."""
|
|
384
|
+
|
|
385
|
+
summary: _Text
|
|
386
|
+
work: list[FeedbackWork] = Field(
|
|
387
|
+
default_factory=list,
|
|
388
|
+
description="Implementation instructions; one may serve several duplicate source items.",
|
|
389
|
+
)
|
|
390
|
+
decisions: list[FeedbackDecision] = Field(min_length=1)
|
|
391
|
+
|
|
392
|
+
@model_validator(mode="after")
|
|
393
|
+
def _identities_are_unique(self) -> Self:
|
|
394
|
+
_require_unique([decision.source_id for decision in self.decisions], "decisions")
|
|
395
|
+
_require_unique([work.id for work in self.work], "work")
|
|
396
|
+
return self
|
|
397
|
+
|
|
398
|
+
@model_validator(mode="after")
|
|
399
|
+
def _work_matches_address_decisions(self) -> Self:
|
|
400
|
+
work_ids = {work.id for work in self.work}
|
|
401
|
+
used = {
|
|
402
|
+
decision.work_id
|
|
403
|
+
for decision in self.decisions
|
|
404
|
+
if decision.action is FeedbackAction.ADDRESS
|
|
405
|
+
}
|
|
406
|
+
unknown = used - work_ids
|
|
407
|
+
if unknown:
|
|
408
|
+
message = f"decisions: work_id {sorted(unknown)} is not an id in `work`"
|
|
409
|
+
raise ValueError(message)
|
|
410
|
+
unused = work_ids - used
|
|
411
|
+
if unused:
|
|
412
|
+
message = f"work: {sorted(unused)} is not the work_id of any 'address' decision"
|
|
413
|
+
raise ValueError(message)
|
|
414
|
+
return self
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
class FeedbackRework(ImplementationReport):
|
|
418
|
+
"""The Implementer's report on the accepted work of a feedback round."""
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
class VerifiedReply(DomainModel):
|
|
422
|
+
"""The reply the Leader verified for one source item."""
|
|
423
|
+
|
|
424
|
+
source_id: _Text
|
|
425
|
+
reply: _Text
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
class FeedbackVerdict(Verdict):
|
|
429
|
+
"""The Leader's verification of a feedback round's changes and replies."""
|
|
430
|
+
|
|
431
|
+
replies: list[VerifiedReply] = Field(
|
|
432
|
+
default_factory=list,
|
|
433
|
+
description=(
|
|
434
|
+
"Required and non-empty iff decision is 'accept': the final reply for every source "
|
|
435
|
+
"item whose decision has a reply; otherwise empty."
|
|
436
|
+
),
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
@model_validator(mode="after")
|
|
440
|
+
def _decision_is_available(self) -> Self:
|
|
441
|
+
if self.decision == DECISION_SUPERSEDE:
|
|
442
|
+
message = "decision: 'supersede' is not available for feedback"
|
|
443
|
+
raise ValueError(message)
|
|
444
|
+
return self
|
|
445
|
+
|
|
446
|
+
@model_validator(mode="after")
|
|
447
|
+
def _replies_required_iff_accept(self) -> Self:
|
|
448
|
+
_require_iff(
|
|
449
|
+
condition=self.decision == DECISION_ACCEPT,
|
|
450
|
+
present=bool(self.replies),
|
|
451
|
+
field="replies",
|
|
452
|
+
rule="decision is 'accept'",
|
|
453
|
+
)
|
|
454
|
+
_require_unique([reply.source_id for reply in self.replies], "replies")
|
|
455
|
+
return self
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
class ReportKind(StrEnum):
|
|
459
|
+
"""The `report.kind` discriminator naming a turn's report model."""
|
|
460
|
+
|
|
461
|
+
TRIAGE = auto()
|
|
462
|
+
IMPLEMENTATION = auto()
|
|
463
|
+
PLAN = auto()
|
|
464
|
+
CONSULT = auto()
|
|
465
|
+
VERDICT = auto()
|
|
466
|
+
FEEDBACK_ANALYSIS = auto()
|
|
467
|
+
FEEDBACK_REWORK = auto()
|
|
468
|
+
FEEDBACK_VERDICT = auto()
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
FEEDBACK_REPORT_KINDS: Final = frozenset(
|
|
472
|
+
{ReportKind.FEEDBACK_ANALYSIS, ReportKind.FEEDBACK_REWORK, ReportKind.FEEDBACK_VERDICT}
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
Report = (
|
|
476
|
+
TriageReport | ImplementationReport | PlanReport | ConsultAnswer | Verdict | FeedbackAnalysis
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
_MODELS_BY_KIND: dict[ReportKind, type[Report]] = {
|
|
480
|
+
ReportKind.TRIAGE: TriageReport,
|
|
481
|
+
ReportKind.IMPLEMENTATION: ImplementationReport,
|
|
482
|
+
ReportKind.PLAN: PlanReport,
|
|
483
|
+
ReportKind.CONSULT: ConsultAnswer,
|
|
484
|
+
ReportKind.VERDICT: Verdict,
|
|
485
|
+
ReportKind.FEEDBACK_ANALYSIS: FeedbackAnalysis,
|
|
486
|
+
ReportKind.FEEDBACK_REWORK: FeedbackRework,
|
|
487
|
+
ReportKind.FEEDBACK_VERDICT: FeedbackVerdict,
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def model_for(kind: ReportKind) -> type[Report]:
|
|
492
|
+
"""Return the pydantic model that validates reports of `kind`."""
|
|
493
|
+
return _MODELS_BY_KIND[kind]
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def schema_for(kind: ReportKind) -> dict[str, Any]:
|
|
497
|
+
"""Return the JSON Schema for `kind`'s report model, written as `report.schema.json`."""
|
|
498
|
+
return model_for(kind).model_json_schema()
|