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.
Files changed (90) hide show
  1. package/README.md +1 -1
  2. package/SKILL.md +2 -2
  3. package/VERSION +1 -1
  4. package/autonomy/completion-council.sh +113 -0
  5. package/autonomy/crash.sh +47 -21
  6. package/autonomy/loki +50 -27
  7. package/autonomy/run.sh +468 -5
  8. package/autonomy/spec-interrogation.sh +550 -0
  9. package/autonomy/telemetry.sh +28 -8
  10. package/bin/postinstall.js +22 -10
  11. package/dashboard/__init__.py +1 -1
  12. package/dashboard/auth.py +117 -2
  13. package/dashboard/telemetry.py +34 -6
  14. package/docs/ACKNOWLEDGEMENTS.md +1 -1
  15. package/docs/COMPETITIVE-ANALYSIS.md +1 -1
  16. package/docs/INSTALLATION.md +10 -3
  17. package/docs/OPEN-CORE-BOUNDARY.md +6 -5
  18. package/docs/P2-SPEC-ROBUSTNESS-PLAN.md +192 -0
  19. package/docs/PRIVACY.md +82 -24
  20. package/docs/R9-OPEN-CORE-HOOKS-PLAN.md +2 -2
  21. package/docs/auto-claude-comparison.md +2 -2
  22. package/docs/certification/README.md +1 -1
  23. package/docs/competitive/bolt-new-analysis.md +1 -1
  24. package/docs/competitive/emergence-others-analysis.md +6 -6
  25. package/docs/competitive/replit-lovable-analysis.md +4 -4
  26. package/docs/enterprise/security.md +43 -3
  27. package/docs/show-hn-post.md +1 -1
  28. package/loki-ts/dist/loki.js +30 -30
  29. package/mcp/__init__.py +1 -1
  30. package/package.json +1 -1
  31. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  32. package/web-app/dist/assets/{AdminPage-CKUOsWZW.js → AdminPage-CcCJ0Sjt.js} +1 -1
  33. package/web-app/dist/assets/{Avatar-CL9Id9Hi.js → Avatar-DK8kmayw.js} +1 -1
  34. package/web-app/dist/assets/{Badge-B12zwlD7.js → Badge-4uAWnemi.js} +1 -1
  35. package/web-app/dist/assets/{Button-CFLVoduT.js → Button-BBMk33tk.js} +1 -1
  36. package/web-app/dist/assets/ComparePage-bt9rwvST.js +1 -0
  37. package/web-app/dist/assets/{GitHubIssuesPanel-CSitxtAX.js → GitHubIssuesPanel-WDbH47UM.js} +1 -1
  38. package/web-app/dist/assets/{GitHubPRsPanel-BIT06FRo.js → GitHubPRsPanel-C2CiYtTx.js} +1 -1
  39. package/web-app/dist/assets/{HomePage-pU_0fGny.js → HomePage-BQk-MUjn.js} +4 -4
  40. package/web-app/dist/assets/{LoginPage-DTZtt2Yb.js → LoginPage-DMOZVGGL.js} +1 -1
  41. package/web-app/dist/assets/{MagicPage-10zfra8o.js → MagicPage-Bzp2Nt1z.js} +1 -1
  42. package/web-app/dist/assets/{MetricsPage-C-wiKUkv.js → MetricsPage-C39JVdsw.js} +1 -1
  43. package/web-app/dist/assets/{NotFoundPage-BDkcmhYe.js → NotFoundPage-6vT_U9UL.js} +1 -1
  44. package/web-app/dist/assets/{ProjectPage-CiCavQ8n.js → ProjectPage-BfFcZp-E.js} +3 -3
  45. package/web-app/dist/assets/{ProjectsPage-BLCXQwwC.js → ProjectsPage-CPMBf8Wt.js} +1 -1
  46. package/web-app/dist/assets/{SettingsPage-PkxtaMyg.js → SettingsPage-BnNN6ETl.js} +1 -1
  47. package/web-app/dist/assets/{ShowcasePage-iECp8Tha.js → ShowcasePage-WDrMf-cx.js} +1 -1
  48. package/web-app/dist/assets/{SystemSettingsPage-DS6Anno1.js → SystemSettingsPage-DX4jb2e8.js} +1 -1
  49. package/web-app/dist/assets/{TeamsPage-ls6h6bNL.js → TeamsPage-BCfqcXzu.js} +1 -1
  50. package/web-app/dist/assets/{TemplatesPage-Bk0QzlPt.js → TemplatesPage-CZvmimDj.js} +1 -1
  51. package/web-app/dist/assets/{TerminalOutput-4-1hWCtZ.js → TerminalOutput-BlRqFwWV.js} +1 -1
  52. package/web-app/dist/assets/{activity-DH3ih2nS.js → activity-CacZsUyr.js} +1 -1
  53. package/web-app/dist/assets/{bell-Gn17S6uv.js → bell-DK2qtHnk.js} +1 -1
  54. package/web-app/dist/assets/{bot-Cbycc3VE.js → bot-CkcUtHad.js} +1 -1
  55. package/web-app/dist/assets/{check-nIAqa-kf.js → check-CbCPjX3M.js} +1 -1
  56. package/web-app/dist/assets/{chevron-left-D2jcWDll.js → chevron-left-5NUKWw3i.js} +1 -1
  57. package/web-app/dist/assets/{circle-alert-CpL4Bhvt.js → circle-alert-S7uFoxC2.js} +1 -1
  58. package/web-app/dist/assets/{clock-IW4Wq86N.js → clock-CaQRrIrs.js} +1 -1
  59. package/web-app/dist/assets/{cloud-Cn8nNuH2.js → cloud-DBAX6c0r.js} +1 -1
  60. package/web-app/dist/assets/{code-xml-BiJBteXf.js → code-xml-De5-EXv3.js} +1 -1
  61. package/web-app/dist/assets/{copy-CnqkyNsi.js → copy-CUkT6k1v.js} +1 -1
  62. package/web-app/dist/assets/{database-CKSReqa5.js → database-BAWf1Gwt.js} +1 -1
  63. package/web-app/dist/assets/{dollar-sign-CDzDY64R.js → dollar-sign-Ji8zk86R.js} +1 -1
  64. package/web-app/dist/assets/{file-code-corner-Box4IwG1.js → file-code-corner-ChtXoBwS.js} +1 -1
  65. package/web-app/dist/assets/{file-plus-DpGqlXF8.js → file-plus-bFa37P76.js} +1 -1
  66. package/web-app/dist/assets/{folder-open-B57dAoBv.js → folder-open-DhXpXscO.js} +1 -1
  67. package/web-app/dist/assets/{git-commit-horizontal-BVbucmO5.js → git-commit-horizontal-DVPeDQ3j.js} +1 -1
  68. package/web-app/dist/assets/{globe-BkOnKl4x.js → globe-BPZgPeeu.js} +1 -1
  69. package/web-app/dist/assets/{hammer-DRbIQ4QU.js → hammer-jLCaujYH.js} +1 -1
  70. package/web-app/dist/assets/{index-CM_b_EhP.js → index-B-0iHBPO.js} +2 -2
  71. package/web-app/dist/assets/{layers-B78BiFiU.js → layers-B1vsrsFW.js} +1 -1
  72. package/web-app/dist/assets/{lightbulb-B-Itbm9g.js → lightbulb-C-uLoq9Y.js} +1 -1
  73. package/web-app/dist/assets/{loader-circle-Oq6NQhW2.js → loader-circle-JTfD-ZuM.js} +1 -1
  74. package/web-app/dist/assets/{lock-DbJ9zxbw.js → lock-G9rxD4gZ.js} +1 -1
  75. package/web-app/dist/assets/{mail-CzMRod6m.js → mail-BJ0PTN_V.js} +1 -1
  76. package/web-app/dist/assets/{package-WZ5osvej.js → package-CXClfLOO.js} +1 -1
  77. package/web-app/dist/assets/{plus-j08lFR-K.js → plus-EoL5OCB7.js} +1 -1
  78. package/web-app/dist/assets/{refresh-cw-CIr7E-g2.js → refresh-cw-BjREUnVq.js} +1 -1
  79. package/web-app/dist/assets/{rotate-ccw-gwoXxDeE.js → rotate-ccw-DahWX07H.js} +1 -1
  80. package/web-app/dist/assets/{save-B8fV_ZpE.js → save-Dek3gCn1.js} +1 -1
  81. package/web-app/dist/assets/{server-D5dO1paz.js → server-D6V1BAia.js} +1 -1
  82. package/web-app/dist/assets/{shield-alert-Du08zhdg.js → shield-alert-BtTK5Sxb.js} +1 -1
  83. package/web-app/dist/assets/{trash-2-DEKSVae5.js → trash-2-BT5o_g0r.js} +1 -1
  84. package/web-app/dist/assets/{trending-down-DBiXUtxJ.js → trending-down-D4Jk7KF3.js} +1 -1
  85. package/web-app/dist/assets/{trending-up-BgmK_tHq.js → trending-up-EQFTzhEo.js} +1 -1
  86. package/web-app/dist/assets/{upload-IaViyeVD.js → upload-JfI5lCSE.js} +1 -1
  87. package/web-app/dist/assets/{usePolling-PiRLqNu6.js → usePolling-BnhPUuGd.js} +1 -1
  88. package/web-app/dist/assets/{user-BB5J8wAF.js → user-DSUiUYtj.js} +1 -1
  89. package/web-app/dist/index.html +1 -1
  90. 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
@@ -1,27 +1,47 @@
1
1
  #!/usr/bin/env bash
2
2
  # Anonymous usage telemetry for Loki Mode
3
- # Opt-out: LOKI_TELEMETRY_DISABLED=true or DO_NOT_TRACK=1
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 opt-out: these checks must mirror loki_collection_enabled in
11
- # autonomy/crash.sh so one switch gates BOTH PostHog usage telemetry and
12
- # crash reporting.
13
- # LOKI_TELEMETRY=off (case-insensitive)
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
- command -v curl >/dev/null 2>&1 || return 1
24
- return 0
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() {
@@ -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
- // Unified opt-out: these checks mirror loki_collection_enabled in
184
- // autonomy/crash.sh so one switch gates BOTH PostHog usage telemetry and
185
- // crash reporting.
186
- function _lokiCollectionDisabled() {
187
- if ((process.env.LOKI_TELEMETRY || '').toLowerCase() === 'off') return true;
188
- if (process.env.LOKI_TELEMETRY_DISABLED === 'true') return true;
189
- if (process.env.DO_NOT_TRACK === '1') return true;
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 true;
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 (!_lokiCollectionDisabled()) {
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');
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.46.0"
10
+ __version__ = "7.48.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try: