loki-mode 11.12.1 → 11.17.1
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/README.md +2 -2
- package/SKILL.md +3 -3
- package/VERSION +1 -1
- package/autonomy/grill.sh +3 -1
- package/autonomy/hooks/validate-bash.sh +51 -13
- package/autonomy/lib/backlog.py +18 -3
- package/autonomy/lib/chat.sh +117 -7
- package/autonomy/lib/claude-flags.sh +20 -0
- package/autonomy/lib/claude_permissions.py +40 -0
- package/autonomy/lib/done-recognition.sh +8 -4
- package/autonomy/lib/engine10-push.sh +56 -7
- package/autonomy/lib/prd-enrich.sh +4 -2
- package/autonomy/lib/proof-generator.py +21 -0
- package/autonomy/lib/proof-verify.py +109 -1
- package/autonomy/lib/trust_trajectory.py +139 -0
- package/autonomy/lib/voter-agents.sh +3 -1
- package/autonomy/lib/workspace.py +2 -3
- package/autonomy/loki +427 -74
- package/autonomy/quickstart.sh +21 -2
- package/autonomy/run.sh +55 -21
- package/autonomy/trigger-server.py +461 -32
- package/bin/loki +4 -1
- package/completions/_loki +1 -1
- package/completions/loki.bash +10 -2
- package/dashboard/__init__.py +1 -1
- package/dashboard/api_start.py +2 -2
- package/dashboard/build_supervisor.py +2 -0
- package/docs/CLI-REFERENCE.md +6 -4
- package/docs/DEPLOYMENT.md +12 -5
- package/docs/EVALUATING.md +13 -0
- package/docs/INSTALLATION.md +2 -2
- package/docs/SIGNED-RECEIPTS.md +7 -0
- package/docs/WORKSPACES.md +2 -2
- package/docs/cost-controls.md +26 -0
- package/docs/merge-queue.md +19 -0
- package/docs/sentry-intake.md +4 -0
- package/docs/slack.md +2 -1
- package/docs/trackers.md +12 -1
- package/docs/v10/BOARD.md +148 -123
- package/docs/v10/CLI-MODERN.md +5 -5
- package/docs/v10/DECISIONS.md +2 -0
- package/docs/v10/ENGINE.md +2 -0
- package/docs/v10/FAILURE-CLASSES.md +18 -0
- package/docs/v10/MODERNIZE.md +32 -1
- package/docs/v10/PROGRESS.md +7 -0
- package/loki-ts/data/finding-schema.json +2 -1
- package/loki-ts/dist/loki.js +1222 -1128
- package/magic/core/debate.py +27 -1
- package/mcp/__init__.py +1 -1
- package/package.json +2 -2
- package/packages/control-plane/dist/server.js +5895 -5292
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/claude.sh +6 -3
- package/providers/model_catalog.json +6 -0
- package/schemas/loki-yaml.schema.json +5 -0
- package/src/policies/claude_permissions.js +114 -0
- package/web-app/dist/assets/{AdminPage-DuT74pf5.js → AdminPage-OVzN6eLm.js} +1 -1
- package/web-app/dist/assets/{Avatar-BtgidR5a.js → Avatar-hCCrxrCZ.js} +1 -1
- package/web-app/dist/assets/{Badge-D2nt8G_I.js → Badge-CDcpkhbJ.js} +1 -1
- package/web-app/dist/assets/{Button-BKl2jOyb.js → Button-8mwWlhEu.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-DaWr0EUX.js → CockpitPage-D8cf3u22.js} +1 -1
- package/web-app/dist/assets/{ComparePage-aIiNhxLr.js → ComparePage-CIgkbDRY.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-C1Q7rV23.js → ErrorBoundary-99kJH9g2.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-C-C0-tOx.js → EvidenceReceiptPanel-C9fj32eS.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-l3IaXDag.js → GitHubIssuesPanel-De43YQ_C.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-DweLBgr5.js → GitHubPRsPanel-7g7ZTnkx.js} +1 -1
- package/web-app/dist/assets/{HomePage-B_KK7AvY.js → HomePage-CcNA24ny.js} +2 -2
- package/web-app/dist/assets/{LoginPage-CKS-KLdH.js → LoginPage-BYjlb-IK.js} +1 -1
- package/web-app/dist/assets/{MagicPage-Cl6CEp8J.js → MagicPage-C0Dwt9Na.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-n9CPUaX0.js → MetricsPage-jDGmdcgg.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-D85eWn70.js → NotFoundPage-C_ZO8tLE.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-CDN7LpSo.js → ProjectPage-B5wnurfQ.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-D4SDmU4-.js → ProjectsPage--EyKt5-D.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-Dby6GnKm.js → SettingsPage-cB0lnfti.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-CgHxeqoU.js → ShowcasePage-Dl16FDkK.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-BaewGHlc.js → SystemSettingsPage-BmAXd4V2.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-905MJyke.js → TeamsPage-BFkWVOZe.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-DMrUHcvF.js → TemplatesPage-Bf2kmEUM.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-BsbEbBYX.js → TerminalOutput-BItv9Wa6.js} +1 -1
- package/web-app/dist/assets/{activity-BuaXhBsC.js → activity-Df2Awg6I.js} +1 -1
- package/web-app/dist/assets/{bell-BfEQ71OG.js → bell-D-1ztTsJ.js} +1 -1
- package/web-app/dist/assets/{bot-DOGeYewg.js → bot-A4qgZsKi.js} +1 -1
- package/web-app/dist/assets/{check-zbjPpb3E.js → check-BPfFpF0c.js} +1 -1
- package/web-app/dist/assets/{chevron-left-DWTOgfFt.js → chevron-left-CfSJvmDi.js} +1 -1
- package/web-app/dist/assets/{circle-alert-BEPKfRzR.js → circle-alert-wx2d1dzb.js} +1 -1
- package/web-app/dist/assets/{clock-C_fX3OHo.js → clock-pQMai6l-.js} +1 -1
- package/web-app/dist/assets/{cloud-DUXHygXQ.js → cloud-DWL470r6.js} +1 -1
- package/web-app/dist/assets/{code-xml-BEVl_9Hw.js → code-xml-BaEQpQLZ.js} +1 -1
- package/web-app/dist/assets/{database-_a22uh7l.js → database-h-N7WkQN.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-l2en7xfg.js → dollar-sign-B5oYiD27.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-DItKl6H4.js → file-code-corner-fbTtTy0o.js} +1 -1
- package/web-app/dist/assets/{file-plus-BSk7qFMA.js → file-plus-PaF6HMo8.js} +1 -1
- package/web-app/dist/assets/{globe-2TveFE5l.js → globe-Ct-GlWID.js} +1 -1
- package/web-app/dist/assets/{hammer-BU6OJlQj.js → hammer-C1oZFX51.js} +1 -1
- package/web-app/dist/assets/{index-DGnZXUYk.js → index-C2egrRhz.js} +3 -3
- package/web-app/dist/assets/{layers-DKw7MNuq.js → layers-Dt5ZK4v2.js} +1 -1
- package/web-app/dist/assets/{loader-circle-CoYfykzD.js → loader-circle-DrpMziic.js} +1 -1
- package/web-app/dist/assets/{lock-D9JzrXDL.js → lock-DOez8MmK.js} +1 -1
- package/web-app/dist/assets/{package-BZtqTIqH.js → package-ClGIkerr.js} +1 -1
- package/web-app/dist/assets/{plus-BGJfo8bV.js → plus-C004i9ua.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-F6lowBgR.js → refresh-cw-Ddfk-7Sy.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-DHOK8dM4.js → rotate-ccw-Bu1KgDjS.js} +1 -1
- package/web-app/dist/assets/{scroll-text-CKcROoMV.js → scroll-text-3q1lW5pm.js} +1 -1
- package/web-app/dist/assets/{server-DbG8MDv6.js → server-pd6x4HW1.js} +1 -1
- package/web-app/dist/assets/{shield-alert-RNKNXdJT.js → shield-alert-B9jGJepT.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-Yxp-UBxS.js → thumbs-up-Cdcc9nQf.js} +1 -1
- package/web-app/dist/assets/{trash-2-DY-eba1n.js → trash-2-foSojXHK.js} +1 -1
- package/web-app/dist/assets/{trending-down-CARiDB3t.js → trending-down-CTTiqlJ1.js} +1 -1
- package/web-app/dist/assets/{trending-up-brQqWbA3.js → trending-up-B_MgmRAx.js} +1 -1
- package/web-app/dist/assets/{upload-D6wRjKhz.js → upload-g6BfALzG.js} +1 -1
- package/web-app/dist/assets/{usePolling-BMrqNbAI.js → usePolling-DrHwvzuh.js} +1 -1
- package/web-app/dist/assets/{user-CK5-EXIj.js → user-COp75m7l.js} +1 -1
- package/web-app/dist/index.html +1 -1
- package/autonomy/lib/doctor-fix.py +0 -279
|
@@ -1441,6 +1441,25 @@ def _collect_journey(loki_dir):
|
|
|
1441
1441
|
n = iv.get("count")
|
|
1442
1442
|
if isinstance(n, int) and n >= 0:
|
|
1443
1443
|
out["interventions"] = n
|
|
1444
|
+
# by_type / waited_s exist only for counters written by the current
|
|
1445
|
+
# handle_pause. A count-only legacy file yields no detail (unmeasured,
|
|
1446
|
+
# never a fabricated zero). waited_s is wall-clock blocked seconds, an
|
|
1447
|
+
# upper bound on human attention, not active time.
|
|
1448
|
+
bt = iv.get("by_type")
|
|
1449
|
+
ws = iv.get("waited_s")
|
|
1450
|
+
if isinstance(bt, dict) and isinstance(ws, int) and not isinstance(ws, bool) and ws >= 0:
|
|
1451
|
+
out["interventions_detail"] = {
|
|
1452
|
+
"by_type": {
|
|
1453
|
+
k: (bt[k] if isinstance(bt.get(k), int) and not isinstance(bt.get(k), bool) and bt[k] >= 0 else 0)
|
|
1454
|
+
for k in ("steer", "input", "authorization", "repair")
|
|
1455
|
+
},
|
|
1456
|
+
"waited_s": ws,
|
|
1457
|
+
}
|
|
1458
|
+
# active_s (measured active human seconds) rides along only when
|
|
1459
|
+
# recorded; absent stays absent, never a fabricated 0.
|
|
1460
|
+
act = iv.get("active_s")
|
|
1461
|
+
if isinstance(act, int) and not isinstance(act, bool) and act >= 0:
|
|
1462
|
+
out["interventions_detail"]["active_s"] = act
|
|
1444
1463
|
|
|
1445
1464
|
# PR state/URL, written by the consent-gated PR step. "prepared" means a PR
|
|
1446
1465
|
# body exists locally and GitHub was NOT mutated.
|
|
@@ -1847,6 +1866,8 @@ def _build_proof(args, loki_dir, target_dir, repo_root):
|
|
|
1847
1866
|
# "unavailable" rather than showing a fabricated zero.
|
|
1848
1867
|
if isinstance(journey, dict) and isinstance(journey.get("interventions"), int):
|
|
1849
1868
|
proof["interventions"] = journey["interventions"]
|
|
1869
|
+
if isinstance(journey, dict) and isinstance(journey.get("interventions_detail"), dict):
|
|
1870
|
+
proof["interventions_detail"] = journey["interventions_detail"]
|
|
1850
1871
|
|
|
1851
1872
|
return proof, run_id
|
|
1852
1873
|
|
|
@@ -1173,10 +1173,34 @@ def git_head(repo):
|
|
|
1173
1173
|
return out.strip() if out and out.strip() else ""
|
|
1174
1174
|
|
|
1175
1175
|
|
|
1176
|
+
def _sparse_included(repo, paths):
|
|
1177
|
+
"""Set of `paths` git's own sparse matcher includes; None when git cannot answer.
|
|
1178
|
+
|
|
1179
|
+
One `git sparse-checkout check-rules` call for the whole batch: it echoes
|
|
1180
|
+
the paths the patterns include, for cone, non-cone and negation alike.
|
|
1181
|
+
Known limit: a .git directory writable by the subject can drop paths from
|
|
1182
|
+
info/sparse-checkout. That hole predates this check (core.sparseCheckout=true
|
|
1183
|
+
alone was enough).
|
|
1184
|
+
"""
|
|
1185
|
+
enc = [p.encode("utf-8", "surrogateescape") for p in paths]
|
|
1186
|
+
try:
|
|
1187
|
+
r = subprocess.run(
|
|
1188
|
+
["git", "-C", repo or ".", "sparse-checkout", "check-rules", "-z"],
|
|
1189
|
+
input=b"".join(e + b"\0" for e in enc),
|
|
1190
|
+
capture_output=True, timeout=60, env=_scoped_env())
|
|
1191
|
+
except (OSError, subprocess.SubprocessError, ValueError):
|
|
1192
|
+
return None
|
|
1193
|
+
if r.returncode != 0:
|
|
1194
|
+
return None
|
|
1195
|
+
out = set(r.stdout.split(b"\0"))
|
|
1196
|
+
return set(p for p, e in zip(paths, enc) if e in out)
|
|
1197
|
+
|
|
1198
|
+
|
|
1176
1199
|
def git_dirty(repo):
|
|
1177
1200
|
"""True/False, or None when git cannot answer. .loki/ is excluded: the
|
|
1178
1201
|
verifier writes its own artifacts there."""
|
|
1179
|
-
out = _vr_git_args(repo, ["status", "--porcelain"
|
|
1202
|
+
out = _vr_git_args(repo, ["status", "--porcelain", "--untracked-files=all",
|
|
1203
|
+
"--no-renames", "--ignore-submodules=none"])
|
|
1180
1204
|
if out is None:
|
|
1181
1205
|
return None
|
|
1182
1206
|
for line in out.splitlines():
|
|
@@ -1185,6 +1209,90 @@ def git_dirty(repo):
|
|
|
1185
1209
|
path = path.strip('"')
|
|
1186
1210
|
if not (path.startswith(".loki/") or path == ".loki"):
|
|
1187
1211
|
return True
|
|
1212
|
+
# A skip-worktree (S/s) or assume-unchanged (lowercase tag) flag hides edits
|
|
1213
|
+
# from status. A flagged path is dirty only when its content differs from
|
|
1214
|
+
# the index; a skip-worktree path absent on disk is sparse-excluded (clean).
|
|
1215
|
+
flagged = _vr_git_args(repo, ["ls-files", "-v", "-z"])
|
|
1216
|
+
if flagged is None:
|
|
1217
|
+
return None
|
|
1218
|
+
cand = []
|
|
1219
|
+
for ent in flagged.split("\0"):
|
|
1220
|
+
tag, path = ent[:1], ent[2:]
|
|
1221
|
+
if not path or path.startswith(".loki/"):
|
|
1222
|
+
continue
|
|
1223
|
+
if tag in ("S", "s") or tag.islower():
|
|
1224
|
+
cand.append((tag in ("S", "s"), path))
|
|
1225
|
+
if not cand:
|
|
1226
|
+
return False
|
|
1227
|
+
staged = _vr_git_args(repo, ["ls-files", "-s", "-z"])
|
|
1228
|
+
if staged is None:
|
|
1229
|
+
return None
|
|
1230
|
+
blobs = {}
|
|
1231
|
+
for ent in staged.split("\0"):
|
|
1232
|
+
meta, _, p = ent.partition("\t")
|
|
1233
|
+
f = meta.split()
|
|
1234
|
+
if len(f) == 3 and f[2] == "0":
|
|
1235
|
+
blobs[p] = (f[0], f[1])
|
|
1236
|
+
sparse = (_vr_git_args(repo, ["config", "--bool", "core.sparseCheckout"]) or "").strip() == "true"
|
|
1237
|
+
root = repo or "."
|
|
1238
|
+
bad = None
|
|
1239
|
+
todo = []
|
|
1240
|
+
sparse_missing = []
|
|
1241
|
+
for skip, path in cand:
|
|
1242
|
+
full = os.path.join(root, path)
|
|
1243
|
+
mode, want = blobs.get(path, ("", ""))
|
|
1244
|
+
if not os.path.lexists(full):
|
|
1245
|
+
if skip and sparse:
|
|
1246
|
+
# Decided after the loop in one batched git call: clean only when
|
|
1247
|
+
# git's sparse patterns exclude the path. An in-cone path flagged
|
|
1248
|
+
# by hand and deleted is a real deletion; fail closed otherwise.
|
|
1249
|
+
sparse_missing.append(path)
|
|
1250
|
+
continue
|
|
1251
|
+
bad = (skip, path)
|
|
1252
|
+
break
|
|
1253
|
+
if mode == "160000":
|
|
1254
|
+
continue
|
|
1255
|
+
if os.path.islink(full):
|
|
1256
|
+
data = os.readlink(full).encode("utf-8", "surrogateescape")
|
|
1257
|
+
if hashlib.sha1(b"blob %d\0" % len(data) + data).hexdigest() != want:
|
|
1258
|
+
bad = (skip, path)
|
|
1259
|
+
break
|
|
1260
|
+
continue
|
|
1261
|
+
todo.append((skip, path))
|
|
1262
|
+
if sparse_missing and not bad:
|
|
1263
|
+
included = _sparse_included(repo, sparse_missing)
|
|
1264
|
+
for path in sparse_missing:
|
|
1265
|
+
if included is None or path in included:
|
|
1266
|
+
bad = (True, path)
|
|
1267
|
+
break
|
|
1268
|
+
for i in range(0, len(todo), 200):
|
|
1269
|
+
if bad:
|
|
1270
|
+
break
|
|
1271
|
+
chunk = todo[i:i + 200]
|
|
1272
|
+
out = _vr_git_args(repo, ["hash-object", "--"] + [p for _, p in chunk])
|
|
1273
|
+
if out is None:
|
|
1274
|
+
return None
|
|
1275
|
+
miss = [(s, p) for (s, p), h in zip(chunk, out.split())
|
|
1276
|
+
if h != blobs.get(p, ("", ""))[1]]
|
|
1277
|
+
if miss:
|
|
1278
|
+
# Identical raw bytes mean clean (CRLF in the index plus autocrlf).
|
|
1279
|
+
# The clean filter is not idempotent: the index may hold unfiltered
|
|
1280
|
+
# bytes that a filtered hash of the worktree file never reproduces,
|
|
1281
|
+
# so the filtered hash alone would call such a file dirty.
|
|
1282
|
+
raw = _vr_git_args(repo, ["hash-object", "--no-filters", "--"] + [p for _, p in miss])
|
|
1283
|
+
if raw is None:
|
|
1284
|
+
return None
|
|
1285
|
+
for (skip, path), h in zip(miss, raw.split()):
|
|
1286
|
+
if h != blobs.get(path, ("", ""))[1]:
|
|
1287
|
+
bad = (skip, path)
|
|
1288
|
+
break
|
|
1289
|
+
if bad:
|
|
1290
|
+
skip, path = bad
|
|
1291
|
+
kind, fix = (("skip-worktree", "--no-skip-worktree") if skip
|
|
1292
|
+
else ("assume-unchanged", "--no-assume-unchanged"))
|
|
1293
|
+
sys.stderr.write("dirty: %s is flagged %s and differs from the index, which hides edits from the proof. "
|
|
1294
|
+
"Run: git update-index %s -- %s\n" % (path, kind, fix, path))
|
|
1295
|
+
return True
|
|
1188
1296
|
return False
|
|
1189
1297
|
|
|
1190
1298
|
|
|
@@ -21,6 +21,14 @@ Direction (up/down/flat) is computed by a median half-split: mean of the later
|
|
|
21
21
|
half minus mean of the earlier half. Robust to one noisy run, no float
|
|
22
22
|
regression needed, and a 2-run series degrades to last-vs-first.
|
|
23
23
|
|
|
24
|
+
Outcome effort definitions (shared with loki-ts/src/metrics/trust.ts and
|
|
25
|
+
benchmarks/bench/attribution.py; see docs/EVALUATING.md "Outcome analytics"):
|
|
26
|
+
accepted = verdict VERIFIED and not later contradicted; misleading completion =
|
|
27
|
+
false_verified / VERIFIED-claimed (attribution false_completion); cost per
|
|
28
|
+
accepted = total spend over ALL runs / accepted, None when nothing was accepted
|
|
29
|
+
or any run's cost is unmeasured. Waited seconds are wall-clock blocked time, an
|
|
30
|
+
upper bound on human attention; active minutes are NOT measured.
|
|
31
|
+
|
|
24
32
|
Public API:
|
|
25
33
|
compute_trajectory(loki_dir) -> dict (schema_version 1)
|
|
26
34
|
format_trajectory_human(traj) -> str
|
|
@@ -160,6 +168,52 @@ def _interventions_value(proof):
|
|
|
160
168
|
return None
|
|
161
169
|
|
|
162
170
|
|
|
171
|
+
_IV_TYPES = ("steer", "input", "authorization", "repair")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _cost_value(proof):
|
|
175
|
+
usd = _obj(proof.get("cost")).get("usd")
|
|
176
|
+
if usd is None or isinstance(usd, bool):
|
|
177
|
+
return None
|
|
178
|
+
try:
|
|
179
|
+
n = float(usd)
|
|
180
|
+
except (TypeError, ValueError):
|
|
181
|
+
return None
|
|
182
|
+
return n if n >= 0 and n == n and n != float("inf") else None
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _intervention_detail(proof):
|
|
186
|
+
"""(by_type, waited_s, active_s) from proof.interventions_detail; None when absent."""
|
|
187
|
+
d = _obj(proof.get("interventions_detail"))
|
|
188
|
+
bt = _obj(d.get("by_type"))
|
|
189
|
+
out = {}
|
|
190
|
+
any_key = False
|
|
191
|
+
for k in _IV_TYPES:
|
|
192
|
+
try:
|
|
193
|
+
n = int(bt.get(k))
|
|
194
|
+
except (TypeError, ValueError):
|
|
195
|
+
n = 0
|
|
196
|
+
out[k] = n if n >= 0 else 0
|
|
197
|
+
if bt.get(k) is not None:
|
|
198
|
+
any_key = True
|
|
199
|
+
w = d.get("waited_s")
|
|
200
|
+
ws = w if isinstance(w, (int, float)) and not isinstance(w, bool) and w >= 0 else None
|
|
201
|
+
a = d.get("active_s")
|
|
202
|
+
act = a if isinstance(a, int) and not isinstance(a, bool) and a >= 0 else None
|
|
203
|
+
return (out if any_key else None), ws, act
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _verdict_evidence(proof):
|
|
207
|
+
raw = str(proof.get("verdict") or "").strip().upper()
|
|
208
|
+
verdict = raw or None
|
|
209
|
+
later = str(proof.get("later_verdict") or "").strip().upper()
|
|
210
|
+
if proof.get("false_verified") is True:
|
|
211
|
+
return verdict, True
|
|
212
|
+
if verdict is None:
|
|
213
|
+
return verdict, None
|
|
214
|
+
return verdict, verdict == "VERIFIED" and later == "FAILED"
|
|
215
|
+
|
|
216
|
+
|
|
163
217
|
def _load_runs(loki_dir):
|
|
164
218
|
"""Read every .loki/proofs/<id>/proof.json into a time-ordered run list."""
|
|
165
219
|
proofs_dir = os.path.join(loki_dir, "proofs")
|
|
@@ -176,7 +230,15 @@ def _load_runs(loki_dir):
|
|
|
176
230
|
if not isinstance(proof, dict):
|
|
177
231
|
# Malformed / partial proof: skip, do not fail the trajectory.
|
|
178
232
|
continue
|
|
233
|
+
verdict, false_verified = _verdict_evidence(proof)
|
|
234
|
+
by_type, waited_s, active_s = _intervention_detail(proof)
|
|
179
235
|
runs.append({
|
|
236
|
+
"verdict": verdict,
|
|
237
|
+
"false_verified": false_verified,
|
|
238
|
+
"cost_usd": _cost_value(proof),
|
|
239
|
+
"interventions_by_type": by_type,
|
|
240
|
+
"waited_s": waited_s,
|
|
241
|
+
"active_s": active_s,
|
|
180
242
|
"run_id": str(proof.get("run_id") or name),
|
|
181
243
|
"generated_at": proof.get("generated_at"),
|
|
182
244
|
"council_pass_rate": _council_pass_value(proof.get("council")),
|
|
@@ -262,6 +324,70 @@ def _direction_for_axis(axis, ordered_values):
|
|
|
262
324
|
}
|
|
263
325
|
|
|
264
326
|
|
|
327
|
+
def outcome_effort(series):
|
|
328
|
+
"""Effort-per-outcome block; every unmeasured input yields None, never 0."""
|
|
329
|
+
accepted = sum(1 for r in series if r["verdict"] == "VERIFIED" and r["false_verified"] is not True)
|
|
330
|
+
claimed = sum(1 for r in series if r["verdict"] == "VERIFIED" or r["false_verified"] is True)
|
|
331
|
+
false_v = sum(1 for r in series if r["false_verified"] is True)
|
|
332
|
+
unmeasured = sum(1 for r in series if r["cost_usd"] is None)
|
|
333
|
+
cost = None
|
|
334
|
+
cost_note = None
|
|
335
|
+
if not series:
|
|
336
|
+
cost_note = "no runs"
|
|
337
|
+
elif accepted == 0:
|
|
338
|
+
cost_note = "no accepted run, cost per accepted is undefined"
|
|
339
|
+
elif unmeasured:
|
|
340
|
+
cost_note = "cost unmeasured for %d of %d runs, a partial sum is not a total" % (unmeasured, len(series))
|
|
341
|
+
else:
|
|
342
|
+
cost = round(sum(r["cost_usd"] for r in series) / accepted, 6)
|
|
343
|
+
with_detail = [r for r in series if r["interventions_by_type"] is not None]
|
|
344
|
+
by_type = None
|
|
345
|
+
if with_detail:
|
|
346
|
+
by_type = {k: sum(r["interventions_by_type"][k] for r in with_detail) for k in _IV_TYPES}
|
|
347
|
+
all_iv = bool(series) and all(r["interventions"] is not None for r in series)
|
|
348
|
+
all_waited = bool(series) and all(r["waited_s"] is not None for r in series)
|
|
349
|
+
all_active = bool(series) and all(r["active_s"] is not None for r in series)
|
|
350
|
+
return {
|
|
351
|
+
"runs": len(series),
|
|
352
|
+
"accepted": accepted,
|
|
353
|
+
"claimed_verified": claimed,
|
|
354
|
+
"false_verified": false_v,
|
|
355
|
+
"misleading_completion_rate": round(false_v / claimed, 4) if claimed else None,
|
|
356
|
+
"cost_per_accepted_usd": cost,
|
|
357
|
+
"cost_note": cost_note,
|
|
358
|
+
"interventions_by_type": by_type,
|
|
359
|
+
"interventions_per_accepted": (
|
|
360
|
+
round(sum(r["interventions"] for r in series) / accepted, 4) if accepted and all_iv else None),
|
|
361
|
+
"waited_min_per_accepted": (
|
|
362
|
+
round(sum(r["waited_s"] for r in series) / 60.0 / accepted, 1) if accepted and all_waited else None),
|
|
363
|
+
"active_min_per_accepted": (
|
|
364
|
+
round(sum(r["active_s"] for r in series) / 60.0 / accepted, 1) if accepted and all_active else None),
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _fmt_outcome_effort(e):
|
|
369
|
+
mc = e.get("misleading_completion_rate")
|
|
370
|
+
lines = ["Outcome effort"]
|
|
371
|
+
lines.append(" Accepted (VERIFIED): %d of %d runs" % (e["accepted"], e["runs"]))
|
|
372
|
+
lines.append(" Misleading completion: %s" % (
|
|
373
|
+
"n/a (no VERIFIED claims)" if mc is None else
|
|
374
|
+
"%d of %d VERIFIED claims (%s%%)" % (e["false_verified"], e["claimed_verified"], round(mc * 100, 1))))
|
|
375
|
+
lines.append(" Cost per accepted: %s" % (
|
|
376
|
+
"n/a (%s)" % (e.get("cost_note") or "unmeasured") if e["cost_per_accepted_usd"] is None
|
|
377
|
+
else "$%.2f" % e["cost_per_accepted_usd"]))
|
|
378
|
+
bt = e.get("interventions_by_type")
|
|
379
|
+
lines.append(" Interventions by type: %s" % (
|
|
380
|
+
"not recorded" if bt is None else ", ".join("%s %d" % (k, bt[k]) for k in _IV_TYPES)))
|
|
381
|
+
ipa = e.get("interventions_per_accepted")
|
|
382
|
+
lines.append(" Interventions per accepted: %s" % ("n/a" if ipa is None else ipa))
|
|
383
|
+
w = e.get("waited_min_per_accepted")
|
|
384
|
+
lines.append(" Minutes per accepted: %s" % (
|
|
385
|
+
"n/a" if w is None else "%s (waited, upper bound)" % w))
|
|
386
|
+
am = e.get("active_min_per_accepted")
|
|
387
|
+
lines.append(" Active minutes per accepted: %s" % ("n/a (not measured)" if am is None else am))
|
|
388
|
+
return lines
|
|
389
|
+
|
|
390
|
+
|
|
265
391
|
def compute_trajectory(loki_dir):
|
|
266
392
|
"""Return the R4 trust trajectory snapshot for a project's .loki dir."""
|
|
267
393
|
runs = _load_runs(loki_dir)
|
|
@@ -276,6 +402,12 @@ def compute_trajectory(loki_dir):
|
|
|
276
402
|
"gate_pass_rate": r["gate_pass_rate"],
|
|
277
403
|
"iterations": r["iterations"],
|
|
278
404
|
"interventions": r["interventions"],
|
|
405
|
+
"cost_usd": r["cost_usd"],
|
|
406
|
+
"interventions_by_type": r["interventions_by_type"],
|
|
407
|
+
"waited_s": r["waited_s"],
|
|
408
|
+
"active_s": r["active_s"],
|
|
409
|
+
"verdict": r["verdict"],
|
|
410
|
+
"false_verified": r["false_verified"],
|
|
279
411
|
})
|
|
280
412
|
|
|
281
413
|
axes = {}
|
|
@@ -317,6 +449,7 @@ def compute_trajectory(loki_dir):
|
|
|
317
449
|
"regressing_axes": regressing_axes,
|
|
318
450
|
"series": series,
|
|
319
451
|
"notes": notes,
|
|
452
|
+
"outcome_effort": outcome_effort(series),
|
|
320
453
|
}
|
|
321
454
|
|
|
322
455
|
|
|
@@ -376,6 +509,9 @@ def format_trajectory_human(traj):
|
|
|
376
509
|
lines.append("Not enough history yet.")
|
|
377
510
|
lines.append("Trust trajectory needs 2+ recorded runs to show a direction.")
|
|
378
511
|
lines.append("Each `loki start` run writes a proof-of-run; come back after the next run.")
|
|
512
|
+
if traj.get("runs_count") and traj.get("outcome_effort"):
|
|
513
|
+
lines.append("")
|
|
514
|
+
lines.extend(_fmt_outcome_effort(traj["outcome_effort"]))
|
|
379
515
|
if traj.get("notes"):
|
|
380
516
|
lines.append("")
|
|
381
517
|
lines.append("Notes")
|
|
@@ -398,6 +534,9 @@ def format_trajectory_human(traj):
|
|
|
398
534
|
lines.append("Overall: mixed (%d improving / %d regressing)." % (imp, reg))
|
|
399
535
|
else:
|
|
400
536
|
lines.append("Overall: stable.")
|
|
537
|
+
if traj.get("outcome_effort"):
|
|
538
|
+
lines.append("")
|
|
539
|
+
lines.extend(_fmt_outcome_effort(traj["outcome_effort"]))
|
|
401
540
|
if traj.get("notes"):
|
|
402
541
|
lines.append("")
|
|
403
542
|
lines.append("Notes")
|
|
@@ -373,8 +373,10 @@ loki_council_dispatch_agents() {
|
|
|
373
373
|
# (completion-council.sh:2792). Fail-closed: a hung or timed-out council can
|
|
374
374
|
# never become a false COMPLETE; it always degrades to the heuristic path
|
|
375
375
|
# (which has its own timeout + conservative defaults).
|
|
376
|
+
_LOKI_PERM_ARGV=(--dangerously-skip-permissions)
|
|
377
|
+
! type _loki_claude_permission_argv >/dev/null 2>&1 || _loki_claude_permission_argv
|
|
376
378
|
response=$(timeout "${LOKI_COUNCIL_REVIEW_TIMEOUT:-600}" \
|
|
377
|
-
env CAVEMAN_DEFAULT_MODE=off claude
|
|
379
|
+
env CAVEMAN_DEFAULT_MODE=off claude "${_LOKI_PERM_ARGV[@]}" \
|
|
378
380
|
-p "$prompt" \
|
|
379
381
|
--agents "$agents_json" \
|
|
380
382
|
--json-schema "$schema_content" 2>"$stderr_log") || rc=$?
|
|
@@ -222,8 +222,7 @@ def run_integration(ws, run_dir, worktrees, heads):
|
|
|
222
222
|
"""Run the integration command once; return the evidence dict."""
|
|
223
223
|
integ = ws.get("integration") or {}
|
|
224
224
|
cmd = integ.get("command")
|
|
225
|
-
ev = {"heads": heads, "exit_code": None, "log_sha256": None, "status": "not_configured"
|
|
226
|
-
"seal": False, "note": "integration evidence is bound by composite.json only when the run seals one"}
|
|
225
|
+
ev = {"heads": heads, "exit_code": None, "log_sha256": None, "status": "not_configured"}
|
|
227
226
|
if not cmd:
|
|
228
227
|
return ev
|
|
229
228
|
timeout = integ.get("timeout_s", 900)
|
|
@@ -245,7 +244,7 @@ def run_integration(ws, run_dir, worktrees, heads):
|
|
|
245
244
|
p.wait()
|
|
246
245
|
rc = 124
|
|
247
246
|
ev["status"] = "failed"
|
|
248
|
-
ev["note"]
|
|
247
|
+
ev["note"] = "timeout after %ss" % timeout
|
|
249
248
|
ev["exit_code"] = rc
|
|
250
249
|
with open(log_path, "rb") as f:
|
|
251
250
|
ev["log_sha256"] = hashlib.sha256(f.read()).hexdigest()
|