loki-mode 11.15.0 → 11.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/SKILL.md +3 -3
  2. package/VERSION +1 -1
  3. package/autonomy/grill.sh +3 -1
  4. package/autonomy/hooks/validate-bash.sh +51 -13
  5. package/autonomy/lib/chat.sh +111 -7
  6. package/autonomy/lib/claude-flags.sh +20 -0
  7. package/autonomy/lib/claude_permissions.py +40 -0
  8. package/autonomy/lib/done-recognition.sh +8 -4
  9. package/autonomy/lib/engine10-push.sh +30 -5
  10. package/autonomy/lib/prd-enrich.sh +4 -2
  11. package/autonomy/lib/proof-generator.py +5 -0
  12. package/autonomy/lib/proof-verify.py +109 -1
  13. package/autonomy/lib/trust_trajectory.py +13 -4
  14. package/autonomy/lib/voter-agents.sh +3 -1
  15. package/autonomy/loki +175 -26
  16. package/autonomy/quickstart.sh +21 -2
  17. package/autonomy/run.sh +16 -6
  18. package/autonomy/trigger-server.py +11 -5
  19. package/completions/loki.bash +2 -2
  20. package/dashboard/__init__.py +1 -1
  21. package/dashboard/api_start.py +2 -2
  22. package/dashboard/build_supervisor.py +2 -0
  23. package/docs/CLI-REFERENCE.md +2 -1
  24. package/docs/EVALUATING.md +1 -1
  25. package/docs/INSTALLATION.md +1 -1
  26. package/docs/v10/BOARD.md +25 -0
  27. package/docs/v10/CLI-MODERN.md +3 -3
  28. package/docs/v10/DECISIONS.md +1 -0
  29. package/docs/v10/ENGINE.md +1 -1
  30. package/docs/v10/FAILURE-CLASSES.md +18 -0
  31. package/docs/v10/PROGRESS.md +4 -0
  32. package/loki-ts/data/finding-schema.json +2 -1
  33. package/loki-ts/dist/loki.js +1135 -1114
  34. package/magic/core/debate.py +27 -1
  35. package/mcp/__init__.py +1 -1
  36. package/package.json +2 -2
  37. package/packages/control-plane/dist/server.js +3761 -3454
  38. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  39. package/providers/claude.sh +6 -3
  40. package/providers/model_catalog.json +6 -0
  41. package/src/policies/claude_permissions.js +114 -0
  42. package/web-app/dist/assets/{AdminPage-bZlvH4TL.js → AdminPage-OVzN6eLm.js} +1 -1
  43. package/web-app/dist/assets/{Avatar-BZrBstQe.js → Avatar-hCCrxrCZ.js} +1 -1
  44. package/web-app/dist/assets/{Badge-Q8l8r4Vq.js → Badge-CDcpkhbJ.js} +1 -1
  45. package/web-app/dist/assets/{Button-B8Bt0A3S.js → Button-8mwWlhEu.js} +1 -1
  46. package/web-app/dist/assets/{CockpitPage-CIYExrXa.js → CockpitPage-D8cf3u22.js} +1 -1
  47. package/web-app/dist/assets/{ComparePage-CBOmHdam.js → ComparePage-CIgkbDRY.js} +1 -1
  48. package/web-app/dist/assets/{ErrorBoundary-BKwDPSnK.js → ErrorBoundary-99kJH9g2.js} +1 -1
  49. package/web-app/dist/assets/{EvidenceReceiptPanel-KqzjphWl.js → EvidenceReceiptPanel-C9fj32eS.js} +1 -1
  50. package/web-app/dist/assets/{GitHubIssuesPanel-lF3GkHRv.js → GitHubIssuesPanel-De43YQ_C.js} +1 -1
  51. package/web-app/dist/assets/{GitHubPRsPanel-BCzipPWD.js → GitHubPRsPanel-7g7ZTnkx.js} +1 -1
  52. package/web-app/dist/assets/{HomePage-CIdnkzaN.js → HomePage-CcNA24ny.js} +2 -2
  53. package/web-app/dist/assets/{LoginPage-D-178nTT.js → LoginPage-BYjlb-IK.js} +1 -1
  54. package/web-app/dist/assets/{MagicPage-BmHuzMT8.js → MagicPage-C0Dwt9Na.js} +1 -1
  55. package/web-app/dist/assets/{MetricsPage-CPCiyDFn.js → MetricsPage-jDGmdcgg.js} +1 -1
  56. package/web-app/dist/assets/{NotFoundPage-B2T6n1gx.js → NotFoundPage-C_ZO8tLE.js} +1 -1
  57. package/web-app/dist/assets/{ProjectPage-DQmpFhb0.js → ProjectPage-B5wnurfQ.js} +3 -3
  58. package/web-app/dist/assets/{ProjectsPage-BmugzFEd.js → ProjectsPage--EyKt5-D.js} +1 -1
  59. package/web-app/dist/assets/{SettingsPage-ZB-RrmgM.js → SettingsPage-cB0lnfti.js} +1 -1
  60. package/web-app/dist/assets/{ShowcasePage-BAhwHt4_.js → ShowcasePage-Dl16FDkK.js} +1 -1
  61. package/web-app/dist/assets/{SystemSettingsPage-Bt7sf2Vu.js → SystemSettingsPage-BmAXd4V2.js} +1 -1
  62. package/web-app/dist/assets/{TeamsPage-B-v5j_8q.js → TeamsPage-BFkWVOZe.js} +1 -1
  63. package/web-app/dist/assets/{TemplatesPage-bdqUL-LG.js → TemplatesPage-Bf2kmEUM.js} +1 -1
  64. package/web-app/dist/assets/{TerminalOutput-CRHEXBwS.js → TerminalOutput-BItv9Wa6.js} +1 -1
  65. package/web-app/dist/assets/{activity-C-ROkmXt.js → activity-Df2Awg6I.js} +1 -1
  66. package/web-app/dist/assets/{bell-DbiCBxQA.js → bell-D-1ztTsJ.js} +1 -1
  67. package/web-app/dist/assets/{bot-DsIgE_qr.js → bot-A4qgZsKi.js} +1 -1
  68. package/web-app/dist/assets/{check-CkAdmHWP.js → check-BPfFpF0c.js} +1 -1
  69. package/web-app/dist/assets/{chevron-left-CYTLivAo.js → chevron-left-CfSJvmDi.js} +1 -1
  70. package/web-app/dist/assets/{circle-alert-BVIcAsFJ.js → circle-alert-wx2d1dzb.js} +1 -1
  71. package/web-app/dist/assets/{clock-Dg3Rq0go.js → clock-pQMai6l-.js} +1 -1
  72. package/web-app/dist/assets/{cloud-CenrqHFl.js → cloud-DWL470r6.js} +1 -1
  73. package/web-app/dist/assets/{code-xml-C2mvfoug.js → code-xml-BaEQpQLZ.js} +1 -1
  74. package/web-app/dist/assets/{database-BKa0_eCb.js → database-h-N7WkQN.js} +1 -1
  75. package/web-app/dist/assets/{dollar-sign-DfelUK1S.js → dollar-sign-B5oYiD27.js} +1 -1
  76. package/web-app/dist/assets/{file-code-corner-CZKWoHVg.js → file-code-corner-fbTtTy0o.js} +1 -1
  77. package/web-app/dist/assets/{file-plus-CZ-m1zMX.js → file-plus-PaF6HMo8.js} +1 -1
  78. package/web-app/dist/assets/{globe-S0kv_GwC.js → globe-Ct-GlWID.js} +1 -1
  79. package/web-app/dist/assets/{hammer-BW1MDdu1.js → hammer-C1oZFX51.js} +1 -1
  80. package/web-app/dist/assets/{index-ACEYsTc2.js → index-C2egrRhz.js} +3 -3
  81. package/web-app/dist/assets/{layers-BrXHYZ-L.js → layers-Dt5ZK4v2.js} +1 -1
  82. package/web-app/dist/assets/{loader-circle-DOdhKFx0.js → loader-circle-DrpMziic.js} +1 -1
  83. package/web-app/dist/assets/{lock-BcFTyg8J.js → lock-DOez8MmK.js} +1 -1
  84. package/web-app/dist/assets/{package-D34cMVQG.js → package-ClGIkerr.js} +1 -1
  85. package/web-app/dist/assets/{plus-CEaMeBxL.js → plus-C004i9ua.js} +1 -1
  86. package/web-app/dist/assets/{refresh-cw-D2Rzxr5U.js → refresh-cw-Ddfk-7Sy.js} +1 -1
  87. package/web-app/dist/assets/{rotate-ccw-C_-EjRLh.js → rotate-ccw-Bu1KgDjS.js} +1 -1
  88. package/web-app/dist/assets/{scroll-text-BSSCMI_c.js → scroll-text-3q1lW5pm.js} +1 -1
  89. package/web-app/dist/assets/{server-BKvLonWt.js → server-pd6x4HW1.js} +1 -1
  90. package/web-app/dist/assets/{shield-alert-DbbQbPdW.js → shield-alert-B9jGJepT.js} +1 -1
  91. package/web-app/dist/assets/{thumbs-up-NoBD1fUR.js → thumbs-up-Cdcc9nQf.js} +1 -1
  92. package/web-app/dist/assets/{trash-2-BXz8rAyd.js → trash-2-foSojXHK.js} +1 -1
  93. package/web-app/dist/assets/{trending-down-D8u535Re.js → trending-down-CTTiqlJ1.js} +1 -1
  94. package/web-app/dist/assets/{trending-up-q8k3kgiy.js → trending-up-B_MgmRAx.js} +1 -1
  95. package/web-app/dist/assets/{upload-Cp7_zeBI.js → upload-g6BfALzG.js} +1 -1
  96. package/web-app/dist/assets/{usePolling-CTHVdcuX.js → usePolling-DrHwvzuh.js} +1 -1
  97. package/web-app/dist/assets/{user-DF7bfSf_.js → user-COp75m7l.js} +1 -1
  98. package/web-app/dist/index.html +1 -1
@@ -183,7 +183,7 @@ def _cost_value(proof):
183
183
 
184
184
 
185
185
  def _intervention_detail(proof):
186
- """(by_type, waited_s) from proof.interventions_detail; None when absent."""
186
+ """(by_type, waited_s, active_s) from proof.interventions_detail; None when absent."""
187
187
  d = _obj(proof.get("interventions_detail"))
188
188
  bt = _obj(d.get("by_type"))
189
189
  out = {}
@@ -198,7 +198,9 @@ def _intervention_detail(proof):
198
198
  any_key = True
199
199
  w = d.get("waited_s")
200
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
201
+ a = d.get("active_s")
202
+ act = a if isinstance(a, int) and not isinstance(a, bool) and a >= 0 else None
203
+ return (out if any_key else None), ws, act
202
204
 
203
205
 
204
206
  def _verdict_evidence(proof):
@@ -229,13 +231,14 @@ def _load_runs(loki_dir):
229
231
  # Malformed / partial proof: skip, do not fail the trajectory.
230
232
  continue
231
233
  verdict, false_verified = _verdict_evidence(proof)
232
- by_type, waited_s = _intervention_detail(proof)
234
+ by_type, waited_s, active_s = _intervention_detail(proof)
233
235
  runs.append({
234
236
  "verdict": verdict,
235
237
  "false_verified": false_verified,
236
238
  "cost_usd": _cost_value(proof),
237
239
  "interventions_by_type": by_type,
238
240
  "waited_s": waited_s,
241
+ "active_s": active_s,
239
242
  "run_id": str(proof.get("run_id") or name),
240
243
  "generated_at": proof.get("generated_at"),
241
244
  "council_pass_rate": _council_pass_value(proof.get("council")),
@@ -343,6 +346,7 @@ def outcome_effort(series):
343
346
  by_type = {k: sum(r["interventions_by_type"][k] for r in with_detail) for k in _IV_TYPES}
344
347
  all_iv = bool(series) and all(r["interventions"] is not None for r in series)
345
348
  all_waited = bool(series) and all(r["waited_s"] is not None for r in series)
349
+ all_active = bool(series) and all(r["active_s"] is not None for r in series)
346
350
  return {
347
351
  "runs": len(series),
348
352
  "accepted": accepted,
@@ -356,6 +360,8 @@ def outcome_effort(series):
356
360
  round(sum(r["interventions"] for r in series) / accepted, 4) if accepted and all_iv else None),
357
361
  "waited_min_per_accepted": (
358
362
  round(sum(r["waited_s"] for r in series) / 60.0 / accepted, 1) if accepted and all_waited else None),
363
+ "active_min_per_accepted": (
364
+ round(sum(r["active_s"] for r in series) / 60.0 / accepted, 1) if accepted and all_active else None),
359
365
  }
360
366
 
361
367
 
@@ -376,7 +382,9 @@ def _fmt_outcome_effort(e):
376
382
  lines.append(" Interventions per accepted: %s" % ("n/a" if ipa is None else ipa))
377
383
  w = e.get("waited_min_per_accepted")
378
384
  lines.append(" Minutes per accepted: %s" % (
379
- "n/a" if w is None else "%s (waited, upper bound; active minutes are not measured)" % w))
385
+ "n/a" if w is None else "%s (waited, upper bound)" % w))
386
+ am = e.get("active_min_per_accepted")
387
+ lines.append(" Active minutes per accepted: %s" % ("n/a (not measured)" if am is None else am))
380
388
  return lines
381
389
 
382
390
 
@@ -397,6 +405,7 @@ def compute_trajectory(loki_dir):
397
405
  "cost_usd": r["cost_usd"],
398
406
  "interventions_by_type": r["interventions_by_type"],
399
407
  "waited_s": r["waited_s"],
408
+ "active_s": r["active_s"],
400
409
  "verdict": r["verdict"],
401
410
  "false_verified": r["false_verified"],
402
411
  })
@@ -373,8 +373,10 @@ loki_council_dispatch_agents() {
373
373
  # (completion-council.sh:2792). Fail-closed: a hung or timed-out council can
374
374
  # never become a false COMPLETE; it always degrades to the heuristic path
375
375
  # (which has its own timeout + conservative defaults).
376
+ _LOKI_PERM_ARGV=(--dangerously-skip-permissions)
377
+ ! type _loki_claude_permission_argv >/dev/null 2>&1 || _loki_claude_permission_argv
376
378
  response=$(timeout "${LOKI_COUNCIL_REVIEW_TIMEOUT:-600}" \
377
- env CAVEMAN_DEFAULT_MODE=off claude --dangerously-skip-permissions \
379
+ env CAVEMAN_DEFAULT_MODE=off claude "${_LOKI_PERM_ARGV[@]}" \
378
380
  -p "$prompt" \
379
381
  --agents "$agents_json" \
380
382
  --json-schema "$schema_content" 2>"$stderr_log") || rc=$?
package/autonomy/loki CHANGED
@@ -132,6 +132,34 @@ _loki_caveman_narration_level() {
132
132
  fi
133
133
  }
134
134
 
135
+ # P12-26b: permission flags for the migrate/doc/heal claude launches. Lazily sources
136
+ # claude-flags.sh (like the caveman wrapper above) and fills _LOKI_PERM_ARGV; unset
137
+ # LOKI_PERMISSION_POLICY keeps --dangerously-skip-permissions byte-identically.
138
+ _loki_perm_argv_for() {
139
+ _LOKI_PERM_ARGV=(--dangerously-skip-permissions)
140
+ # Unset policy: return before sourcing anything (claude-flags.sh exports caveman vars).
141
+ [ "${LOKI_PERMISSION_POLICY:-}" = "compiled" ] || return 0
142
+ _LOKI_PERM_ARGV=(--permission-mode plan)
143
+ local _pa_lib="${_LOKI_SCRIPT_DIR}/lib/claude-flags.sh" _pa_out _pa_a
144
+ if [ ! -f "$_pa_lib" ]; then
145
+ echo "[loki] WARNING: LOKI_PERMISSION_POLICY=compiled: helper missing; using most restrictive mode (plan)" >&2
146
+ return 0
147
+ fi
148
+ # Subshell so the helper's top-level exports never leak into this shell.
149
+ _pa_out="$(
150
+ LOKI_PROJECT_DIR="${1:-${LOKI_PROJECT_DIR:-$PWD}}"
151
+ # shellcheck source=autonomy/lib/claude-flags.sh
152
+ . "$_pa_lib" >/dev/null 2>&1 || exit 1
153
+ declare -F _loki_claude_permission_argv >/dev/null 2>&1 || exit 1
154
+ _loki_claude_permission_argv
155
+ printf '%s\n' "${_LOKI_PERM_ARGV[@]}"
156
+ )" || return 0
157
+ [ -n "$_pa_out" ] || return 0
158
+ _LOKI_PERM_ARGV=()
159
+ while IFS= read -r _pa_a; do _LOKI_PERM_ARGV+=("$_pa_a"); done <<< "$_pa_out"
160
+ return 0
161
+ }
162
+
135
163
  # loki edit: one-pass small change fast path (functions-only, self-guarded).
136
164
  if [ -f "$_LOKI_SCRIPT_DIR/edit.sh" ]; then
137
165
  # shellcheck source=edit.sh
@@ -1797,7 +1825,7 @@ show_help_all() {
1797
1825
  echo " stop Stop execution immediately"
1798
1826
  echo " pause Pause after current session"
1799
1827
  echo " resume Resume a paused or interrupted run (after Ctrl-C or a crash)"
1800
- echo " steer \"<note>\" Nudge a running build (needs LOKI_PROMPT_INJECTION=1)"
1828
+ echo " steer \"<note>\" Nudge a running build (Loki 10 engine; legacy loop needs LOKI_PROMPT_INJECTION=true)"
1801
1829
  echo ""
1802
1830
  echo "Verify / trust:"
1803
1831
  echo " verify [base] Deterministic PR verification (CI-gate exit codes)"
@@ -1842,7 +1870,7 @@ show_help_all() {
1842
1870
  echo ""
1843
1871
  echo "Options for 'start':"
1844
1872
  echo " --provider NAME AI provider: claude (default), codex, cline, aider, opencode"
1845
- echo " --model ID Model tier or route (e.g. ollama/qwen3-coder)"
1873
+ echo " (model route: set LOKI_MODEL_ROUTE, e.g. ollama/qwen3-coder; there is no --model flag)"
1846
1874
  echo " --simple Force simple complexity tier (3 phases)"
1847
1875
  echo " --complex Force complex complexity tier (8 phases)"
1848
1876
  echo " --no-dashboard Disable web dashboard"
@@ -2519,6 +2547,37 @@ _loki_start_handoff() {
2519
2547
  }
2520
2548
 
2521
2549
  # Start Loki Mode
2550
+ # Every value-taking cmd_start flag (its value is never brief text or a run id).
2551
+ # The one list: the --workspace brief scan and the --resume scan both use it.
2552
+ # It also names value flags that are not cmd_start's own, so a scan never
2553
+ # mistakes their value for a brief or run id. --model is honored nowhere (model
2554
+ # routing is LOKI_MODEL_ROUTE); --max-cost only via the engine10 rewrite.
2555
+ _loki_start_flag_takes_value() {
2556
+ case "$1" in
2557
+ --provider|--brief|--budget|--prd|--issue|--isolation|--aider-model|--aider-flags|--cline-model|--compliance|--max-duration|--log-level|--bmad-project|--openspec|--mirofish-rounds|--mirofish-timeout|--output|--config|--vars|--env-file|--remote|--attempts|--mirofish-docker|--spec|--model|--max-cost|--max-wait|--base-wait|--max-iterations|--max-retries|--budget-limit|--session-model|--completion-promise) return 0 ;;
2558
+ esac
2559
+ return 1
2560
+ }
2561
+
2562
+ # Optional-value cmd_start flags (--mirofish [URL]). The one decision on whether
2563
+ # the NEXT token is the flag's value: non-empty and not another --flag. The
2564
+ # cmd_start --mirofish arm and both scans use it, so they cannot disagree.
2565
+ _loki_start_flag_optional_value() { [ "$1" = "--mirofish" ]; }
2566
+ _loki_start_optval_consumes() { [[ -n "${1:-}" && "${1:-}" != --* ]]; }
2567
+
2568
+ # One exit-code mapping for an engine10 resume, shared by cmd_resume and
2569
+ # `loki start --resume`. 0 and 4 (phase reported) -> 0, 5 (publish failed) -> 1.
2570
+ # Sets _LOKI_E10_RC and returns 0 when handled; any other code returns 1 with
2571
+ # _LOKI_E10_RC set to the raw code (cmd_resume falls through, start returns it).
2572
+ _loki_e10_resume_map() {
2573
+ case "$1" in
2574
+ 0 | 4) _LOKI_E10_RC=0 ;;
2575
+ 5) _LOKI_E10_RC=1 ;;
2576
+ *) _LOKI_E10_RC="$1"; return 1 ;;
2577
+ esac
2578
+ return 0
2579
+ }
2580
+
2522
2581
  cmd_start() {
2523
2582
  # First-run welcome (once): open the branded welcome page on the very
2524
2583
  # first start. Writes ~/.loki/.welcomed and never repeats; non-blocking,
@@ -2584,13 +2643,13 @@ cmd_start() {
2584
2643
  local _ps_skip=0
2585
2644
  for _ps_a in ${_ps_rest[@]+"${_ps_rest[@]}"}; do
2586
2645
  if [ "$_ps_skip" = 1 ]; then _ps_skip=0; continue; fi
2646
+ if [ "$_ps_skip" = 3 ]; then _ps_skip=0; if _loki_start_optval_consumes "$_ps_a"; then continue; fi; fi
2587
2647
  if [ "$_ps_skip" = 2 ]; then _ps_skip=0; _ps_brief+=("$_ps_a"); continue; fi
2588
2648
  case "$_ps_a" in
2589
2649
  --brief=*) _ps_brief+=("${_ps_a#*=}") ;;
2590
2650
  --brief) _ps_skip=2 ;;
2591
2651
  # Every value-taking cmd_start flag: its value is never brief text.
2592
- --provider|--budget|--prd|--issue|--model|--max-cost|--isolation|--aider-model|--aider-flags|--cline-model|--compliance|--max-duration|--log-level|--bmad-project|--openspec|--mirofish-rounds|--mirofish-timeout|--output|--config|--vars|--env-file|--remote|--attempts|--max-wait) _ps_skip=1 ;;
2593
- -*) ;;
2652
+ -*) if _loki_start_flag_takes_value "$_ps_a"; then _ps_skip=1; elif _loki_start_flag_optional_value "$_ps_a"; then _ps_skip=3; fi ;;
2594
2653
  *) _ps_brief+=("$_ps_a") ;;
2595
2654
  esac
2596
2655
  done
@@ -2615,6 +2674,34 @@ cmd_start() {
2615
2674
  cmd_deploy "${_ps_dargs[@]}"
2616
2675
  return $?
2617
2676
  fi
2677
+ # P12-07b: a bare `loki start --resume [run-id]` goes to the same engine10
2678
+ # resume path as `loki resume` (util/run_outputs.ts resumeMain), never into a
2679
+ # new build. Exit codes go through _loki_e10_resume_map (0 and 4 -> 0, 5 -> 1); any other code, such as 3 for nothing to resume, is returned as is.
2680
+ local _ps_resume=0 _ps_resume_args=() _ps_rskip=0
2681
+ for _ps_a in ${_ps_rest[@]+"${_ps_rest[@]}"}; do
2682
+ if [ "$_ps_rskip" = 1 ]; then _ps_rskip=0; continue; fi
2683
+ if [ "$_ps_rskip" = 3 ]; then _ps_rskip=0; if _loki_start_optval_consumes "$_ps_a"; then continue; fi; fi
2684
+ case "$_ps_a" in
2685
+ --resume) _ps_resume=1 ;;
2686
+ --*=*) ;;
2687
+ -*) if _loki_start_flag_takes_value "$_ps_a"; then _ps_rskip=1; elif _loki_start_flag_optional_value "$_ps_a"; then _ps_rskip=3; fi ;;
2688
+ *) _ps_resume_args+=("$_ps_a") ;;
2689
+ esac
2690
+ done
2691
+ if [ "$_ps_resume" = 1 ]; then
2692
+ local _ps_rrc=0
2693
+ for _ps_a in ${_ps_resume_args[@]+"${_ps_resume_args[@]}"}; do
2694
+ case "$_ps_a" in
2695
+ e10-[A-Za-z0-9-]*) ;;
2696
+ *)
2697
+ echo -e "${RED}--resume resumes a sealed run; it takes an optional run id, not a task. Run \`loki start <task>\` for a new run.${NC}" >&2
2698
+ return 2 ;;
2699
+ esac
2700
+ done
2701
+ ( _loki_bun_delegate engine10 resume ${_ps_resume_args[@]+"${_ps_resume_args[@]}"} ) || _ps_rrc=$?
2702
+ _loki_e10_resume_map "$_ps_rrc" || true
2703
+ return "$_LOKI_E10_RC"
2704
+ fi
2618
2705
  set -- ${_ps_rest[@]+"${_ps_rest[@]}"}
2619
2706
 
2620
2707
  local args=()
@@ -2675,7 +2762,7 @@ cmd_start() {
2675
2762
  echo " Token from LOKI_REMOTE_TOKEN / LOKI_REMOTE_TOKEN_FILE"
2676
2763
  echo " (never an argument). Env: LOKI_REMOTE_URL."
2677
2764
  echo " --provider NAME AI provider: claude (default), codex, cline, aider, opencode"
2678
- echo " --model NAME Model or route override (for example ollama/qwen3-coder)"
2765
+ echo " (model route: set LOKI_MODEL_ROUTE, for example ollama/qwen3-coder; no --model flag)"
2679
2766
  echo " --allow-haiku Enable Haiku model for the fast tier (default: disabled)"
2680
2767
  echo " --simple Force simple complexity tier (3 phases)"
2681
2768
  echo " --complex Force complex complexity tier (8 phases)"
@@ -2720,6 +2807,7 @@ cmd_start() {
2720
2807
  echo " --pr Worktree + auto-create PR"
2721
2808
  echo " --ship Worktree + PR + auto-merge"
2722
2809
  echo " --detach, -d Run in background (implies --worktree)"
2810
+ echo " --resume [RUN_ID] Resume an interrupted run at the stage it stopped in"
2723
2811
  echo ""
2724
2812
  echo "Preview without spending (any input mode):"
2725
2813
  echo " loki plan <PRD> Analyze a spec WITHOUT executing: complexity,"
@@ -3024,7 +3112,7 @@ cmd_start() {
3024
3112
  shift
3025
3113
  ;;
3026
3114
  --mirofish)
3027
- if [[ -n "${2:-}" ]] && [[ "${2:-}" != --* ]]; then
3115
+ if _loki_start_optval_consumes "${2:-}"; then
3028
3116
  mirofish_url="$2"
3029
3117
  shift 2
3030
3118
  else
@@ -5071,15 +5159,19 @@ cmd_pause() {
5071
5159
  # RUN-25 iter 22 (Wave D #3): `loki steer "<note>"` writes an operator directive
5072
5160
  # to .loki/HUMAN_INPUT.md. engine10 reads it before the next plan or implement
5073
5161
  # session (default on; only LOKI_PROMPT_INJECTION=0/false opts out, steer.ts).
5074
- # The legacy loop injects it only when LOKI_PROMPT_INJECTION=1.
5162
+ # The legacy loop injects it only when LOKI_PROMPT_INJECTION=true.
5075
5163
  # Each steer is a numbered revision: `## Operator directive rev N (ts) actor=A source=S`.
5076
5164
  # engine10 (util/steer.ts) archives the file and appends the revisions to
5077
5165
  # .loki/intent-revisions.jsonl. Actor and source come from LOKI_STEER_ACTOR
5078
5166
  # (default: git user.name, then $USER) and LOKI_STEER_SOURCE (cli, chat or an
5079
5167
  # https comment URL; default cli). Both are reduced to a safe character set.
5080
5168
  cmd_steer() {
5081
- case "${1:-}" in
5082
- --help|-h|help|"")
5169
+ # "--" ends options so guidance beginning with "-" is data, not a flag
5170
+ local _eoo=0
5171
+ if [ "${1:-}" = "--" ]; then _eoo=1; shift; fi
5172
+ case "$_eoo:${1:-}" in
5173
+ 1:?*) ;;
5174
+ *:--help|*:-h|*:help|*:"")
5083
5175
  echo -e "${BOLD}Loki Mode -- steer the running build${NC}"
5084
5176
  echo ""
5085
5177
  echo "Usage: loki steer \"<guidance>\""
@@ -5087,11 +5179,13 @@ cmd_steer() {
5087
5179
  echo "Writes your guidance to .loki/HUMAN_INPUT.md as a numbered revision"
5088
5180
  echo "(rev N, actor, source). The Loki 10 engine applies it once, before the"
5089
5181
  echo "next plan or implement session, and records it in .loki/intent-revisions.jsonl."
5182
+ echo "A steer that arrives after the Wall (the frozen acceptance tests) was authored"
5183
+ echo "makes Seal refuse the run; rerun the task with the new intent included."
5090
5184
  echo ""
5091
5185
  echo " loki steer \"focus on the auth flow, skip the docs for now\""
5092
5186
  echo ""
5093
5187
  echo "Opt out with LOKI_PROMPT_INJECTION=0. The legacy loop reads the note"
5094
- echo "only when LOKI_PROMPT_INJECTION=1."
5188
+ echo "only when LOKI_PROMPT_INJECTION=true."
5095
5189
  [ -z "${1:-}" ] && return 2
5096
5190
  return 0
5097
5191
  ;;
@@ -5127,6 +5221,7 @@ cmd_steer() {
5127
5221
  echo -e "${DIM}The Loki 10 engine applies it before the next plan or implement session.${NC}"
5128
5222
  # Same text the issue comment ack posts (trigger-server.py steer_ack_text).
5129
5223
  echo "Steer rev $rev received: applies at next plan/implement stage."
5224
+ echo "If the Wall is already frozen, Seal will refuse this run: rerun with the new intent."
5130
5225
  ;;
5131
5226
  esac
5132
5227
  return 0
@@ -5289,10 +5384,7 @@ cmd_resume() {
5289
5384
  if ls "$LOKI_DIR"/runs/e10-*/events.jsonl >/dev/null 2>&1; then
5290
5385
  local _e10_rc=0
5291
5386
  ( _loki_bun_delegate engine10 resume "$@" ) || _e10_rc=$?
5292
- case "$_e10_rc" in
5293
- 0 | 4) return 0 ;;
5294
- 5) return 1 ;;
5295
- esac
5387
+ if _loki_e10_resume_map "$_e10_rc"; then return "$_LOKI_E10_RC"; fi
5296
5388
  fi
5297
5389
 
5298
5390
  # Check if there is anything to resume before touching signal files
@@ -6290,7 +6382,7 @@ if _unc and os.path.exists(_unc):
6290
6382
  except Exception:
6291
6383
  pass
6292
6384
  print(" Unstick it : give the loop a concrete next step:")
6293
- print(" loki steer \"<what to try next>\" (needs LOKI_PROMPT_INJECTION=1)")
6385
+ print(" loki steer \"<what to try next>\" (Loki 10 engine; legacy loop needs LOKI_PROMPT_INJECTION=true)")
6294
6386
  print(" or narrow the spec / fix the blocker, then loki resume.")
6295
6387
 
6296
6388
  # Surface the structured per-iteration error record, if one exists at
@@ -18014,9 +18106,10 @@ Tasks:
18014
18106
  # pre-caveman. No-op when caveman is absent.
18015
18107
  local _loki_mig_cm=""
18016
18108
  _loki_mig_cm="$(_loki_caveman_narration_level)"
18109
+ _loki_perm_argv_for "$codebase_path"
18017
18110
  case "$provider_name" in
18018
18111
  claude)
18019
- { (cd "$codebase_path" && if [ -n "$_loki_mig_cm" ]; then CAVEMAN_DEFAULT_MODE="$_loki_mig_cm" claude --dangerously-skip-permissions -p "$phase_prompt" --output-format stream-json --verbose 2>&1; else claude --dangerously-skip-permissions -p "$phase_prompt" --output-format stream-json --verbose 2>&1; fi) | \
18112
+ { (cd "$codebase_path" && if [ -n "$_loki_mig_cm" ]; then CAVEMAN_DEFAULT_MODE="$_loki_mig_cm" claude "${_LOKI_PERM_ARGV[@]}" -p "$phase_prompt" --output-format stream-json --verbose 2>&1; else claude "${_LOKI_PERM_ARGV[@]}" -p "$phase_prompt" --output-format stream-json --verbose 2>&1; fi) | \
18020
18113
  while IFS= read -r line; do
18021
18114
  # Extract text from stream-json
18022
18115
  if echo "$line" | python3 -c "
@@ -18191,9 +18284,10 @@ IMPORTANT RULES:
18191
18284
  # branch, byte-identical to pre-caveman (empty mode is NOT inert). No-op absent.
18192
18285
  local _loki_doc_cm=""
18193
18286
  _loki_doc_cm="$(_loki_caveman_narration_level)"
18287
+ _loki_perm_argv_for "$codebase_path"
18194
18288
  case "$provider_name" in
18195
18289
  claude)
18196
- { (cd "$codebase_path" && if [ -n "$_loki_doc_cm" ]; then CAVEMAN_DEFAULT_MODE="$_loki_doc_cm" claude --dangerously-skip-permissions -p "$doc_prompt" --output-format stream-json --verbose 2>&1; else claude --dangerously-skip-permissions -p "$doc_prompt" --output-format stream-json --verbose 2>&1; fi) | \
18290
+ { (cd "$codebase_path" && if [ -n "$_loki_doc_cm" ]; then CAVEMAN_DEFAULT_MODE="$_loki_doc_cm" claude "${_LOKI_PERM_ARGV[@]}" -p "$doc_prompt" --output-format stream-json --verbose 2>&1; else claude "${_LOKI_PERM_ARGV[@]}" -p "$doc_prompt" --output-format stream-json --verbose 2>&1; fi) | \
18197
18291
  while IFS= read -r line; do
18198
18292
  if echo "$line" | python3 -c "
18199
18293
  import sys, json
@@ -19256,7 +19350,8 @@ Begin healing now. Follow the RARV cycle for each action."
19256
19350
  _loki_heal_cm="$(_loki_caveman_narration_level)"
19257
19351
  case "$provider" in
19258
19352
  claude)
19259
- local run_args=(--dangerously-skip-permissions -p "$heal_prompt" --output-format stream-json --verbose)
19353
+ _loki_perm_argv_for "$codebase_path"
19354
+ local run_args=("${_LOKI_PERM_ARGV[@]}" -p "$heal_prompt" --output-format stream-json --verbose)
19260
19355
  (cd "$codebase_path" && if [ -n "$_loki_heal_cm" ]; then CAVEMAN_DEFAULT_MODE="$_loki_heal_cm" claude "${run_args[@]}" 2>&1; else claude "${run_args[@]}" 2>&1; fi) | \
19261
19356
  while IFS= read -r line; do
19262
19357
  if echo "$line" | python3 -c "
@@ -26031,9 +26126,11 @@ _pr_record_merge() {
26031
26126
 
26032
26127
  # Shared merge gate. One PR (number or branch) merges only when it is not a
26033
26128
  # draft, `gh pr checks` exits 0 after polling pending (rc=8), and, with
26034
- # --require-verified, this run's proof headline is VERIFIED (missing proof or
26035
- # an unreadable headline is never VERIFIED). On refusal it prints the exact
26036
- # blocking gate ("gate: draft|verdict|checks") and leaves the PR open.
26129
+ # --require-verified, every clean proof recorded for the PR's head commit
26130
+ # (facts.git.head_sha == headRefOid, dirty-tree proofs ignored) says VERIFIED; a stale or missing proof
26131
+ # refuses ("gate: head"). The merge passes --match-head-commit so a push after
26132
+ # the gate cannot land unjudged. On refusal it prints the exact blocking gate
26133
+ # ("gate: draft|head|verdict|checks") and leaves the PR open.
26037
26134
  # Usage: _merge_pr_gated <pr> [--require-verified] [--delete-branch]
26038
26135
  _merge_pr_gated() {
26039
26136
  local pr="$1" need_verified=false margs=(--squash) rc d
@@ -26052,14 +26149,59 @@ _merge_pr_gated() {
26052
26149
  _pr_record_merge "$pr" "blocked:draft" "draft"
26053
26150
  return 1
26054
26151
  fi
26152
+ # Pin the merge to the exact head the checks and proof are about to judge.
26153
+ local head=""
26154
+ head="$(gh pr view "$pr" --json headRefOid -q .headRefOid 2>/dev/null)" || head=""
26155
+ case "$head" in
26156
+ *[!0-9a-f]*) head="" ;;
26157
+ esac
26158
+ if [ "${#head}" != "40" ] && [ "${#head}" != "64" ]; then
26159
+ echo "$pr: not merged, gate: head (PR head commit is unreadable); PR left open" >&2
26160
+ _pr_record_merge "$pr" "blocked:head" "head"
26161
+ return 1
26162
+ fi
26055
26163
  if $need_verified; then
26056
- local pid="" headline=""
26164
+ local pid="" headline="" pj="" ph="" cands=() dirty_n=0 unknown_n=0
26165
+ # Resolve the proof recorded for THIS head: the run pointer first, then
26166
+ # any proof in .loki/proofs whose facts.git.head_sha equals the PR head.
26057
26167
  [ -f .loki/state/last-proof-id.txt ] && pid="$(cat .loki/state/last-proof-id.txt 2>/dev/null)"
26058
- if [ -n "$pid" ] && [ -f ".loki/proofs/${pid}/proof.json" ] && command -v jq >/dev/null 2>&1; then
26059
- headline="$(jq -r '.honesty.headline // empty' ".loki/proofs/${pid}/proof.json" 2>/dev/null)"
26168
+ [ -z "$pid" ] || cands+=(".loki/proofs/${pid}/proof.json")
26169
+ cands+=(.loki/proofs/*/proof.json)
26170
+ if command -v jq >/dev/null 2>&1; then
26171
+ # Every clean proof for this head must agree: an older VERIFIED
26172
+ # never overrides a newer NOT_PROVEN. A proof taken on a dirty tree
26173
+ # (facts.git.dirty anything but literal false, incl. missing/null; .loki/ is excluded from that flag) judged
26174
+ # bytes other than the commit and is ignored.
26175
+ local hl="" disagree=false
26176
+ for pj in "${cands[@]}"; do
26177
+ [ -f "$pj" ] || continue
26178
+ ph="$(jq -r '.facts.git.head_sha // empty' "$pj" 2>/dev/null)"
26179
+ [ "$ph" = "$head" ] || continue
26180
+ case "$(jq -r 'if .facts.git.dirty == false then "clean" elif .facts.git.dirty == true then "dirty" else "unknown" end' "$pj" 2>/dev/null)" in
26181
+ clean) ;;
26182
+ dirty) dirty_n=$((dirty_n + 1)); continue ;;
26183
+ *) unknown_n=$((unknown_n + 1)); continue ;;
26184
+ esac
26185
+ hl="$(jq -r '.honesty.headline // empty' "$pj" 2>/dev/null)"
26186
+ [ -n "$hl" ] || hl="UNREADABLE"
26187
+ if [ -z "$headline" ]; then headline="$hl"; elif [ "$headline" != "$hl" ]; then disagree=true; fi
26188
+ done
26189
+ if $disagree; then
26190
+ echo "$pr: not merged, gate: verdict (proofs for PR head ${head:0:12} disagree); PR left open" >&2
26191
+ _pr_record_merge "$pr" "blocked:verdict" "verdict"
26192
+ return 1
26193
+ fi
26194
+ fi
26195
+ if [ -z "$headline" ]; then
26196
+ echo "$pr: not merged, gate: head (no clean proof recorded for PR head ${head:0:12}; proof is stale, dirty ($dirty_n), dirty-unknown ($unknown_n) or missing); PR left open" >&2
26197
+ if [ "$unknown_n" -gt 0 ]; then
26198
+ echo "$pr: $unknown_n proof(s) for this head have an unknown dirty state (the proof predates the dirty flag, or git could not read the tree). Commit or stash local changes, then re-run 'loki start ...' on a clean tree for this head to record a clean proof." >&2
26199
+ fi
26200
+ _pr_record_merge "$pr" "blocked:head" "head"
26201
+ return 1
26060
26202
  fi
26061
26203
  if [ "$headline" != "VERIFIED" ]; then
26062
- echo "$pr: not merged, gate: verdict (run verdict is '${headline:-missing}', need VERIFIED); PR left open" >&2
26204
+ echo "$pr: not merged, gate: verdict (run verdict is '${headline}', need VERIFIED); PR left open" >&2
26063
26205
  _pr_record_merge "$pr" "blocked:verdict" "verdict"
26064
26206
  return 1
26065
26207
  fi
@@ -26074,7 +26216,14 @@ _merge_pr_gated() {
26074
26216
  esac
26075
26217
  return 1
26076
26218
  fi
26077
- if ! gh pr merge "$pr" "${margs[@]}" >/dev/null 2>&1; then
26219
+ if ! gh pr merge "$pr" "${margs[@]}" --match-head-commit "$head" >/dev/null 2>&1; then
26220
+ local head_now=""
26221
+ head_now="$(gh pr view "$pr" --json headRefOid -q .headRefOid 2>/dev/null)" || head_now=""
26222
+ if [ -n "$head_now" ] && [ "$head_now" != "$head" ]; then
26223
+ echo "$pr: not merged, gate: head (PR head moved from ${head:0:12} to ${head_now:0:12} during checks); PR left open" >&2
26224
+ _pr_record_merge "$pr" "blocked:head" "head" "passing"
26225
+ return 1
26226
+ fi
26078
26227
  echo "$pr: not merged, gate: merge (gh pr merge failed); PR left open" >&2
26079
26228
  _pr_record_merge "$pr" "blocked:merge" "merge" "passing"
26080
26229
  return 1
@@ -824,12 +824,31 @@ _qs_classify_invoke() {
824
824
  local to=""
825
825
  if command -v timeout >/dev/null 2>&1; then to="timeout"
826
826
  elif command -v gtimeout >/dev/null 2>&1; then to="gtimeout"; fi
827
+ _LOKI_PERM_ARGV=(--dangerously-skip-permissions)
828
+ if [ "${LOKI_PERMISSION_POLICY:-}" = "compiled" ]; then
829
+ # P12-26b: nothing on this path sources claude-flags.sh. Compute in a subshell so its
830
+ # top-level exports (caveman vars) never leak; fail closed to plan when unavailable.
831
+ local _qs_flags _qs_out _qs_a
832
+ _qs_flags="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)/lib/claude-flags.sh"
833
+ _LOKI_PERM_ARGV=(--permission-mode plan)
834
+ _qs_out="$(
835
+ # shellcheck source=/dev/null
836
+ . "$_qs_flags" >/dev/null 2>&1 || exit 1
837
+ type _loki_claude_permission_argv >/dev/null 2>&1 || exit 1
838
+ _loki_claude_permission_argv
839
+ printf '%s\n' "${_LOKI_PERM_ARGV[@]}"
840
+ )" || _qs_out=""
841
+ if [ -n "$_qs_out" ]; then
842
+ _LOKI_PERM_ARGV=()
843
+ while IFS= read -r _qs_a; do _LOKI_PERM_ARGV+=("$_qs_a"); done <<< "$_qs_out"
844
+ fi
845
+ fi
827
846
  if [ -n "$to" ]; then
828
847
  out=$(CAVEMAN_DEFAULT_MODE=off "$to" "${LOKI_QUICKSTART_CLASSIFY_TIMEOUT}" \
829
- claude --dangerously-skip-permissions -p "$prompt" 2>/dev/null) || rc=$?
848
+ claude "${_LOKI_PERM_ARGV[@]}" -p "$prompt" 2>/dev/null) || rc=$?
830
849
  else
831
850
  out=$(CAVEMAN_DEFAULT_MODE=off \
832
- claude --dangerously-skip-permissions -p "$prompt" 2>/dev/null) || rc=$?
851
+ claude "${_LOKI_PERM_ARGV[@]}" -p "$prompt" 2>/dev/null) || rc=$?
833
852
  fi
834
853
  [ "$rc" -ne 0 ] && return 1
835
854
  [ -z "$out" ] && return 1
package/autonomy/run.sh CHANGED
@@ -6709,13 +6709,15 @@ spawn_worktree_session() {
6709
6709
  # Expand the auto-flag array with the bash-3.2 empty-array guard
6710
6710
  # (${arr[@]+...}) so a bare "${arr[@]}" under set -u does not
6711
6711
  # abort with "unbound variable" when no flags were collected.
6712
+ _LOKI_PERM_ARGV=(--dangerously-skip-permissions)
6713
+ ! type _loki_claude_permission_argv >/dev/null 2>&1 || _loki_claude_permission_argv
6712
6714
  if [ -n "$_loki_wt_cm" ]; then
6713
- CAVEMAN_DEFAULT_MODE="$_loki_wt_cm" claude --dangerously-skip-permissions \
6715
+ CAVEMAN_DEFAULT_MODE="$_loki_wt_cm" claude "${_LOKI_PERM_ARGV[@]}" \
6714
6716
  "${_LOKI_WT_AUTO_FLAGS[@]+"${_LOKI_WT_AUTO_FLAGS[@]}"}" \
6715
6717
  -p "Loki Mode: $task_prompt. Read .loki/CONTINUITY.md for context." \
6716
6718
  >> "$log_file" 2>&1 || _wt_exit=$?
6717
6719
  else
6718
- claude --dangerously-skip-permissions \
6720
+ claude "${_LOKI_PERM_ARGV[@]}" \
6719
6721
  "${_LOKI_WT_AUTO_FLAGS[@]+"${_LOKI_WT_AUTO_FLAGS[@]}"}" \
6720
6722
  -p "Loki Mode: $task_prompt. Read .loki/CONTINUITY.md for context." \
6721
6723
  >> "$log_file" 2>&1 || _wt_exit=$?
@@ -6950,7 +6952,9 @@ Output ONLY the resolved file content with no conflict markers. No explanations.
6950
6952
  # variable (the shell, not the agent, writes the resolved file).
6951
6953
  # It needs no hooks, LSP, CLAUDE.md auto-discovery, or MCP, so
6952
6954
  # --bare is safe and cheaper. Gated + opt-out LOKI_BARE_SUBCALLS=0.
6953
- local _cr_argv=("--dangerously-skip-permissions")
6955
+ _LOKI_PERM_ARGV=(--dangerously-skip-permissions)
6956
+ ! type _loki_claude_permission_argv >/dev/null 2>&1 || _loki_claude_permission_argv
6957
+ local _cr_argv=("${_LOKI_PERM_ARGV[@]}")
6954
6958
  if type loki_subcall_bare_enabled >/dev/null 2>&1 && loki_subcall_bare_enabled; then
6955
6959
  _cr_argv+=("--bare")
6956
6960
  fi
@@ -16282,7 +16286,9 @@ _dispatch_reviewer() {
16282
16286
  # Guardrail, not a sandbox -- echo>/sed -i/etc. remain; the
16283
16287
  # real net is commit-before-agent-wave. Opt out
16284
16288
  # LOKI_REVIEW_TOOL_GUARD=0. See loki_review_guard_denylist.
16285
- local _rv_argv=("--dangerously-skip-permissions")
16289
+ _LOKI_PERM_ARGV=(--dangerously-skip-permissions)
16290
+ ! type _loki_claude_permission_argv >/dev/null 2>&1 || _loki_claude_permission_argv
16291
+ local _rv_argv=("${_LOKI_PERM_ARGV[@]}")
16286
16292
  # OPT-IN advisor pin (v7.104.0, LOKI_ADVISOR_MODEL): reviewers still
16287
16293
  # pass NO --model by default (unchanged behavior: they run on the
16288
16294
  # account default). When the operator/user sets LOKI_ADVISOR_MODEL to
@@ -21352,7 +21358,9 @@ ${_commits}"
21352
21358
  # needed, so --bare is safe and cheaper. Opt out LOKI_BARE_SUBCALLS=0.
21353
21359
  # Always at least --dangerously-skip-permissions, so the array is never
21354
21360
  # empty (empty "${arr[@]}" under set -u errors on bash 3.2, stock macOS).
21355
- local _ic_argv=("--dangerously-skip-permissions")
21361
+ _LOKI_PERM_ARGV=(--dangerously-skip-permissions)
21362
+ ! type _loki_claude_permission_argv >/dev/null 2>&1 || _loki_claude_permission_argv
21363
+ local _ic_argv=("${_LOKI_PERM_ARGV[@]}")
21356
21364
  if type loki_subcall_bare_enabled >/dev/null 2>&1 && loki_subcall_bare_enabled; then
21357
21365
  _ic_argv+=("--bare")
21358
21366
  fi
@@ -25529,7 +25537,9 @@ except Exception as exc:
25529
25537
  # present so the array is never empty (empty "${arr[@]}" under `set -u`
25530
25538
  # is an error on bash 3.2, the stock macOS shell). The autonomy override
25531
25539
  # is appended conditionally.
25532
- local _loki_claude_argv=("--dangerously-skip-permissions" "--model" "$tier_param")
25540
+ _LOKI_PERM_ARGV=(--dangerously-skip-permissions)
25541
+ ! type _loki_claude_permission_argv >/dev/null 2>&1 || _loki_claude_permission_argv
25542
+ local _loki_claude_argv=("${_LOKI_PERM_ARGV[@]}" "--model" "$tier_param")
25533
25543
  if [ "${LOKI_AUTONOMY_OVERRIDE:-on}" != "off" ] \
25534
25544
  && type _loki_autonomy_override_text >/dev/null 2>&1 \
25535
25545
  && type loki_claude_flag_supported >/dev/null 2>&1 \
@@ -494,16 +494,20 @@ CHILD_ENV_DENYLIST = (
494
494
  )
495
495
 
496
496
 
497
- def _scoped_child_env():
498
- return {k: v for k, v in os.environ.items() if k not in CHILD_ENV_DENYLIST}
497
+ def _scoped_child_env(extra_env=None):
498
+ env = {k: v for k, v in os.environ.items() if k not in CHILD_ENV_DENYLIST}
499
+ # Caller values override inherited ones but can never restore a denied secret.
500
+ env.update({k: v for k, v in (extra_env or {}).items() if k not in CHILD_ENV_DENYLIST})
501
+ return env
499
502
 
500
503
 
501
- def run_loki_outcome(args, dry_run=False, stdout_sink=None):
504
+ def run_loki_outcome(args, dry_run=False, stdout_sink=None, extra_env=None):
502
505
  """Run a loki command and report WHICH of the three outcomes occurred.
503
506
 
504
507
  Returns one of OUTCOME_PASSED / OUTCOME_FAILED / OUTCOME_UNKNOWN. This is
505
508
  the honest version of run_loki_command, which answers only "did it launch".
506
509
  When stdout_sink is a list, the decoded stdout is appended to it on completion.
510
+ extra_env adds child env vars (e.g. LOKI_STEER_SOURCE) on top of the scoped env.
507
511
  """
508
512
  cmd = ["loki"] + args
509
513
  if dry_run:
@@ -515,7 +519,7 @@ def run_loki_outcome(args, dry_run=False, stdout_sink=None):
515
519
  cmd,
516
520
  stdout=subprocess.PIPE,
517
521
  stderr=subprocess.PIPE,
518
- env=_scoped_child_env(),
522
+ env=_scoped_child_env(extra_env),
519
523
  )
520
524
  except (FileNotFoundError, OSError) as e:
521
525
  logging.error("Failed to launch %s: %s", " ".join(cmd), e)
@@ -703,7 +707,9 @@ def handle_issue_comment_event(payload, dry_run=False):
703
707
  note = "GitHub comment from %s: %s" % (login or "unknown", body)
704
708
  summary = "steer %s from comment" % ref
705
709
  sink = []
706
- outcome = run_loki_outcome(["steer", note], dry_run=dry_run, stdout_sink=sink)
710
+ # Provenance for the revision log: loki steer records actor= and source= per revision.
711
+ steer_env = {"LOKI_STEER_SOURCE": "github", "LOKI_STEER_ACTOR": login or "unknown"}
712
+ outcome = run_loki_outcome(["steer", note], dry_run=dry_run, stdout_sink=sink, extra_env=steer_env)
707
713
  # Only a confirmed exit 0 proves the steer was written. A detached child
708
714
  # (UNKNOWN) never reports its exit code, so acking would claim an unverified
709
715
  # steer; report it as launched but post nothing.
@@ -27,7 +27,7 @@ _loki_completion() {
27
27
 
28
28
  # If the word starts with a dash, show flags
29
29
  if [[ "$cur" == -* ]]; then
30
- local flags="--provider --max-iterations --parallel --background --bg --simple --complex --github --no-dashboard --sandbox --skip-memory --spec --spec-first --yes --budget --attempts --model --isolation --max-cost --no-pr --explain-route --brief --worktree --pr --ship --detach --workspace --deploy --health-url --auto-rollback --help"
30
+ local flags="--provider --max-iterations --parallel --background --bg --simple --complex --github --no-dashboard --sandbox --skip-memory --spec --spec-first --yes --budget --attempts --model --isolation --max-cost --no-pr --explain-route --brief --worktree --pr --ship --detach --workspace --deploy --health-url --auto-rollback --resume --help"
31
31
  COMPREPLY=( $(compgen -W "${flags}" -- "$cur") )
32
32
  return 0
33
33
  fi
@@ -149,7 +149,7 @@ _loki_completion() {
149
149
 
150
150
  deploy)
151
151
  if [[ "$cur" == -* ]]; then
152
- COMPREPLY=( $(compgen -W "--execute --canary --health-url --auto-rollback --dir --no-clip --allow-unanchored --help" -- "$cur") )
152
+ COMPREPLY=( $(compgen -W "--execute --canary --health-url --health-match --auto-rollback --dir --no-clip --allow-unanchored --help" -- "$cur") )
153
153
  return 0
154
154
  fi
155
155
  ;;
@@ -7,6 +7,6 @@ Modules:
7
7
  control: Session control helpers (status, atomic writes, origin check)
8
8
  """
9
9
 
10
- __version__ = "11.15.0"
10
+ __version__ = "11.17.1"
11
11
 
12
12
  __all__ = ["__version__"]
@@ -12,7 +12,7 @@ argv.
12
12
 
13
13
  ponytail: run state is in memory (a dashboard restart forgets the queue; the
14
14
  runs themselves keep going and their worktrees keep the receipts). The
15
- v10 engine has no resume-with-answer (--resume was removed), so a BLOCKED
15
+ v10 engine has no resume-with-answer (--resume only resumes a publish-phase run), so a BLOCKED
16
16
  question is shown with the re-run command instead of an answer box.
17
17
  """
18
18
  import json
@@ -472,7 +472,7 @@ def backlog_status():
472
472
  for r in _RUNS.values():
473
473
  row = {k: r.get(k) for k in ("number", "title", "status", "pr_url", "question", "detail")}
474
474
  if r["status"] == "blocked":
475
- row["resume_hint"] = ("The engine cannot resume with an answer (--resume was removed). "
475
+ row["resume_hint"] = ("The engine cannot resume with an answer (--resume only finishes the publish phase of a sealed run, not a blocked one). "
476
476
  "Answer on the issue, then re-run: loki %s#%d" % (repo, r["number"]))
477
477
  out.append(row)
478
478
  return {"repo": repo, "concurrency": _concurrency(), "issues": out}