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.
- package/README.md +15 -14
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/lib/backlog.py +167 -36
- package/autonomy/lib/workspace.py +133 -39
- package/autonomy/lib/workspace_comment.py +91 -0
- package/autonomy/loki +92 -22
- package/bin/loki +3 -3
- package/completions/_loki +1 -0
- package/completions/loki.bash +1 -1
- package/dashboard/__init__.py +1 -1
- package/dashboard/api_start.py +26 -1
- package/docs/INSTALLATION.md +1 -1
- package/docs/TOOL-INTEGRATION.md +3 -3
- package/docs/WORKSPACES.md +11 -4
- package/docs/control-plane-container.md +8 -3
- package/docs/dashboard-guide.md +4 -0
- package/docs/exit-codes.md +1 -0
- package/docs/slack.md +12 -4
- package/docs/trackers.md +27 -0
- package/docs/v10/BOARD.md +70 -29
- package/docs/v10/CONTROL-PLANE.md +14 -7
- package/docs/v10/D51-PHASE-B.md +3 -1
- package/docs/v10/D61-SPEED.md +4 -0
- package/docs/v10/D63-CARDS.md +354 -0
- package/docs/v10/DECISIONS.md +44 -0
- package/docs/v10/E2E-D65.md +28 -0
- package/docs/v10/PROGRESS.md +63 -0
- package/loki-ts/dist/loki.js +584 -565
- package/mcp/__init__.py +1 -1
- package/mcp/tests/test_v10_tools.py +56 -0
- package/mcp/v10_tools.py +13 -3
- package/package.json +2 -1
- package/packages/control-plane/dist/server.js +76 -6
- package/packages/control-plane/drizzle/0001_group_id.sql +3 -0
- package/packages/control-plane/drizzle/meta/_journal.json +8 -1
- package/packages/control-plane/src/db/schema.ts +3 -0
- package/packages/control-plane/src/server/answer.ts +1 -1
- package/packages/control-plane/src/server/app.ts +6 -2
- package/packages/control-plane/src/server/auth.ts +51 -0
- package/packages/control-plane/src/server/runs.ts +4 -3
- package/packages/control-plane/src/server/serve.ts +11 -1
- package/packages/control-plane/src/shipper/discover.ts +29 -0
- package/packages/control-plane/ui/dist/assets/{index-53E32yrO.js → index-Jt31nrK-.js} +24 -24
- package/packages/control-plane/ui/dist/index.html +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/schemas/features.schema.json +71 -0
- package/schemas/loki-yaml.schema.json +123 -0
- package/schemas/manifest.schema.json +77 -0
- package/schemas/migration-plan.schema.json +109 -0
- package/schemas/outcome-contract.schema.json +73 -0
- package/schemas/seams.schema.json +90 -0
- package/schemas/status-result.schema.json +30 -0
- package/schemas/why-result.schema.json +51 -0
- package/web-app/dist/assets/{AdminPage-DN_pe8vo.js → AdminPage-OaSCF3M4.js} +1 -1
- package/web-app/dist/assets/{Avatar-BnQsr8z-.js → Avatar-xa3G7_vs.js} +1 -1
- package/web-app/dist/assets/{Badge-DzERiZn2.js → Badge-Ci_UcHij.js} +1 -1
- package/web-app/dist/assets/{Button-CaaoxQAt.js → Button-CFrrmbvL.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-SXhnysJA.js → CockpitPage-6hyVl_QZ.js} +1 -1
- package/web-app/dist/assets/{ComparePage-krxTCFJY.js → ComparePage-B5H56KCG.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-BL96ESMc.js → ErrorBoundary-CBbRHsYr.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-PUiGSpkB.js → EvidenceReceiptPanel-D-Zoneum.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-C0hPs-P2.js → GitHubIssuesPanel-D7XPRsic.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-CLvqNUlu.js → GitHubPRsPanel-CzHbBPAn.js} +1 -1
- package/web-app/dist/assets/{HomePage-X-sq_Rc9.js → HomePage-ClOx5qF1.js} +2 -2
- package/web-app/dist/assets/{LoginPage-DhQuwdua.js → LoginPage-3Yz_9j8E.js} +1 -1
- package/web-app/dist/assets/{MagicPage-CP2ODjfn.js → MagicPage-Dumn48gC.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-C6SYiQES.js → MetricsPage-CYCYVG_8.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-CCDxxYc-.js → NotFoundPage-CV-x_1qH.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-yDF--8FQ.js → ProjectPage-CeO7dnyM.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-BBLspdLn.js → ProjectsPage-BT9mz6iD.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-C_sWbQRw.js → SettingsPage-BL8X9fAB.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-xCMuL0N8.js → ShowcasePage-8f3lnSpc.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-DnJKGCx7.js → SystemSettingsPage-BUNAAzh2.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-BBXdhrSs.js → TeamsPage-Qj-VEZJz.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-AZ1Lr1rn.js → TemplatesPage-b8lKqe5q.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-CtIm9tXc.js → TerminalOutput-jOnfQKB4.js} +1 -1
- package/web-app/dist/assets/{activity-Dk1FK25Q.js → activity-w7GnyBNp.js} +1 -1
- package/web-app/dist/assets/{bell-dhqXyzP4.js → bell-2i4ejMOA.js} +1 -1
- package/web-app/dist/assets/{bot-CExHd4Kf.js → bot-jaPm4NIs.js} +1 -1
- package/web-app/dist/assets/{check-D51zaA1a.js → check-CvNtxeKM.js} +1 -1
- package/web-app/dist/assets/{chevron-left-DEwnQzH1.js → chevron-left-zzBjHAtX.js} +1 -1
- package/web-app/dist/assets/{circle-alert-C6U90Msk.js → circle-alert-CCU0IU2m.js} +1 -1
- package/web-app/dist/assets/{clock-6mFbMzzH.js → clock-BvZckMwN.js} +1 -1
- package/web-app/dist/assets/{cloud-BrZZtE7F.js → cloud-hrMWDdYp.js} +1 -1
- package/web-app/dist/assets/{code-xml-DTFzeepP.js → code-xml-v6l40T-K.js} +1 -1
- package/web-app/dist/assets/{database-B5rEiTre.js → database-l2TMUrQd.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-bQs4rlS_.js → dollar-sign-C2V6OvpO.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-Mo6e6qbf.js → file-code-corner-Bytb5JGA.js} +1 -1
- package/web-app/dist/assets/{file-plus-DzHZ0jpP.js → file-plus-BynCYQB8.js} +1 -1
- package/web-app/dist/assets/{globe-DYN9UhTa.js → globe-DifDdNMH.js} +1 -1
- package/web-app/dist/assets/{hammer-bB0hSRcJ.js → hammer-D7TsOJD6.js} +1 -1
- package/web-app/dist/assets/{index-CIH5CqZB.js → index-Zohfqaz4.js} +3 -3
- package/web-app/dist/assets/{layers-qhFkY8Nu.js → layers-Bk8c2YLS.js} +1 -1
- package/web-app/dist/assets/{loader-circle-BGBkaYH7.js → loader-circle-BrLdbMwP.js} +1 -1
- package/web-app/dist/assets/{lock-DIcsNFum.js → lock-COpmjMyJ.js} +1 -1
- package/web-app/dist/assets/{package-BdIIbZhS.js → package-BMzYdY7F.js} +1 -1
- package/web-app/dist/assets/{plus-Co1Bv8zT.js → plus-B9xpu1Pg.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-CGF4ABWH.js → refresh-cw-C2Lzh-AA.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-BX10Q79h.js → rotate-ccw-2Yyy-uQd.js} +1 -1
- package/web-app/dist/assets/{scroll-text-CbXDlpGp.js → scroll-text-DiK2yhsT.js} +1 -1
- package/web-app/dist/assets/{server-CIhi6yUO.js → server-xlJBgleU.js} +1 -1
- package/web-app/dist/assets/{shield-alert-DJ5fB6Dg.js → shield-alert-D9w4yBCi.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-BEft2T94.js → thumbs-up-CcWfvcE9.js} +1 -1
- package/web-app/dist/assets/{trash-2-CWGUPn7Y.js → trash-2-_hk_pEAp.js} +1 -1
- package/web-app/dist/assets/{trending-down-CjqstvxQ.js → trending-down-CDZT99tC.js} +1 -1
- package/web-app/dist/assets/{trending-up-CTyLjMzE.js → trending-up-A0oreTXc.js} +1 -1
- package/web-app/dist/assets/{upload-BmRx-uv4.js → upload-DfLwhGG2.js} +1 -1
- package/web-app/dist/assets/{usePolling-ChHkSJye.js → usePolling-B7gW2YY_.js} +1 -1
- package/web-app/dist/assets/{user-ZXh_f35z.js → user-C-y7_Oon.js} +1 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
256
|
-
|
|
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
|
|
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=
|
|
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
|
|
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
|
-
#
|
|
7329
|
-
#
|
|
7330
|
-
|
|
7331
|
-
|
|
7332
|
-
""
|
|
7333
|
-
|
|
7334
|
-
|
|
7335
|
-
|
|
7336
|
-
|
|
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).
|
|
12990
|
+
# D65: multi-repo workspaces (D51 Phase B). On by default; LOKI_WORKSPACES=0 disables.
|
|
12925
12991
|
cmd_workspace() {
|
|
12926
|
-
if [ "${LOKI_WORKSPACES:-}"
|
|
12927
|
-
echo -e "${RED}loki workspace is disabled.${NC}
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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] (
|
|
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:-}"
|
|
33890
|
-
echo "loki control is
|
|
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'
|
package/completions/loki.bash
CHANGED
|
@@ -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
|
package/dashboard/__init__.py
CHANGED
package/dashboard/api_start.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/docs/INSTALLATION.md
CHANGED
|
@@ -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.
|
|
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
|
|