loki-mode 7.87.0 → 7.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/autonomy/crash.sh +57 -30
- package/autonomy/lib/own-render.py +617 -0
- package/autonomy/lib/proof-generator.py +27 -1
- package/autonomy/lib/wiki-generator.py +192 -4
- package/autonomy/loki +183 -36
- package/autonomy/run.sh +419 -34
- package/autonomy/spec-interrogation.sh +51 -5
- package/autonomy/telemetry.sh +89 -12
- package/bin/loki +89 -8
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +213 -0
- package/dashboard/static/assets/mermaid.min.js +2030 -0
- package/dashboard/static/index.html +314 -182
- package/dashboard/telemetry.py +62 -15
- package/docs/INSTALLATION.md +2 -2
- package/docs/PRIVACY.md +65 -38
- package/loki-ts/dist/loki.js +249 -243
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Finish-and-own renderer for Loki Mode (Loop 5, v7.88.0).
|
|
3
|
+
|
|
4
|
+
A PURE render layer over EXISTING honest data. It reads the artifacts the
|
|
5
|
+
runner already wrote (the Evidence Receipt proof.json, completion.json,
|
|
6
|
+
USAGE.md, the app-runner state, the assumptions ledger) and restates them in
|
|
7
|
+
plain English for a NON-technical founder.
|
|
8
|
+
|
|
9
|
+
Design rules (LOOP5-FINISH-AND-OWN-PLAN.md):
|
|
10
|
+
- NEVER recompute or fabricate. Every line maps to a real artifact value.
|
|
11
|
+
- The "Is it working?" verdict is taken VERBATIM from honesty.headline.
|
|
12
|
+
Green ("ready") is gated on headline == "VERIFIED" AND tests passed AND
|
|
13
|
+
the build ran. This honesty gate is the core of the lib.
|
|
14
|
+
- Tolerant of missing artifacts: each one absent is marked honestly rather
|
|
15
|
+
than guessed at.
|
|
16
|
+
- No LLM call here (keep it pure / deterministic / testable). The "What you
|
|
17
|
+
have now" paragraph is a deterministic template over the recorded brief +
|
|
18
|
+
diff stat. No invented features.
|
|
19
|
+
- Exit 0 always: this is a report, never a gate.
|
|
20
|
+
|
|
21
|
+
CLI:
|
|
22
|
+
python3 autonomy/lib/own-render.py [--loki-dir .loki] [--md|--json]
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import argparse
|
|
26
|
+
import json
|
|
27
|
+
import os
|
|
28
|
+
import sys
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# tolerant readers
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
def _read_json(path, default=None):
|
|
36
|
+
try:
|
|
37
|
+
with open(path, "r") as f:
|
|
38
|
+
return json.load(f)
|
|
39
|
+
except Exception:
|
|
40
|
+
return default
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _read_text(path, default=""):
|
|
44
|
+
try:
|
|
45
|
+
with open(path, "r", errors="replace") as f:
|
|
46
|
+
return f.read()
|
|
47
|
+
except Exception:
|
|
48
|
+
return default
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _latest_proof(loki_dir):
|
|
52
|
+
"""Return (run_id, proof_dict) for the most recent proof, or (None, None).
|
|
53
|
+
|
|
54
|
+
Proof run dirs are named with a timestamp prefix (YYYYmmddTHHMMSSZ-...),
|
|
55
|
+
so a lexicographic sort puts the newest last. We pick the newest dir that
|
|
56
|
+
actually contains a readable proof.json.
|
|
57
|
+
"""
|
|
58
|
+
proofs_dir = os.path.join(loki_dir, "proofs")
|
|
59
|
+
try:
|
|
60
|
+
entries = sorted(os.listdir(proofs_dir))
|
|
61
|
+
except Exception:
|
|
62
|
+
return None, None
|
|
63
|
+
for run_id in reversed(entries):
|
|
64
|
+
proof = _read_json(os.path.join(proofs_dir, run_id, "proof.json"))
|
|
65
|
+
if isinstance(proof, dict):
|
|
66
|
+
return run_id, proof
|
|
67
|
+
return None, None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _live_url(loki_dir):
|
|
71
|
+
"""Return the live app URL only when the app runner reports it running."""
|
|
72
|
+
state = _read_json(os.path.join(loki_dir, "app-runner", "state.json"))
|
|
73
|
+
if isinstance(state, dict) and state.get("status") == "running":
|
|
74
|
+
url = str(state.get("url") or "").strip()
|
|
75
|
+
if url:
|
|
76
|
+
return url
|
|
77
|
+
return ""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _usage_section(usage_text, header):
|
|
81
|
+
"""Pull the body of a '## <header>' section from USAGE.md, verbatim.
|
|
82
|
+
|
|
83
|
+
Returns the lines under the matching heading up to the next '## ' heading,
|
|
84
|
+
trimmed of blank edges. Empty string when the section is absent.
|
|
85
|
+
"""
|
|
86
|
+
if not usage_text:
|
|
87
|
+
return ""
|
|
88
|
+
lines = usage_text.splitlines()
|
|
89
|
+
out = []
|
|
90
|
+
capturing = False
|
|
91
|
+
want = header.strip().lower()
|
|
92
|
+
for line in lines:
|
|
93
|
+
stripped = line.strip()
|
|
94
|
+
if stripped.startswith("## "):
|
|
95
|
+
if capturing:
|
|
96
|
+
break
|
|
97
|
+
name = stripped[3:].strip().lower()
|
|
98
|
+
# Match on a prefix so "## Verify (it works)" matches "Verify".
|
|
99
|
+
capturing = name == want or name.startswith(want + " ") \
|
|
100
|
+
or name.startswith(want + " (")
|
|
101
|
+
continue
|
|
102
|
+
if capturing:
|
|
103
|
+
out.append(line)
|
|
104
|
+
# Trim leading/trailing blank lines.
|
|
105
|
+
while out and not out[0].strip():
|
|
106
|
+
out.pop(0)
|
|
107
|
+
while out and not out[-1].strip():
|
|
108
|
+
out.pop()
|
|
109
|
+
return "\n".join(out)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _strip_md_fences(text):
|
|
113
|
+
"""Remove markdown code-fence lines (``` or ```lang) from a USAGE.md section
|
|
114
|
+
body. USAGE.md already wraps commands in fenced blocks; own re-wraps each
|
|
115
|
+
section in ONE fence, so leaving the inner fences in produces nested,
|
|
116
|
+
broken-rendering ``` inside ``` for the non-technical reader. We drop the
|
|
117
|
+
fence delimiter lines but keep the content + prose between them verbatim."""
|
|
118
|
+
if not text:
|
|
119
|
+
return ""
|
|
120
|
+
kept = []
|
|
121
|
+
for line in text.splitlines():
|
|
122
|
+
if line.lstrip().startswith("```"):
|
|
123
|
+
continue # drop the fence delimiter line itself
|
|
124
|
+
kept.append(line)
|
|
125
|
+
# Re-trim blank edges left behind after removing fences.
|
|
126
|
+
while kept and not kept[0].strip():
|
|
127
|
+
kept.pop(0)
|
|
128
|
+
while kept and not kept[-1].strip():
|
|
129
|
+
kept.pop()
|
|
130
|
+
return "\n".join(kept)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
# honesty gate (the core)
|
|
135
|
+
# ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
def _is_ready(proof):
|
|
138
|
+
"""Deterministic green gate. True ONLY when:
|
|
139
|
+
- honesty.headline == "VERIFIED", AND
|
|
140
|
+
- facts.tests.status in (passed, verified), AND
|
|
141
|
+
- the build actually ran (facts.build.ran true / status not 'not_run').
|
|
142
|
+
|
|
143
|
+
Any one missing -> not ready. This is the line that must never overclaim.
|
|
144
|
+
"""
|
|
145
|
+
if not isinstance(proof, dict):
|
|
146
|
+
return False
|
|
147
|
+
honesty = proof.get("honesty") or {}
|
|
148
|
+
if str(honesty.get("headline") or "").strip().upper() != "VERIFIED":
|
|
149
|
+
return False
|
|
150
|
+
facts = proof.get("facts") or {}
|
|
151
|
+
tests = facts.get("tests") or {}
|
|
152
|
+
if str(tests.get("status") or "").strip().lower() not in ("passed", "verified"):
|
|
153
|
+
return False
|
|
154
|
+
build = facts.get("build") or {}
|
|
155
|
+
build_ran = bool(build.get("ran")) or \
|
|
156
|
+
str(build.get("status") or "").strip().lower() not in ("not_run", "", "none")
|
|
157
|
+
return build_ran
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ---------------------------------------------------------------------------
|
|
161
|
+
# section builders (markdown). Each returns a list of lines.
|
|
162
|
+
# ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
def _section_what_you_have(proof):
|
|
165
|
+
"""1. 'What you have now' - product-terms restatement of the brief + diff.
|
|
166
|
+
|
|
167
|
+
Deterministic template only: the recorded brief verbatim plus a one-line
|
|
168
|
+
files-changed summary. No invented features, no LLM call.
|
|
169
|
+
"""
|
|
170
|
+
lines = ["## What you have now", ""]
|
|
171
|
+
spec = (proof or {}).get("spec") or {}
|
|
172
|
+
brief = str(spec.get("brief") or "").strip()
|
|
173
|
+
if brief:
|
|
174
|
+
# Restate the brief as the description of what was built. Quote it so the
|
|
175
|
+
# reader sees it is their own words, not a Loki claim.
|
|
176
|
+
first = brief.splitlines()[0].strip()
|
|
177
|
+
lines.append("You asked Loki to build this:")
|
|
178
|
+
lines.append("")
|
|
179
|
+
lines.append("> " + first)
|
|
180
|
+
else:
|
|
181
|
+
lines.append("Loki worked directly on an existing codebase here (no written "
|
|
182
|
+
"spec was recorded for this run).")
|
|
183
|
+
lines.append("")
|
|
184
|
+
|
|
185
|
+
facts = (proof or {}).get("facts") or {}
|
|
186
|
+
git = facts.get("git") or {}
|
|
187
|
+
diff = git.get("diff") or (proof or {}).get("files_changed") or {}
|
|
188
|
+
count = diff.get("count") or 0
|
|
189
|
+
ins = diff.get("insertions") or 0
|
|
190
|
+
dele = diff.get("deletions") or 0
|
|
191
|
+
if count:
|
|
192
|
+
lines.append("It changed %d file%s (%d lines added, %d removed)."
|
|
193
|
+
% (count, "" if count == 1 else "s", ins, dele))
|
|
194
|
+
else:
|
|
195
|
+
lines.append("No file changes were recorded for this run.")
|
|
196
|
+
return lines
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _build_age_note(run_id):
|
|
200
|
+
"""An honest 'this verdict describes the build at <when>' line + a pointer to
|
|
201
|
+
re-verify against current code. A non-technical owner must not read an old
|
|
202
|
+
receipt as a statement about code they edited since the build."""
|
|
203
|
+
lines = []
|
|
204
|
+
when = _run_id_when(run_id)
|
|
205
|
+
if when:
|
|
206
|
+
lines.append("This describes the build Loki finished on %s. If you (or a "
|
|
207
|
+
"developer) changed the code after that, this verdict is about "
|
|
208
|
+
"the older version, not your current files." % when)
|
|
209
|
+
else:
|
|
210
|
+
lines.append("This describes the last build Loki finished. If the code "
|
|
211
|
+
"changed since then, this verdict is about the older version.")
|
|
212
|
+
if run_id:
|
|
213
|
+
lines.append("To confirm it still matches your current code, run: "
|
|
214
|
+
"`loki proof verify %s`" % run_id)
|
|
215
|
+
return lines
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _run_id_when(run_id):
|
|
219
|
+
"""Format a human date from a proof run_id (YYYYmmddTHHMMSSZ-...). Returns ''
|
|
220
|
+
if the id is not in that shape (no fabrication -- only restate what is there)."""
|
|
221
|
+
if not run_id:
|
|
222
|
+
return ""
|
|
223
|
+
stamp = str(run_id).split("-", 1)[0]
|
|
224
|
+
# Expect YYYYmmddTHHMMSSZ
|
|
225
|
+
if len(stamp) >= 16 and stamp[8:9] == "T" and stamp[15:16] == "Z":
|
|
226
|
+
y, mo, d = stamp[0:4], stamp[4:6], stamp[6:8]
|
|
227
|
+
hh, mm = stamp[9:11], stamp[11:13]
|
|
228
|
+
if y.isdigit() and mo.isdigit() and d.isdigit():
|
|
229
|
+
return "%s-%s-%s at %s:%s UTC" % (y, mo, d, hh, mm)
|
|
230
|
+
return ""
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _verdict_translation(proof):
|
|
234
|
+
"""A one-sentence plain-language translation of the honesty verdict for a
|
|
235
|
+
non-technical owner. Honesty gate is sacred here:
|
|
236
|
+
|
|
237
|
+
- ready (_is_ready True) -> a true, non-overclaiming positive line.
|
|
238
|
+
- partial ("VERIFIED ..." headline that is NOT a clean pass, e.g.
|
|
239
|
+
"VERIFIED WITH GAPS") -> "the code is there and it builds, but Loki
|
|
240
|
+
could not fully prove it works - see what is unverified below."
|
|
241
|
+
- everything else (NOT VERIFIED, missing/unknown headline) -> a plain,
|
|
242
|
+
NON-reassuring statement. NEVER softened into something comforting.
|
|
243
|
+
|
|
244
|
+
Returns '' when there is nothing safe to add (we never invent reassurance).
|
|
245
|
+
"""
|
|
246
|
+
headline = str(((proof or {}).get("honesty") or {}).get("headline")
|
|
247
|
+
or "").strip().upper()
|
|
248
|
+
if _is_ready(proof):
|
|
249
|
+
return ("In plain terms: Loki built what you asked for and checked that "
|
|
250
|
+
"it works.")
|
|
251
|
+
# Partial verification: the build produced something Loki could confirm
|
|
252
|
+
# partially, but not fully. Gated on the headline AFFIRMING verification
|
|
253
|
+
# (starts with VERIFIED) while NOT being a clean ready pass. "NOT VERIFIED"
|
|
254
|
+
# starts with "NOT" so it can never reach this branch.
|
|
255
|
+
if headline.startswith("VERIFIED"):
|
|
256
|
+
return ("In plain terms: the code is there and the build ran, but Loki "
|
|
257
|
+
"could not fully prove it works - see what is unverified below.")
|
|
258
|
+
# No affirmative verification at all. State it plainly, do not reassure.
|
|
259
|
+
return ("In plain terms: Loki could not confirm this build works. Treat it "
|
|
260
|
+
"as unfinished until the gaps below are resolved.")
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _section_is_it_working(proof, run_id=None):
|
|
264
|
+
"""2. 'Is it working?' - VERBATIM honesty.headline gating.
|
|
265
|
+
|
|
266
|
+
Green only when _is_ready(). Otherwise plainly states what was not verified
|
|
267
|
+
and lists honesty.degraded[]. NEVER prints a ready/ship line unless ready.
|
|
268
|
+
Always dates the verdict + points to re-verify, so a stale receipt is never
|
|
269
|
+
read as current truth.
|
|
270
|
+
"""
|
|
271
|
+
lines = ["## Is it working?", ""]
|
|
272
|
+
honesty = (proof or {}).get("honesty") or {}
|
|
273
|
+
headline = str(honesty.get("headline") or "").strip()
|
|
274
|
+
degraded = honesty.get("degraded") or []
|
|
275
|
+
|
|
276
|
+
if _is_ready(proof):
|
|
277
|
+
lines.append("Yes. Loki verified this build: the tests passed and the "
|
|
278
|
+
"build ran cleanly.")
|
|
279
|
+
lines.append("")
|
|
280
|
+
lines.append(_verdict_translation(proof))
|
|
281
|
+
lines.append("")
|
|
282
|
+
lines.append("Verdict (Loki's honest receipt): %s" % (headline or "VERIFIED"))
|
|
283
|
+
lines.append("")
|
|
284
|
+
lines.extend(_build_age_note(run_id))
|
|
285
|
+
return lines
|
|
286
|
+
|
|
287
|
+
# Not ready. State the verdict plainly and list every gap.
|
|
288
|
+
if headline:
|
|
289
|
+
lines.append("Not fully verified. Loki's honest verdict for this run is: "
|
|
290
|
+
"%s" % headline)
|
|
291
|
+
else:
|
|
292
|
+
lines.append("Not verified. Loki did not record a verdict for this run.")
|
|
293
|
+
lines.append("")
|
|
294
|
+
lines.append(_verdict_translation(proof))
|
|
295
|
+
lines.append("")
|
|
296
|
+
lines.append("This means Loki is NOT telling you it is ready to ship. Here is "
|
|
297
|
+
"what was not verified:")
|
|
298
|
+
lines.append("")
|
|
299
|
+
if isinstance(degraded, list) and degraded:
|
|
300
|
+
for d in degraded:
|
|
301
|
+
if isinstance(d, dict):
|
|
302
|
+
item = str(d.get("item") or "").strip() or "(unnamed check)"
|
|
303
|
+
status = str(d.get("status") or "").strip()
|
|
304
|
+
reason = str(d.get("reason") or "").strip()
|
|
305
|
+
bits = [b for b in (status, reason) if b]
|
|
306
|
+
tail = (" - " + "; ".join(bits)) if bits else ""
|
|
307
|
+
lines.append("- %s%s" % (item, tail))
|
|
308
|
+
else:
|
|
309
|
+
lines.append("- %s" % str(d))
|
|
310
|
+
else:
|
|
311
|
+
lines.append("- (no specific gaps were itemized, but the verdict above is "
|
|
312
|
+
"not a clean pass)")
|
|
313
|
+
lines.append("")
|
|
314
|
+
lines.extend(_build_age_note(run_id))
|
|
315
|
+
return lines
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _section_run_on_computer(proof, usage_text, live_url):
|
|
319
|
+
"""3. 'How to run it on your computer' - quote Start/Verify from USAGE.md."""
|
|
320
|
+
lines = ["## How to run it on your computer", ""]
|
|
321
|
+
if live_url:
|
|
322
|
+
lines.append("It is running right now on this machine at: %s" % live_url)
|
|
323
|
+
lines.append("")
|
|
324
|
+
# Strip USAGE.md's own ``` fences so each section is wrapped exactly once
|
|
325
|
+
# below (otherwise nested fences render broken for the non-dev reader).
|
|
326
|
+
start = _strip_md_fences(_usage_section(usage_text, "Start"))
|
|
327
|
+
verify = _strip_md_fences(_usage_section(usage_text, "Verify"))
|
|
328
|
+
install = _strip_md_fences(_usage_section(usage_text, "Install"))
|
|
329
|
+
if install:
|
|
330
|
+
lines.append("First, install it:")
|
|
331
|
+
lines.append("")
|
|
332
|
+
lines.append("```")
|
|
333
|
+
lines.append(install)
|
|
334
|
+
lines.append("```")
|
|
335
|
+
lines.append("")
|
|
336
|
+
if start:
|
|
337
|
+
lines.append("To start it:")
|
|
338
|
+
lines.append("")
|
|
339
|
+
lines.append("```")
|
|
340
|
+
lines.append(start)
|
|
341
|
+
lines.append("```")
|
|
342
|
+
lines.append("")
|
|
343
|
+
if verify:
|
|
344
|
+
lines.append("To check it works:")
|
|
345
|
+
lines.append("")
|
|
346
|
+
lines.append("```")
|
|
347
|
+
lines.append(verify)
|
|
348
|
+
lines.append("```")
|
|
349
|
+
if not (start or verify or install):
|
|
350
|
+
lines.append("No run instructions (USAGE.md) were found for this build. "
|
|
351
|
+
"Once you run a build to completion, Loki writes a USAGE.md at "
|
|
352
|
+
"the project root with the exact commands.")
|
|
353
|
+
return lines
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _section_put_online(proof):
|
|
357
|
+
"""4. 'How to put it online' - mention loki deploy / preview; URL if present."""
|
|
358
|
+
lines = ["## How to put it online", ""]
|
|
359
|
+
deployment = (proof or {}).get("deployment") or {}
|
|
360
|
+
deployed_url = str(deployment.get("deployed_url") or "").strip()
|
|
361
|
+
if deployed_url:
|
|
362
|
+
lines.append("This build was deployed. It is live at: %s" % deployed_url)
|
|
363
|
+
lines.append("")
|
|
364
|
+
else:
|
|
365
|
+
lines.append("This build has not been put online yet.")
|
|
366
|
+
lines.append("")
|
|
367
|
+
lines.append("When you are ready, you have two options:")
|
|
368
|
+
lines.append("")
|
|
369
|
+
lines.append("- `loki deploy` - deploy it using your own cloud account.")
|
|
370
|
+
lines.append("- `loki preview --public` - share a temporary public link to the "
|
|
371
|
+
"version running on your computer.")
|
|
372
|
+
return lines
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _section_developer_needs(proof):
|
|
376
|
+
"""5. 'What a developer needs to know' - group changed files by top dir."""
|
|
377
|
+
lines = ["## What a developer needs to know", ""]
|
|
378
|
+
facts = (proof or {}).get("facts") or {}
|
|
379
|
+
git = facts.get("git") or {}
|
|
380
|
+
diff = git.get("diff") or (proof or {}).get("files_changed") or {}
|
|
381
|
+
files = diff.get("files") or []
|
|
382
|
+
if isinstance(files, list) and files:
|
|
383
|
+
groups = {}
|
|
384
|
+
for f in files:
|
|
385
|
+
if not isinstance(f, dict):
|
|
386
|
+
continue
|
|
387
|
+
path = str(f.get("path") or "").strip()
|
|
388
|
+
if not path:
|
|
389
|
+
continue
|
|
390
|
+
top = path.split("/")[0] if "/" in path else "(project root)"
|
|
391
|
+
groups.setdefault(top, 0)
|
|
392
|
+
groups[top] += 1
|
|
393
|
+
if groups:
|
|
394
|
+
lines.append("The changes touch these areas of the codebase:")
|
|
395
|
+
lines.append("")
|
|
396
|
+
for top in sorted(groups):
|
|
397
|
+
n = groups[top]
|
|
398
|
+
lines.append("- %s (%d file%s)" % (top, n, "" if n == 1 else "s"))
|
|
399
|
+
lines.append("")
|
|
400
|
+
else:
|
|
401
|
+
lines.append("No changed-file list was recorded for this run.")
|
|
402
|
+
lines.append("")
|
|
403
|
+
lines.append("A developer should read USAGE.md (run/verify commands) and the "
|
|
404
|
+
"developer handoff notes in .loki/memory/handoffs/.")
|
|
405
|
+
return lines
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _section_verified(proof, run_id):
|
|
409
|
+
"""6. 'What is verified' - the proof commands."""
|
|
410
|
+
lines = ["## What is verified", ""]
|
|
411
|
+
if run_id:
|
|
412
|
+
lines.append("Loki keeps a tamper-evident receipt of exactly what it did. "
|
|
413
|
+
"Anyone can inspect or re-check it:")
|
|
414
|
+
lines.append("")
|
|
415
|
+
lines.append("- `loki proof show %s` - read the full receipt." % run_id)
|
|
416
|
+
lines.append("- `loki proof verify %s` - confirm the receipt has not been "
|
|
417
|
+
"altered." % run_id)
|
|
418
|
+
else:
|
|
419
|
+
lines.append("No receipt was found for this project yet. Loki writes one "
|
|
420
|
+
"when a build completes.")
|
|
421
|
+
return lines
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _section_still_to_do(proof, completion):
|
|
425
|
+
"""7. 'What you still need to do or decide'.
|
|
426
|
+
|
|
427
|
+
Rendered as a numbered, ordered checklist so a non-technical owner knows
|
|
428
|
+
exactly what to handle and roughly in what order: run/try it, resolve
|
|
429
|
+
anything Loki could not verify, then open a PR (merge) and deploy. Every
|
|
430
|
+
item still maps to a real artifact value -- nothing is invented.
|
|
431
|
+
"""
|
|
432
|
+
lines = ["## What you still need to do or decide", ""]
|
|
433
|
+
# Action items, kept in a sensible do-this-first order:
|
|
434
|
+
# 1) review assumptions, 2) resolve unverified gaps, 3) PR/merge, 4) deploy.
|
|
435
|
+
items = []
|
|
436
|
+
|
|
437
|
+
# Assumptions Loki had to make where the spec was ambiguous.
|
|
438
|
+
total = 0
|
|
439
|
+
high = 0
|
|
440
|
+
if isinstance(completion, dict):
|
|
441
|
+
try:
|
|
442
|
+
total = int(completion.get("assumptions_total") or 0)
|
|
443
|
+
except Exception:
|
|
444
|
+
total = 0
|
|
445
|
+
try:
|
|
446
|
+
high = int(completion.get("assumptions_high") or 0)
|
|
447
|
+
except Exception:
|
|
448
|
+
high = 0
|
|
449
|
+
if total > 0:
|
|
450
|
+
msg = ("Review %d assumption%s Loki had to make where your spec was "
|
|
451
|
+
"ambiguous" % (total, "" if total == 1 else "s"))
|
|
452
|
+
if high > 0:
|
|
453
|
+
msg += (" (%d of them high-impact)" % high)
|
|
454
|
+
msg += ". See .loki/assumptions/ledger.md."
|
|
455
|
+
items.append(msg)
|
|
456
|
+
|
|
457
|
+
# Anything not verified (degraded items) is also a to-do.
|
|
458
|
+
honesty = (proof or {}).get("honesty") or {}
|
|
459
|
+
degraded = honesty.get("degraded") or []
|
|
460
|
+
if isinstance(degraded, list) and degraded:
|
|
461
|
+
for d in degraded:
|
|
462
|
+
if isinstance(d, dict):
|
|
463
|
+
item = str(d.get("item") or "").strip()
|
|
464
|
+
reason = str(d.get("reason") or "").strip()
|
|
465
|
+
if item:
|
|
466
|
+
items.append("Address: %s%s"
|
|
467
|
+
% (item, (" (" + reason + ")") if reason else ""))
|
|
468
|
+
|
|
469
|
+
# PR state.
|
|
470
|
+
pr_url = ""
|
|
471
|
+
if isinstance(completion, dict):
|
|
472
|
+
pr_url = str(completion.get("pr_url") or "").strip()
|
|
473
|
+
if pr_url:
|
|
474
|
+
items.append("A pull request was opened: %s" % pr_url)
|
|
475
|
+
else:
|
|
476
|
+
items.append("No pull request was opened. Open one when you are ready to "
|
|
477
|
+
"merge the changes.")
|
|
478
|
+
|
|
479
|
+
# Deployment state.
|
|
480
|
+
deployment = (proof or {}).get("deployment") or {}
|
|
481
|
+
if not str(deployment.get("deployed_url") or "").strip():
|
|
482
|
+
items.append("It is not deployed yet. Use `loki deploy` when you are ready.")
|
|
483
|
+
|
|
484
|
+
if items:
|
|
485
|
+
lines.append("Work through these in order:")
|
|
486
|
+
lines.append("")
|
|
487
|
+
for i, it in enumerate(items, start=1):
|
|
488
|
+
lines.append("%d. %s" % (i, it))
|
|
489
|
+
else:
|
|
490
|
+
lines.append("- Nothing outstanding was recorded. Read the sections above "
|
|
491
|
+
"to decide your next step.")
|
|
492
|
+
return lines
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
# ---------------------------------------------------------------------------
|
|
496
|
+
# top-level render
|
|
497
|
+
# ---------------------------------------------------------------------------
|
|
498
|
+
|
|
499
|
+
def _empty_doc_md(loki_dir):
|
|
500
|
+
return "\n".join([
|
|
501
|
+
"# What Loki built for you",
|
|
502
|
+
"",
|
|
503
|
+
"No completed build was found here yet.",
|
|
504
|
+
"",
|
|
505
|
+
"Run `loki start <spec>` to build something (a one-line idea, a PRD "
|
|
506
|
+
"file, or a GitHub issue all work). When it finishes, come back and run "
|
|
507
|
+
"`loki own` to read this plain-English summary of what you have.",
|
|
508
|
+
"",
|
|
509
|
+
])
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _empty_doc_json(loki_dir):
|
|
513
|
+
return {
|
|
514
|
+
"ok": True,
|
|
515
|
+
"found": False,
|
|
516
|
+
"loki_dir": loki_dir,
|
|
517
|
+
"message": "No completed build found here yet -- run loki start <spec>",
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def render_markdown(loki_dir):
|
|
522
|
+
run_id, proof = _latest_proof(loki_dir)
|
|
523
|
+
if proof is None:
|
|
524
|
+
return _empty_doc_md(loki_dir)
|
|
525
|
+
|
|
526
|
+
completion = _read_json(os.path.join(loki_dir, "state", "completion.json"))
|
|
527
|
+
# USAGE.md lives at the project root (parent of .loki).
|
|
528
|
+
project_root = os.path.dirname(os.path.abspath(loki_dir)) or "."
|
|
529
|
+
usage_text = _read_text(os.path.join(project_root, "USAGE.md"))
|
|
530
|
+
live_url = _live_url(loki_dir)
|
|
531
|
+
|
|
532
|
+
blocks = [["# What Loki built for you", "",
|
|
533
|
+
"Here is what Loki built and how to take it from here, in plain "
|
|
534
|
+
"language - no code reading required."]]
|
|
535
|
+
blocks.append(_section_what_you_have(proof))
|
|
536
|
+
blocks.append(_section_is_it_working(proof, run_id))
|
|
537
|
+
blocks.append(_section_run_on_computer(proof, usage_text, live_url))
|
|
538
|
+
blocks.append(_section_put_online(proof))
|
|
539
|
+
blocks.append(_section_developer_needs(proof))
|
|
540
|
+
blocks.append(_section_verified(proof, run_id))
|
|
541
|
+
blocks.append(_section_still_to_do(proof, completion))
|
|
542
|
+
|
|
543
|
+
parts = []
|
|
544
|
+
for b in blocks:
|
|
545
|
+
parts.append("\n".join(b))
|
|
546
|
+
return "\n\n".join(parts) + "\n"
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def render_json(loki_dir):
|
|
550
|
+
run_id, proof = _latest_proof(loki_dir)
|
|
551
|
+
if proof is None:
|
|
552
|
+
return _empty_doc_json(loki_dir)
|
|
553
|
+
|
|
554
|
+
completion = _read_json(os.path.join(loki_dir, "state", "completion.json")) or {}
|
|
555
|
+
project_root = os.path.dirname(os.path.abspath(loki_dir)) or "."
|
|
556
|
+
usage_text = _read_text(os.path.join(project_root, "USAGE.md"))
|
|
557
|
+
live_url = _live_url(loki_dir)
|
|
558
|
+
|
|
559
|
+
honesty = proof.get("honesty") or {}
|
|
560
|
+
deployment = proof.get("deployment") or {}
|
|
561
|
+
facts = proof.get("facts") or {}
|
|
562
|
+
git = facts.get("git") or {}
|
|
563
|
+
diff = git.get("diff") or proof.get("files_changed") or {}
|
|
564
|
+
|
|
565
|
+
return {
|
|
566
|
+
"ok": True,
|
|
567
|
+
"found": True,
|
|
568
|
+
"run_id": run_id,
|
|
569
|
+
"ready": _is_ready(proof),
|
|
570
|
+
"headline": str(honesty.get("headline") or ""),
|
|
571
|
+
"degraded": honesty.get("degraded") or [],
|
|
572
|
+
"spec_brief": str((proof.get("spec") or {}).get("brief") or ""),
|
|
573
|
+
"files_changed": {
|
|
574
|
+
"count": diff.get("count") or 0,
|
|
575
|
+
"insertions": diff.get("insertions") or 0,
|
|
576
|
+
"deletions": diff.get("deletions") or 0,
|
|
577
|
+
},
|
|
578
|
+
"live_url": live_url,
|
|
579
|
+
"deployed_url": str(deployment.get("deployed_url") or ""),
|
|
580
|
+
"start_command": _usage_section(usage_text, "Start"),
|
|
581
|
+
"verify_command": _usage_section(usage_text, "Verify"),
|
|
582
|
+
"pr_url": str(completion.get("pr_url") or ""),
|
|
583
|
+
"assumptions_total": completion.get("assumptions_total") or 0,
|
|
584
|
+
"assumptions_high": completion.get("assumptions_high") or 0,
|
|
585
|
+
"proof_show_cmd": ("loki proof show %s" % run_id) if run_id else "",
|
|
586
|
+
"proof_verify_cmd": ("loki proof verify %s" % run_id) if run_id else "",
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def main(argv=None):
|
|
591
|
+
parser = argparse.ArgumentParser(
|
|
592
|
+
description="Loki Mode finish-and-own renderer (plain-English ownership doc)"
|
|
593
|
+
)
|
|
594
|
+
parser.add_argument("--loki-dir", default=".loki")
|
|
595
|
+
fmt = parser.add_mutually_exclusive_group()
|
|
596
|
+
fmt.add_argument("--md", action="store_const", const="md", dest="fmt")
|
|
597
|
+
fmt.add_argument("--json", action="store_const", const="json", dest="fmt")
|
|
598
|
+
parser.set_defaults(fmt="md")
|
|
599
|
+
args = parser.parse_args(argv)
|
|
600
|
+
|
|
601
|
+
loki_dir = os.path.abspath(args.loki_dir)
|
|
602
|
+
try:
|
|
603
|
+
if args.fmt == "json":
|
|
604
|
+
print(json.dumps(render_json(loki_dir), indent=2))
|
|
605
|
+
else:
|
|
606
|
+
print(render_markdown(loki_dir))
|
|
607
|
+
except Exception as exc:
|
|
608
|
+
# This is a report, never a gate: emit an honest line and exit 0.
|
|
609
|
+
if args.fmt == "json":
|
|
610
|
+
print(json.dumps({"ok": False, "found": False, "error": str(exc)}))
|
|
611
|
+
else:
|
|
612
|
+
sys.stderr.write("warn: own-render failed: %s\n" % exc)
|
|
613
|
+
return 0
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
if __name__ == "__main__":
|
|
617
|
+
sys.exit(main())
|
|
@@ -550,6 +550,19 @@ def _collect_spec(loki_dir, target_dir):
|
|
|
550
550
|
return {"source": source, "brief": brief}
|
|
551
551
|
|
|
552
552
|
|
|
553
|
+
def _self_version():
|
|
554
|
+
"""Read the installed Loki version from the VERSION file shipped beside this
|
|
555
|
+
generator (package layout: <root>/VERSION and <root>/autonomy/lib/<this>).
|
|
556
|
+
|
|
557
|
+
This is the most robust source: proof-generator.py always ships two dirs
|
|
558
|
+
below VERSION in every distribution channel (npm, Docker, brew), so it is
|
|
559
|
+
correct regardless of the caller's cwd or the target app dir. Returns "" when
|
|
560
|
+
the file cannot be read (never raises)."""
|
|
561
|
+
return _read_text(
|
|
562
|
+
os.path.join(_HERE, "..", "..", "VERSION")
|
|
563
|
+
).strip()
|
|
564
|
+
|
|
565
|
+
|
|
553
566
|
def _collect_meta(loki_dir, repo_root):
|
|
554
567
|
orch = _read_json(
|
|
555
568
|
os.path.join(loki_dir, "state", "orchestrator.json"), default={}
|
|
@@ -560,6 +573,11 @@ def _collect_meta(loki_dir, repo_root):
|
|
|
560
573
|
version = str(orch.get("version") or "")
|
|
561
574
|
if not version and repo_root:
|
|
562
575
|
version = _read_text(os.path.join(repo_root, "VERSION")).strip()
|
|
576
|
+
# Final fallback: the VERSION shipped beside this generator. Robust even when
|
|
577
|
+
# repo_root resolution failed (e.g. the generator runs from outside its
|
|
578
|
+
# package tree against a user app dir that has no VERSION file).
|
|
579
|
+
if not version:
|
|
580
|
+
version = _self_version()
|
|
563
581
|
return started_at, version
|
|
564
582
|
|
|
565
583
|
|
|
@@ -610,7 +628,15 @@ def _build_proof(args, loki_dir, target_dir, repo_root):
|
|
|
610
628
|
run_id = args.run_id or os.environ.get("LOKI_SESSION_ID") or _gen_run_id()
|
|
611
629
|
|
|
612
630
|
started_at, version_from_state = _collect_meta(loki_dir, repo_root)
|
|
613
|
-
|
|
631
|
+
# Treat a literal "unknown" arg as absent: the bash runtime wrapper passes
|
|
632
|
+
# --loki-version "$(get_version ... || echo unknown)", and get_version is not
|
|
633
|
+
# defined in run.sh's process, so the wrapper sends the sentinel "unknown".
|
|
634
|
+
# Letting that win would mask the version that _collect_meta resolves from
|
|
635
|
+
# orchestrator.json / repo VERSION / the VERSION shipped beside this file.
|
|
636
|
+
arg_version = (args.loki_version or "").strip()
|
|
637
|
+
if arg_version.lower() == "unknown":
|
|
638
|
+
arg_version = ""
|
|
639
|
+
loki_version = arg_version or version_from_state or "unknown"
|
|
614
640
|
|
|
615
641
|
cost, model_from_eff = _collect_efficiency(loki_dir)
|
|
616
642
|
provider_name = args.provider or os.environ.get("PROVIDER_NAME") or "claude"
|