loki-mode 7.125.0 → 7.127.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/lib/cockpit-render.sh +358 -0
- package/autonomy/loki +579 -1
- package/dashboard/__init__.py +1 -1
- package/dashboard/static/index.html +143 -94
- package/docs/COCKPIT-SPEC.md +70 -0
- package/docs/COCKPIT-UX-MANDATE.md +53 -0
- package/docs/INSTALLATION.md +2 -2
- package/loki-ts/dist/cockpit.js +19 -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/autonomy/loki
CHANGED
|
@@ -959,6 +959,7 @@ show_help() {
|
|
|
959
959
|
echo " loki config set maxTier sonnet # Cap model cost"
|
|
960
960
|
echo " loki dashboard start # Web dashboard at localhost:57374"
|
|
961
961
|
echo " loki watch # Auto-rerun on PRD changes"
|
|
962
|
+
echo " loki cockpit # Live multi-repo cockpit (terminal image)"
|
|
962
963
|
echo " loki remote # Remote session (phone/browser)"
|
|
963
964
|
echo ""
|
|
964
965
|
echo "Phase A-J features (v7.5.18 - v7.5.28) are default-on. See CHANGELOG."
|
|
@@ -1156,6 +1157,203 @@ detect_arg_type() {
|
|
|
1156
1157
|
echo "unknown"
|
|
1157
1158
|
}
|
|
1158
1159
|
|
|
1160
|
+
# --- loki start detach-default handoff (S1) --------------------------------
|
|
1161
|
+
# On an interactive TTY, `loki start` now detaches the build to the background
|
|
1162
|
+
# by default after a rich, reversible handoff card. A non-interactive shell,
|
|
1163
|
+
# --bg, --yes/-y, or CI takes the exact pre-existing foreground path (no card,
|
|
1164
|
+
# byte-identical behavior). See tests/test-start-handoff.sh.
|
|
1165
|
+
|
|
1166
|
+
# Decide whether the interactive handoff should fire. Returns 0 (fire) only when
|
|
1167
|
+
# stdout is a TTY, --bg was not requested, auto-confirm/-y is off, and we are not
|
|
1168
|
+
# in CI / a non-interactive shell. Any other case returns 1 (take today's path).
|
|
1169
|
+
# $1 = "1" if --bg is present in args, else "".
|
|
1170
|
+
_loki_start_should_handoff() {
|
|
1171
|
+
local bg_requested="${1:-}"
|
|
1172
|
+
[ "$bg_requested" = "1" ] && return 1
|
|
1173
|
+
[ -t 1 ] || return 1
|
|
1174
|
+
[ -t 0 ] || return 1
|
|
1175
|
+
[ "${LOKI_AUTO_CONFIRM:-}" = "true" ] && return 1
|
|
1176
|
+
[ "${CI:-}" = "true" ] && return 1
|
|
1177
|
+
[ "${LOKI_NO_HANDOFF:-}" = "1" ] && return 1
|
|
1178
|
+
return 0
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
# Stare-worthy brand banner (S6). Renders the real Autonomi logo (purple squircle
|
|
1182
|
+
# + white "A" + teal dot) as a crafted truecolor block, next to a "LOKI" wordmark,
|
|
1183
|
+
# straight to stderr. ZERO new tooling: pure ANSI escapes. Degrades by color depth
|
|
1184
|
+
# ($COLORTERM truecolor -> 24-bit; else a clean 256/mono badge). Never required,
|
|
1185
|
+
# always the default -- this is the moment the user stares at.
|
|
1186
|
+
_loki_brand_banner() {
|
|
1187
|
+
local proj="$1"
|
|
1188
|
+
local nc=$'\033[0m' b=$'\033[1m' dim=$'\033[2m'
|
|
1189
|
+
# Truecolor when the terminal advertises it (nearly all modern terminals).
|
|
1190
|
+
if [ "${COLORTERM:-}" = "truecolor" ] || [ "${COLORTERM:-}" = "24bit" ]; then
|
|
1191
|
+
local pB=$'\033[48;2;85;61;233m' # squircle purple bg #553DE9
|
|
1192
|
+
local wF=$'\033[38;2;255;254;251m' # white A
|
|
1193
|
+
local tF=$'\033[38;2;31;197;168m' # teal dot
|
|
1194
|
+
local pF=$'\033[38;2;123;107;240m' # light purple wordmark
|
|
1195
|
+
# 6-row squircle badge; each cell two chars. '#'=A stroke, 'o'=teal dot.
|
|
1196
|
+
local rows=(
|
|
1197
|
+
"......"
|
|
1198
|
+
"..##.."
|
|
1199
|
+
".#..#."
|
|
1200
|
+
".####."
|
|
1201
|
+
".#.o#."
|
|
1202
|
+
".#..#."
|
|
1203
|
+
)
|
|
1204
|
+
printf '\n' >&2
|
|
1205
|
+
local i=0 line r c ch
|
|
1206
|
+
for r in "${rows[@]}"; do
|
|
1207
|
+
line=""
|
|
1208
|
+
for ((c=0; c<${#r}; c++)); do
|
|
1209
|
+
ch="${r:$c:1}"
|
|
1210
|
+
case "$ch" in
|
|
1211
|
+
'#') line+="${pB}${wF}\xE2\x96\x88\xE2\x96\x88${nc}" ;;
|
|
1212
|
+
'o') line+="${pB}${tF}\xE2\x97\x8F ${nc}" ;;
|
|
1213
|
+
*) line+="${pB} ${nc}" ;;
|
|
1214
|
+
esac
|
|
1215
|
+
done
|
|
1216
|
+
# Wordmark + tagline riding beside the badge on two of the rows.
|
|
1217
|
+
case $i in
|
|
1218
|
+
2) printf " %b %b%bLOKI%b\n" "$line" "$b" "$pF" "$nc" >&2 ;;
|
|
1219
|
+
3) printf " %b %bAutonomi cockpit%b\n" "$line" "$dim" "$nc" >&2 ;;
|
|
1220
|
+
*) printf " %b\n" "$line" >&2 ;;
|
|
1221
|
+
esac
|
|
1222
|
+
i=$((i+1))
|
|
1223
|
+
done
|
|
1224
|
+
printf '\n' >&2
|
|
1225
|
+
else
|
|
1226
|
+
# 256-color / limited: a compact, still-branded badge (no truecolor blocks).
|
|
1227
|
+
local p=$'\033[38;5;99m' t=$'\033[38;5;43m'
|
|
1228
|
+
printf '\n %s%s\xE2\x97\x86%s %sLOKI%s %s\xE2\x97\x8F%s %sAutonomi cockpit%s\n\n' \
|
|
1229
|
+
"$b" "$p" "$nc" "$b" "$nc" "$t" "$nc" "$dim" "$nc" >&2
|
|
1230
|
+
fi
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
# E4: a 3-line "what Loki will do" preview from REAL signals only. Line 1 ties
|
|
1234
|
+
# the detected tier to its phase count (simple=3, standard=6, complex=8 -- the
|
|
1235
|
+
# same mapping run.sh uses; auto/unknown stays honest as "auto-detecting"). Line
|
|
1236
|
+
# 2 names the spec, or admits "exploring the codebase" when there is none. Line 3
|
|
1237
|
+
# is the fixed RARV-C closure promise. Never fabricates. Prints 3 lines to stdout.
|
|
1238
|
+
# Args: $1=spec label $2=tier
|
|
1239
|
+
_loki_plan_preview() {
|
|
1240
|
+
local spec="$1" tier="$2"
|
|
1241
|
+
local phases shape
|
|
1242
|
+
case "$(printf '%s' "$tier" | tr '[:upper:]' '[:lower:]')" in
|
|
1243
|
+
simple) phases="3 phases"; shape="a focused build" ;;
|
|
1244
|
+
standard) phases="6 phases"; shape="a full SDLC pass" ;;
|
|
1245
|
+
complex) phases="8 phases"; shape="a deep multi-phase build" ;;
|
|
1246
|
+
*) phases="phase count auto-detecting"; shape="scope from your spec" ;;
|
|
1247
|
+
esac
|
|
1248
|
+
printf ' plan %s -- %s\n' "$phases" "$shape"
|
|
1249
|
+
case "$spec" in
|
|
1250
|
+
""|*"no PRD"*|*"codebase"*)
|
|
1251
|
+
printf ' from exploring the codebase (no spec file)\n' ;;
|
|
1252
|
+
*)
|
|
1253
|
+
printf ' from %s\n' "$spec" ;;
|
|
1254
|
+
esac
|
|
1255
|
+
printf ' loop Reason - Act - Reflect - Verify - Close, until verified\n'
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
# Print the rich handoff card and resolve the user's choice. Echoes exactly one
|
|
1259
|
+
# decision word on stdout:
|
|
1260
|
+
# launch-bg detach only
|
|
1261
|
+
# launch-bg-dashboard detach + open the dashboard in a browser
|
|
1262
|
+
# launch-bg-watch detach + print the `loki cockpit` hint
|
|
1263
|
+
# cancel do not launch (user chose Stop)
|
|
1264
|
+
# All non-cancel decisions detach the build (arm --bg). The choice is remembered
|
|
1265
|
+
# per project under .loki/config/start-view.json; a remembered choice skips the
|
|
1266
|
+
# prompt and prints only a one-line summary.
|
|
1267
|
+
# Args: $1=spec label $2=tier $3=provider
|
|
1268
|
+
_loki_start_handoff() {
|
|
1269
|
+
local spec="$1" tier="$2" prov="$3"
|
|
1270
|
+
local port="${LOKI_DASHBOARD_PORT:-57374}"
|
|
1271
|
+
local dash_url="http://127.0.0.1:${port}"
|
|
1272
|
+
local budget="${LOKI_BUDGET_LIMIT:-none}"
|
|
1273
|
+
local cfg_dir="${LOKI_DIR:-.loki}/config"
|
|
1274
|
+
local cfg_file="$cfg_dir/start-view.json"
|
|
1275
|
+
local pwd_path; pwd_path="$(pwd)"
|
|
1276
|
+
local proj; proj="$(basename "$pwd_path")"
|
|
1277
|
+
|
|
1278
|
+
# Remembered choice: skip the prompt, print one-line summary, reuse decision.
|
|
1279
|
+
local saved=""
|
|
1280
|
+
if [ -f "$cfg_file" ]; then
|
|
1281
|
+
saved="$(sed -n 's/.*"view"[[:space:]]*:[[:space:]]*"\([a-z-]*\)".*/\1/p' "$cfg_file" 2>/dev/null | head -1)"
|
|
1282
|
+
fi
|
|
1283
|
+
if [ -n "$saved" ]; then
|
|
1284
|
+
case "$saved" in
|
|
1285
|
+
both|dashboard|watch|detach)
|
|
1286
|
+
echo -e "${DIM}Detaching to background (remembered view: ${saved}; change with 'loki cockpit' or delete ${cfg_file}).${NC}" >&2
|
|
1287
|
+
case "$saved" in
|
|
1288
|
+
dashboard) echo "launch-bg-dashboard" ;;
|
|
1289
|
+
watch) echo "launch-bg-watch" ;;
|
|
1290
|
+
*) echo "launch-bg" ;;
|
|
1291
|
+
esac
|
|
1292
|
+
return 0
|
|
1293
|
+
;;
|
|
1294
|
+
esac
|
|
1295
|
+
fi
|
|
1296
|
+
|
|
1297
|
+
# Stare-worthy brand banner (S6) then the rich handoff card. Both to stderr so
|
|
1298
|
+
# stdout carries only the decision word.
|
|
1299
|
+
_loki_brand_banner "$proj"
|
|
1300
|
+
{
|
|
1301
|
+
echo -e " ${BOLD}Building ${proj}${NC} \033[38;2;31;197;168m\xE2\x97\x8F${NC} ${DIM}live${NC}"
|
|
1302
|
+
echo ""
|
|
1303
|
+
echo -e " ${DIM}spec${NC} ${spec}"
|
|
1304
|
+
echo -e " ${DIM}path${NC} ${pwd_path}"
|
|
1305
|
+
echo -e " ${DIM}tier${NC} ${tier}"
|
|
1306
|
+
echo -e " ${DIM}provider${NC} ${prov}"
|
|
1307
|
+
echo -e " ${DIM}iteration${NC} starting"
|
|
1308
|
+
echo -e " ${DIM}budget${NC} \$0 / ${budget} (breaker: on)"
|
|
1309
|
+
echo -e " ${DIM}log${NC} ${LOKI_DIR:-.loki}/logs/background-*.log"
|
|
1310
|
+
echo -e " ${DIM}dashboard${NC} ${dash_url}"
|
|
1311
|
+
echo ""
|
|
1312
|
+
# E4 plan preview: 3 lines of real intent before backgrounding.
|
|
1313
|
+
_loki_plan_preview "$spec" "$tier"
|
|
1314
|
+
echo ""
|
|
1315
|
+
echo -e " ${BOLD}Both${NC}[default] ${BOLD}D${NC}ashboard ${BOLD}W${NC}atch De${BOLD}t${NC}ach ${BOLD}S${NC}top"
|
|
1316
|
+
echo -e " ${DIM}opening your cockpit + dashboard in 8s -- press a key to choose${NC}"
|
|
1317
|
+
# First-run delight: only when this project has never chosen a view, teach
|
|
1318
|
+
# the cockpit once. Subsequent runs (config file exists) stay lean.
|
|
1319
|
+
if [ ! -f "$cfg_file" ]; then
|
|
1320
|
+
echo -e " ${DIM}first build here -- 'loki cockpit' reopens this live view any time${NC}"
|
|
1321
|
+
fi
|
|
1322
|
+
} >&2
|
|
1323
|
+
|
|
1324
|
+
local reply=""
|
|
1325
|
+
read -t 8 -r reply 2>/dev/null || reply=""
|
|
1326
|
+
|
|
1327
|
+
local decision="launch-bg" view="both"
|
|
1328
|
+
case "$(printf '%s' "$reply" | tr '[:upper:]' '[:lower:]')" in
|
|
1329
|
+
""|b|both) decision="launch-bg"; view="both" ;;
|
|
1330
|
+
d|dash|dashboard) decision="launch-bg-dashboard"; view="dashboard" ;;
|
|
1331
|
+
w|watch) decision="launch-bg-watch"; view="watch" ;;
|
|
1332
|
+
t|detach) decision="launch-bg"; view="detach" ;;
|
|
1333
|
+
s|stop|cancel|q) decision="cancel"; view="" ;;
|
|
1334
|
+
*) decision="launch-bg"; view="both" ;;
|
|
1335
|
+
esac
|
|
1336
|
+
|
|
1337
|
+
if [ "$decision" = "cancel" ]; then
|
|
1338
|
+
echo -e "${YELLOW}run cancelled${NC}" >&2
|
|
1339
|
+
echo "cancel"
|
|
1340
|
+
return 0
|
|
1341
|
+
fi
|
|
1342
|
+
|
|
1343
|
+
# Remember the choice for next time (best-effort; never blocks the launch).
|
|
1344
|
+
mkdir -p "$cfg_dir" 2>/dev/null && \
|
|
1345
|
+
printf '{"view":"%s"}\n' "$view" > "$cfg_file" 2>/dev/null || true
|
|
1346
|
+
|
|
1347
|
+
if [ "$view" = "both" ]; then
|
|
1348
|
+
echo -e "${DIM}Open the dashboard at ${dash_url} or run 'loki cockpit' to watch.${NC}" >&2
|
|
1349
|
+
elif [ "$view" = "watch" ]; then
|
|
1350
|
+
echo -e "${DIM}Run 'loki cockpit' to watch the build.${NC}" >&2
|
|
1351
|
+
fi
|
|
1352
|
+
|
|
1353
|
+
echo "$decision"
|
|
1354
|
+
return 0
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1159
1357
|
# Start Loki Mode
|
|
1160
1358
|
cmd_start() {
|
|
1161
1359
|
# First-run welcome (once): open the branded welcome page on the very
|
|
@@ -2316,6 +2514,41 @@ cmd_start() {
|
|
|
2316
2514
|
loki_project_graph_discover "$_pg_target" || true
|
|
2317
2515
|
fi
|
|
2318
2516
|
|
|
2517
|
+
# S1: detach-to-background by default on an interactive TTY. Fires a rich,
|
|
2518
|
+
# reversible handoff card, then arms the existing --bg path so run.sh
|
|
2519
|
+
# backgrounds the runner (it writes the log + PID file and prints its own
|
|
2520
|
+
# "Running in Background" banner). The HARD gate below keeps every
|
|
2521
|
+
# non-interactive / --bg / --yes / CI caller on the exact pre-existing
|
|
2522
|
+
# foreground exec -- byte-identical to before this change.
|
|
2523
|
+
local _bg_already=""
|
|
2524
|
+
for _a in ${args[@]+"${args[@]}"}; do
|
|
2525
|
+
if [ "$_a" = "--bg" ]; then _bg_already=1; break; fi
|
|
2526
|
+
done
|
|
2527
|
+
if _loki_start_should_handoff "$_bg_already"; then
|
|
2528
|
+
local _spec_label="${prd_file:-codebase (no PRD)}"
|
|
2529
|
+
local _tier_label="${LOKI_COMPLEXITY:-auto}"
|
|
2530
|
+
local _decision
|
|
2531
|
+
_decision="$(_loki_start_handoff "$_spec_label" "$_tier_label" "$effective_provider")"
|
|
2532
|
+
case "$_decision" in
|
|
2533
|
+
cancel)
|
|
2534
|
+
exit 0
|
|
2535
|
+
;;
|
|
2536
|
+
launch-bg-dashboard|launch-bg)
|
|
2537
|
+
# Both (default) AND Dashboard open the browser -- Both = dashboard
|
|
2538
|
+
# + cockpit, honoring the card's "open dashboard" promise. The
|
|
2539
|
+
# cockpit hint is printed by _loki_start_handoff for the Both path.
|
|
2540
|
+
args+=("--bg")
|
|
2541
|
+
local _du="http://127.0.0.1:${LOKI_DASHBOARD_PORT:-57374}"
|
|
2542
|
+
if command -v open >/dev/null 2>&1; then open "$_du" >/dev/null 2>&1 &
|
|
2543
|
+
elif command -v xdg-open >/dev/null 2>&1; then xdg-open "$_du" >/dev/null 2>&1 & fi
|
|
2544
|
+
;;
|
|
2545
|
+
launch-bg-watch)
|
|
2546
|
+
# Watch (cockpit only) detaches without opening the browser.
|
|
2547
|
+
args+=("--bg")
|
|
2548
|
+
;;
|
|
2549
|
+
esac
|
|
2550
|
+
fi
|
|
2551
|
+
|
|
2319
2552
|
# v7.7.13 fix (user-reported bug): `"${args[@]}"` triggers "unbound
|
|
2320
2553
|
# variable" under bash 3.2 (macOS default) + `set -u` when args is empty.
|
|
2321
2554
|
# User report: `loki start` with no PRD on /Users/lokesh/git/anonima
|
|
@@ -8545,6 +8778,300 @@ cmd_watch() {
|
|
|
8545
8778
|
esac
|
|
8546
8779
|
}
|
|
8547
8780
|
|
|
8781
|
+
#===============================================================================
|
|
8782
|
+
# loki cockpit - Multi-repo live status as a terminal inline image (v7.127.0)
|
|
8783
|
+
#
|
|
8784
|
+
# Renders the Autonomi cockpit frame directly in supported terminals (iTerm2 /
|
|
8785
|
+
# WezTerm / ghostty via the iTerm2 protocol; Kitty via its graphics protocol).
|
|
8786
|
+
# On any unsupported terminal, or when rasterization is unavailable, it falls
|
|
8787
|
+
# back HONESTLY to a compact text summary and points at the browser dashboard.
|
|
8788
|
+
# This never touches `loki watch` (the PRD-file auto-rerun watcher).
|
|
8789
|
+
#===============================================================================
|
|
8790
|
+
|
|
8791
|
+
# Compact honest text summary printed on the fallback path. Reads the same
|
|
8792
|
+
# CockpitState JSON the renderer would have used, so text and image agree.
|
|
8793
|
+
_cockpit_text_summary() {
|
|
8794
|
+
local skill_dir="$1" focus_repo="$2"
|
|
8795
|
+
local state_json
|
|
8796
|
+
state_json="$(cockpit_gather_state "$skill_dir" "$focus_repo" 2>/dev/null || true)"
|
|
8797
|
+
if [ -z "$state_json" ] || ! command -v python3 >/dev/null 2>&1; then
|
|
8798
|
+
echo " (no cockpit state available)"
|
|
8799
|
+
return 0
|
|
8800
|
+
fi
|
|
8801
|
+
LOKI_CK_STATE="$state_json" python3 - <<'PYTXT' 2>/dev/null || echo " (could not render text summary)"
|
|
8802
|
+
import json, os
|
|
8803
|
+
s = json.loads(os.environ.get("LOKI_CK_STATE") or "{}")
|
|
8804
|
+
print(f" Run {s.get('run','?')}")
|
|
8805
|
+
print(f" Iteration {s.get('iteration',0)} Phase {s.get('phase','?')} Verdict {str(s.get('verdict','?')).upper()}")
|
|
8806
|
+
tier = s.get('tier') or '-'
|
|
8807
|
+
prov = s.get('provider') or '-'
|
|
8808
|
+
print(f" Tier {tier} Provider {prov} Budget ${float(s.get('budgetUsd',0)):.2f}")
|
|
8809
|
+
gates = s.get('gates') or []
|
|
8810
|
+
if gates:
|
|
8811
|
+
line = " Gates " + " ".join(f"{g.get('name','?')}={g.get('status','?')}" for g in gates[:8])
|
|
8812
|
+
print(line)
|
|
8813
|
+
council = s.get('council') or []
|
|
8814
|
+
if council:
|
|
8815
|
+
print(" Council " + " ".join(f"{c.get('reviewer','?')}:{c.get('vote','?')}" for c in council[:6]))
|
|
8816
|
+
fleet = s.get('fleet') or []
|
|
8817
|
+
if fleet:
|
|
8818
|
+
cap = 8 # cap so a huge registry (e.g. 171 runs) never floods the summary
|
|
8819
|
+
print(f" Fleet ({len(fleet)}):")
|
|
8820
|
+
for r in fleet[:cap]:
|
|
8821
|
+
mark = "*" if r.get("running") else " "
|
|
8822
|
+
print(f" {mark} {r.get('name','?'):<28} iter {r.get('iteration',0)} {r.get('phase') or r.get('status') or ''}")
|
|
8823
|
+
if len(fleet) > cap:
|
|
8824
|
+
print(f" + {len(fleet) - cap} more")
|
|
8825
|
+
PYTXT
|
|
8826
|
+
}
|
|
8827
|
+
|
|
8828
|
+
cmd_cockpit() {
|
|
8829
|
+
local protocol="auto"
|
|
8830
|
+
local no_image="0"
|
|
8831
|
+
local run_once="0"
|
|
8832
|
+
local follow="1"
|
|
8833
|
+
local interval="2"
|
|
8834
|
+
local focus_repo=""
|
|
8835
|
+
|
|
8836
|
+
while [[ $# -gt 0 ]]; do
|
|
8837
|
+
case "$1" in
|
|
8838
|
+
--help|-h)
|
|
8839
|
+
echo -e "${BOLD}loki cockpit${NC} - Live multi-repo status as a terminal inline image (v7.127.0)"
|
|
8840
|
+
echo ""
|
|
8841
|
+
echo "Usage: loki cockpit [options]"
|
|
8842
|
+
echo ""
|
|
8843
|
+
echo "Renders the Autonomi cockpit (iteration, phase, tier, provider, gates,"
|
|
8844
|
+
echo "council votes, verdict, budget, and the multi-repo fleet) as an inline"
|
|
8845
|
+
echo "image in supported terminals. Falls back to a text summary + the browser"
|
|
8846
|
+
echo "dashboard on terminals without inline-image support."
|
|
8847
|
+
echo ""
|
|
8848
|
+
echo "Options:"
|
|
8849
|
+
echo " --once Render one frame then exit"
|
|
8850
|
+
echo " --follow Re-render on an interval (default)"
|
|
8851
|
+
echo " --interval N Seconds between frames in --follow (default: 2)"
|
|
8852
|
+
echo " --repo PATH Focus one repo instead of the running fleet head"
|
|
8853
|
+
echo " --protocol P iterm2 | kitty | auto (default: auto-detect)"
|
|
8854
|
+
echo " --no-image Force the text/browser fallback"
|
|
8855
|
+
echo " --help, -h Show this help"
|
|
8856
|
+
echo ""
|
|
8857
|
+
echo "Supported terminals: iTerm2, WezTerm, ghostty (iTerm2 protocol); Kitty."
|
|
8858
|
+
echo "Override detection with LOKI_COCKPIT_PROTOCOL=iterm2|kitty."
|
|
8859
|
+
echo ""
|
|
8860
|
+
echo "Note: this is distinct from 'loki watch', which auto-reruns on PRD edits."
|
|
8861
|
+
return 0
|
|
8862
|
+
;;
|
|
8863
|
+
--once) run_once="1"; follow="0"; shift ;;
|
|
8864
|
+
--follow) follow="1"; run_once="0"; shift ;;
|
|
8865
|
+
--interval) interval="${2:-2}"; [ $# -ge 2 ] && shift 2 || shift ;;
|
|
8866
|
+
--interval=*) interval="${1#*=}"; shift ;;
|
|
8867
|
+
--repo) focus_repo="${2:-}"; [ $# -ge 2 ] && shift 2 || shift ;;
|
|
8868
|
+
--repo=*) focus_repo="${1#*=}"; shift ;;
|
|
8869
|
+
--protocol) protocol="${2:-auto}"; [ $# -ge 2 ] && shift 2 || shift ;;
|
|
8870
|
+
--protocol=*) protocol="${1#*=}"; shift ;;
|
|
8871
|
+
--no-image) no_image="1"; shift ;;
|
|
8872
|
+
-*)
|
|
8873
|
+
echo -e "${RED}Unknown option: $1${NC}"
|
|
8874
|
+
echo "Run 'loki cockpit --help' for usage."
|
|
8875
|
+
return 1
|
|
8876
|
+
;;
|
|
8877
|
+
*)
|
|
8878
|
+
if [ -z "$focus_repo" ]; then
|
|
8879
|
+
focus_repo="$1"
|
|
8880
|
+
else
|
|
8881
|
+
echo -e "${RED}Unexpected argument: $1${NC}"
|
|
8882
|
+
return 1
|
|
8883
|
+
fi
|
|
8884
|
+
shift
|
|
8885
|
+
;;
|
|
8886
|
+
esac
|
|
8887
|
+
done
|
|
8888
|
+
|
|
8889
|
+
# Load the render/gather helpers (pure logic lives in loki-ts).
|
|
8890
|
+
local ts_dir="${_LOKI_SCRIPT_DIR}/../loki-ts"
|
|
8891
|
+
local render_lib="${_LOKI_SCRIPT_DIR}/lib/cockpit-render.sh"
|
|
8892
|
+
if [ -f "$render_lib" ]; then
|
|
8893
|
+
# shellcheck disable=SC1090
|
|
8894
|
+
source "$render_lib"
|
|
8895
|
+
else
|
|
8896
|
+
echo -e "${RED}cockpit render library not found${NC} ($render_lib)" >&2
|
|
8897
|
+
return 1
|
|
8898
|
+
fi
|
|
8899
|
+
|
|
8900
|
+
_cockpit_frame() {
|
|
8901
|
+
local rc=0
|
|
8902
|
+
# cockpit_render prints the terminal image to stdout on success (rc 0),
|
|
8903
|
+
# or exits 3 (fallback) / 2 (bun unavailable). Capture stderr for the
|
|
8904
|
+
# FALLBACK reason without swallowing the image bytes on stdout.
|
|
8905
|
+
cockpit_render "$SKILL_DIR" "$ts_dir" "$protocol" "$no_image" "$focus_repo" 2>/tmp/loki-cockpit-err.$$ || rc=$?
|
|
8906
|
+
if [ "$rc" = "0" ]; then
|
|
8907
|
+
rm -f "/tmp/loki-cockpit-err.$$" 2>/dev/null
|
|
8908
|
+
return 0
|
|
8909
|
+
fi
|
|
8910
|
+
# Honest fallback: text summary + browser dashboard pointer. NEVER claim
|
|
8911
|
+
# an image was rendered here.
|
|
8912
|
+
local reason=""
|
|
8913
|
+
if [ -f "/tmp/loki-cockpit-err.$$" ]; then
|
|
8914
|
+
reason="$(sed -n 's/^FALLBACK\t//p' "/tmp/loki-cockpit-err.$$" | head -1)"
|
|
8915
|
+
rm -f "/tmp/loki-cockpit-err.$$" 2>/dev/null
|
|
8916
|
+
fi
|
|
8917
|
+
[ -z "$reason" ] && reason="inline image unavailable in this terminal"
|
|
8918
|
+
echo ""
|
|
8919
|
+
echo -e "${BOLD}Loki Cockpit${NC} ${DIM}(text fallback: ${reason})${NC}"
|
|
8920
|
+
_cockpit_text_summary "$SKILL_DIR" "$focus_repo"
|
|
8921
|
+
echo ""
|
|
8922
|
+
echo -e " Open the full visual dashboard: ${CYAN}loki dashboard${NC}"
|
|
8923
|
+
return 3
|
|
8924
|
+
}
|
|
8925
|
+
|
|
8926
|
+
# E3 footer hints. _cockpit_hint is the persistent key legend under a frame;
|
|
8927
|
+
# _cockpit_hint_key is shown after an explain/steer panel to say how to return.
|
|
8928
|
+
_cockpit_hint() {
|
|
8929
|
+
printf '\n'
|
|
8930
|
+
echo -e " ${DIM}Tab/arrows focus e explain s steer q quit${NC}"
|
|
8931
|
+
}
|
|
8932
|
+
_cockpit_hint_key() {
|
|
8933
|
+
printf '\n'
|
|
8934
|
+
echo -e " ${DIM}press any key to return to the live view${NC}"
|
|
8935
|
+
}
|
|
8936
|
+
|
|
8937
|
+
# Max mtime (whole seconds) across the focus run's state files -- the honest
|
|
8938
|
+
# "something changed" signal for event-driven redraw. Prints empty when stat
|
|
8939
|
+
# is unavailable so the caller falls back to a blind interval. Both BSD stat
|
|
8940
|
+
# (-f %m, macOS) and GNU stat (-c %Y, Linux) are probed; nothing else is
|
|
8941
|
+
# installed. ponytail: whole-second granularity is enough for a status frame.
|
|
8942
|
+
_cockpit_mtime() {
|
|
8943
|
+
local repo="$1" loki base m best=""
|
|
8944
|
+
[ -n "$repo" ] || repo="$(pwd)"
|
|
8945
|
+
loki="$repo/.loki"
|
|
8946
|
+
for base in autonomy-state.json events.jsonl verify/evidence.json council; do
|
|
8947
|
+
local p="$loki/$base"
|
|
8948
|
+
[ -e "$p" ] || continue
|
|
8949
|
+
m="$(stat -f '%m' "$p" 2>/dev/null || stat -c '%Y' "$p" 2>/dev/null || true)"
|
|
8950
|
+
[ -n "$m" ] || continue
|
|
8951
|
+
if [ -z "$best" ] || { [ "$m" -gt "$best" ] 2>/dev/null; }; then best="$m"; fi
|
|
8952
|
+
done
|
|
8953
|
+
printf '%s' "$best"
|
|
8954
|
+
}
|
|
8955
|
+
|
|
8956
|
+
if [ "$run_once" = "1" ]; then
|
|
8957
|
+
_cockpit_frame
|
|
8958
|
+
return 0
|
|
8959
|
+
fi
|
|
8960
|
+
|
|
8961
|
+
# --follow (default): re-render on state change until interrupted. Render the
|
|
8962
|
+
# FIRST frame BEFORE the alt-screen so a fallback terminal keeps its text
|
|
8963
|
+
# summary in the scrollback (looping text is noise -> we stop there).
|
|
8964
|
+
local frame_rc=0
|
|
8965
|
+
_cockpit_frame || frame_rc=$?
|
|
8966
|
+
if [ "$frame_rc" != "0" ]; then
|
|
8967
|
+
# Fallback path: no live image loop; the text summary + dashboard hint
|
|
8968
|
+
# were already printed once. Point the user at --follow-capable UIs.
|
|
8969
|
+
return 0
|
|
8970
|
+
fi
|
|
8971
|
+
|
|
8972
|
+
# Image path: enter the alternate screen buffer so redraws never scroll the
|
|
8973
|
+
# user's scrollback, and restore the terminal cleanly on any exit. The traps
|
|
8974
|
+
# cover Ctrl-C (INT), kill (TERM) and normal return (EXIT). The loop runs in a
|
|
8975
|
+
# subshell so the EXIT trap is scoped to the follow loop and cannot leak into
|
|
8976
|
+
# the caller's shell.
|
|
8977
|
+
#
|
|
8978
|
+
# E3 keyboard interactivity: on a TTY we put the terminal in raw single-key
|
|
8979
|
+
# mode so Tab/arrows/e/s/q are read without Enter, and the wait between frames
|
|
8980
|
+
# becomes a key poll (cockpit_read_key) instead of a blind sleep -- so a key
|
|
8981
|
+
# acts instantly instead of after the cadence. Restore stty + cursor on any
|
|
8982
|
+
# exit. If stdin is not a TTY, interactivity is skipped and the loop still
|
|
8983
|
+
# renders + refreshes exactly as before.
|
|
8984
|
+
local interactive=0
|
|
8985
|
+
if [ -t 0 ] && [ -t 1 ] && command -v stty >/dev/null 2>&1; then
|
|
8986
|
+
interactive=1
|
|
8987
|
+
fi
|
|
8988
|
+
(
|
|
8989
|
+
local _saved_stty=""
|
|
8990
|
+
[ "$interactive" = "1" ] && _saved_stty="$(stty -g 2>/dev/null || true)"
|
|
8991
|
+
_cockpit_restore() {
|
|
8992
|
+
[ -n "$_saved_stty" ] && stty "$_saved_stty" 2>/dev/null || true
|
|
8993
|
+
printf '\033[?25h\033[?1049l' # show cursor, leave alt-screen
|
|
8994
|
+
}
|
|
8995
|
+
trap '_cockpit_restore; exit 0' INT TERM
|
|
8996
|
+
trap '_cockpit_restore' EXIT
|
|
8997
|
+
[ "$interactive" = "1" ] && stty -echo -icanon min 0 time 0 2>/dev/null || true
|
|
8998
|
+
printf '\033[?1049h\033[?25l' # enter alt-screen, hide cursor
|
|
8999
|
+
printf '\033[H\033[2J' # home + clear the alt buffer once
|
|
9000
|
+
_cockpit_frame
|
|
9001
|
+
[ "$interactive" = "1" ] && _cockpit_hint
|
|
9002
|
+
|
|
9003
|
+
# Cadence bounds (seconds): poll every min_cadence for a state change
|
|
9004
|
+
# (anti-thrash upper bound on redraw rate); force a refresh at least every
|
|
9005
|
+
# max_idle so a stalled run still shows a live freshness clock. --interval
|
|
9006
|
+
# sets max_idle (and the blind fallback period when stat is missing).
|
|
9007
|
+
local min_cadence=1
|
|
9008
|
+
local max_idle="$interval"; [ "$max_idle" -ge 2 ] 2>/dev/null || max_idle=2
|
|
9009
|
+
local last_mtime; last_mtime="$(_cockpit_mtime "$focus_repo")"
|
|
9010
|
+
local idle=0
|
|
9011
|
+
while :; do
|
|
9012
|
+
# Wait one cadence step -- via a key poll when interactive (returns
|
|
9013
|
+
# early on a keypress), otherwise a plain sleep. On any wait failure
|
|
9014
|
+
# (interrupted) break out and let the trap restore the terminal.
|
|
9015
|
+
local key=""
|
|
9016
|
+
if [ "$interactive" = "1" ]; then
|
|
9017
|
+
key="$(cockpit_read_key "$min_cadence" 2>/dev/null || true)"
|
|
9018
|
+
else
|
|
9019
|
+
sleep "$min_cadence" 2>/dev/null || break
|
|
9020
|
+
fi
|
|
9021
|
+
idle=$((idle + min_cadence))
|
|
9022
|
+
|
|
9023
|
+
# Dispatch a keypress (no-op if none / not interactive).
|
|
9024
|
+
if [ -n "$key" ]; then
|
|
9025
|
+
local action; action="$(cockpit_handle_key "$key")"
|
|
9026
|
+
case "$action" in
|
|
9027
|
+
quit) break ;;
|
|
9028
|
+
next|prev)
|
|
9029
|
+
local paths; paths="$(cockpit_fleet_paths "$SKILL_DIR")"
|
|
9030
|
+
focus_repo="$(cockpit_cycle_focus "$focus_repo" "$action" "$paths")"
|
|
9031
|
+
last_mtime="$(_cockpit_mtime "$focus_repo")"
|
|
9032
|
+
idle=0
|
|
9033
|
+
printf '\033[H\033[2J'
|
|
9034
|
+
_cockpit_frame || break
|
|
9035
|
+
_cockpit_hint
|
|
9036
|
+
continue ;;
|
|
9037
|
+
explain)
|
|
9038
|
+
printf '\033[H\033[2J'
|
|
9039
|
+
_cockpit_frame
|
|
9040
|
+
cockpit_explain_focus "$focus_repo"
|
|
9041
|
+
_cockpit_hint_key
|
|
9042
|
+
continue ;;
|
|
9043
|
+
steer)
|
|
9044
|
+
printf '\033[H\033[2J'
|
|
9045
|
+
_cockpit_frame
|
|
9046
|
+
cockpit_steer_hint "$focus_repo"
|
|
9047
|
+
_cockpit_hint_key
|
|
9048
|
+
continue ;;
|
|
9049
|
+
esac
|
|
9050
|
+
fi
|
|
9051
|
+
|
|
9052
|
+
local cur; cur="$(_cockpit_mtime "$focus_repo")"
|
|
9053
|
+
local changed=0
|
|
9054
|
+
if [ -n "$cur" ]; then
|
|
9055
|
+
# Event-driven: redraw when a watched file advanced, or after
|
|
9056
|
+
# max_idle seconds with no change (keeps the freshness clock live).
|
|
9057
|
+
if [ "$cur" != "$last_mtime" ] || [ "$idle" -ge "$max_idle" ]; then
|
|
9058
|
+
changed=1; last_mtime="$cur"
|
|
9059
|
+
fi
|
|
9060
|
+
else
|
|
9061
|
+
# stat unavailable -> blind interval fallback.
|
|
9062
|
+
[ "$idle" -ge "$max_idle" ] && changed=1
|
|
9063
|
+
fi
|
|
9064
|
+
if [ "$changed" = "1" ]; then
|
|
9065
|
+
idle=0
|
|
9066
|
+
printf '\033[H\033[2J' # home + clear alt buffer, then redraw
|
|
9067
|
+
_cockpit_frame || break
|
|
9068
|
+
[ "$interactive" = "1" ] && _cockpit_hint
|
|
9069
|
+
fi
|
|
9070
|
+
done
|
|
9071
|
+
)
|
|
9072
|
+
return 0
|
|
9073
|
+
}
|
|
9074
|
+
|
|
8548
9075
|
#===============================================================================
|
|
8549
9076
|
# loki export - Export session data (v6.0.0)
|
|
8550
9077
|
#===============================================================================
|
|
@@ -9808,7 +10335,7 @@ cmd_doctor() {
|
|
|
9808
10335
|
echo " --json Output machine-readable JSON"
|
|
9809
10336
|
echo ""
|
|
9810
10337
|
echo "Checks: node, python3, jq, git, curl, bash version,"
|
|
9811
|
-
echo " claude/codex CLIs, and
|
|
10338
|
+
echo " claude/codex CLIs, disk space, and cockpit render capability."
|
|
9812
10339
|
return 0
|
|
9813
10340
|
;;
|
|
9814
10341
|
*)
|
|
@@ -10260,6 +10787,54 @@ except Exception:
|
|
|
10260
10787
|
fi
|
|
10261
10788
|
echo ""
|
|
10262
10789
|
|
|
10790
|
+
# Cockpit capability (E5): report what `loki cockpit` will actually do in
|
|
10791
|
+
# this terminal. Informational only (does NOT touch pass/warn/fail counts,
|
|
10792
|
+
# like Runtime route above), so the bun-parity matrix stays reconcilable.
|
|
10793
|
+
# The protocol + resvg + path come from a bun probe that reuses the real
|
|
10794
|
+
# detection code (loki-ts) so this report can never drift from behavior;
|
|
10795
|
+
# with no bun we honestly report the text-only path.
|
|
10796
|
+
echo -e "${CYAN}Cockpit:${NC}"
|
|
10797
|
+
# Truecolor: purely from COLORTERM, same signal the renderer relies on.
|
|
10798
|
+
case "${COLORTERM:-}" in
|
|
10799
|
+
truecolor|24bit)
|
|
10800
|
+
echo -e " ${GREEN}PASS${NC} Truecolor: yes (COLORTERM=${COLORTERM})" ;;
|
|
10801
|
+
*)
|
|
10802
|
+
echo -e " ${DIM} -- ${NC} Truecolor: not signalled (COLORTERM unset) -- colors may be approximate" ;;
|
|
10803
|
+
esac
|
|
10804
|
+
local _ck_ts_dir="${_LOKI_SCRIPT_DIR}/../loki-ts"
|
|
10805
|
+
local _ck_entry="$_ck_ts_dir/dist/cockpit.js"
|
|
10806
|
+
[ -f "$_ck_entry" ] || _ck_entry="$_ck_ts_dir/src/cockpit/cli.ts"
|
|
10807
|
+
if command -v bun >/dev/null 2>&1 && [ -f "$_ck_entry" ]; then
|
|
10808
|
+
echo -e " ${GREEN}PASS${NC} Bun present -- cockpit renderer available"
|
|
10809
|
+
local _ck_probe _ck_proto _ck_resvg _ck_path
|
|
10810
|
+
_ck_probe="$(bun "$_ck_entry" --probe 2>/dev/null || true)"
|
|
10811
|
+
_ck_proto="$(printf '%s\n' "$_ck_probe" | sed -n 's/^protocol=//p' | head -1)"
|
|
10812
|
+
_ck_resvg="$(printf '%s\n' "$_ck_probe" | sed -n 's/^resvg=//p' | head -1)"
|
|
10813
|
+
_ck_path="$(printf '%s\n' "$_ck_probe" | sed -n 's/^path=//p' | head -1)"
|
|
10814
|
+
[ -n "$_ck_proto" ] || _ck_proto="none"
|
|
10815
|
+
[ -n "$_ck_resvg" ] || _ck_resvg="no"
|
|
10816
|
+
[ -n "$_ck_path" ] || _ck_path="text+dashboard"
|
|
10817
|
+
if [ "$_ck_proto" = "none" ]; then
|
|
10818
|
+
echo -e " ${DIM} -- ${NC} Inline-image protocol: none (this terminal has no iTerm2/Kitty graphics)"
|
|
10819
|
+
else
|
|
10820
|
+
echo -e " ${GREEN}PASS${NC} Inline-image protocol: ${_ck_proto}"
|
|
10821
|
+
fi
|
|
10822
|
+
if [ "$_ck_resvg" = "yes" ]; then
|
|
10823
|
+
echo -e " ${GREEN}PASS${NC} @resvg/resvg-js: installed (SVG can rasterize to PNG)"
|
|
10824
|
+
else
|
|
10825
|
+
echo -e " ${DIM} -- ${NC} @resvg/resvg-js: not installed (optional; cockpit uses text+dashboard)"
|
|
10826
|
+
fi
|
|
10827
|
+
if [ "$_ck_path" = "image" ]; then
|
|
10828
|
+
echo -e " ${GREEN}PASS${NC} Render path: inline image ('loki cockpit' draws the frame in-terminal)"
|
|
10829
|
+
else
|
|
10830
|
+
echo -e " ${DIM} -- ${NC} Render path: text + browser dashboard ('loki cockpit' prints a summary; open 'loki dashboard')"
|
|
10831
|
+
fi
|
|
10832
|
+
else
|
|
10833
|
+
echo -e " ${YELLOW}WARN${NC} Bun not found -- 'loki cockpit' uses the text summary + browser dashboard (install bun for the inline-image frame)"
|
|
10834
|
+
echo -e " ${DIM} -- ${NC} Render path: text + browser dashboard"
|
|
10835
|
+
fi
|
|
10836
|
+
echo ""
|
|
10837
|
+
|
|
10263
10838
|
# Summary
|
|
10264
10839
|
echo -e "${BOLD}Summary:${NC} ${GREEN}$pass_count passed${NC}, ${RED}$fail_count failed${NC}, ${YELLOW}$warn_count warnings${NC}"
|
|
10265
10840
|
echo ""
|
|
@@ -17043,6 +17618,9 @@ main() {
|
|
|
17043
17618
|
watch)
|
|
17044
17619
|
cmd_watch "$@"
|
|
17045
17620
|
;;
|
|
17621
|
+
cockpit)
|
|
17622
|
+
cmd_cockpit "$@"
|
|
17623
|
+
;;
|
|
17046
17624
|
export)
|
|
17047
17625
|
# CLI consolidation (Phase A): 'export' -> 'report export'.
|
|
17048
17626
|
_deprecated_alias export "report export" "$@"
|