loki-mode 11.15.0 → 11.17.5

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 (119) hide show
  1. package/SKILL.md +3 -3
  2. package/VERSION +1 -1
  3. package/autonomy/grill.sh +27 -3
  4. package/autonomy/hooks/precompact-flush.sh +51 -0
  5. package/autonomy/hooks/validate-bash.sh +941 -13
  6. package/autonomy/lib/chat.sh +116 -7
  7. package/autonomy/lib/claude-flags.sh +157 -0
  8. package/autonomy/lib/claude_permissions.py +60 -0
  9. package/autonomy/lib/decision_record.py +92 -3
  10. package/autonomy/lib/done-recognition.sh +8 -4
  11. package/autonomy/lib/efficiency_cost.py +102 -0
  12. package/autonomy/lib/engine10-push.sh +30 -5
  13. package/autonomy/lib/modernize/py_capture.py +62 -3
  14. package/autonomy/lib/prd-enrich.sh +30 -2
  15. package/autonomy/lib/proof-generator.py +5 -0
  16. package/autonomy/lib/proof-verify.py +109 -1
  17. package/autonomy/lib/trust_trajectory.py +13 -4
  18. package/autonomy/lib/voter-agents.sh +64 -7
  19. package/autonomy/lib/wiki-generator.py +58 -10
  20. package/autonomy/lib/wiki_llm.py +41 -2
  21. package/autonomy/loki +548 -36
  22. package/autonomy/quickstart.sh +21 -2
  23. package/autonomy/run.sh +217 -16
  24. package/autonomy/trigger-server.py +81 -7
  25. package/completions/_loki +3 -0
  26. package/completions/loki.bash +4 -3
  27. package/dashboard/__init__.py +1 -1
  28. package/dashboard/api_start.py +2 -2
  29. package/dashboard/audit.py +204 -17
  30. package/dashboard/build_supervisor.py +2 -0
  31. package/docs/CLI-REFERENCE.md +5 -1
  32. package/docs/EVALUATING.md +3 -3
  33. package/docs/INSTALLATION.md +1 -1
  34. package/docs/environment-variables.md +1 -0
  35. package/docs/trackers.md +6 -0
  36. package/docs/v10/BOARD.md +27 -1
  37. package/docs/v10/CLI-MODERN.md +10 -7
  38. package/docs/v10/DECISIONS.md +16 -0
  39. package/docs/v10/ENGINE.md +2 -1
  40. package/docs/v10/FAILURE-CLASSES.md +77 -0
  41. package/docs/v10/FOUNDER-QUEUE.md +1 -0
  42. package/docs/v10/GUIDE-MODERNIZE.md +9 -4
  43. package/docs/v10/PLAN-V12.md +896 -0
  44. package/docs/v10/PROGRESS.md +11 -0
  45. package/docs/v11/COST-HALF.md +3 -1
  46. package/docs/visual-evidence.md +2 -0
  47. package/loki-ts/data/finding-schema.json +2 -1
  48. package/loki-ts/dist/loki.js +1180 -1112
  49. package/magic/core/debate.py +27 -1
  50. package/mcp/__init__.py +1 -1
  51. package/memory/retrieval.py +15 -3
  52. package/memory/schemas.py +43 -1
  53. package/memory/storage.py +11 -0
  54. package/package.json +2 -2
  55. package/packages/control-plane/dist/server.js +4154 -3551
  56. package/packages/control-plane/src/server/legacy/routes-control.ts +5 -1
  57. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  58. package/providers/claude.sh +7 -3
  59. package/providers/loader.sh +26 -0
  60. package/providers/model_catalog.json +6 -0
  61. package/providers/model_routes.sh +31 -0
  62. package/src/policies/claude_permissions.js +132 -0
  63. package/web-app/dist/assets/{AdminPage-bZlvH4TL.js → AdminPage-C0v2p3jN.js} +1 -1
  64. package/web-app/dist/assets/{Avatar-BZrBstQe.js → Avatar-Cgc0QepK.js} +1 -1
  65. package/web-app/dist/assets/{Badge-Q8l8r4Vq.js → Badge-1X4wQ0sU.js} +1 -1
  66. package/web-app/dist/assets/{Button-B8Bt0A3S.js → Button-CG79n6-I.js} +1 -1
  67. package/web-app/dist/assets/{CockpitPage-CIYExrXa.js → CockpitPage-rSw2BTKk.js} +1 -1
  68. package/web-app/dist/assets/{ComparePage-CBOmHdam.js → ComparePage-B9EVp__S.js} +1 -1
  69. package/web-app/dist/assets/{ErrorBoundary-BKwDPSnK.js → ErrorBoundary-DLQKY8LF.js} +1 -1
  70. package/web-app/dist/assets/{EvidenceReceiptPanel-KqzjphWl.js → EvidenceReceiptPanel-DE0L1L2Q.js} +1 -1
  71. package/web-app/dist/assets/{GitHubIssuesPanel-lF3GkHRv.js → GitHubIssuesPanel-DlKtYrtk.js} +1 -1
  72. package/web-app/dist/assets/{GitHubPRsPanel-BCzipPWD.js → GitHubPRsPanel-C5c5tY3x.js} +1 -1
  73. package/web-app/dist/assets/{HomePage-CIdnkzaN.js → HomePage-ob1natMp.js} +2 -2
  74. package/web-app/dist/assets/{LoginPage-D-178nTT.js → LoginPage-CCQBgvzB.js} +1 -1
  75. package/web-app/dist/assets/{MagicPage-BmHuzMT8.js → MagicPage-CKSuC0Pp.js} +1 -1
  76. package/web-app/dist/assets/{MetricsPage-CPCiyDFn.js → MetricsPage-nq_ygAuG.js} +1 -1
  77. package/web-app/dist/assets/{NotFoundPage-B2T6n1gx.js → NotFoundPage-B6gVLTAn.js} +1 -1
  78. package/web-app/dist/assets/{ProjectPage-DQmpFhb0.js → ProjectPage-pUnvGriO.js} +3 -3
  79. package/web-app/dist/assets/{ProjectsPage-BmugzFEd.js → ProjectsPage-DMFPGLkE.js} +1 -1
  80. package/web-app/dist/assets/{SettingsPage-ZB-RrmgM.js → SettingsPage-DAB_cp87.js} +1 -1
  81. package/web-app/dist/assets/{ShowcasePage-BAhwHt4_.js → ShowcasePage-DTsx_XxI.js} +1 -1
  82. package/web-app/dist/assets/{SystemSettingsPage-Bt7sf2Vu.js → SystemSettingsPage-C2UUqWhW.js} +1 -1
  83. package/web-app/dist/assets/{TeamsPage-B-v5j_8q.js → TeamsPage-C4Sws9U7.js} +1 -1
  84. package/web-app/dist/assets/{TemplatesPage-bdqUL-LG.js → TemplatesPage-B1-79O4n.js} +1 -1
  85. package/web-app/dist/assets/{TerminalOutput-CRHEXBwS.js → TerminalOutput-CDSVt5TB.js} +1 -1
  86. package/web-app/dist/assets/{activity-C-ROkmXt.js → activity-CBEULbio.js} +1 -1
  87. package/web-app/dist/assets/{bell-DbiCBxQA.js → bell-Dp-j77Qo.js} +1 -1
  88. package/web-app/dist/assets/{bot-DsIgE_qr.js → bot-Dh1FlTEJ.js} +1 -1
  89. package/web-app/dist/assets/{check-CkAdmHWP.js → check-C_7N7W4q.js} +1 -1
  90. package/web-app/dist/assets/{chevron-left-CYTLivAo.js → chevron-left-DtZJiu3T.js} +1 -1
  91. package/web-app/dist/assets/{circle-alert-BVIcAsFJ.js → circle-alert-CJK4sNof.js} +1 -1
  92. package/web-app/dist/assets/{clock-Dg3Rq0go.js → clock--0bfAUkP.js} +1 -1
  93. package/web-app/dist/assets/{cloud-CenrqHFl.js → cloud-CRa9nwiN.js} +1 -1
  94. package/web-app/dist/assets/{code-xml-C2mvfoug.js → code-xml-4gRrVGxx.js} +1 -1
  95. package/web-app/dist/assets/{database-BKa0_eCb.js → database-C-A2USGl.js} +1 -1
  96. package/web-app/dist/assets/{dollar-sign-DfelUK1S.js → dollar-sign-BshJt4ML.js} +1 -1
  97. package/web-app/dist/assets/{file-code-corner-CZKWoHVg.js → file-code-corner-Bd--i_IM.js} +1 -1
  98. package/web-app/dist/assets/{file-plus-CZ-m1zMX.js → file-plus-YiNg-aWn.js} +1 -1
  99. package/web-app/dist/assets/{globe-S0kv_GwC.js → globe-CfyHbAbI.js} +1 -1
  100. package/web-app/dist/assets/{hammer-BW1MDdu1.js → hammer-BlGC-yqz.js} +1 -1
  101. package/web-app/dist/assets/{index-ACEYsTc2.js → index-BlCg8mla.js} +3 -3
  102. package/web-app/dist/assets/{layers-BrXHYZ-L.js → layers-DLiA0AcW.js} +1 -1
  103. package/web-app/dist/assets/{loader-circle-DOdhKFx0.js → loader-circle-CkOfS_Q6.js} +1 -1
  104. package/web-app/dist/assets/{lock-BcFTyg8J.js → lock-C5vRE8HM.js} +1 -1
  105. package/web-app/dist/assets/{package-D34cMVQG.js → package-BPzp43h4.js} +1 -1
  106. package/web-app/dist/assets/{plus-CEaMeBxL.js → plus-C4x6sD5J.js} +1 -1
  107. package/web-app/dist/assets/{refresh-cw-D2Rzxr5U.js → refresh-cw-DZn-gB9J.js} +1 -1
  108. package/web-app/dist/assets/{rotate-ccw-C_-EjRLh.js → rotate-ccw-rH63YhnY.js} +1 -1
  109. package/web-app/dist/assets/{scroll-text-BSSCMI_c.js → scroll-text-DTvZMQuh.js} +1 -1
  110. package/web-app/dist/assets/{server-BKvLonWt.js → server-DmwCdhKT.js} +1 -1
  111. package/web-app/dist/assets/{shield-alert-DbbQbPdW.js → shield-alert-Dn5Rtkhx.js} +1 -1
  112. package/web-app/dist/assets/{thumbs-up-NoBD1fUR.js → thumbs-up-D_lHdH-1.js} +1 -1
  113. package/web-app/dist/assets/{trash-2-BXz8rAyd.js → trash-2-CVfeP0Fb.js} +1 -1
  114. package/web-app/dist/assets/{trending-down-D8u535Re.js → trending-down-ouPrjBEm.js} +1 -1
  115. package/web-app/dist/assets/{trending-up-q8k3kgiy.js → trending-up-7H96_KlR.js} +1 -1
  116. package/web-app/dist/assets/{upload-Cp7_zeBI.js → upload-kTTgyfD6.js} +1 -1
  117. package/web-app/dist/assets/{usePolling-CTHVdcuX.js → usePolling-BFIMPvNM.js} +1 -1
  118. package/web-app/dist/assets/{user-DF7bfSf_.js → user-UDo6EQfn.js} +1 -1
  119. package/web-app/dist/index.html +1 -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:-}" != "legacy" ]; 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
@@ -789,7 +789,85 @@ loki_apply_scoped_change_profile() {
789
789
  export LOKI_SCOPED_CHANGE_ACTIVE=1
790
790
  }
791
791
 
792
+ # Small-task fast path (cost S5). Applied AFTER detect_complexity, because the
793
+ # tier is not known when this file is sourced. Trigger: DETECTED_COMPLEXITY is
794
+ # simple and, for a scoped change in an existing repo, the working-tree diff
795
+ # fits LOKI_SMALL_DIFF_BUDGET changed lines (default 400). LOKI_SMALL_PROFILE=0
796
+ # opts out. It never overrides another explicit profile.
797
+ #
798
+ # What this NEVER touches: the deterministic gates, holdout and Seal. It only
799
+ # drops model-bearing orchestration that has no reader on a small task. Every
800
+ # knob uses ":=" so explicit user env always beats the profile.
801
+ loki_small_profile_eligible() {
802
+ [ "${LOKI_SMALL_PROFILE:-1}" != "0" ] || return 1
803
+ [ "${DETECTED_COMPLEXITY:-}" = "simple" ] || return 1
804
+ case "${LOKI_BUILD_PROFILE:-}" in
805
+ ""|small) ;;
806
+ *) return 1 ;;
807
+ esac
808
+ if [ "${LOKI_SCOPED_CHANGE_ACTIVE:-0}" = "1" ]; then
809
+ local changed budget="${LOKI_SMALL_DIFF_BUDGET:-400}"
810
+ changed="$(git -C "${TARGET_DIR:-.}" diff --numstat HEAD 2>/dev/null \
811
+ | awk '{ if ($1 ~ /^[0-9]+$/) n += $1; if ($2 ~ /^[0-9]+$/) n += $2 } END { print n + 0 }')"
812
+ [ "${changed:-0}" -le "$budget" ] 2>/dev/null || return 1
813
+ fi
814
+ return 0
815
+ }
816
+
817
+ loki_apply_small_profile() {
818
+ : "${LOKI_SPEC_GRILL:=0}"
819
+ : "${LOKI_PRD_ENRICH:=0}"
820
+ : "${LOKI_WIKI_AUTO:=0}"
821
+ : "${LOKI_AUTO_DOCS:=false}"
822
+ : "${LOKI_GATE_DEVILS_ADVOCATE:=false}"
823
+ : "${LOKI_COUNCIL_MODEL_VOTERS:=0}"
824
+ : "${LOKI_ADVISOR_MODEL:=haiku}"
825
+ : "${LOKI_SIMPLE:=1}"
826
+ : "${LOKI_DOCS_TIER:=small}"
827
+ export LOKI_SPEC_GRILL LOKI_PRD_ENRICH LOKI_WIKI_AUTO LOKI_AUTO_DOCS
828
+ export LOKI_GATE_DEVILS_ADVOCATE
829
+ export LOKI_COUNCIL_MODEL_VOTERS LOKI_ADVISOR_MODEL LOKI_SIMPLE LOKI_DOCS_TIER
830
+ LOKI_SMALL_PROFILE_ACTIVE=1
831
+ export LOKI_SMALL_PROFILE_ACTIVE
832
+ }
833
+
834
+ # Main-session lean flags, small profile only. Fills _LOKI_SMALL_MAIN_ARGV from
835
+ # loki_lean_child_flags (one argument per line) and widens the OAuth-host
836
+ # "--setting-sources project" to "project,local" so project-local settings
837
+ # still apply. Returns 1 (array empty) on any other profile, so the caller
838
+ # keeps its existing "user,project,local" pin. LOKI_SMALL_LEAN_MAIN=0 opts out.
839
+ # "--bare" is never passed to the main session: it skips hooks and the host
840
+ # command guard. Lean flags apply to child sessions only.
841
+ loki_small_main_session_flags() {
842
+ _LOKI_SMALL_MAIN_ARGV=()
843
+ [ "${LOKI_SMALL_PROFILE_ACTIVE:-0}" = "1" ] || return 1
844
+ [ "${LOKI_SMALL_LEAN_MAIN:-1}" != "0" ] || return 1
845
+ type loki_lean_child_flags >/dev/null 2>&1 || return 1
846
+ local _line _prev=""
847
+ while IFS= read -r _line; do
848
+ [ -n "$_line" ] || continue
849
+ [ "$_line" != "--bare" ] || continue
850
+ if [ "$_prev" = "--setting-sources" ] && [ "$_line" = "project" ]; then
851
+ _line="project,local"
852
+ fi
853
+ _LOKI_SMALL_MAIN_ARGV+=("$_line")
854
+ _prev="$_line"
855
+ done <<<"$(loki_lean_child_flags main)"
856
+ [ "${#_LOKI_SMALL_MAIN_ARGV[@]}" -gt 0 ]
857
+ }
858
+
859
+ loki_maybe_apply_small_profile() {
860
+ loki_small_profile_eligible || return 0
861
+ LOKI_BUILD_PROFILE=small
862
+ export LOKI_BUILD_PROFILE
863
+ loki_apply_build_profile
864
+ }
865
+
792
866
  loki_apply_build_profile() {
867
+ if [ "${LOKI_BUILD_PROFILE:-}" = "small" ]; then
868
+ loki_apply_small_profile
869
+ return 0
870
+ fi
793
871
  [ "${LOKI_BUILD_PROFILE:-}" = "simple-web" ] || return 0
794
872
  : "${LOKI_PHASE_API_TESTS:=false}"
795
873
  : "${LOKI_PHASE_INTEGRATION:=false}"
@@ -6709,13 +6787,15 @@ spawn_worktree_session() {
6709
6787
  # Expand the auto-flag array with the bash-3.2 empty-array guard
6710
6788
  # (${arr[@]+...}) so a bare "${arr[@]}" under set -u does not
6711
6789
  # abort with "unbound variable" when no flags were collected.
6790
+ _LOKI_PERM_ARGV=(--dangerously-skip-permissions)
6791
+ ! type _loki_claude_permission_argv >/dev/null 2>&1 || _loki_claude_permission_argv
6712
6792
  if [ -n "$_loki_wt_cm" ]; then
6713
- CAVEMAN_DEFAULT_MODE="$_loki_wt_cm" claude --dangerously-skip-permissions \
6793
+ CAVEMAN_DEFAULT_MODE="$_loki_wt_cm" claude "${_LOKI_PERM_ARGV[@]}" \
6714
6794
  "${_LOKI_WT_AUTO_FLAGS[@]+"${_LOKI_WT_AUTO_FLAGS[@]}"}" \
6715
6795
  -p "Loki Mode: $task_prompt. Read .loki/CONTINUITY.md for context." \
6716
6796
  >> "$log_file" 2>&1 || _wt_exit=$?
6717
6797
  else
6718
- claude --dangerously-skip-permissions \
6798
+ claude "${_LOKI_PERM_ARGV[@]}" \
6719
6799
  "${_LOKI_WT_AUTO_FLAGS[@]+"${_LOKI_WT_AUTO_FLAGS[@]}"}" \
6720
6800
  -p "Loki Mode: $task_prompt. Read .loki/CONTINUITY.md for context." \
6721
6801
  >> "$log_file" 2>&1 || _wt_exit=$?
@@ -6950,7 +7030,9 @@ Output ONLY the resolved file content with no conflict markers. No explanations.
6950
7030
  # variable (the shell, not the agent, writes the resolved file).
6951
7031
  # It needs no hooks, LSP, CLAUDE.md auto-discovery, or MCP, so
6952
7032
  # --bare is safe and cheaper. Gated + opt-out LOKI_BARE_SUBCALLS=0.
6953
- local _cr_argv=("--dangerously-skip-permissions")
7033
+ _LOKI_PERM_ARGV=(--dangerously-skip-permissions)
7034
+ ! type _loki_claude_permission_argv >/dev/null 2>&1 || _loki_claude_permission_argv
7035
+ local _cr_argv=("${_LOKI_PERM_ARGV[@]}")
6954
7036
  if type loki_subcall_bare_enabled >/dev/null 2>&1 && loki_subcall_bare_enabled; then
6955
7037
  _cr_argv+=("--bare")
6956
7038
  fi
@@ -9668,6 +9750,15 @@ _loki_host_guard_required() {
9668
9750
  LOKI_AUTO_GIT_INIT=0 _loki_workspace_is_engine_owned
9669
9751
  }
9670
9752
 
9753
+ # Host-guard hook timeout, seconds. Mirrors hookTimeoutSeconds in
9754
+ # loki-ts/src/runner/providers.ts (same env var, default 30, positive int <= 99999).
9755
+ _loki_hook_timeout() {
9756
+ case "${LOKI_HOOK_TIMEOUT:-}" in
9757
+ [1-9] | [1-9][0-9] | [1-9][0-9][0-9] | [1-9][0-9][0-9][0-9] | [1-9][0-9][0-9][0-9][0-9]) printf '%s\n' "$LOKI_HOOK_TIMEOUT" ;;
9758
+ *) printf '%s\n' 30 ;;
9759
+ esac
9760
+ }
9761
+
9671
9762
  _loki_prepare_host_guard() {
9672
9763
  _loki_host_guard_required || return 0
9673
9764
 
@@ -9692,12 +9783,12 @@ _loki_prepare_host_guard() {
9692
9783
  fi
9693
9784
 
9694
9785
  local settings_json
9695
- settings_json=$(_LOKI_HOST_GUARD_HOOK="$hook_path" python3 -c '
9786
+ settings_json=$(_LOKI_HOST_GUARD_HOOK="$hook_path" _LOKI_HOOK_TIMEOUT="$(_loki_hook_timeout)" python3 -c '
9696
9787
  import json, os, shlex
9697
9788
  command = "bash " + shlex.quote(os.environ["_LOKI_HOST_GUARD_HOOK"])
9698
9789
  print(json.dumps({"hooks": {"PreToolUse": [{
9699
9790
  "matcher": "Bash",
9700
- "hooks": [{"type": "command", "command": command}],
9791
+ "hooks": [{"type": "command", "command": command, "timeout": int(os.environ["_LOKI_HOOK_TIMEOUT"])}],
9701
9792
  }]}}))
9702
9793
  ' 2>/dev/null) || settings_json=""
9703
9794
  if [ -z "$settings_json" ]; then
@@ -12473,6 +12564,8 @@ enforce_build_check() {
12473
12564
  # none -> positively no build phase at all -> not_applicable (N/A)
12474
12565
  local cmd="" ran="false" applicable="false" exit_code="null" status="not_run"
12475
12566
  local build_class="present"
12567
+ # exec_cmd overrides cmd for execution only (the recorded command stays JSON-safe).
12568
+ local exec_cmd="" exec_env="" exec_tmo=""
12476
12569
 
12477
12570
  # INVERTED classification (three council rounds each found a fake-green hole in
12478
12571
  # an allowlist of build systems -> the allowlist is structurally leaky). The
@@ -12526,7 +12619,24 @@ enforce_build_check() {
12526
12619
  # bazel/anything-unborn silently reads VERIFIED. This makes the ENTIRE
12527
12620
  # no-package.json universe honest by CONSTRUCTION (list-free), which is why
12528
12621
  # this converges where four rounds of adding-to-a-list could not.
12529
- if [ "$has_foreign_manifest" = "false" ] && [ -f "$tree/package.json" ]; then
12622
+ # S3 (cost program): a manifest-less PYTHON tree (no package.json, no foreign
12623
+ # manifest, no setup.py/pyproject) has a runnable, deterministic build
12624
+ # equivalent: byte-compiling every module (syntax + import-time constants).
12625
+ # We RUN it and record verified/failed. This is NOT a not_applicable
12626
+ # claim: a broken .py file fails the build fact instead of hiding behind
12627
+ # "build not run".
12628
+ if [ "$has_foreign_manifest" = "false" ] && [ ! -f "$tree/package.json" ] \
12629
+ && command -v python3 >/dev/null 2>&1 \
12630
+ && find "$tree" -maxdepth 4 -type f -name '*.py' \
12631
+ -not -path '*/.loki/*' -not -path '*/.git/*' -not -path '*/node_modules/*' \
12632
+ -not -path '*/.venv/*' -not -path '*/venv/*' \
12633
+ -print -quit 2>/dev/null | grep -q .; then
12634
+ cmd="python3 -m compileall -q ."; applicable="true"; build_class="runnable"
12635
+ exec_cmd="python3 -m compileall -q -x '(^|/)(\\.loki|\\.git|node_modules|\\.venv|venv)(/|\$)' ."
12636
+ exec_env="PYTHONPYCACHEPREFIX=$quality_dir/.pycache"
12637
+ if command -v timeout >/dev/null 2>&1; then exec_tmo="timeout $timeout_s"
12638
+ elif command -v gtimeout >/dev/null 2>&1; then exec_tmo="gtimeout $timeout_s"; fi
12639
+ elif [ "$has_foreign_manifest" = "false" ] && [ -f "$tree/package.json" ]; then
12530
12640
  local has_build_devdep="false"
12531
12641
  # A build TOOL as a (dev)dependency means a build exists even if it runs
12532
12642
  # under a non-"build" script name -> stay not_run, do not claim N/A.
@@ -12571,7 +12681,13 @@ enforce_build_check() {
12571
12681
  return $?
12572
12682
  else
12573
12683
  local rc=0
12574
- ( cd "$tree" && timeout "$timeout_s" sh -c "$cmd" >/dev/null 2>&1 ) || rc=$?
12684
+ if [ -n "$exec_cmd" ]; then
12685
+ # Python compileall: pycache redirected out of the repo; timeout
12686
+ # prefix is optional so a box without timeout is not a false RED.
12687
+ ( cd "$tree" && env "$exec_env" $exec_tmo sh -c "$exec_cmd" >/dev/null 2>&1 ) || rc=$?
12688
+ else
12689
+ ( cd "$tree" && timeout "$timeout_s" sh -c "$cmd" >/dev/null 2>&1 ) || rc=$?
12690
+ fi
12575
12691
  ran="true"; exit_code="$rc"
12576
12692
  if [ "$rc" -eq 0 ]; then status="verified"; else status="failed"; fi
12577
12693
  : > "$marker" 2>/dev/null || true
@@ -13777,7 +13893,32 @@ _loki_run_pytest_with_timeout() {
13777
13893
  local pytest_timeout="${LOKI_PYTEST_TIMEOUT:-${LOKI_GATE_TIMEOUT:-300}}"
13778
13894
  local _to_cmd=()
13779
13895
  read -r -a _to_cmd <<< "$(_loki_timeout_prefix "$pytest_timeout" 'pytest gate')"
13780
- (cd "$target_dir" && "${_to_cmd[@]}" pytest "$@" 2>&1)
13896
+ # S3 (cost program): bare `pytest` does not put the cwd on sys.path, so
13897
+ # root-level modules imported from tests/ raise ModuleNotFoundError (a false
13898
+ # RED that drives repair loops). `python3 -m pytest` adds cwd. Falls back to
13899
+ # the bare binary only when python3 cannot import pytest.
13900
+ local _py_cmd=(pytest)
13901
+ if command -v python3 >/dev/null 2>&1 && python3 -c 'import pytest' >/dev/null 2>&1; then
13902
+ _py_cmd=(python3 -m pytest)
13903
+ fi
13904
+ local _pt_out _pt_rc
13905
+ _pt_out=$(cd "$target_dir" && "${_to_cmd[@]}" "${_py_cmd[@]}" "$@" 2>&1)
13906
+ _pt_rc=$?
13907
+ # Exit 5 = pytest collected 0 items (module-level-assert files, custom
13908
+ # python_files). Try stdlib unittest discovery before giving up. Zero tests
13909
+ # from BOTH stays exit 5 (the caller records it inconclusive, never verified).
13910
+ if [ "$_pt_rc" -eq 5 ] && command -v python3 >/dev/null 2>&1; then
13911
+ local _ut_out _ut_rc
13912
+ _ut_out=$(cd "$target_dir" && "${_to_cmd[@]}" python3 -m unittest discover -p 'test_*.py' 2>&1)
13913
+ _ut_rc=$?
13914
+ printf '%s\n[loki] pytest collected 0 tests (exit 5); fell back to python3 -m unittest discover\n%s\n' "$_pt_out" "$_ut_out"
13915
+ if [ "$_ut_rc" -eq 0 ] && printf '%s\n' "$_ut_out" | grep -qE '^(Ran 0 tests|NO TESTS RAN$)'; then
13916
+ return 5
13917
+ fi
13918
+ return "$_ut_rc"
13919
+ fi
13920
+ printf '%s\n' "$_pt_out"
13921
+ return "$_pt_rc"
13781
13922
  }
13782
13923
 
13783
13924
  # ============================================================================
@@ -14326,14 +14467,27 @@ sys.stdout.write(t.strip())
14326
14467
  # v7.5.15 (Triage #14): wrapped with configurable timeout via helper.
14327
14468
  output=$(_loki_run_pytest_with_timeout "${TARGET_DIR:-.}" --tb=short)
14328
14469
  pytest_exit=$?
14470
+ # S3: the helper falls back to unittest discover on pytest exit 5; the
14471
+ # record must name the runner that actually produced the verdict.
14472
+ case "$output" in *"fell back to python3 -m unittest discover"*) test_runner="unittest" ;; esac
14329
14473
  if [ "$pytest_exit" -eq 124 ]; then
14330
14474
  local _pt_to="${LOKI_PYTEST_TIMEOUT:-${LOKI_GATE_TIMEOUT:-300}}"
14331
14475
  test_passed=false
14332
14476
  log_warn "pytest gate timed out after ${_pt_to}s (exit 124)"
14333
- details="pytest: TIMED OUT after ${_pt_to}s -- $(echo "$output" | tail -3 | tr '\n' ' ')"
14477
+ details="$test_runner: TIMED OUT after ${_pt_to}s -- $(echo "$output" | tail -3 | tr '\n' ' ')"
14478
+ elif [ "$pytest_exit" -eq 5 ]; then
14479
+ # Zero tests from pytest AND unittest: inconclusive, never verified.
14480
+ # Stays passing only on a POSITIVE "Ran 0 tests" read, which the
14481
+ # zero-test guard below downgrades to no_tests_run; anything else
14482
+ # (no fallback output) is a plain failure.
14483
+ test_runner="unittest"
14484
+ if ! printf '%s\n' "$output" | grep -qE '^(Ran 0 tests|NO TESTS RAN$)'; then
14485
+ test_passed=false
14486
+ fi
14487
+ details="unittest: $(echo "$output" | tail -5 | tr '\n' ' ')"
14334
14488
  else
14335
14489
  [ "$pytest_exit" -ne 0 ] && test_passed=false
14336
- details="pytest: $(echo "$output" | tail -5 | tr '\n' ' ')"
14490
+ details="$test_runner: $(echo "$output" | tail -5 | tr '\n' ' ')"
14337
14491
  fi
14338
14492
  elif [ "$has_python_project" = "true" ] && command -v python3 &>/dev/null; then
14339
14493
  # #139: pytest ABSENT but a Python test suite exists. stdlib unittest
@@ -16282,7 +16436,9 @@ _dispatch_reviewer() {
16282
16436
  # Guardrail, not a sandbox -- echo>/sed -i/etc. remain; the
16283
16437
  # real net is commit-before-agent-wave. Opt out
16284
16438
  # LOKI_REVIEW_TOOL_GUARD=0. See loki_review_guard_denylist.
16285
- local _rv_argv=("--dangerously-skip-permissions")
16439
+ _LOKI_PERM_ARGV=(--dangerously-skip-permissions)
16440
+ ! type _loki_claude_permission_argv >/dev/null 2>&1 || _loki_claude_permission_argv
16441
+ local _rv_argv=("${_LOKI_PERM_ARGV[@]}")
16286
16442
  # OPT-IN advisor pin (v7.104.0, LOKI_ADVISOR_MODEL): reviewers still
16287
16443
  # pass NO --model by default (unchanged behavior: they run on the
16288
16444
  # account default). When the operator/user sets LOKI_ADVISOR_MODEL to
@@ -16317,7 +16473,7 @@ _dispatch_reviewer() {
16317
16473
  _rv_argv+=(
16318
16474
  "--settings" "$LOKI_HOST_GUARD_SETTINGS_JSON"
16319
16475
  "--setting-sources" ""
16320
- "--tools" ""
16476
+ "--tools="
16321
16477
  "--mcp-config" '{"mcpServers":{}}'
16322
16478
  "--strict-mcp-config"
16323
16479
  "--disable-slash-commands"
@@ -17717,6 +17873,11 @@ SPECIALIST_SELECT
17717
17873
  log_error "Code review: Specialist selection failed"
17718
17874
  return 1
17719
17875
  fi
17876
+ # An empty reviewer list must never read as "passed (0/0)": fail closed.
17877
+ if ! printf '%s' "$selected_specialists" | python3 -c 'import sys,json; sys.exit(0 if json.load(sys.stdin).get("reviewers") else 1)' 2>/dev/null; then
17878
+ log_error "Code review: no reviewers selected; review is inconclusive"
17879
+ return 1
17880
+ fi
17720
17881
 
17721
17882
  # Requirements shards are physical dispatches under one logical reviewer.
17722
17883
  # Keep selection and aggregation cardinality unchanged so shards can never
@@ -21352,7 +21513,9 @@ ${_commits}"
21352
21513
  # needed, so --bare is safe and cheaper. Opt out LOKI_BARE_SUBCALLS=0.
21353
21514
  # Always at least --dangerously-skip-permissions, so the array is never
21354
21515
  # empty (empty "${arr[@]}" under set -u errors on bash 3.2, stock macOS).
21355
- local _ic_argv=("--dangerously-skip-permissions")
21516
+ _LOKI_PERM_ARGV=(--dangerously-skip-permissions)
21517
+ ! type _loki_claude_permission_argv >/dev/null 2>&1 || _loki_claude_permission_argv
21518
+ local _ic_argv=("${_LOKI_PERM_ARGV[@]}")
21356
21519
  if type loki_subcall_bare_enabled >/dev/null 2>&1 && loki_subcall_bare_enabled; then
21357
21520
  _ic_argv+=("--bare")
21358
21521
  fi
@@ -21464,6 +21627,20 @@ PY
21464
21627
  return 0
21465
21628
  }
21466
21629
 
21630
+ # At most one wiki regeneration per run, on every tier (cost S5). Args:
21631
+ # iteration, max iterations, iteration exit code. A failed non-final iteration
21632
+ # defers; the first successful iteration, or the final one, spends the single
21633
+ # run. The flag lives in the caller's shell, so call this without a subshell.
21634
+ _auto_wiki_regen_once() {
21635
+ [ "${_LOKI_WIKI_REGEN_DONE:-0}" = "1" ] && return 0
21636
+ local iter="${1:-0}" max="${2:-0}" rc="${3:-1}"
21637
+ if [ "$rc" != "0" ]; then
21638
+ [ "$max" -gt 0 ] 2>/dev/null && [ "$iter" -ge "$max" ] 2>/dev/null || return 0
21639
+ fi
21640
+ _LOKI_WIKI_REGEN_DONE=1
21641
+ _auto_wiki_regen
21642
+ }
21643
+
21467
21644
 
21468
21645
  # Magic Modules COMPOUND: record successful component patterns (v6.77.0)
21469
21646
  # Called at end of each iteration to capture generated/updated components
@@ -24671,6 +24848,13 @@ except Exception:
24671
24848
  fi
24672
24849
 
24673
24850
  log_info "PRD: ${prd_path:-Codebase Analysis Mode}"
24851
+ # Complexity is detected once, here, so the small-task profile can take
24852
+ # effect before spec grill, PRD enrich and the first prompt. The later
24853
+ # detect_complexity guard sees the cached value.
24854
+ if [ -z "${DETECTED_COMPLEXITY:-}" ]; then
24855
+ detect_complexity "$prd_path"
24856
+ fi
24857
+ loki_maybe_apply_small_profile
24674
24858
  log_info "Max retries: $MAX_RETRIES"
24675
24859
  log_info "Max iterations: $MAX_ITERATIONS"
24676
24860
  log_info "Completion promise: $COMPLETION_PROMISE"
@@ -25529,7 +25713,9 @@ except Exception as exc:
25529
25713
  # present so the array is never empty (empty "${arr[@]}" under `set -u`
25530
25714
  # is an error on bash 3.2, the stock macOS shell). The autonomy override
25531
25715
  # is appended conditionally.
25532
- local _loki_claude_argv=("--dangerously-skip-permissions" "--model" "$tier_param")
25716
+ _LOKI_PERM_ARGV=(--dangerously-skip-permissions)
25717
+ ! type _loki_claude_permission_argv >/dev/null 2>&1 || _loki_claude_permission_argv
25718
+ local _loki_claude_argv=("${_LOKI_PERM_ARGV[@]}" "--model" "$tier_param")
25533
25719
  if [ "${LOKI_AUTONOMY_OVERRIDE:-on}" != "off" ] \
25534
25720
  && type _loki_autonomy_override_text >/dev/null 2>&1 \
25535
25721
  && type loki_claude_flag_supported >/dev/null 2>&1 \
@@ -25541,7 +25727,9 @@ except Exception as exc:
25541
25727
  # default. Behavior-neutral (these are the standard sources). Gated +
25542
25728
  # falls back to the implicit default when unsupported. Opt out with
25543
25729
  # LOKI_SETTING_SOURCES=off.
25544
- if ! loki_is_supervised_simple_web \
25730
+ if loki_small_main_session_flags; then
25731
+ _loki_claude_argv+=("${_LOKI_SMALL_MAIN_ARGV[@]}")
25732
+ elif ! loki_is_supervised_simple_web \
25545
25733
  && [ "${LOKI_SETTING_SOURCES:-on}" != "off" ] \
25546
25734
  && type loki_claude_flag_supported >/dev/null 2>&1 \
25547
25735
  && loki_claude_flag_supported "--setting-sources"; then
@@ -26369,6 +26557,15 @@ EOF
26369
26557
  >/dev/null 2>&1 </dev/null & ) 2>/dev/null || true
26370
26558
  fi
26371
26559
 
26560
+ # DESIGN DECISION RECORD (P12-37). The plan (REASON) stage may leave
26561
+ # .loki/signals/DESIGN_DECISION.json (rationale, alternatives, source,
26562
+ # scope). Record it once, then consume it. Non-fatal; no file, no record.
26563
+ local _dd_file="${TARGET_DIR:-.}/.loki/signals/DESIGN_DECISION.json"
26564
+ if [ "${rarv_phase:-}" = "REASON" ] && [ -s "$_dd_file" ] && [ -f "${SCRIPT_DIR:-}/lib/decision_record.py" ]; then
26565
+ LOKI_DIR="${TARGET_DIR:-.}/.loki" python3 "${SCRIPT_DIR}/lib/decision_record.py" design-file "$_dd_file" \
26566
+ >/dev/null 2>&1 </dev/null && rm -f "$_dd_file" || true
26567
+ fi
26568
+
26372
26569
  # AGENT PROMPT SIZE. The call this brackets is 93% of a run's wall clock
26373
26570
  # (1814s of 1941s measured), and its INPUT was never measured -- every
26374
26571
  # reviewer logs its prompt bytes, the dominant call logged nothing.
@@ -27195,7 +27392,7 @@ EOF
27195
27392
  # structure changed). Default-on; opt out LOKI_WIKI_AUTO=0. The `|| true`
27196
27393
  # is belt-and-suspenders -- the function already returns 0 on every path.
27197
27394
  if ! loki_is_supervised_simple_web; then
27198
- _auto_wiki_regen 2>/dev/null || true
27395
+ _auto_wiki_regen_once "$ITERATION_COUNT" "$MAX_ITERATIONS" "$exit_code" 2>/dev/null || true
27199
27396
  fi
27200
27397
 
27201
27398
  # BUG-QG-008: Track iteration for convergence regardless of exit code
@@ -29129,6 +29326,10 @@ main() {
29129
29326
 
29130
29327
  # Show provider info
29131
29328
  log_info "Provider: ${PROVIDER_DISPLAY_NAME:-Claude Code} (${PROVIDER_NAME:-claude})"
29329
+ # C-C2: one line naming the active permission policy (claude provider only).
29330
+ if [ "${PROVIDER_NAME:-claude}" = "claude" ] && declare -F loki_permission_policy_banner >/dev/null 2>&1; then
29331
+ log_info "$(loki_permission_policy_banner)"
29332
+ fi
29132
29333
  if [ "${PROVIDER_DEGRADED:-false}" = "true" ]; then
29133
29334
  # Report what is ACTUALLY missing, read from the capability flags, rather
29134
29335
  # than asserting a fixed pair. The old wording hardcoded "Parallel agents
@@ -203,7 +203,7 @@ MAX_UNIT_REF_LEN = 200
203
203
  # "\d" would accept Unicode digits. Mirrors the TS REPO_RE/REF_RE/UNIT_RE/MID_RE.
204
204
  UNIT_REPO_RE = re.compile(r"[A-Za-z0-9._][A-Za-z0-9._-]*/[A-Za-z0-9._-]+", re.ASCII)
205
205
  UNIT_REF_RE = re.compile(r"[A-Za-z0-9._][A-Za-z0-9._/-]*", re.ASCII)
206
- UNIT_ID_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.-]{0,127}", re.ASCII)
206
+ UNIT_ID_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.-]*(?:/[A-Za-z0-9][A-Za-z0-9_.-]*)*", re.ASCII)
207
207
  UNIT_MID_RE = re.compile(r"mod-[0-9]{8}T[0-9]{6}Z-[0-9a-f]{6}", re.ASCII)
208
208
 
209
209
 
@@ -231,7 +231,7 @@ def validate_unit_job(payload):
231
231
  if (not ref or len(ref) > MAX_UNIT_REF_LEN or not UNIT_REF_RE.fullmatch(ref)
232
232
  or ".." in ref or ref.endswith((".lock", "/", "."))):
233
233
  return None, "invalid ref"
234
- if not UNIT_ID_RE.fullmatch(unit):
234
+ if len(unit) > 128 or ".." in unit or not UNIT_ID_RE.fullmatch(unit):
235
235
  return None, "invalid unit"
236
236
  clean = {"kind": UNIT_JOB_KIND, "repo": repo, "ref": ref, "unit": unit}
237
237
  if "mid" in payload:
@@ -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.
@@ -720,6 +726,57 @@ def handle_issue_comment_event(payload, dry_run=False):
720
726
  return summary, "fired" if success else "error"
721
727
 
722
728
 
729
+ def _adf_text(node):
730
+ """Plain text of a Jira comment body: a string as-is, an ADF document flattened."""
731
+ if isinstance(node, str):
732
+ return node
733
+ if isinstance(node, dict):
734
+ if isinstance(node.get("text"), str):
735
+ return node["text"]
736
+ return " ".join(_adf_text(c) for c in node.get("content") or [])
737
+ return ""
738
+
739
+
740
+ def tracker_comment_steer(kind, payload, is_live, dry_run=False):
741
+ """A Jira/Linear comment from an allowed actor becomes a `loki steer` (source=jira|linear).
742
+
743
+ Allowlist is LOKI_JIRA_STEER_ACTORS (accountIds) or LOKI_LINEAR_STEER_ACTORS (user ids); unset
744
+ rejects everyone. is_live(ident) must confirm a run this server dispatched for the issue is
745
+ still in flight. Returns (summary, status) like handle_issue_comment_event.
746
+ """
747
+ if kind == "jira":
748
+ comment = payload.get("comment") or {}
749
+ who = comment.get("author") or {}
750
+ actor, ident = str(who.get("accountId", "")), str((payload.get("issue") or {}).get("key", ""))
751
+ body, id_ok = _adf_text(comment.get("body")), bool(JIRA_KEY_RE.match(ident))
752
+ allow_var = "LOKI_JIRA_STEER_ACTORS"
753
+ else:
754
+ data = payload.get("data") or {}
755
+ actor = str(data.get("userId") or (data.get("user") or {}).get("id") or "")
756
+ ident = str(data.get("issueId") or (data.get("issue") or {}).get("id") or "")
757
+ body, id_ok, allow_var = _adf_text(data.get("body")), bool(ident), "LOKI_LINEAR_STEER_ACTORS"
758
+ allowed = {a.strip() for a in os.environ.get(allow_var, "").split(",") if a.strip()}
759
+ if not actor or actor not in allowed:
760
+ return None, "skipped (comment author not authorized)"
761
+ if not id_ok:
762
+ return None, "rejected (invalid issue id)"
763
+ if LOKI_COMMENT_MARKER in body:
764
+ return None, "skipped (loki-authored comment)"
765
+ body = re.sub(r"[\x00-\x1f\x7f\s]+", " ", body).strip()[:STEER_NOTE_MAX]
766
+ if not body:
767
+ return None, "skipped (empty comment)"
768
+ if not is_live(ident):
769
+ return None, "skipped (no live run for %s)" % ident
770
+ clean = re.sub(r"[^A-Za-z0-9-]", "", actor)[:39] or "unknown"
771
+ outcome = run_loki_outcome(["steer", "%s comment from %s: %s" % (kind.capitalize(), clean, body)], dry_run=dry_run,
772
+ extra_env={"LOKI_STEER_SOURCE": kind, "LOKI_STEER_ACTOR": clean})
773
+ summary = "steer %s from %s comment" % (ident, kind)
774
+ if outcome == OUTCOME_PASSED:
775
+ send_notification("Trigger fired: %s" % summary)
776
+ return summary, "fired"
777
+ return summary, OUTCOME_UNKNOWN if outcome == OUTCOME_UNKNOWN else "error"
778
+
779
+
723
780
  def handle_issues_event(payload, dry_run=False):
724
781
  """Handle issues event: opened -> loki start <issue-ref> --pr --detach."""
725
782
  action = payload.get("action", "")
@@ -1563,6 +1620,12 @@ class Dispatcher:
1563
1620
  while len(self._reply_targets) > self._job_max:
1564
1621
  self._reply_targets.popitem(last=False)
1565
1622
 
1623
+ def has_reply_target(self, kind, ident):
1624
+ """True while a job this server dispatched for that Jira key / Linear issue id is in flight (targets are popped on completion)."""
1625
+ field = "key" if kind == "jira" else "issue_id"
1626
+ with self._jobs_lock:
1627
+ return any(isinstance(t, dict) and t.get("kind") == kind and t.get(field) == ident for t in self._reply_targets.values())
1628
+
1566
1629
  def pop_reply_target(self, job_id):
1567
1630
  with self._jobs_lock:
1568
1631
  return self._reply_targets.pop(job_id, None)
@@ -2117,6 +2180,11 @@ class WebhookHandler(http.server.BaseHTTPRequestHandler):
2117
2180
  self._send_json(401, {"error": "stale or missing webhookTimestamp"})
2118
2181
  return
2119
2182
  data = payload.get("data")
2183
+ if payload.get("type") == "Comment" and payload.get("action") == "create":
2184
+ summary, status = tracker_comment_steer(
2185
+ "linear", payload, lambda i: self.dispatcher.has_reply_target("linear", i), self.dry_run)
2186
+ self._send_json(200, {"status": status if summary is None else "steered" if status == "fired" else status})
2187
+ return
2120
2188
  if (payload.get("type") != "Issue" or payload.get("action") != "create"
2121
2189
  or not isinstance(data, dict)):
2122
2190
  self._send_json(200, {"status": "skipped"})
@@ -2163,6 +2231,12 @@ class WebhookHandler(http.server.BaseHTTPRequestHandler):
2163
2231
  if not isinstance(payload, dict):
2164
2232
  self._send_json(400, {"error": "payload must be a JSON object"})
2165
2233
  return
2234
+ if payload.get("webhookEvent") == "comment_created":
2235
+ summary, status = tracker_comment_steer(
2236
+ "jira", payload,
2237
+ lambda i: self.dispatcher.has_reply_target("jira", i), self.dry_run)
2238
+ self._send_json(200, {"status": status if summary is None else "steered" if status == "fired" else status})
2239
+ return
2166
2240
  if (payload.get("webhookEvent") not in JIRA_EVENTS
2167
2241
  or not jira_trigger_matches(
2168
2242
  payload, self.jira_assignees, self.jira_labels)):
package/completions/_loki CHANGED
@@ -209,6 +209,7 @@ function _loki_commands {
209
209
  'stop:Stop execution'
210
210
  'pause:Pause execution'
211
211
  'resume:Resume execution'
212
+ 'cancel:Cancel the active Loki 10 run'
212
213
  'steer:Steer a running build with a note (needs LOKI_PROMPT_INJECTION=1)'
213
214
  'status:Show status'
214
215
  'next:Run the right next step for the current build'
@@ -240,6 +241,7 @@ function _loki_commands {
240
241
  'cockpit:Live cockpit view of a running build'
241
242
  'secure:Secure-by-default gate (findings and waivers)'
242
243
  'own:Finish-and-own ownership handoff'
244
+ 'backup:Back up dashboard state (verified archive)'
243
245
  'telemetry:OpenTelemetry management'
244
246
  'agent:Agent type dispatch'
245
247
  'doctor:Check system prerequisites'
@@ -338,6 +340,7 @@ function _loki_commands {
338
340
  }
339
341
 
340
342
  function _loki_start {
343
+ # Rule: list every flag bin/loki routes (Bun/engine10 ones such as --model, --max-cost, --spec, --no-pr too), not only cmd_start's own.
341
344
  _arguments \
342
345
  '--provider[AI provider]:provider name:(claude cline codex aider opencode)' \
343
346
  '--parallel[Parallel mode]' \
@@ -5,7 +5,7 @@ _loki_completion() {
5
5
  _init_completion || return
6
6
 
7
7
  # Main subcommands (must match autonomy/loki main case statement)
8
- local main_commands="start backlog quick monitor demo tour welcome init stop pause resume steer status next ship dashboard web serve api sandbox notify import github issue intent config provider reset memory compound checkpoint council dogfood projects enterprise secrets cockpit secure own handoff doctor watchdog audit metrics syslog onboard share proof receipt outcomes explain plan report cost estimate kpis stats test ci watch telemetry agent context ctx code run export review optimize heal modernize migrate cluster worktree wt trigger failover remote deploy docker mcp magic assets analyze compliance crash control workspace open otel preview quickstart rc rollback merge self-update sentrux setup-skill spec state template trust trust-metrics ultracode update verify voice why wiki bench cleanup logs grill docs cp version completions help keys answer queue issues undo acp verify-pr chat route edit setup models agents diff"
8
+ local main_commands="start backlog quick monitor demo tour welcome init stop pause resume cancel steer status next ship dashboard web serve api sandbox notify import github issue intent config provider reset memory compound checkpoint council dogfood projects enterprise secrets cockpit secure own handoff doctor watchdog audit metrics syslog onboard share proof receipt outcomes explain plan report cost estimate kpis stats test ci watch telemetry agent context ctx code run export review optimize heal modernize migrate cluster worktree wt trigger failover remote deploy docker mcp magic assets analyze compliance crash control workspace open otel preview quickstart rc rollback merge self-update sentrux setup-skill spec state template trust trust-metrics ultracode update verify voice why wiki bench cleanup logs grill docs cp version completions help keys answer queue issues undo acp verify-pr chat route edit setup models agents diff backup restore"
9
9
 
10
10
  # 1. If we are on the first argument (subcommand)
11
11
  if [[ $cword -eq 1 ]]; then
@@ -26,8 +26,9 @@ _loki_completion() {
26
26
  fi
27
27
 
28
28
  # If the word starts with a dash, show flags
29
+ # Rule: list every flag bin/loki routes (Bun/engine10 ones such as --model, --max-cost, --spec, --no-pr too), not only cmd_start's own.
29
30
  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"
31
+ 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
32
  COMPREPLY=( $(compgen -W "${flags}" -- "$cur") )
32
33
  return 0
33
34
  fi
@@ -149,7 +150,7 @@ _loki_completion() {
149
150
 
150
151
  deploy)
151
152
  if [[ "$cur" == -* ]]; then
152
- COMPREPLY=( $(compgen -W "--execute --canary --health-url --auto-rollback --dir --no-clip --allow-unanchored --help" -- "$cur") )
153
+ COMPREPLY=( $(compgen -W "--execute --canary --health-url --health-match --auto-rollback --dir --no-clip --allow-unanchored --help" -- "$cur") )
153
154
  return 0
154
155
  fi
155
156
  ;;
@@ -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.5"
11
11
 
12
12
  __all__ = ["__version__"]