loki-mode 11.5.1 → 11.7.1

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 (116) hide show
  1. package/README.md +19 -2
  2. package/SKILL.md +2 -2
  3. package/VERSION +1 -1
  4. package/autonomy/lib/backlog.py +11 -2
  5. package/autonomy/lib/chat.sh +57 -1
  6. package/autonomy/lib/loki_yaml.py +8 -2
  7. package/autonomy/lib/workspace.py +267 -11
  8. package/autonomy/loki +327 -222
  9. package/autonomy/notification-checker.py +1 -1
  10. package/autonomy/trigger-server.py +185 -2
  11. package/autonomy/verify.sh +4 -0
  12. package/completions/_loki +4 -0
  13. package/completions/loki.bash +20 -2
  14. package/dashboard/__init__.py +3 -10
  15. package/dashboard/control.py +5 -435
  16. package/docs/ARCHITECTURE.md +1 -2
  17. package/docs/CLI-CORE-VERBS.md +2 -6
  18. package/docs/CLI-REFERENCE.md +2 -1
  19. package/docs/COMPONENTS.md +0 -2
  20. package/docs/DEPLOY-PLAN.md +17 -8
  21. package/docs/EVALUATING.md +8 -0
  22. package/docs/INSTALLATION.md +1 -1
  23. package/docs/WORKSPACES.md +15 -2
  24. package/docs/authentication.md +19 -1
  25. package/docs/dev/architecture-reference.md +8 -0
  26. package/docs/dev/release-checklist.md +9 -8
  27. package/docs/enterprise/security.md +3 -234
  28. package/docs/sentry-intake.md +11 -0
  29. package/docs/v10/ARCHITECT-CUTS.md +304 -0
  30. package/docs/v10/CLI-MODERN.md +1 -1
  31. package/docs/v10/DECISIONS.md +5 -5
  32. package/docs/v10/ENGINE.md +735 -299
  33. package/docs/v10/GUARDS.md +16 -14
  34. package/docs/v10/GUIDE.md +5 -7
  35. package/docs/v10/PROGRESS.md +12 -1
  36. package/docs/v11/MARKET-SLICES.md +1 -5
  37. package/integrations/openclaw/README.md +20 -0
  38. package/integrations/vibe-kanban.md +12 -50
  39. package/loki-ts/dist/loki.js +1213 -1178
  40. package/mcp/__init__.py +1 -1
  41. package/mcp/_sdk_loader.py +1 -1
  42. package/package.json +2 -2
  43. package/packages/control-plane/dist/server.js +1090 -924
  44. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  45. package/references/quality-control.md +1 -116
  46. package/schemas/loki-yaml.schema.json +5 -2
  47. package/skills/00-index.md +2 -2
  48. package/skills/artifacts.md +0 -22
  49. package/skills/production.md +2 -38
  50. package/src/policies/engine.js +41 -2
  51. package/src/policies/types.js +49 -0
  52. package/web-app/dist/assets/{AdminPage-Cbu3pXL8.js → AdminPage-S3BYeapm.js} +1 -1
  53. package/web-app/dist/assets/{Avatar-DGqGGcGx.js → Avatar-C1rHwLgn.js} +1 -1
  54. package/web-app/dist/assets/{Badge-Bhlipt6v.js → Badge-CzlnN5aQ.js} +1 -1
  55. package/web-app/dist/assets/{Button-CpN2sO61.js → Button-B1w5AeGC.js} +1 -1
  56. package/web-app/dist/assets/{CockpitPage-BqeDJwdL.js → CockpitPage-Dyeqjfkc.js} +1 -1
  57. package/web-app/dist/assets/{ComparePage-DUieA2ba.js → ComparePage-C6y2GkGR.js} +1 -1
  58. package/web-app/dist/assets/{ErrorBoundary-DTWsYrOm.js → ErrorBoundary-Dg2fcGkR.js} +1 -1
  59. package/web-app/dist/assets/{EvidenceReceiptPanel-BU_B9X19.js → EvidenceReceiptPanel-CxbyjvTj.js} +1 -1
  60. package/web-app/dist/assets/{GitHubIssuesPanel-DEZoLOsP.js → GitHubIssuesPanel-Dpys1xDb.js} +1 -1
  61. package/web-app/dist/assets/{GitHubPRsPanel-BEsnrq7Y.js → GitHubPRsPanel-CRVjWoFl.js} +1 -1
  62. package/web-app/dist/assets/{HomePage-BoUVuq7h.js → HomePage-BKnv-oYw.js} +2 -2
  63. package/web-app/dist/assets/{LoginPage-CmOED7vb.js → LoginPage-BGksmFRd.js} +1 -1
  64. package/web-app/dist/assets/{MagicPage-DATZitrZ.js → MagicPage-OCvhp16M.js} +1 -1
  65. package/web-app/dist/assets/{MetricsPage-DGL1v0I-.js → MetricsPage-CJOJpXWC.js} +1 -1
  66. package/web-app/dist/assets/{NotFoundPage-C2WgOjZd.js → NotFoundPage-C04p3QvN.js} +1 -1
  67. package/web-app/dist/assets/{ProjectPage-BMi1tepj.js → ProjectPage-BRB8txsY.js} +3 -3
  68. package/web-app/dist/assets/{ProjectsPage-vc3y5YPh.js → ProjectsPage-CoUYeQEh.js} +1 -1
  69. package/web-app/dist/assets/{SettingsPage-kbq0Ftvi.js → SettingsPage-BkjmJGfM.js} +1 -1
  70. package/web-app/dist/assets/{ShowcasePage-Bn6Q8Kbg.js → ShowcasePage-B4vU9TjQ.js} +1 -1
  71. package/web-app/dist/assets/{SystemSettingsPage-Basmhs_b.js → SystemSettingsPage-Z2jiVsni.js} +1 -1
  72. package/web-app/dist/assets/{TeamsPage-BTqOc4TR.js → TeamsPage-Ulm8SISq.js} +1 -1
  73. package/web-app/dist/assets/{TemplatesPage-2P9002VL.js → TemplatesPage-C_7MM6Ey.js} +1 -1
  74. package/web-app/dist/assets/{TerminalOutput-CAoSPBrg.js → TerminalOutput-Cumd7Tgs.js} +1 -1
  75. package/web-app/dist/assets/{activity-BcWVWCqU.js → activity-CZAUkbiY.js} +1 -1
  76. package/web-app/dist/assets/{bell-CYRqqgZ6.js → bell-BOvSXpEr.js} +1 -1
  77. package/web-app/dist/assets/{bot-CU7gdn3g.js → bot-DOTZxBYj.js} +1 -1
  78. package/web-app/dist/assets/{check-DLlCpz9V.js → check-BxpE0B5u.js} +1 -1
  79. package/web-app/dist/assets/{chevron-left-CE9Cb30C.js → chevron-left-D6KPvBOA.js} +1 -1
  80. package/web-app/dist/assets/{circle-alert-uou4bjua.js → circle-alert-Bajq86oe.js} +1 -1
  81. package/web-app/dist/assets/{clock-C_dSJcDX.js → clock-CnlHAVJc.js} +1 -1
  82. package/web-app/dist/assets/{cloud-DZ22ITF6.js → cloud-DjaymjwM.js} +1 -1
  83. package/web-app/dist/assets/{code-xml-BrJS0hhS.js → code-xml-BADEw2Ld.js} +1 -1
  84. package/web-app/dist/assets/{database-D2HOmxtl.js → database-B4aKb5Cq.js} +1 -1
  85. package/web-app/dist/assets/{dollar-sign-CQ1vK_6q.js → dollar-sign-DrldXpja.js} +1 -1
  86. package/web-app/dist/assets/{file-code-corner-w2vpGm5d.js → file-code-corner-DVebDVR-.js} +1 -1
  87. package/web-app/dist/assets/{file-plus-DlF6wjkD.js → file-plus-BD-N_-Tl.js} +1 -1
  88. package/web-app/dist/assets/{globe-Dy3lBJg1.js → globe-C7ieInC-.js} +1 -1
  89. package/web-app/dist/assets/{hammer-C3xIiRXN.js → hammer-BSugKIji.js} +1 -1
  90. package/web-app/dist/assets/{index-4t-_IW2y.js → index-nLQgfvOn.js} +3 -3
  91. package/web-app/dist/assets/{layers-Y8Fs8qXI.js → layers-CHXxU2on.js} +1 -1
  92. package/web-app/dist/assets/{loader-circle-CxgN1v4m.js → loader-circle-BEEz0LbD.js} +1 -1
  93. package/web-app/dist/assets/{lock-DFFNbXDs.js → lock-CzQGfcnE.js} +1 -1
  94. package/web-app/dist/assets/{package-5e6qCTUg.js → package-CcHYQLGd.js} +1 -1
  95. package/web-app/dist/assets/{plus-C0Go_ytj.js → plus-DdzgFEXn.js} +1 -1
  96. package/web-app/dist/assets/{refresh-cw-D4i-NW3o.js → refresh-cw-7x0HbH9Y.js} +1 -1
  97. package/web-app/dist/assets/{rotate-ccw-YlKkiUW6.js → rotate-ccw-DBahX1Sp.js} +1 -1
  98. package/web-app/dist/assets/{scroll-text-4qXOHTap.js → scroll-text-BB8y8a9Z.js} +1 -1
  99. package/web-app/dist/assets/{server-BB6zXNp9.js → server-CfkbW2MH.js} +1 -1
  100. package/web-app/dist/assets/{shield-alert-D4tzjB1H.js → shield-alert-DUgD8rBF.js} +1 -1
  101. package/web-app/dist/assets/{thumbs-up-DZiRwdFj.js → thumbs-up-DGoyY6QC.js} +1 -1
  102. package/web-app/dist/assets/{trash-2-Dv_LkRIK.js → trash-2-B6xrSUMr.js} +1 -1
  103. package/web-app/dist/assets/{trending-down-BJYp9PQe.js → trending-down-B6RGKTOx.js} +1 -1
  104. package/web-app/dist/assets/{trending-up-BTcXLklK.js → trending-up-B44DCrt1.js} +1 -1
  105. package/web-app/dist/assets/{upload-fcHqwOxr.js → upload-4kWm33ZY.js} +1 -1
  106. package/web-app/dist/assets/{usePolling-BeJYw_4L.js → usePolling-zkUtRQG6.js} +1 -1
  107. package/web-app/dist/assets/{user-BcDwUCsO.js → user-04cEVIe2.js} +1 -1
  108. package/web-app/dist/index.html +1 -1
  109. package/dashboard/run.py +0 -63
  110. package/docs/openclaw-integration.md +0 -52
  111. package/docs/v10/HANDOFF-NEXT-WEEK.md +0 -40
  112. package/docs/v10/MORNING-BRIEF-1003.md +0 -25
  113. package/docs/v10/MORNING-BRIEF.md +0 -52
  114. package/docs/v10/TODO.md +0 -35
  115. package/mcp/resources.py +0 -351
  116. package/mcp/tools.py +0 -200
@@ -265,7 +265,7 @@ def check_quality_gate(trigger, loki_dir, iteration, notifications):
265
265
  defect as the budget key, in the same file.
266
266
 
267
267
  gate-failures.txt is the canonical failure source used by
268
- proof-generator.py:413 and loki-ts build_prompt_helpers.ts:250, so this
268
+ proof-generator.py:413 and loki-ts build_prompt.ts, so this
269
269
  reuses a proven contract rather than introducing a second one.
270
270
  """
271
271
  failures_file = Path(loki_dir) / "quality" / "gate-failures.txt"
@@ -26,6 +26,12 @@ GitHub, the bearer token authenticates a human operator -- so holding one must
26
26
  never grant the other. If no API token is configured the server still starts but
27
27
  every /jobs request is rejected with 503 and an audit log line.
28
28
 
29
+ Self-healing entry point: POST /sentry accepts a Sentry integration webhook
30
+ authenticated by Sentry's own HMAC (sentry-hook-signature) under a THIRD,
31
+ separate secret (LOKI_SENTRY_WEBHOOK_SECRET or SENTRY_CLIENT_SECRET). A valid
32
+ alert enqueues `loki start "<normalized brief>" --pr --detach`: the output is a
33
+ receipt-gated PR only, never a deploy. No secret means the route answers 503.
34
+
29
35
  Usage:
30
36
  python3 autonomy/trigger-server.py [--port PORT] [--secret SECRET] [--dry-run]
31
37
  [--workers N] [--queue-size N]
@@ -398,6 +404,21 @@ OUTCOME_FAILED = JOB_STATUS_FAILED # launch failed, or exited non-zero
398
404
  OUTCOME_UNKNOWN = JOB_STATUS_UNKNOWN # detached; exit code never observed
399
405
 
400
406
 
407
+ # Secrets this server holds that a spawned `loki start` must never inherit: a
408
+ # prompt-injected run could otherwise forge /sentry, /webhook or /jobs requests.
409
+ CHILD_ENV_DENYLIST = (
410
+ "GITHUB_WEBHOOK_SECRET",
411
+ "LOKI_API_TOKEN",
412
+ "LOKI_API_TOKEN_FILE",
413
+ "LOKI_SENTRY_WEBHOOK_SECRET",
414
+ "SENTRY_CLIENT_SECRET",
415
+ )
416
+
417
+
418
+ def _scoped_child_env():
419
+ return {k: v for k, v in os.environ.items() if k not in CHILD_ENV_DENYLIST}
420
+
421
+
401
422
  def run_loki_outcome(args, dry_run=False):
402
423
  """Run a loki command and report WHICH of the three outcomes occurred.
403
424
 
@@ -414,6 +435,7 @@ def run_loki_outcome(args, dry_run=False):
414
435
  cmd,
415
436
  stdout=subprocess.PIPE,
416
437
  stderr=subprocess.PIPE,
438
+ env=_scoped_child_env(),
417
439
  )
418
440
  except (FileNotFoundError, OSError) as e:
419
441
  logging.error("Failed to launch %s: %s", " ".join(cmd), e)
@@ -577,6 +599,104 @@ def handle_job_event(payload, dry_run=False):
577
599
  return summary, status
578
600
 
579
601
 
602
+ # Pseudo-event for a Sentry alert. Like JOB_EVENT it is NOT a GitHub event and is
603
+ # refused on /webhook: only /sentry, under its own HMAC secret, may enqueue it.
604
+ SENTRY_EVENT = "sentry_issue"
605
+ INTERNAL_EVENTS = (JOB_EVENT, SENTRY_EVENT)
606
+
607
+ # Sentry actions that mean "a new problem needs a fix". resolved/assigned/etc.
608
+ # are skipped so a state change never starts a build.
609
+ SENTRY_ACTIONS = ("triggered", "created")
610
+ SENTRY_BRIEF_MAX = 1500
611
+ ISSUE_PROVIDERS_SH = Path(__file__).resolve().parent / "issue-providers.sh"
612
+
613
+
614
+ def validate_sentry_signature(secret, body, signature_header):
615
+ """Validate Sentry's `sentry-hook-signature` (hex HMAC-SHA256, no prefix).
616
+
617
+ Fails closed: no secret or no header is never valid.
618
+ """
619
+ if not secret or not signature_header:
620
+ return False
621
+ expected = hmac.new(secret.encode("utf-8"), body, hashlib.sha256).hexdigest()
622
+ return constant_time_equals(expected, signature_header.strip().lower())
623
+
624
+
625
+ def normalize_sentry_payload(payload):
626
+ """Run the existing normalize_sentry_json over a Sentry webhook payload.
627
+
628
+ Reuses autonomy/issue-providers.sh (single owner of the Sentry shape) via a
629
+ scoped-env bash call. Returns the normalized dict or None.
630
+ """
631
+ data = payload.get("data")
632
+ if not isinstance(data, dict):
633
+ return None
634
+ raw = {k: data[k] for k in ("issue", "event") if isinstance(data.get(k), dict)}
635
+ if not raw:
636
+ return None
637
+ env = {"PATH": os.environ.get("PATH", "/usr/bin:/bin")}
638
+ ev = raw.get("event") or {}
639
+ env["_LOKI_SENTRY_URL"] = str(ev.get("web_url") or ev.get("url") or "")
640
+ try:
641
+ proc = subprocess.run(
642
+ ["bash", "-c", 'source "$1" && normalize_sentry_json', "_",
643
+ str(ISSUE_PROVIDERS_SH)],
644
+ input=json.dumps(raw).encode("utf-8"), capture_output=True,
645
+ env=env, timeout=20,
646
+ )
647
+ except (OSError, subprocess.SubprocessError):
648
+ return None
649
+ if proc.returncode != 0:
650
+ return None
651
+ try:
652
+ out = json.loads(proc.stdout.decode("utf-8", errors="replace").strip().splitlines()[-1])
653
+ except (ValueError, IndexError):
654
+ return None
655
+ return out if isinstance(out, dict) else None
656
+
657
+
658
+ def build_sentry_brief(norm):
659
+ """One-line, control-char-free brief that can never parse as a CLI flag."""
660
+ culprit = ""
661
+ for line in str(norm.get("body", "")).splitlines():
662
+ if line.startswith("- Culprit: "):
663
+ culprit = line[len("- Culprit: "):].strip()
664
+ break
665
+ parts = ["Fix Sentry issue %s: %s" % (norm.get("number", "?"), norm.get("title", ""))]
666
+ if culprit:
667
+ parts.append("(culprit %s)" % culprit)
668
+ if norm.get("url"):
669
+ parts.append("See %s." % norm["url"])
670
+ parts.append("Reproduce with a regression test, then fix the root cause.")
671
+ brief = CONTROL_CHARS_RE.sub(" ", " ".join(parts))
672
+ brief = " ".join(brief.split())
673
+ return brief.encode("utf-8")[:SENTRY_BRIEF_MAX].decode("utf-8", errors="ignore")
674
+
675
+
676
+ def handle_sentry_event(payload, dry_run=False):
677
+ """Handle a verified Sentry alert: loki start <brief> --pr --detach.
678
+
679
+ PR only. Never passes a deploy argument: a fix still has to earn a VERIFIED
680
+ receipt, and shipping it stays a separate, gated human step.
681
+ """
682
+ action = payload.get("action", "")
683
+ if action not in SENTRY_ACTIONS:
684
+ return None, "skipped (action=%s)" % action
685
+ norm = normalize_sentry_payload(payload)
686
+ if norm is None:
687
+ return None, "rejected (unparseable sentry payload)"
688
+ brief = build_sentry_brief(norm)
689
+ if not valid_spec(brief):
690
+ return None, "rejected (invalid brief)"
691
+ args = ["start", brief, "--pr", "--detach"]
692
+ summary = "sentry issue %s" % norm.get("number", "?")
693
+ success = run_loki_command(args, dry_run=dry_run)
694
+ status = "fired" if success else "error"
695
+ if success:
696
+ send_notification("Trigger fired: %s" % summary)
697
+ return summary, status
698
+
699
+
580
700
  # Map event name -> handler. Keeps do_POST routing declarative. JOB_EVENT rides
581
701
  # the same table (and therefore the same bounded queue and worker pool) but is
582
702
  # explicitly refused on the /webhook path.
@@ -585,6 +705,7 @@ EVENT_HANDLERS = {
585
705
  "pull_request": handle_pull_request_event,
586
706
  "workflow_run": handle_workflow_run_event,
587
707
  JOB_EVENT: handle_job_event,
708
+ SENTRY_EVENT: handle_sentry_event,
588
709
  }
589
710
 
590
711
 
@@ -875,6 +996,8 @@ class WebhookHandler(http.server.BaseHTTPRequestHandler):
875
996
  # Bearer token for the /jobs API. MUST be a different secret from `secret`
876
997
  # above: one authenticates GitHub, the other authenticates a human.
877
998
  api_token = ""
999
+ # Sentry integration client secret for /sentry. Distinct from both above.
1000
+ sentry_secret = ""
878
1001
  dispatcher = None
879
1002
  # Ed25519 key that attests receipts, and the retired public keys that keep
880
1003
  # pre-rotation receipts verifiable. Both default to unconfigured, which
@@ -1179,11 +1302,51 @@ class WebhookHandler(http.server.BaseHTTPRequestHandler):
1179
1302
  log_event(JOB_EVENT, "", job["spec"], "queued")
1180
1303
  self._send_json(202, {"id": job_id, "status": "queued"})
1181
1304
 
1305
+ def _handle_sentry(self):
1306
+ """POST /sentry -- Sentry alert, HMAC-verified, enqueues a fix run."""
1307
+ body = self._read_body()
1308
+ if body is None:
1309
+ return
1310
+ event_type = SENTRY_EVENT
1311
+ if not self.sentry_secret:
1312
+ logging.warning("Rejecting /sentry from %s: no Sentry secret configured",
1313
+ self.address_string())
1314
+ log_event(event_type, "", "", "rejected (no sentry secret configured)")
1315
+ self._send_json(503, {"error": "sentry webhook secret not configured"})
1316
+ return
1317
+ sig = self.headers.get("sentry-hook-signature", "")
1318
+ if not validate_sentry_signature(self.sentry_secret, body, sig):
1319
+ logging.warning("Invalid Sentry signature from %s", self.address_string())
1320
+ log_event(event_type, "", "", "rejected (invalid signature)")
1321
+ self._send_json(401, {"error": "invalid signature"})
1322
+ return
1323
+ try:
1324
+ payload = json.loads(body)
1325
+ except json.JSONDecodeError:
1326
+ self._send_json(400, {"error": "invalid JSON"})
1327
+ return
1328
+ if not isinstance(payload, dict):
1329
+ self._send_json(400, {"error": "payload must be a JSON object"})
1330
+ return
1331
+ action = payload.get("action", "")
1332
+ if self.dispatcher.seen_delivery(self.headers.get("Request-ID", "")):
1333
+ log_event(event_type, action, "", "duplicate")
1334
+ self._send_json(200, {"event": event_type, "status": "duplicate"})
1335
+ return
1336
+ if not self.dispatcher.submit(event_type, payload):
1337
+ log_event(event_type, action, "", "rejected (queue full)")
1338
+ self._send_json(503, {"error": "server busy, retry later"})
1339
+ return
1340
+ self._send_json(202, {"event": event_type, "action": action, "status": "queued"})
1341
+
1182
1342
  def do_POST(self):
1183
1343
  path = self.path.split("?", 1)[0]
1184
1344
  if path == "/jobs":
1185
1345
  self._handle_job_submit()
1186
1346
  return
1347
+ if path == "/sentry":
1348
+ self._handle_sentry()
1349
+ return
1187
1350
  if path != "/webhook":
1188
1351
  self._send_json(404, {"error": "not found"})
1189
1352
  return
@@ -1201,10 +1364,10 @@ class WebhookHandler(http.server.BaseHTTPRequestHandler):
1201
1364
  # never be reachable with the webhook HMAC. Without this, a holder of
1202
1365
  # the webhook secret could POST X-GitHub-Event: loki_job and submit an
1203
1366
  # arbitrary spec, defeating the separate-credential requirement.
1204
- if event_type == JOB_EVENT:
1367
+ if event_type in INTERNAL_EVENTS:
1205
1368
  logging.warning(
1206
1369
  "Rejecting %s on /webhook from %s: submits require the /jobs "
1207
- "API and its own bearer token",
1370
+ "API and its own bearer token (Sentry uses /sentry)",
1208
1371
  JOB_EVENT,
1209
1372
  self.address_string(),
1210
1373
  )
@@ -1350,6 +1513,20 @@ def main():
1350
1513
  )
1351
1514
  api_token = ""
1352
1515
 
1516
+ # Sentry secret: env only (never argv). Must differ from the other two
1517
+ # credentials, since each authenticates a different party.
1518
+ sentry_secret = (os.environ.get("LOKI_SENTRY_WEBHOOK_SECRET")
1519
+ or os.environ.get("SENTRY_CLIENT_SECRET") or "")
1520
+ if sentry_secret and (
1521
+ (secret and constant_time_equals(sentry_secret, secret))
1522
+ or (api_token and constant_time_equals(sentry_secret, api_token))
1523
+ ):
1524
+ logging.error(
1525
+ "The Sentry secret is identical to the webhook secret or API "
1526
+ "token. They MUST differ; disabling /sentry."
1527
+ )
1528
+ sentry_secret = ""
1529
+
1353
1530
  # Persist resolved values, but never write the secret to disk.
1354
1531
  config["port"] = port
1355
1532
  config["dry_run"] = dry_run
@@ -1363,6 +1540,7 @@ def main():
1363
1540
  WebhookHandler.dry_run = dry_run
1364
1541
  WebhookHandler.secret = secret
1365
1542
  WebhookHandler.api_token = api_token
1543
+ WebhookHandler.sentry_secret = sentry_secret
1366
1544
  WebhookHandler.dispatcher = dispatcher
1367
1545
 
1368
1546
  signing_key, signing_kid = load_signing_key(auto_generate=False)
@@ -1404,6 +1582,11 @@ def main():
1404
1582
  "503. Set LOKI_API_TOKEN or LOKI_API_TOKEN_FILE to enable remote "
1405
1583
  "submits."
1406
1584
  )
1585
+ if not sentry_secret:
1586
+ logging.warning(
1587
+ "No Sentry secret configured: POST /sentry is rejected with 503. "
1588
+ "Set LOKI_SENTRY_WEBHOOK_SECRET or SENTRY_CLIENT_SECRET."
1589
+ )
1407
1590
  if not secret:
1408
1591
  logging.warning(
1409
1592
  "No webhook secret configured: ALL webhooks will be rejected with "
@@ -2584,6 +2584,10 @@ OPTIONS:
2584
2584
  is exactly as without --hosted (fail-open, never a silent
2585
2585
  pass). Without --hosted, output is byte-identical to prior
2586
2586
  releases.
2587
+ --seal-composite <run-dir>
2588
+ Seal a workspace run into one signed composite receipt
2589
+ (composite.json). `loki verify <composite.json>` then
2590
+ re-checks it offline. Routed to the engine10 verifier.
2587
2591
  -h, --help Show this help.
2588
2592
 
2589
2593
  GATES (deterministic, reproducible by construction):
package/completions/_loki CHANGED
@@ -313,6 +313,10 @@ function _loki_start {
313
313
  _arguments \
314
314
  '--provider[AI provider]:provider name:(claude cline codex aider opencode)' \
315
315
  '--parallel[Parallel mode]' \
316
+ '--workspace[Run a loki.yaml workspace for the brief]:workspace name:' \
317
+ '--deploy[Deploy after a VERIFIED run via loki deploy --execute]' \
318
+ '--health-url[With --deploy: health probe URL]:url:' \
319
+ '--auto-rollback[With --deploy: roll back on a failed health probe]' \
316
320
  '--background[Background mode]' \
317
321
  '--bg[Background mode]' \
318
322
  '--simple[Force simple complexity]' \
@@ -27,7 +27,7 @@ _loki_completion() {
27
27
 
28
28
  # If the word starts with a dash, show flags
29
29
  if [[ "$cur" == -* ]]; then
30
- local flags="--provider --max-iterations --parallel --background --bg --simple --complex --github --no-dashboard --sandbox --skip-memory --spec --spec-first --yes --budget --attempts --model --isolation --max-cost --no-pr --explain-route --help"
30
+ local flags="--provider --max-iterations --parallel --background --bg --simple --complex --github --no-dashboard --sandbox --skip-memory --spec --spec-first --yes --budget --attempts --model --isolation --max-cost --no-pr --explain-route --workspace --deploy --health-url --auto-rollback --help"
31
31
  COMPREPLY=( $(compgen -W "${flags}" -- "$cur") )
32
32
  return 0
33
33
  fi
@@ -57,7 +57,11 @@ _loki_completion() {
57
57
  ;;
58
58
 
59
59
  memory)
60
- local memory_cmds="list show search stats export clear dedupe index timeline consolidate economics retrieve lessons learn forget episode pattern skill vectors help"
60
+ local memory_cmds="list show search stats export clear dedupe index timeline consolidate economics retrieve lessons learn forget episode pattern skill vectors brief help"
61
+ if [[ "$cur" == -* ]]; then
62
+ COMPREPLY=( $(compgen -W "--task --help" -- "$cur") )
63
+ return 0
64
+ fi
61
65
  COMPREPLY=( $(compgen -W "${memory_cmds}" -- "$cur") )
62
66
  ;;
63
67
 
@@ -136,6 +140,20 @@ _loki_completion() {
136
140
  fi
137
141
  ;;
138
142
 
143
+ deploy)
144
+ if [[ "$cur" == -* ]]; then
145
+ COMPREPLY=( $(compgen -W "--execute --health-url --auto-rollback --help" -- "$cur") )
146
+ return 0
147
+ fi
148
+ ;;
149
+
150
+ verify)
151
+ if [[ "$cur" == -* ]]; then
152
+ COMPREPLY=( $(compgen -W "--base --out --block-on --no-llm --json --explain --check-fresh --hosted --seal-composite --help" -- "$cur") )
153
+ return 0
154
+ fi
155
+ ;;
156
+
139
157
  chat)
140
158
  if [[ "$cur" == -* ]]; then
141
159
  COMPREPLY=( $(compgen -W "--attach --tui --help" -- "$cur") )
@@ -4,16 +4,9 @@ Loki Mode Dashboard Backend
4
4
  FastAPI-based dashboard for managing Loki Mode projects, tasks, and agents.
5
5
 
6
6
  Modules:
7
- control: Session control API (start/stop/pause/resume)
7
+ control: Session control helpers (status, atomic writes, origin check)
8
8
  """
9
9
 
10
- __version__ = "11.5.1"
10
+ __version__ = "11.7.1"
11
11
 
12
- # Expose the control app for easy import
13
- try:
14
- from .control import app as control_app
15
- except ImportError:
16
- # FastAPI not installed, control module not available
17
- control_app = None
18
-
19
- __all__ = ["control_app", "__version__"]
12
+ __all__ = ["__version__"]