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/reasons.py ADDED
@@ -0,0 +1,449 @@
1
+ """The block-reason registry — the closed refusal vocabulary, *as data*.
2
+
3
+ This is the hackability seam for the kernel's single most-important syscall
4
+ (dispatch-os-vision §6 ranks structured refusal first): the closed
5
+ `reason_class` set a no-pick / blocked verdict may carry. Before this module the
6
+ set was a hardcoded enum in `dos.wedge_reason`; adding a reason meant editing the
7
+ package. That is the same mechanism/policy coupling the lane taxonomy already
8
+ broke — `LaneTaxonomy` lifted the job repo's hardcoded `_CLUSTERS` constants into
9
+ per-workspace `SubstrateConfig` data so "the arbiter never mentions a domain lane
10
+ name." This module does the same for reasons: the *mechanism* (emit / verify /
11
+ refuse / man, all keyed on a reason's fields) lives here; the *set of reasons* is
12
+ per-workspace data a host declares.
13
+
14
+ Why a registry and not a runtime-mutable enum
15
+ ==============================================
16
+
17
+ The load-bearing invariant the kernel exists to protect is that every reason is
18
+ **simultaneously emittable, verifiable, and refusable** — the lockstep the
19
+ `wedge_reason` ↔ `picker_oracle` test pins, and the completeness rail DOM's
20
+ `man --check` wants ("no runtime name without a definition"). A monkeypatched
21
+ enum would silently re-open the `UNCLASSIFIED` prose-drift the kernel was built
22
+ to close. So hackability is NOT "mutate the enum at runtime"; it is "**declare
23
+ your closed set once, as data, and let every consumer derive from that single
24
+ declaration.**" A `ReasonRegistry` is exactly one such declaration: closed (you
25
+ can enumerate it), verifiable (every entry carries its category + refusal-ness +
26
+ fix), and projectable (the `man` renderer reads these fields directly).
27
+
28
+ The shape
29
+ =========
30
+
31
+ * `ReasonSpec` — one reason as data: its token, the coarse `category` it rolls
32
+ up to (the `picker_oracle.NoPickCause` value string), whether carrying it
33
+ means *refuse* (route to /replan) vs *advisory*, and the curated `fix` /
34
+ `see_also` text the man-page projects. Co-locating `fix`/`see_also` with the
35
+ token (rather than in a separate doc) is DOM Design-rule 1: the one bit of
36
+ curated prose lives beside the symbol so it cannot drift away from it.
37
+ * `ReasonRegistry` — a closed, ordered set of `ReasonSpec`s with lookup +
38
+ membership + category-map + refusal-set projections. Immutable once built
39
+ (`extend()` returns a NEW registry — you compose, you don't mutate), so the
40
+ "closed set" property holds: a process's active registry is a value, not a
41
+ mutable global a plugin can scribble on mid-run.
42
+
43
+ `BASE_REASONS` is the built-in registry — the seven reasons the job spine shipped
44
+ as a closed enum, reproduced verbatim so `dos.wedge_reason` stays byte-compatible
45
+ and the existing lockstep test passes unchanged. A workspace that wants its own
46
+ reasons calls `BASE_REASONS.extend([...])` (or declares them in `dos.toml`, which
47
+ the loader turns into the same `extend` call) and installs the result on its
48
+ `SubstrateConfig`.
49
+
50
+ Pure stdlib — no third-party imports, no I/O — so `wedge_reason` / `picker_oracle`
51
+ / the man renderer can all import it as a leaf, exactly as they import the old
52
+ `wedge_reason` enum.
53
+ """
54
+
55
+ from __future__ import annotations
56
+
57
+ from dataclasses import dataclass
58
+ from pathlib import Path
59
+ from typing import Iterable
60
+
61
+
62
+ # The coarse categories a reason rolls up to. These string values MUST be members
63
+ # of `picker_oracle.NoPickCause` (the oracle maps a reason onto its verification
64
+ # branch by this string) — the lockstep the refusal-plane test pins. Kept as bare
65
+ # strings here, not an import of `NoPickCause`, so this module stays a leaf with
66
+ # zero `dos`-internal deps (the same circular-import dodge `wedge_reason` used).
67
+ KNOWN_CATEGORIES: frozenset[str] = frozenset({
68
+ "TRUE_DRAIN",
69
+ "OPERATOR_GATE",
70
+ "STALE_CLAIM",
71
+ "MISROUTE",
72
+ "UNCLASSIFIED",
73
+ })
74
+
75
+ # The category an unknown / undeclared token classifies as. A token observed in
76
+ # the wild that is NOT in the active registry surfaces as this — the drift signal
77
+ # the `--check` rail turns into a CI failure (it is a bug to add, not tolerate).
78
+ UNCLASSIFIED = "UNCLASSIFIED"
79
+
80
+
81
+ @dataclass(frozen=True)
82
+ class ReasonSpec:
83
+ """One block reason, as data. The unit a workspace declares to add a reason.
84
+
85
+ Fields:
86
+ token — the `reason_class` string a no-pick / blocked verdict carries
87
+ (canonical UPPER_SNAKE; the registry normalizes case on lookup).
88
+ category — the coarse `picker_oracle.NoPickCause` value this rolls up to
89
+ (must be in `KNOWN_CATEGORIES`; the registry validates).
90
+ refusal — True ⇒ a verdict carrying this token must NOT be rendered
91
+ (route to /replan); False ⇒ advisory-only (deferred-but-valid).
92
+ Defaults True: a no-pick reason is a refusal unless declared
93
+ otherwise, matching today's "all reasons refuse" behavior.
94
+ fix — one-line operator-facing remedy sketch (the man-page TYPICAL FIX
95
+ line). Curated text, co-located with the token by design.
96
+ see_also — man-page SEE ALSO pointers (other reasons / lanes / oracles).
97
+ summary — one-line gloss of what the reason MEANS (the man-page NAME line
98
+ continuation). Optional; falls back to the token.
99
+ """
100
+
101
+ token: str
102
+ category: str
103
+ refusal: bool = True
104
+ fix: str = ""
105
+ see_also: tuple[str, ...] = ()
106
+ summary: str = ""
107
+
108
+ def __post_init__(self) -> None:
109
+ if not self.token or not self.token.strip():
110
+ raise ValueError("ReasonSpec.token must be a non-empty string")
111
+ if self.category not in KNOWN_CATEGORIES:
112
+ raise ValueError(
113
+ f"ReasonSpec {self.token!r} has category {self.category!r}, "
114
+ f"which is not a known NoPickCause value {sorted(KNOWN_CATEGORIES)}. "
115
+ f"A reason must roll up to a category the oracle can verify against."
116
+ )
117
+
118
+ @property
119
+ def key(self) -> str:
120
+ """The normalized lookup key (UPPER, stripped) — what `coerce` matches."""
121
+ return self.token.strip().upper()
122
+
123
+
124
+ @dataclass(frozen=True)
125
+ class ReasonRegistry:
126
+ """A closed, ordered set of `ReasonSpec`s — the active refusal vocabulary.
127
+
128
+ Immutable: `extend()` returns a NEW registry. A process's active registry is
129
+ therefore a value (installed on the `SubstrateConfig`), never a mutable global
130
+ a plugin scribbles on — which is what keeps "closed set" a real property and
131
+ not a hope. Lookup is case-insensitive and whitespace-tolerant (a hand-authored
132
+ envelope written during a prose→data transition still classifies).
133
+ """
134
+
135
+ specs: tuple[ReasonSpec, ...] = ()
136
+
137
+ def __post_init__(self) -> None:
138
+ seen: set[str] = set()
139
+ for s in self.specs:
140
+ if s.key in seen:
141
+ raise ValueError(
142
+ f"duplicate reason token {s.token!r} in registry — a reason "
143
+ f"is declared exactly once (later declarations would shadow "
144
+ f"silently, the drift this registry exists to forbid)"
145
+ )
146
+ seen.add(s.key)
147
+
148
+ # -- lookup ------------------------------------------------------------
149
+ def get(self, token: str | None) -> ReasonSpec | None:
150
+ """The `ReasonSpec` for `token`, or None if not a member of this set."""
151
+ if not token:
152
+ return None
153
+ k = token.strip().upper()
154
+ for s in self.specs:
155
+ if s.key == k:
156
+ return s
157
+ return None
158
+
159
+ def is_known(self, token: str | None) -> bool:
160
+ return self.get(token) is not None
161
+
162
+ def tokens(self) -> tuple[str, ...]:
163
+ """Every declared token, in declaration order."""
164
+ return tuple(s.key for s in self.specs)
165
+
166
+ # -- projections (what the consumers read) -----------------------------
167
+ def category_for(self, token: str | None) -> str:
168
+ """Map a token onto its category value; UNCLASSIFIED for an unknown one.
169
+
170
+ Forward-compatible by construction: a brand-new label does not crash a
171
+ consumer, it classifies as drift until declared (the `--check` rail is
172
+ what turns that drift into a loud CI failure rather than a silent one).
173
+ """
174
+ spec = self.get(token)
175
+ return spec.category if spec is not None else UNCLASSIFIED
176
+
177
+ def is_refusal(self, token: str | None) -> bool:
178
+ """True iff a verdict carrying `token` must NOT be rendered.
179
+
180
+ An unknown token is refused conservatively — a no-pick envelope with an
181
+ unrecognised reason_class is still a no-pick, and launching against it is
182
+ the exact hazard. A *known* token honors its declared `refusal` flag, so a
183
+ workspace can declare an advisory-only reason (refusal=False).
184
+ """
185
+ spec = self.get(token)
186
+ if spec is None:
187
+ return True
188
+ return spec.refusal
189
+
190
+ def category_map(self) -> dict[str, str]:
191
+ """`{token: category}` for every declared reason. The dict `picker_oracle`
192
+ derives its `REASON_CLASS_MAP` from, so a declared reason is verifiable the
193
+ moment it is emittable (no second map to keep in sync)."""
194
+ return {s.key: s.category for s in self.specs}
195
+
196
+ def refusal_tokens(self) -> frozenset[str]:
197
+ """The subset of tokens whose verdicts must route to /replan."""
198
+ return frozenset(s.key for s in self.specs if s.refusal)
199
+
200
+ # -- composition (the hackability verb) --------------------------------
201
+ def extend(self, more: Iterable[ReasonSpec]) -> "ReasonRegistry":
202
+ """Return a NEW registry with `more` appended. The one way to add reasons.
203
+
204
+ Raises if any new token collides with an existing one (the same
205
+ declared-exactly-once guard `__post_init__` enforces) — a workspace
206
+ re-declaring a built-in is a mistake to surface, not to silently honor.
207
+ To *change* a built-in (e.g. flip its refusal flag), build a fresh
208
+ registry from the specs you want rather than extend-over-shadow.
209
+ """
210
+ return ReasonRegistry(specs=tuple(self.specs) + tuple(more))
211
+
212
+
213
+ # ---------------------------------------------------------------------------
214
+ # The built-in registry — the seven reasons the job spine shipped as a closed
215
+ # enum (`dos.wedge_reason.WedgeReason`), reproduced verbatim (token, category,
216
+ # refusal-ness) so `wedge_reason` stays byte-compatible and the lockstep test
217
+ # passes unchanged. The `summary`/`fix`/`see_also` text is lifted from the enum's
218
+ # own comment blocks — the man-page content the DOM plan notes "already exists as
219
+ # structured symbols," now co-located as fields. A foreign workspace ignores this
220
+ # and builds its own (or `extend`s it).
221
+ #
222
+ # Categories mirror `picker_oracle.NoPickCause` values exactly:
223
+ # LANE_DRAINED -> TRUE_DRAIN
224
+ # LANE_BLOCKED_ON_SOAK_GATED_PHASES -> OPERATOR_GATE
225
+ # LANE_LEASE_HELD_BY_LIVE_DISPATCH_LOOP -> OPERATOR_GATE (LEASE_HELD alias)
226
+ # LANE_ALL_INFLIGHT_OR_DEFERRED -> STALE_CLAIM (INFLIGHT alias)
227
+ # LANE_ALL_SHIPPED_INFLIGHT_OR_STALE_STAMP -> STALE_CLAIM (INFLIGHT alias)
228
+ # LANE_ALL_BLOCKED_OR_STALE_STAMP -> OPERATOR_GATE
229
+ # LANE_BLOCKED_ON_OPERATOR_DECISION -> OPERATOR_GATE
230
+ # ---------------------------------------------------------------------------
231
+ BASE_REASONS = ReasonRegistry(specs=(
232
+ ReasonSpec(
233
+ token="LANE_DRAINED",
234
+ category="TRUE_DRAIN",
235
+ refusal=True,
236
+ summary="0 plans + 0 findings — the lane is genuinely drained.",
237
+ fix="Nothing to do — the backlog is empty. /replan to refill if you expect work.",
238
+ see_also=("oracle picker_oracle",),
239
+ ),
240
+ ReasonSpec(
241
+ token="LANE_BLOCKED_ON_SOAK_GATED_PHASES",
242
+ category="OPERATOR_GATE",
243
+ refusal=True,
244
+ summary="Lane has pickable phases but all remaining gate on an open soak window.",
245
+ fix="Wait for the soak window to close, or /replan to re-shape the lane.",
246
+ see_also=("meta gates_on_soak", "meta soak_until", "oracle picker_oracle"),
247
+ ),
248
+ ReasonSpec(
249
+ token="LANE_LEASE_HELD_BY_LIVE_DISPATCH_LOOP",
250
+ category="OPERATOR_GATE",
251
+ refusal=True,
252
+ summary="A foreign, live /dispatch-loop holds this cluster's lane lease.",
253
+ fix="Wait for the holding loop to release the lease (racing it is the "
254
+ "collision the arbiter prevents). The reason string carries the holder.",
255
+ see_also=("lane <holder>", "oracle picker_oracle"),
256
+ ),
257
+ ReasonSpec(
258
+ token="LANE_ALL_INFLIGHT_OR_DEFERRED",
259
+ category="STALE_CLAIM",
260
+ refusal=True,
261
+ summary="Remaining phases are all soft-claimed in-flight by a sibling, "
262
+ "and/or deferred by the plan body's own gate.",
263
+ fix="Wait for the sibling packet to ship/release, or /replan to re-rank.",
264
+ see_also=("oracle picker_oracle",),
265
+ ),
266
+ ReasonSpec(
267
+ token="LANE_ALL_SHIPPED_INFLIGHT_OR_STALE_STAMP",
268
+ category="STALE_CLAIM",
269
+ refusal=True,
270
+ summary="Remaining phases are a mix of shipped-but-unstamped + in-flight "
271
+ "+ stale-stamped (the apply/tailor 'already done or drifting' shape).",
272
+ fix="/replan to reconcile the stale SHIPPED stamps, then re-dispatch.",
273
+ see_also=("oracle ship_oracle",),
274
+ ),
275
+ ReasonSpec(
276
+ token="LANE_ALL_BLOCKED_OR_STALE_STAMP",
277
+ category="OPERATOR_GATE",
278
+ refusal=True,
279
+ summary="Every remaining phase is blocked, or its stamp is drifted "
280
+ "(soak + stamp-drift co-occur).",
281
+ fix="/replan to reconcile stamps and surface the blocks.",
282
+ see_also=("oracle ship_oracle",),
283
+ ),
284
+ ReasonSpec(
285
+ token="LANE_BLOCKED_ON_OPERATOR_DECISION",
286
+ category="OPERATOR_GATE",
287
+ refusal=True,
288
+ summary="Lane is blocked on an unanswered operator decision; the routing "
289
+ "finding is already soft-claimed by a sibling. No automation clears it.",
290
+ fix="Answer the open decision (it surfaces once), then /replan.",
291
+ see_also=("oracle picker_oracle",),
292
+ ),
293
+ # ADM Phase 2 — the typed refuse the SELF_MODIFY admission predicate emits
294
+ # (`dos.self_modify.SelfModifyPredicate`). A lease whose tree includes the
295
+ # orchestrator's own running code (`arbiter.py`, the classifiers, the reason
296
+ # vocabulary, the config seam) is a misrouted lease — work aimed at the kernel
297
+ # adjudicating it rather than at userland — so it rolls up to MISROUTE. Refusal
298
+ # (route the operator to /replan or to --force if the kernel edit is deliberate).
299
+ # Declared here so the arbiter-emitted reason is simultaneously emittable,
300
+ # verifiable (`category_for`), refusable (`is_refusal`), and `dos man wedge
301
+ # SELF_MODIFY`-documented — the Axis-1 completeness rail the predicate rides.
302
+ ReasonSpec(
303
+ token="SELF_MODIFY",
304
+ category="MISROUTE",
305
+ refusal=True,
306
+ summary="Lease tree includes the orchestrator's own running code — a live "
307
+ "loop must not rewrite the kernel that is adjudicating it.",
308
+ fix="Edit kernel runtime files OUTSIDE a live dispatch loop, or pass "
309
+ "--force to override (the operator's explicit 'I am deliberately "
310
+ "editing the kernel between loop runs').",
311
+ # The see_also points at the EXCLUSIVE-lane concept (the kernel's own
312
+ # region runs alone) via the `dos man lane` verb rather than a specific
313
+ # lane NAME — a generic registry must not name a lane a foreign workspace
314
+ # may not declare (the `dos man lane` ref resolves on every workspace; a
315
+ # bare `lane orchestration` dangled on any taxonomy without that lane,
316
+ # which `config_lint.REASON_SEE_ALSO_DANGLES` correctly flagged).
317
+ see_also=("dos man lane", "dos arbitrate"),
318
+ ),
319
+ # docs/115 primitive 4 — the typed refuse that surfaces `durable_schema`'s
320
+ # refuse-don't-guess floor through the closed refusal vocabulary. When a reader
321
+ # meets a durable record (intent ledger, WAL, env-print) tagged at a NON-additive
322
+ # version this kernel predates (`durable_schema.classify` → UNREADABLE_NEWER), the
323
+ # sound answer is to REFUSE the record, never best-effort-parse a shape it does
324
+ # not know. A record this kernel cannot soundly read is work it must route
325
+ # elsewhere (a newer kernel / a migration), not guess at — so it rolls up to
326
+ # MISROUTE, the SELF_MODIFY sibling. Declared here so the floor is simultaneously
327
+ # emittable, verifiable (`category_for`), refusable (`is_refusal`), and
328
+ # `dos man wedge SCHEMA_UNREADABLE`-documented. The carried verdict
329
+ # (`ReadabilityVerdict`: family + understood-ceiling + record-version) is the
330
+ # remedy-with-the-refusal — the MCP `UnsupportedProtocolVersionError(-32004)`
331
+ # `{supported, requested}` shape, which DOS's durable_schema predates.
332
+ ReasonSpec(
333
+ token="SCHEMA_UNREADABLE",
334
+ category="MISROUTE",
335
+ refusal=True,
336
+ summary="A durable record is tagged at a schema version this kernel predates "
337
+ "— refuse-don't-guess (never best-effort-parse an unknown shape).",
338
+ fix="Upgrade the kernel to one that understands this record's schema "
339
+ "version, or run a migration. The refusal carries the family + the "
340
+ "version this kernel reads + the record's version (the supported set).",
341
+ see_also=("durable_schema", "resume", "dos man wedge SELF_MODIFY"),
342
+ ),
343
+ # docs/104 §4 (control-flow arm) — the typed refuse the arbiter emits when an
344
+ # EXPLICIT keyword request names a lane the workspace's taxonomy never heard of
345
+ # and it resolves to no tree. Auto-pick's license is "the caller expressed NO
346
+ # preference"; a named keyword is a preference the kernel cannot place, so
347
+ # silently substituting a different free lane (the old degrade-to-bare) is the
348
+ # refuse-don't-guess violation turned inward — the lease would describe the
349
+ # wrong region and disjointness would guard the wrong tree. Work aimed at a lane
350
+ # that does not exist here is misrouted (vs SELF_MODIFY = misrouted to the
351
+ # kernel, SCHEMA_UNREADABLE = misrouted to a newer kernel) → MISROUTE. Declared
352
+ # here so the arbiter-emitted reason is simultaneously emittable, verifiable
353
+ # (`category_for`), refusable (`is_refusal`), and `dos man wedge UNKNOWN_LANE`-
354
+ # documented — the same completeness rail SELF_MODIFY/SCHEMA_UNREADABLE ride.
355
+ ReasonSpec(
356
+ token="UNKNOWN_LANE",
357
+ category="MISROUTE",
358
+ refusal=True,
359
+ summary="An explicit keyword request named a lane this workspace's taxonomy "
360
+ "does not contain — the kernel refuses to guess a substitute "
361
+ "(auto-pick only chooses when the caller expresses no preference).",
362
+ fix="Pass a lane the workspace knows as --scope (see the refusal's "
363
+ "known-lane list or `dos man lane`), run a bare invocation to auto-pick "
364
+ "any free lane, or register the lane in dos.toml.",
365
+ see_also=("lane", "dos arbitrate", "dos man wedge SELF_MODIFY"),
366
+ ),
367
+ ))
368
+
369
+
370
+ # ---------------------------------------------------------------------------
371
+ # The declarative on-ramp: read reasons out of a workspace's `dos.toml`.
372
+ #
373
+ # `dos init` scaffolds a `dos.toml`; this is the function that turns its
374
+ # `[reasons.*]` table into a `ReasonRegistry` extending `BASE_REASONS`. It is the
375
+ # chosen "no-code" path (operator decision): a host adds a block reason by editing
376
+ # data, not by importing the package. The TOML shape mirrors the dataclass:
377
+ #
378
+ # [reasons.LANE_PARKED_FOR_BUDGET]
379
+ # category = "OPERATOR_GATE" # required; must be a KNOWN_CATEGORIES value
380
+ # refusal = true # optional, default true
381
+ # summary = "lane parked: monthly token budget hit"
382
+ # fix = "raise the budget cap or /replan"
383
+ # see_also = ["meta budget", "oracle picker_oracle"]
384
+ #
385
+ # Behavioral hooks (custom renderers / admission predicates) are NOT declarable in
386
+ # TOML — those load via Python packaging entry_points (see docs/HACKING.md). TOML
387
+ # is for the data axes (reasons, and later lanes/paths); entry_points for code.
388
+ # ---------------------------------------------------------------------------
389
+
390
+
391
+ def specs_from_table(table: dict) -> list[ReasonSpec]:
392
+ """Turn a parsed `[reasons]` TOML table into a list of `ReasonSpec`.
393
+
394
+ `table` is `{token: {category, refusal?, summary?, fix?, see_also?}}` — the
395
+ shape `tomllib.load(...)["reasons"]` yields. Pure (no I/O); raises
396
+ `ValueError` (via `ReasonSpec.__post_init__`) on a bad category or empty
397
+ token, so a malformed declaration fails loudly at load instead of silently
398
+ classifying as drift later.
399
+ """
400
+ specs: list[ReasonSpec] = []
401
+ for token, body in (table or {}).items():
402
+ if not isinstance(body, dict):
403
+ raise ValueError(
404
+ f"[reasons.{token}] must be a table, got {type(body).__name__}"
405
+ )
406
+ if "category" not in body:
407
+ raise ValueError(f"[reasons.{token}] is missing required `category`")
408
+ see = body.get("see_also") or ()
409
+ if isinstance(see, str):
410
+ see = (see,)
411
+ specs.append(ReasonSpec(
412
+ token=str(token),
413
+ category=str(body["category"]),
414
+ refusal=bool(body.get("refusal", True)),
415
+ fix=str(body.get("fix", "")),
416
+ see_also=tuple(str(s) for s in see),
417
+ summary=str(body.get("summary", "")),
418
+ ))
419
+ return specs
420
+
421
+
422
+ def load_from_toml(path: Path | str, *, base: ReasonRegistry = BASE_REASONS) -> ReasonRegistry:
423
+ """Build a `ReasonRegistry` from a `dos.toml`'s `[reasons]` table.
424
+
425
+ Returns `base` unchanged when the file is absent, has no `[reasons]` table, or
426
+ `tomllib` is unavailable (Python < 3.11 with no `tomli`) — the declarative
427
+ path is purely additive, so a missing/empty config degrades to the built-in
428
+ set, never an error. A *present but malformed* `[reasons]` table raises
429
+ (`specs_from_table`), because a host that declared a reason wrong wants that
430
+ surfaced, not swallowed.
431
+ """
432
+ p = Path(path)
433
+ if not p.exists():
434
+ return base
435
+ try:
436
+ import tomllib # py3.11+
437
+ except ModuleNotFoundError: # pragma: no cover - py<3.11 fallback
438
+ try:
439
+ import tomli as tomllib # type: ignore
440
+ except ModuleNotFoundError:
441
+ return base
442
+ # `utf-8-sig` transparently strips a UTF-8 BOM (PowerShell's default `utf8`
443
+ # encoding writes one; raw `tomllib.load(rb)` chokes on it and would silently
444
+ # drop a valid declared table — see the same fix in `config._load_toml_table`).
445
+ data = tomllib.loads(p.read_text(encoding="utf-8-sig"))
446
+ table = data.get("reasons")
447
+ if not isinstance(table, dict) or not table:
448
+ return base
449
+ return base.extend(specs_from_table(table))
dos/reconcile.py ADDED
@@ -0,0 +1,173 @@
1
+ """`reconcile` — the quiet-completion gate (docs/168 Concept 3, docs/207 Phase 4).
2
+
3
+ The picker-boundary closure of the quiet-failure line (docs/149–164). Those docs
4
+ DETECT quiet failure *in a trajectory* (a run that narrated success but the world
5
+ did not move); `reconcile` is what KEEPS a quietly-incomplete unit in the residual
6
+ *across runs* — so the next cycle re-offers it, flagged, instead of believing the
7
+ "✅ done" and dropping it. The `job` repo's FQ-336 quiet-DRAIN storm is the
8
+ motivating bug: a mere *touch* of a plan doc counted as a ship → a false "all
9
+ shipped" → `child_skipped_replan` ×8 re-confirms. A `QUIET_INCOMPLETE` keep would
10
+ have caught it: the touch is the agent's self-report, the oracle is ground truth.
11
+
12
+ It is a JOIN over two verdicts the kernel ALREADY produces — the agent's CLAIM and
13
+ the `oracle` verdict (the same `oracle.is_shipped` `verify` answers from git
14
+ ancestry, never self-report) — NOT a new sensor. The rule is the intent-ledger
15
+ rule (docs/107: a `STEP_CLAIMED` stays, a `STEP_VERIFIED` is what removes work)
16
+ generalized to the picker:
17
+
18
+ > **Fail-closed on the claim.** The agent's word never REMOVES work; only ground
19
+ > truth does. claim-done ∧ oracle-NOT_SHIPPED → QUIET_INCOMPLETE, KEPT in the
20
+ > residual, flagged.
21
+
22
+ The three states (mutually exclusive):
23
+
24
+ * ``VERIFIED`` — the oracle confirms the unit shipped (ground truth). It
25
+ leaves the residual. (The claim is irrelevant here — a
26
+ verified unit is done whether or not the agent claimed it.)
27
+ * ``QUIET_INCOMPLETE`` — the agent CLAIMED done but the oracle says NOT_SHIPPED.
28
+ The dangerous case: a self-report that, believed, would
29
+ silently drop real work. KEPT in the residual, FLAGGED so
30
+ the host routes it (a verifier pass / /replan / a finding).
31
+ * ``HONEST_OPEN`` — the agent did NOT claim done and the oracle says
32
+ NOT_SHIPPED. Honest unfinished work; stays in the residual
33
+ with no flag (it is not a quiet failure, just open).
34
+
35
+ DETECT-and-KEEP, never FIX (docs/164). `reconcile` does not mutate, re-run, or
36
+ correct anything — it KEEPS the work alive and FLAGS the divergence; the host owns
37
+ the correction. The kernel stays a PDP, never a PEP.
38
+
39
+ ⚓ Pure; the oracle verdict + the claim are gathered at the caller boundary and
40
+ handed in (the `oracle.is_shipped` read, the claim parse), exactly like
41
+ `completion.classify` is handed its `AncestryFacts`. So the toolathlon scoring
42
+ gate (docs/207 Phase 4) replays on the committed corpus offline.
43
+ """
44
+
45
+ from __future__ import annotations
46
+
47
+ import enum
48
+ from dataclasses import dataclass
49
+
50
+
51
+ class Reconciliation(str, enum.Enum):
52
+ """The typed quiet-completion verdict (docs/168 §3).
53
+
54
+ `str`-valued so it round-trips a `--json` token / exit code without a lookup
55
+ table (the `Completion` / `gate_classify.Verdict` idiom). The load-bearing
56
+ asymmetry: only VERIFIED removes the unit from the residual; QUIET_INCOMPLETE
57
+ and HONEST_OPEN both KEEP it (the fail-closed-on-the-claim floor).
58
+ """
59
+
60
+ VERIFIED = "VERIFIED" # oracle confirms shipped — leaves the residual
61
+ QUIET_INCOMPLETE = "QUIET_INCOMPLETE" # claimed done BUT oracle says not — KEEP + flag
62
+ HONEST_OPEN = "HONEST_OPEN" # not claimed + not shipped — honest open work, KEEP
63
+
64
+ def __str__(self) -> str: # pragma: no cover - trivial
65
+ return self.value
66
+
67
+ @property
68
+ def keeps_in_residual(self) -> bool:
69
+ """True iff the unit STAYS in the residual (everything but VERIFIED)."""
70
+ return self is not Reconciliation.VERIFIED
71
+
72
+ @property
73
+ def is_quiet_failure(self) -> bool:
74
+ """True iff this is the dangerous case — a claim the oracle refutes."""
75
+ return self is Reconciliation.QUIET_INCOMPLETE
76
+
77
+
78
+ @dataclass(frozen=True)
79
+ class ReconciliationVerdict:
80
+ """The single verdict `reconcile` returns, with the inputs echoed back.
81
+
82
+ ``state`` is the typed `Reconciliation`. ``unit`` is the unit id. ``claimed`` /
83
+ ``oracle_shipped`` are the two inputs the join read (so a surfaced verdict is
84
+ legible). ``reason`` is the operator-facing one-liner. ``flag`` is the routing
85
+ tag a quiet-incomplete carries (``"QUIET_INCOMPLETE"`` so the host can route it),
86
+ empty otherwise.
87
+ """
88
+
89
+ state: Reconciliation
90
+ unit: str
91
+ claimed: bool
92
+ oracle_shipped: bool
93
+ reason: str
94
+ flag: str = ""
95
+
96
+ @property
97
+ def keeps_in_residual(self) -> bool:
98
+ return self.state.keeps_in_residual
99
+
100
+ @property
101
+ def is_quiet_failure(self) -> bool:
102
+ """True iff this is the dangerous case — a claim the oracle refutes."""
103
+ return self.state.is_quiet_failure
104
+
105
+ def to_dict(self) -> dict:
106
+ return {
107
+ "state": self.state.value,
108
+ "unit": self.unit,
109
+ "claimed": self.claimed,
110
+ "oracle_shipped": self.oracle_shipped,
111
+ "keeps_in_residual": self.keeps_in_residual,
112
+ "flag": self.flag,
113
+ "reason": self.reason,
114
+ }
115
+
116
+
117
+ def reconcile(
118
+ unit: str,
119
+ *,
120
+ claimed_done: bool,
121
+ oracle_shipped: bool,
122
+ ) -> ReconciliationVerdict:
123
+ """Reconcile a unit's CLAIM against the ORACLE's ground-truth verdict. PURE.
124
+
125
+ ``claimed_done`` is the agent's self-report ("I finished this unit") — gathered
126
+ at the boundary (a `claim_done` tool call, a plan-meta `shipped:` entry, a
127
+ packet disposition). ``oracle_shipped`` is `oracle.is_shipped`'s verdict from
128
+ git ancestry (the non-forgeable rung) — also gathered at the boundary.
129
+
130
+ The fail-closed-on-the-claim join (docs/168 §3):
131
+
132
+ * oracle_shipped → VERIFIED (leaves the residual; the
133
+ claim is moot — ground truth confirms it).
134
+ * claimed_done ∧ ¬oracle_shipped → QUIET_INCOMPLETE (the agent's word would
135
+ silently drop real work; KEEP + flag).
136
+ * ¬claimed_done ∧ ¬oracle_shipped → HONEST_OPEN (honest unfinished work; KEEP,
137
+ no flag — not a quiet failure).
138
+
139
+ The agent's claim NEVER removes the unit from the residual; only the oracle
140
+ does. Returns a `ReconciliationVerdict`; never raises.
141
+ """
142
+ uid = str(unit)
143
+ if oracle_shipped:
144
+ return ReconciliationVerdict(
145
+ state=Reconciliation.VERIFIED,
146
+ unit=uid,
147
+ claimed=bool(claimed_done),
148
+ oracle_shipped=True,
149
+ reason=(f"oracle confirms {uid} shipped (git ancestry) — verified, "
150
+ f"leaves the residual"),
151
+ )
152
+ if claimed_done:
153
+ return ReconciliationVerdict(
154
+ state=Reconciliation.QUIET_INCOMPLETE,
155
+ unit=uid,
156
+ claimed=True,
157
+ oracle_shipped=False,
158
+ flag="QUIET_INCOMPLETE",
159
+ reason=(
160
+ f"{uid} was CLAIMED done but the oracle says NOT_SHIPPED — a quiet "
161
+ f"failure; KEPT in the residual and flagged (the claim is a "
162
+ f"self-report; only ground truth removes work). Route to a verifier "
163
+ f"pass / /replan / a finding — do NOT believe the claim"
164
+ ),
165
+ )
166
+ return ReconciliationVerdict(
167
+ state=Reconciliation.HONEST_OPEN,
168
+ unit=uid,
169
+ claimed=False,
170
+ oracle_shipped=False,
171
+ reason=(f"{uid} is not claimed and not shipped — honest open work; stays in "
172
+ f"the residual (not a quiet failure)"),
173
+ )