loki-mode 7.7.24 → 7.7.26

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/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: Autonomous spec-to-product system. Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product via the RARV-C closure loop, with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
4
4
  ---
5
5
 
6
- # Loki Mode v7.7.24
6
+ # Loki Mode v7.7.26
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -381,4 +381,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
381
381
 
382
382
  ---
383
383
 
384
- **v7.7.24 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
384
+ **v7.7.26 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.7.24
1
+ 7.7.26
package/autonomy/run.sh CHANGED
@@ -4405,7 +4405,7 @@ generate_dashboard() {
4405
4405
  <body>
4406
4406
  <div class="header">
4407
4407
  <h1>LOKI MODE</h1>
4408
- <div class="subtitle">Autonomous Multi-Agent Startup System</div>
4408
+ <div class="subtitle">Autonomous Spec-to-Product System</div>
4409
4409
  <div class="phase" id="phase">Loading...</div>
4410
4410
  </div>
4411
4411
  <div class="stats">
@@ -12379,7 +12379,7 @@ main() {
12379
12379
  echo " ███████╗╚██████╔╝██║ ██╗██║ ██║ ╚═╝ ██║╚██████╔╝██████╔╝███████╗"
12380
12380
  echo " ╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝"
12381
12381
  echo -e "${NC}"
12382
- echo -e " ${CYAN}Autonomous Multi-Agent Startup System${NC}"
12382
+ echo -e " ${CYAN}Autonomous Spec-to-Product System${NC}"
12383
12383
  echo -e " ${CYAN}Version: $(cat "$PROJECT_DIR/VERSION" 2>/dev/null || echo "4.x.x")${NC}"
12384
12384
  echo ""
12385
12385
 
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.7.24"
10
+ __version__ = "7.7.26"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -2751,14 +2751,61 @@ async def get_token_economics():
2751
2751
 
2752
2752
  @app.post("/api/memory/consolidate", dependencies=[Depends(auth.require_scope("control"))])
2753
2753
  async def consolidate_memory(hours: int = 24):
2754
- """Trigger memory consolidation (stub - returns current state)."""
2755
- return {"status": "ok", "message": f"Consolidation for last {hours}h", "consolidated": 0, "patternsCreated": 0, "patternsMerged": 0, "episodesProcessed": 0}
2754
+ """Run the real episodic-to-semantic consolidation pipeline."""
2755
+ memory_dir = _get_loki_dir() / "memory"
2756
+ try:
2757
+ import sys as _sys
2758
+ project_root = str(_Path(__file__).resolve().parent.parent)
2759
+ if project_root not in _sys.path:
2760
+ _sys.path.insert(0, project_root)
2761
+ from memory.storage import MemoryStorage
2762
+ from memory.consolidation import ConsolidationPipeline
2763
+ storage = MemoryStorage(str(memory_dir))
2764
+ pipeline = ConsolidationPipeline(storage=storage, base_path=str(memory_dir))
2765
+ result = pipeline.consolidate(since_hours=hours)
2766
+ d = result.to_dict()
2767
+ return {
2768
+ "status": "ok",
2769
+ "message": f"Consolidated episodes from the last {hours}h",
2770
+ "consolidated": d.get("patterns_created", 0) + d.get("patterns_merged", 0),
2771
+ "patternsCreated": d.get("patterns_created", 0),
2772
+ "patternsMerged": d.get("patterns_merged", 0),
2773
+ "antiPatternsCreated": d.get("anti_patterns_created", 0),
2774
+ "episodesProcessed": d.get("episodes_processed", 0),
2775
+ "durationSeconds": round(d.get("duration_seconds", 0.0), 3),
2776
+ }
2777
+ except Exception as e:
2778
+ raise HTTPException(status_code=503, detail=f"Consolidation unavailable: {e}")
2756
2779
 
2757
2780
 
2758
2781
  @app.post("/api/memory/retrieve", dependencies=[Depends(auth.require_scope("control"))])
2759
2782
  async def retrieve_memory(query: dict = None):
2760
- """Search memories by query."""
2761
- return {"results": [], "query": query}
2783
+ """Task-aware retrieval against the real memory engine.
2784
+
2785
+ Body: {"goal": str, "phase"?: str, "task_type"?: str, "top_k"?: int}.
2786
+ """
2787
+ query = query or {}
2788
+ goal = (query.get("goal") or query.get("q") or "").strip()
2789
+ if not goal:
2790
+ return {"results": [], "query": query, "message": "provide a 'goal' to retrieve against"}
2791
+ top_k = int(query.get("top_k", 5))
2792
+ top_k = max(1, min(top_k, 50))
2793
+ memory_dir = _get_loki_dir() / "memory"
2794
+ try:
2795
+ import sys as _sys
2796
+ project_root = str(_Path(__file__).resolve().parent.parent)
2797
+ if project_root not in _sys.path:
2798
+ _sys.path.insert(0, project_root)
2799
+ from memory.storage import MemoryStorage
2800
+ from memory.retrieval import MemoryRetrieval
2801
+ retriever = MemoryRetrieval(MemoryStorage(str(memory_dir)))
2802
+ context = {"goal": goal, "phase": query.get("phase", "development")}
2803
+ if query.get("task_type"):
2804
+ context["task_type"] = query["task_type"]
2805
+ results = retriever.retrieve_task_aware(context, top_k=top_k, token_budget=query.get("token_budget"))
2806
+ return {"results": results, "query": {"goal": goal, "top_k": top_k}, "count": len(results)}
2807
+ except Exception as e:
2808
+ raise HTTPException(status_code=503, detail=f"Retrieval unavailable: {e}")
2762
2809
 
2763
2810
 
2764
2811
  @app.get("/api/memory/index")
@@ -13195,7 +13195,7 @@ var LokiDashboard=(()=>{var wt=Object.defineProperty;var ae=Object.getOwnPropert
13195
13195
  border-radius: 4px;
13196
13196
  }
13197
13197
  </style>
13198
- `,a="";this._loading&&this._items.length===0?a='<div class="esc-empty">Loading escalations...</div>':this._error?a='<div class="esc-error">Failed to load escalations: '+this._escapeHtml(this._error)+"</div>":!this._items||this._items.length===0?a='<div class="esc-empty">Escalations: no events yet. Handoff/escalation markdown documents written by the runner under .loki/escalations/ will appear here.</div>':a='<div class="esc-list">'+this._items.map(n=>{let l=this._escapeHtml(n.filename||""),c=this._escapeHtml(this._formatSize(n.size_bytes)),p=this._escapeHtml(this._formatDate(n.modified_at));return'<div class="esc-item" data-filename="'+l+'"><span class="esc-name">'+l+'</span><span class="esc-meta">'+c+" &middot; "+p+"</span></div>"}).join("")+"</div>";let i="";if(this._activeFile){let o=this._escapeHtml(this._activeFile),n;this._activeBodyError?n='<div class="esc-error">Failed to load: '+this._escapeHtml(this._activeBodyError)+"</div>":this._activeBody===null?n='<div class="esc-body">Loading '+o+"...</div>":n='<div class="esc-body">'+this._escapeHtml(this._activeBody)+"</div>",i='<div class="esc-viewer"><div class="esc-viewer-header"><span class="esc-name">'+o+'</span><button class="esc-close-btn" data-action="close">Close</button></div>'+n+"</div>"}t.innerHTML=e+'<div class="esc-wrapper"><div class="esc-explain">Handoff/escalation documents written under .loki/escalations/. Click an entry to view its contents.</div>'+a+i+"</div>",t.querySelectorAll(".esc-item").forEach(o=>{o.addEventListener("click",()=>{let n=o.getAttribute("data-filename");n&&this._openFile(n)})});let r=t.querySelector('.esc-close-btn[data-action="close"]');r&&r.addEventListener("click",()=>this._closeFile())}};typeof customElements<"u"&&!customElements.get("loki-escalations")&&customElements.define("loki-escalations",_t);var yt=class extends h{static get observedAttributes(){return["api-url","theme"]}constructor(){super(),this._transcripts=[],this._loading=!1,this._error=null,this._api=null,this._pollInterval=null}connectedCallback(){super.connectedCallback(),this._setupApi(),this._load(),this._pollInterval=setInterval(()=>this._load(),3e4)}disconnectedCallback(){super.disconnectedCallback(),this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null)}attributeChangedCallback(t,e,a){e!==a&&(t==="api-url"&&this._api&&(this._api.baseUrl=a,this._load()),t==="theme"&&this._applyTheme())}_setupApi(){let t=this.getAttribute("api-url")||(typeof window<"u"?window.location.origin:"");this._api=g({baseUrl:t})}async _load(){this._loading=!0,this._error=null;try{let t=await this._api.get("/api/council/transcripts?limit=10");this._transcripts=Array.isArray(t&&t.transcripts)?t.transcripts:[]}catch(t){this._error=t&&t.message?t.message:String(t),this._transcripts=[]}finally{this._loading=!1,this.render()}}_escapeHtml(t){return t==null?"":String(t).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}_formatTimestamp(t){if(!t)return"--";try{let e=new Date(t);return isNaN(e.getTime())?t:e.toLocaleString()}catch{return t}}_truncate(t,e){if(!t)return"";let a=String(t);return a.length>e?a.slice(0,e)+"...":a}_verdictBadgeHtml(t){let e=String(t||"").toUpperCase();return e==="APPROVE"?'<span class="ct-badge ct-badge-approve">APPROVE</span>':e==="REJECT"?'<span class="ct-badge ct-badge-reject">REJECT</span>':e==="CANNOT_VALIDATE"?'<span class="ct-badge ct-badge-cannot">CANNOT_VALIDATE</span>':'<span class="ct-badge ct-badge-unknown">'+this._escapeHtml(e||"UNKNOWN")+"</span>"}_outcomeBadgeHtml(t){let e=String(t||"").toUpperCase();return e==="APPROVED"?'<span class="ct-badge ct-badge-approve">APPROVED</span>':e==="REJECTED"?'<span class="ct-badge ct-badge-reject">REJECTED</span>':e==="BLOCKED_BY_GATE"?'<span class="ct-badge ct-badge-blocked">BLOCKED BY GATE</span>':'<span class="ct-badge ct-badge-unknown">'+this._escapeHtml(e||"UNKNOWN")+"</span>"}_voterRowHtml(t,e){let a=t.is_contrarian===!0,i=a&&e===!0,s="ct-voter-row";a&&(s+=" ct-voter-contrarian"),i&&(s+=" ct-voter-flipped");let r=this._escapeHtml(t.name||"unknown"),o=this._verdictBadgeHtml(t.verdict),n=this._escapeHtml(this._truncate(t.reasoning,300)),l="",c="";i?(l='<span class="ct-badge ct-badge-override">OVERRIDE</span>',c=`<div class="ct-flip-caption">Devil's Advocate flipped this outcome</div>`):a&&t.triggered&&(l=`<span class="ct-badge ct-badge-da">DEVIL'S ADVOCATE</span>`);let p="";a&&Array.isArray(t.challenges)&&t.challenges.length>0&&(p='<ul class="ct-challenges">'+t.challenges.map(m=>"<li>"+this._escapeHtml(String(m))+"</li>").join("")+"</ul>");let u="";return Array.isArray(t.issues)&&t.issues.length>0&&(u='<ul class="ct-issues">'+t.issues.map(m=>{let f=this._escapeHtml(m.severity||""),x=this._escapeHtml(m.description||"");return'<li><span class="ct-issue-sev ct-issue-sev-'+f.toLowerCase()+'">'+f+"</span> "+x+"</li>"}).join("")+"</ul>"),'<div class="'+s+'"><div class="ct-voter-header"><span class="ct-voter-name">'+r+"</span>"+o+l+"</div>"+(n?'<div class="ct-voter-reason">'+n+"</div>":"")+p+u+c+"</div>"}_transcriptCardHtml(t){let e=this._escapeHtml(String(t.iteration||"--")),a=this._escapeHtml(this._formatTimestamp(t.timestamp)),i=this._escapeHtml(this._truncate(t.task_or_prd,200)),s=this._outcomeBadgeHtml(t.outcome),r=Array.isArray(t.voters)?t.voters:[],o=r.filter(m=>!m.is_contrarian),n=r.filter(m=>m.is_contrarian),l=o.map(m=>this._voterRowHtml(m,!1)).join(""),c="";t.contrarian_triggered&&(c='<div class="ct-contrarian-section"><div class="ct-section-label">Anti-Sycophancy Check</div>'+n.map(f=>this._voterRowHtml(f,t.contrarian_flipped)).join("")+"</div>");let p=typeof t.approve_count=="number"?t.approve_count:"--",u=typeof t.reject_count=="number"?t.reject_count:"--",b=typeof t.threshold=="number"?t.threshold:"--";return'<div class="ct-card"><div class="ct-card-header"><div class="ct-card-meta"><span class="ct-iter-label">Iteration '+e+'</span><span class="ct-ts">'+a+"</span></div>"+s+"</div>"+(i?'<div class="ct-prd-preview">'+i+"</div>":"")+'<div class="ct-tally">Approve: '+p+" &middot; Reject: "+u+" &middot; Threshold: "+b+'</div><div class="ct-voters">'+l+"</div>"+c+"</div>"}render(){let t=this.shadowRoot||this;if(!t)return;let e=`
13198
+ `,a="";this._loading&&this._items.length===0?a='<div class="esc-empty">Loading escalations...</div>':this._error?a='<div class="esc-error">Failed to load escalations: '+this._escapeHtml(this._error)+"</div>":!this._items||this._items.length===0?a='<div class="esc-empty">Escalations: no events yet. Handoff/escalation markdown documents written by the runner under .loki/escalations/ will appear here.</div>':a='<div class="esc-list">'+this._items.map(n=>{let l=this._escapeHtml(n.filename||""),c=this._escapeHtml(this._formatSize(n.size_bytes)),p=this._escapeHtml(this._formatDate(n.modified_at));return'<div class="esc-item" data-filename="'+l+'"><span class="esc-name">'+l+'</span><span class="esc-meta">'+c+" &middot; "+p+"</span></div>"}).join("")+"</div>";let i="";if(this._activeFile){let o=this._escapeHtml(this._activeFile),n;this._activeBodyError?n='<div class="esc-error">Failed to load: '+this._escapeHtml(this._activeBodyError)+"</div>":this._activeBody===null?n='<div class="esc-body">Loading '+o+"...</div>":n='<div class="esc-body">'+this._escapeHtml(this._activeBody)+"</div>",i='<div class="esc-viewer"><div class="esc-viewer-header"><span class="esc-name">'+o+'</span><button class="esc-close-btn" data-action="close">Close</button></div>'+n+"</div>"}t.innerHTML=e+'<div class="esc-wrapper"><div class="esc-explain">Handoff/escalation documents written under .loki/escalations/. Click an entry to view its contents.</div>'+a+i+"</div>",t.querySelectorAll(".esc-item").forEach(o=>{o.addEventListener("click",()=>{let n=o.getAttribute("data-filename");n&&this._openFile(n)})});let r=t.querySelector('.esc-close-btn[data-action="close"]');r&&r.addEventListener("click",()=>this._closeFile())}};typeof customElements<"u"&&!customElements.get("loki-escalations")&&customElements.define("loki-escalations",_t);var yt=class extends h{static get observedAttributes(){return["api-url","theme"]}constructor(){super(),this._transcripts=[],this._hookEvents=[],this._loading=!1,this._error=null,this._api=null,this._pollInterval=null}connectedCallback(){super.connectedCallback(),this._setupApi(),this._load(),this._pollInterval=setInterval(()=>this._load(),3e4)}disconnectedCallback(){super.disconnectedCallback(),this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null)}attributeChangedCallback(t,e,a){e!==a&&(t==="api-url"&&this._api&&(this._api.baseUrl=a,this._load()),t==="theme"&&this._applyTheme())}_setupApi(){let t=this.getAttribute("api-url")||(typeof window<"u"?window.location.origin:"");this._api=g({baseUrl:t})}async _load(){this._loading=!0,this._error=null;try{let t=await this._api.get("/api/council/transcripts?limit=10");this._transcripts=Array.isArray(t&&t.transcripts)?t.transcripts:[]}catch(t){this._error=t&&t.message?t.message:String(t),this._transcripts=[]}try{let t=await this._api.get("/api/council/transcripts?limit=20&type_prefix=claude_hook_");this._hookEvents=Array.isArray(t&&t.hook_events)?t.hook_events:[]}catch{this._hookEvents=[]}finally{this._loading=!1,this.render()}}_escapeHtml(t){return t==null?"":String(t).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}_formatTimestamp(t){if(!t)return"--";try{let e=new Date(t);return isNaN(e.getTime())?t:e.toLocaleString()}catch{return t}}_truncate(t,e){if(!t)return"";let a=String(t);return a.length>e?a.slice(0,e)+"...":a}_verdictBadgeHtml(t){let e=String(t||"").toUpperCase();return e==="APPROVE"?'<span class="ct-badge ct-badge-approve">APPROVE</span>':e==="REJECT"?'<span class="ct-badge ct-badge-reject">REJECT</span>':e==="CANNOT_VALIDATE"?'<span class="ct-badge ct-badge-cannot">CANNOT_VALIDATE</span>':'<span class="ct-badge ct-badge-unknown">'+this._escapeHtml(e||"UNKNOWN")+"</span>"}_outcomeBadgeHtml(t){let e=String(t||"").toUpperCase();return e==="APPROVED"?'<span class="ct-badge ct-badge-approve">APPROVED</span>':e==="REJECTED"?'<span class="ct-badge ct-badge-reject">REJECTED</span>':e==="BLOCKED_BY_GATE"?'<span class="ct-badge ct-badge-blocked">BLOCKED BY GATE</span>':'<span class="ct-badge ct-badge-unknown">'+this._escapeHtml(e||"UNKNOWN")+"</span>"}_voterRowHtml(t,e){let a=t.is_contrarian===!0,i=a&&e===!0,s="ct-voter-row";a&&(s+=" ct-voter-contrarian"),i&&(s+=" ct-voter-flipped");let r=this._escapeHtml(t.name||"unknown"),o=this._verdictBadgeHtml(t.verdict),n=this._escapeHtml(this._truncate(t.reasoning,300)),l="",c="";i?(l='<span class="ct-badge ct-badge-override">OVERRIDE</span>',c=`<div class="ct-flip-caption">Devil's Advocate flipped this outcome</div>`):a&&t.triggered&&(l=`<span class="ct-badge ct-badge-da">DEVIL'S ADVOCATE</span>`);let p="";a&&Array.isArray(t.challenges)&&t.challenges.length>0&&(p='<ul class="ct-challenges">'+t.challenges.map(m=>"<li>"+this._escapeHtml(String(m))+"</li>").join("")+"</ul>");let u="";return Array.isArray(t.issues)&&t.issues.length>0&&(u='<ul class="ct-issues">'+t.issues.map(m=>{let f=this._escapeHtml(m.severity||""),x=this._escapeHtml(m.description||"");return'<li><span class="ct-issue-sev ct-issue-sev-'+f.toLowerCase()+'">'+f+"</span> "+x+"</li>"}).join("")+"</ul>"),'<div class="'+s+'"><div class="ct-voter-header"><span class="ct-voter-name">'+r+"</span>"+o+l+"</div>"+(n?'<div class="ct-voter-reason">'+n+"</div>":"")+p+u+c+"</div>"}_transcriptCardHtml(t){let e=this._escapeHtml(String(t.iteration||"--")),a=this._escapeHtml(this._formatTimestamp(t.timestamp)),i=this._escapeHtml(this._truncate(t.task_or_prd,200)),s=this._outcomeBadgeHtml(t.outcome),r=Array.isArray(t.voters)?t.voters:[],o=r.filter(m=>!m.is_contrarian),n=r.filter(m=>m.is_contrarian),l=o.map(m=>this._voterRowHtml(m,!1)).join(""),c="";t.contrarian_triggered&&(c='<div class="ct-contrarian-section"><div class="ct-section-label">Anti-Sycophancy Check</div>'+n.map(f=>this._voterRowHtml(f,t.contrarian_flipped)).join("")+"</div>");let p=typeof t.approve_count=="number"?t.approve_count:"--",u=typeof t.reject_count=="number"?t.reject_count:"--",b=typeof t.threshold=="number"?t.threshold:"--";return'<div class="ct-card"><div class="ct-card-header"><div class="ct-card-meta"><span class="ct-iter-label">Iteration '+e+'</span><span class="ct-ts">'+a+"</span></div>"+s+"</div>"+(i?'<div class="ct-prd-preview">'+i+"</div>":"")+'<div class="ct-tally">Approve: '+p+" &middot; Reject: "+u+" &middot; Threshold: "+b+'</div><div class="ct-voters">'+l+"</div>"+c+"</div>"}render(){let t=this.shadowRoot||this;if(!t)return;let e=`
13199
13199
  <style>
13200
13200
  :host { display: block; margin-top: 24px; }
13201
13201
  .ct-wrapper {
@@ -13364,7 +13364,7 @@ var LokiDashboard=(()=>{var wt=Object.defineProperty;var ae=Object.getOwnPropert
13364
13364
  .ct-badge-da { background: #fdf3d4; color: #8a6c0e; }
13365
13365
  .ct-badge-unknown { background: var(--bg-secondary, #F8F4F0); color: var(--text-muted, #939084); }
13366
13366
  </style>
13367
- `,a="";this._loading&&this._transcripts.length===0?a='<div class="ct-empty">Loading council transcripts...</div>':this._error?a='<div class="ct-error">Failed to load transcripts: '+this._escapeHtml(this._error)+"</div>":!this._transcripts||this._transcripts.length===0?a='<div class="ct-empty">No council rounds recorded yet -- transcripts appear after the first iteration vote.</div>':a='<div class="ct-list">'+this._transcripts.map(s=>this._transcriptCardHtml(s)).join("")+"</div>",t.innerHTML=e+'<div class="ct-wrapper"><h3 class="ct-heading">Council Transcripts</h3><div class="ct-explain">Per-iteration voting records from .loki/council/transcripts/. Polls every 30 seconds.</div>'+a+"</div>"}};typeof customElements<"u"&&!customElements.get("loki-council-transcripts")&&customElements.define("loki-council-transcripts",yt);var He="1.4.0";function Re(d={}){return d.theme?_.setTheme(d.theme):d.autoDetectContext!==!1?_.init():R.init(),d.apiUrl&&g({baseUrl:d.apiUrl}),{theme:_.getTheme(),context:_.detectContext()}}return le(Be);})();
13367
+ `,a="";this._loading&&this._transcripts.length===0?a='<div class="ct-empty">Loading council transcripts...</div>':this._error?a='<div class="ct-error">Failed to load transcripts: '+this._escapeHtml(this._error)+"</div>":!this._transcripts||this._transcripts.length===0?a='<div class="ct-empty">No council rounds recorded yet -- transcripts appear after the first iteration vote.</div>':a='<div class="ct-list">'+this._transcripts.map(s=>this._transcriptCardHtml(s)).join("")+"</div>",t.innerHTML=e+'<div class="ct-wrapper"><h3 class="ct-heading">Council Transcripts</h3><div class="ct-explain">Per-iteration voting records from .loki/council/transcripts/. Polls every 30 seconds.</div>'+a+this._hookEventsHtml()+"</div>"}_hookEventsHtml(){let t=Array.isArray(this._hookEvents)?this._hookEvents:[],e;return t.length===0?e='<div class="ct-empty">No live tool activity yet -- Claude hook events stream here while a run is active.</div>':e='<div class="ct-voters">'+t.slice(0,20).map(i=>{let s=this._escapeHtml(i.type||i.event||"event"),r=this._escapeHtml(this._formatTimestamp(i.timestamp||i.ts)),o=this._escapeHtml(this._truncate(i.tool||i.message||i.summary||(i.data?JSON.stringify(i.data):""),120));return'<div class="ct-voter-row"><span class="ct-iter-label">'+s+'</span> <span class="ct-ts">'+r+"</span>"+(o?'<div class="ct-prd-preview">'+o+"</div>":"")+"</div>"}).join("")+"</div>",'<h3 class="ct-heading" style="margin-top:24px;">Live Tool Activity</h3><div class="ct-explain">Claude hook events (PreToolUse / PostToolUse / Stop) streamed from .loki/events.jsonl. Lets you watch background tool calls as they run.</div>'+e}};typeof customElements<"u"&&!customElements.get("loki-council-transcripts")&&customElements.define("loki-council-transcripts",yt);var He="1.4.0";function Re(d={}){return d.theme?_.setTheme(d.theme):d.autoDetectContext!==!1?_.init():R.init(),d.apiUrl&&g({baseUrl:d.apiUrl}),{theme:_.getTheme(),context:_.detectContext()}}return le(Be);})();
13368
13368
 
13369
13369
 
13370
13370
  // Initialize dashboard when DOM is ready
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v7.7.24
5
+ **Version:** v7.7.26
6
6
 
7
7
  ---
8
8
 
@@ -447,7 +447,7 @@ cat ~/.claude/skills/loki-mode/SKILL.md | head -10
447
447
  ```yaml
448
448
  ---
449
449
  name: loki-mode
450
- description: Multi-Agent Autonomous Startup System
450
+ description: Autonomous Spec-to-Product System (RARV-C closure loop)
451
451
  ...
452
452
  ---
453
453
  ```
@@ -107,7 +107,7 @@ Loki Mode covers:
107
107
 
108
108
  **Auto-Claude:** Engineering only. No business/marketing agents.
109
109
 
110
- **Verdict: Loki Mode wins** - Complete startup automation vs coding only.
110
+ **Verdict: Loki Mode wins** - Full spec-to-product lifecycle vs coding only.
111
111
 
112
112
  ### 4. Anti-Sycophancy Measures
113
113
  Loki Mode implements CONSENSAGENT (ACL 2025):
@@ -247,7 +247,7 @@ Loki Mode now incorporates proven patterns from Cursor's large-scale agent deplo
247
247
 
248
248
  **Loki Mode is better if you want:**
249
249
  - Research-backed architecture
250
- - Full startup automation (not just coding)
250
+ - Full spec-to-product lifecycle (not just coding)
251
251
  - 41 specialized agents
252
252
  - Anti-sycophancy measures
253
253
  - MIT license
@@ -261,7 +261,7 @@ Auto-Claude has better UX and community. Loki Mode has better architecture and c
261
261
  Auto-Claude is a polished product. Loki Mode is a research-backed system.
262
262
 
263
263
  For pure coding tasks with GUI preference: **Auto-Claude wins**.
264
- For full autonomous startup building with quality guarantees: **Loki Mode wins**.
264
+ For full autonomous spec-to-product delivery with quality guarantees: **Loki Mode wins**.
265
265
 
266
266
  ---
267
267
 
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- var _7=Object.defineProperty;var I7=(K)=>K;function P7(K,$){this[K]=I7.bind(null,$)}var v=(K,$)=>{for(var Q in $)_7(K,Q,{get:$[Q],enumerable:!0,configurable:!0,set:P7.bind($,Q)})};var R=(K,$)=>()=>(K&&($=K(K=0)),$);var t=import.meta.require;var e1={};v(e1,{lokiDir:()=>P,homeLokiDir:()=>k1,findRepoRootForVersion:()=>N1,REPO_ROOT:()=>p});import{resolve as u,dirname as S1}from"path";import{fileURLToPath as L7}from"url";import{existsSync as J1}from"fs";import{homedir as R7}from"os";function E7(){let K=i1;for(let $=0;$<6;$++){if(J1(u(K,"VERSION"))&&J1(u(K,"autonomy/run.sh")))return K;let Q=S1(K);if(Q===K)break;K=Q}return u(i1,"..","..","..")}function N1(K){let $=K;for(let Q=0;Q<6;Q++){if(J1(u($,"VERSION"))&&J1(u($,"autonomy/run.sh")))return $;let X=S1($);if(X===$)break;$=X}return u(K,"..","..","..")}function P(){return process.env.LOKI_DIR??u(process.cwd(),".loki")}function k1(){return u(R7(),".loki")}var i1,p;var g=R(()=>{i1=S1(L7(import.meta.url));p=E7()});import{readFileSync as F7}from"fs";import{resolve as w7,dirname as x7}from"path";import{fileURLToPath as S7}from"url";function G1(){if(o!==null)return o;let K="7.7.24";if(typeof K==="string"&&K.length>0)return o=K,o;try{let $=x7(S7(import.meta.url)),Q=N1($);o=F7(w7(Q,"VERSION"),"utf-8").trim()}catch{o="unknown"}return o}var o=null;var D1=R(()=>{g()});var $0={};v($0,{runOrThrow:()=>N7,run:()=>k,commandVersion:()=>D7,commandExists:()=>h,ShellError:()=>C1});async function k(K,$={}){let Q=Bun.spawn({cmd:[...K],stdout:"pipe",stderr:"pipe",env:$.env?{...process.env,...$.env}:process.env,cwd:$.cwd}),X,Z;if($.timeoutMs&&$.timeoutMs>0)X=setTimeout(()=>{try{Q.kill("SIGTERM")}catch{}Z=setTimeout(()=>{try{Q.kill("SIGKILL")}catch{}},2000)},$.timeoutMs);try{let[W,z,q]=await Promise.all([new Response(Q.stdout).text(),new Response(Q.stderr).text(),Q.exited]);return{stdout:W,stderr:z,exitCode:q}}finally{if(X)clearTimeout(X);if(Z)clearTimeout(Z)}}async function N7(K,$={}){let Q=await k(K,$);if(Q.exitCode!==0)throw new C1(`command failed (${Q.exitCode}): ${K.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function h(K){let $=k7(K),Q=await k(["sh","-c",`command -v ${$}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function k7(K){if(!/^[A-Za-z0-9._/-]+$/.test(K))throw Error(`refused to shell-escape suspect token: ${K}`);return K}async function D7(K,$="--version"){if(!await h(K))return null;let X=await k([K,$],{timeoutMs:5000});if(X.exitCode!==0)return null;return((X.stdout||X.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var C1;var n=R(()=>{C1=class C1 extends Error{message;exitCode;stdout;stderr;constructor(K,$,Q,X){super(K);this.message=K;this.exitCode=$;this.stdout=Q;this.stderr=X;this.name="ShellError"}}});function c(K){return C7?"":K}var C7,E,b,F,T6,O,D,w,H;var a=R(()=>{C7=(process.env.NO_COLOR??"").length>0;E=c("\x1B[0;31m"),b=c("\x1B[0;32m"),F=c("\x1B[1;33m"),T6=c("\x1B[0;34m"),O=c("\x1B[0;36m"),D=c("\x1B[1m"),w=c("\x1B[2m"),H=c("\x1B[0m")});import{existsSync as c7}from"fs";async function i(){if(X1!==void 0)return X1;let K="/opt/homebrew/bin/python3.12";if(c7(K))return X1=K,K;let $=await h("python3.12");if($)return X1=$,$;let Q=await h("python3");return X1=Q,Q}async function s(K,$={}){let Q=await i();if(!Q)return{stdout:"",stderr:"python3 not found",exitCode:127};return k([Q,"-c",K],$)}var X1;var Z1=R(()=>{n()});var G0={};v(G0,{runStatus:()=>Q5});import{existsSync as N,readFileSync as W1,readdirSync as W0,statSync as H0}from"fs";import{resolve as x,basename as a7}from"path";async function r7(){if(await h("jq"))return!0;return process.stdout.write(`${E}Error: jq is required but not installed.${H}
2
+ var _7=Object.defineProperty;var I7=(K)=>K;function P7(K,$){this[K]=I7.bind(null,$)}var v=(K,$)=>{for(var Q in $)_7(K,Q,{get:$[Q],enumerable:!0,configurable:!0,set:P7.bind($,Q)})};var R=(K,$)=>()=>(K&&($=K(K=0)),$);var t=import.meta.require;var e1={};v(e1,{lokiDir:()=>P,homeLokiDir:()=>k1,findRepoRootForVersion:()=>N1,REPO_ROOT:()=>p});import{resolve as u,dirname as S1}from"path";import{fileURLToPath as L7}from"url";import{existsSync as J1}from"fs";import{homedir as R7}from"os";function E7(){let K=i1;for(let $=0;$<6;$++){if(J1(u(K,"VERSION"))&&J1(u(K,"autonomy/run.sh")))return K;let Q=S1(K);if(Q===K)break;K=Q}return u(i1,"..","..","..")}function N1(K){let $=K;for(let Q=0;Q<6;Q++){if(J1(u($,"VERSION"))&&J1(u($,"autonomy/run.sh")))return $;let X=S1($);if(X===$)break;$=X}return u(K,"..","..","..")}function P(){return process.env.LOKI_DIR??u(process.cwd(),".loki")}function k1(){return u(R7(),".loki")}var i1,p;var g=R(()=>{i1=S1(L7(import.meta.url));p=E7()});import{readFileSync as F7}from"fs";import{resolve as w7,dirname as x7}from"path";import{fileURLToPath as S7}from"url";function G1(){if(o!==null)return o;let K="7.7.26";if(typeof K==="string"&&K.length>0)return o=K,o;try{let $=x7(S7(import.meta.url)),Q=N1($);o=F7(w7(Q,"VERSION"),"utf-8").trim()}catch{o="unknown"}return o}var o=null;var D1=R(()=>{g()});var $0={};v($0,{runOrThrow:()=>N7,run:()=>k,commandVersion:()=>D7,commandExists:()=>h,ShellError:()=>C1});async function k(K,$={}){let Q=Bun.spawn({cmd:[...K],stdout:"pipe",stderr:"pipe",env:$.env?{...process.env,...$.env}:process.env,cwd:$.cwd}),X,Z;if($.timeoutMs&&$.timeoutMs>0)X=setTimeout(()=>{try{Q.kill("SIGTERM")}catch{}Z=setTimeout(()=>{try{Q.kill("SIGKILL")}catch{}},2000)},$.timeoutMs);try{let[W,z,q]=await Promise.all([new Response(Q.stdout).text(),new Response(Q.stderr).text(),Q.exited]);return{stdout:W,stderr:z,exitCode:q}}finally{if(X)clearTimeout(X);if(Z)clearTimeout(Z)}}async function N7(K,$={}){let Q=await k(K,$);if(Q.exitCode!==0)throw new C1(`command failed (${Q.exitCode}): ${K.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function h(K){let $=k7(K),Q=await k(["sh","-c",`command -v ${$}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function k7(K){if(!/^[A-Za-z0-9._/-]+$/.test(K))throw Error(`refused to shell-escape suspect token: ${K}`);return K}async function D7(K,$="--version"){if(!await h(K))return null;let X=await k([K,$],{timeoutMs:5000});if(X.exitCode!==0)return null;return((X.stdout||X.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var C1;var n=R(()=>{C1=class C1 extends Error{message;exitCode;stdout;stderr;constructor(K,$,Q,X){super(K);this.message=K;this.exitCode=$;this.stdout=Q;this.stderr=X;this.name="ShellError"}}});function c(K){return C7?"":K}var C7,E,b,F,T6,O,D,w,H;var a=R(()=>{C7=(process.env.NO_COLOR??"").length>0;E=c("\x1B[0;31m"),b=c("\x1B[0;32m"),F=c("\x1B[1;33m"),T6=c("\x1B[0;34m"),O=c("\x1B[0;36m"),D=c("\x1B[1m"),w=c("\x1B[2m"),H=c("\x1B[0m")});import{existsSync as c7}from"fs";async function i(){if(X1!==void 0)return X1;let K="/opt/homebrew/bin/python3.12";if(c7(K))return X1=K,K;let $=await h("python3.12");if($)return X1=$,$;let Q=await h("python3");return X1=Q,Q}async function s(K,$={}){let Q=await i();if(!Q)return{stdout:"",stderr:"python3 not found",exitCode:127};return k([Q,"-c",K],$)}var X1;var Z1=R(()=>{n()});var G0={};v(G0,{runStatus:()=>Q5});import{existsSync as N,readFileSync as W1,readdirSync as W0,statSync as H0}from"fs";import{resolve as x,basename as a7}from"path";async function r7(){if(await h("jq"))return!0;return process.stdout.write(`${E}Error: jq is required but not installed.${H}
3
3
  `),process.stdout.write(`Install with:
4
4
  `),process.stdout.write(` brew install jq (macOS)
5
5
  `),process.stdout.write(` apt install jq (Debian/Ubuntu)
@@ -585,4 +585,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
585
585
  `),2}default:return process.stderr.write(`Unknown command: ${$}
586
586
  `),process.stderr.write(j7),2}}process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var X6=await Q6(Bun.argv.slice(2));process.exit(X6);
587
587
 
588
- //# debugId=2B5B8BCEF68E54B364756E2164756E21
588
+ //# debugId=CE941072AA747DCC64756E2164756E21
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '7.7.24'
60
+ __version__ = '7.7.26'
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "loki-mode",
3
- "version": "7.7.24",
4
- "description": "Loki Mode by Autonomi. Multi-agent autonomous SDLC framework. Spec to deployed app: PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief. 4 AI providers (Claude Code, OpenAI Codex, Cline, Aider). 11 quality gates.",
3
+ "version": "7.7.26",
4
+ "description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 11 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
5
5
  "keywords": [
6
6
  "agent",
7
7
  "agent-orchestration",
@@ -64,6 +64,7 @@
64
64
  "files": [
65
65
  "SKILL.md",
66
66
  "VERSION",
67
+ "tools/",
67
68
  "autonomy/",
68
69
  "providers/",
69
70
  "agents/",
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env python3
2
+ """v7.7.24: cross-project knowledge "lift" report (the memory moat proof).
3
+
4
+ WHAT THIS MEASURES (honestly):
5
+ Loki's moat claim is that knowledge learned on one project helps a
6
+ DIFFERENT project. The transfer mechanism is real and already in the
7
+ codebase: each project's semantic patterns (.loki/memory/semantic/)
8
+ are extracted into an org-wide knowledge graph
9
+ (memory/knowledge_graph.py -> ~/.loki/knowledge/patterns.jsonl), and
10
+ any other project can query that graph (query_patterns).
11
+
12
+ "Lift" here is a RETRIEVAL-COVERAGE metric, not a task-success metric.
13
+ For a target project's set of task goals we count how many RELEVANT
14
+ patterns are retrievable in two conditions:
15
+ baseline: only the target project's own patterns are in the graph
16
+ cross: the target's patterns PLUS sibling projects' patterns
17
+ Lift = (relevant retrieved in cross) - (relevant retrieved in baseline),
18
+ and net-new = relevant patterns that ONLY the sibling projects could
19
+ supply (the target could never have surfaced them alone).
20
+
21
+ WHAT THIS DOES NOT CLAIM:
22
+ - It does NOT claim downstream task success / fewer iterations / lower
23
+ cost. That requires running real LLM tasks end-to-end, which this
24
+ offline harness does not do. Measuring that is a separate, larger
25
+ benchmark.
26
+ - "Relevant" is keyword-overlap against the goal, not semantic ground
27
+ truth. It is a proxy. The number is a coverage signal, not a
28
+ correctness guarantee.
29
+
30
+ The harness is fully self-contained: it seeds synthetic projects in a
31
+ temp dir, points the knowledge graph at a temp knowledge dir, runs both
32
+ conditions, prints a report, and self-cleans. It never touches a real
33
+ ~/.loki/knowledge or any real .loki/memory.
34
+ """
35
+ from __future__ import annotations
36
+
37
+ import argparse
38
+ import json
39
+ import os
40
+ import shutil
41
+ import sys
42
+ import tempfile
43
+ from pathlib import Path
44
+
45
+ _HERE = os.path.dirname(os.path.abspath(__file__))
46
+ _REPO_ROOT = os.path.dirname(_HERE)
47
+ if _REPO_ROOT not in sys.path:
48
+ sys.path.insert(0, _REPO_ROOT)
49
+
50
+
51
+ # Synthetic patterns per source project. Each is a semantic pattern dict
52
+ # matching what memory/knowledge_graph.py reads (name/category/description).
53
+ SOURCE_PROJECTS = {
54
+ "payments-api": [
55
+ {"name": "idempotency-key-on-charge", "category": "reliability",
56
+ "description": "retry-safe charge endpoints require an idempotency key header"},
57
+ {"name": "stripe-webhook-signature-verify", "category": "security",
58
+ "description": "verify stripe webhook signatures before processing payment events"},
59
+ {"name": "decimal-money-never-float", "category": "correctness",
60
+ "description": "represent money as integer cents or Decimal, never float"},
61
+ ],
62
+ "auth-service": [
63
+ {"name": "jwt-short-ttl-refresh-rotation", "category": "security",
64
+ "description": "access tokens short ttl with rotating refresh tokens"},
65
+ {"name": "rate-limit-login-by-ip-and-account", "category": "security",
66
+ "description": "rate limit login attempts per ip and per account to stop credential stuffing"},
67
+ {"name": "argon2-password-hash", "category": "security",
68
+ "description": "hash passwords with argon2id not bcrypt for new services"},
69
+ ],
70
+ }
71
+
72
+ # Patterns the TARGET project already knows on its own (so they are NOT
73
+ # net-new from siblings).
74
+ TARGET_OWN_PATTERNS = [
75
+ {"name": "openapi-spec-first", "category": "design",
76
+ "description": "write the openapi spec before implementing the api"},
77
+ ]
78
+
79
+ # The target project's task goals. Each goal SHOULD be served by a
80
+ # sibling pattern (that the target lacks). These are the realistic
81
+ # overlaps a new billing+login service would hit.
82
+ TARGET_GOALS = [
83
+ "make the charge endpoint safe to retry",
84
+ "verify incoming payment webhooks are authentic",
85
+ "store monetary amounts without rounding errors",
86
+ "secure login against credential stuffing attacks",
87
+ "choose a password hashing algorithm",
88
+ "design the api contract up front", # served by target's OWN pattern
89
+ ]
90
+
91
+
92
+ def _seed_project(root: Path, name: str, patterns: list) -> None:
93
+ semantic = root / name / ".loki" / "memory" / "semantic"
94
+ semantic.mkdir(parents=True, exist_ok=True)
95
+ for i, p in enumerate(patterns):
96
+ with open(semantic / f"pattern_{i}.json", "w") as f:
97
+ json.dump(p, f)
98
+
99
+
100
+ def _relevant(pattern: dict, goal: str) -> bool:
101
+ """Keyword-overlap relevance proxy: any meaningful token from the
102
+ pattern name/description appears in the goal, or vice versa."""
103
+ stop = {"the", "a", "an", "to", "for", "of", "and", "or", "with",
104
+ "without", "is", "are", "be", "up", "on", "in", "by", "not",
105
+ "make", "choose", "store"}
106
+ def toks(s):
107
+ return {t for t in s.lower().replace("-", " ").split() if t not in stop and len(t) > 2}
108
+ goal_t = toks(goal)
109
+ pat_t = toks(pattern.get("name", "")) | toks(pattern.get("description", ""))
110
+ return len(goal_t & pat_t) >= 2
111
+
112
+
113
+ def _coverage(graph, goals, top_k):
114
+ """For each goal, query the graph and count goals that retrieved at
115
+ least one relevant pattern. Returns (covered_goals, served_by_sibling)."""
116
+ covered = 0
117
+ sibling_served = 0
118
+ details = []
119
+ for goal in goals:
120
+ results = graph.query_patterns(goal, max_results=top_k)
121
+ relevant = [r for r in results if _relevant(r, goal)]
122
+ is_covered = len(relevant) > 0
123
+ # served_by_sibling: at least one relevant result came from a
124
+ # non-target source project.
125
+ from_sibling = any(
126
+ r.get("_source_project", "").rsplit("/", 1)[-1] != "target-billing-login"
127
+ for r in relevant
128
+ )
129
+ if is_covered:
130
+ covered += 1
131
+ if is_covered and from_sibling:
132
+ sibling_served += 1
133
+ details.append({
134
+ "goal": goal,
135
+ "covered": is_covered,
136
+ "relevant_count": len(relevant),
137
+ "served_by_sibling": is_covered and from_sibling,
138
+ })
139
+ return covered, sibling_served, details
140
+
141
+
142
+ def run(top_k: int, as_json: bool) -> int:
143
+ tmp = tempfile.mkdtemp(prefix="loki-xproj-lift-")
144
+ try:
145
+ from memory.knowledge_graph import OrganizationKnowledgeGraph
146
+
147
+ projects_root = Path(tmp) / "git"
148
+ projects_root.mkdir(parents=True)
149
+
150
+ # Seed sibling source projects + the target project.
151
+ for name, pats in SOURCE_PROJECTS.items():
152
+ _seed_project(projects_root, name, pats)
153
+ _seed_project(projects_root, "target-billing-login", TARGET_OWN_PATTERNS)
154
+
155
+ target_dir = projects_root / "target-billing-login"
156
+ sibling_dirs = [projects_root / n for n in SOURCE_PROJECTS]
157
+
158
+ # BASELINE: knowledge graph built from the target alone.
159
+ base_kg = OrganizationKnowledgeGraph(
160
+ knowledge_dir=str(Path(tmp) / "knowledge-baseline"))
161
+ base_pats = base_kg.extract_patterns([target_dir])
162
+ base_kg.save_patterns(base_kg.deduplicate_patterns(base_pats))
163
+ base_covered, base_sibling, base_detail = _coverage(base_kg, TARGET_GOALS, top_k)
164
+
165
+ # CROSS: knowledge graph built from target + siblings.
166
+ cross_kg = OrganizationKnowledgeGraph(
167
+ knowledge_dir=str(Path(tmp) / "knowledge-cross"))
168
+ cross_pats = cross_kg.extract_patterns([target_dir] + sibling_dirs)
169
+ cross_kg.save_patterns(cross_kg.deduplicate_patterns(cross_pats))
170
+ cross_covered, cross_sibling, cross_detail = _coverage(cross_kg, TARGET_GOALS, top_k)
171
+
172
+ n = len(TARGET_GOALS)
173
+ lift = cross_covered - base_covered
174
+ report = {
175
+ "goals": n,
176
+ "baseline_covered": base_covered,
177
+ "cross_covered": cross_covered,
178
+ "lift_absolute": lift,
179
+ "lift_pct_points": round(100.0 * lift / n, 1),
180
+ "net_new_from_siblings": cross_sibling - base_sibling,
181
+ "top_k": top_k,
182
+ "method": "retrieval-coverage (keyword-overlap relevance proxy), NOT task-success",
183
+ "per_goal": cross_detail,
184
+ }
185
+
186
+ if as_json:
187
+ print(json.dumps(report, indent=2))
188
+ else:
189
+ print("Cross-project knowledge LIFT report (memory moat proof)")
190
+ print(f" target goals: {n}")
191
+ print(f" covered (target alone): {base_covered}/{n}")
192
+ print(f" covered (target + siblings): {cross_covered}/{n}")
193
+ print(f" LIFT: +{lift} goals "
194
+ f"(+{report['lift_pct_points']} pts)")
195
+ print(f" net-new served by siblings: {report['net_new_from_siblings']}")
196
+ print(f" method: {report['method']}")
197
+ print(" per-goal:")
198
+ for d in cross_detail:
199
+ tag = "sibling" if d["served_by_sibling"] else ("self" if d["covered"] else "MISS")
200
+ print(f" [{tag:7}] {d['goal']}")
201
+
202
+ # Exit non-zero if there is no measurable lift (so it can gate CI:
203
+ # a regression that breaks cross-project transfer would fail here).
204
+ return 0 if lift > 0 else 1
205
+ finally:
206
+ shutil.rmtree(tmp, ignore_errors=True)
207
+
208
+
209
+ def main():
210
+ ap = argparse.ArgumentParser(description="Cross-project knowledge lift report")
211
+ ap.add_argument("--top-k", type=int, default=5, help="patterns retrieved per goal")
212
+ ap.add_argument("--json", action="store_true", help="emit JSON")
213
+ args = ap.parse_args()
214
+ sys.exit(run(args.top_k, args.json))
215
+
216
+
217
+ if __name__ == "__main__":
218
+ main()