loki-mode 7.125.0 → 7.127.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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.125.0
6
+ # Loki Mode v7.127.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.125.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
411
+ **v7.127.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.125.0
1
+ 7.127.0
@@ -0,0 +1,358 @@
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
+ # runner/scanner names the tool; summary is the one-line evidence.
74
+ runner = g.get("runner") or g.get("scanner") or ""
75
+ gates.append({
76
+ "name": g.get("gate", "gate"),
77
+ "status": status,
78
+ "runner": str(runner)[:24],
79
+ "evidence": str(g.get("summary", ""))[:80],
80
+ })
81
+
82
+ # Default model tier per reviewer (matches the standing council roster).
83
+ REVIEWER_TIER = {
84
+ "architecture-strategist": "opus",
85
+ "maintainer-mergeability": "opus",
86
+ "security-sentinel": "opus",
87
+ "test-coverage-auditor": "sonnet",
88
+ "performance-oracle": "opus",
89
+ "dependency-analyst": "sonnet",
90
+ }
91
+
92
+ # Council votes: .loki/council/*.json, each with a vote-ish field. Skips the
93
+ # aggregate state.json (no per-reviewer vote) so only real reviewer entries
94
+ # populate the strip.
95
+ council = []
96
+ cdir = os.path.join(loki, "council")
97
+ try:
98
+ for fn in sorted(os.listdir(cdir)):
99
+ if not fn.endswith(".json") or fn == "state.json":
100
+ continue
101
+ cj = read_json(os.path.join(cdir, fn))
102
+ raw = str(cj.get("vote") or cj.get("verdict") or "").lower()
103
+ vote = "pending"
104
+ if "approve" in raw:
105
+ vote = "approve"
106
+ elif "reject" in raw:
107
+ vote = "reject"
108
+ elif "concern" in raw:
109
+ vote = "concern"
110
+ reviewer = cj.get("reviewer") or cj.get("name") or os.path.splitext(fn)[0]
111
+ reviewer = str(reviewer)
112
+ tier = cj.get("tier") or cj.get("model_tier") or REVIEWER_TIER.get(reviewer, "")
113
+ council.append({"reviewer": reviewer[:24], "vote": vote, "tier": str(tier)[:8]})
114
+ except Exception:
115
+ pass
116
+
117
+ return {
118
+ "iteration": int(iteration) if isinstance(iteration, (int, float)) else 0,
119
+ "phase": phase or "idle",
120
+ "verdict": verdict,
121
+ "gates": gates,
122
+ "council": council,
123
+ "budgetUsd": float(st.get("cost_usd", 0.0) or 0.0),
124
+ "tier": st.get("tier", "") or "",
125
+ "provider": st.get("provider", "") or "",
126
+ }
127
+
128
+
129
+ # Fleet from the registry; graceful empty on any failure.
130
+ fleet = []
131
+ runs = []
132
+ try:
133
+ from dashboard import registry
134
+ runs = registry.get_fleet_runs(include_inactive=True)
135
+ except Exception:
136
+ runs = []
137
+
138
+ for r in runs:
139
+ fleet.append({
140
+ "name": r.get("name") or os.path.basename(r.get("path", "") or "project"),
141
+ "path": r.get("path", "") or "",
142
+ "phase": r.get("phase", "") or "",
143
+ "iteration": r.get("iteration", 0) or 0,
144
+ "status": r.get("status", "") or "",
145
+ "running": bool(r.get("running")),
146
+ })
147
+
148
+ # Focused run: --repo wins; else first running fleet run; else cwd.
149
+ focus_path = focus or cwd
150
+ focus_entry = None
151
+ for r in runs:
152
+ rp = os.path.abspath(r.get("path", "") or "")
153
+ if rp and rp == os.path.abspath(focus_path):
154
+ focus_entry = r
155
+ break
156
+ if focus_entry is None and not focus:
157
+ for r in runs:
158
+ if r.get("running"):
159
+ focus_entry = r
160
+ focus_path = r.get("path", "") or cwd
161
+ break
162
+
163
+ ps = project_state(focus_path)
164
+ run_name = (focus_entry or {}).get("name") if focus_entry else os.path.basename(os.path.abspath(focus_path))
165
+
166
+ # Registry values override file reads where present (registry is fresher).
167
+ if focus_entry:
168
+ if focus_entry.get("iteration"):
169
+ ps["iteration"] = focus_entry["iteration"]
170
+ if focus_entry.get("phase"):
171
+ ps["phase"] = focus_entry["phase"]
172
+ if focus_entry.get("cost_usd"):
173
+ ps["budgetUsd"] = float(focus_entry["cost_usd"])
174
+
175
+ state = {
176
+ "run": run_name or "cockpit",
177
+ "iteration": ps["iteration"],
178
+ "phase": ps["phase"],
179
+ "tier": ps["tier"] or (os.environ.get("LOKI_SESSION_MODEL", "")),
180
+ "provider": ps["provider"] or (os.environ.get("LOKI_PROVIDER", "claude")),
181
+ "verdict": ps["verdict"],
182
+ "budgetUsd": ps["budgetUsd"],
183
+ "budgetLimitUsd": float(os.environ.get("LOKI_MAX_BUDGET_USD", "0") or 0) or None,
184
+ "freshness": "now",
185
+ "gates": ps["gates"],
186
+ "council": ps["council"],
187
+ "fleet": fleet,
188
+ }
189
+ print(json.dumps(state))
190
+ PYCOCKPIT
191
+ }
192
+
193
+ # cockpit_render <skill_dir> <ts_dir> <protocol> <no_image> <focus_repo>
194
+ # Returns: 0 (image emitted to stdout), 3 (fallback), or 2 (bun unavailable).
195
+ # On fallback / bun-missing the caller is responsible for the text summary.
196
+ cockpit_render() {
197
+ local skill_dir="$1" ts_dir="$2" protocol="${3:-auto}" no_image="${4:-0}" focus_repo="${5:-}"
198
+
199
+ # Prefer the shipped bundle (dist/cockpit.js -- present in npm/Docker/brew
200
+ # installs, which ship loki-ts/dist/ but not src/); fall back to the source
201
+ # entry for in-repo development.
202
+ local entry="$ts_dir/dist/cockpit.js"
203
+ [ -f "$entry" ] || entry="$ts_dir/src/cockpit/cli.ts"
204
+ if ! command -v bun >/dev/null 2>&1 || [ ! -f "$entry" ]; then
205
+ return 2
206
+ fi
207
+
208
+ local args=(--protocol "$protocol")
209
+ [ "$no_image" = "1" ] && args+=(--no-image)
210
+
211
+ # Gather -> render. Preserve the TS exit code (0 image / 3 fallback).
212
+ cockpit_gather_state "$skill_dir" "$focus_repo" | bun "$entry" "${args[@]}"
213
+ }
214
+
215
+ #===============================================================================
216
+ # E3: keyboard interactivity for `loki cockpit --follow`.
217
+ # These live here (sourced by cmd_cockpit) so they can be extracted + unit
218
+ # tested without pulling in the whole autonomy/loki CLI.
219
+ #===============================================================================
220
+
221
+ # Pure key -> action mapping. Takes a normalized key token, echoes one action
222
+ # word. Side-effect-free so it is trivially unit-testable.
223
+ # TAB / RIGHT / DOWN -> next (cycle focus forward across the fleet)
224
+ # LEFT / UP -> prev (cycle focus backward)
225
+ # e -> explain (print verify evidence for the focused run)
226
+ # s -> steer (print how to steer the focused run)
227
+ # q / ESC / Ctrl-C -> quit
228
+ # anything else -> noop
229
+ cockpit_handle_key() {
230
+ case "$1" in
231
+ TAB|RIGHT|DOWN) echo "next" ;;
232
+ LEFT|UP) echo "prev" ;;
233
+ e|E) echo "explain" ;;
234
+ s|S) echo "steer" ;;
235
+ q|Q|ESC|$'\x03') echo "quit" ;;
236
+ *) echo "noop" ;;
237
+ esac
238
+ }
239
+
240
+ # Read one keypress (non-blocking, no redraw stall) and normalize it to a token
241
+ # cockpit_handle_key understands. Echoes the token, or nothing if no key was
242
+ # pressed within the timeout. Arrow keys arrive as ESC [ A/B/C/D; a lone ESC (no
243
+ # following bytes) normalizes to ESC (quit). Only usable on a TTY.
244
+ # $1 = timeout seconds (fractional ok; feeds `read -t`)
245
+ cockpit_read_key() {
246
+ local timeout="${1:-2}"
247
+ local k rest
248
+ IFS= read -rsn1 -t "$timeout" k 2>/dev/null || return 1
249
+ case "$k" in
250
+ $'\t') echo "TAB"; return 0 ;;
251
+ $'\x03') echo "ESC"; return 0 ;; # Ctrl-C byte if trapped as data
252
+ $'\x1b')
253
+ # Possible escape sequence: grab up to 2 more bytes without blocking.
254
+ IFS= read -rsn2 -t 0.01 rest 2>/dev/null || rest=""
255
+ case "$rest" in
256
+ '[A') echo "UP" ;;
257
+ '[B') echo "DOWN" ;;
258
+ '[C') echo "RIGHT" ;;
259
+ '[D') echo "LEFT" ;;
260
+ *) echo "ESC" ;;
261
+ esac
262
+ return 0 ;;
263
+ '') return 1 ;; # timed out, no key
264
+ *) echo "$k"; return 0 ;;
265
+ esac
266
+ }
267
+
268
+ # Echo the fleet repo paths (one per line) so the follow loop can cycle focus.
269
+ # Empty output = no fleet (cycling is a no-op). Best-effort, never errors.
270
+ cockpit_fleet_paths() {
271
+ local skill_dir="$1"
272
+ local state_json
273
+ state_json="$(cockpit_gather_state "$skill_dir" "" 2>/dev/null || true)"
274
+ [ -z "$state_json" ] && return 0
275
+ command -v python3 >/dev/null 2>&1 || return 0
276
+ LOKI_CK_STATE="$state_json" python3 - <<'PYPATHS' 2>/dev/null || true
277
+ import json, os
278
+ s = json.loads(os.environ.get("LOKI_CK_STATE") or "{}")
279
+ for r in s.get("fleet") or []:
280
+ p = r.get("path") or ""
281
+ if p:
282
+ print(p)
283
+ PYPATHS
284
+ }
285
+
286
+ # Given the current focus path, a direction (next|prev), and a newline list of
287
+ # fleet paths, echo the path to focus after cycling. Wraps around. If focus is
288
+ # not in the list, next -> first entry, prev -> last entry. Empty list echoes the
289
+ # current focus unchanged. Pure, unit-testable.
290
+ # $1 = current focus path (may be empty) $2 = next|prev $3 = newline paths
291
+ cockpit_cycle_focus() {
292
+ local cur="$1" dir="$2" paths="$3"
293
+ [ -z "$paths" ] && { echo "$cur"; return 0; }
294
+ local -a arr=()
295
+ local line
296
+ while IFS= read -r line; do
297
+ [ -n "$line" ] && arr+=("$line")
298
+ done <<< "$paths"
299
+ local n="${#arr[@]}"
300
+ [ "$n" -eq 0 ] && { echo "$cur"; return 0; }
301
+ # Find current index.
302
+ local idx=-1 i
303
+ for i in "${!arr[@]}"; do
304
+ if [ "${arr[$i]}" = "$cur" ]; then idx="$i"; break; fi
305
+ done
306
+ local target
307
+ if [ "$idx" -lt 0 ]; then
308
+ # Not in list: next -> first, prev -> last.
309
+ if [ "$dir" = "prev" ]; then target=$((n - 1)); else target=0; fi
310
+ elif [ "$dir" = "prev" ]; then
311
+ target=$(( (idx - 1 + n) % n ))
312
+ else
313
+ target=$(( (idx + 1) % n ))
314
+ fi
315
+ echo "${arr[$target]}"
316
+ }
317
+
318
+ # Print the verify evidence ("explain") for the focused run. Reads the run's
319
+ # .loki/verify/evidence.json directly. Never fabricates: says so if absent.
320
+ cockpit_explain_focus() {
321
+ local focus_repo="$1"
322
+ local repo="${focus_repo:-$(pwd)}"
323
+ local ev="$repo/.loki/verify/evidence.json"
324
+ printf '\n'
325
+ printf 'verify --explain (%s)\n' "$repo"
326
+ if [ ! -f "$ev" ] || ! command -v python3 >/dev/null 2>&1; then
327
+ echo " (no evidence.json for this run yet)"
328
+ return 0
329
+ fi
330
+ LOKI_CK_EV="$ev" python3 - <<'PYEV' 2>/dev/null || echo " (could not read evidence)"
331
+ import json, os
332
+ try:
333
+ ev = json.load(open(os.environ["LOKI_CK_EV"]))
334
+ except Exception:
335
+ print(" (could not read evidence)"); raise SystemExit(0)
336
+ print(f" Verdict {str(ev.get('verdict','?')).upper()}")
337
+ checks = ev.get("deterministic_gates") or ev.get("checks") or ev.get("gates") or []
338
+ for c in checks[:12]:
339
+ name = c.get("gate") or c.get("name") or c.get("id") or "?"
340
+ status = c.get("status") or c.get("result") or "?"
341
+ print(f" {name:<28} {status}")
342
+ reason = ev.get("reason") or ev.get("summary")
343
+ if reason:
344
+ print(f" Reason {reason}")
345
+ PYEV
346
+ }
347
+
348
+ # Print a steering hint for the focused run. Steering is opt-in prompt injection;
349
+ # this only tells the operator how, it does not inject anything.
350
+ cockpit_steer_hint() {
351
+ local focus_repo="$1"
352
+ local repo="${focus_repo:-$(pwd)}"
353
+ printf '\n'
354
+ echo "Steer this run"
355
+ echo " Drop a note the next iteration will read:"
356
+ echo " echo 'your guidance' > $repo/.loki/steering.md"
357
+ echo " Or open the dashboard chat: loki dashboard"
358
+ }