loki-mode 10.6.8 → 10.6.10

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 (111) hide show
  1. package/README.md +15 -14
  2. package/SKILL.md +2 -2
  3. package/VERSION +1 -1
  4. package/autonomy/lib/backlog.py +167 -36
  5. package/autonomy/lib/workspace.py +133 -39
  6. package/autonomy/lib/workspace_comment.py +91 -0
  7. package/autonomy/loki +92 -22
  8. package/bin/loki +3 -3
  9. package/completions/_loki +1 -0
  10. package/completions/loki.bash +1 -1
  11. package/dashboard/__init__.py +1 -1
  12. package/dashboard/api_start.py +26 -1
  13. package/docs/INSTALLATION.md +1 -1
  14. package/docs/TOOL-INTEGRATION.md +3 -3
  15. package/docs/WORKSPACES.md +11 -4
  16. package/docs/control-plane-container.md +8 -3
  17. package/docs/dashboard-guide.md +4 -0
  18. package/docs/exit-codes.md +1 -0
  19. package/docs/slack.md +12 -4
  20. package/docs/trackers.md +27 -0
  21. package/docs/v10/BOARD.md +70 -29
  22. package/docs/v10/CONTROL-PLANE.md +14 -7
  23. package/docs/v10/D51-PHASE-B.md +3 -1
  24. package/docs/v10/D61-SPEED.md +4 -0
  25. package/docs/v10/D63-CARDS.md +354 -0
  26. package/docs/v10/DECISIONS.md +44 -0
  27. package/docs/v10/E2E-D65.md +28 -0
  28. package/docs/v10/PROGRESS.md +63 -0
  29. package/loki-ts/dist/loki.js +584 -565
  30. package/mcp/__init__.py +1 -1
  31. package/mcp/tests/test_v10_tools.py +56 -0
  32. package/mcp/v10_tools.py +13 -3
  33. package/package.json +2 -1
  34. package/packages/control-plane/dist/server.js +76 -6
  35. package/packages/control-plane/drizzle/0001_group_id.sql +3 -0
  36. package/packages/control-plane/drizzle/meta/_journal.json +8 -1
  37. package/packages/control-plane/src/db/schema.ts +3 -0
  38. package/packages/control-plane/src/server/answer.ts +1 -1
  39. package/packages/control-plane/src/server/app.ts +6 -2
  40. package/packages/control-plane/src/server/auth.ts +51 -0
  41. package/packages/control-plane/src/server/runs.ts +4 -3
  42. package/packages/control-plane/src/server/serve.ts +11 -1
  43. package/packages/control-plane/src/shipper/discover.ts +29 -0
  44. package/packages/control-plane/ui/dist/assets/{index-53E32yrO.js → index-Jt31nrK-.js} +24 -24
  45. package/packages/control-plane/ui/dist/index.html +1 -1
  46. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  47. package/schemas/features.schema.json +71 -0
  48. package/schemas/loki-yaml.schema.json +123 -0
  49. package/schemas/manifest.schema.json +77 -0
  50. package/schemas/migration-plan.schema.json +109 -0
  51. package/schemas/outcome-contract.schema.json +73 -0
  52. package/schemas/seams.schema.json +90 -0
  53. package/schemas/status-result.schema.json +30 -0
  54. package/schemas/why-result.schema.json +51 -0
  55. package/web-app/dist/assets/{AdminPage-DN_pe8vo.js → AdminPage-OaSCF3M4.js} +1 -1
  56. package/web-app/dist/assets/{Avatar-BnQsr8z-.js → Avatar-xa3G7_vs.js} +1 -1
  57. package/web-app/dist/assets/{Badge-DzERiZn2.js → Badge-Ci_UcHij.js} +1 -1
  58. package/web-app/dist/assets/{Button-CaaoxQAt.js → Button-CFrrmbvL.js} +1 -1
  59. package/web-app/dist/assets/{CockpitPage-SXhnysJA.js → CockpitPage-6hyVl_QZ.js} +1 -1
  60. package/web-app/dist/assets/{ComparePage-krxTCFJY.js → ComparePage-B5H56KCG.js} +1 -1
  61. package/web-app/dist/assets/{ErrorBoundary-BL96ESMc.js → ErrorBoundary-CBbRHsYr.js} +1 -1
  62. package/web-app/dist/assets/{EvidenceReceiptPanel-PUiGSpkB.js → EvidenceReceiptPanel-D-Zoneum.js} +1 -1
  63. package/web-app/dist/assets/{GitHubIssuesPanel-C0hPs-P2.js → GitHubIssuesPanel-D7XPRsic.js} +1 -1
  64. package/web-app/dist/assets/{GitHubPRsPanel-CLvqNUlu.js → GitHubPRsPanel-CzHbBPAn.js} +1 -1
  65. package/web-app/dist/assets/{HomePage-X-sq_Rc9.js → HomePage-ClOx5qF1.js} +2 -2
  66. package/web-app/dist/assets/{LoginPage-DhQuwdua.js → LoginPage-3Yz_9j8E.js} +1 -1
  67. package/web-app/dist/assets/{MagicPage-CP2ODjfn.js → MagicPage-Dumn48gC.js} +1 -1
  68. package/web-app/dist/assets/{MetricsPage-C6SYiQES.js → MetricsPage-CYCYVG_8.js} +1 -1
  69. package/web-app/dist/assets/{NotFoundPage-CCDxxYc-.js → NotFoundPage-CV-x_1qH.js} +1 -1
  70. package/web-app/dist/assets/{ProjectPage-yDF--8FQ.js → ProjectPage-CeO7dnyM.js} +3 -3
  71. package/web-app/dist/assets/{ProjectsPage-BBLspdLn.js → ProjectsPage-BT9mz6iD.js} +1 -1
  72. package/web-app/dist/assets/{SettingsPage-C_sWbQRw.js → SettingsPage-BL8X9fAB.js} +1 -1
  73. package/web-app/dist/assets/{ShowcasePage-xCMuL0N8.js → ShowcasePage-8f3lnSpc.js} +1 -1
  74. package/web-app/dist/assets/{SystemSettingsPage-DnJKGCx7.js → SystemSettingsPage-BUNAAzh2.js} +1 -1
  75. package/web-app/dist/assets/{TeamsPage-BBXdhrSs.js → TeamsPage-Qj-VEZJz.js} +1 -1
  76. package/web-app/dist/assets/{TemplatesPage-AZ1Lr1rn.js → TemplatesPage-b8lKqe5q.js} +1 -1
  77. package/web-app/dist/assets/{TerminalOutput-CtIm9tXc.js → TerminalOutput-jOnfQKB4.js} +1 -1
  78. package/web-app/dist/assets/{activity-Dk1FK25Q.js → activity-w7GnyBNp.js} +1 -1
  79. package/web-app/dist/assets/{bell-dhqXyzP4.js → bell-2i4ejMOA.js} +1 -1
  80. package/web-app/dist/assets/{bot-CExHd4Kf.js → bot-jaPm4NIs.js} +1 -1
  81. package/web-app/dist/assets/{check-D51zaA1a.js → check-CvNtxeKM.js} +1 -1
  82. package/web-app/dist/assets/{chevron-left-DEwnQzH1.js → chevron-left-zzBjHAtX.js} +1 -1
  83. package/web-app/dist/assets/{circle-alert-C6U90Msk.js → circle-alert-CCU0IU2m.js} +1 -1
  84. package/web-app/dist/assets/{clock-6mFbMzzH.js → clock-BvZckMwN.js} +1 -1
  85. package/web-app/dist/assets/{cloud-BrZZtE7F.js → cloud-hrMWDdYp.js} +1 -1
  86. package/web-app/dist/assets/{code-xml-DTFzeepP.js → code-xml-v6l40T-K.js} +1 -1
  87. package/web-app/dist/assets/{database-B5rEiTre.js → database-l2TMUrQd.js} +1 -1
  88. package/web-app/dist/assets/{dollar-sign-bQs4rlS_.js → dollar-sign-C2V6OvpO.js} +1 -1
  89. package/web-app/dist/assets/{file-code-corner-Mo6e6qbf.js → file-code-corner-Bytb5JGA.js} +1 -1
  90. package/web-app/dist/assets/{file-plus-DzHZ0jpP.js → file-plus-BynCYQB8.js} +1 -1
  91. package/web-app/dist/assets/{globe-DYN9UhTa.js → globe-DifDdNMH.js} +1 -1
  92. package/web-app/dist/assets/{hammer-bB0hSRcJ.js → hammer-D7TsOJD6.js} +1 -1
  93. package/web-app/dist/assets/{index-CIH5CqZB.js → index-Zohfqaz4.js} +3 -3
  94. package/web-app/dist/assets/{layers-qhFkY8Nu.js → layers-Bk8c2YLS.js} +1 -1
  95. package/web-app/dist/assets/{loader-circle-BGBkaYH7.js → loader-circle-BrLdbMwP.js} +1 -1
  96. package/web-app/dist/assets/{lock-DIcsNFum.js → lock-COpmjMyJ.js} +1 -1
  97. package/web-app/dist/assets/{package-BdIIbZhS.js → package-BMzYdY7F.js} +1 -1
  98. package/web-app/dist/assets/{plus-Co1Bv8zT.js → plus-B9xpu1Pg.js} +1 -1
  99. package/web-app/dist/assets/{refresh-cw-CGF4ABWH.js → refresh-cw-C2Lzh-AA.js} +1 -1
  100. package/web-app/dist/assets/{rotate-ccw-BX10Q79h.js → rotate-ccw-2Yyy-uQd.js} +1 -1
  101. package/web-app/dist/assets/{scroll-text-CbXDlpGp.js → scroll-text-DiK2yhsT.js} +1 -1
  102. package/web-app/dist/assets/{server-CIhi6yUO.js → server-xlJBgleU.js} +1 -1
  103. package/web-app/dist/assets/{shield-alert-DJ5fB6Dg.js → shield-alert-D9w4yBCi.js} +1 -1
  104. package/web-app/dist/assets/{thumbs-up-BEft2T94.js → thumbs-up-CcWfvcE9.js} +1 -1
  105. package/web-app/dist/assets/{trash-2-CWGUPn7Y.js → trash-2-_hk_pEAp.js} +1 -1
  106. package/web-app/dist/assets/{trending-down-CjqstvxQ.js → trending-down-CDZT99tC.js} +1 -1
  107. package/web-app/dist/assets/{trending-up-CTyLjMzE.js → trending-up-A0oreTXc.js} +1 -1
  108. package/web-app/dist/assets/{upload-BmRx-uv4.js → upload-DfLwhGG2.js} +1 -1
  109. package/web-app/dist/assets/{usePolling-ChHkSJye.js → usePolling-B7gW2YY_.js} +1 -1
  110. package/web-app/dist/assets/{user-ZXh_f35z.js → user-C-y7_Oon.js} +1 -1
  111. package/web-app/dist/index.html +1 -1
@@ -2,9 +2,11 @@
2
2
  """loki workspace: run one issue across several repos (D51 Phase B, D65).
3
3
 
4
4
  loki workspace list
5
+ loki workspace show <name>
5
6
  loki workspace run <name> <issue-ref>
7
+ loki workspace status [<run-id>]
6
8
 
7
- Gated by LOKI_WORKSPACES=1. Workspaces are defined in loki.yaml:
9
+ On by default (LOKI_WORKSPACES=0 disables). Workspaces are defined in loki.yaml:
8
10
 
9
11
  workspaces:
10
12
  shop:
@@ -20,7 +22,12 @@ checked out, and evidence lands in
20
22
  .loki/workspaces/<name>/<run-id>/integration.json. Integration evidence is not
21
23
  part of the Seal.
22
24
 
23
- Exit: 0 all repos ok and integration not failed; 1 otherwise; 2 usage/config.
25
+ Repos with no pending `after` run in parallel, at most `concurrency` at once
26
+ (workspace key or LOKI_WORKSPACE_CONCURRENCY, default 2). SIGINT and SIGTERM
27
+ kill each recorded child process group.
28
+
29
+ Exit: 0 all repos ok and integration not failed; 3 when the only non-ok
30
+ outcomes are budget stops (child exit 3); 1 otherwise; 2 usage/config.
24
31
 
25
32
  Test seam: LOKI_WORKSPACE_LAUNCHER replaces the engine launcher (called with
26
33
  the issue ref or task text, cwd = the repo worktree).
@@ -33,6 +40,8 @@ import re
33
40
  import signal
34
41
  import subprocess
35
42
  import sys
43
+ import threading
44
+ import time
36
45
 
37
46
  HERE = os.path.dirname(os.path.abspath(__file__))
38
47
  sys.path.insert(0, HERE)
@@ -46,8 +55,12 @@ def say(msg):
46
55
  print(msg, flush=True)
47
56
 
48
57
 
58
+ BUDGET_EXIT = 3
59
+
60
+
49
61
  def enabled(env=None):
50
- return (os.environ if env is None else env).get("LOKI_WORKSPACES") == "1"
62
+ """On by default; LOKI_WORKSPACES=0 disables."""
63
+ return (os.environ if env is None else env).get("LOKI_WORKSPACES") != "0"
51
64
 
52
65
 
53
66
  def git(*args, cwd=None):
@@ -194,46 +207,73 @@ def run_workspace(name, ref, workspaces, base_dir=None, launcher=None):
194
207
  launcher = launcher or os.environ.get("LOKI_WORKSPACE_LAUNCHER") or os.path.join(REPO_ROOT, "bin", "loki")
195
208
  child_env_base = dict(os.environ, LOKI_ENGINE="v10", LOKI_NO_BROWSER="1")
196
209
 
197
- outcome, worktrees, heads = {}, {}, {}
198
- running = {}
210
+ outcome, worktrees, heads, kind = {}, {}, {}, {}
211
+ running = {} # repo -> (Popen, log file handle)
212
+ try:
213
+ conc = max(1, int(os.environ.get("LOKI_WORKSPACE_CONCURRENCY") or ws.get("concurrency") or 2))
214
+ except (TypeError, ValueError):
215
+ conc = 2
199
216
 
200
217
  def stop(*_):
201
- for p in running.values():
218
+ for p, _lf in running.values():
202
219
  try:
203
220
  os.killpg(p.pid, signal.SIGTERM)
204
221
  except OSError:
205
222
  pass
206
223
  sys.exit(130)
207
- signal.signal(signal.SIGTERM, stop)
208
-
209
- for i, entry in enumerate(ordered):
210
- repo = entry["repo"]
211
- failed_dep = [d for d in entry.get("after") or [] if outcome.get(d) != "ok"]
212
- if failed_dep:
213
- outcome[repo] = "SKIPPED (predecessor failed: %s)" % ", ".join(failed_dep)
214
- say("workspace: %s %s" % (repo, outcome[repo]))
215
- continue
216
- try:
217
- src = source_checkout(entry)
218
- dest = os.path.join(run_dir, "worktrees", _slug(repo))
219
- wt = prepare(src, dest, "loki/ws-%s-%d" % (run_id, i), entry.get("setup"))
220
- except (RuntimeError, OSError) as e:
221
- outcome[repo] = "FAILED: %s" % e
222
- say("workspace: %s %s" % (repo, outcome[repo]))
223
- continue
224
- worktrees[repo] = wt
225
- siblings = [os.path.join(run_dir, "worktrees", _slug(o["repo"])) for o in ordered if o["repo"] != repo]
226
- arg, extra = task_for(entry, ref, siblings)
227
- log = os.path.join(run_dir, "%s.log" % _slug(repo))
228
- say("workspace: %s running" % repo)
229
- with open(log, "w") as lf:
224
+ if threading.current_thread() is threading.main_thread():
225
+ signal.signal(signal.SIGTERM, stop)
226
+ signal.signal(signal.SIGINT, stop)
227
+
228
+ index = {e["repo"]: i for i, e in enumerate(ordered)}
229
+ pending = list(ordered)
230
+ while pending or running:
231
+ for entry in list(pending):
232
+ repo = entry["repo"]
233
+ deps = entry.get("after") or []
234
+ failed_dep = [d for d in deps if d in kind and kind[d] != "ok"]
235
+ if failed_dep:
236
+ pending.remove(entry)
237
+ outcome[repo] = "SKIPPED (predecessor failed: %s)" % ", ".join(failed_dep)
238
+ kind[repo] = "budget" if all(kind[d] == "budget" for d in failed_dep) else "fail"
239
+ say("workspace: %s %s" % (repo, outcome[repo]))
240
+ for entry in list(pending):
241
+ repo = entry["repo"]
242
+ if len(running) >= conc:
243
+ break
244
+ if not all(kind.get(d) == "ok" for d in entry.get("after") or []):
245
+ continue
246
+ pending.remove(entry)
247
+ try:
248
+ src = source_checkout(entry)
249
+ dest = os.path.join(run_dir, "worktrees", _slug(repo))
250
+ wt = prepare(src, dest, "loki/ws-%s-%d" % (run_id, index[repo]), entry.get("setup"))
251
+ except (RuntimeError, OSError) as e:
252
+ outcome[repo], kind[repo] = "FAILED: %s" % e, "fail"
253
+ say("workspace: %s %s" % (repo, outcome[repo]))
254
+ continue
255
+ worktrees[repo] = wt
256
+ siblings = [os.path.join(run_dir, "worktrees", _slug(o["repo"])) for o in ordered if o["repo"] != repo]
257
+ arg, extra = task_for(entry, ref, siblings)
258
+ lf = open(os.path.join(run_dir, "%s.log" % _slug(repo)), "w")
259
+ say("workspace: %s running" % repo)
230
260
  p = subprocess.Popen([launcher, arg], cwd=wt, env=dict(child_env_base, **extra), stdout=lf,
231
261
  stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL, start_new_session=True)
232
- running[repo] = p
233
- rc = p.wait()
234
- running.pop(repo, None)
235
- outcome[repo] = "ok" if rc == 0 else "FAILED: exit %d" % rc
236
- say("workspace: %s %s" % (repo, outcome[repo]))
262
+ running[repo] = (p, lf)
263
+ for repo, (p, lf) in list(running.items()):
264
+ rc = p.poll()
265
+ if rc is None:
266
+ continue
267
+ lf.close()
268
+ running.pop(repo)
269
+ if rc == 0:
270
+ outcome[repo], kind[repo] = "ok", "ok"
271
+ else:
272
+ outcome[repo] = "FAILED: exit %d" % rc
273
+ kind[repo] = "budget" if rc == BUDGET_EXIT else "fail"
274
+ say("workspace: %s %s" % (repo, outcome[repo]))
275
+ if running:
276
+ time.sleep(0.05)
237
277
 
238
278
  for repo, wt in worktrees.items():
239
279
  r = git("rev-parse", "HEAD", cwd=wt)
@@ -247,21 +287,71 @@ def run_workspace(name, ref, workspaces, base_dir=None, launcher=None):
247
287
  json.dump(ev, f, indent=2, sort_keys=True)
248
288
  os.replace(tmp, ev_path)
249
289
 
290
+ try:
291
+ import workspace_comment
292
+ workspace_comment.post_comments(name, run_id, run_dir, worktrees, ev, say)
293
+ except Exception as e: # commenting must never fail the run
294
+ say("workspace: commenting skipped (%s)" % type(e).__name__)
295
+
250
296
  say("")
251
297
  say("Summary: workspace %s (%s)" % (name, run_id))
252
298
  for repo, res in outcome.items():
253
299
  say("%-30s %s" % (repo, res))
254
300
  say("%-30s %s" % ("integration", ev["status"]))
255
- bad = any(v != "ok" for v in outcome.values()) or ev["status"] == "failed"
256
- return (1 if bad else 0), ev_path
301
+ if ev["status"] == "failed" or any(k == "fail" for k in kind.values()):
302
+ return 1, ev_path
303
+ return (3 if any(k == "budget" for k in kind.values()) else 0), ev_path
304
+
305
+
306
+ def _runs(base_dir):
307
+ """Yield (workspace, run_id, evidence dict) for every recorded run, oldest first."""
308
+ root = os.path.join(base_dir, ".loki", "workspaces")
309
+ if not os.path.isdir(root):
310
+ return
311
+ for name in sorted(os.listdir(root)):
312
+ for run_id in sorted(os.listdir(os.path.join(root, name))):
313
+ path = os.path.join(root, name, run_id, "integration.json")
314
+ try:
315
+ with open(path) as f:
316
+ yield name, run_id, json.load(f)
317
+ except (OSError, ValueError):
318
+ continue
319
+
320
+
321
+ def status(base_dir, run_id=None, say=say):
322
+ """Print per-repo rows from integration.json for one run, or for every run."""
323
+ found = [r for r in _runs(base_dir) if run_id in (None, r[1])]
324
+ if not found:
325
+ say("workspace: no such run %s" % run_id if run_id else "no workspace runs recorded")
326
+ return 0 if run_id is None else 1
327
+ for name, rid, ev in found:
328
+ say("%s %s" % (name, rid))
329
+ for repo, res in sorted((ev.get("outcomes") or {}).items()):
330
+ say("%-30s %s" % (repo, res))
331
+ say("%-30s %s" % ("integration", ev.get("status")))
332
+ return 0
333
+
334
+
335
+ def show(name, workspaces, say=say):
336
+ ws = workspaces.get(name)
337
+ if ws is None:
338
+ say("workspace: unknown workspace %r (known: %s)" % (name, ", ".join(sorted(workspaces)) or "none"))
339
+ return 2
340
+ say("%s (concurrency %s)" % (name, ws.get("concurrency") or 2))
341
+ for e in ws.get("repos") or []:
342
+ extra = "; after: " + ", ".join(e["after"]) if e.get("after") else ""
343
+ say("%s path: %s%s" % (e.get("repo", "?"), e.get("path") or "(clone)", extra))
344
+ integ = ws.get("integration") or {}
345
+ say("integration: %s" % (integ.get("command") or "not configured"))
346
+ return 0
257
347
 
258
348
 
259
349
  def main(argv):
260
350
  if not enabled():
261
- say("loki workspace is disabled. Set LOKI_WORKSPACES=1 to use it.")
351
+ say("loki workspace is disabled (LOKI_WORKSPACES=0). Unset it to use workspaces.")
262
352
  return 2
263
353
  if not argv or argv[0] in ("-h", "--help", "help"):
264
- say("Usage: loki workspace list | run <name> <owner/repo#N | task text>")
354
+ say("Usage: loki workspace list | show <name> | run <name> <owner/repo#N | task text> | status [<run-id>]")
265
355
  return 0 if argv else 2
266
356
  try:
267
357
  workspaces, errors = load_workspaces()
@@ -279,10 +369,14 @@ def main(argv):
279
369
  for n, ws in sorted(workspaces.items()):
280
370
  say("%s: %s" % (n, ", ".join(e.get("repo", "?") for e in ws.get("repos") or [])))
281
371
  return 0
372
+ if cmd == "show" and len(argv) == 2:
373
+ return show(argv[1], workspaces)
374
+ if cmd == "status" and len(argv) <= 2:
375
+ return status(os.getcwd(), argv[1] if len(argv) == 2 else None)
282
376
  if cmd == "run" and len(argv) >= 3:
283
377
  rc, _ = run_workspace(argv[1], " ".join(argv[2:]), workspaces)
284
378
  return rc
285
- say("Usage: loki workspace list | run <name> <issue-ref>")
379
+ say("Usage: loki workspace list | show <name> | run <name> <issue-ref> | status [<run-id>]")
286
380
  return 2
287
381
 
288
382
 
@@ -0,0 +1,91 @@
1
+ """Post the workspace integration result as one PR comment per repo PR (D51-B11).
2
+
3
+ No LLM. Uses the `gh` CLI with the caller's own environment, so GH_TOKEN or
4
+ GITHUB_TOKEN is read by gh itself and never placed in argv, the comment body
5
+ or any log line. Commenting is best effort: it never raises and never changes
6
+ the run's exit code. Opt out with LOKI_WORKSPACE_COMMENT=0.
7
+ """
8
+ import os
9
+ import re
10
+ import subprocess
11
+ import tempfile
12
+
13
+ GH_TIMEOUT_S = 60
14
+
15
+
16
+ def verdict(ev):
17
+ """Map integration evidence to a label. Only a clean exit 0 reads as PASSED."""
18
+ status = ev.get("status")
19
+ if "timeout" in str(ev.get("note") or "").lower():
20
+ return "TIMEOUT"
21
+ if status == "passed" and ev.get("exit_code") == 0:
22
+ return "PASSED"
23
+ if status == "not_configured":
24
+ return "NOT CONFIGURED"
25
+ return "FAILED"
26
+
27
+
28
+ def render(ws_name, run_id, repo, ev):
29
+ heads = ev.get("heads") or {}
30
+ lines = ["Loki workspace `%s` run `%s`" % (ws_name, run_id), "",
31
+ "Integration: **%s** (exit code %s)" % (verdict(ev), ev.get("exit_code")), "",
32
+ "| Repo | Outcome | Head SHA |", "| --- | --- | --- |"]
33
+ outcomes = ev.get("outcomes") or {}
34
+ for r in sorted(heads.keys() | outcomes.keys()):
35
+ out = outcomes.get(r, "unknown")
36
+ out = "ok" if out == "ok" else str(out).replace("|", "/")
37
+ mark = " (this PR)" if r == repo else ""
38
+ lines.append("| %s%s | %s | %s |" % (r, mark, out, heads.get(r) or "none"))
39
+ if ev.get("log_sha256"):
40
+ lines += ["", "Integration log sha256: `%s`" % ev["log_sha256"]]
41
+ lines += ["", "Integration evidence is not part of the Seal."]
42
+ return "\n".join(lines) + "\n"
43
+
44
+
45
+ def _gh(args, cwd):
46
+ return subprocess.run(["gh"] + args, cwd=cwd, stdin=subprocess.DEVNULL, capture_output=True,
47
+ text=True, timeout=GH_TIMEOUT_S)
48
+
49
+
50
+ def _branch(wt):
51
+ r = subprocess.run(["git", "-C", wt, "rev-parse", "--abbrev-ref", "HEAD"], capture_output=True, text=True,
52
+ timeout=GH_TIMEOUT_S)
53
+ b = r.stdout.strip()
54
+ return b if r.returncode == 0 and b and b != "HEAD" else None
55
+
56
+
57
+ def post_comments(ws_name, run_id, run_dir, worktrees, ev, say):
58
+ """One comment per repo that has an open PR on its run branch. Never raises."""
59
+ if os.environ.get("LOKI_WORKSPACE_COMMENT") == "0":
60
+ return 0
61
+ posted = 0
62
+ try:
63
+ for repo, wt in sorted(worktrees.items()):
64
+ try:
65
+ branch = _branch(wt)
66
+ if not branch:
67
+ continue
68
+ r = _gh(["pr", "list", "--repo", repo, "--head", branch, "--state", "open",
69
+ "--json", "number", "--jq", ".[0].number"], wt)
70
+ num = r.stdout.strip()
71
+ if r.returncode != 0 or not re.fullmatch(r"\d+", num):
72
+ continue
73
+ fd, body = tempfile.mkstemp(prefix="comment-", suffix=".md", dir=run_dir) # mode 0600
74
+ try:
75
+ with os.fdopen(fd, "w") as f:
76
+ f.write(render(ws_name, run_id, repo, ev))
77
+ c = _gh(["pr", "comment", num, "--repo", repo, "--body-file", body], wt)
78
+ finally:
79
+ try:
80
+ os.unlink(body)
81
+ except OSError:
82
+ pass
83
+ if c.returncode == 0:
84
+ posted += 1
85
+ else:
86
+ say("workspace: comment on %s#%s failed (gh exit %d)" % (repo, num, c.returncode))
87
+ except (OSError, subprocess.SubprocessError, ValueError) as e:
88
+ say("workspace: comment for %s skipped (%s)" % (repo, type(e).__name__))
89
+ except Exception as e: # never fail the run because commenting failed
90
+ say("workspace: commenting skipped (%s)" % type(e).__name__)
91
+ return posted
package/autonomy/loki CHANGED
@@ -1630,7 +1630,7 @@ show_help() {
1630
1630
  echo " start [SPEC] Start a build (PRD file, GitHub/GitLab/Jira/Azure issue, or no arg)"
1631
1631
  echo " plan <PRD-file> Dry-run analysis: complexity, cost, execution plan"
1632
1632
  echo " backlog <owner/repo> --all|--label X|--issues N,N Run every matching open issue, N in parallel"
1633
- echo " workspace list | run <name> <ref> Multi-repo runs from loki.yaml workspaces (LOKI_WORKSPACES=1)"
1633
+ echo " workspace list | run <name> <ref> Multi-repo runs from loki.yaml workspaces (LOKI_WORKSPACES=0 disables)"
1634
1634
  echo " quickstart [idea] Guided first build: setup, idea, template, plan, go"
1635
1635
  echo ""
1636
1636
  echo "Session:"
@@ -1783,7 +1783,7 @@ show_help() {
1783
1783
  echo " syslog telemetry template test tour trigger trust trust-metrics"
1784
1784
  echo " ultracode update verify version voice watch watchdog web welcome why"
1785
1785
  echo " wiki worktree (wt)"
1786
- echo " control (preview, needs LOKI_CONTROL=1)"
1786
+ echo " control"
1787
1787
  echo ""
1788
1788
  echo "Any command: loki <command> --help"
1789
1789
  echo ""
@@ -1897,7 +1897,74 @@ _loki_dashboard_port_is_ours() {
1897
1897
  return 1
1898
1898
  }
1899
1899
 
1900
+ # C3: the Control Plane is the default UI. Prints a live loopback URL from
1901
+ # ~/.loki/control/instance.json (pid alive, http loopback only, /health says
1902
+ # service=loki-control), or starts `loki control serve` detached and waits for
1903
+ # it to publish one. Returns 1 when the caller should fall back to the old
1904
+ # dashboard (LOKI_CONTROL_DEFAULT=0, no bun, no python3, server did not start).
1905
+ _loki_control_live_url() {
1906
+ local inst="${HOME}/.loki/control/instance.json" fields pid url health
1907
+ [ -f "$inst" ] || return 1
1908
+ fields=$(python3 - "$inst" 2>/dev/null <<'PY'
1909
+ import json, sys, urllib.parse
1910
+ d = json.load(open(sys.argv[1]))
1911
+ pid, url = d.get("pid"), d.get("url")
1912
+ if not isinstance(pid, int) or isinstance(pid, bool) or not isinstance(url, str):
1913
+ sys.exit(1)
1914
+ u = urllib.parse.urlparse(url)
1915
+ if u.scheme != "http" or u.hostname not in ("127.0.0.1", "localhost", "::1"):
1916
+ sys.exit(1)
1917
+ if u.path not in ("", "/") or u.query or u.fragment or u.params or "@" in u.netloc or "?" in url or "#" in url:
1918
+ sys.exit(1)
1919
+ print(pid, url.rstrip("/"))
1920
+ PY
1921
+ ) || return 1
1922
+ pid="${fields%% *}"; url="${fields#* }"
1923
+ kill -0 "$pid" 2>/dev/null || return 1
1924
+ health=$(curl -g -s --max-time 2 "${url}/health" 2>/dev/null) || return 1
1925
+ case "$health" in *'"service"'*'"loki-control"'*) ;; *) return 1 ;; esac
1926
+ printf '%s\n' "$url"
1927
+ }
1928
+
1929
+ _loki_control_ui() {
1930
+ [ "${LOKI_CONTROL_DEFAULT:-1}" = "0" ] && return 1
1931
+ local url="" open_it=1 arg i
1932
+ for arg in "$@"; do [ "$arg" = "--no-open" ] && open_it=0; done
1933
+ if [ "${LOKI_HEADLESS:-0}" = "1" ] || [ "${LOKI_NO_BROWSER:-0}" = "1" ]; then open_it=0; fi
1934
+ command -v python3 >/dev/null 2>&1 || return 1
1935
+ url=$(_loki_control_live_url) || url=""
1936
+ if [ -z "$url" ]; then
1937
+ if ! command -v bun >/dev/null 2>&1; then
1938
+ echo "loki: the Control Plane needs bun (https://bun.sh); using the classic dashboard." >&2
1939
+ return 1
1940
+ fi
1941
+ local cli="$SKILL_DIR/loki-ts/dist/loki.js" cdir="${HOME}/.loki/control" spid
1942
+ [ -f "$cli" ] || cli="$SKILL_DIR/loki-ts/src/cli.ts"
1943
+ mkdir -p "$cdir" && chmod 700 "$cdir" 2>/dev/null || true
1944
+ LOKI_CONTROL=1 LOKI_NO_BROWSER=1 nohup bun "$cli" control serve >"$cdir/serve.log" 2>&1 </dev/null &
1945
+ spid=$!
1946
+ echo "$spid" > "$cdir/serve.pid" 2>/dev/null || true
1947
+ for i in $(seq 1 100); do
1948
+ url=$(_loki_control_live_url) && break
1949
+ url=""
1950
+ kill -0 "$spid" 2>/dev/null || break
1951
+ sleep 0.1
1952
+ done
1953
+ if [ -z "$url" ]; then
1954
+ echo "loki: the Control Plane did not start (see $cdir/serve.log); using the classic dashboard." >&2
1955
+ return 1
1956
+ fi
1957
+ fi
1958
+ if [ "$open_it" = "1" ]; then
1959
+ loki_open_url "$url" || echo "$url"
1960
+ else
1961
+ echo "$url"
1962
+ fi
1963
+ return 0
1964
+ }
1965
+
1900
1966
  cmd_ui() {
1967
+ _loki_control_ui "$@" && return 0
1901
1968
  local port="" pid=""
1902
1969
  if [ -f "$DASHBOARD_PID_FILE" ]; then
1903
1970
  pid=$(cat "$DASHBOARD_PID_FILE" 2>/dev/null)
@@ -7325,16 +7392,15 @@ _loki_dashboard_bind_is_safe() {
7325
7392
  cmd_dashboard() {
7326
7393
  local subcommand="${1:-}"
7327
7394
 
7328
- # LOKI_CONTROL_DEFAULT=1 (off by default): open the Control Plane instead of the old dashboard.
7329
- # The old dashboard stays available when the flag is unset, and via `loki dashboard stop|status|url`.
7330
- if [ "${LOKI_CONTROL_DEFAULT:-0}" = "1" ]; then
7331
- case "$subcommand" in
7332
- ""|start|open)
7333
- echo "LOKI_CONTROL_DEFAULT=1: starting the Control Plane (unset it to use the old dashboard)"
7334
- LOKI_CONTROL=1 exec "$0" control serve
7335
- ;;
7336
- esac
7337
- fi
7395
+ # The Control Plane is the default UI; LOKI_CONTROL_DEFAULT=0 restores the classic dashboard.
7396
+ # Only the bare forms are taken over; `start --port ...` and friends keep the classic server.
7397
+ case "$subcommand" in
7398
+ ""|start|open)
7399
+ if [ $# -le 1 ] || { [ $# -eq 2 ] && [ "$2" = "--no-open" ]; }; then
7400
+ _loki_control_ui "${@:2}" && return 0
7401
+ fi
7402
+ ;;
7403
+ esac
7338
7404
 
7339
7405
  # If no subcommand provided, show help (backward compatible with old behavior)
7340
7406
  if [ -z "$subcommand" ]; then
@@ -12921,10 +12987,10 @@ cmd_config_schema() {
12921
12987
  loki_config_generate_schema
12922
12988
  }
12923
12989
 
12924
- # D65: multi-repo workspaces (D51 Phase B). Gated by LOKI_WORKSPACES=1.
12990
+ # D65: multi-repo workspaces (D51 Phase B). On by default; LOKI_WORKSPACES=0 disables.
12925
12991
  cmd_workspace() {
12926
- if [ "${LOKI_WORKSPACES:-}" != "1" ]; then
12927
- echo -e "${RED}loki workspace is disabled.${NC} Set LOKI_WORKSPACES=1 to use it." >&2
12992
+ if [ "${LOKI_WORKSPACES:-}" = "0" ]; then
12993
+ echo -e "${RED}loki workspace is disabled.${NC} Unset LOKI_WORKSPACES (it is set to 0)." >&2
12928
12994
  return 2
12929
12995
  fi
12930
12996
  command -v python3 >/dev/null 2>&1 || { echo -e "${RED}loki workspace: python3 is required${NC}" >&2; return 2; }
@@ -18714,10 +18780,12 @@ with open(manifest_path, 'w') as f:
18714
18780
  # usable.
18715
18781
  # ---------------------------------------------------------------------------
18716
18782
  cmd_verify() {
18717
- # D48: --pubkey verifies a receipt against a given public key (v10 engine, no ~/.loki needed).
18783
+ # D48: --pubkey FILE or --pubkey=FILE (any position) verifies a receipt against a given public key (v10 engine, no ~/.loki needed).
18718
18784
  local _pk_arg
18719
18785
  for _pk_arg in "$@"; do
18720
- [ "$_pk_arg" = "--pubkey" ] && LOKI_ENGINE=v10 exec "$_LOKI_SCRIPT_DIR/../bin/loki" verify "$@"
18786
+ case "$_pk_arg" in
18787
+ --pubkey|--pubkey=*) LOKI_ENGINE=v10 exec "$_LOKI_SCRIPT_DIR/../bin/loki" verify "$@" ;;
18788
+ esac
18721
18789
  done
18722
18790
  # --fast: deterministic checks only, in milliseconds. Measured on this repo
18723
18791
  # (1,932 tracked files): 298 ms cold, 87 ms warm, 19 ms diff-scoped, against
@@ -21897,6 +21965,8 @@ main() {
21897
21965
  echo "Alias for 'loki api start'. Starts the dashboard HTTP API"
21898
21966
  echo "server (FastAPI, port 57374 by default)."
21899
21967
  echo ""
21968
+ echo "For the UI, run 'loki' (opens the Control Plane) or 'loki control serve'."
21969
+ echo ""
21900
21970
  echo "Options:"
21901
21971
  echo " --host HOST Bind host (default: 127.0.0.1)"
21902
21972
  echo " --port PORT Bind port (default: 57374)"
@@ -33870,14 +33940,14 @@ cmd_outcomes() {
33870
33940
  LOKI_DIR="${LOKI_DIR:-.loki}" python3 "$lib" "$@"
33871
33941
  }
33872
33942
 
33873
- # loki control -- D56 control plane (preview). The live route is the Bun CLI
33943
+ # loki control -- D56 control plane (on by default). The live route is the Bun CLI
33874
33944
  # (loki-ts/src/commands/control.ts); this is the LOKI_LEGACY_BASH / no-shim
33875
- # fallback. It keeps the gate and help in bash and hands the work to Bun.
33945
+ # fallback. It keeps the off switch and help in bash and hands the work to Bun.
33876
33946
  cmd_control() {
33877
33947
  local sub="${1:-}"
33878
33948
  case "$sub" in
33879
33949
  ""|-h|--help|help)
33880
- echo "Usage: loki control <command> [options] (preview, requires LOKI_CONTROL=1)"
33950
+ echo "Usage: loki control <command> [options] (on by default; LOKI_CONTROL=0 turns it off; requires bun)"
33881
33951
  echo ""
33882
33952
  echo "Commands:"
33883
33953
  echo " serve [--port N] [--db PATH] Run the control plane + UI on 127.0.0.1"
@@ -33886,8 +33956,8 @@ cmd_control() {
33886
33956
  return 0
33887
33957
  ;;
33888
33958
  esac
33889
- if [ "${LOKI_CONTROL:-}" != "1" ]; then
33890
- echo "loki control is in preview. Enable it with: export LOKI_CONTROL=1"
33959
+ if [ "${LOKI_CONTROL:-}" = "0" ]; then
33960
+ echo "loki control is off (LOKI_CONTROL=0). Unset it to turn the Control Plane back on."
33891
33961
  return 0
33892
33962
  fi
33893
33963
  local cli="$SKILL_DIR/loki-ts/dist/loki.js"
package/bin/loki CHANGED
@@ -354,12 +354,12 @@ case "${LOKI_ENGINE:-$_loki_engine_default}" in
354
354
  # `loki <ref>`; any other start call (PRD file, flags first, one word) stays legacy.
355
355
  start)
356
356
  case "${2:-}" in
357
- */*\#[0-9]*|http*://*/issues/*|http*://*/browse/*|*" "*) _e10=1; _e10_args=("${@:2}") ;;
357
+ */*\#[0-9]*|http*://*/issues/*|http*://*/browse/*|jira:*|linear:*|https://linear.app/*/issue/*|*" "*) _e10=1; _e10_args=("${@:2}") ;;
358
358
  *) _e10=0 ;;
359
359
  esac
360
360
  ;;
361
361
  # http*://*/issues/* also covers GitLab's /-/issues/ URLs.
362
- */*\#[0-9]*|http*://*/issues/*|http*://*/browse/*) _e10=1 ;;
362
+ */*\#[0-9]*|http*://*/issues/*|http*://*/browse/*|jira:*|linear:*|https://linear.app/*/issue/*) _e10=1 ;;
363
363
  *" "*) _e10=1 ;;
364
364
  # ponytail: a one-word argument stays on the legacy CLI. The telemetry
365
365
  # allowlist is not a command registry (estimate, intent, outcomes are
@@ -567,7 +567,7 @@ fi
567
567
  # Two-token routes (provider show/list, memory list/index) match on the first
568
568
  # token only; the Bun dispatcher handles subcommand routing internally.
569
569
  case "${1:-}" in
570
- version|--version|-v|status|stats|doctor|provider|memory|rollback|internal|kpis|trust|proof|receipt|wiki|crash|control|contract)
570
+ version|--version|-v|status|stats|doctor|provider|memory|rollback|internal|kpis|trust|proof|receipt|wiki|crash|control|contract|slack|answer)
571
571
  # v7.5.2: rollback added (wires loki-ts/src/commands/rollback.ts).
572
572
  # v7.5.3: internal added for autonomy/run.sh phase1-hooks calls.
573
573
  # v7.5.28: kpis added (Phase K MVP: read-only KPI snapshot).
package/completions/_loki CHANGED
@@ -206,6 +206,7 @@ function _loki_commands {
206
206
  'crash:Crash report management'
207
207
  'otel:OpenTelemetry management'
208
208
  'wiki:Wiki commands'
209
+ 'answer:Resume a BLOCKED run with an answer'
209
210
  'control:Control plane (preview, LOKI_CONTROL=1)'
210
211
  'workspace:Multi-project workspaces (preview, LOKI_WORKSPACES=1)'
211
212
  'backlog:Run every matching open issue, N in parallel'
@@ -5,7 +5,7 @@ _loki_completion() {
5
5
  _init_completion || return
6
6
 
7
7
  # Main subcommands (must match autonomy/loki main case statement)
8
- local main_commands="start backlog quick monitor demo tour welcome init stop pause resume steer status next ship dashboard web serve api sandbox notify import github issue intent config provider reset memory compound checkpoint council dogfood projects enterprise secrets cockpit secure own handoff doctor watchdog audit metrics syslog onboard share proof receipt outcomes explain plan report cost estimate kpis stats test ci watch telemetry agent context ctx code run export review optimize heal modernize migrate cluster worktree wt trigger failover remote deploy docker mcp magic assets analyze compliance crash control workspace open otel preview quickstart rc rollback self-update sentrux setup-skill spec state template trust trust-metrics ultracode update verify voice why wiki bench cleanup logs grill docs cp version completions help keys"
8
+ local main_commands="start backlog quick monitor demo tour welcome init stop pause resume steer status next ship dashboard web serve api sandbox notify import github issue intent config provider reset memory compound checkpoint council dogfood projects enterprise secrets cockpit secure own handoff doctor watchdog audit metrics syslog onboard share proof receipt outcomes explain plan report cost estimate kpis stats test ci watch telemetry agent context ctx code run export review optimize heal modernize migrate cluster worktree wt trigger failover remote deploy docker mcp magic assets analyze compliance crash control workspace open otel preview quickstart rc rollback self-update sentrux setup-skill spec state template trust trust-metrics ultracode update verify voice why wiki bench cleanup logs grill docs cp version completions help keys answer"
9
9
 
10
10
  # 1. If we are on the first argument (subcommand)
11
11
  if [[ $cword -eq 1 ]]; then
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "10.6.8"
10
+ __version__ = "10.6.10"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -21,6 +21,7 @@ import os
21
21
  import re
22
22
  import shutil
23
23
  import subprocess
24
+ import sys
24
25
  import threading
25
26
  import time
26
27
  import urllib.error
@@ -156,6 +157,25 @@ def _project_dir() -> str:
156
157
  return os.environ.get("LOKI_PROJECT_DIR") or os.getcwd()
157
158
 
158
159
 
160
+ def _autonomy_lib():
161
+ lib = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "autonomy", "lib")
162
+ if lib not in sys.path:
163
+ sys.path.insert(0, lib)
164
+
165
+
166
+ def _workspaces_enabled() -> bool:
167
+ """Same rule as `loki workspace`: on by default, LOKI_WORKSPACES=0 disables."""
168
+ _autonomy_lib()
169
+ import workspace
170
+ return workspace.enabled()
171
+
172
+
173
+ def _worktree_prep():
174
+ _autonomy_lib()
175
+ import worktree_prep
176
+ return worktree_prep
177
+
178
+
159
179
  def _prepare_workdir(repo: str, number: int, token: str) -> str:
160
180
  """One git worktree per issue, off the repo's default branch."""
161
181
  env = dict(os.environ, GH_TOKEN=token, GIT_TERMINAL_PROMPT="0")
@@ -180,7 +200,12 @@ def _prepare_workdir(repo: str, number: int, token: str) -> str:
180
200
  stamp = time.strftime("%Y%m%d%H%M%S")
181
201
  wt = _home() / "worktrees" / slug / ("issue-%d-%s" % (number, stamp))
182
202
  wt.parent.mkdir(parents=True, exist_ok=True)
183
- _git("worktree", "add", "-b", "loki/backlog-%d-%s" % (number, stamp), str(wt), start, cwd=base)
203
+ branch = "loki/backlog-%d-%s" % (number, stamp)
204
+ if _workspaces_enabled():
205
+ # Shared worktree prep (D51-B06): same per-base lock and dep copy as backlog and workspace runs.
206
+ _worktree_prep().prepare_worktree(base, str(wt), branch, base=start)
207
+ return str(wt)
208
+ _git("worktree", "add", "-b", branch, str(wt), start, cwd=base)
184
209
  return str(wt)
185
210
 
186
211
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v10.6.8
5
+ **Version:** v10.6.10
6
6
 
7
7
  **Engine note:** `loki "<task>"`, `loki owner/repo#N` and `loki quick` run the Loki 10 engine. `loki start` still routes to the legacy engine, which is being removed (planned work resumes 2026-10-07); prefer `loki owner/repo#N`. See [docs/v10/GUIDE.md](v10/GUIDE.md).
8
8