loki-mode 11.7.1 → 11.8.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 +37 -2
- package/autonomy/lib/chat.sh +3 -2
- package/autonomy/loki +1 -1
- package/autonomy/trigger-server.py +269 -2
- package/autonomy/verify.sh +5 -0
- package/completions/loki.bash +2 -2
- package/dashboard/__init__.py +1 -1
- package/docs/CLI-REFERENCE.md +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/docs/sentry-intake.md +22 -0
- package/docs/v10/D61-SPEED.md +12 -0
- package/docs/v10/PROGRESS.md +10 -0
- package/loki-ts/dist/loki.js +895 -878
- package/mcp/__init__.py +1 -1
- package/package.json +2 -2
- package/packages/control-plane/dist/server.js +277 -271
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/web-app/dist/assets/{AdminPage-S3BYeapm.js → AdminPage-BnIg9ilA.js} +1 -1
- package/web-app/dist/assets/{Avatar-C1rHwLgn.js → Avatar-0BTVsQ2A.js} +1 -1
- package/web-app/dist/assets/{Badge-CzlnN5aQ.js → Badge-Bbq8CXgn.js} +1 -1
- package/web-app/dist/assets/{Button-B1w5AeGC.js → Button-CKD4n0ld.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-Dyeqjfkc.js → CockpitPage-DJhLNLAz.js} +1 -1
- package/web-app/dist/assets/{ComparePage-C6y2GkGR.js → ComparePage-Di2akuII.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-Dg2fcGkR.js → ErrorBoundary-9lhsYG-z.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-CxbyjvTj.js → EvidenceReceiptPanel-CQDGXvDr.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-Dpys1xDb.js → GitHubIssuesPanel-Y_w2k9Ej.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-CRVjWoFl.js → GitHubPRsPanel-pMSQedCv.js} +1 -1
- package/web-app/dist/assets/{HomePage-BKnv-oYw.js → HomePage-CIAkiGdt.js} +2 -2
- package/web-app/dist/assets/{LoginPage-BGksmFRd.js → LoginPage-BES4Pk6k.js} +1 -1
- package/web-app/dist/assets/{MagicPage-OCvhp16M.js → MagicPage-B154fMhi.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-CJOJpXWC.js → MetricsPage-BcNyaiUO.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-C04p3QvN.js → NotFoundPage-D28FqYxG.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-BRB8txsY.js → ProjectPage-C_O3u8Jx.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-CoUYeQEh.js → ProjectsPage-uqdahuPA.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-BkjmJGfM.js → SettingsPage-DLvVA9OZ.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-B4vU9TjQ.js → ShowcasePage-BAZP8vsm.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-Z2jiVsni.js → SystemSettingsPage-DPYZkLWh.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-Ulm8SISq.js → TeamsPage-BgSyb5Pd.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-C_7MM6Ey.js → TemplatesPage-D_Jh5VU1.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-Cumd7Tgs.js → TerminalOutput-4v8poDKb.js} +1 -1
- package/web-app/dist/assets/{activity-CZAUkbiY.js → activity-CE2b6kdw.js} +1 -1
- package/web-app/dist/assets/{bell-BOvSXpEr.js → bell-CxJDlLSR.js} +1 -1
- package/web-app/dist/assets/{bot-DOTZxBYj.js → bot-C2WuK4zw.js} +1 -1
- package/web-app/dist/assets/{check-BxpE0B5u.js → check-81epVldw.js} +1 -1
- package/web-app/dist/assets/{chevron-left-D6KPvBOA.js → chevron-left-B6EXh_3C.js} +1 -1
- package/web-app/dist/assets/{circle-alert-Bajq86oe.js → circle-alert-0iqoo-iy.js} +1 -1
- package/web-app/dist/assets/{clock-CnlHAVJc.js → clock-CFrBpNUp.js} +1 -1
- package/web-app/dist/assets/{cloud-DjaymjwM.js → cloud-BBSFhUT_.js} +1 -1
- package/web-app/dist/assets/{code-xml-BADEw2Ld.js → code-xml-BCU2V1rq.js} +1 -1
- package/web-app/dist/assets/{database-B4aKb5Cq.js → database-D2hvO52K.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-DrldXpja.js → dollar-sign-CXFA9b6T.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-DVebDVR-.js → file-code-corner-CqTCEEfS.js} +1 -1
- package/web-app/dist/assets/{file-plus-BD-N_-Tl.js → file-plus-DYFbkGbs.js} +1 -1
- package/web-app/dist/assets/{globe-C7ieInC-.js → globe-DpYHLobk.js} +1 -1
- package/web-app/dist/assets/{hammer-BSugKIji.js → hammer-CZ97SEYo.js} +1 -1
- package/web-app/dist/assets/{index-nLQgfvOn.js → index-DzswgpRo.js} +3 -3
- package/web-app/dist/assets/{layers-CHXxU2on.js → layers-2Tq0edx4.js} +1 -1
- package/web-app/dist/assets/{loader-circle-BEEz0LbD.js → loader-circle-BFP4kUX-.js} +1 -1
- package/web-app/dist/assets/{lock-CzQGfcnE.js → lock-DkKdZH4d.js} +1 -1
- package/web-app/dist/assets/{package-CcHYQLGd.js → package-ChOGD8yf.js} +1 -1
- package/web-app/dist/assets/{plus-DdzgFEXn.js → plus-hggB1o6b.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-7x0HbH9Y.js → refresh-cw-DYwVLOeF.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-DBahX1Sp.js → rotate-ccw-CUdY506q.js} +1 -1
- package/web-app/dist/assets/{scroll-text-BB8y8a9Z.js → scroll-text-Wvidz5uN.js} +1 -1
- package/web-app/dist/assets/{server-CfkbW2MH.js → server-O11QmkCN.js} +1 -1
- package/web-app/dist/assets/{shield-alert-DUgD8rBF.js → shield-alert-DXXx27uV.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-DGoyY6QC.js → thumbs-up-DF8ivs3A.js} +1 -1
- package/web-app/dist/assets/{trash-2-B6xrSUMr.js → trash-2-LA6PW0Nf.js} +1 -1
- package/web-app/dist/assets/{trending-down-B6RGKTOx.js → trending-down-Bw_vLKHT.js} +1 -1
- package/web-app/dist/assets/{trending-up-B44DCrt1.js → trending-up-tX8motha.js} +1 -1
- package/web-app/dist/assets/{upload-4kWm33ZY.js → upload-CxtTWJd2.js} +1 -1
- package/web-app/dist/assets/{usePolling-zkUtRQG6.js → usePolling-D-P5Iofq.js} +1 -1
- package/web-app/dist/assets/{user-04cEVIe2.js → user-dKTcuUFq.js} +1 -1
- package/web-app/dist/index.html +1 -1
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: An autonomous software factory that knows what it is supposed to deliver, and proves it did. Use when the user says Loki Mode or asks to build, fix or verify software autonomously.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v11.
|
|
6
|
+
# Loki Mode v11.8.1
|
|
7
7
|
|
|
8
8
|
**Current entry points (Loki 10 engine):** `loki "<task>"`, `loki owner/repo#N` and `loki quick "<task>"` run the Loki 10 engine and end with a signed receipt (`loki verify`). `loki backlog owner/repo --all` runs many issues in parallel. Guide: `docs/v10/GUIDE.md`. The skill instructions below, and `loki start`, describe the previous bash loop. PRD-file starts (`loki start ./prd.md`) and the opencode provider still run on it until FC38-SWEEP ports them; the legacy-engine command and the LOKI_ENGINE switch remain removed. Prefer `loki owner/repo#N` for issues.
|
|
9
9
|
|
|
@@ -483,4 +483,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
483
483
|
|
|
484
484
|
---
|
|
485
485
|
|
|
486
|
-
**v11.
|
|
486
|
+
**v11.8.1 | [Autonomi](https://www.autonomi.dev/) flagship product | core skill**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
11.
|
|
1
|
+
11.8.1
|
package/autonomy/lib/backlog.py
CHANGED
|
@@ -195,7 +195,8 @@ def load_dag(path, group):
|
|
|
195
195
|
if task.lstrip().startswith("-"):
|
|
196
196
|
raise ValueError("unit %s task text starts with '-' and would be read as an option" % uid)
|
|
197
197
|
units.append({"n": n, "id": uid, "deps": deps[uid], "write_set": list(u.get("writeSet") or []),
|
|
198
|
-
"task": task
|
|
198
|
+
"task": task, "done": u.get("done") is True,
|
|
199
|
+
"spec": u["spec"] if isinstance(u.get("spec"), str) and os.path.isabs(u["spec"]) else ""})
|
|
199
200
|
return group, units
|
|
200
201
|
|
|
201
202
|
|
|
@@ -313,7 +314,10 @@ def main(argv):
|
|
|
313
314
|
"cmd": [launcher, u["task"], "--no-pr"],
|
|
314
315
|
"deps": [key_of[d] for d in u["deps"]],
|
|
315
316
|
"env": {"LOKI_GROUP_ID": group, "LOKI_UNIT_ID": u["id"], "LOKI_UNIT_N": str(u["n"]),
|
|
316
|
-
"LOKI_UNIT_DEPS": ",".join(u["deps"]), "LOKI_UNIT_WRITE_SET": "\n".join(u["write_set"])}
|
|
317
|
+
"LOKI_UNIT_DEPS": ",".join(u["deps"]), "LOKI_UNIT_WRITE_SET": "\n".join(u["write_set"])},
|
|
318
|
+
"done": u["done"]}
|
|
319
|
+
if u["spec"]: # frozen group acceptance for unit_mode (wall.ts)
|
|
320
|
+
spec[k]["env"].update(LOKI_UNIT_SPEC=u["spec"], LOKI_SPEED="1")
|
|
317
321
|
state = {k: {"status": "queued", "ok": None, "cost": None, "ran": False} for k in spec}
|
|
318
322
|
for k in state:
|
|
319
323
|
say("backlog: %s queued" % spec[k]["label"])
|
|
@@ -331,6 +335,32 @@ def main(argv):
|
|
|
331
335
|
signal.signal(signal.SIGTERM, stop_children)
|
|
332
336
|
signal.signal(signal.SIGINT, stop_children)
|
|
333
337
|
|
|
338
|
+
def harvest(k, wt):
|
|
339
|
+
"""Copy the newest unit receipt and events into the group dir before the worktree goes."""
|
|
340
|
+
gid = spec[k]["env"].get("LOKI_GROUP_ID")
|
|
341
|
+
uid = spec[k]["env"].get("LOKI_UNIT_ID")
|
|
342
|
+
if not (spec[k]["unit"] and gid and uid):
|
|
343
|
+
return
|
|
344
|
+
try:
|
|
345
|
+
runs = os.path.join(wt, ".loki", "runs")
|
|
346
|
+
cands = [os.path.join(runs, d) for d in os.listdir(runs)
|
|
347
|
+
if not os.path.islink(os.path.join(runs, d))
|
|
348
|
+
and os.path.isfile(os.path.join(runs, d, "receipt.json"))]
|
|
349
|
+
if not cands:
|
|
350
|
+
return
|
|
351
|
+
src_dir = max(cands, key=lambda d: os.path.getmtime(os.path.join(d, "receipt.json")))
|
|
352
|
+
dst_dir = os.path.join(top, ".loki", "groups", gid, "group", "units", uid)
|
|
353
|
+
os.makedirs(dst_dir, mode=0o700, exist_ok=True)
|
|
354
|
+
for name in ("receipt.json", "events.jsonl"):
|
|
355
|
+
src, dst = os.path.join(src_dir, name), os.path.join(dst_dir, name)
|
|
356
|
+
if os.path.islink(src) or os.path.islink(dst) or not os.path.isfile(src):
|
|
357
|
+
continue
|
|
358
|
+
tmp = dst + ".tmp%d" % os.getpid()
|
|
359
|
+
shutil.copyfile(src, tmp)
|
|
360
|
+
os.replace(tmp, dst)
|
|
361
|
+
except OSError:
|
|
362
|
+
pass
|
|
363
|
+
|
|
334
364
|
def finish(k):
|
|
335
365
|
nonlocal unmeasured
|
|
336
366
|
p, log, wt = running.pop(k)
|
|
@@ -348,6 +378,7 @@ def main(argv):
|
|
|
348
378
|
add_spend(usd)
|
|
349
379
|
state[k].update(status=status, ok=ok, cost=usd)
|
|
350
380
|
say("backlog: %s %s" % (spec[k]["label"], status))
|
|
381
|
+
harvest(k, wt)
|
|
351
382
|
if ok:
|
|
352
383
|
git("worktree", "remove", "--force", wt, cwd=top)
|
|
353
384
|
if not spec[k]["unit"]: # a passing unit branch is kept for the integrator
|
|
@@ -414,6 +445,10 @@ def main(argv):
|
|
|
414
445
|
k = next_ready()
|
|
415
446
|
if k is None:
|
|
416
447
|
break
|
|
448
|
+
if spec[k].get("done"):
|
|
449
|
+
state[k].update(status="VERIFIED (resumed)", ok=True)
|
|
450
|
+
say("backlog: %s VERIFIED (resumed)" % spec[k]["label"])
|
|
451
|
+
continue
|
|
417
452
|
launch(k)
|
|
418
453
|
if running:
|
|
419
454
|
time.sleep(0.2)
|
package/autonomy/lib/chat.sh
CHANGED
|
@@ -47,7 +47,7 @@ Loki chat commands:
|
|
|
47
47
|
/attach attach to the current or most recent loki start run in this repo
|
|
48
48
|
/quick <task> loki quick
|
|
49
49
|
/edit <change> apply one change (build mode for this turn)
|
|
50
|
-
/verify
|
|
50
|
+
/verify [path] [--max-age 7d] loki verify (a receipt path, group receipt, or age limit)
|
|
51
51
|
/receipt loki receipt
|
|
52
52
|
/model <id> set model for this session
|
|
53
53
|
/provider <name> switch provider (claude, codex, cline, aider, opencode)
|
|
@@ -351,7 +351,8 @@ Question: $line" || true
|
|
|
351
351
|
/edit)
|
|
352
352
|
[ -n "$rest" ] || { echo "usage: /edit <change>"; continue; }
|
|
353
353
|
_chat_send build "$rest" ;;
|
|
354
|
-
/verify)
|
|
354
|
+
/verify) # shellcheck disable=SC2086
|
|
355
|
+
_chat_do "$loki" verify $rest ;;
|
|
355
356
|
/receipt) _chat_do "$loki" receipt ;;
|
|
356
357
|
/cost) _chat_do "$loki" cost ;;
|
|
357
358
|
/undo) # shellcheck disable=SC2086
|
package/autonomy/loki
CHANGED
|
@@ -18134,7 +18134,7 @@ cmd_verify() {
|
|
|
18134
18134
|
# verified by the engine10 verifier directly; bin/loki sends a bare verify back here.
|
|
18135
18135
|
for _pk_arg in "$@"; do
|
|
18136
18136
|
case "$_pk_arg" in
|
|
18137
|
-
composite.json|*/composite.json|--seal-composite|--seal-composite
|
|
18137
|
+
composite.json|*/composite.json|--seal-composite|--seal-composite=*|--max-age|--max-age=*|.loki/groups/*/receipt.json|*/.loki/groups/*/receipt.json)
|
|
18138
18138
|
_loki_bun_delegate engine10 verify "$@"
|
|
18139
18139
|
return $? ;;
|
|
18140
18140
|
esac
|
|
@@ -55,8 +55,10 @@ import subprocess
|
|
|
55
55
|
import sys
|
|
56
56
|
import threading
|
|
57
57
|
import time
|
|
58
|
+
import urllib.parse
|
|
58
59
|
from datetime import datetime
|
|
59
60
|
from pathlib import Path
|
|
61
|
+
from urllib.request import Request, urlopen
|
|
60
62
|
|
|
61
63
|
# Receipt attestation. Imported by path because trigger-server.py runs as a
|
|
62
64
|
# script from an arbitrary cwd, so a bare `import receipt_jwt` would depend on
|
|
@@ -412,6 +414,10 @@ CHILD_ENV_DENYLIST = (
|
|
|
412
414
|
"LOKI_API_TOKEN_FILE",
|
|
413
415
|
"LOKI_SENTRY_WEBHOOK_SECRET",
|
|
414
416
|
"SENTRY_CLIENT_SECRET",
|
|
417
|
+
"SLACK_SIGNING_SECRET",
|
|
418
|
+
"SLACK_BOT_TOKEN",
|
|
419
|
+
"LINEAR_WEBHOOK_SECRET",
|
|
420
|
+
"LINEAR_API_KEY",
|
|
415
421
|
)
|
|
416
422
|
|
|
417
423
|
|
|
@@ -622,6 +628,110 @@ def validate_sentry_signature(secret, body, signature_header):
|
|
|
622
628
|
return constant_time_equals(expected, signature_header.strip().lower())
|
|
623
629
|
|
|
624
630
|
|
|
631
|
+
# Slack and Linear delegation. Same signed-body contract as
|
|
632
|
+
# loki-ts/src/contrib/slack_inbound.ts (signSlackBody/verifySlackSignature).
|
|
633
|
+
SIGNATURE_MAX_SKEW_S = 300
|
|
634
|
+
SLACK_REPLY_PREFIX = "https://hooks.slack.com/"
|
|
635
|
+
LINEAR_API_URL = "https://api.linear.app/graphql"
|
|
636
|
+
LINEAR_IDENTIFIER_RE = re.compile(r"^[A-Za-z][A-Za-z0-9]*-\d+$")
|
|
637
|
+
NOTIFY_TIMEOUT_SECONDS = 10
|
|
638
|
+
NOTIFY_TEXT_MAX = 1000
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def sign_slack_body(secret, timestamp, body):
|
|
642
|
+
"""`v0=` + hex HMAC-SHA256 over `v0:<timestamp>:<raw body>` (bytes)."""
|
|
643
|
+
base = b"v0:" + str(timestamp).encode("utf-8") + b":" + body
|
|
644
|
+
return "v0=" + hmac.new(secret.encode("utf-8"), base, hashlib.sha256).hexdigest()
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def verify_slack_signature(secret, timestamp, body, signature, now=None):
|
|
648
|
+
"""Fail closed: no secret, header, numeric timestamp or fresh window -> False."""
|
|
649
|
+
if not secret or not timestamp or not signature:
|
|
650
|
+
return False
|
|
651
|
+
if not re.fullmatch(r"[0-9]+", timestamp):
|
|
652
|
+
return False
|
|
653
|
+
now = time.time() if now is None else now
|
|
654
|
+
if abs(now - int(timestamp)) > SIGNATURE_MAX_SKEW_S:
|
|
655
|
+
return False
|
|
656
|
+
return constant_time_equals(sign_slack_body(secret, timestamp, body), signature)
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def verify_linear_signature(secret, body, signature):
|
|
660
|
+
"""Linear-Signature: hex HMAC-SHA256 of the raw body. Freshness is checked
|
|
661
|
+
separately against the signed body's webhookTimestamp (linear_fresh)."""
|
|
662
|
+
if not secret or not signature:
|
|
663
|
+
return False
|
|
664
|
+
expected = hmac.new(secret.encode("utf-8"), body, hashlib.sha256).hexdigest()
|
|
665
|
+
return constant_time_equals(expected, signature.strip().lower())
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def linear_fresh(payload, now=None):
|
|
669
|
+
ts_val = payload.get("webhookTimestamp")
|
|
670
|
+
if isinstance(ts_val, bool) or not isinstance(ts_val, (int, float)):
|
|
671
|
+
return False
|
|
672
|
+
secs = ts_val / 1000.0 if ts_val > 1e11 else float(ts_val)
|
|
673
|
+
now = time.time() if now is None else now
|
|
674
|
+
return abs(now - secs) <= SIGNATURE_MAX_SKEW_S
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def resolve_chat_secrets(env, secret, api_token, sentry_secret):
|
|
678
|
+
"""Return (slack_secret, linear_secret), blanking any that equals another
|
|
679
|
+
credential: each authenticates a different party and MUST differ."""
|
|
680
|
+
slack = env.get("SLACK_SIGNING_SECRET", "")
|
|
681
|
+
linear = env.get("LINEAR_WEBHOOK_SECRET", "")
|
|
682
|
+
others = [x for x in (secret, api_token, sentry_secret) if x]
|
|
683
|
+
slack_bad = bool(slack) and any(
|
|
684
|
+
constant_time_equals(slack, o) for o in others + ([linear] if linear else []))
|
|
685
|
+
linear_bad = bool(linear) and any(
|
|
686
|
+
constant_time_equals(linear, o) for o in others + ([slack] if slack else []))
|
|
687
|
+
if slack_bad:
|
|
688
|
+
logging.error("The Slack secret is identical to another credential. "
|
|
689
|
+
"They MUST differ; disabling /slack.")
|
|
690
|
+
if linear_bad:
|
|
691
|
+
logging.error("The Linear secret is identical to another credential. "
|
|
692
|
+
"They MUST differ; disabling /linear.")
|
|
693
|
+
return ("" if slack_bad else slack), ("" if linear_bad else linear)
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def _post_json(url, data, headers):
|
|
697
|
+
"""POST JSON with a bounded timeout. Raises on any failure; callers log."""
|
|
698
|
+
req = Request(url, data=json.dumps(data).encode("utf-8"),
|
|
699
|
+
headers=dict(headers, **{"Content-Type": "application/json"}),
|
|
700
|
+
method="POST")
|
|
701
|
+
with urlopen(req, timeout=NOTIFY_TIMEOUT_SECONDS) as resp:
|
|
702
|
+
resp.read()
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def notify_job_complete(target, job_id, status, summary):
|
|
706
|
+
"""Send a completion summary to a reply target captured after verification.
|
|
707
|
+
|
|
708
|
+
Never raises: a notifier failure must not fail the job. The Slack URL is
|
|
709
|
+
re-checked here as defense in depth.
|
|
710
|
+
"""
|
|
711
|
+
if not isinstance(target, dict):
|
|
712
|
+
return
|
|
713
|
+
text = ("loki job %s finished: %s. %s" % (job_id, status, summary or "")).strip()
|
|
714
|
+
text = CONTROL_CHARS_RE.sub(" ", text)[:NOTIFY_TEXT_MAX]
|
|
715
|
+
try:
|
|
716
|
+
if target.get("kind") == "slack":
|
|
717
|
+
url = target.get("response_url", "")
|
|
718
|
+
if not (isinstance(url, str) and url.startswith(SLACK_REPLY_PREFIX)):
|
|
719
|
+
return
|
|
720
|
+
_post_json(url, {"response_type": "in_channel", "text": text}, {})
|
|
721
|
+
elif target.get("kind") == "linear":
|
|
722
|
+
key = os.environ.get("LINEAR_API_KEY", "")
|
|
723
|
+
issue_id = target.get("issue_id", "")
|
|
724
|
+
if not key or not isinstance(issue_id, str) or not issue_id:
|
|
725
|
+
return
|
|
726
|
+
_post_json(LINEAR_API_URL, {
|
|
727
|
+
"query": "mutation($i:String!,$b:String!){commentCreate(input:"
|
|
728
|
+
"{issueId:$i,body:$b}){success}}",
|
|
729
|
+
"variables": {"i": issue_id, "b": text},
|
|
730
|
+
}, {"Authorization": key})
|
|
731
|
+
except Exception as e: # notifier errors never fail a job
|
|
732
|
+
logging.warning("Completion notify for job %s failed: %s", job_id, e)
|
|
733
|
+
|
|
734
|
+
|
|
625
735
|
def normalize_sentry_payload(payload):
|
|
626
736
|
"""Run the existing normalize_sentry_json over a Sentry webhook payload.
|
|
627
737
|
|
|
@@ -750,6 +860,9 @@ class Dispatcher:
|
|
|
750
860
|
self._job_max = max(1, job_history)
|
|
751
861
|
self._jobs = collections.OrderedDict()
|
|
752
862
|
self._jobs_lock = threading.Lock()
|
|
863
|
+
# Reply targets (Slack response_url / Linear issue) keyed by job_id,
|
|
864
|
+
# written only by the verified /slack and /linear handlers.
|
|
865
|
+
self._reply_targets = collections.OrderedDict()
|
|
753
866
|
# Proof-attribution bookkeeping. The proof pointer is global to the
|
|
754
867
|
# working directory, so a receipt only identifies a job when that job
|
|
755
868
|
# was the only build that could have written it. All three are updated
|
|
@@ -887,6 +1000,17 @@ class Dispatcher:
|
|
|
887
1000
|
if summary:
|
|
888
1001
|
entry["summary"] = summary
|
|
889
1002
|
|
|
1003
|
+
def set_reply_target(self, job_id, target):
|
|
1004
|
+
"""Store a verified reply target server side, bounded like _jobs."""
|
|
1005
|
+
with self._jobs_lock:
|
|
1006
|
+
self._reply_targets[job_id] = target
|
|
1007
|
+
while len(self._reply_targets) > self._job_max:
|
|
1008
|
+
self._reply_targets.popitem(last=False)
|
|
1009
|
+
|
|
1010
|
+
def pop_reply_target(self, job_id):
|
|
1011
|
+
with self._jobs_lock:
|
|
1012
|
+
return self._reply_targets.pop(job_id, None)
|
|
1013
|
+
|
|
890
1014
|
def get_job(self, job_id):
|
|
891
1015
|
"""Return a copy of the job record, or None if unknown/evicted.
|
|
892
1016
|
|
|
@@ -980,6 +1104,9 @@ class Dispatcher:
|
|
|
980
1104
|
)
|
|
981
1105
|
if job_id:
|
|
982
1106
|
self.record_job(job_id, status, summary or "")
|
|
1107
|
+
target = self.pop_reply_target(job_id)
|
|
1108
|
+
if target is not None:
|
|
1109
|
+
notify_job_complete(target, job_id, status, summary)
|
|
983
1110
|
finally:
|
|
984
1111
|
self.queue.task_done()
|
|
985
1112
|
|
|
@@ -998,6 +1125,10 @@ class WebhookHandler(http.server.BaseHTTPRequestHandler):
|
|
|
998
1125
|
api_token = ""
|
|
999
1126
|
# Sentry integration client secret for /sentry. Distinct from both above.
|
|
1000
1127
|
sentry_secret = ""
|
|
1128
|
+
# Slack signing secret (/slack) and Linear webhook secret (/linear), each
|
|
1129
|
+
# distinct from every other credential.
|
|
1130
|
+
slack_secret = ""
|
|
1131
|
+
linear_secret = ""
|
|
1001
1132
|
dispatcher = None
|
|
1002
1133
|
# Ed25519 key that attests receipts, and the retired public keys that keep
|
|
1003
1134
|
# pre-rotation receipts verifiable. Both default to unconfigured, which
|
|
@@ -1278,8 +1409,19 @@ class WebhookHandler(http.server.BaseHTTPRequestHandler):
|
|
|
1278
1409
|
self._send_json(400, {"error": "invalid spec"})
|
|
1279
1410
|
return
|
|
1280
1411
|
|
|
1412
|
+
job_id = self._enqueue_job(spec.strip())
|
|
1413
|
+
if job_id is None:
|
|
1414
|
+
return
|
|
1415
|
+
self._send_json(202, {"id": job_id, "status": "queued"})
|
|
1416
|
+
|
|
1417
|
+
def _enqueue_job(self, spec, reply_target=None):
|
|
1418
|
+
"""Record and enqueue one validated job; shared by /jobs, /slack, /linear.
|
|
1419
|
+
|
|
1420
|
+
Returns the job id, or None after sending 429 when the queue is full.
|
|
1421
|
+
reply_target must come from an already-verified request.
|
|
1422
|
+
"""
|
|
1281
1423
|
job_id = secrets.token_urlsafe(12)
|
|
1282
|
-
job = {"spec": spec
|
|
1424
|
+
job = {"spec": spec, "job_id": job_id}
|
|
1283
1425
|
|
|
1284
1426
|
# Record BEFORE enqueueing. A worker can pick the job up the instant
|
|
1285
1427
|
# submit() returns and write "running"/"fired"; record_job is
|
|
@@ -1288,6 +1430,8 @@ class WebhookHandler(http.server.BaseHTTPRequestHandler):
|
|
|
1288
1430
|
# forever. On the 429 path below the record is unreachable (no id is
|
|
1289
1431
|
# handed out) and the store is bounded, so nothing leaks.
|
|
1290
1432
|
self.dispatcher.record_job(job_id, "queued", job["spec"])
|
|
1433
|
+
if reply_target is not None:
|
|
1434
|
+
self.dispatcher.set_reply_target(job_id, reply_target)
|
|
1291
1435
|
|
|
1292
1436
|
# Enforce the existing queue bound so a submit storm cannot fork
|
|
1293
1437
|
# unbounded builds. 429 (not a silent drop) tells the client to retry.
|
|
@@ -1295,11 +1439,111 @@ class WebhookHandler(http.server.BaseHTTPRequestHandler):
|
|
|
1295
1439
|
logging.warning(
|
|
1296
1440
|
"Queue full; shedding job submit from %s", self.address_string()
|
|
1297
1441
|
)
|
|
1442
|
+
self.dispatcher.pop_reply_target(job_id)
|
|
1298
1443
|
log_event(JOB_EVENT, "", "", "rejected (queue full)")
|
|
1299
1444
|
self._send_json(429, {"error": "server busy, retry later"})
|
|
1300
|
-
return
|
|
1445
|
+
return None
|
|
1301
1446
|
|
|
1302
1447
|
log_event(JOB_EVENT, "", job["spec"], "queued")
|
|
1448
|
+
return job_id
|
|
1449
|
+
|
|
1450
|
+
def _handle_slack(self):
|
|
1451
|
+
"""POST /slack -- signed slash command; enqueues a job, ephemeral reply."""
|
|
1452
|
+
body = self._read_body()
|
|
1453
|
+
if body is None:
|
|
1454
|
+
return
|
|
1455
|
+
if not self.slack_secret:
|
|
1456
|
+
log_event(JOB_EVENT, "", "", "rejected (no slack signing secret configured)")
|
|
1457
|
+
self._send_json(503, {"error": "SLACK_SIGNING_SECRET not configured"})
|
|
1458
|
+
return
|
|
1459
|
+
if not verify_slack_signature(
|
|
1460
|
+
self.slack_secret,
|
|
1461
|
+
self.headers.get("X-Slack-Request-Timestamp", ""),
|
|
1462
|
+
body, self.headers.get("X-Slack-Signature", "")):
|
|
1463
|
+
logging.warning("Invalid Slack signature from %s", self.address_string())
|
|
1464
|
+
log_event(JOB_EVENT, "", "", "rejected (invalid slack signature)")
|
|
1465
|
+
self._send_json(401, {"error": "invalid signature"})
|
|
1466
|
+
return
|
|
1467
|
+
# Verified from here on.
|
|
1468
|
+
ctype = (self.headers.get("Content-Type", "") or "").lower()
|
|
1469
|
+
if "json" in ctype:
|
|
1470
|
+
try:
|
|
1471
|
+
data = json.loads(body)
|
|
1472
|
+
except ValueError:
|
|
1473
|
+
self._send_json(400, {"error": "invalid JSON"})
|
|
1474
|
+
return
|
|
1475
|
+
if isinstance(data, dict) and data.get("type") == "url_verification":
|
|
1476
|
+
self._send_json(200, {"challenge": str(data.get("challenge", ""))})
|
|
1477
|
+
return
|
|
1478
|
+
self._send_json(400, {"error": "unsupported slack payload"})
|
|
1479
|
+
return
|
|
1480
|
+
form = urllib.parse.parse_qs(body.decode("utf-8", errors="replace"))
|
|
1481
|
+
text = (form.get("text") or [""])[0]
|
|
1482
|
+
if not valid_spec(text):
|
|
1483
|
+
log_event(JOB_EVENT, "", "", "rejected (invalid slack spec)")
|
|
1484
|
+
self._send_json(200, {"response_type": "ephemeral",
|
|
1485
|
+
"text": "Usage: /loki <spec>. Spec must be a "
|
|
1486
|
+
"one-line brief or ref, not starting with '-'."})
|
|
1487
|
+
return
|
|
1488
|
+
response_url = (form.get("response_url") or [""])[0]
|
|
1489
|
+
target = {"kind": "slack"}
|
|
1490
|
+
if response_url.startswith(SLACK_REPLY_PREFIX):
|
|
1491
|
+
target["response_url"] = response_url
|
|
1492
|
+
job_id = self._enqueue_job(text.strip(), target)
|
|
1493
|
+
if job_id is None:
|
|
1494
|
+
return
|
|
1495
|
+
self._send_json(200, {"response_type": "ephemeral",
|
|
1496
|
+
"text": "Queued loki job %s" % job_id})
|
|
1497
|
+
|
|
1498
|
+
def _handle_linear(self):
|
|
1499
|
+
"""POST /linear -- signed Linear issue webhook; enqueues linear:<identifier>."""
|
|
1500
|
+
body = self._read_body()
|
|
1501
|
+
if body is None:
|
|
1502
|
+
return
|
|
1503
|
+
if not self.linear_secret:
|
|
1504
|
+
log_event(JOB_EVENT, "", "", "rejected (no linear webhook secret configured)")
|
|
1505
|
+
self._send_json(503, {"error": "LINEAR_WEBHOOK_SECRET not configured"})
|
|
1506
|
+
return
|
|
1507
|
+
if not verify_linear_signature(
|
|
1508
|
+
self.linear_secret, body, self.headers.get("Linear-Signature", "")):
|
|
1509
|
+
logging.warning("Invalid Linear signature from %s", self.address_string())
|
|
1510
|
+
log_event(JOB_EVENT, "", "", "rejected (invalid linear signature)")
|
|
1511
|
+
self._send_json(401, {"error": "invalid signature"})
|
|
1512
|
+
return
|
|
1513
|
+
try:
|
|
1514
|
+
payload = json.loads(body)
|
|
1515
|
+
except ValueError:
|
|
1516
|
+
self._send_json(400, {"error": "invalid JSON"})
|
|
1517
|
+
return
|
|
1518
|
+
if not isinstance(payload, dict):
|
|
1519
|
+
self._send_json(400, {"error": "payload must be a JSON object"})
|
|
1520
|
+
return
|
|
1521
|
+
if not linear_fresh(payload):
|
|
1522
|
+
log_event(JOB_EVENT, "", "", "rejected (stale linear timestamp)")
|
|
1523
|
+
self._send_json(401, {"error": "stale or missing webhookTimestamp"})
|
|
1524
|
+
return
|
|
1525
|
+
data = payload.get("data")
|
|
1526
|
+
if (payload.get("type") != "Issue" or payload.get("action") != "create"
|
|
1527
|
+
or not isinstance(data, dict)):
|
|
1528
|
+
self._send_json(200, {"status": "skipped"})
|
|
1529
|
+
return
|
|
1530
|
+
ident = data.get("identifier")
|
|
1531
|
+
if not isinstance(ident, str) or not LINEAR_IDENTIFIER_RE.match(ident):
|
|
1532
|
+
self._send_json(400, {"error": "invalid issue identifier"})
|
|
1533
|
+
return
|
|
1534
|
+
if self.dispatcher.seen_delivery(self.headers.get("Linear-Delivery", "")):
|
|
1535
|
+
self._send_json(200, {"status": "duplicate"})
|
|
1536
|
+
return
|
|
1537
|
+
spec = "linear:%s" % ident
|
|
1538
|
+
if not valid_spec(spec):
|
|
1539
|
+
self._send_json(400, {"error": "invalid spec"})
|
|
1540
|
+
return
|
|
1541
|
+
issue_id = data.get("id")
|
|
1542
|
+
target = {"kind": "linear",
|
|
1543
|
+
"issue_id": issue_id if isinstance(issue_id, str) else ""}
|
|
1544
|
+
job_id = self._enqueue_job(spec, target)
|
|
1545
|
+
if job_id is None:
|
|
1546
|
+
return
|
|
1303
1547
|
self._send_json(202, {"id": job_id, "status": "queued"})
|
|
1304
1548
|
|
|
1305
1549
|
def _handle_sentry(self):
|
|
@@ -1347,6 +1591,12 @@ class WebhookHandler(http.server.BaseHTTPRequestHandler):
|
|
|
1347
1591
|
if path == "/sentry":
|
|
1348
1592
|
self._handle_sentry()
|
|
1349
1593
|
return
|
|
1594
|
+
if path == "/slack":
|
|
1595
|
+
self._handle_slack()
|
|
1596
|
+
return
|
|
1597
|
+
if path == "/linear":
|
|
1598
|
+
self._handle_linear()
|
|
1599
|
+
return
|
|
1350
1600
|
if path != "/webhook":
|
|
1351
1601
|
self._send_json(404, {"error": "not found"})
|
|
1352
1602
|
return
|
|
@@ -1527,6 +1777,11 @@ def main():
|
|
|
1527
1777
|
)
|
|
1528
1778
|
sentry_secret = ""
|
|
1529
1779
|
|
|
1780
|
+
# Slack/Linear secrets: env only. Each must differ from every other
|
|
1781
|
+
# credential, since each authenticates a different party.
|
|
1782
|
+
slack_secret, linear_secret = resolve_chat_secrets(
|
|
1783
|
+
os.environ, secret, api_token, sentry_secret)
|
|
1784
|
+
|
|
1530
1785
|
# Persist resolved values, but never write the secret to disk.
|
|
1531
1786
|
config["port"] = port
|
|
1532
1787
|
config["dry_run"] = dry_run
|
|
@@ -1541,6 +1796,8 @@ def main():
|
|
|
1541
1796
|
WebhookHandler.secret = secret
|
|
1542
1797
|
WebhookHandler.api_token = api_token
|
|
1543
1798
|
WebhookHandler.sentry_secret = sentry_secret
|
|
1799
|
+
WebhookHandler.slack_secret = slack_secret
|
|
1800
|
+
WebhookHandler.linear_secret = linear_secret
|
|
1544
1801
|
WebhookHandler.dispatcher = dispatcher
|
|
1545
1802
|
|
|
1546
1803
|
signing_key, signing_kid = load_signing_key(auto_generate=False)
|
|
@@ -1587,6 +1844,16 @@ def main():
|
|
|
1587
1844
|
"No Sentry secret configured: POST /sentry is rejected with 503. "
|
|
1588
1845
|
"Set LOKI_SENTRY_WEBHOOK_SECRET or SENTRY_CLIENT_SECRET."
|
|
1589
1846
|
)
|
|
1847
|
+
if not slack_secret:
|
|
1848
|
+
logging.warning(
|
|
1849
|
+
"No Slack signing secret configured: POST /slack is rejected with "
|
|
1850
|
+
"503. Set SLACK_SIGNING_SECRET."
|
|
1851
|
+
)
|
|
1852
|
+
if not linear_secret:
|
|
1853
|
+
logging.warning(
|
|
1854
|
+
"No Linear webhook secret configured: POST /linear is rejected with "
|
|
1855
|
+
"503. Set LINEAR_WEBHOOK_SECRET."
|
|
1856
|
+
)
|
|
1590
1857
|
if not secret:
|
|
1591
1858
|
logging.warning(
|
|
1592
1859
|
"No webhook secret configured: ALL webhooks will be rejected with "
|
package/autonomy/verify.sh
CHANGED
|
@@ -2588,6 +2588,11 @@ OPTIONS:
|
|
|
2588
2588
|
Seal a workspace run into one signed composite receipt
|
|
2589
2589
|
(composite.json). `loki verify <composite.json>` then
|
|
2590
2590
|
re-checks it offline. Routed to the engine10 verifier.
|
|
2591
|
+
--max-age <DUR> With a receipt path: refuse a receipt whose signed issue
|
|
2592
|
+
time is older than DUR (Ns, Nm, Nh, Nd or seconds), has
|
|
2593
|
+
no signed issue time, or is more than 300s in the
|
|
2594
|
+
future. Exits 2 (UNCHECKED). Routed to the engine10
|
|
2595
|
+
verifier.
|
|
2591
2596
|
-h, --help Show this help.
|
|
2592
2597
|
|
|
2593
2598
|
GATES (deterministic, reproducible by construction):
|
package/completions/loki.bash
CHANGED
|
@@ -142,14 +142,14 @@ _loki_completion() {
|
|
|
142
142
|
|
|
143
143
|
deploy)
|
|
144
144
|
if [[ "$cur" == -* ]]; then
|
|
145
|
-
COMPREPLY=( $(compgen -W "--execute --health-url --auto-rollback --help" -- "$cur") )
|
|
145
|
+
COMPREPLY=( $(compgen -W "--execute --health-url --auto-rollback --dir --no-clip --allow-unanchored --help" -- "$cur") )
|
|
146
146
|
return 0
|
|
147
147
|
fi
|
|
148
148
|
;;
|
|
149
149
|
|
|
150
150
|
verify)
|
|
151
151
|
if [[ "$cur" == -* ]]; then
|
|
152
|
-
COMPREPLY=( $(compgen -W "--base --out --block-on --no-llm --json --explain --check-fresh --hosted --seal-composite --help" -- "$cur") )
|
|
152
|
+
COMPREPLY=( $(compgen -W "--base --out --block-on --no-llm --json --explain --check-fresh --max-age --hosted --seal-composite --help" -- "$cur") )
|
|
153
153
|
return 0
|
|
154
154
|
fi
|
|
155
155
|
;;
|
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.8.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/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.8.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/sentry-intake.md
CHANGED
|
@@ -22,3 +22,25 @@ The token is sent only to the Sentry host through a curl config on stdin, never
|
|
|
22
22
|
|
|
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
|
+
|
|
26
|
+
## Slack and Linear delegation
|
|
27
|
+
|
|
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
|
+
|
|
30
|
+
Secrets are env only and must differ from each other, the GitHub webhook secret, the API token and the Sentry secret (an equal secret disables its route at startup). A missing secret answers 503 naming the variable, and the server logs a startup warning. None of them reach the `loki start` child environment.
|
|
31
|
+
|
|
32
|
+
Slack (`POST /slack`):
|
|
33
|
+
|
|
34
|
+
1. Set `SLACK_SIGNING_SECRET`. Point a `/loki` slash command (and Events URL verification) at `/slack`.
|
|
35
|
+
2. The signature is `v0=` + hex HMAC-SHA256 over `v0:<X-Slack-Request-Timestamp>:<raw body>`, window 300s, the same contract as `loki-ts/src/contrib/slack_inbound.ts`. Bad, stale or missing signature: 401, nothing queued.
|
|
36
|
+
3. The command text is the spec (one line, not starting with `-`) and runs as `loki start <text> --detach`. Slack gets an ephemeral reply carrying the job id.
|
|
37
|
+
4. On completion the summary is posted to the request's `response_url`, only if it starts with `https://hooks.slack.com/`. Any other host is never contacted.
|
|
38
|
+
|
|
39
|
+
Linear (`POST /linear`):
|
|
40
|
+
|
|
41
|
+
1. Set `LINEAR_WEBHOOK_SECRET`. Point a Linear issue webhook at `/linear`.
|
|
42
|
+
2. `Linear-Signature` is the hex HMAC-SHA256 of the raw body, and the signed `webhookTimestamp` must be within 300s. Otherwise 401.
|
|
43
|
+
3. A created issue enqueues `loki start linear:<identifier> --detach` and answers 202 with the job id. Other events are skipped.
|
|
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
|
+
|
|
46
|
+
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
|
@@ -89,3 +89,15 @@ With `LOKI_SPEED=1` (default off), intake no longer waits on the cheap-model alr
|
|
|
89
89
|
## D82-FLAGS status: default on
|
|
90
90
|
|
|
91
91
|
Per the D82 founder directive, `LOKI_SPEED` is on unless set to `0` (decomposer routing, warm engine on a unix socket, stage prefix, deferred already-done check, lean keyword fallback, unit mode when `LOKI_UNIT_SPEC` is set). `LOKI_SPEED=0` restores the previous behaviour byte for byte. The other D82 opt-outs are `LOKI_VISUAL_EVIDENCE=0`, `LOKI_CONTRACT=0`, `LOKI_SLACK_INBOUND=0` (Slack stays inert until its credential is configured), `LOKI_CONTROL=0` and `LOKI_WORKSPACES=0`. Test: `loki-ts/tests/features/d82_default_on.test.ts`.
|
|
92
|
+
|
|
93
|
+
## Missions and group Seal (v11.8.0)
|
|
94
|
+
|
|
95
|
+
Missions: a spec with two or more milestones runs milestone by milestone. Format: `## Milestone N[: title]` headings (the body is everything up to the next milestone heading), or a top-level `milestones:` list of `- title` lines. Fewer than 2 milestones is not a mission and takes the normal path unchanged. Parsing is `parseMilestones` in `loki-ts/src/features/decompose.ts`; the loop is in `maybeRunGroup` (`loki-ts/src/features/speed/route.ts`), reached from `loki start` and from chat `/run` through the same supervisor call.
|
|
96
|
+
|
|
97
|
+
Each milestone prints `loki: milestone k/N: <title>`, is decomposed on its own, and runs as its own group with its own frozen contract and group Seal. One-unit groups and sequential providers are allowed for milestones (the normal fewer-than-2-units and slot fallbacks do not apply). The mission stops at the first non-zero result, prints `loki: milestone k/N failed (exit X); rerun the same loki start to resume`, and returns that code; later milestones never start. All milestones exit 0 returns 0.
|
|
98
|
+
|
|
99
|
+
Group exit codes: 0 = every unit VERIFIED and the group receipt VERIFIED; 4 = NOT PROVEN (integrated, but no checked group receipt); 1 = failures.
|
|
100
|
+
|
|
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
|
+
|
|
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`.
|
package/docs/v10/PROGRESS.md
CHANGED
|
@@ -2337,3 +2337,13 @@ The swarm is stopped: .loki/V10-STOP exists, the loop has no wake-ups, and the c
|
|
|
2337
2337
|
- 10.6.6 (456058bae): scope control, Control Plane v0, train-cycle fix, docs. All green. Published.
|
|
2338
2338
|
- 10.6.7 (bfce1db12): D60 sprint, all gates, Release, Post-Release Smoke and Promote success. latest and next at 10.6.7.
|
|
2339
2339
|
- Open at handoff: real-repo gate not wired into promote.yml and needs operator provider auth; no end-to-end exit 3 run of the cost cap; root action.yml still uses loki start --simple; about 43 worktrees under .claude/worktrees (cap 15); never push backup/train-67..70 or backup/main-pre-rebuild.
|
|
2340
|
+
|
|
2341
|
+
## 2026-10-15T20:05Z (CoS) 11.7.1 published, v11.8.0 train started
|
|
2342
|
+
- 11.7.0 (8b22e5fba): Tests red on tests/test-help-changelog-drift.sh (three flags missing from help/completion) and tests/test-sigpipe-guard.sh (count 1743 over ceiling 1742). Not published.
|
|
2343
|
+
- 11.7.1 (a01a22740): Release and Tests completed/success (gh run watch log, runs 38794589410 and 38794589637); npm dist-tags next=11.7.1, latest=11.6.0.
|
|
2344
|
+
- v11.8.0 train (workflow wf_e6306e08-008): group Seal, group resume, multi-milestone missions, Slack and Linear delegation on the trigger server, receipt --max-age, deploy completion and pulse ID_RE fixes.
|
|
2345
|
+
|
|
2346
|
+
## 2026-10-15T20:45Z (CoS) v11.8.0 failed CI, v11.8.1 fix-forward
|
|
2347
|
+
- v11.8.0 (b5191b999): Tests run 38800796458 and Release failed on line budgets (core engine10 5020, features 3190), the D66 fence (run_group.ts value-imported seal and verify_cmd), two unclassified checkouts in the L2 registry, macOS-first stat in tests/test-backlog-units.sh:162, and a `loki help verify` mismatch. Not published.
|
|
2348
|
+
- Fix 1d734e6ba (Opus HIGH review APPROVED, 162 pass 0 fail on five suites), merged as 1788db60b. The pulse ID_RE item (E-121) was not in the train and stays open.
|
|
2349
|
+
- Local guards on the merged tree, all rc=0: test-help-changelog-drift, test-sigpipe-guard, test-help-and-log-cap, test-backlog-units, structural-checks, test-ci-only-divergence, test-platform-infra; bun test budget, l2_destructive_registry, route_group, seal_group: 117 pass 0 fail.
|