loki-mode 7.124.0 → 7.126.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -483,6 +483,7 @@ Status legend: "E2E-verified" means we run real spec-to-code builds on it oursel
483
483
  | `loki modernize heal <path>` | Legacy system healing (archaeology, stabilize, isolate, modernize, validate -- v6.67.0; was: `loki heal`) |
484
484
  | `loki pause` / `resume` | Pause/resume after current session |
485
485
  | `loki status` | Show current status |
486
+ | `loki cockpit` | Live multi-repo status as an inline terminal image (Kitty/iTerm2/WezTerm/Ghostty); text + dashboard fallback elsewhere (v7.126.0) |
486
487
  | `loki dashboard` | Open web dashboard |
487
488
  | `loki preview` | Print running app URL and open in browser (Live App Preview, v7.24.0; was: `loki open`) |
488
489
  | `loki web` | Launch Purple Lab web UI [DEPRECATED in v7.44.0 -- use `loki start` which auto-opens the dashboard at http://localhost:57374; for the hosted platform see Autonomi Cloud] |
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.124.0
6
+ # Loki Mode v7.126.0
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -408,4 +408,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
408
408
 
409
409
  ---
410
410
 
411
- **v7.124.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
411
+ **v7.126.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.124.0
1
+ 7.126.0
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env bash
2
+ # autonomy/lib/cockpit-render.sh -- state gathering + render dispatch for
3
+ # `loki cockpit`. Thin wrapper: it builds a CockpitState JSON from the machine
4
+ # registry + each project's .loki state, then pipes it to the pure Bun render
5
+ # pipeline (loki-ts/src/cockpit/cli.ts). All the SVG/PNG/terminal-encode logic
6
+ # lives there so it stays bun-testable; this file only does I/O and fallback.
7
+ #
8
+ # Contract with the TS entry:
9
+ # stdin : CockpitState JSON
10
+ # stdout : raw terminal image escape (image path), empty on fallback
11
+ # exit 0 : image emitted ; exit 3 : fallback (reason on stderr as FALLBACK\t..)
12
+ # other : hard error (bun missing etc.) -> caller falls back too
13
+ #
14
+ # Nothing here changes cmd_watch or any existing command.
15
+
16
+ # cockpit_gather_state <skill_dir> <focus_repo_or_empty>
17
+ # Emits a CockpitState JSON on stdout. Never fails hard: missing files degrade
18
+ # to empty/default fields so the render always has something to show.
19
+ cockpit_gather_state() {
20
+ local skill_dir="$1"
21
+ local focus_repo="${2:-}"
22
+
23
+ if ! command -v python3 >/dev/null 2>&1; then
24
+ # No python: emit a minimal single-repo state from the cwd only.
25
+ printf '{"run":"%s","iteration":0,"phase":"unknown","tier":"","provider":"","verdict":"unknown","budgetUsd":0,"gates":[],"council":[],"fleet":[]}\n' \
26
+ "$(basename "$(pwd)")"
27
+ return 0
28
+ fi
29
+
30
+ LOKI_CK_SKILL="$skill_dir" LOKI_CK_FOCUS="$focus_repo" LOKI_CK_CWD="$(pwd)" \
31
+ python3 - <<'PYCOCKPIT' 2>/dev/null || printf '{"run":"cockpit","iteration":0,"phase":"unknown","tier":"","provider":"","verdict":"unknown","budgetUsd":0,"gates":[],"council":[],"fleet":[]}\n'
32
+ import json, os, sys
33
+
34
+ skill = os.environ.get("LOKI_CK_SKILL", ".")
35
+ focus = (os.environ.get("LOKI_CK_FOCUS") or "").strip()
36
+ cwd = os.environ.get("LOKI_CK_CWD") or os.getcwd()
37
+ sys.path.insert(0, skill)
38
+
39
+
40
+ def read_json(path):
41
+ try:
42
+ with open(path) as f:
43
+ return json.load(f)
44
+ except Exception:
45
+ return {}
46
+
47
+
48
+ def project_state(path):
49
+ """Best-effort per-project state from its .loki/ files."""
50
+ loki = os.path.join(path, ".loki")
51
+ st = read_json(os.path.join(loki, "autonomy-state.json"))
52
+ ev = read_json(os.path.join(loki, "verify", "evidence.json"))
53
+
54
+ iteration = st.get("iterationCount", st.get("iteration", 0)) or 0
55
+ phase = st.get("phase") or st.get("status") or ""
56
+
57
+ verdict = "unknown"
58
+ v = str(ev.get("verdict", "")).upper()
59
+ if v in ("PASS", "VERIFIED"):
60
+ verdict = "verified"
61
+ elif v in ("BLOCKED", "FAIL", "FAILED"):
62
+ verdict = "failed"
63
+ elif v in ("WORKING", "PARTIAL", "INCONCLUSIVE"):
64
+ verdict = "working"
65
+ elif v:
66
+ verdict = "pending"
67
+
68
+ gates = []
69
+ for g in ev.get("deterministic_gates", []) or []:
70
+ gs = str(g.get("status", "")).lower()
71
+ status = {"pass": "pass", "fail": "fail", "failed": "fail",
72
+ "skipped": "skip", "skip": "skip"}.get(gs, "pending")
73
+ gates.append({"name": g.get("gate", "gate"), "status": status})
74
+
75
+ # Council votes: .loki/council/*.json, each with a vote-ish field.
76
+ council = []
77
+ cdir = os.path.join(loki, "council")
78
+ try:
79
+ for fn in sorted(os.listdir(cdir)):
80
+ if not fn.endswith(".json"):
81
+ continue
82
+ cj = read_json(os.path.join(cdir, fn))
83
+ raw = str(cj.get("vote") or cj.get("verdict") or "").lower()
84
+ vote = "pending"
85
+ if "approve" in raw:
86
+ vote = "approve"
87
+ elif "reject" in raw:
88
+ vote = "reject"
89
+ elif "concern" in raw:
90
+ vote = "concern"
91
+ reviewer = cj.get("reviewer") or cj.get("name") or os.path.splitext(fn)[0]
92
+ council.append({"reviewer": str(reviewer)[:16], "vote": vote})
93
+ except Exception:
94
+ pass
95
+
96
+ return {
97
+ "iteration": int(iteration) if isinstance(iteration, (int, float)) else 0,
98
+ "phase": phase or "idle",
99
+ "verdict": verdict,
100
+ "gates": gates,
101
+ "council": council,
102
+ "budgetUsd": float(st.get("cost_usd", 0.0) or 0.0),
103
+ "tier": st.get("tier", "") or "",
104
+ "provider": st.get("provider", "") or "",
105
+ }
106
+
107
+
108
+ # Fleet from the registry; graceful empty on any failure.
109
+ fleet = []
110
+ runs = []
111
+ try:
112
+ from dashboard import registry
113
+ runs = registry.get_fleet_runs(include_inactive=True)
114
+ except Exception:
115
+ runs = []
116
+
117
+ for r in runs:
118
+ fleet.append({
119
+ "name": r.get("name") or os.path.basename(r.get("path", "") or "project"),
120
+ "phase": r.get("phase", "") or "",
121
+ "iteration": r.get("iteration", 0) or 0,
122
+ "status": r.get("status", "") or "",
123
+ "running": bool(r.get("running")),
124
+ })
125
+
126
+ # Focused run: --repo wins; else first running fleet run; else cwd.
127
+ focus_path = focus or cwd
128
+ focus_entry = None
129
+ for r in runs:
130
+ rp = os.path.abspath(r.get("path", "") or "")
131
+ if rp and rp == os.path.abspath(focus_path):
132
+ focus_entry = r
133
+ break
134
+ if focus_entry is None and not focus:
135
+ for r in runs:
136
+ if r.get("running"):
137
+ focus_entry = r
138
+ focus_path = r.get("path", "") or cwd
139
+ break
140
+
141
+ ps = project_state(focus_path)
142
+ run_name = (focus_entry or {}).get("name") if focus_entry else os.path.basename(os.path.abspath(focus_path))
143
+
144
+ # Registry values override file reads where present (registry is fresher).
145
+ if focus_entry:
146
+ if focus_entry.get("iteration"):
147
+ ps["iteration"] = focus_entry["iteration"]
148
+ if focus_entry.get("phase"):
149
+ ps["phase"] = focus_entry["phase"]
150
+ if focus_entry.get("cost_usd"):
151
+ ps["budgetUsd"] = float(focus_entry["cost_usd"])
152
+
153
+ state = {
154
+ "run": run_name or "cockpit",
155
+ "iteration": ps["iteration"],
156
+ "phase": ps["phase"],
157
+ "tier": ps["tier"] or (os.environ.get("LOKI_SESSION_MODEL", "")),
158
+ "provider": ps["provider"] or (os.environ.get("LOKI_PROVIDER", "claude")),
159
+ "verdict": ps["verdict"],
160
+ "budgetUsd": ps["budgetUsd"],
161
+ "budgetLimitUsd": float(os.environ.get("LOKI_MAX_BUDGET_USD", "0") or 0) or None,
162
+ "freshness": "now",
163
+ "gates": ps["gates"],
164
+ "council": ps["council"],
165
+ "fleet": fleet,
166
+ }
167
+ print(json.dumps(state))
168
+ PYCOCKPIT
169
+ }
170
+
171
+ # cockpit_render <skill_dir> <ts_dir> <protocol> <no_image> <focus_repo>
172
+ # Returns: 0 (image emitted to stdout), 3 (fallback), or 2 (bun unavailable).
173
+ # On fallback / bun-missing the caller is responsible for the text summary.
174
+ cockpit_render() {
175
+ local skill_dir="$1" ts_dir="$2" protocol="${3:-auto}" no_image="${4:-0}" focus_repo="${5:-}"
176
+
177
+ # Prefer the shipped bundle (dist/cockpit.js -- present in npm/Docker/brew
178
+ # installs, which ship loki-ts/dist/ but not src/); fall back to the source
179
+ # entry for in-repo development.
180
+ local entry="$ts_dir/dist/cockpit.js"
181
+ [ -f "$entry" ] || entry="$ts_dir/src/cockpit/cli.ts"
182
+ if ! command -v bun >/dev/null 2>&1 || [ ! -f "$entry" ]; then
183
+ return 2
184
+ fi
185
+
186
+ local args=(--protocol "$protocol")
187
+ [ "$no_image" = "1" ] && args+=(--no-image)
188
+
189
+ # Gather -> render. Preserve the TS exit code (0 image / 3 fallback).
190
+ cockpit_gather_state "$skill_dir" "$focus_repo" | bun "$entry" "${args[@]}"
191
+ }
package/autonomy/loki CHANGED
@@ -903,6 +903,7 @@ show_help() {
903
903
  echo " --github Enable GitHub issue import"
904
904
  echo " --no-dashboard Disable web dashboard"
905
905
  echo " --sandbox Run in Docker sandbox for isolation (default: off; requires Docker)"
906
+ echo " --isolation LVL Per-run isolation dial: none (default) | worktree | docker"
906
907
  echo " --skip-memory Skip loading memory context at startup"
907
908
  echo " --fresh-prd Regenerate the PRD from the codebase (no-PRD runs; ignores the reusable generated PRD)"
908
909
  echo " --compliance PRESET Enable compliance mode (default|healthcare|fintech|government)"
@@ -958,6 +959,7 @@ show_help() {
958
959
  echo " loki config set maxTier sonnet # Cap model cost"
959
960
  echo " loki dashboard start # Web dashboard at localhost:57374"
960
961
  echo " loki watch # Auto-rerun on PRD changes"
962
+ echo " loki cockpit # Live multi-repo cockpit (terminal image)"
961
963
  echo " loki remote # Remote session (phone/browser)"
962
964
  echo ""
963
965
  echo "Phase A-J features (v7.5.18 - v7.5.28) are default-on. See CHANGELOG."
@@ -1155,6 +1157,170 @@ detect_arg_type() {
1155
1157
  echo "unknown"
1156
1158
  }
1157
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
+ # Print the rich handoff card and resolve the user's choice. Echoes exactly one
1234
+ # decision word on stdout:
1235
+ # launch-bg detach only
1236
+ # launch-bg-dashboard detach + open the dashboard in a browser
1237
+ # launch-bg-watch detach + print the `loki cockpit` hint
1238
+ # cancel do not launch (user chose Stop)
1239
+ # All non-cancel decisions detach the build (arm --bg). The choice is remembered
1240
+ # per project under .loki/config/start-view.json; a remembered choice skips the
1241
+ # prompt and prints only a one-line summary.
1242
+ # Args: $1=spec label $2=tier $3=provider
1243
+ _loki_start_handoff() {
1244
+ local spec="$1" tier="$2" prov="$3"
1245
+ local port="${LOKI_DASHBOARD_PORT:-57374}"
1246
+ local dash_url="http://127.0.0.1:${port}"
1247
+ local budget="${LOKI_BUDGET_LIMIT:-none}"
1248
+ local cfg_dir="${LOKI_DIR:-.loki}/config"
1249
+ local cfg_file="$cfg_dir/start-view.json"
1250
+ local pwd_path; pwd_path="$(pwd)"
1251
+ local proj; proj="$(basename "$pwd_path")"
1252
+
1253
+ # Remembered choice: skip the prompt, print one-line summary, reuse decision.
1254
+ local saved=""
1255
+ if [ -f "$cfg_file" ]; then
1256
+ saved="$(sed -n 's/.*"view"[[:space:]]*:[[:space:]]*"\([a-z-]*\)".*/\1/p' "$cfg_file" 2>/dev/null | head -1)"
1257
+ fi
1258
+ if [ -n "$saved" ]; then
1259
+ case "$saved" in
1260
+ both|dashboard|watch|detach)
1261
+ echo -e "${DIM}Detaching to background (remembered view: ${saved}; change with 'loki cockpit' or delete ${cfg_file}).${NC}" >&2
1262
+ case "$saved" in
1263
+ dashboard) echo "launch-bg-dashboard" ;;
1264
+ watch) echo "launch-bg-watch" ;;
1265
+ *) echo "launch-bg" ;;
1266
+ esac
1267
+ return 0
1268
+ ;;
1269
+ esac
1270
+ fi
1271
+
1272
+ # Stare-worthy brand banner (S6) then the rich handoff card. Both to stderr so
1273
+ # stdout carries only the decision word.
1274
+ _loki_brand_banner "$proj"
1275
+ {
1276
+ echo -e " ${BOLD}Building ${proj}${NC} \033[38;2;31;197;168m\xE2\x97\x8F${NC} ${DIM}live${NC}"
1277
+ echo ""
1278
+ echo -e " ${DIM}spec${NC} ${spec}"
1279
+ echo -e " ${DIM}path${NC} ${pwd_path}"
1280
+ echo -e " ${DIM}tier${NC} ${tier}"
1281
+ echo -e " ${DIM}provider${NC} ${prov}"
1282
+ echo -e " ${DIM}iteration${NC} starting"
1283
+ echo -e " ${DIM}budget${NC} \$0 / ${budget} (breaker: on)"
1284
+ echo -e " ${DIM}log${NC} ${LOKI_DIR:-.loki}/logs/background-*.log"
1285
+ echo -e " ${DIM}dashboard${NC} ${dash_url}"
1286
+ echo ""
1287
+ echo -e " ${BOLD}Both${NC}[default] ${BOLD}D${NC}ashboard ${BOLD}W${NC}atch De${BOLD}t${NC}ach ${BOLD}S${NC}top"
1288
+ echo -e " ${DIM}opening your cockpit + dashboard in 8s -- press a key to choose${NC}"
1289
+ } >&2
1290
+
1291
+ local reply=""
1292
+ read -t 8 -r reply 2>/dev/null || reply=""
1293
+
1294
+ local decision="launch-bg" view="both"
1295
+ case "$(printf '%s' "$reply" | tr '[:upper:]' '[:lower:]')" in
1296
+ ""|b|both) decision="launch-bg"; view="both" ;;
1297
+ d|dash|dashboard) decision="launch-bg-dashboard"; view="dashboard" ;;
1298
+ w|watch) decision="launch-bg-watch"; view="watch" ;;
1299
+ t|detach) decision="launch-bg"; view="detach" ;;
1300
+ s|stop|cancel|q) decision="cancel"; view="" ;;
1301
+ *) decision="launch-bg"; view="both" ;;
1302
+ esac
1303
+
1304
+ if [ "$decision" = "cancel" ]; then
1305
+ echo -e "${YELLOW}run cancelled${NC}" >&2
1306
+ echo "cancel"
1307
+ return 0
1308
+ fi
1309
+
1310
+ # Remember the choice for next time (best-effort; never blocks the launch).
1311
+ mkdir -p "$cfg_dir" 2>/dev/null && \
1312
+ printf '{"view":"%s"}\n' "$view" > "$cfg_file" 2>/dev/null || true
1313
+
1314
+ if [ "$view" = "both" ]; then
1315
+ echo -e "${DIM}Open the dashboard at ${dash_url} or run 'loki cockpit' to watch.${NC}" >&2
1316
+ elif [ "$view" = "watch" ]; then
1317
+ echo -e "${DIM}Run 'loki cockpit' to watch the build.${NC}" >&2
1318
+ fi
1319
+
1320
+ echo "$decision"
1321
+ return 0
1322
+ }
1323
+
1158
1324
  # Start Loki Mode
1159
1325
  cmd_start() {
1160
1326
  # First-run welcome (once): open the branded welcome page on the very
@@ -1221,6 +1387,8 @@ cmd_start() {
1221
1387
  echo " --no-dashboard Disable web dashboard"
1222
1388
  echo " --api Start dashboard API server alongside the build"
1223
1389
  echo " --sandbox Run in Docker sandbox (default: off; requires Docker)"
1390
+ echo " --isolation LEVEL Per-run isolation: none (default) | worktree | docker."
1391
+ echo " Unified dial; --parallel = worktree, --sandbox = docker."
1224
1392
  echo " --skip-memory Skip loading memory context at startup"
1225
1393
  echo " --fresh-prd Regenerate the PRD from the codebase on a no-PRD run"
1226
1394
  echo " (ignores the reusable generated PRD; aliases: --regen-prd,"
@@ -1385,6 +1553,34 @@ cmd_start() {
1385
1553
  export LOKI_SANDBOX_MODE=true
1386
1554
  shift
1387
1555
  ;;
1556
+ --isolation|--isolation=*)
1557
+ # Unified per-run isolation dial (rec #4). ONE canonical knob that
1558
+ # subsumes the fragmented --parallel (worktree) and --sandbox
1559
+ # (docker) flags, matching the zeroshot none/worktree/docker model
1560
+ # while keeping every existing flag working as an alias:
1561
+ # none -> default (in-place; exactly today's behavior)
1562
+ # worktree -> git-worktree isolation (same mechanism as --parallel)
1563
+ # docker -> Docker sandbox (same mechanism as --sandbox)
1564
+ # Fail-closed on an unknown value: never silently downgrade to a
1565
+ # weaker isolation than the operator asked for.
1566
+ local _iso_val
1567
+ if [[ "$1" == --isolation=* ]]; then
1568
+ _iso_val="${1#--isolation=}"; shift
1569
+ else
1570
+ _iso_val="${2:-}"; shift 2 2>/dev/null || shift
1571
+ fi
1572
+ case "$_iso_val" in
1573
+ none|"")
1574
+ : ;; # default: no isolation, in-place
1575
+ worktree|wt)
1576
+ args+=("--parallel") ;;
1577
+ docker|sandbox)
1578
+ export LOKI_SANDBOX_MODE=true ;;
1579
+ *)
1580
+ echo -e "${RED}Error: --isolation must be one of: none, worktree, docker (got '${_iso_val}')${NC}" >&2
1581
+ return 2 ;;
1582
+ esac
1583
+ ;;
1388
1584
  --skip-memory)
1389
1585
  export LOKI_SKIP_MEMORY=true
1390
1586
  shift
@@ -2285,6 +2481,41 @@ cmd_start() {
2285
2481
  loki_project_graph_discover "$_pg_target" || true
2286
2482
  fi
2287
2483
 
2484
+ # S1: detach-to-background by default on an interactive TTY. Fires a rich,
2485
+ # reversible handoff card, then arms the existing --bg path so run.sh
2486
+ # backgrounds the runner (it writes the log + PID file and prints its own
2487
+ # "Running in Background" banner). The HARD gate below keeps every
2488
+ # non-interactive / --bg / --yes / CI caller on the exact pre-existing
2489
+ # foreground exec -- byte-identical to before this change.
2490
+ local _bg_already=""
2491
+ for _a in ${args[@]+"${args[@]}"}; do
2492
+ if [ "$_a" = "--bg" ]; then _bg_already=1; break; fi
2493
+ done
2494
+ if _loki_start_should_handoff "$_bg_already"; then
2495
+ local _spec_label="${prd_file:-codebase (no PRD)}"
2496
+ local _tier_label="${LOKI_COMPLEXITY:-auto}"
2497
+ local _decision
2498
+ _decision="$(_loki_start_handoff "$_spec_label" "$_tier_label" "$effective_provider")"
2499
+ case "$_decision" in
2500
+ cancel)
2501
+ exit 0
2502
+ ;;
2503
+ launch-bg-dashboard|launch-bg)
2504
+ # Both (default) AND Dashboard open the browser -- Both = dashboard
2505
+ # + cockpit, honoring the card's "open dashboard" promise. The
2506
+ # cockpit hint is printed by _loki_start_handoff for the Both path.
2507
+ args+=("--bg")
2508
+ local _du="http://127.0.0.1:${LOKI_DASHBOARD_PORT:-57374}"
2509
+ if command -v open >/dev/null 2>&1; then open "$_du" >/dev/null 2>&1 &
2510
+ elif command -v xdg-open >/dev/null 2>&1; then xdg-open "$_du" >/dev/null 2>&1 & fi
2511
+ ;;
2512
+ launch-bg-watch)
2513
+ # Watch (cockpit only) detaches without opening the browser.
2514
+ args+=("--bg")
2515
+ ;;
2516
+ esac
2517
+ fi
2518
+
2288
2519
  # v7.7.13 fix (user-reported bug): `"${args[@]}"` triggers "unbound
2289
2520
  # variable" under bash 3.2 (macOS default) + `set -u` when args is empty.
2290
2521
  # User report: `loki start` with no PRD on /Users/lokesh/git/anonima
@@ -8514,6 +8745,173 @@ cmd_watch() {
8514
8745
  esac
8515
8746
  }
8516
8747
 
8748
+ #===============================================================================
8749
+ # loki cockpit - Multi-repo live status as a terminal inline image (v7.126.0)
8750
+ #
8751
+ # Renders the Autonomi cockpit frame directly in supported terminals (iTerm2 /
8752
+ # WezTerm / ghostty via the iTerm2 protocol; Kitty via its graphics protocol).
8753
+ # On any unsupported terminal, or when rasterization is unavailable, it falls
8754
+ # back HONESTLY to a compact text summary and points at the browser dashboard.
8755
+ # This never touches `loki watch` (the PRD-file auto-rerun watcher).
8756
+ #===============================================================================
8757
+
8758
+ # Compact honest text summary printed on the fallback path. Reads the same
8759
+ # CockpitState JSON the renderer would have used, so text and image agree.
8760
+ _cockpit_text_summary() {
8761
+ local skill_dir="$1" focus_repo="$2"
8762
+ local state_json
8763
+ state_json="$(cockpit_gather_state "$skill_dir" "$focus_repo" 2>/dev/null || true)"
8764
+ if [ -z "$state_json" ] || ! command -v python3 >/dev/null 2>&1; then
8765
+ echo " (no cockpit state available)"
8766
+ return 0
8767
+ fi
8768
+ LOKI_CK_STATE="$state_json" python3 - <<'PYTXT' 2>/dev/null || echo " (could not render text summary)"
8769
+ import json, os
8770
+ s = json.loads(os.environ.get("LOKI_CK_STATE") or "{}")
8771
+ print(f" Run {s.get('run','?')}")
8772
+ print(f" Iteration {s.get('iteration',0)} Phase {s.get('phase','?')} Verdict {str(s.get('verdict','?')).upper()}")
8773
+ tier = s.get('tier') or '-'
8774
+ prov = s.get('provider') or '-'
8775
+ print(f" Tier {tier} Provider {prov} Budget ${float(s.get('budgetUsd',0)):.2f}")
8776
+ gates = s.get('gates') or []
8777
+ if gates:
8778
+ line = " Gates " + " ".join(f"{g.get('name','?')}={g.get('status','?')}" for g in gates[:8])
8779
+ print(line)
8780
+ council = s.get('council') or []
8781
+ if council:
8782
+ print(" Council " + " ".join(f"{c.get('reviewer','?')}:{c.get('vote','?')}" for c in council[:6]))
8783
+ fleet = s.get('fleet') or []
8784
+ if fleet:
8785
+ print(f" Fleet ({len(fleet)}):")
8786
+ for r in fleet[:8]:
8787
+ mark = "*" if r.get("running") else " "
8788
+ print(f" {mark} {r.get('name','?'):<28} iter {r.get('iteration',0)} {r.get('phase') or r.get('status') or ''}")
8789
+ PYTXT
8790
+ }
8791
+
8792
+ cmd_cockpit() {
8793
+ local protocol="auto"
8794
+ local no_image="0"
8795
+ local run_once="0"
8796
+ local follow="1"
8797
+ local interval="2"
8798
+ local focus_repo=""
8799
+
8800
+ while [[ $# -gt 0 ]]; do
8801
+ case "$1" in
8802
+ --help|-h)
8803
+ echo -e "${BOLD}loki cockpit${NC} - Live multi-repo status as a terminal inline image (v7.126.0)"
8804
+ echo ""
8805
+ echo "Usage: loki cockpit [options]"
8806
+ echo ""
8807
+ echo "Renders the Autonomi cockpit (iteration, phase, tier, provider, gates,"
8808
+ echo "council votes, verdict, budget, and the multi-repo fleet) as an inline"
8809
+ echo "image in supported terminals. Falls back to a text summary + the browser"
8810
+ echo "dashboard on terminals without inline-image support."
8811
+ echo ""
8812
+ echo "Options:"
8813
+ echo " --once Render one frame then exit"
8814
+ echo " --follow Re-render on an interval (default)"
8815
+ echo " --interval N Seconds between frames in --follow (default: 2)"
8816
+ echo " --repo PATH Focus one repo instead of the running fleet head"
8817
+ echo " --protocol P iterm2 | kitty | auto (default: auto-detect)"
8818
+ echo " --no-image Force the text/browser fallback"
8819
+ echo " --help, -h Show this help"
8820
+ echo ""
8821
+ echo "Supported terminals: iTerm2, WezTerm, ghostty (iTerm2 protocol); Kitty."
8822
+ echo "Override detection with LOKI_COCKPIT_PROTOCOL=iterm2|kitty."
8823
+ echo ""
8824
+ echo "Note: this is distinct from 'loki watch', which auto-reruns on PRD edits."
8825
+ return 0
8826
+ ;;
8827
+ --once) run_once="1"; follow="0"; shift ;;
8828
+ --follow) follow="1"; run_once="0"; shift ;;
8829
+ --interval) interval="${2:-2}"; [ $# -ge 2 ] && shift 2 || shift ;;
8830
+ --interval=*) interval="${1#*=}"; shift ;;
8831
+ --repo) focus_repo="${2:-}"; [ $# -ge 2 ] && shift 2 || shift ;;
8832
+ --repo=*) focus_repo="${1#*=}"; shift ;;
8833
+ --protocol) protocol="${2:-auto}"; [ $# -ge 2 ] && shift 2 || shift ;;
8834
+ --protocol=*) protocol="${1#*=}"; shift ;;
8835
+ --no-image) no_image="1"; shift ;;
8836
+ -*)
8837
+ echo -e "${RED}Unknown option: $1${NC}"
8838
+ echo "Run 'loki cockpit --help' for usage."
8839
+ return 1
8840
+ ;;
8841
+ *)
8842
+ if [ -z "$focus_repo" ]; then
8843
+ focus_repo="$1"
8844
+ else
8845
+ echo -e "${RED}Unexpected argument: $1${NC}"
8846
+ return 1
8847
+ fi
8848
+ shift
8849
+ ;;
8850
+ esac
8851
+ done
8852
+
8853
+ # Load the render/gather helpers (pure logic lives in loki-ts).
8854
+ local ts_dir="${_LOKI_SCRIPT_DIR}/../loki-ts"
8855
+ local render_lib="${_LOKI_SCRIPT_DIR}/lib/cockpit-render.sh"
8856
+ if [ -f "$render_lib" ]; then
8857
+ # shellcheck disable=SC1090
8858
+ source "$render_lib"
8859
+ else
8860
+ echo -e "${RED}cockpit render library not found${NC} ($render_lib)" >&2
8861
+ return 1
8862
+ fi
8863
+
8864
+ _cockpit_frame() {
8865
+ local rc=0
8866
+ # cockpit_render prints the terminal image to stdout on success (rc 0),
8867
+ # or exits 3 (fallback) / 2 (bun unavailable). Capture stderr for the
8868
+ # FALLBACK reason without swallowing the image bytes on stdout.
8869
+ cockpit_render "$SKILL_DIR" "$ts_dir" "$protocol" "$no_image" "$focus_repo" 2>/tmp/loki-cockpit-err.$$ || rc=$?
8870
+ if [ "$rc" = "0" ]; then
8871
+ rm -f "/tmp/loki-cockpit-err.$$" 2>/dev/null
8872
+ return 0
8873
+ fi
8874
+ # Honest fallback: text summary + browser dashboard pointer. NEVER claim
8875
+ # an image was rendered here.
8876
+ local reason=""
8877
+ if [ -f "/tmp/loki-cockpit-err.$$" ]; then
8878
+ reason="$(sed -n 's/^FALLBACK\t//p' "/tmp/loki-cockpit-err.$$" | head -1)"
8879
+ rm -f "/tmp/loki-cockpit-err.$$" 2>/dev/null
8880
+ fi
8881
+ [ -z "$reason" ] && reason="inline image unavailable in this terminal"
8882
+ echo ""
8883
+ echo -e "${BOLD}Loki Cockpit${NC} ${DIM}(text fallback: ${reason})${NC}"
8884
+ _cockpit_text_summary "$SKILL_DIR" "$focus_repo"
8885
+ echo ""
8886
+ echo -e " Open the full visual dashboard: ${CYAN}loki dashboard${NC}"
8887
+ return 3
8888
+ }
8889
+
8890
+ if [ "$run_once" = "1" ]; then
8891
+ _cockpit_frame
8892
+ return 0
8893
+ fi
8894
+
8895
+ # --follow (default): re-render until interrupted. On a fallback terminal we
8896
+ # print the summary once (looping text is noise) and stop; on an image
8897
+ # terminal we clear and redraw each interval.
8898
+ local frame_rc=0
8899
+ _cockpit_frame || frame_rc=$?
8900
+ if [ "$frame_rc" != "0" ]; then
8901
+ # Fallback path: no live image loop; the text summary + dashboard hint
8902
+ # were already printed once. Point the user at --follow-capable UIs.
8903
+ return 0
8904
+ fi
8905
+ # Image path: loop until Ctrl-C.
8906
+ trap 'printf "\n"; return 0' INT
8907
+ while :; do
8908
+ sleep "$interval" 2>/dev/null || break
8909
+ printf '\033[2J\033[H' # clear + home before redraw
8910
+ _cockpit_frame || break
8911
+ done
8912
+ return 0
8913
+ }
8914
+
8517
8915
  #===============================================================================
8518
8916
  # loki export - Export session data (v6.0.0)
8519
8917
  #===============================================================================
@@ -17012,6 +17410,9 @@ main() {
17012
17410
  watch)
17013
17411
  cmd_watch "$@"
17014
17412
  ;;
17413
+ cockpit)
17414
+ cmd_cockpit "$@"
17415
+ ;;
17015
17416
  export)
17016
17417
  # CLI consolidation (Phase A): 'export' -> 'report export'.
17017
17418
  _deprecated_alias export "report export" "$@"