loki-mode 11.6.0 → 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 (86) 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 +315 -21
  9. package/autonomy/trigger-server.py +185 -2
  10. package/autonomy/verify.sh +4 -0
  11. package/completions/_loki +4 -0
  12. package/completions/loki.bash +20 -2
  13. package/dashboard/__init__.py +1 -1
  14. package/docs/CLI-REFERENCE.md +2 -1
  15. package/docs/DEPLOY-PLAN.md +17 -8
  16. package/docs/EVALUATING.md +8 -0
  17. package/docs/INSTALLATION.md +1 -1
  18. package/docs/WORKSPACES.md +15 -2
  19. package/docs/dev/architecture-reference.md +8 -0
  20. package/docs/sentry-intake.md +11 -0
  21. package/docs/v10/CLI-MODERN.md +1 -1
  22. package/loki-ts/dist/loki.js +1009 -975
  23. package/mcp/__init__.py +1 -1
  24. package/package.json +2 -2
  25. package/packages/control-plane/dist/server.js +618 -469
  26. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  27. package/schemas/loki-yaml.schema.json +5 -2
  28. package/src/policies/engine.js +41 -2
  29. package/src/policies/types.js +49 -0
  30. package/web-app/dist/assets/{AdminPage-DhXdJNQk.js → AdminPage-S3BYeapm.js} +1 -1
  31. package/web-app/dist/assets/{Avatar-ixVj76fM.js → Avatar-C1rHwLgn.js} +1 -1
  32. package/web-app/dist/assets/{Badge-BGMSp22s.js → Badge-CzlnN5aQ.js} +1 -1
  33. package/web-app/dist/assets/{Button-pYJc32tG.js → Button-B1w5AeGC.js} +1 -1
  34. package/web-app/dist/assets/{CockpitPage-C1JBBouA.js → CockpitPage-Dyeqjfkc.js} +1 -1
  35. package/web-app/dist/assets/{ComparePage-qD0NV4RK.js → ComparePage-C6y2GkGR.js} +1 -1
  36. package/web-app/dist/assets/{ErrorBoundary-C-yxO58j.js → ErrorBoundary-Dg2fcGkR.js} +1 -1
  37. package/web-app/dist/assets/{EvidenceReceiptPanel-ByPzuE1S.js → EvidenceReceiptPanel-CxbyjvTj.js} +1 -1
  38. package/web-app/dist/assets/{GitHubIssuesPanel-BNv_2BEJ.js → GitHubIssuesPanel-Dpys1xDb.js} +1 -1
  39. package/web-app/dist/assets/{GitHubPRsPanel-CuLZQ9CS.js → GitHubPRsPanel-CRVjWoFl.js} +1 -1
  40. package/web-app/dist/assets/{HomePage-CS_FW8Je.js → HomePage-BKnv-oYw.js} +2 -2
  41. package/web-app/dist/assets/{LoginPage-DwiNf1Wm.js → LoginPage-BGksmFRd.js} +1 -1
  42. package/web-app/dist/assets/{MagicPage-CmQybLPZ.js → MagicPage-OCvhp16M.js} +1 -1
  43. package/web-app/dist/assets/{MetricsPage-D9ERdpv8.js → MetricsPage-CJOJpXWC.js} +1 -1
  44. package/web-app/dist/assets/{NotFoundPage-D9ccgdbF.js → NotFoundPage-C04p3QvN.js} +1 -1
  45. package/web-app/dist/assets/{ProjectPage-B9Jslmgv.js → ProjectPage-BRB8txsY.js} +3 -3
  46. package/web-app/dist/assets/{ProjectsPage-DnU99Go2.js → ProjectsPage-CoUYeQEh.js} +1 -1
  47. package/web-app/dist/assets/{SettingsPage-DcTyx6wo.js → SettingsPage-BkjmJGfM.js} +1 -1
  48. package/web-app/dist/assets/{ShowcasePage-jEY9ghSr.js → ShowcasePage-B4vU9TjQ.js} +1 -1
  49. package/web-app/dist/assets/{SystemSettingsPage-CSGltW-8.js → SystemSettingsPage-Z2jiVsni.js} +1 -1
  50. package/web-app/dist/assets/{TeamsPage-gUuJXb9l.js → TeamsPage-Ulm8SISq.js} +1 -1
  51. package/web-app/dist/assets/{TemplatesPage-37Zhe--v.js → TemplatesPage-C_7MM6Ey.js} +1 -1
  52. package/web-app/dist/assets/{TerminalOutput-B09hRwaR.js → TerminalOutput-Cumd7Tgs.js} +1 -1
  53. package/web-app/dist/assets/{activity-q_4MY9J0.js → activity-CZAUkbiY.js} +1 -1
  54. package/web-app/dist/assets/{bell-X8OC_PFP.js → bell-BOvSXpEr.js} +1 -1
  55. package/web-app/dist/assets/{bot-C-N1PUOD.js → bot-DOTZxBYj.js} +1 -1
  56. package/web-app/dist/assets/{check-B2c76Jmp.js → check-BxpE0B5u.js} +1 -1
  57. package/web-app/dist/assets/{chevron-left-BF76YLNw.js → chevron-left-D6KPvBOA.js} +1 -1
  58. package/web-app/dist/assets/{circle-alert-AeFKBD8h.js → circle-alert-Bajq86oe.js} +1 -1
  59. package/web-app/dist/assets/{clock-CiXRf-FH.js → clock-CnlHAVJc.js} +1 -1
  60. package/web-app/dist/assets/{cloud-DKQ8SRJ5.js → cloud-DjaymjwM.js} +1 -1
  61. package/web-app/dist/assets/{code-xml-DEb6jXWP.js → code-xml-BADEw2Ld.js} +1 -1
  62. package/web-app/dist/assets/{database-6W7e-E9c.js → database-B4aKb5Cq.js} +1 -1
  63. package/web-app/dist/assets/{dollar-sign-Ds2GOuyW.js → dollar-sign-DrldXpja.js} +1 -1
  64. package/web-app/dist/assets/{file-code-corner-CZU0jg52.js → file-code-corner-DVebDVR-.js} +1 -1
  65. package/web-app/dist/assets/{file-plus-DTVYnXCN.js → file-plus-BD-N_-Tl.js} +1 -1
  66. package/web-app/dist/assets/{globe-0mUzKYDe.js → globe-C7ieInC-.js} +1 -1
  67. package/web-app/dist/assets/{hammer-BMHZg3YX.js → hammer-BSugKIji.js} +1 -1
  68. package/web-app/dist/assets/{index-BvOTvjax.js → index-nLQgfvOn.js} +3 -3
  69. package/web-app/dist/assets/{layers-Df7TdcWB.js → layers-CHXxU2on.js} +1 -1
  70. package/web-app/dist/assets/{loader-circle-kPsVfrSY.js → loader-circle-BEEz0LbD.js} +1 -1
  71. package/web-app/dist/assets/{lock-CXYcbEc5.js → lock-CzQGfcnE.js} +1 -1
  72. package/web-app/dist/assets/{package-Cqhua1ID.js → package-CcHYQLGd.js} +1 -1
  73. package/web-app/dist/assets/{plus-lseXhsn-.js → plus-DdzgFEXn.js} +1 -1
  74. package/web-app/dist/assets/{refresh-cw-OZriI6gD.js → refresh-cw-7x0HbH9Y.js} +1 -1
  75. package/web-app/dist/assets/{rotate-ccw-CKCfjPg8.js → rotate-ccw-DBahX1Sp.js} +1 -1
  76. package/web-app/dist/assets/{scroll-text-DoVyyfKb.js → scroll-text-BB8y8a9Z.js} +1 -1
  77. package/web-app/dist/assets/{server-DGy7TflW.js → server-CfkbW2MH.js} +1 -1
  78. package/web-app/dist/assets/{shield-alert-BCzRcMXi.js → shield-alert-DUgD8rBF.js} +1 -1
  79. package/web-app/dist/assets/{thumbs-up-C_znJd5g.js → thumbs-up-DGoyY6QC.js} +1 -1
  80. package/web-app/dist/assets/{trash-2-BosCbeeA.js → trash-2-B6xrSUMr.js} +1 -1
  81. package/web-app/dist/assets/{trending-down-CkR5zDkV.js → trending-down-B6RGKTOx.js} +1 -1
  82. package/web-app/dist/assets/{trending-up-DpUIfOH4.js → trending-up-B44DCrt1.js} +1 -1
  83. package/web-app/dist/assets/{upload-CH9aKc8L.js → upload-4kWm33ZY.js} +1 -1
  84. package/web-app/dist/assets/{usePolling-pzxrHJeg.js → usePolling-zkUtRQG6.js} +1 -1
  85. package/web-app/dist/assets/{user-B7OHa-VQ.js → user-04cEVIe2.js} +1 -1
  86. package/web-app/dist/index.html +1 -1
@@ -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") )
@@ -7,6 +7,6 @@ Modules:
7
7
  control: Session control helpers (status, atomic writes, origin check)
8
8
  """
9
9
 
10
- __version__ = "11.6.0"
10
+ __version__ = "11.7.1"
11
11
 
12
12
  __all__ = ["__version__"]
@@ -1,6 +1,6 @@
1
1
  # CLI reference
2
2
 
3
- Generated from `loki-ts/src/cli/registry.ts` by `scripts/generate-stale-zero.sh` for v11.6.0. Do not edit by hand.
3
+ Generated from `loki-ts/src/cli/registry.ts` by `scripts/generate-stale-zero.sh` for v11.7.1. Do not edit by hand.
4
4
  Hidden and legacy-only commands are omitted; see `docs/v10/CLI-MODERN.md` for the full inventory.
5
5
 
6
6
  | Command | Description |
@@ -19,6 +19,7 @@ Hidden and legacy-only commands are omitted; see `docs/v10/CLI-MODERN.md` for th
19
19
  | `loki memory index` | Show or rebuild the memory index |
20
20
  | `loki memory learn` | Learn lessons from a merged PR's review comments |
21
21
  | `loki memory lessons` | List PR review lessons with uses and outcomes |
22
+ | `loki memory brief` | Print the lessons and learnings block an engine10 run injects |
22
23
  | `loki memory show` | Show one learning |
23
24
  | `loki memory search` | Search learnings |
24
25
  | `loki memory stats` | Memory statistics |
@@ -1,17 +1,26 @@
1
- # DEPLOY-PLAN.md -- `loki deploy` (ADVISORY / PRINT-ONLY)
1
+ # DEPLOY-PLAN.md -- `loki deploy` (original design: advisory print-only; `--execute` now exists)
2
2
 
3
- **Feature:** `loki deploy` -- detect project type + the user's installed cloud CLI,
4
- PRINT (and best-effort copy) the canonical deploy command(s), and **never run them**.
3
+ **Current behavior (v11.7.0).** `loki deploy` still prints the deploy command by default and
4
+ runs nothing. Loki deploys only behind `--execute` plus a VERIFIED, HEAD-anchored receipt, a
5
+ clean tree, a destructive-op refusal and a policy permission (`.loki/policies.json`
6
+ `autonomy: operate`, or an explicit `actions.deploy` allow). With `--health-url URL` it probes the
7
+ deployed URL after the command. A non-2xx result is recorded as FAILED_HEALTH and exits non-zero;
8
+ with `--auto-rollback` it also runs the provider's documented rollback verb (`vercel rollback`,
9
+ `fly releases rollback <captured version>`; Netlify and Cloudflare print a manual note). With no
10
+ URL, health is recorded as "unchecked", never healthy. The deploy record lives in `.loki/deploys`
11
+ and is not part of the signed receipt. `loki start --deploy [--health-url U] [--auto-rollback]`
12
+ chains the same gated command after a start. Not built: canary or staged rollout, and receipt-bound deploy records.
13
+
14
+ Everything below is the original print-only design plan and is kept for history.
5
15
 
6
16
  **Status:** Architecture plan. No implementation code here. Pattern family: the just-shipped
7
17
  `loki preview --public` (`_preview_public` at `autonomy/loki:5306`, helpers from `_read_app_state`
8
18
  at `:5216`, arg parsing in `cmd_preview` at `:5546`-`:5609`, dispatch arm `preview)` at `:14966`).
9
19
 
10
- **Why this exists / why it is safe.** README states (line 447) *"Does not deploy -- human runs
11
- deploy commands"* and (line 452) *"It does NOT access your cloud accounts ... Human oversight is
12
- expected for deployment."* This feature keeps that literally true: it is an **advisory printer**.
13
- It runs `command -v <cli>` ONLY (never the CLI itself, not even `--version`), prints the exact
14
- command, and the **human runs it**. Fully reversible; touches no cloud account.
20
+ **Why the original design was print-only.** The README then stated that deploy commands were run by
21
+ a human and that Loki did not access cloud accounts. The first version was an **advisory printer**:
22
+ it ran `command -v <cli>` ONLY (never the CLI itself, not even `--version`), printed the exact
23
+ command, and the **human ran it**. Fully reversible; touched no cloud account.
15
24
 
16
25
  ---
17
26
 
@@ -211,6 +211,12 @@ Not measured: human effort, recovery success, review quality. Output: `--out-dir
211
211
 
212
212
  ---
213
213
 
214
+ ## 9. Factory-vs-Loki task set (reproducible, partly unmeasured)
215
+
216
+ `benchmarks/attribution/tasks/coord-1-api-web/` (multi-repo coordination) and `benchmarks/attribution/tasks/longspec-1-three-units/` (long spec, three units) ship with held-out acceptance kept outside the workspace. `benchmarks/bench/attribution.py` has three arms: `raw_claude`, `loki` and an optional `droid` arm. The droid arm probes `droid exec --help`; when `droid exec` is unavailable every droid cell is recorded UNMEASURED with a reason, never zero. `python3 benchmarks/bench/attribution.py --dry-run` lists the tasks and arms and makes no model call. No Factory result is published: Factory publishes no Missions benchmark and no droid run has been measured here. Axis where Loki is ahead on documented features: offline-recheckable signed receipts, now composite receipts for multi-repo change sets; Factory documents none.
217
+
218
+ ---
219
+
214
220
  ## What we do not have
215
221
 
216
222
  Stating this plainly, because you will find it out anyway and it is cheaper for
@@ -228,6 +234,8 @@ both of us if you find it here.
228
234
  Replit Agent ship no local CLI and are not covered at all. Treat this as
229
235
  "unclaimed on every surface we can reach", which is stronger than the earlier
230
236
  "unclaimed as far as we can verify" and still short of a proven first.
237
+ - **No scale measurement for multi-repo runs.** No test covers dozens of repos, so none is claimed.
238
+ - **No Factory comparison run.** The droid arm is UNMEASURED until a run exists (section 9).
231
239
  - **Generation is not air-gapped.** See section 2.
232
240
 
233
241
  ---
@@ -2,7 +2,7 @@
2
2
 
3
3
  Loki Mode, by [Autonomi](https://www.autonomi.dev/), turns a spec or an issue into a verified pull request with a receipt you can check. This page covers installing it, a first run, and the options around it.
4
4
 
5
- **Version:** v11.6.0
5
+ **Version:** v11.7.1
6
6
 
7
7
  **Engine note:** `loki start` is the primary command. `loki start`, `loki "<task>"`, `loki owner/repo#N` and `loki quick` all run the Loki 10 engine, whatever the flag order or input form (PRD file, issue ref, task or one-word brief). The previous loop runs only on `--engine legacy`, `LOKI_ENGINE=legacy`, or for one call when a flag Loki 10 cannot honor is present. See the [user guide](v10/GUIDE.md) and the [5-minute quickstart](QUICKSTART.md).
8
8
 
@@ -10,9 +10,13 @@ workspaces:
10
10
  repos:
11
11
  - {repo: acme/api, path: ~/src/api, setup: "npm ci"}
12
12
  - {repo: acme/web, path: ~/src/web, after: [acme/api]}
13
- integration: {command: "make e2e", timeout_s: 900}
13
+ integration: {command: "make e2e", timeout_s: 900, per_stage: false}
14
+ concurrency: 2
15
+ plan: false
14
16
  ```
15
17
 
18
+ The `workspaces:` block is validated and fails closed: a misspelled key (for example `afer`) or a wrong type exits 2 and names the key.
19
+
16
20
  Repos with no pending `after` run in parallel, at most `concurrency` at a time (default 2; set `LOKI_WORKSPACE_CONCURRENCY` to change it).
17
21
 
18
22
  Then:
@@ -21,6 +25,8 @@ Then:
21
25
  loki workspace list
22
26
  loki workspace show shop
23
27
  loki workspace run shop acme/api#12
28
+ loki start --workspace shop "<brief>" # same runner, from the primary command
29
+ loki workspace infer shop # print suggested after: edges
24
30
  loki workspace status # every recorded run, one row per repo
25
31
  loki workspace status <run-id>
26
32
  ```
@@ -30,7 +36,10 @@ loki workspace status <run-id>
30
36
  - A failing repo does not stop the others (continue and report). The command exits 0 only when every repo succeeded and the integration step did not fail. It exits 3 when the only non-ok outcomes are budget stops (a child exited 3, or a dependent was skipped behind one), and 1 for any other failure. Ctrl-C (SIGINT) stops the recorded child process groups like SIGTERM.
31
37
  - The repo that owns the issue ref gets the ref; other repos get a task text naming the ref and the sibling worktree paths as read-only context.
32
38
  - After all repos finish, the `integration` command runs once from the run directory with `LOKI_WS_DIR_<OWNER>_<REPO>` pointing at each worktree. A timeout counts as failed. With no integration configured the status is `not_configured`.
33
- - Evidence is written to `.loki/workspaces/<name>/<run-id>/integration.json`: head SHA per repo, exit code, log sha256. It is not part of the Seal.
39
+ - Evidence is written to `.loki/workspaces/<name>/<run-id>/integration.json`: head SHA per repo, exit code, log sha256. When every repo succeeded, integration did not fail and every child sealed a receipt, the run also seals a signed `composite.json` that binds integration.json and every child receipt (see "Verify a workspace run").
40
+ - `loki workspace infer <name>` prints suggested `after:` edges read from package.json, go.mod and pyproject.toml dependencies on other listed repos. It is print-only and cycle-checked; it never edits loki.yaml. Manifests only: no OpenAPI or import inference.
41
+ - `plan: true` (or `LOKI_WORKSPACE_PLAN=1`) makes one provider call that decomposes the brief per repo into `<run-dir>/plan.json`. A valid plan gives each listed repo its own task; an invalid one falls back to the generic text and prints the reason.
42
+ - `integration: {per_stage: true}` also runs the integration command after each topological layer. A failing stage SKIPs every later layer, so a broken contract stops before downstream repos build on it.
34
43
  - Without `path`, a repo is cloned to `~/.loki/repos/owner__name`.
35
44
  - After the integration step, a PR comment is posted on each repo PR that exists for its run branch, showing the integration status (PASSED, FAILED, TIMEOUT or NOT CONFIGURED) and every head SHA. It uses your own `gh` login or GH_TOKEN, skips silently when there is no PR, never fails the run, and is disabled with `LOKI_WORKSPACE_COMMENT=0`.
36
45
  - Metrics: each run records `timing` (per repo `started_at` and `finished_at`, epoch seconds) in integration.json. `python3 autonomy/lib/workspace_metrics.py <run-dir> --attention-min 30` prints `prs` (repos with outcome ok), `wall_clock_seconds` (earliest start to latest finish), `prs_per_hour` and `prs_per_attention_minute`. Runs recorded before timing existed, and any run where `--attention-min` is not given, print `unmeasured` for the affected rate, never zero.
@@ -46,5 +55,9 @@ After `loki workspace run <name> <ref>` finishes, check the evidence rather than
46
55
  3. `.loki/workspaces/<name>/<run-id>/integration.json` holds `status`, the head SHA per repo and `timing`.
47
56
  4. `python3 autonomy/lib/workspace_metrics.py <run-dir> --attention-min <minutes>` prints `prs`, `prs_per_hour` and `prs_per_attention_minute`; any value it cannot measure prints `unmeasured`.
48
57
  5. Each repo PR for the run branch carries exactly one integration comment.
58
+ 6. `loki verify <run-dir>/composite.json` re-checks the composite offline (add `--pubkey FILE` on another machine). It recomputes the composite hash, re-verifies every child receipt from disk and checks the signature. A tampered, swapped or stale-head child makes it TAMPERED or UNCHECKED; an unsigned child under a signed composite is UNCHECKED. Exit codes are the single-receipt ones: 0 VERIFIED, 1 TAMPERED, 2 UNCHECKED, 3 UNSIGNED. The composite never reads higher than its weakest child: one PARTIAL child caps it at PARTIAL.
59
+ 7. A run that could not seal prints `composite not sealed: <reason>` (for example a repo with no receipt). Seal by hand with `loki verify --seal-composite <run-dir>`; it refuses unless every child has a receipt.
60
+
61
+ Not measured: no scale test exists for large repo counts. Claim only what you have run.
49
62
 
50
63
  `tests/workspace/99-e2e.sh` runs this whole check through both `autonomy/loki` and `bin/loki` with a stub launcher and a stub `gh`.
@@ -105,3 +105,11 @@ These knobs together implement the RARV-C (closure) loop: findings -> override c
105
105
  ## Project memory across runs
106
106
 
107
107
  extract_learnings_from_session (autonomy/run.sh) mirrors learnings and decisions into .loki/memory/learnings/project-*.jsonl. get_relevant_learnings adds a bounded newest-first project_memory list (15 entries, LOKI_PROJECT_MEMORY_MAX_CHARS, default 2000) to .loki/state/relevant-learnings.json at run start. LOKI_PROJECT_MEMORY=0 disables both. Test: tests/test-project-memory.sh.
108
+
109
+ ## Verdicts, composite receipts and the autonomy ladder (v11.7.0)
110
+ - Two sets: receipt verdicts (`engine10/types.ts` Verdict: VERIFIED, PARTIAL, ALREADY_SATISFIED, SPEC_CONFLICT, FAILED) and run outcomes (`engine10/output.ts` Outcome adds BUDGET_STOP, BLOCKED, STALLED, CANCELLED; exit ladder EXIT). Only receipt verdicts are sealed.
111
+ - Composite: `features/speed/seal_group.ts` composeComposite/verifyComposite (schema `loki.v10.composite/1`), sealed by `loki verify --seal-composite <run_dir>`, signed with the existing `signReceipt`, capped by `capGroupVerdict`. Workspace runs seal it after integration.json.
112
+ - Autonomy ladder: `src/policies/engine.js` allowedActions(level) (propose = none, execute = pr, operate = pr + deploy; merge never). engine10 `supervisor.ts` prGate consults `src/policies/check.js`; `loki deploy --execute` consults it for `deploy` and fails closed. `loki trust` graduationAdvice is advisory and never writes the policy file.
113
+ - Parallel units: `route.ts` maybeRunGroup defaults to `features/speed/run_group.ts`, which freezes per-unit acceptance (`features/contract.ts`), runs units, merges with `engine10/integrate.ts` and seals once via sealGroup.
114
+ - Lessons: every sealed run appends a receipt fact (`LOKI_AUTO_LEARN_PR`, default on) outside the receipt body; `implement.ts` injects lessons and relevant learnings inside the untrusted-data fence; `loki memory brief --task T` prints the same block.
115
+
@@ -11,3 +11,14 @@ From a URL (needs `SENTRY_AUTH_TOKEN`; without it nothing is fetched):
11
11
  SENTRY_AUTH_TOKEN=... loki start https://sentry.io/organizations/acme/issues/4001/
12
12
 
13
13
  The token is sent only to the Sentry host through a curl config on stdin, never on argv, and is never printed. Implementation: `autonomy/issue-providers.sh` (`normalize_sentry_json`).
14
+
15
+ ## Webhook: self-healing entry point
16
+
17
+ `autonomy/trigger-server.py` accepts `POST /sentry`, a Sentry integration webhook authenticated by Sentry's own HMAC (`sentry-hook-signature`, hex HMAC-SHA256 of the body).
18
+
19
+ 1. Set the client secret: `LOKI_SENTRY_WEBHOOK_SECRET` (or `SENTRY_CLIENT_SECRET`). It must differ from the GitHub webhook secret and the API token. With no secret the route answers 503 and nothing is enqueued.
20
+ 2. Start the server: `python3 autonomy/trigger-server.py --port 8080` and point the Sentry alert webhook at `/sentry`.
21
+ 3. A valid alert is normalized with `normalize_sentry_json` and enqueues `loki start "<brief>" --pr --detach`. A bad signature gets 401.
22
+
23
+ The output is a receipt-gated PR only, never a deploy. Whether the PR opens is still decided by the policy file (`autonomy`).
24
+
@@ -11,7 +11,7 @@ Classes: KEEP-MODERN (v10-native or engine-neutral), UPDATE (live but needs mode
11
11
  - DROP-LEGACY: 32
12
12
  - DELETE: 14
13
13
  - Total rows: 137
14
- - Registry paths (commands and nested subcommands): 190
14
+ - Registry paths (commands and nested subcommands): 191
15
15
 
16
16
  ## Commands
17
17