loki-mode 11.12.1 → 11.15.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/README.md +2 -2
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/lib/backlog.py +18 -3
- package/autonomy/lib/chat.sh +6 -0
- package/autonomy/lib/engine10-push.sh +26 -2
- package/autonomy/lib/proof-generator.py +16 -0
- package/autonomy/lib/trust_trajectory.py +130 -0
- package/autonomy/lib/workspace.py +2 -3
- package/autonomy/loki +262 -58
- package/autonomy/run.sh +39 -15
- package/autonomy/trigger-server.py +452 -29
- package/bin/loki +4 -1
- package/completions/_loki +1 -1
- package/completions/loki.bash +8 -0
- package/dashboard/__init__.py +1 -1
- package/docs/CLI-REFERENCE.md +5 -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 +123 -123
- package/docs/v10/CLI-MODERN.md +2 -2
- package/docs/v10/DECISIONS.md +1 -0
- package/docs/v10/ENGINE.md +2 -0
- package/docs/v10/MODERNIZE.md +32 -1
- package/docs/v10/PROGRESS.md +3 -0
- package/loki-ts/dist/loki.js +1221 -1148
- package/mcp/__init__.py +1 -1
- package/package.json +2 -2
- package/packages/control-plane/dist/server.js +2486 -2190
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/schemas/loki-yaml.schema.json +5 -0
- package/web-app/dist/assets/{AdminPage-DuT74pf5.js → AdminPage-bZlvH4TL.js} +1 -1
- package/web-app/dist/assets/{Avatar-BtgidR5a.js → Avatar-BZrBstQe.js} +1 -1
- package/web-app/dist/assets/{Badge-D2nt8G_I.js → Badge-Q8l8r4Vq.js} +1 -1
- package/web-app/dist/assets/{Button-BKl2jOyb.js → Button-B8Bt0A3S.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-DaWr0EUX.js → CockpitPage-CIYExrXa.js} +1 -1
- package/web-app/dist/assets/{ComparePage-aIiNhxLr.js → ComparePage-CBOmHdam.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-C1Q7rV23.js → ErrorBoundary-BKwDPSnK.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-C-C0-tOx.js → EvidenceReceiptPanel-KqzjphWl.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-l3IaXDag.js → GitHubIssuesPanel-lF3GkHRv.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-DweLBgr5.js → GitHubPRsPanel-BCzipPWD.js} +1 -1
- package/web-app/dist/assets/{HomePage-B_KK7AvY.js → HomePage-CIdnkzaN.js} +2 -2
- package/web-app/dist/assets/{LoginPage-CKS-KLdH.js → LoginPage-D-178nTT.js} +1 -1
- package/web-app/dist/assets/{MagicPage-Cl6CEp8J.js → MagicPage-BmHuzMT8.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-n9CPUaX0.js → MetricsPage-CPCiyDFn.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-D85eWn70.js → NotFoundPage-B2T6n1gx.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-CDN7LpSo.js → ProjectPage-DQmpFhb0.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-D4SDmU4-.js → ProjectsPage-BmugzFEd.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-Dby6GnKm.js → SettingsPage-ZB-RrmgM.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-CgHxeqoU.js → ShowcasePage-BAhwHt4_.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-BaewGHlc.js → SystemSettingsPage-Bt7sf2Vu.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-905MJyke.js → TeamsPage-B-v5j_8q.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-DMrUHcvF.js → TemplatesPage-bdqUL-LG.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-BsbEbBYX.js → TerminalOutput-CRHEXBwS.js} +1 -1
- package/web-app/dist/assets/{activity-BuaXhBsC.js → activity-C-ROkmXt.js} +1 -1
- package/web-app/dist/assets/{bell-BfEQ71OG.js → bell-DbiCBxQA.js} +1 -1
- package/web-app/dist/assets/{bot-DOGeYewg.js → bot-DsIgE_qr.js} +1 -1
- package/web-app/dist/assets/{check-zbjPpb3E.js → check-CkAdmHWP.js} +1 -1
- package/web-app/dist/assets/{chevron-left-DWTOgfFt.js → chevron-left-CYTLivAo.js} +1 -1
- package/web-app/dist/assets/{circle-alert-BEPKfRzR.js → circle-alert-BVIcAsFJ.js} +1 -1
- package/web-app/dist/assets/{clock-C_fX3OHo.js → clock-Dg3Rq0go.js} +1 -1
- package/web-app/dist/assets/{cloud-DUXHygXQ.js → cloud-CenrqHFl.js} +1 -1
- package/web-app/dist/assets/{code-xml-BEVl_9Hw.js → code-xml-C2mvfoug.js} +1 -1
- package/web-app/dist/assets/{database-_a22uh7l.js → database-BKa0_eCb.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-l2en7xfg.js → dollar-sign-DfelUK1S.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-DItKl6H4.js → file-code-corner-CZKWoHVg.js} +1 -1
- package/web-app/dist/assets/{file-plus-BSk7qFMA.js → file-plus-CZ-m1zMX.js} +1 -1
- package/web-app/dist/assets/{globe-2TveFE5l.js → globe-S0kv_GwC.js} +1 -1
- package/web-app/dist/assets/{hammer-BU6OJlQj.js → hammer-BW1MDdu1.js} +1 -1
- package/web-app/dist/assets/{index-DGnZXUYk.js → index-ACEYsTc2.js} +3 -3
- package/web-app/dist/assets/{layers-DKw7MNuq.js → layers-BrXHYZ-L.js} +1 -1
- package/web-app/dist/assets/{loader-circle-CoYfykzD.js → loader-circle-DOdhKFx0.js} +1 -1
- package/web-app/dist/assets/{lock-D9JzrXDL.js → lock-BcFTyg8J.js} +1 -1
- package/web-app/dist/assets/{package-BZtqTIqH.js → package-D34cMVQG.js} +1 -1
- package/web-app/dist/assets/{plus-BGJfo8bV.js → plus-CEaMeBxL.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-F6lowBgR.js → refresh-cw-D2Rzxr5U.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-DHOK8dM4.js → rotate-ccw-C_-EjRLh.js} +1 -1
- package/web-app/dist/assets/{scroll-text-CKcROoMV.js → scroll-text-BSSCMI_c.js} +1 -1
- package/web-app/dist/assets/{server-DbG8MDv6.js → server-BKvLonWt.js} +1 -1
- package/web-app/dist/assets/{shield-alert-RNKNXdJT.js → shield-alert-DbbQbPdW.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-Yxp-UBxS.js → thumbs-up-NoBD1fUR.js} +1 -1
- package/web-app/dist/assets/{trash-2-DY-eba1n.js → trash-2-BXz8rAyd.js} +1 -1
- package/web-app/dist/assets/{trending-down-CARiDB3t.js → trending-down-D8u535Re.js} +1 -1
- package/web-app/dist/assets/{trending-up-brQqWbA3.js → trending-up-q8k3kgiy.js} +1 -1
- package/web-app/dist/assets/{upload-D6wRjKhz.js → upload-Cp7_zeBI.js} +1 -1
- package/web-app/dist/assets/{usePolling-BMrqNbAI.js → usePolling-CTHVdcuX.js} +1 -1
- package/web-app/dist/assets/{user-CK5-EXIj.js → user-DF7bfSf_.js} +1 -1
- package/web-app/dist/index.html +1 -1
- package/autonomy/lib/doctor-fix.py +0 -279
package/README.md
CHANGED
|
@@ -102,7 +102,7 @@ Time: 4m12s (intake 11s, plan 20s, implement 3m10s, verify 31s)
|
|
|
102
102
|
| `loki agents list\|new\|show\|export --claude` | Custom child agents in `.loki/agents/*.md` |
|
|
103
103
|
| `loki status [run-id]` | Status of the latest run, or of a given run |
|
|
104
104
|
| `loki verify [run-id]` | Check a receipt's hash, signature and event log |
|
|
105
|
-
| `loki answer [run-id]` |
|
|
105
|
+
| `loki answer [run-id]` | Start a follow-up run from a BLOCKED run with your answer (`--text "..."`) |
|
|
106
106
|
| `loki doctor [--fix] [--json]` | Check prerequisites, providers and login; `--fix` applies safe fixes |
|
|
107
107
|
| `loki control serve` | Start the Control Plane (see below) |
|
|
108
108
|
| `loki dashboard` | Open the Control Plane UI, starting it if needed |
|
|
@@ -139,7 +139,7 @@ Required:
|
|
|
139
139
|
|
|
140
140
|
- Claude Code (`claude`), installed and logged in: the default provider and the only one with every feature. The bundled Agent SDK with `ANTHROPIC_API_KEY` can stand in on the main `loki start` path, but `loki edit` and `loki chat` call the `claude` CLI.
|
|
141
141
|
- Python 3.8+ (`python3`)
|
|
142
|
-
- Node.js
|
|
142
|
+
- Node.js 20+ (`node`; 22 LTS is the CI target)
|
|
143
143
|
- `jq`
|
|
144
144
|
- Git 2.x (`git`)
|
|
145
145
|
- `curl`
|
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: An autonomous software factory that knows what it is supposed to deliver, and proves it did. Use when the user says Loki Mode or asks to build, fix or verify software autonomously.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v11.
|
|
6
|
+
# Loki Mode v11.15.0
|
|
7
7
|
|
|
8
8
|
**Current entry points (Loki 10 engine):** `loki "<task>"`, `loki owner/repo#N` and `loki quick "<task>"` run the Loki 10 engine and end with a signed receipt (`loki verify`). `loki backlog owner/repo --all` runs many issues in parallel. Guide: `docs/v10/GUIDE.md`. The skill instructions below, and `loki start`, describe the previous bash loop. PRD-file starts (`loki start ./prd.md`) and the opencode provider still run on it until FC38-SWEEP ports them; the legacy-engine command and the LOKI_ENGINE switch remain removed. Prefer `loki owner/repo#N` for issues.
|
|
9
9
|
|
|
@@ -483,4 +483,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
483
483
|
|
|
484
484
|
---
|
|
485
485
|
|
|
486
|
-
**v11.
|
|
486
|
+
**v11.15.0 | [Autonomi](https://www.autonomi.dev/) flagship product | core skill**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
11.
|
|
1
|
+
11.15.0
|
package/autonomy/lib/backlog.py
CHANGED
|
@@ -308,9 +308,17 @@ def main(argv):
|
|
|
308
308
|
child_env["LOKI_PROVIDER"] = a.provider
|
|
309
309
|
elif cfg.get("provider"):
|
|
310
310
|
child_env["LOKI_PROVIDER"] = cfg["provider"]
|
|
311
|
-
#
|
|
312
|
-
|
|
313
|
-
|
|
311
|
+
# per-run cap forwarded to each child (budgets.per_run is the TS-side alias); an exported LOKI_BUDGET_LIMIT wins over loki.yaml
|
|
312
|
+
bud = cfg.get("budgets", {})
|
|
313
|
+
per_run = bud.get("per_run_usd") or bud.get("per_run")
|
|
314
|
+
if per_run and not env.get("LOKI_BUDGET_LIMIT"):
|
|
315
|
+
child_env["LOKI_BUDGET_LIMIT"] = str(per_run)
|
|
316
|
+
group_cap = None # a DAG group shares one total: the run cap bounds the sum of its units, not each unit
|
|
317
|
+
if dag_units:
|
|
318
|
+
try:
|
|
319
|
+
group_cap = float(env.get("LOKI_BUDGET_LIMIT") or 0) or None
|
|
320
|
+
except ValueError:
|
|
321
|
+
group_cap = None
|
|
314
322
|
day_cap = cfg.get("budgets", {}).get("per_day_usd")
|
|
315
323
|
|
|
316
324
|
# One job table for issues and units: they share one slot pool.
|
|
@@ -444,6 +452,13 @@ def main(argv):
|
|
|
444
452
|
for k in [k for k, (p, _l, _w) in running.items() if p.poll() is not None]:
|
|
445
453
|
finish(k)
|
|
446
454
|
while pending and len(running) < conc:
|
|
455
|
+
group_spent = sum(st["cost"] or 0 for st in state.values())
|
|
456
|
+
if group_cap and group_spent >= group_cap:
|
|
457
|
+
for k in pending:
|
|
458
|
+
state[k].update(status="BUDGET_STOP: group budget $%.2f reached ($%.2f spent)" % (group_cap, group_spent), ok=False)
|
|
459
|
+
say("backlog: %s %s" % (spec[k]["label"], state[k]["status"]))
|
|
460
|
+
pending = []
|
|
461
|
+
break
|
|
447
462
|
spent = spent_today() if day_cap else 0.0
|
|
448
463
|
if day_cap and (spent is None or spent >= day_cap):
|
|
449
464
|
for k in pending:
|
package/autonomy/lib/chat.sh
CHANGED
|
@@ -52,7 +52,9 @@ Loki chat commands:
|
|
|
52
52
|
/model <id> set model for this session
|
|
53
53
|
/provider <name> switch provider (claude, codex, cline, aider, opencode)
|
|
54
54
|
/cost loki cost
|
|
55
|
+
/merge <args> loki merge (pr <pr>, watch <pr> to repair red CI, run, list)
|
|
55
56
|
/diff git diff --stat plus diff
|
|
57
|
+
/doctor [--json] loki doctor --repo: repo readiness score and fixes (alias /ready)
|
|
56
58
|
/undo [args] loki undo
|
|
57
59
|
/help, /exit
|
|
58
60
|
Attached mode (loki chat --attach or /attach): /status /steer <msg> /diff /verify /receipt /stop
|
|
@@ -355,8 +357,12 @@ Question: $line" || true
|
|
|
355
357
|
_chat_do "$loki" verify $rest ;;
|
|
356
358
|
/receipt) _chat_do "$loki" receipt ;;
|
|
357
359
|
/cost) _chat_do "$loki" cost ;;
|
|
360
|
+
/merge) # shellcheck disable=SC2086
|
|
361
|
+
_chat_do "$loki" merge $rest ;;
|
|
358
362
|
/undo) # shellcheck disable=SC2086
|
|
359
363
|
_chat_do "$loki" undo $rest ;;
|
|
364
|
+
/doctor|/ready) # shellcheck disable=SC2086
|
|
365
|
+
_chat_do "$loki" doctor --repo $rest ;;
|
|
360
366
|
/diff) _chat_do git diff --stat; _chat_do git diff ;;
|
|
361
367
|
/model)
|
|
362
368
|
if [ -n "$rest" ]; then _CHAT_MODEL="$rest"; echo "model: $rest"; else echo "model: ${_CHAT_MODEL:-default}"; fi ;;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# engine10-push.sh push-pr <repo-dir> <branch> <title> <body-file> [--draft] [--base <branch>]
|
|
5
5
|
# engine10-push.sh check-origin (exit 0 when the pinned origin is acceptable to push-pr, else the refusal; no network)
|
|
6
6
|
# engine10-push.sh comment <pr-number> <body-file>
|
|
7
|
-
# engine10-push.sh issue-comment <issue-ref> <body-file>
|
|
7
|
+
# engine10-push.sh issue-comment [--update] <issue-ref> <body-file>
|
|
8
8
|
# engine10-push.sh status <sha> <pending|success|failure|error> <description>
|
|
9
9
|
#
|
|
10
10
|
# Holds GitHub credentials, runs no LLM, reads no untrusted text. Inputs come
|
|
@@ -159,11 +159,14 @@ case "$mode" in
|
|
|
159
159
|
fi
|
|
160
160
|
url="$(_e10_gh pr list --repo "$_e10_repo" --head "$branch" --state open --json url --jq '.[0].url')" \
|
|
161
161
|
|| die "gh pr list failed"
|
|
162
|
+
_e10_pr_state=reused
|
|
162
163
|
if [ -z "$url" ] || [ "$url" = "null" ]; then
|
|
164
|
+
_e10_pr_state=created
|
|
163
165
|
url="$(_e10_gh pr create --repo "$_e10_repo" --head "$branch" --title "$title" --body-file "$body" "${draft[@]}" "${base[@]}")" \
|
|
164
166
|
|| die "gh pr create failed"
|
|
165
167
|
url="$(printf '%s\n' "$url" | tail -n 1)"
|
|
166
168
|
fi
|
|
169
|
+
printf '%s\n' "$_e10_pr_state" # marker line before the URL: reused (open PR found) or created
|
|
167
170
|
printf '%s\n' "$url"
|
|
168
171
|
;;
|
|
169
172
|
comment)
|
|
@@ -176,11 +179,32 @@ case "$mode" in
|
|
|
176
179
|
issue-comment)
|
|
177
180
|
# E-67: a FAILED run with no diff (or no remote) has no PR to open; comment on the issue
|
|
178
181
|
# instead so the run's reason is never silently lost. <issue-ref> is owner/repo#N or #N.
|
|
179
|
-
|
|
182
|
+
# --update: the status projector. One comment carrying the marker below is edited in place (created when absent),
|
|
183
|
+
# so an issue shows live run state without a new comment per event. Only a comment written by the authenticated
|
|
184
|
+
# gh user counts, and the body file must carry the marker (it is built by the supervisor, never from task text).
|
|
185
|
+
_e10_upd=""
|
|
186
|
+
[ "${1:-}" != "--update" ] || { _e10_upd=1; shift; }
|
|
187
|
+
[ "$#" -eq 2 ] || die "usage: issue-comment [--update] <issue-ref> <body-file>"
|
|
180
188
|
num="${1##*#}"
|
|
181
189
|
case "$num" in '' | *[!0-9]*) die "cannot extract issue number from: $1" ;; esac
|
|
182
190
|
[ -f "$2" ] || die "body file not found: $2"
|
|
183
191
|
[ -z "$_e10_local" ] || { log_info "local origin: no issue to comment on (no-op)"; exit 0; }
|
|
192
|
+
if [ -n "$_e10_upd" ]; then
|
|
193
|
+
_e10_marker='<!-- loki-status -->'
|
|
194
|
+
grep -qF -- "$_e10_marker" "$2" || die "status body must carry $_e10_marker"
|
|
195
|
+
_e10_me="$(_e10_gh api user --jq .login 2>/dev/null)" || _e10_me=""
|
|
196
|
+
_e10_cid=""
|
|
197
|
+
if [ -n "$_e10_me" ]; then
|
|
198
|
+
_e10_cid="$(_e10_gh api "repos/$_e10_repo/issues/$num/comments" --paginate \
|
|
199
|
+
--jq ".[] | select(.user.login == \"$_e10_me\") | select(.body | contains(\"$_e10_marker\")) | .id" 2>/dev/null | head -n 1)" || _e10_cid=""
|
|
200
|
+
fi
|
|
201
|
+
case "$_e10_cid" in
|
|
202
|
+
'') _e10_gh issue comment "$num" --repo "$_e10_repo" --body-file "$2" || die "gh issue comment failed" ;;
|
|
203
|
+
*[!0-9]*) die "unexpected comment id" ;;
|
|
204
|
+
*) _e10_gh api -X PATCH "repos/$_e10_repo/issues/comments/$_e10_cid" -F "body=@$2" >/dev/null || die "gh api comment update failed" ;;
|
|
205
|
+
esac
|
|
206
|
+
exit 0
|
|
207
|
+
fi
|
|
184
208
|
_e10_gh issue comment "$num" --repo "$_e10_repo" --body-file "$2" || die "gh issue comment failed"
|
|
185
209
|
;;
|
|
186
210
|
status)
|
|
@@ -1441,6 +1441,20 @@ 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
|
+
}
|
|
1444
1458
|
|
|
1445
1459
|
# PR state/URL, written by the consent-gated PR step. "prepared" means a PR
|
|
1446
1460
|
# body exists locally and GitHub was NOT mutated.
|
|
@@ -1847,6 +1861,8 @@ def _build_proof(args, loki_dir, target_dir, repo_root):
|
|
|
1847
1861
|
# "unavailable" rather than showing a fabricated zero.
|
|
1848
1862
|
if isinstance(journey, dict) and isinstance(journey.get("interventions"), int):
|
|
1849
1863
|
proof["interventions"] = journey["interventions"]
|
|
1864
|
+
if isinstance(journey, dict) and isinstance(journey.get("interventions_detail"), dict):
|
|
1865
|
+
proof["interventions_detail"] = journey["interventions_detail"]
|
|
1850
1866
|
|
|
1851
1867
|
return proof, run_id
|
|
1852
1868
|
|
|
@@ -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,50 @@ 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) 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
|
+
return (out if any_key else None), ws
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _verdict_evidence(proof):
|
|
205
|
+
raw = str(proof.get("verdict") or "").strip().upper()
|
|
206
|
+
verdict = raw or None
|
|
207
|
+
later = str(proof.get("later_verdict") or "").strip().upper()
|
|
208
|
+
if proof.get("false_verified") is True:
|
|
209
|
+
return verdict, True
|
|
210
|
+
if verdict is None:
|
|
211
|
+
return verdict, None
|
|
212
|
+
return verdict, verdict == "VERIFIED" and later == "FAILED"
|
|
213
|
+
|
|
214
|
+
|
|
163
215
|
def _load_runs(loki_dir):
|
|
164
216
|
"""Read every .loki/proofs/<id>/proof.json into a time-ordered run list."""
|
|
165
217
|
proofs_dir = os.path.join(loki_dir, "proofs")
|
|
@@ -176,7 +228,14 @@ def _load_runs(loki_dir):
|
|
|
176
228
|
if not isinstance(proof, dict):
|
|
177
229
|
# Malformed / partial proof: skip, do not fail the trajectory.
|
|
178
230
|
continue
|
|
231
|
+
verdict, false_verified = _verdict_evidence(proof)
|
|
232
|
+
by_type, waited_s = _intervention_detail(proof)
|
|
179
233
|
runs.append({
|
|
234
|
+
"verdict": verdict,
|
|
235
|
+
"false_verified": false_verified,
|
|
236
|
+
"cost_usd": _cost_value(proof),
|
|
237
|
+
"interventions_by_type": by_type,
|
|
238
|
+
"waited_s": waited_s,
|
|
180
239
|
"run_id": str(proof.get("run_id") or name),
|
|
181
240
|
"generated_at": proof.get("generated_at"),
|
|
182
241
|
"council_pass_rate": _council_pass_value(proof.get("council")),
|
|
@@ -262,6 +321,65 @@ def _direction_for_axis(axis, ordered_values):
|
|
|
262
321
|
}
|
|
263
322
|
|
|
264
323
|
|
|
324
|
+
def outcome_effort(series):
|
|
325
|
+
"""Effort-per-outcome block; every unmeasured input yields None, never 0."""
|
|
326
|
+
accepted = sum(1 for r in series if r["verdict"] == "VERIFIED" and r["false_verified"] is not True)
|
|
327
|
+
claimed = sum(1 for r in series if r["verdict"] == "VERIFIED" or r["false_verified"] is True)
|
|
328
|
+
false_v = sum(1 for r in series if r["false_verified"] is True)
|
|
329
|
+
unmeasured = sum(1 for r in series if r["cost_usd"] is None)
|
|
330
|
+
cost = None
|
|
331
|
+
cost_note = None
|
|
332
|
+
if not series:
|
|
333
|
+
cost_note = "no runs"
|
|
334
|
+
elif accepted == 0:
|
|
335
|
+
cost_note = "no accepted run, cost per accepted is undefined"
|
|
336
|
+
elif unmeasured:
|
|
337
|
+
cost_note = "cost unmeasured for %d of %d runs, a partial sum is not a total" % (unmeasured, len(series))
|
|
338
|
+
else:
|
|
339
|
+
cost = round(sum(r["cost_usd"] for r in series) / accepted, 6)
|
|
340
|
+
with_detail = [r for r in series if r["interventions_by_type"] is not None]
|
|
341
|
+
by_type = None
|
|
342
|
+
if with_detail:
|
|
343
|
+
by_type = {k: sum(r["interventions_by_type"][k] for r in with_detail) for k in _IV_TYPES}
|
|
344
|
+
all_iv = bool(series) and all(r["interventions"] is not None for r in series)
|
|
345
|
+
all_waited = bool(series) and all(r["waited_s"] is not None for r in series)
|
|
346
|
+
return {
|
|
347
|
+
"runs": len(series),
|
|
348
|
+
"accepted": accepted,
|
|
349
|
+
"claimed_verified": claimed,
|
|
350
|
+
"false_verified": false_v,
|
|
351
|
+
"misleading_completion_rate": round(false_v / claimed, 4) if claimed else None,
|
|
352
|
+
"cost_per_accepted_usd": cost,
|
|
353
|
+
"cost_note": cost_note,
|
|
354
|
+
"interventions_by_type": by_type,
|
|
355
|
+
"interventions_per_accepted": (
|
|
356
|
+
round(sum(r["interventions"] for r in series) / accepted, 4) if accepted and all_iv else None),
|
|
357
|
+
"waited_min_per_accepted": (
|
|
358
|
+
round(sum(r["waited_s"] for r in series) / 60.0 / accepted, 1) if accepted and all_waited else None),
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _fmt_outcome_effort(e):
|
|
363
|
+
mc = e.get("misleading_completion_rate")
|
|
364
|
+
lines = ["Outcome effort"]
|
|
365
|
+
lines.append(" Accepted (VERIFIED): %d of %d runs" % (e["accepted"], e["runs"]))
|
|
366
|
+
lines.append(" Misleading completion: %s" % (
|
|
367
|
+
"n/a (no VERIFIED claims)" if mc is None else
|
|
368
|
+
"%d of %d VERIFIED claims (%s%%)" % (e["false_verified"], e["claimed_verified"], round(mc * 100, 1))))
|
|
369
|
+
lines.append(" Cost per accepted: %s" % (
|
|
370
|
+
"n/a (%s)" % (e.get("cost_note") or "unmeasured") if e["cost_per_accepted_usd"] is None
|
|
371
|
+
else "$%.2f" % e["cost_per_accepted_usd"]))
|
|
372
|
+
bt = e.get("interventions_by_type")
|
|
373
|
+
lines.append(" Interventions by type: %s" % (
|
|
374
|
+
"not recorded" if bt is None else ", ".join("%s %d" % (k, bt[k]) for k in _IV_TYPES)))
|
|
375
|
+
ipa = e.get("interventions_per_accepted")
|
|
376
|
+
lines.append(" Interventions per accepted: %s" % ("n/a" if ipa is None else ipa))
|
|
377
|
+
w = e.get("waited_min_per_accepted")
|
|
378
|
+
lines.append(" Minutes per accepted: %s" % (
|
|
379
|
+
"n/a" if w is None else "%s (waited, upper bound; active minutes are not measured)" % w))
|
|
380
|
+
return lines
|
|
381
|
+
|
|
382
|
+
|
|
265
383
|
def compute_trajectory(loki_dir):
|
|
266
384
|
"""Return the R4 trust trajectory snapshot for a project's .loki dir."""
|
|
267
385
|
runs = _load_runs(loki_dir)
|
|
@@ -276,6 +394,11 @@ def compute_trajectory(loki_dir):
|
|
|
276
394
|
"gate_pass_rate": r["gate_pass_rate"],
|
|
277
395
|
"iterations": r["iterations"],
|
|
278
396
|
"interventions": r["interventions"],
|
|
397
|
+
"cost_usd": r["cost_usd"],
|
|
398
|
+
"interventions_by_type": r["interventions_by_type"],
|
|
399
|
+
"waited_s": r["waited_s"],
|
|
400
|
+
"verdict": r["verdict"],
|
|
401
|
+
"false_verified": r["false_verified"],
|
|
279
402
|
})
|
|
280
403
|
|
|
281
404
|
axes = {}
|
|
@@ -317,6 +440,7 @@ def compute_trajectory(loki_dir):
|
|
|
317
440
|
"regressing_axes": regressing_axes,
|
|
318
441
|
"series": series,
|
|
319
442
|
"notes": notes,
|
|
443
|
+
"outcome_effort": outcome_effort(series),
|
|
320
444
|
}
|
|
321
445
|
|
|
322
446
|
|
|
@@ -376,6 +500,9 @@ def format_trajectory_human(traj):
|
|
|
376
500
|
lines.append("Not enough history yet.")
|
|
377
501
|
lines.append("Trust trajectory needs 2+ recorded runs to show a direction.")
|
|
378
502
|
lines.append("Each `loki start` run writes a proof-of-run; come back after the next run.")
|
|
503
|
+
if traj.get("runs_count") and traj.get("outcome_effort"):
|
|
504
|
+
lines.append("")
|
|
505
|
+
lines.extend(_fmt_outcome_effort(traj["outcome_effort"]))
|
|
379
506
|
if traj.get("notes"):
|
|
380
507
|
lines.append("")
|
|
381
508
|
lines.append("Notes")
|
|
@@ -398,6 +525,9 @@ def format_trajectory_human(traj):
|
|
|
398
525
|
lines.append("Overall: mixed (%d improving / %d regressing)." % (imp, reg))
|
|
399
526
|
else:
|
|
400
527
|
lines.append("Overall: stable.")
|
|
528
|
+
if traj.get("outcome_effort"):
|
|
529
|
+
lines.append("")
|
|
530
|
+
lines.extend(_fmt_outcome_effort(traj["outcome_effort"]))
|
|
401
531
|
if traj.get("notes"):
|
|
402
532
|
lines.append("")
|
|
403
533
|
lines.append("Notes")
|
|
@@ -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()
|