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
|
@@ -820,15 +820,37 @@ print("%d %d" % (total, high))
|
|
|
820
820
|
# resolution (sets confirmed=true). This is deliberately the same teeth the
|
|
821
821
|
# LOKI_ASSUMPTIONS_REQUIRE_CONFIRM=1 path applies to ALL entries, scoped here to
|
|
822
822
|
# just contradictions in default autonomous mode. See the design note below.
|
|
823
|
+
#
|
|
824
|
+
# F50 (zero-config brief mode): a one-line-brief run (LOKI_TTFV=brief) is the
|
|
825
|
+
# zero-config first-pass path. There is no human in the loop and the audience
|
|
826
|
+
# is non-technical: a permanently-blocking contradiction makes a brief run grind
|
|
827
|
+
# to max-iterations and NEVER cleanly complete -- a broken first-run UX for the
|
|
828
|
+
# exact users the brief path exists to serve. Worse, a synthetic brief-PRD that
|
|
829
|
+
# Loki itself wrote is unlikely to carry a real human-authored contradiction;
|
|
830
|
+
# leaving the gate blocking yields no signal a non-technical user can act on.
|
|
831
|
+
# So in brief mode contradictions are RESOLVED-WITH-DEFAULT, not blocked: Loki
|
|
832
|
+
# acknowledges the entry, records resolved_with_default=true plus an honest
|
|
833
|
+
# resolution note, and the gate is allowed to clear. This is NOT hiding the gap:
|
|
834
|
+
# spec_ledger_counts still counts the entry, build_completion_summary still folds
|
|
835
|
+
# it into the proof-of-done, ledger.md still lists it, and `loki own` still tells
|
|
836
|
+
# the user to review the assumptions Loki had to make. PRD mode (a real
|
|
837
|
+
# human-authored spec, where a human CAN resolve a contradiction) keeps the
|
|
838
|
+
# original BLOCKING behavior unchanged. brief mode is detected from LOKI_TTFV=brief
|
|
839
|
+
# (set by cmd_start) and is overridable by the explicit param for tests/callers.
|
|
840
|
+
# Usage: spec_ledger_acknowledge_all [brief_mode] # brief_mode: "brief" to resolve contradictions
|
|
823
841
|
# ---------------------------------------------------------------------------
|
|
824
842
|
spec_ledger_acknowledge_all() {
|
|
825
843
|
[ "${LOKI_ASSUMPTIONS_REQUIRE_CONFIRM:-0}" = "1" ] && return 0
|
|
826
|
-
local dir
|
|
844
|
+
local dir brief_mode
|
|
845
|
+
# F50: brief/zero-config mode resolves contradictions with a default rather
|
|
846
|
+
# than blocking forever. Explicit param wins; otherwise read LOKI_TTFV.
|
|
847
|
+
brief_mode="${1:-${LOKI_TTFV:-}}"
|
|
827
848
|
dir="$(_spec_ledger_dir)"
|
|
828
849
|
[ -d "$dir" ] || return 0
|
|
829
|
-
_SL_DIR="$dir" python3 -c '
|
|
850
|
+
_SL_DIR="$dir" _SL_BRIEF="$brief_mode" python3 -c '
|
|
830
851
|
import glob, json, os, tempfile
|
|
831
852
|
d = os.environ["_SL_DIR"]
|
|
853
|
+
brief = os.environ.get("_SL_BRIEF", "") == "brief"
|
|
832
854
|
for p in glob.glob(os.path.join(d, "a-*.json")):
|
|
833
855
|
try:
|
|
834
856
|
with open(p) as f:
|
|
@@ -837,10 +859,22 @@ for p in glob.glob(os.path.join(d, "a-*.json")):
|
|
|
837
859
|
continue
|
|
838
860
|
if r.get("acknowledged"):
|
|
839
861
|
continue
|
|
840
|
-
|
|
841
|
-
|
|
862
|
+
is_contradiction = r.get("class") == "contradictory"
|
|
863
|
+
# P2-4: a contradiction cannot be assumed away, so it is never auto-acked
|
|
864
|
+
# in default / PRD mode (a human can resolve it there).
|
|
865
|
+
if is_contradiction and not brief:
|
|
842
866
|
continue
|
|
843
867
|
r["acknowledged"] = True
|
|
868
|
+
# F50: brief-mode contradictions are resolved-with-default, marked honestly
|
|
869
|
+
# so the receipt / ledger / `loki own` can show "Loki chose a default here".
|
|
870
|
+
if is_contradiction and brief:
|
|
871
|
+
r["resolved_with_default"] = True
|
|
872
|
+
if not r.get("resolution_note"):
|
|
873
|
+
r["resolution_note"] = (
|
|
874
|
+
"Resolved with a reasonable default in zero-config brief mode "
|
|
875
|
+
"(no human in the loop). Review and refine via a full PRD run "
|
|
876
|
+
"(loki start ./prd.md) if this default is wrong."
|
|
877
|
+
)
|
|
844
878
|
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(p), suffix=".tmp")
|
|
845
879
|
try:
|
|
846
880
|
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
@@ -907,7 +941,17 @@ else:
|
|
|
907
941
|
lines.append("Total assumptions: %d (%d high-severity)" % (len(entries), high))
|
|
908
942
|
lines.append("")
|
|
909
943
|
for e in entries:
|
|
910
|
-
|
|
944
|
+
# F50: surface a resolved-with-default state distinctly so a brief-mode
|
|
945
|
+
# contradiction reads honestly ("Loki chose a default") rather than as a
|
|
946
|
+
# plain ack or a silent pass.
|
|
947
|
+
if e.get("confirmed"):
|
|
948
|
+
state = "confirmed"
|
|
949
|
+
elif e.get("resolved_with_default"):
|
|
950
|
+
state = "resolved-with-default"
|
|
951
|
+
elif e.get("acknowledged"):
|
|
952
|
+
state = "acknowledged"
|
|
953
|
+
else:
|
|
954
|
+
state = "OPEN"
|
|
911
955
|
lines.append("## %s [%s / %s / %s]" % (e.get("id",""), e.get("severity",""), e.get("class",""), state))
|
|
912
956
|
lines.append("")
|
|
913
957
|
lines.append("- Gap: %s" % e.get("gap",""))
|
|
@@ -915,6 +959,8 @@ else:
|
|
|
915
959
|
lines.append("- Why: %s" % e.get("why",""))
|
|
916
960
|
lines.append("- Affects: %s" % e.get("affects",""))
|
|
917
961
|
lines.append("- Source: %s" % e.get("source",""))
|
|
962
|
+
if e.get("resolution_note"):
|
|
963
|
+
lines.append("- Resolution: %s" % e.get("resolution_note"))
|
|
918
964
|
lines.append("")
|
|
919
965
|
out = os.path.join(d, "ledger.md")
|
|
920
966
|
fd, tmp = tempfile.mkstemp(dir=d, suffix=".tmp")
|
package/autonomy/telemetry.sh
CHANGED
|
@@ -1,24 +1,97 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Anonymous usage telemetry for Loki Mode
|
|
3
|
-
# Collection is
|
|
4
|
-
# in
|
|
5
|
-
#
|
|
3
|
+
# Collection is ON BY DEFAULT for individual interactive installs, but AUTO-OFF
|
|
4
|
+
# in enterprise / CI / air-gapped contexts (see _loki_telemetry_auto_off), so
|
|
5
|
+
# enterprise + air-gapped deployments stay silent out of the box (GDPR / FedRAMP
|
|
6
|
+
# safe). Only anonymous diagnostics are ever sent (os, arch, version, error type,
|
|
7
|
+
# sanitized stack signatures); never code, prompts, paths, keys, or repo names.
|
|
6
8
|
# Opt-out (always wins): LOKI_TELEMETRY=off / LOKI_TELEMETRY_DISABLED=true /
|
|
7
9
|
# DO_NOT_TRACK=1 / ~/.loki/config: TELEMETRY_DISABLED=true
|
|
8
|
-
#
|
|
10
|
+
# Force-on: LOKI_TELEMETRY=on OR ~/.loki/config: TELEMETRY_ENABLED=true
|
|
11
|
+
# All calls are fire-and-forget, silent on failure, non-blocking.
|
|
12
|
+
# Disclosed in docs/PRIVACY.md + a one-time disclosure on first use (never covert).
|
|
13
|
+
|
|
14
|
+
# _loki_telemetry_auto_off: returns 0 (true, auto-disable) when the environment is
|
|
15
|
+
# enterprise / CI / non-interactive / air-gapped, where on-by-default would be
|
|
16
|
+
# inappropriate. Keeps "enterprise + air-gapped safe out of the box" literally
|
|
17
|
+
# true while still defaulting ON for ordinary individual users. Shared by every
|
|
18
|
+
# gate (crash.sh, dashboard/telemetry.py mirror this list).
|
|
19
|
+
# - CI/automation: CI / GITHUB_ACTIONS / GITLAB_CI / BUILDKITE / JENKINS_URL /
|
|
20
|
+
# TEAMCITY_VERSION / CONTINUOUS_INTEGRATION
|
|
21
|
+
# - Enterprise opt-out: LOKI_ENTERPRISE=true / LOKI_AIRGAP=true
|
|
22
|
+
# - Non-interactive: no TTY on stdout AND stdin (scripts/pipes/detached)
|
|
23
|
+
_loki_telemetry_auto_off() {
|
|
24
|
+
[ "${CI:-}" = "true" ] && return 0
|
|
25
|
+
[ -n "${GITHUB_ACTIONS:-}" ] && return 0
|
|
26
|
+
[ -n "${GITLAB_CI:-}" ] && return 0
|
|
27
|
+
[ -n "${BUILDKITE:-}" ] && return 0
|
|
28
|
+
[ -n "${JENKINS_URL:-}" ] && return 0
|
|
29
|
+
[ -n "${TEAMCITY_VERSION:-}" ] && return 0
|
|
30
|
+
[ "${CONTINUOUS_INTEGRATION:-}" = "true" ] && return 0
|
|
31
|
+
[ "${LOKI_ENTERPRISE:-}" = "true" ] && return 0
|
|
32
|
+
[ "${LOKI_AIRGAP:-}" = "true" ] && return 0
|
|
33
|
+
# Non-interactive detection (council cH_r1 AC2). Interactivity is resolved
|
|
34
|
+
# EXACTLY ONCE at the real entry point (bin/loki shim top / autonomy/loki
|
|
35
|
+
# main), while the user's TTY is present, and exported as LOKI_TTY_INTERACTIVE.
|
|
36
|
+
# We must trust that explicit signal here instead of a fresh `-t` probe,
|
|
37
|
+
# because this gate runs inside FD-detached subshells (the bin/loki gate-check
|
|
38
|
+
# and the backgrounded emit) where a `-t` probe always sees non-TTY and would
|
|
39
|
+
# wrongly auto-off a real interactive user.
|
|
40
|
+
# Precedence:
|
|
41
|
+
# - LOKI_TTY_INTERACTIVE=1 -> interactive (do NOT auto-off here)
|
|
42
|
+
# - LOKI_TTY_INTERACTIVE=0/other set -> non-interactive (auto-off)
|
|
43
|
+
# - UNSET (gate ran without passing an entry point, e.g. an isolated unit
|
|
44
|
+
# test) -> fall back to the live `-t` probe so isolated gate tests behave.
|
|
45
|
+
if [ -n "${LOKI_TTY_INTERACTIVE:-}" ]; then
|
|
46
|
+
[ "${LOKI_TTY_INTERACTIVE}" = "1" ] && return 1
|
|
47
|
+
return 0
|
|
48
|
+
fi
|
|
49
|
+
if [ ! -t 1 ] && [ ! -t 0 ]; then
|
|
50
|
+
return 0
|
|
51
|
+
fi
|
|
52
|
+
return 1
|
|
53
|
+
}
|
|
9
54
|
|
|
10
55
|
LOKI_POSTHOG_HOST="${LOKI_TELEMETRY_ENDPOINT:-https://us.i.posthog.com}"
|
|
11
56
|
LOKI_POSTHOG_KEY="phc_ya0vGBru41AJWtGNfZZ8H9W4yjoZy4KON0nnayS7s87"
|
|
12
57
|
|
|
58
|
+
# _loki_disclose_telemetry_once: one-time, route-shared disclosure (council
|
|
59
|
+
# cH_r1 AC4). Prints a single anonymous-diagnostics disclosure to the user's
|
|
60
|
+
# REAL stderr the first time collection is ACTUALLY enabled at egress time, then
|
|
61
|
+
# records its OWN marker so it never repeats. This is the SINGLE shared impl used
|
|
62
|
+
# by BOTH the Bun route (bin/loki, which sources this file at top level just for
|
|
63
|
+
# this helper) and the bash route (autonomy/loki main, before its foreground
|
|
64
|
+
# cli_command egress) so no first command is ever covert and the copy never
|
|
65
|
+
# drifts between routes. Keyed on ~/.loki/.telemetry-disclosed, NOT the
|
|
66
|
+
# .loki-first-run sentinel: a first run in CI/auto-off sets .loki-first-run while
|
|
67
|
+
# suppressing disclosure, which must NOT permanently silence the disclosure on a
|
|
68
|
+
# later interactive (enabled) run (the sentinel edge, AC5). Guarded so a single
|
|
69
|
+
# definition wins if both this file and another loader define it.
|
|
70
|
+
if ! declare -f _loki_disclose_telemetry_once >/dev/null 2>&1; then
|
|
71
|
+
_loki_disclose_telemetry_once() {
|
|
72
|
+
local _marker="${HOME}/.loki/.telemetry-disclosed"
|
|
73
|
+
[ -f "$_marker" ] 2>/dev/null && return 0
|
|
74
|
+
printf '%s\n' \
|
|
75
|
+
"Loki Mode sends anonymous diagnostics (os, arch, version, error type only --" \
|
|
76
|
+
"never your code, prompts, paths, or keys) to help fix bugs. Off in enterprise/" \
|
|
77
|
+
"CI/air-gapped setups. Turn it off anytime: loki telemetry off (docs/PRIVACY.md)" >&2
|
|
78
|
+
mkdir -p "${HOME}/.loki" 2>/dev/null || return 0
|
|
79
|
+
: > "$_marker" 2>/dev/null || true
|
|
80
|
+
return 0
|
|
81
|
+
}
|
|
82
|
+
fi
|
|
83
|
+
|
|
13
84
|
_loki_telemetry_enabled() {
|
|
14
|
-
# Unified
|
|
15
|
-
#
|
|
85
|
+
# Unified gate. Default ON for individual interactive installs; auto-OFF in
|
|
86
|
+
# enterprise/CI/air-gapped contexts; explicit opt-out always wins. Precedence
|
|
16
87
|
# MUST mirror loki_collection_enabled in autonomy/crash.sh and _is_enabled in
|
|
17
88
|
# dashboard/telemetry.py so one model gates BOTH usage telemetry and crash
|
|
18
89
|
# reporting.
|
|
19
|
-
# 1. Any opt-out flag present
|
|
20
|
-
# 2. Else
|
|
21
|
-
# 3. Else
|
|
90
|
+
# 1. Any opt-out flag present -> 1 (hard kill, always wins)
|
|
91
|
+
# 2. Else explicit opt-in present -> 0 (force-on, even in CI/enterprise)
|
|
92
|
+
# 3. Else enterprise/CI/air-gapped -> 1 (auto-off, safe out of the box)
|
|
93
|
+
# 4. Else (individual default) -> 0 (on, anonymous diagnostics)
|
|
94
|
+
# All enabled paths still require curl (no egress tool -> off).
|
|
22
95
|
local _telem_lower
|
|
23
96
|
_telem_lower="$(printf '%s' "${LOKI_TELEMETRY:-}" | tr '[:upper:]' '[:lower:]')"
|
|
24
97
|
|
|
@@ -30,7 +103,7 @@ _loki_telemetry_enabled() {
|
|
|
30
103
|
return 1
|
|
31
104
|
fi
|
|
32
105
|
|
|
33
|
-
# --- 2.
|
|
106
|
+
# --- 2. Explicit opt-in forces ON (overrides the enterprise/CI auto-off) ---
|
|
34
107
|
if [ "$_telem_lower" = "on" ]; then
|
|
35
108
|
command -v curl >/dev/null 2>&1 || return 1
|
|
36
109
|
return 0
|
|
@@ -40,8 +113,12 @@ _loki_telemetry_enabled() {
|
|
|
40
113
|
return 0
|
|
41
114
|
fi
|
|
42
115
|
|
|
43
|
-
# --- 3.
|
|
44
|
-
return 1
|
|
116
|
+
# --- 3. Enterprise / CI / air-gapped: auto-off (safe out of the box) ---
|
|
117
|
+
_loki_telemetry_auto_off && return 1
|
|
118
|
+
|
|
119
|
+
# --- 4. Individual interactive default: ON (anonymous diagnostics) ---
|
|
120
|
+
command -v curl >/dev/null 2>&1 || return 1
|
|
121
|
+
return 0
|
|
45
122
|
}
|
|
46
123
|
|
|
47
124
|
_loki_telemetry_id() {
|
package/bin/loki
CHANGED
|
@@ -32,6 +32,21 @@ SCRIPT_DIR=$(dirname "$SCRIPT_PATH")
|
|
|
32
32
|
REPO_ROOT=$(cd "$SCRIPT_DIR/.." 2>/dev/null && pwd)
|
|
33
33
|
BASH_CLI="$REPO_ROOT/autonomy/loki"
|
|
34
34
|
|
|
35
|
+
# v7.89.0 telemetry TTY fix (council cH_r1): resolve interactivity EXACTLY ONCE
|
|
36
|
+
# here, while this shim still owns the user's real TTY, and export it as an
|
|
37
|
+
# explicit signal. The telemetry gates (telemetry.sh / crash.sh / dashboard
|
|
38
|
+
# telemetry.py) must NOT re-probe `-t` later: the gate is evaluated inside
|
|
39
|
+
# subshells that redirect FDs (the `) </dev/null >/dev/null 2>&1` gate-check
|
|
40
|
+
# subshells below), and the backgrounded emit subshell also detaches its FDs, so
|
|
41
|
+
# a fresh `-t` probe there always sees non-TTY and would auto-off a REAL
|
|
42
|
+
# interactive user (killing on-by-default + the disclosure). Set the var only
|
|
43
|
+
# when a terminal is actually present at this entry point; leave it UNSET
|
|
44
|
+
# otherwise so an isolated unit test that bypasses this entry point falls back to
|
|
45
|
+
# the gate's own `-t` probe (documented precedence in the *_auto_off helpers).
|
|
46
|
+
if [ -t 1 ] || [ -t 0 ]; then
|
|
47
|
+
export LOKI_TTY_INTERACTIVE=1
|
|
48
|
+
fi
|
|
49
|
+
|
|
35
50
|
# Resolve which Bun entry to use:
|
|
36
51
|
# 1. LOKI_TS_ENTRY=... -- explicit override (custom builds, tests)
|
|
37
52
|
# 2. BUN_FROM_SOURCE=1 -- prefer src/cli.ts (used by bench --compare-dist
|
|
@@ -87,6 +102,42 @@ else
|
|
|
87
102
|
exec "$BASH_CLI" "$@"
|
|
88
103
|
fi
|
|
89
104
|
|
|
105
|
+
# v7.88.1 covert-path fix (council cF_r2 + cG_r2 sentinel edge): under on-by-default
|
|
106
|
+
# diagnostics, NO telemetry may egress before the user has seen a disclosure once.
|
|
107
|
+
# The cmd_start welcome only shows on `loki start`, so a first `loki version`/`doctor`
|
|
108
|
+
# would otherwise egress covertly. _loki_disclose_telemetry_once prints a one-time
|
|
109
|
+
# disclosure to the user's REAL stderr the first time collection is ACTUALLY enabled
|
|
110
|
+
# at egress time. It is keyed on its OWN marker (~/.loki/.telemetry-disclosed), NOT
|
|
111
|
+
# the .loki-first-run sentinel: the sentinel edge is that a first run in CI/auto-off
|
|
112
|
+
# sets .loki-first-run while suppressing disclosure, which must NOT permanently
|
|
113
|
+
# silence the disclosure on a later interactive (enabled) run. Every enabled egress
|
|
114
|
+
# path (installed + cli_command) calls this first, so whichever fires first discloses.
|
|
115
|
+
#
|
|
116
|
+
# v7.89.0 (council cH_r1 AC4): the helper now lives in autonomy/telemetry.sh as
|
|
117
|
+
# the SINGLE shared impl for both routes (the bash route in autonomy/loki calls
|
|
118
|
+
# the same function before its foreground egress, so the copy never drifts).
|
|
119
|
+
# Source it here at top level just for this helper -- telemetry.sh executes
|
|
120
|
+
# nothing on source beyond defining functions and two host/key vars, so this is
|
|
121
|
+
# side-effect-free for the shim. If the source fails for any reason, define a
|
|
122
|
+
# local fallback so on-by-default disclosure is never silently skipped.
|
|
123
|
+
if [ -f "$REPO_ROOT/autonomy/telemetry.sh" ]; then
|
|
124
|
+
# shellcheck source=../autonomy/telemetry.sh
|
|
125
|
+
source "$REPO_ROOT/autonomy/telemetry.sh" 2>/dev/null || true
|
|
126
|
+
fi
|
|
127
|
+
if ! declare -f _loki_disclose_telemetry_once >/dev/null 2>&1; then
|
|
128
|
+
_loki_disclose_telemetry_once() {
|
|
129
|
+
local _marker="${HOME}/.loki/.telemetry-disclosed"
|
|
130
|
+
[ -f "$_marker" ] 2>/dev/null && return 0
|
|
131
|
+
printf '%s\n' \
|
|
132
|
+
"Loki Mode sends anonymous diagnostics (os, arch, version, error type only --" \
|
|
133
|
+
"never your code, prompts, paths, or keys) to help fix bugs. Off in enterprise/" \
|
|
134
|
+
"CI/air-gapped setups. Turn it off anytime: loki telemetry off (docs/PRIVACY.md)" >&2
|
|
135
|
+
mkdir -p "${HOME}/.loki" 2>/dev/null || return 0
|
|
136
|
+
: > "$_marker" 2>/dev/null || true
|
|
137
|
+
return 0
|
|
138
|
+
}
|
|
139
|
+
fi
|
|
140
|
+
|
|
90
141
|
# v7.4.13: one-shot first-run telemetry, MOVED from autonomy/loki main()
|
|
91
142
|
# into the shim because the 8 ported commands (version, status, doctor,
|
|
92
143
|
# stats, provider, memory) bypass main() entirely -- only unported
|
|
@@ -96,13 +147,26 @@ fi
|
|
|
96
147
|
if [ -z "${LOKI_TELEMETRY_DISABLED:-}" ] && [ "${DO_NOT_TRACK:-}" != "1" ] && [ ! -f "${HOME}/.loki-first-run" ] 2>/dev/null; then
|
|
97
148
|
touch "${HOME}/.loki-first-run" 2>/dev/null || true
|
|
98
149
|
if command -v curl &>/dev/null && [ -f "$REPO_ROOT/autonomy/telemetry.sh" ]; then
|
|
99
|
-
#
|
|
100
|
-
# so
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
#
|
|
104
|
-
|
|
105
|
-
|
|
150
|
+
# Decide with the SAME gate the egress uses (_loki_telemetry_enabled), in a
|
|
151
|
+
# subshell so sourcing telemetry.sh cannot pollute the shim env.
|
|
152
|
+
# AC3: evaluate the GATE in the foreground (do NOT detach FDs here) so it
|
|
153
|
+
# inherits the real TTY / sees the exported LOKI_TTY_INTERACTIVE signal;
|
|
154
|
+
# only the network EMIT below is backgrounded with detached FDs. The
|
|
155
|
+
# sourced helper is silenced via 2>/dev/null and the gate writes nothing
|
|
156
|
+
# to stdout, so foreground evaluation adds no output.
|
|
157
|
+
if ( SCRIPT_DIR="$REPO_ROOT/autonomy"; source "$SCRIPT_DIR/telemetry.sh" 2>/dev/null \
|
|
158
|
+
&& declare -f _loki_telemetry_enabled >/dev/null 2>&1 \
|
|
159
|
+
&& _loki_telemetry_enabled ); then
|
|
160
|
+
# Disclosure BEFORE the installed egress -- see the shared
|
|
161
|
+
# _loki_disclose_telemetry_once helper below (keyed on its OWN
|
|
162
|
+
# disclosed-marker, NOT .loki-first-run, so a first run in CI that
|
|
163
|
+
# suppressed disclosure does not permanently silence it).
|
|
164
|
+
_loki_disclose_telemetry_once
|
|
165
|
+
# Fire-and-forget the installed event; detach all FDs (v7.8.3 broken-pipe fix).
|
|
166
|
+
( SCRIPT_DIR="$REPO_ROOT/autonomy"; source "$SCRIPT_DIR/telemetry.sh" 2>/dev/null \
|
|
167
|
+
&& loki_telemetry "installed" "first_command=${1:-}" 2>/dev/null ) >/dev/null 2>&1 </dev/null &
|
|
168
|
+
disown 2>/dev/null || true
|
|
169
|
+
fi
|
|
106
170
|
fi
|
|
107
171
|
fi
|
|
108
172
|
|
|
@@ -176,6 +240,15 @@ if [ "${1:-}" = "report" ]; then
|
|
|
176
240
|
done
|
|
177
241
|
if [ "$_report_first_sub" = "kpis" ]; then
|
|
178
242
|
if command -v curl &>/dev/null && [ -f "$REPO_ROOT/autonomy/telemetry.sh" ]; then
|
|
243
|
+
# Disclose once before egress if collection is genuinely enabled
|
|
244
|
+
# (covers the case where a cli_command is the first ENABLED egress,
|
|
245
|
+
# e.g. first run was CI/auto-off so .loki-first-run is already set).
|
|
246
|
+
# AC3: foreground gate evaluation (no FD detach) so the exported
|
|
247
|
+
# LOKI_TTY_INTERACTIVE signal / real TTY drives the verdict.
|
|
248
|
+
if ( SCRIPT_DIR="$REPO_ROOT/autonomy"; source "$SCRIPT_DIR/telemetry.sh" 2>/dev/null \
|
|
249
|
+
&& declare -f _loki_telemetry_enabled >/dev/null 2>&1 && _loki_telemetry_enabled ); then
|
|
250
|
+
_loki_disclose_telemetry_once
|
|
251
|
+
fi
|
|
179
252
|
( SCRIPT_DIR="$REPO_ROOT/autonomy"; source "$SCRIPT_DIR/telemetry.sh" 2>/dev/null && loki_telemetry "cli_command" "command=${1:-}" 2>/dev/null ) >/dev/null 2>&1 </dev/null &
|
|
180
253
|
disown 2>/dev/null || true
|
|
181
254
|
fi
|
|
@@ -187,7 +260,7 @@ fi
|
|
|
187
260
|
# Two-token routes (provider show/list, memory list/index) match on the first
|
|
188
261
|
# token only; the Bun dispatcher handles subcommand routing internally.
|
|
189
262
|
case "${1:-}" in
|
|
190
|
-
version|--version|-v|status|stats|doctor|provider|memory|rollback|internal|kpis|trust|proof|wiki|crash)
|
|
263
|
+
version|--version|-v|status|stats|doctor|provider|memory|rollback|internal|kpis|trust|proof|receipt|wiki|crash)
|
|
191
264
|
# v7.5.2: rollback added (wires loki-ts/src/commands/rollback.ts).
|
|
192
265
|
# v7.5.3: internal added for autonomy/run.sh phase1-hooks calls.
|
|
193
266
|
# v7.5.28: kpis added (Phase K MVP: read-only KPI snapshot).
|
|
@@ -210,6 +283,14 @@ case "${1:-}" in
|
|
|
210
283
|
# stdout/stderr/stdin open. Without this, the lingering FD changed
|
|
211
284
|
# pipe-teardown timing and produced a macOS-only broken-pipe in the
|
|
212
285
|
# shim-route CLI test harness (v7.8.2 -> fixed v7.8.3).
|
|
286
|
+
# Disclose once before egress if collection is genuinely enabled
|
|
287
|
+
# (first ENABLED egress may be a cli_command, not installed).
|
|
288
|
+
# AC3: foreground gate evaluation (no FD detach) so the exported
|
|
289
|
+
# LOKI_TTY_INTERACTIVE signal / real TTY drives the verdict.
|
|
290
|
+
if ( SCRIPT_DIR="$REPO_ROOT/autonomy"; source "$SCRIPT_DIR/telemetry.sh" 2>/dev/null \
|
|
291
|
+
&& declare -f _loki_telemetry_enabled >/dev/null 2>&1 && _loki_telemetry_enabled ); then
|
|
292
|
+
_loki_disclose_telemetry_once
|
|
293
|
+
fi
|
|
213
294
|
( SCRIPT_DIR="$REPO_ROOT/autonomy"; source "$SCRIPT_DIR/telemetry.sh" 2>/dev/null && loki_telemetry "cli_command" "command=${1:-}" 2>/dev/null ) >/dev/null 2>&1 </dev/null &
|
|
214
295
|
disown 2>/dev/null || true
|
|
215
296
|
fi
|
package/dashboard/__init__.py
CHANGED
package/dashboard/server.py
CHANGED
|
@@ -10190,6 +10190,219 @@ async def get_proof_html(run_id: str):
|
|
|
10190
10190
|
return FileResponse(str(index_html), media_type="text/html")
|
|
10191
10191
|
|
|
10192
10192
|
|
|
10193
|
+
# ---------------------------------------------------------------------------
|
|
10194
|
+
# Active spec + spec history.
|
|
10195
|
+
#
|
|
10196
|
+
# Gives the dashboard honest visibility into WHAT Loki is building from. The
|
|
10197
|
+
# resolution order mirrors proof-generator.py::_collect_spec so the panel and
|
|
10198
|
+
# the Evidence Receipt can never disagree about the spec source:
|
|
10199
|
+
# 1. PRD file -> session.json prdPath (only when the file still exists)
|
|
10200
|
+
# 2. one-line brief -> .loki/state/brief.txt
|
|
10201
|
+
# 3. generated PRD -> .loki/generated-prd.md
|
|
10202
|
+
# 4. no spec -> codebase-analysis
|
|
10203
|
+
# Issue mode is detected from the latest proof.json's spec.source (a GitHub
|
|
10204
|
+
# issue URL): issue runs re-dispatch through cmd_run, which synthesizes a PRD,
|
|
10205
|
+
# so at runtime they look like a generated-prd; the proof carries the real
|
|
10206
|
+
# issue ref. We never fabricate a type -- when nothing is resolvable we say so.
|
|
10207
|
+
#
|
|
10208
|
+
# History is derived from the proofs the Evidence Receipt already writes
|
|
10209
|
+
# (.loki/proofs/<run_id>/proof.json), newest-first. No new store is invented.
|
|
10210
|
+
# ---------------------------------------------------------------------------
|
|
10211
|
+
# Cap on the spec body returned to the dashboard so a huge PRD cannot bloat the
|
|
10212
|
+
# payload. The panel shows a preview; the full file lives on disk.
|
|
10213
|
+
_SPEC_CONTENT_CAP = 20000
|
|
10214
|
+
# Issue URLs (mirrors autonomy/loki's issue-mode detection regex). Matches a
|
|
10215
|
+
# tracker host followed somewhere by an /issue(s)/ path or a Jira /browse/ key.
|
|
10216
|
+
_ISSUE_URL_RE = re.compile(
|
|
10217
|
+
r"(github\.com|gitlab\.com|atlassian\.net|dev\.azure\.com|"
|
|
10218
|
+
r"visualstudio\.com)/.*\b(issues?|browse)/", re.IGNORECASE)
|
|
10219
|
+
|
|
10220
|
+
|
|
10221
|
+
def _spec_source_is_issue(source: str) -> bool:
|
|
10222
|
+
"""True when a proof spec.source string is a tracker issue reference."""
|
|
10223
|
+
if not source:
|
|
10224
|
+
return False
|
|
10225
|
+
return bool(_ISSUE_URL_RE.search(source))
|
|
10226
|
+
|
|
10227
|
+
|
|
10228
|
+
def _latest_proof(proofs_dir: _Path) -> Optional[dict]:
|
|
10229
|
+
"""Return the newest proof.json dict for the active project, or None."""
|
|
10230
|
+
try:
|
|
10231
|
+
entries = sorted(proofs_dir.iterdir())
|
|
10232
|
+
except (OSError, FileNotFoundError):
|
|
10233
|
+
return None
|
|
10234
|
+
newest = None
|
|
10235
|
+
newest_key = ""
|
|
10236
|
+
for entry in entries:
|
|
10237
|
+
if not entry.is_dir():
|
|
10238
|
+
continue
|
|
10239
|
+
proof_json = entry / "proof.json"
|
|
10240
|
+
if not proof_json.is_file():
|
|
10241
|
+
continue
|
|
10242
|
+
data = _safe_json_read(proof_json, default=None)
|
|
10243
|
+
if not isinstance(data, dict):
|
|
10244
|
+
continue
|
|
10245
|
+
key = data.get("generated_at") or entry.name
|
|
10246
|
+
if newest is None or key >= newest_key:
|
|
10247
|
+
newest = data
|
|
10248
|
+
newest_key = key
|
|
10249
|
+
return newest
|
|
10250
|
+
|
|
10251
|
+
|
|
10252
|
+
def _spec_summary(source: str, brief: str) -> str:
|
|
10253
|
+
"""One-line summary for a history row, derived honestly from the spec.
|
|
10254
|
+
|
|
10255
|
+
Prefers the first non-empty line of the brief; falls back to the source
|
|
10256
|
+
basename. Never fabricates -- returns an honest label when nothing exists.
|
|
10257
|
+
"""
|
|
10258
|
+
if brief:
|
|
10259
|
+
for line in brief.splitlines():
|
|
10260
|
+
line = line.strip().lstrip("#").strip()
|
|
10261
|
+
if line:
|
|
10262
|
+
return line[:160]
|
|
10263
|
+
if source in ("brief", "codebase-analysis", "", None):
|
|
10264
|
+
return {"brief": "one-line brief",
|
|
10265
|
+
"codebase-analysis": "Codebase analysis (no spec)"}.get(
|
|
10266
|
+
source, "Unknown spec")
|
|
10267
|
+
if _spec_source_is_issue(source):
|
|
10268
|
+
return source
|
|
10269
|
+
return os.path.basename(source) or source
|
|
10270
|
+
|
|
10271
|
+
|
|
10272
|
+
def _classify_proof_spec(spec: dict) -> str:
|
|
10273
|
+
"""Map a proof.json spec dict to a dashboard spec type, honestly."""
|
|
10274
|
+
if not isinstance(spec, dict):
|
|
10275
|
+
return "unknown"
|
|
10276
|
+
source = (spec.get("source") or "").strip()
|
|
10277
|
+
if _spec_source_is_issue(source):
|
|
10278
|
+
return "issue"
|
|
10279
|
+
if source == "brief":
|
|
10280
|
+
return "brief"
|
|
10281
|
+
if source == "codebase-analysis":
|
|
10282
|
+
return "codebase-analysis"
|
|
10283
|
+
if not source:
|
|
10284
|
+
return "unknown"
|
|
10285
|
+
# A real filesystem path (PRD or generated PRD). A synthesized PRD lives at
|
|
10286
|
+
# .loki/generated-prd.md or .loki/brief-prd-*.md; anything else is a user
|
|
10287
|
+
# PRD. We report the broad "spec" type and the path so the UI can show it.
|
|
10288
|
+
return "spec"
|
|
10289
|
+
|
|
10290
|
+
|
|
10291
|
+
@app.get("/api/spec", dependencies=[Depends(auth.require_scope("read"))])
|
|
10292
|
+
async def get_active_spec():
|
|
10293
|
+
"""Return the current run's spec source + content, honestly typed.
|
|
10294
|
+
|
|
10295
|
+
Types: prd | brief | spec | issue | codebase-analysis | none. The `none`
|
|
10296
|
+
state is the honest empty state when no run has produced a spec yet. We
|
|
10297
|
+
never invent a spec: each branch reads a real file or says it cannot.
|
|
10298
|
+
"""
|
|
10299
|
+
loki_dir = _get_loki_dir()
|
|
10300
|
+
|
|
10301
|
+
# 1. PRD file recorded by the running orchestrator (session.json prdPath).
|
|
10302
|
+
# Only trust it when the file still exists on disk.
|
|
10303
|
+
session = _safe_json_read(loki_dir / "session.json", default=None)
|
|
10304
|
+
prd_path = ""
|
|
10305
|
+
if isinstance(session, dict):
|
|
10306
|
+
prd_path = (session.get("prdPath") or "").strip()
|
|
10307
|
+
if prd_path:
|
|
10308
|
+
p = _Path(prd_path)
|
|
10309
|
+
if p.is_file():
|
|
10310
|
+
content = _safe_read_text(p)
|
|
10311
|
+
return {
|
|
10312
|
+
"type": "prd",
|
|
10313
|
+
"path": str(p),
|
|
10314
|
+
"content": content[:_SPEC_CONTENT_CAP],
|
|
10315
|
+
"truncated": len(content) > _SPEC_CONTENT_CAP,
|
|
10316
|
+
}
|
|
10317
|
+
|
|
10318
|
+
# 2. one-line brief (zero-config first run). The raw brief is the strongest
|
|
10319
|
+
# honest artifact -- show it verbatim.
|
|
10320
|
+
brief_file = loki_dir / "state" / "brief.txt"
|
|
10321
|
+
if brief_file.is_file():
|
|
10322
|
+
text = _safe_read_text(brief_file).strip()
|
|
10323
|
+
if text:
|
|
10324
|
+
return {"type": "brief", "text": text[:_SPEC_CONTENT_CAP],
|
|
10325
|
+
"truncated": len(text) > _SPEC_CONTENT_CAP}
|
|
10326
|
+
|
|
10327
|
+
# 3. Issue mode: a brief/PRD run that originated from a tracker issue. The
|
|
10328
|
+
# runtime has only the synthesized PRD, but the latest proof.json carries
|
|
10329
|
+
# the real issue ref + brief. Surface it as an issue when we can prove it.
|
|
10330
|
+
latest = _latest_proof(loki_dir / "proofs")
|
|
10331
|
+
if isinstance(latest, dict):
|
|
10332
|
+
pspec = latest.get("spec")
|
|
10333
|
+
if isinstance(pspec, dict):
|
|
10334
|
+
src = (pspec.get("source") or "").strip()
|
|
10335
|
+
if _spec_source_is_issue(src):
|
|
10336
|
+
pbrief = (pspec.get("brief") or "").strip()
|
|
10337
|
+
return {
|
|
10338
|
+
"type": "issue",
|
|
10339
|
+
"ref": src,
|
|
10340
|
+
"title": _spec_summary(src, pbrief),
|
|
10341
|
+
"body": pbrief[:_SPEC_CONTENT_CAP],
|
|
10342
|
+
"truncated": len(pbrief) > _SPEC_CONTENT_CAP,
|
|
10343
|
+
}
|
|
10344
|
+
|
|
10345
|
+
# 4. Generated PRD (synthesized from a brief, or from codebase analysis).
|
|
10346
|
+
for name in ("generated-prd.md",):
|
|
10347
|
+
gen = loki_dir / name
|
|
10348
|
+
if gen.is_file():
|
|
10349
|
+
content = _safe_read_text(gen)
|
|
10350
|
+
if content.strip():
|
|
10351
|
+
return {
|
|
10352
|
+
"type": "spec",
|
|
10353
|
+
"path": str(gen),
|
|
10354
|
+
"content": content[:_SPEC_CONTENT_CAP],
|
|
10355
|
+
"truncated": len(content) > _SPEC_CONTENT_CAP,
|
|
10356
|
+
"generated": True,
|
|
10357
|
+
}
|
|
10358
|
+
|
|
10359
|
+
# 5. Codebase analysis (no explicit spec) -- only claim this when a run has
|
|
10360
|
+
# actually happened (a session or a proof exists). Otherwise honest none.
|
|
10361
|
+
has_session = isinstance(session, dict) and bool(session)
|
|
10362
|
+
if has_session or latest is not None:
|
|
10363
|
+
return {"type": "codebase-analysis"}
|
|
10364
|
+
|
|
10365
|
+
# 6. Nothing yet -- honest empty state, not a fabricated spec.
|
|
10366
|
+
return {"type": "none"}
|
|
10367
|
+
|
|
10368
|
+
|
|
10369
|
+
@app.get("/api/spec/history", dependencies=[Depends(auth.require_scope("read"))])
|
|
10370
|
+
async def get_spec_history():
|
|
10371
|
+
"""Return past specs/issues/briefs for this codebase, newest-first.
|
|
10372
|
+
|
|
10373
|
+
Derived from the Evidence Receipts (.loki/proofs/<run_id>/proof.json) that
|
|
10374
|
+
every run already writes; no separate spec store is invented. Each row:
|
|
10375
|
+
{when, type, summary, run_id}. Missing/corrupt proofs are skipped, never
|
|
10376
|
+
faked. Empty -> {"history": []} (honest empty state).
|
|
10377
|
+
"""
|
|
10378
|
+
proofs_dir = _get_loki_dir() / "proofs"
|
|
10379
|
+
rows: list[dict] = []
|
|
10380
|
+
try:
|
|
10381
|
+
entries = sorted(proofs_dir.iterdir())
|
|
10382
|
+
except (OSError, FileNotFoundError):
|
|
10383
|
+
return {"history": []}
|
|
10384
|
+
for entry in entries:
|
|
10385
|
+
if not entry.is_dir():
|
|
10386
|
+
continue
|
|
10387
|
+
proof_json = entry / "proof.json"
|
|
10388
|
+
if not proof_json.is_file():
|
|
10389
|
+
continue
|
|
10390
|
+
data = _safe_json_read(proof_json, default=None)
|
|
10391
|
+
if not isinstance(data, dict):
|
|
10392
|
+
continue
|
|
10393
|
+
spec = data.get("spec")
|
|
10394
|
+
spec = spec if isinstance(spec, dict) else {}
|
|
10395
|
+
source = (spec.get("source") or "").strip()
|
|
10396
|
+
rows.append({
|
|
10397
|
+
"run_id": data.get("run_id", entry.name),
|
|
10398
|
+
"when": data.get("generated_at"),
|
|
10399
|
+
"type": _classify_proof_spec(spec),
|
|
10400
|
+
"summary": _spec_summary(source, (spec.get("brief") or "").strip()),
|
|
10401
|
+
})
|
|
10402
|
+
rows.sort(key=lambda x: (x.get("when") or ""), reverse=True)
|
|
10403
|
+
return {"history": rows}
|
|
10404
|
+
|
|
10405
|
+
|
|
10193
10406
|
# ---------------------------------------------------------------------------
|
|
10194
10407
|
# R5: Auto-wiki + cited codebase Q&A (Loki's DeepWiki).
|
|
10195
10408
|
#
|