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.
@@ -86,6 +86,180 @@ def _entry_points(root, files):
86
86
  return [c for c in candidates if c in fileset]
87
87
 
88
88
 
89
+ def _mermaid_label(text):
90
+ """Return a Mermaid-safe node label string (no injection, no parse breaks).
91
+
92
+ Mermaid breaks on quotes, brackets, and a handful of metacharacters, and a
93
+ crafted label could otherwise smuggle node/edge syntax. We keep only a
94
+ conservative character set (alphanumerics, space, and a few path-safe
95
+ punctuation marks) and collapse everything else to a space. The result is
96
+ always wrapped by the caller in double quotes inside ["..."], so the empty
97
+ string degrades to an empty-but-valid label rather than a syntax error.
98
+ """
99
+ safe = []
100
+ for ch in str(text or ""):
101
+ if ch.isalnum() or ch in " ._/-":
102
+ safe.append(ch)
103
+ else:
104
+ safe.append(" ")
105
+ out = "".join(safe).strip()
106
+ # Collapse runs of whitespace so labels stay compact + deterministic.
107
+ out = " ".join(out.split())
108
+ return out or "node"
109
+
110
+
111
+ def _classify_data_store(rel):
112
+ """Return a data-store label for a file that looks like a store, else None.
113
+
114
+ Heuristic + deterministic: matches well-known persistence/config surfaces
115
+ by path substring. Only real indexed files reach here, so any node emitted
116
+ is cited. Returns None when the file is not a recognizable data store.
117
+ """
118
+ low = rel.lower()
119
+ checks = [
120
+ ("schema", "Schema"),
121
+ ("migration", "Migrations"),
122
+ ("models", "Data Models"),
123
+ ("model.", "Data Models"),
124
+ ("storage", "Storage"),
125
+ ("database", "Database"),
126
+ ("/db/", "Database"),
127
+ ("db.", "Database"),
128
+ ("repository", "Repository"),
129
+ ("repositories", "Repository"),
130
+ ("dao", "Data Access"),
131
+ ("store.", "Store"),
132
+ (".sql", "SQL"),
133
+ ]
134
+ for needle, label in checks:
135
+ if needle in low:
136
+ return label
137
+ return None
138
+
139
+
140
+ def _data_stores(files, limit=4):
141
+ """Pick recognizable data-store files from the indexed set (real files).
142
+
143
+ Deterministic: scans files in sorted order (build_index returns them
144
+ sorted) and returns the first `limit` matches as {file, label}.
145
+ """
146
+ stores = []
147
+ for rel in files:
148
+ label = _classify_data_store(rel)
149
+ if label:
150
+ stores.append({"file": rel, "label": label})
151
+ if len(stores) >= limit:
152
+ break
153
+ return stores
154
+
155
+
156
+ def _architecture_diagram(index, modules, entries):
157
+ """Deterministic Mermaid flowchart: entry points -> modules -> data stores.
158
+
159
+ Every node is derived from the real codebase index (entry points, top
160
+ modules, data-store files) -- nothing is fabricated. Given the same index
161
+ the output is byte-identical (no Date, no random, fixed iteration order).
162
+ If the index is too sparse to draw a real graph, a minimal honest
163
+ single-node flowchart is returned instead of a fake one.
164
+ """
165
+ entry_nodes = list(entries[:4])
166
+ entry_set = set(entry_nodes)
167
+ # A file that is both an entry point and a top module is drawn once, as an
168
+ # entry point, so a node is never declared twice and no self-edge appears.
169
+ mod_nodes = [m["file"] for m in modules[:6] if m["file"] not in entry_set]
170
+ stores = _data_stores(index["files"])
171
+
172
+ # Sparse-index guard: with no entry points and no modules there is nothing
173
+ # real to draw. Emit a minimal honest diagram rather than inventing nodes.
174
+ if not entry_nodes and not mod_nodes:
175
+ return "flowchart TD\n src[\"Source files\"]"
176
+
177
+ lines = ["flowchart TD"]
178
+ ids = {}
179
+ counter = 0
180
+
181
+ def node_id(key):
182
+ nonlocal counter
183
+ if key not in ids:
184
+ ids[key] = "n%d" % counter
185
+ counter += 1
186
+ return ids[key]
187
+
188
+ # Declare nodes in a fixed order (entries, modules, stores) so the diagram
189
+ # is deterministic for a given index.
190
+ for e in entry_nodes:
191
+ lines.append(" %s[\"%s\"]" % (node_id(e), _mermaid_label(e)))
192
+ for m in mod_nodes:
193
+ lines.append(" %s[\"%s\"]" % (node_id(m), _mermaid_label(m)))
194
+ for s in stores:
195
+ lines.append(
196
+ " %s[(\"%s\")]" % (node_id("store:" + s["file"]), _mermaid_label(s["label"]))
197
+ )
198
+
199
+ # Edges: every entry point feeds every top module (a coarse but honest
200
+ # "entry -> module" relation), and modules feed the data stores. When there
201
+ # are no entry points, modules stand alone at the top.
202
+ sources = entry_nodes if entry_nodes else mod_nodes
203
+ targets = mod_nodes if entry_nodes else []
204
+ for src in sources:
205
+ for tgt in targets:
206
+ lines.append(" %s --> %s" % (node_id(src), node_id(tgt)))
207
+ if stores:
208
+ store_sources = mod_nodes if mod_nodes else entry_nodes
209
+ for src in store_sources[:3]:
210
+ for s in stores:
211
+ lines.append(
212
+ " %s --> %s" % (node_id(src), node_id("store:" + s["file"]))
213
+ )
214
+ return "\n".join(lines)
215
+
216
+
217
+ def _data_flow_diagram(index, modules, entries):
218
+ """Deterministic Mermaid flowchart for the request/data path.
219
+
220
+ Models the path as: entry point -> the top modules in rank order -> data
221
+ store, using only real indexed files. Same index -> same diagram. Falls
222
+ back to a minimal honest diagram when the index is too sparse.
223
+ """
224
+ entry = entries[0] if entries else None
225
+ # Drop the entry file from the module chain so it is not visited twice
226
+ # (which would create a self-edge); the chain stays a simple acyclic path.
227
+ mod_chain = [m["file"] for m in modules[:4] if m["file"] != entry]
228
+ stores = _data_stores(index["files"], limit=1)
229
+
230
+ if not entry and not mod_chain:
231
+ return "flowchart LR\n src[\"Source files\"]"
232
+
233
+ lines = ["flowchart LR"]
234
+ ids = {}
235
+ counter = 0
236
+
237
+ def node_id(key):
238
+ nonlocal counter
239
+ if key not in ids:
240
+ ids[key] = "f%d" % counter
241
+ counter += 1
242
+ return ids[key]
243
+
244
+ # Build an ordered chain of real nodes: entry -> modules -> store.
245
+ chain = []
246
+ if entry:
247
+ chain.append(("entry", entry, _mermaid_label(entry)))
248
+ for m in mod_chain:
249
+ chain.append(("mod", m, _mermaid_label(m)))
250
+ if stores:
251
+ chain.append(("store", "store:" + stores[0]["file"], _mermaid_label(stores[0]["label"])))
252
+
253
+ for kind, key, label in chain:
254
+ if kind == "store":
255
+ lines.append(" %s[(\"%s\")]" % (node_id(key), label))
256
+ else:
257
+ lines.append(" %s[\"%s\"]" % (node_id(key), label))
258
+ for i in range(len(chain) - 1):
259
+ lines.append(" %s --> %s" % (node_id(chain[i][1]), node_id(chain[i + 1][1])))
260
+ return "\n".join(lines)
261
+
262
+
89
263
  def _llm_prose(section, context, fallback):
90
264
  """Get prose for a section from the LLM, or use the deterministic fallback."""
91
265
  prompt = (
@@ -137,8 +311,11 @@ def _section_overview(root, index, modules, entries, context):
137
311
  for m in modules[:4]:
138
312
  line = m["defs"][0]["line"] if m["defs"] else 1
139
313
  citations.append({"file": m["file"], "line": line})
314
+ # Mermaid flowchart derived from the real index (entry -> modules -> stores).
315
+ # Raw mermaid source (no ``` wrapper); the UI wraps/renders it.
316
+ diagram = _architecture_diagram(index, modules, entries)
140
317
  return {"id": "architecture", "title": "Architecture Overview",
141
- "body": prose, "citations": citations}
318
+ "body": prose, "citations": citations, "diagram": diagram}
142
319
 
143
320
 
144
321
  def _section_modules(root, modules, context):
@@ -165,7 +342,7 @@ def _section_modules(root, modules, context):
165
342
  "body": "\n".join(body_parts), "citations": citations}
166
343
 
167
344
 
168
- def _section_data_flow(root, index, entries, context):
345
+ def _section_data_flow(root, index, modules, entries, context):
169
346
  fallback = (
170
347
  "Execution begins at the entry point(s) (%s) and flows through the "
171
348
  "key modules. Trace a request from the entry file into the modules it "
@@ -175,8 +352,11 @@ def _section_data_flow(root, index, entries, context):
175
352
  citations = [{"file": e, "line": 1} for e in entries[:4]]
176
353
  if not citations and index["files"]:
177
354
  citations = [{"file": index["files"][0], "line": 1}]
355
+ # Mermaid data-flow chain derived from the real index (entry -> modules ->
356
+ # store). Raw mermaid source (no ``` wrapper); the UI wraps/renders it.
357
+ diagram = _data_flow_diagram(index, modules, entries)
178
358
  return {"id": "data-flow", "title": "Data Flow",
179
- "body": prose, "citations": citations}
359
+ "body": prose, "citations": citations, "diagram": diagram}
180
360
 
181
361
 
182
362
  def _validate_citations(root, citations):
@@ -203,6 +383,14 @@ def _validate_citations(root, citations):
203
383
 
204
384
  def _render_md(section):
205
385
  lines = ["## %s" % section["title"], "", section["body"], ""]
386
+ # Render the Mermaid diagram (when present) as a fenced mermaid block so
387
+ # the markdown view shows the same visual the dashboard renders.
388
+ diagram = section.get("diagram")
389
+ if diagram:
390
+ lines.append("```mermaid")
391
+ lines.append(diagram)
392
+ lines.append("```")
393
+ lines.append("")
206
394
  if section["citations"]:
207
395
  lines.append("**Sources:**")
208
396
  for c in section["citations"]:
@@ -259,7 +447,7 @@ def main(argv=None):
259
447
  sections = [
260
448
  _section_overview(root, index, modules, entries, context),
261
449
  _section_modules(root, modules, context),
262
- _section_data_flow(root, index, entries, context),
450
+ _section_data_flow(root, index, modules, entries, context),
263
451
  ]
264
452
 
265
453
  # Enforce the grounding contract: validate every citation against disk.
package/autonomy/loki CHANGED
@@ -5315,14 +5315,17 @@ cmd_welcome_terminal() {
5315
5315
  echo -e " Quick start: ${BOLD}loki quickstart${NC} (from your idea) or ${BOLD}loki start ./prd.md${NC} (from a spec)"
5316
5316
  echo -e " Docs: ${BOLD}https://www.autonomi.dev/docs${NC}"
5317
5317
  echo ""
5318
+ # One-time honest disclosure (this welcome screen shows once, via the
5319
+ # WELCOME_MARKER). On-by-default anonymous diagnostics must be disclosed
5320
+ # somewhere so collection is never covert; this single line is that
5321
+ # disclosure. It is NOT repeated on later runs. Enterprise/CI/air-gapped
5322
+ # installs auto-disable collection, so the line only appears when it is
5323
+ # actually on. Opt out anytime: loki telemetry off.
5318
5324
  if _loki_welcome_analytics_on; then
5319
- echo -e " ${DIM}Anonymous usage analytics are ON (you opted in). We never collect${NC}"
5320
- echo -e " ${DIM}prompts, PRDs, or code. Opt out: loki telemetry off${NC}"
5321
- else
5322
- echo -e " ${DIM}Anonymous analytics are off by default. Nothing is sent unless you${NC}"
5323
- echo -e " ${DIM}opt in with: loki telemetry on${NC}"
5325
+ echo -e " ${DIM}Anonymous diagnostics on (os/arch/version/error type only -- never${NC}"
5326
+ echo -e " ${DIM}code, prompts, or paths). Off: loki telemetry off - docs/PRIVACY.md${NC}"
5327
+ echo ""
5324
5328
  fi
5325
- echo ""
5326
5329
  }
5327
5330
 
5328
5331
  cmd_welcome() {
@@ -5419,7 +5422,7 @@ cmd_web() {
5419
5422
  case "$subcommand" in
5420
5423
  start)
5421
5424
  shift || true
5422
- cmd_web_start "$@"
5425
+ cmd_web_redirect_to_dashboard "$@"
5423
5426
  ;;
5424
5427
  stop)
5425
5428
  cmd_web_stop
@@ -5449,51 +5452,87 @@ cmd_web() {
5449
5452
  *)
5450
5453
  # Treat unknown args as options to start (e.g., loki web --no-open)
5451
5454
  shift
5452
- cmd_web_start "$subcommand" "$@"
5455
+ cmd_web_redirect_to_dashboard "$subcommand" "$@"
5453
5456
  ;;
5454
5457
  esac
5455
5458
  }
5456
5459
 
5460
+ # v7.x (F55): `loki web` (the command a user intuitively types) used to launch
5461
+ # the deprecated Purple Lab, whose '/' redirects to '/lab/' and whose port has
5462
+ # none of the real dashboard APIs (/api/status, /trust, etc. all 404). A user
5463
+ # typing the obvious command landed on the wrong, broken-looking surface. The
5464
+ # value-preserving fix: the START path now launches the real dashboard instead.
5465
+ # 'loki web stop|status|logs' still operate on Purple Lab so anyone who started
5466
+ # it the old way (or via scripts) is never stranded. The deprecation banner has
5467
+ # already printed in cmd_web() before we get here.
5468
+ cmd_web_redirect_to_dashboard() {
5469
+ # The dashboard start path does not accept Purple-Lab-only flags
5470
+ # (--no-open, --prd). Filter them so `loki web --no-open` (used in CI) and
5471
+ # `loki web --prd X` keep working instead of erroring on an unknown option.
5472
+ # --port is shared and passes straight through.
5473
+ local orig_args=("$@")
5474
+ local dash_args=()
5475
+ while [[ $# -gt 0 ]]; do
5476
+ case "$1" in
5477
+ --no-open)
5478
+ # Dashboard has no browser-open behavior to suppress; drop it.
5479
+ shift
5480
+ ;;
5481
+ --prd)
5482
+ # Purple-Lab-only prefill; not supported by the dashboard.
5483
+ shift 2 2>/dev/null || shift
5484
+ ;;
5485
+ --prd=*)
5486
+ shift
5487
+ ;;
5488
+ *)
5489
+ dash_args+=("$1")
5490
+ shift
5491
+ ;;
5492
+ esac
5493
+ done
5494
+
5495
+ # Safe empty-array expansion (bash 3.2 + set -u): ${arr[@]+"${arr[@]}"}
5496
+ if ! _deprecated_alias_should_suppress ${orig_args[@]+"${orig_args[@]}"}; then
5497
+ echo "Launching the dashboard (http://localhost:${DASHBOARD_DEFAULT_PORT}) instead of the deprecated Purple Lab." >&2
5498
+ fi
5499
+ cmd_dashboard_start ${dash_args[@]+"${dash_args[@]}"}
5500
+ }
5501
+
5457
5502
  cmd_web_help() {
5458
5503
  echo -e "${BOLD}Purple Lab -- Loki Mode Web UI (deprecated -- use the dashboard)${NC}"
5459
5504
  echo ""
5460
5505
  echo "Usage: loki web [command] [options]"
5461
5506
  echo ""
5462
5507
  echo "DEPRECATED as of v7.44.0: Purple Lab is consolidated into the dashboard."
5463
- echo " For local build + monitoring, use the dashboard (auto-launches at"
5464
- echo " 'loki start', http://localhost:${DASHBOARD_DEFAULT_PORT}; or 'loki dashboard')."
5465
- echo " For the hosted platform, see Autonomi Cloud."
5466
- echo " 'loki web' still works for now (it launches after a deprecation notice),"
5467
- echo " but it will be removed in a future release. Migrate to the dashboard."
5508
+ echo " 'loki web' (no subcommand) now LAUNCHES THE DASHBOARD"
5509
+ echo " (http://localhost:${DASHBOARD_DEFAULT_PORT}) instead of the deprecated Purple"
5510
+ echo " Lab, so the obvious command lands you on the working UI. Use the embedded"
5511
+ echo " 'Lab' tab there to submit a PRD. For the hosted platform, see Autonomi Cloud."
5468
5512
  echo ""
5469
- echo "Note: 'loki web' is Purple Lab (the PRD-input/build-watch UI, port ${PURPLE_LAB_DEFAULT_PORT})."
5470
- echo " It is NOT the same as 'loki dashboard' (operations UI, port ${DASHBOARD_DEFAULT_PORT})."
5471
- echo " Use 'loki dashboard' to monitor running agents, tasks, costs, council, escalations."
5513
+ echo "Note: the canonical command is 'loki dashboard' (operations UI, port ${DASHBOARD_DEFAULT_PORT})."
5514
+ echo " Use it to monitor running agents, tasks, costs, council, escalations."
5472
5515
  echo ""
5473
5516
  echo "Commands:"
5474
- echo " start Start Purple Lab (default)"
5475
- echo " stop Stop Purple Lab server"
5517
+ echo " start Launch the dashboard (default; Purple Lab is deprecated)"
5518
+ echo " stop Stop the Purple Lab server (if one was started the old way)"
5476
5519
  echo " status Show Purple Lab server status"
5477
5520
  echo " logs Show Purple Lab server logs"
5478
5521
  echo " help Show this help"
5479
5522
  echo ""
5480
5523
  echo "Options (for start):"
5481
- echo " --no-open Don't open browser automatically"
5482
- echo " --port PORT Use custom port (default: ${PURPLE_LAB_DEFAULT_PORT})"
5483
- echo ""
5484
- echo "Purple Lab is the product UI where you input PRDs and watch agents build."
5485
- echo "It runs its own backend (web-app/server.py) on port ${PURPLE_LAB_DEFAULT_PORT}."
5524
+ echo " --port PORT Use custom port (default: ${DASHBOARD_DEFAULT_PORT})"
5525
+ echo " --no-open Accepted for back-compat; ignored (no-op on the dashboard)"
5486
5526
  echo ""
5487
5527
  echo "Examples:"
5488
- echo " loki web Start Purple Lab and open in browser"
5489
- echo " loki web --no-open Start without opening browser"
5490
- echo " loki web stop Stop the Purple Lab server"
5528
+ echo " loki web Launch the dashboard"
5529
+ echo " loki web --no-open Launch the dashboard (--no-open is a no-op)"
5530
+ echo " loki web stop Stop a Purple Lab server started the old way"
5491
5531
  echo ""
5492
5532
  echo "Note (since v7.5.30):"
5493
- echo " Purple Lab is also embedded in the Dashboard as a Lab sidebar entry."
5494
- echo " Run 'loki dashboard start' and click 'Lab' to use the same UI"
5495
- echo " without spawning a second port. 'loki web' standalone remains"
5496
- echo " supported (Rule 0); both modes serve the same React bundle."
5533
+ echo " The same PRD-input UI is embedded in the dashboard as a 'Lab' sidebar entry."
5534
+ echo " 'loki web stop|status|logs' still operate on a standalone Purple Lab"
5535
+ echo " process so anyone who started one the old way is never stranded."
5497
5536
  }
5498
5537
 
5499
5538
  cmd_web_start() {
@@ -16110,6 +16149,18 @@ worktree wt projects cp rc trust-metrics serve agent code self_update test help
16110
16149
 
16111
16150
  # Main command dispatcher
16112
16151
  main() {
16152
+ # v7.89.0 telemetry TTY fix (council cH_r1 AC1): resolve interactivity EXACTLY
16153
+ # ONCE here, while autonomy/loki main still owns the user's real TTY, and
16154
+ # export the explicit signal that the telemetry/crash gates trust instead of a
16155
+ # late `-t` re-probe. When invoked via the bin/loki shim the var is already
16156
+ # exported (and re-exporting the same value is a no-op); this covers the path
16157
+ # where autonomy/loki is invoked directly (LOKI_LEGACY_BASH, no-Bun, or a
16158
+ # direct call). Only set when a terminal is actually present; leave UNSET
16159
+ # otherwise so isolated gate unit tests fall back to their own `-t` probe.
16160
+ if [ -z "${LOKI_TTY_INTERACTIVE:-}" ] && { [ -t 1 ] || [ -t 0 ]; }; then
16161
+ export LOKI_TTY_INTERACTIVE=1
16162
+ fi
16163
+
16113
16164
  # v7.5.18: early guard -- LOKI_PROVIDER=gemini is no longer supported.
16114
16165
  if [ "${LOKI_PROVIDER:-}" = "gemini" ]; then
16115
16166
  echo -e "${RED}Error: Provider 'gemini' is deprecated as of v7.5.18 and has been removed.${NC}" >&2
@@ -16131,6 +16182,19 @@ main() {
16131
16182
  # v7.4.13: first-run telemetry moved to bin/loki shim so it fires for
16132
16183
  # both Bun-routed and bash-routed commands (autonomy/loki main() never
16133
16184
  # runs for the 8 ported commands). Marker file: ~/.loki-first-run.
16185
+ #
16186
+ # v7.89.0 (council cH_r1 AC4): this is a FOREGROUND egress on the bash route.
16187
+ # Before it can phone home, the user must have seen the disclosure once -- a
16188
+ # bash-routed first command must never be covert. Evaluate the SAME gate the
16189
+ # egress uses, and if collection is genuinely enabled, disclose once (shared
16190
+ # impl from telemetry.sh, keyed on its own marker, so it never double-prints
16191
+ # with the Bun route or repeats). loki_telemetry itself re-checks the gate, so
16192
+ # this only adds the disclosure; it never changes whether we send.
16193
+ if declare -f _loki_telemetry_enabled >/dev/null 2>&1 \
16194
+ && declare -f _loki_disclose_telemetry_once >/dev/null 2>&1 \
16195
+ && _loki_telemetry_enabled; then
16196
+ _loki_disclose_telemetry_once
16197
+ fi
16134
16198
  loki_telemetry "cli_command" "command=$command" 2>/dev/null || true
16135
16199
 
16136
16200
  # Unified config-file pre-pass (#691). For session commands ONLY, honor
@@ -16504,6 +16568,12 @@ main() {
16504
16568
  # Secure-by-default gate surface: inspect findings + manage waivers.
16505
16569
  cmd_secure "$@"
16506
16570
  ;;
16571
+ own|handoff)
16572
+ # Finish-and-own: a plain-English ownership handoff for a non-technical
16573
+ # owner (what was built, is it working, how to run/deploy, what is left).
16574
+ # `loki handoff` is an alias for `loki own`.
16575
+ cmd_own "$@"
16576
+ ;;
16507
16577
  bench)
16508
16578
  cmd_bench "$@"
16509
16579
  ;;
@@ -21941,13 +22011,32 @@ try {
21941
22011
 
21942
22012
  # Unified collection state (PostHog usage telemetry + crash reporting).
21943
22013
  # loki_collection_enabled is the single source of truth (crash.sh).
21944
- # Collection is OPT-IN: OFF by default, enabled only after opt-in.
22014
+ # Collection is ON BY DEFAULT for individual interactive installs and
22015
+ # auto-off in enterprise/CI/air-gapped/non-interactive contexts; an
22016
+ # explicit opt-out always wins. AC8 (council cH_r1): the status copy
22017
+ # must distinguish "on by default" from an explicit "you opted in".
21945
22018
  echo ""
21946
22019
  if type loki_collection_enabled &>/dev/null; then
21947
22020
  if loki_collection_enabled; then
21948
- echo -e " Collection: ${GREEN}enabled${NC} (you opted in; anonymous diagnostics; opt out with: loki telemetry off)"
22021
+ # Detect an EXPLICIT opt-in (env LOKI_TELEMETRY=on, or the
22022
+ # persistent ~/.loki/config TELEMETRY_ENABLED=true written by
22023
+ # `loki telemetry on`). Anything else that is enabled is the
22024
+ # individual on-by-default path.
22025
+ local _telem_lower_status
22026
+ _telem_lower_status="$(printf '%s' "${LOKI_TELEMETRY:-}" | tr '[:upper:]' '[:lower:]')"
22027
+ local _explicit_optin=false
22028
+ if [ "$_telem_lower_status" = "on" ]; then
22029
+ _explicit_optin=true
22030
+ elif [ -f "$global_config" ] && grep -q "^TELEMETRY_ENABLED=true" "$global_config" 2>/dev/null; then
22031
+ _explicit_optin=true
22032
+ fi
22033
+ if [ "$_explicit_optin" = true ]; then
22034
+ echo -e " Collection: ${GREEN}enabled${NC} (you opted in; anonymous diagnostics; opt out with: loki telemetry off)"
22035
+ else
22036
+ echo -e " Collection: ${GREEN}on by default${NC} (anonymous diagnostics; opt out with: loki telemetry off)"
22037
+ fi
21949
22038
  else
21950
- echo -e " Collection: ${YELLOW}off by default${NC} (nothing sent; opt in with: loki telemetry on)"
22039
+ echo -e " Collection: ${YELLOW}off${NC} (nothing sent; enterprise/CI/air-gapped or opted out; opt in with: loki telemetry on)"
21951
22040
  fi
21952
22041
  fi
21953
22042
 
@@ -30438,6 +30527,60 @@ cmd_bench() {
30438
30527
  bash "$bench_sh" "$@"
30439
30528
  }
30440
30529
 
30530
+ # loki own - finish-and-own (v7.88.0): a plain-English ownership handoff for a
30531
+ # NON-technical owner. A pure render (autonomy/lib/own-render.py) over the data
30532
+ # Loki already captured -- the Evidence Receipt, the completion summary, and
30533
+ # USAGE.md -- so it cannot fabricate: the "is it working?" verdict comes verbatim
30534
+ # from the receipt's honest headline and is never green unless the receipt is
30535
+ # VERIFIED. Default prints the doc; --md writes HANDOFF.md; --json for tooling.
30536
+ # `loki handoff` is an alias.
30537
+ cmd_own() {
30538
+ local renderer="${_LOKI_SCRIPT_DIR}/lib/own-render.py"
30539
+ if [ ! -f "$renderer" ]; then
30540
+ echo -e "${RED}Finish-and-own renderer not found (autonomy/lib/own-render.py).${NC}" >&2
30541
+ exit 2
30542
+ fi
30543
+ case "${1:-}" in
30544
+ --help|-h|help)
30545
+ echo -e "${BOLD}loki own${NC} - a plain-English ownership handoff (alias: loki handoff)"
30546
+ echo ""
30547
+ echo "Usage: loki own [--md | --json]"
30548
+ echo ""
30549
+ echo "Explains, in plain language for a non-technical owner: what was"
30550
+ echo "built, whether Loki verified it works, how to run it, how to put"
30551
+ echo "it online, what a developer needs to know, and what is left to do."
30552
+ echo "It reads the last build's Evidence Receipt + completion summary;"
30553
+ echo "the 'is it working' verdict is the receipt's honest headline (never"
30554
+ echo "green unless the build is VERIFIED)."
30555
+ echo ""
30556
+ echo "Options:"
30557
+ echo " --md Write HANDOFF.md to the project root"
30558
+ echo " --json Emit the structured handoff as JSON"
30559
+ exit 0
30560
+ ;;
30561
+ esac
30562
+ # --md writes HANDOFF.md at the project root (matches the help). The renderer
30563
+ # prints markdown on stdout; the CLI places it as a file, atomically (temp+mv)
30564
+ # so a partial write never leaves a truncated HANDOFF.md. Any other args
30565
+ # (--json, default) pass through and print.
30566
+ if [ "${1:-}" = "--md" ]; then
30567
+ local _handoff="${TARGET_DIR:-.}/HANDOFF.md"
30568
+ local _handoff_tmp="${TARGET_DIR:-.}/.HANDOFF.md.tmp"
30569
+ if python3 "$renderer" --loki-dir "${LOKI_DIR:-.loki}" --md > "$_handoff_tmp" 2>/dev/null; then
30570
+ mv -f "$_handoff_tmp" "$_handoff" && \
30571
+ echo -e "${GREEN}Wrote ${_handoff}${NC} - open it or hand it to whoever owns this build." || \
30572
+ { rm -f "$_handoff_tmp" 2>/dev/null; echo -e "${RED}Could not write HANDOFF.md${NC}" >&2; exit 1; }
30573
+ else
30574
+ rm -f "$_handoff_tmp" 2>/dev/null
30575
+ echo -e "${RED}Could not render the ownership handoff${NC}" >&2
30576
+ exit 1
30577
+ fi
30578
+ exit 0
30579
+ fi
30580
+ python3 "$renderer" --loki-dir "${LOKI_DIR:-.loki}" "$@"
30581
+ exit $?
30582
+ }
30583
+
30441
30584
  # loki secure - the secure-by-default gate surface (v7.87.0).
30442
30585
  # Subcommands: list (show findings) | waive <rule> <file> [reason] | unwaive.
30443
30586
  # Waivers are written to .loki/quality/security-waivers.json, which the gate
@@ -30589,7 +30732,7 @@ cmd_proof() {
30589
30732
  # "No proofs found" line -- matching the Bun route (proof.ts
30590
30733
  # listProofs, which returns before the header when rows is empty).
30591
30734
  if [ "$found" -eq 0 ]; then
30592
- printf "%-26s %-20s %-10s %-9s %s\n" "RUN_ID" "GENERATED_AT" "VERDICT" "COST_USD" "FILES"
30735
+ printf "%-26s %-20s %-18s %-9s %s\n" "RUN_ID" "GENERATED_AT" "VERDICT" "COST_USD" "FILES"
30593
30736
  fi
30594
30737
  found=1
30595
30738
  LOKI_PROOF_JSON="$pj" python3 - <<'PYEOF'
@@ -30607,10 +30750,14 @@ def s(v):
30607
30750
  return "-" if v is None else str(v)
30608
30751
  run_id = d.get("run_id")
30609
30752
  gen = d.get("generated_at")
30610
- verdict = (d.get("council") or {}).get("final_verdict")
30753
+ # The honest verdict lives in honesty.headline (VERIFIED / VERIFIED WITH GAPS /
30754
+ # NOT VERIFIED); fall back to legacy council.final_verdict for older proofs.
30755
+ verdict = (d.get("honesty") or {}).get("headline")
30756
+ if verdict is None:
30757
+ verdict = (d.get("council") or {}).get("final_verdict")
30611
30758
  cost = (d.get("cost") or {}).get("usd")
30612
30759
  files = (d.get("files_changed") or {}).get("count")
30613
- print("{:<26} {:<20} {:<10} {:<9} {}".format(
30760
+ print("{:<26} {:<20} {:<18} {:<9} {}".format(
30614
30761
  s(run_id), s(gen), s(verdict), s(cost), s(files)))
30615
30762
  PYEOF
30616
30763
  done