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/state_health.py
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
"""State-file health — a pure verdict over an external durable state file.
|
|
2
|
+
|
|
3
|
+
The gap this closes
|
|
4
|
+
===================
|
|
5
|
+
|
|
6
|
+
`dos doctor` reports the *path* of the workspace's execution-state file but says
|
|
7
|
+
nothing about its *health*. In practice that file (a host's `execution-state.yaml`,
|
|
8
|
+
a registry, any large append-mostly substrate the kernel does not own the schema of)
|
|
9
|
+
accumulates two debts the kernel could flag but doesn't:
|
|
10
|
+
|
|
11
|
+
1. **Bloat** — cold, never-on-a-hot-path sections (a completed-work log, an
|
|
12
|
+
abandoned-claim log, an archive sink) grow past any size a reader windows to,
|
|
13
|
+
yet every read-modify-write of the file re-parses all of them. This is the
|
|
14
|
+
`should_compact` problem (docs/106 §3.2) pointed at an *external* file instead
|
|
15
|
+
of the lane journal.
|
|
16
|
+
2. **Mid-flight / deferred-obligation drift** — a multi-step transition was
|
|
17
|
+
*intentionally* left partial ("migrate the rest in a quiet window," "flip the
|
|
18
|
+
flag later") with no run holding it, no deadline, and no detector. It looks
|
|
19
|
+
like a healthy steady state, so nothing surfaces that it is owed. The intent
|
|
20
|
+
ledger (docs/107) cannot catch this: there is no `run_id` and no crash — the
|
|
21
|
+
work was *parked by design*, then orphaned. See docs/133.
|
|
22
|
+
|
|
23
|
+
This module is the read-side verdict for both. It is **pure** — it classifies
|
|
24
|
+
caller-gathered evidence against a declared policy and returns a typed verdict; it
|
|
25
|
+
does no I/O, reads no clock it was not handed, and **never performs a fix** (it
|
|
26
|
+
surfaces; a driver or human acts — the docs/99 advisory-only floor). It is the
|
|
27
|
+
`liveness.classify(evidence, policy) -> verdict` template applied to a file's
|
|
28
|
+
health, with `retention.should_compact`'s monotone-threshold posture for the size
|
|
29
|
+
rung.
|
|
30
|
+
|
|
31
|
+
The obligation rung — the docs/133 prototype
|
|
32
|
+
============================================
|
|
33
|
+
|
|
34
|
+
An ``Obligation`` is a deferred transition carrying its **completion predicate as
|
|
35
|
+
a pre-evaluated boolean** (the caller, which can touch the world, evaluates the
|
|
36
|
+
predicate at the boundary and hands the result in; this leaf stays pure). The
|
|
37
|
+
verdict re-states it as one of ``SATISFIED`` / ``PENDING`` / ``BLOCKED`` /
|
|
38
|
+
``STALE`` — the same fail-closed direction every DOS verdict takes (an obligation
|
|
39
|
+
whose predicate the caller could not evaluate degrades toward "still owed," never
|
|
40
|
+
toward "assume done"). A ``PENDING`` obligation past its declared horizon becomes
|
|
41
|
+
``STALE`` — the rung that makes "we left a migration half-done 30 days ago"
|
|
42
|
+
*structurally visible* the way a treeless lane already is.
|
|
43
|
+
|
|
44
|
+
Why a leaf with no I/O
|
|
45
|
+
======================
|
|
46
|
+
|
|
47
|
+
Same kernel/driver split as `retention` (the policy + `should_compact` are pure
|
|
48
|
+
data; the reapers that scandir/unlink live in the driver) and `liveness` (the
|
|
49
|
+
`classify` fold is pure; the boundary gathers the beats). A host's adapter gathers
|
|
50
|
+
the file's section sizes + evaluates each obligation's predicate (both of which
|
|
51
|
+
touch disk), then calls `classify_state_file`. Nothing in the kernel imports *down*
|
|
52
|
+
into a driver to use this.
|
|
53
|
+
|
|
54
|
+
Pure stdlib — no third-party imports, no I/O, no `time` read (the clock is handed
|
|
55
|
+
in as `now_ms`, the way a pure verdict is handed a clock).
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
from __future__ import annotations
|
|
59
|
+
|
|
60
|
+
import enum
|
|
61
|
+
from dataclasses import dataclass, field
|
|
62
|
+
from typing import Mapping, Sequence
|
|
63
|
+
|
|
64
|
+
_MS_PER_DAY = 86_400_000
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# Obligations — the docs/133 deferred-transition record (prototype).
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ObligationStatus(str, enum.Enum):
|
|
73
|
+
"""The adjudicated state of one deferred obligation.
|
|
74
|
+
|
|
75
|
+
`str`-valued so it round-trips a `--json` token without a lookup (`Liveness` /
|
|
76
|
+
`durable_schema.Readability` idiom). The asymmetry is the whole point: only
|
|
77
|
+
SATISFIED clears the debt; everything else keeps it visible, and STALE
|
|
78
|
+
escalates it.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
SATISFIED = "SATISFIED" # the completion predicate holds — the debt is paid, drop it
|
|
82
|
+
PENDING = "PENDING" # not complete, still inside its declared horizon — owed, surfaced
|
|
83
|
+
BLOCKED = "BLOCKED" # a precondition is unmet — a human must clear it before it can complete
|
|
84
|
+
STALE = "STALE" # PENDING past its horizon — escalate (the "forgotten migration" rung)
|
|
85
|
+
|
|
86
|
+
def __str__(self) -> str: # pragma: no cover - trivial
|
|
87
|
+
return self.value
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def is_cleared(self) -> bool:
|
|
91
|
+
"""True iff the obligation no longer needs to be tracked (SATISFIED only)."""
|
|
92
|
+
return self is ObligationStatus.SATISFIED
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def needs_attention(self) -> bool:
|
|
96
|
+
"""True iff a surfacer should report this (everything that is not cleared)."""
|
|
97
|
+
return self is not ObligationStatus.SATISFIED
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True)
|
|
101
|
+
class Obligation:
|
|
102
|
+
"""A deferred transition the kernel must keep visible until its predicate holds.
|
|
103
|
+
|
|
104
|
+
The caller (at the I/O boundary) evaluates the completion predicate and the
|
|
105
|
+
blocked precondition against the live world and hands the booleans in; this
|
|
106
|
+
record stays pure data. Fields:
|
|
107
|
+
|
|
108
|
+
* ``key`` — a stable id for the obligation (e.g. ``"migration:history-store"``).
|
|
109
|
+
* ``description`` — the human one-liner ("drain YAML history into SQLite, then
|
|
110
|
+
flip JOB_HISTORY_IN_STORE and drop the YAML buckets").
|
|
111
|
+
* ``predicate_summary`` — the completion predicate *as text*, for the surfaced
|
|
112
|
+
line ("store ⊇ yaml ∧ flag_flipped ∧ yaml_buckets_empty"). Carried so a
|
|
113
|
+
reader can see WHAT is owed, not just THAT something is.
|
|
114
|
+
* ``satisfied`` — the caller's evaluation of the completion predicate. None
|
|
115
|
+
means "could not evaluate" → fail-closed to PENDING (never SATISFIED).
|
|
116
|
+
* ``blocked`` — True iff a precondition is unmet (the completion cannot even
|
|
117
|
+
be attempted yet); takes precedence over a not-yet-satisfied PENDING.
|
|
118
|
+
* ``declared_at_ms`` — when the obligation was first recorded (the horizon
|
|
119
|
+
anchor). None disables the staleness rung for this obligation.
|
|
120
|
+
* ``horizon_days`` — how long the deferral is acceptable before it escalates
|
|
121
|
+
to STALE. None = no horizon (PENDING forever, never STALE).
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
key: str
|
|
125
|
+
description: str
|
|
126
|
+
predicate_summary: str = ""
|
|
127
|
+
satisfied: bool | None = None
|
|
128
|
+
blocked: bool = False
|
|
129
|
+
declared_at_ms: int | None = None
|
|
130
|
+
horizon_days: float | None = None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def classify_obligation(ob: Obligation, *, now_ms: int) -> ObligationStatus:
|
|
134
|
+
"""Adjudicate one obligation. PURE — the predicate is pre-evaluated by the caller.
|
|
135
|
+
|
|
136
|
+
Order is fail-closed:
|
|
137
|
+
* a satisfied predicate clears it (SATISFIED) — the only exit;
|
|
138
|
+
* else a blocked precondition is BLOCKED (a human must act);
|
|
139
|
+
* else if it has a horizon and is past it, STALE (escalate the forgotten debt);
|
|
140
|
+
* else PENDING (owed, but still within its acceptable deferral window).
|
|
141
|
+
|
|
142
|
+
``satisfied is None`` (the caller could not evaluate the predicate) is treated
|
|
143
|
+
as not-satisfied — the same direction every DOS verdict takes when evidence is
|
|
144
|
+
missing (degrade toward "redo / still owed," never toward "assume done").
|
|
145
|
+
"""
|
|
146
|
+
if ob.satisfied is True:
|
|
147
|
+
return ObligationStatus.SATISFIED
|
|
148
|
+
if ob.blocked:
|
|
149
|
+
return ObligationStatus.BLOCKED
|
|
150
|
+
if (
|
|
151
|
+
ob.declared_at_ms is not None
|
|
152
|
+
and ob.horizon_days is not None
|
|
153
|
+
and (now_ms - ob.declared_at_ms) > ob.horizon_days * _MS_PER_DAY
|
|
154
|
+
):
|
|
155
|
+
return ObligationStatus.STALE
|
|
156
|
+
return ObligationStatus.PENDING
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
# Section-size policy — the bloat rung (the `retention.should_compact` shape).
|
|
161
|
+
# ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@dataclass(frozen=True)
|
|
165
|
+
class StateFilePolicy:
|
|
166
|
+
"""Per-file health caps, as immutable data (the `RetentionPolicy` shape).
|
|
167
|
+
|
|
168
|
+
Every field optional-with-a-default; a host overrides what it cares about.
|
|
169
|
+
``None`` on any cap means "unbounded on this axis" — NOT zero. The caps are
|
|
170
|
+
size tuning; like `RetentionPolicy`, this object carries only thresholds — it
|
|
171
|
+
does not delete anything (a driver does, behind the verdict).
|
|
172
|
+
|
|
173
|
+
* ``max_total_bytes`` — flag the file when its on-disk size exceeds this.
|
|
174
|
+
* ``cold_section_max_rows`` — per-section row cap for the *cold* sections
|
|
175
|
+
(the completed/abandoned/archive logs a reader windows past). A section
|
|
176
|
+
over this is a COMPACTABLE finding.
|
|
177
|
+
* ``cold_sections`` — the names of the sections to apply ``cold_section_max_rows``
|
|
178
|
+
to. Empty ⇒ the size rung only looks at ``max_total_bytes``.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
max_total_bytes: int | None = 200_000
|
|
182
|
+
cold_section_max_rows: int | None = 150
|
|
183
|
+
cold_sections: tuple[str, ...] = ()
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
GENERIC_STATE_FILE_POLICY = StateFilePolicy()
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class SizeVerdict(str, enum.Enum):
|
|
190
|
+
"""Whether a state file is within its size budget."""
|
|
191
|
+
|
|
192
|
+
OK = "OK" # within all size caps
|
|
193
|
+
COMPACTABLE = "COMPACTABLE" # over a cap — worth compacting (the should_compact "True")
|
|
194
|
+
|
|
195
|
+
def __str__(self) -> str: # pragma: no cover - trivial
|
|
196
|
+
return self.value
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# ---------------------------------------------------------------------------
|
|
200
|
+
# Legacy-schema rung — retired field names / enum values still present.
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@dataclass(frozen=True)
|
|
205
|
+
class LegacySchemaFinding:
|
|
206
|
+
"""One retired field name or enum value still present in the file.
|
|
207
|
+
|
|
208
|
+
Generic over what "legacy" means for a given host: the caller declares the
|
|
209
|
+
retired tokens it scanned for and how many it found. Carried as data so the
|
|
210
|
+
surfaced line is legible ("9 entries still on retired `status: KEEP` — renamed
|
|
211
|
+
to ACTIVE/MAINTENANCE/PARK/TOMB").
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
token: str # the retired token, e.g. "status: KEEP" or "slot:"
|
|
215
|
+
count: int # how many occurrences the caller found
|
|
216
|
+
replacement: str = "" # the new form, for the surfaced line
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ---------------------------------------------------------------------------
|
|
220
|
+
# The composite health verdict.
|
|
221
|
+
# ---------------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@dataclass(frozen=True)
|
|
225
|
+
class StateHealthVerdict:
|
|
226
|
+
"""The typed result of `classify_state_file` — every rung's finding, foldable.
|
|
227
|
+
|
|
228
|
+
`findings()` renders the human/`--check` lines (the `config_lint.lint`
|
|
229
|
+
shape: a list of strings a `dos doctor --check` appends). `is_healthy` is the
|
|
230
|
+
one-bit rollup. `to_dict` is the `--json` shape.
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
size: SizeVerdict
|
|
234
|
+
total_bytes: int | None
|
|
235
|
+
oversized_sections: tuple[tuple[str, int], ...] # (section, rows) over the cold cap
|
|
236
|
+
legacy: tuple[LegacySchemaFinding, ...]
|
|
237
|
+
obligations: tuple[tuple[Obligation, ObligationStatus], ...]
|
|
238
|
+
|
|
239
|
+
@property
|
|
240
|
+
def is_healthy(self) -> bool:
|
|
241
|
+
"""True iff no rung has a finding worth surfacing."""
|
|
242
|
+
return (
|
|
243
|
+
self.size is SizeVerdict.OK
|
|
244
|
+
and not self.oversized_sections
|
|
245
|
+
and not self.legacy
|
|
246
|
+
and not any(st.needs_attention for _, st in self.obligations)
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
@property
|
|
250
|
+
def stale_obligations(self) -> tuple[Obligation, ...]:
|
|
251
|
+
"""The obligations past their horizon — the escalation set (the docs/133 point)."""
|
|
252
|
+
return tuple(ob for ob, st in self.obligations if st is ObligationStatus.STALE)
|
|
253
|
+
|
|
254
|
+
def findings(self) -> list[str]:
|
|
255
|
+
"""The surfaced lines — what `dos doctor --check` appends. PURE, deterministic.
|
|
256
|
+
|
|
257
|
+
Ordered most-actionable first: stale obligations, then blocked/pending
|
|
258
|
+
obligations, then bloat, then legacy schema. Empty ⇒ the file is healthy.
|
|
259
|
+
"""
|
|
260
|
+
out: list[str] = []
|
|
261
|
+
# Obligations first — a forgotten mid-flight transition is the highest-signal.
|
|
262
|
+
for ob, st in self.obligations:
|
|
263
|
+
if st is ObligationStatus.STALE:
|
|
264
|
+
pred = f" — owed: {ob.predicate_summary}" if ob.predicate_summary else ""
|
|
265
|
+
out.append(
|
|
266
|
+
f"obligation '{ob.key}' is STALE (deferred past its "
|
|
267
|
+
f"{ob.horizon_days:g}-day horizon){pred}: {ob.description}"
|
|
268
|
+
)
|
|
269
|
+
for ob, st in self.obligations:
|
|
270
|
+
if st is ObligationStatus.BLOCKED:
|
|
271
|
+
out.append(f"obligation '{ob.key}' is BLOCKED (a precondition is unmet): {ob.description}")
|
|
272
|
+
for ob, st in self.obligations:
|
|
273
|
+
if st is ObligationStatus.PENDING:
|
|
274
|
+
pred = f" — owed: {ob.predicate_summary}" if ob.predicate_summary else ""
|
|
275
|
+
out.append(f"obligation '{ob.key}' is PENDING{pred}: {ob.description}")
|
|
276
|
+
# Bloat.
|
|
277
|
+
if self.size is SizeVerdict.COMPACTABLE and self.total_bytes is not None:
|
|
278
|
+
out.append(
|
|
279
|
+
f"state file is {self.total_bytes:,} bytes — over the size budget; "
|
|
280
|
+
f"compact the cold sections"
|
|
281
|
+
)
|
|
282
|
+
for section, rows in self.oversized_sections:
|
|
283
|
+
out.append(f"cold section '{section}' has {rows} rows — over the per-section cap")
|
|
284
|
+
# Legacy schema.
|
|
285
|
+
for lf in self.legacy:
|
|
286
|
+
repl = f" (→ {lf.replacement})" if lf.replacement else ""
|
|
287
|
+
out.append(f"{lf.count} entries still use retired '{lf.token}'{repl}")
|
|
288
|
+
return out
|
|
289
|
+
|
|
290
|
+
def to_dict(self) -> dict:
|
|
291
|
+
return {
|
|
292
|
+
"size": self.size.value,
|
|
293
|
+
"total_bytes": self.total_bytes,
|
|
294
|
+
"oversized_sections": [list(t) for t in self.oversized_sections],
|
|
295
|
+
"legacy": [
|
|
296
|
+
{"token": lf.token, "count": lf.count, "replacement": lf.replacement}
|
|
297
|
+
for lf in self.legacy
|
|
298
|
+
],
|
|
299
|
+
"obligations": [
|
|
300
|
+
{**_obligation_to_dict(ob), "status": st.value}
|
|
301
|
+
for ob, st in self.obligations
|
|
302
|
+
],
|
|
303
|
+
"is_healthy": self.is_healthy,
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _obligation_to_dict(ob: Obligation) -> dict:
|
|
308
|
+
return {
|
|
309
|
+
"key": ob.key,
|
|
310
|
+
"description": ob.description,
|
|
311
|
+
"predicate_summary": ob.predicate_summary,
|
|
312
|
+
"satisfied": ob.satisfied,
|
|
313
|
+
"blocked": ob.blocked,
|
|
314
|
+
"horizon_days": ob.horizon_days,
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@dataclass(frozen=True)
|
|
319
|
+
class StateFileEvidence:
|
|
320
|
+
"""What the boundary gathers about a state file — caller-supplied, pure to fold.
|
|
321
|
+
|
|
322
|
+
The host adapter opens the file (the only I/O), measures it, and evaluates each
|
|
323
|
+
obligation's predicate against the live world, then hands this in. The fold
|
|
324
|
+
below never touches disk.
|
|
325
|
+
|
|
326
|
+
* ``total_bytes`` — the file's on-disk size, or None if unknown.
|
|
327
|
+
* ``section_rows`` — ``{section_name: row_count}`` for the sections the caller
|
|
328
|
+
cares about (it only needs to fill the cold ones the policy names).
|
|
329
|
+
* ``legacy`` — the retired-token findings the caller scanned for (already
|
|
330
|
+
counted; an empty/zero-count finding is dropped by the fold).
|
|
331
|
+
* ``obligations`` — the deferred obligations, predicates already evaluated.
|
|
332
|
+
"""
|
|
333
|
+
|
|
334
|
+
total_bytes: int | None = None
|
|
335
|
+
section_rows: Mapping[str, int] = field(default_factory=dict)
|
|
336
|
+
legacy: Sequence[LegacySchemaFinding] = ()
|
|
337
|
+
obligations: Sequence[Obligation] = ()
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def classify_state_file(
|
|
341
|
+
evidence: StateFileEvidence,
|
|
342
|
+
policy: StateFilePolicy = GENERIC_STATE_FILE_POLICY,
|
|
343
|
+
*,
|
|
344
|
+
now_ms: int,
|
|
345
|
+
) -> StateHealthVerdict:
|
|
346
|
+
"""Classify one external state file's health. PURE — no I/O, clock handed in.
|
|
347
|
+
|
|
348
|
+
Folds the caller-gathered ``evidence`` against ``policy`` into a
|
|
349
|
+
`StateHealthVerdict`. The size rung is monotone (it only ever asks to compact
|
|
350
|
+
*more* as the file grows — `retention.should_compact`'s posture). The
|
|
351
|
+
obligation rung re-states each pre-evaluated obligation via `classify_obligation`.
|
|
352
|
+
The legacy rung drops zero-count findings (a token scanned-for but absent is not
|
|
353
|
+
a finding). All deterministic and order-stable so `--check` output is stable.
|
|
354
|
+
"""
|
|
355
|
+
total = evidence.total_bytes
|
|
356
|
+
|
|
357
|
+
# Size rung — total bytes over cap, OR any named cold section over its row cap.
|
|
358
|
+
over_total = (
|
|
359
|
+
policy.max_total_bytes is not None
|
|
360
|
+
and total is not None
|
|
361
|
+
and total > policy.max_total_bytes
|
|
362
|
+
)
|
|
363
|
+
oversized: list[tuple[str, int]] = []
|
|
364
|
+
if policy.cold_section_max_rows is not None:
|
|
365
|
+
for section in policy.cold_sections:
|
|
366
|
+
rows = evidence.section_rows.get(section)
|
|
367
|
+
if rows is not None and rows > policy.cold_section_max_rows:
|
|
368
|
+
oversized.append((section, rows))
|
|
369
|
+
size_verdict = (
|
|
370
|
+
SizeVerdict.COMPACTABLE if (over_total or oversized) else SizeVerdict.OK
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
# Legacy rung — keep only present (count > 0) findings.
|
|
374
|
+
legacy = tuple(lf for lf in evidence.legacy if lf.count > 0)
|
|
375
|
+
|
|
376
|
+
# Obligation rung — adjudicate each against the handed-in clock.
|
|
377
|
+
obligations = tuple(
|
|
378
|
+
(ob, classify_obligation(ob, now_ms=now_ms)) for ob in evidence.obligations
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
return StateHealthVerdict(
|
|
382
|
+
size=size_verdict,
|
|
383
|
+
total_bytes=total,
|
|
384
|
+
oversized_sections=tuple(oversized),
|
|
385
|
+
legacy=legacy,
|
|
386
|
+
obligations=obligations,
|
|
387
|
+
)
|
dos/status.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""The status digest — one fail-closed, folded fact about a run (docs/120).
|
|
2
|
+
|
|
3
|
+
Layer-1 projection: folds four shipped verdicts (liveness / ledger-verified /
|
|
4
|
+
held lease / resume) into one record. PURE — the four verdicts are computed at
|
|
5
|
+
the caller boundary and handed in (the `arbitrate`/`classify` rule: state in,
|
|
6
|
+
verdict out, no I/O). The record carries **no `claimed` field** by construction:
|
|
7
|
+
a consumer of the digest cannot read a self-report, because the output type has
|
|
8
|
+
no slot for one (docs/120 §3, the fail-closed invariant).
|
|
9
|
+
|
|
10
|
+
This is Phase 1 of docs/120: the pure fold only. The CLI boundary that *gathers*
|
|
11
|
+
the four verdicts (`dos status <run_id>`) is Phase 2; the `dos_status` MCP tool is
|
|
12
|
+
Phase 3. Nothing imports this module yet — it is self-contained and cannot affect
|
|
13
|
+
the running kernel.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
|
|
19
|
+
from dos.intent_ledger import LedgerState
|
|
20
|
+
from dos.liveness import LivenessVerdict
|
|
21
|
+
from dos.resume import ResumePlan
|
|
22
|
+
|
|
23
|
+
# The durable_schema floor (docs/116 §6): the digest is a record other tools read,
|
|
24
|
+
# so tag it. A consumer that understands an older schema refuses a newer digest
|
|
25
|
+
# rather than misparsing it — refuse, don't guess.
|
|
26
|
+
STATUS_DIGEST_SCHEMA = 1
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class ProgressView:
|
|
31
|
+
"""The adjudicated progress view — derived from `LedgerState.verified` ONLY.
|
|
32
|
+
|
|
33
|
+
`verified_count` / `declared_count` / `verified_steps` are read off the
|
|
34
|
+
kernel-minted `verified` map (the TRUSTED rung). There is deliberately no
|
|
35
|
+
`claimed` field here: the agent's self-report (`LedgerState.claimed`, a pointer
|
|
36
|
+
to a commit to check, not proof) is never surfaced through the digest.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
verified_count: int
|
|
40
|
+
declared_count: int
|
|
41
|
+
verified_steps: tuple[str, ...] = ()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class StatusDigest:
|
|
46
|
+
"""One run's status as a single folded, fail-closed fact (docs/120 §5).
|
|
47
|
+
|
|
48
|
+
Every field is an *adjudicated* verdict, never a self-report:
|
|
49
|
+
liveness — the in-flight "is it moving" verdict (`liveness.classify`)
|
|
50
|
+
progress — verified-step view (reads `verified`, never `claimed`)
|
|
51
|
+
region — the run's held lease globs (or () if it holds none)
|
|
52
|
+
resume — the resume verdict once the run has stopped (None while live)
|
|
53
|
+
|
|
54
|
+
`to_dict` is the `--json` A2A contract a peer agent / dashboard parses. The
|
|
55
|
+
one load-bearing property of that shape: `claimed` is absent.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
run_id: str
|
|
59
|
+
liveness: LivenessVerdict
|
|
60
|
+
progress: ProgressView
|
|
61
|
+
region: tuple[str, ...] = ()
|
|
62
|
+
resume: ResumePlan | None = None
|
|
63
|
+
schema: int = STATUS_DIGEST_SCHEMA
|
|
64
|
+
|
|
65
|
+
def to_dict(self) -> dict:
|
|
66
|
+
# The --json shape. `claimed` is ABSENT — that absence is the point
|
|
67
|
+
# (docs/120 §3): a consumer cannot pick a self-report it is never handed.
|
|
68
|
+
return {
|
|
69
|
+
"schema": self.schema,
|
|
70
|
+
"run_id": self.run_id,
|
|
71
|
+
"liveness": self.liveness.to_dict(),
|
|
72
|
+
"progress": {
|
|
73
|
+
"verified_count": self.progress.verified_count,
|
|
74
|
+
"declared_count": self.progress.declared_count,
|
|
75
|
+
"verified_steps": list(self.progress.verified_steps),
|
|
76
|
+
},
|
|
77
|
+
"region": list(self.region),
|
|
78
|
+
"resume": self.resume.to_dict() if self.resume is not None else None,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def status_digest(
|
|
83
|
+
*,
|
|
84
|
+
run_id: str,
|
|
85
|
+
ledger_state: LedgerState,
|
|
86
|
+
liveness_verdict: LivenessVerdict,
|
|
87
|
+
live_region: tuple[str, ...] = (),
|
|
88
|
+
resume_plan: ResumePlan | None = None,
|
|
89
|
+
) -> StatusDigest:
|
|
90
|
+
"""Fold the four already-computed verdicts into one digest. PURE.
|
|
91
|
+
|
|
92
|
+
The four inputs are gathered at the caller boundary (Phase 2) and handed in;
|
|
93
|
+
this function makes no subprocess, file, or clock call — the same posture as
|
|
94
|
+
`resume.resume_plan` and `liveness.classify`.
|
|
95
|
+
|
|
96
|
+
`progress` is built from `ledger_state.verified` ONLY — `ledger_state.claimed`
|
|
97
|
+
is read by nothing here. Fail-closed: an empty / no-intent `LedgerState` yields
|
|
98
|
+
a zero `ProgressView` (declared 0, verified 0), never a raise and never a
|
|
99
|
+
guessed-optimistic default. A run that declared no adjudicable intent is still
|
|
100
|
+
a valid fact ("nothing verified, nothing declared"), not an error.
|
|
101
|
+
"""
|
|
102
|
+
verified = ledger_state.verified # {step_id: VerifiedStep} — the minted rung
|
|
103
|
+
progress = ProgressView(
|
|
104
|
+
verified_count=len(verified),
|
|
105
|
+
declared_count=len(ledger_state.declared_steps),
|
|
106
|
+
verified_steps=tuple(sorted(verified)),
|
|
107
|
+
)
|
|
108
|
+
return StatusDigest(
|
|
109
|
+
run_id=run_id,
|
|
110
|
+
liveness=liveness_verdict,
|
|
111
|
+
progress=progress,
|
|
112
|
+
region=live_region,
|
|
113
|
+
resume=resume_plan,
|
|
114
|
+
)
|