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/rewind_evidence.py ADDED
@@ -0,0 +1,168 @@
1
+ """rewind-evidence — the boundary I/O for the conversation-rewind axis (docs/164 F1.5).
2
+
3
+ `rewind.rewind_plan` is a PURE verdict over `(TurnRef…, SuspendCheckpoint, FireVerdict)`.
4
+ SOMETHING has to gather those off disk: read the run's transcript turns and hash each
5
+ (`digest_turn`), read the SUSPEND record's minted checkpoint off the intent ledger, and
6
+ build the `FireVerdict` from whichever ground-truth stop verdict fired. That is this
7
+ module — the conversation axis's `resume_evidence` sibling: boundary I/O feeding the pure
8
+ core, never inside the verdict.
9
+
10
+ Three boundary jobs, mirroring `resume_evidence`'s shape:
11
+
12
+ * **`gather_turns(...)`** — read the run's transcript off disk (the host-owned
13
+ `transcript.jsonl` beside `intent.jsonl`), hashing each turn's bytes into a
14
+ `TurnRef(index, digest)`. The DIGEST is computed HERE (the byte-author of the
15
+ anchor's identity is the kernel's hash, not the judged agent — the
16
+ `evidence.believe_under_floor` framing that makes the checkpoint non-forgeable).
17
+ A missing/unreadable transcript degrades to NO turns (→ the verdict's UNANCHORED
18
+ floor: no live turn can match the checkpoint, so the kernel rewinds to nothing).
19
+ * **`read_checkpoint(...)`** — pull the minted `SuspendCheckpoint` off the run's
20
+ folded `LedgerState` (`intent_ledger.replay`). The checkpoint was stamped at
21
+ `OP_SUSPEND`; `state.suspend_checkpoint` is already the read-side object (the fold
22
+ decoded it). `absent()` when the run never suspended or an older kernel stamped no
23
+ checkpoint — the honest zero that yields UNANCHORED.
24
+ * **`fire_from(...)`** — wrap an already-computed `Resume`/`Convergence` verdict as a
25
+ `FireVerdict`, NEVER re-deriving it (the `resume`/`completion` reuse-not-reimplement
26
+ rule). The boundary computes the ground-truth stop signal upstream; this only adapts
27
+ its type.
28
+
29
+ The served root/config is passed EXPLICITLY (never the process-global active), so a
30
+ long-lived caller fielding several workspaces gets the right tree (the `resume_evidence`
31
+ discipline). Every failure mode degrades to the SAFE direction: a transcript we cannot
32
+ read is treated as NO turns, so the verdict refuses to rewind (UNANCHORED) rather than
33
+ rewinding to a turn it cannot confirm the kernel stamped — fail-closed, the same posture
34
+ `resume_evidence` takes for an unresolvable SHA.
35
+
36
+ The transcript surface is a CONVENTION, not a kernel-owned format: the host writes its
37
+ turns as JSONL beside the ledger (one record per turn). The kernel reads bytes and hashes
38
+ them; it neither defines nor depends on the turn's internal shape ("the host owns the
39
+ transcript", docs/164 P1.5). A host with its turns elsewhere passes them in directly via
40
+ `turns_from_records` — the file reader is the convenience default, not the contract.
41
+ """
42
+
43
+ from __future__ import annotations
44
+
45
+ import json
46
+ from pathlib import Path
47
+ from typing import Iterable, Optional
48
+
49
+ from dos import config as _config
50
+ from dos import intent_ledger as _il
51
+ from dos.completion import Convergence
52
+ from dos.intent_ledger import LedgerState, SuspendCheckpoint
53
+ from dos.resume import Resume
54
+ from dos.rewind import FireVerdict, TurnRef, digest_turn
55
+
56
+ # The host-owned transcript surface — JSONL, one record per turn, beside intent.jsonl.
57
+ # A CONVENTION (not a kernel format): the kernel hashes each record's bytes, never reads
58
+ # its internal fields. Named here so the reader and a host writer agree on one path.
59
+ TRANSCRIPT_JSONL_NAME = "transcript.jsonl"
60
+
61
+
62
+ def transcript_path_for(
63
+ run_id: str, *, cfg: "_config.SubstrateConfig | None" = None
64
+ ) -> Path:
65
+ """The ``transcript.jsonl`` path for ``run_id`` — beside its ``intent.jsonl``.
66
+
67
+ The conversation-axis sibling of `intent_ledger.ledger_path_for`: the host writes
68
+ its per-turn records here; `gather_turns` hashes them into `TurnRef`s. The kernel
69
+ owns neither the file's existence nor its record shape — a run with no transcript
70
+ simply yields no turns (→ UNANCHORED, the safe floor).
71
+ """
72
+ return _il.run_dir_for(run_id, cfg=cfg) / TRANSCRIPT_JSONL_NAME
73
+
74
+
75
+ def turns_from_records(records: Iterable["bytes | str | dict"]) -> tuple[TurnRef, ...]:
76
+ """Hash an in-memory sequence of turn records into ``TurnRef(index, digest)``.
77
+
78
+ The pure-ish core of the reader (no I/O — the caller already has the records): each
79
+ record's bytes are digested by `digest_turn` and paired with its position. A `dict`
80
+ record is canonicalised with sorted keys before hashing so the digest is stable
81
+ across key-order — the kernel's hash is the anchor's identity, so it must be
82
+ deterministic for the same logical turn.
83
+
84
+ The byte-author discipline (the reason the anchor is non-forgeable): the DIGEST is
85
+ THIS function's hash of the turn, NOT a value the judged agent supplied. An agent
86
+ cannot forge the identity of its own turn's digest, exactly as `arg_provenance`'s
87
+ mint detector turns on the agent not authoring the identity of its own repeated
88
+ output.
89
+ """
90
+ out: list[TurnRef] = []
91
+ for i, rec in enumerate(records):
92
+ if isinstance(rec, dict):
93
+ # Canonical bytes: sorted keys, no whitespace drift → a stable digest for the
94
+ # same logical turn regardless of how the host serialised it.
95
+ raw = json.dumps(rec, sort_keys=True, separators=(",", ":"),
96
+ ensure_ascii=False, default=str)
97
+ elif isinstance(rec, bytes):
98
+ raw = rec
99
+ else:
100
+ raw = str(rec)
101
+ out.append(TurnRef(index=i, digest=digest_turn(raw)))
102
+ return tuple(out)
103
+
104
+
105
+ def gather_turns(
106
+ run_id: str,
107
+ *,
108
+ cfg: "_config.SubstrateConfig | None" = None,
109
+ path: Path | None = None,
110
+ ) -> tuple[TurnRef, ...]:
111
+ """Read the run's transcript off disk → ``TurnRef``s, hashing each turn. Fail-closed.
112
+
113
+ The conversation-axis evidence-gather (the `resume_evidence.gather_ancestry` shape):
114
+ the file read happens HERE; the already-hashed turns are handed to the pure verdict.
115
+ Reads `transcript.jsonl` beside the ledger (or an explicit `path`), one record per
116
+ line, and digests each. Every failure — no file, unreadable, a torn line — degrades
117
+ to the SAFE direction: that turn (or the whole transcript) yields no `TurnRef`, so a
118
+ checkpoint can find no matching live turn and the verdict refuses (UNANCHORED) rather
119
+ than rewinding to a turn it cannot confirm. A torn TAIL line is skipped (the
120
+ `intent_ledger` torn-tail tolerance), not fatal.
121
+ """
122
+ cfg = _config.ensure(cfg)
123
+ p = path or transcript_path_for(run_id, cfg=cfg)
124
+ try:
125
+ text = Path(p).read_text(encoding="utf-8", errors="replace")
126
+ except (OSError, ValueError):
127
+ return () # no readable transcript → no turns → UNANCHORED (fail-closed)
128
+ records: list[str] = []
129
+ for line in text.splitlines():
130
+ ln = line.strip()
131
+ if not ln:
132
+ continue
133
+ # Hash the LINE bytes as the turn (the host owns the record shape; the kernel
134
+ # hashes what the host wrote). A line that happens to be JSON is hashed as its
135
+ # own bytes here — `turns_from_records` canonicalises only when handed a dict.
136
+ records.append(ln)
137
+ return turns_from_records(records)
138
+
139
+
140
+ def read_checkpoint(state: LedgerState) -> SuspendCheckpoint:
141
+ """The minted conversation rewind anchor off the folded ledger (or ``absent()``).
142
+
143
+ `intent_ledger.replay` already decoded the SUSPEND record's `(checkpoint_turn,
144
+ transcript_digest)` into `state.suspend_checkpoint`. This is the trivial accessor
145
+ that names the read for the rewind boundary — the sibling of reading
146
+ `state.suspend_resume_sha` on the git axis. A run that never suspended, or an older
147
+ kernel's SUSPEND that stamped no checkpoint, carries `SuspendCheckpoint.absent()` —
148
+ the honest zero `rewind_plan` maps to UNANCHORED.
149
+ """
150
+ return state.suspend_checkpoint
151
+
152
+
153
+ def fire_from(
154
+ *,
155
+ resume_verdict: Optional[Resume] = None,
156
+ convergence_verdict: Optional[Convergence] = None,
157
+ ) -> FireVerdict:
158
+ """Adapt an already-computed ground-truth stop verdict into a ``FireVerdict``.
159
+
160
+ The boundary computes the stop signal upstream (`resume.resume_plan` → `Resume`;
161
+ `completion.convergence` → `Convergence`) and this wraps it — NEVER re-deriving it
162
+ inside the rewind axis (the reuse-not-reimplement rule). Pass whichever fired; both
163
+ None is a non-firing verdict (→ NO_REWIND, the loop continues).
164
+ """
165
+ return FireVerdict(
166
+ resume_verdict=resume_verdict,
167
+ convergence_verdict=convergence_verdict,
168
+ )
dos/rewind_tokens.py ADDED
@@ -0,0 +1,252 @@
1
+ """The no-good verdict-token registry — the rewind note's closed vocabulary, *as data*.
2
+
3
+ docs/164 §6 (the F1.5 conversation-rewind axis). When the kernel rewinds a
4
+ conversation to a minted checkpoint, the agent re-enters with a **no-good
5
+ annotation**. The single load-bearing rule of that annotation (docs/164 §1, §6)
6
+ is that it may carry **only un-forged bytes** — and the most dangerous failure
7
+ mode is the one `BLOCK` died of: a *generated* explanation of why the branch
8
+ failed ("you should have used a try/except") sneaking in as a note byte. That is
9
+ the forgeable rung; it belongs to F3, behind the apply-gate PEP, never to F1.5.
10
+
11
+ This module is the structural lock that makes that impossible. It is the
12
+ `reasons.py` `ReasonRegistry` pattern (the closed-enum-as-data hackability seam,
13
+ `docs/HACKING.md`) re-aimed at the no-good vocabulary: the *mechanism* (a token
14
+ RENDERS via a registry-owned template over structured fields the kernel computed)
15
+ lives here; the *set of token kinds* is a closed, ordered, immutable registry. A
16
+ `VerdictToken` is **not a string** — it is a frozen `(kind, payload)` where `kind`
17
+ is drawn from the registry and the rendered string is COMPOSED from the registry's
18
+ own template + the structured fields, never from a free-form caller slot. The
19
+ agent can supply neither the template nor a free field, so there is no reachable
20
+ path by which model-generated prose becomes a note byte (the §6 grep-for-generated
21
+ -prose litmus has nothing to find — `tests/test_rewind.py` pins it).
22
+
23
+ Why a registry and not a bare enum
24
+ ==================================
25
+
26
+ The same reason `reasons.py` is a registry: a closed set declared ONCE, as data,
27
+ that every consumer derives from. A `VerdictKind` value that is not in the active
28
+ registry is **not renderable** (`render` raises), so a note cannot carry a token
29
+ whose template the kernel did not author. `extend()` returns a NEW registry (you
30
+ compose, you don't mutate), so the closed-set property is a real value-level
31
+ guarantee, not a hope a plugin could scribble over mid-run. The three built-in
32
+ forms are exactly docs/164 §6's three: `DIVERGED`, `VERIFY_NOT_SHIPPED`,
33
+ `TOOL_STREAM_REPEATING`.
34
+
35
+ Pure stdlib — no third-party imports, no I/O, no `dos`-internal deps (a leaf, like
36
+ `reasons.py`) — so `rewind` can import it as a sibling kernel leaf.
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ from dataclasses import dataclass, field
42
+ from typing import Iterable, Mapping
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class VerdictTokenSpec:
47
+ """One no-good token KIND, as data — the unit a workspace declares to add a form.
48
+
49
+ The `ReasonSpec` analogue. Fields:
50
+
51
+ kind — the closed token identifier (canonical UPPER_SNAKE; the registry
52
+ normalizes case on lookup). The thing a `VerdictToken` references;
53
+ a token whose kind is not a member of the active registry is not
54
+ renderable, which is the structural lock.
55
+ template — a `str.format_map`-style template OWNED BY THE KERNEL, rendered
56
+ over the token's structured `payload`. This is the only source of
57
+ the rendered prose — the caller supplies STRUCTURED FIELDS the
58
+ kernel computed (a sha, a count), never the sentence. An unknown
59
+ payload key is simply absent from the template; a template key the
60
+ payload omits renders as an empty placeholder (a defaulting map),
61
+ so a partial payload degrades to a still-kernel-authored string,
62
+ never a raise that would let an exception text leak.
63
+ fields — the payload keys this form expects (documentation + a `dos man`
64
+ projection; not enforced — an extra key is dropped, a missing key
65
+ blanks). Co-located with the kind by design (the DOM rule).
66
+ summary — one-line gloss of what the token MEANS (the man-page NAME line).
67
+ """
68
+
69
+ kind: str
70
+ template: str
71
+ fields: tuple[str, ...] = ()
72
+ summary: str = ""
73
+
74
+ def __post_init__(self) -> None:
75
+ if not self.kind or not self.kind.strip():
76
+ raise ValueError("VerdictTokenSpec.kind must be a non-empty string")
77
+ if not self.template or not self.template.strip():
78
+ raise ValueError(
79
+ f"VerdictTokenSpec {self.kind!r} must carry a non-empty render "
80
+ f"template — the kernel-authored string the token renders to "
81
+ f"(a token with no template would have no un-forged way to render)"
82
+ )
83
+
84
+ @property
85
+ def key(self) -> str:
86
+ """The normalized lookup key (UPPER, stripped) — what `get` matches."""
87
+ return self.kind.strip().upper()
88
+
89
+
90
+ class _BlankingDict(dict):
91
+ """A `format_map` backing dict that renders a missing key as an empty string.
92
+
93
+ So a template key the payload omits blanks (kernel-authored, still un-forged)
94
+ rather than raising a `KeyError` whose text could leak into a caller's except
95
+ handler. The kernel's template is the author either way.
96
+ """
97
+
98
+ def __missing__(self, key: str) -> str: # pragma: no cover - exercised via render
99
+ return ""
100
+
101
+
102
+ @dataclass(frozen=True)
103
+ class VerdictToken:
104
+ """One no-good annotation token: a `(kind, payload)` pair, NEVER a free string.
105
+
106
+ This is the (a)-class byte of the docs/164 §6 no-good note. The agent cannot
107
+ construct one that carries arbitrary prose: `kind` must resolve to a registry
108
+ spec to render at all, and `payload` is a `{str: str}` map of STRUCTURED fields
109
+ (a sha, a count, a turn index) that the kernel computed — fed into the
110
+ registry-owned template. There is no `text`/`message`/`critique` field a caller
111
+ fills freely. That absence is the lock the §6 grep-litmus relies on.
112
+
113
+ kind — the token identifier; must be a member of the registry that renders
114
+ it (else `render` raises — an undeclared kind has no kernel template).
115
+ payload — structured fields (all coerced to `str`), substituted into the
116
+ registry's template. Extra keys are dropped by the template; missing
117
+ keys blank. Never a sentence — a count, a sha, an index.
118
+ """
119
+
120
+ kind: str
121
+ payload: Mapping[str, str] = field(default_factory=dict)
122
+
123
+ def __post_init__(self) -> None:
124
+ # Coerce the payload to a plain, immutable-by-discipline str→str dict so a
125
+ # caller cannot smuggle a non-string (a callable, a nested structure that a
126
+ # template could `__str__` into prose) into a field slot.
127
+ object.__setattr__(
128
+ self,
129
+ "payload",
130
+ {str(k): str(v) for k, v in dict(self.payload).items()},
131
+ )
132
+
133
+ @property
134
+ def key(self) -> str:
135
+ return self.kind.strip().upper()
136
+
137
+
138
+ @dataclass(frozen=True)
139
+ class RewindTokenRegistry:
140
+ """A closed, ordered set of `VerdictTokenSpec`s — the active no-good vocabulary.
141
+
142
+ The `ReasonRegistry` analogue, immutable: `extend()` returns a NEW registry. A
143
+ process's active registry is a value, never a mutable global a plugin scribbles
144
+ on — which is what keeps "closed set" a real property. The kernel renders a
145
+ `VerdictToken` ONLY through this registry's template for the token's kind, so a
146
+ token whose kind is absent here is structurally unrenderable: there is no
147
+ code path by which generated prose, lacking a registered kind + template, can
148
+ become a rendered note byte.
149
+ """
150
+
151
+ specs: tuple[VerdictTokenSpec, ...] = ()
152
+
153
+ def __post_init__(self) -> None:
154
+ seen: set[str] = set()
155
+ for s in self.specs:
156
+ if s.key in seen:
157
+ raise ValueError(
158
+ f"duplicate token kind {s.kind!r} in registry — a no-good token "
159
+ f"kind is declared exactly once (a later declaration would shadow "
160
+ f"silently, the drift this registry exists to forbid)"
161
+ )
162
+ seen.add(s.key)
163
+
164
+ # -- lookup ------------------------------------------------------------
165
+ def get(self, kind: str | None) -> VerdictTokenSpec | None:
166
+ """The spec for `kind`, or None if not a member of this set."""
167
+ if not kind:
168
+ return None
169
+ k = kind.strip().upper()
170
+ for s in self.specs:
171
+ if s.key == k:
172
+ return s
173
+ return None
174
+
175
+ def is_known(self, kind: str | None) -> bool:
176
+ return self.get(kind) is not None
177
+
178
+ def kinds(self) -> tuple[str, ...]:
179
+ """Every declared kind, in declaration order."""
180
+ return tuple(s.key for s in self.specs)
181
+
182
+ # -- the render mechanism (the whole point) ---------------------------
183
+ def render(self, token: VerdictToken) -> str:
184
+ """Render `token` to its KERNEL-AUTHORED string. The only way a token becomes prose.
185
+
186
+ The string is composed from THIS registry's template for the token's kind +
187
+ the token's structured payload. The caller supplied neither — only the
188
+ structured fields. A token whose kind is not a member of this registry has
189
+ no kernel template, so it is NOT renderable: `render` raises `ValueError`.
190
+ That raise is the structural lock — a generated critique, having no
191
+ registered kind, can never reach a rendered note byte (the §6 litmus). A
192
+ recognised kind with a partial payload blanks the missing fields
193
+ (`_BlankingDict`) rather than raising, so a partial-but-honest token still
194
+ renders a kernel-authored string.
195
+ """
196
+ spec = self.get(token.kind)
197
+ if spec is None:
198
+ raise ValueError(
199
+ f"un-renderable no-good token kind {token.kind!r}: not a member of "
200
+ f"the active RewindTokenRegistry (known: {', '.join(self.kinds()) or '∅'}). "
201
+ f"A token the kernel has no template for cannot author un-forged note "
202
+ f"bytes — this is the docs/164 §6 generated-prose lock, working."
203
+ )
204
+ return spec.template.format_map(_BlankingDict(token.payload))
205
+
206
+ # -- composition (the hackability verb) -------------------------------
207
+ def extend(self, more: Iterable[VerdictTokenSpec]) -> "RewindTokenRegistry":
208
+ """Return a NEW registry with `more` appended. The one way to add forms.
209
+
210
+ Raises on a colliding kind (the same declared-exactly-once guard
211
+ `__post_init__` enforces) — a workspace re-declaring a built-in is a mistake
212
+ to surface, not silently honor.
213
+ """
214
+ return RewindTokenRegistry(specs=tuple(self.specs) + tuple(more))
215
+
216
+
217
+ # ---------------------------------------------------------------------------
218
+ # The built-in registry — exactly docs/164 §6's three no-good forms. Each
219
+ # template is KERNEL-AUTHORED; the {placeholders} are the structured fields the
220
+ # kernel computed (a sha, a count, a turn index), never a caller-supplied
221
+ # sentence. A workspace adds a form via `BASE_REWIND_TOKENS.extend([...])`.
222
+ # ---------------------------------------------------------------------------
223
+
224
+ # The closed kind identifiers (string constants so a builder references them by
225
+ # name without re-typing the literal — the lockstep `wedge_reason` uses).
226
+ KIND_DIVERGED = "DIVERGED"
227
+ KIND_VERIFY_NOT_SHIPPED = "VERIFY_NOT_SHIPPED"
228
+ KIND_TOOL_STREAM_REPEATING = "TOOL_STREAM_REPEATING"
229
+
230
+ BASE_REWIND_TOKENS = RewindTokenRegistry(specs=(
231
+ VerdictTokenSpec(
232
+ kind=KIND_DIVERGED,
233
+ template="resume = DIVERGED",
234
+ fields=(),
235
+ summary="ground truth moved past the resume point — the prior branch is a "
236
+ "dead end (resume.Resume.DIVERGED).",
237
+ ),
238
+ VerdictTokenSpec(
239
+ kind=KIND_VERIFY_NOT_SHIPPED,
240
+ template="verify = NOT_SHIPPED @ {sha}",
241
+ fields=("sha",),
242
+ summary="the claimed phase did not actually ship at the named SHA "
243
+ "(oracle.is_shipped → NOT_SHIPPED).",
244
+ ),
245
+ VerdictTokenSpec(
246
+ kind=KIND_TOOL_STREAM_REPEATING,
247
+ template="tool_stream = REPEATING ×{count} @ turn {turn}",
248
+ fields=("count", "turn"),
249
+ summary="the same (tool, args, result) recurred N times — the env's results "
250
+ "stopped advancing (tool_stream.classify_stream → REPEATING).",
251
+ ),
252
+ ))