Coverage for src/keel/workblock.py: 100%
31 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-02 20:26 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-02 20:26 +0000
1"""Shared work-block contract for daytime and overnight queue runs."""
3from __future__ import annotations
5from collections.abc import Sequence
6from typing import Any
8from . import checkpoint, ledger
9from . import config as cfg
11WORK_BLOCK_SCHEMA_VERSION = "keel.work-block.v1"
12OUTCOME_BUCKETS = (
13 "shipped",
14 "pr_open_not_merged",
15 "deferred",
16 "blocked",
17 "skipped",
18 "needs_input",
19)
20#: The staffing flags a work block / overnight session accepts and hands down.
21#: Before #1017 a batch passed only ``operator_consent.delegated_agent_scope``, so a run
22#: launched with ``--delegate codex --effort high`` produced children that re-resolved
23#: from config and ran the default team — the operator's choice reached the parent and
24#: died there.
25DELEGATION_FLAGS = ("--delegate", "--review-delegate", "--effort", "--team", "--reviewers")
27#: The child handoff every queued issue is dispatched through. Written out in full so the
28#: adapter prose and the published contract cannot describe different handoffs.
29CHILD_HANDOFF_TEMPLATE = (
30 "/keel:ship <issue> [--delegate <provider[:model]>] [--review-delegate <provider>] "
31 "[--effort <low|medium|high>] [--team <profile>] [--reviewers <n>]"
32)
34STOP_CONDITIONS = (
35 "queue-exhausted",
36 "max-items-reached",
37 "time-budget-exhausted",
38 "operator-pause",
39 "consent-gap",
40 "needs-input",
41 "blocked-finding",
42 "merge-window-close",
43 "three-consecutive-unresolved-ci-failures",
44 "user-cancelled",
45)
48def child_ship_args(
49 *,
50 delegate: str | None = None,
51 review_delegates: Sequence[str] = (),
52 effort: str | None = None,
53 team_profile: str | None = None,
54 reviewer_override: int | None = None,
55) -> tuple[str, ...]:
56 """The staffing flags to append to every child ship handoff — set values only.
58 Deterministic and ordered, because the child handoff line is quoted verbatim into the
59 session report: a set that reordered itself between two issues would read as two
60 different teams having run.
61 """
62 args: list[str] = []
63 if delegate:
64 args += ["--delegate", delegate]
65 for reviewer in review_delegates:
66 if reviewer:
67 args += ["--review-delegate", reviewer]
68 if effort:
69 args += ["--effort", effort]
70 if team_profile:
71 args += ["--team", team_profile]
72 if reviewer_override is not None:
73 args += ["--reviewers", str(reviewer_override)]
74 return tuple(args)
77def delegation_as_dict(
78 *,
79 delegate: str | None = None,
80 review_delegates: Sequence[str] = (),
81 effort: str | None = None,
82 team_profile: str | None = None,
83 reviewer_override: int | None = None,
84) -> dict[str, Any]:
85 """What this block hands to each child ship, and what it must report afterwards."""
86 return {
87 "flags": list(DELEGATION_FLAGS),
88 "child_handoff_template": CHILD_HANDOFF_TEMPLATE,
89 "propagate_to_every_child_ship": True,
90 "record_effective_values_in_session_report": True,
91 "effective": {
92 "delegate": delegate,
93 "review_delegates": [reviewer for reviewer in review_delegates if reviewer],
94 "effort": effort,
95 "team": team_profile,
96 "reviewers": reviewer_override,
97 },
98 "child_args": list(
99 child_ship_args(
100 delegate=delegate,
101 review_delegates=review_delegates,
102 effort=effort,
103 team_profile=team_profile,
104 reviewer_override=reviewer_override,
105 )
106 ),
107 }
110def contract_as_dict(
111 *,
112 config: cfg.ProjectConfig,
113 mode: str,
114 transport: dict[str, Any] | None = None,
115 delegation: dict[str, Any] | None = None,
116) -> dict[str, Any]:
117 """Return the consumer-neutral queue/work-block primitive contract."""
118 if mode not in {"daytime", "overnight"}:
119 raise ValueError("work-block mode must be daytime or overnight")
120 github = transport or {}
121 return {
122 "schema_version": WORK_BLOCK_SCHEMA_VERSION,
123 "mode": mode,
124 "base_branch": config.base_branch,
125 "timezone": config.timezone,
126 "merge_window": config.merge_window,
127 "merge_window_mode": config.merge_window_mode,
128 "github_transport": github,
129 "queue": {
130 "accepted_inputs": ["explicit_issue_numbers", "queue_selector"],
131 "explicit_issue_order": "as-provided",
132 "selector_order": [
133 "priority",
134 "issue_number",
135 ],
136 "snapshot_once_per_session": True,
137 "refresh_readiness_between_issues": True,
138 },
139 "per_issue": {
140 "handoff_command": "ship",
141 "isolated_branch_worktree": True,
142 "child_inherits_operator_consent_scope": True,
143 "child_inherits_team_assignment": True,
144 "child_honors_capture_contract": True,
145 "child_appends_run_ledger": True,
146 "child_uses_merge_lock": True,
147 "child_rechecks_merge_window": True,
148 },
149 "failure_policy": {
150 "branch_contamination_policy": "one issue cannot reuse another issue worktree",
151 "non_ready_policy": "skip-or-stop-by-mode-policy",
152 "continue_after_blocked": mode == "overnight",
153 "daytime_operator_can_redirect_between_items": mode == "daytime",
154 },
155 "delegation": delegation if delegation is not None else delegation_as_dict(),
156 "checkpoint": checkpoint.checkpoint_contract_as_dict(config),
157 "run_ledger": ledger.ledger_contract_as_dict(config),
158 "final_report": {
159 "required": True,
160 "outcome_buckets": list(OUTCOME_BUCKETS),
161 "source": "child ship results + run ledger + checkpoint",
162 },
163 "stop_conditions": list(STOP_CONDITIONS),
164 "shared_with": ["overnight"] if mode == "daytime" else ["work-block"],
165 }