"""Job identity, carried ambiently so no command has to know it exists (D-263). Every invocation of `reach` is a job. It gets an id, and every event emitted during it is tagged with that id — which is what lets a detached run's log be read back later, and what correlates the lines of a run that streamed for nine minutes. **Ambient on purpose.** The alternative is threading a job through every command signature, or having each command open one and remember to close it. That is call-site discipline wearing a different hat, and it fails the same way: the fortieth command of a long porting session forgets, and its output silently loses its correlation with nothing to indicate anything is missing. `@command` sets this up; commands never touch it. A `ContextVar` rather than a module global, because a global is only correct until something runs two invocations in one process — which a test harness or a future batch verb does immediately, and which would then interleave two jobs' events under one id with no error anywhere. **Foreground runs get an id but no file.** Only detached runs persist (T-1277). Writing a log for all four gate invocations on every push would create retention pressure for output nobody reads. The cost is that a foreground run killed by a timeout loses its output — which is exactly the case that should have used `--detach`, so the tradeoff points the right way. """ from __future__ import annotations import os import time from contextvars import ContextVar, Token _current: ContextVar[str | None] = ContextVar("reach_job_id", default=None) # How a detached child learns which job it IS. Set by the spawning parent and # read by @command, so the child's events land under the id the parent already # reported to its caller — otherwise the returned id would name a log that # nothing ever wrote to. ENV_JOB_ID = "SR_JOB_ID" def new_id() -> str: """A sortable, readable job id: `20260831T134512-a3f2`. Timestamp-first so `jobs list` sorts chronologically by name alone, and short enough to type or paste without friction. The four random hex characters break ties within the same second — two invocations from one script would otherwise collide and write to the same log. **UTC, matching the `ts` field on every event.** Local time here would put a job id and its own log lines two hours apart, which reads as a bug in the logging every time someone correlates them by eye. """ return f"{time.strftime('%Y%m%dT%H%M%S', time.gmtime())}-{os.urandom(2).hex()}" def begin(job_id: str | None = None) -> Token[str | None]: """Start a job context. Returns the token needed to end it.""" return _current.set(job_id or new_id()) def end(token: Token[str | None]) -> None: """End a job context, restoring whatever was current before it.""" _current.reset(token) def current() -> str | None: """The id of the invocation in progress, or None outside one.""" return _current.get()