loki-mode 11.77.0 → 11.79.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.
Files changed (86) hide show
  1. package/SKILL.md +3 -3
  2. package/VERSION +1 -1
  3. package/autonomy/lib/cockpit-render.sh +1 -1
  4. package/autonomy/lib/proof-generator.py +48 -0
  5. package/autonomy/lib/proof-pr.sh +13 -0
  6. package/autonomy/lib/telemetry-schema.json +4 -0
  7. package/autonomy/loki +280 -13
  8. package/autonomy/run.sh +126 -8
  9. package/autonomy/telemetry.sh +13 -0
  10. package/autonomy/verify.sh +2 -2
  11. package/bin/loki +17 -12
  12. package/completions/_loki +24 -0
  13. package/completions/loki.bash +22 -1
  14. package/dashboard/__init__.py +1 -1
  15. package/docs/{v10/CLI-MODERN.md → CLI-INVENTORY.md} +9 -8
  16. package/docs/CLI-REFERENCE.md +3 -2
  17. package/docs/INSTALLATION.md +1 -1
  18. package/docs/PRIVACY.md +1 -0
  19. package/docs/dev/architecture-reference.md +3 -2
  20. package/docs/dev/project-structure.md +2 -1
  21. package/loki-ts/data/child-env.json +1 -0
  22. package/loki-ts/dist/loki.js +1657 -1645
  23. package/loki-ts/package.json +2 -2
  24. package/mcp/__init__.py +1 -1
  25. package/mcp/requirements.txt +1 -1
  26. package/package.json +3 -3
  27. package/packages/control-plane/dist/server.js +19608 -19067
  28. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  29. package/skills/quality-gates.md +6 -0
  30. package/web-app/dist/assets/{AdminPage-DrlEpTKN.js → AdminPage-Bph4VGNm.js} +1 -1
  31. package/web-app/dist/assets/{Avatar-CJ9juG_K.js → Avatar-Btrppoor.js} +1 -1
  32. package/web-app/dist/assets/{Badge-DcttGvjK.js → Badge-fJFe3CEH.js} +1 -1
  33. package/web-app/dist/assets/{Button-b5O9YocE.js → Button-B_cYY4Me.js} +1 -1
  34. package/web-app/dist/assets/{CockpitPage-Bx6KwmVB.js → CockpitPage-26oBcuYy.js} +1 -1
  35. package/web-app/dist/assets/{ComparePage-BaevXGrD.js → ComparePage-CSuQZUm2.js} +1 -1
  36. package/web-app/dist/assets/{ErrorBoundary-2sZmLblz.js → ErrorBoundary-Cf7DQMz0.js} +1 -1
  37. package/web-app/dist/assets/{EvidenceReceiptPanel-C2XY1fMD.js → EvidenceReceiptPanel-BMZszYBV.js} +1 -1
  38. package/web-app/dist/assets/{GitHubIssuesPanel-DwHuq31X.js → GitHubIssuesPanel-Bt5dkJwW.js} +1 -1
  39. package/web-app/dist/assets/{GitHubPRsPanel-BQMn-YnD.js → GitHubPRsPanel-CL4NbWJ3.js} +1 -1
  40. package/web-app/dist/assets/{HomePage-Dpo55VLr.js → HomePage-Bqugxf45.js} +2 -2
  41. package/web-app/dist/assets/{LoginPage-Dfnw1PTO.js → LoginPage-ENM9QyG8.js} +1 -1
  42. package/web-app/dist/assets/{MagicPage-DoR3K8ef.js → MagicPage-D-7g-mew.js} +1 -1
  43. package/web-app/dist/assets/{MetricsPage-Brf8VAOe.js → MetricsPage-CelqK13G.js} +1 -1
  44. package/web-app/dist/assets/{NotFoundPage-CepMv0TW.js → NotFoundPage-EQ3Aq4Xg.js} +1 -1
  45. package/web-app/dist/assets/{ProjectPage-DKGi8lof.js → ProjectPage-D3Jls_Oh.js} +3 -3
  46. package/web-app/dist/assets/{ProjectsPage-BxPCmo7e.js → ProjectsPage-CgGRkJ3X.js} +1 -1
  47. package/web-app/dist/assets/{SettingsPage-CriniWvU.js → SettingsPage-B79K8xg2.js} +1 -1
  48. package/web-app/dist/assets/{ShowcasePage-B2PV4GFW.js → ShowcasePage-DxSsaGGx.js} +1 -1
  49. package/web-app/dist/assets/{SystemSettingsPage-CQUapqwh.js → SystemSettingsPage-CYNKB-5I.js} +1 -1
  50. package/web-app/dist/assets/{TeamsPage-BkLyYavH.js → TeamsPage-Bit01iEC.js} +1 -1
  51. package/web-app/dist/assets/{TemplatesPage-CeU_rnJk.js → TemplatesPage-DQ3fPi7x.js} +1 -1
  52. package/web-app/dist/assets/{TerminalOutput-oLOjcbqL.js → TerminalOutput-By137hB-.js} +1 -1
  53. package/web-app/dist/assets/{activity-DEHJPfgq.js → activity-rbRWrIjS.js} +1 -1
  54. package/web-app/dist/assets/{bell-BmXaknK2.js → bell-DIiSalJU.js} +1 -1
  55. package/web-app/dist/assets/{bot-B-JWrH_p.js → bot-BQyrbJaf.js} +1 -1
  56. package/web-app/dist/assets/{check-BaspF93s.js → check-BJ_PqlU2.js} +1 -1
  57. package/web-app/dist/assets/{chevron-left-M6VCet0K.js → chevron-left-_QsMbFBT.js} +1 -1
  58. package/web-app/dist/assets/{circle-alert-DVLpodrK.js → circle-alert-DEovFrER.js} +1 -1
  59. package/web-app/dist/assets/{clock-BRy8KpHK.js → clock-Dmrl0gvN.js} +1 -1
  60. package/web-app/dist/assets/{cloud-jTlY1qAB.js → cloud-Bs2a7FCR.js} +1 -1
  61. package/web-app/dist/assets/{code-xml-73Xelkkd.js → code-xml-D2WdclgQ.js} +1 -1
  62. package/web-app/dist/assets/{database-DpMqKrsA.js → database-kbYbPMS5.js} +1 -1
  63. package/web-app/dist/assets/{dollar-sign-Bcw89Epb.js → dollar-sign-BoOAvw6B.js} +1 -1
  64. package/web-app/dist/assets/{file-code-corner-5Drrk2XB.js → file-code-corner-ew5NSFtR.js} +1 -1
  65. package/web-app/dist/assets/{file-plus-D1PmiWXN.js → file-plus-CNV2nXNx.js} +1 -1
  66. package/web-app/dist/assets/{globe-Dq8rgYwT.js → globe-CZo7LjYX.js} +1 -1
  67. package/web-app/dist/assets/{hammer-Duyrm7AU.js → hammer-BedES-IA.js} +1 -1
  68. package/web-app/dist/assets/{index-Dg4Wvsep.js → index-C_uRanCX.js} +3 -3
  69. package/web-app/dist/assets/{layers-zbNbb_Hz.js → layers-B-OfB6O0.js} +1 -1
  70. package/web-app/dist/assets/{loader-circle-vOKp3Yno.js → loader-circle-C8hx3EE8.js} +1 -1
  71. package/web-app/dist/assets/{lock-AsdJPlwj.js → lock-Cyi_Tc3f.js} +1 -1
  72. package/web-app/dist/assets/{package-Dt8-zN2y.js → package-Cfps-Gdr.js} +1 -1
  73. package/web-app/dist/assets/{plus-md8Hlau_.js → plus-CjSNRaN2.js} +1 -1
  74. package/web-app/dist/assets/{refresh-cw-BSTU8fSw.js → refresh-cw-pEhkaiQh.js} +1 -1
  75. package/web-app/dist/assets/{rotate-ccw-D5Wn5NCl.js → rotate-ccw-twl6HuT5.js} +1 -1
  76. package/web-app/dist/assets/{scroll-text-ChmR_jcw.js → scroll-text-B9XWtVcd.js} +1 -1
  77. package/web-app/dist/assets/{server-NY1F729P.js → server-DeBbwC8T.js} +1 -1
  78. package/web-app/dist/assets/{shield-alert-yhSn1RwF.js → shield-alert-BGQQ_YfF.js} +1 -1
  79. package/web-app/dist/assets/{thumbs-up-GEJKgG1X.js → thumbs-up-Dy9nEa04.js} +1 -1
  80. package/web-app/dist/assets/{trash-2-elTR7osS.js → trash-2-DIftKB8Y.js} +1 -1
  81. package/web-app/dist/assets/{trending-down-ptCvIKeg.js → trending-down-Cjh8EsVU.js} +1 -1
  82. package/web-app/dist/assets/{trending-up-DbdbNpEl.js → trending-up-xWyMrayW.js} +1 -1
  83. package/web-app/dist/assets/{upload-D61Rwawf.js → upload-Bh3moncl.js} +1 -1
  84. package/web-app/dist/assets/{usePolling-DNF7DO1q.js → usePolling-C2sF90ff.js} +1 -1
  85. package/web-app/dist/assets/{user-BEYk6Un_.js → user-CtBrnwfC.js} +1 -1
  86. package/web-app/dist/index.html +1 -1
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: Runs an autonomous software factory that knows what it is supposed to deliver and proves it did, with a signed receipt. Use when the user says Loki Mode or asks to build, fix or verify software autonomously.
4
4
  ---
5
5
 
6
- # Loki Mode v11.77.0
6
+ # Loki Mode v11.79.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
 
@@ -227,7 +227,7 @@ Pick modules from this table, one hop from here. Each module over 100 lines open
227
227
  <!-- generated:commands -->
228
228
  Commands (generated from `loki-ts/src/cli/registry.ts`; full reference in `docs/CLI-REFERENCE.md`):
229
229
 
230
- `version`, `status`, `doctor`, `provider`, `memory`, `rollback`, `backup`, `restore`, `proof`, `wiki`, `control`, `runs`, `intent`, `kpis`, `report`, `trust`, `crash`, `contract`, `start`, `slack`, `queue`, `issues`, `undo`, `verify-pr`, `answer`, `engine10`, `help`, `chat`, `edit`, `diff`, `models`, `setup`, `quick`, `quickstart`, `init`, `template`, `verify`, `keys`, `review`, `dashboard`, `config`, `mcp`, `acp`, `stop`, `pause`, `resume`, `cancel`, `why`, `next`, `logs`, `ship`, `deploy`, `import`, `github`, `issue`, `ci`, `modernize`, `share`, `assets`, `export`, `notify`, `tour`, `wow`, `factory`, `welcome`, `onboard`, `setup-skill`, `self-update`, `remote`, `cockpit`, `code`, `context`, `secrets`, `api`, `sandbox`, `docker`, `web`, `preview`, `telemetry`, `syslog`, `explain`, `docs`, `test`, `bench`, `voice`, `own`, `secure`, `compliance`, `enterprise`, `projects`, `audit`, `cost`, `metrics`, `sentrux`, `magic`, `steer`
230
+ `version`, `status`, `doctor`, `provider`, `memory`, `rollback`, `backup`, `restore`, `proof`, `wiki`, `control`, `runs`, `intent`, `kpis`, `report`, `trust`, `crash`, `contract`, `start`, `slack`, `queue`, `issues`, `undo`, `verify-pr`, `answer`, `engine10`, `help`, `chat`, `edit`, `diff`, `models`, `setup`, `quick`, `quickstart`, `init`, `template`, `verify`, `keys`, `review`, `dashboard`, `config`, `mcp`, `acp`, `stop`, `pause`, `resume`, `cancel`, `why`, `next`, `logs`, `ship`, `deploy`, `import`, `github`, `issue`, `ci`, `modernize`, `share`, `assets`, `export`, `notify`, `tour`, `wow`, `try`, `factory`, `welcome`, `onboard`, `setup-skill`, `self-update`, `remote`, `cockpit`, `code`, `context`, `secrets`, `api`, `sandbox`, `docker`, `web`, `preview`, `telemetry`, `syslog`, `explain`, `docs`, `test`, `bench`, `voice`, `own`, `secure`, `compliance`, `enterprise`, `projects`, `audit`, `cost`, `metrics`, `sentrux`, `magic`, `steer`
231
231
  <!-- /generated -->
232
232
 
233
233
  ---
@@ -475,4 +475,4 @@ Implemented, planned and deprecated items, with the version each landed in: `ski
475
475
 
476
476
  ---
477
477
 
478
- **v11.77.0 | [Autonomi](https://www.autonomi.dev/) flagship product | core skill**
478
+ **v11.79.0 | [Autonomi](https://www.autonomi.dev/) flagship product | core skill**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 11.77.0
1
+ 11.79.0
@@ -235,7 +235,7 @@ cockpit_render() {
235
235
  [ "$no_image" = "1" ] && args+=(--no-image)
236
236
 
237
237
  # Gather -> render. Preserve the TS exit code (0 image / 3 fallback).
238
- cockpit_gather_state "$skill_dir" "$focus_repo" | bun "$entry" "${args[@]}"
238
+ cockpit_gather_state "$skill_dir" "$focus_repo" | bun --no-env-file --config=/dev/null "$entry" "${args[@]}"
239
239
  }
240
240
 
241
241
  #===============================================================================
@@ -361,6 +361,51 @@ def _test_results_fresh(quality_dir):
361
361
  return False
362
362
 
363
363
 
364
+ def _collect_advisory_review(loki_dir):
365
+ """Advisory reviewers' Critical/High counts from the latest code review.
366
+
367
+ architecture-strategist, maintainer-mergeability and test-coverage-auditor
368
+ run but hold no veto, so their findings must not vanish: they are recorded
369
+ here (receipt) and rendered in the PR report. DISPLAY ONLY: this never feeds
370
+ the headline. Returns None when no aggregate carries the advisory fields
371
+ (older run, or no review), so existing receipts are byte-identical.
372
+ """
373
+ reviews_dir = os.path.join(loki_dir, "quality", "reviews")
374
+ try:
375
+ ids = [d for d in os.listdir(reviews_dir)
376
+ if os.path.isfile(os.path.join(reviews_dir, d, "aggregate.json"))]
377
+ except OSError:
378
+ return None
379
+ if not ids:
380
+ return None
381
+ latest = max(ids, key=lambda d: (
382
+ os.path.getmtime(os.path.join(reviews_dir, d, "aggregate.json")), d))
383
+ agg = _read_json(os.path.join(reviews_dir, latest, "aggregate.json"), default=None)
384
+ if not isinstance(agg, dict) or "advisory_critical" not in agg:
385
+ return None
386
+ rows = agg.get("reviewers")
387
+ rows = rows if isinstance(rows, list) else []
388
+ reviewers = [
389
+ {"name": str(r.get("name") or ""),
390
+ "critical": _as_int(r.get("critical")), "high": _as_int(r.get("high"))}
391
+ for r in rows if isinstance(r, dict) and r.get("advisory") is True
392
+ ]
393
+ return {
394
+ "review_id": str(agg.get("review_id") or latest),
395
+ "critical": _as_int(agg.get("advisory_critical")),
396
+ "high": _as_int(agg.get("advisory_high")),
397
+ "reviewers": reviewers,
398
+ "veto": False,
399
+ }
400
+
401
+
402
+ def _as_int(v):
403
+ try:
404
+ return int(v)
405
+ except Exception:
406
+ return 0
407
+
408
+
364
409
  def _collect_quality_gates(loki_dir):
365
410
  gates_raw = _read_json(
366
411
  os.path.join(loki_dir, "state", "quality-gates.json"), default=None
@@ -523,7 +568,10 @@ def _collect_quality_gates(loki_dir):
523
568
  if name.startswith("LOKI_PHASE_")
524
569
  and str(value).strip().lower() in ("false", "0", "no", "off")
525
570
  )
571
+ _adv_review = _collect_advisory_review(loki_dir)
572
+ _adv_extra = {"advisory_review": _adv_review} if _adv_review else {}
526
573
  return {
574
+ **_adv_extra,
527
575
  "passed": passed,
528
576
  "total": total,
529
577
  "gates": gates,
@@ -235,6 +235,19 @@ def main():
235
235
  _off = [str(x) for x in _off] if isinstance(_off, list) else []
236
236
  if _off:
237
237
  _line("| Gates disabled | " + ", ".join(sorted(_off)) + " |")
238
+ # Advisory reviewers run without a veto; their Critical/High findings are
239
+ # shown here so a removed veto is never a hidden one. Display only: this row
240
+ # never feeds the headline. Emitted only when something was recorded.
241
+ _adv = _qg.get("advisory_review")
242
+ if isinstance(_adv, dict):
243
+ try:
244
+ _ac = int(_adv.get("critical") or 0)
245
+ _ah = int(_adv.get("high") or 0)
246
+ except Exception:
247
+ _ac = _ah = 0
248
+ if _ac + _ah > 0:
249
+ _line("| Advisory review findings | Critical " + str(_ac) + ", High "
250
+ + str(_ah) + " (recorded, no veto; see the review aggregate) |")
238
251
  _line("| Base sha | `" + (base_sha or "(none)") + "` |")
239
252
  _line("| Head sha | `" + (head_sha or "(none)") + "` |")
240
253
 
@@ -36,6 +36,10 @@
36
36
  "first_run_blocked": {
37
37
  "blocker": {"type": "enum", "values": ["no_provider", "not_logged_in", "not_a_repo", "git_identity", "node", "python3", "jq", "git", "curl", "disk", "skill_symlink", "other"]}
38
38
  },
39
+ "seal_hook_decision": {
40
+ "decision": {"type": "enum", "values": ["allow", "block"]},
41
+ "reason": {"type": "enum", "values": ["none", "tests_failed", "tests_weakened", "no_tests", "contract_unverified", "released", "error"]}
42
+ },
39
43
  "build_verified": {
40
44
  "headline": {"type": "enum", "values": ["VERIFIED", "VERIFIED WITH GAPS", "NOT VERIFIED"]},
41
45
  "route": {"type": "enum", "values": ["engine10"]},
package/autonomy/loki CHANGED
@@ -1864,6 +1864,7 @@ show_help_all() {
1864
1864
  echo " workspace list | infer <name> | run <name> <ref> Multi-repo runs from loki.yaml workspaces (LOKI_WORKSPACES=0 disables)"
1865
1865
  echo " start --workspace <name> \"<brief>\" Run a workspace; start --deploy [--health-url U] deploys after a VERIFIED run"
1866
1866
  echo " quickstart [idea] Guided first build: setup, idea, template, plan, go"
1867
+ echo " try <repo-url> Try Loki on a public repo in a temp clone (--dry-run previews); only the receipt is kept"
1867
1868
  echo ""
1868
1869
  echo "Session:"
1869
1870
  echo " status [--json|--badge|--probe] Show current status (--json machine-readable, --badge deploy badge, --probe re-check deploy health)"
@@ -2023,7 +2024,7 @@ show_help_all() {
2023
2024
  echo " projects proof (receipt) provider quick quickstart rc remote report reset"
2024
2025
  echo " merge restore resume review rollback run runs sandbox secrets secure self-update sentrux"
2025
2026
  echo " serve setup setup-skill share ship spec start state stats status steer stop"
2026
- echo " syslog telemetry template test tour trigger trust trust-metrics"
2027
+ echo " syslog telemetry template test tour trigger trust trust-metrics try"
2027
2028
  echo " ultracode undo update verify version voice watch watchdog web welcome why"
2028
2029
  echo " wiki workspace worktree (wt) wow"
2029
2030
  echo " control"
@@ -2186,7 +2187,7 @@ _loki_control_ui() {
2186
2187
  local cli="$SKILL_DIR/loki-ts/dist/loki.js" cdir="${HOME}/.loki/control" spid
2187
2188
  [ -f "$cli" ] || cli="$SKILL_DIR/loki-ts/src/cli.ts"
2188
2189
  mkdir -p "$cdir" && chmod 700 "$cdir" 2>/dev/null || true
2189
- LOKI_CONTROL=1 LOKI_NO_BROWSER=1 nohup bun "$cli" control serve >"$cdir/serve.log" 2>&1 </dev/null &
2190
+ LOKI_CONTROL=1 LOKI_NO_BROWSER=1 nohup bun --no-env-file --config=/dev/null "$cli" control serve >"$cdir/serve.log" 2>&1 </dev/null &
2190
2191
  spid=$!
2191
2192
  echo "$spid" > "$cdir/serve.pid" 2>/dev/null || true
2192
2193
  for i in $(seq 1 100); do
@@ -5869,8 +5870,27 @@ try:
5869
5870
  except Exception:
5870
5871
  sys.exit(1)
5871
5872
  PYEOF
5872
- local est_json
5873
- est_json="$(cmd_estimate --json 2>/dev/null)" || est_json=""
5873
+ local est_json="" est_pid="" est_ticks=0 est_max est_kid
5874
+ est_max=$(( ${LOKI_NEXT_ESTIMATE_TIMEOUT:-20} * 4 ))
5875
+ # The estimate runs in the background and is polled, so a slow or hung
5876
+ # estimator cannot hang the command (no timeout binary is assumed: macOS
5877
+ # bash 3.2 may lack one). Only the PID started here, and its direct
5878
+ # children, are ever signalled.
5879
+ ( cmd_estimate --json > "$tmp/est" 2>/dev/null ) &
5880
+ est_pid=$!
5881
+ while kill -0 "$est_pid" 2>/dev/null; do
5882
+ if [ "$est_ticks" -ge "$est_max" ]; then
5883
+ for est_kid in $(pgrep -P "$est_pid" 2>/dev/null); do kill "$est_kid" 2>/dev/null || true; done
5884
+ kill "$est_pid" 2>/dev/null || true
5885
+ note="${note:+$note; }estimate unavailable: timed out after ${LOKI_NEXT_ESTIMATE_TIMEOUT:-20}s"
5886
+ : > "$tmp/est"
5887
+ break
5888
+ fi
5889
+ sleep 0.25
5890
+ est_ticks=$((est_ticks + 1))
5891
+ done
5892
+ wait "$est_pid" 2>/dev/null || true
5893
+ est_json="$(cat "$tmp/est" 2>/dev/null)" || est_json=""
5874
5894
  { git grep -nIE 'TODO|FIXME' -- . ':!.loki' 2>/dev/null \
5875
5895
  || grep -rnIE 'TODO|FIXME' --exclude-dir=.git --exclude-dir=.loki --exclude-dir=node_modules . 2>/dev/null; } \
5876
5896
  | head -20 > "$tmp/todos" || true
@@ -15922,9 +15942,9 @@ _loki_bun_delegate() {
15922
15942
 
15923
15943
  t="$(command -v timeout 2>/dev/null || command -v gtimeout 2>/dev/null || true)"
15924
15944
  if [ -n "$t" ]; then
15925
- "$t" -k 1 5 "$bun_bin" "$cli" version >/dev/null 2>&1
15945
+ "$t" -k 1 5 "$bun_bin" --no-env-file --config=/dev/null "$cli" version >/dev/null 2>&1
15926
15946
  else
15927
- "$bun_bin" "$cli" version >/dev/null 2>&1
15947
+ "$bun_bin" --no-env-file --config=/dev/null "$cli" version >/dev/null 2>&1
15928
15948
  fi
15929
15949
  rc=$?
15930
15950
  if [ "$rc" -ne 0 ]; then
@@ -15933,7 +15953,7 @@ _loki_bun_delegate() {
15933
15953
  return 1
15934
15954
  fi
15935
15955
 
15936
- exec "$bun_bin" "$cli" "$cmd" "$@"
15956
+ exec "$bun_bin" --no-env-file --config=/dev/null "$cli" "$cmd" "$@"
15937
15957
  }
15938
15958
 
15939
15959
  # One-line fix for a requirement id (a provider name is an id), read from the shared
@@ -17166,13 +17186,20 @@ PY
17166
17186
  return 0
17167
17187
  }
17168
17188
 
17169
- # Removes only a directory this command created: a direct child of the temp root named loki-wow.*.
17189
+ # Removes only a directory the caller created: a direct child of the temp root named
17190
+ # <prefix>.* (default loki-wow), not a symlink. With a marker file name as $3, the directory
17191
+ # must also hold that file (not a symlink) whose first line equals the directory's own path.
17170
17192
  wow_cleanup_dir() {
17171
- local d="${1:-}" root
17193
+ local d="${1:-}" prefix="${2:-loki-wow}" marker="${3:-}" root marker_value=""
17172
17194
  [ -n "$d" ] && [ -d "$d" ] && [ ! -L "$d" ] || return 0
17173
17195
  root="$(cd "${TMPDIR:-/tmp}" 2>/dev/null && pwd -P)" || return 0
17174
- case "$d" in "$root"/loki-wow.*) ;; *) return 0 ;; esac
17196
+ case "$d" in "$root/$prefix".*) ;; *) return 0 ;; esac
17175
17197
  [ "$(dirname -- "$d")" = "$root" ] || return 0
17198
+ if [ -n "$marker" ]; then
17199
+ [ -f "$d/$marker" ] && [ ! -L "$d/$marker" ] || return 0
17200
+ IFS= read -r marker_value <"$d/$marker" || true
17201
+ [ "$marker_value" = "$d" ] || return 0
17202
+ fi
17176
17203
  rm -rf -- "$d"
17177
17204
  }
17178
17205
 
@@ -23005,6 +23032,243 @@ for r in reqs:
23005
23032
  "
23006
23033
  }
23007
23034
 
23035
+ # loki try: clone a public repo into a run-owned temp dir, pick a starter task, run it
23036
+ # there, keep only the receipt. The caller's checkout is never touched. The temp helpers
23037
+ # below follow the loki_run_tmp_create/cleanup contract (the repo's own copies live under
23038
+ # eval/, which the npm package does not ship).
23039
+ try_tmp_create() {
23040
+ local temp_root d
23041
+ temp_root="$(cd "${TMPDIR:-/tmp}" 2>/dev/null && pwd -P)" || return 1
23042
+ d="$(mktemp -d "${temp_root}/loki-run.XXXXXXXX")" || return 1
23043
+ chmod 700 "$d" || { rmdir "$d" 2>/dev/null; return 1; }
23044
+ if ! printf '%s\n' "$d" >"$d/.loki-run-owned"; then rm -rf -- "$d"; return 1; fi
23045
+ printf '%s\n' "$d"
23046
+ }
23047
+
23048
+ # Removes only a directory try_tmp_create made (loki-run.* child of the temp root whose
23049
+ # .loki-run-owned marker holds its own path); the checks live in wow_cleanup_dir.
23050
+ try_tmp_cleanup() {
23051
+ wow_cleanup_dir "${1:-}" loki-run .loki-run-owned
23052
+ }
23053
+
23054
+ # Prints the starter task for the clone at $1 (source URL $2): the first open GitHub
23055
+ # issue title when gh can read it, else the first TODO line. Prints nothing when neither exists.
23056
+ try_pick_task() {
23057
+ local dest="$1" url="$2" slug title first p l t todo_file
23058
+ case "$url" in
23059
+ https://github.com/*)
23060
+ slug="${url#https://github.com/}"
23061
+ slug="${slug%.git}"
23062
+ slug="${slug%/}"
23063
+ if command -v gh >/dev/null 2>&1; then
23064
+ title="$(gh issue list --repo "$slug" --state open --limit 1 --json title --jq '.[0].title // empty' 2>/dev/null)" || title=""
23065
+ title="$(printf '%s' "$title" | LC_ALL=C tr -d '\000-\011\013-\037\177' | LC_ALL=C tr '\n' ' ')"
23066
+ if [ -n "$title" ]; then
23067
+ printf '%s\n' "Resolve the open issue: ${title:0:200}"
23068
+ return 0
23069
+ fi
23070
+ fi
23071
+ ;;
23072
+ esac
23073
+ todo_file="$(dirname -- "$dest")/todo.txt"
23074
+ git -C "$dest" grep -I -n -e TODO >"$todo_file" 2>/dev/null || true
23075
+ first="$(sed -n 1p "$todo_file" 2>/dev/null)"
23076
+ [ -n "$first" ] || return 0
23077
+ p="${first%%:*}"
23078
+ first="${first#*:}"
23079
+ l="${first%%:*}"
23080
+ t="${first#*:}"
23081
+ t="$(printf '%s' "$t" | LC_ALL=C tr -d '\000-\037\177' | sed -e 's/^[[:space:]]*//' | cut -c1-160)"
23082
+ p="$(printf '%s' "$p" | LC_ALL=C tr -d '\000-\037\177')"
23083
+ printf '%s\n' "Resolve the TODO in ${p} (line ${l}): ${t}"
23084
+ }
23085
+
23086
+ # Copies the newest receipt.json under $1/.loki/runs/<run>/ to a kept location and prints its
23087
+ # path. The clone is untrusted: every level (.loki, runs, the run dir, the file) is opened
23088
+ # without following symlinks, and only a receipt modified after the stamp file $2 (touched
23089
+ # right before loki start) is accepted, so a receipt committed in the repo is never reported.
23090
+ try_keep_receipt() {
23091
+ local dest="$1" stamp="$2" out_root
23092
+ out_root="${LOKI_TRY_RECEIPT_DIR:-${HOME}/.loki/try-receipts}"
23093
+ python3 -I - "$dest" "$stamp" "$out_root" <<'PY'
23094
+ import os, re, stat, sys
23095
+ dest, stamp, out_root = sys.argv[1:4]
23096
+ D = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW
23097
+ try:
23098
+ start = os.stat(stamp).st_mtime_ns
23099
+ root = os.open(dest, D)
23100
+ loki = os.open(".loki", D, dir_fd=root)
23101
+ runs = os.open("runs", D, dir_fd=loki)
23102
+ names = os.listdir(runs)
23103
+ except OSError:
23104
+ sys.exit(1)
23105
+ best = None
23106
+ for name in names:
23107
+ if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]*", name):
23108
+ continue
23109
+ try:
23110
+ rd = os.open(name, D, dir_fd=runs)
23111
+ except OSError:
23112
+ continue
23113
+ try:
23114
+ fd = os.open("receipt.json", os.O_RDONLY | os.O_NOFOLLOW, dir_fd=rd)
23115
+ except OSError:
23116
+ os.close(rd)
23117
+ continue
23118
+ try:
23119
+ st = os.fstat(fd)
23120
+ if stat.S_ISREG(st.st_mode) and st.st_size <= 16 * 1024 * 1024 and st.st_mtime_ns > start:
23121
+ if best is None or st.st_mtime_ns > best[0]:
23122
+ with os.fdopen(os.dup(fd), "rb") as f:
23123
+ best = (st.st_mtime_ns, name, f.read())
23124
+ finally:
23125
+ os.close(fd)
23126
+ os.close(rd)
23127
+ if best is None:
23128
+ sys.exit(1)
23129
+ out = os.path.join(out_root, best[1])
23130
+ os.makedirs(out, exist_ok=True)
23131
+ target = os.path.join(out, "receipt.json")
23132
+ with open(target, "wb") as f:
23133
+ f.write(best[2])
23134
+ print(target)
23135
+ PY
23136
+ }
23137
+
23138
+ _cmd_try_body() {
23139
+ local tmp="$1" url="$2" dry_run="$3" provider="$4" max_cost="$5"
23140
+ local dest="$tmp/repo" task
23141
+ if ! (
23142
+ unset GIT_DIR GIT_WORK_TREE GIT_COMMON_DIR GIT_INDEX_FILE GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES GIT_PREFIX
23143
+ GIT_TERMINAL_PROMPT=0 git -c protocol.ext.allow=never -c core.hooksPath=/dev/null \
23144
+ clone --depth 1 --quiet -- "$url" "$dest"
23145
+ ); then
23146
+ echo "loki try: could not clone $url" >&2
23147
+ return 1
23148
+ fi
23149
+ task="$(try_pick_task "$dest" "$url")"
23150
+ if [ -z "$task" ]; then
23151
+ echo "loki try: no starter task found (no open issue readable, no TODO line) in $url" >&2
23152
+ return 1
23153
+ fi
23154
+ echo "Clone path: $dest"
23155
+ echo "Task: $task"
23156
+ echo "Estimate: one task on the Loki 10 engine, provider $provider, cost capped at \$$max_cost (--max-cost); the receipt records the actual spend."
23157
+ if [ "$dry_run" = true ]; then
23158
+ echo "[dry-run] No model call was made. A real run would execute:"
23159
+ echo " loki start \"<task>\" --no-pr --provider $provider --max-cost $max_cost"
23160
+ return 0
23161
+ fi
23162
+ local rc=0 receipt stamp="$tmp/started.stamp"
23163
+ : >"$stamp"
23164
+ (
23165
+ cd "$dest" && LOKI_ASSUME_YES=1 "$SKILL_DIR/bin/loki" start "$task" --no-pr --provider "$provider" --max-cost "$max_cost"
23166
+ ) || rc=$?
23167
+ if receipt="$(try_keep_receipt "$dest" "$stamp")"; then
23168
+ echo ""
23169
+ echo "Receipt: $receipt"
23170
+ echo "Check it: loki verify $receipt"
23171
+ else
23172
+ echo ""
23173
+ echo "No receipt was written (run exited $rc)."
23174
+ [ "$rc" != "0" ] || rc=1
23175
+ fi
23176
+ return "$rc"
23177
+ }
23178
+
23179
+ cmd_try() {
23180
+ local url="" dry_run=false provider="" max_cost="2"
23181
+ while [[ $# -gt 0 ]]; do
23182
+ case "$1" in
23183
+ --help|-h)
23184
+ echo -e "${BOLD}loki try${NC} - Try Loki on a public repo without touching your checkout"
23185
+ echo ""
23186
+ echo "Usage: loki try <public-repo-url> [--dry-run] [--provider P] [--max-cost N]"
23187
+ echo ""
23188
+ echo "Clones the repo (depth 1) into a run-owned temp directory, picks a starter"
23189
+ echo "task (the first open GitHub issue title, else the first TODO line), runs it"
23190
+ echo "on the Loki 10 engine with no PR, keeps only the receipt, and always removes"
23191
+ echo "the temp clone. Your current checkout is never read or changed."
23192
+ echo ""
23193
+ echo "Options:"
23194
+ echo " --dry-run Clone, then print the clone path, the task and an estimate. No model call, no spend"
23195
+ echo " --provider P claude (default) or opencode, codex, cline, aider"
23196
+ echo " --max-cost N Hard per-run cap in USD, a positive number (default 2)"
23197
+ echo " --help, -h Show this help"
23198
+ echo ""
23199
+ echo "URL schemes: https://, http://, git://, file://"
23200
+ echo "The receipt is kept under \${LOKI_TRY_RECEIPT_DIR:-~/.loki/try-receipts}/<run>/receipt.json;"
23201
+ echo "re-check it with: loki verify <receipt path>"
23202
+ echo ""
23203
+ echo "Examples:"
23204
+ echo " loki try https://github.com/owner/repo --dry-run"
23205
+ echo " loki try https://github.com/owner/repo"
23206
+ return 0
23207
+ ;;
23208
+ --dry-run) dry_run=true; shift ;;
23209
+ --provider)
23210
+ provider="${2:-}"
23211
+ if [ -z "$provider" ]; then echo "loki try: --provider requires a value" >&2; return 2; fi
23212
+ shift 2
23213
+ ;;
23214
+ --provider=*) provider="${1#*=}"; shift ;;
23215
+ --max-cost)
23216
+ max_cost="${2:-}"
23217
+ if [ -z "$max_cost" ]; then echo "loki try: --max-cost requires a value" >&2; return 2; fi
23218
+ shift 2
23219
+ ;;
23220
+ --max-cost=*) max_cost="${1#*=}"; shift ;;
23221
+ -*)
23222
+ echo "loki try: unknown option: $1" >&2
23223
+ echo "Run 'loki try --help'." >&2
23224
+ return 2
23225
+ ;;
23226
+ *)
23227
+ if [ -n "$url" ]; then
23228
+ echo "loki try: only one repo URL is accepted" >&2
23229
+ return 2
23230
+ fi
23231
+ url="$1"
23232
+ shift
23233
+ ;;
23234
+ esac
23235
+ done
23236
+ if [ -z "$url" ]; then
23237
+ echo "Usage: loki try <public-repo-url> [--dry-run]" >&2
23238
+ return 2
23239
+ fi
23240
+ case "$url" in
23241
+ https://*|http://*|git://*|file://*) ;;
23242
+ *) echo "loki try: unsupported URL '$url' (use https://, http://, git:// or file://)" >&2; return 2 ;;
23243
+ esac
23244
+ case "$url" in *[[:space:]]*) echo "loki try: the URL must not contain whitespace" >&2; return 2 ;; esac
23245
+ if ! [[ "$max_cost" =~ ^[0-9]+(\.[0-9]+)?$ ]] || ! awk -v c="$max_cost" 'BEGIN { exit !(c > 0) }'; then
23246
+ echo "loki try: --max-cost must be a positive number of USD, got: $max_cost" >&2
23247
+ return 2
23248
+ fi
23249
+ local use_provider="${provider:-${LOKI_PROVIDER:-claude}}"
23250
+ case "$use_provider" in
23251
+ claude|opencode|codex|cline|aider) ;;
23252
+ *) echo "loki try: unknown provider '$use_provider' (use claude, opencode, codex, cline or aider)" >&2; return 2 ;;
23253
+ esac
23254
+ if [ "$dry_run" != true ] && ! wow_provider_ready "$use_provider"; then
23255
+ echo "loki try: no provider ready for '$use_provider'. Run 'loki doctor', or preview with --dry-run." >&2
23256
+ return 1
23257
+ fi
23258
+
23259
+ local tmp rc=0
23260
+ tmp="$(try_tmp_create)" || { echo "loki try: cannot create a temp directory" >&2; return 1; }
23261
+ # shellcheck disable=SC2064 # expand tmp now; it is local to this call
23262
+ trap "try_tmp_cleanup '$tmp'; trap - INT TERM HUP; exit 130" INT
23263
+ trap "try_tmp_cleanup '$tmp'; trap - INT TERM HUP; exit 143" TERM
23264
+ trap "try_tmp_cleanup '$tmp'; trap - INT TERM HUP; exit 129" HUP
23265
+ _cmd_try_body "$tmp" "$url" "$dry_run" "$use_provider" "$max_cost" || rc=$?
23266
+ trap - INT TERM HUP
23267
+ try_tmp_cleanup "$tmp"
23268
+ if [ ! -e "$tmp" ]; then echo "Removed the temp clone."; fi
23269
+ return "$rc"
23270
+ }
23271
+
23008
23272
  cmd_plan() {
23009
23273
  local prd_file=""
23010
23274
  local show_requirements=false
@@ -23130,7 +23394,7 @@ _suggest_command() {
23130
23394
  ultracode council demo dogfood heal modernize import issue github init template \
23131
23395
  status stop pause resume monitor watch watchdog dashboard web open preview share \
23132
23396
  memory context ctx checkpoint state report kpis cost metrics stats logs otel \
23133
- syslog telemetry crash config provider doctor setup setup-skill onboard quickstart \
23397
+ syslog telemetry crash config provider doctor setup setup-skill onboard quickstart try \
23134
23398
  welcome update self-update rollback merge failover cluster enterprise remote deploy docker \
23135
23399
  sandbox api mcp magic compound assets export wiki control docs explain why audit compliance \
23136
23400
  welcome update self-update rollback failover cluster enterprise remote deploy docker \
@@ -23703,6 +23967,9 @@ main() {
23703
23967
  plan)
23704
23968
  cmd_plan "$@"
23705
23969
  ;;
23970
+ try)
23971
+ cmd_try "$@"
23972
+ ;;
23706
23973
  failover)
23707
23974
  cmd_failover "$@"
23708
23975
  ;;
@@ -30477,7 +30744,7 @@ cmd_estimate() {
30477
30744
  echo "Install Bun (https://bun.sh) or remove LOKI_LEGACY_BASH=1." >&2
30478
30745
  return 1
30479
30746
  fi
30480
- bun "$entry" estimate "$@"
30747
+ bun --no-env-file --config=/dev/null "$entry" estimate "$@"
30481
30748
  }
30482
30749
 
30483
30750
  # Transparent cost view (R3): per-run + per-project spend, model routing, and
@@ -36052,7 +36319,7 @@ cmd_control() {
36052
36319
  return 1
36053
36320
  fi
36054
36321
  [ -f "$cli" ] || cli="$SKILL_DIR/loki-ts/src/cli.ts"
36055
- exec bun "$cli" control "$@"
36322
+ exec bun --no-env-file --config=/dev/null "$cli" control "$@"
36056
36323
  }
36057
36324
 
36058
36325
  cmd_wiki() {