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/decisions_tui.py ADDED
@@ -0,0 +1,251 @@
1
+ """The interactive operator-decision TUI — list + drill-in, over `dos.decisions`.
2
+
3
+ The curses rendering layer for `dos decisions`. It is a thin, **read-only
4
+ router** over `decisions.collect_decisions` (the projection) and
5
+ `decisions.next_steps` (the action bar): it shows the pending decisions, lets the
6
+ operator move through them and read each one's meaning/evidence/fix (projected
7
+ from the `ReasonRegistry`), and — when an action key is pressed — **emits the
8
+ exact shell command to stdout and exits**. The TUI never mutates substrate state
9
+ itself; the operator runs the emitted command in their own shell. That keeps it
10
+ inside the kernel's observe-first / "the manual never blocks a dispatch run"
11
+ discipline (no redraw can fire a dispatch; there is no code path here that
12
+ acquires a lease or launches an agent).
13
+
14
+ **Graceful degradation.** `curses` is absent on stock Windows Python (it needs
15
+ `windows-curses`), and the package is otherwise zero-hard-dependency. So
16
+ `run_tui` imports curses lazily and, on ImportError (or a non-interactive
17
+ stdout), falls straight through to `decisions.render_list_plain` — the plain
18
+ list is the floor, the TUI is the enhancement. `dos decisions` therefore always
19
+ works; it is merely interactive where the terminal supports it.
20
+
21
+ This module is deliberately import-light at module scope (no top-level `import
22
+ curses`) so that importing `dos.decisions_tui` never fails on a curses-less box —
23
+ only *calling* `run_tui` reaches for curses, and that call is guarded.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import subprocess
29
+ import sys
30
+
31
+ from dos import config as _config
32
+ from dos import decisions as _decisions
33
+
34
+
35
+ def _copy_to_clipboard(text: str) -> bool:
36
+ """Best-effort copy `text` to the OS clipboard. Returns True on success.
37
+
38
+ Tries the platform's native clipboard tool; never raises, never adds a hard
39
+ dependency. A False return just means the operator copies by hand.
40
+ """
41
+ candidates: list[list[str]] = []
42
+ if sys.platform == "win32":
43
+ candidates.append(["clip"])
44
+ elif sys.platform == "darwin":
45
+ candidates.append(["pbcopy"])
46
+ else:
47
+ candidates.append(["xclip", "-selection", "clipboard"])
48
+ candidates.append(["xsel", "--clipboard", "--input"])
49
+ for cmd in candidates:
50
+ try:
51
+ p = subprocess.run(cmd, input=text.encode("utf-8"), timeout=5)
52
+ if p.returncode == 0:
53
+ return True
54
+ except (OSError, subprocess.SubprocessError):
55
+ continue
56
+ return False
57
+
58
+
59
+ def run_tui(config: _config.SubstrateConfig | None = None, *, resolver: str | None = "HUMAN") -> int:
60
+ """Run the interactive decisions TUI, or fall back to the plain list.
61
+
62
+ Returns a process exit code. On an action-key press the TUI exits and the
63
+ chosen shell command is printed to stdout (so the operator can run it); `q`
64
+ / Esc exits with no output. When curses is unavailable or stdout is not a
65
+ tty, prints the plain list and returns 0 — the floor that always works.
66
+ """
67
+ cfg = _config.ensure(config)
68
+
69
+ try:
70
+ import curses
71
+ except ImportError:
72
+ # The floor: no curses on this box (e.g. Windows without windows-curses).
73
+ print(_decisions.render_list_plain(
74
+ _decisions.collect_decisions(cfg, resolver=resolver)))
75
+ return 0
76
+
77
+ # `curses.wrapper` restores the terminal on exit/exception. The inner loop
78
+ # returns either None (quit, no action) or a (key, command) tuple for an
79
+ # emitted action — we print the command AFTER wrapper returns so it lands on
80
+ # the restored terminal, not inside the alternate screen.
81
+ result: dict = {}
82
+
83
+ def _inner(stdscr):
84
+ result["value"] = _main_loop(stdscr, curses, cfg, resolver)
85
+
86
+ try:
87
+ curses.wrapper(_inner)
88
+ except curses.error:
89
+ # A terminal too small / not curses-capable mid-run — degrade, don't crash.
90
+ print(_decisions.render_list_plain(
91
+ _decisions.collect_decisions(cfg, resolver=resolver)))
92
+ return 0
93
+
94
+ emitted = result.get("value")
95
+ if not emitted:
96
+ return 0
97
+ key, command = emitted
98
+ if key == "c":
99
+ ok = _copy_to_clipboard(command)
100
+ # On copy, still echo the command so a no-clipboard box isn't left empty.
101
+ suffix = " (copied to clipboard)" if ok else " (copy unavailable — shown above)"
102
+ print(command + suffix)
103
+ return 0
104
+ # Emit-and-exit: the operator runs this in their shell.
105
+ print(command)
106
+ return 0
107
+
108
+
109
+ # Action keys that, when pressed, emit a command and exit. The `m`/`r`/`f`/`j`/`c`
110
+ # set is whatever `next_steps` produced for the selected decision; we look the key
111
+ # up in that list so the TUI and the plain detail stay in lockstep (one source).
112
+ _QUIT_KEYS = frozenset({ord("q"), 27}) # q, Esc
113
+
114
+
115
+ def _init_colors(curses) -> dict:
116
+ """Set up the urgency colour pairs once; return a tier→attr map.
117
+
118
+ Best-effort: a terminal with no colour (or `start_color` failing) yields an
119
+ empty map and `_draw` falls back to bold/normal — the floor never depends on
120
+ colour. Pair indices 1–3 are NOW(red)/SOON(yellow)/LATER(dim).
121
+ """
122
+ attrs: dict = {}
123
+ # Defensive against a stripped/limited curses (or a test double): any missing
124
+ # colour method just means no colour, never a crash. The list still renders
125
+ # via the leading glyph + the selected-row reverse-video, so colour is pure
126
+ # enhancement (the same observe-first floor discipline as the plain list).
127
+ try:
128
+ if not curses.has_colors():
129
+ return attrs
130
+ curses.start_color()
131
+ curses.use_default_colors()
132
+ curses.init_pair(1, curses.COLOR_RED, -1)
133
+ curses.init_pair(2, curses.COLOR_YELLOW, -1)
134
+ curses.init_pair(3, curses.COLOR_WHITE, -1)
135
+ attrs[_decisions.Urgency.NOW] = curses.color_pair(1) | curses.A_BOLD
136
+ attrs[_decisions.Urgency.SOON] = curses.color_pair(2)
137
+ attrs[_decisions.Urgency.LATER] = curses.color_pair(3) | getattr(curses, "A_DIM", 0)
138
+ except Exception: # pragma: no cover - terminal/double-dependent
139
+ return {}
140
+ return attrs
141
+
142
+
143
+ def _main_loop(stdscr, curses, cfg, resolver):
144
+ """The curses event loop. Returns None (quit) or (key, command) (emit).
145
+
146
+ Pure-ish: it re-reads `collect_decisions` on each full redraw so ages update
147
+ and a resolved decision drops off the list, but it performs no writes.
148
+ """
149
+ curses.curs_set(0)
150
+ stdscr.keypad(True)
151
+ urgency_attrs = _init_colors(curses)
152
+
153
+ selected = 0
154
+ rows = _decisions.collect_decisions(cfg, resolver=resolver)
155
+
156
+ while True:
157
+ if rows:
158
+ selected = max(0, min(selected, len(rows) - 1))
159
+ _draw(stdscr, curses, rows, selected, resolver, cfg, urgency_attrs)
160
+
161
+ ch = stdscr.getch()
162
+ if ch in _QUIT_KEYS:
163
+ return None
164
+ if ch in (curses.KEY_DOWN, ord("j")):
165
+ selected += 1
166
+ continue
167
+ if ch in (curses.KEY_UP, ord("k")):
168
+ selected -= 1
169
+ continue
170
+ if ch in (curses.KEY_RESIZE,):
171
+ continue
172
+ if ch in (ord("R"),): # refresh the queue (re-read sources)
173
+ rows = _decisions.collect_decisions(cfg, resolver=resolver)
174
+ continue
175
+ # An action key — look it up in the selected decision's next_steps.
176
+ if rows:
177
+ try:
178
+ key_char = chr(ch).lower()
179
+ except ValueError:
180
+ continue
181
+ steps = _decisions.next_steps(rows[selected], cfg)
182
+ for k, command in steps:
183
+ if k == key_char:
184
+ return (k, command)
185
+ # Unrecognised key — ignore and redraw.
186
+
187
+
188
+ def _draw(stdscr, curses, rows, selected, resolver, cfg=None, urgency_attrs=None):
189
+ """Render one frame: list pane (top) + detail pane (bottom).
190
+
191
+ The list is the triage surface: each row leads with a severity glyph +
192
+ colour (red NOW / yellow SOON / dim LATER, anchored on the same rank the
193
+ queue sorts by), shows the human-readable reason (not the raw enum), and
194
+ ends with the 1–2 live action keys for that row — so the operator sees what
195
+ is on fire and what they can do about it without entering the detail pane.
196
+ """
197
+ cfg = _config.ensure(cfg)
198
+ urgency_attrs = urgency_attrs or {}
199
+ stdscr.erase()
200
+ height, width = stdscr.getmaxyx()
201
+
202
+ def _put(y, x, text, attr=0):
203
+ if 0 <= y < height and x < width:
204
+ stdscr.addnstr(y, x, text, max(0, width - x - 1), attr)
205
+
206
+ scope_label = (resolver or "ALL")
207
+ tally = _decisions.urgency_tally(rows)
208
+ tally_s = f" ({tally})" if tally else ""
209
+ title = f" operator decisions · {scope_label} · {len(rows)} pending{tally_s} "
210
+ _put(0, 0, title.ljust(width - 1), curses.A_REVERSE)
211
+
212
+ # ---- list pane ----
213
+ list_top = 2
214
+ list_h = max(3, (height - 2) // 2)
215
+ if not rows:
216
+ _put(list_top, 2, "(none pending — nothing is waiting on you)")
217
+ for i, d in enumerate(rows[:list_h]):
218
+ cursor = ">" if i == selected else " "
219
+ glyph = _decisions.urgency_glyph(d)
220
+ dup = f" ×{d.dup_count}" if d.dup_count > 1 else ""
221
+ hint = _decisions.fmt_action_hints(d, cfg)
222
+ hint_s = f" {hint}" if hint else ""
223
+ reason = (d.reason_text or d.reason_token)
224
+ line = (f"{cursor}{glyph} {i + 1:>2} {_decisions._fmt_age(d.age_seconds):>4} "
225
+ f"{d.kind.value:<16} {(d.lane or '-'):<10} {reason[:36]}{dup}{hint_s}")
226
+ if i == selected:
227
+ attr = curses.A_BOLD | curses.A_REVERSE
228
+ else:
229
+ attr = urgency_attrs.get(_decisions.urgency_of(d), 0)
230
+ _put(list_top + i, 0, line, attr)
231
+
232
+ # ---- detail pane ----
233
+ detail_top = list_top + list_h + 1
234
+ _put(detail_top - 1, 0, "─" * (width - 1))
235
+ if rows:
236
+ detail = _decisions.render_detail_plain(rows[selected], cfg)
237
+ for j, dl in enumerate(detail.splitlines()):
238
+ _put(detail_top + j, 0, dl)
239
+
240
+ # ---- footer ----
241
+ # Name the live action keys for the SELECTED row in full (incl. copy /
242
+ # let-it-ride), so the operator never has to read the detail pane to learn
243
+ # what they can press. Falls back to the generic hint when nothing is
244
+ # selected. `R refresh · q quit` are always-on and listed once at the end.
245
+ if rows:
246
+ keys = _decisions.footer_keys(rows[selected], cfg) or "(no action)"
247
+ foot = f" ↑/↓ move · {keys} · R refresh · q quit "
248
+ else:
249
+ foot = " ↑/↓ move · R refresh · q quit "
250
+ _put(height - 1, 0, foot.ljust(width - 1), curses.A_REVERSE)
251
+ stdscr.refresh()