loki-mode 8.88.0 → 8.89.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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/dashboard/__init__.py +1 -1
- 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/tools/preflight.sh +340 -0
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 v8.
|
|
6
|
+
# Loki Mode v8.89.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -469,4 +469,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
469
469
|
|
|
470
470
|
---
|
|
471
471
|
|
|
472
|
-
**v8.
|
|
472
|
+
**v8.89.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
8.
|
|
1
|
+
8.89.0
|
package/dashboard/__init__.py
CHANGED
package/loki-ts/dist/loki.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var m_=Object.create;var{getPrototypeOf:u_,defineProperty:eK,getOwnPropertyNames:p_}=Object;var d_=Object.prototype.hasOwnProperty;function c_(Z){return this[Z]}var l_,i_,a_=(Z,X,Q)=>{var Y=Z!=null&&typeof Z==="object";if(Y){var J=X?l_??=new WeakMap:i_??=new WeakMap,z=J.get(Z);if(z)return z}Q=Z!=null?m_(u_(Z)):{};let K=X||!Z||!Z.__esModule?eK(Q,"default",{value:Z,enumerable:!0}):Q;for(let $ of p_(Z))if(!d_.call(K,$))eK(K,$,{get:c_.bind(Z,$),enumerable:!0});if(Y)J.set(Z,K);return K};var HQ=(Z,X)=>()=>(X||Z((X={exports:{}}).exports,X),X.exports);var s_=(Z)=>Z;function n_(Z,X){this[Z]=s_.bind(null,X)}var l0=(Z,X)=>{for(var Q in X)eK(Z,Q,{get:X[Q],enumerable:!0,configurable:!0,set:n_.bind(X,Q)})};var p=(Z,X)=>()=>(Z&&(X=Z(Z=0)),X);var e0=import.meta.require;var kO={};l0(kO,{lokiDir:()=>j0,homeLokiDir:()=>R4,findRepoRootForVersion:()=>X$,REPO_ROOT:()=>i0});import{resolve as n7,dirname as Z$}from"path";import{fileURLToPath as o_}from"url";import{existsSync as UQ}from"fs";import{homedir as r_}from"os";function t_(){let Z=RO;for(let X=0;X<6;X++){if(UQ(n7(Z,"VERSION"))&&UQ(n7(Z,"autonomy/run.sh")))return Z;let Q=Z$(Z);if(Q===Z)break;Z=Q}return n7(RO,"..","..","..")}function X$(Z){let X=Z;for(let Q=0;Q<6;Q++){if(UQ(n7(X,"VERSION"))&&UQ(n7(X,"autonomy/run.sh")))return X;let Y=Z$(X);if(Y===X)break;X=Y}return n7(Z,"..","..","..")}function j0(){return process.env.LOKI_DIR??n7(process.cwd(),".loki")}function R4(){return n7(r_(),".loki")}var RO,i0;var H8=p(()=>{RO=Z$(o_(import.meta.url));i0=t_()});import{readFileSync as e_}from"fs";import{resolve as Zf,dirname as Xf}from"path";import{fileURLToPath as Qf}from"url";function h3(){if(h5!==null)return h5;let Z="8.
|
|
2
|
+
var m_=Object.create;var{getPrototypeOf:u_,defineProperty:eK,getOwnPropertyNames:p_}=Object;var d_=Object.prototype.hasOwnProperty;function c_(Z){return this[Z]}var l_,i_,a_=(Z,X,Q)=>{var Y=Z!=null&&typeof Z==="object";if(Y){var J=X?l_??=new WeakMap:i_??=new WeakMap,z=J.get(Z);if(z)return z}Q=Z!=null?m_(u_(Z)):{};let K=X||!Z||!Z.__esModule?eK(Q,"default",{value:Z,enumerable:!0}):Q;for(let $ of p_(Z))if(!d_.call(K,$))eK(K,$,{get:c_.bind(Z,$),enumerable:!0});if(Y)J.set(Z,K);return K};var HQ=(Z,X)=>()=>(X||Z((X={exports:{}}).exports,X),X.exports);var s_=(Z)=>Z;function n_(Z,X){this[Z]=s_.bind(null,X)}var l0=(Z,X)=>{for(var Q in X)eK(Z,Q,{get:X[Q],enumerable:!0,configurable:!0,set:n_.bind(X,Q)})};var p=(Z,X)=>()=>(Z&&(X=Z(Z=0)),X);var e0=import.meta.require;var kO={};l0(kO,{lokiDir:()=>j0,homeLokiDir:()=>R4,findRepoRootForVersion:()=>X$,REPO_ROOT:()=>i0});import{resolve as n7,dirname as Z$}from"path";import{fileURLToPath as o_}from"url";import{existsSync as UQ}from"fs";import{homedir as r_}from"os";function t_(){let Z=RO;for(let X=0;X<6;X++){if(UQ(n7(Z,"VERSION"))&&UQ(n7(Z,"autonomy/run.sh")))return Z;let Q=Z$(Z);if(Q===Z)break;Z=Q}return n7(RO,"..","..","..")}function X$(Z){let X=Z;for(let Q=0;Q<6;Q++){if(UQ(n7(X,"VERSION"))&&UQ(n7(X,"autonomy/run.sh")))return X;let Y=Z$(X);if(Y===X)break;X=Y}return n7(Z,"..","..","..")}function j0(){return process.env.LOKI_DIR??n7(process.cwd(),".loki")}function R4(){return n7(r_(),".loki")}var RO,i0;var H8=p(()=>{RO=Z$(o_(import.meta.url));i0=t_()});import{readFileSync as e_}from"fs";import{resolve as Zf,dirname as Xf}from"path";import{fileURLToPath as Qf}from"url";function h3(){if(h5!==null)return h5;let Z="8.89.0";if(typeof Z==="string"&&Z.length>0)return h5=Z,h5;try{let X=Xf(Qf(import.meta.url)),Q=X$(X);h5=e_(Zf(Q,"VERSION"),"utf-8").trim()}catch{h5="unknown"}return h5}var h5=null;var BQ=p(()=>{H8()});var bO={};l0(bO,{runOrThrow:()=>jf,run:()=>E0,readStreamCapped:()=>NQ,commandVersion:()=>Tf,commandExists:()=>X9,ShellError:()=>Q$,MAX_STDOUT_BYTES:()=>yO});async function NQ(Z,X=yO){let Q=Z.getReader(),Y=new TextDecoder,J="",z=0;try{while(z<X){let{done:K,value:$}=await Q.read();if(K)break;if(!$)continue;if(z+=$.byteLength,z>X){let W=$.byteLength-(z-X);J+=Y.decode($.subarray(0,W),{stream:!0});break}J+=Y.decode($,{stream:!0})}J+=Y.decode()}finally{try{await Q.cancel()}catch{}Q.releaseLock()}return J}async function E0(Z,X={}){let Q=Bun.spawn({cmd:[...Z],stdout:"pipe",stderr:"pipe",env:X.env?{...process.env,...X.env}:process.env,cwd:X.cwd}),Y,J;if(X.timeoutMs&&X.timeoutMs>0)Y=setTimeout(()=>{try{Q.kill("SIGTERM")}catch{}J=setTimeout(()=>{try{Q.kill("SIGKILL")}catch{}},2000)},X.timeoutMs);try{let[z,K,$]=await Promise.all([NQ(Q.stdout),new Response(Q.stderr).text(),Q.exited]);return{stdout:z,stderr:K,exitCode:$}}finally{if(Y)clearTimeout(Y);if(J)clearTimeout(J)}}async function jf(Z,X={}){let Q=await E0(Z,X);if(Q.exitCode!==0)throw new Q$(`command failed (${Q.exitCode}): ${Z.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function X9(Z){let X=Mf(Z),Q=await E0(["sh","-c",`command -v ${X}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function Mf(Z){if(!/^[A-Za-z0-9._/-]+$/.test(Z))throw Error(`refused to shell-escape suspect token: ${Z}`);return Z}async function Tf(Z,X="--version"){if(!await X9(Z))return null;let Y=await E0([Z,X],{timeoutMs:5000});if(Y.exitCode!==0)return null;return((Y.stdout||Y.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var yO=16777216,Q$;var x9=p(()=>{Q$=class Q$ extends Error{message;exitCode;stdout;stderr;constructor(Z,X,Q,Y){super(Z);this.message=Z;this.exitCode=X;this.stdout=Q;this.stderr=Y;this.name="ShellError"}}});function o7(Z){return wf?"":Z}var wf,L0,F8,p0,zV0,a0,W8,Q9,v;var S6=p(()=>{wf=(process.env.NO_COLOR??"").length>0;L0=o7("\x1B[0;31m"),F8=o7("\x1B[0;32m"),p0=o7("\x1B[1;33m"),zV0=o7("\x1B[0;34m"),a0=o7("\x1B[0;36m"),W8=o7("\x1B[1m"),Q9=o7("\x1B[2m"),v=o7("\x1B[0m")});import{existsSync as bf}from"fs";async function E7(){if(x4!==void 0)return x4;let Z="/opt/homebrew/bin/python3.12";if(bf(Z))return x4=Z,Z;let X=await X9("python3.12");if(X)return x4=X,X;let Q=await X9("python3");return x4=Q,Q}async function Y7(Z,X={}){let Q=await E7();if(!Q)return{stdout:"",stderr:"python3 not found",exitCode:127};return E0([Q,"-c",Z],X)}var x4;var r7=p(()=>{x9()});var ZL={};l0(ZL,{runStatus:()=>Kh});import{existsSync as Y9,readFileSync as g3,readdirSync as iO,statSync as aO}from"fs";import{resolve as h8,basename as rf}from"path";import{homedir as tf}from"os";function sO(Z){let X=Math.trunc(Z);if(X>=1e6)return`${(Math.trunc(X/1e6*10)/10).toFixed(1)}M`;if(X>=1000)return`${(Math.trunc(X/1000*10)/10).toFixed(1)}K`;return String(X)}function nO(Z,X,Q){if(X===0)return null;let Y=Math.trunc(Z*100/X),J=Math.trunc(Z*LQ/X);if(J>LQ)J=LQ;let z=LQ-J,K=F8;if(Y>=80)K=L0;else if(Y>=50)K=p0;let $="=".repeat(Math.max(0,J))+" ".repeat(Math.max(0,z)),W=sO(Z),V=sO(X);return` ${W8}${Q}${v} ${K}[${$}]${v} ${Y}% (${W} / ${V})`}async function Zh(){if(await X9("jq"))return!0;return process.stdout.write(`${L0}Error: jq is required but not installed.${v}
|
|
3
3
|
`),process.stdout.write(`Install with:
|
|
4
4
|
`),process.stdout.write(` brew install jq (macOS)
|
|
5
5
|
`),process.stdout.write(` apt install jq (Debian/Ubuntu)
|
|
@@ -1232,4 +1232,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
1232
1232
|
`),2}case"start":{let{runStart:Y}=await Promise.resolve().then(() => (h_(),f_));return Y(Q)}default:return process.stderr.write(`Unknown command: ${X}
|
|
1233
1233
|
`),process.stderr.write(v_),2}}cO();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var uW0=await mW0(Bun.argv.slice(2));process.exit(uW0);
|
|
1234
1234
|
|
|
1235
|
-
//# debugId=
|
|
1235
|
+
//# debugId=C2908F5759B1B75564756E2164756E21
|
package/mcp/__init__.py
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loki-mode",
|
|
3
3
|
"mcpName": "io.github.asklokesh/loki-mode",
|
|
4
|
-
"version": "8.
|
|
4
|
+
"version": "8.89.0",
|
|
5
5
|
"description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 8 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"agent",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "loki-mode",
|
|
4
4
|
"displayName": "Loki Mode",
|
|
5
|
-
"version": "8.
|
|
5
|
+
"version": "8.89.0",
|
|
6
6
|
"description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 8 quality gates, completion council). Ships Loki's spec-hardening, drift-detection, and deterministic PR verification commands plus the Loki MCP server.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Autonomi",
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Will this run succeed, and what will it likely cost? Answered BEFORE the run.
|
|
3
|
+
#
|
|
4
|
+
# WHY THIS EXISTS. The signals that predict a doomed or expensive build already
|
|
5
|
+
# exist, but a user has to know four commands to collect them, and three of the
|
|
6
|
+
# four are easy to reach for in a form that MUTATES. This is one read-only
|
|
7
|
+
# command that composes them into a single verdict.
|
|
8
|
+
#
|
|
9
|
+
# COMPOSE, NEVER REIMPLEMENT. Every number and every fix string below comes out
|
|
10
|
+
# of the surface that owns it. This repo has already paid for the alternative:
|
|
11
|
+
# four separate provider lists drifted apart, and "unmeasured" was rendered as
|
|
12
|
+
# "$0.00" on four surfaces before the estimator centralised the predicate. So:
|
|
13
|
+
#
|
|
14
|
+
# provider selection providers/loader.sh auto_detect_provider (the ONE list)
|
|
15
|
+
# blockers + fixes loki doctor --json (its detail strings, verbatim)
|
|
16
|
+
# resumable run loki next --dry-run (read-only; loki resume DELETES the
|
|
17
|
+
# PAUSE file, so it can never be the seam a preflight uses)
|
|
18
|
+
# projected cost tools/estimate-run.py --json (its notes, verbatim)
|
|
19
|
+
#
|
|
20
|
+
# When a signal is absent this prints UNKNOWN and says why. It never prints 0
|
|
21
|
+
# for something it did not measure, and it never invents the iteration horizon:
|
|
22
|
+
# --iterations is the estimator's REQUIRED input precisely so a guess cannot be
|
|
23
|
+
# laundered through a real per-iteration cost, and passing a default here would
|
|
24
|
+
# reintroduce that on this surface.
|
|
25
|
+
#
|
|
26
|
+
# Read-only: starts nothing, spends nothing, contacts no provider.
|
|
27
|
+
|
|
28
|
+
set -uo pipefail
|
|
29
|
+
|
|
30
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
31
|
+
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
32
|
+
|
|
33
|
+
# ponytail: overridable so the "missing helper degrades to unavailable" path is
|
|
34
|
+
# testable without moving repo files out from under a live install.
|
|
35
|
+
LOKI_PREFLIGHT_DOCTOR="${LOKI_PREFLIGHT_DOCTOR:-$REPO_ROOT/autonomy/loki}"
|
|
36
|
+
LOKI_PREFLIGHT_ESTIMATOR="${LOKI_PREFLIGHT_ESTIMATOR:-$REPO_ROOT/tools/estimate-run.py}"
|
|
37
|
+
LOKI_PREFLIGHT_LOADER="${LOKI_PREFLIGHT_LOADER:-$REPO_ROOT/providers/loader.sh}"
|
|
38
|
+
|
|
39
|
+
BOLD='\033[1m'; RED='\033[0;31m'; GREEN='\033[0;32m'
|
|
40
|
+
YELLOW='\033[1;33m'; DIM='\033[2m'; NC='\033[0m'
|
|
41
|
+
|
|
42
|
+
# timeout(1) is coreutils; on macOS it exists only via homebrew. Hard-coding it
|
|
43
|
+
# made this script exit 127 on a stock mac -- and made the scrubbed-PATH test
|
|
44
|
+
# impossible to run at all. Degrade to running the helper uncapped rather than
|
|
45
|
+
# failing the whole preflight over a missing convenience.
|
|
46
|
+
_run_capped() {
|
|
47
|
+
local secs="$1"; shift
|
|
48
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
49
|
+
timeout "$secs" "$@"
|
|
50
|
+
elif command -v gtimeout >/dev/null 2>&1; then
|
|
51
|
+
gtimeout "$secs" "$@"
|
|
52
|
+
else
|
|
53
|
+
"$@"
|
|
54
|
+
fi
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
ITERATIONS=""
|
|
58
|
+
WORKSPACE="."
|
|
59
|
+
|
|
60
|
+
while [ "$#" -gt 0 ]; do
|
|
61
|
+
case "$1" in
|
|
62
|
+
--iterations)
|
|
63
|
+
# An empty value must ERROR, not silently read as "flag absent" --
|
|
64
|
+
# that would quietly downgrade a requested projection to UNKNOWN and
|
|
65
|
+
# look like missing history rather than a typo.
|
|
66
|
+
if [ -z "${2:-}" ]; then
|
|
67
|
+
echo "preflight: --iterations requires a value" >&2; exit 64
|
|
68
|
+
fi
|
|
69
|
+
ITERATIONS="$2"; shift 2 ;;
|
|
70
|
+
--iterations=*)
|
|
71
|
+
ITERATIONS="${1#*=}"
|
|
72
|
+
if [ -z "$ITERATIONS" ]; then
|
|
73
|
+
echo "preflight: --iterations requires a value" >&2; exit 64
|
|
74
|
+
fi
|
|
75
|
+
shift ;;
|
|
76
|
+
--help|-h)
|
|
77
|
+
echo "loki preflight -- will this run succeed, and what will it cost?"
|
|
78
|
+
echo ""
|
|
79
|
+
echo "Usage: tools/preflight.sh [WORKSPACE] [--iterations N]"
|
|
80
|
+
echo ""
|
|
81
|
+
echo "Read-only. Starts nothing, spends nothing, contacts no provider."
|
|
82
|
+
echo ""
|
|
83
|
+
echo " --iterations N Project cost over N iterations. Without it the"
|
|
84
|
+
echo " measured per-iteration cost is still shown; the"
|
|
85
|
+
echo " total reads UNKNOWN rather than being guessed."
|
|
86
|
+
exit 0 ;;
|
|
87
|
+
-*)
|
|
88
|
+
echo "preflight: unknown option: $1" >&2; exit 64 ;;
|
|
89
|
+
*)
|
|
90
|
+
WORKSPACE="$1"; shift ;;
|
|
91
|
+
esac
|
|
92
|
+
done
|
|
93
|
+
|
|
94
|
+
if [ -n "$ITERATIONS" ] && ! [[ "$ITERATIONS" =~ ^[1-9][0-9]*$ ]]; then
|
|
95
|
+
echo "preflight: --iterations must be a positive integer, got: $ITERATIONS" >&2
|
|
96
|
+
exit 64
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
if [ ! -d "$WORKSPACE" ]; then
|
|
100
|
+
echo "preflight: no such workspace: $WORKSPACE" >&2
|
|
101
|
+
exit 66
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
BLOCKERS=() # each entry: "what blocks|the command that fixes it"
|
|
105
|
+
WARNINGS=()
|
|
106
|
+
|
|
107
|
+
echo -e "${BOLD}Loki preflight${NC} -- $(cd "$WORKSPACE" && pwd)"
|
|
108
|
+
echo -e "${DIM}Read-only: starts nothing, spends nothing, contacts no provider.${NC}"
|
|
109
|
+
echo ""
|
|
110
|
+
|
|
111
|
+
# ---- Provider ---------------------------------------------------------------
|
|
112
|
+
# auto_detect_provider is THE priority list. Restating the order here is how the
|
|
113
|
+
# four-lists-disagree bug happened, so this sources it and asks.
|
|
114
|
+
echo -e "${BOLD}Provider${NC}"
|
|
115
|
+
PROVIDER=""
|
|
116
|
+
if [ -r "$LOKI_PREFLIGHT_LOADER" ]; then
|
|
117
|
+
# shellcheck disable=SC1090
|
|
118
|
+
if source "$LOKI_PREFLIGHT_LOADER" >/dev/null 2>&1 \
|
|
119
|
+
&& declare -f auto_detect_provider >/dev/null 2>&1; then
|
|
120
|
+
PROVIDER="$(auto_detect_provider 2>/dev/null || true)"
|
|
121
|
+
if [ -n "$PROVIDER" ]; then
|
|
122
|
+
echo -e " ${GREEN}OK${NC} auto-detection would select: ${BOLD}${PROVIDER}${NC}"
|
|
123
|
+
else
|
|
124
|
+
echo -e " ${RED}FAIL${NC} no provider CLI installed"
|
|
125
|
+
fi
|
|
126
|
+
else
|
|
127
|
+
echo -e " ${YELLOW}UNKNOWN${NC} provider loader unreadable -- selection not determined"
|
|
128
|
+
WARNINGS+=("provider selection unavailable: could not load $LOKI_PREFLIGHT_LOADER")
|
|
129
|
+
fi
|
|
130
|
+
else
|
|
131
|
+
echo -e " ${YELLOW}UNKNOWN${NC} provider loader missing -- selection not determined"
|
|
132
|
+
WARNINGS+=("provider selection unavailable: $LOKI_PREFLIGHT_LOADER not found")
|
|
133
|
+
fi
|
|
134
|
+
echo ""
|
|
135
|
+
|
|
136
|
+
# ---- Doctor blockers --------------------------------------------------------
|
|
137
|
+
# --json only. The TEXT path backgrounds a first-run-blocked telemetry emit on
|
|
138
|
+
# failure, which a read-only preflight must not trigger.
|
|
139
|
+
echo -e "${BOLD}Environment${NC}"
|
|
140
|
+
DOCTOR_JSON=""
|
|
141
|
+
if [ -x "$LOKI_PREFLIGHT_DOCTOR" ] || [ -r "$LOKI_PREFLIGHT_DOCTOR" ]; then
|
|
142
|
+
DOCTOR_JSON="$(cd "$WORKSPACE" && _run_capped 120 bash "$LOKI_PREFLIGHT_DOCTOR" doctor --json 2>/dev/null || true)"
|
|
143
|
+
fi
|
|
144
|
+
if [ -z "$DOCTOR_JSON" ]; then
|
|
145
|
+
echo -e " ${YELLOW}UNKNOWN${NC} doctor unavailable -- environment blockers not checked"
|
|
146
|
+
WARNINGS+=("environment check unavailable: could not run '$LOKI_PREFLIGHT_DOCTOR doctor --json'")
|
|
147
|
+
else
|
|
148
|
+
# Surface doctor's OWN blocker text. Re-authoring the fix line is how the
|
|
149
|
+
# advice drifts from the thing that actually installs the dependency.
|
|
150
|
+
DOCTOR_REPORT="$(printf '%s' "$DOCTOR_JSON" | python3 -c '
|
|
151
|
+
import json, sys
|
|
152
|
+
try:
|
|
153
|
+
d = json.load(sys.stdin)
|
|
154
|
+
except Exception:
|
|
155
|
+
sys.exit(3)
|
|
156
|
+
lines = []
|
|
157
|
+
for c in d.get("checks") or []:
|
|
158
|
+
if c.get("status") == "fail":
|
|
159
|
+
name = c.get("name") or c.get("command") or "check"
|
|
160
|
+
cmd = c.get("command") or ""
|
|
161
|
+
lines.append("BLOCK\t%s is missing or too old\tInstall %s, then re-run: loki doctor" % (name, cmd or name))
|
|
162
|
+
ai = d.get("ai_provider") or {}
|
|
163
|
+
if ai.get("status") == "fail":
|
|
164
|
+
detail = ai.get("detail") or "No AI provider CLI."
|
|
165
|
+
what, _, fix = detail.partition("Fix:")
|
|
166
|
+
lines.append("BLOCK\t%s\t%s" % (what.strip().rstrip(".") or "No AI provider CLI",
|
|
167
|
+
fix.strip() or "install a provider CLI"))
|
|
168
|
+
disk = d.get("disk") or {}
|
|
169
|
+
if disk.get("status") == "fail":
|
|
170
|
+
lines.append("BLOCK\tLow disk space (%s GB available)\tFree up disk, then re-run: loki doctor"
|
|
171
|
+
% disk.get("available_gb"))
|
|
172
|
+
s = d.get("summary") or {}
|
|
173
|
+
lines.append("SUMMARY\t%s\t%s" % (s.get("passed"), s.get("warnings")))
|
|
174
|
+
print("\n".join(lines))
|
|
175
|
+
' 2>/dev/null || true)"
|
|
176
|
+
|
|
177
|
+
if [ -z "$DOCTOR_REPORT" ]; then
|
|
178
|
+
echo -e " ${YELLOW}UNKNOWN${NC} doctor output unparseable -- environment blockers not checked"
|
|
179
|
+
WARNINGS+=("environment check unavailable: doctor --json produced no readable result")
|
|
180
|
+
else
|
|
181
|
+
_env_blocks=0
|
|
182
|
+
while IFS=$'\t' read -r kind a b; do
|
|
183
|
+
case "$kind" in
|
|
184
|
+
BLOCK)
|
|
185
|
+
# THE TWO LISTS DISAGREE, and preflight is the first surface
|
|
186
|
+
# that shows both at once. auto_detect_provider iterates
|
|
187
|
+
# claude cline codex aider opencode; doctor's ai_provider
|
|
188
|
+
# checks only the first four. On an opencode-only machine
|
|
189
|
+
# this printed "would select: opencode" directly above
|
|
190
|
+
# "No AI provider CLI" -- a self-contradicting report.
|
|
191
|
+
# Neither producer is editable from here, so the
|
|
192
|
+
# disagreement is NAMED rather than silently resolved:
|
|
193
|
+
# asserting either side is right would be a fifth opinion
|
|
194
|
+
# about which providers count, which is how the four lists
|
|
195
|
+
# drifted apart in the first place.
|
|
196
|
+
if [ "$a" = "No AI provider CLI" ] && [ -n "$PROVIDER" ]; then
|
|
197
|
+
echo -e " ${YELLOW}WARN${NC} doctor reports no provider, but auto-detection selected '${PROVIDER}'"
|
|
198
|
+
echo -e " ${DIM} doctor does not recognise '${PROVIDER}'. Unresolved here: these two${NC}"
|
|
199
|
+
echo -e " ${DIM} checks disagree, so provider readiness is UNCERTAIN, not confirmed.${NC}"
|
|
200
|
+
WARNINGS+=("provider readiness uncertain: auto-detection selected '$PROVIDER' but doctor does not count it as a provider")
|
|
201
|
+
else
|
|
202
|
+
echo -e " ${RED}FAIL${NC} $a"
|
|
203
|
+
BLOCKERS+=("$a|$b")
|
|
204
|
+
_env_blocks=$((_env_blocks + 1))
|
|
205
|
+
fi ;;
|
|
206
|
+
SUMMARY)
|
|
207
|
+
if [ "$_env_blocks" -eq 0 ]; then
|
|
208
|
+
echo -e " ${GREEN}OK${NC} doctor: $a checks passed, $b warning(s)"
|
|
209
|
+
fi ;;
|
|
210
|
+
esac
|
|
211
|
+
done <<< "$DOCTOR_REPORT"
|
|
212
|
+
fi
|
|
213
|
+
fi
|
|
214
|
+
|
|
215
|
+
# A provider is required to run at all. Doctor reports it too, so only add this
|
|
216
|
+
# when doctor could not be consulted -- otherwise the same blocker lists twice.
|
|
217
|
+
if [ -z "$PROVIDER" ] && [ -r "$LOKI_PREFLIGHT_LOADER" ] && [ -z "$DOCTOR_JSON" ]; then
|
|
218
|
+
BLOCKERS+=("No AI provider CLI|npm install -g @anthropic-ai/claude-code")
|
|
219
|
+
fi
|
|
220
|
+
echo ""
|
|
221
|
+
|
|
222
|
+
# ---- Resumable run ----------------------------------------------------------
|
|
223
|
+
# loki next --dry-run resolves the saved run status and prints the exact
|
|
224
|
+
# continuation command WITHOUT acting. loki resume would rm the PAUSE file.
|
|
225
|
+
echo -e "${BOLD}Saved state${NC}"
|
|
226
|
+
if [ ! -d "$WORKSPACE/.loki" ]; then
|
|
227
|
+
echo -e " ${DIM}no .loki/ here -- this would be a fresh run${NC}"
|
|
228
|
+
elif [ -z "$DOCTOR_JSON" ] && [ ! -r "$LOKI_PREFLIGHT_DOCTOR" ]; then
|
|
229
|
+
echo -e " ${YELLOW}UNKNOWN${NC} cannot resolve saved run state (loki CLI unavailable)"
|
|
230
|
+
WARNINGS+=("saved-state check unavailable: $LOKI_PREFLIGHT_DOCTOR not found")
|
|
231
|
+
else
|
|
232
|
+
NEXT_OUT="$(cd "$WORKSPACE" && _run_capped 60 bash "$LOKI_PREFLIGHT_DOCTOR" next --dry-run 2>/dev/null || true)"
|
|
233
|
+
if [ -z "$NEXT_OUT" ]; then
|
|
234
|
+
echo -e " ${DIM}no resumable run found${NC}"
|
|
235
|
+
else
|
|
236
|
+
_outcome="$(printf '%s' "$NEXT_OUT" | sed -n 's/.*Last outcome *: *//p' | head -1)"
|
|
237
|
+
_will="$(printf '%s' "$NEXT_OUT" | sed -n 's/.*Will run *: *//p' | head -1)"
|
|
238
|
+
if [ -n "$_outcome" ]; then
|
|
239
|
+
echo -e " ${YELLOW}FOUND${NC} interrupted or paused run present (last outcome: ${BOLD}${_outcome}${NC})"
|
|
240
|
+
[ -n "$_will" ] && echo -e " continue it with: ${BOLD}${_will}${NC}"
|
|
241
|
+
WARNINGS+=("a resumable run exists here; starting fresh would leave it behind")
|
|
242
|
+
else
|
|
243
|
+
echo -e " ${DIM}no resumable run found${NC}"
|
|
244
|
+
fi
|
|
245
|
+
fi
|
|
246
|
+
fi
|
|
247
|
+
echo ""
|
|
248
|
+
|
|
249
|
+
# ---- Projected cost ---------------------------------------------------------
|
|
250
|
+
echo -e "${BOLD}Projected cost${NC}"
|
|
251
|
+
if [ ! -r "$LOKI_PREFLIGHT_ESTIMATOR" ]; then
|
|
252
|
+
echo -e " ${YELLOW}UNKNOWN${NC} estimator unavailable -- no cost projection"
|
|
253
|
+
WARNINGS+=("cost projection unavailable: $LOKI_PREFLIGHT_ESTIMATOR not found")
|
|
254
|
+
else
|
|
255
|
+
# --iterations is REQUIRED by the estimator by design. Passing 1 here just to
|
|
256
|
+
# satisfy it and then scaling would be the guess it refuses to make, so when
|
|
257
|
+
# the user gave none we ask for the per-iteration basis only and report the
|
|
258
|
+
# total as UNKNOWN.
|
|
259
|
+
_est_iters="${ITERATIONS:-1}"
|
|
260
|
+
EST_JSON="$(_run_capped 60 python3 "$LOKI_PREFLIGHT_ESTIMATOR" --iterations "$_est_iters" "$WORKSPACE" --json 2>/dev/null || true)"
|
|
261
|
+
if [ -z "$EST_JSON" ]; then
|
|
262
|
+
echo -e " ${YELLOW}UNKNOWN${NC} estimator produced no result -- no cost projection"
|
|
263
|
+
WARNINGS+=("cost projection unavailable: estimator did not return a result")
|
|
264
|
+
else
|
|
265
|
+
# The estimator's own notes carry the "not \$0.00" wording. Printed
|
|
266
|
+
# verbatim so this surface cannot drift from the rule it inherits.
|
|
267
|
+
printf '%s' "$EST_JSON" | LOKI_PF_ITERS="${ITERATIONS:-}" python3 -c '
|
|
268
|
+
import json, os, sys
|
|
269
|
+
try:
|
|
270
|
+
d = json.load(sys.stdin)
|
|
271
|
+
except Exception:
|
|
272
|
+
print(" UNKNOWN estimator output unreadable -- no cost projection")
|
|
273
|
+
sys.exit(0)
|
|
274
|
+
iters = os.environ.get("LOKI_PF_ITERS") or ""
|
|
275
|
+
if not d.get("has_basis"):
|
|
276
|
+
print(" UNKNOWN no measured, priced iteration in this workspace")
|
|
277
|
+
for n in d.get("notes") or []:
|
|
278
|
+
print(" " + n)
|
|
279
|
+
else:
|
|
280
|
+
med = d.get("median_cost_per_iteration_usd")
|
|
281
|
+
print(" measured per-iteration cost: $%.4f (median of %d priced iteration(s))"
|
|
282
|
+
% (med, d.get("basis_count") or 0))
|
|
283
|
+
if d.get("single_point_basis"):
|
|
284
|
+
print(" basis is a SINGLE data point -- arithmetically fine,")
|
|
285
|
+
print(" epistemically weak; treat the total as indicative only")
|
|
286
|
+
if iters:
|
|
287
|
+
proj = d.get("projected_cost_usd")
|
|
288
|
+
# Explicit None check. "or 0.0" would render an absent projection as
|
|
289
|
+
# $0.00 -- the exact defect this surface exists to refuse, and it would
|
|
290
|
+
# slip in on the one branch that prints a real dollar figure.
|
|
291
|
+
if proj is None:
|
|
292
|
+
print(" projected total: UNKNOWN -- estimator returned no projection")
|
|
293
|
+
else:
|
|
294
|
+
print(" projected for %s iteration(s): $%.2f [ESTIMATE, not a guarantee]"
|
|
295
|
+
% (iters, proj))
|
|
296
|
+
else:
|
|
297
|
+
print(" projected total: UNKNOWN -- pass --iterations N to project a total")
|
|
298
|
+
print(" (the horizon is not guessed: an invented N would launder")
|
|
299
|
+
print(" a guess through a real per-iteration cost)")
|
|
300
|
+
'
|
|
301
|
+
fi
|
|
302
|
+
fi
|
|
303
|
+
echo ""
|
|
304
|
+
|
|
305
|
+
# ---- Verdict ----------------------------------------------------------------
|
|
306
|
+
echo -e "${BOLD}========================================${NC}"
|
|
307
|
+
if [ "${#BLOCKERS[@]}" -gt 0 ]; then
|
|
308
|
+
echo -e "${RED}${BOLD}VERDICT: BLOCKED${NC}"
|
|
309
|
+
echo ""
|
|
310
|
+
echo "This run cannot succeed until these are fixed:"
|
|
311
|
+
for b in "${BLOCKERS[@]}"; do
|
|
312
|
+
echo -e " - ${BOLD}${b%%|*}${NC}"
|
|
313
|
+
echo -e " fix: ${b#*|}"
|
|
314
|
+
done
|
|
315
|
+
# Warnings are printed under BLOCKED too. Showing them only on the READY
|
|
316
|
+
# paths meant a blocked run silently DROPPED every uncertain signal -- the
|
|
317
|
+
# user fixes the named blocker, re-runs, and only then discovers a second
|
|
318
|
+
# problem that was already known. An unmeasured signal must not become
|
|
319
|
+
# invisible just because something else failed first.
|
|
320
|
+
if [ "${#WARNINGS[@]}" -gt 0 ]; then
|
|
321
|
+
echo ""
|
|
322
|
+
echo "Also uncertain (not blocking):"
|
|
323
|
+
for w in "${WARNINGS[@]}"; do
|
|
324
|
+
echo " - $w"
|
|
325
|
+
done
|
|
326
|
+
fi
|
|
327
|
+
echo ""
|
|
328
|
+
echo "Then re-run: tools/preflight.sh"
|
|
329
|
+
exit 1
|
|
330
|
+
elif [ "${#WARNINGS[@]}" -gt 0 ]; then
|
|
331
|
+
echo -e "${YELLOW}${BOLD}VERDICT: READY WITH WARNINGS${NC}"
|
|
332
|
+
echo ""
|
|
333
|
+
for w in "${WARNINGS[@]}"; do
|
|
334
|
+
echo " - $w"
|
|
335
|
+
done
|
|
336
|
+
exit 0
|
|
337
|
+
else
|
|
338
|
+
echo -e "${GREEN}${BOLD}VERDICT: READY${NC}"
|
|
339
|
+
exit 0
|
|
340
|
+
fi
|