loki-mode 7.121.2 → 7.121.4
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/autonomy/app-runner.sh +33 -0
- package/autonomy/lib/contract-scaffold/README.md +83 -0
- package/autonomy/lib/contract-scaffold/generate.mjs +90 -0
- package/autonomy/lib/contract-scaffold/scaffold.sh +147 -0
- package/autonomy/lib/contract-scaffold/templates/server/index.mjs.tmpl +62 -0
- package/autonomy/lib/contract-scaffold/templates/server/package.json.tmpl +10 -0
- package/autonomy/lib/contract-scaffold/templates/ui/ListView.tsx.tmpl +75 -0
- package/autonomy/lib/contract-scaffold/templates/ui/theme.css.tmpl +99 -0
- package/autonomy/lib/functional-verify.py +246 -0
- package/autonomy/lib/proof-generator.py +97 -3
- package/autonomy/lib/scaffold-hook.sh +82 -0
- package/autonomy/run.sh +75 -6
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +2 -2
- 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
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""functional-verify.py -- FV-1: does the built app actually DO what the spec asked?
|
|
3
|
+
|
|
4
|
+
The completion verifier proves code was written + tests/build passed. It does NOT
|
|
5
|
+
prove the spec's BEHAVIORS work: a "waitlist" spec can be built as a static page
|
|
6
|
+
that captures nothing, yet read VERIFIED (measured: ~half of backend-implying
|
|
7
|
+
specs). This harness closes that gap by EXERCISING the running app against
|
|
8
|
+
assertions DERIVED FROM THE SPEC, and reporting an honest functional signal.
|
|
9
|
+
|
|
10
|
+
SCOPE (FV-1, deliberately narrow + honest):
|
|
11
|
+
- It reports a DESCRIPTIVE signal only. It does NOT feed the VERIFIED headline
|
|
12
|
+
(that is FV-2: a founder-gated trust-semantics decision, council-reviewed).
|
|
13
|
+
- It derives HTTP endpoint assertions from the spec (GET/POST/DELETE <path>) and
|
|
14
|
+
runs the CRUD lifecycle they imply against the LIVE app. It never fabricates:
|
|
15
|
+
an endpoint it cannot reach is `inconclusive`, a behavior that failed is
|
|
16
|
+
`failed`, a behavior proven is `passed`. Same inconclusive-never-false moat as
|
|
17
|
+
the rest of the trust layer.
|
|
18
|
+
- It requires the app to already be running (URL passed in) -- app-runner (>=
|
|
19
|
+
v7.121.3, static sites included) starts it; this harness only probes.
|
|
20
|
+
|
|
21
|
+
Output: JSON to stdout -- {spec_behaviors: [...], summary: {passed, failed,
|
|
22
|
+
inconclusive}, functional_status: verified|partial|failed|inconclusive}.
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
import re
|
|
28
|
+
import sys
|
|
29
|
+
import urllib.error
|
|
30
|
+
import urllib.request
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# --- spec -> behavioral assertions ------------------------------------------
|
|
34
|
+
|
|
35
|
+
# Endpoint declarations in a spec/PRD: "GET /api/tasks", "POST /api/tasks",
|
|
36
|
+
# "DELETE /api/tasks/:id". Method + path; path may carry a :param placeholder.
|
|
37
|
+
_ENDPOINT_RE = re.compile(
|
|
38
|
+
r'\b(GET|POST|PUT|PATCH|DELETE)\s+(/[A-Za-z0-9_./:{}-]*)',
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _clean_path(path: str) -> str:
|
|
43
|
+
"""Strip trailing sentence punctuation a spec sentence leaves on a path
|
|
44
|
+
(e.g. "DELETE /api/tasks/:id." -> "/api/tasks/:id"). Without this, a probe
|
|
45
|
+
hits a wrong URL and fabricates a failure on a working app -- a fake-RED in
|
|
46
|
+
the FV layer itself, exactly what this harness exists to prevent."""
|
|
47
|
+
return path.rstrip(".,;:)]}") or path
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def derive_endpoint_assertions(spec_text: str) -> list[dict]:
|
|
51
|
+
"""Extract {method, path} endpoint assertions the spec explicitly names.
|
|
52
|
+
|
|
53
|
+
Deduped, order-preserved. A path with a :param / {param} is a resource route
|
|
54
|
+
(used for the DELETE-a-created-resource lifecycle below)."""
|
|
55
|
+
seen = set()
|
|
56
|
+
out = []
|
|
57
|
+
for m in _ENDPOINT_RE.finditer(spec_text or ""):
|
|
58
|
+
method = m.group(1).upper()
|
|
59
|
+
path = _clean_path(m.group(2))
|
|
60
|
+
key = (method, path)
|
|
61
|
+
if key in seen:
|
|
62
|
+
continue
|
|
63
|
+
seen.add(key)
|
|
64
|
+
out.append({"method": method, "path": path,
|
|
65
|
+
"is_resource": bool(re.search(r'[:{]', path))})
|
|
66
|
+
return out
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# --- run assertions against the live app ------------------------------------
|
|
70
|
+
|
|
71
|
+
def _req(base_url: str, method: str, path: str, body=None, timeout=8):
|
|
72
|
+
"""One HTTP call. Returns (status_code, json_or_text) or (None, error_str)."""
|
|
73
|
+
url = base_url.rstrip("/") + path
|
|
74
|
+
data = None
|
|
75
|
+
headers = {}
|
|
76
|
+
if body is not None:
|
|
77
|
+
data = json.dumps(body).encode()
|
|
78
|
+
headers["Content-Type"] = "application/json"
|
|
79
|
+
req = urllib.request.Request(url, data=data, method=method, headers=headers)
|
|
80
|
+
try:
|
|
81
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
82
|
+
raw = resp.read().decode("utf-8", "replace")
|
|
83
|
+
try:
|
|
84
|
+
return resp.status, json.loads(raw) if raw else None
|
|
85
|
+
except json.JSONDecodeError:
|
|
86
|
+
return resp.status, raw
|
|
87
|
+
except urllib.error.HTTPError as e:
|
|
88
|
+
return e.code, None
|
|
89
|
+
except (urllib.error.URLError, OSError) as e:
|
|
90
|
+
return None, str(e)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def verify_crud_lifecycle(base_url: str, assertions: list[dict]) -> list[dict]:
|
|
94
|
+
"""The core functional check: for a REST resource the spec names, prove the
|
|
95
|
+
CRUD LIFECYCLE actually works end to end (not just that a route responds):
|
|
96
|
+
POST creates -> the created item appears on GET -> DELETE removes it ->
|
|
97
|
+
it is gone from a subsequent GET.
|
|
98
|
+
This is what distinguishes a working backend from a static shell: a static
|
|
99
|
+
page returns 200 on GET but a POST does not persist. Each behavior is
|
|
100
|
+
passed / failed / inconclusive, never fabricated."""
|
|
101
|
+
results = []
|
|
102
|
+
|
|
103
|
+
# Collection GET/POST endpoints (non-resource) and their resource DELETE.
|
|
104
|
+
gets = [a for a in assertions if a["method"] == "GET" and not a["is_resource"]]
|
|
105
|
+
posts = [a for a in assertions if a["method"] == "POST" and not a["is_resource"]]
|
|
106
|
+
deletes = [a for a in assertions if a["method"] == "DELETE" and a["is_resource"]]
|
|
107
|
+
|
|
108
|
+
# 1. Every named collection GET must actually respond 2xx.
|
|
109
|
+
for a in gets:
|
|
110
|
+
code, payload = _req(base_url, "GET", a["path"])
|
|
111
|
+
if code is None:
|
|
112
|
+
results.append({"behavior": f"GET {a['path']} responds",
|
|
113
|
+
"status": "inconclusive", "detail": f"unreachable: {payload}"})
|
|
114
|
+
elif 200 <= code < 300:
|
|
115
|
+
results.append({"behavior": f"GET {a['path']} responds",
|
|
116
|
+
"status": "passed", "detail": f"HTTP {code}"})
|
|
117
|
+
else:
|
|
118
|
+
results.append({"behavior": f"GET {a['path']} responds",
|
|
119
|
+
"status": "failed", "detail": f"HTTP {code}"})
|
|
120
|
+
|
|
121
|
+
# 2. POST-creates-and-persists lifecycle: for each collection POST that has a
|
|
122
|
+
# matching collection GET, POST an item and assert the collection grew.
|
|
123
|
+
for p in posts:
|
|
124
|
+
coll = p["path"]
|
|
125
|
+
matching_get = next((g for g in gets if g["path"] == coll), None)
|
|
126
|
+
if matching_get is None:
|
|
127
|
+
results.append({"behavior": f"POST {coll} persists (verifiable)",
|
|
128
|
+
"status": "inconclusive",
|
|
129
|
+
"detail": "no matching collection GET to confirm persistence"})
|
|
130
|
+
continue
|
|
131
|
+
before_code, before = _req(base_url, "GET", coll)
|
|
132
|
+
before_n = len(before) if isinstance(before, list) else None
|
|
133
|
+
# A minimal, generic payload. Real apps validate; we send a common shape.
|
|
134
|
+
post_code, created = _req(base_url, "POST", coll,
|
|
135
|
+
body={"title": "fv-probe", "name": "fv-probe",
|
|
136
|
+
"text": "fv-probe"})
|
|
137
|
+
after_code, after = _req(base_url, "GET", coll)
|
|
138
|
+
after_n = len(after) if isinstance(after, list) else None
|
|
139
|
+
if post_code is None or after_code is None:
|
|
140
|
+
results.append({"behavior": f"POST {coll} persists",
|
|
141
|
+
"status": "inconclusive",
|
|
142
|
+
"detail": "app unreachable during lifecycle"})
|
|
143
|
+
elif post_code and 200 <= post_code < 300 and before_n is not None \
|
|
144
|
+
and after_n is not None and after_n > before_n:
|
|
145
|
+
results.append({"behavior": f"POST {coll} persists",
|
|
146
|
+
"status": "passed",
|
|
147
|
+
"detail": f"collection grew {before_n} -> {after_n} after POST"})
|
|
148
|
+
elif post_code and 200 <= post_code < 300 and (before_n is None or after_n is None):
|
|
149
|
+
# POST accepted but the collection is not a JSON array we can count:
|
|
150
|
+
# we cannot PROVE persistence -> honest inconclusive, never a pass.
|
|
151
|
+
results.append({"behavior": f"POST {coll} persists",
|
|
152
|
+
"status": "inconclusive",
|
|
153
|
+
"detail": f"POST HTTP {post_code} but GET is not a countable list"})
|
|
154
|
+
elif post_code in (400, 422):
|
|
155
|
+
# The endpoint EXISTS and rejected OUR probe payload (validation).
|
|
156
|
+
# That is a working backend with a schema our generic {title,name,text}
|
|
157
|
+
# did not satisfy -- NOT proof the behavior is broken. Marking it failed
|
|
158
|
+
# would be a fake-RED (a strict-but-working app read as non-working).
|
|
159
|
+
# Honest inconclusive; FV-2 must never wire a failed verdict off this.
|
|
160
|
+
results.append({"behavior": f"POST {coll} persists",
|
|
161
|
+
"status": "inconclusive",
|
|
162
|
+
"detail": f"POST HTTP {post_code}: endpoint exists but rejected the "
|
|
163
|
+
"probe payload (schema mismatch, not proof of no-persistence)"})
|
|
164
|
+
else:
|
|
165
|
+
# POST 404 (endpoint absent), 501 (not implemented), or 2xx-but-the-
|
|
166
|
+
# collection did NOT grow -> the behavior the spec implies does NOT
|
|
167
|
+
# work (a static shell or a non-persisting backend). Genuine failed.
|
|
168
|
+
results.append({"behavior": f"POST {coll} persists",
|
|
169
|
+
"status": "failed",
|
|
170
|
+
"detail": f"POST HTTP {post_code}; collection {before_n} -> {after_n} "
|
|
171
|
+
"(no persistence -- a static shell or non-working backend)"})
|
|
172
|
+
|
|
173
|
+
# 3. DELETE lifecycle: if the spec names a resource DELETE and we created
|
|
174
|
+
# an item with an id, delete it and assert it's gone.
|
|
175
|
+
created_id = None
|
|
176
|
+
if isinstance(created, dict):
|
|
177
|
+
created_id = created.get("id") or created.get("_id")
|
|
178
|
+
if deletes and created_id is not None:
|
|
179
|
+
dpath_tmpl = deletes[0]["path"]
|
|
180
|
+
dpath = re.sub(r'[:{][A-Za-z0-9_]+[}]?', str(created_id), dpath_tmpl)
|
|
181
|
+
del_code, _ = _req(base_url, "DELETE", dpath)
|
|
182
|
+
_, after_del = _req(base_url, "GET", coll)
|
|
183
|
+
still_there = isinstance(after_del, list) and any(
|
|
184
|
+
isinstance(x, dict) and (x.get("id") == created_id or x.get("_id") == created_id)
|
|
185
|
+
for x in after_del)
|
|
186
|
+
if del_code is None:
|
|
187
|
+
results.append({"behavior": f"DELETE {dpath_tmpl} removes",
|
|
188
|
+
"status": "inconclusive", "detail": "unreachable"})
|
|
189
|
+
elif 200 <= del_code < 300 and not still_there:
|
|
190
|
+
results.append({"behavior": f"DELETE {dpath_tmpl} removes",
|
|
191
|
+
"status": "passed",
|
|
192
|
+
"detail": f"item {created_id} gone after DELETE (HTTP {del_code})"})
|
|
193
|
+
else:
|
|
194
|
+
results.append({"behavior": f"DELETE {dpath_tmpl} removes",
|
|
195
|
+
"status": "failed",
|
|
196
|
+
"detail": f"DELETE HTTP {del_code}; item still present={still_there}"})
|
|
197
|
+
return results
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def summarize(results: list[dict]) -> dict:
|
|
201
|
+
passed = sum(1 for r in results if r["status"] == "passed")
|
|
202
|
+
failed = sum(1 for r in results if r["status"] == "failed")
|
|
203
|
+
inconclusive = sum(1 for r in results if r["status"] == "inconclusive")
|
|
204
|
+
# functional_status: verified only if >=1 behavior passed and NONE failed;
|
|
205
|
+
# failed if any behavior failed; inconclusive if nothing could be exercised.
|
|
206
|
+
if failed > 0:
|
|
207
|
+
status = "failed"
|
|
208
|
+
elif passed > 0 and inconclusive == 0:
|
|
209
|
+
status = "verified"
|
|
210
|
+
elif passed > 0:
|
|
211
|
+
status = "partial"
|
|
212
|
+
else:
|
|
213
|
+
status = "inconclusive"
|
|
214
|
+
return {"passed": passed, "failed": failed, "inconclusive": inconclusive,
|
|
215
|
+
"functional_status": status}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def run(spec_text: str, base_url: str) -> dict:
|
|
219
|
+
assertions = derive_endpoint_assertions(spec_text)
|
|
220
|
+
if not assertions:
|
|
221
|
+
return {"spec_behaviors": [], "summary": {"passed": 0, "failed": 0,
|
|
222
|
+
"inconclusive": 0, "functional_status": "inconclusive"},
|
|
223
|
+
"note": "no HTTP endpoint behaviors derivable from the spec"}
|
|
224
|
+
results = verify_crud_lifecycle(base_url, assertions)
|
|
225
|
+
summ = summarize(results)
|
|
226
|
+
return {"assertions_derived": assertions, "spec_behaviors": results,
|
|
227
|
+
"summary": summ, "functional_status": summ["functional_status"]}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def main(argv):
|
|
231
|
+
if len(argv) < 3:
|
|
232
|
+
print("usage: functional-verify.py <spec_file> <base_url>", file=sys.stderr)
|
|
233
|
+
return 2
|
|
234
|
+
spec_file, base_url = argv[1], argv[2]
|
|
235
|
+
try:
|
|
236
|
+
with open(spec_file, errors="replace") as f:
|
|
237
|
+
spec_text = f.read()
|
|
238
|
+
except OSError as e:
|
|
239
|
+
print(json.dumps({"error": f"cannot read spec: {e}"}))
|
|
240
|
+
return 1
|
|
241
|
+
print(json.dumps(run(spec_text, base_url), indent=2))
|
|
242
|
+
return 0
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
if __name__ == "__main__":
|
|
246
|
+
sys.exit(main(sys.argv))
|
|
@@ -287,9 +287,22 @@ def _collect_quality_gates(loki_dir):
|
|
|
287
287
|
elif os.path.exists(result_json):
|
|
288
288
|
rj = _read_json(result_json, default=None)
|
|
289
289
|
if isinstance(rj, dict):
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
290
|
+
# Read the marker's outcome key. enforce_static_analysis writes
|
|
291
|
+
# `"pass"` (a bool); other markers may write `"passed"` or
|
|
292
|
+
# `"status"`. Try all three so a real result is NEVER misread as
|
|
293
|
+
# not_run: a failing static-analysis marker ({"pass":false,
|
|
294
|
+
# "findings":11}) was collapsing to not_run (the reader looked
|
|
295
|
+
# only for "passed"/"status"), understating a real gate FAILURE
|
|
296
|
+
# as "did not run" -- the receipt read "gaps" where it should
|
|
297
|
+
# read a failed gate. _norm_gate_status maps a bool correctly
|
|
298
|
+
# (True->passed, False->failed). A key that is genuinely absent
|
|
299
|
+
# still defaults to not_run (honest -- never fabricated passed).
|
|
300
|
+
if "pass" in rj:
|
|
301
|
+
status = _norm_gate_status(rj.get("pass"))
|
|
302
|
+
else:
|
|
303
|
+
status = _norm_gate_status(
|
|
304
|
+
rj.get("passed", rj.get("status", "not_run"))
|
|
305
|
+
)
|
|
293
306
|
if status is not None:
|
|
294
307
|
gates.append({"name": gate_name, "status": status})
|
|
295
308
|
seen.add(gate_name)
|
|
@@ -431,6 +444,64 @@ def _norm_tests_status(raw):
|
|
|
431
444
|
return s
|
|
432
445
|
|
|
433
446
|
|
|
447
|
+
def _collect_functional(loki_dir):
|
|
448
|
+
"""Read .loki/quality/functional-results.json (the FV-1 functional harness).
|
|
449
|
+
|
|
450
|
+
Deterministic FACT: did the built app actually DO what the spec asked (run the
|
|
451
|
+
app + exercise spec-derived behaviors -- POST persists, GET reflects, ...), as
|
|
452
|
+
opposed to just compiling and passing unit tests. Tolerates an absent file ->
|
|
453
|
+
status not_run. Shape mirrors the FV-1 harness output:
|
|
454
|
+
{ran, functional_status, passed, failed, inconclusive}.
|
|
455
|
+
|
|
456
|
+
DESCRIPTIVE ONLY (FV-2, record half): this fact is RECORDED on the receipt but
|
|
457
|
+
is deliberately NOT read by _compute_headline / _compute_degraded, so it does
|
|
458
|
+
NOT change what "Verified" means. Making functional-satisfaction gate the green
|
|
459
|
+
headline is a trust-semantics product decision (council + founder), the second
|
|
460
|
+
half of FV-2. Recording it first lets the signal be seen and validated safely.
|
|
461
|
+
"""
|
|
462
|
+
out = {"ran": False, "functional_status": "not_run",
|
|
463
|
+
"passed": 0, "failed": 0, "inconclusive": 0}
|
|
464
|
+
raw = _read_json(
|
|
465
|
+
os.path.join(loki_dir, "quality", "functional-results.json"), default=None
|
|
466
|
+
)
|
|
467
|
+
if not isinstance(raw, dict):
|
|
468
|
+
return out
|
|
469
|
+
out["ran"] = True
|
|
470
|
+
out["functional_status"] = str(raw.get("functional_status") or "inconclusive")
|
|
471
|
+
summary = raw.get("summary") if isinstance(raw.get("summary"), dict) else raw
|
|
472
|
+
for k in ("passed", "failed", "inconclusive"):
|
|
473
|
+
v = summary.get(k)
|
|
474
|
+
if isinstance(v, int):
|
|
475
|
+
out[k] = v
|
|
476
|
+
return out
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _collect_healthcheck(loki_dir):
|
|
480
|
+
"""Read .loki/app-runner/health.json (the app-runner liveness probe).
|
|
481
|
+
|
|
482
|
+
Deterministic FACT: did the built app actually come up and respond (HTTP/PID
|
|
483
|
+
health), as written by app-runner. Absent -> not_run. Shape:
|
|
484
|
+
{ran, ok, status, checked_at}. status: not_run (never checked) | healthy
|
|
485
|
+
(ran, ok:true) | unhealthy (ran, ok:false).
|
|
486
|
+
|
|
487
|
+
DESCRIPTIVE ONLY (Evidence Receipt record half): recorded for transparency,
|
|
488
|
+
NOT read by _compute_headline / _compute_degraded, so it does not change what
|
|
489
|
+
"Verified" means. Gating on it is the founder-gated trust decision (mirrors the
|
|
490
|
+
FV-2 opt-in gate). ponytail: reuses the health.json app-runner already writes.
|
|
491
|
+
"""
|
|
492
|
+
out = {"ran": False, "ok": False, "status": "not_run", "checked_at": ""}
|
|
493
|
+
raw = _read_json(
|
|
494
|
+
os.path.join(loki_dir, "app-runner", "health.json"), default=None
|
|
495
|
+
)
|
|
496
|
+
if not isinstance(raw, dict):
|
|
497
|
+
return out
|
|
498
|
+
out["ran"] = True
|
|
499
|
+
out["ok"] = bool(raw.get("ok"))
|
|
500
|
+
out["checked_at"] = str(raw.get("checked_at") or "")
|
|
501
|
+
out["status"] = "healthy" if out["ok"] else "unhealthy"
|
|
502
|
+
return out
|
|
503
|
+
|
|
504
|
+
|
|
434
505
|
def _collect_tests(loki_dir):
|
|
435
506
|
"""Read .loki/quality/test-results.json.
|
|
436
507
|
|
|
@@ -763,6 +834,8 @@ def _build_proof(args, loki_dir, target_dir, repo_root):
|
|
|
763
834
|
build = _collect_build(loki_dir)
|
|
764
835
|
tests = _collect_tests(loki_dir)
|
|
765
836
|
security = _collect_security(loki_dir)
|
|
837
|
+
functional = _collect_functional(loki_dir) # FV-2 record-half: descriptive only
|
|
838
|
+
healthcheck = _collect_healthcheck(loki_dir) # Evidence Receipt record-half
|
|
766
839
|
evidence_gate = _collect_evidence_gate(loki_dir)
|
|
767
840
|
|
|
768
841
|
deployed_url = os.environ.get("LOKI_DEPLOYED_URL") or None
|
|
@@ -800,6 +873,14 @@ def _build_proof(args, loki_dir, target_dir, repo_root):
|
|
|
800
873
|
for g in (quality_gates.get("gates") or [])
|
|
801
874
|
],
|
|
802
875
|
"security": security,
|
|
876
|
+
# FV-2 (record half): did the app actually DO what the spec asked? Present
|
|
877
|
+
# for transparency; DELIBERATELY NOT read by _compute_headline /
|
|
878
|
+
# _compute_degraded, so it does not (yet) change the verdict. Wiring it into
|
|
879
|
+
# the green headline is the founder-gated trust-semantics decision.
|
|
880
|
+
"functional": functional,
|
|
881
|
+
# Evidence Receipt (record half): did the built app come up + respond?
|
|
882
|
+
# Descriptive; NOT read by _compute_headline (gating is founder-gated).
|
|
883
|
+
"healthcheck": healthcheck,
|
|
803
884
|
"cost": cost,
|
|
804
885
|
"meta": {
|
|
805
886
|
"run_id": run_id,
|
|
@@ -964,12 +1045,25 @@ def _compute_headline(facts, degraded):
|
|
|
964
1045
|
# intent). This keeps the receipt honest about security, not just tests.
|
|
965
1046
|
sec = facts.get("security") or {}
|
|
966
1047
|
sec_high = bool(sec.get("ran") and (sec.get("high_active") or 0) > 0)
|
|
1048
|
+
# FV-2 gate (opt-in via LOKI_FV_GATE=1, default OFF -> headline unchanged). When
|
|
1049
|
+
# enabled, a functional check that RAN and FAILED (the built app does not do what
|
|
1050
|
+
# the spec asked -- a static shell for a backend spec) is a hard failure, same
|
|
1051
|
+
# class as a failed test. Default-off keeps every existing build's verdict
|
|
1052
|
+
# byte-identical; the founder flips it on after reviewing the reclassification.
|
|
1053
|
+
# ponytail: reads the already-recorded functional fact; no new plumbing.
|
|
1054
|
+
fn = facts.get("functional") or {}
|
|
1055
|
+
fn_failed = bool(
|
|
1056
|
+
os.environ.get("LOKI_FV_GATE") == "1"
|
|
1057
|
+
and fn.get("ran")
|
|
1058
|
+
and fn.get("functional_status") == "failed"
|
|
1059
|
+
)
|
|
967
1060
|
any_failed = (
|
|
968
1061
|
tests.get("status") == "failed"
|
|
969
1062
|
or build.get("status") == "failed"
|
|
970
1063
|
or any(g.get("status") == "failed"
|
|
971
1064
|
for g in (facts.get("quality_gates") or []))
|
|
972
1065
|
or sec_high
|
|
1066
|
+
or fn_failed
|
|
973
1067
|
)
|
|
974
1068
|
if any_failed:
|
|
975
1069
|
return "NOT VERIFIED"
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scaffold-hook.sh -- opt-in seam that wires the contract-first scaffold (M1-M3)
|
|
3
|
+
# into the build lane. Mirrors the LOKI_SENTRUX_GATE pattern: sourced ONLY when
|
|
4
|
+
# the gate is enabled, and no-ops safely otherwise.
|
|
5
|
+
#
|
|
6
|
+
# ENABLED by LOKI_SCAFFOLD_CONTRACT_FIRST=1 (default OFF -> the default build path
|
|
7
|
+
# is byte-identical and completely unaffected). When ON, and ONLY for a GREENFIELD
|
|
8
|
+
# target (an effectively-empty project dir) whose PRD implies a web/API backend,
|
|
9
|
+
# it lays down the contract-first + real-backend + design-system skeleton BEFORE
|
|
10
|
+
# the SDLC loop, so the model builds ON a real wired substrate instead of
|
|
11
|
+
# hand-writing disconnected files. It NEVER touches a non-empty/brownfield repo.
|
|
12
|
+
#
|
|
13
|
+
# It is deliberately conservative: on any doubt it does nothing (returns 0) and
|
|
14
|
+
# lets the normal build proceed -- it can only ADD a starting skeleton, never
|
|
15
|
+
# remove or overwrite existing code.
|
|
16
|
+
#
|
|
17
|
+
# PARITY: this is a bash-only ORCHESTRATION seam in run_autonomous, NOT part of
|
|
18
|
+
# the byte-mirrored build_prompt() contract (build_prompt.ts is unchanged). It
|
|
19
|
+
# mirrors the LOKI_SENTRUX_GATE precedent, which is likewise bash-orchestration
|
|
20
|
+
# only (not behavior-mirrored in the bun runner). Because it is DEFAULT-OFF, the
|
|
21
|
+
# bun default path is unaffected. When the bun runner (loki-ts autonomous.ts,
|
|
22
|
+
# currently a skeleton port) reaches run_autonomous behavior parity, it should
|
|
23
|
+
# mirror this opt-in gate.
|
|
24
|
+
|
|
25
|
+
# Is the target dir greenfield (no meaningful source yet)? Ignores .loki, .git,
|
|
26
|
+
# the PRD, and dotfiles. Echoes "yes"/"no".
|
|
27
|
+
_scaffold_is_greenfield() {
|
|
28
|
+
local dir="${1:-.}"
|
|
29
|
+
local n
|
|
30
|
+
n=$(find "$dir" -type f \
|
|
31
|
+
-not -path "*/.loki/*" -not -path "*/.git/*" \
|
|
32
|
+
-not -name "PRD.md" -not -name "prd.md" -not -name ".*" \
|
|
33
|
+
-not -path "*/node_modules/*" 2>/dev/null | head -5 | wc -l | tr -d ' ')
|
|
34
|
+
[ "${n:-0}" -eq 0 ] && echo "yes" || echo "no"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Derive a resource + fields from a PRD, conservatively. Only fires when the PRD
|
|
38
|
+
# clearly names a REST resource; otherwise echoes nothing (caller skips).
|
|
39
|
+
# Kept intentionally simple + general (no product-specific knowledge).
|
|
40
|
+
_scaffold_derive_resource() {
|
|
41
|
+
local prd="$1"
|
|
42
|
+
[ -f "$prd" ] || return 0
|
|
43
|
+
# Look for an explicit "GET/POST /api/<collection>" the way FV-1 does.
|
|
44
|
+
local coll
|
|
45
|
+
coll=$(grep -oiE '(GET|POST)[[:space:]]+/api/[a-z][a-z0-9_-]+' "$prd" 2>/dev/null \
|
|
46
|
+
| grep -oE '/api/[a-z][a-z0-9_-]+' | head -1 | sed 's#/api/##')
|
|
47
|
+
[ -z "$coll" ] && return 0
|
|
48
|
+
# singularize naively for the resource name
|
|
49
|
+
local res="${coll%s}"; [ -z "$res" ] && res="$coll"
|
|
50
|
+
echo "$res"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# The hook. Args: <target_dir> <prd_path>. Safe to call unconditionally; it
|
|
54
|
+
# self-gates on the flag + greenfield + a derivable resource.
|
|
55
|
+
run_contract_scaffold_hook() {
|
|
56
|
+
[ "${LOKI_SCAFFOLD_CONTRACT_FIRST:-0}" = "1" ] || return 0
|
|
57
|
+
local dir="${1:-.}" prd="${2:-}"
|
|
58
|
+
local script_dir
|
|
59
|
+
script_dir="${SCRIPT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
|
60
|
+
|
|
61
|
+
[ "$(_scaffold_is_greenfield "$dir")" = "yes" ] || {
|
|
62
|
+
log_info "contract-scaffold: target is not greenfield -> skipping (never overwrites existing code)" 2>/dev/null || true
|
|
63
|
+
return 0
|
|
64
|
+
}
|
|
65
|
+
local res
|
|
66
|
+
res=$(_scaffold_derive_resource "$prd")
|
|
67
|
+
[ -z "$res" ] && {
|
|
68
|
+
log_info "contract-scaffold: no REST resource derivable from the PRD -> skipping" 2>/dev/null || true
|
|
69
|
+
return 0
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
local scaffold="${script_dir}/lib/contract-scaffold/scaffold.sh"
|
|
73
|
+
local generate="${script_dir}/lib/contract-scaffold/generate.mjs"
|
|
74
|
+
[ -f "$scaffold" ] && [ -f "$generate" ] || return 0
|
|
75
|
+
|
|
76
|
+
log_info "contract-scaffold: greenfield + resource '$res' -> laying down contract-first + backend + design skeleton" 2>/dev/null || true
|
|
77
|
+
bash "$scaffold" "$dir" "$res" 2>/dev/null || true
|
|
78
|
+
if command -v node >/dev/null 2>&1; then
|
|
79
|
+
node "$generate" "$dir" "$res" 2>/dev/null || true
|
|
80
|
+
fi
|
|
81
|
+
return 0
|
|
82
|
+
}
|
package/autonomy/run.sh
CHANGED
|
@@ -8511,6 +8511,30 @@ enforce_build_check() {
|
|
|
8511
8511
|
return 0
|
|
8512
8512
|
}
|
|
8513
8513
|
|
|
8514
|
+
# True when the workspace's package.json declares a non-empty `lint` script, so
|
|
8515
|
+
# the static-analysis gate can run the app's OWN linter (oxlint/eslint/biome/...)
|
|
8516
|
+
# via `npm run lint` rather than only recognizing eslint config files. Pure read
|
|
8517
|
+
# of package.json; returns non-zero on absence / no lint script / unreadable.
|
|
8518
|
+
has_npm_lint_script() {
|
|
8519
|
+
local dir="${1:-.}"
|
|
8520
|
+
local pkg="$dir/package.json"
|
|
8521
|
+
[ -f "$pkg" ] || return 1
|
|
8522
|
+
# Parse with python3 (already a hard dep of the engine) so a `"lint":` inside
|
|
8523
|
+
# a string value or comment cannot yield a false positive; require a real,
|
|
8524
|
+
# non-empty scripts.lint entry.
|
|
8525
|
+
python3 - "$pkg" <<'PYEOF' 2>/dev/null
|
|
8526
|
+
import json, sys
|
|
8527
|
+
try:
|
|
8528
|
+
with open(sys.argv[1]) as fh:
|
|
8529
|
+
data = json.load(fh)
|
|
8530
|
+
except Exception:
|
|
8531
|
+
sys.exit(1)
|
|
8532
|
+
scripts = data.get("scripts") or {}
|
|
8533
|
+
lint = scripts.get("lint")
|
|
8534
|
+
sys.exit(0 if isinstance(lint, str) and lint.strip() else 1)
|
|
8535
|
+
PYEOF
|
|
8536
|
+
}
|
|
8537
|
+
|
|
8514
8538
|
enforce_static_analysis() {
|
|
8515
8539
|
local loki_dir="${TARGET_DIR:-.}/.loki"
|
|
8516
8540
|
local quality_dir="$loki_dir/quality"
|
|
@@ -8539,6 +8563,31 @@ enforce_static_analysis() {
|
|
|
8539
8563
|
done
|
|
8540
8564
|
if [ -n "$abs_files" ]; then
|
|
8541
8565
|
total_checked=$((total_checked + $(echo "$abs_files" | wc -w)))
|
|
8566
|
+
# v7.x (static-analysis honest coverage): ADDITIVELY run the app's OWN
|
|
8567
|
+
# declared `lint` script when present. Generated apps increasingly use
|
|
8568
|
+
# oxlint / biome (not eslint) -- e.g. package.json `"lint": "oxlint"`
|
|
8569
|
+
# with a `.oxlintrc.json` -- which the eslint-config probe below does
|
|
8570
|
+
# not recognize, so the app's real lint check was skipped. Running
|
|
8571
|
+
# `npm run lint` respects whatever linter the app declares. This is
|
|
8572
|
+
# ADDITIVE (not a replacement): we STILL run the eslint/tsc type checks
|
|
8573
|
+
# below, so a lint pass never silences the type-error check for TS apps
|
|
8574
|
+
# (oxlint is not type-aware). rc 127 = the declared linter is not
|
|
8575
|
+
# installed/resolvable -> HONEST skip, never counted as a violation
|
|
8576
|
+
# (rc-only signal; a real lint failure whose OUTPUT mentions "not
|
|
8577
|
+
# found" must still count, so we do NOT grep the message).
|
|
8578
|
+
if has_npm_lint_script "${TARGET_DIR:-.}"; then
|
|
8579
|
+
local lint_out lint_rc=0
|
|
8580
|
+
lint_out=$(cd "${TARGET_DIR:-.}" && npm run --silent lint 2>&1) || lint_rc=$?
|
|
8581
|
+
if [ "$lint_rc" -eq 127 ]; then
|
|
8582
|
+
log_info "Static analysis: app 'lint' script did not resolve a linter (not run, honest skip)"
|
|
8583
|
+
elif [ "$lint_rc" -ne 0 ]; then
|
|
8584
|
+
findings=$((findings + 1))
|
|
8585
|
+
details="${details}Lint (npm run lint): $(echo "$lint_out" | tail -3 | tr '\n' ' '). "
|
|
8586
|
+
fi
|
|
8587
|
+
fi
|
|
8588
|
+
# Type/syntax check path (unchanged contract): eslint when configured,
|
|
8589
|
+
# else tsc project-mode + per-file parse. Runs REGARDLESS of the lint
|
|
8590
|
+
# step above so TS type errors are always caught.
|
|
8542
8591
|
if [ -f "${TARGET_DIR:-.}/.eslintrc.js" ] || [ -f "${TARGET_DIR:-.}/.eslintrc.json" ] || \
|
|
8543
8592
|
[ -f "${TARGET_DIR:-.}/eslint.config.js" ] || [ -f "${TARGET_DIR:-.}/eslint.config.mjs" ]; then
|
|
8544
8593
|
local eslint_out
|
|
@@ -8559,7 +8608,7 @@ enforce_static_analysis() {
|
|
|
8559
8608
|
if [ -f "${TARGET_DIR:-.}/tsconfig.json" ] && command -v tsc &>/dev/null; then
|
|
8560
8609
|
local _has_ts=0
|
|
8561
8610
|
for f in $abs_files; do
|
|
8562
|
-
case "$f" in *.ts|*.tsx) _has_ts=1; break ;; esac
|
|
8611
|
+
case "$f" in *.ts|*.tsx|*.jsx) _has_ts=1; break ;; esac
|
|
8563
8612
|
done
|
|
8564
8613
|
if [ "$_has_ts" -eq 1 ]; then
|
|
8565
8614
|
_ts_project_mode=1
|
|
@@ -8569,7 +8618,7 @@ enforce_static_analysis() {
|
|
|
8569
8618
|
local _changed_ts_errors=""
|
|
8570
8619
|
for f in $js_files; do
|
|
8571
8620
|
case "$f" in
|
|
8572
|
-
*.ts|*.tsx)
|
|
8621
|
+
*.ts|*.tsx|*.jsx)
|
|
8573
8622
|
# tsc emits paths relative to project root with `(line,col):` suffix.
|
|
8574
8623
|
# v7.5.12 Dev11 (R1 MED): use grep -F (literal) so filenames
|
|
8575
8624
|
# containing regex metacharacters cannot cause false positives
|
|
@@ -8591,11 +8640,16 @@ enforce_static_analysis() {
|
|
|
8591
8640
|
fi
|
|
8592
8641
|
fi
|
|
8593
8642
|
for f in $abs_files; do
|
|
8594
|
-
# node --check cannot parse TypeScript / TSX files; it
|
|
8595
|
-
# crashes with ERR_UNKNOWN_FILE_EXTENSION
|
|
8596
|
-
#
|
|
8643
|
+
# node --check cannot parse TypeScript / TSX / JSX files; it
|
|
8644
|
+
# crashes with ERR_UNKNOWN_FILE_EXTENSION (JSX) or a parse error
|
|
8645
|
+
# (TS syntax). Route .ts/.tsx AND .jsx to the JSX-capable tsc
|
|
8646
|
+
# path; only plain .js/.mjs/.cjs go through node --check. Before
|
|
8647
|
+
# the .jsx addition here, every valid .jsx file was falsely
|
|
8648
|
+
# reported as a "Syntax error" (node --check ERR_UNKNOWN_FILE_
|
|
8649
|
+
# EXTENSION) -- a whole class of false static-analysis findings
|
|
8650
|
+
# on React apps.
|
|
8597
8651
|
case "$f" in
|
|
8598
|
-
*.ts|*.tsx)
|
|
8652
|
+
*.ts|*.tsx|*.jsx)
|
|
8599
8653
|
# When tsconfig project-mode handled it above, skip
|
|
8600
8654
|
# the per-file fallback to avoid duplicate / false errors.
|
|
8601
8655
|
if [ "$_ts_project_mode" -eq 1 ]; then
|
|
@@ -16254,6 +16308,21 @@ run_autonomous() {
|
|
|
16254
16308
|
esac
|
|
16255
16309
|
fi
|
|
16256
16310
|
|
|
16311
|
+
# Contract-first scaffold seam (opt-in via LOKI_SCAFFOLD_CONTRACT_FIRST=1,
|
|
16312
|
+
# default OFF -> default build path is byte-identical). Mirrors the
|
|
16313
|
+
# LOKI_SENTRUX_GATE pattern: source the helper only when the gate is enabled,
|
|
16314
|
+
# and it self-gates further on greenfield + a derivable REST resource. It can
|
|
16315
|
+
# ONLY add a starting skeleton (contract-first codegen + real backend + design
|
|
16316
|
+
# system, M1-M3); it never overwrites existing code or touches a brownfield
|
|
16317
|
+
# repo. On any doubt it no-ops and the normal build proceeds unchanged.
|
|
16318
|
+
if [ "${LOKI_SCAFFOLD_CONTRACT_FIRST:-0}" = "1" ]; then
|
|
16319
|
+
# shellcheck disable=SC1090,SC1091
|
|
16320
|
+
source "${SCRIPT_DIR}/lib/scaffold-hook.sh" 2>/dev/null || true
|
|
16321
|
+
if type run_contract_scaffold_hook >/dev/null 2>&1; then
|
|
16322
|
+
run_contract_scaffold_hook "${TARGET_DIR:-.}" "$prd_path" || true
|
|
16323
|
+
fi
|
|
16324
|
+
fi
|
|
16325
|
+
|
|
16257
16326
|
# Auto-detect PRD if not provided
|
|
16258
16327
|
if [ -z "$prd_path" ]; then
|
|
16259
16328
|
log_step "No PRD provided, searching for existing PRD files..."
|
package/dashboard/__init__.py
CHANGED
package/docs/INSTALLATION.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
|
|
4
4
|
|
|
5
|
-
**Version:** v7.121.
|
|
5
|
+
**Version:** v7.121.4
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -396,7 +396,7 @@ provider works inside the container. Provide auth with your Anthropic API key:
|
|
|
396
396
|
# Run Loki Mode in Docker (Claude provider, API-key auth)
|
|
397
397
|
docker run --rm -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
|
|
398
398
|
-v $(pwd):/workspace -w /workspace \
|
|
399
|
-
asklokesh/loki-mode:7.121.
|
|
399
|
+
asklokesh/loki-mode:7.121.4 start ./my-spec.md
|
|
400
400
|
```
|
|
401
401
|
|
|
402
402
|
##### docker compose + .env (no host install)
|
package/loki-ts/dist/loki.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var q8=Object.defineProperty;var J8=($)=>$;function V8($,Q){this[$]=J8.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)q8($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:V8.bind(Q,Z)})};var P=($,Q)=>()=>($&&(Q=$($=0)),Q);var J$=import.meta.require;var m1={};b(m1,{lokiDir:()=>j,homeLokiDir:()=>T$,findRepoRootForVersion:()=>$1,REPO_ROOT:()=>h});import{resolve as t,dirname as e$}from"path";import{fileURLToPath as W8}from"url";import{existsSync as N$}from"fs";import{homedir as H8}from"os";function G8(){let $=v1;for(let Q=0;Q<6;Q++){if(N$(t($,"VERSION"))&&N$(t($,"autonomy/run.sh")))return $;let Z=e$($);if(Z===$)break;$=Z}return t(v1,"..","..","..")}function $1($){let Q=$;for(let Z=0;Z<6;Z++){if(N$(t(Q,"VERSION"))&&N$(t(Q,"autonomy/run.sh")))return Q;let z=e$(Q);if(z===Q)break;Q=z}return t($,"..","..","..")}function j(){return process.env.LOKI_DIR??t(process.cwd(),".loki")}function T$(){return t(H8(),".loki")}var v1,h;var C=P(()=>{v1=e$(W8(import.meta.url));h=G8()});import{readFileSync as U8}from"fs";import{resolve as Y8,dirname as B8}from"path";import{fileURLToPath as M8}from"url";function S$(){if(Q$!==null)return Q$;let $="7.121.
|
|
2
|
+
var q8=Object.defineProperty;var J8=($)=>$;function V8($,Q){this[$]=J8.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)q8($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:V8.bind(Q,Z)})};var P=($,Q)=>()=>($&&(Q=$($=0)),Q);var J$=import.meta.require;var m1={};b(m1,{lokiDir:()=>j,homeLokiDir:()=>T$,findRepoRootForVersion:()=>$1,REPO_ROOT:()=>h});import{resolve as t,dirname as e$}from"path";import{fileURLToPath as W8}from"url";import{existsSync as N$}from"fs";import{homedir as H8}from"os";function G8(){let $=v1;for(let Q=0;Q<6;Q++){if(N$(t($,"VERSION"))&&N$(t($,"autonomy/run.sh")))return $;let Z=e$($);if(Z===$)break;$=Z}return t(v1,"..","..","..")}function $1($){let Q=$;for(let Z=0;Z<6;Z++){if(N$(t(Q,"VERSION"))&&N$(t(Q,"autonomy/run.sh")))return Q;let z=e$(Q);if(z===Q)break;Q=z}return t($,"..","..","..")}function j(){return process.env.LOKI_DIR??t(process.cwd(),".loki")}function T$(){return t(H8(),".loki")}var v1,h;var C=P(()=>{v1=e$(W8(import.meta.url));h=G8()});import{readFileSync as U8}from"fs";import{resolve as Y8,dirname as B8}from"path";import{fileURLToPath as M8}from"url";function S$(){if(Q$!==null)return Q$;let $="7.121.4";if(typeof $==="string"&&$.length>0)return Q$=$,Q$;try{let Q=B8(M8(import.meta.url)),Z=$1(Q);Q$=U8(Y8(Z,"VERSION"),"utf-8").trim()}catch{Q$="unknown"}return Q$}var Q$=null;var Q1=P(()=>{C()});var p1={};b(p1,{runOrThrow:()=>S8,run:()=>R,readStreamCapped:()=>c1,commandVersion:()=>C8,commandExists:()=>u,ShellError:()=>Z1,MAX_STDOUT_BYTES:()=>u1});async function c1($,Q=u1){let Z=$.getReader(),z=new TextDecoder,X="",q=0;try{while(q<Q){let{done:K,value:W}=await Z.read();if(K)break;if(!W)continue;if(q+=W.byteLength,q>Q){let V=W.byteLength-(q-Q);X+=z.decode(W.subarray(0,V),{stream:!0});break}X+=z.decode(W,{stream:!0})}X+=z.decode()}finally{try{await Z.cancel()}catch{}Z.releaseLock()}return X}async function R($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,X;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[q,K,W]=await Promise.all([c1(Z.stdout),new Response(Z.stderr).text(),Z.exited]);return{stdout:q,stderr:K,exitCode:W}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function S8($,Q={}){let Z=await R($,Q);if(Z.exitCode!==0)throw new Z1(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function u($){let Q=D8($),Z=await R(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function D8($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function C8($,Q="--version"){if(!await u($))return null;let z=await R([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var u1=16777216,Z1;var o=P(()=>{Z1=class Z1 extends Error{message;exitCode;stdout;stderr;constructor($,Q,Z,z){super($);this.message=$;this.exitCode=Q;this.stdout=Z;this.stderr=z;this.name="ShellError"}}});function r($){return b8?"":$}var b8,M,S,_,tZ,L,k,y,J;var p=P(()=>{b8=(process.env.NO_COLOR??"").length>0;M=r("\x1B[0;31m"),S=r("\x1B[0;32m"),_=r("\x1B[1;33m"),tZ=r("\x1B[0;34m"),L=r("\x1B[0;36m"),k=r("\x1B[1m"),y=r("\x1B[2m"),J=r("\x1B[0m")});import{existsSync as l8}from"fs";async function Z$(){if(A$!==void 0)return A$;let $="/opt/homebrew/bin/python3.12";if(l8($))return A$=$,$;let Q=await u("python3.12");if(Q)return A$=Q,Q;let Z=await u("python3");return A$=Z,Z}async function z$($,Q={}){let Z=await Z$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return R([Z,"-c",$],Q)}var A$;var V$=P(()=>{o()});var V0={};b(V0,{runStatus:()=>U3});import{existsSync as v,readFileSync as H$,readdirSync as $0,statSync as Q0}from"fs";import{resolve as D,basename as z3}from"path";import{homedir as X3}from"os";function Z0($){let Q=Math.trunc($);if(Q>=1e6)return`${(Math.trunc(Q/1e6*10)/10).toFixed(1)}M`;if(Q>=1000)return`${(Math.trunc(Q/1000*10)/10).toFixed(1)}K`;return String(Q)}function z0($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*C$/Q);if(X>C$)X=C$;let q=C$-X,K=S;if(z>=80)K=M;else if(z>=50)K=_;let W="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),V=Z0($),H=Z0(Q);return` ${k}${Z}${J} ${K}[${W}]${J} ${z}% (${V} / ${H})`}async function q3(){if(await u("jq"))return!0;return process.stdout.write(`${M}Error: jq is required but not installed.${J}
|
|
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)
|
|
@@ -814,4 +814,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
814
814
|
`),2}default:return process.stderr.write(`Unknown command: ${Q}
|
|
815
815
|
`),process.stderr.write(K8),2}}e1();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var SZ=await NZ(Bun.argv.slice(2));process.exit(SZ);
|
|
816
816
|
|
|
817
|
-
//# debugId=
|
|
817
|
+
//# debugId=8F986B79CB65C96964756E2164756E21
|