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.
- package/SKILL.md +3 -3
- package/VERSION +1 -1
- package/autonomy/grill.sh +27 -3
- package/autonomy/hooks/precompact-flush.sh +51 -0
- package/autonomy/hooks/validate-bash.sh +941 -13
- package/autonomy/lib/chat.sh +116 -7
- package/autonomy/lib/claude-flags.sh +157 -0
- package/autonomy/lib/claude_permissions.py +60 -0
- package/autonomy/lib/decision_record.py +92 -3
- package/autonomy/lib/done-recognition.sh +8 -4
- package/autonomy/lib/efficiency_cost.py +102 -0
- package/autonomy/lib/engine10-push.sh +30 -5
- package/autonomy/lib/modernize/py_capture.py +62 -3
- package/autonomy/lib/prd-enrich.sh +30 -2
- package/autonomy/lib/proof-generator.py +5 -0
- package/autonomy/lib/proof-verify.py +109 -1
- package/autonomy/lib/trust_trajectory.py +13 -4
- package/autonomy/lib/voter-agents.sh +64 -7
- package/autonomy/lib/wiki-generator.py +58 -10
- package/autonomy/lib/wiki_llm.py +41 -2
- package/autonomy/loki +548 -36
- package/autonomy/quickstart.sh +21 -2
- package/autonomy/run.sh +217 -16
- package/autonomy/trigger-server.py +81 -7
- package/completions/_loki +3 -0
- package/completions/loki.bash +4 -3
- package/dashboard/__init__.py +1 -1
- package/dashboard/api_start.py +2 -2
- package/dashboard/audit.py +204 -17
- package/dashboard/build_supervisor.py +2 -0
- package/docs/CLI-REFERENCE.md +5 -1
- package/docs/EVALUATING.md +3 -3
- package/docs/INSTALLATION.md +1 -1
- package/docs/environment-variables.md +1 -0
- package/docs/trackers.md +6 -0
- package/docs/v10/BOARD.md +27 -1
- package/docs/v10/CLI-MODERN.md +10 -7
- package/docs/v10/DECISIONS.md +16 -0
- package/docs/v10/ENGINE.md +2 -1
- package/docs/v10/FAILURE-CLASSES.md +77 -0
- package/docs/v10/FOUNDER-QUEUE.md +1 -0
- package/docs/v10/GUIDE-MODERNIZE.md +9 -4
- package/docs/v10/PLAN-V12.md +896 -0
- package/docs/v10/PROGRESS.md +11 -0
- package/docs/v11/COST-HALF.md +3 -1
- package/docs/visual-evidence.md +2 -0
- package/loki-ts/data/finding-schema.json +2 -1
- package/loki-ts/dist/loki.js +1180 -1112
- package/magic/core/debate.py +27 -1
- package/mcp/__init__.py +1 -1
- package/memory/retrieval.py +15 -3
- package/memory/schemas.py +43 -1
- package/memory/storage.py +11 -0
- package/package.json +2 -2
- package/packages/control-plane/dist/server.js +4154 -3551
- package/packages/control-plane/src/server/legacy/routes-control.ts +5 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/claude.sh +7 -3
- package/providers/loader.sh +26 -0
- package/providers/model_catalog.json +6 -0
- package/providers/model_routes.sh +31 -0
- package/src/policies/claude_permissions.js +132 -0
- package/web-app/dist/assets/{AdminPage-bZlvH4TL.js → AdminPage-C0v2p3jN.js} +1 -1
- package/web-app/dist/assets/{Avatar-BZrBstQe.js → Avatar-Cgc0QepK.js} +1 -1
- package/web-app/dist/assets/{Badge-Q8l8r4Vq.js → Badge-1X4wQ0sU.js} +1 -1
- package/web-app/dist/assets/{Button-B8Bt0A3S.js → Button-CG79n6-I.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-CIYExrXa.js → CockpitPage-rSw2BTKk.js} +1 -1
- package/web-app/dist/assets/{ComparePage-CBOmHdam.js → ComparePage-B9EVp__S.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-BKwDPSnK.js → ErrorBoundary-DLQKY8LF.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-KqzjphWl.js → EvidenceReceiptPanel-DE0L1L2Q.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-lF3GkHRv.js → GitHubIssuesPanel-DlKtYrtk.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-BCzipPWD.js → GitHubPRsPanel-C5c5tY3x.js} +1 -1
- package/web-app/dist/assets/{HomePage-CIdnkzaN.js → HomePage-ob1natMp.js} +2 -2
- package/web-app/dist/assets/{LoginPage-D-178nTT.js → LoginPage-CCQBgvzB.js} +1 -1
- package/web-app/dist/assets/{MagicPage-BmHuzMT8.js → MagicPage-CKSuC0Pp.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-CPCiyDFn.js → MetricsPage-nq_ygAuG.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-B2T6n1gx.js → NotFoundPage-B6gVLTAn.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-DQmpFhb0.js → ProjectPage-pUnvGriO.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-BmugzFEd.js → ProjectsPage-DMFPGLkE.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-ZB-RrmgM.js → SettingsPage-DAB_cp87.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-BAhwHt4_.js → ShowcasePage-DTsx_XxI.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-Bt7sf2Vu.js → SystemSettingsPage-C2UUqWhW.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-B-v5j_8q.js → TeamsPage-C4Sws9U7.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-bdqUL-LG.js → TemplatesPage-B1-79O4n.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-CRHEXBwS.js → TerminalOutput-CDSVt5TB.js} +1 -1
- package/web-app/dist/assets/{activity-C-ROkmXt.js → activity-CBEULbio.js} +1 -1
- package/web-app/dist/assets/{bell-DbiCBxQA.js → bell-Dp-j77Qo.js} +1 -1
- package/web-app/dist/assets/{bot-DsIgE_qr.js → bot-Dh1FlTEJ.js} +1 -1
- package/web-app/dist/assets/{check-CkAdmHWP.js → check-C_7N7W4q.js} +1 -1
- package/web-app/dist/assets/{chevron-left-CYTLivAo.js → chevron-left-DtZJiu3T.js} +1 -1
- package/web-app/dist/assets/{circle-alert-BVIcAsFJ.js → circle-alert-CJK4sNof.js} +1 -1
- package/web-app/dist/assets/{clock-Dg3Rq0go.js → clock--0bfAUkP.js} +1 -1
- package/web-app/dist/assets/{cloud-CenrqHFl.js → cloud-CRa9nwiN.js} +1 -1
- package/web-app/dist/assets/{code-xml-C2mvfoug.js → code-xml-4gRrVGxx.js} +1 -1
- package/web-app/dist/assets/{database-BKa0_eCb.js → database-C-A2USGl.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-DfelUK1S.js → dollar-sign-BshJt4ML.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-CZKWoHVg.js → file-code-corner-Bd--i_IM.js} +1 -1
- package/web-app/dist/assets/{file-plus-CZ-m1zMX.js → file-plus-YiNg-aWn.js} +1 -1
- package/web-app/dist/assets/{globe-S0kv_GwC.js → globe-CfyHbAbI.js} +1 -1
- package/web-app/dist/assets/{hammer-BW1MDdu1.js → hammer-BlGC-yqz.js} +1 -1
- package/web-app/dist/assets/{index-ACEYsTc2.js → index-BlCg8mla.js} +3 -3
- package/web-app/dist/assets/{layers-BrXHYZ-L.js → layers-DLiA0AcW.js} +1 -1
- package/web-app/dist/assets/{loader-circle-DOdhKFx0.js → loader-circle-CkOfS_Q6.js} +1 -1
- package/web-app/dist/assets/{lock-BcFTyg8J.js → lock-C5vRE8HM.js} +1 -1
- package/web-app/dist/assets/{package-D34cMVQG.js → package-BPzp43h4.js} +1 -1
- package/web-app/dist/assets/{plus-CEaMeBxL.js → plus-C4x6sD5J.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-D2Rzxr5U.js → refresh-cw-DZn-gB9J.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-C_-EjRLh.js → rotate-ccw-rH63YhnY.js} +1 -1
- package/web-app/dist/assets/{scroll-text-BSSCMI_c.js → scroll-text-DTvZMQuh.js} +1 -1
- package/web-app/dist/assets/{server-BKvLonWt.js → server-DmwCdhKT.js} +1 -1
- package/web-app/dist/assets/{shield-alert-DbbQbPdW.js → shield-alert-Dn5Rtkhx.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-NoBD1fUR.js → thumbs-up-D_lHdH-1.js} +1 -1
- package/web-app/dist/assets/{trash-2-BXz8rAyd.js → trash-2-CVfeP0Fb.js} +1 -1
- package/web-app/dist/assets/{trending-down-D8u535Re.js → trending-down-ouPrjBEm.js} +1 -1
- package/web-app/dist/assets/{trending-up-q8k3kgiy.js → trending-up-7H96_KlR.js} +1 -1
- package/web-app/dist/assets/{upload-Cp7_zeBI.js → upload-kTTgyfD6.js} +1 -1
- package/web-app/dist/assets/{usePolling-CTHVdcuX.js → usePolling-BFIMPvNM.js} +1 -1
- package/web-app/dist/assets/{user-DF7bfSf_.js → user-UDo6EQfn.js} +1 -1
- package/web-app/dist/index.html +1 -1
package/autonomy/quickstart.sh
CHANGED
|
@@ -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
|
|
848
|
+
claude "${_LOKI_PERM_ARGV[@]}" -p "$prompt" 2>/dev/null) || rc=$?
|
|
830
849
|
else
|
|
831
850
|
out=$(CAVEMAN_DEFAULT_MODE=off \
|
|
832
|
-
claude
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
(
|
|
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="
|
|
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="
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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_.-]
|
|
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
|
-
|
|
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
|
-
|
|
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]' \
|
package/completions/loki.bash
CHANGED
|
@@ -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
|
;;
|