loki-mode 7.18.1 → 7.18.3
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.
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/crash.sh +164 -0
- package/autonomy/lib/crash_capture.py +286 -0
- package/autonomy/lib/crash_redact.py +509 -0
- package/autonomy/loki +248 -12
- package/autonomy/run.sh +56 -2
- package/autonomy/telemetry.sh +11 -0
- package/bin/loki +3 -1
- package/bin/postinstall.js +15 -1
- package/dashboard/__init__.py +1 -1
- package/dashboard/telemetry.py +15 -0
- package/docs/CRASH-REPORTING-PLAN.md +527 -0
- package/docs/INSTALLATION.md +1 -1
- package/docs/PRIVACY.md +145 -0
- package/loki-ts/dist/loki.js +265 -226
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Autonomous spec-to-product system. Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product via the RARV-C closure loop, with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v7.18.
|
|
6
|
+
# Loki Mode v7.18.3
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -383,4 +383,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
383
383
|
|
|
384
384
|
---
|
|
385
385
|
|
|
386
|
-
**v7.18.
|
|
386
|
+
**v7.18.3 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.18.
|
|
1
|
+
7.18.3
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Loki Mode crash-reporting bash helpers (Phase 0: local-only, zero egress).
|
|
3
|
+
#
|
|
4
|
+
# This module is the single source of truth for the unified opt-out that gates
|
|
5
|
+
# BOTH PostHog usage telemetry and local crash capture. It is sourceable for
|
|
6
|
+
# helpers only; it executes nothing on source.
|
|
7
|
+
#
|
|
8
|
+
# Opt-out (any one disables collection):
|
|
9
|
+
# - LOKI_TELEMETRY=off (case-insensitive)
|
|
10
|
+
# - LOKI_TELEMETRY_DISABLED=true
|
|
11
|
+
# - DO_NOT_TRACK=1
|
|
12
|
+
# - ~/.loki/config line: TELEMETRY_DISABLED=true
|
|
13
|
+
#
|
|
14
|
+
# All capture is best-effort: it never blocks the parent and always returns 0.
|
|
15
|
+
# Phase 0 has zero network egress, so local capture is always safe and useful
|
|
16
|
+
# for the user to self-inspect what WOULD be sent.
|
|
17
|
+
|
|
18
|
+
# Double-source guard.
|
|
19
|
+
if [ -n "${_LOKI_CRASH_SH_SOURCED:-}" ]; then
|
|
20
|
+
return 0 2>/dev/null || true
|
|
21
|
+
fi
|
|
22
|
+
_LOKI_CRASH_SH_SOURCED=1
|
|
23
|
+
|
|
24
|
+
# Self-locate so we do not depend on the caller's SCRIPT_DIR / _LOKI_SCRIPT_DIR.
|
|
25
|
+
_LOKI_CRASH_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
26
|
+
_LOKI_CRASH_CAPTURE_PY="${_LOKI_CRASH_DIR}/lib/crash_capture.py"
|
|
27
|
+
|
|
28
|
+
# loki_collection_enabled: returns 0 (enabled) unless disabled by any switch.
|
|
29
|
+
# This is the SINGLE source of truth for telemetry + crash collection state.
|
|
30
|
+
loki_collection_enabled() {
|
|
31
|
+
# Env: LOKI_TELEMETRY=off (case-insensitive)
|
|
32
|
+
local telem_lower
|
|
33
|
+
telem_lower="$(printf '%s' "${LOKI_TELEMETRY:-}" | tr '[:upper:]' '[:lower:]')"
|
|
34
|
+
[ "$telem_lower" = "off" ] && return 1
|
|
35
|
+
|
|
36
|
+
# Env: LOKI_TELEMETRY_DISABLED=true
|
|
37
|
+
[ "${LOKI_TELEMETRY_DISABLED:-}" = "true" ] && return 1
|
|
38
|
+
|
|
39
|
+
# Env: DO_NOT_TRACK=1 (community standard)
|
|
40
|
+
[ "${DO_NOT_TRACK:-}" = "1" ] && return 1
|
|
41
|
+
|
|
42
|
+
# Persistent opt-out in ~/.loki/config (matches autonomy/run.sh:643 format).
|
|
43
|
+
if [ -f "${HOME}/.loki/config" ] && grep -q "^TELEMETRY_DISABLED=true" "${HOME}/.loki/config" 2>/dev/null; then
|
|
44
|
+
return 1
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
return 0
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# _loki_crash_python: resolve a python3 interpreter, or return 1 if absent.
|
|
51
|
+
_loki_crash_python() {
|
|
52
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
53
|
+
printf 'python3'
|
|
54
|
+
return 0
|
|
55
|
+
fi
|
|
56
|
+
return 1
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# loki_crash_capture <error_class> <message> <stack> <rarv_phase> <exit_code>
|
|
60
|
+
# Best-effort local capture. Always returns 0. Never blocks the parent.
|
|
61
|
+
# Gated by the unified opt-out: if the user has opted out (LOKI_TELEMETRY=off /
|
|
62
|
+
# DO_NOT_TRACK=1 / loki telemetry off / config TELEMETRY_DISABLED=true), NO
|
|
63
|
+
# local crash file is written, matching the first-run disclosure and
|
|
64
|
+
# docs/PRIVACY.md promise that opt-out disables crash reporting.
|
|
65
|
+
loki_crash_capture() {
|
|
66
|
+
loki_collection_enabled || return 0
|
|
67
|
+
|
|
68
|
+
local error_class="${1:-UnknownError}"
|
|
69
|
+
local message="${2:-}"
|
|
70
|
+
local stack="${3:-}"
|
|
71
|
+
local rarv_phase="${4:-}"
|
|
72
|
+
local exit_code="${5:-}"
|
|
73
|
+
|
|
74
|
+
local py
|
|
75
|
+
py="$(_loki_crash_python)" || return 0
|
|
76
|
+
[ -f "$_LOKI_CRASH_CAPTURE_PY" ] || return 0
|
|
77
|
+
|
|
78
|
+
# Bound the stack so we never pass an oversized argv (E2BIG). The capture
|
|
79
|
+
# script reads the stack from stdin. Keep the last 200 lines, 16 KB cap.
|
|
80
|
+
local bounded_stack
|
|
81
|
+
bounded_stack="$(printf '%s' "$stack" | tail -c 16384 2>/dev/null)"
|
|
82
|
+
|
|
83
|
+
local args=(
|
|
84
|
+
"$_LOKI_CRASH_CAPTURE_PY"
|
|
85
|
+
--error-class "$error_class"
|
|
86
|
+
--message "$message"
|
|
87
|
+
--rarv-phase "$rarv_phase"
|
|
88
|
+
--exit-code "$exit_code"
|
|
89
|
+
--target-dir "${TARGET_DIR:-.}"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Short timeout if available; stock macOS has no `timeout`, so run bare then.
|
|
93
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
94
|
+
printf '%s' "$bounded_stack" | timeout 5 "$py" "${args[@]}" --stack - >/dev/null 2>&1 || true
|
|
95
|
+
else
|
|
96
|
+
printf '%s' "$bounded_stack" | "$py" "${args[@]}" --stack - >/dev/null 2>&1 || true
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
return 0
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# loki_crash_friction <friction_kind> <context>
|
|
103
|
+
# Conservative: caller decides when a real threshold is hit. Best-effort.
|
|
104
|
+
# Always returns 0. friction_kind is one of: retry_loop | rate_limit_loop |
|
|
105
|
+
# gate_failure. The context string is mapped into --message (the capture
|
|
106
|
+
# contract has no --context arg). Gated by the unified opt-out: if the user has
|
|
107
|
+
# opted out, NO local friction file is written either.
|
|
108
|
+
loki_crash_friction() {
|
|
109
|
+
loki_collection_enabled || return 0
|
|
110
|
+
|
|
111
|
+
local friction_kind="${1:-unknown}"
|
|
112
|
+
local context="${2:-}"
|
|
113
|
+
|
|
114
|
+
local py
|
|
115
|
+
py="$(_loki_crash_python)" || return 0
|
|
116
|
+
[ -f "$_LOKI_CRASH_CAPTURE_PY" ] || return 0
|
|
117
|
+
|
|
118
|
+
local args=(
|
|
119
|
+
"$_LOKI_CRASH_CAPTURE_PY"
|
|
120
|
+
--error-class "Friction"
|
|
121
|
+
--message "$context"
|
|
122
|
+
--friction-kind "$friction_kind"
|
|
123
|
+
--target-dir "${TARGET_DIR:-.}"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
127
|
+
timeout 5 "$py" "${args[@]}" </dev/null >/dev/null 2>&1 || true
|
|
128
|
+
else
|
|
129
|
+
"$py" "${args[@]}" </dev/null >/dev/null 2>&1 || true
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
return 0
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# loki_show_disclosure_once: print the first-run disclosure exactly once.
|
|
136
|
+
# Shown regardless of enabled/disabled state, before any egress. Uses the
|
|
137
|
+
# DISCLOSURE_SHOWN sentinel in ~/.loki/config (do not invent a new file).
|
|
138
|
+
# Safe if HOME is unwritable (best-effort).
|
|
139
|
+
loki_show_disclosure_once() {
|
|
140
|
+
local config="${HOME}/.loki/config"
|
|
141
|
+
|
|
142
|
+
# Already shown? Never re-show.
|
|
143
|
+
if [ -f "$config" ] && grep -q "^DISCLOSURE_SHOWN=true" "$config" 2>/dev/null; then
|
|
144
|
+
return 0
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
# Disclosure copy (plan section 6a, verbatim; no emojis, no em dashes).
|
|
148
|
+
{
|
|
149
|
+
echo ""
|
|
150
|
+
echo "Loki Mode auto-creates the issues you hit at github.com/asklokesh/loki-mode"
|
|
151
|
+
echo "and tries to auto-resolve them. If it cannot, we encourage you to open an"
|
|
152
|
+
echo "issue for anything causing hesitation."
|
|
153
|
+
echo "We send anonymous diagnostics only (os, arch, version, error type, sanitized"
|
|
154
|
+
echo "stack signatures). Never your code, prompts, paths, keys, or repo names."
|
|
155
|
+
echo "See docs/PRIVACY.md. Turn this off anytime with: loki telemetry off"
|
|
156
|
+
echo ""
|
|
157
|
+
} >&2
|
|
158
|
+
|
|
159
|
+
# Persist the sentinel (best-effort; never fail the caller).
|
|
160
|
+
mkdir -p "${HOME}/.loki" 2>/dev/null || return 0
|
|
161
|
+
echo "DISCLOSURE_SHOWN=true" >> "$config" 2>/dev/null || true
|
|
162
|
+
|
|
163
|
+
return 0
|
|
164
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Crash context capture for Loki Mode (Phase 0, local-only, no egress).
|
|
3
|
+
|
|
4
|
+
Builds the raw pre-scrub crash context, runs it through the shared scrubber
|
|
5
|
+
(crash_redact.scrub_and_whitelist), and writes the WHITELISTED payload to
|
|
6
|
+
.loki/crash/<fingerprint>-<unixts>.json. Never writes unscrubbed data. There is
|
|
7
|
+
no network egress in Phase 0.
|
|
8
|
+
|
|
9
|
+
FAIL CLOSED: if any step fails such that we cannot guarantee a scrubbed result,
|
|
10
|
+
exit nonzero and write nothing rather than risk a leak. If the scrubber returns
|
|
11
|
+
its safe minimal (ScrubError) shape we still write that record so we have a
|
|
12
|
+
trace, but it carries no raw data.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import platform
|
|
19
|
+
import shutil
|
|
20
|
+
import subprocess
|
|
21
|
+
import sys
|
|
22
|
+
import time
|
|
23
|
+
from datetime import datetime, timezone
|
|
24
|
+
|
|
25
|
+
# Make crash_redact importable regardless of cwd (same trick as
|
|
26
|
+
# proof-generator.py lines 32-37).
|
|
27
|
+
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
28
|
+
if _HERE not in sys.path:
|
|
29
|
+
sys.path.insert(0, _HERE)
|
|
30
|
+
|
|
31
|
+
import crash_redact # noqa: E402
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _read_loki_version():
|
|
35
|
+
"""Read VERSION from the repo root (../../VERSION relative to this file).
|
|
36
|
+
|
|
37
|
+
Best-effort; returns None if unreadable.
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
path = os.path.normpath(os.path.join(_HERE, "..", "..", "VERSION"))
|
|
41
|
+
with open(path, "r") as f:
|
|
42
|
+
v = f.read().strip()
|
|
43
|
+
return v or None
|
|
44
|
+
except Exception:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _discover_runtime_version(env_keys, cmd):
|
|
49
|
+
"""Best-effort runtime version. Tries env keys first, then `<cmd> --version`.
|
|
50
|
+
|
|
51
|
+
Never raises; returns None on any failure.
|
|
52
|
+
"""
|
|
53
|
+
for key in env_keys:
|
|
54
|
+
val = os.environ.get(key)
|
|
55
|
+
if val:
|
|
56
|
+
return val.strip()
|
|
57
|
+
try:
|
|
58
|
+
exe = shutil.which(cmd)
|
|
59
|
+
if not exe:
|
|
60
|
+
return None
|
|
61
|
+
proc = subprocess.run(
|
|
62
|
+
[exe, "--version"],
|
|
63
|
+
capture_output=True,
|
|
64
|
+
text=True,
|
|
65
|
+
timeout=3,
|
|
66
|
+
)
|
|
67
|
+
out = (proc.stdout or proc.stderr or "").strip()
|
|
68
|
+
return out.splitlines()[0].strip() if out else None
|
|
69
|
+
except Exception:
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _discover_git_remote(cwd=None):
|
|
74
|
+
"""Best-effort git remote origin URL via git config. Never raises."""
|
|
75
|
+
try:
|
|
76
|
+
target = cwd or os.getcwd()
|
|
77
|
+
exe = shutil.which("git")
|
|
78
|
+
if not exe:
|
|
79
|
+
return None
|
|
80
|
+
proc = subprocess.run(
|
|
81
|
+
[exe, "-C", target, "config", "--get", "remote.origin.url"],
|
|
82
|
+
capture_output=True,
|
|
83
|
+
text=True,
|
|
84
|
+
timeout=3,
|
|
85
|
+
)
|
|
86
|
+
out = (proc.stdout or "").strip()
|
|
87
|
+
return out or None
|
|
88
|
+
except Exception:
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _captured_at():
|
|
93
|
+
"""UTC timestamp, second precision. Uses datetime.now(timezone.utc) (NOT the
|
|
94
|
+
banned utcnow())."""
|
|
95
|
+
try:
|
|
96
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
97
|
+
except Exception:
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def build_raw_context(
|
|
102
|
+
error_class,
|
|
103
|
+
message,
|
|
104
|
+
stack,
|
|
105
|
+
rarv_phase=None,
|
|
106
|
+
exit_code=None,
|
|
107
|
+
friction_kind=None,
|
|
108
|
+
):
|
|
109
|
+
"""Assemble the raw, pre-scrub crash context.
|
|
110
|
+
|
|
111
|
+
It is FINE for raw to contain unsafe data (message, full stack with paths,
|
|
112
|
+
git remote). The scrub step strips and whitelists everything next. The
|
|
113
|
+
returned dict is NEVER written to disk directly.
|
|
114
|
+
"""
|
|
115
|
+
raw = {
|
|
116
|
+
"os": platform.system() or None,
|
|
117
|
+
"arch": platform.machine() or None,
|
|
118
|
+
"loki_version": _read_loki_version(),
|
|
119
|
+
"node_version": _discover_runtime_version(
|
|
120
|
+
["LOKI_NODE_VERSION", "NODE_VERSION"], "node"
|
|
121
|
+
),
|
|
122
|
+
"bun_version": _discover_runtime_version(
|
|
123
|
+
["LOKI_BUN_VERSION", "BUN_VERSION"], "bun"
|
|
124
|
+
),
|
|
125
|
+
"error_class": error_class,
|
|
126
|
+
"message": message,
|
|
127
|
+
# Raw frames; normalize_stack inside scrub reduces these to symbols.
|
|
128
|
+
"stack": stack if isinstance(stack, list) else (
|
|
129
|
+
stack.splitlines() if isinstance(stack, str) else []
|
|
130
|
+
),
|
|
131
|
+
"rarv_phase": rarv_phase,
|
|
132
|
+
"exit_code": exit_code,
|
|
133
|
+
"friction_kind": friction_kind,
|
|
134
|
+
"captured_at": _captured_at(),
|
|
135
|
+
}
|
|
136
|
+
return raw
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def capture(
|
|
140
|
+
error_class,
|
|
141
|
+
message,
|
|
142
|
+
stack,
|
|
143
|
+
rarv_phase=None,
|
|
144
|
+
exit_code=None,
|
|
145
|
+
friction_kind=None,
|
|
146
|
+
target_dir=None,
|
|
147
|
+
):
|
|
148
|
+
"""Build raw context, scrub, and write the whitelisted payload.
|
|
149
|
+
|
|
150
|
+
Writes to <target_dir or cwd>/.loki/crash/<fingerprint>-<unixts>.json.
|
|
151
|
+
Returns the written path, or None if the write itself failed.
|
|
152
|
+
|
|
153
|
+
NEVER writes unscrubbed data. If scrub returns the safe minimal (ScrubError)
|
|
154
|
+
shape, that minimal dict is still written (under a scruberror-<ts> name,
|
|
155
|
+
since it carries no fingerprint) so a trace exists with no leak.
|
|
156
|
+
"""
|
|
157
|
+
raw = build_raw_context(
|
|
158
|
+
error_class=error_class,
|
|
159
|
+
message=message,
|
|
160
|
+
stack=stack,
|
|
161
|
+
rarv_phase=rarv_phase,
|
|
162
|
+
exit_code=exit_code,
|
|
163
|
+
friction_kind=friction_kind,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
base = target_dir or os.getcwd()
|
|
167
|
+
home = os.environ.get("HOME")
|
|
168
|
+
repo_root = _detect_repo_root(base)
|
|
169
|
+
git_remote = _discover_git_remote(base)
|
|
170
|
+
|
|
171
|
+
scrubbed = crash_redact.scrub_and_whitelist(
|
|
172
|
+
raw,
|
|
173
|
+
home=home,
|
|
174
|
+
repo_root=repo_root,
|
|
175
|
+
git_remote=git_remote,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if not isinstance(scrubbed, dict):
|
|
179
|
+
# Defensive: scrub_and_whitelist always returns a dict, but never trust.
|
|
180
|
+
scrubbed = {
|
|
181
|
+
"error_class": "ScrubError",
|
|
182
|
+
"rules_version": crash_redact.CRASH_RULES_VERSION,
|
|
183
|
+
"redactions_count": 0,
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
ts = int(time.time())
|
|
187
|
+
fingerprint = scrubbed.get("fingerprint")
|
|
188
|
+
if isinstance(fingerprint, str) and fingerprint:
|
|
189
|
+
name = "{}-{}.json".format(fingerprint, ts)
|
|
190
|
+
else:
|
|
191
|
+
# ScrubError shape has no fingerprint; still write a trace.
|
|
192
|
+
name = "scruberror-{}.json".format(ts)
|
|
193
|
+
|
|
194
|
+
crash_dir = os.path.join(base, ".loki", "crash")
|
|
195
|
+
try:
|
|
196
|
+
os.makedirs(crash_dir, exist_ok=True)
|
|
197
|
+
out_path = os.path.join(crash_dir, name)
|
|
198
|
+
with open(out_path, "w") as f:
|
|
199
|
+
json.dump(scrubbed, f, indent=2, sort_keys=True)
|
|
200
|
+
return out_path
|
|
201
|
+
except Exception:
|
|
202
|
+
# Fail closed: if we cannot write the scrubbed file, write nothing.
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _detect_repo_root(start):
|
|
207
|
+
"""Best-effort: walk up from start to find a .git directory. Never raises."""
|
|
208
|
+
try:
|
|
209
|
+
cur = os.path.abspath(start)
|
|
210
|
+
while True:
|
|
211
|
+
if os.path.isdir(os.path.join(cur, ".git")):
|
|
212
|
+
return cur
|
|
213
|
+
parent = os.path.dirname(cur)
|
|
214
|
+
if parent == cur:
|
|
215
|
+
return None
|
|
216
|
+
cur = parent
|
|
217
|
+
except Exception:
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _main():
|
|
222
|
+
parser = argparse.ArgumentParser(
|
|
223
|
+
description="Capture and scrub a Loki Mode crash report (local only)."
|
|
224
|
+
)
|
|
225
|
+
parser.add_argument("--error-class", default="UnknownError")
|
|
226
|
+
parser.add_argument("--message", default="")
|
|
227
|
+
parser.add_argument(
|
|
228
|
+
"--stack",
|
|
229
|
+
default=None,
|
|
230
|
+
help="Stack/traceback text. If omitted, read from stdin.",
|
|
231
|
+
)
|
|
232
|
+
parser.add_argument("--rarv-phase", default=None)
|
|
233
|
+
parser.add_argument("--exit-code", default=None)
|
|
234
|
+
parser.add_argument("--friction-kind", default=None)
|
|
235
|
+
parser.add_argument("--target-dir", default=None)
|
|
236
|
+
args = parser.parse_args()
|
|
237
|
+
|
|
238
|
+
stack_text = args.stack
|
|
239
|
+
# Read the stack from stdin when --stack is omitted (None) OR when it is the
|
|
240
|
+
# explicit "-" sentinel. The bash hook (autonomy/crash.sh) passes
|
|
241
|
+
# `--stack -` while piping the real stack to stdin; treating "-" as the
|
|
242
|
+
# stdin sentinel keeps the bash and TS routes producing the same
|
|
243
|
+
# stack_signature (and therefore the same fingerprint) for one crash.
|
|
244
|
+
if stack_text is None or stack_text == "-":
|
|
245
|
+
try:
|
|
246
|
+
if not sys.stdin.isatty():
|
|
247
|
+
stack_text = sys.stdin.read()
|
|
248
|
+
else:
|
|
249
|
+
stack_text = ""
|
|
250
|
+
except Exception:
|
|
251
|
+
stack_text = ""
|
|
252
|
+
stack_text = stack_text or ""
|
|
253
|
+
|
|
254
|
+
exit_code = args.exit_code
|
|
255
|
+
if exit_code is not None:
|
|
256
|
+
try:
|
|
257
|
+
exit_code = int(exit_code)
|
|
258
|
+
except (TypeError, ValueError):
|
|
259
|
+
# Keep as-is; scrub drops it unless whitelisted, and it is.
|
|
260
|
+
pass
|
|
261
|
+
|
|
262
|
+
path = capture(
|
|
263
|
+
error_class=args.error_class,
|
|
264
|
+
message=args.message,
|
|
265
|
+
stack=stack_text,
|
|
266
|
+
rarv_phase=args.rarv_phase,
|
|
267
|
+
exit_code=exit_code,
|
|
268
|
+
friction_kind=args.friction_kind,
|
|
269
|
+
target_dir=args.target_dir,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
if path is None:
|
|
273
|
+
# Fail closed: nothing written, signal failure.
|
|
274
|
+
sys.exit(1)
|
|
275
|
+
print(path)
|
|
276
|
+
sys.exit(0)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
if __name__ == "__main__":
|
|
280
|
+
try:
|
|
281
|
+
_main()
|
|
282
|
+
except SystemExit:
|
|
283
|
+
raise
|
|
284
|
+
except Exception:
|
|
285
|
+
# FAIL CLOSED: any unexpected failure exits nonzero, writes nothing.
|
|
286
|
+
sys.exit(1)
|