loki-mode 10.6.10 → 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.
- package/README.md +20 -2
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/lib/backlog.py +19 -3
- package/autonomy/lib/fast_verify.py +19 -1
- package/autonomy/lib/proof-pr.sh +34 -3
- package/autonomy/lib/workspace.py +27 -1
- package/autonomy/lib/workspace_metrics.py +85 -0
- package/autonomy/lib/worktree_prep.py +45 -3
- package/autonomy/loki +62 -3
- package/autonomy/run.sh +95 -6
- package/autonomy/verify.sh +87 -20
- package/dashboard/__init__.py +1 -1
- package/dashboard/api_operator.py +27 -0
- package/dashboard/api_runs.py +52 -0
- package/dashboard/api_workspaces.py +140 -0
- package/dashboard/audit.py +8 -2
- package/dashboard/server.py +42 -21
- package/dashboard/static/index.html +4 -3
- package/dashboard/static/proofs.html +5 -0
- package/dashboard/static/start.html +32 -1
- package/docs/INSTALLATION.md +1 -1
- package/docs/SIGNED-RECEIPTS.md +39 -0
- package/docs/UNREACHABLE-TESTS.md +1 -1
- package/docs/WORKSPACES.md +14 -0
- package/docs/audit-logging.md +13 -0
- package/docs/dev/release-checklist.md +5 -0
- package/docs/exit-codes.md +44 -9
- package/docs/v10/BOARD.md +78 -26
- package/docs/v10/DECISIONS.md +29 -0
- package/docs/v10/E2E-D65.md +8 -0
- package/docs/v10/GUIDE.md +1 -1
- package/docs/v10/PROGRESS.md +68 -0
- package/loki-ts/dist/loki.js +545 -536
- package/mcp/__init__.py +1 -1
- package/mcp/tests/test_v10_tools.py +41 -0
- package/mcp/v10_tools.py +13 -0
- package/package.json +1 -1
- package/packages/control-plane/dist/server.js +14 -1
- package/packages/control-plane/drizzle/meta/0001_snapshot.json +364 -0
- package/packages/control-plane/src/server/app.ts +7 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/web-app/dist/assets/{AdminPage-OaSCF3M4.js → AdminPage-oc63I7OH.js} +1 -1
- package/web-app/dist/assets/{Avatar-xa3G7_vs.js → Avatar-BziJJn7b.js} +1 -1
- package/web-app/dist/assets/{Badge-Ci_UcHij.js → Badge-DeTNCD_k.js} +1 -1
- package/web-app/dist/assets/{Button-CFrrmbvL.js → Button-D9wjGlQI.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-6hyVl_QZ.js → CockpitPage-Bw4_akgH.js} +1 -1
- package/web-app/dist/assets/{ComparePage-B5H56KCG.js → ComparePage-XmdsH_wC.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-CBbRHsYr.js → ErrorBoundary-B0_IF9Ew.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-D-Zoneum.js → EvidenceReceiptPanel-Da3d60pX.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-D7XPRsic.js → GitHubIssuesPanel-D9Jiuoyc.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-CzHbBPAn.js → GitHubPRsPanel-DQCUsWAb.js} +1 -1
- package/web-app/dist/assets/{HomePage-ClOx5qF1.js → HomePage-rfyNY25A.js} +2 -2
- package/web-app/dist/assets/{LoginPage-3Yz_9j8E.js → LoginPage-BGHFcJj8.js} +1 -1
- package/web-app/dist/assets/{MagicPage-Dumn48gC.js → MagicPage-CdoeZ5aB.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-CYCYVG_8.js → MetricsPage-CwKr2759.js} +2 -2
- package/web-app/dist/assets/{NotFoundPage-CV-x_1qH.js → NotFoundPage-9qNm-fWi.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-CeO7dnyM.js → ProjectPage-BbeRxI8t.js} +67 -67
- package/web-app/dist/assets/{ProjectsPage-BT9mz6iD.js → ProjectsPage-hw9XHZXR.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-BL8X9fAB.js → SettingsPage-Cq6duAHh.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-8f3lnSpc.js → ShowcasePage-DcpXgNKy.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-BUNAAzh2.js → SystemSettingsPage-BMWp2LCp.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-Qj-VEZJz.js → TeamsPage-Dh1rte-l.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-b8lKqe5q.js → TemplatesPage-BUIMiu9V.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-jOnfQKB4.js → TerminalOutput-g9_MPOQq.js} +1 -1
- package/web-app/dist/assets/{activity-w7GnyBNp.js → activity-BYGoe6e6.js} +1 -1
- package/web-app/dist/assets/{bell-2i4ejMOA.js → bell-CxCZpUMb.js} +1 -1
- package/web-app/dist/assets/{bot-jaPm4NIs.js → bot-Cpbkdqjb.js} +1 -1
- package/web-app/dist/assets/{check-CvNtxeKM.js → check-fO48BSWF.js} +1 -1
- package/web-app/dist/assets/{chevron-left-zzBjHAtX.js → chevron-left-C3J6cmqz.js} +1 -1
- package/web-app/dist/assets/{circle-alert-CCU0IU2m.js → circle-alert-DhdR0TJR.js} +1 -1
- package/web-app/dist/assets/{clock-BvZckMwN.js → clock-Crp889Lj.js} +1 -1
- package/web-app/dist/assets/{cloud-hrMWDdYp.js → cloud-CUoa_Aqa.js} +1 -1
- package/web-app/dist/assets/{code-xml-v6l40T-K.js → code-xml-BZSDiROr.js} +1 -1
- package/web-app/dist/assets/{database-l2TMUrQd.js → database-BC_OF6lp.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-C2V6OvpO.js → dollar-sign-DJUXGU2U.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-Bytb5JGA.js → file-code-corner-CwHaIaDe.js} +1 -1
- package/web-app/dist/assets/{file-plus-BynCYQB8.js → file-plus-DD8rC5Gt.js} +1 -1
- package/web-app/dist/assets/{globe-DifDdNMH.js → globe-CwDBjzLT.js} +1 -1
- package/web-app/dist/assets/{hammer-D7TsOJD6.js → hammer-CzeZBlk1.js} +1 -1
- package/web-app/dist/assets/{index-Zohfqaz4.js → index-BgFxf62u.js} +3 -3
- package/web-app/dist/assets/{layers-Bk8c2YLS.js → layers-DytzdbMj.js} +1 -1
- package/web-app/dist/assets/{loader-circle-BrLdbMwP.js → loader-circle-D8hG34Pb.js} +1 -1
- package/web-app/dist/assets/{lock-COpmjMyJ.js → lock-hk6B4D77.js} +1 -1
- package/web-app/dist/assets/{package-BMzYdY7F.js → package-DO_92_a-.js} +1 -1
- package/web-app/dist/assets/{plus-B9xpu1Pg.js → plus-BKQDzXcw.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-C2Lzh-AA.js → refresh-cw-DKTkN-7t.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-2Yyy-uQd.js → rotate-ccw-Yhmj5NtP.js} +1 -1
- package/web-app/dist/assets/{scroll-text-DiK2yhsT.js → scroll-text-Cc-NigJs.js} +1 -1
- package/web-app/dist/assets/{server-xlJBgleU.js → server-DYIet42E.js} +1 -1
- package/web-app/dist/assets/{shield-alert-D9w4yBCi.js → shield-alert-Cy_zkAp6.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-CcWfvcE9.js → thumbs-up-DtLybu1S.js} +1 -1
- package/web-app/dist/assets/{trash-2-_hk_pEAp.js → trash-2-n9tqmPW_.js} +1 -1
- package/web-app/dist/assets/{trending-down-CDZT99tC.js → trending-down-zHpd0XJs.js} +1 -1
- package/web-app/dist/assets/{trending-up-A0oreTXc.js → trending-up-d8bugNud.js} +1 -1
- package/web-app/dist/assets/{upload-DfLwhGG2.js → upload-CkiOu1GK.js} +1 -1
- package/web-app/dist/assets/{usePolling-B7gW2YY_.js → usePolling-B9aSXIDR.js} +1 -1
- package/web-app/dist/assets/{user-C-y7_Oon.js → user-DbfT_krW.js} +1 -1
- package/web-app/dist/index.html +1 -1
package/dashboard/server.py
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
6435
|
-
except
|
|
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
|
-
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
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(
|
|
6541
|
-
except
|
|
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
|
-
|
|
6753
|
-
|
|
6754
|
-
|
|
6755
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""):""}_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">✕</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
|
-
? '-' : ('
|
|
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>
|
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.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
|
|
package/docs/SIGNED-RECEIPTS.md
CHANGED
|
@@ -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/
|
|
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
|
|
package/docs/WORKSPACES.md
CHANGED
|
@@ -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`.
|
package/docs/audit-logging.md
CHANGED
|
@@ -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 |
|
package/docs/exit-codes.md
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
and
|
|
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
|
|