loki-mode 7.87.0 → 7.88.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 CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 8 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
4
4
  ---
5
5
 
6
- # Loki Mode v7.87.0
6
+ # Loki Mode v7.88.0
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -408,4 +408,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
408
408
 
409
409
  ---
410
410
 
411
- **v7.87.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
411
+ **v7.88.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.87.0
1
+ 7.88.0
@@ -0,0 +1,547 @@
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
+ # ---------------------------------------------------------------------------
113
+ # honesty gate (the core)
114
+ # ---------------------------------------------------------------------------
115
+
116
+ def _is_ready(proof):
117
+ """Deterministic green gate. True ONLY when:
118
+ - honesty.headline == "VERIFIED", AND
119
+ - facts.tests.status in (passed, verified), AND
120
+ - the build actually ran (facts.build.ran true / status not 'not_run').
121
+
122
+ Any one missing -> not ready. This is the line that must never overclaim.
123
+ """
124
+ if not isinstance(proof, dict):
125
+ return False
126
+ honesty = proof.get("honesty") or {}
127
+ if str(honesty.get("headline") or "").strip().upper() != "VERIFIED":
128
+ return False
129
+ facts = proof.get("facts") or {}
130
+ tests = facts.get("tests") or {}
131
+ if str(tests.get("status") or "").strip().lower() not in ("passed", "verified"):
132
+ return False
133
+ build = facts.get("build") or {}
134
+ build_ran = bool(build.get("ran")) or \
135
+ str(build.get("status") or "").strip().lower() not in ("not_run", "", "none")
136
+ return build_ran
137
+
138
+
139
+ # ---------------------------------------------------------------------------
140
+ # section builders (markdown). Each returns a list of lines.
141
+ # ---------------------------------------------------------------------------
142
+
143
+ def _section_what_you_have(proof):
144
+ """1. 'What you have now' - product-terms restatement of the brief + diff.
145
+
146
+ Deterministic template only: the recorded brief verbatim plus a one-line
147
+ files-changed summary. No invented features, no LLM call.
148
+ """
149
+ lines = ["## What you have now", ""]
150
+ spec = (proof or {}).get("spec") or {}
151
+ brief = str(spec.get("brief") or "").strip()
152
+ if brief:
153
+ # Restate the brief as the description of what was built. Quote it so the
154
+ # reader sees it is their own words, not a Loki claim.
155
+ first = brief.splitlines()[0].strip()
156
+ lines.append("You asked Loki to build this:")
157
+ lines.append("")
158
+ lines.append("> " + first)
159
+ else:
160
+ lines.append("Loki worked directly on an existing codebase here (no written "
161
+ "spec was recorded for this run).")
162
+ lines.append("")
163
+
164
+ facts = (proof or {}).get("facts") or {}
165
+ git = facts.get("git") or {}
166
+ diff = git.get("diff") or (proof or {}).get("files_changed") or {}
167
+ count = diff.get("count") or 0
168
+ ins = diff.get("insertions") or 0
169
+ dele = diff.get("deletions") or 0
170
+ if count:
171
+ lines.append("It changed %d file%s (%d lines added, %d removed)."
172
+ % (count, "" if count == 1 else "s", ins, dele))
173
+ else:
174
+ lines.append("No file changes were recorded for this run.")
175
+ return lines
176
+
177
+
178
+ def _build_age_note(run_id):
179
+ """An honest 'this verdict describes the build at <when>' line + a pointer to
180
+ re-verify against current code. A non-technical owner must not read an old
181
+ receipt as a statement about code they edited since the build."""
182
+ lines = []
183
+ when = _run_id_when(run_id)
184
+ if when:
185
+ lines.append("This describes the build Loki finished on %s. If you (or a "
186
+ "developer) changed the code after that, this verdict is about "
187
+ "the older version, not your current files." % when)
188
+ else:
189
+ lines.append("This describes the last build Loki finished. If the code "
190
+ "changed since then, this verdict is about the older version.")
191
+ if run_id:
192
+ lines.append("To confirm it still matches your current code, run: "
193
+ "`loki proof verify %s`" % run_id)
194
+ return lines
195
+
196
+
197
+ def _run_id_when(run_id):
198
+ """Format a human date from a proof run_id (YYYYmmddTHHMMSSZ-...). Returns ''
199
+ if the id is not in that shape (no fabrication -- only restate what is there)."""
200
+ if not run_id:
201
+ return ""
202
+ stamp = str(run_id).split("-", 1)[0]
203
+ # Expect YYYYmmddTHHMMSSZ
204
+ if len(stamp) >= 16 and stamp[8:9] == "T" and stamp[15:16] == "Z":
205
+ y, mo, d = stamp[0:4], stamp[4:6], stamp[6:8]
206
+ hh, mm = stamp[9:11], stamp[11:13]
207
+ if y.isdigit() and mo.isdigit() and d.isdigit():
208
+ return "%s-%s-%s at %s:%s UTC" % (y, mo, d, hh, mm)
209
+ return ""
210
+
211
+
212
+ def _section_is_it_working(proof, run_id=None):
213
+ """2. 'Is it working?' - VERBATIM honesty.headline gating.
214
+
215
+ Green only when _is_ready(). Otherwise plainly states what was not verified
216
+ and lists honesty.degraded[]. NEVER prints a ready/ship line unless ready.
217
+ Always dates the verdict + points to re-verify, so a stale receipt is never
218
+ read as current truth.
219
+ """
220
+ lines = ["## Is it working?", ""]
221
+ honesty = (proof or {}).get("honesty") or {}
222
+ headline = str(honesty.get("headline") or "").strip()
223
+ degraded = honesty.get("degraded") or []
224
+
225
+ if _is_ready(proof):
226
+ lines.append("Yes. Loki verified this build: the tests passed and the "
227
+ "build ran cleanly.")
228
+ lines.append("")
229
+ lines.append("Verdict (Loki's honest receipt): %s" % (headline or "VERIFIED"))
230
+ lines.append("")
231
+ lines.extend(_build_age_note(run_id))
232
+ return lines
233
+
234
+ # Not ready. State the verdict plainly and list every gap.
235
+ if headline:
236
+ lines.append("Not fully verified. Loki's honest verdict for this run is: "
237
+ "%s" % headline)
238
+ else:
239
+ lines.append("Not verified. Loki did not record a verdict for this run.")
240
+ lines.append("")
241
+ lines.append("This means Loki is NOT telling you it is ready to ship. Here is "
242
+ "what was not verified:")
243
+ lines.append("")
244
+ if isinstance(degraded, list) and degraded:
245
+ for d in degraded:
246
+ if isinstance(d, dict):
247
+ item = str(d.get("item") or "").strip() or "(unnamed check)"
248
+ status = str(d.get("status") or "").strip()
249
+ reason = str(d.get("reason") or "").strip()
250
+ bits = [b for b in (status, reason) if b]
251
+ tail = (" - " + "; ".join(bits)) if bits else ""
252
+ lines.append("- %s%s" % (item, tail))
253
+ else:
254
+ lines.append("- %s" % str(d))
255
+ else:
256
+ lines.append("- (no specific gaps were itemized, but the verdict above is "
257
+ "not a clean pass)")
258
+ lines.append("")
259
+ lines.extend(_build_age_note(run_id))
260
+ return lines
261
+
262
+
263
+ def _section_run_on_computer(proof, usage_text, live_url):
264
+ """3. 'How to run it on your computer' - quote Start/Verify from USAGE.md."""
265
+ lines = ["## How to run it on your computer", ""]
266
+ if live_url:
267
+ lines.append("It is running right now on this machine at: %s" % live_url)
268
+ lines.append("")
269
+ start = _usage_section(usage_text, "Start")
270
+ verify = _usage_section(usage_text, "Verify")
271
+ install = _usage_section(usage_text, "Install")
272
+ if install:
273
+ lines.append("First, install it:")
274
+ lines.append("")
275
+ lines.append("```")
276
+ lines.append(install)
277
+ lines.append("```")
278
+ lines.append("")
279
+ if start:
280
+ lines.append("To start it:")
281
+ lines.append("")
282
+ lines.append("```")
283
+ lines.append(start)
284
+ lines.append("```")
285
+ lines.append("")
286
+ if verify:
287
+ lines.append("To check it works:")
288
+ lines.append("")
289
+ lines.append("```")
290
+ lines.append(verify)
291
+ lines.append("```")
292
+ if not (start or verify or install):
293
+ lines.append("No run instructions (USAGE.md) were found for this build. "
294
+ "Once you run a build to completion, Loki writes a USAGE.md at "
295
+ "the project root with the exact commands.")
296
+ return lines
297
+
298
+
299
+ def _section_put_online(proof):
300
+ """4. 'How to put it online' - mention loki deploy / preview; URL if present."""
301
+ lines = ["## How to put it online", ""]
302
+ deployment = (proof or {}).get("deployment") or {}
303
+ deployed_url = str(deployment.get("deployed_url") or "").strip()
304
+ if deployed_url:
305
+ lines.append("This build was deployed. It is live at: %s" % deployed_url)
306
+ lines.append("")
307
+ else:
308
+ lines.append("This build has not been put online yet.")
309
+ lines.append("")
310
+ lines.append("When you are ready, you have two options:")
311
+ lines.append("")
312
+ lines.append("- `loki deploy` - deploy it using your own cloud account.")
313
+ lines.append("- `loki preview --public` - share a temporary public link to the "
314
+ "version running on your computer.")
315
+ return lines
316
+
317
+
318
+ def _section_developer_needs(proof):
319
+ """5. 'What a developer needs to know' - group changed files by top dir."""
320
+ lines = ["## What a developer needs to know", ""]
321
+ facts = (proof or {}).get("facts") or {}
322
+ git = facts.get("git") or {}
323
+ diff = git.get("diff") or (proof or {}).get("files_changed") or {}
324
+ files = diff.get("files") or []
325
+ if isinstance(files, list) and files:
326
+ groups = {}
327
+ for f in files:
328
+ if not isinstance(f, dict):
329
+ continue
330
+ path = str(f.get("path") or "").strip()
331
+ if not path:
332
+ continue
333
+ top = path.split("/")[0] if "/" in path else "(project root)"
334
+ groups.setdefault(top, 0)
335
+ groups[top] += 1
336
+ if groups:
337
+ lines.append("The changes touch these areas of the codebase:")
338
+ lines.append("")
339
+ for top in sorted(groups):
340
+ n = groups[top]
341
+ lines.append("- %s (%d file%s)" % (top, n, "" if n == 1 else "s"))
342
+ lines.append("")
343
+ else:
344
+ lines.append("No changed-file list was recorded for this run.")
345
+ lines.append("")
346
+ lines.append("A developer should read USAGE.md (run/verify commands) and the "
347
+ "developer handoff notes in .loki/memory/handoffs/.")
348
+ return lines
349
+
350
+
351
+ def _section_verified(proof, run_id):
352
+ """6. 'What is verified' - the proof commands."""
353
+ lines = ["## What is verified", ""]
354
+ if run_id:
355
+ lines.append("Loki keeps a tamper-evident receipt of exactly what it did. "
356
+ "Anyone can inspect or re-check it:")
357
+ lines.append("")
358
+ lines.append("- `loki proof show %s` - read the full receipt." % run_id)
359
+ lines.append("- `loki proof verify %s` - confirm the receipt has not been "
360
+ "altered." % run_id)
361
+ else:
362
+ lines.append("No receipt was found for this project yet. Loki writes one "
363
+ "when a build completes.")
364
+ return lines
365
+
366
+
367
+ def _section_still_to_do(proof, completion):
368
+ """7. 'What you still need to do or decide'."""
369
+ lines = ["## What you still need to do or decide", ""]
370
+ items = []
371
+
372
+ # Assumptions Loki had to make where the spec was ambiguous.
373
+ total = 0
374
+ high = 0
375
+ if isinstance(completion, dict):
376
+ try:
377
+ total = int(completion.get("assumptions_total") or 0)
378
+ except Exception:
379
+ total = 0
380
+ try:
381
+ high = int(completion.get("assumptions_high") or 0)
382
+ except Exception:
383
+ high = 0
384
+ if total > 0:
385
+ msg = ("Review %d assumption%s Loki had to make where your spec was "
386
+ "ambiguous" % (total, "" if total == 1 else "s"))
387
+ if high > 0:
388
+ msg += (" (%d of them high-impact)" % high)
389
+ msg += ". See .loki/assumptions/ledger.md."
390
+ items.append(msg)
391
+
392
+ # Anything not verified (degraded items) is also a to-do.
393
+ honesty = (proof or {}).get("honesty") or {}
394
+ degraded = honesty.get("degraded") or []
395
+ if isinstance(degraded, list) and degraded:
396
+ for d in degraded:
397
+ if isinstance(d, dict):
398
+ item = str(d.get("item") or "").strip()
399
+ reason = str(d.get("reason") or "").strip()
400
+ if item:
401
+ items.append("Address: %s%s"
402
+ % (item, (" (" + reason + ")") if reason else ""))
403
+
404
+ # PR state.
405
+ pr_url = ""
406
+ if isinstance(completion, dict):
407
+ pr_url = str(completion.get("pr_url") or "").strip()
408
+ if pr_url:
409
+ items.append("A pull request was opened: %s" % pr_url)
410
+ else:
411
+ items.append("No pull request was opened. Open one when you are ready to "
412
+ "merge the changes.")
413
+
414
+ # Deployment state.
415
+ deployment = (proof or {}).get("deployment") or {}
416
+ if not str(deployment.get("deployed_url") or "").strip():
417
+ items.append("It is not deployed yet. Use `loki deploy` when you are ready.")
418
+
419
+ for it in items:
420
+ lines.append("- %s" % it)
421
+ if not items:
422
+ lines.append("- Nothing outstanding was recorded. Read the sections above "
423
+ "to decide your next step.")
424
+ return lines
425
+
426
+
427
+ # ---------------------------------------------------------------------------
428
+ # top-level render
429
+ # ---------------------------------------------------------------------------
430
+
431
+ def _empty_doc_md(loki_dir):
432
+ return "\n".join([
433
+ "# What Loki built for you",
434
+ "",
435
+ "No completed build was found here yet.",
436
+ "",
437
+ "Run `loki start <spec>` to build something (a one-line idea, a PRD "
438
+ "file, or a GitHub issue all work). When it finishes, come back and run "
439
+ "`loki own` to read this plain-English summary of what you have.",
440
+ "",
441
+ ])
442
+
443
+
444
+ def _empty_doc_json(loki_dir):
445
+ return {
446
+ "ok": True,
447
+ "found": False,
448
+ "loki_dir": loki_dir,
449
+ "message": "No completed build found here yet -- run loki start <spec>",
450
+ }
451
+
452
+
453
+ def render_markdown(loki_dir):
454
+ run_id, proof = _latest_proof(loki_dir)
455
+ if proof is None:
456
+ return _empty_doc_md(loki_dir)
457
+
458
+ completion = _read_json(os.path.join(loki_dir, "state", "completion.json"))
459
+ # USAGE.md lives at the project root (parent of .loki).
460
+ project_root = os.path.dirname(os.path.abspath(loki_dir)) or "."
461
+ usage_text = _read_text(os.path.join(project_root, "USAGE.md"))
462
+ live_url = _live_url(loki_dir)
463
+
464
+ blocks = [["# What Loki built for you", ""]]
465
+ blocks.append(_section_what_you_have(proof))
466
+ blocks.append(_section_is_it_working(proof, run_id))
467
+ blocks.append(_section_run_on_computer(proof, usage_text, live_url))
468
+ blocks.append(_section_put_online(proof))
469
+ blocks.append(_section_developer_needs(proof))
470
+ blocks.append(_section_verified(proof, run_id))
471
+ blocks.append(_section_still_to_do(proof, completion))
472
+
473
+ parts = []
474
+ for b in blocks:
475
+ parts.append("\n".join(b))
476
+ return "\n\n".join(parts) + "\n"
477
+
478
+
479
+ def render_json(loki_dir):
480
+ run_id, proof = _latest_proof(loki_dir)
481
+ if proof is None:
482
+ return _empty_doc_json(loki_dir)
483
+
484
+ completion = _read_json(os.path.join(loki_dir, "state", "completion.json")) or {}
485
+ project_root = os.path.dirname(os.path.abspath(loki_dir)) or "."
486
+ usage_text = _read_text(os.path.join(project_root, "USAGE.md"))
487
+ live_url = _live_url(loki_dir)
488
+
489
+ honesty = proof.get("honesty") or {}
490
+ deployment = proof.get("deployment") or {}
491
+ facts = proof.get("facts") or {}
492
+ git = facts.get("git") or {}
493
+ diff = git.get("diff") or proof.get("files_changed") or {}
494
+
495
+ return {
496
+ "ok": True,
497
+ "found": True,
498
+ "run_id": run_id,
499
+ "ready": _is_ready(proof),
500
+ "headline": str(honesty.get("headline") or ""),
501
+ "degraded": honesty.get("degraded") or [],
502
+ "spec_brief": str((proof.get("spec") or {}).get("brief") or ""),
503
+ "files_changed": {
504
+ "count": diff.get("count") or 0,
505
+ "insertions": diff.get("insertions") or 0,
506
+ "deletions": diff.get("deletions") or 0,
507
+ },
508
+ "live_url": live_url,
509
+ "deployed_url": str(deployment.get("deployed_url") or ""),
510
+ "start_command": _usage_section(usage_text, "Start"),
511
+ "verify_command": _usage_section(usage_text, "Verify"),
512
+ "pr_url": str(completion.get("pr_url") or ""),
513
+ "assumptions_total": completion.get("assumptions_total") or 0,
514
+ "assumptions_high": completion.get("assumptions_high") or 0,
515
+ "proof_show_cmd": ("loki proof show %s" % run_id) if run_id else "",
516
+ "proof_verify_cmd": ("loki proof verify %s" % run_id) if run_id else "",
517
+ }
518
+
519
+
520
+ def main(argv=None):
521
+ parser = argparse.ArgumentParser(
522
+ description="Loki Mode finish-and-own renderer (plain-English ownership doc)"
523
+ )
524
+ parser.add_argument("--loki-dir", default=".loki")
525
+ fmt = parser.add_mutually_exclusive_group()
526
+ fmt.add_argument("--md", action="store_const", const="md", dest="fmt")
527
+ fmt.add_argument("--json", action="store_const", const="json", dest="fmt")
528
+ parser.set_defaults(fmt="md")
529
+ args = parser.parse_args(argv)
530
+
531
+ loki_dir = os.path.abspath(args.loki_dir)
532
+ try:
533
+ if args.fmt == "json":
534
+ print(json.dumps(render_json(loki_dir), indent=2))
535
+ else:
536
+ print(render_markdown(loki_dir))
537
+ except Exception as exc:
538
+ # This is a report, never a gate: emit an honest line and exit 0.
539
+ if args.fmt == "json":
540
+ print(json.dumps({"ok": False, "found": False, "error": str(exc)}))
541
+ else:
542
+ sys.stderr.write("warn: own-render failed: %s\n" % exc)
543
+ return 0
544
+
545
+
546
+ if __name__ == "__main__":
547
+ sys.exit(main())
package/autonomy/loki CHANGED
@@ -16504,6 +16504,12 @@ main() {
16504
16504
  # Secure-by-default gate surface: inspect findings + manage waivers.
16505
16505
  cmd_secure "$@"
16506
16506
  ;;
16507
+ own|handoff)
16508
+ # Finish-and-own: a plain-English ownership handoff for a non-technical
16509
+ # owner (what was built, is it working, how to run/deploy, what is left).
16510
+ # `loki handoff` is an alias for `loki own`.
16511
+ cmd_own "$@"
16512
+ ;;
16507
16513
  bench)
16508
16514
  cmd_bench "$@"
16509
16515
  ;;
@@ -30438,6 +30444,60 @@ cmd_bench() {
30438
30444
  bash "$bench_sh" "$@"
30439
30445
  }
30440
30446
 
30447
+ # loki own - finish-and-own (v7.88.0): a plain-English ownership handoff for a
30448
+ # NON-technical owner. A pure render (autonomy/lib/own-render.py) over the data
30449
+ # Loki already captured -- the Evidence Receipt, the completion summary, and
30450
+ # USAGE.md -- so it cannot fabricate: the "is it working?" verdict comes verbatim
30451
+ # from the receipt's honest headline and is never green unless the receipt is
30452
+ # VERIFIED. Default prints the doc; --md writes HANDOFF.md; --json for tooling.
30453
+ # `loki handoff` is an alias.
30454
+ cmd_own() {
30455
+ local renderer="${_LOKI_SCRIPT_DIR}/lib/own-render.py"
30456
+ if [ ! -f "$renderer" ]; then
30457
+ echo -e "${RED}Finish-and-own renderer not found (autonomy/lib/own-render.py).${NC}" >&2
30458
+ exit 2
30459
+ fi
30460
+ case "${1:-}" in
30461
+ --help|-h|help)
30462
+ echo -e "${BOLD}loki own${NC} - a plain-English ownership handoff (alias: loki handoff)"
30463
+ echo ""
30464
+ echo "Usage: loki own [--md | --json]"
30465
+ echo ""
30466
+ echo "Explains, in plain language for a non-technical owner: what was"
30467
+ echo "built, whether Loki verified it works, how to run it, how to put"
30468
+ echo "it online, what a developer needs to know, and what is left to do."
30469
+ echo "It reads the last build's Evidence Receipt + completion summary;"
30470
+ echo "the 'is it working' verdict is the receipt's honest headline (never"
30471
+ echo "green unless the build is VERIFIED)."
30472
+ echo ""
30473
+ echo "Options:"
30474
+ echo " --md Write HANDOFF.md to the project root"
30475
+ echo " --json Emit the structured handoff as JSON"
30476
+ exit 0
30477
+ ;;
30478
+ esac
30479
+ # --md writes HANDOFF.md at the project root (matches the help). The renderer
30480
+ # prints markdown on stdout; the CLI places it as a file, atomically (temp+mv)
30481
+ # so a partial write never leaves a truncated HANDOFF.md. Any other args
30482
+ # (--json, default) pass through and print.
30483
+ if [ "${1:-}" = "--md" ]; then
30484
+ local _handoff="${TARGET_DIR:-.}/HANDOFF.md"
30485
+ local _handoff_tmp="${TARGET_DIR:-.}/.HANDOFF.md.tmp"
30486
+ if python3 "$renderer" --loki-dir "${LOKI_DIR:-.loki}" --md > "$_handoff_tmp" 2>/dev/null; then
30487
+ mv -f "$_handoff_tmp" "$_handoff" && \
30488
+ echo -e "${GREEN}Wrote ${_handoff}${NC} - open it or hand it to whoever owns this build." || \
30489
+ { rm -f "$_handoff_tmp" 2>/dev/null; echo -e "${RED}Could not write HANDOFF.md${NC}" >&2; exit 1; }
30490
+ else
30491
+ rm -f "$_handoff_tmp" 2>/dev/null
30492
+ echo -e "${RED}Could not render the ownership handoff${NC}" >&2
30493
+ exit 1
30494
+ fi
30495
+ exit 0
30496
+ fi
30497
+ python3 "$renderer" --loki-dir "${LOKI_DIR:-.loki}" "$@"
30498
+ exit $?
30499
+ }
30500
+
30441
30501
  # loki secure - the secure-by-default gate surface (v7.87.0).
30442
30502
  # Subcommands: list (show findings) | waive <rule> <file> [reason] | unwaive.
30443
30503
  # Waivers are written to .loki/quality/security-waivers.json, which the gate
package/autonomy/run.sh CHANGED
@@ -17367,6 +17367,30 @@ main() {
17367
17367
  generate_proof_of_run "$result" || true
17368
17368
  fi
17369
17369
 
17370
+ # Finish-and-own (v7.88.0): write a plain-English ownership handoff
17371
+ # (HANDOFF.md) for a non-technical owner. Runs AFTER the proof so the
17372
+ # "is it working?" verdict reads the receipt's honest headline. Default-on,
17373
+ # opt out with LOKI_HANDOFF=0. Fire-and-forget: best-effort, never blocks
17374
+ # completion (same contract as the proof + usage-regen). A pure render over
17375
+ # the proof + completion + USAGE.md, so it cannot fabricate.
17376
+ if [ "${LOKI_HANDOFF:-1}" != "0" ]; then
17377
+ local _own_render="$SCRIPT_DIR/lib/own-render.py"
17378
+ if [ -f "$_own_render" ] && command -v python3 >/dev/null 2>&1; then
17379
+ # The renderer prints the plain-English doc on stdout (--md); the hook
17380
+ # places it at the project root as HANDOFF.md. Write to a temp then
17381
+ # move, so a partial write never leaves a truncated HANDOFF.md.
17382
+ local _handoff_dir _handoff_md _handoff_tmp
17383
+ _handoff_dir="${TARGET_DIR:-.}"
17384
+ _handoff_md="$_handoff_dir/HANDOFF.md"
17385
+ _handoff_tmp="$_handoff_dir/.HANDOFF.md.tmp"
17386
+ if python3 "$_own_render" --loki-dir "$LOKI_DIR" --md > "$_handoff_tmp" 2>/dev/null; then
17387
+ mv -f "$_handoff_tmp" "$_handoff_md" 2>/dev/null || rm -f "$_handoff_tmp" 2>/dev/null || true
17388
+ else
17389
+ rm -f "$_handoff_tmp" 2>/dev/null || true
17390
+ fi
17391
+ fi
17392
+ fi
17393
+
17370
17394
  # R7 (zero-config first run): "what next / go deeper" framing. Only when the
17371
17395
  # CLI flagged this as a TTFV first run and stdout is a TTY, so it stays
17372
17396
  # silent in CI / pipes and never fires for normal PRD runs. The wording
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.87.0"
10
+ __version__ = "7.88.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -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.87.0
5
+ **Version:** v7.88.0
6
6
 
7
7
  ---
8
8
 
@@ -395,7 +395,7 @@ provider works inside the container. Provide auth with your Anthropic API key:
395
395
  # Run Loki Mode in Docker (Claude provider, API-key auth)
396
396
  docker run --rm -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
397
397
  -v $(pwd):/workspace -w /workspace \
398
- asklokesh/loki-mode:7.87.0 start ./my-spec.md
398
+ asklokesh/loki-mode:7.88.0 start ./my-spec.md
399
399
  ```
400
400
 
401
401
  ##### docker compose + .env (no host install)
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- var QQ=Object.defineProperty;var ZQ=($)=>$;function zQ($,Q){this[$]=ZQ.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)QQ($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:zQ.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var q$=import.meta.require;var h1={};b(h1,{lokiDir:()=>P,homeLokiDir:()=>i$,findRepoRootForVersion:()=>t$,REPO_ROOT:()=>g});import{resolve as a,dirname as r$}from"path";import{fileURLToPath as XQ}from"url";import{existsSync as R$}from"fs";import{homedir as KQ}from"os";function qQ(){let $=b1;for(let Q=0;Q<6;Q++){if(R$(a($,"VERSION"))&&R$(a($,"autonomy/run.sh")))return $;let Z=r$($);if(Z===$)break;$=Z}return a(b1,"..","..","..")}function t$($){let Q=$;for(let Z=0;Z<6;Z++){if(R$(a(Q,"VERSION"))&&R$(a(Q,"autonomy/run.sh")))return Q;let z=r$(Q);if(z===Q)break;Q=z}return a($,"..","..","..")}function P(){return process.env.LOKI_DIR??a(process.cwd(),".loki")}function i$(){return a(KQ(),".loki")}var b1,g;var C=L(()=>{b1=r$(XQ(import.meta.url));g=qQ()});import{readFileSync as VQ}from"fs";import{resolve as JQ,dirname as UQ}from"path";import{fileURLToPath as WQ}from"url";function E$(){if(Q$!==null)return Q$;let $="7.87.0";if(typeof $==="string"&&$.length>0)return Q$=$,Q$;try{let Q=UQ(WQ(import.meta.url)),Z=t$(Q);Q$=VQ(JQ(Z,"VERSION"),"utf-8").trim()}catch{Q$="unknown"}return Q$}var Q$=null;var e$=L(()=>{C()});var g1={};b(g1,{runOrThrow:()=>HQ,run:()=>F,readStreamCapped:()=>m1,commandVersion:()=>BQ,commandExists:()=>f,ShellError:()=>$1,MAX_STDOUT_BYTES:()=>v1});async function m1($,Q=v1){let Z=$.getReader(),z=new TextDecoder,X="",q=0;try{while(q<Q){let{done:K,value:U}=await Z.read();if(K)break;if(!U)continue;if(q+=U.byteLength,q>Q){let J=U.byteLength-(q-Q);X+=z.decode(U.subarray(0,J),{stream:!0});break}X+=z.decode(U,{stream:!0})}X+=z.decode()}finally{try{await Z.cancel()}catch{}Z.releaseLock()}return X}async function F($,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,U]=await Promise.all([m1(Z.stdout),new Response(Z.stderr).text(),Z.exited]);return{stdout:q,stderr:K,exitCode:U}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function HQ($,Q={}){let Z=await F($,Q);if(Z.exitCode!==0)throw new $1(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=GQ($),Z=await F(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function GQ($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function BQ($,Q="--version"){if(!await f($))return null;let z=await F([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var v1=16777216,$1;var d=L(()=>{$1=class $1 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 s($){return YQ?"":$}var YQ,O,S,_,_Z,I,k,h,V;var c=L(()=>{YQ=(process.env.NO_COLOR??"").length>0;O=s("\x1B[0;31m"),S=s("\x1B[0;32m"),_=s("\x1B[1;33m"),_Z=s("\x1B[0;34m"),I=s("\x1B[0;36m"),k=s("\x1B[1m"),h=s("\x1B[2m"),V=s("\x1B[0m")});import{existsSync as jQ}from"fs";async function Z$(){if(Y$!==void 0)return Y$;let $="/opt/homebrew/bin/python3.12";if(jQ($))return Y$=$,$;let Q=await f("python3.12");if(Q)return Y$=Q,Q;let Z=await f("python3");return Y$=Z,Z}async function z$($,Q={}){let Z=await Z$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return F([Z,"-c",$],Q)}var Y$;var V$=L(()=>{d()});var X0={};b(X0,{runStatus:()=>oQ});import{existsSync as y,readFileSync as U$,readdirSync as r1,statSync as t1}from"fs";import{resolve as D,basename as vQ}from"path";import{homedir as mQ}from"os";function i1($){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 e1($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*N$/Q);if(X>N$)X=N$;let q=N$-X,K=S;if(z>=80)K=O;else if(z>=50)K=_;let U="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),J=i1($),W=i1(Q);return` ${k}${Z}${V} ${K}[${U}]${V} ${z}% (${J} / ${W})`}async function fQ(){if(await f("jq"))return!0;return process.stdout.write(`${O}Error: jq is required but not installed.${V}
2
+ var QQ=Object.defineProperty;var ZQ=($)=>$;function zQ($,Q){this[$]=ZQ.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)QQ($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:zQ.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var q$=import.meta.require;var h1={};b(h1,{lokiDir:()=>P,homeLokiDir:()=>i$,findRepoRootForVersion:()=>t$,REPO_ROOT:()=>g});import{resolve as a,dirname as r$}from"path";import{fileURLToPath as XQ}from"url";import{existsSync as R$}from"fs";import{homedir as KQ}from"os";function qQ(){let $=b1;for(let Q=0;Q<6;Q++){if(R$(a($,"VERSION"))&&R$(a($,"autonomy/run.sh")))return $;let Z=r$($);if(Z===$)break;$=Z}return a(b1,"..","..","..")}function t$($){let Q=$;for(let Z=0;Z<6;Z++){if(R$(a(Q,"VERSION"))&&R$(a(Q,"autonomy/run.sh")))return Q;let z=r$(Q);if(z===Q)break;Q=z}return a($,"..","..","..")}function P(){return process.env.LOKI_DIR??a(process.cwd(),".loki")}function i$(){return a(KQ(),".loki")}var b1,g;var C=L(()=>{b1=r$(XQ(import.meta.url));g=qQ()});import{readFileSync as VQ}from"fs";import{resolve as JQ,dirname as UQ}from"path";import{fileURLToPath as WQ}from"url";function E$(){if(Q$!==null)return Q$;let $="7.88.0";if(typeof $==="string"&&$.length>0)return Q$=$,Q$;try{let Q=UQ(WQ(import.meta.url)),Z=t$(Q);Q$=VQ(JQ(Z,"VERSION"),"utf-8").trim()}catch{Q$="unknown"}return Q$}var Q$=null;var e$=L(()=>{C()});var g1={};b(g1,{runOrThrow:()=>HQ,run:()=>F,readStreamCapped:()=>m1,commandVersion:()=>BQ,commandExists:()=>f,ShellError:()=>$1,MAX_STDOUT_BYTES:()=>v1});async function m1($,Q=v1){let Z=$.getReader(),z=new TextDecoder,X="",q=0;try{while(q<Q){let{done:K,value:U}=await Z.read();if(K)break;if(!U)continue;if(q+=U.byteLength,q>Q){let J=U.byteLength-(q-Q);X+=z.decode(U.subarray(0,J),{stream:!0});break}X+=z.decode(U,{stream:!0})}X+=z.decode()}finally{try{await Z.cancel()}catch{}Z.releaseLock()}return X}async function F($,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,U]=await Promise.all([m1(Z.stdout),new Response(Z.stderr).text(),Z.exited]);return{stdout:q,stderr:K,exitCode:U}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function HQ($,Q={}){let Z=await F($,Q);if(Z.exitCode!==0)throw new $1(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=GQ($),Z=await F(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function GQ($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function BQ($,Q="--version"){if(!await f($))return null;let z=await F([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var v1=16777216,$1;var d=L(()=>{$1=class $1 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 s($){return YQ?"":$}var YQ,O,S,_,_Z,I,k,h,V;var c=L(()=>{YQ=(process.env.NO_COLOR??"").length>0;O=s("\x1B[0;31m"),S=s("\x1B[0;32m"),_=s("\x1B[1;33m"),_Z=s("\x1B[0;34m"),I=s("\x1B[0;36m"),k=s("\x1B[1m"),h=s("\x1B[2m"),V=s("\x1B[0m")});import{existsSync as jQ}from"fs";async function Z$(){if(Y$!==void 0)return Y$;let $="/opt/homebrew/bin/python3.12";if(jQ($))return Y$=$,$;let Q=await f("python3.12");if(Q)return Y$=Q,Q;let Z=await f("python3");return Y$=Z,Z}async function z$($,Q={}){let Z=await Z$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return F([Z,"-c",$],Q)}var Y$;var V$=L(()=>{d()});var X0={};b(X0,{runStatus:()=>oQ});import{existsSync as y,readFileSync as U$,readdirSync as r1,statSync as t1}from"fs";import{resolve as D,basename as vQ}from"path";import{homedir as mQ}from"os";function i1($){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 e1($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*N$/Q);if(X>N$)X=N$;let q=N$-X,K=S;if(z>=80)K=O;else if(z>=50)K=_;let U="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),J=i1($),W=i1(Q);return` ${k}${Z}${V} ${K}[${U}]${V} ${z}% (${J} / ${W})`}async function fQ(){if(await f("jq"))return!0;return process.stdout.write(`${O}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)
@@ -796,4 +796,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
796
796
  `),2}default:return process.stderr.write(`Unknown command: ${Q}
797
797
  `),process.stderr.write($Q),2}}s1();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var qZ=await KZ(Bun.argv.slice(2));process.exit(qZ);
798
798
 
799
- //# debugId=7982E696DB64940A64756E2164756E21
799
+ //# debugId=65E93B4739145ECE64756E2164756E21
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '7.87.0'
60
+ __version__ = '7.88.0'
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": "7.87.0",
4
+ "version": "7.88.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": "7.87.0",
5
+ "version": "7.88.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",