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
|
@@ -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
|
|
5320
|
-
echo -e " ${DIM}
|
|
5321
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 "
|
|
5464
|
-
echo "
|
|
5465
|
-
echo "
|
|
5466
|
-
echo " '
|
|
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:
|
|
5470
|
-
echo "
|
|
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
|
|
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 " --
|
|
5482
|
-
echo " --
|
|
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
|
|
5489
|
-
echo " loki web --no-open
|
|
5490
|
-
echo " loki web stop Stop
|
|
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 "
|
|
5494
|
-
echo "
|
|
5495
|
-
echo "
|
|
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
|
|
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
|
-
|
|
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
|
|
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 %-
|
|
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
|
|
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} {:<
|
|
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
|