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/plan_board.py
ADDED
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
"""`dos plan` — the work-terrain projection: every phase, claimed vs oracle-confirmed.
|
|
2
|
+
|
|
3
|
+
The **third projection** of the trust substrate, beside `dos top` and `dos decisions`:
|
|
4
|
+
|
|
5
|
+
* `dos top` — what is *running* now (leases · liveness · verdicts · git).
|
|
6
|
+
* `dos decisions` — what is waiting on *me* now (the four refusal sources).
|
|
7
|
+
* `dos plan` — what is the *shape of the work*, and how far has it shipped.
|
|
8
|
+
|
|
9
|
+
The kernel-honest reframe (CLAUDE.md: the plan schema is NOT in the kernel): a plan
|
|
10
|
+
view is a **verify()-fan-out, not a plan reader**. The reference userland app paints a
|
|
11
|
+
status board from the plan's own self-report (`execution-state.yaml` says IF4.1 is
|
|
12
|
+
done); the kernel is built to distrust exactly that. So here the plan supplies only
|
|
13
|
+
candidate ``(plan, phase)`` rows (via the declared `plan_source` seam), and the *status*
|
|
14
|
+
of every row comes from `oracle.is_shipped` — the truth syscall, registry-first,
|
|
15
|
+
ancestry-checked, never the stamp. The screen exists for one cell: the **divergence
|
|
16
|
+
flag**, where the plan CLAIMS shipped but the oracle says not (or the reverse). That is
|
|
17
|
+
the believed-vs-adjudicated thesis at plan altitude — the one cell a self-reporting view
|
|
18
|
+
structurally cannot show.
|
|
19
|
+
|
|
20
|
+
It is a **read-only projection** (the `dispatch_top` / `decisions` discipline restated):
|
|
21
|
+
it stores nothing, mutates nothing, acquires no lease, launches no agent. Every panel is
|
|
22
|
+
a pure function over an in-memory payload; the only I/O is `snapshot()` at the boundary,
|
|
23
|
+
which reads three already-persisted sources and freezes them:
|
|
24
|
+
|
|
25
|
+
rows <- plan_source.default_rows(cfg) (the declared markdown source, or a
|
|
26
|
+
plugin, or an explicit phase list)
|
|
27
|
+
oracle <- oracle.is_shipped(plan, phase) (the verdict — the WHOLE point)
|
|
28
|
+
leases <- lane_journal.replay(...) (which phase's lane a live lease holds)
|
|
29
|
+
decisions <- decisions.collect_decisions(..) (which phase's lane a gate blocks)
|
|
30
|
+
|
|
31
|
+
It does NOT re-read the world `dos top`/`dos decisions` already read — it COMPOSES them:
|
|
32
|
+
the oracle cross-check reuses `dispatch_top.attach_trust`'s injected-`verify` boundary
|
|
33
|
+
(promoted from a column to the whole screen), the live-lease join reuses
|
|
34
|
+
`dispatch_top.build_lane_states`, and the gate join reuses `decisions.collect_decisions`.
|
|
35
|
+
A plan-row is the spine that ties the other two projections together.
|
|
36
|
+
|
|
37
|
+
Nothing here imports a host. In a repo with **no plans at all** (`plan_source` yields
|
|
38
|
+
``[]``), every row reader returns empty and the screen shows "(no plans declared)" plus
|
|
39
|
+
the `git_delta` recent-ships strip — the same fresh-repo floor `dos top` has, pinned in
|
|
40
|
+
`tests/test_plan_board.py`. The rich live skin lives in `plan_board_tui` (behind the
|
|
41
|
+
`[tui]` extra); this module is import-light so the plain-text renderers are always the
|
|
42
|
+
available floor — the `dispatch_top` / `dispatch_top_tui` split.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from __future__ import annotations
|
|
46
|
+
|
|
47
|
+
import datetime as dt
|
|
48
|
+
import io
|
|
49
|
+
import sys
|
|
50
|
+
from dataclasses import dataclass
|
|
51
|
+
|
|
52
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
53
|
+
try:
|
|
54
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
|
|
55
|
+
except Exception: # pragma: no cover
|
|
56
|
+
pass
|
|
57
|
+
elif not isinstance(sys.stdout, io.TextIOWrapper): # pragma: no cover
|
|
58
|
+
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
|
|
59
|
+
|
|
60
|
+
from dos import config as _config
|
|
61
|
+
from dos import dispatch_top as _dtop
|
|
62
|
+
from dos import git_delta
|
|
63
|
+
from dos import lane_journal
|
|
64
|
+
from dos import plan_source as _plan_source
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# Divergence — the headline. The plan's CLAIM vs the oracle's VERDICT, collapsed
|
|
69
|
+
# to one token the operator reads at a glance. This is the believed-vs-adjudicated
|
|
70
|
+
# cell the whole screen is built around; everything else is context for it.
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
DIV_OK_SHIPPED = "✓shipped" # claim shipped & oracle confirms — agreed, done
|
|
74
|
+
DIV_PENDING = "·pending" # claim open/blocked & oracle says not-yet — agreed, in flight
|
|
75
|
+
DIV_OVERCLAIM = "⚠over-claim" # claim SHIPPED but oracle says NOT — the plan is lying (the headline)
|
|
76
|
+
DIV_UNDERCLAIM = "✓under-claim" # claim open/blocked but oracle CONFIRMS shipped — plan stamp lags
|
|
77
|
+
DIV_UNKNOWN = "—" # the plan claimed nothing — oracle verdict stands alone
|
|
78
|
+
|
|
79
|
+
# The two values that mean "the plan and the oracle DISAGREE" — what the screen tallies
|
|
80
|
+
# as DIVERGENT and what the rich skin paints loud. An over-claim is the dangerous one (a
|
|
81
|
+
# phase the plan calls done that did not ship); an under-claim is benign (stamp drift) but
|
|
82
|
+
# still a divergence worth surfacing.
|
|
83
|
+
_DIVERGENT = frozenset({DIV_OVERCLAIM, DIV_UNDERCLAIM})
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def divergence(claimed_status: str, oracle_shipped: bool) -> str:
|
|
87
|
+
"""The claimed-vs-oracle cell for one phase. Pure.
|
|
88
|
+
|
|
89
|
+
The four-way truth table over (plan claims shipped?) × (oracle confirms shipped?):
|
|
90
|
+
|
|
91
|
+
claim shipped + oracle yes → ✓shipped (agreed done)
|
|
92
|
+
claim shipped + oracle NO → ⚠over-claim (the plan is lying — the headline cell)
|
|
93
|
+
claim not + oracle yes → ✓under-claim(plan stamp lags reality — benign drift)
|
|
94
|
+
claim not + oracle no → ·pending (agreed in flight)
|
|
95
|
+
claim UNKNOWN → — (plan said nothing; oracle stands alone)
|
|
96
|
+
|
|
97
|
+
The oracle is ALWAYS the authority — `claimed_status` only selects which of the four
|
|
98
|
+
cells we are in, it never overrides the verdict. A plan view that trusted the claim
|
|
99
|
+
would be a self-narrating worker; this makes the disagreement the visible artifact.
|
|
100
|
+
"""
|
|
101
|
+
claim_shipped = claimed_status == _plan_source.CLAIMED_SHIPPED
|
|
102
|
+
if claimed_status == _plan_source.CLAIMED_UNKNOWN:
|
|
103
|
+
# The plan claimed nothing — there is no claim to diverge FROM. Report the bare
|
|
104
|
+
# oracle verdict; never call a no-claim row an over/under-claim.
|
|
105
|
+
return DIV_OK_SHIPPED if oracle_shipped else DIV_UNKNOWN
|
|
106
|
+
if claim_shipped and oracle_shipped:
|
|
107
|
+
return DIV_OK_SHIPPED
|
|
108
|
+
if claim_shipped and not oracle_shipped:
|
|
109
|
+
return DIV_OVERCLAIM
|
|
110
|
+
if not claim_shipped and oracle_shipped:
|
|
111
|
+
return DIV_UNDERCLAIM
|
|
112
|
+
return DIV_PENDING
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
# Time helper (compact age — mirrors dispatch_top / decisions).
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _now() -> dt.datetime:
|
|
121
|
+
return dt.datetime.now(dt.timezone.utc)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
# The rendered phase row — pure data, no rich objects, carries the oracle verdict
|
|
126
|
+
# + the joins to the other two projections.
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass(frozen=True)
|
|
131
|
+
class PhaseRow:
|
|
132
|
+
"""One rendered phase — the candidate, the oracle verdict, and the cross-refs.
|
|
133
|
+
|
|
134
|
+
``claimed_status`` is the plan's self-report (from the source); ``oracle_shipped``
|
|
135
|
+
/``oracle_source``/``oracle_sha`` are the verdict (from `oracle.is_shipped`);
|
|
136
|
+
``divergence`` is the headline cell over the two. ``lane_chip`` is the live-lease
|
|
137
|
+
state of the phase's lane (the join to `dos top`, "" when no lane / no lease);
|
|
138
|
+
``decision_ref`` names a pending gate on the phase's lane (the join to
|
|
139
|
+
`dos decisions`, "" when none). The joins are LANE-KEYED and conservative — a row
|
|
140
|
+
links to a lease/decision only when its lane is known and matches.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
plan: str
|
|
144
|
+
phase: str
|
|
145
|
+
doc_path: str = ""
|
|
146
|
+
claimed_status: str = _plan_source.CLAIMED_UNKNOWN
|
|
147
|
+
oracle_shipped: bool = False
|
|
148
|
+
oracle_source: str = "" # "registry" | "grep" | "none"
|
|
149
|
+
oracle_sha: str = ""
|
|
150
|
+
divergence: str = DIV_UNKNOWN
|
|
151
|
+
lane: str = ""
|
|
152
|
+
lane_chip: str = "" # a dispatch_top CHIP_* when a live lease holds the lane
|
|
153
|
+
decision_ref: str = "" # a pending-decision reason token on the lane, or ""
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def is_divergent(self) -> bool:
|
|
157
|
+
"""True iff plan and oracle DISAGREE (over- or under-claim) — the tally key."""
|
|
158
|
+
return self.divergence in _DIVERGENT
|
|
159
|
+
|
|
160
|
+
def to_dict(self) -> dict:
|
|
161
|
+
return {
|
|
162
|
+
"plan": self.plan,
|
|
163
|
+
"phase": self.phase,
|
|
164
|
+
"doc_path": self.doc_path,
|
|
165
|
+
"claimed_status": self.claimed_status,
|
|
166
|
+
"oracle_shipped": self.oracle_shipped,
|
|
167
|
+
"oracle_source": self.oracle_source,
|
|
168
|
+
"oracle_sha": self.oracle_sha,
|
|
169
|
+
"divergence": self.divergence,
|
|
170
|
+
"is_divergent": self.is_divergent,
|
|
171
|
+
"lane": self.lane,
|
|
172
|
+
"lane_chip": self.lane_chip,
|
|
173
|
+
"decision_ref": self.decision_ref,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ---------------------------------------------------------------------------
|
|
178
|
+
# The pure adapter — (plan rows, oracle verify, lane states, decisions) → PhaseRows.
|
|
179
|
+
# This is the unit-test surface; the I/O all lives in snapshot().
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def build_phase_rows(
|
|
184
|
+
rows,
|
|
185
|
+
*,
|
|
186
|
+
verify,
|
|
187
|
+
lane_states=(),
|
|
188
|
+
decisions=(),
|
|
189
|
+
) -> list[PhaseRow]:
|
|
190
|
+
"""Pure: join candidate plan rows with the oracle verdict + the two cross-refs.
|
|
191
|
+
|
|
192
|
+
``rows`` is the `plan_source.PlanRow` list. ``verify`` is the injected
|
|
193
|
+
``(plan, phase) -> ShipVerdict`` (the live path wires `oracle.is_shipped`; tests
|
|
194
|
+
inject a fake) — the SAME boundary `dispatch_top.attach_trust` uses, here fanned over
|
|
195
|
+
every row instead of one verdict. ``lane_states`` are `dispatch_top.LaneState`s (so a
|
|
196
|
+
row whose lane holds a live lease shows its chip); ``decisions`` are
|
|
197
|
+
`decisions.Decision`s (so a row whose lane has a pending gate shows it). Both joins are
|
|
198
|
+
lane-keyed and degrade to "" when the row carries no lane.
|
|
199
|
+
|
|
200
|
+
Never raises on a verify fault — a `verify` that throws or returns a non-verdict
|
|
201
|
+
degrades that row to a NOT-shipped oracle reading (fail-safe, the `attach_trust`
|
|
202
|
+
posture: the screen never crashes on a flaky oracle).
|
|
203
|
+
"""
|
|
204
|
+
chip_by_lane: dict[str, str] = {}
|
|
205
|
+
for s in lane_states:
|
|
206
|
+
lane = getattr(s, "lane", "")
|
|
207
|
+
chip = getattr(s, "chip", "")
|
|
208
|
+
# Only a HELD lane contributes a chip — a FREE lane is no join signal.
|
|
209
|
+
if lane and chip and chip != _dtop.CHIP_FREE:
|
|
210
|
+
chip_by_lane[lane] = chip
|
|
211
|
+
|
|
212
|
+
decision_by_lane: dict[str, str] = {}
|
|
213
|
+
for d in decisions:
|
|
214
|
+
lane = getattr(d, "lane", "")
|
|
215
|
+
if not lane or lane in decision_by_lane:
|
|
216
|
+
continue
|
|
217
|
+
token = getattr(d, "reason_token", "") or getattr(d, "reason_text", "")
|
|
218
|
+
decision_by_lane[lane] = str(token)[:40]
|
|
219
|
+
|
|
220
|
+
out: list[PhaseRow] = []
|
|
221
|
+
for r in rows:
|
|
222
|
+
plan = getattr(r, "plan", "")
|
|
223
|
+
phase = getattr(r, "phase", "")
|
|
224
|
+
claimed = getattr(r, "claimed_status", _plan_source.CLAIMED_UNKNOWN)
|
|
225
|
+
lane = getattr(r, "lane", "") or ""
|
|
226
|
+
shipped, source, sha = _verify_row(verify, plan, phase)
|
|
227
|
+
out.append(PhaseRow(
|
|
228
|
+
plan=plan,
|
|
229
|
+
phase=phase,
|
|
230
|
+
doc_path=getattr(r, "doc_path", ""),
|
|
231
|
+
claimed_status=claimed,
|
|
232
|
+
oracle_shipped=shipped,
|
|
233
|
+
oracle_source=source,
|
|
234
|
+
oracle_sha=sha,
|
|
235
|
+
divergence=divergence(claimed, shipped),
|
|
236
|
+
lane=lane,
|
|
237
|
+
lane_chip=chip_by_lane.get(lane, ""),
|
|
238
|
+
decision_ref=decision_by_lane.get(lane, ""),
|
|
239
|
+
))
|
|
240
|
+
return out
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _verify_row(verify, plan: str, phase: str) -> tuple[bool, str, str]:
|
|
244
|
+
"""Run one row through the injected verify, fail-safe → (shipped, source, sha).
|
|
245
|
+
|
|
246
|
+
``verify`` may return a `ShipVerdict` (the live `oracle.is_shipped`) OR a bare bool
|
|
247
|
+
(the simplest test fake / the `dispatch_top.attach_trust` contract). Both are read
|
|
248
|
+
uniformly here so a caller can inject either. Any raise / unexpected return degrades
|
|
249
|
+
to ``(False, "none", "")`` — a flaky oracle never crashes the board.
|
|
250
|
+
"""
|
|
251
|
+
if verify is None:
|
|
252
|
+
return (False, "", "")
|
|
253
|
+
try:
|
|
254
|
+
res = verify(plan, phase)
|
|
255
|
+
except Exception:
|
|
256
|
+
return (False, "none", "")
|
|
257
|
+
if isinstance(res, bool):
|
|
258
|
+
return (res, "" if not res else "registry", "")
|
|
259
|
+
shipped = bool(getattr(res, "shipped", False))
|
|
260
|
+
source = str(getattr(res, "source", "") or "")
|
|
261
|
+
sha = str(getattr(res, "sha", "") or "")
|
|
262
|
+
return (shipped, source, sha)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# ---------------------------------------------------------------------------
|
|
266
|
+
# The frame — everything one screen shows, as pure data. snapshot() builds it from
|
|
267
|
+
# disk; the renderers + the TUI consume it.
|
|
268
|
+
# ---------------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@dataclass(frozen=True)
|
|
272
|
+
class Frame:
|
|
273
|
+
"""A single rendered moment of `dos plan` — pure, serializable, testable."""
|
|
274
|
+
|
|
275
|
+
workspace: str
|
|
276
|
+
now_iso: str
|
|
277
|
+
phases: tuple[PhaseRow, ...] = ()
|
|
278
|
+
activity: tuple[dict, ...] = () # recent commits [{sha, subject}, …] — the floor
|
|
279
|
+
plan_source: str = "markdown" # which source produced the rows
|
|
280
|
+
initialized: bool = True # did a dos.toml exist (vs. bare repo)?
|
|
281
|
+
|
|
282
|
+
def to_dict(self) -> dict:
|
|
283
|
+
return {
|
|
284
|
+
"workspace": self.workspace,
|
|
285
|
+
"now": self.now_iso,
|
|
286
|
+
"plan_source": self.plan_source,
|
|
287
|
+
"initialized": self.initialized,
|
|
288
|
+
"phases": [p.to_dict() for p in self.phases],
|
|
289
|
+
"activity": [dict(c) for c in self.activity],
|
|
290
|
+
"summary": self.summary(),
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
def summary(self) -> dict:
|
|
294
|
+
"""The one-line tally the footer + `--json` consumers read."""
|
|
295
|
+
total = len(self.phases)
|
|
296
|
+
shipped = sum(1 for p in self.phases if p.oracle_shipped)
|
|
297
|
+
divergent = sum(1 for p in self.phases if p.is_divergent)
|
|
298
|
+
overclaim = sum(1 for p in self.phases if p.divergence == DIV_OVERCLAIM)
|
|
299
|
+
in_flight = sum(1 for p in self.phases if p.lane_chip)
|
|
300
|
+
gated = sum(1 for p in self.phases if p.decision_ref)
|
|
301
|
+
return {
|
|
302
|
+
"phases": total,
|
|
303
|
+
"shipped": shipped,
|
|
304
|
+
"divergent": divergent,
|
|
305
|
+
"over_claims": overclaim,
|
|
306
|
+
"in_flight": in_flight,
|
|
307
|
+
"gated": gated,
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def snapshot(
|
|
312
|
+
config=None,
|
|
313
|
+
*,
|
|
314
|
+
verify=None,
|
|
315
|
+
rows=None,
|
|
316
|
+
source_name: str | None = None,
|
|
317
|
+
activity_limit: int = 10,
|
|
318
|
+
now: dt.datetime | None = None,
|
|
319
|
+
) -> Frame:
|
|
320
|
+
"""Read the sources and freeze one `Frame`. The only I/O in this module.
|
|
321
|
+
|
|
322
|
+
Resolution of the candidate rows (the one host-shaped choice):
|
|
323
|
+
* an explicit ``rows`` list (the CLI's phase-list escape hatch / a test) wins;
|
|
324
|
+
* else a named source (``source_name``, resolved through `plan_source`), run
|
|
325
|
+
fail-safe;
|
|
326
|
+
* else the default markdown source (`plan_source.default_rows`).
|
|
327
|
+
|
|
328
|
+
``verify`` defaults to the live `oracle.is_shipped` bound to this workspace; tests
|
|
329
|
+
inject a fake. Every reader degrades to empty on a missing/torn source, so this
|
|
330
|
+
returns a renderable frame in a **repo with no plans at all** (the headline contract):
|
|
331
|
+
no plan docs → no phase rows → the screen shows "(no plans)" and the
|
|
332
|
+
`git_delta.recent_commits` strip carries it, exactly as `dos top` degrades.
|
|
333
|
+
"""
|
|
334
|
+
cfg = _config.ensure(config)
|
|
335
|
+
now = now or _now()
|
|
336
|
+
|
|
337
|
+
# --- candidate rows (explicit > named source > default markdown) ----------
|
|
338
|
+
src_label = source_name or "markdown"
|
|
339
|
+
if rows is not None:
|
|
340
|
+
plan_rows = list(rows)
|
|
341
|
+
# Explicit rows ALWAYS come from the CLI's positional-phase escape hatch, never a
|
|
342
|
+
# named source — so the provenance label is "explicit" regardless of any --source
|
|
343
|
+
# flag that rode along unused. (Labeling it with the unused source_name would have
|
|
344
|
+
# the header/JSON claim a source that produced none of the shown rows.)
|
|
345
|
+
src_label = "explicit"
|
|
346
|
+
elif source_name:
|
|
347
|
+
try:
|
|
348
|
+
src = _plan_source.resolve_plan_source(source_name)
|
|
349
|
+
plan_rows = _plan_source.run_plan_source(src, cfg)
|
|
350
|
+
except ValueError:
|
|
351
|
+
plan_rows = []
|
|
352
|
+
else:
|
|
353
|
+
plan_rows = _plan_source.default_rows(cfg)
|
|
354
|
+
|
|
355
|
+
# --- the oracle verdict per row (the whole point) -------------------------
|
|
356
|
+
if verify is None:
|
|
357
|
+
verify = _make_oracle_verify(cfg)
|
|
358
|
+
|
|
359
|
+
# --- the two cross-ref joins (compose dos top + dos decisions readers) ----
|
|
360
|
+
lane_states = _live_lane_states(cfg, now=now)
|
|
361
|
+
decisions = _pending_decisions(cfg)
|
|
362
|
+
|
|
363
|
+
phase_rows = build_phase_rows(
|
|
364
|
+
plan_rows, verify=verify, lane_states=lane_states, decisions=decisions
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
# --- git-activity strip (the no-plan floor content) -----------------------
|
|
368
|
+
try:
|
|
369
|
+
activity = git_delta.recent_commits(activity_limit, root=cfg.root)
|
|
370
|
+
except Exception:
|
|
371
|
+
activity = []
|
|
372
|
+
|
|
373
|
+
return Frame(
|
|
374
|
+
workspace=str(cfg.root),
|
|
375
|
+
now_iso=now.replace(microsecond=0).isoformat(),
|
|
376
|
+
phases=tuple(phase_rows),
|
|
377
|
+
activity=tuple(activity),
|
|
378
|
+
plan_source=src_label,
|
|
379
|
+
initialized=(cfg.root / "dos.toml").exists(),
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _make_oracle_verify(cfg):
|
|
384
|
+
"""Build the live ``(plan, phase) -> ShipVerdict`` over `oracle.is_shipped`, bound to cfg.
|
|
385
|
+
|
|
386
|
+
Imported lazily (oracle pulls a heavier chain) and wrapped so a missing oracle
|
|
387
|
+
degrades a row to a NOT-shipped reading rather than crashing the screen. Returns a
|
|
388
|
+
full `ShipVerdict` (not a bool) so the board can show the verdict's `source`/`sha` —
|
|
389
|
+
the richer surface a plan board wants over `dispatch_top`'s bool trust column.
|
|
390
|
+
"""
|
|
391
|
+
try:
|
|
392
|
+
from dos import oracle
|
|
393
|
+
except Exception:
|
|
394
|
+
return None
|
|
395
|
+
|
|
396
|
+
def _verify(plan: str, phase: str):
|
|
397
|
+
try:
|
|
398
|
+
return oracle.is_shipped(plan, phase, cfg=cfg)
|
|
399
|
+
except Exception:
|
|
400
|
+
# Return a full NOT-shipped verdict (not a bare False) so a live oracle that
|
|
401
|
+
# throws internally labels its row `source="none"` — consistent with
|
|
402
|
+
# `_verify_row`'s boundary-raise path, rather than the bool branch's "".
|
|
403
|
+
return oracle.ShipVerdict(plan=plan, phase=phase, shipped=False, source="none")
|
|
404
|
+
|
|
405
|
+
return _verify
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _live_lane_states(cfg, *, now):
|
|
409
|
+
"""The live-lease lane states — reuse `dispatch_top`'s reader, never re-derive.
|
|
410
|
+
|
|
411
|
+
Folds the lane journal to the live-lease set and builds `dispatch_top.LaneState`s so
|
|
412
|
+
a phase row can show whether its lane holds a moving lease. Degrades to ``()`` on any
|
|
413
|
+
torn source (the board then shows no lane chips — never crashes)."""
|
|
414
|
+
try:
|
|
415
|
+
entries = lane_journal.read_all(cfg.paths.lane_journal)
|
|
416
|
+
except Exception:
|
|
417
|
+
entries = []
|
|
418
|
+
try:
|
|
419
|
+
leases = lane_journal.replay(entries)
|
|
420
|
+
except Exception:
|
|
421
|
+
leases = []
|
|
422
|
+
if not leases:
|
|
423
|
+
return ()
|
|
424
|
+
live_by_lane = {str(l.get("lane") or ""): l for l in leases}
|
|
425
|
+
payload = {
|
|
426
|
+
"leases": leases,
|
|
427
|
+
"events_by_lane": _dtop._events_by_lane(entries, live_by_lane),
|
|
428
|
+
}
|
|
429
|
+
try:
|
|
430
|
+
roster = _dtop.lane_roster(cfg)
|
|
431
|
+
return tuple(_dtop.build_lane_states(
|
|
432
|
+
payload, roster=roster, exclusive=tuple(cfg.lanes.exclusive), now=now,
|
|
433
|
+
))
|
|
434
|
+
except Exception:
|
|
435
|
+
return ()
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _pending_decisions(cfg):
|
|
439
|
+
"""The pending operator decisions — reuse `decisions.collect_decisions`, all kinds.
|
|
440
|
+
|
|
441
|
+
We want EVERY pending decision (not just HUMAN-resolvable) so a phase row reflects an
|
|
442
|
+
ORACLE/JUDGE-owned gate too; hence `resolver=None`. Degrades to ``()`` on any fault."""
|
|
443
|
+
try:
|
|
444
|
+
from dos import decisions as _decisions
|
|
445
|
+
return tuple(_decisions.collect_decisions(cfg, resolver=None))
|
|
446
|
+
except Exception:
|
|
447
|
+
return ()
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
# ---------------------------------------------------------------------------
|
|
451
|
+
# Rendering — the plain-text floor (always available; the rich skin is in
|
|
452
|
+
# plan_board_tui). Each renderer is pure over its data, so the tests assert
|
|
453
|
+
# byte-stable output (the dispatch_top renderer discipline).
|
|
454
|
+
# ---------------------------------------------------------------------------
|
|
455
|
+
|
|
456
|
+
_WIDTH = 88
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def render_phases_text(phases: tuple[PhaseRow, ...]) -> str:
|
|
460
|
+
out = ["PHASES [oracle = truth syscall · ⚠ = plan claim diverges from oracle]"]
|
|
461
|
+
if not phases:
|
|
462
|
+
out.append(" (no plans declared — set [paths].plans_glob in dos.toml, or pass phases)")
|
|
463
|
+
return "\n".join(out)
|
|
464
|
+
header = (f" {'plan':<8} {'phase':<14} {'claimed':<8} {'oracle':<14} "
|
|
465
|
+
f"{'lane':<10} gate")
|
|
466
|
+
out.append(header)
|
|
467
|
+
out.append(" " + "-" * (len(header) - 2))
|
|
468
|
+
for p in phases:
|
|
469
|
+
claimed = p.claimed_status or "-"
|
|
470
|
+
lane = p.lane or "-"
|
|
471
|
+
# The lane cell shows the live chip glyph when a lease holds it, else the bare name.
|
|
472
|
+
lane_cell = (p.lane_chip.split()[0] + " " + lane) if p.lane_chip else lane
|
|
473
|
+
gate = p.decision_ref or ""
|
|
474
|
+
out.append(
|
|
475
|
+
f" {p.plan:<8} {p.phase:<14} {claimed:<8} {p.divergence:<14} "
|
|
476
|
+
f"{lane_cell:<10} {gate}".rstrip()
|
|
477
|
+
)
|
|
478
|
+
s = Frame(workspace="", now_iso="", phases=phases).summary()
|
|
479
|
+
out.append(
|
|
480
|
+
f" {s['phases']} phases · {s['shipped']} shipped · {s['divergent']} DIVERGENT "
|
|
481
|
+
f"({s['over_claims']} over-claim) · {s['in_flight']} in-flight · {s['gated']} gated"
|
|
482
|
+
)
|
|
483
|
+
return "\n".join(out)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def render_activity_text(commits: tuple[dict, ...], *, limit: int = 10) -> str:
|
|
487
|
+
out = ["RECENT COMMITS [ground truth — git history]"]
|
|
488
|
+
if not commits:
|
|
489
|
+
out.append(" (no commits — empty or non-git workspace)")
|
|
490
|
+
for c in commits[:limit]:
|
|
491
|
+
sha = str(c.get("sha") or "")[:9]
|
|
492
|
+
subject = str(c.get("subject") or "")
|
|
493
|
+
out.append(f" {sha:<9} {subject}"[: _WIDTH + 2])
|
|
494
|
+
return "\n".join(out)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def render_frame_text(frame: Frame) -> str:
|
|
498
|
+
"""The whole `dos plan --once` screen as plain text — the always-available floor."""
|
|
499
|
+
head = f"┌─ dos plan · {frame.workspace} · {frame.now_iso} "
|
|
500
|
+
out = [head + "─" * max(0, _WIDTH - len(head))]
|
|
501
|
+
if not frame.initialized:
|
|
502
|
+
out.append(" (no dos.toml — generic main/global; `dos init` to declare lanes/plans)")
|
|
503
|
+
out.append("")
|
|
504
|
+
out.append(render_phases_text(frame.phases))
|
|
505
|
+
out.append("")
|
|
506
|
+
out.append(render_activity_text(frame.activity))
|
|
507
|
+
out.append("─" * _WIDTH)
|
|
508
|
+
div = frame.summary()["divergent"]
|
|
509
|
+
if div:
|
|
510
|
+
out.append(f"⚠ {div} phase(s) where the plan's claim DISAGREES with the oracle — "
|
|
511
|
+
f"the cell this screen exists to surface.")
|
|
512
|
+
out.append("read-only · q quit · this screen mutates nothing")
|
|
513
|
+
return "\n".join(out)
|
dos/plan_board_tui.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""The live `dos plan` screen — a `rich.live` poll loop over `plan_board.snapshot`.
|
|
2
|
+
|
|
3
|
+
The rendering layer for `dos plan`'s interactive mode, the work-terrain sibling of
|
|
4
|
+
`dispatch_top_tui`. It re-`snapshot()`s the workspace on a cadence and redraws — a
|
|
5
|
+
read-only board an operator leaves open to watch the plan's claimed-vs-oracle terrain
|
|
6
|
+
move as a fleet ships phases. It mutates nothing: no lease, no launch, no write path;
|
|
7
|
+
the only effect is drawing.
|
|
8
|
+
|
|
9
|
+
**Graceful degradation (the floor that always works).** `rich` is an OPTIONAL dependency
|
|
10
|
+
(the `[tui]` extra) — the kernel core stays PyYAML-only. So this module is import-light
|
|
11
|
+
at module scope (no top-level `import rich`), and `run_plan` imports rich lazily; on
|
|
12
|
+
ImportError (rich not installed) or a non-interactive stdout (a pipe / CI) it falls
|
|
13
|
+
straight through to a single `plan_board.render_frame_text` frame and returns. `dos plan`
|
|
14
|
+
therefore ALWAYS works — `dos plan --once` and a piped `dos plan` print the plain frame
|
|
15
|
+
everywhere; the live redraw is the enhancement where rich is present and stdout is a tty.
|
|
16
|
+
Exactly the lazy-import + plain-floor split `dispatch_top_tui` uses.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import sys
|
|
22
|
+
import time
|
|
23
|
+
|
|
24
|
+
from dos import config as _config
|
|
25
|
+
from dos import plan_board as _board
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _render_once(cfg, **kw) -> str:
|
|
29
|
+
"""One plain-text frame — the floor and the `--once` body."""
|
|
30
|
+
return _board.render_frame_text(_board.snapshot(cfg, **kw))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def run_plan(
|
|
34
|
+
config=None, *, once: bool = False, interval: float = 5.0,
|
|
35
|
+
rows=None, source_name: str | None = None,
|
|
36
|
+
) -> int:
|
|
37
|
+
"""Run the live `dos plan` screen, or print one frame and return.
|
|
38
|
+
|
|
39
|
+
Returns a process exit code (always 0 — a read-only viewer has nothing to fail).
|
|
40
|
+
`once=True`, a non-interactive stdout, or a missing `rich` all collapse to a single
|
|
41
|
+
plain-text frame. Otherwise a `rich.live` loop redraws every `interval` seconds until
|
|
42
|
+
the operator interrupts (Ctrl-C), which exits cleanly — there is no state to unwind.
|
|
43
|
+
|
|
44
|
+
``rows`` / ``source_name`` thread the CLI's row-source choice through to each
|
|
45
|
+
`snapshot()` so the live loop re-harvests the same source on every tick.
|
|
46
|
+
"""
|
|
47
|
+
cfg = _config.ensure(config)
|
|
48
|
+
snap_kw = {"rows": rows, "source_name": source_name}
|
|
49
|
+
|
|
50
|
+
interactive = bool(getattr(sys.stdout, "isatty", lambda: False)())
|
|
51
|
+
if once or not interactive:
|
|
52
|
+
print(_render_once(cfg, **snap_kw))
|
|
53
|
+
return 0
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
from rich.console import Console
|
|
57
|
+
from rich.live import Live
|
|
58
|
+
except ImportError:
|
|
59
|
+
print(_render_once(cfg, **snap_kw))
|
|
60
|
+
print("\n(install `dos-kernel[tui]` for the live auto-refreshing screen)")
|
|
61
|
+
return 0
|
|
62
|
+
|
|
63
|
+
console = Console()
|
|
64
|
+
interval = max(0.5, float(interval))
|
|
65
|
+
try:
|
|
66
|
+
with Live(_renderable(cfg, **snap_kw), console=console, screen=True,
|
|
67
|
+
auto_refresh=False, transient=True) as live:
|
|
68
|
+
while True:
|
|
69
|
+
live.update(_renderable(cfg, **snap_kw), refresh=True)
|
|
70
|
+
time.sleep(interval)
|
|
71
|
+
except KeyboardInterrupt:
|
|
72
|
+
print(_render_once(cfg, **snap_kw))
|
|
73
|
+
return 0
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _renderable(cfg, **kw):
|
|
77
|
+
"""Build the rich renderable for one frame, or a plain string if rich is gone.
|
|
78
|
+
|
|
79
|
+
Reuses the pure plain-text section renderers as the panel bodies — one source of
|
|
80
|
+
truth for content; rich only adds the frame/colour. The phases panel border goes red
|
|
81
|
+
when any divergence is present, so the operator's eye lands on the cell the screen
|
|
82
|
+
exists to surface (the headline made visual)."""
|
|
83
|
+
frame = _board.snapshot(cfg, **kw)
|
|
84
|
+
try:
|
|
85
|
+
from rich.console import Group
|
|
86
|
+
from rich.panel import Panel
|
|
87
|
+
from rich.text import Text
|
|
88
|
+
except Exception: # pragma: no cover - rich present in the live branch
|
|
89
|
+
return _board.render_frame_text(frame)
|
|
90
|
+
|
|
91
|
+
def _panel(title: str, body: str, style: str) -> Panel:
|
|
92
|
+
return Panel(Text(body), title=f"[bold]{title}[/]", border_style=style,
|
|
93
|
+
title_align="left")
|
|
94
|
+
|
|
95
|
+
def _body(text: str) -> str:
|
|
96
|
+
lines = text.splitlines()
|
|
97
|
+
return "\n".join(lines[1:]) if len(lines) > 1 else ""
|
|
98
|
+
|
|
99
|
+
divergent = frame.summary()["divergent"]
|
|
100
|
+
phases_style = "red" if divergent else "cyan"
|
|
101
|
+
header = Text(
|
|
102
|
+
f"dos plan · {frame.workspace} · {frame.now_iso}"
|
|
103
|
+
+ ("" if frame.initialized else " (no dos.toml — generic main/global)")
|
|
104
|
+
+ (f" ⚠ {divergent} divergent" if divergent else ""),
|
|
105
|
+
style="bold red" if divergent else "bold cyan",
|
|
106
|
+
)
|
|
107
|
+
return Group(
|
|
108
|
+
header,
|
|
109
|
+
_panel("phases (claimed vs oracle)", _body(_board.render_phases_text(frame.phases)),
|
|
110
|
+
phases_style),
|
|
111
|
+
_panel("recent commits", _body(_board.render_activity_text(frame.activity)), "green"),
|
|
112
|
+
Text("read-only · Ctrl-C to quit · this screen mutates nothing", style="dim"),
|
|
113
|
+
)
|