loki-mode 7.28.1 → 7.29.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
@@ -68,6 +68,24 @@ if [ -f "$_LOKI_SCRIPT_DIR/crash.sh" ]; then
68
68
  source "$_LOKI_SCRIPT_DIR/crash.sh"
69
69
  fi
70
70
 
71
+ # Provider install offer (v7.29.0): shared, self-contained helper providing
72
+ # detect_any_provider, offer_provider_install, and provider_offer_gate. Sourced
73
+ # here for the bash route; doctor.ts invokes the same file via child_process so
74
+ # the offer copy never drifts between routes. Self-guarded against double-source.
75
+ if [ -f "$_LOKI_SCRIPT_DIR/provider-offer.sh" ]; then
76
+ # shellcheck source=provider-offer.sh
77
+ source "$_LOKI_SCRIPT_DIR/provider-offer.sh"
78
+ fi
79
+
80
+ # Quickstart guided interview (v7.29.0): provides cmd_quickstart and its
81
+ # deterministic offline template matcher. Functions-only file (no top-level
82
+ # command), sourced here so the dispatch case can call cmd_quickstart. Composes
83
+ # the provider offer (above), show_prd_plan, and cmd_start. Self-guarded.
84
+ if [ -f "$_LOKI_SCRIPT_DIR/quickstart.sh" ]; then
85
+ # shellcheck source=quickstart.sh
86
+ source "$_LOKI_SCRIPT_DIR/quickstart.sh"
87
+ fi
88
+
71
89
  # Resolve the script's real path (handles symlinks)
72
90
  resolve_script_path() {
73
91
  local script="$1"
@@ -510,7 +528,7 @@ show_help() {
510
528
  echo "Usage: loki <command> [options]"
511
529
  echo ""
512
530
  echo "New here? Try one of these first:"
513
- echo " loki doctor Check your setup is ready (instant)"
531
+ echo " loki doctor Check your setup is ready (a few seconds)"
514
532
  echo " loki quick \"add a health endpoint\" One small task, start to finish"
515
533
  echo " loki demo Build a sample todo app end to end (real run)"
516
534
  echo " loki start ./prd.md Build from a spec (PRD file, GitHub issue, or no arg)"
@@ -522,6 +540,7 @@ show_help() {
522
540
  echo " quick \"task\" Quick single-task mode (lightweight, 3 iterations max)"
523
541
  echo " monitor [path] Monitor Docker Compose services with auto-fix (v6.67.0)"
524
542
  echo " demo Build a sample todo app end to end (real run)"
543
+ echo " quickstart [idea] Guided first build: setup, idea, template, plan, go"
525
544
  echo " init [name] Project scaffolding with 21 PRD templates"
526
545
  echo " issue <url|num> (deprecated) Use 'loki start <issue-ref>' instead"
527
546
  echo " watch [prd] Auto-rerun on PRD file changes (v6.33.0)"
@@ -675,6 +694,27 @@ show_help() {
675
694
  echo " See: $RUN_SH (header comments) for full list."
676
695
  }
677
696
 
697
+ # Tight newcomer landing for the bare `loki` invocation (no args).
698
+ # v7.28.x UX: the full 70-command table lives in `loki help`; a no-args run
699
+ # now shows only a calm ~12-line orientation so the "what do I do first"
700
+ # block is never buried under the command wall. `loki help`/`--help` are
701
+ # unchanged (still call show_help).
702
+ show_landing() {
703
+ local version
704
+ version=$(get_version)
705
+ echo -e "${BOLD}Loki Mode v$version${NC} - the spec-driven builder that verifies its own work."
706
+ echo ""
707
+ echo -e "First time here? ${CYAN}loki doctor${NC} checks your setup (an AI provider CLI is required)."
708
+ echo ""
709
+ echo "Get started:"
710
+ echo -e " ${CYAN}loki start ./prd.md${NC} Build from a spec (PRD file, GitHub issue, or no arg)"
711
+ echo -e " ${CYAN}loki demo${NC} Build a sample todo app end to end (real run)"
712
+ echo -e " ${CYAN}loki dashboard start${NC} Start the live run monitor (then: loki dashboard open)"
713
+ echo ""
714
+ echo -e "Need help? ${CYAN}loki help${NC} lists every command."
715
+ echo "Tip: preview cost and scope before building: loki plan <your-prd.md>"
716
+ }
717
+
678
718
  # Detect argument type for unified `loki start` (v6.84.0)
679
719
  # Returns one of: prd | issue | empty | unknown
680
720
  # Logic:
@@ -1239,6 +1279,17 @@ cmd_start() {
1239
1279
  esac
1240
1280
  done
1241
1281
 
1282
+ # v7.29.0: provider pre-flight gate. If no provider CLI is installed, offer
1283
+ # to install one (TTY) or print honest manual instructions and exit 2
1284
+ # (non-TTY/CI), BEFORE any spend or runner entry. This moves what used to be
1285
+ # an opaque deep-runner failure to a friendly, actionable top-of-run gate.
1286
+ # --help already returned inside the arg loop, so we never gate help.
1287
+ if declare -f provider_offer_gate >/dev/null 2>&1; then
1288
+ if ! provider_offer_gate; then
1289
+ exit 2
1290
+ fi
1291
+ fi
1292
+
1242
1293
  # Clear any stale raw-brief marker from a PRIOR run before we decide the
1243
1294
  # mode of THIS run. Only the brief branch (below) re-writes it. Without this,
1244
1295
  # a brief run leaves .loki/state/brief.txt behind and a later non-brief run
@@ -2265,10 +2316,18 @@ PYDASH
2265
2316
  # v7.7.30: --all preserves the legacy machine-wide kill. It runs even when
2266
2317
  # the current folder has no live session (the "clean everything" use case),
2267
2318
  # reaping every folder's orphaned loki-run-* temp script (SIGTERM, SIGKILL).
2319
+ #
2320
+ # The kill pattern is "loki-run-" for real users and is NOT user-overridable
2321
+ # at the CLI -- that is the documented machine-wide behavior. LOKI_STOP_ALL_PATTERN
2322
+ # exists ONLY so the test suite can exercise this exact code path against its
2323
+ # own uniquely-marked fake runners without SIGKILLing an unrelated live
2324
+ # loki-run-* on the same machine (e.g. a long SWE-bench instance). It defaults
2325
+ # to "loki-run-" so user-facing semantics are unchanged when unset.
2268
2326
  if [ "$stop_all" = true ]; then
2269
- pkill -f "loki-run-" 2>/dev/null || true
2327
+ local _stop_all_pat="${LOKI_STOP_ALL_PATTERN:-loki-run-}"
2328
+ pkill -f "$_stop_all_pat" 2>/dev/null || true
2270
2329
  sleep 0.5
2271
- pkill -9 -f "loki-run-" 2>/dev/null || true
2330
+ pkill -9 -f "$_stop_all_pat" 2>/dev/null || true
2272
2331
  echo -e "${RED}--all: signalled all loki-run-* processes on this machine.${NC}"
2273
2332
  fi
2274
2333
  }
@@ -3369,7 +3428,7 @@ cmd_provider_set() {
3369
3428
  if [ -z "$new_provider" ]; then
3370
3429
  echo -e "${RED}Error: Provider name required${NC}"
3371
3430
  echo "Usage: loki provider set <claude|codex|cline|aider>"
3372
- exit 1
3431
+ exit 2
3373
3432
  fi
3374
3433
 
3375
3434
  # Validate provider
@@ -3923,12 +3982,16 @@ cmd_dashboard_start() {
3923
3982
 
3924
3983
  if kill -0 "$new_pid" 2>/dev/null; then
3925
3984
  local url="${url_scheme}://${host}:${port}"
3926
- echo -e "${GREEN}Dashboard server started${NC}"
3985
+ echo -e "${GREEN}Dashboard (live run monitor) started${NC}"
3927
3986
  echo ""
3928
3987
  echo " PID: $new_pid"
3929
3988
  echo " URL: $url"
3930
3989
  echo " Logs: $log_file"
3931
3990
  echo ""
3991
+ # v7.28.x UX #5: distinguish the two browser UIs up front so a newcomer
3992
+ # who wants to submit a spec does not land on the ops monitor by mistake.
3993
+ echo -e "${DIM}Looking for the project web UI to submit a spec? loki web (:${PURPLE_LAB_DEFAULT_PORT:-57375})${NC}"
3994
+ echo ""
3932
3995
  echo -e "Open in browser: ${CYAN}loki dashboard open${NC}"
3933
3996
  echo -e "Check status: ${CYAN}loki dashboard status${NC}"
3934
3997
  echo -e "Stop server: ${CYAN}loki dashboard stop${NC}"
@@ -4541,12 +4604,15 @@ cmd_web_start() {
4541
4604
  local url="http://${PURPLE_LAB_DEFAULT_HOST}:${port}/lab/"
4542
4605
 
4543
4606
  if [ "$server_ready" = true ]; then
4544
- echo -e "${GREEN}Purple Lab running at: $url${NC} (PID: $pid)"
4607
+ echo -e "${GREEN}Purple Lab (project web UI) running at: $url${NC} (PID: $pid)"
4545
4608
  else
4546
- echo -e "${YELLOW}Purple Lab starting at: $url${NC} (PID: $pid)"
4609
+ echo -e "${YELLOW}Purple Lab (project web UI) starting at: $url${NC} (PID: $pid)"
4547
4610
  echo "Server may still be loading. Refresh the browser if it does not load immediately."
4548
4611
  fi
4549
4612
  echo -e "Logs: $log_file"
4613
+ # v7.28.x UX #5: distinguish the two browser UIs so the ops monitor and the
4614
+ # spec-input web UI are never confused for each other.
4615
+ echo -e "${DIM}Looking for the live run monitor? loki dashboard (:${DASHBOARD_DEFAULT_PORT:-57374})${NC}"
4550
4616
  echo -e "Stop with: ${CYAN}loki web stop${NC}"
4551
4617
 
4552
4618
  # Open browser only after server is confirmed ready
@@ -5133,7 +5199,7 @@ cmd_issue_parse() {
5133
5199
  if [[ -z "$issue_ref" ]]; then
5134
5200
  echo -e "${RED}Error: Issue reference required${NC}"
5135
5201
  echo "Usage: loki issue parse <issue-ref>"
5136
- exit 1
5202
+ exit 2
5137
5203
  fi
5138
5204
 
5139
5205
  # Find and run issue-parser.sh
@@ -5177,7 +5243,7 @@ cmd_issue_view() {
5177
5243
  if [[ -z "$issue_ref" ]]; then
5178
5244
  echo -e "${RED}Error: Issue reference required${NC}"
5179
5245
  echo "Usage: loki issue view <issue-ref>"
5180
- exit 1
5246
+ exit 2
5181
5247
  fi
5182
5248
 
5183
5249
  # Find and run issue-parser.sh
@@ -5515,7 +5581,7 @@ cmd_run() {
5515
5581
  echo " loki run owner/repo#789 # GitHub with specific repo"
5516
5582
  echo ""
5517
5583
  echo "Run 'loki run --help' for full usage."
5518
- exit 1
5584
+ exit 2
5519
5585
  fi
5520
5586
 
5521
5587
  # Source issue provider abstraction
@@ -5994,7 +6060,7 @@ cmd_issue() {
5994
6060
  echo -e "${RED}Error: No issue number specified${NC}"
5995
6061
  echo "Usage: loki issue <number> or loki issue --number <number>"
5996
6062
  echo "Run 'loki issue --help' for full usage."
5997
- exit 1
6063
+ exit 2
5998
6064
  fi
5999
6065
 
6000
6066
  # Auto-detect repo if not specified
@@ -6912,7 +6978,7 @@ cmd_config_set() {
6912
6978
  if [[ -z "$key" || -z "$value" ]]; then
6913
6979
  echo -e "${RED}Usage: loki config set [--global] <key> <value>${NC}"
6914
6980
  echo "Run 'loki config' for list of settable keys."
6915
- return 1
6981
+ return 2
6916
6982
  fi
6917
6983
 
6918
6984
  # Determine config directory: --global writes to ~/.config/loki-mode/
@@ -7059,7 +7125,7 @@ cmd_config_get() {
7059
7125
 
7060
7126
  if [[ -z "$key" ]]; then
7061
7127
  echo -e "${RED}Usage: loki config get <key>${NC}"
7062
- return 1
7128
+ return 2
7063
7129
  fi
7064
7130
 
7065
7131
  local config_store="$LOKI_DIR/config/settings.json"
@@ -7700,15 +7766,19 @@ cmd_doctor() {
7700
7766
  doctor_check "Cline CLI" cline optional || true
7701
7767
  doctor_check "Aider CLI" aider optional || true
7702
7768
 
7703
- # Check if at least one provider is installed
7704
- local _any_provider=false
7705
- for _dp in claude codex cline aider; do
7706
- command -v "$_dp" &>/dev/null && _any_provider=true && break
7707
- done
7708
- if ! $_any_provider; then
7769
+ # Check if at least one provider is installed (detect_any_provider is the
7770
+ # shared helper from provider-offer.sh, extracted from this exact loop).
7771
+ if ! detect_any_provider; then
7709
7772
  echo -e " ${RED}FAIL${NC} No AI provider CLI installed -- at least one is required"
7710
7773
  echo -e " ${YELLOW}Install: npm install -g @anthropic-ai/claude-code${NC}"
7711
7774
  fail_count=$((fail_count + 1))
7775
+ # v7.29.0: on a TTY (non-json), append the consent-gated install offer.
7776
+ # In report mode the helper is a no-op on non-TTY/CI, so the doctor
7777
+ # output stays byte-identical for the bun-parity matrix. --json never
7778
+ # reaches here (cmd_doctor dispatches to cmd_doctor_json earlier).
7779
+ if declare -f offer_provider_install >/dev/null 2>&1; then
7780
+ offer_provider_install report || true
7781
+ fi
7712
7782
  fi
7713
7783
  echo ""
7714
7784
 
@@ -8781,7 +8851,7 @@ cmd_notify_send() {
8781
8851
  if [ -z "$message" ]; then
8782
8852
  echo -e "${RED}Error: Message required${NC}"
8783
8853
  echo "Usage: loki notify $channel <message>"
8784
- exit 1
8854
+ exit 2
8785
8855
  fi
8786
8856
 
8787
8857
  case "$channel" in
@@ -9002,6 +9072,69 @@ cmd_sandbox() {
9002
9072
  exec "$SANDBOX_SH" "$subcommand" "$@"
9003
9073
  }
9004
9074
 
9075
+ # v7.29.0 (feature #4): print the SIMPLE-tier cost/time/iteration estimate for
9076
+ # the demo PRD, parsed from the single estimator source (show_prd_plan JSON).
9077
+ # Because `loki demo` always runs `loki start --simple` (which exports
9078
+ # LOKI_COMPLEXITY=simple), the estimate is computed with LOKI_COMPLEXITY=simple
9079
+ # so the quoted figures are the ones the demo will actually incur -- honest by
9080
+ # construction (the keystone fix makes show_prd_plan honor LOKI_COMPLEXITY).
9081
+ # Returns 0 when a real estimate was printed, 1 when the estimator failed (the
9082
+ # caller then falls back to a no-number confirm). Never fabricates a number.
9083
+ emit_demo_estimate() {
9084
+ local prd_path="$1"
9085
+ local plan_json=""
9086
+ plan_json=$(LOKI_COMPLEXITY=simple show_prd_plan "$prd_path" "true" "false" 2>/dev/null) || plan_json=""
9087
+
9088
+ if [ -z "$plan_json" ]; then
9089
+ echo -e "${YELLOW}Could not compute a cost estimate (the estimator did not return a result).${NC}"
9090
+ return 1
9091
+ fi
9092
+
9093
+ local parsed
9094
+ parsed=$(printf '%s' "$plan_json" | python3 -c "
9095
+ import json, sys
9096
+ try:
9097
+ d = json.load(sys.stdin)
9098
+ except Exception:
9099
+ sys.exit(1)
9100
+ cost = d.get('cost', {}).get('total_usd')
9101
+ time_est = d.get('time', {}).get('estimated')
9102
+ iters = d.get('iterations', {}).get('estimated')
9103
+ rng = d.get('iterations', {}).get('range', [])
9104
+ tier = d.get('complexity', {}).get('tier', 'simple')
9105
+ if cost is None or time_est is None or iters is None:
9106
+ sys.exit(1)
9107
+ rng_str = ''
9108
+ if isinstance(rng, list) and len(rng) == 2:
9109
+ rng_str = ' (range {}-{})'.format(rng[0], rng[1])
9110
+ print(tier.upper())
9111
+ print('{:.2f}'.format(float(cost)))
9112
+ print(time_est)
9113
+ print('{}{}'.format(iters, rng_str))
9114
+ " 2>/dev/null) || parsed=""
9115
+
9116
+ if [ -z "$parsed" ]; then
9117
+ echo -e "${YELLOW}Could not compute a cost estimate (the estimator did not return a result).${NC}"
9118
+ return 1
9119
+ fi
9120
+
9121
+ local tier_u cost_u time_u iter_u
9122
+ tier_u=$(printf '%s' "$parsed" | sed -n '1p')
9123
+ cost_u=$(printf '%s' "$parsed" | sed -n '2p')
9124
+ time_u=$(printf '%s' "$parsed" | sed -n '3p')
9125
+ iter_u=$(printf '%s' "$parsed" | sed -n '4p')
9126
+
9127
+ echo -e "${BOLD}Estimate (${tier_u} tier, the path this demo actually runs):${NC}"
9128
+ printf ' Cost: ~$%s\n' "$cost_u"
9129
+ echo " Time: ~$time_u"
9130
+ echo " Iterations: $iter_u"
9131
+ echo ""
9132
+ echo -e "${DIM}This is an estimate. Actual usage depends on PRD complexity,"
9133
+ echo -e "code review cycles, and test failures.${NC}"
9134
+ echo ""
9135
+ return 0
9136
+ }
9137
+
9005
9138
  # Demo mode - build a real project from a bundled PRD template
9006
9139
  cmd_demo() {
9007
9140
  # Handle --help
@@ -9034,10 +9167,21 @@ cmd_demo() {
9034
9167
  local provider=""
9035
9168
  local dry_run=false
9036
9169
  local start_args=()
9170
+ # v7.29.0 (feature #4): demo now shows the FORCED-simple cost estimate
9171
+ # before spending and asks for confirmation. --yes / LOKI_ASSUME_YES skip
9172
+ # the prompt but still print the estimate (the spend is never hidden).
9173
+ local assume_yes=false
9174
+ if [ "${LOKI_ASSUME_YES:-}" = "1" ] || [ "${LOKI_ASSUME_YES:-}" = "true" ]; then
9175
+ assume_yes=true
9176
+ fi
9037
9177
 
9038
9178
  # Parse arguments
9039
9179
  while [[ $# -gt 0 ]]; do
9040
9180
  case "$1" in
9181
+ --yes|-y)
9182
+ assume_yes=true
9183
+ shift
9184
+ ;;
9041
9185
  --dir)
9042
9186
  if [[ -z "${2:-}" ]]; then
9043
9187
  echo -e "${RED}Error: --dir requires a path${NC}"
@@ -9078,6 +9222,17 @@ cmd_demo() {
9078
9222
  esac
9079
9223
  done
9080
9224
 
9225
+ # v7.29.0: provider pre-flight gate (skipped for --dry-run, which never
9226
+ # spends). On a TTY, offer to install; on non-TTY/CI, print honest manual
9227
+ # instructions and exit 2 before any spend. Detect-first, so the nested
9228
+ # cmd_start gate below no-ops once a provider is present. --help already
9229
+ # returned at the top of cmd_demo.
9230
+ if [ "$dry_run" != true ] && declare -f provider_offer_gate >/dev/null 2>&1; then
9231
+ if ! provider_offer_gate; then
9232
+ exit 2
9233
+ fi
9234
+ fi
9235
+
9081
9236
  # Fall back to examples/ if templates/ doesn't exist
9082
9237
  if [ ! -f "$demo_prd" ]; then
9083
9238
  demo_prd="$SKILL_DIR/examples/simple-todo-app.md"
@@ -9118,6 +9273,9 @@ cmd_demo() {
9118
9273
  fi
9119
9274
 
9120
9275
  if [ "$dry_run" = true ]; then
9276
+ # v7.29.0: dry-run is now a genuine cost preview -- show the estimate,
9277
+ # no prompt, no spend.
9278
+ emit_demo_estimate "$demo_dir/prd.md" || true
9121
9279
  echo -e "${YELLOW}[dry-run] Would run:${NC}"
9122
9280
  echo " cd $demo_dir"
9123
9281
  echo -n " loki start prd.md --simple --yes"
@@ -9128,6 +9286,52 @@ cmd_demo() {
9128
9286
  return 0
9129
9287
  fi
9130
9288
 
9289
+ # v7.29.0 (feature #4): cost confirm before any spend. The estimate ALWAYS
9290
+ # prints (even with --yes), so the spend is never hidden. The prompt is
9291
+ # shown only on a TTY without --yes; non-TTY without --yes refuses (exit 2)
9292
+ # rather than hanging on read -- the project's "never hang in CI" idiom.
9293
+ local estimate_ok=true
9294
+ emit_demo_estimate "$demo_dir/prd.md" || estimate_ok=false
9295
+
9296
+ # Interactive only when stdin is a real TTY and CI is not forcing
9297
+ # non-interactive (matches the project's first-run gate semantics).
9298
+ local demo_interactive=true
9299
+ if [ ! -t 0 ] || [ -n "${CI:-}" ]; then
9300
+ demo_interactive=false
9301
+ fi
9302
+
9303
+ if [ "$assume_yes" != true ]; then
9304
+ if [ "$demo_interactive" = true ]; then
9305
+ local demo_answer=""
9306
+ if [ "$estimate_ok" = true ]; then
9307
+ # Default YES: Enter proceeds.
9308
+ echo -n "Build the Todo app now? [Y/n] "
9309
+ read -r demo_answer </dev/tty 2>/dev/null || demo_answer=""
9310
+ if [[ -n "$demo_answer" && ! "$demo_answer" =~ ^[Yy] ]]; then
9311
+ echo ""
9312
+ echo -e "Cancelled. Nothing was spent. The demo PRD is at:"
9313
+ echo -e " ${CYAN}$demo_dir/prd.md${NC}"
9314
+ echo -e "Run 'loki plan ${demo_dir}/prd.md' for the full breakdown."
9315
+ return 0
9316
+ fi
9317
+ else
9318
+ # No honest number available: default NO (the safe direction).
9319
+ echo -n "Proceed with the demo build anyway? [y/N] "
9320
+ read -r demo_answer </dev/tty 2>/dev/null || demo_answer=""
9321
+ if [[ ! "$demo_answer" =~ ^[Yy] ]]; then
9322
+ echo ""
9323
+ echo -e "Cancelled. Nothing was spent. The demo PRD is at:"
9324
+ echo -e " ${CYAN}$demo_dir/prd.md${NC}"
9325
+ return 0
9326
+ fi
9327
+ fi
9328
+ else
9329
+ # Non-TTY without --yes: never hang; refuse with an honest message.
9330
+ echo "demo needs confirmation; re-run with --yes to proceed non-interactively" >&2
9331
+ return 2
9332
+ fi
9333
+ fi
9334
+
9131
9335
  echo -e "${DIM}Starting autonomous build...${NC}"
9132
9336
  echo -e "${DIM}(Press Ctrl+C to stop at any time)${NC}"
9133
9337
  echo ""
@@ -9349,7 +9553,16 @@ cmd_quick() {
9349
9553
  echo " loki quick \"fix the login bug in auth.js\""
9350
9554
  echo " loki quick \"add input validation to the signup form\""
9351
9555
  echo " loki quick \"write unit tests for the API endpoints\""
9352
- exit 1
9556
+ exit 2
9557
+ fi
9558
+
9559
+ # v7.29.0: provider pre-flight gate. Offer install on a TTY; print honest
9560
+ # manual instructions and exit 2 on non-TTY/CI, before any spend. --help and
9561
+ # the no-args usage path already returned/exited above.
9562
+ if declare -f provider_offer_gate >/dev/null 2>&1; then
9563
+ if ! provider_offer_gate; then
9564
+ exit 2
9565
+ fi
9353
9566
  fi
9354
9567
 
9355
9568
  local task_desc="$*"
@@ -10692,7 +10905,7 @@ cmd_migrate_start() {
10692
10905
  echo "Specify the migration target (e.g. --target typescript, --target react, --target microservices)"
10693
10906
  echo ""
10694
10907
  echo "Run 'loki migrate --help' for usage."
10695
- return 1
10908
+ return 2
10696
10909
  fi
10697
10910
 
10698
10911
  # Validate compliance preset
@@ -11662,7 +11875,7 @@ for f in data.get('frictions', []):
11662
11875
  if [ -z "$codebase_path" ]; then
11663
11876
  echo -e "${RED}Error: Codebase path is required${NC}"
11664
11877
  echo "Usage: loki heal <path-to-codebase> [options]"
11665
- return 1
11878
+ return 2
11666
11879
  fi
11667
11880
 
11668
11881
  if [[ ! -d "$codebase_path" ]]; then
@@ -12030,7 +12243,7 @@ cmd_migrate() {
12030
12243
  echo "Usage: loki migrate <path-to-codebase> --target <target>"
12031
12244
  echo ""
12032
12245
  echo "Run 'loki migrate --help' for usage."
12033
- return 1
12246
+ return 2
12034
12247
  fi
12035
12248
 
12036
12249
  cmd_migrate_start "$codebase_path" "$target" "$plan_only" "$phase" "$parallel" "$compliance" "$dry_run" "$do_resume" "$multi_repo" "$export_report" "$no_dashboard" "$no_docs"
@@ -12805,6 +13018,20 @@ show_verbose = sys.argv[3] == 'true'
12805
13018
  session_model_env = (os.environ.get('LOKI_SESSION_MODEL', 'sonnet') or 'sonnet').strip().lower()
12806
13019
  legacy_tier_switching = (os.environ.get('LOKI_LEGACY_TIER_SWITCHING', 'false') or 'false').strip().lower() == 'true'
12807
13020
 
13021
+ # v7.29.0: Honor LOKI_COMPLEXITY -- the SAME env var the runner honors at
13022
+ # run.sh:920 -- so a forced-tier run (e.g. 'loki demo' / 'loki start --simple'
13023
+ # which export LOKI_COMPLEXITY=simple) quotes the tier it will actually run,
13024
+ # not the content-derived tier. Without this the estimate diverges from
13025
+ # execution (audit finding #9). Runner vocabulary is auto|simple|standard|complex;
13026
+ # the estimator's internal tiers are simple|moderate|complex|enterprise, so
13027
+ # 'standard' aliases to 'moderate' (the 6-12 iteration analog). 'auto', empty,
13028
+ # and any unrecognized value are ignored (content tier is used) -- honest by
13029
+ # construction. The override is applied AFTER the content score-to-tier map.
13030
+ _forced_complexity_raw = (os.environ.get('LOKI_COMPLEXITY', '') or '').strip().lower()
13031
+ forced_complexity = {'standard': 'moderate'}.get(_forced_complexity_raw, _forced_complexity_raw)
13032
+ if forced_complexity not in ('simple', 'moderate', 'complex', 'enterprise'):
13033
+ forced_complexity = ''
13034
+
12808
13035
  # Map session model name to tier key used in tokens_per_tier below.
12809
13036
  # Unknown models fall through to 'development' (Sonnet) as a safe default.
12810
13037
  _session_tier_map = {
@@ -12957,6 +13184,14 @@ elif complexity_score <= 5:
12957
13184
  else:
12958
13185
  complexity = 'enterprise'
12959
13186
 
13187
+ # v7.29.0: apply the forced-tier override AFTER the content score-to-tier map,
13188
+ # so it is not clobbered by the assignment above. Everything downstream
13189
+ # (iterations, tokens, cost, time) derives from the complexity variable, so the
13190
+ # whole estimate becomes consistent with what the run will actually do.
13191
+ if forced_complexity:
13192
+ complexity = forced_complexity
13193
+ complexity_reasons.insert(0, 'Complexity forced via LOKI_COMPLEXITY=' + _forced_complexity_raw)
13194
+
12960
13195
  # --- Estimate iterations ---
12961
13196
  iteration_map = {
12962
13197
  'simple': (3, 5),
@@ -13187,7 +13422,10 @@ if ui_count > 0:
13187
13422
  color = {'simple': GREEN, 'moderate': YELLOW, 'complex': RED, 'enterprise': RED}
13188
13423
  cx_color = color.get(complexity, NC)
13189
13424
  print(f'\n{CYAN}Complexity{NC}')
13190
- print(f' Tier: {cx_color}{BOLD}{complexity.upper()}{NC} (score: {complexity_score})')
13425
+ if forced_complexity:
13426
+ print(f' Tier: {cx_color}{BOLD}{complexity.upper()}{NC} (forced via LOKI_COMPLEXITY={_forced_complexity_raw})')
13427
+ else:
13428
+ print(f' Tier: {cx_color}{BOLD}{complexity.upper()}{NC} (score: {complexity_score})')
13191
13429
  for reason in complexity_reasons:
13192
13430
  print(f' {DIM}- {reason}{NC}')
13193
13431
 
@@ -13333,7 +13571,7 @@ cmd_plan() {
13333
13571
  if [ -z "$prd_file" ]; then
13334
13572
  echo -e "${RED}Usage: loki plan <PRD file>${NC}"
13335
13573
  echo "Run 'loki plan --help' for usage."
13336
- return 1
13574
+ return 2
13337
13575
  fi
13338
13576
 
13339
13577
  if [ ! -f "$prd_file" ]; then
@@ -13388,7 +13626,9 @@ main() {
13388
13626
  fi
13389
13627
 
13390
13628
  if [ $# -eq 0 ]; then
13391
- show_help
13629
+ # v7.28.x UX: bare `loki` shows a tight newcomer landing, not the full
13630
+ # command wall. `loki help`/`--help` still print show_help unchanged.
13631
+ show_landing
13392
13632
  exit 0
13393
13633
  fi
13394
13634
 
@@ -13416,6 +13656,9 @@ main() {
13416
13656
  demo)
13417
13657
  cmd_demo "$@"
13418
13658
  ;;
13659
+ quickstart)
13660
+ cmd_quickstart "$@"
13661
+ ;;
13419
13662
  welcome)
13420
13663
  cmd_welcome "$@"
13421
13664
  ;;
@@ -17722,7 +17965,7 @@ cmd_enterprise() {
17722
17965
  if [ -z "$name" ]; then
17723
17966
  echo -e "${RED}Error: Token name required${NC}"
17724
17967
  echo "Usage: loki enterprise token generate <name> [--scopes '*'] [--expires 30]"
17725
- exit 1
17968
+ exit 2
17726
17969
  fi
17727
17970
 
17728
17971
  # Validate name is not a flag
@@ -17890,7 +18133,7 @@ else:
17890
18133
 
17891
18134
  if [ -z "$identifier" ]; then
17892
18135
  echo -e "${RED}Error: Token ID or name required${NC}"
17893
- exit 1
18136
+ exit 2
17894
18137
  fi
17895
18138
 
17896
18139
  python3 -c "
@@ -17929,7 +18172,7 @@ else:
17929
18172
 
17930
18173
  if [ -z "$identifier" ]; then
17931
18174
  echo -e "${RED}Error: Token ID or name required${NC}"
17932
- exit 1
18175
+ exit 2
17933
18176
  fi
17934
18177
 
17935
18178
  python3 -c "