dos-kernel 0.22.0__py3-none-win_amd64.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.
- dos/__init__.py +261 -0
- dos/_bin/dos-hook.exe +0 -0
- dos/_filelock.py +255 -0
- dos/_job_policy.py +97 -0
- dos/_tree.py +145 -0
- dos/admission.py +433 -0
- dos/answer_shape.py +299 -0
- dos/arbiter.py +859 -0
- dos/archive_lock.py +266 -0
- dos/arg_provenance.py +814 -0
- dos/attest.py +472 -0
- dos/breaker.py +311 -0
- dos/churn.py +226 -0
- dos/claim_extract.py +229 -0
- dos/claim_ttl.py +150 -0
- dos/cli.py +8721 -0
- dos/commit_audit.py +666 -0
- dos/completion.py +466 -0
- dos/concurrency_class.py +154 -0
- dos/config.py +1380 -0
- dos/config_lint.py +464 -0
- dos/cooldown.py +390 -0
- dos/coverage.py +387 -0
- dos/dangling_intent.py +287 -0
- dos/data_class.py +397 -0
- dos/decisions.py +1274 -0
- dos/decisions_tui.py +251 -0
- dos/dispatch_top.py +740 -0
- dos/dispatch_top_tui.py +116 -0
- dos/drivers/__init__.py +40 -0
- dos/drivers/ci_status.py +630 -0
- dos/drivers/citation_resolve.py +703 -0
- dos/drivers/decision_stop.py +98 -0
- dos/drivers/export_file.py +173 -0
- dos/drivers/export_otlp.py +275 -0
- dos/drivers/export_statsd.py +242 -0
- dos/drivers/hook_dialects.py +391 -0
- dos/drivers/job.py +47 -0
- dos/drivers/llm_judge.py +360 -0
- dos/drivers/memory_recall.py +1231 -0
- dos/drivers/notify_slack.py +373 -0
- dos/drivers/notify_webhook.py +251 -0
- dos/drivers/operator_judge.py +114 -0
- dos/drivers/os_acceptance.py +228 -0
- dos/drivers/paste_log.py +132 -0
- dos/drivers/plan_scope.py +133 -0
- dos/drivers/self_improve.py +375 -0
- dos/drivers/similarity_judge.py +249 -0
- dos/drivers/state_diff.py +274 -0
- dos/drivers/supervisor.py +347 -0
- dos/drivers/watchdog.py +363 -0
- dos/drivers/workshop.py +160 -0
- dos/durable_schema.py +344 -0
- dos/effect_witness.py +393 -0
- dos/efficiency.py +318 -0
- dos/enforce.py +414 -0
- dos/enumerate.py +776 -0
- dos/env_print.py +378 -0
- dos/event_severity.py +258 -0
- dos/evidence.py +692 -0
- dos/exec_capability.py +256 -0
- dos/export_cursor.py +143 -0
- dos/exporter.py +320 -0
- dos/firing_label.py +353 -0
- dos/fleet_roll.py +226 -0
- dos/gate_classify.py +827 -0
- dos/gh4_coverage.py +179 -0
- dos/git_delta.py +122 -0
- dos/guard.py +215 -0
- dos/health.py +552 -0
- dos/help_summary.py +519 -0
- dos/home.py +934 -0
- dos/hook_binary.py +194 -0
- dos/hook_dialect.py +271 -0
- dos/hook_exit.py +191 -0
- dos/hook_install.py +437 -0
- dos/id_alloc.py +304 -0
- dos/improve.py +499 -0
- dos/intent_ledger.py +635 -0
- dos/interpret.py +176 -0
- dos/intervention.py +769 -0
- dos/intervention_eval.py +371 -0
- dos/journal_delta.py +308 -0
- dos/judge_eval.py +328 -0
- dos/judges.py +366 -0
- dos/lane_infer.py +127 -0
- dos/lane_journal.py +1001 -0
- dos/lane_lease.py +952 -0
- dos/lane_overlap.py +228 -0
- dos/lease_health.py +282 -0
- dos/lifecycle.py +211 -0
- dos/liveness.py +352 -0
- dos/lock_modes.py +185 -0
- dos/log_source.py +395 -0
- dos/loop_decide.py +1746 -0
- dos/marker_gate.py +254 -0
- dos/marker_sensor.py +396 -0
- dos/noop_streak.py +280 -0
- dos/notify.py +479 -0
- dos/observe.py +175 -0
- dos/oracle.py +1661 -0
- dos/overlap_eval.py +214 -0
- dos/overlap_policy.py +342 -0
- dos/packet_sidecar.py +267 -0
- dos/phase_shipped.py +1985 -0
- dos/pick_priority.py +225 -0
- dos/pickable.py +369 -0
- dos/picker_oracle.py +1037 -0
- dos/plan_board.py +513 -0
- dos/plan_board_tui.py +113 -0
- dos/plan_source.py +455 -0
- dos/posttool_sensor.py +528 -0
- dos/precursor_gate.py +499 -0
- dos/precursor_gate_eval.py +239 -0
- dos/preflight.py +825 -0
- dos/pretool_sensor.py +490 -0
- dos/proc_delta.py +181 -0
- dos/productivity.py +296 -0
- dos/provider_limit.py +242 -0
- dos/py.typed +4 -0
- dos/reason_morphology.py +299 -0
- dos/reasons.py +449 -0
- dos/reconcile.py +173 -0
- dos/recurring_wedge.py +206 -0
- dos/render.py +393 -0
- dos/result_state.py +468 -0
- dos/resume.py +578 -0
- dos/resume_evidence.py +293 -0
- dos/retention.py +344 -0
- dos/reward.py +372 -0
- dos/rewind.py +587 -0
- dos/rewind_evidence.py +168 -0
- dos/rewind_tokens.py +252 -0
- dos/run_id.py +342 -0
- dos/scope.py +520 -0
- dos/scope_source.py +382 -0
- dos/scout.py +982 -0
- dos/self_modify.py +209 -0
- dos/sibling_scan.py +569 -0
- dos/skills/EXAMPLES.md +584 -0
- dos/skills/dos-class-cycle/SKILL.md +107 -0
- dos/skills/dos-dispatch/SKILL.md +177 -0
- dos/skills/dos-dispatch-loop/SKILL.md +254 -0
- dos/skills/dos-goal-gate/SKILL.md +269 -0
- dos/skills/dos-next-up/SKILL.md +231 -0
- dos/skills/dos-promote/SKILL.md +114 -0
- dos/skills/dos-replan/SKILL.md +159 -0
- dos/skills/dos-replan-loop/SKILL.md +114 -0
- dos/skills/dos-self-improve/SKILL.md +213 -0
- dos/skills/dos-supervise-loop/SKILL.md +180 -0
- dos/skills/dos-unstick/SKILL.md +108 -0
- dos/skills/dos-witness-claim/SKILL.md +251 -0
- dos/stamp.py +1002 -0
- dos/state_health.py +387 -0
- dos/status.py +114 -0
- dos/stop_policy.py +334 -0
- dos/supervise.py +1014 -0
- dos/testwitness.py +392 -0
- dos/timeline.py +1027 -0
- dos/tokens.py +485 -0
- dos/tool_stream.py +393 -0
- dos/tool_stream_eval.py +226 -0
- dos/trace.py +524 -0
- dos/verdict.py +140 -0
- dos/verdict_cli.py +189 -0
- dos/verdict_journal.py +497 -0
- dos/verdict_rollup.py +217 -0
- dos/verdicts.py +181 -0
- dos/wedge_reason.py +282 -0
- dos_kernel-0.22.0.dist-info/METADATA +859 -0
- dos_kernel-0.22.0.dist-info/RECORD +178 -0
- dos_kernel-0.22.0.dist-info/WHEEL +5 -0
- dos_kernel-0.22.0.dist-info/entry_points.txt +39 -0
- dos_kernel-0.22.0.dist-info/licenses/LICENSE +21 -0
- dos_kernel-0.22.0.dist-info/top_level.txt +2 -0
- dos_mcp/__init__.py +52 -0
- dos_mcp/py.typed +2 -0
- dos_mcp/server.py +779 -0
dos/packet_sidecar.py
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""The `.prompts.json` packet sidecar — the WRITE half of the contract.
|
|
2
|
+
|
|
3
|
+
FQ-419/FQ-420 root cure. `dos.preflight` already owns the *read* half of the
|
|
4
|
+
prompt sidecar — `load_packet_sidecar` (PRESENT/ABSENT/CORRUPT), the
|
|
5
|
+
`_sidecar_dropped_refusal` gate, and the `build_context` wiring that refuses a
|
|
6
|
+
packet whose `.prompts.json` is missing. But nothing owned the *write* half:
|
|
7
|
+
the reference userland renderer (`scripts/next_up_render.py:cmd_render`) emitted
|
|
8
|
+
the human packet `.md` and printed `Saved:` / exit-0 **without ever writing the
|
|
9
|
+
machine-readable `.prompts.json` the orchestrator actually launches workers
|
|
10
|
+
from.** Producer and consumer were out of contract — the schema token
|
|
11
|
+
`next-up-prompts-v1` was a *default* in the reader and **defined nowhere**, so
|
|
12
|
+
the renderer simply didn't emit it.
|
|
13
|
+
|
|
14
|
+
The cost was structural and recurring: every clean-validating packet shipped
|
|
15
|
+
without its prompt bodies, and the failure surfaced only one rung downstream as
|
|
16
|
+
a `/fanout` `body_empty_picks` refuse — naming the symptom, never the cause. By
|
|
17
|
+
2026-06-01 it had wedged 6+ consecutive `/dispatch` runs across the apply,
|
|
18
|
+
tailor, and CD lanes (7d live ship-rate 0.0%).
|
|
19
|
+
|
|
20
|
+
This module closes the loop, the same way `dos.wedge_reason` closed the
|
|
21
|
+
no-pick-reason drift: **one place declares the schema, and both ends import
|
|
22
|
+
it.** It carries two kernel responsibilities:
|
|
23
|
+
|
|
24
|
+
1. ``write_packet_sidecar(packet_path, picks)`` — the canonical serializer.
|
|
25
|
+
The renderer calls this; `dos.preflight.load_packet_sidecar` reads exactly
|
|
26
|
+
what it writes. The schema token (`SIDECAR_SCHEMA`) lives here and the
|
|
27
|
+
reader imports it, so the two can never drift to different strings.
|
|
28
|
+
|
|
29
|
+
2. ``assert_packet_shippable(packet_path, rendered_pick_count)`` — the
|
|
30
|
+
PRODUCER-side verify. This is the DOS thesis applied at the source
|
|
31
|
+
(dispatch-os-vision §0, *the kernel is the part that doesn't believe the
|
|
32
|
+
agents*): the renderer is an unreliable, self-narrating worker; its exit-0
|
|
33
|
+
`Saved:` is a self-report. The kernel does not believe it — it re-opens the
|
|
34
|
+
artifact it was told exists and refuses if the prompt bodies are absent,
|
|
35
|
+
corrupt, or empty. Catching the drop here (one rung *above* `/fanout`)
|
|
36
|
+
turns a downstream `body_empty_picks` mystery into a loud, typed
|
|
37
|
+
`RENDERER_SIDECAR_DROPPED` refusal that points straight at the renderer.
|
|
38
|
+
|
|
39
|
+
Pure stdlib (mirrors `preflight` / `wedge_reason` leaf-import character) so the
|
|
40
|
+
renderer can import it without dragging heavy deps. The `WedgeReason` import is
|
|
41
|
+
the only intra-package dependency, and it is itself a leaf module.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import json
|
|
47
|
+
from dataclasses import dataclass, field
|
|
48
|
+
from pathlib import Path
|
|
49
|
+
from typing import Any
|
|
50
|
+
|
|
51
|
+
# Single-source the prompt-sidecar schema token. `dos.preflight.load_packet_sidecar`
|
|
52
|
+
# imports THIS constant for its `d.get("schema", SIDECAR_SCHEMA)` default, so the
|
|
53
|
+
# writer and reader can never disagree on the string. (Before this module the
|
|
54
|
+
# token was a bare literal default in the reader and was emitted by no writer at
|
|
55
|
+
# all — the exact producer/consumer drift `dos.wedge_reason` exists to end.)
|
|
56
|
+
SIDECAR_SCHEMA = "next-up-prompts-v1"
|
|
57
|
+
|
|
58
|
+
# The fields a sidecar pick carries — the contract `dos.preflight` consumes
|
|
59
|
+
# downstream (`load_packet_sidecar` → `merge_picks_with_verdicts`). `prompt_text`
|
|
60
|
+
# is the load-bearing one (the worker's actual prompt body); the rest are
|
|
61
|
+
# routing/observability metadata. A writer that omits `prompt_text`, or writes it
|
|
62
|
+
# empty, is exactly the drop this module refuses.
|
|
63
|
+
_PICK_FIELDS = (
|
|
64
|
+
"n",
|
|
65
|
+
"plan_id",
|
|
66
|
+
"phase_id",
|
|
67
|
+
"phase_title",
|
|
68
|
+
"phase_chain",
|
|
69
|
+
"doc_path",
|
|
70
|
+
"subagent_type",
|
|
71
|
+
"mode",
|
|
72
|
+
"pick_kind",
|
|
73
|
+
"files",
|
|
74
|
+
"gates_on",
|
|
75
|
+
"reserve_paths",
|
|
76
|
+
"prompt_text",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def sidecar_path_for(packet_path: Path) -> Path:
|
|
81
|
+
"""The `.prompts.json` path beside a packet `.md` — the one naming rule.
|
|
82
|
+
|
|
83
|
+
`next-up-2026-06-01-13.md` → `next-up-2026-06-01-13.prompts.json`. Kept here
|
|
84
|
+
(not inlined at the call sites) so the writer and `dos.preflight`'s reader
|
|
85
|
+
derive the sibling path through the *same* function — a future rename touches
|
|
86
|
+
one line. Mirrors `load_packet_sidecar`'s
|
|
87
|
+
`packet_path.with_name(packet_path.stem + ".prompts.json")`.
|
|
88
|
+
"""
|
|
89
|
+
return packet_path.with_name(packet_path.stem + ".prompts.json")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _coerce_pick(raw: dict, *, index: int) -> dict:
|
|
93
|
+
"""Project a renderer pick onto the sidecar pick contract (`_PICK_FIELDS`).
|
|
94
|
+
|
|
95
|
+
Only the contract fields are carried (a renderer pick holds far more
|
|
96
|
+
internal state — `anchors`, `audit`, `one_hop_metric`, … — that the worker
|
|
97
|
+
launch does not need). `n` defaults to the 1-based position so a pick that
|
|
98
|
+
omitted it still numbers correctly. `prompt_text` is taken verbatim from the
|
|
99
|
+
caller — this module does NOT render it (that is the host renderer's job via
|
|
100
|
+
its own template); the kernel only serializes and verifies what it is given.
|
|
101
|
+
"""
|
|
102
|
+
out: dict[str, Any] = {}
|
|
103
|
+
for f in _PICK_FIELDS:
|
|
104
|
+
if f in raw:
|
|
105
|
+
out[f] = raw[f]
|
|
106
|
+
out.setdefault("n", index)
|
|
107
|
+
# Normalize the load-bearing body to a string (never None) so the reader's
|
|
108
|
+
# `len(p.get("prompt_text") or "")` and our own empty-check agree.
|
|
109
|
+
out["prompt_text"] = str(out.get("prompt_text") or "")
|
|
110
|
+
return out
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def build_sidecar_payload(picks: list[dict]) -> dict:
|
|
114
|
+
"""The full sidecar document: `{schema, picks}` — what gets written to disk.
|
|
115
|
+
|
|
116
|
+
Separated from `write_packet_sidecar` so a caller (and a test) can build the
|
|
117
|
+
exact payload without touching the filesystem. Each pick is projected onto
|
|
118
|
+
`_PICK_FIELDS`; `prompt_text` is carried verbatim.
|
|
119
|
+
"""
|
|
120
|
+
return {
|
|
121
|
+
"schema": SIDECAR_SCHEMA,
|
|
122
|
+
"picks": [_coerce_pick(p, index=i) for i, p in enumerate(picks, start=1)],
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def write_packet_sidecar(packet_path: Path, picks: list[dict]) -> Path:
|
|
127
|
+
"""Write `<packet>.prompts.json` beside the packet `.md` and return its path.
|
|
128
|
+
|
|
129
|
+
The canonical serializer the host renderer calls. `dos.preflight.load_packet_sidecar`
|
|
130
|
+
reads exactly this shape (same schema token, same per-pick `prompt_text`
|
|
131
|
+
field), so a packet written here loads as `SIDECAR_PRESENT` with non-empty
|
|
132
|
+
bodies — the write/read contract is closed by construction (a test asserts
|
|
133
|
+
the round-trip).
|
|
134
|
+
|
|
135
|
+
Does NOT validate shippability — a caller that wants the producer-side
|
|
136
|
+
guarantee calls `assert_packet_shippable` after writing (the renderer does
|
|
137
|
+
both: write, then assert, then exit). Writing is kept separate from
|
|
138
|
+
verifying so the verify can also run standalone against a packet written by
|
|
139
|
+
an older renderer.
|
|
140
|
+
"""
|
|
141
|
+
payload = build_sidecar_payload(picks)
|
|
142
|
+
side = sidecar_path_for(packet_path)
|
|
143
|
+
side.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
144
|
+
return side
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
# Producer-side shippability verify — the kernel does not believe the renderer.
|
|
149
|
+
# ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
# `assert_packet_shippable` outcome reasons. These are the structured causes a
|
|
152
|
+
# refuse carries; the host renderer maps a refuse onto the `RENDERER_SIDECAR_DROPPED`
|
|
153
|
+
# WedgeReason for the `.verdict` envelope it writes.
|
|
154
|
+
SHIPPABLE_OK = "ok"
|
|
155
|
+
SHIPPABLE_ABSENT = "sidecar_absent" # the renderer never wrote the sidecar
|
|
156
|
+
SHIPPABLE_CORRUPT = "sidecar_corrupt" # sidecar on disk but bad JSON / wrong shape
|
|
157
|
+
SHIPPABLE_EMPTY_BODIES = "sidecar_empty_bodies" # sidecar present but ≥1 pick body is empty
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@dataclass(frozen=True)
|
|
161
|
+
class ShippableVerdict:
|
|
162
|
+
"""The result of the producer-side sidecar verify.
|
|
163
|
+
|
|
164
|
+
`refuse` is the single load-bearing bool the renderer branches on before
|
|
165
|
+
`Saved:`/exit-0. `reason_code` is one of the `SHIPPABLE_*` constants
|
|
166
|
+
(machine-readable, stable). `reason` is the operator-facing string.
|
|
167
|
+
`empty_body_picks` lists the 1-based pick numbers whose `prompt_text` was
|
|
168
|
+
empty (so the renderer can name them, the same way the downstream gate names
|
|
169
|
+
`body_empty_picks`). `rendered_pick_count` echoes the input so the envelope
|
|
170
|
+
can record what the packet claimed.
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
refuse: bool
|
|
174
|
+
reason_code: str
|
|
175
|
+
reason: str | None = None
|
|
176
|
+
empty_body_picks: list[int] = field(default_factory=list)
|
|
177
|
+
rendered_pick_count: int = 0
|
|
178
|
+
|
|
179
|
+
def envelope(self) -> dict:
|
|
180
|
+
"""A small JSON-able dict for the renderer's `.verdict`/stderr envelope."""
|
|
181
|
+
return {
|
|
182
|
+
"refuse": self.refuse,
|
|
183
|
+
"reason_code": self.reason_code,
|
|
184
|
+
"reason": self.reason,
|
|
185
|
+
"empty_body_picks": self.empty_body_picks,
|
|
186
|
+
"rendered_pick_count": self.rendered_pick_count,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def assert_packet_shippable(
|
|
191
|
+
packet_path: Path, *, rendered_pick_count: int
|
|
192
|
+
) -> ShippableVerdict:
|
|
193
|
+
"""Re-open the just-written sidecar and verify the prompt bodies are real.
|
|
194
|
+
|
|
195
|
+
The DOS thesis at the producer: the renderer's exit-0 `Saved:` is a
|
|
196
|
+
self-report the kernel does not trust. This re-reads the artifact from disk
|
|
197
|
+
(not from the in-memory picks the renderer *thinks* it wrote) and refuses if:
|
|
198
|
+
|
|
199
|
+
* `rendered_pick_count > 0` and the sidecar is **absent** — the renderer
|
|
200
|
+
rendered picks but never serialized their bodies (the FQ-420 root drop);
|
|
201
|
+
* the sidecar is **corrupt** — on disk but unreadable / wrong shape;
|
|
202
|
+
* the sidecar is **present** but one or more picks have an **empty
|
|
203
|
+
`prompt_text`** — a half-built payload that would launch empty workers.
|
|
204
|
+
|
|
205
|
+
Does NOT refuse when `rendered_pick_count <= 0`: a genuine empty DRAIN packet
|
|
206
|
+
legitimately has no sidecar and no bodies (refusing it would mislabel a true
|
|
207
|
+
drain as a renderer drop — the same carve-out `_sidecar_dropped_refusal`
|
|
208
|
+
makes downstream).
|
|
209
|
+
|
|
210
|
+
Reads from disk via `sidecar_path_for(packet_path)`. Pure (no git / network);
|
|
211
|
+
the only I/O is the single sidecar read.
|
|
212
|
+
"""
|
|
213
|
+
if rendered_pick_count <= 0:
|
|
214
|
+
return ShippableVerdict(
|
|
215
|
+
refuse=False, reason_code=SHIPPABLE_OK, rendered_pick_count=rendered_pick_count
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
side = sidecar_path_for(packet_path)
|
|
219
|
+
if not side.exists():
|
|
220
|
+
return ShippableVerdict(
|
|
221
|
+
refuse=True,
|
|
222
|
+
reason_code=SHIPPABLE_ABSENT,
|
|
223
|
+
reason=(
|
|
224
|
+
f"sidecar_dropped:absent rendered_picks={rendered_pick_count} "
|
|
225
|
+
f"(renderer rendered picks but never wrote {side.name} — every "
|
|
226
|
+
f"worker prompt body would be empty)"
|
|
227
|
+
),
|
|
228
|
+
rendered_pick_count=rendered_pick_count,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
doc = json.loads(side.read_text(encoding="utf-8"))
|
|
233
|
+
picks = doc.get("picks", []) if isinstance(doc, dict) else None
|
|
234
|
+
if not isinstance(picks, list):
|
|
235
|
+
raise ValueError("sidecar has no picks list")
|
|
236
|
+
except (OSError, json.JSONDecodeError, ValueError) as exc:
|
|
237
|
+
return ShippableVerdict(
|
|
238
|
+
refuse=True,
|
|
239
|
+
reason_code=SHIPPABLE_CORRUPT,
|
|
240
|
+
reason=(
|
|
241
|
+
f"sidecar_dropped:corrupt rendered_picks={rendered_pick_count} "
|
|
242
|
+
f"({side.name} exists but is unreadable/bad-shape: {exc})"
|
|
243
|
+
),
|
|
244
|
+
rendered_pick_count=rendered_pick_count,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
empty = [
|
|
248
|
+
int(p.get("n", i))
|
|
249
|
+
for i, p in enumerate(picks, start=1)
|
|
250
|
+
if not str(p.get("prompt_text") or "").strip()
|
|
251
|
+
]
|
|
252
|
+
if empty:
|
|
253
|
+
return ShippableVerdict(
|
|
254
|
+
refuse=True,
|
|
255
|
+
reason_code=SHIPPABLE_EMPTY_BODIES,
|
|
256
|
+
reason=(
|
|
257
|
+
f"sidecar_empty_bodies picks={','.join(str(n) for n in empty)} "
|
|
258
|
+
f"({side.name} was written but those picks carry no prompt_text — "
|
|
259
|
+
f"a half-built payload)"
|
|
260
|
+
),
|
|
261
|
+
empty_body_picks=empty,
|
|
262
|
+
rendered_pick_count=rendered_pick_count,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
return ShippableVerdict(
|
|
266
|
+
refuse=False, reason_code=SHIPPABLE_OK, rendered_pick_count=rendered_pick_count
|
|
267
|
+
)
|