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/dispatch_top.py
ADDED
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
"""`dos top` — `top(1)` for a DOS fleet: who holds which lane, what just shipped, what's stuck.
|
|
2
|
+
|
|
3
|
+
The **live-ops** sibling of `dos decisions`. Where the decisions queue answers
|
|
4
|
+
"what is waiting on *me* right now," `dos top` answers "what is *running* right
|
|
5
|
+
now": one near-real-time screen of the lanes, the leases holding them, the recent
|
|
6
|
+
verdicts with a ship-oracle trust cross-check, and any lane that has stopped
|
|
7
|
+
moving. It is the screen an operator leaves open in a side terminal during a
|
|
8
|
+
fleet run — the fleet watchdog the closed-loop-control thesis wants a host to be
|
|
9
|
+
able to build, here as kernel-generic mechanism.
|
|
10
|
+
|
|
11
|
+
It is a **read-only projection** (the `decisions.py` discipline, restated for the
|
|
12
|
+
live axis): it stores nothing, mutates nothing, acquires no lease, launches no
|
|
13
|
+
agent. Every panel is a pure function over an in-memory payload; the only I/O is
|
|
14
|
+
`snapshot()` at the boundary, which reads four already-persisted sources and
|
|
15
|
+
freezes them. Delete this module and you lose the screen, not any data.
|
|
16
|
+
|
|
17
|
+
**Why it is kernel-generic (works in a random new repo).** job's `dispatch_top`
|
|
18
|
+
read its lease world from `fanout_state.py` + `execution-state.yaml` — host
|
|
19
|
+
workflow the kernel is fenced from. This reads the kernel's *own* lease world
|
|
20
|
+
instead:
|
|
21
|
+
|
|
22
|
+
lanes <- config.lanes (the generic `main`/`global` default;
|
|
23
|
+
a workspace's `dos.toml [lanes]` wins)
|
|
24
|
+
leases <- lane_journal.replay(...) (the WAL folded to the live-lease set —
|
|
25
|
+
the same rows execution-state.yaml held)
|
|
26
|
+
liveness <- liveness.classify(...) (per-lane ADVANCING/SPINNING/STALLED,
|
|
27
|
+
the kernel verdict, not a host health)
|
|
28
|
+
verdicts <- .verdict-*.json (recent no-pick/ship envelopes)
|
|
29
|
+
activity <- git_delta.recent_commits (so a zero-lease repo still has content)
|
|
30
|
+
|
|
31
|
+
Nothing here imports a host. In a freshly-`dos init`'d checkout there are no
|
|
32
|
+
leases and no verdicts yet — every reader returns empty and the screen shows the
|
|
33
|
+
lane roster (all FREE) plus the git-activity strip. That is the headline
|
|
34
|
+
contract, pinned in `tests/test_dispatch_top.py`: `snapshot()` against a plain
|
|
35
|
+
git repo with no `dos.toml`, no journal, no plan returns a renderable frame.
|
|
36
|
+
|
|
37
|
+
The rich live-redraw skin + the poll loop live in `dispatch_top_tui` (behind the
|
|
38
|
+
`[tui]` extra); this module is import-light and dependency-free so the plain-text
|
|
39
|
+
renderers are always available — the floor that works everywhere, exactly the
|
|
40
|
+
`decisions` / `decisions_tui` split.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
from __future__ import annotations
|
|
44
|
+
|
|
45
|
+
import datetime as dt
|
|
46
|
+
import io
|
|
47
|
+
import json
|
|
48
|
+
import sys
|
|
49
|
+
from dataclasses import dataclass
|
|
50
|
+
|
|
51
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
52
|
+
try:
|
|
53
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
|
|
54
|
+
except Exception: # pragma: no cover
|
|
55
|
+
pass
|
|
56
|
+
elif not isinstance(sys.stdout, io.TextIOWrapper): # pragma: no cover
|
|
57
|
+
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
|
|
58
|
+
|
|
59
|
+
from dos import config as _config
|
|
60
|
+
from dos import git_delta
|
|
61
|
+
from dos import lane_journal
|
|
62
|
+
from dos import liveness as _liveness
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
# Status chips — the per-lane verdict, collapsed to one glyph the operator reads
|
|
67
|
+
# at a glance. A held lane takes its chip from the kernel liveness verdict; a
|
|
68
|
+
# lane with no lease is FREE. This is the kernel-honest upgrade over job's
|
|
69
|
+
# dispatch-top, which read STALLED/ORPHANED_WORKING/DEAD from fanout_state's
|
|
70
|
+
# audit: here the chip IS `liveness.classify`, the 4th distrust syscall.
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
CHIP_ADVANCING = "🟢 ADVANCING" # held + liveness says ground-truth state moved
|
|
74
|
+
CHIP_SPINNING = "🟡 SPINNING" # held + alive but not moving (burning tokens)
|
|
75
|
+
CHIP_STALLED = "🔴 STALLED" # held + no fresh heartbeat / no commits — dead/hung
|
|
76
|
+
CHIP_SPAWNING = "🔵 SPAWNING" # a run is COMING — a recent OP_SPAWN, no lease yet
|
|
77
|
+
CHIP_FREE = "⚪ FREE" # no lease on this lane
|
|
78
|
+
|
|
79
|
+
# liveness verdict -> chip. One home for the mapping so a new Liveness value
|
|
80
|
+
# surfaces here as a KeyError in tests rather than silently rendering blank.
|
|
81
|
+
_CHIP_BY_LIVENESS = {
|
|
82
|
+
_liveness.Liveness.ADVANCING: CHIP_ADVANCING,
|
|
83
|
+
_liveness.Liveness.SPINNING: CHIP_SPINNING,
|
|
84
|
+
_liveness.Liveness.STALLED: CHIP_STALLED,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# How long a journaled OP_SPAWN keeps a lane reading SPAWNING before it ages out.
|
|
88
|
+
# A loop normally goes SPAWN→preflight→ACQUIRE in seconds; once the ACQUIRE lands a
|
|
89
|
+
# held lease WINS the chip (the spawning fold is no-live-lease-only). The TTL is the
|
|
90
|
+
# self-heal for the OTHER case — a launch that DIES in preflight, which never
|
|
91
|
+
# acquires: its SPAWN ages out on its own rather than wedging a phantom SPAWNING
|
|
92
|
+
# forever (the same self-heal `lane_lease._expire_dead` gives a crashed *holder*,
|
|
93
|
+
# here for a never-born one). 120s is generous for any real preflight while still
|
|
94
|
+
# clearing a dead launch within a couple of `dos top` polls.
|
|
95
|
+
SPAWN_TTL_MS = 120_000
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
# Time helpers (mirrors decisions.py — same tolerant ISO parse + compact age).
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _now() -> dt.datetime:
|
|
104
|
+
return dt.datetime.now(dt.timezone.utc)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _parse_iso(ts: str | None) -> dt.datetime | None:
|
|
108
|
+
if not ts:
|
|
109
|
+
return None
|
|
110
|
+
try:
|
|
111
|
+
return dt.datetime.fromisoformat(str(ts).replace("Z", "+00:00"))
|
|
112
|
+
except (ValueError, TypeError):
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _age_ms(ts: str | None, *, now: dt.datetime) -> int | None:
|
|
117
|
+
"""Age of an ISO stamp in milliseconds as of ``now`` (None if unparseable)."""
|
|
118
|
+
t = _parse_iso(ts)
|
|
119
|
+
if t is None:
|
|
120
|
+
return None
|
|
121
|
+
if t.tzinfo is None:
|
|
122
|
+
t = t.replace(tzinfo=dt.timezone.utc)
|
|
123
|
+
return max(0, int((now - t).total_seconds() * 1000))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _fmt_age(age_ms: int | None) -> str:
|
|
127
|
+
"""Compact age from milliseconds: 45s / 18m / 2h / 3d / '—' when unknown."""
|
|
128
|
+
if age_ms is None:
|
|
129
|
+
return "—"
|
|
130
|
+
s = age_ms // 1000
|
|
131
|
+
if s < 60:
|
|
132
|
+
return f"{s}s"
|
|
133
|
+
if s < 3600:
|
|
134
|
+
return f"{s // 60}m"
|
|
135
|
+
if s < 86400:
|
|
136
|
+
return f"{s // 3600}h"
|
|
137
|
+
return f"{s // 86400}d"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
# Lane roster — derived from config.lanes, never hardcoded (the drift the job
|
|
142
|
+
# DTOP6 guard existed to catch). A held lane outside the roster is surfaced last
|
|
143
|
+
# so a live lease can never be invisible (job DTOP1's "unknown-held-never-
|
|
144
|
+
# invisible" rule, restated against the generic taxonomy).
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def lane_roster(config: _config.SubstrateConfig) -> list[str]:
|
|
149
|
+
"""The always-shown lane order: concurrent (cluster) lanes, then exclusive.
|
|
150
|
+
|
|
151
|
+
Deduped, declaration-order-preserving. For the generic default this is
|
|
152
|
+
``["main", "global"]``; a workspace's `dos.toml [lanes]` replaces it. Never
|
|
153
|
+
raises — an empty taxonomy yields ``[]`` and the screen renders "(no lanes)".
|
|
154
|
+
"""
|
|
155
|
+
seen: set[str] = set()
|
|
156
|
+
out: list[str] = []
|
|
157
|
+
for lane in tuple(config.lanes.concurrent) + tuple(config.lanes.exclusive):
|
|
158
|
+
if lane and lane not in seen:
|
|
159
|
+
seen.add(lane)
|
|
160
|
+
out.append(lane)
|
|
161
|
+
return out
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# ---------------------------------------------------------------------------
|
|
165
|
+
# Lane state model + the pure adapter from a live-lease payload.
|
|
166
|
+
# ---------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@dataclass(frozen=True)
|
|
170
|
+
class LaneState:
|
|
171
|
+
"""One rendered lane row — pure data, no rich objects."""
|
|
172
|
+
|
|
173
|
+
lane: str
|
|
174
|
+
chip: str # one of the CHIP_* constants
|
|
175
|
+
loop_ts: str = "" # holding loop/run ts, "" when FREE
|
|
176
|
+
holder: str = "" # host:pid of the holder, "" when FREE
|
|
177
|
+
heartbeat_age_ms: int | None = None
|
|
178
|
+
liveness_reason: str = "" # the liveness verdict's one-line reason
|
|
179
|
+
is_exclusive: bool = False # an exclusive lane (renders a marker)
|
|
180
|
+
|
|
181
|
+
def to_dict(self) -> dict:
|
|
182
|
+
return {
|
|
183
|
+
"lane": self.lane,
|
|
184
|
+
"chip": self.chip,
|
|
185
|
+
"loop_ts": self.loop_ts,
|
|
186
|
+
"holder": self.holder,
|
|
187
|
+
"heartbeat_age_ms": self.heartbeat_age_ms,
|
|
188
|
+
"liveness_reason": self.liveness_reason,
|
|
189
|
+
"is_exclusive": self.is_exclusive,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _lease_liveness(
|
|
194
|
+
lease: dict, *, events_since: int, now: dt.datetime, policy
|
|
195
|
+
) -> _liveness.LivenessVerdict:
|
|
196
|
+
"""Classify one held lease with the kernel liveness verdict.
|
|
197
|
+
|
|
198
|
+
The boundary builds `ProgressEvidence` from the lease row — the run-start is
|
|
199
|
+
its `acquired_at`, the heartbeat age is `now - heartbeat_at`, and
|
|
200
|
+
``events_since`` is the count of state-mutating lane-journal events recorded
|
|
201
|
+
*strictly after* this lease was acquired (its own ACQUIRE is the anchor, NOT
|
|
202
|
+
progress — a lease that has only sat there since acquire has 0 events-since)
|
|
203
|
+
— and hands it to the PURE `liveness.classify`. `commits_since_start` is 0
|
|
204
|
+
here: a lease records no start SHA, so `dos top`'s Phase-1 liveness rung is
|
|
205
|
+
the heartbeat/event signal (the honest floor LVN Phase 1 set; a SHA-anchored
|
|
206
|
+
commit rung is a later enrichment, not needed for the watchdog screen). The
|
|
207
|
+
clock is injected (`now_ms`), never read inside the verdict — the arbiter
|
|
208
|
+
discipline.
|
|
209
|
+
|
|
210
|
+
Consequence (the bug the first smoke-test caught): an idle just-acquired lease
|
|
211
|
+
with a stale/absent heartbeat now correctly reads STALLED, not ADVANCING — its
|
|
212
|
+
lone ACQUIRE no longer counts as forward motion.
|
|
213
|
+
"""
|
|
214
|
+
now_ms = int(now.timestamp() * 1000)
|
|
215
|
+
started_ms = _age_ms(lease.get("acquired_at"), now=now)
|
|
216
|
+
run_started_ms = (now_ms - started_ms) if started_ms is not None else now_ms
|
|
217
|
+
hb_age = _age_ms(lease.get("heartbeat_at") or lease.get("acquired_at"), now=now)
|
|
218
|
+
ev = _liveness.ProgressEvidence(
|
|
219
|
+
run_started_ms=run_started_ms,
|
|
220
|
+
now_ms=now_ms,
|
|
221
|
+
commits_since_start=0,
|
|
222
|
+
journal_events_since=max(0, events_since),
|
|
223
|
+
last_heartbeat_age_ms=hb_age,
|
|
224
|
+
)
|
|
225
|
+
return _liveness.classify(ev, policy)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def build_lane_states(
|
|
229
|
+
payload: dict,
|
|
230
|
+
*,
|
|
231
|
+
roster: list[str],
|
|
232
|
+
exclusive: tuple[str, ...] = (),
|
|
233
|
+
now: dt.datetime | None = None,
|
|
234
|
+
policy=None,
|
|
235
|
+
) -> list[LaneState]:
|
|
236
|
+
"""Pure adapter: (live-lease payload, roster) → ordered LaneState rows.
|
|
237
|
+
|
|
238
|
+
``payload`` is ``{"leases": [...], "events_by_lane": {lane: count},
|
|
239
|
+
"spawning_by_lane": {lane: SpawnIntent}}`` — the shape `snapshot()` builds from
|
|
240
|
+
`lane_journal.replay` + the journal folds. Every lane in ``roster`` appears
|
|
241
|
+
exactly once; any *held* lane not in ``roster`` is appended last so a live lease
|
|
242
|
+
is never invisible. A lane's chip is: the kernel liveness verdict when held; else
|
|
243
|
+
SPAWNING when a recent OP_SPAWN says a run is coming (the SPAWN→ACQUIRE window);
|
|
244
|
+
else FREE. The clock is passed in (pure given ``now``).
|
|
245
|
+
"""
|
|
246
|
+
now = now or _now()
|
|
247
|
+
policy = policy if policy is not None else _liveness.DEFAULT_POLICY
|
|
248
|
+
leases_by_lane = {str(l.get("lane") or ""): l for l in payload.get("leases", [])}
|
|
249
|
+
events_by_lane = payload.get("events_by_lane", {}) or {}
|
|
250
|
+
spawning_by_lane = payload.get("spawning_by_lane", {}) or {}
|
|
251
|
+
|
|
252
|
+
def _state(lane: str, lease: dict | None) -> LaneState:
|
|
253
|
+
excl = lane in exclusive
|
|
254
|
+
if lease is None:
|
|
255
|
+
# No live lease — but a recent OP_SPAWN means a run is COMING to this
|
|
256
|
+
# lane (the blind SPAWN→ACQUIRE window). Surface it as SPAWNING so the
|
|
257
|
+
# loop is visible the instant it commits to a lane, not only once it has
|
|
258
|
+
# durably acquired. A held lease (above) always wins; a stale SPAWN has
|
|
259
|
+
# already aged out of `spawning_by_lane`.
|
|
260
|
+
intent = spawning_by_lane.get(lane)
|
|
261
|
+
if intent is not None:
|
|
262
|
+
return LaneState(
|
|
263
|
+
lane=lane,
|
|
264
|
+
chip=CHIP_SPAWNING,
|
|
265
|
+
holder=str(getattr(intent, "holder", "") or ""),
|
|
266
|
+
heartbeat_age_ms=getattr(intent, "age_ms", None),
|
|
267
|
+
liveness_reason="a run is spawning — no lease yet",
|
|
268
|
+
is_exclusive=excl,
|
|
269
|
+
)
|
|
270
|
+
return LaneState(lane=lane, chip=CHIP_FREE, is_exclusive=excl)
|
|
271
|
+
verdict = _lease_liveness(
|
|
272
|
+
lease,
|
|
273
|
+
events_since=int(events_by_lane.get(lane, 0) or 0),
|
|
274
|
+
now=now,
|
|
275
|
+
policy=policy,
|
|
276
|
+
)
|
|
277
|
+
return LaneState(
|
|
278
|
+
lane=lane,
|
|
279
|
+
chip=_CHIP_BY_LIVENESS[verdict.verdict],
|
|
280
|
+
loop_ts=str(lease.get("loop_ts") or ""),
|
|
281
|
+
holder=str(lease.get("holder") or ""),
|
|
282
|
+
heartbeat_age_ms=verdict.evidence.last_heartbeat_age_ms,
|
|
283
|
+
liveness_reason=verdict.reason,
|
|
284
|
+
is_exclusive=excl,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
states: list[LaneState] = []
|
|
288
|
+
seen: set[str] = set()
|
|
289
|
+
for lane in roster:
|
|
290
|
+
seen.add(lane)
|
|
291
|
+
states.append(_state(lane, leases_by_lane.get(lane)))
|
|
292
|
+
for lane, lease in leases_by_lane.items():
|
|
293
|
+
if lane and lane not in seen:
|
|
294
|
+
seen.add(lane)
|
|
295
|
+
states.append(_state(lane, lease))
|
|
296
|
+
# A SPAWNING lane outside the roster (a launcher committed to a lane the
|
|
297
|
+
# workspace taxonomy doesn't name) must also never be invisible — the same
|
|
298
|
+
# rule that surfaces an unknown HELD lane, applied to an unknown COMING one.
|
|
299
|
+
for lane in spawning_by_lane:
|
|
300
|
+
if lane and lane not in seen:
|
|
301
|
+
seen.add(lane)
|
|
302
|
+
states.append(_state(lane, None))
|
|
303
|
+
return states
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# ---------------------------------------------------------------------------
|
|
307
|
+
# Recent verdicts — the .verdict-*.json envelopes, newest-first. dos top shows
|
|
308
|
+
# ALL recent verdicts (a ship/accept as well as a wedge), unlike the decisions
|
|
309
|
+
# queue which keeps only the refusal-shaped ones; the trust column cross-checks a
|
|
310
|
+
# claimed ship against the oracle (evidence-over-narrative as a UI affordance).
|
|
311
|
+
# ---------------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
TRUST_OK = "✓oracle" # the oracle confirms the claimed pick shipped
|
|
314
|
+
TRUST_PENDING = "·pending" # an accept/launchable verdict the oracle hasn't seen ship
|
|
315
|
+
TRUST_NA = "—" # nothing to verify (a no-pick wedge/drain)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@dataclass(frozen=True)
|
|
319
|
+
class VerdictRow:
|
|
320
|
+
"""One recent verdict envelope, normalized to a display row."""
|
|
321
|
+
|
|
322
|
+
tag: str
|
|
323
|
+
lane: str
|
|
324
|
+
verdict: str # ACCEPT | WEDGE | DRAIN | … (envelope's own token)
|
|
325
|
+
reason_token: str = "" # closed reason_class when present
|
|
326
|
+
pick: str = "" # "PLAN PHASE" of the lead pick, when present
|
|
327
|
+
trust: str = TRUST_NA
|
|
328
|
+
age_ms: int | None = None
|
|
329
|
+
|
|
330
|
+
def to_dict(self) -> dict:
|
|
331
|
+
return {
|
|
332
|
+
"tag": self.tag, "lane": self.lane, "verdict": self.verdict,
|
|
333
|
+
"reason_token": self.reason_token, "pick": self.pick,
|
|
334
|
+
"trust": self.trust, "age_ms": self.age_ms,
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _envelope_lane(env: dict) -> str:
|
|
339
|
+
# Normalize to the bare dynamic lane handle (dos/119) so a curated-cluster
|
|
340
|
+
# relic scope ("apply cluster (AFR, …)") renders as its handle, identically to
|
|
341
|
+
# the operator-decision queue (`decisions._dynamic_lane_handle`) — one
|
|
342
|
+
# normalizer, two readers can't drift.
|
|
343
|
+
from dos.decisions import _dynamic_lane_handle
|
|
344
|
+
scope = env.get("scope")
|
|
345
|
+
if isinstance(scope, dict):
|
|
346
|
+
return _dynamic_lane_handle(str(scope.get("lane") or scope.get("label") or ""))
|
|
347
|
+
if isinstance(scope, str):
|
|
348
|
+
return _dynamic_lane_handle(scope)
|
|
349
|
+
return _dynamic_lane_handle(str(env.get("lane") or ""))
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _envelope_lead_pick(env: dict) -> str:
|
|
353
|
+
for key in ("picks", "intended_picks"):
|
|
354
|
+
for p in env.get(key, []) or []:
|
|
355
|
+
if isinstance(p, dict):
|
|
356
|
+
plan = str(p.get("plan_id") or "").strip()
|
|
357
|
+
phase = str(p.get("phase_id") or "").strip()
|
|
358
|
+
if plan:
|
|
359
|
+
return f"{plan} {phase}".strip()
|
|
360
|
+
return ""
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def parse_verdict_envelope(env: dict, tag: str, *, now: dt.datetime) -> VerdictRow:
|
|
364
|
+
"""Pure: one parsed `.verdict-<tag>.json` dict → a VerdictRow (no trust yet).
|
|
365
|
+
|
|
366
|
+
Handles both shapes the spine writes: the clean ACCEPT envelope
|
|
367
|
+
(``all_clear``/``picks``, no explicit ``verdict``) and the WEDGE envelope
|
|
368
|
+
(``verdict``/``reason_class``/``intended_picks``). Trust is attached
|
|
369
|
+
separately (`attach_trust`) so this stays pure and the oracle call is an
|
|
370
|
+
injected boundary, exactly as job's DTOP2 kept `attach_trust` over an injected
|
|
371
|
+
`verify`.
|
|
372
|
+
"""
|
|
373
|
+
verdict = str(env.get("verdict") or "").strip().upper()
|
|
374
|
+
if not verdict:
|
|
375
|
+
if env.get("all_clear") and not env.get("blocked"):
|
|
376
|
+
verdict = "ACCEPT"
|
|
377
|
+
elif env.get("blocked"):
|
|
378
|
+
verdict = "WEDGE"
|
|
379
|
+
else:
|
|
380
|
+
verdict = "UNKNOWN"
|
|
381
|
+
return VerdictRow(
|
|
382
|
+
tag=tag,
|
|
383
|
+
lane=_envelope_lane(env),
|
|
384
|
+
verdict=verdict,
|
|
385
|
+
reason_token=str(env.get("reason_class") or "").strip().upper(),
|
|
386
|
+
pick=_envelope_lead_pick(env),
|
|
387
|
+
age_ms=_age_ms(env.get("generated_at") or env.get("ts"), now=now),
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def attach_trust(row: VerdictRow, verify) -> VerdictRow:
|
|
392
|
+
"""Attach the ship-oracle trust chip to a verdict row over an injected ``verify``.
|
|
393
|
+
|
|
394
|
+
``verify`` is a ``(plan, phase) -> bool`` shipped-check (the live path wires
|
|
395
|
+
`oracle.is_shipped`; tests inject a fake). A verdict with no pick has nothing
|
|
396
|
+
to verify (TRUST_NA). A launchable/accept verdict whose pick the oracle has
|
|
397
|
+
not yet seen ship reads ·pending (informational, NOT a false-ship warn — the
|
|
398
|
+
correction job's DTOP2 made: an ACCEPT is a go-ahead, not a ship claim). A
|
|
399
|
+
pick the oracle confirms reads ✓oracle. Never raises — a verify that throws
|
|
400
|
+
degrades the row to its current trust (fail-safe).
|
|
401
|
+
"""
|
|
402
|
+
if not row.pick or verify is None:
|
|
403
|
+
return row
|
|
404
|
+
parts = row.pick.split()
|
|
405
|
+
plan, phase = parts[0], (parts[1] if len(parts) > 1 else "")
|
|
406
|
+
try:
|
|
407
|
+
shipped = bool(verify(plan, phase))
|
|
408
|
+
except Exception:
|
|
409
|
+
return row
|
|
410
|
+
chip = TRUST_OK if shipped else TRUST_PENDING
|
|
411
|
+
return VerdictRow(
|
|
412
|
+
tag=row.tag, lane=row.lane, verdict=row.verdict,
|
|
413
|
+
reason_token=row.reason_token, pick=row.pick, trust=chip, age_ms=row.age_ms,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
# ---------------------------------------------------------------------------
|
|
418
|
+
# The frame — everything one screen shows, as pure data. `snapshot()` builds it
|
|
419
|
+
# from disk; the renderers + the TUI consume it.
|
|
420
|
+
# ---------------------------------------------------------------------------
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
@dataclass(frozen=True)
|
|
424
|
+
class Frame:
|
|
425
|
+
"""A single rendered moment of `dos top` — pure, serializable, testable."""
|
|
426
|
+
|
|
427
|
+
workspace: str
|
|
428
|
+
now_iso: str
|
|
429
|
+
lanes: tuple[LaneState, ...] = ()
|
|
430
|
+
verdicts: tuple[VerdictRow, ...] = ()
|
|
431
|
+
activity: tuple[dict, ...] = () # recent commits [{sha, subject}, …]
|
|
432
|
+
initialized: bool = True # did a dos.toml exist (vs. bare repo)?
|
|
433
|
+
|
|
434
|
+
def to_dict(self) -> dict:
|
|
435
|
+
return {
|
|
436
|
+
"workspace": self.workspace,
|
|
437
|
+
"now": self.now_iso,
|
|
438
|
+
"initialized": self.initialized,
|
|
439
|
+
"lanes": [s.to_dict() for s in self.lanes],
|
|
440
|
+
"verdicts": [v.to_dict() for v in self.verdicts],
|
|
441
|
+
"activity": [dict(c) for c in self.activity],
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
_WORK_OPS = frozenset({
|
|
446
|
+
lane_journal.OP_ACQUIRE, lane_journal.OP_RELEASE,
|
|
447
|
+
lane_journal.OP_SCAVENGE, lane_journal.OP_RECONCILE,
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _entry_ts(e: dict) -> str:
|
|
452
|
+
return str(e.get("ts") or e.get("heartbeat_at") or e.get("acquired_at") or "")
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _events_by_lane(entries: list[dict], live_by_lane: dict[str, dict]) -> dict[str, int]:
|
|
456
|
+
"""Count state-mutating lane-journal events recorded AFTER each lane's acquire.
|
|
457
|
+
|
|
458
|
+
The liveness rung wants *forward* lease-layer work, not the ACQUIRE that created
|
|
459
|
+
the currently-held lease (counting that would make every idle just-acquired lease
|
|
460
|
+
read ADVANCING forever — the first-smoke-test bug). The establishing ACQUIRE is
|
|
461
|
+
excluded by **identity** (the first ACQUIRE we see for this lane in append order
|
|
462
|
+
is its birth), NOT by a `ts > acquired_at` timestamp compare — the exact fix
|
|
463
|
+
`journal_delta.fold_since` adopted (docs/82): the timestamp rule only excluded
|
|
464
|
+
the birth ACQUIRE when its `ts` was not strictly past `acquired_at`, but in a real
|
|
465
|
+
grant those are TWO separate clock reads (`lane_lease.acquire` stamps
|
|
466
|
+
`acquired_at`, then `lane_journal.append` stamps a strictly-later `ts`), so the
|
|
467
|
+
birth ACQUIRE's `ts > acquired_at` is true and it was counted → a held-but-idle
|
|
468
|
+
lane read ADVANCING forever. Identity-keyed birth-skip holds regardless of the
|
|
469
|
+
clock relationship; a later RELEASE/SCAVENGE/RECONCILE or a genuine re-ACQUIRE is
|
|
470
|
+
real work and still counts (a keepalive HEARTBEAT is a beat, not progress, and is
|
|
471
|
+
not in `_WORK_OPS`). A lane with no live lease gets no entry (renders FREE). Pure
|
|
472
|
+
over already-read entries + the replayed live-lease map.
|
|
473
|
+
"""
|
|
474
|
+
out: dict[str, int] = {}
|
|
475
|
+
seen_birth: set[str] = set() # lanes whose establishing ACQUIRE we've skipped
|
|
476
|
+
for e in entries:
|
|
477
|
+
op = str(e.get("op") or "")
|
|
478
|
+
if op not in _WORK_OPS:
|
|
479
|
+
continue
|
|
480
|
+
lane = str(e.get("lane") or "")
|
|
481
|
+
live = live_by_lane.get(lane)
|
|
482
|
+
if not lane or live is None:
|
|
483
|
+
continue
|
|
484
|
+
# Skip exactly the FIRST ACQUIRE for this lane (its birth); count everything
|
|
485
|
+
# after — including a later re-ACQUIRE, which is real lease work.
|
|
486
|
+
if op == lane_journal.OP_ACQUIRE and lane not in seen_birth:
|
|
487
|
+
seen_birth.add(lane)
|
|
488
|
+
continue
|
|
489
|
+
out[lane] = out.get(lane, 0) + 1
|
|
490
|
+
return out
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
@dataclass(frozen=True)
|
|
494
|
+
class SpawnIntent:
|
|
495
|
+
"""A lane reading SPAWNING — a recent OP_SPAWN with no live lease yet (pure data)."""
|
|
496
|
+
|
|
497
|
+
holder: str = ""
|
|
498
|
+
age_ms: int | None = None
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _spawning_lanes(
|
|
502
|
+
entries: list[dict],
|
|
503
|
+
live_by_lane: dict[str, dict],
|
|
504
|
+
*,
|
|
505
|
+
now: dt.datetime,
|
|
506
|
+
ttl_ms: int = SPAWN_TTL_MS,
|
|
507
|
+
) -> dict[str, SpawnIntent]:
|
|
508
|
+
"""Fold recent OP_SPAWN intents into the set of lanes that are SPAWNING.
|
|
509
|
+
|
|
510
|
+
A lane is SPAWNING iff it has a journaled OP_SPAWN within ``ttl_ms`` AND holds no
|
|
511
|
+
live lease. Both gates are load-bearing:
|
|
512
|
+
|
|
513
|
+
* **no live lease** — once the eventual ACQUIRE lands, the held lease WINS the
|
|
514
|
+
chip (the liveness verdict is the truth then); SPAWNING is only the
|
|
515
|
+
SPAWN→ACQUIRE window. A later RELEASE that returns the lane to FREE re-exposes
|
|
516
|
+
any *still-fresh* SPAWN, but a launch normally acquires long before that.
|
|
517
|
+
* **within TTL** — a launch that DIES in preflight never acquires and never
|
|
518
|
+
releases; its SPAWN would otherwise wedge a phantom SPAWNING forever. The TTL
|
|
519
|
+
ages it out on its own — the self-heal `_expire_dead` gives a crashed holder,
|
|
520
|
+
here for a never-born one. This is the safety property that lets the SPAWN be
|
|
521
|
+
a pure forensic record (never a lease): a stale intent simply disappears.
|
|
522
|
+
|
|
523
|
+
Carries the MOST RECENT spawn's holder + age for rendering (a re-launch on the
|
|
524
|
+
same lane refreshes the intent). Pure over already-read entries + the replayed
|
|
525
|
+
live-lease map; the clock is injected. A lane that is already held gets no entry.
|
|
526
|
+
"""
|
|
527
|
+
# Most-recent SPAWN per lane (append order ⇒ last wins), with its age.
|
|
528
|
+
latest: dict[str, dict] = {}
|
|
529
|
+
for e in entries:
|
|
530
|
+
if str(e.get("op") or "") != lane_journal.OP_SPAWN:
|
|
531
|
+
continue
|
|
532
|
+
lane = str(e.get("lane") or "")
|
|
533
|
+
if not lane or lane in live_by_lane:
|
|
534
|
+
continue # held lanes take the liveness chip, not SPAWNING
|
|
535
|
+
latest[lane] = e # append order ⇒ this overwrites with the newer SPAWN
|
|
536
|
+
out: dict[str, SpawnIntent] = {}
|
|
537
|
+
for lane, e in latest.items():
|
|
538
|
+
age = _age_ms(_entry_ts(e), now=now)
|
|
539
|
+
if age is not None and age > ttl_ms:
|
|
540
|
+
continue # a dead-in-preflight launch ages out — no phantom SPAWNING
|
|
541
|
+
# An unreadable/absent `ts` (age is None) keeps the SPAWN visible rather than
|
|
542
|
+
# aging it out — but `append` ALWAYS stamps `ts`, so a real journaled SPAWN
|
|
543
|
+
# has a parseable age and the TTL gate is live; this only protects a
|
|
544
|
+
# hand-built/torn entry from vanishing silently (the row renders age `—`).
|
|
545
|
+
out[lane] = SpawnIntent(holder=str(e.get("holder") or ""), age_ms=age)
|
|
546
|
+
return out
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def snapshot(
|
|
550
|
+
config=None, *, verify=None, verdict_limit: int = 12, activity_limit: int = 10,
|
|
551
|
+
now: dt.datetime | None = None,
|
|
552
|
+
) -> Frame:
|
|
553
|
+
"""Read the four sources and freeze one `Frame`. The only I/O in this module.
|
|
554
|
+
|
|
555
|
+
Every reader degrades to empty on a missing/torn source, so this returns a
|
|
556
|
+
renderable frame in a **brand-new repo with no DOS state at all** (the
|
|
557
|
+
headline contract): no journal → no leases (all lanes FREE), no verdict dir →
|
|
558
|
+
no verdicts, and the git-activity strip from `git_delta.recent_commits` gives
|
|
559
|
+
the screen real content. ``verify`` defaults to the live `oracle.is_shipped`
|
|
560
|
+
bound to this workspace; pass a fake in tests.
|
|
561
|
+
"""
|
|
562
|
+
cfg = _config.ensure(config)
|
|
563
|
+
now = now or _now()
|
|
564
|
+
|
|
565
|
+
# --- leases (lane_journal WAL → live-lease set) + per-lane event counts ----
|
|
566
|
+
entries: list[dict] = []
|
|
567
|
+
try:
|
|
568
|
+
entries = lane_journal.read_all(cfg.paths.lane_journal)
|
|
569
|
+
except Exception:
|
|
570
|
+
entries = []
|
|
571
|
+
try:
|
|
572
|
+
leases = lane_journal.replay(entries)
|
|
573
|
+
except Exception:
|
|
574
|
+
leases = []
|
|
575
|
+
live_by_lane = {str(l.get("lane") or ""): l for l in leases}
|
|
576
|
+
payload = {
|
|
577
|
+
"leases": leases,
|
|
578
|
+
"events_by_lane": _events_by_lane(entries, live_by_lane),
|
|
579
|
+
"spawning_by_lane": _spawning_lanes(entries, live_by_lane, now=now),
|
|
580
|
+
}
|
|
581
|
+
roster = lane_roster(cfg)
|
|
582
|
+
states = build_lane_states(
|
|
583
|
+
payload, roster=roster, exclusive=tuple(cfg.lanes.exclusive), now=now
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
# --- recent verdicts (.verdict-*.json) + trust cross-check ----------------
|
|
587
|
+
if verify is None:
|
|
588
|
+
verify = _make_oracle_verify(cfg)
|
|
589
|
+
verdicts = _read_verdicts(cfg, limit=verdict_limit, verify=verify, now=now)
|
|
590
|
+
|
|
591
|
+
# --- git-activity strip (the fresh-repo content) --------------------------
|
|
592
|
+
try:
|
|
593
|
+
activity = git_delta.recent_commits(activity_limit, root=cfg.root)
|
|
594
|
+
except Exception:
|
|
595
|
+
activity = []
|
|
596
|
+
|
|
597
|
+
return Frame(
|
|
598
|
+
workspace=str(cfg.root),
|
|
599
|
+
now_iso=now.replace(microsecond=0).isoformat(),
|
|
600
|
+
lanes=tuple(states),
|
|
601
|
+
verdicts=tuple(verdicts),
|
|
602
|
+
activity=tuple(activity),
|
|
603
|
+
initialized=(cfg.root / "dos.toml").exists(),
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def _make_oracle_verify(cfg):
|
|
608
|
+
"""Build the live ``(plan, phase) -> bool`` over `oracle.is_shipped`, bound to cfg.
|
|
609
|
+
|
|
610
|
+
Imported lazily (oracle pulls a heavier chain) and wrapped so a missing
|
|
611
|
+
oracle degrades the trust column to NA rather than crashing the screen.
|
|
612
|
+
"""
|
|
613
|
+
try:
|
|
614
|
+
from dos import oracle
|
|
615
|
+
except Exception:
|
|
616
|
+
return None
|
|
617
|
+
|
|
618
|
+
def _verify(plan: str, phase: str) -> bool:
|
|
619
|
+
try:
|
|
620
|
+
return bool(oracle.is_shipped(plan, phase, cfg=cfg).shipped)
|
|
621
|
+
except Exception:
|
|
622
|
+
return False
|
|
623
|
+
|
|
624
|
+
return _verify
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def _read_verdicts(cfg, *, limit: int, verify, now: dt.datetime) -> list[VerdictRow]:
|
|
628
|
+
"""Walk `<next_packets>/.verdict-*.json`, newest-first, → trust-attached rows."""
|
|
629
|
+
ndir = cfg.paths.next_packets
|
|
630
|
+
try:
|
|
631
|
+
if not ndir.exists():
|
|
632
|
+
return []
|
|
633
|
+
files = sorted(ndir.glob(".verdict-*.json"), reverse=True)
|
|
634
|
+
except OSError:
|
|
635
|
+
return []
|
|
636
|
+
rows: list[VerdictRow] = []
|
|
637
|
+
for p in files:
|
|
638
|
+
if len(rows) >= limit:
|
|
639
|
+
break
|
|
640
|
+
try:
|
|
641
|
+
env = json.loads(p.read_text(encoding="utf-8", errors="replace"))
|
|
642
|
+
except (OSError, json.JSONDecodeError):
|
|
643
|
+
continue
|
|
644
|
+
if not isinstance(env, dict):
|
|
645
|
+
continue
|
|
646
|
+
tag = p.name[len(".verdict-"):-len(".json")]
|
|
647
|
+
rows.append(attach_trust(parse_verdict_envelope(env, tag, now=now), verify))
|
|
648
|
+
return rows
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
# ---------------------------------------------------------------------------
|
|
652
|
+
# Rendering — the plain-text floor (always available; the rich skin is in
|
|
653
|
+
# dispatch_top_tui). Each renderer is pure over its data + a `now` string, so the
|
|
654
|
+
# tests assert byte-identical output (the job DTOP renderer discipline).
|
|
655
|
+
# ---------------------------------------------------------------------------
|
|
656
|
+
|
|
657
|
+
_WIDTH = 78
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def render_lanes_text(states: tuple[LaneState, ...]) -> str:
|
|
661
|
+
out = ["LANES"]
|
|
662
|
+
if not states:
|
|
663
|
+
out.append(" (no lanes — declare [lanes] in dos.toml, or `dos init`)")
|
|
664
|
+
for s in states:
|
|
665
|
+
bits: list[str] = []
|
|
666
|
+
if s.loop_ts:
|
|
667
|
+
bits.append(f"loop={s.loop_ts}")
|
|
668
|
+
if s.heartbeat_age_ms is not None:
|
|
669
|
+
# The age field is generic; the LABEL is chip-honest — a SPAWNING lane's
|
|
670
|
+
# age is "how long since the spawn intent", not a heartbeat (it has no
|
|
671
|
+
# lease to beat yet), so it reads `spawn <age>`, a held lane `hb <age>`.
|
|
672
|
+
label = "spawn" if s.chip == CHIP_SPAWNING else "hb"
|
|
673
|
+
bits.append(f"{label} {_fmt_age(s.heartbeat_age_ms)}")
|
|
674
|
+
if s.holder:
|
|
675
|
+
bits.append(s.holder)
|
|
676
|
+
marker = "*" if s.is_exclusive else " "
|
|
677
|
+
detail = " ".join(bits)
|
|
678
|
+
out.append(f" {marker}{s.lane:<13} {s.chip:<13} {detail}".rstrip())
|
|
679
|
+
live = sum(1 for s in states if s.chip == CHIP_ADVANCING)
|
|
680
|
+
spin = sum(1 for s in states if s.chip == CHIP_SPINNING)
|
|
681
|
+
stalled = sum(1 for s in states if s.chip == CHIP_STALLED)
|
|
682
|
+
spawning = sum(1 for s in states if s.chip == CHIP_SPAWNING)
|
|
683
|
+
free = sum(1 for s in states if s.chip == CHIP_FREE)
|
|
684
|
+
tally = (
|
|
685
|
+
f" {len(states)} lanes · {live} advancing · {spin} spinning · "
|
|
686
|
+
f"{stalled} stalled · "
|
|
687
|
+
)
|
|
688
|
+
# Only surface the spawning count when there IS one — keep the steady-state
|
|
689
|
+
# summary (the byte-pinned no-spawn line) unchanged so existing renders/tests
|
|
690
|
+
# are undisturbed; a coming run adds a segment, it doesn't reshape the line.
|
|
691
|
+
if spawning:
|
|
692
|
+
tally += f"{spawning} spawning · "
|
|
693
|
+
tally += f"{free} free"
|
|
694
|
+
out.append(tally)
|
|
695
|
+
return "\n".join(out)
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def render_verdicts_text(rows: tuple[VerdictRow, ...], *, limit: int = 12) -> str:
|
|
699
|
+
out = ["RECENT VERDICTS [trust = ship-oracle cross-check]"]
|
|
700
|
+
if not rows:
|
|
701
|
+
out.append(" (no verdicts yet)")
|
|
702
|
+
for r in rows[:limit]:
|
|
703
|
+
reason = f" {r.reason_token}" if r.reason_token else ""
|
|
704
|
+
trust = r.trust if r.trust != TRUST_NA else ""
|
|
705
|
+
out.append(
|
|
706
|
+
f" {_fmt_age(r.age_ms):>4} {(r.lane or '-'):<12} {r.verdict:<8} "
|
|
707
|
+
f"{(r.pick or '-'):<14} {trust:<10}{reason}".rstrip()
|
|
708
|
+
)
|
|
709
|
+
return "\n".join(out)
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def render_activity_text(commits: tuple[dict, ...], *, limit: int = 10) -> str:
|
|
713
|
+
out = ["RECENT COMMITS [ground truth — git history]"]
|
|
714
|
+
if not commits:
|
|
715
|
+
out.append(" (no commits — empty or non-git workspace)")
|
|
716
|
+
for c in commits[:limit]:
|
|
717
|
+
sha = str(c.get("sha") or "")[:9]
|
|
718
|
+
subject = str(c.get("subject") or "")
|
|
719
|
+
out.append(f" {sha:<9} {subject}"[: _WIDTH + 2])
|
|
720
|
+
return "\n".join(out)
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def render_frame_text(frame: Frame) -> str:
|
|
724
|
+
"""The whole `dos top --once` screen as plain text — the always-available floor."""
|
|
725
|
+
# A long workspace path can exceed the rule width; print the header in full
|
|
726
|
+
# (never truncate the path the operator needs to read) and pad with `─` only
|
|
727
|
+
# when there is room. Truncating here mangled long temp-dir paths in testing.
|
|
728
|
+
head = f"┌─ dos top · {frame.workspace} · {frame.now_iso} "
|
|
729
|
+
out = [head + "─" * max(0, _WIDTH - len(head))]
|
|
730
|
+
if not frame.initialized:
|
|
731
|
+
out.append(" (no dos.toml — showing generic main/global; `dos init` to declare lanes)")
|
|
732
|
+
out.append("")
|
|
733
|
+
out.append(render_lanes_text(frame.lanes))
|
|
734
|
+
out.append("")
|
|
735
|
+
out.append(render_verdicts_text(frame.verdicts))
|
|
736
|
+
out.append("")
|
|
737
|
+
out.append(render_activity_text(frame.activity))
|
|
738
|
+
out.append("─" * _WIDTH)
|
|
739
|
+
out.append("read-only · q quit · this screen mutates nothing")
|
|
740
|
+
return "\n".join(out)
|