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
package/dashboard/auth.py CHANGED
@@ -477,6 +477,111 @@ def _base64url_decode(data: str) -> bytes:
477
477
  return base64.urlsafe_b64decode(data)
478
478
 
479
479
 
480
+ # Role precedence (highest privilege first). When a token carries multiple
481
+ # recognized role claims, the highest-privilege match wins.
482
+ _ROLE_PRECEDENCE = ("admin", "operator", "auditor", "viewer")
483
+
484
+
485
+ def _normalize_claim_values(value) -> set[str]:
486
+ """Normalize an OIDC claim value into a lowercased set of strings.
487
+
488
+ Claim values may be a single string, a space-separated string, or a
489
+ list of strings (different providers use different shapes). All are
490
+ flattened into a set of lowercased tokens for matching against ROLES.
491
+ """
492
+ out: set[str] = set()
493
+ if value is None:
494
+ return out
495
+ if isinstance(value, str):
496
+ for part in value.split():
497
+ if part:
498
+ out.add(part.strip().lower())
499
+ elif isinstance(value, (list, tuple, set)):
500
+ for item in value:
501
+ if isinstance(item, str):
502
+ s = item.strip().lower()
503
+ if s:
504
+ out.add(s)
505
+ return out
506
+
507
+
508
+ def _collect_role_claims(claims: dict) -> set[str]:
509
+ """Collect candidate role/group values from standard OIDC claim shapes.
510
+
511
+ Recognized sources (case-insensitive values flattened into one set):
512
+ - A configurable claim named by LOKI_OIDC_ROLES_CLAIM (supports a dotted
513
+ path for nested claims, e.g. "realm_access.roles" for Keycloak).
514
+ - "roles" (generic)
515
+ - "groups" (generic)
516
+ - "realm_access.roles" (Keycloak)
517
+ - "cognito:groups" (AWS Cognito)
518
+
519
+ Note: "groups"/"cognito:groups" typically carry arbitrary group names,
520
+ not Loki role names. Only values that exactly match one of the four
521
+ built-in role names (admin/operator/viewer/auditor, case-insensitive)
522
+ grant a role. Everything else is ignored and the default role applies.
523
+ """
524
+ candidates: set[str] = set()
525
+
526
+ def _read_dotted(path: str):
527
+ node = claims
528
+ for key in path.split("."):
529
+ if isinstance(node, dict) and key in node:
530
+ node = node[key]
531
+ else:
532
+ return None
533
+ return node
534
+
535
+ configured = os.environ.get("LOKI_OIDC_ROLES_CLAIM", "").strip()
536
+ sources = []
537
+ if configured:
538
+ sources.append(configured)
539
+ sources.extend(["roles", "groups", "realm_access.roles", "cognito:groups"])
540
+
541
+ for src in sources:
542
+ if "." in src:
543
+ val = _read_dotted(src)
544
+ else:
545
+ val = claims.get(src)
546
+ candidates |= _normalize_claim_values(val)
547
+
548
+ return candidates
549
+
550
+
551
+ def _default_oidc_role() -> str:
552
+ """Return the configured default OIDC role, validated against ROLES.
553
+
554
+ Defaults to the least-privileged role ("viewer"). If LOKI_OIDC_DEFAULT_ROLE
555
+ is set to an unrecognized value, falls back to "viewer" (never admin).
556
+ """
557
+ configured = os.environ.get("LOKI_OIDC_DEFAULT_ROLE", "").strip().lower()
558
+ if configured in ROLES:
559
+ return configured
560
+ return "viewer"
561
+
562
+
563
+ def _scopes_from_claims(claims: dict) -> tuple[list[str], str]:
564
+ """Map OIDC token claims to Loki scopes via the existing ROLES mapping.
565
+
566
+ Returns a tuple of (scopes, role_name). If no recognized role claim is
567
+ present, the safe default role (viewer, or LOKI_OIDC_DEFAULT_ROLE) is
568
+ applied. This function NEVER returns ["*"]/admin by default: full access
569
+ is granted only when an explicit admin role claim is present.
570
+ """
571
+ candidate_values = _collect_role_claims(claims)
572
+
573
+ matched_role = None
574
+ for role in _ROLE_PRECEDENCE:
575
+ if role in candidate_values:
576
+ matched_role = role
577
+ break
578
+
579
+ if matched_role is None:
580
+ matched_role = _default_oidc_role()
581
+
582
+ return resolve_scopes(matched_role), matched_role
583
+
584
+
480
585
  def validate_oidc_token(token_str: str) -> Optional[dict]:
481
586
  """Validate an OIDC JWT token.
482
587
 
@@ -489,6 +594,12 @@ def validate_oidc_token(token_str: str) -> Optional[dict]:
489
594
  - Audience matches OIDC_AUDIENCE or OIDC_CLIENT_ID
490
595
  - Token is not expired
491
596
 
597
+ On success, role/group claims are mapped to Loki roles (admin/operator/
598
+ viewer/auditor) via _scopes_from_claims. When no recognized role claim is
599
+ present, the least-privileged default role (viewer, configurable via
600
+ LOKI_OIDC_DEFAULT_ROLE) is applied. OIDC users are never granted ["*"]
601
+ unless an explicit admin role claim is present.
602
+
492
603
  SECURITY CRITICAL: Without PyJWT, JWT signatures are NOT cryptographically
493
604
  verified. An attacker can forge tokens with arbitrary claims. For any
494
605
  production deployment, you MUST install PyJWT + cryptography so that
@@ -529,11 +640,13 @@ def validate_oidc_token(token_str: str) -> Optional[dict]:
529
640
  issuer=OIDC_ISSUER,
530
641
  )
531
642
 
643
+ scopes, role = _scopes_from_claims(decoded)
532
644
  return {
533
645
  "id": decoded.get("sub", ""),
534
646
  "name": decoded.get("name", decoded.get("email", decoded.get("sub", ""))),
535
647
  "email": decoded.get("email", ""),
536
- "scopes": ["*"], # OIDC users get full access
648
+ "scopes": scopes, # mapped from OIDC role/group claims
649
+ "role": role,
537
650
  "auth_method": "oidc",
538
651
  "issuer": decoded.get("iss"),
539
652
  }
@@ -602,11 +715,13 @@ def validate_oidc_token(token_str: str) -> Optional[dict]:
602
715
  return None
603
716
 
604
717
  # Return user info from claims
718
+ scopes, role = _scopes_from_claims(claims)
605
719
  return {
606
720
  "id": claims.get("sub", ""),
607
721
  "name": claims.get("name", claims.get("email", claims.get("sub", ""))),
608
722
  "email": claims.get("email", ""),
609
- "scopes": ["*"], # OIDC users get full access
723
+ "scopes": scopes, # mapped from OIDC role/group claims
724
+ "role": role,
610
725
  "auth_method": "oidc",
611
726
  "issuer": claims.get("iss"),
612
727
  }
@@ -1,6 +1,13 @@
1
1
  """Anonymous usage telemetry for Loki Mode dashboard.
2
2
 
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
4
+ explicitly opts in, so a default install (including air-gapped, GDPR, and
5
+ FedRAMP deployments) never phones home.
6
+
7
+ Opt-in (one required): LOKI_TELEMETRY=on OR ~/.loki/config: TELEMETRY_ENABLED=true
8
+ Opt-out (always wins): LOKI_TELEMETRY=off / LOKI_TELEMETRY_DISABLED=true /
9
+ DO_NOT_TRACK=1 / ~/.loki/config: TELEMETRY_DISABLED=true
10
+
4
11
  All calls are fire-and-forget, silent on failure, non-blocking.
5
12
  """
6
13
 
@@ -19,10 +26,20 @@ _POSTHOG_KEY = "phc_ya0vGBru41AJWtGNfZZ8H9W4yjoZy4KON0nnayS7s87"
19
26
 
20
27
 
21
28
  def _is_enabled():
22
- # Unified opt-out: these checks must mirror loki_collection_enabled in
23
- # autonomy/crash.sh so one switch gates BOTH PostHog usage telemetry and
24
- # crash reporting.
25
- if os.environ.get("LOKI_TELEMETRY", "").lower() == "off":
29
+ # Unified OPT-IN gate. Collection is OFF by default; enabled ONLY when the
30
+ # user has opted in AND has not also opted out. This precedence MUST mirror
31
+ # loki_collection_enabled in autonomy/crash.sh and _loki_telemetry_enabled
32
+ # in autonomy/telemetry.sh so one model gates BOTH PostHog usage telemetry
33
+ # and crash reporting.
34
+ #
35
+ # Precedence:
36
+ # 1. Any opt-out flag present -> False (hard kill, always wins)
37
+ # 2. Else any opt-in flag present -> True
38
+ # 3. Else (default) -> False (no egress)
39
+ telem = os.environ.get("LOKI_TELEMETRY", "").lower()
40
+
41
+ # --- 1. Opt-out always wins ---
42
+ if telem == "off":
26
43
  return False
27
44
  if os.environ.get("LOKI_TELEMETRY_DISABLED") == "true":
28
45
  return False
@@ -30,15 +47,26 @@ def _is_enabled():
30
47
  return False
31
48
  # Persistent opt-out in ~/.loki/config (matches the bash grep prefix
32
49
  # semantics: any line beginning with TELEMETRY_DISABLED=true).
50
+ config_enabled = False
33
51
  try:
34
52
  config_path = Path.home() / ".loki" / "config"
35
53
  if config_path.is_file():
36
54
  for line in config_path.read_text().splitlines():
37
55
  if line.startswith("TELEMETRY_DISABLED=true"):
38
56
  return False
57
+ if line.startswith("TELEMETRY_ENABLED=true"):
58
+ config_enabled = True
39
59
  except Exception:
40
60
  pass
41
- return True
61
+
62
+ # --- 2. Opt-in required to enable ---
63
+ if telem == "on":
64
+ return True
65
+ if config_enabled:
66
+ return True
67
+
68
+ # --- 3. Default: OFF ---
69
+ return False
42
70
 
43
71
 
44
72
  def _get_distinct_id():
@@ -336,7 +336,7 @@ Based on research synthesis, the following improvements are planned:
336
336
 
337
337
  This acknowledgements file documents the research and resources that influenced Loki Mode's design. All referenced works retain their original licenses and copyrights.
338
338
 
339
- Loki Mode itself is released under the MIT License.
339
+ Loki Mode itself is released under the Business Source License 1.1 (BUSL-1.1), a source-available license.
340
340
 
341
341
  ---
342
342
 
@@ -45,7 +45,7 @@ GSD is the closest competitor -- a context engineering system that spawns fresh
45
45
  | **Enterprise Security** | `--dangerously-skip-permissions` | MCP sandboxed | Sandboxed | Audit logs, RBAC | Staged autonomy | Sandboxed |
46
46
  | **Cross-Project Learning** | No | AgentDB | No | No | No | Limited |
47
47
  | **Observability** | Dashboard + STATUS.txt | Real-time tracing | Logs | Full tracing | Built-in | Full |
48
- | **Pricing** | Free (OSS) | Free (OSS) | Free (OSS) | $25+/mo | $20-400/mo | $20-500/mo |
48
+ | **Pricing** | Free (source-available) | Free (OSS) | Free (OSS) | $25+/mo | $20-400/mo | $20-500/mo |
49
49
  | **Production Ready** | Experimental | Production | Production | Production | Production | Production |
50
50
  | **Resource Monitoring** | Yes (v2.18.5) | Unknown | No | No | No | No |
51
51
  | **State Recovery** | Yes (checkpoints) | Yes (AgentDB) | Limited | Yes | Git worktrees | Yes |
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v7.46.0
5
+ **Version:** v7.48.0
6
6
 
7
7
  ---
8
8
 
@@ -114,11 +114,18 @@ faster routed commands and forward-compat with v8.0.0.
114
114
  - Installs the `loki` CLI binary to your PATH (`bin/loki` shim)
115
115
  - Subsequent `loki setup-skill` creates symlinks at `~/.claude/skills/loki-mode`, `~/.codex/skills/loki-mode`
116
116
 
117
- **Opt out of anonymous install telemetry:**
117
+ **Anonymous telemetry is OPT-IN and OFF by default.** A default `npm install`
118
+ sends nothing, so air-gapped and enterprise installs are safe out of the box. To
119
+ opt in to anonymous diagnostics, run `loki telemetry on` or set
120
+ `LOKI_TELEMETRY=on`. To make opting in impossible across a fleet, bake an
121
+ opt-out into your base image (opt-out always wins):
118
122
  ```bash
123
+ # Hard-disable everywhere (belt and suspenders; opt-out always wins):
119
124
  LOKI_TELEMETRY_DISABLED=true npm install -g loki-mode
120
125
  # Or set DO_NOT_TRACK=1
121
126
  ```
127
+ See [PRIVACY.md](./PRIVACY.md) for the exact data sent and the full opt-in /
128
+ opt-out model.
122
129
 
123
130
  **Update:** `npm update -g loki-mode`
124
131
 
@@ -389,7 +396,7 @@ provider works inside the container. Provide auth with your Anthropic API key:
389
396
  # Run Loki Mode in Docker (Claude provider, API-key auth)
390
397
  docker run --rm -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
391
398
  -v $(pwd):/workspace -w /workspace \
392
- asklokesh/loki-mode:7.46.0 start ./my-spec.md
399
+ asklokesh/loki-mode:7.48.0 start ./my-spec.md
393
400
  ```
394
401
 
395
402
  ##### docker compose + .env (no host install)
@@ -1,15 +1,16 @@
1
1
  # Loki Mode open-core boundary
2
2
 
3
- Loki Mode is and stays open source. This document draws the line between what is
4
- free forever and what hosted/paid/enterprise plans would add on top. R9 ships
3
+ Loki Mode is and stays source-available (BUSL-1.1) and free to self-host. This
4
+ document draws the line between what is free forever and what
5
+ hosted/paid/enterprise plans would add on top. R9 ships
5
6
  the SEAMS for that line; it does not ship a hosted backend, a license server, or
6
7
  any paywall on existing functionality.
7
8
 
8
9
  ## Principle
9
10
 
10
- OSS is fully functional with zero hosted backend. Every capability Loki has
11
- today runs locally, free, with no account, no license key, and no network call
12
- to any Loki service. Hosted/paid features are ADDITIVE convenience and
11
+ The free self-hosted tier is fully functional with zero hosted backend. Every
12
+ capability Loki has today runs locally, free, with no account, no license key,
13
+ and no network call to any Loki service. Hosted/paid features are ADDITIVE convenience and
13
14
  team/enterprise layers, never a removal or gating of something that is free
14
15
  today.
15
16
 
@@ -0,0 +1,192 @@
1
+ # P2 Spec Robustness Plan (P2-1 spec interrogation gate + P2-2 assumption ledger)
2
+
3
+ Status: design for implementation. No version bump, no commit in this arc.
4
+
5
+ ## Goal
6
+
7
+ Loki must stay accurate even when the input spec is WRONG, ambiguous, or
8
+ incomplete. Today two building blocks already detect spec defects but neither
9
+ feeds the autonomous loop:
10
+
11
+ - `autonomy/grill.sh` invokes the provider once with a Devil's-Advocate prompt
12
+ and writes 10-15 hardest spec questions to `.loki/grill/report.md`. It is
13
+ CLI-only (`grep grill autonomy/run.sh` = 0 invocations) and nothing reads its
14
+ output.
15
+ - `autonomy/prd-analyzer.py` detects missing PRD dimensions and has a
16
+ deterministic `_make_assumption()` map, writing `.loki/prd-observations.md`,
17
+ which nothing reads. Its interactive Q&A is inert in non-TTY (autonomous) runs.
18
+
19
+ The fix: run interrogation automatically in DISCOVERY, classify the findings,
20
+ record every spec gap as a first-class ASSUMPTION in a tracked ledger, BLOCK
21
+ completion while high-severity assumptions are unconfirmed-and-unacknowledged,
22
+ and surface the ledger in the proof-of-done output. Defects are SURFACED as
23
+ recorded assumptions, never silently autocorrected.
24
+
25
+ ## Core design decision: auto-acknowledgment lifecycle (prevents the trap)
26
+
27
+ A naive "block completion while any high-severity assumption is unconfirmed"
28
+ hard-blocks EVERY ambiguous run to max-iterations, because in autonomous
29
+ (non-TTY) mode no human can ever set `confirmed=yes`. We never reach the
30
+ "done, plus here is what I assumed" output the goal demands.
31
+
32
+ Resolution: split the gate from the lifecycle.
33
+
34
+ - The gate `council_assumption_ledger_gate` is a PURE function of ledger state.
35
+ It blocks iff an entry has `severity=high AND confirmed=false AND
36
+ acknowledged=false`.
37
+ - The auto-acknowledgment lifecycle lives in run.sh (NOT in the gate). Once an
38
+ assumption has been written into the ledger AND injected into the build prompt
39
+ at least once, run.sh marks it `acknowledged=true`. Default-on; opt-out
40
+ `LOKI_ASSUMPTIONS_REQUIRE_CONFIRM=1` keeps a human-in-the-loop path where only
41
+ `confirmed=true` clears the block.
42
+
43
+ This is the OPPOSITE of silent autocorrect: the assumption is recorded,
44
+ injected into the agent's prompt, and surfaced in proof-of-done. Acknowledgment
45
+ records "Loki has SEEN this gap and proceeded with a stated default", not "Loki
46
+ hid it". The gate still has teeth on the first iteration (high-sev unacknowledged
47
+ blocks) and in the require-confirm path.
48
+
49
+ ## Severity rule (deterministic, no LLM)
50
+
51
+ grill emits no severity. Classify by section / keyword on the read side:
52
+
53
+ - HIGH: security blind spots; scale/reliability blind spots; missing or
54
+ untestable acceptance criteria; any line containing contradiction keywords
55
+ (contradict, conflict, inconsistent, mutually exclusive).
56
+ - MEDIUM: ambiguities; unstated assumptions; underspecified behavior; all
57
+ prd-analyzer missing-dimension assumptions.
58
+
59
+ This guarantees a HIGH tier exists (so the gate has teeth) and is fully
60
+ deterministic (so tests are reproducible).
61
+
62
+ ## Taxonomy mapping (classification, read-side only)
63
+
64
+ grill section -> finding class:
65
+ - "Ambiguities and missing acceptance criteria" -> ambiguous (HIGH if the line
66
+ references acceptance criteria / testable / measurable; else MEDIUM)
67
+ - "Unstated assumptions" -> underspecified (MEDIUM)
68
+ - "Security blind spots" -> missing (HIGH)
69
+ - "Scale and reliability blind spots" -> missing (HIGH)
70
+ - any line with a contradiction keyword (any section) -> contradictory (HIGH)
71
+ - prd-analyzer missing dimensions -> missing (MEDIUM, deterministic default)
72
+
73
+ "None identified." lines are skipped (no fabricated findings).
74
+
75
+ grill output contract is NOT changed (it is parsed by the loki-grill skill).
76
+ We classify a COPY of its markdown; grill.sh stays byte-identical.
77
+
78
+ ## No-fabrication rule for ledger content
79
+
80
+ A grill finding is a QUESTION, not a resolution. The ledger `assumption` field
81
+ for a grill-derived gap is an honest "spec gives no answer; proceeding with the
82
+ implementer default for <area>" plus `affects=<area>`. We do NOT invent a
83
+ specific resolution the build will not actually follow. prd-analyzer assumptions
84
+ reuse its existing deterministic `_make_assumption()` text verbatim.
85
+
86
+ ## Ledger schema (`.loki/assumptions/`)
87
+
88
+ One JSON file per assumption: `.loki/assumptions/<id>.json`, plus a
89
+ human-readable `.loki/assumptions/ledger.md` rollup regenerated on each write.
90
+ Each entry:
91
+
92
+ ```json
93
+ {
94
+ "id": "a-0001",
95
+ "gap": "<the spec defect / unanswered question, verbatim>",
96
+ "assumption": "<honest stated default Loki proceeds with>",
97
+ "why": "<why this assumption / where the gap came from: grill|prd-analyzer>",
98
+ "severity": "high|medium",
99
+ "class": "ambiguous|contradictory|underspecified|missing",
100
+ "affects": "<area, e.g. security, acceptance-criteria, data-model>",
101
+ "source": "grill|prd-analyzer",
102
+ "confirmed": false,
103
+ "acknowledged": false,
104
+ "created_at": "<iso8601>"
105
+ }
106
+ ```
107
+
108
+ Stable id = `a-` + zero-padded counter over existing files (idempotent: a second
109
+ DISCOVERY run with the same findings does not duplicate; dedupe on the `gap`
110
+ text hash).
111
+
112
+ ## Build surface (files + functions)
113
+
114
+ 1. NEW `autonomy/spec-interrogation.sh` (sourced by run.sh; standalone-testable):
115
+ - `spec_interrogation_classify_report <report.md path>`: pure classifier.
116
+ Reads grill markdown, emits one TSV/JSON finding per question line with
117
+ class + severity. Takes a file so a fixture report drives the test with no
118
+ `claude` call.
119
+ - `spec_interrogation_severity_for <section> <line>`: deterministic severity.
120
+ - `spec_ledger_write <gap> <assumption> <why> <severity> <class> <affects>
121
+ <source>`: idempotent writer (dedupe on gap hash) -> `.loki/assumptions/`.
122
+ - `spec_ledger_rebuild_md`: regenerate `.loki/assumptions/ledger.md`.
123
+ - `spec_ledger_high_unresolved_count`: count entries with
124
+ `severity=high AND confirmed=false AND acknowledged=false` (gate input;
125
+ also reused by the summary).
126
+ - `spec_ledger_acknowledge_all`: set `acknowledged=true` on all entries
127
+ (auto-ack lifecycle helper; default path).
128
+ - `spec_interrogation_run <spec_path>`: orchestrator. Default-on
129
+ (`LOKI_SPEC_GRILL=0` opts out). Provider-aware: source grill.sh, call
130
+ `grill_check_provider`; if provider absent, log honest message, skip the
131
+ grill subcall (NO fabricated questions), but STILL fold prd-analyzer
132
+ missing-dimension assumptions into the ledger so degrade surfaces something
133
+ non-blocking. On provider present: run `grill_main` (writes report.md),
134
+ classify it, write ledger entries. Always non-fatal to the run.
135
+
136
+ 2. `autonomy/run.sh` DISCOVERY (~13056, after prd-analyzer + council_init,
137
+ before the iteration loop): source spec-interrogation.sh and call
138
+ `spec_interrogation_run "$prd_path"`. This is the grep-able grill invocation
139
+ the task requires. Best-effort (`|| true`), never blocks startup.
140
+
141
+ 3. `autonomy/run.sh` auto-ack lifecycle: after the build prompt is constructed
142
+ each iteration (assumptions are injected into the prompt via build_prompt),
143
+ call `spec_ledger_acknowledge_all` UNLESS
144
+ `LOKI_ASSUMPTIONS_REQUIRE_CONFIRM=1`. Inject the high-severity assumption
145
+ list into the build prompt (so the agent sees the gaps it must respect).
146
+
147
+ 4. `autonomy/completion-council.sh` `council_assumption_ledger_gate` (new),
148
+ slotted into `council_evaluate` right after `council_evidence_gate`
149
+ (mirrors 2510-2513). Same defensive `COUNCIL_STATE_DIR` default, opt-out
150
+ `LOKI_ASSUMPTION_GATE=0`. Blocks iff `spec_ledger_high_unresolved_count > 0`.
151
+ Writes `.loki/council/assumption-block.json` on block, removes it on pass.
152
+ Also wired into the completion-promise route in run.sh (~14525 pattern) and
153
+ the code_review gate chain (~15013) so the promise path cannot bypass it.
154
+
155
+ 5. `autonomy/run.sh` `build_completion_summary` (~2637): emit an
156
+ "Assumptions recorded: N (M high-severity)" block into COMPLETION.txt and the
157
+ ledger list, plus the count into completion.json. So "done" means "done, plus
158
+ here are the N places your spec was ambiguous and what I assumed."
159
+
160
+ 6. NEW `tests/test-spec-interrogation.sh` (bash convention, ok/bad counters,
161
+ source the module, mktemp fixtures):
162
+ - (a) classifier on a fixture grill report writes classified findings to the
163
+ ledger (ambiguous/contradictory/underspecified/missing + high/medium).
164
+ - (b) a ledger with one high/confirmed:false/acknowledged:false entry makes
165
+ `council_assumption_ledger_gate` return 1 (BLOCK) and write
166
+ assumption-block.json.
167
+ - (c) clean spec (no high-sev entries, or all acknowledged) -> gate returns 0
168
+ (no spurious block), no block file.
169
+ - (d) no provider -> `spec_interrogation_run` degrades cleanly: honest
170
+ message, prd-analyzer assumptions still folded (medium, non-blocking), run
171
+ proceeds, gate passes.
172
+
173
+ ## Gate reachability (resolved open question)
174
+
175
+ The existing gates fire from THREE sites: `council_evaluate` (~2510), the
176
+ completion-promise route (~14525), and the code_review gate chain (~15013). The
177
+ new gate is wired into all three so high-sev unacknowledged assumptions cannot
178
+ slip through the promise path.
179
+
180
+ ## Opt-out knobs (all default-on, intelligent)
181
+
182
+ - `LOKI_SPEC_GRILL=0` -> skip interrogation entirely.
183
+ - `LOKI_ASSUMPTION_GATE=0` -> gate is pass-through.
184
+ - `LOKI_ASSUMPTIONS_REQUIRE_CONFIRM=1` -> require human `confirmed=true`
185
+ (disables auto-ack); the human-in-the-loop path.
186
+
187
+ No "user must decide the type" knob. Classification + severity are automatic.
188
+
189
+ ## Constraints
190
+
191
+ No emojis, no em dashes, no version bump, no commit, no push. Provider-aware,
192
+ degrade cleanly, no fabricated questions when provider absent.
package/docs/PRIVACY.md CHANGED
@@ -6,14 +6,21 @@ match the code, the code is the bug; please open an issue.
6
6
 
7
7
  ## Summary
8
8
 
9
- - Loki Mode collects anonymous diagnostics to help find and fix bugs.
10
- - It NEVER collects your code, prompts, PRDs, file paths, environment values,
11
- API keys, repository names, emails, or IP addresses.
12
- - In this version (crash reporting Phase 0), NOTHING is sent automatically.
13
- Crash reports are written to a local directory only, so you can inspect
14
- exactly what a future version would send.
15
- - You can opt out at any time with a single switch. The same switch also
16
- disables the existing anonymous usage telemetry described below.
9
+ - Anonymous diagnostics are OPT-IN and OFF by default. A default install sends
10
+ no telemetry or diagnostics of any kind. This covers a default `npm install`,
11
+ the CLI (session and command events), the dashboard, and the welcome page
12
+ form. Air-gapped, GDPR, and FedRAMP deployments are safe out of the box: an
13
+ untouched install sends us no telemetry or diagnostics. (This statement scopes
14
+ to telemetry and diagnostics; provider CLIs you configure, such as Claude or
15
+ Codex, make their own network calls under your own credentials and are
16
+ governed by their vendors.)
17
+ - When you DO opt in, Loki Mode collects anonymous diagnostics to help find and
18
+ fix bugs. It NEVER collects your code, prompts, PRDs, file paths, environment
19
+ values, API keys, repository names, emails, or IP addresses.
20
+ - Crash reporting (Phase 0) is local-only with zero network egress regardless,
21
+ and is also gated by opt-in, so a default install writes nothing at all.
22
+ - You opt in with a single switch (`loki telemetry on` or `LOKI_TELEMETRY=on`)
23
+ and can opt back out at any time. Opt-out always wins over opt-in.
17
24
 
18
25
  ## Two collection paths exist
19
26
 
@@ -39,17 +46,35 @@ Phase 0 behavior:
39
46
  GitHub issue URL so you can submit it manually if you choose. Loki Mode does
40
47
  not submit anything for you in this version.
41
48
 
42
- ### 2. Usage telemetry (existing, anonymous)
49
+ ### 2. Usage telemetry (anonymous, opt-in)
43
50
 
44
- Loki Mode already ships anonymous usage telemetry via PostHog. This predates the
45
- crash-reporting feature and is disclosed here for completeness.
51
+ Loki Mode can send anonymous usage telemetry via PostHog, but ONLY after you opt
52
+ in. By default it is OFF and nothing is sent.
46
53
 
47
- - Events: `session_start`, `session_end`, and an install-time event.
48
- - These are anonymous and gated by the same opt-out described below.
49
- - They never carry your code, prompts, paths, keys, or repository names.
54
+ - Endpoint: `https://us.i.posthog.com/capture/` (override with
55
+ `LOKI_TELEMETRY_ENDPOINT`). The PostHog project key is a public ingest key.
56
+ - Events: `install` (on `npm install`), `session_start`, `session_end`,
57
+ `cli_command`, and `dashboard_start`.
58
+ - Exact payload (every event): `os` (uname system), `arch` (CPU arch),
59
+ `version` (Loki Mode version), `channel` (npm / docker / homebrew / skill /
60
+ source), and a random per-machine `distinct_id` (a uuid4 stored in
61
+ `~/.loki-telemetry-id`, never an email or name). The `install` event also adds
62
+ `node_version` and `providers_installed` (which provider CLIs were detected,
63
+ e.g. "claude,codex"). Some events add a small free-of-PII property such as the
64
+ command name. No code, prompts, paths, keys, repo names, emails, or IPs.
50
65
 
51
- This document and the first-run notice describe BOTH paths. The opt-out is
52
- unified: one switch disables crash reporting AND usage telemetry together.
66
+ ### 3. Welcome page form (anonymous, explicit submit, opt-in)
67
+
68
+ The `loki welcome` page (`assets/welcome/welcome.html`) shows an optional form.
69
+ It NEVER sends anything on page load and is rendered inert unless you have opted
70
+ in. If you have opted in AND you choose to fill in and submit the form, it sends
71
+ these additional self-reported fields to PostHog: your role, company size, and
72
+ the tools you use, plus the same anonymous `distinct_id`. It still never sends
73
+ your name, email, or IP. In headless / Docker / CI environments there is no
74
+ browser, so this path never runs.
75
+
76
+ This document and the first-run notice describe ALL paths. The model is unified:
77
+ one opt-in enables them and one opt-out (which always wins) disables them.
53
78
 
54
79
  ## What is collected (the whitelist)
55
80
 
@@ -90,17 +115,46 @@ prompts, briefs, and diffs can never reach the payload even if a redaction rule
90
115
  were to miss something. Secrets are additionally scrubbed by the shared redactor
91
116
  before whitelisting.
92
117
 
93
- ## How to opt out
118
+ ## How to opt in (and opt back out)
119
+
120
+ Collection is OFF by default. To turn it on, use ANY one of:
94
121
 
95
- Any one of the following disables BOTH crash reporting and usage telemetry:
122
+ - Run `loki telemetry on` (persists `TELEMETRY_ENABLED=true` to `~/.loki/config`)
123
+ - Set the environment variable `LOKI_TELEMETRY=on` (exact word `on`,
124
+ case-insensitive; values like `1` or `true` do NOT count as consent)
125
+
126
+ To opt back out at any time, use ANY one of the following. Opt-out always wins
127
+ over opt-in, so setting one of these guarantees nothing is collected or sent:
96
128
 
97
- - Set the environment variable `LOKI_TELEMETRY=off`
98
129
  - Run `loki telemetry off`
130
+ - Set `LOKI_TELEMETRY=off`
99
131
  - Set `DO_NOT_TRACK=1` (the cross-tool community convention)
100
132
  - Set `LOKI_TELEMETRY_DISABLED=true`
101
133
 
102
- To re-enable later, run `loki telemetry on` or unset the variables. Once you opt
103
- out, the first-run notice is never shown again.
134
+ ### Precedence (exact)
135
+
136
+ 1. If any opt-out flag is set, collection is OFF (hard kill, always wins).
137
+ 2. Else if any opt-in flag is set, collection is ON.
138
+ 3. Otherwise (the default), collection is OFF.
139
+
140
+ ### Air-gapped and enterprise deployments
141
+
142
+ Because collection is opt-in, a default install in an air-gapped, GDPR, or
143
+ FedRAMP environment sends us no telemetry or diagnostics: there is nothing to
144
+ turn off because there is nothing on. To make opting in impossible by accident
145
+ across a fleet, bake `LOKI_TELEMETRY_DISABLED=true` (or `DO_NOT_TRACK=1`) into
146
+ your base image or CI environment; opt-out always wins regardless of any later
147
+ opt-in.
148
+
149
+ This same gate covers ALL paths: the `npm install` event, CLI session and
150
+ command events, the dashboard event, the welcome form, and local crash capture.
151
+
152
+ ### OpenTelemetry (separate, self-hosted)
153
+
154
+ `loki telemetry enable [endpoint]` and `LOKI_OTEL_ENDPOINT` configure optional
155
+ OpenTelemetry tracing to an endpoint YOU run. There is no default endpoint, so
156
+ this never egresses to us; it is opt-in by definition and points only where you
157
+ tell it to.
104
158
 
105
159
  ## Where reports are stored locally
106
160
 
@@ -129,12 +183,16 @@ that choice plainly so you can decide whether to opt out.
129
183
 
130
184
  ## Compliance posture
131
185
 
186
+ - Opt-in by default: nothing is collected or sent unless the user explicitly
187
+ opts in. A default install (including air-gapped) sends us no telemetry or
188
+ diagnostics.
132
189
  - Anonymous by design: no PII is in the whitelist; emails and IP addresses are
133
- denied outright.
190
+ denied outright. The welcome form's role / company-size / tools fields are
191
+ self-reported and anonymous (no name, email, or IP).
134
192
  - Disclosed: this document plus a first-run notice describe collection before
135
193
  any egress occurs.
136
- - Opt-out is persistent and friction-free (see above) and applies to both
137
- collection paths.
194
+ - Opt-out is persistent, friction-free, and ALWAYS wins over opt-in. It applies
195
+ to every collection path.
138
196
  - The project id is non-reversible (one-way hash).
139
197
  - Deletion: you can delete local reports yourself by removing files under
140
198
  `.loki/crash/`.
@@ -3,8 +3,8 @@
3
3
  Status: SEAMS implemented (this worktree). NOT a live hosted backend.
4
4
 
5
5
  R9 in the competitive-stickiness arc is the open-core monetization layer: keep
6
- Loki fully open source and free, while adding the SEAMS where hosted, enterprise,
7
- and paid plans would attach later. R9 ships the seams only. There is no Loki
6
+ Loki fully source-available (BUSL-1.1) and free to self-host, while adding the
7
+ SEAMS where hosted, enterprise, and paid plans would attach later. R9 ships the seams only. There is no Loki
8
8
  hosted service, no license-verification backend, and no paid gate on any
9
9
  existing feature. Every honest stub is labeled as such.
10
10