loki-mode 7.19.0 → 7.19.1
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/completion-council.sh +201 -0
- package/autonomy/run.sh +48 -3
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +40 -11
- package/dashboard/static/index.html +543 -497
- package/docs/INSTALLATION.md +1 -1
- package/docs/VERIFIED-COMPLETION-PLAN.md +462 -0
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/skills/quality-gates.md +30 -0
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Autonomous spec-to-product system. Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product via the RARV-C closure loop, with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v7.19.
|
|
6
|
+
# Loki Mode v7.19.1
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -383,4 +383,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
383
383
|
|
|
384
384
|
---
|
|
385
385
|
|
|
386
|
-
**v7.19.
|
|
386
|
+
**v7.19.1 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.19.
|
|
1
|
+
7.19.1
|
|
@@ -893,6 +893,200 @@ GATE_EOF
|
|
|
893
893
|
return 0
|
|
894
894
|
}
|
|
895
895
|
|
|
896
|
+
#===============================================================================
|
|
897
|
+
# Council Evidence Hard Gate (v7.19.1) - "verified completion"
|
|
898
|
+
#===============================================================================
|
|
899
|
+
# Block the completion-approval path unless there is real on-disk evidence that
|
|
900
|
+
# the run actually shipped: a nonzero git diff vs the run-start SHA AND a green
|
|
901
|
+
# test signal (where a test suite exists). Cloned from council_checklist_gate:
|
|
902
|
+
# return 0 = pass (OK to complete), return 1 = block (treated as CONTINUE).
|
|
903
|
+
# Blocks ONLY on positive fabrication evidence (empty diff, or a runner that
|
|
904
|
+
# actually ran and was red); every inconclusive case passes through so a
|
|
905
|
+
# legitimate completion is never falsely stopped. Default-on; opt out with
|
|
906
|
+
# LOKI_EVIDENCE_GATE=0 (byte-identical to prior behavior, no read/write).
|
|
907
|
+
council_evidence_gate() {
|
|
908
|
+
# Knob first: opt-out is exact-as-today, before any file read or write.
|
|
909
|
+
[ "${LOKI_EVIDENCE_GATE:-1}" = "0" ] && return 0
|
|
910
|
+
|
|
911
|
+
# The gate may run even when the completion council is disabled
|
|
912
|
+
# (LOKI_COUNCIL_ENABLED=false leaves COUNCIL_STATE_DIR unset by council_init),
|
|
913
|
+
# because it now also guards the default completion-promise route. Default
|
|
914
|
+
# the block-report dir to .loki/council so we never write to filesystem root.
|
|
915
|
+
if [ -z "${COUNCIL_STATE_DIR:-}" ]; then
|
|
916
|
+
COUNCIL_STATE_DIR="${TARGET_DIR:-.}/.loki/council"
|
|
917
|
+
fi
|
|
918
|
+
|
|
919
|
+
# --- Evidence check (a): nonzero diff vs run-start SHA (committed UNION working tree) ---
|
|
920
|
+
local base_sha=""
|
|
921
|
+
if [ -n "${_LOKI_RUN_START_SHA:-}" ]; then
|
|
922
|
+
base_sha="$_LOKI_RUN_START_SHA"
|
|
923
|
+
elif [ -f ".loki/state/start-sha" ]; then
|
|
924
|
+
base_sha="$(cat .loki/state/start-sha 2>/dev/null || echo "")"
|
|
925
|
+
fi
|
|
926
|
+
|
|
927
|
+
# diff_fails stays "false" in every inconclusive branch below (no git repo,
|
|
928
|
+
# no baseline). The block decision (block iff diff_fails OR test_fails) thus
|
|
929
|
+
# treats inconclusive as pass-through by construction; no separate flag is
|
|
930
|
+
# read, so none is tracked (avoids SC2034 dead-assignment).
|
|
931
|
+
local diff_fails="false"
|
|
932
|
+
local diff_files=0
|
|
933
|
+
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
934
|
+
# No git repo => cannot prove fabrication => inconclusive => pass-through.
|
|
935
|
+
:
|
|
936
|
+
elif [ -z "$base_sha" ]; then
|
|
937
|
+
# No baseline captured (non-git/zero-commit run, or never set) =>
|
|
938
|
+
# inconclusive => pass-through. Never false-block a legit first run.
|
|
939
|
+
:
|
|
940
|
+
else
|
|
941
|
+
# Count the UNION of three change sources (auto-commit is not guaranteed,
|
|
942
|
+
# so committed-only would false-block a dirty-but-real working tree):
|
|
943
|
+
# committed since baseline, unstaged, staged.
|
|
944
|
+
local committed_files unstaged_files staged_files untracked_files
|
|
945
|
+
if committed_files=$(git diff --name-only "$base_sha" HEAD 2>/dev/null); then
|
|
946
|
+
:
|
|
947
|
+
else
|
|
948
|
+
# Base present but unreachable (e.g. shallow clone): fall back to
|
|
949
|
+
# working-tree diff vs HEAD (mirrors proof-generator.py fallback).
|
|
950
|
+
committed_files=$(git diff --name-only HEAD 2>/dev/null || echo "")
|
|
951
|
+
fi
|
|
952
|
+
unstaged_files=$(git diff --name-only HEAD 2>/dev/null || echo "")
|
|
953
|
+
staged_files=$(git diff --cached --name-only 2>/dev/null || echo "")
|
|
954
|
+
# Untracked new files: a greenfield first run creates files that are not
|
|
955
|
+
# yet committed, staged, or seen by diff HEAD. Without this fourth source
|
|
956
|
+
# the union would be empty and the gate would false-block legitimate new
|
|
957
|
+
# work. --exclude-standard respects .gitignore so build artifacts and
|
|
958
|
+
# node_modules do not count as evidence.
|
|
959
|
+
untracked_files=$(git ls-files --others --exclude-standard 2>/dev/null || echo "")
|
|
960
|
+
# Exclude Loki's own runtime state from the union: .loki/ holds the
|
|
961
|
+
# gate's inputs (e.g. .loki/quality/test-results.json is always present
|
|
962
|
+
# at gate time) and other runtime files that are not gitignored, so
|
|
963
|
+
# counting them would make the gate toothless (the union would never be
|
|
964
|
+
# empty). Loki's own state is not project work / completion evidence.
|
|
965
|
+
local union_files
|
|
966
|
+
union_files=$(printf '%s\n%s\n%s\n%s\n' "$committed_files" "$unstaged_files" "$staged_files" "$untracked_files" | grep -v '^$' | grep -vE '^\.loki/' | sort -u)
|
|
967
|
+
if [ -n "$union_files" ]; then
|
|
968
|
+
diff_files=$(printf '%s\n' "$union_files" | wc -l | tr -d ' ')
|
|
969
|
+
else
|
|
970
|
+
diff_files=0
|
|
971
|
+
fi
|
|
972
|
+
if [ "$diff_files" -eq 0 ]; then
|
|
973
|
+
diff_fails="true"
|
|
974
|
+
fi
|
|
975
|
+
fi
|
|
976
|
+
|
|
977
|
+
# --- Evidence check (b): tests green ---
|
|
978
|
+
local tr_file=".loki/quality/test-results.json"
|
|
979
|
+
# Like diff_fails, test_fails stays "false" on INCONCLUSIVE / missing-file
|
|
980
|
+
# branches, so inconclusive is pass-through by construction and no separate
|
|
981
|
+
# flag is read (avoids SC2034 dead-assignment).
|
|
982
|
+
local test_fails="false"
|
|
983
|
+
local test_runner="none"
|
|
984
|
+
local test_pass="true"
|
|
985
|
+
if [ -f "$tr_file" ]; then
|
|
986
|
+
local test_status
|
|
987
|
+
test_status=$(_TR_FILE="$tr_file" python3 -c "
|
|
988
|
+
import json, os, sys
|
|
989
|
+
tr_file = os.environ['_TR_FILE']
|
|
990
|
+
try:
|
|
991
|
+
with open(tr_file) as f:
|
|
992
|
+
d = json.load(f)
|
|
993
|
+
except (json.JSONDecodeError, IOError, KeyError, ValueError):
|
|
994
|
+
print('INCONCLUSIVE:none:true')
|
|
995
|
+
sys.exit(0)
|
|
996
|
+
runner = d.get('runner', 'none')
|
|
997
|
+
passed = d.get('pass', True)
|
|
998
|
+
if runner == 'none':
|
|
999
|
+
print('PASS:none:true')
|
|
1000
|
+
elif passed is False:
|
|
1001
|
+
print('FAIL:%s:false' % runner)
|
|
1002
|
+
else:
|
|
1003
|
+
print('PASS:%s:true' % runner)
|
|
1004
|
+
" 2>/dev/null || echo "INCONCLUSIVE:none:true")
|
|
1005
|
+
local _verdict="${test_status%%:*}"
|
|
1006
|
+
local _rest="${test_status#*:}"
|
|
1007
|
+
test_runner="${_rest%%:*}"
|
|
1008
|
+
test_pass="${_rest#*:}"
|
|
1009
|
+
if [ "$_verdict" = "FAIL" ]; then
|
|
1010
|
+
test_fails="true"
|
|
1011
|
+
fi
|
|
1012
|
+
# INCONCLUSIVE => test_fails stays "false" => pass-through.
|
|
1013
|
+
fi
|
|
1014
|
+
# Missing test-results.json (the else of the -f check) likewise leaves
|
|
1015
|
+
# test_fails="false" => inconclusive => pass-through (no file = no gate).
|
|
1016
|
+
|
|
1017
|
+
# --- Block decision: block iff DIFF FAILS or TEST FAILS ---
|
|
1018
|
+
if [ "$diff_fails" != "true" ] && [ "$test_fails" != "true" ]; then
|
|
1019
|
+
# Gate passes: remove any stale block report.
|
|
1020
|
+
if [ -f "$COUNCIL_STATE_DIR/evidence-block.json" ]; then
|
|
1021
|
+
rm -f "$COUNCIL_STATE_DIR/evidence-block.json"
|
|
1022
|
+
fi
|
|
1023
|
+
return 0
|
|
1024
|
+
fi
|
|
1025
|
+
|
|
1026
|
+
# Determine reason and build human-readable failure list.
|
|
1027
|
+
local reason="no_evidence_of_completion"
|
|
1028
|
+
if [ "$diff_fails" = "true" ] && [ "$test_fails" = "true" ]; then
|
|
1029
|
+
reason="empty_diff_and_tests_red"
|
|
1030
|
+
elif [ "$diff_fails" = "true" ]; then
|
|
1031
|
+
reason="empty_diff"
|
|
1032
|
+
elif [ "$test_fails" = "true" ]; then
|
|
1033
|
+
reason="tests_red"
|
|
1034
|
+
fi
|
|
1035
|
+
|
|
1036
|
+
local failures=""
|
|
1037
|
+
if [ "$diff_fails" = "true" ]; then
|
|
1038
|
+
failures="empty git diff vs run-start SHA (nothing shipped)"
|
|
1039
|
+
log_warn "[Council] Evidence gate BLOCKED: empty git diff vs run-start SHA"
|
|
1040
|
+
fi
|
|
1041
|
+
if [ "$test_fails" = "true" ]; then
|
|
1042
|
+
if [ -n "$failures" ]; then
|
|
1043
|
+
failures="${failures}|test runner '${test_runner}' ran and was red"
|
|
1044
|
+
else
|
|
1045
|
+
failures="test runner '${test_runner}' ran and was red"
|
|
1046
|
+
fi
|
|
1047
|
+
log_warn "[Council] Evidence gate BLOCKED: test runner '${test_runner}' was red"
|
|
1048
|
+
fi
|
|
1049
|
+
|
|
1050
|
+
# Rail 3 (one-step self-rescue): the terminal user (no dashboard open) must
|
|
1051
|
+
# be told, right at the block site, how to opt out of the gate. A false
|
|
1052
|
+
# block (e.g. a pre-existing red test the run cannot fix) is otherwise a
|
|
1053
|
+
# dead-end until max-iterations. This single line keeps the gate safe to
|
|
1054
|
+
# ship default-on.
|
|
1055
|
+
log_warn "[Council] Run will keep iterating until there is real evidence of completion. To opt out: set LOKI_EVIDENCE_GATE=0"
|
|
1056
|
+
|
|
1057
|
+
# Write block report (atomic temp+mv, mirroring gate-block.json).
|
|
1058
|
+
mkdir -p "$COUNCIL_STATE_DIR" 2>/dev/null || true
|
|
1059
|
+
local ev_file="$COUNCIL_STATE_DIR/evidence-block.json"
|
|
1060
|
+
local ev_tmp="${ev_file}.tmp"
|
|
1061
|
+
local timestamp
|
|
1062
|
+
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1063
|
+
local failures_json diff_ok tests_ok base_for_json
|
|
1064
|
+
failures_json=$(_FAILURES="$failures" python3 -c "
|
|
1065
|
+
import json, os
|
|
1066
|
+
items = [s for s in os.environ['_FAILURES'].split('|') if s]
|
|
1067
|
+
print(json.dumps(items[:5]))
|
|
1068
|
+
" 2>/dev/null || echo '[]')
|
|
1069
|
+
if [ "$diff_fails" = "true" ]; then diff_ok="false"; else diff_ok="true"; fi
|
|
1070
|
+
if [ "$test_fails" = "true" ]; then tests_ok="false"; else tests_ok="true"; fi
|
|
1071
|
+
base_for_json="${base_sha:-}"
|
|
1072
|
+
cat > "$ev_tmp" << EVIDENCE_EOF
|
|
1073
|
+
{
|
|
1074
|
+
"status": "blocked",
|
|
1075
|
+
"blocked": true,
|
|
1076
|
+
"blocked_at": "$timestamp",
|
|
1077
|
+
"iteration": ${ITERATION_COUNT:-0},
|
|
1078
|
+
"reason": "$reason",
|
|
1079
|
+
"checks": {
|
|
1080
|
+
"diff": {"ok": $diff_ok, "base_sha": "$base_for_json", "files_changed": $diff_files, "sources": "committed|unstaged|staged|untracked union"},
|
|
1081
|
+
"tests": {"ok": $tests_ok, "runner": "$test_runner", "pass": $test_pass}
|
|
1082
|
+
},
|
|
1083
|
+
"failures": $failures_json
|
|
1084
|
+
}
|
|
1085
|
+
EVIDENCE_EOF
|
|
1086
|
+
mv "$ev_tmp" "$ev_file"
|
|
1087
|
+
return 1
|
|
1088
|
+
}
|
|
1089
|
+
|
|
896
1090
|
#===============================================================================
|
|
897
1091
|
# Council Member Review - Individual member evaluation
|
|
898
1092
|
#===============================================================================
|
|
@@ -1524,6 +1718,13 @@ council_evaluate() {
|
|
|
1524
1718
|
return 1 # CONTINUE - can't complete with critical failures
|
|
1525
1719
|
fi
|
|
1526
1720
|
|
|
1721
|
+
# Phase 2.5 (v7.19.1): evidence hard gate - block completion unless there is
|
|
1722
|
+
# real evidence that files changed AND tests are green.
|
|
1723
|
+
if ! council_evidence_gate; then
|
|
1724
|
+
log_info "[Council] Completion blocked by evidence hard gate"
|
|
1725
|
+
return 1 # CONTINUE - cannot complete without real evidence
|
|
1726
|
+
fi
|
|
1727
|
+
|
|
1527
1728
|
# Compute threshold using the same ceiling(2/3) formula as council_vote and council_aggregate_votes
|
|
1528
1729
|
local _eval_threshold=$(( (COUNCIL_SIZE * 2 + 2) / 3 ))
|
|
1529
1730
|
|
package/autonomy/run.sh
CHANGED
|
@@ -9833,10 +9833,24 @@ except (json.JSONDecodeError, KeyError, TypeError, OSError):
|
|
|
9833
9833
|
# BUG-RUN-003: Restore ITERATION_COUNT from persisted state
|
|
9834
9834
|
ITERATION_COUNT=$(python3 -c "import json; print(json.load(open('.loki/autonomy-state.json')).get('iterationCount', 0))" 2>/dev/null || echo "0")
|
|
9835
9835
|
|
|
9836
|
-
# Reset retry count if previous session ended in a
|
|
9837
|
-
#
|
|
9836
|
+
# Reset retry count + iteration count if previous session ended in a
|
|
9837
|
+
# terminal state. A fresh `loki start` after a terminal run is a NEW
|
|
9838
|
+
# run and must start from a fresh baseline. This matters for the
|
|
9839
|
+
# verified-completion evidence gate (v7.19.1): the run-start SHA
|
|
9840
|
+
# recapture in run_autonomous is gated on ITERATION_COUNT==0, so a
|
|
9841
|
+
# stale count here would leave the gate diffing against the PRIOR
|
|
9842
|
+
# run's start SHA (toothless). Terminal states covered:
|
|
9843
|
+
# - failure terminals: failed|max_iterations_reached|
|
|
9844
|
+
# max_retries_exceeded|exited
|
|
9845
|
+
# - success terminals: council_approved|council_force_approved|
|
|
9846
|
+
# completion_promise_fulfilled (the run finished; a re-run is new)
|
|
9847
|
+
# - running: previous process died mid-run (crash); nothing resumes
|
|
9848
|
+
# from "running" (paused/interrupted are the explicit resume
|
|
9849
|
+
# signals), so this closes the crash-rerun toothless-gate path.
|
|
9850
|
+
# Deliberately NOT reset (genuine resume / user re-run expecting to
|
|
9851
|
+
# continue): paused, interrupted, budget_exceeded, stopped.
|
|
9838
9852
|
case "$prev_status" in
|
|
9839
|
-
failed|max_iterations_reached|max_retries_exceeded|exited)
|
|
9853
|
+
failed|max_iterations_reached|max_retries_exceeded|exited|council_approved|council_force_approved|completion_promise_fulfilled|running)
|
|
9840
9854
|
log_info "Previous session ended with status: $prev_status. Resetting for new session."
|
|
9841
9855
|
RETRY_COUNT=0
|
|
9842
9856
|
ITERATION_COUNT=0
|
|
@@ -11412,6 +11426,21 @@ run_autonomous() {
|
|
|
11412
11426
|
load_state
|
|
11413
11427
|
local retry=$RETRY_COUNT
|
|
11414
11428
|
|
|
11429
|
+
# Capture run-start SHA for the evidence hard gate (v7.19.1).
|
|
11430
|
+
# Fresh-run-aware: recapture HEAD when ITERATION_COUNT==0 (fresh invocation,
|
|
11431
|
+
# reset, or corrupted/missing baseline); preserve only on a genuine resume
|
|
11432
|
+
# (ITERATION_COUNT>0) so the diff window is not moved mid-run. A naive
|
|
11433
|
+
# set-if-absent would leave a stale first-run baseline on every later run,
|
|
11434
|
+
# making the gate toothless. Non-git or zero-commit repos write an empty
|
|
11435
|
+
# file, which the gate treats as inconclusive (pass-through).
|
|
11436
|
+
local _start_sha_file=".loki/state/start-sha"
|
|
11437
|
+
mkdir -p ".loki/state"
|
|
11438
|
+
if [ "${ITERATION_COUNT:-0}" -eq 0 ] || [ ! -s "$_start_sha_file" ]; then
|
|
11439
|
+
(cd "${TARGET_DIR:-.}" && git rev-parse HEAD 2>/dev/null) > "$_start_sha_file" 2>/dev/null || true
|
|
11440
|
+
fi
|
|
11441
|
+
_LOKI_RUN_START_SHA="$(cat "$_start_sha_file" 2>/dev/null || echo "")"
|
|
11442
|
+
export _LOKI_RUN_START_SHA
|
|
11443
|
+
|
|
11415
11444
|
# Notify dashboard of active project directory (for AI Chat cross-directory usage)
|
|
11416
11445
|
if command -v curl &>/dev/null; then
|
|
11417
11446
|
local project_cwd
|
|
@@ -12413,6 +12442,20 @@ if __name__ == "__main__":
|
|
|
12413
12442
|
log_warn " Review details under .loki/quality/reviews/ ; gate_failures=${gate_failures}"
|
|
12414
12443
|
_gate_block_for_completion=""
|
|
12415
12444
|
# Fall through; the gate-failed loop continues normally
|
|
12445
|
+
# v7.19.1: the verified-completion evidence gate must also guard the
|
|
12446
|
+
# DEFAULT completion route (a completion claim via loki_complete_task
|
|
12447
|
+
# / the completion-promise text), not only the interval-gated council
|
|
12448
|
+
# path. Otherwise an agent can self-assert "done" with an empty diff
|
|
12449
|
+
# and red tests and exit as completion_promise_fulfilled, bypassing
|
|
12450
|
+
# the gate entirely -- exactly the fabrication this feature prevents.
|
|
12451
|
+
# Mirrors the code_review block above (B-17). Opt-out: the gate's own
|
|
12452
|
+
# LOKI_EVIDENCE_GATE=0 (council_evidence_gate returns 0 immediately
|
|
12453
|
+
# when disabled, so this branch never fires). Gate output (reason +
|
|
12454
|
+
# opt-out hint) is printed by council_evidence_gate itself.
|
|
12455
|
+
elif check_completion_promise "$iter_output" && type council_evidence_gate &>/dev/null && ! council_evidence_gate; then
|
|
12456
|
+
log_warn "Completion claim rejected: evidence gate found no proof of completion (empty diff vs run-start SHA, or red tests)."
|
|
12457
|
+
log_warn " Details under .loki/council/evidence-block.json ; opt out with LOKI_EVIDENCE_GATE=0"
|
|
12458
|
+
# Fall through; keep iterating until there is real evidence.
|
|
12416
12459
|
elif check_completion_promise "$iter_output"; then
|
|
12417
12460
|
echo ""
|
|
12418
12461
|
if [ -n "$COMPLETION_PROMISE" ]; then
|
|
@@ -12765,6 +12808,8 @@ check_human_intervention() {
|
|
|
12765
12808
|
rm -f "$loki_dir/signals/COUNCIL_REVIEW_REQUESTED"
|
|
12766
12809
|
if type council_checklist_gate &>/dev/null && ! council_checklist_gate; then
|
|
12767
12810
|
log_info "Council force-review: blocked by checklist hard gate"
|
|
12811
|
+
elif type council_evidence_gate &>/dev/null && ! council_evidence_gate; then
|
|
12812
|
+
log_info "Council force-review: blocked by evidence hard gate"
|
|
12768
12813
|
elif type council_vote &>/dev/null && council_vote; then
|
|
12769
12814
|
log_header "COMPLETION COUNCIL: FORCE REVIEW - PROJECT COMPLETE"
|
|
12770
12815
|
# BUG #17 fix: Write COMPLETED marker, generate council report, and
|
package/dashboard/__init__.py
CHANGED
package/dashboard/server.py
CHANGED
|
@@ -6569,17 +6569,46 @@ _DEFAULT_QUALITY_GATES = [
|
|
|
6569
6569
|
|
|
6570
6570
|
@app.get("/api/council/gate")
|
|
6571
6571
|
async def get_council_gate():
|
|
6572
|
-
"""Get council hard gate status.
|
|
6573
|
-
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
|
|
6578
|
-
|
|
6579
|
-
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6572
|
+
"""Get council hard gate status.
|
|
6573
|
+
|
|
6574
|
+
Surfaces TWO independent hard gates, both written to .loki/council/:
|
|
6575
|
+
- gate-block.json: the legacy quality hard gate
|
|
6576
|
+
- evidence-block.json: the verified-completion evidence gate (v7.19.1),
|
|
6577
|
+
which blocks STOP unless there is real evidence
|
|
6578
|
+
(nonzero diff vs run-start SHA AND green tests).
|
|
6579
|
+
Either being present means completion is blocked. The response keeps the
|
|
6580
|
+
legacy top-level shape (blocked/gates) for backward compatibility and adds
|
|
6581
|
+
an `evidence` key so the UI can show WHY a verified-completion block fired.
|
|
6582
|
+
"""
|
|
6583
|
+
council_dir = _get_loki_dir() / "council"
|
|
6584
|
+
gate_file = council_dir / "gate-block.json"
|
|
6585
|
+
evidence_file = council_dir / "evidence-block.json"
|
|
6586
|
+
|
|
6587
|
+
# Legacy quality gate (backward-compatible top level).
|
|
6588
|
+
if gate_file.exists():
|
|
6589
|
+
try:
|
|
6590
|
+
data = json.loads(gate_file.read_text())
|
|
6591
|
+
if "gates" not in data:
|
|
6592
|
+
data["gates"] = _DEFAULT_QUALITY_GATES
|
|
6593
|
+
except (json.JSONDecodeError, IOError):
|
|
6594
|
+
data = {"blocked": False, "gates": _DEFAULT_QUALITY_GATES, "error": "Failed to read gate file"}
|
|
6595
|
+
else:
|
|
6596
|
+
data = {"blocked": False, "gates": _DEFAULT_QUALITY_GATES}
|
|
6597
|
+
|
|
6598
|
+
# Verified-completion evidence gate (additive).
|
|
6599
|
+
if evidence_file.exists():
|
|
6600
|
+
try:
|
|
6601
|
+
evidence = json.loads(evidence_file.read_text())
|
|
6602
|
+
except (json.JSONDecodeError, IOError):
|
|
6603
|
+
evidence = {"blocked": True, "error": "Failed to read evidence-block file"}
|
|
6604
|
+
data["evidence"] = evidence
|
|
6605
|
+
# If either gate blocks, the overall status is blocked.
|
|
6606
|
+
if evidence.get("blocked"):
|
|
6607
|
+
data["blocked"] = True
|
|
6608
|
+
else:
|
|
6609
|
+
data["evidence"] = {"blocked": False}
|
|
6610
|
+
|
|
6611
|
+
return data
|
|
6583
6612
|
|
|
6584
6613
|
|
|
6585
6614
|
# =============================================================================
|