loki-mode 10.5.16 → 10.5.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/.claude-plugin/marketplace.json +12 -0
  2. package/README.md +2 -0
  3. package/SKILL.md +2 -2
  4. package/VERSION +1 -1
  5. package/autonomy/lib/backlog.py +280 -0
  6. package/autonomy/lib/loki_yaml.py +225 -0
  7. package/autonomy/loki +65 -6
  8. package/autonomy/verify.sh +11 -0
  9. package/completions/_loki +1 -0
  10. package/completions/loki.bash +2 -2
  11. package/dashboard/__init__.py +1 -1
  12. package/docs/INSTALLATION.md +1 -1
  13. package/docs/exit-codes.md +15 -0
  14. package/docs/v10/BOARD.md +19 -11
  15. package/docs/v10/DECISIONS.md +36 -0
  16. package/docs/v10/FOUNDER-QUEUE.md +1 -0
  17. package/docs/v10/PROGRESS.md +41 -0
  18. package/loki-ts/dist/loki.js +415 -407
  19. package/mcp/__init__.py +1 -1
  20. package/package.json +1 -1
  21. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  22. package/web-app/dist/assets/{AdminPage-B1zdOJaF.js → AdminPage-DxGZSpFg.js} +1 -1
  23. package/web-app/dist/assets/{Avatar-DKjAJ_w7.js → Avatar-BZEp23D8.js} +1 -1
  24. package/web-app/dist/assets/{Badge-DP-UrPLv.js → Badge-DnDwqDLu.js} +1 -1
  25. package/web-app/dist/assets/{Button-c8qBEA2K.js → Button-C9KDP4c1.js} +1 -1
  26. package/web-app/dist/assets/{CockpitPage-CFljzGbi.js → CockpitPage-DRlh3LV2.js} +1 -1
  27. package/web-app/dist/assets/{ComparePage-BNKqdNRu.js → ComparePage-C_gE45dQ.js} +1 -1
  28. package/web-app/dist/assets/{ErrorBoundary-DNkpXGt5.js → ErrorBoundary-CL-J9-Cs.js} +1 -1
  29. package/web-app/dist/assets/{EvidenceReceiptPanel-CQd49V4Z.js → EvidenceReceiptPanel-DABd176Y.js} +1 -1
  30. package/web-app/dist/assets/{GitHubIssuesPanel-B9wji_yi.js → GitHubIssuesPanel-C4lYbm6O.js} +1 -1
  31. package/web-app/dist/assets/{GitHubPRsPanel-BkfCV4ag.js → GitHubPRsPanel-CgiUixwo.js} +1 -1
  32. package/web-app/dist/assets/{HomePage-BVV3Cowy.js → HomePage-DC99RIrz.js} +2 -2
  33. package/web-app/dist/assets/{LoginPage-CWiOenLJ.js → LoginPage-Bd72QWNk.js} +1 -1
  34. package/web-app/dist/assets/{MagicPage-DckrjTVd.js → MagicPage-CBouFs2K.js} +1 -1
  35. package/web-app/dist/assets/{MetricsPage-DNBnH68C.js → MetricsPage-c9MyDcBp.js} +1 -1
  36. package/web-app/dist/assets/{NotFoundPage-Ca8za8mL.js → NotFoundPage-ZPhiebiW.js} +1 -1
  37. package/web-app/dist/assets/{ProjectPage-C92sm9xi.js → ProjectPage-DKwrALUb.js} +3 -3
  38. package/web-app/dist/assets/{ProjectsPage-BxsCyJ95.js → ProjectsPage-X1uZKZC_.js} +1 -1
  39. package/web-app/dist/assets/{SettingsPage-BKg-6aHI.js → SettingsPage-CiZKZRto.js} +1 -1
  40. package/web-app/dist/assets/{ShowcasePage-Bw_fI5rG.js → ShowcasePage-k_imKfeh.js} +1 -1
  41. package/web-app/dist/assets/{SystemSettingsPage-Bc-2OWUp.js → SystemSettingsPage-Wx9ESLll.js} +1 -1
  42. package/web-app/dist/assets/{TeamsPage-GsYRD1b9.js → TeamsPage-B7upn-I7.js} +1 -1
  43. package/web-app/dist/assets/{TemplatesPage-BN7ZlULA.js → TemplatesPage-oJf_VKgh.js} +1 -1
  44. package/web-app/dist/assets/{TerminalOutput-d7__ysTd.js → TerminalOutput-BdfPlgLI.js} +1 -1
  45. package/web-app/dist/assets/{activity-ycOMpCGz.js → activity-BcZTsgWH.js} +1 -1
  46. package/web-app/dist/assets/{bell-B93o4lsL.js → bell-JNEkjWQ2.js} +1 -1
  47. package/web-app/dist/assets/{bot-CB0kmriG.js → bot-DMAJobP_.js} +1 -1
  48. package/web-app/dist/assets/{check-BAYNAOEO.js → check-DPTry-z2.js} +1 -1
  49. package/web-app/dist/assets/{chevron-left-Bgu8UgyR.js → chevron-left-DSqOEtTV.js} +1 -1
  50. package/web-app/dist/assets/{circle-alert-n_FciWUP.js → circle-alert-BbL8Vysv.js} +1 -1
  51. package/web-app/dist/assets/{clock-CNceeS66.js → clock-BHEr5lGW.js} +1 -1
  52. package/web-app/dist/assets/{cloud-CJBEdb7m.js → cloud-C48PunYo.js} +1 -1
  53. package/web-app/dist/assets/{code-xml-BHnKVqrH.js → code-xml-Boa7kRqT.js} +1 -1
  54. package/web-app/dist/assets/{database-DSVoL7Pl.js → database-Eu35Gv7z.js} +1 -1
  55. package/web-app/dist/assets/{dollar-sign-48dvOe4H.js → dollar-sign-BO9fCnHW.js} +1 -1
  56. package/web-app/dist/assets/{file-code-corner-8yzS5DeZ.js → file-code-corner-DHsSDIL_.js} +1 -1
  57. package/web-app/dist/assets/{file-plus-BcuVrrey.js → file-plus-BYiQLe5l.js} +1 -1
  58. package/web-app/dist/assets/{globe-Bb-O58oN.js → globe-BPZ5sQI2.js} +1 -1
  59. package/web-app/dist/assets/{hammer-DeMMY6xW.js → hammer-DlOLTsH3.js} +1 -1
  60. package/web-app/dist/assets/{index-9hue9wt2.js → index-BrgkuqSA.js} +3 -3
  61. package/web-app/dist/assets/{layers-DmCoMrQC.js → layers-DMNdn1GR.js} +1 -1
  62. package/web-app/dist/assets/{loader-circle-CJvjM7JW.js → loader-circle-_e9fypHL.js} +1 -1
  63. package/web-app/dist/assets/{lock-Dpje5cWO.js → lock-D_elxMZb.js} +1 -1
  64. package/web-app/dist/assets/{package-mA_hCaf8.js → package-Bw-7g3JI.js} +1 -1
  65. package/web-app/dist/assets/{plus-Db0JnAoC.js → plus-CZeq0pgJ.js} +1 -1
  66. package/web-app/dist/assets/{refresh-cw-DbCL1y7h.js → refresh-cw-D0WMyffs.js} +1 -1
  67. package/web-app/dist/assets/{rotate-ccw-Bt6pp50d.js → rotate-ccw-CcKyNwXG.js} +1 -1
  68. package/web-app/dist/assets/{scroll-text-7j0hXxm7.js → scroll-text-C0L9Vqpm.js} +1 -1
  69. package/web-app/dist/assets/{server-BMJYtpVZ.js → server-SgwFv6cd.js} +1 -1
  70. package/web-app/dist/assets/{shield-alert-Bu8Pbdy-.js → shield-alert-D1tESlfI.js} +1 -1
  71. package/web-app/dist/assets/{thumbs-up-8erzRJ8k.js → thumbs-up-bUtUA15O.js} +1 -1
  72. package/web-app/dist/assets/{trash-2-DBC_D_Hv.js → trash-2-C9Nv1kKk.js} +1 -1
  73. package/web-app/dist/assets/{trending-down-DMNq5hSm.js → trending-down-Bh107AJe.js} +1 -1
  74. package/web-app/dist/assets/{trending-up-9b1IqBqR.js → trending-up-25BqhLVq.js} +1 -1
  75. package/web-app/dist/assets/{upload-SoA0iiLG.js → upload-DDYtMbN5.js} +1 -1
  76. package/web-app/dist/assets/{usePolling-DLP2FX9M.js → usePolling-BSDT0il_.js} +1 -1
  77. package/web-app/dist/assets/{user-D7Pb9wW3.js → user-CKpUEUhK.js} +1 -1
  78. package/web-app/dist/index.html +1 -1
@@ -23,6 +23,18 @@
23
23
  "mcp",
24
24
  "loki"
25
25
  ]
26
+ },
27
+ {
28
+ "name": "loki-seal",
29
+ "source": "./packages/loki-seal",
30
+ "description": "Stop hook that refuses 'done' while tests are red or were deleted, skipped, xfailed or weakened. Runs locally, no model calls.",
31
+ "homepage": "https://github.com/asklokesh/loki-mode",
32
+ "category": "testing",
33
+ "keywords": [
34
+ "verification",
35
+ "tests",
36
+ "hooks"
37
+ ]
26
38
  }
27
39
  ]
28
40
  }
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  Your agent says done. Loki proves it.
2
2
 
3
3
  An autonomous software factory that knows what it is supposed to deliver, and proves it did.
4
+ Install: `/plugin marketplace add asklokesh/loki-mode` then `/plugin install loki-seal@loki-mode`
5
+
4
6
 
5
7
  <!-- 20-second loki-seal GIF: placeholder until recorded. Script: packages/loki-seal/demo/demo.tape -->
6
8
  [loki-seal demo script](packages/loki-seal/demo/demo.tape)
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 v10.5.16
6
+ # Loki Mode v10.5.17
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -470,4 +470,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
470
470
 
471
471
  ---
472
472
 
473
- **v10.5.16 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
473
+ **v10.5.17 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 10.5.16
1
+ 10.5.17
@@ -0,0 +1,280 @@
1
+ #!/usr/bin/env python3
2
+ """loki backlog: run one v10 issue-mode run per open issue, N in parallel (D51).
3
+
4
+ loki backlog owner/repo --all | --label X | --issues 1,2,3
5
+ [--concurrency N] [--dry-run]
6
+
7
+ Each issue gets its own git worktree and branch (loki/backlog-N) beside the
8
+ checkout; the launcher runs inside it. Success = the child exited 0 (the engine's
9
+ ladder: 0 VERIFIED, 1 FAILED, 3 BUDGET_STOP, 4 BLOCKED, 5 STALLED). A draft PR
10
+ from a failed run is still FAILED. Exit: 0 all ok, 3 only budget stops, 1 other
11
+ failures, 2 usage/config error.
12
+
13
+ Test seam: LOKI_BACKLOG_LAUNCHER replaces the real launcher (called with the
14
+ issue ref, cwd = the issue worktree).
15
+ """
16
+ import argparse
17
+ import datetime
18
+ import json
19
+ import os
20
+ import re
21
+ import shutil
22
+ import signal
23
+ import subprocess
24
+ import sys
25
+ import time
26
+
27
+ HERE = os.path.dirname(os.path.abspath(__file__))
28
+ sys.path.insert(0, HERE)
29
+ import loki_yaml # noqa: E402
30
+
31
+ REPO_ROOT = os.path.abspath(os.path.join(HERE, "..", ".."))
32
+ REPO_RE = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$")
33
+
34
+
35
+ def say(msg):
36
+ print(msg, flush=True)
37
+
38
+
39
+ def die(msg, rc=2):
40
+ print("loki backlog: " + msg, file=sys.stderr)
41
+ return rc
42
+
43
+
44
+ def redact(text, token):
45
+ return text.replace(token, "***") if token and len(token) >= 6 else text
46
+
47
+
48
+ def git(*args, cwd=None):
49
+ return subprocess.run(["git", *args], cwd=cwd, capture_output=True, text=True)
50
+
51
+
52
+ def origin_slug(cwd):
53
+ url = git("remote", "get-url", "origin", cwd=cwd).stdout.strip()
54
+ m = re.search(r"github\.com[:/]([^/]+/[^/]+?)(?:\.git)?/?$", url)
55
+ return m.group(1) if m else ""
56
+
57
+
58
+ def list_issues(repo, label, env):
59
+ cmd = ["gh", "issue", "list", "--repo", repo, "--state", "open", "--limit", "200",
60
+ "--json", "number,title,labels"]
61
+ if label:
62
+ cmd += ["--label", label]
63
+ r = subprocess.run(cmd, capture_output=True, text=True, env=env)
64
+ if r.returncode != 0:
65
+ raise RuntimeError("gh issue list failed: " + (r.stderr.strip().splitlines() or ["unknown error"])[0])
66
+ return sorted(({"number": i["number"], "title": i.get("title", "")} for i in json.loads(r.stdout or "[]")),
67
+ key=lambda i: i["number"])
68
+
69
+
70
+ # ---- daily spend ledger (outside any worktree) -----------------------------
71
+ def ledger_path():
72
+ return os.path.join(os.path.expanduser("~"), ".loki", "backlog-spend.json")
73
+
74
+
75
+ def spent_today():
76
+ try:
77
+ with open(ledger_path()) as f:
78
+ return float(json.load(f).get(datetime.date.today().isoformat(), 0))
79
+ except (OSError, ValueError):
80
+ return 0.0
81
+
82
+
83
+ def add_spend(usd):
84
+ p = ledger_path()
85
+ try:
86
+ with open(p) as f:
87
+ data = json.load(f)
88
+ except (OSError, ValueError):
89
+ data = {}
90
+ k = datetime.date.today().isoformat()
91
+ data[k] = round(float(data.get(k, 0)) + usd, 6)
92
+ os.makedirs(os.path.dirname(p), exist_ok=True)
93
+ with open(p, "w") as f:
94
+ json.dump(data, f)
95
+
96
+
97
+ def parse_result(rc, text):
98
+ """-> (ok, status line text, cost_usd or None)."""
99
+ pr = re.search(r"^PR:\s+(https?://\S+)", text, re.M)
100
+ why = re.search(r"^Reason:\s+(.+)$", text, re.M)
101
+ cost = re.search(r"^Cost:\s+(?:partial:\s+)?\$([0-9]+(?:\.[0-9]+)?)", text, re.M)
102
+ usd = float(cost.group(1)) if cost else None
103
+ reason = why.group(1).strip() if why else ""
104
+ if rc == 0:
105
+ return True, ("PR " + pr.group(1)) if pr else "VERIFIED", usd
106
+ if rc == 4:
107
+ return False, "BLOCKED: " + (reason or "see the run log"), usd
108
+ if rc == 3:
109
+ return False, "BUDGET_STOP: " + (reason or "run cost cap reached"), usd
110
+ return False, "FAILED: " + (reason or "exit %d" % rc), usd
111
+
112
+
113
+ def main(argv):
114
+ ap = argparse.ArgumentParser(prog="loki backlog", add_help=True)
115
+ ap.add_argument("repo")
116
+ sel = ap.add_mutually_exclusive_group()
117
+ sel.add_argument("--all", action="store_true")
118
+ sel.add_argument("--label")
119
+ sel.add_argument("--issues")
120
+ ap.add_argument("--concurrency", type=int)
121
+ ap.add_argument("--dry-run", action="store_true")
122
+ a = ap.parse_args(argv)
123
+
124
+ if not REPO_RE.match(a.repo):
125
+ return die("repo must be owner/repo, got %r" % a.repo)
126
+ if not (a.all or a.label or a.issues):
127
+ return die("choose one of --all, --label X, --issues 1,2,3")
128
+ numbers = None
129
+ if a.issues:
130
+ if not re.match(r"^\d+(,\d+)*$", a.issues):
131
+ return die("--issues takes a comma-separated list of numbers, got %r" % a.issues)
132
+ numbers = [int(n) for n in a.issues.split(",")]
133
+
134
+ try:
135
+ cfg, cfg_path, errors = loki_yaml.load()
136
+ except loki_yaml.CannotCheck as e:
137
+ return die("could not read loki.yaml: %s" % e)
138
+ if errors:
139
+ print("loki backlog: invalid configuration (%s):" % (cfg_path or "environment"), file=sys.stderr)
140
+ for e in errors:
141
+ print(" - " + e, file=sys.stderr)
142
+ return 2
143
+ conc = a.concurrency or cfg.get("concurrency", 2)
144
+ if conc < 1:
145
+ return die("--concurrency must be >= 1")
146
+
147
+ env = dict(os.environ)
148
+ token = ""
149
+ tok_env = cfg.get("git", {}).get("token_env")
150
+ if tok_env:
151
+ token = os.environ.get(tok_env, "")
152
+ if not token:
153
+ return die("git.token_env names %s but that environment variable is not set" % tok_env)
154
+ env["GH_TOKEN"] = env["GITHUB_TOKEN"] = token
155
+
156
+ top = git("rev-parse", "--show-toplevel").stdout.strip()
157
+ if not top:
158
+ return die("run this from a checkout of %s" % a.repo)
159
+ if origin_slug(top).lower() != a.repo.lower():
160
+ return die("this checkout's origin is %r, not %s; cd into a checkout of %s"
161
+ % (origin_slug(top) or "unknown", a.repo, a.repo))
162
+
163
+ try:
164
+ issues = ([{"number": n, "title": ""} for n in numbers] if numbers
165
+ else list_issues(a.repo, a.label, env))
166
+ except RuntimeError as e:
167
+ return die(redact(str(e), token), 1)
168
+
169
+ say("backlog: %s concurrency %d, %d issue(s)" % (a.repo, conc, len(issues)))
170
+ if a.dry_run:
171
+ for i in issues:
172
+ say("backlog: would run #%d %s" % (i["number"], i["title"]))
173
+ return 0
174
+ if not issues:
175
+ say("backlog: nothing to do")
176
+ return 0
177
+
178
+ # ---- launch ------------------------------------------------------------
179
+ wt_root = top.rstrip("/") + "-backlog"
180
+ os.makedirs(os.path.join(wt_root, "logs"), exist_ok=True)
181
+ launcher = os.environ.get("LOKI_BACKLOG_LAUNCHER") or os.path.join(REPO_ROOT, "bin", "loki")
182
+ child_env = dict(env, LOKI_ENGINE="v10", LOKI_NO_BROWSER="1")
183
+ for key, var in (("models.default", "LOKI_MODEL_DEVELOPMENT"), ("models.cheap", "LOKI_MODEL_FAST")):
184
+ sect, name = key.split(".")
185
+ if cfg.get(sect, {}).get(name):
186
+ child_env[var] = cfg[sect][name]
187
+ if cfg.get("provider"):
188
+ child_env["LOKI_PROVIDER"] = cfg["provider"]
189
+ # ponytail: per-run cap is forwarded only; the v10 engine itself caps on time
190
+ if cfg.get("budgets", {}).get("per_run_usd"):
191
+ child_env["LOKI_BUDGET_LIMIT"] = str(cfg["budgets"]["per_run_usd"])
192
+ day_cap = cfg.get("budgets", {}).get("per_day_usd")
193
+
194
+ state = {i["number"]: {"status": "queued", "ok": None, "cost": None, "ran": False} for i in issues}
195
+ for n in state:
196
+ say("backlog: #%d queued" % n)
197
+ pending = [i["number"] for i in issues]
198
+ running = {} # n -> (Popen, log path, worktree)
199
+ unmeasured = 0
200
+
201
+ def stop_children(*_):
202
+ for p, _l, _w in running.values():
203
+ try:
204
+ os.killpg(p.pid, signal.SIGTERM)
205
+ except OSError:
206
+ pass
207
+ sys.exit(130)
208
+ signal.signal(signal.SIGTERM, stop_children)
209
+ signal.signal(signal.SIGINT, stop_children)
210
+
211
+ def finish(n):
212
+ nonlocal unmeasured
213
+ p, log, wt = running.pop(n)
214
+ try:
215
+ with open(log, errors="replace") as f:
216
+ text = redact(f.read(), token)
217
+ with open(log, "w") as f:
218
+ f.write(text)
219
+ except OSError:
220
+ text = ""
221
+ ok, status, usd = parse_result(p.returncode, text)
222
+ if usd is None:
223
+ unmeasured += 1
224
+ else:
225
+ add_spend(usd)
226
+ state[n].update(status=status, ok=ok, cost=usd)
227
+ say("backlog: #%d %s" % (n, status))
228
+ if ok:
229
+ git("worktree", "remove", "--force", wt, cwd=top)
230
+ git("branch", "-D", "loki/backlog-%d" % n, cwd=top)
231
+
232
+ def launch(n):
233
+ wt = os.path.join(wt_root, "issue-%d" % n)
234
+ if os.path.exists(wt):
235
+ git("worktree", "remove", "--force", wt, cwd=top)
236
+ r = git("worktree", "add", "-B", "loki/backlog-%d" % n, wt, "HEAD", cwd=top)
237
+ if r.returncode != 0:
238
+ err = (r.stderr.strip().splitlines() or ["git error"])[-1]
239
+ state[n].update(status="FAILED: worktree: " + redact(err, token), ok=False)
240
+ say("backlog: #%d %s" % (n, state[n]["status"]))
241
+ return
242
+ log = os.path.join(wt_root, "logs", "issue-%d.log" % n)
243
+ cmd = [launcher, "%s#%d" % (a.repo, n)]
244
+ with open(log, "w") as lf:
245
+ p = subprocess.Popen(cmd, cwd=wt, env=child_env, stdout=lf, stderr=subprocess.STDOUT,
246
+ stdin=subprocess.DEVNULL, start_new_session=True)
247
+ running[n] = (p, log, wt)
248
+ state[n].update(status="running", ran=True)
249
+ say("backlog: #%d running" % n)
250
+
251
+ while pending or running:
252
+ for n in [n for n, (p, _l, _w) in running.items() if p.poll() is not None]:
253
+ finish(n)
254
+ while pending and len(running) < conc:
255
+ if day_cap and spent_today() >= day_cap:
256
+ for n in pending:
257
+ state[n].update(status="BUDGET_STOP: daily budget $%.2f reached ($%.2f spent)" % (day_cap, spent_today()), ok=False)
258
+ say("backlog: #%d %s" % (n, state[n]["status"]))
259
+ pending = []
260
+ break
261
+ launch(pending.pop(0))
262
+ if running:
263
+ time.sleep(0.2)
264
+
265
+ # ---- summary -----------------------------------------------------------
266
+ say("")
267
+ say("Summary: %s" % a.repo)
268
+ say("%-8s %-8s %s" % ("ISSUE", "COST", "RESULT"))
269
+ for n, s in state.items():
270
+ say("%-8s %-8s %s" % ("#%d" % n, ("$%.2f" % s["cost"] if s["cost"] is not None else "unmeasured" if s["ran"] else "-"), s["status"]))
271
+ measured = sum(s["cost"] or 0 for s in state.values())
272
+ say("Total measured cost: $%.2f%s" % (measured, " (%d run(s) unmeasured, not counted as $0)" % unmeasured if unmeasured else ""))
273
+ bad = [s for s in state.values() if not s["ok"]]
274
+ if not bad:
275
+ return 0
276
+ return 3 if all(s["status"].startswith("BUDGET_STOP") for s in bad) else 1
277
+
278
+
279
+ if __name__ == "__main__":
280
+ sys.exit(main(sys.argv[1:]))
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env python3
2
+ """loki.yaml: lookup, env overrides and validation (D51).
3
+
4
+ Shared by `loki backlog`, `loki config validate` and the dashboard.
5
+ Single source of truth for the key set: schemas/loki-yaml.schema.json.
6
+
7
+ from loki_yaml import load
8
+ cfg, path, errors = load()
9
+
10
+ CLI:
11
+ loki_yaml.py validate [FILE] rc 0 ok, 1 invalid or none found, 2 cannot check
12
+ loki_yaml.py show effective config as JSON (env overrides applied)
13
+
14
+ Never echoes a string value for an x-secret-ref field: a user who pastes a real
15
+ token into token_env must not see it printed back.
16
+ """
17
+ import json
18
+ import os
19
+ import subprocess
20
+ import sys
21
+
22
+ HERE = os.path.dirname(os.path.abspath(__file__))
23
+ SCHEMA_PATH = os.path.join(HERE, "..", "..", "schemas", "loki-yaml.schema.json")
24
+
25
+ # dotted key -> env var (CLI flag > env > loki.yaml > default)
26
+ ENV_OVERRIDES = {
27
+ "provider": "LOKI_PROVIDER",
28
+ "models.default": "LOKI_MODEL_DEVELOPMENT",
29
+ "models.cheap": "LOKI_MODEL_FAST",
30
+ "git.token_env": "LOKI_GIT_TOKEN_ENV",
31
+ "concurrency": "LOKI_BACKLOG_CONCURRENCY",
32
+ "budgets.per_run_usd": "LOKI_BUDGET_PER_RUN_USD",
33
+ "budgets.per_day_usd": "LOKI_BUDGET_PER_DAY_USD",
34
+ "notifications.slack_webhook_env": "LOKI_SLACK_WEBHOOK_ENV",
35
+ }
36
+
37
+
38
+ class CannotCheck(Exception):
39
+ """No usable YAML parser: callers must not read this as 'valid'."""
40
+
41
+
42
+ def load_schema():
43
+ with open(SCHEMA_PATH) as f:
44
+ return json.load(f)
45
+
46
+
47
+ def find_config(cwd=None, home=None):
48
+ """Repo-root loki.yaml first, else ~/.loki/loki.yaml. First found wins."""
49
+ cwd = cwd or os.getcwd()
50
+ home = home or os.path.expanduser("~")
51
+ try:
52
+ top = subprocess.run(["git", "-C", cwd, "rev-parse", "--show-toplevel"],
53
+ capture_output=True, text=True).stdout.strip()
54
+ except OSError:
55
+ top = ""
56
+ for cand in (os.path.join(top or cwd, "loki.yaml"),
57
+ os.path.join(home, ".loki", "loki.yaml")):
58
+ if os.path.isfile(cand):
59
+ return cand
60
+ return None
61
+
62
+
63
+ def parse_yaml(path):
64
+ try:
65
+ import yaml
66
+ with open(path) as f:
67
+ return yaml.safe_load(f)
68
+ except ImportError:
69
+ pass
70
+ try:
71
+ r = subprocess.run(["yq", "eval", "-o=json", ".", path], capture_output=True, text=True)
72
+ except OSError:
73
+ raise CannotCheck("no YAML parser: install PyYAML (pip install pyyaml) or yq")
74
+ if r.returncode != 0:
75
+ raise ValueError("invalid YAML: " + (r.stderr.strip().splitlines() or ["parse error"])[0])
76
+ return json.loads(r.stdout)
77
+
78
+
79
+ def _type_ok(val, t):
80
+ if t == "object":
81
+ return isinstance(val, dict)
82
+ if t == "array":
83
+ return isinstance(val, list)
84
+ if t == "string":
85
+ return isinstance(val, str)
86
+ if t == "integer":
87
+ return isinstance(val, int) and not isinstance(val, bool)
88
+ if t == "number":
89
+ return isinstance(val, (int, float)) and not isinstance(val, bool)
90
+ return True
91
+
92
+
93
+ def validate(data, schema, path="", out=None):
94
+ """Validate against the JSON-schema subset used by loki-yaml.schema.json."""
95
+ import re
96
+ out = [] if out is None else out
97
+ label = path or "(top level)"
98
+ t = schema.get("type")
99
+ secret = schema.get("x-secret-ref")
100
+ shown = "" if secret or not isinstance(data, (str, int, float)) else " (got %r)" % (data,)
101
+ if t and not _type_ok(data, t):
102
+ out.append("%s: must be %s%s" % (label, {"integer": "an integer", "object": "a mapping",
103
+ "array": "a list", "string": "a string", "number": "a number"}[t], shown))
104
+ return out
105
+ if isinstance(data, dict):
106
+ props = schema.get("properties", {})
107
+ for k, v in data.items():
108
+ kp = (path + "." if path else "") + str(k)
109
+ if k not in props:
110
+ if schema.get("additionalProperties") is False:
111
+ out.append("%s: unknown key (allowed: %s)" % (kp, ", ".join(sorted(props))))
112
+ else:
113
+ validate(v, props[k], kp, out)
114
+ elif isinstance(data, list):
115
+ for i, v in enumerate(data):
116
+ validate(v, schema.get("items", {}), "%s[%d]" % (path, i), out)
117
+ elif isinstance(data, str):
118
+ if secret and not re.match(schema.get("pattern", ""), data):
119
+ out.append("%s: must be the NAME of an environment variable (UPPER_CASE letters, "
120
+ "digits, underscore), not the secret itself" % label)
121
+ elif "pattern" in schema and not re.match(schema["pattern"], data):
122
+ out.append("%s: does not match %s%s" % (label, schema["pattern"], shown))
123
+ elif "enum" in schema and data not in schema["enum"]:
124
+ out.append("%s: must be one of %s%s" % (label, ", ".join(schema["enum"]), shown))
125
+ elif len(data) < schema.get("minLength", 0):
126
+ out.append("%s: must not be empty" % label)
127
+ if _type_ok(data, "number") and not isinstance(data, bool):
128
+ if "minimum" in schema and data < schema["minimum"]:
129
+ out.append("%s: must be >= %s%s" % (label, schema["minimum"], shown))
130
+ elif "maximum" in schema and data > schema["maximum"]:
131
+ out.append("%s: must be <= %s%s" % (label, schema["maximum"], shown))
132
+ elif "exclusiveMinimum" in schema and data <= schema["exclusiveMinimum"]:
133
+ out.append("%s: must be > %s%s" % (label, schema["exclusiveMinimum"], shown))
134
+ return out
135
+
136
+
137
+ def _schema_type(schema, dotted):
138
+ for part in dotted.split("."):
139
+ schema = schema["properties"][part]
140
+ return schema.get("type")
141
+
142
+
143
+ def apply_env(cfg, schema, env):
144
+ """Overlay env overrides onto cfg. Returns {dotted_key: env_var} of what was applied."""
145
+ applied = {}
146
+ for dotted, var in ENV_OVERRIDES.items():
147
+ raw = env.get(var)
148
+ if raw is None or raw == "":
149
+ continue
150
+ val = raw
151
+ t = _schema_type(schema, dotted)
152
+ try:
153
+ if t == "integer":
154
+ val = int(raw)
155
+ elif t == "number":
156
+ val = float(raw)
157
+ except ValueError:
158
+ pass # stays a string; validation reports it, naming the env var
159
+ node = cfg
160
+ parts = dotted.split(".")
161
+ for p in parts[:-1]:
162
+ node = node.setdefault(p, {})
163
+ node[parts[-1]] = val
164
+ applied[dotted] = var
165
+ return applied
166
+
167
+
168
+ def load(cwd=None, env=None, home=None, path=None):
169
+ """Return (cfg, path_or_None, errors). Raises CannotCheck when YAML cannot be parsed."""
170
+ env = os.environ if env is None else env
171
+ schema = load_schema()
172
+ path = path or find_config(cwd, home)
173
+ cfg, errors = {}, []
174
+ if path:
175
+ try:
176
+ data = parse_yaml(path)
177
+ except CannotCheck:
178
+ raise
179
+ except Exception as e:
180
+ return {}, path, ["%s: %s" % (path, e)]
181
+ if data is None:
182
+ data = {}
183
+ if not isinstance(data, dict):
184
+ return {}, path, ["%s: top level must be a mapping" % path]
185
+ cfg = data
186
+ applied = apply_env(cfg, schema, env)
187
+ for e in validate(cfg, schema):
188
+ key = e.split(":", 1)[0]
189
+ if key in applied:
190
+ e = e.replace(key + ":", "%s (from env %s):" % (key, applied[key]), 1)
191
+ errors.append(e)
192
+ return cfg, path, errors
193
+
194
+
195
+ def main(argv):
196
+ cmd = argv[0] if argv else ""
197
+ if cmd not in ("validate", "show"):
198
+ print("usage: loki_yaml.py validate [FILE] | show", file=sys.stderr)
199
+ return 2
200
+ explicit = argv[1] if cmd == "validate" and len(argv) > 1 else None
201
+ if explicit and not os.path.isfile(explicit):
202
+ print("loki: config validate: file not found: %s" % explicit, file=sys.stderr)
203
+ return 1
204
+ try:
205
+ cfg, path, errors = load(path=explicit)
206
+ except CannotCheck as e:
207
+ print("loki: config validate: could not check: %s" % e, file=sys.stderr)
208
+ return 2
209
+ if cmd == "show":
210
+ print(json.dumps({"path": path, "config": cfg, "errors": errors}, indent=2))
211
+ return 1 if errors else 0
212
+ if path is None and not errors:
213
+ print("loki: no loki.yaml found (looked in the repo root and ~/.loki/loki.yaml)", file=sys.stderr)
214
+ return 1
215
+ if errors:
216
+ print("INVALID: %s" % (path or "environment overrides"))
217
+ for e in errors:
218
+ print(" - " + e)
219
+ return 1
220
+ print("OK: %s" % path)
221
+ return 0
222
+
223
+
224
+ if __name__ == "__main__":
225
+ sys.exit(main(sys.argv[1:]))
package/autonomy/loki CHANGED
@@ -1627,6 +1627,7 @@ show_help() {
1627
1627
  echo "Build:"
1628
1628
  echo " start [SPEC] Start a build (PRD file, GitHub/GitLab/Jira/Azure issue, or no arg)"
1629
1629
  echo " plan <PRD-file> Dry-run analysis: complexity, cost, execution plan"
1630
+ echo " backlog <owner/repo> --all|--label X|--issues N,N Run every matching open issue, N in parallel"
1630
1631
  echo " quickstart [idea] Guided first build: setup, idea, template, plan, go"
1631
1632
  echo ""
1632
1633
  echo "Session:"
@@ -1765,7 +1766,7 @@ show_help() {
1765
1766
  # missing from this list, so a new command cannot ship invisible.
1766
1767
  echo ""
1767
1768
  echo "All commands:"
1768
- echo " agent analyze api assets audit bench checkpoint (cp) ci cleanup"
1769
+ echo " agent analyze api assets audit backlog bench checkpoint (cp) ci cleanup"
1769
1770
  echo " cluster cockpit code compliance completions compound config context (ctx)"
1770
1771
  echo " cost council crash dashboard demo deploy docker docs doctor dogfood"
1771
1772
  echo " enterprise estimate explain export failover github grill heal help"
@@ -9306,6 +9307,32 @@ _deploy_receipt_verdict() {
9306
9307
  # evaluated, so no key set is written and the verdict is UNCHECKED, never
9307
9308
  # TAMPERED: the hash is fine, we just cannot vouch for who signed it. A
9308
9309
  # local key only proves "a holder of this key", not an independent build.
9310
+ # Token SHAPE before any key lookup (mirrors engine10 checkAttestation): a present
9311
+ # attestation that is not a string, not a 3-part JWS, or whose header/payload are not
9312
+ # JSON with alg EdDSA is junk, so TAMPERED, never UNCHECKED, with or without a key.
9313
+ if ! python3 -E - "$pj" >/dev/null 2>&1 <<'PYSHAPE'
9314
+ import sys
9315
+ sys.path[:] = [p for p in sys.path if p not in ("", ".")]
9316
+ import json, base64
9317
+ def dec(x):
9318
+ return json.loads(base64.urlsafe_b64decode(x + "=" * (-len(x) % 4)))
9319
+ v = json.load(open(sys.argv[1])).get("verification") or {}
9320
+ t = v.get("attestation")
9321
+ if t is None or t == "":
9322
+ sys.exit(0)
9323
+ if not isinstance(t, str):
9324
+ sys.exit(1)
9325
+ parts = t.split(".")
9326
+ if len(parts) != 3 or not all(parts):
9327
+ sys.exit(1)
9328
+ try:
9329
+ h = dec(parts[0]); dec(parts[1])
9330
+ except Exception:
9331
+ sys.exit(1)
9332
+ sys.exit(0 if isinstance(h, dict) and h.get("alg") == "EdDSA" else 1)
9333
+ PYSHAPE
9334
+ then printf '%s' "TAMPERED"; return 0; fi
9335
+
9309
9336
  local jwks att
9310
9337
  jwks="$(mktemp "${TMPDIR:-/tmp}/loki-jwks.XXXXXX")" || { printf '%s' "UNCHECKED"; return 0; }
9311
9338
  python3 -E - "${_LOKI_SCRIPT_DIR}" "$pj" >"$jwks" 2>/dev/null <<'PY' || true
@@ -12593,7 +12620,7 @@ cmd_config() {
12593
12620
  echo " get KEY Get a configuration value"
12594
12621
  echo " example Print an annotated config template (for --config)"
12595
12622
  echo " schema Print the key -> env-var mapping table"
12596
- echo " validate FILE Validate a config file (--config) without applying it"
12623
+ echo " validate [FILE] Validate loki.yaml (no FILE: repo root, then ~/.loki) or a --config file"
12597
12624
  echo ""
12598
12625
  echo "Settable keys (v6.0.0):"
12599
12626
  echo " maxTier Cost ceiling: opus, sonnet, haiku (default: opus)"
@@ -12797,14 +12824,43 @@ cmd_config_schema() {
12797
12824
  loki_config_generate_schema
12798
12825
  }
12799
12826
 
12827
+ # D51: headless backlog runner. One v10 issue-mode run per open issue, each in
12828
+ # its own worktree and branch, at most N in parallel (loki.yaml concurrency).
12829
+ cmd_backlog() {
12830
+ case "${1:-}" in
12831
+ ""|--help|-h|help)
12832
+ echo -e "${BOLD}loki backlog${NC} - run a v10 issue-mode run for every matching open issue"
12833
+ echo ""
12834
+ echo "Usage: loki backlog owner/repo (--all | --label X | --issues 1,2,3) [--concurrency N] [--dry-run]"
12835
+ echo ""
12836
+ echo " --all Every open issue"
12837
+ echo " --label X Open issues with label X"
12838
+ echo " --issues 1,2,3 Exactly these issue numbers"
12839
+ echo " --concurrency N Max runs in parallel (default: loki.yaml concurrency, else 2)"
12840
+ echo " --dry-run List what would run; launch nothing"
12841
+ echo ""
12842
+ echo "Run from a checkout of owner/repo. Each issue runs in its own worktree on loki/backlog-N."
12843
+ echo "Config: loki.yaml (repo root, then ~/.loki/loki.yaml); see docs/loki.yaml.example."
12844
+ echo "Exit: 0 every issue ended VERIFIED or with a PR; 3 only budget stops; 1 otherwise; 2 usage."
12845
+ return 0
12846
+ ;;
12847
+ esac
12848
+ command -v python3 >/dev/null 2>&1 || { echo -e "${RED}loki backlog: python3 is required${NC}" >&2; return 2; }
12849
+ python3 "$_LOKI_SCRIPT_DIR/lib/backlog.py" "$@"
12850
+ }
12851
+
12800
12852
  # #691: validate a config file (--config) WITHOUT applying it. Reports
12801
12853
  # unresolved ${VAR} refs, raw-secret literals (error), and per-value validation
12802
12854
  # failures. Non-zero exit on any failure.
12803
12855
  cmd_config_validate() {
12804
- local file="${1:-}"
12805
- if [ -z "$file" ]; then
12806
- echo -e "${RED}Usage: loki config validate <file>${NC}" >&2
12807
- return 2
12856
+ local file="${1:-}" _loki_yaml
12857
+ # D51: loki.yaml (UI + headless config) has its own schema; no argument
12858
+ # validates the discovered loki.yaml (repo root, then ~/.loki/loki.yaml).
12859
+ case "$(basename -- "$file")" in "" | loki.yaml | loki.yml | loki.yaml.example) _loki_yaml=1 ;; *) _loki_yaml=0 ;; esac
12860
+ if [ "$_loki_yaml" = 1 ]; then
12861
+ command -v python3 >/dev/null 2>&1 || { echo -e "${RED}config validate: python3 is required${NC}" >&2; return 2; }
12862
+ python3 "$_LOKI_SCRIPT_DIR/lib/loki_yaml.py" validate ${file:+"$file"}
12863
+ return $?
12808
12864
  fi
12809
12865
  if ! declare -f loki_config_validate_file >/dev/null 2>&1; then
12810
12866
  echo -e "${RED}config validate: config-map.sh helper not loaded${NC}" >&2
@@ -21767,6 +21823,9 @@ main() {
21767
21823
  assets)
21768
21824
  cmd_assets "$@"
21769
21825
  ;;
21826
+ backlog)
21827
+ cmd_backlog "$@"
21828
+ ;;
21770
21829
  config)
21771
21830
  cmd_config "$@"
21772
21831
  ;;