loki-mode 7.46.0 → 7.48.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/README.md +1 -1
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +113 -0
- package/autonomy/crash.sh +47 -21
- package/autonomy/loki +50 -27
- package/autonomy/run.sh +468 -5
- package/autonomy/spec-interrogation.sh +550 -0
- package/autonomy/telemetry.sh +28 -8
- package/bin/postinstall.js +22 -10
- package/dashboard/__init__.py +1 -1
- package/dashboard/auth.py +117 -2
- package/dashboard/telemetry.py +34 -6
- package/docs/ACKNOWLEDGEMENTS.md +1 -1
- package/docs/COMPETITIVE-ANALYSIS.md +1 -1
- package/docs/INSTALLATION.md +10 -3
- package/docs/OPEN-CORE-BOUNDARY.md +6 -5
- package/docs/P2-SPEC-ROBUSTNESS-PLAN.md +192 -0
- package/docs/PRIVACY.md +82 -24
- package/docs/R9-OPEN-CORE-HOOKS-PLAN.md +2 -2
- package/docs/auto-claude-comparison.md +2 -2
- package/docs/certification/README.md +1 -1
- package/docs/competitive/bolt-new-analysis.md +1 -1
- package/docs/competitive/emergence-others-analysis.md +6 -6
- package/docs/competitive/replit-lovable-analysis.md +4 -4
- package/docs/enterprise/security.md +43 -3
- package/docs/show-hn-post.md +1 -1
- package/loki-ts/dist/loki.js +30 -30
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/web-app/dist/assets/{AdminPage-CKUOsWZW.js → AdminPage-CcCJ0Sjt.js} +1 -1
- package/web-app/dist/assets/{Avatar-CL9Id9Hi.js → Avatar-DK8kmayw.js} +1 -1
- package/web-app/dist/assets/{Badge-B12zwlD7.js → Badge-4uAWnemi.js} +1 -1
- package/web-app/dist/assets/{Button-CFLVoduT.js → Button-BBMk33tk.js} +1 -1
- package/web-app/dist/assets/ComparePage-bt9rwvST.js +1 -0
- package/web-app/dist/assets/{GitHubIssuesPanel-CSitxtAX.js → GitHubIssuesPanel-WDbH47UM.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-BIT06FRo.js → GitHubPRsPanel-C2CiYtTx.js} +1 -1
- package/web-app/dist/assets/{HomePage-pU_0fGny.js → HomePage-BQk-MUjn.js} +4 -4
- package/web-app/dist/assets/{LoginPage-DTZtt2Yb.js → LoginPage-DMOZVGGL.js} +1 -1
- package/web-app/dist/assets/{MagicPage-10zfra8o.js → MagicPage-Bzp2Nt1z.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-C-wiKUkv.js → MetricsPage-C39JVdsw.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-BDkcmhYe.js → NotFoundPage-6vT_U9UL.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-CiCavQ8n.js → ProjectPage-BfFcZp-E.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-BLCXQwwC.js → ProjectsPage-CPMBf8Wt.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-PkxtaMyg.js → SettingsPage-BnNN6ETl.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-iECp8Tha.js → ShowcasePage-WDrMf-cx.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-DS6Anno1.js → SystemSettingsPage-DX4jb2e8.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-ls6h6bNL.js → TeamsPage-BCfqcXzu.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-Bk0QzlPt.js → TemplatesPage-CZvmimDj.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-4-1hWCtZ.js → TerminalOutput-BlRqFwWV.js} +1 -1
- package/web-app/dist/assets/{activity-DH3ih2nS.js → activity-CacZsUyr.js} +1 -1
- package/web-app/dist/assets/{bell-Gn17S6uv.js → bell-DK2qtHnk.js} +1 -1
- package/web-app/dist/assets/{bot-Cbycc3VE.js → bot-CkcUtHad.js} +1 -1
- package/web-app/dist/assets/{check-nIAqa-kf.js → check-CbCPjX3M.js} +1 -1
- package/web-app/dist/assets/{chevron-left-D2jcWDll.js → chevron-left-5NUKWw3i.js} +1 -1
- package/web-app/dist/assets/{circle-alert-CpL4Bhvt.js → circle-alert-S7uFoxC2.js} +1 -1
- package/web-app/dist/assets/{clock-IW4Wq86N.js → clock-CaQRrIrs.js} +1 -1
- package/web-app/dist/assets/{cloud-Cn8nNuH2.js → cloud-DBAX6c0r.js} +1 -1
- package/web-app/dist/assets/{code-xml-BiJBteXf.js → code-xml-De5-EXv3.js} +1 -1
- package/web-app/dist/assets/{copy-CnqkyNsi.js → copy-CUkT6k1v.js} +1 -1
- package/web-app/dist/assets/{database-CKSReqa5.js → database-BAWf1Gwt.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-CDzDY64R.js → dollar-sign-Ji8zk86R.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-Box4IwG1.js → file-code-corner-ChtXoBwS.js} +1 -1
- package/web-app/dist/assets/{file-plus-DpGqlXF8.js → file-plus-bFa37P76.js} +1 -1
- package/web-app/dist/assets/{folder-open-B57dAoBv.js → folder-open-DhXpXscO.js} +1 -1
- package/web-app/dist/assets/{git-commit-horizontal-BVbucmO5.js → git-commit-horizontal-DVPeDQ3j.js} +1 -1
- package/web-app/dist/assets/{globe-BkOnKl4x.js → globe-BPZgPeeu.js} +1 -1
- package/web-app/dist/assets/{hammer-DRbIQ4QU.js → hammer-jLCaujYH.js} +1 -1
- package/web-app/dist/assets/{index-CM_b_EhP.js → index-B-0iHBPO.js} +2 -2
- package/web-app/dist/assets/{layers-B78BiFiU.js → layers-B1vsrsFW.js} +1 -1
- package/web-app/dist/assets/{lightbulb-B-Itbm9g.js → lightbulb-C-uLoq9Y.js} +1 -1
- package/web-app/dist/assets/{loader-circle-Oq6NQhW2.js → loader-circle-JTfD-ZuM.js} +1 -1
- package/web-app/dist/assets/{lock-DbJ9zxbw.js → lock-G9rxD4gZ.js} +1 -1
- package/web-app/dist/assets/{mail-CzMRod6m.js → mail-BJ0PTN_V.js} +1 -1
- package/web-app/dist/assets/{package-WZ5osvej.js → package-CXClfLOO.js} +1 -1
- package/web-app/dist/assets/{plus-j08lFR-K.js → plus-EoL5OCB7.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-CIr7E-g2.js → refresh-cw-BjREUnVq.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-gwoXxDeE.js → rotate-ccw-DahWX07H.js} +1 -1
- package/web-app/dist/assets/{save-B8fV_ZpE.js → save-Dek3gCn1.js} +1 -1
- package/web-app/dist/assets/{server-D5dO1paz.js → server-D6V1BAia.js} +1 -1
- package/web-app/dist/assets/{shield-alert-Du08zhdg.js → shield-alert-BtTK5Sxb.js} +1 -1
- package/web-app/dist/assets/{trash-2-DEKSVae5.js → trash-2-BT5o_g0r.js} +1 -1
- package/web-app/dist/assets/{trending-down-DBiXUtxJ.js → trending-down-D4Jk7KF3.js} +1 -1
- package/web-app/dist/assets/{trending-up-BgmK_tHq.js → trending-up-EQFTzhEo.js} +1 -1
- package/web-app/dist/assets/{upload-IaViyeVD.js → upload-JfI5lCSE.js} +1 -1
- package/web-app/dist/assets/{usePolling-PiRLqNu6.js → usePolling-BnhPUuGd.js} +1 -1
- package/web-app/dist/assets/{user-BB5J8wAF.js → user-DSUiUYtj.js} +1 -1
- package/web-app/dist/index.html +1 -1
- package/web-app/dist/assets/ComparePage-Dg0UdZAk.js +0 -1
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# autonomy/spec-interrogation.sh - P2-1 spec interrogation + P2-2 assumption ledger.
|
|
3
|
+
#
|
|
4
|
+
# Net-new spec-robustness capability. Loki stays accurate even when the input
|
|
5
|
+
# spec is WRONG, ambiguous, or incomplete by DETECTING spec defects in the
|
|
6
|
+
# DISCOVERY phase and SURFACING them as first-class RECORDED ASSUMPTIONS, never
|
|
7
|
+
# silently autocorrecting.
|
|
8
|
+
#
|
|
9
|
+
# It reuses two existing building blocks unchanged:
|
|
10
|
+
# - autonomy/grill.sh the Devil's-Advocate spec interrogation (provider
|
|
11
|
+
# subcall) that writes .loki/grill/report.md.
|
|
12
|
+
# - autonomy/prd-analyzer.py deterministic missing-dimension detection that
|
|
13
|
+
# already generates assumption text (_make_assumption).
|
|
14
|
+
#
|
|
15
|
+
# This module:
|
|
16
|
+
# 1. classifies grill's report.md into structured findings
|
|
17
|
+
# (ambiguous / contradictory / underspecified / missing) with a
|
|
18
|
+
# deterministic severity (high / medium) -- NO LLM, reproducible.
|
|
19
|
+
# 2. records each spec gap as a first-class ledger entry under .loki/assumptions/.
|
|
20
|
+
# 3. exposes spec_ledger_high_unresolved_count for the completion gate
|
|
21
|
+
# (council_assumption_ledger_gate in completion-council.sh).
|
|
22
|
+
#
|
|
23
|
+
# Design note (auto-acknowledgment lifecycle):
|
|
24
|
+
# The completion gate blocks iff an entry is severity=high AND confirmed=false
|
|
25
|
+
# AND acknowledged=false. In autonomous (non-TTY) mode no human can ever set
|
|
26
|
+
# confirmed=yes, so the auto-acknowledgment lifecycle (run.sh) marks an
|
|
27
|
+
# assumption acknowledged once it has been injected into the build prompt at
|
|
28
|
+
# least once. That is the OPPOSITE of silent autocorrect: the gap is recorded,
|
|
29
|
+
# prompt-injected, and surfaced in proof-of-done. LOKI_ASSUMPTIONS_REQUIRE_CONFIRM=1
|
|
30
|
+
# disables auto-ack for a human-in-the-loop path (only confirmed=true clears).
|
|
31
|
+
#
|
|
32
|
+
# Provider-aware + clean degrade: grill needs a provider CLI; when absent we log
|
|
33
|
+
# an honest message, skip the grill subcall (NO fabricated questions), but STILL
|
|
34
|
+
# fold prd-analyzer's deterministic missing-dimension assumptions into the ledger
|
|
35
|
+
# as medium (non-blocking) so degrade still surfaces something.
|
|
36
|
+
#
|
|
37
|
+
# Opt-out knobs (all default-on):
|
|
38
|
+
# LOKI_SPEC_GRILL=0 skip interrogation entirely
|
|
39
|
+
# LOKI_ASSUMPTION_GATE=0 completion gate is pass-through (gate file)
|
|
40
|
+
# LOKI_ASSUMPTIONS_REQUIRE_CONFIRM=1 require human confirmed=true (no auto-ack)
|
|
41
|
+
|
|
42
|
+
set -uo pipefail
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# Logging shims. run.sh provides log_* helpers; when sourced standalone (tests,
|
|
46
|
+
# direct invocation) fall back to stderr so the module is self-contained.
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
if ! type log_info >/dev/null 2>&1; then
|
|
49
|
+
log_info() { printf '%s\n' "$*" >&2; }
|
|
50
|
+
fi
|
|
51
|
+
if ! type log_warn >/dev/null 2>&1; then
|
|
52
|
+
log_warn() { printf '[warn] %s\n' "$*" >&2; }
|
|
53
|
+
fi
|
|
54
|
+
if ! type log_step >/dev/null 2>&1; then
|
|
55
|
+
log_step() { printf '%s\n' "$*" >&2; }
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
SPEC_LEDGER_DIR_DEFAULT=".loki/assumptions"
|
|
59
|
+
|
|
60
|
+
# Resolve the ledger directory (respects TARGET_DIR like the rest of the runner).
|
|
61
|
+
_spec_ledger_dir() {
|
|
62
|
+
printf '%s/%s' "${TARGET_DIR:-.}" "$SPEC_LEDGER_DIR_DEFAULT"
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
# Deterministic severity for a grill finding given its section + line text.
|
|
67
|
+
# HIGH: security / scale / reliability / missing-or-untestable acceptance
|
|
68
|
+
# criteria / explicit contradiction. MEDIUM: everything else.
|
|
69
|
+
# Echoes "high" or "medium".
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
spec_interrogation_severity_for() {
|
|
72
|
+
local section="$1"
|
|
73
|
+
local line="$2"
|
|
74
|
+
local lc_section lc_line
|
|
75
|
+
lc_section="$(printf '%s' "$section" | tr '[:upper:]' '[:lower:]')"
|
|
76
|
+
lc_line="$(printf '%s' "$line" | tr '[:upper:]' '[:lower:]')"
|
|
77
|
+
|
|
78
|
+
# Explicit contradiction keywords escalate to high regardless of section.
|
|
79
|
+
case "$lc_line" in
|
|
80
|
+
*contradict*|*conflict*|*inconsistent*|*mutually\ exclusive*)
|
|
81
|
+
printf 'high'; return 0 ;;
|
|
82
|
+
esac
|
|
83
|
+
|
|
84
|
+
# Section-driven severity.
|
|
85
|
+
case "$lc_section" in
|
|
86
|
+
*security*|*scale*|*reliability*)
|
|
87
|
+
printf 'high'; return 0 ;;
|
|
88
|
+
esac
|
|
89
|
+
|
|
90
|
+
# Missing or untestable acceptance criteria are high (cannot verify done).
|
|
91
|
+
case "$lc_line" in
|
|
92
|
+
*acceptance\ criteria*|*acceptance\ criterion*|*testable*|*measurable*|*definition\ of\ done*)
|
|
93
|
+
printf 'high'; return 0 ;;
|
|
94
|
+
esac
|
|
95
|
+
|
|
96
|
+
printf 'medium'
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
# Map a grill section heading to a finding class.
|
|
101
|
+
# Echoes one of: ambiguous | contradictory | underspecified | missing
|
|
102
|
+
# (contradictory is also forced at line level when a contradiction keyword hits).
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
spec_interrogation_class_for() {
|
|
105
|
+
local section="$1"
|
|
106
|
+
local line="$2"
|
|
107
|
+
local lc_section lc_line
|
|
108
|
+
lc_section="$(printf '%s' "$section" | tr '[:upper:]' '[:lower:]')"
|
|
109
|
+
lc_line="$(printf '%s' "$line" | tr '[:upper:]' '[:lower:]')"
|
|
110
|
+
|
|
111
|
+
case "$lc_line" in
|
|
112
|
+
*contradict*|*conflict*|*inconsistent*|*mutually\ exclusive*)
|
|
113
|
+
printf 'contradictory'; return 0 ;;
|
|
114
|
+
esac
|
|
115
|
+
|
|
116
|
+
case "$lc_section" in
|
|
117
|
+
*security*|*scale*|*reliability*) printf 'missing'; return 0 ;;
|
|
118
|
+
*unstated\ assumption*) printf 'underspecified'; return 0 ;;
|
|
119
|
+
*ambiguit*|*acceptance*) printf 'ambiguous'; return 0 ;;
|
|
120
|
+
esac
|
|
121
|
+
printf 'ambiguous'
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
# Map a grill section heading to an "affects" area for the ledger.
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
_spec_affects_for() {
|
|
128
|
+
local section="$1"
|
|
129
|
+
local lc_section
|
|
130
|
+
lc_section="$(printf '%s' "$section" | tr '[:upper:]' '[:lower:]')"
|
|
131
|
+
case "$lc_section" in
|
|
132
|
+
*security*) printf 'security' ;;
|
|
133
|
+
*scale*|*reliability*) printf 'scale-reliability' ;;
|
|
134
|
+
*acceptance*|*ambiguit*) printf 'acceptance-criteria' ;;
|
|
135
|
+
*unstated\ assumption*) printf 'requirements' ;;
|
|
136
|
+
*) printf 'requirements' ;;
|
|
137
|
+
esac
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
# Stable, dedupe-safe id for a gap: a-<8 hex of the gap text>.
|
|
142
|
+
# Idempotent: the same gap text always yields the same id, so re-running
|
|
143
|
+
# DISCOVERY does not duplicate ledger entries.
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
_spec_gap_id() {
|
|
146
|
+
local gap="$1"
|
|
147
|
+
local h
|
|
148
|
+
if command -v shasum >/dev/null 2>&1; then
|
|
149
|
+
h="$(printf '%s' "$gap" | shasum 2>/dev/null | cut -c1-8)"
|
|
150
|
+
elif command -v sha1sum >/dev/null 2>&1; then
|
|
151
|
+
h="$(printf '%s' "$gap" | sha1sum 2>/dev/null | cut -c1-8)"
|
|
152
|
+
else
|
|
153
|
+
# cksum fallback (always present): pad/truncate to 8 chars.
|
|
154
|
+
h="$(printf '%s' "$gap" | cksum 2>/dev/null | cut -d' ' -f1)"
|
|
155
|
+
h="$(printf '%08x' "${h:-0}" 2>/dev/null | cut -c1-8)"
|
|
156
|
+
fi
|
|
157
|
+
printf 'a-%s' "${h:-00000000}"
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
# ---------------------------------------------------------------------------
|
|
161
|
+
# Write (or skip-if-present) one ledger entry.
|
|
162
|
+
# Usage: spec_ledger_write <gap> <assumption> <why> <severity> <class> <affects> <source>
|
|
163
|
+
# Idempotent on the gap id. Returns 0 always (best-effort; never fails a run).
|
|
164
|
+
# ---------------------------------------------------------------------------
|
|
165
|
+
spec_ledger_write() {
|
|
166
|
+
local gap="$1" assumption="$2" why="$3" severity="$4" class="$5" affects="$6" source="$7"
|
|
167
|
+
local dir id file
|
|
168
|
+
dir="$(_spec_ledger_dir)"
|
|
169
|
+
mkdir -p "$dir" 2>/dev/null || return 0
|
|
170
|
+
id="$(_spec_gap_id "$gap")"
|
|
171
|
+
file="$dir/$id.json"
|
|
172
|
+
# Idempotent: if this gap is already recorded, do not overwrite (preserves
|
|
173
|
+
# any confirmed/acknowledged state set since).
|
|
174
|
+
[ -f "$file" ] && return 0
|
|
175
|
+
|
|
176
|
+
local ts
|
|
177
|
+
ts="$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "")"
|
|
178
|
+
_SL_ID="$id" _SL_GAP="$gap" _SL_ASSUMP="$assumption" _SL_WHY="$why" \
|
|
179
|
+
_SL_SEV="$severity" _SL_CLASS="$class" _SL_AFFECTS="$affects" \
|
|
180
|
+
_SL_SOURCE="$source" _SL_TS="$ts" _SL_FILE="$file" python3 -c '
|
|
181
|
+
import json, os, tempfile
|
|
182
|
+
rec = {
|
|
183
|
+
"id": os.environ["_SL_ID"],
|
|
184
|
+
"gap": os.environ["_SL_GAP"],
|
|
185
|
+
"assumption": os.environ["_SL_ASSUMP"],
|
|
186
|
+
"why": os.environ["_SL_WHY"],
|
|
187
|
+
"severity": os.environ["_SL_SEV"],
|
|
188
|
+
"class": os.environ["_SL_CLASS"],
|
|
189
|
+
"affects": os.environ["_SL_AFFECTS"],
|
|
190
|
+
"source": os.environ["_SL_SOURCE"],
|
|
191
|
+
"confirmed": False,
|
|
192
|
+
"acknowledged": False,
|
|
193
|
+
"created_at": os.environ["_SL_TS"],
|
|
194
|
+
}
|
|
195
|
+
out = os.environ["_SL_FILE"]
|
|
196
|
+
d = os.path.dirname(out)
|
|
197
|
+
fd, tmp = tempfile.mkstemp(dir=d, suffix=".tmp")
|
|
198
|
+
try:
|
|
199
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
200
|
+
json.dump(rec, f, indent=2)
|
|
201
|
+
os.replace(tmp, out)
|
|
202
|
+
except Exception:
|
|
203
|
+
try: os.unlink(tmp)
|
|
204
|
+
except OSError: pass
|
|
205
|
+
raise
|
|
206
|
+
' 2>/dev/null || true
|
|
207
|
+
return 0
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# ---------------------------------------------------------------------------
|
|
211
|
+
# Classify a grill report.md into ledger entries.
|
|
212
|
+
# Usage: spec_interrogation_classify_report <report.md path>
|
|
213
|
+
# Pure: reads the markdown, writes ledger entries. No provider call. This is the
|
|
214
|
+
# function the test (a) drives with a fixture report.
|
|
215
|
+
# Returns 0 on success (including zero findings), 1 if the report is missing.
|
|
216
|
+
# ---------------------------------------------------------------------------
|
|
217
|
+
spec_interrogation_classify_report() {
|
|
218
|
+
local report="$1"
|
|
219
|
+
[ -f "$report" ] || return 1
|
|
220
|
+
|
|
221
|
+
local section=""
|
|
222
|
+
local line stripped q
|
|
223
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
224
|
+
# Track the current "### Section" heading.
|
|
225
|
+
case "$line" in
|
|
226
|
+
"### "*)
|
|
227
|
+
section="${line#"### "}"
|
|
228
|
+
continue ;;
|
|
229
|
+
"## "*)
|
|
230
|
+
# A top-level heading (e.g. "## Grill findings") is not a finding
|
|
231
|
+
# section; reset so stray numbered lines under it are ignored
|
|
232
|
+
# until a real ### section starts.
|
|
233
|
+
section=""
|
|
234
|
+
continue ;;
|
|
235
|
+
esac
|
|
236
|
+
|
|
237
|
+
[ -z "$section" ] && continue
|
|
238
|
+
|
|
239
|
+
# Finding lines look like "1. <question>" or "- <question>".
|
|
240
|
+
case "$line" in
|
|
241
|
+
[0-9]*". "*)
|
|
242
|
+
q="${line#*. }" ;;
|
|
243
|
+
"- "*)
|
|
244
|
+
q="${line#- }" ;;
|
|
245
|
+
*)
|
|
246
|
+
continue ;;
|
|
247
|
+
esac
|
|
248
|
+
|
|
249
|
+
# Trim leading/trailing whitespace.
|
|
250
|
+
stripped="$(printf '%s' "$q" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
|
251
|
+
[ -z "$stripped" ] && continue
|
|
252
|
+
# Skip explicit "None identified." placeholders (no fabricated findings).
|
|
253
|
+
case "$stripped" in
|
|
254
|
+
"None identified"*|"None."*|"None"|"N/A"*) continue ;;
|
|
255
|
+
esac
|
|
256
|
+
|
|
257
|
+
local sev class affects assumption
|
|
258
|
+
sev="$(spec_interrogation_severity_for "$section" "$stripped")"
|
|
259
|
+
class="$(spec_interrogation_class_for "$section" "$stripped")"
|
|
260
|
+
affects="$(_spec_affects_for "$section")"
|
|
261
|
+
# No-fabrication: the finding is a QUESTION; the honest assumption is a
|
|
262
|
+
# stated default, NOT an invented resolution the build will not follow.
|
|
263
|
+
assumption="Spec gives no answer; proceeding with the implementer default for ${affects}."
|
|
264
|
+
|
|
265
|
+
spec_ledger_write \
|
|
266
|
+
"$stripped" \
|
|
267
|
+
"$assumption" \
|
|
268
|
+
"grill: ${section}" \
|
|
269
|
+
"$sev" \
|
|
270
|
+
"$class" \
|
|
271
|
+
"$affects" \
|
|
272
|
+
"grill"
|
|
273
|
+
done < "$report"
|
|
274
|
+
return 0
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
# ---------------------------------------------------------------------------
|
|
278
|
+
# Fold prd-analyzer's deterministic missing-dimension assumptions into the
|
|
279
|
+
# ledger as medium (non-blocking). Reads .loki/prd-observations.md "Assumptions
|
|
280
|
+
# Made" section. Best-effort; runs even when no provider is available so degrade
|
|
281
|
+
# still surfaces something. Usage: spec_ledger_fold_prd_observations [path]
|
|
282
|
+
# ---------------------------------------------------------------------------
|
|
283
|
+
# shellcheck disable=SC2120 # optional [path] arg by design (see Usage above); callers pass none
|
|
284
|
+
spec_ledger_fold_prd_observations() {
|
|
285
|
+
local obs="${1:-${TARGET_DIR:-.}/.loki/prd-observations.md}"
|
|
286
|
+
[ -f "$obs" ] || return 0
|
|
287
|
+
|
|
288
|
+
local in_section="false" line item
|
|
289
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
290
|
+
case "$line" in
|
|
291
|
+
"## Assumptions Made"*) in_section="true"; continue ;;
|
|
292
|
+
"## "*) in_section="false"; continue ;;
|
|
293
|
+
esac
|
|
294
|
+
[ "$in_section" = "true" ] || continue
|
|
295
|
+
case "$line" in
|
|
296
|
+
"- "*)
|
|
297
|
+
item="${line#- }"
|
|
298
|
+
item="$(printf '%s' "$item" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" ;;
|
|
299
|
+
*)
|
|
300
|
+
continue ;;
|
|
301
|
+
esac
|
|
302
|
+
[ -z "$item" ] && continue
|
|
303
|
+
# The analyzer emits "No assumptions needed; PRD is comprehensive" when
|
|
304
|
+
# the PRD is clean: that is not a gap, skip it.
|
|
305
|
+
case "$item" in
|
|
306
|
+
"No assumptions needed"*) continue ;;
|
|
307
|
+
esac
|
|
308
|
+
# Use the analyzer's assumption text as the gap so each distinct missing
|
|
309
|
+
# dimension gets its own ledger entry (the dedupe id derives from the gap
|
|
310
|
+
# text; a constant gap would collapse all dimensions into one entry).
|
|
311
|
+
spec_ledger_write \
|
|
312
|
+
"Missing PRD dimension: ${item}" \
|
|
313
|
+
"$item" \
|
|
314
|
+
"prd-analyzer: missing dimension" \
|
|
315
|
+
"medium" \
|
|
316
|
+
"missing" \
|
|
317
|
+
"requirements" \
|
|
318
|
+
"prd-analyzer"
|
|
319
|
+
done < "$obs"
|
|
320
|
+
return 0
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
# ---------------------------------------------------------------------------
|
|
324
|
+
# Count ledger entries that BLOCK completion: severity=high AND confirmed=false
|
|
325
|
+
# AND acknowledged=false. Echoes an integer. Used by the council gate and the
|
|
326
|
+
# completion summary. Zero when the ledger dir is absent.
|
|
327
|
+
# ---------------------------------------------------------------------------
|
|
328
|
+
spec_ledger_high_unresolved_count() {
|
|
329
|
+
local dir
|
|
330
|
+
dir="$(_spec_ledger_dir)"
|
|
331
|
+
if [ ! -d "$dir" ]; then printf '0'; return 0; fi
|
|
332
|
+
_SL_DIR="$dir" python3 -c '
|
|
333
|
+
import glob, json, os
|
|
334
|
+
d = os.environ["_SL_DIR"]
|
|
335
|
+
n = 0
|
|
336
|
+
for p in glob.glob(os.path.join(d, "a-*.json")):
|
|
337
|
+
try:
|
|
338
|
+
with open(p) as f:
|
|
339
|
+
r = json.load(f)
|
|
340
|
+
except Exception:
|
|
341
|
+
continue
|
|
342
|
+
if r.get("severity") == "high" and not r.get("confirmed") and not r.get("acknowledged"):
|
|
343
|
+
n += 1
|
|
344
|
+
print(n)
|
|
345
|
+
' 2>/dev/null || printf '0'
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
# Total ledger entries + high count, "total high" on one line. For summaries.
|
|
349
|
+
spec_ledger_counts() {
|
|
350
|
+
local dir
|
|
351
|
+
dir="$(_spec_ledger_dir)"
|
|
352
|
+
if [ ! -d "$dir" ]; then printf '0 0'; return 0; fi
|
|
353
|
+
_SL_DIR="$dir" python3 -c '
|
|
354
|
+
import glob, json, os
|
|
355
|
+
d = os.environ["_SL_DIR"]
|
|
356
|
+
total = high = 0
|
|
357
|
+
for p in glob.glob(os.path.join(d, "a-*.json")):
|
|
358
|
+
try:
|
|
359
|
+
with open(p) as f:
|
|
360
|
+
r = json.load(f)
|
|
361
|
+
except Exception:
|
|
362
|
+
continue
|
|
363
|
+
total += 1
|
|
364
|
+
if r.get("severity") == "high":
|
|
365
|
+
high += 1
|
|
366
|
+
print("%d %d" % (total, high))
|
|
367
|
+
' 2>/dev/null || printf '0 0'
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
# ---------------------------------------------------------------------------
|
|
371
|
+
# Auto-acknowledgment lifecycle helper: set acknowledged=true on every ledger
|
|
372
|
+
# entry. run.sh calls this once an iteration AFTER assumptions are injected into
|
|
373
|
+
# the build prompt (unless LOKI_ASSUMPTIONS_REQUIRE_CONFIRM=1). Best-effort.
|
|
374
|
+
# ---------------------------------------------------------------------------
|
|
375
|
+
spec_ledger_acknowledge_all() {
|
|
376
|
+
[ "${LOKI_ASSUMPTIONS_REQUIRE_CONFIRM:-0}" = "1" ] && return 0
|
|
377
|
+
local dir
|
|
378
|
+
dir="$(_spec_ledger_dir)"
|
|
379
|
+
[ -d "$dir" ] || return 0
|
|
380
|
+
_SL_DIR="$dir" python3 -c '
|
|
381
|
+
import glob, json, os, tempfile
|
|
382
|
+
d = os.environ["_SL_DIR"]
|
|
383
|
+
for p in glob.glob(os.path.join(d, "a-*.json")):
|
|
384
|
+
try:
|
|
385
|
+
with open(p) as f:
|
|
386
|
+
r = json.load(f)
|
|
387
|
+
except Exception:
|
|
388
|
+
continue
|
|
389
|
+
if r.get("acknowledged"):
|
|
390
|
+
continue
|
|
391
|
+
r["acknowledged"] = True
|
|
392
|
+
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(p), suffix=".tmp")
|
|
393
|
+
try:
|
|
394
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
395
|
+
json.dump(r, f, indent=2)
|
|
396
|
+
os.replace(tmp, p)
|
|
397
|
+
except Exception:
|
|
398
|
+
try: os.unlink(tmp)
|
|
399
|
+
except OSError: pass
|
|
400
|
+
' 2>/dev/null || true
|
|
401
|
+
return 0
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
# ---------------------------------------------------------------------------
|
|
405
|
+
# Build a compact prompt-injection block listing high-severity assumptions, so
|
|
406
|
+
# the build agent sees the spec gaps it must respect. Echoes the block (empty
|
|
407
|
+
# when no high-sev entries). Used by build_prompt in run.sh.
|
|
408
|
+
# ---------------------------------------------------------------------------
|
|
409
|
+
spec_ledger_prompt_block() {
|
|
410
|
+
local dir
|
|
411
|
+
dir="$(_spec_ledger_dir)"
|
|
412
|
+
[ -d "$dir" ] || return 0
|
|
413
|
+
_SL_DIR="$dir" python3 -c '
|
|
414
|
+
import glob, json, os
|
|
415
|
+
d = os.environ["_SL_DIR"]
|
|
416
|
+
rows = []
|
|
417
|
+
for p in sorted(glob.glob(os.path.join(d, "a-*.json"))):
|
|
418
|
+
try:
|
|
419
|
+
with open(p) as f:
|
|
420
|
+
r = json.load(f)
|
|
421
|
+
except Exception:
|
|
422
|
+
continue
|
|
423
|
+
if r.get("severity") != "high" or r.get("confirmed"):
|
|
424
|
+
continue
|
|
425
|
+
rows.append("- [%s] %s -> assumed: %s" % (r.get("affects",""), r.get("gap",""), r.get("assumption","")))
|
|
426
|
+
if rows:
|
|
427
|
+
print("SPEC ASSUMPTIONS (high-severity, recorded because the spec was ambiguous; respect these or fix the spec): " + " ".join(rows))
|
|
428
|
+
' 2>/dev/null || true
|
|
429
|
+
return 0
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
# ---------------------------------------------------------------------------
|
|
433
|
+
# Regenerate the human-readable ledger rollup .loki/assumptions/ledger.md.
|
|
434
|
+
# Best-effort. Called after writes and surfaced in proof-of-done.
|
|
435
|
+
# ---------------------------------------------------------------------------
|
|
436
|
+
spec_ledger_rebuild_md() {
|
|
437
|
+
local dir
|
|
438
|
+
dir="$(_spec_ledger_dir)"
|
|
439
|
+
[ -d "$dir" ] || return 0
|
|
440
|
+
_SL_DIR="$dir" python3 -c '
|
|
441
|
+
import glob, json, os, tempfile
|
|
442
|
+
d = os.environ["_SL_DIR"]
|
|
443
|
+
entries = []
|
|
444
|
+
for p in sorted(glob.glob(os.path.join(d, "a-*.json"))):
|
|
445
|
+
try:
|
|
446
|
+
with open(p) as f:
|
|
447
|
+
entries.append(json.load(f))
|
|
448
|
+
except Exception:
|
|
449
|
+
continue
|
|
450
|
+
lines = ["# Assumption ledger", ""]
|
|
451
|
+
if not entries:
|
|
452
|
+
lines.append("No assumptions recorded. The spec was complete and unambiguous.")
|
|
453
|
+
else:
|
|
454
|
+
high = sum(1 for e in entries if e.get("severity") == "high")
|
|
455
|
+
lines.append("Total assumptions: %d (%d high-severity)" % (len(entries), high))
|
|
456
|
+
lines.append("")
|
|
457
|
+
for e in entries:
|
|
458
|
+
state = "confirmed" if e.get("confirmed") else ("acknowledged" if e.get("acknowledged") else "OPEN")
|
|
459
|
+
lines.append("## %s [%s / %s / %s]" % (e.get("id",""), e.get("severity",""), e.get("class",""), state))
|
|
460
|
+
lines.append("")
|
|
461
|
+
lines.append("- Gap: %s" % e.get("gap",""))
|
|
462
|
+
lines.append("- Assumption: %s" % e.get("assumption",""))
|
|
463
|
+
lines.append("- Why: %s" % e.get("why",""))
|
|
464
|
+
lines.append("- Affects: %s" % e.get("affects",""))
|
|
465
|
+
lines.append("- Source: %s" % e.get("source",""))
|
|
466
|
+
lines.append("")
|
|
467
|
+
out = os.path.join(d, "ledger.md")
|
|
468
|
+
fd, tmp = tempfile.mkstemp(dir=d, suffix=".tmp")
|
|
469
|
+
try:
|
|
470
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
471
|
+
f.write("\n".join(lines) + "\n")
|
|
472
|
+
os.replace(tmp, out)
|
|
473
|
+
except Exception:
|
|
474
|
+
try: os.unlink(tmp)
|
|
475
|
+
except OSError: pass
|
|
476
|
+
' 2>/dev/null || true
|
|
477
|
+
return 0
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
# ---------------------------------------------------------------------------
|
|
481
|
+
# DISCOVERY orchestrator: run spec interrogation and populate the ledger.
|
|
482
|
+
# Usage: spec_interrogation_run <spec_path>
|
|
483
|
+
# Default-on; LOKI_SPEC_GRILL=0 opts out. Always non-fatal to the run.
|
|
484
|
+
# ---------------------------------------------------------------------------
|
|
485
|
+
spec_interrogation_run() {
|
|
486
|
+
local spec_path="${1:-}"
|
|
487
|
+
|
|
488
|
+
if [ "${LOKI_SPEC_GRILL:-1}" = "0" ]; then
|
|
489
|
+
return 0
|
|
490
|
+
fi
|
|
491
|
+
|
|
492
|
+
# Source grill.sh for grill_main + grill_check_provider. Best-effort: if it
|
|
493
|
+
# is missing we still fold prd-analyzer assumptions below.
|
|
494
|
+
local _self_dir grill_sh
|
|
495
|
+
_self_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
496
|
+
grill_sh="$_self_dir/grill.sh"
|
|
497
|
+
local grill_available="false"
|
|
498
|
+
if [ -f "$grill_sh" ]; then
|
|
499
|
+
# shellcheck disable=SC1090
|
|
500
|
+
. "$grill_sh" 2>/dev/null && grill_available="true"
|
|
501
|
+
fi
|
|
502
|
+
|
|
503
|
+
log_step "Spec interrogation (DISCOVERY): surfacing ambiguities as recorded assumptions..."
|
|
504
|
+
|
|
505
|
+
# Provider-aware grill subcall. Degrade cleanly (no fabricated questions).
|
|
506
|
+
if [ "$grill_available" = "true" ] && type grill_check_provider >/dev/null 2>&1; then
|
|
507
|
+
if grill_check_provider 2>/dev/null; then
|
|
508
|
+
local report_dir
|
|
509
|
+
report_dir="${TARGET_DIR:-.}/.loki/grill"
|
|
510
|
+
# grill_main resolves the spec source itself; pass the explicit path
|
|
511
|
+
# when we have one so it grills exactly the active spec.
|
|
512
|
+
if [ -n "$spec_path" ] && [ -f "$spec_path" ]; then
|
|
513
|
+
grill_main "$spec_path" --out "$report_dir" >/dev/null 2>&1 || \
|
|
514
|
+
log_warn "Spec interrogation: grill subcall failed; continuing with prd-analyzer assumptions only."
|
|
515
|
+
else
|
|
516
|
+
grill_main --out "$report_dir" >/dev/null 2>&1 || \
|
|
517
|
+
log_warn "Spec interrogation: grill subcall failed; continuing with prd-analyzer assumptions only."
|
|
518
|
+
fi
|
|
519
|
+
local report="$report_dir/report.md"
|
|
520
|
+
if [ -f "$report" ]; then
|
|
521
|
+
spec_interrogation_classify_report "$report" || true
|
|
522
|
+
fi
|
|
523
|
+
else
|
|
524
|
+
log_warn "Spec interrogation: no provider CLI available; skipping the Devil's-Advocate grill (no fabricated questions). Recording prd-analyzer assumptions only."
|
|
525
|
+
fi
|
|
526
|
+
else
|
|
527
|
+
log_warn "Spec interrogation: grill module unavailable; recording prd-analyzer assumptions only."
|
|
528
|
+
fi
|
|
529
|
+
|
|
530
|
+
# Always fold prd-analyzer's deterministic missing-dimension assumptions
|
|
531
|
+
# (works with no provider) so degrade still surfaces something.
|
|
532
|
+
spec_ledger_fold_prd_observations || true
|
|
533
|
+
|
|
534
|
+
spec_ledger_rebuild_md || true
|
|
535
|
+
|
|
536
|
+
local counts total high
|
|
537
|
+
counts="$(spec_ledger_counts)"
|
|
538
|
+
total="${counts%% *}"
|
|
539
|
+
high="${counts##* }"
|
|
540
|
+
if [ "${total:-0}" != "0" ]; then
|
|
541
|
+
log_info "Spec interrogation recorded ${total} assumption(s) (${high} high-severity) under .loki/assumptions/."
|
|
542
|
+
fi
|
|
543
|
+
return 0
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
# Allow direct execution for debugging: bash autonomy/spec-interrogation.sh <spec>
|
|
547
|
+
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
|
|
548
|
+
spec_interrogation_run "${1:-}"
|
|
549
|
+
exit $?
|
|
550
|
+
fi
|
package/autonomy/telemetry.sh
CHANGED
|
@@ -1,27 +1,47 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Anonymous usage telemetry for Loki Mode
|
|
3
|
-
#
|
|
3
|
+
# Collection is OPT-IN and OFF by default. Nothing is sent unless the user opts
|
|
4
|
+
# in, so a default install never phones home (air-gapped / GDPR / FedRAMP safe).
|
|
5
|
+
# Opt-in: LOKI_TELEMETRY=on OR ~/.loki/config: TELEMETRY_ENABLED=true
|
|
6
|
+
# Opt-out (always wins): LOKI_TELEMETRY=off / LOKI_TELEMETRY_DISABLED=true /
|
|
7
|
+
# DO_NOT_TRACK=1 / ~/.loki/config: TELEMETRY_DISABLED=true
|
|
4
8
|
# All calls are fire-and-forget, silent on failure, non-blocking
|
|
5
9
|
|
|
6
10
|
LOKI_POSTHOG_HOST="${LOKI_TELEMETRY_ENDPOINT:-https://us.i.posthog.com}"
|
|
7
11
|
LOKI_POSTHOG_KEY="phc_ya0vGBru41AJWtGNfZZ8H9W4yjoZy4KON0nnayS7s87"
|
|
8
12
|
|
|
9
13
|
_loki_telemetry_enabled() {
|
|
10
|
-
# Unified
|
|
11
|
-
#
|
|
12
|
-
# crash
|
|
13
|
-
#
|
|
14
|
+
# Unified OPT-IN gate. Returns 0 (enabled) ONLY when the user opted in AND
|
|
15
|
+
# did not also opt out. Opt-out always wins; default is OFF. This precedence
|
|
16
|
+
# MUST mirror loki_collection_enabled in autonomy/crash.sh and _is_enabled in
|
|
17
|
+
# dashboard/telemetry.py so one model gates BOTH usage telemetry and crash
|
|
18
|
+
# reporting.
|
|
19
|
+
# 1. Any opt-out flag present -> 1 (hard kill, always wins)
|
|
20
|
+
# 2. Else any opt-in flag present -> 0
|
|
21
|
+
# 3. Else (default) -> 1 (no egress)
|
|
14
22
|
local _telem_lower
|
|
15
23
|
_telem_lower="$(printf '%s' "${LOKI_TELEMETRY:-}" | tr '[:upper:]' '[:lower:]')"
|
|
24
|
+
|
|
25
|
+
# --- 1. Opt-out always wins ---
|
|
16
26
|
[ "$_telem_lower" = "off" ] && return 1
|
|
17
27
|
[ "${LOKI_TELEMETRY_DISABLED:-}" = "true" ] && return 1
|
|
18
28
|
[ "${DO_NOT_TRACK:-}" = "1" ] && return 1
|
|
19
|
-
# Persistent opt-out in ~/.loki/config
|
|
20
29
|
if [ -f "${HOME}/.loki/config" ] && grep -q "^TELEMETRY_DISABLED=true" "${HOME}/.loki/config" 2>/dev/null; then
|
|
21
30
|
return 1
|
|
22
31
|
fi
|
|
23
|
-
|
|
24
|
-
|
|
32
|
+
|
|
33
|
+
# --- 2. Opt-in required to enable ---
|
|
34
|
+
if [ "$_telem_lower" = "on" ]; then
|
|
35
|
+
command -v curl >/dev/null 2>&1 || return 1
|
|
36
|
+
return 0
|
|
37
|
+
fi
|
|
38
|
+
if [ -f "${HOME}/.loki/config" ] && grep -q "^TELEMETRY_ENABLED=true" "${HOME}/.loki/config" 2>/dev/null; then
|
|
39
|
+
command -v curl >/dev/null 2>&1 || return 1
|
|
40
|
+
return 0
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# --- 3. Default: OFF ---
|
|
44
|
+
return 1
|
|
25
45
|
}
|
|
26
46
|
|
|
27
47
|
_loki_telemetry_id() {
|
package/bin/postinstall.js
CHANGED
|
@@ -179,23 +179,35 @@ console.log('');
|
|
|
179
179
|
console.log('New here? Run `loki welcome` for a 30-second tour.');
|
|
180
180
|
console.log('');
|
|
181
181
|
|
|
182
|
-
// Anonymous install telemetry (fire-and-forget, silent)
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
// crash
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
182
|
+
// Anonymous install telemetry (fire-and-forget, silent).
|
|
183
|
+
// Collection is OPT-IN and OFF by default: a default `npm install` (including
|
|
184
|
+
// air-gapped, GDPR, and FedRAMP environments) sends NOTHING. This precedence
|
|
185
|
+
// mirrors loki_collection_enabled in autonomy/crash.sh, _is_enabled in
|
|
186
|
+
// dashboard/telemetry.py, and _loki_telemetry_enabled in autonomy/telemetry.sh.
|
|
187
|
+
// 1. Any opt-out flag present -> false (hard kill, always wins)
|
|
188
|
+
// 2. Else any opt-in flag present -> true
|
|
189
|
+
// 3. Else (default) -> false (no egress)
|
|
190
|
+
function _lokiCollectionEnabled() {
|
|
191
|
+
const telem = (process.env.LOKI_TELEMETRY || '').toLowerCase();
|
|
192
|
+
// 1. Opt-out always wins.
|
|
193
|
+
if (telem === 'off') return false;
|
|
194
|
+
if (process.env.LOKI_TELEMETRY_DISABLED === 'true') return false;
|
|
195
|
+
if (process.env.DO_NOT_TRACK === '1') return false;
|
|
196
|
+
let configEnabled = false;
|
|
190
197
|
try {
|
|
191
198
|
const cfg = path.join(homeDir, '.loki', 'config');
|
|
192
199
|
const lines = fs.readFileSync(cfg, 'utf8').split('\n');
|
|
193
|
-
if (lines.some((l) => l.startsWith('TELEMETRY_DISABLED=true'))) return
|
|
200
|
+
if (lines.some((l) => l.startsWith('TELEMETRY_DISABLED=true'))) return false;
|
|
201
|
+
if (lines.some((l) => l.startsWith('TELEMETRY_ENABLED=true'))) configEnabled = true;
|
|
194
202
|
} catch {}
|
|
203
|
+
// 2. Opt-in required.
|
|
204
|
+
if (telem === 'on') return true;
|
|
205
|
+
if (configEnabled) return true;
|
|
206
|
+
// 3. Default: OFF.
|
|
195
207
|
return false;
|
|
196
208
|
}
|
|
197
209
|
try {
|
|
198
|
-
if (
|
|
210
|
+
if (_lokiCollectionEnabled()) {
|
|
199
211
|
const https = require('https');
|
|
200
212
|
const crypto = require('crypto');
|
|
201
213
|
const idFile = path.join(homeDir, '.loki-telemetry-id');
|