loki-mode 7.66.1 → 7.67.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/app-runner.sh +128 -7
- package/autonomy/loki +92 -50
- package/autonomy/run.sh +122 -33
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +2 -2
- package/loki-ts/dist/loki.js +112 -110
- package/mcp/__init__.py +1 -1
- package/memory/consolidation.py +71 -11
- package/memory/storage.py +25 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 8 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v7.
|
|
6
|
+
# Loki Mode v7.67.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -406,4 +406,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
406
406
|
|
|
407
407
|
---
|
|
408
408
|
|
|
409
|
-
**v7.
|
|
409
|
+
**v7.67.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.
|
|
1
|
+
7.67.0
|
package/autonomy/app-runner.sh
CHANGED
|
@@ -778,7 +778,12 @@ app_runner_init() {
|
|
|
778
778
|
local _project_hash
|
|
779
779
|
_project_hash=$(echo "$dir" | (md5sum 2>/dev/null || md5 -r 2>/dev/null || echo "$$") | cut -c1-8)
|
|
780
780
|
_APP_RUNNER_DOCKER_CONTAINER="loki-app-${_project_hash}"
|
|
781
|
-
|
|
781
|
+
# Hash the image tag the same way the container name is hashed so two
|
|
782
|
+
# Dockerfile-based projects do not clobber each other's image (an
|
|
783
|
+
# unhashed `loki-app` tag would be shared across every project). Build
|
|
784
|
+
# tag and run image arg MUST stay identical.
|
|
785
|
+
local _image_tag="loki-app-${_project_hash}"
|
|
786
|
+
_APP_RUNNER_METHOD="docker build -t ${_image_tag} . && docker run -d -p ${_APP_RUNNER_PORT}:${_APP_RUNNER_PORT} --name ${_APP_RUNNER_DOCKER_CONTAINER} ${_image_tag}"
|
|
782
787
|
_APP_RUNNER_IS_DOCKER=true
|
|
783
788
|
_write_detection "dockerfile" "$_APP_RUNNER_METHOD"
|
|
784
789
|
log_info "App Runner: detected Dockerfile"
|
|
@@ -1012,6 +1017,59 @@ _app_runner_compose_running_count() {
|
|
|
1012
1017
|
return 0
|
|
1013
1018
|
}
|
|
1014
1019
|
|
|
1020
|
+
# Decide whether to prepend `exec` to the launched method. `exec` replaces the
|
|
1021
|
+
# bash wrapper with the command so the captured PID is the app itself (PID
|
|
1022
|
+
# identity for npm start / python app.py etc.). That is ONLY valid for a SINGLE
|
|
1023
|
+
# command. A compound method like `docker build ... && docker run ...` must NOT
|
|
1024
|
+
# be exec'd: `exec docker build` would replace the shell and the `&& docker run`
|
|
1025
|
+
# half would never run (the verified HIGH-1 bug -- image builds, no container).
|
|
1026
|
+
# Detection runs on the METHOD STRING ONLY, never the assembled launch line: the
|
|
1027
|
+
# assembled line always contains `;` (from the PORT env prefix and the pgid
|
|
1028
|
+
# `echo $$`), so testing it would mark every method compound and silently drop
|
|
1029
|
+
# the exec optimization for single commands.
|
|
1030
|
+
# Echoes "exec " for a single command, or "" (empty) for a compound command.
|
|
1031
|
+
_app_runner_exec_prefix() {
|
|
1032
|
+
local method="$1"
|
|
1033
|
+
case "$method" in
|
|
1034
|
+
*"&&"*|*"||"*|*";"*)
|
|
1035
|
+
# Compound: let bash run the full sequence as a child (no exec).
|
|
1036
|
+
printf '%s' ""
|
|
1037
|
+
;;
|
|
1038
|
+
*)
|
|
1039
|
+
printf '%s' "exec "
|
|
1040
|
+
;;
|
|
1041
|
+
esac
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
# Liveness predicate for the Dockerfile (single-image `docker run -d`) path,
|
|
1045
|
+
# which -- unlike compose -- has a project-hashed container name in
|
|
1046
|
+
# $_APP_RUNNER_DOCKER_CONTAINER. The method is a compound `docker build && docker
|
|
1047
|
+
# run -d` launched WITHOUT exec, so the captured PID is the short-lived bash
|
|
1048
|
+
# wrapper: it stays alive for the (possibly multi-minute) build, then exits right
|
|
1049
|
+
# after `docker run -d` detaches. Therefore liveness is:
|
|
1050
|
+
# alive = container running OR wrapper PID still alive (build in progress)
|
|
1051
|
+
# dead = wrapper PID dead AND container not running
|
|
1052
|
+
# This tolerates a slow-but-succeeding build while a genuinely broken Dockerfile
|
|
1053
|
+
# still trips the watchdog breaker (wrapper dies, no container, 5x). Returns 0
|
|
1054
|
+
# when alive, 1 when dead. Never hard-fails (guarded for set -u / future set -e).
|
|
1055
|
+
_app_runner_dockerfile_container_running() {
|
|
1056
|
+
local _name="${_APP_RUNNER_DOCKER_CONTAINER:-}"
|
|
1057
|
+
[ -z "$_name" ] && return 1
|
|
1058
|
+
if command -v docker >/dev/null 2>&1; then
|
|
1059
|
+
local _state
|
|
1060
|
+
_state=$(docker inspect -f '{{.State.Running}}' "$_name" 2>/dev/null || true)
|
|
1061
|
+
if [ "$_state" = "true" ]; then
|
|
1062
|
+
return 0
|
|
1063
|
+
fi
|
|
1064
|
+
fi
|
|
1065
|
+
# Container not (yet) running: the build may still be in progress. The wrapper
|
|
1066
|
+
# PID being alive is the build-in-progress signal.
|
|
1067
|
+
if [ -n "${_APP_RUNNER_PID:-}" ] && kill -0 "$_APP_RUNNER_PID" 2>/dev/null; then
|
|
1068
|
+
return 0
|
|
1069
|
+
fi
|
|
1070
|
+
return 1
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1015
1073
|
# Read the RUNTIME published host port of the identified primary web service from
|
|
1016
1074
|
# `docker compose ps` (the live mapping), as opposed to the config-declared port
|
|
1017
1075
|
# from `docker compose config`. The config port is correct for fixed mappings
|
|
@@ -1127,6 +1185,25 @@ app_runner_start() {
|
|
|
1127
1185
|
_port_env_prefix="export PORT=$_APP_RUNNER_PORT HTTP_PORT=$_APP_RUNNER_PORT SERVER_PORT=$_APP_RUNNER_PORT APP_PORT=$_APP_RUNNER_PORT; "
|
|
1128
1186
|
fi
|
|
1129
1187
|
|
|
1188
|
+
# Conditional exec (HIGH-1 fix): only `exec` a SINGLE command. A compound
|
|
1189
|
+
# method (`docker build ... && docker run ...`) must run as a child so BOTH
|
|
1190
|
+
# halves execute -- `exec docker build` would replace the shell and never
|
|
1191
|
+
# reach `&& docker run`. Computed on the method string ONLY (see
|
|
1192
|
+
# _app_runner_exec_prefix), not the assembled launch line.
|
|
1193
|
+
local _exec_prefix
|
|
1194
|
+
_exec_prefix=$(_app_runner_exec_prefix "$_APP_RUNNER_METHOD")
|
|
1195
|
+
|
|
1196
|
+
# Dockerfile path: `docker run --name <hashed>` fails if a stale (exited)
|
|
1197
|
+
# container with that name still exists. This happens on a watchdog restart
|
|
1198
|
+
# (the prior run's container was stopped, not removed) and would make every
|
|
1199
|
+
# auto-restart fail with "name already in use". Remove any stale container
|
|
1200
|
+
# by name before launch. Idempotent and safe when none exists. Compose has no
|
|
1201
|
+
# _APP_RUNNER_DOCKER_CONTAINER, so this is Dockerfile-path only.
|
|
1202
|
+
if [ "$_APP_RUNNER_IS_DOCKER" = true ] && [ -n "${_APP_RUNNER_DOCKER_CONTAINER:-}" ] \
|
|
1203
|
+
&& command -v docker >/dev/null 2>&1; then
|
|
1204
|
+
docker rm -f "$_APP_RUNNER_DOCKER_CONTAINER" >/dev/null 2>&1 || true
|
|
1205
|
+
fi
|
|
1206
|
+
|
|
1130
1207
|
# Start the process in a new process group
|
|
1131
1208
|
if command -v setsid >/dev/null 2>&1; then
|
|
1132
1209
|
_APP_RUNNER_HAS_SETSID=true
|
|
@@ -1136,7 +1213,7 @@ app_runner_start() {
|
|
|
1136
1213
|
# Note: $_APP_RUNNER_METHOD has passed _validate_app_command (whitelist).
|
|
1137
1214
|
# The `--` after `bash -lc` prevents flag injection if the assembled
|
|
1138
1215
|
# script string ever begins with a `-`.
|
|
1139
|
-
(cd "$dir" && setsid bash -lc -- "$_port_env_prefix"'echo $$ > "'"$_pgid_file"'";
|
|
1216
|
+
(cd "$dir" && setsid bash -lc -- "$_port_env_prefix"'echo $$ > "'"$_pgid_file"'"; '"$_exec_prefix$_APP_RUNNER_METHOD" >> "$_APP_RUNNER_DIR/app.log" 2>&1) &
|
|
1140
1217
|
local _subshell_pid=$!
|
|
1141
1218
|
# Wait briefly for the pgid file to appear, then read the real PGID
|
|
1142
1219
|
local _pgid_wait=0
|
|
@@ -1154,7 +1231,7 @@ app_runner_start() {
|
|
|
1154
1231
|
_APP_RUNNER_HAS_SETSID=false
|
|
1155
1232
|
# Note: $_APP_RUNNER_METHOD has passed _validate_app_command (whitelist).
|
|
1156
1233
|
# The `--` after `bash -lc` prevents flag injection.
|
|
1157
|
-
(cd "$dir" && bash -lc -- "${_port_env_prefix}
|
|
1234
|
+
(cd "$dir" && bash -lc -- "${_port_env_prefix}${_exec_prefix}$_APP_RUNNER_METHOD" >> "$_APP_RUNNER_DIR/app.log" 2>&1) &
|
|
1158
1235
|
_APP_RUNNER_PID=$!
|
|
1159
1236
|
fi
|
|
1160
1237
|
# Register with central PID registry if available
|
|
@@ -1212,6 +1289,24 @@ app_runner_start() {
|
|
|
1212
1289
|
_write_app_state "failed"
|
|
1213
1290
|
return 1
|
|
1214
1291
|
fi
|
|
1292
|
+
elif [ "$_APP_RUNNER_IS_DOCKER" = true ] && [ -n "${_APP_RUNNER_DOCKER_CONTAINER:-}" ]; then
|
|
1293
|
+
# Dockerfile path (HIGH-1): `docker build && docker run -d` is compound, so
|
|
1294
|
+
# it is launched WITHOUT exec and the captured PID is the short-lived bash
|
|
1295
|
+
# wrapper that exits once the detached container is up. Liveness keys on the
|
|
1296
|
+
# container (or the wrapper still building), NOT the wrapper PID -- the same
|
|
1297
|
+
# reasoning as the compose branch above. Port mapping is the fixed
|
|
1298
|
+
# `-p PORT:PORT` from detection and the URL is already set, so no port
|
|
1299
|
+
# reconciliation or PID identity token is needed here.
|
|
1300
|
+
if _app_runner_dockerfile_container_running; then
|
|
1301
|
+
_write_app_state "running"
|
|
1302
|
+
log_info "App Runner: Dockerfile container '$_APP_RUNNER_DOCKER_CONTAINER' starting/running on port $_APP_RUNNER_PORT"
|
|
1303
|
+
return 0
|
|
1304
|
+
else
|
|
1305
|
+
log_error "App Runner: Dockerfile container failed to start (no running container, build wrapper exited)"
|
|
1306
|
+
_APP_RUNNER_CRASH_COUNT=$(( _APP_RUNNER_CRASH_COUNT + 1 ))
|
|
1307
|
+
_write_app_state "failed"
|
|
1308
|
+
return 1
|
|
1309
|
+
fi
|
|
1215
1310
|
elif kill -0 "$_APP_RUNNER_PID" 2>/dev/null; then
|
|
1216
1311
|
# Reconcile recorded port with the port the app actually bound (finding
|
|
1217
1312
|
# #597), so state.json / detection.json / the preview URL point at the
|
|
@@ -1456,6 +1551,23 @@ app_runner_health_check() {
|
|
|
1456
1551
|
return 0
|
|
1457
1552
|
fi
|
|
1458
1553
|
|
|
1554
|
+
# Dockerfile path (HIGH-1): the detached `docker run -d` container's liveness
|
|
1555
|
+
# is the container running (or the build wrapper still building), NOT the
|
|
1556
|
+
# ephemeral bash wrapper PID. Without this branch the wrapper PID dies after
|
|
1557
|
+
# the build detaches the container and the PID check below would report the
|
|
1558
|
+
# live container as crashed -> watchdog tears it down and rebuilds forever.
|
|
1559
|
+
if [ "$_APP_RUNNER_IS_DOCKER" = true ] && [ -n "${_APP_RUNNER_DOCKER_CONTAINER:-}" ]; then
|
|
1560
|
+
if _app_runner_dockerfile_container_running; then
|
|
1561
|
+
_write_health "true"
|
|
1562
|
+
_write_app_state "running"
|
|
1563
|
+
return 0
|
|
1564
|
+
else
|
|
1565
|
+
_write_health "false"
|
|
1566
|
+
_write_app_state "crashed"
|
|
1567
|
+
return 1
|
|
1568
|
+
fi
|
|
1569
|
+
fi
|
|
1570
|
+
|
|
1459
1571
|
# Check PID is alive (non-docker-compose methods)
|
|
1460
1572
|
if ! kill -0 "$_APP_RUNNER_PID" 2>/dev/null; then
|
|
1461
1573
|
_write_health "false"
|
|
@@ -1580,7 +1692,16 @@ app_runner_watchdog() {
|
|
|
1580
1692
|
# This is what makes the service-aware health logic actually fire in the
|
|
1581
1693
|
# live monitoring loop (not just in isolation). On an unhealthy web service
|
|
1582
1694
|
# it restarts the stack under the same crash-count circuit breaker.
|
|
1583
|
-
|
|
1695
|
+
# Detached-docker paths (compose stacks AND the Dockerfile `docker run -d`
|
|
1696
|
+
# container) both exit their captured wrapper PID once the container is up, so
|
|
1697
|
+
# `kill -0` is the wrong liveness signal. Delegate to app_runner_health_check,
|
|
1698
|
+
# whose container-aware branches (compose web service / hashed Dockerfile
|
|
1699
|
+
# container) own the real liveness check, under the same crash-count circuit
|
|
1700
|
+
# breaker. Without including the Dockerfile container here, the wrapper PID
|
|
1701
|
+
# would read as dead after the build detaches and the watchdog would tear the
|
|
1702
|
+
# live container down and rebuild forever (the HIGH-1 symptom).
|
|
1703
|
+
if [ "$_APP_RUNNER_IS_DOCKER" = true ] && \
|
|
1704
|
+
{ echo "$_APP_RUNNER_METHOD" | grep -q "docker compose" || [ -n "${_APP_RUNNER_DOCKER_CONTAINER:-}" ]; }; then
|
|
1584
1705
|
if app_runner_health_check; then
|
|
1585
1706
|
# BUG 3 fix: the breaker is meant to fire on 5 CONSECUTIVE failures.
|
|
1586
1707
|
# A confirmed-healthy observation clears any accumulated count so a
|
|
@@ -1590,7 +1711,7 @@ app_runner_watchdog() {
|
|
|
1590
1711
|
return 0
|
|
1591
1712
|
fi
|
|
1592
1713
|
_APP_RUNNER_CRASH_COUNT=$(( _APP_RUNNER_CRASH_COUNT + 1 ))
|
|
1593
|
-
log_warn "App Runner:
|
|
1714
|
+
log_warn "App Runner: docker container unhealthy (crash #$_APP_RUNNER_CRASH_COUNT)"
|
|
1594
1715
|
if [ "$_APP_RUNNER_CRASH_COUNT" -ge 5 ]; then
|
|
1595
1716
|
log_error "App Runner: crash limit reached (5), marking as crashed"
|
|
1596
1717
|
tail -20 "$_APP_RUNNER_DIR/app.log" 2>/dev/null | while IFS= read -r line; do
|
|
@@ -1601,9 +1722,9 @@ app_runner_watchdog() {
|
|
|
1601
1722
|
fi
|
|
1602
1723
|
local _c_backoff=$(( 1 << _APP_RUNNER_CRASH_COUNT ))
|
|
1603
1724
|
[ "$_c_backoff" -gt 30 ] && _c_backoff=30
|
|
1604
|
-
log_info "App Runner: restarting
|
|
1725
|
+
log_info "App Runner: restarting docker app in ${_c_backoff}s..."
|
|
1605
1726
|
sleep "$_c_backoff"
|
|
1606
|
-
app_runner_start || log_warn "App Runner:
|
|
1727
|
+
app_runner_start || log_warn "App Runner: docker auto-restart failed"
|
|
1607
1728
|
return 0
|
|
1608
1729
|
fi
|
|
1609
1730
|
|
package/autonomy/loki
CHANGED
|
@@ -2204,6 +2204,72 @@ _kill_pid() {
|
|
|
2204
2204
|
fi
|
|
2205
2205
|
}
|
|
2206
2206
|
|
|
2207
|
+
# v7.7.34 group-kill, factored (loki-stop-F1). Reaps the orchestrator's whole
|
|
2208
|
+
# process group via the recorded pgid so the autonomous agent (claude/codex/
|
|
2209
|
+
# aider), which shares the orchestrator group and would otherwise reparent to
|
|
2210
|
+
# init and keep editing files, is killed atomically. This is the SAME logic the
|
|
2211
|
+
# no-arg `loki stop` path used inline; it is now shared so the by-id path
|
|
2212
|
+
# (`loki stop <session-id>`) gets identical reaping instead of skipping it.
|
|
2213
|
+
#
|
|
2214
|
+
# Args: one or more pgid-file paths. Each is read, validated (numeric, > 1, NOT
|
|
2215
|
+
# this shell's own group), and group-killed; the file is removed after. A
|
|
2216
|
+
# protected-pid conflict (dashboard / app-runner / registered pids that happen
|
|
2217
|
+
# to share the group) downgrades the kill to a per-pid kill that excludes the
|
|
2218
|
+
# protected pids, so a group-kill never tears down the dashboard. Every kill is
|
|
2219
|
+
# `|| true` guarded -- safe under set -e (killing an already-dead member, or an
|
|
2220
|
+
# empty group, returns non-zero legitimately).
|
|
2221
|
+
_stop_group_by_pgid_files() {
|
|
2222
|
+
local _stop_pgid_file
|
|
2223
|
+
for _stop_pgid_file in "$@"; do
|
|
2224
|
+
[ -f "$_stop_pgid_file" ] || continue
|
|
2225
|
+
local _spgid
|
|
2226
|
+
_spgid=$(cat "$_stop_pgid_file" 2>/dev/null | tr -d ' ')
|
|
2227
|
+
case "$_spgid" in ''|*[!0-9]*) continue ;; esac
|
|
2228
|
+
[ "$_spgid" -gt 1 ] 2>/dev/null || continue
|
|
2229
|
+
local _my_pgid
|
|
2230
|
+
_my_pgid=$(ps -o pgid= -p $$ 2>/dev/null | tr -d ' ')
|
|
2231
|
+
[ "$_spgid" = "$_my_pgid" ] && continue # never kill our own group
|
|
2232
|
+
# Collect protected pids (dashboard, app-runner, registered pids) so
|
|
2233
|
+
# a group-kill never takes down the dashboard if it happens to share
|
|
2234
|
+
# the orchestrator group. Mirrors the dashboard Python route.
|
|
2235
|
+
local _protected=" "
|
|
2236
|
+
local _pf
|
|
2237
|
+
if [ -d "$LOKI_DIR/pids" ]; then
|
|
2238
|
+
for _pf in "$LOKI_DIR/pids"/*.json; do
|
|
2239
|
+
[ -f "$_pf" ] || continue
|
|
2240
|
+
_protected="${_protected}$(basename "$_pf" .json) "
|
|
2241
|
+
done
|
|
2242
|
+
fi
|
|
2243
|
+
for _pf in "$LOKI_DIR/dashboard/dashboard.pid" "${HOME}/.loki/dashboard/dashboard.pid"; do
|
|
2244
|
+
[ -f "$_pf" ] && _protected="${_protected}$(cat "$_pf" 2>/dev/null | tr -d ' ') "
|
|
2245
|
+
done
|
|
2246
|
+
# Does any protected pid share this group?
|
|
2247
|
+
local _conflict=0 _gp
|
|
2248
|
+
for _gp in $(ps -axo pid=,pgid= 2>/dev/null | awk -v g="$_spgid" '$2==g{print $1}'); do
|
|
2249
|
+
case "$_protected" in *" $_gp "*) _conflict=1; break ;; esac
|
|
2250
|
+
done
|
|
2251
|
+
if [ "$_conflict" = "1" ]; then
|
|
2252
|
+
# Per-pid kill of group members EXCLUDING protected pids.
|
|
2253
|
+
for _gp in $(ps -axo pid=,pgid= 2>/dev/null | awk -v g="$_spgid" '$2==g{print $1}'); do
|
|
2254
|
+
case "$_protected" in *" $_gp "*) continue ;; esac
|
|
2255
|
+
[ "$_gp" = "$$" ] && continue
|
|
2256
|
+
kill -TERM "$_gp" 2>/dev/null || true
|
|
2257
|
+
done
|
|
2258
|
+
sleep 1
|
|
2259
|
+
for _gp in $(ps -axo pid=,pgid= 2>/dev/null | awk -v g="$_spgid" '$2==g{print $1}'); do
|
|
2260
|
+
case "$_protected" in *" $_gp "*) continue ;; esac
|
|
2261
|
+
[ "$_gp" = "$$" ] && continue
|
|
2262
|
+
kill -KILL "$_gp" 2>/dev/null || true
|
|
2263
|
+
done
|
|
2264
|
+
else
|
|
2265
|
+
kill -TERM -- -"$_spgid" 2>/dev/null || true
|
|
2266
|
+
sleep 1
|
|
2267
|
+
kill -KILL -- -"$_spgid" 2>/dev/null || true
|
|
2268
|
+
fi
|
|
2269
|
+
rm -f "$_stop_pgid_file" 2>/dev/null || true
|
|
2270
|
+
done
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2207
2273
|
# Stop a specific session by its session ID
|
|
2208
2274
|
_stop_session_by_id() {
|
|
2209
2275
|
local sid="$1"
|
|
@@ -2296,6 +2362,29 @@ cmd_stop() {
|
|
|
2296
2362
|
# Stop a specific session by ID
|
|
2297
2363
|
if [ -n "$target_session" ]; then
|
|
2298
2364
|
if is_session_running "$target_session"; then
|
|
2365
|
+
# loki-stop-F1: group-kill FIRST (v7.7.34 discipline), scoped to THIS
|
|
2366
|
+
# session's recorded pgid. Without this the by-id path reaped only the
|
|
2367
|
+
# orchestrator pid (via _stop_session_by_id -> _kill_pid), leaving the
|
|
2368
|
+
# autonomous agent (claude/codex/aider) -- which shares the
|
|
2369
|
+
# orchestrator's process group -- to reparent to init and keep editing
|
|
2370
|
+
# files. That is exactly the v7.7.34 orphaned-agent bug, reopened on
|
|
2371
|
+
# the by-id path. The pgid is session-scoped: run.sh writes it next to
|
|
2372
|
+
# the session pid as ${pid_file%.pid}.pgid, so we only ever touch THIS
|
|
2373
|
+
# session's group (modern sessions/<id>/loki.pgid + legacy
|
|
2374
|
+
# run-<id>.pgid), never a sibling session or another folder.
|
|
2375
|
+
#
|
|
2376
|
+
# Deliberately NOT mirrored from the no-arg path: the docker reap,
|
|
2377
|
+
# session.json->stopped, and dashboard registry mark are folder/global
|
|
2378
|
+
# side effects (the docker container is named by workspace sha with no
|
|
2379
|
+
# per-session container; registry.mark_project_stopped marks the whole
|
|
2380
|
+
# project; session.json is the folder-level skill session). Firing them
|
|
2381
|
+
# on a by-id stop would mismark the project / kill a docker run while a
|
|
2382
|
+
# sibling session in the same folder is still building. The group-kill
|
|
2383
|
+
# alone closes the stated orphaned-agent hole; folder-global teardown
|
|
2384
|
+
# stays on the no-arg / --all paths.
|
|
2385
|
+
_stop_group_by_pgid_files \
|
|
2386
|
+
"$LOKI_DIR/sessions/$target_session/loki.pgid" \
|
|
2387
|
+
"$LOKI_DIR/run-${target_session}.pgid"
|
|
2299
2388
|
_stop_session_by_id "$target_session"
|
|
2300
2389
|
echo "Stopped session: $target_session"
|
|
2301
2390
|
else
|
|
@@ -2381,56 +2470,9 @@ cmd_stop() {
|
|
|
2381
2470
|
# session leader), so signaling the whole group reaps the orchestrator
|
|
2382
2471
|
# AND the agent child atomically. Killing only the orchestrator pid lets
|
|
2383
2472
|
# the agent reparent to init and keep editing files -- the reported bug.
|
|
2384
|
-
#
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
[ -f "$_stop_pgid_file" ] || continue
|
|
2388
|
-
local _spgid
|
|
2389
|
-
_spgid=$(cat "$_stop_pgid_file" 2>/dev/null | tr -d ' ')
|
|
2390
|
-
case "$_spgid" in ''|*[!0-9]*) continue ;; esac
|
|
2391
|
-
[ "$_spgid" -gt 1 ] 2>/dev/null || continue
|
|
2392
|
-
local _my_pgid
|
|
2393
|
-
_my_pgid=$(ps -o pgid= -p $$ 2>/dev/null | tr -d ' ')
|
|
2394
|
-
[ "$_spgid" = "$_my_pgid" ] && continue # never kill our own group
|
|
2395
|
-
# Collect protected pids (dashboard, app-runner, registered pids) so
|
|
2396
|
-
# a group-kill never takes down the dashboard if it happens to share
|
|
2397
|
-
# the orchestrator group. Mirrors the dashboard Python route.
|
|
2398
|
-
local _protected=" "
|
|
2399
|
-
local _pf
|
|
2400
|
-
if [ -d "$LOKI_DIR/pids" ]; then
|
|
2401
|
-
for _pf in "$LOKI_DIR/pids"/*.json; do
|
|
2402
|
-
[ -f "$_pf" ] || continue
|
|
2403
|
-
_protected="${_protected}$(basename "$_pf" .json) "
|
|
2404
|
-
done
|
|
2405
|
-
fi
|
|
2406
|
-
for _pf in "$LOKI_DIR/dashboard/dashboard.pid" "${HOME}/.loki/dashboard/dashboard.pid"; do
|
|
2407
|
-
[ -f "$_pf" ] && _protected="${_protected}$(cat "$_pf" 2>/dev/null | tr -d ' ') "
|
|
2408
|
-
done
|
|
2409
|
-
# Does any protected pid share this group?
|
|
2410
|
-
local _conflict=0 _gp
|
|
2411
|
-
for _gp in $(ps -axo pid=,pgid= 2>/dev/null | awk -v g="$_spgid" '$2==g{print $1}'); do
|
|
2412
|
-
case "$_protected" in *" $_gp "*) _conflict=1; break ;; esac
|
|
2413
|
-
done
|
|
2414
|
-
if [ "$_conflict" = "1" ]; then
|
|
2415
|
-
# Per-pid kill of group members EXCLUDING protected pids.
|
|
2416
|
-
for _gp in $(ps -axo pid=,pgid= 2>/dev/null | awk -v g="$_spgid" '$2==g{print $1}'); do
|
|
2417
|
-
case "$_protected" in *" $_gp "*) continue ;; esac
|
|
2418
|
-
[ "$_gp" = "$$" ] && continue
|
|
2419
|
-
kill -TERM "$_gp" 2>/dev/null || true
|
|
2420
|
-
done
|
|
2421
|
-
sleep 1
|
|
2422
|
-
for _gp in $(ps -axo pid=,pgid= 2>/dev/null | awk -v g="$_spgid" '$2==g{print $1}'); do
|
|
2423
|
-
case "$_protected" in *" $_gp "*) continue ;; esac
|
|
2424
|
-
[ "$_gp" = "$$" ] && continue
|
|
2425
|
-
kill -KILL "$_gp" 2>/dev/null || true
|
|
2426
|
-
done
|
|
2427
|
-
else
|
|
2428
|
-
kill -TERM -- -"$_spgid" 2>/dev/null || true
|
|
2429
|
-
sleep 1
|
|
2430
|
-
kill -KILL -- -"$_spgid" 2>/dev/null || true
|
|
2431
|
-
fi
|
|
2432
|
-
rm -f "$_stop_pgid_file" 2>/dev/null || true
|
|
2433
|
-
done
|
|
2473
|
+
# Factored into _stop_group_by_pgid_files (loki-stop-F1) so the by-id stop
|
|
2474
|
+
# path performs identical reaping. Here we pass the GLOBAL pgid files.
|
|
2475
|
+
_stop_group_by_pgid_files "$LOKI_DIR/loki.pgid" "$LOKI_DIR/run.pgid"
|
|
2434
2476
|
|
|
2435
2477
|
local killed_pid=""
|
|
2436
2478
|
for pid_file in "$LOKI_DIR/loki.pid" "$LOKI_DIR/run.pid"; do
|
package/autonomy/run.sh
CHANGED
|
@@ -1725,12 +1725,25 @@ detect_complexity() {
|
|
|
1725
1725
|
# Markdown PRD: count headers and checkboxes
|
|
1726
1726
|
feature_count=$(grep -c "^##\|^- \[" "$prd_path" 2>/dev/null || echo "0")
|
|
1727
1727
|
fi
|
|
1728
|
+
# WAVE8 FIX run.sh-provider-F1 (HIGH): grep -c prints "0" AND exits 1 on
|
|
1729
|
+
# zero matches; with the '|| echo "0"' fallback that yields "0\n0", which
|
|
1730
|
+
# crashes the integer tests below ([: 0\n0: integer expression expected)
|
|
1731
|
+
# and silently drops complexity from simple->standard. Strip to digits
|
|
1732
|
+
# after every assignment path (jq, both greps), mirroring file_count:1688.
|
|
1733
|
+
# "0\n0" -> "00" -> arithmetically 0.
|
|
1734
|
+
feature_count="${feature_count:-0}"
|
|
1735
|
+
feature_count="${feature_count//[^0-9]/}"
|
|
1736
|
+
feature_count="${feature_count:-0}"
|
|
1728
1737
|
|
|
1729
1738
|
# Count distinct sections (h2/h3 headers) for structural complexity (#74)
|
|
1730
1739
|
local section_count=0
|
|
1731
1740
|
if [[ "$prd_path" != *.json ]]; then
|
|
1732
1741
|
section_count=$(grep -c "^##\|^###" "$prd_path" 2>/dev/null || echo "0")
|
|
1733
1742
|
fi
|
|
1743
|
+
# WAVE8 FIX run.sh-provider-F1: same grep -c double-output guard.
|
|
1744
|
+
section_count="${section_count:-0}"
|
|
1745
|
+
section_count="${section_count//[^0-9]/}"
|
|
1746
|
+
section_count="${section_count:-0}"
|
|
1734
1747
|
|
|
1735
1748
|
# PRD complexity uses content length, feature count, AND structural depth (#74)
|
|
1736
1749
|
# A PRD with multiple sections or substantial content is not "simple" even with few project files
|
|
@@ -8927,6 +8940,63 @@ _dispatch_reviewer() {
|
|
|
8927
8940
|
esac
|
|
8928
8941
|
}
|
|
8929
8942
|
|
|
8943
|
+
# WAVE8 FIX run.sh-F1/F3 (CRITICAL/HIGH): SAFE-DEFAULT verdict classification.
|
|
8944
|
+
# Given a reviewer file, extract the VERDICT: line (tolerant of leading
|
|
8945
|
+
# markdown like '**VERDICT:**' or '# VERDICT:' so fewer reviewers fall to
|
|
8946
|
+
# NO_VERDICT) and classify it as one of: FAIL, PASS, AMBIGUOUS, NONE.
|
|
8947
|
+
# FAIL -> verdict text contains FAIL/REJECT/BLOCK (verbose suffixes like
|
|
8948
|
+
# "FAIL - [Critical] SQLi", "FAIL.", "FAIL (3 criticals)" all match)
|
|
8949
|
+
# PASS -> verdict text contains PASS/APPROVE (and NOT a fail token); this
|
|
8950
|
+
# preserves the deliberate "PASS with concerns" = pass semantics.
|
|
8951
|
+
# AMBIGUOUS -> a VERDICT: line exists but matches neither (unparseable token).
|
|
8952
|
+
# Callers MUST treat this as non-passing (safe direction), never pass.
|
|
8953
|
+
# NONE -> no parseable VERDICT: line at all (empty / missing).
|
|
8954
|
+
# FAIL-first ordering means a verdict naming both (rare) blocks -- the safe way.
|
|
8955
|
+
# Mirrors the council's _council_parse_vote: parse-miss defaults to the safe
|
|
8956
|
+
# (blocking) direction, never to pass.
|
|
8957
|
+
_classify_verdict() {
|
|
8958
|
+
local file="$1"
|
|
8959
|
+
[ -f "$file" ] && [ -s "$file" ] || { echo "NONE"; return 0; }
|
|
8960
|
+
local verdict
|
|
8961
|
+
# Tolerant anchor: optional leading whitespace, then optional markdown
|
|
8962
|
+
# markers (* # >), then optional whitespace, then VERDICT:. This rescues
|
|
8963
|
+
# '**VERDICT:** FAIL', '# VERDICT: PASS', '> VERDICT: FAIL' that the strict
|
|
8964
|
+
# '^VERDICT:' anchor missed (those previously became NO_VERDICT and dropped
|
|
8965
|
+
# the reviewer's dissent).
|
|
8966
|
+
verdict=$(grep -iE "^[[:space:]]*[*#>]*[[:space:]]*VERDICT:" "$file" \
|
|
8967
|
+
| head -1 \
|
|
8968
|
+
| sed -E 's/^[[:space:]]*[*#>]*[[:space:]]*[Vv][Ee][Rr][Dd][Ii][Cc][Tt]:[*[:space:]]*//' \
|
|
8969
|
+
| tr '[:lower:]' '[:upper:]')
|
|
8970
|
+
# Classify on the FIRST verdict TOKEN only, not a substring scan of the whole
|
|
8971
|
+
# despaced line. A whole-line scan is asymmetric and wrong: "PASS, no failures
|
|
8972
|
+
# found" or "PASS - no blocking issues" contain FAIL/BLOCK as substrings and
|
|
8973
|
+
# would misclassify a valid PASS as FAIL (a false-block, and worse, it breaks
|
|
8974
|
+
# the unanimous-PASS Devil's-Advocate trigger -> indirect false-PASS). Take
|
|
8975
|
+
# the leading alphabetic run as the verdict word: "FAIL - [Critical] x" ->
|
|
8976
|
+
# FAIL, "PASS, no failures" -> PASS. Strip leading markdown emphasis first.
|
|
8977
|
+
verdict=$(printf '%s' "$verdict" | sed -E 's/^[*_`[:space:]]+//')
|
|
8978
|
+
local _vtok
|
|
8979
|
+
_vtok=$(printf '%s' "$verdict" | sed -E 's/[^A-Z].*$//')
|
|
8980
|
+
if [ -z "$_vtok" ]; then echo "NONE"; return 0; fi
|
|
8981
|
+
case "$_vtok" in
|
|
8982
|
+
FAIL|FAILED|FAILURE|REJECT|REJECTED|BLOCK|BLOCKED) echo "FAIL" ;;
|
|
8983
|
+
PASS|PASSED|APPROVE|APPROVED|OK) echo "PASS" ;;
|
|
8984
|
+
*) echo "AMBIGUOUS" ;;
|
|
8985
|
+
esac
|
|
8986
|
+
}
|
|
8987
|
+
|
|
8988
|
+
# WAVE8 FIX run.sh-F2 (HIGH): SAFE-DEFAULT severity detection. Returns 0
|
|
8989
|
+
# (blocking) if the reviewer file names a Critical or High severity finding in
|
|
8990
|
+
# any realistic emitted form: bracketed '[Critical]', bold '**Critical**',
|
|
8991
|
+
# 'Severity: High', or a bullet line '- Critical' / '* High'. The strict
|
|
8992
|
+
# bracket-only match previously missed unbracketed forms, so a FAIL naming an
|
|
8993
|
+
# unbracketed Critical was treated as non-blocking. BSD/GNU portable (no \b).
|
|
8994
|
+
_severity_is_blocking() {
|
|
8995
|
+
local file="$1"
|
|
8996
|
+
[ -f "$file" ] || return 1
|
|
8997
|
+
grep -qiE '(\[(critical|high)\])|(\*\*[[:space:]]*(critical|high)[[:space:]]*\*\*)|(severity:?[[:space:]]*(critical|high))|(^[[:space:]]*[-*][[:space:]]+(critical|high)([[:space:]:.,*]|$))' "$file"
|
|
8998
|
+
}
|
|
8999
|
+
|
|
8930
9000
|
run_code_review() {
|
|
8931
9001
|
local loki_dir="${TARGET_DIR:-.}/.loki"
|
|
8932
9002
|
local review_dir="$loki_dir/quality/reviews"
|
|
@@ -9284,21 +9354,27 @@ BUILD_PROMPT
|
|
|
9284
9354
|
continue
|
|
9285
9355
|
fi
|
|
9286
9356
|
|
|
9287
|
-
# Extract verdict
|
|
9357
|
+
# Extract + classify verdict (WAVE8 FIX run.sh-F1/F3). _classify_verdict
|
|
9358
|
+
# uses a markdown-tolerant anchor (rescues '**VERDICT:** FAIL') and a
|
|
9359
|
+
# SAFE-DEFAULT contract: FAIL=any FAIL/REJECT/BLOCK token (so verbose
|
|
9360
|
+
# "FAIL - [Critical] SQLi" / "FAIL." / "FAIL (3 criticals)" all count as
|
|
9361
|
+
# FAIL, previously mis-counted as PASS); PASS=PASS/APPROVE; AMBIGUOUS=a
|
|
9362
|
+
# verdict line that parses to neither; NONE=no parseable verdict line.
|
|
9288
9363
|
local verdict
|
|
9289
|
-
verdict=$(
|
|
9290
|
-
|
|
9291
|
-
# FIX A2: a "real verdict" is
|
|
9292
|
-
#
|
|
9293
|
-
#
|
|
9294
|
-
#
|
|
9295
|
-
#
|
|
9296
|
-
#
|
|
9297
|
-
#
|
|
9298
|
-
#
|
|
9299
|
-
#
|
|
9300
|
-
|
|
9301
|
-
|
|
9364
|
+
verdict=$(_classify_verdict "$review_output")
|
|
9365
|
+
|
|
9366
|
+
# FIX A2 + WAVE8 FIX run.sh-F1/F3: a "real verdict" is a parseable
|
|
9367
|
+
# VERDICT line that classifies cleanly to PASS or FAIL. NONE (no usable
|
|
9368
|
+
# verdict line) AND AMBIGUOUS (a verdict line whose token is neither PASS
|
|
9369
|
+
# nor FAIL, e.g. "VERDICT: UNCLEAR") are BOTH routed to the NO_VERDICT
|
|
9370
|
+
# path. This is the SAFE-DEFAULT contract: an unparseable token must NOT
|
|
9371
|
+
# silently pass. It cannot count toward pass_count, and merely bumping
|
|
9372
|
+
# fail_count would be inert (only has_blocking / review_inconclusive gate
|
|
9373
|
+
# the return). So we treat it as a non-real verdict; the
|
|
9374
|
+
# real_verdict_count < reviewer_count check below then makes the review
|
|
9375
|
+
# inconclusive -> bounded retry -> block (FIX 3 machinery).
|
|
9376
|
+
if [ "$verdict" = "NONE" ] || [ "$verdict" = "AMBIGUOUS" ]; then
|
|
9377
|
+
log_warn "Reviewer $reviewer_name returned no usable verdict (empty, unparseable, or ambiguous token)"
|
|
9302
9378
|
verdicts_summary="${verdicts_summary}${reviewer_name}:NO_VERDICT "
|
|
9303
9379
|
((no_output_count++))
|
|
9304
9380
|
continue
|
|
@@ -9306,8 +9382,9 @@ BUILD_PROMPT
|
|
|
9306
9382
|
((real_verdict_count++))
|
|
9307
9383
|
if [ "$verdict" = "FAIL" ]; then
|
|
9308
9384
|
((fail_count++))
|
|
9309
|
-
# Check for Critical/High severity findings
|
|
9310
|
-
|
|
9385
|
+
# Check for Critical/High severity findings (bracketed OR unbracketed
|
|
9386
|
+
# OR bold OR 'Severity:' OR bullet form -- WAVE8 FIX run.sh-F2).
|
|
9387
|
+
if _severity_is_blocking "$review_output"; then
|
|
9311
9388
|
has_blocking=true
|
|
9312
9389
|
log_error "BLOCKING: $reviewer_name found Critical/High severity issues"
|
|
9313
9390
|
else
|
|
@@ -9320,16 +9397,25 @@ BUILD_PROMPT
|
|
|
9320
9397
|
verdicts_summary="${verdicts_summary}${reviewer_name}:${verdict:-UNKNOWN} "
|
|
9321
9398
|
done
|
|
9322
9399
|
|
|
9323
|
-
# Finding #596 FIX A2
|
|
9324
|
-
#
|
|
9325
|
-
#
|
|
9326
|
-
#
|
|
9327
|
-
#
|
|
9400
|
+
# Finding #596 FIX A2 + WAVE8 FIX run.sh-F3: a review is INCONCLUSIVE (=>
|
|
9401
|
+
# blocking) whenever FEWER reviewers returned a usable verdict than were
|
|
9402
|
+
# dispatched. The original gate only fired on real_verdict_count==0 (ALL
|
|
9403
|
+
# reviewers empty); a MIXED review (e.g. 1 of 3 NO_VERDICT, 2 PASS) silently
|
|
9404
|
+
# passed on the surviving majority and dropped the malformed reviewer's
|
|
9405
|
+
# potential dissent (Devil's Advocate never fired). Now ANY NO_VERDICT
|
|
9406
|
+
# reviewer makes the review inconclusive: a dropped reviewer is a dropped
|
|
9407
|
+
# vote, and the safe direction is to refuse to pass on a partial council.
|
|
9408
|
+
# The markdown-tolerant anchor in _classify_verdict already rescues most
|
|
9409
|
+
# real-but-wrapped verdicts, so this fires only on genuinely unusable output.
|
|
9410
|
+
# Optional bounded retry first (LOKI_REVIEW_RETRY=1, default on) so a
|
|
9411
|
+
# transient empty-output blip does not hard-block; the retry re-runs the
|
|
9412
|
+
# whole review with the (now .loki-excluded) diff. Opt out of the block
|
|
9413
|
+
# entirely with LOKI_REVIEW_INCONCLUSIVE_BLOCK=0 (records, never blocks).
|
|
9328
9414
|
local review_inconclusive=false
|
|
9329
|
-
if [ "$reviewer_count" -gt 0 ] && [ "$real_verdict_count" -
|
|
9415
|
+
if [ "$reviewer_count" -gt 0 ] && [ "$real_verdict_count" -lt "$reviewer_count" ]; then
|
|
9330
9416
|
review_inconclusive=true
|
|
9331
|
-
log_error "CODE REVIEW INCONCLUSIVE:
|
|
9332
|
-
log_error "
|
|
9417
|
+
log_error "CODE REVIEW INCONCLUSIVE: only $real_verdict_count of $reviewer_count reviewers returned a usable verdict (no_output=$no_output_count)"
|
|
9418
|
+
log_error " A partial review drops dissent; refusing to pass the gate without every reviewer's verdict."
|
|
9333
9419
|
if [ "${LOKI_REVIEW_RETRY:-1}" = "1" ] && [ "${_LOKI_REVIEW_RETRYING:-0}" != "1" ]; then
|
|
9334
9420
|
log_warn " Retrying code review once (LOKI_REVIEW_RETRY=1)..."
|
|
9335
9421
|
_LOKI_REVIEW_RETRYING=1 run_code_review
|
|
@@ -9437,9 +9523,12 @@ BUILD_DA_PROMPT
|
|
|
9437
9523
|
_dispatch_reviewer "$da_prompt_text" "$da_output" || true
|
|
9438
9524
|
|
|
9439
9525
|
if [ -f "$da_output" ] && [ -s "$da_output" ]; then
|
|
9526
|
+
# WAVE8 FIX run.sh-F1/F2: classify with the shared SAFE-DEFAULT
|
|
9527
|
+
# helpers so a verbose DA "VERDICT: FAIL - [Critical] ..." (and
|
|
9528
|
+
# AMBIGUOUS tokens) and an unbracketed Critical/High both block.
|
|
9440
9529
|
local da_verdict
|
|
9441
|
-
da_verdict=$(
|
|
9442
|
-
if [ "$da_verdict" = "FAIL" ]
|
|
9530
|
+
da_verdict=$(_classify_verdict "$da_output")
|
|
9531
|
+
if { [ "$da_verdict" = "FAIL" ] || [ "$da_verdict" = "AMBIGUOUS" ]; } && _severity_is_blocking "$da_output"; then
|
|
9443
9532
|
has_blocking=true
|
|
9444
9533
|
# Audit accuracy: aggregate.json was written above (line ~8429)
|
|
9445
9534
|
# with has_blocking=false (entering this block requires a
|
|
@@ -9465,7 +9554,7 @@ DA_AGG_PATCH
|
|
|
9465
9554
|
log_error "DEVIL'S ADVOCATE: found Critical/High issue the unanimous council missed -- BLOCK"
|
|
9466
9555
|
{
|
|
9467
9556
|
echo "DEVILS_ADVOCATE_BLOCK: Critical/High found after unanimous PASS"
|
|
9468
|
-
grep -iE
|
|
9557
|
+
grep -iE '(\[(critical|high)\])|(\*\*[[:space:]]*(critical|high)[[:space:]]*\*\*)|(severity:?[[:space:]]*(critical|high))|(^[[:space:]]*[-*][[:space:]]+(critical|high)([[:space:]:.,*]|$))' "$da_output" || true
|
|
9469
9558
|
} >> "$review_dir/$review_id/anti-sycophancy.txt"
|
|
9470
9559
|
else
|
|
9471
9560
|
log_info "Devil's Advocate: no additional Critical/High issues found"
|
|
@@ -9487,16 +9576,16 @@ DA_AGG_PATCH
|
|
|
9487
9576
|
return 1
|
|
9488
9577
|
fi
|
|
9489
9578
|
|
|
9490
|
-
# Finding #596 FIX A2: an inconclusive review (
|
|
9491
|
-
# already exhausted or disabled) blocks
|
|
9492
|
-
# the 'verified before done' promise: a
|
|
9493
|
-
# cannot stand in for a
|
|
9579
|
+
# Finding #596 FIX A2 + WAVE8 FIX run.sh-F3: an inconclusive review (fewer
|
|
9580
|
+
# usable verdicts than reviewers, retry already exhausted or disabled) blocks
|
|
9581
|
+
# unless explicitly opted out. This is the 'verified before done' promise: a
|
|
9582
|
+
# review missing any reviewer's verdict cannot stand in for a full review.
|
|
9494
9583
|
if [ "$review_inconclusive" = "true" ]; then
|
|
9495
9584
|
if [ "${LOKI_REVIEW_INCONCLUSIVE_BLOCK:-1}" = "0" ]; then
|
|
9496
|
-
log_warn "Code review inconclusive (
|
|
9585
|
+
log_warn "Code review inconclusive ($real_verdict_count/$reviewer_count real verdicts) but LOKI_REVIEW_INCONCLUSIVE_BLOCK=0 - not blocking"
|
|
9497
9586
|
return 0
|
|
9498
9587
|
fi
|
|
9499
|
-
log_error "CODE REVIEW BLOCKED: inconclusive (
|
|
9588
|
+
log_error "CODE REVIEW BLOCKED: inconclusive ($real_verdict_count/$reviewer_count reviewers returned a usable verdict)"
|
|
9500
9589
|
log_error " Review details: $review_dir/$review_id/ ; opt out with LOKI_REVIEW_INCONCLUSIVE_BLOCK=0"
|
|
9501
9590
|
return 1
|
|
9502
9591
|
fi
|