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