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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +483 -0
- package/autonomy/config.example.yaml +26 -0
- package/autonomy/run.sh +103 -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/UNCERTAINTY-ESCALATION-PLAN.md +396 -0
- 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 +115 -0
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
|
|
9837
|
-
#
|
|
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
|
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
|
# =============================================================================
|