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/trace.py ADDED
@@ -0,0 +1,524 @@
1
+ """`dos trace <run_id>` — the cross-surface join, as a read-only projection (docs/137).
2
+
3
+ DOS has a correlation **spine** (`run_id` + `parent_id`/`root_id`, sortable,
4
+ lineage-carrying — `run_id.py`, docs/64) and three durable surfaces that compose:
5
+ the WAL (`lane_journal`), the intent ledger (`intent.jsonl`, docs/107), and git.
6
+ What it lacked was a **verb to walk from one part to the others** — "show me
7
+ everything this run touched: its lineage, the lanes it held / was refused, the
8
+ steps it claimed vs the kernel verified, the commits it landed." This module is
9
+ that walk.
10
+
11
+ It is a **read-only projection**, never a store — the same posture as
12
+ `dos decisions` / `dos top` / `dos plan` (`decisions.py`'s module doc states the
13
+ contract): it stores nothing of its own, takes no lease, mints no belief, and
14
+ adjudicates *nothing new*. `build_trace` joins surfaces that already persist their
15
+ own truth, by the id that already exists (`run_id`) — it does NOT mint a second
16
+ parallel `correlation_id`/`trace_id`/UUID (that would be a second spine to keep in
17
+ sync, the `memory-is-an-unverified-agent` failure at the identity layer). Delete
18
+ this module and you lose the reader, not any data.
19
+
20
+ **The join keys are the existing ones, nothing fabricated** (docs/137 non-goal):
21
+
22
+ run_id spine ↔ intent ledger ↔ WAL (after docs/118 Size S stamped it
23
+ onto the ACQUIRE — the *grant* side, completing what the
24
+ *refuse* side already had)
25
+ (loop_ts, lane) within the WAL (a lease's identity)
26
+ SHA intent ledger (`start_sha`, `STEP_VERIFIED.sha`) ↔ git
27
+
28
+ **The honesty discipline is inherited verbatim** (docs/103 / docs/118): the
29
+ *adjudicated* columns (verified steps, commits, refusals) come from git ancestry +
30
+ the WAL, never the agent's self-report; `claimed` is shown as *believed* and
31
+ labelled as such, beside the verified column, with the residual (declared −
32
+ verified) visible. A lease event that carries no `run_id` (a pre-Build-1 ACQUIRE,
33
+ or a writer that didn't pass one) is surfaced under an explicit `(unattributed)`
34
+ note — NEVER silently dropped and NEVER guessed onto this run by a time window
35
+ (the docs/118 "fail toward no-match" rule). `trace` over a workspace that never
36
+ journaled with run-ids therefore honestly reports "0 attributable lease events"
37
+ rather than a fabricated lane list.
38
+
39
+ Pure-stdlib, read-only — the readers (`run_id.read_run_json`,
40
+ `intent_ledger.read_all`, `lane_journal.read_all`, `git_delta.commits_since`) are
41
+ the only I/O, reused verbatim so `trace` defines no new read; the assembly +
42
+ ranking + render are pure and are the unit-test surface (mirrors
43
+ `decisions.collect_decisions` / `timeline.build_timeline`).
44
+ """
45
+
46
+ from __future__ import annotations
47
+
48
+ import io
49
+ import sys
50
+ from dataclasses import dataclass, field
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 git_delta as _git_delta
62
+ from dos import intent_ledger as _intent
63
+ from dos import lane_journal as _lj
64
+ from dos import run_id as _run_id
65
+
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # The joined value objects — each a pure projection of one surface, plus the
69
+ # frame that carries all four. Frozen + `to_dict()` so `--json` round-trips
70
+ # (mirrors `decisions.Decision`).
71
+ # ---------------------------------------------------------------------------
72
+
73
+
74
+ @dataclass(frozen=True)
75
+ class LeaseEvent:
76
+ """One WAL event attributed to this run — a row in the lanes column.
77
+
78
+ `op` is the journal op (ACQUIRE/RELEASE/HEARTBEAT/SCAVENGE/REFUSE/HALT);
79
+ `lane` + `loop_ts` are the `(loop_ts, lane)` lease identity; `reason` is the
80
+ recorded prose; `attributed_by` names HOW this event was tied to the run —
81
+ `lease.run_id` (the docs/118 grant-side join), `entry.run_id` (a refuse/halt
82
+ that carried it), so a reader can see the join is real, not a time guess.
83
+ """
84
+
85
+ op: str
86
+ lane: str
87
+ loop_ts: str
88
+ ts: str
89
+ reason: str
90
+ attributed_by: str # "lease.run_id" | "entry.run_id"
91
+ reason_class: str = ""
92
+
93
+ def to_dict(self) -> dict:
94
+ return {
95
+ "op": self.op,
96
+ "lane": self.lane,
97
+ "loop_ts": self.loop_ts,
98
+ "ts": self.ts,
99
+ "reason": self.reason,
100
+ "reason_class": self.reason_class,
101
+ "attributed_by": self.attributed_by,
102
+ }
103
+
104
+
105
+ @dataclass(frozen=True)
106
+ class StepRow:
107
+ """One declared step, with its believed (claimed) vs adjudicated (verified) state.
108
+
109
+ The epistemic surface (docs/107 §3.2): `claimed_sha` is the agent's distrusted
110
+ self-report; `verified_sha`/`verified_via` is the kernel's minted belief (the
111
+ only "done"). `state` is the folded verdict: VERIFIED (kernel-confirmed),
112
+ CLAIMED (self-reported but not confirmed — the residual a resume would re-do),
113
+ or PENDING (declared, no claim yet).
114
+ """
115
+
116
+ step_id: str
117
+ state: str # "VERIFIED" | "CLAIMED" | "PENDING"
118
+ claimed_sha: str = ""
119
+ verified_sha: str = ""
120
+ verified_via: str = ""
121
+
122
+ def to_dict(self) -> dict:
123
+ return {
124
+ "step_id": self.step_id,
125
+ "state": self.state,
126
+ "claimed_sha": self.claimed_sha,
127
+ "verified_sha": self.verified_sha,
128
+ "verified_via": self.verified_via,
129
+ }
130
+
131
+
132
+ @dataclass(frozen=True)
133
+ class TraceFrame:
134
+ """The full cross-surface join for one run — what every reader/renderer consumes.
135
+
136
+ Assembled by `build_trace`. Carries the four surfaces side by side:
137
+ * spine — the run's own id + lineage (parent/root/process), + the
138
+ ancestors/descendants found by a `root_id` scan.
139
+ * intent — the declared goal/plan/phase + `start_sha`, and the step rows
140
+ (claimed-vs-verified, the residual).
141
+ * wal — the lease events attributed to this run + the count of
142
+ ACQUIREs seen on the workspace that carried NO run_id
143
+ (`unattributed_acquires`, the honest "couldn't join" tally).
144
+ * git — the commits since `start_sha` (the forward git delta).
145
+ Every list is empty-on-missing-surface (the reader degrades, never crashes).
146
+ """
147
+
148
+ run_id: str
149
+ found: bool # was there ANY surface for this run_id?
150
+ # spine
151
+ run_json: dict = field(default_factory=dict)
152
+ parent_id: str = ""
153
+ root_id: str = ""
154
+ process_id: str = ""
155
+ ancestors: tuple[str, ...] = () # run_ids up the lineage (parent → root)
156
+ descendants: tuple[str, ...] = () # run_ids whose parent chain reaches this one
157
+ # intent
158
+ has_intent: bool = False
159
+ goal: str = ""
160
+ plan: str = ""
161
+ phase: str = ""
162
+ start_sha: str = ""
163
+ steps: tuple[StepRow, ...] = ()
164
+ corrupt_ledger_lines: int = 0
165
+ unreadable_newer: bool = False
166
+ # wal
167
+ lease_events: tuple[LeaseEvent, ...] = ()
168
+ unattributed_acquires: int = 0
169
+ # git
170
+ commits: tuple[dict, ...] = ()
171
+
172
+ @property
173
+ def residual(self) -> tuple[str, ...]:
174
+ """Declared steps NOT kernel-verified — the resume residual (docs/107)."""
175
+ return tuple(s.step_id for s in self.steps if s.state != "VERIFIED")
176
+
177
+ def to_dict(self) -> dict:
178
+ return {
179
+ "run_id": self.run_id,
180
+ "found": self.found,
181
+ "spine": {
182
+ "run_json": self.run_json,
183
+ "parent_id": self.parent_id,
184
+ "root_id": self.root_id,
185
+ "process_id": self.process_id,
186
+ "ancestors": list(self.ancestors),
187
+ "descendants": list(self.descendants),
188
+ },
189
+ "intent": {
190
+ "has_intent": self.has_intent,
191
+ "goal": self.goal,
192
+ "plan": self.plan,
193
+ "phase": self.phase,
194
+ "start_sha": self.start_sha,
195
+ "steps": [s.to_dict() for s in self.steps],
196
+ "residual": list(self.residual),
197
+ "corrupt_ledger_lines": self.corrupt_ledger_lines,
198
+ "unreadable_newer": self.unreadable_newer,
199
+ },
200
+ "wal": {
201
+ "lease_events": [e.to_dict() for e in self.lease_events],
202
+ "unattributed_acquires": self.unattributed_acquires,
203
+ },
204
+ "git": {"commits": list(self.commits)},
205
+ }
206
+
207
+
208
+ # ---------------------------------------------------------------------------
209
+ # Surface readers — each returns its slice; all degrade to empty on a missing /
210
+ # malformed surface (the `decisions.py` defensive-loader posture).
211
+ # ---------------------------------------------------------------------------
212
+
213
+
214
+ def _read_spine(run_id: str, cfg) -> dict:
215
+ """The run's own `run.json` (or {} if the run-dir/stamp is absent)."""
216
+ run_dir = _intent.run_dir_for(run_id, cfg=cfg)
217
+ return _run_id.read_run_json(run_dir) or {}
218
+
219
+
220
+ def _scan_lineage(run_id: str, root_id: str, cfg) -> tuple[tuple[str, ...], tuple[str, ...]]:
221
+ """(ancestors, descendants) for `run_id` from a single scan of the run-dirs.
222
+
223
+ Reads every sibling run-dir's `run.json` once and builds the parent map, then:
224
+ * ancestors — walk `parent_id` up from this run to its root.
225
+ * descendants — every run whose ancestor chain passes through this run.
226
+ Pure over the materialized map. Cheap (one stat+read per run-dir) and OPTIONAL —
227
+ a missing runs/ dir yields ((), ()), the same empty degrade as every reader.
228
+ The `root_id` narrows the scan to this run's tree when set.
229
+ """
230
+ runs_dir = cfg.paths.fanout_runs
231
+ if not runs_dir.exists():
232
+ return ((), ())
233
+ parent_of: dict[str, str] = {}
234
+ root_of: dict[str, str] = {}
235
+ try:
236
+ children = list(runs_dir.iterdir())
237
+ except OSError:
238
+ return ((), ())
239
+ for d in children:
240
+ if not d.is_dir():
241
+ continue
242
+ data = _run_id.read_run_json(d)
243
+ if not isinstance(data, dict):
244
+ continue
245
+ rid = str(data.get("run_id") or d.name)
246
+ parent_of[rid] = str(data.get("parent_id") or "")
247
+ root_of[rid] = str(data.get("root_id") or "")
248
+
249
+ # Ancestors: walk parent links up from this run (guard against a cycle).
250
+ ancestors: list[str] = []
251
+ seen: set[str] = {run_id}
252
+ cur = parent_of.get(run_id, "")
253
+ while cur and cur not in seen:
254
+ ancestors.append(cur)
255
+ seen.add(cur)
256
+ cur = parent_of.get(cur, "")
257
+
258
+ # Descendants: a run is below us iff walking ITS parent chain reaches us.
259
+ def _reaches(rid: str) -> bool:
260
+ hops = 0
261
+ cur = parent_of.get(rid, "")
262
+ local: set[str] = set()
263
+ while cur and cur not in local and hops < 10000:
264
+ if cur == run_id:
265
+ return True
266
+ local.add(cur)
267
+ cur = parent_of.get(cur, "")
268
+ hops += 1
269
+ return False
270
+
271
+ descendants = sorted(
272
+ rid for rid in parent_of
273
+ if rid != run_id and _reaches(rid)
274
+ )
275
+ return (tuple(ancestors), tuple(descendants))
276
+
277
+
278
+ def _read_intent(run_id: str, cfg) -> tuple[_intent.LedgerState, tuple[StepRow, ...]]:
279
+ """Replay the run's intent ledger → (LedgerState, step rows).
280
+
281
+ The step rows fold claimed-vs-verified into a single per-step verdict: a step
282
+ in `verified` is VERIFIED (the minted belief); else in `claimed` is CLAIMED
283
+ (self-reported, not confirmed — the residual); else PENDING. Declared order is
284
+ preserved; a claimed/verified step NOT in `declared_steps` (a run that claimed
285
+ a step it never declared) is appended after, so nothing is hidden.
286
+ """
287
+ entries = _intent.read_all(run_id, cfg=cfg)
288
+ state = _intent.replay(entries)
289
+ rows: list[StepRow] = []
290
+ seen: set[str] = set()
291
+
292
+ def _row_for(sid: str) -> StepRow:
293
+ v = state.verified.get(sid)
294
+ if v is not None:
295
+ return StepRow(step_id=sid, state="VERIFIED",
296
+ claimed_sha=state.claimed.get(sid, ""),
297
+ verified_sha=v.sha, verified_via=v.via)
298
+ if sid in state.claimed:
299
+ return StepRow(step_id=sid, state="CLAIMED",
300
+ claimed_sha=state.claimed.get(sid, ""))
301
+ return StepRow(step_id=sid, state="PENDING")
302
+
303
+ for sid in state.declared_steps:
304
+ rows.append(_row_for(sid))
305
+ seen.add(sid)
306
+ # Claimed/verified steps the run never declared — surface them, don't hide.
307
+ for sid in list(state.verified.keys()) + list(state.claimed.keys()):
308
+ if sid not in seen:
309
+ rows.append(_row_for(sid))
310
+ seen.add(sid)
311
+ return state, tuple(rows)
312
+
313
+
314
+ def _read_wal(run_id: str, cfg) -> tuple[tuple[LeaseEvent, ...], int]:
315
+ """(lease events attributed to this run, count of unattributed ACQUIREs).
316
+
317
+ Joins the WAL to the spine on `run_id`, two ways:
318
+ * a lease event (ACQUIRE/RELEASE/HEARTBEAT/SCAVENGE) whose reconstructed
319
+ live-lease carries `run_id` (the docs/118 grant-side join) — attributed
320
+ via "lease.run_id".
321
+ * a REFUSE/HALT entry whose top-level `run_id` matches — attributed via
322
+ "entry.run_id" (the refuse side already carried it).
323
+
324
+ An ACQUIRE seen on the workspace that carries NO `run_id` is COUNTED (so the
325
+ operator sees the join's coverage) but NEVER attributed to this run by time —
326
+ the docs/118 "fail toward no-match" rule. RELEASE/HEARTBEAT carry no lease body
327
+ of their own, so they are attributed via the live lease they fold against:
328
+ `replay` is what propagates the ACQUIRE's `run_id` onto the live lease, so we
329
+ match a RELEASE/HEARTBEAT by its `(loop_ts, lane)` identity belonging to a
330
+ run-attributed live lease.
331
+ """
332
+ path = cfg.paths.lane_journal
333
+ try:
334
+ entries = _lj.read_all(path)
335
+ except Exception:
336
+ return ((), 0)
337
+
338
+ # First pass: which (loop_ts, lane) identities belong to THIS run, learned from
339
+ # any ACQUIRE that carries the run_id (on the nested lease or inline). This lets
340
+ # a later RELEASE/HEARTBEAT — which has no lease body — be attributed by identity.
341
+ identities: set[tuple[str, str]] = set()
342
+ unattributed_acquires = 0
343
+ for e in entries:
344
+ if str(e.get("op") or "") != _lj.OP_ACQUIRE:
345
+ continue
346
+ lease = e.get("lease") if isinstance(e.get("lease"), dict) else e
347
+ rid = str((lease.get("run_id") if isinstance(lease, dict) else "") or e.get("run_id") or "")
348
+ if rid == run_id:
349
+ identities.add(_lj._lease_identity(e))
350
+ elif not rid:
351
+ unattributed_acquires += 1
352
+
353
+ out: list[LeaseEvent] = []
354
+ for e in entries:
355
+ op = str(e.get("op") or "")
356
+ ts = str(e.get("ts") or "")
357
+ reason = str(e.get("reason") or "")
358
+ if op in (_lj.OP_REFUSE, _lj.OP_HALT):
359
+ # The refuse/halt side already carries run_id at the top level.
360
+ if str(e.get("run_id") or "") != run_id:
361
+ continue
362
+ out.append(LeaseEvent(
363
+ op=op, lane=str(e.get("lane") or ""),
364
+ loop_ts=str(e.get("loop_ts") or ""), ts=ts, reason=reason,
365
+ attributed_by="entry.run_id",
366
+ reason_class=str(e.get("reason_class") or ""),
367
+ ))
368
+ continue
369
+ if op in (_lj.OP_ACQUIRE, _lj.OP_RELEASE, _lj.OP_HEARTBEAT,
370
+ _lj.OP_SCAVENGE):
371
+ ident = _lj._lease_identity(e)
372
+ if ident not in identities:
373
+ continue # not this run's lease (or an unattributed ACQUIRE)
374
+ out.append(LeaseEvent(
375
+ op=op, lane=ident[1], loop_ts=ident[0], ts=ts, reason=reason,
376
+ attributed_by="lease.run_id",
377
+ ))
378
+ return (tuple(out), unattributed_acquires)
379
+
380
+
381
+ def build_trace(run_id: str, config=None) -> TraceFrame:
382
+ """Assemble the full cross-surface join for `run_id` (docs/137). Read-only.
383
+
384
+ Joins the spine (`run.json` + lineage), the intent ledger (claimed-vs-verified),
385
+ the WAL (lease events attributed by `run_id`), and git (commits since
386
+ `start_sha`) — every join by an id that already exists, nothing fabricated.
387
+ `found` is True iff ANY surface had something for this run (a stamp, a ledger,
388
+ or a WAL event); a wholly unknown run_id yields `found=False` and empty slices.
389
+ """
390
+ cfg = config if config is not None else _config.active()
391
+
392
+ spine = _read_spine(run_id, cfg)
393
+ parent_id = str(spine.get("parent_id") or "")
394
+ root_id = str(spine.get("root_id") or run_id)
395
+ process_id = str(spine.get("process_id") or "")
396
+ ancestors, descendants = _scan_lineage(run_id, root_id, cfg)
397
+
398
+ state, steps = _read_intent(run_id, cfg)
399
+ lease_events, unattributed = _read_wal(run_id, cfg)
400
+ commits = tuple(_git_delta.commits_since(state.start_sha, root=cfg.root)) \
401
+ if state.start_sha else ()
402
+
403
+ found = bool(spine) or state.has_intent or bool(lease_events) \
404
+ or bool(state.claimed) or bool(state.verified)
405
+
406
+ return TraceFrame(
407
+ run_id=run_id,
408
+ found=found,
409
+ run_json=spine,
410
+ parent_id=parent_id,
411
+ root_id=root_id,
412
+ process_id=process_id,
413
+ ancestors=ancestors,
414
+ descendants=descendants,
415
+ has_intent=state.has_intent,
416
+ goal=state.goal,
417
+ plan=state.plan,
418
+ phase=state.phase,
419
+ start_sha=state.start_sha,
420
+ steps=steps,
421
+ corrupt_ledger_lines=state.corrupt_lines,
422
+ unreadable_newer=state.unreadable_newer,
423
+ lease_events=lease_events,
424
+ unattributed_acquires=unattributed,
425
+ commits=commits,
426
+ )
427
+
428
+
429
+ # ---------------------------------------------------------------------------
430
+ # Rendering — the plain text floor (provenance-first) + JSON.
431
+ # ---------------------------------------------------------------------------
432
+
433
+
434
+ _STEP_GLYPH = {"VERIFIED": "OK ", "CLAIMED": "?? ", "PENDING": "···"}
435
+
436
+
437
+ def render_text(t: TraceFrame) -> str:
438
+ """The operator-facing walk: lineage → intent → lanes → commits.
439
+
440
+ Provenance-first: who the run is, what it tried, what it touched, what it
441
+ actually shipped — believed (claimed) beside adjudicated (verified). Reuses the
442
+ small-column idiom of `decisions.render_list_plain` / `timeline.render_text`.
443
+ """
444
+ out: list[str] = []
445
+ out.append(f"# trace · {t.run_id}")
446
+ if not t.found:
447
+ out.append(f" no surface found for run {t.run_id} "
448
+ f"(no run.json, no intent ledger, no attributed WAL event)")
449
+ return "\n".join(out)
450
+
451
+ # --- spine / lineage ---------------------------------------------------
452
+ out.append("")
453
+ out.append("## spine")
454
+ out.append(f" process {t.process_id or '-'}")
455
+ out.append(f" parent {t.parent_id or '-(root)'}")
456
+ out.append(f" root {t.root_id or t.run_id}"
457
+ + (" (this run is the root)" if t.root_id in ("", t.run_id) else ""))
458
+ if t.ancestors:
459
+ out.append(f" ancestry {' → '.join(t.ancestors)}")
460
+ if t.descendants:
461
+ shown = list(t.descendants[:6])
462
+ more = f" (+{len(t.descendants) - 6} more)" if len(t.descendants) > 6 else ""
463
+ out.append(f" children {', '.join(shown)}{more}")
464
+
465
+ # --- intent (claimed vs verified) -------------------------------------
466
+ out.append("")
467
+ out.append("## intent")
468
+ if not t.has_intent:
469
+ out.append(" (no INTENT declared — this run recorded no goal/plan/steps)")
470
+ else:
471
+ if t.goal:
472
+ out.append(f" goal {t.goal[:120]}")
473
+ if t.plan or t.phase:
474
+ out.append(f" target {t.plan or '-'} :: {t.phase or '-'}")
475
+ out.append(f" start {t.start_sha or '-'}")
476
+ if t.steps:
477
+ n_ver = sum(1 for s in t.steps if s.state == "VERIFIED")
478
+ out.append(f" steps {n_ver}/{len(t.steps)} kernel-verified "
479
+ f"(believed-claimed shown beside adjudicated-verified):")
480
+ for s in t.steps:
481
+ glyph = _STEP_GLYPH.get(s.state, s.state)
482
+ detail = ""
483
+ if s.state == "VERIFIED":
484
+ detail = f"sha={s.verified_sha[:10]} via={s.verified_via or '?'}"
485
+ elif s.state == "CLAIMED":
486
+ detail = f"claimed sha={s.claimed_sha[:10]} — NOT kernel-verified"
487
+ out.append(f" {glyph} {s.step_id:<22} {detail}")
488
+ if t.residual:
489
+ out.append(f" residual {len(t.residual)} step(s) not verified: "
490
+ f"{', '.join(t.residual[:8])}"
491
+ + (" …" if len(t.residual) > 8 else ""))
492
+ if t.corrupt_ledger_lines:
493
+ note = " (incl. a record this kernel is too OLD to read — migrate)" \
494
+ if t.unreadable_newer else ""
495
+ out.append(f" ⚠ ledger {t.corrupt_ledger_lines} corrupt/unreadable line(s){note}")
496
+
497
+ # --- WAL (lanes held / refused) ---------------------------------------
498
+ out.append("")
499
+ out.append("## lanes (WAL)")
500
+ if not t.lease_events:
501
+ out.append(" (no lease event attributed to this run by run_id)")
502
+ else:
503
+ header = f" {'op':<10} {'lane':<12} {'loop_ts':<22} via"
504
+ out.append(header)
505
+ out.append(" " + "-" * (len(header) - 2))
506
+ for e in t.lease_events:
507
+ out.append(f" {e.op:<10} {(e.lane or '-'):<12} "
508
+ f"{(e.loop_ts or '-'):<22} {e.attributed_by}")
509
+ if t.unattributed_acquires:
510
+ out.append(f" note: {t.unattributed_acquires} ACQUIRE(s) on this workspace "
511
+ f"carried NO run_id and were NOT attributed by time "
512
+ f"(stamp --run-id to join them; docs/137)")
513
+
514
+ # --- git --------------------------------------------------------------
515
+ out.append("")
516
+ out.append("## commits since start")
517
+ if not t.commits:
518
+ out.append(f" (none since {t.start_sha or '-'})")
519
+ else:
520
+ for c in t.commits[:20]:
521
+ out.append(f" {str(c.get('sha',''))[:10]} {str(c.get('subject',''))[:72]}")
522
+ if len(t.commits) > 20:
523
+ out.append(f" (+{len(t.commits) - 20} more)")
524
+ return "\n".join(out)
dos/verdict.py ADDED
@@ -0,0 +1,140 @@
1
+ """The typed-verdict contract — one ABI the distrust verbs share (docs/86).
2
+
3
+ `liveness` and `scope` are two instances of the *same* shape, and `verify`'s
4
+ `ShipVerdict` is a third that drifted. This module NAMES that shape as a contract
5
+ so a third party can add a verdict the way you add a device driver — and so the
6
+ CLI, the `dos decisions` queue, and the MCP server can treat every verdict
7
+ uniformly instead of special-casing each verb.
8
+
9
+ It is deliberately written **after** there were two real instances to generalize
10
+ from (`liveness.classify`, `scope.classify`) — the "generalize last" discipline
11
+ (docs/86 §4): a contract invented before its instances is machinery imported
12
+ ahead of need. This module adds NO behavior to the existing verbs; it is a pure
13
+ description (Protocols) plus a conformance check a future verdict registry can
14
+ call. Nothing here is imported by `liveness`/`scope`/`oracle` — the arrow points
15
+ the other way (a consumer reads this; the verbs don't depend on it).
16
+
17
+ The contract, in one line:
18
+
19
+ classify(Evidence, Policy) -> Verdict[V]
20
+
21
+ where the pieces are exactly what `liveness`/`scope` already are:
22
+
23
+ Evidence — a frozen dataclass of facts GATHERED BY THE CALLER (no I/O inside
24
+ the verdict). The git read / config read / clock lives at the CLI
25
+ boundary; `classify` receives the already-gathered facts. This is
26
+ what lets a verdict be replay-tested on frozen fixtures.
27
+ Policy — a frozen dataclass of thresholds, declarable per-workspace in
28
+ `dos.toml [<verb>]` (the `[liveness]`/`[scope]` seam), defaults
29
+ GENERIC. "Mechanism is kernel, knobs are config."
30
+ Verdict — the typed answer: a CLOSED-ENUM `verdict` (never a bare bool), a
31
+ one-line operator-facing `reason`, the echoed `evidence`, and a
32
+ `to_dict()` for the JSON/MCP/renderer seam. The provenance — which
33
+ rung/signal answered — is carried IN the verdict (liveness's
34
+ `evidence`, scope's spill lists, `ShipVerdict.source`).
35
+
36
+ This is the three kernel design laws fused into one type: *typed verdict over
37
+ binary gate* (the closed enum), *evidence over narrative* (Evidence is gathered
38
+ from artifacts, never the agent's claim), and *the give lives in provenance*
39
+ (docs/76 — the rung, not the adjudication, is what flexes).
40
+
41
+ SCOPE — the contract is for the EPISTEMIC verbs only: the ones that answer "is
42
+ this claim about ground-truth state true?" (`verify`, `liveness`, `scope`, and
43
+ the candidates `acceptance`/`identity`). `arbitrate()` and `spawn/reap` share the
44
+ `classify` *shape* (state-in → typed-out, pure) but their output is an EFFECT
45
+ decision (acquire/refuse) or an IDENTITY record, not a belief about the world —
46
+ they are cousins, not members. Forcing them under this Protocol would make it a
47
+ god-type that means nothing; the boundary is part of the contract.
48
+
49
+ The four-gate registration test a verdict must pass to be a kernel verb (docs/85
50
+ §2, enforced by a future `dos.verdicts.register`): (1) it answers a claim about
51
+ ground-truth state; (2) its evidence is unforgeable by the agent; (3) it is
52
+ domain-free; (4) its verdict is a mechanical closed enum. Fail (1) → it is an
53
+ advisory JUDGE (`drivers/llm_judge`), not a verdict. Fail (3) → it is a driver
54
+ oracle on the seam, not a kernel verb. Open set of verbs, CLOSED shape per verb —
55
+ the closed-enum-as-data hackability pattern (HACKING.md) lifted from vocabularies
56
+ to verdicts.
57
+ """
58
+
59
+ from __future__ import annotations
60
+
61
+ from typing import Any, Protocol, runtime_checkable
62
+
63
+
64
+ @runtime_checkable
65
+ class TypedVerdict(Protocol):
66
+ """The shape every distrust verdict returns. `liveness.LivenessVerdict` and
67
+ `scope.ScopeVerdict` satisfy it structurally (no inheritance needed — this is
68
+ a duck-typed Protocol).
69
+
70
+ verdict — a `str`-valued Enum member (the closed verdict vocabulary). The
71
+ `str` base lets it round-trip through a CLI token / exit-code map
72
+ without a lookup table; `.value` is the wire form.
73
+ reason — a one-line, operator-facing summary of WHY this verdict, naming
74
+ the driving evidence (legible distrust — the renderer seam).
75
+ to_dict — the JSON shape: the verdict, the reason, and the evidence behind
76
+ it, so `--output json` / an MCP tool emits the answer AND its
77
+ grounds in one object.
78
+ """
79
+
80
+ @property
81
+ def verdict(self) -> Any: # a str-valued enum member
82
+ ...
83
+
84
+ @property
85
+ def reason(self) -> str:
86
+ ...
87
+
88
+ def to_dict(self) -> dict:
89
+ ...
90
+
91
+
92
+ @runtime_checkable
93
+ class Classifier(Protocol):
94
+ """A pure verdict function: `classify(evidence, policy) -> TypedVerdict`.
95
+
96
+ The shared signature of `liveness.classify` and `scope.classify`. `policy`
97
+ carries a default, so a caller may invoke `classify(evidence)`; a verb's
98
+ `gather()` (the boundary I/O) produces the `evidence` argument. A `Classifier`
99
+ MUST NOT perform I/O — that is the property the conformance check below cannot
100
+ enforce statically but every kernel verb's test suite poisons `subprocess`/
101
+ `open`/`time` to prove (see `test_liveness`/`test_scope`).
102
+ """
103
+
104
+ def __call__(self, evidence: Any, policy: Any = ..., /) -> TypedVerdict:
105
+ ...
106
+
107
+
108
+ def conforms(verdict_obj: Any) -> bool:
109
+ """True when `verdict_obj` satisfies the `TypedVerdict` contract at runtime.
110
+
111
+ The check a future `dos.verdicts.register` runs on a third party's verdict
112
+ sample before exposing it as a `dos <verb>` subcommand / a decisions-queue row
113
+ / an MCP tool. Structural, not nominal: any object with a `str`-enum `verdict`,
114
+ a `str` `reason`, and a callable `to_dict()` whose result is JSON-shaped passes
115
+ — no base class required.
116
+
117
+ Note this validates the *verdict value's* shape, which is gate (4) of the
118
+ four-gate test (a mechanical closed enum + a legible reason). Gates (1)–(3)
119
+ — ground-truth claim, unforgeable evidence, domain-free — are properties of
120
+ the verb's DESIGN that a runtime check cannot see; they stay a review
121
+ responsibility (the contract documents them so the reviewer has the checklist).
122
+ """
123
+ if not isinstance(verdict_obj, TypedVerdict):
124
+ return False
125
+ # `verdict` must be a str-valued enum member (the closed vocabulary), not a
126
+ # bare bool or free string — the "typed verdict over binary gate" law.
127
+ import enum
128
+
129
+ v = verdict_obj.verdict
130
+ if not isinstance(v, enum.Enum) or not isinstance(v, str):
131
+ return False
132
+ if not isinstance(verdict_obj.reason, str):
133
+ return False
134
+ try:
135
+ d = verdict_obj.to_dict()
136
+ except Exception:
137
+ return False
138
+ if not isinstance(d, dict) or "verdict" not in d:
139
+ return False
140
+ return True