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 +1 -0
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/lib/cockpit-render.sh +191 -0
- package/autonomy/loki +401 -0
- 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 +16 -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/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.
|
|
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.
|
|
411
|
+
**v7.126.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.
|
|
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" "$@"
|