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/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
- echo "Outcome: $outcome_label"
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
- echo "Your app is live at: $live_app_url (served locally on this machine)"
2701
- echo " Dashboard: ${_dash_scheme}://127.0.0.1:${DASHBOARD_PORT:-57374}/ (App Runner -> Live App)"
2702
- echo ""
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
- echo "Tasks: pending=$pending in_progress=$in_progress completed=$completed failed=$failed"
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', defaulting to 0"
3983
- BUDGET_LIMIT=0
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
- # Generic patterns that work across all providers
10391
- # - HTTP 429 status code
10392
- # - "rate limit" / "rate-limit" / "ratelimit" text
10393
- # - "too many requests" text
10394
- # - "quota exceeded" text
10395
- # - "request limit" text
10396
- # - "retry after" / "retry-after" headers
10397
- if grep -qiE '(429|rate.?limit|too many requests|quota exceeded|request limit|retry.?after)' "$log_file" 2>/dev/null; then
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" format
10402
- if grep -qE 'resets [0-9]+[ap]m' "$log_file" 2>/dev/null; then
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
- '--lines[Number of lines]:number:' \
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 gemini)' \
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
- '*:PRD File:_files'
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 {
@@ -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 logs 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 test ci watch telemetry agent context code run export review optimize heal migrate cluster worktree trigger failover remote version completions help"
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
- COMPREPLY=( $(compgen -W "claude codex gemini" -- "$cur") )
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
- # Otherwise, default to file completion (for PRD files)
33
- COMPREPLY=( $(compgen -f -- "$cur") )
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 "--follow -f --lines -n --help" -- "$cur") )
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)
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.81.1"
10
+ __version__ = "7.83.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -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
- if INTEGRITY_ENABLED:
260
- entry_json = json.dumps(entry, sort_keys=True, default=str)
261
- entry["_integrity_hash"] = _compute_chain_hash(entry_json, _last_hash)
262
- _last_hash = entry["_integrity_hash"]
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
- log_file = _get_current_log_file()
265
- _rotate_logs_if_needed(log_file)
289
+ log_file = _get_current_log_file()
290
+ _rotate_logs_if_needed(log_file)
266
291
 
267
- with open(log_file, "a") as f:
268
- f.write(json.dumps(entry) + "\n")
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