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 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.8.0
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.8.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
472
+ **v8.9.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 8.8.0
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
- db_keywords = ['postgresql', 'postgres', 'mysql', 'mongodb', 'sqlite',
17633
- 'dynamodb', 'cassandra', 'redis', 'prisma', 'typeorm',
17634
- 'sequelize', 'drizzle', 'knex', 'database', 'migration']
17635
- for kw in db_keywords:
17636
- if kw in content_lower:
17637
- databases.append(kw)
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
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "8.8.0"
10
+ __version__ = "8.9.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -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.3-codex" on codex and "medium -> claude-sonnet-5" on claude
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
- "$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.",
6
- "pricing": {
7
- "fable": { "input": 10.0, "output": 50.0 },
8
- "opus": { "input": 5.0, "output": 25.0 },
9
- "sonnet": { "input": 3.0, "output": 15.0 },
10
- "haiku": { "input": 1.0, "output": 5.0 },
11
- "gpt-5.3-codex": { "input": 1.75, "output": 14.0 }
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
  }