wyrm-mcp 7.11.0 → 8.0.0

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/dist/bridge.js CHANGED
@@ -1 +1 @@
1
- import{readFileSync as y,writeFileSync as O,readdirSync as M,existsSync as _}from"fs";import{createHash as q}from"crypto";import{join as h}from"path";import{homedir as R}from"os";import{MemoryArtifacts as T}from"./memory-artifacts.js";import{recordWrite as g}from"./receipts.js";import{buildRenderPlan as B,renderToDisk as C,renderForClient as F,makeRenderDeps as I}from"./render-target.js";import{makeBridgeDeps as x,sweepProject as E}from"./reverse-bridge.js";function W(){return process.env.WYRM_BRIDGE_CONFIG??h(R(),".wyrm","bridge.json")}function P(){return process.env.WYRM_BRIDGE_STATE??h(R(),".wyrm","bridge-state.json")}function ee(){try{return JSON.parse(y(W(),"utf8"))}catch{return null}}function N(){try{return JSON.parse(y(P(),"utf8"))}catch{return{}}}function D(r){try{O(P(),JSON.stringify(r,null,1))}catch{}}function H(r,n,t){const o=t??h(R(),".wyrm","remember-distill-state.json");if(!_(o))return 0;let d=0;try{const e=JSON.parse(y(o,"utf8"));for(const[c,i]of Object.entries(e)){const s=`${r}:${c}`;n[s]||(n[s]=i,d++)}}catch{}return d}const J=r=>q("sha1").update(r).digest("hex");function L(r,n,t,o,d={}){const e={source:t.name,type:"remember",stored:0,dropped:0,queued:0,rendered:0},c=t.windowDays&&t.windowDays>=1?t.windowDays:7,i=t.maxPerRun&&t.maxPerRun>=1?t.maxPerRun:80,s=r.prepare("SELECT id FROM projects WHERE path = ?").get(t.projectPath);if(!s)return e.note=`no project at ${t.projectPath}`,e;const u=new Date(Date.now()-c*864e5);let p=[];try{p=M(t.dir).filter(a=>/^today-\d{4}-\d{2}-\d{2}(\.done)?\.md$/.test(a)).map(a=>({f:a,date:a.match(/\d{4}-\d{2}-\d{2}/)[0]})).filter(({date:a})=>new Date(a+"T00:00:00")>=u).sort((a,l)=>l.date.localeCompare(a.date))}catch{return e.note=`cannot read ${t.dir}`,e}let m=0;for(const{f:a,date:l}of p){const j=`${t.name}:today-${l}`,w=new Set(o[j]??[]);let S="";try{S=y(h(t.dir,a),"utf8")}catch{continue}const b=S.split(/^## /m).slice(1).map(f=>"## "+f.trim());for(const f of b){if(m>=i)break;if(f.length<30)continue;const $=J(f);if(w.has($))continue;if(d.dryRun){m++,e.stored++;continue}const k=`[bridge:${t.name} ${l}] ${f}`.slice(0,2400),v=n.add(s.id,{kind:"pattern",problem:k,confidence:.75,needsReview:0,createdBy:`bridge:${t.name}`});g(r,{tool:"wyrm_bridge",outcome:"stored",refTable:"mem",refId:v.id,source:`bridge:${t.name}`,projectId:s.id}),w.add($),m++,e.stored++}o[j]=[...w]}return m>=i&&(e.note=`hit per-run cap ${i} \u2014 remainder next run`),e}async function G(r,n,t,o={}){const d={source:n.name,type:"render",stored:0,dropped:0,queued:0,rendered:0},e=r.prepare("SELECT id, name, path FROM projects WHERE path = ?").get(n.projectPath);if(!e)return d.note=`no project at ${n.projectPath}`,d;const c=n.clients??[],i=I(r);try{const s=B(i,e,t),u={"MEMORY.md":s.memoryMd};for(const m of c){const a=F(m,s.model,t);u[a.relPath]=a.block}const p=await E(x(r),{id:e.id,path:e.path},u,{dryRun:o.dryRun});d.queued+=p.added,p.added>0&&g(r,{tool:"wyrm_bridge",outcome:"queued",reason:`render guard \u2014 ${p.added} human edit(s) harvested to review before overwrite`,source:`bridge:${n.name}`,projectId:e.id})}catch{}if(!o.dryRun){const{writes:s}=C(i,e,t,{clients:c});d.rendered=s.filter(u=>u.action!=="skipped").length}return d}async function Y(r,n,t={}){const o={source:n.name,type:"reverse",stored:0,dropped:0,queued:0,rendered:0},d=r.prepare("SELECT id, path FROM projects WHERE path = ?").get(n.projectPath);if(!d)return o.note=`no project at ${n.projectPath}`,o;const e=await E(x(r),d,{},{dryRun:t.dryRun});return o.queued=e.added,o.dropped=e.skipped,e.added>0&&g(r,{tool:"wyrm_bridge",outcome:"queued",reason:`reverse sweep \u2014 ${e.added} native-file edit(s) queued for review`,source:`bridge:${n.name}`,projectId:d.id}),o}async function te(r,n,t,o={}){const d=new T(r),e=N(),c=[];for(const i of n.sources)o.only&&i.name!==o.only||(i.type==="remember"?(H(i.name,e)>0&&D(e),c.push(L(r,d,i,e,o))):i.type==="render"?c.push(await G(r,i,t,o)):c.push(await Y(r,i,o)));return o.dryRun||D(e),c}export{H as adoptLegacyDistillState,W as configPath,ee as loadConfig,N as loadState,te as runBridge,L as runRememberSource,G as runRenderSource,Y as runReverseSource,D as saveState,P as statePath};
1
+ import{readFileSync as h,writeFileSync as D,readdirSync as k,existsSync as F}from"fs";import{createHash as _}from"crypto";import{join as y}from"path";import{homedir as g}from"os";import{MemoryArtifacts as I}from"./memory-artifacts.js";import{recordWrite as R}from"./receipts.js";import{buildRenderPlan as L,renderToDisk as b,renderForClient as q,makeRenderDeps as A}from"./render-target.js";import{makeBridgeDeps as x,sweepProject as $}from"./reverse-bridge.js";function B(){return process.env.WYRM_BRIDGE_CONFIG??y(g(),".wyrm","bridge.json")}function C(){return process.env.WYRM_BRIDGE_STATE??y(g(),".wyrm","bridge-state.json")}function ee(){try{return JSON.parse(h(B(),"utf8"))}catch{return null}}function W(){try{return JSON.parse(h(C(),"utf8"))}catch{return{}}}function v(t){try{D(C(),JSON.stringify(t,null,1))}catch{}}function H(t,i,r){const n=r??y(g(),".wyrm","remember-distill-state.json");if(!F(n))return 0;let c=0;try{const e=JSON.parse(h(n,"utf8"));for(const[d,a]of Object.entries(e)){const s=`${t}:${d}`;i[s]||(i[s]=a,c++)}}catch{}return c}const N=t=>_("sha1").update(t).digest("hex");function J(t,i,r,n,c={}){const e={source:r.name,type:"remember",stored:0,dropped:0,queued:0,rendered:0},d=r.windowDays&&r.windowDays>=1?r.windowDays:7,a=r.maxPerRun&&r.maxPerRun>=1?r.maxPerRun:80,s=t.prepare("SELECT id FROM projects WHERE path = ?").get(r.projectPath);if(!s)return e.note=`no project at ${r.projectPath}`,e;const p=new Date(Date.now()-d*864e5);let u=[];try{u=k(r.dir).filter(o=>/^today-\d{4}-\d{2}-\d{2}(\.done)?\.md$/.test(o)).map(o=>({f:o,date:o.match(/\d{4}-\d{2}-\d{2}/)[0]})).filter(({date:o})=>new Date(o+"T00:00:00")>=p).sort((o,m)=>m.date.localeCompare(o.date))}catch{return e.note=`cannot read ${r.dir}`,e}let f=0;for(const{f:o,date:m}of u){const S=`${r.name}:today-${m}`,w=new Set(n[S]??[]);let E="";try{E=h(y(r.dir,o),"utf8")}catch{continue}const O=E.split(/^## /m).slice(1).map(l=>"## "+l.trim());for(const l of O){if(f>=a)break;if(l.length<30)continue;const j=N(l);if(w.has(j))continue;if(c.dryRun){f++,e.stored++;continue}const P=`[bridge:${r.name} ${m}] ${l}`.slice(0,2400),T=i.add(s.id,{kind:"pattern",problem:P,confidence:.75,needsReview:0,createdBy:`bridge:${r.name}`});R(t,{tool:"wyrm_bridge",outcome:"stored",refTable:"mem",refId:T.id,source:`bridge:${r.name}`,projectId:s.id}),w.add(j),f++,e.stored++}n[S]=[...w]}return f>=a&&(e.note=`hit per-run cap ${a} \u2014 remainder next run`),e}async function M(t,i,r,n={}){const c={source:i.name,type:"render",stored:0,dropped:0,queued:0,rendered:0},e=t.prepare("SELECT id, name, path FROM projects WHERE path = ?").get(i.projectPath);if(!e)return c.note=`no project at ${i.projectPath}`,c;const d=i.clients??[],a=A(t);try{const s=L(a,e,r),p={"MEMORY.md":s.memoryMd};for(const f of d){const o=q(f,s.model,r);p[o.relPath]=o.block}const u=await $(x(t),{id:e.id,path:e.path},p,{dryRun:n.dryRun});c.queued+=u.added,u.added>0&&R(t,{tool:"wyrm_bridge",outcome:"queued",reason:`render guard \u2014 ${u.added} human edit(s) harvested to review before overwrite`,source:`bridge:${i.name}`,projectId:e.id})}catch{}if(!n.dryRun){const{writes:s}=b(a,e,r,{clients:d});c.rendered=s.filter(p=>p.action!=="skipped").length}return c}async function G(t,i,r={}){const n={source:i.name,type:"reverse",stored:0,dropped:0,queued:0,rendered:0},c=t.prepare("SELECT id, path FROM projects WHERE path = ?").get(i.projectPath);if(!c)return n.note=`no project at ${i.projectPath}`,n;const e=await $(x(t),c,{},{dryRun:r.dryRun});return n.queued=e.added,n.dropped=e.skipped,e.added>0&&R(t,{tool:"wyrm_bridge",outcome:"queued",reason:`reverse sweep \u2014 ${e.added} native-file edit(s) queued for review`,source:`bridge:${i.name}`,projectId:c.id}),n}async function te(t,i,r,n={}){const c=new I(t),e=W(),d=[];for(const a of i.sources)n.only&&a.name!==n.only||(a.type==="remember"?(H(a.name,e)>0&&v(e),d.push(J(t,c,a,e,n))):a.type==="render"?d.push(await M(t,a,r,n)):d.push(await G(t,a,n)));return n.dryRun||v(e),d}async function re(t,i,r,n={}){const c=Math.max(2,n.intervalSec??15)*1e3,e=i.sources.filter(p=>p.type==="render");if(e.length===0)return;const d=new Map;for(const p of e){const u=t.prepare("SELECT id FROM projects WHERE path = ?").get(p.projectPath);u&&d.set(p.name,u.id)}let a=n.fromCursor??t.prepare("SELECT COALESCE(MAX(cursor), 0) AS c FROM events").get().c,s=0;for(;n.iterations==null||s<n.iterations;){s++,await new Promise(o=>setTimeout(o,c));const p=t.prepare("SELECT DISTINCT project_id FROM events WHERE cursor > ?").all(a);if(a=t.prepare("SELECT COALESCE(MAX(cursor), 0) AS c FROM events").get().c,p.length===0)continue;const u=new Set(p.map(o=>String(o.project_id))),f=[];for(const o of e){const m=d.get(o.name);if(!(m==null||!u.has(String(m))))try{f.push(await M(t,o,r()))}catch{}}f.length>0&&n.onCycle?.(f)}}export{H as adoptLegacyDistillState,B as configPath,ee as loadConfig,W as loadState,te as runBridge,J as runRememberSource,M as runRenderSource,G as runReverseSource,v as saveState,C as statePath,re as watchBridge};
@@ -1 +1 @@
1
- import{logger as _}from"./logger.js";const o={confidence:.4,recency:.2,relevance:.3,usefulness:.1};function R(e=process.env){const n=e.WYRM_RANK_WEIGHTS;if(!n)return{...o};try{const t=JSON.parse(n),c={confidence:s(t.confidence??o.confidence),recency:s(t.recency??o.recency),relevance:s(t.relevance??o.relevance),usefulness:s(t.usefulness??o.usefulness)},r=c.confidence+c.recency+c.relevance+c.usefulness;return r<=0?{...o}:(c.confidence/=r,c.recency/=r,c.relevance/=r,c.usefulness/=r,c)}catch(t){return _.warn(`Invalid WYRM_RANK_WEIGHTS, using defaults: ${t.message}`),{...o}}}function s(e){return Number.isFinite(e)?Math.max(0,Math.min(1,e)):0}const d=.25,f=30;function N(e=process.env){const n=Number(e.WYRM_RECALL_RECENCY_WEIGHT);return Number.isFinite(n)?s(n):d}function L(e=process.env){const n=Number(e.WYRM_RECALL_RECENCY_HALFLIFE_DAYS);return Number.isFinite(n)&&n>=1?n:f}function T(e,n,t,c=new Date){const r=s(n);if(r===0)return 1;const a=e&&/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/.test(e)?e.replace(" ","T")+"Z":e,u=a?new Date(a).getTime():NaN;if(!Number.isFinite(u))return 1-r;const i=Math.max(0,(c.getTime()-u)/864e5),l=t>=1?t:f;return 1-r+r*Math.pow(2,-i/l)}const p=.5,A=/^\s*\[auto:(precompact|sessionend|sessionstart)/i;function M(e){return e&&A.test(e)?p:0}function m(e,n,t=new Date){const c=s(e.confidence??.5),r=s(e.usefulness??.5),a=s(e.relevance??.5),u=e.updatedAt?Math.max(0,(t.getTime()-new Date(e.updatedAt).getTime())/(1440*60*1e3)):1/0,i=Number.isFinite(u)?Math.exp(-u/30):0,l=c*n.confidence+i*n.recency+a*n.relevance+r*n.usefulness,E=s(e.sourcePenalty??0);return s(l*(1-E))}export{p as AUTO_SUMMARY_PENALTY,f as DEFAULT_RECALL_RECENCY_HALFLIFE_DAYS,d as DEFAULT_RECALL_RECENCY_WEIGHT,o as DEFAULT_WEIGHTS,M as autoSummaryPenalty,R as loadWeightsFromEnv,L as recallRecencyHalfLifeDays,N as recallRecencyWeight,T as recencyMultiplier,m as score};
1
+ import{logger as m}from"./logger.js";const u={confidence:.4,recency:.2,relevance:.3,usefulness:.1};function E(e=process.env){const t=e.WYRM_RANK_WEIGHTS;if(!t)return{...u};try{const n=JSON.parse(t),s={confidence:c(n.confidence??u.confidence),recency:c(n.recency??u.recency),relevance:c(n.relevance??u.relevance),usefulness:c(n.usefulness??u.usefulness)},r=s.confidence+s.recency+s.relevance+s.usefulness;return r<=0?{...u}:(s.confidence/=r,s.recency/=r,s.relevance/=r,s.usefulness/=r,s)}catch(n){return m.warn(`Invalid WYRM_RANK_WEIGHTS, using defaults: ${n.message}`),{...u}}}function c(e){return Number.isFinite(e)?Math.max(0,Math.min(1,e)):0}const p=.25,f=30;function _(e=process.env){const t=Number(e.WYRM_RECALL_RECENCY_WEIGHT);return Number.isFinite(t)?c(t):p}function N(e=process.env){const t=Number(e.WYRM_RECALL_RECENCY_HALFLIFE_DAYS);return Number.isFinite(t)&&t>=1?t:f}const y=[/\bignore\s+(all\s+)?(previous|prior|above|earlier)\s+(instructions?|prompts?|context)\b/i,/\bdisregard\s+(all\s+)?(previous|prior|your)\s+(instructions?|rules|guidelines)\b/i,/\byou\s+(must|are\s+required\s+to)\s+(now\s+)?(obey|comply|execute|run|send|forward|delete)\b/i,/\b(system|assistant)\s*prompt\s*[:=]/i,/\bBEGIN\s+(SYSTEM|ADMIN|OVERRIDE)\b/,/\bnew\s+(system\s+)?instructions?\s*[:=]/i,/\b(reveal|print|exfiltrate|send)\s+(your|the)\s+(api\s*key|token|secret|password|credentials)\b/i,/\bcurl\s+[^\n]{0,120}\|\s*(ba)?sh\b/i,/\bact\s+as\s+(the\s+)?(root|admin|system)\b/i,/\bdo\s+not\s+(tell|inform|alert)\s+the\s+(user|operator|owner)\b/i];function D(e){return e?y.some(t=>t.test(e)):!1}function M(e){return e==="imported"?"\u26A0 [imported] ":e==="untrusted"?"\u26A0 [untrusted] ":""}function T(e,t=new Date){const n=e.toLowerCase();if(/\btoday\b/.test(n))return{fromDaysAgo:1.5,toDaysAgo:0};if(/\byesterday\b/.test(n))return{fromDaysAgo:2.5,toDaysAgo:.5};if(/\b(this|past)\s+week\b/.test(n))return{fromDaysAgo:7,toDaysAgo:0};if(/\blast\s+week\b/.test(n))return{fromDaysAgo:14,toDaysAgo:5};const s=n.match(/\blast\s+(\d{1,3})\s+days?\b/);if(s)return{fromDaysAgo:Number(s[1])+.5,toDaysAgo:0};const r=n.match(/\b(\d{1,3})\s+days?\s+ago\b/);if(r){const o=Number(r[1]);return{fromDaysAgo:o+1.5,toDaysAgo:Math.max(0,o-1.5)}}const a=n.match(/\b(\d{4})-(\d{2})-(\d{2})\b/);if(a){const o=new Date(`${a[1]}-${a[2]}-${a[3]}T12:00:00Z`).getTime();if(Number.isFinite(o)){const i=(t.getTime()-o)/864e5;return{fromDaysAgo:i+1,toDaysAgo:Math.max(0,i-1)}}}return null}function x(e=process.env){const t=Number(e.WYRM_RECALL_TEMPORAL_WEIGHT);return Number.isFinite(t)?c(t):.5}function R(e,t,n,s=new Date){const r=c(n);if(r===0)return 1;const a=e&&/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/.test(e)?e.replace(" ","T")+"Z":e,o=a?new Date(a).getTime():NaN;if(!Number.isFinite(o))return 1-r;const i=(s.getTime()-o)/864e5;return i<=t.fromDaysAgo&&i>=t.toDaysAgo?1:1-r}function h(e=process.env){const t=Number(e.WYRM_RECALL_USEFULNESS_WEIGHT);return Number.isFinite(t)?Math.max(0,Math.min(.5,t)):.15}function w(e,t,n){const s=Math.max(0,Math.min(.5,n));if(s===0||!e||e<=0)return 1;const r=Math.max(0,Math.min(1,(t??0)/e));return 1+s*r*Math.min(1,e/3)}function L(e,t,n,s=new Date){const r=c(t);if(r===0)return 1;const a=e&&/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/.test(e)?e.replace(" ","T")+"Z":e,o=a?new Date(a).getTime():NaN;if(!Number.isFinite(o))return 1-r;const i=Math.max(0,(s.getTime()-o)/864e5),l=n>=1?n:f;return 1-r+r*Math.pow(2,-i/l)}const b=.5,A=/^\s*\[auto:(precompact|sessionend|sessionstart)/i;function v(e){return e&&A.test(e)?b:0}function F(e,t,n=new Date){const s=c(e.confidence??.5),r=c(e.usefulness??.5),a=c(e.relevance??.5),o=e.updatedAt?Math.max(0,(n.getTime()-new Date(e.updatedAt).getTime())/(1440*60*1e3)):1/0,i=Number.isFinite(o)?Math.exp(-o/30):0,l=s*t.confidence+i*t.recency+a*t.relevance+r*t.usefulness,d=c(e.sourcePenalty??0);return c(l*(1-d))}export{b as AUTO_SUMMARY_PENALTY,f as DEFAULT_RECALL_RECENCY_HALFLIFE_DAYS,p as DEFAULT_RECALL_RECENCY_WEIGHT,u as DEFAULT_WEIGHTS,v as autoSummaryPenalty,T as extractTemporalWindow,E as loadWeightsFromEnv,D as looksInjectionShaped,N as recallRecencyHalfLifeDays,_ as recallRecencyWeight,x as recallTemporalWeight,h as recallUsefulnessWeight,L as recencyMultiplier,F as score,R as temporalMultiplier,M as trustMarker,w as usefulnessMultiplier};
@@ -0,0 +1,5 @@
1
+ function w(a=process.env){const t=Number(a.WYRM_RECALL_GRAPH_WEIGHT);return Number.isFinite(t)?Math.max(0,Math.min(.5,t)):.15}function y(a,t,h){const c=new Set(h.toLowerCase().split(/[^a-z0-9]+/).filter(e=>e.length>=3));if(c.size===0)return null;const r=a.prepare("SELECT id, name FROM entities WHERE project_id = ? LIMIT 500").all(t);if(r.length===0)return null;const l=new Set(r.map(e=>e.id));let s=[];try{s=a.prepare("SELECT ea.entity_id AS entity_id, ea.alias AS alias FROM entity_aliases ea JOIN entities e ON e.id = ea.entity_id WHERE e.project_id = ? LIMIT 1000").all(t).filter(e=>l.has(e.entity_id))}catch{}const d=new Map(r.map(e=>[e.id,e.name.toLowerCase()])),n=new Map,u=e=>{const i=e.toLowerCase().split(/[^a-z0-9]+/).filter(o=>o.length>=3);return i.length>0&&i.every(o=>c.has(o))};for(const e of r){if(n.size>=5)break;u(e.name)&&n.set(e.id,e.name.toLowerCase())}for(const e of s){if(n.size>=5)break;if(!n.has(e.entity_id)&&u(e.alias)){const i=d.get(e.entity_id);i&&n.set(e.entity_id,i)}}if(n.size===0)return null;const f=[...n.keys()],m=f.map(()=>"?").join(","),M=a.prepare(`
2
+ SELECT source_entity_id AS s, target_entity_id AS t, MAX(weight) AS w FROM relationships
3
+ WHERE project_id = ? AND (source_entity_id IN (${m}) OR target_entity_id IN (${m}))
4
+ GROUP BY s, t LIMIT 50
5
+ `).all(t,...f,...f),p=new Map;for(const e of M){const i=n.has(e.s)?e.t:e.s;if(n.has(i))continue;const o=d.get(i);if(!o)continue;const _=Math.max(0,Math.min(1,e.w??1));(p.get(o)??0)<_&&p.set(o,_)}const g=[...p.entries()].slice(0,10).map(([e,i])=>({name:e,weight:i}));return{direct:[...n.values()],neighbors:g}}function E(a,t,h){const c=Math.max(0,Math.min(.5,h));if(!t||c===0)return 1;const r=a.toLowerCase();for(const s of t.direct)if(s.length>=3&&r.includes(s))return 1+c;let l=0;for(const s of t.neighbors)s.name.length>=3&&r.includes(s.name)&&(l=Math.max(l,s.weight));return l>0?1+c/2*l:1}export{E as graphMultiplier,y as queryGraphContext,w as recallGraphWeight};
@@ -1,78 +1,78 @@
1
- import{join as B}from"path";import{createHash as K}from"crypto";import{TOOL_ANNOTATIONS as C}from"../tool-annotations.js";import{renderResult as S,withGlyph as E,renderErrorResponse as R,validationErrorBody as A}from"../render.js";import{ValidationError as J,asEnum as Y}from"../validate.js";import{daemonOr as L}from"../daemon-writer.js";import{classifyCapture as Q}from"../capture.js";import{recordWrite as D}from"../receipts.js";import{extractCandidates as O,candidateToArtifact as F,escapeLikePattern as M}from"../auto-capture.js";import{parseTrace as X,segmentsToText as G}from"../trace-harvest.js";import{getActor as V,runWithActor as z}from"./boundary.js";import{harvestProjects as Z}from"../harvest.js";import{readFileSync as N,readdirSync as ee,statSync as W}from"fs";import{join as U}from"path";import{homedir as te}from"os";import{readSpecDir as re,specTaskSignature as ie}from"../spec-kit.js";const ve=[{name:"wyrm_capture",description:"Use to save anything worth keeping - the single write funnel. Auto-classifies notes, decisions, and lessons into quest, ground truth, or memory; risky writes queue for wyrm_review. mode=direct (store a proven pattern or lesson learned so future agents reuse it), extract (pull memories from a conversation transcript or freeform text), trace (harvest a session JSONL / tool-call log into run-tagged review candidates), session (close out a work session into reusable knowledge), artifacts (harvest a repo's READMEs and recent commits into durable facts), import source=git|pr|rules, spec (turn a tasks.md into tracked work items), checklist. Keywords: remember this, jot down, write down, future reference.",inputSchema:{type:"object",properties:{content:{type:"string",description:"classify/rules"},project_id:{type:"number",description:"classify/import/spec"},tags:{type:"array",items:{type:"string"}},mode:{type:"string",enum:["auto","quest","truth","memory","classify","direct","extract","trace","session","artifacts","import","spec","checklist"],description:"quest|truth|memory force the type"},source:{type:"string",enum:["git","pr","rules"],description:"import"},projectPath:{type:"string",description:"non-classify"},kind:{type:"string",description:"direct"},problem:{type:"string",description:"direct"},validatedFix:{type:"string",description:"direct"},text:{type:"string",description:"extract"},sessionId:{type:"string",description:"session"},candidates:{type:"array",items:{type:"object"},description:"session"},dryRun:{type:"boolean",description:"artifacts"},commits:{type:"array",items:{type:"object"},description:"import git"},title:{type:"string",description:"import pr"},body:{type:"string",description:"import pr"},specDir:{type:"string",description:"spec"},problemType:{type:"string",description:"checklist"},whenToUse:{type:"string",description:"checklist"},steps:{type:"array",items:{type:"string"},description:"checklist"}},required:[]},outputSchema:{type:"object",properties:{status:{type:"string",enum:["captured","queued_for_review"]},type:{type:"string"},subtype:{type:"string"},confidence:{type:"number"},reasoning:{type:"string"},id:{type:"integer"},ref:{type:"string"},needs_review:{type:"boolean"},reason:{type:"string"},artifact_id:{type:"integer"},conflicts_with:{type:"array"},advisory_conflicts:{type:"array"}},required:["status"]},annotations:C.wyrm_capture,aliases:[],handler:async(k,{store:_,raw:$,memory:y,truths:m,cache:w})=>{const{content:a,project_id:f,projectPath:d,tags:l,mode:u}=k;if(typeof a!="string"||!a.trim()){const o=k;return o.problem||o.validated_fix||o.validatedFix||o.kind?R(A("wyrm_capture","mode","you passed pattern/lesson fields (kind/problem/validated_fix) \u2014 set mode:'direct' to store them as a distilled artifact.")):R(A("wyrm_capture","content","content is required for auto capture (or use mode:'direct' with problem + validated_fix)."))}let b=Q(a);u&&u!=="auto"&&(b={type:u,subtype:{quest:"quest",truth:"decision",memory:"pattern"}[u]??u,confidence:100,reasoning:`Mode override: ${u}`});const{type:i,subtype:t,confidence:g,reasoning:r}=b,e=f??(d?_.getProject(d)?.id??null:null);let c=0,n="",v=!1,s;if(i==="quest"){if(e===null)return R(A("wyrm_capture","projectPath","is required for quest capture"));c=(await L("quest_add",e,{title:a.slice(0,200),description:"",priority:"medium",tags:l?.join(",")},()=>_.addQuest(e,a.slice(0,200),"","medium",l?.join(",")))).id,n="quest",w.invalidate("wyrm_all_quests"),w.invalidate("wyrm_stats")}else if(i==="truth"){if(e===null)return R(A("wyrm_capture","projectPath","is required for truth capture"));if(g>=80){const o=t??"other",I=m.getCurrent(e).filter(p=>p.category===o);if(I.length>0){const p={kind:"pattern",problem:`Potential conflict with ${I.length} existing truth(s) in category "${o}"`,validatedFix:a,whyItWorked:"Pending review \u2014 possible supersession",tags:l??[],confidence:g/100,needsReview:1},j=await L("artifact_add",e,p,()=>y.add(e,p)),q=D($(),{tool:"wyrm_capture",outcome:"queued",refTable:"mem",refId:j.id,reason:`conflict_check \u2014 possible supersession of ${I.length} truth(s) in "${o}"; invisible to recall until wyrm_review`,projectId:e});return S({status:"queued_for_review",reason:"conflict_check",artifact_id:j.id,conflicts_with:I.map(T=>({id:T.id,content:T.value.slice(0,80)})),receipt:q})}}if(u!=="truth"&&g<100){const o={kind:"pattern",problem:a,validatedFix:"",whyItWorked:"",tags:l??[],confidence:g/100,needsReview:1};c=(await L("artifact_add",e,o,()=>y.add(e,o))).id,n="mem",v=!0}else{const o={category:"decision",key:a.slice(0,60),value:a};c=(await L("truth_set",e,o,()=>m.set(e,o))).id,n="truth",w.invalidate("wyrm_truth_get"),w.invalidate("wyrm_context_build")}}else{if(e===null)return R(A("wyrm_capture","projectPath","is required for memory capture"));const o=g>=75,x={kind:t,problem:a,validatedFix:"",whyItWorked:"",tags:l??[],confidence:g/100,needsReview:o?0:1};if(c=(await L("artifact_add",e,x,()=>y.add(e,x))).id,n="mem",v=!o,t==="heuristic"&&g>=75&&e!==null)try{const p=a.replace(/['"*]/g," ").trim().split(/\s+/).slice(0,5).join(" "),j=$().prepare(`
1
+ import{join as K}from"path";import{createHash as O}from"crypto";import{TOOL_ANNOTATIONS as D}from"../tool-annotations.js";import{renderResult as S,withGlyph as E,renderErrorResponse as R,validationErrorBody as C}from"../render.js";import{ValidationError as J,asEnum as Y}from"../validate.js";import{daemonOr as L}from"../daemon-writer.js";import{classifyCapture as Q}from"../capture.js";import{recordWrite as A}from"../receipts.js";import{extractCandidates as F,candidateToArtifact as N,escapeLikePattern as M}from"../auto-capture.js";import{parseTrace as X,segmentsToText as G}from"../trace-harvest.js";import{getActor as V,runWithActor as z}from"./boundary.js";import{harvestProjects as Z}from"../harvest.js";import{readFileSync as U,readdirSync as ee,statSync as W}from"fs";import{join as H}from"path";import{homedir as te}from"os";import{readSpecDir as re,specTaskSignature as ie}from"../spec-kit.js";const ve=[{name:"wyrm_capture",description:"Use to save anything worth keeping - the single write funnel. Auto-classifies notes, decisions, and lessons into quest, ground truth, or memory; risky writes queue for wyrm_review. mode=direct (store a proven pattern or lesson learned so future agents reuse it), extract (pull memories from a conversation transcript or freeform text), trace (harvest a session JSONL / tool-call log into run-tagged review candidates), session (close out a work session into reusable knowledge), artifacts (harvest a repo's READMEs and recent commits into durable facts), import source=git|pr|rules, spec (turn a tasks.md into tracked work items), checklist. Keywords: remember this, jot down, write down, future reference.",inputSchema:{type:"object",properties:{content:{type:"string",description:"classify/rules"},project_id:{type:"number",description:"classify/import/spec"},tags:{type:"array",items:{type:"string"}},mode:{type:"string",enum:["auto","quest","truth","memory","classify","direct","extract","trace","session","artifacts","import","spec","checklist"],description:"quest|truth|memory force the type"},source:{type:"string",enum:["git","pr","rules"],description:"import"},projectPath:{type:"string",description:"non-classify"},kind:{type:"string",description:"direct"},problem:{type:"string",description:"direct"},validatedFix:{type:"string",description:"direct"},text:{type:"string",description:"extract"},sessionId:{type:"string",description:"session"},candidates:{type:"array",items:{type:"object"},description:"session"},dryRun:{type:"boolean",description:"artifacts"},commits:{type:"array",items:{type:"object"},description:"import git"},title:{type:"string",description:"import pr"},body:{type:"string",description:"import pr"},specDir:{type:"string",description:"spec"},problemType:{type:"string",description:"checklist"},whenToUse:{type:"string",description:"checklist"},steps:{type:"array",items:{type:"string"},description:"checklist"}},required:[]},outputSchema:{type:"object",properties:{status:{type:"string",enum:["captured","queued_for_review"]},type:{type:"string"},subtype:{type:"string"},confidence:{type:"number"},reasoning:{type:"string"},id:{type:"integer"},ref:{type:"string"},needs_review:{type:"boolean"},reason:{type:"string"},artifact_id:{type:"integer"},conflicts_with:{type:"array"},advisory_conflicts:{type:"array"}},required:["status"]},annotations:D.wyrm_capture,aliases:[],handler:async($,{store:b,raw:x,memory:w,truths:l,cache:h})=>{const{content:a,project_id:g,projectPath:n,tags:f,mode:p}=$;if(typeof a!="string"||!a.trim()){const r=$;return r.problem||r.validated_fix||r.validatedFix||r.kind?R(C("wyrm_capture","mode","you passed pattern/lesson fields (kind/problem/validated_fix) \u2014 set mode:'direct' to store them as a distilled artifact.")):R(C("wyrm_capture","content","content is required for auto capture (or use mode:'direct' with problem + validated_fix)."))}let v=Q(a);p&&p!=="auto"&&(v={type:p,subtype:{quest:"quest",truth:"decision",memory:"pattern"}[p]??p,confidence:100,reasoning:`Mode override: ${p}`});const{type:d,subtype:s,confidence:u,reasoning:o}=v,e=g??(n?b.getProject(n)?.id??null:null);let i=0,y="",j=!1,t;if(d==="quest"){if(e===null)return R(C("wyrm_capture","projectPath","is required for quest capture"));i=(await L("quest_add",e,{title:a.slice(0,200),description:"",priority:"medium",tags:f?.join(",")},()=>b.addQuest(e,a.slice(0,200),"","medium",f?.join(",")))).id,y="quest",h.invalidate("wyrm_all_quests"),h.invalidate("wyrm_stats")}else if(d==="truth"){if(e===null)return R(C("wyrm_capture","projectPath","is required for truth capture"));if(u>=80){const r=s??"other",P=l.getCurrent(e).filter(c=>c.category===r);if(P.length>0){const c={kind:"pattern",problem:`Potential conflict with ${P.length} existing truth(s) in category "${r}"`,validatedFix:a,whyItWorked:"Pending review \u2014 possible supersession",tags:f??[],confidence:u/100,needsReview:1},k=await L("artifact_add",e,c,()=>w.add(e,c)),q=A(x(),{tool:"wyrm_capture",outcome:"queued",refTable:"mem",refId:k.id,reason:`conflict_check \u2014 possible supersession of ${P.length} truth(s); review to activate`,projectId:e});return S({status:"queued_for_review",reason:"conflict_check",artifact_id:k.id,conflicts_with:P.map(T=>({id:T.id,content:T.value.slice(0,80)})),receipt:q})}}if(p!=="truth"&&u<100){const r={kind:"pattern",problem:a,validatedFix:"",whyItWorked:"",tags:f??[],confidence:u/100,needsReview:1};i=(await L("artifact_add",e,r,()=>w.add(e,r))).id,y="mem",j=!0}else{const r={category:"decision",key:a.slice(0,60),value:a};i=(await L("truth_set",e,r,()=>l.set(e,r))).id,y="truth",h.invalidate("wyrm_truth_get"),h.invalidate("wyrm_context_build")}}else{if(e===null)return R(C("wyrm_capture","projectPath","is required for memory capture"));const r=u>=75,I={kind:s,problem:a,validatedFix:"",whyItWorked:"",tags:f??[],confidence:u/100,needsReview:r?0:1};if(i=(await L("artifact_add",e,I,()=>w.add(e,I))).id,y="mem",j=!r,s==="heuristic"&&u>=75&&e!==null)try{const c=a.replace(/['"*]/g," ").trim().split(/\s+/).slice(0,5).join(" "),k=x().prepare(`
2
2
  SELECT a.id, a.problem FROM memory_artifacts a
3
3
  JOIN memory_artifacts_fts ON a.id = memory_artifacts_fts.rowid
4
4
  WHERE memory_artifacts_fts MATCH ? AND a.project_id = ? AND a.kind = 'heuristic' AND a.needs_review = 0
5
5
  LIMIT 3
6
- `).all(p,e);j.length>0&&(s=j.map(q=>({id:q.id,content:q.problem.slice(0,80)})))}catch{}}const h=D($(),{tool:"wyrm_capture",outcome:v?"queued":"stored",refTable:n,refId:c,reason:v?`needs_review (classifier confidence ${g}%) \u2014 invisible to recall until wyrm_review`:void 0,projectId:e??void 0}),P={status:"captured",type:i,subtype:t,confidence:g,reasoning:r,id:c,ref:`${n}:${c}`,needs_review:v,receipt:h,...s&&s.length>0?{advisory_conflicts:s}:{}};return S(P,(o,x)=>{let I=`Captured as ${o.type}: ${o.subtype}
7
- Confidence: ${o.confidence}% | ${o.reasoning}
8
- ID: ${o.ref}`;o.needs_review&&(I+=`
9
- `+E(x.warn,"Stored for review -- run `wyrm_review` to activate"));const p=o.advisory_conflicts;return p&&p.length>0&&(I+=`
10
-
11
- `+E(x.warn,`Advisory: ${p.length} similar heuristic(s) found:`)+`
12
- `+p.map(j=>` ${x.bullet} [${j.id}] ${j.content}`).join(`
13
- `)),I})}},{name:"wyrm_remember",description:"Store a distilled knowledge artifact \u2014 proven patterns, lessons learned, anti-patterns, and reasoning traces. These are recalled automatically by wyrm_context_build to help AI models apply past knowledge to new tasks. Example: wyrm_remember({ projectPath: '/home/user/api', kind: 'anti_pattern', problem: 'Jest suite hung after adding the SSE server', validatedFix: 'close() the server in afterAll \u2014 its keep-alive timer held the event loop open', whyItWorked: 'open handles block jest exit', outcome: 'positive', tags: ['jest', 'sse'] })",inputSchema:{type:"object",properties:{projectPath:{type:"string",description:"Project this knowledge belongs to"},kind:{type:"string",enum:["reasoning_trace","lesson","pattern","anti_pattern","heuristic"],description:"reasoning_trace: a solved problem with steps; lesson: a general insight; pattern: a proven approach; anti_pattern: what NOT to do; heuristic: a rule of thumb"},problem:{type:"string",description:"What was being solved or observed (be specific and searchable)"},constraints:{type:"string",description:"What conditions or constraints applied"},validatedFix:{type:"string",description:"What actually worked \u2014 the validated solution or approach"},whyItWorked:{type:"string",description:"The insight behind WHY this worked (key for transfer learning)"},outcome:{type:"string",enum:["positive","negative","neutral"],description:"Was this approach successful?"},tags:{type:"array",items:{type:"string"},description:"Searchable tags (e.g. ['auth', 'rate-limit', 'typescript'])"},confidence:{type:"number",description:"Confidence level 0.0\u20131.0 (default: 1.0)"},sourceSessionId:{type:"number",description:"Session ID where this was discovered (optional)"}},required:["projectPath","kind","problem"]},outputSchema:{type:"object",properties:{id:{type:"integer"},kind:{type:"string",enum:["reasoning_trace","lesson","pattern","anti_pattern","heuristic"]},problem:{type:"string"},validated_fix:{type:["string","null"]},why_it_worked:{type:["string","null"]},confidence:{type:"number"},tags:{type:["string","null"]}},required:["id","kind","problem","validated_fix","why_it_worked","confidence","tags"]},annotations:C.wyrm_remember,aliases:[],handler:async(k,{store:_,memory:$,raw:y})=>{const{projectPath:m,kind:w,problem:a,constraints:f,outcome:d,tags:l,confidence:u,sourceSessionId:b}=k,i=k,t=i.validatedFix??i.validated_fix,g=i.whyItWorked??i.why_it_worked,r=Y("kind",w,["reasoning_trace","lesson","pattern","anti_pattern","heuristic"]);if(r===void 0)throw new J("kind","is required");const e=_.getProject(m);if(!e)return R(A("wyrm_remember","projectPath","project not found"));const c={kind:r,problem:a,constraints:f,validatedFix:t,whyItWorked:g,outcome:d,tags:l,confidence:u,sourceSessionId:b},n=await L("artifact_add",e.id,c,()=>$.add(e.id,c)),v=D(y(),{tool:"wyrm_remember",outcome:n.needs_review===1?"queued":"stored",refTable:"mem",refId:n.id,reason:n.needs_review===1?"needs_review \u2014 invisible to recall until approved (wyrm_review)":void 0,source:n.created_by,projectId:e.id}),s={id:n.id,kind:n.kind,problem:n.problem,validated_fix:n.validated_fix??null,why_it_worked:n.why_it_worked??null,confidence:n.confidence,tags:n.tags??null,receipt:v};return S(s,(h,P)=>{let o=E(P.brand,"**Memory Stored**")+`
14
-
15
- `;return o+=`${P.bullet} **Kind:** ${h.kind}
16
- `,o+=`${P.bullet} **ID:** ${h.id}
17
- `,o+=`${P.bullet} **Problem:** ${h.problem}
18
- `,h.validated_fix&&(o+=`${P.bullet} **Solution:** ${h.validated_fix}
19
- `),h.why_it_worked&&(o+=`${P.bullet} **Why it worked:** ${h.why_it_worked}
20
- `),o+=`${P.bullet} **Confidence:** ${(h.confidence*100).toFixed(0)}%
21
- `,h.tags&&(o+=`${P.bullet} **Tags:** ${h.tags}
22
- `),o+="\n_Use `wyrm_recall` to retrieve similar memories, or `wyrm_context_build` to assemble a task brief._",o})}},{name:"wyrm_auto_capture",description:"Auto-extract durable memories from freeform text (a session transcript, notes, a chat) into the REVIEW QUEUE. A LOCAL extractor \u2014 a configurable Ollama model (WYRM_EXTRACT_MODEL / the `model` arg) if set, else a deterministic heuristic \u2014 pulls out truths, failures, decisions, patterns, and lessons as needs_review candidates; approve/reject with wyrm_review (nothing auto-trusted). Closes the 'just talk, it remembers' gap without a cloud LLM. Deduped + idempotent. Example: wyrm_auto_capture({ projectPath: '/home/user/api', text: 'Deploy failed twice because wrangler.toml pointed at the old KV id; switched to the new binding and decided to pin binding names in CI.' })",inputSchema:{type:"object",properties:{projectPath:{type:"string",description:"Project to file the extracted candidates under"},text:{type:"string",description:"Freeform text to extract durable memories from"},model:{type:"string",description:"Optional Ollama model for extraction (overrides WYRM_EXTRACT_MODEL); omit for deterministic extraction"}},required:["projectPath","text"]},outputSchema:{type:"object",properties:{method:{type:"string"},model:{type:["string","null"]},added:{type:"integer"},skipped:{type:"integer"},candidates:{type:"array",items:{type:"object",properties:{kind:{type:"string"},text:{type:"string"}},required:["kind","text"]}}},required:["method","model","added","skipped","candidates"]},annotations:C.wyrm_auto_capture,aliases:[],handler:async(k,{store:_,raw:$,memory:y,cache:m})=>{const{projectPath:w,text:a,model:f}=k,d=_.getProject(w);if(!d)return R(A("wyrm_auto_capture","projectPath","project not found"));if(!a||a.trim().length<20)return R(A("wyrm_auto_capture","text","must be at least 20 characters"));const{candidates:l,method:u,model:b}=await O(a,{model:f}),i=$();let t=0,g=0;for(const e of l){const c=F(e),n=c.tags[c.tags.length-1];if(i.prepare("SELECT 1 FROM memory_artifacts WHERE project_id = ? AND tags LIKE ? ESCAPE '\\' LIMIT 1").get(d.id,"%"+M(n)+"%")){g++,D(i,{tool:"wyrm_auto_capture",outcome:"dropped",reason:"dedup \u2014 identical extraction signature already present",source:"auto-extract",projectId:d.id});continue}const v=y.add(d.id,{kind:c.kind,problem:c.problem,tags:c.tags,confidence:c.confidence,needsReview:1,createdBy:"auto-extract"});D(i,{tool:"wyrm_auto_capture",outcome:"queued",refTable:"mem",refId:v.id,reason:"needs_review \u2014 auto-extracted candidate; invisible to recall until wyrm_review",source:"auto-extract",projectId:d.id}),t++}t>0&&m.invalidate();const r={method:u,model:b??null,added:t,skipped:g,receipt:{outcome:t>0?"queued":"dropped",reason:`${t} queued for review, ${g} dropped as duplicates`},candidates:l.slice(0,8).map(e=>({kind:e.kind,text:e.text.slice(0,80)}))};return S(r,(e,c)=>{const n=e.candidates.map(v=>` ${c.bullet} [${v.kind}] ${v.text}`).join(`
23
- `);return E(c.brand,`Auto-capture (${e.method}${e.model?` - ${e.model}`:""}) -- ${e.added} candidate(s) -> review queue, ${e.skipped} already present.`)+`
24
- ${n}
25
-
26
- Approve/reject with wyrm_review.`})}},{name:"wyrm_capture_trace",description:"Harvest a harness's working trace into run-tagged review-queue candidates \u2014 OFFLINE. Ingests a Claude Code session transcript (JSONL), a ~/.dragon/traces file/dir, or WYRM_TRACE_TOOL_CALLS event output. The SAME local extractor as auto-capture (Ollama WYRM_EXTRACT_MODEL if set, else deterministic \u2014 never a cloud LLM) pulls durable truths/failures/decisions/patterns/lessons; secrets (API keys, tokens, passwords) are REDACTED before anything is stored; candidates land needs_review=1 stamped with the run_id (arg or ambient envelope). Approve/reject with wyrm_review; deduped + idempotent. Pass `trace` (inline text) OR `path` (a trace file or the ~/.dragon/traces dir). Reached via wyrm_capture({ mode: 'trace' }).",inputSchema:{type:"object",properties:{projectPath:{type:"string",description:"Project to file the candidates under"},trace:{type:"string",description:"Inline trace text (JSONL transcript / dragon records / tool-call events)"},path:{type:"string",description:"Path to a trace file, or the ~/.dragon/traces directory (newest file is read); omit ~ for absolute"},format:{type:"string",enum:["auto","claude-jsonl","dragon","tool-calls"],description:"Trace format (default auto-detect)"},run_id:{type:"string",description:"Tag candidates with this run (default: ambient envelope run_id)"},model:{type:"string",description:"Optional Ollama model for extraction (overrides WYRM_EXTRACT_MODEL); omit for deterministic"}},required:["projectPath"]},outputSchema:{type:"object",properties:{method:{type:"string"},model:{type:["string","null"]},format:{type:"string"},source:{type:"string"},run_id:{type:["string","null"]},segments:{type:"integer"},added:{type:"integer"},skipped:{type:"integer"},candidates:{type:"array",items:{type:"object",properties:{kind:{type:"string"},text:{type:"string"}},required:["kind","text"]}}},required:["method","model","format","source","run_id","segments","added","skipped","candidates"]},annotations:C.wyrm_capture_trace,aliases:[],handler:async(k,{store:_,raw:$,memory:y,cache:m})=>{const{projectPath:w,trace:a,path:f,format:d,run_id:l,model:u}=k,b=_.getProject(w);if(!b)return R(A("wyrm_capture_trace","projectPath","project not found"));const i=V(),t=typeof l=="string"&&l.trim()?l.trim().slice(0,64):i.run_id;let g="",r="inline";if(typeof a=="string"&&a.trim())g=a,r="inline";else if(typeof f=="string"&&f.trim()){let p=f.trim();(p==="~"||p.startsWith("~/"))&&(p=U(te(),p.slice(1)));try{if(W(p).isDirectory()){const q=ee(p).map(T=>U(p,T)).filter(T=>{try{return W(T).isFile()}catch{return!1}}).sort((T,H)=>W(H).mtimeMs-W(T).mtimeMs);if(q.length===0)return R(A("wyrm_capture_trace","path","no trace files in directory"));g=N(q[0],"utf-8"),r=q[0]}else g=N(p,"utf-8"),r=p}catch(j){return R(A("wyrm_capture_trace","path",`could not read trace path: ${j.message}`))}}else return R(A("wyrm_capture_trace","trace","provide inline trace or path to trace file"));const{segments:e,format:c}=X(g,d??"auto"),n=G(e);if(n.trim().length<20){const p={method:"deterministic",model:null,format:c,source:r,run_id:t,segments:e.length,added:0,skipped:0,candidates:[]};return S(p,(j,q)=>E(q.brand,`Trace harvest (${j.format}) -- no durable content extracted from ${j.segments} segment(s).`))}const{candidates:v,method:s,model:h}=await O(n,{model:u}),P=$();let o=0,x=0;for(const p of v){const j=F(p),q=j.tags[j.tags.length-1];if(P.prepare("SELECT 1 FROM memory_artifacts WHERE project_id = ? AND tags LIKE ? ESCAPE '\\' LIMIT 1").get(b.id,"%"+M(q)+"%")){x++;continue}const T=t?[...j.tags,"trace",`run:${t}`]:[...j.tags,"trace"];z({agent_id:i.agent_id,run_id:t,source:t?"param":i.source},()=>y.add(b.id,{kind:j.kind,problem:j.problem,tags:T,confidence:j.confidence,needsReview:1,createdBy:"trace-harvest"})),o++}o>0&&m.invalidate();const I={method:s,model:h??null,format:c,source:r,run_id:t,segments:e.length,added:o,skipped:x,candidates:v.slice(0,8).map(p=>({kind:p.kind,text:p.text.slice(0,80)}))};return S(I,(p,j)=>{const q=p.candidates.map(T=>` ${j.bullet} [${T.kind}] ${T.text}`).join(`
27
- `);return E(j.brand,`Trace harvest (${p.format}, ${p.method}${p.model?` - ${p.model}`:""}) -- ${p.segments} segment(s) -> ${p.added} candidate(s) to review queue, ${p.skipped} already present${p.run_id?` [run:${p.run_id}]`:""}.`)+(q?`
6
+ `).all(c,e);k.length>0&&(t=k.map(q=>({id:q.id,content:q.problem.slice(0,80)})))}catch{}}const _=A(x(),{tool:"wyrm_capture",outcome:j?"queued":"stored",refTable:y,refId:i,reason:j?`needs_review (confidence ${u}%) \u2014 review to activate`:void 0,projectId:e??void 0}),m={status:"captured",type:d,subtype:s,confidence:u,reasoning:o,id:i,ref:`${y}:${i}`,needs_review:j,receipt:_,...t&&t.length>0?{advisory_conflicts:t}:{}};return S(m,(r,I)=>{let P=`Captured as ${r.type}: ${r.subtype}
7
+ Confidence: ${r.confidence}% | ${r.reasoning}
8
+ ID: ${r.ref}`;r.needs_review&&(P+=`
9
+ `+E(I.warn,"Stored for review -- run `wyrm_review` to activate"));const c=r.advisory_conflicts;return c&&c.length>0&&(P+=`
10
+
11
+ `+E(I.warn,`Advisory: ${c.length} similar heuristic(s) found:`)+`
12
+ `+c.map(k=>` ${I.bullet} [${k.id}] ${k.content}`).join(`
13
+ `)),P})}},{name:"wyrm_remember",description:"Store a distilled knowledge artifact \u2014 proven patterns, lessons learned, anti-patterns, and reasoning traces. These are recalled automatically by wyrm_context_build to help AI models apply past knowledge to new tasks. Example: wyrm_remember({ projectPath: '/home/user/api', kind: 'anti_pattern', problem: 'Jest suite hung after adding the SSE server', validatedFix: 'close() the server in afterAll \u2014 its keep-alive timer held the event loop open', whyItWorked: 'open handles block jest exit', outcome: 'positive', tags: ['jest', 'sse'] })",inputSchema:{type:"object",properties:{projectPath:{type:"string",description:"Project this knowledge belongs to"},kind:{type:"string",enum:["reasoning_trace","lesson","pattern","anti_pattern","heuristic"],description:"reasoning_trace: a solved problem with steps; lesson: a general insight; pattern: a proven approach; anti_pattern: what NOT to do; heuristic: a rule of thumb"},problem:{type:"string",description:"What was being solved or observed (be specific and searchable)"},constraints:{type:"string",description:"What conditions or constraints applied"},validatedFix:{type:"string",description:"What actually worked \u2014 the validated solution or approach"},whyItWorked:{type:"string",description:"The insight behind WHY this worked (key for transfer learning)"},outcome:{type:"string",enum:["positive","negative","neutral"],description:"Was this approach successful?"},tags:{type:"array",items:{type:"string"},description:"Searchable tags (e.g. ['auth', 'rate-limit', 'typescript'])"},confidence:{type:"number",description:"Confidence level 0.0\u20131.0 (default: 1.0)"},sourceSessionId:{type:"number",description:"Session ID where this was discovered (optional)"}},required:["projectPath","kind","problem"]},outputSchema:{type:"object",properties:{id:{type:"integer"},kind:{type:"string",enum:["reasoning_trace","lesson","pattern","anti_pattern","heuristic"]},problem:{type:"string"},validated_fix:{type:["string","null"]},why_it_worked:{type:["string","null"]},confidence:{type:"number"},tags:{type:["string","null"]}},required:["id","kind","problem","validated_fix","why_it_worked","confidence","tags"]},annotations:D.wyrm_remember,aliases:[],handler:async($,{store:b,memory:x,raw:w})=>{const{projectPath:l,kind:h,problem:a,constraints:g,outcome:n,tags:f,confidence:p,sourceSessionId:v}=$,d=$,s=d.validatedFix??d.validated_fix,u=d.whyItWorked??d.why_it_worked,o=Y("kind",h,["reasoning_trace","lesson","pattern","anti_pattern","heuristic"]);if(o===void 0)throw new J("kind","is required");const e=b.getProject(l);if(!e)return R(C("wyrm_remember","projectPath","project not found"));const i={kind:o,problem:a,constraints:g,validatedFix:s,whyItWorked:u,outcome:n,tags:f,confidence:p,sourceSessionId:v},y=await L("artifact_add",e.id,i,()=>x.add(e.id,i)),j=A(w(),{tool:"wyrm_remember",outcome:y.needs_review===1?"queued":"stored",refTable:"mem",refId:y.id,reason:y.needs_review===1?"needs_review \u2014 hidden until approved":void 0,source:y.created_by,projectId:e.id}),t={id:y.id,kind:y.kind,problem:y.problem,validated_fix:y.validated_fix??null,why_it_worked:y.why_it_worked??null,confidence:y.confidence,tags:y.tags??null,receipt:j};return S(t,(_,m)=>{let r=E(m.brand,"**Memory Stored**")+`
14
+
15
+ `;return r+=`${m.bullet} **Kind:** ${_.kind}
16
+ `,r+=`${m.bullet} **ID:** ${_.id}
17
+ `,r+=`${m.bullet} **Problem:** ${_.problem}
18
+ `,_.validated_fix&&(r+=`${m.bullet} **Solution:** ${_.validated_fix}
19
+ `),_.why_it_worked&&(r+=`${m.bullet} **Why it worked:** ${_.why_it_worked}
20
+ `),r+=`${m.bullet} **Confidence:** ${(_.confidence*100).toFixed(0)}%
21
+ `,_.tags&&(r+=`${m.bullet} **Tags:** ${_.tags}
22
+ `),r+="\n_Use `wyrm_recall` to retrieve similar memories, or `wyrm_context_build` to assemble a task brief._",r})}},{name:"wyrm_auto_capture",description:"Auto-extract durable memories from freeform text (a session transcript, notes, a chat) into the REVIEW QUEUE. A LOCAL extractor \u2014 a configurable Ollama model (WYRM_EXTRACT_MODEL / the `model` arg) if set, else a deterministic heuristic \u2014 pulls out truths, failures, decisions, patterns, and lessons as needs_review candidates; approve/reject with wyrm_review (nothing auto-trusted). Closes the 'just talk, it remembers' gap without a cloud LLM. Deduped + idempotent. Example: wyrm_auto_capture({ projectPath: '/home/user/api', text: 'Deploy failed twice because wrangler.toml pointed at the old KV id; switched to the new binding and decided to pin binding names in CI.' })",inputSchema:{type:"object",properties:{projectPath:{type:"string",description:"Project to file the extracted candidates under"},text:{type:"string",description:"Freeform text to extract durable memories from"},model:{type:"string",description:"Optional Ollama model for extraction (overrides WYRM_EXTRACT_MODEL); omit for deterministic extraction"}},required:["projectPath","text"]},outputSchema:{type:"object",properties:{method:{type:"string"},model:{type:["string","null"]},added:{type:"integer"},skipped:{type:"integer"},candidates:{type:"array",items:{type:"object",properties:{kind:{type:"string"},text:{type:"string"}},required:["kind","text"]}}},required:["method","model","added","skipped","candidates"]},annotations:D.wyrm_auto_capture,aliases:[],handler:async($,{store:b,raw:x,memory:w,cache:l})=>{const{projectPath:h,text:a,model:g}=$,n=b.getProject(h);if(!n)return R(C("wyrm_auto_capture","projectPath","project not found"));if(!a||a.trim().length<20)return R(C("wyrm_auto_capture","text","must be at least 20 characters"));const{candidates:f,method:p,model:v}=await F(a,{model:g}),d=x();let s=0,u=0;for(const e of f){const i=N(e),y=i.tags[i.tags.length-1];if(d.prepare("SELECT 1 FROM memory_artifacts WHERE project_id = ? AND tags LIKE ? ESCAPE '\\' LIMIT 1").get(n.id,"%"+M(y)+"%")){u++,A(d,{tool:"wyrm_auto_capture",outcome:"dropped",reason:"dedup \u2014 already captured",source:"auto-extract",projectId:n.id});continue}const j=w.add(n.id,{kind:i.kind,problem:i.problem,tags:i.tags,confidence:i.confidence,needsReview:1,createdBy:"auto-extract"});A(d,{tool:"wyrm_auto_capture",outcome:"queued",refTable:"mem",refId:j.id,reason:"needs_review \u2014 auto-extracted; review to activate",source:"auto-extract",projectId:n.id}),s++}s>0&&l.invalidate();const o={method:p,model:v??null,added:s,skipped:u,receipt:{outcome:s>0?"queued":"dropped",reason:`${s} queued for review, ${u} dropped as duplicates`},candidates:f.slice(0,8).map(e=>({kind:e.kind,text:e.text.slice(0,80)}))};return S(o,(e,i)=>{const y=e.candidates.map(j=>` ${i.bullet} [${j.kind}] ${j.text}`).join(`
23
+ `);return E(i.brand,`Auto-capture (${e.method}${e.model?` - ${e.model}`:""}) -- ${e.added} candidate(s) -> review queue, ${e.skipped} already present.`)+`
24
+ ${y}
25
+
26
+ Approve/reject with wyrm_review.`})}},{name:"wyrm_capture_trace",description:"Harvest a harness's working trace into run-tagged review-queue candidates \u2014 OFFLINE. Ingests a Claude Code session transcript (JSONL), a ~/.dragon/traces file/dir, or WYRM_TRACE_TOOL_CALLS event output. The SAME local extractor as auto-capture (Ollama WYRM_EXTRACT_MODEL if set, else deterministic \u2014 never a cloud LLM) pulls durable truths/failures/decisions/patterns/lessons; secrets (API keys, tokens, passwords) are REDACTED before anything is stored; candidates land needs_review=1 stamped with the run_id (arg or ambient envelope). Approve/reject with wyrm_review; deduped + idempotent. Pass `trace` (inline text) OR `path` (a trace file or the ~/.dragon/traces dir). Reached via wyrm_capture({ mode: 'trace' }).",inputSchema:{type:"object",properties:{projectPath:{type:"string",description:"Project to file the candidates under"},trace:{type:"string",description:"Inline trace text (JSONL transcript / dragon records / tool-call events)"},path:{type:"string",description:"Path to a trace file, or the ~/.dragon/traces directory (newest file is read); omit ~ for absolute"},format:{type:"string",enum:["auto","claude-jsonl","dragon","tool-calls"],description:"Trace format (default auto-detect)"},run_id:{type:"string",description:"Tag candidates with this run (default: ambient envelope run_id)"},model:{type:"string",description:"Optional Ollama model for extraction (overrides WYRM_EXTRACT_MODEL); omit for deterministic"}},required:["projectPath"]},outputSchema:{type:"object",properties:{method:{type:"string"},model:{type:["string","null"]},format:{type:"string"},source:{type:"string"},run_id:{type:["string","null"]},segments:{type:"integer"},added:{type:"integer"},skipped:{type:"integer"},candidates:{type:"array",items:{type:"object",properties:{kind:{type:"string"},text:{type:"string"}},required:["kind","text"]}}},required:["method","model","format","source","run_id","segments","added","skipped","candidates"]},annotations:D.wyrm_capture_trace,aliases:[],handler:async($,{store:b,raw:x,memory:w,cache:l})=>{const{projectPath:h,trace:a,path:g,format:n,run_id:f,model:p}=$,v=b.getProject(h);if(!v)return R(C("wyrm_capture_trace","projectPath","project not found"));const d=V(),s=typeof f=="string"&&f.trim()?f.trim().slice(0,64):d.run_id;let u="",o="inline";if(typeof a=="string"&&a.trim())u=a,o="inline";else if(typeof g=="string"&&g.trim()){let c=g.trim();(c==="~"||c.startsWith("~/"))&&(c=H(te(),c.slice(1)));try{if(W(c).isDirectory()){const q=ee(c).map(T=>H(c,T)).filter(T=>{try{return W(T).isFile()}catch{return!1}}).sort((T,B)=>W(B).mtimeMs-W(T).mtimeMs);if(q.length===0)return R(C("wyrm_capture_trace","path","no trace files in directory"));u=U(q[0],"utf-8"),o=q[0]}else u=U(c,"utf-8"),o=c}catch(k){return R(C("wyrm_capture_trace","path",`could not read trace path: ${k.message}`))}}else return R(C("wyrm_capture_trace","trace","provide inline trace or path to trace file"));const{segments:e,format:i}=X(u,n??"auto"),y=G(e);if(y.trim().length<20){const c={method:"deterministic",model:null,format:i,source:o,run_id:s,segments:e.length,added:0,skipped:0,candidates:[]};return S(c,(k,q)=>E(q.brand,`Trace harvest (${k.format}) -- no durable content extracted from ${k.segments} segment(s).`))}const{candidates:j,method:t,model:_}=await F(y,{model:p}),m=x();let r=0,I=0;for(const c of j){const k=N(c),q=k.tags[k.tags.length-1];if(m.prepare("SELECT 1 FROM memory_artifacts WHERE project_id = ? AND tags LIKE ? ESCAPE '\\' LIMIT 1").get(v.id,"%"+M(q)+"%")){I++;continue}const T=s?[...k.tags,"trace",`run:${s}`]:[...k.tags,"trace"];z({agent_id:d.agent_id,run_id:s,source:s?"param":d.source},()=>w.add(v.id,{kind:k.kind,problem:k.problem,tags:T,confidence:k.confidence,needsReview:1,createdBy:"trace-harvest"})),r++}r>0&&l.invalidate();const P={method:t,model:_??null,format:i,source:o,run_id:s,segments:e.length,added:r,skipped:I,candidates:j.slice(0,8).map(c=>({kind:c.kind,text:c.text.slice(0,80)}))};return S(P,(c,k)=>{const q=c.candidates.map(T=>` ${k.bullet} [${T.kind}] ${T.text}`).join(`
27
+ `);return E(k.brand,`Trace harvest (${c.format}, ${c.method}${c.model?` - ${c.model}`:""}) -- ${c.segments} segment(s) -> ${c.added} candidate(s) to review queue, ${c.skipped} already present${c.run_id?` [run:${c.run_id}]`:""}.`)+(q?`
28
28
  ${q}`:"")+`
29
29
 
30
- Approve/reject with wyrm_review.`})}},{name:"wyrm_distill",description:"Distill a work session into candidate memory artifacts for review. Parses the session's objectives, decisions, and outcomes into structured artifacts (lessons, patterns, anti-patterns) and queues them for approval via wyrm_review. Use at session end to extract institutional knowledge.",inputSchema:{type:"object",properties:{projectPath:{type:"string",description:"Project root path"},sessionId:{type:"string",description:"Session ID to distill"},candidates:{type:"array",description:"Pre-parsed candidate artifacts to store for review",items:{type:"object",properties:{kind:{type:"string",enum:["lesson","pattern","anti_pattern","heuristic","reasoning_trace"]},title:{type:"string"},content:{type:"string"},tags:{type:"array",items:{type:"string"}},confidence:{type:"number"}},required:["kind","title","content"]}}},required:["projectPath","candidates"]},outputSchema:{type:"object",properties:{count:{type:"integer"},artifacts:{type:"array",items:{type:"object",properties:{id:{type:"integer"},kind:{type:"string"},title:{type:"string"}},required:["id","kind","title"]}}},required:["count","artifacts"]},annotations:C.wyrm_distill,aliases:[],handler:(k,{store:_,memory:$})=>{const{projectPath:y,candidates:m}=k,w=_.getProject(y);if(!w)return{content:[{type:"text",text:`Project not found: ${y}`}],isError:!0};if(!m||m.length===0)return{content:[{type:"text",text:`**Distill**
30
+ Approve/reject with wyrm_review.`})}},{name:"wyrm_distill",description:"Distill a work session into candidate memory artifacts for review. Parses the session's objectives, decisions, and outcomes into structured artifacts (lessons, patterns, anti-patterns) and queues them for approval via wyrm_review. Use at session end to extract institutional knowledge.",inputSchema:{type:"object",properties:{projectPath:{type:"string",description:"Project root path"},sessionId:{type:"string",description:"Session ID to distill"},candidates:{type:"array",description:"Pre-parsed candidate artifacts to store for review",items:{type:"object",properties:{kind:{type:"string",enum:["lesson","pattern","anti_pattern","heuristic","reasoning_trace"]},title:{type:"string"},content:{type:"string"},tags:{type:"array",items:{type:"string"}},confidence:{type:"number"}},required:["kind","title","content"]}}},required:["projectPath","candidates"]},outputSchema:{type:"object",properties:{count:{type:"integer"},artifacts:{type:"array",items:{type:"object",properties:{id:{type:"integer"},kind:{type:"string"},title:{type:"string"}},required:["id","kind","title"]}}},required:["count","artifacts"]},annotations:D.wyrm_distill,aliases:[],handler:($,{store:b,memory:x})=>{const{projectPath:w,candidates:l}=$,h=b.getProject(w);if(!h)return{content:[{type:"text",text:`Project not found: ${w}`}],isError:!0};if(!l||l.length===0)return{content:[{type:"text",text:`**Distill**
31
31
 
32
- No candidates provided. Provide an array of candidate artifacts to distill.`}],isError:!0};const a=[];for(const d of m){const l=$.add(w.id,{kind:d.kind,problem:d.title,validatedFix:d.content,tags:d.tags??[],confidence:d.confidence??.7,needsReview:1});a.push({id:l.id,kind:d.kind,title:d.title})}const f={count:a.length,artifacts:a};return S(f,(d,l)=>{let u=E(l.brand,`**Distillation Complete** -- ${d.count} artifact${d.count!==1?"s":""} queued for review`)+`
32
+ No candidates provided. Provide an array of candidate artifacts to distill.`}],isError:!0};const a=[];for(const n of l){const f=x.add(h.id,{kind:n.kind,problem:n.title,validatedFix:n.content,tags:n.tags??[],confidence:n.confidence??.7,needsReview:1});a.push({id:f.id,kind:n.kind,title:n.title})}const g={count:a.length,artifacts:a};return S(g,(n,f)=>{let p=E(f.brand,`**Distillation Complete** -- ${n.count} artifact${n.count!==1?"s":""} queued for review`)+`
33
33
 
34
- `;return u+=d.artifacts.map(b=>`${l.bullet} [#${b.id}] **${b.kind}**: ${b.title}`).join(`
35
- `),u+="\n\nUse `wyrm_review` with each ID to approve or reject.",u})}},{name:"wyrm_harvest",description:"Auto-populate memory from artifacts you already produce. Walks a project (or ALL registered projects) and harvests durable facts from its docs (README/CLAUDE/AGENTS/ARCHITECTURE) plus recent commit subjects (git log) into the REVIEW QUEUE \u2014 approve/reject with wyrm_review, nothing is auto-trusted. Idempotent (re-runs skip what's already harvested). Use dryRun to preview. The fix for a thin corpus.",inputSchema:{type:"object",properties:{projectPath:{type:"string",description:"Harvest one project; omit to harvest ALL registered projects"},dryRun:{type:"boolean",description:"Preview what would be harvested without writing anything"},gitLimit:{type:"number",description:"How many recent commits to scan per project (default 30, max 200)"},includeCode:{type:"boolean",description:"Also harvest CODE: tech-stack facts from manifests (package.json/Cargo.toml/\u2026) + TODO/FIXME markers into the review queue, AND index code symbols (functions/classes/types) for search. Heavier \u2014 scope to one project for the full code pass."}}},outputSchema:{type:"object",properties:{dry_run:{type:"boolean"},projects_scanned:{type:"integer"},added:{type:"integer"},skipped:{type:"integer"},top:{type:"array",items:{type:"object",properties:{project:{type:"string"},added:{type:"integer"},skipped:{type:"integer"}},required:["project","added","skipped"]}},sample:{type:"array",items:{type:"string"}},code_symbols:{type:["object","null"],properties:{symbols:{type:"integer"},projects:{type:"integer"},files:{type:"integer"}},required:["symbols","projects","files"]}},required:["dry_run","projects_scanned","added","skipped","top","sample","code_symbols"]},annotations:C.wyrm_harvest,aliases:[],handler:(k,{store:_,raw:$,memory:y,symbols:m,cache:w})=>{const{projectPath:a,dryRun:f,gitLimit:d,includeCode:l}=k,u=$(),b={existsBySig:(s,h)=>!!u.prepare("SELECT 1 FROM memory_artifacts WHERE project_id = ? AND tags LIKE ? ESCAPE '\\' LIMIT 1").get(s,"%"+M(h)+"%"),addCandidate:(s,h)=>{y.add(s,{kind:h.kind,problem:h.text,tags:[...h.tags,h.sig],confidence:h.confidence,needsReview:1,createdBy:"harvest"})}};let i;if(a){const s=_.getProject(a);if(!s)return{content:[{type:"text",text:"Project not found"}],isError:!0};i=[{id:s.id,name:s.name,path:s.path}]}else i=_.getAllProjects(500).map(s=>({id:s.id,name:s.name,path:s.path}));const{reports:t,totalAdded:g,totalSkipped:r}=Z(b,i,{dryRun:f,gitLimit:d,includeCode:l});!f&&g>0&&w.invalidate();let e=null;if(l&&!f){let s=0,h=0,P=0;for(const o of i)try{const x=m.indexProject(o.id,o.path);s+=x.files,h+=x.symbols,P++}catch{}e={symbols:h,projects:P,files:s}}const c=t.filter(s=>s.added>0).sort((s,h)=>h.added-s.added).slice(0,15).map(s=>({project:s.project,added:s.added,skipped:s.skipped})),n=t.flatMap(s=>s.sample??[]).slice(0,6),v={dry_run:!!f,projects_scanned:i.length,added:g,skipped:r,top:c,sample:n,code_symbols:e};return S(v,(s,h)=>{const P=s.code_symbols?`
36
- Indexed ${s.code_symbols.symbols} code symbols across ${s.code_symbols.projects} project(s) (${s.code_symbols.files} files) -> searchable via wyrm_symbol_search.`:"",o=s.top.map(I=>` +${String(I.added).padStart(3)} (skip ${I.skipped}) ${I.project}`).join(`
37
- `),x=s.sample.map(I=>` ${h.bullet} ${I}`).join(`
38
- `);return E(h.brand,`Harvest ${s.dry_run?"(dry run) ":""}-- ${s.added} candidate(s) -> review queue across ${s.projects_scanned} project(s); ${s.skipped} already present.`)+`${P}
39
- ${o}`+(x?`
34
+ `;return p+=n.artifacts.map(v=>`${f.bullet} [#${v.id}] **${v.kind}**: ${v.title}`).join(`
35
+ `),p+="\n\nUse `wyrm_review` with each ID to approve or reject.",p})}},{name:"wyrm_harvest",description:"Auto-populate memory from artifacts you already produce. Walks a project (or ALL registered projects) and harvests durable facts from its docs (README/CLAUDE/AGENTS/ARCHITECTURE) plus recent commit subjects (git log) into the REVIEW QUEUE \u2014 approve/reject with wyrm_review, nothing is auto-trusted. Idempotent (re-runs skip what's already harvested). Use dryRun to preview. The fix for a thin corpus.",inputSchema:{type:"object",properties:{projectPath:{type:"string",description:"Harvest one project; omit to harvest ALL registered projects"},dryRun:{type:"boolean",description:"Preview what would be harvested without writing anything"},gitLimit:{type:"number",description:"How many recent commits to scan per project (default 30, max 200)"},includeCode:{type:"boolean",description:"Also harvest CODE: tech-stack facts from manifests (package.json/Cargo.toml/\u2026) + TODO/FIXME markers into the review queue, AND index code symbols (functions/classes/types) for search. Heavier \u2014 scope to one project for the full code pass."}}},outputSchema:{type:"object",properties:{dry_run:{type:"boolean"},projects_scanned:{type:"integer"},added:{type:"integer"},skipped:{type:"integer"},top:{type:"array",items:{type:"object",properties:{project:{type:"string"},added:{type:"integer"},skipped:{type:"integer"}},required:["project","added","skipped"]}},sample:{type:"array",items:{type:"string"}},code_symbols:{type:["object","null"],properties:{symbols:{type:"integer"},projects:{type:"integer"},files:{type:"integer"}},required:["symbols","projects","files"]}},required:["dry_run","projects_scanned","added","skipped","top","sample","code_symbols"]},annotations:D.wyrm_harvest,aliases:[],handler:($,{store:b,raw:x,memory:w,symbols:l,cache:h})=>{const{projectPath:a,dryRun:g,gitLimit:n,includeCode:f}=$,p=x(),v={existsBySig:(t,_)=>!!p.prepare("SELECT 1 FROM memory_artifacts WHERE project_id = ? AND tags LIKE ? ESCAPE '\\' LIMIT 1").get(t,"%"+M(_)+"%"),addCandidate:(t,_)=>{const m=w.add(t,{kind:_.kind,problem:_.text,tags:[..._.tags,_.sig],confidence:_.confidence,needsReview:1,createdBy:"harvest"});A(p,{tool:"wyrm_harvest",outcome:"queued",refTable:"mem",refId:m.id,reason:"needs_review \u2014 harvested; review to activate",source:"harvest",projectId:t})},onSkip:t=>{A(p,{tool:"wyrm_harvest",outcome:"dropped",reason:"dedup \u2014 signature already harvested",source:"harvest",projectId:t})}};let d;if(a){const t=b.getProject(a);if(!t)return{content:[{type:"text",text:"Project not found"}],isError:!0};d=[{id:t.id,name:t.name,path:t.path}]}else d=b.getAllProjects(500).map(t=>({id:t.id,name:t.name,path:t.path}));const{reports:s,totalAdded:u,totalSkipped:o}=Z(v,d,{dryRun:g,gitLimit:n,includeCode:f});!g&&u>0&&h.invalidate();let e=null;if(f&&!g){let t=0,_=0,m=0;for(const r of d)try{const I=l.indexProject(r.id,r.path);t+=I.files,_+=I.symbols,m++}catch{}e={symbols:_,projects:m,files:t}}const i=s.filter(t=>t.added>0).sort((t,_)=>_.added-t.added).slice(0,15).map(t=>({project:t.project,added:t.added,skipped:t.skipped})),y=s.flatMap(t=>t.sample??[]).slice(0,6),j={dry_run:!!g,projects_scanned:d.length,added:u,skipped:o,top:i,sample:y,code_symbols:e};return S(j,(t,_)=>{const m=t.code_symbols?`
36
+ Indexed ${t.code_symbols.symbols} code symbols across ${t.code_symbols.projects} project(s) (${t.code_symbols.files} files) -> searchable via wyrm_symbol_search.`:"",r=t.top.map(P=>` +${String(P.added).padStart(3)} (skip ${P.skipped}) ${P.project}`).join(`
37
+ `),I=t.sample.map(P=>` ${_.bullet} ${P}`).join(`
38
+ `);return E(_.brand,`Harvest ${t.dry_run?"(dry run) ":""}-- ${t.added} candidate(s) -> review queue across ${t.projects_scanned} project(s); ${t.skipped} already present.`)+`${m}
39
+ ${r}`+(I?`
40
40
 
41
41
  Sample:
42
- ${x}`:"")+`
42
+ ${I}`:"")+`
43
43
 
44
- Approve/reject with wyrm_review.`})}},{name:"wyrm_import_git",description:"Import a list of commits into the Wyrm memory review queue. Useful for onboarding a project's history or capturing recent work.",inputSchema:{type:"object",properties:{project_id:{type:"number",description:"Project ID"},commits:{type:"array",description:"Array of commit objects",items:{type:"object",properties:{message:{type:"string"},diff_summary:{type:"string"},author:{type:"string"},date:{type:"string"}},required:["message"]}},auto_approve:{type:"boolean",description:"Skip review queue and activate immediately (default: false)"}},required:["project_id","commits"]},outputSchema:{type:"object",properties:{captured:{type:"integer"},skipped:{type:"integer"},auto_approved:{type:"boolean"}},required:["captured","skipped","auto_approved"]},annotations:C.wyrm_import_git,aliases:[],handler:(k,{store:_,memory:$,raw:y})=>{const{project_id:m,commits:w,auto_approve:a}=k;if(!_.getProjectById(m))return{content:[{type:"text",text:`Project ID ${m} not found.`}],isError:!0};let d=0,l=0;const u=y();for(const i of w){const t=i.message??"";if(/^Merge /i.test(t)){l++;continue}if(/^(chore|bump|release|version)/i.test(t)){l++;continue}const g="gi:"+K("sha1").update(`${t}
45
- ${i.diff_summary??""}`).digest("hex").slice(0,16);if(u.prepare("SELECT 1 FROM memory_artifacts WHERE project_id = ? AND tags LIKE ? ESCAPE '\\' LIMIT 1").get(m,"%"+M(g)+"%")){l++,D(u,{tool:"wyrm_import_git",outcome:"dropped",reason:"dedup \u2014 this commit was already imported (gi: signature match)",source:"import:git",projectId:m});continue}let r="pattern";/^fix(\(.+\))?:/i.test(t)?r="lesson":/^feat(\(.+\))?:/i.test(t)?r="pattern":/^refactor(\(.+\))?:/i.test(t)&&(r="heuristic");const e=t.split(":")[0]??"commit",c=$.add(m,{kind:r,problem:t,validatedFix:i.diff_summary??"",whyItWorked:`Committed by ${i.author??"unknown"} on ${i.date??"unknown"}`,tags:["git","commit",e.toLowerCase(),g],confidence:.6,needsReview:a?0:1});D(u,{tool:"wyrm_import_git",outcome:a?"stored":"queued",refTable:"mem",refId:c.id,reason:a?void 0:"needs_review \u2014 imported trust lane; invisible to recall until wyrm_review",source:"import:git",projectId:m}),d++}return S({captured:d,skipped:l,auto_approved:!!a},(i,t)=>E(t.brand,"**Git Import Complete**")+`
44
+ Approve/reject with wyrm_review.`})}},{name:"wyrm_import_git",description:"Import a list of commits into the Wyrm memory review queue. Useful for onboarding a project's history or capturing recent work.",inputSchema:{type:"object",properties:{project_id:{type:"number",description:"Project ID"},commits:{type:"array",description:"Array of commit objects",items:{type:"object",properties:{message:{type:"string"},diff_summary:{type:"string"},author:{type:"string"},date:{type:"string"}},required:["message"]}},auto_approve:{type:"boolean",description:"Skip review queue and activate immediately (default: false)"}},required:["project_id","commits"]},outputSchema:{type:"object",properties:{captured:{type:"integer"},skipped:{type:"integer"},auto_approved:{type:"boolean"}},required:["captured","skipped","auto_approved"]},annotations:D.wyrm_import_git,aliases:[],handler:($,{store:b,memory:x,raw:w})=>{const{project_id:l,commits:h,auto_approve:a}=$;if(!b.getProjectById(l))return{content:[{type:"text",text:`Project ID ${l} not found.`}],isError:!0};let n=0,f=0;const p=w();for(const d of h){const s=d.message??"";if(/^Merge /i.test(s)){f++;continue}if(/^(chore|bump|release|version)/i.test(s)){f++;continue}const u="gi:"+O("sha1").update(`${s}
45
+ ${d.diff_summary??""}`).digest("hex").slice(0,16);if(p.prepare("SELECT 1 FROM memory_artifacts WHERE project_id = ? AND tags LIKE ? ESCAPE '\\' LIMIT 1").get(l,"%"+M(u)+"%")){f++,A(p,{tool:"wyrm_import_git",outcome:"dropped",reason:"dedup \u2014 this commit was already imported (gi: signature match)",source:"import:git",projectId:l});continue}let o="pattern";/^fix(\(.+\))?:/i.test(s)?o="lesson":/^feat(\(.+\))?:/i.test(s)?o="pattern":/^refactor(\(.+\))?:/i.test(s)&&(o="heuristic");const e=s.split(":")[0]??"commit",i=x.add(l,{kind:o,problem:s,validatedFix:d.diff_summary??"",whyItWorked:`Committed by ${d.author??"unknown"} on ${d.date??"unknown"}`,tags:["git","commit",e.toLowerCase(),u],confidence:.6,needsReview:a?0:1});A(p,{tool:"wyrm_import_git",outcome:a?"stored":"queued",refTable:"mem",refId:i.id,reason:a?void 0:"needs_review \u2014 imported; review to activate",source:"import:git",projectId:l}),n++}return S({captured:n,skipped:f,auto_approved:!!a},(d,s)=>E(s.brand,"**Git Import Complete**")+`
46
46
 
47
- ${t.bullet} **Captured:** ${i.captured}
48
- ${t.bullet} **Skipped:** ${i.skipped} (merges/chores)
49
- ${t.bullet} **Review needed:** ${i.auto_approved?"No (auto-approved)":`Yes -- run \`wyrm_review\` to activate ${i.captured} artifact${i.captured!==1?"s":""}`}`)}},{name:"wyrm_import_pr",description:"Import a pull request (title, body, review comments) into the Wyrm memory review queue to extract patterns and heuristics.",inputSchema:{type:"object",properties:{project_id:{type:"number",description:"Project ID"},title:{type:"string",description:"PR title"},body:{type:"string",description:"PR description / body"},review_comments:{type:"array",items:{type:"string"},description:"Review comment strings"},auto_approve:{type:"boolean",description:"Skip review queue (default: false)"}},required:["project_id","title","body"]},outputSchema:{type:"object",properties:{created:{type:"integer"},auto_approved:{type:"boolean"}},required:["created","auto_approved"]},annotations:C.wyrm_import_pr,aliases:[],handler:(k,{store:_,memory:$})=>{const{project_id:y,title:m,body:w,review_comments:a,auto_approve:f}=k;if(!_.getProjectById(y))return{content:[{type:"text",text:`Project ID ${y} not found.`}],isError:!0};let l=0;const u=f?0:1;$.add(y,{kind:"pattern",problem:`${m}
47
+ ${s.bullet} **Captured:** ${d.captured}
48
+ ${s.bullet} **Skipped:** ${d.skipped} (merges/chores)
49
+ ${s.bullet} **Review needed:** ${d.auto_approved?"No (auto-approved)":`Yes -- run \`wyrm_review\` to activate ${d.captured} artifact${d.captured!==1?"s":""}`}`)}},{name:"wyrm_import_pr",description:"Import a pull request (title, body, review comments) into the Wyrm memory review queue to extract patterns and heuristics.",inputSchema:{type:"object",properties:{project_id:{type:"number",description:"Project ID"},title:{type:"string",description:"PR title"},body:{type:"string",description:"PR description / body"},review_comments:{type:"array",items:{type:"string"},description:"Review comment strings"},auto_approve:{type:"boolean",description:"Skip review queue (default: false)"}},required:["project_id","title","body"]},outputSchema:{type:"object",properties:{created:{type:"integer"},auto_approved:{type:"boolean"}},required:["created","auto_approved"]},annotations:D.wyrm_import_pr,aliases:[],handler:($,{store:b,memory:x,raw:w})=>{const{project_id:l,title:h,body:a,review_comments:g,auto_approve:n}=$;if(!b.getProjectById(l))return{content:[{type:"text",text:`Project ID ${l} not found.`}],isError:!0};let p=0,v=0;const d=n?0:1,s=w(),u=(e,i,y)=>{const j="pr:"+O("sha1").update(i).digest("hex").slice(0,16);if(s.prepare("SELECT 1 FROM memory_artifacts WHERE project_id = ? AND tags LIKE ? ESCAPE '\\' LIMIT 1").get(l,"%"+M(j)+"%")){v++,A(s,{tool:"wyrm_import_pr",outcome:"dropped",reason:"dedup \u2014 already imported (pr: signature)",source:"import:pr",projectId:l});return}const t=x.add(l,{kind:e,problem:i,validatedFix:"",whyItWorked:"",tags:[...y,j],confidence:.65,needsReview:d,createdBy:"import:pr"});A(s,{tool:"wyrm_import_pr",outcome:n?"stored":"queued",refTable:"mem",refId:t.id,reason:n?void 0:"needs_review \u2014 imported; review to activate",source:"import:pr",projectId:l}),p++};u("pattern",`${h}
50
50
 
51
- ${w}`,validatedFix:"",whyItWorked:"",tags:["git","pr"],confidence:.65,needsReview:u}),l++;for(const i of a??[])/\b(should|must|always|never|prefer|avoid|use|don't)\b/i.test(i)&&($.add(y,{kind:"heuristic",problem:i,validatedFix:"",whyItWorked:"",tags:["git","pr","review"],confidence:.65,needsReview:u}),l++);return S({created:l,auto_approved:!!f},(i,t)=>E(t.brand,"**PR Import Complete**")+`
51
+ ${a}`,["git","pr"]);for(const e of g??[])/\b(should|must|always|never|prefer|avoid|use|don't)\b/i.test(e)&&u("heuristic",e,["git","pr","review"]);return S({created:p,skipped:v,auto_approved:!!n},(e,i)=>E(i.brand,"**PR Import Complete**")+`
52
52
 
53
- ${t.bullet} **Artifacts created:** ${i.created}
54
- ${t.bullet} **Review needed:** ${i.auto_approved?"No (auto-approved)":"Yes -- run `wyrm_review` to activate"}`)}},{name:"wyrm_import_rules",description:"Import a .cursorrules / copilot-instructions file into Wyrm. Parses by section and creates ground truths (for constraint/rule language) or memory artifacts (for general guidelines).",inputSchema:{type:"object",properties:{project_id:{type:"number",description:"Project ID"},content:{type:"string",description:"Full text of the rules file"},format:{type:"string",enum:["cursorrules","copilot","plain"],description:"Source format (default: plain)"},source_file:{type:"string",description:"Filename for provenance (optional)"}},required:["project_id","content"]},outputSchema:{type:"object",properties:{format:{type:"string"},source:{type:"string"},truths_created:{type:"integer"},artifacts_created:{type:"integer"}},required:["format","source","truths_created","artifacts_created"]},annotations:C.wyrm_import_rules,aliases:[],handler:(k,{store:_,memory:$,truths:y})=>{const{project_id:m,content:w,format:a,source_file:f}=k;if(!_.getProjectById(m))return{content:[{type:"text",text:`Project ID ${m} not found.`}],isError:!0};const l=a??"plain",u=f??"rules",b=["imported",l,u],i=w.split(/\n(?=#)/),t=w.split(/\n\n+/),g=(i.length>=t.length?i:t).map(n=>n.trim()).filter(n=>n.length>=15);let r=0,e=0;for(const n of g)/\b(always|never|must|use|don't|avoid|prefer)\b/i.test(n)?(y.set(m,{category:"constraint",key:n.slice(0,50).replace(/\n/g," "),value:n,source:u}),r++):($.add(m,{kind:"heuristic",problem:n,validatedFix:"",whyItWorked:"",tags:b,confidence:.7,needsReview:1}),e++);return S({format:l,source:u,truths_created:r,artifacts_created:e},(n,v)=>E(v.brand,`**Rules Import Complete** (${n.format})`)+`
53
+ ${i.bullet} **Artifacts created:** ${e.created}
54
+ ${i.bullet} **Review needed:** ${e.auto_approved?"No (auto-approved)":"Yes -- run `wyrm_review` to activate"}`)}},{name:"wyrm_import_rules",description:"Import a .cursorrules / copilot-instructions file into Wyrm. Parses by section and creates ground truths (for constraint/rule language) or memory artifacts (for general guidelines).",inputSchema:{type:"object",properties:{project_id:{type:"number",description:"Project ID"},content:{type:"string",description:"Full text of the rules file"},format:{type:"string",enum:["cursorrules","copilot","plain"],description:"Source format (default: plain)"},source_file:{type:"string",description:"Filename for provenance (optional)"}},required:["project_id","content"]},outputSchema:{type:"object",properties:{format:{type:"string"},source:{type:"string"},truths_created:{type:"integer"},artifacts_created:{type:"integer"}},required:["format","source","truths_created","artifacts_created"]},annotations:D.wyrm_import_rules,aliases:[],handler:async($,{store:b,memory:x,truths:w,raw:l})=>{const{project_id:h,content:a,format:g,source_file:n}=$;if(!b.getProjectById(h))return{content:[{type:"text",text:`Project ID ${h} not found.`}],isError:!0};const p=g??"plain",v=n??"rules",d=["imported",p,v],s=a.split(/\n(?=#)/),u=a.split(/\n\n+/),o=(s.length>=u.length?s:u).map(m=>m.trim()).filter(m=>m.length>=15);let e=0,i=0,y=0;const{isConfigShapedTruth:j}=await import("../metabolize.js"),t=l();for(const m of o){const r=m.slice(0,50).replace(/\n/g," ");if(/\b(always|never|must|use|don't|avoid|prefer)\b/i.test(m)&&!j(r,m)){const I=w.set(h,{category:"constraint",key:r,value:m,source:v});A(t,{tool:"wyrm_import_rules",outcome:"stored",refTable:"truth",refId:I.id,source:"import:rules",projectId:h}),e++}else{const I=x.add(h,{kind:"heuristic",problem:m,validatedFix:"",whyItWorked:"",tags:d,confidence:.7,needsReview:1,createdBy:"import:rules"});j(r,m)&&y++,A(t,{tool:"wyrm_import_rules",outcome:"queued",refTable:"mem",refId:I.id,reason:j(r,m)?"truth-shape lint \u2014 config shape routed to review":"needs_review \u2014 imported; review to activate",source:"import:rules",projectId:h}),i++}}return S({format:p,source:v,truths_created:e,artifacts_created:i,linted:y},(m,r)=>E(r.brand,`**Rules Import Complete** (${m.format})`)+`
55
55
 
56
- ${v.bullet} **Ground truths created:** ${n.truths_created} (active immediately)
57
- ${v.bullet} **Memory artifacts created:** ${n.artifacts_created} (pending review)
58
- ${v.bullet} **Source:** ${n.source}
56
+ ${r.bullet} **Ground truths created:** ${m.truths_created} (active immediately)
57
+ ${r.bullet} **Memory artifacts created:** ${m.artifacts_created} (pending review)
58
+ ${r.bullet} **Source:** ${m.source}
59
59
 
60
- Run \`wyrm_review\` to approve the memory artifacts.`)}},{name:"wyrm_spec_register",description:"Make spec-kit Wyrm-native. Reads a spec directory (spec.md title/summary + tasks.md task list) and creates one Wyrm quest per task, linked to a project, with a stored spec\u2192project link. Idempotent \u2014 re-running updates the same quests (deduped by a per-spec-task signature) instead of duplicating.",inputSchema:{type:"object",properties:{specDir:{type:"string",description:"Absolute path to the spec directory (e.g. '.../dragon-platform/specs/001-ghostmesh')"},projectPath:{type:"string",description:"Path of the Wyrm project to attach the spec + quests to (must already be registered)"},priority:{type:"string",enum:["critical","high","medium","low"],description:"Priority for the generated quests (default 'medium')"}},required:["specDir","projectPath"]},outputSchema:{type:"object",properties:{spec_dir:{type:"string"},title:{type:["string","null"]},project:{type:"string"},tasks_parsed:{type:"integer"},quests_created:{type:"integer"},quests_updated:{type:"integer"},quest_ids:{type:"array",items:{type:"integer"}}},required:["spec_dir","title","project","tasks_parsed","quests_created","quests_updated","quest_ids"]},annotations:C.wyrm_spec_register,aliases:[],handler:(k,{store:_,indexing:$})=>{const{specDir:y,projectPath:m,priority:w}=k,a=["specDir","projectPath"].filter(r=>{const e=k[r];return!e&&e!==0&&e!==!1});if(a.length>0)return{content:[{type:"text",text:`Missing required arguments: ${a.join(", ")}`}],isError:!0};const f=_.getProject(m);if(!f)return{content:[{type:"text",text:`**Spec Register**: Project not found at "${m}". Register it first (wyrm_scan_projects / wyrm_session_start).`}],isError:!0};const d=re(y);if(d.tasks.length===0){const r={spec_dir:y,title:d.title??null,project:f.name,tasks_parsed:0,quests_created:0,quests_updated:0,quest_ids:[]};return S(r,(e,c)=>E(c.brand,`**Spec Register**: No tasks found in ${B(e.spec_dir,"tasks.md")}. Nothing to create.`))}const l=w||"medium";let u=0,b=0;const i=[];for(const r of d.tasks){const e=ie(y,r.id),c=`${r.id}: ${r.title}`,n=e,v=_.findQuestBySpecSignature(f.id,e);if(v)_.updateQuestFields(v.id,{title:c,tags:n}),i.push(v.id),b++;else{const s=_.addQuest(f.id,c,`spec-kit task from ${y}`,l,n);$()?.enqueue("quest",s.id,f.id),i.push(s.id),u++}}const t=_.upsertSpec(f.id,y,d.title,d.summary,d.tasks.length),g={spec_dir:t.spec_dir,title:t.title??null,project:f.name,tasks_parsed:d.tasks.length,quests_created:u,quests_updated:b,quest_ids:i};return S(g,(r,e)=>{let c=E(e.brand,"**Spec Registered**")+`
60
+ Run \`wyrm_review\` to approve the memory artifacts.`)}},{name:"wyrm_spec_register",description:"Make spec-kit Wyrm-native. Reads a spec directory (spec.md title/summary + tasks.md task list) and creates one Wyrm quest per task, linked to a project, with a stored spec\u2192project link. Idempotent \u2014 re-running updates the same quests (deduped by a per-spec-task signature) instead of duplicating.",inputSchema:{type:"object",properties:{specDir:{type:"string",description:"Absolute path to the spec directory (e.g. '.../dragon-platform/specs/001-ghostmesh')"},projectPath:{type:"string",description:"Path of the Wyrm project to attach the spec + quests to (must already be registered)"},priority:{type:"string",enum:["critical","high","medium","low"],description:"Priority for the generated quests (default 'medium')"}},required:["specDir","projectPath"]},outputSchema:{type:"object",properties:{spec_dir:{type:"string"},title:{type:["string","null"]},project:{type:"string"},tasks_parsed:{type:"integer"},quests_created:{type:"integer"},quests_updated:{type:"integer"},quest_ids:{type:"array",items:{type:"integer"}}},required:["spec_dir","title","project","tasks_parsed","quests_created","quests_updated","quest_ids"]},annotations:D.wyrm_spec_register,aliases:[],handler:($,{store:b,indexing:x})=>{const{specDir:w,projectPath:l,priority:h}=$,a=["specDir","projectPath"].filter(o=>{const e=$[o];return!e&&e!==0&&e!==!1});if(a.length>0)return{content:[{type:"text",text:`Missing required arguments: ${a.join(", ")}`}],isError:!0};const g=b.getProject(l);if(!g)return{content:[{type:"text",text:`**Spec Register**: Project not found at "${l}". Register it first (wyrm_scan_projects / wyrm_session_start).`}],isError:!0};const n=re(w);if(n.tasks.length===0){const o={spec_dir:w,title:n.title??null,project:g.name,tasks_parsed:0,quests_created:0,quests_updated:0,quest_ids:[]};return S(o,(e,i)=>E(i.brand,`**Spec Register**: No tasks found in ${K(e.spec_dir,"tasks.md")}. Nothing to create.`))}const f=h||"medium";let p=0,v=0;const d=[];for(const o of n.tasks){const e=ie(w,o.id),i=`${o.id}: ${o.title}`,y=e,j=b.findQuestBySpecSignature(g.id,e);if(j)b.updateQuestFields(j.id,{title:i,tags:y}),d.push(j.id),v++;else{const t=b.addQuest(g.id,i,`spec-kit task from ${w}`,f,y);x()?.enqueue("quest",t.id,g.id),d.push(t.id),p++}}const s=b.upsertSpec(g.id,w,n.title,n.summary,n.tasks.length),u={spec_dir:s.spec_dir,title:s.title??null,project:g.name,tasks_parsed:n.tasks.length,quests_created:p,quests_updated:v,quest_ids:d};return S(u,(o,e)=>{let i=E(e.brand,"**Spec Registered**")+`
61
61
 
62
- `;return c+=`Spec: ${r.title||"(untitled)"}
63
- `,t.summary&&(c+=`Summary: ${t.summary}
64
- `),c+=`Dir: ${r.spec_dir}
65
- `,c+=`Project: ${r.project}
66
- `,c+=`Tasks parsed: ${r.tasks_parsed}
67
- `,c+=`Quests created: ${r.quests_created} - updated: ${r.quests_updated}
68
- `,c+=`
62
+ `;return i+=`Spec: ${o.title||"(untitled)"}
63
+ `,s.summary&&(i+=`Summary: ${s.summary}
64
+ `),i+=`Dir: ${o.spec_dir}
65
+ `,i+=`Project: ${o.project}
66
+ `,i+=`Tasks parsed: ${o.tasks_parsed}
67
+ `,i+=`Quests created: ${o.quests_created} - updated: ${o.quests_updated}
68
+ `,i+=`
69
69
  \`\`\`json
70
- ${JSON.stringify({spec:r.spec_dir,title:r.title,project:r.project,tasksParsed:r.tasks_parsed,questsCreated:r.quests_created,questsUpdated:r.quests_updated,questIds:r.quest_ids},null,2)}
71
- \`\`\``,c})}},{name:"wyrm_scaffold_save",description:"Save a reasoning scaffold \u2014 a structured checklist that guides step-by-step thinking for a specific problem type. Scaffolds are automatically surfaced by wyrm_context_build when the task matches, helping AI models apply proven reasoning patterns.",inputSchema:{type:"object",properties:{projectPath:{type:"string",description:"Project path (optional, null for global scaffolds)"},problemType:{type:"string",description:"Short slug for the problem type (e.g. 'api-design', 'security-review', 'db-migration')"},description:{type:"string",description:"What kind of task this scaffold helps with"},whenToUse:{type:"string",description:"Conditions that indicate this scaffold should be applied"},steps:{type:"array",items:{type:"string"},description:"Ordered checklist steps to follow"},verificationSteps:{type:"array",items:{type:"string"},description:"Steps to verify the work is correct"},doNotApplyIf:{type:"string",description:"Conditions where this scaffold does NOT apply"},tags:{type:"array",items:{type:"string"},description:"Searchable tags"}},required:["problemType","description","whenToUse","steps"]},outputSchema:{type:"object",properties:{id:{type:"integer"},problem_type:{type:"string"},description:{type:"string"},steps:{type:"integer"}},required:["id","problem_type","description","steps"]},annotations:C.wyrm_scaffold_save,aliases:[],handler:(k,{store:_,scaffolds:$,cache:y})=>{const{projectPath:m,problemType:w,description:a,whenToUse:f,steps:d,verificationSteps:l,doNotApplyIf:u,tags:b}=k,i=m?_.getProject(m)?.id??void 0:void 0,t=$.save({projectId:i,problemType:w,description:a,whenToUse:f,steps:d,verificationSteps:l??void 0,doNotApplyIf:u?[u]:void 0,tags:b??[]});y.invalidate("wyrm_scaffold_get"),y.invalidate("wyrm_context_build");const g={id:t.id,problem_type:w,description:a,steps:d.length};return S(g,(r,e)=>E(e.brand,"**Reasoning Scaffold Saved**")+`
70
+ ${JSON.stringify({spec:o.spec_dir,title:o.title,project:o.project,tasksParsed:o.tasks_parsed,questsCreated:o.quests_created,questsUpdated:o.quests_updated,questIds:o.quest_ids},null,2)}
71
+ \`\`\``,i})}},{name:"wyrm_scaffold_save",description:"Save a reasoning scaffold \u2014 a structured checklist that guides step-by-step thinking for a specific problem type. Scaffolds are automatically surfaced by wyrm_context_build when the task matches, helping AI models apply proven reasoning patterns.",inputSchema:{type:"object",properties:{projectPath:{type:"string",description:"Project path (optional, null for global scaffolds)"},problemType:{type:"string",description:"Short slug for the problem type (e.g. 'api-design', 'security-review', 'db-migration')"},description:{type:"string",description:"What kind of task this scaffold helps with"},whenToUse:{type:"string",description:"Conditions that indicate this scaffold should be applied"},steps:{type:"array",items:{type:"string"},description:"Ordered checklist steps to follow"},verificationSteps:{type:"array",items:{type:"string"},description:"Steps to verify the work is correct"},doNotApplyIf:{type:"string",description:"Conditions where this scaffold does NOT apply"},tags:{type:"array",items:{type:"string"},description:"Searchable tags"}},required:["problemType","description","whenToUse","steps"]},outputSchema:{type:"object",properties:{id:{type:"integer"},problem_type:{type:"string"},description:{type:"string"},steps:{type:"integer"}},required:["id","problem_type","description","steps"]},annotations:D.wyrm_scaffold_save,aliases:[],handler:($,{store:b,scaffolds:x,cache:w})=>{const{projectPath:l,problemType:h,description:a,whenToUse:g,steps:n,verificationSteps:f,doNotApplyIf:p,tags:v}=$,d=l?b.getProject(l)?.id??void 0:void 0,s=x.save({projectId:d,problemType:h,description:a,whenToUse:g,steps:n,verificationSteps:f??void 0,doNotApplyIf:p?[p]:void 0,tags:v??[]});w.invalidate("wyrm_scaffold_get"),w.invalidate("wyrm_context_build");const u={id:s.id,problem_type:h,description:a,steps:n.length};return S(u,(o,e)=>E(e.brand,"**Reasoning Scaffold Saved**")+`
72
72
 
73
- ${e.bullet} **Problem Type:** ${r.problem_type}
74
- ${e.bullet} **Description:** ${r.description}
75
- ${e.bullet} **Steps:** ${r.steps} checklist items
76
- ${e.bullet} **ID:** ${r.id}
73
+ ${e.bullet} **Problem Type:** ${o.problem_type}
74
+ ${e.bullet} **Description:** ${o.description}
75
+ ${e.bullet} **Steps:** ${o.steps} checklist items
76
+ ${e.bullet} **ID:** ${o.id}
77
77
 
78
- This scaffold will be surfaced by \`wyrm_context_build\` when tasks match "${r.problem_type}".`)}}];export{ve as captureToolSpecs};
78
+ This scaffold will be surfaced by \`wyrm_context_build\` when tasks match "${o.problem_type}".`)}}];export{ve as captureToolSpecs};
package/dist/harvest.js CHANGED
@@ -1,5 +1,5 @@
1
- import{execFileSync as m}from"child_process";import{readFileSync as h,existsSync as g}from"fs";import{join as p}from"path";import{createHash as x}from"crypto";const $=["README.md","CLAUDE.md","AGENTS.md","ARCHITECTURE.md"],k=40,u=o=>x("sha1").update(o).digest("hex").slice(0,8);function y(o){const s=[];for(const c of $){const e=p(o,c);if(!g(e))continue;let n;try{n=h(e,"utf-8")}catch{continue}for(const t of n.split(/\n(?=#{1,4}\s)/)){const i=t.match(/^#{1,4}\s+(.+)/);if(!i)continue;const d=/[\x00-\x1f\x7f]/g,r=i[1].replace(/[#*`]/g,"").replace(d,"").trim(),l=t.slice(i[0].length).split(`
2
- `).map(a=>a.trim()).filter(a=>a&&!a.startsWith("#")&&!a.startsWith("|")&&!a.startsWith("```")).slice(0,3).join(" ").replace(d,"").replace(/\s+/g," ");if(!r||l.length<24)continue;const f=`${r}: ${l}`.slice(0,600);if(s.push({kind:"lesson",text:f,source:`doc:${c}`,sig:`doc:${c}:${u(f)}`,tags:["harvest","doc",c],confidence:.6}),s.length>=k)return s}}return s}const C=/^(merge|wip|typo|fixup|squash|amend|\.+|chore: lint|format)\b/i;function S(o,s=30){if(!g(p(o,".git")))return[];let c;try{c=m("git",["-C",o,"log",`-n${Math.max(1,Math.min(s,200))}`,"--no-merges","--pretty=%h%x09%s"],{encoding:"utf-8",timeout:5e3})}catch{return[]}const e=[];for(const n of c.split(`
3
- `)){const t=n.trim();if(!t)continue;const i=t.indexOf(" ");if(i<0)continue;const d=t.slice(0,i),r=t.slice(i+1).trim();r.length<8||C.test(r)||e.push({kind:"pattern",text:r.slice(0,300),source:"git",sig:`git:${d}`,tags:["harvest","git",`sha:${d}`],confidence:.45})}return e}function v(o){const s=[],c=p(o,"package.json");if(g(c))try{const e=JSON.parse(h(c,"utf-8")),n=Object.keys({...e.dependencies,...e.devDependencies}).slice(0,16),t=Object.keys(e.scripts??{}).slice(0,10);if(n.length||t.length){const i=`Stack (package.json): ${n.join(", ")||"\u2014"}${t.length?` \xB7 scripts: ${t.join(", ")}`:""}`.slice(0,600);s.push({kind:"lesson",text:i,source:"code:package.json",sig:`code:pkg:${u(i)}`,tags:["harvest","code","stack"],confidence:.6})}}catch{}for(const e of["Cargo.toml","composer.json","requirements.txt","pyproject.toml","go.mod","wrangler.toml"]){const n=p(o,e);if(g(n))try{const t=h(n,"utf-8").split(`
4
- `).map(i=>i.trim()).filter(Boolean).slice(0,6).join(" ").replace(/\s+/g," ").slice(0,400);t&&s.push({kind:"lesson",text:`Stack (${e}): ${t}`,source:`code:${e}`,sig:`code:${e}:${u(t)}`,tags:["harvest","code","stack"],confidence:.55})}catch{}}if(g(p(o,".git")))try{const e=m("git",["-C",o,"grep","-nIE","\\b(TODO|FIXME|HACK|XXX)\\b","--","*.ts","*.tsx","*.js","*.py","*.rs","*.go","*.php","*.rb"],{encoding:"utf-8",timeout:6e3,maxBuffer:4194304}),n=/[\x00-\x1f\x7f]/g;let t=0;for(const i of e.split(`
5
- `)){if(t>=25)break;const d=i.trim();if(!d)continue;const r=d.match(/^([^:]+):(\d+):(.*)$/);if(!r)continue;const l=r[3].trim().replace(n,"").slice(0,200);if(l.length<8)continue;const f=`${r[1]}:${r[2]} \u2014 ${l}`;s.push({kind:"anti_pattern",text:f,source:"code:todo",sig:`code:todo:${r[1]}:${r[2]}:${u(l)}`,tags:["harvest","code","todo"],confidence:.4}),t++}}catch{}return s}function b(o,s,c={}){const e=y(s.path),n=S(s.path,c.gitLimit??30),t=c.includeCode?v(s.path):[],i=[...e,...n,...t];let d=0,r=0;const l=[],f=new Set;for(const a of i){if(f.has(a.sig)||o.existsBySig(s.id,a.sig)){r++;continue}f.add(a.sig),c.dryRun||o.addCandidate(s.id,a),d++,l.length<5&&l.push(`[${a.source}] ${a.text.slice(0,80)}`)}return{project:s.name,docFacts:e.length,commits:n.length,codeSignals:t.length,added:d,skipped:r,sample:l}}function R(o,s,c={}){const e=s.map(n=>b(o,n,c));return{reports:e,totalAdded:e.reduce((n,t)=>n+t.added,0),totalSkipped:e.reduce((n,t)=>n+t.skipped,0)}}export{v as harvestCodeSignals,y as harvestDocFacts,S as harvestGitCommits,b as harvestProject,R as harvestProjects};
1
+ import{execFileSync as m}from"child_process";import{readFileSync as h,existsSync as g}from"fs";import{join as u}from"path";import{createHash as x}from"crypto";const $=["README.md","CLAUDE.md","AGENTS.md","ARCHITECTURE.md"],k=40,p=o=>x("sha1").update(o).digest("hex").slice(0,8);function y(o){const n=[];for(const c of $){const e=u(o,c);if(!g(e))continue;let s;try{s=h(e,"utf-8")}catch{continue}for(const t of s.split(/\n(?=#{1,4}\s)/)){const i=t.match(/^#{1,4}\s+(.+)/);if(!i)continue;const d=/[\x00-\x1f\x7f]/g,r=i[1].replace(/[#*`]/g,"").replace(d,"").trim(),f=t.slice(i[0].length).split(`
2
+ `).map(a=>a.trim()).filter(a=>a&&!a.startsWith("#")&&!a.startsWith("|")&&!a.startsWith("```")).slice(0,3).join(" ").replace(d,"").replace(/\s+/g," ");if(!r||f.length<24)continue;const l=`${r}: ${f}`.slice(0,600);if(n.push({kind:"lesson",text:l,source:`doc:${c}`,sig:`doc:${c}:${p(l)}`,tags:["harvest","doc",c],confidence:.6}),n.length>=k)return n}}return n}const S=/^(merge|wip|typo|fixup|squash|amend|\.+|chore: lint|format)\b/i;function C(o,n=30){if(!g(u(o,".git")))return[];let c;try{c=m("git",["-C",o,"log",`-n${Math.max(1,Math.min(n,200))}`,"--no-merges","--pretty=%h%x09%s"],{encoding:"utf-8",timeout:5e3})}catch{return[]}const e=[];for(const s of c.split(`
3
+ `)){const t=s.trim();if(!t)continue;const i=t.indexOf(" ");if(i<0)continue;const d=t.slice(0,i),r=t.slice(i+1).trim();r.length<8||S.test(r)||e.push({kind:"pattern",text:r.slice(0,300),source:"git",sig:`git:${d}`,tags:["harvest","git",`sha:${d}`],confidence:.45})}return e}function v(o){const n=[],c=u(o,"package.json");if(g(c))try{const e=JSON.parse(h(c,"utf-8")),s=Object.keys({...e.dependencies,...e.devDependencies}).slice(0,16),t=Object.keys(e.scripts??{}).slice(0,10);if(s.length||t.length){const i=`Stack (package.json): ${s.join(", ")||"\u2014"}${t.length?` \xB7 scripts: ${t.join(", ")}`:""}`.slice(0,600);n.push({kind:"lesson",text:i,source:"code:package.json",sig:`code:pkg:${p(i)}`,tags:["harvest","code","stack"],confidence:.6})}}catch{}for(const e of["Cargo.toml","composer.json","requirements.txt","pyproject.toml","go.mod","wrangler.toml"]){const s=u(o,e);if(g(s))try{const t=h(s,"utf-8").split(`
4
+ `).map(i=>i.trim()).filter(Boolean).slice(0,6).join(" ").replace(/\s+/g," ").slice(0,400);t&&n.push({kind:"lesson",text:`Stack (${e}): ${t}`,source:`code:${e}`,sig:`code:${e}:${p(t)}`,tags:["harvest","code","stack"],confidence:.55})}catch{}}if(g(u(o,".git")))try{const e=m("git",["-C",o,"grep","-nIE","\\b(TODO|FIXME|HACK|XXX)\\b","--","*.ts","*.tsx","*.js","*.py","*.rs","*.go","*.php","*.rb"],{encoding:"utf-8",timeout:6e3,maxBuffer:4194304}),s=/[\x00-\x1f\x7f]/g;let t=0;for(const i of e.split(`
5
+ `)){if(t>=25)break;const d=i.trim();if(!d)continue;const r=d.match(/^([^:]+):(\d+):(.*)$/);if(!r)continue;const f=r[3].trim().replace(s,"").slice(0,200);if(f.length<8)continue;const l=`${r[1]}:${r[2]} \u2014 ${f}`;n.push({kind:"anti_pattern",text:l,source:"code:todo",sig:`code:todo:${r[1]}:${r[2]}:${p(f)}`,tags:["harvest","code","todo"],confidence:.4}),t++}}catch{}return n}function b(o,n,c={}){const e=y(n.path),s=C(n.path,c.gitLimit??30),t=c.includeCode?v(n.path):[],i=[...e,...s,...t];let d=0,r=0;const f=[],l=new Set;for(const a of i){if(l.has(a.sig)||o.existsBySig(n.id,a.sig)){r++,c.dryRun||o.onSkip?.(n.id,a);continue}l.add(a.sig),c.dryRun||o.addCandidate(n.id,a),d++,f.length<5&&f.push(`[${a.source}] ${a.text.slice(0,80)}`)}return{project:n.name,docFacts:e.length,commits:s.length,codeSignals:t.length,added:d,skipped:r,sample:f}}function j(o,n,c={}){const e=n.map(s=>b(o,s,c));return{reports:e,totalAdded:e.reduce((s,t)=>s+t.added,0),totalSkipped:e.reduce((s,t)=>s+t.skipped,0)}}export{v as harvestCodeSignals,y as harvestDocFacts,C as harvestGitCommits,b as harvestProject,j as harvestProjects};
@@ -1,9 +1,9 @@
1
- import{emitEvent as V}from"./events.js";import{sanitizeFtsQuery as $,buildFtsMatchQuery as x}from"./security.js";import{getActor as z}from"./handlers/boundary.js";import{buildPage as G,DEFAULT_PAGE_SIZE as X}from"./keyset.js";import{rerankConfigured as J,rerankCandidates as Q}from"./rerank.js";import{autoSummaryPenalty as U,recallRecencyWeight as B,recallRecencyHalfLifeDays as P,recencyMultiplier as K}from"./context-ranking.js";const Y=40,Z=10,q=.3;class ce{db;vectorStore;setVectorStore(t){this.vectorStore=t}getVectorStore(){return this.vectorStore}constructor(t){this.db=t}indexArtifact(t){if(!this.vectorStore)return;const e=`${t.problem}${t.validated_fix?" "+t.validated_fix:""}`.slice(0,2e3);this.vectorStore.addVector(e,"artifact",t.id,t.project_id).catch(()=>{})}add(t,e){if(typeof e.problem!="string"||!e.problem.trim())throw new Error("memory.add: 'problem' is required (a non-empty string).");const n=e.tags?.length?e.tags.join(","):null,i=z(),r=this.db.prepare(`
1
+ import{emitEvent as ne}from"./events.js";import{sanitizeFtsQuery as P,buildFtsMatchQuery as K}from"./security.js";import{getActor as se}from"./handlers/boundary.js";import{buildPage as ie,DEFAULT_PAGE_SIZE as re}from"./keyset.js";import{rerankConfigured as ae,rerankCandidates as ce}from"./rerank.js";import{autoSummaryPenalty as Y,recallRecencyWeight as j,recallRecencyHalfLifeDays as G,recencyMultiplier as V,extractTemporalWindow as z,recallTemporalWeight as X,temporalMultiplier as J,recallUsefulnessWeight as Q,usefulnessMultiplier as Z,looksInjectionShaped as oe,trustMarker as de}from"./context-ranking.js";import{queryGraphContext as le,graphMultiplier as fe,recallGraphWeight as ue}from"./graph-recall.js";const q=40,me=10,_e=.3;class Ce{db;vectorStore;setVectorStore(t){this.vectorStore=t}getVectorStore(){return this.vectorStore}constructor(t){this.db=t}indexArtifact(t){if(!this.vectorStore)return;const e=`${t.problem}${t.validated_fix?" "+t.validated_fix:""}`.slice(0,2e3);this.vectorStore.addVector(e,"artifact",t.id,t.project_id).catch(()=>{})}add(t,e){if(typeof e.problem!="string"||!e.problem.trim())throw new Error("memory.add: 'problem' is required (a non-empty string).");const n=e.tags?.length?e.tags.join(","):null,s=se(),r=e.createdBy??"local",a=e.sourceTrust??(r.startsWith("bridge:")?"operator":r.startsWith("import:")||(n??"").includes("imported_from:")?"imported":"agent");let o=e.needsReview??0,i=null;if(o===0&&a==="agent"&&s.agent_id&&(process.env.WYRM_PROBATION??"")==="1"){const d=Number(process.env.WYRM_PROBATION_HOURS),D=Number.isFinite(d)&&d>=1?d:24,p=this.db.prepare("SELECT MIN(created_at) AS f FROM memory_artifacts WHERE agent_id = ?").get(s.agent_id),N=p.f?new Date(p.f.replace(" ","T")+"Z").getTime():NaN;(!Number.isFinite(N)||Date.now()-N<D*36e5)&&(o=1,i="probation")}const m=i?n?`${n},${i}`:i:n,_=this.db.prepare(`
2
2
  INSERT INTO memory_artifacts
3
3
  (project_id, kind, problem, constraints, validated_fix, why_it_worked,
4
- outcome, source_session_id, tags, confidence, needs_review, created_by, agent_id, run_id)
5
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
6
- `).run(t,e.kind,e.problem.trim(),e.constraints?.trim()??null,e.validatedFix?.trim()??null,e.whyItWorked?.trim()??null,e.outcome??"neutral",e.sourceSessionId??null,n,e.confidence??1,e.needsReview??0,e.createdBy??"local",i.agent_id,i.run_id),s=this.get(r.lastInsertRowid);return e.needsReview||(V(this.db,{projectId:t,kind:"capture",refTable:"memory_artifacts",refId:s.id,isShared:!!s.is_shared}),this.indexArtifact(s)),s}get(t){return this.db.prepare("SELECT * FROM memory_artifacts WHERE id = ?").get(t)??null}update(t,e){return this.db.prepare(`
4
+ outcome, source_session_id, tags, confidence, needs_review, created_by, agent_id, run_id, source_trust)
5
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
6
+ `).run(t,e.kind,e.problem.trim(),e.constraints?.trim()??null,e.validatedFix?.trim()??null,e.whyItWorked?.trim()??null,e.outcome??"neutral",e.sourceSessionId??null,m,e.confidence??1,o,r,s.agent_id,s.run_id,a),c=this.get(_.lastInsertRowid);return o||(ne(this.db,{projectId:t,kind:"capture",refTable:"memory_artifacts",refId:c.id,isShared:!!c.is_shared}),this.indexArtifact(c)),c}get(t){return this.db.prepare("SELECT * FROM memory_artifacts WHERE id = ?").get(t)??null}update(t,e){return this.db.prepare(`
7
7
  UPDATE memory_artifacts SET
8
8
  confidence = COALESCE(?, confidence),
9
9
  validated_fix = COALESCE(?, validated_fix),
@@ -22,17 +22,17 @@ import{emitEvent as V}from"./events.js";import{sanitizeFtsQuery as $,buildFtsMat
22
22
  END,
23
23
  updated_at = datetime('now')
24
24
  WHERE id = ?
25
- `).run(e?1:0,t)}recall(t,e,n={}){const i=n.limit??10,r=n.minConfidence??0,s=this.searchByFts(t,e,Y,n.kind,r),o=this.searchByTags(t,e,Y,n.kind,r),a=new Set,u=[];s.forEach((l,c)=>{a.add(l.id),u.push({artifact:l,inFts:!0,inTag:!1,ftsRank:c})});for(const l of o)if(!a.has(l.id))a.add(l.id),u.push({artifact:l,inFts:!1,inTag:!0});else{const c=u.find(D=>D.artifact.id===l.id);c&&(c.inTag=!0)}return u.filter(({artifact:l})=>l.confidence>=r).map(({artifact:l,inFts:c,inTag:D,ftsRank:y})=>{let S;if(c){const w=1-(y??0)/(s.length+1);S=(D?.7:.55)+.45*w}else S=.4;S*=l.confidence,l.outcome==="positive"&&(S*=1.1);const h=(Date.now()-new Date(l.last_validated_at).getTime())/(1e3*60*60*24),R=Math.max(.5,1-h/180);return S*=R,{artifact:l,relevance_score:Math.min(1,S),match_type:c&&D?"both":c?"fts":"tag"}}).sort((l,c)=>c.relevance_score-l.relevance_score).slice(0,i)}async recallHybrid(t,e,n={}){const i=n.limit??10,r=n.minConfidence??0,s=n.rerank===!0&&J(),o=Math.min(Math.max(i*2,20),50),a=s?o:i;if(!this.vectorStore)return this.recall(t,e,n);const u=Math.max(50,a*5),E=Date.now(),l=this.searchByFts(t,e,u,n.kind,r),c=Date.now()-E,D=Date.now();let y=[];try{y=(await this.vectorStore.search(e,u,t,["artifact"])).map(p=>({id:p.content_id,s:p.similarity}))}catch{}const S=Date.now()-D;if(y.length===0)return this.recall(t,e,n);const h=Date.now(),R=60,w=Number(process.env.WYRM_RERANK_ALPHA),L=Number.isFinite(w)?Math.max(0,Math.min(1,w)):.7,m=new Map;if(process.env.WYRM_RERANK_FUSION==="rrf")l.forEach((f,p)=>m.set(f.id,(m.get(f.id)??0)+1/(R+p+1))),y.forEach((f,p)=>m.set(f.id,(m.get(f.id)??0)+1/(R+p+1)));else{const f=l.length+1;l.forEach((v,j)=>m.set(v.id,(m.get(v.id)??0)+(1-L)*(1-j/f)));const p=y.map(v=>v.s),A=Math.min(...p),F=Math.max(...p),W=F-A;y.forEach(v=>m.set(v.id,(m.get(v.id)??0)+L*(W>1e-9?(v.s-A)/W:1)))}const C=new Set(l.map(f=>f.id)),I=new Set(y.map(f=>f.id)),k=new Map(l.map(f=>[f.id,f])),d=[...m.entries()].sort((f,p)=>p[1]-f[1]),_=B(),g=P(),b=new Date,T=Math.min(d.length,Math.max(a*4,50)),N=[];for(const[f,p]of d.slice(0,T)){const A=k.get(f)??this.get(f)??void 0;A&&(A.confidence<r||A.needs_review===1||A.supersedes_id!=null||n.kind&&A.kind!==n.kind||N.push({id:f,s:p*K(A.updated_at??A.created_at,_,g,b),art:A}))}N.sort((f,p)=>p.s-f.s);const O=N[0]?.s??1,M=[];for(const{id:f,s:p,art:A}of N){if(M.length>=a)break;const F=C.has(f)&&I.has(f)?"hybrid":I.has(f)?"vector":"fts";M.push({artifact:A,relevance_score:Math.min(1,p/O),match_type:F})}const H=Date.now()-h;if(s&&M.length>0){const f=Date.now(),p=await this.rerankResults(e,M);return n.onStats?.({ftsMs:c,vectorMs:S,fusionMs:H,rerankMs:Date.now()-f,candidates:M.length}),p.slice(0,i)}return n.onStats?.({ftsMs:c,vectorMs:S,fusionMs:H,candidates:M.length}),M}async rerankResults(t,e){const n=e.map(a=>({id:a.artifact.id,text:`${a.artifact.problem}${a.artifact.validated_fix?" "+a.artifact.validated_fix:""}`.slice(0,2e3)})),i=await Q(t,n);if(!i)return e;const r=new Map(e.map(a=>[a.artifact.id,a])),s=[],o=i.length;return i.forEach((a,u)=>{const E=r.get(a.id);E&&s.push({...E,relevance_score:o>0?(o-u)/o:E.relevance_score})}),s}async recallHybridGlobal(t,e={}){const n=e.limit??10,i=e.minConfidence??0,r=Math.max(50,n*5),s=this.searchByFtsGlobal(t,r,e.kind,i),o=()=>s.slice(0,n).map((d,_)=>({artifact:d,relevance_score:1-_/(s.length+1),match_type:"fts"}));if(!this.vectorStore)return o();let a=[];try{a=(await this.vectorStore.search(t,r,void 0,["artifact"])).map(_=>({id:_.content_id,s:_.similarity}))}catch{}if(a.length===0)return o();const u=60,E=Number(process.env.WYRM_RERANK_ALPHA),l=Number.isFinite(E)?Math.max(0,Math.min(1,E)):.7,c=new Map;if(process.env.WYRM_RERANK_FUSION==="rrf")s.forEach((d,_)=>c.set(d.id,(c.get(d.id)??0)+1/(u+_+1))),a.forEach((d,_)=>c.set(d.id,(c.get(d.id)??0)+1/(u+_+1)));else{const d=s.length+1;s.forEach((N,O)=>c.set(N.id,(c.get(N.id)??0)+(1-l)*(1-O/d)));const _=a.map(N=>N.s),g=Math.min(..._),b=Math.max(..._),T=b-g;a.forEach(N=>c.set(N.id,(c.get(N.id)??0)+l*(T>1e-9?(N.s-g)/T:1)))}const D=new Set(s.map(d=>d.id)),y=new Set(a.map(d=>d.id)),S=new Map(s.map(d=>[d.id,d])),h=[...c.entries()].sort((d,_)=>_[1]-d[1]),R=B(),w=P(),L=new Date,m=Math.min(h.length,Math.max(n*4,50)),C=[];for(const[d,_]of h.slice(0,m)){const g=S.get(d)??this.get(d)??void 0;g&&(g.confidence<i||g.needs_review===1||g.supersedes_id!=null||e.kind&&g.kind!==e.kind||C.push({id:d,s:_*K(g.updated_at??g.created_at,R,w,L),art:g}))}C.sort((d,_)=>_.s-d.s);const I=C[0]?.s??1,k=[];for(const{id:d,s:_,art:g}of C){if(k.length>=n)break;const b=D.has(d)&&y.has(d)?"hybrid":y.has(d)?"vector":"fts";k.push({artifact:g,relevance_score:Math.min(1,_/I),match_type:b})}return k}searchByFtsGlobal(t,e,n,i=0){if(!this.db)return[];let r="";try{const a=$(t);r=a?x(a):""}catch{return[]}if(!r)return[];const s=n?"AND a.kind = ?":"",o=[r,i];n&&o.push(n),o.push(e);try{return this.db.prepare(`
25
+ `).run(e?1:0,t)}recall(t,e,n={}){const s=n.limit??10,r=n.minConfidence??0,a=this.searchByFts(t,e,q,n.kind,r),o=this.searchByTags(t,e,q,n.kind,r),i=new Set,m=[];a.forEach((c,d)=>{i.add(c.id),m.push({artifact:c,inFts:!0,inTag:!1,ftsRank:d})});for(const c of o)if(!i.has(c.id))i.add(c.id),m.push({artifact:c,inFts:!1,inTag:!0});else{const d=m.find(D=>D.artifact.id===c.id);d&&(d.inTag=!0)}return m.filter(({artifact:c})=>c.confidence>=r).map(({artifact:c,inFts:d,inTag:D,ftsRank:p})=>{let N;if(d){const u=1-(p??0)/(a.length+1);N=(D?.7:.55)+.45*u}else N=.4;N*=c.confidence,c.outcome==="positive"&&(N*=1.1);const M=(Date.now()-new Date(c.last_validated_at).getTime())/(1e3*60*60*24),w=Math.max(.5,1-M/180);return N*=w,{artifact:c,relevance_score:Math.min(1,N),match_type:d&&D?"both":d?"fts":"tag"}}).sort((c,d)=>d.relevance_score-c.relevance_score).slice(0,s)}async recallHybrid(t,e,n={}){const s=n.limit??10,r=n.minConfidence??0,a=n.rerank===!0&&ae(),o=Math.min(Math.max(s*2,20),50),i=a?o:s;if(!this.vectorStore)return this.recall(t,e,n);const m=Math.max(50,i*5),_=Date.now(),c=this.searchByFts(t,e,m,n.kind,r),d=Date.now()-_,D=Date.now();let p=[];try{p=(await this.vectorStore.search(e,m,t,["artifact"])).map(E=>({id:E.content_id,s:E.similarity}))}catch{}const N=Date.now()-D;if(p.length===0)return this.recall(t,e,n);const M=Date.now(),w=60,u=Number(process.env.WYRM_RERANK_ALPHA),g=Number.isFinite(u)?Math.max(0,Math.min(1,u)):.7,A=new Map;if(process.env.WYRM_RERANK_FUSION==="rrf")c.forEach((f,E)=>A.set(f.id,(A.get(f.id)??0)+1/(w+E+1))),p.forEach((f,E)=>A.set(f.id,(A.get(f.id)??0)+1/(w+E+1)));else{const f=c.length+1;c.forEach((L,te)=>A.set(L.id,(A.get(L.id)??0)+(1-g)*(1-te/f)));const E=p.map(L=>L.s),R=Math.min(...E),F=Math.max(...E),I=F-R;p.forEach(L=>A.set(L.id,(A.get(L.id)??0)+g*(I>1e-9?(L.s-R)/I:1)))}const O=new Set(c.map(f=>f.id)),C=new Set(p.map(f=>f.id)),$=new Map(c.map(f=>[f.id,f])),y=[...A.entries()].sort((f,E)=>E[1]-f[1]),x=j(),W=G(),l=X(),h=l>0?z(e):null,S=Q(),k=ue(),b=k>0?le(this.db,t,e):null,v=new Date,U=Math.min(y.length,Math.max(i*4,50)),H=[];for(const[f,E]of y.slice(0,U)){const R=$.get(f)??this.get(f)??void 0;if(!R||R.confidence<r||R.needs_review===1||R.supersedes_id!=null||n.kind&&R.kind!==n.kind)continue;const F=R.updated_at??R.created_at;let I=E*V(F,x,W,v);h&&(I*=J(R.created_at,h,l,v)),I*=Z(R.reuse_count,R.reuse_success_count,S),b&&(I*=fe(`${R.problem} ${R.validated_fix??""}`,b,k)),H.push({id:f,s:I,art:R})}H.sort((f,E)=>E.s-f.s);const ee=H[0]?.s??1,T=[];for(const{id:f,s:E,art:R}of H){if(T.length>=i)break;const F=O.has(f)&&C.has(f)?"hybrid":C.has(f)?"vector":"fts";T.push({artifact:R,relevance_score:Math.min(1,E/ee),match_type:F})}const B=Date.now()-M;if(a&&T.length>0){const f=Date.now(),E=await this.rerankResults(e,T);return n.onStats?.({ftsMs:d,vectorMs:N,fusionMs:B,rerankMs:Date.now()-f,candidates:T.length}),E.slice(0,s)}return n.onStats?.({ftsMs:d,vectorMs:N,fusionMs:B,candidates:T.length}),T}async rerankResults(t,e){const n=e.map(i=>({id:i.artifact.id,text:`${i.artifact.problem}${i.artifact.validated_fix?" "+i.artifact.validated_fix:""}`.slice(0,2e3)})),s=await ce(t,n);if(!s)return e;const r=new Map(e.map(i=>[i.artifact.id,i])),a=[],o=s.length;return s.forEach((i,m)=>{const _=r.get(i.id);_&&a.push({..._,relevance_score:o>0?(o-m)/o:_.relevance_score})}),a}async recallHybridGlobal(t,e={}){const n=e.limit??10,s=e.minConfidence??0,r=Math.max(50,n*5),a=this.searchByFtsGlobal(t,r,e.kind,s),o=()=>a.slice(0,n).map((l,h)=>({artifact:l,relevance_score:1-h/(a.length+1),match_type:"fts"}));if(!this.vectorStore)return o();let i=[];try{i=(await this.vectorStore.search(t,r,void 0,["artifact"])).map(h=>({id:h.content_id,s:h.similarity}))}catch{}if(i.length===0)return o();const m=60,_=Number(process.env.WYRM_RERANK_ALPHA),c=Number.isFinite(_)?Math.max(0,Math.min(1,_)):.7,d=new Map;if(process.env.WYRM_RERANK_FUSION==="rrf")a.forEach((l,h)=>d.set(l.id,(d.get(l.id)??0)+1/(m+h+1))),i.forEach((l,h)=>d.set(l.id,(d.get(l.id)??0)+1/(m+h+1)));else{const l=a.length+1;a.forEach((v,U)=>d.set(v.id,(d.get(v.id)??0)+(1-c)*(1-U/l)));const h=i.map(v=>v.s),S=Math.min(...h),k=Math.max(...h),b=k-S;i.forEach(v=>d.set(v.id,(d.get(v.id)??0)+c*(b>1e-9?(v.s-S)/b:1)))}const D=new Set(a.map(l=>l.id)),p=new Set(i.map(l=>l.id)),N=new Map(a.map(l=>[l.id,l])),M=[...d.entries()].sort((l,h)=>h[1]-l[1]),w=j(),u=G(),g=X(),A=g>0?z(t):null,O=Q(),C=new Date,$=Math.min(M.length,Math.max(n*4,50)),y=[];for(const[l,h]of M.slice(0,$)){const S=N.get(l)??this.get(l)??void 0;if(!S||S.confidence<s||S.needs_review===1||S.supersedes_id!=null||e.kind&&S.kind!==e.kind)continue;const k=S.updated_at??S.created_at;let b=h*V(k,w,u,C);A&&(b*=J(S.created_at,A,g,C)),b*=Z(S.reuse_count,S.reuse_success_count,O),y.push({id:l,s:b,art:S})}y.sort((l,h)=>h.s-l.s);const x=y[0]?.s??1,W=[];for(const{id:l,s:h,art:S}of y){if(W.length>=n)break;const k=D.has(l)&&p.has(l)?"hybrid":p.has(l)?"vector":"fts";W.push({artifact:S,relevance_score:Math.min(1,h/x),match_type:k})}return W}searchByFtsGlobal(t,e,n,s=0){if(!this.db)return[];let r="";try{const i=P(t);r=i?K(i):""}catch{return[]}if(!r)return[];const a=n?"AND a.kind = ?":"",o=[r,s];n&&o.push(n),o.push(e);try{return this.db.prepare(`
26
26
  SELECT a.* FROM memory_artifacts a
27
27
  JOIN memory_artifacts_fts ON a.id = memory_artifacts_fts.rowid
28
28
  WHERE memory_artifacts_fts MATCH ?
29
29
  AND a.confidence >= ?
30
30
  AND a.supersedes_id IS NULL
31
31
  AND a.needs_review = 0
32
- ${s}
32
+ ${a}
33
33
  ORDER BY rank, a.confidence DESC
34
34
  LIMIT ?
35
- `).all(...o)}catch{return[]}}searchByFts(t,e,n,i,r=0){let s="";try{const u=$(e);s=u?x(u):""}catch{return this.listRecent(t,n,i)}if(!s)return this.listRecent(t,n,i);const o=i?"AND a.kind = ?":"",a=[s,t,r];i&&a.push(i),a.push(n);try{return this.db.prepare(`
35
+ `).all(...o)}catch{return[]}}searchByFts(t,e,n,s,r=0){let a="";try{const m=P(e);a=m?K(m):""}catch{return this.listRecent(t,n,s)}if(!a)return this.listRecent(t,n,s);const o=s?"AND a.kind = ?":"",i=[a,t,r];s&&i.push(s),i.push(n);try{return this.db.prepare(`
36
36
  SELECT a.* FROM memory_artifacts a
37
37
  JOIN memory_artifacts_fts ON a.id = memory_artifacts_fts.rowid
38
38
  WHERE memory_artifacts_fts MATCH ?
@@ -43,43 +43,43 @@ import{emitEvent as V}from"./events.js";import{sanitizeFtsQuery as $,buildFtsMat
43
43
  ${o}
44
44
  ORDER BY rank, a.confidence DESC
45
45
  LIMIT ?
46
- `).all(...a)}catch{return this.listRecent(t,n,i)}}searchByTags(t,e,n,i,r=0){const s=e.toLowerCase().split(/\s+/).filter(c=>c.length>2);if(!s.length)return[];const o=c=>c.replace(/[%_\\]/g,"\\$&"),a=s.map(()=>"LOWER(tags) LIKE ? ESCAPE '\\'").join(" OR "),u=s.map(c=>`%${o(c)}%`),E=i?"AND kind = ?":"",l=[t,r,...u];return i&&l.push(i),l.push(n),this.db.prepare(`
46
+ `).all(...i)}catch{return this.listRecent(t,n,s)}}searchByTags(t,e,n,s,r=0){const a=e.toLowerCase().split(/\s+/).filter(d=>d.length>2);if(!a.length)return[];const o=d=>d.replace(/[%_\\]/g,"\\$&"),i=a.map(()=>"LOWER(tags) LIKE ? ESCAPE '\\'").join(" OR "),m=a.map(d=>`%${o(d)}%`),_=s?"AND kind = ?":"",c=[t,r,...m];return s&&c.push(s),c.push(n),this.db.prepare(`
47
47
  SELECT * FROM memory_artifacts
48
48
  WHERE project_id = ? AND confidence >= ? AND supersedes_id IS NULL
49
49
  AND needs_review = 0
50
- AND (${a})
51
- ${E}
50
+ AND (${i})
51
+ ${_}
52
52
  ORDER BY confidence DESC, reuse_success_count DESC
53
53
  LIMIT ?
54
- `).all(...l)}listRecent(t,e=20,n){const i=n?"AND kind = ?":"",r=[t];return n&&r.push(n),r.push(e),this.db.prepare(`
54
+ `).all(...c)}listRecent(t,e=20,n){const s=n?"AND kind = ?":"",r=[t];return n&&r.push(n),r.push(e),this.db.prepare(`
55
55
  SELECT * FROM memory_artifacts
56
56
  WHERE project_id = ? AND supersedes_id IS NULL AND needs_review = 0
57
- ${i}
57
+ ${s}
58
58
  ORDER BY confidence DESC, created_at DESC
59
59
  LIMIT ?
60
- `).all(...r)}buildContextBrief(t,e,n={}){const i=n.kinds??["pattern","heuristic","reasoning_trace","lesson","anti_pattern"],r=n.maxItems??Z,s=n.minConfidence??q,o=this.recall(t,e,{limit:r*2,minConfidence:s});o.sort((h,R)=>(U(h.artifact.problem)>0?1:0)-(U(R.artifact.problem)>0?1:0));const a=o.filter(h=>i.includes(h.artifact.kind)).slice(0,r),u=new Map;for(const h of a){const R=u.get(h.artifact.kind)??[];R.push(h),u.set(h.artifact.kind,R)}const E={pattern:"\u2705 Proven Patterns",heuristic:"\u{1F4A1} Heuristics",reasoning_trace:"\u{1F9E0} Past Reasoning",lesson:"\u{1F4DA} Lessons Learned",anti_pattern:"\u26A0\uFE0F Anti-Patterns to Avoid"},l=["pattern","heuristic","reasoning_trace","lesson","anti_pattern"],c=[],D=[];let y=0;for(const h of l){if(!i.includes(h))continue;const R=u.get(h)??[];if(!R.length)continue;const w=[];for(const L of R){if(y>=r)break;const m=L.artifact;let C=`**Problem:** ${m.problem}`;m.constraints&&(C+=`
61
- _Constraints:_ ${m.constraints}`),m.validated_fix&&(C+=`
62
- _Solution:_ ${m.validated_fix}`),m.why_it_worked&&(C+=`
63
- _Why it worked:_ ${m.why_it_worked}`),m.outcome==="negative"&&(C+=`
64
- _Note: This approach failed \u2014 avoid it_`),w.push(C),D.push(m.id),y++}w.length&&c.push({heading:E[h],items:w,source:h})}let S="";if(c.length>0){S+=`---
60
+ `).all(...r)}buildContextBrief(t,e,n={}){const s=n.kinds??["pattern","heuristic","reasoning_trace","lesson","anti_pattern"],r=n.maxItems??me,a=n.minConfidence??_e,o=this.recall(t,e,{limit:r*2,minConfidence:a});o.sort((u,g)=>(Y(u.artifact.problem)>0?1:0)-(Y(g.artifact.problem)>0?1:0));const i=[],_=o.filter(u=>{const g=u.artifact.source_trust;return(g==="imported"||g==="untrusted")&&oe(`${u.artifact.problem} ${u.artifact.validated_fix??""}`)?(i.push(u.artifact.id),!1):!0}).filter(u=>s.includes(u.artifact.kind)).slice(0,r),c=new Map;for(const u of _){const g=c.get(u.artifact.kind)??[];g.push(u),c.set(u.artifact.kind,g)}const d={pattern:"\u2705 Proven Patterns",heuristic:"\u{1F4A1} Heuristics",reasoning_trace:"\u{1F9E0} Past Reasoning",lesson:"\u{1F4DA} Lessons Learned",anti_pattern:"\u26A0\uFE0F Anti-Patterns to Avoid"},D=["pattern","heuristic","reasoning_trace","lesson","anti_pattern"],p=[],N=[];let M=0;for(const u of D){if(!s.includes(u))continue;const g=c.get(u)??[];if(!g.length)continue;const A=[];for(const O of g){if(M>=r)break;const C=O.artifact;let y=`${de(C.source_trust)}**Problem:** ${C.problem}`;C.constraints&&(y+=`
61
+ _Constraints:_ ${C.constraints}`),C.validated_fix&&(y+=`
62
+ _Solution:_ ${C.validated_fix}`),C.why_it_worked&&(y+=`
63
+ _Why it worked:_ ${C.why_it_worked}`),C.outcome==="negative"&&(y+=`
64
+ _Note: This approach failed \u2014 avoid it_`),A.push(y),N.push(C.id),M++}A.length&&p.push({heading:d[u],items:A,source:u})}let w="";if(p.length>0){w+=`---
65
65
  ## \u{F115D} Memory Brief
66
66
  _Relevant past knowledge from Wyrm:_
67
67
 
68
- `;for(const h of c){S+=`### ${h.heading}
69
- `;for(const R of h.items)S+=`- ${R}
70
- `;S+=`
71
- `}S+=`---
72
- `}return{sections:c,text:S,sourceIds:D}}getStats(t){const e=this.db.prepare("SELECT COUNT(*) as n FROM memory_artifacts WHERE project_id = ? AND supersedes_id IS NULL").get(t).n,n=this.db.prepare("SELECT COUNT(*) as n FROM memory_artifacts WHERE project_id = ? AND supersedes_id IS NOT NULL").get(t).n,i=this.db.prepare("SELECT AVG(confidence) as v FROM memory_artifacts WHERE project_id = ? AND supersedes_id IS NULL").get(t).v??0,r=this.db.prepare("SELECT kind, COUNT(*) as cnt FROM memory_artifacts WHERE project_id = ? AND supersedes_id IS NULL GROUP BY kind").all(t),s={};for(const o of r)s[o.kind]=o.cnt;return{total:e,byKind:s,avgConfidence:Math.round(i*100)/100,supersededCount:n}}listAll(t,e={}){const n=e.kind?"AND kind = ?":"",i=e.includeSuperseded?"":"AND supersedes_id IS NULL",r=[t];return e.kind&&r.push(e.kind),r.push(e.limit??50),this.db.prepare(`
68
+ `;for(const u of p){w+=`### ${u.heading}
69
+ `;for(const g of u.items)w+=`- ${g}
70
+ `;w+=`
71
+ `}w+=`---
72
+ `}return{sections:p,text:w,sourceIds:N}}getStats(t){const e=this.db.prepare("SELECT COUNT(*) as n FROM memory_artifacts WHERE project_id = ? AND supersedes_id IS NULL").get(t).n,n=this.db.prepare("SELECT COUNT(*) as n FROM memory_artifacts WHERE project_id = ? AND supersedes_id IS NOT NULL").get(t).n,s=this.db.prepare("SELECT AVG(confidence) as v FROM memory_artifacts WHERE project_id = ? AND supersedes_id IS NULL").get(t).v??0,r=this.db.prepare("SELECT kind, COUNT(*) as cnt FROM memory_artifacts WHERE project_id = ? AND supersedes_id IS NULL GROUP BY kind").all(t),a={};for(const o of r)a[o.kind]=o.cnt;return{total:e,byKind:a,avgConfidence:Math.round(s*100)/100,supersededCount:n}}listAll(t,e={}){const n=e.kind?"AND kind = ?":"",s=e.includeSuperseded?"":"AND supersedes_id IS NULL",r=[t];return e.kind&&r.push(e.kind),r.push(e.limit??50),this.db.prepare(`
73
73
  SELECT * FROM memory_artifacts
74
- WHERE project_id = ? ${n} ${i}
74
+ WHERE project_id = ? ${n} ${s}
75
75
  ORDER BY confidence DESC, created_at DESC
76
76
  LIMIT ?
77
- `).all(...r)}listPage(t,e={pageSize:X}){const n=e.kind?"AND kind = ?":"",i=e.includeSuperseded?"":"AND supersedes_id IS NULL",r="AND needs_review = 0",s=[t];e.kind&&s.push(e.kind);let o="";if(e.after){const E=Number(e.after.sortKey);Number.isFinite(E)&&(o="AND (confidence < ? OR (confidence = ? AND id < ?))",s.push(E,E,e.after.id))}const a=e.pageSize+1;s.push(a);const u=this.db.prepare(`
77
+ `).all(...r)}listPage(t,e={pageSize:re}){const n=e.kind?"AND kind = ?":"",s=e.includeSuperseded?"":"AND supersedes_id IS NULL",r="AND needs_review = 0",a=[t];e.kind&&a.push(e.kind);let o="";if(e.after){const _=Number(e.after.sortKey);Number.isFinite(_)&&(o="AND (confidence < ? OR (confidence = ? AND id < ?))",a.push(_,_,e.after.id))}const i=e.pageSize+1;a.push(i);const m=this.db.prepare(`
78
78
  SELECT * FROM memory_artifacts
79
- WHERE project_id = ? ${n} ${i} ${r} ${o}
79
+ WHERE project_id = ? ${n} ${s} ${r} ${o}
80
80
  ORDER BY confidence DESC, id DESC
81
81
  LIMIT ?
82
- `).all(...s);return G(u,e.pageSize,E=>({sortKey:String(E.confidence),id:E.id}))}pruneStale(t={}){const e=t.minConfidence??.3,n=t.olderThanDays??90,i=t.dryRun??!0,r=t.projectId!=null,s=[e,`-${n} days`];r&&s.push(t.projectId);const o=this.db.prepare(`
82
+ `).all(...a);return ie(m,e.pageSize,_=>({sortKey:String(_.confidence),id:_.id}))}pruneStale(t={}){const e=t.minConfidence??.3,n=t.olderThanDays??90,s=t.dryRun??!0,r=t.projectId!=null,a=[e,`-${n} days`];r&&a.push(t.projectId);const o=this.db.prepare(`
83
83
  SELECT id, kind, problem, confidence, last_accessed_at
84
84
  FROM memory_artifacts
85
85
  WHERE confidence < ?
@@ -89,4 +89,4 @@ _Relevant past knowledge from Wyrm:_
89
89
  ${r?"AND project_id = ?":""}
90
90
  ORDER BY confidence ASC
91
91
  LIMIT 500
92
- `).all(...s);return i||o.length===0?{candidates:o,deleted:0,dryRun:i}:{candidates:o,deleted:this.deleteArtifacts(o.map(a=>a.id)),dryRun:i}}deleteArtifacts(t){if(t.length===0)return 0;const e=t.map(()=>"?").join(",");return this.db.prepare(`DELETE FROM memory_artifacts WHERE id IN (${e}) AND needs_review = 0`).run(...t).changes}}export{ce as MemoryArtifacts};
92
+ `).all(...a);return s||o.length===0?{candidates:o,deleted:0,dryRun:s}:{candidates:o,deleted:this.deleteArtifacts(o.map(i=>i.id)),dryRun:s}}deleteArtifacts(t){if(t.length===0)return 0;const e=t.map(()=>"?").join(",");return this.db.prepare(`DELETE FROM memory_artifacts WHERE id IN (${e}) AND needs_review = 0`).run(...t).changes}}export{Ce as MemoryArtifacts};
@@ -1113,6 +1113,13 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
1113
1113
  );
1114
1114
  CREATE INDEX IF NOT EXISTS idx_write_ledger_ts ON write_ledger(ts);
1115
1115
  CREATE INDEX IF NOT EXISTS idx_write_ledger_tool ON write_ledger(tool, ts);
1116
+ `)}},{version:33,description:"v8 F5 \u2014 source_trust provenance lane on memory artifacts (v8-A5): operator > agent > imported > untrusted. Conservative backfill: bridge:* (the operator's own daily record) \u2192 operator; import:*/imported_from tags \u2192 imported; everything else stays agent. Unknown NEVER backfills to operator.",up:e=>{e.prepare("PRAGMA table_info(memory_artifacts)").all().some(E=>E.name==="source_trust")||e.exec(`
1117
+ ALTER TABLE memory_artifacts ADD COLUMN source_trust TEXT NOT NULL DEFAULT 'agent'
1118
+ CHECK (source_trust IN ('operator', 'agent', 'imported', 'untrusted'));
1119
+ `),e.exec(`
1120
+ UPDATE memory_artifacts SET source_trust = 'operator' WHERE created_by LIKE 'bridge:%';
1121
+ UPDATE memory_artifacts SET source_trust = 'imported'
1122
+ WHERE created_by LIKE 'import:%' OR tags LIKE '%imported_from:%';
1116
1123
  `)}}];function d(e){e.exec(`
1117
1124
  CREATE TABLE IF NOT EXISTS schema_versions (
1118
1125
  version INTEGER PRIMARY KEY,
package/dist/presence.js CHANGED
@@ -1,4 +1,4 @@
1
- import{readFileSync as l}from"node:fs";import{getActor as s}from"./handlers/boundary.js";function i(r){if(!Number.isInteger(r)||r<=0)return null;try{const e=l(`/proc/${r}/stat`,"utf8");return e.slice(e.lastIndexOf(")")+2).trimEnd().split(" ")[19]??null}catch{return null}}function c(r,e){const t=i(r);return t!==null&&t===e}function _(r){try{const t=r.metadata?JSON.parse(r.metadata):null;if(t&&typeof t.pid=="number"&&typeof t.pid_start=="string")return c(t.pid,t.pid_start)?"alive-pid":"dead-pid"}catch{}return(Date.now()-new Date(r.last_heartbeat.replace(" ","T")+"Z").getTime())/1e3<r.ttl_seconds?"alive-ttl":"stale"}class E{db;constructor(e){this.db=e}membershipRole(e,t){if(!e)return null;try{return this.db.prepare("SELECT role FROM run_agents WHERE run_id = ? AND agent_id = ?").get(e,t)?.role??null}catch{return null}}announce(e){const t=e.metadata?JSON.stringify(e.metadata):null,n=s().run_id,a=e.role??this.membershipRole(n,e.agent_id);return this.db.prepare(`
1
+ import{readFileSync as u}from"node:fs";import{getActor as c}from"./handlers/boundary.js";function _(r){if(!Number.isInteger(r)||r<=0)return null;try{const e=u(`/proc/${r}/stat`,"utf8");return e.slice(e.lastIndexOf(")")+2).trimEnd().split(" ")[19]??null}catch{return null}}function o(r,e){const t=_(r);return t!==null&&t===e}function d(r){try{const t=r.metadata?JSON.parse(r.metadata):null;if(t&&typeof t.pid=="number"&&typeof t.pid_start=="string")return o(t.pid,t.pid_start)?"alive-pid":"dead-pid"}catch{}return(Date.now()-new Date(r.last_heartbeat.replace(" ","T")+"Z").getTime())/1e3<r.ttl_seconds?"alive-ttl":"stale"}class g{db;constructor(e){this.db=e}membershipRole(e,t){if(!e)return null;try{return this.db.prepare("SELECT role FROM run_agents WHERE run_id = ? AND agent_id = ?").get(e,t)?.role??null}catch{return null}}announce(e){const t=e.metadata?JSON.stringify(e.metadata):null,n=c().run_id,a=e.role??this.membershipRole(n,e.agent_id);return this.db.prepare(`
2
2
  INSERT INTO agent_presence
3
3
  (agent_id, agent_kind, project_id, current_file, current_symbol,
4
4
  current_quest_id, ttl_seconds, metadata, run_id, role)
@@ -14,23 +14,15 @@ import{readFileSync as l}from"node:fs";import{getActor as s}from"./handlers/boun
14
14
  run_id = excluded.run_id,
15
15
  role = excluded.role,
16
16
  last_heartbeat = datetime('now')
17
- `).run(e.agent_id,e.agent_kind,e.project_id??null,e.current_file??null,e.current_symbol??null,e.current_quest_id??null,e.ttl_seconds??300,t,n,a),this.get(e.agent_id)}get(e){return this.db.prepare("SELECT * FROM agent_presence WHERE agent_id = ?").get(e)??null}liveAgents(e){const t=e==null?`SELECT * FROM agent_presence
18
- WHERE (julianday('now') - julianday(last_heartbeat)) * 86400 < ttl_seconds
19
- ORDER BY last_heartbeat DESC`:`SELECT * FROM agent_presence
20
- WHERE project_id = ?
21
- AND (julianday('now') - julianday(last_heartbeat)) * 86400 < ttl_seconds
22
- ORDER BY last_heartbeat DESC`;return e==null?this.db.prepare(t).all():this.db.prepare(t).all(e)}agentsOnFile(e){return this.db.prepare(`
17
+ `).run(e.agent_id,e.agent_kind,e.project_id??null,e.current_file??null,e.current_symbol??null,e.current_quest_id??null,e.ttl_seconds??300,t,n,a),this.get(e.agent_id)}get(e){return this.db.prepare("SELECT * FROM agent_presence WHERE agent_id = ?").get(e)??null}liveAgents(e){const t=e==null?"SELECT * FROM agent_presence ORDER BY last_heartbeat DESC":"SELECT * FROM agent_presence WHERE project_id = ? ORDER BY last_heartbeat DESC";return(e==null?this.db.prepare(t).all():this.db.prepare(t).all(e)).filter(a=>{const s=d(a);return s==="alive-pid"||s==="alive-ttl"})}agentsOnFile(e){return this.db.prepare(`
23
18
  SELECT * FROM agent_presence
24
19
  WHERE current_file = ?
25
20
  AND (julianday('now') - julianday(last_heartbeat)) * 86400 < ttl_seconds
26
21
  ORDER BY last_heartbeat DESC
27
- `).all(e)}release(e){return this.db.prepare("DELETE FROM quest_claims WHERE agent_id = ?").run(e),this.db.prepare("DELETE FROM agent_presence WHERE agent_id = ?").run(e).changes>0}reap(){const e=this.db.prepare(`
28
- DELETE FROM agent_presence
29
- WHERE (julianday('now') - julianday(last_heartbeat)) * 86400 >= ttl_seconds
30
- `).run(),t=this.db.prepare(`
22
+ `).all(e)}release(e){return this.db.prepare("DELETE FROM quest_claims WHERE agent_id = ?").run(e),this.db.prepare("DELETE FROM agent_presence WHERE agent_id = ?").run(e).changes>0}reap(){const e=this.db.prepare("SELECT * FROM agent_presence").all(),t=this.db.prepare("DELETE FROM agent_presence WHERE id = ?");let n=0;for(const l of e){const i=d(l);(i==="dead-pid"||i==="stale")&&(t.run(l.id),n++)}const a={changes:n},s=this.db.prepare(`
31
23
  DELETE FROM quest_claims
32
24
  WHERE (julianday('now') - julianday(claimed_at)) * 86400 >= claim_ttl_seconds
33
- `).run();return{presence:e.changes,claims:t.changes}}claimQuest(e,t,n=1800){this.reap();try{const a=s().run_id;return this.db.prepare(`
25
+ `).run();return{presence:a.changes,claims:s.changes}}claimQuest(e,t,n=1800){this.reap();try{const a=c().run_id;return this.db.prepare(`
34
26
  INSERT INTO quest_claims (quest_id, agent_id, claim_ttl_seconds, run_id, role)
35
27
  VALUES (?, ?, ?, ?, ?)
36
28
  `).run(e,t,n,a,this.membershipRole(a,t)),this.getClaim(e)}catch{return null}}releaseRunClaims(e){return this.db.prepare("DELETE FROM quest_claims WHERE run_id = ?").run(e).changes}releaseClaim(e,t){return this.db.prepare(`
@@ -48,4 +40,4 @@ import{readFileSync as l}from"node:fs";import{getActor as s}from"./handlers/boun
48
40
  JOIN agent_presence p ON p.agent_id = c.agent_id
49
41
  WHERE q.project_id = ?
50
42
  ORDER BY c.claimed_at DESC
51
- `;return e==null?this.db.prepare(t).all():this.db.prepare(t).all(e)}}export{E as AgentPresence,c as pidAlive,_ as presenceLiveness,i as processStartTime};
43
+ `;return e==null?this.db.prepare(t).all():this.db.prepare(t).all(e)}}export{g as AgentPresence,o as pidAlive,d as presenceLiveness,_ as processStartTime};
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "contract": "wyrm-cloud-memory-v2",
3
- "version": "7.11.0",
3
+ "version": "8.0.0",
4
4
  "derivedFrom": "standard tier MINUS device-local/egress/subprocess (src/cloud-profile.ts)",
5
5
  "count": 19,
6
6
  "tools": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "contract": "wyrm-cloud-memory-v1",
3
- "version": "7.11.0",
3
+ "version": "8.0.0",
4
4
  "tools": [
5
5
  {
6
6
  "name": "wyrm_recall",
package/dist/wyrm-cli.js CHANGED
@@ -42,14 +42,14 @@ import{join as q,dirname as ne}from"path";import{homedir as Te}from"os";import{e
42
42
  `+String(i.value).slice(0,500)),console.log(d.bold("Created: ")+i.created_at);break}case"session":{const i=s.prepare("SELECT * FROM sessions WHERE id = ?").get(t);if(!i){m(`Session ${t} not found`);break}console.log(d.bold("Date: ")+i.date),console.log(d.bold("Objectives: ")+`
43
43
  `+i.objectives),i.completed&&console.log(d.bold("Completed: ")+`
44
44
  `+i.completed),i.notes&&console.log(d.bold("Notes: ")+`
45
- `+i.notes);break}default:m(`Unknown type prefix: ${r}. Use mem|quest|truth|data|session`)}n.close()}async function Je(l){const{positional:a,flags:e}=C(l),r=a[0];r||(m('Usage: wyrm capture "<content>" [--project <name>] [--mode auto|quest|truth|memory]'),process.exit(1));const o=e.project,t=e.mode,n=_(),s=n.getDatabase();let i=null;if(o){const k=x(n,o);k||(m(`Project not found: ${o}`),n.close(),process.exit(1)),i=k.id}let p=We(r);t&&t!=="auto"&&(p={type:t,subtype:{quest:"quest",truth:"decision",memory:"pattern"}[t]??t,confidence:100,reasoning:`Mode override: ${t}`});const{type:c,subtype:f,confidence:g,reasoning:u}=p;(c==="quest"||c==="truth"||c==="memory")&&i===null&&(m('A --project is required to capture. Use: wyrm capture "<text>" --project <name>'),n.close(),process.exit(1));let h=0,y="",w=!1;const $=new G(s),R=new pe(s);if(me(R,new ue(s)),c==="quest")h=n.addQuest(i,r.slice(0,200),"","medium").id,y="quest";else if(c==="truth")t!=="truth"&&g<100?(h=$.add(i,{kind:"pattern",problem:r,confidence:g/100,needsReview:1}).id,y="mem",w=!0):(h=R.set(i,{category:"decision",key:r.slice(0,60),value:r}).id,y="truth");else{const k=g>=75;h=$.add(i,{kind:f,problem:r,confidence:g/100,needsReview:k?0:1}).id,y="mem",k||(w=!0)}const{recordWrite:j}=await import("./receipts.js"),E=j(s,{tool:"wyrm_capture",outcome:w?"queued":"stored",refTable:y,refId:h,reason:w?`needs_review (classifier confidence ${g}%) \u2014 invisible to recall until reviewed`:void 0,source:"cli",projectId:i??void 0});n.close(),b(`Captured as ${c}: ${f}`),console.log(`${d.dim("Confidence:")} ${g}% | ${d.dim(u)}`),console.log(`${d.dim("ID:")} ${y}:${h}`),w&&console.log(`${Y.warning} Stored for review \u2014 run ${d.cyan("wyrm review")} to activate`),console.log(`receipt: ${E.outcome} ${E.ref??""}${E.reason?` \u2014 ${E.reason}`:""}`)}async function ze(l){const{flags:a}=C(l);if(a.writes!==!0){m("Usage: wyrm digest --writes [--since <days>] [--json]"),process.exitCode=1;return}const e=Number(a.since)>=1?Number(a.since):1,{buildWriteDigest:r}=await import("./receipts.js"),o=_();try{const t=r(o.getDatabase(),e);if(a.json===!0){process.stdout.write(JSON.stringify(t)+`
45
+ `+i.notes);break}default:m(`Unknown type prefix: ${r}. Use mem|quest|truth|data|session`)}n.close()}async function Je(l){const{positional:a,flags:e}=C(l),r=a[0];r||(m('Usage: wyrm capture "<content>" [--project <name>] [--mode auto|quest|truth|memory]'),process.exit(1));const o=e.project,t=e.mode,n=_(),s=n.getDatabase();let i=null;if(o){const k=x(n,o);k||(m(`Project not found: ${o}`),n.close(),process.exit(1)),i=k.id}let p=We(r);t&&t!=="auto"&&(p={type:t,subtype:{quest:"quest",truth:"decision",memory:"pattern"}[t]??t,confidence:100,reasoning:`Mode override: ${t}`});const{type:c,subtype:f,confidence:g,reasoning:u}=p;(c==="quest"||c==="truth"||c==="memory")&&i===null&&(m('A --project is required to capture. Use: wyrm capture "<text>" --project <name>'),n.close(),process.exit(1));let h=0,y="",w=!1;const $=new G(s),R=new pe(s);if(me(R,new ue(s)),c==="quest")h=n.addQuest(i,r.slice(0,200),"","medium").id,y="quest";else if(c==="truth")t!=="truth"&&g<100?(h=$.add(i,{kind:"pattern",problem:r,confidence:g/100,needsReview:1}).id,y="mem",w=!0):(h=R.set(i,{category:"decision",key:r.slice(0,60),value:r}).id,y="truth");else{const k=g>=75;h=$.add(i,{kind:f,problem:r,confidence:g/100,needsReview:k?0:1}).id,y="mem",k||(w=!0)}const{recordWrite:j}=await import("./receipts.js"),E=j(s,{tool:"wyrm_capture",outcome:w?"queued":"stored",refTable:y,refId:h,reason:w?`needs_review (confidence ${g}%) \u2014 review to activate`:void 0,source:"cli",projectId:i??void 0});n.close(),b(`Captured as ${c}: ${f}`),console.log(`${d.dim("Confidence:")} ${g}% | ${d.dim(u)}`),console.log(`${d.dim("ID:")} ${y}:${h}`),w&&console.log(`${Y.warning} Stored for review \u2014 run ${d.cyan("wyrm review")} to activate`),console.log(`receipt: ${E.outcome} ${E.ref??""}${E.reason?` \u2014 ${E.reason}`:""}`)}async function ze(l){const{flags:a}=C(l);if(a.writes!==!0){m("Usage: wyrm digest --writes [--since <days>] [--json]"),process.exitCode=1;return}const e=Number(a.since)>=1?Number(a.since):1,{buildWriteDigest:r}=await import("./receipts.js"),o=_();try{const t=r(o.getDatabase(),e);if(a.json===!0){process.stdout.write(JSON.stringify(t)+`
46
46
  `);return}if(S(`Write digest \u2014 last ${t.since}`),console.log(`${d.bold("Total writes:")} ${t.total} ${d.bold("Outcomes:")} ${Object.entries(t.byOutcome).map(([n,s])=>`${n} ${s}`).join(" \xB7 ")||"none"}`),console.log(`${d.bold("Review queue:")} ${t.reviewQueueDepth} awaiting review${t.reviewQueueDepth>0?d.dim(" (invisible to recall until approved)"):""}`),t.byTool.length>0&&console.log(P(["Tool","Outcome","N"],t.byTool.map(n=>[n.tool,n.outcome,String(n.n)]))),t.reasons.length>0){console.log(d.bold("Non-stored reasons:"));for(const n of t.reasons)console.log(` ${n.outcome} \xD7${n.n} \u2014 ${n.reason.slice(0,110)}`)}t.bySource.length>0&&console.log(d.bold("By source: ")+t.bySource.map(n=>`${n.source} ${n.n}`).join(" \xB7 "))}finally{o.close()}}async function Qe(l){const{flags:a}=C(l),e=T(a.session,0),r=a.path,o=a.project,t=T(a["max-chars"],6e3),n=a.quiet===!0,s=console.log;console.log=()=>{};const i=_();try{const p=new Me(i.getDatabase());let c=e;if(c<=0){let u=r?i.getProject(r):void 0;if(!u&&o&&(u=x(i,o)??void 0),u||(u=i.getProject(process.cwd())),!u){n||process.stderr.write(`wyrm rehydrate: no Wyrm project for this directory, nothing to restore.
47
47
  `);return}const h=i.getRecentSessions(u.id,1);if(h.length===0){n||process.stderr.write(`wyrm rehydrate: project "${u.name}" has no prior sessions yet.
48
48
  `);return}c=h[0].id}const f=p.rehydrate(c);if(!f){n||process.stderr.write(`wyrm rehydrate: session ${c} not found.
49
49
  `);return}let g=f.briefing_markdown;t>0&&g.length>t&&(g=g.slice(0,t)+`
50
50
 
51
51
  _... brief truncated at ${t} chars, run \`wyrm show session:${f.session_id}\` for the full record._`),process.stdout.write(g+`
52
- `)}finally{i.close(),console.log=s}}async function Ze(l){const{flags:a}=C(l),{runMetabolize:e}=await import("./metabolize.js"),r=_(),o=console.log;try{let t;if(typeof a.project=="string"){const s=x(r,a.project);if(!s){m(`Project not found: ${a.project}`),process.exitCode=1;return}t=s.id}const n=e(r.getDatabase(),{projectId:t,dryRun:a["dry-run"]===!0,near:a["no-near"]===!0?!1:void 0});console.log(`metabolize${n.dryRun?" (dry-run)":""} \u2014 scanned ${n.scanned} \xB7 exact-merged ${n.exactMerged} (superseded, not deleted) \xB7 near-dup candidates queued ${n.nearCandidatesQueued} \xB7 decayed ${n.decayed}`)}finally{r.close(),console.log=o}}async function Xe(l){const{positional:a,flags:e}=C(l),r=a[0],o=await import("./bridge.js");if(r==="init"){const{existsSync:t,writeFileSync:n}=await import("node:fs"),s=o.configPath();if(t(s)&&e.force!==!0){m(`${s} already exists (use --force to overwrite).`),process.exitCode=1;return}const i=(await import("node:os")).homedir(),p={sources:[{type:"remember",name:"remember",dir:`${i}/.remember`,projectPath:i,windowDays:7}]};n(s,JSON.stringify(p,null,2)),b(`Wrote ${s} (1 source: remember). Add render/reverse sources per project as needed.`);return}if(r==="status"){const t=o.loadConfig();if(!t){m(`No bridge config at ${o.configPath()} \u2014 run \`wyrm bridge init\`.`),process.exitCode=1;return}const n=o.loadState();S("Bridge sources");for(const s of t.sources){const i=Object.keys(n).filter(c=>c.startsWith(s.name+":")),p=i.reduce((c,f)=>c+n[f].length,0);console.log(` ${s.name} [${s.type}] \u2192 ${"projectPath"in s?s.projectPath:""}${s.type==="remember"?` (${i.length} files tracked, ${p} sections bridged)`:""}`)}return}if(r==="run"){const t=o.loadConfig();if(!t){m(`No bridge config at ${o.configPath()} \u2014 run \`wyrm bridge init\`.`),process.exitCode=1;return}const n=_(),s=console.log;try{const i={wyrm_version:M().version??"unknown",compiled_at:new Date().toISOString()},p=await o.runBridge(n.getDatabase(),t,i,{only:typeof e.source=="string"?e.source:void 0,dryRun:e["dry-run"]===!0});for(const c of p)console.log(`bridge:${c.source} [${c.type}]${e["dry-run"]===!0?" (dry-run)":""} \u2014 stored ${c.stored} \xB7 queued ${c.queued} \xB7 dropped ${c.dropped} \xB7 rendered ${c.rendered}${c.note?` \xB7 ${c.note}`:""}`);return}finally{n.close(),console.log=s}}m("Usage: wyrm bridge <init|status|run> [--source <name>] [--dry-run] [--force]"),process.exitCode=1}async function et(l){const{positional:a,flags:e}=C(l);if(a[0]!=="log"){m("Usage: wyrm session log [--path <dir>] [--run <id>] [--objectives <t>] [--completed <t>] [--issues <t>] [--commits <t>] [--notes <t>]"),process.exitCode=1;return}const o=c=>typeof e[c]=="string"?e[c]:void 0,t=o("path"),n=o("project"),s=o("run")?.slice(0,64),i=console.log;console.log=()=>{};const p=_();try{let c=t?p.getProject(t):void 0;if(!c&&n&&(c=x(p,n)??void 0),c||(c=p.getProject(process.cwd())),!c){process.stderr.write(`wyrm session log: no Wyrm project for this directory, nothing recorded.
52
+ `)}finally{i.close(),console.log=s}}async function Ze(l){const{flags:a}=C(l),{runMetabolize:e}=await import("./metabolize.js"),r=_(),o=console.log;try{let t;if(typeof a.project=="string"){const s=x(r,a.project);if(!s){m(`Project not found: ${a.project}`),process.exitCode=1;return}t=s.id}const n=e(r.getDatabase(),{projectId:t,dryRun:a["dry-run"]===!0,near:a["no-near"]===!0?!1:void 0});console.log(`metabolize${n.dryRun?" (dry-run)":""} \u2014 scanned ${n.scanned} \xB7 exact-merged ${n.exactMerged} (superseded, not deleted) \xB7 near-dup candidates queued ${n.nearCandidatesQueued} \xB7 decayed ${n.decayed}`)}finally{r.close(),console.log=o}}async function Xe(l){const{positional:a,flags:e}=C(l),r=a[0],o=await import("./bridge.js");if(r==="init"){const{existsSync:t,writeFileSync:n}=await import("node:fs"),s=o.configPath();if(t(s)&&e.force!==!0){m(`${s} already exists (use --force to overwrite).`),process.exitCode=1;return}const i=(await import("node:os")).homedir(),p={sources:[{type:"remember",name:"remember",dir:`${i}/.remember`,projectPath:i,windowDays:7}]};n(s,JSON.stringify(p,null,2)),b(`Wrote ${s} (1 source: remember). Add render/reverse sources per project as needed.`);return}if(r==="status"){const t=o.loadConfig();if(!t){m(`No bridge config at ${o.configPath()} \u2014 run \`wyrm bridge init\`.`),process.exitCode=1;return}const n=o.loadState();S("Bridge sources");for(const s of t.sources){const i=Object.keys(n).filter(c=>c.startsWith(s.name+":")),p=i.reduce((c,f)=>c+n[f].length,0);console.log(` ${s.name} [${s.type}] \u2192 ${"projectPath"in s?s.projectPath:""}${s.type==="remember"?` (${i.length} files tracked, ${p} sections bridged)`:""}`)}return}if(r==="run"){const t=o.loadConfig();if(!t){m(`No bridge config at ${o.configPath()} \u2014 run \`wyrm bridge init\`.`),process.exitCode=1;return}const n=_(),s=console.log;try{const i={wyrm_version:M().version??"unknown",compiled_at:new Date().toISOString()},p=await o.runBridge(n.getDatabase(),t,i,{only:typeof e.source=="string"?e.source:void 0,dryRun:e["dry-run"]===!0});for(const c of p)console.log(`bridge:${c.source} [${c.type}]${e["dry-run"]===!0?" (dry-run)":""} \u2014 stored ${c.stored} \xB7 queued ${c.queued} \xB7 dropped ${c.dropped} \xB7 rendered ${c.rendered}${c.note?` \xB7 ${c.note}`:""}`);if(e.watch===!0){const c=Number(e.interval)>=2?Number(e.interval):15;console.log(`bridge watch \u2014 re-rendering on write events (poll ${c}s, Ctrl-C to stop)`),await o.watchBridge(n.getDatabase(),t,()=>({wyrm_version:M().version??"unknown",compiled_at:new Date().toISOString()}),{intervalSec:c,onCycle:f=>{for(const g of f)console.log(`bridge:${g.source} re-rendered (${g.rendered} file(s), ${g.queued} edit(s) harvested)`)}})}return}finally{n.close(),console.log=s}}m("Usage: wyrm bridge <init|status|run> [--source <name>] [--dry-run] [--force]"),process.exitCode=1}async function et(l){const{positional:a,flags:e}=C(l);if(a[0]!=="log"){m("Usage: wyrm session log [--path <dir>] [--run <id>] [--objectives <t>] [--completed <t>] [--issues <t>] [--commits <t>] [--notes <t>]"),process.exitCode=1;return}const o=c=>typeof e[c]=="string"?e[c]:void 0,t=o("path"),n=o("project"),s=o("run")?.slice(0,64),i=console.log;console.log=()=>{};const p=_();try{let c=t?p.getProject(t):void 0;if(!c&&n&&(c=x(p,n)??void 0),c||(c=p.getProject(process.cwd())),!c){process.stderr.write(`wyrm session log: no Wyrm project for this directory, nothing recorded.
53
53
  `);return}const f={};for(const h of["objectives","completed","issues","commits","notes"]){const y=o(h);y&&(f[h]=y.slice(0,8e3))}const g=p.getDatabase();let u;s&&(u=g.prepare("SELECT id FROM sessions WHERE project_id = ? AND run_id = ? ORDER BY id DESC LIMIT 1").get(c.id,s)?.id),u?p.updateSession(u,f):(u=p.createSession(c.id,f).id,s&&g.prepare("UPDATE sessions SET run_id = ? WHERE id = ?").run(s,u)),process.stdout.write(`session:${u}
54
54
  `)}finally{p.close(),console.log=i}}async function tt(l){const{positional:a,flags:e}=C(l),r=a[0],o=p=>typeof e[p]=="string"?e[p]:void 0,{AgentPresence:t,processStartTime:n,presenceLiveness:s}=await import("./presence.js"),i=_();try{const p=new t(i.getDatabase());if(r==="announce"){const c=o("agent");if(!c){m("Usage: wyrm presence announce --agent <id> [--kind <kind>] [--project <name>|--path <dir>] [--quest <n>] [--pid <n>|--auto-pid] [--ttl <sec>] [--role <r>]"),process.exitCode=1;return}const f=o("path");let g=f?i.getProject(f):void 0;!g&&o("project")&&(g=x(i,o("project"))??void 0);const u=e["auto-pid"]===!0?process.ppid:Number(o("pid")),h={};let y="";if(Number.isInteger(u)&&u>0){const R=n(u);R?(h.pid=u,h.pid_start=R,y=` \xB7 pid ${u} (start ${R})`):y=` \xB7 pid ${u} unreadable \u2014 TTL fallback`}const w=Number(o("ttl")),$=p.announce({agent_id:c,agent_kind:o("kind")??"cli",project_id:g?.id??null,current_quest_id:o("quest")?Number(o("quest")):null,ttl_seconds:Number.isFinite(w)&&w>0?w:h.pid?86400:300,metadata:Object.keys(h).length?h:void 0,role:o("role")??null});process.stdout.write(`presence:${$.id} ${c}${y}
55
55
  `);return}if(r==="list"||r==="ls"||r===void 0){const c=i.getDatabase().prepare("SELECT * FROM agent_presence ORDER BY last_heartbeat DESC").all();if(c.length===0){process.stdout.write(`No agents on the board.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "contract": "wyrm-cloud-memory-v1",
3
- "version": "7.11.0",
3
+ "version": "8.0.0",
4
4
  "generatedBy": "scripts/gen-tool-manifest.mjs (from the dist/ registry modules — never hand-edited)",
5
5
  "tiers": {
6
6
  "core": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wyrm-mcp",
3
- "version": "7.11.0",
3
+ "version": "8.0.0",
4
4
  "mcpName": "lk.ghosts/wyrm",
5
5
  "description": "Local-first persistent memory for AI agents over MCP. Ground truths, negative learning (recorded failures block repeats), decision causality, hybrid recall, live memory streams, run-attributed fleet memory — a structured SQLite memory on your machine, no cloud or LLM required. Claude / Copilot / Cursor / Windsurf / Codex.",
6
6
  "type": "module",