dos-kernel 0.22.0__py3-none-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dos/__init__.py +261 -0
- dos/_bin/dos-hook.exe +0 -0
- dos/_filelock.py +255 -0
- dos/_job_policy.py +97 -0
- dos/_tree.py +145 -0
- dos/admission.py +433 -0
- dos/answer_shape.py +299 -0
- dos/arbiter.py +859 -0
- dos/archive_lock.py +266 -0
- dos/arg_provenance.py +814 -0
- dos/attest.py +472 -0
- dos/breaker.py +311 -0
- dos/churn.py +226 -0
- dos/claim_extract.py +229 -0
- dos/claim_ttl.py +150 -0
- dos/cli.py +8721 -0
- dos/commit_audit.py +666 -0
- dos/completion.py +466 -0
- dos/concurrency_class.py +154 -0
- dos/config.py +1380 -0
- dos/config_lint.py +464 -0
- dos/cooldown.py +390 -0
- dos/coverage.py +387 -0
- dos/dangling_intent.py +287 -0
- dos/data_class.py +397 -0
- dos/decisions.py +1274 -0
- dos/decisions_tui.py +251 -0
- dos/dispatch_top.py +740 -0
- dos/dispatch_top_tui.py +116 -0
- dos/drivers/__init__.py +40 -0
- dos/drivers/ci_status.py +630 -0
- dos/drivers/citation_resolve.py +703 -0
- dos/drivers/decision_stop.py +98 -0
- dos/drivers/export_file.py +173 -0
- dos/drivers/export_otlp.py +275 -0
- dos/drivers/export_statsd.py +242 -0
- dos/drivers/hook_dialects.py +391 -0
- dos/drivers/job.py +47 -0
- dos/drivers/llm_judge.py +360 -0
- dos/drivers/memory_recall.py +1231 -0
- dos/drivers/notify_slack.py +373 -0
- dos/drivers/notify_webhook.py +251 -0
- dos/drivers/operator_judge.py +114 -0
- dos/drivers/os_acceptance.py +228 -0
- dos/drivers/paste_log.py +132 -0
- dos/drivers/plan_scope.py +133 -0
- dos/drivers/self_improve.py +375 -0
- dos/drivers/similarity_judge.py +249 -0
- dos/drivers/state_diff.py +274 -0
- dos/drivers/supervisor.py +347 -0
- dos/drivers/watchdog.py +363 -0
- dos/drivers/workshop.py +160 -0
- dos/durable_schema.py +344 -0
- dos/effect_witness.py +393 -0
- dos/efficiency.py +318 -0
- dos/enforce.py +414 -0
- dos/enumerate.py +776 -0
- dos/env_print.py +378 -0
- dos/event_severity.py +258 -0
- dos/evidence.py +692 -0
- dos/exec_capability.py +256 -0
- dos/export_cursor.py +143 -0
- dos/exporter.py +320 -0
- dos/firing_label.py +353 -0
- dos/fleet_roll.py +226 -0
- dos/gate_classify.py +827 -0
- dos/gh4_coverage.py +179 -0
- dos/git_delta.py +122 -0
- dos/guard.py +215 -0
- dos/health.py +552 -0
- dos/help_summary.py +519 -0
- dos/home.py +934 -0
- dos/hook_binary.py +194 -0
- dos/hook_dialect.py +271 -0
- dos/hook_exit.py +191 -0
- dos/hook_install.py +437 -0
- dos/id_alloc.py +304 -0
- dos/improve.py +499 -0
- dos/intent_ledger.py +635 -0
- dos/interpret.py +176 -0
- dos/intervention.py +769 -0
- dos/intervention_eval.py +371 -0
- dos/journal_delta.py +308 -0
- dos/judge_eval.py +328 -0
- dos/judges.py +366 -0
- dos/lane_infer.py +127 -0
- dos/lane_journal.py +1001 -0
- dos/lane_lease.py +952 -0
- dos/lane_overlap.py +228 -0
- dos/lease_health.py +282 -0
- dos/lifecycle.py +211 -0
- dos/liveness.py +352 -0
- dos/lock_modes.py +185 -0
- dos/log_source.py +395 -0
- dos/loop_decide.py +1746 -0
- dos/marker_gate.py +254 -0
- dos/marker_sensor.py +396 -0
- dos/noop_streak.py +280 -0
- dos/notify.py +479 -0
- dos/observe.py +175 -0
- dos/oracle.py +1661 -0
- dos/overlap_eval.py +214 -0
- dos/overlap_policy.py +342 -0
- dos/packet_sidecar.py +267 -0
- dos/phase_shipped.py +1985 -0
- dos/pick_priority.py +225 -0
- dos/pickable.py +369 -0
- dos/picker_oracle.py +1037 -0
- dos/plan_board.py +513 -0
- dos/plan_board_tui.py +113 -0
- dos/plan_source.py +455 -0
- dos/posttool_sensor.py +528 -0
- dos/precursor_gate.py +499 -0
- dos/precursor_gate_eval.py +239 -0
- dos/preflight.py +825 -0
- dos/pretool_sensor.py +490 -0
- dos/proc_delta.py +181 -0
- dos/productivity.py +296 -0
- dos/provider_limit.py +242 -0
- dos/py.typed +4 -0
- dos/reason_morphology.py +299 -0
- dos/reasons.py +449 -0
- dos/reconcile.py +173 -0
- dos/recurring_wedge.py +206 -0
- dos/render.py +393 -0
- dos/result_state.py +468 -0
- dos/resume.py +578 -0
- dos/resume_evidence.py +293 -0
- dos/retention.py +344 -0
- dos/reward.py +372 -0
- dos/rewind.py +587 -0
- dos/rewind_evidence.py +168 -0
- dos/rewind_tokens.py +252 -0
- dos/run_id.py +342 -0
- dos/scope.py +520 -0
- dos/scope_source.py +382 -0
- dos/scout.py +982 -0
- dos/self_modify.py +209 -0
- dos/sibling_scan.py +569 -0
- dos/skills/EXAMPLES.md +584 -0
- dos/skills/dos-class-cycle/SKILL.md +107 -0
- dos/skills/dos-dispatch/SKILL.md +177 -0
- dos/skills/dos-dispatch-loop/SKILL.md +254 -0
- dos/skills/dos-goal-gate/SKILL.md +269 -0
- dos/skills/dos-next-up/SKILL.md +231 -0
- dos/skills/dos-promote/SKILL.md +114 -0
- dos/skills/dos-replan/SKILL.md +159 -0
- dos/skills/dos-replan-loop/SKILL.md +114 -0
- dos/skills/dos-self-improve/SKILL.md +213 -0
- dos/skills/dos-supervise-loop/SKILL.md +180 -0
- dos/skills/dos-unstick/SKILL.md +108 -0
- dos/skills/dos-witness-claim/SKILL.md +251 -0
- dos/stamp.py +1002 -0
- dos/state_health.py +387 -0
- dos/status.py +114 -0
- dos/stop_policy.py +334 -0
- dos/supervise.py +1014 -0
- dos/testwitness.py +392 -0
- dos/timeline.py +1027 -0
- dos/tokens.py +485 -0
- dos/tool_stream.py +393 -0
- dos/tool_stream_eval.py +226 -0
- dos/trace.py +524 -0
- dos/verdict.py +140 -0
- dos/verdict_cli.py +189 -0
- dos/verdict_journal.py +497 -0
- dos/verdict_rollup.py +217 -0
- dos/verdicts.py +181 -0
- dos/wedge_reason.py +282 -0
- dos_kernel-0.22.0.dist-info/METADATA +859 -0
- dos_kernel-0.22.0.dist-info/RECORD +178 -0
- dos_kernel-0.22.0.dist-info/WHEEL +5 -0
- dos_kernel-0.22.0.dist-info/entry_points.txt +39 -0
- dos_kernel-0.22.0.dist-info/licenses/LICENSE +21 -0
- dos_kernel-0.22.0.dist-info/top_level.txt +2 -0
- dos_mcp/__init__.py +52 -0
- dos_mcp/py.typed +2 -0
- dos_mcp/server.py +779 -0
dos/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
|
+
))
|