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/archive_lock.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Mutex for the /fanout-true-headless-multi-agent Step 9.5 archive ceremony.
|
|
2
|
+
|
|
3
|
+
Two near-concurrent fanouts can both reach Step 9.5 inside a ~30s window. Both
|
|
4
|
+
write to `docs/_fanout_runs/INDEX.md` and stage `docs/_plans/execution-state.yaml`
|
|
5
|
+
through `git add`. Without serialization, the second orchestrator's edits are
|
|
6
|
+
absorbed into the first orchestrator's commit (queue finding #34, observed
|
|
7
|
+
20260508T0502Z vs 0458Z `f88a800`).
|
|
8
|
+
|
|
9
|
+
Lock semantics:
|
|
10
|
+
Path: docs/_fanout_runs/.archive.lock (single shared mutex; gitignored)
|
|
11
|
+
Owner: free-text tag, e.g. `fanout-<UTC-ts>`.
|
|
12
|
+
TTL: 5 minutes — Step 9.5 archive should never run longer; older = orphan.
|
|
13
|
+
|
|
14
|
+
Acquire is atomic O_CREAT|O_EXCL. On EEXIST, the helper reads the existing
|
|
15
|
+
lock and decides:
|
|
16
|
+
- same owner → re-entrant; refresh acquired_at, exit 0.
|
|
17
|
+
- other owner, age <TTL → poll up to --retries times at --retry-interval
|
|
18
|
+
seconds; if still busy, exit 1.
|
|
19
|
+
- other owner, age ≥TTL → steal via an atomic compare-and-swap on the
|
|
20
|
+
lock inode (`_steal_stale_lock`): rename the
|
|
21
|
+
stale lock to a per-stealer temp (atomic; only
|
|
22
|
+
one stealer wins) then O_EXCL-recreate. Two
|
|
23
|
+
concurrent stealers can never both end up
|
|
24
|
+
holding the mutex (the old unlink+recreate was a
|
|
25
|
+
TOCTOU that let them).
|
|
26
|
+
|
|
27
|
+
Release unlinks the lock if owner matches; on owner mismatch (something
|
|
28
|
+
stole it) prints a warning to stderr and still exits 0 — the archive
|
|
29
|
+
ceremony shouldn't fail over a release race.
|
|
30
|
+
|
|
31
|
+
Subcommands:
|
|
32
|
+
acquire <owner> [--retries 5] [--retry-interval 2] [--ttl-seconds 300]
|
|
33
|
+
Exit 0 on success (printed: "acquired" | "re-entrant <prev-acquired-at>"
|
|
34
|
+
| "stole-stale <prev-owner> age=<seconds>s").
|
|
35
|
+
Exit 1 if the lock is owned by another live process and retries
|
|
36
|
+
are exhausted (printed: "busy <prev-owner> age=<seconds>s").
|
|
37
|
+
|
|
38
|
+
release <owner> [--force]
|
|
39
|
+
Exit 0 if removed (or absent). With --force, removes regardless of
|
|
40
|
+
owner (operator-orphaned cleanup).
|
|
41
|
+
|
|
42
|
+
status
|
|
43
|
+
Prints lock state: "free" | "held <owner> age=<seconds>s"
|
|
44
|
+
| "stale <owner> age=<seconds>s".
|
|
45
|
+
"""
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
import argparse
|
|
49
|
+
import datetime as dt
|
|
50
|
+
import os
|
|
51
|
+
import sys
|
|
52
|
+
import time
|
|
53
|
+
from pathlib import Path
|
|
54
|
+
|
|
55
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
56
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
57
|
+
|
|
58
|
+
from dos import _filelock
|
|
59
|
+
from dos import config as _config
|
|
60
|
+
|
|
61
|
+
DEFAULT_TTL_SECONDS = 300
|
|
62
|
+
DEFAULT_RETRIES = 5
|
|
63
|
+
DEFAULT_RETRY_INTERVAL = 2.0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _lock_path() -> Path:
|
|
67
|
+
"""The cross-process archive lock path for the active workspace.
|
|
68
|
+
|
|
69
|
+
Resolves against the injected config (separation refactor), with an env
|
|
70
|
+
override for tests. Re-resolved each call so a test that re-points the
|
|
71
|
+
workspace after import still redirects.
|
|
72
|
+
"""
|
|
73
|
+
env = os.environ.get("DISPATCH_ARCHIVE_LOCK_PATH")
|
|
74
|
+
if env:
|
|
75
|
+
return Path(env)
|
|
76
|
+
return _config.active().paths.archive_lock
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# Module-level handle for back-compat with callers that read the attribute.
|
|
80
|
+
LOCK_PATH = _lock_path()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _now() -> dt.datetime:
|
|
84
|
+
return dt.datetime.now(dt.timezone.utc)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _now_iso() -> str:
|
|
88
|
+
return _now().strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _parse_iso(s: str) -> dt.datetime | None:
|
|
92
|
+
for fmt in ("%Y-%m-%dT%H:%M:%SZ", "%Y-%m-%dT%H:%MZ"):
|
|
93
|
+
try:
|
|
94
|
+
return dt.datetime.strptime(s, fmt).replace(tzinfo=dt.timezone.utc)
|
|
95
|
+
except (ValueError, TypeError):
|
|
96
|
+
continue
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _read_lock_at(path: Path) -> dict | None:
|
|
101
|
+
"""Parse a lock file at an arbitrary path → its `{key: value}` info dict.
|
|
102
|
+
|
|
103
|
+
Delegates to the shared `_filelock.read_lock` so this mutex and its siblings
|
|
104
|
+
(lane_lease, home) parse a lock the one same way the shared steal CAS does."""
|
|
105
|
+
return _filelock.read_lock(path)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _read_lock() -> dict | None:
|
|
109
|
+
return _read_lock_at(_lock_path())
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _write_lock(owner: str) -> None:
|
|
113
|
+
"""Atomic O_CREAT|O_EXCL create. Raises FileExistsError if lock present.
|
|
114
|
+
|
|
115
|
+
Delegates to the shared `_filelock.write_lock` — the one O_EXCL create every
|
|
116
|
+
mutex in the package uses."""
|
|
117
|
+
_filelock.write_lock(_lock_path(), owner)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _steal_stale_lock(owner: str, stale: dict) -> bool:
|
|
121
|
+
"""Atomically steal the SPECIFIC stale lock `stale` for `owner`. True iff WE won.
|
|
122
|
+
|
|
123
|
+
Delegates to the shared `_filelock.steal_stale` — the ONE value-keyed
|
|
124
|
+
compare-and-swap every mutex steals through (so the TOCTOU that let two stealers
|
|
125
|
+
both hold the lock cannot be re-introduced by a per-site copy). `stale` is the
|
|
126
|
+
lock-info dict the caller just `_read_lock`'d; the CAS only displaces the EXACT
|
|
127
|
+
stale lock it names, restoring a racer's fresh lock on mismatch. See
|
|
128
|
+
`_filelock.steal_stale` for the full three-step discipline."""
|
|
129
|
+
return _filelock.steal_stale(_lock_path(), owner, stale)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _refresh_lock(owner: str) -> None:
|
|
133
|
+
"""Re-write the lock with the current timestamp (re-entrant case)."""
|
|
134
|
+
body = f"owner: {owner}\nacquired_at: {_now_iso()}\npid: {os.getpid()}\n"
|
|
135
|
+
_lock_path().write_text(body, encoding="utf-8")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _age_seconds(info: dict) -> float | None:
|
|
139
|
+
ts = _parse_iso(info.get("acquired_at", ""))
|
|
140
|
+
if ts is None:
|
|
141
|
+
return None
|
|
142
|
+
return (_now() - ts).total_seconds()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def cmd_acquire(args: argparse.Namespace) -> int:
|
|
146
|
+
owner = args.owner
|
|
147
|
+
ttl = args.ttl_seconds
|
|
148
|
+
retries = args.retries
|
|
149
|
+
interval = args.retry_interval
|
|
150
|
+
|
|
151
|
+
for attempt in range(retries + 1):
|
|
152
|
+
try:
|
|
153
|
+
_write_lock(owner)
|
|
154
|
+
print(f"acquired {owner}")
|
|
155
|
+
return 0
|
|
156
|
+
except FileExistsError:
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
info = _read_lock()
|
|
160
|
+
if info is None:
|
|
161
|
+
# Lock was unlinked between EEXIST and read; retry the create.
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
prev_owner = info.get("owner", "<unknown>")
|
|
165
|
+
age = _age_seconds(info)
|
|
166
|
+
|
|
167
|
+
if prev_owner == owner:
|
|
168
|
+
_refresh_lock(owner)
|
|
169
|
+
prev_at = info.get("acquired_at", "<unknown>")
|
|
170
|
+
print(f"re-entrant {owner} prev-acquired-at={prev_at}")
|
|
171
|
+
return 0
|
|
172
|
+
|
|
173
|
+
if age is not None and age >= ttl:
|
|
174
|
+
# Atomic compare-and-swap steal keyed on the stale lock's identity
|
|
175
|
+
# (`info`): only the process that displaces the EXACT stale lock it
|
|
176
|
+
# observed proceeds; a concurrent stealer that already won (and
|
|
177
|
+
# re-created a fresh lock) is detected and conceded to, so two
|
|
178
|
+
# processes can never both come away holding the mutex (the old
|
|
179
|
+
# unlink-then-create TOCTOU). A lost steal falls through to retry.
|
|
180
|
+
if not _steal_stale_lock(owner, info):
|
|
181
|
+
continue
|
|
182
|
+
print(f"stole-stale {prev_owner} age={age:.0f}s")
|
|
183
|
+
return 0
|
|
184
|
+
|
|
185
|
+
if attempt < retries:
|
|
186
|
+
age_str = f"{age:.0f}s" if age is not None else "?s"
|
|
187
|
+
print(f" waiting on {prev_owner} (age={age_str}); retry {attempt + 1}/{retries}", file=sys.stderr)
|
|
188
|
+
time.sleep(interval)
|
|
189
|
+
continue
|
|
190
|
+
|
|
191
|
+
age_str = f"{age:.0f}s" if age is not None else "?s"
|
|
192
|
+
print(f"busy {prev_owner} age={age_str}", file=sys.stderr)
|
|
193
|
+
return 1
|
|
194
|
+
|
|
195
|
+
return 1
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def cmd_release(args: argparse.Namespace) -> int:
|
|
199
|
+
owner = args.owner
|
|
200
|
+
info = _read_lock()
|
|
201
|
+
if info is None:
|
|
202
|
+
print("released (no-lock)")
|
|
203
|
+
return 0
|
|
204
|
+
|
|
205
|
+
if args.force:
|
|
206
|
+
try:
|
|
207
|
+
_lock_path().unlink()
|
|
208
|
+
except FileNotFoundError:
|
|
209
|
+
pass
|
|
210
|
+
print(f"force-released (was: {info.get('owner', '<unknown>')})")
|
|
211
|
+
return 0
|
|
212
|
+
|
|
213
|
+
if info.get("owner") != owner:
|
|
214
|
+
print(f"WARN: lock owner mismatch (have={info.get('owner', '<unknown>')}, expected={owner}); leaving alone", file=sys.stderr)
|
|
215
|
+
return 0
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
_lock_path().unlink()
|
|
219
|
+
except FileNotFoundError:
|
|
220
|
+
pass
|
|
221
|
+
print(f"released {owner}")
|
|
222
|
+
return 0
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def cmd_status(args: argparse.Namespace) -> int:
|
|
226
|
+
info = _read_lock()
|
|
227
|
+
if info is None:
|
|
228
|
+
print("free")
|
|
229
|
+
return 0
|
|
230
|
+
owner = info.get("owner", "<unknown>")
|
|
231
|
+
age = _age_seconds(info)
|
|
232
|
+
age_str = f"{age:.0f}s" if age is not None else "?s"
|
|
233
|
+
if age is not None and age >= args.ttl_seconds:
|
|
234
|
+
print(f"stale {owner} age={age_str}")
|
|
235
|
+
else:
|
|
236
|
+
print(f"held {owner} age={age_str}")
|
|
237
|
+
return 0
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def main() -> int:
|
|
241
|
+
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
242
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
243
|
+
|
|
244
|
+
p_acq = sub.add_parser("acquire", help="Acquire the archive lock (atomic + retry-with-backoff)")
|
|
245
|
+
p_acq.add_argument("owner", help='Owner tag, e.g. "fanout-20260508T1900Z"')
|
|
246
|
+
p_acq.add_argument("--retries", type=int, default=DEFAULT_RETRIES)
|
|
247
|
+
p_acq.add_argument("--retry-interval", type=float, default=DEFAULT_RETRY_INTERVAL, help="Seconds between retries")
|
|
248
|
+
p_acq.add_argument("--ttl-seconds", type=int, default=DEFAULT_TTL_SECONDS, help="Stale-threshold; older locks get stolen")
|
|
249
|
+
|
|
250
|
+
p_rel = sub.add_parser("release", help="Release the archive lock")
|
|
251
|
+
p_rel.add_argument("owner", help="Owner tag — must match holder unless --force")
|
|
252
|
+
p_rel.add_argument("--force", action="store_true", help="Remove regardless of owner (operator orphan-cleanup)")
|
|
253
|
+
|
|
254
|
+
p_st = sub.add_parser("status", help="Print current lock state")
|
|
255
|
+
p_st.add_argument("--ttl-seconds", type=int, default=DEFAULT_TTL_SECONDS)
|
|
256
|
+
|
|
257
|
+
args = ap.parse_args()
|
|
258
|
+
return {
|
|
259
|
+
"acquire": cmd_acquire,
|
|
260
|
+
"release": cmd_release,
|
|
261
|
+
"status": cmd_status,
|
|
262
|
+
}[args.cmd](args)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
if __name__ == "__main__":
|
|
266
|
+
sys.exit(main())
|