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/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 disk space."
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" "$@"
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.125.0"
10
+ __version__ = "7.127.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try: