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/gh4_coverage.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""GH4 commit-coverage predicates — pure ship-stamp / claim-footprint matching.
|
|
2
|
+
|
|
3
|
+
Lifted from the job userland's ``scripts/fanout_state.py`` (MQ3X P1, docs/62).
|
|
4
|
+
These three predicates answer two distrust questions the GH4 post-commit
|
|
5
|
+
auto-stamp asks before believing "this commit shipped (plan, phase)":
|
|
6
|
+
|
|
7
|
+
* ``claim_covered`` — does any committed path fall inside the claim's
|
|
8
|
+
declared footprint? (explicit glob / explicit
|
|
9
|
+
files / fanout-archive bundle / plan-doc edit)
|
|
10
|
+
* ``coverage_is_plandoc_only`` — was the claim covered ONLY by a plan-doc edit
|
|
11
|
+
(branch 3b) and none of the stronger footprints?
|
|
12
|
+
The surface of the CRS3 false-stamp (2026-06-02):
|
|
13
|
+
a sibling phase's ship edits the SHARED plan doc
|
|
14
|
+
and would auto-stamp a phase whose deliverable
|
|
15
|
+
was never touched → the picker drops a live phase
|
|
16
|
+
→ the lane wedges.
|
|
17
|
+
* ``subject_matches_stamp`` — does the commit SUBJECT look like the expected
|
|
18
|
+
ship-stamp for this plan?
|
|
19
|
+
|
|
20
|
+
This module is a ``dos`` Layer-1 leaf in the ``scope`` / ``sibling_scan`` mold:
|
|
21
|
+
**pure** ``(entry, committed_paths, subject)`` → ``bool``, zero fs / git / clock /
|
|
22
|
+
config. All I/O — running ``git show --name-only``, resolving the plan-doc path,
|
|
23
|
+
reading ``STATE_PATH`` — happens in the CALLER (the job adapter's
|
|
24
|
+
``_gh4_scan_claims_after_commit``), which gathers the evidence then calls these.
|
|
25
|
+
That is what lets the whole predicate family be replay-tested on frozen fixtures.
|
|
26
|
+
|
|
27
|
+
Two impure GH4 siblings (``subject_is_prelaunch_staging_only``,
|
|
28
|
+
``plandoc_only_lacks_deliverable``) deliberately stay job-side: they transitively
|
|
29
|
+
reach ``STATE_PATH`` via ``_gh4_claim_plan_files → _resolve_phase_plan_files →
|
|
30
|
+
_resolve_plan_doc_path`` (the boundary litmus in docs/62 §0). Only these three
|
|
31
|
+
leaf-pure predicates lift.
|
|
32
|
+
|
|
33
|
+
Function names drop the ``_gh4_`` prefix (the module namespace carries it); the
|
|
34
|
+
job shim re-exports them under their old ``_gh4_*`` names so every existing
|
|
35
|
+
caller resolves unchanged.
|
|
36
|
+
"""
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import fnmatch
|
|
40
|
+
import re
|
|
41
|
+
|
|
42
|
+
# A ``dispatched_by: fanout-<TS>`` tag — the claim was dispatched by a fanout run
|
|
43
|
+
# whose archive bundle lives under ``docs/_fanout_runs/<TS>/``.
|
|
44
|
+
FANOUT_TS_RE = re.compile(r"^fanout-(\d{8}T\d{6}Z)$", re.IGNORECASE)
|
|
45
|
+
|
|
46
|
+
# Commit-subject shapes accepted as a generic ship-stamp (matched against the
|
|
47
|
+
# SUBJECT line only). Permissive on purpose — a false-positive auto-transition is
|
|
48
|
+
# safer than over-routing to the warn queue (a mistaken transition surfaces as a
|
|
49
|
+
# stamp-drift row on the next pre-screen; over-routing buries real signal under
|
|
50
|
+
# noise). The plan-token check is tightened with the actual plan id at match time.
|
|
51
|
+
STAMP_PATTERNS_GENERIC = (
|
|
52
|
+
re.compile(r"^chore\(working-tree\)", re.IGNORECASE),
|
|
53
|
+
re.compile(r"^docs/fanout:", re.IGNORECASE),
|
|
54
|
+
re.compile(r"^docs/dispatch:", re.IGNORECASE),
|
|
55
|
+
re.compile(r"^docs/_fanout_runs", re.IGNORECASE),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def claim_covered(entry: dict, committed: list[str],
|
|
60
|
+
plan_doc_path: str | None) -> bool:
|
|
61
|
+
"""Return True iff any committed path is inside the claim's footprint.
|
|
62
|
+
|
|
63
|
+
Resolution order:
|
|
64
|
+
1. Explicit ``path_glob`` on the entry (str): fnmatch any committed path.
|
|
65
|
+
2. Explicit ``files`` on the entry (list[str]): exact match or prefix match
|
|
66
|
+
(treat a trailing ``/`` as a directory prefix).
|
|
67
|
+
3. Fallback (the common case — schema rows today carry NEITHER):
|
|
68
|
+
a) ``dispatched_by: fanout-<TS>`` AND any committed path is under
|
|
69
|
+
``docs/_fanout_runs/<TS>/`` (archive-bundled ship).
|
|
70
|
+
b) ``plan_doc_path`` resolves and any committed path equals it (a
|
|
71
|
+
plan-doc edit covering this plan).
|
|
72
|
+
"""
|
|
73
|
+
if not committed:
|
|
74
|
+
return False
|
|
75
|
+
norm_committed = [p.replace("\\", "/") for p in committed if p]
|
|
76
|
+
|
|
77
|
+
# 1) explicit path_glob
|
|
78
|
+
glob = entry.get("path_glob")
|
|
79
|
+
if isinstance(glob, str) and glob.strip():
|
|
80
|
+
pat = glob.strip().replace("\\", "/")
|
|
81
|
+
for p in norm_committed:
|
|
82
|
+
if fnmatch.fnmatch(p, pat):
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
# 2) explicit files
|
|
86
|
+
files = entry.get("files")
|
|
87
|
+
if isinstance(files, list) and files:
|
|
88
|
+
explicit = {str(f).replace("\\", "/") for f in files if f}
|
|
89
|
+
for p in norm_committed:
|
|
90
|
+
if p in explicit:
|
|
91
|
+
return True
|
|
92
|
+
for f in explicit:
|
|
93
|
+
if f.endswith("/") and p.startswith(f):
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
# 3a) fanout-archive bundled commit
|
|
97
|
+
by = (entry.get("dispatched_by") or "").strip()
|
|
98
|
+
m = FANOUT_TS_RE.match(by)
|
|
99
|
+
if m:
|
|
100
|
+
ts = m.group(1)
|
|
101
|
+
prefix = f"docs/_fanout_runs/{ts}/"
|
|
102
|
+
for p in norm_committed:
|
|
103
|
+
if p.startswith(prefix):
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
# 3b) plan-doc covered
|
|
107
|
+
if plan_doc_path:
|
|
108
|
+
pdp = plan_doc_path.replace("\\", "/")
|
|
109
|
+
for p in norm_committed:
|
|
110
|
+
if p == pdp:
|
|
111
|
+
return True
|
|
112
|
+
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def coverage_is_plandoc_only(
|
|
117
|
+
entry: dict, committed: list[str], plan_doc_path: str | None,
|
|
118
|
+
) -> bool:
|
|
119
|
+
"""True iff ``claim_covered`` would pass ONLY via branch 3b (a plan-doc edit)
|
|
120
|
+
— i.e. the commit touched the plan doc but matched NONE of the stronger
|
|
121
|
+
footprints (explicit ``path_glob`` / explicit ``files`` / fanout-archive
|
|
122
|
+
bundle 3a).
|
|
123
|
+
|
|
124
|
+
This is the surface of the CRS3 false-stamp (2026-06-02): a real ship commit
|
|
125
|
+
(``CRS2: …``) edits the SHARED plan doc that holds both CRS2 and CRS3 meta, so
|
|
126
|
+
3b covers the CRS3 claim even though no CRS3 deliverable was touched; combined
|
|
127
|
+
with the permissive subject token-match (``\\bCRS3\\b`` in the body) it
|
|
128
|
+
auto-stamps CRS3 shipped → ship_oracle reads registry→SHIPPED → the picker
|
|
129
|
+
drops a phase whose deliverable does not exist → every dispatch on that lane
|
|
130
|
+
WEDGEs. Caller uses this to demand a *deliverable* overlap before stamping such
|
|
131
|
+
a coverage. Best-effort; pure.
|
|
132
|
+
"""
|
|
133
|
+
if not committed or not plan_doc_path:
|
|
134
|
+
return False
|
|
135
|
+
norm = [p.replace("\\", "/") for p in committed if p]
|
|
136
|
+
pdp = plan_doc_path.replace("\\", "/")
|
|
137
|
+
if pdp not in norm:
|
|
138
|
+
return False # not covered via 3b at all
|
|
139
|
+
# Stronger footprints — if ANY of these match, coverage is NOT plan-doc-only.
|
|
140
|
+
glob = entry.get("path_glob")
|
|
141
|
+
if isinstance(glob, str) and glob.strip():
|
|
142
|
+
pat = glob.strip().replace("\\", "/")
|
|
143
|
+
if any(fnmatch.fnmatch(p, pat) for p in norm):
|
|
144
|
+
return False
|
|
145
|
+
files = entry.get("files")
|
|
146
|
+
if isinstance(files, list) and files:
|
|
147
|
+
explicit = {str(f).replace("\\", "/") for f in files if f}
|
|
148
|
+
for p in norm:
|
|
149
|
+
if p in explicit or any(
|
|
150
|
+
f.endswith("/") and p.startswith(f) for f in explicit):
|
|
151
|
+
return False
|
|
152
|
+
by = (entry.get("dispatched_by") or "").strip()
|
|
153
|
+
m = FANOUT_TS_RE.match(by)
|
|
154
|
+
if m and any(p.startswith(f"docs/_fanout_runs/{m.group(1)}/") for p in norm):
|
|
155
|
+
return False
|
|
156
|
+
# Covered, and the ONLY thing that covered it was the plan-doc edit.
|
|
157
|
+
return True
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def subject_matches_stamp(subject: str, plan: str) -> bool:
|
|
161
|
+
"""Return True iff the commit ``subject`` looks like the expected ship-stamp
|
|
162
|
+
for a claim against ``plan``. Permissive — see ``STAMP_PATTERNS_GENERIC``."""
|
|
163
|
+
if not subject:
|
|
164
|
+
return False
|
|
165
|
+
s = subject.strip().splitlines()[0] if subject else ""
|
|
166
|
+
plan_lc = (plan or "").strip().lower()
|
|
167
|
+
if plan_lc:
|
|
168
|
+
# `<plan>:` / `docs/<plan>:` / `<plan>/` / `docs/<plan>-plan` etc.
|
|
169
|
+
if re.match(rf"^(?:docs/)?{re.escape(plan_lc)}(?:-plan)?[:/]", s,
|
|
170
|
+
re.IGNORECASE):
|
|
171
|
+
return True
|
|
172
|
+
# Plan id may appear anywhere in the subject as a token (e.g.
|
|
173
|
+
# `docs/git-hygiene: GH4 — ...` for a GH4 claim).
|
|
174
|
+
if re.search(rf"\b{re.escape(plan_lc)}\d*\b", s, re.IGNORECASE):
|
|
175
|
+
return True
|
|
176
|
+
for pat in STAMP_PATTERNS_GENERIC:
|
|
177
|
+
if pat.match(s):
|
|
178
|
+
return True
|
|
179
|
+
return False
|
dos/git_delta.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""git-delta — the "commits since a start SHA" evidence, one shared reader.
|
|
2
|
+
|
|
3
|
+
`git log <start-sha>..HEAD` is the authoritative *forward-progress delta* for a
|
|
4
|
+
run: how many commits landed on the served workspace since the run began. Two
|
|
5
|
+
callers need exactly this fold and must not drift from each other:
|
|
6
|
+
|
|
7
|
+
* `dos.timeline` — Stage 6 of the dispatch handoff view ("N commits since
|
|
8
|
+
start").
|
|
9
|
+
* `dos.liveness` (via the `dos liveness` CLI's evidence-gather) — the git rung
|
|
10
|
+
of the temporal verdict: ≥1 commit since start is the `ADVANCING` floor
|
|
11
|
+
(docs/82, LVN Phase 1b).
|
|
12
|
+
|
|
13
|
+
This module is the single home for that read so LVN does not re-implement
|
|
14
|
+
`timeline`'s git rung (the LVN-1b directive). It is **boundary I/O**, not a pure
|
|
15
|
+
verdict: like `pick_oracle`'s gather and `verify`'s git reads, the subprocess
|
|
16
|
+
happens HERE, at the caller boundary, and the already-counted delta is handed to
|
|
17
|
+
the pure classifier. `dos.liveness.classify` itself never calls this — it takes
|
|
18
|
+
`commits_since_start: int` as already-gathered evidence (the arbiter discipline).
|
|
19
|
+
|
|
20
|
+
The repo root is passed in EXPLICITLY (never read from the process-global active
|
|
21
|
+
config), so a long-lived caller fielding several workspaces — the MCP server, a
|
|
22
|
+
fleet daemon — gets the right tree without mutating global state. Every failure
|
|
23
|
+
mode (no SHA, non-git dir, git missing, timeout, non-zero exit) degrades to an
|
|
24
|
+
empty list: a liveness verdict in a repo with no git history is `0 commits`, the
|
|
25
|
+
honest floor, never a crash (the no-plan / fail-safe discipline).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import subprocess
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
# Cap the git call so a pathological repo can't hang an evidence-gather. Matches
|
|
34
|
+
# the 10s bound `timeline._git_log` has always used.
|
|
35
|
+
_GIT_TIMEOUT_S = 10
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def commits_since(start_sha: str, *, root: Path | str) -> list[dict[str, str]]:
|
|
39
|
+
"""`git log <start_sha>..HEAD` over ``root``, as ``[{sha, subject}, …]``.
|
|
40
|
+
|
|
41
|
+
Newest-first (git's default order). Returns ``[]`` for any of: an empty
|
|
42
|
+
``start_sha`` (a run with no recorded start commit), a non-git ``root``, a
|
|
43
|
+
missing ``git`` binary, a timeout, or a non-zero git exit (e.g. an unknown
|
|
44
|
+
SHA). The empty list is the safe degrade — a caller reads it as "no forward
|
|
45
|
+
delta observed," never as an error to propagate.
|
|
46
|
+
"""
|
|
47
|
+
if not start_sha:
|
|
48
|
+
return []
|
|
49
|
+
try:
|
|
50
|
+
raw = subprocess.run(
|
|
51
|
+
["git", "log", f"{start_sha}..HEAD", "--pretty=format:%h%x09%s"],
|
|
52
|
+
cwd=str(root),
|
|
53
|
+
capture_output=True,
|
|
54
|
+
text=True,
|
|
55
|
+
check=False,
|
|
56
|
+
timeout=_GIT_TIMEOUT_S,
|
|
57
|
+
)
|
|
58
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
59
|
+
return []
|
|
60
|
+
if raw.returncode != 0:
|
|
61
|
+
return []
|
|
62
|
+
out: list[dict[str, str]] = []
|
|
63
|
+
for line in raw.stdout.splitlines():
|
|
64
|
+
if not line.strip():
|
|
65
|
+
continue
|
|
66
|
+
parts = line.split("\t", 1)
|
|
67
|
+
if len(parts) != 2:
|
|
68
|
+
continue
|
|
69
|
+
out.append({"sha": parts[0], "subject": parts[1]})
|
|
70
|
+
return out
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def count_commits_since(start_sha: str, *, root: Path | str) -> int:
|
|
74
|
+
"""Just the count — the single number `dos.liveness`'s git rung needs.
|
|
75
|
+
|
|
76
|
+
A thin fold over `commits_since` so the LVN evidence-gather reads one int
|
|
77
|
+
without materialising the subject list it does not use.
|
|
78
|
+
"""
|
|
79
|
+
return len(commits_since(start_sha, root=root))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def recent_commits(n: int = 10, *, root: Path | str) -> list[dict[str, str]]:
|
|
83
|
+
"""The last ``n`` commits on ``root``, newest-first, as ``[{sha, subject}, …]``.
|
|
84
|
+
|
|
85
|
+
The *unanchored* sibling of `commits_since`: where that answers "what landed
|
|
86
|
+
since a run's start SHA," this answers "what has landed lately, period" — the
|
|
87
|
+
one git read `dos top` needs to show real movement in a repo with **no
|
|
88
|
+
leases and no plan at all** (a freshly-`dos init`'d checkout). Kept here so
|
|
89
|
+
the kernel's git-evidence reads stay in one home rather than `dispatch_top`
|
|
90
|
+
spawning its own subprocess.
|
|
91
|
+
|
|
92
|
+
Same fail-safe contract as `commits_since`: returns ``[]`` for a non-positive
|
|
93
|
+
``n``, a non-git ``root``, a missing ``git`` binary, a timeout, or a non-zero
|
|
94
|
+
git exit (e.g. a repo with zero commits — `git log` exits non-zero on an
|
|
95
|
+
unborn HEAD). The empty list is the honest floor — "no history observed,"
|
|
96
|
+
never an error to propagate. ``root`` is explicit (never the process-global
|
|
97
|
+
active config), the long-lived-caller discipline `commits_since` set.
|
|
98
|
+
"""
|
|
99
|
+
if n <= 0:
|
|
100
|
+
return []
|
|
101
|
+
try:
|
|
102
|
+
raw = subprocess.run(
|
|
103
|
+
["git", "log", f"-{int(n)}", "--pretty=format:%h%x09%s"],
|
|
104
|
+
cwd=str(root),
|
|
105
|
+
capture_output=True,
|
|
106
|
+
text=True,
|
|
107
|
+
check=False,
|
|
108
|
+
timeout=_GIT_TIMEOUT_S,
|
|
109
|
+
)
|
|
110
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
111
|
+
return []
|
|
112
|
+
if raw.returncode != 0:
|
|
113
|
+
return []
|
|
114
|
+
out: list[dict[str, str]] = []
|
|
115
|
+
for line in raw.stdout.splitlines():
|
|
116
|
+
if not line.strip():
|
|
117
|
+
continue
|
|
118
|
+
parts = line.split("\t", 1)
|
|
119
|
+
if len(parts) != 2:
|
|
120
|
+
continue
|
|
121
|
+
out.append({"sha": parts[0], "subject": parts[1]})
|
|
122
|
+
return out
|
dos/guard.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""dos.guard — the headless-launch wrapper (the argv shim, docs/134 §4).
|
|
2
|
+
|
|
3
|
+
`dos guard [opts] -- <host-cmd> [host-args]` frames a non-interactive agent
|
|
4
|
+
launch (`claude -p …`, or any host taking the same flags) with the DOS wiring,
|
|
5
|
+
then execs the host command. It injects two things the host already honors:
|
|
6
|
+
|
|
7
|
+
* ``--mcp-config '<json>'`` — mounts the DOS MCP server (``dos-mcp``) so the
|
|
8
|
+
agent *can* call ``dos_verify`` / ``dos_arbitrate`` mid-run. (Works today —
|
|
9
|
+
``dos-mcp`` is a shipped console script.)
|
|
10
|
+
* ``--settings '<json>'`` — carries a ``Stop`` hook (the verify-on-stop
|
|
11
|
+
binding, docs/134 §2) and/or an ``--append-system-prompt`` instruction.
|
|
12
|
+
This is the ONLY way to add a hook to a headless run — there is no
|
|
13
|
+
``--hooks`` flag (verified against ``claude --help``).
|
|
14
|
+
|
|
15
|
+
The split is the whole contract: everything after ``--`` is the host command,
|
|
16
|
+
passed through byte-for-byte. DOS computes the two injected flags and appends
|
|
17
|
+
them; an unrecognized host flag is the host's problem, not ours (degrade to
|
|
18
|
+
passthrough). This module is a **layer-3 helper** — it names no host internals
|
|
19
|
+
beyond the two public flags, takes no lease, and computes a plan a test can
|
|
20
|
+
assert without ever launching a subprocess.
|
|
21
|
+
|
|
22
|
+
Design discipline (mirrors the kernel's "pure core, I/O at the boundary"):
|
|
23
|
+
``build_guard_plan`` is PURE — options in, a ``GuardPlan`` (the injected JSON +
|
|
24
|
+
the final argv) out, no environment read, no process spawned. The CLI verb does
|
|
25
|
+
the one impure thing (``os.execvp`` / ``subprocess``) over the plan the pure
|
|
26
|
+
function returned, and ``--print-config`` dumps the plan without launching.
|
|
27
|
+
|
|
28
|
+
This is a CONSUMER surface, the MCP server's sibling: it *frames* a host launch,
|
|
29
|
+
it does not get inside the host. The advisory-only boundary (docs/99) is intact —
|
|
30
|
+
nothing here forces a process; the dev typed ``dos guard``, and the host is what
|
|
31
|
+
honors the flags.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import json
|
|
37
|
+
import shlex
|
|
38
|
+
from dataclasses import dataclass, field
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# The console script that serves the DOS syscalls as MCP tools (pyproject
|
|
42
|
+
# [project.scripts]). The wrapper points the host's --mcp-config at it by name;
|
|
43
|
+
# resolution is the host's PATH lookup, same as any MCP stdio server.
|
|
44
|
+
DEFAULT_MCP_COMMAND = "dos-mcp"
|
|
45
|
+
|
|
46
|
+
# The default Stop-hook command — the docs/134 §2 verify-on-stop binding, now
|
|
47
|
+
# SHIPPED (`cmd_hook_stop`). The hook stays OPT-IN (`--verify-on-stop`), never
|
|
48
|
+
# injected by default, because it changes the launch's stop behavior — that is a
|
|
49
|
+
# decision the dev makes explicitly, not a silent default. (The MCP mount, which
|
|
50
|
+
# only ADDS a callable tool, is the safe default.)
|
|
51
|
+
DEFAULT_STOP_HOOK_COMMAND = "dos hook stop --workspace ."
|
|
52
|
+
|
|
53
|
+
# The one instruction that makes the docs/134 §2.1 explicit-marker rung reliable:
|
|
54
|
+
# the agent declares what to verify in a form the claim extractor lifts exactly.
|
|
55
|
+
DEFAULT_CLAIM_PROMPT = (
|
|
56
|
+
"When you complete a unit of work, end your turn with a line of the form "
|
|
57
|
+
"`DOS-CLAIM: <plan> <phase>` naming the plan and phase you claim shipped, so "
|
|
58
|
+
"it can be verified against git."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class GuardPlan:
|
|
64
|
+
"""The pure result of framing a launch: what to inject and the final argv.
|
|
65
|
+
|
|
66
|
+
``argv`` is the complete command to exec (host command + appended DOS flags).
|
|
67
|
+
``mcp_config`` / ``settings`` are the JSON objects injected (kept separately
|
|
68
|
+
so ``--print-config`` can show them legibly and tests can assert on them).
|
|
69
|
+
``notes`` carries any honesty caveats surfaced to the user (e.g. the Stop
|
|
70
|
+
hook targeting a not-yet-built verb).
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
argv: list[str]
|
|
74
|
+
mcp_config: dict | None
|
|
75
|
+
settings: dict | None
|
|
76
|
+
host_command: list[str]
|
|
77
|
+
notes: list[str] = field(default_factory=list)
|
|
78
|
+
|
|
79
|
+
def to_dict(self) -> dict:
|
|
80
|
+
return {
|
|
81
|
+
"argv": self.argv,
|
|
82
|
+
"mcp_config": self.mcp_config,
|
|
83
|
+
"settings": self.settings,
|
|
84
|
+
"host_command": self.host_command,
|
|
85
|
+
"notes": self.notes,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def build_settings(
|
|
90
|
+
*,
|
|
91
|
+
verify_on_stop: bool,
|
|
92
|
+
stop_hook_command: str = DEFAULT_STOP_HOOK_COMMAND,
|
|
93
|
+
append_prompt: str | None = None,
|
|
94
|
+
) -> dict | None:
|
|
95
|
+
"""Build the ``--settings`` JSON object, or None if nothing to inject.
|
|
96
|
+
|
|
97
|
+
Pure. The Stop hook is only present when ``verify_on_stop`` is True — see the
|
|
98
|
+
DEFAULT_STOP_HOOK_COMMAND note on why it is opt-in.
|
|
99
|
+
"""
|
|
100
|
+
settings: dict = {}
|
|
101
|
+
if verify_on_stop:
|
|
102
|
+
# The host's settings.json hook shape (verified): an event → a list of
|
|
103
|
+
# matcher-groups, each carrying a list of {type, command} hooks.
|
|
104
|
+
settings["hooks"] = {
|
|
105
|
+
"Stop": [
|
|
106
|
+
{"hooks": [{"type": "command", "command": stop_hook_command}]}
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
if append_prompt:
|
|
110
|
+
# Carried in settings too — the host merges an appendSystemPrompt setting
|
|
111
|
+
# the same way the CLI --append-system-prompt flag does. (We pass it via
|
|
112
|
+
# settings rather than a second flag so the whole injection is two flags
|
|
113
|
+
# max, and so --print-config shows one legible object.)
|
|
114
|
+
settings["appendSystemPrompt"] = append_prompt
|
|
115
|
+
return settings or None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def build_mcp_config(
|
|
119
|
+
*, mount_mcp: bool, mcp_command: str = DEFAULT_MCP_COMMAND
|
|
120
|
+
) -> dict | None:
|
|
121
|
+
"""Build the ``--mcp-config`` JSON object, or None if not mounting.
|
|
122
|
+
|
|
123
|
+
Pure. The DOS server is mounted under the key ``dos`` so the agent's tools
|
|
124
|
+
are ``mcp__dos__verify`` etc.
|
|
125
|
+
"""
|
|
126
|
+
if not mount_mcp:
|
|
127
|
+
return None
|
|
128
|
+
return {"mcpServers": {"dos": {"command": mcp_command}}}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def build_guard_plan(
|
|
132
|
+
host_command: list[str],
|
|
133
|
+
*,
|
|
134
|
+
mount_mcp: bool = True,
|
|
135
|
+
verify_on_stop: bool = False,
|
|
136
|
+
add_claim_prompt: bool = False,
|
|
137
|
+
strict_mcp: bool = False,
|
|
138
|
+
stop_hook_command: str = DEFAULT_STOP_HOOK_COMMAND,
|
|
139
|
+
mcp_command: str = DEFAULT_MCP_COMMAND,
|
|
140
|
+
) -> GuardPlan:
|
|
141
|
+
"""Frame a host launch with the DOS wiring. PURE — no I/O, no subprocess.
|
|
142
|
+
|
|
143
|
+
Returns the complete ``argv`` to exec plus the injected objects. Raises
|
|
144
|
+
``ValueError`` only on an empty host command (the one contract error).
|
|
145
|
+
"""
|
|
146
|
+
# argparse.REMAINDER keeps a literal leading `--` separator in the captured
|
|
147
|
+
# list; strip one so callers may pass the host command with or without it.
|
|
148
|
+
if host_command and host_command[0] == "--":
|
|
149
|
+
host_command = host_command[1:]
|
|
150
|
+
|
|
151
|
+
if not host_command:
|
|
152
|
+
raise ValueError(
|
|
153
|
+
"dos guard needs a host command after `--` "
|
|
154
|
+
"(e.g. `dos guard -- claude -p \"...\"`)."
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
notes: list[str] = []
|
|
158
|
+
|
|
159
|
+
append_prompt = DEFAULT_CLAIM_PROMPT if add_claim_prompt else None
|
|
160
|
+
mcp_config = build_mcp_config(mount_mcp=mount_mcp, mcp_command=mcp_command)
|
|
161
|
+
settings = build_settings(
|
|
162
|
+
verify_on_stop=verify_on_stop,
|
|
163
|
+
stop_hook_command=stop_hook_command,
|
|
164
|
+
append_prompt=append_prompt,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Build the final argv: host command, then the appended DOS flags. We append
|
|
168
|
+
# (never prepend) so the host's own flags parse first and ours are additive —
|
|
169
|
+
# and so a host that doesn't recognize a flag fails on OUR flag, legibly,
|
|
170
|
+
# rather than mangling the user's command.
|
|
171
|
+
argv = list(host_command)
|
|
172
|
+
if mcp_config is not None:
|
|
173
|
+
argv += ["--mcp-config", json.dumps(mcp_config, sort_keys=True)]
|
|
174
|
+
if strict_mcp:
|
|
175
|
+
# Only use the servers we injected — the CI-honest form (verified flag).
|
|
176
|
+
argv += ["--strict-mcp-config"]
|
|
177
|
+
if settings is not None:
|
|
178
|
+
argv += ["--settings", json.dumps(settings, sort_keys=True)]
|
|
179
|
+
|
|
180
|
+
return GuardPlan(
|
|
181
|
+
argv=argv,
|
|
182
|
+
mcp_config=mcp_config,
|
|
183
|
+
settings=settings,
|
|
184
|
+
host_command=list(host_command),
|
|
185
|
+
notes=notes,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def render_plan_human(plan: GuardPlan) -> str:
|
|
190
|
+
"""A legible multi-line dump of what `dos guard` would inject and run."""
|
|
191
|
+
lines: list[str] = []
|
|
192
|
+
lines.append("dos guard — launch plan")
|
|
193
|
+
lines.append("")
|
|
194
|
+
lines.append("host command (passed through verbatim):")
|
|
195
|
+
lines.append(" " + " ".join(shlex.quote(a) for a in plan.host_command))
|
|
196
|
+
lines.append("")
|
|
197
|
+
if plan.mcp_config is not None:
|
|
198
|
+
lines.append("injected --mcp-config:")
|
|
199
|
+
lines.append(" " + json.dumps(plan.mcp_config, sort_keys=True))
|
|
200
|
+
else:
|
|
201
|
+
lines.append("injected --mcp-config: (none — MCP mount disabled)")
|
|
202
|
+
if plan.settings is not None:
|
|
203
|
+
lines.append("injected --settings:")
|
|
204
|
+
lines.append(" " + json.dumps(plan.settings, sort_keys=True))
|
|
205
|
+
else:
|
|
206
|
+
lines.append("injected --settings: (none)")
|
|
207
|
+
lines.append("")
|
|
208
|
+
lines.append("final argv to exec:")
|
|
209
|
+
lines.append(" " + " ".join(shlex.quote(a) for a in plan.argv))
|
|
210
|
+
if plan.notes:
|
|
211
|
+
lines.append("")
|
|
212
|
+
lines.append("notes:")
|
|
213
|
+
for n in plan.notes:
|
|
214
|
+
lines.append(" ! " + n)
|
|
215
|
+
return "\n".join(lines)
|