loki-mode 7.50.0 → 7.52.0
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 +2 -2
- package/autonomy/grill.sh +1 -1
- package/autonomy/lib/claude-flags.sh +15 -11
- package/autonomy/lib/wiki_llm.py +1 -1
- package/autonomy/loki +7 -7
- package/autonomy/run.sh +232 -9
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +2 -2
- package/loki-ts/dist/loki.js +2 -2
- package/magic/core/debate.py +4 -2
- package/magic/core/generator.py +1 -1
- package/mcp/__init__.py +1 -1
- package/mcp/lsp_proxy.py +203 -0
- package/mcp/tests/test_lsp_proxy.py +169 -0
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/codex.sh +16 -11
- package/references/multi-provider.md +1 -1
- package/skills/model-selection.md +7 -4
- package/skills/providers.md +2 -2
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 8 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v7.
|
|
6
|
+
# Loki Mode v7.52.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -407,4 +407,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
407
407
|
|
|
408
408
|
---
|
|
409
409
|
|
|
410
|
-
**v7.
|
|
410
|
+
**v7.52.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.
|
|
1
|
+
7.52.0
|
|
@@ -2038,7 +2038,7 @@ ISSUES: CRITICAL:description (optional, one per line per issue)"
|
|
|
2038
2038
|
;;
|
|
2039
2039
|
codex)
|
|
2040
2040
|
if command -v codex &>/dev/null; then
|
|
2041
|
-
verdict=$(codex exec --
|
|
2041
|
+
verdict=$(codex exec --sandbox workspace-write "$prompt" 2>/dev/null)
|
|
2042
2042
|
fi
|
|
2043
2043
|
;;
|
|
2044
2044
|
gemini)
|
|
@@ -2139,7 +2139,7 @@ REASON: your reasoning"
|
|
|
2139
2139
|
;;
|
|
2140
2140
|
codex)
|
|
2141
2141
|
if command -v codex &>/dev/null; then
|
|
2142
|
-
verdict=$(codex exec --
|
|
2142
|
+
verdict=$(codex exec --sandbox workspace-write "$prompt" 2>/dev/null)
|
|
2143
2143
|
fi
|
|
2144
2144
|
;;
|
|
2145
2145
|
gemini)
|
package/autonomy/grill.sh
CHANGED
|
@@ -227,7 +227,7 @@ grill_invoke_provider() {
|
|
|
227
227
|
return $GRILL_EXIT_ERROR
|
|
228
228
|
fi
|
|
229
229
|
local out
|
|
230
|
-
out="$(printf '%s' "$prompt" | _grill_with_timeout "${LOKI_GRILL_TIMEOUT:-180}" codex exec --
|
|
230
|
+
out="$(printf '%s' "$prompt" | _grill_with_timeout "${LOKI_GRILL_TIMEOUT:-180}" codex exec --sandbox workspace-write - 2>/dev/null)"
|
|
231
231
|
if [ -z "$out" ]; then
|
|
232
232
|
_grill_err "provider returned no output"
|
|
233
233
|
return $GRILL_EXIT_ERROR
|
|
@@ -63,31 +63,35 @@ loki_remaining_budget() {
|
|
|
63
63
|
local budget_file="${TARGET_DIR:-.}/.loki/metrics/budget.json"
|
|
64
64
|
local spend="0"
|
|
65
65
|
if [ -f "$budget_file" ]; then
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
# Pass the path via env var (os.environ), NOT string interpolation, so a
|
|
67
|
+
# path containing a single quote (or other python/shell-breaking char)
|
|
68
|
+
# cannot break the parse. Single-quoted program -> bash interpolates nothing.
|
|
69
|
+
spend=$(_LOKI_BUDGET_FILE="$budget_file" python3 -c '
|
|
70
|
+
import json, os, sys
|
|
68
71
|
try:
|
|
69
|
-
with open(
|
|
72
|
+
with open(os.environ["_LOKI_BUDGET_FILE"]) as f:
|
|
70
73
|
d = json.load(f)
|
|
71
|
-
v = d.get(
|
|
74
|
+
v = d.get("current_spend", 0)
|
|
72
75
|
print(float(v))
|
|
73
76
|
except Exception:
|
|
74
77
|
print(0)
|
|
75
|
-
|
|
78
|
+
' 2>/dev/null)
|
|
76
79
|
fi
|
|
77
80
|
# Compute remaining via python3 (bash floats are unreliable across awk/bc variations).
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
# Pass limit/spend via env vars too (same hardening; single-quoted program).
|
|
82
|
+
_LOKI_BUDGET_LIMIT="$limit" _LOKI_BUDGET_SPEND="$spend" python3 -c '
|
|
83
|
+
import os, sys
|
|
80
84
|
try:
|
|
81
|
-
limit = float(
|
|
82
|
-
spend = float(
|
|
85
|
+
limit = float(os.environ["_LOKI_BUDGET_LIMIT"])
|
|
86
|
+
spend = float(os.environ["_LOKI_BUDGET_SPEND"])
|
|
83
87
|
rem = limit - spend
|
|
84
88
|
# Strictly positive; otherwise emit nothing (caller decides whether to bail or warn).
|
|
85
89
|
if rem > 0:
|
|
86
90
|
# Round to 2 decimal places for the CLI.
|
|
87
|
-
print(f
|
|
91
|
+
print(f"{rem:.2f}")
|
|
88
92
|
except Exception:
|
|
89
93
|
pass
|
|
90
|
-
|
|
94
|
+
' 2>/dev/null
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
# ---------- Fallback model ----------
|
package/autonomy/lib/wiki_llm.py
CHANGED
|
@@ -57,7 +57,7 @@ def invoke_llm(prompt, timeout=120):
|
|
|
57
57
|
|
|
58
58
|
cmds = {
|
|
59
59
|
"claude": ["claude", "-p", prompt],
|
|
60
|
-
"codex": ["codex", "exec", "--
|
|
60
|
+
"codex": ["codex", "exec", "--sandbox", "workspace-write", prompt],
|
|
61
61
|
"cline": ["cline", "-y", prompt],
|
|
62
62
|
"aider": ["aider", "--message", prompt, "--yes-always", "--no-auto-commits"],
|
|
63
63
|
}
|
package/autonomy/loki
CHANGED
|
@@ -3785,7 +3785,7 @@ cmd_provider_info() {
|
|
|
3785
3785
|
echo "Name: Codex CLI"
|
|
3786
3786
|
echo "Vendor: OpenAI"
|
|
3787
3787
|
echo "CLI: codex"
|
|
3788
|
-
echo "Flag: --
|
|
3788
|
+
echo "Flag: --sandbox workspace-write"
|
|
3789
3789
|
echo ""
|
|
3790
3790
|
echo "Features:"
|
|
3791
3791
|
echo " - Autonomous mode"
|
|
@@ -11641,7 +11641,7 @@ except Exception: pass
|
|
|
11641
11641
|
done; } && phase_exit=0 || phase_exit=$?
|
|
11642
11642
|
;;
|
|
11643
11643
|
codex)
|
|
11644
|
-
(cd "$codebase_path" && codex exec --
|
|
11644
|
+
(cd "$codebase_path" && codex exec --sandbox workspace-write "$phase_prompt" 2>&1) || phase_exit=$?
|
|
11645
11645
|
;;
|
|
11646
11646
|
cline)
|
|
11647
11647
|
(cd "$codebase_path" && cline -y "$phase_prompt" 2>&1) || phase_exit=$?
|
|
@@ -11814,7 +11814,7 @@ except Exception: pass
|
|
|
11814
11814
|
done; } && doc_exit=0 || doc_exit=$?
|
|
11815
11815
|
;;
|
|
11816
11816
|
codex)
|
|
11817
|
-
(cd "$codebase_path" && codex exec --
|
|
11817
|
+
(cd "$codebase_path" && codex exec --sandbox workspace-write "$doc_prompt" 2>&1) || doc_exit=$?
|
|
11818
11818
|
;;
|
|
11819
11819
|
cline)
|
|
11820
11820
|
(cd "$codebase_path" && cline -y "$doc_prompt" 2>&1) || doc_exit=$?
|
|
@@ -12445,7 +12445,7 @@ except Exception: pass
|
|
|
12445
12445
|
done && heal_exit=0 || heal_exit=$?
|
|
12446
12446
|
;;
|
|
12447
12447
|
codex)
|
|
12448
|
-
(cd "$codebase_path" && codex exec --
|
|
12448
|
+
(cd "$codebase_path" && codex exec --sandbox workspace-write "$heal_prompt" 2>&1) || heal_exit=$?
|
|
12449
12449
|
;;
|
|
12450
12450
|
cline)
|
|
12451
12451
|
(cd "$codebase_path" && cline -y "$heal_prompt" 2>&1) || heal_exit=$?
|
|
@@ -22069,7 +22069,7 @@ USER TASK: ${prompt}"
|
|
|
22069
22069
|
claude -p "$full_prompt" 2>&1 || agent_exit=$?
|
|
22070
22070
|
;;
|
|
22071
22071
|
codex)
|
|
22072
|
-
codex exec --
|
|
22072
|
+
codex exec --sandbox workspace-write "$full_prompt" 2>&1 || agent_exit=$?
|
|
22073
22073
|
;;
|
|
22074
22074
|
cline)
|
|
22075
22075
|
cline -y "$full_prompt" 2>&1 || agent_exit=$?
|
|
@@ -22200,7 +22200,7 @@ $diff"
|
|
|
22200
22200
|
|
|
22201
22201
|
case "$provider" in
|
|
22202
22202
|
claude) claude -p "$review_prompt" 2>&1 ;;
|
|
22203
|
-
codex) codex exec --
|
|
22203
|
+
codex) codex exec --sandbox workspace-write "$review_prompt" 2>&1 ;;
|
|
22204
22204
|
cline) cline -y "$review_prompt" 2>&1 ;;
|
|
22205
22205
|
*) echo -e "${RED}Unknown provider: $provider${NC}"; return 1 ;;
|
|
22206
22206
|
esac
|
|
@@ -23870,7 +23870,7 @@ _docs_invoke_provider() {
|
|
|
23870
23870
|
result=$($t_prefix env CAVEMAN_DEFAULT_MODE=off claude -p "$prompt" 2>/dev/null) || exit_code=$?
|
|
23871
23871
|
;;
|
|
23872
23872
|
codex)
|
|
23873
|
-
result=$($t_prefix codex exec --
|
|
23873
|
+
result=$($t_prefix codex exec --sandbox workspace-write "$prompt" 2>/dev/null) || exit_code=$?
|
|
23874
23874
|
;;
|
|
23875
23875
|
cline)
|
|
23876
23876
|
result=$($t_prefix cline -y "$prompt" 2>/dev/null) || exit_code=$?
|
package/autonomy/run.sh
CHANGED
|
@@ -1480,7 +1480,40 @@ check_policy() {
|
|
|
1480
1480
|
elif [ $exit_code -eq 2 ]; then
|
|
1481
1481
|
log_warn "Policy requires APPROVAL: $result"
|
|
1482
1482
|
audit_agent_action "policy_approval_required" "Policy requires approval" "enforcement=$enforcement_point"
|
|
1483
|
-
#
|
|
1483
|
+
# P3-3 (v7.51.0): honor the approval requirement when the operator has
|
|
1484
|
+
# opted into enforcement. This is OPT-IN and changes NOTHING for existing
|
|
1485
|
+
# users: the wait fires only when staged autonomy is on
|
|
1486
|
+
# (LOKI_STAGED_AUTONOMY=true) or the explicit
|
|
1487
|
+
# LOKI_POLICY_APPROVAL_ENFORCE=1 knob is set. Otherwise it stays advisory
|
|
1488
|
+
# (log + proceed), preserving the historical default behavior. The wait
|
|
1489
|
+
# reuses the same .loki/signals/ file-signal mechanism as staged-autonomy
|
|
1490
|
+
# plan approval (check_staged_autonomy), extended with a reject arm so an
|
|
1491
|
+
# operator can deny (deny == policy DENIED == return 1).
|
|
1492
|
+
if [ "$STAGED_AUTONOMY" = "true" ] || [ "${LOKI_POLICY_APPROVAL_ENFORCE:-0}" = "1" ]; then
|
|
1493
|
+
local _approve_sig=".loki/signals/POLICY_APPROVED"
|
|
1494
|
+
local _reject_sig=".loki/signals/POLICY_REJECTED"
|
|
1495
|
+
log_warn "Policy enforcement: waiting for approval at enforcement point '$enforcement_point'."
|
|
1496
|
+
log_warn " Approve: create $_approve_sig | Reject: create $_reject_sig"
|
|
1497
|
+
audit_agent_action "policy_approval_wait" "Waiting for policy approval signal" "enforcement=$enforcement_point"
|
|
1498
|
+
while [ ! -f "$_approve_sig" ] && [ ! -f "$_reject_sig" ]; do
|
|
1499
|
+
sleep 5
|
|
1500
|
+
done
|
|
1501
|
+
if [ -f "$_reject_sig" ]; then
|
|
1502
|
+
rm -f "$_reject_sig" "$_approve_sig" 2>/dev/null || true
|
|
1503
|
+
log_error "Policy REJECTED by operator at enforcement point '$enforcement_point'"
|
|
1504
|
+
audit_agent_action "policy_approval_rejected" "Operator rejected policy approval" "enforcement=$enforcement_point"
|
|
1505
|
+
emit_event_json "policy_denied" \
|
|
1506
|
+
"enforcement=$enforcement_point" \
|
|
1507
|
+
"result=operator_rejected"
|
|
1508
|
+
return 1
|
|
1509
|
+
fi
|
|
1510
|
+
rm -f "$_approve_sig" 2>/dev/null || true
|
|
1511
|
+
log_info "Policy approved by operator at enforcement point '$enforcement_point'; continuing."
|
|
1512
|
+
audit_agent_action "policy_approval_granted" "Operator approved policy" "enforcement=$enforcement_point"
|
|
1513
|
+
return 0
|
|
1514
|
+
fi
|
|
1515
|
+
# Default (no staged autonomy, no enforce knob): advisory only -- log and
|
|
1516
|
+
# proceed. This preserves the historical behavior for existing users.
|
|
1484
1517
|
return 0
|
|
1485
1518
|
fi
|
|
1486
1519
|
return 0
|
|
@@ -3231,7 +3264,7 @@ spawn_worktree_session() {
|
|
|
3231
3264
|
fi
|
|
3232
3265
|
;;
|
|
3233
3266
|
codex)
|
|
3234
|
-
codex exec --
|
|
3267
|
+
codex exec --sandbox workspace-write --skip-git-repo-check \
|
|
3235
3268
|
"Loki Mode: $task_prompt. Read .loki/CONTINUITY.md for context." \
|
|
3236
3269
|
>> "$log_file" 2>&1 || _wt_exit=$?
|
|
3237
3270
|
;;
|
|
@@ -3447,7 +3480,7 @@ Output ONLY the resolved file content with no conflict markers. No explanations.
|
|
|
3447
3480
|
resolution=$(CAVEMAN_DEFAULT_MODE=off claude "${_cr_argv[@]}" -p "$conflict_prompt" --output-format text 2>/dev/null)
|
|
3448
3481
|
;;
|
|
3449
3482
|
codex)
|
|
3450
|
-
resolution=$(codex exec --
|
|
3483
|
+
resolution=$(codex exec --sandbox workspace-write --skip-git-repo-check "$conflict_prompt" 2>/dev/null)
|
|
3451
3484
|
;;
|
|
3452
3485
|
cline)
|
|
3453
3486
|
resolution=$(invoke_cline_capture "$conflict_prompt" 2>/dev/null)
|
|
@@ -6166,7 +6199,7 @@ check_command_allowed() {
|
|
|
6166
6199
|
# run.sh does not directly execute arbitrary shell commands from user or agent
|
|
6167
6200
|
# input. Command execution is handled by the AI CLI's own permission model:
|
|
6168
6201
|
# - Claude Code: --dangerously-skip-permissions (with its own allowlist)
|
|
6169
|
-
# - Codex CLI: --
|
|
6202
|
+
# - Codex CLI: exec --sandbox workspace-write or exec --dangerously-bypass-approvals-and-sandbox
|
|
6170
6203
|
#
|
|
6171
6204
|
# HUMAN_INPUT.md content is injected as a text prompt to the AI agent (not
|
|
6172
6205
|
# executed as a shell command), and is already guarded by:
|
|
@@ -7757,6 +7790,55 @@ os.replace(tmp, out)
|
|
|
7757
7790
|
else
|
|
7758
7791
|
log_info "Coverage: not measured (${COVERAGE_REASON:-unknown}); pass-through, not blocking"
|
|
7759
7792
|
fi
|
|
7793
|
+
else
|
|
7794
|
+
# P3-5/coverage-honesty (v7.51.0): measurement is OPT-IN (it re-runs the
|
|
7795
|
+
# suite instrumented, which would double every test run -- a UX
|
|
7796
|
+
# regression for an autonomous loop). At default-off we deliberately do
|
|
7797
|
+
# NOT measure, but we STILL write a coverage fact so the run manifest /
|
|
7798
|
+
# reproducibility record always has one honest coverage shape. This is
|
|
7799
|
+
# the "missing-artifact" fix, not a hollow gate: measured=false, pct=null,
|
|
7800
|
+
# blocked=false, with an explicit reason. ZERO runtime (no instrumented
|
|
7801
|
+
# re-run). Reuses the EXACT python3 writer + schema used at default-on so
|
|
7802
|
+
# consumers see a single shape. Single-pass, never blocks.
|
|
7803
|
+
_LOKI_COV_MEASURED="false" \
|
|
7804
|
+
_LOKI_COV_PCT="" \
|
|
7805
|
+
_LOKI_COV_TOOL="none" \
|
|
7806
|
+
_LOKI_COV_REASON="not requested (set LOKI_COVERAGE_GATE=1 to measure)" \
|
|
7807
|
+
_LOKI_COV_MIN="$min_coverage" \
|
|
7808
|
+
_LOKI_COV_ENFORCED="0" \
|
|
7809
|
+
_LOKI_COV_BLOCKED="false" \
|
|
7810
|
+
_LOKI_COV_RUNNER="$test_runner" \
|
|
7811
|
+
_LOKI_COV_OUT="$quality_dir/coverage.json" \
|
|
7812
|
+
python3 -c "
|
|
7813
|
+
import json, os, tempfile
|
|
7814
|
+
out=os.environ['_LOKI_COV_OUT']
|
|
7815
|
+
measured = os.environ.get('_LOKI_COV_MEASURED','false') == 'true'
|
|
7816
|
+
pct_raw = os.environ.get('_LOKI_COV_PCT','')
|
|
7817
|
+
try:
|
|
7818
|
+
pct = float(pct_raw) if (measured and pct_raw != '') else None
|
|
7819
|
+
except ValueError:
|
|
7820
|
+
pct = None
|
|
7821
|
+
def b(v): return os.environ.get(v,'false') == 'true'
|
|
7822
|
+
def i(v):
|
|
7823
|
+
try: return int(float(os.environ.get(v,'0')))
|
|
7824
|
+
except (TypeError, ValueError): return 0
|
|
7825
|
+
rec = {
|
|
7826
|
+
'measured': measured,
|
|
7827
|
+
'pct': pct,
|
|
7828
|
+
'tool': os.environ.get('_LOKI_COV_TOOL','none'),
|
|
7829
|
+
'runner': os.environ.get('_LOKI_COV_RUNNER','none'),
|
|
7830
|
+
'threshold': i('_LOKI_COV_MIN'),
|
|
7831
|
+
'enforced': os.environ.get('_LOKI_COV_ENFORCED','0') == '1',
|
|
7832
|
+
'blocked': b('_LOKI_COV_BLOCKED'),
|
|
7833
|
+
'reason': os.environ.get('_LOKI_COV_REASON','') if not measured else '',
|
|
7834
|
+
'timestamp': __import__('datetime').datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ'),
|
|
7835
|
+
}
|
|
7836
|
+
d=os.path.dirname(out)
|
|
7837
|
+
fd, tmp=tempfile.mkstemp(dir=d, suffix='.json')
|
|
7838
|
+
with os.fdopen(fd,'w') as f:
|
|
7839
|
+
json.dump(rec, f, indent=2)
|
|
7840
|
+
os.replace(tmp, out)
|
|
7841
|
+
" 2>/dev/null || true
|
|
7760
7842
|
fi
|
|
7761
7843
|
|
|
7762
7844
|
if [ "$test_passed" = "true" ]; then
|
|
@@ -7854,6 +7936,65 @@ ensure_completion_test_evidence() {
|
|
|
7854
7936
|
return 0
|
|
7855
7937
|
}
|
|
7856
7938
|
|
|
7939
|
+
# P1-1 (v7.51.0): ADVISORY consumer for the evidence-gate detail record that
|
|
7940
|
+
# completion-council.sh:_write_evidence_details writes on EVERY evidence-gate run
|
|
7941
|
+
# (pass and block) to .loki/council/evidence-gate-details.json. Until now run.sh
|
|
7942
|
+
# had ZERO consumers of that file -- the audit record was durable but invisible
|
|
7943
|
+
# to the operator and to the next-iteration prompt. This surfaces a one-line
|
|
7944
|
+
# advisory summary (verdict + diff axis + tests axis). It NEVER blocks and NEVER
|
|
7945
|
+
# introduces a new gate (the evidence gate itself already blocks; this is purely
|
|
7946
|
+
# visibility). Absent or malformed file -> degrade silently (no error, no block).
|
|
7947
|
+
surface_evidence_gate_details() {
|
|
7948
|
+
local _det_file="${TARGET_DIR:-.}/.loki/council/evidence-gate-details.json"
|
|
7949
|
+
[ -f "$_det_file" ] || return 0
|
|
7950
|
+
local _summary
|
|
7951
|
+
_summary=$(_LOKI_EGD_FILE="$_det_file" python3 -c "
|
|
7952
|
+
import json, os, sys
|
|
7953
|
+
try:
|
|
7954
|
+
with open(os.environ['_LOKI_EGD_FILE']) as f:
|
|
7955
|
+
d = json.load(f)
|
|
7956
|
+
except Exception:
|
|
7957
|
+
sys.exit(0)
|
|
7958
|
+
if not isinstance(d, dict):
|
|
7959
|
+
sys.exit(0)
|
|
7960
|
+
verdict = d.get('verdict', 'unknown')
|
|
7961
|
+
diff = d.get('diff', {}) if isinstance(d.get('diff'), dict) else {}
|
|
7962
|
+
tests = d.get('tests', {}) if isinstance(d.get('tests'), dict) else {}
|
|
7963
|
+
diff_ok = diff.get('ok')
|
|
7964
|
+
tests_ok = tests.get('ok')
|
|
7965
|
+
runner = tests.get('runner', 'none')
|
|
7966
|
+
parts = ['verdict=%s' % verdict]
|
|
7967
|
+
parts.append('diff_ok=%s' % diff_ok)
|
|
7968
|
+
parts.append('tests_ok=%s (runner=%s)' % (tests_ok, runner))
|
|
7969
|
+
if diff.get('inconclusive'):
|
|
7970
|
+
parts.append('diff_inconclusive=%s' % (diff.get('inconclusive_reason') or 'yes'))
|
|
7971
|
+
if tests.get('inconclusive'):
|
|
7972
|
+
parts.append('tests_inconclusive=%s' % (tests.get('inconclusive_reason') or 'yes'))
|
|
7973
|
+
print(' '.join(str(p) for p in parts))
|
|
7974
|
+
" 2>/dev/null) || return 0
|
|
7975
|
+
[ -n "$_summary" ] || return 0
|
|
7976
|
+
if printf '%s' "$_summary" | grep -q "verdict=block"; then
|
|
7977
|
+
log_warn "[Council] Evidence-gate details: $_summary"
|
|
7978
|
+
else
|
|
7979
|
+
log_info "[Council] Evidence-gate details: $_summary"
|
|
7980
|
+
fi
|
|
7981
|
+
return 0
|
|
7982
|
+
}
|
|
7983
|
+
|
|
7984
|
+
# P1-1 (v7.51.0): wrapper that runs the evidence gate, then surfaces its detail
|
|
7985
|
+
# record on BOTH the pass and block paths, and returns the gate's exact rc. This
|
|
7986
|
+
# preserves the elif chain's `! council_evidence_gate` semantics byte-for-byte
|
|
7987
|
+
# (fall-through on pass so the held-out and assumption gates downstream still
|
|
7988
|
+
# evaluate; block on a 1). The surface call is advisory-only and never affects
|
|
7989
|
+
# the returned rc. The detail file is fresh here -- _write_evidence_details ran
|
|
7990
|
+
# inside council_evidence_gate just above on this same iteration.
|
|
7991
|
+
_evidence_gate_and_surface() {
|
|
7992
|
+
local _rc=0
|
|
7993
|
+
council_evidence_gate || _rc=$?
|
|
7994
|
+
surface_evidence_gate_details || true
|
|
7995
|
+
return $_rc
|
|
7996
|
+
}
|
|
7997
|
+
|
|
7857
7998
|
# ============================================================================
|
|
7858
7999
|
# Documentation Staleness Check (v6.75.0)
|
|
7859
8000
|
# Checks if generated documentation is stale relative to HEAD
|
|
@@ -8496,7 +8637,7 @@ _dispatch_reviewer() {
|
|
|
8496
8637
|
--output-format text > "$review_output" 2>/dev/null
|
|
8497
8638
|
;;
|
|
8498
8639
|
codex)
|
|
8499
|
-
codex exec --
|
|
8640
|
+
codex exec --sandbox workspace-write --skip-git-repo-check "$prompt_text" \
|
|
8500
8641
|
> "$review_output" 2>/dev/null
|
|
8501
8642
|
;;
|
|
8502
8643
|
cline)
|
|
@@ -9220,7 +9361,7 @@ ADVERSARIAL_EOF
|
|
|
9220
9361
|
;;
|
|
9221
9362
|
codex)
|
|
9222
9363
|
if command -v codex &>/dev/null; then
|
|
9223
|
-
codex exec --
|
|
9364
|
+
codex exec --sandbox workspace-write --skip-git-repo-check "$adversarial_prompt" \
|
|
9224
9365
|
> "$result_file" 2>/dev/null || true
|
|
9225
9366
|
fi
|
|
9226
9367
|
;;
|
|
@@ -14576,7 +14717,7 @@ if __name__ == "__main__":
|
|
|
14576
14717
|
# Uses dynamic tier from RARV phase (tier_param already set above)
|
|
14577
14718
|
{ LOKI_CODEX_REASONING_EFFORT="$tier_param" \
|
|
14578
14719
|
CODEX_MODEL_REASONING_EFFORT="$tier_param" \
|
|
14579
|
-
codex exec --
|
|
14720
|
+
codex exec --sandbox workspace-write --skip-git-repo-check \
|
|
14580
14721
|
"$prompt" 2>&1 | tee -a "$log_file" "$agent_log" "$iter_output"; \
|
|
14581
14722
|
} && exit_code=0 || exit_code=$?
|
|
14582
14723
|
;;
|
|
@@ -14818,6 +14959,88 @@ if __name__ == "__main__":
|
|
|
14818
14959
|
log_warn "Mutation integrity gate FAILED ($mt_count consecutive) - HIGH test-fitting detected"
|
|
14819
14960
|
fi
|
|
14820
14961
|
fi
|
|
14962
|
+
# LSP diagnostics gate (P1-5 bash-route parity, v7.51.0). Closes the
|
|
14963
|
+
# parity gap: the Bun route ships runLSPDiagnostics
|
|
14964
|
+
# (loki-ts/src/runner/quality_gates.ts) with a route-neutral Python
|
|
14965
|
+
# writer (mcp/lsp_proxy.py); the bash route had NO writer/reader.
|
|
14966
|
+
# This block runs the SAME writer and mirrors the TS blocking
|
|
14967
|
+
# semantics byte-for-byte:
|
|
14968
|
+
# - Gate is OPT-IN: default OFF. Enabled by LOKI_GATE_LSP_DIAGNOSTICS=true
|
|
14969
|
+
# (the single toggle; mirrors flag("LOKI_GATE_LSP_DIAGNOSTICS", false)
|
|
14970
|
+
# at quality_gates.ts:1717). No second knob.
|
|
14971
|
+
# - When enabled, count_errors > 0 -> BLOCK (mirrors
|
|
14972
|
+
# "if (errorCount > 0) { passed: false }" at quality_gates.ts:1667).
|
|
14973
|
+
# - warnings only -> advisory PASS (quality_gates.ts:1673).
|
|
14974
|
+
# - artifact absent/malformed -> honest pass-through, NEVER block
|
|
14975
|
+
# (quality_gates.ts:1646 returns passed:true on null artifact).
|
|
14976
|
+
# The writer is OPT-OUT-able with LOKI_GATE_LSP_WRITER=0 (operator can
|
|
14977
|
+
# supply a pre-built artifact), matching the TS escape hatch
|
|
14978
|
+
# (quality_gates.ts:1630). cwd must be the install dir (PROJECT_DIR =
|
|
14979
|
+
# $SCRIPT_DIR/.. ) so `-m mcp.lsp_proxy` imports, while --root points
|
|
14980
|
+
# at the TARGET project the loop is building (mirrors
|
|
14981
|
+
# runLSPDiagnosticsWriter: cwd=REPO_ROOT, --root=ctx.cwd).
|
|
14982
|
+
if [ "${LOKI_GATE_LSP_DIAGNOSTICS:-false}" = "true" ] && [ "$ITERATION_COUNT" -gt 0 ]; then
|
|
14983
|
+
log_info "Quality gate: LSP diagnostics..."
|
|
14984
|
+
# WRITER: route-neutral Python, same program as the Bun route.
|
|
14985
|
+
if [ "${LOKI_GATE_LSP_WRITER:-1}" != "0" ]; then
|
|
14986
|
+
( cd "$PROJECT_DIR" && LOKI_DIR="${TARGET_DIR:-.}/.loki" python3 -m mcp.lsp_proxy --write-diagnostics --root "${TARGET_DIR:-.}" ) >/dev/null 2>&1 || true
|
|
14987
|
+
fi
|
|
14988
|
+
# READER: read counts, mirror TS block policy.
|
|
14989
|
+
local _lsp_file="${TARGET_DIR:-.}/.loki/quality/lsp-diagnostics.json"
|
|
14990
|
+
local _lsp_verdict="absent"
|
|
14991
|
+
if [ -f "$_lsp_file" ]; then
|
|
14992
|
+
_lsp_verdict=$(_LOKI_LSP_FILE="$_lsp_file" python3 -c "
|
|
14993
|
+
import json, os, sys
|
|
14994
|
+
try:
|
|
14995
|
+
with open(os.environ['_LOKI_LSP_FILE']) as f:
|
|
14996
|
+
d = json.load(f)
|
|
14997
|
+
except Exception:
|
|
14998
|
+
print('absent'); sys.exit(0)
|
|
14999
|
+
if not isinstance(d, dict):
|
|
15000
|
+
print('absent'); sys.exit(0)
|
|
15001
|
+
diags = d.get('diagnostics') if isinstance(d.get('diagnostics'), list) else []
|
|
15002
|
+
ce = d.get('count_errors')
|
|
15003
|
+
cw = d.get('count_warnings')
|
|
15004
|
+
errors = ce if isinstance(ce, int) else sum(1 for x in diags if isinstance(x, dict) and x.get('severity') == 1)
|
|
15005
|
+
warns = cw if isinstance(cw, int) else sum(1 for x in diags if isinstance(x, dict) and x.get('severity') == 2)
|
|
15006
|
+
if errors > 0:
|
|
15007
|
+
print('block %d %d' % (errors, warns))
|
|
15008
|
+
elif warns > 0:
|
|
15009
|
+
print('warn %d %d' % (errors, warns))
|
|
15010
|
+
else:
|
|
15011
|
+
print('clean 0 0')
|
|
15012
|
+
" 2>/dev/null) || _lsp_verdict="absent"
|
|
15013
|
+
[ -n "$_lsp_verdict" ] || _lsp_verdict="absent"
|
|
15014
|
+
fi
|
|
15015
|
+
case "$_lsp_verdict" in
|
|
15016
|
+
block*)
|
|
15017
|
+
local _lsp_e _lsp_w
|
|
15018
|
+
_lsp_e=$(printf '%s' "$_lsp_verdict" | awk '{print $2}')
|
|
15019
|
+
_lsp_w=$(printf '%s' "$_lsp_verdict" | awk '{print $3}')
|
|
15020
|
+
local lsp_count
|
|
15021
|
+
lsp_count=$(track_gate_failure "lsp_diagnostics")
|
|
15022
|
+
gate_failures="${gate_failures}lsp_diagnostics,"
|
|
15023
|
+
log_warn "LSP diagnostics gate FAILED ($lsp_count consecutive) - ${_lsp_e} error(s), ${_lsp_w} warning(s); LSP reports compiler/type errors"
|
|
15024
|
+
;;
|
|
15025
|
+
warn*)
|
|
15026
|
+
local _lsp_w2
|
|
15027
|
+
_lsp_w2=$(printf '%s' "$_lsp_verdict" | awk '{print $3}')
|
|
15028
|
+
clear_gate_failure "lsp_diagnostics"
|
|
15029
|
+
log_info "LSP diagnostics: 0 errors, ${_lsp_w2} warning(s) (advisory)"
|
|
15030
|
+
;;
|
|
15031
|
+
clean*)
|
|
15032
|
+
clear_gate_failure "lsp_diagnostics"
|
|
15033
|
+
log_info "LSP diagnostics: 0 errors, 0 warnings"
|
|
15034
|
+
;;
|
|
15035
|
+
*)
|
|
15036
|
+
# Absent or malformed artifact: honest pass-through, never
|
|
15037
|
+
# block (mirrors quality_gates.ts:1646). Do not fabricate
|
|
15038
|
+
# a clean verdict from absence.
|
|
15039
|
+
clear_gate_failure "lsp_diagnostics"
|
|
15040
|
+
log_info "LSP diagnostics: no lsp-diagnostics.json artifact (lsp not available) -- gate did not run"
|
|
15041
|
+
;;
|
|
15042
|
+
esac
|
|
15043
|
+
fi
|
|
14821
15044
|
# Code review gate (upgraded from advisory, with escalation)
|
|
14822
15045
|
if [ "$PHASE_CODE_REVIEW" = "true" ] && [ "$ITERATION_COUNT" -gt 0 ]; then
|
|
14823
15046
|
log_info "Quality gate: code review..."
|
|
@@ -15096,7 +15319,7 @@ if __name__ == "__main__":
|
|
|
15096
15319
|
# LOKI_EVIDENCE_GATE=0 (council_evidence_gate returns 0 immediately
|
|
15097
15320
|
# when disabled, so this branch never fires). Gate output (reason +
|
|
15098
15321
|
# opt-out hint) is printed by council_evidence_gate itself.
|
|
15099
|
-
elif [ "$_completion_claimed" = 1 ] && type council_evidence_gate &>/dev/null && !
|
|
15322
|
+
elif [ "$_completion_claimed" = 1 ] && type council_evidence_gate &>/dev/null && ! _evidence_gate_and_surface; then
|
|
15100
15323
|
log_warn "Completion claim rejected: evidence gate found no proof of completion (empty diff vs run-start SHA, or red tests)."
|
|
15101
15324
|
log_warn " Details under .loki/council/evidence-block.json ; opt out with LOKI_EVIDENCE_GATE=0"
|
|
15102
15325
|
# Fall through; keep iterating until there is real evidence.
|
|
@@ -15595,7 +15818,7 @@ check_human_intervention() {
|
|
|
15595
15818
|
fi
|
|
15596
15819
|
if type council_checklist_gate &>/dev/null && ! council_checklist_gate; then
|
|
15597
15820
|
log_info "Council force-review: blocked by checklist hard gate"
|
|
15598
|
-
elif type council_evidence_gate &>/dev/null && !
|
|
15821
|
+
elif type council_evidence_gate &>/dev/null && ! _evidence_gate_and_surface; then
|
|
15599
15822
|
log_info "Council force-review: blocked by evidence hard gate"
|
|
15600
15823
|
elif type council_heldout_gate &>/dev/null && ! council_heldout_gate; then
|
|
15601
15824
|
log_info "Council force-review: blocked by held-out spec-eval hard gate"
|
package/dashboard/__init__.py
CHANGED
package/docs/INSTALLATION.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
|
|
4
4
|
|
|
5
|
-
**Version:** v7.
|
|
5
|
+
**Version:** v7.52.0
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -396,7 +396,7 @@ provider works inside the container. Provide auth with your Anthropic API key:
|
|
|
396
396
|
# Run Loki Mode in Docker (Claude provider, API-key auth)
|
|
397
397
|
docker run --rm -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
|
|
398
398
|
-v $(pwd):/workspace -w /workspace \
|
|
399
|
-
asklokesh/loki-mode:7.
|
|
399
|
+
asklokesh/loki-mode:7.52.0 start ./my-spec.md
|
|
400
400
|
```
|
|
401
401
|
|
|
402
402
|
##### docker compose + .env (no host install)
|
package/loki-ts/dist/loki.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var r6=Object.defineProperty;var t6=($)=>$;function i6($,Q){this[$]=t6.bind(null,Q)}var h=($,Q)=>{for(var Z in Q)r6($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:i6.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var K$=import.meta.require;var D1={};h(D1,{lokiDir:()=>P,homeLokiDir:()=>n$,findRepoRootForVersion:()=>o$,REPO_ROOT:()=>g});import{resolve as n,dirname as d$}from"path";import{fileURLToPath as e6}from"url";import{existsSync as P$}from"fs";import{homedir as $Q}from"os";function QQ(){let $=S1;for(let Q=0;Q<6;Q++){if(P$(n($,"VERSION"))&&P$(n($,"autonomy/run.sh")))return $;let Z=d$($);if(Z===$)break;$=Z}return n(S1,"..","..","..")}function o$($){let Q=$;for(let Z=0;Z<6;Z++){if(P$(n(Q,"VERSION"))&&P$(n(Q,"autonomy/run.sh")))return Q;let z=d$(Q);if(z===Q)break;Q=z}return n($,"..","..","..")}function P(){return process.env.LOKI_DIR??n(process.cwd(),".loki")}function n$(){return n($Q(),".loki")}var S1,g;var b=L(()=>{S1=d$(e6(import.meta.url));g=QQ()});import{readFileSync as ZQ}from"fs";import{resolve as zQ,dirname as XQ}from"path";import{fileURLToPath as KQ}from"url";function j$(){if($$!==null)return $$;let $="7.
|
|
2
|
+
var r6=Object.defineProperty;var t6=($)=>$;function i6($,Q){this[$]=t6.bind(null,Q)}var h=($,Q)=>{for(var Z in Q)r6($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:i6.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var K$=import.meta.require;var D1={};h(D1,{lokiDir:()=>P,homeLokiDir:()=>n$,findRepoRootForVersion:()=>o$,REPO_ROOT:()=>g});import{resolve as n,dirname as d$}from"path";import{fileURLToPath as e6}from"url";import{existsSync as P$}from"fs";import{homedir as $Q}from"os";function QQ(){let $=S1;for(let Q=0;Q<6;Q++){if(P$(n($,"VERSION"))&&P$(n($,"autonomy/run.sh")))return $;let Z=d$($);if(Z===$)break;$=Z}return n(S1,"..","..","..")}function o$($){let Q=$;for(let Z=0;Z<6;Z++){if(P$(n(Q,"VERSION"))&&P$(n(Q,"autonomy/run.sh")))return Q;let z=d$(Q);if(z===Q)break;Q=z}return n($,"..","..","..")}function P(){return process.env.LOKI_DIR??n(process.cwd(),".loki")}function n$(){return n($Q(),".loki")}var S1,g;var b=L(()=>{S1=d$(e6(import.meta.url));g=QQ()});import{readFileSync as ZQ}from"fs";import{resolve as zQ,dirname as XQ}from"path";import{fileURLToPath as KQ}from"url";function j$(){if($$!==null)return $$;let $="7.52.0";if(typeof $==="string"&&$.length>0)return $$=$,$$;try{let Q=XQ(KQ(import.meta.url)),Z=o$(Q);$$=ZQ(zQ(Z,"VERSION"),"utf-8").trim()}catch{$$="unknown"}return $$}var $$=null;var a$=L(()=>{b()});var b1={};h(b1,{runOrThrow:()=>qQ,run:()=>k,commandVersion:()=>WQ,commandExists:()=>f,ShellError:()=>s$});async function k($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,X;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[q,K,W]=await Promise.all([new Response(Z.stdout).text(),new Response(Z.stderr).text(),Z.exited]);return{stdout:q,stderr:K,exitCode:W}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function qQ($,Q={}){let Z=await k($,Q);if(Z.exitCode!==0)throw new s$(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=VQ($),Z=await k(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function VQ($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function WQ($,Q="--version"){if(!await f($))return null;let z=await k([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var s$;var d=L(()=>{s$=class s$ extends Error{message;exitCode;stdout;stderr;constructor($,Q,Z,z){super($);this.message=$;this.exitCode=Q;this.stdout=Z;this.stderr=z;this.name="ShellError"}}});function a($){return JQ?"":$}var JQ,T,S,_,wZ,I,R,y,V;var c=L(()=>{JQ=(process.env.NO_COLOR??"").length>0;T=a("\x1B[0;31m"),S=a("\x1B[0;32m"),_=a("\x1B[1;33m"),wZ=a("\x1B[0;34m"),I=a("\x1B[0;36m"),R=a("\x1B[1m"),y=a("\x1B[2m"),V=a("\x1B[0m")});import{existsSync as wQ}from"fs";async function Q$(){if(G$!==void 0)return G$;let $="/opt/homebrew/bin/python3.12";if(wQ($))return G$=$,$;let Q=await f("python3.12");if(Q)return G$=Q,Q;let Z=await f("python3");return G$=Z,Z}async function Z$($,Q={}){let Z=await Q$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return k([Z,"-c",$],Q)}var G$;var q$=L(()=>{d()});var e1={};h(e1,{runStatus:()=>uQ});import{existsSync as v,readFileSync as W$,readdirSync as d1,statSync as o1}from"fs";import{resolve as C,basename as DQ}from"path";import{homedir as CQ}from"os";function n1($){let Q=Math.trunc($);if(Q>=1e6)return`${(Math.trunc(Q/1e6*10)/10).toFixed(1)}M`;if(Q>=1000)return`${(Math.trunc(Q/1000*10)/10).toFixed(1)}K`;return String(Q)}function a1($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*k$/Q);if(X>k$)X=k$;let q=k$-X,K=S;if(z>=80)K=T;else if(z>=50)K=_;let W="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),J=n1($),U=n1(Q);return` ${R}${Z}${V} ${K}[${W}]${V} ${z}% (${J} / ${U})`}async function hQ(){if(await f("jq"))return!0;return process.stdout.write(`${T}Error: jq is required but not installed.${V}
|
|
3
3
|
`),process.stdout.write(`Install with:
|
|
4
4
|
`),process.stdout.write(` brew install jq (macOS)
|
|
5
5
|
`),process.stdout.write(` apt install jq (Debian/Ubuntu)
|
|
@@ -790,4 +790,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
790
790
|
`),2}default:return process.stderr.write(`Unknown command: ${Q}
|
|
791
791
|
`),process.stderr.write(s6),2}}l1();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var KZ=await XZ(Bun.argv.slice(2));process.exit(KZ);
|
|
792
792
|
|
|
793
|
-
//# debugId=
|
|
793
|
+
//# debugId=D3609A2FE6BB9BAE64756E2164756E21
|
package/magic/core/debate.py
CHANGED
|
@@ -482,8 +482,10 @@ class DebateRunner:
|
|
|
482
482
|
if provider == "claude":
|
|
483
483
|
return ["claude", "--dangerously-skip-permissions", "-p", prompt]
|
|
484
484
|
if provider == "codex":
|
|
485
|
-
# Codex uses `exec --
|
|
486
|
-
|
|
485
|
+
# Codex uses `exec --sandbox workspace-write` with the prompt as
|
|
486
|
+
# positional (codex 0.132.0 deprecated --full-auto; workspace-write
|
|
487
|
+
# is the documented replacement, exec is non-interactive by default).
|
|
488
|
+
return ["codex", "exec", "--sandbox", "workspace-write", prompt]
|
|
487
489
|
if provider == "gemini":
|
|
488
490
|
return ["gemini", "--approval-mode=yolo", prompt]
|
|
489
491
|
if provider == "cline":
|
package/magic/core/generator.py
CHANGED
|
@@ -180,7 +180,7 @@ class ComponentGenerator:
|
|
|
180
180
|
if provider == "claude":
|
|
181
181
|
cmd = base_cmd + [binary, "-p", prompt]
|
|
182
182
|
elif provider == "codex":
|
|
183
|
-
cmd = base_cmd + [binary, "exec", "--
|
|
183
|
+
cmd = base_cmd + [binary, "exec", "--sandbox", "workspace-write", prompt]
|
|
184
184
|
elif provider == "gemini":
|
|
185
185
|
cmd = base_cmd + [binary, "--approval-mode=yolo", prompt]
|
|
186
186
|
elif provider == "cline":
|
package/mcp/__init__.py
CHANGED
package/mcp/lsp_proxy.py
CHANGED
|
@@ -1304,6 +1304,180 @@ async def lsp_find_definition_by_name(symbol: str,
|
|
|
1304
1304
|
# MAIN
|
|
1305
1305
|
# ============================================================
|
|
1306
1306
|
|
|
1307
|
+
# ============================================================
|
|
1308
|
+
# DIAGNOSTICS WRITER (P1-5 quality gate)
|
|
1309
|
+
# ============================================================
|
|
1310
|
+
#
|
|
1311
|
+
# The LSP-diagnostics quality gate (loki-ts/src/runner/quality_gates.ts,
|
|
1312
|
+
# runLSPDiagnostics) READS <lokiDir>/quality/lsp-diagnostics.json but nothing
|
|
1313
|
+
# wrote it -- the gate was inert. This is the WRITER, invoked the same way on
|
|
1314
|
+
# both routes (Bun gate calls `python3 -m mcp.lsp_proxy --write-diagnostics`;
|
|
1315
|
+
# the bash route, when wired by the run.sh owner, calls the identical command),
|
|
1316
|
+
# so a single program produces byte-identical output for both. NO TS/bash
|
|
1317
|
+
# re-implementation of the aggregation -- that is the whole point of putting it
|
|
1318
|
+
# here.
|
|
1319
|
+
#
|
|
1320
|
+
# It enumerates the changed files itself (HEAD~1 -> --cached -> ls-files,
|
|
1321
|
+
# mirroring runStaticAnalysis's chain in quality_gates.ts so file selection
|
|
1322
|
+
# cannot diverge from the sibling static-analysis gate), queries each
|
|
1323
|
+
# supported file via the SAME in-process LSP client cache (_get_or_spawn_client,
|
|
1324
|
+
# one process for all files -- not python3-per-file), aggregates, and writes
|
|
1325
|
+
# the minimal deterministic shape the gate consumes.
|
|
1326
|
+
#
|
|
1327
|
+
# HONESTY (never fabricate a clean verdict from absence): when NO supported
|
|
1328
|
+
# language server is on PATH, or NO changed file maps to an available server,
|
|
1329
|
+
# the writer writes NO artifact and removes any stale one. The gate's existing
|
|
1330
|
+
# absence path then fires ("gate did not run") instead of a manufactured
|
|
1331
|
+
# "0 errors, 0 warnings" clean verdict. Likewise on any unrecoverable error
|
|
1332
|
+
# the writer leaves no artifact.
|
|
1333
|
+
#
|
|
1334
|
+
# DETERMINISM: the artifact carries ONLY the fields the gate reads
|
|
1335
|
+
# (count_errors, count_warnings, diagnostics[].severity/.message/.file).
|
|
1336
|
+
# Non-deterministic proxy fields (elapsed_ms, ranges, source) are dropped, and
|
|
1337
|
+
# diagnostics are sorted stably (file, severity, message) so the same inputs
|
|
1338
|
+
# always serialize byte-identically across runs and routes.
|
|
1339
|
+
|
|
1340
|
+
_DIAG_ARTIFACT_REL = os.path.join('quality', 'lsp-diagnostics.json')
|
|
1341
|
+
|
|
1342
|
+
|
|
1343
|
+
def _writer_loki_dir() -> str:
|
|
1344
|
+
"""Resolve the .loki dir the artifact is written under. Honors LOKI_DIR
|
|
1345
|
+
(matching loki-ts/src/util/paths.ts lokiDir()), else <cwd>/.loki."""
|
|
1346
|
+
env = os.environ.get('LOKI_DIR')
|
|
1347
|
+
if env:
|
|
1348
|
+
return env
|
|
1349
|
+
return os.path.join(os.getcwd(), '.loki')
|
|
1350
|
+
|
|
1351
|
+
|
|
1352
|
+
def _writer_changed_files(root: str) -> List[str]:
|
|
1353
|
+
"""Return changed files relative to `root`, mirroring the HEAD~1 ->
|
|
1354
|
+
--cached -> ls-files chain in runStaticAnalysis (quality_gates.ts). An
|
|
1355
|
+
empty-but-successful `git diff HEAD~1 HEAD` is honored as "no changes
|
|
1356
|
+
this iteration" and does NOT fall through to ls-files (parity with the
|
|
1357
|
+
tryGit null-vs-empty distinction in the TS gate)."""
|
|
1358
|
+
def _try_git(git_args: List[str]) -> Optional[str]:
|
|
1359
|
+
try:
|
|
1360
|
+
proc = subprocess.run(
|
|
1361
|
+
['git', '-C', root, *git_args],
|
|
1362
|
+
capture_output=True, text=True, timeout=30,
|
|
1363
|
+
)
|
|
1364
|
+
except (OSError, subprocess.SubprocessError):
|
|
1365
|
+
return None
|
|
1366
|
+
if proc.returncode != 0:
|
|
1367
|
+
return None
|
|
1368
|
+
return proc.stdout
|
|
1369
|
+
|
|
1370
|
+
raw: str
|
|
1371
|
+
head_tilde = _try_git(['diff', '--name-only', 'HEAD~1', 'HEAD'])
|
|
1372
|
+
if head_tilde is not None:
|
|
1373
|
+
raw = head_tilde
|
|
1374
|
+
else:
|
|
1375
|
+
cached = _try_git(['diff', '--name-only', '--cached'])
|
|
1376
|
+
if cached is not None and cached.strip():
|
|
1377
|
+
raw = cached
|
|
1378
|
+
else:
|
|
1379
|
+
ls_files = _try_git(['ls-files'])
|
|
1380
|
+
raw = ls_files if (ls_files is not None and ls_files.strip()) else ''
|
|
1381
|
+
return [line.strip() for line in raw.splitlines() if line.strip()]
|
|
1382
|
+
|
|
1383
|
+
|
|
1384
|
+
def write_diagnostics_artifact(root: Optional[str] = None,
|
|
1385
|
+
loki_dir: Optional[str] = None) -> Dict[str, Any]:
|
|
1386
|
+
"""Enumerate changed files, query LSP diagnostics per supported file via
|
|
1387
|
+
the shared in-process client cache, aggregate, and atomically write the
|
|
1388
|
+
gate artifact. Returns a status dict (also printed by --write-diagnostics).
|
|
1389
|
+
|
|
1390
|
+
Writes NO artifact (and removes a stale one) when there is nothing real to
|
|
1391
|
+
measure -- no detected server, or no changed file maps to a detected
|
|
1392
|
+
server -- so the gate's absence path fires honestly instead of a fabricated
|
|
1393
|
+
clean verdict."""
|
|
1394
|
+
root = root or os.getcwd()
|
|
1395
|
+
loki_dir = loki_dir or _writer_loki_dir()
|
|
1396
|
+
artifact_path = os.path.join(loki_dir, _DIAG_ARTIFACT_REL)
|
|
1397
|
+
|
|
1398
|
+
def _remove_stale() -> None:
|
|
1399
|
+
try:
|
|
1400
|
+
if os.path.isfile(artifact_path):
|
|
1401
|
+
os.remove(artifact_path)
|
|
1402
|
+
except OSError:
|
|
1403
|
+
pass
|
|
1404
|
+
|
|
1405
|
+
detected = _detect_lsps()
|
|
1406
|
+
if not detected:
|
|
1407
|
+
_remove_stale()
|
|
1408
|
+
return {'measured': False, 'reason': 'no-language-server-on-path',
|
|
1409
|
+
'wrote_artifact': False}
|
|
1410
|
+
|
|
1411
|
+
changed_rel = _writer_changed_files(root)
|
|
1412
|
+
# Keep only files whose language has a detected server AND that exist on
|
|
1413
|
+
# disk (skip deleted/renamed diff entries).
|
|
1414
|
+
targets: List[str] = []
|
|
1415
|
+
for rel in changed_rel:
|
|
1416
|
+
abs_path = rel if os.path.isabs(rel) else os.path.join(root, rel)
|
|
1417
|
+
if not os.path.isfile(abs_path):
|
|
1418
|
+
continue
|
|
1419
|
+
lang = _suffix_to_language(abs_path)
|
|
1420
|
+
if lang is None or lang not in detected:
|
|
1421
|
+
continue
|
|
1422
|
+
targets.append(abs_path)
|
|
1423
|
+
|
|
1424
|
+
if not targets:
|
|
1425
|
+
_remove_stale()
|
|
1426
|
+
return {'measured': False, 'reason': 'no-changed-file-with-detected-server',
|
|
1427
|
+
'wrote_artifact': False,
|
|
1428
|
+
'detected': sorted(detected.keys())}
|
|
1429
|
+
|
|
1430
|
+
all_diags: List[Dict[str, Any]] = []
|
|
1431
|
+
queried = 0
|
|
1432
|
+
for abs_path in targets:
|
|
1433
|
+
try:
|
|
1434
|
+
raw = _lsp_get_diagnostics_blocking(abs_path)
|
|
1435
|
+
parsed = json.loads(raw)
|
|
1436
|
+
except (OSError, ValueError):
|
|
1437
|
+
continue
|
|
1438
|
+
if 'error' in parsed:
|
|
1439
|
+
# Server present in detection but failed for this file (spawn
|
|
1440
|
+
# failure, unsupported, etc.). Skip it -- do NOT count it clean.
|
|
1441
|
+
continue
|
|
1442
|
+
queried += 1
|
|
1443
|
+
for d in parsed.get('diagnostics', []) or []:
|
|
1444
|
+
sev = d.get('severity')
|
|
1445
|
+
if not isinstance(sev, int):
|
|
1446
|
+
continue
|
|
1447
|
+
all_diags.append({
|
|
1448
|
+
'file': abs_path,
|
|
1449
|
+
'severity': sev,
|
|
1450
|
+
'message': str(d.get('message', '')),
|
|
1451
|
+
})
|
|
1452
|
+
|
|
1453
|
+
if queried == 0:
|
|
1454
|
+
# Every detected-server file failed to produce a usable result. We
|
|
1455
|
+
# measured nothing real -- do not fabricate a clean artifact.
|
|
1456
|
+
_remove_stale()
|
|
1457
|
+
return {'measured': False, 'reason': 'no-file-yielded-diagnostics',
|
|
1458
|
+
'wrote_artifact': False, 'detected': sorted(detected.keys())}
|
|
1459
|
+
|
|
1460
|
+
# Stable sort so the same inputs serialize identically across runs/routes.
|
|
1461
|
+
all_diags.sort(key=lambda d: (d['file'], d['severity'], d['message']))
|
|
1462
|
+
count_errors = sum(1 for d in all_diags if d['severity'] == 1)
|
|
1463
|
+
count_warnings = sum(1 for d in all_diags if d['severity'] == 2)
|
|
1464
|
+
artifact = {
|
|
1465
|
+
'count_errors': count_errors,
|
|
1466
|
+
'count_warnings': count_warnings,
|
|
1467
|
+
'diagnostics': all_diags,
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
os.makedirs(os.path.dirname(artifact_path), exist_ok=True)
|
|
1471
|
+
body = json.dumps(artifact, indent=2, sort_keys=True) + '\n'
|
|
1472
|
+
tmp_path = f'{artifact_path}.tmp.{os.getpid()}'
|
|
1473
|
+
with open(tmp_path, 'w', encoding='utf-8') as fh:
|
|
1474
|
+
fh.write(body)
|
|
1475
|
+
os.replace(tmp_path, artifact_path)
|
|
1476
|
+
return {'measured': True, 'wrote_artifact': True, 'path': artifact_path,
|
|
1477
|
+
'files_queried': queried, 'count_errors': count_errors,
|
|
1478
|
+
'count_warnings': count_warnings}
|
|
1479
|
+
|
|
1480
|
+
|
|
1307
1481
|
def main() -> None:
|
|
1308
1482
|
import argparse
|
|
1309
1483
|
parser = argparse.ArgumentParser(
|
|
@@ -1317,7 +1491,36 @@ def main() -> None:
|
|
|
1317
1491
|
'--port', type=int, default=8422,
|
|
1318
1492
|
help='Port for HTTP transport (default: 8422).',
|
|
1319
1493
|
)
|
|
1494
|
+
parser.add_argument(
|
|
1495
|
+
'--write-diagnostics', action='store_true',
|
|
1496
|
+
help='One-shot: enumerate changed files, query LSP diagnostics for '
|
|
1497
|
+
'each supported file, and write the quality-gate artifact at '
|
|
1498
|
+
'<LOKI_DIR>/quality/lsp-diagnostics.json. Writes nothing when no '
|
|
1499
|
+
'language server is available (the gate then reports "did not '
|
|
1500
|
+
'run"). Used by the LSP-diagnostics quality gate on both routes.',
|
|
1501
|
+
)
|
|
1502
|
+
parser.add_argument(
|
|
1503
|
+
'--root', default=None,
|
|
1504
|
+
help='Project root to enumerate changed files in (default: cwd). The '
|
|
1505
|
+
'caller MUST pass the TARGET project dir here, not the loki '
|
|
1506
|
+
'install dir -- the install dir is only the import path for '
|
|
1507
|
+
'`-m mcp.lsp_proxy`.',
|
|
1508
|
+
)
|
|
1320
1509
|
args = parser.parse_args()
|
|
1510
|
+
|
|
1511
|
+
if args.write_diagnostics:
|
|
1512
|
+
# One-shot writer mode -- no MCP server, no event loop. Always cleans
|
|
1513
|
+
# up any spawned LSP clients before exit. `--root` is the TARGET
|
|
1514
|
+
# project (where the diff lives); it is independent of the process cwd
|
|
1515
|
+
# (which the caller may set to the install dir so `-m mcp.lsp_proxy`
|
|
1516
|
+
# imports).
|
|
1517
|
+
try:
|
|
1518
|
+
status = write_diagnostics_artifact(root=args.root)
|
|
1519
|
+
finally:
|
|
1520
|
+
_cleanup_all_clients()
|
|
1521
|
+
print(json.dumps(status))
|
|
1522
|
+
return
|
|
1523
|
+
|
|
1321
1524
|
# SIGTERM handler so docker stop / supervisord stop triggers cleanup
|
|
1322
1525
|
# symmetrically to atexit. Re-raises via default handler so the
|
|
1323
1526
|
# process actually exits.
|
|
@@ -373,5 +373,174 @@ class LSPClientShutdownTests(unittest.TestCase):
|
|
|
373
373
|
fake_proc.kill.assert_called()
|
|
374
374
|
|
|
375
375
|
|
|
376
|
+
class DiagnosticsWriterTests(unittest.TestCase):
|
|
377
|
+
"""P1-5: the diagnostics WRITER that feeds the LSP-diagnostics quality
|
|
378
|
+
gate (loki-ts/src/runner/quality_gates.ts runLSPDiagnostics). Proves
|
|
379
|
+
non-vacuity (real severity-1 diagnostics -> blocking artifact) at the
|
|
380
|
+
recording/aggregation layer, and the no-false-fire honesty paths (no
|
|
381
|
+
server / no measurable file -> NO artifact, so the gate's absence path
|
|
382
|
+
fires instead of a fabricated clean verdict)."""
|
|
383
|
+
|
|
384
|
+
def setUp(self):
|
|
385
|
+
self.lp = _import_lsp_proxy()
|
|
386
|
+
self.lp._reset_detection_cache()
|
|
387
|
+
|
|
388
|
+
def _make_repo(self, files):
|
|
389
|
+
"""Create a tmp dir with the given {relpath: contents} and return it."""
|
|
390
|
+
d = tempfile.mkdtemp(prefix='loki-lsp-writer-')
|
|
391
|
+
for rel, body in files.items():
|
|
392
|
+
p = os.path.join(d, rel)
|
|
393
|
+
os.makedirs(os.path.dirname(p), exist_ok=True)
|
|
394
|
+
with open(p, 'w', encoding='utf-8') as fh:
|
|
395
|
+
fh.write(body)
|
|
396
|
+
return d
|
|
397
|
+
|
|
398
|
+
def test_no_server_writes_no_artifact(self):
|
|
399
|
+
"""No detected language server -> measured:false, no artifact. The
|
|
400
|
+
gate must then report 'did not run', never a clean verdict."""
|
|
401
|
+
root = self._make_repo({'a.ts': 'const x = 1;\n'})
|
|
402
|
+
loki = tempfile.mkdtemp(prefix='loki-lsp-out-')
|
|
403
|
+
with mock.patch.object(self.lp, '_detect_lsps', return_value={}):
|
|
404
|
+
status = self.lp.write_diagnostics_artifact(root=root, loki_dir=loki)
|
|
405
|
+
self.assertFalse(status['measured'])
|
|
406
|
+
self.assertFalse(status['wrote_artifact'])
|
|
407
|
+
self.assertFalse(os.path.isfile(
|
|
408
|
+
os.path.join(loki, 'quality', 'lsp-diagnostics.json')))
|
|
409
|
+
|
|
410
|
+
def test_server_present_but_no_matching_changed_file(self):
|
|
411
|
+
"""Server detected for rust, but the changed file is .ts -> nothing
|
|
412
|
+
real to measure -> no artifact."""
|
|
413
|
+
root = self._make_repo({'a.ts': 'const x = 1;\n'})
|
|
414
|
+
loki = tempfile.mkdtemp(prefix='loki-lsp-out-')
|
|
415
|
+
with mock.patch.object(self.lp, '_detect_lsps',
|
|
416
|
+
return_value={'rust': '/usr/bin/rust-analyzer'}), \
|
|
417
|
+
mock.patch.object(self.lp, '_writer_changed_files',
|
|
418
|
+
return_value=['a.ts']):
|
|
419
|
+
status = self.lp.write_diagnostics_artifact(root=root, loki_dir=loki)
|
|
420
|
+
self.assertFalse(status['measured'])
|
|
421
|
+
self.assertEqual(status['reason'], 'no-changed-file-with-detected-server')
|
|
422
|
+
self.assertFalse(os.path.isfile(
|
|
423
|
+
os.path.join(loki, 'quality', 'lsp-diagnostics.json')))
|
|
424
|
+
|
|
425
|
+
def test_real_error_recorded_and_blocks(self):
|
|
426
|
+
"""NON-VACUITY: a severity-1 diagnostic from the per-file LSP query is
|
|
427
|
+
recorded into the artifact with count_errors>0 -- the exact shape the
|
|
428
|
+
gate blocks on. Diagnostics source is mocked at the proxy boundary so
|
|
429
|
+
this exercises the writer's enumeration + aggregation + serialization
|
|
430
|
+
WITHOUT fabricating a verdict (the gate still independently reads the
|
|
431
|
+
file)."""
|
|
432
|
+
root = self._make_repo({'src/main.py': 'x: int = "nope"\n'})
|
|
433
|
+
loki = tempfile.mkdtemp(prefix='loki-lsp-out-')
|
|
434
|
+
fake = json.dumps({
|
|
435
|
+
'diagnostics': [
|
|
436
|
+
{'severity': 1, 'message': 'Type error', 'range': {}, 'source': 'x'},
|
|
437
|
+
{'severity': 2, 'message': 'Unused', 'range': {}},
|
|
438
|
+
{'severity': 3, 'message': 'Info note'},
|
|
439
|
+
],
|
|
440
|
+
'count_errors': 1, 'count_warnings': 1,
|
|
441
|
+
'language': 'python', 'elapsed_ms': 123.4,
|
|
442
|
+
})
|
|
443
|
+
with mock.patch.object(self.lp, '_detect_lsps',
|
|
444
|
+
return_value={'python': '/usr/bin/pyright'}), \
|
|
445
|
+
mock.patch.object(self.lp, '_writer_changed_files',
|
|
446
|
+
return_value=['src/main.py']), \
|
|
447
|
+
mock.patch.object(self.lp, '_lsp_get_diagnostics_blocking',
|
|
448
|
+
return_value=fake):
|
|
449
|
+
status = self.lp.write_diagnostics_artifact(root=root, loki_dir=loki)
|
|
450
|
+
self.assertTrue(status['measured'])
|
|
451
|
+
self.assertTrue(status['wrote_artifact'])
|
|
452
|
+
self.assertEqual(status['count_errors'], 1)
|
|
453
|
+
path = os.path.join(loki, 'quality', 'lsp-diagnostics.json')
|
|
454
|
+
self.assertTrue(os.path.isfile(path))
|
|
455
|
+
with open(path, encoding='utf-8') as fh:
|
|
456
|
+
artifact = json.load(fh)
|
|
457
|
+
self.assertEqual(artifact['count_errors'], 1)
|
|
458
|
+
self.assertEqual(artifact['count_warnings'], 1)
|
|
459
|
+
# Minimal deterministic shape: only severity/message/file survive;
|
|
460
|
+
# elapsed_ms / range / source are stripped.
|
|
461
|
+
self.assertEqual(len(artifact['diagnostics']), 3)
|
|
462
|
+
for d in artifact['diagnostics']:
|
|
463
|
+
self.assertEqual(set(d.keys()), {'file', 'severity', 'message'})
|
|
464
|
+
self.assertNotIn('elapsed_ms', artifact)
|
|
465
|
+
|
|
466
|
+
def test_per_file_lsp_error_does_not_count_clean(self):
|
|
467
|
+
"""A detected server that returns {'error':...} for the only changed
|
|
468
|
+
file means we measured nothing -> no artifact (NOT a fabricated
|
|
469
|
+
clean verdict)."""
|
|
470
|
+
root = self._make_repo({'src/main.py': 'x = 1\n'})
|
|
471
|
+
loki = tempfile.mkdtemp(prefix='loki-lsp-out-')
|
|
472
|
+
with mock.patch.object(self.lp, '_detect_lsps',
|
|
473
|
+
return_value={'python': '/usr/bin/pyright'}), \
|
|
474
|
+
mock.patch.object(self.lp, '_writer_changed_files',
|
|
475
|
+
return_value=['src/main.py']), \
|
|
476
|
+
mock.patch.object(self.lp, '_lsp_get_diagnostics_blocking',
|
|
477
|
+
return_value=json.dumps({'error': 'spawn failed'})):
|
|
478
|
+
status = self.lp.write_diagnostics_artifact(root=root, loki_dir=loki)
|
|
479
|
+
self.assertFalse(status['measured'])
|
|
480
|
+
self.assertEqual(status['reason'], 'no-file-yielded-diagnostics')
|
|
481
|
+
self.assertFalse(os.path.isfile(
|
|
482
|
+
os.path.join(loki, 'quality', 'lsp-diagnostics.json')))
|
|
483
|
+
|
|
484
|
+
def test_clean_file_writes_zero_artifact(self):
|
|
485
|
+
"""A measured file with no diagnostics writes a real 0/0 artifact --
|
|
486
|
+
this is a MEASURED clean result (we queried a live server), distinct
|
|
487
|
+
from the absence path."""
|
|
488
|
+
root = self._make_repo({'src/main.py': 'x = 1\n'})
|
|
489
|
+
loki = tempfile.mkdtemp(prefix='loki-lsp-out-')
|
|
490
|
+
with mock.patch.object(self.lp, '_detect_lsps',
|
|
491
|
+
return_value={'python': '/usr/bin/pyright'}), \
|
|
492
|
+
mock.patch.object(self.lp, '_writer_changed_files',
|
|
493
|
+
return_value=['src/main.py']), \
|
|
494
|
+
mock.patch.object(self.lp, '_lsp_get_diagnostics_blocking',
|
|
495
|
+
return_value=json.dumps({
|
|
496
|
+
'diagnostics': [], 'count_errors': 0,
|
|
497
|
+
'count_warnings': 0})):
|
|
498
|
+
status = self.lp.write_diagnostics_artifact(root=root, loki_dir=loki)
|
|
499
|
+
self.assertTrue(status['measured'])
|
|
500
|
+
path = os.path.join(loki, 'quality', 'lsp-diagnostics.json')
|
|
501
|
+
with open(path, encoding='utf-8') as fh:
|
|
502
|
+
artifact = json.load(fh)
|
|
503
|
+
self.assertEqual(artifact['count_errors'], 0)
|
|
504
|
+
self.assertEqual(artifact['count_warnings'], 0)
|
|
505
|
+
self.assertEqual(artifact['diagnostics'], [])
|
|
506
|
+
|
|
507
|
+
def test_stale_artifact_removed_when_unmeasured(self):
|
|
508
|
+
"""A previously-written artifact must be removed when a later run
|
|
509
|
+
measures nothing, so last iteration's errors cannot block forever."""
|
|
510
|
+
root = self._make_repo({'a.ts': 'const x = 1;\n'})
|
|
511
|
+
loki = tempfile.mkdtemp(prefix='loki-lsp-out-')
|
|
512
|
+
qdir = os.path.join(loki, 'quality')
|
|
513
|
+
os.makedirs(qdir, exist_ok=True)
|
|
514
|
+
stale = os.path.join(qdir, 'lsp-diagnostics.json')
|
|
515
|
+
with open(stale, 'w', encoding='utf-8') as fh:
|
|
516
|
+
fh.write('{"count_errors": 5, "count_warnings": 0, "diagnostics": []}')
|
|
517
|
+
with mock.patch.object(self.lp, '_detect_lsps', return_value={}):
|
|
518
|
+
self.lp.write_diagnostics_artifact(root=root, loki_dir=loki)
|
|
519
|
+
self.assertFalse(os.path.isfile(stale))
|
|
520
|
+
|
|
521
|
+
def test_deterministic_serialization(self):
|
|
522
|
+
"""Same inputs -> byte-identical artifact (route + run parity)."""
|
|
523
|
+
root = self._make_repo({'src/main.py': 'x = 1\n'})
|
|
524
|
+
loki1 = tempfile.mkdtemp(prefix='loki-lsp-out-')
|
|
525
|
+
loki2 = tempfile.mkdtemp(prefix='loki-lsp-out-')
|
|
526
|
+
fake = json.dumps({'diagnostics': [
|
|
527
|
+
{'severity': 2, 'message': 'b'},
|
|
528
|
+
{'severity': 1, 'message': 'a'},
|
|
529
|
+
]})
|
|
530
|
+
for lk in (loki1, loki2):
|
|
531
|
+
with mock.patch.object(self.lp, '_detect_lsps',
|
|
532
|
+
return_value={'python': '/usr/bin/pyright'}), \
|
|
533
|
+
mock.patch.object(self.lp, '_writer_changed_files',
|
|
534
|
+
return_value=['src/main.py']), \
|
|
535
|
+
mock.patch.object(self.lp, '_lsp_get_diagnostics_blocking',
|
|
536
|
+
return_value=fake):
|
|
537
|
+
self.lp.write_diagnostics_artifact(root=root, loki_dir=lk)
|
|
538
|
+
with open(os.path.join(loki1, 'quality', 'lsp-diagnostics.json'), 'rb') as fh:
|
|
539
|
+
b1 = fh.read()
|
|
540
|
+
with open(os.path.join(loki2, 'quality', 'lsp-diagnostics.json'), 'rb') as fh:
|
|
541
|
+
b2 = fh.read()
|
|
542
|
+
self.assertEqual(b1, b2)
|
|
543
|
+
|
|
544
|
+
|
|
376
545
|
if __name__ == '__main__':
|
|
377
546
|
unittest.main()
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loki-mode",
|
|
3
3
|
"mcpName": "io.github.asklokesh/loki-mode",
|
|
4
|
-
"version": "7.
|
|
4
|
+
"version": "7.52.0",
|
|
5
5
|
"description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 8 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"agent",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "loki-mode",
|
|
4
4
|
"displayName": "Loki Mode",
|
|
5
|
-
"version": "7.
|
|
5
|
+
"version": "7.52.0",
|
|
6
6
|
"description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 8 quality gates, completion council). Ships Loki's spec-hardening, drift-detection, and deterministic PR verification commands plus the Loki MCP server.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Autonomi",
|
package/providers/codex.sh
CHANGED
|
@@ -29,10 +29,14 @@ PROVIDER_CLI="codex"
|
|
|
29
29
|
|
|
30
30
|
# CLI Invocation
|
|
31
31
|
# Note: codex uses positional prompt after "exec" subcommand
|
|
32
|
-
# VERIFIED:
|
|
33
|
-
#
|
|
32
|
+
# VERIFIED: codex 0.132.0 deprecates --full-auto (prints a deprecation warning
|
|
33
|
+
# and the flag is gone from `codex exec --help`). Use --sandbox workspace-write,
|
|
34
|
+
# which is the documented replacement and the sandbox --full-auto expanded to.
|
|
35
|
+
# `codex exec` is the non-interactive subcommand: it runs at approval "never"
|
|
36
|
+
# with no --ask-for-approval flag, so --sandbox workspace-write alone keeps the
|
|
37
|
+
# loop fully autonomous (verified against codex 0.132.0: no approval prompt).
|
|
34
38
|
# Alternative: "exec --dangerously-bypass-approvals-and-sandbox" (legacy, no sandbox)
|
|
35
|
-
PROVIDER_AUTONOMOUS_FLAG="exec --
|
|
39
|
+
PROVIDER_AUTONOMOUS_FLAG="exec --sandbox workspace-write --skip-git-repo-check"
|
|
36
40
|
PROVIDER_PROMPT_FLAG=""
|
|
37
41
|
PROVIDER_PROMPT_POSITIONAL=true
|
|
38
42
|
|
|
@@ -124,7 +128,7 @@ provider_version() {
|
|
|
124
128
|
provider_invoke() {
|
|
125
129
|
local prompt="$1"
|
|
126
130
|
shift
|
|
127
|
-
codex exec --
|
|
131
|
+
codex exec --sandbox workspace-write --skip-git-repo-check \
|
|
128
132
|
--model "$PROVIDER_MODEL_DEVELOPMENT" \
|
|
129
133
|
"$prompt" "$@"
|
|
130
134
|
}
|
|
@@ -182,11 +186,13 @@ resolve_model_for_tier() {
|
|
|
182
186
|
|
|
183
187
|
# Tier-aware invocation.
|
|
184
188
|
#
|
|
185
|
-
#
|
|
186
|
-
#
|
|
187
|
-
#
|
|
188
|
-
#
|
|
189
|
-
#
|
|
189
|
+
# Aligned with codex CLI 0.132.0 (verified: --full-auto deprecated/removed
|
|
190
|
+
# from `codex exec --help`). `codex exec` is the non-interactive subcommand and
|
|
191
|
+
# runs at approval "never" with no --ask-for-approval flag, so --sandbox
|
|
192
|
+
# workspace-write alone keeps the loop autonomous (verified: no approval prompt
|
|
193
|
+
# on codex 0.132.0). workspace-write is the documented --full-auto replacement
|
|
194
|
+
# and the safer default (scoped disk writes) over danger-full-access; readable
|
|
195
|
+
# in process listings.
|
|
190
196
|
#
|
|
191
197
|
# Optional env knobs:
|
|
192
198
|
# LOKI_CODEX_WEB_SEARCH=true enable codex --search (live web)
|
|
@@ -227,8 +233,7 @@ provider_invoke_with_tier() {
|
|
|
227
233
|
LOKI_CODEX_REASONING_EFFORT="$effort" \
|
|
228
234
|
CODEX_MODEL_REASONING_EFFORT="$effort" \
|
|
229
235
|
codex exec \
|
|
230
|
-
--
|
|
231
|
-
--sandbox danger-full-access \
|
|
236
|
+
--sandbox workspace-write \
|
|
232
237
|
--skip-git-repo-check \
|
|
233
238
|
--model "$model" \
|
|
234
239
|
"${extra_flags[@]}" \
|
|
@@ -286,7 +286,7 @@ All CLI flags have been verified against actual CLI help output:
|
|
|
286
286
|
| Provider | Flag | Verified Version | Notes |
|
|
287
287
|
|----------|------|------------------|-------|
|
|
288
288
|
| Claude | `--dangerously-skip-permissions` | v2.1.34 | Autonomous mode |
|
|
289
|
-
| Codex | `--
|
|
289
|
+
| Codex | `--sandbox workspace-write` | v0.132.0 | Recommended (--full-auto deprecated 0.125+); legacy: `exec --dangerously-bypass-approvals-and-sandbox` |
|
|
290
290
|
| Cline | `--auto-approve` | latest | Autonomous mode |
|
|
291
291
|
| Aider | `--yes-always` | latest | Autonomous mode |
|
|
292
292
|
|
|
@@ -231,13 +231,16 @@ Claude models support an `effort` parameter that controls reasoning depth withou
|
|
|
231
231
|
|
|
232
232
|
**Note:** The effort parameter and thinking prefixes serve different purposes. Effort controls the model's internal reasoning budget; thinking prefixes guide the structure of the response.
|
|
233
233
|
|
|
234
|
-
### Codex --
|
|
234
|
+
### Codex --sandbox workspace-write Flag
|
|
235
235
|
|
|
236
|
-
Codex CLI
|
|
236
|
+
Codex CLI deprecated `--full-auto` in v0.125+ (removed from `codex exec --help`,
|
|
237
|
+
emits a deprecation warning if used). The documented replacement is
|
|
238
|
+
`--sandbox workspace-write`. The `exec` subcommand is non-interactive by default
|
|
239
|
+
(approval: never), so the sandbox flag alone keeps the loop autonomous:
|
|
237
240
|
|
|
238
241
|
```bash
|
|
239
|
-
# Recommended (
|
|
240
|
-
codex --
|
|
242
|
+
# Recommended (codex 0.125+)
|
|
243
|
+
codex exec --sandbox workspace-write "$prompt"
|
|
241
244
|
|
|
242
245
|
# Legacy (still supported)
|
|
243
246
|
codex exec --dangerously-bypass-approvals-and-sandbox "$prompt"
|
package/skills/providers.md
CHANGED
|
@@ -6,7 +6,7 @@ Loki Mode supports four AI providers for autonomous execution.
|
|
|
6
6
|
|
|
7
7
|
> **CLI Flags Verified:** The autonomous mode flags have been verified against actual CLI help output:
|
|
8
8
|
> - Claude: `--dangerously-skip-permissions` (verified)
|
|
9
|
-
> - Codex: `exec --
|
|
9
|
+
> - Codex: `exec --sandbox workspace-write --skip-git-repo-check` (the harness invocation; --skip-git-repo-check required on fresh non-git dirs; --full-auto deprecated in codex 0.125+, workspace-write is the documented replacement) or `exec --dangerously-bypass-approvals-and-sandbox` (legacy)
|
|
10
10
|
|
|
11
11
|
| Feature | Claude Code | OpenAI Codex | Cline CLI | Aider |
|
|
12
12
|
|---------|-------------|--------------|-----------|-------|
|
|
@@ -70,7 +70,7 @@ Task(model="haiku", ...) # Fast tier (parallelize)
|
|
|
70
70
|
**Invocation:**
|
|
71
71
|
```bash
|
|
72
72
|
# Recommended (v0.98.0+)
|
|
73
|
-
codex exec --
|
|
73
|
+
codex exec --sandbox workspace-write --skip-git-repo-check "$prompt"
|
|
74
74
|
|
|
75
75
|
# Legacy (still supported)
|
|
76
76
|
codex exec --dangerously-bypass-approvals-and-sandbox "$prompt"
|