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/run_id.py
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""The single source of truth for what a runtime *run-id* is (CID-series).
|
|
3
|
+
|
|
4
|
+
docs/64_correlation-id-spine-plan.md — CID1 (the throughline slice).
|
|
5
|
+
|
|
6
|
+
The reference userland app is saturated with IDs at the *plan/phase* altitude
|
|
7
|
+
(~90 series prefixes, ~230 baseline dirs, the FQ-NNN findings queue). The thin spot is
|
|
8
|
+
the **runtime**: a `/dispatch` / `/fanout` / `/dispatch-loop` iteration is
|
|
9
|
+
identified only by its UTC directory name (`docs/_fanout_runs/20260531T143451Z/`),
|
|
10
|
+
a string that
|
|
11
|
+
|
|
12
|
+
- is NOT collision-safe across concurrent same-host loops (two loops can
|
|
13
|
+
mint the same second — the recurring WinError5 / torn-write race), and
|
|
14
|
+
- carries NO lineage (the dispatch → next-up → fanout → N×`claude -p` tree
|
|
15
|
+
is reconstructed by timestamp-correlation + git-log grep, not a join).
|
|
16
|
+
|
|
17
|
+
A `RunId` fixes both without losing the one good property the bare timestamp
|
|
18
|
+
has — sortability:
|
|
19
|
+
|
|
20
|
+
RID-<base32-ts-ms><sep><base32-entropy>
|
|
21
|
+
└ Crockford base32 of epoch-ms (sortable) └ (pid, monotonic_ns) tail
|
|
22
|
+
|
|
23
|
+
Lexicographic sort on the token == chronological order, so it drops straight
|
|
24
|
+
into the existing timestamp-named dirs. The entropy tail (derived from
|
|
25
|
+
`(pid, monotonic_ns)`, the same collision-safe idiom the reference userland app
|
|
26
|
+
uses for its stable event ids) makes two ids minted in the same
|
|
27
|
+
millisecond distinct.
|
|
28
|
+
|
|
29
|
+
DESIGN RULES (docs/64):
|
|
30
|
+
- This module adds NO new series prefix. It mints ONE id *kind* (`run_id`)
|
|
31
|
+
and carries lineage in three explicit fields (run_id / parent_id / root_id).
|
|
32
|
+
- The clock and entropy source are **injectable** so tests are deterministic
|
|
33
|
+
(the reference userland app bans non-deterministic time in reproducible
|
|
34
|
+
paths for exactly this reason). Production callers use the module defaults.
|
|
35
|
+
- Telemetry never blocks: callers wrap mint() failures and degrade to the
|
|
36
|
+
bare timestamp. mint() itself never raises on normal input.
|
|
37
|
+
|
|
38
|
+
The minted `RunId.run_id` is shaped to drop directly into the reference userland
|
|
39
|
+
app's run-context id field (currently a bare uuid4), so the
|
|
40
|
+
event spine — whose `compute_event_id(run_id, ...)` already makes run_id its
|
|
41
|
+
first component — lights up the moment a run sets the context.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import argparse
|
|
47
|
+
import itertools
|
|
48
|
+
import json
|
|
49
|
+
import os
|
|
50
|
+
import sys
|
|
51
|
+
import threading
|
|
52
|
+
import time
|
|
53
|
+
from dataclasses import dataclass
|
|
54
|
+
from pathlib import Path
|
|
55
|
+
from typing import Callable
|
|
56
|
+
|
|
57
|
+
from dos import _filelock
|
|
58
|
+
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
# Encoding — Crockford base32 (no I/L/O/U; case-insensitive; sortable).
|
|
61
|
+
# We keep our own tiny encoder rather than pull a dep; the alphabet is ordered
|
|
62
|
+
# so that lexicographic compare on the encoded string matches numeric order.
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
_CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" # 32 symbols, ascending
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _b32(n: int, *, width: int) -> str:
|
|
68
|
+
"""Left-zero-padded Crockford base32 of a non-negative int.
|
|
69
|
+
|
|
70
|
+
Fixed ``width`` keeps every id the same length so lexicographic sort over a
|
|
71
|
+
batch of ids is total (a shorter encoding would sort before a longer one
|
|
72
|
+
regardless of value).
|
|
73
|
+
"""
|
|
74
|
+
if n < 0:
|
|
75
|
+
raise ValueError("run-id components must be non-negative")
|
|
76
|
+
out = []
|
|
77
|
+
for _ in range(width):
|
|
78
|
+
out.append(_CROCKFORD[n & 0x1F])
|
|
79
|
+
n >>= 5
|
|
80
|
+
return "".join(reversed(out))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# Crockford base32 carries 5 bits/symbol. Current epoch-ms (~1.78e12, May 2026)
|
|
84
|
+
# is ~41 bits, so 8 symbols (40 bits, max ~1.10e12 ms ≈ year 2004) WRAP — the
|
|
85
|
+
# high bit is lost and sortability breaks past that boundary. 9 symbols carry
|
|
86
|
+
# 45 bits (max ~3.52e13 ms ≈ year 3084), which covers epoch-ms with headroom.
|
|
87
|
+
# (Regression pinned by test_minted_token_validates_and_decodes.)
|
|
88
|
+
_TS_WIDTH = 9
|
|
89
|
+
# 30 bits of entropy → 6 symbols. Plenty to separate same-ms mints on one host.
|
|
90
|
+
_ENTROPY_WIDTH = 6
|
|
91
|
+
_ENTROPY_BITS = _ENTROPY_WIDTH * 5 # 30
|
|
92
|
+
|
|
93
|
+
PREFIX = "RID-"
|
|
94
|
+
PROCESS_PREFIX = "PROC-"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _default_clock_ms() -> int:
|
|
98
|
+
"""Wall-clock epoch-ms. Injectable so tests pin a fixed instant."""
|
|
99
|
+
return int(time.time() * 1000)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# A strictly-increasing in-process counter. monotonic_ns() ALONE is not enough:
|
|
103
|
+
# on Windows its resolution is coarse (~15 ms), so a rapid mint batch reads the
|
|
104
|
+
# SAME ns for thousands of calls — folding that to 30 bits then collapses the
|
|
105
|
+
# batch to a handful of distinct ids (the observed 12/5000 collision-safety
|
|
106
|
+
# failure). A per-process counter is monotonic regardless of clock resolution,
|
|
107
|
+
# so consecutive same-host mints are ALWAYS distinct. Lock-guarded so concurrent
|
|
108
|
+
# threads in one process can't read the same counter value.
|
|
109
|
+
_MINT_COUNTER = itertools.count()
|
|
110
|
+
_MINT_COUNTER_LOCK = threading.Lock()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _next_mint_seq() -> int:
|
|
114
|
+
with _MINT_COUNTER_LOCK:
|
|
115
|
+
return next(_MINT_COUNTER)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _default_entropy() -> int:
|
|
119
|
+
"""Per-mint entropy from ``(pid, in-process counter, monotonic_ns)`` — an
|
|
120
|
+
extension of the collision-safe idiom `_stable_event_id` uses, hardened for
|
|
121
|
+
coarse-resolution clocks. The in-process counter occupies the LOW bits so it
|
|
122
|
+
survives the ``_ENTROPY_BITS`` fold (it is the part that guarantees two mints
|
|
123
|
+
in the same wall-clock millisecond — even the same monotonic_ns tick —
|
|
124
|
+
differ); pid + monotonic_ns fill the high bits to separate concurrent
|
|
125
|
+
processes on one host and add wall-time variation. Folded to ``_ENTROPY_BITS``.
|
|
126
|
+
"""
|
|
127
|
+
seq = _next_mint_seq()
|
|
128
|
+
# Counter in the low bits (survives the fold and is the distinctness floor);
|
|
129
|
+
# pid + monotonic_ns xored into the high bits for cross-process separation.
|
|
130
|
+
raw = seq ^ ((os.getpid() << 13) ^ (time.monotonic_ns() << 7))
|
|
131
|
+
return raw & ((1 << _ENTROPY_BITS) - 1)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass(frozen=True)
|
|
135
|
+
class RunId:
|
|
136
|
+
"""A minted run-id plus its lineage and the process it belongs to.
|
|
137
|
+
|
|
138
|
+
``run_id`` — this invocation's own sortable, collision-safe token.
|
|
139
|
+
``process_id`` — the repeatable-process slug (PROC-…), declared not minted;
|
|
140
|
+
lets "the same process across invocations" be a query.
|
|
141
|
+
``parent_id`` — the run_id that launched this one (None for a root).
|
|
142
|
+
``root_id`` — top of the tree (== run_id for a root; inherited otherwise).
|
|
143
|
+
``ts_ms`` — the epoch-ms encoded in run_id, kept for cheap reads.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
run_id: str
|
|
147
|
+
process_id: str
|
|
148
|
+
parent_id: str | None
|
|
149
|
+
root_id: str
|
|
150
|
+
ts_ms: int
|
|
151
|
+
|
|
152
|
+
def to_dict(self) -> dict:
|
|
153
|
+
"""The exact shape written to a run-dir's ``run.json`` (CID1)."""
|
|
154
|
+
return {
|
|
155
|
+
"run_id": self.run_id,
|
|
156
|
+
"process_id": self.process_id,
|
|
157
|
+
"parent_id": self.parent_id,
|
|
158
|
+
"root_id": self.root_id,
|
|
159
|
+
"ts_ms": self.ts_ms,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def mint(
|
|
164
|
+
process_id: str,
|
|
165
|
+
*,
|
|
166
|
+
parent: "RunId | str | None" = None,
|
|
167
|
+
root_id: str | None = None,
|
|
168
|
+
clock_ms: Callable[[], int] = _default_clock_ms,
|
|
169
|
+
entropy: Callable[[], int] = _default_entropy,
|
|
170
|
+
) -> RunId:
|
|
171
|
+
"""Mint a fresh ``RunId`` for one invocation of ``process_id``.
|
|
172
|
+
|
|
173
|
+
Lineage: pass ``parent`` (a RunId or its run_id string) for a child; the
|
|
174
|
+
child inherits the parent's ``root_id`` and sets ``parent_id`` to the
|
|
175
|
+
parent's run_id. A root (operator-initiated) passes no parent and becomes
|
|
176
|
+
its own root. ``root_id`` may be passed explicitly when only the string is
|
|
177
|
+
known (e.g. inherited from an env var across a `claude -p` boundary).
|
|
178
|
+
|
|
179
|
+
``clock_ms`` / ``entropy`` are injected in tests for determinism.
|
|
180
|
+
"""
|
|
181
|
+
if not process_id:
|
|
182
|
+
raise ValueError("process_id is required (e.g. 'fanout', 'dispatch-loop')")
|
|
183
|
+
proc = process_id if process_id.startswith(PROCESS_PREFIX) else PROCESS_PREFIX + process_id
|
|
184
|
+
|
|
185
|
+
ts_ms = int(clock_ms())
|
|
186
|
+
token = PREFIX + _b32(ts_ms, width=_TS_WIDTH) + _b32(entropy() & ((1 << _ENTROPY_BITS) - 1), width=_ENTROPY_WIDTH)
|
|
187
|
+
|
|
188
|
+
parent_id: str | None
|
|
189
|
+
if parent is None:
|
|
190
|
+
parent_id = None
|
|
191
|
+
elif isinstance(parent, RunId):
|
|
192
|
+
parent_id = parent.run_id
|
|
193
|
+
root_id = root_id or parent.root_id
|
|
194
|
+
else:
|
|
195
|
+
parent_id = str(parent)
|
|
196
|
+
|
|
197
|
+
resolved_root = root_id or token # a root is its own root
|
|
198
|
+
return RunId(
|
|
199
|
+
run_id=token,
|
|
200
|
+
process_id=proc,
|
|
201
|
+
parent_id=parent_id,
|
|
202
|
+
root_id=resolved_root,
|
|
203
|
+
ts_ms=ts_ms,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def is_run_id(s: str) -> bool:
|
|
208
|
+
"""True iff ``s`` is a structurally-valid minted run-id token."""
|
|
209
|
+
if not isinstance(s, str) or not s.startswith(PREFIX):
|
|
210
|
+
return False
|
|
211
|
+
body = s[len(PREFIX):]
|
|
212
|
+
if len(body) != _TS_WIDTH + _ENTROPY_WIDTH:
|
|
213
|
+
return False
|
|
214
|
+
return all(c in _CROCKFORD for c in body)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def ts_ms_of(run_id: str) -> int | None:
|
|
218
|
+
"""Decode the epoch-ms a run-id encodes (None if not a valid token)."""
|
|
219
|
+
if not is_run_id(run_id):
|
|
220
|
+
return None
|
|
221
|
+
ts_part = run_id[len(PREFIX):len(PREFIX) + _TS_WIDTH]
|
|
222
|
+
n = 0
|
|
223
|
+
for c in ts_part:
|
|
224
|
+
n = (n << 5) | _CROCKFORD.index(c)
|
|
225
|
+
return n
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# ---------------------------------------------------------------------------
|
|
229
|
+
# Lineage transport across a `claude -p` boundary (CID2/CID3 will wire this in;
|
|
230
|
+
# defined here so the contract lives next to the minter, not scattered).
|
|
231
|
+
# ---------------------------------------------------------------------------
|
|
232
|
+
ENV_RUN_ID = "CID_RUN_ID"
|
|
233
|
+
ENV_PARENT_ID = "CID_PARENT_ID"
|
|
234
|
+
ENV_ROOT_ID = "CID_ROOT_ID"
|
|
235
|
+
ENV_PROCESS_ID = "CID_PROCESS_ID"
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def lineage_env(run: RunId) -> dict[str, str]:
|
|
239
|
+
"""The env block a parent sets so a child subprocess can inherit lineage."""
|
|
240
|
+
env = {ENV_RUN_ID: run.run_id, ENV_ROOT_ID: run.root_id, ENV_PROCESS_ID: run.process_id}
|
|
241
|
+
if run.parent_id:
|
|
242
|
+
env[ENV_PARENT_ID] = run.parent_id
|
|
243
|
+
return env
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def mint_child_from_env(
|
|
247
|
+
process_id: str,
|
|
248
|
+
*,
|
|
249
|
+
env: dict[str, str] | None = None,
|
|
250
|
+
clock_ms: Callable[[], int] = _default_clock_ms,
|
|
251
|
+
entropy: Callable[[], int] = _default_entropy,
|
|
252
|
+
) -> RunId:
|
|
253
|
+
"""Mint a child run-id inheriting lineage from ``CID_*`` env vars.
|
|
254
|
+
|
|
255
|
+
If no parent env is present (an operator-initiated root), this is a root
|
|
256
|
+
mint. The parent's run_id becomes this child's ``parent_id``; the root is
|
|
257
|
+
inherited from ``CID_ROOT_ID`` (falling back to the parent run_id, then to
|
|
258
|
+
self for a root).
|
|
259
|
+
"""
|
|
260
|
+
e = env if env is not None else dict(os.environ)
|
|
261
|
+
parent = e.get(ENV_RUN_ID)
|
|
262
|
+
root = e.get(ENV_ROOT_ID) or parent
|
|
263
|
+
return mint(
|
|
264
|
+
process_id,
|
|
265
|
+
parent=parent,
|
|
266
|
+
root_id=root,
|
|
267
|
+
clock_ms=clock_ms,
|
|
268
|
+
entropy=entropy,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# ---------------------------------------------------------------------------
|
|
273
|
+
# Run-dir read-back — the CID1 query path. Resolve a run-dir → its run.json.
|
|
274
|
+
# ---------------------------------------------------------------------------
|
|
275
|
+
RUN_JSON_NAME = "run.json"
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def write_run_json(run_dir: Path, run: RunId) -> Path:
|
|
279
|
+
"""Stamp ``run.json`` into a run-dir. Returns the path written.
|
|
280
|
+
|
|
281
|
+
Never raises on a telemetry-only failure path the way callers expect — the
|
|
282
|
+
caller wraps this; here we just do the atomic-ish write.
|
|
283
|
+
"""
|
|
284
|
+
run_dir.mkdir(parents=True, exist_ok=True)
|
|
285
|
+
target = run_dir / RUN_JSON_NAME
|
|
286
|
+
tmp = run_dir / (RUN_JSON_NAME + ".tmp")
|
|
287
|
+
tmp.write_text(json.dumps(run.to_dict(), indent=2, sort_keys=True), encoding="utf-8")
|
|
288
|
+
_filelock.atomic_replace(tmp, target) # atomic on same fs; win32 rename-race hardened
|
|
289
|
+
return target
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def read_run_json(run_dir: Path) -> dict | None:
|
|
293
|
+
"""Read a run-dir's ``run.json`` (None if absent / unreadable)."""
|
|
294
|
+
target = Path(run_dir) / RUN_JSON_NAME
|
|
295
|
+
if not target.exists():
|
|
296
|
+
return None
|
|
297
|
+
try:
|
|
298
|
+
return json.loads(target.read_text(encoding="utf-8"))
|
|
299
|
+
except Exception: # noqa: BLE001 — a corrupt stamp must not crash a read
|
|
300
|
+
return None
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _cmd_mint(args: argparse.Namespace) -> int:
|
|
304
|
+
run = mint(args.process, parent=args.parent, root_id=args.root)
|
|
305
|
+
print(json.dumps(run.to_dict(), indent=2, sort_keys=True))
|
|
306
|
+
if args.write_dir:
|
|
307
|
+
path = write_run_json(Path(args.write_dir), run)
|
|
308
|
+
print(f"# wrote {path}", file=sys.stderr)
|
|
309
|
+
return 0
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _cmd_show(args: argparse.Namespace) -> int:
|
|
313
|
+
"""Resolve a run-dir → its run-id + lineage (the CID1 read-back)."""
|
|
314
|
+
data = read_run_json(Path(args.dir))
|
|
315
|
+
if data is None:
|
|
316
|
+
print(f"no {RUN_JSON_NAME} in {args.dir}", file=sys.stderr)
|
|
317
|
+
return 1
|
|
318
|
+
print(json.dumps(data, indent=2, sort_keys=True))
|
|
319
|
+
return 0
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def main(argv: list[str] | None = None) -> int:
|
|
323
|
+
p = argparse.ArgumentParser(description="Mint / inspect runtime run-ids (CID-series; docs/64).")
|
|
324
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
325
|
+
|
|
326
|
+
m = sub.add_parser("mint", help="mint a run-id (optionally stamp run.json into a dir)")
|
|
327
|
+
m.add_argument("process", help="process slug, e.g. 'fanout' / 'dispatch-loop'")
|
|
328
|
+
m.add_argument("--parent", default=None, help="parent run_id (for a child mint)")
|
|
329
|
+
m.add_argument("--root", default=None, help="root run_id (inherited across a subprocess boundary)")
|
|
330
|
+
m.add_argument("--write-dir", default=None, help="run-dir to stamp run.json into")
|
|
331
|
+
m.set_defaults(func=_cmd_mint)
|
|
332
|
+
|
|
333
|
+
s = sub.add_parser("show", help="resolve a run-dir → its run-id + lineage")
|
|
334
|
+
s.add_argument("dir", help="a run-dir (e.g. docs/_fanout_runs/<ts>/)")
|
|
335
|
+
s.set_defaults(func=_cmd_show)
|
|
336
|
+
|
|
337
|
+
args = p.parse_args(argv)
|
|
338
|
+
return args.func(args)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
if __name__ == "__main__":
|
|
342
|
+
raise SystemExit(main())
|