loki-mode 11.39.2 → 11.41.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 (80) hide show
  1. package/SKILL.md +5 -5
  2. package/VERSION +1 -1
  3. package/autonomy/lib/efficiency_cost.py +24 -0
  4. package/autonomy/route.sh +81 -21
  5. package/autonomy/run.sh +12 -3
  6. package/autonomy/trigger-server.py +62 -25
  7. package/bin/loki +1 -0
  8. package/dashboard/__init__.py +1 -1
  9. package/dashboard/server.py +22 -1
  10. package/docs/CLI-REFERENCE.md +1 -1
  11. package/docs/DEPLOYMENT.md +5 -3
  12. package/docs/INSTALLATION.md +1 -1
  13. package/docs/KNOWN-ISSUES.md +1 -1
  14. package/docs/exit-codes.md +0 -3
  15. package/docs/merge-queue.md +1 -1
  16. package/docs/sentry-intake.md +6 -6
  17. package/loki-ts/data/model-pricing.json +1 -0
  18. package/loki-ts/dist/loki.js +1101 -1101
  19. package/mcp/__init__.py +1 -1
  20. package/package.json +2 -2
  21. package/packages/control-plane/dist/ask-tools-server.js +15 -0
  22. package/packages/control-plane/dist/server.js +242 -226
  23. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  24. package/web-app/dist/assets/{AdminPage-sgGQRfrl.js → AdminPage-P3gsF22s.js} +1 -1
  25. package/web-app/dist/assets/{Avatar-B2svYMEF.js → Avatar-DfZ0kvq2.js} +1 -1
  26. package/web-app/dist/assets/{Badge-CNe3sQge.js → Badge-C5fbiaZR.js} +1 -1
  27. package/web-app/dist/assets/{Button-DNJqkQN4.js → Button-B5jGt7jZ.js} +1 -1
  28. package/web-app/dist/assets/{CockpitPage-J30dOs9I.js → CockpitPage-BnpuQn_T.js} +1 -1
  29. package/web-app/dist/assets/{ComparePage-Bx4MYrKs.js → ComparePage-BwX4TQ41.js} +1 -1
  30. package/web-app/dist/assets/{ErrorBoundary-peEQQHtL.js → ErrorBoundary-yB3DLU8x.js} +1 -1
  31. package/web-app/dist/assets/{EvidenceReceiptPanel-CA1L7LRZ.js → EvidenceReceiptPanel-ZAI2-wq_.js} +1 -1
  32. package/web-app/dist/assets/{GitHubIssuesPanel-CNdTJ0j1.js → GitHubIssuesPanel-DnKJAP_p.js} +1 -1
  33. package/web-app/dist/assets/{GitHubPRsPanel-CC4KL-FR.js → GitHubPRsPanel-DB9k5ZWn.js} +1 -1
  34. package/web-app/dist/assets/{HomePage-t6Idf5Sj.js → HomePage-BXqw0kte.js} +2 -2
  35. package/web-app/dist/assets/{LoginPage-CQ_XVRxV.js → LoginPage-YTq__thD.js} +1 -1
  36. package/web-app/dist/assets/{MagicPage-eEfHRM3-.js → MagicPage-cK4Voorh.js} +1 -1
  37. package/web-app/dist/assets/{MetricsPage-CbHLD11s.js → MetricsPage-9GZEVtBC.js} +1 -1
  38. package/web-app/dist/assets/{NotFoundPage-BdX_TqJ3.js → NotFoundPage-BFltgr29.js} +1 -1
  39. package/web-app/dist/assets/{ProjectPage-gJ0-4_3Z.js → ProjectPage-Dl1uGLpM.js} +3 -3
  40. package/web-app/dist/assets/{ProjectsPage-BBPwXrRQ.js → ProjectsPage-B5C9Qp2r.js} +1 -1
  41. package/web-app/dist/assets/{SettingsPage-D5ebwLI2.js → SettingsPage-rZeN9P-3.js} +1 -1
  42. package/web-app/dist/assets/{ShowcasePage-ClUDqkTu.js → ShowcasePage-CIpRNIwb.js} +1 -1
  43. package/web-app/dist/assets/{SystemSettingsPage-CWC5lImn.js → SystemSettingsPage-DA21AT1n.js} +1 -1
  44. package/web-app/dist/assets/{TeamsPage-D9WxpiIq.js → TeamsPage-Bfff6dlv.js} +1 -1
  45. package/web-app/dist/assets/{TemplatesPage-L7wg9zWx.js → TemplatesPage-COk_7uh-.js} +1 -1
  46. package/web-app/dist/assets/{TerminalOutput-RYOEwdOh.js → TerminalOutput-DcZ7Y8RN.js} +1 -1
  47. package/web-app/dist/assets/{activity-Cn5uiZp_.js → activity-DwBipFux.js} +1 -1
  48. package/web-app/dist/assets/{bell-DaQOp6Iz.js → bell-BNJ0XegR.js} +1 -1
  49. package/web-app/dist/assets/{bot-zPjnDgkd.js → bot-BytqEOpz.js} +1 -1
  50. package/web-app/dist/assets/{check-C-TgWgrY.js → check-DbTwoddF.js} +1 -1
  51. package/web-app/dist/assets/{chevron-left-BVrdwnGW.js → chevron-left-CSsct9Tt.js} +1 -1
  52. package/web-app/dist/assets/{circle-alert-BPE5_3Zf.js → circle-alert-CS1P2iTs.js} +1 -1
  53. package/web-app/dist/assets/{clock-CyauZDh4.js → clock-DCJ39VhK.js} +1 -1
  54. package/web-app/dist/assets/{cloud-Cjvpg5zE.js → cloud-_7nRyHTV.js} +1 -1
  55. package/web-app/dist/assets/{code-xml-DMBFBfXi.js → code-xml-DAftmEi4.js} +1 -1
  56. package/web-app/dist/assets/{database-UZm9Krwy.js → database-BoHhojPP.js} +1 -1
  57. package/web-app/dist/assets/{dollar-sign-BLW3cA0c.js → dollar-sign-CuaEpf1r.js} +1 -1
  58. package/web-app/dist/assets/{file-code-corner-keKipJ82.js → file-code-corner-D3C0H2eT.js} +1 -1
  59. package/web-app/dist/assets/{file-plus-DOuuT2rg.js → file-plus-DRmrq8_N.js} +1 -1
  60. package/web-app/dist/assets/{globe-Blc_IgBm.js → globe-Du5vCS0i.js} +1 -1
  61. package/web-app/dist/assets/{hammer-RAF8Y8zB.js → hammer-DkfxwcLQ.js} +1 -1
  62. package/web-app/dist/assets/{index-DVir_e8y.js → index-ZmKBwWdn.js} +3 -3
  63. package/web-app/dist/assets/{layers-BPAW51o2.js → layers-CbdK-349.js} +1 -1
  64. package/web-app/dist/assets/{loader-circle-QDP024YT.js → loader-circle-D5ms6nnc.js} +1 -1
  65. package/web-app/dist/assets/{lock-DS2mqpHL.js → lock-BxC0fjGK.js} +1 -1
  66. package/web-app/dist/assets/{package-DHbdsteV.js → package-DUSYFBey.js} +1 -1
  67. package/web-app/dist/assets/{plus-Cgrtf3pX.js → plus-IXJ_Iw5l.js} +1 -1
  68. package/web-app/dist/assets/{refresh-cw-DRdlWZ0J.js → refresh-cw-BsYeQhUs.js} +1 -1
  69. package/web-app/dist/assets/{rotate-ccw-Bsqp_AY2.js → rotate-ccw-ijMjH7vU.js} +1 -1
  70. package/web-app/dist/assets/{scroll-text-Dx_s7jyz.js → scroll-text-CIy0owUs.js} +1 -1
  71. package/web-app/dist/assets/{server-CtvpCh7a.js → server-BIctaKaw.js} +1 -1
  72. package/web-app/dist/assets/{shield-alert-ukCb5WjD.js → shield-alert-BOn8ZRj9.js} +1 -1
  73. package/web-app/dist/assets/{thumbs-up-D7cuCB5Q.js → thumbs-up-BSq8ePaY.js} +1 -1
  74. package/web-app/dist/assets/{trash-2-DIn74n9D.js → trash-2-CzF3qiiR.js} +1 -1
  75. package/web-app/dist/assets/{trending-down-R4-Husx0.js → trending-down-CQJ9PWzb.js} +1 -1
  76. package/web-app/dist/assets/{trending-up-CadBf4fF.js → trending-up-DfTOaGYJ.js} +1 -1
  77. package/web-app/dist/assets/{upload-CaU6G8CF.js → upload-B7LrdY0s.js} +1 -1
  78. package/web-app/dist/assets/{usePolling-BqEWhbKR.js → usePolling-CvyeIA-Q.js} +1 -1
  79. package/web-app/dist/assets/{user-D6Z9ASax.js → user-Dwe-VKy5.js} +1 -1
  80. 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.39.2
6
+ # Loki Mode v11.41.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
 
@@ -259,9 +259,9 @@ loki start --provider codex ./prd.json # GPT-5.3 Codex, degraded mode
259
259
  loki start --provider cline ./prd.md # Cline CLI, degraded mode
260
260
  loki start --provider aider ./prd.md # Aider (18+ providers), degraded mode
261
261
 
262
- # Parallel mode (git worktrees, Claude only)
263
- loki start ./prd.md --parallel
264
- loki start 123 --ship # Issue -> PR -> auto-merge
262
+ # Parallel and auto-merge (legacy loop only; the default engine refuses these flags)
263
+ loki start ./prd.md --parallel --engine legacy
264
+ loki start 123 --ship --engine legacy # Issue -> PR -> auto-merge (default engine: 'loki start 123' opens the PR, then 'loki merge-queue')
265
265
 
266
266
  # Run any loki command inside the published Docker image, zero config (v7.45.0).
267
267
  # Bind-mounts the current folder to /workspace so .loki state, resume, and
@@ -476,4 +476,4 @@ Implemented, planned and deprecated items, with the version each landed in: `ski
476
476
 
477
477
  ---
478
478
 
479
- **v11.39.2 | [Autonomi](https://www.autonomi.dev/) flagship product | core skill**
479
+ **v11.41.0 | [Autonomi](https://www.autonomi.dev/) flagship product | core skill**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 11.39.2
1
+ 11.41.0
@@ -32,6 +32,7 @@ __all__ = [
32
32
  "read_child_ledger",
33
33
  "load_prices",
34
34
  "price_from_tokens",
35
+ "inference_geo_multiplier",
35
36
  "DEFAULT_PRICES_PATH",
36
37
  ]
37
38
 
@@ -48,6 +49,29 @@ DEFAULT_PRICES_PATH = os.path.join(
48
49
  # small helpers (self-contained copies; proof-generator.py keeps its own)
49
50
  # ---------------------------------------------------------------------------
50
51
 
52
+ # Price multiplier for LOKI_INFERENCE_GEO, read from the same file the TS gate
53
+ # reads (inference_geo_multipliers in loki-ts/data/model-pricing.json). Unset,
54
+ # global and unknown values are 1.0; an unreadable file falls back to 1.1 for
55
+ # "us" so a budget never silently under-counts.
56
+ MODEL_PRICING_PATH = os.path.join(_REPO_ROOT, "loki-ts", "data", "model-pricing.json")
57
+
58
+
59
+ def inference_geo_multiplier(env=None, pricing_path=None):
60
+ geo = ((env if env is not None else os.environ).get("LOKI_INFERENCE_GEO") or "").strip().lower()
61
+ if not geo:
62
+ return 1.0
63
+ table = None
64
+ try:
65
+ with open(pricing_path or MODEL_PRICING_PATH, "r") as f:
66
+ table = json.load(f).get("inference_geo_multipliers")
67
+ except Exception:
68
+ table = None
69
+ if not isinstance(table, dict):
70
+ table = {"us": 1.1}
71
+ m = table.get(geo)
72
+ return float(m) if isinstance(m, (int, float)) and m > 0 else 1.0
73
+
74
+
51
75
  def _read_json(path, default=None):
52
76
  try:
53
77
  with open(path, "r") as f:
package/autonomy/route.sh CHANGED
@@ -4,20 +4,49 @@
4
4
  # order and input form never change the engine:
5
5
  # engine10 is the default for a PRD file, an issue ref, a multi-word task and a
6
6
  # one-word brief, whether the flags come first or last.
7
- # The legacy engine runs only on --engine legacy / LOKI_ENGINE=legacy, or, for
8
- # one specific flag engine10 cannot honor, for that call only (with a notice).
7
+ # The legacy engine runs only on --engine legacy / LOKI_ENGINE=legacy, or a size or
8
+ # env-provider fallback. A flag never changes the engine: engine10 maps it or refuses it.
9
9
  # Pure functions: no exec, no side effects except R_* globals and the printers.
10
10
  # Bash 3.2 compatible (no associative arrays, no ${var,,}).
11
11
 
12
12
  # Flags that take a separate value token.
13
- _ROUTE_VALUE_FLAGS=" --provider --budget --budget-limit --max-cost --max-iterations --max-retries --session-model --completion-promise --base-wait --max-wait --prd --brief --attempts --spec --aider-model --aider-flags --cline-model --isolation --output --engine --mirofish-rounds --mirofish-timeout "
13
+ _ROUTE_VALUE_FLAGS=" --provider --budget --budget-limit --max-cost --max-iterations --max-retries --session-model --completion-promise --base-wait --max-wait --prd --brief --attempts --spec --aider-model --aider-flags --cline-model --isolation --output --engine --mirofish-rounds --mirofish-timeout --max-duration --log-level --compliance --cost --remote --issue --bmad-project --openspec --mirofish-docker --destination --domain --env-file --health-url --health-match "
14
14
  # Flags engine10 honors as-is.
15
- _ROUTE_E10_FLAGS=" --no-pr --deep --json --verbose --provider --max-cost --spec --attempts "
15
+ _ROUTE_E10_FLAGS=" --no-pr --deep --json --verbose --quiet --resume --provider --max-cost --spec --attempts "
16
+ # The most a run cap can reach (capCeilingS(true) in loki-ts/src/util/run_cap.ts: the subscription ceiling).
17
+ _ROUTE_CAP_CEILING_S=3600
16
18
  # Accepted and dropped (no-ops on a non-interactive run).
17
- _ROUTE_NOOP_FLAGS=" --yes -y --no-plan --no-mirofish --no-dashboard "
19
+ # --pr is a no-op because engine10 opens a PR by default (--no-pr turns it off).
20
+ _ROUTE_NOOP_FLAGS=" --yes -y --no-plan --no-mirofish --no-dashboard --pr "
21
+
22
+ # _route_refusal_hint FLAG: the engine10 replacement for a legacy-only flag (no engine10 equivalent exists).
23
+ _route_refusal_hint() {
24
+ case "$1" in
25
+ --ship) echo "engine10 opens the PR itself and does not auto-merge; merge it with 'loki merge-queue', or pass --engine legacy to keep --ship" ;;
26
+ --worktree|-w) echo "engine10 runs each attempt in its own isolated worktree already; drop the flag, or pass --engine legacy for a git-worktree run" ;;
27
+ --detach|-d) echo "run it in the background yourself ('nohup loki start ... &') or queue it with 'loki queue run', or pass --engine legacy for --detach" ;;
28
+ --parallel) echo "use 'loki queue run --parallel K' or 'loki issues run --parallel K' for parallel runs, or pass --engine legacy for --parallel" ;;
29
+ *) echo "drop it, or pass --engine legacy to run the legacy loop with it" ;;
30
+ esac
31
+ }
18
32
 
19
33
  _route_has() { case "$1" in *" $2 "*) return 0 ;; esac; return 1; }
20
34
 
35
+ # _route_duration_s VALUE: seconds from 90, 90s, 90m or 2h; empty output when unusable or zero.
36
+ _route_duration_s() {
37
+ local v="$1" n unit
38
+ case "$v" in
39
+ [0-9]*[0-9]|[0-9]) n="$v"; unit=s ;;
40
+ [0-9]*[smh]) n="${v%?}"; unit="${v#"$n"}" ;;
41
+ *) return 0 ;;
42
+ esac
43
+ case "$n" in *[!0-9]*|"") return 0 ;; esac
44
+ n=$((10#$n))
45
+ [ "$n" -gt 0 ] || return 0
46
+ case "$unit" in m) n=$((n * 60)) ;; h) n=$((n * 3600)) ;; esac
47
+ printf '%s' "$n"
48
+ }
49
+
21
50
  _route_json_str() {
22
51
  printf '%s' "$1" | tr '\n\t\r' ' ' | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'
23
52
  }
@@ -45,9 +74,9 @@ loki_route_resolve() {
45
74
  local verb="$1"; shift
46
75
  R_DECIDED=0; R_E10=0; R_HARD=0; R_ENGINE=""; R_REASON=""; R_INPUT_TYPE=""; R_INPUT=""
47
76
  R_PROVIDER=""; R_MODEL=""; R_NOTICE=""; R_ERR=""; R_EXPLAIN=0; R_JSON=0; R_SESSION_MODEL=""; R_VERB=""
48
- R_E10_ARGS=(); R_CLEAN=(); R_ADAPTER=""; R_CAPDIFF=()
77
+ R_E10_ARGS=(); R_CLEAN=(); R_ADAPTER=""; R_CAPDIFF=(); R_E10_CAP_S=""; R_DRY=0; R_MAXCOST=""
49
78
 
50
- local a engine_req="" help=0 legacy_flag="" session_model="" provider_flag=""
79
+ local a engine_req="" dry=0 help=0 legacy_flag="" session_model="" provider_flag="" max_duration="" cap_err="" input_flag=""
51
80
  local -a positionals=() passthru=()
52
81
  local i=0 n=$#
53
82
  local -a argv=("$@")
@@ -73,15 +102,21 @@ loki_route_resolve() {
73
102
  fi
74
103
  fi
75
104
  case "$name" in
76
- --prd) positionals+=("$val"); continue ;;
77
- --brief) positionals+=("$val"); continue ;;
105
+ --prd|--brief) input_flag="$name"; positionals+=("$val"); continue ;;
78
106
  --provider) provider_flag="$val"; passthru+=(--provider "$val"); continue ;;
79
107
  --session-model) session_model="$val"; continue ;;
80
- --budget|--budget-limit|--max-cost) passthru+=(--max-cost "$val"); continue ;;
108
+ --budget|--budget-limit|--max-cost) passthru+=(--max-cost "$val"); R_MAXCOST="$val"; continue ;;
109
+ --dry-run) dry=1; continue ;;
81
110
  --json) R_JSON=1; passthru+=(--json); continue ;;
82
111
  -v) passthru+=(--verbose); continue ;;
83
112
  --attempts) passthru+=(--attempts "$val"); continue ;;
84
113
  --spec) passthru+=(--spec "$val"); continue ;;
114
+ --max-duration)
115
+ max_duration="$(_route_duration_s "$val")"
116
+ if [ -z "$max_duration" ] && [ -z "$cap_err" ]; then
117
+ cap_err="loki: --max-duration '$val' is not a duration (use seconds, or 90m / 2h)"
118
+ fi
119
+ continue ;;
85
120
  esac
86
121
  if _route_has "$_ROUTE_NOOP_FLAGS" "$name"; then continue; fi
87
122
  if _route_has "$_ROUTE_E10_FLAGS" "$name"; then passthru+=("$name"); continue; fi
@@ -131,10 +166,10 @@ loki_route_resolve() {
131
166
  task|issue) ;;
132
167
  *) R_INPUT_TYPE=""; return 0 ;;
133
168
  esac
134
- if [ -n "$legacy_flag" ] && [ -z "$engine_req" ]; then R_INPUT_TYPE=""; return 0; fi
135
169
  fi
136
170
 
137
171
  R_DECIDED=1
172
+ if [ -n "$cap_err" ]; then R_ERR="$cap_err"; return 0; fi
138
173
  if [ "$R_INPUT_TYPE" = "missing-prd" ]; then
139
174
  R_ERR="loki: PRD file not found: ${positionals[0]}"
140
175
  return 0
@@ -177,19 +212,39 @@ loki_route_resolve() {
177
212
  if [ -n "$task" ]; then R_E10_ARGS+=("$task"); fi
178
213
  R_E10_ARGS+=("${passthru[@]+"${passthru[@]}"}")
179
214
 
215
+ # --prd and --brief name the input themselves; a second input would have glued its words into the task.
216
+ if [ -n "$input_flag" ] && [ "$npos" -gt 1 ]; then
217
+ R_ERR="loki: $input_flag cannot be combined with another input (${positionals[*]}); give one input"
218
+ return 0
219
+ fi
220
+ # A flag never changes the engine (#236): an unmapped one is refused, not routed to legacy.
221
+ if [ -n "$legacy_flag" ] && [ "$engine_req" != "legacy" ]; then
222
+ R_ERR="loki: flag $legacy_flag is not supported by the Loki 10 engine; $(_route_refusal_hint "$legacy_flag")"
223
+ return 0
224
+ fi
225
+ # Wall-clock cap: LOKI_E10_CAP_S seconds, bounded by the run-cap ceiling.
226
+ if [ -n "$max_duration" ] && [ "$engine_req" != "legacy" ]; then
227
+ R_E10_CAP_S="$max_duration"
228
+ if [ "$R_E10_CAP_S" -gt "$_ROUTE_CAP_CEILING_S" ]; then
229
+ R_E10_CAP_S="$_ROUTE_CAP_CEILING_S"
230
+ R_NOTICE="loki: --max-duration $max_duration s is above the engine10 run-cap ceiling; using ${_ROUTE_CAP_CEILING_S}s."
231
+ fi
232
+ fi
233
+ if [ -n "$provider_flag" ] && [ "$engine_req" != "legacy" ]; then
234
+ case "$provider_flag" in
235
+ claude|codex|cline|aider|opencode) ;;
236
+ *)
237
+ R_ERR="loki: --provider $provider_flag has no Loki 10 engine invoker (use claude, codex, cline, aider or opencode, or pass --engine legacy)"
238
+ return 0 ;;
239
+ esac
240
+ fi
241
+ # --dry-run (engine10 has no preview of its own for these inputs): print the route, then exit 0 without a run.
242
+ if [ "$dry" = "1" ] && [ "$engine_req" != "legacy" ]; then R_DRY=1; fi
180
243
  local why=""
181
244
  if [ "$engine_req" = "legacy" ]; then
182
245
  R_ENGINE=legacy; R_REASON="explicit: --engine legacy or LOKI_ENGINE=legacy"
183
246
  elif [ "$engine_req" = "engine10" ]; then
184
- if [ -n "$legacy_flag" ]; then
185
- R_ERR="loki: flag $legacy_flag is not supported by the engine10 engine; drop it or use --engine legacy"
186
- return 0
187
- fi
188
247
  R_ENGINE=engine10; R_E10=1; R_HARD=1; R_REASON="explicit: --engine engine10"
189
- elif [ -n "$legacy_flag" ]; then
190
- R_ENGINE=legacy
191
- R_REASON="flag $legacy_flag is not supported by engine10"
192
- R_NOTICE="loki: routing this call to the legacy engine because of $legacy_flag (engine10 does not support it). Drop the flag to use engine10, or pass --engine legacy to silence this."
193
248
  else
194
249
  case "$R_PROVIDER" in
195
250
  claude|codex|cline|aider|opencode) ;;
@@ -245,9 +300,9 @@ loki_route_print() {
245
300
  for d in ${R_CAPDIFF[@]+"${R_CAPDIFF[@]}"}; do
246
301
  diffs="${diffs:+$diffs,}\"$(_route_json_str "$d")\""
247
302
  done
248
- printf '{"engine":"%s","adapter":"%s","provider":"%s","model":"%s","input_type":"%s","input":"%s","reason":"%s","capability_differences":[%s]}\n' \
303
+ printf '{"engine":"%s","adapter":"%s","provider":"%s","model":"%s","input_type":"%s","input":"%s","reason":"%s","capability_differences":[%s],"run_cap_s":%s}\n' \
249
304
  "$(_route_json_str "$R_ENGINE")" "$(_route_json_str "$R_ADAPTER")" "$(_route_json_str "$R_PROVIDER")" "$(_route_json_str "$R_MODEL")" \
250
- "$(_route_json_str "$R_INPUT_TYPE")" "$(_route_json_str "$shown")" "$(_route_json_str "$R_REASON")" "$diffs"
305
+ "$(_route_json_str "$R_INPUT_TYPE")" "$(_route_json_str "$shown")" "$(_route_json_str "$R_REASON")" "$diffs" "${R_E10_CAP_S:-null}"
251
306
  else
252
307
  printf 'engine: %s\nadapter: %s\nprovider: %s\nmodel: %s\ninput: %s (%s)\nreason: %s\n' \
253
308
  "$R_ENGINE" "$R_ADAPTER" "$R_PROVIDER" "$R_MODEL" "$R_INPUT_TYPE" "$shown" "$R_REASON"
@@ -296,6 +351,11 @@ loki_route_dispatch() {
296
351
  if [ "$R_DECIDED" != "1" ]; then return 0; fi
297
352
  if [ -n "$R_ERR" ]; then echo "$R_ERR" >&2; return 8; fi
298
353
  if [ "$R_EXPLAIN" = "1" ]; then loki_route_print; return 7; fi
354
+ if [ "$R_DRY" = "1" ]; then
355
+ loki_route_print
356
+ printf 'max-cost: %s\nrun-cap: %ss\ndry-run: nothing was started\n' "${R_MAXCOST:-default (engine10 resolves its own)}" "${R_E10_CAP_S:-$_ROUTE_CAP_CEILING_S}"
357
+ return 7
358
+ fi
299
359
  if [ -n "$R_NOTICE" ]; then echo "$R_NOTICE" >&2; fi
300
360
  R_VERB="$verb"
301
361
  return 0
package/autonomy/run.sh CHANGED
@@ -20502,8 +20502,16 @@ check_budget_limit() {
20502
20502
  # Calculate cost from per-iteration efficiency files (same source as /api/cost)
20503
20503
  if [ -d "$efficiency_dir" ]; then
20504
20504
  current_cost=$(python3 -c "
20505
- import json, glob
20505
+ import json, glob, os, sys
20506
20506
  total = 0.0
20507
+ # LOKI_INFERENCE_GEO multiplier on token-priced records, from the same JSON the
20508
+ # TS gate reads (autonomy/lib/efficiency_cost.py); the fallback keeps us at 1.1.
20509
+ try:
20510
+ sys.path.insert(0, '${SCRIPT_DIR}/lib')
20511
+ from efficiency_cost import inference_geo_multiplier
20512
+ geo = inference_geo_multiplier()
20513
+ except Exception:
20514
+ geo = 1.1 if os.environ.get('LOKI_INFERENCE_GEO', '').strip().lower() == 'us' else 1.0
20507
20515
  pricing = {
20508
20516
  'fable': {'input': 10.00, 'output': 50.00, 'cache_read': 0.25, 'cache_write': 12.5, 'cache_write_1h': 20},
20509
20517
  'claude-fable-5': {'input': 10.00, 'output': 50.00, 'cache_read': 0.25, 'cache_write': 12.5, 'cache_write_1h': 20},
@@ -20544,8 +20552,9 @@ for f in glob.glob('${efficiency_dir}/*.json'):
20544
20552
  # same run reports two different spends.
20545
20553
  cr = d.get('cache_read_tokens', 0) or 0
20546
20554
  cw = d.get('cache_creation_tokens', 0) or 0
20547
- total += (inp / 1_000_000) * p['input'] + (out / 1_000_000) * p['output']
20548
- total += (cr / 1_000_000) * p.get('cache_read', p['input'] * 0.1) + (cw / 1_000_000) * p.get('cache_write', p['input'] * 1.25)
20555
+ rec = (inp / 1_000_000) * p['input'] + (out / 1_000_000) * p['output']
20556
+ rec += (cr / 1_000_000) * p.get('cache_read', p['input'] * 0.1) + (cw / 1_000_000) * p.get('cache_write', p['input'] * 1.25)
20557
+ total += rec * geo
20549
20558
  except: pass
20550
20559
  print(round(total, 4))
20551
20560
  " 2>/dev/null || echo "0")
@@ -29,7 +29,7 @@ every /jobs request is rejected with 503 and an audit log line.
29
29
  Self-healing entry point: POST /sentry accepts a Sentry integration webhook
30
30
  authenticated by Sentry's own HMAC (sentry-hook-signature) under a THIRD,
31
31
  separate secret (LOKI_SENTRY_WEBHOOK_SECRET or SENTRY_CLIENT_SECRET). A valid
32
- alert enqueues `loki start "<normalized brief>" --pr --detach`: the output is a
32
+ alert enqueues `loki start "<normalized brief>" --pr`: the output is a
33
33
  receipt-gated PR only, never a deploy. No secret means the route answers 503.
34
34
 
35
35
  Usage:
@@ -268,9 +268,9 @@ def constant_time_equals(a, b):
268
268
 
269
269
 
270
270
  # How long to wait for a dispatched `loki start` to finish before we stop
271
- # waiting on it. The child is launched detached (--detach) so it backgrounds
272
- # itself quickly; this bound only guards the worker thread against a wedged
273
- # launch. The child keeps running independently after we stop waiting.
271
+ # waiting on it. The start runs in the foreground on the Loki 10 engine; this bound only
272
+ # stops the worker thread waiting on a long run. The child keeps running and
273
+ # is reaped in the background after we stop waiting.
274
274
  DISPATCH_WAIT_SECONDS = 30
275
275
 
276
276
  # Defaults for the bounded worker queue.
@@ -445,6 +445,32 @@ def send_notification(message):
445
445
  pass
446
446
 
447
447
 
448
+ def _dispatch_log_paths():
449
+ """Fresh stdout and stderr log paths for one dispatch under .loki/triggers."""
450
+ paths = []
451
+ for suffix in (".out", ".err"):
452
+ fd, path = tempfile.mkstemp(prefix="dispatch-", suffix=suffix, dir=str(get_loki_dir()))
453
+ os.close(fd)
454
+ paths.append(path)
455
+ return paths
456
+
457
+
458
+ def _read_bytes(path):
459
+ try:
460
+ with open(path, "rb") as f:
461
+ return f.read()
462
+ except OSError:
463
+ return b""
464
+
465
+
466
+ def _remove_quietly(*paths):
467
+ for path in paths:
468
+ try:
469
+ os.unlink(path)
470
+ except OSError:
471
+ pass
472
+
473
+
448
474
  def _reap_child(proc):
449
475
  """Wait on a child process so it is always reaped (no zombie).
450
476
 
@@ -514,29 +540,34 @@ def run_loki_outcome(args, dry_run=False, stdout_sink=None, extra_env=None):
514
540
  logging.info("[DRY-RUN] Would run: %s", " ".join(cmd))
515
541
  return OUTCOME_PASSED
516
542
  logging.info("Running: %s", " ".join(cmd))
543
+ # Child output goes to per-run files, never a pipe: past the wait window
544
+ # nothing reads a pipe, so a child writing more than the ~64KB pipe buffer
545
+ # would block forever. The files double as the run log for a detached child.
546
+ out_path, err_path = _dispatch_log_paths()
517
547
  try:
518
- proc = subprocess.Popen(
519
- cmd,
520
- stdout=subprocess.PIPE,
521
- stderr=subprocess.PIPE,
522
- env=_scoped_child_env(extra_env),
523
- )
548
+ with open(out_path, "wb") as out_f, open(err_path, "wb") as err_f:
549
+ proc = subprocess.Popen(
550
+ cmd,
551
+ stdout=out_f,
552
+ stderr=err_f,
553
+ env=_scoped_child_env(extra_env),
554
+ )
524
555
  except (FileNotFoundError, OSError) as e:
525
556
  logging.error("Failed to launch %s: %s", " ".join(cmd), e)
557
+ _remove_quietly(out_path, err_path)
526
558
  return OUTCOME_FAILED
527
559
 
528
560
  try:
529
- stdout, stderr = proc.communicate(timeout=DISPATCH_WAIT_SECONDS)
530
- if stdout_sink is not None:
531
- stdout_sink.append((stdout or b"").decode("utf-8", errors="replace"))
561
+ proc.wait(timeout=DISPATCH_WAIT_SECONDS)
532
562
  except subprocess.TimeoutExpired:
533
563
  # Detached past the wait window. The reaper collects the child, but we
534
564
  # never see its exit code, so the outcome is genuinely UNKNOWN -- NOT a
535
565
  # pass. Reporting success here is exactly the defect this replaces.
536
566
  logging.info(
537
- "Dispatch pid=%d still running after %ds; reaping in background",
567
+ "Dispatch pid=%d still running after %ds; reaping in background, output in %s",
538
568
  proc.pid,
539
569
  DISPATCH_WAIT_SECONDS,
570
+ out_path,
540
571
  )
541
572
  threading.Thread(
542
573
  target=_reap_child,
@@ -546,6 +577,12 @@ def run_loki_outcome(args, dry_run=False, stdout_sink=None, extra_env=None):
546
577
  ).start()
547
578
  return OUTCOME_UNKNOWN
548
579
 
580
+ stdout = _read_bytes(out_path)
581
+ stderr = _read_bytes(err_path)
582
+ _remove_quietly(out_path, err_path)
583
+ if stdout_sink is not None:
584
+ stdout_sink.append(stdout.decode("utf-8", errors="replace"))
585
+
549
586
  if proc.returncode == 0:
550
587
  logging.info("Dispatch pid=%d completed (exit 0)", proc.pid)
551
588
  return OUTCOME_PASSED
@@ -778,7 +815,7 @@ def tracker_comment_steer(kind, payload, is_live, dry_run=False):
778
815
 
779
816
 
780
817
  def handle_issues_event(payload, dry_run=False):
781
- """Handle issues event: opened -> loki start <issue-ref> --pr --detach."""
818
+ """Handle issues event: opened -> loki start <issue-ref> --pr."""
782
819
  action = payload.get("action", "")
783
820
  if action != "opened":
784
821
  return None, "skipped (action=%s)" % action
@@ -801,7 +838,7 @@ def handle_issues_event(payload, dry_run=False):
801
838
  ref = str(issue_number)
802
839
  if repo_full_name:
803
840
  ref = "%s#%s" % (repo_full_name, issue_number)
804
- args = ["start", ref, "--pr", "--detach"]
841
+ args = ["start", ref, "--pr"]
805
842
  summary = "issue #%s opened in %s" % (issue_number, repo_full_name)
806
843
  success = run_loki_command(args, dry_run=dry_run)
807
844
  status = "fired" if success else "error"
@@ -811,7 +848,7 @@ def handle_issues_event(payload, dry_run=False):
811
848
 
812
849
 
813
850
  def handle_pull_request_event(payload, dry_run=False):
814
- """Handle pull_request event: synchronize -> loki start <pr-ref> --detach."""
851
+ """Handle pull_request event: synchronize -> loki start <pr-ref>."""
815
852
  action = payload.get("action", "")
816
853
  if action != "synchronize":
817
854
  return None, "skipped (action=%s)" % action
@@ -828,7 +865,7 @@ def handle_pull_request_event(payload, dry_run=False):
828
865
  ref = str(pr_number)
829
866
  if repo_full_name:
830
867
  ref = "%s#%s" % (repo_full_name, pr_number)
831
- args = ["start", ref, "--detach"]
868
+ args = ["start", ref]
832
869
  summary = "PR #%s synchronized in %s" % (pr_number, repo_full_name)
833
870
  success = run_loki_command(args, dry_run=dry_run)
834
871
  status = "fired" if success else "error"
@@ -852,7 +889,7 @@ def handle_workflow_run_event(payload, dry_run=False):
852
889
  summary = "workflow '%s' failed in %s" % (wf_name, repo_full_name)
853
890
  # CI-failure context: re-run the current spec in the working directory so
854
891
  # the agent can repair the failing build. No issue/PR ref to attach here.
855
- args = ["start", "--detach"]
892
+ args = ["start"]
856
893
  success = run_loki_command(args, dry_run=dry_run)
857
894
  status = "fired" if success else "error"
858
895
  if success:
@@ -862,14 +899,14 @@ def handle_workflow_run_event(payload, dry_run=False):
862
899
 
863
900
  def brief_start_args(brief):
864
901
  """argv for a delegated PR run. `--brief` forces brief mode so the text is
865
- never reclassified as a PRD path or issue URL, and `--pr --detach` are only
902
+ never reclassified as a PRD path or issue URL, and `--pr` are only
866
903
  honored by `loki start` in brief mode because cmd_start routes them through
867
904
  the worktree + PR path."""
868
- return ["start", "--brief", brief, "--pr", "--detach"]
905
+ return ["start", "--brief", brief, "--pr"]
869
906
 
870
907
 
871
908
  def handle_job_event(payload, dry_run=False):
872
- """Handle a remotely-submitted job: loki start <spec> --detach.
909
+ """Handle a remotely-submitted job: loki start <spec>.
873
910
 
874
911
  Runs on the same worker pool as the webhook handlers. The spec was already
875
912
  validated by valid_spec() at admission; it is re-checked here so this
@@ -890,7 +927,7 @@ def handle_job_event(payload, dry_run=False):
890
927
  elif payload.get("pr") is True:
891
928
  args = brief_start_args(spec.strip())
892
929
  else:
893
- args = ["start", spec.strip(), "--detach"]
930
+ args = ["start", spec.strip()]
894
931
  summary = "job %s: %s" % (payload.get("job_id", "?"), spec.strip())
895
932
  # A remote submitter gates CI on this, so report the BUILD's outcome, not
896
933
  # merely that a build was launched. "fired" (launched) and "passed" must
@@ -1338,7 +1375,7 @@ def build_sentry_brief(norm, payload=None):
1338
1375
 
1339
1376
 
1340
1377
  def handle_sentry_event(payload, dry_run=False):
1341
- """Handle a verified Sentry alert: loki start <brief> --pr --detach.
1378
+ """Handle a verified Sentry alert: loki start <brief> --pr.
1342
1379
 
1343
1380
  PR only. Never passes a deploy argument: a fix still has to earn a VERIFIED
1344
1381
  receipt, and shipping it stays a separate, gated human step.
@@ -1535,7 +1572,7 @@ class Dispatcher:
1535
1572
  borrowed-receipt failure this design exists to prevent.
1536
1573
 
1537
1574
  Attribution is deliberately NOT resolved when the dispatch returns.
1538
- `loki start --detach` returns as soon as the child forks, while the
1575
+ `loki start` can outlast the dispatch wait window, and the
1539
1576
  build runs for minutes and writes its receipt at the very end -- so a
1540
1577
  window closed at dispatch-return would always see an unchanged pointer
1541
1578
  and every real build would 404. Resolution happens lazily in
package/bin/loki CHANGED
@@ -635,6 +635,7 @@ if [ -f "$REPO_ROOT/autonomy/route.sh" ] && { [ "$_LOKI_START_ATTEMPTS" != "1" ]
635
635
  case "$R_SESSION_MODEL" in small) R_SESSION_MODEL=fast ;; medium) R_SESSION_MODEL=development ;; high) R_SESSION_MODEL=planning ;; esac
636
636
  export LOKI_SESSION_MODEL="$R_SESSION_MODEL"
637
637
  fi
638
+ if [ "$R_E10" = "1" ] && [ -n "${R_E10_CAP_S:-}" ]; then export LOKI_E10_CAP_S="$R_E10_CAP_S"; fi
638
639
  if [ "$R_E10" != "1" ]; then
639
640
  set -- "$([ "$R_VERB" = "bare" ] && echo start || echo "$R_VERB")" ${R_CLEAN[@]+"${R_CLEAN[@]}"}
640
641
  fi
@@ -7,6 +7,6 @@ Modules:
7
7
  control: Session control helpers (status, atomic writes, origin check)
8
8
  """
9
9
 
10
- __version__ = "11.39.2"
10
+ __version__ = "11.41.0"
11
11
 
12
12
  __all__ = ["__version__"]
@@ -7830,6 +7830,25 @@ def _record_is_measured(rec: Any) -> bool:
7830
7830
  return False
7831
7831
 
7832
7832
 
7833
+ def _inference_geo_multiplier() -> float:
7834
+ """LOKI_INFERENCE_GEO price multiplier. Mirrors inference_geo_multiplier() in
7835
+ autonomy/lib/efficiency_cost.py (same JSON key, us falls back to 1.1); kept as
7836
+ a local copy for the same reason as _MEASURED_FIELDS."""
7837
+ geo = (os.environ.get("LOKI_INFERENCE_GEO") or "").strip().lower()
7838
+ if not geo:
7839
+ return 1.0
7840
+ table = None
7841
+ try:
7842
+ path = _Path(__file__).resolve().parent.parent / "loki-ts" / "data" / "model-pricing.json"
7843
+ table = json.loads(path.read_text()).get("inference_geo_multipliers")
7844
+ except Exception:
7845
+ table = None
7846
+ if not isinstance(table, dict):
7847
+ table = {"us": 1.1}
7848
+ m = table.get(geo)
7849
+ return float(m) if isinstance(m, (int, float)) and m > 0 else 1.0
7850
+
7851
+
7833
7852
  def _calculate_model_cost(
7834
7853
  model: str,
7835
7854
  input_tokens: int,
@@ -7856,7 +7875,9 @@ def _calculate_model_cost(
7856
7875
  output_cost = (output_tokens / 1_000_000) * pricing.get("output", 15.00)
7857
7876
  cache_read_cost = (cache_read_tokens / 1_000_000) * pricing.get("cache_read", inp_rate * 0.1)
7858
7877
  cache_write_cost = (cache_creation_tokens / 1_000_000) * pricing.get("cache_write", inp_rate * 1.25)
7859
- return round(input_cost + output_cost + cache_read_cost + cache_write_cost, 6)
7878
+ return round(
7879
+ (input_cost + output_cost + cache_read_cost + cache_write_cost) * _inference_geo_multiplier(), 6
7880
+ )
7860
7881
 
7861
7882
 
7862
7883
  @app.get("/api/cost", dependencies=[Depends(auth.require_scope("read"))])
@@ -1,6 +1,6 @@
1
1
  # CLI reference
2
2
 
3
- Generated from `loki-ts/src/cli/registry.ts` by `scripts/generate-stale-zero.sh` for v11.39.2. Do not edit by hand.
3
+ Generated from `loki-ts/src/cli/registry.ts` by `scripts/generate-stale-zero.sh` for v11.41.0. Do not edit by hand.
4
4
  Hidden and legacy-only commands are omitted; see `docs/v10/CLI-MODERN.md` for the full inventory.
5
5
 
6
6
  | Command | Description |
@@ -119,9 +119,11 @@ logged and ignored.
119
119
 
120
120
  | Event | Action | What runs |
121
121
  |---|---|---|
122
- | `issues` | `opened` | `loki start owner/repo#N --pr --detach` |
123
- | `pull_request` | `synchronize` | `loki start owner/repo#N --detach` |
124
- | `workflow_run` | `completed` with `conclusion=failure` | `loki start --detach` |
122
+ | `issues` | `opened` | `loki start owner/repo#N --pr` |
123
+ | `pull_request` | `synchronize` | `loki start owner/repo#N` |
124
+ | `workflow_run` | `completed` with `conclusion=failure` | `loki start` |
125
+
126
+ The trigger server runs each of these starts in the foreground on the Loki 10 engine (Wall and VERIFIED apply). A run longer than the dispatch wait window is reaped in the background and its job status stays unknown until it exits.
125
127
 
126
128
  Note what is **not** there. Labelling an issue does nothing. Commenting on an
127
129
  issue does nothing. Opening a pull request does nothing -- for PRs it is
@@ -2,7 +2,7 @@
2
2
 
3
3
  Loki Mode, by [Autonomi](https://www.autonomi.dev/), turns a spec or an issue into a verified pull request with a receipt you can check. This page covers installing it, a first run, and the options around it.
4
4
 
5
- **Version:** v11.39.2
5
+ **Version:** v11.41.0
6
6
 
7
7
  **Engine note:** `loki start` is the primary command. `loki start`, `loki "<task>"`, `loki owner/repo#N` and `loki quick` all run the Loki 10 engine, whatever the flag order or input form (PRD file, issue ref, task or one-word brief). The previous loop runs only on `--engine legacy`, `LOKI_ENGINE=legacy`, or for one call when a flag Loki 10 cannot honor is present. See the [user guide](v10/GUIDE.md) and the [5-minute quickstart](QUICKSTART.md).
8
8
 
@@ -4,7 +4,7 @@ Issues in the current release that are known and not yet fixed. Report new ones
4
4
 
5
5
  ## Which engine runs
6
6
 
7
- From 11.4.0, `loki start`, `loki "<task>"`, `loki owner/repo#N` and `loki quick` run the Loki 10 engine whatever the flag order or input form. The previous loop runs only on `--engine legacy`, `LOKI_ENGINE=legacy`, for a provider with no Loki 10 invoker, for a PRD over 100000 bytes, or for one call when a flag Loki 10 cannot honor is present (`--simple`, `--parallel`, `--sandbox`, ...). Check with `loki route start <args> --json`: it reports `engine`, `adapter`, `reason` and `capability_differences`. The full table is in the Routing contract section of `docs/exit-codes.md`.
7
+ From 11.4.0, `loki start`, `loki "<task>"`, `loki owner/repo#N` and `loki quick` run the Loki 10 engine whatever the flag order or input form. The previous loop runs only on `--engine legacy`, `LOKI_ENGINE=legacy`, for a provider with no Loki 10 invoker, for a PRD over 100000 bytes, or never because of a flag: a flag Loki 10 cannot honor (`--simple`, `--sandbox`, `--ship`, `--worktree`, `--detach`, `--parallel`, ...) is refused with exit 2 and names its replacement, and `--engine legacy` keeps it. `--pr` is accepted (Loki 10 opens the PR by default), `--quiet` and `--resume` are honored, and `--dry-run` prints the route, max-cost and run cap without starting. Check with `loki route start <args> --json`: it reports `engine`, `adapter`, `reason` and `capability_differences`. The full table is in the Routing contract section of `docs/exit-codes.md`.
8
8
 
9
9
  ## Exit codes
10
10
 
@@ -455,13 +455,10 @@ The table below is rendered from live `loki route --json` output by `tests/test-
455
455
  | `start p.md --provider cline` | engine10 | engine10:cline | 1 listed | default: prd input runs on engine10 regardless of flag order |
456
456
  | `start p.md --provider aider` | engine10 | engine10:aider | 1 listed | default: prd input runs on engine10 regardless of flag order |
457
457
  | `start p.md --provider opencode` | engine10 | engine10:opencode | 1 listed | default: prd input runs on engine10 regardless of flag order |
458
- | `start p.md --provider gemini` | legacy | legacy-bash:gemini | 2 listed | provider gemini has no engine10 invoker (use claude, codex, cline, aider or opencode) |
459
458
  | `start p.md --engine legacy` | legacy | legacy-bash:claude | 1 listed | explicit: --engine legacy or LOKI_ENGINE=legacy |
460
459
  | `start --engine legacy p.md` | legacy | legacy-bash:claude | 1 listed | explicit: --engine legacy or LOKI_ENGINE=legacy |
461
460
  | `LOKI_ENGINE=legacy start p.md` | legacy | legacy-bash:claude | 1 listed | explicit: --engine legacy or LOKI_ENGINE=legacy |
462
461
  | `start p.md --engine engine10` | engine10 | engine10:claude | none | explicit: --engine engine10 |
463
462
  | `LOKI_ENGINE=engine10 start p.md` | engine10 | engine10:claude | none | explicit: --engine engine10 |
464
- | `start p.md --parallel` | legacy | legacy-bash:claude | 2 listed | flag --parallel is not supported by engine10 |
465
- | `start --parallel p.md` | legacy | legacy-bash:claude | 2 listed | flag --parallel is not supported by engine10 |
466
463
  | `start big.md` | legacy | legacy-bash:claude | 1 listed | PRD is larger than 100000 bytes (engine10 takes the task as one argument) |
467
464
  <!-- route-table:end -->
@@ -28,5 +28,5 @@ tail to a bounded repair session on the PR branch (LOKI_PR_REPAIR_CMD, default a
28
28
  argument), pushes, and re-checks, up to LOKI_PR_REPAIR_MAX attempts (default 2).
29
29
  Green goes through the same merge gate as `loki merge pr`. When attempts run out
30
30
  the PR is returned to draft, nothing merges, and the last failure log is printed.
31
- `loki start --ship` calls this after it opens the PR; `/merge watch <pr>` runs it
31
+ `loki start --ship --engine legacy` calls this after it opens the PR; `/merge watch <pr>` runs it
32
32
  from `loki chat`.