loki-mode 7.41.0 → 7.41.2
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 +138 -3
- package/autonomy/lib/claude-flags.sh +56 -3
- package/autonomy/loki +70 -6
- package/autonomy/run.sh +343 -10
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +95 -2
- package/dashboard/static/index.html +58 -32
- package/docs/INSTALLATION.md +1 -1
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/skills/quality-gates.md +39 -2
package/autonomy/run.sh
CHANGED
|
@@ -2560,20 +2560,24 @@ except Exception:
|
|
|
2560
2560
|
[ -z "$branch" ] && branch="unknown"
|
|
2561
2561
|
head_sha="$( (cd "${TARGET_DIR:-.}" && git rev-parse HEAD) 2>/dev/null || true )"
|
|
2562
2562
|
|
|
2563
|
+
# Finding #596 (HIGH): exclude .loki/ and .git/ from the summary diff/stat and
|
|
2564
|
+
# from the "Review the work" command we print, so the user is never told to
|
|
2565
|
+
# review a .loki-bloated diff and the displayed counts match the gated diff.
|
|
2566
|
+
local _summary_pathspec=(-- . ':(exclude).loki/' ':(exclude).git/' ':(exclude)**/.loki/**')
|
|
2563
2567
|
if [ -n "$start_sha" ]; then
|
|
2564
|
-
diff_stat="$( (cd "${TARGET_DIR:-.}" && git diff --stat "${start_sha}..HEAD") 2>/dev/null || true )"
|
|
2568
|
+
diff_stat="$( (cd "${TARGET_DIR:-.}" && git diff --stat "${start_sha}..HEAD" "${_summary_pathspec[@]}") 2>/dev/null || true )"
|
|
2565
2569
|
# Parse the git diff --shortstat tail for counts (locale-stable enough
|
|
2566
2570
|
# for our display; failures leave the zeros in place).
|
|
2567
2571
|
local shortstat
|
|
2568
|
-
shortstat="$( (cd "${TARGET_DIR:-.}" && git diff --shortstat "${start_sha}..HEAD") 2>/dev/null || true )"
|
|
2572
|
+
shortstat="$( (cd "${TARGET_DIR:-.}" && git diff --shortstat "${start_sha}..HEAD" "${_summary_pathspec[@]}") 2>/dev/null || true )"
|
|
2569
2573
|
if [ -n "$shortstat" ]; then
|
|
2570
2574
|
files_changed="$(printf '%s\n' "$shortstat" | grep -oE '[0-9]+ file' | grep -oE '[0-9]+' | head -1)"
|
|
2571
2575
|
insertions="$(printf '%s\n' "$shortstat" | grep -oE '[0-9]+ insertion' | grep -oE '[0-9]+' | head -1)"
|
|
2572
2576
|
deletions="$(printf '%s\n' "$shortstat" | grep -oE '[0-9]+ deletion' | grep -oE '[0-9]+' | head -1)"
|
|
2573
2577
|
fi
|
|
2574
|
-
review_cmd="git diff ${start_sha}..HEAD"
|
|
2578
|
+
review_cmd="git diff ${start_sha}..HEAD -- . ':(exclude).loki/'"
|
|
2575
2579
|
else
|
|
2576
|
-
review_cmd="git diff HEAD"
|
|
2580
|
+
review_cmd="git diff HEAD -- . ':(exclude).loki/'"
|
|
2577
2581
|
fi
|
|
2578
2582
|
[ -z "$files_changed" ] && files_changed=0
|
|
2579
2583
|
[ -z "$insertions" ] && insertions=0
|
|
@@ -7166,6 +7170,11 @@ enforce_test_coverage() {
|
|
|
7166
7170
|
cat > "$quality_dir/test-results.json" << TREOF
|
|
7167
7171
|
{"timestamp":"$(date -u +%Y-%m-%dT%H:%M:%SZ)","runner":"none","pass":true,"summary":"No test runner detected"}
|
|
7168
7172
|
TREOF
|
|
7173
|
+
# Finding #598: stamp the per-iteration freshness marker so a later
|
|
7174
|
+
# completion-route capture (ensure_completion_test_evidence) reuses this
|
|
7175
|
+
# run instead of re-running the suite. Single source of truth for "tests
|
|
7176
|
+
# ran this iteration", set on every return path that writes results.
|
|
7177
|
+
printf '%s\n' "${ITERATION_COUNT:-0}" > "$quality_dir/.test-results.iter" 2>/dev/null || true
|
|
7169
7178
|
return 0
|
|
7170
7179
|
fi
|
|
7171
7180
|
|
|
@@ -7175,6 +7184,8 @@ TREOF
|
|
|
7175
7184
|
cat > "$quality_dir/test-results.json" << TREOF
|
|
7176
7185
|
{"timestamp":"$(date -u +%Y-%m-%dT%H:%M:%SZ)","runner":"$test_runner","pass":$test_passed,"min_coverage":$min_coverage,"summary":"$details"}
|
|
7177
7186
|
TREOF
|
|
7187
|
+
# Finding #598: stamp the per-iteration freshness marker (see above).
|
|
7188
|
+
printf '%s\n' "${ITERATION_COUNT:-0}" > "$quality_dir/.test-results.iter" 2>/dev/null || true
|
|
7178
7189
|
|
|
7179
7190
|
if [ "$test_passed" = "true" ]; then
|
|
7180
7191
|
touch "$quality_dir/unit-tests.pass"
|
|
@@ -7189,6 +7200,82 @@ TREOF
|
|
|
7189
7200
|
fi
|
|
7190
7201
|
}
|
|
7191
7202
|
|
|
7203
|
+
# ============================================================================
|
|
7204
|
+
# Finding #598 (HIGH): ensure REAL test evidence exists before the
|
|
7205
|
+
# verified-completion evidence gate runs on a completion claim.
|
|
7206
|
+
#
|
|
7207
|
+
# The evidence gate (council_evidence_gate) blocks completion iff the diff is
|
|
7208
|
+
# empty OR tests are red. The test axis reads .loki/quality/test-results.json.
|
|
7209
|
+
# When that file is ABSENT or inconclusive (runner==none / unparsable), the gate
|
|
7210
|
+
# treats the test axis as pass-through, so completion could be claimed on a
|
|
7211
|
+
# nonzero diff alone with NO test evidence -- half-blind. This happens whenever
|
|
7212
|
+
# enforce_test_coverage did not run this iteration, e.g. LOKI_HARD_GATES=false or
|
|
7213
|
+
# PHASE_UNIT_TESTS=false (the gate at the quality-gate ladder is skipped) while
|
|
7214
|
+
# the completion-promise route still fires the evidence gate.
|
|
7215
|
+
#
|
|
7216
|
+
# Rather than letting absent evidence pass (Option 1, which would live in the
|
|
7217
|
+
# off-limits completion-council.sh), we GENERATE real evidence here (Option 2,
|
|
7218
|
+
# preferred + autonomous): run the project's own test command via the existing
|
|
7219
|
+
# detect-and-run enforce_test_coverage, which persists a fresh test-results.json.
|
|
7220
|
+
# The gate then reads true PASS/FAIL. If no test runner truly exists, the file
|
|
7221
|
+
# records runner:none and the test axis legitimately stays pass-through.
|
|
7222
|
+
#
|
|
7223
|
+
# Behavior:
|
|
7224
|
+
# - Default ON. Opt out with LOKI_COMPLETION_TEST_CAPTURE=0.
|
|
7225
|
+
# - Cheap: skips when a fresh test-results.json already exists for THIS
|
|
7226
|
+
# iteration (freshness marker .loki/quality/.test-results.iter), so we never
|
|
7227
|
+
# re-run the suite the quality-gate ladder already ran.
|
|
7228
|
+
# - Best-effort: enforce_test_coverage returns nonzero on red tests; that is
|
|
7229
|
+
# EXPECTED and must not crash the completion path. The gate is the decider,
|
|
7230
|
+
# so we always swallow the rc with `|| true` and let the gate read the file.
|
|
7231
|
+
# - CWD invariant: enforce_test_coverage writes ${TARGET_DIR}/.loki/...; the
|
|
7232
|
+
# gate reads .loki/... relative to CWD. Both are invoked from the same loop
|
|
7233
|
+
# body where CWD == TARGET_DIR (or TARGET_DIR=="."), matching the existing
|
|
7234
|
+
# gate call sites.
|
|
7235
|
+
# ============================================================================
|
|
7236
|
+
ensure_completion_test_evidence() {
|
|
7237
|
+
[ "${LOKI_COMPLETION_TEST_CAPTURE:-1}" = "0" ] && return 0
|
|
7238
|
+
type enforce_test_coverage &>/dev/null || return 0
|
|
7239
|
+
|
|
7240
|
+
local loki_dir="${TARGET_DIR:-.}/.loki"
|
|
7241
|
+
local quality_dir="$loki_dir/quality"
|
|
7242
|
+
local tr_file="$quality_dir/test-results.json"
|
|
7243
|
+
local iter_marker="$quality_dir/.test-results.iter"
|
|
7244
|
+
local this_iter="${ITERATION_COUNT:-0}"
|
|
7245
|
+
|
|
7246
|
+
# Freshness guard: if results already exist for this iteration, reuse them.
|
|
7247
|
+
if [ -f "$tr_file" ] && [ -f "$iter_marker" ]; then
|
|
7248
|
+
local marked
|
|
7249
|
+
marked="$(cat "$iter_marker" 2>/dev/null || echo "")"
|
|
7250
|
+
if [ "$marked" = "$this_iter" ]; then
|
|
7251
|
+
log_info "Completion test evidence: reusing this iteration's test-results.json"
|
|
7252
|
+
return 0
|
|
7253
|
+
fi
|
|
7254
|
+
fi
|
|
7255
|
+
|
|
7256
|
+
log_info "Completion test evidence: capturing fresh test results before evidence gate (opt out: LOKI_COMPLETION_TEST_CAPTURE=0)"
|
|
7257
|
+
# Record the test-results.json mtime BEFORE capture so we only mark this
|
|
7258
|
+
# iteration "fresh" if enforce_test_coverage actually (re)wrote the file.
|
|
7259
|
+
# Guards LOW-2 (bug-hunt): if capture is interrupted before writing while a
|
|
7260
|
+
# prior-iteration file exists, the marker must NOT advance and let stale
|
|
7261
|
+
# evidence read as fresh. Window is narrow but the check is cheap.
|
|
7262
|
+
local _results_file="$quality_dir/test-results.json"
|
|
7263
|
+
local _mtime_before=""
|
|
7264
|
+
[ -f "$_results_file" ] && _mtime_before=$(stat -f %m "$_results_file" 2>/dev/null || stat -c %Y "$_results_file" 2>/dev/null || echo "")
|
|
7265
|
+
# The gate decides on the persisted file; a red suite (nonzero rc) is expected
|
|
7266
|
+
# and must not abort the completion path here.
|
|
7267
|
+
enforce_test_coverage || true
|
|
7268
|
+
mkdir -p "$quality_dir" 2>/dev/null || true
|
|
7269
|
+
local _mtime_after=""
|
|
7270
|
+
[ -f "$_results_file" ] && _mtime_after=$(stat -f %m "$_results_file" 2>/dev/null || stat -c %Y "$_results_file" 2>/dev/null || echo "")
|
|
7271
|
+
# Only advance the freshness marker when the results file was actually
|
|
7272
|
+
# produced/updated by THIS capture (mtime advanced or file newly created).
|
|
7273
|
+
if [ -n "$_mtime_after" ] && [ "$_mtime_after" != "$_mtime_before" ]; then
|
|
7274
|
+
printf '%s\n' "$this_iter" > "$iter_marker" 2>/dev/null || true
|
|
7275
|
+
fi
|
|
7276
|
+
return 0
|
|
7277
|
+
}
|
|
7278
|
+
|
|
7192
7279
|
# ============================================================================
|
|
7193
7280
|
# Documentation Staleness Check (v6.75.0)
|
|
7194
7281
|
# Checks if generated documentation is stale relative to HEAD
|
|
@@ -7252,7 +7339,11 @@ run_doc_quality_gate() {
|
|
|
7252
7339
|
fi
|
|
7253
7340
|
else
|
|
7254
7341
|
score=$((score - 10))
|
|
7255
|
-
|
|
7342
|
+
if [ "${LOKI_AUTO_DOCS:-true}" = "true" ]; then
|
|
7343
|
+
issues+=("No generated documentation found (auto-generation did not complete)")
|
|
7344
|
+
else
|
|
7345
|
+
issues+=("No generated documentation found (auto-docs disabled; run 'loki docs generate')")
|
|
7346
|
+
fi
|
|
7256
7347
|
fi
|
|
7257
7348
|
|
|
7258
7349
|
# Check 3: Package documentation (for npm/pip packages)
|
|
@@ -7277,6 +7368,52 @@ run_doc_quality_gate() {
|
|
|
7277
7368
|
[ "$score" -ge 70 ]
|
|
7278
7369
|
}
|
|
7279
7370
|
|
|
7371
|
+
# ============================================================================
|
|
7372
|
+
# Auto-Documentation Generation (intelligent default)
|
|
7373
|
+
# Generates the .loki/docs/ suite before the documentation gate evaluates so
|
|
7374
|
+
# the gate scores on real generated docs instead of nagging the user to run
|
|
7375
|
+
# 'loki docs generate' by hand. Default-on; opt out with LOKI_AUTO_DOCS=false.
|
|
7376
|
+
#
|
|
7377
|
+
# Bounded: runs at most once per run when docs are missing, and again only
|
|
7378
|
+
# when the existing docs are >10 commits stale (the same threshold the gate
|
|
7379
|
+
# and staleness check use). 'loki docs generate' writes its manifest
|
|
7380
|
+
# unconditionally (template fallback when no provider), so the missing-docs
|
|
7381
|
+
# trigger fires exactly once. Best-effort: never fails the iteration loop.
|
|
7382
|
+
# ============================================================================
|
|
7383
|
+
|
|
7384
|
+
auto_generate_docs_if_needed() {
|
|
7385
|
+
[ "${LOKI_AUTO_DOCS:-true}" = "true" ] || return 0
|
|
7386
|
+
|
|
7387
|
+
local project_dir="${TARGET_DIR:-.}"
|
|
7388
|
+
local manifest="$project_dir/.loki/docs/docs-manifest.json"
|
|
7389
|
+
local needs_gen=false
|
|
7390
|
+
|
|
7391
|
+
if [ ! -f "$manifest" ]; then
|
|
7392
|
+
needs_gen=true
|
|
7393
|
+
else
|
|
7394
|
+
# Regenerate only when the existing docs are substantially stale.
|
|
7395
|
+
local doc_sha
|
|
7396
|
+
doc_sha=$(python3 -c "import json; print(json.load(open('$manifest')).get('git_sha', ''))" 2>/dev/null)
|
|
7397
|
+
if [ -n "$doc_sha" ]; then
|
|
7398
|
+
local behind
|
|
7399
|
+
behind=$(git -C "$project_dir" rev-list --count "$doc_sha..HEAD" 2>/dev/null || echo "0")
|
|
7400
|
+
[ "$behind" -gt 10 ] && needs_gen=true
|
|
7401
|
+
fi
|
|
7402
|
+
fi
|
|
7403
|
+
|
|
7404
|
+
[ "$needs_gen" = "true" ] || return 0
|
|
7405
|
+
|
|
7406
|
+
local loki_bin="$SCRIPT_DIR/loki"
|
|
7407
|
+
[ -x "$loki_bin" ] || return 0
|
|
7408
|
+
|
|
7409
|
+
log_info "Auto-documentation: generating .loki/docs/ before documentation gate..."
|
|
7410
|
+
# Synchronous so docs exist before the gate scores. Provider-agnostic:
|
|
7411
|
+
# 'loki docs generate' picks the run's provider and falls back to
|
|
7412
|
+
# template-based docs when no provider CLI is available.
|
|
7413
|
+
"$loki_bin" docs generate "$project_dir" >/dev/null 2>&1 || \
|
|
7414
|
+
log_warn "Auto-documentation: generation did not complete (gate will score on what exists)"
|
|
7415
|
+
}
|
|
7416
|
+
|
|
7280
7417
|
# ============================================================================
|
|
7281
7418
|
# Magic Modules Debate Gate - Gate 12 (v6.77.0)
|
|
7282
7419
|
# Runs when any .loki/magic/specs/*.md changed since last iteration.
|
|
@@ -7583,16 +7720,28 @@ run_code_review() {
|
|
|
7583
7720
|
review_id="review-$(date -u +%Y%m%dT%H%M%SZ)-${ITERATION_COUNT:-0}"
|
|
7584
7721
|
mkdir -p "$review_dir/$review_id"
|
|
7585
7722
|
|
|
7586
|
-
# Get diff from last commit (staged changes)
|
|
7723
|
+
# Get diff from last commit (staged changes).
|
|
7724
|
+
#
|
|
7725
|
+
# Finding #596 (HIGH): exclude .loki/ and .git/ from the review diff via git
|
|
7726
|
+
# pathspec. When .loki/ is git-tracked the diff bloats (observed 2.18MB of
|
|
7727
|
+
# runtime state), the reviewer prompt overflows, the model returns EMPTY, and
|
|
7728
|
+
# every reviewer records NO_OUTPUT -> the gate passes with ZERO real review.
|
|
7729
|
+
# The evidence gate already excludes .loki/ (see the grep -vE near the
|
|
7730
|
+
# porcelain read); mirror that here so the code-review gate is never defeated
|
|
7731
|
+
# by Loki's own state. Behavior is identical when .loki/ is untracked (the
|
|
7732
|
+
# common case) because git ignores the exclude pathspec for paths it does not
|
|
7733
|
+
# track. ':(exclude).git/' is harmless (git never diffs .git/) and is kept
|
|
7734
|
+
# only for parity with the evidence-gate exclusion list.
|
|
7735
|
+
local _review_pathspec=(-- . ':(exclude).loki/' ':(exclude).git/' ':(exclude)**/.loki/**')
|
|
7587
7736
|
local diff_content
|
|
7588
|
-
diff_content=$(git -C "${TARGET_DIR:-.}" diff HEAD~1 2>/dev/null || git -C "${TARGET_DIR:-.}" diff --cached 2>/dev/null || echo "")
|
|
7737
|
+
diff_content=$(git -C "${TARGET_DIR:-.}" diff HEAD~1 "${_review_pathspec[@]}" 2>/dev/null || git -C "${TARGET_DIR:-.}" diff --cached "${_review_pathspec[@]}" 2>/dev/null || echo "")
|
|
7589
7738
|
if [ -z "$diff_content" ]; then
|
|
7590
7739
|
log_info "Code review: No diff to review, skipping"
|
|
7591
7740
|
return 0
|
|
7592
7741
|
fi
|
|
7593
7742
|
|
|
7594
7743
|
local changed_files
|
|
7595
|
-
changed_files=$(git -C "${TARGET_DIR:-.}" diff --name-only HEAD~1 2>/dev/null || git -C "${TARGET_DIR:-.}" diff --name-only --cached 2>/dev/null || echo "")
|
|
7744
|
+
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 "")
|
|
7596
7745
|
|
|
7597
7746
|
log_header "CODE REVIEW: $review_id"
|
|
7598
7747
|
|
|
@@ -7980,6 +8129,14 @@ BUILD_PROMPT
|
|
|
7980
8129
|
local pass_count=0
|
|
7981
8130
|
local fail_count=0
|
|
7982
8131
|
local verdicts_summary=""
|
|
8132
|
+
# Finding #596 FIX A2 (HIGH): count REAL verdicts (a reviewer file that
|
|
8133
|
+
# exists, is non-empty, AND carries a recognized VERDICT: PASS|FAIL line).
|
|
8134
|
+
# A review where every reviewer produced no usable verdict (all NO_OUTPUT,
|
|
8135
|
+
# e.g. the model returned EMPTY because a .loki-bloated prompt overflowed)
|
|
8136
|
+
# must NOT silently pass with pass_count=0/fail_count=0/has_blocking=false.
|
|
8137
|
+
# Such a review proves nothing, so we treat it as INCONCLUSIVE -> blocking.
|
|
8138
|
+
local real_verdict_count=0
|
|
8139
|
+
local no_output_count=0
|
|
7983
8140
|
|
|
7984
8141
|
for i in $(seq 0 $((reviewer_count - 1))); do
|
|
7985
8142
|
local reviewer_name
|
|
@@ -7989,6 +8146,7 @@ BUILD_PROMPT
|
|
|
7989
8146
|
if [ ! -f "$review_output" ] || [ ! -s "$review_output" ]; then
|
|
7990
8147
|
log_warn "Reviewer $reviewer_name produced no output"
|
|
7991
8148
|
verdicts_summary="${verdicts_summary}${reviewer_name}:NO_OUTPUT "
|
|
8149
|
+
((no_output_count++))
|
|
7992
8150
|
continue
|
|
7993
8151
|
fi
|
|
7994
8152
|
|
|
@@ -7996,6 +8154,22 @@ BUILD_PROMPT
|
|
|
7996
8154
|
local verdict
|
|
7997
8155
|
verdict=$(grep -i "^VERDICT:" "$review_output" | head -1 | sed 's/^VERDICT:[[:space:]]*//' | tr '[:lower:]' '[:upper:]' | tr -d '[:space:]')
|
|
7998
8156
|
|
|
8157
|
+
# FIX A2: a "real verdict" is the PRESENCE of a non-empty VERDICT: line,
|
|
8158
|
+
# not a specific token. A non-empty file with NO VERDICT line (garbage or
|
|
8159
|
+
# a truncated reply) previously counted as PASS and could approve the gate
|
|
8160
|
+
# on a meaningless file; now it is a non-verdict (not real, not a pass).
|
|
8161
|
+
# We deliberately keep the original non-FAIL=pass semantics for any file
|
|
8162
|
+
# that DOES carry a verdict line (PASS, APPROVE, "PASS with concerns",
|
|
8163
|
+
# etc. all count as pass) so verbose-but-real verdicts are never
|
|
8164
|
+
# false-blocked. The only added block relative to shipped behavior is the
|
|
8165
|
+
# zero-real-verdicts (all-empty) case.
|
|
8166
|
+
if [ -z "$verdict" ]; then
|
|
8167
|
+
log_warn "Reviewer $reviewer_name produced no VERDICT line (empty or unparseable reply)"
|
|
8168
|
+
verdicts_summary="${verdicts_summary}${reviewer_name}:NO_VERDICT "
|
|
8169
|
+
((no_output_count++))
|
|
8170
|
+
continue
|
|
8171
|
+
fi
|
|
8172
|
+
((real_verdict_count++))
|
|
7999
8173
|
if [ "$verdict" = "FAIL" ]; then
|
|
8000
8174
|
((fail_count++))
|
|
8001
8175
|
# Check for Critical/High severity findings
|
|
@@ -8012,6 +8186,23 @@ BUILD_PROMPT
|
|
|
8012
8186
|
verdicts_summary="${verdicts_summary}${reviewer_name}:${verdict:-UNKNOWN} "
|
|
8013
8187
|
done
|
|
8014
8188
|
|
|
8189
|
+
# Finding #596 FIX A2: zero real verdicts when reviewers were expected =>
|
|
8190
|
+
# INCONCLUSIVE => blocking. Optional bounded retry first (LOKI_REVIEW_RETRY=1,
|
|
8191
|
+
# default on) so a transient empty-output blip does not hard-block; the retry
|
|
8192
|
+
# re-runs the whole review with the (now .loki-excluded) diff. Opt out of the
|
|
8193
|
+
# block entirely with LOKI_REVIEW_INCONCLUSIVE_BLOCK=0 (records, never blocks).
|
|
8194
|
+
local review_inconclusive=false
|
|
8195
|
+
if [ "$reviewer_count" -gt 0 ] && [ "$real_verdict_count" -eq 0 ]; then
|
|
8196
|
+
review_inconclusive=true
|
|
8197
|
+
log_error "CODE REVIEW INCONCLUSIVE: 0 of $reviewer_count reviewers returned a usable verdict (no_output=$no_output_count)"
|
|
8198
|
+
log_error " An all-empty review proves nothing; refusing to pass the gate on zero real verdicts."
|
|
8199
|
+
if [ "${LOKI_REVIEW_RETRY:-1}" = "1" ] && [ "${_LOKI_REVIEW_RETRYING:-0}" != "1" ]; then
|
|
8200
|
+
log_warn " Retrying code review once (LOKI_REVIEW_RETRY=1)..."
|
|
8201
|
+
_LOKI_REVIEW_RETRYING=1 run_code_review
|
|
8202
|
+
return $?
|
|
8203
|
+
fi
|
|
8204
|
+
fi
|
|
8205
|
+
|
|
8015
8206
|
# Save aggregate results via python3 + env vars (no shell interpolation in JSON)
|
|
8016
8207
|
export LOKI_REVIEW_AGG_FILE="$review_dir/$review_id/aggregate.json"
|
|
8017
8208
|
export LOKI_REVIEW_AGG_ID="$review_id"
|
|
@@ -8020,6 +8211,8 @@ BUILD_PROMPT
|
|
|
8020
8211
|
export LOKI_REVIEW_AGG_FAIL="$fail_count"
|
|
8021
8212
|
export LOKI_REVIEW_AGG_BLOCKING="$has_blocking"
|
|
8022
8213
|
export LOKI_REVIEW_AGG_VERDICTS="$verdicts_summary"
|
|
8214
|
+
export LOKI_REVIEW_AGG_REAL="$real_verdict_count"
|
|
8215
|
+
export LOKI_REVIEW_AGG_INCONCLUSIVE="$review_inconclusive"
|
|
8023
8216
|
python3 << 'AGG_SCRIPT'
|
|
8024
8217
|
import json, os
|
|
8025
8218
|
result = {
|
|
@@ -8028,6 +8221,8 @@ result = {
|
|
|
8028
8221
|
"pass_count": int(os.environ["LOKI_REVIEW_AGG_PASS"]),
|
|
8029
8222
|
"fail_count": int(os.environ["LOKI_REVIEW_AGG_FAIL"]),
|
|
8030
8223
|
"has_blocking": os.environ["LOKI_REVIEW_AGG_BLOCKING"] == "true",
|
|
8224
|
+
"real_verdict_count": int(os.environ["LOKI_REVIEW_AGG_REAL"]),
|
|
8225
|
+
"inconclusive": os.environ["LOKI_REVIEW_AGG_INCONCLUSIVE"] == "true",
|
|
8031
8226
|
"verdicts": os.environ["LOKI_REVIEW_AGG_VERDICTS"].strip()
|
|
8032
8227
|
}
|
|
8033
8228
|
with open(os.environ["LOKI_REVIEW_AGG_FILE"], "w") as f:
|
|
@@ -8035,12 +8230,15 @@ with open(os.environ["LOKI_REVIEW_AGG_FILE"], "w") as f:
|
|
|
8035
8230
|
AGG_SCRIPT
|
|
8036
8231
|
unset LOKI_REVIEW_AGG_FILE LOKI_REVIEW_AGG_ID LOKI_REVIEW_AGG_ITER
|
|
8037
8232
|
unset LOKI_REVIEW_AGG_PASS LOKI_REVIEW_AGG_FAIL LOKI_REVIEW_AGG_BLOCKING LOKI_REVIEW_AGG_VERDICTS
|
|
8233
|
+
unset LOKI_REVIEW_AGG_REAL LOKI_REVIEW_AGG_INCONCLUSIVE
|
|
8038
8234
|
|
|
8039
8235
|
emit_event_json "code_review_complete" \
|
|
8040
8236
|
"review_id=$review_id" \
|
|
8041
8237
|
"pass_count=$pass_count" \
|
|
8042
8238
|
"fail_count=$fail_count" \
|
|
8043
8239
|
"has_blocking=$has_blocking" \
|
|
8240
|
+
"real_verdict_count=$real_verdict_count" \
|
|
8241
|
+
"inconclusive=$review_inconclusive" \
|
|
8044
8242
|
"iteration=$ITERATION_COUNT"
|
|
8045
8243
|
|
|
8046
8244
|
# Anti-sycophancy check: unanimous PASS is suspicious
|
|
@@ -8059,6 +8257,20 @@ AGG_SCRIPT
|
|
|
8059
8257
|
return 1
|
|
8060
8258
|
fi
|
|
8061
8259
|
|
|
8260
|
+
# Finding #596 FIX A2: an inconclusive review (zero real verdicts, retry
|
|
8261
|
+
# already exhausted or disabled) blocks unless explicitly opted out. This is
|
|
8262
|
+
# the 'verified before done' promise: a review that produced no usable verdict
|
|
8263
|
+
# cannot stand in for a real review.
|
|
8264
|
+
if [ "$review_inconclusive" = "true" ]; then
|
|
8265
|
+
if [ "${LOKI_REVIEW_INCONCLUSIVE_BLOCK:-1}" = "0" ]; then
|
|
8266
|
+
log_warn "Code review inconclusive (0/$reviewer_count real verdicts) but LOKI_REVIEW_INCONCLUSIVE_BLOCK=0 - not blocking"
|
|
8267
|
+
return 0
|
|
8268
|
+
fi
|
|
8269
|
+
log_error "CODE REVIEW BLOCKED: inconclusive (0/$reviewer_count reviewers returned a usable verdict)"
|
|
8270
|
+
log_error " Review details: $review_dir/$review_id/ ; opt out with LOKI_REVIEW_INCONCLUSIVE_BLOCK=0"
|
|
8271
|
+
return 1
|
|
8272
|
+
fi
|
|
8273
|
+
|
|
8062
8274
|
log_info "Code review passed ($pass_count/$reviewer_count PASS, $fail_count FAIL - no blocking issues)"
|
|
8063
8275
|
return 0
|
|
8064
8276
|
}
|
|
@@ -13758,6 +13970,12 @@ if __name__ == "__main__":
|
|
|
13758
13970
|
fi
|
|
13759
13971
|
fi
|
|
13760
13972
|
fi
|
|
13973
|
+
# Auto-generate docs (default-on) BEFORE the staleness check and the
|
|
13974
|
+
# gate, so neither nags the user to run 'loki docs generate' by hand.
|
|
13975
|
+
# Opt out with LOKI_AUTO_DOCS=false.
|
|
13976
|
+
if [ "$ITERATION_COUNT" -gt 0 ]; then
|
|
13977
|
+
auto_generate_docs_if_needed
|
|
13978
|
+
fi
|
|
13761
13979
|
# Documentation staleness check (v6.75.0)
|
|
13762
13980
|
if [ "$ITERATION_COUNT" -gt 0 ]; then
|
|
13763
13981
|
run_doc_staleness_check
|
|
@@ -13858,6 +14076,15 @@ if __name__ == "__main__":
|
|
|
13858
14076
|
# Completion Council check (v5.25.0) - multi-agent voting on completion
|
|
13859
14077
|
# Runs before completion promise check since council is more comprehensive
|
|
13860
14078
|
log_step "Post-iteration: checking completion council..."
|
|
14079
|
+
# Finding #598 (HIGH): council_should_stop calls council_evidence_gate
|
|
14080
|
+
# internally; ensure fresh test evidence exists first so its test axis
|
|
14081
|
+
# is not half-blind when the quality-gate ladder did not run this
|
|
14082
|
+
# iteration (e.g. LOKI_HARD_GATES=false). Idempotent: the freshness
|
|
14083
|
+
# guard reuses results the ladder already wrote in the common case, so
|
|
14084
|
+
# tests are never run twice per iteration. Best-effort, never blocks.
|
|
14085
|
+
if type ensure_completion_test_evidence &>/dev/null; then
|
|
14086
|
+
ensure_completion_test_evidence || true
|
|
14087
|
+
fi
|
|
13861
14088
|
if type council_should_stop &>/dev/null && council_should_stop; then
|
|
13862
14089
|
echo ""
|
|
13863
14090
|
log_header "COMPLETION COUNCIL: PROJECT COMPLETE"
|
|
@@ -13920,6 +14147,15 @@ if __name__ == "__main__":
|
|
|
13920
14147
|
if [ "$_completion_claimed" = 1 ] && type council_reverify_checklist &>/dev/null; then
|
|
13921
14148
|
council_reverify_checklist 2>/dev/null || true
|
|
13922
14149
|
fi
|
|
14150
|
+
# Finding #598 (HIGH): generate real test evidence before the evidence
|
|
14151
|
+
# gate fires, so the gate's test axis is never half-blind on absent
|
|
14152
|
+
# test-results.json. Only on an actual completion claim (mirrors the
|
|
14153
|
+
# reverify guard above) so the suite does not run every iteration.
|
|
14154
|
+
# Type-guarded + best-effort: never blocks the completion path itself;
|
|
14155
|
+
# the evidence gate below is the decider that reads the file.
|
|
14156
|
+
if [ "$_completion_claimed" = 1 ] && type ensure_completion_test_evidence &>/dev/null; then
|
|
14157
|
+
ensure_completion_test_evidence || true
|
|
14158
|
+
fi
|
|
13923
14159
|
if [ -n "$_gate_block_for_completion" ] && [ "$_completion_claimed" = 1 ]; then
|
|
13924
14160
|
log_warn "Completion claim rejected: code review is BLOCKED for this iteration (Critical/High findings). Fix review issues before completion."
|
|
13925
14161
|
log_warn " Review details under .loki/quality/reviews/ ; gate_failures=${gate_failures}"
|
|
@@ -14199,6 +14435,89 @@ kill_provider_child() {
|
|
|
14199
14435
|
return 1
|
|
14200
14436
|
}
|
|
14201
14437
|
|
|
14438
|
+
# Authoritative self-reap of THIS run's process group on a normal completion.
|
|
14439
|
+
#
|
|
14440
|
+
# Why this exists: a normal completion (council stop / max-iterations /
|
|
14441
|
+
# completion promise) returns from run_autonomous() into main()'s cleanup
|
|
14442
|
+
# block, which reaps the app-runner but NOT the orchestrator's own process
|
|
14443
|
+
# group. The provider agent (claude/codex/...) and any subagents it spawned
|
|
14444
|
+
# share the orchestrator's group; if one detached or was reparented to init,
|
|
14445
|
+
# it survived the `exit` and kept consuming CPU (observed: ~27 min orphan).
|
|
14446
|
+
# The external `loki stop` path (autonomy/loki) already reaps the whole group
|
|
14447
|
+
# via the recorded pgid; this brings the SAME authoritative reap to the
|
|
14448
|
+
# completion path so a clean finish leaves no orphans.
|
|
14449
|
+
#
|
|
14450
|
+
# Foreign-run safety (CRITICAL): this is pgid-scoped to the group THIS run
|
|
14451
|
+
# recorded at .loki/loki.pgid -- it NEVER uses a name-based `pkill claude`
|
|
14452
|
+
# sweep. A concurrent foreign loki run is its own session leader with a
|
|
14453
|
+
# DIFFERENT pgid and a different .loki, so it can never be a member of our
|
|
14454
|
+
# group and is structurally unreachable. The pgid file only exists when this
|
|
14455
|
+
# runner setsid'd into its own session (LOKI_OWN_SESSION=1, recorded at
|
|
14456
|
+
# ~run.sh:15034); in interactive foreground we share the user's shell group,
|
|
14457
|
+
# leave the pgid absent, and skip this reap entirely -- so Ctrl+C semantics
|
|
14458
|
+
# and the user's shell are untouched.
|
|
14459
|
+
reap_own_process_group() {
|
|
14460
|
+
local loki_dir="${TARGET_DIR:-.}/.loki"
|
|
14461
|
+
# Resolve the pgid file the same way main() recorded it (global or per-session).
|
|
14462
|
+
local _reap_pgid_file="$loki_dir/loki.pid"
|
|
14463
|
+
if [ -n "${LOKI_SESSION_ID:-}" ]; then
|
|
14464
|
+
_reap_pgid_file="$loki_dir/sessions/${LOKI_SESSION_ID}/loki.pid"
|
|
14465
|
+
fi
|
|
14466
|
+
_reap_pgid_file="${_reap_pgid_file%.pid}.pgid"
|
|
14467
|
+
[ -f "$_reap_pgid_file" ] || return 0 # interactive / no own session: skip
|
|
14468
|
+
|
|
14469
|
+
local _pgid
|
|
14470
|
+
_pgid=$(cat "$_reap_pgid_file" 2>/dev/null | tr -d ' ')
|
|
14471
|
+
case "$_pgid" in ''|*[!0-9]*) return 0 ;; esac
|
|
14472
|
+
[ "$_pgid" -gt 1 ] 2>/dev/null || return 0 # never touch pgid 0/1
|
|
14473
|
+
|
|
14474
|
+
# Safety: the recorded pgid MUST be our own group. If it isn't (stale file
|
|
14475
|
+
# from a prior run, copied tree), refuse -- killing a group we do not own
|
|
14476
|
+
# could hit unrelated processes.
|
|
14477
|
+
local _my_pgid
|
|
14478
|
+
_my_pgid=$(ps -o pgid= -p $$ 2>/dev/null | tr -d ' ')
|
|
14479
|
+
[ -n "$_my_pgid" ] && [ "$_pgid" = "$_my_pgid" ] || return 0
|
|
14480
|
+
|
|
14481
|
+
# Collect protected pids (dashboard, app-runner, registered children) so the
|
|
14482
|
+
# reap never takes down the shared dashboard if it happens to share our
|
|
14483
|
+
# group. Mirrors the `loki stop` / dashboard reaper protection set.
|
|
14484
|
+
local _protected=" $$ "
|
|
14485
|
+
local _pf _p
|
|
14486
|
+
if [ -d "$loki_dir/pids" ]; then
|
|
14487
|
+
for _pf in "$loki_dir/pids"/*.json; do
|
|
14488
|
+
[ -f "$_pf" ] || continue
|
|
14489
|
+
_p=$(basename "$_pf" .json)
|
|
14490
|
+
case "$_p" in ''|*[!0-9]*) continue ;; esac
|
|
14491
|
+
_protected="${_protected}${_p} "
|
|
14492
|
+
done
|
|
14493
|
+
for _pf in "$loki_dir/pids"/*.pid; do
|
|
14494
|
+
[ -f "$_pf" ] || continue
|
|
14495
|
+
_p=$(cat "$_pf" 2>/dev/null | head -1 | tr -d '[:space:]')
|
|
14496
|
+
[ -n "$_p" ] && _protected="${_protected}${_p} "
|
|
14497
|
+
done
|
|
14498
|
+
fi
|
|
14499
|
+
for _pf in "$loki_dir/dashboard/dashboard.pid" "${HOME}/.loki/dashboard/dashboard.pid"; do
|
|
14500
|
+
[ -f "$_pf" ] && _protected="${_protected}$(cat "$_pf" 2>/dev/null | tr -d ' ') "
|
|
14501
|
+
done
|
|
14502
|
+
|
|
14503
|
+
# Per-pid TERM then KILL of group members, EXCLUDING $$ (so main() survives
|
|
14504
|
+
# to finish its remaining cleanup and exit normally) and protected pids. We
|
|
14505
|
+
# do per-pid (not a blanket `kill -- -PGID`) precisely so $$ and the
|
|
14506
|
+
# dashboard are spared -- a group-wide signal cannot exclude members.
|
|
14507
|
+
local _gp _did=0
|
|
14508
|
+
for _gp in $(ps -axo pid=,pgid= 2>/dev/null | awk -v g="$_pgid" '$2==g{print $1}'); do
|
|
14509
|
+
case "$_protected" in *" $_gp "*) continue ;; esac
|
|
14510
|
+
kill -TERM "$_gp" 2>/dev/null && _did=1
|
|
14511
|
+
done
|
|
14512
|
+
[ "$_did" = "1" ] || return 0
|
|
14513
|
+
sleep 1
|
|
14514
|
+
for _gp in $(ps -axo pid=,pgid= 2>/dev/null | awk -v g="$_pgid" '$2==g{print $1}'); do
|
|
14515
|
+
case "$_protected" in *" $_gp "*) continue ;; esac
|
|
14516
|
+
kill -KILL "$_gp" 2>/dev/null || true
|
|
14517
|
+
done
|
|
14518
|
+
return 0
|
|
14519
|
+
}
|
|
14520
|
+
|
|
14202
14521
|
# Check for human intervention signals
|
|
14203
14522
|
check_human_intervention() {
|
|
14204
14523
|
local loki_dir="${TARGET_DIR:-.}/.loki"
|
|
@@ -14333,6 +14652,11 @@ check_human_intervention() {
|
|
|
14333
14652
|
if type council_reverify_checklist &>/dev/null; then
|
|
14334
14653
|
council_reverify_checklist 2>/dev/null || true
|
|
14335
14654
|
fi
|
|
14655
|
+
# Finding #598 (HIGH): generate real test evidence before the force-review
|
|
14656
|
+
# evidence gate, so the test axis is not half-blind on absent results.
|
|
14657
|
+
if type ensure_completion_test_evidence &>/dev/null; then
|
|
14658
|
+
ensure_completion_test_evidence || true
|
|
14659
|
+
fi
|
|
14336
14660
|
if type council_checklist_gate &>/dev/null && ! council_checklist_gate; then
|
|
14337
14661
|
log_info "Council force-review: blocked by checklist hard gate"
|
|
14338
14662
|
elif type council_evidence_gate &>/dev/null && ! council_evidence_gate; then
|
|
@@ -15205,13 +15529,22 @@ main() {
|
|
|
15205
15529
|
app_runner_cleanup
|
|
15206
15530
|
fi
|
|
15207
15531
|
stop_status_monitor
|
|
15532
|
+
# v7.41.x: authoritatively reap THIS run's process group on a normal
|
|
15533
|
+
# completion (council stop / max-iterations / completion promise), the same
|
|
15534
|
+
# group reap the STOP signal does. Without this, a provider agent that
|
|
15535
|
+
# detached or reparented survived the exit and ran as an orphan (~27 min in
|
|
15536
|
+
# the reported brownfield run). pgid-scoped to .loki/loki.pgid + excludes
|
|
15537
|
+
# $$/dashboard, so it cannot touch a foreign loki run. No-ops in interactive
|
|
15538
|
+
# foreground (no own session => no pgid file), preserving Ctrl+C semantics.
|
|
15539
|
+
reap_own_process_group 2>/dev/null || true
|
|
15208
15540
|
local loki_dir="${TARGET_DIR:-.}/.loki"
|
|
15209
|
-
rm -f "$loki_dir/loki.pid" 2>/dev/null
|
|
15541
|
+
rm -f "$loki_dir/loki.pid" "$loki_dir/loki.pgid" 2>/dev/null
|
|
15210
15542
|
# UT2-13: Clear cli-provider marker on normal session end.
|
|
15211
15543
|
rm -f "$loki_dir/state/cli-provider" 2>/dev/null || true
|
|
15212
15544
|
# Clean up per-session PID file if running with session ID
|
|
15213
15545
|
if [ -n "${LOKI_SESSION_ID:-}" ]; then
|
|
15214
|
-
rm -f "$loki_dir/sessions/${LOKI_SESSION_ID}/loki.pid"
|
|
15546
|
+
rm -f "$loki_dir/sessions/${LOKI_SESSION_ID}/loki.pid" \
|
|
15547
|
+
"$loki_dir/sessions/${LOKI_SESSION_ID}/loki.pgid" 2>/dev/null
|
|
15215
15548
|
fi
|
|
15216
15549
|
# Mark session.json as stopped
|
|
15217
15550
|
if [ -f "$loki_dir/session.json" ]; then
|
package/dashboard/__init__.py
CHANGED
package/dashboard/server.py
CHANGED
|
@@ -6973,17 +6973,110 @@ async def get_council_gate():
|
|
|
6973
6973
|
# App Runner Endpoints (v5.45.0)
|
|
6974
6974
|
# =============================================================================
|
|
6975
6975
|
|
|
6976
|
+
# Health checkpoints are written once per autonomous iteration (the app-runner
|
|
6977
|
+
# watchdog fires from the run.sh iteration loop, not a fixed timer), so a single
|
|
6978
|
+
# long healthy iteration can legitimately leave last_health.checked_at minutes
|
|
6979
|
+
# old. The staleness threshold below is therefore deliberately generous and is
|
|
6980
|
+
# used ONLY when we cannot verify liveness from a real OS pid (e.g. docker
|
|
6981
|
+
# compose, whose main_pid is a short-lived `up -d` subshell). When a real pid is
|
|
6982
|
+
# present, pid liveness -- not the timestamp -- is the authoritative signal, so a
|
|
6983
|
+
# live run is never reported as dead just because a health beat was missed.
|
|
6984
|
+
_APP_RUNNER_STALE_HEALTH_SECONDS = 600
|
|
6985
|
+
|
|
6986
|
+
|
|
6987
|
+
def _pid_is_alive(pid):
|
|
6988
|
+
"""Return True if pid refers to a live process, False if it is gone.
|
|
6989
|
+
|
|
6990
|
+
Uses signal 0 (no signal sent, just an existence/permission probe). Guards
|
|
6991
|
+
against the pid<=0 footgun: kill(0, 0) targets the caller's own process
|
|
6992
|
+
group and would falsely report "alive". A PermissionError means the process
|
|
6993
|
+
exists but is owned by another user (still alive). Any other error is
|
|
6994
|
+
treated as indeterminate (None) so the caller can fall back safely.
|
|
6995
|
+
"""
|
|
6996
|
+
try:
|
|
6997
|
+
pid = int(pid)
|
|
6998
|
+
except (TypeError, ValueError):
|
|
6999
|
+
return None
|
|
7000
|
+
if pid <= 0:
|
|
7001
|
+
return None
|
|
7002
|
+
try:
|
|
7003
|
+
os.kill(pid, 0)
|
|
7004
|
+
return True
|
|
7005
|
+
except ProcessLookupError:
|
|
7006
|
+
return False
|
|
7007
|
+
except PermissionError:
|
|
7008
|
+
return True
|
|
7009
|
+
except OSError:
|
|
7010
|
+
return None
|
|
7011
|
+
|
|
7012
|
+
|
|
7013
|
+
def _health_checked_age_seconds(state):
|
|
7014
|
+
"""Seconds since last_health.checked_at, or None if unparseable/absent."""
|
|
7015
|
+
health = state.get("last_health")
|
|
7016
|
+
if not isinstance(health, dict):
|
|
7017
|
+
return None
|
|
7018
|
+
checked_at = health.get("checked_at")
|
|
7019
|
+
if not checked_at:
|
|
7020
|
+
return None
|
|
7021
|
+
try:
|
|
7022
|
+
ts = datetime.fromisoformat(str(checked_at).replace("Z", "+00:00"))
|
|
7023
|
+
except (ValueError, TypeError):
|
|
7024
|
+
return None
|
|
7025
|
+
if ts.tzinfo is None:
|
|
7026
|
+
ts = ts.replace(tzinfo=timezone.utc)
|
|
7027
|
+
return (datetime.now(timezone.utc) - ts).total_seconds()
|
|
7028
|
+
|
|
7029
|
+
|
|
7030
|
+
def _reconcile_app_runner_liveness(state):
|
|
7031
|
+
"""Correct a frozen state.json so a dead run is not reported as running.
|
|
7032
|
+
|
|
7033
|
+
state.json is written by the app-runner and is not updated once the app or
|
|
7034
|
+
the orchestrator dies, so a crashed run leaves status frozen at "running".
|
|
7035
|
+
Here we cross-check the recorded main_pid against the real OS before
|
|
7036
|
+
returning, and only ever downgrade -- never upgrade -- the status:
|
|
7037
|
+
- recorded running/starting + pid genuinely gone -> "stopped"
|
|
7038
|
+
- recorded running/starting + pid not verifiable +
|
|
7039
|
+
last_health.checked_at older than the threshold -> "stale"
|
|
7040
|
+
Any failure falls back to the raw recorded status (fail open to the writer's
|
|
7041
|
+
own claim rather than fabricating a state). Returns the (possibly modified)
|
|
7042
|
+
state dict.
|
|
7043
|
+
"""
|
|
7044
|
+
if not isinstance(state, dict):
|
|
7045
|
+
return state
|
|
7046
|
+
status = state.get("status")
|
|
7047
|
+
if status not in ("running", "starting"):
|
|
7048
|
+
return state
|
|
7049
|
+
try:
|
|
7050
|
+
alive = _pid_is_alive(state.get("main_pid"))
|
|
7051
|
+
if alive is False:
|
|
7052
|
+
state["status"] = "stopped"
|
|
7053
|
+
state["liveness"] = "pid_gone"
|
|
7054
|
+
return state
|
|
7055
|
+
if alive is None:
|
|
7056
|
+
# Cannot verify via pid (e.g. compose subshell pid). Fall back to
|
|
7057
|
+
# the health-beat freshness with a generous threshold.
|
|
7058
|
+
age = _health_checked_age_seconds(state)
|
|
7059
|
+
if age is not None and age > _APP_RUNNER_STALE_HEALTH_SECONDS:
|
|
7060
|
+
state["status"] = "stale"
|
|
7061
|
+
state["liveness"] = "health_stale"
|
|
7062
|
+
except Exception:
|
|
7063
|
+
# Never let a liveness probe break the status endpoint.
|
|
7064
|
+
return state
|
|
7065
|
+
return state
|
|
7066
|
+
|
|
7067
|
+
|
|
6976
7068
|
@app.get("/api/app-runner/status")
|
|
6977
7069
|
async def get_app_runner_status():
|
|
6978
|
-
"""Get app runner current status."""
|
|
7070
|
+
"""Get app runner current status (with dead-run liveness reconciliation)."""
|
|
6979
7071
|
loki_dir = _get_loki_dir()
|
|
6980
7072
|
state_file = loki_dir / "app-runner" / "state.json"
|
|
6981
7073
|
if not state_file.exists():
|
|
6982
7074
|
return {"status": "not_initialized"}
|
|
6983
7075
|
try:
|
|
6984
|
-
|
|
7076
|
+
state = json.loads(state_file.read_text())
|
|
6985
7077
|
except (json.JSONDecodeError, OSError):
|
|
6986
7078
|
return {"status": "error"}
|
|
7079
|
+
return _reconcile_app_runner_liveness(state)
|
|
6987
7080
|
|
|
6988
7081
|
|
|
6989
7082
|
def _get_log_redactor():
|