loki-mode 11.8.1 → 11.10.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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/lib/backlog.py +15 -3
- package/autonomy/lib/workspace.py +95 -4
- package/autonomy/loki +189 -108
- package/autonomy/trigger-server.py +302 -4
- package/bin/loki +1 -1
- package/completions/_loki +34 -1
- package/completions/loki.bash +10 -3
- package/dashboard/__init__.py +1 -1
- package/docs/ARCHITECTURE.md +0 -1
- package/docs/CLI-REFERENCE.md +1 -1
- package/docs/COMPONENTS.md +1 -1
- package/docs/DEPLOY-PLAN.md +13 -3
- package/docs/INSTALLATION.md +1 -1
- package/docs/WORKSPACES.md +1 -1
- package/docs/dev/architecture-reference.md +1 -1
- package/docs/sentry-intake.md +15 -1
- package/docs/v10/D61-SPEED.md +2 -0
- package/docs/v10/PROGRESS.md +25 -1
- package/loki-ts/dist/loki.js +1016 -946
- package/mcp/__init__.py +1 -1
- package/package.json +2 -2
- package/packages/control-plane/dist/ask-tools-server.js +13 -4
- package/packages/control-plane/dist/server.js +457 -279
- package/packages/control-plane/src/server/diffstat.ts +10 -2
- package/packages/control-plane/src/server/legacy/routes-control.ts +8 -4
- package/packages/control-plane/src/server/routes/checkpoints.ts +11 -2
- package/packages/control-plane/ui/dist/assets/index-u8cd6TRQ.js +190 -0
- package/packages/control-plane/ui/dist/index.html +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/web-app/dist/assets/{AdminPage-BnIg9ilA.js → AdminPage-D3PgG7SH.js} +1 -1
- package/web-app/dist/assets/{Avatar-0BTVsQ2A.js → Avatar-Ct77hvlG.js} +1 -1
- package/web-app/dist/assets/{Badge-Bbq8CXgn.js → Badge-Cg_gZaJF.js} +1 -1
- package/web-app/dist/assets/{Button-CKD4n0ld.js → Button-i9r2G84n.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-DJhLNLAz.js → CockpitPage-C3UyLXyp.js} +1 -1
- package/web-app/dist/assets/{ComparePage-Di2akuII.js → ComparePage-B1dXHga_.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-9lhsYG-z.js → ErrorBoundary-CQ_xREw0.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-CQDGXvDr.js → EvidenceReceiptPanel-DwUxd_Xi.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-Y_w2k9Ej.js → GitHubIssuesPanel-C0jAnuAd.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-pMSQedCv.js → GitHubPRsPanel-B4pNXJMz.js} +1 -1
- package/web-app/dist/assets/{HomePage-CIAkiGdt.js → HomePage-BC9UTS1l.js} +2 -2
- package/web-app/dist/assets/{LoginPage-BES4Pk6k.js → LoginPage-CnpdwK8F.js} +1 -1
- package/web-app/dist/assets/{MagicPage-B154fMhi.js → MagicPage-Bj0lZuNp.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-BcNyaiUO.js → MetricsPage-yyw3VnfX.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-D28FqYxG.js → NotFoundPage-LMxkJP15.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-C_O3u8Jx.js → ProjectPage-2x0MRkNu.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-uqdahuPA.js → ProjectsPage-A8J8o1gj.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-DLvVA9OZ.js → SettingsPage-fwvOVcRb.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-BAZP8vsm.js → ShowcasePage-DqI4sUwk.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-DPYZkLWh.js → SystemSettingsPage-CyytfNo_.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-BgSyb5Pd.js → TeamsPage-BTTe76W1.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-D_Jh5VU1.js → TemplatesPage-CIp3OTlL.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-4v8poDKb.js → TerminalOutput-dX5L4ky-.js} +1 -1
- package/web-app/dist/assets/{activity-CE2b6kdw.js → activity-uHXr2AaF.js} +1 -1
- package/web-app/dist/assets/{bell-CxJDlLSR.js → bell-D6RbxRMo.js} +1 -1
- package/web-app/dist/assets/{bot-C2WuK4zw.js → bot-D31w2Mu6.js} +1 -1
- package/web-app/dist/assets/{check-81epVldw.js → check-CoKJksbr.js} +1 -1
- package/web-app/dist/assets/{chevron-left-B6EXh_3C.js → chevron-left-Bjx7oX7s.js} +1 -1
- package/web-app/dist/assets/{circle-alert-0iqoo-iy.js → circle-alert-DOAh8Jmi.js} +1 -1
- package/web-app/dist/assets/{clock-CFrBpNUp.js → clock-DgEXPNWH.js} +1 -1
- package/web-app/dist/assets/{cloud-BBSFhUT_.js → cloud-0dXJFzBv.js} +1 -1
- package/web-app/dist/assets/{code-xml-BCU2V1rq.js → code-xml-DytQ_bq7.js} +1 -1
- package/web-app/dist/assets/{database-D2hvO52K.js → database-Qyjryb72.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-CXFA9b6T.js → dollar-sign-BzhTVy1e.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-CqTCEEfS.js → file-code-corner-DTdSOALw.js} +1 -1
- package/web-app/dist/assets/{file-plus-DYFbkGbs.js → file-plus-VdjOG1RK.js} +1 -1
- package/web-app/dist/assets/{globe-DpYHLobk.js → globe-BhxNOfyM.js} +1 -1
- package/web-app/dist/assets/{hammer-CZ97SEYo.js → hammer-MQzrw8ee.js} +1 -1
- package/web-app/dist/assets/{index-DzswgpRo.js → index-DaxrMbHf.js} +3 -3
- package/web-app/dist/assets/{layers-2Tq0edx4.js → layers-C7nusTP-.js} +1 -1
- package/web-app/dist/assets/{loader-circle-BFP4kUX-.js → loader-circle-B1qBkgOY.js} +1 -1
- package/web-app/dist/assets/{lock-DkKdZH4d.js → lock-BdkOxK2O.js} +1 -1
- package/web-app/dist/assets/{package-ChOGD8yf.js → package-RVyK96Zn.js} +1 -1
- package/web-app/dist/assets/{plus-hggB1o6b.js → plus-ntmwFRrp.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-DYwVLOeF.js → refresh-cw-BJ39b_PP.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-CUdY506q.js → rotate-ccw-uPx_-3vz.js} +1 -1
- package/web-app/dist/assets/{scroll-text-Wvidz5uN.js → scroll-text-D9qvXkj-.js} +1 -1
- package/web-app/dist/assets/{server-O11QmkCN.js → server-gs4xmD8Y.js} +1 -1
- package/web-app/dist/assets/{shield-alert-DXXx27uV.js → shield-alert-O2_YffGI.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-DF8ivs3A.js → thumbs-up-B3RVBhQx.js} +1 -1
- package/web-app/dist/assets/{trash-2-LA6PW0Nf.js → trash-2-DEUt7qhq.js} +1 -1
- package/web-app/dist/assets/{trending-down-Bw_vLKHT.js → trending-down-oyTns4UZ.js} +1 -1
- package/web-app/dist/assets/{trending-up-tX8motha.js → trending-up-CdpZK0Oc.js} +1 -1
- package/web-app/dist/assets/{upload-CxtTWJd2.js → upload-D9sbjjC8.js} +1 -1
- package/web-app/dist/assets/{usePolling-D-P5Iofq.js → usePolling-6ognrnyV.js} +1 -1
- package/web-app/dist/assets/{user-dKTcuUFq.js → user-CBKc8QoI.js} +1 -1
- package/web-app/dist/index.html +1 -1
- package/memory/rag_injector.py +0 -161
- package/packages/control-plane/ui/dist/assets/index-DbLzr87u.js +0 -190
|
@@ -38,6 +38,7 @@ Usage:
|
|
|
38
38
|
"""
|
|
39
39
|
|
|
40
40
|
import argparse
|
|
41
|
+
import base64
|
|
41
42
|
import collections
|
|
42
43
|
import hashlib
|
|
43
44
|
import hmac
|
|
@@ -56,7 +57,7 @@ import sys
|
|
|
56
57
|
import threading
|
|
57
58
|
import time
|
|
58
59
|
import urllib.parse
|
|
59
|
-
from datetime import datetime
|
|
60
|
+
from datetime import datetime, timezone
|
|
60
61
|
from pathlib import Path
|
|
61
62
|
from urllib.request import Request, urlopen
|
|
62
63
|
|
|
@@ -418,6 +419,8 @@ CHILD_ENV_DENYLIST = (
|
|
|
418
419
|
"SLACK_BOT_TOKEN",
|
|
419
420
|
"LINEAR_WEBHOOK_SECRET",
|
|
420
421
|
"LINEAR_API_KEY",
|
|
422
|
+
"JIRA_WEBHOOK_SECRET",
|
|
423
|
+
"TEAMS_SECURITY_TOKEN",
|
|
421
424
|
)
|
|
422
425
|
|
|
423
426
|
|
|
@@ -583,6 +586,14 @@ def handle_workflow_run_event(payload, dry_run=False):
|
|
|
583
586
|
return summary, status
|
|
584
587
|
|
|
585
588
|
|
|
589
|
+
def brief_start_args(brief):
|
|
590
|
+
"""argv for a delegated PR run. `--brief` forces brief mode so the text is
|
|
591
|
+
never reclassified as a PRD path or issue URL, and `--pr --detach` are only
|
|
592
|
+
honored by `loki start` in brief mode because cmd_start routes them through
|
|
593
|
+
the worktree + PR path."""
|
|
594
|
+
return ["start", "--brief", brief, "--pr", "--detach"]
|
|
595
|
+
|
|
596
|
+
|
|
586
597
|
def handle_job_event(payload, dry_run=False):
|
|
587
598
|
"""Handle a remotely-submitted job: loki start <spec> --detach.
|
|
588
599
|
|
|
@@ -594,7 +605,11 @@ def handle_job_event(payload, dry_run=False):
|
|
|
594
605
|
spec = payload.get("spec")
|
|
595
606
|
if not valid_spec(spec):
|
|
596
607
|
return None, "rejected (invalid spec)"
|
|
597
|
-
|
|
608
|
+
# Jira/Teams delegation sets pr=True: receipt-gated PR run, never a deploy.
|
|
609
|
+
if payload.get("pr") is True:
|
|
610
|
+
args = brief_start_args(spec.strip())
|
|
611
|
+
else:
|
|
612
|
+
args = ["start", spec.strip(), "--detach"]
|
|
598
613
|
summary = "job %s: %s" % (payload.get("job_id", "?"), spec.strip())
|
|
599
614
|
# A remote submitter gates CI on this, so report the BUILD's outcome, not
|
|
600
615
|
# merely that a build was launched. "fired" (launched) and "passed" must
|
|
@@ -693,6 +708,167 @@ def resolve_chat_secrets(env, secret, api_token, sentry_secret):
|
|
|
693
708
|
return ("" if slack_bad else slack), ("" if linear_bad else linear)
|
|
694
709
|
|
|
695
710
|
|
|
711
|
+
# Jira Cloud and Microsoft Teams delegation. Both enqueue a PR-only run.
|
|
712
|
+
JIRA_KEY_RE = re.compile(r"^[A-Z][A-Z0-9_]*-\d+$")
|
|
713
|
+
JIRA_EVENTS = ("jira:issue_created", "jira:issue_updated")
|
|
714
|
+
JIRA_TRIGGER_FIELDS = ("assignee", "labels")
|
|
715
|
+
DELEGATED_BRIEF_MAX = 1500
|
|
716
|
+
HTML_TAG_RE = re.compile(r"<[^>]*>")
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def verify_jira_signature(secret, body, signature):
|
|
720
|
+
"""X-Hub-Signature: `sha256=` + hex HMAC-SHA256 of the raw body. Jira sends
|
|
721
|
+
no timestamp, so replay is bounded by delivery-id dedup in the handler."""
|
|
722
|
+
if not secret or not signature:
|
|
723
|
+
return False
|
|
724
|
+
sig = signature.strip().lower()
|
|
725
|
+
if not sig.startswith("sha256="):
|
|
726
|
+
return False
|
|
727
|
+
expected = hmac.new(secret.encode("utf-8"), body, hashlib.sha256).hexdigest()
|
|
728
|
+
return constant_time_equals(expected, sig[len("sha256="):])
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def sign_teams_body(token, body):
|
|
732
|
+
"""Base64 HMAC-SHA256 of the raw body, keyed by the base64-decoded token."""
|
|
733
|
+
key = base64.b64decode(token, validate=True)
|
|
734
|
+
return base64.b64encode(hmac.new(key, body, hashlib.sha256).digest()).decode("ascii")
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def verify_teams_signature(token, body, authorization):
|
|
738
|
+
"""Authorization: `HMAC <base64>`. Fails closed on a bad token or header."""
|
|
739
|
+
if not token or not authorization:
|
|
740
|
+
return False
|
|
741
|
+
scheme, _, value = authorization.strip().partition(" ")
|
|
742
|
+
if scheme.upper() != "HMAC" or not value.strip():
|
|
743
|
+
return False
|
|
744
|
+
try:
|
|
745
|
+
expected = sign_teams_body(token, body)
|
|
746
|
+
except ValueError:
|
|
747
|
+
return False
|
|
748
|
+
return constant_time_equals(expected, value.strip())
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
def teams_dedup_key(activity):
|
|
752
|
+
"""Dedup key for a Teams activity: activity ids are unique only within a
|
|
753
|
+
conversation, so scope by conversation id plus serviceUrl and tenant when
|
|
754
|
+
present. Empty string (never deduplicated) when the activity id is absent."""
|
|
755
|
+
act_id = activity.get("id")
|
|
756
|
+
if not isinstance(act_id, str) or not act_id:
|
|
757
|
+
return ""
|
|
758
|
+
|
|
759
|
+
def _s(v):
|
|
760
|
+
return v if isinstance(v, str) else ""
|
|
761
|
+
|
|
762
|
+
conv = activity.get("conversation")
|
|
763
|
+
conv = conv if isinstance(conv, dict) else {}
|
|
764
|
+
cdata = activity.get("channelData")
|
|
765
|
+
tenant = cdata.get("tenant") if isinstance(cdata, dict) else None
|
|
766
|
+
tenant_id = _s(tenant.get("id")) if isinstance(tenant, dict) else ""
|
|
767
|
+
tenant_id = tenant_id or _s(conv.get("tenantId"))
|
|
768
|
+
return "teams:" + json.dumps(
|
|
769
|
+
[_s(conv.get("id")), _s(activity.get("serviceUrl")), tenant_id, act_id])
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
def teams_fresh(activity, now=None):
|
|
773
|
+
"""The activity's ISO-8601 `timestamp` must be within the skew window."""
|
|
774
|
+
raw = activity.get("timestamp")
|
|
775
|
+
if not isinstance(raw, str) or not raw:
|
|
776
|
+
return False
|
|
777
|
+
try:
|
|
778
|
+
dt = datetime.fromisoformat(raw.strip().replace("Z", "+00:00"))
|
|
779
|
+
except ValueError:
|
|
780
|
+
return False
|
|
781
|
+
if dt.tzinfo is None:
|
|
782
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
783
|
+
now = time.time() if now is None else now
|
|
784
|
+
return abs(now - dt.timestamp()) <= SIGNATURE_MAX_SKEW_S
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def _csv_set(value):
|
|
788
|
+
return frozenset(x.strip() for x in value.split(",") if x.strip())
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
def _one_line(text, limit=DELEGATED_BRIEF_MAX):
|
|
792
|
+
text = " ".join(CONTROL_CHARS_RE.sub(" ", str(text or "")).split())
|
|
793
|
+
return text.encode("utf-8")[:limit].decode("utf-8", errors="ignore")
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
def jira_trigger_matches(payload, assignees, labels):
|
|
797
|
+
"""True when the issue is assigned to a configured user or carries a
|
|
798
|
+
configured label. An update only counts if it changed assignee or labels,
|
|
799
|
+
so unrelated edits never re-fire a run."""
|
|
800
|
+
issue = payload.get("issue")
|
|
801
|
+
fields = issue.get("fields") if isinstance(issue, dict) else None
|
|
802
|
+
if not isinstance(fields, dict):
|
|
803
|
+
return False
|
|
804
|
+
if payload.get("webhookEvent") == "jira:issue_updated":
|
|
805
|
+
items = (payload.get("changelog") or {}).get("items")
|
|
806
|
+
changed = ({i.get("field") for i in items if isinstance(i, dict)}
|
|
807
|
+
if isinstance(items, list) else set())
|
|
808
|
+
if not changed.intersection(JIRA_TRIGGER_FIELDS):
|
|
809
|
+
return False
|
|
810
|
+
who = fields.get("assignee")
|
|
811
|
+
ids = set()
|
|
812
|
+
if isinstance(who, dict):
|
|
813
|
+
for k in ("accountId", "emailAddress", "displayName"):
|
|
814
|
+
if isinstance(who.get(k), str):
|
|
815
|
+
ids.add(who[k])
|
|
816
|
+
if ids.intersection(assignees):
|
|
817
|
+
return True
|
|
818
|
+
have = fields.get("labels")
|
|
819
|
+
return isinstance(have, list) and bool(
|
|
820
|
+
{x for x in have if isinstance(x, str)}.intersection(labels))
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
def adf_text(node, out=None):
|
|
824
|
+
"""Flatten an Atlassian Document Format tree to its text nodes."""
|
|
825
|
+
top = out is None
|
|
826
|
+
out = [] if top else out
|
|
827
|
+
if isinstance(node, dict):
|
|
828
|
+
if isinstance(node.get("text"), str):
|
|
829
|
+
out.append(node["text"])
|
|
830
|
+
for child in node.get("content") or []:
|
|
831
|
+
adf_text(child, out)
|
|
832
|
+
elif isinstance(node, list):
|
|
833
|
+
for child in node:
|
|
834
|
+
adf_text(child, out)
|
|
835
|
+
return " ".join(out) if top else None
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def build_jira_brief(issue):
|
|
839
|
+
fields = issue.get("fields") or {}
|
|
840
|
+
desc = fields.get("description")
|
|
841
|
+
if isinstance(desc, dict):
|
|
842
|
+
desc = adf_text(desc)
|
|
843
|
+
parts = ["Resolve Jira issue %s: %s." % (issue.get("key"), fields.get("summary") or "")]
|
|
844
|
+
if isinstance(desc, str) and desc.strip():
|
|
845
|
+
parts.append(desc)
|
|
846
|
+
parts.append("Reproduce with a regression test, then fix the root cause.")
|
|
847
|
+
return _one_line(" ".join(parts))
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
def build_teams_brief(activity):
|
|
851
|
+
text = re.sub(r"<at>.*?</at>", " ", str(activity.get("text") or ""), flags=re.S)
|
|
852
|
+
return _one_line(HTML_TAG_RE.sub(" ", text))
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
def resolve_delegation_secrets(env, others):
|
|
856
|
+
"""Return (jira_secret, teams_token), blanking any equal to another
|
|
857
|
+
credential in `others` or to each other."""
|
|
858
|
+
jira = env.get("JIRA_WEBHOOK_SECRET", "")
|
|
859
|
+
teams = env.get("TEAMS_SECURITY_TOKEN", "")
|
|
860
|
+
others = [x for x in others if x]
|
|
861
|
+
out = []
|
|
862
|
+
for name, val, peer in (("Jira", jira, teams), ("Teams", teams, jira)):
|
|
863
|
+
bad = bool(val) and any(constant_time_equals(val, o)
|
|
864
|
+
for o in others + ([peer] if peer else []))
|
|
865
|
+
if bad:
|
|
866
|
+
logging.error("The %s secret is identical to another credential. "
|
|
867
|
+
"They MUST differ; disabling its route.", name)
|
|
868
|
+
out.append("" if bad else val)
|
|
869
|
+
return tuple(out)
|
|
870
|
+
|
|
871
|
+
|
|
696
872
|
def _post_json(url, data, headers):
|
|
697
873
|
"""POST JSON with a bounded timeout. Raises on any failure; callers log."""
|
|
698
874
|
req = Request(url, data=json.dumps(data).encode("utf-8"),
|
|
@@ -798,7 +974,7 @@ def handle_sentry_event(payload, dry_run=False):
|
|
|
798
974
|
brief = build_sentry_brief(norm)
|
|
799
975
|
if not valid_spec(brief):
|
|
800
976
|
return None, "rejected (invalid brief)"
|
|
801
|
-
args =
|
|
977
|
+
args = brief_start_args(brief)
|
|
802
978
|
summary = "sentry issue %s" % norm.get("number", "?")
|
|
803
979
|
success = run_loki_command(args, dry_run=dry_run)
|
|
804
980
|
status = "fired" if success else "error"
|
|
@@ -1129,6 +1305,12 @@ class WebhookHandler(http.server.BaseHTTPRequestHandler):
|
|
|
1129
1305
|
# distinct from every other credential.
|
|
1130
1306
|
slack_secret = ""
|
|
1131
1307
|
linear_secret = ""
|
|
1308
|
+
# Jira webhook secret (/jira), Teams outgoing-webhook token (/teams), and
|
|
1309
|
+
# the Jira trigger filters (assignee ids/emails/names, labels).
|
|
1310
|
+
jira_secret = ""
|
|
1311
|
+
teams_secret = ""
|
|
1312
|
+
jira_assignees = frozenset()
|
|
1313
|
+
jira_labels = frozenset()
|
|
1132
1314
|
dispatcher = None
|
|
1133
1315
|
# Ed25519 key that attests receipts, and the retired public keys that keep
|
|
1134
1316
|
# pre-rotation receipts verifiable. Both default to unconfigured, which
|
|
@@ -1414,7 +1596,7 @@ class WebhookHandler(http.server.BaseHTTPRequestHandler):
|
|
|
1414
1596
|
return
|
|
1415
1597
|
self._send_json(202, {"id": job_id, "status": "queued"})
|
|
1416
1598
|
|
|
1417
|
-
def _enqueue_job(self, spec, reply_target=None):
|
|
1599
|
+
def _enqueue_job(self, spec, reply_target=None, pr=False):
|
|
1418
1600
|
"""Record and enqueue one validated job; shared by /jobs, /slack, /linear.
|
|
1419
1601
|
|
|
1420
1602
|
Returns the job id, or None after sending 429 when the queue is full.
|
|
@@ -1422,6 +1604,8 @@ class WebhookHandler(http.server.BaseHTTPRequestHandler):
|
|
|
1422
1604
|
"""
|
|
1423
1605
|
job_id = secrets.token_urlsafe(12)
|
|
1424
1606
|
job = {"spec": spec, "job_id": job_id}
|
|
1607
|
+
if pr:
|
|
1608
|
+
job["pr"] = True
|
|
1425
1609
|
|
|
1426
1610
|
# Record BEFORE enqueueing. A worker can pick the job up the instant
|
|
1427
1611
|
# submit() returns and write "running"/"fired"; record_job is
|
|
@@ -1546,6 +1730,99 @@ class WebhookHandler(http.server.BaseHTTPRequestHandler):
|
|
|
1546
1730
|
return
|
|
1547
1731
|
self._send_json(202, {"id": job_id, "status": "queued"})
|
|
1548
1732
|
|
|
1733
|
+
def _handle_jira(self):
|
|
1734
|
+
"""POST /jira -- signed Jira Cloud webhook; enqueues a PR-only run."""
|
|
1735
|
+
body = self._read_body()
|
|
1736
|
+
if body is None:
|
|
1737
|
+
return
|
|
1738
|
+
if not self.jira_secret:
|
|
1739
|
+
log_event(JOB_EVENT, "", "", "rejected (no jira webhook secret configured)")
|
|
1740
|
+
self._send_json(503, {"error": "JIRA_WEBHOOK_SECRET not configured"})
|
|
1741
|
+
return
|
|
1742
|
+
if not verify_jira_signature(
|
|
1743
|
+
self.jira_secret, body, self.headers.get("X-Hub-Signature", "")):
|
|
1744
|
+
logging.warning("Invalid Jira signature from %s", self.address_string())
|
|
1745
|
+
log_event(JOB_EVENT, "", "", "rejected (invalid jira signature)")
|
|
1746
|
+
self._send_json(401, {"error": "invalid signature"})
|
|
1747
|
+
return
|
|
1748
|
+
try:
|
|
1749
|
+
payload = json.loads(body)
|
|
1750
|
+
except ValueError:
|
|
1751
|
+
self._send_json(400, {"error": "invalid JSON"})
|
|
1752
|
+
return
|
|
1753
|
+
if not isinstance(payload, dict):
|
|
1754
|
+
self._send_json(400, {"error": "payload must be a JSON object"})
|
|
1755
|
+
return
|
|
1756
|
+
if (payload.get("webhookEvent") not in JIRA_EVENTS
|
|
1757
|
+
or not jira_trigger_matches(
|
|
1758
|
+
payload, self.jira_assignees, self.jira_labels)):
|
|
1759
|
+
self._send_json(200, {"status": "skipped"})
|
|
1760
|
+
return
|
|
1761
|
+
issue = payload["issue"]
|
|
1762
|
+
key = issue.get("key")
|
|
1763
|
+
if not isinstance(key, str) or not JIRA_KEY_RE.match(key):
|
|
1764
|
+
self._send_json(400, {"error": "invalid issue key"})
|
|
1765
|
+
return
|
|
1766
|
+
brief = build_jira_brief(issue)
|
|
1767
|
+
if not valid_spec(brief):
|
|
1768
|
+
self._send_json(400, {"error": "invalid brief"})
|
|
1769
|
+
return
|
|
1770
|
+
# Jira has no signed timestamp, and the delivery id header is not covered
|
|
1771
|
+
# by the HMAC, so also dedup on a hash of the signed body: a captured body
|
|
1772
|
+
# cannot be replayed by changing or dropping the header.
|
|
1773
|
+
dup_id = self.dispatcher.seen_delivery(
|
|
1774
|
+
self.headers.get("X-Atlassian-Webhook-Identifier", ""))
|
|
1775
|
+
dup_body = self.dispatcher.seen_delivery(
|
|
1776
|
+
"jira-body:" + hashlib.sha256(body).hexdigest())
|
|
1777
|
+
if dup_id or dup_body:
|
|
1778
|
+
self._send_json(200, {"status": "duplicate"})
|
|
1779
|
+
return
|
|
1780
|
+
job_id = self._enqueue_job(brief, pr=True)
|
|
1781
|
+
if job_id is None:
|
|
1782
|
+
return
|
|
1783
|
+
self._send_json(202, {"id": job_id, "status": "queued"})
|
|
1784
|
+
|
|
1785
|
+
def _handle_teams(self):
|
|
1786
|
+
"""POST /teams -- Teams outgoing webhook; enqueues a PR-only run."""
|
|
1787
|
+
body = self._read_body()
|
|
1788
|
+
if body is None:
|
|
1789
|
+
return
|
|
1790
|
+
if not self.teams_secret:
|
|
1791
|
+
log_event(JOB_EVENT, "", "", "rejected (no teams security token configured)")
|
|
1792
|
+
self._send_json(503, {"error": "TEAMS_SECURITY_TOKEN not configured"})
|
|
1793
|
+
return
|
|
1794
|
+
if not verify_teams_signature(
|
|
1795
|
+
self.teams_secret, body, self.headers.get("Authorization", "")):
|
|
1796
|
+
logging.warning("Invalid Teams signature from %s", self.address_string())
|
|
1797
|
+
log_event(JOB_EVENT, "", "", "rejected (invalid teams signature)")
|
|
1798
|
+
self._send_json(401, {"error": "invalid signature"})
|
|
1799
|
+
return
|
|
1800
|
+
try:
|
|
1801
|
+
activity = json.loads(body)
|
|
1802
|
+
except ValueError:
|
|
1803
|
+
self._send_json(400, {"error": "invalid JSON"})
|
|
1804
|
+
return
|
|
1805
|
+
if not isinstance(activity, dict):
|
|
1806
|
+
self._send_json(400, {"error": "payload must be a JSON object"})
|
|
1807
|
+
return
|
|
1808
|
+
if not teams_fresh(activity):
|
|
1809
|
+
log_event(JOB_EVENT, "", "", "rejected (stale teams timestamp)")
|
|
1810
|
+
self._send_json(401, {"error": "stale or missing timestamp"})
|
|
1811
|
+
return
|
|
1812
|
+
brief = build_teams_brief(activity)
|
|
1813
|
+
if not valid_spec(brief):
|
|
1814
|
+
self._send_json(200, {"type": "message",
|
|
1815
|
+
"text": "Usage: @loki <brief>. One-line brief, "
|
|
1816
|
+
"not starting with '-'."})
|
|
1817
|
+
return
|
|
1818
|
+
if self.dispatcher.seen_delivery(teams_dedup_key(activity)):
|
|
1819
|
+
self._send_json(200, {"type": "message", "text": "Duplicate, already queued."})
|
|
1820
|
+
return
|
|
1821
|
+
job_id = self._enqueue_job(brief, pr=True)
|
|
1822
|
+
if job_id is None:
|
|
1823
|
+
return
|
|
1824
|
+
self._send_json(200, {"type": "message", "text": "Queued loki job %s" % job_id})
|
|
1825
|
+
|
|
1549
1826
|
def _handle_sentry(self):
|
|
1550
1827
|
"""POST /sentry -- Sentry alert, HMAC-verified, enqueues a fix run."""
|
|
1551
1828
|
body = self._read_body()
|
|
@@ -1597,6 +1874,12 @@ class WebhookHandler(http.server.BaseHTTPRequestHandler):
|
|
|
1597
1874
|
if path == "/linear":
|
|
1598
1875
|
self._handle_linear()
|
|
1599
1876
|
return
|
|
1877
|
+
if path == "/jira":
|
|
1878
|
+
self._handle_jira()
|
|
1879
|
+
return
|
|
1880
|
+
if path == "/teams":
|
|
1881
|
+
self._handle_teams()
|
|
1882
|
+
return
|
|
1600
1883
|
if path != "/webhook":
|
|
1601
1884
|
self._send_json(404, {"error": "not found"})
|
|
1602
1885
|
return
|
|
@@ -1782,6 +2065,11 @@ def main():
|
|
|
1782
2065
|
slack_secret, linear_secret = resolve_chat_secrets(
|
|
1783
2066
|
os.environ, secret, api_token, sentry_secret)
|
|
1784
2067
|
|
|
2068
|
+
jira_secret, teams_secret = resolve_delegation_secrets(
|
|
2069
|
+
os.environ, [secret, api_token, sentry_secret, slack_secret, linear_secret])
|
|
2070
|
+
WebhookHandler.jira_assignees = _csv_set(os.environ.get("JIRA_ASSIGNEES", ""))
|
|
2071
|
+
WebhookHandler.jira_labels = _csv_set(os.environ.get("JIRA_LABELS", ""))
|
|
2072
|
+
|
|
1785
2073
|
# Persist resolved values, but never write the secret to disk.
|
|
1786
2074
|
config["port"] = port
|
|
1787
2075
|
config["dry_run"] = dry_run
|
|
@@ -1798,6 +2086,8 @@ def main():
|
|
|
1798
2086
|
WebhookHandler.sentry_secret = sentry_secret
|
|
1799
2087
|
WebhookHandler.slack_secret = slack_secret
|
|
1800
2088
|
WebhookHandler.linear_secret = linear_secret
|
|
2089
|
+
WebhookHandler.jira_secret = jira_secret
|
|
2090
|
+
WebhookHandler.teams_secret = teams_secret
|
|
1801
2091
|
WebhookHandler.dispatcher = dispatcher
|
|
1802
2092
|
|
|
1803
2093
|
signing_key, signing_kid = load_signing_key(auto_generate=False)
|
|
@@ -1854,6 +2144,14 @@ def main():
|
|
|
1854
2144
|
"No Linear webhook secret configured: POST /linear is rejected with "
|
|
1855
2145
|
"503. Set LINEAR_WEBHOOK_SECRET."
|
|
1856
2146
|
)
|
|
2147
|
+
if not jira_secret:
|
|
2148
|
+
logging.warning(
|
|
2149
|
+
"No Jira webhook secret configured: POST /jira is rejected with "
|
|
2150
|
+
"503. Set JIRA_WEBHOOK_SECRET.")
|
|
2151
|
+
if not teams_secret:
|
|
2152
|
+
logging.warning(
|
|
2153
|
+
"No Teams security token configured: POST /teams is rejected with "
|
|
2154
|
+
"503. Set TEAMS_SECURITY_TOKEN.")
|
|
1857
2155
|
if not secret:
|
|
1858
2156
|
logging.warning(
|
|
1859
2157
|
"No webhook secret configured: ALL webhooks will be rejected with "
|
package/bin/loki
CHANGED
|
@@ -794,7 +794,7 @@ fi
|
|
|
794
794
|
# Two-token routes (provider show/list, memory list/index) match on the first
|
|
795
795
|
# token only; the Bun dispatcher handles subcommand routing internally.
|
|
796
796
|
case "${1:-}" in
|
|
797
|
-
version|--version|-v|status|stats|doctor|provider|memory|rollback|internal|kpis|trust|proof|receipt|wiki|crash|control|contract|slack|queue|issues|answer|undo|verify-pr|completion|__complete)
|
|
797
|
+
version|--version|-v|status|stats|doctor|provider|memory|rollback|internal|kpis|estimate|trust|proof|receipt|wiki|crash|control|contract|slack|queue|issues|answer|undo|verify-pr|completion|__complete)
|
|
798
798
|
# v7.5.2: rollback added (wires loki-ts/src/commands/rollback.ts).
|
|
799
799
|
# v7.5.3: internal added for autonomy/run.sh phase1-hooks calls.
|
|
800
800
|
# v7.5.28: kpis added (Phase K MVP: read-only KPI snapshot).
|
package/completions/_loki
CHANGED
|
@@ -56,6 +56,13 @@ function _loki {
|
|
|
56
56
|
agent)
|
|
57
57
|
_loki_agent
|
|
58
58
|
;;
|
|
59
|
+
estimate)
|
|
60
|
+
_arguments \
|
|
61
|
+
'--json[Output machine-readable JSON]' \
|
|
62
|
+
'--model[Model whose rough prior to use]:model:' \
|
|
63
|
+
'--provider[Provider whose default model is used]:provider:' \
|
|
64
|
+
'--help[Show help]'
|
|
65
|
+
;;
|
|
59
66
|
status)
|
|
60
67
|
_arguments \
|
|
61
68
|
'--json[Output machine-readable JSON]' \
|
|
@@ -162,6 +169,27 @@ function _loki {
|
|
|
162
169
|
code)
|
|
163
170
|
_loki_code
|
|
164
171
|
;;
|
|
172
|
+
deploy)
|
|
173
|
+
_arguments \
|
|
174
|
+
'--dir[Project directory to scan]:directory:_directories' \
|
|
175
|
+
'--no-clip[Do not copy the command to the clipboard]' \
|
|
176
|
+
'--execute[Run the deploy command if the receipt gate passes]' \
|
|
177
|
+
'--canary[With --execute: staged rollout (Vercel promote, Fly canary)]' \
|
|
178
|
+
'--allow-unanchored[Proceed when the receipt records no baseline]' \
|
|
179
|
+
'--health-url[Health probe URL]:url:' \
|
|
180
|
+
'--auto-rollback[Roll back on a failed health probe]' \
|
|
181
|
+
'--help[Show help]'
|
|
182
|
+
;;
|
|
183
|
+
verify)
|
|
184
|
+
_arguments \
|
|
185
|
+
'--pubkey[Check against this public key only]:key file:_files' \
|
|
186
|
+
'--sign-deploy[Sign a loki.deploy/1 deploy record in place]:record:_files' \
|
|
187
|
+
'--receipt[Cited receipt file for a deploy record]:receipt:_files' \
|
|
188
|
+
'--max-age[Reject a receipt older than DUR]:duration:' \
|
|
189
|
+
'--seal-composite[Sign a workspace composite]:run dir:_directories' \
|
|
190
|
+
'--help[Show help]' \
|
|
191
|
+
'*:receipt or deploy record:_files'
|
|
192
|
+
;;
|
|
165
193
|
esac
|
|
166
194
|
;;
|
|
167
195
|
esac
|
|
@@ -231,7 +259,7 @@ function _loki_commands {
|
|
|
231
259
|
'plan:Preview the build plan for a spec'
|
|
232
260
|
'report:Reporting commands (cost, kpis, share)'
|
|
233
261
|
'cost:Show cost of recent runs (--detail for cache + trend)'
|
|
234
|
-
'estimate:
|
|
262
|
+
'estimate:Estimate run cost from run history (--json, --model, --provider)'
|
|
235
263
|
'kpis:Key performance indicators'
|
|
236
264
|
'stats:Session statistics'
|
|
237
265
|
'preview:Open the running app preview'
|
|
@@ -313,6 +341,11 @@ function _loki_start {
|
|
|
313
341
|
_arguments \
|
|
314
342
|
'--provider[AI provider]:provider name:(claude cline codex aider opencode)' \
|
|
315
343
|
'--parallel[Parallel mode]' \
|
|
344
|
+
'--brief[Zero-config brief mode]:brief text:' \
|
|
345
|
+
'--worktree[Git worktree isolation]' \
|
|
346
|
+
'--pr[Worktree plus auto-create PR]' \
|
|
347
|
+
'--ship[Worktree plus PR plus auto-merge]' \
|
|
348
|
+
'--detach[Run in background (implies --worktree)]' \
|
|
316
349
|
'--workspace[Run a loki.yaml workspace for the brief]:workspace name:' \
|
|
317
350
|
'--deploy[Deploy after a VERIFIED run via loki deploy --execute]' \
|
|
318
351
|
'--health-url[With --deploy: health probe URL]:url:' \
|
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 --workspace --deploy --health-url --auto-rollback --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 --brief --worktree --pr --ship --detach --workspace --deploy --health-url --auto-rollback --help"
|
|
31
31
|
COMPREPLY=( $(compgen -W "${flags}" -- "$cur") )
|
|
32
32
|
return 0
|
|
33
33
|
fi
|
|
@@ -120,6 +120,13 @@ _loki_completion() {
|
|
|
120
120
|
COMPREPLY=( $(compgen -W "${syslog_cmds}" -- "$cur") )
|
|
121
121
|
;;
|
|
122
122
|
|
|
123
|
+
estimate)
|
|
124
|
+
if [[ "$cur" == -* ]]; then
|
|
125
|
+
COMPREPLY=( $(compgen -W "--json --model --provider --help" -- "$cur") )
|
|
126
|
+
return 0
|
|
127
|
+
fi
|
|
128
|
+
;;
|
|
129
|
+
|
|
123
130
|
status)
|
|
124
131
|
if [[ "$cur" == -* ]]; then
|
|
125
132
|
COMPREPLY=( $(compgen -W "--json --help" -- "$cur") )
|
|
@@ -142,14 +149,14 @@ _loki_completion() {
|
|
|
142
149
|
|
|
143
150
|
deploy)
|
|
144
151
|
if [[ "$cur" == -* ]]; then
|
|
145
|
-
COMPREPLY=( $(compgen -W "--execute --health-url --auto-rollback --dir --no-clip --allow-unanchored --help" -- "$cur") )
|
|
152
|
+
COMPREPLY=( $(compgen -W "--execute --canary --health-url --auto-rollback --dir --no-clip --allow-unanchored --help" -- "$cur") )
|
|
146
153
|
return 0
|
|
147
154
|
fi
|
|
148
155
|
;;
|
|
149
156
|
|
|
150
157
|
verify)
|
|
151
158
|
if [[ "$cur" == -* ]]; then
|
|
152
|
-
COMPREPLY=( $(compgen -W "--base --out --block-on --no-llm --json --explain --check-fresh --max-age --hosted --seal-composite --help" -- "$cur") )
|
|
159
|
+
COMPREPLY=( $(compgen -W "--base --out --block-on --no-llm --json --explain --check-fresh --max-age --hosted --seal-composite --sign-deploy --receipt --pubkey --help" -- "$cur") )
|
|
153
160
|
return 0
|
|
154
161
|
fi
|
|
155
162
|
;;
|
package/dashboard/__init__.py
CHANGED
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -209,7 +209,6 @@ memory/ Memory system (Python package, ~19 modules)
|
|
|
209
209
|
embeddings.py Vector embeddings (optional)
|
|
210
210
|
vector_index.py Vector search index
|
|
211
211
|
cross_project.py Cross-project knowledge transfer
|
|
212
|
-
rag_injector.py RAG context injection into prompts
|
|
213
212
|
knowledge_graph.py Knowledge graph over memories
|
|
214
213
|
layers/ Progressive-disclosure loading
|
|
215
214
|
|
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.10.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 |
|
package/docs/COMPONENTS.md
CHANGED
|
@@ -132,7 +132,7 @@ across projects.
|
|
|
132
132
|
- `consolidation.py` - Episodic-to-semantic consolidation pipeline.
|
|
133
133
|
- `token_economics.py` - Discovery vs read token accounting.
|
|
134
134
|
- `embeddings.py`, `vector_index.py` - Optional embedding-based similarity.
|
|
135
|
-
- `cross_project.py`, `knowledge_graph.py
|
|
135
|
+
- `cross_project.py`, `knowledge_graph.py` - Cross-project
|
|
136
136
|
recall, knowledge graph, and RAG prompt injection (v7.1.0+).
|
|
137
137
|
- `app_graph.py`, `error_log.py`, `ingest.py`, `namespace.py`, `replay.py`,
|
|
138
138
|
`unified_access.py` - Supporting modules.
|
package/docs/DEPLOY-PLAN.md
CHANGED
|
@@ -7,9 +7,19 @@ clean tree, a destructive-op refusal and a policy permission (`.loki/policies.js
|
|
|
7
7
|
deployed URL after the command. A non-2xx result is recorded as FAILED_HEALTH and exits non-zero;
|
|
8
8
|
with `--auto-rollback` it also runs the provider's documented rollback verb (`vercel rollback`,
|
|
9
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
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
URL, health is recorded as "unchecked", never healthy. The deploy record in `.loki/deploys` is a
|
|
11
|
+
signed `loki.deploy/1` attestation bound to the receipt that authorized it (fields: receipt_sha256,
|
|
12
|
+
head_sha, target, command, health_status, health_url, rollback_attempted, rollback_exit,
|
|
13
|
+
started_at, finished_at). It is signed with the existing receipt signer (Ed25519, no new crypto)
|
|
14
|
+
and the receipt itself is unchanged. `loki verify <record.json>` re-checks it offline (also with
|
|
15
|
+
`--pubkey FILE`; `--receipt FILE` names the cited receipt when it is not beside the record). It
|
|
16
|
+
fails closed: a flipped byte is TAMPERED, an unsigned record or an unknown key is UNCHECKED, and a
|
|
17
|
+
cited receipt that is not VERIFIED is never reported as VERIFIED.
|
|
18
|
+
`loki deploy --execute --canary` is a staged rollout using only documented provider verbs: Vercel
|
|
19
|
+
deploys without `--prod`, health-checks the preview URL, then runs `vercel promote <url>` (a failed
|
|
20
|
+
probe never promotes); Fly runs `fly deploy --strategy canary`; Netlify and Cloudflare document no
|
|
21
|
+
staged path and refuse `--canary` with a sentence saying so.
|
|
22
|
+
`loki start --deploy [--health-url U] [--auto-rollback]` chains the same gated command after a start.
|
|
13
23
|
|
|
14
24
|
Everything below is the original print-only design plan and is kept for history.
|
|
15
25
|
|
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.10.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
|
@@ -37,7 +37,7 @@ loki workspace status <run-id>
|
|
|
37
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.
|
|
38
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`.
|
|
39
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.
|
|
40
|
+
- `loki workspace infer <name>` prints suggested `after:` edges read from package.json, go.mod and pyproject.toml dependencies on other listed repos. It also suggests an edge from (1) a source import (TS/JS import or require, Python import, Go import path) of a sibling's package or module name even when the manifest does not declare it, scanning at most 400 source files per repo, and (2) a client generator config (openapitools.json, openapi-generator*.yaml, orval.config.*) whose input spec path lies inside a sibling checkout. Each inferred edge prints its evidence as `file:line`. It is print-only and cycle-checked; it never edits loki.yaml.
|
|
41
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
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.
|
|
43
43
|
- Without `path`, a repo is cloned to `~/.loki/repos/owner__name`.
|
|
@@ -39,7 +39,7 @@ model explicitly rather than inherit the session model.
|
|
|
39
39
|
- **Progressive Disclosure**: 3-layer loading (index, timeline, full details)
|
|
40
40
|
- **Token Economics**: Discovery vs read token tracking
|
|
41
41
|
- **Vector Search**: Optional embedding-based similarity (sentence-transformers)
|
|
42
|
-
- **Cross-project +
|
|
42
|
+
- **Cross-project index + knowledge graph**: `memory/cross_project.py`, `memory/knowledge_graph.py` (added v7.x)
|
|
43
43
|
- **Managed memory client**: `memory/managed_memory/` -- gated on `LOKI_MANAGED_MEMORY=true`. See `skills/memory.md`.
|
|
44
44
|
- **CLI**: `loki memory index|timeline|consolidate|economics|retrieve|episode|pattern|skill|vectors`
|
|
45
45
|
- **API**: REST endpoints at `/api/memory/*`
|
package/docs/sentry-intake.md
CHANGED
|
@@ -23,7 +23,7 @@ The token is sent only to the Sentry host through a curl config on stdin, never
|
|
|
23
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
24
|
|
|
25
25
|
|
|
26
|
-
## Slack and
|
|
26
|
+
## Slack, Linear, Jira and Teams delegation
|
|
27
27
|
|
|
28
28
|
`loki trigger` server setup for delegating a job from chat or a tracker. Both routes enqueue through the same job queue and runner as `POST /jobs`; a completion summary goes back to the origin. Nothing unauthenticated can start a job or choose a reply URL.
|
|
29
29
|
|
|
@@ -43,4 +43,18 @@ Linear (`POST /linear`):
|
|
|
43
43
|
3. A created issue enqueues `loki start linear:<identifier> --detach` and answers 202 with the job id. Other events are skipped.
|
|
44
44
|
4. If `LINEAR_API_KEY` is set, a comment with the result is posted on the issue through `https://api.linear.app/graphql`. Without it nothing is posted.
|
|
45
45
|
|
|
46
|
+
Jira (`POST /jira`):
|
|
47
|
+
|
|
48
|
+
1. Set `JIRA_WEBHOOK_SECRET` (the secret on a Jira Cloud webhook) and point the webhook at `/jira`. Set `JIRA_ASSIGNEES` (comma-separated accountId, email or display name) and/or `JIRA_LABELS`. With neither set, every event is skipped.
|
|
49
|
+
2. `X-Hub-Signature` is `sha256=` + hex HMAC-SHA256 of the raw body. Bad or missing signature: 401. Jira signs no timestamp and the `X-Atlassian-Webhook-Identifier` header is not covered by the HMAC, so replay is bounded two ways: a repeated delivery id, or a repeated signed body (sha256 of the raw body, so changing or dropping the header does not help), answers `duplicate` and queues nothing. A replay of a body Jira never sent twice still needs the secret to forge.
|
|
50
|
+
3. `jira:issue_created`, or `jira:issue_updated` whose changelog touches assignee or labels, runs only when the issue is assigned to a configured user or has a configured label. The key, summary and description are folded into a one-line brief (1500 bytes max; an Atlassian Document Format description is flattened to its text) and run as `loki start --brief "<brief>" --pr --detach`. Answers 202 with the job id.
|
|
51
|
+
|
|
52
|
+
Teams (`POST /teams`):
|
|
53
|
+
|
|
54
|
+
1. Set `TEAMS_SECURITY_TOKEN` (the base64 token Teams shows when you create the outgoing webhook) and point the webhook at `/teams`.
|
|
55
|
+
2. `Authorization: HMAC <base64>` is the base64 HMAC-SHA256 of the raw body keyed by the base64-decoded token. Bad signature: 401. The activity `timestamp` must be within 300s; a stale or missing one is refused with 401.
|
|
56
|
+
3. The message text (with `<at>` mentions and tags removed) is the brief and runs as `loki start --brief "<brief>" --pr --detach`. Teams gets a synchronous reply whose text starts with "Queued" and ends with the job id. A repeated activity id is not queued twice.
|
|
57
|
+
|
|
58
|
+
Jira and Teams runs are receipt-gated PR runs only: the server always passes `--pr` and never a deploy argument, so shipping stays a separate gated step. `--brief` forces brief mode, so text that ends in `.md` or contains an issue URL is never reclassified, and `loki start --brief ... --pr/--worktree/--detach` runs in an isolated worktree, detached, and opens a PR carrying the Evidence Receipt (the same machinery as `loki start <issue>`). Sentry alerts use the same form. Neither secret reaches the `loki start` child environment, and both must differ from every other credential. A missing secret answers 503 naming the variable.
|
|
59
|
+
|
|
46
60
|
Reply targets are stored server side keyed by job id, captured only after verification. Notifier errors are logged and never fail the job. Poll status with `GET /jobs/<id>`.
|
package/docs/v10/D61-SPEED.md
CHANGED
|
@@ -101,3 +101,5 @@ Group exit codes: 0 = every unit VERIFIED and the group receipt VERIFIED; 4 = NO
|
|
|
101
101
|
Resume: rerun the same `loki start`. Each milestone group is found again by the hash of its body, so finished milestones re-verify and return 0 fast and the run continues at the failed one. No extra state is kept. `LOKI_GROUP_RESUME=0` disables auto-resume and starts every group fresh. Check any group receipt with `loki verify .loki/groups/<gid>/receipt.json`.
|
|
102
102
|
|
|
103
103
|
Test command precedence: `LOKI_GROUP_TEST_CMD` (whitespace split) wins over the repo's own test script. Test: `loki-ts/tests/engine10/route_group.test.ts`.
|
|
104
|
+
|
|
105
|
+
Fix units: when group integration stops on a merge conflict or a failing suite, one bounded fix unit runs through the same unit launcher (same scoped env, one slot) from the group branch, then integration restarts once. A conflict fix takes the conflicting unit's place; a suite fix goes last and receives the failing test names and output tail. Max one fix per conflict and 2 per group; `LOKI_GROUP_FIX_UNITS=0` restores stop-and-name. The suite, every unit receipt and the group Seal still decide the verdict, and a failed fix keeps the stop-and-name output (exit 1). A fix unit is stored in `dag.json` and resumes like any unit. Code: `loki-ts/src/features/speed/fix_units.ts`; tests: `loki-ts/tests/engine10/seal_group.test.ts`.
|