loki-mode 7.71.0 → 7.72.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKILL.md 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, 8 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.71.0
6
+ # Loki Mode v7.72.0
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -406,4 +406,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
406
406
 
407
407
  ---
408
408
 
409
- **v7.71.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
409
+ **v7.72.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.71.0
1
+ 7.72.0
package/autonomy/loki CHANGED
@@ -5207,42 +5207,14 @@ cmd_web_status() {
5207
5207
  echo "Purple Lab is not running."
5208
5208
  }
5209
5209
 
5210
- # Open the running app preview (the app Loki built and started locally).
5211
- # Surfaces the existing app-runner state; does not start or change the app.
5212
- cmd_preview() {
5213
- local open_browser=true
5214
- case "${1:-}" in
5215
- --help|-h|help)
5216
- echo -e "${BOLD}Loki Mode -- open the running app preview${NC}"
5217
- echo ""
5218
- echo "Usage: loki preview [--no-open]"
5219
- echo " loki open (alias)"
5220
- echo ""
5221
- echo "Prints the URL of the app Loki built and started locally, then"
5222
- echo "opens it in your browser. The app runner starts the app after the"
5223
- echo "first successful build iteration. This serves a real local build"
5224
- echo "from localhost on your machine; it is not hosted."
5225
- echo ""
5226
- echo "Options:"
5227
- echo " --no-open Print the URL and status only; do not open a browser"
5228
- echo " --help, -h Show this help and exit"
5229
- return 0
5230
- ;;
5231
- --no-open)
5232
- open_browser=false
5233
- ;;
5234
- esac
5235
-
5236
- local state_file="${LOKI_DIR}/app-runner/state.json"
5237
- if [ ! -f "$state_file" ]; then
5238
- echo "No app running. The app runner starts after the first successful build iteration."
5239
- echo "Run 'loki status' to check the current run."
5240
- return 0
5241
- fi
5242
-
5243
- # Parse url/status/port. Prefer python3 (used throughout); fall back to grep.
5244
- # Pass the path as argv (not inline interpolation) so a path with quotes
5245
- # cannot break the script, and parse all three fields in one invocation.
5210
+ # Read app-runner state.json and echo url, status, port (one per line, in that
5211
+ # order). Shared by the plain preview browser path and the --public tunnel path
5212
+ # so there is no parse drift between them. Path is passed as argv (not inline
5213
+ # interpolation) so a path with quotes cannot break the script. Always exits 0;
5214
+ # callers re-split with sed -n '1p/2p/3p'. Logic is byte-identical to the
5215
+ # original inline parse in cmd_preview (do not "clean up" -- behavior-neutral).
5216
+ _read_app_state() {
5217
+ local state_file="$1"
5246
5218
  local url status port parsed
5247
5219
  if command -v python3 &> /dev/null; then
5248
5220
  parsed=$(python3 -c "import json,sys
@@ -5261,6 +5233,404 @@ except Exception:
5261
5233
  status=$(grep -oE '"status"[[:space:]]*:[[:space:]]*"[^"]*"' "$state_file" 2>/dev/null | head -1 | sed 's/.*"status"[[:space:]]*:[[:space:]]*"//;s/"$//')
5262
5234
  port=$(grep -oE '"port"[[:space:]]*:[[:space:]]*[0-9]+' "$state_file" 2>/dev/null | head -1 | grep -oE '[0-9]+$')
5263
5235
  fi
5236
+ printf '%s\n%s\n%s\n' "$url" "$status" "$port"
5237
+ }
5238
+
5239
+ # Pure extractor: pull the first cloudflared quick-tunnel URL out of a log file.
5240
+ # Reads a FILE (not a live process) so it is unit-testable. Echoes the URL or
5241
+ # nothing. Always returns 0 -- a no-match must not abort the caller under
5242
+ # `set -o pipefail` (grep exits 1 on no match, so the pipeline is `|| true`d).
5243
+ _extract_tunnel_url_cloudflared() {
5244
+ local logfile="${1:-}"
5245
+ [ -f "$logfile" ] || return 0
5246
+ grep -oE 'https://[a-z0-9-]+\.trycloudflare\.com' "$logfile" 2>/dev/null | head -1 || true
5247
+ }
5248
+
5249
+ # Pure extractor: pull the public_url out of an ngrok 4040 API JSON dump.
5250
+ # Reads a FILE so it is unit-testable. Prefers the https tunnel. Uses python3
5251
+ # json (path passed as argv -- no shell interpolation into the heredoc), with a
5252
+ # grep fallback if python3 is absent or the parse fails. Echoes URL or nothing;
5253
+ # always returns 0.
5254
+ _extract_tunnel_url_ngrok() {
5255
+ local json_file="${1:-}"
5256
+ [ -f "$json_file" ] || return 0
5257
+ local out=""
5258
+ if command -v python3 >/dev/null 2>&1; then
5259
+ out=$(python3 - "$json_file" <<'PYEXTRACT' 2>/dev/null || true
5260
+ import json, sys
5261
+ try:
5262
+ with open(sys.argv[1]) as fh:
5263
+ data = json.load(fh)
5264
+ except Exception:
5265
+ sys.exit(0)
5266
+ tunnels = data.get("tunnels", []) if isinstance(data, dict) else []
5267
+ urls = [t.get("public_url", "") for t in tunnels if isinstance(t, dict) and t.get("public_url")]
5268
+ https = [u for u in urls if u.startswith("https://")]
5269
+ chosen = https[0] if https else (urls[0] if urls else "")
5270
+ if chosen:
5271
+ print(chosen)
5272
+ PYEXTRACT
5273
+ )
5274
+ fi
5275
+ if [ -z "$out" ]; then
5276
+ # Grep fallback: take https public_url values first, else any public_url.
5277
+ out=$(grep -oE '"public_url"[[:space:]]*:[[:space:]]*"https://[^"]*"' "$json_file" 2>/dev/null | head -1 | grep -oE 'https://[^"]*' || true)
5278
+ if [ -z "$out" ]; then
5279
+ out=$(grep -oE '"public_url"[[:space:]]*:[[:space:]]*"[^"]*"' "$json_file" 2>/dev/null | head -1 | sed 's/.*"public_url"[[:space:]]*:[[:space:]]*"//;s/"$//' || true)
5280
+ fi
5281
+ fi
5282
+ printf '%s' "$out"
5283
+ }
5284
+
5285
+ # Teardown for a launched tunnel: TERM, brief grace, then KILL, then remove the
5286
+ # log. All steps are `|| true` so the EXIT/INT/TERM trap is set -e safe and never
5287
+ # leaves an orphaned public tunnel. Args: tunnel_pid [logfile].
5288
+ _preview_public_teardown() {
5289
+ local tunnel_pid="${1:-}"
5290
+ local logfile="${2:-}"
5291
+ if [ -n "$tunnel_pid" ]; then
5292
+ kill -TERM "$tunnel_pid" 2>/dev/null || true
5293
+ sleep 1
5294
+ kill -KILL "$tunnel_pid" 2>/dev/null || true
5295
+ fi
5296
+ if [ -n "$logfile" ]; then
5297
+ rm -f "$logfile" 2>/dev/null || true
5298
+ fi
5299
+ }
5300
+
5301
+ # Expose the already-running local app over a PUBLIC URL by wrapping the user's
5302
+ # OWN tunnel CLI (cloudflared or ngrok). Consent-gated, default-OFF. Loki never
5303
+ # proxies traffic and never bundles a binary. Args: provider yes no_host_rewrite.
5304
+ # Returns non-zero on any precondition failure or declined/refused consent
5305
+ # (except an interactive decline, which is a clean exit 0).
5306
+ _preview_public() {
5307
+ local provider="${1:-}"
5308
+ local assume_yes="${2:-false}"
5309
+ local no_host_rewrite="${3:-false}"
5310
+
5311
+ local state_file="${LOKI_DIR}/app-runner/state.json"
5312
+
5313
+ # Precondition 1: state.json must exist.
5314
+ if [ ! -f "$state_file" ]; then
5315
+ echo "No app running. Nothing to expose." >&2
5316
+ echo "Start a build (loki start) or check status (loki status) first." >&2
5317
+ return 1
5318
+ fi
5319
+
5320
+ # Read url/status/port via the shared helper (same parse as plain preview).
5321
+ local _state url status port
5322
+ _state=$(_read_app_state "$state_file")
5323
+ url=$(printf '%s\n' "$_state" | sed -n '1p')
5324
+ status=$(printf '%s\n' "$_state" | sed -n '2p')
5325
+ port=$(printf '%s\n' "$_state" | sed -n '3p')
5326
+
5327
+ # Precondition 2: status must be running.
5328
+ if [ "$status" != "running" ]; then
5329
+ echo "App is not running (status: ${status:-unknown}). Refusing to expose." >&2
5330
+ echo "The app runner starts the app after a successful build iteration." >&2
5331
+ return 1
5332
+ fi
5333
+
5334
+ # Precondition 3: resolve URL/port (fallback mirrors the plain path).
5335
+ if [ -z "$url" ]; then
5336
+ url="http://localhost:${port:-3000}"
5337
+ fi
5338
+ local probe_port="${port:-3000}"
5339
+
5340
+ # Precondition 4: PORT must actually be reachable. Never tunnel a dead port.
5341
+ # Reuses the curl-readiness poll pattern (autonomy/loki:4979).
5342
+ local retries=0
5343
+ local port_ready=false
5344
+ while [ $retries -lt 6 ]; do
5345
+ if curl -s "http://localhost:${probe_port}" > /dev/null 2>&1; then
5346
+ port_ready=true
5347
+ break
5348
+ fi
5349
+ sleep 0.5
5350
+ retries=$((retries + 1))
5351
+ done
5352
+ if [ "$port_ready" != true ]; then
5353
+ echo "Port ${probe_port} is not responding. Refusing to expose a dead port." >&2
5354
+ echo "Confirm the app is up locally (loki preview) before sharing it." >&2
5355
+ return 1
5356
+ fi
5357
+
5358
+ # Consent (default-OFF). Print the full warning every time (even with --yes).
5359
+ echo "WARNING: This makes the app running on THIS machine reachable by ANYONE who has"
5360
+ echo "the URL, over the public internet, using YOUR tunnel account."
5361
+ echo "- The app may have NO authentication. Anyone with the link can use it."
5362
+ echo "- Traffic flows through your own cloudflared/ngrok account, not through Loki."
5363
+ echo "- This stays up until you stop it. Stop it when you are done."
5364
+ echo ""
5365
+
5366
+ if [ "$assume_yes" = true ]; then
5367
+ : # --yes: warning printed above; skip the prompt.
5368
+ elif [ -t 0 ]; then
5369
+ # Interactive TTY. Default-N: only ^[Yy] proceeds. (Deliberately NOT the
5370
+ # default-Y idiom at :1894 -- public exposure is unsafe by default.)
5371
+ local confirm=""
5372
+ echo -e "Expose this app publicly? [y/N] \c"
5373
+ read -r confirm || true
5374
+ if [[ ! "$confirm" =~ ^[Yy] ]]; then
5375
+ echo "Aborted. App was not exposed."
5376
+ return 0
5377
+ fi
5378
+ else
5379
+ # Non-TTY without --yes: never silently expose.
5380
+ echo "Refusing to expose a public tunnel non-interactively without --yes." >&2
5381
+ return 1
5382
+ fi
5383
+
5384
+ # === SEAM (Agent B): provider detection + tunnel launch + URL extraction ===
5385
+
5386
+ # Provider detection (SS5). command -v based so a PATH stub works in tests.
5387
+ # If $provider was requested, require exactly that one; else prefer
5388
+ # cloudflared (no account needed for quick tunnels) then ngrok.
5389
+ local chosen_provider=""
5390
+ if [ -n "$provider" ]; then
5391
+ # Allowlist: only cloudflared/ngrok are supported. Without this guard an
5392
+ # arbitrary on-PATH binary name (e.g. --provider ls) would set
5393
+ # chosen_provider and fall through to the ngrok launch branch below.
5394
+ case "$provider" in
5395
+ cloudflared|ngrok) ;;
5396
+ *)
5397
+ echo -e "${RED}Unsupported tunnel provider: ${provider}${NC}" >&2
5398
+ echo "Supported providers: cloudflared, ngrok" >&2
5399
+ return 1
5400
+ ;;
5401
+ esac
5402
+ if command -v "$provider" >/dev/null 2>&1; then
5403
+ chosen_provider="$provider"
5404
+ fi
5405
+ else
5406
+ if command -v cloudflared >/dev/null 2>&1; then
5407
+ chosen_provider="cloudflared"
5408
+ elif command -v ngrok >/dev/null 2>&1; then
5409
+ chosen_provider="ngrok"
5410
+ fi
5411
+ fi
5412
+
5413
+ if [ -z "$chosen_provider" ]; then
5414
+ # Honest install hint (mirrors the gh-missing block). Never pretend
5415
+ # success, never download a binary. Loki wraps YOUR OWN client.
5416
+ if [ -n "$provider" ]; then
5417
+ echo -e "${RED}Requested tunnel provider not found: ${provider}${NC}" >&2
5418
+ else
5419
+ echo -e "${RED}No tunnel CLI found (cloudflared or ngrok)${NC}" >&2
5420
+ fi
5421
+ echo "" >&2
5422
+ echo "Loki wraps YOUR OWN tunnel client. It never downloads or bundles one." >&2
5423
+ echo "Install one of the following, then re-run:" >&2
5424
+ echo "" >&2
5425
+ echo " cloudflared (no account needed for quick tunnels):" >&2
5426
+ echo " brew install cloudflared # macOS" >&2
5427
+ echo " https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/ # all platforms" >&2
5428
+ echo "" >&2
5429
+ echo " ngrok (needs a free authtoken):" >&2
5430
+ echo " brew install ngrok # macOS" >&2
5431
+ echo " https://ngrok.com/download # all platforms" >&2
5432
+ return 1
5433
+ fi
5434
+
5435
+ # State dir, parallel to app-runner/.
5436
+ mkdir -p "${LOKI_DIR}/preview" 2>/dev/null || true
5437
+ local log_file="${LOKI_DIR}/preview/${chosen_provider}.log"
5438
+
5439
+ # Build the launch command (SS7/SS8). Host-header rewrite is default-ON to
5440
+ # fix the #1 dev-server "Invalid Host header" failure; --no-host-rewrite
5441
+ # opts out.
5442
+ local -a tunnel_cmd=()
5443
+ if [ "$chosen_provider" = "cloudflared" ]; then
5444
+ tunnel_cmd=(cloudflared tunnel --url "http://localhost:${probe_port}")
5445
+ if [ "$no_host_rewrite" != true ]; then
5446
+ tunnel_cmd+=(--http-host-header localhost)
5447
+ fi
5448
+ else
5449
+ # ngrok
5450
+ tunnel_cmd=(ngrok http "${probe_port}")
5451
+ if [ "$no_host_rewrite" != true ]; then
5452
+ tunnel_cmd+=(--host-header=rewrite)
5453
+ fi
5454
+ fi
5455
+
5456
+ echo ""
5457
+ echo "Starting ${chosen_provider} tunnel for http://localhost:${probe_port} ..."
5458
+
5459
+ # Launch redirected to a log, in the background; capture its pid.
5460
+ "${tunnel_cmd[@]}" > "$log_file" 2>&1 &
5461
+ local tunnel_pid=$!
5462
+
5463
+ # Install teardown immediately so no exit path leaves an orphaned tunnel.
5464
+ trap '_preview_public_teardown "$tunnel_pid" "$log_file"' EXIT INT TERM
5465
+
5466
+ # Poll for the public URL. cloudflared writes it to its log; ngrok exposes
5467
+ # it via the local 4040 API. Bounded loop (~20 x sleep 0.5 = ~10s). Also
5468
+ # detect early tunnel-process death so we fail honestly instead of hanging.
5469
+ local public_url=""
5470
+ local tries=0
5471
+ while [ $tries -lt 20 ]; do
5472
+ # Early death detection: if the tunnel process is gone, report and bail.
5473
+ if ! kill -0 "$tunnel_pid" 2>/dev/null; then
5474
+ echo -e "${RED}${chosen_provider} exited; see ${log_file}${NC}" >&2
5475
+ # Surface the tail of the log for context.
5476
+ tail -n 10 "$log_file" 2>/dev/null >&2 || true
5477
+ if [ "$chosen_provider" = "ngrok" ]; then
5478
+ echo "If this is an auth error, add your token: ngrok config add-authtoken <token>" >&2
5479
+ fi
5480
+ # The EXIT trap fires at SHELL exit, not function return, so reap the
5481
+ # process now and clear the trap. Keep the log for inspection (the
5482
+ # message points at it), so omit the log arg to teardown.
5483
+ _preview_public_teardown "$tunnel_pid"
5484
+ trap - EXIT INT TERM
5485
+ return 1
5486
+ fi
5487
+
5488
+ if [ "$chosen_provider" = "cloudflared" ]; then
5489
+ public_url=$(_extract_tunnel_url_cloudflared "$log_file")
5490
+ else
5491
+ local api_json="${LOKI_DIR}/preview/ngrok-api.json"
5492
+ if curl -s "http://127.0.0.1:4040/api/tunnels" -o "$api_json" 2>/dev/null; then
5493
+ public_url=$(_extract_tunnel_url_ngrok "$api_json")
5494
+ fi
5495
+ rm -f "$api_json" 2>/dev/null || true
5496
+ fi
5497
+
5498
+ if [ -n "$public_url" ]; then
5499
+ break
5500
+ fi
5501
+ sleep 0.5
5502
+ tries=$((tries + 1))
5503
+ done
5504
+
5505
+ if [ -z "$public_url" ]; then
5506
+ echo -e "${RED}Timed out waiting for a public URL from ${chosen_provider}.${NC}" >&2
5507
+ tail -n 10 "$log_file" 2>/dev/null >&2 || true
5508
+ if [ "$chosen_provider" = "ngrok" ]; then
5509
+ echo "ngrok may be missing an authtoken: ngrok config add-authtoken <token>" >&2
5510
+ fi
5511
+ # Reap now (the EXIT trap fires only at shell exit, not function return,
5512
+ # which would orphan the tunnel back in cmd_preview/main). Keep the log
5513
+ # for inspection, so omit the log arg.
5514
+ _preview_public_teardown "$tunnel_pid"
5515
+ trap - EXIT INT TERM
5516
+ return 1
5517
+ fi
5518
+
5519
+ # URL captured. Print it clearly and re-print the live warning.
5520
+ echo ""
5521
+ echo -e "${GREEN}Public preview URL:${NC} ${public_url}"
5522
+ echo ""
5523
+ echo "WARNING: This URL is live and reachable by ANYONE who has it, over the"
5524
+ echo "public internet, through YOUR ${chosen_provider} account. The app may have no"
5525
+ echo "authentication. This stays up until you stop it."
5526
+ echo ""
5527
+ echo "Press Ctrl+C to stop sharing."
5528
+
5529
+ # Foreground-block on the tunnel. Ctrl+C delivers INT -> trap -> teardown.
5530
+ # `wait` returns 130 on signal; guard so set -e does not abort the clean exit.
5531
+ wait "$tunnel_pid" 2>/dev/null || true
5532
+
5533
+ # Idempotent final teardown (the INT trap may already have run) + remove the
5534
+ # log on this success/Ctrl+C path. Clear the trap so it does not re-fire at
5535
+ # shell exit with out-of-scope locals.
5536
+ _preview_public_teardown "$tunnel_pid" "$log_file"
5537
+ trap - EXIT INT TERM
5538
+
5539
+ echo ""
5540
+ echo "Tunnel stopped."
5541
+ return 0
5542
+ }
5543
+
5544
+ # Open the running app preview (the app Loki built and started locally).
5545
+ # Surfaces the existing app-runner state; does not start or change the app.
5546
+ cmd_preview() {
5547
+ local open_browser=true
5548
+ local public=false
5549
+ local provider=""
5550
+ local assume_yes=false
5551
+ local no_host_rewrite=false
5552
+
5553
+ # Parse options. Loop so flag combos like `--public --yes` work; preserve
5554
+ # the existing leniency on unknown args (no hard error -> no behavior drift).
5555
+ while [ $# -gt 0 ]; do
5556
+ case "${1:-}" in
5557
+ --help|-h|help)
5558
+ echo -e "${BOLD}Loki Mode -- open the running app preview${NC}"
5559
+ echo ""
5560
+ echo "Usage: loki preview [--no-open]"
5561
+ echo " loki preview --public [--provider cloudflared|ngrok] [--yes] [--no-host-rewrite]"
5562
+ echo " loki open (alias)"
5563
+ echo ""
5564
+ echo "Prints the URL of the app Loki built and started locally, then"
5565
+ echo "opens it in your browser. The app runner starts the app after the"
5566
+ echo "first successful build iteration. This serves a real local build"
5567
+ echo "from localhost on your machine; it is not hosted."
5568
+ echo ""
5569
+ echo "Options:"
5570
+ echo " --no-open Print the URL and status only; do not open a browser"
5571
+ echo " --public Expose the running app over a PUBLIC URL via your own"
5572
+ echo " tunnel CLI (cloudflared or ngrok). Consent-gated,"
5573
+ echo " default-OFF. Loki never proxies traffic or bundles a"
5574
+ echo " binary; it wraps YOUR OWN tunnel client."
5575
+ echo " --provider NAME Tunnel provider for --public: cloudflared or ngrok"
5576
+ echo " (default: auto-detect, cloudflared first)"
5577
+ echo " --yes Skip the --public consent prompt (warning is still"
5578
+ echo " printed). Required to use --public non-interactively"
5579
+ echo " --no-host-rewrite Do not rewrite the Host header on the tunnel"
5580
+ echo " --help, -h Show this help and exit"
5581
+ echo ""
5582
+ echo "WARNING (--public): exposes THIS machine's app to ANYONE with the URL,"
5583
+ echo "over the public internet, through your own tunnel account. The app may"
5584
+ echo "have no authentication. It stays up until you stop it."
5585
+ return 0
5586
+ ;;
5587
+ --no-open)
5588
+ open_browser=false
5589
+ ;;
5590
+ --public)
5591
+ public=true
5592
+ ;;
5593
+ --provider)
5594
+ provider="${2:-}"
5595
+ # Guard the value-consuming shift: if --provider is the LAST arg
5596
+ # (no value), an unguarded shift here plus the loop's trailing
5597
+ # shift would underflow and abort under set -e. Only consume a
5598
+ # value when one is actually present.
5599
+ [ $# -ge 2 ] && shift
5600
+ ;;
5601
+ --yes)
5602
+ assume_yes=true
5603
+ ;;
5604
+ --no-host-rewrite)
5605
+ no_host_rewrite=true
5606
+ ;;
5607
+ esac
5608
+ shift
5609
+ done
5610
+
5611
+ # --public branches into the tunnel path BEFORE the browser-open logic.
5612
+ # Capture the exit code (set -e safe: a bare non-zero return on the call
5613
+ # line would trip set -e before `return` runs).
5614
+ if [ "$public" = true ]; then
5615
+ local rc=0
5616
+ _preview_public "$provider" "$assume_yes" "$no_host_rewrite" || rc=$?
5617
+ return $rc
5618
+ fi
5619
+
5620
+ local state_file="${LOKI_DIR}/app-runner/state.json"
5621
+ if [ ! -f "$state_file" ]; then
5622
+ echo "No app running. The app runner starts after the first successful build iteration."
5623
+ echo "Run 'loki status' to check the current run."
5624
+ return 0
5625
+ fi
5626
+
5627
+ # Parse url/status/port via the shared helper, then re-split (same logic the
5628
+ # inline parse used; behavior-neutral for the plain preview path).
5629
+ local url status port _state
5630
+ _state=$(_read_app_state "$state_file")
5631
+ url=$(printf '%s\n' "$_state" | sed -n '1p')
5632
+ status=$(printf '%s\n' "$_state" | sed -n '2p')
5633
+ port=$(printf '%s\n' "$_state" | sed -n '3p')
5264
5634
 
5265
5635
  if [ "$status" != "running" ]; then
5266
5636
  echo "App is not running (status: ${status:-unknown})."
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.71.0"
10
+ __version__ = "7.72.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v7.71.0
5
+ **Version:** v7.72.0
6
6
 
7
7
  ---
8
8
 
@@ -395,7 +395,7 @@ provider works inside the container. Provide auth with your Anthropic API key:
395
395
  # Run Loki Mode in Docker (Claude provider, API-key auth)
396
396
  docker run --rm -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
397
397
  -v $(pwd):/workspace -w /workspace \
398
- asklokesh/loki-mode:7.71.0 start ./my-spec.md
398
+ asklokesh/loki-mode:7.72.0 start ./my-spec.md
399
399
  ```
400
400
 
401
401
  ##### docker compose + .env (no host install)
@@ -0,0 +1,77 @@
1
+ # PREVIEW-LINK-PLAN.md -- Public Preview Link (BYO-tunnel)
2
+
3
+ ## Product Owner scope locks (decided 2026-06-18)
4
+ 1. Command surface: `loki preview --public` (a flag on the existing command, NOT a new top-level command and NOT `loki share preview` which is a deprecated alias to report-gist). Respects the CLI-consolidation mandate.
5
+ 2. Lifecycle: FOREGROUND-blocking with a trap teardown + "Press Ctrl+C to stop sharing." (Safer than background+pidfile: no orphaned public tunnel a user forgets about.)
6
+ 3. Default provider / detection order: cloudflared first (quick tunnels need NO account), ngrok second (needs authtoken). `--provider cloudflared|ngrok` override.
7
+ 4. Host-header rewrite: default-ON (cloudflared `--http-host-header localhost`, ngrok `--host-header=rewrite`) with a `--no-host-rewrite` escape. Fixes the #1 dev-server "Invalid Host header" failure.
8
+ 5. Bun parity: bash-only is acceptable for v7.72.0 (HUD precedent; the Bun runner is dormant for the live path). No loki-ts mirror required now.
9
+ 6. Consent: explicit, default-NO. Interactive `[y/N]` on a TTY (only ^[Yy] proceeds); `--yes` skips the prompt but still prints the warning; non-TTY without `--yes` REFUSES.
10
+
11
+ ## 1. Goal
12
+ Loki builds + runs the app locally and `loki preview` (cmd_preview, autonomy/loki:5212) opens it at http://localhost:PORT. There is no way to share the running app. Add a consent-gated `--public` path that creates a PUBLIC URL for the already-running local app by wrapping the USER'S OWN tunnel CLI (cloudflared or ngrok). The app + the user's creds stay on their machine; Loki never proxies traffic and never bundles/downloads a binary. Delivers the "share what was built" wow (Replit/Lovable/Bolt have it) without breaking "your keys, nothing leaves your network."
13
+
14
+ ## 2. Command surface + dispatch
15
+ `loki preview --public` (+ `--provider`, `--yes`, `--no-host-rewrite`). The `preview)` dispatch arm (autonomy/loki:14596 -> cmd_preview "$@") already forwards args; no dispatch-table change. Add the flags to cmd_preview's arg parser (~:5214); `--public` branches into a new `_preview_public` helper BEFORE the existing browser-open logic. Update cmd_preview --help (~:5216-5229).
16
+
17
+ ## 3. Precondition checks (REUSE cmd_preview state.json read)
18
+ Refactor the inline parse at autonomy/loki:5246-5263 into a shared `_read_app_state <state_file>` echoing url/status/port/primary_service; both the existing browser-open path and `--public` call it (no drift). Then, in order, each with honest degrade + non-zero exit:
19
+ 1. state.json exists (${LOKI_DIR}/app-runner/state.json) -> else "No app running. loki start / loki status".
20
+ 2. status == running (mirror :5265) -> else "App is not running (status: X)".
21
+ 3. URL/port resolved (fallback http://localhost:${port:-3000} per :5271-5273).
22
+ 4. PORT reachability (NEW): poll with the curl-readiness pattern at autonomy/loki:4979 (curl -s http://localhost:PORT >/dev/null, few retries, sleep 0.5, retries=$((retries+1))). Dead port -> "not exposing a dead port", non-zero. Never tunnel a dead port.
23
+
24
+ ## 4. Consent (load-bearing, default-OFF)
25
+ - Interactive TTY ([ -t 0 ]): print the full warning (SS9), prompt `Expose this app publicly? [y/N] ` via `read -r` (idiom at :1897-1902) but DEFAULT-N (only ^[Yy] proceeds; deliberately NOT the default-Y at :1894 -- public exposure is unsafe).
26
+ - --yes: skips the prompt; warning still PRINTED.
27
+ - Non-TTY without --yes: REFUSE ("Refusing to expose a public tunnel non-interactively without --yes"), non-zero. Never silently expose.
28
+
29
+ ## 5. BYO-CLI detection + install hint (command -v based, so a PATH stub works in CI)
30
+ Order (override with --provider): cloudflared, then ngrok, else honest install hint + non-zero. NEVER pretend success, NEVER download a binary. Hint mirrors the gh-missing block at :28304-28311; names brew + official URLs for both; states Loki wraps YOUR OWN client.
31
+
32
+ ## 6. URL extraction (pure, testable: read from file/string, not a live process)
33
+ - cloudflared (`cloudflared tunnel --url http://localhost:PORT [--http-host-header localhost]`): quick-tunnel URL prints to stderr/log. Redirect stdout+stderr to ${LOKI_DIR}/preview/cloudflared.log; poll (bounded ~20 x sleep 0.5, tries=$((tries+1))) `grep -oE 'https://[a-z0-9-]+\.trycloudflare\.com'` first match. Timeout no-match -> teardown + non-zero.
34
+ - ngrok (`ngrok http PORT [--host-header=rewrite]`): scrape the local API `curl -s http://127.0.0.1:4040/api/tunnels` -> .tunnels[].public_url (prefer https); python3 json parse, grep fallback; bounded poll. No authtoken -> 4040 never comes up -> honest "ngrok config add-authtoken" hint, non-zero.
35
+ Factor `_extract_tunnel_url_cloudflared <logfile>` and `_extract_tunnel_url_ngrok <json-file-or-string>` as PURE functions for the stub test.
36
+
37
+ ## 7. Lifecycle (foreground + trap, per lock #2)
38
+ - `tunnel_cmd ... > "$log" 2>&1 & tunnel_pid=$!`
39
+ - `trap '_preview_public_teardown "$tunnel_pid"' EXIT INT TERM` immediately. Teardown: `kill -TERM "$tunnel_pid" 2>/dev/null || true; sleep 1; kill -KILL "$tunnel_pid" 2>/dev/null || true` + remove log/pidfile (all || true, set -e safe).
40
+ - After URL capture: print public URL + live warning + "Press Ctrl+C to stop sharing." Then `wait "$tunnel_pid"`. Ctrl+C -> trap -> clean teardown.
41
+ - State dir ${LOKI_DIR}/preview/ (mkdir -p), parallel to app-runner/.
42
+
43
+ ## 8. Host-header (default-ON, lock #4; escape --no-host-rewrite)
44
+ Dev servers (Vite/Next dev/webpack/Django ALLOWED_HOSTS) reject a tunneled Host: <random>.trycloudflare.com with "Invalid Host header". cloudflared `--http-host-header localhost`; ngrok `--host-header=rewrite`. Verify the exact flag against the installed CLI version at runtime; do not hardcode blindly. Document that production-style servers may still need the tunnel host added to their allowlist.
45
+
46
+ ## 9. Help + warning copy (honest, no fabricated safety claims)
47
+ Help appended to cmd_preview --help: --public, --provider, --yes, --no-host-rewrite (per SS2). Warning printed before the prompt every time:
48
+ ```
49
+ WARNING: This makes the app running on THIS machine reachable by ANYONE who has
50
+ the URL, over the public internet, using YOUR tunnel account.
51
+ - The app may have NO authentication. Anyone with the link can use it.
52
+ - Traffic flows through your own cloudflared/ngrok account, not through Loki.
53
+ - This stays up until you stop it. Stop it when you are done.
54
+ ```
55
+ No "secure"/"encrypted" claims beyond what the tunnel CLI itself provides.
56
+
57
+ ## 10. Degrade / error table (all set -e safe)
58
+ state.json absent / status!=running / dead port / no CLI / non-TTY-no-yes / URL-capture-timeout / ngrok-no-authtoken -> honest message + non-zero. Consent declined -> "Aborted. App was not exposed." exit 0. Ctrl+C -> trap teardown + "Tunnel stopped." exit 0.
59
+
60
+ ## 11. Test plan (no real tunnel in CI; FAKE binary on PATH + pure extractors)
61
+ 1. Consent: pipe `n` -> Aborted, no spawn; `y` (fake bin) -> proceeds; --yes skips prompt but prints warning; non-TTY no --yes -> refuse + non-zero.
62
+ 2. CLI-absent: PATH without cloudflared/ngrok -> install hint + non-zero, no download.
63
+ 3. URL extraction: cloudflared stub script prints a fixed trycloudflare URL to its log -> assert extractor returns it (+ a real-format log fixture); ngrok extractor against a fixture 4040 JSON -> assert public_url; empty log -> timeout path tears down + non-zero.
64
+ 4. Preconditions: missing state.json; status=building; unreachable port -> each right message + non-zero.
65
+ 5. Teardown: SIGINT to the fake-bin run -> child pid gone, log/pidfile cleaned, no orphan.
66
+ 6. set -e / lint: bash parity + shellcheck/local-ci over the new code (x=$((x+1)), escaped $ in python heredocs, path as argv).
67
+ Mirror the existing CLI test harness that covers cmd_preview/gist-share.
68
+
69
+ ## 12. Task list
70
+ Agent A (surface, consent, preconditions): extract _read_app_state from :5246-5263 + repoint the existing browser path (regression-test plain `loki preview`); add flags to arg parse; branch --public into _preview_public; preconditions incl port poll (:4979 pattern); consent (warning + default-N + non-TTY/--yes); update --help.
71
+ Agent B (detection, extraction, lifecycle): command -v detection + order + hint; pure _extract_tunnel_url_cloudflared / _extract_tunnel_url_ngrok; foreground launch + trap teardown; host-header flags + --no-host-rewrite.
72
+ Both: tests per SS11; local-ci/parity gate; v7.72.0 bump + changelog (integrator); no commit/push unless asked.
73
+
74
+ ## Critical files
75
+ - autonomy/loki (cmd_preview :5212; arg parse :5214; help :5216-5229; state read to extract :5246-5263; dispatch :14596; consent prompt :1897; nohup/pidfile :4964-4968; curl poll :4979; pgid teardown :2221)
76
+ - autonomy/app-runner.sh (state.json writer :104-135 -- url/status/port/primary_service source of truth)
77
+ - The bash test harness covering cmd_preview (mirror its PATH-stub + fixture style)
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- var t6=Object.defineProperty;var i6=($)=>$;function e6($,Q){this[$]=i6.bind(null,Q)}var h=($,Q)=>{for(var Z in Q)t6($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:e6.bind(Q,Z)})};var P=($,Q)=>()=>($&&(Q=$($=0)),Q);var q$=import.meta.require;var D1={};h(D1,{lokiDir:()=>j,homeLokiDir:()=>r$,findRepoRootForVersion:()=>s$,REPO_ROOT:()=>g});import{resolve as a,dirname as a$}from"path";import{fileURLToPath as $Q}from"url";import{existsSync as F$}from"fs";import{homedir as QQ}from"os";function ZQ(){let $=S1;for(let Q=0;Q<6;Q++){if(F$(a($,"VERSION"))&&F$(a($,"autonomy/run.sh")))return $;let Z=a$($);if(Z===$)break;$=Z}return a(S1,"..","..","..")}function s$($){let Q=$;for(let Z=0;Z<6;Z++){if(F$(a(Q,"VERSION"))&&F$(a(Q,"autonomy/run.sh")))return Q;let z=a$(Q);if(z===Q)break;Q=z}return a($,"..","..","..")}function j(){return process.env.LOKI_DIR??a(process.cwd(),".loki")}function r$(){return a(QQ(),".loki")}var S1,g;var b=P(()=>{S1=a$($Q(import.meta.url));g=ZQ()});import{readFileSync as zQ}from"fs";import{resolve as XQ,dirname as KQ}from"path";import{fileURLToPath as qQ}from"url";function R$(){if(Q$!==null)return Q$;let $="7.71.0";if(typeof $==="string"&&$.length>0)return Q$=$,Q$;try{let Q=KQ(qQ(import.meta.url)),Z=s$(Q);Q$=zQ(XQ(Z,"VERSION"),"utf-8").trim()}catch{Q$="unknown"}return Q$}var Q$=null;var t$=P(()=>{b()});var b1={};h(b1,{runOrThrow:()=>VQ,run:()=>k,commandVersion:()=>WQ,commandExists:()=>f,ShellError:()=>i$});async function k($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,X;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[q,K,W]=await Promise.all([new Response(Z.stdout).text(),new Response(Z.stderr).text(),Z.exited]);return{stdout:q,stderr:K,exitCode:W}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function VQ($,Q={}){let Z=await k($,Q);if(Z.exitCode!==0)throw new i$(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=JQ($),Z=await k(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function JQ($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function WQ($,Q="--version"){if(!await f($))return null;let z=await k([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var i$;var d=P(()=>{i$=class i$ extends Error{message;exitCode;stdout;stderr;constructor($,Q,Z,z){super($);this.message=$;this.exitCode=Q;this.stdout=Z;this.stderr=z;this.name="ShellError"}}});function s($){return UQ?"":$}var UQ,T,S,_,_Z,I,x,y,V;var c=P(()=>{UQ=(process.env.NO_COLOR??"").length>0;T=s("\x1B[0;31m"),S=s("\x1B[0;32m"),_=s("\x1B[1;33m"),_Z=s("\x1B[0;34m"),I=s("\x1B[0;36m"),x=s("\x1B[1m"),y=s("\x1B[2m"),V=s("\x1B[0m")});import{existsSync as _Q}from"fs";async function Z$(){if(Y$!==void 0)return Y$;let $="/opt/homebrew/bin/python3.12";if(_Q($))return Y$=$,$;let Q=await f("python3.12");if(Q)return Y$=Q,Q;let Z=await f("python3");return Y$=Z,Z}async function z$($,Q={}){let Z=await Z$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return k([Z,"-c",$],Q)}var Y$;var V$=P(()=>{d()});var e1={};h(e1,{runStatus:()=>cQ});import{existsSync as v,readFileSync as W$,readdirSync as d1,statSync as o1}from"fs";import{resolve as D,basename as CQ}from"path";import{homedir as bQ}from"os";function n1($){let Q=Math.trunc($);if(Q>=1e6)return`${(Math.trunc(Q/1e6*10)/10).toFixed(1)}M`;if(Q>=1000)return`${(Math.trunc(Q/1000*10)/10).toFixed(1)}K`;return String(Q)}function a1($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*x$/Q);if(X>x$)X=x$;let q=x$-X,K=S;if(z>=80)K=T;else if(z>=50)K=_;let W="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),J=n1($),U=n1(Q);return` ${x}${Z}${V} ${K}[${W}]${V} ${z}% (${J} / ${U})`}async function yQ(){if(await f("jq"))return!0;return process.stdout.write(`${T}Error: jq is required but not installed.${V}
2
+ var t6=Object.defineProperty;var i6=($)=>$;function e6($,Q){this[$]=i6.bind(null,Q)}var h=($,Q)=>{for(var Z in Q)t6($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:e6.bind(Q,Z)})};var P=($,Q)=>()=>($&&(Q=$($=0)),Q);var q$=import.meta.require;var D1={};h(D1,{lokiDir:()=>j,homeLokiDir:()=>r$,findRepoRootForVersion:()=>s$,REPO_ROOT:()=>g});import{resolve as a,dirname as a$}from"path";import{fileURLToPath as $Q}from"url";import{existsSync as F$}from"fs";import{homedir as QQ}from"os";function ZQ(){let $=S1;for(let Q=0;Q<6;Q++){if(F$(a($,"VERSION"))&&F$(a($,"autonomy/run.sh")))return $;let Z=a$($);if(Z===$)break;$=Z}return a(S1,"..","..","..")}function s$($){let Q=$;for(let Z=0;Z<6;Z++){if(F$(a(Q,"VERSION"))&&F$(a(Q,"autonomy/run.sh")))return Q;let z=a$(Q);if(z===Q)break;Q=z}return a($,"..","..","..")}function j(){return process.env.LOKI_DIR??a(process.cwd(),".loki")}function r$(){return a(QQ(),".loki")}var S1,g;var b=P(()=>{S1=a$($Q(import.meta.url));g=ZQ()});import{readFileSync as zQ}from"fs";import{resolve as XQ,dirname as KQ}from"path";import{fileURLToPath as qQ}from"url";function R$(){if(Q$!==null)return Q$;let $="7.72.0";if(typeof $==="string"&&$.length>0)return Q$=$,Q$;try{let Q=KQ(qQ(import.meta.url)),Z=s$(Q);Q$=zQ(XQ(Z,"VERSION"),"utf-8").trim()}catch{Q$="unknown"}return Q$}var Q$=null;var t$=P(()=>{b()});var b1={};h(b1,{runOrThrow:()=>VQ,run:()=>k,commandVersion:()=>WQ,commandExists:()=>f,ShellError:()=>i$});async function k($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,X;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[q,K,W]=await Promise.all([new Response(Z.stdout).text(),new Response(Z.stderr).text(),Z.exited]);return{stdout:q,stderr:K,exitCode:W}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function VQ($,Q={}){let Z=await k($,Q);if(Z.exitCode!==0)throw new i$(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=JQ($),Z=await k(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function JQ($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function WQ($,Q="--version"){if(!await f($))return null;let z=await k([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var i$;var d=P(()=>{i$=class i$ extends Error{message;exitCode;stdout;stderr;constructor($,Q,Z,z){super($);this.message=$;this.exitCode=Q;this.stdout=Z;this.stderr=z;this.name="ShellError"}}});function s($){return UQ?"":$}var UQ,T,S,_,_Z,I,x,y,V;var c=P(()=>{UQ=(process.env.NO_COLOR??"").length>0;T=s("\x1B[0;31m"),S=s("\x1B[0;32m"),_=s("\x1B[1;33m"),_Z=s("\x1B[0;34m"),I=s("\x1B[0;36m"),x=s("\x1B[1m"),y=s("\x1B[2m"),V=s("\x1B[0m")});import{existsSync as _Q}from"fs";async function Z$(){if(Y$!==void 0)return Y$;let $="/opt/homebrew/bin/python3.12";if(_Q($))return Y$=$,$;let Q=await f("python3.12");if(Q)return Y$=Q,Q;let Z=await f("python3");return Y$=Z,Z}async function z$($,Q={}){let Z=await Z$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return k([Z,"-c",$],Q)}var Y$;var V$=P(()=>{d()});var e1={};h(e1,{runStatus:()=>cQ});import{existsSync as v,readFileSync as W$,readdirSync as d1,statSync as o1}from"fs";import{resolve as D,basename as CQ}from"path";import{homedir as bQ}from"os";function n1($){let Q=Math.trunc($);if(Q>=1e6)return`${(Math.trunc(Q/1e6*10)/10).toFixed(1)}M`;if(Q>=1000)return`${(Math.trunc(Q/1000*10)/10).toFixed(1)}K`;return String(Q)}function a1($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*x$/Q);if(X>x$)X=x$;let q=x$-X,K=S;if(z>=80)K=T;else if(z>=50)K=_;let W="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),J=n1($),U=n1(Q);return` ${x}${Z}${V} ${K}[${W}]${V} ${z}% (${J} / ${U})`}async function yQ(){if(await f("jq"))return!0;return process.stdout.write(`${T}Error: jq is required but not installed.${V}
3
3
  `),process.stdout.write(`Install with:
4
4
  `),process.stdout.write(` brew install jq (macOS)
5
5
  `),process.stdout.write(` apt install jq (Debian/Ubuntu)
@@ -793,4 +793,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
793
793
  `),2}default:return process.stderr.write(`Unknown command: ${Q}
794
794
  `),process.stderr.write(r6),2}}l1();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var qZ=await KZ(Bun.argv.slice(2));process.exit(qZ);
795
795
 
796
- //# debugId=BBA47536FE0B2AB264756E2164756E21
796
+ //# debugId=7A89607799C5087964756E2164756E21
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '7.71.0'
60
+ __version__ = '7.72.0'
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "loki-mode",
3
3
  "mcpName": "io.github.asklokesh/loki-mode",
4
- "version": "7.71.0",
4
+ "version": "7.72.0",
5
5
  "description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 8 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
6
6
  "keywords": [
7
7
  "agent",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "loki-mode",
4
4
  "displayName": "Loki Mode",
5
- "version": "7.71.0",
5
+ "version": "7.72.0",
6
6
  "description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 8 quality gates, completion council). Ships Loki's spec-hardening, drift-detection, and deterministic PR verification commands plus the Loki MCP server.",
7
7
  "author": {
8
8
  "name": "Autonomi",