loki-mode 8.8.0 → 8.9.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/loki +59 -6
- package/autonomy/run.sh +94 -0
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +44 -1
- package/loki-ts/data/model-pricing.json +35 -10
- package/loki-ts/dist/loki.js +107 -106
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/codex.sh +21 -3
- package/providers/model_catalog.json +20 -8
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 v8.
|
|
6
|
+
# Loki Mode v8.9.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -469,4 +469,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
469
469
|
|
|
470
470
|
---
|
|
471
471
|
|
|
472
|
-
**v8.
|
|
472
|
+
**v8.9.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
8.
|
|
1
|
+
8.9.0
|
package/autonomy/loki
CHANGED
|
@@ -17629,12 +17629,37 @@ else:
|
|
|
17629
17629
|
integrations.append(kw)
|
|
17630
17630
|
|
|
17631
17631
|
# Count database mentions
|
|
17632
|
-
|
|
17633
|
-
|
|
17634
|
-
|
|
17635
|
-
|
|
17636
|
-
|
|
17637
|
-
|
|
17632
|
+
# Count DISTINCT data stores. This feeds a +1-complexity-for-multiple
|
|
17633
|
+
# rule, so what counts as multiple is load-bearing: it pushed a
|
|
17634
|
+
# 322-word issue from moderate to complex, and complex means 18 iterations
|
|
17635
|
+
# instead of 9.
|
|
17636
|
+
#
|
|
17637
|
+
# Three ways the old flat list over-counted a single-database project:
|
|
17638
|
+
# 1. Aliases. 'postgresql' and 'postgres' were separate entries, so one
|
|
17639
|
+
# PostgreSQL app scored TWO data stores by itself. That alone tripped
|
|
17640
|
+
# the rule on a real user's issue.
|
|
17641
|
+
# 2. ORMs. prisma/typeorm/sequelize/drizzle/knex are ACCESS LAYERS, not
|
|
17642
|
+
# stores. Prisma plus PostgreSQL is one database, not two.
|
|
17643
|
+
# 3. Generic words. 'database' and 'migration' matched any PRD that used
|
|
17644
|
+
# the words at all, so a migration-to-the-database line scored two.
|
|
17645
|
+
#
|
|
17646
|
+
# Aliases collapse to one canonical name, ORMs and generic terms are gone.
|
|
17647
|
+
db_aliases = {
|
|
17648
|
+
'postgresql': 'postgres', 'postgres': 'postgres', 'psql': 'postgres',
|
|
17649
|
+
'mysql': 'mysql', 'mariadb': 'mysql',
|
|
17650
|
+
'mongodb': 'mongodb', 'mongo': 'mongodb',
|
|
17651
|
+
'sqlite': 'sqlite',
|
|
17652
|
+
'dynamodb': 'dynamodb',
|
|
17653
|
+
'cassandra': 'cassandra',
|
|
17654
|
+
'redis': 'redis',
|
|
17655
|
+
'elasticsearch': 'elasticsearch',
|
|
17656
|
+
'clickhouse': 'clickhouse',
|
|
17657
|
+
}
|
|
17658
|
+
_seen_db = set()
|
|
17659
|
+
for kw, canonical in db_aliases.items():
|
|
17660
|
+
if kw in content_lower and canonical not in _seen_db:
|
|
17661
|
+
_seen_db.add(canonical)
|
|
17662
|
+
databases.append(canonical)
|
|
17638
17663
|
|
|
17639
17664
|
# Count UI components
|
|
17640
17665
|
ui_keywords = ['dashboard', 'form', 'table', 'modal', 'navbar',
|
|
@@ -17717,6 +17742,34 @@ iteration_map = {
|
|
|
17717
17742
|
}
|
|
17718
17743
|
min_iter, max_iter = iteration_map[complexity]
|
|
17719
17744
|
|
|
17745
|
+
# --- Capability-aware iteration budget ---------------------------------------
|
|
17746
|
+
# The map above was written when a model could not hold a whole feature in one
|
|
17747
|
+
# pass, so work was chopped into many small iterations. On a frontier tier that
|
|
17748
|
+
# is now actively harmful: each iteration boundary is a context reset, and
|
|
17749
|
+
# re-planning mid-feature is where a run loses the thread and burns money.
|
|
17750
|
+
#
|
|
17751
|
+
# A frontier model should be handed the whole job and left to finish it. So the
|
|
17752
|
+
# budget is divided by the capability tier the run will actually dispatch,
|
|
17753
|
+
# rather than assuming the weakest case for everyone. The floor is 1, never 0.
|
|
17754
|
+
#
|
|
17755
|
+
# This scales the ESTIMATE and the ceiling, not the work. A run that genuinely
|
|
17756
|
+
# needs more iterations still takes them; nothing here forces an early stop.
|
|
17757
|
+
_tier_raw = (os.environ.get('LOKI_SESSION_MODEL') or '').strip().lower()
|
|
17758
|
+
_frontier = ('high', 'planning', 'opus', 'sol')
|
|
17759
|
+
_small = ('small', 'fast', 'haiku', 'luna')
|
|
17760
|
+
if _tier_raw in _frontier:
|
|
17761
|
+
# Frontier: one pass for anything short of enterprise scope.
|
|
17762
|
+
min_iter = 1
|
|
17763
|
+
max_iter = max(1, max_iter // 4)
|
|
17764
|
+
elif _tier_raw in _small:
|
|
17765
|
+
# Small models genuinely benefit from more, smaller steps.
|
|
17766
|
+
pass
|
|
17767
|
+
else:
|
|
17768
|
+
# medium / unset: the common default. Halve the legacy budget -- current
|
|
17769
|
+
# workhorse models complete in far fewer passes than this map assumed.
|
|
17770
|
+
min_iter = max(1, min_iter // 2)
|
|
17771
|
+
max_iter = max(min_iter, max_iter // 2)
|
|
17772
|
+
|
|
17720
17773
|
# Adjust based on specific counts
|
|
17721
17774
|
if endpoint_count > 10:
|
|
17722
17775
|
max_iter = max(max_iter, max_iter + endpoint_count // 5)
|
package/autonomy/run.sh
CHANGED
|
@@ -630,6 +630,70 @@ PY
|
|
|
630
630
|
export LOKI_SPEC_SHA256
|
|
631
631
|
}
|
|
632
632
|
|
|
633
|
+
# Scoped-change profile: a bug fix or a single feature in an EXISTING repo.
|
|
634
|
+
#
|
|
635
|
+
# Measured on a real user's run: a 322-word GitHub issue against an existing
|
|
636
|
+
# codebase spent 25+ minutes still inside iteration 1. The work itself was
|
|
637
|
+
# genuine, but the build was also running competitor web research, load and
|
|
638
|
+
# performance testing, regression simulation and UAT -- all of which default to
|
|
639
|
+
# true, and none of which a scoped issue fix needs. That is the difference
|
|
640
|
+
# between a 5-minute fix and a 30-minute one.
|
|
641
|
+
#
|
|
642
|
+
# What this NEVER touches: code review, security, tests, E2E, and the
|
|
643
|
+
# completion council all stay on. Speed here comes from not running phases that
|
|
644
|
+
# are irrelevant to the change, never from skipping verification. A greenfield
|
|
645
|
+
# build or a whole-repo refactor does not match this profile and keeps the full
|
|
646
|
+
# suite.
|
|
647
|
+
#
|
|
648
|
+
# Auto-detected rather than another flag the user has to know: an existing git
|
|
649
|
+
# repo with real history, plus a spec that reads as a scoped change. Set
|
|
650
|
+
# LOKI_SCOPED_CHANGE=0 to force the full suite, or =1 to force this profile.
|
|
651
|
+
loki_detect_scoped_change() {
|
|
652
|
+
# Explicit operator intent always wins, in both directions.
|
|
653
|
+
case "${LOKI_SCOPED_CHANGE:-}" in
|
|
654
|
+
0|false) return 1 ;;
|
|
655
|
+
1|true) return 0 ;;
|
|
656
|
+
esac
|
|
657
|
+
|
|
658
|
+
# Greenfield is not a scoped change: no repo, or a repo with almost no
|
|
659
|
+
# history, means we are building something new.
|
|
660
|
+
local target="${TARGET_DIR:-.}"
|
|
661
|
+
[ -d "$target/.git" ] || return 1
|
|
662
|
+
local commits
|
|
663
|
+
commits="$(git -C "$target" rev-list --count HEAD 2>/dev/null || echo 0)"
|
|
664
|
+
[ "${commits:-0}" -ge 5 ] || return 1
|
|
665
|
+
|
|
666
|
+
# An issue-sourced spec is the canonical scoped change: someone filed a
|
|
667
|
+
# discrete request against code that already exists.
|
|
668
|
+
[ -n "${LOKI_ISSUE_REF:-}" ] && return 0
|
|
669
|
+
case "${LOKI_PRD_FILE:-}" in
|
|
670
|
+
*prd-issue-*) return 0 ;;
|
|
671
|
+
esac
|
|
672
|
+
|
|
673
|
+
return 1
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
loki_apply_scoped_change_profile() {
|
|
677
|
+
loki_detect_scoped_change || return 0
|
|
678
|
+
|
|
679
|
+
# Off: cannot affect the correctness of a scoped change to existing code.
|
|
680
|
+
: "${LOKI_PHASE_WEB_RESEARCH:=false}"
|
|
681
|
+
: "${LOKI_PHASE_PERFORMANCE:=false}"
|
|
682
|
+
: "${LOKI_PHASE_REGRESSION:=false}"
|
|
683
|
+
: "${LOKI_PHASE_UAT:=false}"
|
|
684
|
+
|
|
685
|
+
# On: every trust gate, unchanged. These are the moat.
|
|
686
|
+
: "${LOKI_PHASE_CODE_REVIEW:=true}"
|
|
687
|
+
: "${LOKI_PHASE_SECURITY:=true}"
|
|
688
|
+
: "${LOKI_PHASE_UNIT_TESTS:=true}"
|
|
689
|
+
: "${LOKI_PHASE_E2E_TESTS:=true}"
|
|
690
|
+
|
|
691
|
+
export LOKI_PHASE_WEB_RESEARCH LOKI_PHASE_PERFORMANCE LOKI_PHASE_REGRESSION
|
|
692
|
+
export LOKI_PHASE_UAT LOKI_PHASE_CODE_REVIEW LOKI_PHASE_SECURITY
|
|
693
|
+
export LOKI_PHASE_UNIT_TESTS LOKI_PHASE_E2E_TESTS
|
|
694
|
+
export LOKI_SCOPED_CHANGE_ACTIVE=1
|
|
695
|
+
}
|
|
696
|
+
|
|
633
697
|
loki_apply_build_profile() {
|
|
634
698
|
[ "${LOKI_BUILD_PROFILE:-}" = "simple-web" ] || return 0
|
|
635
699
|
: "${LOKI_PHASE_API_TESTS:=false}"
|
|
@@ -772,6 +836,36 @@ print(catalog["providers"]["claude"]["cli_aliases"].get(os.environ["_LOKI_SELECT
|
|
|
772
836
|
export LOKI_SDK_JUDGE_MODEL LOKI_SDK_PRD_ENRICH_MODEL LOKI_SDK_REVIEW_MODEL
|
|
773
837
|
}
|
|
774
838
|
loki_apply_build_profile
|
|
839
|
+
loki_apply_scoped_change_profile
|
|
840
|
+
|
|
841
|
+
# Default hang guard for EVERY build, not just simple-web.
|
|
842
|
+
#
|
|
843
|
+
# The two timeouts above are set inside loki_apply_build_profile(), which
|
|
844
|
+
# returns immediately unless LOKI_BUILD_PROFILE=simple-web. So on a normal
|
|
845
|
+
# build both resolved to 0, and 0 means no guard at all -- verified by running
|
|
846
|
+
# the deadline helper directly: `deadline.py 0 0 3 -- sleep 5` runs to
|
|
847
|
+
# completion unkilled. A provider that hung had nothing to stop it.
|
|
848
|
+
#
|
|
849
|
+
# IDLE only, and no retry. That is what keeps this compatible with the standing
|
|
850
|
+
# objection recorded above (search: "former invoke_with_timeout"), whose two
|
|
851
|
+
# reasons remain correct:
|
|
852
|
+
#
|
|
853
|
+
# 1. "No safe generous default" applies to a fixed TOTAL timeout, which
|
|
854
|
+
# cannot tell a long legitimate iteration from a hang. An idle timeout
|
|
855
|
+
# can: it measures silence, not duration. Verified both directions --
|
|
856
|
+
# `sleep 600` under a 120s idle cap dies, while a process emitting output
|
|
857
|
+
# every second survives indefinitely. A coding agent streams constantly;
|
|
858
|
+
# one silent for two minutes is not working.
|
|
859
|
+
# 2. "Wrong retry semantics" stands, so nothing here retries. The call is
|
|
860
|
+
# killed, and the existing failure path handles it. Re-running an agent
|
|
861
|
+
# that may have already edited files remains off the table.
|
|
862
|
+
#
|
|
863
|
+
# 7200s hard ceiling is a backstop against a process that streams forever
|
|
864
|
+
# without converging; the idle cap is the load-bearing guard. Both are
|
|
865
|
+
# overridable, and setting either to 0 restores the old unguarded behaviour.
|
|
866
|
+
: "${LOKI_PROVIDER_IDLE_TIMEOUT:=120}"
|
|
867
|
+
: "${LOKI_PROVIDER_CALL_TIMEOUT:=7200}"
|
|
868
|
+
export LOKI_PROVIDER_IDLE_TIMEOUT LOKI_PROVIDER_CALL_TIMEOUT
|
|
775
869
|
|
|
776
870
|
loki_background_services_enabled() {
|
|
777
871
|
! loki_is_supervised_simple_web
|
package/dashboard/__init__.py
CHANGED
package/dashboard/server.py
CHANGED
|
@@ -678,6 +678,37 @@ async def _push_loki_state_loop() -> None:
|
|
|
678
678
|
except (json.JSONDecodeError, KeyError):
|
|
679
679
|
pass
|
|
680
680
|
|
|
681
|
+
# Third source: the .loki/pids/ registry, which a
|
|
682
|
+
# CLI-started background run DOES write. Without this
|
|
683
|
+
# the dashboard reported STOPPED for a healthy build:
|
|
684
|
+
# `loki start` writes neither loki.pid nor session.json
|
|
685
|
+
# (run.sh only UPDATES session.json when it already
|
|
686
|
+
# exists), so both checks above failed and every such
|
|
687
|
+
# run fell through to "stopped" while it was actively
|
|
688
|
+
# working. Confirmed against a live build: STATUS.txt
|
|
689
|
+
# said BUILDING and iterations were advancing while the
|
|
690
|
+
# dashboard showed STOPPED with 0 agents.
|
|
691
|
+
#
|
|
692
|
+
# Liveness is proven with os.kill(pid, 0), never by the
|
|
693
|
+
# file's presence -- a stale entry from a crashed run
|
|
694
|
+
# must NOT read as alive, which is the same
|
|
695
|
+
# anti-stale rule BUG-NEW-006 established above.
|
|
696
|
+
if not _pid_alive:
|
|
697
|
+
try:
|
|
698
|
+
_pid_dir = loki_dir / "pids"
|
|
699
|
+
for _entry in _pid_dir.glob("*.json"):
|
|
700
|
+
_rec = _safe_json_read(_entry, {})
|
|
701
|
+
if _rec.get("kind") not in ("wrapper", "runner"):
|
|
702
|
+
continue
|
|
703
|
+
try:
|
|
704
|
+
os.kill(int(_rec.get("pid", 0)), 0)
|
|
705
|
+
except (ValueError, OSError, ProcessLookupError):
|
|
706
|
+
continue
|
|
707
|
+
_pid_alive = True
|
|
708
|
+
break
|
|
709
|
+
except OSError:
|
|
710
|
+
pass
|
|
711
|
+
|
|
681
712
|
status_str = raw.get("mode", "autonomous")
|
|
682
713
|
# Control files are the AUTHORITY, and they are checked
|
|
683
714
|
# first. dashboard-state.json's "mode" is written by the
|
|
@@ -2925,7 +2956,7 @@ def _provider_model_offers(provider: str) -> list[dict]:
|
|
|
2925
2956
|
Every other provider is offered the generic tiers (small/medium/high), which
|
|
2926
2957
|
are provider-independent, each annotated with the concrete model id the
|
|
2927
2958
|
catalog says that provider dispatches. That is what makes the picker read
|
|
2928
|
-
"medium -> gpt-5.
|
|
2959
|
+
"medium -> gpt-5.6-terra" on codex and "medium -> claude-sonnet-5" on claude
|
|
2929
2960
|
without the frontend knowing a single model id.
|
|
2930
2961
|
"""
|
|
2931
2962
|
if provider == "claude":
|
|
@@ -7267,6 +7298,15 @@ _DEFAULT_PRICING = {
|
|
|
7267
7298
|
"haiku": {"input": 1.00, "output": 5.00},
|
|
7268
7299
|
# OpenAI Codex
|
|
7269
7300
|
"gpt-5.3-codex": {"input": 1.50, "output": 12.00},
|
|
7301
|
+
# gpt-5.6 line: sol (high) / terra (medium, default) / luna (small).
|
|
7302
|
+
# UNVERIFIED RATES. The model IDs are confirmed against
|
|
7303
|
+
# developers.openai.com/api/docs/models, but OpenAI's published per-token
|
|
7304
|
+
# prices for this line were not, so these are placeholders scaled from the
|
|
7305
|
+
# gpt-5.3 rate. They drive a display estimate only, never a gate. Replace
|
|
7306
|
+
# from the pricing page; tools/probe-model-catalog.py is the refresh path.
|
|
7307
|
+
"gpt-5.6-sol": {"input": 2.50, "output": 20.00},
|
|
7308
|
+
"gpt-5.6-terra": {"input": 1.50, "output": 12.00},
|
|
7309
|
+
"gpt-5.6-luna": {"input": 0.50, "output": 4.00},
|
|
7270
7310
|
}
|
|
7271
7311
|
|
|
7272
7312
|
# Active pricing - starts with defaults, updated from .loki/pricing.json
|
|
@@ -7843,6 +7883,9 @@ _PROVIDER_LABELS = {
|
|
|
7843
7883
|
"sonnet": "Sonnet 5",
|
|
7844
7884
|
"haiku": "Haiku 4.5",
|
|
7845
7885
|
"gpt-5.3-codex": "GPT-5.3 Codex",
|
|
7886
|
+
"gpt-5.6-sol": "GPT-5.6 Sol",
|
|
7887
|
+
"gpt-5.6-terra": "GPT-5.6 Terra",
|
|
7888
|
+
"gpt-5.6-luna": "GPT-5.6 Luna",
|
|
7846
7889
|
}
|
|
7847
7890
|
|
|
7848
7891
|
# Display-only pricing notes, keyed by model. These annotate the pricing table in
|
|
@@ -1,13 +1,38 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
2
|
+
"$schema_version": 1,
|
|
3
|
+
"_comment": "Rolling pricing table consumed by loki-ts/src/runner/budget.ts. Update this file when Anthropic / OpenAI / others publish new prices; no code change required. Pricing is USD per 1 million tokens. Aliases (opus/sonnet/haiku) point to the latest model of that family per providers/model_catalog.json.",
|
|
4
|
+
"_updated": "2026-06-30",
|
|
5
|
+
"_source": "https://www.anthropic.com/pricing + provider docs. sonnet=claude-sonnet-5 (list $3/$15; intro $2/$10 through 2026-08-31). opus=claude-opus-4-8. codex=gpt-5.3-codex standard tier. Cache tiers: read 0.1x input, write 1.25x input (Anthropic + OpenAI published multipliers).",
|
|
6
|
+
"pricing": {
|
|
7
|
+
"fable": {
|
|
8
|
+
"input": 10.0,
|
|
9
|
+
"output": 50.0,
|
|
10
|
+
"cache_read": 1.0,
|
|
11
|
+
"cache_write": 12.5
|
|
12
|
+
},
|
|
13
|
+
"opus": {
|
|
14
|
+
"input": 5.0,
|
|
15
|
+
"output": 25.0,
|
|
16
|
+
"cache_read": 0.5,
|
|
17
|
+
"cache_write": 6.25
|
|
18
|
+
},
|
|
19
|
+
"sonnet": {
|
|
20
|
+
"input": 3.0,
|
|
21
|
+
"output": 15.0,
|
|
22
|
+
"cache_read": 0.3,
|
|
23
|
+
"cache_write": 3.75
|
|
24
|
+
},
|
|
25
|
+
"haiku": {
|
|
26
|
+
"input": 1.0,
|
|
27
|
+
"output": 5.0,
|
|
28
|
+
"cache_read": 0.1,
|
|
29
|
+
"cache_write": 1.25
|
|
30
|
+
},
|
|
31
|
+
"gpt-5.3-codex": {
|
|
32
|
+
"input": 1.75,
|
|
33
|
+
"output": 14.0,
|
|
34
|
+
"cache_read": 0.175,
|
|
35
|
+
"cache_write": 2.1875
|
|
12
36
|
}
|
|
37
|
+
}
|
|
13
38
|
}
|