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/noop_streak.py
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""NOS — the no-op-streak verdict: *how many turns in a row produced zero ground-truth delta?*
|
|
2
|
+
|
|
3
|
+
docs/259 §Follow-up 1 — the **generalization of the wait-marker budget** off its one
|
|
4
|
+
special case ("markers emitted") onto the general one ("no-op turns since the last
|
|
5
|
+
forward delta"). `loop_decide.wait_marker_budget` already answers a count-vs-cap
|
|
6
|
+
question for ONE flavor of no-op turn: a `claude -p` keep-alive marker — a full
|
|
7
|
+
assistant turn that replays the whole context out of cache and produces nothing but
|
|
8
|
+
"still waiting." But a `ScheduleWakeup`-poll loop that re-reads a `.output` file in a
|
|
9
|
+
tight tick, or any loop that wakes, finds no change, and goes back to sleep, is the
|
|
10
|
+
*same* pathology: a turn that paid the cache-replay cost and moved no ground truth.
|
|
11
|
+
This module is the verdict that makes those one verdict — "the run has taken N no-op
|
|
12
|
+
turns since it last made a forward delta; has it spent its budget?"
|
|
13
|
+
|
|
14
|
+
This is `wait_marker_budget`'s **generalization**, and it sits in the temporal-verdict
|
|
15
|
+
family (`liveness` / `tool_stream` / `productivity` / **`noop_streak`**) — the same
|
|
16
|
+
pure-verdict shape, re-aimed once more:
|
|
17
|
+
|
|
18
|
+
liveness.classify (ProgressEvidence, policy) -> LivenessVerdict (did state move AT ALL?)
|
|
19
|
+
tool_stream.classify_stream (ToolStream, policy) -> StreamVerdict (is the tool stream repeating?)
|
|
20
|
+
productivity.classify (WorkHistory, policy) -> ProductivityVerdict (is the work-RATE fading?)
|
|
21
|
+
noop_streak.classify (NoOpHistory, policy) -> NoOpStreakVerdict (how many no-op turns since a forward delta?)
|
|
22
|
+
^ THIS module
|
|
23
|
+
|
|
24
|
+
It is a COUNT-vs-cap verdict, not a TREND verdict — which is why it is `wait_marker_budget`'s
|
|
25
|
+
sibling and NOT a second mode of `productivity`. `productivity` reads a *vector* of
|
|
26
|
+
per-step work magnitudes and asks "is the rate falling" (`deltas[-1]`/`deltas[-2]`);
|
|
27
|
+
`noop_streak` reads a *single* monotone counter — the run-length of consecutive
|
|
28
|
+
no-op turns since the last forward delta — and asks "is that run-length past its
|
|
29
|
+
budget." Folding the streak into `productivity.WorkHistory` would overload its
|
|
30
|
+
well-defined "fading rate" semantics with a different question and collide its
|
|
31
|
+
"withhold the accusation / reject negatives" floor with this guard's opposite
|
|
32
|
+
conservative direction (below). So the streak gets its own small verdict, the way
|
|
33
|
+
`productivity` got its own rather than living inside `liveness`.
|
|
34
|
+
|
|
35
|
+
**The forward-delta reset is the load-bearing idea** (docs/259 §Follow-up 2). The
|
|
36
|
+
count is not "no-op turns ever" — it is "no-op turns *since the last forward delta*."
|
|
37
|
+
A forward delta (a commit, a real tool result, a host re-entering a fresh wait phase)
|
|
38
|
+
ZEROES the streak, the `tool_stream` ADVANCING analogue: progress earns the loop a
|
|
39
|
+
fresh budget. Without a reset the count is a strict lifetime monotone (what
|
|
40
|
+
`wait_marker_budget` is today); with one, a long-lived session that legitimately
|
|
41
|
+
makes progress and then re-enters a wait phase starts fresh instead of being refused
|
|
42
|
+
on a stale tally. The reset lives at the BOUNDARY (`marker_sensor.record_reset`
|
|
43
|
+
appends an `op:"RESET"` record; the replayed count is markers-after-the-last-reset);
|
|
44
|
+
this pure verdict just reads the resulting count.
|
|
45
|
+
|
|
46
|
+
**Byte-clean by construction.** A no-op turn is a turn the *runtime* observed to
|
|
47
|
+
produce zero ground-truth delta — it is counted by the durable accumulator
|
|
48
|
+
(`marker_sensor`), never threaded through the agent's own narration. So EXHAUSTED is
|
|
49
|
+
"the environment recorded N no-op turns since the last forward delta," never "the
|
|
50
|
+
agent says it has waited long enough" — a quantity, not a self-report (the docs/138
|
|
51
|
+
invariant `liveness`/`productivity`/`tool_stream` all keep).
|
|
52
|
+
|
|
53
|
+
**Advisory.** Like `wait_marker_budget` and `liveness.SPINNING`, EXHAUSTED REPORTS;
|
|
54
|
+
it never kills a process. A loop consults it and chooses to stop holding its turn
|
|
55
|
+
open (the marker hook is the first consumer — it blocks the Stop while the budget is
|
|
56
|
+
LIVE, allows the Stop once EXHAUSTED); nothing here enforces.
|
|
57
|
+
|
|
58
|
+
**The conservative direction is the OPPOSITE of `productivity`'s** — and that is why
|
|
59
|
+
it is a separate verdict. `productivity` withholds the DIMINISHING accusation when in
|
|
60
|
+
doubt (a missing delta is "still productive"). A *cost* guard must do the reverse:
|
|
61
|
+
when in doubt, count the no-op turn (so the guard refuses one *more* keep-alive turn,
|
|
62
|
+
never one *fewer*) — over-spending on a missed count is the failure to avoid. The
|
|
63
|
+
accumulator honors this by leaving a torn/unreadable RESET as "the reset didn't
|
|
64
|
+
happen" (the count stays HIGHER → EXHAUSTED sooner → refuse more); this verdict
|
|
65
|
+
honors it by treating `noop_turns >= max_streak` as EXHAUSTED (the `>=`, not `>`, so
|
|
66
|
+
the budget is spent the instant it is reached). Refusing one no-op turn too early
|
|
67
|
+
costs at most one missed poll; the real Bash `<task-notification>` (which fires on
|
|
68
|
+
the child's true exit regardless) is the safety net.
|
|
69
|
+
|
|
70
|
+
**No-telemetry / no-plan discipline** (the `test_verify_no_plan` sibling): NOS needs
|
|
71
|
+
*nothing* but the no-op-turn count the caller already replayed. No plan, no registry,
|
|
72
|
+
no clock — `classify()` makes no I/O at all (it is timeless, like `productivity`; it
|
|
73
|
+
reads a count, not an age). A caller with a count gets a verdict; a caller with 0 gets
|
|
74
|
+
the honest LIVE floor (a fresh wait phase has spent nothing yet).
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
from __future__ import annotations
|
|
78
|
+
|
|
79
|
+
import enum
|
|
80
|
+
from dataclasses import dataclass
|
|
81
|
+
from pathlib import Path
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class NoOpStreak(str, enum.Enum):
|
|
85
|
+
"""The typed no-op-streak verdict — two states, mutually exclusive.
|
|
86
|
+
|
|
87
|
+
`str`-valued so it round-trips through a CLI stdout token / exit-code map without
|
|
88
|
+
a lookup table (mirrors `liveness.Liveness` / `productivity.Productivity` /
|
|
89
|
+
`gate_classify.Verdict`).
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
LIVE = "LIVE" # the streak is under the cap — another no-op turn is permitted
|
|
93
|
+
EXHAUSTED = "EXHAUSTED" # the streak has reached the cap — refuse further no-op turns
|
|
94
|
+
|
|
95
|
+
def __str__(self) -> str: # pragma: no cover - trivial
|
|
96
|
+
return self.value
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass(frozen=True)
|
|
100
|
+
class NoOpStreakPolicy:
|
|
101
|
+
"""The cap that separates LIVE / EXHAUSTED — policy, not mechanism.
|
|
102
|
+
|
|
103
|
+
The same "mechanism is kernel, threshold is config" split as `productivity`'s
|
|
104
|
+
floor and `loop_decide`'s `max_iterations`. The default is GENERIC and equals
|
|
105
|
+
`wait_marker_budget`'s default (4) — so the generalized verdict refuses at the
|
|
106
|
+
same budget the shipped marker lever does (the marker case is the special case,
|
|
107
|
+
and it must not drift). A workspace declares its own in `dos.toml [noop_streak]`,
|
|
108
|
+
the closed-config-as-data pattern (`[tool_stream]` / `[productivity]` / `[stamp]`).
|
|
109
|
+
|
|
110
|
+
max_streak — the **no-op-turn budget since the last forward delta**: the most
|
|
111
|
+
consecutive zero-delta turns a loop may take before the verdict
|
|
112
|
+
refuses the next one. `wait_marker_budget`'s `max_markers`,
|
|
113
|
+
generalized off "markers" onto "no-op turns." A streak that REACHES
|
|
114
|
+
this cap (`>=`, the cost-guard direction) is EXHAUSTED.
|
|
115
|
+
|
|
116
|
+
Default: 4 — `wait_marker_budget`'s per-run cap, one below the `keepalive_poll`
|
|
117
|
+
telemetry flag (>=5), so the runtime refusal lands one turn before the post-hoc
|
|
118
|
+
alarm would fire.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
max_streak: int = 4
|
|
122
|
+
|
|
123
|
+
def __post_init__(self) -> None:
|
|
124
|
+
if self.max_streak < 0:
|
|
125
|
+
raise ValueError("max_streak must be non-negative")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
DEFAULT_POLICY = NoOpStreakPolicy()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass(frozen=True)
|
|
132
|
+
class NoOpHistory:
|
|
133
|
+
"""The no-op-turn count `classify()` reads — gathered by the CALLER at the boundary.
|
|
134
|
+
|
|
135
|
+
No clock, no I/O inside the verdict — the arbiter rule, sharpened: NOS is
|
|
136
|
+
*timeless* (it reads a count, never an age). The caller's boundary
|
|
137
|
+
(`marker_sensor.marker_count`, which replays the session's `.dos/markers/<sid>.jsonl`
|
|
138
|
+
tally into markers-since-the-last-RESET) measures the streak and freezes it here.
|
|
139
|
+
|
|
140
|
+
noop_turns — the count of no-op turns SINCE the last forward-delta reset. A
|
|
141
|
+
no-op turn is a turn the runtime observed to produce zero ground-truth
|
|
142
|
+
delta (today: one keep-alive wait-marker; the generalization also
|
|
143
|
+
admits a poll-tick that found no change). 0 is "a fresh wait phase,
|
|
144
|
+
nothing spent yet" — the LIVE floor. Negative is rejected: a streak
|
|
145
|
+
length is a non-negative count.
|
|
146
|
+
|
|
147
|
+
The single load-bearing read is `noop_turns` vs the policy's `max_streak` — the
|
|
148
|
+
same count-vs-cap `wait_marker_budget(markers_emitted, max_markers)` makes. The
|
|
149
|
+
count is carried (not just a bool) so `--output json` can echo it and the verdict
|
|
150
|
+
can hand back the incremented value to carry into the next decision.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
noop_turns: int = 0
|
|
154
|
+
|
|
155
|
+
def __post_init__(self) -> None:
|
|
156
|
+
if self.noop_turns < 0:
|
|
157
|
+
raise ValueError("noop_turns must be non-negative (a count of no-op turns)")
|
|
158
|
+
|
|
159
|
+
@classmethod
|
|
160
|
+
def of(cls, noop_turns: int) -> "NoOpHistory":
|
|
161
|
+
"""Build a history from a replayed no-op-turn count."""
|
|
162
|
+
return cls(noop_turns)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass(frozen=True)
|
|
166
|
+
class NoOpStreakVerdict:
|
|
167
|
+
"""The single verdict `classify()` returns, with the count echoed back.
|
|
168
|
+
|
|
169
|
+
`verdict` is the typed `NoOpStreak`. `allow` is the convenience bit the marker
|
|
170
|
+
hook keys on (`allow == (verdict is NoOpStreak.LIVE)`): True to permit one more
|
|
171
|
+
no-op turn, False to refuse it. `noop_turns` is the count to carry into the *next*
|
|
172
|
+
decision — incremented when allowed (this no-op turn now happened), unchanged when
|
|
173
|
+
refused (the refused turn did not happen) — byte-mirroring
|
|
174
|
+
`WaitMarkerDecision.markers_emitted`. `reason` is operator-facing. `to_dict` is the
|
|
175
|
+
json shape (the legible-distrust renderer seam: the operator sees not just
|
|
176
|
+
EXHAUSTED but the count and cap behind it).
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
verdict: NoOpStreak
|
|
180
|
+
allow: bool
|
|
181
|
+
noop_turns: int
|
|
182
|
+
reason: str
|
|
183
|
+
|
|
184
|
+
def to_dict(self) -> dict:
|
|
185
|
+
return {
|
|
186
|
+
"verdict": self.verdict.value,
|
|
187
|
+
"allow": self.allow,
|
|
188
|
+
"noop_turns": self.noop_turns,
|
|
189
|
+
"reason": self.reason,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def classify(
|
|
194
|
+
history: NoOpHistory, policy: NoOpStreakPolicy = DEFAULT_POLICY
|
|
195
|
+
) -> NoOpStreakVerdict:
|
|
196
|
+
"""Classify a run's no-op streak against its budget. PURE — no I/O.
|
|
197
|
+
|
|
198
|
+
The `wait_marker_budget` arithmetic, generalized: a count vs a cap, with the
|
|
199
|
+
cost-guard `>=` (the budget is spent the instant it is reached, not one past).
|
|
200
|
+
|
|
201
|
+
* EXHAUSTED (refuse) — `noop_turns >= max_streak`: the run has taken its whole
|
|
202
|
+
budget of no-op turns since the last forward delta. The next one would replay
|
|
203
|
+
full context out of cache for no work; refuse it (the loop ends its turn and
|
|
204
|
+
waits on the real completion signal). The count carried forward is UNCHANGED
|
|
205
|
+
(a refused turn did not happen).
|
|
206
|
+
* LIVE (allow) — `noop_turns < max_streak`: budget remains; permit one more no-op
|
|
207
|
+
turn and carry `noop_turns + 1` (this turn now happened) into the next decision.
|
|
208
|
+
|
|
209
|
+
A `max_streak == 0` policy refuses the FIRST no-op turn (`0 >= 0`) — the degenerate
|
|
210
|
+
`wait_marker_budget(0, 0)` preserves, and the honest reading of "no budget at all."
|
|
211
|
+
"""
|
|
212
|
+
if history.noop_turns >= policy.max_streak:
|
|
213
|
+
return NoOpStreakVerdict(
|
|
214
|
+
verdict=NoOpStreak.EXHAUSTED,
|
|
215
|
+
allow=False,
|
|
216
|
+
noop_turns=history.noop_turns,
|
|
217
|
+
reason=(
|
|
218
|
+
f"no-op streak budget exhausted "
|
|
219
|
+
f"({history.noop_turns}/{policy.max_streak} no-op turns since the last "
|
|
220
|
+
f"forward delta) — each further turn replays full context out of cache "
|
|
221
|
+
f"for no work; wait on the real completion signal, a forward delta resets "
|
|
222
|
+
f"the streak"
|
|
223
|
+
),
|
|
224
|
+
)
|
|
225
|
+
return NoOpStreakVerdict(
|
|
226
|
+
verdict=NoOpStreak.LIVE,
|
|
227
|
+
allow=True,
|
|
228
|
+
noop_turns=history.noop_turns + 1,
|
|
229
|
+
reason=(
|
|
230
|
+
f"no-op streak {history.noop_turns + 1}/{policy.max_streak} since the last "
|
|
231
|
+
f"forward delta — budget remains"
|
|
232
|
+
),
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# ---------------------------------------------------------------------------
|
|
237
|
+
# The declarative on-ramp — read a policy out of dos.toml (mirror tool_stream/productivity).
|
|
238
|
+
# ---------------------------------------------------------------------------
|
|
239
|
+
def policy_from_table(table: dict) -> NoOpStreakPolicy:
|
|
240
|
+
"""Turn a parsed `[noop_streak]` TOML table into a `NoOpStreakPolicy`. PURE (no I/O).
|
|
241
|
+
|
|
242
|
+
`table` is `{max_streak?}` — the shape `tomllib.load(...)["noop_streak"]` yields. A
|
|
243
|
+
missing key falls back to the generic default; a malformed value raises (via
|
|
244
|
+
`NoOpStreakPolicy.__post_init__`), so a bad declaration fails loudly at load (the
|
|
245
|
+
`tool_stream.policy_from_table` posture).
|
|
246
|
+
"""
|
|
247
|
+
if not table:
|
|
248
|
+
return DEFAULT_POLICY
|
|
249
|
+
return NoOpStreakPolicy(
|
|
250
|
+
max_streak=int(table.get("max_streak", DEFAULT_POLICY.max_streak)),
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def load_from_toml(
|
|
255
|
+
path: "Path | str", *, base: NoOpStreakPolicy = DEFAULT_POLICY
|
|
256
|
+
) -> NoOpStreakPolicy:
|
|
257
|
+
"""Build a `NoOpStreakPolicy` from a `dos.toml`'s `[noop_streak]` table.
|
|
258
|
+
|
|
259
|
+
Returns `base` unchanged when the file is absent, has no `[noop_streak]` table, or
|
|
260
|
+
`tomllib` is unavailable — the declarative path is purely additive, so a missing/empty
|
|
261
|
+
config degrades to the generic default, never an error (the `tool_stream.load_from_toml`
|
|
262
|
+
contract). A *present but malformed* table raises (`NoOpStreakPolicy.__post_init__`).
|
|
263
|
+
Reads with `utf-8-sig` to strip a PowerShell-written BOM (the
|
|
264
|
+
`reasons`/`intervention`/`tool_stream` `load_from_toml` fix).
|
|
265
|
+
"""
|
|
266
|
+
p = Path(path)
|
|
267
|
+
if not p.exists():
|
|
268
|
+
return base
|
|
269
|
+
try:
|
|
270
|
+
import tomllib # py3.11+
|
|
271
|
+
except ModuleNotFoundError: # pragma: no cover - py<3.11 fallback
|
|
272
|
+
try:
|
|
273
|
+
import tomli as tomllib # type: ignore
|
|
274
|
+
except ModuleNotFoundError:
|
|
275
|
+
return base
|
|
276
|
+
data = tomllib.loads(p.read_text(encoding="utf-8-sig"))
|
|
277
|
+
table = data.get("noop_streak")
|
|
278
|
+
if not isinstance(table, dict) or not table:
|
|
279
|
+
return base
|
|
280
|
+
return policy_from_table(table)
|