loki-mode 7.70.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 +2 -2
- package/VERSION +1 -1
- package/autonomy/loki +406 -36
- package/autonomy/run.sh +143 -0
- package/dashboard/__init__.py +1 -1
- package/docs/BUILD-HUD-PLAN.md +90 -0
- package/docs/INSTALLATION.md +2 -2
- package/docs/PREVIEW-LINK-PLAN.md +77 -0
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
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.
|
|
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.
|
|
409
|
+
**v7.72.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.
|
|
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
|
-
#
|
|
5211
|
-
#
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
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})."
|
package/autonomy/run.sh
CHANGED
|
@@ -1005,6 +1005,129 @@ log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
|
|
|
1005
1005
|
log_step() { echo -e "${CYAN}[STEP]${NC} $*"; }
|
|
1006
1006
|
log_debug() { [[ "${LOKI_DEBUG:-}" == "true" ]] && echo -e "${CYAN}[DEBUG]${NC} $*" >&2 || true; }
|
|
1007
1007
|
|
|
1008
|
+
# Live Build HUD (v7.71.0): a single append-only per-iteration status line on the
|
|
1009
|
+
# interactive TTY path. Pure additive stdout decoration -- never piped into any
|
|
1010
|
+
# tee, so the dashboard agent.log and the stream-json parser are untouched. The
|
|
1011
|
+
# whole function is structured so any internal failure still returns 0; it can
|
|
1012
|
+
# never abort the iteration loop. Gated: TTY + not background + LOKI_HUD != 0.
|
|
1013
|
+
# Usage: render_build_hud <iter> <phase> <duration_secs>
|
|
1014
|
+
render_build_hud() {
|
|
1015
|
+
# Gate first: emit nothing unless interactive TTY, not --bg, and not opted out.
|
|
1016
|
+
# Inverted into an early return so the off-TTY path is byte-identical (no output).
|
|
1017
|
+
if ! { [ -t 1 ] && [ "${BACKGROUND_MODE:-false}" != "true" ] && [ "${LOKI_HUD:-1}" != "0" ]; }; then
|
|
1018
|
+
return 0
|
|
1019
|
+
fi
|
|
1020
|
+
|
|
1021
|
+
local _iter="${1:-0}" _phase="${2:-?}" _dur="${3:-0}"
|
|
1022
|
+
[ -z "$_phase" ] && _phase="?"
|
|
1023
|
+
# Sanitize numerics so set -u arithmetic/format below can never error out.
|
|
1024
|
+
case "$_dur" in (*[!0-9]*|'') _dur=0 ;; esac
|
|
1025
|
+
local _max="${MAX_ITERATIONS:-0}"
|
|
1026
|
+
case "$_max" in (*[!0-9]*|'') _max=0 ;; esac
|
|
1027
|
+
|
|
1028
|
+
# Field list: each field is appended only when its data is present. Missing
|
|
1029
|
+
# data => the field is omitted entirely (never a fabricated value).
|
|
1030
|
+
local _fields="$_phase"
|
|
1031
|
+
_fields="${_fields} | iter ${_iter}/${_max}"
|
|
1032
|
+
|
|
1033
|
+
# Cost from the context tracker totals (the same field the dashboard shows).
|
|
1034
|
+
# NOTE: tracking.json totals accumulate across runs in the same dir (not reset
|
|
1035
|
+
# on a fresh `loki start`), so this is cumulative cost for the project dir, not
|
|
1036
|
+
# strictly this single run. Labeled plainly "$" / "cost" to avoid overclaiming.
|
|
1037
|
+
# OMIT entirely when empty/missing/non-Claude.
|
|
1038
|
+
local _cost=""
|
|
1039
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
1040
|
+
_cost="$(python3 -c "
|
|
1041
|
+
import json
|
|
1042
|
+
try:
|
|
1043
|
+
t = json.load(open('.loki/context/tracking.json'))
|
|
1044
|
+
c = t.get('totals', {}).get('total_cost_usd', 0)
|
|
1045
|
+
c = float(c)
|
|
1046
|
+
if c > 0:
|
|
1047
|
+
print('%.2f' % c)
|
|
1048
|
+
except Exception:
|
|
1049
|
+
pass
|
|
1050
|
+
" 2>/dev/null || true)"
|
|
1051
|
+
fi
|
|
1052
|
+
[ -n "$_cost" ] && _fields="${_fields} | \$${_cost}"
|
|
1053
|
+
|
|
1054
|
+
# Files changed (+ins/-del and file count) vs the run start SHA. Reuse the
|
|
1055
|
+
# build_completion_summary diff approach incl. the .loki/.git exclude pathspec.
|
|
1056
|
+
# OMIT when no start sha, not a git repo, or no diff data.
|
|
1057
|
+
local _start_sha="${_LOKI_RUN_START_SHA:-}"
|
|
1058
|
+
if [ -n "$_start_sha" ]; then
|
|
1059
|
+
local _shortstat _ins _del _files
|
|
1060
|
+
_shortstat="$( (cd "${TARGET_DIR:-.}" && git diff --shortstat "${_start_sha}..HEAD" -- . ':(exclude).loki/' ':(exclude).git/' ':(exclude)**/.loki/**') 2>/dev/null || true )"
|
|
1061
|
+
if [ -n "$_shortstat" ]; then
|
|
1062
|
+
_files="$(printf '%s\n' "$_shortstat" | grep -oE '[0-9]+ file' | grep -oE '[0-9]+' | head -1 2>/dev/null || true)"
|
|
1063
|
+
_ins="$(printf '%s\n' "$_shortstat" | grep -oE '[0-9]+ insertion' | grep -oE '[0-9]+' | head -1 2>/dev/null || true)"
|
|
1064
|
+
_del="$(printf '%s\n' "$_shortstat" | grep -oE '[0-9]+ deletion' | grep -oE '[0-9]+' | head -1 2>/dev/null || true)"
|
|
1065
|
+
[ -z "$_files" ] && _files=0
|
|
1066
|
+
[ -z "$_ins" ] && _ins=0
|
|
1067
|
+
[ -z "$_del" ] && _del=0
|
|
1068
|
+
_fields="${_fields} | +${_ins}/-${_del} (${_files} files)"
|
|
1069
|
+
fi
|
|
1070
|
+
fi
|
|
1071
|
+
|
|
1072
|
+
# Per-iteration time (the duration arg). Labeled "took" (not "iter") so it
|
|
1073
|
+
# does not collide with the "iter N/max" iteration-count field above.
|
|
1074
|
+
_fields="${_fields} | took $(_hud_fmt_secs "$_dur")"
|
|
1075
|
+
|
|
1076
|
+
# Elapsed time for the whole run, from the once-captured run-start epoch.
|
|
1077
|
+
local _start_epoch="${_LOKI_RUN_START_EPOCH:-}"
|
|
1078
|
+
case "$_start_epoch" in (*[!0-9]*|'') _start_epoch="" ;; esac
|
|
1079
|
+
if [ -n "$_start_epoch" ]; then
|
|
1080
|
+
local _now _elapsed
|
|
1081
|
+
_now="$(date +%s 2>/dev/null || true)"
|
|
1082
|
+
case "$_now" in (*[!0-9]*|'') _now="" ;; esac
|
|
1083
|
+
if [ -n "$_now" ] && [ "$_now" -ge "$_start_epoch" ] 2>/dev/null; then
|
|
1084
|
+
_elapsed=$(( _now - _start_epoch ))
|
|
1085
|
+
_fields="${_fields} | elapsed $(_hud_fmt_secs "$_elapsed")"
|
|
1086
|
+
fi
|
|
1087
|
+
fi
|
|
1088
|
+
|
|
1089
|
+
# ETA (PO lock #1): omit by default. Only a SMALL user-set LOKI_MAX_ITERATIONS
|
|
1090
|
+
# (not the 1000 default) yields a meaningful target, so that is the single ETA
|
|
1091
|
+
# trigger here. A LOKI_BUDGET_LIMIT cap is also an allowed trigger per the
|
|
1092
|
+
# plan, but a budget-derived ETA needs cost-rate math that is easy to get
|
|
1093
|
+
# wrong; per "keep it simple and safe, when unsure omit", budget ETA is left
|
|
1094
|
+
# out rather than rendered approximately.
|
|
1095
|
+
local _lmi="${LOKI_MAX_ITERATIONS:-}"
|
|
1096
|
+
case "$_lmi" in (*[!0-9]*|'') _lmi="" ;; esac
|
|
1097
|
+
if [ -n "$_lmi" ] && [ "$_lmi" -gt 0 ] 2>/dev/null && [ "$_lmi" -lt 1000 ] 2>/dev/null \
|
|
1098
|
+
&& [ "$_iter" -gt 0 ] 2>/dev/null && [ "$_iter" -lt "$_lmi" ] 2>/dev/null \
|
|
1099
|
+
&& [ -n "$_start_epoch" ]; then
|
|
1100
|
+
local _now2 _el2 _per _remain_iters _eta
|
|
1101
|
+
_now2="$(date +%s 2>/dev/null || true)"
|
|
1102
|
+
case "$_now2" in (*[!0-9]*|'') _now2="" ;; esac
|
|
1103
|
+
if [ -n "$_now2" ] && [ "$_now2" -ge "$_start_epoch" ] 2>/dev/null; then
|
|
1104
|
+
_el2=$(( _now2 - _start_epoch ))
|
|
1105
|
+
_per=$(( _el2 / _iter ))
|
|
1106
|
+
_remain_iters=$(( _lmi - _iter ))
|
|
1107
|
+
_eta=$(( _per * _remain_iters ))
|
|
1108
|
+
[ "$_eta" -gt 0 ] 2>/dev/null && _fields="${_fields} | eta ~$(_hud_fmt_secs "$_eta")"
|
|
1109
|
+
fi
|
|
1110
|
+
fi
|
|
1111
|
+
|
|
1112
|
+
echo -e "${CYAN}[HUD]${NC} ${_fields}"
|
|
1113
|
+
return 0
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
# Format a whole-seconds integer as a compact duration: 37s, 2m11s, 1h03m.
|
|
1117
|
+
# Best-effort and set -u safe; any odd input degrades to "0s".
|
|
1118
|
+
_hud_fmt_secs() {
|
|
1119
|
+
local _s="${1:-0}"
|
|
1120
|
+
case "$_s" in (*[!0-9]*|'') _s=0 ;; esac
|
|
1121
|
+
if [ "$_s" -lt 60 ] 2>/dev/null; then
|
|
1122
|
+
printf '%ds' "$_s"
|
|
1123
|
+
elif [ "$_s" -lt 3600 ] 2>/dev/null; then
|
|
1124
|
+
printf '%dm%02ds' "$(( _s / 60 ))" "$(( _s % 60 ))"
|
|
1125
|
+
else
|
|
1126
|
+
printf '%dh%02dm' "$(( _s / 3600 ))" "$(( (_s % 3600) / 60 ))"
|
|
1127
|
+
fi
|
|
1128
|
+
return 0
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1008
1131
|
#===============================================================================
|
|
1009
1132
|
# Process Registry (PID Supervisor)
|
|
1010
1133
|
# Central registry of all spawned child processes for reliable cleanup
|
|
@@ -14156,6 +14279,13 @@ except Exception:
|
|
|
14156
14279
|
_LOKI_RUN_START_SHA="$(cat "$_start_sha_file" 2>/dev/null || echo "")"
|
|
14157
14280
|
export _LOKI_RUN_START_SHA
|
|
14158
14281
|
|
|
14282
|
+
# Live Build HUD (v7.71.0): capture the run-start epoch ONCE for the elapsed
|
|
14283
|
+
# field. Pure assignment (`:` builtin), emits no output, so the non-TTY/CI/Bun
|
|
14284
|
+
# parity surface is byte-identical. := preserves a value across a resume only
|
|
14285
|
+
# within the same process; a fresh run stamps now.
|
|
14286
|
+
: "${_LOKI_RUN_START_EPOCH:=$(date +%s)}"
|
|
14287
|
+
export _LOKI_RUN_START_EPOCH
|
|
14288
|
+
|
|
14159
14289
|
# Session-scope the mid-flight model override (model-honesty fix). The
|
|
14160
14290
|
# override file (.loki/state/model-override) is a LIVE-RUN control: the
|
|
14161
14291
|
# dashboard UI and docs state it "applies to the current run". A leftover
|
|
@@ -15779,6 +15909,13 @@ else:
|
|
|
15779
15909
|
if [ $exit_code -eq 0 ]; then
|
|
15780
15910
|
# Episode trace already captured by auto_capture_episode above (v6.15.0)
|
|
15781
15911
|
|
|
15912
|
+
# Live Build HUD (v7.71.0): one append-only status line per successful
|
|
15913
|
+
# iteration. At the top of the success branch so it fires for ALL
|
|
15914
|
+
# success sub-paths (incl. perpetual mode / completion). TTY-gated and
|
|
15915
|
+
# `|| true` so it can never abort the loop. Never tee'd -> dashboard
|
|
15916
|
+
# agent.log + stream parser untouched.
|
|
15917
|
+
render_build_hud "${ITERATION_COUNT:-0}" "${rarv_phase:-?}" "${duration:-0}" || true
|
|
15918
|
+
|
|
15782
15919
|
# Perpetual mode: NEVER stop, always continue
|
|
15783
15920
|
if [ "$PERPETUAL_MODE" = "true" ]; then
|
|
15784
15921
|
log_info "Perpetual mode: Ignoring exit, continuing immediately..."
|
|
@@ -16034,6 +16171,12 @@ else:
|
|
|
16034
16171
|
# Only apply retry logic for ERRORS (non-zero exit code)
|
|
16035
16172
|
# Episode trace already captured by auto_capture_episode above (v6.15.0)
|
|
16036
16173
|
|
|
16174
|
+
# Live Build HUD (v7.71.0): a failing iteration still shows motion (lock
|
|
16175
|
+
# #3). At the top of the failure fall-through so it fires for ALL failure
|
|
16176
|
+
# sub-paths, incl. the rate-limit/failover branch that `continue`s before
|
|
16177
|
+
# the "Will retry" log_warn below. TTY-gated, `|| true`, never tee'd.
|
|
16178
|
+
render_build_hud "${ITERATION_COUNT:-0}" "${rarv_phase:-?}" "${duration:-0}" || true
|
|
16179
|
+
|
|
16037
16180
|
# Checkpoint failed iteration state (v5.57.0)
|
|
16038
16181
|
create_checkpoint "iteration-${ITERATION_COUNT} failed (exit=$exit_code)" "iteration-${ITERATION_COUNT}-fail"
|
|
16039
16182
|
|
package/dashboard/__init__.py
CHANGED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# BUILD-HUD-PLAN.md -- In-Terminal Live Build HUD
|
|
2
|
+
|
|
3
|
+
## Product Owner scope locks (decided 2026-06-18)
|
|
4
|
+
1. ETA: OMIT unless an explicit small LOKI_MAX_ITERATIONS or a LOKI_BUDGET_LIMIT cap is present. Default behavior shows no ETA.
|
|
5
|
+
2. Phase wording: show the RARV phase name (REASON/ACT/REFLECT/VERIFY) -- that is what the live loop tracks. Do not add the planning/development tier word.
|
|
6
|
+
3. Failure-iteration render: YES, render the HUD on both the success and the retry/failure path (the "is it working" moment matters most when an iteration fails).
|
|
7
|
+
4. Opt-out var: LOKI_HUD (default 1 = on; LOKI_HUD=0 disables). TTY-gated.
|
|
8
|
+
|
|
9
|
+
## 1. Goal
|
|
10
|
+
When a user runs `loki start ./prd.md` in a foreground (interactive TTY) terminal, emit a single clean per-iteration status line at each iteration boundary showing: RARV phase, iteration N/max, cumulative cost, files changed (+/-) vs run start_sha, and per-iteration + elapsed time (ETA only when a real target exists per lock #1). Pure additive stdout decoration on the interactive path. Must not change non-TTY/CI/`--bg` output by a single byte, must not touch the stream-json parser, `log_step` lines, or the dashboard, and must degrade cleanly when cost data is unavailable.
|
|
11
|
+
|
|
12
|
+
## 2. Live route confirmation (re-verified)
|
|
13
|
+
- Orchestrator: `autonomy/run.sh`, `set -uo pipefail` (NOT `set -e`).
|
|
14
|
+
- Iteration loop body: provider dispatched after the RARV banner (`run.sh:14662`); `rarv_phase` set at `run.sh:14589` via `get_rarv_phase_name "$ITERATION_COUNT"` (in scope for the whole iteration body).
|
|
15
|
+
- `ITERATION_COUNT` = `run.sh:620` (init 0); `MAX_ITERATIONS` = `run.sh:619` (default `LOKI_MAX_ITERATIONS:-1000`).
|
|
16
|
+
- Provider exit + `duration=$((end_time - start_time))` computed at `run.sh:15279-15280`.
|
|
17
|
+
- Success path continues at `log_step "Starting next iteration..."` `run.sh:16026`.
|
|
18
|
+
- Bun runner `loki-ts/src/runner/autonomous.ts` `runAutonomous` is dormant for `loki start` (bash route is live).
|
|
19
|
+
|
|
20
|
+
## 3. Render point -- iteration END boundary (NOT start)
|
|
21
|
+
Render once per iteration, after the provider exits and cost/duration are available, before the loop continues. Insert a single call
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
render_build_hud "$ITERATION_COUNT" "$rarv_phase" "$duration" || true
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
immediately after `duration` is computed (`run.sh:15280`) and after the result-cost file for this iteration has been flushed by the embedded stream parser (~`run.sh:15206-15208`). Iteration start is wrong: current iteration cost/files do not exist yet. Per lock #3, also add a second guarded call on the failure/retry path (same region, after the exit-code log_warn).
|
|
28
|
+
|
|
29
|
+
## 4. Data sources (reuse only -- do not re-implement)
|
|
30
|
+
| Field | Source | Notes |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| Phase | `$rarv_phase` (already set `run.sh:14589`) | REASON/ACT/REFLECT/VERIFY. |
|
|
33
|
+
| Iteration N/max | `$ITERATION_COUNT` / `$MAX_ITERATIONS` | Already in scope. |
|
|
34
|
+
| Cumulative cost | `.loki/context/tracking.json` -> `totals.total_cost_usd` (read pattern at `run.sh:4531`) | Cumulative for the run. Do NOT re-sum result-cost-*.json. |
|
|
35
|
+
| Files changed (+/-) | `git diff --shortstat "${_LOKI_RUN_START_SHA}..HEAD"` with the exclude pathspec from `build_completion_summary` (`run.sh:2607-2618`) | Reuse `_LOKI_RUN_START_SHA` (exported `run.sh:14157`) + `':(exclude).loki/' ':(exclude).git/'`. |
|
|
36
|
+
| Per-iteration time | `$duration` (`run.sh:15280`) | Already computed. |
|
|
37
|
+
| Elapsed (run) | In-process epoch var captured once at loop entry (see SS6) | Parity-safe: var assignment emits no output. |
|
|
38
|
+
| ETA | Per lock #1: only when explicit small max or budget cap | Default: omit. |
|
|
39
|
+
|
|
40
|
+
## 5. Env / TTY gate
|
|
41
|
+
HUD renders only when ALL hold (first lines of `render_build_hud`, early `return 0` otherwise):
|
|
42
|
+
```
|
|
43
|
+
[ -t 1 ] && [ "${BACKGROUND_MODE:-false}" != "true" ] && [ "${LOKI_HUD:-1}" != "0" ]
|
|
44
|
+
```
|
|
45
|
+
- `[ -t 1 ]` is the established idiom (`run.sh:10241, 13929`).
|
|
46
|
+
- `BACKGROUND_MODE` guard mirrors the auto-open gate (`run.sh:10241`), covering `--bg`/`--detach`.
|
|
47
|
+
|
|
48
|
+
## 6. Rendering approach -- append-only clean line, never `\r`
|
|
49
|
+
The agent streams provider output to the same TTY during the iteration. A repainting `\r` bar would fight that stream. Therefore: a single append-only log line emitted only at the iteration boundary (after the stream for that iteration ended), styled like `log_step`/`log_info`. No carriage-return repaint, no cursor save/restore, no background thread. Distinct prefix `[HUD]` in `${CYAN}`/`${NC}` (`run.sh:1005`). Run-elapsed source: `: "${_LOKI_RUN_START_EPOCH:=$(date +%s)}"` placed once before the loop (pure assignment, parity-safe; do NOT stat start-sha mtime -- unreliable on resume).
|
|
50
|
+
|
|
51
|
+
Illustrative line:
|
|
52
|
+
```
|
|
53
|
+
[HUD] REASON | iter 3/1000 | $0.42 | +180/-24 (4 files) | took 37s | elapsed 2m11s
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## 7. set -uo pipefail safety + degrade
|
|
57
|
+
- Guard every var with defaults: `${ITERATION_COUNT:-0}`, `${MAX_ITERATIONS:-0}`, `${rarv_phase:-?}`, `${_LOKI_RUN_START_SHA:-}`, `${_LOKI_RUN_START_EPOCH:-}`.
|
|
58
|
+
- No `((x++))` from unset/zero; arithmetic only on integer-defaulted values.
|
|
59
|
+
- Every external read best-effort: `2>/dev/null || true`, matching existing helpers.
|
|
60
|
+
- Degrade: cost unavailable -> OMIT the cost field (never print $0.00 as real, never crash). files unavailable -> omit files field. The call site is `render_build_hud ... || true` so the helper can never abort the loop.
|
|
61
|
+
|
|
62
|
+
## 8. Byte-identical-off-TTY guarantee (two mechanisms)
|
|
63
|
+
1. Gate: helper returns before emitting unless TTY + not background + LOKI_HUD != 0.
|
|
64
|
+
2. stdout-only, never tee'd: HUD uses plain `echo`/`printf` to stdout, NEVER piped into `tee -a "$log_file" "$agent_log"` (`run.sh:14643, 14662`). The dashboard reads `.loki/logs/agent.log`; the HUD never enters any `tee -a`, so logs + dashboard are untouched by construction. The stream-json parser reads the provider's stdout pipe, not the orchestrator terminal stdout.
|
|
65
|
+
|
|
66
|
+
## 9. Parity argument (bash-only HUD is safe)
|
|
67
|
+
- `runAutonomous` is dormant for `loki start`; the bash loop is the live route -> no live parity gap.
|
|
68
|
+
- `tests/test-bash-bun-parity.sh` compares build_prompt output, PHASE_KEYS/SDLC list, the effort matrix, report/stats stdout -- NOT the autonomous loop terminal stdout. A loop HUD line is invisible to every parity invariant. No Bun mirror required (track a follow-up if runAutonomous becomes live).
|
|
69
|
+
|
|
70
|
+
## 10. Test plan (`tests/test-build-hud.sh`, wired into run-all-tests)
|
|
71
|
+
1. HUD appears on a TTY (pty harness): assert `[HUD]` + `iter 1/`; with a fake tracking.json assert cost field; remove it and assert line still appears without cost (degrade).
|
|
72
|
+
2. HUD absent off-TTY byte-identical: capture full stdout off-TTY with LOKI_HUD unset and with LOKI_HUD=0; assert `diff` clean (HUD adds nothing off-TTY).
|
|
73
|
+
3. Opt-out on TTY: TTY + LOKI_HUD=0 -> no `[HUD]`.
|
|
74
|
+
4. Dashboard/log untouched: after a stubbed TTY iteration, assert `.loki/logs/agent.log` has no `[HUD]` (not tee'd).
|
|
75
|
+
5. set -u safety: run helper under `bash -u` with all vars unset; assert exit 0, no unbound error.
|
|
76
|
+
6. Regression: `tests/test-bash-bun-parity.sh` still green unchanged.
|
|
77
|
+
|
|
78
|
+
## 11. Step-by-step task list
|
|
79
|
+
Agent A -- implementation (`autonomy/run.sh` only):
|
|
80
|
+
1. Add `render_build_hud()` near logging helpers (after `run.sh:1005`); first lines = SS5 gate + early `return 0`; body builds line from present fields only; stdout-only echo; `[HUD]` + CYAN/NC; wrapped so it can never abort the loop.
|
|
81
|
+
2. Cumulative-cost read reusing the tracking.json totals pattern (`run.sh:4531`), `2>/dev/null || true`.
|
|
82
|
+
3. Files-changed read reusing `git diff --shortstat "${_LOKI_RUN_START_SHA}..HEAD"` + exclude pathspec (`run.sh:2607-2618`).
|
|
83
|
+
4. `: "${_LOKI_RUN_START_EPOCH:=$(date +%s)}"` once before the loop.
|
|
84
|
+
5. Single call after `duration` at `run.sh:15280`; second guarded call on the failure path (lock #3).
|
|
85
|
+
6. Confirm NOT inside any `tee -a`; stdout only.
|
|
86
|
+
|
|
87
|
+
Agent B -- tests + docs:
|
|
88
|
+
7. Write `tests/test-build-hud.sh` (all 6 cases); wire into `tests/run-all-tests.sh`.
|
|
89
|
+
8. Document `LOKI_HUD` in `.env.example` + env reference docs.
|
|
90
|
+
9. Run the new test + parity test; confirm green + off-TTY byte-identical.
|
package/docs/INSTALLATION.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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)
|
package/loki-ts/dist/loki.js
CHANGED
|
@@ -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.
|
|
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=
|
|
796
|
+
//# debugId=7A89607799C5087964756E2164756E21
|
package/mcp/__init__.py
CHANGED
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.
|
|
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.
|
|
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",
|