loki-mode 7.91.0 → 7.92.0
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/loki +154 -27
- package/autonomy/provider-offer.sh +8 -1
- package/autonomy/run.sh +615 -13
- package/dashboard/__init__.py +1 -1
- package/dashboard/static/index.html +162 -134
- package/docs/INSTALLATION.md +2 -2
- package/loki-ts/dist/loki.js +5 -5
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
package/autonomy/run.sh
CHANGED
|
@@ -218,6 +218,11 @@ if [[ -z "${LOKI_RUNNING_FROM_TEMP:-}" ]] && [[ "${BASH_SOURCE[0]}" == "${0}" ]]
|
|
|
218
218
|
# BUG-XC-011: Set trap BEFORE exec so the temp file gets cleaned up
|
|
219
219
|
trap 'rm -f "$TEMP_SCRIPT"' EXIT
|
|
220
220
|
export LOKI_RUNNING_FROM_TEMP=1
|
|
221
|
+
# Record the EXACT temp-copy path so the post-exec cleanup trap deletes THIS
|
|
222
|
+
# temp file and NEVER the canonical source (root cause of the recurring
|
|
223
|
+
# "run.sh self-deleted on build spawn" bug: an inherited LOKI_RUNNING_FROM_TEMP
|
|
224
|
+
# skips the self-copy block, leaving BASH_SOURCE[0]=the real run.sh).
|
|
225
|
+
export LOKI_TEMP_SCRIPT_PATH="$TEMP_SCRIPT"
|
|
221
226
|
export LOKI_ORIGINAL_SCRIPT_DIR="$SCRIPT_DIR"
|
|
222
227
|
export LOKI_ORIGINAL_PROJECT_DIR="$PROJECT_DIR"
|
|
223
228
|
exec "$TEMP_SCRIPT" "$@"
|
|
@@ -227,9 +232,14 @@ fi
|
|
|
227
232
|
SCRIPT_DIR="${LOKI_ORIGINAL_SCRIPT_DIR:-$SCRIPT_DIR}"
|
|
228
233
|
PROJECT_DIR="${LOKI_ORIGINAL_PROJECT_DIR:-$PROJECT_DIR}"
|
|
229
234
|
|
|
230
|
-
# Clean up temp
|
|
231
|
-
|
|
232
|
-
|
|
235
|
+
# Clean up ONLY the recorded temp copy, and ONLY if it is a real temp file.
|
|
236
|
+
# Deleting BASH_SOURCE[0] here was the bug: an inherited LOKI_RUNNING_FROM_TEMP
|
|
237
|
+
# made BASH_SOURCE[0] the canonical source, so run.sh deleted itself on spawned
|
|
238
|
+
# builds. Guard on the recorded path being a real /tmp/loki-run-* file.
|
|
239
|
+
if [[ "${LOKI_RUNNING_FROM_TEMP:-}" == "1" ]] \
|
|
240
|
+
&& [[ -n "${LOKI_TEMP_SCRIPT_PATH:-}" ]] \
|
|
241
|
+
&& [[ "${LOKI_TEMP_SCRIPT_PATH}" == /tmp/loki-run-* || "${LOKI_TEMP_SCRIPT_PATH}" == "${TMPDIR:-/tmp}"loki-run-* ]]; then
|
|
242
|
+
trap 'rm -f "${LOKI_TEMP_SCRIPT_PATH}" 2>/dev/null' EXIT
|
|
233
243
|
fi
|
|
234
244
|
|
|
235
245
|
#===============================================================================
|
|
@@ -1040,6 +1050,179 @@ log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
|
|
|
1040
1050
|
log_step() { echo -e "${CYAN}[STEP]${NC} $*"; }
|
|
1041
1051
|
log_debug() { [[ "${LOKI_DEBUG:-}" == "true" ]] && echo -e "${CYAN}[DEBUG]${NC} $*" >&2 || true; }
|
|
1042
1052
|
|
|
1053
|
+
#===============================================================================
|
|
1054
|
+
# Failure diagnosis helpers (T2.4 / T2.5 / T2.6)
|
|
1055
|
+
#
|
|
1056
|
+
# These make a failing or crashed build self-explanatory: a copy-pasteable
|
|
1057
|
+
# "loki why" hint on any non-zero exit, a durable CLASSIFIED LAST_ERROR record
|
|
1058
|
+
# on a failed iteration, and a best-effort terminal record on an untrapped
|
|
1059
|
+
# death. Every one is best-effort and NEVER alters the build's exit code.
|
|
1060
|
+
#===============================================================================
|
|
1061
|
+
|
|
1062
|
+
# _loki_surface_why_hint (T2.4): print the "loki why" hint to stderr and write
|
|
1063
|
+
# it to .loki/NEXT_STEPS.txt. Called once from main() finalization when the run
|
|
1064
|
+
# failed (result != 0). Best-effort: never crashes, never changes exit code.
|
|
1065
|
+
_loki_surface_why_hint() {
|
|
1066
|
+
local loki_dir="${TARGET_DIR:-.}/.loki"
|
|
1067
|
+
local hint="For a plain-language diagnosis of what happened, run: loki why"
|
|
1068
|
+
printf '%s\n' "$hint" >&2 || true
|
|
1069
|
+
mkdir -p "$loki_dir" 2>/dev/null || true
|
|
1070
|
+
printf '%s\n' "$hint" > "$loki_dir/NEXT_STEPS.txt" 2>/dev/null || true
|
|
1071
|
+
return 0
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
# _loki_write_last_error (T2.5): write a durable, classified failure record to
|
|
1075
|
+
# .loki/state/LAST_ERROR.json. Schema:
|
|
1076
|
+
# {
|
|
1077
|
+
# "iteration": <int>,
|
|
1078
|
+
# "error_class": "provider_empty_output"|"build_timeout"|"rate_limited"
|
|
1079
|
+
# |"auth_error"|"unknown",
|
|
1080
|
+
# "brief": "<one honest sentence>",
|
|
1081
|
+
# "timestamp": "<UTC ISO-8601>"
|
|
1082
|
+
# }
|
|
1083
|
+
# This is what `loki why` (in autonomy/loki, NOT owned here) can later read.
|
|
1084
|
+
# Built via python3 so the free-text brief can never break the JSON. Entirely
|
|
1085
|
+
# best-effort: any failure is swallowed and the build is never crashed.
|
|
1086
|
+
# Usage: _loki_write_last_error <iteration> <error_class> <brief>
|
|
1087
|
+
_loki_write_last_error() {
|
|
1088
|
+
local iteration="${1:-0}"
|
|
1089
|
+
local error_class="${2:-unknown}"
|
|
1090
|
+
local brief="${3:-An iteration failed.}"
|
|
1091
|
+
local loki_dir="${TARGET_DIR:-.}/.loki"
|
|
1092
|
+
local state_dir="$loki_dir/state"
|
|
1093
|
+
mkdir -p "$state_dir" 2>/dev/null || true
|
|
1094
|
+
LOKI_LE_ITER="$iteration" \
|
|
1095
|
+
LOKI_LE_CLASS="$error_class" \
|
|
1096
|
+
LOKI_LE_BRIEF="$brief" \
|
|
1097
|
+
LOKI_LE_FILE="$state_dir/LAST_ERROR.json" \
|
|
1098
|
+
python3 -c "
|
|
1099
|
+
import json, os, tempfile
|
|
1100
|
+
try:
|
|
1101
|
+
rec = {
|
|
1102
|
+
'iteration': int(os.environ.get('LOKI_LE_ITER', '0') or 0),
|
|
1103
|
+
'error_class': os.environ.get('LOKI_LE_CLASS', 'unknown'),
|
|
1104
|
+
'brief': os.environ.get('LOKI_LE_BRIEF', ''),
|
|
1105
|
+
'timestamp': __import__('datetime').datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ'),
|
|
1106
|
+
}
|
|
1107
|
+
target = os.environ['LOKI_LE_FILE']
|
|
1108
|
+
d = os.path.dirname(target)
|
|
1109
|
+
fd, tmp = tempfile.mkstemp(dir=d, suffix='.json')
|
|
1110
|
+
with os.fdopen(fd, 'w') as f:
|
|
1111
|
+
json.dump(rec, f)
|
|
1112
|
+
os.replace(tmp, target)
|
|
1113
|
+
except Exception:
|
|
1114
|
+
pass
|
|
1115
|
+
" 2>/dev/null || true
|
|
1116
|
+
return 0
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
# _loki_classify_iteration_error (T2.5 helper): map an iteration's signals to one
|
|
1120
|
+
# of the LAST_ERROR error_class values. Conservative: only returns a specific
|
|
1121
|
+
# class when a signal confidently supports it, else "unknown". Never fabricates
|
|
1122
|
+
# build_timeout (no timeout signal is detected here, so it is reserved for a
|
|
1123
|
+
# caller that has one). Echoes the class on stdout.
|
|
1124
|
+
# Usage: _loki_classify_iteration_error <iter_output_file> <empty_output_flag 0|1>
|
|
1125
|
+
_loki_classify_iteration_error() {
|
|
1126
|
+
local iter_output="${1:-}"
|
|
1127
|
+
local empty_flag="${2:-0}"
|
|
1128
|
+
if [ "$empty_flag" = "1" ]; then
|
|
1129
|
+
echo "provider_empty_output"
|
|
1130
|
+
return 0
|
|
1131
|
+
fi
|
|
1132
|
+
# Rate limit: reuse the same detector the retry path uses.
|
|
1133
|
+
if [ -n "$iter_output" ] && [ -f "$iter_output" ] && detect_rate_limit "$iter_output" 2>/dev/null | grep -qE '^[1-9]'; then
|
|
1134
|
+
echo "rate_limited"
|
|
1135
|
+
return 0
|
|
1136
|
+
fi
|
|
1137
|
+
# Auth error: a clear 401/403/unauthorized in the output tail.
|
|
1138
|
+
if [ -n "$iter_output" ] && [ -f "$iter_output" ]; then
|
|
1139
|
+
local _tail
|
|
1140
|
+
_tail="$(tail -n 40 "$iter_output" 2>/dev/null || true)"
|
|
1141
|
+
if printf '%s\n' "$_tail" | grep -qiE '(http[ /]?40[13]|status[: ]+40[13]|unauthorized|invalid api key|authentication[_ ]error|401 )' 2>/dev/null; then
|
|
1142
|
+
echo "auth_error"
|
|
1143
|
+
return 0
|
|
1144
|
+
fi
|
|
1145
|
+
fi
|
|
1146
|
+
echo "unknown"
|
|
1147
|
+
return 0
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
# _loki_terminal_record (T2.6, SAFE SUBSET): on an untrapped exit where the
|
|
1151
|
+
# persisted run status is still "running" (a true mid-provider-call crash on a
|
|
1152
|
+
# trappable signal -- SIGTERM/SIGINT/SIGHUP via the lock-release trap), leave a
|
|
1153
|
+
# best-effort, classified LAST_ERROR record so a post-crash `loki why` is not
|
|
1154
|
+
# stale. Piggybacks the existing lock-release EXIT trap (see main()) -- it does
|
|
1155
|
+
# NOT install a new broad EXIT trap.
|
|
1156
|
+
#
|
|
1157
|
+
# IMPORTANT (why this does NOT rewrite autonomy-state.json): the resume-detection
|
|
1158
|
+
# block in this file (search "Durable resume") keys the ENT-2 pod-loss resume on
|
|
1159
|
+
# prev_status == "running". Flipping the status to "exited" here would make a
|
|
1160
|
+
# crashed-but-resumable build (LOKI_DURABLE_STATE=1) reset to iteration 0 on the
|
|
1161
|
+
# next start, destroying durable progress. So this writes ONLY the LAST_ERROR
|
|
1162
|
+
# side-record (which is what `loki why` reads) and deliberately leaves the
|
|
1163
|
+
# status untouched. SIGKILL / power-loss are uncatchable (no trap fires); the
|
|
1164
|
+
# ENT-2 durable-resume path covers those. Never alters the exit code; best-effort.
|
|
1165
|
+
_loki_terminal_record() {
|
|
1166
|
+
local state_file
|
|
1167
|
+
state_file="$(_loki_state_file 2>/dev/null)" || return 0
|
|
1168
|
+
[ -n "$state_file" ] || return 0
|
|
1169
|
+
[ -f "$state_file" ] || return 0
|
|
1170
|
+
local _status
|
|
1171
|
+
_status="$(LOKI_TR_FILE="$state_file" python3 -c "
|
|
1172
|
+
import json, os
|
|
1173
|
+
try:
|
|
1174
|
+
print(json.load(open(os.environ['LOKI_TR_FILE'])).get('status','unknown'))
|
|
1175
|
+
except Exception:
|
|
1176
|
+
print('unknown')
|
|
1177
|
+
" 2>/dev/null || echo "unknown")"
|
|
1178
|
+
# Only act on a genuinely mid-flight "running" status. Any settled status
|
|
1179
|
+
# (council_approved, failed, exited, paused, ...) is left untouched.
|
|
1180
|
+
[ "$_status" = "running" ] || return 0
|
|
1181
|
+
# Leave a classified LAST_ERROR so `loki why` has something honest -- WITHOUT
|
|
1182
|
+
# touching autonomy-state.json (preserving the ENT-2 "running" resume signal).
|
|
1183
|
+
_loki_write_last_error "${ITERATION_COUNT:-0}" "unknown" \
|
|
1184
|
+
"The build process exited unexpectedly before finishing (possible crash or kill)." 2>/dev/null || true
|
|
1185
|
+
return 0
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
# _loki_write_rate_limit_signal (T2.7): write a best-effort
|
|
1189
|
+
# .loki/signals/RATE_LIMITED file. This is FORWARD-LAID infrastructure: no
|
|
1190
|
+
# consumer reads it yet (a future dashboard / external watcher could, to tell a
|
|
1191
|
+
# normal provider rate-limit wait apart from a hang). The user-visible signal
|
|
1192
|
+
# today is the log_info line at the wait site; this file is the durable record. Schema:
|
|
1193
|
+
# {"rate_limited": true, "wait_seconds": <int>, "reset_time": "<string>"}
|
|
1194
|
+
# Built via python3 so the reset-time string can never break the JSON. Entirely
|
|
1195
|
+
# best-effort: never crashes, never alters the build.
|
|
1196
|
+
# Usage: _loki_write_rate_limit_signal <wait_seconds> <reset_time>
|
|
1197
|
+
_loki_write_rate_limit_signal() {
|
|
1198
|
+
local wait_seconds="${1:-0}"
|
|
1199
|
+
local reset_time="${2:-}"
|
|
1200
|
+
local signals_dir="${TARGET_DIR:-.}/.loki/signals"
|
|
1201
|
+
mkdir -p "$signals_dir" 2>/dev/null || true
|
|
1202
|
+
LOKI_RL_WAIT="$wait_seconds" \
|
|
1203
|
+
LOKI_RL_RESET="$reset_time" \
|
|
1204
|
+
LOKI_RL_FILE="$signals_dir/RATE_LIMITED" \
|
|
1205
|
+
python3 -c "
|
|
1206
|
+
import json, os, tempfile
|
|
1207
|
+
try:
|
|
1208
|
+
w = os.environ.get('LOKI_RL_WAIT', '0')
|
|
1209
|
+
try:
|
|
1210
|
+
w = int(w)
|
|
1211
|
+
except Exception:
|
|
1212
|
+
w = 0
|
|
1213
|
+
rec = {'rate_limited': True, 'wait_seconds': w, 'reset_time': os.environ.get('LOKI_RL_RESET', '')}
|
|
1214
|
+
target = os.environ['LOKI_RL_FILE']
|
|
1215
|
+
d = os.path.dirname(target)
|
|
1216
|
+
fd, tmp = tempfile.mkstemp(dir=d, suffix='.json')
|
|
1217
|
+
with os.fdopen(fd, 'w') as f:
|
|
1218
|
+
json.dump(rec, f)
|
|
1219
|
+
os.replace(tmp, target)
|
|
1220
|
+
except Exception:
|
|
1221
|
+
pass
|
|
1222
|
+
" 2>/dev/null || true
|
|
1223
|
+
return 0
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1043
1226
|
# Live Build HUD (v7.71.0): a single append-only per-iteration status line on the
|
|
1044
1227
|
# interactive TTY path. Pure additive stdout decoration -- never piped into any
|
|
1045
1228
|
# tee, so the dashboard agent.log and the stream-json parser are untouched. The
|
|
@@ -1384,6 +1567,34 @@ emit_event_json() {
|
|
|
1384
1567
|
log_debug "Event: $event_type - $json_data"
|
|
1385
1568
|
}
|
|
1386
1569
|
|
|
1570
|
+
# Per-stage timeline event (v7.91.x). Emits one stage_complete record per
|
|
1571
|
+
# quality-gate / build stage so the SaaS timeline can show where build time
|
|
1572
|
+
# goes within an iteration (the provider call itself is already bracketed by
|
|
1573
|
+
# iteration_start/iteration_complete). This is purely ADDITIVE: it appends one
|
|
1574
|
+
# event line via emit_event_json and never changes a gate verdict, gate exit
|
|
1575
|
+
# code, or control flow. Duration is computed by the caller (whole-second
|
|
1576
|
+
# resolution, sufficient for multi-second gates) and carried on the event so
|
|
1577
|
+
# the SaaS does not have to infer it from coarse ISO timestamps.
|
|
1578
|
+
# emit_stage_complete <stage_name> <status: pass|fail> <start_epoch_seconds>
|
|
1579
|
+
# Event shape:
|
|
1580
|
+
# {type:"stage_complete", timestamp, data:{stage, status, duration_s, iteration}}
|
|
1581
|
+
# Best-effort: any failure is swallowed so it can never block the build.
|
|
1582
|
+
emit_stage_complete() {
|
|
1583
|
+
local stage="$1"
|
|
1584
|
+
local status="$2"
|
|
1585
|
+
local t0="$3"
|
|
1586
|
+
local now dur
|
|
1587
|
+
now=$(date +%s 2>/dev/null) || return 0
|
|
1588
|
+
[ -n "$t0" ] || return 0
|
|
1589
|
+
dur=$(( now - t0 ))
|
|
1590
|
+
[ "$dur" -ge 0 ] 2>/dev/null || dur=0
|
|
1591
|
+
emit_event_json "stage_complete" \
|
|
1592
|
+
"stage=$stage" \
|
|
1593
|
+
"status=$status" \
|
|
1594
|
+
"duration_s=$dur" \
|
|
1595
|
+
"iteration=${ITERATION_COUNT:-0}" 2>/dev/null || true
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1387
1598
|
# Trust-layer metrics event writer (benchmark program section 3). Appends one
|
|
1388
1599
|
# durable record per trust event to .loki/metrics/trust-events.jsonl via the
|
|
1389
1600
|
# Python writer (single source of truth for the JSONL schema). This is ADDITIVE
|
|
@@ -1760,9 +1971,93 @@ get_iteration_duration_ms() {
|
|
|
1760
1971
|
# Supports Docker/K8s secret file mounts as fallback.
|
|
1761
1972
|
#===============================================================================
|
|
1762
1973
|
|
|
1974
|
+
# Zero-friction preflight helpers (T1.1). These run for ALL environments
|
|
1975
|
+
# (not just Docker/K8s) BEFORE the build starts, via validate_api_keys. git is a
|
|
1976
|
+
# genuine hard requirement (the build inits a repo) so a missing git BLOCKS with a
|
|
1977
|
+
# copy-pasteable fix; node-version and network reachability are ADVISORY (warn and
|
|
1978
|
+
# continue, fail-open) so a probabilistic or optional signal never blocks a working
|
|
1979
|
+
# user. The goal: surface real problems early without ever wrongly refusing to start.
|
|
1980
|
+
#
|
|
1981
|
+
# _loki_check_node_version: ADVISORY only. If node is present and its major
|
|
1982
|
+
# version is < 18, log a warning (node only matters for node-based builds) and
|
|
1983
|
+
# continue. If node is absent entirely this is a NO-OP. Always returns 0 -- it
|
|
1984
|
+
# never blocks the build (fail-open); the real node call, if any, is the test.
|
|
1985
|
+
_loki_check_node_version() {
|
|
1986
|
+
command -v node >/dev/null 2>&1 || return 0
|
|
1987
|
+
local node_version major
|
|
1988
|
+
node_version="$(node --version 2>/dev/null || echo '')"
|
|
1989
|
+
# node --version -> "v20.11.0"; extract the leading major integer.
|
|
1990
|
+
major="$(printf '%s' "$node_version" | sed -E 's/^v?([0-9]+).*/\1/')"
|
|
1991
|
+
# Advisory only: node is OPTIONAL (absence is a no-op above, and many builds -
|
|
1992
|
+
# Python/Go/Rust - never touch node). A present-but-old node only matters for
|
|
1993
|
+
# node-based builds, so WARN and continue (fail-open); never hard-block a
|
|
1994
|
+
# working user. The actual node call (only for JS/TS work) is the real test.
|
|
1995
|
+
if [ -n "$major" ] && [[ "$major" =~ ^[0-9]+$ ]] && [ "$major" -lt 18 ]; then
|
|
1996
|
+
log_warn "Node.js >= 18 recommended for node-based builds; found ${node_version:-unknown}. Upgrade if your project uses node: https://nodejs.org"
|
|
1997
|
+
fi
|
|
1998
|
+
return 0
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
# _loki_check_git_present: the build initializes a git repo, so git is required.
|
|
2002
|
+
_loki_check_git_present() {
|
|
2003
|
+
if ! command -v git >/dev/null 2>&1; then
|
|
2004
|
+
log_error "Git is required (the build initializes a repo). Install: https://git-scm.com/downloads"
|
|
2005
|
+
return 1
|
|
2006
|
+
fi
|
|
2007
|
+
return 0
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
# _loki_check_network_reachable: ADVISORY only. A fast (3s) reachability probe to
|
|
2011
|
+
# the active provider endpoint that WARNS and continues if it cannot reach it -- a
|
|
2012
|
+
# curl failure does not prove the provider CLI cannot connect (3s timeout under
|
|
2013
|
+
# load, transient DNS, or a proxy set for the CLI but not the shell all curl-fail
|
|
2014
|
+
# while the real build succeeds). Always returns 0 (fail-open); the actual
|
|
2015
|
+
# provider call is the authoritative connectivity test. Skipped entirely when curl
|
|
2016
|
+
# is missing, when LOKI_SKIP_NET_PREFLIGHT=1, when ANTHROPIC_BASE_URL is set (alt
|
|
2017
|
+
# provider endpoint we cannot assume), or for any provider whose endpoint we do
|
|
2018
|
+
# not know. It NEVER blocks the build.
|
|
2019
|
+
_loki_check_network_reachable() {
|
|
2020
|
+
local provider="${1:-claude}"
|
|
2021
|
+
[ "${LOKI_SKIP_NET_PREFLIGHT:-}" = "1" ] && return 0
|
|
2022
|
+
command -v curl >/dev/null 2>&1 || return 0
|
|
2023
|
+
# Alternate provider base URL set -> do not assume the default endpoint.
|
|
2024
|
+
[ -n "${ANTHROPIC_BASE_URL:-}" ] && return 0
|
|
2025
|
+
|
|
2026
|
+
local endpoint=""
|
|
2027
|
+
case "$provider" in
|
|
2028
|
+
claude) endpoint="https://api.anthropic.com" ;;
|
|
2029
|
+
*) return 0 ;; # unknown endpoint -> fail open, never guess
|
|
2030
|
+
esac
|
|
2031
|
+
|
|
2032
|
+
# Advisory only: a fast curl probe failing does NOT prove the provider CLI
|
|
2033
|
+
# cannot connect (a 3s timeout under load, transient DNS, or a proxy set for
|
|
2034
|
+
# the CLI but not the shell all curl-fail while the real build succeeds). WARN
|
|
2035
|
+
# and continue - the actual provider call is the authoritative connectivity
|
|
2036
|
+
# test. Silence this with LOKI_SKIP_NET_PREFLIGHT=1. Never hard-block here.
|
|
2037
|
+
if ! curl -sS -m 3 -o /dev/null "$endpoint" 2>/dev/null; then
|
|
2038
|
+
log_warn "Could not verify network reachability to the AI provider (firewall/VPN/transient?). Continuing; the provider call will be the real test. Silence with LOKI_SKIP_NET_PREFLIGHT=1."
|
|
2039
|
+
fi
|
|
2040
|
+
return 0
|
|
2041
|
+
}
|
|
2042
|
+
|
|
1763
2043
|
validate_api_keys() {
|
|
1764
2044
|
local provider="${LOKI_PROVIDER:-claude}"
|
|
1765
2045
|
|
|
2046
|
+
# Zero-friction preflight (T1.1): toolchain + reachability checks that apply
|
|
2047
|
+
# to EVERY environment, run BEFORE the Docker/K8s early-return below so they
|
|
2048
|
+
# are not silently skipped in the common local case. Node/git are genuinely
|
|
2049
|
+
# required (node only when present-but-too-old); the network probe is
|
|
2050
|
+
# fail-open and opt-out (LOKI_SKIP_NET_PREFLIGHT=1).
|
|
2051
|
+
if ! _loki_check_node_version; then
|
|
2052
|
+
return 1
|
|
2053
|
+
fi
|
|
2054
|
+
if ! _loki_check_git_present; then
|
|
2055
|
+
return 1
|
|
2056
|
+
fi
|
|
2057
|
+
if ! _loki_check_network_reachable "$provider"; then
|
|
2058
|
+
return 1
|
|
2059
|
+
fi
|
|
2060
|
+
|
|
1766
2061
|
# CLI tools (claude, codex, cline, aider) use their own login sessions.
|
|
1767
2062
|
# Only require API keys inside Docker/K8s where CLI login isn't available.
|
|
1768
2063
|
if [[ ! -f "/.dockerenv" ]] && [[ -z "${KUBERNETES_SERVICE_HOST:-}" ]]; then
|
|
@@ -6189,6 +6484,152 @@ audit_log() {
|
|
|
6189
6484
|
echo "$log_entry" >> "$audit_file"
|
|
6190
6485
|
}
|
|
6191
6486
|
|
|
6487
|
+
#===============================================================================
|
|
6488
|
+
# Engine-owned workspace git-init (Plan #16, Option A-1)
|
|
6489
|
+
#===============================================================================
|
|
6490
|
+
|
|
6491
|
+
# Resolve a path to its physical absolute form (symlinks + .. collapsed) using
|
|
6492
|
+
# whatever is available; falls back to the input unchanged. Portable across the
|
|
6493
|
+
# BSD (macOS) and GNU userlands the engine runs on.
|
|
6494
|
+
_loki_resolve_path() {
|
|
6495
|
+
local p="${1:-}"
|
|
6496
|
+
[ -n "$p" ] || { printf '%s' ""; return 0; }
|
|
6497
|
+
if command -v realpath >/dev/null 2>&1; then
|
|
6498
|
+
realpath "$p" 2>/dev/null && return 0
|
|
6499
|
+
fi
|
|
6500
|
+
# python3 is a hard engine dependency; use it as the portable fallback.
|
|
6501
|
+
python3 - "$p" <<'PYRESOLVE' 2>/dev/null && return 0
|
|
6502
|
+
import os, sys
|
|
6503
|
+
print(os.path.realpath(sys.argv[1]))
|
|
6504
|
+
PYRESOLVE
|
|
6505
|
+
printf '%s' "$p"
|
|
6506
|
+
}
|
|
6507
|
+
|
|
6508
|
+
# True (0) when TARGET_DIR is an engine-owned, freshly-minted build workspace
|
|
6509
|
+
# that Loki may auto-git-init without surprising a user. Two honest signals:
|
|
6510
|
+
# 1. LOKI_AUTO_GIT_INIT=1 -- explicit opt-in (non-SaaS automation).
|
|
6511
|
+
# 2. LOKI_TARGET_DIR is set AND realpath-contained under one of the
|
|
6512
|
+
# colon-separated LOKI_WORKSPACE_ROOTS dirs (the v7.91.0 SaaS route: the
|
|
6513
|
+
# BFF mints <root>/<buildId> and the server pins LOKI_TARGET_DIR to it).
|
|
6514
|
+
# A user's own folder (`loki start ./prd.md`, no workspace, roots unset) is
|
|
6515
|
+
# NEVER engine-owned -- it must not get a silent .git. Realpath containment (not
|
|
6516
|
+
# a prefix string match) so /root/build-other does not match /root/build.
|
|
6517
|
+
_loki_workspace_is_engine_owned() {
|
|
6518
|
+
[ "${LOKI_AUTO_GIT_INIT:-0}" = "1" ] && return 0
|
|
6519
|
+
|
|
6520
|
+
local roots_raw="${LOKI_WORKSPACE_ROOTS:-}"
|
|
6521
|
+
[ -n "${LOKI_TARGET_DIR:-}" ] || return 1
|
|
6522
|
+
[ -n "$roots_raw" ] || return 1
|
|
6523
|
+
|
|
6524
|
+
local ws_real root_real
|
|
6525
|
+
ws_real="$(_loki_resolve_path "${TARGET_DIR:-.}")"
|
|
6526
|
+
[ -n "$ws_real" ] || return 1
|
|
6527
|
+
|
|
6528
|
+
local IFS=':'
|
|
6529
|
+
local root
|
|
6530
|
+
for root in $roots_raw; do
|
|
6531
|
+
[ -n "$root" ] || continue
|
|
6532
|
+
root_real="$(_loki_resolve_path "$root")"
|
|
6533
|
+
[ -n "$root_real" ] || continue
|
|
6534
|
+
# Exact match or contained: ws == root, or ws starts with root + "/".
|
|
6535
|
+
if [ "$ws_real" = "$root_real" ] || case "$ws_real" in "$root_real"/*) true ;; *) false ;; esac; then
|
|
6536
|
+
return 0
|
|
6537
|
+
fi
|
|
6538
|
+
done
|
|
6539
|
+
return 1
|
|
6540
|
+
}
|
|
6541
|
+
|
|
6542
|
+
# Plan #16 Option A-1: establish git in an engine-owned build workspace so the
|
|
6543
|
+
# review/verify gate (run_code_review) and branch-isolation chain can actually
|
|
6544
|
+
# run. Without git history, run_code_review's diff resolves empty and the gate
|
|
6545
|
+
# SKIPS (silently reporting PASS) -- so a build that was never reviewed could
|
|
6546
|
+
# earn a VERIFIED receipt. This makes the gate RUN; it does NOT force a green
|
|
6547
|
+
# (a build whose review/tests fail still gets an honest verdict).
|
|
6548
|
+
#
|
|
6549
|
+
# Constraints honored:
|
|
6550
|
+
# - Engine-owned workspaces ONLY (see _loki_workspace_is_engine_owned). A
|
|
6551
|
+
# user's own folder is never silently git-init'd.
|
|
6552
|
+
# - Already a git repo (user's repo OR the engine source tree) -> NO-OP. Only
|
|
6553
|
+
# a non-git workspace is initialized.
|
|
6554
|
+
# - One INITIAL commit (not a bare init): a zero-commit unborn HEAD breaks the
|
|
6555
|
+
# start-SHA capture (git rev-parse HEAD) and re-trips the HEAD~1 skip. The
|
|
6556
|
+
# commit uses --allow-empty because a greenfield workspace at build start
|
|
6557
|
+
# holds only .loki/ (the spec lands in .loki/specs/), which .loki/.gitignore
|
|
6558
|
+
# excludes -> nothing to stage -> a bare commit would fail and leave an
|
|
6559
|
+
# unborn HEAD. --allow-empty guarantees a real HEAD either way.
|
|
6560
|
+
# - Neutral repo-local identity (loki-build) so the initial commit AND the
|
|
6561
|
+
# later commit_session_changes commit never inherit a developer's global git
|
|
6562
|
+
# identity. Repo-local sticks on a freshly-init'd workspace (no revert hook).
|
|
6563
|
+
# - Secrets never committed: brownfield files are staged through the same
|
|
6564
|
+
# secret-scan guard (_commit_path_looks_secret / _commit_scan_secret_file)
|
|
6565
|
+
# used by commit_session_changes; any offender unstages the whole set and
|
|
6566
|
+
# the initial commit falls back to --allow-empty (HEAD still established).
|
|
6567
|
+
maybe_git_init_engine_workspace() {
|
|
6568
|
+
command -v git >/dev/null 2>&1 || return 0
|
|
6569
|
+
_loki_workspace_is_engine_owned || return 0
|
|
6570
|
+
|
|
6571
|
+
local ws="${TARGET_DIR:-.}"
|
|
6572
|
+
[ -d "$ws" ] || return 0
|
|
6573
|
+
|
|
6574
|
+
# Already a git repo (user repo or engine source tree): do nothing.
|
|
6575
|
+
if git -C "$ws" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
6576
|
+
return 0
|
|
6577
|
+
fi
|
|
6578
|
+
|
|
6579
|
+
log_info "Engine-owned workspace is not a git repo; initializing for review/verify gates"
|
|
6580
|
+
|
|
6581
|
+
if ! git -C "$ws" init -q >/dev/null 2>&1; then
|
|
6582
|
+
log_warn "git init failed in engine workspace; review gate will skip (non-fatal)"
|
|
6583
|
+
return 0
|
|
6584
|
+
fi
|
|
6585
|
+
|
|
6586
|
+
# Neutral repo-local identity for the initial AND session-end commits.
|
|
6587
|
+
git -C "$ws" config user.name "loki-build" >/dev/null 2>&1 || true
|
|
6588
|
+
git -C "$ws" config user.email "loki-build@autonomi.dev" >/dev/null 2>&1 || true
|
|
6589
|
+
|
|
6590
|
+
# Self-ignore .loki/ runtime state so no commit ever stages it (mirrors
|
|
6591
|
+
# setup_agent_branch). Idempotent.
|
|
6592
|
+
mkdir -p "$ws/.loki" 2>/dev/null || true
|
|
6593
|
+
[ -f "$ws/.loki/.gitignore" ] || printf '*\n' > "$ws/.loki/.gitignore" 2>/dev/null || true
|
|
6594
|
+
|
|
6595
|
+
# Stage everything except .loki/ and an obvious-secret-path denylist (same
|
|
6596
|
+
# first-cut excludes commit_session_changes uses). The scan loop below is the
|
|
6597
|
+
# real guarantee for nested/weak secrets.
|
|
6598
|
+
git -C "$ws" add -A \
|
|
6599
|
+
':!.loki' ':!.loki/' \
|
|
6600
|
+
':!.env' ':!.env.*' ':!*.env' \
|
|
6601
|
+
':!*.key' ':!*.pem' ':!*.p12' ':!*.keystore' \
|
|
6602
|
+
':!id_rsa*' ':!*.token' ':!credentials*' >/dev/null 2>&1 || true
|
|
6603
|
+
|
|
6604
|
+
# Secret-scan staged files. ANY offender -> unstage all (safe default: never
|
|
6605
|
+
# commit a possible secret). The --allow-empty commit below still runs so a
|
|
6606
|
+
# real HEAD is established regardless.
|
|
6607
|
+
local _offenders=""
|
|
6608
|
+
local _f
|
|
6609
|
+
while IFS= read -r -d '' _f; do
|
|
6610
|
+
[ -f "$ws/$_f" ] || continue
|
|
6611
|
+
if _commit_path_looks_secret "$ws/$_f" || _commit_scan_secret_file "$ws/$_f"; then
|
|
6612
|
+
_offenders="${_offenders}${_offenders:+, }${_f}"
|
|
6613
|
+
fi
|
|
6614
|
+
done < <(git -C "$ws" diff --cached --name-only -z 2>/dev/null)
|
|
6615
|
+
|
|
6616
|
+
if [ -n "$_offenders" ]; then
|
|
6617
|
+
git -C "$ws" reset >/dev/null 2>&1 || true
|
|
6618
|
+
log_warn "Initial commit: possible secret in ${_offenders}; left unstaged (baseline commit will be empty)"
|
|
6619
|
+
fi
|
|
6620
|
+
|
|
6621
|
+
# ONE initial commit. --allow-empty: a greenfield workspace stages nothing
|
|
6622
|
+
# (only .loki/, which is ignored), so a bare commit would fail and leave an
|
|
6623
|
+
# unborn HEAD -- the exact failure mode this whole block exists to avoid.
|
|
6624
|
+
if git -C "$ws" commit --allow-empty -q -m "loki: initial build workspace baseline" >/dev/null 2>&1; then
|
|
6625
|
+
log_info "Initialized git in engine workspace (initial baseline commit created)"
|
|
6626
|
+
audit_log "WORKSPACE_GIT_INIT" "workspace=$ws"
|
|
6627
|
+
else
|
|
6628
|
+
log_warn "Initial baseline commit failed; review gate may skip (non-fatal)"
|
|
6629
|
+
fi
|
|
6630
|
+
return 0
|
|
6631
|
+
}
|
|
6632
|
+
|
|
6192
6633
|
#===============================================================================
|
|
6193
6634
|
# Branch Protection for Agent Changes
|
|
6194
6635
|
#===============================================================================
|
|
@@ -9673,17 +10114,89 @@ run_code_review() {
|
|
|
9673
10114
|
# common case) because git ignores the exclude pathspec for paths it does not
|
|
9674
10115
|
# track. ':(exclude).git/' is harmless (git never diffs .git/) and is kept
|
|
9675
10116
|
# only for parity with the evidence-gate exclusion list.
|
|
9676
|
-
|
|
9677
|
-
|
|
9678
|
-
|
|
10117
|
+
# Finding #596 + Plan #16: exclude .loki/, .git/, AND the standard dependency
|
|
10118
|
+
# / build noise dirs. The A-2 temp-index `git add -A` below stages EVERY
|
|
10119
|
+
# untracked file, and a fresh greenfield workspace has no root .gitignore yet
|
|
10120
|
+
# (only .loki/.gitignore, scoped to .loki/). Without these excludes a build
|
|
10121
|
+
# that ran `npm install` / `pip install` before writing a .gitignore would
|
|
10122
|
+
# stage node_modules/ (or .venv/, dist/, build/) into the temp index, bloat
|
|
10123
|
+
# the review diff to multi-MB, overflow the reviewer prompt, force NO_OUTPUT,
|
|
10124
|
+
# and defeat the gate -- exactly the Finding #596 class. The diff pathspec
|
|
10125
|
+
# filters the OUTPUT regardless of what the temp index staged, so this one
|
|
10126
|
+
# list fixes both diff_content and changed_files. Mirrors the metrics-path
|
|
10127
|
+
# noise set (run.sh:12592-12598) plus __pycache__ and vendor.
|
|
10128
|
+
local _review_pathspec=(-- . \
|
|
10129
|
+
':(exclude).loki/' ':(exclude).git/' ':(exclude)**/.loki/**' \
|
|
10130
|
+
':(exclude)node_modules/' ':(exclude)**/node_modules/**' \
|
|
10131
|
+
':(exclude)venv/' ':(exclude).venv/' ':(exclude)**/venv/**' ':(exclude)**/.venv/**' \
|
|
10132
|
+
':(exclude)dist/' ':(exclude)build/' ':(exclude)**/dist/**' ':(exclude)**/build/**' \
|
|
10133
|
+
':(exclude)__pycache__/' ':(exclude)**/__pycache__/**' \
|
|
10134
|
+
':(exclude)vendor/' ':(exclude)**/vendor/**')
|
|
10135
|
+
|
|
10136
|
+
# Plan #16 (A-2): make the review diff base robust to shallow/fresh history,
|
|
10137
|
+
# and surface NEW (untracked) files -- the whole greenfield build is new
|
|
10138
|
+
# files, which `git diff <base>` (tracked-only) never shows.
|
|
10139
|
+
#
|
|
10140
|
+
# Base preference, mirroring the proven metrics-path fallback chain:
|
|
10141
|
+
# 1. ${_LOKI_RUN_START_SHA} when it resolves to a commit -- the correct
|
|
10142
|
+
# per-run baseline (captured at run start; also what the summary/"Review
|
|
10143
|
+
# the work" command uses). After A-1, a fresh workspace has exactly one
|
|
10144
|
+
# commit at iteration 1, so HEAD~1 does NOT resolve -- the start-SHA is
|
|
10145
|
+
# what makes the gate run on iteration 1.
|
|
10146
|
+
# 2. HEAD~1 when it resolves (the live engine-source case, deep history).
|
|
10147
|
+
# 3. the git empty-tree object (computed, never a hardcoded SHA-1 constant,
|
|
10148
|
+
# so it survives a SHA-256 repo) so a single-commit repo still yields a
|
|
10149
|
+
# real diff against an empty baseline instead of an empty string.
|
|
10150
|
+
local _review_base=""
|
|
10151
|
+
if [ -n "${_LOKI_RUN_START_SHA:-}" ] && \
|
|
10152
|
+
git -C "${TARGET_DIR:-.}" rev-parse --verify --quiet "${_LOKI_RUN_START_SHA}^{commit}" >/dev/null 2>&1; then
|
|
10153
|
+
_review_base="${_LOKI_RUN_START_SHA}"
|
|
10154
|
+
elif git -C "${TARGET_DIR:-.}" rev-parse --verify --quiet 'HEAD~1^{commit}' >/dev/null 2>&1; then
|
|
10155
|
+
_review_base="HEAD~1"
|
|
10156
|
+
else
|
|
10157
|
+
_review_base="$(git -C "${TARGET_DIR:-.}" hash-object -t tree /dev/null 2>/dev/null || echo "")"
|
|
10158
|
+
fi
|
|
10159
|
+
|
|
10160
|
+
# Build the review diff against a THROWAWAY index (GIT_INDEX_FILE points at a
|
|
10161
|
+
# fresh, nonexistent path) so a `git add -A` captures the full working tree --
|
|
10162
|
+
# new + modified files alike -- WITHOUT touching the real index. This avoids
|
|
10163
|
+
# disturbing any other porcelain consumer (the evidence hard gate's status
|
|
10164
|
+
# read, create_checkpoint's `git stash create`, commit_session_changes). The
|
|
10165
|
+
# `.loki/.gitignore` (`*`) keeps runtime state out; the pathspec is a belt-
|
|
10166
|
+
# and-suspenders exclude. `diff --cached <base>` then compares that staged
|
|
10167
|
+
# snapshot to the baseline -- a real unified diff the reviewer can read.
|
|
10168
|
+
local diff_content=""
|
|
10169
|
+
local changed_files=""
|
|
10170
|
+
if [ -n "$_review_base" ]; then
|
|
10171
|
+
local _rev_idx
|
|
10172
|
+
_rev_idx="$(mktemp -u "${TMPDIR:-/tmp}/loki-revidx.XXXXXX")"
|
|
10173
|
+
GIT_INDEX_FILE="$_rev_idx" git -C "${TARGET_DIR:-.}" add -A 2>/dev/null || true
|
|
10174
|
+
diff_content=$(GIT_INDEX_FILE="$_rev_idx" git -C "${TARGET_DIR:-.}" diff --cached "$_review_base" "${_review_pathspec[@]}" 2>/dev/null || echo "")
|
|
10175
|
+
changed_files=$(GIT_INDEX_FILE="$_rev_idx" git -C "${TARGET_DIR:-.}" diff --cached --name-only "$_review_base" "${_review_pathspec[@]}" 2>/dev/null || echo "")
|
|
10176
|
+
rm -f "$_rev_idx" 2>/dev/null || true
|
|
10177
|
+
fi
|
|
10178
|
+
|
|
9679
10179
|
if [ -z "$diff_content" ]; then
|
|
9680
|
-
|
|
10180
|
+
# Honesty (Finding #596 class): a silent PASS on an empty diff is a trust
|
|
10181
|
+
# gap ONLY when the run actually produced changes we failed to diff.
|
|
10182
|
+
# Distinguish a genuine no-op iteration (nothing changed -> legitimate
|
|
10183
|
+
# PASS) from "changes exist but we could not compute a diff" (must not
|
|
10184
|
+
# pass silently). Use the same .loki/.git-excluding porcelain the
|
|
10185
|
+
# evidence gate uses to detect real changes independent of the diff base.
|
|
10186
|
+
local _dirty
|
|
10187
|
+
_dirty=$(git -C "${TARGET_DIR:-.}" status --porcelain "${_review_pathspec[@]}" 2>/dev/null | head -1 || echo "")
|
|
10188
|
+
if [ -n "$_dirty" ] || [ -n "$changed_files" ]; then
|
|
10189
|
+
# Return non-zero so the gate dispatcher records a failure (it calls
|
|
10190
|
+
# track_gate_failure on the else branch). Do NOT call track_gate_failure
|
|
10191
|
+
# here: it echoes its count to stdout (callers capture it) and the
|
|
10192
|
+
# dispatcher would double-count.
|
|
10193
|
+
log_warn "Code review: workspace has changes but the review diff is empty (could not compute a diff base); NOT passing the gate silently"
|
|
10194
|
+
return 1
|
|
10195
|
+
fi
|
|
10196
|
+
log_info "Code review: no changes this iteration, skipping (genuine no-op)"
|
|
9681
10197
|
return 0
|
|
9682
10198
|
fi
|
|
9683
10199
|
|
|
9684
|
-
local changed_files
|
|
9685
|
-
changed_files=$(git -C "${TARGET_DIR:-.}" diff --name-only HEAD~1 "${_review_pathspec[@]}" 2>/dev/null || git -C "${TARGET_DIR:-.}" diff --name-only --cached "${_review_pathspec[@]}" 2>/dev/null || echo "")
|
|
9686
|
-
|
|
9687
10200
|
log_header "CODE REVIEW: $review_id"
|
|
9688
10201
|
|
|
9689
10202
|
# Phase 3 (v7.0.0): managed code-review council. When the flag is on,
|
|
@@ -15914,9 +16427,13 @@ if __name__ == "__main__":
|
|
|
15914
16427
|
esac
|
|
15915
16428
|
|
|
15916
16429
|
# BUG-EC-013: Detect empty provider output (0 bytes = no work done)
|
|
16430
|
+
# T2.5: track this distinct cause so the failure path can classify the
|
|
16431
|
+
# durable LAST_ERROR record as provider_empty_output specifically.
|
|
16432
|
+
local _empty_output=0
|
|
15917
16433
|
if [ -f "$iter_output" ] && [ ! -s "$iter_output" ] && [ $exit_code -eq 0 ]; then
|
|
15918
16434
|
log_warn "Provider returned empty output (0 bytes) despite exit code 0 -- treating as error"
|
|
15919
16435
|
exit_code=1
|
|
16436
|
+
_empty_output=1
|
|
15920
16437
|
fi
|
|
15921
16438
|
|
|
15922
16439
|
save_state $retry "exited" $exit_code
|
|
@@ -16029,27 +16546,33 @@ if __name__ == "__main__":
|
|
|
16029
16546
|
# Static analysis gate
|
|
16030
16547
|
if [ "${PHASE_STATIC_ANALYSIS:-true}" = "true" ]; then
|
|
16031
16548
|
log_info "Quality gate: static analysis..."
|
|
16549
|
+
local _stg_t0=$(date +%s 2>/dev/null); local _stg_ok=pass
|
|
16032
16550
|
if enforce_static_analysis; then
|
|
16033
16551
|
clear_gate_failure "static_analysis"
|
|
16034
16552
|
else
|
|
16553
|
+
_stg_ok=fail
|
|
16035
16554
|
local sa_count
|
|
16036
16555
|
sa_count=$(track_gate_failure "static_analysis")
|
|
16037
16556
|
gate_failures="${gate_failures}static_analysis,"
|
|
16038
16557
|
log_warn "Static analysis FAILED ($sa_count consecutive) - findings injected into next iteration"
|
|
16039
16558
|
fi
|
|
16559
|
+
emit_stage_complete "static_analysis" "$_stg_ok" "$_stg_t0"
|
|
16040
16560
|
fi
|
|
16041
16561
|
# Secure-by-default scan (v7.87.0). Advisory by default (never
|
|
16042
16562
|
# blocks); records .loki/quality/security-findings.json each
|
|
16043
16563
|
# iteration. Blocks only on un-waived HIGH when LOKI_SECURE_GATE=block.
|
|
16044
16564
|
log_info "Quality gate: security scan (advisory)..."
|
|
16565
|
+
local _stg_t0=$(date +%s 2>/dev/null); local _stg_ok=pass
|
|
16045
16566
|
if run_secure_scan; then
|
|
16046
16567
|
clear_gate_failure "security_scan"
|
|
16047
16568
|
else
|
|
16569
|
+
_stg_ok=fail
|
|
16048
16570
|
local sec_count
|
|
16049
16571
|
sec_count=$(track_gate_failure "security_scan")
|
|
16050
16572
|
gate_failures="${gate_failures}security_scan,"
|
|
16051
16573
|
log_warn "Security gate BLOCKED ($sec_count consecutive) - un-waived HIGH findings (LOKI_SECURE_GATE=block)"
|
|
16052
16574
|
fi
|
|
16575
|
+
emit_stage_complete "security_scan" "$_stg_ok" "$_stg_t0"
|
|
16053
16576
|
# BUG-ST-002: Check pause signal between quality gates
|
|
16054
16577
|
if [ -f "${TARGET_DIR:-.}/.loki/PAUSE" ] || [ -f "${TARGET_DIR:-.}/.loki/STOP" ]; then
|
|
16055
16578
|
log_warn "Pause/stop signal detected between quality gates - deferring remaining gates"
|
|
@@ -16063,11 +16586,13 @@ if __name__ == "__main__":
|
|
|
16063
16586
|
# Test coverage gate
|
|
16064
16587
|
if [ "${PHASE_UNIT_TESTS:-true}" = "true" ]; then
|
|
16065
16588
|
log_info "Quality gate: test suite (pass/fail)..."
|
|
16589
|
+
local _stg_t0=$(date +%s 2>/dev/null); local _stg_ok=pass
|
|
16066
16590
|
# F49: isolate HOME so the project's suite cannot pollute the
|
|
16067
16591
|
# user's real home when it execs the generated app.
|
|
16068
16592
|
if _loki_with_app_sandbox enforce_test_coverage; then
|
|
16069
16593
|
clear_gate_failure "test_coverage"
|
|
16070
16594
|
else
|
|
16595
|
+
_stg_ok=fail
|
|
16071
16596
|
local tc_count
|
|
16072
16597
|
tc_count=$(track_gate_failure "test_coverage")
|
|
16073
16598
|
gate_failures="${gate_failures}test_coverage,"
|
|
@@ -16081,6 +16606,7 @@ if __name__ == "__main__":
|
|
|
16081
16606
|
log_warn "Test suite gate FAILED ($tc_count consecutive) - must pass next iteration"
|
|
16082
16607
|
fi
|
|
16083
16608
|
fi
|
|
16609
|
+
emit_stage_complete "test_suite" "$_stg_ok" "$_stg_t0"
|
|
16084
16610
|
fi
|
|
16085
16611
|
# BUG-ST-002: Check pause signal between quality gates (after test coverage)
|
|
16086
16612
|
if [ -f "${TARGET_DIR:-.}/.loki/PAUSE" ] || [ -f "${TARGET_DIR:-.}/.loki/STOP" ]; then
|
|
@@ -16093,26 +16619,32 @@ if __name__ == "__main__":
|
|
|
16093
16619
|
# Mock integrity gate (P0-3): block on CRITICAL/HIGH mock problems.
|
|
16094
16620
|
if [ "${LOKI_GATE_MOCK:-true}" = "true" ] && [ "$ITERATION_COUNT" -gt 0 ]; then
|
|
16095
16621
|
log_info "Quality gate: mock integrity..."
|
|
16622
|
+
local _stg_t0=$(date +%s 2>/dev/null); local _stg_ok=pass
|
|
16096
16623
|
if enforce_mock_integrity; then
|
|
16097
16624
|
clear_gate_failure "mock_integrity"
|
|
16098
16625
|
else
|
|
16626
|
+
_stg_ok=fail
|
|
16099
16627
|
local mk_count
|
|
16100
16628
|
mk_count=$(track_gate_failure "mock_integrity")
|
|
16101
16629
|
gate_failures="${gate_failures}mock_integrity,"
|
|
16102
16630
|
log_warn "Mock integrity gate FAILED ($mk_count consecutive) - CRITICAL/HIGH mock problems"
|
|
16103
16631
|
fi
|
|
16632
|
+
emit_stage_complete "mock_integrity" "$_stg_ok" "$_stg_t0"
|
|
16104
16633
|
fi
|
|
16105
16634
|
# Test mutation integrity gate (P0-3): block on HIGH test-fitting.
|
|
16106
16635
|
if [ "${LOKI_GATE_MUTATION:-true}" = "true" ] && [ "$ITERATION_COUNT" -gt 0 ]; then
|
|
16107
16636
|
log_info "Quality gate: test mutation integrity..."
|
|
16637
|
+
local _stg_t0=$(date +%s 2>/dev/null); local _stg_ok=pass
|
|
16108
16638
|
if enforce_mutation_integrity; then
|
|
16109
16639
|
clear_gate_failure "mutation_integrity"
|
|
16110
16640
|
else
|
|
16641
|
+
_stg_ok=fail
|
|
16111
16642
|
local mt_count
|
|
16112
16643
|
mt_count=$(track_gate_failure "mutation_integrity")
|
|
16113
16644
|
gate_failures="${gate_failures}mutation_integrity,"
|
|
16114
16645
|
log_warn "Mutation integrity gate FAILED ($mt_count consecutive) - HIGH test-fitting detected"
|
|
16115
16646
|
fi
|
|
16647
|
+
emit_stage_complete "mutation_integrity" "$_stg_ok" "$_stg_t0"
|
|
16116
16648
|
fi
|
|
16117
16649
|
# LSP diagnostics gate (P1-5 bash-route parity, v7.51.0; default-on
|
|
16118
16650
|
# advisory-surfacing as of v7.57.0). Closes the parity gap: the Bun
|
|
@@ -16148,6 +16680,7 @@ if __name__ == "__main__":
|
|
|
16148
16680
|
# runLSPDiagnosticsWriter: cwd=REPO_ROOT, --root=ctx.cwd).
|
|
16149
16681
|
if { [ "${LOKI_GATE_LSP_DIAGNOSTICS:-true}" = "true" ] || [ "${LOKI_GATE_LSP_DIAGNOSTICS:-true}" = "1" ]; } && [ "$ITERATION_COUNT" -gt 0 ]; then
|
|
16150
16682
|
log_info "Quality gate: LSP diagnostics..."
|
|
16683
|
+
local _stg_t0=$(date +%s 2>/dev/null); local _stg_ok=pass
|
|
16151
16684
|
# WRITER: route-neutral Python, same program as the Bun route.
|
|
16152
16685
|
if [ "${LOKI_GATE_LSP_WRITER:-1}" != "0" ]; then
|
|
16153
16686
|
( cd "$PROJECT_DIR" && LOKI_DIR="${TARGET_DIR:-.}/.loki" python3 -m mcp.lsp_proxy --write-diagnostics --root "${TARGET_DIR:-.}" ) >/dev/null 2>&1 || true
|
|
@@ -16181,6 +16714,7 @@ else:
|
|
|
16181
16714
|
fi
|
|
16182
16715
|
case "$_lsp_verdict" in
|
|
16183
16716
|
block*)
|
|
16717
|
+
_stg_ok=fail
|
|
16184
16718
|
local _lsp_e _lsp_w
|
|
16185
16719
|
_lsp_e=$(printf '%s' "$_lsp_verdict" | awk '{print $2}')
|
|
16186
16720
|
_lsp_w=$(printf '%s' "$_lsp_verdict" | awk '{print $3}')
|
|
@@ -16207,6 +16741,7 @@ else:
|
|
|
16207
16741
|
log_info "LSP diagnostics: no lsp-diagnostics.json artifact (lsp not available) -- gate did not run"
|
|
16208
16742
|
;;
|
|
16209
16743
|
esac
|
|
16744
|
+
emit_stage_complete "lsp_diagnostics" "$_stg_ok" "$_stg_t0"
|
|
16210
16745
|
fi
|
|
16211
16746
|
# Semantic test-authenticity gate -- mid-iteration ADVISORY arm
|
|
16212
16747
|
# (v7.57.0 default-on surfacing). Clones the mock arm (~15126)
|
|
@@ -16259,9 +16794,11 @@ else:
|
|
|
16259
16794
|
# Code review gate (upgraded from advisory, with escalation)
|
|
16260
16795
|
if [ "$PHASE_CODE_REVIEW" = "true" ] && [ "$ITERATION_COUNT" -gt 0 ]; then
|
|
16261
16796
|
log_info "Quality gate: code review..."
|
|
16797
|
+
local _stg_t0=$(date +%s 2>/dev/null); local _stg_ok=pass
|
|
16262
16798
|
if run_code_review; then
|
|
16263
16799
|
clear_gate_failure "code_review"
|
|
16264
16800
|
else
|
|
16801
|
+
_stg_ok=fail
|
|
16265
16802
|
local cr_count
|
|
16266
16803
|
cr_count=$(track_gate_failure "code_review")
|
|
16267
16804
|
# BUG-QG-007: Always append to gate_failures regardless of escalation tier
|
|
@@ -16286,7 +16823,7 @@ else:
|
|
|
16286
16823
|
esac
|
|
16287
16824
|
fi
|
|
16288
16825
|
if [ "$_phase1_overrode" = "true" ]; then
|
|
16289
|
-
|
|
16826
|
+
_stg_ok=pass # BLOCK lifted; continue without escalation
|
|
16290
16827
|
elif [ "$cr_count" -ge "$GATE_PAUSE_LIMIT" ]; then
|
|
16291
16828
|
log_error "Gate escalation: code_review failed $cr_count times (>= $GATE_PAUSE_LIMIT) - forcing PAUSE for human intervention"
|
|
16292
16829
|
echo "PAUSE" > "${TARGET_DIR:-.}/.loki/signals/GATE_ESCALATION"
|
|
@@ -16316,6 +16853,7 @@ else:
|
|
|
16316
16853
|
bun "${SCRIPT_DIR}/../loki-ts/dist/loki.js" internal phase1-hooks reflect "$ITERATION_COUNT" 2>/dev/null || true
|
|
16317
16854
|
fi
|
|
16318
16855
|
fi
|
|
16856
|
+
emit_stage_complete "code_review" "$_stg_ok" "$_stg_t0"
|
|
16319
16857
|
fi
|
|
16320
16858
|
# Auto-generate docs (default-on) BEFORE the staleness check and the
|
|
16321
16859
|
# gate, so neither nags the user to run 'loki docs generate' by hand.
|
|
@@ -16330,26 +16868,32 @@ else:
|
|
|
16330
16868
|
# Documentation quality gate - Gate 7 (Documentation Coverage)
|
|
16331
16869
|
if [ "${LOKI_GATE_DOC_COVERAGE:-true}" = "true" ] && [ "$ITERATION_COUNT" -gt 0 ]; then
|
|
16332
16870
|
log_info "Quality gate: documentation coverage..."
|
|
16871
|
+
local _stg_t0=$(date +%s 2>/dev/null); local _stg_ok=pass
|
|
16333
16872
|
if run_doc_quality_gate; then
|
|
16334
16873
|
clear_gate_failure "doc_coverage"
|
|
16335
16874
|
else
|
|
16875
|
+
_stg_ok=fail
|
|
16336
16876
|
local dc_count
|
|
16337
16877
|
dc_count=$(track_gate_failure "doc_coverage")
|
|
16338
16878
|
gate_failures="${gate_failures}doc_coverage,"
|
|
16339
16879
|
log_warn "Documentation coverage gate: Score below threshold ($dc_count consecutive)"
|
|
16340
16880
|
fi
|
|
16881
|
+
emit_stage_complete "doc_coverage" "$_stg_ok" "$_stg_t0"
|
|
16341
16882
|
fi
|
|
16342
16883
|
# Magic Modules debate gate - Gate 12 (v6.77.0)
|
|
16343
16884
|
if [ "${LOKI_GATE_MAGIC_DEBATE:-true}" = "true" ] && [ "$ITERATION_COUNT" -gt 0 ]; then
|
|
16344
16885
|
log_info "Quality gate: magic modules debate..."
|
|
16886
|
+
local _stg_t0=$(date +%s 2>/dev/null); local _stg_ok=pass
|
|
16345
16887
|
if run_magic_debate_gate; then
|
|
16346
16888
|
clear_gate_failure "magic_debate"
|
|
16347
16889
|
else
|
|
16890
|
+
_stg_ok=fail
|
|
16348
16891
|
local md_count
|
|
16349
16892
|
md_count=$(track_gate_failure "magic_debate")
|
|
16350
16893
|
gate_failures="${gate_failures}magic_debate,"
|
|
16351
16894
|
log_warn "Magic Modules debate gate: BLOCK severity detected ($md_count consecutive)"
|
|
16352
16895
|
fi
|
|
16896
|
+
emit_stage_complete "magic_debate" "$_stg_ok" "$_stg_t0"
|
|
16353
16897
|
fi
|
|
16354
16898
|
# Store gate failures for prompt injection
|
|
16355
16899
|
if [ -n "$gate_failures" ]; then
|
|
@@ -16684,6 +17228,24 @@ else:
|
|
|
16684
17228
|
# the "Will retry" log_warn below. TTY-gated, `|| true`, never tee'd.
|
|
16685
17229
|
render_build_hud "${ITERATION_COUNT:-0}" "${rarv_phase:-?}" "${duration:-0}" || true
|
|
16686
17230
|
|
|
17231
|
+
# T2.5: durable, classified failure record for `loki why`. Best-effort,
|
|
17232
|
+
# never crashes the build. Skip signal-induced exits (130 SIGINT /
|
|
17233
|
+
# 143 SIGTERM / 137 SIGKILL): a user/operator interrupt is not an error
|
|
17234
|
+
# to record (mirrors the crash-capture exclusion above). Classification
|
|
17235
|
+
# is conservative -- provider_empty_output | rate_limited | auth_error,
|
|
17236
|
+
# else unknown; never fabricates a class that no signal supports.
|
|
17237
|
+
if [ "$exit_code" -ne 130 ] && [ "$exit_code" -ne 143 ] && [ "$exit_code" -ne 137 ]; then
|
|
17238
|
+
local _err_class _err_brief
|
|
17239
|
+
_err_class="$(_loki_classify_iteration_error "$iter_output" "${_empty_output:-0}")"
|
|
17240
|
+
case "$_err_class" in
|
|
17241
|
+
provider_empty_output) _err_brief="The provider returned no output (0 bytes) on this iteration -- no work was done." ;;
|
|
17242
|
+
rate_limited) _err_brief="The provider rate-limited the request; the build will wait and retry." ;;
|
|
17243
|
+
auth_error) _err_brief="The provider rejected the request as unauthorized (check your login or API key)." ;;
|
|
17244
|
+
*) _err_brief="Iteration ${ITERATION_COUNT:-?} failed with exit code ${exit_code} (cause not classified)." ;;
|
|
17245
|
+
esac
|
|
17246
|
+
_loki_write_last_error "${ITERATION_COUNT:-0}" "$_err_class" "$_err_brief" || true
|
|
17247
|
+
fi
|
|
17248
|
+
|
|
16687
17249
|
# Checkpoint failed iteration state (v5.57.0)
|
|
16688
17250
|
create_checkpoint "iteration-${ITERATION_COUNT} failed (exit=$exit_code)" "iteration-${ITERATION_COUNT}-fail"
|
|
16689
17251
|
|
|
@@ -16703,7 +17265,15 @@ else:
|
|
|
16703
17265
|
wait_time=$rate_limit_wait
|
|
16704
17266
|
local human_time=$(format_duration $wait_time)
|
|
16705
17267
|
log_warn "Rate limit detected! Waiting until reset (~$human_time)..."
|
|
16706
|
-
|
|
17268
|
+
local _reset_at
|
|
17269
|
+
_reset_at="$(date -v+${wait_time}S '+%I:%M %p' 2>/dev/null || date -d "+${wait_time} seconds" '+%I:%M %p' 2>/dev/null || echo 'soon')"
|
|
17270
|
+
log_info "Rate limit resets at approximately $_reset_at"
|
|
17271
|
+
# T2.7: elevate the wait to an explicit, reassuring INFO line (the
|
|
17272
|
+
# human time was previously only at DEBUG inside detect_rate_limit's
|
|
17273
|
+
# calculated-backoff branch) so a multi-minute wait does not look
|
|
17274
|
+
# like a hang, and persist a machine-readable signal for watchers.
|
|
17275
|
+
log_info "Rate-limited by the provider; waiting ~${wait_time}s (resets ${_reset_at}). This is normal, not a hang."
|
|
17276
|
+
_loki_write_rate_limit_signal "$wait_time" "$_reset_at" || true
|
|
16707
17277
|
notify_rate_limit "$wait_time"
|
|
16708
17278
|
else
|
|
16709
17279
|
wait_time=$(calculate_wait $retry)
|
|
@@ -16743,6 +17313,10 @@ else:
|
|
|
16743
17313
|
done
|
|
16744
17314
|
echo ""
|
|
16745
17315
|
|
|
17316
|
+
# T2.7: the wait is over -- clear the RATE_LIMITED signal so it never
|
|
17317
|
+
# lingers stale once the build resumes. Best-effort.
|
|
17318
|
+
rm -f "${TARGET_DIR:-.}/.loki/signals/RATE_LIMITED" 2>/dev/null || true
|
|
17319
|
+
|
|
16746
17320
|
# Clean up per-iteration output file
|
|
16747
17321
|
rm -f "$iter_output" 2>/dev/null
|
|
16748
17322
|
|
|
@@ -17727,8 +18301,15 @@ main() {
|
|
|
17727
18301
|
fi
|
|
17728
18302
|
# Release on session-process exit so a fresh `loki start` can
|
|
17729
18303
|
# immediately re-acquire after this one finishes / is killed.
|
|
18304
|
+
# T2.6: piggyback the existing lock-release trap (we deliberately do NOT
|
|
18305
|
+
# add a new broad EXIT trap) to write a best-effort terminal record on an
|
|
18306
|
+
# untrapped non-zero exit where the run status is still "running" -- so a
|
|
18307
|
+
# post-crash `loki why` is not stale. _loki_terminal_record is a strict
|
|
18308
|
+
# no-op on every graceful path (status already settled) and never alters
|
|
18309
|
+
# the exit code. SIGKILL/power-loss stay uncatchable (no trap fires); the
|
|
18310
|
+
# ENT-2 durable-resume path covers those.
|
|
17730
18311
|
# shellcheck disable=SC2064
|
|
17731
|
-
trap "safe_release_lock '$lock_file'" EXIT INT TERM HUP
|
|
18312
|
+
trap "_loki_terminal_record || true; safe_release_lock '$lock_file'" EXIT INT TERM HUP
|
|
17732
18313
|
|
|
17733
18314
|
# Check PID file after acquiring lock
|
|
17734
18315
|
if [ -f "$pid_file" ]; then
|
|
@@ -17843,6 +18424,13 @@ main() {
|
|
|
17843
18424
|
# resume/rollback sees the restored state. Zero behavior change for local.
|
|
17844
18425
|
_loki_object_store_hydrate_checkpoints || true
|
|
17845
18426
|
|
|
18427
|
+
# Plan #16 (A-1): establish git in an engine-owned, non-git build workspace
|
|
18428
|
+
# BEFORE branch setup + start-SHA capture (both need a resolvable HEAD), so
|
|
18429
|
+
# the per-iteration review/verify gate actually runs instead of skipping on
|
|
18430
|
+
# an empty diff. No-op for the engine source tree, a user's own repo, or a
|
|
18431
|
+
# non-engine-owned folder. See maybe_git_init_engine_workspace.
|
|
18432
|
+
maybe_git_init_engine_workspace
|
|
18433
|
+
|
|
17846
18434
|
# Setup agent branch protection (isolates agent changes to a feature branch)
|
|
17847
18435
|
setup_agent_branch
|
|
17848
18436
|
|
|
@@ -17884,6 +18472,13 @@ main() {
|
|
|
17884
18472
|
fi
|
|
17885
18473
|
fi
|
|
17886
18474
|
|
|
18475
|
+
# Clear any stale per-run diagnosis record from a PRIOR run before this one
|
|
18476
|
+
# starts. LAST_ERROR.json is a single side-record; if it survived a previous
|
|
18477
|
+
# failed run it must not surface next to THIS run's outcome (a stale error
|
|
18478
|
+
# shown beside a fresh success would be a fake-green-adjacent lie). Mirrors
|
|
18479
|
+
# the RATE_LIMITED signal clear. Best-effort; never blocks the run.
|
|
18480
|
+
rm -f "${TARGET_DIR:-.}/.loki/state/LAST_ERROR.json" 2>/dev/null || true
|
|
18481
|
+
|
|
17887
18482
|
if [ "$PARALLEL_MODE" = "true" ]; then
|
|
17888
18483
|
# Parallel mode: orchestrate multiple worktrees
|
|
17889
18484
|
log_header "Running in Parallel Mode"
|
|
@@ -18133,6 +18728,13 @@ except (json.JSONDecodeError, OSError): pass
|
|
|
18133
18728
|
" 2>/dev/null || true
|
|
18134
18729
|
fi
|
|
18135
18730
|
|
|
18731
|
+
# T2.4: on ANY non-zero final result, surface a plain-language next step
|
|
18732
|
+
# (print to stderr + write .loki/NEXT_STEPS.txt). Single chokepoint at the
|
|
18733
|
+
# finalization exit so it fires once and never double-prints on success.
|
|
18734
|
+
if [ "$result" != "0" ]; then
|
|
18735
|
+
_loki_surface_why_hint || true
|
|
18736
|
+
fi
|
|
18737
|
+
|
|
18136
18738
|
exit $result
|
|
18137
18739
|
}
|
|
18138
18740
|
|