loki-mode 7.19.0 → 7.19.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/autonomy/run.sh CHANGED
@@ -124,6 +124,26 @@
124
124
  # LOKI_NOTIFICATIONS - Enable desktop notifications (default: true)
125
125
  # LOKI_NOTIFICATION_SOUND - Play sound with notifications (default: true)
126
126
  #
127
+ # Uncertainty-Gated Escalation (v7.19.2, default-on):
128
+ # LOKI_UNCERTAINTY_ESCALATION - Master on/off for proactive stuck-escalation (default: 1; set 0 to
129
+ # disable; byte-identical when off). Decision lives in
130
+ # completion-council.sh (uncertainty_should_escalate); action in run.sh.
131
+ # NOTE: AUTONOMY_MODE defaults to "perpetual"; in perpetual mode PAUSE
132
+ # is auto-cleared by check_human_intervention, so escalation degrades
133
+ # to notify-only (notification fires, run does NOT halt).
134
+ # LOKI_UNCERTAINTY_ROUNDS - Consecutive rounds where >=2 of 3 proxies must co-occur before
135
+ # escalating (default: 2; recommended range 2-3). Debounces transient
136
+ # noise: a single hot proxy never escalates alone.
137
+ # LOKI_UNCERTAINTY_NOCHANGE_MIN - Proxy 1 threshold: consecutive_no_change value that marks p1 hot.
138
+ # (default: COUNCIL_STAGNATION_LIMIT - 1, i.e. one below the circuit-
139
+ # breaker limit so escalation fires before the breaker ends the run).
140
+ # Floored at 1 at runtime.
141
+ # LOKI_UNCERTAINTY_SPLIT_ROUNDS - Proxy 3 threshold: number of consecutive trailing council verdicts
142
+ # that must be REJECTED-with-approver (split) to mark p3 hot
143
+ # (default: 2). Between council votes p3 may be stale; it is always
144
+ # fresh when proxy 1 is hot because proxy 1 hot forces a circuit-
145
+ # breaker vote that refreshes verdicts.
146
+ #
127
147
  # Human Intervention (Auto-Claude pattern):
128
148
  # PAUSE file: touch .loki/PAUSE - pauses after current session
129
149
  # HUMAN_INPUT.md: echo "instructions" > .loki/HUMAN_INPUT.md
@@ -286,6 +306,10 @@ parse_simple_yaml() {
286
306
  set_from_yaml "$file" "completion.council.check_interval" "LOKI_COUNCIL_CHECK_INTERVAL"
287
307
  set_from_yaml "$file" "completion.council.min_iterations" "LOKI_COUNCIL_MIN_ITERATIONS"
288
308
  set_from_yaml "$file" "completion.council.stagnation_limit" "LOKI_COUNCIL_STAGNATION_LIMIT"
309
+ set_from_yaml "$file" "completion.uncertainty.escalation" "LOKI_UNCERTAINTY_ESCALATION"
310
+ set_from_yaml "$file" "completion.uncertainty.rounds" "LOKI_UNCERTAINTY_ROUNDS"
311
+ set_from_yaml "$file" "completion.uncertainty.nochange_min" "LOKI_UNCERTAINTY_NOCHANGE_MIN"
312
+ set_from_yaml "$file" "completion.uncertainty.split_rounds" "LOKI_UNCERTAINTY_SPLIT_ROUNDS"
289
313
 
290
314
  # Model
291
315
  set_from_yaml "$file" "model.prompt_repetition" "LOKI_PROMPT_REPETITION"
@@ -428,6 +452,10 @@ parse_yaml_with_yq() {
428
452
  "completion.council.check_interval:LOKI_COUNCIL_CHECK_INTERVAL"
429
453
  "completion.council.min_iterations:LOKI_COUNCIL_MIN_ITERATIONS"
430
454
  "completion.council.stagnation_limit:LOKI_COUNCIL_STAGNATION_LIMIT"
455
+ "completion.uncertainty.escalation:LOKI_UNCERTAINTY_ESCALATION"
456
+ "completion.uncertainty.rounds:LOKI_UNCERTAINTY_ROUNDS"
457
+ "completion.uncertainty.nochange_min:LOKI_UNCERTAINTY_NOCHANGE_MIN"
458
+ "completion.uncertainty.split_rounds:LOKI_UNCERTAINTY_SPLIT_ROUNDS"
431
459
  "model.prompt_repetition:LOKI_PROMPT_REPETITION"
432
460
  "model.confidence_routing:LOKI_CONFIDENCE_ROUTING"
433
461
  "model.autonomy_mode:LOKI_AUTONOMY_MODE"
@@ -9833,10 +9861,24 @@ except (json.JSONDecodeError, KeyError, TypeError, OSError):
9833
9861
  # BUG-RUN-003: Restore ITERATION_COUNT from persisted state
9834
9862
  ITERATION_COUNT=$(python3 -c "import json; print(json.load(open('.loki/autonomy-state.json')).get('iterationCount', 0))" 2>/dev/null || echo "0")
9835
9863
 
9836
- # Reset retry count if previous session ended in a terminal state
9837
- # This allows new sessions to start fresh after failures
9864
+ # Reset retry count + iteration count if previous session ended in a
9865
+ # terminal state. A fresh `loki start` after a terminal run is a NEW
9866
+ # run and must start from a fresh baseline. This matters for the
9867
+ # verified-completion evidence gate (v7.19.1): the run-start SHA
9868
+ # recapture in run_autonomous is gated on ITERATION_COUNT==0, so a
9869
+ # stale count here would leave the gate diffing against the PRIOR
9870
+ # run's start SHA (toothless). Terminal states covered:
9871
+ # - failure terminals: failed|max_iterations_reached|
9872
+ # max_retries_exceeded|exited
9873
+ # - success terminals: council_approved|council_force_approved|
9874
+ # completion_promise_fulfilled (the run finished; a re-run is new)
9875
+ # - running: previous process died mid-run (crash); nothing resumes
9876
+ # from "running" (paused/interrupted are the explicit resume
9877
+ # signals), so this closes the crash-rerun toothless-gate path.
9878
+ # Deliberately NOT reset (genuine resume / user re-run expecting to
9879
+ # continue): paused, interrupted, budget_exceeded, stopped.
9838
9880
  case "$prev_status" in
9839
- failed|max_iterations_reached|max_retries_exceeded|exited)
9881
+ failed|max_iterations_reached|max_retries_exceeded|exited|council_approved|council_force_approved|completion_promise_fulfilled|running)
9840
9882
  log_info "Previous session ended with status: $prev_status. Resetting for new session."
9841
9883
  RETRY_COUNT=0
9842
9884
  ITERATION_COUNT=0
@@ -11412,6 +11454,21 @@ run_autonomous() {
11412
11454
  load_state
11413
11455
  local retry=$RETRY_COUNT
11414
11456
 
11457
+ # Capture run-start SHA for the evidence hard gate (v7.19.1).
11458
+ # Fresh-run-aware: recapture HEAD when ITERATION_COUNT==0 (fresh invocation,
11459
+ # reset, or corrupted/missing baseline); preserve only on a genuine resume
11460
+ # (ITERATION_COUNT>0) so the diff window is not moved mid-run. A naive
11461
+ # set-if-absent would leave a stale first-run baseline on every later run,
11462
+ # making the gate toothless. Non-git or zero-commit repos write an empty
11463
+ # file, which the gate treats as inconclusive (pass-through).
11464
+ local _start_sha_file=".loki/state/start-sha"
11465
+ mkdir -p ".loki/state"
11466
+ if [ "${ITERATION_COUNT:-0}" -eq 0 ] || [ ! -s "$_start_sha_file" ]; then
11467
+ (cd "${TARGET_DIR:-.}" && git rev-parse HEAD 2>/dev/null) > "$_start_sha_file" 2>/dev/null || true
11468
+ fi
11469
+ _LOKI_RUN_START_SHA="$(cat "$_start_sha_file" 2>/dev/null || echo "")"
11470
+ export _LOKI_RUN_START_SHA
11471
+
11415
11472
  # Notify dashboard of active project directory (for AI Chat cross-directory usage)
11416
11473
  if command -v curl &>/dev/null; then
11417
11474
  local project_cwd
@@ -12361,6 +12418,33 @@ if __name__ == "__main__":
12361
12418
  council_track_iteration "$log_file"
12362
12419
  fi
12363
12420
 
12421
+ # Uncertainty-gated escalation (v7.19.2, Slice B action).
12422
+ # The decision lives in completion-council.sh:uncertainty_should_escalate
12423
+ # (pure, debounced once-per-stuck-episode, knob-first on
12424
+ # LOKI_UNCERTAINTY_ESCALATION). This block only ACTS when the function
12425
+ # returns rc 0. The type guard keeps it a silent no-op if the decision
12426
+ # function is not present (byte-identical when the feature is absent/off).
12427
+ if type uncertainty_should_escalate &>/dev/null && uncertainty_should_escalate; then
12428
+ log_error "[Uncertainty] Escalating to human: >=2 of 3 stuck-signals co-occurred for N rounds (no-change / oscillation / council-split). PAUSE written; handoff saved."
12429
+ log_warn "[Uncertainty] To opt out of proactive escalation: set LOKI_UNCERTAINTY_ESCALATION=0"
12430
+ # Structured handoff doc before the bare PAUSE (mirrors GATE precedent).
12431
+ write_structured_handoff "uncertainty_escalation"
12432
+ notify_intervention_needed "Uncertainty escalation: >=2 of 3 stuck-signals co-occurred for N rounds"
12433
+ # Marker file for dashboard / external consumers. Empty touch has no
12434
+ # partial-write window, so atomic temp+mv is not required here.
12435
+ mkdir -p "${TARGET_DIR:-.}/.loki/signals"
12436
+ touch "${TARGET_DIR:-.}/.loki/signals/UNCERTAINTY_ESCALATION"
12437
+ # PAUSE is consumed by check_human_intervention: it halts in
12438
+ # non-perpetual mode; in perpetual mode it auto-clears + notifies.
12439
+ # That degrade is free; we add no consumer logic here.
12440
+ touch "${TARGET_DIR:-.}/.loki/PAUSE"
12441
+ # Perpetual-mode honesty: detect with the SAME vars the existing PAUSE
12442
+ # consumer uses (run.sh check_human_intervention), print-only.
12443
+ if [ "$AUTONOMY_MODE" = "perpetual" ] || [ "$PERPETUAL_MODE" = "true" ]; then
12444
+ log_warn "[Uncertainty] Perpetual mode: PAUSE will be auto-cleared; this is notify-only and will NOT halt the run."
12445
+ fi
12446
+ fi
12447
+
12364
12448
  # Check for success - ONLY stop on explicit completion promise
12365
12449
  # There's never a "complete" product - always improvements, bugs, features
12366
12450
  if [ $exit_code -eq 0 ]; then
@@ -12413,6 +12497,20 @@ if __name__ == "__main__":
12413
12497
  log_warn " Review details under .loki/quality/reviews/ ; gate_failures=${gate_failures}"
12414
12498
  _gate_block_for_completion=""
12415
12499
  # Fall through; the gate-failed loop continues normally
12500
+ # v7.19.1: the verified-completion evidence gate must also guard the
12501
+ # DEFAULT completion route (a completion claim via loki_complete_task
12502
+ # / the completion-promise text), not only the interval-gated council
12503
+ # path. Otherwise an agent can self-assert "done" with an empty diff
12504
+ # and red tests and exit as completion_promise_fulfilled, bypassing
12505
+ # the gate entirely -- exactly the fabrication this feature prevents.
12506
+ # Mirrors the code_review block above (B-17). Opt-out: the gate's own
12507
+ # LOKI_EVIDENCE_GATE=0 (council_evidence_gate returns 0 immediately
12508
+ # when disabled, so this branch never fires). Gate output (reason +
12509
+ # opt-out hint) is printed by council_evidence_gate itself.
12510
+ elif check_completion_promise "$iter_output" && type council_evidence_gate &>/dev/null && ! council_evidence_gate; then
12511
+ log_warn "Completion claim rejected: evidence gate found no proof of completion (empty diff vs run-start SHA, or red tests)."
12512
+ log_warn " Details under .loki/council/evidence-block.json ; opt out with LOKI_EVIDENCE_GATE=0"
12513
+ # Fall through; keep iterating until there is real evidence.
12416
12514
  elif check_completion_promise "$iter_output"; then
12417
12515
  echo ""
12418
12516
  if [ -n "$COMPLETION_PROMISE" ]; then
@@ -12765,6 +12863,8 @@ check_human_intervention() {
12765
12863
  rm -f "$loki_dir/signals/COUNCIL_REVIEW_REQUESTED"
12766
12864
  if type council_checklist_gate &>/dev/null && ! council_checklist_gate; then
12767
12865
  log_info "Council force-review: blocked by checklist hard gate"
12866
+ elif type council_evidence_gate &>/dev/null && ! council_evidence_gate; then
12867
+ log_info "Council force-review: blocked by evidence hard gate"
12768
12868
  elif type council_vote &>/dev/null && council_vote; then
12769
12869
  log_header "COMPLETION COUNCIL: FORCE REVIEW - PROJECT COMPLETE"
12770
12870
  # 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.2"
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
  # =============================================================================