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/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()
|