loki-mode 9.49.2 → 9.49.4
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/.github/workflows/loki-issue-to-pr.yml +1 -1
- package/README.md +2 -2
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/lib/claude-flags.sh +18 -2
- package/autonomy/lib/proof-verify.py +4 -2
- package/autonomy/notification-checker.py +45 -17
- package/autonomy/run.sh +43 -4
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +21 -8
- package/docs/INSTALLATION.md +1 -1
- package/docs/P0-SWEEP-PLAN.md +1 -1
- package/docs/cost-controls.md +1 -1
- package/docs/cursor-comparison.md +1 -1
- package/docs/environment-variables.md +1 -1
- package/loki-ts/dist/loki.js +240 -240
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/skills/quality-gates.md +1 -1
- package/web-app/dist/assets/{AdminPage-DpV2vW3y.js → AdminPage-y6_b2Xo4.js} +1 -1
- package/web-app/dist/assets/{Avatar-BXeMYK1_.js → Avatar-BFPkXhIJ.js} +1 -1
- package/web-app/dist/assets/{Badge-D8WddQk1.js → Badge-BF3mhT57.js} +1 -1
- package/web-app/dist/assets/{Button-CFzTTVeJ.js → Button-BDp9aJ9q.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-D1eQe9QF.js → CockpitPage-5TKICCoA.js} +1 -1
- package/web-app/dist/assets/{ComparePage-uX3OPphr.js → ComparePage-BSuNsUHy.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-wE3oxaG6.js → ErrorBoundary-ut3wrUmu.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-B_Wouz6N.js → EvidenceReceiptPanel-CcaBwEI-.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-DRlEeFuw.js → GitHubIssuesPanel-DJeYgzAa.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-CCv6_I74.js → GitHubPRsPanel-DurZgSaj.js} +1 -1
- package/web-app/dist/assets/{HomePage-EG-jq8nR.js → HomePage-Bs451LAy.js} +1 -1
- package/web-app/dist/assets/{LoginPage-CEOnBICH.js → LoginPage-HeRGJeZy.js} +1 -1
- package/web-app/dist/assets/{MagicPage-DeWaFdsG.js → MagicPage-CUKrqsZs.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-CG8CuSSJ.js → MetricsPage-DvY1csgz.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-Mz5rOat6.js → NotFoundPage-BZDZ-B77.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-D2Fh6tVw.js → ProjectPage-CqHkEZkl.js} +25 -25
- package/web-app/dist/assets/{ProjectsPage-B8cRWkWa.js → ProjectsPage-CLvCK1Y8.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-DKpsst6x.js → SettingsPage-DP7OmdwP.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-qab065PM.js → ShowcasePage-BGS2jTGH.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-9qmEH2Y6.js → SystemSettingsPage-P_rlLh0x.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-CW6lvW-0.js → TeamsPage-DxtBDcBC.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-B-a61T6a.js → TemplatesPage-5e-faH3W.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-CJy13rAn.js → TerminalOutput-togzv9Zl.js} +1 -1
- package/web-app/dist/assets/{activity-CA4moIvC.js → activity-Dreyb_Kr.js} +1 -1
- package/web-app/dist/assets/{bell-JyOeobLN.js → bell-DWH0drES.js} +1 -1
- package/web-app/dist/assets/{bot-Cu4UwP5w.js → bot-78MoCuDl.js} +1 -1
- package/web-app/dist/assets/{check-Drzd8-9c.js → check-ikfylPst.js} +1 -1
- package/web-app/dist/assets/{chevron-left-ClvdXm3Y.js → chevron-left-DXUqkd8J.js} +1 -1
- package/web-app/dist/assets/{circle-alert-CstyZfKG.js → circle-alert-Da74VO8P.js} +1 -1
- package/web-app/dist/assets/{clock-Bnz6tlQN.js → clock-mlGCFq8l.js} +1 -1
- package/web-app/dist/assets/{cloud-IPIgE4P3.js → cloud-B_Y5ZrIO.js} +1 -1
- package/web-app/dist/assets/{code-xml-BOZbQiOt.js → code-xml-Bbom7YWV.js} +1 -1
- package/web-app/dist/assets/{database-CHclPjog.js → database-CLcofiVp.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-D6RFNgdZ.js → dollar-sign-C5mkjXIM.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-CNayCvOY.js → file-code-corner-CVrkaOQP.js} +1 -1
- package/web-app/dist/assets/{file-plus-BmVfFwsX.js → file-plus-BV9240gX.js} +1 -1
- package/web-app/dist/assets/{folder-open-ChzGfLRr.js → folder-open-D_6yV78G.js} +1 -1
- package/web-app/dist/assets/{globe-DtXgCG3Z.js → globe-aZ0Zso4I.js} +1 -1
- package/web-app/dist/assets/{hammer-DCa0I_7V.js → hammer-EgOQNukG.js} +1 -1
- package/web-app/dist/assets/index-BSgmPwC0.css +1 -0
- package/web-app/dist/assets/{index-Cu-njJ8o.js → index-DvZ0Kk2x.js} +2 -2
- package/web-app/dist/assets/{layers-DxIhn7NZ.js → layers-BOVKmepE.js} +1 -1
- package/web-app/dist/assets/{lightbulb-D7Pzhd06.js → lightbulb-DdASgqOG.js} +1 -1
- package/web-app/dist/assets/{loader-circle-DXgc_u5I.js → loader-circle-0K8yLgHl.js} +1 -1
- package/web-app/dist/assets/{lock-qRAVU53b.js → lock-XzlDMy8M.js} +1 -1
- package/web-app/dist/assets/{mail-CHe3KyCw.js → mail-COu670_W.js} +1 -1
- package/web-app/dist/assets/{minus-B8p0FoYZ.js → minus-bQpUt9ff.js} +1 -1
- package/web-app/dist/assets/{package-C5LDn8SI.js → package-BtXTKusI.js} +1 -1
- package/web-app/dist/assets/{plus-CRd_FHOA.js → plus-PxeiD8ju.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-DKoSN8s3.js → refresh-cw-CjqzYczt.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-Nf8zB8G0.js → rotate-ccw-CY0os4DH.js} +1 -1
- package/web-app/dist/assets/{save-B8ytQLgW.js → save-v8qep5cY.js} +1 -1
- package/web-app/dist/assets/{server-D_VRDkaD.js → server-pIIs9vNp.js} +1 -1
- package/web-app/dist/assets/{shield-alert-PsDkSyFD.js → shield-alert-CsDqKXwA.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-Da1QQhqo.js → thumbs-up-CAh3HVzH.js} +1 -1
- package/web-app/dist/assets/{trash-2-ChoylpGQ.js → trash-2-Dt6urTyY.js} +1 -1
- package/web-app/dist/assets/{trending-down-CX2fUeho.js → trending-down-DrtBd_C9.js} +1 -1
- package/web-app/dist/assets/{trending-up-CGyYgZH7.js → trending-up-D-DbCHXm.js} +1 -1
- package/web-app/dist/assets/{upload-KkeVyg67.js → upload-D4l6DCOt.js} +1 -1
- package/web-app/dist/assets/{usePolling-C5f4xrXD.js → usePolling-D0aoTX80.js} +1 -1
- package/web-app/dist/assets/{user-WCGccIST.js → user-BQf_XJO_.js} +1 -1
- package/web-app/dist/index.html +2 -2
- package/web-app/dist/assets/index-D98GzaW8.css +0 -1
|
@@ -61,7 +61,7 @@ jobs:
|
|
|
61
61
|
# Pinned to a TAG, not @main. A moving ref would hand every user
|
|
62
62
|
# whatever main holds at the moment their issue is labeled, including a
|
|
63
63
|
# half-landed release. Bump this line deliberately.
|
|
64
|
-
uses: asklokesh/loki-mode/.github/actions/issue-to-pr@v9.49.
|
|
64
|
+
uses: asklokesh/loki-mode/.github/actions/issue-to-pr@v9.49.4
|
|
65
65
|
env:
|
|
66
66
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
67
67
|
with:
|
package/README.md
CHANGED
|
@@ -532,14 +532,14 @@ Required:
|
|
|
532
532
|
|
|
533
533
|
- An agent provider CLI: [Claude Code](https://docs.claude.com/en/docs/claude-code) (`claude`, Tier 1, recommended and E2E-verified - the provider Loki Mode is built for). Cline, Codex, Aider, and opencode are supported as experimental providers (wiring in place; not yet E2E-verified by us). Loki cannot run a build without one of these installed and authenticated.
|
|
534
534
|
- Python 3.10+ (`python3`) for the dashboard, memory system, and orchestration helpers.
|
|
535
|
+
- Node.js 18+ (`node`) for the npm install path and the bundled runtime.
|
|
536
|
+
- `jq` for the JSON that the shell flows and the quality gates parse.
|
|
535
537
|
- Git 2.x (`git`) for checkpoints and worktrees.
|
|
536
538
|
- `curl` for installation and network calls.
|
|
537
539
|
|
|
538
540
|
Recommended:
|
|
539
541
|
|
|
540
542
|
- Bun 1.3.0+ (`bun`) for the fast runtime (the recommended install path above installs it).
|
|
541
|
-
- Node.js 18+ and npm if you install via npm instead of Bun.
|
|
542
|
-
- `jq` for nicer JSON handling in shell flows.
|
|
543
543
|
- Docker if you want Loki's App Runner to run containerized projects, or to run Loki itself from the published image.
|
|
544
544
|
|
|
545
545
|
You also need credentials for whichever provider you use (for Claude Code, an authenticated `claude` login or `ANTHROPIC_API_KEY`). `loki doctor` flags a missing or unauthenticated provider as the first thing to fix.
|
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 v9.49.
|
|
6
|
+
# Loki Mode v9.49.4
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -470,4 +470,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
470
470
|
|
|
471
471
|
---
|
|
472
472
|
|
|
473
|
-
**v9.49.
|
|
473
|
+
**v9.49.4 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
9.49.
|
|
1
|
+
9.49.4
|
|
@@ -51,7 +51,21 @@ loki_effort_for_tier() {
|
|
|
51
51
|
|
|
52
52
|
# ---------- Remaining budget ----------
|
|
53
53
|
# Compute remaining budget = LOKI_BUDGET_LIMIT - cumulative spend so far.
|
|
54
|
-
# Spend lives in .loki/metrics/budget.json under "
|
|
54
|
+
# Spend lives in .loki/metrics/budget.json under "budget_used".
|
|
55
|
+
#
|
|
56
|
+
# It was read as "current_spend" until v9.50.0, a key NO production writer has
|
|
57
|
+
# ever emitted. All six writers of that file (run.sh:6581,17851,17869,
|
|
58
|
+
# loki-ts/src/runner/budget.ts:267, dashboard/server.py:7953, autonomy/loki)
|
|
59
|
+
# write "budget_used". The old citation pointed at run.sh:8167+, which is
|
|
60
|
+
# per-iteration token parsing and writes no budget key at all.
|
|
61
|
+
#
|
|
62
|
+
# Effect of the bug: spend always read 0, so --max-budget-usd was handed the
|
|
63
|
+
# FULL cap on every call instead of the remainder, and the per-call backstop
|
|
64
|
+
# never tightened as spend accumulated. Only reachable for users who set
|
|
65
|
+
# LOKI_BUDGET_LIMIT explicitly (run.sh sets BUDGET_LIMIT, not LOKI_*), i.e.
|
|
66
|
+
# exactly the cost-conscious users who asked for the cap.
|
|
67
|
+
# "current_spend" is still accepted as a fallback so a budget.json written by
|
|
68
|
+
# an in-flight older run still parses.
|
|
55
69
|
# Emit empty string when budget is unlimited (LOKI_BUDGET_LIMIT unset or 0).
|
|
56
70
|
# Emit empty when remaining <= 0 (caller decides what to do; we never emit 0).
|
|
57
71
|
loki_remaining_budget() {
|
|
@@ -71,7 +85,9 @@ import json, os, sys
|
|
|
71
85
|
try:
|
|
72
86
|
with open(os.environ["_LOKI_BUDGET_FILE"]) as f:
|
|
73
87
|
d = json.load(f)
|
|
74
|
-
v = d.get("
|
|
88
|
+
v = d.get("budget_used")
|
|
89
|
+
if v is None:
|
|
90
|
+
v = d.get("current_spend", 0)
|
|
75
91
|
print(float(v))
|
|
76
92
|
except Exception:
|
|
77
93
|
print(0)
|
|
@@ -324,7 +324,8 @@ def _compute_headline(facts, degraded):
|
|
|
324
324
|
or tests.get("status") == "failed"
|
|
325
325
|
or build.get("status") == "failed"
|
|
326
326
|
or any(g.get("status") == "failed"
|
|
327
|
-
for g in (facts.get("quality_gates") or [])
|
|
327
|
+
for g in (facts.get("quality_gates") or [])
|
|
328
|
+
if _is_exogenous(g))
|
|
328
329
|
or sec_high
|
|
329
330
|
or fn_failed
|
|
330
331
|
)
|
|
@@ -342,7 +343,8 @@ def _compute_headline(facts, degraded):
|
|
|
342
343
|
tests.get("status") == "verified"
|
|
343
344
|
or build.get("status") == "verified"
|
|
344
345
|
or any(g.get("status") == "passed"
|
|
345
|
-
for g in (facts.get("quality_gates") or [])
|
|
346
|
+
for g in (facts.get("quality_gates") or [])
|
|
347
|
+
if _is_exogenous(g))
|
|
346
348
|
)
|
|
347
349
|
if any_verified and degraded:
|
|
348
350
|
return "VERIFIED WITH GAPS"
|
|
@@ -165,7 +165,15 @@ def check_budget_threshold(trigger, loki_dir, iteration, notifications):
|
|
|
165
165
|
state = json.loads(state_file.read_text())
|
|
166
166
|
budget = state.get("budget", {})
|
|
167
167
|
limit = budget.get("limit", 0)
|
|
168
|
-
|
|
168
|
+
# run.sh:7084 nests .loki/metrics/budget.json verbatim under "budget",
|
|
169
|
+
# and that file's spend key is "budget_used". Reading only "used" meant
|
|
170
|
+
# this trigger never fired in production: verified against the exact
|
|
171
|
+
# state run.sh writes, 0 notifications were produced at 85% of cap.
|
|
172
|
+
# "used" stays as a fallback for the /api/cost-shaped payload
|
|
173
|
+
# (autonomy/loki:28122), which does write that key.
|
|
174
|
+
used = budget.get("budget_used")
|
|
175
|
+
if used is None:
|
|
176
|
+
used = budget.get("used", 0)
|
|
169
177
|
if limit <= 0:
|
|
170
178
|
return None
|
|
171
179
|
pct = (used / limit) * 100
|
|
@@ -241,24 +249,44 @@ def check_file_access(trigger, loki_dir, iteration, notifications):
|
|
|
241
249
|
|
|
242
250
|
|
|
243
251
|
def check_quality_gate(trigger, loki_dir, iteration, notifications):
|
|
244
|
-
"""Check for quality gate failures. Reports ALL failed gates, not just first.
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
252
|
+
"""Check for quality gate failures. Reports ALL failed gates, not just first.
|
|
253
|
+
|
|
254
|
+
Reads .loki/quality/gate-failures.txt, which is what run.sh actually writes
|
|
255
|
+
(:24100, :24142, :24553) as a comma-terminated list of failing gate names,
|
|
256
|
+
e.g. "mock_integrity,".
|
|
257
|
+
|
|
258
|
+
This previously read state["qualityGates"] out of dashboard-state.json,
|
|
259
|
+
sourced from .loki/state/quality-gates.json (run.sh:6972). NOTHING writes
|
|
260
|
+
that file: all seven repo references are readers, autonomy/hooks/
|
|
261
|
+
quality-gate.sh:12 guards on `[ ! -f ]`, and proof-generator.py:345 records
|
|
262
|
+
the same finding as issue #125 and already works around it by reading these
|
|
263
|
+
per-gate artifacts. The aggregate was therefore always absent, qualityGates
|
|
264
|
+
was always null, and this trigger could never fire -- the same dead-reader
|
|
265
|
+
defect as the budget key, in the same file.
|
|
266
|
+
|
|
267
|
+
gate-failures.txt is the canonical failure source used by
|
|
268
|
+
proof-generator.py:413 and loki-ts build_prompt_helpers.ts:250, so this
|
|
269
|
+
reuses a proven contract rather than introducing a second one.
|
|
270
|
+
"""
|
|
271
|
+
failures_file = Path(loki_dir) / "quality" / "gate-failures.txt"
|
|
249
272
|
results = []
|
|
250
273
|
try:
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
274
|
+
if failures_file.exists():
|
|
275
|
+
# Cap the read: a pathological file must not be loaded whole
|
|
276
|
+
# (same reason as build_prompt.ts:707).
|
|
277
|
+
raw = failures_file.read_text(errors="replace")[:8000]
|
|
278
|
+
seen = set()
|
|
279
|
+
for name in raw.replace("\n", ",").split(","):
|
|
280
|
+
gate_name = name.strip()
|
|
281
|
+
if not gate_name or gate_name in seen:
|
|
282
|
+
continue
|
|
283
|
+
seen.add(gate_name)
|
|
284
|
+
msg = trigger["message"].format(gate=gate_name)
|
|
285
|
+
results.append(make_notification(
|
|
286
|
+
trigger["id"], trigger["severity"], msg, iteration,
|
|
287
|
+
{"gate": gate_name},
|
|
288
|
+
))
|
|
289
|
+
except (OSError, KeyError):
|
|
262
290
|
pass
|
|
263
291
|
return results if results else None
|
|
264
292
|
|
package/autonomy/run.sh
CHANGED
|
@@ -1181,10 +1181,12 @@ PERPETUAL_MODE=${LOKI_PERPETUAL_MODE:-false}
|
|
|
1181
1181
|
#
|
|
1182
1182
|
# MEASURED, not chosen. Real per-iteration wall clock across every recorded run
|
|
1183
1183
|
# on this machine: median 718s, max 1746s. So the 1000 default is an 8.3-DAY
|
|
1184
|
-
# ceiling
|
|
1185
|
-
# disabled
|
|
1186
|
-
#
|
|
1187
|
-
# "
|
|
1184
|
+
# ceiling. When this analysis was written it was also the ONLY backstop,
|
|
1185
|
+
# because the other two valves shipped disabled. That is no longer true of the
|
|
1186
|
+
# budget: since v9.42.0 LOKI_BUDGET_LIMIT defaults to "100.00" (see :527 --
|
|
1187
|
+
# note `-` not `:-`, so only an EXPLICIT "" means unlimited), and breaching it
|
|
1188
|
+
# pauses the run. LOKI_MAX_DURATION does still default to 0 (check_max_duration
|
|
1189
|
+
# returns "never stop" at :18278).
|
|
1188
1190
|
#
|
|
1189
1191
|
# It also contradicts our own documentation. SETUP.md tells users to RAISE the
|
|
1190
1192
|
# budget for large work with LOKI_MAX_ITERATIONS=40, and the demo uses 10 -- so
|
|
@@ -17860,6 +17862,43 @@ BUDGETUPD_EOF
|
|
|
17860
17862
|
return 0
|
|
17861
17863
|
fi
|
|
17862
17864
|
|
|
17865
|
+
# LIVE COST VISIBILITY. The loop has always computed cumulative spend here,
|
|
17866
|
+
# every iteration, and printed NOTHING until 80% of the cap. That silence is
|
|
17867
|
+
# the "predatory / un-capped" feeling users report about agentic tools: the
|
|
17868
|
+
# enforcement was never missing (the cap pauses at 100.00 by default), only
|
|
17869
|
+
# the visibility was, so spend was discoverable solely after the fact.
|
|
17870
|
+
#
|
|
17871
|
+
# Reuses the $current_cost computed above; deliberately NOT a second reader.
|
|
17872
|
+
# Two readers of one number disagreeing is the exact defect class fixed in
|
|
17873
|
+
# 405da630, and a per-iteration cost line is not worth reintroducing it.
|
|
17874
|
+
#
|
|
17875
|
+
# Safe on stdout: this function has exactly two non-comment references in
|
|
17876
|
+
# the file -- its own definition, and the single loop call site, which
|
|
17877
|
+
# tests the exit code and never captures output. Verified with a
|
|
17878
|
+
# comment-excluding grep plus a positive control.
|
|
17879
|
+
#
|
|
17880
|
+
# This comment deliberately does NOT spell out the call-site expression.
|
|
17881
|
+
# tests/test-max-duration.sh:151 counts that exact string across this file
|
|
17882
|
+
# and asserts there is exactly ONE, so quoting it here made the count 2 and
|
|
17883
|
+
# turned a green suite red -- a text guard firing on prose written to
|
|
17884
|
+
# explain it. Describe the call site; never reproduce it.
|
|
17885
|
+
#
|
|
17886
|
+
# log_info is additionally gated by _loki_log_enabled, so quiet modes stay
|
|
17887
|
+
# quiet.
|
|
17888
|
+
# The zero test must be NUMERIC. `[ "$current_cost" != "0" ]` is a string
|
|
17889
|
+
# compare and the python above emits "0.0", so a run with nothing recorded
|
|
17890
|
+
# would announce "Cost so far: $0.0" -- claiming a measurement we do not
|
|
17891
|
+
# have, which is the exact fabrication this codebase deletes on sight.
|
|
17892
|
+
#
|
|
17893
|
+
# No else-branch: BUDGET_LIMIT empty means unlimited, and check_budget_limit
|
|
17894
|
+
# returns at its first line in that case, so any "no cap set" arm here would
|
|
17895
|
+
# be unreachable code advertising a message that can never print. The
|
|
17896
|
+
# no-cap disclosure already lives at run-start (show_run_start_estimate).
|
|
17897
|
+
if [ -n "$current_cost" ] \
|
|
17898
|
+
&& awk -v c="$current_cost" 'BEGIN{exit !(c+0 > 0)}' 2>/dev/null; then
|
|
17899
|
+
log_info "Cost so far: \$${current_cost} of \$${BUDGET_LIMIT} cap (iteration ${ITERATION_COUNT:-0})."
|
|
17900
|
+
fi
|
|
17901
|
+
|
|
17863
17902
|
# Update budget.json with current usage (not exceeded)
|
|
17864
17903
|
if [ -n "$current_cost" ] && [ "$current_cost" != "0" ]; then
|
|
17865
17904
|
cat > ".loki/metrics/budget.json" << BUDGETUPD_EOF
|
package/dashboard/__init__.py
CHANGED
package/dashboard/server.py
CHANGED
|
@@ -261,16 +261,16 @@ def _real_client_host(request: Request):
|
|
|
261
261
|
client = getattr(request, "client", None)
|
|
262
262
|
host = getattr(client, "host", None) if client else None
|
|
263
263
|
if host is None:
|
|
264
|
-
return None
|
|
264
|
+
return None, False
|
|
265
265
|
if host in _trusted_proxies():
|
|
266
266
|
fwd = request.headers.get("x-forwarded-for", "")
|
|
267
267
|
first = fwd.split(",")[0].strip()
|
|
268
268
|
if first:
|
|
269
|
-
return first
|
|
270
|
-
return host
|
|
269
|
+
return first, True
|
|
270
|
+
return host, False
|
|
271
271
|
|
|
272
272
|
|
|
273
|
-
def _is_local_caller(host) -> bool:
|
|
273
|
+
def _is_local_caller(host, from_forwarded: bool = False) -> bool:
|
|
274
274
|
"""True only for a caller we can positively identify as non-routable.
|
|
275
275
|
|
|
276
276
|
A peer that is not an IP literal (ASGI test transports report
|
|
@@ -285,7 +285,18 @@ def _is_local_caller(host) -> bool:
|
|
|
285
285
|
try:
|
|
286
286
|
return ipaddress.ip_address(host).is_loopback
|
|
287
287
|
except ValueError:
|
|
288
|
-
|
|
288
|
+
# A DIRECT peer that is not an IP literal is genuinely local: ASGI test
|
|
289
|
+
# transports report "testclient" and UDS transports report names, and
|
|
290
|
+
# refusing those closes no hole while breaking 17 tests.
|
|
291
|
+
#
|
|
292
|
+
# A FORWARDED value is different in kind. It arrived inside a request
|
|
293
|
+
# header, so treating an unparseable one as local lets any caller behind
|
|
294
|
+
# a trusted proxy pass the local check by sending a non-IP token.
|
|
295
|
+
# Measured: with LOKI_TRUSTED_PROXIES set, "not-an-ip", "unknown" and
|
|
296
|
+
# "testclient" all returned local, while an empty value correctly did
|
|
297
|
+
# not. "unknown" is a literal some proxies emit when the client address
|
|
298
|
+
# is unavailable, so this is reachable without an attacker choosing it.
|
|
299
|
+
return not from_forwarded
|
|
289
300
|
|
|
290
301
|
|
|
291
302
|
def require_local_or_authenticated(request: Request) -> None:
|
|
@@ -316,13 +327,13 @@ def require_local_or_authenticated(request: Request) -> None:
|
|
|
316
327
|
"""
|
|
317
328
|
if auth.ENTERPRISE_AUTH_ENABLED or auth.OIDC_ENABLED:
|
|
318
329
|
return
|
|
319
|
-
host = _real_client_host(request)
|
|
330
|
+
host, host_from_forwarded = _real_client_host(request)
|
|
320
331
|
if host is None:
|
|
321
332
|
raise HTTPException(
|
|
322
333
|
status_code=403,
|
|
323
334
|
detail="control requires an identifiable client; enable "
|
|
324
335
|
"LOKI_ENTERPRISE_AUTH to allow remote access")
|
|
325
|
-
if _is_local_caller(host):
|
|
336
|
+
if _is_local_caller(host, host_from_forwarded):
|
|
326
337
|
return
|
|
327
338
|
raise HTTPException(
|
|
328
339
|
status_code=403,
|
|
@@ -1211,14 +1222,16 @@ class WebSocketBoundaryMiddleware:
|
|
|
1211
1222
|
if not (auth.ENTERPRISE_AUTH_ENABLED or auth.OIDC_ENABLED):
|
|
1212
1223
|
client = scope.get("client")
|
|
1213
1224
|
host = client[0] if client else None
|
|
1225
|
+
host_from_forwarded = False
|
|
1214
1226
|
if host in _trusted_proxies():
|
|
1215
1227
|
for raw_name, raw_value in scope.get("headers", []):
|
|
1216
1228
|
if raw_name == b"x-forwarded-for":
|
|
1217
1229
|
first = raw_value.decode("latin-1").split(",")[0].strip()
|
|
1218
1230
|
if first:
|
|
1219
1231
|
host = first
|
|
1232
|
+
host_from_forwarded = True
|
|
1220
1233
|
break
|
|
1221
|
-
if not _is_local_caller(host):
|
|
1234
|
+
if not _is_local_caller(host, host_from_forwarded):
|
|
1222
1235
|
# 1008 = policy violation. Closing with a code beats an
|
|
1223
1236
|
# accept-then-drop, which reads to a client as a flaky network.
|
|
1224
1237
|
await send({"type": "websocket.close", "code": 1008})
|
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:** v9.49.
|
|
5
|
+
**Version:** v9.49.4
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
package/docs/P0-SWEEP-PLAN.md
CHANGED
|
@@ -42,7 +42,7 @@ Honest count after this sweep: 8 gates.
|
|
|
42
42
|
| 5 | Mock Integrity Detector | enforce_mock_integrity -> tests/detect-mock-problems.sh | Yes (HIGH blocks) | LOKI_GATE_MOCK=false |
|
|
43
43
|
| 6 | Test Mutation Detector | enforce_mutation_integrity -> tests/detect-test-mutations.sh | Yes (HIGH blocks) | LOKI_GATE_MUTATION=false |
|
|
44
44
|
| 7 | Documentation Coverage | run_doc_quality_gate (run.sh:7388) | Yes | LOKI_GATE_DOC_COVERAGE=false |
|
|
45
|
-
| 8 | Magic Modules Debate | run_magic_debate_gate (run.sh:7495) | Yes (BLOCK sev) | LOKI_GATE_MAGIC_DEBATE=false |
|
|
45
|
+
| 8 | Magic Modules Debate | run_magic_debate_gate (run.sh:7495) | No, advisory by default. NOTE added 2026-09-13: this row said "Yes (BLOCK sev)" and that was never true on either route; bash enforces only with LOKI_GATE_MAGIC_DEBATE_BLOCKING=true and Bun self-skips unless LOKI_GATE_MAGIC_DEBATE="true". Line numbers in this document are as grep-verified in June 2026 and are left unchanged. | LOKI_GATE_MAGIC_DEBATE=false |
|
|
46
46
|
|
|
47
47
|
Conditional auditor (documented separately, NOT numbered): Backward-Compatibility
|
|
48
48
|
/ legacy-healing-auditor (healing mode only). Removed: Input/Output Guardrails.
|
package/docs/cost-controls.md
CHANGED
|
@@ -25,7 +25,7 @@ They bound different things, and a run that stalls needs all three.
|
|
|
25
25
|
|
|
26
26
|
| Cap | Bounds | Default |
|
|
27
27
|
|---|---|---|
|
|
28
|
-
| `LOKI_BUDGET_LIMIT` | Total spend, in USD |
|
|
28
|
+
| `LOKI_BUDGET_LIMIT` | Total spend, in USD | `100.00` (breaching pauses the run) |
|
|
29
29
|
| `LOKI_MAX_ITERATIONS` | Number of iterations | 1000 |
|
|
30
30
|
| `LOKI_MAX_DURATION` | Wall-clock time | unset (no cap) |
|
|
31
31
|
|
|
@@ -76,7 +76,7 @@ velocity_quality_balance:
|
|
|
76
76
|
5. Mock Integrity Detector - HIGH blocks
|
|
77
77
|
6. Test Mutation Detector - HIGH blocks
|
|
78
78
|
7. Documentation Coverage
|
|
79
|
-
8. Magic Modules Debate -
|
|
79
|
+
8. Magic Modules Debate - advisory by default on both routes (bash needs LOKI_GATE_MAGIC_DEBATE_BLOCKING=true; Bun self-skips unless LOKI_GATE_MAGIC_DEBATE="true")
|
|
80
80
|
|
|
81
81
|
Conditional auditor (not numbered): backward-compatibility / legacy-healing-auditor (healing mode only).
|
|
82
82
|
|
|
@@ -23,7 +23,7 @@ with nowhere to look them up.
|
|
|
23
23
|
|---|---|---|
|
|
24
24
|
| `LOKI_PRD_FILE` | none | Path to a spec file, as an alternative to the positional argument. |
|
|
25
25
|
| `LOKI_MAX_ITERATIONS` | `1000` | Hard cap on iterations. Reaching it is a deterministic terminal failure, not a success. Also in `config schema`. |
|
|
26
|
-
| `LOKI_BUDGET_LIMIT` |
|
|
26
|
+
| `LOKI_BUDGET_LIMIT` | `100.00` | Spend cap in USD. Breaching it **pauses** the run (writes `.loki/PAUSE` and saves state); it never kills work or discards a deliverable. Remove `.loki/PAUSE` to resume, or raise the cap. Set to `""` for no cap. A warning is logged at 80%. |
|
|
27
27
|
| `LOKI_MAX_DURATION` | unset (no cap) | Wall-clock cap in seconds. Stops cleanly at the next iteration boundary with a `max_duration_reached` terminal status. `loki start --max-duration` also accepts `90m` / `2h`. |
|
|
28
28
|
| `LOKI_AUTO_CONFIRM` | unset | `true`/`false` to control prompts. Takes precedence over `CI`. |
|
|
29
29
|
| `LOKI_CONFIG_DUMP` | `0` | `1` prints the resolved configuration and exits **without starting a run or spending anything**. |
|