loki-mode 7.28.2 → 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/README.md CHANGED
@@ -28,6 +28,7 @@
28
28
  - **Production quality built in** -- 11 quality gates (`skills/quality-gates.md`), blind 3-reviewer code review (`run.sh:run_code_review()`), anti-sycophancy checks
29
29
  - **Standalone verification: `loki verify`** -- Run Loki's deterministic gates (build, tests, static analysis, secret scan, dependency audit) against any branch or PR diff, including code written by other agents or humans. CI-ready exit codes (0 VERIFIED, 1 CONCERNS, 2 BLOCKED), machine-readable evidence at `.loki/verify/evidence.json`. Inconclusive evidence is never reported as VERIFIED (v7.27.0).
30
30
  - **Living spec and pre-build interrogation** -- `loki spec` locks a spec and detects drift deterministically (`spec.lock`, `drift-report.json`, and a `SPEC_DRIFT` finding in `loki verify` with CI exit codes), so you can tell when the build diverges from what was agreed. `loki grill` runs a Devil's-Advocate interrogation of the spec before you build, surfacing gaps and contradictions early (v7.28.0).
31
+ - **Guided first build: `loki quickstart`** -- four quick questions (setup check, one-line idea, template pick, plan review) and your build starts; pressing Enter through every step builds the sample Todo app. The plan step quotes the real cost/time estimate before anything is spent, and `loki demo` now confirms its estimate the same way. If no AI provider CLI is installed, Loki offers to install Claude Code (consent-gated, interactive terminals only) (v7.29.0).
31
32
  - **Live App Preview** -- The dashboard embeds the locally-running app in an iframe so you can interact with it immediately during a build. Use `loki preview` (alias `loki open`) to print the URL and open it in your browser. Local-first: no hosted service, no vendor lock (v7.24.0).
32
33
  - **Compose-first fullstack** -- When a spec needs more than one service (web + database + cache) Loki generates a 12-factor `docker-compose.yml` with healthchecks, `depends_on` wiring, env-var config, and a `.env.example`. The Live App Preview surfaces the web service URL (not a database port), and health reflects the web service's Docker healthcheck so a crashed app shows as crashed even when the database stays up. Single-service apps stay on a plain run command. All local-first, no hosted service (v7.26.0).
33
34
  - **Intelligent `loki start`** -- For interactive foreground runs the dashboard auto-opens in the browser (cross-platform; skipped in CI, SSH-without-TTY, and piped runs; opt out with `LOKI_NO_AUTO_OPEN=1`). The completion summary shows "Your app is live at <url>" so you know exactly where to try what Loki just built. The autonomous loop passes Claude Code's `--effort`, `--max-budget-usd`, and `--fallback-model` on every iteration (each gated on CLI support and individual opt-out env vars) for better long-run unattended execution (v7.25.0).
@@ -101,7 +102,7 @@ loki quick "build a landing page with a signup form"
101
102
  |--------|---------|-------|
102
103
  | **Bun (recommended)** | `bun install -g loki-mode` | Fastest startup for CLI commands. |
103
104
  | **Homebrew** | `brew tap asklokesh/tap && brew install loki-mode` | Auto-installs Bun as a dep |
104
- | **Docker** | `docker pull asklokesh/loki-mode:7.28.2 && docker run --rm asklokesh/loki-mode:7.28.2 start prd.md` | Bun pre-installed in image |
105
+ | **Docker** | `docker pull asklokesh/loki-mode:7.29.0 && docker run --rm asklokesh/loki-mode:7.29.0 start prd.md` | Bun pre-installed in image |
105
106
  | **npm (compat)** | `npm install -g loki-mode` | Works without Bun (bash fallback). Migrate any time with `loki self-update --to bun`. |
106
107
 
107
108
  **Upgrading:**
@@ -161,7 +162,7 @@ The next major release sunsets the Bash runtime entirely. There is no firm calen
161
162
  | Method | Command |
162
163
  |--------|---------|
163
164
  | **Homebrew** | `brew tap asklokesh/tap && brew install loki-mode` |
164
- | **Docker** | `docker pull asklokesh/loki-mode:7.28.2` |
165
+ | **Docker** | `docker pull asklokesh/loki-mode:7.29.0` |
165
166
  | **Inside Claude Code** | `claude --dangerously-skip-permissions` then type "Loki Mode" |
166
167
  | **Git clone** | `git clone https://github.com/asklokesh/loki-mode.git` |
167
168
 
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 11 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
4
4
  ---
5
5
 
6
- # Loki Mode v7.28.2
6
+ # Loki Mode v7.29.0
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -342,6 +342,12 @@ Two completion-trust features extend the verification gates. Full details in `sk
342
342
  - **Held-out spec evals:** ~25% of checklist items (deterministic `sha256(id)` order, `N >= 4`) are reserved into `.loki/checklist/held-out.json` and excluded from the build prompt feed; the completion council blocks if a held-out item fails. Opt out with `LOKI_HELDOUT_GATE=0`. Honest limit: this guards the prompt feed, not a sandbox; the reservation file is on disk and an agent with filesystem access can read it.
343
343
  - **Inconclusive-baseline disclosure:** when the evidence gate cannot establish a diff baseline (`no_git_repo` / `no_run_start_sha`) it writes `.loki/state/evidence-inconclusive.json` and `COMPLETION.txt` carries an honest "not independently verified" line. It never blocks non-git projects; red tests still block.
344
344
 
345
+ ## First-run UX (v7.29.0)
346
+
347
+ - **`loki quickstart`:** guided 4-step first build (setup check, one-line idea, offline template match, plan review with real estimator figures); Enter-through-everything builds the sample Todo app; non-TTY/CI exits 2 with an automation hint.
348
+ - **Provider install offer:** when no provider CLI is found, doctor and the start/demo/quick/quickstart pre-flight offer to install Claude Code. Consent-gated on an interactive TTY only; the single command executed is printed first; auth handoff via `claude auth login` with readiness confirmed by `claude auth status`. Opt out: `LOKI_NO_INSTALL_OFFER=1`.
349
+ - **`loki demo` cost confirm:** the estimate always prints before spending; `--yes` skips the prompt, never the estimate. `LOKI_COMPLEXITY` is honored by `loki plan` with an honest forced-tier note.
350
+
345
351
  ---
346
352
 
347
353
  ## Concurrency and Security Hardening (v7.5.7 - v7.5.13)
@@ -392,4 +398,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
392
398
 
393
399
  ---
394
400
 
395
- **v7.28.2 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
401
+ **v7.29.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.28.2
1
+ 7.29.0
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"
@@ -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)"
@@ -1260,6 +1279,17 @@ cmd_start() {
1260
1279
  esac
1261
1280
  done
1262
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
+
1263
1293
  # Clear any stale raw-brief marker from a PRIOR run before we decide the
1264
1294
  # mode of THIS run. Only the brief branch (below) re-writes it. Without this,
1265
1295
  # a brief run leaves .loki/state/brief.txt behind and a later non-brief run
@@ -2286,10 +2316,18 @@ PYDASH
2286
2316
  # v7.7.30: --all preserves the legacy machine-wide kill. It runs even when
2287
2317
  # the current folder has no live session (the "clean everything" use case),
2288
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.
2289
2326
  if [ "$stop_all" = true ]; then
2290
- 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
2291
2329
  sleep 0.5
2292
- pkill -9 -f "loki-run-" 2>/dev/null || true
2330
+ pkill -9 -f "$_stop_all_pat" 2>/dev/null || true
2293
2331
  echo -e "${RED}--all: signalled all loki-run-* processes on this machine.${NC}"
2294
2332
  fi
2295
2333
  }
@@ -7728,15 +7766,19 @@ cmd_doctor() {
7728
7766
  doctor_check "Cline CLI" cline optional || true
7729
7767
  doctor_check "Aider CLI" aider optional || true
7730
7768
 
7731
- # Check if at least one provider is installed
7732
- local _any_provider=false
7733
- for _dp in claude codex cline aider; do
7734
- command -v "$_dp" &>/dev/null && _any_provider=true && break
7735
- done
7736
- 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
7737
7772
  echo -e " ${RED}FAIL${NC} No AI provider CLI installed -- at least one is required"
7738
7773
  echo -e " ${YELLOW}Install: npm install -g @anthropic-ai/claude-code${NC}"
7739
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
7740
7782
  fi
7741
7783
  echo ""
7742
7784
 
@@ -9030,6 +9072,69 @@ cmd_sandbox() {
9030
9072
  exec "$SANDBOX_SH" "$subcommand" "$@"
9031
9073
  }
9032
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
+
9033
9138
  # Demo mode - build a real project from a bundled PRD template
9034
9139
  cmd_demo() {
9035
9140
  # Handle --help
@@ -9062,10 +9167,21 @@ cmd_demo() {
9062
9167
  local provider=""
9063
9168
  local dry_run=false
9064
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
9065
9177
 
9066
9178
  # Parse arguments
9067
9179
  while [[ $# -gt 0 ]]; do
9068
9180
  case "$1" in
9181
+ --yes|-y)
9182
+ assume_yes=true
9183
+ shift
9184
+ ;;
9069
9185
  --dir)
9070
9186
  if [[ -z "${2:-}" ]]; then
9071
9187
  echo -e "${RED}Error: --dir requires a path${NC}"
@@ -9106,6 +9222,17 @@ cmd_demo() {
9106
9222
  esac
9107
9223
  done
9108
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
+
9109
9236
  # Fall back to examples/ if templates/ doesn't exist
9110
9237
  if [ ! -f "$demo_prd" ]; then
9111
9238
  demo_prd="$SKILL_DIR/examples/simple-todo-app.md"
@@ -9146,6 +9273,9 @@ cmd_demo() {
9146
9273
  fi
9147
9274
 
9148
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
9149
9279
  echo -e "${YELLOW}[dry-run] Would run:${NC}"
9150
9280
  echo " cd $demo_dir"
9151
9281
  echo -n " loki start prd.md --simple --yes"
@@ -9156,6 +9286,52 @@ cmd_demo() {
9156
9286
  return 0
9157
9287
  fi
9158
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
+
9159
9335
  echo -e "${DIM}Starting autonomous build...${NC}"
9160
9336
  echo -e "${DIM}(Press Ctrl+C to stop at any time)${NC}"
9161
9337
  echo ""
@@ -9380,6 +9556,15 @@ cmd_quick() {
9380
9556
  exit 2
9381
9557
  fi
9382
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
9566
+ fi
9567
+
9383
9568
  local task_desc="$*"
9384
9569
  local version=$(get_version)
9385
9570
  local max_iter="${LOKI_MAX_ITERATIONS:-3}"
@@ -12833,6 +13018,20 @@ show_verbose = sys.argv[3] == 'true'
12833
13018
  session_model_env = (os.environ.get('LOKI_SESSION_MODEL', 'sonnet') or 'sonnet').strip().lower()
12834
13019
  legacy_tier_switching = (os.environ.get('LOKI_LEGACY_TIER_SWITCHING', 'false') or 'false').strip().lower() == 'true'
12835
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
+
12836
13035
  # Map session model name to tier key used in tokens_per_tier below.
12837
13036
  # Unknown models fall through to 'development' (Sonnet) as a safe default.
12838
13037
  _session_tier_map = {
@@ -12985,6 +13184,14 @@ elif complexity_score <= 5:
12985
13184
  else:
12986
13185
  complexity = 'enterprise'
12987
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
+
12988
13195
  # --- Estimate iterations ---
12989
13196
  iteration_map = {
12990
13197
  'simple': (3, 5),
@@ -13215,7 +13422,10 @@ if ui_count > 0:
13215
13422
  color = {'simple': GREEN, 'moderate': YELLOW, 'complex': RED, 'enterprise': RED}
13216
13423
  cx_color = color.get(complexity, NC)
13217
13424
  print(f'\n{CYAN}Complexity{NC}')
13218
- 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})')
13219
13429
  for reason in complexity_reasons:
13220
13430
  print(f' {DIM}- {reason}{NC}')
13221
13431
 
@@ -13446,6 +13656,9 @@ main() {
13446
13656
  demo)
13447
13657
  cmd_demo "$@"
13448
13658
  ;;
13659
+ quickstart)
13660
+ cmd_quickstart "$@"
13661
+ ;;
13449
13662
  welcome)
13450
13663
  cmd_welcome "$@"
13451
13664
  ;;
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env bash
2
+ # provider-offer.sh -- shared, self-contained provider install offer (v7.29.0).
3
+ #
4
+ # Single source of truth for "no AI provider CLI found" handling, used by BOTH
5
+ # the bash CLI (autonomy/loki, sourced) and the Bun-routed doctor
6
+ # (loki-ts/src/commands/doctor.ts, via child_process). Parity is by
7
+ # construction: there is exactly one prompt + npm install + login handoff
8
+ # implementation, and both routes call it.
9
+ #
10
+ # Self-containment contract (load-bearing for parity): this file depends ONLY
11
+ # on bash builtins + npm/claude on PATH. It defines its own colors and never
12
+ # reads $RED/$NC or any other variable owned by autonomy/loki, because when
13
+ # doctor.ts spawns it standalone those variables are unset. If this file ever
14
+ # starts depending on loki's environment, the bash-route and Bun-route bytes
15
+ # diverge and the bun-parity matrix breaks.
16
+ #
17
+ # Security posture (design 1.7): the ONLY command ever executed on the user's
18
+ # behalf is `npm install -g @anthropic-ai/claude-code`, only after explicit
19
+ # consent, with the exact command printed first. No sudo. No curl-pipe-bash.
20
+ # Non-interactive / CI contexts never run an install.
21
+
22
+ # Guard against double-source (loki may source this more than once via reloads).
23
+ if [ -n "${_LOKI_PROVIDER_OFFER_SOURCED:-}" ]; then
24
+ return 0 2>/dev/null || true
25
+ fi
26
+ _LOKI_PROVIDER_OFFER_SOURCED=1
27
+
28
+ # --- Self-contained colors (honor NO_COLOR; no dependency on loki) ----------
29
+ if [ -n "${NO_COLOR:-}" ] || [ ! -t 1 ]; then
30
+ _PO_RED=''; _PO_YELLOW=''; _PO_BOLD=''; _PO_DIM=''; _PO_NC=''
31
+ else
32
+ _PO_RED=$'\033[0;31m'
33
+ _PO_YELLOW=$'\033[1;33m'
34
+ _PO_BOLD=$'\033[1m'
35
+ _PO_NC=$'\033[0m'
36
+ fi
37
+
38
+ # The one canonical install command. Quoted everywhere; never re-derived.
39
+ _PO_INSTALL_CMD="npm install -g @anthropic-ai/claude-code"
40
+
41
+ # detect_any_provider: true (0) if any supported provider CLI is on PATH.
42
+ # Extracted verbatim from the loki doctor detection loop (design 1.2).
43
+ detect_any_provider() {
44
+ local _dp
45
+ for _dp in claude codex cline aider; do
46
+ command -v "$_dp" >/dev/null 2>&1 && return 0
47
+ done
48
+ return 1
49
+ }
50
+
51
+ # _po_assume_yes: true when the user has opted into unattended confirmation.
52
+ # Honors --yes (LOKI_AUTO_CONFIRM, set by loki:1013) and LOKI_ASSUME_YES.
53
+ _po_assume_yes() {
54
+ [ "${LOKI_ASSUME_YES:-}" = "1" ] && return 0
55
+ [ "${LOKI_AUTO_CONFIRM:-}" = "true" ] && return 0
56
+ return 1
57
+ }
58
+
59
+ # _po_non_interactive: true when we must NEVER prompt (non-TTY or CI).
60
+ # Mirrors cmd_welcome_maybe_firstrun (loki:4286) and maybe_show_auto_plan.
61
+ _po_non_interactive() {
62
+ [ ! -t 1 ] && return 0
63
+ [ ! -t 0 ] && return 0
64
+ [ -n "${CI:-}" ] && return 0
65
+ return 1
66
+ }
67
+
68
+ # _po_run_login: offer (or auto-accept) the claude auth login handoff after a
69
+ # successful install. Inherited stdio; Loki never handles credentials.
70
+ _po_run_login() {
71
+ # claude must actually be on PATH for login to make sense.
72
+ if ! command -v claude >/dev/null 2>&1; then
73
+ printf "%sInstalled, but 'claude' is not on your PATH yet. You may need to restart your shell or add npm's global bin to PATH (npm config get prefix). Run 'loki doctor' to recheck.%s\n" "$_PO_YELLOW" "$_PO_NC"
74
+ return 0
75
+ fi
76
+
77
+ local do_login=""
78
+ if _po_assume_yes; then
79
+ do_login="y"
80
+ else
81
+ printf 'Claude Code installed.\n'
82
+ printf '\n'
83
+ printf 'You still need to authenticate. Run the login flow now? [Y/n] '
84
+ read -r do_login || do_login="n"
85
+ fi
86
+ case "$do_login" in
87
+ ""|y|Y|yes|YES)
88
+ if claude auth login; then
89
+ # Do not trust the exit code alone: verify the session is
90
+ # actually authenticated before claiming readiness (council
91
+ # HIGH: the old path could falsely report success).
92
+ if claude auth status 2>/dev/null | grep -q '"loggedIn"[[:space:]]*:[[:space:]]*true'; then
93
+ printf "%sProvider ready. Run 'loki doctor' to confirm, or 'loki quickstart' to build.%s\n" "$_PO_BOLD" "$_PO_NC"
94
+ return 0
95
+ fi
96
+ printf "Login finished but authentication could not be confirmed. Run 'claude auth status' to check, then 'loki doctor'.\n"
97
+ return 0
98
+ fi
99
+ printf "Login not completed. Run 'claude auth login' when ready, then 'loki doctor'.\n"
100
+ return 0
101
+ ;;
102
+ *)
103
+ printf "Login not completed. Run 'claude auth login' when ready, then 'loki doctor'.\n"
104
+ return 0
105
+ ;;
106
+ esac
107
+ }
108
+
109
+ # _po_do_install: run the one consented command, print it first, handle result.
110
+ # Returns 0 on success, non-zero on failure (caller decides exit behavior).
111
+ _po_do_install() {
112
+ printf 'Installing Claude Code (%s) ...\n' "$_PO_INSTALL_CMD"
113
+ # The exact, fixed argv. No interpolation, no extra flags. (design 1.7)
114
+ # Capture npm's exit code directly (not via `if`, whose statement status is
115
+ # 0 when the condition is false with no else, masking the real npm code).
116
+ local code=0
117
+ npm install -g @anthropic-ai/claude-code || code=$?
118
+ if [ "$code" -eq 0 ]; then
119
+ printf '\n'
120
+ _po_run_login
121
+ return 0
122
+ fi
123
+ printf '%sInstall failed (npm exited %s). You can retry manually:%s\n' "$_PO_RED" "$code" "$_PO_NC"
124
+ printf ' %s\n' "$_PO_INSTALL_CMD"
125
+ printf 'If this is a permissions error, see https://docs.npmjs.com/resolving-eacces-permissions-errors\n'
126
+ return "$code"
127
+ }
128
+
129
+ # offer_provider_install <mode>
130
+ # mode = "report" -> doctor: append the offer on a TTY; on non-TTY/CI do
131
+ # NOTHING (doctor already printed the FAIL + install line,
132
+ # and we must keep non-TTY/json bytes identical for parity).
133
+ # Never exits the process.
134
+ # mode = "gate" -> start/demo/quick pre-flight: on non-TTY/CI print the
135
+ # honest one-liner to stderr and return 2. On a TTY, prompt;
136
+ # on decline return 2. On accept install + login.
137
+ #
138
+ # Honors:
139
+ # LOKI_NO_INSTALL_OFFER=1 -> never prompt; print manual command (1.4)
140
+ # --yes / LOKI_ASSUME_YES -> auto-accept install + login (1.4)
141
+ offer_provider_install() {
142
+ local mode="${1:-gate}"
143
+
144
+ # Opt-out: never offer, just surface the manual command.
145
+ if [ "${LOKI_NO_INSTALL_OFFER:-}" = "1" ]; then
146
+ if [ "$mode" = "gate" ]; then
147
+ printf 'No AI provider CLI found. Install one when ready:\n' >&2
148
+ printf ' %s (then: claude auth login)\n' "$_PO_INSTALL_CMD" >&2
149
+ return 2
150
+ fi
151
+ printf '\n'
152
+ printf 'Install a provider when ready:\n'
153
+ printf ' %s (then: claude auth login)\n' "$_PO_INSTALL_CMD"
154
+ printf ' Other supported providers: codex, cline, aider.\n'
155
+ return 0
156
+ fi
157
+
158
+ # Non-interactive / CI: NEVER prompt, NEVER install.
159
+ #
160
+ # gate (start/demo/quick): print the honest one-liner to stderr and return 2
161
+ # so the caller exits with an actionable message before any spend.
162
+ # report (doctor): stay SILENT. doctor has already printed the FAIL line and
163
+ # the install command on stdout, so no information is lost. Silence here is
164
+ # load-bearing for parity: doctor.ts gates its child_process bridge on
165
+ # process.stdout.isTTY, so on a non-TTY/CI run the Bun route emits nothing
166
+ # extra. If report-mode printed a stderr line, the bash route would diverge
167
+ # from Bun in exactly the no-provider/non-TTY case the bun-parity matrix
168
+ # captures (2>&1) on CI runners, which have no provider installed.
169
+ if _po_non_interactive; then
170
+ if [ "$mode" = "gate" ]; then
171
+ printf 'No AI provider CLI found; cannot prompt to install in a non-interactive shell. Run: %s\n' "$_PO_INSTALL_CMD" >&2
172
+ return 2
173
+ fi
174
+ return 0
175
+ fi
176
+
177
+ # npm missing: degraded path, never attempt a non-npm install.
178
+ if ! command -v npm >/dev/null 2>&1; then
179
+ printf '\n'
180
+ printf '%sNo AI provider CLI was found, and npm is not installed either, so Loki%s\n' "$_PO_BOLD" "$_PO_NC"
181
+ printf 'cannot install one for you.\n'
182
+ printf '\n'
183
+ printf 'Install Node.js + npm first (https://nodejs.org), then run:\n'
184
+ printf ' %s\n' "$_PO_INSTALL_CMD"
185
+ printf ' claude auth login\n'
186
+ printf '\n'
187
+ printf "Already have a provider via another method? Make sure 'claude' (or codex,\n"
188
+ printf "cline, aider) is on your PATH, then run 'loki doctor'.\n"
189
+ [ "$mode" = "gate" ] && return 2
190
+ return 0
191
+ fi
192
+
193
+ # TTY, npm present: the interactive offer.
194
+ printf '\n'
195
+ printf 'No AI provider CLI was found. Loki needs one agent CLI to run a build.\n'
196
+ printf '\n'
197
+ printf 'Claude Code is the recommended provider (full feature support).\n'
198
+ printf ' Install: %s\n' "$_PO_INSTALL_CMD"
199
+ printf ' Then: claude auth login\n'
200
+ printf '\n'
201
+
202
+ local answer=""
203
+ if _po_assume_yes; then
204
+ answer="y"
205
+ else
206
+ printf 'Install Claude Code now? [Y/n] '
207
+ read -r answer || answer="n"
208
+ fi
209
+
210
+ case "$answer" in
211
+ ""|y|Y|yes|YES)
212
+ if _po_do_install; then
213
+ return 0
214
+ fi
215
+ # Install failed: honest failure already printed by _po_do_install.
216
+ [ "$mode" = "gate" ] && return 2
217
+ return 1
218
+ ;;
219
+ *)
220
+ printf 'Skipped. Install a provider when ready:\n'
221
+ printf ' %s (then: claude auth login)\n' "$_PO_INSTALL_CMD"
222
+ printf 'Other supported providers: codex, cline, aider.\n'
223
+ [ "$mode" = "gate" ] && return 2
224
+ return 0
225
+ ;;
226
+ esac
227
+ }
228
+
229
+ # provider_offer_gate: convenience wrapper for the start/demo/quick pre-flight.
230
+ # Returns 0 if a provider is present (or one was just installed); returns 2 to
231
+ # signal the caller should `exit 2` (no provider, declined or non-interactive).
232
+ provider_offer_gate() {
233
+ detect_any_provider && return 0
234
+ offer_provider_install gate || return 2
235
+ # After an accepted install, re-detect; if still absent, fail the gate.
236
+ detect_any_provider && return 0
237
+ return 2
238
+ }
239
+
240
+ # Executed directly (doctor.ts child_process bridge, or manual): run the offer.
241
+ # When sourced by autonomy/loki, this block does not run.
242
+ if [ "${BASH_SOURCE[0]}" = "$0" ]; then
243
+ case "${1:-report}" in
244
+ offer|report) offer_provider_install report ;;
245
+ gate) offer_provider_install gate ;;
246
+ detect) detect_any_provider ;;
247
+ *) offer_provider_install report ;;
248
+ esac
249
+ fi