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/loki CHANGED
@@ -265,6 +265,30 @@ _loki_new_session_exec() {
265
265
  fi
266
266
  }
267
267
 
268
+ # Actionable PRD-not-found exit. Both PRD-path existence guards in cmd_start route
269
+ # through here so they cannot drift: the user always learns the two recoveries
270
+ # (fix the path, or drop the arg to auto-generate from the codebase). If a sibling
271
+ # .md file looks like a near-miss of the bad path, surface it honestly as a hint.
272
+ # Prints to stderr and exits 1. Honest: only suggests a "did you mean" when a real
273
+ # .md sibling exists; never fabricates a path.
274
+ _prd_not_found_die() {
275
+ local bad_path="$1"
276
+ echo -e "${RED}Error: PRD file not found: $bad_path${NC}" >&2
277
+ echo -e "${YELLOW}Hint: provide a path to an existing .md/.json/.txt/.yaml/.yml file, or run 'loki start' with no args to analyze the current directory.${NC}" >&2
278
+ # Best-effort near-miss suggestion: if a .md file sits in the same directory
279
+ # as the bad path, point at it. Read-only; silent when nothing matches.
280
+ local dir base suggestion
281
+ dir="$(dirname "$bad_path" 2>/dev/null)" || dir="."
282
+ base="$(basename "$bad_path" 2>/dev/null)"
283
+ if [ -d "$dir" ] && [ -n "$base" ]; then
284
+ suggestion="$(ls -1 "$dir"/*.md 2>/dev/null | head -1)"
285
+ if [ -n "$suggestion" ]; then
286
+ echo -e "${YELLOW}Did you mean: $suggestion ?${NC}" >&2
287
+ fi
288
+ fi
289
+ exit 1
290
+ }
291
+
268
292
  # Anonymous usage telemetry
269
293
  PROJECT_DIR="$SKILL_DIR"
270
294
  _TELEMETRY_SCRIPT="$SKILL_DIR/autonomy/telemetry.sh"
@@ -714,6 +738,7 @@ show_help() {
714
738
  echo ""
715
739
  echo "Session:"
716
740
  echo " status [--json] Show current status (--json for machine-readable)"
741
+ echo " next Run the right next step for you (resume / ship / why)"
717
742
  echo " stop Stop execution immediately"
718
743
  echo " pause Pause after current session"
719
744
  echo " resume Resume paused execution"
@@ -745,7 +770,7 @@ show_help() {
745
770
  echo " version Show version"
746
771
  echo " help Show this help ('loki help aliases' for old names)"
747
772
  echo ""
748
- echo "More commands (grill, spec, deploy, cleanup, init, watch, demo, web, api,"
773
+ echo "More commands (ship, grill, spec, deploy, cleanup, init, watch, demo, web, api,"
749
774
  echo "logs, github, import, council, proof, audit, compliance, agent, template,"
750
775
  echo "magic, docs, wiki, ci, test, bench, secrets, telemetry, crash, worktree,"
751
776
  echo "failover, monitor, remote, ...) are dispatchable and documented via"
@@ -909,6 +934,7 @@ show_landing() {
909
934
  echo -e "First time here? ${CYAN}loki doctor${NC} checks your setup (an AI provider CLI is required)."
910
935
  echo ""
911
936
  echo "Get started:"
937
+ echo -e " ${CYAN}loki quickstart${NC} Guided first build from your idea (no PRD needed)"
912
938
  echo -e " ${CYAN}loki start ./prd.md${NC} Build from a spec (PRD file, GitHub issue, or no arg)"
913
939
  echo -e " ${CYAN}loki demo${NC} Build a sample todo app end to end (real run)"
914
940
  echo -e " ${CYAN}loki dashboard start${NC} Start the live run monitor (then: loki dashboard open)"
@@ -1386,6 +1412,13 @@ cmd_start() {
1386
1412
  echo -e "${RED}--budget requires a numeric USD amount (e.g., --budget 5.00)${NC}"
1387
1413
  exit 1
1388
1414
  fi
1415
+ # Reject a non-positive budget: a 0 (or 0.00) cap makes
1416
+ # check_budget_limit pause before any work runs (cost >= 0 is
1417
+ # always true), which looks like a silent hang. Require > 0.
1418
+ if ! awk -v b="$2" 'BEGIN{exit !(b+0 > 0)}'; then
1419
+ echo -e "${RED}--budget must be greater than 0 (a 0 budget pauses before any work)${NC}" >&2
1420
+ exit 1
1421
+ fi
1389
1422
  export LOKI_BUDGET_LIMIT="$2"
1390
1423
  shift 2
1391
1424
  else
@@ -1399,6 +1432,11 @@ cmd_start() {
1399
1432
  echo -e "${RED}--budget requires a numeric USD amount (e.g., --budget=5.00)${NC}"
1400
1433
  exit 1
1401
1434
  fi
1435
+ # Reject a non-positive budget (see --budget arm above).
1436
+ if ! awk -v b="$budget_val" 'BEGIN{exit !(b+0 > 0)}'; then
1437
+ echo -e "${RED}--budget must be greater than 0 (a 0 budget pauses before any work)${NC}" >&2
1438
+ exit 1
1439
+ fi
1402
1440
  export LOKI_BUDGET_LIMIT="$budget_val"
1403
1441
  shift
1404
1442
  ;;
@@ -1613,6 +1651,10 @@ cmd_start() {
1613
1651
  version=$(get_version)
1614
1652
  local _ttfv_max_iter="${LOKI_MAX_ITERATIONS:-3}"
1615
1653
  mkdir -p "$LOKI_DIR" 2>/dev/null || true
1654
+ # Reap stale per-PID temp PRDs from prior runs (the process exec-replaces
1655
+ # into run.sh, so these are never cleaned on exit and accumulate).
1656
+ find "$LOKI_DIR" -maxdepth 1 -name 'brief-prd-*.md' -mtime +1 -delete 2>/dev/null || true
1657
+ find "$LOKI_DIR" -maxdepth 1 -name 'quick-prd-*.md' -mtime +1 -delete 2>/dev/null || true
1616
1658
  local brief_prd="$LOKI_DIR/brief-prd-$$.md"
1617
1659
  synthesize_brief_prd "$brief_prd" "$brief_text"
1618
1660
  prd_file="$brief_prd"
@@ -1670,9 +1712,7 @@ cmd_start() {
1670
1712
  case "$prd_file" in
1671
1713
  *.md|*.json|*.txt|*.yaml|*.yml)
1672
1714
  if [ ! -f "$prd_file" ]; then
1673
- echo "Error: PRD file not found: $prd_file" >&2
1674
- echo "Hint: provide a path to an existing .md/.json/.txt/.yaml/.yml file, or run 'loki start' with no args to analyze the current directory." >&2
1675
- exit 1
1715
+ _prd_not_found_die "$prd_file"
1676
1716
  fi
1677
1717
  ;;
1678
1718
  esac
@@ -1695,8 +1735,7 @@ cmd_start() {
1695
1735
  case "$prd_file" in
1696
1736
  *.md|*.json|*.txt|*.yaml|*.yml)
1697
1737
  if [ ! -f "$prd_file" ]; then
1698
- echo -e "${RED}Error: PRD file not found: $prd_file${NC}" >&2
1699
- exit 1
1738
+ _prd_not_found_die "$prd_file"
1700
1739
  fi
1701
1740
  ;;
1702
1741
  esac
@@ -1997,7 +2036,13 @@ cmd_start() {
1997
2036
  fi
1998
2037
  if [ -n "$_existing_pid" ] && kill -0 "$_existing_pid" 2>/dev/null; then
1999
2038
  echo -e "${RED}Error: another loki instance is running (pid $_existing_pid).${NC}" >&2
2000
- echo -e "${YELLOW}Run 'loki stop' first, then retry 'loki start'.${NC}" >&2
2039
+ # 'loki stop' is folder-scoped by default (v7.7.30), so it only stops a
2040
+ # run in THIS directory. If the live instance is in another project,
2041
+ # 'loki stop' here reports "No active session" and the user is stuck in
2042
+ # a start-says-running / stop-says-clean loop. Point at the global stop
2043
+ # and at 'loki status' to find the live run.
2044
+ echo -e "${YELLOW}Run 'loki stop' here, or 'loki stop --all' if it is running in another project.${NC}" >&2
2045
+ echo -e "${YELLOW}See 'loki status' for the live run, then retry 'loki start'.${NC}" >&2
2001
2046
  exit 1
2002
2047
  fi
2003
2048
  # PID is stale (file present but process gone). Log + remove + continue.
@@ -2938,6 +2983,323 @@ cmd_resume() {
2938
2983
  fi
2939
2984
  }
2940
2985
 
2986
+ # Resolve the terminal-run status from the same already-captured artifacts that
2987
+ # cmd_why reads (no new state, no spend). Echoes a single token: the resolved
2988
+ # status string (e.g. council_approved, paused, max_iterations_reached), or the
2989
+ # literal "none" when no run exists here yet. Used by cmd_next so its status->
2990
+ # action mapping reads exactly the same state cmd_why does (no drift).
2991
+ # Read-only; never fabricates.
2992
+ _loki_resolve_run_status() {
2993
+ local loki_dir="${LOKI_DIR:-.loki}"
2994
+ local state_file="$loki_dir/autonomy-state.json"
2995
+ if [ -n "${LOKI_SESSION_ID:-}" ] && [ -f "$loki_dir/sessions/${LOKI_SESSION_ID}/autonomy-state.json" ]; then
2996
+ state_file="$loki_dir/sessions/${LOKI_SESSION_ID}/autonomy-state.json"
2997
+ fi
2998
+ local completion_file="$loki_dir/state/completion.json"
2999
+
3000
+ if [ ! -f "$state_file" ] && [ ! -f "$completion_file" ]; then
3001
+ printf '%s\n' "none"
3002
+ return 0
3003
+ fi
3004
+
3005
+ _LOKI_NA_STATE="$state_file" _LOKI_NA_COMPLETION="$completion_file" python3 - <<'NASTATUS'
3006
+ import json, os
3007
+ def load(p):
3008
+ try:
3009
+ with open(p) as f: return json.load(f)
3010
+ except Exception: return {}
3011
+ state = load(os.environ.get("_LOKI_NA_STATE", ""))
3012
+ comp = load(os.environ.get("_LOKI_NA_COMPLETION", ""))
3013
+ # Mirror cmd_why's COMP_OUTCOME_ALIASES so a completion-only state (a rotated or
3014
+ # crashed run with no live status, only completion.json's outcome) resolves to
3015
+ # the canonical status loki next's action map keys on. Without this, an outcome
3016
+ # of "complete"/"max_iterations" drifted to the default "loki status" instead of
3017
+ # "loki ship"/"loki resume".
3018
+ COMP_OUTCOME_ALIASES = {
3019
+ "complete": "council_approved",
3020
+ "max_iterations": "max_iterations_reached",
3021
+ "intervention": "paused",
3022
+ }
3023
+ status = state.get("status")
3024
+ if not status:
3025
+ raw_outcome = comp.get("outcome") or ""
3026
+ status = COMP_OUTCOME_ALIASES.get(raw_outcome, raw_outcome) or "unknown"
3027
+ print(status)
3028
+ NASTATUS
3029
+ }
3030
+
3031
+ # Map a resolved status to the single concrete next-action SENTENCE (the same
3032
+ # wording cmd_why prints under "What to do"). Echoes the action string. Kept in
3033
+ # lockstep with cmd_why's GUIDE map so why and next agree.
3034
+ _loki_next_action() {
3035
+ local status="${1:-unknown}"
3036
+ case "$status" in
3037
+ council_approved)
3038
+ echo "Review the diff and open a PR (loki ship, or git push + gh pr create)." ;;
3039
+ council_force_approved)
3040
+ echo "Review the diff carefully before merging -- convergence was not unanimous." ;;
3041
+ completion_promise_fulfilled)
3042
+ echo "Verify the promised outcome, then review and PR (loki ship)." ;;
3043
+ max_iterations_reached)
3044
+ echo "Raise LOKI_MAX_ITERATIONS (or narrow the spec), then resume (loki resume)." ;;
3045
+ max_retries_exceeded)
3046
+ echo "Read .loki/logs for the recurring error, fix the root cause, then re-run (loki why)." ;;
3047
+ failed)
3048
+ echo "Read the failure (loki why), fix it, then re-run." ;;
3049
+ policy_blocked)
3050
+ echo "Review the blocking finding (loki why); address it or use the documented override." ;;
3051
+ budget_exceeded)
3052
+ echo "Raise LOKI_BUDGET_LIMIT or accept the partial result, then resume (loki resume)." ;;
3053
+ paused)
3054
+ echo "Resume with: loki resume." ;;
3055
+ interrupted)
3056
+ echo "Resume with: loki resume." ;;
3057
+ stopped)
3058
+ echo "Start a new build (loki start), or resume if you meant to continue." ;;
3059
+ force_stopped)
3060
+ echo "Start a new build, or investigate why a force-stop was needed (loki why)." ;;
3061
+ running)
3062
+ echo "A build looks active (or crashed mid-run); check loki status." ;;
3063
+ none)
3064
+ echo "Run a build first: loki start <spec>." ;;
3065
+ *)
3066
+ echo "Check loki status and loki why for detail." ;;
3067
+ esac
3068
+ }
3069
+
3070
+ # loki next -- one command that runs the right next step for you.
3071
+ # Reads the SAME already-captured state as cmd_why (no new state, no spend),
3072
+ # resolves the terminal status, and proposes the single mapped next step, then
3073
+ # runs it on confirmation. Default on a TTY is confirm-then-run; --yes runs it
3074
+ # without asking; --dry-run and any non-TTY context only PRINT (honest: never
3075
+ # auto-acts in CI / when piped). Strictly additive: reuses cmd_resume, cmd_ship,
3076
+ # cmd_why, and cmd_status; never touches the build path.
3077
+ cmd_next() {
3078
+ local assume_yes=false
3079
+ local dry_run=false
3080
+ case "${1:-}" in
3081
+ --help|-h|help)
3082
+ echo -e "${BOLD}Loki Mode -- run the right next step for you${NC}"
3083
+ echo ""
3084
+ echo "Usage: loki next [--yes] [--dry-run]"
3085
+ echo ""
3086
+ echo "Reads the last build's recorded outcome (the same state 'loki why'"
3087
+ echo "reads -- no new run, no cost) and carries you forward by running the"
3088
+ echo "single mapped next step: resume a paused/interrupted run, finish a"
3089
+ echo "council-approved build (loki ship), or route a failure to 'loki why'."
3090
+ echo ""
3091
+ echo "Default on a terminal: it shows the proposed step and asks before"
3092
+ echo "running it. Non-interactive (piped/CI) it only prints the step;"
3093
+ echo "it never acts on its own there."
3094
+ echo ""
3095
+ echo "Options:"
3096
+ echo " --yes Run the proposed step without asking"
3097
+ echo " --dry-run Print the proposed step only; do not run it"
3098
+ echo " --help, -h Show this help and exit"
3099
+ return 0
3100
+ ;;
3101
+ --yes|-y) assume_yes=true ;;
3102
+ --dry-run) dry_run=true ;;
3103
+ "") : ;;
3104
+ *) echo -e "${RED}Unknown flag: $1${NC}" >&2; echo "Usage: loki next [--yes] [--dry-run]" >&2; return 1 ;;
3105
+ esac
3106
+ # Allow a second flag (e.g. --dry-run --yes order-agnostic).
3107
+ case "${2:-}" in
3108
+ --yes|-y) assume_yes=true ;;
3109
+ --dry-run) dry_run=true ;;
3110
+ esac
3111
+
3112
+ local status
3113
+ status="$(_loki_resolve_run_status)"
3114
+ if [ "$status" = "none" ]; then
3115
+ echo "loki next: no run found here yet." >&2
3116
+ echo "Run a build first: loki start <spec>" >&2
3117
+ return 1
3118
+ fi
3119
+
3120
+ local action_text
3121
+ action_text="$(_loki_next_action "$status")"
3122
+
3123
+ # Map the status to the concrete callable that carries the user forward.
3124
+ # action_cmd is a human label; the case below dispatches to the real function.
3125
+ local action_cmd=""
3126
+ case "$status" in
3127
+ paused|interrupted) action_cmd="loki resume" ;;
3128
+ council_approved|council_force_approved|completion_promise_fulfilled)
3129
+ action_cmd="loki ship" ;;
3130
+ max_iterations_reached|budget_exceeded) action_cmd="loki resume" ;;
3131
+ failed|max_retries_exceeded|policy_blocked|force_stopped)
3132
+ action_cmd="loki why" ;;
3133
+ *) action_cmd="loki status" ;;
3134
+ esac
3135
+
3136
+ echo -e "${BOLD}Loki: next${NC}"
3137
+ echo " Last outcome : $status"
3138
+ echo " Next step : $action_text"
3139
+ echo " Will run : $action_cmd"
3140
+ echo ""
3141
+
3142
+ # Honesty gate: only ACT on an interactive terminal with consent, or with
3143
+ # --yes. --dry-run and non-TTY always print-only.
3144
+ if [ "$dry_run" = true ]; then
3145
+ echo "(dry-run: not running. Run '$action_cmd' yourself, or 'loki next --yes'.)"
3146
+ return 0
3147
+ fi
3148
+ if [ "$assume_yes" != true ]; then
3149
+ if [ ! -t 0 ] || [ ! -t 1 ]; then
3150
+ echo "(non-interactive: not running. Run '$action_cmd', or 'loki next --yes'.)"
3151
+ return 0
3152
+ fi
3153
+ local reply=""
3154
+ printf "Run '%s' now? [y/N] " "$action_cmd"
3155
+ read -r reply || reply=""
3156
+ case "$reply" in
3157
+ y|Y|yes|YES) : ;;
3158
+ *) echo "Skipped. Run '$action_cmd' when ready."; return 0 ;;
3159
+ esac
3160
+ fi
3161
+
3162
+ # Dispatch to the real command. For budget/iteration caps, print the env-var
3163
+ # guidance first (the resume alone will not lift the cap), then resume.
3164
+ case "$status" in
3165
+ max_iterations_reached)
3166
+ echo "Note: raise the cap first if you want more iterations, e.g.:"
3167
+ echo " LOKI_MAX_ITERATIONS=<n> loki resume"
3168
+ echo ""
3169
+ ;;
3170
+ budget_exceeded)
3171
+ echo "Note: raise the budget first if you want to continue, e.g.:"
3172
+ echo " LOKI_BUDGET_LIMIT=<usd> loki resume"
3173
+ echo ""
3174
+ ;;
3175
+ esac
3176
+ case "$action_cmd" in
3177
+ "loki resume") cmd_resume ;;
3178
+ "loki ship") cmd_ship ;;
3179
+ "loki why") cmd_why ;;
3180
+ *) cmd_status ;;
3181
+ esac
3182
+ return $?
3183
+ }
3184
+
3185
+ # loki ship -- the build is done, now get it out (review + PR advice in one flow).
3186
+ # Composes existing surfaces only: optionally opens the running app (--preview),
3187
+ # runs the SAME diff quality gates as cmd_review, and on a clean-enough result
3188
+ # prints the exact branch-aware PR command via the shared print_pr_advice (the
3189
+ # same advisory cmd_deploy uses). PRINT-ONLY for shipping: it NEVER pushes and
3190
+ # NEVER deploys; it prints (and clipboards, as the advisory already does) the
3191
+ # git push + PR command for YOU to run. If review finds HIGH/CRITICAL it stops
3192
+ # and points at the findings instead of advising a PR.
3193
+ cmd_ship() {
3194
+ local do_preview=false
3195
+ local review_args=()
3196
+ while [ $# -gt 0 ]; do
3197
+ case "${1:-}" in
3198
+ --help|-h|help)
3199
+ echo -e "${BOLD}Loki Mode -- finish the build: review, then PR advice (print-only)${NC}"
3200
+ echo ""
3201
+ echo "Usage: loki ship [--preview] [--yes] [review options]"
3202
+ echo ""
3203
+ echo "Runs the natural finish line in one command:"
3204
+ echo " 1. (with --preview) opens the app Loki built and started locally"
3205
+ echo " 2. runs 'loki review' on your uncommitted/branch diff (the same"
3206
+ echo " quality gates)"
3207
+ echo " 3. on a clean-enough result, PRINTS the exact branch-aware"
3208
+ echo " git push + pull-request command for YOU to run"
3209
+ echo ""
3210
+ echo "It is advisory only for shipping: it NEVER runs 'git push', NEVER"
3211
+ echo "creates a PR, and NEVER deploys. You run the printed command."
3212
+ echo "If review finds HIGH or CRITICAL findings it stops and points you"
3213
+ echo "at the findings instead of advising a PR (exit code 1)."
3214
+ echo ""
3215
+ echo "Options:"
3216
+ echo " --preview Open the running local app first (loki preview)"
3217
+ echo " --yes, -y Pass through to review (skip its confirmation prompt)"
3218
+ echo " --help, -h Show this help and exit"
3219
+ echo ""
3220
+ echo "Any other options are passed through to 'loki review'"
3221
+ echo "(e.g. --staged, --since <commit>, --severity high)."
3222
+ return 0
3223
+ ;;
3224
+ --preview) do_preview=true ;;
3225
+ *) review_args+=("$1") ;;
3226
+ esac
3227
+ shift
3228
+ done
3229
+
3230
+ # 1. Optional preview of the running local app. Only when an app-runner state
3231
+ # file exists, so we never imply an app is running when it is not.
3232
+ if [ "$do_preview" = true ]; then
3233
+ local loki_dir="${LOKI_DIR:-.loki}"
3234
+ if [ -f "$loki_dir/state/app-runner.json" ] || [ -f "$loki_dir/app-runner.json" ]; then
3235
+ echo -e "${BOLD}Opening the running app...${NC}"
3236
+ cmd_preview || true
3237
+ echo ""
3238
+ else
3239
+ echo "(no running app detected; skipping preview. Start one with loki start.)"
3240
+ echo ""
3241
+ fi
3242
+ fi
3243
+
3244
+ # 2. Run the SAME diff quality gates as cmd_review. Capture its documented
3245
+ # exit code (0 clean, 1 HIGH, 2 CRITICAL). set -e safe: capture via || rc=$?.
3246
+ echo -e "${BOLD}Running quality gates on the diff (loki review)...${NC}"
3247
+ echo ""
3248
+ local rc=0
3249
+ cmd_review "${review_args[@]}" || rc=$?
3250
+
3251
+ if [ "$rc" -ge 1 ]; then
3252
+ echo ""
3253
+ if [ "$rc" -ge 2 ]; then
3254
+ echo -e "${RED}Review found CRITICAL findings. Not advising a PR yet.${NC}" >&2
3255
+ else
3256
+ echo -e "${YELLOW}Review found HIGH findings. Not advising a PR yet.${NC}" >&2
3257
+ fi
3258
+ echo "Fix the findings above (or run 'loki review' for the full report), then 'loki ship' again." >&2
3259
+ return 1
3260
+ fi
3261
+
3262
+ # 3. Clean enough: print the branch-aware PR command (the SAME advisory
3263
+ # cmd_deploy uses). Derive base/head exactly as cmd_deploy does.
3264
+ echo ""
3265
+ echo -e "${GREEN}Review is clean (no HIGH or CRITICAL findings).${NC}"
3266
+ echo ""
3267
+ local dir="."
3268
+ local head="" base=""
3269
+ head="$(git -C "$dir" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")"
3270
+ [ -n "$head" ] || head="HEAD"
3271
+ case "$head" in
3272
+ loki/*)
3273
+ if [ -s "$dir/.loki/state/base-branch.txt" ]; then
3274
+ base="$(head -n1 "$dir/.loki/state/base-branch.txt" 2>/dev/null || echo "")"
3275
+ fi
3276
+ ;;
3277
+ esac
3278
+ if [ -z "$base" ]; then
3279
+ local origin_head=""
3280
+ origin_head="$(git -C "$dir" symbolic-ref refs/remotes/origin/HEAD 2>/dev/null || echo "")"
3281
+ if [ -n "$origin_head" ]; then
3282
+ base="${origin_head##*/}"
3283
+ fi
3284
+ fi
3285
+
3286
+ if declare -F print_pr_advice >/dev/null 2>&1; then
3287
+ if [ -n "$base" ]; then
3288
+ print_pr_advice "$base" "$head" "$dir"
3289
+ else
3290
+ echo "Could not determine the PR base branch automatically; set it manually:"
3291
+ print_pr_advice "manually-set-base" "$head" "$dir"
3292
+ fi
3293
+ else
3294
+ echo "To open a pull request:"
3295
+ echo " git push -u origin ${head}"
3296
+ echo " Open a pull request for ${head} (set the base branch manually)."
3297
+ fi
3298
+ echo ""
3299
+ echo -e "${DIM}Loki does not push or deploy for you; run the command above when ready.${NC}"
3300
+ return 0
3301
+ }
3302
+
2941
3303
  # loki why -- actionable failure/outcome diagnosis (B5).
2942
3304
  # Reads the already-captured run artifacts (no new state): the terminal run state
2943
3305
  # (.loki/<autonomy-state>.json: status, lastExitCode, iterationCount), the durable
@@ -2951,7 +3313,7 @@ cmd_why() {
2951
3313
  --json) as_json=1 ;;
2952
3314
  --help|-h) echo "Usage: loki why [--json] -- explain the last build's outcome and what to do next"; return 0 ;;
2953
3315
  "" ) : ;;
2954
- *) echo -e "${RED}Unknown flag: $1${NC}"; echo "Usage: loki why [--json]"; return 1 ;;
3316
+ *) echo -e "${RED}Unknown flag: $1${NC}" >&2; echo "Usage: loki why [--json]" >&2; return 1 ;;
2955
3317
  esac
2956
3318
 
2957
3319
  local loki_dir="${LOKI_DIR:-.loki}"
@@ -2985,7 +3347,10 @@ WHYJSON
2985
3347
  # Human-readable report. The diagnosis maps the terminal status to a plain
2986
3348
  # explanation + a concrete next action; everything is sourced from the files,
2987
3349
  # nothing is invented.
3350
+ local _why_head_sha
3351
+ _why_head_sha="$(git rev-parse HEAD 2>/dev/null || echo "")"
2988
3352
  _LOKI_WHY_STATE="$state_file" _LOKI_WHY_COMPLETION="$completion_file" \
3353
+ _LOKI_WHY_HEAD_SHA="$_why_head_sha" \
2989
3354
  _LOKI_WHY_HANDOFFS="$loki_dir/memory/handoffs" python3 - <<'WHYTXT'
2990
3355
  import json, os, glob
2991
3356
  def load(p):
@@ -2994,7 +3359,22 @@ def load(p):
2994
3359
  except Exception: return {}
2995
3360
  state = load(os.environ.get("_LOKI_WHY_STATE", ""))
2996
3361
  comp = load(os.environ.get("_LOKI_WHY_COMPLETION", ""))
2997
- status = state.get("status") or comp.get("outcome") or "unknown"
3362
+
3363
+ # completion.json uses a terminal-outcome vocabulary (complete / max_iterations /
3364
+ # failed / stopped / force_stopped / intervention) that differs from the GUIDE
3365
+ # keys below. Map it onto the GUIDE keys so the diagnosis is never "no mapping".
3366
+ # The live state-file status (running / exited / paused / ...) is preferred when
3367
+ # present; we only fall back to the (normalized) completion outcome.
3368
+ COMP_OUTCOME_ALIASES = {
3369
+ "complete": "council_approved",
3370
+ "max_iterations": "max_iterations_reached",
3371
+ "intervention": "paused",
3372
+ # failed / stopped / force_stopped already match GUIDE keys verbatim.
3373
+ }
3374
+ status = state.get("status")
3375
+ if not status:
3376
+ raw_outcome = comp.get("outcome") or ""
3377
+ status = COMP_OUTCOME_ALIASES.get(raw_outcome, raw_outcome) or "unknown"
2998
3378
  exit_code = state.get("lastExitCode")
2999
3379
  iters = state.get("iterationCount")
3000
3380
 
@@ -3026,10 +3406,25 @@ GUIDE = {
3026
3406
  "Start a new build, or investigate why a force-stop was needed."),
3027
3407
  "running": ("The recorded state says a build is still running (or crashed mid-run).",
3028
3408
  "If no build is active it likely crashed; in durable mode (LOKI_DURABLE_STATE=1) a restart resumes, else loki start re-runs."),
3409
+ "exited": ("The build process exited mid-iteration (likely a crash, kill, or empty provider output).",
3410
+ "Read .loki/logs/ for the last error; if nothing is running it crashed -- re-run with loki start, or loki resume in durable mode."),
3029
3411
  }
3030
3412
  meaning, action = GUIDE.get(status, ("No diagnosis mapping for this status; see the raw fields below.",
3031
3413
  "Check loki status and .loki/logs/ for detail."))
3032
3414
 
3415
+ # The completion.json branch/changes/PR belong to the LAST COMPLETED run. When
3416
+ # the live state shows a build that is still running or crashed mid-iteration
3417
+ # (running/exited), or when the completion record's head_sha no longer matches
3418
+ # the current git HEAD, those fields describe a PREVIOUS run -- not this one.
3419
+ # Label them honestly so a crashed run is never reported with a stale PR/branch.
3420
+ live_statuses = {"running", "exited"}
3421
+ head_sha = os.environ.get("_LOKI_WHY_HEAD_SHA", "")
3422
+ comp_head = comp.get("head_sha", "")
3423
+ comp_is_stale = bool(state.get("status") in live_statuses) or bool(
3424
+ head_sha and comp_head and comp_head != head_sha
3425
+ )
3426
+ comp_label = " (from previous completed run)" if comp_is_stale else ""
3427
+
3033
3428
  print("Loki: why")
3034
3429
  print("=" * 60)
3035
3430
  print(f" Outcome : {status}")
@@ -3038,11 +3433,11 @@ if exit_code is not None:
3038
3433
  if iters is not None:
3039
3434
  print(f" Iterations : {iters}")
3040
3435
  if comp.get("branch"):
3041
- print(f" Branch : {comp['branch']}")
3436
+ print(f" Branch : {comp['branch']}{comp_label}")
3042
3437
  if comp.get("files_changed") is not None:
3043
- print(f" Changes : {comp.get('files_changed',0)} files (+{comp.get('insertions',0)}/-{comp.get('deletions',0)})")
3438
+ print(f" Changes : {comp.get('files_changed',0)} files (+{comp.get('insertions',0)}/-{comp.get('deletions',0)}){comp_label}")
3044
3439
  if comp.get("pr_url"):
3045
- print(f" PR : {comp['pr_url']}")
3440
+ print(f" PR : {comp['pr_url']}{comp_label}")
3046
3441
  print()
3047
3442
  print(f" What happened: {meaning}")
3048
3443
  print(f" What to do : {action}")
@@ -3235,6 +3630,43 @@ cmd_status() {
3235
3630
  fi
3236
3631
  fi
3237
3632
 
3633
+ # v7.82: surface the single most valuable, most shareable fact -- where the
3634
+ # built app is running -- when the app runner reports a live app. Display-only
3635
+ # read of .loki/app-runner/state.json (same shape build_completion_summary
3636
+ # reads); silent when no app is running. The --json path (cmd_status_json) is
3637
+ # untouched, so machine output stays byte-identical.
3638
+ if [ -f "$LOKI_DIR/app-runner/state.json" ]; then
3639
+ local app_url
3640
+ app_url=$(_LOKI_APP_STATE="$LOKI_DIR/app-runner/state.json" python3 -c "
3641
+ import json, os
3642
+ try:
3643
+ d = json.load(open(os.environ['_LOKI_APP_STATE']))
3644
+ print(d.get('url', '') if d.get('status') == 'running' else '')
3645
+ except Exception:
3646
+ print('')" 2>/dev/null)
3647
+ if [ -n "$app_url" ]; then
3648
+ echo ""
3649
+ echo -e "${GREEN}App running:${NC} ${BOLD}${app_url}${NC} ${DIM}- open it to try it${NC}"
3650
+ fi
3651
+ fi
3652
+
3653
+ # v7.82: completion-aware one-liner. When a finished run left a completion
3654
+ # record, show its outcome and point at \`loki why\` for the full story. Reads
3655
+ # the persisted completion.json (no recompute); silent when absent.
3656
+ if [ -f "$LOKI_DIR/state/completion.json" ]; then
3657
+ local last_outcome
3658
+ last_outcome=$(_LOKI_CJ="$LOKI_DIR/state/completion.json" python3 -c "
3659
+ import json, os
3660
+ try:
3661
+ d = json.load(open(os.environ['_LOKI_CJ']))
3662
+ print(d.get('outcome', '') or '')
3663
+ except Exception:
3664
+ print('')" 2>/dev/null)
3665
+ if [ -n "$last_outcome" ]; then
3666
+ echo -e "${DIM}Last run: ${last_outcome} - run 'loki why' for details${NC}"
3667
+ fi
3668
+ fi
3669
+
3238
3670
  echo ""
3239
3671
  echo -e "${DIM} Tip: loki analyze context show - detailed token breakdown${NC}"
3240
3672
  echo -e "${DIM} Tip: loki analyze code overview - codebase intelligence${NC}"
@@ -4817,7 +5249,7 @@ cmd_welcome_terminal() {
4817
5249
  echo -e " ${CYAN}Cross-project memory${NC} Lessons compound, so agents stop repeating mistakes."
4818
5250
  echo -e " ${CYAN}Provider-agnostic${NC} Claude, Codex, Cline, or Aider. Your keys, your infra."
4819
5251
  echo ""
4820
- echo -e " Quick start: ${BOLD}loki start ./prd.md${NC}"
5252
+ echo -e " Quick start: ${BOLD}loki quickstart${NC} (from your idea) or ${BOLD}loki start ./prd.md${NC} (from a spec)"
4821
5253
  echo -e " Docs: ${BOLD}https://www.autonomi.dev/docs${NC}"
4822
5254
  echo ""
4823
5255
  if _loki_welcome_analytics_on; then
@@ -5767,12 +6199,20 @@ cmd_preview() {
5767
6199
  public=true
5768
6200
  ;;
5769
6201
  --provider)
5770
- provider="${2:-}"
5771
- # Guard the value-consuming shift: if --provider is the LAST arg
5772
- # (no value), an unguarded shift here plus the loop's trailing
5773
- # shift would underflow and abort under set -e. Only consume a
5774
- # value when one is actually present.
5775
- [ $# -ge 2 ] && shift
6202
+ # Guard against a missing OR flag-shaped value: an unguarded
6203
+ # shift when --provider is the last arg underflows under set -e,
6204
+ # and a bare "--provider --public" would otherwise swallow the
6205
+ # next flag as the provider name.
6206
+ case "${2:-}" in
6207
+ -*|"")
6208
+ echo "loki preview --provider requires a value (cloudflared|ngrok)" >&2
6209
+ return 1
6210
+ ;;
6211
+ *)
6212
+ provider="$2"
6213
+ shift
6214
+ ;;
6215
+ esac
5776
6216
  ;;
5777
6217
  --yes)
5778
6218
  assume_yes=true
@@ -6122,11 +6562,14 @@ cmd_deploy() {
6122
6562
  return 0
6123
6563
  ;;
6124
6564
  --dir)
6125
- dir="${2:-.}"
6126
- # Guard the value-consuming shift: if --dir is the LAST arg (no
6127
- # value), an unguarded shift plus the loop's trailing shift would
6128
- # underflow and abort under set -e. Consume a value only if present.
6129
- [ $# -ge 2 ] && shift
6565
+ # Guard against a missing OR flag-shaped value: an unguarded
6566
+ # shift when --dir is the last arg underflows under set -e, and a
6567
+ # bare "--dir --no-clip" would otherwise treat the next flag as
6568
+ # the scan directory.
6569
+ case "${2:-}" in
6570
+ -*|"") echo "loki deploy --dir requires a directory path" >&2; return 1 ;;
6571
+ *) dir="$2"; shift ;;
6572
+ esac
6130
6573
  ;;
6131
6574
  --no-clip)
6132
6575
  do_clip=false
@@ -8013,7 +8456,7 @@ cmd_assets() {
8013
8456
  ;;
8014
8457
  *)
8015
8458
  echo -e "${RED}Unknown subcommand: $subcommand${NC}" >&2
8016
- echo "Run 'loki assets --help' for usage."
8459
+ echo "Run 'loki assets --help' for usage." >&2
8017
8460
  return 1
8018
8461
  ;;
8019
8462
  esac
@@ -9464,11 +9907,17 @@ cmd_doctor() {
9464
9907
  return 1
9465
9908
  elif [ "$warn_count" -gt 0 ]; then
9466
9909
  echo -e "${YELLOW}All required checks passed with some warnings.${NC}"
9467
- return 0
9468
9910
  else
9469
9911
  echo -e "${GREEN}All checks passed. System is ready for Loki Mode.${NC}"
9470
- return 0
9471
9912
  fi
9913
+ # Setup verified (no required check failed): hand the user straight to a
9914
+ # first build with a copy-paste command, so they never dead-end here. Gated
9915
+ # on fail_count==0 above, so a failing setup is never told to build. The
9916
+ # --json path returns long before this code, so machine output is untouched.
9917
+ echo ""
9918
+ echo "Next: loki quickstart (guided first build from your idea, no PRD needed)"
9919
+ echo " or loki demo (builds a sample todo app end to end) or loki start ./prd.md"
9920
+ return 0
9472
9921
  }
9473
9922
 
9474
9923
  # JSON output for loki doctor --json
@@ -11081,6 +11530,10 @@ cmd_quick() {
11081
11530
  # BUG-PU-005: Use unique filename to prevent race conditions when
11082
11531
  # multiple simultaneous `loki quick` commands run in the same project
11083
11532
  mkdir -p "$LOKI_DIR"
11533
+ # Reap stale per-PID temp PRDs from prior runs (the process exec-replaces
11534
+ # into run.sh, so these are never cleaned on exit and accumulate).
11535
+ find "$LOKI_DIR" -maxdepth 1 -name 'quick-prd-*.md' -mtime +1 -delete 2>/dev/null || true
11536
+ find "$LOKI_DIR" -maxdepth 1 -name 'brief-prd-*.md' -mtime +1 -delete 2>/dev/null || true
11084
11537
  local quick_prd="$LOKI_DIR/quick-prd-$$.md"
11085
11538
  cat > "$quick_prd" << QPRDEOF
11086
11539
  # Quick Task
@@ -15482,12 +15935,22 @@ cmd_plan() {
15482
15935
  done
15483
15936
 
15484
15937
  if [ -z "$prd_file" ]; then
15938
+ # Under --json, a machine consumer expects JSON on every path, including
15939
+ # errors -- never ANSI-colored prose. Emit a structured error to stdout.
15940
+ if [ "$show_json" = true ]; then
15941
+ printf '{"error":"missing PRD file argument"}\n'
15942
+ return 2
15943
+ fi
15485
15944
  echo -e "${RED}Usage: loki plan <PRD file>${NC}"
15486
15945
  echo "Run 'loki plan --help' for usage."
15487
15946
  return 2
15488
15947
  fi
15489
15948
 
15490
15949
  if [ ! -f "$prd_file" ]; then
15950
+ if [ "$show_json" = true ]; then
15951
+ printf '{"error":"PRD file not found","prd_file":"%s"}\n' "$prd_file"
15952
+ return 1
15953
+ fi
15491
15954
  echo -e "${RED}PRD file not found: $prd_file${NC}"
15492
15955
  return 1
15493
15956
  fi
@@ -15528,6 +15991,60 @@ maybe_show_auto_plan() {
15528
15991
  show_prd_plan "$abs_prd" "false" "false"
15529
15992
  }
15530
15993
 
15994
+ # Suggest the closest known command for a typo (the did-you-mean hint).
15995
+ # Prints "Did you mean 'X'?" to stderr when an unknown command is within a
15996
+ # small Levenshtein distance of a real command. Silent (returns 1) when
15997
+ # nothing is close enough, so a genuinely-unknown command is not given a
15998
+ # misleading suggestion. Pure bash + awk (no extra deps); the candidate list
15999
+ # is the canonical set of top-level commands accepted by the dispatcher.
16000
+ _suggest_command() {
16001
+ local typo="$1"
16002
+ [ -n "$typo" ] || return 1
16003
+ # Canonical top-level command names (keep in sync with the dispatch case
16004
+ # below). Deprecated aliases are intentionally included so a typo of an
16005
+ # alias still resolves to a helpful pointer.
16006
+ local known="start run quick plan grill spec verify proof trust review \
16007
+ ultracode council demo dogfood heal modernize import issue github init template \
16008
+ status stop pause resume monitor watch watchdog dashboard web open preview share \
16009
+ memory context ctx checkpoint state report kpis cost metrics stats logs otel \
16010
+ syslog telemetry crash config provider doctor setup-skill onboard quickstart \
16011
+ welcome update self-update rollback failover cluster enterprise remote deploy docker \
16012
+ sandbox api mcp magic compound assets export wiki docs explain why audit compliance \
16013
+ secrets analyze optimize bench ci reset cleanup notify trigger voice sentrux \
16014
+ worktree wt projects cp rc trust-metrics serve agent code self_update test help version"
16015
+ local best
16016
+ best=$(printf '%s\n' $known | awk -v t="$typo" '
16017
+ function min3(a, b, c) { if (a < b) { if (a < c) return a; return c } if (b < c) return b; return c }
16018
+ function lev(s1, s2, n, m, i, j, prev, cur, cost, tmp) {
16019
+ n = length(s1); m = length(s2)
16020
+ if (n == 0) return m
16021
+ if (m == 0) return n
16022
+ for (j = 0; j <= m; j++) prev[j] = j
16023
+ for (i = 1; i <= n; i++) {
16024
+ cur[0] = i
16025
+ for (j = 1; j <= m; j++) {
16026
+ cost = (substr(s1, i, 1) == substr(s2, j, 1)) ? 0 : 1
16027
+ cur[j] = min3(prev[j] + 1, cur[j-1] + 1, prev[j-1] + cost)
16028
+ }
16029
+ for (j = 0; j <= m; j++) prev[j] = cur[j]
16030
+ }
16031
+ return prev[m]
16032
+ }
16033
+ { d = lev(t, $1); if (d < bestd || NR == 1) { bestd = d; bestc = $1 } }
16034
+ END {
16035
+ # Only suggest when the edit distance is small relative to the typo
16036
+ # length: <=2 for short commands, <=3 for longer ones. This avoids
16037
+ # absurd suggestions (e.g. "xyz" -> "web").
16038
+ thr = (length(t) <= 4) ? 2 : 3
16039
+ if (bestd <= thr) print bestc
16040
+ }')
16041
+ if [ -n "$best" ]; then
16042
+ printf '%s\n' "$best"
16043
+ return 0
16044
+ fi
16045
+ return 1
16046
+ }
16047
+
15531
16048
  # Main command dispatcher
15532
16049
  main() {
15533
16050
  # v7.5.18: early guard -- LOKI_PROVIDER=gemini is no longer supported.
@@ -15622,6 +16139,9 @@ main() {
15622
16139
  why)
15623
16140
  cmd_why "$@"
15624
16141
  ;;
16142
+ next)
16143
+ cmd_next "$@"
16144
+ ;;
15625
16145
  stats)
15626
16146
  # CLI consolidation (Phase A): 'stats' is a deprecated alias of
15627
16147
  # 'report session'. On the Bun route this arm is never reached
@@ -15645,6 +16165,9 @@ main() {
15645
16165
  deploy)
15646
16166
  cmd_deploy "$@"
15647
16167
  ;;
16168
+ ship)
16169
+ cmd_ship "$@"
16170
+ ;;
15648
16171
  open)
15649
16172
  # CLI consolidation (Phase A): 'open' is a deprecated alias of 'preview'.
15650
16173
  _deprecated_alias open preview "$@"
@@ -15952,8 +16475,12 @@ main() {
15952
16475
  fi
15953
16476
  ;;
15954
16477
  *)
15955
- echo -e "${RED}Unknown command: $command${NC}"
15956
- echo "Run 'loki help' for usage."
16478
+ echo -e "${RED}Unknown command: $command${NC}" >&2
16479
+ local _suggestion
16480
+ if _suggestion=$(_suggest_command "$command"); then
16481
+ echo "Did you mean 'loki ${_suggestion}'?" >&2
16482
+ fi
16483
+ echo "Run 'loki help' for usage." >&2
15957
16484
  exit 1
15958
16485
  ;;
15959
16486
  esac
@@ -18329,9 +18856,15 @@ try:
18329
18856
  print('No relevant memories found')
18330
18857
  else:
18331
18858
  for i, r in enumerate(results, 1):
18332
- source = r.get('source', 'unknown')
18333
- summary = r.get('summary', r.get('pattern', 'No summary'))[:80]
18334
- score = r.get('score', 0)
18859
+ # Retrieval results expose _source/_score/_weighted_score and the
18860
+ # raw stored fields (pattern/description/goal/name), not
18861
+ # source/summary/score. Read the fields that actually exist so the
18862
+ # output is not a uniform [unknown] ... (score 0.00).
18863
+ source = r.get('_source', 'unknown')
18864
+ score = r.get('_score', r.get('_weighted_score', 0))
18865
+ summary = (r.get('pattern') or r.get('description') or r.get('goal')
18866
+ or (r.get('context') or {}).get('goal') or r.get('name')
18867
+ or 'No summary')[:80]
18335
18868
  print(f'{i}. [{source}] {summary}... (score: {score:.2f})')
18336
18869
  except ImportError as e:
18337
18870
  print(f'Error: Required module not found - {e}')
@@ -21892,7 +22425,7 @@ cmd_trust_metrics() {
21892
22425
  echo "Run 'loki trust-metrics' inside each project directory."
21893
22426
  exit 2
21894
22427
  ;;
21895
- *) echo -e "${RED}Unknown option: $1${NC}"; echo "Run 'loki trust-metrics --help' for usage."; exit 1 ;;
22428
+ *) echo -e "${RED}Unknown option: $1${NC}" >&2; echo "Run 'loki trust-metrics --help' for usage." >&2; exit 1 ;;
21896
22429
  esac
21897
22430
  done
21898
22431
 
@@ -21947,7 +22480,7 @@ cmd_cost() {
21947
22480
  --json) show_json=true; shift ;;
21948
22481
  --last) last_n="${2:-0}"; shift 2 ;;
21949
22482
  --last=*) last_n="${1#*=}"; shift ;;
21950
- *) echo -e "${RED}Unknown option: $1${NC}"; echo "Run 'loki cost --help' for usage."; exit 1 ;;
22483
+ *) echo -e "${RED}Unknown option: $1${NC}" >&2; echo "Run 'loki cost --help' for usage." >&2; exit 1 ;;
21951
22484
  esac
21952
22485
  done
21953
22486
 
@@ -22262,7 +22795,7 @@ cmd_metrics() {
22262
22795
  echo "$response"
22263
22796
  exit 0
22264
22797
  ;;
22265
- *) echo -e "${RED}Unknown option: $1${NC}"; echo "Run 'loki metrics --help' for usage."; exit 1 ;;
22798
+ *) echo -e "${RED}Unknown option: $1${NC}" >&2; echo "Run 'loki metrics --help' for usage." >&2; exit 1 ;;
22266
22799
  esac
22267
22800
  done
22268
22801
 
@@ -23336,12 +23869,15 @@ _code_diff() {
23336
23869
  # Output shell completion scripts
23337
23870
  cmd_completions() {
23338
23871
  local shell="${1:-bash}"
23872
+ # Consume the subcommand so the install arm can read an optional shell
23873
+ # override as its own positional ($1). bash/zsh raw-output arms ignore $@.
23874
+ shift 2>/dev/null || true
23339
23875
  local skill_dir
23340
-
23876
+
23341
23877
  # Find the skill directory (where autonomy/loki is located)
23342
23878
  skill_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
23343
23879
  local completions_dir="$skill_dir/completions"
23344
-
23880
+
23345
23881
  case "$shell" in
23346
23882
  bash)
23347
23883
  if [ -f "$completions_dir/loki.bash" ]; then
@@ -23359,24 +23895,77 @@ cmd_completions() {
23359
23895
  exit 1
23360
23896
  fi
23361
23897
  ;;
23898
+ install)
23899
+ # One-command install: detect the current shell, append a single
23900
+ # guarded line to the right rc file exactly once (idempotent via a
23901
+ # sentinel comment), then tell the user how to activate it. This
23902
+ # replaces the error-prone manual recipe with something that just
23903
+ # works after one command.
23904
+ local detected target_shell rc_file
23905
+ detected="$(basename "${SHELL:-}" 2>/dev/null)"
23906
+ # Allow an explicit override: loki completions install bash|zsh
23907
+ target_shell="${1:-$detected}"
23908
+ case "$target_shell" in
23909
+ bash)
23910
+ rc_file="${HOME}/.bashrc"
23911
+ ;;
23912
+ zsh)
23913
+ rc_file="${HOME}/.zshrc"
23914
+ ;;
23915
+ *)
23916
+ echo -e "${RED}Could not detect a supported shell (got: ${target_shell:-unknown}).${NC}" >&2
23917
+ echo "Specify one explicitly: loki completions install bash | loki completions install zsh" >&2
23918
+ exit 1
23919
+ ;;
23920
+ esac
23921
+ local sentinel="# loki-mode completions"
23922
+ if [ -f "$rc_file" ] && grep -qF "$sentinel" "$rc_file" 2>/dev/null; then
23923
+ echo "Loki completions already installed in ${rc_file} (no change)."
23924
+ echo "Activate now with: source ${rc_file}"
23925
+ return 0
23926
+ fi
23927
+ # Append the sentinel plus a single eval line that re-loads the
23928
+ # completion script at shell start. eval (not redirect) is the
23929
+ # correct persistent form: it sources the script into the
23930
+ # interactive shell each time the rc file runs.
23931
+ {
23932
+ printf '\n%s\n' "$sentinel"
23933
+ printf '%s\n' "eval \"\$(loki completions ${target_shell})\""
23934
+ } >> "$rc_file" || {
23935
+ echo -e "${RED}Failed to write to ${rc_file}.${NC}" >&2
23936
+ exit 1
23937
+ }
23938
+ echo "Installed Loki ${target_shell} completions into ${rc_file}."
23939
+ echo "Activate now with: source ${rc_file} (or restart your shell)"
23940
+ return 0
23941
+ ;;
23362
23942
  *)
23363
23943
  echo -e "${BOLD}Loki Shell Completions${NC}"
23364
23944
  echo ""
23365
23945
  echo "Output shell completion scripts for bash or zsh."
23366
23946
  echo ""
23367
23947
  echo "Usage: loki completions <shell>"
23948
+ echo " loki completions install [bash|zsh]"
23368
23949
  echo ""
23369
23950
  echo "Shells:"
23370
- echo " bash Bash completion script"
23371
- echo " zsh Zsh completion script"
23951
+ echo " bash Bash completion script"
23952
+ echo " zsh Zsh completion script"
23953
+ echo " install Auto-install completions into your shell rc file"
23372
23954
  echo ""
23373
23955
  echo "Installation:"
23374
23956
  echo ""
23375
- echo "Bash:"
23376
- echo " eval \"\$(loki completions bash)\" >> ~/.bashrc"
23957
+ echo " One command (recommended):"
23958
+ echo " loki completions install"
23959
+ echo ""
23960
+ echo " Manual (persistent), Bash:"
23961
+ echo " echo 'eval \"\$(loki completions bash)\"' >> ~/.bashrc"
23962
+ echo " Manual (persistent), Zsh:"
23963
+ echo " echo 'eval \"\$(loki completions zsh)\"' >> ~/.zshrc"
23377
23964
  echo ""
23378
- echo "Zsh:"
23379
- echo " eval \"\$(loki completions zsh)\" >> ~/.zshrc"
23965
+ echo " Session only (current shell), Bash:"
23966
+ echo " eval \"\$(loki completions bash)\""
23967
+ echo " Session only (current shell), Zsh:"
23968
+ echo " eval \"\$(loki completions zsh)\""
23380
23969
  echo ""
23381
23970
  exit 1
23382
23971
  ;;
@@ -29444,7 +30033,7 @@ cmd_share() {
29444
30033
  --private) visibility=""; shift ;;
29445
30034
  --format) format="${2:-markdown}"; shift 2 ;;
29446
30035
  --format=*) format="${1#*=}"; shift ;;
29447
- *) echo -e "${RED}Unknown option: $1${NC}"; echo "Run 'loki share --help' for usage."; exit 1 ;;
30036
+ *) echo -e "${RED}Unknown option: $1${NC}" >&2; echo "Run 'loki share --help' for usage." >&2; exit 1 ;;
29448
30037
  esac
29449
30038
  done
29450
30039
 
@@ -30143,7 +30732,14 @@ cmd_docker() {
30143
30732
  local -a fwd=()
30144
30733
  while [ $# -gt 0 ]; do
30145
30734
  case "$1" in
30146
- --image) [ $# -ge 2 ] && { shift; export LOKI_DOCKER_IMAGE="$1"; } || { echo "loki docker --image requires a value" >&2; return 1; }; shift ;;
30735
+ --image)
30736
+ # Guard against a missing OR flag-shaped value so "--image
30737
+ # --dry-run" cannot swallow the next flag as the image ref.
30738
+ case "${2:-}" in
30739
+ -*|"") echo "loki docker --image requires a value (e.g., --image asklokesh/loki-mode:latest)" >&2; return 1 ;;
30740
+ *) shift; export LOKI_DOCKER_IMAGE="$1"; shift ;;
30741
+ esac
30742
+ ;;
30147
30743
  --dry-run) dry_run=1; shift ;;
30148
30744
  --api) with_api=1; fwd+=("$1"); shift ;;
30149
30745
  *) fwd+=("$1"); shift ;;