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/resume.py
ADDED
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
"""resume — the third ARIES phase: replay-to-the-last-verified-point, then PROPOSE re-dispatch (docs/107 §2,§3.3,§5).
|
|
2
|
+
|
|
3
|
+
> **A crashed or paused run is a stale self-report about unfinished work, so
|
|
4
|
+
> resuming it is the distrust primitive pointed at the run's own intent — record
|
|
5
|
+
> what it *meant* to do (distrusted), re-verify how far the *fossils* say it got,
|
|
6
|
+
> fold the difference into a residual and a non-forgeable re-entry SHA, and
|
|
7
|
+
> *propose* — never perform — the continuation.**
|
|
8
|
+
|
|
9
|
+
`lane_journal.replay` is the ARIES **redo** fold; `94 §3.2` named that DOS does not
|
|
10
|
+
own **undo**. Resume is the *third* phase the WAL framing implies: **analysis →
|
|
11
|
+
redo → CONTINUE**. This module is the pure verdict over a reconstructed intent —
|
|
12
|
+
`liveness.classify`'s sibling, field-for-field:
|
|
13
|
+
|
|
14
|
+
arbiter.arbitrate (request, live_leases, config) -> decision
|
|
15
|
+
liveness.classify (ProgressEvidence, policy) -> LivenessVerdict
|
|
16
|
+
resume.resume_plan (LedgerState, AncestryFacts, policy) -> ResumePlan
|
|
17
|
+
^ THIS module
|
|
18
|
+
|
|
19
|
+
All I/O — reading the ledger, asking git which claimed SHAs are in ancestry,
|
|
20
|
+
re-verifying a step on the non-forgeable rung — happens in the CALLER (the `dos
|
|
21
|
+
resume` CLI's evidence-gather), exactly as `liveness`'s git/journal reads happen
|
|
22
|
+
outside `classify`. `resume_plan` makes no subprocess, file, or clock call: the
|
|
23
|
+
ancestry membership is a field on `AncestryFacts`, never re-derived inside the
|
|
24
|
+
verdict. That is what lets the whole recovery LOGIC be replay-tested on frozen
|
|
25
|
+
ledger + frozen ancestry fixtures — no live multi-minute crashed run needed
|
|
26
|
+
(`docs/107 §3.3`, the `loop_decide`/`journal_delta` design value, restated for the
|
|
27
|
+
resume axis).
|
|
28
|
+
|
|
29
|
+
The belief/effect line (`docs/107 §2`, the `94 §2` checkpoint/restore split):
|
|
30
|
+
|
|
31
|
+
* **A resume point is a BELIEF the kernel may MINT** — "run R got verifiably as
|
|
32
|
+
far as SHA `abc123` (steps 1–2 of intent I); the residual is steps 3–5." An
|
|
33
|
+
epistemic claim over unforgeable git artifacts. The kernel is allowed to
|
|
34
|
+
produce it; `resume_plan` is where it does.
|
|
35
|
+
* **A resume is an EFFECT the kernel may only PROPOSE.** Re-spawning, re-acquiring
|
|
36
|
+
the lane, handing the worker the residual — those mutate the world. They live
|
|
37
|
+
behind a human (`dos resume --plan` prints the residual + re-entry SHA and
|
|
38
|
+
exits; a `decisions` emit-and-exit row prints the re-dispatch command) or a
|
|
39
|
+
driver. **The kernel never re-spawns and never re-runs the work** (the §8
|
|
40
|
+
non-goal, the `99` advisory-only floor on the resume axis).
|
|
41
|
+
|
|
42
|
+
The safety property (`docs/107 §5`, the load-bearing "*safely*"): the resume point
|
|
43
|
+
stands on the most-accountable fossil (git ancestry), NEVER on the ledger's
|
|
44
|
+
self-reported "I finished step 3." You resume *from the last committed, verified
|
|
45
|
+
SHA*, never the last *claimed* step — so re-execution re-does at most the
|
|
46
|
+
uncommitted tail, which is idempotent by construction (it produced no durable
|
|
47
|
+
effect, or it would be a commit). The dead run's `STEP_CLAIMED` records are treated
|
|
48
|
+
exactly as `103` treats a recalled memory: a prior commitment, re-verified against
|
|
49
|
+
ground truth at read time, never replayed as present fact.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
from __future__ import annotations
|
|
53
|
+
|
|
54
|
+
import enum
|
|
55
|
+
from dataclasses import dataclass
|
|
56
|
+
|
|
57
|
+
from dos.intent_ledger import LedgerState
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Resume(str, enum.Enum):
|
|
61
|
+
"""The typed resume verdict — four states, mutually exclusive (§3.3).
|
|
62
|
+
|
|
63
|
+
`str`-valued so it round-trips a `--json` token / exit-code map without a
|
|
64
|
+
lookup table (the `Liveness` / `gate_classify.Verdict` idiom).
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
RESUMABLE = "RESUMABLE" # clean resume-point SHA + non-empty residual: continue from here
|
|
68
|
+
COMPLETE = "COMPLETE" # residual empty — every declared step verified; nothing to resume
|
|
69
|
+
DIVERGED = "DIVERGED" # ground truth moved past the resume point — REFUSE, raise a decision
|
|
70
|
+
UNRESUMABLE = "UNRESUMABLE" # no INTENT / corrupt-past-fold / too-new schema: don't guess a residual
|
|
71
|
+
|
|
72
|
+
def __str__(self) -> str: # pragma: no cover - trivial
|
|
73
|
+
return self.value
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True)
|
|
77
|
+
class ResumePolicy:
|
|
78
|
+
"""The knobs that shape the resume verdict — policy, not mechanism.
|
|
79
|
+
|
|
80
|
+
The `LivenessPolicy` split: mechanism is the kernel, thresholds are config.
|
|
81
|
+
Defaults are GENERIC (no host tuning); a workspace could declare its own in
|
|
82
|
+
`dos.toml [resume]` (a future seam, like `[liveness]`).
|
|
83
|
+
|
|
84
|
+
require_nonforgeable_rung — when True (the §5 req-2 default), a step's
|
|
85
|
+
`STEP_VERIFIED` must have been minted on a NON-forgeable rung (`via` is
|
|
86
|
+
`file-path`/`registry`, never the forgeable subject-grep). A step whose
|
|
87
|
+
verified `via` is forgeable/empty is NOT counted toward the resume point
|
|
88
|
+
(it stays in the residual): a resume point built on a forgeable verdict is
|
|
89
|
+
worse than none — it would skip work that never really happened. The mint
|
|
90
|
+
(`intent_ledger.step_verified_entry`) is supposed to refuse a forgeable
|
|
91
|
+
rung already; this is the verdict-side belt to that suspenders.
|
|
92
|
+
treat_untagged_as_corrupt — when True, an UNTAGGED/legacy ledger record makes
|
|
93
|
+
the fold unsound (UNRESUMABLE). Default False: a legacy record is folded
|
|
94
|
+
permissively (the tolerant side of the `durable_schema.UNTAGGED` contract);
|
|
95
|
+
only an UNREADABLE_NEWER (too-new schema) forces UNRESUMABLE regardless.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
require_nonforgeable_rung: bool = True
|
|
99
|
+
treat_untagged_as_corrupt: bool = False
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
DEFAULT_POLICY = ResumePolicy()
|
|
103
|
+
|
|
104
|
+
# The verify rungs a step may stand on (§5 req 2). The forgeable subject-grep rung
|
|
105
|
+
# (`grep`/`subject`/`body-mention`/`direct`/… — anything that matches a phase token
|
|
106
|
+
# in commit PROSE) is DEFEATED by `git commit --allow-empty` (an empty commit whose
|
|
107
|
+
# subject names the step), so it is NOT a safe resume anchor. The non-forgeable
|
|
108
|
+
# rungs prove an ARTEFACT moved: the registry row (a ship-time stamp) or the
|
|
109
|
+
# file-path / diff-content overlap. These are the only `via` values
|
|
110
|
+
# `resume_evidence.verify_step` can MINT (it always writes `file-path`); the set is
|
|
111
|
+
# an ALLOW-LIST (fail-closed: an unknown `via` is treated as forgeable).
|
|
112
|
+
#
|
|
113
|
+
# IMPORTANT (the docs/107 §5 / docs/103 fix): `via` is an UNAUTHENTICATED HINT, not
|
|
114
|
+
# authority. The intent ledger is written BY the distrusted run, so a malicious run
|
|
115
|
+
# can append its own `STEP_VERIFIED {via: "file-path"}` for a step it never did.
|
|
116
|
+
# The kernel therefore RE-ADJUDICATES the non-forgeable rung at READ time
|
|
117
|
+
# (`resume_evidence.adjudicate_verified_steps` re-runs the footprint check on the
|
|
118
|
+
# claimed SHA) and the pure verdict consumes that re-adjudicated set
|
|
119
|
+
# (`AncestryFacts.steps_verified_at_read`), NEVER the stored `via`. `via` survives
|
|
120
|
+
# only as a forensic label + the policy belt-to-suspenders in `_verified_on_safe_rung`.
|
|
121
|
+
NONFORGEABLE_RUNGS = frozenset({"file-path", "registry"})
|
|
122
|
+
|
|
123
|
+
# The minimum hex length a prefix match in `AncestryFacts.contains` will honour
|
|
124
|
+
# (below it, only an exact match counts) — git's own abbreviated-SHA floor, so a
|
|
125
|
+
# 1–2 char claimed token can never spuriously collide with an unrelated ancestry
|
|
126
|
+
# commit. 7 is git's conventional short-SHA length.
|
|
127
|
+
_MIN_PREFIX = 7
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass(frozen=True)
|
|
131
|
+
class AncestryFacts:
|
|
132
|
+
"""The boundary's RE-ADJUDICATED evidence — gathered by the CALLER (§3.3, §5).
|
|
133
|
+
|
|
134
|
+
Frozen and handed to the pure verdict (the `ProgressEvidence` shape). The CLI's
|
|
135
|
+
evidence-gather (`resume_evidence.gather_ancestry`) decodes the ledger
|
|
136
|
+
(`intent_ledger.replay`), asks git which SHAs are reachable from HEAD AND
|
|
137
|
+
RE-RUNS the non-forgeable footprint check on each claimed step — never trusting
|
|
138
|
+
the agent-written `STEP_VERIFIED` record's authority (it is written by the
|
|
139
|
+
distrusted run; docs/107 §5, the docs/103 move).
|
|
140
|
+
|
|
141
|
+
shas_in_ancestry — the set of commit SHAs reachable from HEAD on the served
|
|
142
|
+
workspace. A claimed step whose SHA is NOT in here is a
|
|
143
|
+
step the agent claimed but never landed — fail-closed, the
|
|
144
|
+
resume must redo it.
|
|
145
|
+
steps_verified_at_read — the set of STEP IDS the boundary RE-ADJUDICATED as
|
|
146
|
+
standing on the non-forgeable rung AT READ TIME (their
|
|
147
|
+
claimed/recorded SHA is in ancestry AND its commit footprint
|
|
148
|
+
is real, not `--allow-empty`). THIS is the authority the
|
|
149
|
+
pure verdict trusts for "done" — NOT the agent-written
|
|
150
|
+
`STEP_VERIFIED.via`. A forged `STEP_VERIFIED` pointing at an
|
|
151
|
+
unrelated real commit fails the footprint-region re-check and
|
|
152
|
+
is absent here, so it is redone. Empty ⇒ no step re-verified
|
|
153
|
+
(the safe floor when the boundary couldn't re-check).
|
|
154
|
+
head_sha — the workspace's current HEAD (DIVERGED framing / forensics).
|
|
155
|
+
lane_advanced_past_resume — True iff ground truth advanced on the run's lane
|
|
156
|
+
PAST the would-be resume point in a way the residual can't
|
|
157
|
+
be cleanly grafted onto. The CALLER computes it; the verdict
|
|
158
|
+
consumes it. True ⇒ DIVERGED (§5 req 3).
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
shas_in_ancestry: frozenset[str] = frozenset()
|
|
162
|
+
steps_verified_at_read: frozenset[str] = frozenset()
|
|
163
|
+
head_sha: str = ""
|
|
164
|
+
lane_advanced_past_resume: bool = False
|
|
165
|
+
|
|
166
|
+
def contains(self, sha: str) -> bool:
|
|
167
|
+
"""True iff `sha` is in ancestry. Prefix-tolerant but collision-guarded.
|
|
168
|
+
|
|
169
|
+
Git short SHAs and full SHAs both appear (the ledger stores whatever the
|
|
170
|
+
agent claimed; `git_delta` returns short). Match by prefix in EITHER
|
|
171
|
+
direction so a 7-char claimed sha matches a 40-char ancestry sha and vice
|
|
172
|
+
versa — the same tolerant comparison `oracle` does. To foreclose a SPURIOUS
|
|
173
|
+
prefix collision (a 1–2 char claimed token matching an unrelated ancestry
|
|
174
|
+
sha), a prefix match requires the shorter side to be ≥ `_MIN_PREFIX` hex
|
|
175
|
+
chars; below that only an EXACT match counts. An empty sha never matches.
|
|
176
|
+
"""
|
|
177
|
+
s = (sha or "").strip().lower()
|
|
178
|
+
if not s:
|
|
179
|
+
return False
|
|
180
|
+
for a in self.shas_in_ancestry:
|
|
181
|
+
a = (a or "").strip().lower()
|
|
182
|
+
if not a:
|
|
183
|
+
continue
|
|
184
|
+
if s == a:
|
|
185
|
+
return True
|
|
186
|
+
# A prefix match is only honoured when the SHORTER side is long enough
|
|
187
|
+
# to be an unambiguous abbreviated git SHA — git itself rejects ambiguous
|
|
188
|
+
# short SHAs, and a 1–2 char token must not match an unrelated commit.
|
|
189
|
+
shorter = min(len(s), len(a))
|
|
190
|
+
if shorter >= _MIN_PREFIX and (s.startswith(a) or a.startswith(s)):
|
|
191
|
+
return True
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@dataclass(frozen=True)
|
|
196
|
+
class ResumePlan:
|
|
197
|
+
"""The single verdict `resume_plan` returns, with the derivation echoed back.
|
|
198
|
+
|
|
199
|
+
`verdict` is the typed `Resume`. `reason` is the operator-facing one-liner.
|
|
200
|
+
`resume_sha` is the minted re-entry point (the newest commit backing a
|
|
201
|
+
contiguous prefix of verified steps; "" when none / COMPLETE / UNRESUMABLE).
|
|
202
|
+
`residual` is the ordered remaining step ids (declared-minus-verified, with a
|
|
203
|
+
claimed-but-unverified step staying IN the residual — fail-closed). `verified`
|
|
204
|
+
is the contiguous-verified prefix the resume point rests on. `predecessor_run_id`
|
|
205
|
+
is the dead/parked run; `already_proposed` echoes the §5-req-4 idempotence flag.
|
|
206
|
+
`to_dict` is the `--json` shape (the `LivenessVerdict.to_dict` idiom)."""
|
|
207
|
+
|
|
208
|
+
verdict: Resume
|
|
209
|
+
reason: str
|
|
210
|
+
run_id: str
|
|
211
|
+
resume_sha: str = ""
|
|
212
|
+
residual: tuple[str, ...] = ()
|
|
213
|
+
verified: tuple[str, ...] = ()
|
|
214
|
+
predecessor_run_id: str = ""
|
|
215
|
+
already_proposed: bool = False
|
|
216
|
+
|
|
217
|
+
def to_dict(self) -> dict:
|
|
218
|
+
return {
|
|
219
|
+
"verdict": self.verdict.value,
|
|
220
|
+
"reason": self.reason,
|
|
221
|
+
"run_id": self.run_id,
|
|
222
|
+
"resume_sha": self.resume_sha,
|
|
223
|
+
"residual": list(self.residual),
|
|
224
|
+
"verified": list(self.verified),
|
|
225
|
+
"predecessor_run_id": self.predecessor_run_id,
|
|
226
|
+
"already_proposed": self.already_proposed,
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _verified_on_safe_rung(state: LedgerState, step_id: str, ancestry: AncestryFacts,
|
|
231
|
+
policy: ResumePolicy) -> bool:
|
|
232
|
+
"""True iff `step_id` is a safe resume anchor — RE-ADJUDICATED at read, not trusted.
|
|
233
|
+
|
|
234
|
+
The docs/107 §5 / docs/103 fix. The intent ledger is written BY the distrusted
|
|
235
|
+
run, so a `STEP_VERIFIED` record's `via`/`sha` are an UNAUTHENTICATED HINT, never
|
|
236
|
+
authority. A step counts as done ONLY when the BOUNDARY re-adjudicated it at read
|
|
237
|
+
time — i.e. `step_id ∈ ancestry.steps_verified_at_read`, the set
|
|
238
|
+
`resume_evidence.adjudicate_verified_steps` built by RE-RUNNING the non-forgeable
|
|
239
|
+
footprint check (`step_stands_on_nonforgeable_rung`) on the claimed SHA. A forged
|
|
240
|
+
`STEP_VERIFIED {via: "file-path"}` pointing at an unrelated real commit fails that
|
|
241
|
+
re-check (its footprint isn't the step's work) and is absent from the set, so it
|
|
242
|
+
is redone — the §5 break the adversarial review found, closed.
|
|
243
|
+
|
|
244
|
+
The stored `via` survives only as the policy belt-to-suspenders: when
|
|
245
|
+
`require_nonforgeable_rung` is True (the default) a record whose `via` is itself
|
|
246
|
+
forgeable is rejected even if the boundary somehow re-adjudicated it, and the
|
|
247
|
+
in-ancestry guard defends against a re-adjudicated SHA later rewritten out of
|
|
248
|
+
history. The AUTHORITY is `steps_verified_at_read`; `via`/`contains` only narrow
|
|
249
|
+
it further (fail-closed). When the boundary supplied NO re-adjudication (an empty
|
|
250
|
+
`steps_verified_at_read` — a pure test or a boundary that couldn't re-check), the
|
|
251
|
+
safe floor is "nothing verified," so a stored `STEP_VERIFIED` alone never counts.
|
|
252
|
+
"""
|
|
253
|
+
if step_id not in ancestry.steps_verified_at_read:
|
|
254
|
+
return False
|
|
255
|
+
vs = state.verified.get(step_id)
|
|
256
|
+
if vs is None or not vs.sha:
|
|
257
|
+
return False
|
|
258
|
+
if not ancestry.contains(vs.sha):
|
|
259
|
+
return False
|
|
260
|
+
if policy.require_nonforgeable_rung and vs.via not in NONFORGEABLE_RUNGS:
|
|
261
|
+
return False
|
|
262
|
+
return True
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def resume_plan(
|
|
266
|
+
state: LedgerState,
|
|
267
|
+
ancestry: AncestryFacts,
|
|
268
|
+
policy: ResumePolicy = DEFAULT_POLICY,
|
|
269
|
+
) -> ResumePlan:
|
|
270
|
+
"""Compute the resume verdict from a folded ledger + ancestry facts. PURE — no I/O.
|
|
271
|
+
|
|
272
|
+
The fold (`docs/107 §3.3`):
|
|
273
|
+
1. **UNRESUMABLE floor first.** No INTENT record (nothing declared → no
|
|
274
|
+
residual to ground), OR an UNREADABLE_NEWER schema record (this kernel is
|
|
275
|
+
too old to soundly read the ledger; refuse, don't guess — §6), OR a corrupt
|
|
276
|
+
fold the policy treats as unsound: return UNRESUMABLE. *Don't guess a
|
|
277
|
+
residual you can't ground* (the `94 §4.2` INSUFFICIENT_DATA twin).
|
|
278
|
+
2. **Compute the verified set, fail-closed.** A declared step counts as done
|
|
279
|
+
ONLY if `_verified_on_safe_rung` (a `STEP_VERIFIED` whose SHA is in
|
|
280
|
+
ancestry, on a non-forgeable rung). A `STEP_CLAIMED` without such a
|
|
281
|
+
verification — including one the agent claimed but never landed — is NOT
|
|
282
|
+
done; it stays in the residual (the agent must redo it).
|
|
283
|
+
3. **Residual + resume point.** residual = declared_steps minus the verified
|
|
284
|
+
set (order preserved). The resume-point SHA is the SHA backing the LAST
|
|
285
|
+
step of the *contiguous verified prefix* — the last point past which
|
|
286
|
+
nothing is confirmed. (Contiguous: a hole — step 2 verified but step 1 not
|
|
287
|
+
— means the resume must restart from before the hole, so only the unbroken
|
|
288
|
+
leading run of verified steps anchors the point.)
|
|
289
|
+
4. **DIVERGED if ground truth moved past it.** If `ancestry
|
|
290
|
+
.lane_advanced_past_resume`, the lane advanced past the resume point in a
|
|
291
|
+
way the residual can't cleanly graft onto → DIVERGED (refuse + raise a
|
|
292
|
+
decision, never overwrite fresh work; §5 req 3).
|
|
293
|
+
5. **COMPLETE if the residual is empty** — every declared step verified; the
|
|
294
|
+
run finished, it just never wrote a clean terminal record.
|
|
295
|
+
6. **RESUMABLE otherwise** — a clean resume point (or the start SHA when no
|
|
296
|
+
step is verified yet) + a non-empty residual: continue from here.
|
|
297
|
+
|
|
298
|
+
The verdict is advisory: it MINTS the resume point and computes the residual;
|
|
299
|
+
the act of continuing is a driver's/human's (the §8 non-goal, the `99` floor).
|
|
300
|
+
"""
|
|
301
|
+
rid = state.run_id
|
|
302
|
+
|
|
303
|
+
# 1. The UNRESUMABLE floor.
|
|
304
|
+
if state.unreadable_newer:
|
|
305
|
+
return ResumePlan(
|
|
306
|
+
verdict=Resume.UNRESUMABLE,
|
|
307
|
+
reason=(
|
|
308
|
+
"ledger contains a record this kernel is too OLD to read soundly "
|
|
309
|
+
"(schema newer than understood) — refusing to guess a residual from "
|
|
310
|
+
"a misread intent; run the explicit migration fold (§6)"
|
|
311
|
+
),
|
|
312
|
+
run_id=rid,
|
|
313
|
+
)
|
|
314
|
+
if policy.treat_untagged_as_corrupt and state.corrupt_lines > 0:
|
|
315
|
+
return ResumePlan(
|
|
316
|
+
verdict=Resume.UNRESUMABLE,
|
|
317
|
+
reason=(
|
|
318
|
+
f"ledger has {state.corrupt_lines} corrupt/unreadable record(s) and "
|
|
319
|
+
f"policy treats those as an unsound fold — refusing to guess a residual"
|
|
320
|
+
),
|
|
321
|
+
run_id=rid,
|
|
322
|
+
)
|
|
323
|
+
if not state.has_intent:
|
|
324
|
+
return ResumePlan(
|
|
325
|
+
verdict=Resume.UNRESUMABLE,
|
|
326
|
+
reason=(
|
|
327
|
+
"no INTENT record in the ledger — the run declared no goal, so there "
|
|
328
|
+
"is no residual to ground; nothing to resume (the honest floor)"
|
|
329
|
+
),
|
|
330
|
+
run_id=rid,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
declared = list(state.declared_steps)
|
|
334
|
+
already = bool(state.resume_proposed)
|
|
335
|
+
# The start SHA is the fallback anchor, but it comes from the agent's INTENT
|
|
336
|
+
# record — a SELF-REPORT (docs/107 §3.2). Echoing it as the "non-forgeable
|
|
337
|
+
# re-entry SHA" without checking ancestry would be the docs/103 disease (trusting
|
|
338
|
+
# a self-report) inside the kernel built to refuse it. Gate it: only an
|
|
339
|
+
# in-ancestry start SHA is a real anchor; otherwise drop to "" (force the driver
|
|
340
|
+
# to re-derive from HEAD) — never echo an unverified self-reported SHA.
|
|
341
|
+
safe_start = state.start_sha if ancestry.contains(state.start_sha) else ""
|
|
342
|
+
|
|
343
|
+
# A run with a free-form goal and NO enumerated steps: re-enter from the
|
|
344
|
+
# (ancestry-checked) start SHA with the whole goal as the residual. We cannot
|
|
345
|
+
# compute a step-granular resume point, so the goal itself is the single residual
|
|
346
|
+
# unit. DIVERGED still applies — a free-form resume must NOT overwrite fresh work
|
|
347
|
+
# any more than an enumerated one (the §5 req-3 refusal has no free-form carve-out).
|
|
348
|
+
if not declared:
|
|
349
|
+
residual_goal = (state.goal or f"{state.plan} {state.phase}".strip() or "(declared goal)",)
|
|
350
|
+
if ancestry.lane_advanced_past_resume:
|
|
351
|
+
return ResumePlan(
|
|
352
|
+
verdict=Resume.DIVERGED,
|
|
353
|
+
reason=(
|
|
354
|
+
f"free-form goal but ground truth advanced past the resume point "
|
|
355
|
+
f"{safe_start[:12] or '(start)'} on this run's lane — refusing to "
|
|
356
|
+
f"re-do the whole goal over fresh work (§5 req 3 applies to "
|
|
357
|
+
f"free-form resume too)"
|
|
358
|
+
),
|
|
359
|
+
run_id=rid, resume_sha=safe_start, residual=residual_goal,
|
|
360
|
+
verified=(), already_proposed=already,
|
|
361
|
+
)
|
|
362
|
+
return ResumePlan(
|
|
363
|
+
verdict=Resume.RESUMABLE,
|
|
364
|
+
reason=(
|
|
365
|
+
"no enumerated steps — re-enter from the run's start SHA with the "
|
|
366
|
+
"whole declared goal as the residual (step-granular resume needs a "
|
|
367
|
+
"declared step list)"
|
|
368
|
+
+ ("" if safe_start else "; start SHA not in ancestry, re-derive from HEAD")
|
|
369
|
+
),
|
|
370
|
+
run_id=rid,
|
|
371
|
+
resume_sha=safe_start,
|
|
372
|
+
residual=residual_goal,
|
|
373
|
+
verified=(),
|
|
374
|
+
already_proposed=already,
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
# 2 + 3. The verified set (fail-closed, RE-ADJUDICATED) and the contiguous prefix.
|
|
378
|
+
contiguous_prefix: list[str] = []
|
|
379
|
+
broken = False
|
|
380
|
+
for sid in declared:
|
|
381
|
+
is_done = _verified_on_safe_rung(state, sid, ancestry, policy)
|
|
382
|
+
if is_done and not broken:
|
|
383
|
+
contiguous_prefix.append(sid)
|
|
384
|
+
elif not is_done:
|
|
385
|
+
broken = True # first hole — nothing at/after it counts as done for resume
|
|
386
|
+
|
|
387
|
+
# The residual = declared MINUS the CONTIGUOUS verified prefix (NOT minus the full
|
|
388
|
+
# verified set). A step verified but DOWNSTREAM of a hole (s2 verified, s1 not)
|
|
389
|
+
# must still be redone — the resume restarts from before the hole, so everything
|
|
390
|
+
# at/after it is residual. Basing the residual on the contiguous prefix keeps the
|
|
391
|
+
# coverage invariant `verified ∪ residual == declared` AND ensures no residual
|
|
392
|
+
# step is excluded while the re-entry SHA sits before it (the disagreement the
|
|
393
|
+
# adversarial review found). `verified` (reported) == the contiguous prefix.
|
|
394
|
+
prefix_set = set(contiguous_prefix)
|
|
395
|
+
residual = tuple(s for s in declared if s not in prefix_set)
|
|
396
|
+
|
|
397
|
+
# The resume-point SHA = the SHA backing the LAST contiguous-verified step (the
|
|
398
|
+
# last point past which nothing is confirmed). No verified prefix ⇒ the
|
|
399
|
+
# ancestry-checked start SHA (re-enter from the start of the run's work).
|
|
400
|
+
if contiguous_prefix:
|
|
401
|
+
resume_sha = state.verified[contiguous_prefix[-1]].sha
|
|
402
|
+
else:
|
|
403
|
+
resume_sha = safe_start
|
|
404
|
+
|
|
405
|
+
# 4. COMPLETE before DIVERGED — a fully-finished run (empty residual) is DONE, not
|
|
406
|
+
# diverged: there is no stale residual to graft, so lane movement past it is
|
|
407
|
+
# irrelevant. (Checking DIVERGED first mislabelled a finished run and defeated
|
|
408
|
+
# its GC — the adversarial-review high finding.)
|
|
409
|
+
if not residual:
|
|
410
|
+
return ResumePlan(
|
|
411
|
+
verdict=Resume.COMPLETE,
|
|
412
|
+
reason=(
|
|
413
|
+
f"all {len(declared)} declared step(s) verified against ancestry — "
|
|
414
|
+
f"nothing to resume; the run finished, it just never wrote a clean "
|
|
415
|
+
f"terminal record"
|
|
416
|
+
),
|
|
417
|
+
run_id=rid,
|
|
418
|
+
resume_sha=resume_sha,
|
|
419
|
+
residual=(),
|
|
420
|
+
verified=tuple(contiguous_prefix),
|
|
421
|
+
already_proposed=already,
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
# 5. DIVERGED — there IS a residual AND ground truth moved past the resume point.
|
|
425
|
+
if ancestry.lane_advanced_past_resume:
|
|
426
|
+
return ResumePlan(
|
|
427
|
+
verdict=Resume.DIVERGED,
|
|
428
|
+
reason=(
|
|
429
|
+
f"ground truth advanced past the resume point {resume_sha[:12] or '(start)'} "
|
|
430
|
+
f"on this run's lane — a successor or a human committed there; refusing "
|
|
431
|
+
f"to graft {len(residual)} stale residual step(s) over fresh work "
|
|
432
|
+
f"(merge-conflict-as-verdict, §5 req 3)"
|
|
433
|
+
),
|
|
434
|
+
run_id=rid,
|
|
435
|
+
resume_sha=resume_sha,
|
|
436
|
+
residual=residual,
|
|
437
|
+
verified=tuple(contiguous_prefix),
|
|
438
|
+
already_proposed=already,
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
# 6. RESUMABLE — a clean resume point + a non-empty residual.
|
|
442
|
+
n_claimed_unverified = sum(
|
|
443
|
+
1 for s in residual if s in state.claimed and s not in prefix_set
|
|
444
|
+
)
|
|
445
|
+
claimed_note = (
|
|
446
|
+
f" ({n_claimed_unverified} were CLAIMED but not re-verified against ancestry "
|
|
447
|
+
f"— the resume must redo them)" if n_claimed_unverified else ""
|
|
448
|
+
)
|
|
449
|
+
return ResumePlan(
|
|
450
|
+
verdict=Resume.RESUMABLE,
|
|
451
|
+
reason=(
|
|
452
|
+
f"re-verified {len(contiguous_prefix)}/{len(declared)} step(s) against "
|
|
453
|
+
f"ancestry; resume from {resume_sha[:12] or '(start SHA)'} with "
|
|
454
|
+
f"{len(residual)} residual step(s){claimed_note}"
|
|
455
|
+
),
|
|
456
|
+
run_id=rid,
|
|
457
|
+
resume_sha=resume_sha,
|
|
458
|
+
residual=residual,
|
|
459
|
+
verified=tuple(contiguous_prefix),
|
|
460
|
+
already_proposed=already,
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
# --------------------------------------------------------------------------
|
|
465
|
+
# Reachability — the docs/106 GC verdict, extended from leases to unfinished work (§4).
|
|
466
|
+
# --------------------------------------------------------------------------
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
class Reachability(str, enum.Enum):
|
|
470
|
+
"""Is a run-dir GARBAGE (collectible) or REACHABLE (retain)? — the §4 GC clause.
|
|
471
|
+
|
|
472
|
+
`docs/106`'s reachability-is-a-verdict rule, extended from leases to *unfinished
|
|
473
|
+
work*: **what is reachable is what an adjudicator says can still make progress,
|
|
474
|
+
not what holds a reference or beat a clock.** A refcount/TTL is UNSOUND here — a
|
|
475
|
+
crashed run's `intent.jsonl` holds a resumable residual that no live reference
|
|
476
|
+
points at, yet it is NOT garbage; and a SUSPENDED (parked) run released its lane
|
|
477
|
+
but its ledger is reachable, not collectible. So reachability is ADJUDICATED (the
|
|
478
|
+
`ResumePlan`), never counted.
|
|
479
|
+
|
|
480
|
+
`str`-valued for the `--json` token / exit-code idiom.
|
|
481
|
+
"""
|
|
482
|
+
|
|
483
|
+
REACHABLE = "REACHABLE" # still resumable (or parked-and-resumable) — RETAIN regardless of age
|
|
484
|
+
COLLECTIBLE = "COLLECTIBLE" # terminal-COMPLETE or UNRESUMABLE — safe to GC the run-dir
|
|
485
|
+
|
|
486
|
+
def __str__(self) -> str: # pragma: no cover - trivial
|
|
487
|
+
return self.value
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
@dataclass(frozen=True)
|
|
491
|
+
class ReachabilityVerdict:
|
|
492
|
+
"""The typed run-dir reachability verdict + the resume verdict it rests on.
|
|
493
|
+
|
|
494
|
+
Carries the `ResumePlan.verdict` it derived from + whether the run is SUSPENDED
|
|
495
|
+
so a surfaced GC decision is legible ("retained: SUSPENDED-RESUMABLE, parked by
|
|
496
|
+
the operator" vs "collectible: COMPLETE — every step verified"). `to_dict` is
|
|
497
|
+
the `--json` shape.
|
|
498
|
+
"""
|
|
499
|
+
|
|
500
|
+
reachability: Reachability
|
|
501
|
+
reason: str
|
|
502
|
+
run_id: str
|
|
503
|
+
resume_verdict: Resume
|
|
504
|
+
suspended: bool = False
|
|
505
|
+
|
|
506
|
+
@property
|
|
507
|
+
def is_collectible(self) -> bool:
|
|
508
|
+
return self.reachability is Reachability.COLLECTIBLE
|
|
509
|
+
|
|
510
|
+
def to_dict(self) -> dict:
|
|
511
|
+
return {
|
|
512
|
+
"reachability": self.reachability.value,
|
|
513
|
+
"reason": self.reason,
|
|
514
|
+
"run_id": self.run_id,
|
|
515
|
+
"resume_verdict": self.resume_verdict.value,
|
|
516
|
+
"suspended": self.suspended,
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def classify_run_dir_reachability(
|
|
521
|
+
state: LedgerState,
|
|
522
|
+
plan: ResumePlan,
|
|
523
|
+
) -> ReachabilityVerdict:
|
|
524
|
+
"""Is this run-dir garbage? PURE — over a folded ledger + its resume plan (§4).
|
|
525
|
+
|
|
526
|
+
The single §4 clause, stated exactly: **a run-dir is garbage only if its run is
|
|
527
|
+
terminal-COMPLETE or its resume plan is UNRESUMABLE; a SUSPENDED-with-RESUMABLE
|
|
528
|
+
run-dir is retained regardless of age.** Concretely:
|
|
529
|
+
|
|
530
|
+
* RESUMABLE → REACHABLE (a residual a successor can still make progress on —
|
|
531
|
+
this is the whole point of the ledger; never GC it). A SUSPENDED-RESUMABLE
|
|
532
|
+
run is the same verdict via a different door (`docs/107 §4`): pause made
|
|
533
|
+
scavenge-immune.
|
|
534
|
+
* DIVERGED → REACHABLE (it needs a HUMAN decision, not a reaper — collecting
|
|
535
|
+
it would silently drop a conflict that must be surfaced; the merge-conflict-
|
|
536
|
+
as-verdict rule keeps the evidence around).
|
|
537
|
+
* COMPLETE → COLLECTIBLE (every declared step verified — the run finished; its
|
|
538
|
+
ledger is forensics, safe to reap under the host's retention window).
|
|
539
|
+
* UNRESUMABLE→ COLLECTIBLE (no INTENT / unreadable-newer / unsound fold — there
|
|
540
|
+
is nothing to resume, so the run-dir is not holding recoverable work).
|
|
541
|
+
|
|
542
|
+
The age/TTL is deliberately ABSENT from this verdict (the `docs/106` unsound-
|
|
543
|
+
refcount lesson): retention is decided by the ADJUDICATOR (can it still make
|
|
544
|
+
progress?), never by a clock. A host's reaper may add an age GRACE *on top* —
|
|
545
|
+
"collectible AND older than N days" — but it may never reap a REACHABLE run-dir
|
|
546
|
+
no matter how old, which is the safety property this clause guarantees.
|
|
547
|
+
"""
|
|
548
|
+
rid = plan.run_id or state.run_id
|
|
549
|
+
susp = state.suspended
|
|
550
|
+
if plan.verdict is Resume.RESUMABLE:
|
|
551
|
+
door = "SUSPENDED-RESUMABLE (parked, scavenge-immune)" if susp else "RESUMABLE"
|
|
552
|
+
return ReachabilityVerdict(
|
|
553
|
+
reachability=Reachability.REACHABLE,
|
|
554
|
+
reason=(
|
|
555
|
+
f"{door} — a residual a successor can still make progress on; "
|
|
556
|
+
f"retained regardless of age (reachability is adjudicated, not "
|
|
557
|
+
f"refcounted — docs/106)"
|
|
558
|
+
),
|
|
559
|
+
run_id=rid, resume_verdict=plan.verdict, suspended=susp,
|
|
560
|
+
)
|
|
561
|
+
if plan.verdict is Resume.DIVERGED:
|
|
562
|
+
return ReachabilityVerdict(
|
|
563
|
+
reachability=Reachability.REACHABLE,
|
|
564
|
+
reason=(
|
|
565
|
+
"DIVERGED — needs a human decision, not a reaper; retained so the "
|
|
566
|
+
"conflict is surfaced, never silently collected"
|
|
567
|
+
),
|
|
568
|
+
run_id=rid, resume_verdict=plan.verdict, suspended=susp,
|
|
569
|
+
)
|
|
570
|
+
# COMPLETE or UNRESUMABLE — nothing recoverable; collectible (host adds an age grace).
|
|
571
|
+
why = ("COMPLETE — every declared step verified; the run finished"
|
|
572
|
+
if plan.verdict is Resume.COMPLETE
|
|
573
|
+
else "UNRESUMABLE — no recoverable work to resume")
|
|
574
|
+
return ReachabilityVerdict(
|
|
575
|
+
reachability=Reachability.COLLECTIBLE,
|
|
576
|
+
reason=f"{why}; the run-dir holds no resumable residual — safe to GC",
|
|
577
|
+
run_id=rid, resume_verdict=plan.verdict, suspended=susp,
|
|
578
|
+
)
|