loki-mode 8.9.1 → 8.11.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/lib/iteration_attribution.py +91 -0
- package/autonomy/loki +3 -0
- package/autonomy/run.sh +308 -2
- package/dashboard/__init__.py +1 -1
- package/loki-ts/dist/loki.js +283 -282
- 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 +49 -0
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.11.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.11.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
8.
|
|
1
|
+
8.11.0
|
|
@@ -163,12 +163,103 @@ def _render(s):
|
|
|
163
163
|
return "\n".join(lines)
|
|
164
164
|
|
|
165
165
|
|
|
166
|
+
def _fmt_secs(ms):
|
|
167
|
+
return f"{ms / 1000.0:.0f}s"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
WINDOW = 3 # last N iterations rendered; the block ships in EVERY prompt
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def prompt_block(loki_dir):
|
|
174
|
+
"""Render the run's own efficiency trend for injection into the next prompt.
|
|
175
|
+
|
|
176
|
+
WHY THIS EXISTS. The engine has written .loki/metrics/efficiency/iteration-N.json
|
|
177
|
+
every iteration for its entire life and never once read it back into a decision.
|
|
178
|
+
Cost, duration and cache data flowed OUT to a budget breaker and an offline
|
|
179
|
+
report, never IN to the agent producing the cost. The agent was the only party
|
|
180
|
+
to the run with no visibility into its own efficiency.
|
|
181
|
+
|
|
182
|
+
THE INCENTIVE TRAP, AND THE GUARD. A block reporting cost and duration ALONE
|
|
183
|
+
instructs the model to be cheap, and the cheapest iteration is the one that
|
|
184
|
+
does less work and verifies less. That is gate-weakening through the front
|
|
185
|
+
door, with no gate edited. So spend is NEVER rendered without the progress /
|
|
186
|
+
rework split beside it: the actionable number is "you failed N iterations and
|
|
187
|
+
repeated them", whose correct response is "stop failing gates", not "spend
|
|
188
|
+
less". Enforced by test, not by convention.
|
|
189
|
+
|
|
190
|
+
Returns "" when there are no usable records, so an absent or empty metrics
|
|
191
|
+
dir adds NOTHING to the prompt (no dangling header). This is why the block is
|
|
192
|
+
rendered here rather than from attribute(), whose notes list is non-empty even
|
|
193
|
+
on an empty dir.
|
|
194
|
+
"""
|
|
195
|
+
recs = _iteration_records(loki_dir)
|
|
196
|
+
if not recs:
|
|
197
|
+
return ""
|
|
198
|
+
|
|
199
|
+
s = attribute(loki_dir)
|
|
200
|
+
lines = []
|
|
201
|
+
|
|
202
|
+
# Per-iteration tail: the shape of the trend (getting slower? pricier?) is
|
|
203
|
+
# what a model can actually steer on, and it is invisible in an aggregate.
|
|
204
|
+
# Windowed to the last few so the block stays a few lines in EVERY prompt.
|
|
205
|
+
tail = recs[-WINDOW:]
|
|
206
|
+
for r in tail:
|
|
207
|
+
it = r.get("iteration", "?")
|
|
208
|
+
parts = []
|
|
209
|
+
dur = r.get("duration_ms")
|
|
210
|
+
if isinstance(dur, (int, float)) and dur >= 0:
|
|
211
|
+
parts.append(_fmt_secs(dur))
|
|
212
|
+
cost = r.get("cost_usd")
|
|
213
|
+
if isinstance(cost, (int, float)):
|
|
214
|
+
parts.append(f"${float(cost):.2f}")
|
|
215
|
+
cread = r.get("cache_read_tokens")
|
|
216
|
+
inp = r.get("input_tokens")
|
|
217
|
+
if isinstance(cread, (int, float)) and isinstance(inp, (int, float)):
|
|
218
|
+
denom = float(inp) + float(cread)
|
|
219
|
+
if denom > 0:
|
|
220
|
+
parts.append(f"cache {cread / denom:.0%}")
|
|
221
|
+
st = str(r.get("status", "")).strip().lower()
|
|
222
|
+
if st:
|
|
223
|
+
parts.append(st)
|
|
224
|
+
lines.append(f" iter {it}: " + ", ".join(parts) if parts else f" iter {it}")
|
|
225
|
+
|
|
226
|
+
if not lines:
|
|
227
|
+
return ""
|
|
228
|
+
|
|
229
|
+
header = "EFFICIENCY TREND (your own last %d iteration(s); steer on it):" % len(tail)
|
|
230
|
+
out = [header] + lines
|
|
231
|
+
|
|
232
|
+
# The anti-incentive guard: spend never ships without the outcome split.
|
|
233
|
+
prog = s["progress"]["count"]
|
|
234
|
+
rew = s["rework"]["count"]
|
|
235
|
+
total = s["iterations"]
|
|
236
|
+
out.append(f" progress {prog}/{total}, rework {rew}/{total}")
|
|
237
|
+
share = s.get("rework_cost_share")
|
|
238
|
+
if rew and share is not None:
|
|
239
|
+
out.append(
|
|
240
|
+
f" {rew} iteration(s) failed and were repeated, costing {share:.0%} of the run. "
|
|
241
|
+
"Cut rework by fixing what the gate flagged, NEVER by verifying less."
|
|
242
|
+
)
|
|
243
|
+
return "\n".join(out)
|
|
244
|
+
|
|
245
|
+
|
|
166
246
|
def main():
|
|
167
247
|
ap = argparse.ArgumentParser(description=__doc__)
|
|
168
248
|
ap.add_argument("--loki-dir", default=".loki")
|
|
169
249
|
ap.add_argument("--json", action="store_true")
|
|
250
|
+
ap.add_argument(
|
|
251
|
+
"--prompt-block",
|
|
252
|
+
action="store_true",
|
|
253
|
+
help="render the compact trend block for prompt injection (empty when no records)",
|
|
254
|
+
)
|
|
170
255
|
args = ap.parse_args()
|
|
171
256
|
|
|
257
|
+
if args.prompt_block:
|
|
258
|
+
block = prompt_block(args.loki_dir)
|
|
259
|
+
if block:
|
|
260
|
+
print(block)
|
|
261
|
+
return 0
|
|
262
|
+
|
|
172
263
|
s = attribute(args.loki_dir)
|
|
173
264
|
if args.json:
|
|
174
265
|
print(json.dumps(s, indent=2))
|
package/autonomy/loki
CHANGED
|
@@ -26798,6 +26798,9 @@ ig = r.get("_ignored_executable_fields") or []
|
|
|
26798
26798
|
if ig:
|
|
26799
26799
|
print(f" NOTE: ignored executable-looking fields (never run): {', '.join(ig)}")
|
|
26800
26800
|
print("Stored in .loki/agents/installed.json -- visible to 'loki agent list/info/run'.")
|
|
26801
|
+
print("It also joins the code-review reviewer pool: when its focus keywords match")
|
|
26802
|
+
print("a diff it is dispatched as a real reviewer (appended to the built-in battery,")
|
|
26803
|
+
print("never replacing one). See skills/quality-gates.md 'Adding Your Own Reviewer'.")
|
|
26801
26804
|
PYEOF
|
|
26802
26805
|
else
|
|
26803
26806
|
echo -e "${RED}Install failed:${NC} $result"
|
package/autonomy/run.sh
CHANGED
|
@@ -4154,6 +4154,135 @@ except Exception:
|
|
|
4154
4154
|
print('')" "$_fp_file" 2>/dev/null)"
|
|
4155
4155
|
fi
|
|
4156
4156
|
|
|
4157
|
+
# Where the time went, per stage. Same "written but never read" story as
|
|
4158
|
+
# first-preview above: emit_stage_complete (run.sh:2413) has appended a
|
|
4159
|
+
# stage_complete record -- stage, status, duration_s, iteration -- to
|
|
4160
|
+
# events.jsonl since v7.91.x, and NOTHING consumed it. A founder watching a
|
|
4161
|
+
# 322-word issue take 25+ minutes inside iteration 1 had no way to see which
|
|
4162
|
+
# step ate it, because the measurement existed and was never surfaced.
|
|
4163
|
+
#
|
|
4164
|
+
# Read-only aggregation over a file the run already wrote: no new subprocess
|
|
4165
|
+
# per stage, no new writer, one python3 pass at terminal time. We deliberately
|
|
4166
|
+
# render the AGENT remainder (wall clock minus summed stages) rather than
|
|
4167
|
+
# stages alone. The 9 emit_stage_complete sites are all post-iteration gates,
|
|
4168
|
+
# which sum to seconds; a table of only those would print "gates: 90s" on a
|
|
4169
|
+
# 25-minute run and still not answer the question. The remainder is the
|
|
4170
|
+
# provider/agent time, and it is usually the answer.
|
|
4171
|
+
#
|
|
4172
|
+
# Best-effort and honest about absence: no events file, no stage records, or
|
|
4173
|
+
# unparseable lines render NOTHING rather than a fabricated zero -- same
|
|
4174
|
+
# reasoning as first_preview_s. A wrong timing table is worse than silence.
|
|
4175
|
+
local stage_timing=""
|
|
4176
|
+
local _ev_file="$loki_dir/events.jsonl"
|
|
4177
|
+
if [ -f "$_ev_file" ]; then
|
|
4178
|
+
stage_timing="$(LOKI_RUN_START_EPOCH="${_LOKI_RUN_START_EPOCH:-}" python3 -c "
|
|
4179
|
+
import json, os, sys
|
|
4180
|
+
tot = {}
|
|
4181
|
+
order = []
|
|
4182
|
+
first = last = None
|
|
4183
|
+
|
|
4184
|
+
# events.jsonl is NEVER truncated between runs (no rm/rotate anywhere in the
|
|
4185
|
+
# tree), so a second 'loki start' in the same workspace would otherwise sum
|
|
4186
|
+
# stages from every previous run against THIS run's wall clock -- inflating
|
|
4187
|
+
# staged past wall and silently killing the total/remainder rows. Filter to
|
|
4188
|
+
# records at or after this run's start. The ISO timestamp is already on every
|
|
4189
|
+
# record, so this costs nothing extra.
|
|
4190
|
+
run_start_iso = None
|
|
4191
|
+
_es = (os.environ.get('LOKI_RUN_START_EPOCH') or '').strip()
|
|
4192
|
+
if _es:
|
|
4193
|
+
try:
|
|
4194
|
+
from datetime import datetime, timezone
|
|
4195
|
+
run_start_iso = datetime.fromtimestamp(float(_es), timezone.utc)
|
|
4196
|
+
except Exception:
|
|
4197
|
+
run_start_iso = None
|
|
4198
|
+
|
|
4199
|
+
def _in_run(ts):
|
|
4200
|
+
if run_start_iso is None or not isinstance(ts, str) or not ts:
|
|
4201
|
+
return True # no reliable boundary: keep (old behavior)
|
|
4202
|
+
try:
|
|
4203
|
+
from datetime import datetime
|
|
4204
|
+
t = datetime.fromisoformat(ts.replace('Z', '+00:00'))
|
|
4205
|
+
return t >= run_start_iso
|
|
4206
|
+
except Exception:
|
|
4207
|
+
return True
|
|
4208
|
+
try:
|
|
4209
|
+
with open(sys.argv[1], errors='replace') as fh:
|
|
4210
|
+
for line in fh:
|
|
4211
|
+
line = line.strip()
|
|
4212
|
+
if not line:
|
|
4213
|
+
continue
|
|
4214
|
+
try:
|
|
4215
|
+
rec = json.loads(line)
|
|
4216
|
+
except Exception:
|
|
4217
|
+
continue # malformed line: skip, never abort the summary
|
|
4218
|
+
if not isinstance(rec, dict):
|
|
4219
|
+
continue
|
|
4220
|
+
ts = rec.get('timestamp')
|
|
4221
|
+
if not _in_run(ts):
|
|
4222
|
+
continue # record belongs to an earlier run in this workspace
|
|
4223
|
+
if isinstance(ts, str) and ts:
|
|
4224
|
+
if first is None:
|
|
4225
|
+
first = ts
|
|
4226
|
+
last = ts
|
|
4227
|
+
if rec.get('type') != 'stage_complete':
|
|
4228
|
+
continue
|
|
4229
|
+
d = rec.get('data') or {}
|
|
4230
|
+
if not isinstance(d, dict):
|
|
4231
|
+
continue
|
|
4232
|
+
name = d.get('stage')
|
|
4233
|
+
dur = d.get('duration_s')
|
|
4234
|
+
if not name or not isinstance(dur, (int, float)) or dur < 0:
|
|
4235
|
+
continue
|
|
4236
|
+
if name not in tot:
|
|
4237
|
+
tot[name] = 0.0
|
|
4238
|
+
order.append(name)
|
|
4239
|
+
tot[name] += float(dur)
|
|
4240
|
+
except Exception:
|
|
4241
|
+
sys.exit(0)
|
|
4242
|
+
if not tot:
|
|
4243
|
+
sys.exit(0)
|
|
4244
|
+
|
|
4245
|
+
def human(s):
|
|
4246
|
+
s = int(round(s))
|
|
4247
|
+
return '%dm %02ds' % (s // 60, s % 60) if s >= 60 else '%ds' % s
|
|
4248
|
+
|
|
4249
|
+
# Wall clock: prefer the run-start epoch the runner exported; else derive from
|
|
4250
|
+
# the first/last event timestamps. Absent both, we print stages with no total
|
|
4251
|
+
# rather than inventing a denominator.
|
|
4252
|
+
wall = None
|
|
4253
|
+
env_start = os.environ.get('LOKI_RUN_START_EPOCH') or ''
|
|
4254
|
+
try:
|
|
4255
|
+
if env_start.strip():
|
|
4256
|
+
import time
|
|
4257
|
+
wall = time.time() - float(env_start)
|
|
4258
|
+
except Exception:
|
|
4259
|
+
wall = None
|
|
4260
|
+
if wall is None and first and last:
|
|
4261
|
+
try:
|
|
4262
|
+
from datetime import datetime
|
|
4263
|
+
f = datetime.fromisoformat(first.replace('Z', '+00:00'))
|
|
4264
|
+
l = datetime.fromisoformat(last.replace('Z', '+00:00'))
|
|
4265
|
+
wall = (l - f).total_seconds()
|
|
4266
|
+
except Exception:
|
|
4267
|
+
wall = None
|
|
4268
|
+
|
|
4269
|
+
staged = sum(tot.values())
|
|
4270
|
+
out = []
|
|
4271
|
+
for name in sorted(order, key=lambda n: -tot[n]):
|
|
4272
|
+
out.append(' %-22s %s' % (name.replace('_', ' '), human(tot[name])))
|
|
4273
|
+
if wall is not None and wall >= staged:
|
|
4274
|
+
rem = wall - staged
|
|
4275
|
+
# The bucket that answers 'where did the 25 minutes go'.
|
|
4276
|
+
# NOT labeled 'agent': the provider call is itself a bracketed stage above,
|
|
4277
|
+
# so this remainder is everything else (checklist verification, app runner,
|
|
4278
|
+
# playwright, council, memory). Calling it 'agent' would print two different
|
|
4279
|
+
# measurements under one name.
|
|
4280
|
+
out.append(' %-22s %s' % ('other (unaccounted)', human(rem)))
|
|
4281
|
+
out.append(' %-22s %s' % ('total', human(wall)))
|
|
4282
|
+
print('\n'.join(out))
|
|
4283
|
+
" "$_ev_file" 2>/dev/null)"
|
|
4284
|
+
fi
|
|
4285
|
+
|
|
4157
4286
|
# Branch + diff stats vs the run-start SHA (best-effort; non-git or empty
|
|
4158
4287
|
# baseline yields empty values, which we render as "unknown"/"0").
|
|
4159
4288
|
local start_sha="${_LOKI_RUN_START_SHA:-}"
|
|
@@ -4299,6 +4428,11 @@ except Exception:
|
|
|
4299
4428
|
fi
|
|
4300
4429
|
printf '%-14s %s\n' "Tasks:" "pending=$pending in_progress=$in_progress completed=$completed failed=$failed"
|
|
4301
4430
|
echo ""
|
|
4431
|
+
if [ -n "$stage_timing" ]; then
|
|
4432
|
+
echo "Where the time went:"
|
|
4433
|
+
echo "$stage_timing"
|
|
4434
|
+
echo ""
|
|
4435
|
+
fi
|
|
4302
4436
|
if [ -n "$evidence_inconclusive_line" ]; then
|
|
4303
4437
|
echo "$evidence_inconclusive_line"
|
|
4304
4438
|
echo ""
|
|
@@ -13821,6 +13955,42 @@ if types_file and os.path.exists(types_file):
|
|
|
13821
13955
|
except Exception:
|
|
13822
13956
|
pass # Fall back to hardcoded specialists
|
|
13823
13957
|
|
|
13958
|
+
# R10 extension seam: agents installed by the user via `loki agent install`
|
|
13959
|
+
# (.loki/agents/installed.json) join the reviewer pool. Built-ins above are
|
|
13960
|
+
# gated on a hardcoded FOCUS_KEYWORDS allowlist, which no user-chosen type can
|
|
13961
|
+
# ever match, so an installed agent was silently dropped and its persona never
|
|
13962
|
+
# reached a reviewer. Keywords come from the manifest's own `focus` list, which
|
|
13963
|
+
# hub_install.py already validates as <= 200-char strings.
|
|
13964
|
+
# Data only: hub_install.py never executes anything from a manifest.
|
|
13965
|
+
# Kept in a SEPARATE dict, never merged into SPECIALISTS: entering the built-in
|
|
13966
|
+
# pool would let a user agent win a `ranked[:want]` slot and DISPLACE a built-in
|
|
13967
|
+
# reviewer (observed displacing security-sentinel before this was split out),
|
|
13968
|
+
# and would also flip the all-zero defaults path.
|
|
13969
|
+
INSTALLED_SPECIALISTS = {}
|
|
13970
|
+
try:
|
|
13971
|
+
import importlib.util as _ilu
|
|
13972
|
+
_hub_path = os.path.join(os.path.dirname(os.path.abspath(types_file)), "hub_install.py")
|
|
13973
|
+
_spec = _ilu.spec_from_file_location("loki_hub_install", _hub_path)
|
|
13974
|
+
_hub = _ilu.module_from_spec(_spec)
|
|
13975
|
+
_spec.loader.exec_module(_hub)
|
|
13976
|
+
for _inst in _hub.installed_agent_list():
|
|
13977
|
+
_t = _inst.get("type", "")
|
|
13978
|
+
# Never let an installed agent shadow a built-in reviewer perspective.
|
|
13979
|
+
if not _t or _t in SPECIALISTS:
|
|
13980
|
+
continue
|
|
13981
|
+
_kw = [str(k).strip().lower() for k in _inst.get("focus", []) if str(k).strip()]
|
|
13982
|
+
if not _kw:
|
|
13983
|
+
continue # No keywords means it could never score; skip rather than always-on.
|
|
13984
|
+
INSTALLED_SPECIALISTS[_t] = {
|
|
13985
|
+
"keywords": _kw,
|
|
13986
|
+
"focus": _inst.get("capabilities", "") or _inst.get("name", _t),
|
|
13987
|
+
"checks": "Review from " + _inst.get("name", _t) + " perspective: " + ", ".join(_inst.get("focus", [])),
|
|
13988
|
+
"priority": 100 + len(INSTALLED_SPECIALISTS),
|
|
13989
|
+
"persona": _inst.get("persona", ""),
|
|
13990
|
+
}
|
|
13991
|
+
except Exception:
|
|
13992
|
+
pass # Corrupt or absent installed.json must never break code review.
|
|
13993
|
+
|
|
13824
13994
|
diff_path = os.environ.get("LOKI_REVIEW_DIFF_FILE", "")
|
|
13825
13995
|
files_path = os.environ.get("LOKI_REVIEW_FILES_FILE", "")
|
|
13826
13996
|
|
|
@@ -13880,6 +14050,23 @@ if all(s == 0 for s in scores.values()):
|
|
|
13880
14050
|
else:
|
|
13881
14051
|
selected = ranked[:want]
|
|
13882
14052
|
|
|
14053
|
+
# User-installed agents are APPENDED, never allowed to compete for the `want`
|
|
14054
|
+
# built-in slots -- same discipline as the dependency-analyst append below, so
|
|
14055
|
+
# installing an agent can only ADD scrutiny, never remove a built-in reviewer.
|
|
14056
|
+
# Only those whose keywords actually matched this diff fire, so an installed
|
|
14057
|
+
# a11y auditor stays silent on a backend-only change.
|
|
14058
|
+
# ponytail: hard cap of 2, no env var. Each appended agent costs one more LLM
|
|
14059
|
+
# reviewer call every iteration. Raise the constant if that ceiling bites.
|
|
14060
|
+
_MAX_INSTALLED_REVIEWERS = 2
|
|
14061
|
+
installed_selected = []
|
|
14062
|
+
for _n, _spec in INSTALLED_SPECIALISTS.items():
|
|
14063
|
+
scores[_n] = sum(1 for kw in _spec["keywords"] if kw in search_text)
|
|
14064
|
+
for _n in sorted(INSTALLED_SPECIALISTS, key=lambda n: (-scores[n], INSTALLED_SPECIALISTS[n]["priority"])):
|
|
14065
|
+
if len(installed_selected) >= _MAX_INSTALLED_REVIEWERS:
|
|
14066
|
+
break
|
|
14067
|
+
if scores[_n] > 0:
|
|
14068
|
+
installed_selected.append(_n)
|
|
14069
|
+
|
|
13883
14070
|
# A changed JavaScript manifest or lockfile always receives the specialist that
|
|
13884
14071
|
# understands the compact Git/npm metadata. Append rather than replace so a
|
|
13885
14072
|
# dependency change never removes another keyword-selected review perspective.
|
|
@@ -13929,6 +14116,13 @@ reviewers = mandatory + [
|
|
|
13929
14116
|
"checks": SPECIALISTS[name]["checks"]
|
|
13930
14117
|
}
|
|
13931
14118
|
for name in selected
|
|
14119
|
+
] + [
|
|
14120
|
+
{
|
|
14121
|
+
"name": name,
|
|
14122
|
+
"focus": INSTALLED_SPECIALISTS[name]["focus"],
|
|
14123
|
+
"checks": INSTALLED_SPECIALISTS[name]["checks"]
|
|
14124
|
+
}
|
|
14125
|
+
for name in installed_selected
|
|
13932
14126
|
]
|
|
13933
14127
|
if os.environ.get("LOKI_REVIEW_REQUIREMENTS_ONLY") == "1":
|
|
13934
14128
|
reviewers = [
|
|
@@ -13939,7 +14133,7 @@ if os.environ.get("LOKI_REVIEW_REQUIREMENTS_ONLY") == "1":
|
|
|
13939
14133
|
result = {
|
|
13940
14134
|
"reviewers": reviewers,
|
|
13941
14135
|
"scores": {n: scores[n] for n in scores},
|
|
13942
|
-
"pool_size": len(SPECIALISTS)
|
|
14136
|
+
"pool_size": len(SPECIALISTS) + len(installed_selected)
|
|
13943
14137
|
}
|
|
13944
14138
|
print(json.dumps(result))
|
|
13945
14139
|
SPECIALIST_SELECT
|
|
@@ -16412,6 +16606,9 @@ pricing = {
|
|
|
16412
16606
|
'sonnet': {'input': 3.00, 'output': 15.00},
|
|
16413
16607
|
'haiku': {'input': 1.00, 'output': 5.00},
|
|
16414
16608
|
'gpt-5.3-codex': {'input': 1.75, 'output': 14.00},
|
|
16609
|
+
'gpt-5.6-sol': {'input': 2.50, 'output': 20.00},
|
|
16610
|
+
'gpt-5.6-terra': {'input': 1.50, 'output': 12.00},
|
|
16611
|
+
'gpt-5.6-luna': {'input': 0.50, 'output': 4.00},
|
|
16415
16612
|
}
|
|
16416
16613
|
for f in glob.glob('${efficiency_dir}/*.json'):
|
|
16417
16614
|
try:
|
|
@@ -16424,7 +16621,20 @@ for f in glob.glob('${efficiency_dir}/*.json'):
|
|
|
16424
16621
|
p = pricing.get(model, pricing['sonnet'])
|
|
16425
16622
|
inp = d.get('input_tokens', 0)
|
|
16426
16623
|
out = d.get('output_tokens', 0)
|
|
16624
|
+
# Cache tiers. The writer has emitted these since v6.82.0 and they
|
|
16625
|
+
# DOMINATE real traffic: a measured iteration carried 797,496
|
|
16626
|
+
# cache-read tokens against 10,272 of plain input. Pricing them at
|
|
16627
|
+
# zero under-counted a real iteration 5.4x, so a breaker set to
|
|
16628
|
+
# stop a runaway let it run far past the cap. Published multipliers:
|
|
16629
|
+
# cache read 0.1x input, cache write 1.25x input.
|
|
16630
|
+
#
|
|
16631
|
+
# This mirrors the TS route's calculateCostFromRecords
|
|
16632
|
+
# (loki-ts/src/runner/budget.ts). Both routes must agree or the
|
|
16633
|
+
# same run reports two different spends.
|
|
16634
|
+
cr = d.get('cache_read_tokens', 0) or 0
|
|
16635
|
+
cw = d.get('cache_creation_tokens', 0) or 0
|
|
16427
16636
|
total += (inp / 1_000_000) * p['input'] + (out / 1_000_000) * p['output']
|
|
16637
|
+
total += (cr / 1_000_000) * (p['input'] * 0.1) + (cw / 1_000_000) * (p['input'] * 1.25)
|
|
16428
16638
|
except: pass
|
|
16429
16639
|
print(round(total, 4))
|
|
16430
16640
|
" 2>/dev/null || echo "0")
|
|
@@ -16726,8 +16936,64 @@ check_completion_promise() {
|
|
|
16726
16936
|
}
|
|
16727
16937
|
|
|
16728
16938
|
# Check if max iterations reached
|
|
16939
|
+
# EVIDENCE-AWARE ITERATION CAP.
|
|
16940
|
+
#
|
|
16941
|
+
# The cap used to be a bare counter: it consulted no gate, no council, and no
|
|
16942
|
+
# evidence. A run one step from finishing was cut off identically to a run
|
|
16943
|
+
# thrashing in circles, and both reported the same terminal.
|
|
16944
|
+
#
|
|
16945
|
+
# An iteration count is a PROXY for "is this converging". Where real evidence
|
|
16946
|
+
# exists, prefer the evidence. Two signals are already on disk at this point:
|
|
16947
|
+
#
|
|
16948
|
+
# 1. the model's own completion request (.loki/signals/COMPLETION_REQUESTED),
|
|
16949
|
+
# which the agent writes when it believes the work is done
|
|
16950
|
+
# 2. gate state (.loki/quality/gate-failures.txt), which says whether the
|
|
16951
|
+
# last verification pass actually found anything
|
|
16952
|
+
#
|
|
16953
|
+
# When the model says it is done AND no gate is failing, the run gets ONE extra
|
|
16954
|
+
# iteration to land it. That is the difference between a finished product and a
|
|
16955
|
+
# terminal failure at the buzzer.
|
|
16956
|
+
#
|
|
16957
|
+
# WHY THIS CANNOT LOOP FOREVER, which is the only thing that matters here:
|
|
16958
|
+
# the grace is granted at most once per run (a marker file, checked before it
|
|
16959
|
+
# is written), it requires POSITIVE evidence rather than the absence of a
|
|
16960
|
+
# signal, and it extends by exactly one iteration. A run that keeps claiming
|
|
16961
|
+
# done without finishing gets the cap, once, and then stops. Published
|
|
16962
|
+
# measurements put automated-verifier false-negative rates near 24%, so an
|
|
16963
|
+
# unbounded verifier-driven loop would burn real money on already-correct work.
|
|
16964
|
+
# This is deliberately a bounded nudge, not a verifier-driven terminal.
|
|
16965
|
+
#
|
|
16966
|
+
# LOKI_ITERATION_GRACE=0 restores the pure counter.
|
|
16967
|
+
_iteration_grace_available() {
|
|
16968
|
+
[ "${LOKI_ITERATION_GRACE:-1}" != "0" ] || return 1
|
|
16969
|
+
|
|
16970
|
+
local _loki_root="${TARGET_DIR:-.}/.loki"
|
|
16971
|
+
local _marker="$_loki_root/state/iteration-grace-used"
|
|
16972
|
+
[ -f "$_marker" ] && return 1
|
|
16973
|
+
|
|
16974
|
+
# POSITIVE evidence the model believes it is done. Absence is not evidence.
|
|
16975
|
+
[ -f "$_loki_root/signals/COMPLETION_REQUESTED" ] || return 1
|
|
16976
|
+
|
|
16977
|
+
# ...and nothing is currently failing. A non-empty gate-failures.txt means
|
|
16978
|
+
# the last verification pass found real problems, so a "done" claim on top
|
|
16979
|
+
# of it is exactly the case the cap should still stop.
|
|
16980
|
+
local _gf="$_loki_root/quality/gate-failures.txt"
|
|
16981
|
+
if [ -s "$_gf" ]; then
|
|
16982
|
+
return 1
|
|
16983
|
+
fi
|
|
16984
|
+
|
|
16985
|
+
mkdir -p "$_loki_root/state" 2>/dev/null || true
|
|
16986
|
+
printf 'granted at iteration %s\n' "${ITERATION_COUNT:-0}" > "$_marker" 2>/dev/null || true
|
|
16987
|
+
return 0
|
|
16988
|
+
}
|
|
16989
|
+
|
|
16729
16990
|
check_max_iterations() {
|
|
16730
16991
|
if [ $ITERATION_COUNT -ge $MAX_ITERATIONS ]; then
|
|
16992
|
+
if _iteration_grace_available; then
|
|
16993
|
+
MAX_ITERATIONS=$((MAX_ITERATIONS + 1))
|
|
16994
|
+
log_info "Iteration cap reached, but the agent reports done with no failing gate -- granting ONE final iteration to land it (once per run; LOKI_ITERATION_GRACE=0 to disable)."
|
|
16995
|
+
return 1
|
|
16996
|
+
fi
|
|
16731
16997
|
log_warn "Max iterations ($MAX_ITERATIONS) reached. Stopping."
|
|
16732
16998
|
return 0
|
|
16733
16999
|
fi
|
|
@@ -18827,6 +19093,31 @@ if d.get('blocked'):
|
|
|
18827
19093
|
memory_context_section="CONTEXT: $context_injection"
|
|
18828
19094
|
fi
|
|
18829
19095
|
|
|
19096
|
+
# Efficiency trend injection -- close the eval feedback loop.
|
|
19097
|
+
# .loki/metrics/efficiency/iteration-N.json has been written every iteration
|
|
19098
|
+
# for the engine's entire life and read back only by a stop-only budget
|
|
19099
|
+
# breaker and an offline report, never by the agent producing the cost.
|
|
19100
|
+
#
|
|
19101
|
+
# SINGLE RENDERER: the text comes from iteration_attribution.py --prompt-block,
|
|
19102
|
+
# the exact same entry point the Bun route calls (build_prompt.ts
|
|
19103
|
+
# buildEfficiencyTrend), so the two routes are byte-identical by construction
|
|
19104
|
+
# rather than by two renderers kept in sync forever.
|
|
19105
|
+
#
|
|
19106
|
+
# Emits "" on absent/empty metrics, so an unmeasured run adds NOTHING.
|
|
19107
|
+
# Opt out with LOKI_EVAL_TREND=0.
|
|
19108
|
+
# Accepts BOTH "0" and "false" (case-insensitive): this repo uses both
|
|
19109
|
+
# toggle conventions, and honouring only one makes the other a silent no-op.
|
|
19110
|
+
# Byte-mirrored in build_prompt.ts buildEfficiencyTrend().
|
|
19111
|
+
local _eval_trend_optout
|
|
19112
|
+
_eval_trend_optout="$(printf '%s' "${LOKI_EVAL_TREND:-1}" | tr '[:upper:]' '[:lower:]')"
|
|
19113
|
+
local efficiency_trend=""
|
|
19114
|
+
if [ "$_eval_trend_optout" != "0" ] && [ "$_eval_trend_optout" != "false" ] \
|
|
19115
|
+
&& [ -r "${SCRIPT_DIR}/lib/iteration_attribution.py" ] \
|
|
19116
|
+
&& [ -d ".loki" ]; then
|
|
19117
|
+
efficiency_trend="$(python3 "${SCRIPT_DIR}/lib/iteration_attribution.py" \
|
|
19118
|
+
--loki-dir ".loki" --prompt-block 2>/dev/null || true)"
|
|
19119
|
+
fi
|
|
19120
|
+
|
|
18830
19121
|
# PRD Checklist status injection (v5.44.0)
|
|
18831
19122
|
local checklist_status=""
|
|
18832
19123
|
if [ -n "$prd" ] && [ ! -f ".loki/checklist/checklist.json" ]; then
|
|
@@ -19194,6 +19485,10 @@ except Exception:
|
|
|
19194
19485
|
[ -n "$app_runner_info" ] && printf '%s\n' "$app_runner_info"
|
|
19195
19486
|
[ -n "$playwright_info" ] && printf '%s\n' "$playwright_info"
|
|
19196
19487
|
[ -n "$memory_context_section" ] && printf '%s\n' "$memory_context_section"
|
|
19488
|
+
# Volatile per-iteration data: belongs below [CACHE_BREAKPOINT], never in the
|
|
19489
|
+
# cache-stable prefix. Same ordinal position as the Bun route (after the
|
|
19490
|
+
# context section, before the completion instruction).
|
|
19491
|
+
[ -n "$efficiency_trend" ] && printf '%s\n' "$efficiency_trend"
|
|
19197
19492
|
printf '%s\n' "$completion_instruction"
|
|
19198
19493
|
printf '</dynamic_context>\n'
|
|
19199
19494
|
}
|
|
@@ -21857,6 +22152,11 @@ if __name__ == "__main__":
|
|
|
21857
22152
|
|
|
21858
22153
|
log_info "${PROVIDER_DISPLAY_NAME:-Claude} exited with code $exit_code after ${duration}s"
|
|
21859
22154
|
|
|
22155
|
+
# The provider call is the largest single bucket in any iteration and was
|
|
22156
|
+
# the one the founder could not see. start_time already exists, so this
|
|
22157
|
+
# costs zero extra subprocesses -- we pass the existing epoch through.
|
|
22158
|
+
emit_stage_complete "agent" "$([ "$exit_code" -eq 0 ] 2>/dev/null && echo pass || echo fail)" "$start_time"
|
|
22159
|
+
|
|
21860
22160
|
# v7.5.12 Gap A: Distinguish signal-induced exits (130/143/137) from clean failure.
|
|
21861
22161
|
# Without this, post-iteration logic may quietly proceed past a SIGINT/SIGTERM,
|
|
21862
22162
|
# leaving stale state and confusing the next iteration. Any non-zero exit is a
|
|
@@ -22403,8 +22703,14 @@ if __name__ == "__main__":
|
|
|
22403
22703
|
# Auto-generate docs (default-on) BEFORE the staleness check and the
|
|
22404
22704
|
# gate, so neither nags the user to run 'loki docs generate' by hand.
|
|
22405
22705
|
# Opt out with LOKI_AUTO_DOCS=false.
|
|
22706
|
+
# Bracketed because this is the single biggest non-provider step in
|
|
22707
|
+
# the loop (the doc suite has cost ~25min on a real build) and it was
|
|
22708
|
+
# the one nobody could see. Reuses the existing helper: one date call,
|
|
22709
|
+
# no new subprocess per stage.
|
|
22406
22710
|
if [ "$ITERATION_COUNT" -gt 0 ] && ! loki_is_supervised_simple_web; then
|
|
22407
|
-
|
|
22711
|
+
local _docgen_t0=$(date +%s 2>/dev/null); local _docgen_ok=pass
|
|
22712
|
+
auto_generate_docs_if_needed || _docgen_ok=fail
|
|
22713
|
+
emit_stage_complete "doc_generation" "$_docgen_ok" "$_docgen_t0"
|
|
22408
22714
|
fi
|
|
22409
22715
|
# Documentation staleness check (v6.75.0)
|
|
22410
22716
|
if [ "$ITERATION_COUNT" -gt 0 ] && ! loki_is_supervised_simple_web; then
|