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/intent_ledger.py ADDED
@@ -0,0 +1,635 @@
1
+ """The intent ledger — a third durable surface for *declared intent + adjudicated progress* (docs/107 §3).
2
+
3
+ > **The WAL answers "what was decided about leases." The intent ledger answers
4
+ > "what was the run trying to accomplish, and how far did the *evidence* say it
5
+ > got." The first is the kernel believing only effects; the second is the kernel
6
+ > storing a self-report so it can later distrust it against the fossils.**
7
+
8
+ DOS already records what a run **decided** (`lane_journal`: leases taken, dropped,
9
+ evicted) and what it **committed** (git ancestry). It records nothing about what a
10
+ run was **trying to do** and **how far it got** on the part that isn't a commit
11
+ yet — so when a run crashes mid-flight, a successor cannot *continue* it (only
12
+ `SCAVENGE` its lane). This module is the missing log: an append-only, `fsync`'d,
13
+ replay-foldable record of **declared intent** and **progress beats against it**,
14
+ one per run, keyed by `run_id` from birth.
15
+
16
+ It is `lane_journal`'s sibling, deliberately byte-mirroring its ARIES discipline
17
+ (`docs/107 §3`):
18
+
19
+ * **Same shape, different subject.** `append`/`read_all`/`replay`/`compact`,
20
+ `fsync` on write, torn-tail tolerant (`read_all` skips ONLY the torn final
21
+ line, keeps a mid-file `_CORRUPT` sentinel). Where `lane_journal` folds to a
22
+ *live-lease set*, this folds to a *declared-intent + verified-step set* (the
23
+ `LedgerState`) the pure `resume.resume_plan` consumes.
24
+ * **Different key, which closes a gap as a side effect.** Keyed by `run_id`
25
+ (the ledger lives at ``.dos/runs/<run_id>/intent.jsonl``, next to `run.json`),
26
+ NOT by `(loop_ts, lane)`. So the `(loop_ts,lane)→run_id` join `94 §7` /
27
+ `journal_delta`'s "HARD PROBLEM" flagged is *sidestepped* — the resumption
28
+ data is in a log keyed by `run_id` from the start. The WAL stays exactly what
29
+ it is (lease correctness); this carries the run-scoped progress it was never
30
+ meant to hold.
31
+
32
+ **The epistemic spine** (`docs/107 §3.2`, the `103`/`102` move): the asymmetry
33
+ between `STEP_CLAIMED` (the agent's say-so — *content*, distrusted) and
34
+ `STEP_VERIFIED` (a minted belief over git ancestry — *structure*, trusted). Replay
35
+ reads `STEP_VERIFIED`s as done and treats every `STEP_CLAIMED` without a matching
36
+ `STEP_VERIFIED` as **not done** — fail-closed. This module holds the *vocabulary,
37
+ the writers, and the pure replay fold*; the `STEP_VERIFIED` MINT (re-verifying a
38
+ claimed SHA against ancestry on the non-forgeable rung) is a CLI-boundary helper
39
+ (it does git I/O), exactly as `liveness`'s evidence-gather is a boundary, not the
40
+ pure verdict. The pure verdict over a `LedgerState` lives in `dos.resume`.
41
+
42
+ Write is library-only and happens UNDER the run's own writer (the dispatch loop /
43
+ a driver), each run owning its own file — there is no cross-run contention, so
44
+ unlike the WAL there is no shared mutex to hold (one writer per `run_id`). `O_APPEND`
45
+ + `fsync` is the durability floor.
46
+ """
47
+
48
+ from __future__ import annotations
49
+
50
+ import datetime as dt
51
+ import json
52
+ import os
53
+ import sys
54
+ from dataclasses import dataclass, field
55
+ from pathlib import Path
56
+ from typing import Iterable, Mapping
57
+
58
+ try:
59
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
60
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
61
+ except Exception:
62
+ pass
63
+
64
+ from dos import config as _config
65
+ from dos import durable_schema as _schema
66
+
67
+ # The durable-schema family + version every intent-ledger record carries (§6).
68
+ # Bumped ONLY on a NON-additive shape change; a new op or a new optional field is
69
+ # additive and does NOT bump it (the `durable_schema` contract). This kernel
70
+ # UNDERSTANDS up to `INTENT_LEDGER_SCHEMA` — a record tagged higher is REFUSED at
71
+ # read time (`read_all`'s schema gate), never guessed.
72
+ SCHEMA_FAMILY = "intent-ledger"
73
+ INTENT_LEDGER_SCHEMA = 1
74
+
75
+ INTENT_JSONL_NAME = "intent.jsonl"
76
+
77
+ # The closed op vocabulary (§3.2). Additive: a future op a newer writer emits is
78
+ # SKIPPED by an older `replay` (it acts only on the ops it knows), the same
79
+ # forward-compat the lane-journal `_STATE_MUTATING_OPS` gate gives — so adding an
80
+ # op never bumps the schema version.
81
+ OP_INTENT = "INTENT" # a run declares its goal (at spawn / first dispatch)
82
+ OP_STEP_CLAIMED = "STEP_CLAIMED" # the agent SAYS it finished a unit of work (forgeable)
83
+ OP_STEP_VERIFIED = "STEP_VERIFIED" # the kernel CONFIRMED a claimed step against ancestry
84
+ OP_SUSPEND = "SUSPEND" # a run voluntarily yields (pause; §4)
85
+ OP_RESUME_PROPOSED = "RESUME_PROPOSED" # a successor minted a resume point + proposed continuation
86
+ OP_CORRUPT = "_CORRUPT" # replay hit an unparseable non-trailing line (sentinel)
87
+
88
+ # The ops `replay` folds into the LedgerState. `_CORRUPT` and any unknown op are
89
+ # recorded-but-not-folded (the lane-journal `_STATE_MUTATING_OPS` posture): a
90
+ # torn/foreign line must never silently mutate the reconstructed intent.
91
+ _FOLDED_OPS = frozenset(
92
+ {OP_INTENT, OP_STEP_CLAIMED, OP_STEP_VERIFIED, OP_SUSPEND, OP_RESUME_PROPOSED}
93
+ )
94
+
95
+
96
+ def ledger_now_iso() -> str:
97
+ """Second-resolution UTC stamp for ledger entries (the `lane_journal` idiom)."""
98
+ return dt.datetime.now(dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
99
+
100
+
101
+ # --------------------------------------------------------------------------
102
+ # Where it lives — the run-dir the spine already creates, keyed by run_id.
103
+ # --------------------------------------------------------------------------
104
+
105
+
106
+ def run_dir_for(run_id: str, *, cfg: "_config.SubstrateConfig | None" = None) -> Path:
107
+ """The run-dir ``<runs>/<run_id>/`` for ``run_id`` under the active workspace.
108
+
109
+ Rides the layout's run-dir tree (`paths.fanout_runs`, which is `.dos/runs`
110
+ under the generic layout — the same tree the spine stamps `run.json` into).
111
+ Keyed by the run-id token itself, NOT a UTC-timestamp dir name: the ledger is
112
+ correlated-by-construction with the spine (`docs/107 §3.1`). Pure path
113
+ arithmetic — never creates the dir (a read-only caller must be able to ASK for
114
+ the path without a write; `append` is the only creator).
115
+ """
116
+ cfg = _config.ensure(cfg)
117
+ return cfg.paths.fanout_runs / run_id
118
+
119
+
120
+ def ledger_path_for(run_id: str, *, cfg: "_config.SubstrateConfig | None" = None) -> Path:
121
+ """The ``intent.jsonl`` path for ``run_id`` (next to its ``run.json``)."""
122
+ return run_dir_for(run_id, cfg=cfg) / INTENT_JSONL_NAME
123
+
124
+
125
+ # --------------------------------------------------------------------------
126
+ # I/O — append (fsync, library-only) + read_all (torn-tail + schema gate).
127
+ # --------------------------------------------------------------------------
128
+
129
+
130
+ def append(run_id: str, entry: dict, *, path: Path | None = None,
131
+ cfg: "_config.SubstrateConfig | None" = None) -> dict:
132
+ """Append one entry to ``run_id``'s intent ledger and `fsync` it. Returns the stamped entry.
133
+
134
+ Stamps `run_id` (the key — always THIS run's), `ts` (if absent), and the §6
135
+ `schema` tag (if the builder didn't already), then writes one canonical-JSON
136
+ line + newline, `flush` + `os.fsync` so the record is durable before the
137
+ function returns (log-before-act, the WAL invariant). `O_APPEND` makes the
138
+ write atomic w.r.t. any other appender at the OS level; one writer per `run_id`
139
+ means there is no cross-run mutex to hold (unlike the shared WAL).
140
+
141
+ The entry shape is the caller's decision payload (use the `*_entry` builders);
142
+ this only fills the universal fields. `path` overrides the resolved run-dir
143
+ location (tests / a driver writing elsewhere).
144
+ """
145
+ p = path or ledger_path_for(run_id, cfg=cfg)
146
+ e = dict(entry)
147
+ e["run_id"] = run_id # the key is authoritative — always this run's
148
+ e.setdefault("ts", ledger_now_iso())
149
+ if _schema.SCHEMA_KEY not in e:
150
+ e.update(_schema.tag(SCHEMA_FAMILY, INTENT_LEDGER_SCHEMA))
151
+ line = json.dumps(e, sort_keys=True, default=str, ensure_ascii=False) + "\n"
152
+ p.parent.mkdir(parents=True, exist_ok=True)
153
+ fd = os.open(str(p), os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o644)
154
+ try:
155
+ os.write(fd, line.encode("utf-8"))
156
+ os.fsync(fd)
157
+ finally:
158
+ os.close(fd)
159
+ return e
160
+
161
+
162
+ def read_all(run_id: str | None = None, *, path: Path | None = None,
163
+ cfg: "_config.SubstrateConfig | None" = None,
164
+ understands: int = INTENT_LEDGER_SCHEMA) -> list[dict]:
165
+ """Return every ledger entry for ``run_id`` in append order, schema-gated.
166
+
167
+ Two distrust postures layered (the §6 floor on top of the ARIES floor):
168
+
169
+ * **Torn-tail tolerance** (the `lane_journal.read_all` contract): an
170
+ unparseable TRAILING line (a crash mid-`append`) is skipped — a
171
+ half-written record is "didn't happen", the safe WAL read. A non-trailing
172
+ unparseable line is a real integrity breach, kept as an `_CORRUPT`
173
+ sentinel so an audit/replay still flags it.
174
+ * **Schema gate** (§6, the refuse-don't-guess floor): a parseable record
175
+ whose `schema` tag is a NON-additively-newer version than `understands` is
176
+ NOT returned as data — it is replaced by an `_CORRUPT`-style
177
+ `_UNREADABLE` sentinel carrying the readability verdict, so `replay`/the
178
+ fold treat it as un-foldable rather than best-effort-parsing a shape this
179
+ kernel does not know. An UNTAGGED (legacy/pre-tag) record is treated
180
+ permissively as readable (the family's implicit v1) — the tolerant-fold
181
+ side of the `durable_schema.UNTAGGED` contract. A WRONG_FAMILY record (a
182
+ foreign line in the file) is likewise kept as an `_UNREADABLE` sentinel.
183
+
184
+ Pass `run_id` to resolve the run-dir path, or `path` to read a specific file
185
+ (tests). `understands` is the reader's schema ceiling (defaults to this
186
+ kernel's) — injectable so a test can simulate an OLD reader meeting a NEW
187
+ record.
188
+ """
189
+ p = path or (ledger_path_for(run_id, cfg=cfg) if run_id else None)
190
+ if p is None:
191
+ raise ValueError("read_all needs a run_id or an explicit path")
192
+ if not p.exists():
193
+ return []
194
+ try:
195
+ raw = p.read_text(encoding="utf-8", errors="replace")
196
+ except OSError:
197
+ return []
198
+ lines = raw.splitlines()
199
+ out: list[dict] = []
200
+ for i, line in enumerate(lines):
201
+ s = line.strip()
202
+ if not s:
203
+ continue
204
+ try:
205
+ obj = json.loads(s)
206
+ except json.JSONDecodeError:
207
+ # Torn final line → "didn't happen"; a mid-file corrupt line → sentinel.
208
+ if i == len(lines) - 1:
209
+ break
210
+ out.append({"op": OP_CORRUPT, "_raw": s, "_line": i})
211
+ continue
212
+ if not isinstance(obj, dict):
213
+ continue
214
+ # The §6 schema gate. UNTAGGED/READABLE proceed; UNREADABLE_NEWER and
215
+ # WRONG_FAMILY become an un-foldable sentinel that records WHY (so a
216
+ # surfaced resume verdict can say "v3 record, kernel reads ≤ v1 — migrate").
217
+ v = _schema.classify(obj, family=SCHEMA_FAMILY, understands=understands)
218
+ if v.readability in (_schema.Readability.READABLE, _schema.Readability.UNTAGGED):
219
+ out.append(obj)
220
+ else:
221
+ out.append({
222
+ "op": OP_CORRUPT,
223
+ "_unreadable": v.to_dict(),
224
+ "_raw": s,
225
+ "_line": i,
226
+ })
227
+ return out
228
+
229
+
230
+ # --------------------------------------------------------------------------
231
+ # The replay fold → LedgerState (pure; the resume verdict's evidence shape).
232
+ # --------------------------------------------------------------------------
233
+
234
+
235
+ @dataclass(frozen=True)
236
+ class SuspendCheckpoint:
237
+ """The CONVERSATION rewind anchor a SUSPEND record may carry (docs/164 F1.5).
238
+
239
+ Defined HERE — beside the SUSPEND record it serializes onto — because it is a
240
+ durable-record shape, the sibling of the git-axis `suspend_resume_sha`. The SAME
241
+ SUSPEND record carries both: `resume_sha` (the git re-entry point, read by
242
+ `resume.resume_plan`) and this `(turn_index, transcript_digest)` (the
243
+ conversation re-entry checkpoint, read by `rewind.rewind_plan`). `rewind` imports
244
+ this type from here — the one-way layering arrow (`rewind` → `intent_ledger`),
245
+ never the reverse.
246
+
247
+ `transcript_digest` is the kernel's hash of the transcript-up-to-`turn_index` at
248
+ suspend time — the NON-FORGEABLE anchor (its byte-author is the kernel's hash, not
249
+ the judged agent). The conversation rewind is valid ONLY if the live turn at
250
+ `turn_index` still digests to it (`rewind` enforces this — else UNANCHORED).
251
+
252
+ `present=False` is the honest zero: a SUSPEND from a kernel too old to stamp a
253
+ checkpoint (the additive-evolution case) folds back to an absent checkpoint, never
254
+ a guessed one. `from_record` builds it from a folded SUSPEND dict, tolerating the
255
+ missing fields.
256
+ """
257
+
258
+ turn_index: int = -1
259
+ transcript_digest: str = ""
260
+ present: bool = False
261
+
262
+ @classmethod
263
+ def absent(cls) -> "SuspendCheckpoint":
264
+ """The honest zero — no minted checkpoint (an older kernel's SUSPEND)."""
265
+ return cls(turn_index=-1, transcript_digest="", present=False)
266
+
267
+ @classmethod
268
+ def from_record(cls, entry: "Mapping | dict") -> "SuspendCheckpoint":
269
+ """Build from a SUSPEND record's additive fields, tolerating their absence.
270
+
271
+ A SUSPEND with no `transcript_digest` (an older kernel, or a git-only suspend)
272
+ folds to `absent()` — the skip-unknown tolerant-read rule. Present iff a
273
+ non-empty `transcript_digest` was recorded (the digest is the load-bearing
274
+ field; a `checkpoint_turn` with no digest is not a usable anchor).
275
+ """
276
+ digest = str(entry.get("transcript_digest") or "")
277
+ if not digest:
278
+ return cls.absent()
279
+ turn_raw = entry.get("checkpoint_turn")
280
+ try:
281
+ turn = int(turn_raw)
282
+ except (TypeError, ValueError):
283
+ turn = -1
284
+ return cls(turn_index=turn, transcript_digest=digest, present=True)
285
+
286
+
287
+ @dataclass(frozen=True)
288
+ class LedgerState:
289
+ """The reconstructed intent of one run — `replay`'s output, `resume_plan`'s input.
290
+
291
+ The intent-ledger analogue of `lane_journal.replay`'s live-lease list: a pure
292
+ fold of the entry sequence into "what did this run DECLARE, and which steps did
293
+ the kernel VERIFY." Deliberately carries CLAIMED and VERIFIED separately — the
294
+ whole epistemic point (§3.2) is that they are not the same, and the resume fold
295
+ treats only VERIFIED as done.
296
+
297
+ run_id — the run this state describes (the ledger's key).
298
+ goal — the declared free-form goal string (the latest INTENT's).
299
+ plan / phase — the declared (plan, phase) if the run named one (else "").
300
+ start_sha — the run's declared start commit (the resume floor anchor).
301
+ declared_steps — the ordered step ids the INTENT declared (may be empty: a
302
+ run with a free-form goal and no enumerated steps).
303
+ step_regions — {step_id: (glob, …)} — each step's declared FILE REGION
304
+ (repo-relative globs). OPTIONAL: a step with no region falls
305
+ back to the non-empty-footprint check. When present, the
306
+ resume re-adjudication (`resume_evidence`) requires the
307
+ step's commit footprint to INTERSECT this region, closing the
308
+ §5 hole where a forged record points at a real-but-unrelated
309
+ commit (a commit outside the step's region isn't its work).
310
+ claimed — {step_id: claimed_sha} — the agent's self-reports
311
+ (DISTRUSTED; a pointer to a commit to check, not proof).
312
+ verified — {step_id: VerifiedStep} — steps the kernel CONFIRMED
313
+ against ancestry on the non-forgeable rung (TRUSTED).
314
+ suspended — True iff the run's last lifecycle record is a SUSPEND
315
+ (it parked voluntarily; §4) and no later INTENT re-opened it.
316
+ suspend_resume_sha— the resume-point SHA the SUSPEND recorded (a cheaper,
317
+ still-re-verified hint; "" if not suspended / not given).
318
+ suspend_checkpoint— the CONVERSATION rewind anchor the SUSPEND recorded (docs/164
319
+ F1.5): a `(turn_index, transcript_digest)` the kernel stamped,
320
+ read by `rewind.rewind_plan`. The sibling of `suspend_resume_sha`
321
+ on the SAME SUSPEND record (git axis reads the SHA, conversation
322
+ axis reads this). `absent()` if not suspended / an older kernel's
323
+ SUSPEND that stamped no checkpoint (the additive-evolution zero).
324
+ resume_proposed — predecessor run_ids a RESUME_PROPOSED was already minted
325
+ for (idempotence; §5 req 4): a second resume sees these and
326
+ does not double-propose.
327
+ corrupt_lines — count of `_CORRUPT`/`_UNREADABLE` sentinels seen (a
328
+ non-zero count is an integrity signal the resume verdict
329
+ degrades on — UNRESUMABLE when the fold isn't sound).
330
+ unreadable_newer — True iff ≥1 sentinel was an UNREADABLE_NEWER schema (a
331
+ record this kernel is too OLD to read): the §6 floor —
332
+ resume must refuse, not guess.
333
+ """
334
+
335
+ run_id: str
336
+ goal: str = ""
337
+ plan: str = ""
338
+ phase: str = ""
339
+ start_sha: str = ""
340
+ declared_steps: tuple[str, ...] = ()
341
+ step_regions: dict[str, tuple[str, ...]] = field(default_factory=dict)
342
+ claimed: dict[str, str] = field(default_factory=dict)
343
+ verified: dict[str, "VerifiedStep"] = field(default_factory=dict)
344
+ suspended: bool = False
345
+ suspend_resume_sha: str = ""
346
+ suspend_checkpoint: SuspendCheckpoint = field(default_factory=SuspendCheckpoint.absent)
347
+ resume_proposed: tuple[str, ...] = ()
348
+ corrupt_lines: int = 0
349
+ unreadable_newer: bool = False
350
+
351
+ @property
352
+ def has_intent(self) -> bool:
353
+ """True iff at least one INTENT record was folded (a goal/plan/steps exist).
354
+
355
+ UNRESUMABLE's floor: with no INTENT there is no declared work to compute a
356
+ residual from — `resume_plan` returns UNRESUMABLE rather than guessing one.
357
+ """
358
+ return bool(self.goal or self.plan or self.declared_steps)
359
+
360
+
361
+ @dataclass(frozen=True)
362
+ class VerifiedStep:
363
+ """A step the kernel confirmed against ancestry (`STEP_VERIFIED`'s payload).
364
+
365
+ `sha` is the ancestry-backed commit; `via` names the verify RUNG that backed it
366
+ (`file-path`/`registry`/… — NEVER the forgeable subject-grep, §5 req 2);
367
+ `rungs`/`verdicts` echo the backing detail for forensics. This is the minted
368
+ belief — the only thing resume reads as "done."
369
+ """
370
+
371
+ step_id: str
372
+ sha: str
373
+ via: str = ""
374
+ verdicts: tuple[str, ...] = ()
375
+
376
+
377
+ def replay(entries: Iterable[dict]) -> LedgerState:
378
+ """Fold the ledger sequence into a `LedgerState`. PURE — entries in, state out.
379
+
380
+ The intent-ledger redo fold (the third ARIES phase's input). Folding rules
381
+ (later records win for scalar fields; sets accumulate):
382
+
383
+ * INTENT → set goal/plan/phase/start_sha/declared_steps; a later
384
+ INTENT (a re-declared/re-opened run) overrides and clears
385
+ `suspended` (the run is live again).
386
+ * STEP_CLAIMED → record claimed[step_id] = claimed_sha (the distrusted
387
+ self-report — a pointer to a commit to check).
388
+ * STEP_VERIFIED → record verified[step_id] = VerifiedStep (the minted
389
+ belief; the ONLY "done" signal).
390
+ * SUSPEND → mark suspended + carry its recorded resume-point SHA.
391
+ * RESUME_PROPOSED → record the predecessor run_id (idempotence).
392
+ * _CORRUPT / _UNREADABLE / unknown → counted, never folded into intent (a
393
+ torn/foreign/too-new line must not mutate the
394
+ reconstructed goal — the lane-journal skip-unknown rule).
395
+
396
+ Returns a frozen `LedgerState`; `replay([])` is an empty state with
397
+ `has_intent == False` (the UNRESUMABLE floor for a run that declared nothing).
398
+ """
399
+ run_id = ""
400
+ goal = ""
401
+ plan = ""
402
+ phase = ""
403
+ start_sha = ""
404
+ declared_steps: tuple[str, ...] = ()
405
+ step_regions: dict[str, tuple[str, ...]] = {}
406
+ claimed: dict[str, str] = {}
407
+ verified: dict[str, VerifiedStep] = {}
408
+ suspended = False
409
+ suspend_resume_sha = ""
410
+ suspend_checkpoint = SuspendCheckpoint.absent()
411
+ resume_proposed: list[str] = []
412
+ corrupt = 0
413
+ unreadable_newer = False
414
+
415
+ for e in entries:
416
+ op = str(e.get("op") or "")
417
+ rid = str(e.get("run_id") or "")
418
+ if rid:
419
+ run_id = rid
420
+ if op not in _FOLDED_OPS:
421
+ # _CORRUPT / _UNREADABLE / unknown — recorded, not folded.
422
+ if op == OP_CORRUPT:
423
+ corrupt += 1
424
+ un = e.get("_unreadable")
425
+ if isinstance(un, dict) and un.get("readability") == "UNREADABLE_NEWER":
426
+ unreadable_newer = True
427
+ continue
428
+ if op == OP_INTENT:
429
+ goal = str(e.get("goal") or goal)
430
+ plan = str(e.get("plan") or plan)
431
+ phase = str(e.get("phase") or phase)
432
+ start_sha = str(e.get("start_sha") or start_sha)
433
+ steps = e.get("declared_steps")
434
+ if isinstance(steps, (list, tuple)):
435
+ declared_steps = tuple(str(s) for s in steps)
436
+ regions = e.get("step_regions")
437
+ if isinstance(regions, dict):
438
+ step_regions = {
439
+ str(k): tuple(str(g) for g in v)
440
+ for k, v in regions.items()
441
+ if isinstance(v, (list, tuple))
442
+ }
443
+ # A fresh INTENT re-opens a parked run (it is live again).
444
+ suspended = False
445
+ suspend_resume_sha = ""
446
+ suspend_checkpoint = SuspendCheckpoint.absent()
447
+ elif op == OP_STEP_CLAIMED:
448
+ sid = str(e.get("step_id") or "")
449
+ if sid:
450
+ claimed[sid] = str(e.get("sha") or "")
451
+ elif op == OP_STEP_VERIFIED:
452
+ sid = str(e.get("step_id") or "")
453
+ if sid:
454
+ vds = e.get("verdicts")
455
+ verified[sid] = VerifiedStep(
456
+ step_id=sid,
457
+ sha=str(e.get("sha") or ""),
458
+ via=str(e.get("via") or ""),
459
+ verdicts=tuple(str(v) for v in vds) if isinstance(vds, (list, tuple)) else (),
460
+ )
461
+ elif op == OP_SUSPEND:
462
+ suspended = True
463
+ suspend_resume_sha = str(e.get("resume_sha") or e.get("sha") or "")
464
+ # The conversation-rewind anchor (docs/164 F1.5) — additive, tolerant of
465
+ # absence (an older kernel's SUSPEND folds to an absent checkpoint).
466
+ suspend_checkpoint = SuspendCheckpoint.from_record(e)
467
+ elif op == OP_RESUME_PROPOSED:
468
+ pred = str(e.get("predecessor_run_id") or e.get("predecessor") or "")
469
+ if pred and pred not in resume_proposed:
470
+ resume_proposed.append(pred)
471
+
472
+ return LedgerState(
473
+ run_id=run_id,
474
+ goal=goal,
475
+ plan=plan,
476
+ phase=phase,
477
+ start_sha=start_sha,
478
+ declared_steps=declared_steps,
479
+ step_regions=dict(step_regions),
480
+ claimed=dict(claimed),
481
+ verified=dict(verified),
482
+ suspended=suspended,
483
+ suspend_resume_sha=suspend_resume_sha,
484
+ suspend_checkpoint=suspend_checkpoint,
485
+ resume_proposed=tuple(resume_proposed),
486
+ corrupt_lines=corrupt,
487
+ unreadable_newer=unreadable_newer,
488
+ )
489
+
490
+
491
+ # --------------------------------------------------------------------------
492
+ # Entry builders — the writer's vocabulary, defined HERE (one home), pure.
493
+ # Each carries the §6 schema tag so even a record written directly (not via
494
+ # `append`) is self-declaring.
495
+ # --------------------------------------------------------------------------
496
+
497
+
498
+ def intent_entry(
499
+ *,
500
+ goal: str = "",
501
+ plan: str = "",
502
+ phase: str = "",
503
+ start_sha: str = "",
504
+ declared_steps: Iterable[str] | None = None,
505
+ step_regions: "dict[str, Iterable[str]] | None" = None,
506
+ env: "Mapping | None" = None,
507
+ ) -> dict:
508
+ """Build an INTENT entry — a run declaring its goal (at spawn / first dispatch).
509
+
510
+ `goal` is a free-form intent string; `plan`/`phase` the structured target if one
511
+ exists; `start_sha` the run's start commit (the resume floor anchor); `declared_steps`
512
+ the ordered step ids the run means to complete (may be omitted — a free-form goal).
513
+ `step_regions` (OPTIONAL) maps a step id → its file region (repo-relative globs): at
514
+ resume, a step's verifying commit footprint must INTERSECT this region (§5, the
515
+ real-but-unrelated-commit defense). A step with no region falls back to the
516
+ non-empty-footprint check. Believed AS A CLAIM at resume (§3.2): the residual is
517
+ computed from it, but every "done" is re-verified.
518
+
519
+ `env` (OPTIONAL) is the run's environment print — ``cfg.env.to_dict()`` (an
520
+ `env_print.EnvPrint`), recorded at birth so the fossil says *under what* the run
521
+ declared its intent (``docs/115`` primitive 1: kernel version + SHA + Python +
522
+ OS + declared tools). Purely ADDITIVE — an INTENT with no `env` is a run from a
523
+ kernel that did not stamp prints, read back unchanged (the additive-evolution
524
+ contract: a new optional field never bumps `INTENT_LEDGER_SCHEMA`). The kernel
525
+ RECORDS it; it does not yet adjudicate on it (a later phase reads env-divergence
526
+ as a resume signal). The print is data, not a decision input — the docs/76 line.
527
+ """
528
+ e = {
529
+ **_schema.tag(SCHEMA_FAMILY, INTENT_LEDGER_SCHEMA),
530
+ "op": OP_INTENT,
531
+ "goal": goal,
532
+ "plan": plan,
533
+ "phase": phase,
534
+ "start_sha": start_sha,
535
+ }
536
+ if declared_steps is not None:
537
+ e["declared_steps"] = [str(s) for s in declared_steps]
538
+ if step_regions is not None:
539
+ e["step_regions"] = {str(k): [str(g) for g in v] for k, v in step_regions.items()}
540
+ if env is not None:
541
+ e["env"] = dict(env)
542
+ return e
543
+
544
+
545
+ def step_claimed_entry(step_id: str, sha: str) -> dict:
546
+ """Build a STEP_CLAIMED entry — the agent SAYS it finished a step (forgeable).
547
+
548
+ `sha` is the commit the agent CLAIMS landed the step. Never believed on its own
549
+ (§3.2): a pointer to a commit to check, not proof. The `STEP_VERIFIED` mint is
550
+ what turns a claim into a belief.
551
+ """
552
+ return {
553
+ **_schema.tag(SCHEMA_FAMILY, INTENT_LEDGER_SCHEMA),
554
+ "op": OP_STEP_CLAIMED,
555
+ "step_id": str(step_id),
556
+ "sha": str(sha or ""),
557
+ }
558
+
559
+
560
+ def step_verified_entry(step_id: str, sha: str, *, via: str = "",
561
+ verdicts: Iterable[str] | None = None) -> dict:
562
+ """Build a STEP_VERIFIED entry — the kernel CONFIRMED a claimed step (§5).
563
+
564
+ Written ONLY by the CLI-boundary mint (`dos.resume.verify_step` / the dispatch
565
+ loop) after re-checking the claimed SHA against ancestry on the NON-FORGEABLE
566
+ rung (§5 req 2: `via` is `file-path`/`registry`, never the forgeable
567
+ subject-grep). The minted belief — the only "done" resume reads.
568
+ """
569
+ return {
570
+ **_schema.tag(SCHEMA_FAMILY, INTENT_LEDGER_SCHEMA),
571
+ "op": OP_STEP_VERIFIED,
572
+ "step_id": str(step_id),
573
+ "sha": str(sha or ""),
574
+ "via": str(via or ""),
575
+ "verdicts": [str(v) for v in verdicts] if verdicts is not None else [],
576
+ }
577
+
578
+
579
+ def suspend_entry(*, reason: str = "", resume_sha: str = "",
580
+ residual: Iterable[str] | None = None,
581
+ checkpoint: "SuspendCheckpoint | None" = None) -> dict:
582
+ """Build a SUSPEND entry — a run voluntarily yields (pause; §4).
583
+
584
+ `resume_sha` is the recorded resume point at suspend time (a cheaper hint than a
585
+ full re-derivation — but still re-verified at resume, since a suspend an hour ago
586
+ may be stale). `residual` is the remaining step ids at suspend time (forensic).
587
+ Believed as a recorded DECISION (not a progress claim) — but the resume still
588
+ re-checks ancestry (§4).
589
+
590
+ `checkpoint` (OPTIONAL — docs/164 F1.5) is the CONVERSATION rewind anchor: a
591
+ `(turn_index, transcript_digest)` the kernel stamped at suspend time, the
592
+ sibling of the git-axis `resume_sha`. The SAME SUSPEND record carries both —
593
+ the git-rewind axis (`resume.resume_plan`) reads `resume_sha`, the
594
+ conversation-rewind axis (`rewind.rewind_plan`) reads these two fields. Written
595
+ only when present, as two additive fields `"checkpoint_turn"` + `"transcript_digest"`.
596
+ PURELY ADDITIVE: a SUSPEND from an older kernel that wrote no checkpoint reads
597
+ back unchanged (the additive-evolution contract above — a new optional field
598
+ never bumps `INTENT_LEDGER_SCHEMA`), and a kernel too OLD to know the fields
599
+ simply ignores them (the skip-unknown tolerant-read rule). The digest is the
600
+ NON-FORGEABLE rewind anchor: the kernel rewinds to a turn IT stamped here, never
601
+ to a turn the agent claims (the §6 conversation-axis litmus).
602
+ """
603
+ e = {
604
+ **_schema.tag(SCHEMA_FAMILY, INTENT_LEDGER_SCHEMA),
605
+ "op": OP_SUSPEND,
606
+ "reason": str(reason or ""),
607
+ "resume_sha": str(resume_sha or ""),
608
+ }
609
+ if residual is not None:
610
+ e["residual"] = [str(s) for s in residual]
611
+ if checkpoint is not None:
612
+ # Two additive fields, written only when a checkpoint was minted. The
613
+ # conversation axis reads these; the git axis reads `resume_sha` above.
614
+ e["checkpoint_turn"] = int(checkpoint.turn_index)
615
+ e["transcript_digest"] = str(checkpoint.transcript_digest or "")
616
+ return e
617
+
618
+
619
+ def resume_proposed_entry(*, predecessor_run_id: str, resume_sha: str = "",
620
+ residual: Iterable[str] | None = None) -> dict:
621
+ """Build a RESUME_PROPOSED entry — a successor minted a resume point + proposed continuation (§5).
622
+
623
+ Recorded on the SUCCESSOR's ledger for forensics + idempotence (§5 req 4): a
624
+ second resume attempt sees this predecessor already proposed-for and does not
625
+ double-propose. `predecessor_run_id` is the dead/parked run being resumed.
626
+ """
627
+ e = {
628
+ **_schema.tag(SCHEMA_FAMILY, INTENT_LEDGER_SCHEMA),
629
+ "op": OP_RESUME_PROPOSED,
630
+ "predecessor_run_id": str(predecessor_run_id),
631
+ "resume_sha": str(resume_sha or ""),
632
+ }
633
+ if residual is not None:
634
+ e["residual"] = [str(s) for s in residual]
635
+ return e