dos-kernel 0.22.0__py3-none-win_arm64.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/pick_priority.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""`pick_priority` — the freshness sort-key producer (docs/254).
|
|
2
|
+
|
|
3
|
+
The picker substrate already answers *is there anything pickable* (`pickable`),
|
|
4
|
+
*have I tried it* (`cooldown`), and *did the claim hold* (`reconcile`). But a fleet
|
|
5
|
+
can still **churn**: a dispatch loop that re-attempts a unit it has already tried —
|
|
6
|
+
one that did not move — instead of picking up new, not-started work. The job repo
|
|
7
|
+
measured this directly (docs/254): over 24h, 19 dispatch runs shipped only 1 pick
|
|
8
|
+
(5.3%); 18 of 19 DRAINED/BLOCKED, re-confirming known-drained units.
|
|
9
|
+
|
|
10
|
+
The root cause is an **ordering** gap, not a gate gap. The host's plan-sort key was
|
|
11
|
+
`(priority, status, id)` — there is no *freshness* term, so a never-attempted plan
|
|
12
|
+
and a plan drained 18× in a row sort identically, and ties break on alphabetical
|
|
13
|
+
`id` (blind to churn). `cooldown` only *gates* a unit after its window; the moment
|
|
14
|
+
that window lapses the churned unit sorts right back to the top next to fresh work,
|
|
15
|
+
because the sort itself never learned it was a repeat offender.
|
|
16
|
+
|
|
17
|
+
This module is the missing **ordering** primitive: it folds the attempt history the
|
|
18
|
+
host ALREADY records (the `cooldown` ledger) into a freshness rank, so the picker
|
|
19
|
+
prefers new work *within each priority tier*. Two signals (docs/254):
|
|
20
|
+
|
|
21
|
+
1. **Never-attempted first** — a unit with zero recorded attempts outranks any
|
|
22
|
+
attempted unit. The direct "pick up new not-started work" signal.
|
|
23
|
+
2. **Staler last-attempt first (LRU)** — among attempted units, least-recently-tried
|
|
24
|
+
wins, so attention rotates across the residual and nothing is permanently starved.
|
|
25
|
+
|
|
26
|
+
The safety invariant — why this is safe
|
|
27
|
+
========================================
|
|
28
|
+
|
|
29
|
+
> **Freshness is a TIE-BREAKER. Its `sort_key` is appended AFTER the host's
|
|
30
|
+
> `(priority, status)` key, so it can only reorder WITHIN a priority/status tier —
|
|
31
|
+
> it never gates a unit in or out, and never reorders across tiers.**
|
|
32
|
+
|
|
33
|
+
The consequences, each load-bearing:
|
|
34
|
+
|
|
35
|
+
* A P1 unit ALWAYS outranks a P2 unit, attempted or not — freshness never overrides
|
|
36
|
+
operator priority. (Contrast a stronger cooldown gate, which could *starve* a
|
|
37
|
+
ready high-priority unit by holding it out entirely.)
|
|
38
|
+
* Freshness changes ORDER, never ADMISSIBILITY. It cannot keep work out and cannot
|
|
39
|
+
let held work in. This is the same shape as the overlap-policy floor ("a policy
|
|
40
|
+
can only refuse-more, never admit"): here the primitive can only
|
|
41
|
+
*reorder-within-tier*, never *gate*. So a bug here degrades to "wrong order,"
|
|
42
|
+
never "starved work" or "double-booked lane."
|
|
43
|
+
|
|
44
|
+
⚓ Fail-open to never-attempted. A missing / garbled attempt summary degrades a unit
|
|
45
|
+
to `NEVER_ATTEMPTED` (sorts FIRST) — the pre-fix behaviour, never a refusal. This
|
|
46
|
+
matches the `cooldown` ledger's own observability-grade posture (an unreadable row
|
|
47
|
+
can only DELAY, never block): the safe direction for an ordering hint is "treat it
|
|
48
|
+
as fresh," the opposite of the correctness-read refuse-don't-guess floor.
|
|
49
|
+
|
|
50
|
+
⚓ Pure; host gathers state. `classify(unit_id, summary)` makes no file/git/clock
|
|
51
|
+
call. The host reads the attempt ledger at the boundary (it already does, for
|
|
52
|
+
`cooldown`) and hands in an `AttemptSummary`, exactly like `cooldown.cooldown_verdict`
|
|
53
|
+
is handed its attempt records. So the ordering contract replays on a frozen summary
|
|
54
|
+
list with no disk.
|
|
55
|
+
|
|
56
|
+
⚓ Parameter-free mechanism. Both signals come straight off the ledger; there are NO
|
|
57
|
+
tunable thresholds (unlike `[cooldown]`'s windows), so there is deliberately no
|
|
58
|
+
`[pick_priority]` config table. A future attempt-count or time-decay variant would
|
|
59
|
+
add one; this leaf does not.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
from __future__ import annotations
|
|
63
|
+
|
|
64
|
+
import enum
|
|
65
|
+
from dataclasses import dataclass
|
|
66
|
+
from typing import Optional
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# AttemptSummary — the per-unit fact the host hands in (derived from the ledger).
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True)
|
|
75
|
+
class AttemptSummary:
|
|
76
|
+
"""The attempt facts `classify` reads for one unit — PURE data the host gathers.
|
|
77
|
+
|
|
78
|
+
The host derives this from the same attempt ledger `cooldown` already reads
|
|
79
|
+
(`pick_cooldown.latest_attempts` in the job repo, or `lane_journal` OP_ATTEMPT
|
|
80
|
+
records): a unit ABSENT from that map is never-attempted; a unit present carries
|
|
81
|
+
its most-recent attempt's ms-epoch stamp.
|
|
82
|
+
|
|
83
|
+
* ``attempted`` — has this unit EVER been recorded as a pick-attempt?
|
|
84
|
+
* ``last_attempt_ms`` — the most-recent attempt's ms-epoch stamp (``None`` when
|
|
85
|
+
never attempted, or when a present row's stamp was unreadable — treated as
|
|
86
|
+
most-stale so it sorts earliest among attempted units; degrade-never-crash).
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
attempted: bool = False
|
|
90
|
+
last_attempt_ms: Optional[int] = None
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def never(cls) -> "AttemptSummary":
|
|
94
|
+
"""A never-attempted summary — the fail-open default (sorts FIRST)."""
|
|
95
|
+
return cls(attempted=False, last_attempt_ms=None)
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def at(cls, last_attempt_ms: Optional[int]) -> "AttemptSummary":
|
|
99
|
+
"""An attempted summary stamped at ``last_attempt_ms`` (None → most-stale)."""
|
|
100
|
+
return cls(attempted=True, last_attempt_ms=last_attempt_ms)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
# Freshness — the closed two-value verdict.
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class Freshness(str, enum.Enum):
|
|
109
|
+
"""Whether a unit has ever been attempted (docs/254).
|
|
110
|
+
|
|
111
|
+
`str`-valued so it round-trips a `--json` token / exit code without a lookup
|
|
112
|
+
table (the `CooldownState` / `Reconciliation` idiom). The two members are the
|
|
113
|
+
only freshness tiers; the LRU ordering among `ATTEMPTED` units lives in the
|
|
114
|
+
`PickPriority.sort_key`, not in a third enum member.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
NEVER_ATTEMPTED = "NEVER_ATTEMPTED" # zero recorded attempts — pick this first
|
|
118
|
+
ATTEMPTED = "ATTEMPTED" # tried before — demote below fresh work
|
|
119
|
+
|
|
120
|
+
def __str__(self) -> str: # pragma: no cover - trivial
|
|
121
|
+
return self.value
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
# PickPriority — the typed verdict carrying the load-bearing sort_key.
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass(frozen=True)
|
|
130
|
+
class PickPriority:
|
|
131
|
+
"""A unit's freshness verdict + the `sort_key` a picker appends to its own key.
|
|
132
|
+
|
|
133
|
+
Frozen + the kernel verdict idiom. ``freshness`` is the typed tier;
|
|
134
|
+
``last_attempt_ms`` is the stamp the LRU order reads (0 when never attempted or
|
|
135
|
+
unknown); ``reason`` is the operator-facing one-liner.
|
|
136
|
+
|
|
137
|
+
The load-bearing field is `sort_key` — the tuple a host appends AFTER its
|
|
138
|
+
`(priority, status)` key so freshness breaks ties WITHIN a tier and nowhere else.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
unit_id: str
|
|
142
|
+
freshness: Freshness
|
|
143
|
+
last_attempt_ms: int = 0
|
|
144
|
+
reason: str = ""
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def sort_key(self) -> tuple[int, int]:
|
|
148
|
+
"""The lower-wins tuple a picker appends to its `(priority, status, …)` key.
|
|
149
|
+
|
|
150
|
+
* NEVER_ATTEMPTED → ``(0, 0)`` — sorts FIRST (pick new work).
|
|
151
|
+
* ATTEMPTED → ``(1, last_attempt_ms)`` — sorts after all fresh work,
|
|
152
|
+
then ascending by last-attempt stamp = least-recently-tried first (LRU).
|
|
153
|
+
|
|
154
|
+
Lower wins, matching the host's existing lower-wins tuple sort. Because this
|
|
155
|
+
is appended after the priority/status terms, it can ONLY reorder within a
|
|
156
|
+
tier — never across tiers, never in/out of the candidate set (the safety
|
|
157
|
+
invariant in the module docstring).
|
|
158
|
+
"""
|
|
159
|
+
if self.freshness is Freshness.NEVER_ATTEMPTED:
|
|
160
|
+
return (0, 0)
|
|
161
|
+
return (1, self.last_attempt_ms)
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def is_fresh(self) -> bool:
|
|
165
|
+
"""True iff this unit has never been attempted (the inverse a picker reads)."""
|
|
166
|
+
return self.freshness is Freshness.NEVER_ATTEMPTED
|
|
167
|
+
|
|
168
|
+
def to_dict(self) -> dict:
|
|
169
|
+
return {
|
|
170
|
+
"unit_id": self.unit_id,
|
|
171
|
+
"freshness": self.freshness.value,
|
|
172
|
+
"last_attempt_ms": self.last_attempt_ms,
|
|
173
|
+
"sort_key": list(self.sort_key),
|
|
174
|
+
"reason": self.reason,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# ---------------------------------------------------------------------------
|
|
179
|
+
# classify — the pure fold over a unit's attempt summary.
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def classify(unit_id: str, summary: Optional[AttemptSummary]) -> PickPriority:
|
|
184
|
+
"""Fold a unit's attempt summary into its freshness verdict. PURE — no I/O.
|
|
185
|
+
|
|
186
|
+
``summary`` is the `AttemptSummary` the host derived from the attempt ledger
|
|
187
|
+
(the same ledger `cooldown` reads). The fold (docs/254):
|
|
188
|
+
|
|
189
|
+
* ``summary`` missing, or ``attempted is False`` → ``NEVER_ATTEMPTED`` (sorts
|
|
190
|
+
first). The fail-open default: a unit the host could not summarise is treated
|
|
191
|
+
as fresh, never refused.
|
|
192
|
+
* ``attempted is True`` → ``ATTEMPTED`` carrying ``last_attempt_ms`` (a missing
|
|
193
|
+
/ non-int stamp coerces to 0 → most-stale, sorts earliest among attempted).
|
|
194
|
+
|
|
195
|
+
Returns a `PickPriority`; never raises.
|
|
196
|
+
"""
|
|
197
|
+
uid = str(unit_id)
|
|
198
|
+
|
|
199
|
+
# Fail-open: no summary, or an explicitly never-attempted one → fresh.
|
|
200
|
+
if summary is None or not getattr(summary, "attempted", False):
|
|
201
|
+
return PickPriority(
|
|
202
|
+
unit_id=uid,
|
|
203
|
+
freshness=Freshness.NEVER_ATTEMPTED,
|
|
204
|
+
last_attempt_ms=0,
|
|
205
|
+
reason="no recorded pick-attempt — never-attempted; pick this before "
|
|
206
|
+
"any already-tried unit (fresh work first)",
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Attempted — carry the last-attempt stamp for the LRU order. A missing / garbled
|
|
210
|
+
# stamp coerces to 0 (most-stale) so a present-but-unstamped row still sorts as
|
|
211
|
+
# attempted, just earliest among them — degrade-never-crash.
|
|
212
|
+
raw = getattr(summary, "last_attempt_ms", None)
|
|
213
|
+
try:
|
|
214
|
+
last_ms = int(raw) if raw is not None else 0
|
|
215
|
+
except (TypeError, ValueError):
|
|
216
|
+
last_ms = 0
|
|
217
|
+
|
|
218
|
+
return PickPriority(
|
|
219
|
+
unit_id=uid,
|
|
220
|
+
freshness=Freshness.ATTEMPTED,
|
|
221
|
+
last_attempt_ms=last_ms,
|
|
222
|
+
reason=(f"already attempted (last at {last_ms}ms) — demoted below "
|
|
223
|
+
f"never-attempted work; among attempted units the least-recently-"
|
|
224
|
+
f"tried sorts first (LRU)"),
|
|
225
|
+
)
|
dos/pickable.py
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""`pickable` — the pre-dispatch gate (docs/168 Concept 2).
|
|
2
|
+
|
|
3
|
+
The kernel already owns four ground-truth syscalls (`verify`/`oracle`,
|
|
4
|
+
`arbitrate`, `liveness`, `scout`/`loop_decide`). But a fleet's throughput is
|
|
5
|
+
lost *before a worker launches* to a question the kernel did not yet own:
|
|
6
|
+
|
|
7
|
+
> "Is there anything here a worker could actually pick up — and if not, *why
|
|
8
|
+
> not*, precisely enough to route?"
|
|
9
|
+
|
|
10
|
+
The `job` host answered this in its own code
|
|
11
|
+
(`fanout_state._phase_universe_has_pickable_phase`,
|
|
12
|
+
`next_up_context._attach_pick_gates`, `plan_pickability._phase_gate_reason`),
|
|
13
|
+
and every bug in that re-implementation was a fleet-wide wedge: the drain-trap
|
|
14
|
+
(FQ-493 / ASI #475 / RTN / FMP — the pick-count oracle counted a DEFERRED /
|
|
15
|
+
DRAFT / operator-gated phase as *pickable*), the FQ-420 un-typed gate-set, the
|
|
16
|
+
picker-invisibility gap. The `picker_oracle` module is a **post-hoc audit** that
|
|
17
|
+
reconstructs ground truth *after* a dispatch emitted a verdict, to *measure*
|
|
18
|
+
picker precision/recall — it is NOT a pre-dispatch gate the picker can call to
|
|
19
|
+
decide what to offer. This module is that gate.
|
|
20
|
+
|
|
21
|
+
The relationship to `picker_oracle`:
|
|
22
|
+
|
|
23
|
+
* `pickable.classify` → the **pre-flight gate** — decide what to offer.
|
|
24
|
+
* `picker_oracle` → the **post-flight audit** — was the gate right?
|
|
25
|
+
|
|
26
|
+
They share ONE vocabulary. `HoldReason` is the FINER closed set; it collapses to
|
|
27
|
+
the coarse `picker_oracle.NoPickCause` via `.to_no_pick_cause`, so the gate and
|
|
28
|
+
the audit can never drift (the same `gate_classify` → `dispatch-loop` shape that
|
|
29
|
+
already worked). Concretely: a `HELD(OPERATOR_GATED)` pre-flight is exactly the
|
|
30
|
+
case `picker_oracle` audits as `OPERATOR_GATE` — one enum, two consumers.
|
|
31
|
+
|
|
32
|
+
The keystone is `HoldReason.is_redispatch_invariant`. The single most expensive
|
|
33
|
+
recurring mistake was a loop that kept re-dispatching a lane whose *only* hold
|
|
34
|
+
reason was one a re-dispatch cannot change (`DRAFT_CLASS`, `OPERATOR_GATED`,
|
|
35
|
+
`SOAK_OPEN`, `DEPENDENCY_UNMET`). `loop_decide` reads that flag and gains a clean
|
|
36
|
+
rung: a lane held only by re-dispatch-invariant reasons is STOP-now, not
|
|
37
|
+
continue — the honest-STOP that was a per-run human override becomes a kernel
|
|
38
|
+
rule (docs/168 §5; the same move docs/145 made for the stall reader).
|
|
39
|
+
|
|
40
|
+
⚓ Pure; host gathers state. Identical seam to `dos.scout.choose` reading a
|
|
41
|
+
sibling `HealthVerdict`: `classify(unit_state, …)` is `pure(state)`, all the I/O
|
|
42
|
+
(read the plan class, the soak index, the live claims) on the host adapter side.
|
|
43
|
+
The host's `_phase_universe_has_pickable_phase` becomes a thin
|
|
44
|
+
`all(classify(u, …).held for u in units)` instead of bespoke gate logic.
|
|
45
|
+
|
|
46
|
+
⚓ Degrade, never crash. A missing key in `unit_state` is treated as its falsy
|
|
47
|
+
default; `classify` never raises. The picker-invisibility gap was a *silent*
|
|
48
|
+
drop; the cure here is a typed verdict the picker can always produce.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
from __future__ import annotations
|
|
52
|
+
|
|
53
|
+
import enum
|
|
54
|
+
from dataclasses import dataclass
|
|
55
|
+
from typing import Mapping, Optional
|
|
56
|
+
|
|
57
|
+
from dos import picker_oracle
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# HoldReason — the finer closed set of reasons a unit is not offerable.
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class HoldReason(str, enum.Enum):
|
|
66
|
+
"""Why a declared work unit is NOT offerable to a worker right now.
|
|
67
|
+
|
|
68
|
+
The single closed enum of hold reasons — the keystone of docs/168 §2. The
|
|
69
|
+
drain-trap existed because "gated" and "shipped" were collapsed into a single
|
|
70
|
+
boolean ("has a pickable phase: y/n") with no reason. As a kernel enum the
|
|
71
|
+
reasons become the contract every picker shares, and the consequence-routing
|
|
72
|
+
(a `DRAFT_CLASS` hold → `/promote`; an `OPERATOR_GATED` hold → escalate a
|
|
73
|
+
decision; a `SOAK_OPEN` hold → wait, never `/replan`) is derivable from the
|
|
74
|
+
reason instead of re-discovered per incident.
|
|
75
|
+
|
|
76
|
+
`str`-valued so it round-trips through a host's JSON/stdout token without a
|
|
77
|
+
lookup table (mirrors `gate_classify.Verdict`, `picker_oracle.NoPickCause`).
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
SHIPPED = "SHIPPED" # already verified shipped — drop from residual
|
|
81
|
+
IN_FLIGHT = "IN_FLIGHT" # a live worker is already on this unit
|
|
82
|
+
SOFT_CLAIMED_ELSEWHERE = "SOFT_CLAIMED_ELSEWHERE" # a sibling fanout holds a live soft-claim
|
|
83
|
+
DRAFT_CLASS = "DRAFT_CLASS" # plan is DRAFT — phases not greenlit for build (FMP / #493)
|
|
84
|
+
OPERATOR_GATED = "OPERATOR_GATED" # blocked on an open operator decision (ASI / #475)
|
|
85
|
+
SOAK_OPEN = "SOAK_OPEN" # a soak deadline has not yet elapsed (RTN)
|
|
86
|
+
DEPENDENCY_UNMET = "DEPENDENCY_UNMET" # a prerequisite unit has not shipped
|
|
87
|
+
COOLDOWN = "COOLDOWN" # tried recently; per-pick cooldown window not elapsed
|
|
88
|
+
UNPARSEABLE = "UNPARSEABLE" # the unit's declaration could not be parsed (typed, not silent)
|
|
89
|
+
STALE_CLAIM = "STALE_CLAIM" # blocked by a claim that is itself orphaned/stale
|
|
90
|
+
|
|
91
|
+
def __str__(self) -> str: # pragma: no cover - trivial
|
|
92
|
+
return self.value
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def is_redispatch_invariant(self) -> bool:
|
|
96
|
+
"""True iff a re-dispatch CANNOT change this hold.
|
|
97
|
+
|
|
98
|
+
The keystone for the `loop_decide` honest-STOP rung (docs/168 §5). A lane
|
|
99
|
+
held only by these reasons will re-block identically on the next
|
|
100
|
+
iteration — re-dispatching it is pure waste. The four members:
|
|
101
|
+
|
|
102
|
+
* DRAFT_CLASS — only an operator promotion (DRAFT→ACTIVE) un-gates it.
|
|
103
|
+
* OPERATOR_GATED — only an operator decision un-gates it.
|
|
104
|
+
* SOAK_OPEN — only the passage of wall-clock time un-gates it
|
|
105
|
+
(a re-dispatch *now* cannot fast-forward the soak).
|
|
106
|
+
* DEPENDENCY_UNMET — only shipping the prerequisite un-gates it; a
|
|
107
|
+
re-dispatch of THIS unit cannot.
|
|
108
|
+
|
|
109
|
+
The re-dispatch-CURABLE reasons are deliberately NOT here:
|
|
110
|
+
|
|
111
|
+
* SHIPPED — terminal (drop from residual; the loop is done with it,
|
|
112
|
+
not stuck on it — it never re-enters a dispatch attempt).
|
|
113
|
+
* IN_FLIGHT / SOFT_CLAIMED_ELSEWHERE / STALE_CLAIM — clear when the holder
|
|
114
|
+
finishes / a claim ages out / a scavenge runs.
|
|
115
|
+
* COOLDOWN — clears when the cooldown window elapses.
|
|
116
|
+
* UNPARSEABLE — clears when the host fixes/re-parses the declaration.
|
|
117
|
+
"""
|
|
118
|
+
return self in _REDISPATCH_INVARIANT
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def to_no_pick_cause(self) -> "picker_oracle.NoPickCause":
|
|
122
|
+
"""Map onto the coarse post-hoc `picker_oracle.NoPickCause` vocabulary.
|
|
123
|
+
|
|
124
|
+
docs/168 §2: the pre-flight gate and the post-hoc audit MUST share one
|
|
125
|
+
vocabulary so a `HELD(reason)` the picker emits is the same thing the
|
|
126
|
+
oracle later audits. The finer `HoldReason` collapses onto the coarse
|
|
127
|
+
`NoPickCause` exactly as `picker_oracle._LEGACY_REASON_ALIASES` already
|
|
128
|
+
collapses `OPERATOR_GATED`/`SOAK_OPEN` → `OPERATOR_GATE`:
|
|
129
|
+
|
|
130
|
+
* DRAFT_CLASS / OPERATOR_GATED / SOAK_OPEN → OPERATOR_GATE
|
|
131
|
+
* IN_FLIGHT / SOFT_CLAIMED_ELSEWHERE / STALE_CLAIM → STALE_CLAIM
|
|
132
|
+
* SHIPPED / DEPENDENCY_UNMET → TRUE_DRAIN
|
|
133
|
+
* COOLDOWN → TRUE_DRAIN
|
|
134
|
+
* UNPARSEABLE → UNCLASSIFIED
|
|
135
|
+
"""
|
|
136
|
+
return _TO_NO_PICK_CAUSE[self]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
_REDISPATCH_INVARIANT: frozenset[HoldReason] = frozenset(
|
|
140
|
+
{
|
|
141
|
+
HoldReason.DRAFT_CLASS,
|
|
142
|
+
HoldReason.OPERATOR_GATED,
|
|
143
|
+
HoldReason.SOAK_OPEN,
|
|
144
|
+
HoldReason.DEPENDENCY_UNMET,
|
|
145
|
+
}
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# The coarse mapping (docs/168 §2). Total over `HoldReason` — pinned by
|
|
150
|
+
# `tests/test_pickable.py` (every member maps to a real `NoPickCause`).
|
|
151
|
+
_TO_NO_PICK_CAUSE: dict[HoldReason, "picker_oracle.NoPickCause"] = {
|
|
152
|
+
HoldReason.SHIPPED: picker_oracle.NoPickCause.TRUE_DRAIN,
|
|
153
|
+
HoldReason.IN_FLIGHT: picker_oracle.NoPickCause.STALE_CLAIM,
|
|
154
|
+
HoldReason.SOFT_CLAIMED_ELSEWHERE: picker_oracle.NoPickCause.STALE_CLAIM,
|
|
155
|
+
HoldReason.DRAFT_CLASS: picker_oracle.NoPickCause.OPERATOR_GATE,
|
|
156
|
+
HoldReason.OPERATOR_GATED: picker_oracle.NoPickCause.OPERATOR_GATE,
|
|
157
|
+
HoldReason.SOAK_OPEN: picker_oracle.NoPickCause.OPERATOR_GATE,
|
|
158
|
+
HoldReason.DEPENDENCY_UNMET: picker_oracle.NoPickCause.TRUE_DRAIN,
|
|
159
|
+
HoldReason.COOLDOWN: picker_oracle.NoPickCause.TRUE_DRAIN,
|
|
160
|
+
HoldReason.UNPARSEABLE: picker_oracle.NoPickCause.UNCLASSIFIED,
|
|
161
|
+
HoldReason.STALE_CLAIM: picker_oracle.NoPickCause.STALE_CLAIM,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# ---------------------------------------------------------------------------
|
|
166
|
+
# Pickability — the typed verdict.
|
|
167
|
+
# ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@dataclass(frozen=True)
|
|
171
|
+
class Pickability:
|
|
172
|
+
"""Whether a unit is offerable to a worker right now, and the typed reason
|
|
173
|
+
it is not.
|
|
174
|
+
|
|
175
|
+
Frozen + classmethod-constructors, the kernel verdict idiom (mirrors
|
|
176
|
+
`admission.AdmissionVerdict.admit()/refuse()`):
|
|
177
|
+
|
|
178
|
+
* `Pickability.OFFERABLE()` — nothing holds the unit; offer it.
|
|
179
|
+
* `Pickability.HELD(reason, ev)` — held by exactly one typed `HoldReason`,
|
|
180
|
+
with operator-facing `evidence`.
|
|
181
|
+
|
|
182
|
+
`held` is the load-bearing field a picker branches on (the inverse of
|
|
183
|
+
`OFFERABLE`); `reason` is `None` iff `held is False`.
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
held: bool
|
|
187
|
+
reason: Optional[HoldReason] = None
|
|
188
|
+
evidence: str = ""
|
|
189
|
+
|
|
190
|
+
@classmethod
|
|
191
|
+
def OFFERABLE(cls) -> "Pickability":
|
|
192
|
+
"""An offerable verdict — no hold applies; a worker may pick this up."""
|
|
193
|
+
return cls(held=False, reason=None, evidence="")
|
|
194
|
+
|
|
195
|
+
@classmethod
|
|
196
|
+
def HELD(cls, reason: HoldReason, evidence: str = "") -> "Pickability":
|
|
197
|
+
"""A held verdict carrying the single typed `reason` it is not offerable
|
|
198
|
+
and an operator-facing `evidence` line."""
|
|
199
|
+
return cls(held=True, reason=reason, evidence=evidence)
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def is_redispatch_invariant(self) -> bool:
|
|
203
|
+
"""True iff this verdict is HELD by a re-dispatch-invariant reason.
|
|
204
|
+
|
|
205
|
+
The convenience the `loop_decide` rung reads: `held` AND the reason is
|
|
206
|
+
one a re-dispatch cannot change. `OFFERABLE` is never invariant (it is
|
|
207
|
+
not held at all)."""
|
|
208
|
+
return self.held and self.reason is not None and self.reason.is_redispatch_invariant
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ---------------------------------------------------------------------------
|
|
212
|
+
# classify — the pure pre-dispatch gate.
|
|
213
|
+
# ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def classify(
|
|
217
|
+
unit_state: Mapping,
|
|
218
|
+
*,
|
|
219
|
+
now_ms: int,
|
|
220
|
+
policy: Optional[Mapping] = None,
|
|
221
|
+
) -> Pickability:
|
|
222
|
+
"""Decide whether a declared work unit is offerable. PURE — no I/O.
|
|
223
|
+
|
|
224
|
+
`unit_state` is a dict the HOST pre-gathers (all the file/git/registry reads
|
|
225
|
+
happen on the adapter side — the same seam as `dos.scout.choose` reading a
|
|
226
|
+
sibling `HealthVerdict`). Recognised keys, each defaulting to its falsy value
|
|
227
|
+
when absent (degrade-never-crash):
|
|
228
|
+
|
|
229
|
+
* shipped: bool — the unit is already verified shipped.
|
|
230
|
+
* in_flight: bool — a live worker is on this unit now.
|
|
231
|
+
* soft_claimed_elsewhere: bool — a sibling fanout holds a live soft-claim.
|
|
232
|
+
* plan_class: str — the plan's class ("DRAFT" → DRAFT_CLASS).
|
|
233
|
+
* operator_gated: bool — blocked on an open operator decision.
|
|
234
|
+
* soak_open: bool — a soak deadline has not yet elapsed.
|
|
235
|
+
* dependency_unmet: bool — a prerequisite unit has not shipped.
|
|
236
|
+
* cooldown_until_ms: int | None — per-pick cooldown wall; held iff `now_ms`
|
|
237
|
+
is strictly before it.
|
|
238
|
+
* unparseable: bool — the declaration could not be parsed.
|
|
239
|
+
|
|
240
|
+
`now_ms` is the caller's clock (an input, never read from the wall here — the
|
|
241
|
+
same discipline as `liveness.classify`), used only for the COOLDOWN check.
|
|
242
|
+
`policy` is reserved for host-declared knobs (docs/168 §"mechanism-not-
|
|
243
|
+
policy"); unused today, accepted so the signature is stable.
|
|
244
|
+
|
|
245
|
+
Precedence (most-terminal / most-specific first, documented in docs/168 §2):
|
|
246
|
+
|
|
247
|
+
1. SHIPPED — terminal; nothing else matters once it shipped.
|
|
248
|
+
2. UNPARSEABLE — a typed "I could not parse this" beats every
|
|
249
|
+
content gate (a gate read off an unparseable
|
|
250
|
+
declaration is meaningless; surface the parse
|
|
251
|
+
failure instead of a derived hold).
|
|
252
|
+
3. the in-flight family — IN_FLIGHT, then SOFT_CLAIMED_ELSEWHERE, then
|
|
253
|
+
STALE_CLAIM (a live worker / claim wins over a
|
|
254
|
+
class/gate/soak/dep reason — the unit IS being
|
|
255
|
+
worked, the gate would only matter once it frees).
|
|
256
|
+
4. DRAFT_CLASS — the plan class gate.
|
|
257
|
+
5. OPERATOR_GATED — an open operator decision.
|
|
258
|
+
6. SOAK_OPEN — an unelapsed soak.
|
|
259
|
+
7. DEPENDENCY_UNMET — an unshipped prerequisite.
|
|
260
|
+
8. COOLDOWN — the per-pick cooldown wall (the most transient,
|
|
261
|
+
curable by time alone — checked last).
|
|
262
|
+
|
|
263
|
+
Returns `Pickability.OFFERABLE()` when no hold applies.
|
|
264
|
+
"""
|
|
265
|
+
s = unit_state or {}
|
|
266
|
+
|
|
267
|
+
def _b(key: str) -> bool:
|
|
268
|
+
# Defensive truthiness — a missing key is its falsy default; never raise.
|
|
269
|
+
try:
|
|
270
|
+
return bool(s.get(key))
|
|
271
|
+
except AttributeError: # pragma: no cover - non-Mapping degrade path
|
|
272
|
+
return False
|
|
273
|
+
|
|
274
|
+
# 1. SHIPPED — terminal. Once verified shipped the unit leaves the residual;
|
|
275
|
+
# no later gate can resurrect it.
|
|
276
|
+
if _b("shipped"):
|
|
277
|
+
return Pickability.HELD(
|
|
278
|
+
HoldReason.SHIPPED,
|
|
279
|
+
"unit is verified shipped — drop from the residual",
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# 2. UNPARSEABLE — a typed parse failure beats every content gate. The
|
|
283
|
+
# picker-invisibility gap was a SILENT drop; this is the cure — surface a
|
|
284
|
+
# refusal reason instead of an empty universe.
|
|
285
|
+
if _b("unparseable"):
|
|
286
|
+
return Pickability.HELD(
|
|
287
|
+
HoldReason.UNPARSEABLE,
|
|
288
|
+
"the unit's declaration could not be parsed — surfaced as a typed "
|
|
289
|
+
"refusal rather than silently dropped",
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# 3. The in-flight family — a live worker / claim on the unit. It IS being
|
|
293
|
+
# worked (or held by a sibling), so a class/gate/soak reason is moot until
|
|
294
|
+
# it frees; report the live holder, most-specific first.
|
|
295
|
+
if _b("in_flight"):
|
|
296
|
+
return Pickability.HELD(
|
|
297
|
+
HoldReason.IN_FLIGHT,
|
|
298
|
+
"a live worker is already on this unit",
|
|
299
|
+
)
|
|
300
|
+
if _b("soft_claimed_elsewhere"):
|
|
301
|
+
return Pickability.HELD(
|
|
302
|
+
HoldReason.SOFT_CLAIMED_ELSEWHERE,
|
|
303
|
+
"a sibling fanout holds a live soft-claim on this unit",
|
|
304
|
+
)
|
|
305
|
+
if _b("stale_claim"):
|
|
306
|
+
return Pickability.HELD(
|
|
307
|
+
HoldReason.STALE_CLAIM,
|
|
308
|
+
"blocked by a claim that is itself orphaned/stale",
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# 4. DRAFT_CLASS — the plan is DRAFT; its phases are not greenlit for build.
|
|
312
|
+
# Only an operator promotion (DRAFT→ACTIVE) un-gates it — a /replan cannot
|
|
313
|
+
# (FMP / decision #493). Re-dispatch-invariant.
|
|
314
|
+
plan_class = ""
|
|
315
|
+
try:
|
|
316
|
+
plan_class = str(s.get("plan_class") or "").strip().upper()
|
|
317
|
+
except AttributeError: # pragma: no cover - non-Mapping degrade path
|
|
318
|
+
plan_class = ""
|
|
319
|
+
if plan_class == "DRAFT":
|
|
320
|
+
return Pickability.HELD(
|
|
321
|
+
HoldReason.DRAFT_CLASS,
|
|
322
|
+
"plan is DRAFT-class — phases are not greenlit for build; only an "
|
|
323
|
+
"operator promotion (DRAFT→ACTIVE) un-gates it, not a /replan",
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
# 5. OPERATOR_GATED — an open operator decision blocks it (ASI / #475).
|
|
327
|
+
# Re-dispatch-invariant: only an operator answer un-gates it.
|
|
328
|
+
if _b("operator_gated"):
|
|
329
|
+
return Pickability.HELD(
|
|
330
|
+
HoldReason.OPERATOR_GATED,
|
|
331
|
+
"blocked on an open operator decision — escalate the decision; a "
|
|
332
|
+
"re-dispatch cannot answer it",
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
# 6. SOAK_OPEN — a soak deadline has not yet elapsed (RTN). Re-dispatch-
|
|
336
|
+
# invariant: only the passage of time un-gates it; never /replan, wait.
|
|
337
|
+
if _b("soak_open"):
|
|
338
|
+
return Pickability.HELD(
|
|
339
|
+
HoldReason.SOAK_OPEN,
|
|
340
|
+
"a soak deadline has not yet elapsed — wait for the soak to close; a "
|
|
341
|
+
"re-dispatch now cannot fast-forward it",
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
# 7. DEPENDENCY_UNMET — a prerequisite unit has not shipped. Re-dispatch-
|
|
345
|
+
# invariant: only shipping the prerequisite un-gates THIS unit.
|
|
346
|
+
if _b("dependency_unmet"):
|
|
347
|
+
return Pickability.HELD(
|
|
348
|
+
HoldReason.DEPENDENCY_UNMET,
|
|
349
|
+
"a prerequisite unit has not shipped — ship the dependency first; a "
|
|
350
|
+
"re-dispatch of this unit cannot",
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
# 8. COOLDOWN — the per-pick cooldown wall. The most transient hold (curable
|
|
354
|
+
# by wall-clock time alone), so it is checked last. Held iff `now_ms` is
|
|
355
|
+
# strictly before the wall; a missing/None/zero wall never holds.
|
|
356
|
+
cooldown_until = s.get("cooldown_until_ms")
|
|
357
|
+
if cooldown_until is not None:
|
|
358
|
+
try:
|
|
359
|
+
wall = int(cooldown_until)
|
|
360
|
+
except (TypeError, ValueError): # pragma: no cover - defensive
|
|
361
|
+
wall = 0
|
|
362
|
+
if wall > 0 and now_ms < wall:
|
|
363
|
+
return Pickability.HELD(
|
|
364
|
+
HoldReason.COOLDOWN,
|
|
365
|
+
f"per-pick cooldown active until {wall}ms (now {now_ms}ms) — "
|
|
366
|
+
f"the window has not elapsed",
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
return Pickability.OFFERABLE()
|