loki-mode 7.81.1 → 7.83.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +88 -27
- package/autonomy/lib/config-map.sh +61 -15
- package/autonomy/loki +641 -45
- package/autonomy/run.sh +202 -31
- package/completions/_loki +84 -6
- package/completions/loki.bash +25 -7
- package/dashboard/__init__.py +1 -1
- package/dashboard/audit.py +36 -10
- package/dashboard/registry.py +67 -24
- package/dashboard/server.py +131 -74
- package/docs/INSTALLATION.md +2 -2
- package/events/bus.py +17 -1
- package/events/emit.sh +15 -1
- package/loki-ts/dist/loki.js +7 -4
- package/lokistore/cloud.py +17 -1
- package/lokistore/factory.py +6 -1
- package/lokistore/local.py +11 -3
- package/mcp/__init__.py +1 -1
- package/mcp/server.py +16 -4
- package/memory/cross_project.py +74 -5
- package/memory/embeddings.py +19 -1
- package/memory/retrieval.py +81 -153
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/codex.sh +9 -4
- package/memory/tree_index.py +0 -499
- package/memory/tree_search.py +0 -305
package/autonomy/run.sh
CHANGED
|
@@ -2674,34 +2674,35 @@ except Exception:
|
|
|
2674
2674
|
fi
|
|
2675
2675
|
|
|
2676
2676
|
# ---- Durable human-readable file: .loki/COMPLETION.txt --------------------
|
|
2677
|
+
# Presentation-only receipt: a single fixed-width label column, the live-app
|
|
2678
|
+
# URL elevated right under the outcome headline, and a consistent rule line.
|
|
2679
|
+
# Stays pure ASCII (no emoji, no dashes, no color codes) so it pastes cleanly
|
|
2680
|
+
# into a PR description or a chat. Same facts and values as before.
|
|
2677
2681
|
{
|
|
2678
2682
|
echo "Loki Mode run summary"
|
|
2679
2683
|
echo "====================="
|
|
2680
2684
|
echo ""
|
|
2681
|
-
|
|
2682
|
-
echo "Branch: $branch"
|
|
2683
|
-
echo "Files changed: $files_changed (+$insertions / -$deletions)"
|
|
2684
|
-
echo "Finished: $ts"
|
|
2685
|
-
echo ""
|
|
2686
|
-
if [ -n "$delegate_branch" ]; then
|
|
2687
|
-
echo "Delegate branch: $delegate_branch"
|
|
2688
|
-
fi
|
|
2689
|
-
if [ -n "$pr_url" ]; then
|
|
2690
|
-
echo "Pull request: $pr_url"
|
|
2691
|
-
elif [ "$outcome" = "complete" ]; then
|
|
2692
|
-
echo "Pull request: not opened (set LOKI_DELEGATE_PR=1 to open one)"
|
|
2693
|
-
fi
|
|
2694
|
-
echo ""
|
|
2685
|
+
printf '%-14s %s\n' "Outcome:" "$outcome_label"
|
|
2695
2686
|
if [ -n "$live_app_url" ]; then
|
|
2696
2687
|
# Compute the dashboard scheme the same way start_dashboard does
|
|
2697
2688
|
# (url_scheme is local to that function, not visible here).
|
|
2698
2689
|
local _dash_scheme="http"
|
|
2699
2690
|
[ -n "${LOKI_TLS_CERT:-}" ] && [ -n "${LOKI_TLS_KEY:-}" ] && _dash_scheme="https"
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2691
|
+
printf '%-14s %s\n' "Live app:" "$live_app_url (served locally on this machine)"
|
|
2692
|
+
printf '%-14s %s\n' "Dashboard:" "${_dash_scheme}://127.0.0.1:${DASHBOARD_PORT:-57374}/ (App Runner -> Live App)"
|
|
2693
|
+
fi
|
|
2694
|
+
printf '%-14s %s\n' "Branch:" "$branch"
|
|
2695
|
+
printf '%-14s %s\n' "Files:" "$files_changed (+$insertions / -$deletions)"
|
|
2696
|
+
printf '%-14s %s\n' "Finished:" "$ts"
|
|
2697
|
+
if [ -n "$delegate_branch" ]; then
|
|
2698
|
+
printf '%-14s %s\n' "Delegate:" "$delegate_branch"
|
|
2703
2699
|
fi
|
|
2704
|
-
|
|
2700
|
+
if [ -n "$pr_url" ]; then
|
|
2701
|
+
printf '%-14s %s\n' "Pull request:" "$pr_url"
|
|
2702
|
+
elif [ "$outcome" = "complete" ]; then
|
|
2703
|
+
printf '%-14s %s\n' "Pull request:" "not opened (set LOKI_DELEGATE_PR=1 to open one)"
|
|
2704
|
+
fi
|
|
2705
|
+
printf '%-14s %s\n' "Tasks:" "pending=$pending in_progress=$in_progress completed=$completed failed=$failed"
|
|
2705
2706
|
echo ""
|
|
2706
2707
|
if [ -n "$evidence_inconclusive_line" ]; then
|
|
2707
2708
|
echo "$evidence_inconclusive_line"
|
|
@@ -2901,10 +2902,130 @@ emit_completion_summary() {
|
|
|
2901
2902
|
local outcome="${1:-complete}"
|
|
2902
2903
|
local urgency="${2:-normal}"
|
|
2903
2904
|
build_completion_summary "$outcome"
|
|
2905
|
+
# Render the screenshot-worthy completion card inline on a foreground TTY run,
|
|
2906
|
+
# AFTER the durable files are written. The card re-reads the persisted
|
|
2907
|
+
# completion.json (never recomputes) so it can never diverge from the file.
|
|
2908
|
+
print_completion_card
|
|
2904
2909
|
send_notification "${_LOKI_SUMMARY_TITLE:-Run finished}" "${_LOKI_SUMMARY_BODY:-}" "$urgency"
|
|
2905
2910
|
return 0
|
|
2906
2911
|
}
|
|
2907
2912
|
|
|
2913
|
+
#===============================================================================
|
|
2914
|
+
# print_completion_card (visible-delight completion card)
|
|
2915
|
+
#
|
|
2916
|
+
# Display-only. Renders a boxed summary card to the interactive TTY at the close
|
|
2917
|
+
# of a foreground run: outcome, branch, files changed (+ins/-del), the live-app
|
|
2918
|
+
# URL with a "try it" line when an app is running, the copy-pasteable review
|
|
2919
|
+
# command, and the recorded-assumptions count. This is the single most
|
|
2920
|
+
# screenshot-worthy moment of a run, which was previously only written to a file.
|
|
2921
|
+
#
|
|
2922
|
+
# It reads ONLY from the already-persisted .loki/state/completion.json (written
|
|
2923
|
+
# by build_completion_summary just before this is called) and the app-runner
|
|
2924
|
+
# state, so the card and the durable file are guaranteed identical. Nothing is
|
|
2925
|
+
# computed or written here.
|
|
2926
|
+
#
|
|
2927
|
+
# Gate (same shape as the HUD at the colors block): interactive stdout, not
|
|
2928
|
+
# --bg, and not opted out via LOKI_COMPLETION_CARD=0. Off-TTY / --bg / --json
|
|
2929
|
+
# paths emit nothing, so machine output stays byte-identical. Wrapped so any
|
|
2930
|
+
# internal failure still returns 0 and can never abort the completion path.
|
|
2931
|
+
#===============================================================================
|
|
2932
|
+
print_completion_card() {
|
|
2933
|
+
# Gate first: emit nothing unless interactive TTY, not background, not opted out.
|
|
2934
|
+
if ! { [ -t 1 ] && [ "${BACKGROUND_MODE:-false}" != "true" ] && [ "${LOKI_COMPLETION_CARD:-1}" != "0" ]; }; then
|
|
2935
|
+
return 0
|
|
2936
|
+
fi
|
|
2937
|
+
|
|
2938
|
+
local loki_dir="${TARGET_DIR:-.}/.loki"
|
|
2939
|
+
local _cj="$loki_dir/state/completion.json"
|
|
2940
|
+
[ -f "$_cj" ] || return 0
|
|
2941
|
+
|
|
2942
|
+
# Pull the fields we render straight from the persisted record. The python3
|
|
2943
|
+
# call emits one field per line in a fixed order; we read them line by line
|
|
2944
|
+
# so empty fields (e.g. no pr_url) keep their positions (a single delimiter
|
|
2945
|
+
# split would collapse adjacent empties). Any failure leaves the card
|
|
2946
|
+
# unrendered. Trailing-newline guard: NUL-free, fields are single-line.
|
|
2947
|
+
local _fields
|
|
2948
|
+
_fields="$(python3 -c "
|
|
2949
|
+
import json,sys
|
|
2950
|
+
try:
|
|
2951
|
+
d=json.load(open(sys.argv[1]))
|
|
2952
|
+
except Exception:
|
|
2953
|
+
sys.exit(0)
|
|
2954
|
+
def g(k):
|
|
2955
|
+
v=d.get(k,'')
|
|
2956
|
+
return '' if v is None else str(v).replace('\n',' ')
|
|
2957
|
+
for k in ['outcome','branch','files_changed','insertions','deletions',
|
|
2958
|
+
'review_cmd','pr_url','assumptions_total','assumptions_high']:
|
|
2959
|
+
print(g(k))
|
|
2960
|
+
" "$_cj" 2>/dev/null)" || return 0
|
|
2961
|
+
[ -z "$_fields" ] && return 0
|
|
2962
|
+
|
|
2963
|
+
local _outcome _branch _files _ins _del _review _pr _atotal _ahigh
|
|
2964
|
+
{
|
|
2965
|
+
IFS= read -r _outcome
|
|
2966
|
+
IFS= read -r _branch
|
|
2967
|
+
IFS= read -r _files
|
|
2968
|
+
IFS= read -r _ins
|
|
2969
|
+
IFS= read -r _del
|
|
2970
|
+
IFS= read -r _review
|
|
2971
|
+
IFS= read -r _pr
|
|
2972
|
+
IFS= read -r _atotal
|
|
2973
|
+
IFS= read -r _ahigh
|
|
2974
|
+
} <<EOF
|
|
2975
|
+
$_fields
|
|
2976
|
+
EOF
|
|
2977
|
+
|
|
2978
|
+
# Human outcome label (mirror build_completion_summary's mapping).
|
|
2979
|
+
local _label
|
|
2980
|
+
case "$_outcome" in
|
|
2981
|
+
complete) _label="Completed" ;;
|
|
2982
|
+
max_iterations) _label="Max iterations" ;;
|
|
2983
|
+
stopped) _label="Stopped" ;;
|
|
2984
|
+
force_stopped) _label="Stopped (not verified-complete)" ;;
|
|
2985
|
+
failed) _label="Failed" ;;
|
|
2986
|
+
intervention) _label="Needs input" ;;
|
|
2987
|
+
*) _label="$_outcome" ;;
|
|
2988
|
+
esac
|
|
2989
|
+
|
|
2990
|
+
# Live app URL (best-effort), same read as build_completion_summary.
|
|
2991
|
+
local _url="" _app_state="$loki_dir/app-runner/state.json"
|
|
2992
|
+
if [ -f "$_app_state" ]; then
|
|
2993
|
+
_url="$(python3 -c "import json,sys
|
|
2994
|
+
try:
|
|
2995
|
+
d=json.load(open(sys.argv[1]))
|
|
2996
|
+
print(d.get('url','') if d.get('status')=='running' else '')
|
|
2997
|
+
except Exception:
|
|
2998
|
+
print('')" "$_app_state" 2>/dev/null)" || _url=""
|
|
2999
|
+
fi
|
|
3000
|
+
|
|
3001
|
+
# Box width matches log_header (66 inner columns). Render lines that fit;
|
|
3002
|
+
# this is decoration, so over-wide content is simply not boxed-truncated
|
|
3003
|
+
# (the durable file carries the full text).
|
|
3004
|
+
echo ""
|
|
3005
|
+
echo -e "${GREEN}+================================================================+${NC}"
|
|
3006
|
+
echo -e "${GREEN}|${NC} ${BOLD}Loki Mode: ${_label}${NC}"
|
|
3007
|
+
echo -e "${GREEN}|${NC}"
|
|
3008
|
+
if [ -n "$_url" ]; then
|
|
3009
|
+
echo -e "${GREEN}|${NC} ${BOLD}${CYAN}Your app is live at ${_url}${NC} ${DIM}- open it to try it${NC}"
|
|
3010
|
+
echo -e "${GREEN}|${NC}"
|
|
3011
|
+
fi
|
|
3012
|
+
echo -e "${GREEN}|${NC} Branch: ${BOLD}${_branch}${NC}"
|
|
3013
|
+
echo -e "${GREEN}|${NC} Files: ${BOLD}${_files}${NC} changed ${GREEN}+${_ins}${NC} / ${RED}-${_del}${NC}"
|
|
3014
|
+
case "$_atotal" in ''|0) : ;; *)
|
|
3015
|
+
echo -e "${GREEN}|${NC} Spec assumptions recorded: ${BOLD}${_atotal}${NC} (${_ahigh} high) ${DIM}see .loki/assumptions/ledger.md${NC}"
|
|
3016
|
+
;;
|
|
3017
|
+
esac
|
|
3018
|
+
if [ -n "$_pr" ]; then
|
|
3019
|
+
echo -e "${GREEN}|${NC} Pull request: ${_pr}"
|
|
3020
|
+
fi
|
|
3021
|
+
echo -e "${GREEN}|${NC}"
|
|
3022
|
+
echo -e "${GREEN}|${NC} ${DIM}Review the work:${NC}"
|
|
3023
|
+
echo -e "${GREEN}|${NC} ${_review}"
|
|
3024
|
+
echo -e "${GREEN}+================================================================+${NC}"
|
|
3025
|
+
echo ""
|
|
3026
|
+
return 0
|
|
3027
|
+
}
|
|
3028
|
+
|
|
2908
3029
|
#===============================================================================
|
|
2909
3030
|
# on_run_complete (Slice 3: opt-in local git output on success)
|
|
2910
3031
|
#
|
|
@@ -3979,9 +4100,18 @@ EOF
|
|
|
3979
4100
|
if [ -n "$BUDGET_LIMIT" ]; then
|
|
3980
4101
|
# Validate budget limit is numeric before writing JSON
|
|
3981
4102
|
if ! echo "$BUDGET_LIMIT" | grep -qE '^[0-9]+(\.[0-9]+)?$'; then
|
|
3982
|
-
log_warn "Invalid BUDGET_LIMIT '$BUDGET_LIMIT',
|
|
3983
|
-
BUDGET_LIMIT=
|
|
4103
|
+
log_warn "Invalid BUDGET_LIMIT '$BUDGET_LIMIT', ignoring (no cap set)"
|
|
4104
|
+
BUDGET_LIMIT=""
|
|
4105
|
+
fi
|
|
4106
|
+
# Mirror the CLI guard: a non-positive cap (0/0.00) would make
|
|
4107
|
+
# check_budget_limit pause before any work runs. Treat it as "no cap"
|
|
4108
|
+
# rather than a silent pre-work pause.
|
|
4109
|
+
if [ -n "$BUDGET_LIMIT" ] && ! awk -v b="$BUDGET_LIMIT" 'BEGIN{exit !(b+0 > 0)}'; then
|
|
4110
|
+
log_warn "BUDGET_LIMIT '$BUDGET_LIMIT' is not greater than 0, ignoring (no cap set)"
|
|
4111
|
+
BUDGET_LIMIT=""
|
|
3984
4112
|
fi
|
|
4113
|
+
fi
|
|
4114
|
+
if [ -n "$BUDGET_LIMIT" ]; then
|
|
3985
4115
|
cat > ".loki/metrics/budget.json" << BUDGET_EOF
|
|
3986
4116
|
{
|
|
3987
4117
|
"limit": $BUDGET_LIMIT,
|
|
@@ -10386,20 +10516,32 @@ check_primary_recovery() {
|
|
|
10386
10516
|
# Returns: 0 if rate limit detected, 1 otherwise
|
|
10387
10517
|
is_rate_limited() {
|
|
10388
10518
|
local log_file="$1"
|
|
10389
|
-
|
|
10390
|
-
|
|
10391
|
-
# -
|
|
10392
|
-
#
|
|
10393
|
-
# -
|
|
10394
|
-
# - "
|
|
10395
|
-
# -
|
|
10396
|
-
|
|
10397
|
-
|
|
10519
|
+
[ -f "$log_file" ] || return 1
|
|
10520
|
+
|
|
10521
|
+
# Only consider the TAIL of the log: a real provider rate-limit appears at the
|
|
10522
|
+
# END of the iteration (the call that failed), not buried in mid-run prose.
|
|
10523
|
+
# Scanning the whole file false-positived on the agent's OWN output (a build
|
|
10524
|
+
# that printed or generated rate-limiting code -- "rate limit", "429",
|
|
10525
|
+
# "retry-after" as source/text -- wrongly triggered a multi-minute wait).
|
|
10526
|
+
local tail_txt
|
|
10527
|
+
tail_txt=$(tail -n 40 "$log_file" 2>/dev/null) || return 1
|
|
10528
|
+
|
|
10529
|
+
# Require an ERROR CONTEXT, not a bare keyword: a rate-limit token must
|
|
10530
|
+
# co-occur (same line) with a genuine provider-error frame -- an explicit
|
|
10531
|
+
# error word ("Error"/"failed"/"exceeded"), an HTTP/status frame, or the
|
|
10532
|
+
# canonical "429 Too Many Requests" phrasing. This distinguishes a real
|
|
10533
|
+
# failing API line from the words "rate limit"/"retry-after"/"429" appearing
|
|
10534
|
+
# in the model's own generated source or prose. Note: a bare "429" or a bare
|
|
10535
|
+
# "retry-after" is NOT sufficient on its own (both occur in generated code).
|
|
10536
|
+
local _err='(error|errored|failed|exceeded|http[ /]?[0-9]|status[: ]+[0-9]|too many requests)'
|
|
10537
|
+
local _rl='(rate.?limit|too many requests|quota exceeded|request limit|429[ )"]*too many|retry.?after)'
|
|
10538
|
+
if printf '%s\n' "$tail_txt" | grep -qiE "(${_rl}).*(${_err})|(${_err}).*(${_rl})" 2>/dev/null; then
|
|
10398
10539
|
return 0
|
|
10399
10540
|
fi
|
|
10400
10541
|
|
|
10401
|
-
# Claude-specific: "resets Xam/pm"
|
|
10402
|
-
|
|
10542
|
+
# Claude-specific: the explicit "resets Xam/pm" reset-time line is itself an
|
|
10543
|
+
# unambiguous provider rate-limit signal (the CLI only prints it on a limit).
|
|
10544
|
+
if printf '%s\n' "$tail_txt" | grep -qE 'resets [0-9]+[ap]m' 2>/dev/null; then
|
|
10403
10545
|
return 0
|
|
10404
10546
|
fi
|
|
10405
10547
|
|
|
@@ -16572,6 +16714,35 @@ main() {
|
|
|
16572
16714
|
exit 1
|
|
16573
16715
|
fi
|
|
16574
16716
|
|
|
16717
|
+
# v7.82: one-line "Building:" headline under the start banner so the opening
|
|
16718
|
+
# frame reflects the user's own intent (their PRD / brief / this codebase),
|
|
16719
|
+
# not a generic banner. Display-only, derived from already-resolved values:
|
|
16720
|
+
# PRD basename for a file, the recorded brief text for a brief run, or fixed
|
|
16721
|
+
# text for a no-arg in-repo run. Truncated to one tidy line. Gated to an
|
|
16722
|
+
# interactive TTY, not --bg, and opt-out via LOKI_START_HEADLINE=0, so the
|
|
16723
|
+
# off-TTY / background path is byte-identical (no output). Best-effort; a
|
|
16724
|
+
# failure here must never affect parsing or the build flow.
|
|
16725
|
+
if [ -t 1 ] && [ "${BACKGROUND_MODE:-false}" != "true" ] && [ "${LOKI_START_HEADLINE:-1}" != "0" ]; then
|
|
16726
|
+
local _headline=""
|
|
16727
|
+
if [ -n "$PRD_PATH" ]; then
|
|
16728
|
+
_headline="$(basename "$PRD_PATH" 2>/dev/null || echo "$PRD_PATH")"
|
|
16729
|
+
elif [ -f ".loki/state/brief.txt" ]; then
|
|
16730
|
+
# Recorded one-line brief (written by cmd_start). Collapse to a single
|
|
16731
|
+
# line and truncate to ~60 chars so the banner stays clean.
|
|
16732
|
+
local _brief
|
|
16733
|
+
_brief="$(tr '\n' ' ' < .loki/state/brief.txt 2>/dev/null | sed 's/ */ /g; s/^ //; s/ $//')"
|
|
16734
|
+
if [ -n "$_brief" ]; then
|
|
16735
|
+
if [ "${#_brief}" -gt 60 ]; then
|
|
16736
|
+
_brief="${_brief:0:57}..."
|
|
16737
|
+
fi
|
|
16738
|
+
_headline="\"$_brief\""
|
|
16739
|
+
fi
|
|
16740
|
+
fi
|
|
16741
|
+
[ -z "$_headline" ] && _headline="analyzing this codebase"
|
|
16742
|
+
echo -e " ${BOLD}${CYAN}Building: ${_headline}${NC}"
|
|
16743
|
+
echo ""
|
|
16744
|
+
fi
|
|
16745
|
+
|
|
16575
16746
|
# Handle background mode
|
|
16576
16747
|
if [ "$BACKGROUND_MODE" = "true" ]; then
|
|
16577
16748
|
# Initialize .loki directory first
|
package/completions/_loki
CHANGED
|
@@ -11,7 +11,7 @@ function _loki {
|
|
|
11
11
|
case $state in
|
|
12
12
|
(args)
|
|
13
13
|
case $line[1] in
|
|
14
|
-
start)
|
|
14
|
+
start|quick)
|
|
15
15
|
_loki_start
|
|
16
16
|
;;
|
|
17
17
|
council)
|
|
@@ -67,8 +67,12 @@ function _loki {
|
|
|
67
67
|
_arguments \
|
|
68
68
|
'--follow[Follow logs in real-time]' \
|
|
69
69
|
'-f[Follow logs in real-time]' \
|
|
70
|
-
'--
|
|
71
|
-
'-n[Number of lines]:number:'
|
|
70
|
+
'--tail[Number of lines to show]:number:' \
|
|
71
|
+
'-n[Number of lines to show]:number:' \
|
|
72
|
+
'--all[Show all lines]' \
|
|
73
|
+
'-a[Show all lines]' \
|
|
74
|
+
'--help[Show help]' \
|
|
75
|
+
'-h[Show help]'
|
|
72
76
|
;;
|
|
73
77
|
issue)
|
|
74
78
|
_loki_issue
|
|
@@ -97,7 +101,7 @@ function _loki {
|
|
|
97
101
|
'1:subcommand:(list show open share)'
|
|
98
102
|
;;
|
|
99
103
|
completions)
|
|
100
|
-
_arguments '1:shell:(bash zsh)'
|
|
104
|
+
_arguments '1:shell:(bash zsh install)'
|
|
101
105
|
;;
|
|
102
106
|
monitor)
|
|
103
107
|
_directories
|
|
@@ -118,6 +122,7 @@ function _loki_commands {
|
|
|
118
122
|
commands=(
|
|
119
123
|
'start:Start Loki Mode'
|
|
120
124
|
'quick:Quick single-task mode'
|
|
125
|
+
'quickstart:Guided first build from your idea'
|
|
121
126
|
'monitor:Monitor Docker Compose services with auto-fix'
|
|
122
127
|
'demo:Interactive 60-second demo'
|
|
123
128
|
'init:Interactive PRD builder'
|
|
@@ -125,11 +130,16 @@ function _loki_commands {
|
|
|
125
130
|
'pause:Pause execution'
|
|
126
131
|
'resume:Resume execution'
|
|
127
132
|
'status:Show status'
|
|
133
|
+
'next:Run the right next step for the current build'
|
|
134
|
+
'ship:Finish the build (gates + PR advice) in one command'
|
|
135
|
+
'why:Explain why the last run did what it did'
|
|
128
136
|
'dashboard:Dashboard commands'
|
|
137
|
+
'web:Start the web app surface'
|
|
129
138
|
'logs:View session logs'
|
|
130
139
|
'serve:Start API server'
|
|
131
140
|
'api:API server commands'
|
|
132
141
|
'sandbox:Docker sandbox commands'
|
|
142
|
+
'docker:Run Loki in a Docker container'
|
|
133
143
|
'notify:Notification commands'
|
|
134
144
|
'import:Import GitHub issues'
|
|
135
145
|
'issue:GitHub issue commands'
|
|
@@ -149,8 +159,66 @@ function _loki_commands {
|
|
|
149
159
|
'metrics:Session productivity report'
|
|
150
160
|
'share:Share session report as GitHub Gist'
|
|
151
161
|
'proof:Inspect/share proof-of-run artifacts'
|
|
162
|
+
'preview:Preview the locally-running app'
|
|
163
|
+
'deploy:Deploy the built product (CI/CD-aware)'
|
|
152
164
|
'context:Context window management'
|
|
165
|
+
'ctx:Context window management (alias)'
|
|
153
166
|
'code:Codebase intelligence'
|
|
167
|
+
'web:Start the web dashboard'
|
|
168
|
+
'plan:Preview the build plan for a spec'
|
|
169
|
+
'report:Reporting commands (cost, kpis, share)'
|
|
170
|
+
'cost:Show cost of recent runs'
|
|
171
|
+
'kpis:Key performance indicators'
|
|
172
|
+
'stats:Session statistics'
|
|
173
|
+
'preview:Open the running app preview'
|
|
174
|
+
'deploy:Advisory deploy command (print-only)'
|
|
175
|
+
'docker:Run Loki in a Docker container'
|
|
176
|
+
'mcp:MCP server commands'
|
|
177
|
+
'magic:Spec-driven component generation'
|
|
178
|
+
'assets:Export/import shareable team assets'
|
|
179
|
+
'spec:Living spec drift detection'
|
|
180
|
+
'verify:Deterministic PR verification'
|
|
181
|
+
'grill:Interrogate a spec before building'
|
|
182
|
+
'trust:Visible trust trajectory'
|
|
183
|
+
'trust-metrics:Trust metrics report'
|
|
184
|
+
'why:Explain the last build outcome'
|
|
185
|
+
'heal:Legacy system healing'
|
|
186
|
+
'modernize:Modernize a legacy system'
|
|
187
|
+
'migrate:Migration commands'
|
|
188
|
+
'analyze:Analyze a codebase'
|
|
189
|
+
'compliance:Compliance reporting'
|
|
190
|
+
'crash:Crash report management'
|
|
191
|
+
'otel:OpenTelemetry management'
|
|
192
|
+
'wiki:Wiki commands'
|
|
193
|
+
'explain:Explain Loki concepts'
|
|
194
|
+
'optimize:Optimization commands'
|
|
195
|
+
'review:Code review commands'
|
|
196
|
+
'export:Export session data'
|
|
197
|
+
'test:Run tests'
|
|
198
|
+
'ci:CI helpers'
|
|
199
|
+
'watch:Watch mode'
|
|
200
|
+
'audit:Audit commands'
|
|
201
|
+
'syslog:System log commands'
|
|
202
|
+
'cluster:Cluster commands'
|
|
203
|
+
'failover:Failover commands'
|
|
204
|
+
'remote:Remote execution commands'
|
|
205
|
+
'worktree:Git worktree commands'
|
|
206
|
+
'wt:Git worktree commands (alias)'
|
|
207
|
+
'trigger:Trigger commands'
|
|
208
|
+
'cleanup:Clean up Loki state'
|
|
209
|
+
'rollback:Roll back a change'
|
|
210
|
+
'update:Update Loki Mode'
|
|
211
|
+
'self-update:Update Loki Mode'
|
|
212
|
+
'setup-skill:Set up the Claude Code skill'
|
|
213
|
+
'quickstart:Quickstart guide'
|
|
214
|
+
'sentrux:Sentrux integration'
|
|
215
|
+
'state:Inspect run state'
|
|
216
|
+
'template:PRD template commands'
|
|
217
|
+
'ultracode:Ultra code review'
|
|
218
|
+
'voice:Voice commands'
|
|
219
|
+
'bench:Run benchmarks'
|
|
220
|
+
'open:Open a project resource'
|
|
221
|
+
'rc:Release candidate commands'
|
|
154
222
|
'version:Show version'
|
|
155
223
|
'completions:Output shell completions'
|
|
156
224
|
'help:Show help'
|
|
@@ -160,7 +228,7 @@ function _loki_commands {
|
|
|
160
228
|
|
|
161
229
|
function _loki_start {
|
|
162
230
|
_arguments \
|
|
163
|
-
'--provider[AI provider]:provider name:(claude codex
|
|
231
|
+
'--provider[AI provider]:provider name:(claude codex cline aider)' \
|
|
164
232
|
'--parallel[Parallel mode]' \
|
|
165
233
|
'--background[Background mode]' \
|
|
166
234
|
'--bg[Background mode]' \
|
|
@@ -174,7 +242,17 @@ function _loki_start {
|
|
|
174
242
|
'-y[Skip confirmation prompts]' \
|
|
175
243
|
'--budget[Cost budget limit in USD]:amount:' \
|
|
176
244
|
'--max-iterations[Max iterations]:number:' \
|
|
177
|
-
'*:
|
|
245
|
+
'*:spec file:_loki_spec_files'
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
# Bias spec completion toward the file shapes Loki accepts (.md/.json/.txt/
|
|
249
|
+
# .yaml/.yml), while still offering directories so the user can navigate into a
|
|
250
|
+
# subfolder. Mirrors the bash completion. Makes the most-common command
|
|
251
|
+
# tab-driven and typo-proof, cutting "PRD file not found" mistakes.
|
|
252
|
+
function _loki_spec_files {
|
|
253
|
+
_alternative \
|
|
254
|
+
'specs:spec file:_files -g "*.(md|json|txt|yaml|yml)"' \
|
|
255
|
+
'dirs:directory:_files -/'
|
|
178
256
|
}
|
|
179
257
|
|
|
180
258
|
function _loki_council {
|
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 quick monitor demo init stop pause resume status dashboard
|
|
8
|
+
local main_commands="start quick monitor demo init stop pause resume status next ship dashboard web serve api sandbox notify import github issue config provider reset memory compound checkpoint council dogfood projects enterprise secrets doctor watchdog audit metrics syslog onboard share proof explain plan report cost 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 open otel preview quickstart rc rollback 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"
|
|
9
9
|
|
|
10
10
|
# 1. If we are on the first argument (subcommand)
|
|
11
11
|
if [[ $cword -eq 1 ]]; then
|
|
@@ -15,10 +15,13 @@ _loki_completion() {
|
|
|
15
15
|
|
|
16
16
|
# 2. Handle subcommands and their specific flags/args
|
|
17
17
|
case "${words[1]}" in
|
|
18
|
-
start)
|
|
18
|
+
start|quick)
|
|
19
19
|
# If the previous word was --provider, show provider names
|
|
20
20
|
if [[ "$prev" == "--provider" ]]; then
|
|
21
|
-
|
|
21
|
+
# Active providers (loki rejects gemini since v7.5.18; cline/aider
|
|
22
|
+
# are supported). Keep in sync with providers/loader.sh
|
|
23
|
+
# SUPPORTED_PROVIDERS.
|
|
24
|
+
COMPREPLY=( $(compgen -W "claude codex cline aider" -- "$cur") )
|
|
22
25
|
return 0
|
|
23
26
|
fi
|
|
24
27
|
|
|
@@ -29,8 +32,23 @@ _loki_completion() {
|
|
|
29
32
|
return 0
|
|
30
33
|
fi
|
|
31
34
|
|
|
32
|
-
#
|
|
33
|
-
|
|
35
|
+
# Bias toward the spec-shaped files Loki actually accepts
|
|
36
|
+
# (.md/.json/.txt/.yaml/.yml), plus directories so the user can still
|
|
37
|
+
# navigate into a subfolder. This makes the most-common command
|
|
38
|
+
# tab-driven and typo-proof, cutting "PRD file not found" mistakes.
|
|
39
|
+
# If the spec-filtered set is empty (no specs here), fall back to
|
|
40
|
+
# generic file completion so nothing the user could type is lost.
|
|
41
|
+
local _specs _dirs _eg_was_set=0
|
|
42
|
+
shopt -q extglob && _eg_was_set=1
|
|
43
|
+
shopt -s extglob
|
|
44
|
+
_specs=$(compgen -f -X '!*.@(md|json|txt|yaml|yml)' -- "$cur")
|
|
45
|
+
[[ "$_eg_was_set" -eq 0 ]] && shopt -u extglob
|
|
46
|
+
_dirs=$(compgen -d -- "$cur")
|
|
47
|
+
if [[ -n "$_specs" || -n "$_dirs" ]]; then
|
|
48
|
+
COMPREPLY=( $_specs $_dirs )
|
|
49
|
+
else
|
|
50
|
+
COMPREPLY=( $(compgen -f -- "$cur") )
|
|
51
|
+
fi
|
|
34
52
|
;;
|
|
35
53
|
|
|
36
54
|
council)
|
|
@@ -119,7 +137,7 @@ _loki_completion() {
|
|
|
119
137
|
|
|
120
138
|
logs)
|
|
121
139
|
if [[ "$cur" == -* ]]; then
|
|
122
|
-
COMPREPLY=( $(compgen -W "--
|
|
140
|
+
COMPREPLY=( $(compgen -W "--tail -n --all -a --follow -f --help -h" -- "$cur") )
|
|
123
141
|
return 0
|
|
124
142
|
fi
|
|
125
143
|
;;
|
|
@@ -195,7 +213,7 @@ _loki_completion() {
|
|
|
195
213
|
;;
|
|
196
214
|
|
|
197
215
|
completions)
|
|
198
|
-
COMPREPLY=( $(compgen -W "bash zsh" -- "$cur") )
|
|
216
|
+
COMPREPLY=( $(compgen -W "bash zsh install" -- "$cur") )
|
|
199
217
|
;;
|
|
200
218
|
|
|
201
219
|
context|ctx)
|
package/dashboard/__init__.py
CHANGED
package/dashboard/audit.py
CHANGED
|
@@ -21,6 +21,7 @@ import logging.handlers
|
|
|
21
21
|
import os
|
|
22
22
|
import socket
|
|
23
23
|
import sys
|
|
24
|
+
import threading
|
|
24
25
|
from datetime import datetime, timezone
|
|
25
26
|
from pathlib import Path
|
|
26
27
|
from typing import Any, Optional
|
|
@@ -47,6 +48,16 @@ _SYSLOG_PROTO = os.environ.get("LOKI_AUDIT_SYSLOG_PROTO", "udp").lower().strip()
|
|
|
47
48
|
INTEGRITY_ENABLED = os.environ.get("LOKI_AUDIT_NO_INTEGRITY", "").lower() not in ("true", "1", "yes")
|
|
48
49
|
_last_hash: str = "0" * 64 # Genesis hash
|
|
49
50
|
|
|
51
|
+
# Serializes the chain read-modify-write + file append in log_event(). Without
|
|
52
|
+
# it, concurrent callers (the dashboard fans audit writes out across async
|
|
53
|
+
# handlers / asyncio.to_thread workers) interleave the unsynchronized
|
|
54
|
+
# _last_hash RMW with the file append: lines get written in a different order
|
|
55
|
+
# than the hashes were chained, which breaks the tamper-evident chain so
|
|
56
|
+
# verify_all_logs() reports valid:False even though no entry was tampered with.
|
|
57
|
+
# Holding this lock makes "compute hash, update _last_hash, append the line" a
|
|
58
|
+
# single atomic step so on-disk line order always matches chain order.
|
|
59
|
+
_hash_lock = threading.Lock()
|
|
60
|
+
|
|
50
61
|
|
|
51
62
|
def _recover_last_hash() -> str:
|
|
52
63
|
"""Recover the last integrity hash from the most recent audit log file.
|
|
@@ -254,20 +265,35 @@ def log_event(
|
|
|
254
265
|
"details": details or {},
|
|
255
266
|
}
|
|
256
267
|
|
|
257
|
-
# Tamper-evident chain hash
|
|
268
|
+
# Tamper-evident chain hash + file append, serialized as one atomic step.
|
|
269
|
+
#
|
|
270
|
+
# The chain hash is a read-modify-write of the module-global _last_hash, and
|
|
271
|
+
# the line must land in the file in the same order the hashes were chained.
|
|
272
|
+
# _hash_lock guards the whole "compute hash -> update _last_hash -> append"
|
|
273
|
+
# critical section so concurrent callers cannot interleave and break the
|
|
274
|
+
# chain (see _hash_lock definition above).
|
|
275
|
+
#
|
|
276
|
+
# NOTE for async callers: log_event() does blocking file I/O while holding
|
|
277
|
+
# this lock. When called from an asyncio handler it SHOULD be offloaded with
|
|
278
|
+
# `await asyncio.to_thread(audit.log_event, ...)` so a slow disk does not
|
|
279
|
+
# stall the dashboard event loop. The thread-safety here is what makes that
|
|
280
|
+
# offload safe; rewriting every call site to async is a separate, larger
|
|
281
|
+
# change and is intentionally not done here.
|
|
258
282
|
global _last_hash
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
283
|
+
with _hash_lock:
|
|
284
|
+
if INTEGRITY_ENABLED:
|
|
285
|
+
entry_json = json.dumps(entry, sort_keys=True, default=str)
|
|
286
|
+
entry["_integrity_hash"] = _compute_chain_hash(entry_json, _last_hash)
|
|
287
|
+
_last_hash = entry["_integrity_hash"]
|
|
263
288
|
|
|
264
|
-
|
|
265
|
-
|
|
289
|
+
log_file = _get_current_log_file()
|
|
290
|
+
_rotate_logs_if_needed(log_file)
|
|
266
291
|
|
|
267
|
-
|
|
268
|
-
|
|
292
|
+
with open(log_file, "a") as f:
|
|
293
|
+
f.write(json.dumps(entry) + "\n")
|
|
269
294
|
|
|
270
|
-
# Forward to syslog if configured
|
|
295
|
+
# Forward to syslog if configured (outside the lock: fire-and-forget and
|
|
296
|
+
# must never extend the critical section / block other writers).
|
|
271
297
|
_forward_to_syslog(entry)
|
|
272
298
|
|
|
273
299
|
return entry
|