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 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.0
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.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
386
+ **v7.19.1 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.19.0
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 terminal state
9837
- # This allows new sessions to start fresh after failures
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
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.19.0"
10
+ __version__ = "7.19.1"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -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
- gate_file = _get_loki_dir() / "council" / "gate-block.json"
6574
- if not gate_file.exists():
6575
- return {"blocked": False, "gates": _DEFAULT_QUALITY_GATES}
6576
- try:
6577
- data = json.loads(gate_file.read_text())
6578
- if "gates" not in data:
6579
- data["gates"] = _DEFAULT_QUALITY_GATES
6580
- return data
6581
- except (json.JSONDecodeError, IOError):
6582
- return {"blocked": False, "gates": _DEFAULT_QUALITY_GATES, "error": "Failed to read gate file"}
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
  # =============================================================================