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.
Files changed (178) hide show
  1. dos/__init__.py +261 -0
  2. dos/_bin/dos-hook.exe +0 -0
  3. dos/_filelock.py +255 -0
  4. dos/_job_policy.py +97 -0
  5. dos/_tree.py +145 -0
  6. dos/admission.py +433 -0
  7. dos/answer_shape.py +299 -0
  8. dos/arbiter.py +859 -0
  9. dos/archive_lock.py +266 -0
  10. dos/arg_provenance.py +814 -0
  11. dos/attest.py +472 -0
  12. dos/breaker.py +311 -0
  13. dos/churn.py +226 -0
  14. dos/claim_extract.py +229 -0
  15. dos/claim_ttl.py +150 -0
  16. dos/cli.py +8721 -0
  17. dos/commit_audit.py +666 -0
  18. dos/completion.py +466 -0
  19. dos/concurrency_class.py +154 -0
  20. dos/config.py +1380 -0
  21. dos/config_lint.py +464 -0
  22. dos/cooldown.py +390 -0
  23. dos/coverage.py +387 -0
  24. dos/dangling_intent.py +287 -0
  25. dos/data_class.py +397 -0
  26. dos/decisions.py +1274 -0
  27. dos/decisions_tui.py +251 -0
  28. dos/dispatch_top.py +740 -0
  29. dos/dispatch_top_tui.py +116 -0
  30. dos/drivers/__init__.py +40 -0
  31. dos/drivers/ci_status.py +630 -0
  32. dos/drivers/citation_resolve.py +703 -0
  33. dos/drivers/decision_stop.py +98 -0
  34. dos/drivers/export_file.py +173 -0
  35. dos/drivers/export_otlp.py +275 -0
  36. dos/drivers/export_statsd.py +242 -0
  37. dos/drivers/hook_dialects.py +391 -0
  38. dos/drivers/job.py +47 -0
  39. dos/drivers/llm_judge.py +360 -0
  40. dos/drivers/memory_recall.py +1231 -0
  41. dos/drivers/notify_slack.py +373 -0
  42. dos/drivers/notify_webhook.py +251 -0
  43. dos/drivers/operator_judge.py +114 -0
  44. dos/drivers/os_acceptance.py +228 -0
  45. dos/drivers/paste_log.py +132 -0
  46. dos/drivers/plan_scope.py +133 -0
  47. dos/drivers/self_improve.py +375 -0
  48. dos/drivers/similarity_judge.py +249 -0
  49. dos/drivers/state_diff.py +274 -0
  50. dos/drivers/supervisor.py +347 -0
  51. dos/drivers/watchdog.py +363 -0
  52. dos/drivers/workshop.py +160 -0
  53. dos/durable_schema.py +344 -0
  54. dos/effect_witness.py +393 -0
  55. dos/efficiency.py +318 -0
  56. dos/enforce.py +414 -0
  57. dos/enumerate.py +776 -0
  58. dos/env_print.py +378 -0
  59. dos/event_severity.py +258 -0
  60. dos/evidence.py +692 -0
  61. dos/exec_capability.py +256 -0
  62. dos/export_cursor.py +143 -0
  63. dos/exporter.py +320 -0
  64. dos/firing_label.py +353 -0
  65. dos/fleet_roll.py +226 -0
  66. dos/gate_classify.py +827 -0
  67. dos/gh4_coverage.py +179 -0
  68. dos/git_delta.py +122 -0
  69. dos/guard.py +215 -0
  70. dos/health.py +552 -0
  71. dos/help_summary.py +519 -0
  72. dos/home.py +934 -0
  73. dos/hook_binary.py +194 -0
  74. dos/hook_dialect.py +271 -0
  75. dos/hook_exit.py +191 -0
  76. dos/hook_install.py +437 -0
  77. dos/id_alloc.py +304 -0
  78. dos/improve.py +499 -0
  79. dos/intent_ledger.py +635 -0
  80. dos/interpret.py +176 -0
  81. dos/intervention.py +769 -0
  82. dos/intervention_eval.py +371 -0
  83. dos/journal_delta.py +308 -0
  84. dos/judge_eval.py +328 -0
  85. dos/judges.py +366 -0
  86. dos/lane_infer.py +127 -0
  87. dos/lane_journal.py +1001 -0
  88. dos/lane_lease.py +952 -0
  89. dos/lane_overlap.py +228 -0
  90. dos/lease_health.py +282 -0
  91. dos/lifecycle.py +211 -0
  92. dos/liveness.py +352 -0
  93. dos/lock_modes.py +185 -0
  94. dos/log_source.py +395 -0
  95. dos/loop_decide.py +1746 -0
  96. dos/marker_gate.py +254 -0
  97. dos/marker_sensor.py +396 -0
  98. dos/noop_streak.py +280 -0
  99. dos/notify.py +479 -0
  100. dos/observe.py +175 -0
  101. dos/oracle.py +1661 -0
  102. dos/overlap_eval.py +214 -0
  103. dos/overlap_policy.py +342 -0
  104. dos/packet_sidecar.py +267 -0
  105. dos/phase_shipped.py +1985 -0
  106. dos/pick_priority.py +225 -0
  107. dos/pickable.py +369 -0
  108. dos/picker_oracle.py +1037 -0
  109. dos/plan_board.py +513 -0
  110. dos/plan_board_tui.py +113 -0
  111. dos/plan_source.py +455 -0
  112. dos/posttool_sensor.py +528 -0
  113. dos/precursor_gate.py +499 -0
  114. dos/precursor_gate_eval.py +239 -0
  115. dos/preflight.py +825 -0
  116. dos/pretool_sensor.py +490 -0
  117. dos/proc_delta.py +181 -0
  118. dos/productivity.py +296 -0
  119. dos/provider_limit.py +242 -0
  120. dos/py.typed +4 -0
  121. dos/reason_morphology.py +299 -0
  122. dos/reasons.py +449 -0
  123. dos/reconcile.py +173 -0
  124. dos/recurring_wedge.py +206 -0
  125. dos/render.py +393 -0
  126. dos/result_state.py +468 -0
  127. dos/resume.py +578 -0
  128. dos/resume_evidence.py +293 -0
  129. dos/retention.py +344 -0
  130. dos/reward.py +372 -0
  131. dos/rewind.py +587 -0
  132. dos/rewind_evidence.py +168 -0
  133. dos/rewind_tokens.py +252 -0
  134. dos/run_id.py +342 -0
  135. dos/scope.py +520 -0
  136. dos/scope_source.py +382 -0
  137. dos/scout.py +982 -0
  138. dos/self_modify.py +209 -0
  139. dos/sibling_scan.py +569 -0
  140. dos/skills/EXAMPLES.md +584 -0
  141. dos/skills/dos-class-cycle/SKILL.md +107 -0
  142. dos/skills/dos-dispatch/SKILL.md +177 -0
  143. dos/skills/dos-dispatch-loop/SKILL.md +254 -0
  144. dos/skills/dos-goal-gate/SKILL.md +269 -0
  145. dos/skills/dos-next-up/SKILL.md +231 -0
  146. dos/skills/dos-promote/SKILL.md +114 -0
  147. dos/skills/dos-replan/SKILL.md +159 -0
  148. dos/skills/dos-replan-loop/SKILL.md +114 -0
  149. dos/skills/dos-self-improve/SKILL.md +213 -0
  150. dos/skills/dos-supervise-loop/SKILL.md +180 -0
  151. dos/skills/dos-unstick/SKILL.md +108 -0
  152. dos/skills/dos-witness-claim/SKILL.md +251 -0
  153. dos/stamp.py +1002 -0
  154. dos/state_health.py +387 -0
  155. dos/status.py +114 -0
  156. dos/stop_policy.py +334 -0
  157. dos/supervise.py +1014 -0
  158. dos/testwitness.py +392 -0
  159. dos/timeline.py +1027 -0
  160. dos/tokens.py +485 -0
  161. dos/tool_stream.py +393 -0
  162. dos/tool_stream_eval.py +226 -0
  163. dos/trace.py +524 -0
  164. dos/verdict.py +140 -0
  165. dos/verdict_cli.py +189 -0
  166. dos/verdict_journal.py +497 -0
  167. dos/verdict_rollup.py +217 -0
  168. dos/verdicts.py +181 -0
  169. dos/wedge_reason.py +282 -0
  170. dos_kernel-0.22.0.dist-info/METADATA +859 -0
  171. dos_kernel-0.22.0.dist-info/RECORD +178 -0
  172. dos_kernel-0.22.0.dist-info/WHEEL +5 -0
  173. dos_kernel-0.22.0.dist-info/entry_points.txt +39 -0
  174. dos_kernel-0.22.0.dist-info/licenses/LICENSE +21 -0
  175. dos_kernel-0.22.0.dist-info/top_level.txt +2 -0
  176. dos_mcp/__init__.py +52 -0
  177. dos_mcp/py.typed +2 -0
  178. 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
+ )