loki-mode 7.50.0 → 7.51.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/run.sh +226 -3
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +2 -2
- package/loki-ts/dist/loki.js +2 -2
- 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/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.51.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.51.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.
|
|
1
|
+
7.51.0
|
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
|
|
@@ -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
|
|
@@ -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.51.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.51.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.51.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=8015709BAB9E625464756E2164756E21
|
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.51.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.51.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",
|