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/help_summary.py ADDED
@@ -0,0 +1,519 @@
1
+ """`dos helped` — the operator-facing "what did DOS catch for me?" projection.
2
+
3
+ DOS fires a firehose of enforcement decisions every session — a SELF_MODIFY
4
+ block, a lane COLLISION refused, a tool-stream WARN re-surfaced — and each one is
5
+ durably banked as an `OP_ENFORCE` record on the lane WAL (`lane_journal`,
6
+ docs/189 §C4). But until this module **nobody ever told the operator it was
7
+ happening**: the hook emitted a `deny`/`additionalContext` to the *agent*, and the
8
+ record went to a JSONL file no human reads. So the substrate could be quietly
9
+ saving a fleet from a dozen self-overwrites a day and the person running it would
10
+ never know — the observability "ran out" one rung short of the human (the docs/204
11
+ §4 wall, applied to DOS's own value).
12
+
13
+ This module closes that last rung. It is a **read-only projection** over the
14
+ enforcement stream the WAL already carries (the `observe`/`decisions`/`trace`
15
+ contract): it reads the OP_ENFORCE records, folds them into a "DOS helped with N
16
+ things" rollup (by intervention rung, by typed reason class, by tool), and the
17
+ hook path uses its cadence helper to surface a one-line nudge in the operator's
18
+ normal flow every Nth fire. It mints no belief, takes no lease, adjudicates
19
+ *nothing new* — the verdicts it counts were minted by the sensors; this only
20
+ folds and renders them. Delete it and you lose the reader, not the data.
21
+
22
+ Design rules (inherited from `observe`/`verdict_journal` — the projection scope):
23
+
24
+ * **Pure where it can be.** `summarize()` / `should_nudge()` / `nudge_line()`
25
+ take records / an index and return data — entries in, value out, no disk — so
26
+ the suite folds them without touching a file. Only the CLI verb's single
27
+ `read_all` at the boundary touches the journal.
28
+ * **Byte-clean by construction (docs/138).** Every field this counts —
29
+ `intervention`, `reason_class`, `tool`, `withheld`, `ts` — is **env-authored**:
30
+ the kernel wrote the OP_ENFORCE record downstream of an already-decided verdict.
31
+ No agent narration enters the count; a run cannot self-report its way to a
32
+ bigger "helped" number.
33
+ * **"Helped" is the rungs that changed behavior.** By default a help is a BLOCK
34
+ (a refused/withheld call) or a WARN (a surfaced correction) — the two rungs that
35
+ actually intervened. A passive OBSERVE log is recorded but is NOT a help, so the
36
+ number stays honest and is never inflated by silent logging.
37
+
38
+ There is deliberately no writer here — this module only reads what the sensors
39
+ already banked, so it can never journal a "help" the kernel did not enforce.
40
+ """
41
+ from __future__ import annotations
42
+
43
+ import re
44
+ from dataclasses import dataclass, field
45
+
46
+ # The intervention rungs that count as a "help" — the two that changed behavior.
47
+ # A BLOCK refused/withheld a call; a WARN surfaced a correction. A passive OBSERVE
48
+ # is recorded on the WAL but is NOT a help (counting it would inflate the number
49
+ # with silent logging that intervened in nothing). DEFER is the "ask a human" rung
50
+ # — also a real intervention, so it counts. Closed set, matched case-folded.
51
+ HELP_RUNGS: tuple[str, ...] = ("BLOCK", "WARN", "DEFER")
52
+
53
+ # Plain-English, one-line meaning of each refusal class an operator sees in the
54
+ # rollup — the answer to "what does `admission`/`SELF_MODIFY`/… actually mean?"
55
+ # Keyed by the actual tokens the kernel writes to a record's `reason_class`: the
56
+ # `BASE_REASONS` refusal tokens an ENFORCE record can carry (`reasons.py`) + the
57
+ # `CLASS_BUDGET_EXHAUSTED` named arbiter refuse (`arbiter.py`) + the env-authored
58
+ # handler-name fallbacks (`admission`/`provenance`, written when a record predates
59
+ # the typed-token lift). Keys are matched case-insensitively so an older `admission`
60
+ # record and a typed `SELF_MODIFY` token both resolve. An unknown key degrades to no
61
+ # gloss — we never invent an explanation, so a token added to the vocabulary later
62
+ # without a gloss here just shows bare, exactly as today. This is reference DATA, not
63
+ # a verdict: it explains an already-counted help, it never decides whether one IS a help.
64
+ REASON_GLOSSARY: dict[str, str] = {
65
+ "SELF_MODIFY": "an agent tried to edit the kernel's own running code "
66
+ "while a loop was adjudicating it",
67
+ "UNKNOWN_LANE": "an agent requested a lane this workspace doesn't declare",
68
+ "SCHEMA_UNREADABLE": "a durable record was tagged at a schema version this "
69
+ "kernel predates — refused rather than mis-parsed",
70
+ "CLASS_BUDGET_EXHAUSTED": "the concurrency budget for this class of work was "
71
+ "already full",
72
+ # The env-authored handler-name fallbacks (written when a record predates the
73
+ # typed-token lift) — explained as the rung that proposed the block.
74
+ "admission": "the lane-admission rung refused the call (usually a SELF_MODIFY "
75
+ "edit or a held-lane collision)",
76
+ "provenance": "the provenance rung refused the call (the claimed effect could "
77
+ "not be witnessed)",
78
+ "UNCLASSIFIED": "the kernel refused the call but recorded no typed reason "
79
+ "(an older record, predating the reason-class lift)",
80
+ }
81
+
82
+
83
+ def explain_reason(reason_class: str) -> str:
84
+ """The one-line plain-English meaning of a refusal class, or "" if unknown.
85
+
86
+ Case-insensitive lookup into `REASON_GLOSSARY`. Returns "" for an unknown class
87
+ so a renderer shows the bare token rather than an invented explanation — we never
88
+ guess what a class means. Pure (a string in, a string out)."""
89
+ if not reason_class:
90
+ return ""
91
+ return REASON_GLOSSARY.get(reason_class, REASON_GLOSSARY.get(reason_class.upper(), ""))
92
+
93
+ # The in-flow nudge cadence: surface once on the FIRST help of a session (so the
94
+ # operator learns the substrate is working), then every 5th after. `1` and every
95
+ # multiple of `_NUDGE_EVERY` from there (1, 5, 10, 15, …).
96
+ _NUDGE_EVERY = 5
97
+
98
+
99
+ # ---------------------------------------------------------------------------
100
+ # The fold — OP_ENFORCE records in, a typed rollup out. Pure (no disk).
101
+ # ---------------------------------------------------------------------------
102
+
103
+
104
+ @dataclass(frozen=True)
105
+ class Example:
106
+ """One concrete help, for the `--explain` drill-down — env-authored, never narrated.
107
+
108
+ `target` is the path(s) the refusal was about (from the kernel-written `reason`);
109
+ `tool` is the tool call; `ts` is when; `reason` is the kernel's own one-line
110
+ explanation. Every field is bytes the sensor authored downstream of the verdict
111
+ (docs/138), so an example is a faithful record of what DOS caught, not a story.
112
+ """
113
+
114
+ target: str = ""
115
+ tool: str = ""
116
+ ts: str = ""
117
+ reason: str = ""
118
+
119
+ def to_dict(self) -> dict:
120
+ return {"target": self.target, "tool": self.tool, "ts": self.ts,
121
+ "reason": self.reason}
122
+
123
+
124
+ @dataclass(frozen=True)
125
+ class HelpSummary:
126
+ """The "DOS helped with N things" rollup over a set of OP_ENFORCE records.
127
+
128
+ `total` is the help count (BLOCK + WARN + DEFER records); `by_rung` /
129
+ `by_reason` / `by_tool` are the breakdowns; `withheld` is how many of the helps
130
+ were calls actually refused (the strictest, most defensible subset). `enforced`
131
+ is the count of *all* enforcement records seen (helps + passive OBSERVE), so a
132
+ renderer can be honest that some firings were observe-only. `examples` maps a
133
+ reason class to a few concrete `Example`s (for the `--explain` drill-down); it is
134
+ populated only when `with_examples=True` so the cheap rollup path stays cheap.
135
+ `since` / `latest` echo the time window the count covers (the first/last `ts`).
136
+ """
137
+
138
+ total: int = 0
139
+ enforced: int = 0
140
+ withheld: int = 0
141
+ by_rung: dict[str, int] = field(default_factory=dict)
142
+ by_reason: dict[str, int] = field(default_factory=dict)
143
+ by_tool: dict[str, int] = field(default_factory=dict)
144
+ examples: dict[str, tuple[Example, ...]] = field(default_factory=dict)
145
+ since: str = ""
146
+ latest: str = ""
147
+
148
+ @property
149
+ def blocked(self) -> int:
150
+ return self.by_rung.get("BLOCK", 0)
151
+
152
+ @property
153
+ def warned(self) -> int:
154
+ return self.by_rung.get("WARN", 0)
155
+
156
+ @property
157
+ def deferred(self) -> int:
158
+ return self.by_rung.get("DEFER", 0)
159
+
160
+ def to_dict(self) -> dict:
161
+ out = {
162
+ "total": self.total,
163
+ "enforced": self.enforced,
164
+ "withheld": self.withheld,
165
+ "blocked": self.blocked,
166
+ "warned": self.warned,
167
+ "deferred": self.deferred,
168
+ "by_rung": dict(self.by_rung),
169
+ "by_reason": dict(self.by_reason),
170
+ "by_tool": dict(self.by_tool),
171
+ "since": self.since,
172
+ "latest": self.latest,
173
+ }
174
+ if self.examples:
175
+ out["examples"] = {
176
+ cls: [e.to_dict() for e in exs]
177
+ for cls, exs in self.examples.items()
178
+ }
179
+ out["glossary"] = {
180
+ cls: explain_reason(cls) for cls in self.by_reason
181
+ if explain_reason(cls)
182
+ }
183
+ return out
184
+
185
+
186
+ def _rung_of(rec: dict) -> str:
187
+ """The intervention rung of an OP_ENFORCE record, upper-cased.
188
+
189
+ Reads the top-level `intervention` token `enforce_entry` lifts (the cheap
190
+ forensic field), degrading an absent/blank token to "" — never guessed.
191
+ """
192
+ val = rec.get("intervention") or ""
193
+ return str(val).strip().upper()
194
+
195
+
196
+ def is_help(rec: dict, *, help_rungs: tuple[str, ...] = HELP_RUNGS) -> bool:
197
+ """True iff this OP_ENFORCE record is a behavior-changing help (BLOCK/WARN/DEFER).
198
+
199
+ The single predicate the whole module turns on — keep the "what counts" rule in
200
+ exactly one place. A record whose `op` is not ENFORCE, or whose rung is OBSERVE
201
+ / blank / unknown, is not a help.
202
+ """
203
+ if rec.get("op") != "ENFORCE":
204
+ return False
205
+ return _rung_of(rec) in help_rungs
206
+
207
+
208
+ # The env-authored target path(s) live in the parenthesized list inside the
209
+ # kernel-written `reason` text: `… running code (src/dos/arbiter.py, …) — refusing …`.
210
+ # We extract that list (and only that — a path-shaped, slash-or-backslash token), so
211
+ # the operator sees WHICH file was blocked. This reads the kernel's OWN sentence, not
212
+ # agent narration — the `reason` was authored by the sensor downstream of the verdict
213
+ # (docs/138), so the example stays byte-clean: a run cannot inject a path here.
214
+ _PAREN_PATHS = re.compile(r"\(([^)]*)\)")
215
+ _PATH_TOKEN = re.compile(r"[\w./\\-]+\.[\w]+")
216
+
217
+
218
+ def _target_of(rec: dict) -> str:
219
+ """The concrete path(s) a record's refusal was about, or "" — env-authored.
220
+
221
+ Pulls the parenthesized path list out of the kernel-written `reason` (e.g.
222
+ `(src/dos/arbiter.py)`), keeping only path-shaped tokens, joined back with ", ".
223
+ Falls back to the record's `proposal.reason` then the `lane`. Reads only
224
+ kernel-authored bytes; never the agent's. Pure (a record in, a string out)."""
225
+ text = str(rec.get("reason") or "")
226
+ if not text:
227
+ body = rec.get("proposal")
228
+ if isinstance(body, dict):
229
+ text = str(body.get("reason") or "")
230
+ for chunk in _PAREN_PATHS.findall(text):
231
+ paths = _PATH_TOKEN.findall(chunk)
232
+ if paths:
233
+ return ", ".join(paths)
234
+ return ""
235
+
236
+
237
+ def _recover_reason_class(rec: dict) -> str:
238
+ """The TYPED refusal class for a record, recovered as far as the env allows.
239
+
240
+ Prefers the top-level `reason_class`, then the SAME token nested in the
241
+ env-authored `proposal` body (present on older records whose top-level token was
242
+ never lifted — the 092ad29 gap), then the env-authored `handler` name, then
243
+ "UNCLASSIFIED". Every source is kernel-written; the human-readable `reason` prose
244
+ is never mined. This is the fix for the misleading "admission 597 / SELF_MODIFY 13"
245
+ split — both are SELF_MODIFY, but the older 597 lost their top-level token, so we
246
+ recover it from the proposal body and the two buckets collapse into the honest one."""
247
+ cls = str(rec.get("reason_class") or "").strip()
248
+ if cls:
249
+ return cls
250
+ body = rec.get("proposal")
251
+ if isinstance(body, dict):
252
+ cls = str(body.get("reason_class") or "").strip()
253
+ if cls:
254
+ return cls
255
+ return str(rec.get("handler") or "").strip() or "UNCLASSIFIED"
256
+
257
+
258
+ # How many distinct examples to bank per reason class for the `--explain` view.
259
+ # A small cap: the drill-down shows the SHAPE of what was caught, not the firehose.
260
+ _EXAMPLES_PER_REASON = 3
261
+
262
+
263
+ def summarize(
264
+ records,
265
+ *,
266
+ holder: str = "",
267
+ since: str = "",
268
+ help_rungs: tuple[str, ...] = HELP_RUNGS,
269
+ with_examples: bool = False,
270
+ ) -> HelpSummary:
271
+ """Fold OP_ENFORCE records into a "DOS helped with N things" rollup. PURE.
272
+
273
+ `records` is any iterable of journal entries (the `lane_journal.read_all`
274
+ output, or a hand-built list in a test). `holder` filters to one session/owner
275
+ (the OP_ENFORCE `holder` is the session id — see `_journal_pretool_outcome`),
276
+ so the in-flow nudge and the stop digest can count *this* session's helps.
277
+ `since` keeps only records with `ts >= since` (ISO-8601 sorts lexically, so a
278
+ string compare is the window) — for `dos helped --since`. `help_rungs` is the
279
+ "what counts" set (defaults to BLOCK/WARN/DEFER). `with_examples` additionally
280
+ banks a few concrete `Example`s per reason class (for `dos helped --explain`) —
281
+ off by default so the cheap rollup path stays cheap.
282
+
283
+ Entries in, counts out, no disk — the unit-test surface, mirroring
284
+ `verdict_journal.rollup`.
285
+ """
286
+ total = 0
287
+ enforced = 0
288
+ withheld = 0
289
+ by_rung: dict[str, int] = {}
290
+ by_reason: dict[str, int] = {}
291
+ by_tool: dict[str, int] = {}
292
+ examples: dict[str, list[Example]] = {}
293
+ seen_targets: dict[str, set[str]] = {}
294
+ first_ts = ""
295
+ last_ts = ""
296
+
297
+ for rec in records:
298
+ if rec.get("op") != "ENFORCE":
299
+ continue
300
+ if holder and str(rec.get("holder") or "") != holder:
301
+ continue
302
+ ts = str(rec.get("ts") or "")
303
+ if since and ts and ts < since:
304
+ continue
305
+ enforced += 1
306
+ if ts:
307
+ if not first_ts or ts < first_ts:
308
+ first_ts = ts
309
+ if ts > last_ts:
310
+ last_ts = ts
311
+ rung = _rung_of(rec)
312
+ if rung not in help_rungs:
313
+ continue # recorded (counted in `enforced`) but not a help
314
+ total += 1
315
+ by_rung[rung] = by_rung.get(rung, 0) + 1
316
+ if rec.get("withheld") is True:
317
+ withheld += 1
318
+ # The TYPED reason class, recovered as far as the env allows (top-level →
319
+ # the same token nested in the proposal body → the env-authored handler name
320
+ # → UNCLASSIFIED). All kernel-written — the `reason` prose is never mined.
321
+ # Recovering the nested token is what collapses the misleading
322
+ # "admission 597 / SELF_MODIFY 13" split into the honest single bucket.
323
+ reason_class = _recover_reason_class(rec)
324
+ by_reason[reason_class] = by_reason.get(reason_class, 0) + 1
325
+ tool = str(rec.get("tool") or "").strip() or "-"
326
+ by_tool[tool] = by_tool.get(tool, 0) + 1
327
+
328
+ if with_examples:
329
+ bank = examples.setdefault(reason_class, [])
330
+ if len(bank) < _EXAMPLES_PER_REASON:
331
+ target = _target_of(rec)
332
+ # Prefer DISTINCT targets so the few examples shown are different
333
+ # files, not the same path three times.
334
+ seen = seen_targets.setdefault(reason_class, set())
335
+ if not target or target not in seen:
336
+ if target:
337
+ seen.add(target)
338
+ reason_text = str(rec.get("reason") or "").strip()
339
+ body = rec.get("proposal")
340
+ if not reason_text and isinstance(body, dict):
341
+ reason_text = str(body.get("reason") or "").strip()
342
+ bank.append(Example(
343
+ target=target, tool=tool, ts=ts,
344
+ reason=_first_sentence(reason_text)))
345
+
346
+ return HelpSummary(
347
+ total=total,
348
+ enforced=enforced,
349
+ withheld=withheld,
350
+ by_rung=dict(sorted(by_rung.items(), key=lambda kv: (-kv[1], kv[0]))),
351
+ by_reason=dict(sorted(by_reason.items(), key=lambda kv: (-kv[1], kv[0]))),
352
+ by_tool=dict(sorted(by_tool.items(), key=lambda kv: (-kv[1], kv[0]))),
353
+ examples={cls: tuple(exs) for cls, exs in examples.items()},
354
+ since=first_ts,
355
+ latest=last_ts,
356
+ )
357
+
358
+
359
+ def _first_sentence(text: str, *, limit: int = 160) -> str:
360
+ """The first sentence of a kernel `reason`, trimmed for one-line display.
361
+
362
+ The full `reason` carries the explanation plus a "Pass --force …" trailer; the
363
+ operator-facing example wants just the first clause. Splits on the em-dash the
364
+ SELF_MODIFY/collision sentences use, else the first period, else hard-truncates.
365
+ Pure (a string in, a string out); reads kernel-authored bytes only."""
366
+ if not text:
367
+ return ""
368
+ for sep in ("—", " - ", ". "):
369
+ head = text.split(sep, 1)[0].strip()
370
+ if head and head != text:
371
+ return head[:limit].rstrip()
372
+ return text[:limit].rstrip()
373
+
374
+
375
+ # ---------------------------------------------------------------------------
376
+ # The cadence — when does the in-flow nudge fire? PURE (an index in, a bool out).
377
+ # ---------------------------------------------------------------------------
378
+
379
+
380
+ def should_nudge(help_index: int, *, every: int = _NUDGE_EVERY) -> bool:
381
+ """True iff the operator nudge should fire on this help (1-based count).
382
+
383
+ "First + every 5th": fire on the 1st help of the session (so the operator sees
384
+ the substrate is alive) and every `every`th after — indices 1, 5, 10, 15, ….
385
+ `help_index` is the running BLOCK/WARN/DEFER count *including* this firing
386
+ (1-based). A non-positive index never nudges.
387
+ """
388
+ if help_index <= 0:
389
+ return False
390
+ if help_index == 1:
391
+ return True
392
+ return help_index % every == 0
393
+
394
+
395
+ # ---------------------------------------------------------------------------
396
+ # Rendering — the one-line in-flow nudge + the full operator rollup.
397
+ # ---------------------------------------------------------------------------
398
+
399
+
400
+ def nudge_line(summary: HelpSummary) -> str:
401
+ """The one-line in-flow nudge appended to the hook's additionalContext.
402
+
403
+ Operator-facing, single sentence, no narration: "DOS has caught N things this
404
+ session (X blocked, Y warned)." Surfaced on the 1st + every 5th help so the
405
+ operator learns, in their normal flow, that the substrate is working — without
406
+ a separate command and without nagging.
407
+ """
408
+ parts: list[str] = []
409
+ if summary.blocked:
410
+ parts.append(f"{summary.blocked} blocked")
411
+ if summary.warned:
412
+ parts.append(f"{summary.warned} warned")
413
+ if summary.deferred:
414
+ parts.append(f"{summary.deferred} deferred")
415
+ detail = f" ({', '.join(parts)})" if parts else ""
416
+ noun = "thing" if summary.total == 1 else "things"
417
+ return (
418
+ f"DOS has caught {summary.total} {noun} this session{detail}. "
419
+ f"Run `dos helped` for the breakdown."
420
+ )
421
+
422
+
423
+ def render_summary_text(summary: HelpSummary, *, scope: str = "") -> str:
424
+ """The full `dos helped` operator rollup — headline + breakdowns. Pure.
425
+
426
+ Leads with the headline count, then the by-reason-class and by-tool tables (the
427
+ "what kind of help, on which tool" an operator wants), and an honest footer
428
+ noting how many firings were observe-only (recorded but not a behavior-change).
429
+ """
430
+ out: list[str] = []
431
+ title = "# dos helped"
432
+ if scope:
433
+ title += f" · {scope}"
434
+ out.append(title)
435
+ noun = "thing" if summary.total == 1 else "things"
436
+ out.append(f" DOS has caught {summary.total} {noun}"
437
+ + (f" since {summary.since}" if summary.since else ""))
438
+ if summary.total:
439
+ rung_parts = []
440
+ if summary.blocked:
441
+ rung_parts.append(f"{summary.blocked} blocked")
442
+ if summary.warned:
443
+ rung_parts.append(f"{summary.warned} warned")
444
+ if summary.deferred:
445
+ rung_parts.append(f"{summary.deferred} deferred")
446
+ out.append(f" {', '.join(rung_parts)}"
447
+ + (f" · {summary.withheld} calls actually refused"
448
+ if summary.withheld else ""))
449
+ if not summary.total:
450
+ out.append(" (no behavior-changing interventions recorded yet — "
451
+ "DOS has been observing, not blocking)")
452
+ if summary.enforced:
453
+ out.append(f" ({summary.enforced} enforcement record(s) seen, "
454
+ f"all observe-only)")
455
+ return "\n".join(out)
456
+ if summary.by_reason:
457
+ out.append("")
458
+ out.append(" by reason")
459
+ for reason, n in summary.by_reason.items():
460
+ gloss = explain_reason(reason)
461
+ line = f" {reason:<22} {n:>4}"
462
+ if gloss:
463
+ line += f" {gloss}" # the plain-English meaning, inline
464
+ out.append(line)
465
+ if summary.by_tool:
466
+ out.append("")
467
+ out.append(" by tool")
468
+ for tool, n in summary.by_tool.items():
469
+ out.append(f" {tool:<22} {n:>4}")
470
+ observe_only = summary.enforced - summary.total
471
+ if observe_only > 0:
472
+ out.append("")
473
+ out.append(f" ({observe_only} further firing(s) were observe-only — "
474
+ f"recorded, but changed nothing)")
475
+ # Point the operator at the drill-down — the answer to "but WHICH ones?"
476
+ out.append("")
477
+ out.append(" Run `dos helped --explain` for concrete examples (which files, why).")
478
+ return "\n".join(out)
479
+
480
+
481
+ def render_explain_text(summary: HelpSummary, *, scope: str = "") -> str:
482
+ """The `dos helped --explain` drill-down — per reason class: meaning + examples.
483
+
484
+ The answer to "but WHICH ones, and what does `admission` mean?": for each reason
485
+ class, the plain-English gloss, the count, and a few concrete examples (the file
486
+ blocked, the tool, the kernel's own one-line reason). Every shown field is
487
+ env-authored (docs/138) — the gloss is reference data, the examples are bytes the
488
+ sensor wrote downstream of the verdict; no agent narration appears. Pure.
489
+ """
490
+ out: list[str] = []
491
+ title = "# dos helped --explain"
492
+ if scope:
493
+ title += f" · {scope}"
494
+ out.append(title)
495
+ noun = "thing" if summary.total == 1 else "things"
496
+ out.append(f" DOS has caught {summary.total} {noun}"
497
+ + (f" since {summary.since}" if summary.since else ""))
498
+ if not summary.total:
499
+ out.append("")
500
+ out.append(" (no behavior-changing interventions recorded yet — "
501
+ "DOS has been observing, not blocking)")
502
+ return "\n".join(out)
503
+ for reason, n in summary.by_reason.items():
504
+ out.append("")
505
+ plural = "block" if n == 1 else "blocks"
506
+ out.append(f" {reason} ({n} {plural})")
507
+ gloss = explain_reason(reason)
508
+ if gloss:
509
+ out.append(f" means: {gloss}")
510
+ exs = summary.examples.get(reason, ())
511
+ if exs:
512
+ out.append(" e.g.")
513
+ for e in exs:
514
+ where = e.target or "(target not recorded)"
515
+ tool = f" via {e.tool}" if e.tool and e.tool != "-" else ""
516
+ out.append(f" · {where}{tool}")
517
+ if e.reason:
518
+ out.append(f" {e.reason}")
519
+ return "\n".join(out)