loki-mode 8.9.0 → 8.10.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 +275 -5
- 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.10.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.10.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
8.
|
|
1
|
+
8.10.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")
|
|
@@ -18827,6 +19037,31 @@ if d.get('blocked'):
|
|
|
18827
19037
|
memory_context_section="CONTEXT: $context_injection"
|
|
18828
19038
|
fi
|
|
18829
19039
|
|
|
19040
|
+
# Efficiency trend injection -- close the eval feedback loop.
|
|
19041
|
+
# .loki/metrics/efficiency/iteration-N.json has been written every iteration
|
|
19042
|
+
# for the engine's entire life and read back only by a stop-only budget
|
|
19043
|
+
# breaker and an offline report, never by the agent producing the cost.
|
|
19044
|
+
#
|
|
19045
|
+
# SINGLE RENDERER: the text comes from iteration_attribution.py --prompt-block,
|
|
19046
|
+
# the exact same entry point the Bun route calls (build_prompt.ts
|
|
19047
|
+
# buildEfficiencyTrend), so the two routes are byte-identical by construction
|
|
19048
|
+
# rather than by two renderers kept in sync forever.
|
|
19049
|
+
#
|
|
19050
|
+
# Emits "" on absent/empty metrics, so an unmeasured run adds NOTHING.
|
|
19051
|
+
# Opt out with LOKI_EVAL_TREND=0.
|
|
19052
|
+
# Accepts BOTH "0" and "false" (case-insensitive): this repo uses both
|
|
19053
|
+
# toggle conventions, and honouring only one makes the other a silent no-op.
|
|
19054
|
+
# Byte-mirrored in build_prompt.ts buildEfficiencyTrend().
|
|
19055
|
+
local _eval_trend_optout
|
|
19056
|
+
_eval_trend_optout="$(printf '%s' "${LOKI_EVAL_TREND:-1}" | tr '[:upper:]' '[:lower:]')"
|
|
19057
|
+
local efficiency_trend=""
|
|
19058
|
+
if [ "$_eval_trend_optout" != "0" ] && [ "$_eval_trend_optout" != "false" ] \
|
|
19059
|
+
&& [ -r "${SCRIPT_DIR}/lib/iteration_attribution.py" ] \
|
|
19060
|
+
&& [ -d ".loki" ]; then
|
|
19061
|
+
efficiency_trend="$(python3 "${SCRIPT_DIR}/lib/iteration_attribution.py" \
|
|
19062
|
+
--loki-dir ".loki" --prompt-block 2>/dev/null || true)"
|
|
19063
|
+
fi
|
|
19064
|
+
|
|
18830
19065
|
# PRD Checklist status injection (v5.44.0)
|
|
18831
19066
|
local checklist_status=""
|
|
18832
19067
|
if [ -n "$prd" ] && [ ! -f ".loki/checklist/checklist.json" ]; then
|
|
@@ -19194,6 +19429,10 @@ except Exception:
|
|
|
19194
19429
|
[ -n "$app_runner_info" ] && printf '%s\n' "$app_runner_info"
|
|
19195
19430
|
[ -n "$playwright_info" ] && printf '%s\n' "$playwright_info"
|
|
19196
19431
|
[ -n "$memory_context_section" ] && printf '%s\n' "$memory_context_section"
|
|
19432
|
+
# Volatile per-iteration data: belongs below [CACHE_BREAKPOINT], never in the
|
|
19433
|
+
# cache-stable prefix. Same ordinal position as the Bun route (after the
|
|
19434
|
+
# context section, before the completion instruction).
|
|
19435
|
+
[ -n "$efficiency_trend" ] && printf '%s\n' "$efficiency_trend"
|
|
19197
19436
|
printf '%s\n' "$completion_instruction"
|
|
19198
19437
|
printf '</dynamic_context>\n'
|
|
19199
19438
|
}
|
|
@@ -21857,6 +22096,11 @@ if __name__ == "__main__":
|
|
|
21857
22096
|
|
|
21858
22097
|
log_info "${PROVIDER_DISPLAY_NAME:-Claude} exited with code $exit_code after ${duration}s"
|
|
21859
22098
|
|
|
22099
|
+
# The provider call is the largest single bucket in any iteration and was
|
|
22100
|
+
# the one the founder could not see. start_time already exists, so this
|
|
22101
|
+
# costs zero extra subprocesses -- we pass the existing epoch through.
|
|
22102
|
+
emit_stage_complete "agent" "$([ "$exit_code" -eq 0 ] 2>/dev/null && echo pass || echo fail)" "$start_time"
|
|
22103
|
+
|
|
21860
22104
|
# v7.5.12 Gap A: Distinguish signal-induced exits (130/143/137) from clean failure.
|
|
21861
22105
|
# Without this, post-iteration logic may quietly proceed past a SIGINT/SIGTERM,
|
|
21862
22106
|
# leaving stale state and confusing the next iteration. Any non-zero exit is a
|
|
@@ -22403,8 +22647,14 @@ if __name__ == "__main__":
|
|
|
22403
22647
|
# Auto-generate docs (default-on) BEFORE the staleness check and the
|
|
22404
22648
|
# gate, so neither nags the user to run 'loki docs generate' by hand.
|
|
22405
22649
|
# Opt out with LOKI_AUTO_DOCS=false.
|
|
22650
|
+
# Bracketed because this is the single biggest non-provider step in
|
|
22651
|
+
# the loop (the doc suite has cost ~25min on a real build) and it was
|
|
22652
|
+
# the one nobody could see. Reuses the existing helper: one date call,
|
|
22653
|
+
# no new subprocess per stage.
|
|
22406
22654
|
if [ "$ITERATION_COUNT" -gt 0 ] && ! loki_is_supervised_simple_web; then
|
|
22407
|
-
|
|
22655
|
+
local _docgen_t0=$(date +%s 2>/dev/null); local _docgen_ok=pass
|
|
22656
|
+
auto_generate_docs_if_needed || _docgen_ok=fail
|
|
22657
|
+
emit_stage_complete "doc_generation" "$_docgen_ok" "$_docgen_t0"
|
|
22408
22658
|
fi
|
|
22409
22659
|
# Documentation staleness check (v6.75.0)
|
|
22410
22660
|
if [ "$ITERATION_COUNT" -gt 0 ] && ! loki_is_supervised_simple_web; then
|
|
@@ -22580,9 +22830,23 @@ if __name__ == "__main__":
|
|
|
22580
22830
|
run_memory_consolidation
|
|
22581
22831
|
# No on_run_complete: a force-stop must never open a "done" PR.
|
|
22582
22832
|
emit_completion_summary force_stopped
|
|
22583
|
-
|
|
22833
|
+
# Exit 20, not 0. Every other signal here already says this
|
|
22834
|
+
# run is NOT verified-complete -- the header, the warning,
|
|
22835
|
+
# the refusal to open a PR -- but the exit code said the
|
|
22836
|
+
# opposite, and the exit code is the only one a CI job, a
|
|
22837
|
+
# Kubernetes Job, or a shell `&&` actually reads. A
|
|
22838
|
+
# stagnation force-stop was therefore indistinguishable
|
|
22839
|
+
# from success to every automated caller.
|
|
22840
|
+
#
|
|
22841
|
+
# 20 is the established "deterministic terminal failure"
|
|
22842
|
+
# code, already used by max_iterations_reached
|
|
22843
|
+
# (run.sh:20796, :20877) for the same class of outcome:
|
|
22844
|
+
# the run stopped without verifying the work. Retrying is
|
|
22845
|
+
# pointless; a human needs to look. Two terminals with the
|
|
22846
|
+
# same meaning must not report opposite exit codes.
|
|
22847
|
+
save_state $retry "force_stopped" 20
|
|
22584
22848
|
rm -f "$iter_output" 2>/dev/null
|
|
22585
|
-
return
|
|
22849
|
+
return 20
|
|
22586
22850
|
fi
|
|
22587
22851
|
echo ""
|
|
22588
22852
|
if loki_is_supervised_simple_web; then
|
|
@@ -24610,8 +24874,14 @@ except Exception:
|
|
|
24610
24874
|
_final_state_file="$(_loki_state_file)"
|
|
24611
24875
|
_final_status=$(LOKI_STATE_FILE="$_final_state_file" python3 -c "import json, os; print(json.load(open(os.environ['LOKI_STATE_FILE'])).get('status','unknown'))" 2>/dev/null || echo "unknown")
|
|
24612
24876
|
case "$_final_status" in
|
|
24613
|
-
council_approved|council_force_approved|deterministic_gates_passed|completion_promise_fulfilled|
|
|
24877
|
+
council_approved|council_force_approved|deterministic_gates_passed|completion_promise_fulfilled|paused|interrupted|stopped)
|
|
24614
24878
|
result=0 ;;
|
|
24879
|
+
# force_stopped belongs HERE too, for the same reason. A council
|
|
24880
|
+
# force-stop (stagnation, or a flood of done-signals) means the run
|
|
24881
|
+
# gave up WITHOUT verifying the work -- the code already says so in
|
|
24882
|
+
# its header, its warning, and its refusal to open a PR. Reporting
|
|
24883
|
+
# it as a clean stop made it indistinguishable from success to the
|
|
24884
|
+
# only consumer that matters to automation: the exit code.
|
|
24615
24885
|
# budget_exceeded belongs HERE, not with the human-controlled stops.
|
|
24616
24886
|
# It sat in the result=0 arm on the rationale that "a human will
|
|
24617
24887
|
# resume", which is true of `paused` (a human pressed pause) and
|