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_mcp/server.py
ADDED
|
@@ -0,0 +1,779 @@
|
|
|
1
|
+
"""The FastMCP server — DOS syscalls as MCP tools.
|
|
2
|
+
|
|
3
|
+
Run it as ``dos-mcp`` (the console script) or ``python -m dos_mcp.server``. It
|
|
4
|
+
serves over stdio by default, which is what an MCP host (Claude Desktop, Cursor,
|
|
5
|
+
Cline, …) launches and talks to. See the package docstring for the design fence:
|
|
6
|
+
this consumes `dos`, the kernel never imports it.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import functools
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
import threading
|
|
15
|
+
from typing import Any
|
|
16
|
+
from urllib.parse import unquote
|
|
17
|
+
|
|
18
|
+
# Force UTF-8 on the streams, matching the spine modules' discipline — a verdict
|
|
19
|
+
# summary / man line may carry an em-dash or middot, and a host on a cp1252
|
|
20
|
+
# console must not crash the server on it. (The MCP transport is JSON, but be
|
|
21
|
+
# defensive about any stray stderr logging too.)
|
|
22
|
+
for _stream in (sys.stdout, sys.stderr):
|
|
23
|
+
if hasattr(_stream, "reconfigure"):
|
|
24
|
+
try:
|
|
25
|
+
_stream.reconfigure(encoding="utf-8", errors="replace")
|
|
26
|
+
except Exception:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
from mcp.server.fastmcp import FastMCP
|
|
31
|
+
except ModuleNotFoundError as e: # pragma: no cover - install-hint path
|
|
32
|
+
raise SystemExit(
|
|
33
|
+
"dos-mcp requires the MCP server framework, which is an optional extra.\n"
|
|
34
|
+
"Install it with: pip install 'dos-kernel[mcp]' (or: pip install mcp)\n"
|
|
35
|
+
f"(original import error: {e})"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
import dos # noqa: E402 — intentionally after the MCP-framework import guard above
|
|
39
|
+
from dos import config as _config # noqa: E402 — (so a missing [mcp] extra fails with a hint)
|
|
40
|
+
from dos import interpret as _interpret # noqa: E402 — shared with the CLI's --explain
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# Workspace config — the `dos` CLI's four-table dos.toml readback, shared.
|
|
45
|
+
#
|
|
46
|
+
# The readback (generic base + [reasons]/[stamp]/[lanes]/[paths]) lives in ONE
|
|
47
|
+
# place, `config.load_workspace_config`, which the CLI also calls — so the two
|
|
48
|
+
# surfaces can't drift. The server's only divergence from the CLI is what it
|
|
49
|
+
# does with the result: the CLI `set_active`s it (correct for a one-shot
|
|
50
|
+
# process); the server passes it EXPLICITLY into each syscall
|
|
51
|
+
# (`oracle.is_shipped(cfg=...)`, `arbiter.arbitrate(config=...)`) — the
|
|
52
|
+
# "explicit SubstrateConfig in code" rung — because a long-lived server fields
|
|
53
|
+
# concurrent calls against different workspaces and must never mutate a
|
|
54
|
+
# process-global. A malformed table is routed to stderr as a server log line
|
|
55
|
+
# (MCP hosts capture stderr), never crashing a tool that doesn't touch that axis.
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
def _load_workspace_config(workspace: str | None) -> "_config.SubstrateConfig":
|
|
58
|
+
"""Build the config for ``workspace`` (None/"." → cwd), folding in dos.toml.
|
|
59
|
+
|
|
60
|
+
Thin adapter over `config.load_workspace_config`; see that function for the
|
|
61
|
+
layering + asymmetry contract. A workspace with no ``dos.toml`` is
|
|
62
|
+
byte-identical to the generic built-in default.
|
|
63
|
+
"""
|
|
64
|
+
def _warn(label: str, message: str) -> None:
|
|
65
|
+
print(f"[dos-mcp] ignoring malformed [{label}] in "
|
|
66
|
+
f"{workspace or '.'}/dos.toml: {message}", file=sys.stderr)
|
|
67
|
+
|
|
68
|
+
# `gather_env=False`: NONE of this server's tools read `cfg.env` (the runtime
|
|
69
|
+
# EnvPrint — kernel version/SHA/platform/tools), so probing it on every tool
|
|
70
|
+
# call wasted a `git rev-parse` subprocess + (first call) a WMI platform query
|
|
71
|
+
# — ~tens of ms per call for a field thrown away. Skipping it leaves `env=None`
|
|
72
|
+
# (the documented "not recorded" state every consumer already handles). If a
|
|
73
|
+
# future tool needs the print, build that one call's config with the default
|
|
74
|
+
# (gather_env=True) — the gatherer memoizes per process, so the cost is paid
|
|
75
|
+
# at most once for the server's lifetime.
|
|
76
|
+
return _config.load_workspace_config(workspace, gather_env=False, warn=_warn)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
# Agent-facing interpretation — turn a kernel verdict into one line of "what
|
|
81
|
+
# this means for your NEXT action."
|
|
82
|
+
#
|
|
83
|
+
# The functions themselves live in `dos.interpret` (the kernel-side presentation
|
|
84
|
+
# seam, beside `dos.render`), NOT here — so the `dos` CLI's `--explain` flag and
|
|
85
|
+
# these MCP tools call the SAME code and can never drift (the parity is
|
|
86
|
+
# structural, pinned by tests/test_interpret_parity.py). They are PURE
|
|
87
|
+
# PRESENTATION, added to a tool's return as an `interpretation` field ALONGSIDE
|
|
88
|
+
# the kernel's own verbatim verdict fields — never replacing them. This honors
|
|
89
|
+
# the renderer invariant (HACKING.md Axis 4 / docs/76): the hint is strictly
|
|
90
|
+
# downstream of an already-decided verdict, so it can never leak policy back into
|
|
91
|
+
# the adjudication. The worst a wrong hint can do is read awkwardly; it cannot
|
|
92
|
+
# mis-verify a ship or mis-admit a lease. The point is Claude-friendliness: a
|
|
93
|
+
# model acts better on "treat as NOT done; do not rely on a worker's claim" than
|
|
94
|
+
# on a bare `{"shipped": false}`.
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
# The tool-call deadline — the kernel's STALLED verdict, applied to this server.
|
|
97
|
+
#
|
|
98
|
+
# A tool body that never returns is the server narrating "I'm working" while
|
|
99
|
+
# making NO forward progress — exactly `liveness.Verdict.STALLED` ("no fresh
|
|
100
|
+
# heartbeat, no commits — dead/hung"). The kernel preaches that distrust for a
|
|
101
|
+
# WORKER run; we apply it to our own MCP surface: a tool call is a mini-run, so
|
|
102
|
+
# bound it with a wall-clock deadline and, on expiry, return a TYPED STALLED
|
|
103
|
+
# envelope from the closed verdict vocabulary instead of hanging the host.
|
|
104
|
+
#
|
|
105
|
+
# This matters most on a hot, multi-session tree: a peer's `git commit` holds
|
|
106
|
+
# `.git/index.lock`, and a syscall that shells `git show HEAD` / `git diff`
|
|
107
|
+
# blocks on it. The CLI computing the SAME verdict in ~300 ms is the
|
|
108
|
+
# ground-truth witness that the kernel logic is healthy — the stall is the
|
|
109
|
+
# TRANSPORT, so the envelope says "fall back to the CLI," and (docs/99) it is
|
|
110
|
+
# advisory: surface, do NOT auto-retry (a retry on a held lock just stalls
|
|
111
|
+
# again — the poll-loop antipattern). See docs/282.
|
|
112
|
+
#
|
|
113
|
+
# The budget is POLICY: env `DOS_MCP_TOOL_DEADLINE_MS` (default 5000); 0/blank
|
|
114
|
+
# disables the wrapper entirely (byte-identical to the pre-deadline server, so
|
|
115
|
+
# a host that wants the old unbounded behavior opts out). Mechanism only — no
|
|
116
|
+
# `src/dos/` leaf is touched; the one-way arrow (dos_mcp imports dos) holds.
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
def _tool_deadline_ms() -> int:
|
|
119
|
+
"""The per-tool wall-clock budget in ms (env-driven policy; 0 disables)."""
|
|
120
|
+
raw = os.environ.get("DOS_MCP_TOOL_DEADLINE_MS", "5000").strip()
|
|
121
|
+
try:
|
|
122
|
+
return max(0, int(raw))
|
|
123
|
+
except ValueError:
|
|
124
|
+
return 5000
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _with_deadline(fn: Any, budget_ms: int) -> Any:
|
|
128
|
+
"""Race ``fn`` against ``budget_ms``; on expiry return a typed STALLED dict.
|
|
129
|
+
|
|
130
|
+
The tool bodies are synchronous (they shell git / read files), so a blocked
|
|
131
|
+
call would hang the event loop. We run the body in a daemon thread and join
|
|
132
|
+
with the budget: if it is still alive past the deadline, the call returns a
|
|
133
|
+
STALLED verdict promptly and the zombie thread is left to drain when the OS
|
|
134
|
+
resource frees (a daemon thread can't be force-killed in CPython — acceptable
|
|
135
|
+
for a stall escape hatch; the point is the *call* returns, not that the work
|
|
136
|
+
is reaped). The fast path (body returns in time) passes through byte-identical.
|
|
137
|
+
"""
|
|
138
|
+
if budget_ms <= 0:
|
|
139
|
+
return fn
|
|
140
|
+
|
|
141
|
+
@functools.wraps(fn)
|
|
142
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
143
|
+
box: dict[str, Any] = {}
|
|
144
|
+
|
|
145
|
+
def _run() -> None:
|
|
146
|
+
try:
|
|
147
|
+
box["result"] = fn(*args, **kwargs)
|
|
148
|
+
except BaseException as exc: # noqa: BLE001 — re-raised on the caller thread
|
|
149
|
+
box["error"] = exc
|
|
150
|
+
|
|
151
|
+
worker = threading.Thread(target=_run, name=f"dos-mcp:{fn.__name__}",
|
|
152
|
+
daemon=True)
|
|
153
|
+
worker.start()
|
|
154
|
+
worker.join(budget_ms / 1000.0)
|
|
155
|
+
if worker.is_alive():
|
|
156
|
+
return {
|
|
157
|
+
"verdict": "STALLED",
|
|
158
|
+
"reason": (
|
|
159
|
+
f"tool {fn.__name__!r} exceeded its {budget_ms} ms deadline — "
|
|
160
|
+
"the server or a shared OS resource (e.g. the git index lock on "
|
|
161
|
+
"a hot tree) is blocked; the call did not return."
|
|
162
|
+
),
|
|
163
|
+
"fallback": (
|
|
164
|
+
"The kernel verdict is reachable on the CLI (which is healthy "
|
|
165
|
+
"even when this transport stalls): run the matching `dos` verb "
|
|
166
|
+
"for this tool. This stall is the TRANSPORT, not the syscall."
|
|
167
|
+
),
|
|
168
|
+
"advice": (
|
|
169
|
+
"Advisory (do not auto-retry a held lock — it will stall again). "
|
|
170
|
+
"Surface this and either use the CLI or wait for the lock holder "
|
|
171
|
+
"to finish."
|
|
172
|
+
),
|
|
173
|
+
}
|
|
174
|
+
if "error" in box:
|
|
175
|
+
raise box["error"]
|
|
176
|
+
return box.get("result")
|
|
177
|
+
|
|
178
|
+
return wrapper
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
# Server construction
|
|
183
|
+
# ---------------------------------------------------------------------------
|
|
184
|
+
def build_server() -> FastMCP:
|
|
185
|
+
"""Construct the FastMCP server with the DOS syscall tools registered.
|
|
186
|
+
|
|
187
|
+
Factored out of `main()` so a test can build the server and introspect /
|
|
188
|
+
call the tools without starting the stdio transport.
|
|
189
|
+
"""
|
|
190
|
+
mcp = FastMCP(
|
|
191
|
+
"dos",
|
|
192
|
+
instructions=(
|
|
193
|
+
"DOS — the domain-free trust substrate for fleets of autonomous "
|
|
194
|
+
"agents. The kernel is the part that doesn't believe the agents. "
|
|
195
|
+
"Use `dos_verify` to confirm a claim landed from git evidence rather "
|
|
196
|
+
"than a worker's self-report; `dos_arbitrate` to decide whether two "
|
|
197
|
+
"workers may run concurrently without colliding on the same files; "
|
|
198
|
+
"and `dos_refuse_reasons` / `dos_check_reason` to refuse with a "
|
|
199
|
+
"structured, verifiable reason from a closed vocabulary instead of "
|
|
200
|
+
"free-text. All tools take an optional `workspace` (a repo path); it "
|
|
201
|
+
"defaults to the server's working directory."
|
|
202
|
+
),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Install the tool-call deadline transparently: wrap `mcp.tool` so every
|
|
206
|
+
# `@mcp.tool()` registration below races its body against the wall-clock
|
|
207
|
+
# budget and returns a typed STALLED verdict on expiry instead of hanging
|
|
208
|
+
# (docs/282). `functools.wraps` in `_with_deadline` preserves the function
|
|
209
|
+
# name, docstring, and signature, so FastMCP's schema introspection of each
|
|
210
|
+
# tool is unchanged — the deadline is invisible to the wire contract. When
|
|
211
|
+
# the budget is 0 (env opt-out) `_with_deadline` returns the body untouched,
|
|
212
|
+
# so this is byte-identical to the pre-deadline server.
|
|
213
|
+
_budget_ms = _tool_deadline_ms()
|
|
214
|
+
_raw_tool = mcp.tool
|
|
215
|
+
|
|
216
|
+
def _tool(*d_args: Any, **d_kwargs: Any):
|
|
217
|
+
_register = _raw_tool(*d_args, **d_kwargs)
|
|
218
|
+
|
|
219
|
+
def _decorate(fn: Any) -> Any:
|
|
220
|
+
return _register(_with_deadline(fn, _budget_ms))
|
|
221
|
+
|
|
222
|
+
return _decorate
|
|
223
|
+
|
|
224
|
+
mcp.tool = _tool # type: ignore[method-assign]
|
|
225
|
+
|
|
226
|
+
@mcp.tool()
|
|
227
|
+
def dos_verify(plan: str, phase: str, workspace: str = ".") -> dict[str, Any]:
|
|
228
|
+
"""Did (plan, phase) actually ship? — the truth syscall.
|
|
229
|
+
|
|
230
|
+
USE THIS WHEN: another agent (or the user) *claims* a task/phase/feature
|
|
231
|
+
is done, and you want to confirm it from real evidence before relying on
|
|
232
|
+
it or building on top of it. This is the antidote to a self-narrating
|
|
233
|
+
worker: it answers from artifacts, never from anyone's word.
|
|
234
|
+
|
|
235
|
+
It checks, in order: a run-registry row (status=done), then a git-log
|
|
236
|
+
grep over the workspace's ship-commit grammar, then an honest
|
|
237
|
+
`source="none"` when there is no positive evidence. Works against a plain
|
|
238
|
+
git repo with **no plan and no registry** — point it at any repo.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
plan: the plan / series id (e.g. "AUTH", "RS").
|
|
242
|
+
phase: the phase id within that plan (e.g. "AUTH2", "RS1").
|
|
243
|
+
workspace: the repo root to verify against (default: cwd). Its
|
|
244
|
+
`dos.toml [stamp]` grammar is honored if present.
|
|
245
|
+
|
|
246
|
+
Returns {plan, phase, shipped, source, sha?, summary?, interpretation}.
|
|
247
|
+
`shipped` is the closed binary judgment; `source` names which authority
|
|
248
|
+
answered ("registry" | "grep" | "none") — a thin answer can never
|
|
249
|
+
masquerade as a strong one. `interpretation` (added by this server) tells
|
|
250
|
+
you in one line what the verdict means for your next action.
|
|
251
|
+
"""
|
|
252
|
+
from dos import oracle
|
|
253
|
+
cfg = _load_workspace_config(workspace)
|
|
254
|
+
verdict = oracle.is_shipped(plan, phase, cfg=cfg).to_dict()
|
|
255
|
+
verdict["interpretation"] = _interpret.verify(verdict)
|
|
256
|
+
return verdict
|
|
257
|
+
|
|
258
|
+
@mcp.tool()
|
|
259
|
+
def dos_commit_audit(ref: str = "HEAD", workspace: str = ".") -> dict[str, Any]:
|
|
260
|
+
"""Does a commit's CLAIM match what its DIFF actually did? — author-neutral.
|
|
261
|
+
|
|
262
|
+
USE THIS WHEN: you (or a worker) are about to report a commit as "done," OR
|
|
263
|
+
you are reviewing someone's commit and want to know whether its message can
|
|
264
|
+
be trusted. It is the plan-free form of the truth syscall: a commit subject
|
|
265
|
+
is forgeable (whoever wrote the message authored it), the files it touched
|
|
266
|
+
are not (git did). So it catches a `fix: ...` that touched only a README, an
|
|
267
|
+
`--allow-empty "shipped"`, or a "tests pass" that deleted the assertions —
|
|
268
|
+
the SAME way whether a human or an agent wrote the commit.
|
|
269
|
+
|
|
270
|
+
Needs no plan, no phase, no config — point it at any commit in any git repo.
|
|
271
|
+
It grades did-the-diff-do-the-KIND-of-thing-claimed, NEVER whether the code
|
|
272
|
+
is correct (run the tests for that).
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
ref: a commit ref (default "HEAD"). A `A..B` range is NOT supported by
|
|
276
|
+
this tool — call it per-commit so each verdict is its own object.
|
|
277
|
+
workspace: the repo root the commit lives in (default: cwd).
|
|
278
|
+
|
|
279
|
+
Returns {sha, verdict ("OK"|"CLAIM_UNWITNESSED"|"ABSTAIN"), claim_kind,
|
|
280
|
+
witness ("diff-witnessed"|"subject-only"|"abstain"), reason, source_files,
|
|
281
|
+
test_files, interpretation}. `witness` is the forgeability rung:
|
|
282
|
+
`diff-witnessed` is non-forgeable evidence; `subject-only` means the claim
|
|
283
|
+
rests on the message text alone. `interpretation` (added by this server)
|
|
284
|
+
tells you in one line what to do next.
|
|
285
|
+
"""
|
|
286
|
+
from dos import commit_audit as _ca
|
|
287
|
+
cfg = _load_workspace_config(workspace)
|
|
288
|
+
v = _ca.audit_commit(ref, root=cfg.paths.root)
|
|
289
|
+
if v is None:
|
|
290
|
+
out = {
|
|
291
|
+
"sha": "", "verdict": "ABSTAIN", "claim_kind": "none",
|
|
292
|
+
"witness": "abstain",
|
|
293
|
+
"reason": f"cannot read commit '{ref}' (not a git repo, or bad ref)",
|
|
294
|
+
"source_files": [], "test_files": [],
|
|
295
|
+
}
|
|
296
|
+
out["interpretation"] = (
|
|
297
|
+
"UNREADABLE — the ref could not be read; there is no commit to audit "
|
|
298
|
+
"here. Check the ref and the workspace path.")
|
|
299
|
+
return out
|
|
300
|
+
out = v.to_dict()
|
|
301
|
+
out["interpretation"] = _interpret.commit_audit(out)
|
|
302
|
+
return out
|
|
303
|
+
|
|
304
|
+
@mcp.tool()
|
|
305
|
+
def dos_arbitrate(
|
|
306
|
+
lane: str = "",
|
|
307
|
+
kind: str = "",
|
|
308
|
+
tree: list[str] | None = None,
|
|
309
|
+
live_leases: list[dict[str, Any]] | None = None,
|
|
310
|
+
force: bool = False,
|
|
311
|
+
workspace: str = ".",
|
|
312
|
+
) -> dict[str, Any]:
|
|
313
|
+
"""May a worker take this lane right now? — the pure admission kernel.
|
|
314
|
+
|
|
315
|
+
USE THIS WHEN: you are about to start work that touches a set of files
|
|
316
|
+
(or dispatch a sub-agent to), and other agents may be working in the same
|
|
317
|
+
repo concurrently. Call this FIRST to find out whether your file-tree
|
|
318
|
+
collides with work already in flight. It is the mechanism that stops two
|
|
319
|
+
agents editing the same files at once.
|
|
320
|
+
|
|
321
|
+
State in, decision out, no I/O. Decides whether a new worker may acquire
|
|
322
|
+
`lane` given the `live_leases` already held, using the workspace's lane
|
|
323
|
+
taxonomy and a tree-disjointness rule (two workers may run concurrently
|
|
324
|
+
iff their file trees don't overlap beyond a small threshold).
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
lane: the requested lane ("" = a bare auto-pick request — the arbiter
|
|
328
|
+
walks the workspace's autopick ladder for a free, disjoint lane).
|
|
329
|
+
kind: "cluster" | "keyword" | "global" | "" (bare → auto-pick).
|
|
330
|
+
tree: the requested file tree as repo-relative globs. If omitted and
|
|
331
|
+
a `lane` is named, the lane's canonical tree from `dos.toml` is
|
|
332
|
+
used.
|
|
333
|
+
live_leases: the leases currently held — a list of dicts each with at
|
|
334
|
+
least {lane, lane_kind, tree}. Empty/omitted = nothing live.
|
|
335
|
+
force: operator override — honor an explicit `lane` literally, skip
|
|
336
|
+
the disjointness refuse (still respects a live exclusive lane).
|
|
337
|
+
workspace: the repo root whose lane taxonomy to arbitrate over
|
|
338
|
+
(default: cwd). Its `dos.toml [lanes]` is honored if present.
|
|
339
|
+
|
|
340
|
+
Returns {outcome ("acquire"|"refuse"), lane, lane_kind, tree,
|
|
341
|
+
auto_picked, reason, free_clusters, pick_count, interpretation}. On a
|
|
342
|
+
refuse, `reason` explains why and `free_clusters` lists lanes you could
|
|
343
|
+
take instead. `interpretation` (added by this server) is a one-line
|
|
344
|
+
GO/STOP verdict for your next action.
|
|
345
|
+
|
|
346
|
+
Note: unlike `dos arbitrate --force` on the CLI, this tool never persists
|
|
347
|
+
a decision — it is a pure adjudication. An MCP tool decides; it does not
|
|
348
|
+
write to the workspace.
|
|
349
|
+
"""
|
|
350
|
+
from dos import arbiter
|
|
351
|
+
from dos.admission import built_in_predicates
|
|
352
|
+
cfg = _load_workspace_config(workspace)
|
|
353
|
+
req_tree = list(tree or [])
|
|
354
|
+
if not req_tree and lane:
|
|
355
|
+
req_tree = cfg.lanes.tree_for(lane)
|
|
356
|
+
# Scope the SELF_MODIFY guard to the kernel-source files that actually
|
|
357
|
+
# exist under the SERVED workspace: a foreign repo's `**/*` lane cannot
|
|
358
|
+
# edit a `src/dos/` file that isn't there, so it must not trip the guard.
|
|
359
|
+
# We pass `config=cfg` so the guard reads the CACHED `cfg.workspace` facts
|
|
360
|
+
# `_load_workspace_config` already gathered — no second disk probe per
|
|
361
|
+
# tool call, which matters for a long-lived server fielding concurrent
|
|
362
|
+
# workspaces (the explicit-config / no-global-mutation discipline). These
|
|
363
|
+
# are the workspace-scoped BUILT-INS only (no `dos.predicates` plugin
|
|
364
|
+
# discovery — this tool stays plugin-free, matching its prior behavior).
|
|
365
|
+
decision = arbiter.arbitrate(
|
|
366
|
+
requested_lane=lane or "",
|
|
367
|
+
requested_kind=kind or "",
|
|
368
|
+
requested_tree=req_tree,
|
|
369
|
+
live_leases=list(live_leases or []),
|
|
370
|
+
config=cfg,
|
|
371
|
+
force=force,
|
|
372
|
+
predicates=built_in_predicates(config=cfg),
|
|
373
|
+
).to_dict()
|
|
374
|
+
decision["interpretation"] = _interpret.arbitrate(decision)
|
|
375
|
+
return decision
|
|
376
|
+
|
|
377
|
+
@mcp.tool()
|
|
378
|
+
def dos_refuse_reasons(workspace: str = ".") -> dict[str, Any]:
|
|
379
|
+
"""The closed structured-refusal vocabulary for this workspace.
|
|
380
|
+
|
|
381
|
+
USE THIS WHEN: you need to decline / refuse / report-blocked and want to
|
|
382
|
+
do it with a *structured* reason the system can verify, instead of
|
|
383
|
+
free-text prose. Browse this list, pick the token that fits, and emit
|
|
384
|
+
THAT (verify it first with `dos_check_reason`).
|
|
385
|
+
|
|
386
|
+
DOS refuses with a *reason from a closed set* — every reason is
|
|
387
|
+
simultaneously **emittable** (a producer may stamp it), **verifiable** (an
|
|
388
|
+
oracle can check the condition it names), and **refusable** (the loop
|
|
389
|
+
knows to route it to a replan). That is what makes "no" a first-class,
|
|
390
|
+
auditable value rather than a dead end.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
workspace: the repo root (default: cwd). Reasons declared in its
|
|
394
|
+
`dos.toml [reasons]` table are included alongside the built-ins.
|
|
395
|
+
|
|
396
|
+
Returns {workspace, count, reasons: [{token, category, refusal, summary,
|
|
397
|
+
fix, see_also}, ...]}. `category` is the coarse class the reason rolls up
|
|
398
|
+
to (TRUE_DRAIN | OPERATOR_GATE | STALE_CLAIM | MISROUTE | UNCLASSIFIED);
|
|
399
|
+
`refusal` is whether carrying it blocks (vs advisory-only).
|
|
400
|
+
"""
|
|
401
|
+
cfg = _load_workspace_config(workspace)
|
|
402
|
+
reg = cfg.reasons
|
|
403
|
+
return {
|
|
404
|
+
"workspace": str(cfg.paths.root),
|
|
405
|
+
"count": len(reg.specs),
|
|
406
|
+
"reasons": [
|
|
407
|
+
{
|
|
408
|
+
"token": s.key,
|
|
409
|
+
"category": s.category,
|
|
410
|
+
"refusal": s.refusal,
|
|
411
|
+
"summary": s.summary,
|
|
412
|
+
"fix": s.fix,
|
|
413
|
+
"see_also": list(s.see_also),
|
|
414
|
+
}
|
|
415
|
+
for s in reg.specs
|
|
416
|
+
],
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
@mcp.tool()
|
|
420
|
+
def dos_check_reason(reason_class: str, workspace: str = ".") -> dict[str, Any]:
|
|
421
|
+
"""Is `reason_class` a member of the closed refusal vocabulary?
|
|
422
|
+
|
|
423
|
+
USE THIS WHEN: you have a reason token in mind for a refusal and want to
|
|
424
|
+
confirm it is real BEFORE emitting it. The companion to
|
|
425
|
+
`dos_refuse_reasons`: emit only a reason this returns `known=true` for.
|
|
426
|
+
An unknown token is the `UNCLASSIFIED` prose-drift the kernel exists to
|
|
427
|
+
kill — this tool surfaces it as a bug to declare, not tolerate.
|
|
428
|
+
|
|
429
|
+
Args:
|
|
430
|
+
reason_class: the reason token to check (case-insensitive, e.g.
|
|
431
|
+
"LANE_DRAINED").
|
|
432
|
+
workspace: the repo root (default: cwd); its declared reasons count.
|
|
433
|
+
|
|
434
|
+
Returns {reason_class, known, category, refusal, summary?, fix?,
|
|
435
|
+
interpretation}. When `known` is false, `category` is "UNCLASSIFIED" and
|
|
436
|
+
`refusal` is true (an unrecognised refusal is refused conservatively);
|
|
437
|
+
`interpretation` tells you whether it is safe to emit.
|
|
438
|
+
"""
|
|
439
|
+
cfg = _load_workspace_config(workspace)
|
|
440
|
+
reg = cfg.reasons
|
|
441
|
+
spec = reg.get(reason_class)
|
|
442
|
+
out: dict[str, Any] = {
|
|
443
|
+
"reason_class": reason_class,
|
|
444
|
+
"known": spec is not None,
|
|
445
|
+
"category": reg.category_for(reason_class),
|
|
446
|
+
"refusal": reg.is_refusal(reason_class),
|
|
447
|
+
}
|
|
448
|
+
if spec is not None:
|
|
449
|
+
out["summary"] = spec.summary
|
|
450
|
+
out["fix"] = spec.fix
|
|
451
|
+
out["see_also"] = list(spec.see_also)
|
|
452
|
+
out["interpretation"] = _interpret.check_reason(out)
|
|
453
|
+
return out
|
|
454
|
+
|
|
455
|
+
@mcp.tool()
|
|
456
|
+
def dos_recall(name: str, workspace: str = ".", store: str = "") -> dict[str, Any]:
|
|
457
|
+
"""Is this recalled memory still TRUE? — re-verify a memory at read time.
|
|
458
|
+
|
|
459
|
+
USE THIS WHEN: a saved memory / note is about to be injected as context and
|
|
460
|
+
it NAMES a concrete artifact (a commit SHA, an import/flag, a file path).
|
|
461
|
+
A memory is a frozen self-report from a past session — the least
|
|
462
|
+
trustworthy signal in the stack, yet recall hands it to you wearing the
|
|
463
|
+
authority of a fact. Call this to re-check its claims against git + the
|
|
464
|
+
working tree NOW, instead of trusting the body. This is `dos_verify`'s
|
|
465
|
+
discipline (evidence, not self-report) aimed at the agent's own memory
|
|
466
|
+
(docs/103).
|
|
467
|
+
|
|
468
|
+
It parses the memory's frontmatter (trusted structure), extracts the
|
|
469
|
+
checkable claims in its body + the polarity each asserts (is this code
|
|
470
|
+
claimed PRESENT? this commit SHIPPED?), and re-probes each against ground
|
|
471
|
+
truth: a comment-aware working-tree grep for a code token, git
|
|
472
|
+
merge-base ancestry for a SHA, git history for a path. Returns a closed
|
|
473
|
+
recall verdict; on anything but RECALL_FRESH, present the memory hedged or
|
|
474
|
+
withhold it — never inject its raw content as confirmed.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
name: the memory's frontmatter `name` / slug (resolved against the
|
|
478
|
+
store) or a direct path to the `.md` file.
|
|
479
|
+
workspace: the repo root whose git/working-tree is ground truth
|
|
480
|
+
(default: cwd).
|
|
481
|
+
store: the agent-memory directory (default: the documented
|
|
482
|
+
`~/.claude/projects/<workspace>/memory` layout). Pass it explicitly
|
|
483
|
+
when the memory store is elsewhere.
|
|
484
|
+
|
|
485
|
+
Returns {memory, verdict, type, culprit, claims, interpretation}.
|
|
486
|
+
`verdict` is one of RECALL_FRESH / RECALL_STALE / RECALL_UNVERIFIABLE;
|
|
487
|
+
`culprit` (on STALE) is the deciding claim + the git evidence behind it;
|
|
488
|
+
`interpretation` (added here) tells you in one line what to do next. The
|
|
489
|
+
driver is resolved by name — the kernel never imports it.
|
|
490
|
+
"""
|
|
491
|
+
import importlib
|
|
492
|
+
cfg = _load_workspace_config(workspace)
|
|
493
|
+
mr = importlib.import_module("dos.drivers.memory_recall")
|
|
494
|
+
verdict = mr.recall_one(name, cfg=cfg, store=store or None).to_dict()
|
|
495
|
+
verdict["interpretation"] = mr.interpret(verdict) # gloss single-sourced in the DRIVER
|
|
496
|
+
return verdict
|
|
497
|
+
|
|
498
|
+
@mcp.tool()
|
|
499
|
+
def dos_doctor(workspace: str = ".") -> dict[str, Any]:
|
|
500
|
+
"""The machine-readable workspace report — paths, lanes, stamp grammar.
|
|
501
|
+
|
|
502
|
+
What an agent reads once to discover a workspace's layout instead of
|
|
503
|
+
hardcoding it: where plans live, the lane taxonomy `dos_arbitrate` will
|
|
504
|
+
use, the ship-stamp grammar `dos_verify` recognizes, and whether the root
|
|
505
|
+
is a git repo. Read-only — resolves everything without creating `.dos/`.
|
|
506
|
+
|
|
507
|
+
Args:
|
|
508
|
+
workspace: the repo root to report on (default: cwd).
|
|
509
|
+
|
|
510
|
+
Returns {dos_version, workspace, git, paths, lanes, stamp}.
|
|
511
|
+
"""
|
|
512
|
+
cfg = _load_workspace_config(workspace)
|
|
513
|
+
return {
|
|
514
|
+
"dos_version": dos.__version__,
|
|
515
|
+
"workspace": str(cfg.paths.root),
|
|
516
|
+
"git": (cfg.paths.root / ".git").exists(),
|
|
517
|
+
"paths": {
|
|
518
|
+
"root": str(cfg.paths.root),
|
|
519
|
+
"execution_state": str(cfg.paths.execution_state),
|
|
520
|
+
"plans_glob": cfg.paths.plans_glob,
|
|
521
|
+
"style": cfg.paths.style,
|
|
522
|
+
},
|
|
523
|
+
"lanes": {
|
|
524
|
+
"concurrent": list(cfg.lanes.concurrent),
|
|
525
|
+
"exclusive": list(cfg.lanes.exclusive),
|
|
526
|
+
"autopick": list(cfg.lanes.autopick),
|
|
527
|
+
"trees": {k: list(v) for k, v in cfg.lanes.trees.items()},
|
|
528
|
+
},
|
|
529
|
+
"stamp": cfg.stamp.to_dict(),
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
@mcp.tool()
|
|
533
|
+
def dos_status(
|
|
534
|
+
run_id: str,
|
|
535
|
+
start_sha: str = "",
|
|
536
|
+
lane: str = "",
|
|
537
|
+
loop_ts: str = "",
|
|
538
|
+
stopped: bool = False,
|
|
539
|
+
live: bool = False,
|
|
540
|
+
now_ms: int | None = None,
|
|
541
|
+
workspace: str = ".",
|
|
542
|
+
) -> dict[str, Any]:
|
|
543
|
+
"""One folded, fail-closed status fact for a run — liveness · progress · region · resume.
|
|
544
|
+
|
|
545
|
+
USE THIS WHEN: you want a single A2A-shaped answer to "what is the state of
|
|
546
|
+
run X right now?" without trusting any worker's self-report. It folds FOUR
|
|
547
|
+
adjudicated kernel verdicts into one record — liveness (is it moving?),
|
|
548
|
+
ledger-VERIFIED progress (never the agent's claim), the run's held-lease
|
|
549
|
+
region, and the resume plan (only once the run has stopped). It is the
|
|
550
|
+
legible, peer-readable form of `dos_verify`'s distrust discipline aimed at a
|
|
551
|
+
whole run instead of one phase.
|
|
552
|
+
|
|
553
|
+
The load-bearing property (docs/120 §3): the digest has **no `claimed`
|
|
554
|
+
field** by construction. A peer reading this result structurally cannot pick
|
|
555
|
+
up a self-report it is never handed — `progress` is built from the kernel's
|
|
556
|
+
VERIFIED rung only. Fail-closed everywhere: a run with no intent ledger is a
|
|
557
|
+
valid zero-progress fact (not an error); a run holding no lease has an empty
|
|
558
|
+
`region`; the resume verdict is null while the run is live.
|
|
559
|
+
|
|
560
|
+
Args:
|
|
561
|
+
run_id: the run-id (RID-…) the digest is keyed on.
|
|
562
|
+
start_sha: the run's start commit (commits since = the liveness forward
|
|
563
|
+
delta). Default: the run's declared start_sha off its intent ledger,
|
|
564
|
+
else empty (a conservative 0-commit floor).
|
|
565
|
+
lane / loop_ts: this run's lease identity; together they scope the
|
|
566
|
+
liveness journal rungs to THIS lease. Omit ⇒ the commit rung decides.
|
|
567
|
+
stopped / live: override the automatic stopped-predicate (which is
|
|
568
|
+
`ledger SUSPENDed OR liveness STALLED`). `stopped` forces the resume
|
|
569
|
+
read; `live` skips it. The resume read runs the expensive ancestry
|
|
570
|
+
re-adjudication, so it is gated — never run on a live run.
|
|
571
|
+
now_ms: wall-clock epoch-ms (default: now). Injectable for determinism.
|
|
572
|
+
workspace: the repo root the run lives under (default: cwd).
|
|
573
|
+
|
|
574
|
+
Returns the digest dict {schema, run_id, liveness, progress, region, resume}
|
|
575
|
+
— and deliberately NO `claimed` key (the fail-closed A2A contract). On a bad
|
|
576
|
+
run-id, returns an {error, run_id} dict rather than raising.
|
|
577
|
+
"""
|
|
578
|
+
import time
|
|
579
|
+
from dos import (git_delta, intent_ledger, journal_delta, lane_journal,
|
|
580
|
+
liveness as _lvn, resume as _resume, resume_evidence,
|
|
581
|
+
run_id as _rid, status as _status)
|
|
582
|
+
|
|
583
|
+
cfg = _load_workspace_config(workspace)
|
|
584
|
+
started_ms = _rid.ts_ms_of(run_id)
|
|
585
|
+
if started_ms is None:
|
|
586
|
+
return {"error": f"{run_id!r} is not a valid run-id token "
|
|
587
|
+
f"(expected an RID-… minted by `dos run-id mint`)",
|
|
588
|
+
"run_id": run_id}
|
|
589
|
+
now = now_ms if now_ms is not None else int(time.time() * 1000)
|
|
590
|
+
|
|
591
|
+
# Read A — the intent ledger (first: it sources start_sha + the stopped
|
|
592
|
+
# predicate). Fail-closed: no ledger → a zero LedgerState, never a raise.
|
|
593
|
+
entries = intent_ledger.read_all(run_id, cfg=cfg)
|
|
594
|
+
ledger_state = (intent_ledger.replay(entries) if entries
|
|
595
|
+
else intent_ledger.LedgerState(run_id=run_id))
|
|
596
|
+
resolved_start = (start_sha or "").strip() or ledger_state.start_sha
|
|
597
|
+
|
|
598
|
+
# Read B — liveness. The clock/git/journal reads happen HERE at the boundary
|
|
599
|
+
# (the explicit-cfg discipline: every read takes cfg / root=cfg.paths.root),
|
|
600
|
+
# then the PURE classifier folds them.
|
|
601
|
+
commits = git_delta.count_commits_since(resolved_start, root=cfg.paths.root)
|
|
602
|
+
lease_key = (str(loop_ts), str(lane)) if lane and loop_ts else None
|
|
603
|
+
try:
|
|
604
|
+
j_entries = lane_journal.read_all(path=cfg.paths.lane_journal)
|
|
605
|
+
except Exception: # noqa: BLE001 — a bad journal must not crash the verdict
|
|
606
|
+
j_entries = []
|
|
607
|
+
jd = journal_delta.fold_since(j_entries, run_started_ms=started_ms,
|
|
608
|
+
now_ms=now, lease_key=lease_key)
|
|
609
|
+
liveness_verdict = _lvn.classify(_lvn.ProgressEvidence(
|
|
610
|
+
run_started_ms=started_ms, now_ms=now,
|
|
611
|
+
commits_since_start=commits,
|
|
612
|
+
journal_events_since=jd.events_since_start,
|
|
613
|
+
last_heartbeat_age_ms=jd.newest_heartbeat_age_ms,
|
|
614
|
+
))
|
|
615
|
+
|
|
616
|
+
# Read C — the held-lease region (the spine join: lease.run_id == run_id),
|
|
617
|
+
# reusing the already-read journal entries. `.get()` so an old un-stamped
|
|
618
|
+
# ACQUIRE simply doesn't match (region () — backward-compat, never a raise).
|
|
619
|
+
live_region: tuple[str, ...] = ()
|
|
620
|
+
for lease in lane_journal.replay(j_entries):
|
|
621
|
+
if str(lease.get("run_id") or "") == run_id:
|
|
622
|
+
tree = lease.get("tree")
|
|
623
|
+
live_region = (tuple(str(g) for g in tree)
|
|
624
|
+
if isinstance(tree, (list, tuple)) else ())
|
|
625
|
+
break
|
|
626
|
+
|
|
627
|
+
# Read D — resume, CONDITIONAL on the stopped predicate (skips the expensive
|
|
628
|
+
# ancestry re-adjudication on a live run). stopped/live override the auto rule.
|
|
629
|
+
is_stopped = bool(ledger_state.suspended
|
|
630
|
+
or liveness_verdict.verdict is _lvn.Liveness.STALLED)
|
|
631
|
+
if stopped:
|
|
632
|
+
is_stopped = True
|
|
633
|
+
if live:
|
|
634
|
+
is_stopped = False
|
|
635
|
+
resume_plan = None
|
|
636
|
+
if is_stopped and ledger_state.has_intent:
|
|
637
|
+
anc = resume_evidence.gather_ancestry(ledger_state, cfg=cfg)
|
|
638
|
+
resume_plan = _resume.resume_plan(ledger_state, anc)
|
|
639
|
+
|
|
640
|
+
digest = _status.status_digest(
|
|
641
|
+
run_id=run_id, ledger_state=ledger_state,
|
|
642
|
+
liveness_verdict=liveness_verdict,
|
|
643
|
+
live_region=live_region, resume_plan=resume_plan,
|
|
644
|
+
)
|
|
645
|
+
return digest.to_dict() # the same no-`claimed` shape as the CLI --json
|
|
646
|
+
|
|
647
|
+
# -----------------------------------------------------------------------
|
|
648
|
+
# Resources — browsable context, not just callable tools. A host (and the
|
|
649
|
+
# user) can READ these to load the workspace's refusal vocabulary and lane
|
|
650
|
+
# taxonomy as context, e.g. before deciding how to refuse or which lane to
|
|
651
|
+
# take. URIs are addressable; the `{workspace}`-templated variants let a host
|
|
652
|
+
# browse a specific repo. Read-only, like the tools they mirror.
|
|
653
|
+
# -----------------------------------------------------------------------
|
|
654
|
+
def _reasons_markdown(workspace: str) -> str:
|
|
655
|
+
cfg = _load_workspace_config(workspace)
|
|
656
|
+
lines = [f"# DOS refusal vocabulary — {cfg.paths.root}",
|
|
657
|
+
"",
|
|
658
|
+
"The closed set of reasons a blocked/no-pick verdict may carry. "
|
|
659
|
+
"Emit only a token listed here (it is simultaneously emittable, "
|
|
660
|
+
"verifiable, and refusable).", ""]
|
|
661
|
+
for s in cfg.reasons.specs:
|
|
662
|
+
lines.append(f"## `{s.key}` ({s.category}"
|
|
663
|
+
+ ("" if s.refusal else ", advisory-only") + ")")
|
|
664
|
+
if s.summary:
|
|
665
|
+
lines.append(s.summary)
|
|
666
|
+
if s.fix:
|
|
667
|
+
lines.append(f"- **fix:** {s.fix}")
|
|
668
|
+
lines.append("")
|
|
669
|
+
return "\n".join(lines)
|
|
670
|
+
|
|
671
|
+
def _lanes_markdown(workspace: str) -> str:
|
|
672
|
+
cfg = _load_workspace_config(workspace)
|
|
673
|
+
lanes = cfg.lanes
|
|
674
|
+
lines = [f"# DOS lane taxonomy — {cfg.paths.root}",
|
|
675
|
+
"",
|
|
676
|
+
"Concurrent lanes run in parallel iff their file trees are "
|
|
677
|
+
"disjoint; exclusive lanes run alone. `dos_arbitrate` decides "
|
|
678
|
+
"admission over these.", "",
|
|
679
|
+
f"- **concurrent:** {', '.join(lanes.concurrent) or '(none)'}",
|
|
680
|
+
f"- **exclusive:** {', '.join(lanes.exclusive) or '(none)'}",
|
|
681
|
+
f"- **autopick order:** {', '.join(lanes.autopick) or '(none)'}",
|
|
682
|
+
"", "## Trees", ""]
|
|
683
|
+
for name in sorted(set(lanes.concurrent) | set(lanes.exclusive)
|
|
684
|
+
| set(lanes.trees)):
|
|
685
|
+
tree = ", ".join(lanes.tree_for(name)) or "(no tree declared)"
|
|
686
|
+
lines.append(f"- `{name}`: {tree}")
|
|
687
|
+
return "\n".join(lines)
|
|
688
|
+
|
|
689
|
+
@mcp.resource("dos://reasons", mime_type="text/markdown")
|
|
690
|
+
def reasons_resource() -> str:
|
|
691
|
+
"""The refusal vocabulary for the server's default workspace (cwd)."""
|
|
692
|
+
return _reasons_markdown(".")
|
|
693
|
+
|
|
694
|
+
@mcp.resource("dos://reasons/{workspace}", mime_type="text/markdown")
|
|
695
|
+
def reasons_resource_ws(workspace: str) -> str:
|
|
696
|
+
"""The refusal vocabulary for a specific workspace path.
|
|
697
|
+
|
|
698
|
+
The `{workspace}` URI segment is a single path segment (FastMCP matches
|
|
699
|
+
it as `[^/]+`), so a workspace path is carried percent-encoded — an
|
|
700
|
+
absolute POSIX root like `/srv/ws` would otherwise inject a bare slash
|
|
701
|
+
and make the URI unroutable (the Windows path `C:\\ws` has none, which is
|
|
702
|
+
why this only bit Linux). Decode it back to the real path here.
|
|
703
|
+
"""
|
|
704
|
+
return _reasons_markdown(unquote(workspace))
|
|
705
|
+
|
|
706
|
+
@mcp.resource("dos://lanes", mime_type="text/markdown")
|
|
707
|
+
def lanes_resource() -> str:
|
|
708
|
+
"""The lane taxonomy for the server's default workspace (cwd)."""
|
|
709
|
+
return _lanes_markdown(".")
|
|
710
|
+
|
|
711
|
+
@mcp.resource("dos://lanes/{workspace}", mime_type="text/markdown")
|
|
712
|
+
def lanes_resource_ws(workspace: str) -> str:
|
|
713
|
+
"""The lane taxonomy for a specific workspace path.
|
|
714
|
+
|
|
715
|
+
See `reasons_resource_ws` — the `{workspace}` segment is percent-encoded
|
|
716
|
+
so an absolute path survives FastMCP's `[^/]+` segment match; decode it.
|
|
717
|
+
"""
|
|
718
|
+
return _lanes_markdown(unquote(workspace))
|
|
719
|
+
|
|
720
|
+
# -----------------------------------------------------------------------
|
|
721
|
+
# Prompts — user-invokable entry points. These surface in the host UI (e.g.
|
|
722
|
+
# as slash-commands in Claude Desktop) so a USER can drive DOS directly,
|
|
723
|
+
# without knowing the tool names. Each returns a short instruction that
|
|
724
|
+
# teaches the agent the right tool + sequence — the "use it directly with
|
|
725
|
+
# Claude" path the README describes.
|
|
726
|
+
# -----------------------------------------------------------------------
|
|
727
|
+
@mcp.prompt(title="Verify a claim actually shipped")
|
|
728
|
+
def verify_a_claim(plan: str, phase: str, workspace: str = ".") -> str:
|
|
729
|
+
"""Confirm a (plan, phase) really shipped, from evidence not self-report."""
|
|
730
|
+
return (
|
|
731
|
+
f"Use the `dos_verify` tool with plan={plan!r}, phase={phase!r}, "
|
|
732
|
+
f"workspace={workspace!r}. Then tell me plainly whether it shipped, "
|
|
733
|
+
f"citing the `source` (registry / git commit / no evidence) and the "
|
|
734
|
+
f"sha if there is one. Do NOT take anyone's word that it shipped — "
|
|
735
|
+
f"rely only on what `dos_verify` returns."
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
@mcp.prompt(title="Can I safely take this lane?")
|
|
739
|
+
def can_i_take_this_lane(lane: str, tree: str = "",
|
|
740
|
+
workspace: str = ".") -> str:
|
|
741
|
+
"""Check whether starting work on a lane/file-tree collides with live work."""
|
|
742
|
+
tree_note = (f" Its file tree is: {tree} (pass as the `tree` argument, "
|
|
743
|
+
f"split into a list of globs).") if tree else ""
|
|
744
|
+
return (
|
|
745
|
+
f"Use the `dos_arbitrate` tool to decide whether I may take lane "
|
|
746
|
+
f"{lane!r} in workspace {workspace!r} right now.{tree_note} If you "
|
|
747
|
+
f"know what leases are currently live, pass them as `live_leases`. "
|
|
748
|
+
f"Then give me a clear GO or STOP, and if STOP, list any free lanes I "
|
|
749
|
+
f"could take instead."
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
@mcp.prompt(title="Refuse with a structured reason")
|
|
753
|
+
def refuse_with_a_reason(situation: str, workspace: str = ".") -> str:
|
|
754
|
+
"""Pick a verifiable refusal reason for a situation, instead of free text."""
|
|
755
|
+
return (
|
|
756
|
+
f"I need to refuse / report-blocked for this situation: {situation}\n\n"
|
|
757
|
+
f"First call `dos_refuse_reasons` (workspace={workspace!r}) to see the "
|
|
758
|
+
f"closed vocabulary. Pick the single token that best fits, confirm it "
|
|
759
|
+
f"with `dos_check_reason`, then refuse using THAT token (not free-text "
|
|
760
|
+
f"prose). If nothing fits, say so and suggest a new reason to declare "
|
|
761
|
+
f"in dos.toml [reasons]."
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
return mcp
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def main(argv: list[str] | None = None) -> int:
|
|
768
|
+
"""Console-script entrypoint — build the server and serve over stdio.
|
|
769
|
+
|
|
770
|
+
`argv` is accepted for symmetry with the `dos` CLI and to keep the signature
|
|
771
|
+
test-friendly; the stdio transport takes no arguments today.
|
|
772
|
+
"""
|
|
773
|
+
server = build_server()
|
|
774
|
+
server.run() # stdio transport — what an MCP host launches and speaks to
|
|
775
|
+
return 0
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
if __name__ == "__main__":
|
|
779
|
+
raise SystemExit(main())
|