custos-code 0.0.1__py3-none-any.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.
@@ -0,0 +1,172 @@
1
+ """Devin Path C: no tool log, so the ledger is built from state (see docs/DEVIN.md, A2).
2
+
3
+ The public v1 API exposes session metadata, chat messages, `structured_output` and the PRs a
4
+ session opened -- never the shell it ran. Path C therefore reads a *bundle*: the API's
5
+ GetSessionResponse plus whatever state evidence the caller could collect. Report = PR description
6
+ + structured_output + last Devin message. Evidence = git + CI + checkout, which is the state half
7
+ of invariant 4: an edit or commit claim can be confirmed here, while "ran the tests locally"
8
+ cannot. Path C sessions therefore carry a `no_tool_log` META row and a reduced integrity score so
9
+ Tier 1/2 answer `unrecorded` rather than inventing a witness.
10
+
11
+ Bundle (JSON, one object) -- either a raw GetSessionResponse or:
12
+ {"session": <GetSessionResponse>,
13
+ "git": {"root": "/abs", "branch": ..., "commits": [{"sha","subject","ts","files":[...],"stat":...}]},
14
+ "checks": [{"name","conclusion","started_at","completed_at","url","output"}],
15
+ "checkout": {"root": "/abs", "files": [{"path","exists","sha256","bytes"}]}}
16
+
17
+ `fetch_bundle` builds one from the v1 API (GET /v1/sessions/{id}); `nudge` is the correction loop
18
+ for a running session (POST /v1/sessions/{id}/message). Both are opt-in network calls.
19
+
20
+ Owner: Ananya.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import json
26
+ import os
27
+ import urllib.error
28
+ import urllib.request
29
+ from datetime import UTC, datetime
30
+ from typing import Any
31
+
32
+ from ..ledger import chain, redact
33
+ from ..models import EventKind, LedgerEvent, Session
34
+ from .state import Builder, as_dict, as_list, as_text, as_ts, checkout, checks, commits, no_tool_log
35
+
36
+ API_ROOT = "https://api.devin.ai/v1"
37
+
38
+
39
+ def _messages(b: Builder, messages: list[Any], fallback: datetime) -> tuple[datetime, str | None]:
40
+ """Chat messages are report material, not evidence: Devin's own text never witnesses an action."""
41
+ last_devin: str | None = None
42
+ ts = fallback
43
+ for raw in messages:
44
+ msg = as_dict(raw)
45
+ ts = as_ts(msg.get("timestamp") or msg.get("created_at"), ts)
46
+ body = as_text(msg.get("message") or msg.get("content"))
47
+ if not body:
48
+ continue
49
+ kind = EventKind.USER if str(msg.get("type", "")).startswith("user") else EventKind.TEXT
50
+ b.store_output(b.add(kind=kind, ts=ts), body)
51
+ if kind is EventKind.TEXT:
52
+ last_devin = body
53
+ return ts, last_devin
54
+
55
+
56
+ def build_report(
57
+ session: dict[str, Any], pr: dict[str, Any], last_message: str | None
58
+ ) -> str | None:
59
+ """Path C's report is the union of the three places Devin states what it did."""
60
+ parts: list[str] = []
61
+ body = as_text(pr.get("body") or pr.get("description"))
62
+ if body:
63
+ parts.append(f"## Pull request {pr.get('url') or pr.get('html_url') or ''}\n{body}".strip())
64
+ structured = session.get("structured_output")
65
+ if structured:
66
+ parts.append(f"## structured_output\n{as_text(structured)}")
67
+ if last_message:
68
+ parts.append(f"## Final message\n{last_message}")
69
+ return "\n\n".join(parts) if parts else None
70
+
71
+
72
+ def parse(path: str) -> tuple[Session, list[LedgerEvent], str | None]:
73
+ with open(path, encoding="utf-8") as fh:
74
+ bundle = json.load(fh)
75
+ if not isinstance(bundle, dict):
76
+ raise ValueError(f"{path}: expected a Devin bundle object")
77
+ session = as_dict(bundle.get("session") or bundle)
78
+ git = as_dict(bundle.get("git"))
79
+ probes = as_dict(bundle.get("checkout"))
80
+
81
+ session_id = str(session.get("session_id") or session.get("id") or "devin-unknown")
82
+ started = as_ts(session.get("created_at"), datetime.fromtimestamp(0, tz=UTC))
83
+ prs = as_list(session.get("pull_requests")) or (
84
+ [session["pull_request"]] if session.get("pull_request") else []
85
+ )
86
+ pr = as_dict(prs[0]) if prs else {}
87
+ cwd = str(git.get("root") or probes.get("root") or "") or None
88
+
89
+ b = Builder(session_id, cwd)
90
+ b.add(
91
+ kind=EventKind.META,
92
+ ts=started,
93
+ input=redact(
94
+ {
95
+ "event": "session_meta",
96
+ "path": "C",
97
+ "status": str(session.get("status_enum", "")),
98
+ "snapshot_id": str(session.get("snapshot_id") or ""),
99
+ "playbook_id": str(session.get("playbook_id") or ""),
100
+ "pull_request": str(pr.get("url") or pr.get("html_url") or ""),
101
+ }
102
+ ),
103
+ )
104
+ no_tool_log(
105
+ b, started, "Devin public API exposes no tool calls; evidence is git + CI + checkout", "C"
106
+ )
107
+
108
+ ts, last_devin = _messages(b, as_list(session.get("messages")), started)
109
+ ts = commits(b, git, ts)
110
+ ts = checks(b, as_list(bundle.get("checks")), ts)
111
+ checkout(b, probes, ts)
112
+
113
+ events = chain(b.events)
114
+ meta = Session(
115
+ id=session_id,
116
+ source="devin",
117
+ agent=str(session.get("agent") or "devin"),
118
+ model=str(session.get("model")) if session.get("model") else None,
119
+ started=started,
120
+ ended=ts,
121
+ cwd=cwd,
122
+ git_branch=str(git.get("branch") or pr.get("head") or "") or None,
123
+ n_events=len(events),
124
+ ledger_root_hash=events[-1].hash if events else "",
125
+ # Path C sees state only; half the ladder is blind here and the score must say so.
126
+ integrity_score=0.5,
127
+ )
128
+ return meta, events, build_report(session, pr, last_devin)
129
+
130
+
131
+ def _api(method: str, url: str, token: str, body: dict[str, Any] | None = None) -> dict[str, Any]:
132
+ data = json.dumps(body).encode() if body is not None else None
133
+ request = urllib.request.Request(
134
+ url,
135
+ data=data,
136
+ method=method,
137
+ headers={
138
+ "Authorization": f"Bearer {token}",
139
+ "Content-Type": "application/json",
140
+ },
141
+ )
142
+ with urllib.request.urlopen(request, timeout=30) as response: # noqa: S310 - fixed https host
143
+ payload = response.read().decode()
144
+ return as_dict(json.loads(payload)) if payload.strip() else {}
145
+
146
+
147
+ def _key(token: str | None) -> str:
148
+ key = token or os.environ.get("DEVIN_API_KEY", "")
149
+ if not key:
150
+ raise ValueError("no Devin API key: pass token= or set DEVIN_API_KEY")
151
+ return key
152
+
153
+
154
+ def fetch_bundle(
155
+ session_id: str, token: str | None = None, *, api_root: str = API_ROOT
156
+ ) -> dict[str, Any]:
157
+ """GET /v1/sessions/{id}. Nothing leaves the machine unless the caller asks for this."""
158
+ return {"session": _api("GET", f"{api_root}/sessions/{session_id}", _key(token))}
159
+
160
+
161
+ def nudge(
162
+ session_id: str, message: str, token: str | None = None, *, api_root: str = API_ROOT
163
+ ) -> dict[str, Any]:
164
+ """Correction loop for a running session: POST /v1/sessions/{id}/message (docs/DEVIN.md)."""
165
+ try:
166
+ return _api(
167
+ "POST", f"{api_root}/sessions/{session_id}/message", _key(token), {"message": message}
168
+ )
169
+ except (
170
+ urllib.error.HTTPError
171
+ ) as exc: # a finished session refuses messages; caller falls back to a PR comment
172
+ raise RuntimeError(f"devin message rejected ({exc.code}): {exc.reason}") from exc
@@ -0,0 +1,379 @@
1
+ """Class M: the recorder that needs no harness at all (docs/ADAPTERS.md §4).
2
+
3
+ The floor of the coverage argument: with no hooks and no session file, a shell trap still knows
4
+ every command, its exit status, its cwd and its pid chain, and `git reflog` still knows every ref
5
+ move. `install_snippet` emits the bash/zsh lines `custos-code record` writes into the user's rc file;
6
+ this module parses what they log.
7
+
8
+ Wire format, one JSON object per line in ~/.custos-code/machine/<host>-<date>.jsonl:
9
+ {"recorder":"custos-code-machine","v":1,"event":"start|end|fs|git","ts":<epoch float>,
10
+ "cmd":"pytest -q","cwd":"/abs","tty":"/dev/ttys004","pid":123,"ppid":99,"ppid_chain":["bash","codex"],
11
+ "exit":0,"dur_ms":2250,"out":"<optional captured output>",
12
+ "path":"src/a.py","op":"modified", # event=fs
13
+ "ref":"HEAD","from":"abc","to":"def","subject":"..."} # event=git
14
+
15
+ Attribution: class M cannot prove which agent ran a command, so the recorded pid/ppid are a hint
16
+ and nothing more. Alone, these rows support outcome claims but leave tool attribution
17
+ `unwitnessed` rather than guessing from timing. Merging them into a class-H/F ledger to fill its
18
+ exit-code gaps is deliberately not implemented: the join is timing-based, the machine log is
19
+ writable by anything the agent runs, and a merge would have to re-chain and mark every borrowed
20
+ row to keep invariant 1 legible. See docs/ADAPTERS.md §4.
21
+
22
+ Owner: Ananya.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import hashlib
28
+ import json
29
+ import os
30
+ import socket
31
+ from datetime import UTC, datetime
32
+ from typing import Any
33
+
34
+ from ..ledger import MAX_OUTPUT_BYTES, chain, redact
35
+ from ..models import EventFlags, EventKind, LedgerEvent, Session
36
+ from ..parsers import is_piped
37
+
38
+ LOG_DIR = os.path.expanduser("~/.custos-code/machine")
39
+ WIRE_VERSION = 1
40
+ # The product rename did not change the wire schema. Keep historical logs readable.
41
+ RECORDER_NAMES = ("custos-code-machine", "receipts-machine")
42
+
43
+ BASH_SNIPPET = r"""# >>> custos-code recorder (class M) >>>
44
+ __custos_code_log() { printf '%s\n' "$1" >> "$CUSTOS_CODE_MACHINE_LOG"; }
45
+ __custos_code_preexec() {
46
+ [ -n "$COMP_LINE" ] && return
47
+ [ "$BASH_COMMAND" = "$PROMPT_COMMAND" ] && return
48
+ __CUSTOS_CODE_CMD="$BASH_COMMAND"; __CUSTOS_CODE_T0=$(date +%s.%N)
49
+ __custos_code_log "$(CUSTOS_CODE_EVENT=start CUSTOS_CODE_CMD="$__CUSTOS_CODE_CMD" CUSTOS_CODE_PID=$$ CUSTOS_CODE_PPID=$PPID CUSTOS_CODE_TTY="$CUSTOS_CODE_TTY" custos-code _record-line)"
50
+ }
51
+ __custos_code_precmd() {
52
+ local rc=$?
53
+ [ -z "$__CUSTOS_CODE_CMD" ] && return
54
+ __custos_code_log "$(CUSTOS_CODE_EVENT=end CUSTOS_CODE_CMD="$__CUSTOS_CODE_CMD" CUSTOS_CODE_RC=$rc CUSTOS_CODE_T0="$__CUSTOS_CODE_T0" CUSTOS_CODE_PID=$$ CUSTOS_CODE_PPID=$PPID CUSTOS_CODE_TTY="$CUSTOS_CODE_TTY" custos-code _record-line)"
55
+ __CUSTOS_CODE_CMD=
56
+ }
57
+ export CUSTOS_CODE_TTY="${CUSTOS_CODE_TTY:-$(tty 2>/dev/null || echo)}"
58
+ export CUSTOS_CODE_MACHINE_LOG="${CUSTOS_CODE_MACHINE_LOG:-$HOME/.custos-code/machine/$(hostname -s)-$(date +%F).jsonl}"
59
+ mkdir -p "$(dirname "$CUSTOS_CODE_MACHINE_LOG")"
60
+ trap '__custos_code_preexec' DEBUG
61
+ PROMPT_COMMAND="__custos_code_precmd${PROMPT_COMMAND:+; $PROMPT_COMMAND}"
62
+ # <<< custos-code recorder (class M) <<<
63
+ """
64
+
65
+ ZSH_SNIPPET = r"""# >>> custos-code recorder (class M) >>>
66
+ export CUSTOS_CODE_TTY="${CUSTOS_CODE_TTY:-$(tty 2>/dev/null || echo)}"
67
+ export CUSTOS_CODE_MACHINE_LOG="${CUSTOS_CODE_MACHINE_LOG:-$HOME/.custos-code/machine/$(hostname -s)-$(date +%F).jsonl}"
68
+ mkdir -p "${CUSTOS_CODE_MACHINE_LOG:h}"
69
+ __custos_code_preexec() {
70
+ __CUSTOS_CODE_CMD=$1; __CUSTOS_CODE_T0=$EPOCHREALTIME
71
+ CUSTOS_CODE_EVENT=start CUSTOS_CODE_CMD="$__CUSTOS_CODE_CMD" CUSTOS_CODE_PID=$$ CUSTOS_CODE_PPID=$PPID \
72
+ CUSTOS_CODE_TTY="$CUSTOS_CODE_TTY" custos-code _record-line >> "$CUSTOS_CODE_MACHINE_LOG"
73
+ }
74
+ __custos_code_precmd() {
75
+ local rc=$?
76
+ [[ -z $__CUSTOS_CODE_CMD ]] && return
77
+ CUSTOS_CODE_EVENT=end CUSTOS_CODE_CMD="$__CUSTOS_CODE_CMD" CUSTOS_CODE_RC=$rc CUSTOS_CODE_T0="$__CUSTOS_CODE_T0" \
78
+ CUSTOS_CODE_PID=$$ CUSTOS_CODE_PPID=$PPID CUSTOS_CODE_TTY="$CUSTOS_CODE_TTY" \
79
+ custos-code _record-line >> "$CUSTOS_CODE_MACHINE_LOG"
80
+ __CUSTOS_CODE_CMD=
81
+ }
82
+ autoload -Uz add-zsh-hook
83
+ add-zsh-hook preexec __custos_code_preexec
84
+ add-zsh-hook precmd __custos_code_precmd
85
+ # <<< custos-code recorder (class M) <<<
86
+ """
87
+
88
+
89
+ def install_snippet(shell: str) -> str:
90
+ """The rc-file lines for `shell`. Non-interactive shells need the PATH-first wrapper instead."""
91
+ if shell in ("bash", "sh"):
92
+ return BASH_SNIPPET
93
+ if shell == "zsh":
94
+ return ZSH_SNIPPET
95
+ raise ValueError(f"no recorder snippet for {shell!r}; supported: bash, sh, zsh")
96
+
97
+
98
+ # docs/ADAPTERS.md §4 promises this ("non-interactive shells: a PATH-first bash and sh wrapper
99
+ # that logs argv and exit status and execs the real shell") and §7's VERIFY names exactly the gap
100
+ # it closes: "DEBUG trap behaviour inside the shells Claude Code and Codex spawn (they run
101
+ # `bash -c`/`zsh -lc`)". They do not source ~/.bashrc -- POSIX shells only read startup files for
102
+ # interactive or login shells, and `bash -c "cmd"` is neither -- so BASH_SNIPPET's `trap ... DEBUG`
103
+ # and ZSH_SNIPPET's `preexec` hook never attach inside a command an agent spawns this way. Nothing
104
+ # in this module implemented the wrapper before now; `install_snippet` only ever returned the
105
+ # interactive rc-file forms.
106
+ #
107
+ # `{real}` is resolved once, at install time, to an absolute path outside the wrapper's own
108
+ # directory (the same "bake in the real path instead of re-resolving through PATH" idiom already
109
+ # used by `hooks._run.sh`'s `custos_code_cmd` and `rerun._worker_argv`) -- re-resolving "bash" via
110
+ # PATH inside the wrapper would just find itself again if its own directory is still first.
111
+ #
112
+ # The shebang is `#!/bin/bash`, not `#!/usr/bin/env bash`: `env` re-resolves `bash` through PATH,
113
+ # and `--wrapper --install` puts this wrapper's own directory *first* on PATH, so `env` would find
114
+ # the wrapper again, whose shebang runs `env bash` again -- forever. `REAL` is baked in precisely
115
+ # to avoid this trap for the interpreter *inside* the script; the shebang needs the same treatment
116
+ # for the interpreter that runs the script itself, and unlike `REAL` it cannot be resolved at
117
+ # install time (it has to be correct before the script has run a single line), so it is the one
118
+ # absolute path in this file that is not `which`-resolved -- `/bin/bash` is as close to universal
119
+ # as a hardcoded path gets on the platforms this targets.
120
+ #
121
+ # The two `custos-code _record-line` calls redirect stderr to /dev/null *before* redirecting stdout
122
+ # to the log (`2>/dev/null >> "$LOG"`, not `>> "$LOG" 2>/dev/null`): bash sets up redirections in
123
+ # order, so if the log's directory does not exist, the `>>` open failure is itself an error, and
124
+ # whichever fd swap happened first decides where that error goes. With `2>/dev/null` first, it's
125
+ # already gone before the failing `>>` has anywhere else to send it. The same reasoning is why
126
+ # `mkdir -p` gets its own `2>/dev/null`: with no redirect at all, a permission-denied `mkdir` would
127
+ # print straight to the wrapped command's own stderr -- exactly the failure mode `3433813` fixed
128
+ # elsewhere (custos-code' own instrumentation manufacturing the evidence a rule then judges).
129
+ #
130
+ # `CUSTOS_CODE_MACHINE_LOG` is assigned, not exported: exporting it would hand every child process
131
+ # (including `$REAL "$@"` and everything it spawns) the ledger's own path, letting an agent that
132
+ # only needed to run a command also overwrite or forge rows in the log describing it. Making that
133
+ # safe against a *deliberately* adversarial agent needs harness signatures and per-row provenance
134
+ # marking -- real design work, tracked separately -- so this only closes the accidental case for
135
+ # now: nothing downstream of the wrapper can find the path by looking at its own environment.
136
+ WRAPPER_TEMPLATE = r"""#!/bin/bash
137
+ # >>> custos-code recorder (class M), PATH-first wrapper >>>
138
+ # Installed by `custos-code record --wrapper`; intercepts a PATH lookup for {name} that an
139
+ # agent-spawned, non-interactive shell (`{name} -c "cmd"`) would otherwise resolve straight to the
140
+ # real interpreter, invisibly to install_snippet's rc-file hooks. Logs start/end the same way the
141
+ # interactive snippets do (`custos-code _record-line`, same wire format), then runs the real {name}
142
+ # and exits with its exact status. Never captures stdout/stderr: those pass straight through.
143
+ REAL={real}
144
+ case "$1" in
145
+ -*c*) if [ "$#" -ge 2 ]; then __CUSTOS_CODE_CMD="$2"; else __CUSTOS_CODE_CMD="$*"; fi ;;
146
+ # a login/command flag bundle (-c, -lc, -ic, ...) carries the command as $2; anything else
147
+ # (including a script on stdin, which has no argv command at all) falls back to argv itself.
148
+ *) __CUSTOS_CODE_CMD="$*" ;;
149
+ esac
150
+ CUSTOS_CODE_MACHINE_LOG="${{CUSTOS_CODE_MACHINE_LOG:-$HOME/.custos-code/machine/$(hostname -s)-$(date +%F).jsonl}}"
151
+ mkdir -p "$(dirname "$CUSTOS_CODE_MACHINE_LOG")" 2>/dev/null || true
152
+ __T0=$(date +%s.%N)
153
+ CUSTOS_CODE_EVENT=start CUSTOS_CODE_CMD="$__CUSTOS_CODE_CMD" CUSTOS_CODE_PID=$$ CUSTOS_CODE_PPID=$PPID \
154
+ custos-code _record-line 2>/dev/null >> "$CUSTOS_CODE_MACHINE_LOG" || true
155
+ "$REAL" "$@"
156
+ __RC=$?
157
+ CUSTOS_CODE_EVENT=end CUSTOS_CODE_CMD="$__CUSTOS_CODE_CMD" CUSTOS_CODE_RC=$__RC CUSTOS_CODE_T0="$__T0" \
158
+ CUSTOS_CODE_PID=$$ CUSTOS_CODE_PPID=$PPID \
159
+ custos-code _record-line 2>/dev/null >> "$CUSTOS_CODE_MACHINE_LOG" || true
160
+ exit $__RC
161
+ # <<< custos-code recorder (class M) <<<
162
+ """
163
+
164
+ WRAPPER_DIR = os.path.expanduser("~/.custos-code/bin")
165
+
166
+
167
+ def wrapper_script(name: str, real_path: str) -> str:
168
+ """Render the PATH-first wrapper for `name` (`bash` or `sh`), calling through to `real_path`."""
169
+ import shlex
170
+
171
+ return WRAPPER_TEMPLATE.format(name=name, real=shlex.quote(real_path))
172
+
173
+
174
+ def install_wrapper(bin_dir: str | None = None, which: Any = None) -> dict[str, str]:
175
+ """Write `bash`/`sh` wrapper scripts into `bin_dir` (default `~/.custos-code/bin`), executable,
176
+ each baked with the real interpreter's current, already-resolved absolute path. Returns
177
+ {name: written_path}; raises FileNotFoundError naming whichever of bash/sh isn't on PATH at
178
+ all, since a wrapper with nothing real to call through to would only break the shell.
179
+
180
+ Installing the *directory* onto PATH (ahead of the system one) is the caller's job -- this
181
+ only ever writes files under `bin_dir`, never touches PATH, an rc file, or anything outside it.
182
+ """
183
+ import shutil as _shutil
184
+ import stat
185
+
186
+ which = which or _shutil.which
187
+ target = bin_dir or WRAPPER_DIR
188
+ os.makedirs(target, exist_ok=True)
189
+ written: dict[str, str] = {}
190
+ for name in ("bash", "sh"):
191
+ real = which(name)
192
+ if not real or os.path.dirname(os.path.abspath(real)) == os.path.abspath(target):
193
+ raise FileNotFoundError(
194
+ f"no real {name!r} found on PATH outside {target} to wrap -- refusing to install "
195
+ "a wrapper that could only ever call itself"
196
+ )
197
+ path = os.path.join(target, name)
198
+ with open(path, "w", encoding="utf-8") as fh:
199
+ fh.write(wrapper_script(name, os.path.abspath(real)))
200
+ os.chmod(path, os.stat(path).st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
201
+ written[name] = path
202
+ return written
203
+
204
+
205
+ def default_log(now: datetime | None = None) -> str:
206
+ stamp = (now or datetime.now()).strftime("%Y-%m-%d")
207
+ return os.path.join(LOG_DIR, f"{socket.gethostname().split('.')[0]}-{stamp}.jsonl")
208
+
209
+
210
+ def record_line(env: dict[str, str] | None = None) -> str:
211
+ """Build one wire line from the recorder's environment. Called by `custos-code _record-line`."""
212
+ e = dict(os.environ if env is None else env)
213
+ event = e.get("CUSTOS_CODE_EVENT", "end")
214
+ now = float(e.get("CUSTOS_CODE_TS") or datetime.now(tz=UTC).timestamp())
215
+ # the snippets pass the shell's own pid; without them, this process's parent *is* that shell
216
+ pid = _pid(e.get("CUSTOS_CODE_PID", "")) or os.getppid()
217
+ line: dict[str, Any] = {
218
+ "recorder": "custos-code-machine",
219
+ "v": WIRE_VERSION,
220
+ "event": event,
221
+ "ts": now,
222
+ "cmd": e.get("CUSTOS_CODE_CMD", ""),
223
+ "cwd": e.get("PWD", ""),
224
+ "tty": e.get("CUSTOS_CODE_TTY", ""),
225
+ "pid": pid,
226
+ "ppid": _pid(e.get("CUSTOS_CODE_PPID", "")),
227
+ }
228
+ if event == "end":
229
+ rc = e.get("CUSTOS_CODE_RC", "")
230
+ line["exit"] = int(rc) if rc.lstrip("-").isdigit() else None
231
+ try:
232
+ line["dur_ms"] = int((now - float(e["CUSTOS_CODE_T0"])) * 1000)
233
+ except (KeyError, ValueError):
234
+ line["dur_ms"] = None
235
+ return json.dumps(redact(line), sort_keys=True)
236
+
237
+
238
+ def _pid(value: str) -> int | None:
239
+ return int(value) if value.isdigit() else None
240
+
241
+
242
+ def _paths(record: dict[str, Any], cwd: str | None) -> list[str]:
243
+ path = record.get("path")
244
+ if not isinstance(path, str) or not path:
245
+ return []
246
+ return [path if os.path.isabs(path) or not cwd else os.path.normpath(os.path.join(cwd, path))]
247
+
248
+
249
+ def parse(path: str) -> tuple[Session, list[LedgerEvent], str | None]:
250
+ """Machine log -> ledger. There is no report here: class M gets it from the UI or the PR."""
251
+ events: list[LedgerEvent] = []
252
+ seq = 0
253
+ started: datetime | None = None
254
+ ended = datetime.fromtimestamp(0, tz=UTC)
255
+ cwd: str | None = None
256
+ host = os.path.basename(path).rsplit("-", 3)[0]
257
+ session_id = f"machine:{os.path.basename(path)}"
258
+ open_calls: dict[str, int] = {}
259
+
260
+ with open(path, encoding="utf-8") as fh:
261
+ for line in fh:
262
+ line = line.strip()
263
+ if not line:
264
+ continue
265
+ try:
266
+ rec = json.loads(line)
267
+ except json.JSONDecodeError:
268
+ continue
269
+ if not isinstance(rec, dict) or rec.get("recorder") not in RECORDER_NAMES:
270
+ continue
271
+ ts = datetime.fromtimestamp(float(rec.get("ts") or 0), tz=UTC)
272
+ started = started or ts
273
+ ended = max(ended, ts)
274
+ cwd = str(rec.get("cwd") or cwd or "") or None
275
+ kind_name = str(rec.get("event", ""))
276
+ command = str(rec.get("cmd", ""))
277
+ key = f"{rec.get('pid')}:{command}"
278
+
279
+ if kind_name == "start":
280
+ events.append(
281
+ LedgerEvent(
282
+ seq=seq,
283
+ ts=ts,
284
+ session_id=session_id,
285
+ kind=EventKind.CALL,
286
+ tool="Bash",
287
+ cwd=cwd,
288
+ input=redact(
289
+ {"command": command, "ppid_chain": rec.get("ppid_chain") or []}
290
+ ),
291
+ flags=EventFlags(piped=is_piped(command)),
292
+ )
293
+ )
294
+ open_calls[key] = seq
295
+ seq += 1
296
+ continue
297
+
298
+ if kind_name == "end":
299
+ exit_code = rec.get("exit")
300
+ event = LedgerEvent(
301
+ seq=seq,
302
+ ts=ts,
303
+ session_id=session_id,
304
+ kind=EventKind.RESULT,
305
+ tool="Bash",
306
+ cwd=cwd,
307
+ input=redact({"command": command}),
308
+ exit_code=int(exit_code) if isinstance(exit_code, int) else None,
309
+ duration_ms=rec.get("dur_ms") if isinstance(rec.get("dur_ms"), int) else None,
310
+ flags=EventFlags(piped=is_piped(command), error=bool(exit_code)),
311
+ )
312
+ out = rec.get("out")
313
+ if isinstance(out, str) and out:
314
+ blob = str(redact(out))
315
+ raw = blob.encode()
316
+ event.output_hash = hashlib.sha256(raw).hexdigest()
317
+ event.output = raw[:MAX_OUTPUT_BYTES].decode(errors="ignore")
318
+ event.flags.truncated = len(raw) > MAX_OUTPUT_BYTES
319
+ else:
320
+ # the shell trap sees status, never stdout: outcome is known, output is not
321
+ event.flags.stderr_dropped = True
322
+ open_calls.pop(key, None)
323
+ events.append(event)
324
+ seq += 1
325
+ continue
326
+
327
+ if kind_name == "fs":
328
+ events.append(
329
+ LedgerEvent(
330
+ seq=seq,
331
+ ts=ts,
332
+ session_id=session_id,
333
+ kind=EventKind.RESULT,
334
+ tool="Edit",
335
+ cwd=cwd,
336
+ paths=_paths(rec, cwd),
337
+ input={"op": str(rec.get("op", "modified")), "watcher": "fs"},
338
+ exit_code=0,
339
+ )
340
+ )
341
+ seq += 1
342
+ continue
343
+
344
+ if kind_name == "git":
345
+ events.append(
346
+ LedgerEvent(
347
+ seq=seq,
348
+ ts=ts,
349
+ session_id=session_id,
350
+ kind=EventKind.RESULT,
351
+ tool="Git",
352
+ cwd=cwd,
353
+ exit_code=0,
354
+ input=redact(
355
+ {
356
+ "ref": str(rec.get("ref", "")),
357
+ "from": str(rec.get("from", "")),
358
+ "to": str(rec.get("to", "")),
359
+ "subject": str(rec.get("subject", "")),
360
+ }
361
+ ),
362
+ )
363
+ )
364
+ seq += 1
365
+
366
+ chained = chain(events)
367
+ meta = Session(
368
+ id=session_id,
369
+ source="machine",
370
+ agent=f"unknown ({host})",
371
+ started=started,
372
+ ended=ended,
373
+ cwd=cwd,
374
+ n_events=len(chained),
375
+ ledger_root_hash=chained[-1].hash if chained else "",
376
+ # no tool attribution and no captured stdout: honest about what this class can settle
377
+ integrity_score=0.6,
378
+ )
379
+ return meta, chained, None