loki-mode 10.6.11 → 10.6.12

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 (98) hide show
  1. package/README.md +20 -2
  2. package/SKILL.md +2 -2
  3. package/VERSION +1 -1
  4. package/autonomy/lib/backlog.py +19 -3
  5. package/autonomy/lib/fast_verify.py +19 -1
  6. package/autonomy/lib/proof-pr.sh +34 -3
  7. package/autonomy/lib/workspace.py +27 -1
  8. package/autonomy/lib/workspace_metrics.py +85 -0
  9. package/autonomy/lib/worktree_prep.py +45 -3
  10. package/autonomy/loki +62 -3
  11. package/autonomy/run.sh +95 -6
  12. package/autonomy/verify.sh +87 -20
  13. package/dashboard/__init__.py +1 -1
  14. package/dashboard/api_operator.py +27 -0
  15. package/dashboard/api_runs.py +52 -0
  16. package/dashboard/api_workspaces.py +140 -0
  17. package/dashboard/audit.py +8 -2
  18. package/dashboard/server.py +42 -21
  19. package/dashboard/static/index.html +4 -3
  20. package/dashboard/static/proofs.html +5 -0
  21. package/dashboard/static/start.html +32 -1
  22. package/docs/INSTALLATION.md +1 -1
  23. package/docs/SIGNED-RECEIPTS.md +39 -0
  24. package/docs/UNREACHABLE-TESTS.md +1 -1
  25. package/docs/WORKSPACES.md +14 -0
  26. package/docs/audit-logging.md +13 -0
  27. package/docs/dev/release-checklist.md +5 -0
  28. package/docs/exit-codes.md +44 -9
  29. package/docs/v10/BOARD.md +73 -25
  30. package/docs/v10/DECISIONS.md +22 -0
  31. package/docs/v10/GUIDE.md +1 -1
  32. package/docs/v10/PROGRESS.md +54 -0
  33. package/loki-ts/dist/loki.js +545 -536
  34. package/mcp/__init__.py +1 -1
  35. package/mcp/tests/test_v10_tools.py +41 -0
  36. package/mcp/v10_tools.py +13 -0
  37. package/package.json +1 -1
  38. package/packages/control-plane/dist/server.js +14 -1
  39. package/packages/control-plane/drizzle/meta/0001_snapshot.json +364 -0
  40. package/packages/control-plane/src/server/app.ts +7 -1
  41. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  42. package/web-app/dist/assets/{AdminPage-BnPQzCoC.js → AdminPage-oc63I7OH.js} +1 -1
  43. package/web-app/dist/assets/{Avatar-DBpPssag.js → Avatar-BziJJn7b.js} +1 -1
  44. package/web-app/dist/assets/{Badge-CxlVZMg8.js → Badge-DeTNCD_k.js} +1 -1
  45. package/web-app/dist/assets/{Button-BXMmAcji.js → Button-D9wjGlQI.js} +1 -1
  46. package/web-app/dist/assets/{CockpitPage-Birxgrlx.js → CockpitPage-Bw4_akgH.js} +1 -1
  47. package/web-app/dist/assets/{ComparePage-BqqDXwLS.js → ComparePage-XmdsH_wC.js} +1 -1
  48. package/web-app/dist/assets/{ErrorBoundary-C0u7Mi2z.js → ErrorBoundary-B0_IF9Ew.js} +1 -1
  49. package/web-app/dist/assets/{EvidenceReceiptPanel-Cp-3O8W8.js → EvidenceReceiptPanel-Da3d60pX.js} +1 -1
  50. package/web-app/dist/assets/{GitHubIssuesPanel-B_W8k95U.js → GitHubIssuesPanel-D9Jiuoyc.js} +1 -1
  51. package/web-app/dist/assets/{GitHubPRsPanel-BqvyZ1r1.js → GitHubPRsPanel-DQCUsWAb.js} +1 -1
  52. package/web-app/dist/assets/{HomePage-BQ7osrwA.js → HomePage-rfyNY25A.js} +2 -2
  53. package/web-app/dist/assets/{LoginPage-B2aHa3wI.js → LoginPage-BGHFcJj8.js} +1 -1
  54. package/web-app/dist/assets/{MagicPage-DwIxsJzJ.js → MagicPage-CdoeZ5aB.js} +1 -1
  55. package/web-app/dist/assets/{MetricsPage-CRhnEmp8.js → MetricsPage-CwKr2759.js} +2 -2
  56. package/web-app/dist/assets/{NotFoundPage-ByEZNNyC.js → NotFoundPage-9qNm-fWi.js} +1 -1
  57. package/web-app/dist/assets/{ProjectPage-LSU-iiUP.js → ProjectPage-BbeRxI8t.js} +67 -67
  58. package/web-app/dist/assets/{ProjectsPage-lBbt1xYZ.js → ProjectsPage-hw9XHZXR.js} +1 -1
  59. package/web-app/dist/assets/{SettingsPage-DOdfwqdK.js → SettingsPage-Cq6duAHh.js} +1 -1
  60. package/web-app/dist/assets/{ShowcasePage-CjsS6oSQ.js → ShowcasePage-DcpXgNKy.js} +1 -1
  61. package/web-app/dist/assets/{SystemSettingsPage-O-HGI2S_.js → SystemSettingsPage-BMWp2LCp.js} +1 -1
  62. package/web-app/dist/assets/{TeamsPage-Mt8EP17C.js → TeamsPage-Dh1rte-l.js} +1 -1
  63. package/web-app/dist/assets/{TemplatesPage-CVVy4Iz7.js → TemplatesPage-BUIMiu9V.js} +1 -1
  64. package/web-app/dist/assets/{TerminalOutput-XUgC02sX.js → TerminalOutput-g9_MPOQq.js} +1 -1
  65. package/web-app/dist/assets/{activity-DU5SJfx3.js → activity-BYGoe6e6.js} +1 -1
  66. package/web-app/dist/assets/{bell-C5YepG66.js → bell-CxCZpUMb.js} +1 -1
  67. package/web-app/dist/assets/{bot-BRExedZU.js → bot-Cpbkdqjb.js} +1 -1
  68. package/web-app/dist/assets/{check-DsyIFRaT.js → check-fO48BSWF.js} +1 -1
  69. package/web-app/dist/assets/{chevron-left-DdK1iGTs.js → chevron-left-C3J6cmqz.js} +1 -1
  70. package/web-app/dist/assets/{circle-alert-C_4QlfMh.js → circle-alert-DhdR0TJR.js} +1 -1
  71. package/web-app/dist/assets/{clock-BY9MJ-Yl.js → clock-Crp889Lj.js} +1 -1
  72. package/web-app/dist/assets/{cloud-Cl1jOslN.js → cloud-CUoa_Aqa.js} +1 -1
  73. package/web-app/dist/assets/{code-xml-BRRBRbeE.js → code-xml-BZSDiROr.js} +1 -1
  74. package/web-app/dist/assets/{database-BtZdEQTc.js → database-BC_OF6lp.js} +1 -1
  75. package/web-app/dist/assets/{dollar-sign-BqQ0fBnq.js → dollar-sign-DJUXGU2U.js} +1 -1
  76. package/web-app/dist/assets/{file-code-corner-Q9YsC7IC.js → file-code-corner-CwHaIaDe.js} +1 -1
  77. package/web-app/dist/assets/{file-plus-GrZYy7o7.js → file-plus-DD8rC5Gt.js} +1 -1
  78. package/web-app/dist/assets/{globe-CNKuJhK7.js → globe-CwDBjzLT.js} +1 -1
  79. package/web-app/dist/assets/{hammer-Cauj0_Mr.js → hammer-CzeZBlk1.js} +1 -1
  80. package/web-app/dist/assets/{index-4c5tMfoO.js → index-BgFxf62u.js} +3 -3
  81. package/web-app/dist/assets/{layers-IEQ3AZ0i.js → layers-DytzdbMj.js} +1 -1
  82. package/web-app/dist/assets/{loader-circle-DkC6SXf0.js → loader-circle-D8hG34Pb.js} +1 -1
  83. package/web-app/dist/assets/{lock-Db0jlkGG.js → lock-hk6B4D77.js} +1 -1
  84. package/web-app/dist/assets/{package-BkagJWcN.js → package-DO_92_a-.js} +1 -1
  85. package/web-app/dist/assets/{plus-BH3dF7wB.js → plus-BKQDzXcw.js} +1 -1
  86. package/web-app/dist/assets/{refresh-cw-3r-v6CaV.js → refresh-cw-DKTkN-7t.js} +1 -1
  87. package/web-app/dist/assets/{rotate-ccw-DuqsPonD.js → rotate-ccw-Yhmj5NtP.js} +1 -1
  88. package/web-app/dist/assets/{scroll-text-OaKgXAF3.js → scroll-text-Cc-NigJs.js} +1 -1
  89. package/web-app/dist/assets/{server-283wBoqS.js → server-DYIet42E.js} +1 -1
  90. package/web-app/dist/assets/{shield-alert-Cfka1gvI.js → shield-alert-Cy_zkAp6.js} +1 -1
  91. package/web-app/dist/assets/{thumbs-up-Be3PhCnV.js → thumbs-up-DtLybu1S.js} +1 -1
  92. package/web-app/dist/assets/{trash-2-BDKSlpn4.js → trash-2-n9tqmPW_.js} +1 -1
  93. package/web-app/dist/assets/{trending-down-SkioL8cr.js → trending-down-zHpd0XJs.js} +1 -1
  94. package/web-app/dist/assets/{trending-up-CzCzHgB7.js → trending-up-d8bugNud.js} +1 -1
  95. package/web-app/dist/assets/{upload-DlhOQ6b3.js → upload-CkiOu1GK.js} +1 -1
  96. package/web-app/dist/assets/{usePolling-Z7LLnCrf.js → usePolling-B9aSXIDR.js} +1 -1
  97. package/web-app/dist/assets/{user-BKegc_v2.js → user-DbfT_krW.js} +1 -1
  98. package/web-app/dist/index.html +1 -1
@@ -108,6 +108,20 @@ def _safe_json_read(path: _Path, default: Any = None) -> Any:
108
108
  return default
109
109
 
110
110
 
111
+ def _read_json_or_503(path: _Path, what: str) -> Any:
112
+ """Read JSON; a missing file is the caller's empty case, anything else is a 503.
113
+
114
+ An unreadable (denied) or corrupt file must never read as "no data": the
115
+ caller would render an empty list as a healthy empty store.
116
+ """
117
+ try:
118
+ return json.loads(path.read_text(encoding="utf-8", errors="replace"))
119
+ except FileNotFoundError:
120
+ raise
121
+ except (OSError, ValueError) as e:
122
+ raise HTTPException(status_code=503, detail=f"{what} unreadable: {type(e).__name__}")
123
+
124
+
111
125
  def _safe_read_text(path: _Path) -> str:
112
126
  """Read a text file with UTF-8 encoding, replacing non-UTF-8 bytes.
113
127
 
@@ -1039,7 +1053,9 @@ async def _push_loki_state_loop() -> None:
1039
1053
  "complexity": _sk_complexity,
1040
1054
  "mode": "autonomous",
1041
1055
  "provider": _sd.get("provider", "claude"),
1042
- "running_agents": 0,
1056
+ # Unmeasured: no skill-session file counts
1057
+ # agents, so send null, never a claimed 0.
1058
+ "running_agents": None,
1043
1059
  "pending_tasks": _sk_pending,
1044
1060
  "current_task": _sk_current_task,
1045
1061
  "version": _sk_version,
@@ -6431,8 +6447,8 @@ async def list_episodes(limit: int = Query(default=50, ge=1, le=1000)):
6431
6447
  if ep_dir.exists():
6432
6448
  for f in _memory_episode_files(ep_dir):
6433
6449
  try:
6434
- loaded.append(json.loads(f.read_text()))
6435
- except Exception:
6450
+ loaded.append(_read_json_or_503(f, "episode"))
6451
+ except FileNotFoundError:
6436
6452
  pass
6437
6453
  loaded.sort(key=lambda e: e.get("timestamp", "") if isinstance(e, dict) else "", reverse=True)
6438
6454
  return loaded[:limit]
@@ -6492,13 +6508,11 @@ async def list_patterns():
6492
6508
  # Fallback to JSON
6493
6509
  sem_dir = _get_loki_dir() / "memory" / "semantic"
6494
6510
  patterns_file = sem_dir / "patterns.json"
6495
- if patterns_file.exists():
6496
- try:
6497
- data = json.loads(patterns_file.read_text())
6498
- return data if isinstance(data, list) else data.get("patterns", [])
6499
- except Exception:
6500
- pass
6501
- return []
6511
+ try:
6512
+ data = _read_json_or_503(patterns_file, "patterns")
6513
+ except FileNotFoundError:
6514
+ return []
6515
+ return data if isinstance(data, list) else data.get("patterns", [])
6502
6516
 
6503
6517
 
6504
6518
  @app.get("/api/memory/patterns/{pattern_id}", dependencies=[Depends(auth.require_scope("read"))])
@@ -6537,8 +6551,8 @@ async def list_skills():
6537
6551
  if skills_dir.exists():
6538
6552
  for f in _memory_skill_files(skills_dir):
6539
6553
  try:
6540
- skills.append(json.loads(f.read_text()))
6541
- except Exception:
6554
+ skills.append(_read_json_or_503(f, "skill"))
6555
+ except FileNotFoundError:
6542
6556
  pass
6543
6557
  return skills
6544
6558
 
@@ -6749,12 +6763,10 @@ async def retrieve_memory(query: dict = None):
6749
6763
  async def get_memory_index():
6750
6764
  """Get memory index (Layer 1 - lightweight discovery)."""
6751
6765
  index_file = _get_loki_dir() / "memory" / "index.json"
6752
- if index_file.exists():
6753
- try:
6754
- return json.loads(index_file.read_text())
6755
- except Exception:
6756
- pass
6757
- return {"topics": [], "lastUpdated": None}
6766
+ try:
6767
+ return _read_json_or_503(index_file, "memory index")
6768
+ except FileNotFoundError:
6769
+ return {"topics": [], "lastUpdated": None}
6758
6770
 
6759
6771
 
6760
6772
  @app.get("/api/memory/timeline", dependencies=[Depends(auth.require_scope("read"))])
@@ -8985,7 +8997,8 @@ async def get_notification_triggers():
8985
8997
  try:
8986
8998
  return json.loads(triggers_file.read_text())
8987
8999
  except (json.JSONDecodeError, OSError):
8988
- return {"triggers": []}
9000
+ # Unreadable is unknown, not empty: null plus an error, never [].
9001
+ return {"triggers": None, "error": "triggers.json unreadable"}
8989
9002
 
8990
9003
 
8991
9004
  @app.put("/api/notifications/triggers", dependencies=[Depends(auth.require_scope("control"))])
@@ -12568,16 +12581,24 @@ async def list_proofs():
12568
12581
  items: list[dict] = []
12569
12582
  try:
12570
12583
  entries = sorted(proofs_dir.iterdir())
12571
- except (OSError, FileNotFoundError):
12584
+ except FileNotFoundError:
12572
12585
  return {"proofs": []}
12586
+ except OSError as e:
12587
+ raise HTTPException(status_code=503, detail=f"proofs unreadable: {type(e).__name__}")
12573
12588
  for entry in entries:
12574
12589
  if not entry.is_dir():
12575
12590
  continue
12576
12591
  proof_json = entry / "proof.json"
12577
12592
  if not proof_json.is_file():
12578
12593
  continue
12579
- data = _safe_json_read(proof_json, default=None)
12594
+ try:
12595
+ data = _read_json_or_503(proof_json, "proof")
12596
+ except HTTPException as e:
12597
+ # One bad receipt is an error row, never a silently dropped run.
12598
+ items.append({"run_id": entry.name, "error": e.detail})
12599
+ continue
12580
12600
  if not isinstance(data, dict):
12601
+ items.append({"run_id": entry.name, "error": "proof unreadable: not an object"})
12581
12602
  continue
12582
12603
  # Deterministic honesty headline (single source of truth, same access as
12583
12604
  # proofs_summary's bucketing). Read, never recomputed. The list endpoint
@@ -8974,7 +8974,7 @@ var LokiDashboard=(()=>{var Le=Object.defineProperty;var kt=Object.getOwnPropert
8974
8974
 
8975
8975
  ${e}
8976
8976
  </div>
8977
- `,this.shadowRoot.querySelectorAll(".tab").forEach(t=>{t.addEventListener("click",()=>{this._setTab(t.dataset.tab)})})}};customElements.get("loki-context-tracker")||customElements.define("loki-context-tracker",de);var Je={critical:"var(--loki-red, #ef4444)",warning:"var(--loki-yellow, #eab308)",info:"var(--loki-blue, #3b82f6)",success:"var(--loki-green, #1FC5A8)"},ot={build:{label:"Build",icon:"B"},quality:{label:"Quality",icon:"Q"},system:{label:"System",icon:"S"},security:{label:"Security",icon:"!"}},ce=class extends v{static get observedAttributes(){return["api-url","theme"]}constructor(){super(),this._notifications=[],this._triggers=[],this._summary=null,this._connected=!1,this._loadFailed=!1,this._activeTab="feed",this._categoryFilter="all",this._panelOpen=!0,this._pollInterval=null}connectedCallback(){super.connectedCallback(),this._loadNotifications(),this._loadTriggers(),this._startPolling()}disconnectedCallback(){super.disconnectedCallback(),this._stopPolling()}attributeChangedCallback(e,t,i){t!==i&&(e==="api-url"&&(this._loadNotifications(),this._loadTriggers()),e==="theme"&&this._applyTheme())}async _loadNotifications(){try{let e=this.getAttribute("api-url")||window.location.origin,t=await fetch(e+"/api/notifications");if(t.ok){let i=await t.json();i.error?this._markLoadFailed():(this._notifications=i.notifications||[],this._summary=i.summary||null,this._connected=!0,this._loadFailed=!1)}else this._markLoadFailed()}catch{this._markLoadFailed()}this.render()}_markLoadFailed(){this._connected=!1,this._loadFailed=!0,this._summary=null}async _loadTriggers(){try{let e=this.getAttribute("api-url")||window.location.origin,t=await fetch(e+"/api/notifications/triggers");if(t.ok){let i=await t.json();this._triggers=i.triggers||[]}}catch{}}async _acknowledgeNotification(e){let t=this.getAttribute("api-url")||window.location.origin;try{await fetch(t+"/api/notifications/"+encodeURIComponent(e)+"/acknowledge",{method:"POST"})}catch{}this._loadNotifications()}async _unacknowledgeNotification(e){let t=this.getAttribute("api-url")||window.location.origin;await fetch(t+"/api/notifications/"+encodeURIComponent(e)+"/unacknowledge",{method:"POST"}),this._loadNotifications()}async _acknowledgeAll(){let e=this.getAttribute("api-url")||window.location.origin,t=this._notifications.filter(i=>!i.acknowledged);for(let i of t)await fetch(e+"/api/notifications/"+encodeURIComponent(i.id)+"/acknowledge",{method:"POST"});this._loadNotifications()}async _toggleTrigger(e,t){let i=this.getAttribute("api-url")||window.location.origin,a=this._triggers.map(s=>s.id===e?{...s,enabled:t}:s);await fetch(i+"/api/notifications/triggers",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({triggers:a})}),this._triggers=a,this.render()}_startPolling(){this._poll=b({loadFn:()=>{this._loadNotifications(),this._loadTriggers()},intervalMs:5e3,element:this,immediate:!1})}_stopPolling(){this._poll&&(this._poll.stop(),this._poll=null)}_formatTime(e){if(!e)return"";try{let t=new Date(e),a=new Date-t,s=Math.floor(a/1e3),r=Math.floor(s/60),o=Math.floor(r/60),n=Math.floor(o/24);return s<60?s+"s ago":r<60?r+"m ago":o<24?o+"h ago":n<7?n+"d ago":t.toLocaleDateString()}catch{return String(e)}}_getTimeGroup(e){if(!e)return"Other";try{let t=new Date(e),i=new Date,a=new Date(i.getFullYear(),i.getMonth(),i.getDate()),s=new Date(a);s.setDate(s.getDate()-1);let r=new Date(a);return r.setDate(r.getDate()-7),t>=a?"Today":t>=s?"Yesterday":t>=r?"This Week":"Earlier"}catch{return"Other"}}_escapeHTML(e){return e?String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"):""}_getSeverityColor(e){return Je[e]||Je.info}_getCategory(e){return e.category||e.type||"system"}_switchTab(e){this._activeTab=e,this.render()}_setCategoryFilter(e){this._categoryFilter=e,this.render()}_togglePanel(){this._panelOpen=!this._panelOpen,this.render()}_bindEvents(){let e=this.shadowRoot;e.querySelectorAll(".tab").forEach(a=>{a.addEventListener("click",()=>{this._switchTab(a.dataset.tab)})}),e.querySelectorAll(".cat-btn").forEach(a=>{a.addEventListener("click",()=>{this._setCategoryFilter(a.dataset.cat)})}),e.querySelectorAll(".ack-btn").forEach(a=>{a.addEventListener("click",s=>{s.stopPropagation(),this._acknowledgeNotification(a.dataset.id)})}),e.querySelectorAll(".unread-btn").forEach(a=>{a.addEventListener("click",s=>{s.stopPropagation(),this._unacknowledgeNotification(a.dataset.id)})});let t=e.querySelector(".ack-all-btn");t&&t.addEventListener("click",()=>{this._acknowledgeAll()});let i=e.querySelector(".bell-icon");i&&i.addEventListener("click",()=>{this._togglePanel()}),e.querySelectorAll(".toggle input").forEach(a=>{a.addEventListener("change",()=>{this._toggleTrigger(a.dataset.triggerId,a.checked)})}),e.querySelectorAll(".dismiss-btn").forEach(a=>{a.addEventListener("click",s=>{s.stopPropagation(),this._acknowledgeNotification(a.dataset.id)})})}_renderBellIcon(){let e=this._summary?.unacknowledged;return`
8977
+ `,this.shadowRoot.querySelectorAll(".tab").forEach(t=>{t.addEventListener("click",()=>{this._setTab(t.dataset.tab)})})}};customElements.get("loki-context-tracker")||customElements.define("loki-context-tracker",de);var Je={critical:"var(--loki-red, #ef4444)",warning:"var(--loki-yellow, #eab308)",info:"var(--loki-blue, #3b82f6)",success:"var(--loki-green, #1FC5A8)"},ot={build:{label:"Build",icon:"B"},quality:{label:"Quality",icon:"Q"},system:{label:"System",icon:"S"},security:{label:"Security",icon:"!"}},ce=class extends v{static get observedAttributes(){return["api-url","theme"]}constructor(){super(),this._notifications=[],this._triggers=[],this._triggersFailed=!1,this._summary=null,this._connected=!1,this._loadFailed=!1,this._activeTab="feed",this._categoryFilter="all",this._panelOpen=!0,this._pollInterval=null}connectedCallback(){super.connectedCallback(),this._loadNotifications(),this._loadTriggers(),this._startPolling()}disconnectedCallback(){super.disconnectedCallback(),this._stopPolling()}attributeChangedCallback(e,t,i){t!==i&&(e==="api-url"&&(this._loadNotifications(),this._loadTriggers()),e==="theme"&&this._applyTheme())}async _loadNotifications(){try{let e=this.getAttribute("api-url")||window.location.origin,t=await fetch(e+"/api/notifications");if(t.ok){let i=await t.json();i.error?this._markLoadFailed():(this._notifications=i.notifications||[],this._summary=i.summary||null,this._connected=!0,this._loadFailed=!1)}else this._markLoadFailed()}catch{this._markLoadFailed()}this.render()}_markLoadFailed(){this._connected=!1,this._loadFailed=!0,this._summary=null}async _loadTriggers(){try{let e=this.getAttribute("api-url")||window.location.origin,t=await fetch(e+"/api/notifications/triggers");if(t.ok){let i=await t.json();i.error||i.triggers==null?this._triggersFailed=!0:(this._triggers=i.triggers,this._triggersFailed=!1)}else this._triggersFailed=!0}catch{this._triggersFailed=!0}}async _acknowledgeNotification(e){let t=this.getAttribute("api-url")||window.location.origin;try{await fetch(t+"/api/notifications/"+encodeURIComponent(e)+"/acknowledge",{method:"POST"})}catch{}this._loadNotifications()}async _unacknowledgeNotification(e){let t=this.getAttribute("api-url")||window.location.origin;await fetch(t+"/api/notifications/"+encodeURIComponent(e)+"/unacknowledge",{method:"POST"}),this._loadNotifications()}async _acknowledgeAll(){let e=this.getAttribute("api-url")||window.location.origin,t=this._notifications.filter(i=>!i.acknowledged);for(let i of t)await fetch(e+"/api/notifications/"+encodeURIComponent(i.id)+"/acknowledge",{method:"POST"});this._loadNotifications()}async _toggleTrigger(e,t){let i=this.getAttribute("api-url")||window.location.origin,a=this._triggers.map(s=>s.id===e?{...s,enabled:t}:s);await fetch(i+"/api/notifications/triggers",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({triggers:a})}),this._triggers=a,this.render()}_startPolling(){this._poll=b({loadFn:()=>{this._loadNotifications(),this._loadTriggers()},intervalMs:5e3,element:this,immediate:!1})}_stopPolling(){this._poll&&(this._poll.stop(),this._poll=null)}_formatTime(e){if(!e)return"";try{let t=new Date(e),a=new Date-t,s=Math.floor(a/1e3),r=Math.floor(s/60),o=Math.floor(r/60),n=Math.floor(o/24);return s<60?s+"s ago":r<60?r+"m ago":o<24?o+"h ago":n<7?n+"d ago":t.toLocaleDateString()}catch{return String(e)}}_getTimeGroup(e){if(!e)return"Other";try{let t=new Date(e),i=new Date,a=new Date(i.getFullYear(),i.getMonth(),i.getDate()),s=new Date(a);s.setDate(s.getDate()-1);let r=new Date(a);return r.setDate(r.getDate()-7),t>=a?"Today":t>=s?"Yesterday":t>=r?"This Week":"Earlier"}catch{return"Other"}}_escapeHTML(e){return e?String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"):""}_getSeverityColor(e){return Je[e]||Je.info}_getCategory(e){return e.category||e.type||"system"}_switchTab(e){this._activeTab=e,this.render()}_setCategoryFilter(e){this._categoryFilter=e,this.render()}_togglePanel(){this._panelOpen=!this._panelOpen,this.render()}_bindEvents(){let e=this.shadowRoot;e.querySelectorAll(".tab").forEach(a=>{a.addEventListener("click",()=>{this._switchTab(a.dataset.tab)})}),e.querySelectorAll(".cat-btn").forEach(a=>{a.addEventListener("click",()=>{this._setCategoryFilter(a.dataset.cat)})}),e.querySelectorAll(".ack-btn").forEach(a=>{a.addEventListener("click",s=>{s.stopPropagation(),this._acknowledgeNotification(a.dataset.id)})}),e.querySelectorAll(".unread-btn").forEach(a=>{a.addEventListener("click",s=>{s.stopPropagation(),this._unacknowledgeNotification(a.dataset.id)})});let t=e.querySelector(".ack-all-btn");t&&t.addEventListener("click",()=>{this._acknowledgeAll()});let i=e.querySelector(".bell-icon");i&&i.addEventListener("click",()=>{this._togglePanel()}),e.querySelectorAll(".toggle input").forEach(a=>{a.addEventListener("change",()=>{this._toggleTrigger(a.dataset.triggerId,a.checked)})}),e.querySelectorAll(".dismiss-btn").forEach(a=>{a.addEventListener("click",s=>{s.stopPropagation(),this._acknowledgeNotification(a.dataset.id)})})}_renderBellIcon(){let e=this._summary?.unacknowledged;return`
8978
8978
  <div class="bell-container">
8979
8979
  <button class="bell-icon" title="${e!=null?`${e} unread notifications`:"Unread count unknown"}">
8980
8980
  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@@ -9020,7 +9020,7 @@ var LokiDashboard=(()=>{var Le=Object.defineProperty;var kt=Object.getOwnPropert
9020
9020
  <button class="dismiss-btn" data-id="${this._escapeHTML(r.id)}" title="Dismiss">&#10005;</button>
9021
9021
  </span>
9022
9022
  </div>
9023
- `}).join(""));return a}_renderTriggerList(){return this._triggers.length===0?'<div class="empty-state">No triggers configured</div>':this._triggers.map(e=>{let t=this._getSeverityColor(e.severity),i=e.threshold_pct!=null?`Threshold: ${e.threshold_pct}%`:e.pattern||"";return`
9023
+ `}).join(""));return a}_renderTriggerList(){return this._triggersFailed&&this._triggers.length===0?'<div class="empty-state">Could not load triggers</div>':this._triggers.length===0?'<div class="empty-state">No triggers configured</div>':this._triggers.map(e=>{let t=this._getSeverityColor(e.severity),i=e.threshold_pct!=null?`Threshold: ${e.threshold_pct}%`:e.pattern||"";return`
9024
9024
  <div class="trigger-row">
9025
9025
  <label class="toggle">
9026
9026
  <input type="checkbox" data-trigger-id="${this._escapeHTML(e.id)}" ${e.enabled?"checked":""}>
@@ -15184,7 +15184,8 @@ document.addEventListener('DOMContentLoaded', function() {
15184
15184
  var files = (x.files_changed === null || x.files_changed === undefined)
15185
15185
  ? '-' : String(x.files_changed);
15186
15186
  var cost = (x.cost_usd === null || x.cost_usd === undefined)
15187
- ? '-' : ('$' + Number(x.cost_usd).toFixed(2));
15187
+ ? '-' : ((x.cost_partial === true ? 'at least ' : '')
15188
+ + '$' + Number(x.cost_usd).toFixed(2));
15188
15189
  var when = x.generated_at ? String(x.generated_at).slice(0, 16).replace('T', ' ') : '-';
15189
15190
  var link = x.has_html
15190
15191
  ? ('<a href="/api/proofs/' + encodeURIComponent(x.run_id || '') + '/html"'
@@ -147,6 +147,11 @@
147
147
  for (var i = 0; i < proofs.length; i++) {
148
148
  var p = proofs[i];
149
149
  var rid = esc(p.run_id);
150
+ if (p.error) {
151
+ rows += '<div class="row"><div class="top"><span class="rid">' + rid +
152
+ '</span><span class="ts">Could not read this proof: ' + esc(p.error) + "</span></div></div>";
153
+ continue;
154
+ }
150
155
  var usd = (p.cost_usd !== null && p.cost_usd !== undefined) ? atLeastUsd(p.cost_usd, p.cost_partial) : "";
151
156
  var fc = (p.files_changed !== null && p.files_changed !== undefined) ? (p.files_changed + " files") : "";
152
157
  var verdict = p.final_verdict ? '<span class="badge ' + badgeClass(p.final_verdict) + '" title="Recorded council vote, not a verification result">council ' + esc(p.final_verdict) + "</span>" : "";
@@ -48,6 +48,8 @@ table{width:100%;border-collapse:collapse}th,td{text-align:left;padding:6px 8px;
48
48
  <button class="sec" id="refresh">Refresh</button><span class="mut" id="conc"></span></div>
49
49
  <table><thead><tr><th></th><th>#</th><th>Issue</th><th>Size</th><th>Status</th></tr></thead><tbody id="rows"></tbody></table>
50
50
  <div id="bl-msg"></div></section>
51
+ <section class="card" id="ws"><h2>Workspace runs</h2>
52
+ <div id="ws-body" class="mut">Loading...</div></section>
51
53
  </main>
52
54
  <script>
53
55
  const $=id=>document.getElementById(id);
@@ -86,6 +88,35 @@ function render(){
86
88
  tr.appendChild(sc);tb.appendChild(tr)})}
87
89
  async function poll(){try{const s=await api('GET','/api/backlog/status');status={};s.issues.forEach(i=>status[i.number]=i);
88
90
  $('conc').textContent='concurrency '+s.concurrency;if(issues.length)render()}catch(e){}}
91
+ async function loadWorkspaceRuns(){
92
+ const b=$('ws-body');
93
+ let r;
94
+ try{r=await api('GET','/api/operator/workspaces/runs')}
95
+ catch(e){b.replaceChildren(el('div','Could not load workspace runs: '+e.message,'bad'));return}
96
+ const runs=Array.isArray(r&&r.runs)?r.runs:null;
97
+ if(!runs){b.replaceChildren(el('div','Could not load workspace runs: unexpected response','bad'));return}
98
+ if(!runs.length){b.replaceChildren(el('div','No runs'+(r.reason?' ('+r.reason+')':''),'mut'));return}
99
+ const t=el('table');
100
+ t.appendChild(el('thead')).appendChild(el('tr')).append(el('th','Run'),el('th','Repo'),el('th','Status'),el('th','Freshness'));
101
+ const tb=el('tbody');
102
+ runs.forEach(run=>{
103
+ const id=(run.workspace||'?')+'/'+(run.run_id||'?');
104
+ if(run.state==='unreadable'){
105
+ const tr=el('tr');tr.appendChild(el('td',id));
106
+ const td=el('td','Unreadable run: '+(run.error||'unknown error'),'bad');td.colSpan=3;tr.appendChild(td);tb.appendChild(tr);return}
107
+ const oc=run.outcomes&&typeof run.outcomes==='object'?run.outcomes:{};
108
+ (run.repos||[]).forEach(rp=>{
109
+ const rs=oc[rp.repo]==null?'':String(oc[rp.repo]);
110
+ const tr=el('tr');tr.appendChild(el('td',id));tr.appendChild(el('td',rp.repo||'?'));
111
+ tr.appendChild(el('td',rs||'-','st '+(rs.startsWith('ok')?'ok':rs?'bad':'')));
112
+ const f=el('td');
113
+ if(rp.stale===true)f.appendChild(el('span','stale','tag bad'));
114
+ else if(rp.stale==null&&rp.reason)f.appendChild(el('span',rp.reason,'mut'));
115
+ tr.appendChild(f);tb.appendChild(tr)});
116
+ const ig=run.status==null?'':String(run.status);
117
+ const tr=el('tr');tr.appendChild(el('td',id));tr.appendChild(el('td','integration','mut'));
118
+ const sc=el('td');sc.appendChild(el('span',ig||'-','st '+(ig||'')));tr.appendChild(sc);tr.appendChild(el('td'));tb.appendChild(tr)});
119
+ t.appendChild(tb);b.replaceChildren(t)}
89
120
  async function go(btn,msg,fn){btn.disabled=true;try{await fn();say(msg,'Done')}catch(e){say(msg,e.message,1)}btn.disabled=false;await state()}
90
121
  $('s1-go').onclick=e=>go(e.target,'s1-msg',async()=>{await api('POST','/api/onboarding/provider',{provider:$('prov').value,api_key:$('key').value||null});$('key').value=''});
91
122
  $('s2-go').onclick=e=>go(e.target,'s2-msg',async()=>{await api('POST','/api/onboarding/github',{token:$('pat').value});$('pat').value=''});
@@ -94,7 +125,7 @@ $('sel').onclick=e=>go(e.target,'bl-msg',async()=>{const n=[...document.querySel
94
125
  if(!n.length)throw new Error('Select at least one issue');await api('POST','/api/backlog/run',{issues:n});await poll()});
95
126
  $('all').onclick=e=>go(e.target,'bl-msg',async()=>{await api('POST','/api/backlog/run',{all:true});await poll()});
96
127
  $('refresh').onclick=loadIssues;
97
- state();setInterval(poll,3000);
128
+ state();loadWorkspaceRuns();setInterval(poll,3000);
98
129
  </script>
99
130
  </body>
100
131
  </html>
@@ -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.11
5
+ **Version:** v10.6.12
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
 
@@ -99,6 +99,45 @@ is required rather than optional: with a single unlabeled key, the first
99
99
  rotation would make every previously-issued receipt fail verification -- and a
100
100
  receipt that stops verifying is indistinguishable from a tampered one.
101
101
 
102
+ ### Export as an in-toto Statement (DSSE)
103
+
104
+ `loki verify <run-id> --export-dsse > receipt.dsse.json` prints a verified v10
105
+ receipt as an in-toto Statement v1 (`_type` `https://in-toto.io/Statement/v1`)
106
+ inside a DSSE envelope (`payloadType` `application/vnd.in-toto+json`). The
107
+ subject is the commit and tree the receipt covers (`gitCommit`, `gitTree`), the
108
+ `predicateType` is `https://autonomi.dev/loki/receipt/v10`, and the predicate is
109
+ the receipt body unchanged. It is signed with the same Ed25519 receipt key (the
110
+ `keyid` is the receipt `kid`) over the DSSE PAE bytes, so any DSSE verifier given
111
+ the public key (`loki keys export`) can check it. `loki verify receipt.dsse.json`
112
+ accepts the envelope too (add `--pubkey FILE` on another machine).
113
+
114
+ Export rules:
115
+
116
+ - Only a receipt that verifies AND whose run outcome is `VERIFIED` or
117
+ `ALREADY_SATISFIED` is exported. A tampered, unsigned, unchecked or
118
+ failed-run receipt is refused: non-zero exit, empty stdout, reason on stderr.
119
+ Exit codes: 1 tampered, 2 unsigned or unchecked (also an unreadable or non-JSON
120
+ receipt), 4 verified receipt of a run whose outcome is not `VERIFIED` or
121
+ `ALREADY_SATISFIED`, 66 no signing key found.
122
+ - The input must be a receipt, not an envelope: `--export-dsse` on a DSSE envelope
123
+ exits 1 ("the input is already a DSSE envelope"). When the receipt is found by
124
+ run id, an envelope under that id whose `predicate.run_id` differs is refused
125
+ with exit 1 before export.
126
+ - Group (multi-repo) receipts export too: the single-read override applies only
127
+ to the top-level receipt path, so each sub-receipt is read from its own file.
128
+ - The receipt file is read once; the bytes that were verified are the bytes signed.
129
+ - After a key rotation the envelope is signed by the current key and its `keyid`
130
+ is that key (the retired private key is not available); the predicate keeps the
131
+ receipt's own `verification.kid`, and `loki verify` checks it against the active
132
+ plus `LOKI_RECEIPT_RETIRED_PUBKEYS` keys. The export prints a note when the two differ.
133
+ - Verification uses a threshold of one: a single good signature whose keyid is
134
+ known verifies the envelope. If no signature has a known keyid the result is
135
+ UNCHECKED (exit 2), however many signatures there are; a known key with a bad
136
+ signature is TAMPERED (exit 1).
137
+ - An envelope found by run id (`.loki/runs/<run-id>/receipt.json`) must carry
138
+ `predicate.run_id` equal to that run id, otherwise exit 1. Verifying an explicit
139
+ file path does not apply this check.
140
+
102
141
  ### In a Kubernetes cluster
103
142
 
104
143
  The Helm chart wires this for you. Generate a key and pass it as a file:
@@ -35,7 +35,7 @@ Run the relevant `tests/integration/` script locally with real credentials in a
35
35
 
36
36
  **Manual procedure that closes the gap:**
37
37
 
38
- Install on a Windows host with WSL2 + Ubuntu, run `loki doctor` and the smoke commands, file an issue with the output. Document any divergence in `docs/PLATFORM-SUPPORT.md` (file does not exist yet -- create it on first finding).
38
+ Install on a Windows host with WSL2 + Ubuntu, run `loki doctor` and the smoke commands, file an issue with the output. Document any divergence in the issue, and see `docs/INSTALLATION.md` and `docs/alternative-installations.md` for supported install paths.
39
39
 
40
40
  **Partial substitute we ship:**
41
41
 
@@ -33,4 +33,18 @@ loki workspace status <run-id>
33
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.
34
34
  - Without `path`, a repo is cloned to `~/.loki/repos/owner__name`.
35
35
  - 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
+ - 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.
37
+ - Verify a run: see "Verify a workspace run" below.
36
38
  - Backlog (`loki backlog`) and dashboard starts now share the workspace worktree prep: a per-repo lock, a clean start point and copied dependencies. `LOKI_WORKSPACES=0` restores the previous worktree creation (D51-B05, D51-B06).
39
+
40
+ ## Verify a workspace run
41
+
42
+ After `loki workspace run <name> <ref>` finishes, check the evidence rather than trusting the exit code alone:
43
+
44
+ 1. Exit code: 0 means every repo succeeded and integration did not fail; 3 means only budget stops; 1 is any other failure.
45
+ 2. `loki workspace status` prints one row per repo and the integration status for each recorded run.
46
+ 3. `.loki/workspaces/<name>/<run-id>/integration.json` holds `status`, the head SHA per repo and `timing`.
47
+ 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
+ 5. Each repo PR for the run branch carries exactly one integration comment.
49
+
50
+ `tests/workspace/99-e2e.sh` runs this whole check through both `autonomy/loki` and `bin/loki` with a stub launcher and a stub `gh`.
@@ -304,6 +304,19 @@ if not result['valid']:
304
304
  print(f"First tampered line: {result['first_tampered_line']}")
305
305
  ```
306
306
 
307
+ You can also verify a whole audit directory from the command line:
308
+
309
+ ```bash
310
+ python3 dashboard/audit.py verify ~/.loki/dashboard/audit
311
+ ```
312
+
313
+ It prints one JSON object and exits 0 for a verified chain, 1 for a tampered
314
+ chain, and 2 on a usage error or when it checked nothing (a missing or empty
315
+ directory, or only files with no integrity hashes; the JSON then carries
316
+ `"status": "nothing_checked"`). Nothing checked is not a verified chain.
317
+ `python3 dashboard/audit.py tip <dir>` is unchanged and still exits 0 on an
318
+ empty chain.
319
+
307
320
  ### Disabling Chain Hashing
308
321
 
309
322
  ```bash
@@ -141,6 +141,11 @@ brew update && brew info loki-mode
141
141
  gh release view vX.Y.Z
142
142
  ```
143
143
 
144
+ ### Train cycle: previous release must be on npm
145
+ `scripts/train-cycle.sh` (phase C) refuses to cut release N+1 until the previous `release: vX` version is visible on npm (`npm_has_version`). While it is missing, the cycle logs `PREV_NOT_ON_NPM X` and returns 0 (waiting is not a failure). An unreachable registry fails closed and adds `reason=NPM_UNREACHABLE`. After `LOKI_TC_NPM_WAIT_MIN` minutes (default 45) since the previous release commit the log becomes `PREV_NOT_ON_NPM_STALE ... refusing release, needs a human`; the release stays refused either way.
146
+
147
+ Operator action: wait for the Release workflow publish to land and for the D49 auto-promote to run. Never manually promote or move the `latest` dist-tag, and never unpublish. If the STALE line appears, investigate the failed or stuck publish run rather than bypassing the check.
148
+
144
149
  ### Distribution Channel Checklist
145
150
 
146
151
  | Channel | Dashboard API | Dashboard Frontend | Memory System | Skills/References |
@@ -92,6 +92,32 @@ not rank above UNCHECKED.
92
92
  | Engine10 (`loki-ts`, `verify_cmd.ts`) | exit 3, `attestation: UNSIGNED, integrity not attested; refusing (pass --allow-unsigned to accept)` | exit 0, `attestation: UNSIGNED (accepted by --allow-unsigned; integrity not attested)` |
93
93
  | Legacy shell (`autonomy/verify.sh`) | BLOCKED (non-zero) | the receipt line passes with the same accepted line |
94
94
 
95
+ ### UNCHECKED attestations and a missing run-id pointer on legacy (D76)
96
+
97
+ Legacy `loki verify` (`autonomy/verify.sh`) no longer passes a receipt whose
98
+ attestation it cannot evaluate. A well-formed token whose kid matches no local key
99
+ (and no `LOKI_RECEIPT_RETIRED_PUBKEYS` entry) prints `attestation: UNCHECKED`,
100
+ exits 2 (BLOCKED) and the `VERDICT:` line is not VERIFIED, matching engine10.
101
+ Anyone can mint a token with a foreign kid, so exit 0 was a downgrade path. The
102
+ output names the honest cross-machine path: `loki verify --pubkey FILE <run-id>`
103
+ checks a Loki 10 run receipt against the signer's public key, but it does not read
104
+ a legacy `.loki/proofs/<id>/proof.json`; for a legacy receipt signed elsewhere, add
105
+ the signer's public key (PEM) to `LOKI_RECEIPT_RETIRED_PUBKEYS`. A token signed by
106
+ the local key still verifies with exit 0.
107
+
108
+ Run standalone (`bash autonomy/verify.sh`, outside the `loki` CLI), the script
109
+ has no attestation verdict helper, so every receipt prints
110
+ `attestation: UNCHECKED (run via loki verify)` and the receipt check fails. This
111
+ holds even with `--allow-unsigned`, which accepts only a receipt the CLI has
112
+ classified as UNSIGNED. Run `loki verify` to evaluate the attestation.
113
+
114
+ A missing `.loki/state/last-proof-id.txt` no longer skips the receipt check. If
115
+ proofs exist under `.loki/proofs` the run is `receipt: NOT VERIFIED` (exit 2); a
116
+ pointer naming a missing proof is the same. A tree that never recorded a proof
117
+ prints `receipt: NONE (no proof was recorded in this tree; no receipt was checked)`
118
+ and is not failed for it, because there is nothing to verify; that line is not a
119
+ receipt pass.
120
+
95
121
  Engine10 `loki verify [run-id]` exits: 0 verified, 1 tampered, 2 unchecked,
96
122
  3 unsigned (refused), 4 run outcome not verified (a sealed receipt of a FAILED or
97
123
  otherwise unverified run), 66 no runs. The outcome check runs before the UNSIGNED
@@ -104,6 +130,12 @@ never the local JWKS. Exits: 0 VERIFIED, 1 TAMPERED, 2 UNCHECKED (kid does not
104
130
  match the key, or the key file is unusable), 3 UNSIGNED (a receipt with no
105
131
  signature is refused even with `--allow-unsigned`, because the caller asked for a
106
132
  signature check), 4 run outcome not verified (checked before UNSIGNED).
133
+
134
+ `loki verify <run-id|receipt.json> --export-dsse` (v10 only) exits 0 after
135
+ printing the envelope, 1 TAMPERED, an envelope given as input, or an envelope
136
+ under a run id whose `predicate.run_id` differs, 2 UNSIGNED, UNCHECKED or an
137
+ unreadable receipt, 4 a verified receipt whose run outcome is not VERIFIED or
138
+ ALREADY_SATISFIED, 66 no signing key (or no runs). Refusals print nothing on stdout.
107
139
  `--pubkey` may come before or after the run-id and may be written `--pubkey=FILE`; `--pubkey` with no file or an empty `--pubkey=`, a repeated `--pubkey`, an unknown option, or more than one run-id exits 2.
108
140
 
109
141
  An early draft spec listed `1=BLOCKED, 2=CONCERNS`. That ordering was rejected:
@@ -112,7 +144,7 @@ severity-rises-with-the-code rule that every other command follows.
112
144
 
113
145
  ### Known gaps until v10.0.0
114
146
 
115
- The table above is the target. On the current release, four inputs do not
147
+ The table above is the target. On the current release, three inputs do not
116
148
  return what it implies. Measured on this checkout:
117
149
 
118
150
  | Input | Command | Exit today | Target |
@@ -120,16 +152,19 @@ return what it implies. Measured on this checkout:
120
152
  | Empty diff (a git repo with no changes vs base) | `loki verify` | 1 (CONCERNS) | 3 |
121
153
  | Not a git directory | `loki verify` | 1 (CONCERNS) | 2 |
122
154
  | Unknown flag | `loki verify --no-such-flag` | 3 | 64 |
123
- | `--fast` with nothing scanned (0 files) | `loki verify --fast <empty-dir> --no-cache` | 0 | nonzero |
124
155
 
125
- `loki verify --fast` (`autonomy/lib/fast_verify.py`) also exits 0 for a
126
- nonexistent root and silently ignores unknown flags. Until v10.0.0, do not
127
- trust exit 0 from `--fast` alone: check stdout for `INCONCLUSIVE`, which it
128
- prints when nothing was scanned.
156
+ The gap is tracked as the pending moat case P2.verify-exit-contract,
157
+ milestone v10.0.0, in `tests/moat/pending.txt`.
158
+
159
+ ### `loki verify --fast`
129
160
 
130
- Both gaps are tracked as pending moat cases, milestone v10.0.0, in
131
- `tests/moat/pending.txt`: P2.verify-exit-contract (the first three rows)
132
- and P2.fast-verify-inconclusive-not-zero (the `--fast` row).
161
+ | Code | Meaning |
162
+ |---|---|
163
+ | 0 | PASS: files were scanned and nothing blocking was found |
164
+ | 1 | FAIL: a blocking finding |
165
+ | 2 | The root directory does not exist |
166
+ | 3 | INCONCLUSIVE: nothing was scanned (0 files) |
167
+ | 64 | Unknown option (`--path DIR` and `--help` are supported; `--help` exits 0) |
133
168
 
134
169
  ## `loki proof verify <id>`
135
170