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.
- package/README.md +19 -2
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/lib/backlog.py +11 -2
- package/autonomy/lib/chat.sh +57 -1
- package/autonomy/lib/loki_yaml.py +8 -2
- package/autonomy/lib/workspace.py +267 -11
- package/autonomy/loki +315 -21
- package/autonomy/trigger-server.py +185 -2
- package/autonomy/verify.sh +4 -0
- package/completions/_loki +4 -0
- package/completions/loki.bash +20 -2
- package/dashboard/__init__.py +1 -1
- package/docs/CLI-REFERENCE.md +2 -1
- package/docs/DEPLOY-PLAN.md +17 -8
- package/docs/EVALUATING.md +8 -0
- package/docs/INSTALLATION.md +1 -1
- package/docs/WORKSPACES.md +15 -2
- package/docs/dev/architecture-reference.md +8 -0
- package/docs/sentry-intake.md +11 -0
- package/docs/v10/CLI-MODERN.md +1 -1
- package/loki-ts/dist/loki.js +1009 -975
- package/mcp/__init__.py +1 -1
- package/package.json +2 -2
- package/packages/control-plane/dist/server.js +618 -469
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/schemas/loki-yaml.schema.json +5 -2
- package/src/policies/engine.js +41 -2
- package/src/policies/types.js +49 -0
- package/web-app/dist/assets/{AdminPage-DhXdJNQk.js → AdminPage-S3BYeapm.js} +1 -1
- package/web-app/dist/assets/{Avatar-ixVj76fM.js → Avatar-C1rHwLgn.js} +1 -1
- package/web-app/dist/assets/{Badge-BGMSp22s.js → Badge-CzlnN5aQ.js} +1 -1
- package/web-app/dist/assets/{Button-pYJc32tG.js → Button-B1w5AeGC.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-C1JBBouA.js → CockpitPage-Dyeqjfkc.js} +1 -1
- package/web-app/dist/assets/{ComparePage-qD0NV4RK.js → ComparePage-C6y2GkGR.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-C-yxO58j.js → ErrorBoundary-Dg2fcGkR.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-ByPzuE1S.js → EvidenceReceiptPanel-CxbyjvTj.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-BNv_2BEJ.js → GitHubIssuesPanel-Dpys1xDb.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-CuLZQ9CS.js → GitHubPRsPanel-CRVjWoFl.js} +1 -1
- package/web-app/dist/assets/{HomePage-CS_FW8Je.js → HomePage-BKnv-oYw.js} +2 -2
- package/web-app/dist/assets/{LoginPage-DwiNf1Wm.js → LoginPage-BGksmFRd.js} +1 -1
- package/web-app/dist/assets/{MagicPage-CmQybLPZ.js → MagicPage-OCvhp16M.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-D9ERdpv8.js → MetricsPage-CJOJpXWC.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-D9ccgdbF.js → NotFoundPage-C04p3QvN.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-B9Jslmgv.js → ProjectPage-BRB8txsY.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-DnU99Go2.js → ProjectsPage-CoUYeQEh.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-DcTyx6wo.js → SettingsPage-BkjmJGfM.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-jEY9ghSr.js → ShowcasePage-B4vU9TjQ.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-CSGltW-8.js → SystemSettingsPage-Z2jiVsni.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-gUuJXb9l.js → TeamsPage-Ulm8SISq.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-37Zhe--v.js → TemplatesPage-C_7MM6Ey.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-B09hRwaR.js → TerminalOutput-Cumd7Tgs.js} +1 -1
- package/web-app/dist/assets/{activity-q_4MY9J0.js → activity-CZAUkbiY.js} +1 -1
- package/web-app/dist/assets/{bell-X8OC_PFP.js → bell-BOvSXpEr.js} +1 -1
- package/web-app/dist/assets/{bot-C-N1PUOD.js → bot-DOTZxBYj.js} +1 -1
- package/web-app/dist/assets/{check-B2c76Jmp.js → check-BxpE0B5u.js} +1 -1
- package/web-app/dist/assets/{chevron-left-BF76YLNw.js → chevron-left-D6KPvBOA.js} +1 -1
- package/web-app/dist/assets/{circle-alert-AeFKBD8h.js → circle-alert-Bajq86oe.js} +1 -1
- package/web-app/dist/assets/{clock-CiXRf-FH.js → clock-CnlHAVJc.js} +1 -1
- package/web-app/dist/assets/{cloud-DKQ8SRJ5.js → cloud-DjaymjwM.js} +1 -1
- package/web-app/dist/assets/{code-xml-DEb6jXWP.js → code-xml-BADEw2Ld.js} +1 -1
- package/web-app/dist/assets/{database-6W7e-E9c.js → database-B4aKb5Cq.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-Ds2GOuyW.js → dollar-sign-DrldXpja.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-CZU0jg52.js → file-code-corner-DVebDVR-.js} +1 -1
- package/web-app/dist/assets/{file-plus-DTVYnXCN.js → file-plus-BD-N_-Tl.js} +1 -1
- package/web-app/dist/assets/{globe-0mUzKYDe.js → globe-C7ieInC-.js} +1 -1
- package/web-app/dist/assets/{hammer-BMHZg3YX.js → hammer-BSugKIji.js} +1 -1
- package/web-app/dist/assets/{index-BvOTvjax.js → index-nLQgfvOn.js} +3 -3
- package/web-app/dist/assets/{layers-Df7TdcWB.js → layers-CHXxU2on.js} +1 -1
- package/web-app/dist/assets/{loader-circle-kPsVfrSY.js → loader-circle-BEEz0LbD.js} +1 -1
- package/web-app/dist/assets/{lock-CXYcbEc5.js → lock-CzQGfcnE.js} +1 -1
- package/web-app/dist/assets/{package-Cqhua1ID.js → package-CcHYQLGd.js} +1 -1
- package/web-app/dist/assets/{plus-lseXhsn-.js → plus-DdzgFEXn.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-OZriI6gD.js → refresh-cw-7x0HbH9Y.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-CKCfjPg8.js → rotate-ccw-DBahX1Sp.js} +1 -1
- package/web-app/dist/assets/{scroll-text-DoVyyfKb.js → scroll-text-BB8y8a9Z.js} +1 -1
- package/web-app/dist/assets/{server-DGy7TflW.js → server-CfkbW2MH.js} +1 -1
- package/web-app/dist/assets/{shield-alert-BCzRcMXi.js → shield-alert-DUgD8rBF.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-C_znJd5g.js → thumbs-up-DGoyY6QC.js} +1 -1
- package/web-app/dist/assets/{trash-2-BosCbeeA.js → trash-2-B6xrSUMr.js} +1 -1
- package/web-app/dist/assets/{trending-down-CkR5zDkV.js → trending-down-B6RGKTOx.js} +1 -1
- package/web-app/dist/assets/{trending-up-DpUIfOH4.js → trending-up-B44DCrt1.js} +1 -1
- package/web-app/dist/assets/{upload-CH9aKc8L.js → upload-4kWm33ZY.js} +1 -1
- package/web-app/dist/assets/{usePolling-pzxrHJeg.js → usePolling-zkUtRQG6.js} +1 -1
- package/web-app/dist/assets/{user-B7OHa-VQ.js → user-04cEVIe2.js} +1 -1
- 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
|
|
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 "
|
package/autonomy/verify.sh
CHANGED
|
@@ -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]' \
|
package/completions/loki.bash
CHANGED
|
@@ -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") )
|
package/dashboard/__init__.py
CHANGED
package/docs/CLI-REFERENCE.md
CHANGED
|
@@ -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.
|
|
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 |
|
package/docs/DEPLOY-PLAN.md
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
|
-
# DEPLOY-PLAN.md -- `loki deploy` (
|
|
1
|
+
# DEPLOY-PLAN.md -- `loki deploy` (original design: advisory print-only; `--execute` now exists)
|
|
2
2
|
|
|
3
|
-
**
|
|
4
|
-
|
|
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
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
package/docs/EVALUATING.md
CHANGED
|
@@ -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
|
---
|
package/docs/INSTALLATION.md
CHANGED
|
@@ -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.
|
|
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
|
|
package/docs/WORKSPACES.md
CHANGED
|
@@ -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.
|
|
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
|
+
|
package/docs/sentry-intake.md
CHANGED
|
@@ -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
|
+
|
package/docs/v10/CLI-MODERN.md
CHANGED
|
@@ -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):
|
|
14
|
+
- Registry paths (commands and nested subcommands): 191
|
|
15
15
|
|
|
16
16
|
## Commands
|
|
17
17
|
|