loki-mode 11.5.1 → 11.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.md +19 -2
  2. package/SKILL.md +2 -2
  3. package/VERSION +1 -1
  4. package/autonomy/lib/backlog.py +11 -2
  5. package/autonomy/lib/chat.sh +57 -1
  6. package/autonomy/lib/loki_yaml.py +8 -2
  7. package/autonomy/lib/workspace.py +267 -11
  8. package/autonomy/loki +327 -222
  9. package/autonomy/notification-checker.py +1 -1
  10. package/autonomy/trigger-server.py +185 -2
  11. package/autonomy/verify.sh +4 -0
  12. package/completions/_loki +4 -0
  13. package/completions/loki.bash +20 -2
  14. package/dashboard/__init__.py +3 -10
  15. package/dashboard/control.py +5 -435
  16. package/docs/ARCHITECTURE.md +1 -2
  17. package/docs/CLI-CORE-VERBS.md +2 -6
  18. package/docs/CLI-REFERENCE.md +2 -1
  19. package/docs/COMPONENTS.md +0 -2
  20. package/docs/DEPLOY-PLAN.md +17 -8
  21. package/docs/EVALUATING.md +8 -0
  22. package/docs/INSTALLATION.md +1 -1
  23. package/docs/WORKSPACES.md +15 -2
  24. package/docs/authentication.md +19 -1
  25. package/docs/dev/architecture-reference.md +8 -0
  26. package/docs/dev/release-checklist.md +9 -8
  27. package/docs/enterprise/security.md +3 -234
  28. package/docs/sentry-intake.md +11 -0
  29. package/docs/v10/ARCHITECT-CUTS.md +304 -0
  30. package/docs/v10/CLI-MODERN.md +1 -1
  31. package/docs/v10/DECISIONS.md +5 -5
  32. package/docs/v10/ENGINE.md +735 -299
  33. package/docs/v10/GUARDS.md +16 -14
  34. package/docs/v10/GUIDE.md +5 -7
  35. package/docs/v10/PROGRESS.md +12 -1
  36. package/docs/v11/MARKET-SLICES.md +1 -5
  37. package/integrations/openclaw/README.md +20 -0
  38. package/integrations/vibe-kanban.md +12 -50
  39. package/loki-ts/dist/loki.js +1213 -1178
  40. package/mcp/__init__.py +1 -1
  41. package/mcp/_sdk_loader.py +1 -1
  42. package/package.json +2 -2
  43. package/packages/control-plane/dist/server.js +1090 -924
  44. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  45. package/references/quality-control.md +1 -116
  46. package/schemas/loki-yaml.schema.json +5 -2
  47. package/skills/00-index.md +2 -2
  48. package/skills/artifacts.md +0 -22
  49. package/skills/production.md +2 -38
  50. package/src/policies/engine.js +41 -2
  51. package/src/policies/types.js +49 -0
  52. package/web-app/dist/assets/{AdminPage-Cbu3pXL8.js → AdminPage-S3BYeapm.js} +1 -1
  53. package/web-app/dist/assets/{Avatar-DGqGGcGx.js → Avatar-C1rHwLgn.js} +1 -1
  54. package/web-app/dist/assets/{Badge-Bhlipt6v.js → Badge-CzlnN5aQ.js} +1 -1
  55. package/web-app/dist/assets/{Button-CpN2sO61.js → Button-B1w5AeGC.js} +1 -1
  56. package/web-app/dist/assets/{CockpitPage-BqeDJwdL.js → CockpitPage-Dyeqjfkc.js} +1 -1
  57. package/web-app/dist/assets/{ComparePage-DUieA2ba.js → ComparePage-C6y2GkGR.js} +1 -1
  58. package/web-app/dist/assets/{ErrorBoundary-DTWsYrOm.js → ErrorBoundary-Dg2fcGkR.js} +1 -1
  59. package/web-app/dist/assets/{EvidenceReceiptPanel-BU_B9X19.js → EvidenceReceiptPanel-CxbyjvTj.js} +1 -1
  60. package/web-app/dist/assets/{GitHubIssuesPanel-DEZoLOsP.js → GitHubIssuesPanel-Dpys1xDb.js} +1 -1
  61. package/web-app/dist/assets/{GitHubPRsPanel-BEsnrq7Y.js → GitHubPRsPanel-CRVjWoFl.js} +1 -1
  62. package/web-app/dist/assets/{HomePage-BoUVuq7h.js → HomePage-BKnv-oYw.js} +2 -2
  63. package/web-app/dist/assets/{LoginPage-CmOED7vb.js → LoginPage-BGksmFRd.js} +1 -1
  64. package/web-app/dist/assets/{MagicPage-DATZitrZ.js → MagicPage-OCvhp16M.js} +1 -1
  65. package/web-app/dist/assets/{MetricsPage-DGL1v0I-.js → MetricsPage-CJOJpXWC.js} +1 -1
  66. package/web-app/dist/assets/{NotFoundPage-C2WgOjZd.js → NotFoundPage-C04p3QvN.js} +1 -1
  67. package/web-app/dist/assets/{ProjectPage-BMi1tepj.js → ProjectPage-BRB8txsY.js} +3 -3
  68. package/web-app/dist/assets/{ProjectsPage-vc3y5YPh.js → ProjectsPage-CoUYeQEh.js} +1 -1
  69. package/web-app/dist/assets/{SettingsPage-kbq0Ftvi.js → SettingsPage-BkjmJGfM.js} +1 -1
  70. package/web-app/dist/assets/{ShowcasePage-Bn6Q8Kbg.js → ShowcasePage-B4vU9TjQ.js} +1 -1
  71. package/web-app/dist/assets/{SystemSettingsPage-Basmhs_b.js → SystemSettingsPage-Z2jiVsni.js} +1 -1
  72. package/web-app/dist/assets/{TeamsPage-BTqOc4TR.js → TeamsPage-Ulm8SISq.js} +1 -1
  73. package/web-app/dist/assets/{TemplatesPage-2P9002VL.js → TemplatesPage-C_7MM6Ey.js} +1 -1
  74. package/web-app/dist/assets/{TerminalOutput-CAoSPBrg.js → TerminalOutput-Cumd7Tgs.js} +1 -1
  75. package/web-app/dist/assets/{activity-BcWVWCqU.js → activity-CZAUkbiY.js} +1 -1
  76. package/web-app/dist/assets/{bell-CYRqqgZ6.js → bell-BOvSXpEr.js} +1 -1
  77. package/web-app/dist/assets/{bot-CU7gdn3g.js → bot-DOTZxBYj.js} +1 -1
  78. package/web-app/dist/assets/{check-DLlCpz9V.js → check-BxpE0B5u.js} +1 -1
  79. package/web-app/dist/assets/{chevron-left-CE9Cb30C.js → chevron-left-D6KPvBOA.js} +1 -1
  80. package/web-app/dist/assets/{circle-alert-uou4bjua.js → circle-alert-Bajq86oe.js} +1 -1
  81. package/web-app/dist/assets/{clock-C_dSJcDX.js → clock-CnlHAVJc.js} +1 -1
  82. package/web-app/dist/assets/{cloud-DZ22ITF6.js → cloud-DjaymjwM.js} +1 -1
  83. package/web-app/dist/assets/{code-xml-BrJS0hhS.js → code-xml-BADEw2Ld.js} +1 -1
  84. package/web-app/dist/assets/{database-D2HOmxtl.js → database-B4aKb5Cq.js} +1 -1
  85. package/web-app/dist/assets/{dollar-sign-CQ1vK_6q.js → dollar-sign-DrldXpja.js} +1 -1
  86. package/web-app/dist/assets/{file-code-corner-w2vpGm5d.js → file-code-corner-DVebDVR-.js} +1 -1
  87. package/web-app/dist/assets/{file-plus-DlF6wjkD.js → file-plus-BD-N_-Tl.js} +1 -1
  88. package/web-app/dist/assets/{globe-Dy3lBJg1.js → globe-C7ieInC-.js} +1 -1
  89. package/web-app/dist/assets/{hammer-C3xIiRXN.js → hammer-BSugKIji.js} +1 -1
  90. package/web-app/dist/assets/{index-4t-_IW2y.js → index-nLQgfvOn.js} +3 -3
  91. package/web-app/dist/assets/{layers-Y8Fs8qXI.js → layers-CHXxU2on.js} +1 -1
  92. package/web-app/dist/assets/{loader-circle-CxgN1v4m.js → loader-circle-BEEz0LbD.js} +1 -1
  93. package/web-app/dist/assets/{lock-DFFNbXDs.js → lock-CzQGfcnE.js} +1 -1
  94. package/web-app/dist/assets/{package-5e6qCTUg.js → package-CcHYQLGd.js} +1 -1
  95. package/web-app/dist/assets/{plus-C0Go_ytj.js → plus-DdzgFEXn.js} +1 -1
  96. package/web-app/dist/assets/{refresh-cw-D4i-NW3o.js → refresh-cw-7x0HbH9Y.js} +1 -1
  97. package/web-app/dist/assets/{rotate-ccw-YlKkiUW6.js → rotate-ccw-DBahX1Sp.js} +1 -1
  98. package/web-app/dist/assets/{scroll-text-4qXOHTap.js → scroll-text-BB8y8a9Z.js} +1 -1
  99. package/web-app/dist/assets/{server-BB6zXNp9.js → server-CfkbW2MH.js} +1 -1
  100. package/web-app/dist/assets/{shield-alert-D4tzjB1H.js → shield-alert-DUgD8rBF.js} +1 -1
  101. package/web-app/dist/assets/{thumbs-up-DZiRwdFj.js → thumbs-up-DGoyY6QC.js} +1 -1
  102. package/web-app/dist/assets/{trash-2-Dv_LkRIK.js → trash-2-B6xrSUMr.js} +1 -1
  103. package/web-app/dist/assets/{trending-down-BJYp9PQe.js → trending-down-B6RGKTOx.js} +1 -1
  104. package/web-app/dist/assets/{trending-up-BTcXLklK.js → trending-up-B44DCrt1.js} +1 -1
  105. package/web-app/dist/assets/{upload-fcHqwOxr.js → upload-4kWm33ZY.js} +1 -1
  106. package/web-app/dist/assets/{usePolling-BeJYw_4L.js → usePolling-zkUtRQG6.js} +1 -1
  107. package/web-app/dist/assets/{user-BcDwUCsO.js → user-04cEVIe2.js} +1 -1
  108. package/web-app/dist/index.html +1 -1
  109. package/dashboard/run.py +0 -63
  110. package/docs/openclaw-integration.md +0 -52
  111. package/docs/v10/HANDOFF-NEXT-WEEK.md +0 -40
  112. package/docs/v10/MORNING-BRIEF-1003.md +0 -25
  113. package/docs/v10/MORNING-BRIEF.md +0 -52
  114. package/docs/v10/TODO.md +0 -35
  115. package/mcp/resources.py +0 -351
  116. package/mcp/tools.py +0 -200
@@ -1,62 +1,23 @@
1
1
  """
2
- Loki Mode Session Control API
2
+ Loki Mode session-control helpers
3
3
 
4
- FastAPI-based session control endpoints for the Loki Mode dashboard.
5
- Provides start/stop/pause/resume functionality and real-time status updates.
6
-
7
- Usage:
8
- uvicorn dashboard.control:app --host 0.0.0.0 --port 57374
9
- # Or run with the CLI:
10
- loki dashboard start
4
+ Shared helpers for the dashboard: session status, atomic JSON writes, skill-dir
5
+ discovery and the browser-mutation Origin check. The HTTP control routes live
6
+ in dashboard/server.py (the only dashboard app).
11
7
  """
12
8
 
13
- import asyncio
14
9
  import fcntl
15
10
  import json
16
11
  import os
17
- import signal
18
- import subprocess
19
12
  import tempfile
20
13
  from datetime import datetime, timezone
21
14
  from pathlib import Path
22
15
  from typing import Optional
23
16
  from urllib.parse import urlsplit
24
17
 
25
- from fastapi import Depends, FastAPI, HTTPException, Request
26
- from fastapi.middleware.cors import CORSMiddleware
27
- from fastapi.responses import JSONResponse, StreamingResponse
18
+ from fastapi import Request
28
19
  from pydantic import BaseModel
29
20
 
30
- # Auth gating for the standalone control app.
31
- #
32
- # This module also defines a self-contained FastAPI `app` whose own docstring
33
- # invites operators to expose it via `uvicorn dashboard.control:app`. When that
34
- # happens the state-mutating endpoints (start/stop/pause/resume) MUST honor the
35
- # same scope checks as the primary dashboard (dashboard/server.py), otherwise a
36
- # user who follows the docstring stands up an unauthenticated control plane that
37
- # can launch arbitrary builds and kill running sessions even when
38
- # LOKI_ENTERPRISE_AUTH / OIDC are configured.
39
- #
40
- # auth.require_scope is a no-op (allows access) when no auth method is enabled,
41
- # so this import is safe for the default anonymous-localhost workflow and only
42
- # enforces when the operator has explicitly turned auth on. The import is
43
- # defensive: if the package context is unavailable (e.g. the file is run from a
44
- # path where the relative import fails) we fall back to a gate that always
45
- # allows, preserving the prior behavior rather than crashing import.
46
- try:
47
- from . import auth as _auth
48
-
49
- def _require_control_scope():
50
- return Depends(_auth.require_scope("control"))
51
- except Exception: # pragma: no cover - defensive fallback for non-package runs
52
- def _require_control_scope():
53
- async def _noop() -> bool:
54
- return True
55
-
56
- return Depends(_noop)
57
-
58
- _CONTROL_DEP = _require_control_scope()
59
-
60
21
 
61
22
  def _normalized_http_origin(value: str) -> tuple[str, str, int] | None:
62
23
  """Return a comparable browser origin, or ``None`` when malformed.
@@ -250,104 +211,6 @@ def atomic_write_json(file_path: Path, data: dict, use_lock: bool = True):
250
211
  except Exception as e:
251
212
  raise RuntimeError(f"Failed to write {file_path}: {e}")
252
213
 
253
- # FastAPI app
254
- app = FastAPI(
255
- title="Loki Mode Control API",
256
- description="Session control endpoints for Loki Mode dashboard",
257
- version="1.0.0"
258
- )
259
-
260
- # CORS middleware for dashboard frontend - restricted to localhost by default.
261
- # Set LOKI_DASHBOARD_CORS to override (comma-separated origins).
262
- _cors_default = "http://localhost:57374,http://127.0.0.1:57374"
263
- _cors_raw = os.environ.get("LOKI_DASHBOARD_CORS", _cors_default)
264
- _cors_origins = [o.strip() for o in _cors_raw.split(",") if o.strip()]
265
- if "*" in _cors_origins or _cors_raw.strip() == "*":
266
- if os.environ.get("LOKI_ENV") == "production":
267
- raise RuntimeError(
268
- "Wildcard CORS ('*') is not allowed in production. "
269
- "Set LOKI_DASHBOARD_CORS to a specific origin list, "
270
- "or set LOKI_ENV != production."
271
- )
272
- import logging
273
-
274
- logging.getLogger(__name__).warning(
275
- "LOKI_DASHBOARD_CORS is set to '*' -- all origins are allowed. "
276
- "This is insecure for production deployments."
277
- )
278
- app.add_middleware(
279
- CORSMiddleware,
280
- allow_origins=_cors_origins,
281
- allow_credentials=True,
282
- allow_methods=["*"],
283
- allow_headers=["*"],
284
- )
285
-
286
-
287
- @app.middleware("http")
288
- async def browser_mutation_boundary(request: Request, call_next):
289
- """Refuse cross-origin browser mutations before control side effects."""
290
- if request.method in ("POST", "PUT", "PATCH", "DELETE"):
291
- if not browser_mutation_origin_allowed(request, _cors_origins):
292
- return JSONResponse(
293
- status_code=403,
294
- content={"detail": "cross-origin mutation refused"},
295
- )
296
- return await call_next(request)
297
-
298
-
299
- # Request/Response models
300
- class StartRequest(BaseModel):
301
- """Request body for starting a Loki Mode session."""
302
- prd: Optional[str] = None
303
- provider: str = "claude"
304
- parallel: bool = False
305
- background: bool = True
306
-
307
- def validate_provider(self) -> None:
308
- """Validate provider is from allowed list."""
309
- allowed_providers = ["claude", "codex", "gemini", "cline", "aider"]
310
- if self.provider not in allowed_providers:
311
- raise ValueError(f"Invalid provider: {self.provider}. Must be one of: {', '.join(allowed_providers)}")
312
-
313
- def validate_prd_path(self) -> None:
314
- """Validate PRD path is safe and exists."""
315
- if not self.prd:
316
- return
317
-
318
- # Check for path traversal sequences
319
- if ".." in self.prd:
320
- raise ValueError("PRD path contains path traversal sequence (..)")
321
-
322
- # Resolve to absolute path and verify it exists
323
- prd_path = Path(self.prd).resolve()
324
- if not prd_path.exists():
325
- raise ValueError(f"PRD file does not exist: {self.prd}")
326
-
327
- # Verify it's a file, not a directory
328
- if not prd_path.is_file():
329
- raise ValueError(f"PRD path is not a file: {self.prd}")
330
-
331
- # Verify path resolves within CWD or a reasonable parent.
332
- # FAIL CLOSED if the cwd is gone: this is a path-confinement check, so
333
- # an unresolvable base must reject the path, never skip the comparison.
334
- try:
335
- cwd = Path.cwd().resolve()
336
- except OSError as exc:
337
- raise ValueError(
338
- "cannot validate PRD path: the working directory no longer "
339
- "exists (%s)" % exc
340
- ) from exc
341
- try:
342
- prd_path.relative_to(cwd)
343
- except ValueError:
344
- # Not within CWD - check if it's within user's home or project directory
345
- home = Path.home().resolve()
346
- try:
347
- prd_path.relative_to(home)
348
- except ValueError:
349
- raise ValueError(f"PRD path is outside allowed directories: {self.prd}")
350
-
351
214
 
352
215
  class StatusResponse(BaseModel):
353
216
  """Current session status."""
@@ -365,13 +228,6 @@ class StatusResponse(BaseModel):
365
228
  timestamp: str
366
229
 
367
230
 
368
- class ControlResponse(BaseModel):
369
- """Generic control operation response."""
370
- success: bool
371
- message: str
372
- pid: Optional[int] = None
373
-
374
-
375
231
  # Utility functions
376
232
  def read_file_safe(filepath: Path) -> str:
377
233
  """Read file contents safely, returning empty string on error."""
@@ -520,289 +376,3 @@ def get_status() -> StatusResponse:
520
376
  complexity=complexity,
521
377
  timestamp=datetime.now(timezone.utc).isoformat()
522
378
  )
523
-
524
-
525
- # API Endpoints
526
- @app.get("/api/control/health")
527
- async def health_check():
528
- """Health check endpoint."""
529
- return {"status": "ok", "version": get_version()}
530
-
531
-
532
- @app.get("/api/control/status", response_model=StatusResponse)
533
- async def get_session_status():
534
- """Get current session status."""
535
- return get_status()
536
-
537
-
538
- @app.post("/api/control/start", response_model=ControlResponse, dependencies=[_CONTROL_DEP])
539
- async def start_session(request: StartRequest):
540
- """
541
- Start a Loki Mode session.
542
-
543
- Args:
544
- request: Start request with PRD path, provider, and options
545
-
546
- Returns:
547
- ControlResponse with success status and PID
548
- """
549
- # Validate input
550
- try:
551
- request.validate_provider()
552
- request.validate_prd_path()
553
- except ValueError as e:
554
- raise HTTPException(status_code=400, detail=str(e))
555
-
556
- # Check if already running
557
- status = get_status()
558
- if status.state == "running":
559
- raise HTTPException(
560
- status_code=409,
561
- detail=f"Session already running with PID {status.pid}"
562
- )
563
-
564
- # Verify run.sh exists
565
- if not RUN_SH.exists():
566
- raise HTTPException(
567
- status_code=500,
568
- detail=f"run.sh not found at {RUN_SH}"
569
- )
570
-
571
- # Build command arguments
572
- args = [str(RUN_SH), "--provider", request.provider]
573
- if request.parallel:
574
- args.append("--parallel")
575
- if request.background:
576
- args.append("--bg")
577
- if request.prd:
578
- args.append(request.prd)
579
-
580
- try:
581
- # Start the process
582
- # RUN_SH is a trusted fixed path; provider and PRD were validated above,
583
- # and argv is passed directly without a command shell.
584
- process = subprocess.Popen(
585
- args, # lgtm[py/command-line-injection]
586
- stdout=subprocess.DEVNULL,
587
- stderr=subprocess.DEVNULL,
588
- start_new_session=True,
589
- # A deleted cwd would make Popen itself raise; fall back to the
590
- # skill tree rather than failing to launch the run at all.
591
- cwd=str(_cwd_or_skill_dir()),
592
- shell=False,
593
- )
594
-
595
- # Save provider for status tracking
596
- (STATE_DIR / "provider").write_text(request.provider)
597
-
598
- # Emit start event
599
- emit_event("session_start", {
600
- "pid": process.pid,
601
- "provider": request.provider,
602
- "prd": request.prd,
603
- "parallel": request.parallel
604
- })
605
-
606
- return ControlResponse(
607
- success=True,
608
- message=f"Session started with provider {request.provider}",
609
- pid=process.pid
610
- )
611
- except Exception as e:
612
- raise HTTPException(status_code=500, detail=str(e))
613
-
614
-
615
- @app.post("/api/control/stop", response_model=ControlResponse, dependencies=[_CONTROL_DEP])
616
- async def stop_session():
617
- """
618
- Stop the current Loki Mode session.
619
-
620
- Creates a STOP file to signal graceful shutdown and attempts
621
- to terminate the process directly.
622
- """
623
- stop_file = LOKI_DIR / "STOP"
624
- pid_file = LOKI_DIR / "loki.pid"
625
-
626
- # Create STOP file for graceful shutdown
627
- stop_file.parent.mkdir(parents=True, exist_ok=True)
628
- stop_file.write_text(datetime.now(timezone.utc).isoformat())
629
-
630
- # Also try to terminate process directly
631
- pid_str = read_file_safe(pid_file)
632
- pid = None
633
- if pid_str.isdigit():
634
- pid = int(pid_str)
635
- if is_process_running(pid):
636
- try:
637
- os.kill(pid, signal.SIGTERM)
638
- except (OSError, ProcessLookupError):
639
- pass
640
-
641
- # Mark session.json as stopped (skill-invoked sessions)
642
- session_file = LOKI_DIR / "session.json"
643
- if session_file.exists():
644
- try:
645
- # Read current session data
646
- session_data = json.loads(session_file.read_text())
647
- session_data["status"] = "stopped"
648
-
649
- # Atomic write with file locking to prevent race conditions
650
- atomic_write_json(session_file, session_data, use_lock=True)
651
- except (json.JSONDecodeError, OSError, KeyError, RuntimeError):
652
- pass
653
-
654
- # Emit stop event
655
- emit_event("session_stop", {"pid": pid, "reason": "user_request"})
656
-
657
- return ControlResponse(
658
- success=True,
659
- message="Stop signal sent",
660
- pid=pid
661
- )
662
-
663
-
664
- @app.post("/api/control/pause", response_model=ControlResponse, dependencies=[_CONTROL_DEP])
665
- async def pause_session():
666
- """
667
- Pause the current Loki Mode session.
668
-
669
- Creates a PAUSE file that signals the runner to pause
670
- after completing the current task.
671
- """
672
- pause_file = LOKI_DIR / "PAUSE"
673
-
674
- # Create PAUSE file
675
- pause_file.parent.mkdir(parents=True, exist_ok=True)
676
- pause_file.write_text(datetime.now(timezone.utc).isoformat())
677
-
678
- # Get current PID for response
679
- pid_file = LOKI_DIR / "loki.pid"
680
- pid_str = read_file_safe(pid_file)
681
- pid = int(pid_str) if pid_str.isdigit() else None
682
-
683
- # Emit pause event
684
- emit_event("session_pause", {"pid": pid})
685
-
686
- return ControlResponse(
687
- success=True,
688
- message="Pause signal sent - session will pause after current task",
689
- pid=pid
690
- )
691
-
692
-
693
- @app.post("/api/control/resume", response_model=ControlResponse, dependencies=[_CONTROL_DEP])
694
- async def resume_session():
695
- """
696
- Resume a paused Loki Mode session.
697
-
698
- Removes the PAUSE and STOP files to allow the session to continue.
699
- """
700
- pause_file = LOKI_DIR / "PAUSE"
701
- stop_file = LOKI_DIR / "STOP"
702
-
703
- # Remove control files
704
- try:
705
- pause_file.unlink(missing_ok=True)
706
- except Exception:
707
- pass
708
-
709
- try:
710
- stop_file.unlink(missing_ok=True)
711
- except Exception:
712
- pass
713
-
714
- # Get current PID for response
715
- pid_file = LOKI_DIR / "loki.pid"
716
- pid_str = read_file_safe(pid_file)
717
- pid = int(pid_str) if pid_str.isdigit() else None
718
-
719
- # Emit resume event
720
- emit_event("session_resume", {"pid": pid})
721
-
722
- return ControlResponse(
723
- success=True,
724
- message="Session resumed",
725
- pid=pid
726
- )
727
-
728
-
729
- @app.get("/api/control/events")
730
- async def stream_events():
731
- """
732
- Stream events as Server-Sent Events (SSE).
733
-
734
- Provides real-time updates to the dashboard frontend.
735
- """
736
- async def event_generator():
737
- # Send initial status
738
- status = get_status()
739
- yield f"data: {json.dumps(status.model_dump())}\n\n"
740
-
741
- # Track file position for incremental reads
742
- last_position = 0
743
- if EVENTS_FILE.exists():
744
- last_position = EVENTS_FILE.stat().st_size
745
-
746
- # Stream updates
747
- while True:
748
- # Send status update every 2 seconds
749
- status = get_status()
750
- yield f"event: status\ndata: {json.dumps(status.model_dump())}\n\n"
751
-
752
- # Check for new events
753
- if EVENTS_FILE.exists():
754
- current_size = EVENTS_FILE.stat().st_size
755
- if current_size > last_position:
756
- with open(EVENTS_FILE, "r") as f:
757
- f.seek(last_position)
758
- for line in f:
759
- line = line.strip()
760
- if line:
761
- yield f"event: log\ndata: {line}\n\n"
762
- last_position = current_size
763
-
764
- await asyncio_sleep(2)
765
-
766
- return StreamingResponse(
767
- event_generator(),
768
- media_type="text/event-stream",
769
- headers={
770
- "Cache-Control": "no-cache",
771
- "Connection": "keep-alive"
772
- }
773
- )
774
-
775
-
776
- @app.get("/api/control/logs")
777
- async def get_logs(lines: int = 50):
778
- """
779
- Get recent log lines from the session log.
780
-
781
- Args:
782
- lines: Number of lines to return (default 50, max 10000)
783
- """
784
- lines = min(max(lines, 1), 10000)
785
- log_file = LOG_DIR / "session.log"
786
-
787
- if not log_file.exists():
788
- return {"logs": [], "total": 0}
789
-
790
- content = log_file.read_text()
791
- all_lines = [line for line in content.split("\n") if line.strip()]
792
- recent_lines = all_lines[-lines:]
793
-
794
- return {"logs": recent_lines, "total": len(all_lines)}
795
-
796
-
797
- # Helper for async sleep
798
- async def asyncio_sleep(seconds: float):
799
- """Async sleep helper."""
800
- await asyncio.sleep(seconds)
801
-
802
-
803
- # Run with uvicorn if executed directly
804
- if __name__ == "__main__":
805
- import uvicorn
806
- port = int(os.environ.get("LOKI_DASHBOARD_PORT", "57374"))
807
- host = os.environ.get("LOKI_DASHBOARD_HOST", "127.0.0.1")
808
- uvicorn.run(app, host=host, port=port)
@@ -215,8 +215,7 @@ memory/ Memory system (Python package, ~19 modules)
215
215
 
216
216
  mcp/ Model Context Protocol server
217
217
  server.py MCP server (tools + resources + prompts)
218
- tools.py / magic_tools.py / managed_tools.py Tool registries
219
- resources.py MCP resources
218
+ magic_tools.py / managed_tools.py Tool registries
220
219
  lsp_proxy.py LSP proxy (references, definitions, symbols)
221
220
 
222
221
  dashboard/ FastAPI control-plane backend
@@ -98,12 +98,8 @@ The process exit code equals `exit_code`.
98
98
  | 3 | Missing component, or verifier error |
99
99
  | 64 | Usage error for `verify`, `quick` and `start` (unknown option, missing task) |
100
100
 
101
- Severity rises with the code. `loki verify` exits 0 VERIFIED, 1 CONCERNS, 2
102
- BLOCKED or not a git directory, 3 verifier error or empty diff, 64 usage error.
103
- Gate CI with `loki verify || fail` and treat any code of 2 or higher as worse
104
- than CONCERNS. From 11.4.0 this is a breaking change for scripts that tested
105
- `rc == 1` on an empty diff or a non-git directory. Per-command detail:
106
- `docs/exit-codes.md`.
101
+ Severity rises with the code. `loki verify` codes and the CI-gate guidance are
102
+ in `docs/exit-codes.md`.
107
103
 
108
104
  ## Unknown commands
109
105
 
@@ -1,6 +1,6 @@
1
1
  # CLI reference
2
2
 
3
- Generated from `loki-ts/src/cli/registry.ts` by `scripts/generate-stale-zero.sh` for v11.5.1. Do not edit by hand.
3
+ Generated from `loki-ts/src/cli/registry.ts` by `scripts/generate-stale-zero.sh` for v11.7.1. Do not edit by hand.
4
4
  Hidden and legacy-only commands are omitted; see `docs/v10/CLI-MODERN.md` for the full inventory.
5
5
 
6
6
  | Command | Description |
@@ -19,6 +19,7 @@ Hidden and legacy-only commands are omitted; see `docs/v10/CLI-MODERN.md` for th
19
19
  | `loki memory index` | Show or rebuild the memory index |
20
20
  | `loki memory learn` | Learn lessons from a merged PR's review comments |
21
21
  | `loki memory lessons` | List PR review lessons with uses and outcomes |
22
+ | `loki memory brief` | Print the lessons and learnings block an engine10 run injects |
22
23
  | `loki memory show` | Show one learning |
23
24
  | `loki memory search` | Search learnings |
24
25
  | `loki memory stats` | Memory statistics |
@@ -249,13 +249,11 @@ search) to MCP clients.
249
249
  **Key files:**
250
250
  - `server.py` - MCP server (47 tools: 31 in-file + magic + gated managed + 8 cp; plus
251
251
  3 resources, 2 prompts).
252
- - `tools.py` - Core tool implementations.
253
252
  - `magic_tools.py` - Magic Modules tools.
254
253
  - `cp_tools.py` - Read-only Control Plane data tools (`cp_*`, GET only; the CP URL
255
254
  and token come from the MCP child env and are never returned).
256
255
  - `managed_tools.py` - Managed-memory tool (`loki_memory_redact`, gated on
257
256
  `LOKI_MANAGED_AGENTS=true` and `LOKI_MANAGED_MEMORY=true`).
258
- - `resources.py` - MCP resources.
259
257
  - `lsp_proxy.py` - LSP proxy (find references, go to definition, symbol lookup).
260
258
  - `learning_collector.py` - Learning signal collection.
261
259
  - `_sdk_loader.py` - Bootstraps the Python MCP SDK on first run.
@@ -1,17 +1,26 @@
1
- # DEPLOY-PLAN.md -- `loki deploy` (ADVISORY / PRINT-ONLY)
1
+ # DEPLOY-PLAN.md -- `loki deploy` (original design: advisory print-only; `--execute` now exists)
2
2
 
3
- **Feature:** `loki deploy` -- detect project type + the user's installed cloud CLI,
4
- PRINT (and best-effort copy) the canonical deploy command(s), and **never run them**.
3
+ **Current behavior (v11.7.0).** `loki deploy` still prints the deploy command by default and
4
+ runs nothing. Loki deploys only behind `--execute` plus a VERIFIED, HEAD-anchored receipt, a
5
+ clean tree, a destructive-op refusal and a policy permission (`.loki/policies.json`
6
+ `autonomy: operate`, or an explicit `actions.deploy` allow). With `--health-url URL` it probes the
7
+ deployed URL after the command. A non-2xx result is recorded as FAILED_HEALTH and exits non-zero;
8
+ with `--auto-rollback` it also runs the provider's documented rollback verb (`vercel rollback`,
9
+ `fly releases rollback <captured version>`; Netlify and Cloudflare print a manual note). With no
10
+ URL, health is recorded as "unchecked", never healthy. The deploy record lives in `.loki/deploys`
11
+ and is not part of the signed receipt. `loki start --deploy [--health-url U] [--auto-rollback]`
12
+ chains the same gated command after a start. Not built: canary or staged rollout, and receipt-bound deploy records.
13
+
14
+ Everything below is the original print-only design plan and is kept for history.
5
15
 
6
16
  **Status:** Architecture plan. No implementation code here. Pattern family: the just-shipped
7
17
  `loki preview --public` (`_preview_public` at `autonomy/loki:5306`, helpers from `_read_app_state`
8
18
  at `:5216`, arg parsing in `cmd_preview` at `:5546`-`:5609`, dispatch arm `preview)` at `:14966`).
9
19
 
10
- **Why this exists / why it is safe.** README states (line 447) *"Does not deploy -- human runs
11
- deploy commands"* and (line 452) *"It does NOT access your cloud accounts ... Human oversight is
12
- expected for deployment."* This feature keeps that literally true: it is an **advisory printer**.
13
- It runs `command -v <cli>` ONLY (never the CLI itself, not even `--version`), prints the exact
14
- command, and the **human runs it**. Fully reversible; touches no cloud account.
20
+ **Why the original design was print-only.** The README then stated that deploy commands were run by
21
+ a human and that Loki did not access cloud accounts. The first version was an **advisory printer**:
22
+ it ran `command -v <cli>` ONLY (never the CLI itself, not even `--version`), printed the exact
23
+ command, and the **human ran it**. Fully reversible; touched no cloud account.
15
24
 
16
25
  ---
17
26
 
@@ -211,6 +211,12 @@ Not measured: human effort, recovery success, review quality. Output: `--out-dir
211
211
 
212
212
  ---
213
213
 
214
+ ## 9. Factory-vs-Loki task set (reproducible, partly unmeasured)
215
+
216
+ `benchmarks/attribution/tasks/coord-1-api-web/` (multi-repo coordination) and `benchmarks/attribution/tasks/longspec-1-three-units/` (long spec, three units) ship with held-out acceptance kept outside the workspace. `benchmarks/bench/attribution.py` has three arms: `raw_claude`, `loki` and an optional `droid` arm. The droid arm probes `droid exec --help`; when `droid exec` is unavailable every droid cell is recorded UNMEASURED with a reason, never zero. `python3 benchmarks/bench/attribution.py --dry-run` lists the tasks and arms and makes no model call. No Factory result is published: Factory publishes no Missions benchmark and no droid run has been measured here. Axis where Loki is ahead on documented features: offline-recheckable signed receipts, now composite receipts for multi-repo change sets; Factory documents none.
217
+
218
+ ---
219
+
214
220
  ## What we do not have
215
221
 
216
222
  Stating this plainly, because you will find it out anyway and it is cheaper for
@@ -228,6 +234,8 @@ both of us if you find it here.
228
234
  Replit Agent ship no local CLI and are not covered at all. Treat this as
229
235
  "unclaimed on every surface we can reach", which is stronger than the earlier
230
236
  "unclaimed as far as we can verify" and still short of a proven first.
237
+ - **No scale measurement for multi-repo runs.** No test covers dozens of repos, so none is claimed.
238
+ - **No Factory comparison run.** The droid arm is UNMEASURED until a run exists (section 9).
231
239
  - **Generation is not air-gapped.** See section 2.
232
240
 
233
241
  ---
@@ -2,7 +2,7 @@
2
2
 
3
3
  Loki Mode, by [Autonomi](https://www.autonomi.dev/), turns a spec or an issue into a verified pull request with a receipt you can check. This page covers installing it, a first run, and the options around it.
4
4
 
5
- **Version:** v11.5.1
5
+ **Version:** v11.7.1
6
6
 
7
7
  **Engine note:** `loki start` is the primary command. `loki start`, `loki "<task>"`, `loki owner/repo#N` and `loki quick` all run the Loki 10 engine, whatever the flag order or input form (PRD file, issue ref, task or one-word brief). The previous loop runs only on `--engine legacy`, `LOKI_ENGINE=legacy`, or for one call when a flag Loki 10 cannot honor is present. See the [user guide](v10/GUIDE.md) and the [5-minute quickstart](QUICKSTART.md).
8
8
 
@@ -10,9 +10,13 @@ workspaces:
10
10
  repos:
11
11
  - {repo: acme/api, path: ~/src/api, setup: "npm ci"}
12
12
  - {repo: acme/web, path: ~/src/web, after: [acme/api]}
13
- integration: {command: "make e2e", timeout_s: 900}
13
+ integration: {command: "make e2e", timeout_s: 900, per_stage: false}
14
+ concurrency: 2
15
+ plan: false
14
16
  ```
15
17
 
18
+ The `workspaces:` block is validated and fails closed: a misspelled key (for example `afer`) or a wrong type exits 2 and names the key.
19
+
16
20
  Repos with no pending `after` run in parallel, at most `concurrency` at a time (default 2; set `LOKI_WORKSPACE_CONCURRENCY` to change it).
17
21
 
18
22
  Then:
@@ -21,6 +25,8 @@ Then:
21
25
  loki workspace list
22
26
  loki workspace show shop
23
27
  loki workspace run shop acme/api#12
28
+ loki start --workspace shop "<brief>" # same runner, from the primary command
29
+ loki workspace infer shop # print suggested after: edges
24
30
  loki workspace status # every recorded run, one row per repo
25
31
  loki workspace status <run-id>
26
32
  ```
@@ -30,7 +36,10 @@ loki workspace status <run-id>
30
36
  - A failing repo does not stop the others (continue and report). The command exits 0 only when every repo succeeded and the integration step did not fail. It exits 3 when the only non-ok outcomes are budget stops (a child exited 3, or a dependent was skipped behind one), and 1 for any other failure. Ctrl-C (SIGINT) stops the recorded child process groups like SIGTERM.
31
37
  - The repo that owns the issue ref gets the ref; other repos get a task text naming the ref and the sibling worktree paths as read-only context.
32
38
  - After all repos finish, the `integration` command runs once from the run directory with `LOKI_WS_DIR_<OWNER>_<REPO>` pointing at each worktree. A timeout counts as failed. With no integration configured the status is `not_configured`.
33
- - Evidence is written to `.loki/workspaces/<name>/<run-id>/integration.json`: head SHA per repo, exit code, log sha256. It is not part of the Seal.
39
+ - Evidence is written to `.loki/workspaces/<name>/<run-id>/integration.json`: head SHA per repo, exit code, log sha256. When every repo succeeded, integration did not fail and every child sealed a receipt, the run also seals a signed `composite.json` that binds integration.json and every child receipt (see "Verify a workspace run").
40
+ - `loki workspace infer <name>` prints suggested `after:` edges read from package.json, go.mod and pyproject.toml dependencies on other listed repos. It is print-only and cycle-checked; it never edits loki.yaml. Manifests only: no OpenAPI or import inference.
41
+ - `plan: true` (or `LOKI_WORKSPACE_PLAN=1`) makes one provider call that decomposes the brief per repo into `<run-dir>/plan.json`. A valid plan gives each listed repo its own task; an invalid one falls back to the generic text and prints the reason.
42
+ - `integration: {per_stage: true}` also runs the integration command after each topological layer. A failing stage SKIPs every later layer, so a broken contract stops before downstream repos build on it.
34
43
  - Without `path`, a repo is cloned to `~/.loki/repos/owner__name`.
35
44
  - After the integration step, a PR comment is posted on each repo PR that exists for its run branch, showing the integration status (PASSED, FAILED, TIMEOUT or NOT CONFIGURED) and every head SHA. It uses your own `gh` login or GH_TOKEN, skips silently when there is no PR, never fails the run, and is disabled with `LOKI_WORKSPACE_COMMENT=0`.
36
45
  - Metrics: each run records `timing` (per repo `started_at` and `finished_at`, epoch seconds) in integration.json. `python3 autonomy/lib/workspace_metrics.py <run-dir> --attention-min 30` prints `prs` (repos with outcome ok), `wall_clock_seconds` (earliest start to latest finish), `prs_per_hour` and `prs_per_attention_minute`. Runs recorded before timing existed, and any run where `--attention-min` is not given, print `unmeasured` for the affected rate, never zero.
@@ -46,5 +55,9 @@ After `loki workspace run <name> <ref>` finishes, check the evidence rather than
46
55
  3. `.loki/workspaces/<name>/<run-id>/integration.json` holds `status`, the head SHA per repo and `timing`.
47
56
  4. `python3 autonomy/lib/workspace_metrics.py <run-dir> --attention-min <minutes>` prints `prs`, `prs_per_hour` and `prs_per_attention_minute`; any value it cannot measure prints `unmeasured`.
48
57
  5. Each repo PR for the run branch carries exactly one integration comment.
58
+ 6. `loki verify <run-dir>/composite.json` re-checks the composite offline (add `--pubkey FILE` on another machine). It recomputes the composite hash, re-verifies every child receipt from disk and checks the signature. A tampered, swapped or stale-head child makes it TAMPERED or UNCHECKED; an unsigned child under a signed composite is UNCHECKED. Exit codes are the single-receipt ones: 0 VERIFIED, 1 TAMPERED, 2 UNCHECKED, 3 UNSIGNED. The composite never reads higher than its weakest child: one PARTIAL child caps it at PARTIAL.
59
+ 7. A run that could not seal prints `composite not sealed: <reason>` (for example a repo with no receipt). Seal by hand with `loki verify --seal-composite <run-dir>`; it refuses unless every child has a receipt.
60
+
61
+ Not measured: no scale test exists for large repo counts. Claim only what you have run.
49
62
 
50
63
  `tests/workspace/99-e2e.sh` runs this whole check through both `autonomy/loki` and `bin/loki` with a stub launcher and a stub `gh`.